Audit: DOM stats (total nodes, depth, width) and hero card formatter (#1673)
* Audit: num DOM nodes created by the page * update gatherer * More useful audit * Add card formatter * Cleanup * audit file rename * Update audit tests. Add pretty print support for card formatter * css tweaks * Smoketests, card formatter tests * Ignore in page functions in istanbul * feedback * feedback
This commit is contained in:
Родитель
f79adc793c
Коммит
220b9f0171
|
@ -0,0 +1,94 @@
|
|||
<!doctype html>
|
||||
<!--
|
||||
* Copyright 2017 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.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>DoBetterWeb - DOM size tester</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<section>
|
||||
<div>
|
||||
<div>6</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<div id="attachshadow">7</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<template>
|
||||
<div><div><div><div>10, but noop in template</div></div></div></div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
<div id="attachshadow-big" class="test this out">4</div>
|
||||
<div>3</div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</footer>
|
||||
<script>
|
||||
function withShadowDOMTest() {
|
||||
const el = document.querySelector('#attachshadow');
|
||||
el.innerHTML = `<div>7</div>`;
|
||||
el.attachShadow({mode: 'open'}).innerHTML = `
|
||||
<div>
|
||||
<div class="here">9</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const el2 = document.querySelector('#attachshadow-big');
|
||||
el2.attachShadow({mode: 'open'}).innerHTML = `
|
||||
<div>
|
||||
<div>6</div>
|
||||
<div>6</div>
|
||||
<div>6</div>
|
||||
<div>6</div>
|
||||
<div>6</div>
|
||||
<div>6</div>
|
||||
<div>6</div>
|
||||
<div>6</div>
|
||||
<div>6</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function domTest(numNodes=1500) {
|
||||
const frag = new DocumentFragment();
|
||||
for (let i = 0; i < numNodes; ++i) {
|
||||
frag.appendChild(document.createElement('span'));
|
||||
}
|
||||
document.body.appendChild(frag);
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
if (params.has('smallDOM')) {
|
||||
domTest(1300);
|
||||
} else if (params.has('largeDOM')) {
|
||||
domTest(6000);
|
||||
}
|
||||
if (params.has('withShadowDOM')) {
|
||||
withShadowDOMTest();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -148,6 +148,51 @@ module.exports = [
|
|||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
initialUrl: 'http://localhost:10200/dobetterweb/domtester.html?smallDOM',
|
||||
url: 'http://localhost:10200/dobetterweb/domtester.html?smallDOM',
|
||||
audits: {
|
||||
'dom-size': {
|
||||
score: 100,
|
||||
extendedInfo: {
|
||||
value: {
|
||||
0: {value: '1,323'},
|
||||
1: {value: '7'},
|
||||
2: {value: '1,303'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
initialUrl: 'http://localhost:10200/dobetterweb/domtester.html?largeDOM&withShadowDOM',
|
||||
url: 'http://localhost:10200/dobetterweb/domtester.html?largeDOM&withShadowDOM',
|
||||
audits: {
|
||||
'dom-size': {
|
||||
score: 0,
|
||||
extendedInfo: {
|
||||
value: {
|
||||
0: {value: '6,024'},
|
||||
1: {value: '9'},
|
||||
2: {value: '6,003'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
initialUrl: 'http://localhost:10200/dobetterweb/domtester.html?withShadowDOM',
|
||||
url: 'http://localhost:10200/dobetterweb/domtester.html?withShadowDOM',
|
||||
audits: {
|
||||
'dom-size': {
|
||||
score: 100,
|
||||
extendedInfo: {
|
||||
value: {
|
||||
0: {value: '24'},
|
||||
1: {value: '9'},
|
||||
2: {value: '9'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
initialUrl: 'http://localhost:10200/online-only.html',
|
||||
url: 'http://localhost:10200/online-only.html',
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2017 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 see how the size of DOM it creates. Stats like
|
||||
* tree depth, # children, and total nodes are returned. The score is calculated
|
||||
* based solely on the total number of nodes found on the page.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Audit = require('../audit');
|
||||
const TracingProcessor = require('../../lib/traces/tracing-processor');
|
||||
const Formatter = require('../../formatters/formatter');
|
||||
|
||||
const MAX_DOM_NODES = 1500;
|
||||
const MAX_DOM_TREE_WIDTH = 60;
|
||||
const MAX_DOM_TREE_DEPTH = 32;
|
||||
|
||||
// Parameters for log-normal CDF scoring. See https://www.desmos.com/calculator/9cyxpm5qgp.
|
||||
const SCORING_POINT_OF_DIMINISHING_RETURNS = 2400;
|
||||
const SCORING_MEDIAN = 3000;
|
||||
|
||||
class DOMSize extends Audit {
|
||||
static get MAX_DOM_NODES() {
|
||||
return MAX_DOM_NODES;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!AuditMeta}
|
||||
*/
|
||||
static get meta() {
|
||||
return {
|
||||
category: 'Performance',
|
||||
name: 'dom-size',
|
||||
description: 'Avoids an excessive DOM size',
|
||||
optimalValue: DOMSize.MAX_DOM_NODES.toLocaleString() + ' nodes',
|
||||
helpText: 'Browser engineers recommend pages contain fewer than ' +
|
||||
`~${DOMSize.MAX_DOM_NODES.toLocaleString()} DOM nodes. The sweet spot is a tree depth < ` +
|
||||
`${MAX_DOM_TREE_DEPTH} elements and fewer than ${MAX_DOM_TREE_WIDTH} ` +
|
||||
'children/parent element. A large DOM can increase memory, cause longer ' +
|
||||
'[style calculations](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), ' +
|
||||
'and produce costly [layout reflows](https://developers.google.com/speed/articles/reflow). [Learn more](https://developers.google.com/web/fundamentals/performance/rendering/).',
|
||||
requiredArtifacts: ['DOMStats']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Artifacts} artifacts
|
||||
* @return {!AuditResult}
|
||||
*/
|
||||
static audit(artifacts) {
|
||||
const stats = artifacts.DOMStats;
|
||||
|
||||
/**
|
||||
* html >
|
||||
* body >
|
||||
* div >
|
||||
* span
|
||||
*/
|
||||
const depthSnippet = stats.depth.pathToElement.reduce((str, curr, i) => {
|
||||
return `${str}\n` + ' '.repeat(i) + `${curr} >`;
|
||||
}, '').replace(/>$/g, '').trim();
|
||||
const widthSnippet = 'Element with most children:\n' +
|
||||
stats.width.pathToElement[stats.width.pathToElement.length - 1];
|
||||
|
||||
// Use the CDF of a log-normal distribution for scoring.
|
||||
// <= 1500: score≈100
|
||||
// 3000: score=50
|
||||
// >= 5970: score≈0
|
||||
const distribution = TracingProcessor.getLogNormalDistribution(
|
||||
SCORING_MEDIAN, SCORING_POINT_OF_DIMINISHING_RETURNS);
|
||||
let score = 100 * distribution.computeComplementaryPercentile(stats.totalDOMNodes);
|
||||
|
||||
// Clamp the score to 0 <= x <= 100.
|
||||
score = Math.max(0, Math.min(100, score));
|
||||
|
||||
const cards = [
|
||||
{title: 'Total DOM Nodes', value: stats.totalDOMNodes.toLocaleString()},
|
||||
{title: 'DOM Depth', value: stats.depth.max.toLocaleString(), snippet: depthSnippet},
|
||||
{title: 'Maximum Children', value: stats.width.max.toLocaleString(), snippet: widthSnippet}
|
||||
];
|
||||
|
||||
return DOMSize.generateAuditResult({
|
||||
rawValue: stats.totalDOMNodes,
|
||||
optimalValue: this.meta.optimalValue,
|
||||
score: Math.round(score),
|
||||
displayValue: `${stats.totalDOMNodes.toLocaleString()} nodes`,
|
||||
extendedInfo: {
|
||||
formatter: Formatter.SUPPORTED_FORMATS.CARD,
|
||||
value: cards
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = DOMSize;
|
|
@ -43,6 +43,7 @@
|
|||
"dobetterweb/document-write",
|
||||
"dobetterweb/geolocation-on-start",
|
||||
"dobetterweb/notification-on-start",
|
||||
"dobetterweb/domstats",
|
||||
"dobetterweb/optimized-images",
|
||||
"dobetterweb/tags-blocking-first-paint",
|
||||
"dobetterweb/websql"
|
||||
|
@ -87,8 +88,9 @@
|
|||
"byte-efficiency/unused-css-rules",
|
||||
"byte-efficiency/uses-optimized-images",
|
||||
"byte-efficiency/uses-responsive-images",
|
||||
"dobetterweb/external-anchors-use-rel-noopener",
|
||||
"dobetterweb/appcache-manifest",
|
||||
"dobetterweb/dom-size",
|
||||
"dobetterweb/external-anchors-use-rel-noopener",
|
||||
"dobetterweb/geolocation-on-start",
|
||||
"dobetterweb/link-blocking-first-paint",
|
||||
"dobetterweb/no-console-time",
|
||||
|
@ -380,6 +382,10 @@
|
|||
"categorizable": false,
|
||||
"items": [{
|
||||
"audits": {
|
||||
"dom-size": {
|
||||
"expectedValue": 100,
|
||||
"weight": 1
|
||||
},
|
||||
"critical-request-chains": {
|
||||
"expectedValue": 0,
|
||||
"weight": 1
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2017 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 Formatter = require('./formatter');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const html = fs.readFileSync(path.join(__dirname, 'partials/cards.html'), 'utf8');
|
||||
|
||||
class Card extends Formatter {
|
||||
static getFormatter(type) {
|
||||
switch (type) {
|
||||
case 'pretty':
|
||||
return result => {
|
||||
if (!result || !Array.isArray(result)) {
|
||||
return '';
|
||||
}
|
||||
let output = '';
|
||||
result.forEach(item => {
|
||||
output += ` - ${item.title}: ${item.value}\n`;
|
||||
});
|
||||
return output;
|
||||
};
|
||||
|
||||
case 'html':
|
||||
// Returns a handlebars string to be used by the Report.
|
||||
return html;
|
||||
|
||||
default:
|
||||
throw new Error('Unknown formatter type');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Card;
|
|
@ -40,6 +40,7 @@ class Formatter {
|
|||
static _getFormatters() {
|
||||
this._formatters = {
|
||||
accessibility: require('./accessibility'),
|
||||
card: require('./cards'),
|
||||
criticalRequestChains: require('./critical-request-chains'),
|
||||
urllist: require('./url-list'),
|
||||
null: require('./null-formatter'),
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
.cards__container {
|
||||
--padding: 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.scorecard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1 1 150px;
|
||||
flex-direction: column;
|
||||
padding: calc(var(--padding) / 2);
|
||||
padding-top: calc(32px + var(--padding) / 2);
|
||||
border-radius: 3px;
|
||||
margin-right: var(--padding);
|
||||
position: relative;
|
||||
color: var(--secondary-text-color);
|
||||
line-height: inherit;
|
||||
border: 1px solid #ebebeb;
|
||||
}
|
||||
.scorecard-title {
|
||||
/*text-transform: uppercase;*/
|
||||
font-size: var(--subitem-font-size);
|
||||
line-height: var(--heading-line-height);
|
||||
background-color: #eee;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
}
|
||||
.scorecard-value {
|
||||
font-size: 24px;
|
||||
}
|
||||
.scorecard-summary {
|
||||
margin-top: calc(var(--padding) / 2);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<ul class="subitem__details cards__container">
|
||||
{{#each this}}
|
||||
<div class="subitem__detail scorecard" {{#if snippet}}title="{{snippet}}"{{/if}}>
|
||||
<div class="scorecard-title">{{title}}</div>
|
||||
<div class="scorecard-value">{{value}}</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</ul>
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2017 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 Gathers stats about the max height and width of the DOM tree
|
||||
* and total number of nodes used on the page.
|
||||
*/
|
||||
|
||||
/* global document */
|
||||
|
||||
'use strict';
|
||||
|
||||
const Gatherer = require('../gatherer');
|
||||
|
||||
/**
|
||||
* Constructs a pretty label from element's selectors. For example, given
|
||||
* <div id="myid" class="myclass">, returns 'div#myid.myclass'.
|
||||
* @param {!HTMLElement} element
|
||||
* @return {!string}
|
||||
*/
|
||||
/* istanbul ignore next */
|
||||
function createSelectorsLabel(element) {
|
||||
let name = element.localName;
|
||||
const idAttr = element.getAttribute && element.getAttribute('id');
|
||||
if (idAttr) {
|
||||
name += `#${idAttr}`;
|
||||
}
|
||||
const className = element.className;
|
||||
if (className) {
|
||||
name += `.${className.replace(/\s+/g, '.')}`;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!HTMLElement} element
|
||||
* @return {!Array<string>}
|
||||
*/
|
||||
/* istanbul ignore next */
|
||||
function elementPathInDOM(element) {
|
||||
const path = [createSelectorsLabel(element)];
|
||||
let node = element;
|
||||
while (node) {
|
||||
node = node.parentNode && node.parentNode.host ? node.parentNode.host : node.parentElement;
|
||||
if (node) {
|
||||
path.unshift(createSelectorsLabel(node));
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the maximum tree depth of the DOM.
|
||||
* @param {!HTMLElement} element Root of the tree to look in.
|
||||
* @param {boolean=} deep True to include shadow roots. Defaults to true.
|
||||
* @return {!number}
|
||||
*/
|
||||
/* istanbul ignore next */
|
||||
function getDOMStats(element, deep=true) {
|
||||
let deepestNode = null;
|
||||
let maxDepth = 0;
|
||||
let maxWidth = 0;
|
||||
let parentWithMostChildren = null;
|
||||
|
||||
const _calcDOMWidthAndHeight = function(element, depth=1) {
|
||||
if (depth > maxDepth) {
|
||||
deepestNode = element;
|
||||
maxDepth = depth;
|
||||
}
|
||||
if (element.children.length > maxWidth) {
|
||||
parentWithMostChildren = element;
|
||||
maxWidth = element.children.length;
|
||||
}
|
||||
|
||||
let child = element.firstElementChild;
|
||||
while (child) {
|
||||
_calcDOMWidthAndHeight(child, depth + 1);
|
||||
// If node has shadow dom, traverse into that tree.
|
||||
if (deep && child.shadowRoot) {
|
||||
_calcDOMWidthAndHeight(child.shadowRoot, depth + 1);
|
||||
}
|
||||
child = child.nextElementSibling;
|
||||
}
|
||||
|
||||
return {maxDepth, maxWidth};
|
||||
};
|
||||
|
||||
const result = _calcDOMWidthAndHeight(element);
|
||||
|
||||
return {
|
||||
totalDOMNodes: document.querySelectorAll('html, html /deep/ *').length,
|
||||
depth: {
|
||||
max: result.maxDepth,
|
||||
pathToElement: elementPathInDOM(deepestNode),
|
||||
},
|
||||
width: {
|
||||
max: result.maxWidth,
|
||||
pathToElement: elementPathInDOM(parentWithMostChildren)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class DOMStats extends Gatherer {
|
||||
/**
|
||||
* @param {!Object} options
|
||||
* @return {!Promise<!Array<!Object>>}
|
||||
*/
|
||||
afterPass(options) {
|
||||
const expression = `(function() {
|
||||
${createSelectorsLabel.toString()};
|
||||
${elementPathInDOM.toString()};
|
||||
return (${getDOMStats.toString()}(document.documentElement));
|
||||
})()`;
|
||||
return options.driver.evaluateAsync(expression);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DOMStats;
|
|
@ -64,6 +64,7 @@ class ReportGenerator {
|
|||
// Cannot DRY this up and dynamically create paths because fs.readdirSync
|
||||
// doesn't browserify well with a variable path. See https://github.com/substack/brfs/issues/36.
|
||||
const partialStyles = [
|
||||
fs.readFileSync(__dirname + '/../formatters/partials/cards.css', 'utf8'),
|
||||
fs.readFileSync(__dirname + '/../formatters/partials/critical-request-chains.css', 'utf8'),
|
||||
fs.readFileSync(__dirname + '/../formatters/partials/table.css', 'utf8'),
|
||||
fs.readFileSync(__dirname + '/../formatters/partials/url-list.css', 'utf8'),
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Copyright 2017 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 DOMSize = require('../../../audits/dobetterweb/dom-size.js');
|
||||
const assert = require('assert');
|
||||
|
||||
/* eslint-env mocha */
|
||||
|
||||
describe('Num DOM nodes audit', () => {
|
||||
const numNodes = DOMSize.MAX_DOM_NODES;
|
||||
const artifact = {
|
||||
DOMStats: {
|
||||
totalDOMNodes: numNodes,
|
||||
depth: {max: 1, pathToElement: ['html', 'body', 'div', 'span']},
|
||||
width: {max: 2, pathToElement: ['html', 'body']}
|
||||
}
|
||||
};
|
||||
|
||||
const snippet = 'html >\n' +
|
||||
' body >\n' +
|
||||
' div >\n' +
|
||||
' span';
|
||||
|
||||
it('calculates score hitting top of distribution', () => {
|
||||
const auditResult = DOMSize.audit(artifact);
|
||||
assert.equal(auditResult.score, 100);
|
||||
assert.equal(auditResult.rawValue, numNodes);
|
||||
assert.equal(auditResult.optimalValue, `${DOMSize.MAX_DOM_NODES.toLocaleString()} nodes`);
|
||||
assert.equal(auditResult.displayValue, `${numNodes.toLocaleString()} nodes`);
|
||||
assert.equal(auditResult.extendedInfo.value[0].title, 'Total DOM Nodes');
|
||||
assert.equal(auditResult.extendedInfo.value[0].value, numNodes.toLocaleString());
|
||||
assert.equal(auditResult.extendedInfo.value[1].title, 'DOM Depth');
|
||||
assert.equal(auditResult.extendedInfo.value[1].value, 1);
|
||||
assert.equal(auditResult.extendedInfo.value[1].snippet, snippet, 'generates snippet');
|
||||
assert.equal(auditResult.extendedInfo.value[2].title, 'Maximum Children');
|
||||
assert.equal(auditResult.extendedInfo.value[2].value, 2);
|
||||
});
|
||||
|
||||
it('calculates score hitting mid distribution', () => {
|
||||
artifact.DOMStats.totalDOMNodes = 3100;
|
||||
assert.equal(DOMSize.audit(artifact).score, 43);
|
||||
});
|
||||
|
||||
it('calculates score hitting bottom of distribution', () => {
|
||||
artifact.DOMStats.totalDOMNodes = 5970;
|
||||
assert.equal(DOMSize.audit(artifact).score, 0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Copyright 2017 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';
|
||||
|
||||
/* eslint-env mocha */
|
||||
|
||||
const CardsFormatter = require('../../formatters/cards.js');
|
||||
const Handlebars = require('handlebars');
|
||||
const assert = require('assert');
|
||||
|
||||
describe('CardsFormatter', () => {
|
||||
const extendedInfo = {
|
||||
value: [
|
||||
{title: 'Total DOM Nodes', value: 3500},
|
||||
{title: 'DOM Depth', value: 10, snippet: 'snippet'},
|
||||
{title: 'Maximum Children', value: 20, snippet: 'snippet2'}
|
||||
]
|
||||
};
|
||||
|
||||
it('handles invalid input', () => {
|
||||
const pretty = CardsFormatter.getFormatter('pretty');
|
||||
assert.equal(pretty(), '');
|
||||
assert.equal(pretty(null), '');
|
||||
assert.equal(pretty({}), '');
|
||||
assert.equal(pretty({results: 'blah'}), '');
|
||||
});
|
||||
|
||||
it('generates valid pretty output', () => {
|
||||
const pretty = CardsFormatter.getFormatter('pretty');
|
||||
const output = pretty(extendedInfo.value);
|
||||
const str = ` - ${extendedInfo.value[0].title}: ${extendedInfo.value[0].value}\n` +
|
||||
` - ${extendedInfo.value[1].title}: ${extendedInfo.value[1].value}\n` +
|
||||
` - ${extendedInfo.value[2].title}: ${extendedInfo.value[2].value}\n`;
|
||||
assert.equal(output, str);
|
||||
});
|
||||
|
||||
it('generates valid html output', () => {
|
||||
const formatter = CardsFormatter.getFormatter('html');
|
||||
const template = Handlebars.compile(formatter);
|
||||
const output = template(extendedInfo.value).split('\n').join('');
|
||||
assert.ok(output.match('title="snippet"'), 'adds title attribute for snippet');
|
||||
assert.ok(output.match('class="subitem__details cards__container"'), 'adds wrapper class');
|
||||
assert.equal(output.match(/class=\"[^"]*scorecard-value/g).length, extendedInfo.value.length);
|
||||
assert.equal(output.match(/class=\"[^"]*scorecard-title/g).length, extendedInfo.value.length);
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче