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>
180 lines
6.7 KiB
Text
180 lines
6.7 KiB
Text
/**
|
|
* @license
|
|
* Copyright 2021 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import log from 'lighthouse-logger';
|
|
|
|
import {waitForFullyLoaded, waitForFrameNavigated, waitForUserToContinue} from './wait-for-condition.js';
|
|
import * as constants from '../../config/constants.js';
|
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
import UrlUtils from '../../lib/url-utils.js';
|
|
|
|
const UIStrings = {
|
|
/**
|
|
* @description Warning that the web page redirected during testing and that may have affected the load.
|
|
* @example {https://example.com/requested/page} requested
|
|
* @example {https://example.com/final/resolved/page} final
|
|
*/
|
|
warningRedirected: 'The page may not be loading as expected because your test URL ' +
|
|
`({requested}) was redirected to {final}. ` +
|
|
'Try testing the second URL directly.',
|
|
/**
|
|
* @description Warning that Lighthouse timed out while waiting for the page to load.
|
|
*/
|
|
warningTimeout: 'The page loaded too slowly to finish within the time limit. ' +
|
|
'Results may be incomplete.',
|
|
};
|
|
|
|
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
|
|
|
|
// Controls how long to wait after FCP before continuing
|
|
const DEFAULT_PAUSE_AFTER_FCP = 0;
|
|
// Controls how long to wait after onLoad before continuing
|
|
const DEFAULT_PAUSE_AFTER_LOAD = 0;
|
|
// Controls how long to wait between network requests before determining the network is quiet
|
|
const DEFAULT_NETWORK_QUIET_THRESHOLD = 5000;
|
|
// Controls how long to wait between longtasks before determining the CPU is idle, off by default
|
|
const DEFAULT_CPU_QUIET_THRESHOLD = 0;
|
|
|
|
/** @typedef {{waitUntil: Array<'fcp'|'load'|'navigated'>} & Partial<LH.Config.Settings>} NavigationOptions */
|
|
|
|
/** @param {NavigationOptions} options */
|
|
function resolveWaitForFullyLoadedOptions(options) {
|
|
let {pauseAfterFcpMs, pauseAfterLoadMs, networkQuietThresholdMs, cpuQuietThresholdMs} = options;
|
|
let maxWaitMs = options.maxWaitForLoad;
|
|
let maxFCPMs = options.maxWaitForFcp;
|
|
|
|
if (typeof pauseAfterFcpMs !== 'number') pauseAfterFcpMs = DEFAULT_PAUSE_AFTER_FCP;
|
|
if (typeof pauseAfterLoadMs !== 'number') pauseAfterLoadMs = DEFAULT_PAUSE_AFTER_LOAD;
|
|
if (typeof networkQuietThresholdMs !== 'number') {
|
|
networkQuietThresholdMs = DEFAULT_NETWORK_QUIET_THRESHOLD;
|
|
}
|
|
if (typeof cpuQuietThresholdMs !== 'number') cpuQuietThresholdMs = DEFAULT_CPU_QUIET_THRESHOLD;
|
|
if (typeof maxWaitMs !== 'number') maxWaitMs = constants.defaultSettings.maxWaitForLoad;
|
|
if (typeof maxFCPMs !== 'number') maxFCPMs = constants.defaultSettings.maxWaitForFcp;
|
|
|
|
if (!options.waitUntil.includes('fcp')) maxFCPMs = undefined;
|
|
|
|
return {
|
|
pauseAfterFcpMs,
|
|
pauseAfterLoadMs,
|
|
networkQuietThresholdMs,
|
|
cpuQuietThresholdMs,
|
|
maxWaitForLoadedMs: maxWaitMs,
|
|
maxWaitForFcpMs: maxFCPMs,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Navigates to the given URL, assuming that the page is not already on this URL.
|
|
* Resolves on the url of the loaded page, taking into account any redirects.
|
|
* Typical use of this method involves navigating to a neutral page such as `about:blank` in between
|
|
* navigations.
|
|
*
|
|
* @param {LH.Gatherer.Driver} driver
|
|
* @param {LH.NavigationRequestor} requestor
|
|
* @param {NavigationOptions} options
|
|
* @return {Promise<{requestedUrl: string, mainDocumentUrl: string, warnings: Array<LH.IcuMessage>}>}
|
|
*/
|
|
async function gotoURL(driver, requestor, options) {
|
|
const status = typeof requestor === 'string' ?
|
|
{msg: `Navigating to ${requestor}`, id: 'lh:driver:navigate'} :
|
|
{msg: 'Navigating using a user defined function', id: 'lh:driver:navigate'};
|
|
log.time(status);
|
|
|
|
const session = driver.defaultSession;
|
|
const networkMonitor = driver.networkMonitor;
|
|
|
|
// Enable the events and network monitor needed to track navigation progress.
|
|
await session.sendCommand('Page.enable');
|
|
await session.sendCommand('Page.setLifecycleEventsEnabled', {enabled: true});
|
|
|
|
let waitForNavigationTriggered;
|
|
if (typeof requestor === 'string') {
|
|
// No timeout needed for Page.navigate. See https://github.com/GoogleChrome/lighthouse/pull/6413
|
|
session.setNextProtocolTimeout(Infinity);
|
|
waitForNavigationTriggered = session.sendCommand('Page.navigate', {url: requestor});
|
|
} else {
|
|
waitForNavigationTriggered = requestor();
|
|
}
|
|
|
|
const waitForNavigated = options.waitUntil.includes('navigated');
|
|
const waitForLoad = options.waitUntil.includes('load');
|
|
const waitForFcp = options.waitUntil.includes('fcp');
|
|
|
|
/** @type {Array<Promise<{timedOut: boolean}>>} */
|
|
const waitConditionPromises = [];
|
|
|
|
if (waitForNavigated) {
|
|
const navigatedPromise = waitForFrameNavigated(session).promise;
|
|
waitConditionPromises.push(navigatedPromise.then(() => ({timedOut: false})));
|
|
}
|
|
|
|
if (waitForLoad) {
|
|
const waitOptions = resolveWaitForFullyLoadedOptions(options);
|
|
waitConditionPromises.push(waitForFullyLoaded(session, networkMonitor, waitOptions));
|
|
} else if (waitForFcp) {
|
|
throw new Error('Cannot wait for FCP without waiting for page load');
|
|
}
|
|
|
|
const waitConditions = await Promise.race([
|
|
session.onCrashPromise(),
|
|
Promise.all(waitConditionPromises),
|
|
]);
|
|
const timedOut = waitConditions.some(condition => condition.timedOut);
|
|
const navigationUrls = await networkMonitor.getNavigationUrls();
|
|
|
|
let requestedUrl = navigationUrls.requestedUrl;
|
|
if (typeof requestor === 'string') {
|
|
if (requestedUrl && !UrlUtils.equalWithExcludedFragments(requestor, requestedUrl)) {
|
|
log.error(
|
|
'Navigation',
|
|
`Provided URL (${requestor}) did not match initial navigation URL (${requestedUrl})`
|
|
);
|
|
}
|
|
requestedUrl = requestor;
|
|
}
|
|
if (!requestedUrl) throw Error('No navigations detected when running user defined requestor.');
|
|
|
|
const mainDocumentUrl = navigationUrls.mainDocumentUrl || requestedUrl;
|
|
|
|
// Bring `Page.navigate` errors back into the promise chain. See https://github.com/GoogleChrome/lighthouse/pull/6739.
|
|
await waitForNavigationTriggered;
|
|
|
|
if (options.debugNavigation) {
|
|
await waitForUserToContinue(driver);
|
|
}
|
|
|
|
log.timeEnd(status);
|
|
return {
|
|
requestedUrl,
|
|
mainDocumentUrl,
|
|
warnings: getNavigationWarnings({timedOut, mainDocumentUrl, requestedUrl}),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {{timedOut: boolean, requestedUrl: string, mainDocumentUrl: string; }} navigation
|
|
* @return {Array<LH.IcuMessage>}
|
|
*/
|
|
function getNavigationWarnings(navigation) {
|
|
const {requestedUrl, mainDocumentUrl} = navigation;
|
|
/** @type {Array<LH.IcuMessage>} */
|
|
const warnings = [];
|
|
|
|
if (navigation.timedOut) warnings.push(str_(UIStrings.warningTimeout));
|
|
|
|
if (!UrlUtils.equalWithExcludedFragments(requestedUrl, mainDocumentUrl)) {
|
|
warnings.push(str_(UIStrings.warningRedirected, {
|
|
requested: requestedUrl,
|
|
final: mainDocumentUrl,
|
|
}));
|
|
}
|
|
|
|
return warnings;
|
|
}
|
|
|
|
export {gotoURL, getNavigationWarnings, UIStrings};
|