Compare commits

...

8 Commits

Author SHA1 Message Date
nuno maduro 2c7cc1dc87 fix: static callable datasets 2026-08-13 18:38:27 +01:00
Lazizbek Ergashev 9b36f44b3e fix(tia): include the PHP minor version in the fingerprint (#1860) 2026-08-13 16:16:15 +01:00
Lazizbek Ergashev 2e58918201 fix(tia): treat a test skipped from a hook as finished (#1863) 2026-08-13 16:15:45 +01:00
nuno maduro a8d4770c0b chore: fix version 2026-08-12 15:37:18 +01:00
nuno maduro 208f447a10 chore: style 2026-08-12 15:29:21 +01:00
Daniel Polito 6b7ca13660 feat: allow configuring the TIA directory (#1787)
* feat: allow configuring the TIA directory

* refactor: simplify TIA directory resolution

* fix: bootstrap TIA state after configuration

* chore: apply Rector to TIA storage test

* fix: stabilize checks on pull requests

* fix: align TIA fixture branch resolution

* fix: preserve success snapshot formatting

* fix: order TIA storage snapshot

* chore: keep TIA directory changes scoped
2026-08-12 14:53:45 +01:00
nuno maduro bf7c6b3134 fix(tia): casing issues 2026-08-11 01:47:14 +01:00
Caleb White 617226ca01 fix: fail missing snapshots in CI (#1842)
Prevent snapshot assertions from silently creating missing snapshots during CI runs unless --update-snapshots is enabled.
2026-08-11 01:02:44 +01:00
22 changed files with 402 additions and 25 deletions
+19
View File
@@ -190,6 +190,24 @@ async function listPageFiles(pagesDir) {
return out return out
} }
// `existsSync()` ignores casing on APFS and NTFS, and on a macOS bind mount seen from
// inside a Linux container, so the `resources/js/Pages` candidate resolves in a project
// whose directory is really `resources/js/pages`. Every page path derives from the
// directory accepted below, so a wrong-cased one detaches the pages tree from the paths
// the PHP side reports. `readdir()` is case-exact everywhere.
export async function matchesDiskCasing(projectRoot, rel) {
let current = projectRoot
for (const segment of rel.split('/')) {
let entries
try { entries = await readdir(current) } catch { return false }
if (!entries.includes(segment)) return false
current = join(current, segment)
}
return true
}
async function discoverPagesDir() { async function discoverPagesDir() {
const override = process.env.TIA_VITE_PAGES_DIR const override = process.env.TIA_VITE_PAGES_DIR
if (override && override.length > 0) { if (override && override.length > 0) {
@@ -199,6 +217,7 @@ async function discoverPagesDir() {
for (const rel of PAGE_DIR_CANDIDATES) { for (const rel of PAGE_DIR_CANDIDATES) {
const abs = resolve(PROJECT_ROOT, rel) const abs = resolve(PROJECT_ROOT, rel)
if (!existsSync(abs)) continue if (!existsSync(abs)) continue
if (!(await matchesDiskCasing(PROJECT_ROOT, rel))) continue
const files = await listPageFiles(abs) const files = await listPageFiles(abs)
if (files.length > 0) return abs if (files.length > 0) return abs
} }
+2 -2
View File
@@ -23,7 +23,7 @@
"nunomaduro/termwind": "^2.4.0", "nunomaduro/termwind": "^2.4.0",
"pestphp/pest-plugin": "^5.0.0", "pestphp/pest-plugin": "^5.0.0",
"pestphp/pest-plugin-arch": "^5.0.0", "pestphp/pest-plugin-arch": "^5.0.0",
"pestphp/pest-plugin-mutate": "^5.0.1", "pestphp/pest-plugin-mutate": "^5.0.2",
"pestphp/pest-plugin-profanity": "^5.0.0", "pestphp/pest-plugin-profanity": "^5.0.0",
"phpunit/phpunit": "^13.3.0", "phpunit/phpunit": "^13.3.0",
"symfony/process": "^8.1.0" "symfony/process": "^8.1.0"
@@ -60,7 +60,7 @@
}, },
"require-dev": { "require-dev": {
"pestphp/pest-dev-tools": "^5.0.0", "pestphp/pest-dev-tools": "^5.0.0",
"pestphp/pest-plugin-browser": "^5.0.0", "pestphp/pest-plugin-browser": "^5.0.1",
"pestphp/pest-plugin-phpstan": "^5.0.2", "pestphp/pest-plugin-phpstan": "^5.0.2",
"pestphp/pest-plugin-rector": "^5.0.3", "pestphp/pest-plugin-rector": "^5.0.3",
"pestphp/pest-plugin-type-coverage": "^5.0.2", "pestphp/pest-plugin-type-coverage": "^5.0.2",
+1 -1
View File
@@ -37,9 +37,9 @@ final class Kernel
private const array BOOTSTRAPPERS = [ private const array BOOTSTRAPPERS = [
Bootstrappers\BootOverrides::class, Bootstrappers\BootOverrides::class,
Bootstrappers\BootPhpUnitConfiguration::class, Bootstrappers\BootPhpUnitConfiguration::class,
Plugins\Tia\Bootstrapper::class,
Bootstrappers\BootSubscribers::class, Bootstrappers\BootSubscribers::class,
Bootstrappers\BootFiles::class, Bootstrappers\BootFiles::class,
Plugins\Tia\Bootstrapper::class,
Bootstrappers\BootView::class, Bootstrappers\BootView::class,
Bootstrappers\BootKernelDump::class, Bootstrappers\BootKernelDump::class,
Bootstrappers\BootExcludeList::class, Bootstrappers\BootExcludeList::class,
+6
View File
@@ -734,6 +734,12 @@ final class Expectation
}; };
if (! $snapshots->has()) { if (! $snapshots->has()) {
if (! Snapshot::shouldCreateMissingSnapshots()) {
$filename = $snapshots->filename();
Assert::fail($message === '' ? "Snapshot is missing at [$filename]. Run Pest with --update-snapshots to create it." : $message);
}
$filename = $snapshots->save($string); $filename = $snapshots->save($string);
TestSuite::getInstance()->registerSnapshotChange("Snapshot created at [$filename]"); TestSuite::getInstance()->registerSnapshotChange("Snapshot created at [$filename]");
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Pest;
function version(): string function version(): string
{ {
return '5.0.5'; return '5.1.1';
} }
function testDirectory(string $file = ''): string function testDirectory(string $file = ''): string
+39
View File
@@ -16,6 +16,36 @@ final class Snapshot implements HandlesArguments
public static bool $updateSnapshots = false; public static bool $updateSnapshots = false;
/**
* @var list<string>
*/
private const array CI_ENVIRONMENT_VARIABLES = [
'CI',
'GITHUB_ACTIONS',
'GITLAB_CI',
'CIRCLECI',
'TRAVIS',
'APPVEYOR',
'BITBUCKET_BUILD_NUMBER',
'BUILDKITE',
'TEAMCITY_VERSION',
'JENKINS_URL',
'SYSTEM_COLLECTIONURI',
'CI_NAME',
'TASKCLUSTER_ROOT_URL',
'DRONE',
'WERCKER',
'NEVERCODE',
'SEMAPHORE',
'NETLIFY',
'NOW_BUILDER',
];
public static function shouldCreateMissingSnapshots(): bool
{
return self::$updateSnapshots || ! self::runningOnCI();
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@@ -119,4 +149,13 @@ final class Snapshot implements HandlesArguments
return true; return true;
} }
private static function runningOnCI(): bool
{
if (Environment::name() === Environment::CI) {
return true;
}
return array_any(self::CI_ENVIRONMENT_VARIABLES, fn (string $environmentVariable): bool => getenv($environmentVariable) !== false);
}
} }
+10
View File
@@ -11,6 +11,16 @@ use Pest\Support\Container;
*/ */
final class Configuration final class Configuration
{ {
/**
* @return $this
*/
public function directory(string $directory): self
{
Storage::useDirectory($directory);
return $this;
}
/** /**
* @return $this * @return $this
*/ */
+1 -1
View File
@@ -40,7 +40,7 @@ final readonly class Fingerprint
'js_config' => self::jsConfigHash($projectRoot), 'js_config' => self::jsConfigHash($projectRoot),
], ],
'environmental' => [ 'environmental' => [
'php_minor' => PHP_MAJOR_VERSION, 'php_minor' => PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION,
], ],
]; ];
+27
View File
@@ -85,6 +85,10 @@ final class JsModuleGraph
continue; continue;
} }
if (! self::matchesDiskCasing($projectRoot, $rel)) {
continue;
}
if (self::dirHasPageFile($abs)) { if (self::dirHasPageFile($abs)) {
return $abs; return $abs;
} }
@@ -93,12 +97,30 @@ final class JsModuleGraph
return null; return null;
} }
private static function matchesDiskCasing(string $projectRoot, string $relative): bool
{
$current = rtrim($projectRoot, DIRECTORY_SEPARATOR);
foreach (explode('/', $relative) as $segment) {
$entries = @scandir($current);
if ($entries === false || ! in_array($segment, $entries, true)) {
return false;
}
$current .= DIRECTORY_SEPARATOR.$segment;
}
return true;
}
private static function dirHasPageFile(string $dir): bool private static function dirHasPageFile(string $dir): bool
{ {
try { try {
$iterator = new \RecursiveIteratorIterator( $iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS), new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::LEAVES_ONLY,
\RecursiveIteratorIterator::CATCH_GET_CHILD,
); );
} catch (\UnexpectedValueException) { } catch (\UnexpectedValueException) {
return false; return false;
@@ -265,9 +287,11 @@ final class JsModuleGraph
if ($jsRoot !== null && is_dir($jsRoot)) { if ($jsRoot !== null && is_dir($jsRoot)) {
$entries = []; $entries = [];
try {
$iterator = new \RecursiveIteratorIterator( $iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($jsRoot, \FilesystemIterator::SKIP_DOTS), new \RecursiveDirectoryIterator($jsRoot, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::LEAVES_ONLY,
\RecursiveIteratorIterator::CATCH_GET_CHILD,
); );
/** @var \SplFileInfo $file */ /** @var \SplFileInfo $file */
@@ -280,6 +304,9 @@ final class JsModuleGraph
.':'.$file->getSize() .':'.$file->getSize()
.':'.$file->getMTime(); .':'.$file->getMTime();
} }
} catch (\UnexpectedValueException|\RuntimeException) {
return null;
}
sort($entries); sort($entries);
+1 -1
View File
@@ -117,7 +117,7 @@ final class ResultCollector
public function hasUnfinishedTest(): bool public function hasUnfinishedTest(): bool
{ {
return $this->currentTestId !== null; return $this->currentTestId !== null && ! isset($this->results[$this->currentTestId]);
} }
public function recordAssertions(string $testId, int $assertions): void public function recordAssertions(string $testId, int $assertions): void
+14
View File
@@ -9,8 +9,17 @@ namespace Pest\Plugins\Tia;
*/ */
final class Storage final class Storage
{ {
private static ?string $directory = null;
public static function tempDir(string $projectRoot): string public static function tempDir(string $projectRoot): string
{ {
if (self::$directory !== null) {
$isAbsolute = str_starts_with(self::$directory, DIRECTORY_SEPARATOR)
|| preg_match('/^[a-z]:[\\\\\/]/i', self::$directory) === 1;
return $isAbsolute ? self::$directory : $projectRoot.DIRECTORY_SEPARATOR.self::$directory;
}
$home = self::homeDir(); $home = self::homeDir();
if ($home === null) { if ($home === null) {
@@ -25,6 +34,11 @@ final class Storage
.DIRECTORY_SEPARATOR.self::projectKey($projectRoot); .DIRECTORY_SEPARATOR.self::projectKey($projectRoot);
} }
public static function useDirectory(?string $directory): void
{
self::$directory = $directory;
}
public static function purge(string $projectRoot): void public static function purge(string $projectRoot): void
{ {
$dir = self::tempDir($projectRoot); $dir = self::tempDir($projectRoot);
+1 -1
View File
@@ -141,7 +141,7 @@ final class DatasetsRepository
$datasets[$index] = self::getScopedDataset($data, $currentTestFile); $datasets[$index] = self::getScopedDataset($data, $currentTestFile);
} }
if (is_callable($datasets[$index])) { if (! is_array($datasets[$index]) && is_callable($datasets[$index])) {
$datasets[$index] = call_user_func($datasets[$index]); $datasets[$index] = call_user_func($datasets[$index]);
} }
+6 -1
View File
@@ -56,7 +56,12 @@ final class SnapshotRepository
file_put_contents($snapshotFilename, $snapshot); file_put_contents($snapshotFilename, $snapshot);
return str_replace(dirname($this->testsPath).'/', '', $snapshotFilename); return $this->filename();
}
public function filename(): string
{
return str_replace(dirname($this->testsPath).'/', '', $this->getSnapshotFilename());
} }
public function flush(): void public function flush(): void
@@ -1,5 +1,5 @@
Pest Testing Framework 5.0.5. Pest Testing Framework 5.1.1.
USAGE: pest <file> [options] USAGE: pest <file> [options]
@@ -1,3 +1,3 @@
Pest Testing Framework 5.0.5. Pest Testing Framework 5.1.1.
+18 -1
View File
@@ -1764,6 +1764,7 @@
✓ it shows the correct description for long texts with newlines ✓ it shows the correct description for long texts with newlines
✓ it shows the correct description for arrays with many elements ✓ it shows the correct description for arrays with many elements
✓ it shows the correct description of datasets with html ✓ it shows the correct description of datasets with html
✓ it does not treat a two element dataset of class names as a callable
PASS Tests\Unit\Expectations\OppositeExpectation PASS Tests\Unit\Expectations\OppositeExpectation
✓ it throw expectation failed exception with string argument ✓ it throw expectation failed exception with string argument
@@ -1906,6 +1907,14 @@
✓ does not throw when an integer --random-order-seed is passed as a separate argv element ✓ does not throw when an integer --random-order-seed is passed as a separate argv element
✓ still detects --tia when an integer argument is present ✓ still detects --tia when an integer argument is present
PASS Tests\Unit\Plugins\Tia\JsModuleGraph
✓ it accepts a page directory candidate only when every segment matches the casing on disk with dataset "exact"
✓ it accepts a page directory candidate only when every segment matches the casing on disk with dataset "wrong leaf"
✓ it accepts a page directory candidate only when every segment matches the casing on disk with dataset "wrong parent"
✓ it accepts a page directory candidate only when every segment matches the casing on disk with dataset "absent"
✓ it resolves the pages directory with the casing it has on disk
✓ it fingerprints a project whose js tree holds a directory it cannot open
PASS Tests\Unit\Plugins\Tia\Lockfiles\PackageLock PASS Tests\Unit\Plugins\Tia\Lockfiles\PackageLock
✓ it applies only to package-lock.json ✓ it applies only to package-lock.json
✓ it returns null for contents that are not an npm lockfile ✓ it returns null for contents that are not an npm lockfile
@@ -1928,6 +1937,10 @@
✓ it records assertions against the same per-dataset key ✓ it records assertions against the same per-dataset key
✓ it leaves a test without a dataset keyed by class and method ✓ it leaves a test without a dataset keyed by class and method
PASS Tests\Unit\Plugins\Tia\Storage
✓ it uses a project-relative configured directory
✓ it uses an absolute configured directory
PASS Tests\Unit\Plugins\Tia\TableExtractor PASS Tests\Unit\Plugins\Tia\TableExtractor
✓ fromSql() → it extracts tables from plain DML ✓ fromSql() → it extracts tables from plain DML
✓ fromSql() → it extracts tables from joins ✓ fromSql() → it extracts tables from joins
@@ -2021,6 +2034,10 @@
✓ it builds the expected alias map from a vite config with ('laravel-plugin-default') ✓ it builds the expected alias map from a vite config with ('laravel-plugin-default')
✓ it builds the expected alias map from a vite config with ('no-alias-no-plugin') ✓ it builds the expected alias map from a vite config with ('no-alias-no-plugin')
✓ it builds the expected alias map from a vite config with ('no-config') ✓ it builds the expected alias map from a vite config with ('no-config')
✓ it accepts a page directory candidate only when it matches the casing on disk with ('exact')
✓ it accepts a page directory candidate only when it matches the casing on disk with ('wrong leaf')
✓ it accepts a page directory candidate only when it matches the casing on disk with ('wrong parent')
✓ it accepts a page directory candidate only when it matches the casing on disk with ('absent')
PASS Tests\Unit\Preset PASS Tests\Unit\Preset
✓ preset invalid name ✓ preset invalid name
@@ -2209,4 +2226,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: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1560 passed (3400 assertions) Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 40 todos, 35 skipped, 1573 passed (3414 assertions)
@@ -33,6 +33,34 @@ test('a complete run prunes a deleted test', function (array $arguments): void {
->and($delta->structureMoved())->toBeFalse($delta->summary()); ->and($delta->structureMoved())->toBeFalse($delta->summary());
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows(); })->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
test('a complete run stays complete when the last test is skipped from a hook', function (array $arguments): void {
$project = Project::make('master');
$project->seed('master');
$project->write('tests/Unit/GreeterTest.php', <<<'PHP'
<?php
declare(strict_types=1);
use Fixture\App\Greeter;
beforeEach(function (): void {
test()->markTestSkipped('not today');
});
test('greets a person', function (): void {
expect((new Greeter)->greet('Nuno'))->toBe('Hello, Nuno!');
});
PHP);
$result = $project->pest(...$arguments);
$delta = $project->delta();
expect($result->tally())->toContain('1 skipped')
->and($delta->removed())->toBe(1, $delta->summary())
->and($delta->added())->toBe(0, $delta->summary());
})->with(Project::SEQUENTIAL_AND_PARALLEL)->skipOnWindows();
test('a complete run records nothing for a test file the graph does not know', function (): void { test('a complete run records nothing for a test file the graph does not know', function (): void {
$project = Project::make('master'); $project = Project::make('master');
$project->seed('master'); $project->seed('master');
+24
View File
@@ -109,3 +109,27 @@ it('shows the correct description of datasets with html', function (): void {
expect($descriptions[0])->toBe('(\'<div class="flex items-center"></div>\')'); expect($descriptions[0])->toBe('(\'<div class="flex items-center"></div>\')');
}); });
it('does not treat a two element dataset of class names as a callable', function (): void {
$datasets = DatasetsRepository::resolve([
[
MagicCallDataset::class,
AnotherMagicCallDataset::class,
],
], __FILE__);
expect(array_values($datasets))->toBe([
[MagicCallDataset::class],
[AnotherMagicCallDataset::class],
]);
});
class MagicCallDataset
{
public static function __callStatic(string $name, array $arguments): void
{
throw new RuntimeException('This dataset should not be called.');
}
}
class AnotherMagicCallDataset {}
+86
View File
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
use Pest\Plugins\Tia\JsModuleGraph;
function tiaJsModuleGraphCall(string $method, mixed ...$arguments): mixed
{
return new ReflectionMethod(JsModuleGraph::class, $method)->invoke(null, ...$arguments);
}
function tiaJsModuleGraphProject(): string
{
$root = sys_get_temp_dir().'/pest-tia-js-module-graph-'.bin2hex(random_bytes(6));
mkdir($root.'/resources/js/pages', 0755, true);
file_put_contents($root.'/vite.config.ts', "export default {}\n");
file_put_contents($root.'/resources/js/pages/Dashboard.vue', "<template>ok</template>\n");
return $root;
}
function tiaJsModuleGraphRemove(string $path): void
{
if (! is_dir($path)) {
@unlink($path);
return;
}
@chmod($path, 0755);
$entries = @scandir($path);
foreach ($entries === false ? [] : $entries as $entry) {
if ($entry === '.') {
continue;
}
if ($entry === '..') {
continue;
}
tiaJsModuleGraphRemove($path.'/'.$entry);
}
@rmdir($path);
}
beforeEach(function (): void {
$this->projectRoot = tiaJsModuleGraphProject();
});
afterEach(function (): void {
tiaJsModuleGraphRemove($this->projectRoot);
});
it('accepts a page directory candidate only when every segment matches the casing on disk', function (string $candidate, bool $expected): void {
expect(tiaJsModuleGraphCall('matchesDiskCasing', $this->projectRoot, $candidate))->toBe($expected);
})->with([
'exact' => ['resources/js/pages', true],
'wrong leaf' => ['resources/js/Pages', false],
'wrong parent' => ['Resources/js/pages', false],
'absent' => ['assets/js/pages', false],
]);
it('resolves the pages directory with the casing it has on disk', function (): void {
$expected = $this->projectRoot
.DIRECTORY_SEPARATOR.'resources'
.DIRECTORY_SEPARATOR.'js'
.DIRECTORY_SEPARATOR.'pages';
expect(tiaJsModuleGraphCall('firstExistingPagesDir', $this->projectRoot))->toBe($expected);
});
it('fingerprints a project whose js tree holds a directory it cannot open', function (): void {
$locked = $this->projectRoot.'/resources/js/locked';
mkdir($locked, 0755, true);
file_put_contents($locked.'/Secret.vue', "<template>ok</template>\n");
chmod($locked, 0000);
if (is_readable($locked)) {
$this->markTestSkipped('the current user reads directories regardless of their mode.');
}
expect(tiaJsModuleGraphCall('fingerprint', $this->projectRoot))->toBeString();
});
+39
View File
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
use Pest\Plugins\Tia\Bootstrapper;
use Pest\Plugins\Tia\Configuration;
use Pest\Plugins\Tia\Contracts\State;
use Pest\Plugins\Tia\FileState;
use Pest\Plugins\Tia\Storage;
use Pest\Support\Container;
use Pest\TestSuite;
afterEach(function (): void {
Storage::useDirectory(null);
});
it('uses a project-relative configured directory', function (): void {
$container = new Container;
$testSuite = new TestSuite(sys_get_temp_dir(), 'tests');
$container->add(TestSuite::class, $testSuite);
(new Configuration)->directory('.pest/tia');
new Bootstrapper($container)->boot();
$state = $container->get(State::class);
if (! $state instanceof FileState) {
throw new RuntimeException('Expected the TIA state to use file storage.');
}
expect(Storage::tempDir('/project'))->toBe('/project'.DIRECTORY_SEPARATOR.'.pest/tia')
->and($state->pathFor('graph.json'))->toBe($testSuite->rootPath.DIRECTORY_SEPARATOR.'.pest/tia/graph.json');
});
it('uses an absolute configured directory', function (): void {
(new Configuration)->directory('/tmp/pest-tia');
expect(Storage::tempDir('/project'))->toBe('/tmp/pest-tia');
});
+63
View File
@@ -357,6 +357,63 @@ function tiaViteAliasResults(): array
return $cache = ['roots' => $roots, 'aliases' => $aliases]; return $cache = ['roots' => $roots, 'aliases' => $aliases];
} }
function tiaViteCasingFixtures(): array
{
return [
'exact' => ['resources/js/pages', true],
'wrong leaf' => ['resources/js/Pages', false],
'wrong parent' => ['Resources/js/pages', false],
'absent' => ['assets/js/pages', false],
];
}
function tiaViteCasingResults(): array
{
static $cache = null;
if ($cache !== null) {
return $cache;
}
$root = sys_get_temp_dir().'/pest-tia-vite-casing-'.bin2hex(random_bytes(6));
mkdir($root.'/resources/js/pages', 0755, true);
file_put_contents($root.'/resources/js/pages/Dashboard.vue', "<template>ok</template>\n");
$helper = str_replace('\\', '/', tiaViteHelperPath());
$normalized = str_replace('\\', '/', $root);
$payload = [];
foreach (tiaViteCasingFixtures() as $name => [$candidate]) {
$payload[] = ['name' => $name, 'candidate' => $candidate];
}
$inputFile = tempnam(sys_get_temp_dir(), 'tia-vite-casing-');
file_put_contents($inputFile, json_encode($payload));
$input = str_replace('\\', '/', $inputFile);
$script = <<<JS
import { matchesDiskCasing } from '{$helper}'
import { readFileSync } from 'node:fs'
const cases = JSON.parse(readFileSync('{$input}', 'utf8'))
const out = {}
for (const c of cases) out[c.name] = await matchesDiskCasing('{$normalized}', c.candidate)
process.stdout.write(JSON.stringify(out))
JS;
$process = new Process(['node', '--input-type=module', '-e', $script]);
$process->mustRun();
$results = json_decode($process->getOutput(), true, flags: JSON_THROW_ON_ERROR);
@unlink($inputFile);
@unlink($root.'/resources/js/pages/Dashboard.vue');
@rmdir($root.'/resources/js/pages');
@rmdir($root.'/resources/js');
@rmdir($root.'/resources');
@rmdir($root);
return $cache = $results;
}
beforeEach(function (): void { beforeEach(function (): void {
if ((new ExecutableFinder)->find('node') === null) { if ((new ExecutableFinder)->find('node') === null) {
$this->markTestSkipped('node is not available.'); $this->markTestSkipped('node is not available.');
@@ -409,3 +466,9 @@ it('builds the expected alias map from a vite config', function (string $name):
expect($results['aliases'][$name])->toEqual($expected); expect($results['aliases'][$name])->toEqual($expected);
})->with(array_keys(tiaViteAliasFixtures())); })->with(array_keys(tiaViteAliasFixtures()));
it('accepts a page directory candidate only when it matches the casing on disk', function (string $name): void {
[, $expected] = tiaViteCasingFixtures()[$name];
expect(tiaViteCasingResults()[$name])->toBe($expected);
})->with(array_keys(tiaViteCasingFixtures()));
+2 -2
View File
@@ -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 = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1542 passed (3345 assertions)';", "\$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1555 passed (3359 assertions)';",
$file, $file,
); );
file_put_contents(__FILE__, $file); file_put_contents(__FILE__, $file);
} }
$expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1542 passed (3345 assertions)'; $expected = '2 deprecated, 4 warnings, 5 incomplete, 3 notices, 40 todos, 27 skipped, 1555 passed (3359 assertions)';
expect($output) expect($output)
->toContain("Tests: {$expected}") ->toContain("Tests: {$expected}")