diff --git a/lib/hanami/rspec.rb b/lib/hanami/rspec.rb index febd5b6..3e0acbe 100644 --- a/lib/hanami/rspec.rb +++ b/lib/hanami/rspec.rb @@ -35,6 +35,7 @@ def self.gem_loader Hanami::CLI.after "install", Commands::Install Hanami::CLI.after "generate slice", Commands::Generate::Slice Hanami::CLI.after "generate action", Commands::Generate::Action + Hanami::CLI.after "generate part", Commands::Generate::Part end end end diff --git a/lib/hanami/rspec/commands.rb b/lib/hanami/rspec/commands.rb index e04876f..5dc7461 100644 --- a/lib/hanami/rspec/commands.rb +++ b/lib/hanami/rspec/commands.rb @@ -99,6 +99,21 @@ def call(options, **) generator.call(app.namespace, slice, controller, action) end end + + # @since 2.1.0 + # @api private + class Part < Hanami::CLI::Commands::App::Command + # @since 2.1.0 + # @api private + def call(options, **) + # FIXME: dry-cli kwargs aren't correctly forwarded in Ruby 3 + slice = inflector.underscore(Shellwords.shellescape(options[:slice])) if options[:slice] + name = inflector.underscore(Shellwords.shellescape(options[:name])) + + generator = Generators::Part.new(fs: fs, inflector: inflector) + generator.call(app.namespace, slice, name) + end + end end end end diff --git a/lib/hanami/rspec/generators/part.rb b/lib/hanami/rspec/generators/part.rb new file mode 100644 index 0000000..b8e5bf3 --- /dev/null +++ b/lib/hanami/rspec/generators/part.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require "erb" + +module Hanami + module RSpec + module Generators + # @since 2.1.0 + # @api private + class Part + # @since 2.1.0 + # @api private + def initialize(fs:, inflector:) + @fs = fs + @inflector = inflector + end + + # @since 2.1.0 + # @api private + def call(app, slice, name, context: Hanami::CLI::Generators::App::PartContext.new(inflector, app, slice, name)) + if slice + generate_for_slice(slice, context) + else + generate_for_app(context) + end + end + + private + + # @since 2.1.0 + # @api private + def generate_for_slice(slice, context) + generate_base_part_for_app(context) + generate_base_part_for_slice(context, slice) + + fs.write( + "spec/slices/#{slice}/views/parts/#{context.underscored_name}_spec.rb", + t("part_slice_spec.erb", context) + ) + end + + # @since 2.1.0 + # @api private + def generate_for_app(context) + generate_base_part_for_app(context) + + fs.write( + "spec/views/parts/#{context.underscored_name}_spec.rb", + t("part_spec.erb", context) + ) + end + + # @since 2.1.0 + # @api private + def generate_base_part_for_app(context) + path = fs.join("spec", "views", "part_spec.rb") + return if fs.exist?(path) + + fs.write( + path, + t("part_base_spec.erb", context) + ) + end + + # @since 2.1.0 + # @api private + def generate_base_part_for_slice(context, slice) + path = "spec/slices/#{slice}/views/part_spec.rb" + return if fs.exist?(path) + + fs.write( + path, + t("part_slice_base_spec.erb", context) + ) + end + + # @since 2.1.0 + # @api private + attr_reader :fs + + # @since 2.1.0 + # @api private + attr_reader :inflector + + # @since 2.1.0 + # @api private + def template(path, context) + require "erb" + + ERB.new( + File.read(__dir__ + "/part/#{path}"), + trim_mode: "-" + ).result(context.ctx) + end + + # @since 2.1.0 + # @api private + alias_method :t, :template + end + end + end +end diff --git a/lib/hanami/rspec/generators/part/part_base_spec.erb b/lib/hanami/rspec/generators/part/part_base_spec.erb new file mode 100644 index 0000000..89eff7f --- /dev/null +++ b/lib/hanami/rspec/generators/part/part_base_spec.erb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.describe <%= camelized_app_name %>::Views::Part do +<%- if ruby_omit_hash_values? -%> + subject { described_class.new(value:) } +<%- else -%> + subject { described_class.new(value: value) } +<%- end -%> + let(:value) { double("value") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end +end diff --git a/lib/hanami/rspec/generators/part/part_slice_base_spec.erb b/lib/hanami/rspec/generators/part/part_slice_base_spec.erb new file mode 100644 index 0000000..ceb8716 --- /dev/null +++ b/lib/hanami/rspec/generators/part/part_slice_base_spec.erb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.describe <%= camelized_slice_name %>::Views::Part do +<%- if ruby_omit_hash_values? -%> + subject { described_class.new(value:) } +<%- else -%> + subject { described_class.new(value: value) } +<%- end -%> + let(:value) { double("value") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end +end diff --git a/lib/hanami/rspec/generators/part/part_slice_spec.erb b/lib/hanami/rspec/generators/part/part_slice_spec.erb new file mode 100644 index 0000000..cb93d7c --- /dev/null +++ b/lib/hanami/rspec/generators/part/part_slice_spec.erb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.describe <%= camelized_slice_name %>::Views::Parts::<%= camelized_name %> do +<%- if ruby_omit_hash_values? -%> + subject { described_class.new(value:) } +<%- else -%> + subject { described_class.new(value: value) } +<%- end -%> + let(:value) { double("<%= underscored_name %>") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end +end diff --git a/lib/hanami/rspec/generators/part/part_spec.erb b/lib/hanami/rspec/generators/part/part_spec.erb new file mode 100644 index 0000000..8bd449c --- /dev/null +++ b/lib/hanami/rspec/generators/part/part_spec.erb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.describe <%= camelized_app_name %>::Views::Parts::<%= camelized_name %> do +<%- if ruby_omit_hash_values? -%> + subject { described_class.new(value:) } +<%- else -%> + subject { described_class.new(value: value) } +<%- end -%> + let(:value) { double("<%= underscored_name %>") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a1aa0a8..c2d14ff 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true require "hanami/rspec" +require "fileutils" + +TMP = File.join(Dir.pwd, "tmp") RSpec.configure do |config| config.expect_with :rspec do |expectations| @@ -11,6 +14,10 @@ mocks.verify_partial_doubles = true end + config.after do + FileUtils.rm_rf(TMP) if File.directory?(TMP) + end + config.shared_context_metadata_behavior = :apply_to_host_groups config.filter_run_when_matching :focus @@ -28,5 +35,3 @@ Kernel.srand config.seed end - -TMP = File.join(Dir.pwd, "tmp") diff --git a/spec/unit/hanami/rspec/commands/generate/part_spec.rb b/spec/unit/hanami/rspec/commands/generate/part_spec.rb new file mode 100644 index 0000000..4e007ed --- /dev/null +++ b/spec/unit/hanami/rspec/commands/generate/part_spec.rb @@ -0,0 +1,310 @@ +# frozen_string_literal: true + +require "hanami" +require "securerandom" + +RSpec.describe Hanami::RSpec::Commands::Generate::Part do + describe "#call" do + subject { described_class.new(fs: fs, inflector: inflector) } + + let(:fs) { Dry::Files.new } + let(:inflector) { Dry::Inflector.new } + + let(:app_name) { "Bookshelf" } + + let(:part_name) { "client" } + + context "app" do + context "without base part" do + it "generates spec file" do + within_application_directory do + subject.call({name: part_name}) + + if ruby_omit_hash_values? + base_part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{app_name}::Views::Part do + subject { described_class.new(value:) } + let(:value) { double("value") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + expect(fs.read("spec/views/part_spec.rb")).to eq(base_part_spec) + + part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{app_name}::Views::Parts::Client do + subject { described_class.new(value:) } + let(:value) { double("client") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + expect(fs.read("spec/views/parts/client_spec.rb")).to eq(part_spec) + end + + unless ruby_omit_hash_values? + base_part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{app_name}::Views::Part do + subject { described_class.new(value: value) } + let(:value) { double("value") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + expect(fs.read("spec/views/part_spec.rb")).to eq(base_part_spec) + + part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{app_name}::Views::Parts::Client do + subject { described_class.new(value: value) } + let(:value) { double("client") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + expect(fs.read("spec/views/parts/client_spec.rb")).to eq(part_spec) + end + end + end + end + + context "with base part" do + it "generates spec file" do + within_application_directory do + fs.touch("spec/views/part_spec.rb") + + subject.call({name: part_name}) + + if ruby_omit_hash_values? + <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{app_name}::Views::Parts::Client do + subject { described_class.new(value:) } + let(:value) { double("client") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + else + part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{app_name}::Views::Parts::Client do + subject { described_class.new(value: value) } + let(:value) { double("client") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + + expect(fs.read("spec/views/parts/client_spec.rb")).to eq(part_spec) + end + end + end + end + end + + context "slice" do + let(:slice) { "main" } + let(:slice_name) { "Main" } + + context "without base part" do + it "generates spec file" do + within_application_directory do + subject.call({slice: slice, name: part_name}) + + if ruby_omit_hash_values? + base_part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{app_name}::Views::Part do + subject { described_class.new(value:) } + let(:value) { double("value") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + expect(fs.read("spec/views/part_spec.rb")).to eq(base_part_spec) + + base_slice_part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{slice_name}::Views::Part do + subject { described_class.new(value:) } + let(:value) { double("value") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + expect(fs.read("spec/slices/#{slice}/views/part_spec.rb")).to eq(base_slice_part_spec) + + part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{slice_name}::Views::Parts::Client do + subject { described_class.new(value:) } + let(:value) { double("client") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + expect(fs.read("spec/slices/#{slice}/views/parts/client_spec.rb")).to eq(part_spec) + end + + unless ruby_omit_hash_values? + base_part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{app_name}::Views::Part do + subject { described_class.new(value: value) } + let(:value) { double("value") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + expect(fs.read("spec/views/part_spec.rb")).to eq(base_part_spec) + + base_slice_part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{slice_name}::Views::Part do + subject { described_class.new(value: value) } + let(:value) { double("value") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + expect(fs.read("spec/slices/#{slice}/views/part_spec.rb")).to eq(base_slice_part_spec) + + part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{slice_name}::Views::Parts::Client do + subject { described_class.new(value: value) } + let(:value) { double("client") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + expect(fs.read("spec/slices/#{slice}/views/parts/client_spec.rb")).to eq(part_spec) + end + end + end + end + + context "with base part" do + it "generates spec file" do + within_application_directory do + fs.touch("spec/views/part_spec.rb") + fs.touch("spec/slices/#{slice}/views/part_spec.rb") + + subject.call({slice: slice, name: part_name}) + + if ruby_omit_hash_values? + part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{slice_name}::Views::Parts::Client do + subject { described_class.new(value:) } + let(:value) { double("client") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + else + part_spec = <<~EXPECTED + # frozen_string_literal: true + + RSpec.describe #{slice_name}::Views::Parts::Client do + subject { described_class.new(value: value) } + let(:value) { double("client") } + + it "works" do + expect(subject).to be_kind_of(described_class) + end + end + EXPECTED + end + expect(fs.read("spec/slices/#{slice}/views/parts/client_spec.rb")).to eq(part_spec) + end + end + end + end + end + + private + + def within_application_directory(app: app_name) + dir = fs.join(TMP, SecureRandom.uuid, app) + + fs.mkdir(dir) + fs.chdir(dir) do + app_code = <<~CODE + # frozen_string_literal: true + + require "hanami" + + module #{app} + class App < Hanami::App + end + end + CODE + fs.write("config/app.rb", app_code) + + routes = <<~CODE + # frozen_string_literal: true + + require "hanami/routes" + + module #{app} + class Routes < Hanami::Routes + define do + root { "Hello from Hanami" } + end + end + end + CODE + fs.write("config/routes.rb", routes) + + yield + end + end + + def ruby_omit_hash_values? + RUBY_VERSION >= "3.1" + end +end