Skip to content

Commit 0b15629

Browse files
strakerWilcoFiersstephenmathieson
authored
fix: skip unloaded iframes for all apis (#752)
* fix: skip unloaded iframes for all apis * no loaded * Update packages/webdriverio/src/index.ts Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com> * Update packages/webdriverio/src/index.ts Co-authored-by: Stephen Mathieson <571265+stephenmathieson@users.noreply.github.com> * reject * assert properly --------- Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com> Co-authored-by: Stephen Mathieson <571265+stephenmathieson@users.noreply.github.com>
1 parent 2e621f7 commit 0b15629

File tree

13 files changed

+152
-52
lines changed

13 files changed

+152
-52
lines changed

packages/playwright/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/playwright/test/axe-playwright.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,24 @@ describe('@axe-core/playwright', () => {
437437
assert.equal(res?.status(), 200);
438438
assert.strictEqual(count, 9);
439439
});
440+
441+
it('handles unloaded iframes (e.g. loading=lazy)', async () => {
442+
const res = await page.goto(`${addr}/external/lazy-loaded-iframe.html`);
443+
444+
const results = await new AxeBuilder({ page })
445+
.options({ runOnly: ['label', 'frame-tested'] })
446+
.analyze();
447+
448+
assert.equal(res?.status(), 200);
449+
assert.lengthOf(results.incomplete, 0);
450+
assert.equal(results.violations[0].id, 'label');
451+
assert.lengthOf(results.violations[0].nodes, 1);
452+
assert.deepEqual(results.violations[0].nodes[0].target, [
453+
'#ifr-lazy',
454+
'#lazy-baz',
455+
'input'
456+
]);
457+
})
440458
});
441459

442460
describe('include/exclude', () => {

packages/puppeteer/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/puppeteer/src/utils.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import assert from 'assert';
12
import * as fs from 'fs';
23
import * as Axe from 'axe-core';
34
import { Frame } from 'puppeteer';
@@ -73,14 +74,18 @@ export async function getChildFrame(
7374
export async function assertFrameReady(frame: Frame): Promise<void> {
7475
// Wait so that we know there is an execution context.
7576
// Assume that if we have an html node we have an execution context.
76-
// Check if the page is loaded.
77-
let pageReady = false;
7877
try {
79-
pageReady = await frame.evaluate(pageIsLoaded);
78+
// Puppeteer freezes on unloaded iframes. Set a race timeout in order to handle that.
79+
// @see https://github.com/dequelabs/axe-core-npm/issues/727
80+
const timeoutPromise = new Promise((resolve, reject) => {
81+
setTimeout(() => {
82+
reject();
83+
}, 1000)
84+
});
85+
const evaluatePromise = frame.evaluate(pageIsLoaded);
86+
const readyState = await Promise.race([timeoutPromise, evaluatePromise]);
87+
assert(readyState);
8088
} catch {
81-
/* ignore */
82-
}
83-
if (!pageReady) {
8489
throw new Error('Page/Frame is not ready');
8590
}
8691
}

packages/puppeteer/test/axe-puppeteer.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,25 @@ describe('AxePuppeteer', function () {
872872
assert.equal(res?.status(), 200);
873873
assert.deepEqual(pageResults, frameResults);
874874
});
875+
876+
it('skips unloaded iframes (e.g. loading=lazy)', async () => {
877+
const res = await page.goto(`${addr}/external/lazy-loaded-iframe.html`);
878+
const results = await new AxePuppeteer(page)
879+
.options({ runOnly: ['label', 'frame-tested'] })
880+
.analyze();
881+
882+
assert.equal(res?.status(), 200);
883+
assert.equal(results.incomplete[0].id, 'frame-tested');
884+
assert.lengthOf(results.incomplete[0].nodes, 1);
885+
assert.deepEqual(results.incomplete[0].nodes[0].target, ['#ifr-lazy', '#lazy-iframe']);
886+
assert.equal(results.violations[0].id, 'label');
887+
assert.lengthOf(results.violations[0].nodes, 1);
888+
assert.deepEqual(results.violations[0].nodes[0].target, [
889+
'#ifr-lazy',
890+
'#lazy-baz',
891+
'input'
892+
]);
893+
})
875894
});
876895

877896
describe('axe.finishRun errors', () => {

packages/webdriverio/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/webdriverio/src/index.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
axeRunPartial,
1111
axeFinishRun,
1212
axeRunLegacy,
13-
configureAllowedOrigins
13+
configureAllowedOrigins,
14+
FRAME_LOAD_TIMEOUT
1415
} from './utils';
1516
import { getFilename } from 'cross-dirname';
1617
import { pathToFileURL } from 'url';
@@ -241,7 +242,21 @@ export default class AxeBuilder {
241242
return await this.runLegacy(context);
242243
}
243244

244-
const partials = await this.runPartialRecursive(context);
245+
// ensure we fail quickly if an iframe cannot be loaded (instead of waiting
246+
// the default length of 30 seconds)
247+
const { pageLoad } = await this.client.getTimeouts();
248+
this.client.setTimeout({
249+
pageLoad: FRAME_LOAD_TIMEOUT,
250+
});
251+
252+
let partials: PartialResults | null
253+
try {
254+
partials = await this.runPartialRecursive(context);
255+
} finally {
256+
this.client.setTimeout({
257+
pageLoad,
258+
});
259+
}
245260

246261
try {
247262
return await this.finishRun(partials);
@@ -300,7 +315,8 @@ export default class AxeBuilder {
300315
*/
301316

302317
private async runPartialRecursive(
303-
context: SerialContextObject
318+
context: SerialContextObject,
319+
frameStack: Element[] = []
304320
): Promise<PartialResults> {
305321
const frameContexts = await axeGetFrameContext(this.client, context);
306322
const partials: PartialResults = [
@@ -313,10 +329,16 @@ export default class AxeBuilder {
313329
assert(frame, `Expect frame of "${frameSelector}" to be defined`);
314330
await this.client.switchToFrame(frame);
315331
await axeSourceInject(this.client, this.script);
316-
partials.push(...(await this.runPartialRecursive(frameContext)));
332+
partials.push(...(await this.runPartialRecursive(frameContext, [...frameStack, frame])));
317333
} catch (error) {
334+
const [topWindow] = await this.client.getWindowHandles()
335+
await this.client.switchToWindow(topWindow)
336+
337+
for (const frameElm of frameStack) {
338+
await this.client.switchToFrame(frameElm);
339+
}
340+
318341
partials.push(null);
319-
await this.client.switchToParentFrame();
320342
}
321343
}
322344
await this.client.switchToParentFrame();

packages/webdriverio/src/utils.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import type {
1010
SerialContextObject
1111
} from 'axe-core';
1212

13+
export const FRAME_LOAD_TIMEOUT = 1000
14+
1315
/**
1416
* Validates that the client provided is WebdriverIO v5 or v6.
1517
*/
@@ -83,6 +85,7 @@ export const axeSourceInject = async (
8385
client: Browser,
8486
axeSource: string
8587
): Promise<{ runPartialSupported: boolean }> => {
88+
await assertFrameReady(client);
8689
return promisify(
8790
// Had to use executeAsync() because we could not use multiline statements in client.execute()
8891
// we were able to return a single boolean in a line but not when assigned to a variable.
@@ -98,6 +101,34 @@ export const axeSourceInject = async (
98101
);
99102
};
100103

104+
async function assertFrameReady(client: Browser): Promise<void> {
105+
// Wait so that we know there is an execution context.
106+
// Assume that if we have an html node we have an execution context.
107+
try {
108+
/*
109+
When using the devtools protocol trying to call
110+
client.execute() on an unloaded iframe would cause
111+
the code to hang indefinitely since it is using
112+
Puppeteer which freezes on unloaded iframes. Set a
113+
race timeout in order to handle that. Code taken
114+
from our @axe-core/puppeteer utils function.
115+
@see https://github.com/dequelabs/axe-core-npm/issues/727
116+
*/
117+
const timeoutPromise = new Promise((resolve, reject) => {
118+
setTimeout(() => {
119+
reject();
120+
}, FRAME_LOAD_TIMEOUT)
121+
});
122+
const executePromise = client.execute(() => {
123+
return document.readyState === 'complete'
124+
});
125+
const readyState = await Promise.race([timeoutPromise, executePromise]);
126+
assert(readyState);
127+
} catch {
128+
throw new Error('Page/Frame is not ready');
129+
}
130+
}
131+
101132
export const axeRunPartial = (
102133
client: Browser,
103134
context?: SerialContextObject,

packages/webdriverio/test/axe-webdriverio.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,40 @@ describe('@axe-core/webdriverio', () => {
850850
normalResults.testEngine.name = legacyResults.testEngine.name;
851851
assert.deepEqual(normalResults, legacyResults);
852852
});
853+
854+
it('handles unloaded iframes (e.g. loading=lazy)', async () => {
855+
await client.url(`${addr}/lazy-loaded-iframe.html`);
856+
const title = await client.getTitle();
857+
858+
const results = await new AxeBuilder({client})
859+
.options({ runOnly: ['label', 'frame-tested'] })
860+
.analyze();
861+
862+
assert.notEqual(title, 'Error');
863+
assert.equal(results.incomplete[0].id, 'frame-tested');
864+
assert.lengthOf(results.incomplete[0].nodes, 1);
865+
assert.deepEqual(results.incomplete[0].nodes[0].target, ['#ifr-lazy', '#lazy-iframe']);
866+
assert.equal(results.violations[0].id, 'label');
867+
assert.lengthOf(results.violations[0].nodes, 1);
868+
assert.deepEqual(results.violations[0].nodes[0].target, [
869+
'#ifr-lazy',
870+
'#lazy-baz',
871+
'input'
872+
]);
873+
});
874+
875+
it('resets pageLoad timeout to user setting', async () => {
876+
await client.url(`${addr}/lazy-loaded-iframe.html`);
877+
client.setTimeout({ pageLoad: 500 })
878+
const title = await client.getTitle();
879+
880+
const results = await new AxeBuilder({client})
881+
.options({ runOnly: ['label', 'frame-tested'] })
882+
.analyze();
883+
884+
const timeout = await client.getTimeouts();
885+
assert.equal(timeout.pageLoad, 500);
886+
});
853887
});
854888

855889
describe('logOrRethrowError', () => {

packages/webdriverjs/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/webdriverjs/test/axe-webdriverjs.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ describe('@axe-core/webdriverjs', () => {
398398
});
399399

400400
it('skips unloaded iframes (e.g. loading=lazy)', async () => {
401-
await driver.get(`${addr}/lazy-loaded-iframe.html`);
401+
await driver.get(`${addr}/external/lazy-loaded-iframe.html`);
402402
const title = await driver.getTitle();
403403

404404
const results = await new AxeBuilder(driver)
@@ -408,18 +408,18 @@ describe('@axe-core/webdriverjs', () => {
408408
assert.notEqual(title, 'Error');
409409
assert.equal(results.incomplete[0].id, 'frame-tested');
410410
assert.lengthOf(results.incomplete[0].nodes, 1);
411-
assert.deepEqual(results.incomplete[0].nodes[0].target, ['#parent', '#lazy-iframe']);
411+
assert.deepEqual(results.incomplete[0].nodes[0].target, ['#ifr-lazy', '#lazy-iframe']);
412412
assert.equal(results.violations[0].id, 'label');
413413
assert.lengthOf(results.violations[0].nodes, 1);
414414
assert.deepEqual(results.violations[0].nodes[0].target, [
415-
'#parent',
416-
'#child',
415+
'#ifr-lazy',
416+
'#lazy-baz',
417417
'input'
418418
]);
419419
})
420420

421421
it('resets pageLoad timeout to user setting', async () => {
422-
await driver.get(`${addr}/lazy-loaded-iframe.html`);
422+
await driver.get(`${addr}/external/lazy-loaded-iframe.html`);
423423
driver.manage().setTimeouts({ pageLoad: 500 })
424424
const title = await driver.getTitle();
425425

packages/webdriverjs/test/fixtures/iframe-lazy-1.html

Lines changed: 0 additions & 15 deletions
This file was deleted.

packages/webdriverjs/test/fixtures/lazy-loaded-iframe.html

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)