From 9abe3c1edd07490ae36b5e7c974f3916ae8f8b30 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 4 Jul 2026 02:11:20 +0000 Subject: [PATCH] Normalize cookie sameSite for Playwright addCookies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cookie-Editor / Chrome cookie exports use sameSite spellings that Playwright rejects — "no_restriction", "unspecified", lowercase "lax"/"strict", or null — causing every browser-automated post to fail at ctx.addCookies with: cookies[N].sameSite: expected one of (Strict|Lax|None) parseCookies now maps those to Playwright's exact enum (no_restriction → None, strict → Strict, everything else → Lax) and forces Secure on SameSite=None cookies, which Chromium rejects otherwise. Co-Authored-By: Claude Opus 4.8 --- lib/sp/platforms/browser.ts | 41 +++++++++++++++++++++-------- tests/sp/parse-cookies.test.ts | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 tests/sp/parse-cookies.test.ts diff --git a/lib/sp/platforms/browser.ts b/lib/sp/platforms/browser.ts index 3920f9f6..eb3914ff 100644 --- a/lib/sp/platforms/browser.ts +++ b/lib/sp/platforms/browser.ts @@ -71,20 +71,41 @@ async function launchContext(cookies: BrowserCookie[]): Promise<{ } } +// Playwright's addCookies only accepts sameSite "Strict" | "Lax" | "None". +// Cookie-Editor / Chrome exports use other spellings ("no_restriction", +// "unspecified", lowercase "lax"/"strict") or null, which fail the enum check +// with: cookies[N].sameSite: expected one of (Strict|Lax|None). Map them. +function normalizeSameSite(value: unknown): BrowserCookie["sameSite"] { + switch (String(value ?? "").toLowerCase()) { + case "strict": + return "Strict"; + case "none": + case "no_restriction": + return "None"; + default: + // "lax", "unspecified", "", null, or anything unexpected. + return "Lax"; + } +} + export function parseCookies(raw: string): BrowserCookie[] { const parsed = JSON.parse(raw); const arr: unknown[] = Array.isArray(parsed) ? parsed : parsed.cookies ?? parsed; if (!Array.isArray(arr)) throw new Error("Cookie JSON must be an array."); - return arr.map((c: any) => ({ - name: c.name, - value: c.value, - domain: c.domain ?? c.host ?? "", - path: c.path ?? "/", - expires: c.expirationDate ?? c.expires ?? -1, - httpOnly: c.httpOnly ?? false, - secure: c.secure ?? false, - sameSite: (c.sameSite as BrowserCookie["sameSite"]) ?? "Lax", - })); + return arr.map((c: any) => { + const sameSite = normalizeSameSite(c.sameSite); + return { + name: c.name, + value: c.value, + domain: c.domain ?? c.host ?? "", + path: c.path ?? "/", + expires: c.expirationDate ?? c.expires ?? -1, + httpOnly: c.httpOnly ?? false, + // Chromium rejects SameSite=None cookies that aren't Secure. + secure: (c.secure ?? false) || sameSite === "None", + sameSite, + }; + }); } // ---------- Reddit ---------- diff --git a/tests/sp/parse-cookies.test.ts b/tests/sp/parse-cookies.test.ts new file mode 100644 index 00000000..b39becea --- /dev/null +++ b/tests/sp/parse-cookies.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from "vitest"; +import { parseCookies } from "@/lib/sp/platforms/browser"; + +const ALLOWED = new Set(["Strict", "Lax", "None"]); + +describe("parseCookies sameSite normalization", () => { + it("maps Chrome/Cookie-Editor sameSite spellings to Playwright's enum", () => { + const raw = JSON.stringify([ + { name: "a", value: "1", domain: "x.com", sameSite: "no_restriction" }, + { name: "b", value: "2", domain: "x.com", sameSite: "lax" }, + { name: "c", value: "3", domain: "x.com", sameSite: "strict" }, + { name: "d", value: "4", domain: "x.com", sameSite: "unspecified" }, + { name: "e", value: "5", domain: "x.com", sameSite: null }, + { name: "f", value: "6", domain: "x.com" }, // missing entirely + ]); + const cookies = parseCookies(raw); + expect(cookies.map((c) => c.sameSite)).toEqual([ + "None", + "Lax", + "Strict", + "Lax", + "Lax", + "Lax", + ]); + // Every value is one Playwright accepts. + for (const c of cookies) expect(ALLOWED.has(c.sameSite!)).toBe(true); + }); + + it("forces Secure on SameSite=None cookies (Chromium rejects otherwise)", () => { + const raw = JSON.stringify([ + { name: "n", value: "1", domain: "x.com", sameSite: "no_restriction", secure: false }, + { name: "l", value: "2", domain: "x.com", sameSite: "lax", secure: false }, + ]); + const [none, lax] = parseCookies(raw); + expect(none.secure).toBe(true); + expect(lax.secure).toBe(false); + }); + + it("accepts already-correct capitalized values and cookie-wrapper JSON", () => { + const raw = JSON.stringify({ + cookies: [{ name: "a", value: "1", domain: "x.com", sameSite: "None", secure: true }], + }); + const [c] = parseCookies(raw); + expect(c.sameSite).toBe("None"); + expect(c.secure).toBe(true); + }); +});