Skip to content

Commit 233d297

Browse files
committed
feature tests
1 parent cd435fc commit 233d297

File tree

14 files changed

+426
-172
lines changed

14 files changed

+426
-172
lines changed

src/BackpackServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ public function publishFiles()
181181
__DIR__.'/resources/views/ui/inc/menu_items.blade.php' => resource_path('views/vendor/backpack/ui/inc/menu_items.blade.php'),
182182
];
183183
$backpack_custom_routes_file = [__DIR__.$this->customRoutesFilePath => base_path($this->customRoutesFilePath)];
184+
$backpack_stubs = [__DIR__.'/resources/stubs/crud-testing' => resource_path('views/vendor/backpack/crud/stubs/crud-testing')];
184185

185186
// calculate the path from current directory to get the vendor path
186187
$vendorPath = dirname(__DIR__, 3);
@@ -202,6 +203,7 @@ public function publishFiles()
202203
$this->publishes($backpack_views, 'views');
203204
$this->publishes($backpack_menu_contents_view, 'menu_contents');
204205
$this->publishes($backpack_custom_routes_file, 'custom_routes');
206+
$this->publishes($backpack_stubs, 'stubs');
205207
$this->publishes($gravatar_assets, 'gravatar');
206208
$this->publishes($minimum, 'minimum');
207209
}

src/app/Console/Commands/GenerateCrudTests.php

Lines changed: 75 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

src/app/Library/CrudPanel/Traits/Input.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ trait Input
2121
* @param bool|string $relationMethod
2222
* @return array
2323
*/
24-
private function splitInputIntoDirectAndRelations($inputs, $relationDetails = null, $relationMethod = false)
24+
public function splitInputIntoDirectAndRelations($inputs, $relationDetails = null, $relationMethod = false)
2525
{
2626
$crudFields = $relationDetails['crudFields'] ?? [];
2727
$model = $relationDetails['model'] ?? false;

src/app/Library/CrudTesting/CrudControllerDiscovery.php

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Support\Str;
88
use ReflectionClass;
99
use Backpack\CRUD\CrudManager;
10+
use Illuminate\Support\Facades\Route;
1011

1112
/**
1213
* Discovers CrudControllers in the application and extracts their configuration.
@@ -21,7 +22,10 @@ class CrudControllerDiscovery
2122
*/
2223
public static function discover($paths = null): array
2324
{
24-
$paths = $paths ?? [app_path('Http/Controllers')];
25+
if ($paths === null) {
26+
$paths = config('backpack.testing.controllers_path', app_path('Http/Controllers'));
27+
}
28+
2529
$paths = is_array($paths) ? $paths : [$paths];
2630

2731
$controllers = [];
@@ -86,10 +90,18 @@ public static function analyzeController(string $controllerClass): array
8690
*/
8791
protected static function getOperations(ReflectionClass $reflection): array
8892
{
89-
$traits = $reflection->getTraitNames();
93+
$traits = [];
94+
$currentReflection = $reflection;
95+
96+
// Collect traits from the class and all its parents
97+
while ($currentReflection) {
98+
$traits = array_merge($traits, $currentReflection->getTraitNames());
99+
$currentReflection = $currentReflection->getParentClass();
100+
}
101+
90102
$operations = [];
91103

92-
foreach ($traits as $trait) {
104+
foreach (array_unique($traits) as $trait) {
93105
try {
94106
$traitReflection = new ReflectionClass($trait);
95107
} catch (\ReflectionException $e) {
@@ -168,20 +180,38 @@ protected static function getClassNameFromFile(string $filePath): ?string
168180
*/
169181
public static function buildCrudPanel(string $controllerClass, string $operation = 'list'): object
170182
{
183+
self::clearCrudPanelBindings($controllerClass);
184+
185+
TestConfigHelper::applyConfiguration($controllerClass, $operation);
186+
171187
$controller = app()->make($controllerClass);
172-
$request = request([
173-
'operation' => $operation
174-
]);
188+
175189
if (! CrudManager::hasCrudPanel($controllerClass)) {
176190

177-
$controller->initializeCrudPanel($request);
191+
$controller->initializeCrudPanel(request());
178192

179193
return CrudManager::getCrudPanel($controllerClass);
180194
}
181195

182196
$controller->setupCrudController($operation);
183197

184198
return CrudManager::getCrudPanel($controller);
199+
200+
201+
}
202+
203+
private static function clearCrudPanelBindings(): void
204+
{
205+
if (app()->bound('crud')) {
206+
app()->forgetInstance('crud');
207+
app()->forgetInstance(\Backpack\CRUD\app\Library\CrudPanel\CrudPanel::class);
208+
}
185209

210+
if (app()->bound('CrudManager')) {
211+
app()->forgetInstance('CrudManager');
212+
}
213+
214+
\Illuminate\Support\Facades\Facade::clearResolvedInstance('CrudManager');
215+
\Illuminate\Support\Facades\Facade::clearResolvedInstance('crud');
186216
}
187217
}

0 commit comments

Comments
 (0)