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(region): contents in iframes should pass the region rule if the iframe itself is in a region #2949

Merged

Conversation

clottman
Copy link
Contributor

@clottman clottman commented May 19, 2021

If there's an iframe on the page, elements within that iframe should all be treated as being contained in a region if the iframe element was contained on the iframe.

Also adds lots of tests for this behavior.

Closes issue: #2690

@clottman clottman requested a review from a team as a code owner May 19, 2021 22:27
@clottman clottman changed the title fix(region) - let fix(region) - contents in iframes should always pass the region rule May 19, 2021
@WilcoFiers WilcoFiers self-assigned this May 21, 2021
@@ -1,6 +1,7 @@
{
"id": "region",
"selector": "body *",
"matches": "is-initiator-matches",
Copy link
Contributor

@WilcoFiers WilcoFiers May 21, 2021

Choose a reason for hiding this comment

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

This is also going to cause valid region issues to be ignored. The trick to figure out is how to distinguish these three examples:

<!-- failing h1 -->
<iframe srcdoc="<h1>hello world</h1>"></iframe>

<!-- passing h1 -->
<main>
  <iframe srcdoc="<h1>hello world</h1>"></iframe>
</main>

<!-- passing h1 -->
  <iframe srcdoc="<main>
    <h1>hello world</h1>
</main>"></iframe>

<!-- inapplicable comment -->
<iframe srcdoc="<-- empty frame -->"></iframe>

Axe-core currently fails passing iframe 1, your PR fixes that, but it does so by also passing the failed iframe. I think what we'd need to do is to pass information about which frames are in a region to a new after method, and then in the after method to pass all the content of iframes that are in a region. That'll need to be recursive too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For this case:

<!-- inapplicable comment -->
<iframe srcdoc="<-- empty frame -->"></iframe>

We can't mark this as inapplicable because we don't know the iframe is empty until after the rule has already run.

@clottman clottman changed the title fix(region) - contents in iframes should always pass the region rule fix(region) - contents in iframes should pass the region rule if the iframe itself is in a region May 26, 2021
@clottman clottman requested a review from WilcoFiers May 26, 2021 19:51
@WilcoFiers WilcoFiers changed the title fix(region) - contents in iframes should pass the region rule if the iframe itself is in a region fix(region): contents in iframes should pass the region rule if the iframe itself is in a region Jun 3, 2021
@clottman clottman force-pushed the 2690-ignore-iframe-content-for-region branch from 412e610 to 9f3a87a Compare June 3, 2021 22:11
@@ -290,14 +290,6 @@ describe('region', function() {
assert.isTrue(checkEvaluate.apply(checkContext, checkArgs));
});

it('treats iframe elements as regions', function() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is now done in the after method, so this test won't pass any more.

@clottman clottman requested a review from WilcoFiers June 3, 2021 22:39
lib/core/utils/match-ancestry.js Show resolved Hide resolved
test/checks/navigation/region-after.js Outdated Show resolved Hide resolved
Copy link
Contributor

@WilcoFiers WilcoFiers left a comment

Choose a reason for hiding this comment

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

Some cleanup. Rest looks good.

function regionAfter(results) {
results.forEach((r, index) => {
// this element failed the check
if (!r.result) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should prefer the "exit early" code style.

Suggested change
if (!r.result) {
if (r.result) {
return
}

// this element failed the check
if (!r.result) {
// we need to see if it's actually contained in an iframe that passed the check
const ancestryMinusLast = r.node.ancestry.slice(0, -1);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const ancestryMinusLast = r.node.ancestry.slice(0, -1);
const frameAncestry = r.node.ancestry.slice(0, -1);

const ancestryMinusLast = r.node.ancestry.slice(0, -1);

// we only need to check up to index - 1 because the iframe this element might be contained in will have been found before this element
for (const earlierResult of results.slice(0, index)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This feels unnecessarily complex. I suggest you create a list of frames with result:true, and then try to match against that. You have no business running matchAncestry against anything other than iframes. You don't need to do it if ancestry has a length of 1 either.

That also lets you use clearer variable names. It took me a while to figure out what earlierResult was even for.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That strategy won't work because we need to be able to evaluate nested iframes correctly - when an iframe is contained in another iframe that should pass, but the inner iframe does not pass on its own, we don't know until we've iterated sequentially through that the inner iframe should pass and thus be considered part of the set that should be matched against.

Consider this (which is represented in the full integration tests as region-pass-nested-iframe.html):

<main>
  <iframe id='outer' srcdoc="<iframe id='inner' srcdoc='<p>hello</p>'></iframe></iframe>
</main>

Here's the code I was working on, which has the problem described above, if you want to play around with it yourself.

function regionAfter(results) {
  const passingFrameAncestries = results.filter(
    r => r.result && r.data.isIframe
  ).map(r => r.node.ancestry);

  results.forEach((r, index) => {
    // this element failed the check, and it's in an iframe
    if (!r.result && r.node.ancestry.length > 1) {
      // we need to see if it's actually contained in an iframe that passed the check
      const frameAncestry = r.node.ancestry.slice(0, -1);

      for (const frame of passingFrameAncestries) {
        if (matchAncestry(frameAncestry, frame)) {
          r.result = true;
          break;
        }
      }
    }
  });

  // iframe elements should always pass
  results.forEach(r => {
    if (!r.result && r.data.isIframe) {
      r.result = true;
    }
  });
  return results;
}

assert.equal(results[1].result, true);
assert.equal(results[2].result, true);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a test missing from this, one that I strongly suspect fails.

Suggested change
});
});
it('should pass content if an grandparent frame passes', function() {
var results = checks.region.after([
{
data: { isIframe: true },
node: {
ancestry: ['html > body > iframe']
},
result: true
},
{
data: { isIframe: true },
node: {
ancestry: ['html > body > iframe', 'html > body > iframe']
},
result: false
},
{
data: { isIframe: false },
node: {
ancestry: [
'html > body > iframe',
'html > body > iframe',
'html > body > p'
]
},
result: false
}
]);
assert.equal(results[0].result, true);
assert.equal(results[1].result, true);
assert.equal(results[2].result, true);
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's actually an integration test for this already - region-pass-nested-iframe.html. It's the reason I had to slightly modify your suggested code when I made region-after easier to understand.

@WilcoFiers WilcoFiers merged commit 43145d6 into dequelabs:develop Jun 23, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants