sun-sword 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -2
  3. data/Gemfile +8 -0
  4. data/Gemfile.lock +114 -55
  5. data/README.md +35 -13
  6. data/Rakefile +6 -10
  7. data/lib/generators/sun_sword/USAGE +22 -3
  8. data/lib/generators/sun_sword/frontend_generator.rb +77 -39
  9. data/lib/generators/sun_sword/frontend_generator_spec.rb +539 -0
  10. data/lib/generators/sun_sword/init_generator.rb +2 -0
  11. data/lib/generators/sun_sword/init_generator_spec.rb +82 -0
  12. data/lib/generators/sun_sword/scaffold_generator.rb +189 -24
  13. data/lib/generators/sun_sword/scaffold_generator_spec.rb +1414 -0
  14. data/lib/generators/sun_sword/templates_frontend/{Procfile.dev → Procfile.dev.tt} +1 -0
  15. data/lib/generators/sun_sword/templates_frontend/bin/{watch → watch.tt} +2 -1
  16. data/lib/generators/sun_sword/templates_frontend/config/{vite.json → vite.json.tt} +2 -1
  17. data/lib/generators/sun_sword/templates_frontend/controllers/application_controller.rb.tt +2 -2
  18. data/lib/generators/sun_sword/templates_frontend/controllers/tests_controller.rb +36 -0
  19. data/lib/generators/sun_sword/templates_frontend/controllers/tests_controller_spec.rb +62 -0
  20. data/lib/generators/sun_sword/templates_frontend/env.development +1 -1
  21. data/lib/generators/sun_sword/templates_frontend/frontend/entrypoints/application.js +2 -2
  22. data/lib/generators/sun_sword/templates_frontend/frontend/pages/tests-stimulus.js +31 -0
  23. data/lib/generators/sun_sword/templates_frontend/frontend/pages/web.js +12 -0
  24. data/lib/generators/sun_sword/templates_frontend/frontend/stylesheets/application.css +1 -3
  25. data/lib/generators/sun_sword/templates_frontend/helpers/application_helper.rb +17 -0
  26. data/lib/generators/sun_sword/templates_frontend/helpers/application_helper_spec.rb +30 -0
  27. data/lib/generators/sun_sword/templates_frontend/package.json.tt +14 -0
  28. data/lib/generators/sun_sword/templates_frontend/views/components/_action_destroy.html.erb.tt +1 -1
  29. data/lib/generators/sun_sword/templates_frontend/views/components/_alert.html.erb.tt +1 -1
  30. data/lib/generators/sun_sword/templates_frontend/views/layouts/application.html.erb.tt +3 -3
  31. data/lib/generators/sun_sword/templates_frontend/views/tests/_frame_content.html.erb +9 -0
  32. data/lib/generators/sun_sword/templates_frontend/views/tests/_log_entry.html.erb +4 -0
  33. data/lib/generators/sun_sword/templates_frontend/views/tests/_updated_content.html.erb +6 -0
  34. data/lib/generators/sun_sword/templates_frontend/views/tests/stimulus.html.erb +45 -0
  35. data/lib/generators/sun_sword/templates_frontend/views/tests/turbo_drive.html.erb +55 -0
  36. data/lib/generators/sun_sword/templates_frontend/views/tests/turbo_frame.html.erb +87 -0
  37. data/lib/generators/sun_sword/templates_frontend/vite.config.ts.tt +1 -1
  38. data/lib/generators/sun_sword/templates_init/config/initializers/sun_sword.rb +1 -0
  39. data/lib/generators/sun_sword/templates_scaffold/controllers/controller.rb.tt +24 -24
  40. data/lib/generators/sun_sword/templates_scaffold/controllers/controller_spec.rb.tt +398 -0
  41. data/lib/generators/sun_sword/templates_scaffold/views/index.html.erb.tt +5 -5
  42. data/lib/generators/sun_sword/templates_scaffold/views/show.html.erb.tt +3 -0
  43. data/lib/generators/tmp/db/structures/test_structure.yaml +42 -0
  44. data/lib/sun-sword.rb +1 -0
  45. data/lib/sun_sword/configuration_spec.rb +77 -0
  46. data/lib/sun_sword/version.rb +1 -1
  47. metadata +84 -30
  48. data/lib/generators/sun_sword/templates_frontend/controllers/site_controller.rb +0 -16
  49. data/lib/generators/sun_sword/templates_frontend/db/seeds.rb +0 -3
  50. data/lib/generators/sun_sword/templates_frontend/db/structures/example.yaml.tt +0 -106
  51. data/lib/generators/sun_sword/templates_frontend/frontend/pages/stimulus.js +0 -10
  52. data/lib/generators/sun_sword/templates_frontend/package.json +0 -7
  53. data/lib/generators/sun_sword/templates_frontend/views/site/_comment.html.erb.tt +0 -3
  54. data/lib/generators/sun_sword/templates_frontend/views/site/stimulus.html.erb.tt +0 -26
@@ -0,0 +1,1414 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'ostruct'
5
+ require 'generators/sun_sword/scaffold_generator'
6
+
7
+ RSpec.describe SunSword::ScaffoldGenerator, type: :generator do
8
+ include FileUtils
9
+
10
+ let(:destination_root) { File.expand_path('../tmp', __dir__) }
11
+
12
+ let(:structure_file) { 'test_structure.yaml' }
13
+ let(:structure_content) do
14
+ {
15
+ 'model' => 'TestModel',
16
+ 'resource_name' => 'test_models',
17
+ 'actor' => 'admin',
18
+ 'resource_owner_id' => 'user_id',
19
+ 'entity' => {
20
+ 'skipped_fields' => ['created_at', 'updated_at'],
21
+ 'custom_fields' => []
22
+ },
23
+ 'domains' => {
24
+ 'action_list' => {
25
+ 'use_case' => {
26
+ 'contract' => ['name', 'email']
27
+ }
28
+ },
29
+ 'action_fetch_by_id' => {
30
+ 'use_case' => {
31
+ 'contract' => ['id', 'name', 'email']
32
+ }
33
+ },
34
+ 'action_create' => {
35
+ 'use_case' => {
36
+ 'contract' => ['name', 'email']
37
+ }
38
+ },
39
+ 'action_update' => {
40
+ 'use_case' => {
41
+ 'contract' => ['name', 'email']
42
+ }
43
+ },
44
+ 'action_destroy' => {
45
+ 'use_case' => {
46
+ 'contract' => ['id']
47
+ }
48
+ }
49
+ },
50
+ 'controllers' => {
51
+ 'form_fields' => [
52
+ { 'name' => 'name', 'type' => 'string' },
53
+ { 'name' => 'email', 'type' => 'string' }
54
+ ]
55
+ }
56
+ }.to_yaml
57
+ end
58
+
59
+ before do
60
+ FileUtils.mkdir_p(destination_root) unless Dir.exist?(destination_root)
61
+ # Create the required structure file
62
+ FileUtils.mkdir_p(File.join(destination_root, 'db', 'structures'))
63
+ File.write(File.join(destination_root, 'db', 'structures', structure_file), structure_content)
64
+ end
65
+
66
+ after do
67
+ FileUtils.rm_rf(destination_root) if Dir.exist?(destination_root)
68
+ end
69
+
70
+ describe 'arguments' do
71
+ it 'accepts structure argument' do
72
+ generator = described_class.new(['test'])
73
+ expect(generator.arg_structure).to eq('test')
74
+ end
75
+
76
+ it 'accepts scope argument' do
77
+ generator = described_class.new(['test', 'scope:admin'])
78
+ expect(generator.arg_scope).to eq({ 'scope' => 'admin' })
79
+ end
80
+ end
81
+
82
+ describe 'source root' do
83
+ it 'has the correct source root' do
84
+ source_path = File.expand_path('templates_scaffold', File.dirname(__FILE__))
85
+ expect(described_class.source_root).to eq(source_path)
86
+ end
87
+ end
88
+
89
+ describe '#setup_variables' do
90
+ let(:generator) do
91
+ described_class.new(['test'], {}).tap do |g|
92
+ g.destination_root = destination_root
93
+ end
94
+ end
95
+
96
+ before do
97
+ # Mock the model class
98
+ stub_const('TestModel', Class.new)
99
+ allow(TestModel).to receive(:columns).and_return([
100
+ double(name: 'id', type: :integer),
101
+ double(name: 'name', type: :string),
102
+ double(name: 'email', type: :string),
103
+ double(name: 'created_at', type: :datetime),
104
+ double(name: 'updated_at', type: :datetime)
105
+ ])
106
+ allow(TestModel).to receive(:columns_hash).and_return({
107
+ 'id' => double(type: :integer),
108
+ 'name' => double(type: :string),
109
+ 'email' => double(type: :string)
110
+ })
111
+ end
112
+
113
+ it 'loads structure configuration' do
114
+ Dir.chdir(destination_root) do
115
+ generator.send(:setup_variables)
116
+ expect(generator.instance_variable_get(:@structure)).to be_a(Hashie::Mash)
117
+ end
118
+ end
119
+
120
+ it 'sets actor from structure' do
121
+ Dir.chdir(destination_root) do
122
+ generator.send(:setup_variables)
123
+ expect(generator.instance_variable_get(:@actor)).to eq('admin')
124
+ end
125
+ end
126
+
127
+ it 'sets variable subject' do
128
+ Dir.chdir(destination_root) do
129
+ generator.send(:setup_variables)
130
+ expect(generator.instance_variable_get(:@variable_subject)).to eq('test_model')
131
+ end
132
+ end
133
+
134
+ it 'sets scope path' do
135
+ Dir.chdir(destination_root) do
136
+ generator.send(:setup_variables)
137
+ expect(generator.instance_variable_get(:@scope_path)).to eq('test_models')
138
+ end
139
+ end
140
+ end
141
+
142
+ describe '#build_usecase_filename' do
143
+ let(:generator) do
144
+ described_class.new(['test'], {}).tap do |g|
145
+ g.destination_root = destination_root
146
+ g.instance_variable_set(:@actor, 'admin')
147
+ g.instance_variable_set(:@variable_subject, 'testmodel')
148
+ end
149
+ end
150
+
151
+ it 'builds filename with actor, action and subject' do
152
+ result = generator.send(:build_usecase_filename, 'list')
153
+ expect(result).to eq('AdminListTestmodel')
154
+ end
155
+
156
+ it 'appends suffix when provided' do
157
+ result = generator.send(:build_usecase_filename, 'create', '_contract')
158
+ expect(result).to eq('AdminCreateTestmodelContract')
159
+ end
160
+ end
161
+
162
+ describe '#contract_fields' do
163
+ let(:generator) do
164
+ described_class.new(['test'], {}).tap do |g|
165
+ g.destination_root = destination_root
166
+ g.instance_variable_set(:@model_class, TestModel)
167
+ g.instance_variable_set(:@skipped_fields, ['created_at', 'updated_at'])
168
+ end
169
+ end
170
+
171
+ before do
172
+ stub_const('TestModel', Class.new)
173
+ allow(TestModel).to receive(:columns).and_return([
174
+ double(name: 'id', type: :integer),
175
+ double(name: 'name', type: :string),
176
+ double(name: 'email', type: :string),
177
+ double(name: 'created_at', type: :datetime),
178
+ double(name: 'updated_at', type: :datetime)
179
+ ])
180
+ end
181
+
182
+ it 'returns model columns excluding skipped fields as tuples [name, type]' do
183
+ result = generator.send(:contract_fields)
184
+ expect(result).to contain_exactly(['id', 'integer'], ['name', 'string'], ['email', 'string'])
185
+ end
186
+ end
187
+
188
+ describe '#strong_params' do
189
+ let(:generator) do
190
+ described_class.new(['test'], {}).tap do |g|
191
+ g.destination_root = destination_root
192
+ g.instance_variable_set(:@controllers, Hashie::Mash.new({
193
+ 'form_fields' => [
194
+ { 'name' => 'name', 'type' => 'string' },
195
+ { 'name' => 'email', 'type' => 'string' },
196
+ { 'name' => 'attachments', 'type' => 'files' }
197
+ ]
198
+ }))
199
+ g.instance_variable_set(:@model_class, TestModel)
200
+ end
201
+ end
202
+
203
+ before do
204
+ stub_const('TestModel', Class.new)
205
+ allow(TestModel).to receive(:columns_hash).and_return({
206
+ 'name' => double(type: :string),
207
+ 'email' => double(type: :string),
208
+ 'attachments' => double(type: :files)
209
+ })
210
+ end
211
+
212
+ it 'generates strong params for different field types' do
213
+ result = generator.send(:strong_params)
214
+ expect(result).to include(':name')
215
+ expect(result).to include(':email')
216
+ expect(result).to include('{ attachments: [] }')
217
+ end
218
+ end
219
+
220
+ describe '#namespace_exists?' do
221
+ let(:generator) do
222
+ described_class.new(['test'], {}).tap do |g|
223
+ g.destination_root = destination_root
224
+ g.instance_variable_set(:@route_scope_path, 'admin')
225
+ g.instance_variable_set(:@engine_scope_path, 'admin')
226
+ end
227
+ end
228
+
229
+ before do
230
+ FileUtils.mkdir_p(File.join(destination_root, 'config'))
231
+ end
232
+
233
+ context 'when routes file exists and contains namespace' do
234
+ before do
235
+ routes_content = <<~RUBY
236
+ Rails.application.routes.draw do
237
+ namespace :admin do
238
+ end
239
+ end
240
+ RUBY
241
+ File.write(File.join(destination_root, 'config', 'routes.rb'), routes_content)
242
+ end
243
+
244
+ it 'returns true' do
245
+ Dir.chdir(destination_root) do
246
+ result = generator.send(:namespace_exists?)
247
+ expect(result).to be true
248
+ end
249
+ end
250
+ end
251
+
252
+ context 'when routes file exists but does not contain namespace' do
253
+ before do
254
+ routes_content = <<~RUBY
255
+ Rails.application.routes.draw do
256
+ end
257
+ RUBY
258
+ File.write(File.join(destination_root, 'config', 'routes.rb'), routes_content)
259
+ end
260
+
261
+ it 'returns false' do
262
+ Dir.chdir(destination_root) do
263
+ result = generator.send(:namespace_exists?)
264
+ expect(result).to be false
265
+ end
266
+ end
267
+ end
268
+
269
+ context 'when routes file does not exist' do
270
+ it 'returns false' do
271
+ Dir.chdir(destination_root) do
272
+ result = generator.send(:namespace_exists?)
273
+ expect(result).to be false
274
+ end
275
+ end
276
+ end
277
+ end
278
+
279
+ describe 'engine support' do
280
+ describe '#engine_path' do
281
+ it 'returns nil when no engine option is set' do
282
+ generator = described_class.new(['test'], {})
283
+ expect(generator.send(:engine_path)).to be_nil
284
+ end
285
+
286
+ it 'returns engine path when engine option is set' do
287
+ generator = described_class.new(['test'], { engine: 'admin' })
288
+ allow(generator).to receive(:detect_engine_path).and_return('engines/admin')
289
+
290
+ expect(generator.send(:engine_path)).to eq('engines/admin')
291
+ end
292
+ end
293
+
294
+ describe '#detect_engine_path' do
295
+ let(:generator) { described_class.new(['test'], { engine: 'admin' }) }
296
+
297
+ it 'detects engine in engines/ directory' do
298
+ allow(Dir).to receive(:exist?).and_call_original
299
+ allow(Dir).to receive(:exist?).with('engines/admin').and_return(true)
300
+ allow(File).to receive(:exist?).and_call_original
301
+ allow(File).to receive(:exist?).with('engines/admin/admin.gemspec').and_return(true)
302
+
303
+ expect(generator.send(:detect_engine_path)).to eq('engines/admin')
304
+ end
305
+
306
+ it 'detects engine in components/ directory' do
307
+ allow(Dir).to receive(:exist?).and_call_original
308
+ allow(Dir).to receive(:exist?).with('engines/admin').and_return(false)
309
+ allow(Dir).to receive(:exist?).with('components/admin').and_return(true)
310
+ allow(File).to receive(:exist?).and_call_original
311
+ allow(File).to receive(:exist?).with('components/admin/admin.gemspec').and_return(true)
312
+
313
+ expect(generator.send(:detect_engine_path)).to eq('components/admin')
314
+ end
315
+
316
+ it 'returns nil when engine not found' do
317
+ allow(Dir).to receive(:exist?).and_call_original
318
+ allow(Dir).to receive(:exist?).with(anything).and_return(false)
319
+
320
+ expect(generator.send(:detect_engine_path)).to be_nil
321
+ end
322
+ end
323
+
324
+ describe '#path_app' do
325
+ it 'returns "app" when no engine is set' do
326
+ generator = described_class.new(['test'], {})
327
+ expect(generator.send(:path_app)).to eq('app')
328
+ end
329
+
330
+ it 'returns engine app path when engine is set' do
331
+ generator = described_class.new(['test'], { engine: 'admin' })
332
+ allow(generator).to receive(:engine_path).and_return('engines/admin')
333
+
334
+ expect(generator.send(:path_app)).to eq('engines/admin/app')
335
+ end
336
+ end
337
+
338
+ describe '#structure_file_path' do
339
+ it 'returns default path when no engine options' do
340
+ generator = described_class.new(['test'], {})
341
+ generator.instance_variable_set(:@arg_structure, 'user')
342
+
343
+ expect(generator.send(:structure_file_path)).to eq('db/structures/user_structure.yaml')
344
+ end
345
+
346
+ it 'returns engine path when engine_structure option is set' do
347
+ generator = described_class.new(['test'], { engine_structure: 'core' })
348
+ generator.instance_variable_set(:@arg_structure, 'user')
349
+
350
+ allow(generator).to receive(:detect_structure_engine_path).with('core').and_return('engines/core')
351
+
352
+ expect(generator.send(:structure_file_path)).to eq('engines/core/db/structures/user_structure.yaml')
353
+ end
354
+
355
+ it 'uses engine option as fallback for structure path' do
356
+ generator = described_class.new(['test'], { engine: 'admin' })
357
+ generator.instance_variable_set(:@arg_structure, 'user')
358
+
359
+ allow(generator).to receive(:detect_structure_engine_path).with('admin').and_return('engines/admin')
360
+
361
+ expect(generator.send(:structure_file_path)).to eq('engines/admin/db/structures/user_structure.yaml')
362
+ end
363
+
364
+ it 'raises error when structure engine not found' do
365
+ generator = described_class.new(['test'], { engine_structure: 'nonexistent' })
366
+ generator.instance_variable_set(:@arg_structure, 'user')
367
+
368
+ allow(generator).to receive(:detect_structure_engine_path).with('nonexistent').and_return(nil)
369
+
370
+ expect { generator.send(:structure_file_path) }.to raise_error(Thor::Error, /Structure file not found/)
371
+ end
372
+ end
373
+
374
+ describe '#routes_file_path' do
375
+ it 'returns default path when no engine' do
376
+ generator = described_class.new(['test'], {})
377
+ expect(generator.send(:routes_file_path)).to eq('config/routes.rb')
378
+ end
379
+
380
+ it 'returns engine routes path when engine is set' do
381
+ generator = described_class.new(['test'], { engine: 'admin' })
382
+ allow(generator).to receive(:engine_path).and_return('engines/admin')
383
+
384
+ expect(generator.send(:routes_file_path)).to eq('engines/admin/config/routes.rb')
385
+ end
386
+ end
387
+
388
+ describe '#validate_engine' do
389
+ it 'does nothing when no engine option is set' do
390
+ generator = described_class.new(['test'], {})
391
+ expect { generator.validate_engine }.not_to raise_error
392
+ end
393
+
394
+ it 'raises error when engine does not exist' do
395
+ generator = described_class.new(['test'], { engine: 'nonexistent' })
396
+ allow(generator).to receive(:engine_exists?).and_return(false)
397
+ allow(generator).to receive(:available_engines).and_return(['admin', 'api'])
398
+
399
+ expect { generator.validate_engine }.to raise_error(Thor::Error, /Engine 'nonexistent' not found/)
400
+ end
401
+
402
+ it 'succeeds when engine exists' do
403
+ generator = described_class.new(['test'], { engine: 'admin' })
404
+ allow(generator).to receive(:engine_exists?).and_return(true)
405
+
406
+ expect { generator.validate_engine }.not_to raise_error
407
+ end
408
+ end
409
+
410
+ describe '#detect_structure_engine_path' do
411
+ it 'detects structure directory in engine' do
412
+ generator = described_class.new(['test'], {})
413
+ allow(Dir).to receive(:exist?).and_call_original
414
+ allow(Dir).to receive(:exist?).with('engines/core/db/structures').and_return(true)
415
+
416
+ expect(generator.send(:detect_structure_engine_path, 'core')).to eq('engines/core')
417
+ end
418
+
419
+ it 'returns nil when structure directory not found' do
420
+ generator = described_class.new(['test'], {})
421
+ allow(Dir).to receive(:exist?).and_call_original
422
+ allow(Dir).to receive(:exist?).with(anything).and_return(false)
423
+
424
+ expect(generator.send(:detect_structure_engine_path, 'core')).to be_nil
425
+ end
426
+ end
427
+ end
428
+
429
+ describe 'spec file generation' do
430
+ let(:test_model_class) do
431
+ Class.new do
432
+ def self.name
433
+ 'TestModel'
434
+ end
435
+
436
+ def self.columns
437
+ [
438
+ OpenStruct.new(name: 'id', type: :integer),
439
+ OpenStruct.new(name: 'name', type: :string),
440
+ OpenStruct.new(name: 'email', type: :string)
441
+ ]
442
+ end
443
+
444
+ def self.create!(attrs)
445
+ new
446
+ end
447
+
448
+ def self.new
449
+ Object.new
450
+ end
451
+ end
452
+ end
453
+
454
+ let(:generator) do
455
+ described_class.new(['test'], {}).tap do |g|
456
+ g.destination_root = destination_root
457
+ end
458
+ end
459
+
460
+ before do
461
+ mkdir_p(destination_root)
462
+ mkdir_p(File.join(destination_root, 'db', 'structures'))
463
+ File.write(File.join(destination_root, 'db', 'structures', structure_file), structure_content)
464
+
465
+ # Stub the model class
466
+ stub_const('TestModel', test_model_class)
467
+ end
468
+
469
+ after do
470
+ rm_rf(destination_root) if Dir.exist?(destination_root)
471
+ end
472
+
473
+ describe '#create_spec_files' do
474
+ it 'generates controller spec file in same directory as controller' do
475
+ Dir.chdir(destination_root) do
476
+ generator.send(:setup_variables)
477
+ generator.send(:create_spec_files)
478
+
479
+ controller_spec_path = File.join(destination_root, 'app', 'controllers', '', 'test_models_controller_spec.rb')
480
+ expect(File.exist?(controller_spec_path)).to be true
481
+ end
482
+ end
483
+
484
+ it 'controller spec contains correct class name' do
485
+ Dir.chdir(destination_root) do
486
+ generator.send(:setup_variables)
487
+ generator.send(:create_spec_files)
488
+
489
+ controller_spec_path = File.join(destination_root, 'app', 'controllers', '', 'test_models_controller_spec.rb')
490
+ content = File.read(controller_spec_path)
491
+ expect(content).to include('RSpec.describe TestModelsController')
492
+ end
493
+ end
494
+
495
+ it 'controller spec contains CRUD action tests with use case mocking' do
496
+ Dir.chdir(destination_root) do
497
+ generator.send(:setup_variables)
498
+ generator.send(:create_spec_files)
499
+
500
+ controller_spec_path = File.join(destination_root, 'app', 'controllers', '', 'test_models_controller_spec.rb')
501
+ content = File.read(controller_spec_path)
502
+
503
+ expect(content).to include('GET #index')
504
+ expect(content).to include('GET #show')
505
+ expect(content).to include('GET #new')
506
+ expect(content).to include('GET #edit')
507
+ expect(content).to include('POST #create')
508
+ expect(content).to include('PATCH #update')
509
+ expect(content).to include('DELETE #destroy')
510
+ expect(content).to include('instance_double')
511
+ expect(content).to include('Dry::Monads::Success')
512
+ expect(content).to include('Dry::Monads::Failure')
513
+ end
514
+ end
515
+
516
+ it 'controller spec includes private methods tests' do
517
+ Dir.chdir(destination_root) do
518
+ generator.send(:setup_variables)
519
+ generator.send(:create_spec_files)
520
+
521
+ controller_spec_path = File.join(destination_root, 'app', 'controllers', '', 'test_models_controller_spec.rb')
522
+ content = File.read(controller_spec_path)
523
+
524
+ expect(content).to include('private methods')
525
+ expect(content).to include('#build_contract')
526
+ expect(content).to include('#set_test_model')
527
+ expect(content).to include('#test_model_params')
528
+ end
529
+ end
530
+ end
531
+
532
+ describe 'spec file generation with scope' do
533
+ let(:generator_with_scope) do
534
+ described_class.new(['test', 'scope:admin'], {}).tap do |g|
535
+ g.destination_root = destination_root
536
+ end
537
+ end
538
+
539
+ it 'generates controller spec in correct scope directory' do
540
+ Dir.chdir(destination_root) do
541
+ generator_with_scope.send(:setup_variables)
542
+ generator_with_scope.send(:create_spec_files)
543
+
544
+ controller_spec_path = File.join(destination_root, 'app', 'controllers', 'admin', 'test_models_controller_spec.rb')
545
+ expect(File.exist?(controller_spec_path)).to be true
546
+ end
547
+ end
548
+
549
+ it 'controller spec includes scoped class name and use case paths' do
550
+ Dir.chdir(destination_root) do
551
+ generator_with_scope.send(:setup_variables)
552
+ generator_with_scope.send(:create_spec_files)
553
+
554
+ controller_spec_path = File.join(destination_root, 'app', 'controllers', 'admin', 'test_models_controller_spec.rb')
555
+ content = File.read(controller_spec_path)
556
+ expect(content).to include('RSpec.describe Admin::TestModelsController')
557
+ expect(content).to include('Core::UseCases::Admin')
558
+ end
559
+ end
560
+ end
561
+
562
+ describe 'spec file generation for engine' do
563
+ let(:generator_with_engine) do
564
+ described_class.new(['test'], { engine: 'admin' }).tap do |g|
565
+ g.destination_root = destination_root
566
+ end
567
+ end
568
+
569
+ before do
570
+ mkdir_p(File.join(destination_root, 'engines', 'admin'))
571
+ mkdir_p(File.join(destination_root, 'engines', 'admin', 'db', 'structures'))
572
+ File.write(File.join(destination_root, 'engines', 'admin', 'db', 'structures', structure_file), structure_content)
573
+ mkdir_p(File.join(destination_root, 'db', 'structures'))
574
+ File.write(File.join(destination_root, 'db', 'structures', structure_file), structure_content)
575
+
576
+ stub_const('TestModel', test_model_class)
577
+ allow(generator_with_engine).to receive(:engine_path).and_return('engines/admin')
578
+ allow(generator_with_engine).to receive(:engine_exists?).and_return(true)
579
+ allow(generator_with_engine).to receive(:detect_structure_engine_path).with(any_args).and_return(nil)
580
+ allow(generator_with_engine).to receive(:structure_file_path).and_return(File.join('db', 'structures', structure_file))
581
+ end
582
+
583
+ it 'generates controller spec in engine directory' do
584
+ Dir.chdir(destination_root) do
585
+ generator_with_engine.send(:setup_variables)
586
+ generator_with_engine.send(:create_spec_files)
587
+
588
+ controller_spec_path = File.join(destination_root, 'engines', 'admin', 'app', 'controllers', 'admin', 'test_models_controller_spec.rb')
589
+ expect(File.exist?(controller_spec_path)).to be true
590
+ end
591
+ end
592
+
593
+ it 'controller spec contains use case mocking and factory bot' do
594
+ Dir.chdir(destination_root) do
595
+ generator_with_engine.send(:setup_variables)
596
+ generator_with_engine.send(:create_spec_files)
597
+
598
+ controller_spec_path = File.join(destination_root, 'engines', 'admin', 'app', 'controllers', 'admin', 'test_models_controller_spec.rb')
599
+ content = File.read(controller_spec_path)
600
+ expect(content).to include('instance_double')
601
+ expect(content).to include('create(:')
602
+ expect(content).to include('build(:')
603
+ end
604
+ end
605
+ end
606
+ end
607
+
608
+ describe '#generate_form_fields_html' do
609
+ let(:generator) do
610
+ described_class.new(['test'], {}).tap do |g|
611
+ g.destination_root = destination_root
612
+ g.instance_variable_set(:@variable_subject, 'test_model')
613
+ g.instance_variable_set(:@mapping_fields, {
614
+ string: :text_field,
615
+ text: :text_area,
616
+ integer: :number_field,
617
+ float: :number_field,
618
+ decimal: :number_field,
619
+ boolean: :check_box,
620
+ date: :date_select,
621
+ datetime: :datetime_select,
622
+ timestamp: :datetime_select,
623
+ time: :time_select,
624
+ enum: :select,
625
+ file: :file_field,
626
+ files: :file_fields
627
+ })
628
+ end
629
+ end
630
+
631
+ context 'with text_field (string type)' do
632
+ before do
633
+ generator.instance_variable_set(:@form_fields, [
634
+ { name: 'name', type: 'string' }
635
+ ])
636
+ end
637
+
638
+ it 'generates text_field HTML' do
639
+ result = generator.send(:generate_form_fields_html)
640
+
641
+ expect(result).to include('form.text_field :name')
642
+ expect(result).to include("id: 'test_model_name'")
643
+ expect(result).to include("for: 'test_model_name'")
644
+ end
645
+ end
646
+
647
+ context 'with text_area (text type)' do
648
+ before do
649
+ generator.instance_variable_set(:@form_fields, [
650
+ { name: 'description', type: 'text' }
651
+ ])
652
+ end
653
+
654
+ it 'generates text_area HTML' do
655
+ result = generator.send(:generate_form_fields_html)
656
+
657
+ expect(result).to include('form.text_area :description')
658
+ expect(result).to include("id: 'test_model_description'")
659
+ expect(result).to include('rows: 3')
660
+ end
661
+ end
662
+
663
+ context 'with number_field (integer type)' do
664
+ before do
665
+ generator.instance_variable_set(:@form_fields, [
666
+ { name: 'age', type: 'integer' }
667
+ ])
668
+ end
669
+
670
+ it 'generates number_field HTML' do
671
+ result = generator.send(:generate_form_fields_html)
672
+
673
+ expect(result).to include('form.number_field :age')
674
+ expect(result).to include("id: 'test_model_age'")
675
+ end
676
+ end
677
+
678
+ context 'with number_field (float type)' do
679
+ before do
680
+ generator.instance_variable_set(:@form_fields, [
681
+ { name: 'price', type: 'float' }
682
+ ])
683
+ end
684
+
685
+ it 'generates number_field HTML' do
686
+ result = generator.send(:generate_form_fields_html)
687
+
688
+ expect(result).to include('form.number_field :price')
689
+ end
690
+ end
691
+
692
+ context 'with number_field (decimal type)' do
693
+ before do
694
+ generator.instance_variable_set(:@form_fields, [
695
+ { name: 'amount', type: 'decimal' }
696
+ ])
697
+ end
698
+
699
+ it 'generates number_field HTML' do
700
+ result = generator.send(:generate_form_fields_html)
701
+
702
+ expect(result).to include('form.number_field :amount')
703
+ end
704
+ end
705
+
706
+ context 'with check_box (boolean type)' do
707
+ before do
708
+ generator.instance_variable_set(:@form_fields, [
709
+ { name: 'active', type: 'boolean' }
710
+ ])
711
+ end
712
+
713
+ it 'generates check_box HTML' do
714
+ result = generator.send(:generate_form_fields_html)
715
+
716
+ expect(result).to include('form.check_box :active')
717
+ expect(result).to include("id: 'test_model_active'")
718
+ expect(result).to include('relative flex items-start')
719
+ end
720
+ end
721
+
722
+ context 'with select (enum type)' do
723
+ before do
724
+ generator.instance_variable_set(:@form_fields, [
725
+ { name: 'status', type: 'enum' }
726
+ ])
727
+ end
728
+
729
+ it 'generates select HTML' do
730
+ result = generator.send(:generate_form_fields_html)
731
+
732
+ expect(result).to include('form.select :status')
733
+ expect(result).to include("id: 'test_model_status'")
734
+ expect(result).to include('options_for_select')
735
+ end
736
+ end
737
+
738
+ context 'with date_select (date type)' do
739
+ before do
740
+ generator.instance_variable_set(:@form_fields, [
741
+ { name: 'birthday', type: 'date' }
742
+ ])
743
+ end
744
+
745
+ it 'generates date_select HTML with correct label_input_id' do
746
+ result = generator.send(:generate_form_fields_html)
747
+
748
+ expect(result).to include('form.date_select :birthday')
749
+ expect(result).to include("for: 'test_model_birthday_1i'")
750
+ expect(result).to include("id_prefix: 'test_model_birthday'")
751
+ end
752
+ end
753
+
754
+ context 'with datetime_select (datetime type)' do
755
+ before do
756
+ generator.instance_variable_set(:@form_fields, [
757
+ { name: 'created_at', type: 'datetime' }
758
+ ])
759
+ end
760
+
761
+ it 'generates datetime_select HTML with correct label_input_id' do
762
+ result = generator.send(:generate_form_fields_html)
763
+
764
+ expect(result).to include('form.datetime_select :created_at')
765
+ expect(result).to include("for: 'test_model_created_at_1i'")
766
+ expect(result).to include("id_prefix: 'test_model_created_at'")
767
+ end
768
+ end
769
+
770
+ context 'with datetime_select (timestamp type)' do
771
+ before do
772
+ generator.instance_variable_set(:@form_fields, [
773
+ { name: 'updated_at', type: 'timestamp' }
774
+ ])
775
+ end
776
+
777
+ it 'generates datetime_select HTML' do
778
+ result = generator.send(:generate_form_fields_html)
779
+
780
+ expect(result).to include('form.datetime_select :updated_at')
781
+ expect(result).to include("for: 'test_model_updated_at_1i'")
782
+ end
783
+ end
784
+
785
+ context 'with time_select (time type)' do
786
+ before do
787
+ generator.instance_variable_set(:@form_fields, [
788
+ { name: 'start_time', type: 'time' }
789
+ ])
790
+ end
791
+
792
+ it 'generates time_select HTML with correct label_input_id' do
793
+ result = generator.send(:generate_form_fields_html)
794
+
795
+ expect(result).to include('form.time_select :start_time')
796
+ expect(result).to include("for: 'test_model_start_time_4i'")
797
+ expect(result).to include("id_prefix: 'test_model_start_time'")
798
+ end
799
+ end
800
+
801
+ context 'with file_field (file type)' do
802
+ before do
803
+ generator.instance_variable_set(:@form_fields, [
804
+ { name: 'avatar', type: 'file' }
805
+ ])
806
+ end
807
+
808
+ it 'generates file_field HTML' do
809
+ result = generator.send(:generate_form_fields_html)
810
+
811
+ expect(result).to include('form.file_field :avatar')
812
+ expect(result).to include("id: 'test_model_avatar'")
813
+ expect(result).to include('border border-dashed')
814
+ expect(result).not_to include('multiple: true')
815
+ end
816
+ end
817
+
818
+ context 'with file_fields (files type)' do
819
+ before do
820
+ generator.instance_variable_set(:@form_fields, [
821
+ { name: 'attachments', type: 'files' }
822
+ ])
823
+ end
824
+
825
+ it 'generates file_field HTML with multiple' do
826
+ result = generator.send(:generate_form_fields_html)
827
+
828
+ expect(result).to include('form.file_field :attachments')
829
+ expect(result).to include("id: 'test_model_attachments'")
830
+ expect(result).to include('multiple: true')
831
+ end
832
+ end
833
+
834
+ context 'with multiple fields' do
835
+ before do
836
+ generator.instance_variable_set(:@form_fields, [
837
+ { name: 'name', type: 'string' },
838
+ { name: 'email', type: 'string' },
839
+ { name: 'active', type: 'boolean' }
840
+ ])
841
+ end
842
+
843
+ it 'generates HTML for all fields' do
844
+ result = generator.send(:generate_form_fields_html)
845
+
846
+ expect(result).to include('form.text_field :name')
847
+ expect(result).to include('form.text_field :email')
848
+ expect(result).to include('form.check_box :active')
849
+ end
850
+ end
851
+
852
+ context 'with empty form_fields' do
853
+ before do
854
+ generator.instance_variable_set(:@form_fields, [])
855
+ end
856
+
857
+ it 'returns empty string' do
858
+ result = generator.send(:generate_form_fields_html)
859
+
860
+ expect(result).to eq('')
861
+ end
862
+ end
863
+
864
+ context 'with unknown field type' do
865
+ before do
866
+ generator.instance_variable_set(:@form_fields, [
867
+ { name: 'custom_field', type: 'unknown' }
868
+ ])
869
+ end
870
+
871
+ it 'falls back to text_field' do
872
+ result = generator.send(:generate_form_fields_html)
873
+
874
+ expect(result).to include('form.text_field :custom_field')
875
+ end
876
+ end
877
+
878
+ context 'HTML structure' do
879
+ before do
880
+ generator.instance_variable_set(:@form_fields, [
881
+ { name: 'name', type: 'string' }
882
+ ])
883
+ end
884
+
885
+ it 'includes proper div structure' do
886
+ result = generator.send(:generate_form_fields_html)
887
+
888
+ expect(result).to include('sm:grid sm:grid-cols-3')
889
+ expect(result).to include('sm:items-start')
890
+ expect(result).to include('mt-2 sm:col-span-2 sm:mt-0')
891
+ expect(result).to include('</div>')
892
+ end
893
+ end
894
+ end
895
+
896
+ describe '#create_root_folder' do
897
+ let(:generator) do
898
+ described_class.new(['test'], {}).tap do |g|
899
+ g.destination_root = destination_root
900
+ g.instance_variable_set(:@engine_scope_path, '')
901
+ g.instance_variable_set(:@scope_path, 'test_models')
902
+ end
903
+ end
904
+
905
+ before do
906
+ allow(generator).to receive(:empty_directory)
907
+ end
908
+
909
+ it 'calls empty_directory with correct path' do
910
+ generator.send(:create_root_folder)
911
+
912
+ expect(generator).to have_received(:empty_directory).with(
913
+ File.join('app', 'views', '', 'test_models')
914
+ )
915
+ end
916
+
917
+ context 'with engine scope' do
918
+ before do
919
+ generator.instance_variable_set(:@engine_scope_path, 'admin')
920
+ allow(generator).to receive(:path_app).and_return('app')
921
+ end
922
+
923
+ it 'calls empty_directory with engine path' do
924
+ generator.send(:create_root_folder)
925
+
926
+ expect(generator).to have_received(:empty_directory).with(
927
+ File.join('app', 'views', 'admin', 'test_models')
928
+ )
929
+ end
930
+ end
931
+
932
+ context 'with route scope' do
933
+ before do
934
+ generator.instance_variable_set(:@engine_scope_path, 'dashboard')
935
+ allow(generator).to receive(:path_app).and_return('app')
936
+ end
937
+
938
+ it 'calls empty_directory with scope path' do
939
+ generator.send(:create_root_folder)
940
+
941
+ expect(generator).to have_received(:empty_directory).with(
942
+ File.join('app', 'views', 'dashboard', 'test_models')
943
+ )
944
+ end
945
+ end
946
+ end
947
+
948
+ describe '#create_controller_file' do
949
+ let(:generator) do
950
+ described_class.new(['test'], {}).tap do |g|
951
+ g.destination_root = destination_root
952
+ g.instance_variable_set(:@engine_scope_path, '')
953
+ g.instance_variable_set(:@scope_path, 'test_models')
954
+ end
955
+ end
956
+
957
+ before do
958
+ allow(generator).to receive(:template)
959
+ end
960
+
961
+ it 'calls template with correct path' do
962
+ generator.send(:create_controller_file)
963
+
964
+ expect(generator).to have_received(:template).with(
965
+ 'controllers/controller.rb.tt',
966
+ File.join('app', 'controllers', '', 'test_models_controller.rb')
967
+ )
968
+ end
969
+
970
+ context 'with engine scope' do
971
+ before do
972
+ generator.instance_variable_set(:@engine_scope_path, 'admin')
973
+ end
974
+
975
+ it 'calls template with engine path' do
976
+ generator.send(:create_controller_file)
977
+
978
+ expect(generator).to have_received(:template).with(
979
+ 'controllers/controller.rb.tt',
980
+ File.join('app', 'controllers', 'admin', 'test_models_controller.rb')
981
+ )
982
+ end
983
+ end
984
+
985
+ context 'with route scope' do
986
+ before do
987
+ generator.instance_variable_set(:@engine_scope_path, 'dashboard')
988
+ end
989
+
990
+ it 'calls template with scope path' do
991
+ generator.send(:create_controller_file)
992
+
993
+ expect(generator).to have_received(:template).with(
994
+ 'controllers/controller.rb.tt',
995
+ File.join('app', 'controllers', 'dashboard', 'test_models_controller.rb')
996
+ )
997
+ end
998
+ end
999
+ end
1000
+
1001
+ describe '#create_view_file' do
1002
+ let(:generator) do
1003
+ described_class.new(['test'], {}).tap do |g|
1004
+ g.destination_root = destination_root
1005
+ g.instance_variable_set(:@engine_scope_path, '')
1006
+ g.instance_variable_set(:@scope_path, 'test_models')
1007
+ g.instance_variable_set(:@form_fields, [])
1008
+ end
1009
+ end
1010
+
1011
+ before do
1012
+ allow(generator).to receive(:generate_form_fields_html).and_return('<div>form fields</div>')
1013
+ allow(generator).to receive(:template)
1014
+ end
1015
+
1016
+ it 'calls generate_form_fields_html' do
1017
+ generator.send(:create_view_file)
1018
+
1019
+ expect(generator).to have_received(:generate_form_fields_html)
1020
+ end
1021
+
1022
+ it 'assigns form_fields_html to instance variable' do
1023
+ generator.send(:create_view_file)
1024
+
1025
+ expect(generator.instance_variable_get(:@form_fields_html)).to eq('<div>form fields</div>')
1026
+ end
1027
+
1028
+ it 'calls template for all view files' do
1029
+ generator.send(:create_view_file)
1030
+
1031
+ expect(generator).to have_received(:template).with(
1032
+ 'views/_form.html.erb.tt',
1033
+ File.join('app', 'views', '', 'test_models', '_form.html.erb')
1034
+ )
1035
+ expect(generator).to have_received(:template).with(
1036
+ 'views/edit.html.erb.tt',
1037
+ File.join('app', 'views', '', 'test_models', 'edit.html.erb')
1038
+ )
1039
+ expect(generator).to have_received(:template).with(
1040
+ 'views/index.html.erb.tt',
1041
+ File.join('app', 'views', '', 'test_models', 'index.html.erb')
1042
+ )
1043
+ expect(generator).to have_received(:template).with(
1044
+ 'views/new.html.erb.tt',
1045
+ File.join('app', 'views', '', 'test_models', 'new.html.erb')
1046
+ )
1047
+ expect(generator).to have_received(:template).with(
1048
+ 'views/show.html.erb.tt',
1049
+ File.join('app', 'views', '', 'test_models', 'show.html.erb')
1050
+ )
1051
+ end
1052
+
1053
+ context 'with engine scope' do
1054
+ before do
1055
+ generator.instance_variable_set(:@engine_scope_path, 'admin')
1056
+ end
1057
+
1058
+ it 'calls template with engine path' do
1059
+ generator.send(:create_view_file)
1060
+
1061
+ expect(generator).to have_received(:template).with(
1062
+ 'views/_form.html.erb.tt',
1063
+ File.join('app', 'views', 'admin', 'test_models', '_form.html.erb')
1064
+ )
1065
+ end
1066
+ end
1067
+ end
1068
+
1069
+ describe '#create_link_file' do
1070
+ let(:generator) do
1071
+ described_class.new(['test'], {}).tap do |g|
1072
+ g.destination_root = destination_root
1073
+ g.instance_variable_set(:@engine_scope_path, 'admin')
1074
+ g.instance_variable_set(:@scope_path, 'test_models')
1075
+ end
1076
+ end
1077
+
1078
+ before do
1079
+ FileUtils.mkdir_p(File.join(destination_root, 'app', 'views', 'components', 'layouts'))
1080
+ FileUtils.mkdir_p(File.join(destination_root, 'app', 'views', 'components', 'menu'))
1081
+ FileUtils.mkdir_p(File.join(destination_root, 'config'))
1082
+ File.write(File.join(destination_root, 'config', 'routes.rb'), "Rails.application.routes.draw do\nend\n")
1083
+
1084
+ allow(generator).to receive(:template)
1085
+ allow(generator).to receive(:inject_into_file)
1086
+ allow(generator).to receive(:insert_into_file)
1087
+ allow(generator).to receive(:namespace_exists?).and_return(false)
1088
+ end
1089
+
1090
+ it 'calls template for menu link' do
1091
+ Dir.chdir(destination_root) do
1092
+ generator.send(:create_link_file)
1093
+ end
1094
+
1095
+ expect(generator).to have_received(:template).with(
1096
+ 'views/components/menu/link.html.erb.tt',
1097
+ File.join('app', 'views/components/menu', '_link_to_test_models.html.erb')
1098
+ )
1099
+ end
1100
+
1101
+ context 'when sidebar exists' do
1102
+ before do
1103
+ sidebar_path = File.join(destination_root, 'app', 'views/components/layouts/_sidebar.html.erb')
1104
+ File.write(sidebar_path, " <%# generate_link %>\n")
1105
+ end
1106
+
1107
+ it 'injects link into sidebar' do
1108
+ Dir.chdir(destination_root) do
1109
+ generator.send(:create_link_file)
1110
+ end
1111
+
1112
+ expect(generator).to have_received(:inject_into_file).with(
1113
+ File.join('app', 'views/components/layouts/_sidebar.html.erb'),
1114
+ anything,
1115
+ before: " <%# generate_link %>\n"
1116
+ )
1117
+ end
1118
+ end
1119
+
1120
+ context 'when sidebar does not exist' do
1121
+ before do
1122
+ FileUtils.rm_f(File.join(destination_root, 'app', 'views/components/layouts/_sidebar.html.erb'))
1123
+ end
1124
+
1125
+ it 'does not inject into sidebar' do
1126
+ Dir.chdir(destination_root) do
1127
+ generator.send(:create_link_file)
1128
+ end
1129
+
1130
+ expect(generator).not_to have_received(:inject_into_file).with(
1131
+ File.join('app', 'views/components/layouts/_sidebar.html.erb'),
1132
+ anything,
1133
+ anything
1134
+ )
1135
+ end
1136
+ end
1137
+
1138
+ context 'when link already exists' do
1139
+ before do
1140
+ link_path = File.join(destination_root, 'app', 'views/components/menu/_link_to_test_models.html.erb')
1141
+ FileUtils.touch(link_path)
1142
+ end
1143
+
1144
+ it 'does not create template again' do
1145
+ Dir.chdir(destination_root) do
1146
+ generator.send(:create_link_file)
1147
+ end
1148
+
1149
+ expect(generator).not_to have_received(:template)
1150
+ end
1151
+ end
1152
+
1153
+ context 'when namespace does not exist' do
1154
+ before do
1155
+ allow(generator).to receive(:namespace_exists?).and_return(false)
1156
+ allow(generator).to receive(:routes_file_path).and_return(File.join(destination_root, 'config', 'routes.rb'))
1157
+ allow(generator).to receive(:options).and_return({})
1158
+ allow(generator).to receive(:engine_path).and_return(nil)
1159
+ end
1160
+
1161
+ it 'injects routes at root level' do
1162
+ Dir.chdir(destination_root) do
1163
+ generator.send(:create_link_file)
1164
+ end
1165
+
1166
+ expect(generator).to have_received(:inject_into_file).with(
1167
+ File.join(destination_root, 'config', 'routes.rb'),
1168
+ " resources :test_models\n",
1169
+ after: "Rails.application.routes.draw do\n"
1170
+ )
1171
+ end
1172
+ end
1173
+
1174
+ context 'when namespace exists' do
1175
+ before do
1176
+ allow(generator).to receive(:namespace_exists?).and_return(true)
1177
+ end
1178
+
1179
+ it 'does not create namespace again' do
1180
+ Dir.chdir(destination_root) do
1181
+ generator.send(:create_link_file)
1182
+ end
1183
+
1184
+ expect(generator).not_to have_received(:insert_into_file)
1185
+ end
1186
+ end
1187
+
1188
+ context 'with engine scope' do
1189
+ let(:generator_with_engine) do
1190
+ described_class.new(['test'], { engine: 'admin' }).tap do |g|
1191
+ g.destination_root = destination_root
1192
+ g.instance_variable_set(:@engine_scope_path, 'admin')
1193
+ g.instance_variable_set(:@scope_path, 'test_models')
1194
+ end
1195
+ end
1196
+
1197
+ before do
1198
+ FileUtils.mkdir_p(File.join(destination_root, 'config'))
1199
+ File.write(File.join(destination_root, 'config', 'routes.rb'), "Admin::Engine.routes.draw do\nend\n")
1200
+ allow(generator_with_engine).to receive(:template)
1201
+ allow(generator_with_engine).to receive(:inject_into_file)
1202
+ allow(generator_with_engine).to receive(:routes_file_path).and_return(File.join(destination_root, 'config', 'routes.rb'))
1203
+ allow(generator_with_engine).to receive(:engine_path).and_return('engines/admin')
1204
+ allow(generator_with_engine).to receive(:options).and_return({ engine: 'admin' })
1205
+ end
1206
+
1207
+ it 'injects resource routes' do
1208
+ Dir.chdir(destination_root) do
1209
+ generator_with_engine.send(:create_link_file)
1210
+ end
1211
+
1212
+ expect(generator_with_engine).to have_received(:inject_into_file).with(
1213
+ File.join(destination_root, 'config', 'routes.rb'),
1214
+ " resources :test_models\n",
1215
+ after: "Admin::Engine.routes.draw do\n"
1216
+ )
1217
+ end
1218
+ end
1219
+
1220
+ context 'without engine scope' do
1221
+ before do
1222
+ generator.instance_variable_set(:@engine_scope_path, '')
1223
+ allow(generator).to receive(:routes_file_path).and_return(File.join(destination_root, 'config', 'routes.rb'))
1224
+ end
1225
+
1226
+ it 'injects resource routes at root level' do
1227
+ Dir.chdir(destination_root) do
1228
+ generator.send(:create_link_file)
1229
+ end
1230
+
1231
+ expect(generator).to have_received(:inject_into_file).with(
1232
+ anything,
1233
+ anything,
1234
+ after: "Rails.application.routes.draw do\n"
1235
+ )
1236
+ end
1237
+ end
1238
+ end
1239
+
1240
+ describe '#running' do
1241
+ let(:generator) do
1242
+ described_class.new(['test'], {}).tap do |g|
1243
+ g.destination_root = destination_root
1244
+ end
1245
+ end
1246
+
1247
+ before do
1248
+ allow(generator).to receive(:setup_variables)
1249
+ allow(generator).to receive(:create_root_folder)
1250
+ allow(generator).to receive(:create_controller_file)
1251
+ allow(generator).to receive(:create_spec_files)
1252
+ allow(generator).to receive(:create_view_file)
1253
+ allow(generator).to receive(:create_link_file)
1254
+ end
1255
+
1256
+ it 'calls all methods in correct order' do
1257
+ generator.running
1258
+
1259
+ expect(generator).to have_received(:setup_variables).ordered
1260
+ expect(generator).to have_received(:create_root_folder).ordered
1261
+ expect(generator).to have_received(:create_controller_file).ordered
1262
+ expect(generator).to have_received(:create_spec_files).ordered
1263
+ expect(generator).to have_received(:create_view_file).ordered
1264
+ expect(generator).to have_received(:create_link_file).ordered
1265
+ end
1266
+
1267
+ it 'calls all required methods' do
1268
+ generator.running
1269
+
1270
+ expect(generator).to have_received(:setup_variables)
1271
+ expect(generator).to have_received(:create_root_folder)
1272
+ expect(generator).to have_received(:create_controller_file)
1273
+ expect(generator).to have_received(:create_spec_files)
1274
+ expect(generator).to have_received(:create_view_file)
1275
+ expect(generator).to have_received(:create_link_file)
1276
+ end
1277
+ end
1278
+
1279
+ describe '#available_engines' do
1280
+ let(:generator) { described_class.new(['test'], {}) }
1281
+
1282
+ context 'when engines exist in engines/ directory' do
1283
+ before do
1284
+ allow(Dir).to receive(:exist?).and_call_original
1285
+ allow(Dir).to receive(:exist?).with('engines').and_return(true)
1286
+ allow(Dir).to receive(:exist?).with('components').and_return(false)
1287
+ allow(Dir).to receive(:exist?).with('gems').and_return(false)
1288
+ allow(Dir).to receive(:exist?).with('.').and_return(true)
1289
+
1290
+ allow(Dir).to receive(:glob).with('engines/*').and_return(['engines/admin', 'engines/api'])
1291
+ allow(Dir).to receive(:glob).with('components/*').and_return([])
1292
+ allow(Dir).to receive(:glob).with('gems/*').and_return([])
1293
+ allow(Dir).to receive(:glob).with('./*').and_return([])
1294
+
1295
+ allow(Dir).to receive(:exist?).with('engines/admin').and_return(true)
1296
+ allow(Dir).to receive(:exist?).with('engines/api').and_return(true)
1297
+
1298
+ allow(File).to receive(:exist?).and_call_original
1299
+ allow(File).to receive(:exist?).with('engines/admin/admin.gemspec').and_return(true)
1300
+ allow(File).to receive(:exist?).with('engines/api/api.gemspec').and_return(true)
1301
+ end
1302
+
1303
+ it 'returns engines from engines directory' do
1304
+ result = generator.send(:available_engines)
1305
+
1306
+ expect(result).to include('admin')
1307
+ expect(result).to include('api')
1308
+ end
1309
+ end
1310
+
1311
+ context 'when engines exist in components/ directory' do
1312
+ before do
1313
+ allow(Dir).to receive(:exist?).and_call_original
1314
+ allow(Dir).to receive(:exist?).with('engines').and_return(false)
1315
+ allow(Dir).to receive(:exist?).with('components').and_return(true)
1316
+ allow(Dir).to receive(:exist?).with('gems').and_return(false)
1317
+ allow(Dir).to receive(:exist?).with('.').and_return(false)
1318
+
1319
+ allow(Dir).to receive(:glob).and_call_original
1320
+ allow(Dir).to receive(:glob).with('components/*').and_return(['components/core'])
1321
+ allow(Dir).to receive(:exist?).with('components/core').and_return(true)
1322
+ allow(File).to receive(:exist?).and_call_original
1323
+ allow(File).to receive(:exist?).with('components/core/core.gemspec').and_return(true)
1324
+ end
1325
+
1326
+ it 'returns engines from components directory' do
1327
+ result = generator.send(:available_engines)
1328
+
1329
+ expect(result).to include('core')
1330
+ end
1331
+ end
1332
+
1333
+ context 'when engines exist in gems/ directory' do
1334
+ before do
1335
+ allow(Dir).to receive(:exist?).and_call_original
1336
+ allow(Dir).to receive(:exist?).with('engines').and_return(false)
1337
+ allow(Dir).to receive(:exist?).with('components').and_return(false)
1338
+ allow(Dir).to receive(:exist?).with('gems').and_return(true)
1339
+ allow(Dir).to receive(:exist?).with('.').and_return(false)
1340
+
1341
+ allow(Dir).to receive(:glob).and_call_original
1342
+ allow(Dir).to receive(:glob).with('gems/*').and_return(['gems/shared'])
1343
+ allow(Dir).to receive(:exist?).with('gems/shared').and_return(true)
1344
+ allow(File).to receive(:exist?).and_call_original
1345
+ allow(File).to receive(:exist?).with('gems/shared/shared.gemspec').and_return(true)
1346
+ end
1347
+
1348
+ it 'returns engines from gems directory' do
1349
+ result = generator.send(:available_engines)
1350
+
1351
+ expect(result).to include('shared')
1352
+ end
1353
+ end
1354
+
1355
+ context 'when engines exist in root directory' do
1356
+ before do
1357
+ allow(Dir).to receive(:exist?).and_call_original
1358
+ allow(Dir).to receive(:exist?).with('engines').and_return(false)
1359
+ allow(Dir).to receive(:exist?).with('components').and_return(false)
1360
+ allow(Dir).to receive(:exist?).with('gems').and_return(false)
1361
+ allow(Dir).to receive(:exist?).with('.').and_return(true)
1362
+
1363
+ allow(Dir).to receive(:glob).with('./*').and_return(['./root_engine'])
1364
+ allow(Dir).to receive(:exist?).with('./root_engine').and_return(true)
1365
+ allow(File).to receive(:exist?).with('./root_engine/root_engine.gemspec').and_return(true)
1366
+ end
1367
+
1368
+ it 'returns engines from root directory' do
1369
+ result = generator.send(:available_engines)
1370
+
1371
+ expect(result).to include('root_engine')
1372
+ end
1373
+ end
1374
+
1375
+ context 'when duplicate engines exist' do
1376
+ before do
1377
+ allow(Dir).to receive(:exist?).and_call_original
1378
+ allow(Dir).to receive(:exist?).with('engines').and_return(true)
1379
+ allow(Dir).to receive(:exist?).with('components').and_return(true)
1380
+ allow(Dir).to receive(:exist?).with('gems').and_return(false)
1381
+ allow(Dir).to receive(:exist?).with('.').and_return(false)
1382
+
1383
+ allow(Dir).to receive(:glob).and_call_original
1384
+ allow(Dir).to receive(:glob).with('engines/*').and_return(['engines/admin'])
1385
+ allow(Dir).to receive(:glob).with('components/*').and_return(['components/admin'])
1386
+
1387
+ allow(Dir).to receive(:exist?).with('engines/admin').and_return(true)
1388
+ allow(Dir).to receive(:exist?).with('components/admin').and_return(true)
1389
+
1390
+ allow(File).to receive(:exist?).and_call_original
1391
+ allow(File).to receive(:exist?).with('engines/admin/admin.gemspec').and_return(true)
1392
+ allow(File).to receive(:exist?).with('components/admin/admin.gemspec').and_return(true)
1393
+ end
1394
+
1395
+ it 'returns unique engines' do
1396
+ result = generator.send(:available_engines)
1397
+
1398
+ expect(result.count('admin')).to eq(1)
1399
+ end
1400
+ end
1401
+
1402
+ context 'when no engines exist' do
1403
+ before do
1404
+ allow(Dir).to receive(:exist?).and_return(false)
1405
+ end
1406
+
1407
+ it 'returns empty array' do
1408
+ result = generator.send(:available_engines)
1409
+
1410
+ expect(result).to eq([])
1411
+ end
1412
+ end
1413
+ end
1414
+ end