Skip to content
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

fix(reporter): Run inside isolated contexts #3129

Merged
merged 3 commits into from
Aug 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
root: true,
extends: ['prettier'],
parserOptions: {
ecmaVersion: 2021
Expand Down Expand Up @@ -70,6 +71,10 @@ module.exports = {
overrides: [
{
files: ['lib/**/*.js'],
excludedFiles: [
'lib/core/reporters/**/*.js',
'lib/**/*-after.js'
],
stephenmathieson marked this conversation as resolved.
Show resolved Hide resolved
parserOptions: {
sourceType: 'module'
},
Expand All @@ -87,6 +92,23 @@ module.exports = {
'no-use-before-define': 'off'
}
},
{
// after functions and reporters will not be run inside the same context as axe.run so should not access browser globals that require context specific information (window.location, window.getComputedStyles, etc.)
files: [
stephenmathieson marked this conversation as resolved.
Show resolved Hide resolved
'lib/**/*-after.js',
'lib/core/reporters/**/*.js'
],
parserOptions: {
sourceType: 'module'
},
env: {},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid the exclusion above, just do:

Suggested change
env: {},
env: {
browser: false,
etc: ...
},

globals: {},
rules: {
'func-names': [2, 'as-needed'],
'prefer-const': 2,
'no-use-before-define': 'off'
}
},
{
files: ['test/**/*.js'],
parserOptions: {
Expand Down
15 changes: 9 additions & 6 deletions axe.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,8 @@ declare namespace axe {
preload?: boolean;
performanceTimer?: boolean;
}
interface AxeResults {
interface AxeResults extends EnvironmentData {
toolOptions: RunOptions;
testEngine: TestEngine;
testRunner: TestRunner;
testEnvironment: TestEnvironment;
url: string;
timestamp: string;
passes: Result[];
violations: Result[];
incomplete: Result[];
Expand Down Expand Up @@ -262,6 +257,7 @@ declare namespace axe {
interface PartialResult {
frames: SerialDqElement[];
results: PartialRuleResult[];
environmentData?: EnvironmentData;
}
interface FrameContext {
frameSelector: CrossTreeSelector;
Expand All @@ -271,6 +267,13 @@ declare namespace axe {
getFrameContexts: (context?: ElementContext) => FrameContext[];
shadowSelect: (selector: CrossTreeSelector) => Element | null;
}
interface EnvironmentData {
testEngine: TestEngine;
testRunner: TestRunner;
testEnvironment: TestEnvironment;
url: string;
timestamp: string;
}

let version: string;
let plugins: any;
Expand Down
7 changes: 7 additions & 0 deletions doc/run-partial.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ The `axe.utils.getFrameContexts` method takes any valid context, and returns an

- `frameSelector`: This is a CSS selector, or array of CSS selectors in case of nodes in a shadow DOM tree to locate the frame element to be tested.
- `frameContext`: This is an object is a context object that should be tested in the particular frame.

## Custom Rulesets and Reporters

Because `axe.finishRun` does not run inside the page, the `reporter` and `after` methods do not have access to the top-level `window` and `document` objects, and might not have access to common browser APIs. Axe-core reporter use the `environmentData` property that is set on the partialResult object of the initiator.

Because of this constraint, custom reporters, and custom rulesets that add `after` methods must not rely on browser APIs or globals. Any data needed for either should either be taken from the `environmentData` property, or collected in an `evaluate` method of a check, and stored using its `.data()` method.

## Recommendations

When building integrations with browser drivers using axe-core, it is safer and more stable to use `axe.runPartial` and `axe.finishRun` then to use `axe.run`. These two methods ensure that no information from one frame is ever handed off to another. That way if any script in a frame interferes with the `axe` object, or with `window.postMessage`, other frames will not be affected.
Expand Down
3 changes: 2 additions & 1 deletion lib/core/public/finish-run.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {

export default function finishRun(partialResults, options = {}) {
options = clone(options);
const { environmentData } = partialResults.find(r => r.environmentData) || {}

// normalize the runOnly option for the output of reporters toolOptions
axe._audit.normalizeOptions(options);
Expand All @@ -20,7 +21,7 @@ export default function finishRun(partialResults, options = {}) {
results.forEach(publishMetaData);
results = results.map(finalizeRuleResult);

return createReport(results, options);
return createReport(results, { environmentData, ...options });
}

function setFrameSpec(partialResults) {
Expand Down
9 changes: 7 additions & 2 deletions lib/core/public/run-partial.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Context from '../base/context';
import teardown from './teardown';
import { DqElement, getSelectorData, assert } from '../utils';
import { DqElement, getSelectorData, assert, getEnvironmentData } from '../utils';
import normalizeRunParams from './run/normalize-run-params';

export default function runPartial(...args) {
Expand Down Expand Up @@ -28,7 +28,12 @@ export default function runPartial(...args) {
const frames = contextObj.frames.map(({ node }) => {
return new DqElement(node, options).toJSON();
});
return { results, frames };
let environmentData;
if (contextObj.initiator) {
environmentData = getEnvironmentData();
}

return { results, frames, environmentData };
})
.finally(() => {
axe._running = false;
Expand Down
3 changes: 2 additions & 1 deletion lib/core/public/run-virtual-rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
publishMetaData,
finalizeRuleResult,
aggregateResult,
getEnvironmentData,
getRule
} from '../utils';

Expand Down Expand Up @@ -54,7 +55,7 @@ function runVirtualRule(ruleId, vNode, options = {}) {
);

return {
...helpers.getEnvironmentData(),
...getEnvironmentData(),
...results,
toolOptions: options
};
Expand Down
39 changes: 0 additions & 39 deletions lib/core/reporters/helpers/get-environment-data.js

This file was deleted.

3 changes: 0 additions & 3 deletions lib/core/reporters/helpers/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import failureSummary from './failure-summary';
import getEnvironmentData from './get-environment-data';
import incompleteFallbackMessage from './incomplete-fallback-msg';
import processAggregate from './process-aggregate';

Expand All @@ -8,14 +7,12 @@ import processAggregate from './process-aggregate';
axe._thisWillBeDeletedDoNotUse = axe._thisWillBeDeletedDoNotUse || {};
axe._thisWillBeDeletedDoNotUse.helpers = {
failureSummary,
getEnvironmentData,
incompleteFallbackMessage,
processAggregate
};

export {
failureSummary,
getEnvironmentData,
incompleteFallbackMessage,
processAggregate
};
15 changes: 6 additions & 9 deletions lib/core/reporters/na.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import { processAggregate, getEnvironmentData } from './helpers';
import { processAggregate } from './helpers';
import { getEnvironmentData } from '../utils';

const naReporter = (results, options, callback) => {
console.warn(
'"na" reporter will be deprecated in axe v4.0. Use the "v2" reporter instead.'
);

if (typeof options === 'function') {
callback = options;
options = {};
}

var out = processAggregate(results, options);
const { environmentData, ...toolOptions } = options;
callback({
...getEnvironmentData(),
toolOptions: options,
violations: out.violations,
passes: out.passes,
incomplete: out.incomplete,
inapplicable: out.inapplicable
...getEnvironmentData(environmentData),
toolOptions,
...processAggregate(results, options)
});
};

Expand Down
12 changes: 7 additions & 5 deletions lib/core/reporters/no-passes.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { processAggregate, getEnvironmentData } from './helpers';
import { processAggregate } from './helpers';
import { getEnvironmentData } from '../utils';

const noPassesReporter = (results, options, callback) => {
if (typeof options === 'function') {
callback = options;
options = {};
}
const { environmentData, ...toolOptions } = options;
// limit result processing to types we want to include in the output
options.resultTypes = ['violations'];

var out = processAggregate(results, options);
var { violations } = processAggregate(results, options);

callback({
...getEnvironmentData(),
toolOptions: options,
violations: out.violations
...getEnvironmentData(environmentData),
toolOptions,
violations
});
};

Expand Down
11 changes: 5 additions & 6 deletions lib/core/reporters/raw-env.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { getEnvironmentData } from './helpers';
import { getEnvironmentData } from '../utils';
import rawReporter from './raw';

const rawEnvReporter = (results, options, callback) => {
if (typeof options === 'function') {
callback = options;
options = {};
}
function rawCallback(raw) {
const env = getEnvironmentData();
const { environmentData, ...toolOptions } = options;
rawReporter(results, toolOptions, (raw) => {
const env = getEnvironmentData(environmentData);
callback({ raw, env });
}

rawReporter(results, options, rawCallback);
});
};

export default rawEnvReporter;
21 changes: 8 additions & 13 deletions lib/core/reporters/v1.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import {
processAggregate,
failureSummary,
getEnvironmentData
} from './helpers';
import { processAggregate, failureSummary } from './helpers';
import { getEnvironmentData } from '../utils'

const v1Reporter = (results, options, callback) => {
if (typeof options === 'function') {
callback = options;
options = {};
}
var out = processAggregate(results, options);
};
const { environmentData, ...toolOptions } = options;
const out = processAggregate(results, options);

const addFailureSummaries = result => {
result.nodes.forEach(nodeResult => {
Expand All @@ -21,12 +19,9 @@ const v1Reporter = (results, options, callback) => {
out.violations.forEach(addFailureSummaries);

callback({
...getEnvironmentData(),
toolOptions: options,
violations: out.violations,
passes: out.passes,
incomplete: out.incomplete,
inapplicable: out.inapplicable
...getEnvironmentData(environmentData),
toolOptions,
...out
});
};

Expand Down
13 changes: 6 additions & 7 deletions lib/core/reporters/v2.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { processAggregate, getEnvironmentData } from './helpers';
import { processAggregate } from './helpers';
import { getEnvironmentData } from '../utils';

const v2Reporter = (results, options, callback) => {
if (typeof options === 'function') {
callback = options;
options = {};
}
const { environmentData, ...toolOptions } = options;
var out = processAggregate(results, options);
callback({
...getEnvironmentData(),
toolOptions: options,
violations: out.violations,
passes: out.passes,
incomplete: out.incomplete,
inapplicable: out.inapplicable
...getEnvironmentData(environmentData),
toolOptions,
...out
});
};

Expand Down
47 changes: 47 additions & 0 deletions lib/core/utils/get-environment-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Add information about the environment axe was run in.
* @return {EnvironmentData}
*/
export default function getEnvironmentData(metadata = null, win = window) {
if (metadata && typeof metadata === 'object') {
return metadata;
} else if (typeof win !== 'object') {
return {}
}

return {
testEngine: {
name: 'axe-core',
version: axe.version
},
testRunner: {
name: axe._audit.brand
},
testEnvironment: getTestEnvironment(win),
timestamp: new Date().toISOString(),
url: win.location?.href
};
}

function getTestEnvironment(win) {
if (!win.navigator || typeof win.navigator !== 'object') {
return {}
}
const { navigator, innerHeight, innerWidth } = win;
const { angle, type } = getOrientation(win) || {}
return {
userAgent: navigator.userAgent,
windowWidth: innerWidth,
windowHeight: innerHeight,
orientationAngle: angle,
orientationType: type
}
}

function getOrientation({ screen }) {
return (
screen.orientation ||
screen.msOrientation ||
screen.mozOrientation
);
}
Loading