From 3a95586f0e5096f1aebe84e1a74699318aee23c4 Mon Sep 17 00:00:00 2001 From: Tim Geisendoerfer Date: Sun, 7 Sep 2025 22:02:05 +0200 Subject: [PATCH 1/3] add cookie assertions --- src/Api/Concerns/MakesCookieAssertions.php | 74 ++++++++++ src/Api/Webpage.php | 1 + src/Playwright/Page.php | 139 +++++++++++------- .../Webpage/AssertCookieMissingTest.php | 57 +++++++ tests/Browser/Webpage/AssertHasCookieTest.php | 57 +++++++ tests/Browser/Webpage/AssertNoCookiesTest.php | 26 ++++ 6 files changed, 298 insertions(+), 56 deletions(-) create mode 100644 src/Api/Concerns/MakesCookieAssertions.php create mode 100644 tests/Browser/Webpage/AssertCookieMissingTest.php create mode 100644 tests/Browser/Webpage/AssertHasCookieTest.php create mode 100644 tests/Browser/Webpage/AssertNoCookiesTest.php diff --git a/src/Api/Concerns/MakesCookieAssertions.php b/src/Api/Concerns/MakesCookieAssertions.php new file mode 100644 index 00000000..0b01430e --- /dev/null +++ b/src/Api/Concerns/MakesCookieAssertions.php @@ -0,0 +1,74 @@ +page->cookies(); + + expect($cookies) + ->toHaveKey($key, $value, sprintf( + 'Expected cookie [%s] to be present on the page initially with the url [%s], but it was not found', + $key, + $this->initialUrl, + )); + + return $this; + } + + /** + * Asserts the page does not have a specific cookie. + * + * @param string $key The name of the cookie. + * @param mixed $value The value of the cookie. + */ + public function assertCookieMissing(string $key, mixed $value = new Any()): Webpage + { + $cookies = $this->page->cookies(); + + expect($cookies) + ->not()->toHaveKey($key, $value, sprintf( + 'Expected cookie [%s] to not be present on the page initially with the url [%s], but it was found', + $key, + $this->initialUrl, + )); + + return $this; + } + + /** + * Asserts there are no cookies on the page. + */ + public function assertNoCookies(): Webpage + { + $cookies = $this->page->cookies(); + + expect($cookies)->toBeEmpty(sprintf( + 'Expected no cookies on the page initially with the url [%s], but found %s: %s', + $this->initialUrl, + count($cookies), + implode(', ', array_keys($cookies)), + )); + + return $this; + } +} diff --git a/src/Api/Webpage.php b/src/Api/Webpage.php index 37271685..2312fc8e 100644 --- a/src/Api/Webpage.php +++ b/src/Api/Webpage.php @@ -19,6 +19,7 @@ final class Webpage Concerns\InteractsWithToolbar, Concerns\InteractsWithViewPort, Concerns\MakesConsoleAssertions, + Concerns\MakesCookieAssertions, Concerns\MakesElementAssertions, Concerns\MakesScreenshotAssertions, Concerns\MakesUrlAssertions; diff --git a/src/Playwright/Page.php b/src/Playwright/Page.php index d97f3db3..bbc84706 100644 --- a/src/Playwright/Page.php +++ b/src/Playwright/Page.php @@ -5,6 +5,7 @@ namespace Pest\Browser\Playwright; use Generator; +use Illuminate\Support\Str; use Pest\Browser\Execution; use Pest\Browser\Support\ImageDiffView; use Pest\Browser\Support\JavaScriptSerializer; @@ -65,6 +66,21 @@ public function url(): string return $url; } + /** + * Evaluates a JavaScript expression in the page context. + */ + public function evaluate(string $pageFunction, mixed $arg = null): mixed + { + $params = [ + 'expression' => $pageFunction, + 'arg' => JavaScriptSerializer::serializeArgument($arg), + ]; + + $response = $this->sendMessage('evaluateExpression', $params); + + return $this->processResultResponse($response); + } + /** * Performs the given callback in unstrict mode. * @@ -121,6 +137,14 @@ public function querySelector(string $selector): ?Element return $this->locator($selector)->elementHandle(); } + /** + * Create a locator for the specified selector. + */ + public function locator(string $selector): Locator + { + return new Locator($this->frameGuid, $selector, $this->strictLocators); + } + /** * Finds all elements matching the specified selector. * @@ -145,14 +169,6 @@ public function querySelectorAll(string $selector): array return $elements; } - /** - * Create a locator for the specified selector. - */ - public function locator(string $selector): Locator - { - return new Locator($this->frameGuid, $selector, $this->strictLocators); - } - /** * Create a locator that matches elements by role. * @@ -342,21 +358,6 @@ public function setContent(string $html): self return $this; } - /** - * Evaluates a JavaScript expression in the page context. - */ - public function evaluate(string $pageFunction, mixed $arg = null): mixed - { - $params = [ - 'expression' => $pageFunction, - 'arg' => JavaScriptSerializer::serializeArgument($arg), - ]; - - $response = $this->sendMessage('evaluateExpression', $params); - - return $this->processResultResponse($response); - } - /** * Evaluates a JavaScript expression and returns a JSHandle. */ @@ -425,26 +426,26 @@ public function reload(): self } /** - * Make screenshot of the page. + * Make screenshot of a specific element. */ - public function screenshot(bool $fullPage = true, ?string $filename = null): ?string + public function screenshotElement(string $selector, ?string $filename = null): string { - $binary = $this->screenshotBinary($fullPage); - - if ($binary === null) { - return null; - } + $locator = $this->locator($selector); + $binary = $locator->screenshot(); return Screenshot::save($binary, $filename); } /** - * Make screenshot of a specific element. + * Make screenshot of the page. */ - public function screenshotElement(string $selector, ?string $filename = null): string + public function screenshot(bool $fullPage = true, ?string $filename = null): ?string { - $locator = $this->locator($selector); - $binary = $locator->screenshot(); + $binary = $this->screenshotBinary($fullPage); + + if ($binary === null) { + return null; + } return Screenshot::save($binary, $filename); } @@ -463,6 +464,31 @@ public function consoleLogs(): array return $consoleLogs; } + /** + * Get the cookies from the page, if any. + * + * @return array + */ + public function cookies(): array + { + $cookieString = $this->evaluate('document.cookie || []'); + + if (blank($cookieString)) { + return []; + } + + /** @var array $cookies */ + $cookies = Str::of(is_string($cookieString) ? $cookieString : '') + ->explode(';') + ->mapWithKeys(function (string $cookie): array { + $value = explode('=', $cookie); + + return [Str::trim($value[0]) => Str::trim($value[1])]; + })->toArray(); + + return $cookies; + } + /** * Get the broken images from the page, if any. * @@ -476,7 +502,8 @@ public function brokenImages(): array .filter(img => img.complete && img.naturalWidth === 0) .map(img => img.src); } - JS); + JS + ); /** @var array $brokenImages */ return $brokenImages; @@ -591,27 +618,6 @@ public function isClosed(): bool return $this->closed; } - /** - * Screenshots the page and returns the binary data. - */ - private function screenshotBinary(bool $fullPage = true): ?string - { - $response = Client::instance()->execute( - $this->guid, - 'screenshot', - $this->screenshotOptions($fullPage) - ); - - /** @var array{result: array{binary: string|null}} $message */ - foreach ($response as $message) { - if (isset($message['result']['binary'])) { - return $message['result']['binary']; - } - } - - return null; - } - /** * Send a message to the frame (for frame-related operations) * @@ -648,6 +654,27 @@ private function isPageLevelOperation(string $method): bool return in_array($method, $pageLevelOperations, true); } + /** + * Screenshots the page and returns the binary data. + */ + private function screenshotBinary(bool $fullPage = true): ?string + { + $response = Client::instance()->execute( + $this->guid, + 'screenshot', + $this->screenshotOptions($fullPage) + ); + + /** @var array{result: array{binary: string|null}} $message */ + foreach ($response as $message) { + if (isset($message['result']['binary'])) { + return $message['result']['binary']; + } + } + + return null; + } + /** * @return array */ diff --git a/tests/Browser/Webpage/AssertCookieMissingTest.php b/tests/Browser/Webpage/AssertCookieMissingTest.php new file mode 100644 index 00000000..e12e54ce --- /dev/null +++ b/tests/Browser/Webpage/AssertCookieMissingTest.php @@ -0,0 +1,57 @@ + ' + + '); + + $page = visit('/'); + + $page->assertCookieMissing('nonexistent'); +}); + +it('may assert cookie is missing with value', function (): void { + Route::get('/', fn (): string => ' + + '); + + $page = visit('/'); + + $page->assertCookieMissing('test', 5); +}); + +it('may fail when asserting cookie is missing but it exists', function (): void { + Route::get('/', fn (): string => ' + + '); + + $page = visit('/'); + + $page->assertCookieMissing('test'); +})->throws(ExpectationFailedException::class, 'Expected cookie'); + +it('may fail when asserting cookie is missing with value but it exists with that value', function (): void { + Route::get('/', fn (): string => ' + + '); + + $page = visit('/'); + + $page->assertCookieMissing('test', 1); +})->throws(ExpectationFailedException::class, 'Expected cookie'); diff --git a/tests/Browser/Webpage/AssertHasCookieTest.php b/tests/Browser/Webpage/AssertHasCookieTest.php new file mode 100644 index 00000000..19f2b507 --- /dev/null +++ b/tests/Browser/Webpage/AssertHasCookieTest.php @@ -0,0 +1,57 @@ + ' + + '); + + $page = visit('/'); + + $page->assertHasCookie('test'); +}); + +it('may assert cookie exists with value', function (): void { + Route::get('/', fn (): string => ' + + '); + + $page = visit('/'); + + $page->assertHasCookie('test', 1); +}); + +it('may fail when asserting cookie exists but it does not', function (): void { + Route::get('/', fn (): string => ' + + '); + + $page = visit('/'); + + $page->assertHasCookie('foobar'); +})->throws(ExpectationFailedException::class, 'but it was not found'); + +it('may fail when asserting cookie exists with value but value does not match', function (): void { + Route::get('/', fn (): string => ' + + '); + + $page = visit('/'); + + $page->assertHasCookie('test', 2); +})->throws(ExpectationFailedException::class, 'but it was not found'); diff --git a/tests/Browser/Webpage/AssertNoCookiesTest.php b/tests/Browser/Webpage/AssertNoCookiesTest.php new file mode 100644 index 00000000..4703f62f --- /dev/null +++ b/tests/Browser/Webpage/AssertNoCookiesTest.php @@ -0,0 +1,26 @@ + '

Page with no cookies

'); + + $page = visit('/'); + + $page->assertNoCookies(); +}); + +it('may fail when asserting no cookies but there are cookies', function (): void { + Route::get('/', fn (): string => ' + + '); + + $page = visit('/'); + + $page->assertNoCookies(); +})->throws(ExpectationFailedException::class, 'but found 2'); From d5cafa001986c1f76fac113d21627f0c6aa24325 Mon Sep 17 00:00:00 2001 From: Tim Geisendoerfer Date: Sun, 7 Sep 2025 22:46:50 +0200 Subject: [PATCH 2/3] revert formatting changes and remove dependencies to laravel helpers --- src/Playwright/Page.php | 130 ++++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/src/Playwright/Page.php b/src/Playwright/Page.php index bbc84706..857c1830 100644 --- a/src/Playwright/Page.php +++ b/src/Playwright/Page.php @@ -5,7 +5,6 @@ namespace Pest\Browser\Playwright; use Generator; -use Illuminate\Support\Str; use Pest\Browser\Execution; use Pest\Browser\Support\ImageDiffView; use Pest\Browser\Support\JavaScriptSerializer; @@ -66,21 +65,6 @@ public function url(): string return $url; } - /** - * Evaluates a JavaScript expression in the page context. - */ - public function evaluate(string $pageFunction, mixed $arg = null): mixed - { - $params = [ - 'expression' => $pageFunction, - 'arg' => JavaScriptSerializer::serializeArgument($arg), - ]; - - $response = $this->sendMessage('evaluateExpression', $params); - - return $this->processResultResponse($response); - } - /** * Performs the given callback in unstrict mode. * @@ -137,14 +121,6 @@ public function querySelector(string $selector): ?Element return $this->locator($selector)->elementHandle(); } - /** - * Create a locator for the specified selector. - */ - public function locator(string $selector): Locator - { - return new Locator($this->frameGuid, $selector, $this->strictLocators); - } - /** * Finds all elements matching the specified selector. * @@ -169,6 +145,14 @@ public function querySelectorAll(string $selector): array return $elements; } + /** + * Create a locator for the specified selector. + */ + public function locator(string $selector): Locator + { + return new Locator($this->frameGuid, $selector, $this->strictLocators); + } + /** * Create a locator that matches elements by role. * @@ -358,6 +342,21 @@ public function setContent(string $html): self return $this; } + /** + * Evaluates a JavaScript expression in the page context. + */ + public function evaluate(string $pageFunction, mixed $arg = null): mixed + { + $params = [ + 'expression' => $pageFunction, + 'arg' => JavaScriptSerializer::serializeArgument($arg), + ]; + + $response = $this->sendMessage('evaluateExpression', $params); + + return $this->processResultResponse($response); + } + /** * Evaluates a JavaScript expression and returns a JSHandle. */ @@ -425,17 +424,6 @@ public function reload(): self return $this; } - /** - * Make screenshot of a specific element. - */ - public function screenshotElement(string $selector, ?string $filename = null): string - { - $locator = $this->locator($selector); - $binary = $locator->screenshot(); - - return Screenshot::save($binary, $filename); - } - /** * Make screenshot of the page. */ @@ -450,6 +438,17 @@ public function screenshot(bool $fullPage = true, ?string $filename = null): ?st return Screenshot::save($binary, $filename); } + /** + * Make screenshot of a specific element. + */ + public function screenshotElement(string $selector, ?string $filename = null): string + { + $locator = $this->locator($selector); + $binary = $locator->screenshot(); + + return Screenshot::save($binary, $filename); + } + /** * Get the console logs from the page, if any. * @@ -473,18 +472,20 @@ public function cookies(): array { $cookieString = $this->evaluate('document.cookie || []'); - if (blank($cookieString)) { + if (empty($cookieString)) { return []; } /** @var array $cookies */ - $cookies = Str::of(is_string($cookieString) ? $cookieString : '') - ->explode(';') - ->mapWithKeys(function (string $cookie): array { - $value = explode('=', $cookie); + $cookies = []; + $cookiePairs = explode(';', is_string($cookieString) ? $cookieString : ''); - return [Str::trim($value[0]) => Str::trim($value[1])]; - })->toArray(); + foreach ($cookiePairs as $cookie) { + $value = explode('=', $cookie, 2); + if (count($value) >= 2) { + $cookies[mb_trim($value[0])] = mb_trim($value[1]); + } + } return $cookies; } @@ -502,8 +503,7 @@ public function brokenImages(): array .filter(img => img.complete && img.naturalWidth === 0) .map(img => img.src); } - JS - ); + JS); /** @var array $brokenImages */ return $brokenImages; @@ -618,6 +618,27 @@ public function isClosed(): bool return $this->closed; } + /** + * Screenshots the page and returns the binary data. + */ + private function screenshotBinary(bool $fullPage = true): ?string + { + $response = Client::instance()->execute( + $this->guid, + 'screenshot', + $this->screenshotOptions($fullPage) + ); + + /** @var array{result: array{binary: string|null}} $message */ + foreach ($response as $message) { + if (isset($message['result']['binary'])) { + return $message['result']['binary']; + } + } + + return null; + } + /** * Send a message to the frame (for frame-related operations) * @@ -654,27 +675,6 @@ private function isPageLevelOperation(string $method): bool return in_array($method, $pageLevelOperations, true); } - /** - * Screenshots the page and returns the binary data. - */ - private function screenshotBinary(bool $fullPage = true): ?string - { - $response = Client::instance()->execute( - $this->guid, - 'screenshot', - $this->screenshotOptions($fullPage) - ); - - /** @var array{result: array{binary: string|null}} $message */ - foreach ($response as $message) { - if (isset($message['result']['binary'])) { - return $message['result']['binary']; - } - } - - return null; - } - /** * @return array */ From 673f260166230b06e1cb8c743f1a3c7486ff79ba Mon Sep 17 00:00:00 2001 From: Tim Geisendoerfer Date: Sun, 7 Sep 2025 22:50:12 +0200 Subject: [PATCH 3/3] cleanup --- src/Playwright/Page.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Playwright/Page.php b/src/Playwright/Page.php index 857c1830..34ffca83 100644 --- a/src/Playwright/Page.php +++ b/src/Playwright/Page.php @@ -472,10 +472,6 @@ public function cookies(): array { $cookieString = $this->evaluate('document.cookie || []'); - if (empty($cookieString)) { - return []; - } - /** @var array $cookies */ $cookies = []; $cookiePairs = explode(';', is_string($cookieString) ? $cookieString : '');