Skip to content

Commit 98ead11

Browse files
authored
fix(alert): use findElement in Android custom button lookup (#449)
1 parent 476f653 commit 98ead11

2 files changed

Lines changed: 116 additions & 2 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, test, expect, jest, beforeEach } from '@jest/globals';
2+
3+
jest.unstable_mockModule('../../../persistence.js', () => ({
4+
readAllPersistedSessions: jest.fn(async () => []),
5+
removePersistedSession: jest.fn(async () => {}),
6+
}));
7+
8+
jest.unstable_mockModule('../../../session-store.js', () => ({
9+
getDriver: jest.fn(),
10+
setSession: jest.fn(),
11+
getPlatformName: jest.fn(() => 'Android'),
12+
PLATFORM: { ios: 'iOS', android: 'Android' },
13+
}));
14+
15+
jest.unstable_mockModule('../../../command.js', () => ({
16+
elementClick: jest.fn(async () => {}),
17+
execute: jest.fn(async () => {}),
18+
findElement: jest.fn(),
19+
getPageSource: jest.fn(async () => '<hierarchy/>'),
20+
}));
21+
22+
jest.unstable_mockModule('../../../locators/generate-all-locators.js', () => ({
23+
generateAllElementLocators: jest.fn(() => [
24+
{
25+
text: 'OK',
26+
contentDesc: 'OK',
27+
clickable: true,
28+
locators: { 'accessibility id': 'OK' },
29+
},
30+
]),
31+
}));
32+
33+
const { getDriver } = await import('../../../session-store.js');
34+
const { elementClick, findElement } = await import('../../../command.js');
35+
36+
const mockGetDriver = getDriver as jest.MockedFunction<typeof getDriver>;
37+
const mockElementClick = elementClick as jest.MockedFunction<
38+
typeof elementClick
39+
>;
40+
const mockFindElement = findElement as jest.MockedFunction<typeof findElement>;
41+
42+
// A remote WebDriver client resolves a missing element as this object instead
43+
// of throwing; the raw driver.findElement path would treat it as a real hit.
44+
const SWALLOWED_NO_SUCH_ELEMENT = {
45+
error: 'no such element',
46+
message: 'An element could not be located on the page',
47+
};
48+
49+
const mockServer = { addTool: jest.fn() } as any;
50+
51+
async function loadTool(): Promise<{
52+
execute: (...args: any[]) => Promise<any>;
53+
}> {
54+
const mod = await import('../../../tools/interactions/handle-alert.js');
55+
mod.default(mockServer);
56+
return (mockServer.addTool as jest.MockedFunction<any>).mock.calls.at(
57+
-1
58+
)?.[0];
59+
}
60+
61+
function textFromResult(result: {
62+
content: Array<{ type: string; text?: string }>;
63+
}): string | undefined {
64+
const block = result.content[0];
65+
return block && 'text' in block ? block.text : undefined;
66+
}
67+
68+
describe('appium_alert Android custom button', () => {
69+
beforeEach(() => {
70+
mockGetDriver.mockReturnValue({
71+
findElement: jest.fn(async () => SWALLOWED_NO_SUCH_ELEMENT),
72+
} as any);
73+
mockElementClick.mockReset();
74+
mockFindElement.mockReset();
75+
});
76+
77+
test('does not click when findElement re-throws a swallowed remote error', async () => {
78+
// command.findElement surfaces the W3C "no such element" a remote client
79+
// otherwise resolves silently, so the locator loop must treat it as a miss.
80+
mockFindElement.mockRejectedValue(
81+
Object.assign(new Error('no such element'), { name: 'no such element' })
82+
);
83+
84+
const tool = await loadTool();
85+
const result = await tool.execute(
86+
{ action: 'accept', buttonLabel: 'OK' },
87+
undefined
88+
);
89+
90+
expect(result.isError).toBe(true);
91+
expect(textFromResult(result)).toContain('Could not find element');
92+
expect(mockElementClick).not.toHaveBeenCalled();
93+
});
94+
95+
test('clicks the resolved element when findElement succeeds', async () => {
96+
mockFindElement.mockResolvedValue({
97+
'element-6066-11e4-a52e-4f735466cecf': 'el-1',
98+
});
99+
100+
const tool = await loadTool();
101+
const result = await tool.execute(
102+
{ action: 'accept', buttonLabel: 'OK' },
103+
undefined
104+
);
105+
106+
expect(result.isError).toBeFalsy();
107+
expect(mockElementClick).toHaveBeenCalledTimes(1);
108+
});
109+
});

src/tools/interactions/handle-alert.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { z } from 'zod';
33
import { generateAllElementLocators } from '../../locators/generate-all-locators.js';
44
import { getPlatformName, PLATFORM } from '../../session-store.js';
55
import type { DriverInstance } from '../../session-store.js';
6-
import { elementClick, execute, getPageSource } from '../../command.js';
6+
import {
7+
elementClick,
8+
execute,
9+
findElement,
10+
getPageSource,
11+
} from '../../command.js';
712
import {
813
resolveDriver,
914
textResult,
@@ -134,7 +139,7 @@ async function handleAndroidAlert(
134139
continue;
135140
}
136141
try {
137-
button = await driver.findElement(strategy, selector);
142+
button = await findElement(driver, strategy, selector);
138143
break;
139144
} catch {
140145
continue;

0 commit comments

Comments
 (0)