Skip to content
This repository has been archived by the owner on Jul 15, 2023. It is now read-only.

Implement react-a11y-iframes rule #692

Merged
merged 14 commits into from
Jan 19, 2019
Merged
Show file tree
Hide file tree
Changes from 9 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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,15 @@ We recommend you specify exact versions of lint libraries, including `tslint-mic
</td>
<td>6.0.0</td>
</tr>
<tr>
<td>
<code>react-a11y-iframes</code>
</td>
<td>
Enforce that iframe elements are not empty, have title, and are unique.
</td>
<td>6.1.0</td>
</tr>
<tr>
<td>
<code>informative-docs</code>
Expand Down
100 changes: 100 additions & 0 deletions src/reactA11yIframesRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as ts from 'typescript';
import * as Lint from 'tslint';
import * as tsutils from 'tsutils';

import { ExtendedMetadata } from './utils/ExtendedMetadata';
import { getJsxAttributesFromJsxElement } from './utils/JsxAttribute';

const IFRAME_ELEMENT_NAME: string = 'iframe';
const TITLE_ATTRIBUTE_NAME: string = 'title';
const SRC_ATTRIBUTE_NAME: string = 'src';
const HIDDEN_ATTRIBUTE_NAME: string = 'hidden';
const IFRAME_EMPTY_TITLE_ERROR_STRING: string = 'An iframe element must have a non-empty title.';
const IFRAME_EMPTY_OR_HIDDEN_ERROR_STRING: string = 'An iframe element should not be hidden or empty.';
const IFRAME_UNIQUE_TITLE_ERROR_STRING: string = 'An iframe element must have a unique title.';

export class Rule extends Lint.Rules.AbstractRule {
public static metadata: ExtendedMetadata = {
ruleName: 'react-a11y-iframes',
type: 'functionality',
description: 'Enforce that iframe elements are not empty, have title and are unique.',
IllusionMH marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

Comma was added to README.md, but not here.

options: null, // tslint:disable-line:no-null-keyword
optionsDescription: '',
typescriptOnly: false,
issueClass: 'Non-SDL',
issueType: 'Error',
severity: 'Important',
level: 'Opportunity for Excellence',
group: 'Accessibility'
};

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return sourceFile.languageVariant === ts.LanguageVariant.JSX
? this.applyWithWalker(new ReactA11yIframesRuleWalker(sourceFile, this.getOptions()))
: [];
}
}

class ReactA11yIframesRuleWalker extends Lint.RuleWalker {
protected visitVariableDeclaration(node: ts.VariableDeclaration): void {
IllusionMH marked this conversation as resolved.
Show resolved Hide resolved
this.validate(node);
super.visitVariableDeclaration(node);
}

private validate(node: ts.Node) {
const titles: Set<string> = new Set();

this.getJsxElements(node).forEach((element: ts.Node) => {
const attributes = getJsxAttributesFromJsxElement(element);

// Validate that iframe has a non-empty title
const titleAttribute = attributes[TITLE_ATTRIBUTE_NAME];
const titleAttributeText = this.getAttributeText(titleAttribute);
if (!titleAttribute || !titleAttributeText) {
this.addFailureAt(element.getStart(), element.getWidth(), IFRAME_EMPTY_TITLE_ERROR_STRING);
}

// Validate the iframe title is unique
if (titleAttributeText && titles.has(titleAttributeText)) {
this.addFailureAt(element.getStart(), element.getWidth(), IFRAME_UNIQUE_TITLE_ERROR_STRING);
} else if (titleAttributeText) {
titles.add(titleAttributeText);
}

// Validate that iframe is not empty or hidden
const hiddenAttribute = attributes[HIDDEN_ATTRIBUTE_NAME];
const srcAttribute = attributes[SRC_ATTRIBUTE_NAME];
if (hiddenAttribute || !srcAttribute || !this.getAttributeText(srcAttribute)) {
this.addFailureAt(element.getStart(), element.getWidth(), IFRAME_EMPTY_OR_HIDDEN_ERROR_STRING);
}
});
}

private getJsxElements(node: ts.Node): (ts.JsxElement | ts.JsxSelfClosingElement)[] {
const elements: (ts.JsxElement | ts.JsxSelfClosingElement)[] = [];
tsutils.forEachToken(node, (childNode: ts.Node) => {
const parentNode = childNode.parent;
if (childNode.getText() === IFRAME_ELEMENT_NAME) {
if (tsutils.isJsxOpeningElement(parentNode)) {
elements.push(parentNode.parent);
}
if (tsutils.isJsxSelfClosingElement(parentNode)) {
elements.push(parentNode);
}
}
});
return elements;
}

private getAttributeText(attribute: ts.JsxAttribute): string | undefined {
if (attribute && attribute.initializer) {
if (tsutils.isJsxExpression(attribute.initializer)) {
return attribute.initializer.expression ? attribute.initializer.expression.getText() : undefined;
}
if (tsutils.isStringLiteral(attribute.initializer)) {
return attribute.initializer.text;
}
}
return undefined;
}
}
132 changes: 132 additions & 0 deletions src/tests/reactA11yIframesRuleTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Utils } from '../utils/Utils';
import { TestHelper } from './TestHelper';
/**
* Unit tests.
*/
const IFRAME_EMPTY_TITLE_ERROR_STRING: string = 'An iframe element must have a non-empty title.';
const IFRAME_EMPTY_OR_HIDDEN_ERROR_STRING: string = 'An iframe element should not be hidden or empty.';
const IFRAME_UNIQUE_TITLE_ERROR_STRING: string = 'An iframe element must have a unique title.';

describe('reactA11yIframesRuleTests', (): void => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This file isn't needed anymore and should be removed.
All tests are now in new format (thanks for converting them! 🙌) that are even better - with checks where error range ends.

const ruleName: string = 'react-a11y-iframes';
it('should pass if iframe title is not empty and iframe is not hidden or empty', (): void => {
const script: string = `
import React = require('react');
const someComponent = () => <iframe title="I'm a non empty title" src="http://someSource.com"/>;
`;

TestHelper.assertViolations(ruleName, script, []);
});
it('should pass if iframe title is non-empty expression and iframe is not hidden or empty', (): void => {
const script: string = `
import React = require('react');
const someComponent = () => <iframe title={"hello there"} src="http://someSource.com"/>;
`;

TestHelper.assertViolations(ruleName, script, []);
});
it('should fail if iframe title is empty', (): void => {
const script: string = `
import React = require('react');
const someComponent = () => <iframe title="" src="http://someSource.com"/>;
`;

TestHelper.assertViolations(ruleName, script, [
{
failure: IFRAME_EMPTY_TITLE_ERROR_STRING,
name: Utils.absolutePath('file.tsx'),
ruleName: ruleName,
startPosition: { character: 37, line: 3 }
}
]);
});
it('should fail if iframe has no title', (): void => {
const script: string = `
import React = require('react');
const someComponent = () => <iframe src="http://someSource.com" />;
`;

TestHelper.assertViolations(ruleName, script, [
{
failure: IFRAME_EMPTY_TITLE_ERROR_STRING,
name: Utils.absolutePath('file.tsx'),
ruleName: ruleName,
startPosition: { character: 37, line: 3 }
}
]);
});
it('should fail if iframe has no source', (): void => {
const script: string = `
import React = require('react');
const someComponent = () => <iframe title="hi there"></iframe>;
`;

TestHelper.assertViolations(ruleName, script, [
{
failure: IFRAME_EMPTY_OR_HIDDEN_ERROR_STRING,
name: Utils.absolutePath('file.tsx'),
ruleName: ruleName,
startPosition: { character: 37, line: 3 }
}
]);
});
it('should fail if iframe source is empty', (): void => {
const script: string = `
import React = require('react');
const someComponent = () => <iframe title="hi there" src=""/>;
`;

TestHelper.assertViolations(ruleName, script, [
{
failure: IFRAME_EMPTY_OR_HIDDEN_ERROR_STRING,
name: Utils.absolutePath('file.tsx'),
ruleName: ruleName,
startPosition: { character: 37, line: 3 }
}
]);
});
it('should fail if iframe is hidden', (): void => {
const script: string = `
import React = require('react');
const someComponent = () => <iframe title="hi there" src="http://someSource.com" hidden></iframe>;
`;

TestHelper.assertViolations(ruleName, script, [
{
failure: IFRAME_EMPTY_OR_HIDDEN_ERROR_STRING,
name: Utils.absolutePath('file.tsx'),
ruleName: ruleName,
startPosition: { character: 37, line: 3 }
}
]);
});
it('should pass if iframe title is unique', (): void => {
const script: string = `
import React = require('react');
const someComponent = () => <>
<iframe title="hi there" src="http://someSource.com"></iframe>
<iframe title="hello there" src="http://someSource.com"></iframe>
</>
`;

TestHelper.assertViolations(ruleName, script, []);
});
it('should fail if iframe title is not unique', (): void => {
const script: string = `
import React = require('react');
const someComponent = () => <>
<iframe title="hi there" src="http://someSource.com"></iframe>
<iframe title="hi there" src="http://someSource.com"></iframe>
</>
`;

TestHelper.assertViolations(ruleName, script, [
{
failure: IFRAME_UNIQUE_TITLE_ERROR_STRING,
name: Utils.absolutePath('file.tsx'),
ruleName: ruleName,
startPosition: { character: 13, line: 5 }
}
]);
});
});
24 changes: 24 additions & 0 deletions tests/react-a11y-iframes/test.tsx.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

const someComponent = () => <iframe title="I'm a non empty title" src="http://someSource.com"/>;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit. Would expect component function to start from upper case letter.


const someComponent = () => <iframe title={"hello there"} src="http://someSource.com"/>;

const someComponent = () => <iframe title="" src="http://someSource.com"/>;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [An iframe element must have a non-empty title.]
const someComponent = () => <iframe src="http://someSource.com" />;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [An iframe element must have a non-empty title.]
const someComponent = () => <iframe title="hi there"></iframe>;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [An iframe element should not be hidden or empty.]
const someComponent = () => <iframe title="hi there" src=""/>;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [An iframe element should not be hidden or empty.]
const someComponent = () => <iframe title="hi there" src="http://someSource.com" hidden></iframe>;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [An iframe element should not be hidden or empty.]
const someComponent = () => <>
<iframe title="hi there" src="http://someSource.com"></iframe>
<iframe title="hello there" src="http://someSource.com"></iframe>
</>
Copy link
Contributor

Choose a reason for hiding this comment

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

nit. indentation looks off in these two blocks (same as in mocha test file where these strings were deeply nested).

const someComponent = () => <>
<iframe title="hi there" src="http://someSource.com"></iframe>
<iframe title="hi there" src="http://someSource.com"></iframe>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [An iframe element must have a unique title.]
</>
5 changes: 5 additions & 0 deletions tests/react-a11y-iframes/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"react-a11y-iframes": true
}
}
Loading