@wdio/devtools-service
Version:
A WebdriverIO service that allows you to run Chrome DevTools commands in your tests
148 lines (147 loc) • 6.89 kB
JavaScript
import Diagnostics from 'lighthouse/lighthouse-core/audits/diagnostics.js';
import MainThreadWorkBreakdown from 'lighthouse/lighthouse-core/audits/mainthread-work-breakdown.js';
import Metrics from 'lighthouse/lighthouse-core/audits/metrics.js';
import ServerResponseTime from 'lighthouse/lighthouse-core/audits/server-response-time.js';
import CumulativeLayoutShift from 'lighthouse/lighthouse-core/audits/metrics/cumulative-layout-shift.js';
import FirstContentfulPaint from 'lighthouse/lighthouse-core/audits/metrics/first-contentful-paint.js';
import LargestContentfulPaint from 'lighthouse/lighthouse-core/audits/metrics/largest-contentful-paint.js';
import SpeedIndex from 'lighthouse/lighthouse-core/audits/metrics/speed-index.js';
import InteractiveMetric from 'lighthouse/lighthouse-core/audits/metrics/interactive.js';
import TotalBlockingTime from 'lighthouse/lighthouse-core/audits/metrics/total-blocking-time.js';
import ReportScoring from 'lighthouse/lighthouse-core/scoring.js';
import defaultConfig from 'lighthouse/lighthouse-core/config/default-config.js';
import logger from '@wdio/logger';
import { DEFAULT_FORM_FACTOR, PWA_AUDITS } from './constants.js';
const log = logger('@wdio/devtools-service:Auditor');
export default class Auditor {
_traceLogs;
_devtoolsLogs;
_formFactor;
_url;
constructor(_traceLogs, _devtoolsLogs, _formFactor) {
this._traceLogs = _traceLogs;
this._devtoolsLogs = _devtoolsLogs;
this._formFactor = _formFactor;
if (_traceLogs) {
this._url = _traceLogs.pageUrl;
}
}
_audit(AUDIT, params = {}) {
const auditContext = {
options: {
...AUDIT.defaultOptions
},
settings: {
throttlingMethod: 'devtools',
formFactor: this._formFactor || DEFAULT_FORM_FACTOR
},
LighthouseRunWarnings: false,
computedCache: new Map()
};
try {
return AUDIT.audit({
traces: { defaultPass: this._traceLogs },
devtoolsLogs: { defaultPass: this._devtoolsLogs },
TestedAsMobileDevice: true,
GatherContext: { gatherMode: 'navigation' },
...params
}, auditContext);
}
catch (error) {
log.error(error);
return {
score: 0,
error
};
}
}
/**
* an Auditor instance is created for every trace so provide an updateCommands
* function to receive the latest performance metrics with the browser instance
*/
updateCommands(browser, customFn) {
const commands = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter(fnName => fnName !== 'constructor' && fnName !== 'updateCommands' && !fnName.startsWith('_'));
commands.forEach(fnName => browser.addCommand(fnName, customFn || this[fnName].bind(this)));
}
/**
* Returns a list with a breakdown of all main thread task and their total duration
*/
async getMainThreadWorkBreakdown() {
const result = await this._audit(MainThreadWorkBreakdown);
return result.details.items.map(({ group, duration }) => ({ group, duration }));
}
/**
* Get some useful diagnostics about the page load
*/
async getDiagnostics() {
const result = await this._audit(Diagnostics);
/**
* return null if Audit fails
*/
if (!Object.prototype.hasOwnProperty.call(result, 'details')) {
return null;
}
return result.details.items[0];
}
/**
* Get most common used performance metrics
*/
async getMetrics() {
const serverResponseTime = await this._audit(ServerResponseTime, { URL: this._url });
const cumulativeLayoutShift = await this._audit(CumulativeLayoutShift);
const result = await this._audit(Metrics);
const metrics = result.details.items[0] || {};
return {
timeToFirstByte: Math.round(serverResponseTime.numericValue),
serverResponseTime: Math.round(serverResponseTime.numericValue),
domContentLoaded: metrics.observedDomContentLoaded,
firstVisualChange: metrics.observedFirstVisualChange,
firstPaint: metrics.observedFirstPaint,
firstContentfulPaint: metrics.firstContentfulPaint,
firstMeaningfulPaint: metrics.firstMeaningfulPaint,
largestContentfulPaint: metrics.largestContentfulPaint,
lastVisualChange: metrics.observedLastVisualChange,
interactive: metrics.interactive,
load: metrics.observedLoad,
speedIndex: metrics.speedIndex,
totalBlockingTime: metrics.totalBlockingTime,
maxPotentialFID: metrics.maxPotentialFID,
cumulativeLayoutShift: cumulativeLayoutShift.numericValue,
};
}
/**
* Returns the Lighthouse Performance Score which is a weighted mean of the following metrics: firstMeaningfulPaint, interactive, speedIndex
*/
async getPerformanceScore() {
const auditResults = {
'speed-index': await this._audit(SpeedIndex),
'first-contentful-paint': await this._audit(FirstContentfulPaint),
'largest-contentful-paint': await this._audit(LargestContentfulPaint),
'cumulative-layout-shift': await this._audit(CumulativeLayoutShift),
'total-blocking-time': await this._audit(TotalBlockingTime),
interactive: await this._audit(InteractiveMetric)
};
if (!auditResults.interactive || !auditResults['cumulative-layout-shift'] || !auditResults['first-contentful-paint'] ||
!auditResults['largest-contentful-paint'] || !auditResults['speed-index'] || !auditResults['total-blocking-time']) {
log.info('One or multiple required metrics couldn\'t be found, setting performance score to: null');
return null;
}
const scores = defaultConfig.categories.performance.auditRefs.filter((auditRef) => auditRef.weight).map((auditRef) => ({
score: auditResults[auditRef.id].score,
weight: auditRef.weight,
}));
return ReportScoring.arithmeticMean(scores);
}
async _auditPWA(params, auditsToBeRun = Object.keys(PWA_AUDITS)) {
const audits = await Promise.all(Object.entries(PWA_AUDITS)
.filter(([name]) => auditsToBeRun.includes(name))
.map(async ([name, Audit]) => [name, await this._audit(Audit, params)]));
return {
passed: !audits.find(([, result]) => result.score < 1),
details: audits.reduce((details, [name, result]) => {
details[name] = result;
return details;
}, {})
};
}
}