Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d7d6cdb
Add HasGenerationSummary trait with showGenerationSummary method
dissto Sep 1, 2025
cfb55f4
Add InteractsWithFilesystem trait for test file generation and manage…
dissto Sep 1, 2025
24c84bd
Add RendersFilamentTests trait for rendering resource tests
dissto Sep 1, 2025
e058863
Add InteractsWithUserInput trait for user panel and resource selection
dissto Sep 1, 2025
79944bb
Refactor FilamentTestsCommand to enhance user interaction and test ge…
dissto Sep 1, 2025
acda6bb
Enhance HasGenerationSummary trait to display generated test files in…
dissto Sep 1, 2025
12632d5
Update HasGenerationSummary trait to use warning for no generated tes…
dissto Sep 1, 2025
51bcd0c
Refactor HasGenerationSummary to improve test file display and count
dissto Sep 1, 2025
24e57e5
Refactor InteractsWithFilesystem to improve file handling and test ge…
dissto Sep 1, 2025
7f81655
Refactor RendersFilamentTests to return structured output with test c…
dissto Sep 1, 2025
0759c47
Refactor HasGenerationSummary to streamline generated files output st…
dissto Sep 1, 2025
89a161f
Add --force option to FilamentTestsCommand for overwriting existing t…
dissto Sep 1, 2025
4f9e6a5
Refactor HasGenerationSummary to use panel names instead of IDs in ge…
dissto Sep 1, 2025
3b528e9
Use the getter method instead
dissto Sep 1, 2025
3e53874
Fix test count calculation in HasGenerationSummary and InteractsWithF…
dissto Sep 1, 2025
60f2463
Add generation summary display to InteractsWithFilesystem trait
dissto Sep 1, 2025
8e7639f
Add getRenderers method to FilamentTestsCommand for test renderer man…
dissto Sep 1, 2025
3ec33f0
Add renderTestsForResource method to InteractsWithFilesystem trait fo…
dissto Sep 1, 2025
c2a83ff
Refactor type hints and improve method signatures in various classes
dissto Sep 1, 2025
2bc5cf1
Pint
dissto Sep 1, 2025
a4d36df
Add return type hint to getResourceTable method in InteractsWithTable…
dissto Sep 1, 2025
9fac937
Fix using deprecated `->getActions()` call
dissto Sep 1, 2025
6541818
Annotation no longer needed with the correct return type from the int…
dissto Sep 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 15 additions & 133 deletions src/Commands/FilamentTestsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,150 +2,32 @@

namespace CodeWithDennis\FilamentTests\Commands;

use App\Filament\Resources\Users\UserResource;
use CodeWithDennis\FilamentTests\TestRenderers\BeforeEach;
use CodeWithDennis\FilamentTests\TestRenderers\Resources\Pages\Create\CanRenderCreatePageTest;
use CodeWithDennis\FilamentTests\TestRenderers\Resources\Pages\Edit\CanRenderEditPageTest;
use CodeWithDennis\FilamentTests\TestRenderers\Resources\Pages\Index\CanRenderIndexPageTest;
use Filament\Facades\Filament;
use CodeWithDennis\FilamentTests\Concerns\Commands\HasGenerationSummary;
use CodeWithDennis\FilamentTests\Concerns\Commands\InteractsWithFilesystem;
use CodeWithDennis\FilamentTests\Concerns\Commands\InteractsWithUserInput;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;

use function Laravel\Prompts\multiselect;

class FilamentTestsCommand extends Command
{
protected $signature = 'make:filament-test
{--skip-pint : Skip running Pint on generated files}';

protected $description = 'Create a new test for a Filament component';
use HasGenerationSummary;
use InteractsWithFilesystem;
use InteractsWithUserInput;

// protected ?Collection $resources = null;
protected $signature = 'make:filament-test
{--skip-pint : Skip running Pint on generated files}
{--force : Overwrite existing test files without confirmation}';

public function __construct(
protected ?Collection $resources = null,
protected ?Collection $panels = null,
protected array $generatedFiles = [],
protected ?Filesystem $files = null,
) {
$this->resources ??= collect();
$this->panels ??= collect();
$this->files ??= new Filesystem;
parent::__construct();
}
protected $description = 'Create tests for your Filament resources';

public function handle(): void
{
// $this->panels = $this->askUserToSelectPanels();
// $this->resources = $this->askUserToSelectWhichResourcesFromTheSelectedPanel();

$this->panels = collect(['admin']);
$this->resources = collect([
'admin' => [
UserResource::class,
],
]);

foreach ($this->resources as $resourceClasses) {
foreach ($resourceClasses as $resourceClass) {
$rendered = $this->renderTestsForResource($resourceClass);

$filePath = $this->getTestFilePath($resourceClass);
$this->files->ensureDirectoryExists(dirname($filePath));

file_put_contents($filePath, $rendered);

$this->generatedFiles[] = $filePath;

$this->info("Created test for {$resourceClass} → {$filePath}");
}
}

$this->runPintOnGeneratedFiles();
}

/**
* Render all tests for a single resource.
*/
protected function renderTestsForResource(string $resourceClass): string
{
return implode("\n\n", [
'<?php',
BeforeEach::build($resourceClass)->render(),
CanRenderIndexPageTest::build($resourceClass)->render(),
// CanRenderCreatePageTest::build($resourceClass)->render(),
// CanRenderEditPageTest::build($resourceClass)->render(),
]);
}

protected function runPintOnGeneratedFiles(): void
{
if ($this->generatedFiles === []) {
return;
}

if ($this->option('skip-pint')) {
return;
}

$files = implode(' ', $this->generatedFiles);

Process::run("vendor/bin/pint {$files}");
}

protected function getTestFilePath(string $resourceClass): string
{
$relativeClass = str($resourceClass)
->replaceFirst('App\\', '')
->replace('\\', '/');

return base_path("tests/Feature/{$relativeClass}Test.php");
}

protected function askUserToSelectPanels(): Collection
{
$allPanels = collect(Filament::getPanels());

$selectedPanelIds = multiselect(
label: 'Which Filament panel do you want to use?',
options: $allPanels->mapWithKeys(fn ($panel) => [
$panel->getId() => $panel->getId(),
])->toArray(),
);

return collect($selectedPanelIds);
}

public function askUserToSelectWhichResourcesFromTheSelectedPanel(): Collection
{
$resourcesByPanel = $this->panels->mapWithKeys(fn ($panelId) => [
$panelId => collect(Filament::getPanel($panelId)?->getResources() ?? [])
->mapWithKeys(fn ($resource) => [$resource => class_basename($resource)])
->toArray(),
]);

$selectedResources = collect();

foreach ($this->panels as $panelId) {
$resources = $resourcesByPanel[$panelId] ?? [];

if (empty($resources)) {
continue;
}
$this->panels = $this->askUserToSelectPanels();
$this->resources = $this->askUserToSelectResourcesFromTheSelectedPanels();

$selected = multiselect(
label: "Select resources for panel: {$panelId}",
options: $resources,
required: true,
);
$this->generateTests();

if ($selected !== []) {
$selectedResources[$panelId] = $selected;
}
}
$this->showGenerationSummary();

return $selectedResources;
$this->runPintOnGeneratedTests();
}
}
34 changes: 34 additions & 0 deletions src/Concerns/Commands/HasGenerationSummary.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace CodeWithDennis\FilamentTests\Concerns\Commands;

use function Laravel\Prompts\table;
use function Laravel\Prompts\warning;

trait HasGenerationSummary
{
protected function showGenerationSummary(): void
{
if (blank($this->getGeneratedFiles())) {
warning('No test files were generated.');

return;
}

$rows = collect($this->getGeneratedFiles())
->flatMap(fn ($resources, $panelName) => collect($resources)
->map(fn ($data, $resource): array => [
$resource,
$panelName,
$data['num_tests'] ?? 0,
])
)
->values()
->all();

table(
['Resource', 'Panel', '# Tests'],
$rows
);
}
}
78 changes: 78 additions & 0 deletions src/Concerns/Commands/InteractsWithFilesystem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace CodeWithDennis\FilamentTests\Concerns\Commands;

use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;

use function Laravel\Prompts\confirm;
use function Laravel\Prompts\info;

trait InteractsWithFilesystem
{
use RendersFilamentTests;

protected array $generatedFiles = [];

protected function getGeneratedFiles(): array
{
return $this->generatedFiles;
}

protected function runPintOnGeneratedTests(): void
{
if (empty($this->getGeneratedFiles()) || $this->option('skip-pint')) {
return;
}

$files = collect($this->getGeneratedFiles())
->map(fn ($resources) => collect($resources)->pluck('path'))
->flatten()
->implode(' ');

Process::run("vendor/bin/pint {$files}");
}

protected function getTestFilePath(string $resourceClass): string
{
$relativeClass = str($resourceClass)
->replaceFirst('App\\', '')
->replace('\\', '/');

return base_path("tests/Feature/{$relativeClass}Test.php");
}

protected function generateTestsForSelectedResource(string $resource, ?string $panel = null): void
{
$filePath = $this->getTestFilePath($resource);
$force = (bool) $this->option('force');

if (File::exists($filePath) && ! $force && ! confirm("The tests for {$resource} already exists. Do you want to overwrite it?", false)) {
info("Skipped generating test for {$resource}.");

return;
}

$renderResult = $this->renderTestsForResource($resource);

File::ensureDirectoryExists(dirname((string) $filePath));
File::put($filePath, $renderResult['content']);

$panelKey = $panel ?? 'default';

$this->generatedFiles[$panelKey][$resource] = [
'path' => $filePath,
'num_tests' => (int) $renderResult['num_tests'] - 1, // -1 for the BeforeEach
];
}

protected function generateTests(): void
{
collect($this->getSelectedResources())
->each(function ($resources, $panelId): void {
collect($resources)
->flatten()
->each(fn (string $resourceClass) => $this->generateTestsForSelectedResource($resourceClass, $panelId));
});
}
}
71 changes: 71 additions & 0 deletions src/Concerns/Commands/InteractsWithUserInput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace CodeWithDennis\FilamentTests\Concerns\Commands;

use Filament\Facades\Filament;
use Filament\Panel;
use Illuminate\Support\Collection;

use function Laravel\Prompts\multiselect;

trait InteractsWithUserInput
{
protected Collection $panels;

protected Collection $resources;

protected function getSelectedPanels(): Collection
{
return $this->panels ??= collect();
}

protected function getSelectedResources(): Collection
{
return $this->resources ??= collect();
}

protected function askUserToSelectPanels(): Collection
{
$allPanels = collect(Filament::getPanels());

if ($allPanels->count() === 1) {
return collect([$allPanels->first()->getId()]);
}

$options = $allPanels->mapWithKeys(fn (Panel $panel) => [$panel->getId() => $panel->getId()])->toArray();

return collect(multiselect(
label: 'Which Filament panel/s do you want to generate tests for?',
options: $options,
required: true,
hint: 'You can select multiple panels',
));
}

protected function askUserToSelectResourcesFromTheSelectedPanels(): Collection
{
$selectedResources = collect();

foreach ($this->getSelectedPanels() as $panelId) {
$resources = collect(Filament::getPanel($panelId)?->getResources() ?? [])
->mapWithKeys(fn ($resource) => [$resource => class_basename($resource)])
->toArray();

if (empty($resources)) {
continue;
}

$selected = multiselect(
label: "Select resources for panel: {$panelId}",
options: $resources,
required: true,
);

if ($selected !== []) {
$selectedResources[$panelId] = $selected;
}
}

return $selectedResources;
}
}
40 changes: 40 additions & 0 deletions src/Concerns/Commands/RendersFilamentTests.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace CodeWithDennis\FilamentTests\Concerns\Commands;

use CodeWithDennis\FilamentTests\TestRenderers\BaseTest;
use CodeWithDennis\FilamentTests\TestRenderers\BeforeEach;
use CodeWithDennis\FilamentTests\TestRenderers\Resources\Pages\Index\CanRenderIndexPageTest;

trait RendersFilamentTests
{
protected function renderTestsForResource(string $resource): array
{
$renderers = collect($this->getRenderers());

$output = $renderers
->map(fn (string $renderer) =>
/** @var BaseTest $renderer */
$renderer::build($resource)->render())
->prepend('<?php')
->implode("\n\n");

return [
'content' => $output,
'num_tests' => $renderers->count(),
];
}

/**
* @return class-string<BaseTest>[]
*/
protected function getRenderers(): array
{
return [
BeforeEach::class,
CanRenderIndexPageTest::class,
// CanRenderCreatePageTest::class,
// CanRenderEditPageTest::class,
];
}
}