Skip to content

Commit 628bf35

Browse files
authored
feat: enable webview context handling and command proxying in driver (#131)
1 parent c9a3422 commit 628bf35

File tree

3 files changed

+147
-8
lines changed

3 files changed

+147
-8
lines changed

src/driver.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,37 @@ import {
3030
isFlutterDriverCommand,
3131
waitForFlutterServerToBeActive,
3232
} from './utils';
33-
import { util } from 'appium/support';
33+
import { logger, util } from 'appium/support';
3434
import { androidPortForward, androidRemovePortForward } from './android';
3535
import { iosPortForward, iosRemovePortForward } from './iOS';
3636
import type { PortForwardCallback, PortReleaseCallback } from './types';
3737
import _ from 'lodash';
3838

39+
import type { RouteMatcher } from '@appium/types';
40+
41+
const WEBVIEW_NO_PROXY = [
42+
[`GET`, new RegExp(`^/session/[^/]+/appium`)],
43+
[`GET`, new RegExp(`^/session/[^/]+/context`)],
44+
[`GET`, new RegExp(`^/session/[^/]+/element/[^/]+/rect`)],
45+
[`GET`, new RegExp(`^/session/[^/]+/log/types$`)],
46+
[`GET`, new RegExp(`^/session/[^/]+/orientation`)],
47+
[`POST`, new RegExp(`^/session/[^/]+/appium`)],
48+
[`POST`, new RegExp(`^/session/[^/]+/context`)],
49+
[`POST`, new RegExp(`^/session/[^/]+/log$`)],
50+
[`POST`, new RegExp(`^/session/[^/]+/orientation`)],
51+
[`POST`, new RegExp(`^/session/[^/]+/touch/multi/perform`)],
52+
[`POST`, new RegExp(`^/session/[^/]+/touch/perform`)],
53+
] as import('@appium/types').RouteMatcher[];
54+
3955
export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
4056
// @ts-ignore
4157
public proxydriver: XCUITestDriver | AndroidUiautomator2Driver;
4258
public flutterPort: number | null | undefined;
4359
private internalCaps: DriverCaps<FlutterDriverConstraints> | undefined;
4460
public proxy: JWProxy | undefined;
61+
private proxyWebViewActive: boolean = false;
62+
public readonly NATIVE_CONTEXT_NAME: string = `NATIVE_APP`;
63+
public currentContext: string = this.NATIVE_CONTEXT_NAME;
4564
click = click;
4665
findElOrEls = findElOrEls;
4766
getText = getText;
@@ -193,12 +212,44 @@ export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
193212
}
194213

195214
async executeCommand(command: any, ...args: any) {
196-
if (isFlutterDriverCommand(command)) {
215+
if (
216+
this.currentContext === this.NATIVE_CONTEXT_NAME &&
217+
isFlutterDriverCommand(command)
218+
) {
197219
return await super.executeCommand(command, ...args);
198220
}
221+
222+
this.handleContextSwitch(command, args);
223+
logger.default.info(
224+
`Executing the proxy command: ${command} with args: ${args}`,
225+
);
199226
return await this.proxydriver.executeCommand(command as string, ...args);
200227
}
201228

229+
private handleContextSwitch(command: string, args: any[]): void {
230+
if (command === 'setContext') {
231+
const isWebviewContext =
232+
typeof args[0] === 'string' && args[0].includes('WEBVIEW');
233+
if (typeof args[0] === 'string' && args[0].length > 0) {
234+
this.currentContext = args[0];
235+
} else {
236+
logger.default.warn(
237+
`Attempted to set context to invalid value: ${args[0]}. Keeping current context: ${this.currentContext}`,
238+
);
239+
}
240+
241+
if (isWebviewContext) {
242+
this.proxyWebViewActive = true;
243+
} else {
244+
this.proxyWebViewActive = false;
245+
}
246+
}
247+
}
248+
249+
public getProxyAvoidList(): RouteMatcher[] {
250+
return WEBVIEW_NO_PROXY;
251+
}
252+
202253
public async createSession(
203254
...args: any[]
204255
): Promise<DefaultCreateSessionResult<FlutterDriverConstraints>> {
@@ -382,8 +433,20 @@ export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
382433
return await this.proxydriver.execute(script, args);
383434
}
384435

385-
canProxy() {
386-
return false;
436+
public proxyActive(): boolean {
437+
// In WebView context, all request should go to each driver
438+
// so that they can handle http request properly.
439+
// On iOS, WebView context is handled by XCUITest driver while Android is by chromedriver.
440+
// It means XCUITest driver should keep the XCUITest driver as a proxy,
441+
// while UIAutomator2 driver should proxy to chromedriver instead of UIA2 proxy.
442+
return (
443+
this.proxyWebViewActive &&
444+
!(this.proxydriver instanceof XCUITestDriver)
445+
);
446+
}
447+
448+
public canProxy(): boolean {
449+
return this.proxyWebViewActive;
387450
}
388451

389452
async deleteSession() {
@@ -400,6 +463,8 @@ export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
400463

401464
async mobilelaunchApp(appId: string, args: string[], environment: any) {
402465
let activateAppResponse;
466+
this.currentContext = this.NATIVE_CONTEXT_NAME;
467+
this.proxyWebViewActive = false;
403468
const launchArgs = _.assign(
404469
{ arguments: [] as string[] },
405470
{ arguments: args, environment },

test/specs/test.e2e.js

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ async function performLogin(userName = 'admin', password = '1234') {
55
const att = await browser.flutterByValueKey$('username_text_field');
66
console.log(await att.getAttribute('all'));
77
await browser.flutterByValueKey$('username_text_field').clearValue();
8-
await $(
9-
'//android.view.View[@content-desc="username_text_field"]/android.widget.EditText',
10-
).addValue(userName);
8+
await browser.flutterByValueKey$('username_text_field').addValue(userName);
119

1210
await browser.flutterByValueKey$('password_text_field').clearValue();
1311
await browser.flutterByValueKey$('password').addValue(password);
@@ -21,8 +19,29 @@ async function openScreen(screenTitle) {
2119
await screenListElement.click();
2220
}
2321

22+
async function switchToWebview(timeout = 5000) {
23+
const webviewContext = await browser.waitUntil(
24+
async () => {
25+
const contexts = await browser.getContexts();
26+
return contexts.find((ctx) => ctx.includes('WEBVIEW'));
27+
},
28+
{
29+
timeout,
30+
timeoutMsg: `WEBVIEW context not found within ${timeout / 1000}s`,
31+
},
32+
);
33+
34+
await browser.switchContext(webviewContext);
35+
return webviewContext;
36+
}
37+
2438
describe('My Login application', () => {
2539
afterEach(async () => {
40+
const currentContext = await browser.getContext();
41+
if (currentContext !== 'NATIVE_APP') {
42+
await browser.switchContext('NATIVE_APP');
43+
}
44+
2645
const appID = browser.isIOS
2746
? 'com.example.appiumTestingApp'
2847
: 'com.example.appium_testing_app';
@@ -225,4 +244,58 @@ describe('My Login application', () => {
225244
.getText();
226245
expect(dropped).toEqual('The box is dropped');
227246
});
247+
248+
it('should switch to webview context and validate the page title', async () => {
249+
await performLogin();
250+
await openScreen('Web View');
251+
await switchToWebview();
252+
253+
await browser.waitUntil(
254+
async () => (await browser.getTitle()) === 'Hacker News',
255+
{
256+
timeout: 10000,
257+
timeoutMsg: 'Expected Hacker News title not found',
258+
},
259+
);
260+
261+
const title = await browser.getTitle();
262+
expect(title).toEqual(
263+
'Hacker News',
264+
'Webview title did not match expected',
265+
);
266+
});
267+
268+
it('should execute native commands correctly while in Webview context', async () => {
269+
await performLogin();
270+
await openScreen('Web View');
271+
await switchToWebview();
272+
273+
// Verify no-proxy native commands still operate while in webview context
274+
const currentContext = await browser.getContext();
275+
expect(currentContext).toContain('WEBVIEW');
276+
277+
const contexts = await browser.getContexts();
278+
expect(Array.isArray(contexts)).toBe(true);
279+
expect(contexts.length).toBeGreaterThan(0);
280+
281+
const windowHandle = await browser.getWindowHandle();
282+
expect(typeof windowHandle).toBe('string');
283+
284+
const pageSource = await browser.getPageSource();
285+
expect(typeof pageSource).toBe('string');
286+
});
287+
288+
it('should switch back and forth between native and Webview contexts', async () => {
289+
await performLogin();
290+
await openScreen('Web View');
291+
292+
await switchToWebview();
293+
expect(await browser.getContext()).toContain('WEBVIEW');
294+
295+
await browser.switchContext('NATIVE_APP');
296+
expect(await browser.getContext()).toBe('NATIVE_APP');
297+
298+
await switchToWebview();
299+
expect(await browser.getContext()).toContain('WEBVIEW');
300+
});
228301
});

wdio.conf.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ export const config: Options.Testrunner = {
125125
args: {
126126
basePath: '/wd/hub',
127127
port: 4723,
128-
log: join(process.cwd(), 'appium-logs', 'logs.txt')
128+
log: join(process.cwd(), 'appium-logs', 'logs.txt'),
129+
allowInsecure: 'chromedriver_autodownload',
129130
},
130131
},
131132
],

0 commit comments

Comments
 (0)