diff --git a/lib/core/base/context.js b/lib/core/base/context.js index f957a780a5..f26f559ee6 100644 --- a/lib/core/base/context.js +++ b/lib/core/base/context.js @@ -42,29 +42,31 @@ function pushUniqueFrame(collection, frame) { function pushUniqueFrameSelector(context, type, selectorArray) { context.frames = context.frames || []; - var result, frame; - var frames = document.querySelectorAll(selectorArray.shift()); + const frameSelector = selectorArray.shift(); + const frames = document.querySelectorAll(frameSelector); - frameloop: for (var i = 0, l = frames.length; i < l; i++) { - frame = frames[i]; - for (var j = 0, l2 = context.frames.length; j < l2; j++) { - if (context.frames[j].node === frame) { - context.frames[j][type].push(selectorArray); - break frameloop; + Array.from(frames).forEach(frame => { + context.frames.forEach(contextFrame => { + if (contextFrame.node === frame) { + contextFrame[type].push(selectorArray); } - } - result = { - node: frame, - include: [], - exclude: [] - }; + }); - if (selectorArray) { - result[type].push(selectorArray); + if (!context.frames.find(result => result.node === frame)) { + const result = { + node: frame, + include: [], + exclude: [] + }; + + if (selectorArray) { + result[type].push(selectorArray); + } + + context.frames.push(result); } + }); - context.frames.push(result); - } } /** diff --git a/lib/core/utils/get-frame-contexts.js b/lib/core/utils/get-frame-contexts.js new file mode 100644 index 0000000000..f658874d4e --- /dev/null +++ b/lib/core/utils/get-frame-contexts.js @@ -0,0 +1,12 @@ +import Context from '../base/context'; +import getAncestry from './get-ancestry'; + +export default function getFrameContexts(context) { + const { frames } = new Context(context); + return frames.map(({ node, include, exclude }) => { + const frameContext = { include, exclude }; + frameContext.initiator = false; + const frameSelector = getAncestry(node); + return { frameSelector, frameContext }; + }); +} diff --git a/lib/core/utils/index.js b/lib/core/utils/index.js index bde396b7c7..813ba2bd91 100644 --- a/lib/core/utils/index.js +++ b/lib/core/utils/index.js @@ -29,6 +29,7 @@ export { default as getAllChecks } from './get-all-checks'; export { default as getBaseLang } from './get-base-lang'; export { default as getCheckMessage } from './get-check-message'; export { default as getCheckOption } from './get-check-option'; +export { default as getFrameContexts } from './get-frame-contexts'; export { default as getFriendlyUriEnd } from './get-friendly-uri-end'; export { default as getNodeAttributes } from './get-node-attributes'; export { default as getNodeFromTree } from './get-node-from-tree'; diff --git a/test/core/utils/get-frame-context.js b/test/core/utils/get-frame-context.js new file mode 100644 index 0000000000..f55185f294 --- /dev/null +++ b/test/core/utils/get-frame-context.js @@ -0,0 +1,274 @@ +describe('utils.getFrameContexts', function() { + var getFrameContexts = axe.utils.getFrameContexts; + var shadowSupported = axe.testUtils.shadowSupport.v1; + var fixture = document.querySelector('#fixture'); + + it('returns an empty array if the page has no frames', function() { + var frameContext = getFrameContexts(); + assert.isArray(frameContext); + assert.lengthOf(frameContext, 0); + }); + + it('sets context.initiator to false for each included frame', function() { + fixture.innerHTML = + '' + '' + ''; + + var contexts = getFrameContexts().map(function(frameData) { + return frameData.frameContext; + }); + + assert.lengthOf(contexts, 3); + assert.isFalse(contexts[0].initiator); + assert.isFalse(contexts[1].initiator); + assert.isFalse(contexts[2].initiator); + }); + + it('returns a `frameSelector` for each included frame', function() { + fixture.innerHTML = + '' + '' + ''; + + var selectors = getFrameContexts().map(function(frameData) { + return frameData.frameSelector; + }); + assert.lengthOf(selectors, 3); + assert.include(selectors[0], 'iframe:nth-child(1)'); + assert.include(selectors[1], 'iframe:nth-child(2)'); + assert.include(selectors[2], 'iframe:nth-child(3)'); + }); + + it('returns a `frameContext` for each included frame', function() { + fixture.innerHTML = + '' + + '' + + ''; + var context = { + include: [ + ['#f1', 'header'], + ['#f2', 'main'] + ], + exclude: [['#f3', 'footer']] + }; + var contexts = getFrameContexts(context).map(function(frameData) { + return frameData.frameContext; + }); + + assert.lengthOf(contexts, 3); + assert.deepEqual(contexts[0], { + initiator: false, + include: [['header']], + exclude: [] + }); + assert.deepEqual(contexts[1], { + initiator: false, + include: [['main']], + exclude: [] + }); + assert.deepEqual(contexts[2], { + initiator: false, + include: [], + exclude: [['footer']] + }); + }); + + it('excludes non-frame contexts', function() { + fixture.innerHTML = ''; + var context = { + include: [['#header'], ['a'], ['#f1', 'header']] + }; + var contexts = getFrameContexts(context).map(function(frameData) { + return frameData.frameContext; + }); + + assert.lengthOf(contexts, 1); + assert.deepEqual(contexts[0], { + initiator: false, + include: [['header']], + exclude: [] + }); + }); + + it('mixes contexts if the frame is selected twice', function() { + fixture.innerHTML = + '' + ''; + var context = { + include: [ + ['#f1', 'header'], + ['#f2', 'footer'] + ], + exclude: [['iframe', 'main']] + }; + var contexts = getFrameContexts(context).map(function(frameData) { + return frameData.frameContext; + }); + assert.lengthOf(contexts, 2); + assert.deepEqual(contexts[0], { + initiator: false, + include: [['header']], + exclude: [['main']] + }); + assert.deepEqual(contexts[1], { + initiator: false, + include: [['footer']], + exclude: [['main']] + }); + }); + + it('combines include/exclude arrays of frames selected twice', function() { + fixture.innerHTML = ''; + var context = { + include: [ + ['iframe', 'header'], + ['iframe', 'main'] + ], + exclude: [ + ['iframe', 'aside'], + ['iframe', 'footer'] + ] + }; + var contexts = getFrameContexts(context).map(function(frameData) { + return frameData.frameContext; + }); + + assert.lengthOf(contexts, 1); + assert.deepEqual(contexts[0], { + initiator: false, + include: [['header'], ['main']], + exclude: [['aside'], ['footer']] + }); + }); + + it('skips excluded frames', function() { + fixture.innerHTML = + '' + + '' + + ''; + var context = { + exclude: [[['#f2']]] + }; + var selectors = getFrameContexts(context).map(function(frameData) { + return frameData.frameSelector; + }); + assert.lengthOf(selectors, 2); + assert.include(selectors[0], 'iframe:nth-child(1)'); + assert.include(selectors[1], 'iframe:nth-child(3)'); + }); + + it('skips frames excluded by a parent', function() { + fixture.innerHTML = ''; + var frameContexts = getFrameContexts({ + exclude: [['#fixture']] + }); + assert.lengthOf(frameContexts, 0); + }); + + it('normalizes the context', function() { + var frameContexts; + fixture.innerHTML = + '' + ''; + frameContexts = getFrameContexts('#f1'); + assert.lengthOf(frameContexts, 1); + assert.include(frameContexts[0].frameSelector, 'iframe:nth-child(1)'); + assert.deepEqual(frameContexts[0].frameContext, { + initiator: false, + include: [], + exclude: [] + }); + + frameContexts = getFrameContexts({ include: ['#f1'] }); + assert.lengthOf(frameContexts, 1); + assert.include(frameContexts[0].frameSelector, 'iframe:nth-child(1)'); + assert.deepEqual(frameContexts[0].frameContext, { + initiator: false, + include: [], + exclude: [] + }); + + frameContexts = getFrameContexts({ exclude: ['#f2'] }); + assert.include(frameContexts[0].frameSelector, 'iframe:nth-child(1)'); + assert.lengthOf(frameContexts, 1); + assert.deepEqual(frameContexts[0].frameContext, { + initiator: false, + include: [], + exclude: [] + }); + }); + + it('accepts elements', function() { + var frameContexts; + fixture.innerHTML = + '' + ''; + var f1 = fixture.querySelector('#f1'); + var f2 = fixture.querySelector('#f2'); + frameContexts = getFrameContexts(f1); + assert.lengthOf(frameContexts, 1); + assert.include(frameContexts[0].frameSelector, 'iframe:nth-child(1)'); + assert.deepEqual(frameContexts[0].frameContext, { + initiator: false, + include: [], + exclude: [] + }); + + frameContexts = getFrameContexts({ include: [f1] }); + assert.lengthOf(frameContexts, 1); + assert.include(frameContexts[0].frameSelector, 'iframe:nth-child(1)'); + assert.deepEqual(frameContexts[0].frameContext, { + initiator: false, + include: [], + exclude: [] + }); + + frameContexts = getFrameContexts({ exclude: [f2] }); + assert.include(frameContexts[0].frameSelector, 'iframe:nth-child(1)'); + assert.lengthOf(frameContexts, 1); + assert.deepEqual(frameContexts[0].frameContext, { + initiator: false, + include: [], + exclude: [] + }); + }); + + it('works with nested frames', function() { + fixture.innerHTML = + '' + ''; + var context = { + include: [ + ['#f1', '#f3', 'header'], + ['#f2', '#f4', '#f5', 'footer'] + ], + exclude: [['#f2', '#f6', '#f7', '#f7', 'main']] + }; + var contexts = getFrameContexts(context).map(function(frameData) { + return frameData.frameContext; + }); + + assert.lengthOf(contexts, 2); + assert.deepEqual(contexts[0], { + initiator: false, + include: [['#f3', 'header']], + exclude: [] + }); + assert.deepEqual(contexts[1], { + initiator: false, + include: [['#f4', '#f5', 'footer']], + exclude: [['#f6', '#f7', '#f7', 'main']] + }); + }); + + (shadowSupported ? it : xit)('works on iframes in shadow dom', function() { + fixture.innerHTML = '
'; + var div = fixture.querySelector('div'); + var shadowRoot = div.attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = '
'; + + var frameContext = getFrameContexts(); + + assert.lengthOf(frameContext, 1); + assert.lengthOf(frameContext[0].frameSelector, 2); + assert.equal(frameContext[0].frameSelector[1], 'main > iframe'); + assert.deepEqual(frameContext[0].frameContext, { + initiator: false, + include: [], + exclude: [] + }); + }); +});