Skip to content

Commit acf75fb

Browse files
ralyodioclaude
andauthored
Normalize cookie sameSite for Playwright addCookies (#64)
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 <noreply@anthropic.com>
1 parent 6dfb20e commit acf75fb

2 files changed

Lines changed: 78 additions & 10 deletions

File tree

‎lib/sp/platforms/browser.ts‎

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,41 @@ async function launchContext(cookies: BrowserCookie[]): Promise<{
7171
}
7272
}
7373

74+
// Playwright's addCookies only accepts sameSite "Strict" | "Lax" | "None".
75+
// Cookie-Editor / Chrome exports use other spellings ("no_restriction",
76+
// "unspecified", lowercase "lax"/"strict") or null, which fail the enum check
77+
// with: cookies[N].sameSite: expected one of (Strict|Lax|None). Map them.
78+
function normalizeSameSite(value: unknown): BrowserCookie["sameSite"] {
79+
switch (String(value ?? "").toLowerCase()) {
80+
case "strict":
81+
return "Strict";
82+
case "none":
83+
case "no_restriction":
84+
return "None";
85+
default:
86+
// "lax", "unspecified", "", null, or anything unexpected.
87+
return "Lax";
88+
}
89+
}
90+
7491
export function parseCookies(raw: string): BrowserCookie[] {
7592
const parsed = JSON.parse(raw);
7693
const arr: unknown[] = Array.isArray(parsed) ? parsed : parsed.cookies ?? parsed;
7794
if (!Array.isArray(arr)) throw new Error("Cookie JSON must be an array.");
78-
return arr.map((c: any) => ({
79-
name: c.name,
80-
value: c.value,
81-
domain: c.domain ?? c.host ?? "",
82-
path: c.path ?? "/",
83-
expires: c.expirationDate ?? c.expires ?? -1,
84-
httpOnly: c.httpOnly ?? false,
85-
secure: c.secure ?? false,
86-
sameSite: (c.sameSite as BrowserCookie["sameSite"]) ?? "Lax",
87-
}));
95+
return arr.map((c: any) => {
96+
const sameSite = normalizeSameSite(c.sameSite);
97+
return {
98+
name: c.name,
99+
value: c.value,
100+
domain: c.domain ?? c.host ?? "",
101+
path: c.path ?? "/",
102+
expires: c.expirationDate ?? c.expires ?? -1,
103+
httpOnly: c.httpOnly ?? false,
104+
// Chromium rejects SameSite=None cookies that aren't Secure.
105+
secure: (c.secure ?? false) || sameSite === "None",
106+
sameSite,
107+
};
108+
});
88109
}
89110

90111
// ---------- Reddit ----------

‎tests/sp/parse-cookies.test.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, it, expect } from "vitest";
2+
import { parseCookies } from "@/lib/sp/platforms/browser";
3+
4+
const ALLOWED = new Set(["Strict", "Lax", "None"]);
5+
6+
describe("parseCookies sameSite normalization", () => {
7+
it("maps Chrome/Cookie-Editor sameSite spellings to Playwright's enum", () => {
8+
const raw = JSON.stringify([
9+
{ name: "a", value: "1", domain: "x.com", sameSite: "no_restriction" },
10+
{ name: "b", value: "2", domain: "x.com", sameSite: "lax" },
11+
{ name: "c", value: "3", domain: "x.com", sameSite: "strict" },
12+
{ name: "d", value: "4", domain: "x.com", sameSite: "unspecified" },
13+
{ name: "e", value: "5", domain: "x.com", sameSite: null },
14+
{ name: "f", value: "6", domain: "x.com" }, // missing entirely
15+
]);
16+
const cookies = parseCookies(raw);
17+
expect(cookies.map((c) => c.sameSite)).toEqual([
18+
"None",
19+
"Lax",
20+
"Strict",
21+
"Lax",
22+
"Lax",
23+
"Lax",
24+
]);
25+
// Every value is one Playwright accepts.
26+
for (const c of cookies) expect(ALLOWED.has(c.sameSite!)).toBe(true);
27+
});
28+
29+
it("forces Secure on SameSite=None cookies (Chromium rejects otherwise)", () => {
30+
const raw = JSON.stringify([
31+
{ name: "n", value: "1", domain: "x.com", sameSite: "no_restriction", secure: false },
32+
{ name: "l", value: "2", domain: "x.com", sameSite: "lax", secure: false },
33+
]);
34+
const [none, lax] = parseCookies(raw);
35+
expect(none.secure).toBe(true);
36+
expect(lax.secure).toBe(false);
37+
});
38+
39+
it("accepts already-correct capitalized values and cookie-wrapper JSON", () => {
40+
const raw = JSON.stringify({
41+
cookies: [{ name: "a", value: "1", domain: "x.com", sameSite: "None", secure: true }],
42+
});
43+
const [c] = parseCookies(raw);
44+
expect(c.sameSite).toBe("None");
45+
expect(c.secure).toBe(true);
46+
});
47+
});

0 commit comments

Comments
 (0)