Repository navigation
feat!: target Appium 4 beta #442
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
90a310a
7a954bb
1df09e3
eab82db
5f2e7fd
49f4726
6c32ce9
b355b67
209baf1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,14 @@ | ||
| import {errors, isErrorType} from 'appium/driver.js'; | ||
| import {util} from 'appium/support.js'; | ||
| import {ConditionUnmetError} from 'asyncbox'; | ||
|
|
||
| import type {Mac2Driver} from '../driver.js'; | ||
|
|
||
| // Older asyncbox (possibly bundled with the base driver) throws a plain Error | ||
| function isConditionUnmet(e: unknown): boolean { | ||
| return e instanceof ConditionUnmetError || /Condition unmet/.test((e as Error)?.message ?? ''); | ||
| } | ||
|
|
||
| /** | ||
| * This is needed to make lookup by image working | ||
| * | ||
|
|
@@ -32,8 +39,44 @@ export async function findElOrEls( | |
| normalizedStrategy = 'class chain'; | ||
| } | ||
|
|
||
| return await this.wda.proxy.command(endpoint, 'POST', { | ||
| using: normalizedStrategy, | ||
| value: selector, | ||
| }); | ||
| const doFind = async (): Promise<any> => | ||
| await this.wda.proxy.command(endpoint, 'POST', { | ||
| using: normalizedStrategy, | ||
| value: selector, | ||
| }); | ||
| if (!this.implicitWaitMs) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. The check is now |
||
| return await doFind(); | ||
| } | ||
|
|
||
| // The WDA server ignores the /timeouts API, so the implicit wait is applied here | ||
| let result: any; | ||
| let lastError: Error | undefined; | ||
| // A lookup error other than NoSuchElementError stops the wait and must reach the caller as is | ||
| let lookupError: unknown; | ||
| try { | ||
| await this.implicitWaitForCondition(async () => { | ||
| try { | ||
| result = await doFind(); | ||
| } catch (e) { | ||
| if (!isErrorType(e, errors.NoSuchElementError)) { | ||
| lookupError = e; | ||
| throw e; | ||
| } | ||
| lastError = e as Error; | ||
| return false; | ||
| } | ||
| return !mult || (Array.isArray(result) && result.length > 0); | ||
| }); | ||
| } catch (e) { | ||
| if (lookupError !== undefined) { | ||
| throw lookupError; | ||
| } | ||
| if (!isConditionUnmet(e)) { | ||
| throw e; | ||
| } | ||
| if (!mult && lastError) { | ||
| throw lastError; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,13 +3,13 @@ import type { | |
| HTTPMethod, | ||
| HTTPBody, | ||
| DefaultCreateSessionResult, | ||
| DriverData, | ||
| InitialOpts, | ||
| StringRecord, | ||
| ExternalDriver, | ||
| DriverCaps, | ||
| DriverOpts, | ||
| W3CDriverCaps, | ||
| TimeoutValue, | ||
| } from '@appium/types'; | ||
| import type {Chromedriver} from 'appium-chromedriver'; | ||
| import {BaseDriver, DeviceSettings, errors} from 'appium/driver.js'; | ||
|
|
@@ -217,27 +217,40 @@ export class Mac2Driver | |
| } | ||
|
|
||
| // Timeouts belong to the session: keep them here and mirror to Chromedriver in a web view | ||
| override async implicitWaitW3C(ms: number): Promise<void> { | ||
| await super.implicitWaitW3C(ms); | ||
| await this.chromedriver?.jwproxy.command('/timeouts', 'POST', {implicit: this.implicitWaitMs}); | ||
| } | ||
|
|
||
| override async pageLoadTimeoutW3C(ms: number): Promise<void> { | ||
| if (!this.chromedriver) { | ||
| return await super.pageLoadTimeoutW3C(ms); | ||
| override async timeouts( | ||
| type?: string, | ||
| ms?: number | string, | ||
| script?: TimeoutValue, | ||
| pageLoad?: TimeoutValue, | ||
| implicit?: TimeoutValue, | ||
| command?: number, | ||
| ): Promise<void> { | ||
| if (type && ms !== undefined) { | ||
| // Deprecated 'type'/'ms' form: normalize to the W3C one | ||
| const legacyMs = this.parseTimeoutArgument(ms); | ||
| ({script, pageLoad, implicit} = { | ||
| script: type === 'script' ? legacyMs : undefined, | ||
| pageLoad: type === 'page load' ? legacyMs : undefined, | ||
| implicit: type === 'implicit' ? legacyMs : undefined, | ||
| }); | ||
| command = type === 'command' ? legacyMs : undefined; | ||
| type = ms = undefined; | ||
|
mykola-mokhnach marked this conversation as resolved.
Resolved
|
||
| } | ||
| const pageLoad = this.parseTimeoutArgument(ms); | ||
| await this.chromedriver.jwproxy.command('/timeouts', 'POST', {pageLoad}); | ||
| this._pageLoadTimeoutMs = pageLoad; | ||
| } | ||
|
|
||
| override async scriptTimeoutW3C(ms: number): Promise<void> { | ||
| if (!this.chromedriver) { | ||
| return await super.scriptTimeoutW3C(ms); | ||
| if (pageLoad !== undefined && !this.chromedriver) { | ||
| throw new errors.NotImplementedError('Not implemented: page load timeout is only supported in web view contexts'); | ||
| } | ||
| await super.timeouts(type, ms, script, pageLoad, implicit, command); | ||
| if (script !== undefined) { | ||
| this._scriptTimeoutMs = script; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we distinguish an unset script timeout from an explicitly supplied
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of a separate unset sentinel, I dropped the driver's own |
||
| } | ||
| if (pageLoad !== undefined) { | ||
| this._pageLoadTimeoutMs = pageLoad; | ||
| } | ||
| for (const [name, value] of Object.entries({script, pageLoad, implicit})) { | ||
| if (value !== undefined) { | ||
| await this.chromedriver?.jwproxy.command('/timeouts', 'POST', {[name]: value}); | ||
| } | ||
| } | ||
| const script = this.parseTimeoutArgument(ms); | ||
| await this.chromedriver.jwproxy.command('/timeouts', 'POST', {script}); | ||
| this._scriptTimeoutMs = script; | ||
| } | ||
|
|
||
| isWebviewEnabled(): boolean { | ||
|
|
@@ -281,12 +294,9 @@ export class Mac2Driver | |
| } | ||
|
|
||
| override async createSession( | ||
| w3cCaps1: W3CMac2DriverCaps, | ||
| w3cCaps2?: W3CMac2DriverCaps, | ||
| w3cCaps3?: W3CMac2DriverCaps, | ||
| driverData?: DriverData[], | ||
| w3cCapabilities: W3CMac2DriverCaps, | ||
| ): Promise<DefaultCreateSessionResult<Mac2Constraints>> { | ||
| const [sessionId, caps] = await super.createSession(w3cCaps1, w3cCaps2, w3cCaps3, driverData); | ||
| const [sessionId, caps] = await super.createSession(w3cCapabilities); | ||
| this._wda = WDA_MAC_SERVER; | ||
| this.caps = caps as Mac2DriverCaps; | ||
| // oxlint-disable-next-line no-self-assign -- narrows this.opts' type for the rest of the method | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import {describe, it} from 'node:test'; | ||
|
|
||
| import {errors} from 'appium/driver.js'; | ||
|
|
||
| import {Mac2Driver} from '../../lib/driver.js'; | ||
|
|
||
| describe('findElOrEls', () => { | ||
| function makeDriver(responses: (() => unknown)[], implicitWaitMs: number) { | ||
| const driver = new Mac2Driver({} as any); | ||
| const calls: string[] = []; | ||
| (driver as any)._wda = { | ||
| proxy: { | ||
| command: async (url: string) => { | ||
| calls.push(url); | ||
| const respond = responses[Math.min(calls.length, responses.length) - 1]; | ||
| return respond(); | ||
| }, | ||
| }, | ||
| }; | ||
| driver.setImplicitWait(implicitWaitMs); | ||
| return {driver, calls}; | ||
| } | ||
|
|
||
| const notFound = () => { | ||
| throw new errors.NoSuchElementError(); | ||
| }; | ||
|
|
||
| it('sends a single request if the implicit wait is not set', async () => { | ||
| const {driver, calls} = makeDriver([notFound], 0); | ||
| await assert.rejects(driver.findElOrEls('accessibility id', 'foo', false), errors.NoSuchElementError); | ||
| assert.equal(calls.length, 1); | ||
| }); | ||
|
|
||
| it('retries a single element lookup within the implicit wait', async () => { | ||
| const element = {ELEMENT: '1'}; | ||
| const {driver, calls} = makeDriver([notFound, () => element], 2000); | ||
| assert.deepEqual(await driver.findElOrEls('accessibility id', 'foo', false), element); | ||
| assert.equal(calls.length, 2); | ||
| }); | ||
|
|
||
| it('retries a multiple elements lookup within the implicit wait', async () => { | ||
| const elements = [{ELEMENT: '1'}]; | ||
| const {driver, calls} = makeDriver([() => [], () => elements], 2000); | ||
| assert.deepEqual(await driver.findElOrEls('accessibility id', 'foo', true), elements); | ||
| assert.equal(calls.length, 2); | ||
| }); | ||
|
|
||
| it('throws the lookup error once the implicit wait expires', async () => { | ||
| const {driver, calls} = makeDriver([notFound], 600); | ||
| await assert.rejects(driver.findElOrEls('accessibility id', 'foo', false), errors.NoSuchElementError); | ||
| assert.ok(calls.length > 1); | ||
| }); | ||
|
|
||
| it('returns an empty list once the implicit wait expires', async () => { | ||
| const {driver, calls} = makeDriver([() => []], 600); | ||
| assert.deepEqual(await driver.findElOrEls('accessibility id', 'foo', true), []); | ||
| assert.ok(calls.length > 1); | ||
| }); | ||
|
|
||
| it('does not retry on other errors', async () => { | ||
| const {driver, calls} = makeDriver( | ||
| [ | ||
| () => { | ||
| throw new errors.InvalidSelectorError(); | ||
| }, | ||
| ], | ||
| 2000, | ||
| ); | ||
| await assert.rejects(driver.findElOrEls('xpath', '//', false), errors.InvalidSelectorError); | ||
| assert.equal(calls.length, 1); | ||
| }); | ||
|
|
||
| it('throws a lookup error whose message looks like the wait timeout', async () => { | ||
| const {driver, calls} = makeDriver( | ||
| [ | ||
| () => { | ||
| throw new errors.InvalidSelectorError('Invalid selector: Condition unmet'); | ||
| }, | ||
| ], | ||
| 2000, | ||
| ); | ||
| await assert.rejects(driver.findElOrEls('xpath', '//', false), errors.InvalidSelectorError); | ||
| await assert.rejects(driver.findElOrEls('xpath', '//', true), errors.InvalidSelectorError); | ||
| assert.equal(calls.length, 2); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.