Skip to content

Commit 24e4fdb

Browse files
committed
chore: Infer session capabilities from start_app_session and browser tool inputs
- Preferring headless browsers
1 parent 6c41763 commit 24e4fdb

4 files changed

Lines changed: 153 additions & 23 deletions

File tree

src/recording/code-generator.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ function formatParams(params: Record<string, unknown>): string {
1212
.join(' ');
1313
}
1414

15+
function indentJson(value: unknown): string {
16+
return JSON.stringify(value, null, 2)
17+
.split('\n')
18+
.map((line, i) => (i > 0 ? ` ${line}` : line))
19+
.join('\n');
20+
}
21+
1522
function generateStep(step: RecordedStep): string {
1623
if (step.tool === '__session_transition__') {
1724
const newId = (step.params.newSessionId as string) ?? 'unknown';
@@ -24,6 +31,50 @@ function generateStep(step: RecordedStep): string {
2431

2532
const p = step.params;
2633
switch (step.tool) {
34+
case 'start_browser': {
35+
const browserName = p.browser === 'edge' ? 'msedge' : String(p.browser ?? 'chrome');
36+
const headless = p.headless !== false;
37+
const width = (p.windowWidth as number | undefined) ?? 1920;
38+
const height = (p.windowHeight as number | undefined) ?? 1080;
39+
const args: string[] = [`--window-size=${width},${height}`];
40+
if (headless && browserName !== 'safari') {
41+
args.push('--headless=new', '--disable-gpu', '--disable-dev-shm-usage');
42+
}
43+
const caps: Record<string, unknown> = { browserName };
44+
if (browserName === 'chrome') caps['goog:chromeOptions'] = { args };
45+
else if (browserName === 'msedge') caps['ms:edgeOptions'] = { args };
46+
else if (browserName === 'firefox' && headless) caps['moz:firefoxOptions'] = { args: ['-headless'] };
47+
const extra = p.capabilities as Record<string, unknown> | undefined;
48+
const merged = extra ? { ...caps, ...extra } : caps;
49+
const nav = p.navigationUrl ? `\nawait browser.url('${escapeStr(p.navigationUrl)}');` : '';
50+
return `const browser = await remote({\n capabilities: ${indentJson(merged)}\n});${nav}`;
51+
}
52+
case 'start_app_session': {
53+
const caps: Record<string, unknown> = {
54+
platformName: p.platform,
55+
'appium:deviceName': p.deviceName,
56+
...(p.platformVersion !== undefined && { 'appium:platformVersion': p.platformVersion }),
57+
...(p.automationName !== undefined && { 'appium:automationName': p.automationName }),
58+
...(p.appPath !== undefined && { 'appium:app': p.appPath }),
59+
...(p.udid !== undefined && { 'appium:udid': p.udid }),
60+
...(p.noReset !== undefined && { 'appium:noReset': p.noReset }),
61+
...(p.fullReset !== undefined && { 'appium:fullReset': p.fullReset }),
62+
...(p.autoGrantPermissions !== undefined && { 'appium:autoGrantPermissions': p.autoGrantPermissions }),
63+
...(p.autoAcceptAlerts !== undefined && { 'appium:autoAcceptAlerts': p.autoAcceptAlerts }),
64+
...(p.autoDismissAlerts !== undefined && { 'appium:autoDismissAlerts': p.autoDismissAlerts }),
65+
...(p.appWaitActivity !== undefined && { 'appium:appWaitActivity': p.appWaitActivity }),
66+
...(p.newCommandTimeout !== undefined && { 'appium:newCommandTimeout': p.newCommandTimeout }),
67+
...((p.capabilities as Record<string, unknown> | undefined) ?? {}),
68+
};
69+
const config: Record<string, unknown> = {
70+
protocol: 'http',
71+
hostname: p.appiumHost ?? 'localhost',
72+
port: p.appiumPort ?? 4723,
73+
path: p.appiumPath ?? '/',
74+
capabilities: caps,
75+
};
76+
return `const browser = await remote(${indentJson(config)});`;
77+
}
2778
case 'navigate':
2879
return `await browser.url('${escapeStr(p.url)}');`;
2980
case 'click_element':
@@ -51,16 +102,7 @@ function generateStep(step: RecordedStep): string {
51102
}
52103
}
53104

54-
function buildHeader(history: SessionHistory): string {
55-
const caps = JSON.stringify(history.capabilities, null, 2)
56-
.split('\n')
57-
.map((line) => ` ${line}`)
58-
.join('\n');
59-
return `import { remote } from 'webdriverio';\nconst browser = await remote(\n${caps}\n);`;
60-
}
61-
62105
export function generateCode(history: SessionHistory): string {
63-
const header = buildHeader(history);
64106
const steps = history.steps.map(generateStep).join('\n');
65-
return `${header}\n\n${steps}\n\nawait browser.deleteSession();`;
107+
return `import { remote } from 'webdriverio';\n\n${steps}\n\nawait browser.deleteSession();`;
66108
}

src/tools/app-session.tool.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,31 @@ export const startAppTool: ToolCallback = async (args: {
185185
type: platform.toLowerCase() as 'ios' | 'android',
186186
startedAt: new Date().toISOString(),
187187
capabilities: mergedCapabilities as Record<string, unknown>,
188-
steps: [],
188+
steps: [{
189+
index: 1,
190+
tool: 'start_app_session',
191+
params: {
192+
platform, deviceName,
193+
...(platformVersion !== undefined && { platformVersion }),
194+
...(automationName !== undefined && { automationName }),
195+
...(appPath !== undefined && { appPath }),
196+
...(udid !== undefined && { udid }),
197+
...(noReset !== undefined && { noReset }),
198+
...(fullReset !== undefined && { fullReset }),
199+
...(autoGrantPermissions !== undefined && { autoGrantPermissions }),
200+
...(autoAcceptAlerts !== undefined && { autoAcceptAlerts }),
201+
...(autoDismissAlerts !== undefined && { autoDismissAlerts }),
202+
...(appWaitActivity !== undefined && { appWaitActivity }),
203+
...(newCommandTimeout !== undefined && { newCommandTimeout }),
204+
...(appiumHost !== undefined && { appiumHost }),
205+
...(appiumPort !== undefined && { appiumPort }),
206+
...(appiumPath !== undefined && { appiumPath }),
207+
...(Object.keys(userCapabilities).length > 0 && { capabilities: userCapabilities }),
208+
},
209+
status: 'ok',
210+
durationMs: 0,
211+
timestamp: new Date().toISOString(),
212+
}],
189213
});
190214

191215
state.currentSession = sessionId;

src/tools/browser.tool.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type SupportedBrowser = z.infer<typeof browserSchema>;
1111

1212
export const startBrowserToolDefinition: ToolDefinition = {
1313
name: 'start_browser',
14-
description: 'starts a browser session (Chrome, Firefox, Edge, Safari) and sets it to the current state',
14+
description: 'starts a browser session (Chrome, Firefox, Edge, Safari) and sets it to the current state. Prefer headless: true unless the user explicitly asks to see the browser.',
1515
inputSchema: {
1616
browser: browserSchema.describe('Browser to launch: chrome, firefox, edge, safari (default: chrome)'),
1717
headless: z.boolean().optional().default(true),
@@ -187,7 +187,14 @@ export const startBrowserTool: ToolCallback = async ({
187187
type: 'browser',
188188
startedAt: new Date().toISOString(),
189189
capabilities: wdioBrowser.capabilities as Record<string, unknown>,
190-
steps: [],
190+
steps: [{
191+
index: 1,
192+
tool: 'start_browser',
193+
params: { browser, headless, windowWidth, windowHeight, ...(navigationUrl && { navigationUrl }), ...(Object.keys(userCapabilities).length > 0 && { capabilities: userCapabilities }) },
194+
status: 'ok',
195+
durationMs: 0,
196+
timestamp: new Date().toISOString(),
197+
}],
191198
});
192199

193200
state.currentSession = sessionId;

tests/recording/code-generator.test.ts

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,31 @@ import { describe, expect, it } from 'vitest';
33
import { generateCode } from '../../src/recording/code-generator';
44
import type { SessionHistory, RecordedStep } from '../../src/types/recording';
55

6+
const START_BROWSER_STEP: RecordedStep = {
7+
index: 1,
8+
tool: 'start_browser',
9+
params: { browser: 'chrome', headless: true, windowWidth: 1920, windowHeight: 1080 },
10+
status: 'ok',
11+
durationMs: 0,
12+
timestamp: '2026-01-01T00:00:00.000Z',
13+
};
14+
615
function makeHistory(steps: Partial<RecordedStep>[]): SessionHistory {
16+
const extraSteps = steps.map((s, i) => ({
17+
index: i + 2,
18+
tool: 'navigate',
19+
params: {},
20+
status: 'ok' as const,
21+
durationMs: 10,
22+
timestamp: '2026-01-01T00:00:00.000Z',
23+
...s,
24+
}));
725
return {
826
sessionId: 'test-123',
927
type: 'browser',
1028
startedAt: '2026-01-01T00:00:00.000Z',
11-
capabilities: { browserName: 'chrome' },
12-
steps: steps.map((s, i) => ({
13-
index: i + 1,
14-
tool: 'navigate',
15-
params: {},
16-
status: 'ok',
17-
durationMs: 10,
18-
timestamp: '2026-01-01T00:00:00.000Z',
19-
...s,
20-
})),
29+
capabilities: {},
30+
steps: [START_BROWSER_STEP, ...extraSteps],
2131
};
2232
}
2333

@@ -28,6 +38,53 @@ describe('generateCode - header', () => {
2838
expect(code).toContain('await browser.deleteSession();');
2939
expect(code).toContain('browserName');
3040
});
41+
42+
it('generates start_browser as const browser = await remote()', () => {
43+
const code = generateCode(makeHistory([]));
44+
expect(code).toContain('const browser = await remote(');
45+
expect(code).toContain('"browserName": "chrome"');
46+
});
47+
48+
it('appends browser.url() when navigationUrl is set on start_browser', () => {
49+
const history: SessionHistory = {
50+
sessionId: 'nav-123',
51+
type: 'browser',
52+
startedAt: '2026-01-01T00:00:00.000Z',
53+
capabilities: {},
54+
steps: [{
55+
index: 1,
56+
tool: 'start_browser',
57+
params: { browser: 'chrome', headless: false, windowWidth: 1920, windowHeight: 1080, navigationUrl: 'https://github.com/login' },
58+
status: 'ok',
59+
durationMs: 0,
60+
timestamp: '2026-01-01T00:00:00.000Z',
61+
}],
62+
};
63+
const code = generateCode(history);
64+
expect(code).toContain("await browser.url('https://github.com/login');");
65+
});
66+
67+
it('generates start_app_session with appium caps and server config', () => {
68+
const history: SessionHistory = {
69+
sessionId: 'app-123',
70+
type: 'ios',
71+
startedAt: '2026-01-01T00:00:00.000Z',
72+
capabilities: {},
73+
steps: [{
74+
index: 1,
75+
tool: 'start_app_session',
76+
params: { platform: 'iOS', deviceName: 'iPhone 14', platformVersion: '17.0', appPath: '/app/MyApp.app' },
77+
status: 'ok',
78+
durationMs: 0,
79+
timestamp: '2026-01-01T00:00:00.000Z',
80+
}],
81+
};
82+
const code = generateCode(history);
83+
expect(code).toContain('const browser = await remote(');
84+
expect(code).toContain('"platformName": "iOS"');
85+
expect(code).toContain('"appium:deviceName": "iPhone 14"');
86+
expect(code).toContain('"appium:app": "/app/MyApp.app"');
87+
});
3188
});
3289

3390
describe('generateCode - tool mappings', () => {

0 commit comments

Comments
 (0)