diff --git a/CHANGELOG.md b/CHANGELOG.md index a405956..658de2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Before 1.0, breaking changes are released in minor versions. ### Changed +- `getCookieJar()` now returns a jar backed by the browser context, so it reads the cookies the browser actually holds and writing to it reaches the browser. The internal `CookieJarSync`, which copied cookies into a separate jar at a few fixed points, is gone. +- **BC break:** cookies in a `CookieJar` passed to `PlaywrightClient` are now written into the browser context. They were previously overwritten by the context's own cookies and never sent. - **BC break:** `PlaywrightTestCase` now extends `WebTestCase` instead of `KernelTestCase`. Subclasses that define members inherited from `WebTestCase`, such as `createClient()`, must use compatible signatures. - **BC break:** `PlaywrightTestCase::logout()` now accepts an optional firewall context and returns `static` instead of `void`. Overrides must change their signature to `logout(string $firewallContext = 'main'): static`. - **BC break:** `assertSelectorExists()`, `assertSelectorNotExists()`, `assertSelectorTextContains()`, and `assertResponseIsSuccessful()` now use the public static Symfony `WebTestCase` signatures. Overrides of the previous protected instance methods must be updated. Calls from tests remain compatible. diff --git a/docs/architecture.md b/docs/architecture.md index dd4492b..2bf4374 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -17,7 +17,8 @@ requests to a web server, Playwright intercepts all outgoing requests and routes 3. **Bridge Utilities**: - `RequestConverter`: Translates Playwright requests (headers, body, cookies) to Symfony `HttpFoundation` requests. - `ResponseConverter`: Translates Symfony responses back to Playwright `fulfill` options. - - `CookieJarSync`: Keeps the browser's cookies in sync with Symfony's `CookieJar`. + - `Cookie\CookieJar`: A BrowserKit `CookieJar` backed by the browser context, so the client's jar reads and + writes the cookies the browser actually holds. 4. **Browser Registry (`BrowserRegistry`)**: Browser lifecycle manager that handles process management and `BrowserContext` isolation. 5. **Asset Layer (`AssetServer`)**: A high-performance bypassing layer that serves static files and AssetMapper assets diff --git a/docs/bridge/browserkit.md b/docs/bridge/browserkit.md index df909b3..8e3607f 100644 --- a/docs/bridge/browserkit.md +++ b/docs/bridge/browserkit.md @@ -38,7 +38,7 @@ $crawler = $client->submit($form); - Real browser interactions: click/submit use Playwright, not synthetic HTTP. - `request()` with method !== GET constructs and submits a synthetic in-page form to preserve browser semantics. - Response mapping: uses last Playwright Response (status, headers) + page content for BrowserKit Response. -- Cookies: kept in sync between BrowserKit CookieJar and Playwright BrowserContext. +- Cookies: the BrowserKit CookieJar is backed by the Playwright BrowserContext, so both read and write one store. ## Options diff --git a/docs/cookies.md b/docs/cookies.md index 9a1aa35..cc0f9ee 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -368,6 +368,24 @@ $context->deleteCookie(string $name); $context->clearCookies(); ``` +### BrowserKit Cookie Jar + +`getCookieJar()` returns a jar backed by the browser context, so it reflects what the browser holds, including cookies +set by javascript, and writing to it reaches the browser: + +```php +use Symfony\Component\BrowserKit\Cookie; + +$jar = $client->getCookieJar(); + +$jar->set(new Cookie('notice', '1', domain: 'localhost')); +$jar->get('notice'); +$jar->expire('notice'); // removes every domain and path variant of the name +$jar->clear(); +``` + +A cookie set without a domain is scoped to the host being browsed. + ## Best Practices 1. ✅ **Always use domain parameter** instead of url diff --git a/docs/dom-crawler.md b/docs/dom-crawler.md index c8f03f9..6b84d50 100644 --- a/docs/dom-crawler.md +++ b/docs/dom-crawler.md @@ -79,10 +79,10 @@ Unlike standard `WebTestCase`, calling `click()` or `submit()` through the bridg - Executes CSS transitions and animations. - Respects `target="_blank"` and other browser-native behaviors. -### Automatic State Synchronization +### Cookies Come From the Browser -The bridge includes a synchronization layer (`CookieJarSync`) that ensures cookies set by the browser (via JS) and -cookies set by Symfony (via Headers) are always consistent within your test session. +`getCookieJar()` returns a BrowserKit jar backed by the browser context. Cookies set by the browser (via JS) and +cookies set by Symfony (via headers) live in one store, so there is nothing to keep consistent. ### Asynchronous Resilience diff --git a/src/BrowserKit/PlaywrightClient.php b/src/BrowserKit/PlaywrightClient.php index e2f9ba6..80e6285 100644 --- a/src/BrowserKit/PlaywrightClient.php +++ b/src/BrowserKit/PlaywrightClient.php @@ -17,7 +17,7 @@ use Playwright\Browser\BrowserContextInterface; use Playwright\Exception\TimeoutException; use Playwright\Page\PageInterface; -use Playwright\Symfony\Util\CookieJarSync; +use Playwright\Symfony\Cookie\CookieJar as PlaywrightCookieJar; use Playwright\Symfony\Util\FormInteractor; use Playwright\Symfony\Util\XPathHelper; use Symfony\Component\BrowserKit\AbstractBrowser; @@ -111,12 +111,18 @@ public function __construct( ?History $history = null, ?CookieJar $cookieJar = null, ) { - parent::__construct($server, $history, $cookieJar); + $jar = new PlaywrightCookieJar($context, static fn (): string => parse_url($page->url(), \PHP_URL_HOST) ?: 'localhost'); + + // a jar handed in was previously seeded from the context and then ignored by the browser; + // pushing its cookies in makes them count for real + foreach ($cookieJar?->all() ?? [] as $cookie) { + $jar->set($cookie); + } + + parent::__construct($server, $history, $jar); $this->context = $context; $this->page = $page; - - CookieJarSync::fromContext($this->cookieJar, $this->context); } /** @@ -221,8 +227,6 @@ private function navigate(string $url): BrowserKitResponse $status = $playwrightResponse?->status() ?? 200; $headers = $playwrightResponse?->headers() ?? []; - CookieJarSync::toJarFromUrl($this->cookieJar, $this->context, $this->page->url()); - return $this->createBrowserKitResponse($content, $status, $headers); } @@ -305,8 +309,6 @@ private function submitSyntheticForm(string $action, string $method, array $para $status = 200; $headers = []; - CookieJarSync::toJarFromUrl($this->cookieJar, $this->context, $this->page->url()); - return $this->createBrowserKitResponse($content, $status, $headers); } @@ -316,7 +318,6 @@ private function refreshSnapshotAndResponse(): Crawler $status = 200; $headers = []; - CookieJarSync::toJarFromUrl($this->cookieJar, $this->context, $this->page->url()); $this->lastResponse = $this->createBrowserKitResponse($content, $status, $headers); return new Crawler($content, $this->page->url()); diff --git a/src/Client/PlaywrightKernelClient.php b/src/Client/PlaywrightKernelClient.php index 0e2c295..7e944f1 100644 --- a/src/Client/PlaywrightKernelClient.php +++ b/src/Client/PlaywrightKernelClient.php @@ -18,7 +18,7 @@ use Playwright\Network\RequestInterface; use Playwright\Page\PageInterface; use Playwright\Symfony\Client\Interception\AssetServer; -use Playwright\Symfony\Util\CookieJarSync; +use Playwright\Symfony\Cookie\CookieJar; use Playwright\Symfony\Util\FormInteractor; use Playwright\Symfony\Util\XPathHelper; use Psr\Log\LoggerInterface; @@ -187,7 +187,9 @@ public function __construct( private ?LoggerInterface $logger = null, private readonly bool $debugLogging = false, ) { - parent::__construct($server); + $context = $browser->getContext(); + + parent::__construct($server, cookieJar: $context ? new CookieJar($context, static fn (): string => parse_url($baseUrl, \PHP_URL_HOST) ?: 'localhost') : null); $this->session = $browser; @@ -199,10 +201,7 @@ public function __construct( $this->assetServer = $assetServer; $this->logger = $logger ?? new NullLogger(); - if ($context = $this->session->getContext()) { - $context->addInitScript(self::FETCH_REDIRECT_SCRIPT); - CookieJarSync::fromContext($this->getCookieJar(), $context); - } + $context?->addInitScript(self::FETCH_REDIRECT_SCRIPT); } public function catchExceptions(bool $catchExceptions): void @@ -223,10 +222,6 @@ public function visit(string $path): PageInterface $page->goto($url); - if ($context = $this->session->getContext()) { - CookieJarSync::toJarFromUrl($this->getCookieJar(), $context, $page->url()); - } - return $page; } @@ -351,7 +346,6 @@ public function setCookie(string $name, string $value, array $options = []): voi } $context->addCookies([$cookie]); - CookieJarSync::toJarFromUrl($this->getCookieJar(), $context, $this->getBaseUrl()); } public function getCookie(string $name, ?string $url = null): ?string diff --git a/src/Cookie/CookieJar.php b/src/Cookie/CookieJar.php new file mode 100644 index 0000000..5357938 --- /dev/null +++ b/src/Cookie/CookieJar.php @@ -0,0 +1,184 @@ + + * + * @internal + */ +final class CookieJar extends BrowserKitCookieJar +{ + /** + * @param \Closure(): string $defaultDomain the domain to scope cookies set without one, usually + * the host currently being browsed + */ + public function __construct( + private readonly BrowserContextInterface $context, + private readonly \Closure $defaultDomain, + ) { + } + + public function set(Cookie $cookie): void + { + // playwright needs a domain/path pair: passing a url instead makes it derive the path from + // that url, which silently widens a cookie scoped to something narrower + $data = [ + 'name' => $cookie->getName(), + 'value' => $cookie->getValue(), + 'domain' => $cookie->getDomain() ?: ($this->defaultDomain)(), + 'path' => $cookie->getPath(), + 'secure' => $cookie->isSecure(), + 'httpOnly' => $cookie->isHttpOnly(), + ]; + + if (null !== $expires = $cookie->getExpiresTime()) { + $data['expires'] = (int) $expires; + } + + // playwright only accepts these three, spelled exactly like this + $sameSite = match (strtolower((string) $cookie->getSameSite())) { + 'strict' => 'Strict', + 'lax' => 'Lax', + 'none' => 'None', + default => null, + }; + + if (null !== $sameSite) { + $data['sameSite'] = $sameSite; + } + + $this->context->addCookies([$data]); + } + + public function get(string $name, string $path = '/', ?string $domain = null): ?Cookie + { + foreach ($this->all() as $cookie) { + if ($name !== $cookie->getName() || !str_starts_with($path, $cookie->getPath())) { + continue; + } + + if (null === $domain || '' === $cookie->getDomain() || str_ends_with('.'.$domain, '.'.ltrim($cookie->getDomain(), '.'))) { + return $cookie; + } + } + + return null; + } + + /** + * Unlike BrowserKit's jar, which is keyed by domain and path, this removes the cookie from + * every domain and path it is stored under. + */ + public function expire(string $name, ?string $path = '/', ?string $domain = null): void + { + $this->context->deleteCookie($name); + } + + public function clear(): void + { + $this->context->clearCookies(); + } + + /** + * @return Cookie[] + */ + public function all(): array + { + return array_map(self::toBrowserKitCookie(...), $this->context->cookies()); + } + + /** + * @return array + */ + public function allValues(string $uri, bool $returnsRawValue = false): array + { + $values = []; + + // the browser does the matching the parent does by hand: domain, path, secure and expiry + foreach ($this->context->cookies([$uri]) as $cookie) { + $cookie = self::toBrowserKitCookie($cookie); + + $values[$cookie->getName()] = $returnsRawValue ? $cookie->getRawValue() : $cookie->getValue(); + } + + return $values; + } + + /** + * @return array + */ + public function allRawValues(string $uri): array + { + return $this->allValues($uri, true); + } + + /** + * @param array $cookie + */ + private static function toBrowserKitCookie(array $cookie): Cookie + { + $sameSite = self::toString($cookie['sameSite'] ?? null); + + return new Cookie( + name: self::toString($cookie['name'] ?? null), + value: self::toString($cookie['value'] ?? null), + expires: self::normalizeExpires($cookie['expires'] ?? null), + path: self::toString($cookie['path'] ?? null) ?: '/', + domain: self::toString($cookie['domain'] ?? null), + secure: (bool) ($cookie['secure'] ?? false), + httponly: (bool) ($cookie['httpOnly'] ?? false), + // playwright capitalizes it, BrowserKit round-trips whatever it was given + samesite: '' === $sameSite ? null : strtolower($sameSite), + ); + } + + /** + * Playwright reports "expires" as a number: -1 for session cookies, a Unix timestamp + * (possibly float) otherwise. BrowserKit only accepts an int since 8.1; before that the + * parameter is ?string, parsed with createFromFormat('U'), so a numeric string is what + * satisfies every supported version. Past timestamps count as expired, so negatives must + * map to null. + */ + private static function normalizeExpires(mixed $expires): ?string + { + if (!is_numeric($expires)) { + return null; + } + + $timestamp = (int) $expires; + + return $timestamp < 0 ? null : (string) $timestamp; + } + + private static function toString(mixed $value): string + { + return is_scalar($value) ? (string) $value : ''; + } +} diff --git a/src/Util/CookieJarSync.php b/src/Util/CookieJarSync.php deleted file mode 100644 index 80600f5..0000000 --- a/src/Util/CookieJarSync.php +++ /dev/null @@ -1,88 +0,0 @@ - - * - * @internal - */ -final class CookieJarSync -{ - /** - * Seeds the jar with every cookie currently stored in the browser context. - */ - public static function fromContext(CookieJar $jar, BrowserContextInterface $context): void - { - foreach ($context->cookies() as $cookie) { - $jar->set(self::toBrowserKitCookie($cookie)); - } - } - - /** - * Seeds the jar with the context cookies that match the given URL. - */ - public static function toJarFromUrl(CookieJar $jar, BrowserContextInterface $context, string $url): void - { - foreach ($context->cookies([$url]) as $cookie) { - $jar->set(self::toBrowserKitCookie($cookie)); - } - } - - /** - * @param array $cookie - */ - private static function toBrowserKitCookie(array $cookie): Cookie - { - return new Cookie( - name: self::toString($cookie['name'] ?? ''), - value: self::toString($cookie['value'] ?? ''), - expires: self::normalizeExpires($cookie['expires'] ?? null), - path: self::toString($cookie['path'] ?? '/'), - domain: self::toString($cookie['domain'] ?? ''), - secure: (bool) ($cookie['secure'] ?? false), - httponly: (bool) ($cookie['httpOnly'] ?? false), - ); - } - - /** - * Playwright reports "expires" as a number: -1 for session cookies, a Unix - * timestamp (possibly float) otherwise. BrowserKit only accepts an int since - * 8.1; before that the parameter is ?string, parsed with createFromFormat('U'), - * so a numeric string is what satisfies every supported version. Past - * timestamps count as expired, so negatives must map to null. - */ - private static function normalizeExpires(mixed $expires): ?string - { - if (!is_numeric($expires)) { - return null; - } - - $timestamp = (int) $expires; - - return $timestamp < 0 ? null : (string) $timestamp; - } - - private static function toString(mixed $value): string - { - return is_scalar($value) ? (string) $value : ''; - } -} diff --git a/tests/BrowserKit/PlaywrightClientTest.php b/tests/BrowserKit/PlaywrightClientTest.php index bd9b268..3997107 100644 --- a/tests/BrowserKit/PlaywrightClientTest.php +++ b/tests/BrowserKit/PlaywrightClientTest.php @@ -20,15 +20,15 @@ use Playwright\Exception\TimeoutException; use Playwright\Page\PageInterface; use Playwright\Symfony\BrowserKit\PlaywrightClient; +use Playwright\Symfony\Cookie\CookieJar as PlaywrightCookieJar; use Playwright\Symfony\Tests\Client\Fixtures\FakeBrowserContext; use Playwright\Symfony\Tests\Client\Fixtures\FakePage; -use Playwright\Symfony\Util\CookieJarSync; use Playwright\Symfony\Util\XPathHelper; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\DomCrawler\Link; #[CoversClass(PlaywrightClient::class)] -#[UsesClass(CookieJarSync::class)] +#[UsesClass(PlaywrightCookieJar::class)] #[UsesClass(XPathHelper::class)] final class PlaywrightClientTest extends TestCase { diff --git a/tests/Client/Fixtures/FakeBrowserContext.php b/tests/Client/Fixtures/FakeBrowserContext.php index 13057c4..167718e 100644 --- a/tests/Client/Fixtures/FakeBrowserContext.php +++ b/tests/Client/Fixtures/FakeBrowserContext.php @@ -32,6 +32,8 @@ class FakeBrowserContext implements BrowserContextInterface public array $cookies = []; /** @var list */ public array $initScripts = []; + /** @var array|null */ + public ?array $lastCookiesUrls = null; public array $extraHTTPHeaders = []; public ?array $httpCredentials = null; public int $waitForPopupCalls = 0; @@ -83,6 +85,8 @@ public function close(): void public function cookies(?array $urls = null): array { + $this->lastCookiesUrls = $urls; + return $this->cookies; } diff --git a/tests/Client/PlaywrightKernelClientTest.php b/tests/Client/PlaywrightKernelClientTest.php index 3dc17da..524a08b 100644 --- a/tests/Client/PlaywrightKernelClientTest.php +++ b/tests/Client/PlaywrightKernelClientTest.php @@ -24,12 +24,12 @@ use Playwright\Symfony\Client\PlaywrightKernelClient; use Playwright\Symfony\Client\RequestConverter; use Playwright\Symfony\Client\ResponseConverter; +use Playwright\Symfony\Cookie\CookieJar as PlaywrightCookieJar; use Playwright\Symfony\Tests\Client\Fixtures\FakeBrowserContext; use Playwright\Symfony\Tests\Client\Fixtures\FakeLogger; use Playwright\Symfony\Tests\Client\Fixtures\FakePage; use Playwright\Symfony\Tests\Client\Fixtures\TestBrowserRegistry; use Playwright\Symfony\Tests\Fixtures\MockRequest; -use Playwright\Symfony\Util\CookieJarSync; use Symfony\Bundle\FrameworkBundle\Test\TestBrowserToken; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DomCrawler\Crawler; @@ -53,7 +53,7 @@ #[CoversClass(ResponseConverter::class)] #[CoversClass(AssetServer::class)] #[CoversClass(AssetFile::class)] -#[UsesClass(CookieJarSync::class)] +#[UsesClass(PlaywrightCookieJar::class)] class PlaywrightKernelClientTest extends TestCase { private TestBrowserRegistry $browser; diff --git a/tests/Cookie/CookieJarTest.php b/tests/Cookie/CookieJarTest.php new file mode 100644 index 0000000..6a8b5e5 --- /dev/null +++ b/tests/Cookie/CookieJarTest.php @@ -0,0 +1,159 @@ +createJar($context)->set(new Cookie( + name: 'session', + value: 'abc', + expires: (string) $expires, + path: '/admin', + domain: 'example.com', + secure: true, + httponly: true, + samesite: 'lax', + )); + + $this->assertSame([[ + 'name' => 'session', + 'value' => 'abc', + 'domain' => 'example.com', + 'path' => '/admin', + 'secure' => true, + 'httpOnly' => true, + 'expires' => $expires, + // playwright capitalizes it + 'sameSite' => 'Lax', + ]], $context->cookies); + } + + public function testSetFallsBackToTheDefaultDomain(): void + { + $context = new FakeBrowserContext(); + + $this->createJar($context)->set(new Cookie('no_domain', 'v')); + + $this->assertSame('localhost', $context->cookies[0]['domain']); + } + + public function testAllReadsFromTheContext(): void + { + $expires = time() + 3600; + $context = new FakeBrowserContext(); + $context->addCookies([ + // playwright reports -1 for a session cookie and may report a float timestamp + ['name' => 'session', 'value' => 's', 'domain' => 'localhost', 'path' => '/', 'expires' => -1], + ['name' => 'kept', 'value' => 'k', 'domain' => 'localhost', 'path' => '/', 'expires' => $expires + 0.5, 'sameSite' => 'Strict'], + ]); + + $cookies = $this->createJar($context)->all(); + + $this->assertCount(2, $cookies); + $this->assertNull($cookies[0]->getExpiresTime()); + $this->assertSame((string) $expires, $cookies[1]->getExpiresTime()); + $this->assertSame('strict', $cookies[1]->getSameSite()); + } + + public function testGetMatchesNamePathAndDomain(): void + { + $context = new FakeBrowserContext(); + $context->addCookies([ + ['name' => 'a', 'value' => 'root', 'domain' => 'example.com', 'path' => '/'], + ['name' => 'b', 'value' => 'nested', 'domain' => 'example.com', 'path' => '/admin'], + ]); + + $jar = $this->createJar($context); + + $this->assertSame('root', $jar->get('a')?->getValue()); + $this->assertSame('nested', $jar->get('b', '/admin')?->getValue()); + $this->assertNull($jar->get('b'), 'a cookie scoped to /admin does not match /'); + $this->assertNull($jar->get('a', '/', 'other.com')); + $this->assertNull($jar->get('missing')); + } + + public function testExpireRemovesEveryDomainAndPathVariant(): void + { + $context = new FakeBrowserContext(); + $context->addCookies([ + ['name' => 'dupe', 'value' => '1', 'domain' => 'example.com', 'path' => '/'], + ['name' => 'kept', 'value' => '2', 'domain' => 'example.com', 'path' => '/'], + ]); + + $this->createJar($context)->expire('dupe'); + + $this->assertSame(['kept'], array_column($context->cookies, 'name')); + } + + public function testClearEmptiesTheContext(): void + { + $context = new FakeBrowserContext(); + $context->addCookies([['name' => 'a', 'value' => '1', 'domain' => 'localhost', 'path' => '/']]); + + $this->createJar($context)->clear(); + + $this->assertSame([], $context->cookies); + } + + public function testAllValuesLetsTheBrowserScopeToTheUrl(): void + { + $context = new FakeBrowserContext(); + $context->addCookies([['name' => 'a', 'value' => 'a b', 'domain' => 'example.com', 'path' => '/']]); + + $jar = $this->createJar($context); + + $this->assertSame(['a' => 'a b'], $jar->allValues('http://example.com/admin')); + $this->assertSame(['http://example.com/admin'], $context->lastCookiesUrls); + $this->assertSame(['a' => 'a%20b'], $jar->allRawValues('http://example.com/admin')); + } + + public function testUpdateFromSetCookieWritesThroughToTheContext(): void + { + $context = new FakeBrowserContext(); + + $this->createJar($context)->updateFromSetCookie(['a=1; path=/; domain=example.com'], 'http://example.com/'); + + $this->assertSame('example.com', $context->cookies[0]['domain']); + $this->assertSame('1', $context->cookies[0]['value']); + } + + public function testUpdateFromResponseWritesThroughToTheContext(): void + { + $context = new FakeBrowserContext(); + $response = new Response('', 200, ['Set-Cookie' => ['a=1']]); + + $this->createJar($context)->updateFromResponse($response, 'http://example.com/'); + + $this->assertSame('a', $context->cookies[0]['name']); + } + + private function createJar(FakeBrowserContext $context): CookieJar + { + return new CookieJar($context, static fn (): string => 'localhost'); + } +} diff --git a/tests/Integration/SecurityIntegrationTest.php b/tests/Integration/SecurityIntegrationTest.php index b58ffbc..f7f652a 100644 --- a/tests/Integration/SecurityIntegrationTest.php +++ b/tests/Integration/SecurityIntegrationTest.php @@ -25,12 +25,12 @@ use Playwright\Symfony\Client\PlaywrightKernelClient; use Playwright\Symfony\Client\RequestConverter; use Playwright\Symfony\Client\ResponseConverter; +use Playwright\Symfony\Cookie\CookieJar as PlaywrightCookieJar; use Playwright\Symfony\Test\PlaywrightTestCase; use Playwright\Symfony\Tests\Client\Fixtures\FakeBrowserContext; use Playwright\Symfony\Tests\Client\Fixtures\FakePage; use Playwright\Symfony\Tests\Client\Fixtures\TestBrowserRegistry; use Playwright\Symfony\Tests\Fixtures\App\TestKernel; -use Playwright\Symfony\Util\CookieJarSync; use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\KernelInterface; @@ -43,7 +43,7 @@ #[UsesClass(FilesystemProxy::class)] #[UsesClass(BrowserRegistry::class)] #[UsesClass(AssetServer::class)] -#[UsesClass(CookieJarSync::class)] +#[UsesClass(PlaywrightCookieJar::class)] final class SecurityIntegrationTest extends TestCase { #[RunInSeparateProcess] diff --git a/tests/Util/CookieJarSyncTest.php b/tests/Util/CookieJarSyncTest.php deleted file mode 100644 index f247b72..0000000 --- a/tests/Util/CookieJarSyncTest.php +++ /dev/null @@ -1,120 +0,0 @@ -addCookies([ - ['name' => 'c1', 'value' => 'v1', 'domain' => 'localhost', 'path' => '/'], - ['name' => 'c2', 'value' => 'v2', 'domain' => 'example.com', 'path' => '/app'], - ]); - - $jar = new CookieJar(); - CookieJarSync::fromContext($jar, $context); - - $this->assertNotNull($jar->get('c1', '/', 'localhost')); - $this->assertSame('v1', $jar->get('c1', '/', 'localhost')->getValue()); - - $this->assertNotNull($jar->get('c2', '/app', 'example.com')); - $this->assertSame('v2', $jar->get('c2', '/app', 'example.com')->getValue()); - } - - public function testFromContextAcceptsNumericExpires(): void - { - $future = time() + 3600; - - $context = new FakeBrowserContext(); - $context->addCookies([ - // Playwright reports "expires" as a number: -1 for session cookies, - // a Unix timestamp (possibly float) otherwise. - ['name' => 'session', 'value' => 's', 'domain' => 'localhost', 'path' => '/', 'expires' => -1], - ['name' => 'float', 'value' => 'f', 'domain' => 'localhost', 'path' => '/', 'expires' => $future + 0.5], - ['name' => 'int', 'value' => 'i', 'domain' => 'localhost', 'path' => '/', 'expires' => $future], - ]); - - $jar = new CookieJar(); - CookieJarSync::fromContext($jar, $context); - - $session = $jar->get('session', '/', 'localhost'); - $this->assertNotNull($session); - $this->assertNull($session->getExpiresTime()); - - $float = $jar->get('float', '/', 'localhost'); - $this->assertNotNull($float); - $this->assertSame((string) $future, $float->getExpiresTime()); - - $int = $jar->get('int', '/', 'localhost'); - $this->assertNotNull($int); - $this->assertSame((string) $future, $int->getExpiresTime()); - } - - public function testToJarFromUrlAcceptsNumericExpires(): void - { - $context = new FakeBrowserContext(); - $context->addCookies([ - ['name' => 'site', 'value' => 'main', 'domain' => 'localhost', 'path' => '/', 'expires' => -1], - ]); - - $jar = new CookieJar(); - CookieJarSync::toJarFromUrl($jar, $context, 'http://localhost/foo'); - - $site = $jar->get('site'); - $this->assertNotNull($site); - $this->assertNull($site->getExpiresTime()); - } - - public function testToJarFromUrlFiltersCookies(): void - { - $context = new FakeBrowserContext(); - $context->addCookies([ - ['name' => 'site', 'value' => 'main', 'domain' => 'localhost', 'path' => '/'], - ]); - - $jar = new CookieJar(); - CookieJarSync::toJarFromUrl($jar, $context, 'http://localhost/foo'); - - $this->assertNotNull($jar->get('site')); - $this->assertSame('main', $jar->get('site')->getValue()); - } - - public function testPlaywrightSessionCookieRemainsValidInBrowserKitJar(): void - { - $context = new FakeBrowserContext(); - $context->addCookies([ - [ - 'name' => 'session', - 'value' => 'session-id', - 'domain' => 'localhost', - 'path' => '/', - 'expires' => -1, - ], - ]); - - $jar = new CookieJar(); - CookieJarSync::toJarFromUrl($jar, $context, 'http://localhost/'); - - $this->assertSame('session-id', $jar->get('session')?->getValue()); - } -}