Skip to content

Commit

Permalink
fix: work with extremely large results (dequelabs#667)
Browse files Browse the repository at this point in the history
* fix: work with extremely large partial results

* fix tests

* try again
  • Loading branch information
straker authored Feb 13, 2023
1 parent 31bb4c5 commit 395d5fc
Show file tree
Hide file tree
Showing 17 changed files with 232 additions and 71 deletions.
4 changes: 2 additions & 2 deletions packages/playwright/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions packages/playwright/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import axeCore from 'axe-core';
declare global {
interface Window {
axe: typeof axeCore;
partialResults: string;
}
}
export const axeGetFrameContexts = ({
Expand All @@ -36,8 +37,12 @@ export const axeRunPartial = ({
};

export const axeFinishRun = ({
partialResults,
options
}: FinishRunParams): Promise<AxeResults> => {
return window.axe.finishRun(partialResults, options);
return window.axe.finishRun(JSON.parse(window.partialResults), options);
};

export function chunkResultString(chunk: string) {
window.partialResults ??= '';
window.partialResults += chunk;
}
20 changes: 18 additions & 2 deletions packages/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import {
axeFinishRun,
axeGetFrameContexts,
axeRunPartial,
axeShadowSelect
axeShadowSelect,
chunkResultString
} from './browser';
import AxePartialRunner from './AxePartialRunner';

Expand Down Expand Up @@ -283,9 +284,24 @@ export default class AxeBuilder {

blankPage.evaluate(this.script());
blankPage.evaluate(await this.axeConfigure());

// evaluate has a size limit on the number of characters so we'll need
// to split partialResults into chunks if it exceeds that limit.
const sizeLimit = 60_000_000;
const partialString = JSON.stringify(partialResults);

async function chunkResults(result: string): Promise<void> {
const chunk = result.substring(0, sizeLimit);
await blankPage.evaluate(chunkResultString, chunk);

if (result.length > sizeLimit) {
return await chunkResults(result.substr(sizeLimit));
}
}

await chunkResults(partialString);
return await blankPage
.evaluate(axeFinishRun, {
partialResults,
options
})
.finally(async () => {
Expand Down
1 change: 0 additions & 1 deletion packages/playwright/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,5 @@ export interface RunPartialParams {
}

export interface FinishRunParams {
partialResults: PartialResults[];
options: RunOptions;
}
20 changes: 20 additions & 0 deletions packages/playwright/tests/axe-playwright.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ describe('@axe-core/playwright', () => {
path.join(externalPath, 'axe-force-legacy.js'),
'utf8'
);
const axeLargePartial = fs.readFileSync(
path.join(externalPath, 'axe-large-partial.js'),
'utf8'
);

before(async () => {
const app = express();
Expand Down Expand Up @@ -118,6 +122,22 @@ describe('@axe-core/playwright', () => {
assert.isUndefined(err);
});

it('handles large results', async function () {
/* this test handles a large amount of partial results a timeout may be required */
this.timeout(100_000);
const res = await await page.goto(`${addr}/external/index.html`);

assert.equal(res?.status(), 200);

const results = await new AxeBuilder({
page,
axeSource: axeSource + axeLargePartial
}).analyze();

assert.lengthOf(results.passes, 1);
assert.equal(results.passes[0].id, 'duplicate-id');
});

it('reports frame-tested', async () => {
const res = await page.goto(`${addr}/external/crash-parent.html`);
const results = await new AxeBuilder({
Expand Down
4 changes: 2 additions & 2 deletions packages/puppeteer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 19 additions & 2 deletions packages/puppeteer/src/axePuppeteer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
axeFinishRun,
axeConfigure,
axeRunLegacy,
axeRunPartialSupport
axeRunPartialSupport,
chunkResultString
} from './browser';
import { AnalyzeCB, PartialResults } from './types';
import { iframeSelector, injectJS } from './legacy';
Expand Down Expand Up @@ -280,8 +281,24 @@ export class AxePuppeteer {
);

await frameSourceInject(blankPage.mainFrame(), axeSource, config);

// evaluate has a size limit on the number of characters so we'll need
// to split partialResults into chunks if it exceeds that limit.
const sizeLimit = 60_000_000;
const partialString = JSON.stringify(partialResults);

async function chunkResults(result: string): Promise<void> {
const chunk = result.substring(0, sizeLimit);
await blankPage.evaluate(chunkResultString, chunk);

if (result.length > sizeLimit) {
return await chunkResults(result.substr(sizeLimit));
}
}

await chunkResults(partialString);
return await blankPage
.evaluate(axeFinishRun, partialResults, axeOptions)
.evaluate(axeFinishRun, axeOptions)
.finally(async () => {
await blankPage.close();
});
Expand Down
13 changes: 8 additions & 5 deletions packages/puppeteer/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ declare global {
// tslint:disable-next-line:interface-name
interface Window {
axe: typeof Axe;
partialResults: string;
}
}

Expand Down Expand Up @@ -48,11 +49,8 @@ export function axeRunPartial(
return window.axe.runPartial(context, options);
}

export function axeFinishRun(
partials: PartialResults,
options: Axe.RunOptions
): Promise<Axe.AxeResults> {
return window.axe.finishRun(partials, options);
export function axeFinishRun(options: Axe.RunOptions): Promise<Axe.AxeResults> {
return window.axe.finishRun(JSON.parse(window.partialResults), options);
}

// Defined at top-level to clarify that it can't capture variables from outer scope.
Expand All @@ -62,3 +60,8 @@ export function axeRunLegacy(
): Promise<Axe.AxeResults> {
return window.axe.run(context || document, options || {});
}

export function chunkResultString(chunk: string) {
window.partialResults ??= '';
window.partialResults += chunk;
}
21 changes: 21 additions & 0 deletions packages/puppeteer/test/axePuppeteer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('AxePuppeteer', function () {
let axeSource: string;
let axeCrasherSource: string;
let axeForceLegacy: string;
let axeLargePartial: string;

before(async () => {
const axePath = require.resolve('axe-core');
Expand All @@ -46,6 +47,10 @@ describe('AxePuppeteer', function () {
path.join(externalPath, 'axe-force-legacy.js'),
'utf8'
);
axeLargePartial = fs.readFileSync(
path.join(externalPath, 'axe-large-partial.js'),
'utf8'
);
});

before(async () => {
Expand Down Expand Up @@ -171,6 +176,22 @@ describe('AxePuppeteer', function () {
assert.isUndefined(err);
});

it('handles large results', async function () {
/* this test handles a large amount of partial results a timeout may be required */
this.timeout(50_000);
const res = await await page.goto(`${addr}/external/index.html`);

assert.equal(res?.status(), 200);

const results = await new AxePuppeteer(
page,
axeSource + axeLargePartial
).analyze();

assert.lengthOf(results.passes, 1);
assert.equal(results.passes[0].id, 'duplicate-id');
});

it('returns the same results from runPartial as from legacy mode', async () => {
const res = await page.goto(`${addr}/external/nested-iframes.html`);
const legacyResults = await new AxePuppeteer(
Expand Down
4 changes: 2 additions & 2 deletions packages/webdriverio/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions packages/webdriverio/src/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ describe('@axe-core/webdriverio', () => {
path.join(axeTestFixtures, 'axe-force-legacy.js'),
'utf8'
);
const axeLargePartial = fs.readFileSync(
path.join(axeTestFixtures, 'axe-large-partial.js'),
'utf8'
);

beforeEach(async () => {
const app = express();
Expand Down Expand Up @@ -482,6 +486,20 @@ describe('@axe-core/webdriverio', () => {
assert.isNull(error);
});

it('handles large results', async function () {
/* this test handles a large amount of partial results a timeout may be required */
this.timeout(100_000);
await client.url(`${addr}/external/index.html`);

const results = await new AxeBuilder({
client,
axeSource: axeSource + axeLargePartial
}).analyze();

assert.lengthOf(results.passes, 1);
assert.equal(results.passes[0].id, 'duplicate-id');
});

it('returns correct results metadata', async () => {
await client.url(`${addr}/index.html`);
const title = await client.getTitle();
Expand Down
41 changes: 33 additions & 8 deletions packages/webdriverio/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,23 +165,48 @@ export const axeFinishRun = (
partialResults: PartialResults,
options: RunOptions
): Promise<AxeResults> => {
return promisify(
client
.executeAsync<string, never>(
`var callback = arguments[arguments.length - 1];
// executeScript has a size limit of ~32 million characters so we'll need
// to split partialResults into chunks if it exceeds that limit.
// since we need to stringify twice we need to leave room for the double escaped quotes
const sizeLimit = 15_000_000;
const partialString = JSON.stringify(
partialResults.map(res => JSON.stringify(res))
);
function chunkResults(result: string): Promise<void> {
const chunk = JSON.stringify(result.substring(0, sizeLimit));
return promisify(
client.execute(
`
window.partialResults ??= '';
window.partialResults += ${chunk};
`
)
).then(() => {
if (result.length > sizeLimit) {
return chunkResults(result.substr(sizeLimit));
}
});
}

return chunkResults(partialString)
.then(() => {
return promisify(
client.executeAsync<string, never>(
`var callback = arguments[arguments.length - 1];
${axeSource};
window.axe.configure({
branding: { application: 'webdriverio' }
});
var partialResults = ${JSON.stringify(partialResults)};
var partialResults = JSON.parse(window.partialResults).map(res => JSON.parse(res));
var options = ${JSON.stringify(options || {})};
window.axe.finishRun(partialResults, options).then(function (axeResults) {
callback(JSON.stringify(axeResults))
});`
)
.then((r: string) => deserialize<AxeResults>(r))
);
)
);
})
.then((r: string) => deserialize<AxeResults>(r));
};

export const configureAllowedOrigins = (
Expand Down
4 changes: 2 additions & 2 deletions packages/webdriverjs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 395d5fc

Please sign in to comment.