@@ -19,8 +19,7 @@ class GenerateCrudTests extends Command
1919 {--operation= : Only generate tests for the given CRUD operation}
2020 {--type= : The type of test to generate (browser or feature)}
2121 {--framework=phpunit : The testing framework to use (phpunit or pest)}
22- {--force : Overwrite existing test classes}
23- {--dry-run : Show what would be generated without writing files} ' ;
22+ {--force : Overwrite existing test classes} ' ;
2423
2524 /**
2625 * The console command description.
@@ -107,8 +106,8 @@ public function handle(): int
107106
108107 $ this ->info ('Test generation finished. ' );
109108
110- if (! empty ($ this ->generatedFiles ) && $ this -> confirm ( ' Do you want to run the generated tests now? ' , true ) ) {
111- $ this ->runGeneratedTests ( );
109+ if (! empty ($ this ->generatedFiles )) {
110+ $ this ->info ( ' To run the tests call: php artisan test ' );
112111 }
113112
114113 return self ::SUCCESS ;
@@ -131,7 +130,13 @@ protected function runGeneratedTests(): void
131130 $ binaryPath = base_path ("vendor/bin/ {$ binary }" );
132131
133132 if (file_exists ($ binaryPath )) {
134- $ command = '" ' .PHP_BINARY .'" " ' .$ binaryPath .'" ' .implode (' ' , array_map (fn ($ f ) => '" ' .$ f .'" ' , $ featureTests ));
133+ $ command = '" ' .PHP_BINARY .'" " ' .$ binaryPath .'" ' ;
134+
135+ if (file_exists (base_path ('phpunit.xml ' ))) {
136+ $ command .= ' --configuration " ' .base_path ('phpunit.xml ' ).'" ' ;
137+ }
138+
139+ $ command .= ' ' .implode (' ' , array_map (fn ($ f ) => '" ' .$ f .'" ' , $ featureTests ));
135140 passthru ($ command );
136141 } elseif ($ framework === 'phpunit ' && $ this ->getApplication ()->has ('test ' )) {
137142 $ this ->call ('test ' , ['args ' => $ featureTests ]);
@@ -219,7 +224,7 @@ protected function generateTestForOperation(array $controllerInfo, string $opera
219224 $ operationStubPath = $ this ->getStubPath ('operations/ ' .$ stubName );
220225
221226 if (! File::exists ($ operationStubPath )) {
222- $ this ->skippedOperations [$ controllerInfo ['short_name ' ]][] = $ operation ;
227+ $ this ->skippedOperations [$ controllerInfo ['short_name ' ]][] = " $ operation ( $ type ) " ;
223228 return ;
224229 }
225230
@@ -231,15 +236,31 @@ protected function generateTestForOperation(array $controllerInfo, string $opera
231236 return ;
232237 }
233238
239+ // Replace __MARK_TEST_AS_SKIPPED__ placeholder
240+ $ hasFactory = $ model && class_exists ($ model ) && method_exists ($ model , 'factory ' ) && file_exists (database_path ('factories/ ' .class_basename ($ model ).'Factory.php ' ));
241+
242+ if ($ hasFactory ) {
243+ // If the model has a factory, remove the placeholder
244+ $ methods = str_replace ('__MARK_TEST_AS_SKIPPED__ ' , '' , $ methods );
245+ } else {
246+ // If no factory, mark the test as skipped
247+ $ methods = str_replace (
248+ '__MARK_TEST_AS_SKIPPED__ ' ,
249+ '$this->markTestSkipped( \'Factory not found for model \' . $this->model); ' ,
250+ $ methods
251+ );
252+ }
253+
234254 $ className = $ this ->resolveClassName ($ controllerInfo , $ operation );
235255
236- $ controllerName = Str::replaceLast ('Controller ' , '' , $ controllerInfo ['short_name ' ]);
256+ $ controllerShortName = Str::replaceLast ('Controller ' , '' , $ controllerInfo ['short_name ' ]);
257+ $ controllerRelPath = $ this ->getRelativeNamespace ($ controllerInfo ['class ' ]);
237258
238259 $ namespace = $ type === 'feature '
239- ? 'Tests \\Feature \\' .$ controllerName
240- : 'Tests \\Browser \\' .$ controllerName ;
260+ ? 'Tests \\Feature \\' .$ controllerRelPath
261+ : 'Tests \\Browser \\' .$ controllerRelPath ;
241262
242- $ baseClassName = $ controllerName .'TestBase ' ;
263+ $ baseClassName = $ controllerShortName .'TestBase ' ;
243264 $ this ->ensureBaseTestClassExists ($ namespace , $ baseClassName , $ controllerInfo , $ config , $ type );
244265
245266 $ operationConfig = $ this ->extractOperationConfig ($ config );
@@ -262,20 +283,14 @@ protected function generateTestForOperation(array $controllerInfo, string $opera
262283 'methods ' => $ methods ,
263284 ]);
264285
265- $ filePath = $ this ->determineOutputPath ($ className , $ controllerName , $ type );
286+ $ filePath = $ this ->determineOutputPath ($ className , $ controllerRelPath , $ type );
266287
267288 if ($ this ->shouldSkipExisting ($ filePath )) {
268289 $ this ->line (" ⏭️ Skipping {$ operation } (file exists, use --force to overwrite) " );
269290
270291 return ;
271292 }
272293
273- if ($ this ->option ('dry-run ' )) {
274- $ this ->line (" 📝 Would write {$ filePath }" );
275-
276- return ;
277- }
278-
279294 File::ensureDirectoryExists (dirname ($ filePath ));
280295 File::put ($ filePath , $ testClass );
281296
@@ -318,7 +333,7 @@ protected function shouldSkipExisting(string $filePath): bool
318333 */
319334 protected function ensureBaseTestClassExists (string $ namespace , string $ className , array $ controllerInfo , array $ config , string $ type ): void
320335 {
321- $ controllerName = Str:: replaceLast ( ' Controller ' , '' , $ controllerInfo ['short_name ' ]);
336+ $ controllerName = $ this -> getRelativeNamespace ( $ controllerInfo ['class ' ]);
322337 $ filePath = $ this ->determineOutputPath ($ className , $ controllerName , $ type );
323338
324339 if (in_array ($ filePath , $ this ->generatedBaseClasses )) {
@@ -565,7 +580,8 @@ protected function determineOutputPath(string $className, string $controllerName
565580 : base_path ('tests/Browser ' );
566581
567582 if ($ controllerName ) {
568- $ baseDir .= DIRECTORY_SEPARATOR .$ controllerName ;
583+ $ pathFn = fn ($ path ) => str_replace ('\\' , DIRECTORY_SEPARATOR , $ path );
584+ $ baseDir .= DIRECTORY_SEPARATOR .$ pathFn ($ controllerName );
569585 }
570586
571587 if (! File::isDirectory ($ baseDir )) {
@@ -597,28 +613,54 @@ protected function normalizeRoute(string $route): string
597613 }
598614
599615 /**
600- * Get the path to a stub file, respecting the chosen framework.
616+ * Get the path to a stub file, respecting the chosen framework and published stubs .
601617 */
602618 protected function getStubPath (string $ name ): string
603619 {
604620 $ framework = $ this ->option ('framework ' );
605- $ basePath = __DIR__ .'/../../../resources/stubs/crud-testing/ ' ;
606621
607- // If framework is defined and not default, try to find framework-specific stub
608- if ($ framework && $ framework !== 'phpunit ' ) {
609- // First try nested directory: frameworks/{framework}/{stub}
610- $ namespaced = $ basePath . $ framework . '/ ' . $ name ;
611- if (File::exists ($ namespaced )) {
612- return $ namespaced ;
622+ $ searchPaths = [
623+ resource_path ('views/vendor/backpack/crud/stubs/crud-testing/ ' ),
624+ __DIR__ .'/../../../resources/stubs/crud-testing/ ' ,
625+ ];
626+
627+ foreach ($ searchPaths as $ basePath ) {
628+ // If framework is defined and not default, try to find framework-specific stub
629+ if ($ framework && $ framework !== 'phpunit ' ) {
630+ // First try nested directory: frameworks/{framework}/{stub}
631+ $ namespaced = $ basePath . $ framework . '/ ' . $ name ;
632+ if (File::exists ($ namespaced )) {
633+ return $ namespaced ;
634+ }
635+
636+ // Then try prefixed: {framework}-{stub}
637+ $ prefixed = $ basePath . $ framework . '- ' . $ name ;
638+ if (File::exists ($ prefixed )) {
639+ return $ prefixed ;
640+ }
613641 }
614-
615- // Then try prefixed: {framework}-{stub}
616- $ prefixed = $ basePath . $ framework . '- ' . $ name ;
617- if (File::exists ($ prefixed )) {
618- return $ prefixed ;
642+
643+ if (File::exists ($ basePath . $ name )) {
644+ return $ basePath . $ name ;
619645 }
620646 }
621647
622- return $ basePath . $ name ;
648+ return __DIR__ .'/../../../resources/stubs/crud-testing/ ' . $ name ;
649+ }
650+
651+ /**
652+ * Get the relative namespace for the test class based on controller structure.
653+ */
654+ protected function getRelativeNamespace (string $ controllerClass ): string
655+ {
656+ $ rootNamespace = 'App \\Http \\Controllers \\' ;
657+
658+ if (Str::startsWith ($ controllerClass , $ rootNamespace )) {
659+ $ relative = Str::after ($ controllerClass , $ rootNamespace );
660+ } else {
661+ $ relative = class_basename ($ controllerClass );
662+ }
663+
664+ return Str::replaceLast ('Controller ' , '' , $ relative );
623665 }
624666}
0 commit comments