scripts
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
+5
-1
@@ -10,7 +10,10 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts"
|
||||
"dev": "tsx src/index.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"format": "prettier --write 'src/**/*.ts'",
|
||||
"format:check": "prettier --check 'src/**/*.ts'"
|
||||
},
|
||||
"keywords": [
|
||||
"browser",
|
||||
@@ -28,6 +31,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"prettier": "^3.7.4",
|
||||
"tsx": "^4.6.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
|
||||
+45
-147
@@ -113,10 +113,7 @@ interface SnapshotData {
|
||||
/**
|
||||
* Execute a command and return a response
|
||||
*/
|
||||
export async function executeCommand(
|
||||
command: Command,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
export async function executeCommand(command: Command, browser: BrowserManager): Promise<Response> {
|
||||
try {
|
||||
switch (command.action) {
|
||||
case 'launch':
|
||||
@@ -382,10 +379,7 @@ async function handleNavigate(
|
||||
});
|
||||
}
|
||||
|
||||
async function handleClick(
|
||||
command: ClickCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleClick(command: ClickCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
await page.click(command.selector, {
|
||||
button: command.button,
|
||||
@@ -396,10 +390,7 @@ async function handleClick(
|
||||
return successResponse(command.id, { clicked: true });
|
||||
}
|
||||
|
||||
async function handleType(
|
||||
command: TypeCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleType(command: TypeCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
|
||||
if (command.clear) {
|
||||
@@ -413,10 +404,7 @@ async function handleType(
|
||||
return successResponse(command.id, { typed: true });
|
||||
}
|
||||
|
||||
async function handlePress(
|
||||
command: PressCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handlePress(command: PressCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
|
||||
if (command.selector) {
|
||||
@@ -482,10 +470,7 @@ async function handleEvaluate(
|
||||
return successResponse(command.id, { result });
|
||||
}
|
||||
|
||||
async function handleWait(
|
||||
command: WaitCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleWait(command: WaitCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
|
||||
if (command.selector) {
|
||||
@@ -503,10 +488,7 @@ async function handleWait(
|
||||
return successResponse(command.id, { waited: true });
|
||||
}
|
||||
|
||||
async function handleScroll(
|
||||
command: ScrollCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleScroll(command: ScrollCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
|
||||
if (command.selector) {
|
||||
@@ -514,9 +496,12 @@ async function handleScroll(
|
||||
await element.scrollIntoViewIfNeeded();
|
||||
|
||||
if (command.x !== undefined || command.y !== undefined) {
|
||||
await element.evaluate((el, { x, y }) => {
|
||||
el.scrollBy(x ?? 0, y ?? 0);
|
||||
}, { x: command.x, y: command.y });
|
||||
await element.evaluate(
|
||||
(el, { x, y }) => {
|
||||
el.scrollBy(x ?? 0, y ?? 0);
|
||||
},
|
||||
{ x: command.x, y: command.y }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Scroll the page
|
||||
@@ -547,10 +532,7 @@ async function handleScroll(
|
||||
return successResponse(command.id, { scrolled: true });
|
||||
}
|
||||
|
||||
async function handleSelect(
|
||||
command: SelectCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleSelect(command: SelectCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
const values = Array.isArray(command.values) ? command.values : [command.values];
|
||||
|
||||
@@ -559,10 +541,7 @@ async function handleSelect(
|
||||
return successResponse(command.id, { selected: values });
|
||||
}
|
||||
|
||||
async function handleHover(
|
||||
command: HoverCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleHover(command: HoverCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
await page.hover(command.selector);
|
||||
|
||||
@@ -642,37 +621,25 @@ async function handleWindowNew(
|
||||
|
||||
// New handlers for enhanced Playwright parity
|
||||
|
||||
async function handleFill(
|
||||
command: FillCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleFill(command: FillCommand, browser: BrowserManager): Promise<Response> {
|
||||
const frame = browser.getFrame();
|
||||
await frame.fill(command.selector, command.value);
|
||||
return successResponse(command.id, { filled: true });
|
||||
}
|
||||
|
||||
async function handleCheck(
|
||||
command: CheckCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleCheck(command: CheckCommand, browser: BrowserManager): Promise<Response> {
|
||||
const frame = browser.getFrame();
|
||||
await frame.check(command.selector);
|
||||
return successResponse(command.id, { checked: true });
|
||||
}
|
||||
|
||||
async function handleUncheck(
|
||||
command: UncheckCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleUncheck(command: UncheckCommand, browser: BrowserManager): Promise<Response> {
|
||||
const frame = browser.getFrame();
|
||||
await frame.uncheck(command.selector);
|
||||
return successResponse(command.id, { unchecked: true });
|
||||
}
|
||||
|
||||
async function handleUpload(
|
||||
command: UploadCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleUpload(command: UploadCommand, browser: BrowserManager): Promise<Response> {
|
||||
const frame = browser.getFrame();
|
||||
const files = Array.isArray(command.files) ? command.files : [command.files];
|
||||
await frame.setInputFiles(command.selector, files);
|
||||
@@ -688,28 +655,19 @@ async function handleDoubleClick(
|
||||
return successResponse(command.id, { clicked: true });
|
||||
}
|
||||
|
||||
async function handleFocus(
|
||||
command: FocusCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleFocus(command: FocusCommand, browser: BrowserManager): Promise<Response> {
|
||||
const frame = browser.getFrame();
|
||||
await frame.focus(command.selector);
|
||||
return successResponse(command.id, { focused: true });
|
||||
}
|
||||
|
||||
async function handleDrag(
|
||||
command: DragCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleDrag(command: DragCommand, browser: BrowserManager): Promise<Response> {
|
||||
const frame = browser.getFrame();
|
||||
await frame.dragAndDrop(command.source, command.target);
|
||||
return successResponse(command.id, { dragged: true });
|
||||
}
|
||||
|
||||
async function handleFrame(
|
||||
command: FrameCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleFrame(command: FrameCommand, browser: BrowserManager): Promise<Response> {
|
||||
await browser.switchToFrame({
|
||||
selector: command.selector,
|
||||
name: command.name,
|
||||
@@ -841,9 +799,7 @@ async function handleStorageGet(
|
||||
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
||||
|
||||
if (command.key) {
|
||||
const value = await page.evaluate(
|
||||
`${storageType}.getItem(${JSON.stringify(command.key)})`
|
||||
);
|
||||
const value = await page.evaluate(`${storageType}.getItem(${JSON.stringify(command.key)})`);
|
||||
return successResponse(command.id, { key: command.key, value });
|
||||
} else {
|
||||
const data = await page.evaluate(`
|
||||
@@ -885,18 +841,12 @@ async function handleStorageClear(
|
||||
return successResponse(command.id, { cleared: true });
|
||||
}
|
||||
|
||||
async function handleDialog(
|
||||
command: DialogCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleDialog(command: DialogCommand, browser: BrowserManager): Promise<Response> {
|
||||
browser.setDialogHandler(command.response, command.promptText);
|
||||
return successResponse(command.id, { handler: 'set', response: command.response });
|
||||
}
|
||||
|
||||
async function handlePdf(
|
||||
command: PdfCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handlePdf(command: PdfCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
await page.pdf({
|
||||
path: command.path,
|
||||
@@ -907,10 +857,7 @@ async function handlePdf(
|
||||
|
||||
// Network & Request handlers
|
||||
|
||||
async function handleRoute(
|
||||
command: RouteCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleRoute(command: RouteCommand, browser: BrowserManager): Promise<Response> {
|
||||
await browser.addRoute(command.url, {
|
||||
response: command.response,
|
||||
abort: command.abort,
|
||||
@@ -1005,10 +952,7 @@ async function handleUserAgent(
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDevice(
|
||||
command: DeviceCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleDevice(command: DeviceCommand, browser: BrowserManager): Promise<Response> {
|
||||
const device = browser.getDevice(command.device);
|
||||
if (!device) {
|
||||
const available = browser.listDevices().slice(0, 10).join(', ');
|
||||
@@ -1078,10 +1022,7 @@ async function handleGetAttribute(
|
||||
return successResponse(command.id, { attribute: command.attribute, value });
|
||||
}
|
||||
|
||||
async function handleGetText(
|
||||
command: GetTextCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleGetText(command: GetTextCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
const text = await page.textContent(command.selector);
|
||||
return successResponse(command.id, { text });
|
||||
@@ -1114,10 +1055,7 @@ async function handleIsChecked(
|
||||
return successResponse(command.id, { checked });
|
||||
}
|
||||
|
||||
async function handleCount(
|
||||
command: CountCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleCount(command: CountCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
const count = await page.locator(command.selector).count();
|
||||
return successResponse(command.id, { count });
|
||||
@@ -1187,10 +1125,7 @@ async function handleHarStart(
|
||||
return successResponse(command.id, { started: true });
|
||||
}
|
||||
|
||||
async function handleHarStop(
|
||||
command: HarStopCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleHarStop(command: HarStopCommand, browser: BrowserManager): Promise<Response> {
|
||||
// HAR recording is handled at context level
|
||||
// For now, we save tracked requests as a simplified HAR-like format
|
||||
const requests = browser.getRequests();
|
||||
@@ -1219,10 +1154,7 @@ async function handleStateLoad(
|
||||
});
|
||||
}
|
||||
|
||||
async function handleConsole(
|
||||
command: ConsoleCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleConsole(command: ConsoleCommand, browser: BrowserManager): Promise<Response> {
|
||||
if (command.clear) {
|
||||
browser.clearConsoleMessages();
|
||||
return successResponse(command.id, { cleared: true });
|
||||
@@ -1233,10 +1165,7 @@ async function handleConsole(
|
||||
return successResponse(command.id, { messages });
|
||||
}
|
||||
|
||||
async function handleErrors(
|
||||
command: ErrorsCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleErrors(command: ErrorsCommand, browser: BrowserManager): Promise<Response> {
|
||||
if (command.clear) {
|
||||
browser.clearPageErrors();
|
||||
return successResponse(command.id, { cleared: true });
|
||||
@@ -1256,10 +1185,7 @@ async function handleKeyboard(
|
||||
return successResponse(command.id, { pressed: command.keys });
|
||||
}
|
||||
|
||||
async function handleWheel(
|
||||
command: WheelCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleWheel(command: WheelCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
|
||||
if (command.selector) {
|
||||
@@ -1271,10 +1197,7 @@ async function handleWheel(
|
||||
return successResponse(command.id, { scrolled: true });
|
||||
}
|
||||
|
||||
async function handleTap(
|
||||
command: TapCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleTap(command: TapCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
await page.tap(command.selector);
|
||||
return successResponse(command.id, { tapped: true });
|
||||
@@ -1310,10 +1233,7 @@ async function handleHighlight(
|
||||
return successResponse(command.id, { highlighted: true });
|
||||
}
|
||||
|
||||
async function handleClear(
|
||||
command: ClearCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleClear(command: ClearCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
await page.locator(command.selector).clear();
|
||||
return successResponse(command.id, { cleared: true });
|
||||
@@ -1439,18 +1359,12 @@ async function handleEmulateMedia(
|
||||
return successResponse(command.id, { emulated: true });
|
||||
}
|
||||
|
||||
async function handleOffline(
|
||||
command: OfflineCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleOffline(command: OfflineCommand, browser: BrowserManager): Promise<Response> {
|
||||
await browser.setOffline(command.offline);
|
||||
return successResponse(command.id, { offline: command.offline });
|
||||
}
|
||||
|
||||
async function handleHeaders(
|
||||
command: HeadersCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleHeaders(command: HeadersCommand, browser: BrowserManager): Promise<Response> {
|
||||
await browser.setExtraHeaders(command.headers);
|
||||
return successResponse(command.id, { set: true });
|
||||
}
|
||||
@@ -1521,10 +1435,7 @@ async function handleGetByTestId(
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNth(
|
||||
command: NthCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleNth(command: NthCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
const base = page.locator(command.selector);
|
||||
const locator = command.index === -1 ? base.last() : base.nth(command.index);
|
||||
@@ -1589,10 +1500,7 @@ async function handleTimezone(
|
||||
});
|
||||
}
|
||||
|
||||
async function handleLocale(
|
||||
command: LocaleCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleLocale(command: LocaleCommand, browser: BrowserManager): Promise<Response> {
|
||||
// Locale must be set at context creation
|
||||
return successResponse(command.id, {
|
||||
note: 'Locale must be set at browser launch. Use --locale flag.',
|
||||
@@ -1630,10 +1538,7 @@ async function handleMouseDown(
|
||||
return successResponse(command.id, { down: true });
|
||||
}
|
||||
|
||||
async function handleMouseUp(
|
||||
command: MouseUpCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleMouseUp(command: MouseUpCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
await page.mouse.up({ button: command.button ?? 'left' });
|
||||
return successResponse(command.id, { up: true });
|
||||
@@ -1675,19 +1580,13 @@ async function handleAddInitScript(
|
||||
return successResponse(command.id, { added: true });
|
||||
}
|
||||
|
||||
async function handleKeyDown(
|
||||
command: KeyDownCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleKeyDown(command: KeyDownCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
await page.keyboard.down(command.key);
|
||||
return successResponse(command.id, { down: true, key: command.key });
|
||||
}
|
||||
|
||||
async function handleKeyUp(
|
||||
command: KeyUpCommand,
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
async function handleKeyUp(command: KeyUpCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
await page.keyboard.up(command.key);
|
||||
return successResponse(command.id, { up: true, key: command.key });
|
||||
@@ -1723,7 +1622,7 @@ async function handleWaitForDownload(
|
||||
filePath = command.path;
|
||||
await download.saveAs(filePath);
|
||||
} else {
|
||||
filePath = await download.path() || download.suggestedFilename();
|
||||
filePath = (await download.path()) || download.suggestedFilename();
|
||||
}
|
||||
|
||||
return successResponse(command.id, {
|
||||
@@ -1738,10 +1637,9 @@ async function handleResponseBody(
|
||||
browser: BrowserManager
|
||||
): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
const response = await page.waitForResponse(
|
||||
resp => resp.url().includes(command.url),
|
||||
{ timeout: command.timeout }
|
||||
);
|
||||
const response = await page.waitForResponse((resp) => resp.url().includes(command.url), {
|
||||
timeout: command.timeout,
|
||||
});
|
||||
|
||||
const body = await response.text();
|
||||
let parsed: unknown = body;
|
||||
|
||||
+27
-10
@@ -1,4 +1,16 @@
|
||||
import { chromium, firefox, webkit, devices, type Browser, type BrowserContext, type Page, type Frame, type Dialog, type Request, type Route } from 'playwright';
|
||||
import {
|
||||
chromium,
|
||||
firefox,
|
||||
webkit,
|
||||
devices,
|
||||
type Browser,
|
||||
type BrowserContext,
|
||||
type Page,
|
||||
type Frame,
|
||||
type Dialog,
|
||||
type Request,
|
||||
type Route,
|
||||
} from 'playwright';
|
||||
import type { LaunchCommand } from './types.js';
|
||||
|
||||
interface TrackedRequest {
|
||||
@@ -155,7 +167,7 @@ export class BrowserManager {
|
||||
*/
|
||||
getRequests(filter?: string): TrackedRequest[] {
|
||||
if (filter) {
|
||||
return this.trackedRequests.filter(r => r.url.includes(filter));
|
||||
return this.trackedRequests.filter((r) => r.url.includes(filter));
|
||||
}
|
||||
return this.trackedRequests;
|
||||
}
|
||||
@@ -173,7 +185,12 @@ export class BrowserManager {
|
||||
async addRoute(
|
||||
url: string,
|
||||
options: {
|
||||
response?: { status?: number; body?: string; contentType?: string; headers?: Record<string, string> };
|
||||
response?: {
|
||||
status?: number;
|
||||
body?: string;
|
||||
contentType?: string;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
abort?: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
@@ -254,7 +271,7 @@ export class BrowserManager {
|
||||
/**
|
||||
* Get device descriptor
|
||||
*/
|
||||
getDevice(deviceName: string): typeof devices[keyof typeof devices] | undefined {
|
||||
getDevice(deviceName: string): (typeof devices)[keyof typeof devices] | undefined {
|
||||
return devices[deviceName as keyof typeof devices];
|
||||
}
|
||||
|
||||
@@ -420,11 +437,8 @@ export class BrowserManager {
|
||||
|
||||
// Select browser type
|
||||
const browserType = options.browser ?? 'chromium';
|
||||
const launcher = browserType === 'firefox'
|
||||
? firefox
|
||||
: browserType === 'webkit'
|
||||
? webkit
|
||||
: chromium;
|
||||
const launcher =
|
||||
browserType === 'firefox' ? firefox : browserType === 'webkit' ? webkit : chromium;
|
||||
|
||||
// Launch browser
|
||||
this.browser = await launcher.launch({
|
||||
@@ -466,7 +480,10 @@ export class BrowserManager {
|
||||
/**
|
||||
* Create a new window (new context)
|
||||
*/
|
||||
async newWindow(viewport?: { width: number; height: number }): Promise<{ index: number; total: number }> {
|
||||
async newWindow(viewport?: {
|
||||
width: number;
|
||||
height: number;
|
||||
}): Promise<{ index: number; total: number }> {
|
||||
if (!this.browser) {
|
||||
throw new Error('Browser not launched');
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ async function waitForSocket(maxAttempts = 30): Promise<boolean> {
|
||||
debug('Socket found after', i * 100, 'ms');
|
||||
return true;
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
debug('Socket not found after', maxAttempts * 100, 'ms');
|
||||
return false;
|
||||
|
||||
+5
-1
@@ -106,7 +106,11 @@ export async function startDaemon(): Promise<void> {
|
||||
}
|
||||
|
||||
// Auto-launch browser if not already launched and this isn't a launch command
|
||||
if (!browser.isLaunched() && parseResult.command.action !== 'launch' && parseResult.command.action !== 'close') {
|
||||
if (
|
||||
!browser.isLaunched() &&
|
||||
parseResult.command.action !== 'launch' &&
|
||||
parseResult.command.action !== 'close'
|
||||
) {
|
||||
await browser.launch({ id: 'auto', action: 'launch', headless: true });
|
||||
}
|
||||
|
||||
|
||||
+84
-22
@@ -22,7 +22,9 @@ function listSessions(): string[] {
|
||||
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
||||
process.kill(pid, 0);
|
||||
sessions.push(match[1]);
|
||||
} catch { /* Process not running */ }
|
||||
} catch {
|
||||
/* Process not running */
|
||||
}
|
||||
}
|
||||
}
|
||||
return sessions;
|
||||
@@ -178,10 +180,10 @@ function printResponse(response: Response, jsonMode: boolean): void {
|
||||
} else if (data.cookies) {
|
||||
const cookies = data.cookies as Array<{ name: string; value: string }>;
|
||||
if (cookies.length === 0) console.log(c('dim', 'No cookies'));
|
||||
else cookies.forEach(ck => console.log(`${c('cyan', ck.name)}: ${ck.value}`));
|
||||
else cookies.forEach((ck) => console.log(`${c('cyan', ck.name)}: ${ck.value}`));
|
||||
} else if (data.tabs) {
|
||||
const tabs = data.tabs as Array<{ index: number; url: string; title: string; active: boolean }>;
|
||||
tabs.forEach(t => {
|
||||
tabs.forEach((t) => {
|
||||
const marker = t.active ? c('green', '→') : ' ';
|
||||
console.log(`${marker} [${t.index}] ${t.title || c('dim', '(untitled)')}`);
|
||||
if (t.url) console.log(c('dim', ` ${t.url}`));
|
||||
@@ -191,18 +193,19 @@ function printResponse(response: Response, jsonMode: boolean): void {
|
||||
} else if (data.messages) {
|
||||
const msgs = data.messages as Array<{ type: string; text: string }>;
|
||||
if (msgs.length === 0) console.log(c('dim', 'No messages'));
|
||||
else msgs.forEach(m => {
|
||||
const col = m.type === 'error' ? 'red' : m.type === 'warning' ? 'yellow' : 'dim';
|
||||
console.log(`${c(col, `[${m.type}]`)} ${m.text}`);
|
||||
});
|
||||
else
|
||||
msgs.forEach((m) => {
|
||||
const col = m.type === 'error' ? 'red' : m.type === 'warning' ? 'yellow' : 'dim';
|
||||
console.log(`${c(col, `[${m.type}]`)} ${m.text}`);
|
||||
});
|
||||
} else if (data.errors) {
|
||||
const errs = data.errors as Array<{ message: string }>;
|
||||
if (errs.length === 0) console.log(c('dim', 'No errors'));
|
||||
else errs.forEach(e => console.log(c('red', '✗'), e.message));
|
||||
else errs.forEach((e) => console.log(c('red', '✗'), e.message));
|
||||
} else if (data.requests) {
|
||||
const reqs = data.requests as Array<{ method: string; url: string }>;
|
||||
if (reqs.length === 0) console.log(c('dim', 'No requests'));
|
||||
else reqs.forEach(r => console.log(`${c('cyan', r.method)} ${r.url}`));
|
||||
else reqs.forEach((r) => console.log(`${c('cyan', r.method)} ${r.url}`));
|
||||
} else if (data.moved) {
|
||||
console.log(c('green', '✓'), `Moved to (${data.x}, ${data.y})`);
|
||||
} else if (data.body !== undefined && data.status !== undefined) {
|
||||
@@ -225,7 +228,28 @@ function printResponse(response: Response, jsonMode: boolean): void {
|
||||
console.log(c('green', '✓'), 'Browser launched');
|
||||
} else if (data.state) {
|
||||
console.log(c('green', '✓'), `Load state: ${data.state}`);
|
||||
} else if (Object.keys(data).some(k => ['clicked', 'typed', 'filled', 'pressed', 'hovered', 'scrolled', 'selected', 'waited', 'checked', 'unchecked', 'focused', 'set', 'cleared', 'started', 'down', 'up'].includes(k))) {
|
||||
} else if (
|
||||
Object.keys(data).some((k) =>
|
||||
[
|
||||
'clicked',
|
||||
'typed',
|
||||
'filled',
|
||||
'pressed',
|
||||
'hovered',
|
||||
'scrolled',
|
||||
'selected',
|
||||
'waited',
|
||||
'checked',
|
||||
'unchecked',
|
||||
'focused',
|
||||
'set',
|
||||
'cleared',
|
||||
'started',
|
||||
'down',
|
||||
'up',
|
||||
].includes(k)
|
||||
)
|
||||
) {
|
||||
console.log(c('green', '✓'), 'Done');
|
||||
} else {
|
||||
console.log(c('green', '✓'), JSON.stringify(data));
|
||||
@@ -286,7 +310,11 @@ async function handleIs(args: string[], id: string): Promise<Record<string, unkn
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFind(args: string[], id: string, flags: Flags): Promise<Record<string, unknown>> {
|
||||
async function handleFind(
|
||||
args: string[],
|
||||
id: string,
|
||||
flags: Flags
|
||||
): Promise<Record<string, unknown>> {
|
||||
const locator = args[0];
|
||||
const value = args[1];
|
||||
const subaction = args[2] || 'click';
|
||||
@@ -305,7 +333,14 @@ async function handleFind(args: string[], id: string, flags: Flags): Promise<Rec
|
||||
case 'label':
|
||||
return { id, action: 'getbylabel', label: value, subaction, value: fillValue, exact };
|
||||
case 'placeholder':
|
||||
return { id, action: 'getbyplaceholder', placeholder: value, subaction, value: fillValue, exact };
|
||||
return {
|
||||
id,
|
||||
action: 'getbyplaceholder',
|
||||
placeholder: value,
|
||||
subaction,
|
||||
value: fillValue,
|
||||
exact,
|
||||
};
|
||||
case 'alt':
|
||||
return { id, action: 'getbyalttext', text: value, subaction, exact };
|
||||
case 'title':
|
||||
@@ -325,7 +360,9 @@ async function handleFind(args: string[], id: string, flags: Flags): Promise<Rec
|
||||
return { id, action: 'nth', selector: sel, index: idx, subaction: act, value: val };
|
||||
}
|
||||
default:
|
||||
err(`Unknown locator: ${locator}. Options: role, text, label, placeholder, alt, title, testid, first, last, nth`);
|
||||
err(
|
||||
`Unknown locator: ${locator}. Options: role, text, label, placeholder, alt, title, testid, first, last, nth`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,24 +416,40 @@ async function handleSet(args: string[], id: string): Promise<Record<string, unk
|
||||
if (!args[1]) err('Usage: veb set headers <json>');
|
||||
try {
|
||||
return { id, action: 'headers', headers: JSON.parse(args[1]) };
|
||||
} catch { err('Invalid JSON for headers'); }
|
||||
} catch {
|
||||
err('Invalid JSON for headers');
|
||||
}
|
||||
break;
|
||||
case 'credentials':
|
||||
case 'auth':
|
||||
if (!args[1] || !args[2]) err('Usage: veb set credentials <user> <pass>');
|
||||
return { id, action: 'credentials', username: args[1], password: args[2] };
|
||||
case 'media': {
|
||||
const colorScheme = args.includes('dark') ? 'dark' : args.includes('light') ? 'light' : undefined;
|
||||
const media = args.includes('print') ? 'print' : args.includes('screen') ? 'screen' : undefined;
|
||||
const colorScheme = args.includes('dark')
|
||||
? 'dark'
|
||||
: args.includes('light')
|
||||
? 'light'
|
||||
: undefined;
|
||||
const media = args.includes('print')
|
||||
? 'print'
|
||||
: args.includes('screen')
|
||||
? 'screen'
|
||||
: undefined;
|
||||
return { id, action: 'emulatemedia', colorScheme, media };
|
||||
}
|
||||
default:
|
||||
err(`Unknown: veb set ${setting}. Options: viewport, device, geo, offline, headers, credentials, media`);
|
||||
err(
|
||||
`Unknown: veb set ${setting}. Options: viewport, device, geo, offline, headers, credentials, media`
|
||||
);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function handleNetwork(args: string[], id: string, allArgs: string[]): Promise<Record<string, unknown>> {
|
||||
async function handleNetwork(
|
||||
args: string[],
|
||||
id: string,
|
||||
allArgs: string[]
|
||||
): Promise<Record<string, unknown>> {
|
||||
const action = args[0];
|
||||
|
||||
switch (action) {
|
||||
@@ -406,7 +459,13 @@ async function handleNetwork(args: string[], id: string, allArgs: string[]): Pro
|
||||
const abort = allArgs.includes('--abort');
|
||||
const bodyIdx = allArgs.indexOf('--body');
|
||||
const body = bodyIdx !== -1 ? allArgs[bodyIdx + 1] : undefined;
|
||||
return { id, action: 'route', url, abort, response: body ? { body, contentType: 'application/json' } : undefined };
|
||||
return {
|
||||
id,
|
||||
action: 'route',
|
||||
url,
|
||||
abort,
|
||||
response: body ? { body, contentType: 'application/json' } : undefined,
|
||||
};
|
||||
}
|
||||
case 'unroute':
|
||||
return { id, action: 'unroute', url: args[1] };
|
||||
@@ -448,7 +507,9 @@ async function handleCookies(args: string[], id: string): Promise<Record<string,
|
||||
if (!args[1]) err('Usage: veb cookies set <json>');
|
||||
try {
|
||||
return { id, action: 'cookies_set', cookies: JSON.parse(args[1]) };
|
||||
} catch { err('Invalid JSON for cookies'); }
|
||||
} catch {
|
||||
err('Invalid JSON for cookies');
|
||||
}
|
||||
} else if (sub === 'clear') {
|
||||
return { id, action: 'cookies_clear' };
|
||||
} else {
|
||||
@@ -844,7 +905,8 @@ async function main(): Promise<void> {
|
||||
break;
|
||||
|
||||
case 'multiselect':
|
||||
if (!args[0] || args.length < 2) err('Usage: veb multiselect <selector> <value1> [value2...]');
|
||||
if (!args[0] || args.length < 2)
|
||||
err('Usage: veb multiselect <selector> <value1> [value2...]');
|
||||
cmd = { id, action: 'multiselect', selector: args[0], values: args.slice(1) };
|
||||
break;
|
||||
|
||||
@@ -864,7 +926,7 @@ async function main(): Promise<void> {
|
||||
if (sessions.length === 0) {
|
||||
console.log(c('dim', 'No active sessions'));
|
||||
} else {
|
||||
sessions.forEach(s => {
|
||||
sessions.forEach((s) => {
|
||||
const marker = s === current ? c('green', '→') : ' ';
|
||||
console.log(`${marker} ${c('cyan', s)}`);
|
||||
});
|
||||
|
||||
+41
-32
@@ -11,10 +11,12 @@ const baseCommandSchema = z.object({
|
||||
const launchSchema = baseCommandSchema.extend({
|
||||
action: z.literal('launch'),
|
||||
headless: z.boolean().optional(),
|
||||
viewport: z.object({
|
||||
width: z.number().positive(),
|
||||
height: z.number().positive(),
|
||||
}).optional(),
|
||||
viewport: z
|
||||
.object({
|
||||
width: z.number().positive(),
|
||||
height: z.number().positive(),
|
||||
})
|
||||
.optional(),
|
||||
browser: z.enum(['chromium', 'firefox', 'webkit']).optional(),
|
||||
});
|
||||
|
||||
@@ -125,17 +127,19 @@ const cookiesGetSchema = baseCommandSchema.extend({
|
||||
|
||||
const cookiesSetSchema = baseCommandSchema.extend({
|
||||
action: z.literal('cookies_set'),
|
||||
cookies: z.array(z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
url: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
expires: z.number().optional(),
|
||||
httpOnly: z.boolean().optional(),
|
||||
secure: z.boolean().optional(),
|
||||
sameSite: z.enum(['Strict', 'Lax', 'None']).optional(),
|
||||
})),
|
||||
cookies: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
url: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
expires: z.number().optional(),
|
||||
httpOnly: z.boolean().optional(),
|
||||
secure: z.boolean().optional(),
|
||||
sameSite: z.enum(['Strict', 'Lax', 'None']).optional(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const cookiesClearSchema = baseCommandSchema.extend({
|
||||
@@ -169,18 +173,22 @@ const dialogSchema = baseCommandSchema.extend({
|
||||
const pdfSchema = baseCommandSchema.extend({
|
||||
action: z.literal('pdf'),
|
||||
path: z.string().min(1),
|
||||
format: z.enum(['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6']).optional(),
|
||||
format: z
|
||||
.enum(['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6'])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const routeSchema = baseCommandSchema.extend({
|
||||
action: z.literal('route'),
|
||||
url: z.string().min(1),
|
||||
response: z.object({
|
||||
status: z.number().optional(),
|
||||
body: z.string().optional(),
|
||||
contentType: z.string().optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
}).optional(),
|
||||
response: z
|
||||
.object({
|
||||
status: z.number().optional(),
|
||||
body: z.string().optional(),
|
||||
contentType: z.string().optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
abort: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -658,10 +666,12 @@ const tabCloseSchema = baseCommandSchema.extend({
|
||||
|
||||
const windowNewSchema = baseCommandSchema.extend({
|
||||
action: z.literal('window_new'),
|
||||
viewport: z.object({
|
||||
width: z.number().positive(),
|
||||
height: z.number().positive(),
|
||||
}).optional(),
|
||||
viewport: z
|
||||
.object({
|
||||
width: z.number().positive(),
|
||||
height: z.number().positive(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// Union schema for all commands
|
||||
@@ -800,17 +810,16 @@ export function parseCommand(input: string): ParseResult {
|
||||
}
|
||||
|
||||
// Extract id for error responses if possible
|
||||
const id = typeof json === 'object' && json !== null && 'id' in json
|
||||
? String((json as { id: unknown }).id)
|
||||
: undefined;
|
||||
const id =
|
||||
typeof json === 'object' && json !== null && 'id' in json
|
||||
? String((json as { id: unknown }).id)
|
||||
: undefined;
|
||||
|
||||
// Validate against schema
|
||||
const result = commandSchema.safeParse(json);
|
||||
|
||||
if (!result.success) {
|
||||
const errors = result.error.errors
|
||||
.map(e => `${e.path.join('.')}: ${e.message}`)
|
||||
.join(', ');
|
||||
const errors = result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||
return { success: false, error: `Validation error: ${errors}`, id };
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -165,7 +165,18 @@ export interface DialogCommand extends BaseCommand {
|
||||
export interface PdfCommand extends BaseCommand {
|
||||
action: 'pdf';
|
||||
path: string;
|
||||
format?: 'Letter' | 'Legal' | 'Tabloid' | 'Ledger' | 'A0' | 'A1' | 'A2' | 'A3' | 'A4' | 'A5' | 'A6';
|
||||
format?:
|
||||
| 'Letter'
|
||||
| 'Legal'
|
||||
| 'Tabloid'
|
||||
| 'Ledger'
|
||||
| 'A0'
|
||||
| 'A1'
|
||||
| 'A2'
|
||||
| 'A3'
|
||||
| 'A4'
|
||||
| 'A5'
|
||||
| 'A6';
|
||||
}
|
||||
|
||||
// Network interception
|
||||
|
||||
Reference in New Issue
Block a user