From 4c30a6d99e4726179b9815347c466e5ed54817f8 Mon Sep 17 00:00:00 2001 From: Jonathan Mateman Date: Tue, 3 Feb 2026 17:48:22 +0100 Subject: [PATCH] Add playwright tracing support --- .gitignore | 1 + src/Api/Concerns/HasTracing.php | 45 +++++ src/Api/Webpage.php | 3 +- src/Enums/TracingOption.php | 15 ++ .../TracingOptionNotSupportedException.php | 15 ++ .../UsesBrowserTestCaseMethodFilter.php | 2 + src/Playwright/Artifact.php | 33 ++++ src/Playwright/Browser.php | 19 +- src/Playwright/Context.php | 11 ++ src/Playwright/Playwright.php | 22 +++ src/Playwright/Tracing.php | 169 ++++++++++++++++++ src/Plugin.php | 46 ++++- tests/Browser/Webpage/TracingTest.php | 30 ++++ 13 files changed, 402 insertions(+), 9 deletions(-) create mode 100644 src/Api/Concerns/HasTracing.php create mode 100644 src/Enums/TracingOption.php create mode 100644 src/Exceptions/TracingOptionNotSupportedException.php create mode 100644 src/Playwright/Artifact.php create mode 100644 src/Playwright/Tracing.php create mode 100644 tests/Browser/Webpage/TracingTest.php diff --git a/.gitignore b/.gitignore index 4733d042..4892e98d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ coverage.xml # Playwright node_modules/ /tests/Browser/Screenshots +/tests/Browser/Tracing # MacOS .DS_Store diff --git a/src/Api/Concerns/HasTracing.php b/src/Api/Concerns/HasTracing.php new file mode 100644 index 00000000..449d9291 --- /dev/null +++ b/src/Api/Concerns/HasTracing.php @@ -0,0 +1,45 @@ +page->context()->tracing()->start([ + 'screenshots' => $screenshots, + 'snapshots' => $snapshots, + ]); + $this->page->context()->tracing()->startChunk(); + + return $this; + } + + /** + * Stops tracing and saves artifact + */ + public function stopTracing(?string $filename = null): self + { + $artifact = $this->page->context()->tracing()->stopChunk([ + 'mode' => 'archive', + ]); + $this->page->context()->tracing()->stop(); + + $artifact->saveAs(['path' => Tracing::path($filename)]); + + return $this; + } +} diff --git a/src/Api/Webpage.php b/src/Api/Webpage.php index d42876a7..e38b0db4 100644 --- a/src/Api/Webpage.php +++ b/src/Api/Webpage.php @@ -11,7 +11,8 @@ final readonly class Webpage { - use Concerns\HasWaitCapabilities, + use Concerns\HasTracing, + Concerns\HasWaitCapabilities, Concerns\InteractsWithElements, Concerns\InteractsWithFrames, Concerns\InteractsWithScreen, diff --git a/src/Enums/TracingOption.php b/src/Enums/TracingOption.php new file mode 100644 index 00000000..bd139915 --- /dev/null +++ b/src/Enums/TracingOption.php @@ -0,0 +1,15 @@ +playwright()->start(); Screenshot::cleanup(); + Tracing::cleanup(); } return true; diff --git a/src/Playwright/Artifact.php b/src/Playwright/Artifact.php new file mode 100644 index 00000000..35f56233 --- /dev/null +++ b/src/Playwright/Artifact.php @@ -0,0 +1,33 @@ + $params + */ + public function saveAs(array $params = []): void + { + $response = Client::instance()->execute($this->guid, 'saveAs', $params); + $this->processVoidResponse($response); + } +} diff --git a/src/Playwright/Browser.php b/src/Playwright/Browser.php index 12b0df26..a831c140 100644 --- a/src/Playwright/Browser.php +++ b/src/Playwright/Browser.php @@ -4,6 +4,7 @@ namespace Pest\Browser\Playwright; +use Pest\Browser\Enums\TracingOption; use Pest\Browser\Exceptions\BrowserAlreadyClosedException; /** @@ -47,15 +48,29 @@ public function newContext(array $options = []): Context $response = Client::instance()->execute($this->guid, 'newContext', $options); - /** @var array{result: array{context: array{guid: string|null}}} $message */ + /** @var array{result: array{context: array{guid: string|null}}, params: array{type: string|null, guid: string}} $message */ foreach ($response as $message) { + if (isset($message['params']['type']) && $message['params']['type'] === 'Tracing') { + $tracing = new Tracing($message['params']['guid']); + } if (isset($message['result']['context']['guid'])) { - $context = new Context($this, $message['result']['context']['guid']); + assert(isset($tracing), 'Tracing object was not initialized.'); + $context = new Context($this, $tracing, $message['result']['context']['guid']); } } + assert(isset($tracing), 'Tracing object was not initialized.'); assert(isset($context), 'Browser context was not created successfully.'); + // Auto start tracing + if (Playwright::tracingOption() !== TracingOption::OFF) { + $tracing->start([ + 'screenshots' => true, + 'snapshots' => true, + ]); + $tracing->startChunk(); + } + $this->contexts[] = $context; return $context; diff --git a/src/Playwright/Context.php b/src/Playwright/Context.php index 447d8582..3966a28a 100644 --- a/src/Playwright/Context.php +++ b/src/Playwright/Context.php @@ -23,6 +23,7 @@ final class Context */ public function __construct( private readonly Browser $browser, + private readonly Tracing $tracing, private readonly string $guid ) { // @@ -36,6 +37,14 @@ public function browser(): Browser return $this->browser; } + /** + * Gets the tracing instance. + */ + public function tracing(): Tracing + { + return $this->tracing; + } + /** * Creates a new page in the context. */ @@ -69,6 +78,8 @@ public function close(): void return; } + $this->tracing->close(); + try { // fix this... $response = $this->sendMessage('close'); diff --git a/src/Playwright/Playwright.php b/src/Playwright/Playwright.php index c0cffae1..7e4c4836 100644 --- a/src/Playwright/Playwright.php +++ b/src/Playwright/Playwright.php @@ -6,6 +6,7 @@ use Pest\Browser\Enums\BrowserType; use Pest\Browser\Enums\ColorScheme; +use Pest\Browser\Enums\TracingOption; /** * @internal @@ -34,6 +35,11 @@ final class Playwright */ private static bool $shouldDiffOnScreenshotAssertions = false; + /** + * The tracing option. + */ + private static TracingOption $tracingOption = TracingOption::OFF; + /** * The default browser type. */ @@ -197,6 +203,22 @@ public static function reset(): void } } + /** + * Sets the default browser type. + */ + public static function setTracingOption(TracingOption $tracingOption): void + { + self::$tracingOption = $tracingOption; + } + + /** + * Get the default browser type. + */ + public static function tracingOption(): TracingOption + { + return self::$tracingOption; + } + /** * Sets the default browser type. */ diff --git a/src/Playwright/Tracing.php b/src/Playwright/Tracing.php new file mode 100644 index 00000000..6aee9e39 --- /dev/null +++ b/src/Playwright/Tracing.php @@ -0,0 +1,169 @@ +rootPath + .'/tests/Browser/Tracing'; + } + + /** + * Return the path for a tracing file. + */ + public static function path(?string $filename = null): string + { + if ($filename === null) { + // @phpstan-ignore-next-line + $filename = str_replace('__pest_evaluable_', '', test()->name()); + } + + $path = self::dir().'/'.mb_ltrim($filename, '/'); + + // check if there is extension, if not, add .zip + if (pathinfo($path, PATHINFO_EXTENSION) === '') { + $path .= '.zip'; + } + + return $path; + } + + /** + * Clean up the tracing directory. + */ + public static function cleanup(): void + { + if (is_dir(self::dir()) === false) { + return; + } + + $files = glob(self::dir().'/*'); + + if (is_array($files)) { + foreach ($files as $file) { + @unlink($file); + } + } + + @rmdir(self::dir()); + } + + /** + * Start tracing + * + * @param array $params + */ + public function start(array $params = []): void + { + if ($this->isTracing) { + return; + } + $response = Client::instance()->execute($this->guid, 'tracingStart', $params); + $this->processVoidResponse($response); + $this->isTracing = true; + } + + /** + * Start chunk + * + * @param array $params + */ + public function startChunk(array $params = []): void + { + if ($this->isTracingChunk) { + return; + } + $response = Client::instance()->execute($this->guid, 'tracingStartChunk', $params); + $this->processVoidResponse($response); + $this->isTracingChunk = true; + } + + /** + * Stop chunk + * + * @param array $params + */ + public function stopChunk(array $params = []): Artifact + { + $response = Client::instance()->execute($this->guid, 'tracingStopChunk', $params); + + /** @var array{result: array{artifact: array{guid: string|null}}} $message */ + foreach ($response as $message) { + if (isset($message['result']['artifact']['guid'])) { + $artifact = new Artifact($message['result']['artifact']['guid']); + } + } + + assert(isset($artifact), 'Tracing artifact was not created'); + + $this->isTracingChunk = false; + + return $artifact; + } + + /** + * Stop tracing + * + * @param array $params + */ + public function stop(array $params = []): void + { + $response = Client::instance()->execute($this->guid, 'tracingStop', $params); + $this->processVoidResponse($response); + + $this->isTracing = false; + } + + public function close(): void + { + if ($this->isTracingChunk) { + if (Playwright::tracingOption() === TracingOption::OFF) { + $this->stopChunk([ + 'mode' => 'discard', + ]); + } else { + $artifact = $this->stopChunk([ + 'mode' => 'archive', + ]); + + if (is_dir(self::dir()) === false) { + @mkdir(self::dir(), 0755, true); + } + + $artifact->saveAs(['path' => self::path()]); + } + } + + if ($this->isTracing) { + $this->stop(); + } + } +} diff --git a/src/Plugin.php b/src/Plugin.php index 90ff64ca..93f09c07 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -7,10 +7,13 @@ use Error; use Pest\Browser\Enums\BrowserType; use Pest\Browser\Enums\ColorScheme; +use Pest\Browser\Enums\TracingOption; use Pest\Browser\Exceptions\BrowserNotSupportedException; use Pest\Browser\Exceptions\OptionNotSupportedInParallelException; +use Pest\Browser\Exceptions\TracingOptionNotSupportedException; use Pest\Browser\Filters\UsesBrowserTestCaseMethodFilter; use Pest\Browser\Playwright\Playwright; +use Pest\Browser\Playwright\Tracing; use Pest\Contracts\Plugins\Bootable; use Pest\Contracts\Plugins\HandlesArguments; use Pest\Contracts\Plugins\Terminable; @@ -41,18 +44,24 @@ public function boot(): void ->addTestCaseMethodFilter(new UsesBrowserTestCaseMethodFilter()); pest()->afterEach(function (): void { - if (Playwright::shouldDebugAssertions()) { - /** @var TestStatus $status */ - $status = $this->status(); // @phpstan-ignore-line + /** @var TestStatus $status */ + $status = $this->status(); // @phpstan-ignore-line + $failed_or_error = $status->isFailure() || $status->isError(); - if ($status->isFailure() || $status->isError()) { - Execution::instance()->debug($status); - } + if (Playwright::shouldDebugAssertions() && $failed_or_error) { + Execution::instance()->debug($status); } ServerManager::instance()->http()->flush(); Playwright::reset(); + + if (Playwright::tracingOption() === TracingOption::RETAIN_ON_FAILURE && $failed_or_error === false) { + @unlink(Tracing::path()); + if (glob(Tracing::dir().'/*') === []) { + @rmdir(Tracing::dir()); + } + } })->in($this->in()); } @@ -93,6 +102,31 @@ public function handleArguments(array $arguments): array $arguments = $this->popArgument('--light', $arguments); } + if ($this->hasArgument('--trace', $arguments)) { + $index = array_search('--trace', $arguments, true); + + if ($index === false || ! isset($arguments[$index + 1])) { + throw new TracingOptionNotSupportedException( + 'The "--trace" argument requires a value. Usage: --trace