testium-driver-wd
Version:
Testium backend powered by wd
317 lines (284 loc) • 8.68 kB
JavaScript
/*
* Copyright (c) 2015, Groupon, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* Neither the name of GROUPON nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
;
const assert = require('assert');
const isEmpty = require('lodash/isEmpty');
const flatMap = require('lodash/flatMap');
/**
* @typedef import('lighthouse/types/lhr') LH
*/
const defaultConfig = {
extends: 'lighthouse:default',
settings: {
onlyCategories: [],
throttlingMethod: 'simulate',
},
};
const defaultFlags = {
chromeFlags: [
'--disable-gpu',
'--headless',
'--disable-storage-reset',
'--enable-logging',
'--disable-device-emulation',
'--no-sandbox',
],
};
/**
* @param {string} type
* @param {Array<string>} [skipAudits]
* @return {Object}
*/
function getConfigByType(type, skipAudits) {
const config = JSON.parse(JSON.stringify(defaultConfig));
if (type) {
config.settings.onlyCategories.push(type);
if (Array.isArray(skipAudits) && skipAudits.length) {
config.settings = { ...config.settings, skipAudits };
}
}
return config;
}
/**
*
* @param {Record<string, any>} success
* @param {Record<string, any>} failure
* @return {number}
*/
function getTotalScore(success, failure) {
const successCount = Object.keys(success).length;
const errorCount = Object.keys(failure).length;
if (successCount + errorCount === 0) {
return 0;
}
return (successCount * 100) / (successCount + errorCount);
}
/**
*
* @param {LH.Result} results
* @return {{score: number, errorString(): string, success(string): Record<string, auditInfo>, audits: LH.Result.audits, errors(string): Record<string, any>, isSuccess(number): boolean}}
*/
function parseLhResult(results) {
if (!results || !results.lhr || !results.lhr.audits) {
throw new Error(`Error fetching lighthouse audit results`);
}
const audits = results.lhr.audits;
const errorsObj = {};
const notApplicableObj = {};
const successObj = {};
/**
*
* @typedef {{description: LH.Audit.Result.description, details: LH.Audit.Result.details}} auditInfo
*/
Object.values(audits).forEach(audit => {
const { id, description, details, score, scoreDisplayMode } = audit;
/**
*
* @type {auditInfo}
*/
const auditInfo = { description, details };
if (score) {
successObj[id] = auditInfo;
} else if (scoreDisplayMode === 'notApplicable') {
notApplicableObj[id] = auditInfo;
} else if (scoreDisplayMode !== 'manual') {
const snippets = [];
if (details && !isEmpty(details.items)) {
details.items.forEach(item =>
snippets.push((item.node && item.node.snippet) || '')
);
}
errorsObj[id] = { description, snippets };
}
});
const score = getTotalScore(successObj, errorsObj);
return {
audits,
score,
/**
* @param {number} cutoff
*/
isSuccess(cutoff) {
assert.ok(
score >= cutoff,
`Score ${score} is smaller than expected cutoff score ${cutoff}`
);
},
/**
* @param {string} type
* @return {Record<string, auditInfo>}
*/
success(type) {
return type ? successObj[type] : successObj;
},
/**
* @param {string} type
* @return {Record<string, any>}
*/
errors(type) {
return type ? errorsObj[type] : errorsObj;
},
/**
* @return {string}
*/
errorString() {
const errorList = [];
for (const id in errorsObj) {
const description = errorsObj[id].description;
const snippets = errorsObj[id].snippets;
errorList.push(`${description}:\n\t${snippets.join('\n\t')}`);
}
return errorList.join('\n\n');
},
};
}
/**
*
* @param {string} category
* @param {number} expectedScore
* @param {Array<string>} [skipAudits]
* @return {Promise<any>}
*/
function assertScoreByCategory(category, expectedScore, skipAudits = []) {
expectedScore = expectedScore || 100;
const config = getConfigByType(category, skipAudits);
return this.runLighthouseAudit(defaultFlags, config).then(results => {
assert.ok(
results.score >= expectedScore,
`${category} score ${results.score} to be greater than ${expectedScore}`
);
return results;
});
}
exports.assertScoreByCategory = assertScoreByCategory;
/**
*
* @param {number} expectedScore
* @param {Array<string>} [skipAudits]
* @return {Promise<any>}
*/
function assertPerformanceScore(expectedScore, skipAudits = []) {
return this.assertScoreByCategory('performance', expectedScore, [
...skipAudits,
'final-screenshot',
'is-on-https',
'screenshot-thumbnails',
]);
}
exports.assertPerformanceScore = assertPerformanceScore;
/**
*
* @param {number} expectedScore
* @param {Array<string>} [skipAudits]
* @return {Promise<any>}
*/
function assertAccessibilityScore(expectedScore, skipAudits = []) {
return this.assertScoreByCategory('accessibility', expectedScore, skipAudits);
}
exports.assertAccessibilityScore = assertAccessibilityScore;
/**
*
* @param {number} expectedScore
* @param {Array<string>} skipAudits
* @return {Promise<any>}
*/
function assertBestPracticesScore(expectedScore, skipAudits = []) {
return this.assertScoreByCategory(
'best-practices',
expectedScore,
skipAudits
);
}
exports.assertBestPracticesScore = assertBestPracticesScore;
/**
*
* @param {number} expectedScore
* @param {Array<string>} [skipAudits]
* @return {Promise<any>}
*/
function assertSeoScore(expectedScore, skipAudits = []) {
return this.assertScoreByCategory('seo', expectedScore, skipAudits);
}
exports.assertSeoScore = assertSeoScore;
/**
*
* @param {number} expectedScore
* @param {Array<string>} [skipAudits]
* @return {Promise<any>}
*/
function assertPwaScore(expectedScore, skipAudits = []) {
return this.assertScoreByCategory('pwa', expectedScore, skipAudits);
}
exports.assertPwaScore = assertPwaScore;
/**
*
* @param {?Object} flags
* @param {?Object} lhConfig
* @legacy
*/
function runLighthouseAudit(flags, lhConfig) {
return this.getLighthouseData(flags, lhConfig).then(parseLhResult);
}
exports.runLighthouseAudit = runLighthouseAudit;
async function a11yAudit(options = {}) {
const { flags, config, ignore } = options;
let violationsFilter; // function to filter violations
if (typeof ignore === 'function') {
violationsFilter = violation => !ignore(violation);
} else if (Array.isArray(ignore)) {
// e.g. ignore: ['html-has-lang']
violationsFilter = violation => !ignore.includes(violation.id);
} else {
violationsFilter = () => true;
}
const result = await this.getLighthouseData(
flags || defaultFlags,
config || getConfigByType('accessibility')
);
return flatMap(
result.artifacts.Accessibility.violations
.filter(violationsFilter)
.map(violation => {
const { nodes, ...violationData } = violation;
return nodes.map(node => {
return {
...violationData,
auditId: violationData.id,
selector: Array.isArray(node.target) ? node.target.join(' ') : '',
path: node.path,
snippet: node.snippet,
};
});
})
);
}
exports.a11yAudit = a11yAudit;