diff --git a/packages/webdriverjs/src/index.ts b/packages/webdriverjs/src/index.ts index 40ea4f9b..0ce555c6 100644 --- a/packages/webdriverjs/src/index.ts +++ b/packages/webdriverjs/src/index.ts @@ -1,4 +1,4 @@ -import { WebDriver } from 'selenium-webdriver'; +import type { WebDriver, WebElement } from 'selenium-webdriver'; import axe, { RunOptions, Spec, @@ -174,7 +174,17 @@ export default class AxeBuilder { return this.runLegacy(context); } - const partials = await this.runPartialRecursive(context, true); + // ensure we fail quickly if an iframe cannot be loaded (instead of waiting + // the default length of 30 seconds) + const { pageLoad } = await this.driver.manage().getTimeouts(); + this.driver.manage().setTimeouts({ pageLoad: 1000 }); + + let partials: string[] + try { + partials = await this.runPartialRecursive(context); + } finally { + this.driver.manage().setTimeouts({ pageLoad }); + } try { return await this.finishRun(partials); @@ -212,9 +222,9 @@ export default class AxeBuilder { */ private async runPartialRecursive( context: SerialContextObject, - initiator = false + frameStack: WebElement[] = [] ): Promise { - if (!initiator) { + if (frameStack.length) { await axeSourceInject(this.driver, this.axeSource, this.config); } // IMPORTANT: axeGetFrameContext MUST be called before axeRunPartial @@ -224,17 +234,25 @@ export default class AxeBuilder { ]; for (const { frameContext, frameSelector, frame } of frameContexts) { - let switchedFrame = false; try { assert(frame, `Expect frame of "${frameSelector}" to be defined`); await this.driver.switchTo().frame(frame); - switchedFrame = true; - partials.push(...(await this.runPartialRecursive(frameContext))); + partials.push(...(await this.runPartialRecursive(frameContext, [...frameStack, frame]))); await this.driver.switchTo().parentFrame(); } catch { - if (switchedFrame) { - await this.driver.switchTo().parentFrame(); + /* + When switchTo().frame() fails using chromedriver (but not geckodriver) + it puts the driver into a really bad state that is impossible to + recover from. So we need to switch back to the main window and then + go back to the desired iframe context + */ + const win = await this.driver.getWindowHandle(); + await this.driver.switchTo().window(win); + + for (const frameElm of frameStack) { + await this.driver.switchTo().frame(frameElm); } + partials.push('null'); } } @@ -248,6 +266,7 @@ export default class AxeBuilder { const { driver, axeSource, config, option } = this; const win = await driver.getWindowHandle(); + await driver.switchTo().window(win); try { await driver.executeScript(`window.open('about:blank')`); diff --git a/packages/webdriverjs/test/axe-webdriverjs.spec.ts b/packages/webdriverjs/test/axe-webdriverjs.spec.ts index 35184777..27ae5c35 100644 --- a/packages/webdriverjs/test/axe-webdriverjs.spec.ts +++ b/packages/webdriverjs/test/axe-webdriverjs.spec.ts @@ -396,6 +396,40 @@ describe('@axe-core/webdriverjs', () => { normalResults.testEngine.name = legacyResults.testEngine.name; assert.deepEqual(normalResults, legacyResults); }); + + it('skips unloaded iframes (e.g. loading=lazy)', async () => { + await driver.get(`${addr}/lazy-loaded-iframe.html`); + const title = await driver.getTitle(); + + const results = await new AxeBuilder(driver) + .options({ runOnly: ['label', 'frame-tested'] }) + .analyze(); + + assert.notEqual(title, 'Error'); + assert.equal(results.incomplete[0].id, 'frame-tested'); + assert.lengthOf(results.incomplete[0].nodes, 1); + assert.deepEqual(results.incomplete[0].nodes[0].target, ['#parent', '#lazy-iframe']); + assert.equal(results.violations[0].id, 'label'); + assert.lengthOf(results.violations[0].nodes, 1); + assert.deepEqual(results.violations[0].nodes[0].target, [ + '#parent', + '#child', + 'input' + ]); + }) + + it('resets pageLoad timeout to user setting', async () => { + await driver.get(`${addr}/lazy-loaded-iframe.html`); + driver.manage().setTimeouts({ pageLoad: 500 }) + const title = await driver.getTitle(); + + const results = await new AxeBuilder(driver) + .options({ runOnly: ['label', 'frame-tested'] }) + .analyze(); + + const timeout = await driver.manage().getTimeouts(); + assert.equal(timeout.pageLoad, 500); + }) }); describe('withRules', () => { diff --git a/packages/webdriverjs/test/fixtures/iframe-lazy-1.html b/packages/webdriverjs/test/fixtures/iframe-lazy-1.html new file mode 100644 index 00000000..65aea201 --- /dev/null +++ b/packages/webdriverjs/test/fixtures/iframe-lazy-1.html @@ -0,0 +1,15 @@ + + + + Lazy Loading Iframe Parent + + +
+

iframe context test

+
+ +
+
+ + \ No newline at end of file diff --git a/packages/webdriverjs/test/fixtures/lazy-loaded-iframe.html b/packages/webdriverjs/test/fixtures/lazy-loaded-iframe.html new file mode 100644 index 00000000..53969219 --- /dev/null +++ b/packages/webdriverjs/test/fixtures/lazy-loaded-iframe.html @@ -0,0 +1,14 @@ + + + + Lazy Loading Iframe Root + + +
+

iframe context test

+
+ +
+
+ +