@lhci/utils
Advanced tools
Comparing version 0.3.8 to 0.3.9
{ | ||
"name": "@lhci/utils", | ||
"version": "0.3.8", | ||
"version": "0.3.9", | ||
"license": "Apache-2.0", | ||
@@ -17,3 +17,3 @@ "repository": { | ||
}, | ||
"gitHead": "2204f91151a2558a1ea3fe68677c5f6a9385747f" | ||
"gitHead": "b5367a74d973da2a9b60a7251797d3dd2421d1b7" | ||
} |
@@ -22,2 +22,3 @@ /** | ||
* @property {number[]} values | ||
* @property {boolean} passed | ||
* @property {LHCI.AssertCommand.AssertionFailureLevel} [level] | ||
@@ -45,8 +46,8 @@ * @property {string} [auditId] | ||
/** @type {Record<AssertionType, {operator: string, passesFn(actual: number, expected: number): boolean}>} */ | ||
/** @type {Record<AssertionType, {operator: string, passedFn(actual: number, expected: number): boolean}>} */ | ||
const AUDIT_TYPE_OPERATORS = { | ||
auditRan: {operator: '==', passesFn: (actual, expected) => actual === expected}, | ||
minScore: {operator: '>=', passesFn: (actual, expected) => actual >= expected}, | ||
maxLength: {operator: '<=', passesFn: (actual, expected) => actual <= expected}, | ||
maxNumericValue: {operator: '<=', passesFn: (actual, expected) => actual <= expected}, | ||
auditRan: {operator: '==', passedFn: (actual, expected) => actual === expected}, | ||
minScore: {operator: '>=', passedFn: (actual, expected) => actual >= expected}, | ||
maxLength: {operator: '<=', passedFn: (actual, expected) => actual <= expected}, | ||
maxNumericValue: {operator: '<=', passedFn: (actual, expected) => actual <= expected}, | ||
}; | ||
@@ -106,6 +107,8 @@ | ||
const didRun = values.map(value => (isFiniteNumber(value) ? 1 : 0)); | ||
return [{name: 'auditRan', expected: 1, actual: 0, values: didRun, operator: '=='}]; | ||
return [ | ||
{name: 'auditRan', expected: 1, actual: 0, values: didRun, operator: '==', passed: false}, | ||
]; | ||
} | ||
const {operator, passesFn} = AUDIT_TYPE_OPERATORS[assertionType]; | ||
const {operator, passedFn} = AUDIT_TYPE_OPERATORS[assertionType]; | ||
const actualValue = getValueForAggregationMethod( | ||
@@ -116,3 +119,2 @@ filteredValues, | ||
); | ||
if (passesFn(actualValue, expectedValue)) return []; | ||
@@ -126,2 +128,3 @@ return [ | ||
operator, | ||
passed: passedFn(actualValue, expectedValue), | ||
}, | ||
@@ -136,3 +139,3 @@ ]; | ||
*/ | ||
function getAssertionResults(possibleAuditResults, options) { | ||
function getStandardAssertionResults(possibleAuditResults, options) { | ||
const {minScore, maxLength, maxNumericValue, aggregationMethod = 'optimistic'} = options; | ||
@@ -147,2 +150,3 @@ if (possibleAuditResults.some(result => result === undefined)) { | ||
operator: '>=', | ||
passed: false, | ||
}, | ||
@@ -269,3 +273,3 @@ ]; | ||
return getAssertionResults(psuedoAudits, assertionOptions).map(result => ({ | ||
return getStandardAssertionResults(psuedoAudits, assertionOptions).map(result => ({ | ||
...result, | ||
@@ -314,3 +318,3 @@ auditProperty: categoryId, | ||
return getAssertionResults(psuedoAuditResults, assertionOptions).map(result => ({ | ||
return getStandardAssertionResults(psuedoAuditResults, assertionOptions).map(result => ({ | ||
...result, | ||
@@ -320,3 +324,3 @@ auditProperty: auditProperty.join('.'), | ||
} else { | ||
return getAssertionResults(auditResults, assertionOptions); | ||
return getStandardAssertionResults(auditResults, assertionOptions); | ||
} | ||
@@ -382,3 +386,3 @@ } | ||
*/ | ||
function getAllFilteredAssertionResults(baseOptions, unfilteredLhrs) { | ||
function getAllAssertionResultsForUrl(baseOptions, unfilteredLhrs) { | ||
const { | ||
@@ -430,2 +434,4 @@ assertions, | ||
/** | ||
* Computes all assertion results for the given LHR-set and options. | ||
* | ||
* @param {LHCI.AssertCommand.Options} options | ||
@@ -452,9 +458,10 @@ * @param {LH.Result[]} lhrs | ||
for (const baseOptions of arrayOfOptions) { | ||
results.push(...getAllFilteredAssertionResults(baseOptions, lhrSet)); | ||
results.push(...getAllAssertionResultsForUrl(baseOptions, lhrSet)); | ||
} | ||
} | ||
return results; | ||
if (options.includePassedAssertions) return results; | ||
return results.filter(result => !result.passed); | ||
} | ||
module.exports = {getAllAssertionResults}; |
@@ -141,2 +141,10 @@ /** | ||
/** @param {number|null|undefined} value @param {[number, number]} cutoffs */ | ||
function getMetricScoreLevel(value, cutoffs) { | ||
if (typeof value !== 'number') return 'error'; | ||
if (value <= cutoffs[0]) return 'pass'; | ||
if (value <= cutoffs[1]) return 'average'; | ||
return 'fail'; | ||
} | ||
/** @param {LHCI.AuditDiff} diff */ | ||
@@ -640,2 +648,3 @@ function getDiffSeverity(diff) { | ||
getMostSevereDiffLabel, | ||
getMetricScoreLevel, | ||
zipBaseAndCompareItems, | ||
@@ -642,0 +651,0 @@ synthesizeItemKeyDiffs, |
@@ -65,2 +65,4 @@ /** | ||
'CIRCLE_SHA1', | ||
// GitLab CI | ||
'CI_COMMIT_SHA', | ||
]); | ||
@@ -67,0 +69,0 @@ if (envHash) return envHash; |
@@ -138,3 +138,3 @@ /** | ||
/** @type {LHCI.YargsOptions} */ | ||
let merged = {...ci.assert, ...ci.collect, ...ci.upload, ...ci.server}; | ||
let merged = {...ci.wizard, ...ci.assert, ...ci.collect, ...ci.upload, ...ci.server}; | ||
if (ci.extends) { | ||
@@ -163,8 +163,15 @@ const extendedRcFilePath = path.resolve(path.dirname(pathToRcFile), ci.extends); | ||
/** @param {string|undefined} pathToRcFile */ | ||
/** | ||
* @param {string|undefined} pathToRcFile | ||
* @return {string|undefined} | ||
*/ | ||
function resolveRcFilePath(pathToRcFile) { | ||
if (pathToRcFile) return pathToRcFile; | ||
if (typeof pathToRcFile === 'string') return path.resolve(process.cwd(), pathToRcFile); | ||
return hasOptedOutOfRcDetection() ? undefined : findRcFile(); | ||
} | ||
// AFAIK this can't be expressed in JSDoc yet, so fallback to coercive typedef | ||
// @see https://github.com/microsoft/TypeScript/issues/24929 | ||
/** @typedef {((s: string) => string) & ((s: undefined) => string|undefined) & ((s: string|undefined) => string|undefined)} ResolveRcFilePathTrueType */ | ||
module.exports = { | ||
@@ -174,5 +181,5 @@ loadRcFile, | ||
findRcFile, | ||
resolveRcFilePath, | ||
resolveRcFilePath: /** @type {ResolveRcFilePathTrueType} */ (resolveRcFilePath), | ||
flattenRcToConfig, | ||
hasOptedOutOfRcDetection, | ||
}; |
@@ -73,5 +73,30 @@ /** | ||
/** | ||
* @template {any[]} T | ||
* @param {(...args: T) => void} fn | ||
* @param {number} time | ||
* @return {{(...args: T): void; cancel: () => void;}} | ||
*/ | ||
function debounce(fn, time) { | ||
/** @type {NodeJS.Timeout|undefined} */ | ||
let timeout; | ||
const cancel = () => { | ||
if (timeout) clearTimeout(timeout); | ||
timeout = undefined; | ||
}; | ||
/** @param {T} args */ | ||
const debouncedFn = (...args) => { | ||
if (timeout) clearTimeout(timeout); | ||
timeout = setTimeout(() => fn(...args), time); | ||
}; | ||
debouncedFn.cancel = cancel; | ||
return debouncedFn; | ||
} | ||
module.exports = { | ||
merge, | ||
kebabCase, | ||
debounce, | ||
/** | ||
@@ -165,2 +190,28 @@ * Generates an array of numbers from `from` (inclusive) to `to` (exclusive) | ||
/** | ||
* @template TArr | ||
* @param {Array<TArr>} items | ||
* @param {(o: TArr) => number} keyFn | ||
*/ | ||
maxBy(items, keyFn) { | ||
let maxItem = undefined; | ||
let maxValue = -Infinity; | ||
for (const item of items) { | ||
const value = keyFn(item); | ||
if (value > maxValue) { | ||
maxItem = item; | ||
maxValue = value; | ||
} | ||
} | ||
return maxItem; | ||
}, | ||
/** | ||
* @template TArr | ||
* @param {Array<TArr>} items | ||
* @param {(o: TArr) => number} keyFn | ||
*/ | ||
minBy(items, keyFn) { | ||
return this.maxBy(items, o => -keyFn(o)); | ||
}, | ||
/** | ||
* @template T | ||
@@ -205,2 +256,6 @@ * @param {T} object | ||
}, | ||
uniqueId: (() => { | ||
let id = 1; | ||
return () => id++; | ||
})(), | ||
}; |
@@ -15,5 +15,4 @@ /** | ||
// To include in next LHR update... | ||
// - Not applicable audits | ||
// - Informative audits | ||
// - 0-value numericValue | ||
// - LCP/CLS/TBT | ||
@@ -168,2 +167,5 @@ /** @typedef {import('./lhr-generator').AuditGenDef} AuditGenDef */ | ||
'a11y-aria-label': {passRate: 0.5, items: elements}, | ||
'a11y-aria-name': {passRate: 0.5, items: elements}, | ||
'a11y-aria-title': {passRate: 0.5, items: elements}, | ||
'a11y-color-contrast': {passRate: 0.5, items: elements}, | ||
@@ -467,3 +469,3 @@ 'a11y-labels': {passRate: 0.5, items: elements}, | ||
const runs = []; | ||
for (let i = 0; i < 100; i++) { | ||
for (let i = 0; i < 500; i++) { | ||
const runAt = new Date( | ||
@@ -502,3 +504,3 @@ new Date((builds[i - 1] || {runAt: new Date('2019-07-01')}).runAt).getTime() + | ||
const urls = [`http://localhost:${port}/index.html`, `http://localhost:${port}/about.html`]; | ||
for (let j = 0; j < 10; j++) { | ||
for (let j = 0; j < 5; j++) { | ||
for (const url of urls) { | ||
@@ -523,7 +525,15 @@ runs.push({ | ||
} | ||
if (typeof audit.score === 'number') { | ||
audit.score = audit.score * multiplier; | ||
if (audit.scoreDisplayMode === 'binary') { | ||
audit.score = Math.random() > 0.5 ? 1 : 0; | ||
if (Math.random() < 0.2) { | ||
audit.score = null; | ||
audit.scoreDisplayMode = Math.random() > 0.5 ? 'notApplicable' : 'informative'; | ||
} | ||
} | ||
} | ||
for (const categoryId of Object.keys(lhr.categories)) { | ||
lhr.categories[categoryId].score = Math.random(); | ||
} | ||
return JSON.stringify(lhr); | ||
@@ -530,0 +540,0 @@ }, |
@@ -38,4 +38,5 @@ /** | ||
if (auditId.startsWith('uses-')) return 'load-opportunities'; | ||
if (auditId.startsWith('a11y')) return 'accessibility'; | ||
if (auditId.startsWith('seo')) return 'seo'; | ||
if (auditId.startsWith('a11y-aria')) return 'a11y-aria'; | ||
if (auditId.startsWith('a11y')) return 'a11y-best-practices'; | ||
if (auditId.startsWith('seo')) return 'seo-content'; | ||
if (auditId.startsWith('best-practices')) return 'best-practices'; | ||
@@ -42,0 +43,0 @@ if (auditId.startsWith('pwa-fast-reliable')) return 'pwa-fast-reliable'; |
382170
8860