mirror of
https://github.com/pestphp/pest.git
synced 2026-06-05 02:52:12 +02:00
Compare commits
5 Commits
7bea819978
...
99cc4e0146
| Author | SHA1 | Date | |
|---|---|---|---|
| 99cc4e0146 | |||
| a47e6f8fef | |||
| 536d79f765 | |||
| 65c0fbc528 | |||
| 9e4cf4b665 |
@ -6,10 +6,22 @@ import { createRequire } from 'node:module'
|
||||
import { resolve, relative, extname, sep, join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const PAGE_EXTENSIONS = new Set(['.vue', '.tsx', '.jsx', '.svelte'])
|
||||
const PAGE_EXTENSIONS = new Set([
|
||||
'.vue', '.svelte',
|
||||
'.tsx', '.jsx',
|
||||
'.ts', '.js',
|
||||
'.mts', '.cts', '.mjs', '.cjs',
|
||||
])
|
||||
const ASSET_EXT_RE = /\.(css|scss|sass|less|styl|stylus|svg|png|jpe?g|gif|webp|avif|ico|bmp|woff2?|ttf|eot|otf|md|mdx|txt|html|mp4|webm|mp3|wav|ogg|m4a|pdf|wasm|glsl|frag|vert)$/i
|
||||
const PROJECT_ROOT = resolve(process.argv[2] ?? process.cwd())
|
||||
const PAGES_REL = (process.env.TIA_VITE_PAGES_DIR ?? 'resources/js/Pages').replace(/\\/g, '/')
|
||||
const PAGE_DIR_CANDIDATES = [
|
||||
'resources/js/Pages',
|
||||
'resources/js/pages',
|
||||
'assets/js/Pages',
|
||||
'assets/js/pages',
|
||||
'assets/Pages',
|
||||
'assets/pages',
|
||||
]
|
||||
|
||||
async function loadRolldown() {
|
||||
const projectRequire = createRequire(join(PROJECT_ROOT, 'package.json'))
|
||||
@ -64,6 +76,22 @@ async function listPageFiles(pagesDir) {
|
||||
return out
|
||||
}
|
||||
|
||||
async function discoverPagesDir() {
|
||||
const override = process.env.TIA_VITE_PAGES_DIR
|
||||
if (override && override.length > 0) {
|
||||
return resolve(PROJECT_ROOT, override.replace(/\\/g, '/'))
|
||||
}
|
||||
|
||||
for (const rel of PAGE_DIR_CANDIDATES) {
|
||||
const abs = resolve(PROJECT_ROOT, rel)
|
||||
if (!existsSync(abs)) continue
|
||||
const files = await listPageFiles(abs)
|
||||
if (files.length > 0) return abs
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function componentNameFor(pageAbs, pagesDir) {
|
||||
const rel = relative(pagesDir, pageAbs).split(sep).join('/')
|
||||
const ext = extname(rel)
|
||||
@ -79,7 +107,13 @@ function isLocalSpecifier(source, aliasKeys) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const pagesDir = resolve(PROJECT_ROOT, PAGES_REL)
|
||||
const pagesDir = await discoverPagesDir()
|
||||
|
||||
if (pagesDir === null) {
|
||||
process.stdout.write('{}')
|
||||
return
|
||||
}
|
||||
|
||||
const pages = await listPageFiles(pagesDir)
|
||||
|
||||
if (pages.length === 0) {
|
||||
|
||||
@ -26,8 +26,8 @@ final readonly class BootSubscribers implements Bootstrapper
|
||||
Subscribers\EnsureKernelDumpIsFlushed::class,
|
||||
Subscribers\EnsureTeamCityEnabled::class,
|
||||
Subscribers\EnsureTiaIsRunningPestTestsOnly::class,
|
||||
Subscribers\EnsureTiaCoverageIsRecorded::class,
|
||||
Subscribers\EnsureTiaCoverageIsFlushed::class,
|
||||
Subscribers\EnsureTiaStarts::class,
|
||||
Subscribers\EnsureTiaEnds::class,
|
||||
Subscribers\EnsureTiaResultsAreCollected::class,
|
||||
Subscribers\EnsureTiaResultIsRecordedOnPassed::class,
|
||||
Subscribers\EnsureTiaResultIsRecordedOnFailed::class,
|
||||
|
||||
@ -9,8 +9,8 @@ use Pest\Exceptions\DatasetArgumentsMismatch;
|
||||
use Pest\Panic;
|
||||
use Pest\Plugins\Tia;
|
||||
use Pest\Plugins\Tia\Collectors;
|
||||
use Pest\Plugins\Tia\Enums\ReplayType;
|
||||
use Pest\Plugins\Tia\Recorder;
|
||||
use Pest\Plugins\Tia\Replay;
|
||||
use Pest\Preset;
|
||||
use Pest\Support\ChainableClosure;
|
||||
use Pest\Support\Container;
|
||||
@ -85,7 +85,7 @@ trait Testable
|
||||
* The active replay mode for this test, set in `setUp()` and checked
|
||||
* in `__runTest()` / `tearDown()` to skip the body and after-each.
|
||||
*/
|
||||
private Replay $__replay = Replay::No;
|
||||
private ReplayType $__replay = ReplayType::None;
|
||||
|
||||
/**
|
||||
* The cached assertion count to replay, captured when entering replay mode.
|
||||
@ -279,16 +279,16 @@ trait Testable
|
||||
/** @var Tia $tia */
|
||||
$tia = Container::getInstance()->get(Tia::class);
|
||||
$status = $tia->getStatus(self::$__filename, $this::class.'::'.$this->name());
|
||||
$replay = Replay::fromStatus($status);
|
||||
$replay = ReplayType::fromStatus($status);
|
||||
|
||||
if ($replay !== Replay::No) {
|
||||
if ($replay !== ReplayType::None) {
|
||||
assert($status !== null);
|
||||
|
||||
match ($replay) {
|
||||
Replay::Pass, Replay::Risky => $this->__beginReplay($replay, $tia),
|
||||
Replay::Skipped => $this->markTestSkipped($status->message()),
|
||||
Replay::Incomplete => $this->markTestIncomplete($status->message()),
|
||||
Replay::Failure => throw new AssertionFailedError($status->message() ?: 'Cached failure'),
|
||||
ReplayType::Pass, ReplayType::Risky => $this->__beginReplay($replay, $tia),
|
||||
ReplayType::Skipped => $this->markTestSkipped($status->message()),
|
||||
ReplayType::Incomplete => $this->markTestIncomplete($status->message()),
|
||||
ReplayType::Failure => throw new AssertionFailedError($status->message() ?: 'Cached failure'),
|
||||
};
|
||||
|
||||
return;
|
||||
@ -314,7 +314,7 @@ trait Testable
|
||||
$this->__callClosure($beforeEach, $arguments);
|
||||
}
|
||||
|
||||
private function __beginReplay(Replay $replay, Tia $tia): void
|
||||
private function __beginReplay(ReplayType $replay, Tia $tia): void
|
||||
{
|
||||
$this->__replay = $replay;
|
||||
$this->__replayAssertions = $tia->getAssertionCount($this::class.'::'.$this->name());
|
||||
@ -353,7 +353,7 @@ trait Testable
|
||||
*/
|
||||
protected function tearDown(...$arguments): void
|
||||
{
|
||||
if ($this->__replay !== Replay::No) {
|
||||
if ($this->__replay !== ReplayType::None) {
|
||||
TestSuite::getInstance()->test = null;
|
||||
|
||||
return;
|
||||
@ -384,8 +384,8 @@ trait Testable
|
||||
*/
|
||||
private function __runTest(Closure $closure, ...$args): mixed
|
||||
{
|
||||
if ($this->__replay === Replay::Pass || $this->__replay === Replay::Risky) {
|
||||
if ($this->__replay === Replay::Pass && $this->__replayAssertions === 0) {
|
||||
if ($this->__replay === ReplayType::Pass || $this->__replay === ReplayType::Risky) {
|
||||
if ($this->__replay === ReplayType::Pass && $this->__replayAssertions === 0) {
|
||||
$this->expectNotToPerformAssertions();
|
||||
}
|
||||
|
||||
|
||||
@ -126,7 +126,7 @@ final class Tia implements AddsOutput, HandlesArguments, Terminable
|
||||
/** @var array{structural: array<string, mixed>, environmental: array<string, mixed>}|null */
|
||||
private ?array $startFingerprint = null;
|
||||
|
||||
private bool $piggybackCoverage = false;
|
||||
private bool $piggybackCoverage = false;
|
||||
|
||||
private bool $recordingActive = false;
|
||||
|
||||
@ -194,12 +194,16 @@ private bool $piggybackCoverage = false;
|
||||
*/
|
||||
public static function isEnabledForRun(array $arguments): bool
|
||||
{
|
||||
if (self::argumentPresent(self::NO_OPTION, $arguments)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$watchPatterns = Container::getInstance()->get(WatchPatterns::class);
|
||||
assert($watchPatterns instanceof WatchPatterns);
|
||||
|
||||
self::applyWatchPatternMarks($arguments, $watchPatterns);
|
||||
|
||||
if (in_array(self::OPTION, $arguments, true) || self::envFlagEnabled(self::ENV_TIA)) {
|
||||
if (self::argumentPresent(self::OPTION, $arguments) || self::envFlagEnabled(self::ENV_TIA)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -207,7 +211,7 @@ private bool $piggybackCoverage = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! ($watchPatterns->isLocally() && in_array('--ci', $arguments, true));
|
||||
return ! ($watchPatterns->isLocally() && self::argumentPresent('--ci', $arguments));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -215,16 +219,37 @@ private bool $piggybackCoverage = false;
|
||||
*/
|
||||
private static function applyWatchPatternMarks(array $arguments, WatchPatterns $watchPatterns): void
|
||||
{
|
||||
if (in_array(self::LOCALLY_OPTION, $arguments, true) || self::envFlagEnabled(self::ENV_LOCALLY)) {
|
||||
if (self::argumentPresent(self::LOCALLY_OPTION, $arguments) || self::envFlagEnabled(self::ENV_LOCALLY)) {
|
||||
$watchPatterns->markEnabled();
|
||||
$watchPatterns->markLocally();
|
||||
}
|
||||
|
||||
if (in_array(self::BASELINED_OPTION, $arguments, true) || self::envFlagEnabled(self::ENV_BASELINED)) {
|
||||
if (self::argumentPresent(self::BASELINED_OPTION, $arguments) || self::envFlagEnabled(self::ENV_BASELINED)) {
|
||||
$watchPatterns->markBaselined();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors {@see \Pest\Plugins\Concerns\HandleArguments::hasArgument()} for
|
||||
* use from static contexts — matches both `--flag` and `--flag=value`.
|
||||
*
|
||||
* @param array<int, string> $arguments
|
||||
*/
|
||||
private static function argumentPresent(string $argument, array $arguments): bool
|
||||
{
|
||||
foreach ($arguments as $arg) {
|
||||
if ($arg === $argument) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_starts_with($arg, "$argument=")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function envFlagEnabled(string $name): bool
|
||||
{
|
||||
return filter_var(getenv($name), FILTER_VALIDATE_BOOL);
|
||||
@ -852,7 +877,7 @@ private bool $piggybackCoverage = false;
|
||||
$this->output->writeln('');
|
||||
|
||||
if ($affected === []) {
|
||||
$this->renderChild('TIA mode enabled.');
|
||||
$this->renderChild('Experimental TIA mode enabled.');
|
||||
|
||||
return;
|
||||
}
|
||||
@ -1391,7 +1416,7 @@ private bool $piggybackCoverage = false;
|
||||
$coverage = Container::getInstance()->get(Coverage::class);
|
||||
assert($coverage instanceof Coverage);
|
||||
|
||||
return $coverage->coverage === true;
|
||||
return $coverage->coverage;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
use Pest\Exceptions\BaselineFetchFailed;
|
||||
use Pest\Panic;
|
||||
use Pest\Plugins\Tia;
|
||||
@ -94,7 +93,7 @@ final readonly class BaselineSync
|
||||
if ($payload === null) {
|
||||
if ($failureKind === 'no-runs' || $failureKind === null) {
|
||||
$this->startCooldown();
|
||||
$this->emitPublishInstructions($repo);
|
||||
$this->emitPublishInstructions();
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -162,7 +161,7 @@ final readonly class BaselineSync
|
||||
return $seconds.'s';
|
||||
}
|
||||
|
||||
private function emitPublishInstructions(string $repo): void
|
||||
private function emitPublishInstructions(): void
|
||||
{
|
||||
if ($this->isCi()) {
|
||||
$this->renderBadge('INFO', 'No baseline yet — this run will produce one.');
|
||||
@ -170,23 +169,8 @@ final readonly class BaselineSync
|
||||
return;
|
||||
}
|
||||
|
||||
$yaml = $this->isLaravel()
|
||||
? $this->laravelWorkflowYaml()
|
||||
: $this->genericWorkflowYaml();
|
||||
|
||||
$this->renderBadge('WARN', 'No baseline published yet — recording locally.');
|
||||
$this->renderChild('To share the baseline with your team, add this workflow to the repo:');
|
||||
$this->renderChild('.github/workflows/tia-baseline.yml');
|
||||
|
||||
$indentedYaml = array_map(
|
||||
static fn (string $line): string => ' '.$line,
|
||||
explode("\n", $yaml),
|
||||
);
|
||||
|
||||
$this->output->writeln(['', ...$indentedYaml, '']);
|
||||
|
||||
$this->renderChild(sprintf('Commit, push, then run once: gh workflow run tia-baseline.yml -R %s', $repo));
|
||||
$this->renderChild('Details: https://pestphp.com/docs/tia');
|
||||
$this->renderChild('See https://pestphp.com/docs/tia for how to publish one from CI.');
|
||||
}
|
||||
|
||||
private function isCi(): bool
|
||||
@ -196,79 +180,6 @@ final readonly class BaselineSync
|
||||
|| getenv('CIRCLECI') === 'true';
|
||||
}
|
||||
|
||||
private function isLaravel(): bool
|
||||
{
|
||||
return class_exists(InstalledVersions::class)
|
||||
&& InstalledVersions::isInstalled('laravel/framework');
|
||||
}
|
||||
|
||||
private function laravelWorkflowYaml(): string
|
||||
{
|
||||
return <<<'YAML'
|
||||
name: TIA Baseline
|
||||
on:
|
||||
push: { branches: [main] }
|
||||
schedule: [{ cron: '0 3 * * *' }]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
baseline:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { fetch-depth: 0 }
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
coverage: xdebug
|
||||
extensions: json, dom, curl, libxml, mbstring, zip, pdo, pdo_sqlite, sqlite3, bcmath, intl
|
||||
- run: cp .env.example .env
|
||||
- run: composer install --no-interaction --prefer-dist
|
||||
- run: php artisan key:generate
|
||||
- run: ./vendor/bin/pest --parallel --tia --coverage
|
||||
- name: Stage baseline for upload
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p .pest-tia-baseline
|
||||
cp -R "$HOME/.pest/tia"/*/. .pest-tia-baseline/
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pest-tia-baseline
|
||||
path: .pest-tia-baseline/
|
||||
retention-days: 30
|
||||
YAML;
|
||||
}
|
||||
|
||||
private function genericWorkflowYaml(): string
|
||||
{
|
||||
return <<<'YAML'
|
||||
name: TIA Baseline
|
||||
on:
|
||||
push: { branches: [main] }
|
||||
schedule: [{ cron: '0 3 * * *' }]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
baseline:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { fetch-depth: 0 }
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with: { php-version: '8.4', coverage: xdebug }
|
||||
- run: composer install --no-interaction --prefer-dist
|
||||
- run: ./vendor/bin/pest --parallel --tia --coverage
|
||||
- name: Stage baseline for upload
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p .pest-tia-baseline
|
||||
cp -R "$HOME/.pest/tia"/*/. .pest-tia-baseline/
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pest-tia-baseline
|
||||
path: .pest-tia-baseline/
|
||||
retention-days: 30
|
||||
YAML;
|
||||
}
|
||||
|
||||
private function detectGitHubRepo(string $projectRoot): ?string
|
||||
{
|
||||
$gitConfig = $projectRoot.DIRECTORY_SEPARATOR.'.git'.DIRECTORY_SEPARATOR.'config';
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||
namespace Pest\Plugins\Tia\Contracts;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use PHPUnit\Runner\CodeCoverage as PhpUnitCodeCoverage;
|
||||
use ReflectionClass;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
|
||||
@ -2,16 +2,16 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
namespace Pest\Plugins\Tia\Enums;
|
||||
|
||||
use PHPUnit\Framework\TestStatus\TestStatus;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
enum Replay
|
||||
enum ReplayType
|
||||
{
|
||||
case No;
|
||||
case None;
|
||||
case Pass;
|
||||
case Risky;
|
||||
case Skipped;
|
||||
@ -21,7 +21,7 @@ enum Replay
|
||||
public static function fromStatus(?TestStatus $status): self
|
||||
{
|
||||
if (! $status instanceof TestStatus) {
|
||||
return self::No;
|
||||
return self::None;
|
||||
}
|
||||
|
||||
return match (true) {
|
||||
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use Pest\Factories\Attribute;
|
||||
use Pest\Factories\TestCaseFactory;
|
||||
use Pest\Factories\TestCaseMethodFactory;
|
||||
use Pest\Support\Container;
|
||||
@ -1012,7 +1011,7 @@ final class Graph
|
||||
}
|
||||
|
||||
foreach ($method->attributes as $attribute) {
|
||||
if (! $attribute instanceof Attribute || $attribute->name !== Group::class) {
|
||||
if ($attribute->name !== Group::class) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,31 @@ final class JsModuleGraph
|
||||
'vite.config.mts',
|
||||
];
|
||||
|
||||
/**
|
||||
* Candidate page directories, in priority order. Must stay in sync with
|
||||
* `PAGE_DIR_CANDIDATES` in bin/pest-tia-vite-deps.mjs.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
private const array PAGE_DIR_CANDIDATES = [
|
||||
'resources/js/Pages',
|
||||
'resources/js/pages',
|
||||
'assets/js/Pages',
|
||||
'assets/js/pages',
|
||||
'assets/Pages',
|
||||
'assets/pages',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private const array PAGE_EXTENSIONS = [
|
||||
'vue', 'svelte',
|
||||
'tsx', 'jsx',
|
||||
'ts', 'js',
|
||||
'mts', 'cts', 'mjs', 'cjs',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
@ -51,8 +76,44 @@ final class JsModuleGraph
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (['Pages', 'pages'] as $dir) {
|
||||
if (is_dir($projectRoot.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'js'.DIRECTORY_SEPARATOR.$dir)) {
|
||||
return self::firstExistingPagesDir($projectRoot) !== null;
|
||||
}
|
||||
|
||||
private static function firstExistingPagesDir(string $projectRoot): ?string
|
||||
{
|
||||
foreach (self::PAGE_DIR_CANDIDATES as $rel) {
|
||||
$abs = $projectRoot.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $rel);
|
||||
|
||||
if (! is_dir($abs)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self::dirHasPageFile($abs)) {
|
||||
return $abs;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function dirHasPageFile(string $dir): bool
|
||||
{
|
||||
try {
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::LEAVES_ONLY,
|
||||
);
|
||||
} catch (\UnexpectedValueException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var \SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if (! $file->isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array(strtolower($file->getExtension()), self::PAGE_EXTENSIONS, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -188,17 +249,21 @@ final class JsModuleGraph
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (['Pages', 'pages'] as $dir) {
|
||||
if (is_dir($projectRoot.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'js'.DIRECTORY_SEPARATOR.$dir)) {
|
||||
$parts[] = 'pagesDir:'.$dir;
|
||||
$override = getenv('TIA_VITE_PAGES_DIR');
|
||||
|
||||
break;
|
||||
}
|
||||
if (is_string($override) && $override !== '') {
|
||||
$parts[] = 'pagesDirOverride:'.$override;
|
||||
}
|
||||
|
||||
$jsRoot = $projectRoot.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'js';
|
||||
$pagesDir = self::firstExistingPagesDir($projectRoot);
|
||||
|
||||
if (is_dir($jsRoot)) {
|
||||
if ($pagesDir !== null) {
|
||||
$parts[] = 'pagesDir:'.str_replace($projectRoot.DIRECTORY_SEPARATOR, '', $pagesDir);
|
||||
}
|
||||
|
||||
$jsRoot = $pagesDir !== null ? dirname($pagesDir) : null;
|
||||
|
||||
if ($jsRoot !== null && is_dir($jsRoot)) {
|
||||
$entries = [];
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
|
||||
@ -7,6 +7,7 @@ namespace Pest\Plugins\Tia\WatchDefaults;
|
||||
use Composer\InstalledVersions;
|
||||
use Pest\Browser\Support\BrowserTestIdentifier;
|
||||
use Pest\Factories\TestCaseFactory;
|
||||
use Pest\Plugins\Tia\Contracts\WatchDefault;
|
||||
use Pest\TestSuite;
|
||||
|
||||
/**
|
||||
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
use Pest\Plugins\Tia\Contracts\WatchDefault;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
use Pest\Plugins\Tia\Contracts\WatchDefault;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
use Pest\Plugins\Tia\Contracts\WatchDefault;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
||||
@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||
|
||||
use Pest\Plugins\Tia\Contracts\WatchDefault;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
|
||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Pest\Plugins\Tia\WatchDefaults;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
use Pest\Plugins\Tia\Contracts\WatchDefault;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
||||
@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pest\Plugins\Tia;
|
||||
|
||||
use Pest\Plugins\Tia\WatchDefaults\WatchDefault;
|
||||
use Pest\Plugins\Tia\Contracts\WatchDefault;
|
||||
use Pest\TestSuite;
|
||||
|
||||
/**
|
||||
|
||||
@ -11,7 +11,7 @@ use PHPUnit\Event\Test\FinishedSubscriber;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final readonly class EnsureTiaCoverageIsFlushed implements FinishedSubscriber
|
||||
final readonly class EnsureTiaEnds implements FinishedSubscriber
|
||||
{
|
||||
public function __construct(private Recorder $recorder) {}
|
||||
|
||||
@ -12,7 +12,7 @@ use PHPUnit\Event\Test\PreparedSubscriber;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final readonly class EnsureTiaCoverageIsRecorded implements PreparedSubscriber
|
||||
final readonly class EnsureTiaStarts implements PreparedSubscriber
|
||||
{
|
||||
public function __construct(private Recorder $recorder) {}
|
||||
|
||||
@ -22,7 +22,7 @@ describe('of()', function () {
|
||||
describe('PHP files', function () {
|
||||
it('produces the same hash regardless of whitespace differences', function () {
|
||||
$a = ContentHash::ofContent('a.php', "<?php \$foo = 1;\n\necho \$foo;");
|
||||
$b = ContentHash::ofContent('a.php', "<?php \$foo=1; echo \$foo;");
|
||||
$b = ContentHash::ofContent('a.php', '<?php $foo=1; echo $foo;');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
@ -56,8 +56,8 @@ describe('PHP files', function () {
|
||||
});
|
||||
|
||||
it('detects code changes', function () {
|
||||
$a = ContentHash::ofContent('a.php', "<?php \$foo = 1;");
|
||||
$b = ContentHash::ofContent('a.php', "<?php \$foo = 2;");
|
||||
$a = ContentHash::ofContent('a.php', '<?php $foo = 1;');
|
||||
$b = ContentHash::ofContent('a.php', '<?php $foo = 2;');
|
||||
|
||||
expect($a)->not->toBe($b);
|
||||
});
|
||||
@ -70,8 +70,8 @@ describe('PHP files', function () {
|
||||
});
|
||||
|
||||
it('treats variable renames as a change', function () {
|
||||
$a = ContentHash::ofContent('a.php', "<?php \$foo = 1;");
|
||||
$b = ContentHash::ofContent('a.php', "<?php \$bar = 1;");
|
||||
$a = ContentHash::ofContent('a.php', '<?php $foo = 1;');
|
||||
$b = ContentHash::ofContent('a.php', '<?php $bar = 1;');
|
||||
|
||||
expect($a)->not->toBe($b);
|
||||
});
|
||||
@ -92,43 +92,43 @@ describe('PHP files', function () {
|
||||
|
||||
describe('Blade files', function () {
|
||||
it('strips blade comments', function () {
|
||||
$a = ContentHash::ofContent('a.blade.php', "<div>{{-- a comment --}}Hello</div>");
|
||||
$b = ContentHash::ofContent('a.blade.php', "<div>Hello</div>");
|
||||
$a = ContentHash::ofContent('a.blade.php', '<div>{{-- a comment --}}Hello</div>');
|
||||
$b = ContentHash::ofContent('a.blade.php', '<div>Hello</div>');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
|
||||
it('strips multi-line blade comments', function () {
|
||||
$a = ContentHash::ofContent('a.blade.php', "<div>\n{{--\n multi\n line\n--}}\nHello\n</div>");
|
||||
$b = ContentHash::ofContent('a.blade.php', "<div> Hello </div>");
|
||||
$b = ContentHash::ofContent('a.blade.php', '<div> Hello </div>');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
|
||||
it('collapses whitespace', function () {
|
||||
$a = ContentHash::ofContent('a.blade.php', "<div>\n Hello\n World\n</div>");
|
||||
$b = ContentHash::ofContent('a.blade.php', "<div> Hello World </div>");
|
||||
$b = ContentHash::ofContent('a.blade.php', '<div> Hello World </div>');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
|
||||
it('detects content changes', function () {
|
||||
$a = ContentHash::ofContent('a.blade.php', "<div>Hello</div>");
|
||||
$b = ContentHash::ofContent('a.blade.php', "<div>Goodbye</div>");
|
||||
$a = ContentHash::ofContent('a.blade.php', '<div>Hello</div>');
|
||||
$b = ContentHash::ofContent('a.blade.php', '<div>Goodbye</div>');
|
||||
|
||||
expect($a)->not->toBe($b);
|
||||
});
|
||||
|
||||
it('keeps blade directives intact', function () {
|
||||
$a = ContentHash::ofContent('a.blade.php', "@if(\$user)Hi @endif");
|
||||
$b = ContentHash::ofContent('a.blade.php', "@if(\$user)Bye @endif");
|
||||
$a = ContentHash::ofContent('a.blade.php', '@if($user)Hi @endif');
|
||||
$b = ContentHash::ofContent('a.blade.php', '@if($user)Bye @endif');
|
||||
|
||||
expect($a)->not->toBe($b);
|
||||
});
|
||||
|
||||
it('does not use the PHP tokenizer for blade files', function () {
|
||||
$a = ContentHash::ofContent('a.blade.php', "<?php // not stripped ?> hello");
|
||||
$b = ContentHash::ofContent('a.blade.php', "<?php ?> hello");
|
||||
$a = ContentHash::ofContent('a.blade.php', '<?php // not stripped ?> hello');
|
||||
$b = ContentHash::ofContent('a.blade.php', '<?php ?> hello');
|
||||
|
||||
expect($a)->not->toBe($b);
|
||||
});
|
||||
@ -137,70 +137,70 @@ describe('Blade files', function () {
|
||||
describe('JavaScript-like files', function () {
|
||||
it('strips line comments', function () {
|
||||
$a = ContentHash::ofContent('a.js', "// a comment\nconst foo = 1;");
|
||||
$b = ContentHash::ofContent('a.js', "const foo = 1;");
|
||||
$b = ContentHash::ofContent('a.js', 'const foo = 1;');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
|
||||
it('strips block comments on their own lines', function () {
|
||||
$a = ContentHash::ofContent('a.js', "/* block */\nconst foo = 1;");
|
||||
$b = ContentHash::ofContent('a.js', "const foo = 1;");
|
||||
$b = ContentHash::ofContent('a.js', 'const foo = 1;');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
|
||||
it('collapses whitespace', function () {
|
||||
$a = ContentHash::ofContent('a.js', "const foo = 1;\n\nconst bar = 2;");
|
||||
$b = ContentHash::ofContent('a.js', "const foo = 1; const bar = 2;");
|
||||
$b = ContentHash::ofContent('a.js', 'const foo = 1; const bar = 2;');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
|
||||
it('detects code changes', function () {
|
||||
$a = ContentHash::ofContent('a.js', "const foo = 1;");
|
||||
$b = ContentHash::ofContent('a.js', "const foo = 2;");
|
||||
$a = ContentHash::ofContent('a.js', 'const foo = 1;');
|
||||
$b = ContentHash::ofContent('a.js', 'const foo = 2;');
|
||||
|
||||
expect($a)->not->toBe($b);
|
||||
});
|
||||
|
||||
it('does not strip inline trailing comments', function () {
|
||||
$a = ContentHash::ofContent('a.js', "const foo = 1; // inline");
|
||||
$b = ContentHash::ofContent('a.js', "const foo = 1;");
|
||||
$a = ContentHash::ofContent('a.js', 'const foo = 1; // inline');
|
||||
$b = ContentHash::ofContent('a.js', 'const foo = 1;');
|
||||
|
||||
expect($a)->not->toBe($b);
|
||||
});
|
||||
|
||||
it('applies the same rules to .ts files', function () {
|
||||
$a = ContentHash::ofContent('a.ts', "// comment\nconst foo: number = 1;");
|
||||
$b = ContentHash::ofContent('a.ts', "const foo: number = 1;");
|
||||
$b = ContentHash::ofContent('a.ts', 'const foo: number = 1;');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
|
||||
it('applies the same rules to .tsx files', function () {
|
||||
$a = ContentHash::ofContent('a.tsx', "// comment\nconst Foo = () => <div/>;");
|
||||
$b = ContentHash::ofContent('a.tsx', "const Foo = () => <div/>;");
|
||||
$b = ContentHash::ofContent('a.tsx', 'const Foo = () => <div/>;');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
|
||||
it('applies the same rules to .jsx files', function () {
|
||||
$a = ContentHash::ofContent('a.jsx', "// comment\nconst Foo = () => <div/>;");
|
||||
$b = ContentHash::ofContent('a.jsx', "const Foo = () => <div/>;");
|
||||
$b = ContentHash::ofContent('a.jsx', 'const Foo = () => <div/>;');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
|
||||
it('applies the same rules to .vue files', function () {
|
||||
$a = ContentHash::ofContent('a.vue', "<script>\n// comment\nexport default {}\n</script>");
|
||||
$b = ContentHash::ofContent('a.vue', "<script> export default {} </script>");
|
||||
$b = ContentHash::ofContent('a.vue', '<script> export default {} </script>');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
|
||||
it('applies the same rules to .svelte files', function () {
|
||||
$a = ContentHash::ofContent('a.svelte', "<script>\n// comment\nlet foo = 1;\n</script>");
|
||||
$b = ContentHash::ofContent('a.svelte', "<script> let foo = 1; </script>");
|
||||
$b = ContentHash::ofContent('a.svelte', '<script> let foo = 1; </script>');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
@ -208,7 +208,7 @@ describe('JavaScript-like files', function () {
|
||||
it('applies the same rules to .mjs, .cjs, and .mts files', function () {
|
||||
foreach (['mjs', 'cjs', 'mts'] as $ext) {
|
||||
$a = ContentHash::ofContent("a.$ext", "// comment\nexport const foo = 1;");
|
||||
$b = ContentHash::ofContent("a.$ext", "export const foo = 1;");
|
||||
$b = ContentHash::ofContent("a.$ext", 'export const foo = 1;');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
}
|
||||
@ -217,22 +217,22 @@ describe('JavaScript-like files', function () {
|
||||
|
||||
describe('unknown extensions', function () {
|
||||
it('hashes the raw content for unknown extensions', function () {
|
||||
$a = ContentHash::ofContent('a.txt', "hello world");
|
||||
$b = ContentHash::ofContent('a.txt', "hello world");
|
||||
$a = ContentHash::ofContent('a.txt', 'hello world');
|
||||
$b = ContentHash::ofContent('a.txt', 'hello world');
|
||||
|
||||
expect($a)->toBe($b);
|
||||
});
|
||||
|
||||
it('does not normalise whitespace for unknown extensions', function () {
|
||||
$a = ContentHash::ofContent('a.txt', "hello world");
|
||||
$b = ContentHash::ofContent('a.txt', "hello world");
|
||||
$a = ContentHash::ofContent('a.txt', 'hello world');
|
||||
$b = ContentHash::ofContent('a.txt', 'hello world');
|
||||
|
||||
expect($a)->not->toBe($b);
|
||||
});
|
||||
|
||||
it('does not strip comments for unknown extensions', function () {
|
||||
$a = ContentHash::ofContent('a.txt', "// not a comment here\nhello");
|
||||
$b = ContentHash::ofContent('a.txt', "hello");
|
||||
$b = ContentHash::ofContent('a.txt', 'hello');
|
||||
|
||||
expect($a)->not->toBe($b);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user