mirror of
https://github.com/pestphp/pest.git
synced 2026-09-07 15:23:34 +02:00
fix: livewire
This commit is contained in:
+178
-5
@@ -18,6 +18,19 @@ use PHPUnit\TextUI\Configuration\Registry;
|
|||||||
*/
|
*/
|
||||||
final class Graph
|
final class Graph
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Livewire's generated-file directories, relative to its cache directory,
|
||||||
|
* mapped to the extension each one writes. Only these three land in the
|
||||||
|
* graph — scripts and styles are never rendered or executed by PHP.
|
||||||
|
*
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private const array LIVEWIRE_GENERATED_PATHS = [
|
||||||
|
'/livewire/views/' => '.blade.php',
|
||||||
|
'/livewire/placeholders/' => '.blade.php',
|
||||||
|
'/livewire/classes/' => '.php',
|
||||||
|
];
|
||||||
|
|
||||||
/** @var array<int, string> */
|
/** @var array<int, string> */
|
||||||
private array $files = [];
|
private array $files = [];
|
||||||
|
|
||||||
@@ -103,14 +116,15 @@ final class Graph
|
|||||||
|
|
||||||
$this->applyTestFileChanges($nonMigrationPaths, $affectedSet);
|
$this->applyTestFileChanges($nonMigrationPaths, $affectedSet);
|
||||||
|
|
||||||
$staticallyHandledBlade = $this->applyBladeStaticChanges($nonMigrationPaths, $affectedSet);
|
$handledBlade = $this->applyBladeStaticChanges($nonMigrationPaths, $affectedSet)
|
||||||
|
+ $this->applyLivewireComponentChanges($nonMigrationPaths, $affectedSet);
|
||||||
|
|
||||||
$this->applyWatchPatternFallback(
|
$this->applyWatchPatternFallback(
|
||||||
$nonMigrationPaths,
|
$nonMigrationPaths,
|
||||||
$unparseableMigrations,
|
$unparseableMigrations,
|
||||||
$preciselyHandledPages,
|
$preciselyHandledPages,
|
||||||
$sharedFilesResolved,
|
$sharedFilesResolved,
|
||||||
$staticallyHandledBlade,
|
$handledBlade,
|
||||||
$affectedSet,
|
$affectedSet,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -489,12 +503,171 @@ final class Graph
|
|||||||
return $staticallyHandled;
|
return $staticallyHandled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Livewire compiles single- and multi-file components into generated files
|
||||||
|
* under the (per-worker) compiled view directory, so the graph only ever
|
||||||
|
* holds those generated paths — never the component source the developer
|
||||||
|
* edited. Reproduce Livewire's hash to walk that mapping backwards.
|
||||||
|
*
|
||||||
|
* @param list<string> $nonMigrationPaths
|
||||||
|
* @param array<string, true> $affectedSet
|
||||||
|
* @return array<string, true>
|
||||||
|
*/
|
||||||
|
private function applyLivewireComponentChanges(array $nonMigrationPaths, array &$affectedSet): array
|
||||||
|
{
|
||||||
|
$generatedIds = $this->livewireGeneratedFileIds();
|
||||||
|
|
||||||
|
if ($generatedIds === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var array<int, array<string, true>> $sourcesByGeneratedId */
|
||||||
|
$sourcesByGeneratedId = [];
|
||||||
|
|
||||||
|
foreach ($nonMigrationPaths as $rel) {
|
||||||
|
foreach ($this->livewireSourcePaths($rel) as $sourcePath) {
|
||||||
|
foreach ($generatedIds[$this->livewireHash($sourcePath)] ?? [] as $id) {
|
||||||
|
$sourcesByGeneratedId[$id][$rel] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sourcesByGeneratedId === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$handled = [];
|
||||||
|
|
||||||
|
foreach ($this->edges as $testFile => $ids) {
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
if (! isset($sourcesByGeneratedId[$id])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$affectedSet[$testFile] = true;
|
||||||
|
$handled += $sourcesByGeneratedId[$id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component sources whose Livewire hash a changed file could carry: the
|
||||||
|
* file itself when it is a single-file component, and its directory when it
|
||||||
|
* sits inside a multi-file component — a class or asset sibling of the view
|
||||||
|
* is compiled under the directory's hash, not its own.
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function livewireSourcePaths(string $rel): array
|
||||||
|
{
|
||||||
|
$sourcePaths = [];
|
||||||
|
|
||||||
|
if (str_ends_with($rel, '.blade.php')) {
|
||||||
|
$sourcePaths[] = $rel;
|
||||||
|
}
|
||||||
|
|
||||||
|
$componentDirectory = dirname($rel);
|
||||||
|
|
||||||
|
if ($this->isLivewireMultiFileDirectory($componentDirectory)) {
|
||||||
|
$sourcePaths[] = $componentDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sourcePaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors Livewire\Finder\Finder::hasValidMultiFileComponentSource(): a
|
||||||
|
* multi-file component is a directory holding both "<name>.php" and
|
||||||
|
* "<name>.blade.php", where "<name>" is the directory name with the ⚡
|
||||||
|
* marker stripped, collapsed to "index" for the index convention.
|
||||||
|
*/
|
||||||
|
private function isLivewireMultiFileDirectory(string $componentDirectory): bool
|
||||||
|
{
|
||||||
|
$directoryName = basename($componentDirectory);
|
||||||
|
|
||||||
|
if (str_contains($directoryName, 'index')) {
|
||||||
|
$directoryName = 'index';
|
||||||
|
}
|
||||||
|
|
||||||
|
$componentName = preg_replace('/⚡[\x{FE0E}\x{FE0F}]?/u', '', $directoryName);
|
||||||
|
|
||||||
|
if ($componentName === null || $componentName === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = $this->projectRoot.'/'.$componentDirectory.'/'.$componentName;
|
||||||
|
|
||||||
|
return is_file($source.'.php') && is_file($source.'.blade.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors Livewire\Compiler\CacheManager::getHash(): the first eight hex
|
||||||
|
* digits of md5() over the source path relative to base_path(), leading
|
||||||
|
* separator included. Should Livewire ever change that scheme, nothing
|
||||||
|
* matches and the watch-pattern fallback takes over again.
|
||||||
|
*/
|
||||||
|
private function livewireHash(string $sourcePath): string
|
||||||
|
{
|
||||||
|
return substr(md5(DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $sourcePath)), 0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index every Livewire-generated file already in the graph by its hash. The
|
||||||
|
* same component yields one entry per parallel worker, so a hash maps to a
|
||||||
|
* list of ids rather than a single one.
|
||||||
|
*
|
||||||
|
* @return array<string, list<int>>
|
||||||
|
*/
|
||||||
|
private function livewireGeneratedFileIds(): array
|
||||||
|
{
|
||||||
|
$generated = [];
|
||||||
|
|
||||||
|
foreach ($this->fileIds as $path => $id) {
|
||||||
|
$hash = $this->livewireGeneratedHash($path);
|
||||||
|
|
||||||
|
if ($hash === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$generated[$hash][] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $generated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function livewireGeneratedHash(string $path): ?string
|
||||||
|
{
|
||||||
|
$normalized = '/'.ltrim($path, '/');
|
||||||
|
|
||||||
|
foreach (self::LIVEWIRE_GENERATED_PATHS as $directory => $extension) {
|
||||||
|
if (! str_ends_with($normalized, $extension)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$position = strrpos($normalized, $directory);
|
||||||
|
|
||||||
|
if ($position === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = substr($normalized, $position + strlen($directory), -strlen($extension));
|
||||||
|
|
||||||
|
if (preg_match('/^[0-9a-f]{8}$/', $hash) === 1) {
|
||||||
|
return $hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param list<string> $nonMigrationPaths
|
* @param list<string> $nonMigrationPaths
|
||||||
* @param list<string> $unparseableMigrations
|
* @param list<string> $unparseableMigrations
|
||||||
* @param array<string, true> $preciselyHandledPages
|
* @param array<string, true> $preciselyHandledPages
|
||||||
* @param array<string, true> $sharedFilesResolved
|
* @param array<string, true> $sharedFilesResolved
|
||||||
* @param array<string, true> $staticallyHandledBlade
|
* @param array<string, true> $handledBlade
|
||||||
* @param array<string, true> $affectedSet
|
* @param array<string, true> $affectedSet
|
||||||
*/
|
*/
|
||||||
private function applyWatchPatternFallback(
|
private function applyWatchPatternFallback(
|
||||||
@@ -502,7 +675,7 @@ final class Graph
|
|||||||
array $unparseableMigrations,
|
array $unparseableMigrations,
|
||||||
array $preciselyHandledPages,
|
array $preciselyHandledPages,
|
||||||
array $sharedFilesResolved,
|
array $sharedFilesResolved,
|
||||||
array $staticallyHandledBlade,
|
array $handledBlade,
|
||||||
array &$affectedSet,
|
array &$affectedSet,
|
||||||
): void {
|
): void {
|
||||||
$unknownToGraph = $unparseableMigrations;
|
$unknownToGraph = $unparseableMigrations;
|
||||||
@@ -514,7 +687,7 @@ final class Graph
|
|||||||
if (isset($sharedFilesResolved[$rel])) {
|
if (isset($sharedFilesResolved[$rel])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isset($staticallyHandledBlade[$rel])) {
|
if (isset($handledBlade[$rel])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (! isset($this->fileIds[$rel])) {
|
if (! isset($this->fileIds[$rel])) {
|
||||||
|
|||||||
@@ -1707,6 +1707,12 @@
|
|||||||
✓ a skip outranks an issue triggered on the way to it
|
✓ a skip outranks an issue triggered on the way to it
|
||||||
✓ a suppressed issue is not recorded
|
✓ a suppressed issue is not recorded
|
||||||
|
|
||||||
|
PASS Tests\Features\Tia\LivewireComponents
|
||||||
|
✓ a changed single-file component selects only the tests that rendered it, across workers
|
||||||
|
✓ a changed multi-file component class selects the tests that rendered the component
|
||||||
|
✓ a deleted single-file component selects the tests that rendered it
|
||||||
|
✓ a Blade file with no generated view still falls back to the watch pattern
|
||||||
|
|
||||||
PASS Tests\Features\Tia\PartialRunWriteTier
|
PASS Tests\Features\Tia\PartialRunWriteTier
|
||||||
✓ a filtered run rewrites only the test that ran
|
✓ a filtered run rewrites only the test that ran
|
||||||
✓ a filtered run under --tia announces that tia does not apply
|
✓ a filtered run under --tia announces that tia does not apply
|
||||||
@@ -2106,6 +2112,15 @@
|
|||||||
✓ rerun tracking → it flags cached failures whose file is unknown
|
✓ rerun tracking → it flags cached failures whose file is unknown
|
||||||
✓ applyBladeStaticChanges() → it maps an anonymous index component to the views that render it
|
✓ applyBladeStaticChanges() → it maps an anonymous index component to the views that render it
|
||||||
✓ applyBladeStaticChanges() → it falls back to watch patterns for components with no matched usage
|
✓ applyBladeStaticChanges() → it falls back to watch patterns for components with no matched usage
|
||||||
|
✓ Livewire component views → it maps a changed component to generated views from different workers
|
||||||
|
✓ Livewire component views → it maps documented SFC locations with dataset "default pages namespace"
|
||||||
|
✓ Livewire component views → it maps documented SFC locations with dataset "default layouts namespace without emoji"
|
||||||
|
✓ Livewire component views → it maps documented SFC locations with dataset "additional component location"
|
||||||
|
✓ Livewire component views → it maps documented MFC locations using the component directory hash with dataset "default component location"
|
||||||
|
✓ Livewire component views → it maps documented MFC locations using the component directory hash with dataset "default component location without emoji"
|
||||||
|
✓ Livewire component views → it maps documented MFC locations using the component directory hash with dataset "index convention"
|
||||||
|
✓ Livewire component views → it preserves direct view edges for class-based components
|
||||||
|
✓ Livewire component views → it falls back to watch patterns when no generated view matches
|
||||||
✓ markKnownTestFiles() → it makes a test file with no edges known
|
✓ markKnownTestFiles() → it makes a test file with no edges known
|
||||||
✓ markKnownTestFiles() → it does not clobber edges of an already-known test file
|
✓ markKnownTestFiles() → it does not clobber edges of an already-known test file
|
||||||
✓ markKnownTestFiles() → it ignores paths outside the project root
|
✓ markKnownTestFiles() → it ignores paths outside the project root
|
||||||
@@ -2417,4 +2432,4 @@
|
|||||||
✓ pass with dataset with ('my-datas-set-value')
|
✓ pass with dataset with ('my-datas-set-value')
|
||||||
✓ within describe → pass with dataset with ('my-datas-set-value')
|
✓ within describe → pass with dataset with ('my-datas-set-value')
|
||||||
|
|
||||||
Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1743 passed (3953 assertions)
|
Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1756 passed (3974 assertions)
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Tests\Fixtures\Tia\Project;
|
||||||
|
|
||||||
|
afterEach(function (): void {
|
||||||
|
Project::destroyAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Livewire never renders a single- or multi-file component source directly: it
|
||||||
|
* compiles it into `<compiled views>/livewire/{views,classes}/<hash>.<ext>` and
|
||||||
|
* renders that, so the recorded graph only ever holds the generated path. These
|
||||||
|
* helpers seed the graph the way a real recording run would leave it.
|
||||||
|
*/
|
||||||
|
function tiaLivewireHash(string $sourcePath): string
|
||||||
|
{
|
||||||
|
return substr(md5(DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $sourcePath)), 0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $generatedToTest generated path → test file that rendered it
|
||||||
|
*/
|
||||||
|
function tiaSeedWithGeneratedViews(Project $project, array $generatedToTest): void
|
||||||
|
{
|
||||||
|
$project->seed('master');
|
||||||
|
|
||||||
|
$project->mutateGraph(function (array $graph) use ($generatedToTest): array {
|
||||||
|
foreach ($generatedToTest as $generated => $testFile) {
|
||||||
|
$id = count($graph['files']);
|
||||||
|
$graph['files'][$id] = $generated;
|
||||||
|
$graph['edges'][$testFile][] = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $graph;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('a changed single-file component selects only the tests that rendered it, across workers', function (): void {
|
||||||
|
$project = Project::make('master', 'livewire-watch');
|
||||||
|
$project->write('resources/views/pages/⚡orders.blade.php', "<div>orders</div>\n");
|
||||||
|
$project->git()->commit('add the component');
|
||||||
|
|
||||||
|
$hash = tiaLivewireHash('resources/views/pages/⚡orders.blade.php');
|
||||||
|
|
||||||
|
// The same component, compiled once per parallel worker.
|
||||||
|
tiaSeedWithGeneratedViews($project, [
|
||||||
|
'storage/framework/views/test_1/livewire/views/'.$hash.'.blade.php' => 'tests/Unit/GreeterTest.php',
|
||||||
|
'storage/framework/views/test_2/livewire/views/'.$hash.'.blade.php' => 'tests/Unit/CalculatorTest.php',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$project->write('resources/views/pages/⚡orders.blade.php', "<div>orders v2</div>\n");
|
||||||
|
$project->snapshot();
|
||||||
|
|
||||||
|
$result = $project->pest('--tia');
|
||||||
|
|
||||||
|
expect($result->exitCode)->toBe(0, $result->describe())
|
||||||
|
->and($result->affected())->toBe(4, $result->describe())
|
||||||
|
->and($result->replayed())->toBe(2, $result->describe());
|
||||||
|
})->skipOnWindows();
|
||||||
|
|
||||||
|
test('a changed multi-file component class selects the tests that rendered the component', function (): void {
|
||||||
|
$project = Project::make('master', 'livewire-watch');
|
||||||
|
$project->write('resources/views/components/⚡counter/counter.blade.php', "<div>{{ \$count }}</div>\n");
|
||||||
|
$project->write('resources/views/components/⚡counter/counter.php', "<?php\n\nreturn 1;\n");
|
||||||
|
$project->git()->commit('add the component');
|
||||||
|
|
||||||
|
// A multi-file component is compiled under the hash of its *directory*, and
|
||||||
|
// its class is what PHP executes — so the class sibling, not the view, is
|
||||||
|
// what the graph can be reached through.
|
||||||
|
tiaSeedWithGeneratedViews($project, [
|
||||||
|
'storage/framework/views/livewire/classes/'.tiaLivewireHash('resources/views/components/⚡counter').'.php' => 'tests/Unit/GreeterTest.php',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$project->write('resources/views/components/⚡counter/counter.php', "<?php\n\nreturn 2;\n");
|
||||||
|
$project->snapshot();
|
||||||
|
|
||||||
|
$result = $project->pest('--tia');
|
||||||
|
|
||||||
|
expect($result->exitCode)->toBe(0, $result->describe())
|
||||||
|
->and($result->affected())->toBe(2, $result->describe())
|
||||||
|
->and($result->replayed())->toBe(4, $result->describe());
|
||||||
|
})->skipOnWindows();
|
||||||
|
|
||||||
|
test('a deleted single-file component selects the tests that rendered it', function (): void {
|
||||||
|
$project = Project::make('master', 'livewire-watch');
|
||||||
|
$project->write('resources/views/pages/⚡orders.blade.php', "<div>orders</div>\n");
|
||||||
|
$project->git()->commit('add the component');
|
||||||
|
|
||||||
|
tiaSeedWithGeneratedViews($project, [
|
||||||
|
'storage/framework/views/livewire/views/'.tiaLivewireHash('resources/views/pages/⚡orders.blade.php').'.blade.php' => 'tests/Unit/GreeterTest.php',
|
||||||
|
]);
|
||||||
|
|
||||||
|
unlink($project->path('resources/views/pages/⚡orders.blade.php'));
|
||||||
|
$project->snapshot();
|
||||||
|
|
||||||
|
$result = $project->pest('--tia');
|
||||||
|
|
||||||
|
expect($result->exitCode)->toBe(0, $result->describe())
|
||||||
|
->and($result->affected())->toBe(2, $result->describe())
|
||||||
|
->and($result->replayed())->toBe(4, $result->describe());
|
||||||
|
})->skipOnWindows();
|
||||||
|
|
||||||
|
test('a Blade file with no generated view still falls back to the watch pattern', function (): void {
|
||||||
|
$project = Project::make('master', 'livewire-watch');
|
||||||
|
$project->write('resources/views/pages/⚡orders.blade.php', "<div>orders</div>\n");
|
||||||
|
$project->write('resources/views/unrelated.blade.php', "<div>unrelated</div>\n");
|
||||||
|
$project->git()->commit('add the views');
|
||||||
|
|
||||||
|
tiaSeedWithGeneratedViews($project, [
|
||||||
|
'storage/framework/views/livewire/views/'.tiaLivewireHash('resources/views/pages/⚡orders.blade.php').'.blade.php' => 'tests/Unit/GreeterTest.php',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$project->write('resources/views/unrelated.blade.php', "<div>unrelated v2</div>\n");
|
||||||
|
$project->snapshot();
|
||||||
|
|
||||||
|
$result = $project->pest('--tia');
|
||||||
|
|
||||||
|
expect($result->exitCode)->toBe(0, $result->describe())
|
||||||
|
->and($result->affected())->toBe(Project::TOTAL_TESTS, $result->describe())
|
||||||
|
->and($result->replayed())->toBe(0, $result->describe());
|
||||||
|
})->skipOnWindows();
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__.'/../app/Calculator.php';
|
||||||
|
require_once __DIR__.'/../app/Greeter.php';
|
||||||
|
|
||||||
|
// Stands in for the built-in Laravel/Livewire watch defaults, which are not
|
||||||
|
// applicable here because the fixture installs neither package. Without the
|
||||||
|
// generated-view mapping, every changed Blade file lands on this rule and
|
||||||
|
// invalidates the whole suite — which is exactly what issue #1808 reported.
|
||||||
|
pest()->tia()->watch(['resources/views/**' => 'tests']);
|
||||||
@@ -147,6 +147,121 @@ describe('applyBladeStaticChanges()', function (): void {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Livewire component views', function (): void {
|
||||||
|
beforeEach(function (): void {
|
||||||
|
$this->projectRoot = sys_get_temp_dir().'/pest-tia-livewire-sfc-'.bin2hex(random_bytes(4));
|
||||||
|
mkdir($this->projectRoot, 0755, true);
|
||||||
|
|
||||||
|
$this->watchPatterns = new WatchPatterns;
|
||||||
|
$this->watchPatterns->add(['resources/views/**' => 'tests/Feature']);
|
||||||
|
Container::getInstance()->add(WatchPatterns::class, $this->watchPatterns);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function (): void {
|
||||||
|
$files = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($this->projectRoot, FilesystemIterator::SKIP_DOTS),
|
||||||
|
RecursiveIteratorIterator::CHILD_FIRST,
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
assert($file instanceof SplFileInfo);
|
||||||
|
|
||||||
|
if ($file->isDir()) {
|
||||||
|
@rmdir($file->getPathname());
|
||||||
|
} else {
|
||||||
|
@unlink($file->getPathname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@rmdir($this->projectRoot);
|
||||||
|
|
||||||
|
Container::getInstance()->add(WatchPatterns::class, new WatchPatterns);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps a changed component to generated views from different workers', function (): void {
|
||||||
|
$ordersPath = 'resources/views/components/orders.blade.php';
|
||||||
|
$usersPath = 'resources/views/components/admin/⚡users.blade.php';
|
||||||
|
$ordersHash = substr(md5(DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $ordersPath)), 0, 8);
|
||||||
|
$usersHash = substr(md5(DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $usersPath)), 0, 8);
|
||||||
|
|
||||||
|
mkdir(dirname($this->projectRoot.'/'.$ordersPath), 0755, true);
|
||||||
|
mkdir(dirname($this->projectRoot.'/'.$usersPath), 0755, true);
|
||||||
|
file_put_contents($this->projectRoot.'/'.$ordersPath, '<div>Orders</div>');
|
||||||
|
file_put_contents($this->projectRoot.'/'.$usersPath, '<div>Users</div>');
|
||||||
|
|
||||||
|
$graph = new Graph($this->projectRoot);
|
||||||
|
$graph->link('tests/Feature/OrdersTest.php', 'storage/framework/views/test_1/livewire/views/'.$ordersHash.'.blade.php');
|
||||||
|
$graph->link('tests/Feature/UsersTest.php', 'storage/framework/views/test_2/livewire/views/'.$usersHash.'.blade.php');
|
||||||
|
|
||||||
|
expect($graph->affected([$ordersPath]))->toBe(['tests/Feature/OrdersTest.php']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps documented SFC locations', function (string $sourcePath): void {
|
||||||
|
$hash = substr(md5(DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $sourcePath)), 0, 8);
|
||||||
|
|
||||||
|
mkdir(dirname($this->projectRoot.'/'.$sourcePath), 0755, true);
|
||||||
|
file_put_contents($this->projectRoot.'/'.$sourcePath, '<div>Component</div>');
|
||||||
|
|
||||||
|
$graph = new Graph($this->projectRoot);
|
||||||
|
$graph->link('tests/Feature/ComponentTest.php', 'storage/framework/views/test_3/livewire/views/'.$hash.'.blade.php');
|
||||||
|
$graph->link('tests/Feature/UnrelatedTest.php', 'storage/framework/views/test_4/livewire/views/deadbeef.blade.php');
|
||||||
|
|
||||||
|
expect($graph->affected([$sourcePath]))->toBe(['tests/Feature/ComponentTest.php']);
|
||||||
|
})->with([
|
||||||
|
'default pages namespace' => ['resources/views/pages/post/⚡create.blade.php'],
|
||||||
|
'default layouts namespace without emoji' => ['resources/views/layouts/app.blade.php'],
|
||||||
|
'additional component location' => ['resources/views/widgets/orders.blade.php'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('maps documented MFC locations using the component directory hash', function (string $componentDirectory, string $viewPath): void {
|
||||||
|
$hash = substr(md5(DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $componentDirectory)), 0, 8);
|
||||||
|
$classPath = $componentDirectory.'/'.basename($viewPath, '.blade.php').'.php';
|
||||||
|
|
||||||
|
mkdir($this->projectRoot.'/'.$componentDirectory, 0755, true);
|
||||||
|
file_put_contents($this->projectRoot.'/'.$viewPath, '<div>Component</div>');
|
||||||
|
file_put_contents($this->projectRoot.'/'.$classPath, '<?php');
|
||||||
|
|
||||||
|
$graph = new Graph($this->projectRoot);
|
||||||
|
$graph->link('tests/Feature/ComponentTest.php', 'storage/framework/views/test_5/livewire/views/'.$hash.'.blade.php');
|
||||||
|
$graph->link('tests/Feature/UnrelatedTest.php', 'storage/framework/views/test_6/livewire/views/deadbeef.blade.php');
|
||||||
|
|
||||||
|
expect($graph->affected([$viewPath]))->toBe(['tests/Feature/ComponentTest.php']);
|
||||||
|
})->with([
|
||||||
|
'default component location' => ['resources/views/components/post/⚡create', 'resources/views/components/post/⚡create/create.blade.php'],
|
||||||
|
'default component location without emoji' => ['resources/views/components/post/create', 'resources/views/components/post/create/create.blade.php'],
|
||||||
|
'index convention' => ['resources/views/components/post/⚡index', 'resources/views/components/post/⚡index/index.blade.php'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('preserves direct view edges for class-based components', function (): void {
|
||||||
|
$viewPath = 'resources/views/livewire/create-post.blade.php';
|
||||||
|
|
||||||
|
mkdir(dirname($this->projectRoot.'/'.$viewPath), 0755, true);
|
||||||
|
file_put_contents($this->projectRoot.'/'.$viewPath, '<div>Create post</div>');
|
||||||
|
|
||||||
|
$graph = new Graph($this->projectRoot);
|
||||||
|
$graph->link('tests/Feature/CreatePostTest.php', $viewPath);
|
||||||
|
|
||||||
|
expect($graph->affected([$viewPath]))->toBe(['tests/Feature/CreatePostTest.php']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to watch patterns when no generated view matches', function (): void {
|
||||||
|
$ordersPath = 'resources/views/components/orders.blade.php';
|
||||||
|
$ordersHash = substr(md5(DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $ordersPath)), 0, 8);
|
||||||
|
$unmatchedPath = 'resources/views/components/unmatched.blade.php';
|
||||||
|
|
||||||
|
mkdir(dirname($this->projectRoot.'/'.$ordersPath), 0755, true);
|
||||||
|
file_put_contents($this->projectRoot.'/'.$ordersPath, '<div>Orders</div>');
|
||||||
|
file_put_contents($this->projectRoot.'/'.$unmatchedPath, '<div>Unmatched</div>');
|
||||||
|
|
||||||
|
$graph = new Graph($this->projectRoot);
|
||||||
|
$graph->link('tests/Feature/OrdersTest.php', 'storage/framework/views/test_1/livewire/views/'.$ordersHash.'.blade.php');
|
||||||
|
$graph->link('tests/Feature/UsersTest.php', 'storage/framework/views/test_2/livewire/views/deadbeef.blade.php');
|
||||||
|
|
||||||
|
expect($graph->affected([$unmatchedPath]))
|
||||||
|
->toBe(['tests/Feature/OrdersTest.php', 'tests/Feature/UsersTest.php']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('markKnownTestFiles()', function (): void {
|
describe('markKnownTestFiles()', function (): void {
|
||||||
it('makes a test file with no edges known', function (): void {
|
it('makes a test file with no edges known', function (): void {
|
||||||
$graph = new Graph(sys_get_temp_dir());
|
$graph = new Graph(sys_get_temp_dir());
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ test('parallel', function () use ($run): void {
|
|||||||
$file = file_get_contents(__FILE__);
|
$file = file_get_contents(__FILE__);
|
||||||
$file = preg_replace(
|
$file = preg_replace(
|
||||||
'/\$expected = \'.*?\';/',
|
'/\$expected = \'.*?\';/',
|
||||||
"\$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1531 passed (3334 assertions)';",
|
"\$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1540 passed (3343 assertions)';",
|
||||||
$file,
|
$file,
|
||||||
);
|
);
|
||||||
file_put_contents(__FILE__, $file);
|
file_put_contents(__FILE__, $file);
|
||||||
}
|
}
|
||||||
|
|
||||||
$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1531 passed (3334 assertions)';
|
$expected = '1 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1540 passed (3343 assertions)';
|
||||||
|
|
||||||
expect($output)
|
expect($output)
|
||||||
->toContain("Tests: {$expected}")
|
->toContain("Tests: {$expected}")
|
||||||
|
|||||||
Reference in New Issue
Block a user