Skip to content

Instantly share code, notes, and snippets.

@aabccd021
Created July 27, 2025 08:49
Show Gist options
  • Save aabccd021/aefa0e513ee20dc6c4f4bd9ab5b643f1 to your computer and use it in GitHub Desktop.
Save aabccd021/aefa0e513ee20dc6c4f4bd9ab5b643f1 to your computer and use it in GitHub Desktop.
Netero playwright
import * as fs from "node:fs";
import type { Locator, Page } from "playwright";
export type FormInput =
| {
type: "text";
value: string;
}
| {
type: "radio";
value: string;
}
| {
type: "checkbox";
checked: boolean;
};
export type Action =
| {
action: "goto-url";
value: string;
}
| {
action: "goto";
xpath: string;
}
| {
action: "submit";
button?: string;
data?: Record<string, { type: "text"; value: string }>;
}
| {
action: "time-advance";
value: number;
}
| {
action: "assert-url";
expected: string;
}
| {
action: "assert-attribute";
xpath: string;
attribute: string;
expected: string;
}
| {
action: "assert-text";
xpath: string;
expected: string;
};
export type Scenario = {
prev: string;
steps: string[];
};
export type Config = {
steps: Record<string, Action>;
scenarios: Record<string, Scenario>;
};
async function handleInput(
form: Locator,
name: string,
input: FormInput,
): Promise<void> {
if (input.type === "text") {
const element = form.locator(`//input[@name='${name}']`);
await element.fill(input.value);
return;
}
if (input.type === "checkbox") {
const element = form.locator(`//input[@name='${name}']`);
if (input.checked) {
await element.check();
} else {
await element.uncheck();
}
return;
}
if (input.type === "radio") {
const element = form.locator(
`//input[@name='${name}' and @value='${input.value}']`,
);
await element.check();
return;
}
input satisfies never;
throw new Error(`Unknown input type: ${JSON.stringify(input)}`);
}
export async function handleAction(
neteroState: string,
page: Page,
action: Action,
): Promise<void> {
if (action.action === "goto-url") {
await page.goto(action.value);
return;
}
if (action.action === "goto") {
await page.locator(action.xpath).click();
return;
}
if (action.action === "submit") {
const form = page.locator("//form");
for (const [name, input] of Object.entries(action.data ?? {})) {
await handleInput(form, name, input);
}
const submitButton =
action.button !== undefined
? page.locator(action.button)
: form.locator("//button[@type='submit' or @action='submit']");
await submitButton.click();
return;
}
if (action.action === "time-advance") {
const oldTimeStr = await fs.promises.readFile(
`${neteroState}/now.txt`,
"utf-8",
);
const oldTime = parseInt(oldTimeStr, 10);
const newTime = oldTime + action.value;
await fs.promises.writeFile(`${neteroState}/now.txt`, newTime.toString());
return;
}
if (action.action === "assert-url") {
const url = page.url();
if (!new RegExp(action.expected).test(url)) {
throw new Error(`Expected URL ${url} to match ${action.expected}`);
}
return;
}
if (action.action === "assert-attribute") {
const attributeValue = await page
.locator(action.xpath)
.getAttribute(action.attribute);
if (attributeValue === null) {
throw new Error(
`Attribute "${action.attribute}" not found for element at "${action.xpath}"`,
);
}
if (!new RegExp(action.expected).test(attributeValue)) {
throw new Error(
`Expected attribute "${action.attribute}" to match "${action.expected}", but got "${attributeValue}"`,
);
}
return;
}
if (action.action === "assert-text") {
const textContent = await page.locator(action.xpath).textContent();
if (textContent === null) {
throw new Error(
`Text content not found for element at "${action.xpath}"`,
);
}
if (!new RegExp(action.expected).test(textContent)) {
throw new Error(
`Expected text content "${textContent}" to match "${action.expected}"`,
);
}
return;
}
action satisfies never;
}
import * as fs from "node:fs";
import * as os from "node:os";
import * as util from "node:util";
import { chromium } from "playwright";
import { type Config, handleAction } from "./action";
let theme: "light" | "dark" | undefined;
const { values: args } = util.parseArgs({
args: process.argv.slice(2),
options: {
theme: {
type: "string",
},
config: {
type: "string",
},
scenario: {
type: "string",
},
},
strict: true,
allowPositionals: true,
});
if (
args.theme === "light" ||
args.theme === "dark" ||
args.theme === undefined
) {
theme = args.theme;
} else {
throw new Error(
`Invalid theme: ${args.theme}. Must be "light", "dark", or undefined.`,
);
}
if (args.config === undefined) {
throw new Error("No actions provided. Use --config to specify them.");
}
if (args.scenario === undefined) {
throw new Error("No scenario provided. Use --scenario to specify it.");
}
const neteroState = process.env["NETERO_STATE"];
if (neteroState === undefined) {
throw new Error("NETERO_STATE environment variable is not set.");
}
const configStr = fs.readFileSync(args.config, "utf-8");
const config: Config = JSON.parse(configStr);
const scenario = config.scenarios[args.scenario];
if (scenario === undefined) {
throw new Error(`Scenario "${args.scenario}" not found in config.`);
}
const activeBrowser = fs.readFileSync(
`${neteroState}/active-browser.txt`,
"utf-8",
);
const activeTab = fs.readFileSync(`${neteroState}/active-tab.txt`, "utf-8");
const dataDir = os.tmpdir();
const browser = await chromium.launchPersistentContext(dataDir, {
headless: true,
colorScheme: theme,
});
browser.on("close", () => {
process.exit(0);
});
const cookieFile = `${neteroState}/browser/${activeBrowser}/cookie.json`;
const urlFile = `${neteroState}/browser/${activeBrowser}/tab/${activeTab}/url.txt`;
if (fs.existsSync(cookieFile)) {
const cookiesStr = fs.readFileSync(cookieFile, "utf-8");
const cookies = JSON.parse(cookiesStr);
await browser.addCookies(cookies);
}
const page = await browser.newPage();
if (fs.existsSync(urlFile)) {
const url = fs.readFileSync(urlFile, "utf-8");
await page.goto(url);
}
for (const step of scenario.steps) {
const action = config.steps[step];
if (action === undefined) {
throw new Error(`Action "${step}" not found in config.`);
}
await handleAction(neteroState, page, action);
}
const url = page.url();
const cookies = await browser.cookies();
await Promise.all([
fs.promises.writeFile(
`${neteroState}/browser/${activeBrowser}/tab/${activeTab}/url.txt`,
url,
),
fs.promises.writeFile(
`${neteroState}/browser/${activeBrowser}/cookie.json`,
JSON.stringify(cookies, null, 2),
),
]);
process.exit(0);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment