Repository: GoogleChrome/lighthouse (js)

Issue:
Audit doctype
Recommend `<!DOCTYPE html>` to avoid quirks mode.

https://developer.mozilla.org/en-US/docs/Glossary/Doctype

Pull request description:
new_audit(dobetterweb): add new audit for doctype
[WIP] Adds audit for `Doctype`

Closes #5213

Patch:
diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md
index 3973f3038..8124528f7 100644
--- a/.github/ISSUE_TEMPLATE/Bug_report.md
+++ b/.github/ISSUE_TEMPLATE/Bug_report.md
@@ -6,8 +6,6 @@ about: Report something working incorrectly
 
 <!-- Before creating an issue please make sure you are using the latest version and have checked for duplicate issues. -->
 
-## Bug report
-
 #### Provide the steps to reproduce
 1. Run LH on <affected url>
 
diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md
index d7a01a24e..aaa6fcf6c 100644
--- a/.github/ISSUE_TEMPLATE/Feature_request.md
+++ b/.github/ISSUE_TEMPLATE/Feature_request.md
@@ -8,9 +8,7 @@ about: Suggest an idea to us!
 
 <!-- If this is a new audit please review the audit doc https://github.com/GoogleChrome/lighthouse/blob/master/docs/new-audits.md -->
 
-## Feature request
-
-**Summary**
+**Feature request summary**
 <!-- Describe what you want to be added -->
 <!-- Are you willing to work on this yourself? -->
 
diff --git a/lighthouse-core/audits/dobetterweb/doctype.js b/lighthouse-core/audits/dobetterweb/doctype.js
new file mode 100644
index 000000000..ec2e71150
--- /dev/null
+++ b/lighthouse-core/audits/dobetterweb/doctype.js
@@ -0,0 +1,73 @@
+/**
+ * @license Copyright 2018 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.
+ */
+'use strict';
+
+const Audit = require('../audit');
+
+class Doctype extends Audit {
+  /**
+   * @return {LH.Audit.Meta}
+   */
+  static get meta() {
+    return {
+      id: 'doctype',
+      title: 'Page has the HTML doctype',
+      failureTitle: 'Page is missing the HTML doctype',
+      description: 'Specifying a doctype prevents the browser from switching to quirks-mode.' +
+                'Read more on the ' +
+                '[MDN Web Docs page](https://developer.mozilla.org/en-US/docs/Glossary/Doctype)',
+      requiredArtifacts: ['Doctype'],
+    };
+  }
+
+  /**
+   * @param {LH.Artifacts} artifacts
+   * @return {LH.Audit.Product}
+   */
+  static audit(artifacts) {
+    if (!artifacts.Doctype) {
+      return {
+        rawValue: false,
+        explanation: 'Document must contain a doctype',
+      };
+    }
+
+    // only set constants once we know there is a doctype
+    const doctypeName = artifacts.Doctype.name.trim();
+    const doctypePublicId = artifacts.Doctype.publicId;
+    const doctypeSystemId = artifacts.Doctype.systemId;
+
+    if (doctypePublicId !== '') {
+      return {
+        rawValue: false,
+        explanation: 'Expected publicId to be an empty string',
+      };
+    }
+
+    if (doctypeSystemId !== '') {
+      return {
+        rawValue: false,
+        explanation: 'Expected systemId to be an empty string',
+      };
+    }
+
+    /* Note that the value for name is case sensitive,
+       and must be the string `html`. For details see:
+       https://html.spec.whatwg.org/multipage/parsing.html#the-initial-insertion-mode */
+    if (doctypeName === 'html') {
+      return {
+        rawValue: true,
+      };
+    } else {
+      return {
+        rawValue: false,
+        explanation: 'Doctype name must be the lowercase string `html`',
+      };
+    }
+  }
+}
+
+module.exports = Doctype;
diff --git a/lighthouse-core/config/default-config.js b/lighthouse-core/config/default-config.js
index f53971884..e81a856b4 100644
--- a/lighthouse-core/config/default-config.js
+++ b/lighthouse-core/config/default-config.js
@@ -1,5 +1,5 @@
 /**
- * @license Copyright 2017 Google Inc. All Rights Reserved.
+ * @license Copyright 2018 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.
  */
@@ -31,6 +31,7 @@ module.exports = {
       'accessibility',
       'dobetterweb/anchors-with-no-rel-noopener',
       'dobetterweb/appcache',
+      'dobetterweb/doctype',
       'dobetterweb/domstats',
       'dobetterweb/js-libraries',
       'dobetterweb/optimized-images',
@@ -161,6 +162,7 @@ module.exports = {
     'byte-efficiency/uses-responsive-images',
     'byte-efficiency/efficient-animated-content',
     'dobetterweb/appcache-manifest',
+    'dobetterweb/doctype',
     'dobetterweb/dom-size',
     'dobetterweb/external-anchors-use-rel-noopener',
     'dobetterweb/geolocation-on-start',
@@ -373,6 +375,7 @@ module.exports = {
         {id: 'no-document-write', weight: 1},
         {id: 'external-anchors-use-rel-noopener', weight: 1},
         {id: 'geolocation-on-start', weight: 1},
+        {id: 'doctype', weight: 1},
         {id: 'no-vulnerable-libraries', weight: 1},
         {id: 'notification-on-start', weight: 1},
         {id: 'deprecations', weight: 1},
diff --git a/lighthouse-core/gather/gatherers/dobetterweb/doctype.js b/lighthouse-core/gather/gatherers/dobetterweb/doctype.js
new file mode 100644
index 000000000..65d88f054
--- /dev/null
+++ b/lighthouse-core/gather/gatherers/dobetterweb/doctype.js
@@ -0,0 +1,36 @@
+/**
+ * @license Copyright 2018 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.
+ */
+'use strict';
+
+const Gatherer = require('../gatherer');
+
+/**
+ * Get and return `name`, `publicId`, `systemId` from
+ * `document.doctype`
+ * @return {{name: string, publicId: string, systemId: string} | null}
+ */
+function getDoctype() {
+  // An example of this is warnerbros.com/archive/spacejam/movie/jam.htm
+  if (!document.doctype) { // eslint-disable-line no-undef
+    return null;
+  }
+
+  const {name, publicId, systemId} = document.doctype; // eslint-disable-line no-undef
+  return {name, publicId, systemId};
+}
+
+class Doctype extends Gatherer {
+  /**
+   * @param {LH.Gatherer.PassContext} passContext
+   * @return {Promise<LH.Artifacts['Doctype']>}
+   */
+  afterPass(passContext) {
+    const driver = passContext.driver;
+    return driver.evaluateAsync(`(${getDoctype.toString()}())`);
+  }
+}
+
+module.exports = Doctype;
diff --git a/lighthouse-core/gather/gatherers/fonts.js b/lighthouse-core/gather/gatherers/fonts.js
index 7dc0e1f64..f580e476c 100644
--- a/lighthouse-core/gather/gatherers/fonts.js
+++ b/lighthouse-core/gather/gatherers/fonts.js
@@ -140,8 +140,8 @@ function getFontFaceFromStylesheets() {
       oldNode.parentNode && oldNode.parentNode.insertBefore(newNode, oldNode);
       oldNode.remove();
 
-      // Give each stylesheet 1s to load before giving up
-      setTimeout(() => resolve([{err: {message: 'Could not load stylesheet (timeout)'}}]), 1000);
+      // Give each stylesheet 5s to load before giving up
+      setTimeout(() => resolve([{err: {message: 'Could not load stylesheet (timeout)'}}]), 5000);
     });
   }
 
diff --git a/typings/artifacts.d.ts b/typings/artifacts.d.ts
index 4d1798eae..28a1b3257 100644
--- a/typings/artifacts.d.ts
+++ b/typings/artifacts.d.ts
@@ -56,6 +56,9 @@ declare global {
       CrawlableLinks: {href: string, text: string}[];
       /** CSS coverage information for styles used by page's final state. */
       CSSUsage: {rules: Crdp.CSS.RuleUsage[], stylesheets: Artifacts.CSSStyleSheetInfo[]};
+      /** Information on the document's doctype(or null if not present), specifically the name, publicId, and systemId.
+          All properties default to an empty string if not present */
+      Doctype: Artifacts.Doctype | null;
       /** Information on the size of all DOM nodes in the page and the most extreme members. */
       DOMStats: Artifacts.DOMStats;
       /** Relevant attributes and child properties of all <object>s, <embed>s and <applet>s in the page. */
@@ -173,6 +176,12 @@ declare global {
         content: string;
       }
 
+      export interface Doctype {
+        name: string;
+        publicId: string;
+        systemId: string;
+      }
+
       export interface DOMStats {
         totalDOMNodes: number;
         width: {max: number, pathToElement: Array<string>, snippet: string};


Test patch:
diff --git a/lighthouse-core/test/audits/dobetterweb/doctype-test.js b/lighthouse-core/test/audits/dobetterweb/doctype-test.js
new file mode 100644
index 000000000..937aa568a
--- /dev/null
+++ b/lighthouse-core/test/audits/dobetterweb/doctype-test.js
@@ -0,0 +1,80 @@
+/**
+ * @license Copyright 2018 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.
+ */
+'use strict';
+
+const Audit = require('../../../audits/dobetterweb/doctype.js');
+const assert = require('assert');
+
+/* eslint-env mocha */
+
+describe('DOBETTERWEB: doctype audit', () => {
+  it('fails when document does not contain a doctype', () => {
+    const auditResult = Audit.audit({
+      Doctype: null,
+    });
+    assert.equal(auditResult.rawValue, false);
+    assert.equal(auditResult.explanation, 'Document must contain a doctype');
+  });
+
+  it('fails when the value of the name attribute is a value other then lowercase "html"', () => {
+    const auditResult = Audit.audit({
+      Doctype: {
+        name: 'xml',
+        publicId: '',
+        systemId: '',
+      },
+    });
+    assert.equal(auditResult.rawValue, false);
+    assert.equal(auditResult.explanation, 'Doctype name must be the lowercase string `html`');
+  });
+
+  it('fails when the value of the name attribute is not the lowercase string "html"', () => {
+    const auditResult = Audit.audit({
+      Doctype: {
+        name: 'HTML',
+        publicId: '',
+        systemId: '',
+      },
+    });
+    assert.equal(auditResult.rawValue, false);
+    assert.equal(auditResult.explanation, 'Doctype name must be the lowercase string `html`');
+  });
+
+  it('fails when the publicId attribute is not an empty string', () => {
+    const auditResult = Audit.audit({
+      Doctype: {
+        name: 'html',
+        publicId: '189655',
+        systemId: '',
+      },
+    });
+    assert.equal(auditResult.rawValue, false);
+    assert.equal(auditResult.explanation, 'Expected publicId to be an empty string');
+  });
+
+  it('fails when the systemId attribute is not an empty string', () => {
+    const auditResult = Audit.audit({
+      Doctype: {
+        name: 'html',
+        publicId: '',
+        systemId: '189655',
+      },
+    });
+    assert.equal(auditResult.rawValue, false);
+    assert.equal(auditResult.explanation, 'Expected systemId to be an empty string');
+  });
+
+  it('succeeds when document contains a doctype, and the name value is "html"', () => {
+    const auditResult = Audit.audit({
+      Doctype: {
+        name: 'html',
+        publicId: '',
+        systemId: '',
+      },
+    });
+    assert.equal(auditResult.rawValue, true);
+  });
+});
diff --git a/lighthouse-core/test/results/artifacts/artifacts.json b/lighthouse-core/test/results/artifacts/artifacts.json
index 9a98b1cb6..ee1761c5e 100644
--- a/lighthouse-core/test/results/artifacts/artifacts.json
+++ b/lighthouse-core/test/results/artifacts/artifacts.json
@@ -2521,5 +2521,10 @@
   },
   "HTMLWithoutJavaScript": {
     "bodyText": "Do better web tester page\nHi there!\ntouchmove section\n "
+  },
+  "Doctype": {
+    "name": "html",
+    "systemId": "",
+    "publicId": ""
   }
 }
diff --git a/lighthouse-core/test/results/sample_v2.json b/lighthouse-core/test/results/sample_v2.json
index 4a535323d..28cad43f8 100644
--- a/lighthouse-core/test/results/sample_v2.json
+++ b/lighthouse-core/test/results/sample_v2.json
@@ -2202,6 +2202,14 @@
       "rawValue": false,
       "displayValue": "Found \"clock.appcache\""
     },
+    "doctype": {
+      "id": "doctype",
+      "title": "Page has the HTML doctype",
+      "description": "Specifying a doctype prevents the browser from switching to quirks-mode.Read more on the [MDN Web Docs page](https://developer.mozilla.org/en-US/docs/Glossary/Doctype)",
+      "score": 1,
+      "scoreDisplayMode": "binary",
+      "rawValue": true
+    },
     "dom-size": {
       "id": "dom-size",
       "title": "Avoids an excessive DOM size",
@@ -3261,6 +3269,10 @@
           "id": "geolocation-on-start",
           "weight": 1
         },
+        {
+          "id": "doctype",
+          "weight": 1
+        },
         {
           "id": "no-vulnerable-libraries",
           "weight": 1
@@ -3287,7 +3299,7 @@
         }
       ],
       "id": "best-practices",
-      "score": 0
+      "score": 0.07
     },
     "seo": {
       "title": "SEO",