-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
new_audit: add charset declaration audit #10284
Changes from 8 commits
a4f7f95
c7e0bce
f70bbae
7138424
40cbf99
f7b7f78
040d371
7d0aa10
593a9ca
c203014
b3373a2
e8d6a59
0819a00
fd8d954
c520a89
0e3fe0d
1880566
be329a3
25fa9f8
2d45a80
154aa10
4e85a84
925c202
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,84 @@ | ||||||
/** | ||||||
* @license Copyright 2016 Google Inc. All Rights Reserved. | ||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||||||
*/ | ||||||
|
||||||
/** | ||||||
* @fileoverview Audits a page to ensure charset it configured properly. | ||||||
* It must be defined within the first 1024 bytes of the HTML document, defined in the HTTP header, or in a BOM. | ||||||
Beytoven marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
*/ | ||||||
Beytoven marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
'use strict'; | ||||||
|
||||||
const Audit = require('../audit.js'); | ||||||
const i18n = require('../../lib/i18n/i18n.js'); | ||||||
const MainResource = require('../../computed/main-resource.js'); | ||||||
const CONTENT_TYPE_HEADER = 'content-type'; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yesterday we moved these down to above the class dfn. so i think you have a few more commits to push |
||||||
const CHARSET_META_REGEX = /<meta.*charset="?.{1,}"?.*>/gm; | ||||||
const CHARSET_HTTP_REGEX = /charset=.{1,}/gm; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i mentioned you could add these consts to the module.exports, and that way you can write some unit tests against them. iirc, you wrote these regexs to handle some extra fancy cases that the current unit tests dont cover. like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
let me know if you have any questions on this. i'm thinking 1 test with like 10-ish assertions using various html variants. |
||||||
|
||||||
const UIStrings = { | ||||||
/** Title of a Lighthouse audit that provides detail on if the charset is set properly for a page. This title is shown when the charset is defined correctly. */ | ||||||
Beytoven marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
title: 'Properly defines charset', | ||||||
/** Title of a Lighthouse audit that provides detail on if the charset is set properly for a page. This title is shown when the charset meta tag is missing or defined too late in the page. */ | ||||||
failureTitle: 'Charset element is missing or occurs too late on the page', | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
/** Description of a Lighthouse audit that tells the user why the charset needs to be defined early on. */ | ||||||
description: 'A character encoding declaration is required whether it is done explicitly ' + | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. slight rewording. let's remove mention of the BOM here, as I dont think we actually want to recommend it. the linked resource takes care of mentioning it anyway.
|
||||||
'in the first 1024 bytes of the page source, through a Byte Order Mark (BOM), ' + | ||||||
'or in the content-type HTTP header. ' + | ||||||
'[Learn more](https://www.w3.org/International/questions/qa-html-encoding-declarations).', | ||||||
}; | ||||||
|
||||||
const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings); | ||||||
|
||||||
class CharsetDefined extends Audit { | ||||||
/** | ||||||
* @return {LH.Audit.Meta} | ||||||
*/ | ||||||
static get meta() { | ||||||
return { | ||||||
id: 'charset', | ||||||
title: str_(UIStrings.title), | ||||||
failureTitle: str_(UIStrings.failureTitle), | ||||||
description: str_(UIStrings.description), | ||||||
requiredArtifacts: ['MainDocumentContent', 'URL', 'devtoolsLogs'], | ||||||
}; | ||||||
} | ||||||
|
||||||
/** | ||||||
* @param {LH.Artifacts} artifacts | ||||||
* @param {LH.Audit.Context} context | ||||||
* @return {Promise<LH.Audit.Product>} | ||||||
*/ | ||||||
static audit(artifacts, context) { | ||||||
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS]; | ||||||
return MainResource.request({devtoolsLog, URL: artifacts.URL}, context) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these days we'd write this with async await instead. a little nicer since you can drop the indentation below. i'd recommend it |
||||||
.then(mainResource => { | ||||||
let charsetIsSet = false; | ||||||
// Check the http header 'content-type' to see if charset is defined there | ||||||
if (mainResource.responseHeaders) { | ||||||
const contentTypeHeader = mainResource.responseHeaders | ||||||
.find(header => header.name.toLowerCase() === CONTENT_TYPE_HEADER); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nothing too fancy about this const so i'd inline it here. |
||||||
|
||||||
if (contentTypeHeader) { | ||||||
charsetIsSet = contentTypeHeader.value.match(CHARSET_HTTP_REGEX) !== null; | ||||||
} | ||||||
} | ||||||
|
||||||
// Check if there is a BOM byte marker | ||||||
const BOM_FIRSTCHAR = 65279; | ||||||
charsetIsSet = charsetIsSet || artifacts.MainDocumentContent.charCodeAt(0) === BOM_FIRSTCHAR; | ||||||
|
||||||
// Check if charset is defined within the first 1024 characters(~1024 bytes) of the HTML document | ||||||
charsetIsSet = charsetIsSet || | ||||||
artifacts.MainDocumentContent.slice(0, 1024).match(CHARSET_META_REGEX) !== null; | ||||||
|
||||||
return { | ||||||
score: Number(charsetIsSet), | ||||||
}; | ||||||
}); | ||||||
} | ||||||
} | ||||||
|
||||||
module.exports = CharsetDefined; | ||||||
module.exports.UIStrings = UIStrings; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,185 @@ | ||||||
/** | ||||||
* @license Copyright 2016 Google Inc. All Rights Reserved. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||||||
*/ | ||||||
'use strict'; | ||||||
|
||||||
const CharsetDefinedAudit = require('../../../audits/dobetterweb/charset.js'); | ||||||
const assert = require('assert'); | ||||||
const networkRecordsToDevtoolsLog = require('../../network-records-to-devtools-log.js'); | ||||||
|
||||||
/* eslint-env jest */ | ||||||
|
||||||
describe('Charset defined audit', () => { | ||||||
it('succeeds when the page contains the charset meta tag', () => { | ||||||
const finalUrl = 'https://example.com/'; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we try to strike a weird balance between DRY and WET in our tests. in this case i think this test would benefit from a if you search for |
||||||
const mainResource = { | ||||||
url: finalUrl, | ||||||
responseHeaders: [], | ||||||
}; | ||||||
const devtoolsLog = networkRecordsToDevtoolsLog([mainResource]); | ||||||
const artifacts = { | ||||||
devtoolsLogs: {[CharsetDefinedAudit.DEFAULT_PASS]: devtoolsLog}, | ||||||
URL: {finalUrl}, | ||||||
MainDocumentContent: '<meta charset="utf-8" />', | ||||||
}; | ||||||
|
||||||
const context = {computedCache: new Map()}; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not a huge deal but you could also move this computedCache thing into generateArtifacts and then you'd have something like...
|
||||||
return CharsetDefinedAudit.audit(artifacts, context).then(auditResult => { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah we can also go async/await here in the tests, too..
const auditResult = await CharsetDefinedAudit.audit(artifacts, context);
assert.equal(auditResult.score, 1); |
||||||
assert.equal(auditResult.score, 1); | ||||||
}); | ||||||
}); | ||||||
|
||||||
it('succeeds when the page has the charset defined in the content-type meta tag', () => { | ||||||
const finalUrl = 'https://example.com/'; | ||||||
const mainResource = { | ||||||
url: finalUrl, | ||||||
responseHeaders: [], | ||||||
}; | ||||||
const devtoolsLog = networkRecordsToDevtoolsLog([mainResource]); | ||||||
const artifacts = { | ||||||
devtoolsLogs: {[CharsetDefinedAudit.DEFAULT_PASS]: devtoolsLog}, | ||||||
URL: {finalUrl}, | ||||||
MainDocumentContent: '<meta http-equiv="Content-type" content="text/html; charset=utf-8" />', | ||||||
}; | ||||||
|
||||||
const context = {computedCache: new Map()}; | ||||||
return CharsetDefinedAudit.audit(artifacts, context).then(auditResult => { | ||||||
assert.equal(auditResult.score, 1); | ||||||
}); | ||||||
}); | ||||||
|
||||||
it('succeeds when the page has the charset defined in the content-type http header', () => { | ||||||
const finalUrl = 'https://example.com/'; | ||||||
const mainResource = { | ||||||
url: finalUrl, | ||||||
responseHeaders: [ | ||||||
{name: 'content-type', value: 'text/html; charset=UTF-8'}, | ||||||
], | ||||||
}; | ||||||
const devtoolsLog = networkRecordsToDevtoolsLog([mainResource]); | ||||||
const artifacts = { | ||||||
devtoolsLogs: {[CharsetDefinedAudit.DEFAULT_PASS]: devtoolsLog}, | ||||||
URL: {finalUrl}, | ||||||
MainDocumentContent: '<meta http-equiv="Content-type" content="text/html" />', | ||||||
}; | ||||||
|
||||||
const context = {computedCache: new Map()}; | ||||||
return CharsetDefinedAudit.audit(artifacts, context).then(auditResult => { | ||||||
assert.equal(auditResult.score, 1); | ||||||
}); | ||||||
}); | ||||||
|
||||||
it('succeeds when the page has the charset defined via BOM', () => { | ||||||
const finalUrl = 'https://example.com/'; | ||||||
const mainResource = { | ||||||
url: finalUrl, | ||||||
responseHeaders: [ | ||||||
{name: 'content-type', value: 'text/html'}, | ||||||
], | ||||||
}; | ||||||
const devtoolsLog = networkRecordsToDevtoolsLog([mainResource]); | ||||||
const artifacts = { | ||||||
devtoolsLogs: {[CharsetDefinedAudit.DEFAULT_PASS]: devtoolsLog}, | ||||||
URL: {finalUrl}, | ||||||
MainDocumentContent: '\ufeff<meta http-equiv="Content-type" content="text/html" />', | ||||||
}; | ||||||
|
||||||
const context = {computedCache: new Map()}; | ||||||
return CharsetDefinedAudit.audit(artifacts, context).then(auditResult => { | ||||||
assert.equal(auditResult.score, 1); | ||||||
}); | ||||||
}); | ||||||
|
||||||
it('fails when the page does not have charset defined', () => { | ||||||
const finalUrl = 'https://example.com/'; | ||||||
const mainResource = { | ||||||
url: finalUrl, | ||||||
responseHeaders: [ | ||||||
{name: 'content-type', value: 'text/html'}, | ||||||
], | ||||||
}; | ||||||
const devtoolsLog = networkRecordsToDevtoolsLog([mainResource]); | ||||||
const artifacts = { | ||||||
devtoolsLogs: {[CharsetDefinedAudit.DEFAULT_PASS]: devtoolsLog}, | ||||||
URL: {finalUrl}, | ||||||
MainDocumentContent: '<meta http-equiv="Content-type" content="text/html" />', | ||||||
}; | ||||||
|
||||||
const context = {computedCache: new Map()}; | ||||||
return CharsetDefinedAudit.audit(artifacts, context).then(auditResult => { | ||||||
assert.equal(auditResult.score, 0); | ||||||
}); | ||||||
}); | ||||||
|
||||||
it('fails when the page has charset defined too late in the page', () => { | ||||||
const finalUrl = 'https://example.com/'; | ||||||
const mainResource = { | ||||||
url: finalUrl, | ||||||
responseHeaders: [ | ||||||
{name: 'content-type', value: 'text/html'}, | ||||||
], | ||||||
}; | ||||||
const bigString = new Array(1024).fill(' ').join(''); | ||||||
const devtoolsLog = networkRecordsToDevtoolsLog([mainResource]); | ||||||
const artifacts = { | ||||||
devtoolsLogs: {[CharsetDefinedAudit.DEFAULT_PASS]: devtoolsLog}, | ||||||
URL: {finalUrl}, | ||||||
MainDocumentContent: '<html><head>' + bigString + '<meta charset="utf-8" />hello', | ||||||
}; | ||||||
|
||||||
const context = {computedCache: new Map()}; | ||||||
return CharsetDefinedAudit.audit(artifacts, context).then(auditResult => { | ||||||
assert.equal(auditResult.score, 0); | ||||||
}); | ||||||
}); | ||||||
|
||||||
it('passes when the page has charset defined almost too late in the page', () => { | ||||||
const finalUrl = 'https://example.com/'; | ||||||
const mainResource = { | ||||||
url: finalUrl, | ||||||
responseHeaders: [ | ||||||
{name: 'content-type', value: 'text/html'}, | ||||||
], | ||||||
}; | ||||||
const bigString = new Array(900).fill(' ').join(''); | ||||||
const devtoolsLog = networkRecordsToDevtoolsLog([mainResource]); | ||||||
const artifacts = { | ||||||
devtoolsLogs: {[CharsetDefinedAudit.DEFAULT_PASS]: devtoolsLog}, | ||||||
URL: {finalUrl}, | ||||||
MainDocumentContent: '<html><head>' + bigString + '<meta charset="utf-8" />hello', | ||||||
}; | ||||||
|
||||||
const context = {computedCache: new Map()}; | ||||||
return CharsetDefinedAudit.audit(artifacts, context).then(auditResult => { | ||||||
assert.equal(auditResult.score, 1); | ||||||
}); | ||||||
}); | ||||||
|
||||||
it('fails when charset only partially defined in the first 1024 bytes of the page', () => { | ||||||
const finalUrl = 'https://example.com/'; | ||||||
const mainResource = { | ||||||
url: finalUrl, | ||||||
responseHeaders: [ | ||||||
{name: 'content-type', value: 'text/html'}, | ||||||
], | ||||||
}; | ||||||
const prelude = '<html><head>'; | ||||||
const charsetHTML = '<meta charset="utf-8" />'; | ||||||
// 1024 bytes should be halfway through the meta tag | ||||||
const bigString = new Array(1024 - prelude.length - charsetHTML.length / 2).fill(' ').join(''); | ||||||
|
||||||
const devtoolsLog = networkRecordsToDevtoolsLog([mainResource]); | ||||||
const artifacts = { | ||||||
devtoolsLogs: {[CharsetDefinedAudit.DEFAULT_PASS]: devtoolsLog}, | ||||||
URL: {finalUrl}, | ||||||
MainDocumentContent: prelude + bigString + charsetHTML + 'hello', | ||||||
}; | ||||||
|
||||||
const context = {computedCache: new Map()}; | ||||||
return CharsetDefinedAudit.audit(artifacts, context).then(auditResult => { | ||||||
assert.equal(auditResult.score, 0); | ||||||
}); | ||||||
}); | ||||||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.