Skip to content

Commit 5025422

Browse files
committed
feat: Don’t use Playwright’s BrowserContext options object format directly; introduce AuthContext abstraction
1 parent 41c5651 commit 5025422

File tree

12 files changed

+139
-34
lines changed

12 files changed

+139
-34
lines changed

.github/actions/auth/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ Authenticate with Playwright and save session state. Supports HTTP Basic and for
2323
2424
### Outputs
2525

26-
#### `playwright_context_options`
26+
#### `auth_context`
2727

28-
Stringified JSON object containing [Playwright `BrowserContext`](https://playwright.dev/docs/api/class-browsercontext) options, including `httpCredentials` and `storageState`. For example: `{"httpCredentials":{"username":"some-user",password:"correct-horse-battery-staple"},"storageState":"/tmp/.auth/12345678/sessionState.json"}`
28+
Stringified JSON object containing `username`, `password`, `cookies`, and/or `localStorage` from an authenticated session. For example: `{"username":"some-user","password":"correct-horse-battery-staple","cookies":[{"name":"theme-preference","value":"light","domain":"primer.style","path":"/"}],"localStorage":{"https://primer.style":{"theme-preference":"light"}}}`

.github/actions/auth/action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ inputs:
1313
required: true
1414

1515
outputs:
16-
playwright_context_options:
17-
description: "Stringified JSON object containing Playwright 'BrowserContext' options, including 'httpCredentials' and 'storageState'"
16+
auth_context:
17+
description: "Stringified JSON object containing 'username', 'password', 'cookies', and/or 'localStorage' from an authenticated session"
1818

1919
runs:
2020
using: "node20"

.github/actions/auth/src/index.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { AuthContextOutput } from "./types.d.js";
12
import crypto from "node:crypto";
23
import process from "node:process";
34
import * as url from "node:url";
@@ -67,16 +68,22 @@ export default async function () {
6768
// This occurs if HTTP Basic auth succeeded, or if the page does not require authentication.
6869
}
6970

70-
// Write authenticated session state to a file and output its path
71-
await context.storageState({ path: sessionStatePath });
72-
core.info(`Wrote authenticated session state to ${sessionStatePath}`);
73-
core.setOutput(
74-
"playwright_context_options",
75-
JSON.stringify({
76-
httpCredentials: { username, password },
77-
storageState: sessionStatePath,
78-
})
79-
);
71+
// Output authenticated session state
72+
const { cookies, origins } = await context.storageState();
73+
const authContextOutput: AuthContextOutput = {
74+
username,
75+
password,
76+
cookies,
77+
localStorage: origins.reduce((acc, { origin, localStorage }) => {
78+
acc[origin] = localStorage.reduce((acc, { name, value }) => {
79+
acc[name] = value;
80+
return acc;
81+
}, {} as Record<string, string>);
82+
return acc;
83+
}, {} as Record<string, Record<string, string>>),
84+
};
85+
core.setOutput("auth_context", JSON.stringify(authContextOutput));
86+
core.debug("Output: 'auth_context'");
8087
} catch (error) {
8188
if (page) {
8289
core.info(`Errored at page URL: ${page.url()}`);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export type Cookie = {
2+
name: string;
3+
value: string;
4+
domain: string;
5+
path: string;
6+
expires?: number;
7+
httpOnly?: boolean;
8+
secure?: boolean;
9+
sameSite?: "Strict" | "Lax" | "None";
10+
};
11+
12+
export type LocalStorage = {
13+
[origin: string]: {
14+
[key: string]: string;
15+
};
16+
};
17+
18+
export type AuthContextOutput = {
19+
username?: string;
20+
password?: string;
21+
cookies?: Cookie[];
22+
localStorage?: LocalStorage;
23+
};

.github/actions/find/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ https://primer.style
1515
https://primer.style/octicons/
1616
```
1717

18-
#### `playwright_context_options`
18+
#### `auth_context`
1919

20-
**Optional** Stringified JSON object containing [Playwright `BrowserContext`](https://playwright.dev/docs/api/class-browsercontext) options, including `httpCredentials` and `storageState`. For example: `{"httpCredentials":{"username":"some-user",password:"correct-horse-battery-staple"},"storageState":"/tmp/.auth/12345678/sessionState.json"}`
20+
**Optional** Stringified JSON object containing `username`, `password`, `cookies`, and/or `localStorage` from an authenticated session. For example: `{"username":"some-user","password":"correct-horse-battery-staple","cookies":[{"name":"theme-preference","value":"light","domain":"primer.style","path":"/"}],"localStorage":{"https://primer.style":{"theme-preference":"light"}}}`
2121

2222
### Outputs
2323

.github/actions/find/action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ inputs:
66
description: "Newline-delimited list of URLs to check for accessibility issues"
77
required: true
88
multiline: true
9-
playwright_context_options:
10-
description: "Stringified JSON object containing Playwright 'BrowserContext' options, including 'httpCredentials' and 'storageState'"
9+
auth_context:
10+
description: "Stringified JSON object containing 'username', 'password', 'cookies', and/or 'localStorage' from an authenticated session"
1111
required: false
1212

1313
outputs:
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type playwright from "playwright";
2+
import type { Cookie, LocalStorage, AuthContextInput } from "./types.js";
3+
4+
export class AuthContext implements AuthContextInput {
5+
readonly username?: string;
6+
readonly password?: string;
7+
readonly cookies?: Cookie[];
8+
readonly localStorage?: LocalStorage;
9+
10+
constructor({ username, password, cookies, localStorage }: AuthContextInput) {
11+
this.username = username;
12+
this.password = password;
13+
this.cookies = cookies;
14+
this.localStorage = localStorage;
15+
}
16+
17+
toPlaywrightBrowserContextOptions(): playwright.BrowserContextOptions {
18+
const playwrightBrowserContextOptions: playwright.BrowserContextOptions =
19+
{};
20+
if (this.username && this.password) {
21+
playwrightBrowserContextOptions.httpCredentials = {
22+
username: this.username,
23+
password: this.password,
24+
};
25+
}
26+
if (this.cookies || this.localStorage) {
27+
playwrightBrowserContextOptions.storageState = {
28+
// Add default values for fields Playwright requires which aren’t actually required by the Cookie API.
29+
cookies:
30+
this.cookies?.map((cookie) => ({
31+
expires: -1,
32+
httpOnly: false,
33+
secure: false,
34+
sameSite: "Lax",
35+
...cookie,
36+
})) ?? [],
37+
// Transform the localStorage object into the shape Playwright expects.
38+
origins:
39+
Object.entries(this.localStorage ?? {}).map(([origin, kv]) => ({
40+
origin,
41+
localStorage: Object.entries(kv).map(([name, value]) => ({
42+
name,
43+
value,
44+
})),
45+
})) ?? [],
46+
};
47+
}
48+
return playwrightBrowserContextOptions;
49+
}
50+
}

.github/actions/find/src/findForUrl.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type { Finding } from './types.d.js';
22
import AxeBuilder from '@axe-core/playwright'
33
import playwright from 'playwright';
4+
import { AuthContext } from './authContext.js';
45

5-
export async function findForUrl(url: string, playwrightContextOptions?: playwright.BrowserContextOptions): Promise<Finding[]> {
6+
export async function findForUrl(url: string, authContext?: AuthContext): Promise<Finding[]> {
67
const browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.CI ? '/usr/bin/google-chrome' : undefined });
7-
const context = await browser.newContext(playwrightContextOptions);
8+
const contextOptions = authContext?.toPlaywrightBrowserContextOptions() ?? {};
9+
const context = await browser.newContext(contextOptions);
810
const page = await context.newPage();
911
await page.goto(url);
1012

.github/actions/find/src/index.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
1-
import type playwright from "playwright";
1+
import type { AuthContextInput } from "./types.js";
22
import core from "@actions/core";
3+
import { AuthContext } from "./authContext.js";
34
import { findForUrl } from "./findForUrl.js";
45

56
export default async function () {
67
core.info("Starting 'find' action");
78
const urls = core.getMultilineInput("urls", { required: true });
89
core.debug(`Input: 'urls: ${JSON.stringify(urls)}'`);
9-
const playwrightContextOptions: playwright.BrowserContextOptions = JSON.parse(
10-
core.getInput("playwright_context_options", { required: false }) ?? "{}"
10+
const authContextInput: AuthContextInput = JSON.parse(
11+
core.getInput("auth_context", { required: false }) ?? "{}"
1112
);
12-
if (playwrightContextOptions?.httpCredentials?.password) {
13-
core.setSecret(playwrightContextOptions.httpCredentials.password);
14-
}
13+
const authContext = new AuthContext(authContextInput);
1514

1615
let findings = [];
1716
for (const url of urls) {
1817
core.info(`Scanning ${url}`);
19-
const findingsForUrl = await findForUrl(url, playwrightContextOptions);
18+
const findingsForUrl = await findForUrl(url, authContext);
2019
if (findingsForUrl.length === 0) {
2120
core.info(`No accessibility gaps were found on ${url}`);
2221
continue;

.github/actions/find/src/types.d.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,28 @@ export type Finding = {
55
problemUrl: string;
66
solutionShort: string;
77
solutionLong?: string;
8-
}
8+
};
9+
10+
export type Cookie = {
11+
name: string;
12+
value: string;
13+
domain: string;
14+
path: string;
15+
expires?: number;
16+
httpOnly?: boolean;
17+
secure?: boolean;
18+
sameSite?: "Strict" | "Lax" | "None";
19+
};
20+
21+
export type LocalStorage = {
22+
[origin: string]: {
23+
[key: string]: string;
24+
};
25+
};
26+
27+
export type AuthContextInput = {
28+
username?: string;
29+
password?: string;
30+
cookies?: Cookie[];
31+
localStorage?: LocalStorage;
32+
};

0 commit comments

Comments
 (0)