fix: add commander playwright wrapper
This commit is contained in:
@@ -12,6 +12,9 @@ interface CheckItem {
|
||||
|
||||
const syntaxFiles = [
|
||||
"scripts/cli.ts",
|
||||
"scripts/playwright-cli.ts",
|
||||
"scripts/playwright-cli-contract-test.ts",
|
||||
"scripts/src/playwright-cli.ts",
|
||||
"scripts/src/check.ts",
|
||||
"scripts/src/artifact-registry.ts",
|
||||
"scripts/src/auth-broker.ts",
|
||||
@@ -281,6 +284,8 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
if (options.files) {
|
||||
items.push(
|
||||
fileItem("scripts/cli.ts"),
|
||||
fileItem("scripts/playwright-cli.ts"),
|
||||
fileItem("scripts/src/playwright-cli.ts"),
|
||||
fileItem("AGENTS.md"),
|
||||
fileItem("TEST.md"),
|
||||
fileItem("docs/reference/artifact-registry.md"),
|
||||
@@ -335,6 +340,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
fileItem("scripts/gh-cli-issue-guard-contract-test.ts"),
|
||||
fileItem("scripts/gh-cli-pr-files-contract-test.ts"),
|
||||
fileItem("scripts/gh-cli-pr-contract-test.ts"),
|
||||
fileItem("scripts/playwright-cli-contract-test.ts"),
|
||||
fileItem("scripts/code-queue-pr-preflight-example.ts"),
|
||||
fileItem("scripts/schedule-cli-contract-test.ts"),
|
||||
fileItem("scripts/server-cleanup-plan-contract-test.ts"),
|
||||
@@ -385,6 +391,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
items.push(commandItem("gh:issue-guard-contract", ["bun", "scripts/gh-cli-issue-guard-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("gh:pr-files-contract", ["bun", "scripts/gh-cli-pr-files-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("gh:pr-contract", ["bun", "scripts/gh-cli-pr-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("playwright:cli-wrapper-contract", ["bun", "scripts/playwright-cli-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("auth-broker:p0-contract", ["bun", "scripts/auth-broker-contract-test.ts"], 30_000));
|
||||
} else {
|
||||
items.push(skippedItem("typescript:scripts", "scripts TypeScript typecheck is opt-in", "--scripts-typecheck or --full"));
|
||||
@@ -416,6 +423,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
items.push(skippedItem("gh:issue-guard-contract", "GitHub issue CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("gh:pr-files-contract", "GitHub PR files/stat contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("gh:pr-contract", "GitHub PR CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("playwright:cli-wrapper-contract", "Playwright wrapper/headless/session contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("auth-broker:p0-contract", "Auth Broker P0 skeleton and CLI adapter contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
}
|
||||
if (options.logs) {
|
||||
|
||||
@@ -74,6 +74,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "network perf [--service code-queue --path /api/tasks/overview?limit=30 --count N --concurrency N --label before|after]", description: "Benchmark frontend -> backend-core -> provider/adapter user-service networking and report latency/proxy-mode distributions." },
|
||||
{ command: "ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs", description: "Manage D601 k3s Tekton CI; artifact publish commands build commit-pinned images in CI without deploying CD." },
|
||||
{ command: "e2e run [--only pattern[,pattern...]] [--skip pattern[,pattern...]]", description: "Run selected public/internal/Playwright E2E checks; use --only for focused iteration and rerun without filters for final regression." },
|
||||
{ command: "bun scripts/playwright-cli.ts screenshot|open|eval ...", description: "Repo-owned Playwright wrapper for commander browser checks; headless by default, supports storage-state --session, and returns JSON guidance for unsupported interactive daemon commands." },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { chromium, firefox, webkit, type Browser, type BrowserContext, type BrowserType, type Page } from "playwright";
|
||||
import { repoRoot } from "./config";
|
||||
import { emitError, emitJson } from "./output";
|
||||
|
||||
type BrowserName = "chromium" | "firefox" | "webkit";
|
||||
type WaitUntil = "load" | "domcontentloaded" | "networkidle" | "commit";
|
||||
type PlaywrightCommand = "help" | "open" | "screenshot" | "eval" | "session-list" | "session-delete";
|
||||
|
||||
interface ParsedOptions {
|
||||
command: PlaywrightCommand | string;
|
||||
commandArgs: string[];
|
||||
browserName: BrowserName;
|
||||
dryRun: boolean;
|
||||
headless: boolean;
|
||||
sessionId: string;
|
||||
timeoutMs: number;
|
||||
waitUntil: WaitUntil;
|
||||
screenshotPath: string | null;
|
||||
selector: string | null;
|
||||
fullPage: boolean;
|
||||
}
|
||||
|
||||
const stateRoot = join(repoRoot, ".state", "playwright-cli");
|
||||
const defaultScreenshotPath = join(stateRoot, "screenshots", "latest.png");
|
||||
const supportedCommands = ["open", "screenshot", "eval", "session-list", "session-delete"];
|
||||
const unsupportedInteractiveCommands = new Set([
|
||||
"click",
|
||||
"dblclick",
|
||||
"fill",
|
||||
"type",
|
||||
"press",
|
||||
"hover",
|
||||
"drag",
|
||||
"select",
|
||||
"check",
|
||||
"uncheck",
|
||||
"snapshot",
|
||||
"tab-list",
|
||||
"tab-new",
|
||||
"tab-close",
|
||||
"tab-select",
|
||||
"close",
|
||||
"config",
|
||||
]);
|
||||
|
||||
function usage(): Record<string, unknown> {
|
||||
return {
|
||||
command: "playwright-cli",
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/playwright-cli.ts open <url> [--session id] [--screenshot path] [--headed|--headless]",
|
||||
"bun scripts/playwright-cli.ts screenshot <url> [path] [--session id] [--selector css] [--full-page]",
|
||||
"bun scripts/playwright-cli.ts eval <url> <javascript-expression> [--session id]",
|
||||
"bun scripts/playwright-cli.ts session-list",
|
||||
"bun scripts/playwright-cli.ts session-delete [sessionId]",
|
||||
],
|
||||
defaults: {
|
||||
browser: "chromium",
|
||||
headless: true,
|
||||
session: "default",
|
||||
waitUntil: "domcontentloaded",
|
||||
timeoutMs: 30_000,
|
||||
stateRoot,
|
||||
},
|
||||
behavior: [
|
||||
"This is a repo-owned short-run wrapper for commander browser checks, not the external agent-skill passthrough.",
|
||||
"It runs headless by default, so screenshots/open checks work on hosts without an X server.",
|
||||
"Named sessions persist Playwright storageState JSON between invocations; no long-running browser daemon or element-ref click session is kept.",
|
||||
"Use --headed only when a display is available; on headless hosts, run headed checks with xvfb-run -a bun scripts/playwright-cli.ts ... --headed. The xvfb fallback requires both xvfb-run and xauth.",
|
||||
],
|
||||
unsupported: {
|
||||
reason: "persistent interactive session commands require a browser daemon that this wrapper does not provide",
|
||||
commands: Array.from(unsupportedInteractiveCommands).sort(),
|
||||
},
|
||||
externalSkillGap: {
|
||||
observed: "The external playwright skill at ~/.agents/skills/playwright currently behaves like npx playwright passthrough while its SKILL.md may advertise richer session commands.",
|
||||
sourceOfTruth: "Track repo-owned commander checks here; update the external skill source separately if the skill itself must expose daemon/session commands.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): ParsedOptions {
|
||||
let browserName: BrowserName = "chromium";
|
||||
let dryRun = false;
|
||||
let headless = true;
|
||||
let sessionId = "default";
|
||||
let timeoutMs = 30_000;
|
||||
let waitUntil: WaitUntil = "domcontentloaded";
|
||||
let screenshotPath: string | null = null;
|
||||
let selector: string | null = null;
|
||||
let fullPage = false;
|
||||
const positional: string[] = [];
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index] ?? "";
|
||||
const next = argv[index + 1];
|
||||
if (arg === "--help" || arg === "-h" || arg === "help") {
|
||||
positional.push("help");
|
||||
} else if (arg === "--dry-run") {
|
||||
dryRun = true;
|
||||
} else if (arg === "--headless") {
|
||||
headless = true;
|
||||
} else if (arg === "--headed") {
|
||||
headless = false;
|
||||
} else if (arg === "--full-page" || arg === "--fullPage") {
|
||||
fullPage = true;
|
||||
} else if (arg.startsWith("--session=")) {
|
||||
sessionId = nonEmptyValue("--session", arg.slice("--session=".length));
|
||||
} else if (arg === "--session") {
|
||||
sessionId = nonEmptyValue("--session", next);
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--browser=")) {
|
||||
browserName = parseBrowser(arg.slice("--browser=".length));
|
||||
} else if (arg === "--browser") {
|
||||
browserName = parseBrowser(next);
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--timeout-ms=")) {
|
||||
timeoutMs = parsePositiveInteger("--timeout-ms", arg.slice("--timeout-ms=".length));
|
||||
} else if (arg === "--timeout-ms") {
|
||||
timeoutMs = parsePositiveInteger("--timeout-ms", next);
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--wait-until=")) {
|
||||
waitUntil = parseWaitUntil(arg.slice("--wait-until=".length));
|
||||
} else if (arg === "--wait-until") {
|
||||
waitUntil = parseWaitUntil(next);
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--screenshot=") || arg.startsWith("--filename=")) {
|
||||
const value = arg.includes("--screenshot=") ? arg.slice("--screenshot=".length) : arg.slice("--filename=".length);
|
||||
screenshotPath = resolvePath(nonEmptyValue("--screenshot", value));
|
||||
} else if (arg === "--screenshot" || arg === "--filename") {
|
||||
screenshotPath = resolvePath(nonEmptyValue(arg, next));
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--selector=")) {
|
||||
selector = nonEmptyValue("--selector", arg.slice("--selector=".length));
|
||||
} else if (arg === "--selector") {
|
||||
selector = nonEmptyValue("--selector", next);
|
||||
index += 1;
|
||||
} else {
|
||||
positional.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
const [command = "help", ...commandArgs] = positional;
|
||||
return { command, commandArgs, browserName, dryRun, headless, sessionId, timeoutMs, waitUntil, screenshotPath, selector, fullPage };
|
||||
}
|
||||
|
||||
function parseBrowser(value: string | undefined): BrowserName {
|
||||
const browser = nonEmptyValue("--browser", value);
|
||||
if (browser === "chromium" || browser === "chrome") return "chromium";
|
||||
if (browser === "firefox") return "firefox";
|
||||
if (browser === "webkit") return "webkit";
|
||||
throw new Error(`--browser must be chromium, firefox, or webkit; received ${browser}`);
|
||||
}
|
||||
|
||||
function parseWaitUntil(value: string | undefined): WaitUntil {
|
||||
const waitUntil = nonEmptyValue("--wait-until", value);
|
||||
if (waitUntil === "load" || waitUntil === "domcontentloaded" || waitUntil === "networkidle" || waitUntil === "commit") return waitUntil;
|
||||
throw new Error(`--wait-until must be load, domcontentloaded, networkidle, or commit; received ${waitUntil}`);
|
||||
}
|
||||
|
||||
function parsePositiveInteger(name: string, value: string | undefined): number {
|
||||
const parsed = Number(nonEmptyValue(name, value));
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${name} must be a positive integer`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function nonEmptyValue(name: string, value: string | undefined): string {
|
||||
if (value === undefined || value.length === 0) throw new Error(`${name} requires a non-empty value`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function resolvePath(value: string): string {
|
||||
return resolve(repoRoot, value);
|
||||
}
|
||||
|
||||
function safeSessionId(value: string): string {
|
||||
if (!/^[A-Za-z0-9._-]{1,80}$/u.test(value)) {
|
||||
throw new Error("--session must contain only letters, numbers, dot, underscore, or dash, max 80 characters");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function sessionsDir(): string {
|
||||
return join(stateRoot, "sessions");
|
||||
}
|
||||
|
||||
function sessionFile(sessionId: string): string {
|
||||
return join(sessionsDir(), `${safeSessionId(sessionId)}.storage.json`);
|
||||
}
|
||||
|
||||
function browserType(name: BrowserName): BrowserType {
|
||||
if (name === "firefox") return firefox;
|
||||
if (name === "webkit") return webkit;
|
||||
return chromium;
|
||||
}
|
||||
|
||||
function redactOptions(options: ParsedOptions): Record<string, unknown> {
|
||||
return {
|
||||
command: options.command,
|
||||
commandArgs: options.commandArgs,
|
||||
browser: options.browserName,
|
||||
headless: options.headless,
|
||||
sessionId: options.sessionId,
|
||||
timeoutMs: options.timeoutMs,
|
||||
waitUntil: options.waitUntil,
|
||||
screenshotPath: options.screenshotPath,
|
||||
selector: options.selector,
|
||||
fullPage: options.fullPage,
|
||||
sessionStatePath: sessionFile(options.sessionId),
|
||||
};
|
||||
}
|
||||
|
||||
async function createContext(options: ParsedOptions): Promise<{ browser: Browser; context: BrowserContext; close: () => Promise<void> }> {
|
||||
let browser: Browser;
|
||||
try {
|
||||
browser = await browserType(options.browserName).launch({ headless: options.headless });
|
||||
} catch (error) {
|
||||
if (!options.headless && isDisplayFailure(error)) {
|
||||
throw new Error(`headed browser launch failed because no display is available; rerun with --headless or use xvfb-run -a bun scripts/playwright-cli.ts ${process.argv.slice(2).join(" ")}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const storageState = sessionFile(options.sessionId);
|
||||
const context = await browser.newContext(existsSync(storageState) ? { storageState } : {});
|
||||
return {
|
||||
browser,
|
||||
context,
|
||||
close: async () => {
|
||||
await context.close();
|
||||
await browser.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isDisplayFailure(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return /Target page, context or browser has been closed|Missing X server|no display|DISPLAY|xvfb-run|Looks like you launched a headed browser/u.test(message);
|
||||
}
|
||||
|
||||
async function gotoPage(page: Page, url: string, options: ParsedOptions): Promise<number | null> {
|
||||
const response = await page.goto(url, { timeout: options.timeoutMs, waitUntil: options.waitUntil });
|
||||
return response?.status() ?? null;
|
||||
}
|
||||
|
||||
async function runOpen(options: ParsedOptions): Promise<Record<string, unknown>> {
|
||||
const url = nonEmptyValue("open url", options.commandArgs[0]);
|
||||
const sessionStatePath = sessionFile(options.sessionId);
|
||||
const screenshotPath = options.screenshotPath;
|
||||
const plan = {
|
||||
ok: true,
|
||||
action: "open",
|
||||
url,
|
||||
browser: options.browserName,
|
||||
headless: options.headless,
|
||||
sessionId: options.sessionId,
|
||||
sessionStatePath,
|
||||
timeoutMs: options.timeoutMs,
|
||||
waitUntil: options.waitUntil,
|
||||
screenshotPath,
|
||||
};
|
||||
if (options.dryRun) return { ...plan, dryRun: true, mutation: false };
|
||||
|
||||
mkdirSync(sessionsDir(), { recursive: true });
|
||||
const { context, close } = await createContext(options);
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
const status = await gotoPage(page, url, options);
|
||||
const title = await page.title();
|
||||
if (screenshotPath !== null) {
|
||||
mkdirSync(dirname(screenshotPath), { recursive: true });
|
||||
await page.screenshot({ path: screenshotPath, fullPage: options.fullPage });
|
||||
}
|
||||
await context.storageState({ path: sessionStatePath });
|
||||
return {
|
||||
...plan,
|
||||
dryRun: false,
|
||||
mutation: true,
|
||||
status,
|
||||
finalUrl: page.url(),
|
||||
title,
|
||||
sessionSaved: true,
|
||||
};
|
||||
} finally {
|
||||
await close();
|
||||
}
|
||||
}
|
||||
|
||||
async function runScreenshot(options: ParsedOptions): Promise<Record<string, unknown>> {
|
||||
const url = nonEmptyValue("screenshot url", options.commandArgs[0]);
|
||||
const positionalPath = options.commandArgs[1] === undefined ? null : resolvePath(options.commandArgs[1]);
|
||||
const screenshotPath = options.screenshotPath ?? positionalPath ?? defaultScreenshotPath;
|
||||
const sessionStatePath = sessionFile(options.sessionId);
|
||||
const plan = {
|
||||
ok: true,
|
||||
action: "screenshot",
|
||||
url,
|
||||
browser: options.browserName,
|
||||
headless: options.headless,
|
||||
sessionId: options.sessionId,
|
||||
sessionStatePath,
|
||||
timeoutMs: options.timeoutMs,
|
||||
waitUntil: options.waitUntil,
|
||||
screenshotPath,
|
||||
selector: options.selector,
|
||||
fullPage: options.fullPage,
|
||||
};
|
||||
if (options.dryRun) return { ...plan, dryRun: true, mutation: false };
|
||||
|
||||
mkdirSync(sessionsDir(), { recursive: true });
|
||||
mkdirSync(dirname(screenshotPath), { recursive: true });
|
||||
const { context, close } = await createContext(options);
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
const status = await gotoPage(page, url, options);
|
||||
const title = await page.title();
|
||||
if (options.selector !== null) {
|
||||
await page.locator(options.selector).screenshot({ path: screenshotPath, timeout: options.timeoutMs });
|
||||
} else {
|
||||
await page.screenshot({ path: screenshotPath, fullPage: options.fullPage });
|
||||
}
|
||||
await context.storageState({ path: sessionStatePath });
|
||||
return {
|
||||
...plan,
|
||||
dryRun: false,
|
||||
mutation: true,
|
||||
status,
|
||||
finalUrl: page.url(),
|
||||
title,
|
||||
sessionSaved: true,
|
||||
};
|
||||
} finally {
|
||||
await close();
|
||||
}
|
||||
}
|
||||
|
||||
async function runEval(options: ParsedOptions): Promise<Record<string, unknown>> {
|
||||
const url = nonEmptyValue("eval url", options.commandArgs[0]);
|
||||
const expression = nonEmptyValue("eval expression", options.commandArgs[1]);
|
||||
const sessionStatePath = sessionFile(options.sessionId);
|
||||
const plan = {
|
||||
ok: true,
|
||||
action: "eval",
|
||||
url,
|
||||
browser: options.browserName,
|
||||
headless: options.headless,
|
||||
sessionId: options.sessionId,
|
||||
sessionStatePath,
|
||||
timeoutMs: options.timeoutMs,
|
||||
waitUntil: options.waitUntil,
|
||||
expressionPreview: expression.length > 200 ? `${expression.slice(0, 200)}...` : expression,
|
||||
};
|
||||
if (options.dryRun) return { ...plan, dryRun: true, mutation: false };
|
||||
|
||||
mkdirSync(sessionsDir(), { recursive: true });
|
||||
const { context, close } = await createContext(options);
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
const status = await gotoPage(page, url, options);
|
||||
const value = await page.evaluate((source) => {
|
||||
return Function(`"use strict"; return (${source});`)();
|
||||
}, expression);
|
||||
await context.storageState({ path: sessionStatePath });
|
||||
return {
|
||||
...plan,
|
||||
dryRun: false,
|
||||
mutation: true,
|
||||
status,
|
||||
finalUrl: page.url(),
|
||||
value,
|
||||
sessionSaved: true,
|
||||
};
|
||||
} finally {
|
||||
await close();
|
||||
}
|
||||
}
|
||||
|
||||
function runSessionList(): Record<string, unknown> {
|
||||
if (!existsSync(sessionsDir())) return { ok: true, sessions: [], stateRoot };
|
||||
const glob = new Bun.Glob("*.storage.json");
|
||||
const sessions = Array.from(glob.scanSync({ cwd: sessionsDir() })).map((file) => file.replace(/\.storage\.json$/u, ""));
|
||||
return { ok: true, sessions: sessions.sort(), stateRoot };
|
||||
}
|
||||
|
||||
function runSessionDelete(options: ParsedOptions): Record<string, unknown> {
|
||||
const target = options.commandArgs[0] ?? options.sessionId;
|
||||
const file = sessionFile(target);
|
||||
const existed = existsSync(file);
|
||||
if (options.dryRun) return { ok: true, dryRun: true, mutation: false, sessionId: target, path: file, existed };
|
||||
if (existed) rmSync(file);
|
||||
return { ok: true, dryRun: false, mutation: existed, sessionId: target, path: file, deleted: existed };
|
||||
}
|
||||
|
||||
function unsupportedCommand(command: string, options: ParsedOptions): Record<string, unknown> {
|
||||
return {
|
||||
ok: false,
|
||||
error: "unsupported-command",
|
||||
command,
|
||||
supportedCommands,
|
||||
reason: unsupportedInteractiveCommands.has(command)
|
||||
? "This repo-owned wrapper is short-run/headless and does not keep a live page with element refs between invocations."
|
||||
: "Unknown Playwright wrapper command.",
|
||||
actualBehavior: "Use open/screenshot/eval for bounded commander checks, or call npx playwright directly when you intentionally need upstream Playwright CLI behavior.",
|
||||
next: [
|
||||
"bun scripts/playwright-cli.ts screenshot <url> /tmp/page.png --session <id>",
|
||||
"bun scripts/playwright-cli.ts open <url> --session <id> --screenshot /tmp/page.png",
|
||||
"xvfb-run -a bun scripts/playwright-cli.ts open <url> --headed --screenshot /tmp/page.png",
|
||||
],
|
||||
launchPlan: redactOptions(options),
|
||||
};
|
||||
}
|
||||
|
||||
function displayUnavailableError(error: unknown): Record<string, unknown> {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
ok: false,
|
||||
error: "display-unavailable",
|
||||
reason: "A headed browser requires an X server/display, but this host does not expose one.",
|
||||
message,
|
||||
next: [
|
||||
"rerun the same command without --headed",
|
||||
`xvfb-run -a bun scripts/playwright-cli.ts ${process.argv.slice(2).join(" ")}`,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function runPlaywrightCli(argv: string[]): Promise<void> {
|
||||
const options = parseArgs(argv);
|
||||
const commandName = `playwright-cli ${argv.join(" ")}`.trim();
|
||||
if (options.command === "help" || options.command === "--help" || options.command === "-h") {
|
||||
emitJson(commandName, usage());
|
||||
return;
|
||||
}
|
||||
if (options.command === "open") {
|
||||
emitJson(commandName, await runOpen(options));
|
||||
return;
|
||||
}
|
||||
if (options.command === "screenshot") {
|
||||
emitJson(commandName, await runScreenshot(options));
|
||||
return;
|
||||
}
|
||||
if (options.command === "eval") {
|
||||
emitJson(commandName, await runEval(options));
|
||||
return;
|
||||
}
|
||||
if (options.command === "session-list") {
|
||||
emitJson(commandName, runSessionList());
|
||||
return;
|
||||
}
|
||||
if (options.command === "session-delete") {
|
||||
emitJson(commandName, runSessionDelete(options));
|
||||
return;
|
||||
}
|
||||
emitJson(commandName, unsupportedCommand(options.command, options), false);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
export function handlePlaywrightCliError(error: unknown): void {
|
||||
if (isDisplayFailure(error)) {
|
||||
emitJson("playwright-cli", displayUnavailableError(error), false);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
emitError("playwright-cli", error);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user