Skip to content

Commit 9cebdbd

Browse files
committed
fix: Use correct abstraction for attach_browser, start_app_session, start_browser tools
- Correctly capture the capabilities of the sessions
1 parent 186a03a commit 9cebdbd

12 files changed

Lines changed: 166 additions & 106 deletions

src/config/appium.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export function buildIOSCapabilities(
9797
// Add any additional custom options
9898
for (const [key, value] of Object.entries(options)) {
9999
if (
100-
!['deviceName', 'platformVersion', 'automationName', 'autoAcceptAlerts', 'autoDismissAlerts', 'udid', 'noReset', 'fullReset', 'newCommandTimeout'].includes(
100+
!['deviceName', 'platformVersion', 'automationName', 'autoGrantPermissions', 'autoAcceptAlerts', 'autoDismissAlerts', 'udid', 'noReset', 'fullReset', 'newCommandTimeout'].includes(
101101
key,
102102
)
103103
) {
@@ -156,7 +156,7 @@ export function buildAndroidCapabilities(
156156
// Add any additional custom options
157157
for (const [key, value] of Object.entries(options)) {
158158
if (
159-
!['deviceName', 'platformVersion', 'automationName', 'autoGrantPermissions', 'appWaitActivity', 'noReset', 'fullReset', 'newCommandTimeout'].includes(
159+
!['deviceName', 'platformVersion', 'automationName', 'autoGrantPermissions', 'autoAcceptAlerts', 'autoDismissAlerts', 'appWaitActivity', 'noReset', 'fullReset', 'newCommandTimeout'].includes(
160160
key,
161161
)
162162
) {

src/recording/code-generator.ts

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function indentJson(value: unknown): string {
1919
.join('\n');
2020
}
2121

22-
function generateStep(step: RecordedStep): string {
22+
function generateStep(step: RecordedStep, history: SessionHistory): string {
2323
if (step.tool === '__session_transition__') {
2424
const newId = (step.params.newSessionId as string) ?? 'unknown';
2525
return `// --- new session: ${newId} started at ${step.timestamp} ---`;
@@ -32,49 +32,23 @@ function generateStep(step: RecordedStep): string {
3232
const p = step.params;
3333
switch (step.tool) {
3434
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;
4935
const nav = p.navigationUrl ? `\nawait browser.url('${escapeStr(p.navigationUrl)}');` : '';
50-
return `const browser = await remote({\n capabilities: ${indentJson(merged)}\n});${nav}`;
36+
return `const browser = await remote({\n capabilities: ${indentJson(history.capabilities)}\n});${nav}`;
5137
}
5238
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-
};
6939
const config: Record<string, unknown> = {
7040
protocol: 'http',
71-
hostname: p.appiumHost ?? 'localhost',
72-
port: p.appiumPort ?? 4723,
73-
path: p.appiumPath ?? '/',
74-
capabilities: caps,
41+
hostname: history.appiumConfig?.hostname ?? 'localhost',
42+
port: history.appiumConfig?.port ?? 4723,
43+
path: history.appiumConfig?.path ?? '/',
44+
capabilities: history.capabilities,
7545
};
7646
return `const browser = await remote(${indentJson(config)});`;
7747
}
48+
case 'attach_browser': {
49+
const nav = p.navigationUrl ? `\nawait browser.url('${escapeStr(p.navigationUrl)}');` : '';
50+
return `const browser = await remote({\n capabilities: ${indentJson(history.capabilities)}\n});${nav}`;
51+
}
7852
case 'navigate':
7953
return `await browser.url('${escapeStr(p.url)}');`;
8054
case 'click_element':
@@ -103,6 +77,6 @@ function generateStep(step: RecordedStep): string {
10377
}
10478

10579
export function generateCode(history: SessionHistory): string {
106-
const steps = history.steps.map(generateStep).join('\n');
80+
const steps = history.steps.map(step => generateStep(step, history)).join('\n');
10781
return `import { remote } from 'webdriverio';\n\n${steps}\n\nawait browser.deleteSession();`;
10882
}

src/server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,10 @@ const registerTool = (definition: ToolDefinition, callback: ToolCallback) =>
9494
}, callback);
9595

9696
// Browser and App Session Management
97-
registerTool(startBrowserToolDefinition, startBrowserTool);
98-
registerTool(startAppToolDefinition, startAppTool);
97+
registerTool(startBrowserToolDefinition, withRecording('start_browser', startBrowserTool));
98+
registerTool(startAppToolDefinition, withRecording('start_app_session', startAppTool));
9999
registerTool(closeSessionToolDefinition, closeSessionTool);
100-
registerTool(attachBrowserToolDefinition, attachBrowserTool);
100+
registerTool(attachBrowserToolDefinition, withRecording('attach_browser', attachBrowserTool));
101101
registerTool(emulateDeviceToolDefinition, emulateDeviceTool);
102102
registerTool(navigateToolDefinition, withRecording('navigate', navigateTool));
103103

src/tools/app-session.tool.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -185,31 +185,8 @@ 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: [{
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-
}],
188+
appiumConfig: { hostname: serverConfig.hostname, port: serverConfig.port, path: serverConfig.path },
189+
steps: [],
213190
});
214191

215192
state.currentSession = sessionId;

src/tools/attach-browser.tool.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ export const attachBrowserTool: ToolCallback = async ({
8282
capabilities: browser.capabilities,
8383
isAttached: true,
8484
});
85+
state.sessionHistory.set(sessionId, {
86+
sessionId,
87+
type: 'browser',
88+
startedAt: new Date().toISOString(),
89+
capabilities: {
90+
browserName: 'chrome',
91+
'goog:chromeOptions': {
92+
debuggerAddress: `${host}:${port}`,
93+
args: [`--user-data-dir=${userDataDir}`],
94+
},
95+
},
96+
steps: [],
97+
});
8598

8699
if (activeUrl) {
87100
await browser.url(activeUrl);

src/tools/browser.tool.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,7 @@ export const startBrowserTool: ToolCallback = async ({
187187
type: 'browser',
188188
startedAt: new Date().toISOString(),
189189
capabilities: wdioBrowser.capabilities as Record<string, unknown>,
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-
}],
190+
steps: [],
198191
});
199192

200193
state.currentSession = sessionId;

src/tools/context.tool.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,7 @@ export const switchContextTool: ToolCallback = async (args: {
8787
if (index >= 0 && index < contexts.length) {
8888
targetContext = contexts[index] as string;
8989
} else {
90-
return {
91-
content: [
92-
{
93-
type: 'text',
94-
text: `Error: Invalid context index ${context}. Available contexts: ${contexts.length}`,
95-
},
96-
],
97-
};
90+
throw new Error(`Error: Invalid context index ${context}. Available contexts: ${contexts.length}`);
9891
}
9992
}
10093

src/types/recording.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ export interface SessionHistory {
1414
startedAt: string; // ISO 8601
1515
endedAt?: string; // set on session close
1616
capabilities: Record<string, unknown>; // full resolved capabilities
17+
appiumConfig?: { hostname: string; port: number; path: string }; // app sessions only
1718
steps: RecordedStep[];
1819
}

tests/config/appium-config.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { buildAndroidCapabilities, buildIOSCapabilities } from '../../src/config/appium.config';
3+
4+
describe('buildAndroidCapabilities', () => {
5+
// Simulate how app-session.tool.ts calls this — all params destructured, unset ones are undefined
6+
const defaultOptions = { deviceName: 'emulator-5554', autoAcceptAlerts: undefined, autoDismissAlerts: undefined, autoGrantPermissions: undefined };
7+
8+
it('includes autoAcceptAlerts: true by default when param is undefined', () => {
9+
const caps = buildAndroidCapabilities('/app.apk', defaultOptions);
10+
expect(caps['appium:autoAcceptAlerts']).toBe(true);
11+
});
12+
13+
it('includes autoGrantPermissions: true by default when param is undefined', () => {
14+
const caps = buildAndroidCapabilities('/app.apk', defaultOptions);
15+
expect(caps['appium:autoGrantPermissions']).toBe(true);
16+
});
17+
18+
it('respects explicit autoAcceptAlerts: false', () => {
19+
const caps = buildAndroidCapabilities('/app.apk', { ...defaultOptions, autoAcceptAlerts: false });
20+
expect(caps['appium:autoAcceptAlerts']).toBe(false);
21+
});
22+
23+
it('sets autoDismissAlerts and clears autoAcceptAlerts when autoDismissAlerts is set', () => {
24+
const caps = buildAndroidCapabilities('/app.apk', { ...defaultOptions, autoDismissAlerts: true });
25+
expect(caps['appium:autoDismissAlerts']).toBe(true);
26+
expect(caps['appium:autoAcceptAlerts']).toBeUndefined();
27+
});
28+
});
29+
30+
describe('buildIOSCapabilities', () => {
31+
const defaultOptions = { deviceName: 'iPhone 15', autoAcceptAlerts: undefined, autoDismissAlerts: undefined, autoGrantPermissions: undefined };
32+
33+
it('includes autoAcceptAlerts: true by default when param is undefined', () => {
34+
const caps = buildIOSCapabilities('/app.app', defaultOptions);
35+
expect(caps['appium:autoAcceptAlerts']).toBe(true);
36+
});
37+
38+
it('includes autoGrantPermissions: true by default when param is undefined', () => {
39+
const caps = buildIOSCapabilities('/app.app', defaultOptions);
40+
expect(caps['appium:autoGrantPermissions']).toBe(true);
41+
});
42+
});

tests/recording/code-generator.test.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ function makeHistory(steps: Partial<RecordedStep>[]): SessionHistory {
2626
sessionId: 'test-123',
2727
type: 'browser',
2828
startedAt: '2026-01-01T00:00:00.000Z',
29-
capabilities: {},
29+
capabilities: {
30+
browserName: 'chrome',
31+
'goog:chromeOptions': { args: ['--window-size=1920,1080', '--headless=new'] },
32+
},
3033
steps: [START_BROWSER_STEP, ...extraSteps],
3134
};
3235
}
@@ -39,18 +42,61 @@ describe('generateCode - header', () => {
3942
expect(code).toContain('browserName');
4043
});
4144

42-
it('generates start_browser as const browser = await remote()', () => {
43-
const code = generateCode(makeHistory([]));
45+
it('generates start_browser using history.capabilities, not reconstructed from params', () => {
46+
const history: SessionHistory = {
47+
sessionId: 'caps-123',
48+
type: 'browser',
49+
startedAt: '2026-01-01T00:00:00.000Z',
50+
capabilities: {
51+
browserName: 'chrome',
52+
acceptInsecureCerts: true,
53+
'goog:chromeOptions': { args: ['--headless=new', '--custom-flag'] },
54+
},
55+
steps: [{
56+
index: 1,
57+
tool: 'start_browser',
58+
params: { browser: 'chrome', headless: true },
59+
status: 'ok',
60+
durationMs: 100,
61+
timestamp: '2026-01-01T00:00:00.000Z',
62+
}],
63+
};
64+
const code = generateCode(history);
4465
expect(code).toContain('const browser = await remote(');
4566
expect(code).toContain('"browserName": "chrome"');
67+
expect(code).toContain('--custom-flag'); // only present in history.capabilities
68+
});
69+
70+
it('generates attach_browser using history.capabilities', () => {
71+
const history: SessionHistory = {
72+
sessionId: 'attach-123',
73+
type: 'browser',
74+
startedAt: '2026-01-01T00:00:00.000Z',
75+
capabilities: {
76+
browserName: 'chrome',
77+
'goog:chromeOptions': { debuggerAddress: 'localhost:9222', args: ['--user-data-dir=/tmp/chrome-debug'] },
78+
},
79+
steps: [{
80+
index: 1,
81+
tool: 'attach_browser',
82+
params: { port: 9222, host: 'localhost', userDataDir: '/tmp/chrome-debug' },
83+
status: 'ok',
84+
durationMs: 100,
85+
timestamp: '2026-01-01T00:00:00.000Z',
86+
}],
87+
};
88+
const code = generateCode(history);
89+
expect(code).toContain('const browser = await remote(');
90+
expect(code).toContain('"debuggerAddress": "localhost:9222"');
91+
expect(code).toContain('--user-data-dir=/tmp/chrome-debug');
4692
});
4793

4894
it('appends browser.url() when navigationUrl is set on start_browser', () => {
4995
const history: SessionHistory = {
5096
sessionId: 'nav-123',
5197
type: 'browser',
5298
startedAt: '2026-01-01T00:00:00.000Z',
53-
capabilities: {},
99+
capabilities: { browserName: 'chrome' },
54100
steps: [{
55101
index: 1,
56102
tool: 'start_browser',
@@ -64,26 +110,30 @@ describe('generateCode - header', () => {
64110
expect(code).toContain("await browser.url('https://github.com/login');");
65111
});
66112

67-
it('generates start_app_session with appium caps and server config', () => {
113+
it('generates start_app_session using history.appiumConfig for connection config', () => {
68114
const history: SessionHistory = {
69115
sessionId: 'app-123',
70-
type: 'ios',
116+
type: 'android',
71117
startedAt: '2026-01-01T00:00:00.000Z',
72-
capabilities: {},
118+
capabilities: {
119+
platformName: 'Android',
120+
'appium:deviceName': 'emulator-5554',
121+
'appium:app': '/app/MyApp.apk',
122+
},
123+
appiumConfig: { hostname: '127.0.0.1', port: 4723, path: '/' },
73124
steps: [{
74125
index: 1,
75126
tool: 'start_app_session',
76-
params: { platform: 'iOS', deviceName: 'iPhone 14', platformVersion: '17.0', appPath: '/app/MyApp.app' },
127+
params: { platform: 'Android', deviceName: 'emulator-5554' }, // no appiumHost in params
77128
status: 'ok',
78-
durationMs: 0,
129+
durationMs: 100,
79130
timestamp: '2026-01-01T00:00:00.000Z',
80131
}],
81132
};
82133
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"');
134+
expect(code).toContain('"hostname": "127.0.0.1"'); // from history.appiumConfig, not params fallback
135+
expect(code).toContain('"port": 4723');
136+
expect(code).toContain('"platformName": "Android"');
87137
});
88138
});
89139

0 commit comments

Comments
 (0)