From 510cfcffdc0c37349ae2e597b80c62d8d70799fb Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Tue, 6 Jun 2023 15:56:11 -0600 Subject: [PATCH 1/4] fix(webdriverjs): skip unloaded iframes --- packages/webdriverjs/src/index.ts | 39 +++++++++++++++---- .../webdriverjs/test/axe-webdriverjs.spec.ts | 34 ++++++++++++++++ .../test/fixtures/iframe-lazy-1.html | 15 +++++++ .../test/fixtures/lazy-loaded-iframe.html | 14 +++++++ 4 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 packages/webdriverjs/test/fixtures/iframe-lazy-1.html create mode 100644 packages/webdriverjs/test/fixtures/lazy-loaded-iframe.html diff --git a/packages/webdriverjs/src/index.ts b/packages/webdriverjs/src/index.ts index 40ea4f9b..c99bb501 100644 --- a/packages/webdriverjs/src/index.ts +++ b/packages/webdriverjs/src/index.ts @@ -1,4 +1,4 @@ -import { WebDriver } from 'selenium-webdriver'; +import { WebDriver, WebElement } from 'selenium-webdriver'; import axe, { RunOptions, Spec, @@ -30,6 +30,7 @@ export default class AxeBuilder { private builderOptions: BuilderOptions; private legacyMode = false; private errorUrl: string; + private frameStack: WebElement[] constructor( driver: WebDriver, @@ -45,6 +46,7 @@ export default class AxeBuilder { this.builderOptions = builderOptions || {}; this.errorUrl = 'https://github.com/dequelabs/axe-core-npm/blob/develop/packages/webdriverjs/error-handling.md'; + this.frameStack = []; } /** @@ -174,7 +176,15 @@ 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 }); + this.frameStack = []; + + const partials = await this.runPartialRecursive(context, null, true); + + this.driver.manage().setTimeouts({ pageLoad }); try { return await this.finishRun(partials); @@ -212,6 +222,7 @@ export default class AxeBuilder { */ private async runPartialRecursive( context: SerialContextObject, + frame: null | WebElement, initiator = false ): Promise { if (!initiator) { @@ -223,18 +234,31 @@ export default class AxeBuilder { await axeRunPartial(this.driver, context, this.option) ]; + if (frame) { + this.frameStack.push(frame); + } + 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, frame))); + this.frameStack.pop(); await this.driver.switchTo().parentFrame(); } catch { - if (switchedFrame) { - await this.driver.switchTo().parentFrame(); + /* + When switchTo().frame() fails using chromedriver (but not gekodriver) + 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 this.frameStack) { + await this.driver.switchTo().frame(frameElm); } + partials.push('null'); } } @@ -248,6 +272,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

+
+ +
+
+ + From ed00ede2482c7d739e1d1a921f66917b8345d1e5 Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 8 Jun 2023 08:58:31 -0600 Subject: [PATCH 2/4] suggestions --- packages/webdriverjs/src/index.ts | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/webdriverjs/src/index.ts b/packages/webdriverjs/src/index.ts index c99bb501..c65106bd 100644 --- a/packages/webdriverjs/src/index.ts +++ b/packages/webdriverjs/src/index.ts @@ -1,4 +1,4 @@ -import { WebDriver, WebElement } from 'selenium-webdriver'; +import { WebDriver, WebElement, By } from 'selenium-webdriver'; import axe, { RunOptions, Spec, @@ -30,7 +30,6 @@ export default class AxeBuilder { private builderOptions: BuilderOptions; private legacyMode = false; private errorUrl: string; - private frameStack: WebElement[] constructor( driver: WebDriver, @@ -46,7 +45,6 @@ export default class AxeBuilder { this.builderOptions = builderOptions || {}; this.errorUrl = 'https://github.com/dequelabs/axe-core-npm/blob/develop/packages/webdriverjs/error-handling.md'; - this.frameStack = []; } /** @@ -180,11 +178,13 @@ export default class AxeBuilder { // the default length of 30 seconds) const { pageLoad } = await this.driver.manage().getTimeouts(); this.driver.manage().setTimeouts({ pageLoad: 1000 }); - this.frameStack = []; - const partials = await this.runPartialRecursive(context, null, true); - - this.driver.manage().setTimeouts({ pageLoad }); + let partials: string[] + try { + partials = await this.runPartialRecursive(context); + } finally { + this.driver.manage().setTimeouts({ pageLoad }); + } try { return await this.finishRun(partials); @@ -222,10 +222,9 @@ export default class AxeBuilder { */ private async runPartialRecursive( context: SerialContextObject, - frame: null | WebElement, - 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 @@ -234,20 +233,15 @@ export default class AxeBuilder { await axeRunPartial(this.driver, context, this.option) ]; - if (frame) { - this.frameStack.push(frame); - } - for (const { frameContext, frameSelector, frame } of frameContexts) { try { assert(frame, `Expect frame of "${frameSelector}" to be defined`); await this.driver.switchTo().frame(frame); - partials.push(...(await this.runPartialRecursive(frameContext, frame))); - this.frameStack.pop(); + partials.push(...(await this.runPartialRecursive(frameContext, [...frameStack, frame]))); await this.driver.switchTo().parentFrame(); } catch { /* - When switchTo().frame() fails using chromedriver (but not gekodriver) + 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 @@ -255,7 +249,7 @@ export default class AxeBuilder { const win = await this.driver.getWindowHandle(); await this.driver.switchTo().window(win); - for (const frameElm of this.frameStack) { + for (const frameElm of frameStack) { await this.driver.switchTo().frame(frameElm); } From 04ab35064e60ce0ef53ced540711761122d4a6ac Mon Sep 17 00:00:00 2001 From: Steven Lambert Date: Thu, 8 Jun 2023 08:59:16 -0600 Subject: [PATCH 3/4] remove By --- packages/webdriverjs/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webdriverjs/src/index.ts b/packages/webdriverjs/src/index.ts index c65106bd..7718d92a 100644 --- a/packages/webdriverjs/src/index.ts +++ b/packages/webdriverjs/src/index.ts @@ -1,4 +1,4 @@ -import { WebDriver, WebElement, By } from 'selenium-webdriver'; +import { WebDriver, WebElement } from 'selenium-webdriver'; import axe, { RunOptions, Spec, From 0daf70c54f4ff6d33953dacdd6d5a23d63465aa6 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Thu, 8 Jun 2023 11:46:41 -0600 Subject: [PATCH 4/4] Update packages/webdriverjs/src/index.ts Co-authored-by: Gabe <41127686+Zidious@users.noreply.github.com> --- packages/webdriverjs/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webdriverjs/src/index.ts b/packages/webdriverjs/src/index.ts index 7718d92a..0ce555c6 100644 --- a/packages/webdriverjs/src/index.ts +++ b/packages/webdriverjs/src/index.ts @@ -1,4 +1,4 @@ -import { WebDriver, WebElement } from 'selenium-webdriver'; +import type { WebDriver, WebElement } from 'selenium-webdriver'; import axe, { RunOptions, Spec,