Rocky_Mountain_Vending/.pnpm-store/v10/files/0e/83c9e5272a0753779238bf1934bd9b54ba8a4ae30d82c2476333fd50ccab3a725e0e676d8779d797ca910b552f2fa1611e7ffacd789c035e404fbfe3582d7e
DMleadgen 46d973904b
Initial commit: Rocky Mountain Vending website
Next.js website for Rocky Mountain Vending company featuring:
- Product catalog with Stripe integration
- Service areas and parts pages
- Admin dashboard with Clerk authentication
- SEO optimized pages with JSON-LD structured data

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 16:22:15 -07:00

502 lines
16 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview An assertion library for comparing smoke-test expectations
* against the results actually collected from Lighthouse.
*/
import log from 'lighthouse-logger';
import {LocalConsole} from './lib/local-console.js';
import {chromiumVersionCheck} from './version-check.js';
/**
* @typedef Difference
* @property {string} path
* @property {any} actual
* @property {any} expected
*/
/**
* @typedef Comparison
* @property {string} name
* @property {any} actual
* @property {any} expected
* @property {boolean} equal
* @property {Difference[]|null} diffs
*/
const NUMBER_REGEXP = /(?:\d|\.)+/.source;
const OPS_REGEXP = /<=?|>=?|\+\/-|±/.source;
// An optional number, optional whitespace, an operator, optional whitespace, a number.
const NUMERICAL_EXPECTATION_REGEXP =
new RegExp(`^(${NUMBER_REGEXP})?\\s*(${OPS_REGEXP})\\s*(${NUMBER_REGEXP})$`);
/**
* Checks if the actual value matches the expectation. Does not recursively search. This supports
* - Greater than/less than operators, e.g. "<100", ">90"
* - Regular expressions
* - Strict equality
* - plus or minus a margin of error, e.g. '10+/-5', '100±10'
*
* @param {*} actual
* @param {*} expected
* @return {boolean}
*/
function matchesExpectation(actual, expected) {
if (typeof actual === 'number' && NUMERICAL_EXPECTATION_REGEXP.test(expected)) {
const parts = expected.match(NUMERICAL_EXPECTATION_REGEXP);
const [, prefixNumber, operator, postfixNumber] = parts;
switch (operator) {
case '>':
return actual > postfixNumber;
case '>=':
return actual >= postfixNumber;
case '<':
return actual < postfixNumber;
case '<=':
return actual <= postfixNumber;
case '+/-':
case '±':
return Math.abs(actual - prefixNumber) <= postfixNumber;
default:
throw new Error(`unexpected operator ${operator}`);
}
} else if (typeof actual === 'string' && expected instanceof RegExp && expected.test(actual)) {
return true;
} else {
// Strict equality check, plus NaN equivalence.
return Object.is(actual, expected);
}
}
/**
* Walk down expected result, comparing to actual result. If a difference is found,
* the path to the difference is returned, along with the expected primitive value
* and the value actually found at that location. If no difference is found, returns
* null.
*
* Only checks own enumerable properties, not object prototypes, and will loop
* until the stack is exhausted, so works best with simple objects (e.g. parsed JSON).
* @param {string} path
* @param {*} actual
* @param {*} expected
* @return {Difference[]|null}
*/
function findDifferences(path, actual, expected) {
if (matchesExpectation(actual, expected)) {
return null;
}
// If they aren't both an object we can't recurse further, so this is the difference.
if (actual === null || expected === null || typeof actual !== 'object' ||
typeof expected !== 'object' || expected instanceof RegExp) {
return [{
path,
actual,
expected,
}];
}
/** @type {Difference[]} */
const diffs = [];
/** @type {any[]|undefined} */
let inclExclCopy;
// We only care that all expected's own properties are on actual (and not the other way around).
// Note an expected `undefined` can match an actual that is either `undefined` or not defined.
for (const key of Object.keys(expected)) {
// Bracket numbers, but property names requiring quotes will still be unquoted.
const keyAccessor = /^\d+$/.test(key) ? `[${key}]` : `.${key}`;
const keyPath = path + keyAccessor;
const expectedValue = expected[key];
if (key === '_includes') {
if (Array.isArray(actual)) {
inclExclCopy = [...actual];
} else if (typeof actual === 'object') {
inclExclCopy = Object.entries(actual);
}
if (!Array.isArray(expectedValue)) throw new Error('Array subset must be array');
if (!inclExclCopy) {
diffs.push({
path,
actual: 'Actual value is not an array or object',
expected,
});
continue;
}
for (const expectedEntry of expectedValue) {
const matchingIndex =
inclExclCopy.findIndex(actualEntry =>
!findDifferences(keyPath, actualEntry, expectedEntry));
if (matchingIndex !== -1) {
inclExclCopy.splice(matchingIndex, 1);
continue;
}
diffs.push({
path,
actual: 'Item not found in array',
expected: expectedEntry,
});
}
continue;
}
if (key === '_excludes') {
// Re-use state from `_includes` check, if there was one.
if (!inclExclCopy) {
if (Array.isArray(actual)) {
// We won't be removing items, so we can just copy the reference.
inclExclCopy = actual;
} else if (typeof actual === 'object') {
inclExclCopy = Object.entries(actual);
}
}
if (!Array.isArray(expectedValue)) throw new Error('Array subset must be array');
if (!inclExclCopy) {
diffs.push({
path,
actual: 'Actual value is not an array or object',
expected,
});
continue;
}
const expectedExclusions = expectedValue;
for (const expectedExclusion of expectedExclusions) {
const matchingIndex = inclExclCopy.findIndex(actualEntry =>
!findDifferences(keyPath, actualEntry, expectedExclusion));
if (matchingIndex !== -1) {
diffs.push({
path,
actual: inclExclCopy[matchingIndex],
expected: {
message: 'Expected to not find matching entry via _excludes',
expectedExclusion,
},
});
}
}
continue;
}
const actualValue = actual[key];
const subDifferences = findDifferences(keyPath, actualValue, expectedValue);
if (subDifferences) diffs.push(...subDifferences);
}
// If the expected value is an array, assert the length as well.
// This still allows for asserting that the first n elements of an array are specified elements,
// but requires using an object literal (ex: {0: x, 1: y, 2: z} matches [x, y, z, q, w, e] and
// {0: x, 1: y, 2: z, length: 5} does not match [x, y, z].
if (Array.isArray(expected) && actual.length !== expected.length) {
diffs.push({
path: `${path}.length`,
actual,
expected,
});
}
if (diffs.length === 0) return null;
return diffs;
}
/**
* @param {string} name name of the value being asserted on (e.g. the result of a certain audit)
* @param {any} actualResult
* @param {any} expectedResult
* @return {Comparison}
*/
function makeComparison(name, actualResult, expectedResult) {
const diffs = findDifferences(name, actualResult, expectedResult);
return {
name,
actual: actualResult,
expected: expectedResult,
equal: !diffs,
diffs,
};
}
/**
* Delete expectations that don't match environment criteria.
* @param {LocalConsole} localConsole
* @param {LH.Result} lhr
* @param {Smokehouse.ExpectedRunnerResult} expected
* @param {{runner?: string}=} reportOptions
*/
function pruneExpectations(localConsole, lhr, expected, reportOptions) {
/**
* Lazily compute the Chrome version because some reports are explicitly asserting error conditions.
* @returns {string}
*/
function getChromeVersionString() {
const userAgent = lhr.environment.hostUserAgent;
const userAgentMatch = /Chrome\/([\d.]+)/.exec(userAgent); // Chrome/85.0.4174.0
if (!userAgentMatch) throw new Error('Could not get chrome version.');
const versionString = userAgentMatch[1];
if (versionString.split('.').length !== 4) throw new Error(`unexpected ua: ${userAgent}`);
return versionString;
}
/**
* @param {*} obj
*/
function failsChromeVersionCheck(obj) {
return !chromiumVersionCheck({
version: getChromeVersionString(),
min: obj._minChromiumVersion,
max: obj._maxChromiumVersion,
});
}
/**
* @param {*} obj
*/
function pruneRecursively(obj) {
/**
* @param {string} key
*/
const remove = (key) => {
if (Array.isArray(obj)) {
obj.splice(Number(key), 1);
} else {
delete obj[key];
}
};
// Because we may be deleting keys, we should iterate the keys backwards
// otherwise arrays with multiple pruning checks will skip elements.
for (const [key, value] of Object.entries(obj).reverse()) {
if (!value || typeof value !== 'object') {
continue;
}
if (failsChromeVersionCheck(value)) {
localConsole.log([
`[${key}] failed chrome version check, pruning expectation:`,
JSON.stringify(value, null, 2),
`Actual Chromium version: ${getChromeVersionString()}`,
].join(' '));
remove(key);
} else if (value._runner && reportOptions?.runner !== value._runner) {
localConsole.log([
`[${key}] is only for runner ${value._runner}, pruning expectation:`,
JSON.stringify(value, null, 2),
].join(' '));
remove(key);
} else if (value._excludeRunner && reportOptions?.runner === value._excludeRunner) {
localConsole.log([
`[${key}] is excluded for runner ${value._excludeRunner}, pruning expectation:`,
JSON.stringify(value, null, 2),
].join(' '));
remove(key);
} else {
pruneRecursively(value);
}
}
delete obj._skipInBundled;
delete obj._minChromiumVersion;
delete obj._maxChromiumVersion;
delete obj._runner;
delete obj._excludeRunner;
}
const cloned = structuredClone(expected);
pruneRecursively(cloned);
return cloned;
}
/**
* Collate results into comparisons of actual and expected scores on each audit/artifact.
* @param {LocalConsole} localConsole
* @param {{lhr: LH.Result, artifacts: LH.Artifacts, networkRequests?: string[]}} actual
* @param {Smokehouse.ExpectedRunnerResult} expected
* @return {Comparison[]}
*/
function collateResults(localConsole, actual, expected) {
// If actual run had a runtimeError, expected *must* have a runtimeError.
// Relies on the fact that an `undefined` argument to makeComparison() can only match `undefined`.
const runtimeErrorAssertion = makeComparison('runtimeError', actual.lhr.runtimeError,
expected.lhr.runtimeError);
// Same for warnings, exclude the slow CPU warning which is flaky and differs between CI machines.
const warnings = actual.lhr.runWarnings
.filter(warning => !warning.includes('loaded too slowly'))
.filter(warning => !warning.includes('a slower CPU'));
const runWarningsAssertion = makeComparison('runWarnings', warnings,
expected.lhr.runWarnings || []);
/** @type {Comparison[]} */
let artifactAssertions = [];
if (expected.artifacts) {
const expectedArtifacts = expected.artifacts;
const artifactNames = /** @type {(keyof LH.Artifacts)[]} */ (Object.keys(expectedArtifacts));
const actualArtifacts = actual.artifacts || {};
artifactAssertions = artifactNames.map(artifactName => {
if (!(artifactName in actualArtifacts)) {
localConsole.log(log.redify('Error: ') +
`Config run did not generate artifact ${artifactName}`);
}
const actualResult = actualArtifacts[artifactName];
const expectedResult = expectedArtifacts[artifactName];
return makeComparison(artifactName + ' artifact', actualResult, expectedResult);
});
}
/** @type {Comparison[]} */
let auditAssertions = [];
auditAssertions = Object.keys(expected.lhr.audits).map(auditName => {
const actualResult = actual.lhr.audits[auditName];
if (!actualResult) {
localConsole.log(log.redify('Error: ') +
`Config did not trigger run of expected audit ${auditName}`);
}
const expectedResult = expected.lhr.audits[auditName];
return makeComparison(auditName + ' audit', actualResult, expectedResult);
});
/** @type {Comparison[]} */
const extraAssertions = [];
if (expected.lhr.timing) {
const comparison = makeComparison('timing', actual.lhr.timing, expected.lhr.timing);
extraAssertions.push(comparison);
}
if (expected.networkRequests) {
extraAssertions.push(makeComparison(
'Requests',
actual.networkRequests,
expected.networkRequests
));
}
if (expected.lhr.fullPageScreenshot) {
extraAssertions.push(makeComparison('fullPageScreenshot', actual.lhr.fullPageScreenshot,
expected.lhr.fullPageScreenshot));
}
return [
makeComparison('final url', actual.lhr.finalDisplayedUrl, expected.lhr.finalDisplayedUrl),
runtimeErrorAssertion,
runWarningsAssertion,
...artifactAssertions,
...auditAssertions,
...extraAssertions,
];
}
/**
* @param {unknown} obj
*/
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
/**
* Log the result of an assertion of actual and expected results to the provided
* console.
* @param {LocalConsole} localConsole
* @param {Comparison} assertion
*/
function reportAssertion(localConsole, assertion) {
// @ts-expect-error - this doesn't exist now but could one day, so try not to break the future
const _toJSON = RegExp.prototype.toJSON;
// @ts-expect-error
// eslint-disable-next-line no-extend-native
RegExp.prototype.toJSON = RegExp.prototype.toString;
if (assertion.equal) {
if (isPlainObject(assertion.actual)) {
localConsole.log(` ${log.greenify(log.tick)} ${assertion.name}`);
} else {
localConsole.log(` ${log.greenify(log.tick)} ${assertion.name}: ` +
log.greenify(assertion.actual));
}
} else {
if (assertion.diffs?.length) {
for (const diff of assertion.diffs) {
const msg = `
${log.redify(log.cross)} difference at ${log.bold}${diff.path}${log.reset}
expected: ${JSON.stringify(diff.expected)}
found: ${JSON.stringify(diff.actual)}\n`;
localConsole.log(msg);
}
const fullActual = assertion.actual !== undefined ?
JSON.stringify(assertion.actual, null, 2).replace(/\n/g, '\n ') :
'undefined\n ';
localConsole.log(` found result:
${log.redify(fullActual)}
`);
} else {
localConsole.log(` ${log.redify(log.cross)} ${assertion.name}:
expected: ${JSON.stringify(assertion.expected)}
found: ${JSON.stringify(assertion.actual)}
`);
}
}
// @ts-expect-error
// eslint-disable-next-line no-extend-native
RegExp.prototype.toJSON = _toJSON;
}
/**
* Log all the comparisons between actual and expected test results, then print
* summary. Returns count of passed and failed tests.
* @param {{lhr: LH.Result, artifacts: LH.Artifacts, networkRequests?: string[]}} actual
* @param {Smokehouse.ExpectedRunnerResult} expected
* @param {{runner?: string, isDebug?: boolean}=} reportOptions
* @return {{passed: number, failed: number, log: string}}
*/
function getAssertionReport(actual, expected, reportOptions = {}) {
const localConsole = new LocalConsole();
expected = pruneExpectations(localConsole, actual.lhr, expected, reportOptions);
const comparisons = collateResults(localConsole, actual, expected);
let correctCount = 0;
let failedCount = 0;
comparisons.forEach(assertion => {
if (assertion.equal) {
correctCount++;
} else {
failedCount++;
}
if (!assertion.equal || reportOptions.isDebug) {
reportAssertion(localConsole, assertion);
}
});
return {
passed: correctCount,
failed: failedCount,
log: localConsole.getLog(),
};
}
export {
getAssertionReport,
findDifferences,
};