Skip to content

Commit

Permalink
Add rule to check the markup validity
Browse files Browse the repository at this point in the history
  • Loading branch information
qzhou1607-zz committed Jun 28, 2017
1 parent 22f2e18 commit b744e53
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 3 deletions.
3 changes: 2 additions & 1 deletion .sonarrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
"maxLoadWaitTime": 30000
}
},
"formatter": "stylish",
"formatter": "codeframe",
"rulesTimeout": 120000,
"rules": {
"axe": "warning",
"content-type": "warning",
"disallowed-headers": "warning",
"disown-opener": "warning",
"highest-available-document-mode": "warning",
"html-checker": "warning",
"manifest-exists": "warning",
"manifest-file-extension": "warning",
"manifest-is-valid": "warning",
Expand Down
1 change: 1 addition & 0 deletions docs/user-guide/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* [`content-type`](content-type.md)
* [`highest-available-document-mode`](highest-available-document-mode.md)
* [`no-friendly-error-pages`](no-friendly-error-pages.md)
* [`html-checker`](html-checker.md)

## Performance

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"file-url": "^2.0.2",
"globby": "^6.1.0",
"handlebars": "^4.0.10",
"html-validator": "^2.2.1",
"iconv-lite": "^0.4.17",
"inquirer": "^3.0.6",
"is-ci": "^1.0.10",
Expand Down
4 changes: 2 additions & 2 deletions src/lib/rule-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class RuleContext {
}

/** Reports a problem with the resource. */
public async report(resource: string, element: IAsyncHTMLElement, message: string, content?: string, location?: IProblemLocation, severity?: Severity): Promise<void> { //eslint-disable-line require-await
public async report(resource: string, element: IAsyncHTMLElement, message: string, content?: string, location?: IProblemLocation, severity?: Severity, codeSnippet?: string): Promise<void> { //eslint-disable-line require-await
let position: IProblemLocation = location;
let sourceCode: string = null;

Expand All @@ -107,7 +107,7 @@ export class RuleContext {
this.sonar.report(
this.id,
severity || this.severity,
sourceCode,
sourceCode || codeSnippet,
position,
message,
resource
Expand Down
107 changes: 107 additions & 0 deletions src/lib/rules/html-checker/html-checker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* @fileoverview Validating html using `the Nu html checker`;
* https://validator.w3.org/nu/
*/

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

import { debug as d } from '../../utils/debug';
import { IRule, IRuleBuilder } from '../../types'; // eslint-disable-line no-unused-vars
import { RuleContext } from '../../rule-context'; // eslint-disable-line no-unused-vars

import { ITargetFetchEnd, IScanEnd, IProblemLocation, Severity } from '../../types'; // eslint-disable-line no-unused-vars

const debug = d(__filename);

// ------------------------------------------------------------------------------
// Public
// ------------------------------------------------------------------------------

const rule: IRuleBuilder = {
create(context: RuleContext): IRule {
/** The promise that represents the scan by html checker. */
let htmlCheckerPromise: Promise<any>;
/** The options to pass to the html checker. */
const scanOptions = {
data: '',
format: 'json'
};

type Error = { // eslint-disable-line no-unused-vars
extract: string, // code snippet
firstColumn: number,
lastLine: number,
hiliteStart: number,
message: string,
subType: string
};

const start = (data: ITargetFetchEnd) => {
const { response } = data;
const htmlChecker = require('html-validator');

scanOptions.data = response.body.content;
htmlCheckerPromise = htmlChecker(scanOptions);
};

const end = async (data: IScanEnd) => {
const { resource } = data;
let result;

if (!htmlCheckerPromise) {
return;
}

debug(`Waiting for Html Checker results for ${resource}`);

try {
result = await htmlCheckerPromise;
} catch (e) {
debug(`Error getting html checker result for ${resource}.`);
await context.report(resource, null, `Couldn't get results from Html Checker for ${resource}.`);

return;
}

debug(`Received Html Checker results for ${resource}`);

const reportPromises = result.messages.map((error: Error): Promise<void> => {
const position: IProblemLocation = {
column: error.firstColumn,
elementColumn: error.hiliteStart + 1,
elementLine: 1, // We will pass in the single-line code snippet generated from the html checker, so the elementLine is always 1
line: error.lastLine
};

return context.report(resource, null, error.message, null, position, Severity[error.subType], error.extract);
});

try {
await Promise.all(reportPromises);
} catch (e) {
debug(`Error reporting the html checker results.`);

return;
}
};

return {
'scan::end': end,
'targetfetch::end': start
};
},
meta: {
docs: {
category: 'Interoperability',
description: 'Validating html using `the Nu html checker`'
},
fixable: 'code',
recommended: true,
schema: [],
worksWithLocalFiles: false
}
};

module.exports = rule;
95 changes: 95 additions & 0 deletions tests/lib/rules/html-checker/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* eslint sort-keys: 0, no-undefined: 0 */

import * as mock from 'mock-require';

import { RuleTest } from '../../../helpers/rule-test-type'; // eslint-disable-line no-unused-vars
import * as ruleRunner from '../../../helpers/rule-runner';
import { getRuleName } from '../../../../src/lib/utils/rule-helpers';
const ruleName = getRuleName(__dirname);
const exampleUrl = 'https://example.com/';

const htmlCheckerMock = (messages) => {
const mockedChecker = () => {
if (!messages) {
return Promise.reject('Error.');
}

return Promise.resolve(messages);
};

mock('html-validator', mockedChecker);
};

// Html checker response that contains no errors
const noErrorJSON = {
url: exampleUrl,
messages: []
};

// Html checker response that contains errors/warnings
const errorJSON = {
url: exampleUrl,
messages: [
{
type: 'info',
lastLine: 1,
lastColumn: 3114,
firstColumn: 3046,
subType: 'error',
message: '“role="none"” is not yet supported in all browsers. Consider instead either using “role="presentation"” or “role="none presentation"”.',
extract: 'stration"><img src="/images/iceberg-left.svg" id="iceberg1" alt="" role="none"> <img ',
hiliteStart: 10,
hiliteLength: 69
},
{
type: 'info',
lastLine: 1,
lastColumn: 3462,
firstColumn: 3459,
subType: 'warning',
message: 'Consider using the “h1” element as a top-level heading only (all “h1” elements are treated as top-level headings by many screen readers and other tools)',
extract: '-section"><h1>example<',
hiliteStart: 10,
hiliteLength: 4
}
]
};

const testsForDefaults: Array<RuleTest> = [
{
name: 'No reports if html checker returns no messages',
serverUrl: exampleUrl,
before() {
htmlCheckerMock(noErrorJSON);
}
},
{
name: 'Reports warnings/errors if the html checker returns messages',
serverUrl: exampleUrl,
reports: [{
message: errorJSON.messages[0].message,
position: { column: errorJSON.messages[0].firstColumn, line: errorJSON.messages[0].lastLine }

}, {
message: errorJSON.messages[1].message,
position: { column: errorJSON.messages[1].firstColumn, line: errorJSON.messages[1].lastLine }
}],
before() {
htmlCheckerMock(errorJSON);
}
}
];

const testForErrors: Array<RuleTest> = [
{
name: 'Reports error when not able to get result from the HTML Checker',
serverUrl: exampleUrl,
reports: [{ message: `Couldn't get results from Html Checker for ${exampleUrl}.` }],
before() {
htmlCheckerMock(null);
}
}
];

ruleRunner.testRule(ruleName, testsForDefaults, { serial: true });
ruleRunner.testRule(ruleName, testForErrors);

0 comments on commit b744e53

Please sign in to comment.