diff --git a/src/driver.ts b/src/driver.ts index 388faed..bcc6aef 100644 --- a/src/driver.ts +++ b/src/driver.ts @@ -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 { // @ts-ignore public proxydriver: XCUITestDriver | AndroidUiautomator2Driver; public flutterPort: number | null | undefined; private internalCaps: DriverCaps | 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; @@ -193,12 +212,44 @@ export class AppiumFlutterDriver extends BaseDriver { } 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> { @@ -382,8 +433,20 @@ export class AppiumFlutterDriver extends BaseDriver { 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() { @@ -400,6 +463,8 @@ export class AppiumFlutterDriver extends BaseDriver { 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 }, diff --git a/test/specs/test.e2e.js b/test/specs/test.e2e.js index 027ddc2..6408b08 100644 --- a/test/specs/test.e2e.js +++ b/test/specs/test.e2e.js @@ -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); @@ -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'; @@ -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'); + }); }); diff --git a/wdio.conf.ts b/wdio.conf.ts index 8fd7036..17324db 100644 --- a/wdio.conf.ts +++ b/wdio.conf.ts @@ -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', }, }, ],