fix(tia): casing issues

This commit is contained in:
nuno maduro
2026-08-11 01:47:11 +01:00
parent 617226ca01
commit bf7c6b3134
7 changed files with 224 additions and 17 deletions
+13 -1
View File
@@ -1906,6 +1906,14 @@
✓ 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
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
✓ it applies only to package-lock.json
✓ it returns null for contents that are not an npm lockfile
@@ -2021,6 +2029,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 ('no-alias-no-plugin')
✓ 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
✓ preset invalid name
@@ -2209,4 +2221,4 @@
✓ 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, 1570 passed (3410 assertions)
+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();
});
+63
View File
@@ -357,6 +357,63 @@ function tiaViteAliasResults(): array
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 {
if ((new ExecutableFinder)->find('node') === null) {
$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);
})->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 = preg_replace(
'/\$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, 1552 passed (3355 assertions)';",
$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, 1552 passed (3355 assertions)';
expect($output)
->toContain("Tests: {$expected}")