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": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/index.js",
|
"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": [
|
"keywords": [
|
||||||
"browser",
|
"browser",
|
||||||
@@ -28,6 +31,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
|
"prettier": "^3.7.4",
|
||||||
"tsx": "^4.6.0",
|
"tsx": "^4.6.0",
|
||||||
"typescript": "^5.3.0"
|
"typescript": "^5.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+114
-216
@@ -113,10 +113,7 @@ interface SnapshotData {
|
|||||||
/**
|
/**
|
||||||
* Execute a command and return a response
|
* Execute a command and return a response
|
||||||
*/
|
*/
|
||||||
export async function executeCommand(
|
export async function executeCommand(command: Command, browser: BrowserManager): Promise<Response> {
|
||||||
command: Command,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
try {
|
try {
|
||||||
switch (command.action) {
|
switch (command.action) {
|
||||||
case 'launch':
|
case 'launch':
|
||||||
@@ -375,56 +372,47 @@ async function handleNavigate(
|
|||||||
await page.goto(command.url, {
|
await page.goto(command.url, {
|
||||||
waitUntil: command.waitUntil ?? 'load',
|
waitUntil: command.waitUntil ?? 'load',
|
||||||
});
|
});
|
||||||
|
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
url: page.url(),
|
url: page.url(),
|
||||||
title: await page.title(),
|
title: await page.title(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClick(
|
async function handleClick(command: ClickCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: ClickCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
await page.click(command.selector, {
|
await page.click(command.selector, {
|
||||||
button: command.button,
|
button: command.button,
|
||||||
clickCount: command.clickCount,
|
clickCount: command.clickCount,
|
||||||
delay: command.delay,
|
delay: command.delay,
|
||||||
});
|
});
|
||||||
|
|
||||||
return successResponse(command.id, { clicked: true });
|
return successResponse(command.id, { clicked: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleType(
|
async function handleType(command: TypeCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: TypeCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
if (command.clear) {
|
if (command.clear) {
|
||||||
await page.fill(command.selector, '');
|
await page.fill(command.selector, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.type(command.selector, command.text, {
|
await page.type(command.selector, command.text, {
|
||||||
delay: command.delay,
|
delay: command.delay,
|
||||||
});
|
});
|
||||||
|
|
||||||
return successResponse(command.id, { typed: true });
|
return successResponse(command.id, { typed: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePress(
|
async function handlePress(command: PressCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: PressCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
if (command.selector) {
|
if (command.selector) {
|
||||||
await page.press(command.selector, command.key);
|
await page.press(command.selector, command.key);
|
||||||
} else {
|
} else {
|
||||||
await page.keyboard.press(command.key);
|
await page.keyboard.press(command.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return successResponse(command.id, { pressed: true });
|
return successResponse(command.id, { pressed: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,21 +421,21 @@ async function handleScreenshot(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response<ScreenshotData>> {
|
): Promise<Response<ScreenshotData>> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
const options: Parameters<Page['screenshot']>[0] = {
|
const options: Parameters<Page['screenshot']>[0] = {
|
||||||
fullPage: command.fullPage,
|
fullPage: command.fullPage,
|
||||||
type: command.format ?? 'png',
|
type: command.format ?? 'png',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (command.format === 'jpeg' && command.quality !== undefined) {
|
if (command.format === 'jpeg' && command.quality !== undefined) {
|
||||||
options.quality = command.quality;
|
options.quality = command.quality;
|
||||||
}
|
}
|
||||||
|
|
||||||
let target: Page | ReturnType<Page['locator']> = page;
|
let target: Page | ReturnType<Page['locator']> = page;
|
||||||
if (command.selector) {
|
if (command.selector) {
|
||||||
target = page.locator(command.selector);
|
target = page.locator(command.selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.path) {
|
if (command.path) {
|
||||||
await target.screenshot({ ...options, path: command.path });
|
await target.screenshot({ ...options, path: command.path });
|
||||||
return successResponse(command.id, { path: command.path });
|
return successResponse(command.id, { path: command.path });
|
||||||
@@ -464,7 +452,7 @@ async function handleSnapshot(
|
|||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
// Use ariaSnapshot which returns a string representation of the accessibility tree
|
// Use ariaSnapshot which returns a string representation of the accessibility tree
|
||||||
const snapshot = await page.locator(':root').ariaSnapshot();
|
const snapshot = await page.locator(':root').ariaSnapshot();
|
||||||
|
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
snapshot: snapshot ?? 'Empty page',
|
snapshot: snapshot ?? 'Empty page',
|
||||||
});
|
});
|
||||||
@@ -475,19 +463,16 @@ async function handleEvaluate(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response<EvaluateData>> {
|
): Promise<Response<EvaluateData>> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
// Evaluate the script directly as a string expression
|
// Evaluate the script directly as a string expression
|
||||||
const result = await page.evaluate(command.script);
|
const result = await page.evaluate(command.script);
|
||||||
|
|
||||||
return successResponse(command.id, { result });
|
return successResponse(command.id, { result });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleWait(
|
async function handleWait(command: WaitCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: WaitCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
if (command.selector) {
|
if (command.selector) {
|
||||||
await page.waitForSelector(command.selector, {
|
await page.waitForSelector(command.selector, {
|
||||||
state: command.state ?? 'visible',
|
state: command.state ?? 'visible',
|
||||||
@@ -499,30 +484,30 @@ async function handleWait(
|
|||||||
// Default: wait for load state
|
// Default: wait for load state
|
||||||
await page.waitForLoadState('load');
|
await page.waitForLoadState('load');
|
||||||
}
|
}
|
||||||
|
|
||||||
return successResponse(command.id, { waited: true });
|
return successResponse(command.id, { waited: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleScroll(
|
async function handleScroll(command: ScrollCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: ScrollCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
if (command.selector) {
|
if (command.selector) {
|
||||||
const element = page.locator(command.selector);
|
const element = page.locator(command.selector);
|
||||||
await element.scrollIntoViewIfNeeded();
|
await element.scrollIntoViewIfNeeded();
|
||||||
|
|
||||||
if (command.x !== undefined || command.y !== undefined) {
|
if (command.x !== undefined || command.y !== undefined) {
|
||||||
await element.evaluate((el, { x, y }) => {
|
await element.evaluate(
|
||||||
el.scrollBy(x ?? 0, y ?? 0);
|
(el, { x, y }) => {
|
||||||
}, { x: command.x, y: command.y });
|
el.scrollBy(x ?? 0, y ?? 0);
|
||||||
|
},
|
||||||
|
{ x: command.x, y: command.y }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Scroll the page
|
// Scroll the page
|
||||||
let deltaX = command.x ?? 0;
|
let deltaX = command.x ?? 0;
|
||||||
let deltaY = command.y ?? 0;
|
let deltaY = command.y ?? 0;
|
||||||
|
|
||||||
if (command.direction) {
|
if (command.direction) {
|
||||||
const amount = command.amount ?? 100;
|
const amount = command.amount ?? 100;
|
||||||
switch (command.direction) {
|
switch (command.direction) {
|
||||||
@@ -540,32 +525,26 @@ async function handleScroll(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.evaluate(`window.scrollBy(${deltaX}, ${deltaY})`);
|
await page.evaluate(`window.scrollBy(${deltaX}, ${deltaY})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return successResponse(command.id, { scrolled: true });
|
return successResponse(command.id, { scrolled: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSelect(
|
async function handleSelect(command: SelectCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: SelectCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const values = Array.isArray(command.values) ? command.values : [command.values];
|
const values = Array.isArray(command.values) ? command.values : [command.values];
|
||||||
|
|
||||||
await page.selectOption(command.selector, values);
|
await page.selectOption(command.selector, values);
|
||||||
|
|
||||||
return successResponse(command.id, { selected: values });
|
return successResponse(command.id, { selected: values });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleHover(
|
async function handleHover(command: HoverCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: HoverCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
await page.hover(command.selector);
|
await page.hover(command.selector);
|
||||||
|
|
||||||
return successResponse(command.id, { hovered: true });
|
return successResponse(command.id, { hovered: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,14 +553,14 @@ async function handleContent(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response<ContentData>> {
|
): Promise<Response<ContentData>> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
let html: string;
|
let html: string;
|
||||||
if (command.selector) {
|
if (command.selector) {
|
||||||
html = await page.locator(command.selector).innerHTML();
|
html = await page.locator(command.selector).innerHTML();
|
||||||
} else {
|
} else {
|
||||||
html = await page.content();
|
html = await page.content();
|
||||||
}
|
}
|
||||||
|
|
||||||
return successResponse(command.id, { html });
|
return successResponse(command.id, { html });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,37 +621,25 @@ async function handleWindowNew(
|
|||||||
|
|
||||||
// New handlers for enhanced Playwright parity
|
// New handlers for enhanced Playwright parity
|
||||||
|
|
||||||
async function handleFill(
|
async function handleFill(command: FillCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: FillCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const frame = browser.getFrame();
|
const frame = browser.getFrame();
|
||||||
await frame.fill(command.selector, command.value);
|
await frame.fill(command.selector, command.value);
|
||||||
return successResponse(command.id, { filled: true });
|
return successResponse(command.id, { filled: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCheck(
|
async function handleCheck(command: CheckCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: CheckCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const frame = browser.getFrame();
|
const frame = browser.getFrame();
|
||||||
await frame.check(command.selector);
|
await frame.check(command.selector);
|
||||||
return successResponse(command.id, { checked: true });
|
return successResponse(command.id, { checked: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUncheck(
|
async function handleUncheck(command: UncheckCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: UncheckCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const frame = browser.getFrame();
|
const frame = browser.getFrame();
|
||||||
await frame.uncheck(command.selector);
|
await frame.uncheck(command.selector);
|
||||||
return successResponse(command.id, { unchecked: true });
|
return successResponse(command.id, { unchecked: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUpload(
|
async function handleUpload(command: UploadCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: UploadCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const frame = browser.getFrame();
|
const frame = browser.getFrame();
|
||||||
const files = Array.isArray(command.files) ? command.files : [command.files];
|
const files = Array.isArray(command.files) ? command.files : [command.files];
|
||||||
await frame.setInputFiles(command.selector, files);
|
await frame.setInputFiles(command.selector, files);
|
||||||
@@ -688,28 +655,19 @@ async function handleDoubleClick(
|
|||||||
return successResponse(command.id, { clicked: true });
|
return successResponse(command.id, { clicked: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFocus(
|
async function handleFocus(command: FocusCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: FocusCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const frame = browser.getFrame();
|
const frame = browser.getFrame();
|
||||||
await frame.focus(command.selector);
|
await frame.focus(command.selector);
|
||||||
return successResponse(command.id, { focused: true });
|
return successResponse(command.id, { focused: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDrag(
|
async function handleDrag(command: DragCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: DragCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const frame = browser.getFrame();
|
const frame = browser.getFrame();
|
||||||
await frame.dragAndDrop(command.source, command.target);
|
await frame.dragAndDrop(command.source, command.target);
|
||||||
return successResponse(command.id, { dragged: true });
|
return successResponse(command.id, { dragged: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFrame(
|
async function handleFrame(command: FrameCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: FrameCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
await browser.switchToFrame({
|
await browser.switchToFrame({
|
||||||
selector: command.selector,
|
selector: command.selector,
|
||||||
name: command.name,
|
name: command.name,
|
||||||
@@ -732,7 +690,7 @@ async function handleGetByRole(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const locator = page.getByRole(command.role as any, { name: command.name });
|
const locator = page.getByRole(command.role as any, { name: command.name });
|
||||||
|
|
||||||
switch (command.subaction) {
|
switch (command.subaction) {
|
||||||
case 'click':
|
case 'click':
|
||||||
await locator.click();
|
await locator.click();
|
||||||
@@ -755,7 +713,7 @@ async function handleGetByText(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const locator = page.getByText(command.text, { exact: command.exact });
|
const locator = page.getByText(command.text, { exact: command.exact });
|
||||||
|
|
||||||
switch (command.subaction) {
|
switch (command.subaction) {
|
||||||
case 'click':
|
case 'click':
|
||||||
await locator.click();
|
await locator.click();
|
||||||
@@ -772,7 +730,7 @@ async function handleGetByLabel(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const locator = page.getByLabel(command.label);
|
const locator = page.getByLabel(command.label);
|
||||||
|
|
||||||
switch (command.subaction) {
|
switch (command.subaction) {
|
||||||
case 'click':
|
case 'click':
|
||||||
await locator.click();
|
await locator.click();
|
||||||
@@ -792,7 +750,7 @@ async function handleGetByPlaceholder(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const locator = page.getByPlaceholder(command.placeholder);
|
const locator = page.getByPlaceholder(command.placeholder);
|
||||||
|
|
||||||
switch (command.subaction) {
|
switch (command.subaction) {
|
||||||
case 'click':
|
case 'click':
|
||||||
await locator.click();
|
await locator.click();
|
||||||
@@ -839,11 +797,9 @@ async function handleStorageGet(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
||||||
|
|
||||||
if (command.key) {
|
if (command.key) {
|
||||||
const value = await page.evaluate(
|
const value = await page.evaluate(`${storageType}.getItem(${JSON.stringify(command.key)})`);
|
||||||
`${storageType}.getItem(${JSON.stringify(command.key)})`
|
|
||||||
);
|
|
||||||
return successResponse(command.id, { key: command.key, value });
|
return successResponse(command.id, { key: command.key, value });
|
||||||
} else {
|
} else {
|
||||||
const data = await page.evaluate(`
|
const data = await page.evaluate(`
|
||||||
@@ -867,7 +823,7 @@ async function handleStorageSet(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
||||||
|
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
`${storageType}.setItem(${JSON.stringify(command.key)}, ${JSON.stringify(command.value)})`
|
`${storageType}.setItem(${JSON.stringify(command.key)}, ${JSON.stringify(command.value)})`
|
||||||
);
|
);
|
||||||
@@ -880,23 +836,17 @@ async function handleStorageClear(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
const storageType = command.type === 'local' ? 'localStorage' : 'sessionStorage';
|
||||||
|
|
||||||
await page.evaluate(`${storageType}.clear()`);
|
await page.evaluate(`${storageType}.clear()`);
|
||||||
return successResponse(command.id, { cleared: true });
|
return successResponse(command.id, { cleared: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDialog(
|
async function handleDialog(command: DialogCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: DialogCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
browser.setDialogHandler(command.response, command.promptText);
|
browser.setDialogHandler(command.response, command.promptText);
|
||||||
return successResponse(command.id, { handler: 'set', response: command.response });
|
return successResponse(command.id, { handler: 'set', response: command.response });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePdf(
|
async function handlePdf(command: PdfCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: PdfCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
await page.pdf({
|
await page.pdf({
|
||||||
path: command.path,
|
path: command.path,
|
||||||
@@ -907,10 +857,7 @@ async function handlePdf(
|
|||||||
|
|
||||||
// Network & Request handlers
|
// Network & Request handlers
|
||||||
|
|
||||||
async function handleRoute(
|
async function handleRoute(command: RouteCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: RouteCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
await browser.addRoute(command.url, {
|
await browser.addRoute(command.url, {
|
||||||
response: command.response,
|
response: command.response,
|
||||||
abort: command.abort,
|
abort: command.abort,
|
||||||
@@ -934,10 +881,10 @@ async function handleRequests(
|
|||||||
browser.clearRequests();
|
browser.clearRequests();
|
||||||
return successResponse(command.id, { cleared: true });
|
return successResponse(command.id, { cleared: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start tracking if not already
|
// Start tracking if not already
|
||||||
browser.startRequestTracking();
|
browser.startRequestTracking();
|
||||||
|
|
||||||
const requests = browser.getRequests(command.filter);
|
const requests = browser.getRequests(command.filter);
|
||||||
return successResponse(command.id, { requests });
|
return successResponse(command.id, { requests });
|
||||||
}
|
}
|
||||||
@@ -947,14 +894,14 @@ async function handleDownload(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
const [download] = await Promise.all([
|
const [download] = await Promise.all([
|
||||||
page.waitForEvent('download'),
|
page.waitForEvent('download'),
|
||||||
page.click(command.selector),
|
page.click(command.selector),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await download.saveAs(command.path);
|
await download.saveAs(command.path);
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
path: command.path,
|
path: command.path,
|
||||||
suggestedFilename: download.suggestedFilename(),
|
suggestedFilename: download.suggestedFilename(),
|
||||||
});
|
});
|
||||||
@@ -965,7 +912,7 @@ async function handleGeolocation(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
await browser.setGeolocation(command.latitude, command.longitude, command.accuracy);
|
await browser.setGeolocation(command.latitude, command.longitude, command.accuracy);
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
latitude: command.latitude,
|
latitude: command.latitude,
|
||||||
longitude: command.longitude,
|
longitude: command.longitude,
|
||||||
});
|
});
|
||||||
@@ -976,7 +923,7 @@ async function handlePermissions(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
await browser.setPermissions(command.permissions, command.grant);
|
await browser.setPermissions(command.permissions, command.grant);
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
permissions: command.permissions,
|
permissions: command.permissions,
|
||||||
granted: command.grant,
|
granted: command.grant,
|
||||||
});
|
});
|
||||||
@@ -987,7 +934,7 @@ async function handleViewport(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
await browser.setViewport(command.width, command.height);
|
await browser.setViewport(command.width, command.height);
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
width: command.width,
|
width: command.width,
|
||||||
height: command.height,
|
height: command.height,
|
||||||
});
|
});
|
||||||
@@ -1000,25 +947,22 @@ async function handleUserAgent(
|
|||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const context = page.context();
|
const context = page.context();
|
||||||
// Note: Can't change user agent after context is created, but we can for new pages
|
// Note: Can't change user agent after context is created, but we can for new pages
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
note: 'User agent can only be set at launch time. Use device command instead.',
|
note: 'User agent can only be set at launch time. Use device command instead.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDevice(
|
async function handleDevice(command: DeviceCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: DeviceCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const device = browser.getDevice(command.device);
|
const device = browser.getDevice(command.device);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
const available = browser.listDevices().slice(0, 10).join(', ');
|
const available = browser.listDevices().slice(0, 10).join(', ');
|
||||||
throw new Error(`Unknown device: ${command.device}. Available: ${available}...`);
|
throw new Error(`Unknown device: ${command.device}. Available: ${available}...`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply device viewport
|
// Apply device viewport
|
||||||
await browser.setViewport(device.viewport.width, device.viewport.height);
|
await browser.setViewport(device.viewport.width, device.viewport.height);
|
||||||
|
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
device: command.device,
|
device: command.device,
|
||||||
viewport: device.viewport,
|
viewport: device.viewport,
|
||||||
userAgent: device.userAgent,
|
userAgent: device.userAgent,
|
||||||
@@ -1078,10 +1022,7 @@ async function handleGetAttribute(
|
|||||||
return successResponse(command.id, { attribute: command.attribute, value });
|
return successResponse(command.id, { attribute: command.attribute, value });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGetText(
|
async function handleGetText(command: GetTextCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: GetTextCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const text = await page.textContent(command.selector);
|
const text = await page.textContent(command.selector);
|
||||||
return successResponse(command.id, { text });
|
return successResponse(command.id, { text });
|
||||||
@@ -1114,10 +1055,7 @@ async function handleIsChecked(
|
|||||||
return successResponse(command.id, { checked });
|
return successResponse(command.id, { checked });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCount(
|
async function handleCount(command: CountCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: CountCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const count = await page.locator(command.selector).count();
|
const count = await page.locator(command.selector).count();
|
||||||
return successResponse(command.id, { count });
|
return successResponse(command.id, { count });
|
||||||
@@ -1140,7 +1078,7 @@ async function handleVideoStart(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
// Video recording requires context-level setup at launch
|
// Video recording requires context-level setup at launch
|
||||||
// For now, return a note about this limitation
|
// For now, return a note about this limitation
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
note: 'Video recording must be enabled at browser launch. Use --video flag when starting.',
|
note: 'Video recording must be enabled at browser launch. Use --video flag when starting.',
|
||||||
path: command.path,
|
path: command.path,
|
||||||
});
|
});
|
||||||
@@ -1187,14 +1125,11 @@ async function handleHarStart(
|
|||||||
return successResponse(command.id, { started: true });
|
return successResponse(command.id, { started: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleHarStop(
|
async function handleHarStop(command: HarStopCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: HarStopCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
// HAR recording is handled at context level
|
// HAR recording is handled at context level
|
||||||
// For now, we save tracked requests as a simplified HAR-like format
|
// For now, we save tracked requests as a simplified HAR-like format
|
||||||
const requests = browser.getRequests();
|
const requests = browser.getRequests();
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
path: command.path,
|
path: command.path,
|
||||||
requestCount: requests.length,
|
requestCount: requests.length,
|
||||||
});
|
});
|
||||||
@@ -1213,35 +1148,29 @@ async function handleStateLoad(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
// Storage state is loaded at context creation
|
// Storage state is loaded at context creation
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
note: 'Storage state must be loaded at browser launch. Use --state flag.',
|
note: 'Storage state must be loaded at browser launch. Use --state flag.',
|
||||||
path: command.path,
|
path: command.path,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleConsole(
|
async function handleConsole(command: ConsoleCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: ConsoleCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
if (command.clear) {
|
if (command.clear) {
|
||||||
browser.clearConsoleMessages();
|
browser.clearConsoleMessages();
|
||||||
return successResponse(command.id, { cleared: true });
|
return successResponse(command.id, { cleared: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.startConsoleTracking();
|
browser.startConsoleTracking();
|
||||||
const messages = browser.getConsoleMessages();
|
const messages = browser.getConsoleMessages();
|
||||||
return successResponse(command.id, { messages });
|
return successResponse(command.id, { messages });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleErrors(
|
async function handleErrors(command: ErrorsCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: ErrorsCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
if (command.clear) {
|
if (command.clear) {
|
||||||
browser.clearPageErrors();
|
browser.clearPageErrors();
|
||||||
return successResponse(command.id, { cleared: true });
|
return successResponse(command.id, { cleared: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.startErrorTracking();
|
browser.startErrorTracking();
|
||||||
const errors = browser.getPageErrors();
|
const errors = browser.getPageErrors();
|
||||||
return successResponse(command.id, { errors });
|
return successResponse(command.id, { errors });
|
||||||
@@ -1256,25 +1185,19 @@ async function handleKeyboard(
|
|||||||
return successResponse(command.id, { pressed: command.keys });
|
return successResponse(command.id, { pressed: command.keys });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleWheel(
|
async function handleWheel(command: WheelCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: WheelCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
if (command.selector) {
|
if (command.selector) {
|
||||||
const element = page.locator(command.selector);
|
const element = page.locator(command.selector);
|
||||||
await element.hover();
|
await element.hover();
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.mouse.wheel(command.deltaX ?? 0, command.deltaY ?? 0);
|
await page.mouse.wheel(command.deltaX ?? 0, command.deltaY ?? 0);
|
||||||
return successResponse(command.id, { scrolled: true });
|
return successResponse(command.id, { scrolled: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleTap(
|
async function handleTap(command: TapCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: TapCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
await page.tap(command.selector);
|
await page.tap(command.selector);
|
||||||
return successResponse(command.id, { tapped: true });
|
return successResponse(command.id, { tapped: true });
|
||||||
@@ -1285,7 +1208,7 @@ async function handleClipboard(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
switch (command.operation) {
|
switch (command.operation) {
|
||||||
case 'copy':
|
case 'copy':
|
||||||
await page.keyboard.press('Control+c');
|
await page.keyboard.press('Control+c');
|
||||||
@@ -1310,10 +1233,7 @@ async function handleHighlight(
|
|||||||
return successResponse(command.id, { highlighted: true });
|
return successResponse(command.id, { highlighted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClear(
|
async function handleClear(command: ClearCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: ClearCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
await page.locator(command.selector).clear();
|
await page.locator(command.selector).clear();
|
||||||
return successResponse(command.id, { cleared: true });
|
return successResponse(command.id, { cleared: true });
|
||||||
@@ -1400,13 +1320,13 @@ async function handleAddScript(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
if (command.content) {
|
if (command.content) {
|
||||||
await page.addScriptTag({ content: command.content });
|
await page.addScriptTag({ content: command.content });
|
||||||
} else if (command.url) {
|
} else if (command.url) {
|
||||||
await page.addScriptTag({ url: command.url });
|
await page.addScriptTag({ url: command.url });
|
||||||
}
|
}
|
||||||
|
|
||||||
return successResponse(command.id, { added: true });
|
return successResponse(command.id, { added: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1415,13 +1335,13 @@ async function handleAddStyle(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
|
|
||||||
if (command.content) {
|
if (command.content) {
|
||||||
await page.addStyleTag({ content: command.content });
|
await page.addStyleTag({ content: command.content });
|
||||||
} else if (command.url) {
|
} else if (command.url) {
|
||||||
await page.addStyleTag({ url: command.url });
|
await page.addStyleTag({ url: command.url });
|
||||||
}
|
}
|
||||||
|
|
||||||
return successResponse(command.id, { added: true });
|
return successResponse(command.id, { added: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1439,18 +1359,12 @@ async function handleEmulateMedia(
|
|||||||
return successResponse(command.id, { emulated: true });
|
return successResponse(command.id, { emulated: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOffline(
|
async function handleOffline(command: OfflineCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: OfflineCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
await browser.setOffline(command.offline);
|
await browser.setOffline(command.offline);
|
||||||
return successResponse(command.id, { offline: command.offline });
|
return successResponse(command.id, { offline: command.offline });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleHeaders(
|
async function handleHeaders(command: HeadersCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: HeadersCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
await browser.setExtraHeaders(command.headers);
|
await browser.setExtraHeaders(command.headers);
|
||||||
return successResponse(command.id, { set: true });
|
return successResponse(command.id, { set: true });
|
||||||
}
|
}
|
||||||
@@ -1470,7 +1384,7 @@ async function handleGetByAltText(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const locator = page.getByAltText(command.text, { exact: command.exact });
|
const locator = page.getByAltText(command.text, { exact: command.exact });
|
||||||
|
|
||||||
switch (command.subaction) {
|
switch (command.subaction) {
|
||||||
case 'click':
|
case 'click':
|
||||||
await locator.click();
|
await locator.click();
|
||||||
@@ -1487,7 +1401,7 @@ async function handleGetByTitle(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const locator = page.getByTitle(command.text, { exact: command.exact });
|
const locator = page.getByTitle(command.text, { exact: command.exact });
|
||||||
|
|
||||||
switch (command.subaction) {
|
switch (command.subaction) {
|
||||||
case 'click':
|
case 'click':
|
||||||
await locator.click();
|
await locator.click();
|
||||||
@@ -1504,7 +1418,7 @@ async function handleGetByTestId(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const locator = page.getByTestId(command.testId);
|
const locator = page.getByTestId(command.testId);
|
||||||
|
|
||||||
switch (command.subaction) {
|
switch (command.subaction) {
|
||||||
case 'click':
|
case 'click':
|
||||||
await locator.click();
|
await locator.click();
|
||||||
@@ -1521,14 +1435,11 @@ async function handleGetByTestId(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleNth(
|
async function handleNth(command: NthCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: NthCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const base = page.locator(command.selector);
|
const base = page.locator(command.selector);
|
||||||
const locator = command.index === -1 ? base.last() : base.nth(command.index);
|
const locator = command.index === -1 ? base.last() : base.nth(command.index);
|
||||||
|
|
||||||
switch (command.subaction) {
|
switch (command.subaction) {
|
||||||
case 'click':
|
case 'click':
|
||||||
await locator.click();
|
await locator.click();
|
||||||
@@ -1583,18 +1494,15 @@ async function handleTimezone(
|
|||||||
// This is a limitation - it sets for the current context
|
// This is a limitation - it sets for the current context
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
await page.context().setGeolocation({ latitude: 0, longitude: 0 }); // Trigger context awareness
|
await page.context().setGeolocation({ latitude: 0, longitude: 0 }); // Trigger context awareness
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
note: 'Timezone must be set at browser launch. Use --timezone flag.',
|
note: 'Timezone must be set at browser launch. Use --timezone flag.',
|
||||||
timezone: command.timezone,
|
timezone: command.timezone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLocale(
|
async function handleLocale(command: LocaleCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: LocaleCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
// Locale must be set at context creation
|
// Locale must be set at context creation
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
note: 'Locale must be set at browser launch. Use --locale flag.',
|
note: 'Locale must be set at browser launch. Use --locale flag.',
|
||||||
locale: command.locale,
|
locale: command.locale,
|
||||||
});
|
});
|
||||||
@@ -1630,10 +1538,7 @@ async function handleMouseDown(
|
|||||||
return successResponse(command.id, { down: true });
|
return successResponse(command.id, { down: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMouseUp(
|
async function handleMouseUp(command: MouseUpCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: MouseUpCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
await page.mouse.up({ button: command.button ?? 'left' });
|
await page.mouse.up({ button: command.button ?? 'left' });
|
||||||
return successResponse(command.id, { up: true });
|
return successResponse(command.id, { up: true });
|
||||||
@@ -1675,19 +1580,13 @@ async function handleAddInitScript(
|
|||||||
return successResponse(command.id, { added: true });
|
return successResponse(command.id, { added: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleKeyDown(
|
async function handleKeyDown(command: KeyDownCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: KeyDownCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
await page.keyboard.down(command.key);
|
await page.keyboard.down(command.key);
|
||||||
return successResponse(command.id, { down: true, key: command.key });
|
return successResponse(command.id, { down: true, key: command.key });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleKeyUp(
|
async function handleKeyUp(command: KeyUpCommand, browser: BrowserManager): Promise<Response> {
|
||||||
command: KeyUpCommand,
|
|
||||||
browser: BrowserManager
|
|
||||||
): Promise<Response> {
|
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
await page.keyboard.up(command.key);
|
await page.keyboard.up(command.key);
|
||||||
return successResponse(command.id, { up: true, key: command.key });
|
return successResponse(command.id, { up: true, key: command.key });
|
||||||
@@ -1717,16 +1616,16 @@ async function handleWaitForDownload(
|
|||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const download = await page.waitForEvent('download', { timeout: command.timeout });
|
const download = await page.waitForEvent('download', { timeout: command.timeout });
|
||||||
|
|
||||||
let filePath: string;
|
let filePath: string;
|
||||||
if (command.path) {
|
if (command.path) {
|
||||||
filePath = command.path;
|
filePath = command.path;
|
||||||
await download.saveAs(filePath);
|
await download.saveAs(filePath);
|
||||||
} else {
|
} else {
|
||||||
filePath = await download.path() || download.suggestedFilename();
|
filePath = (await download.path()) || download.suggestedFilename();
|
||||||
}
|
}
|
||||||
|
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
filename: download.suggestedFilename(),
|
filename: download.suggestedFilename(),
|
||||||
url: download.url(),
|
url: download.url(),
|
||||||
@@ -1738,20 +1637,19 @@ async function handleResponseBody(
|
|||||||
browser: BrowserManager
|
browser: BrowserManager
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
const response = await page.waitForResponse(
|
const response = await page.waitForResponse((resp) => resp.url().includes(command.url), {
|
||||||
resp => resp.url().includes(command.url),
|
timeout: command.timeout,
|
||||||
{ timeout: command.timeout }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const body = await response.text();
|
const body = await response.text();
|
||||||
let parsed: unknown = body;
|
let parsed: unknown = body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(body);
|
parsed = JSON.parse(body);
|
||||||
} catch {
|
} catch {
|
||||||
// Keep as string if not JSON
|
// Keep as string if not JSON
|
||||||
}
|
}
|
||||||
|
|
||||||
return successResponse(command.id, {
|
return successResponse(command.id, {
|
||||||
url: response.url(),
|
url: response.url(),
|
||||||
status: response.status(),
|
status: response.status(),
|
||||||
|
|||||||
+36
-19
@@ -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';
|
import type { LaunchCommand } from './types.js';
|
||||||
|
|
||||||
interface TrackedRequest {
|
interface TrackedRequest {
|
||||||
@@ -68,7 +80,7 @@ export class BrowserManager {
|
|||||||
*/
|
*/
|
||||||
async switchToFrame(options: { selector?: string; name?: string; url?: string }): Promise<void> {
|
async switchToFrame(options: { selector?: string; name?: string; url?: string }): Promise<void> {
|
||||||
const page = this.getPage();
|
const page = this.getPage();
|
||||||
|
|
||||||
if (options.selector) {
|
if (options.selector) {
|
||||||
const frameElement = await page.$(options.selector);
|
const frameElement = await page.$(options.selector);
|
||||||
if (!frameElement) {
|
if (!frameElement) {
|
||||||
@@ -106,12 +118,12 @@ export class BrowserManager {
|
|||||||
*/
|
*/
|
||||||
setDialogHandler(response: 'accept' | 'dismiss', promptText?: string): void {
|
setDialogHandler(response: 'accept' | 'dismiss', promptText?: string): void {
|
||||||
const page = this.getPage();
|
const page = this.getPage();
|
||||||
|
|
||||||
// Remove existing handler if any
|
// Remove existing handler if any
|
||||||
if (this.dialogHandler) {
|
if (this.dialogHandler) {
|
||||||
page.removeListener('dialog', this.dialogHandler);
|
page.removeListener('dialog', this.dialogHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dialogHandler = async (dialog: Dialog) => {
|
this.dialogHandler = async (dialog: Dialog) => {
|
||||||
if (response === 'accept') {
|
if (response === 'accept') {
|
||||||
await dialog.accept(promptText);
|
await dialog.accept(promptText);
|
||||||
@@ -119,7 +131,7 @@ export class BrowserManager {
|
|||||||
await dialog.dismiss();
|
await dialog.dismiss();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
page.on('dialog', this.dialogHandler);
|
page.on('dialog', this.dialogHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +167,7 @@ export class BrowserManager {
|
|||||||
*/
|
*/
|
||||||
getRequests(filter?: string): TrackedRequest[] {
|
getRequests(filter?: string): TrackedRequest[] {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
return this.trackedRequests.filter(r => r.url.includes(filter));
|
return this.trackedRequests.filter((r) => r.url.includes(filter));
|
||||||
}
|
}
|
||||||
return this.trackedRequests;
|
return this.trackedRequests;
|
||||||
}
|
}
|
||||||
@@ -173,12 +185,17 @@ export class BrowserManager {
|
|||||||
async addRoute(
|
async addRoute(
|
||||||
url: string,
|
url: string,
|
||||||
options: {
|
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;
|
abort?: boolean;
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const page = this.getPage();
|
const page = this.getPage();
|
||||||
|
|
||||||
const handler = async (route: Route) => {
|
const handler = async (route: Route) => {
|
||||||
if (options.abort) {
|
if (options.abort) {
|
||||||
await route.abort();
|
await route.abort();
|
||||||
@@ -193,7 +210,7 @@ export class BrowserManager {
|
|||||||
await route.continue();
|
await route.continue();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.routes.set(url, handler);
|
this.routes.set(url, handler);
|
||||||
await page.route(url, handler);
|
await page.route(url, handler);
|
||||||
}
|
}
|
||||||
@@ -203,7 +220,7 @@ export class BrowserManager {
|
|||||||
*/
|
*/
|
||||||
async removeRoute(url?: string): Promise<void> {
|
async removeRoute(url?: string): Promise<void> {
|
||||||
const page = this.getPage();
|
const page = this.getPage();
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
const handler = this.routes.get(url);
|
const handler = this.routes.get(url);
|
||||||
if (handler) {
|
if (handler) {
|
||||||
@@ -254,7 +271,7 @@ export class BrowserManager {
|
|||||||
/**
|
/**
|
||||||
* Get device descriptor
|
* 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];
|
return devices[deviceName as keyof typeof devices];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,11 +437,8 @@ export class BrowserManager {
|
|||||||
|
|
||||||
// Select browser type
|
// Select browser type
|
||||||
const browserType = options.browser ?? 'chromium';
|
const browserType = options.browser ?? 'chromium';
|
||||||
const launcher = browserType === 'firefox'
|
const launcher =
|
||||||
? firefox
|
browserType === 'firefox' ? firefox : browserType === 'webkit' ? webkit : chromium;
|
||||||
: browserType === 'webkit'
|
|
||||||
? webkit
|
|
||||||
: chromium;
|
|
||||||
|
|
||||||
// Launch browser
|
// Launch browser
|
||||||
this.browser = await launcher.launch({
|
this.browser = await launcher.launch({
|
||||||
@@ -435,10 +449,10 @@ export class BrowserManager {
|
|||||||
const context = await this.browser.newContext({
|
const context = await this.browser.newContext({
|
||||||
viewport: options.viewport ?? { width: 1280, height: 720 },
|
viewport: options.viewport ?? { width: 1280, height: 720 },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set default timeout to 10 seconds (Playwright default is 30s)
|
// Set default timeout to 10 seconds (Playwright default is 30s)
|
||||||
context.setDefaultTimeout(10000);
|
context.setDefaultTimeout(10000);
|
||||||
|
|
||||||
this.contexts.push(context);
|
this.contexts.push(context);
|
||||||
|
|
||||||
// Create initial page
|
// Create initial page
|
||||||
@@ -466,7 +480,10 @@ export class BrowserManager {
|
|||||||
/**
|
/**
|
||||||
* Create a new window (new context)
|
* 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) {
|
if (!this.browser) {
|
||||||
throw new Error('Browser not launched');
|
throw new Error('Browser not launched');
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -33,7 +33,7 @@ async function waitForSocket(maxAttempts = 30): Promise<boolean> {
|
|||||||
debug('Socket found after', i * 100, 'ms');
|
debug('Socket found after', i * 100, 'ms');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await new Promise(r => setTimeout(r, 100));
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
}
|
}
|
||||||
debug('Socket not found after', maxAttempts * 100, 'ms');
|
debug('Socket not found after', maxAttempts * 100, 'ms');
|
||||||
return false;
|
return false;
|
||||||
@@ -49,7 +49,7 @@ export async function ensureDaemon(): Promise<void> {
|
|||||||
debug('Daemon already running');
|
debug('Daemon already running');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('Starting daemon...');
|
debug('Starting daemon...');
|
||||||
const daemonPath = path.join(__dirname, 'daemon.js');
|
const daemonPath = path.join(__dirname, 'daemon.js');
|
||||||
const child = spawn(process.execPath, [daemonPath], {
|
const child = spawn(process.execPath, [daemonPath], {
|
||||||
@@ -58,13 +58,13 @@ export async function ensureDaemon(): Promise<void> {
|
|||||||
env: { ...process.env, VEB_DAEMON: '1', VEB_SESSION: session },
|
env: { ...process.env, VEB_DAEMON: '1', VEB_SESSION: session },
|
||||||
});
|
});
|
||||||
child.unref();
|
child.unref();
|
||||||
|
|
||||||
// Wait for socket to be created
|
// Wait for socket to be created
|
||||||
const ready = await waitForSocket();
|
const ready = await waitForSocket();
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
throw new Error('Failed to start daemon');
|
throw new Error('Failed to start daemon');
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(`Daemon started for session "${session}"`);
|
debug(`Daemon started for session "${session}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,23 +74,23 @@ export async function ensureDaemon(): Promise<void> {
|
|||||||
export async function sendCommand(command: Record<string, unknown>): Promise<Response> {
|
export async function sendCommand(command: Record<string, unknown>): Promise<Response> {
|
||||||
const socketPath = getSocketPath();
|
const socketPath = getSocketPath();
|
||||||
debug('Sending command:', JSON.stringify(command));
|
debug('Sending command:', JSON.stringify(command));
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
const socket = net.createConnection(socketPath);
|
const socket = net.createConnection(socketPath);
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
debug('Connected to daemon, sending command...');
|
debug('Connected to daemon, sending command...');
|
||||||
socket.write(JSON.stringify(command) + '\n');
|
socket.write(JSON.stringify(command) + '\n');
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
socket.on('data', (data) => {
|
||||||
buffer += data.toString();
|
buffer += data.toString();
|
||||||
debug('Received data:', buffer.length, 'bytes');
|
debug('Received data:', buffer.length, 'bytes');
|
||||||
|
|
||||||
// Try to parse complete JSON from buffer
|
// Try to parse complete JSON from buffer
|
||||||
const newlineIdx = buffer.indexOf('\n');
|
const newlineIdx = buffer.indexOf('\n');
|
||||||
if (newlineIdx !== -1) {
|
if (newlineIdx !== -1) {
|
||||||
@@ -106,14 +106,14 @@ export async function sendCommand(command: Record<string, unknown>): Promise<Res
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', (err) => {
|
socket.on('error', (err) => {
|
||||||
debug('Socket error:', err.message);
|
debug('Socket error:', err.message);
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
reject(new Error(`Connection error: ${err.message}`));
|
reject(new Error(`Connection error: ${err.message}`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('close', () => {
|
socket.on('close', () => {
|
||||||
debug('Socket closed, resolved:', resolved, 'buffer:', buffer.length);
|
debug('Socket closed, resolved:', resolved, 'buffer:', buffer.length);
|
||||||
if (!resolved && buffer.trim()) {
|
if (!resolved && buffer.trim()) {
|
||||||
@@ -127,7 +127,7 @@ export async function sendCommand(command: Record<string, unknown>): Promise<Res
|
|||||||
reject(new Error('Connection closed without response'));
|
reject(new Error('Connection closed without response'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Timeout after 15 seconds (allows for 10s Playwright timeout + overhead)
|
// Timeout after 15 seconds (allows for 10s Playwright timeout + overhead)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
|
|||||||
+25
-21
@@ -45,7 +45,7 @@ export function getPidFile(session?: string): string {
|
|||||||
export function isDaemonRunning(session?: string): boolean {
|
export function isDaemonRunning(session?: string): boolean {
|
||||||
const pidFile = getPidFile(session);
|
const pidFile = getPidFile(session);
|
||||||
if (!fs.existsSync(pidFile)) return false;
|
if (!fs.existsSync(pidFile)) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
||||||
// Check if process exists
|
// Check if process exists
|
||||||
@@ -78,43 +78,47 @@ export function cleanupSocket(session?: string): void {
|
|||||||
export async function startDaemon(): Promise<void> {
|
export async function startDaemon(): Promise<void> {
|
||||||
// Clean up any stale socket
|
// Clean up any stale socket
|
||||||
cleanupSocket();
|
cleanupSocket();
|
||||||
|
|
||||||
const browser = new BrowserManager();
|
const browser = new BrowserManager();
|
||||||
let shuttingDown = false;
|
let shuttingDown = false;
|
||||||
|
|
||||||
const server = net.createServer((socket) => {
|
const server = net.createServer((socket) => {
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
|
|
||||||
socket.on('data', async (data) => {
|
socket.on('data', async (data) => {
|
||||||
buffer += data.toString();
|
buffer += data.toString();
|
||||||
|
|
||||||
// Process complete lines
|
// Process complete lines
|
||||||
while (buffer.includes('\n')) {
|
while (buffer.includes('\n')) {
|
||||||
const newlineIdx = buffer.indexOf('\n');
|
const newlineIdx = buffer.indexOf('\n');
|
||||||
const line = buffer.substring(0, newlineIdx);
|
const line = buffer.substring(0, newlineIdx);
|
||||||
buffer = buffer.substring(newlineIdx + 1);
|
buffer = buffer.substring(newlineIdx + 1);
|
||||||
|
|
||||||
if (!line.trim()) continue;
|
if (!line.trim()) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parseResult = parseCommand(line);
|
const parseResult = parseCommand(line);
|
||||||
|
|
||||||
if (!parseResult.success) {
|
if (!parseResult.success) {
|
||||||
const resp = errorResponse(parseResult.id ?? 'unknown', parseResult.error);
|
const resp = errorResponse(parseResult.id ?? 'unknown', parseResult.error);
|
||||||
socket.write(serializeResponse(resp) + '\n');
|
socket.write(serializeResponse(resp) + '\n');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-launch browser if not already launched and this isn't a launch command
|
// 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 });
|
await browser.launch({ id: 'auto', action: 'launch', headless: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle close command specially
|
// Handle close command specially
|
||||||
if (parseResult.command.action === 'close') {
|
if (parseResult.command.action === 'close') {
|
||||||
const response = await executeCommand(parseResult.command, browser);
|
const response = await executeCommand(parseResult.command, browser);
|
||||||
socket.write(serializeResponse(response) + '\n');
|
socket.write(serializeResponse(response) + '\n');
|
||||||
|
|
||||||
if (!shuttingDown) {
|
if (!shuttingDown) {
|
||||||
shuttingDown = true;
|
shuttingDown = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -125,7 +129,7 @@ export async function startDaemon(): Promise<void> {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await executeCommand(parseResult.command, browser);
|
const response = await executeCommand(parseResult.command, browser);
|
||||||
socket.write(serializeResponse(response) + '\n');
|
socket.write(serializeResponse(response) + '\n');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -134,28 +138,28 @@ export async function startDaemon(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', () => {
|
socket.on('error', () => {
|
||||||
// Client disconnected, ignore
|
// Client disconnected, ignore
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const socketPath = getSocketPath();
|
const socketPath = getSocketPath();
|
||||||
const pidFile = getPidFile();
|
const pidFile = getPidFile();
|
||||||
|
|
||||||
// Write PID file before listening
|
// Write PID file before listening
|
||||||
fs.writeFileSync(pidFile, process.pid.toString());
|
fs.writeFileSync(pidFile, process.pid.toString());
|
||||||
|
|
||||||
server.listen(socketPath, () => {
|
server.listen(socketPath, () => {
|
||||||
// Daemon is ready
|
// Daemon is ready
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on('error', (err) => {
|
server.on('error', (err) => {
|
||||||
console.error('Server error:', err);
|
console.error('Server error:', err);
|
||||||
cleanupSocket();
|
cleanupSocket();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle shutdown signals
|
// Handle shutdown signals
|
||||||
const shutdown = async () => {
|
const shutdown = async () => {
|
||||||
if (shuttingDown) return;
|
if (shuttingDown) return;
|
||||||
@@ -165,10 +169,10 @@ export async function startDaemon(): Promise<void> {
|
|||||||
cleanupSocket();
|
cleanupSocket();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on('SIGINT', shutdown);
|
process.on('SIGINT', shutdown);
|
||||||
process.on('SIGTERM', shutdown);
|
process.on('SIGTERM', shutdown);
|
||||||
|
|
||||||
// Keep process alive
|
// Keep process alive
|
||||||
process.stdin.resume();
|
process.stdin.resume();
|
||||||
}
|
}
|
||||||
|
|||||||
+162
-100
@@ -22,7 +22,9 @@ function listSessions(): string[] {
|
|||||||
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
||||||
process.kill(pid, 0);
|
process.kill(pid, 0);
|
||||||
sessions.push(match[1]);
|
sessions.push(match[1]);
|
||||||
} catch { /* Process not running */ }
|
} catch {
|
||||||
|
/* Process not running */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sessions;
|
return sessions;
|
||||||
@@ -134,14 +136,14 @@ function printResponse(response: Response, jsonMode: boolean): void {
|
|||||||
console.log(JSON.stringify(response));
|
console.log(JSON.stringify(response));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
console.error(c('red', '✗ Error:'), response.error);
|
console.error(c('red', '✗ Error:'), response.error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = response.data as Record<string, unknown>;
|
const data = response.data as Record<string, unknown>;
|
||||||
|
|
||||||
if (data.url && data.title) {
|
if (data.url && data.title) {
|
||||||
console.log(c('green', '✓'), c('bold', data.title as string));
|
console.log(c('green', '✓'), c('bold', data.title as string));
|
||||||
console.log(c('dim', ` ${data.url}`));
|
console.log(c('dim', ` ${data.url}`));
|
||||||
@@ -178,10 +180,10 @@ function printResponse(response: Response, jsonMode: boolean): void {
|
|||||||
} else if (data.cookies) {
|
} else if (data.cookies) {
|
||||||
const cookies = data.cookies as Array<{ name: string; value: string }>;
|
const cookies = data.cookies as Array<{ name: string; value: string }>;
|
||||||
if (cookies.length === 0) console.log(c('dim', 'No cookies'));
|
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) {
|
} else if (data.tabs) {
|
||||||
const tabs = data.tabs as Array<{ index: number; url: string; title: string; active: boolean }>;
|
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', '→') : ' ';
|
const marker = t.active ? c('green', '→') : ' ';
|
||||||
console.log(`${marker} [${t.index}] ${t.title || c('dim', '(untitled)')}`);
|
console.log(`${marker} [${t.index}] ${t.title || c('dim', '(untitled)')}`);
|
||||||
if (t.url) console.log(c('dim', ` ${t.url}`));
|
if (t.url) console.log(c('dim', ` ${t.url}`));
|
||||||
@@ -191,18 +193,19 @@ function printResponse(response: Response, jsonMode: boolean): void {
|
|||||||
} else if (data.messages) {
|
} else if (data.messages) {
|
||||||
const msgs = data.messages as Array<{ type: string; text: string }>;
|
const msgs = data.messages as Array<{ type: string; text: string }>;
|
||||||
if (msgs.length === 0) console.log(c('dim', 'No messages'));
|
if (msgs.length === 0) console.log(c('dim', 'No messages'));
|
||||||
else msgs.forEach(m => {
|
else
|
||||||
const col = m.type === 'error' ? 'red' : m.type === 'warning' ? 'yellow' : 'dim';
|
msgs.forEach((m) => {
|
||||||
console.log(`${c(col, `[${m.type}]`)} ${m.text}`);
|
const col = m.type === 'error' ? 'red' : m.type === 'warning' ? 'yellow' : 'dim';
|
||||||
});
|
console.log(`${c(col, `[${m.type}]`)} ${m.text}`);
|
||||||
|
});
|
||||||
} else if (data.errors) {
|
} else if (data.errors) {
|
||||||
const errs = data.errors as Array<{ message: string }>;
|
const errs = data.errors as Array<{ message: string }>;
|
||||||
if (errs.length === 0) console.log(c('dim', 'No errors'));
|
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) {
|
} else if (data.requests) {
|
||||||
const reqs = data.requests as Array<{ method: string; url: string }>;
|
const reqs = data.requests as Array<{ method: string; url: string }>;
|
||||||
if (reqs.length === 0) console.log(c('dim', 'No requests'));
|
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) {
|
} else if (data.moved) {
|
||||||
console.log(c('green', '✓'), `Moved to (${data.x}, ${data.y})`);
|
console.log(c('green', '✓'), `Moved to (${data.x}, ${data.y})`);
|
||||||
} else if (data.body !== undefined && data.status !== undefined) {
|
} 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');
|
console.log(c('green', '✓'), 'Browser launched');
|
||||||
} else if (data.state) {
|
} else if (data.state) {
|
||||||
console.log(c('green', '✓'), `Load state: ${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');
|
console.log(c('green', '✓'), 'Done');
|
||||||
} else {
|
} else {
|
||||||
console.log(c('green', '✓'), JSON.stringify(data));
|
console.log(c('green', '✓'), JSON.stringify(data));
|
||||||
@@ -239,7 +263,7 @@ function printResponse(response: Response, jsonMode: boolean): void {
|
|||||||
async function handleGet(args: string[], id: string): Promise<Record<string, unknown>> {
|
async function handleGet(args: string[], id: string): Promise<Record<string, unknown>> {
|
||||||
const what = args[0];
|
const what = args[0];
|
||||||
const selector = args[1];
|
const selector = args[1];
|
||||||
|
|
||||||
switch (what) {
|
switch (what) {
|
||||||
case 'text':
|
case 'text':
|
||||||
if (!selector) err('Selector required: veb get text <selector>');
|
if (!selector) err('Selector required: veb get text <selector>');
|
||||||
@@ -271,9 +295,9 @@ async function handleGet(args: string[], id: string): Promise<Record<string, unk
|
|||||||
async function handleIs(args: string[], id: string): Promise<Record<string, unknown>> {
|
async function handleIs(args: string[], id: string): Promise<Record<string, unknown>> {
|
||||||
const what = args[0];
|
const what = args[0];
|
||||||
const selector = args[1];
|
const selector = args[1];
|
||||||
|
|
||||||
if (!selector) err(`Selector required: veb is ${what} <selector>`);
|
if (!selector) err(`Selector required: veb is ${what} <selector>`);
|
||||||
|
|
||||||
switch (what) {
|
switch (what) {
|
||||||
case 'visible':
|
case 'visible':
|
||||||
return { id, action: 'isvisible', selector };
|
return { id, action: 'isvisible', selector };
|
||||||
@@ -286,17 +310,21 @@ 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 locator = args[0];
|
||||||
const value = args[1];
|
const value = args[1];
|
||||||
const subaction = args[2] || 'click';
|
const subaction = args[2] || 'click';
|
||||||
const fillValue = args[3];
|
const fillValue = args[3];
|
||||||
|
|
||||||
if (!value) err(`Value required: veb find ${locator} <value> <action>`);
|
if (!value) err(`Value required: veb find ${locator} <value> <action>`);
|
||||||
|
|
||||||
const exact = flags.exact;
|
const exact = flags.exact;
|
||||||
const name = flags.name;
|
const name = flags.name;
|
||||||
|
|
||||||
switch (locator) {
|
switch (locator) {
|
||||||
case 'role':
|
case 'role':
|
||||||
return { id, action: 'getbyrole', role: value, subaction, value: fillValue, name, exact };
|
return { id, action: 'getbyrole', role: value, subaction, value: fillValue, name, exact };
|
||||||
@@ -305,7 +333,14 @@ async function handleFind(args: string[], id: string, flags: Flags): Promise<Rec
|
|||||||
case 'label':
|
case 'label':
|
||||||
return { id, action: 'getbylabel', label: value, subaction, value: fillValue, exact };
|
return { id, action: 'getbylabel', label: value, subaction, value: fillValue, exact };
|
||||||
case 'placeholder':
|
case 'placeholder':
|
||||||
return { id, action: 'getbyplaceholder', placeholder: value, subaction, value: fillValue, exact };
|
return {
|
||||||
|
id,
|
||||||
|
action: 'getbyplaceholder',
|
||||||
|
placeholder: value,
|
||||||
|
subaction,
|
||||||
|
value: fillValue,
|
||||||
|
exact,
|
||||||
|
};
|
||||||
case 'alt':
|
case 'alt':
|
||||||
return { id, action: 'getbyalttext', text: value, subaction, exact };
|
return { id, action: 'getbyalttext', text: value, subaction, exact };
|
||||||
case 'title':
|
case 'title':
|
||||||
@@ -325,13 +360,15 @@ async function handleFind(args: string[], id: string, flags: Flags): Promise<Rec
|
|||||||
return { id, action: 'nth', selector: sel, index: idx, subaction: act, value: val };
|
return { id, action: 'nth', selector: sel, index: idx, subaction: act, value: val };
|
||||||
}
|
}
|
||||||
default:
|
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`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMouse(args: string[], id: string): Promise<Record<string, unknown>> {
|
async function handleMouse(args: string[], id: string): Promise<Record<string, unknown>> {
|
||||||
const action = args[0];
|
const action = args[0];
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'move': {
|
case 'move': {
|
||||||
const x = parseInt(args[1], 10);
|
const x = parseInt(args[1], 10);
|
||||||
@@ -355,7 +392,7 @@ async function handleMouse(args: string[], id: string): Promise<Record<string, u
|
|||||||
|
|
||||||
async function handleSet(args: string[], id: string): Promise<Record<string, unknown>> {
|
async function handleSet(args: string[], id: string): Promise<Record<string, unknown>> {
|
||||||
const setting = args[0];
|
const setting = args[0];
|
||||||
|
|
||||||
switch (setting) {
|
switch (setting) {
|
||||||
case 'viewport': {
|
case 'viewport': {
|
||||||
const w = parseInt(args[1], 10);
|
const w = parseInt(args[1], 10);
|
||||||
@@ -379,26 +416,42 @@ async function handleSet(args: string[], id: string): Promise<Record<string, unk
|
|||||||
if (!args[1]) err('Usage: veb set headers <json>');
|
if (!args[1]) err('Usage: veb set headers <json>');
|
||||||
try {
|
try {
|
||||||
return { id, action: 'headers', headers: JSON.parse(args[1]) };
|
return { id, action: 'headers', headers: JSON.parse(args[1]) };
|
||||||
} catch { err('Invalid JSON for headers'); }
|
} catch {
|
||||||
|
err('Invalid JSON for headers');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'credentials':
|
case 'credentials':
|
||||||
case 'auth':
|
case 'auth':
|
||||||
if (!args[1] || !args[2]) err('Usage: veb set credentials <user> <pass>');
|
if (!args[1] || !args[2]) err('Usage: veb set credentials <user> <pass>');
|
||||||
return { id, action: 'credentials', username: args[1], password: args[2] };
|
return { id, action: 'credentials', username: args[1], password: args[2] };
|
||||||
case 'media': {
|
case 'media': {
|
||||||
const colorScheme = args.includes('dark') ? 'dark' : args.includes('light') ? 'light' : undefined;
|
const colorScheme = args.includes('dark')
|
||||||
const media = args.includes('print') ? 'print' : args.includes('screen') ? 'screen' : undefined;
|
? 'dark'
|
||||||
|
: args.includes('light')
|
||||||
|
? 'light'
|
||||||
|
: undefined;
|
||||||
|
const media = args.includes('print')
|
||||||
|
? 'print'
|
||||||
|
: args.includes('screen')
|
||||||
|
? 'screen'
|
||||||
|
: undefined;
|
||||||
return { id, action: 'emulatemedia', colorScheme, media };
|
return { id, action: 'emulatemedia', colorScheme, media };
|
||||||
}
|
}
|
||||||
default:
|
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 {};
|
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];
|
const action = args[0];
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'route': {
|
case 'route': {
|
||||||
const url = args[1];
|
const url = args[1];
|
||||||
@@ -406,7 +459,13 @@ async function handleNetwork(args: string[], id: string, allArgs: string[]): Pro
|
|||||||
const abort = allArgs.includes('--abort');
|
const abort = allArgs.includes('--abort');
|
||||||
const bodyIdx = allArgs.indexOf('--body');
|
const bodyIdx = allArgs.indexOf('--body');
|
||||||
const body = bodyIdx !== -1 ? allArgs[bodyIdx + 1] : undefined;
|
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':
|
case 'unroute':
|
||||||
return { id, action: 'unroute', url: args[1] };
|
return { id, action: 'unroute', url: args[1] };
|
||||||
@@ -425,11 +484,11 @@ async function handleNetwork(args: string[], id: string, allArgs: string[]): Pro
|
|||||||
async function handleStorage(args: string[], id: string): Promise<Record<string, unknown>> {
|
async function handleStorage(args: string[], id: string): Promise<Record<string, unknown>> {
|
||||||
const type = args[0] as 'local' | 'session';
|
const type = args[0] as 'local' | 'session';
|
||||||
const sub = args[1];
|
const sub = args[1];
|
||||||
|
|
||||||
if (type !== 'local' && type !== 'session') {
|
if (type !== 'local' && type !== 'session') {
|
||||||
err('Usage: veb storage <local|session> [get|set|clear] [key] [value]');
|
err('Usage: veb storage <local|session> [get|set|clear] [key] [value]');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'set') {
|
if (sub === 'set') {
|
||||||
if (!args[2] || !args[3]) err(`Usage: veb storage ${type} set <key> <value>`);
|
if (!args[2] || !args[3]) err(`Usage: veb storage ${type} set <key> <value>`);
|
||||||
return { id, action: 'storage_set', type, key: args[2], value: args[3] };
|
return { id, action: 'storage_set', type, key: args[2], value: args[3] };
|
||||||
@@ -443,12 +502,14 @@ async function handleStorage(args: string[], id: string): Promise<Record<string,
|
|||||||
|
|
||||||
async function handleCookies(args: string[], id: string): Promise<Record<string, unknown>> {
|
async function handleCookies(args: string[], id: string): Promise<Record<string, unknown>> {
|
||||||
const sub = args[0];
|
const sub = args[0];
|
||||||
|
|
||||||
if (sub === 'set') {
|
if (sub === 'set') {
|
||||||
if (!args[1]) err('Usage: veb cookies set <json>');
|
if (!args[1]) err('Usage: veb cookies set <json>');
|
||||||
try {
|
try {
|
||||||
return { id, action: 'cookies_set', cookies: JSON.parse(args[1]) };
|
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') {
|
} else if (sub === 'clear') {
|
||||||
return { id, action: 'cookies_clear' };
|
return { id, action: 'cookies_clear' };
|
||||||
} else {
|
} else {
|
||||||
@@ -459,7 +520,7 @@ async function handleCookies(args: string[], id: string): Promise<Record<string,
|
|||||||
|
|
||||||
async function handleTab(args: string[], id: string): Promise<Record<string, unknown>> {
|
async function handleTab(args: string[], id: string): Promise<Record<string, unknown>> {
|
||||||
const sub = args[0];
|
const sub = args[0];
|
||||||
|
|
||||||
if (sub === 'new') {
|
if (sub === 'new') {
|
||||||
return { id, action: 'tab_new' };
|
return { id, action: 'tab_new' };
|
||||||
} else if (sub === 'list' || sub === 'ls' || !sub) {
|
} else if (sub === 'list' || sub === 'ls' || !sub) {
|
||||||
@@ -476,7 +537,7 @@ async function handleTab(args: string[], id: string): Promise<Record<string, unk
|
|||||||
|
|
||||||
async function handleTrace(args: string[], id: string): Promise<Record<string, unknown>> {
|
async function handleTrace(args: string[], id: string): Promise<Record<string, unknown>> {
|
||||||
const sub = args[0];
|
const sub = args[0];
|
||||||
|
|
||||||
if (sub === 'start') {
|
if (sub === 'start') {
|
||||||
return { id, action: 'trace_start', screenshots: true, snapshots: true };
|
return { id, action: 'trace_start', screenshots: true, snapshots: true };
|
||||||
} else if (sub === 'stop') {
|
} else if (sub === 'stop') {
|
||||||
@@ -491,7 +552,7 @@ async function handleTrace(args: string[], id: string): Promise<Record<string, u
|
|||||||
async function handleState(args: string[], id: string): Promise<Record<string, unknown>> {
|
async function handleState(args: string[], id: string): Promise<Record<string, unknown>> {
|
||||||
const sub = args[0];
|
const sub = args[0];
|
||||||
const path = args[1];
|
const path = args[1];
|
||||||
|
|
||||||
if (sub === 'save') {
|
if (sub === 'save') {
|
||||||
if (!path) err('Usage: veb state save <path>');
|
if (!path) err('Usage: veb state save <path>');
|
||||||
return { id, action: 'state_save', path };
|
return { id, action: 'state_save', path };
|
||||||
@@ -531,13 +592,13 @@ function parseFlags(args: string[]): { flags: Flags; cleanArgs: string[] } {
|
|||||||
session: process.env.VEB_SESSION || 'default',
|
session: process.env.VEB_SESSION || 'default',
|
||||||
exact: false,
|
exact: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanArgs: string[] = [];
|
const cleanArgs: string[] = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
while (i < args.length) {
|
while (i < args.length) {
|
||||||
const arg = args[i];
|
const arg = args[i];
|
||||||
|
|
||||||
if (arg === '--json') {
|
if (arg === '--json') {
|
||||||
flags.json = true;
|
flags.json = true;
|
||||||
} else if (arg === '--full' || arg === '-f') {
|
} else if (arg === '--full' || arg === '-f') {
|
||||||
@@ -565,7 +626,7 @@ function parseFlags(args: string[]): { flags: Flags; cleanArgs: string[] } {
|
|||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { flags, cleanArgs };
|
return { flags, cleanArgs };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,21 +637,21 @@ function parseFlags(args: string[]): { flags: Flags; cleanArgs: string[] } {
|
|||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const rawArgs = process.argv.slice(2);
|
const rawArgs = process.argv.slice(2);
|
||||||
const { flags, cleanArgs } = parseFlags(rawArgs);
|
const { flags, cleanArgs } = parseFlags(rawArgs);
|
||||||
|
|
||||||
if (flags.debug) setDebug(true);
|
if (flags.debug) setDebug(true);
|
||||||
setSession(flags.session);
|
setSession(flags.session);
|
||||||
|
|
||||||
if (cleanArgs.length === 0 || rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
if (cleanArgs.length === 0 || rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
||||||
printHelp();
|
printHelp();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = cleanArgs[0];
|
const command = cleanArgs[0];
|
||||||
const args = cleanArgs.slice(1);
|
const args = cleanArgs.slice(1);
|
||||||
const id = genId();
|
const id = genId();
|
||||||
|
|
||||||
let cmd: Record<string, unknown>;
|
let cmd: Record<string, unknown>;
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
// === Core Commands ===
|
// === Core Commands ===
|
||||||
case 'open':
|
case 'open':
|
||||||
@@ -601,85 +662,85 @@ async function main(): Promise<void> {
|
|||||||
cmd = { id, action: 'navigate', url };
|
cmd = { id, action: 'navigate', url };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'click':
|
case 'click':
|
||||||
if (!args[0]) err('Selector required');
|
if (!args[0]) err('Selector required');
|
||||||
cmd = { id, action: 'click', selector: args[0] };
|
cmd = { id, action: 'click', selector: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'dblclick':
|
case 'dblclick':
|
||||||
if (!args[0]) err('Selector required');
|
if (!args[0]) err('Selector required');
|
||||||
cmd = { id, action: 'dblclick', selector: args[0] };
|
cmd = { id, action: 'dblclick', selector: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'type':
|
case 'type':
|
||||||
if (!args[0] || !args[1]) err('Usage: veb type <selector> <text>');
|
if (!args[0] || !args[1]) err('Usage: veb type <selector> <text>');
|
||||||
cmd = { id, action: 'type', selector: args[0], text: args.slice(1).join(' ') };
|
cmd = { id, action: 'type', selector: args[0], text: args.slice(1).join(' ') };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'fill':
|
case 'fill':
|
||||||
if (!args[0] || !args[1]) err('Usage: veb fill <selector> <text>');
|
if (!args[0] || !args[1]) err('Usage: veb fill <selector> <text>');
|
||||||
cmd = { id, action: 'fill', selector: args[0], value: args.slice(1).join(' ') };
|
cmd = { id, action: 'fill', selector: args[0], value: args.slice(1).join(' ') };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'press':
|
case 'press':
|
||||||
case 'key':
|
case 'key':
|
||||||
if (!args[0]) err('Key required');
|
if (!args[0]) err('Key required');
|
||||||
cmd = { id, action: 'press', key: args[0] };
|
cmd = { id, action: 'press', key: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'keydown':
|
case 'keydown':
|
||||||
if (!args[0]) err('Key required');
|
if (!args[0]) err('Key required');
|
||||||
cmd = { id, action: 'keydown', key: args[0] };
|
cmd = { id, action: 'keydown', key: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'keyup':
|
case 'keyup':
|
||||||
if (!args[0]) err('Key required');
|
if (!args[0]) err('Key required');
|
||||||
cmd = { id, action: 'keyup', key: args[0] };
|
cmd = { id, action: 'keyup', key: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'hover':
|
case 'hover':
|
||||||
if (!args[0]) err('Selector required');
|
if (!args[0]) err('Selector required');
|
||||||
cmd = { id, action: 'hover', selector: args[0] };
|
cmd = { id, action: 'hover', selector: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'focus':
|
case 'focus':
|
||||||
if (!args[0]) err('Selector required');
|
if (!args[0]) err('Selector required');
|
||||||
cmd = { id, action: 'focus', selector: args[0] };
|
cmd = { id, action: 'focus', selector: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'check':
|
case 'check':
|
||||||
if (!args[0]) err('Selector required');
|
if (!args[0]) err('Selector required');
|
||||||
cmd = { id, action: 'check', selector: args[0] };
|
cmd = { id, action: 'check', selector: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'uncheck':
|
case 'uncheck':
|
||||||
if (!args[0]) err('Selector required');
|
if (!args[0]) err('Selector required');
|
||||||
cmd = { id, action: 'uncheck', selector: args[0] };
|
cmd = { id, action: 'uncheck', selector: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'select':
|
case 'select':
|
||||||
if (!args[0] || !args[1]) err('Usage: veb select <selector> <value>');
|
if (!args[0] || !args[1]) err('Usage: veb select <selector> <value>');
|
||||||
cmd = { id, action: 'select', selector: args[0], value: args[1] };
|
cmd = { id, action: 'select', selector: args[0], value: args[1] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'drag':
|
case 'drag':
|
||||||
if (!args[0] || !args[1]) err('Usage: veb drag <source> <target>');
|
if (!args[0] || !args[1]) err('Usage: veb drag <source> <target>');
|
||||||
cmd = { id, action: 'drag', source: args[0], target: args[1] };
|
cmd = { id, action: 'drag', source: args[0], target: args[1] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'upload':
|
case 'upload':
|
||||||
if (!args[0] || !args[1]) err('Usage: veb upload <selector> <files...>');
|
if (!args[0] || !args[1]) err('Usage: veb upload <selector> <files...>');
|
||||||
cmd = { id, action: 'upload', selector: args[0], files: args.slice(1) };
|
cmd = { id, action: 'upload', selector: args[0], files: args.slice(1) };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'scroll': {
|
case 'scroll': {
|
||||||
const dir = args[0] || 'down';
|
const dir = args[0] || 'down';
|
||||||
const amount = parseInt(args[1], 10) || 300;
|
const amount = parseInt(args[1], 10) || 300;
|
||||||
cmd = { id, action: 'scroll', direction: dir, amount, selector: flags.selector };
|
cmd = { id, action: 'scroll', direction: dir, amount, selector: flags.selector };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'wait': {
|
case 'wait': {
|
||||||
const target = args[0];
|
const target = args[0];
|
||||||
// Check for flags
|
// Check for flags
|
||||||
@@ -701,83 +762,83 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'screenshot': {
|
case 'screenshot': {
|
||||||
const path = args[0];
|
const path = args[0];
|
||||||
cmd = { id, action: 'screenshot', path, fullPage: flags.full, selector: flags.selector };
|
cmd = { id, action: 'screenshot', path, fullPage: flags.full, selector: flags.selector };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'pdf':
|
case 'pdf':
|
||||||
if (!args[0]) err('Path required');
|
if (!args[0]) err('Path required');
|
||||||
cmd = { id, action: 'pdf', path: args[0] };
|
cmd = { id, action: 'pdf', path: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'snapshot':
|
case 'snapshot':
|
||||||
cmd = { id, action: 'snapshot' };
|
cmd = { id, action: 'snapshot' };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'eval':
|
case 'eval':
|
||||||
if (!args[0]) err('Script required');
|
if (!args[0]) err('Script required');
|
||||||
cmd = { id, action: 'evaluate', script: args.join(' ') };
|
cmd = { id, action: 'evaluate', script: args.join(' ') };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'close':
|
case 'close':
|
||||||
case 'quit':
|
case 'quit':
|
||||||
case 'exit':
|
case 'exit':
|
||||||
cmd = { id, action: 'close' };
|
cmd = { id, action: 'close' };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// === Navigation ===
|
// === Navigation ===
|
||||||
case 'back':
|
case 'back':
|
||||||
cmd = { id, action: 'back' };
|
cmd = { id, action: 'back' };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'forward':
|
case 'forward':
|
||||||
cmd = { id, action: 'forward' };
|
cmd = { id, action: 'forward' };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'reload':
|
case 'reload':
|
||||||
cmd = { id, action: 'reload' };
|
cmd = { id, action: 'reload' };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// === Grouped Commands ===
|
// === Grouped Commands ===
|
||||||
case 'get':
|
case 'get':
|
||||||
cmd = await handleGet(args, id);
|
cmd = await handleGet(args, id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'is':
|
case 'is':
|
||||||
cmd = await handleIs(args, id);
|
cmd = await handleIs(args, id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'find':
|
case 'find':
|
||||||
cmd = await handleFind(args, id, flags);
|
cmd = await handleFind(args, id, flags);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'mouse':
|
case 'mouse':
|
||||||
cmd = await handleMouse(args, id);
|
cmd = await handleMouse(args, id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'set':
|
case 'set':
|
||||||
cmd = await handleSet(args, id);
|
cmd = await handleSet(args, id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'network':
|
case 'network':
|
||||||
cmd = await handleNetwork(args, id, rawArgs);
|
cmd = await handleNetwork(args, id, rawArgs);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'storage':
|
case 'storage':
|
||||||
cmd = await handleStorage(args, id);
|
cmd = await handleStorage(args, id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'cookies':
|
case 'cookies':
|
||||||
cmd = await handleCookies(args, id);
|
cmd = await handleCookies(args, id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'tab':
|
case 'tab':
|
||||||
cmd = await handleTab(args, id);
|
cmd = await handleTab(args, id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'window':
|
case 'window':
|
||||||
if (args[0] === 'new') {
|
if (args[0] === 'new') {
|
||||||
cmd = { id, action: 'window_new' };
|
cmd = { id, action: 'window_new' };
|
||||||
@@ -785,7 +846,7 @@ async function main(): Promise<void> {
|
|||||||
err('Usage: veb window new');
|
err('Usage: veb window new');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'frame':
|
case 'frame':
|
||||||
if (!args[0]) err('Selector required');
|
if (!args[0]) err('Selector required');
|
||||||
if (args[0] === 'main') {
|
if (args[0] === 'main') {
|
||||||
@@ -794,7 +855,7 @@ async function main(): Promise<void> {
|
|||||||
cmd = { id, action: 'frame', selector: args[0] };
|
cmd = { id, action: 'frame', selector: args[0] };
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'dialog':
|
case 'dialog':
|
||||||
if (args[0] === 'accept') {
|
if (args[0] === 'accept') {
|
||||||
cmd = { id, action: 'dialog', response: 'accept', promptText: args[1] };
|
cmd = { id, action: 'dialog', response: 'accept', promptText: args[1] };
|
||||||
@@ -804,59 +865,60 @@ async function main(): Promise<void> {
|
|||||||
err('Usage: veb dialog accept|dismiss');
|
err('Usage: veb dialog accept|dismiss');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'trace':
|
case 'trace':
|
||||||
cmd = await handleTrace(args, id);
|
cmd = await handleTrace(args, id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'state':
|
case 'state':
|
||||||
cmd = await handleState(args, id);
|
cmd = await handleState(args, id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'console':
|
case 'console':
|
||||||
cmd = { id, action: 'console', clear: rawArgs.includes('--clear') };
|
cmd = { id, action: 'console', clear: rawArgs.includes('--clear') };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'errors':
|
case 'errors':
|
||||||
cmd = { id, action: 'errors', clear: rawArgs.includes('--clear') };
|
cmd = { id, action: 'errors', clear: rawArgs.includes('--clear') };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'highlight':
|
case 'highlight':
|
||||||
if (!args[0]) err('Selector required');
|
if (!args[0]) err('Selector required');
|
||||||
cmd = { id, action: 'highlight', selector: args[0] };
|
cmd = { id, action: 'highlight', selector: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'scrollintoview':
|
case 'scrollintoview':
|
||||||
case 'scrollinto':
|
case 'scrollinto':
|
||||||
if (!args[0]) err('Selector required');
|
if (!args[0]) err('Selector required');
|
||||||
cmd = { id, action: 'scrollintoview', selector: args[0] };
|
cmd = { id, action: 'scrollintoview', selector: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'initscript':
|
case 'initscript':
|
||||||
if (!args[0]) err('Script required');
|
if (!args[0]) err('Script required');
|
||||||
cmd = { id, action: 'addinitscript', script: args.join(' ') };
|
cmd = { id, action: 'addinitscript', script: args.join(' ') };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'inserttext':
|
case 'inserttext':
|
||||||
case 'insert':
|
case 'insert':
|
||||||
if (!args[0]) err('Text required');
|
if (!args[0]) err('Text required');
|
||||||
cmd = { id, action: 'inserttext', text: args.join(' ') };
|
cmd = { id, action: 'inserttext', text: args.join(' ') };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'multiselect':
|
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) };
|
cmd = { id, action: 'multiselect', selector: args[0], values: args.slice(1) };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'download':
|
case 'download':
|
||||||
cmd = { id, action: 'waitfordownload', path: args[0] };
|
cmd = { id, action: 'waitfordownload', path: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'response':
|
case 'response':
|
||||||
if (!args[0]) err('URL pattern required');
|
if (!args[0]) err('URL pattern required');
|
||||||
cmd = { id, action: 'responsebody', url: args[0] };
|
cmd = { id, action: 'responsebody', url: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'session':
|
case 'session':
|
||||||
if (args[0] === 'list' || args[0] === 'ls') {
|
if (args[0] === 'list' || args[0] === 'ls') {
|
||||||
const sessions = listSessions();
|
const sessions = listSessions();
|
||||||
@@ -864,7 +926,7 @@ async function main(): Promise<void> {
|
|||||||
if (sessions.length === 0) {
|
if (sessions.length === 0) {
|
||||||
console.log(c('dim', 'No active sessions'));
|
console.log(c('dim', 'No active sessions'));
|
||||||
} else {
|
} else {
|
||||||
sessions.forEach(s => {
|
sessions.forEach((s) => {
|
||||||
const marker = s === current ? c('green', '→') : ' ';
|
const marker = s === current ? c('green', '→') : ' ';
|
||||||
console.log(`${marker} ${c('cyan', s)}`);
|
console.log(`${marker} ${c('cyan', s)}`);
|
||||||
});
|
});
|
||||||
@@ -874,7 +936,7 @@ async function main(): Promise<void> {
|
|||||||
console.log(c('cyan', getSession()));
|
console.log(c('cyan', getSession()));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Legacy aliases for backwards compatibility ===
|
// === Legacy aliases for backwards compatibility ===
|
||||||
case 'url':
|
case 'url':
|
||||||
cmd = { id, action: 'url' };
|
cmd = { id, action: 'url' };
|
||||||
@@ -888,13 +950,13 @@ async function main(): Promise<void> {
|
|||||||
case 'extract':
|
case 'extract':
|
||||||
cmd = { id, action: 'content', selector: args[0] };
|
cmd = { id, action: 'content', selector: args[0] };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.error(c('red', 'Unknown command:'), command);
|
console.error(c('red', 'Unknown command:'), command);
|
||||||
console.error(c('dim', 'Run: veb --help'));
|
console.error(c('dim', 'Run: veb --help'));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await send(cmd);
|
const response = await send(cmd);
|
||||||
printResponse(response, flags.json);
|
printResponse(response, flags.json);
|
||||||
|
|||||||
+43
-34
@@ -11,10 +11,12 @@ const baseCommandSchema = z.object({
|
|||||||
const launchSchema = baseCommandSchema.extend({
|
const launchSchema = baseCommandSchema.extend({
|
||||||
action: z.literal('launch'),
|
action: z.literal('launch'),
|
||||||
headless: z.boolean().optional(),
|
headless: z.boolean().optional(),
|
||||||
viewport: z.object({
|
viewport: z
|
||||||
width: z.number().positive(),
|
.object({
|
||||||
height: z.number().positive(),
|
width: z.number().positive(),
|
||||||
}).optional(),
|
height: z.number().positive(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
browser: z.enum(['chromium', 'firefox', 'webkit']).optional(),
|
browser: z.enum(['chromium', 'firefox', 'webkit']).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,17 +127,19 @@ const cookiesGetSchema = baseCommandSchema.extend({
|
|||||||
|
|
||||||
const cookiesSetSchema = baseCommandSchema.extend({
|
const cookiesSetSchema = baseCommandSchema.extend({
|
||||||
action: z.literal('cookies_set'),
|
action: z.literal('cookies_set'),
|
||||||
cookies: z.array(z.object({
|
cookies: z.array(
|
||||||
name: z.string(),
|
z.object({
|
||||||
value: z.string(),
|
name: z.string(),
|
||||||
url: z.string().optional(),
|
value: z.string(),
|
||||||
domain: z.string().optional(),
|
url: z.string().optional(),
|
||||||
path: z.string().optional(),
|
domain: z.string().optional(),
|
||||||
expires: z.number().optional(),
|
path: z.string().optional(),
|
||||||
httpOnly: z.boolean().optional(),
|
expires: z.number().optional(),
|
||||||
secure: z.boolean().optional(),
|
httpOnly: z.boolean().optional(),
|
||||||
sameSite: z.enum(['Strict', 'Lax', 'None']).optional(),
|
secure: z.boolean().optional(),
|
||||||
})),
|
sameSite: z.enum(['Strict', 'Lax', 'None']).optional(),
|
||||||
|
})
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const cookiesClearSchema = baseCommandSchema.extend({
|
const cookiesClearSchema = baseCommandSchema.extend({
|
||||||
@@ -169,18 +173,22 @@ const dialogSchema = baseCommandSchema.extend({
|
|||||||
const pdfSchema = baseCommandSchema.extend({
|
const pdfSchema = baseCommandSchema.extend({
|
||||||
action: z.literal('pdf'),
|
action: z.literal('pdf'),
|
||||||
path: z.string().min(1),
|
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({
|
const routeSchema = baseCommandSchema.extend({
|
||||||
action: z.literal('route'),
|
action: z.literal('route'),
|
||||||
url: z.string().min(1),
|
url: z.string().min(1),
|
||||||
response: z.object({
|
response: z
|
||||||
status: z.number().optional(),
|
.object({
|
||||||
body: z.string().optional(),
|
status: z.number().optional(),
|
||||||
contentType: z.string().optional(),
|
body: z.string().optional(),
|
||||||
headers: z.record(z.string()).optional(),
|
contentType: z.string().optional(),
|
||||||
}).optional(),
|
headers: z.record(z.string()).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
abort: z.boolean().optional(),
|
abort: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -658,10 +666,12 @@ const tabCloseSchema = baseCommandSchema.extend({
|
|||||||
|
|
||||||
const windowNewSchema = baseCommandSchema.extend({
|
const windowNewSchema = baseCommandSchema.extend({
|
||||||
action: z.literal('window_new'),
|
action: z.literal('window_new'),
|
||||||
viewport: z.object({
|
viewport: z
|
||||||
width: z.number().positive(),
|
.object({
|
||||||
height: z.number().positive(),
|
width: z.number().positive(),
|
||||||
}).optional(),
|
height: z.number().positive(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Union schema for all commands
|
// Union schema for all commands
|
||||||
@@ -783,7 +793,7 @@ const commandSchema = z.discriminatedUnion('action', [
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Parse result type
|
// Parse result type
|
||||||
export type ParseResult =
|
export type ParseResult =
|
||||||
| { success: true; command: Command }
|
| { success: true; command: Command }
|
||||||
| { success: false; error: string; id?: string };
|
| { success: false; error: string; id?: string };
|
||||||
|
|
||||||
@@ -800,17 +810,16 @@ export function parseCommand(input: string): ParseResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract id for error responses if possible
|
// Extract id for error responses if possible
|
||||||
const id = typeof json === 'object' && json !== null && 'id' in json
|
const id =
|
||||||
? String((json as { id: unknown }).id)
|
typeof json === 'object' && json !== null && 'id' in json
|
||||||
: undefined;
|
? String((json as { id: unknown }).id)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Validate against schema
|
// Validate against schema
|
||||||
const result = commandSchema.safeParse(json);
|
const result = commandSchema.safeParse(json);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
const errors = result.error.errors
|
const errors = result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
||||||
.map(e => `${e.path.join('.')}: ${e.message}`)
|
|
||||||
.join(', ');
|
|
||||||
return { success: false, error: `Validation error: ${errors}`, id };
|
return { success: false, error: `Validation error: ${errors}`, id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -165,7 +165,18 @@ export interface DialogCommand extends BaseCommand {
|
|||||||
export interface PdfCommand extends BaseCommand {
|
export interface PdfCommand extends BaseCommand {
|
||||||
action: 'pdf';
|
action: 'pdf';
|
||||||
path: string;
|
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
|
// Network interception
|
||||||
|
|||||||
Reference in New Issue
Block a user