From e3dc5c914449bf401a961c3a7dbe8ad3fdcdd2fd Mon Sep 17 00:00:00 2001 From: Veljko Vujanovic Date: Mon, 6 Apr 2026 12:44:34 +0200 Subject: [PATCH 1/3] feat: add video on test failure This adds a new feature to store video on test failure optionally just like in Playwright --- src/Api/PendingAwaitablePage.php | 12 +++ src/Configuration.php | 10 +++ .../BrowserExpectationFailedException.php | 6 ++ src/Playwright/Playwright.php | 65 ++++++++++++++ src/Plugin.php | 17 ++++ src/Support/Video.php | 64 +++++++++++++ .../Configuration/VideoConfigurationTest.php | 58 ++++++++++++ tests/Unit/Support/VideoTest.php | 90 +++++++++++++++++++ 8 files changed, 322 insertions(+) create mode 100644 src/Support/Video.php create mode 100644 tests/Unit/Configuration/VideoConfigurationTest.php create mode 100644 tests/Unit/Support/VideoTest.php diff --git a/src/Api/PendingAwaitablePage.php b/src/Api/PendingAwaitablePage.php index 83ad7b77..aa8df8eb 100644 --- a/src/Api/PendingAwaitablePage.php +++ b/src/Api/PendingAwaitablePage.php @@ -170,6 +170,18 @@ private function createAwaitablePage(): AwaitableWebpage */ private function buildAwaitablePage(array $options): AwaitableWebpage { + if (Playwright::shouldRecordVideoOnFailure()) { + // @phpstan-ignore-next-line + $testName = str_replace('__pest_evaluable_', '', test()->name()); + // @phpstan-ignore-next-line + $sanitized = (string) preg_replace('/[^a-zA-Z0-9_-]/', '_', $testName); + $videoDir = sys_get_temp_dir().DIRECTORY_SEPARATOR.'pest-browser-videos'.DIRECTORY_SEPARATOR.uniqid('', true); + mkdir($videoDir, 0755, true); + $options['recordVideo'] = ['dir' => $videoDir]; + // @phpstan-ignore-next-line + Playwright::registerVideoRecording($videoDir, $sanitized); + } + $browser = Playwright::browser($this->browserType)->launch(); $context = $browser->newContext([ diff --git a/src/Configuration.php b/src/Configuration.php index bf3c458f..a99e4e95 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -124,4 +124,14 @@ public function diff(): self return $this; } + + /** + * Records a video of the browser session and saves it on test failure. + */ + public function recordVideoOnFailure(): self + { + Playwright::setRecordVideoOnFailure(); + + return $this; + } } diff --git a/src/Exceptions/BrowserExpectationFailedException.php b/src/Exceptions/BrowserExpectationFailedException.php index 8fff0be3..eb3de960 100644 --- a/src/Exceptions/BrowserExpectationFailedException.php +++ b/src/Exceptions/BrowserExpectationFailedException.php @@ -30,6 +30,12 @@ public static function from(Page $page, ExpectationFailedException $e): Expectat } } + $pendingVideoDestName = Playwright::pendingVideoDestName(); + + if ($pendingVideoDestName !== null) { + $message .= " A video recording of the browser session has been saved to [Tests/Browser/Videos/$pendingVideoDestName.webm]."; + } + $consoleLogs = $page->consoleLogs(); if (count($consoleLogs) > 0) { diff --git a/src/Playwright/Playwright.php b/src/Playwright/Playwright.php index c0cffae1..678a6522 100644 --- a/src/Playwright/Playwright.php +++ b/src/Playwright/Playwright.php @@ -59,6 +59,21 @@ final class Playwright */ private static ?string $host = null; + /** + * Whether to record video on test failure. + */ + private static bool $recordVideoOnFailure = false; + + /** + * The temporary directory where the current test's video is being recorded. + */ + private static ?string $pendingVideoDir = null; + + /** + * The sanitized test name used as the video destination filename. + */ + private static ?string $pendingVideoDestName = null; + /** * Get a browser factory for the given browser type. */ @@ -187,6 +202,56 @@ public static function shouldDebugAssertions(): bool return self::$shouldDebugAssertions; } + /** + * Set whether to record video on test failure. + */ + public static function setRecordVideoOnFailure(): void + { + self::$recordVideoOnFailure = true; + } + + /** + * Whether to record video on test failure. + */ + public static function shouldRecordVideoOnFailure(): bool + { + return self::$recordVideoOnFailure; + } + + /** + * Register a video recording for the current test. + */ + public static function registerVideoRecording(string $dir, string $destName): void + { + self::$pendingVideoDir = $dir; + self::$pendingVideoDestName = $destName; + } + + /** + * Get the pending video temporary directory. + */ + public static function pendingVideoDir(): ?string + { + return self::$pendingVideoDir; + } + + /** + * Get the pending video destination filename (without extension). + */ + public static function pendingVideoDestName(): ?string + { + return self::$pendingVideoDestName; + } + + /** + * Clear the pending video recording state. + */ + public static function clearVideoRecording(): void + { + self::$pendingVideoDir = null; + self::$pendingVideoDestName = null; + } + /** * Reset playwright state, reset browser types, without closing them. */ diff --git a/src/Plugin.php b/src/Plugin.php index 90ff64ca..65cd5a89 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -11,6 +11,7 @@ use Pest\Browser\Exceptions\OptionNotSupportedInParallelException; use Pest\Browser\Filters\UsesBrowserTestCaseMethodFilter; use Pest\Browser\Playwright\Playwright; +use Pest\Browser\Support\Video; use Pest\Contracts\Plugins\Bootable; use Pest\Contracts\Plugins\HandlesArguments; use Pest\Contracts\Plugins\Terminable; @@ -50,9 +51,19 @@ public function boot(): void } } + $videoDir = Playwright::pendingVideoDir(); + $videoDestName = Playwright::pendingVideoDestName(); + ServerManager::instance()->http()->flush(); Playwright::reset(); + + if ($videoDir !== null && $videoDestName !== null) { + /** @var TestStatus $videoStatus */ + $videoStatus = $this->status(); // @phpstan-ignore-line + Video::handleRecording($videoDir, $videoDestName, $videoStatus->isFailure() || $videoStatus->isError()); + Playwright::clearVideoRecording(); + } })->in($this->in()); } @@ -93,6 +104,12 @@ public function handleArguments(array $arguments): array $arguments = $this->popArgument('--light', $arguments); } + if ($this->hasArgument('--record-video', $arguments)) { + Playwright::setRecordVideoOnFailure(); + + $arguments = $this->popArgument('--record-video', $arguments); + } + if ($this->hasArgument('--browser', $arguments)) { $index = array_search('--browser', $arguments, true); diff --git a/src/Support/Video.php b/src/Support/Video.php new file mode 100644 index 00000000..3ba31dc4 --- /dev/null +++ b/src/Support/Video.php @@ -0,0 +1,64 @@ +rootPath + .'/tests/Browser/Videos'; + } + + /** + * Handles a recorded video after a test completes. + * + * If the test failed, moves the video to the videos directory with the test name. + * If the test passed, deletes the temporary video file to save disk space. + */ + public static function handleRecording(string $tempDir, string $destName, bool $testFailed): void + { + $result = glob($tempDir.DIRECTORY_SEPARATOR.'*.webm'); + /** @var array $videos */ + $videos = $result !== false ? $result : []; + + if (! $testFailed || $videos === []) { + foreach ($videos as $video) { + @unlink($video); + } + @rmdir($tempDir); + + return; + } + + if (is_dir(self::dir()) === false) { + mkdir(self::dir(), 0755, true); + } + + $destFile = self::dir().DIRECTORY_SEPARATOR.$destName.'.webm'; + + // Avoid overwriting an existing video with the same name + if (file_exists($destFile)) { + $destFile = self::dir().DIRECTORY_SEPARATOR.$destName.'-'.time().'.webm'; + } + + rename($videos[0], $destFile); + + // Clean up extra videos if multiple pages were recorded in the same context + foreach (array_slice($videos, 1) as $extra) { + @unlink($extra); + } + + @rmdir($tempDir); + } +} diff --git a/tests/Unit/Configuration/VideoConfigurationTest.php b/tests/Unit/Configuration/VideoConfigurationTest.php new file mode 100644 index 00000000..436dcd5a --- /dev/null +++ b/tests/Unit/Configuration/VideoConfigurationTest.php @@ -0,0 +1,58 @@ +getProperty('recordVideoOnFailure'); + $flag->setValue(null, false); + + $dir = $ref->getProperty('pendingVideoDir'); + $dir->setValue(null, null); + + $name = $ref->getProperty('pendingVideoDestName'); + $name->setValue(null, null); +}); + +it('returns false for recordVideoOnFailure by default', function (): void { + expect(Playwright::shouldRecordVideoOnFailure())->toBeFalse(); +}); + +it('sets recordVideoOnFailure flag via Playwright', function (): void { + Playwright::setRecordVideoOnFailure(); + + expect(Playwright::shouldRecordVideoOnFailure())->toBeTrue(); +}); + +it('enables video recording via Configuration fluent API', function (): void { + $config = new Configuration(); + + $result = $config->recordVideoOnFailure(); + + expect($result)->toBeInstanceOf(Configuration::class); + expect(Playwright::shouldRecordVideoOnFailure())->toBeTrue(); +}); + +it('registers a video recording with a directory and destination name', function (): void { + expect(Playwright::pendingVideoDir())->toBeNull(); + expect(Playwright::pendingVideoDestName())->toBeNull(); + + Playwright::registerVideoRecording('/tmp/test-dir', 'my_test'); + + expect(Playwright::pendingVideoDir())->toBe('/tmp/test-dir'); + expect(Playwright::pendingVideoDestName())->toBe('my_test'); +}); + +it('clears video recording state', function (): void { + Playwright::registerVideoRecording('/tmp/test-dir', 'my_test'); + + Playwright::clearVideoRecording(); + + expect(Playwright::pendingVideoDir())->toBeNull(); + expect(Playwright::pendingVideoDestName())->toBeNull(); +}); diff --git a/tests/Unit/Support/VideoTest.php b/tests/Unit/Support/VideoTest.php new file mode 100644 index 00000000..7d1a7cb3 --- /dev/null +++ b/tests/Unit/Support/VideoTest.php @@ -0,0 +1,90 @@ +toBeFalse(); + expect(is_dir($tempDir))->toBeFalse(); +}); + +it('moves the video to the videos directory when the test fails', function (): void { + $tempDir = sys_get_temp_dir().'/pest-video-test-'.uniqid('', true); + mkdir($tempDir, 0755, true); + $videoFile = $tempDir.'/video.webm'; + file_put_contents($videoFile, 'fake-video-data'); + + Video::handleRecording($tempDir, 'my_failed_test', true); + + $destFile = Video::dir().'/my_failed_test.webm'; + + expect(file_exists($destFile))->toBeTrue(); + expect(file_exists($videoFile))->toBeFalse(); + expect(is_dir($tempDir))->toBeFalse(); + + @unlink($destFile); +}); + +it('does nothing when the temp directory has no webm files', function (): void { + $tempDir = sys_get_temp_dir().'/pest-video-test-'.uniqid('', true); + mkdir($tempDir, 0755, true); + + Video::handleRecording($tempDir, 'my_test', true); + + // No exception thrown and temp dir is cleaned up + expect(is_dir($tempDir))->toBeFalse(); +}); + +it('avoids overwriting an existing video by appending a timestamp', function (): void { + $tempDir = sys_get_temp_dir().'/pest-video-test-'.uniqid('', true); + mkdir($tempDir, 0755, true); + $videoFile = $tempDir.'/video.webm'; + file_put_contents($videoFile, 'fake-video-data'); + + // Pre-create a file with the expected destination name + if (is_dir(Video::dir()) === false) { + mkdir(Video::dir(), 0755, true); + } + $existingFile = Video::dir().'/duplicate_test.webm'; + file_put_contents($existingFile, 'existing-video'); + + Video::handleRecording($tempDir, 'duplicate_test', true); + + // The original file must still exist + expect(file_exists($existingFile))->toBeTrue(); + + // A second file with a timestamp suffix must have been created + $videos = glob(Video::dir().'/duplicate_test-*.webm'); + expect($videos)->not->toBeEmpty(); + + @unlink($existingFile); + foreach ((array) $videos as $v) { + @unlink((string) $v); + } +}); + +it('cleans up extra videos when more than one webm is present', function (): void { + $tempDir = sys_get_temp_dir().'/pest-video-test-'.uniqid('', true); + mkdir($tempDir, 0755, true); + + file_put_contents($tempDir.'/first.webm', 'video-1'); + file_put_contents($tempDir.'/second.webm', 'video-2'); + + Video::handleRecording($tempDir, 'multi_page_test', true); + + $destFile = Video::dir().'/multi_page_test.webm'; + expect(file_exists($destFile))->toBeTrue(); + + // Extra file must be gone + expect(file_exists($tempDir.'/second.webm'))->toBeFalse(); + + @unlink($destFile); +}); From ddd23e5c8426a565b288f31c48edad223acce285 Mon Sep 17 00:00:00 2001 From: Veljko Vujanovic Date: Mon, 6 Apr 2026 12:56:15 +0200 Subject: [PATCH 2/3] fix: overwrite existing video on re-run instead of keeping timestamp copy --- src/Support/Video.php | 4 ++-- tests/Unit/Support/VideoTest.php | 26 ++++++++++++-------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Support/Video.php b/src/Support/Video.php index 3ba31dc4..4278ce4b 100644 --- a/src/Support/Video.php +++ b/src/Support/Video.php @@ -47,9 +47,9 @@ public static function handleRecording(string $tempDir, string $destName, bool $ $destFile = self::dir().DIRECTORY_SEPARATOR.$destName.'.webm'; - // Avoid overwriting an existing video with the same name + // Remove any existing video for this test so re-runs stay clean if (file_exists($destFile)) { - $destFile = self::dir().DIRECTORY_SEPARATOR.$destName.'-'.time().'.webm'; + unlink($destFile); } rename($videos[0], $destFile); diff --git a/tests/Unit/Support/VideoTest.php b/tests/Unit/Support/VideoTest.php index 7d1a7cb3..7f565109 100644 --- a/tests/Unit/Support/VideoTest.php +++ b/tests/Unit/Support/VideoTest.php @@ -43,32 +43,30 @@ expect(is_dir($tempDir))->toBeFalse(); }); -it('avoids overwriting an existing video by appending a timestamp', function (): void { +it('overwrites an existing video when the test fails again', function (): void { $tempDir = sys_get_temp_dir().'/pest-video-test-'.uniqid('', true); mkdir($tempDir, 0755, true); $videoFile = $tempDir.'/video.webm'; - file_put_contents($videoFile, 'fake-video-data'); + file_put_contents($videoFile, 'new-video-data'); // Pre-create a file with the expected destination name if (is_dir(Video::dir()) === false) { mkdir(Video::dir(), 0755, true); } - $existingFile = Video::dir().'/duplicate_test.webm'; - file_put_contents($existingFile, 'existing-video'); + $destFile = Video::dir().'/overwrite_test.webm'; + file_put_contents($destFile, 'old-video-data'); - Video::handleRecording($tempDir, 'duplicate_test', true); + Video::handleRecording($tempDir, 'overwrite_test', true); - // The original file must still exist - expect(file_exists($existingFile))->toBeTrue(); + // Destination must contain the new content + expect(file_exists($destFile))->toBeTrue(); + expect(file_get_contents($destFile))->toBe('new-video-data'); - // A second file with a timestamp suffix must have been created - $videos = glob(Video::dir().'/duplicate_test-*.webm'); - expect($videos)->not->toBeEmpty(); + // No timestamp-suffixed files should exist + $extras = glob(Video::dir().'/overwrite_test-*.webm'); + expect($extras)->toBeEmpty(); - @unlink($existingFile); - foreach ((array) $videos as $v) { - @unlink((string) $v); - } + @unlink($destFile); }); it('cleans up extra videos when more than one webm is present', function (): void { From fdb81aff775a2f2b1b54697d1b4b0733fa9ca67f Mon Sep 17 00:00:00 2001 From: "Veljko V." Date: Sat, 11 Apr 2026 21:04:55 +0200 Subject: [PATCH 3/3] fix: record video on failure using Playwright Artifact.saveAs --- .gitignore | 1 + src/Api/PendingAwaitablePage.php | 31 ++++--- src/Playwright/Context.php | 14 +++- src/Playwright/Page.php | 50 +++++++++-- src/Playwright/Playwright.php | 22 ++--- src/Plugin.php | 36 +++++--- src/Support/Video.php | 42 ---------- .../Configuration/VideoConfigurationTest.php | 18 ++-- tests/Unit/Support/VideoTest.php | 83 +------------------ 9 files changed, 125 insertions(+), 172 deletions(-) diff --git a/.gitignore b/.gitignore index 4733d042..ab06b104 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ coverage.xml # Playwright node_modules/ /tests/Browser/Screenshots +/tests/Browser/Videos # MacOS .DS_Store diff --git a/src/Api/PendingAwaitablePage.php b/src/Api/PendingAwaitablePage.php index aa8df8eb..10b5edda 100644 --- a/src/Api/PendingAwaitablePage.php +++ b/src/Api/PendingAwaitablePage.php @@ -175,29 +175,40 @@ private function buildAwaitablePage(array $options): AwaitableWebpage $testName = str_replace('__pest_evaluable_', '', test()->name()); // @phpstan-ignore-next-line $sanitized = (string) preg_replace('/[^a-zA-Z0-9_-]/', '_', $testName); - $videoDir = sys_get_temp_dir().DIRECTORY_SEPARATOR.'pest-browser-videos'.DIRECTORY_SEPARATOR.uniqid('', true); - mkdir($videoDir, 0755, true); - $options['recordVideo'] = ['dir' => $videoDir]; - // @phpstan-ignore-next-line - Playwright::registerVideoRecording($videoDir, $sanitized); } - $browser = Playwright::browser($this->browserType)->launch(); - - $context = $browser->newContext([ + $contextOptions = [ 'locale' => 'en-US', 'timezoneId' => 'UTC', 'colorScheme' => Playwright::defaultColorScheme()->value, ...$this->device->context(), ...$options, - ]); + ]; + + if (Playwright::shouldRecordVideoOnFailure()) { + // Empty array — Playwright uses its own temp storage; we retrieve via saveAs + $contextOptions['recordVideo'] = []; + } + + $browser = Playwright::browser($this->browserType)->launch(); + + $context = $browser->newContext($contextOptions); $context->addInitScript(InitScript::get()); $url = ComputeUrl::from($this->url); + // Strip context-only options before passing to goto + $gotoOptions = array_diff_key($options, array_flip(['host'])); + + $page = $context->newPage()->goto($url, $gotoOptions); + + if (Playwright::shouldRecordVideoOnFailure()) { + Playwright::registerVideoRecording($page, $sanitized); // @phpstan-ignore-line + } + return new AwaitableWebpage( - $context->newPage()->goto($url, $options), + $page, $url, ); } diff --git a/src/Playwright/Context.php b/src/Playwright/Context.php index 447d8582..b83b55fd 100644 --- a/src/Playwright/Context.php +++ b/src/Playwright/Context.php @@ -45,8 +45,9 @@ public function newPage(): Page $frameGuid = ''; $pageGuid = ''; + $artifactGuid = null; - /** @var array{method: string|null, params: array{type: string|null, guid: string, initializer: array{url: string}}, result: array{page: array{guid: string|null}}} $message */ + /** @var array{method: string|null, params: array{type: string|null, guid: string, initializer: array{url: string, mainFrame?: array{guid: string}, video?: array{guid: string}}}, result: array{page: array{guid: string|null}}} $message */ foreach ($response as $message) { if (isset($message['method']) && $message['method'] === '__create__' && (isset($message['params']['type']) && $message['params']['type'] === 'Frame')) { $frameGuid = $message['params']['guid']; @@ -55,9 +56,18 @@ public function newPage(): Page if (isset($message['result']['page']['guid'])) { $pageGuid = $message['result']['page']['guid']; } + + // The video recording artifact GUID is sent in the Page __create__ initializer + if ( + isset($message['method'], $message['params']['type'], $message['params']['initializer']['video']['guid']) + && $message['method'] === '__create__' + && $message['params']['type'] === 'Page' + ) { + $artifactGuid = $message['params']['initializer']['video']['guid']; + } } - return new Page($this, $pageGuid, $frameGuid); + return new Page($this, $pageGuid, $frameGuid, $artifactGuid); } /** diff --git a/src/Playwright/Page.php b/src/Playwright/Page.php index d97f3db3..84c35e6a 100644 --- a/src/Playwright/Page.php +++ b/src/Playwright/Page.php @@ -39,6 +39,7 @@ public function __construct( private readonly Context $context, private readonly string $guid, private readonly string $frameGuid, + private readonly ?string $artifactGuid = null, ) { // } @@ -51,6 +52,42 @@ public function context(): Context return $this->context; } + /** + * Save the recorded video to the given absolute path. + * This calls Playwright's Artifact.saveAs and blocks until the file is written. + * Returns false if no video artifact is associated with this page. + */ + public function saveVideo(string $destPath): bool + { + if ($this->artifactGuid === null) { + return false; + } + + $response = Client::instance()->execute($this->artifactGuid, 'saveAs', ['path' => $destPath]); + + foreach ($response as $_) { + // consume until done + } + + return true; + } + + /** + * Delete the recorded video artifact (used when test passes). + */ + public function deleteVideo(): void + { + if ($this->artifactGuid === null) { + return; + } + + $response = Client::instance()->execute($this->artifactGuid, 'delete'); + + foreach ($response as $_) { + // consume + } + } + /** * Get the current URL of the page. */ @@ -480,7 +517,6 @@ public function brokenImages(): array /** @var array $brokenImages */ return $brokenImages; - } /** @@ -541,7 +577,8 @@ public function expectScreenshot(bool $fullPage, bool $openDiff): void $openDiff ); - throw new ExpectationFailedException(<<<'EOT' + throw new ExpectationFailedException( + <<<'EOT' Screenshot does not match the last one. - Expected? Update the snapshots with [--update-snapshots]. - Not expected? Re-run the test with [--diff] to see the differences. @@ -558,7 +595,8 @@ public function expectScreenshot(bool $fullPage, bool $openDiff): void $openDiff, ); - throw new ExpectationFailedException(<<<'EOT' + throw new ExpectationFailedException( + <<<'EOT' Screenshot does not match the last one. - Expected? Update the snapshots with [--update-snapshots]. EOT, @@ -571,9 +609,11 @@ public function expectScreenshot(bool $fullPage, bool $openDiff): void */ public function close(): void { - if ($this->context->browser()->isClosed() + if ( + $this->context->browser()->isClosed() || $this->context->isClosed() - || $this->closed) { + || $this->closed + ) { return; } diff --git a/src/Playwright/Playwright.php b/src/Playwright/Playwright.php index 678a6522..c1221dc5 100644 --- a/src/Playwright/Playwright.php +++ b/src/Playwright/Playwright.php @@ -60,14 +60,14 @@ final class Playwright private static ?string $host = null; /** - * Whether to record video on test failure. + * Whether to record video on failure. */ private static bool $recordVideoOnFailure = false; /** - * The temporary directory where the current test's video is being recorded. + * The page being recorded for the current test. */ - private static ?string $pendingVideoDir = null; + private static ?Page $pendingVideoPage = null; /** * The sanitized test name used as the video destination filename. @@ -203,7 +203,7 @@ public static function shouldDebugAssertions(): bool } /** - * Set whether to record video on test failure. + * Enable video recording on failure. */ public static function setRecordVideoOnFailure(): void { @@ -211,7 +211,7 @@ public static function setRecordVideoOnFailure(): void } /** - * Whether to record video on test failure. + * Whether video recording on failure is enabled. */ public static function shouldRecordVideoOnFailure(): bool { @@ -221,18 +221,18 @@ public static function shouldRecordVideoOnFailure(): bool /** * Register a video recording for the current test. */ - public static function registerVideoRecording(string $dir, string $destName): void + public static function registerVideoRecording(Page $page, string $destName): void { - self::$pendingVideoDir = $dir; + self::$pendingVideoPage = $page; self::$pendingVideoDestName = $destName; } /** - * Get the pending video temporary directory. + * Get the pending video page. */ - public static function pendingVideoDir(): ?string + public static function pendingVideoPage(): ?Page { - return self::$pendingVideoDir; + return self::$pendingVideoPage; } /** @@ -248,7 +248,7 @@ public static function pendingVideoDestName(): ?string */ public static function clearVideoRecording(): void { - self::$pendingVideoDir = null; + self::$pendingVideoPage = null; self::$pendingVideoDestName = null; } diff --git a/src/Plugin.php b/src/Plugin.php index 65cd5a89..7f1ab18d 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -51,17 +51,33 @@ public function boot(): void } } - $videoDir = Playwright::pendingVideoDir(); + $videoPage = Playwright::pendingVideoPage(); $videoDestName = Playwright::pendingVideoDestName(); ServerManager::instance()->http()->flush(); Playwright::reset(); - if ($videoDir !== null && $videoDestName !== null) { + if ($videoPage instanceof \Pest\Browser\Playwright\Page && $videoDestName !== null) { /** @var TestStatus $videoStatus */ $videoStatus = $this->status(); // @phpstan-ignore-line - Video::handleRecording($videoDir, $videoDestName, $videoStatus->isFailure() || $videoStatus->isError()); + + if ($videoStatus->isFailure() || $videoStatus->isError()) { + if (is_dir(Video::dir()) === false) { + mkdir(Video::dir(), 0755, true); + } + + $destFile = Video::dir().DIRECTORY_SEPARATOR.$videoDestName.'.webm'; + + if (file_exists($destFile)) { + unlink($destFile); + } + + $videoPage->saveVideo($destFile); + } else { + $videoPage->deleteVideo(); + } + Playwright::clearVideoRecording(); } })->in($this->in()); @@ -80,6 +96,12 @@ public function handleArguments(array $arguments): array $arguments = $this->popArgument('--headed', $arguments); } + if ($this->hasArgument('--record-video', $arguments)) { + Playwright::setRecordVideoOnFailure(); + + $arguments = $this->popArgument('--record-video', $arguments); + } + if ($this->hasArgument('--diff', $arguments)) { Playwright::setShouldDiffOnScreenshotAssertions(); @@ -104,12 +126,6 @@ public function handleArguments(array $arguments): array $arguments = $this->popArgument('--light', $arguments); } - if ($this->hasArgument('--record-video', $arguments)) { - Playwright::setRecordVideoOnFailure(); - - $arguments = $this->popArgument('--record-video', $arguments); - } - if ($this->hasArgument('--browser', $arguments)) { $index = array_search('--browser', $arguments, true); @@ -124,7 +140,7 @@ public function handleArguments(array $arguments): array if (($browser = BrowserType::tryFrom($browser)) === null) { throw new BrowserNotSupportedException( 'The specified browser type is not supported. Supported types are: '. - implode(', ', array_map(fn (BrowserType $type): string => mb_strtolower($type->name), BrowserType::cases())) + implode(', ', array_map(fn (BrowserType $type): string => mb_strtolower($type->name), BrowserType::cases())) ); } diff --git a/src/Support/Video.php b/src/Support/Video.php index 4278ce4b..15c33aef 100644 --- a/src/Support/Video.php +++ b/src/Support/Video.php @@ -19,46 +19,4 @@ public static function dir(): string return TestSuite::getInstance()->rootPath .'/tests/Browser/Videos'; } - - /** - * Handles a recorded video after a test completes. - * - * If the test failed, moves the video to the videos directory with the test name. - * If the test passed, deletes the temporary video file to save disk space. - */ - public static function handleRecording(string $tempDir, string $destName, bool $testFailed): void - { - $result = glob($tempDir.DIRECTORY_SEPARATOR.'*.webm'); - /** @var array $videos */ - $videos = $result !== false ? $result : []; - - if (! $testFailed || $videos === []) { - foreach ($videos as $video) { - @unlink($video); - } - @rmdir($tempDir); - - return; - } - - if (is_dir(self::dir()) === false) { - mkdir(self::dir(), 0755, true); - } - - $destFile = self::dir().DIRECTORY_SEPARATOR.$destName.'.webm'; - - // Remove any existing video for this test so re-runs stay clean - if (file_exists($destFile)) { - unlink($destFile); - } - - rename($videos[0], $destFile); - - // Clean up extra videos if multiple pages were recorded in the same context - foreach (array_slice($videos, 1) as $extra) { - @unlink($extra); - } - - @rmdir($tempDir); - } } diff --git a/tests/Unit/Configuration/VideoConfigurationTest.php b/tests/Unit/Configuration/VideoConfigurationTest.php index 436dcd5a..833ad44c 100644 --- a/tests/Unit/Configuration/VideoConfigurationTest.php +++ b/tests/Unit/Configuration/VideoConfigurationTest.php @@ -6,14 +6,13 @@ use Pest\Browser\Playwright\Playwright; beforeEach(function (): void { - // Reflect into private Playwright state and reset video-related fields $ref = new ReflectionClass(Playwright::class); $flag = $ref->getProperty('recordVideoOnFailure'); $flag->setValue(null, false); - $dir = $ref->getProperty('pendingVideoDir'); - $dir->setValue(null, null); + $page = $ref->getProperty('pendingVideoPage'); + $page->setValue(null, null); $name = $ref->getProperty('pendingVideoDestName'); $name->setValue(null, null); @@ -38,21 +37,14 @@ expect(Playwright::shouldRecordVideoOnFailure())->toBeTrue(); }); -it('registers a video recording with a directory and destination name', function (): void { - expect(Playwright::pendingVideoDir())->toBeNull(); +it('returns null for pendingVideoPage and pendingVideoDestName by default', function (): void { + expect(Playwright::pendingVideoPage())->toBeNull(); expect(Playwright::pendingVideoDestName())->toBeNull(); - - Playwright::registerVideoRecording('/tmp/test-dir', 'my_test'); - - expect(Playwright::pendingVideoDir())->toBe('/tmp/test-dir'); - expect(Playwright::pendingVideoDestName())->toBe('my_test'); }); it('clears video recording state', function (): void { - Playwright::registerVideoRecording('/tmp/test-dir', 'my_test'); - Playwright::clearVideoRecording(); - expect(Playwright::pendingVideoDir())->toBeNull(); + expect(Playwright::pendingVideoPage())->toBeNull(); expect(Playwright::pendingVideoDestName())->toBeNull(); }); diff --git a/tests/Unit/Support/VideoTest.php b/tests/Unit/Support/VideoTest.php index 7f565109..7f0446f8 100644 --- a/tests/Unit/Support/VideoTest.php +++ b/tests/Unit/Support/VideoTest.php @@ -4,85 +4,10 @@ use Pest\Browser\Support\Video; -it('deletes the video when the test passes', function (): void { - $tempDir = sys_get_temp_dir().'/pest-video-test-'.uniqid('', true); - mkdir($tempDir, 0755, true); - $videoFile = $tempDir.'/video.webm'; - file_put_contents($videoFile, 'fake-video-data'); - - Video::handleRecording($tempDir, 'my_test', false); - - expect(file_exists($videoFile))->toBeFalse(); - expect(is_dir($tempDir))->toBeFalse(); -}); - -it('moves the video to the videos directory when the test fails', function (): void { - $tempDir = sys_get_temp_dir().'/pest-video-test-'.uniqid('', true); - mkdir($tempDir, 0755, true); - $videoFile = $tempDir.'/video.webm'; - file_put_contents($videoFile, 'fake-video-data'); - - Video::handleRecording($tempDir, 'my_failed_test', true); - - $destFile = Video::dir().'/my_failed_test.webm'; - - expect(file_exists($destFile))->toBeTrue(); - expect(file_exists($videoFile))->toBeFalse(); - expect(is_dir($tempDir))->toBeFalse(); - - @unlink($destFile); -}); - -it('does nothing when the temp directory has no webm files', function (): void { - $tempDir = sys_get_temp_dir().'/pest-video-test-'.uniqid('', true); - mkdir($tempDir, 0755, true); - - Video::handleRecording($tempDir, 'my_test', true); - - // No exception thrown and temp dir is cleaned up - expect(is_dir($tempDir))->toBeFalse(); +it('returns the videos directory path', function (): void { + expect(Video::dir())->toEndWith('/tests/Browser/Videos'); }); -it('overwrites an existing video when the test fails again', function (): void { - $tempDir = sys_get_temp_dir().'/pest-video-test-'.uniqid('', true); - mkdir($tempDir, 0755, true); - $videoFile = $tempDir.'/video.webm'; - file_put_contents($videoFile, 'new-video-data'); - - // Pre-create a file with the expected destination name - if (is_dir(Video::dir()) === false) { - mkdir(Video::dir(), 0755, true); - } - $destFile = Video::dir().'/overwrite_test.webm'; - file_put_contents($destFile, 'old-video-data'); - - Video::handleRecording($tempDir, 'overwrite_test', true); - - // Destination must contain the new content - expect(file_exists($destFile))->toBeTrue(); - expect(file_get_contents($destFile))->toBe('new-video-data'); - - // No timestamp-suffixed files should exist - $extras = glob(Video::dir().'/overwrite_test-*.webm'); - expect($extras)->toBeEmpty(); - - @unlink($destFile); -}); - -it('cleans up extra videos when more than one webm is present', function (): void { - $tempDir = sys_get_temp_dir().'/pest-video-test-'.uniqid('', true); - mkdir($tempDir, 0755, true); - - file_put_contents($tempDir.'/first.webm', 'video-1'); - file_put_contents($tempDir.'/second.webm', 'video-2'); - - Video::handleRecording($tempDir, 'multi_page_test', true); - - $destFile = Video::dir().'/multi_page_test.webm'; - expect(file_exists($destFile))->toBeTrue(); - - // Extra file must be gone - expect(file_exists($tempDir.'/second.webm'))->toBeFalse(); - - @unlink($destFile); +it('videos directory is under tests/Browser', function (): void { + expect(Video::dir())->toContain('tests/Browser/Videos'); });