* fix: align CLI command payloads with protocol * fix(cli): support multi-value select in CLI
947 lines
23 KiB
TypeScript
947 lines
23 KiB
TypeScript
import { z } from 'zod';
|
|
import type { Command, Response } from './types.js';
|
|
|
|
// Base schema for all commands
|
|
const baseCommandSchema = z.object({
|
|
id: z.string(),
|
|
action: z.string(),
|
|
});
|
|
|
|
// Individual action schemas
|
|
const launchSchema = baseCommandSchema.extend({
|
|
action: z.literal('launch'),
|
|
headless: z.boolean().optional(),
|
|
viewport: z
|
|
.object({
|
|
width: z.number().positive(),
|
|
height: z.number().positive(),
|
|
})
|
|
.optional(),
|
|
browser: z.enum(['chromium', 'firefox', 'webkit']).optional(),
|
|
cdpPort: z.number().positive().optional(),
|
|
executablePath: z.string().optional(),
|
|
extensions: z.array(z.string()).optional(),
|
|
headers: z.record(z.string()).optional(),
|
|
proxy: z
|
|
.object({
|
|
server: z.string().min(1),
|
|
bypass: z.string().optional(),
|
|
username: z.string().optional(),
|
|
password: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
});
|
|
|
|
const navigateSchema = baseCommandSchema.extend({
|
|
action: z.literal('navigate'),
|
|
url: z.string().min(1),
|
|
waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle']).optional(),
|
|
headers: z.record(z.string()).optional(),
|
|
});
|
|
|
|
const clickSchema = baseCommandSchema.extend({
|
|
action: z.literal('click'),
|
|
selector: z.string().min(1),
|
|
button: z.enum(['left', 'right', 'middle']).optional(),
|
|
clickCount: z.number().positive().optional(),
|
|
delay: z.number().nonnegative().optional(),
|
|
});
|
|
|
|
const typeSchema = baseCommandSchema.extend({
|
|
action: z.literal('type'),
|
|
selector: z.string().min(1),
|
|
text: z.string(),
|
|
delay: z.number().nonnegative().optional(),
|
|
clear: z.boolean().optional(),
|
|
});
|
|
|
|
const fillSchema = baseCommandSchema.extend({
|
|
action: z.literal('fill'),
|
|
selector: z.string().min(1),
|
|
value: z.string(),
|
|
});
|
|
|
|
const checkSchema = baseCommandSchema.extend({
|
|
action: z.literal('check'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const uncheckSchema = baseCommandSchema.extend({
|
|
action: z.literal('uncheck'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const uploadSchema = baseCommandSchema.extend({
|
|
action: z.literal('upload'),
|
|
selector: z.string().min(1),
|
|
files: z.union([z.string(), z.array(z.string())]),
|
|
});
|
|
|
|
const dblclickSchema = baseCommandSchema.extend({
|
|
action: z.literal('dblclick'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const focusSchema = baseCommandSchema.extend({
|
|
action: z.literal('focus'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const dragSchema = baseCommandSchema.extend({
|
|
action: z.literal('drag'),
|
|
source: z.string().min(1),
|
|
target: z.string().min(1),
|
|
});
|
|
|
|
const frameSchema = baseCommandSchema.extend({
|
|
action: z.literal('frame'),
|
|
selector: z.string().min(1).optional(),
|
|
name: z.string().optional(),
|
|
url: z.string().optional(),
|
|
});
|
|
|
|
const mainframeSchema = baseCommandSchema.extend({
|
|
action: z.literal('mainframe'),
|
|
});
|
|
|
|
const getByRoleSchema = baseCommandSchema.extend({
|
|
action: z.literal('getbyrole'),
|
|
role: z.string().min(1),
|
|
name: z.string().optional(),
|
|
subaction: z.enum(['click', 'fill', 'check', 'hover']),
|
|
value: z.string().optional(),
|
|
});
|
|
|
|
const getByTextSchema = baseCommandSchema.extend({
|
|
action: z.literal('getbytext'),
|
|
text: z.string().min(1),
|
|
exact: z.boolean().optional(),
|
|
subaction: z.enum(['click', 'hover']),
|
|
});
|
|
|
|
const getByLabelSchema = baseCommandSchema.extend({
|
|
action: z.literal('getbylabel'),
|
|
label: z.string().min(1),
|
|
subaction: z.enum(['click', 'fill', 'check']),
|
|
value: z.string().optional(),
|
|
});
|
|
|
|
const getByPlaceholderSchema = baseCommandSchema.extend({
|
|
action: z.literal('getbyplaceholder'),
|
|
placeholder: z.string().min(1),
|
|
subaction: z.enum(['click', 'fill']),
|
|
value: z.string().optional(),
|
|
});
|
|
|
|
const cookiesGetSchema = baseCommandSchema.extend({
|
|
action: z.literal('cookies_get'),
|
|
urls: z.array(z.string()).optional(),
|
|
});
|
|
|
|
const cookiesSetSchema = baseCommandSchema.extend({
|
|
action: z.literal('cookies_set'),
|
|
cookies: z.array(
|
|
z.object({
|
|
name: z.string(),
|
|
value: z.string(),
|
|
url: z.string().optional(),
|
|
domain: z.string().optional(),
|
|
path: z.string().optional(),
|
|
expires: z.number().optional(),
|
|
httpOnly: z.boolean().optional(),
|
|
secure: z.boolean().optional(),
|
|
sameSite: z.enum(['Strict', 'Lax', 'None']).optional(),
|
|
})
|
|
),
|
|
});
|
|
|
|
const cookiesClearSchema = baseCommandSchema.extend({
|
|
action: z.literal('cookies_clear'),
|
|
});
|
|
|
|
const storageGetSchema = baseCommandSchema.extend({
|
|
action: z.literal('storage_get'),
|
|
key: z.string().optional(),
|
|
type: z.enum(['local', 'session']),
|
|
});
|
|
|
|
const storageSetSchema = baseCommandSchema.extend({
|
|
action: z.literal('storage_set'),
|
|
key: z.string().min(1),
|
|
value: z.string(),
|
|
type: z.enum(['local', 'session']),
|
|
});
|
|
|
|
const storageClearSchema = baseCommandSchema.extend({
|
|
action: z.literal('storage_clear'),
|
|
type: z.enum(['local', 'session']),
|
|
});
|
|
|
|
const dialogSchema = baseCommandSchema.extend({
|
|
action: z.literal('dialog'),
|
|
response: z.enum(['accept', 'dismiss']),
|
|
promptText: z.string().optional(),
|
|
});
|
|
|
|
const pdfSchema = baseCommandSchema.extend({
|
|
action: z.literal('pdf'),
|
|
path: z.string().min(1),
|
|
format: z
|
|
.enum(['Letter', 'Legal', 'Tabloid', 'Ledger', 'A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6'])
|
|
.optional(),
|
|
});
|
|
|
|
const routeSchema = baseCommandSchema.extend({
|
|
action: z.literal('route'),
|
|
url: z.string().min(1),
|
|
response: z
|
|
.object({
|
|
status: z.number().optional(),
|
|
body: z.string().optional(),
|
|
contentType: z.string().optional(),
|
|
headers: z.record(z.string()).optional(),
|
|
})
|
|
.optional(),
|
|
abort: z.boolean().optional(),
|
|
});
|
|
|
|
const unrouteSchema = baseCommandSchema.extend({
|
|
action: z.literal('unroute'),
|
|
url: z.string().optional(),
|
|
});
|
|
|
|
const requestsSchema = baseCommandSchema.extend({
|
|
action: z.literal('requests'),
|
|
filter: z.string().optional(),
|
|
clear: z.boolean().optional(),
|
|
});
|
|
|
|
const downloadSchema = baseCommandSchema.extend({
|
|
action: z.literal('download'),
|
|
selector: z.string().min(1),
|
|
path: z.string().min(1),
|
|
});
|
|
|
|
const geolocationSchema = baseCommandSchema.extend({
|
|
action: z.literal('geolocation'),
|
|
latitude: z.number(),
|
|
longitude: z.number(),
|
|
accuracy: z.number().optional(),
|
|
});
|
|
|
|
const permissionsSchema = baseCommandSchema.extend({
|
|
action: z.literal('permissions'),
|
|
permissions: z.array(z.string()),
|
|
grant: z.boolean(),
|
|
});
|
|
|
|
const viewportSchema = baseCommandSchema.extend({
|
|
action: z.literal('viewport'),
|
|
width: z.number().positive(),
|
|
height: z.number().positive(),
|
|
});
|
|
|
|
const userAgentSchema = baseCommandSchema.extend({
|
|
action: z.literal('useragent'),
|
|
userAgent: z.string().min(1),
|
|
});
|
|
|
|
const deviceSchema = baseCommandSchema.extend({
|
|
action: z.literal('device'),
|
|
device: z.string().min(1),
|
|
});
|
|
|
|
const backSchema = baseCommandSchema.extend({
|
|
action: z.literal('back'),
|
|
});
|
|
|
|
const forwardSchema = baseCommandSchema.extend({
|
|
action: z.literal('forward'),
|
|
});
|
|
|
|
const reloadSchema = baseCommandSchema.extend({
|
|
action: z.literal('reload'),
|
|
});
|
|
|
|
const urlSchema = baseCommandSchema.extend({
|
|
action: z.literal('url'),
|
|
});
|
|
|
|
const titleSchema = baseCommandSchema.extend({
|
|
action: z.literal('title'),
|
|
});
|
|
|
|
const getAttributeSchema = baseCommandSchema.extend({
|
|
action: z.literal('getattribute'),
|
|
selector: z.string().min(1),
|
|
attribute: z.string().min(1),
|
|
});
|
|
|
|
const getTextSchema = baseCommandSchema.extend({
|
|
action: z.literal('gettext'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const isVisibleSchema = baseCommandSchema.extend({
|
|
action: z.literal('isvisible'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const isEnabledSchema = baseCommandSchema.extend({
|
|
action: z.literal('isenabled'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const isCheckedSchema = baseCommandSchema.extend({
|
|
action: z.literal('ischecked'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const countSchema = baseCommandSchema.extend({
|
|
action: z.literal('count'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const boundingBoxSchema = baseCommandSchema.extend({
|
|
action: z.literal('boundingbox'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const stylesSchema = baseCommandSchema.extend({
|
|
action: z.literal('styles'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const videoStartSchema = baseCommandSchema.extend({
|
|
action: z.literal('video_start'),
|
|
path: z.string().min(1),
|
|
});
|
|
|
|
const videoStopSchema = baseCommandSchema.extend({
|
|
action: z.literal('video_stop'),
|
|
});
|
|
|
|
// Recording schemas (Playwright native video recording)
|
|
const recordingStartSchema = baseCommandSchema.extend({
|
|
action: z.literal('recording_start'),
|
|
path: z.string().min(1),
|
|
url: z.string().min(1).optional(),
|
|
});
|
|
|
|
const recordingStopSchema = baseCommandSchema.extend({
|
|
action: z.literal('recording_stop'),
|
|
});
|
|
|
|
const recordingRestartSchema = baseCommandSchema.extend({
|
|
action: z.literal('recording_restart'),
|
|
path: z.string().min(1),
|
|
url: z.string().min(1).optional(),
|
|
});
|
|
|
|
const traceStartSchema = baseCommandSchema.extend({
|
|
action: z.literal('trace_start'),
|
|
screenshots: z.boolean().optional(),
|
|
snapshots: z.boolean().optional(),
|
|
});
|
|
|
|
const traceStopSchema = baseCommandSchema.extend({
|
|
action: z.literal('trace_stop'),
|
|
path: z.string().min(1),
|
|
});
|
|
|
|
const harStartSchema = baseCommandSchema.extend({
|
|
action: z.literal('har_start'),
|
|
});
|
|
|
|
const harStopSchema = baseCommandSchema.extend({
|
|
action: z.literal('har_stop'),
|
|
path: z.string().min(1),
|
|
});
|
|
|
|
const stateSaveSchema = baseCommandSchema.extend({
|
|
action: z.literal('state_save'),
|
|
path: z.string().min(1),
|
|
});
|
|
|
|
const stateLoadSchema = baseCommandSchema.extend({
|
|
action: z.literal('state_load'),
|
|
path: z.string().min(1),
|
|
});
|
|
|
|
const consoleSchema = baseCommandSchema.extend({
|
|
action: z.literal('console'),
|
|
clear: z.boolean().optional(),
|
|
});
|
|
|
|
const errorsSchema = baseCommandSchema.extend({
|
|
action: z.literal('errors'),
|
|
clear: z.boolean().optional(),
|
|
});
|
|
|
|
const keyboardSchema = baseCommandSchema.extend({
|
|
action: z.literal('keyboard'),
|
|
keys: z.string().min(1),
|
|
});
|
|
|
|
const wheelSchema = baseCommandSchema.extend({
|
|
action: z.literal('wheel'),
|
|
deltaX: z.number().optional(),
|
|
deltaY: z.number().optional(),
|
|
selector: z.string().optional(),
|
|
});
|
|
|
|
const tapSchema = baseCommandSchema.extend({
|
|
action: z.literal('tap'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const clipboardSchema = baseCommandSchema.extend({
|
|
action: z.literal('clipboard'),
|
|
operation: z.enum(['copy', 'paste', 'read']),
|
|
text: z.string().optional(),
|
|
});
|
|
|
|
const highlightSchema = baseCommandSchema.extend({
|
|
action: z.literal('highlight'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const clearSchema = baseCommandSchema.extend({
|
|
action: z.literal('clear'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const selectAllSchema = baseCommandSchema.extend({
|
|
action: z.literal('selectall'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const innerTextSchema = baseCommandSchema.extend({
|
|
action: z.literal('innertext'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const innerHtmlSchema = baseCommandSchema.extend({
|
|
action: z.literal('innerhtml'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const inputValueSchema = baseCommandSchema.extend({
|
|
action: z.literal('inputvalue'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const setValueSchema = baseCommandSchema.extend({
|
|
action: z.literal('setvalue'),
|
|
selector: z.string().min(1),
|
|
value: z.string(),
|
|
});
|
|
|
|
const dispatchSchema = baseCommandSchema.extend({
|
|
action: z.literal('dispatch'),
|
|
selector: z.string().min(1),
|
|
event: z.string().min(1),
|
|
eventInit: z.record(z.unknown()).optional(),
|
|
});
|
|
|
|
const evalHandleSchema = baseCommandSchema.extend({
|
|
action: z.literal('evalhandle'),
|
|
script: z.string().min(1),
|
|
});
|
|
|
|
const exposeSchema = baseCommandSchema.extend({
|
|
action: z.literal('expose'),
|
|
name: z.string().min(1),
|
|
});
|
|
|
|
const addScriptSchema = baseCommandSchema.extend({
|
|
action: z.literal('addscript'),
|
|
content: z.string().optional(),
|
|
url: z.string().optional(),
|
|
});
|
|
|
|
const addStyleSchema = baseCommandSchema.extend({
|
|
action: z.literal('addstyle'),
|
|
content: z.string().optional(),
|
|
url: z.string().optional(),
|
|
});
|
|
|
|
const emulateMediaSchema = baseCommandSchema.extend({
|
|
action: z.literal('emulatemedia'),
|
|
media: z.enum(['screen', 'print']).nullable().optional(),
|
|
colorScheme: z.enum(['light', 'dark', 'no-preference']).nullable().optional(),
|
|
reducedMotion: z.enum(['reduce', 'no-preference']).nullable().optional(),
|
|
forcedColors: z.enum(['active', 'none']).nullable().optional(),
|
|
});
|
|
|
|
const offlineSchema = baseCommandSchema.extend({
|
|
action: z.literal('offline'),
|
|
offline: z.boolean(),
|
|
});
|
|
|
|
const headersSchema = baseCommandSchema.extend({
|
|
action: z.literal('headers'),
|
|
headers: z.record(z.string()),
|
|
});
|
|
|
|
const pauseSchema = baseCommandSchema.extend({
|
|
action: z.literal('pause'),
|
|
});
|
|
|
|
const getByAltTextSchema = baseCommandSchema.extend({
|
|
action: z.literal('getbyalttext'),
|
|
text: z.string().min(1),
|
|
exact: z.boolean().optional(),
|
|
subaction: z.enum(['click', 'hover']),
|
|
});
|
|
|
|
const getByTitleSchema = baseCommandSchema.extend({
|
|
action: z.literal('getbytitle'),
|
|
text: z.string().min(1),
|
|
exact: z.boolean().optional(),
|
|
subaction: z.enum(['click', 'hover']),
|
|
});
|
|
|
|
const getByTestIdSchema = baseCommandSchema.extend({
|
|
action: z.literal('getbytestid'),
|
|
testId: z.string().min(1),
|
|
subaction: z.enum(['click', 'fill', 'check', 'hover']),
|
|
value: z.string().optional(),
|
|
});
|
|
|
|
const nthSchema = baseCommandSchema.extend({
|
|
action: z.literal('nth'),
|
|
selector: z.string().min(1),
|
|
index: z.number(),
|
|
subaction: z.enum(['click', 'fill', 'check', 'hover', 'text']),
|
|
value: z.string().optional(),
|
|
});
|
|
|
|
const waitForUrlSchema = baseCommandSchema.extend({
|
|
action: z.literal('waitforurl'),
|
|
url: z.string().min(1),
|
|
timeout: z.number().positive().optional(),
|
|
});
|
|
|
|
const waitForLoadStateSchema = baseCommandSchema.extend({
|
|
action: z.literal('waitforloadstate'),
|
|
state: z.enum(['load', 'domcontentloaded', 'networkidle']),
|
|
timeout: z.number().positive().optional(),
|
|
});
|
|
|
|
const setContentSchema = baseCommandSchema.extend({
|
|
action: z.literal('setcontent'),
|
|
html: z.string(),
|
|
});
|
|
|
|
const timezoneSchema = baseCommandSchema.extend({
|
|
action: z.literal('timezone'),
|
|
timezone: z.string().min(1),
|
|
});
|
|
|
|
const localeSchema = baseCommandSchema.extend({
|
|
action: z.literal('locale'),
|
|
locale: z.string().min(1),
|
|
});
|
|
|
|
const credentialsSchema = baseCommandSchema.extend({
|
|
action: z.literal('credentials'),
|
|
username: z.string(),
|
|
password: z.string(),
|
|
});
|
|
|
|
const mouseMoveSchema = baseCommandSchema.extend({
|
|
action: z.literal('mousemove'),
|
|
x: z.number(),
|
|
y: z.number(),
|
|
});
|
|
|
|
const mouseDownSchema = baseCommandSchema.extend({
|
|
action: z.literal('mousedown'),
|
|
button: z.enum(['left', 'right', 'middle']).optional(),
|
|
});
|
|
|
|
const mouseUpSchema = baseCommandSchema.extend({
|
|
action: z.literal('mouseup'),
|
|
button: z.enum(['left', 'right', 'middle']).optional(),
|
|
});
|
|
|
|
const bringToFrontSchema = baseCommandSchema.extend({
|
|
action: z.literal('bringtofront'),
|
|
});
|
|
|
|
const waitForFunctionSchema = baseCommandSchema.extend({
|
|
action: z.literal('waitforfunction'),
|
|
expression: z.string().min(1),
|
|
timeout: z.number().positive().optional(),
|
|
});
|
|
|
|
const scrollIntoViewSchema = baseCommandSchema.extend({
|
|
action: z.literal('scrollintoview'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const addInitScriptSchema = baseCommandSchema.extend({
|
|
action: z.literal('addinitscript'),
|
|
script: z.string().min(1),
|
|
});
|
|
|
|
const keyDownSchema = baseCommandSchema.extend({
|
|
action: z.literal('keydown'),
|
|
key: z.string().min(1),
|
|
});
|
|
|
|
const keyUpSchema = baseCommandSchema.extend({
|
|
action: z.literal('keyup'),
|
|
key: z.string().min(1),
|
|
});
|
|
|
|
const insertTextSchema = baseCommandSchema.extend({
|
|
action: z.literal('inserttext'),
|
|
text: z.string(),
|
|
});
|
|
|
|
const multiSelectSchema = baseCommandSchema.extend({
|
|
action: z.literal('multiselect'),
|
|
selector: z.string().min(1),
|
|
values: z.array(z.string()),
|
|
});
|
|
|
|
const waitForDownloadSchema = baseCommandSchema.extend({
|
|
action: z.literal('waitfordownload'),
|
|
path: z.string().optional(),
|
|
timeout: z.number().positive().optional(),
|
|
});
|
|
|
|
const responseBodySchema = baseCommandSchema.extend({
|
|
action: z.literal('responsebody'),
|
|
url: z.string().min(1),
|
|
timeout: z.number().positive().optional(),
|
|
});
|
|
|
|
// Screencast schemas for streaming browser viewport
|
|
const screencastStartSchema = baseCommandSchema.extend({
|
|
action: z.literal('screencast_start'),
|
|
format: z.enum(['jpeg', 'png']).optional(),
|
|
quality: z.number().min(0).max(100).optional(),
|
|
maxWidth: z.number().positive().optional(),
|
|
maxHeight: z.number().positive().optional(),
|
|
everyNthFrame: z.number().positive().optional(),
|
|
});
|
|
|
|
const screencastStopSchema = baseCommandSchema.extend({
|
|
action: z.literal('screencast_stop'),
|
|
});
|
|
|
|
// Input injection schemas for pair browsing
|
|
const inputMouseSchema = baseCommandSchema.extend({
|
|
action: z.literal('input_mouse'),
|
|
type: z.enum(['mousePressed', 'mouseReleased', 'mouseMoved', 'mouseWheel']),
|
|
x: z.number(),
|
|
y: z.number(),
|
|
button: z.enum(['left', 'right', 'middle', 'none']).optional(),
|
|
clickCount: z.number().positive().optional(),
|
|
deltaX: z.number().optional(),
|
|
deltaY: z.number().optional(),
|
|
modifiers: z.number().optional(),
|
|
});
|
|
|
|
const inputKeyboardSchema = baseCommandSchema.extend({
|
|
action: z.literal('input_keyboard'),
|
|
type: z.enum(['keyDown', 'keyUp', 'char']),
|
|
key: z.string().optional(),
|
|
code: z.string().optional(),
|
|
text: z.string().optional(),
|
|
modifiers: z.number().optional(),
|
|
});
|
|
|
|
const inputTouchSchema = baseCommandSchema.extend({
|
|
action: z.literal('input_touch'),
|
|
type: z.enum(['touchStart', 'touchEnd', 'touchMove', 'touchCancel']),
|
|
touchPoints: z.array(
|
|
z.object({
|
|
x: z.number(),
|
|
y: z.number(),
|
|
id: z.number().optional(),
|
|
})
|
|
),
|
|
modifiers: z.number().optional(),
|
|
});
|
|
|
|
const pressSchema = baseCommandSchema.extend({
|
|
action: z.literal('press'),
|
|
key: z.string().min(1),
|
|
selector: z.string().min(1).optional(),
|
|
});
|
|
|
|
const screenshotSchema = baseCommandSchema.extend({
|
|
action: z.literal('screenshot'),
|
|
path: z.string().nullable().optional(),
|
|
fullPage: z.boolean().optional(),
|
|
selector: z.string().min(1).optional(),
|
|
format: z.enum(['png', 'jpeg']).optional(),
|
|
quality: z.number().min(0).max(100).optional(),
|
|
});
|
|
|
|
const snapshotSchema = baseCommandSchema.extend({
|
|
action: z.literal('snapshot'),
|
|
interactive: z.boolean().optional(),
|
|
maxDepth: z.number().nonnegative().optional(),
|
|
compact: z.boolean().optional(),
|
|
selector: z.string().optional(),
|
|
});
|
|
|
|
const evaluateSchema = baseCommandSchema.extend({
|
|
action: z.literal('evaluate'),
|
|
script: z.string().min(1),
|
|
args: z.array(z.unknown()).optional(),
|
|
});
|
|
|
|
const waitSchema = baseCommandSchema.extend({
|
|
action: z.literal('wait'),
|
|
selector: z.string().min(1).optional(),
|
|
timeout: z.number().positive().optional(),
|
|
state: z.enum(['attached', 'detached', 'visible', 'hidden']).optional(),
|
|
});
|
|
|
|
const scrollSchema = baseCommandSchema.extend({
|
|
action: z.literal('scroll'),
|
|
selector: z.string().min(1).optional(),
|
|
x: z.number().optional(),
|
|
y: z.number().optional(),
|
|
direction: z.enum(['up', 'down', 'left', 'right']).optional(),
|
|
amount: z.number().positive().optional(),
|
|
});
|
|
|
|
const selectSchema = baseCommandSchema.extend({
|
|
action: z.literal('select'),
|
|
selector: z.string().min(1),
|
|
values: z.union([z.string(), z.array(z.string())]),
|
|
});
|
|
|
|
const hoverSchema = baseCommandSchema.extend({
|
|
action: z.literal('hover'),
|
|
selector: z.string().min(1),
|
|
});
|
|
|
|
const contentSchema = baseCommandSchema.extend({
|
|
action: z.literal('content'),
|
|
selector: z.string().min(1).optional(),
|
|
});
|
|
|
|
const closeSchema = baseCommandSchema.extend({
|
|
action: z.literal('close'),
|
|
});
|
|
|
|
// Tab/Window schemas
|
|
const tabNewSchema = baseCommandSchema.extend({
|
|
action: z.literal('tab_new'),
|
|
url: z.string().min(1).optional(),
|
|
});
|
|
|
|
const tabListSchema = baseCommandSchema.extend({
|
|
action: z.literal('tab_list'),
|
|
});
|
|
|
|
const tabSwitchSchema = baseCommandSchema.extend({
|
|
action: z.literal('tab_switch'),
|
|
index: z.number().nonnegative(),
|
|
});
|
|
|
|
const tabCloseSchema = baseCommandSchema.extend({
|
|
action: z.literal('tab_close'),
|
|
index: z.number().nonnegative().optional(),
|
|
});
|
|
|
|
const windowNewSchema = baseCommandSchema.extend({
|
|
action: z.literal('window_new'),
|
|
viewport: z
|
|
.object({
|
|
width: z.number().positive(),
|
|
height: z.number().positive(),
|
|
})
|
|
.optional(),
|
|
});
|
|
|
|
// Union schema for all commands
|
|
const commandSchema = z.discriminatedUnion('action', [
|
|
launchSchema,
|
|
navigateSchema,
|
|
clickSchema,
|
|
typeSchema,
|
|
fillSchema,
|
|
checkSchema,
|
|
uncheckSchema,
|
|
uploadSchema,
|
|
dblclickSchema,
|
|
focusSchema,
|
|
dragSchema,
|
|
frameSchema,
|
|
mainframeSchema,
|
|
getByRoleSchema,
|
|
getByTextSchema,
|
|
getByLabelSchema,
|
|
getByPlaceholderSchema,
|
|
pressSchema,
|
|
screenshotSchema,
|
|
snapshotSchema,
|
|
evaluateSchema,
|
|
waitSchema,
|
|
scrollSchema,
|
|
selectSchema,
|
|
hoverSchema,
|
|
contentSchema,
|
|
closeSchema,
|
|
tabNewSchema,
|
|
tabListSchema,
|
|
tabSwitchSchema,
|
|
tabCloseSchema,
|
|
windowNewSchema,
|
|
cookiesGetSchema,
|
|
cookiesSetSchema,
|
|
cookiesClearSchema,
|
|
storageGetSchema,
|
|
storageSetSchema,
|
|
storageClearSchema,
|
|
dialogSchema,
|
|
pdfSchema,
|
|
routeSchema,
|
|
unrouteSchema,
|
|
requestsSchema,
|
|
downloadSchema,
|
|
geolocationSchema,
|
|
permissionsSchema,
|
|
viewportSchema,
|
|
userAgentSchema,
|
|
deviceSchema,
|
|
backSchema,
|
|
forwardSchema,
|
|
reloadSchema,
|
|
urlSchema,
|
|
titleSchema,
|
|
getAttributeSchema,
|
|
getTextSchema,
|
|
isVisibleSchema,
|
|
isEnabledSchema,
|
|
isCheckedSchema,
|
|
countSchema,
|
|
boundingBoxSchema,
|
|
stylesSchema,
|
|
videoStartSchema,
|
|
videoStopSchema,
|
|
recordingStartSchema,
|
|
recordingStopSchema,
|
|
recordingRestartSchema,
|
|
traceStartSchema,
|
|
traceStopSchema,
|
|
harStartSchema,
|
|
harStopSchema,
|
|
stateSaveSchema,
|
|
stateLoadSchema,
|
|
consoleSchema,
|
|
errorsSchema,
|
|
keyboardSchema,
|
|
wheelSchema,
|
|
tapSchema,
|
|
clipboardSchema,
|
|
highlightSchema,
|
|
clearSchema,
|
|
selectAllSchema,
|
|
innerTextSchema,
|
|
innerHtmlSchema,
|
|
inputValueSchema,
|
|
setValueSchema,
|
|
dispatchSchema,
|
|
evalHandleSchema,
|
|
exposeSchema,
|
|
addScriptSchema,
|
|
addStyleSchema,
|
|
emulateMediaSchema,
|
|
offlineSchema,
|
|
headersSchema,
|
|
pauseSchema,
|
|
getByAltTextSchema,
|
|
getByTitleSchema,
|
|
getByTestIdSchema,
|
|
nthSchema,
|
|
waitForUrlSchema,
|
|
waitForLoadStateSchema,
|
|
setContentSchema,
|
|
timezoneSchema,
|
|
localeSchema,
|
|
credentialsSchema,
|
|
mouseMoveSchema,
|
|
mouseDownSchema,
|
|
mouseUpSchema,
|
|
bringToFrontSchema,
|
|
waitForFunctionSchema,
|
|
scrollIntoViewSchema,
|
|
addInitScriptSchema,
|
|
keyDownSchema,
|
|
keyUpSchema,
|
|
insertTextSchema,
|
|
multiSelectSchema,
|
|
waitForDownloadSchema,
|
|
responseBodySchema,
|
|
screencastStartSchema,
|
|
screencastStopSchema,
|
|
inputMouseSchema,
|
|
inputKeyboardSchema,
|
|
inputTouchSchema,
|
|
]);
|
|
|
|
// Parse result type
|
|
export type ParseResult =
|
|
| { success: true; command: Command }
|
|
| { success: false; error: string; id?: string };
|
|
|
|
/**
|
|
* Parse a JSON string into a validated command
|
|
*/
|
|
export function parseCommand(input: string): ParseResult {
|
|
// First, try to parse JSON
|
|
let json: unknown;
|
|
try {
|
|
json = JSON.parse(input);
|
|
} catch {
|
|
return { success: false, error: 'Invalid JSON' };
|
|
}
|
|
|
|
// Extract id for error responses if possible
|
|
const id =
|
|
typeof json === 'object' && json !== null && 'id' in json
|
|
? String((json as { id: unknown }).id)
|
|
: undefined;
|
|
|
|
// Validate against schema
|
|
const result = commandSchema.safeParse(json);
|
|
|
|
if (!result.success) {
|
|
const errors = result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
|
return { success: false, error: `Validation error: ${errors}`, id };
|
|
}
|
|
|
|
return { success: true, command: result.data as Command };
|
|
}
|
|
|
|
/**
|
|
* Create a success response
|
|
*/
|
|
export function successResponse<T>(id: string, data: T): Response<T> {
|
|
return { id, success: true, data };
|
|
}
|
|
|
|
/**
|
|
* Create an error response
|
|
*/
|
|
export function errorResponse(id: string, error: string): Response {
|
|
return { id, success: false, error };
|
|
}
|
|
|
|
/**
|
|
* Serialize a response to JSON string
|
|
*/
|
|
export function serializeResponse(response: Response): string {
|
|
return JSON.stringify(response);
|
|
}
|