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

refactor: break up context.js and clean it up a bit #3794

Merged
merged 2 commits into from
Nov 23, 2022
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
271 changes: 63 additions & 208 deletions lib/core/base/context.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import createFrameContext from './create-frame-context';
import { createFrameContext } from './context/create-frame-context';
import { normalizeContext } from './context/normalize-context';
import { parseSelectorArray } from './context/parse-selector-array';
import {
findBy,
getNodeFromTree,
getFlattenedTree,
select,
isNodeInContext,
Expand All @@ -11,208 +12,6 @@ import {
} from '../utils';
import { isVisibleToScreenReaders } from '../../commons/dom';

/**
* Pushes a unique frame onto `frames` array, filtering any hidden iframes
* @private
* @param {Object} Context Parent context for the frame
* @param {HTMLElement} frame The frame to push onto Context
*/
function pushUniqueFrame(context, frame) {
if (
!isVisibleToScreenReaders(frame) ||
findBy(context.frames, 'node', frame)
) {
return;
}
context.frames.push(createFrameContext(frame, context));
}

/**
* Check if a normalized context tests the full page
* @private
*/
function isPageContext({ include }) {
return (
include.length === 1 && include[0].actualNode === document.documentElement
);
}

/**
* Unshift selectors of matching iframes
* @private
* @param {Context} context The context object to operate on and assign to
* @param {String} type The "type" of context, 'include' or 'exclude'
* @param {Array} selectorArray Array of CSS selectors, each element represents a frame;
* where the last element is the actual node
*/
function pushUniqueFrameSelector(context, type, selectorArray) {
context.frames = context.frames || [];

const frameSelector = selectorArray.shift();
const frames = document.querySelectorAll(frameSelector);

Array.from(frames).forEach(frame => {
context.frames.forEach(contextFrame => {
if (contextFrame.node === frame) {
contextFrame[type].push(selectorArray);
}
});

if (!context.frames.find(result => result.node === frame)) {
const result = createFrameContext(frame, context);
if (selectorArray) {
result[type].push(selectorArray);
}

context.frames.push(result);
}
});
}

/**
* Normalize the input of "context" so that many different methods of input are accepted
* @private
* @param {Mixed} context The configuration object passed to `Context`
* @return {Object} Normalized context spec to include both `include` and `exclude` arrays
*/
function normalizeContext(context) {
// typeof NodeList.length in PhantomJS === function
if (
(context && typeof context === 'object') ||
context instanceof window.NodeList
) {
if (context instanceof window.Node) {
return {
include: [context],
exclude: []
};
}

if (
context.hasOwnProperty('include') ||
context.hasOwnProperty('exclude')
) {
return {
include:
context.include && +context.include.length
? context.include
: [document],
exclude: context.exclude || []
};
}

if (context.length === +context.length) {
return {
include: context,
exclude: []
};
}
}

if (typeof context === 'string') {
return {
include: [context],
exclude: []
};
}

return {
include: [document],
exclude: []
};
}

/**
* Finds frames in context, converts selectors to Element references and pushes unique frames
* @private
* @param {Context} context The instance of Context to operate on
* @param {String} type The "type" of thing to parse, "include" or "exclude"
* @return {Array} Parsed array of matching elements
*/
function parseSelectorArray(context, type) {
var item,
result = [],
nodeList;
for (var i = 0, l = context[type].length; i < l; i++) {
item = context[type][i];
// selector
if (typeof item === 'string') {
nodeList = Array.from(document.querySelectorAll(item));
//eslint no-loop-func:0
result = result.concat(
nodeList.map(node => {
return getNodeFromTree(node);
})
);
break;
} else if (item && item.length && !(item instanceof window.Node)) {
if (item.length > 1) {
pushUniqueFrameSelector(context, type, item);
} else {
nodeList = Array.from(document.querySelectorAll(item[0]));
//eslint no-loop-func:0
result = result.concat(
nodeList.map(node => {
return getNodeFromTree(node);
})
);
}
} else if (item instanceof window.Node) {
if (item.documentElement instanceof window.Node) {
result.push(context.flatTree[0]);
} else {
result.push(getNodeFromTree(item));
}
}
}

// filter nulls
return result.filter(r => r);
}

/**
* Check that the context, as well as each frame includes at least 1 element
* @private
* @param {context} context
* @return {Error}
*/
function validateContext(context) {
if (context.include.length === 0) {
if (context.frames.length === 0) {
var env = respondable.isInFrame() ? 'frame' : 'page';
return new Error('No elements found for include in ' + env + ' Context');
}
context.frames.forEach((frame, i) => {
if (frame.include.length === 0) {
return new Error(
'No elements found for include in Context of frame ' + i
);
}
});
Comment on lines -185 to -191
Copy link
Contributor Author

@WilcoFiers WilcoFiers Nov 23, 2022

Choose a reason for hiding this comment

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

I left this out because it never worked. We're returning in a forEach. That does nothing. This is completely pointless too. To test the entire frame, include has to be empty. An empty include is a valid option.

}
}

/**
* For a context-like object, find its shared root node
*/
function getRootNode({ include, exclude }) {
const selectors = Array.from(include).concat(Array.from(exclude));
// Find the first Element.ownerDocument or Document
for (var i = 0; i < selectors.length; ++i) {
var item = selectors[i];

if (item instanceof window.Element) {
return item.ownerDocument.documentElement;
}

if (item instanceof window.Document) {
return item.documentElement;
}
}

return document.documentElement;
}

/**
* Holds context of includes, excludes and frames for analysis.
*
Expand Down Expand Up @@ -266,12 +65,68 @@ export default function Context(spec, flatTree) {
}

// Validate outside of a frame
var err = validateContext(this);
if (err instanceof Error) {
throw err;
}
validateContext(this);

if (!Array.isArray(this.include)) {
this.include = Array.from(this.include);
}
this.include.sort(nodeSorter); // ensure that the order of the include nodes is document order
}

/**
* Pushes a unique frame onto `frames` array, filtering any hidden iframes
* @private
* @param {Object} Context Parent context for the frame
* @param {HTMLElement} frame The frame to push onto Context
*/
function pushUniqueFrame(context, frame) {
if (
!isVisibleToScreenReaders(frame) ||
findBy(context.frames, 'node', frame)
) {
return;
}
context.frames.push(createFrameContext(frame, context));
}

/**
* Check if a normalized context tests the full page
* @private
*/
function isPageContext({ include }) {
return (
include.length === 1 && include[0].actualNode === document.documentElement
);
}

/**
* Check that the context, as well as each frame includes at least 1 element
* @private
* @param {context} context
* @return {Error}
*/
function validateContext(context) {
if (context.include.length === 0 && context.frames.length === 0) {
const env = respondable.isInFrame() ? 'frame' : 'page';
throw new Error('No elements found for include in ' + env + ' Context');
}
}

/**
* For a context-like object, find its shared root node
*/
function getRootNode({ include, exclude }) {
const selectors = Array.from(include).concat(Array.from(exclude));
// Find the first Element.ownerDocument or Document
for (let i = 0; i < selectors.length; i++) {
const item = selectors[i];
if (item instanceof window.Element) {
return item.ownerDocument.documentElement;
}

if (item instanceof window.Document) {
return item.documentElement;
}
}
return document.documentElement;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default function createFrameContext(frame, { focusable, page }) {
export function createFrameContext(frame, { focusable, page }) {
return {
node: frame,
include: [],
Expand Down
52 changes: 52 additions & 0 deletions lib/core/base/context/normalize-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Normalize the input of "context" so that many different methods of input are accepted
* @private
* @param {Mixed} context The configuration object passed to `Context`
* @return {Object} Normalized context spec to include both `include` and `exclude` arrays
*/
export function normalizeContext(context) {
// typeof NodeList.length in PhantomJS === function
if (
(context && typeof context === 'object') ||
context instanceof window.NodeList
) {
if (context instanceof window.Node) {
return {
include: [context],
exclude: []
};
}

if (
context.hasOwnProperty('include') ||
context.hasOwnProperty('exclude')
) {
return {
include:
context.include && +context.include.length
? context.include
: [document],
exclude: context.exclude || []
};
}

if (context.length === +context.length) {
return {
include: context,
exclude: []
};
}
}

if (typeof context === 'string') {
return {
include: [[context]],
exclude: []
};
}

return {
include: [document],
exclude: []
};
}
Loading