diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index cbe50985..7b9da1f9 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -189,7 +189,29 @@ final class TestCaseFactory private static \$__filename = '$filename'; + private array \$__snapshotChanges = []; + $methodsCode + + /** @postCondition */ + protected function __MarkTestIncompleteIfSnapshotHaveChanged(): void + { + if (empty(\$this->__snapshotChanges)) { + return; + } + + if (count(\$this->__snapshotChanges) === 1) { + \$this->markTestIncomplete(\$this->__snapshotChanges[0]); + + return; + } + + \$formattedMessages = implode(PHP_EOL, array_map(function (string \$message) { + return "- \$message"; + }, \$this->__snapshotChanges)); + + \$this->markTestIncomplete(\$formattedMessages); + } } PHP; diff --git a/src/Mixins/Expectation.php b/src/Mixins/Expectation.php index b90e601a..d3d8cbd3 100644 --- a/src/Mixins/Expectation.php +++ b/src/Mixins/Expectation.php @@ -805,6 +805,12 @@ final class Expectation */ public function toMatchSnapshot(string $message = ''): self { + $snapshots = TestSuite::getInstance()->snapshots; + $snapshots->startNewExpectation(); + + $testCase = TestSuite::getInstance()->test; + assert($testCase instanceof TestCase); + $string = match (true) { is_string($this->value) => $this->value, is_object($this->value) && method_exists($this->value, '__toString') => $this->value->__toString(), @@ -817,22 +823,18 @@ final class Expectation default => InvalidExpectationValue::expected('array|object|string'), }; - $testCase = TestSuite::getInstance()->test; - assert($testCase instanceof TestCase); - $snapshots = TestSuite::getInstance()->snapshots; - - if ($snapshots->has($testCase, $string)) { - [$filename, $content] = $snapshots->get($testCase, $string); + if ($snapshots->has()) { + [$filename, $content] = $snapshots->get(); Assert::assertSame( - $content, - $string, + strtr($content, ["\r\n" => "\n", "\r" => "\n"]), + strtr($string, ["\r\n" => "\n", "\r" => "\n"]), $message === '' ? "Failed asserting that the string value matches its snapshot ($filename)." : $message ); } else { - $filename = $snapshots->save($testCase, $string); + $filename = $snapshots->save($string); - $testCase::markTestIncomplete('Snapshot created at ['.$filename.'].'); + TestSuite::getInstance()->registerSnapshotChange("Snapshot created at [$filename]"); } return $this; diff --git a/src/Repositories/SnapshotRepository.php b/src/Repositories/SnapshotRepository.php index 89a4135e..1cd3e666 100644 --- a/src/Repositories/SnapshotRepository.php +++ b/src/Repositories/SnapshotRepository.php @@ -5,14 +5,16 @@ declare(strict_types=1); namespace Pest\Repositories; use Pest\Exceptions\ShouldNotHappen; -use Pest\Support\Str; -use PHPUnit\Framework\TestCase; +use Pest\TestSuite; /** * @internal */ final class SnapshotRepository { + /** @var array */ + private static array $expectationsCounter = []; + /** * Creates a snapshot repository instance. */ @@ -25,11 +27,9 @@ final class SnapshotRepository /** * Checks if the snapshot exists. */ - public function has(TestCase $testCase, string $description): bool + public function has(): bool { - [$filename, $description] = $this->getFilenameAndDescription($testCase); - - return file_exists($this->getSnapshotFilename($filename, $description)); + return file_exists($this->getSnapshotFilename()); } /** @@ -39,11 +39,9 @@ final class SnapshotRepository * * @throws ShouldNotHappen */ - public function get(TestCase $testCase, string $description): array + public function get(): array { - [$filename, $description] = $this->getFilenameAndDescription($testCase); - - $contents = file_get_contents($snapshotFilename = $this->getSnapshotFilename($filename, $description)); + $contents = file_get_contents($snapshotFilename = $this->getSnapshotFilename()); if ($contents === false) { throw ShouldNotHappen::fromMessage('Snapshot file could not be read.'); @@ -57,11 +55,9 @@ final class SnapshotRepository /** * Saves the given snapshot for the given test case. */ - public function save(TestCase $testCase, string $snapshot): string + public function save(string $snapshot): string { - [$filename, $description] = $this->getFilenameAndDescription($testCase); - - $snapshotFilename = $this->getSnapshotFilename($filename, $description); + $snapshotFilename = $this->getSnapshotFilename(); if (! file_exists(dirname($snapshotFilename))) { mkdir(dirname($snapshotFilename), 0755, true); @@ -103,33 +99,43 @@ final class SnapshotRepository } } - /** - * Gets the snapshot's "filename" and "description". - * - * @return array{0: string, 1: string} - */ - private function getFilenameAndDescription(TestCase $testCase): array - { - $filename = (fn () => self::$__filename)->call($testCase, $testCase::class); // @phpstan-ignore-line - - $description = str_replace('__pest_evaluable_', '', $testCase->name()); - $datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($testCase->dataSetAsStringWithData())); - - $description = str_replace(' ', '_', $description.$datasetAsString); - - return [$filename, $description]; - } - /** * Gets the snapshot's "filename". */ - private function getSnapshotFilename(string $filename, string $description): string + private function getSnapshotFilename(): string { - $relativePath = str_replace($this->testsPath, '', $filename); + $relativePath = str_replace($this->testsPath, '', TestSuite::getInstance()->getFilename()); // remove extension from filename $relativePath = substr($relativePath, 0, (int) strrpos($relativePath, '.')); + $description = TestSuite::getInstance()->getDescription(); + + if ($this->getCurrentSnapshotCounter() > 1) { + $description .= '__'.$this->getCurrentSnapshotCounter(); + } + return sprintf('%s/%s.snap', $this->testsPath.'/'.$this->snapshotsPath.$relativePath, $description); } + + private function getCurrentSnapshotKey(): string + { + return TestSuite::getInstance()->getFilename().'###'.TestSuite::getInstance()->getDescription(); + } + + private function getCurrentSnapshotCounter(): int + { + return self::$expectationsCounter[$this->getCurrentSnapshotKey()] ?? 0; + } + + public function startNewExpectation(): void + { + $key = $this->getCurrentSnapshotKey(); + + if (! isset(self::$expectationsCounter[$key])) { + self::$expectationsCounter[$key] = 0; + } + + self::$expectationsCounter[$key]++; + } } diff --git a/src/TestSuite.php b/src/TestSuite.php index 3504e764..885baa2f 100644 --- a/src/TestSuite.php +++ b/src/TestSuite.php @@ -11,6 +11,7 @@ use Pest\Repositories\BeforeAllRepository; use Pest\Repositories\BeforeEachRepository; use Pest\Repositories\SnapshotRepository; use Pest\Repositories\TestRepository; +use Pest\Support\Str; use PHPUnit\Framework\TestCase; /** @@ -105,4 +106,28 @@ final class TestSuite return self::$instance; } + + public function getFilename(): string + { + assert($this->test instanceof TestCase); + + return (fn () => self::$__filename)->call($this->test, $this->test::class); // @phpstan-ignore-line + } + + public function getDescription(): string + { + assert($this->test instanceof TestCase); + + $description = str_replace('__pest_evaluable_', '', $this->test->name()); + $datasetAsString = str_replace('__pest_evaluable_', '', Str::evaluable($this->test->dataSetAsStringWithData())); + + return str_replace(' ', '_', $description.$datasetAsString); + } + + public function registerSnapshotChange(string $message): void + { + assert($this->test instanceof TestCase); + + (fn (): string => $this->__snapshotChanges[] = $message)->call($this->test, $this->test::class); // @phpstan-ignore-line + } } diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations.snap new file mode 100644 index 00000000..7b5a8676 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations.snap @@ -0,0 +1 @@ +foo bar 1 \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations__2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations__2.snap new file mode 100644 index 00000000..206d0d02 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations__2.snap @@ -0,0 +1 @@ +foo bar 2 \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1_.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1_.snap new file mode 100644 index 00000000..7b5a8676 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1_.snap @@ -0,0 +1 @@ +foo bar 1 \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1___2.snap new file mode 100644 index 00000000..206d0d02 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set___1____1___2.snap @@ -0,0 +1 @@ +foo bar 2 \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar__.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar__.snap new file mode 100644 index 00000000..7b5a8676 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar__.snap @@ -0,0 +1 @@ +foo bar 1 \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar____2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar____2.snap new file mode 100644 index 00000000..206d0d02 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____bar______bar____2.snap @@ -0,0 +1 @@ +foo bar 2 \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz__.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz__.snap new file mode 100644 index 00000000..7b5a8676 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz__.snap @@ -0,0 +1 @@ +foo bar 1 \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz____2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz____2.snap new file mode 100644 index 00000000..206d0d02 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____baz______baz____2.snap @@ -0,0 +1 @@ +foo bar 2 \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo__.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo__.snap new file mode 100644 index 00000000..7b5a8676 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo__.snap @@ -0,0 +1 @@ +foo bar 1 \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo____2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo____2.snap new file mode 100644 index 00000000..206d0d02 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/multiple_snapshot_expectations_with_datasets_with_data_set____foo______foo____2.snap @@ -0,0 +1 @@ +foo bar 2 \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with______toString___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with______toString___2.snap new file mode 100644 index 00000000..c2b4dc0a --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with______toString___2.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toArray___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toArray___2.snap new file mode 100644 index 00000000..afd4f5f9 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toArray___2.snap @@ -0,0 +1,3 @@ +{ + "key": "
\n
\n
\n

Snapshot<\/h1>\n <\/div>\n <\/div>\n <\/div>" +} \ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toString___2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toString___2.snap new file mode 100644 index 00000000..c2b4dc0a --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with__toString___2.snap @@ -0,0 +1,7 @@ +
+
+
+

Snapshot

+
+
+
\ No newline at end of file diff --git a/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_array__2.snap b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_array__2.snap new file mode 100644 index 00000000..afd4f5f9 --- /dev/null +++ b/tests/.pest/snapshots/Features/Expect/toMatchSnapshot/pass_with_array__2.snap @@ -0,0 +1,3 @@ +{ + "key": "
\n
\n
\n

Snapshot<\/h1>\n <\/div>\n <\/div>\n <\/div>" +} \ No newline at end of file diff --git a/tests/Features/Expect/toMatchSnapshot.php b/tests/Features/Expect/toMatchSnapshot.php index 29a63a59..86320e38 100644 --- a/tests/Features/Expect/toMatchSnapshot.php +++ b/tests/Features/Expect/toMatchSnapshot.php @@ -16,13 +16,13 @@ beforeEach(function () { }); test('pass', function () { - TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + TestSuite::getInstance()->snapshots->save($this->snapshotable); expect($this->snapshotable)->toMatchSnapshot(); }); test('pass with `__toString`', function () { - TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + TestSuite::getInstance()->snapshots->save($this->snapshotable); $object = new class($this->snapshotable) { @@ -40,7 +40,7 @@ test('pass with `__toString`', function () { }); test('pass with `toString`', function () { - TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + TestSuite::getInstance()->snapshots->save($this->snapshotable); $object = new class($this->snapshotable) { @@ -58,8 +58,8 @@ test('pass with `toString`', function () { }); test('pass with dataset', function ($data) { - TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); - [$filename] = TestSuite::getInstance()->snapshots->get($this, $this->snapshotable); + TestSuite::getInstance()->snapshots->save($this->snapshotable); + [$filename] = TestSuite::getInstance()->snapshots->get(); expect($filename)->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap') ->and($this->snapshotable)->toMatchSnapshot(); @@ -67,8 +67,8 @@ test('pass with dataset', function ($data) { describe('within describe', function () { test('pass with dataset', function ($data) { - TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); - [$filename] = TestSuite::getInstance()->snapshots->get($this, $this->snapshotable); + TestSuite::getInstance()->snapshots->save($this->snapshotable); + [$filename] = TestSuite::getInstance()->snapshots->get(); expect($filename)->toEndWith('pass_with_dataset_with_data_set____my_datas_set_value______my_datas_set_value__.snap') ->and($this->snapshotable)->toMatchSnapshot(); @@ -76,7 +76,7 @@ describe('within describe', function () { })->with(['my-datas-set-value']); test('pass with `toArray`', function () { - TestSuite::getInstance()->snapshots->save($this, json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT)); + TestSuite::getInstance()->snapshots->save(json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT)); $object = new class($this->snapshotable) { @@ -96,7 +96,7 @@ test('pass with `toArray`', function () { }); test('pass with array', function () { - TestSuite::getInstance()->snapshots->save($this, json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT)); + TestSuite::getInstance()->snapshots->save(json_encode(['key' => $this->snapshotable], JSON_PRETTY_PRINT)); expect([ 'key' => $this->snapshotable, @@ -104,19 +104,31 @@ test('pass with array', function () { }); test('failures', function () { - TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + TestSuite::getInstance()->snapshots->save($this->snapshotable); expect('contain that does not match snapshot')->toMatchSnapshot(); })->throws(ExpectationFailedException::class, 'Failed asserting that two strings are identical.'); test('failures with custom message', function () { - TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + TestSuite::getInstance()->snapshots->save($this->snapshotable); expect('contain that does not match snapshot')->toMatchSnapshot('oh no'); })->throws(ExpectationFailedException::class, 'oh no'); test('not failures', function () { - TestSuite::getInstance()->snapshots->save($this, $this->snapshotable); + TestSuite::getInstance()->snapshots->save($this->snapshotable); expect($this->snapshotable)->not->toMatchSnapshot(); })->throws(ExpectationFailedException::class); + +test('multiple snapshot expectations', function () { + expect('foo bar 1')->toMatchSnapshot(); + + expect('foo bar 2')->toMatchSnapshot(); +}); + +test('multiple snapshot expectations with datasets', function () { + expect('foo bar 1')->toMatchSnapshot(); + + expect('foo bar 2')->toMatchSnapshot(); +})->with([1, 'foo', 'bar', 'baz']);