Skip to content

Commit b4e62e4

Browse files
committed
add image assertions
1 parent e26433d commit b4e62e4

File tree

9 files changed

+279
-16
lines changed

9 files changed

+279
-16
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ To make snapshot assertions, use the `Spatie\Snapshots\MatchesSnapshots` trait i
9797
- `assertMatchesTextSnapshot($actual)`
9898
- `assertMatchesXmlSnapshot($actual)`
9999
- `assertMatchesYamlSnapshot($actual)`
100+
- `assertMachesImageSnapshot($actual)`
100101

101102
### Snapshot Testing 101
102103

@@ -180,6 +181,17 @@ The `assertMatchesFileHashSnapshot($filePath)` assertion asserts that the hash o
180181

181182
The `assertMatchesFileSnapshot($filePath)` assertion works almost the same way as the file hash assertion, except that it actually saves the whole file in the snapshots directory. If the assertion fails, it places the failed file next to the snapshot file so they can easily be manually compared. The persisted failed file is automatically deleted when the test passes. This assertion is most useful when working with binary files that should be manually compared like images or pdfs.
182183

184+
### Image snapshots
185+
186+
The `assertImageSnapshot` requires the [spatie/pixelmatch-php](https://github.com/spatie/pixelmatch-php) package to be installed.
187+
188+
This assertion will pass if the given image is nearly identical to the snapshot that was made the first time the test was run. You can customize the threshold by passing a second argument to the assertion. Higher values will make the comparison more sensitive. The threshold should be between 0 and 1.
189+
190+
```php
191+
$this->assertMatchesImageSnapshot($imagePath, 0.1);
192+
```
193+
194+
183195
### Customizing Snapshot Ids and Directories
184196

185197
Snapshot ids are generated via the `getSnapshotId` method on the `MatchesSnapshot` trait. Override the method to customize the id. By default, a snapshot id exists of the test name, the test case name and an incrementing value, e.g. `Test__my_test_case__1`.

build/report.junit.xml

Lines changed: 117 additions & 0 deletions
Large diffs are not rendered by default.

composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
"symfony/serializer": "^5.2|^6.2",
3131
"symfony/yaml": "^5.2|^6.2"
3232
},
33+
"require-dev": {
34+
"spatie/pixelmatch-php": "dev-main",
35+
"spatie/ray": "^1.37"
36+
},
3337
"autoload": {
3438
"psr-4": {
3539
"Spatie\\Snapshots\\": "src"
@@ -53,4 +57,5 @@
5357
"dev-v5": "5.0-dev"
5458
}
5559
}
60+
5661
}

src/Drivers/ImageDriver.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Spatie\Snapshots\Drivers;
4+
5+
use PHPUnit\Framework\Assert;
6+
use PHPUnit\Framework\ExpectationFailedException;
7+
use Spatie\Pixelmatch\Exceptions\CouldNotCompare;
8+
use Spatie\Pixelmatch\Pixelmatch;
9+
use Spatie\Snapshots\Driver;
10+
11+
class ImageDriver implements Driver
12+
{
13+
public function __construct(
14+
protected float $threshold = 0.1,
15+
protected bool $includeAa = true,
16+
)
17+
{
18+
}
19+
20+
public function serialize($data): string
21+
{
22+
return file_get_contents($data);
23+
}
24+
25+
public function extension(): string
26+
{
27+
return 'png';
28+
}
29+
30+
public function match($expected, $actual)
31+
{
32+
$tempPath = sys_get_temp_dir();
33+
34+
$expectedTempPath = $tempPath . '/expected.png';
35+
file_put_contents($expectedTempPath, $expected);
36+
37+
$actualTempPath = $tempPath . '/actual.png';
38+
file_put_contents($actualTempPath, $actual);
39+
40+
$pixelMatch = Pixelmatch::new($expectedTempPath, $actualTempPath)
41+
->threshold($this->threshold)
42+
->includeAa($this->includeAa);
43+
44+
try {
45+
$result = $pixelMatch->matches();
46+
} catch (CouldNotCompare $exception) {
47+
throw new
48+
ExpectationFailedException($exception->getMessage());
49+
}
50+
51+
Assert::assertTrue($result);
52+
}
53+
}

src/MatchesSnapshots.php

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Spatie\Snapshots\Concerns\SnapshotDirectoryAware;
88
use Spatie\Snapshots\Concerns\SnapshotIdAware;
99
use Spatie\Snapshots\Drivers\HtmlDriver;
10+
use Spatie\Snapshots\Drivers\ImageDriver;
1011
use Spatie\Snapshots\Drivers\JsonDriver;
1112
use Spatie\Snapshots\Drivers\ObjectDriver;
1213
use Spatie\Snapshots\Drivers\TextDriver;
@@ -50,7 +51,7 @@ public function markTestIncompleteIfSnapshotsHaveChanged()
5051

5152
public function assertMatchesSnapshot($actual, Driver $driver = null): void
5253
{
53-
if (! is_null($driver)) {
54+
if (!is_null($driver)) {
5455
$this->doSnapshotAssertion($actual, $driver);
5556

5657
return;
@@ -67,7 +68,7 @@ public function assertMatchesSnapshot($actual, Driver $driver = null): void
6768

6869
public function assertMatchesFileHashSnapshot(string $filePath): void
6970
{
70-
if (! file_exists($filePath)) {
71+
if (!file_exists($filePath)) {
7172
$this->fail('File does not exist');
7273
}
7374

@@ -111,15 +112,27 @@ public function assertMatchesYamlSnapshot($actual): void
111112
$this->assertMatchesSnapshot($actual, new YamlDriver());
112113
}
113114

115+
public function assertMatchesImageSnapshot(
116+
$actual,
117+
float $threshold = 0.1,
118+
bool $includeAa = true
119+
): void
120+
{
121+
$this->assertMatchesSnapshot($actual, new ImageDriver(
122+
$threshold,
123+
$includeAa,
124+
));
125+
}
126+
114127
/*
115128
* Determines the directory where file snapshots are stored. By default a
116129
* `__snapshots__/files` directory is created at the same level as the
117130
* test class.
118131
*/
119132
protected function getFileSnapshotDirectory(): string
120133
{
121-
return $this->getSnapshotDirectory().
122-
DIRECTORY_SEPARATOR.
134+
return $this->getSnapshotDirectory() .
135+
DIRECTORY_SEPARATOR .
123136
'files';
124137
}
125138

@@ -148,7 +161,7 @@ protected function shouldUpdateSnapshots(): bool
148161
*/
149162
protected function shouldCreateSnapshots(): bool
150163
{
151-
return ! in_array('--without-creating-snapshots', $_SERVER['argv'], true)
164+
return !in_array('--without-creating-snapshots', $_SERVER['argv'], true)
152165
&& getenv('CREATE_SNAPSHOTS') !== 'false';
153166
}
154167

@@ -162,7 +175,7 @@ protected function doSnapshotAssertion(mixed $actual, Driver $driver)
162175
$driver
163176
);
164177

165-
if (! $snapshot->exists()) {
178+
if (!$snapshot->exists()) {
166179
$this->assertSnapshotShouldBeCreated($snapshot->filename());
167180

168181
$this->createSnapshotAndMarkTestIncomplete($snapshot, $actual);
@@ -190,7 +203,7 @@ protected function doSnapshotAssertion(mixed $actual, Driver $driver)
190203

191204
protected function doFileSnapshotAssertion(string $filePath): void
192205
{
193-
if (! file_exists($filePath)) {
206+
if (!file_exists($filePath)) {
194207
$this->fail('File does not exist');
195208
}
196209

@@ -204,7 +217,7 @@ protected function doFileSnapshotAssertion(string $filePath): void
204217

205218
$this->snapshotIncrementor++;
206219

207-
$snapshotId = $this->getSnapshotId().'.'.$fileExtension;
220+
$snapshotId = $this->getSnapshotId() . '.' . $fileExtension;
208221
$snapshotId = Filename::cleanFilename($snapshotId);
209222

210223
// If $filePath has a different file extension than the snapshot, the test should fail
@@ -227,13 +240,13 @@ protected function doFileSnapshotAssertion(string $filePath): void
227240
$this->fail("File did not match the snapshot file extension (expected: {$expectedExtension}, was: {$fileExtension})");
228241
}
229242

230-
$failedSnapshotId = $snapshotId.'_failed.'.$fileExtension;
243+
$failedSnapshotId = $snapshotId . '_failed.' . $fileExtension;
231244

232245
if ($fileSystem->has($failedSnapshotId)) {
233246
$fileSystem->delete($failedSnapshotId);
234247
}
235248

236-
if (! $fileSystem->has($snapshotId)) {
249+
if (!$fileSystem->has($snapshotId)) {
237250
$this->assertSnapshotShouldBeCreated($failedSnapshotId);
238251

239252
$fileSystem->copy($filePath, $snapshotId);
@@ -243,7 +256,7 @@ protected function doFileSnapshotAssertion(string $filePath): void
243256
return;
244257
}
245258

246-
if (! $fileSystem->fileEquals($filePath, $snapshotId)) {
259+
if (!$fileSystem->fileEquals($filePath, $snapshotId)) {
247260
if ($this->shouldUpdateSnapshots()) {
248261
$fileSystem->copy($filePath, $snapshotId);
249262

@@ -276,8 +289,8 @@ protected function updateSnapshotAndMarkTestIncomplete(Snapshot $snapshot, $actu
276289

277290
protected function rethrowExpectationFailedExceptionWithUpdateSnapshotsPrompt($exception): void
278291
{
279-
$newMessage = $exception->getMessage()."\n\n".
280-
'Snapshots can be updated by passing '.
292+
$newMessage = $exception->getMessage() . "\n\n" .
293+
'Snapshots can be updated by passing ' .
281294
'`-d --update-snapshots` through PHPUnit\'s CLI arguments.';
282295

283296
$exceptionReflection = new ReflectionObject($exception);
@@ -301,9 +314,9 @@ protected function assertSnapshotShouldBeCreated(string $snapshotFileName): void
301314
}
302315

303316
$this->fail(
304-
"Snapshot \"$snapshotFileName\" does not exist.\n".
305-
'You can automatically create it by removing '.
306-
'the `CREATE_SNAPSHOTS=false` env var, or '.
317+
"Snapshot \"$snapshotFileName\" does not exist.\n" .
318+
'You can automatically create it by removing ' .
319+
'the `CREATE_SNAPSHOTS=false` env var, or ' .
307320
'`-d --without-creating-snapshots` of PHPUnit\'s CLI arguments.'
308321
);
309322
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace Spatie\Snapshots\Test\Unit\Drivers;
4+
5+
use PHPUnit\Framework\ExpectationFailedException;
6+
use PHPUnit\Framework\TestCase;
7+
use Spatie\Snapshots\Drivers\ImageDriver;
8+
9+
class ImageDriverTest extends TestCase
10+
{
11+
public function setUp(): void
12+
{
13+
parent::setUp();
14+
15+
$this->driver = new ImageDriver();
16+
17+
$this->pathToImageA = __DIR__ . '/../test_files/testA.png';
18+
$this->pathToImageB = __DIR__ . '/../test_files/testB.png';
19+
$this->pathToImageWithDifferentDimensions = __DIR__ . '/../test_files/testC.png';
20+
21+
}
22+
23+
/** @test */
24+
public function it_can_serialize_an_image()
25+
{
26+
$data = $this->driver->serialize($this->pathToImageA);
27+
28+
$this->assertEquals($data, file_get_contents($this->pathToImageA));
29+
}
30+
31+
/** @test */
32+
public function it_can_determine_that_two_images_are_the_same()
33+
{
34+
$this->driver->match(
35+
file_get_contents($this->pathToImageA),
36+
file_get_contents($this->pathToImageA),
37+
);
38+
39+
$this->doesNotPerformAssertions();
40+
}
41+
42+
/** @test */
43+
public function it_can_determine_that_two_images_are_not_same()
44+
{
45+
$this->expectException(ExpectationFailedException::class);
46+
47+
$this->driver->match(
48+
file_get_contents($this->pathToImageA),
49+
file_get_contents($this->pathToImageB),
50+
);
51+
}
52+
53+
/** @test */
54+
public function it_will_determine_that_two_images_with_different_dimensions_are_different()
55+
{
56+
$this->expectException(ExpectationFailedException::class);
57+
58+
$this->driver->match(
59+
file_get_contents($this->pathToImageA),
60+
file_get_contents($this->pathToImageWithDifferentDimensions),
61+
);
62+
}
63+
}

tests/Unit/test_files/testA.png

149 KB
Loading

tests/Unit/test_files/testB.png

180 KB
Loading

tests/Unit/test_files/testC.png

462 KB
Loading

0 commit comments

Comments
 (0)