Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 69 additions & 4 deletions src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,37 @@ import {
isFlutterDriverCommand,
waitForFlutterServerToBeActive,
} from './utils';
import { util } from 'appium/support';
import { logger, util } from 'appium/support';
import { androidPortForward, androidRemovePortForward } from './android';
import { iosPortForward, iosRemovePortForward } from './iOS';
import type { PortForwardCallback, PortReleaseCallback } from './types';
import _ from 'lodash';

import type { RouteMatcher } from '@appium/types';

const WEBVIEW_NO_PROXY = [
[`GET`, new RegExp(`^/session/[^/]+/appium`)],
[`GET`, new RegExp(`^/session/[^/]+/context`)],
[`GET`, new RegExp(`^/session/[^/]+/element/[^/]+/rect`)],
[`GET`, new RegExp(`^/session/[^/]+/log/types$`)],
[`GET`, new RegExp(`^/session/[^/]+/orientation`)],
[`POST`, new RegExp(`^/session/[^/]+/appium`)],
[`POST`, new RegExp(`^/session/[^/]+/context`)],
[`POST`, new RegExp(`^/session/[^/]+/log$`)],
[`POST`, new RegExp(`^/session/[^/]+/orientation`)],
[`POST`, new RegExp(`^/session/[^/]+/touch/multi/perform`)],
[`POST`, new RegExp(`^/session/[^/]+/touch/perform`)],
] as import('@appium/types').RouteMatcher[];

export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
// @ts-ignore
public proxydriver: XCUITestDriver | AndroidUiautomator2Driver;
public flutterPort: number | null | undefined;
private internalCaps: DriverCaps<FlutterDriverConstraints> | undefined;
public proxy: JWProxy | undefined;
private proxyWebViewActive: boolean = false;
public readonly NATIVE_CONTEXT_NAME: string = `NATIVE_APP`;
public currentContext: string = this.NATIVE_CONTEXT_NAME;
click = click;
findElOrEls = findElOrEls;
getText = getText;
Expand Down Expand Up @@ -193,12 +212,44 @@ export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
}

async executeCommand(command: any, ...args: any) {
if (isFlutterDriverCommand(command)) {
if (
this.currentContext === this.NATIVE_CONTEXT_NAME &&
isFlutterDriverCommand(command)
) {
return await super.executeCommand(command, ...args);
}

this.handleContextSwitch(command, args);
logger.default.info(
`Executing the proxy command: ${command} with args: ${args}`,
);
return await this.proxydriver.executeCommand(command as string, ...args);
}

private handleContextSwitch(command: string, args: any[]): void {
if (command === 'setContext') {
const isWebviewContext =
typeof args[0] === 'string' && args[0].includes('WEBVIEW');
if (typeof args[0] === 'string' && args[0].length > 0) {
this.currentContext = args[0];
} else {
logger.default.warn(
`Attempted to set context to invalid value: ${args[0]}. Keeping current context: ${this.currentContext}`,
);
}

if (isWebviewContext) {
this.proxyWebViewActive = true;
} else {
this.proxyWebViewActive = false;
}
}
}

public getProxyAvoidList(): RouteMatcher[] {
return WEBVIEW_NO_PROXY;
}

public async createSession(
...args: any[]
): Promise<DefaultCreateSessionResult<FlutterDriverConstraints>> {
Expand Down Expand Up @@ -382,8 +433,20 @@ export class AppiumFlutterDriver extends BaseDriver<FlutterDriverConstraints> {
return await this.proxydriver.execute(script, args);
}

canProxy() {
return false;
public proxyActive(): boolean {
// In WebView context, all request should go to each driver
// so that they can handle http request properly.
// On iOS, WebView context is handled by XCUITest driver while Android is by chromedriver.
// It means XCUITest driver should keep the XCUITest driver as a proxy,
// while UIAutomator2 driver should proxy to chromedriver instead of UIA2 proxy.
return (
this.proxyWebViewActive &&
!(this.proxydriver instanceof XCUITestDriver)
);
}

public canProxy(): boolean {
return this.proxyWebViewActive;
}

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

async mobilelaunchApp(appId: string, args: string[], environment: any) {
let activateAppResponse;
this.currentContext = this.NATIVE_CONTEXT_NAME;
this.proxyWebViewActive = false;
const launchArgs = _.assign(
{ arguments: [] as string[] },
{ arguments: args, environment },
Expand Down
79 changes: 76 additions & 3 deletions test/specs/test.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ async function performLogin(userName = 'admin', password = '1234') {
const att = await browser.flutterByValueKey$('username_text_field');
console.log(await att.getAttribute('all'));
await browser.flutterByValueKey$('username_text_field').clearValue();
await $(
'//android.view.View[@content-desc="username_text_field"]/android.widget.EditText',
).addValue(userName);
await browser.flutterByValueKey$('username_text_field').addValue(userName);

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

async function switchToWebview(timeout = 5000) {
const webviewContext = await browser.waitUntil(
async () => {
const contexts = await browser.getContexts();
return contexts.find((ctx) => ctx.includes('WEBVIEW'));
},
{
timeout,
timeoutMsg: `WEBVIEW context not found within ${timeout / 1000}s`,
},
);

await browser.switchContext(webviewContext);
return webviewContext;
}

describe('My Login application', () => {
afterEach(async () => {
const currentContext = await browser.getContext();
if (currentContext !== 'NATIVE_APP') {
await browser.switchContext('NATIVE_APP');
}

const appID = browser.isIOS
? 'com.example.appiumTestingApp'
: 'com.example.appium_testing_app';
Expand Down Expand Up @@ -225,4 +244,58 @@ describe('My Login application', () => {
.getText();
expect(dropped).toEqual('The box is dropped');
});

it('should switch to webview context and validate the page title', async () => {
await performLogin();
await openScreen('Web View');
await switchToWebview();

await browser.waitUntil(
async () => (await browser.getTitle()) === 'Hacker News',
{
timeout: 10000,
timeoutMsg: 'Expected Hacker News title not found',
},
);

const title = await browser.getTitle();
expect(title).toEqual(
'Hacker News',
'Webview title did not match expected',
);
});

it('should execute native commands correctly while in Webview context', async () => {
await performLogin();
await openScreen('Web View');
await switchToWebview();

// Verify no-proxy native commands still operate while in webview context
const currentContext = await browser.getContext();
expect(currentContext).toContain('WEBVIEW');

const contexts = await browser.getContexts();
expect(Array.isArray(contexts)).toBe(true);
expect(contexts.length).toBeGreaterThan(0);

const windowHandle = await browser.getWindowHandle();
expect(typeof windowHandle).toBe('string');

const pageSource = await browser.getPageSource();
expect(typeof pageSource).toBe('string');
});

it('should switch back and forth between native and Webview contexts', async () => {
await performLogin();
await openScreen('Web View');

await switchToWebview();
expect(await browser.getContext()).toContain('WEBVIEW');

await browser.switchContext('NATIVE_APP');
expect(await browser.getContext()).toBe('NATIVE_APP');

await switchToWebview();
expect(await browser.getContext()).toContain('WEBVIEW');
});
});
3 changes: 2 additions & 1 deletion wdio.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ export const config: Options.Testrunner = {
args: {
basePath: '/wd/hub',
port: 4723,
log: join(process.cwd(), 'appium-logs', 'logs.txt')
log: join(process.cwd(), 'appium-logs', 'logs.txt'),
allowInsecure: 'chromedriver_autodownload',
},
},
],
Expand Down
Loading