fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

9
node_modules/lighthouse/AUTHORS generated vendored Normal file
View File

@@ -0,0 +1,9 @@
# This is the list of Lighthouse's significant contributors.
#
# This does not necessarily list everyone who has contributed code,
# especially since many employees of one corporation may be contributing.
# To see the full list of contributors, see the revision history in
# source control.
Google LLC
Sebastian Kreft

174
node_modules/lighthouse/CONTRIBUTING.md generated vendored Normal file
View File

@@ -0,0 +1,174 @@
# For Contributors
We'd love your help! This doc covers how to become a contributor and submit code to the project.
## Where can I start?
We tag issues that are good candidates for those new to the code with [`good first issue`](https://github.com/GoogleChrome/lighthouse/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22). We recommend you start there!
## Follow the coding style
The `.eslintrc.cjs` file defines all. We use [JSDoc](http://usejsdoc.org/) with [TypeScript `checkJs`](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html#supported-jsdoc). Annotations are encouraged for all contributions.
## Learn about the architecture
See [Lighthouse Architecture](./docs/architecture.md), our overview and tour of the codebase.
## Contributing a patch
If you have a contribution for our [documentation](https://developer.chrome.com/docs/lighthouse/), please submit it in the [developer.chrome.com repo](https://github.com/GoogleChrome/developer.chrome.com).
1. Submit an issue describing your proposed change.
1. The maintainers will respond to your issue promptly.
1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details below).
1. Fork the repo, develop and test your code changes.
1. Ensure that your code adheres to the existing style in the sample to which you are contributing.
1. Submit a pull request.
If you've submitted a number of significant patches, feel free to add yourself in a PR to the project's `AUTHORS` [file](https://github.com/GoogleChrome/lighthouse/blob/main/AUTHORS) in the root of the repo to be recognized for your contributions!
## Audit PRs
If proposing a new audit for Lighthouse, see the [new audit proposal guide](./docs/new-audits.md) and open an issue for discussion before starting.
A PR for a new audit or changing an existing audit almost always needs the following:
1. If new, add the audit to the [default config file](core/config/default-config.js) (or, rarely, one of the other config files) so Lighthouse will run it.
1. **Unit tests**: in the matching test file (e.g. tests for `core/audits/my-swell-audit.js` go in `core/test/audits/my-swell-audit-test.js`).
1. **Smoke (end-to-end) tests**: search through the [existing test expectations](cli/test/smokehouse/test-definitions/) to see if there's a logical place to add a check for your change, or (as a last resort) add a new smoke test.
1. Run `yarn update:sample-json` to update the [sample Lighthouse result JSON](core/test/results/sample_v2.json) kept in the repo for testing. This will also pull any strings needed for localization into the correct files.
### Audit `description` Guidelines
Keep the `description` of an audit as short as possible. When a reference doc for the audit exists on
developers.google.com/web, the `description` should only explain *why* the user should care
about the audit, not *how* to fix it.
Do:
Serve images that are smaller than the user's viewport to save cellular data and
improve load time. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/oversized-images).
Don't:
Serve images that are smaller than the user's viewport to save cellular data and
improve load time. Consider using responsive images and client hints.
If no reference doc exists yet, then you can use the `description` as a stopgap for explaining
both why the audit is important and how to fix it.
## Gatherer PRs
Gatherers have to interface with the inherently-complex real world and they also run *while* loading and/or testing the page, risking interfering with themselves. As a result, gatherers should strive to be as simple as possible and leave as much processing as possible to computed artifacts and audits.
It can be tempting to serialize the entire state of the world into the artifact the gatherer produces. Try to limit the artifact to exactly the information needed for current audits while leaving room for extensibility as needs change in the future.
A PR adding or changing a gatherer almost always needs to include the following:
1. If new, add the gatherer to the [default config file](core/config/default-config.js) (or, rarely, one of the other config files) so Lighthouse will run it.
1. **Unit tests**: gatherer execution often takes place mostly on the browser side, either through protocol functionality or executing javascript in the test page. This makes gatherers difficult to unit test without extensive mocking, ending up mostly exercising the mocks instead of the actual gatherer.
As a result, we mostly rely on smoke testing for gatherers. However, if there are parts of a gatherer that naturally lend themselves to unit testing, the new tests would go in the matching test file (e.g. tests for `core/gather/gatherers/reap.js` go in `core/test/gather/gatherers/reap-test.js`).
1. **Smoke (end-to-end) tests**: search through the [existing test expectations](cli/test/smokehouse/test-definitions/) to see if there's a logical place to add a check for your change, or (as a last resort) add a new smoke test if one is required.
It's most important to get true end-to-end coverage, so be sure that audits that consume the new gatherer output are in the expectations. Artifacts can also have expectations for those intermediate results.
1. **Golden artifacts**: `sample_v2.json` is generated from a set of artifacts that come from running LH against `dbw_tester.html`. Those artifacts likely need to be updated after gatherer changes with `yarn update:sample-artifacts`, but limit to just the artifact being altered if possible. For example:
```sh
# update just the ScriptElements artifact
yarn update:sample-artifacts ScriptElements
```
This command works for updating `yarn update:sample-artifacts devtoolsLogs` or `traces` as well, but the resulting `sample_v2.json` churn may be extensive and you might be better off editing manually.
1. Run `yarn update:sample-json` to update the [sample Lighthouse result JSON](core/test/results/sample_v2.json) kept in the repo for testing. This will also pull any strings needed for localization into the correct files.
## Protobuf errors
If there is an error in one of the proto tests (`proto-test.js` or `psi-test.js`), you may need to install `protobuf` locally for debugging. See the instructions for installing and running in the [proto readme](proto/README.md).
## Adding Images to a Readme
If you are adding an image to a readme use the absolute path to the image for the specific commit hash where the image was introduced. This requires multiple commits.
1. Make the commit to introduce the image.
1. Get the [absolute path](https://help.github.com/articles/getting-permanent-links-to-files/) to the image with the commit hash e.g. `https://raw.githubusercontent.com/GoogleChrome/lighthouse/e7997b3db01de3553d8cb208a40f3d4fd350195c/assets/example_dev_tools.png`
1. Add to readme as an absolute reference to that image.
If you are updating an image that already exists: commit it, then update the readme to point the image with that new commits hash absolute url.
## Pull request titles
We're using [conventional-commit](https://conventionalcommits.org/) for our commit messages. Since all PRs are squashed, we enforce this format for PR titles rather than individual git commits. A [`commitlint` bot](https://github.com/paulirish/commitlintbot) will update the status of your PR based on the title's conformance. The expected format is:
> type(scope): message subject
* The `type` must be one of: `new_audit` `core` `tests` `i18n`, `docs` `deps` `report` `cli` `clients` `misc`. (See [`.cz-config`](https://github.com/GoogleChrome/lighthouse/blob/main/.cz-config.js#L13))
* The `scope` is optional, but recommended. Any string is allowed; it should indicate what the change affects.
* The `message subject` should be pithy and direct.
The [commitizen CLI](https://github.com/commitizen/cz-cli) can help to construct these commit messages.
## Sign the Contributor License Agreement
We'd love to accept your sample apps and patches! Before we can take them, we have to jump a couple of legal hurdles.
Please fill out either the individual or corporate Contributor License Agreement (CLA).
* If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual).
* If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate).
Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to
accept your pull requests.
## Tracking Errors
We track our errors in the wild with Sentry. In general, do not worry about wrapping your audits or gatherers in try/catch blocks and reporting every error that could possibly occur; `core/runner.js` and `core/gather/*-runner.js` already catch and report any errors that occur while running a gatherer or audit, including errors fatal to the entire run. However, there are some situations when you might want to explicitly handle an error and report it to Sentry or wrap it to avoid reporting. Generally, you can interact with Sentry simply by requiring the `core/lib/sentry.js` file and call its methods. The module exports a delegate that will correctly handle the error reporting based on the user's opt-in preference and will simply no-op if they haven't so you don't need to check.
#### If you have an expected error that is recoverable but want to track how frequently it happens, *use Sentry.captureException*.
```js
const Sentry = require('./core/lib/sentry');
try {
doRiskyThing();
} catch (err) {
Sentry.captureException(err, {
tags: {audit: 'audit-name'},
level: 'warning',
});
doFallbackThing();
}
```
#### If you need to track a code path that doesn't necessarily mean an error occurred, *use Sentry.captureMessage*.
NOTE: If the message you're capturing is dynamic/based on user data or you need a stack trace, then create a fake error instead and use `Sentry.captureException` so that the instances will be grouped together in Sentry.
```js
const Sentry = require('./core/lib/sentry');
if (networkRecords.length === 1) {
Sentry.captureMessage('Site only had 1 network request', {level: 'info'});
return null;
} else {
// do your thang
}
```
#### Level Guide
- `info` for events that don't indicate a bug but should be tracked
- `warning` for events that might indicate unexpected behavior but is recoverable
- `error` for events that caused an audit/gatherer failure but were not fatal
- `fatal` for events that caused Lighthouse to exit early and not produce a report
# For Maintainers
The [release guide](./docs/releasing.md).

202
node_modules/lighthouse/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

16
node_modules/lighthouse/build-tracker.config.js generated vendored Normal file
View File

@@ -0,0 +1,16 @@
// https://buildtracker.dev/docs/installation/#upload-your-builds
module.exports = {
applicationUrl: 'https://lh-build-tracker.herokuapp.com',
artifacts: [
'dist/lightrider/lighthouse-lr-bundle.js',
'dist/extension/scripts/lighthouse-ext-bundle.js',
'dist/lighthouse-dt-bundle.js',
'dist/gh-pages/viewer/src/bundled.js',
'dist/gh-pages/treemap/src/bundled.js',
'dist/lightrider/report-generator-bundle.js',
'dist/dt-report-resources/report.js',
'dist/dt-report-resources/report-generator.js',
],
};

6085
node_modules/lighthouse/changelog-pre10.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

5
node_modules/lighthouse/cli/bin.d.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
/**
* @return {Promise<LH.RunnerResult|void>}
*/
export function begin(): Promise<LH.RunnerResult | void>;
//# sourceMappingURL=bin.d.ts.map

140
node_modules/lighthouse/cli/bin.js generated vendored Normal file
View File

@@ -0,0 +1,140 @@
/**
* @license Copyright 2016 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview The relationship between these CLI modules:
*
* index.js : only calls bin.js's begin()
* cli-flags.js : leverages yargs to read argv, outputs LH.CliFlags
* bin.js : CLI args processing. cwd, list/print commands
* run.js : chrome-launcher bits, calling core, output to Printer
*
* index ----> bin ----> run ----> printer
* ⭏ ⭎ ⭏ ⭎
* cli-flags lh-core/index
*/
import fs from 'fs';
import path from 'path';
import url from 'url';
import log from 'lighthouse-logger';
import * as commands from './commands/commands.js';
import * as Printer from './printer.js';
import {getFlags} from './cli-flags.js';
import {runLighthouse} from './run.js';
import {askPermission} from './sentry-prompt.js';
import {LH_ROOT} from '../root.js';
import {Sentry} from '../core/lib/sentry.js';
const pkg = JSON.parse(fs.readFileSync(LH_ROOT + '/package.json', 'utf-8'));
/**
* @return {boolean}
*/
function isDev() {
return fs.existsSync(path.join(LH_ROOT, '/.git'));
}
/**
* @return {Promise<LH.RunnerResult|void>}
*/
async function begin() {
const cliFlags = getFlags();
// Process terminating command
if (cliFlags.listAllAudits) {
commands.listAudits();
}
// Process terminating command
if (cliFlags.listLocales) {
commands.listLocales();
}
// Process terminating command
if (cliFlags.listTraceCategories) {
commands.listTraceCategories();
}
const urlUnderTest = cliFlags._[0];
/** @type {LH.Config|undefined} */
let config;
if (cliFlags.configPath) {
// Resolve the config file path relative to where cli was called.
cliFlags.configPath = path.resolve(process.cwd(), cliFlags.configPath);
if (cliFlags.configPath.endsWith('.json')) {
config = JSON.parse(fs.readFileSync(cliFlags.configPath, 'utf-8'));
} else {
const configModuleUrl = url.pathToFileURL(cliFlags.configPath).href;
config = (await import(configModuleUrl)).default;
}
} else if (cliFlags.preset) {
config = (await import(`../core/config/${cliFlags.preset}-config.js`)).default;
}
if (cliFlags.budgetPath) {
cliFlags.budgetPath = path.resolve(process.cwd(), cliFlags.budgetPath);
/** @type {Array<LH.Budget>} */
const parsedBudget = JSON.parse(fs.readFileSync(cliFlags.budgetPath, 'utf8'));
cliFlags.budgets = parsedBudget;
}
// set logging preferences
cliFlags.logLevel = 'info';
if (cliFlags.verbose) {
cliFlags.logLevel = 'verbose';
} else if (cliFlags.quiet) {
cliFlags.logLevel = 'silent';
}
log.setLevel(cliFlags.logLevel);
if (
cliFlags.output.length === 1 &&
cliFlags.output[0] === Printer.OutputMode.json &&
!cliFlags.outputPath
) {
cliFlags.outputPath = 'stdout';
}
if (cliFlags.precomputedLanternDataPath) {
const lanternDataStr = fs.readFileSync(cliFlags.precomputedLanternDataPath, 'utf8');
/** @type {LH.PrecomputedLanternData} */
const data = JSON.parse(lanternDataStr);
if (!data.additionalRttByOrigin || !data.serverResponseTimeByOrigin) {
throw new Error('Invalid precomputed lantern data file');
}
cliFlags.precomputedLanternData = data;
}
// By default, cliFlags.enableErrorReporting is undefined so the user is
// prompted. This can be overridden with an explicit flag or by the cached
// answer returned by askPermission().
if (typeof cliFlags.enableErrorReporting === 'undefined') {
cliFlags.enableErrorReporting = await askPermission();
}
if (cliFlags.enableErrorReporting) {
await Sentry.init({
url: urlUnderTest,
flags: cliFlags,
environmentData: {
serverName: 'redacted', // prevent sentry from using hostname
environment: isDev() ? 'development' : 'production',
release: pkg.version,
},
});
}
return runLighthouse(urlUnderTest, cliFlags, config);
}
export {
begin,
};

258
node_modules/lighthouse/cli/cli-flags.d.ts generated vendored Normal file
View File

@@ -0,0 +1,258 @@
/**
* @param {string=} manualArgv
* @param {{noExitOnFailure?: boolean}=} options
* @return {LH.CliFlags}
*/
export function getFlags(manualArgv?: string | undefined, options?: {
noExitOnFailure?: boolean;
} | undefined): LH.CliFlags;
/**
* @param {string=} manualArgv
*/
export function getYargsParser(manualArgv?: string | undefined): yargs.Argv<yargs.Omit<yargs.Omit<yargs.Omit<yargs.Omit<yargs.Omit<yargs.Omit<yargs.Omit<yargs.Omit<{
_: string[] | undefined;
} & {
"cli-flags-path": unknown;
}, "verbose" | "quiet"> & yargs.InferredOptionTypes<{
verbose: {
type: "boolean";
default: boolean;
describe: string;
};
quiet: {
type: "boolean";
default: boolean;
describe: string;
};
}>, "port" | "screenEmulation" | "emulatedUserAgent" | "hostname" | "preset" | "save-assets" | "list-all-audits" | "list-locales" | "list-trace-categories" | "debug-navigation" | "legacy-navigation" | "additional-trace-categories" | "config-path" | "chrome-flags" | "form-factor" | "max-wait-for-load" | "enable-error-reporting" | "gather-mode" | "audit-mode" | "only-audits" | "only-categories" | "skip-audits" | "budget-path" | "disable-full-page-screenshot"> & yargs.InferredOptionTypes<{
'save-assets': {
type: "boolean";
default: boolean;
describe: string;
};
'list-all-audits': {
type: "boolean";
default: boolean;
describe: string;
};
'list-locales': {
type: "boolean";
default: boolean;
describe: string;
};
'list-trace-categories': {
type: "boolean";
default: boolean;
describe: string;
};
'debug-navigation': {
type: "boolean";
describe: string;
};
'legacy-navigation': {
type: "boolean";
default: boolean;
describe: string;
};
'additional-trace-categories': {
type: "string";
describe: string;
};
'config-path': {
type: "string";
describe: string;
};
preset: {
type: "string";
describe: string;
};
'chrome-flags': {
type: "string";
default: string;
describe: string;
};
port: {
type: "number";
default: number;
describe: string;
};
hostname: {
type: "string";
default: string;
describe: string;
};
'form-factor': {
type: "string";
describe: string;
};
screenEmulation: {
describe: string;
coerce: typeof coerceScreenEmulation;
};
emulatedUserAgent: {
type: "string";
coerce: typeof coerceOptionalStringBoolean;
describe: string;
};
'max-wait-for-load': {
type: "number";
describe: string;
};
'enable-error-reporting': {
type: "boolean";
describe: string;
};
'gather-mode': {
alias: string;
coerce: typeof coerceOptionalStringBoolean;
describe: string;
};
'audit-mode': {
alias: string;
coerce: typeof coerceOptionalStringBoolean;
describe: string;
};
'only-audits': {
array: true;
type: "string";
coerce: typeof splitCommaSeparatedValues;
describe: string;
};
'only-categories': {
array: true;
type: "string";
coerce: typeof splitCommaSeparatedValues;
describe: string;
};
'skip-audits': {
array: true;
type: "string";
coerce: typeof splitCommaSeparatedValues;
describe: string;
};
'budget-path': {
type: "string";
describe: string;
};
'disable-full-page-screenshot': {
type: "boolean";
describe: string;
};
}>, "output" | "view" | "output-path"> & yargs.InferredOptionTypes<{
output: {
type: "array";
default: readonly ["html"];
coerce: typeof coerceOutput;
describe: string;
};
'output-path': {
type: "string";
coerce: typeof coerceOutputPath;
describe: string;
};
view: {
type: "boolean";
default: boolean;
describe: string;
};
}>, "locale" | "blocked-url-patterns" | "disable-storage-reset" | "throttling-method"> & yargs.InferredOptionTypes<{
locale: {
coerce: typeof coerceLocale;
describe: string;
};
'blocked-url-patterns': {
array: true;
type: "string";
describe: string;
};
'disable-storage-reset': {
type: "boolean";
describe: string;
};
'throttling-method': {
type: "string";
describe: string;
};
}> & {
throttling: import("../types/lh.js").ThrottlingSettings | undefined;
}, "channel" | "plugins" | "extra-headers" | "precomputed-lantern-data-path" | "lantern-data-output-path" | "chrome-ignore-default-flags"> & yargs.InferredOptionTypes<{
'extra-headers': {
coerce: typeof coerceExtraHeaders;
describe: string;
};
'precomputed-lantern-data-path': {
type: "string";
describe: string;
};
'lantern-data-output-path': {
type: "string";
describe: string;
};
plugins: {
array: true;
type: "string";
coerce: typeof splitCommaSeparatedValues;
describe: string;
};
channel: {
type: "string";
default: string;
};
'chrome-ignore-default-flags': {
type: "boolean";
default: boolean;
};
}>, "form-factor"> & {
"form-factor": "mobile" | "desktop" | undefined;
}, "throttling-method"> & {
"throttling-method": "devtools" | "simulate" | "provided" | undefined;
}, "preset"> & {
preset: "desktop" | "experimental" | "perf" | undefined;
}>;
import yargs from 'yargs';
/**
* Take yarg's unchecked object value and ensure it is a proper LH.screenEmulationSettings.
* @param {unknown} value
* @return {Partial<LH.ScreenEmulationSettings>|undefined}
*/
declare function coerceScreenEmulation(value: unknown): Partial<LH.ScreenEmulationSettings> | undefined;
/**
* @param {unknown} value
* @return {boolean|string|undefined}
*/
declare function coerceOptionalStringBoolean(value: unknown): boolean | string | undefined;
/**
* Support comma-separated values for some array flags by splitting on any ',' found.
* @param {Array<string>=} strings
* @return {Array<string>=}
*/
declare function splitCommaSeparatedValues(strings?: Array<string> | undefined): Array<string> | undefined;
/**
* Coerce output CLI input to `LH.SharedFlagsSettings['output']` or throw if not possible.
* @param {Array<unknown>} values
* @return {Array<LH.OutputMode>}
*/
declare function coerceOutput(values: Array<unknown>): Array<LH.OutputMode>;
/**
* Verifies outputPath is something we can actually write to.
* @param {unknown=} value
* @return {string=}
*/
declare function coerceOutputPath(value?: unknown | undefined): string | undefined;
/**
* Verifies value is a string, then coerces type to LH.Locale for convenience. However, don't
* allowlist specific locales. Why? So we can support the user who requests 'es-MX' (unsupported)
* and we'll fall back to 'es' (supported).
* @param {unknown} value
* @return {LH.Locale|undefined}
*/
declare function coerceLocale(value: unknown): LH.Locale | undefined;
/**
* `--extra-headers` comes in as a JSON string or a path to a JSON string, but the flag value
* needs to be the parsed object. Load file (if necessary) and returns the parsed object.
* @param {unknown} value
* @return {LH.SharedFlagsSettings['extraHeaders']}
*/
declare function coerceExtraHeaders(value: unknown): LH.SharedFlagsSettings['extraHeaders'];
export {};
//# sourceMappingURL=cli-flags.d.ts.map

553
node_modules/lighthouse/cli/cli-flags.js generated vendored Normal file
View File

@@ -0,0 +1,553 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/* eslint-disable max-len */
import fs from 'fs';
import path from 'path';
import yargs from 'yargs';
import * as yargsHelpers from 'yargs/helpers';
import {LH_ROOT} from '../root.js';
import {isObjectOfUnknownValues} from '../shared/type-verifiers.js';
/**
* @param {string=} manualArgv
*/
function getYargsParser(manualArgv) {
const y = manualArgv ?
// @ts-expect-error - undocumented, but yargs() supports parsing a single `string`.
yargs(manualArgv) :
yargs(yargsHelpers.hideBin(process.argv));
return y.help('help')
.version(JSON.parse(fs.readFileSync(`${LH_ROOT}/package.json`, 'utf-8')).version)
.showHelpOnFail(false, 'Specify --help for available options')
.usage('lighthouse <url> <options>')
.example(
'lighthouse <url> --view', 'Opens the HTML report in a browser after the run completes')
.example(
'lighthouse <url> --config-path=./myconfig.js',
'Runs Lighthouse with your own configuration: custom audits, report generation, etc.')
.example(
'lighthouse <url> --output=json --output-path=./report.json --save-assets',
'Save trace, screenshots, and named JSON report.')
.example(
'lighthouse <url> --screenEmulation.disabled --throttling-method=provided --no-emulated-user-agent',
'Disable emulation and all throttling')
.example(
'lighthouse <url> --chrome-flags="--window-size=412,660"',
'Launch Chrome with a specific window size')
.example(
'lighthouse <url> --quiet --chrome-flags="--headless"',
'Launch Headless Chrome, turn off logging')
.example(
'lighthouse <url> --extra-headers "{\\"Cookie\\":\\"monster=blue\\", \\"x-men\\":\\"wolverine\\"}"',
'Stringify\'d JSON HTTP Header key/value pairs to send in requests')
.example(
'lighthouse <url> --extra-headers=./path/to/file.json',
'Path to JSON file of HTTP Header key/value pairs to send in requests')
.example(
'lighthouse <url> --only-categories=performance,pwa',
'Only run the specified categories. Available categories: accessibility, best-practices, performance, pwa, seo')
// We only have the single string positional argument, the url.
.option('_', {
array: true, // Always an array, but this lets the type system know.
type: 'string',
})
/*
* Also accept a file for all of these flags. Yargs will merge in and override the file-based
* flags with the command-line flags.
*
* i.e. when command-line `--throttling-method=provided` and file `throttlingMethod: "devtools"`,
* throttlingMethod will be `provided`.
*
* @see https://github.com/yargs/yargs/blob/a6e67f15a61558d0ba28bfe53385332f0ce5d431/docs/api.md#config
*/
.option('cli-flags-path', {
config: true,
describe: 'The path to a JSON file that contains the desired CLI flags to apply. Flags specified at the command line will still override the file-based ones.',
})
// Logging
.options({
'verbose': {
type: 'boolean',
default: false,
describe: 'Displays verbose logging',
},
'quiet': {
type: 'boolean',
default: false,
describe: 'Displays no progress, debug logs, or errors',
},
})
.group(['verbose', 'quiet'], 'Logging:')
// Configuration
.options({
'save-assets': {
type: 'boolean',
default: false,
describe: 'Save the trace contents & devtools logs to disk',
},
'list-all-audits': {
type: 'boolean',
default: false,
describe: 'Prints a list of all available audits and exits',
},
'list-locales': {
type: 'boolean',
default: false,
describe: 'Prints a list of all supported locales and exits',
},
'list-trace-categories': {
type: 'boolean',
default: false,
describe: 'Prints a list of all required trace categories and exits',
},
'debug-navigation': {
type: 'boolean',
describe: 'Pause after page load to wait for permission to continue the run, evaluate `continueLighthouseRun` in the console to continue.',
},
'legacy-navigation': {
type: 'boolean',
default: false,
describe: '[DEPRECATED] Use the legacy navigation runner to gather results. Only use this if you are using a pre-10.0 custom Lighthouse config, or if Lighthouse unexpectedly fails after updating to 10.0. Please file a bug if you need this flag for Lighthouse to work.',
},
'additional-trace-categories': {
type: 'string',
describe: 'Additional categories to capture with the trace (comma-delimited).',
},
'config-path': {
type: 'string',
describe: `The path to the config JSON.
An example config file: core/config/lr-desktop-config.js`,
},
'preset': {
type: 'string',
describe: `Use a built-in configuration.
WARNING: If the --config-path flag is provided, this preset will be ignored.`,
},
'chrome-flags': {
type: 'string',
default: '',
describe: `Custom flags to pass to Chrome (space-delimited). For a full list of flags, see https://bit.ly/chrome-flags
Additionally, use the CHROME_PATH environment variable to use a specific Chrome binary. Requires Chromium version 66.0 or later. If omitted, any detected Chrome Canary or Chrome stable will be used.`,
},
'port': {
type: 'number',
default: 0,
describe: 'The port to use for the debugging protocol. Use 0 for a random port',
},
'hostname': {
type: 'string',
default: '127.0.0.1',
describe: 'The hostname to use for the debugging protocol.',
},
'form-factor': {
type: 'string',
describe: 'Determines how performance metrics are scored and if mobile-only audits are skipped. For desktop, --preset=desktop instead.',
},
'screenEmulation': {
describe: 'Sets screen emulation parameters. See also --preset. Use --screenEmulation.disabled to disable. Otherwise set these 4 parameters individually: --screenEmulation.mobile --screenEmulation.width=360 --screenEmulation.height=640 --screenEmulation.deviceScaleFactor=2',
coerce: coerceScreenEmulation,
},
'emulatedUserAgent': {
type: 'string',
coerce: coerceOptionalStringBoolean,
describe: 'Sets useragent emulation',
},
'max-wait-for-load': {
type: 'number',
describe: 'The timeout (in milliseconds) to wait before the page is considered done loading and the run should continue. WARNING: Very high values can lead to large traces and instability',
},
'enable-error-reporting': {
type: 'boolean',
describe: 'Enables error reporting, overriding any saved preference. --no-enable-error-reporting will do the opposite. More: https://github.com/GoogleChrome/lighthouse/blob/main/docs/error-reporting.md',
},
'gather-mode': {
alias: 'G',
coerce: coerceOptionalStringBoolean,
describe: 'Collect artifacts from a connected browser and save to disk. (Artifacts folder path may optionally be provided). If audit-mode is not also enabled, the run will quit early.',
},
'audit-mode': {
alias: 'A',
coerce: coerceOptionalStringBoolean,
describe: 'Process saved artifacts from disk. (Artifacts folder path may be provided, otherwise defaults to ./latest-run/)',
},
'only-audits': {
array: true,
type: 'string',
coerce: splitCommaSeparatedValues,
describe: 'Only run the specified audits',
},
'only-categories': {
array: true,
type: 'string',
coerce: splitCommaSeparatedValues,
describe: 'Only run the specified categories. Available categories: accessibility, best-practices, performance, pwa, seo',
},
'skip-audits': {
array: true,
type: 'string',
coerce: splitCommaSeparatedValues,
describe: 'Run everything except these audits',
},
'budget-path': {
type: 'string',
describe: 'The path to the budget.json file for LightWallet.',
},
'disable-full-page-screenshot': {
type: 'boolean',
describe: 'Disables collection of the full page screenshot, which can be quite large',
},
})
.group([
'save-assets', 'list-all-audits', 'list-locales', 'list-trace-categories', 'additional-trace-categories',
'config-path', 'preset', 'chrome-flags', 'port', 'hostname', 'form-factor', 'screenEmulation', 'emulatedUserAgent',
'max-wait-for-load', 'enable-error-reporting', 'gather-mode', 'audit-mode',
'only-audits', 'only-categories', 'skip-audits', 'budget-path', 'disable-full-page-screenshot',
], 'Configuration:')
// Output
.options({
'output': {
type: 'array',
default: /** @type {const} */ (['html']),
coerce: coerceOutput,
describe: 'Reporter for the results, supports multiple values. choices: "json", "html", "csv"',
},
'output-path': {
type: 'string',
coerce: coerceOutputPath,
describe: `The file path to output the results. Use 'stdout' to write to stdout.
If using JSON output, default is stdout.
If using HTML or CSV output, default is a file in the working directory with a name based on the test URL and date.
If using multiple outputs, --output-path is appended with the standard extension for each output type. "reports/my-run" -> "reports/my-run.report.html", "reports/my-run.report.json", etc.
Example: --output-path=./lighthouse-results.html`,
},
'view': {
type: 'boolean',
default: false,
describe: 'Open HTML report in your browser',
},
})
.group(['output', 'output-path', 'view'], 'Output:')
// Other options.
.options({
'locale': {
coerce: coerceLocale,
describe: 'The locale/language the report should be formatted in',
},
'blocked-url-patterns': {
array: true,
type: 'string',
describe: 'Block any network requests to the specified URL patterns',
},
'disable-storage-reset': {
type: 'boolean',
describe: 'Disable clearing the browser cache and other storage APIs before a run',
},
'throttling-method': {
type: 'string',
describe: 'Controls throttling method',
},
})
// Throttling settings, parsed as an object.
.option('throttling', {
coerce: coerceThrottling,
})
.describe({
'throttling.rttMs': 'Controls simulated network RTT (TCP layer)',
'throttling.throughputKbps': 'Controls simulated network download throughput',
'throttling.requestLatencyMs': 'Controls emulated network RTT (HTTP layer)',
'throttling.downloadThroughputKbps': 'Controls emulated network download throughput',
'throttling.uploadThroughputKbps': 'Controls emulated network upload throughput',
'throttling.cpuSlowdownMultiplier': 'Controls simulated + emulated CPU throttling',
})
.options({
'extra-headers': {
coerce: coerceExtraHeaders,
describe: 'Set extra HTTP Headers to pass with request',
},
'precomputed-lantern-data-path': {
type: 'string',
describe: 'Path to the file where lantern simulation data should be read from, overwriting the lantern observed estimates for RTT and server latency.',
},
'lantern-data-output-path': {
type: 'string',
describe: 'Path to the file where lantern simulation data should be written to, can be used in a future run with the `precomputed-lantern-data-path` flag.',
},
'plugins': {
array: true,
type: 'string',
coerce: splitCommaSeparatedValues,
describe: 'Run the specified plugins',
},
'channel': {
type: 'string',
default: 'cli',
},
'chrome-ignore-default-flags': {
type: 'boolean',
default: false,
},
})
// Choices added outside of `options()` and cast so tsc picks them up.
.choices('form-factor', /** @type {const} */ (['mobile', 'desktop']))
.choices('throttling-method', /** @type {const} */ (['devtools', 'provided', 'simulate']))
.choices('preset', /** @type {const} */ (['perf', 'experimental', 'desktop']))
.check(argv => {
// Lighthouse doesn't need a URL if...
// - We're just listing the available options.
// - We're just printing the config.
// - We're in auditMode (and we have artifacts already)
// If one of these don't apply, if no URL, stop the program and ask for one.
const isPrintSomethingMode = argv.listAllAudits || argv.listLocales || argv.listTraceCategories;
const isOnlyAuditMode = !!argv.auditMode && !argv.gatherMode;
if (isPrintSomethingMode || isOnlyAuditMode) {
return true;
} else if (argv._.length > 0) {
return true;
}
throw new Error('Please provide a url');
})
.epilogue('For more information on Lighthouse, see https://developers.google.com/web/tools/lighthouse/.')
.wrap(y.terminalWidth());
}
/**
* @param {string=} manualArgv
* @param {{noExitOnFailure?: boolean}=} options
* @return {LH.CliFlags}
*/
function getFlags(manualArgv, options = {}) {
let parser = getYargsParser(manualArgv);
if (options.noExitOnFailure) {
// Silence console.error() logging and don't process.exit().
// `parser.fail(false)` can be used in yargs once v17 is released.
parser = parser.fail((msg, err) => {
if (err) throw err;
else if (msg) throw new Error(msg);
});
}
// Augmenting yargs type with auto-camelCasing breaks in tsc@4.1.2 and @types/yargs@15.0.11,
// so for now cast to add yarg's camelCase properties to type.
const argv = /** @type {Awaited<typeof parser.argv>} */ (parser.argv);
const cliFlags = /** @type {typeof argv & LH.Util.CamelCasify<typeof argv>} */ (argv);
// yargs will return `undefined` for options that have a `coerce` function but
// are not actually present in the user input. Instead of passing properties
// explicitly set to undefined, delete them from the flags object.
for (const [k, v] of Object.entries(cliFlags)) {
if (v === undefined) delete cliFlags[k];
}
return cliFlags;
}
/**
* Support comma-separated values for some array flags by splitting on any ',' found.
* @param {Array<string>=} strings
* @return {Array<string>=}
*/
function splitCommaSeparatedValues(strings) {
if (!strings) return;
return strings.flatMap(value => value.split(','));
}
/**
* @param {unknown} value
* @return {boolean|string|undefined}
*/
function coerceOptionalStringBoolean(value) {
if (value === undefined) return;
if (typeof value !== 'string' && typeof value !== 'boolean') {
throw new Error('Invalid value: Argument must be a string or a boolean');
}
return value;
}
/**
* Coerce output CLI input to `LH.SharedFlagsSettings['output']` or throw if not possible.
* @param {Array<unknown>} values
* @return {Array<LH.OutputMode>}
*/
function coerceOutput(values) {
const outputTypes = ['json', 'html', 'csv'];
const errorHint = `Argument 'output' must be an array from choices "${outputTypes.join('", "')}"`;
if (!values.every(/** @return {item is string} */ item => typeof item === 'string')) {
throw new Error('Invalid values. ' + errorHint);
}
// Allow parsing of comma-separated values.
const strings = values.flatMap(value => value.split(','));
const validValues = strings.filter(/** @return {str is LH.OutputMode} */ str => {
if (!outputTypes.includes(str)) {
throw new Error(`"${str}" is not a valid 'output' value. ` + errorHint);
}
return true;
});
return validValues;
}
/**
* Verifies outputPath is something we can actually write to.
* @param {unknown=} value
* @return {string=}
*/
function coerceOutputPath(value) {
if (value === undefined) return;
if (typeof value !== 'string' || !value || !fs.existsSync(path.dirname(value))) {
throw new Error(`--output-path (${value}) cannot be written to`);
}
return value;
}
/**
* Verifies value is a string, then coerces type to LH.Locale for convenience. However, don't
* allowlist specific locales. Why? So we can support the user who requests 'es-MX' (unsupported)
* and we'll fall back to 'es' (supported).
* @param {unknown} value
* @return {LH.Locale|undefined}
*/
function coerceLocale(value) {
if (value === undefined) return;
if (typeof value !== 'string') throw new Error(`Invalid value: Argument 'locale' must be a string`);
return /** @type {LH.Locale} */ (value);
}
/**
* `--extra-headers` comes in as a JSON string or a path to a JSON string, but the flag value
* needs to be the parsed object. Load file (if necessary) and returns the parsed object.
* @param {unknown} value
* @return {LH.SharedFlagsSettings['extraHeaders']}
*/
function coerceExtraHeaders(value) {
// TODO: this function does not actually verify the object type.
if (value === undefined) return value;
if (typeof value === 'object') return /** @type {LH.SharedFlagsSettings['extraHeaders']} */ (value);
if (typeof value !== 'string') {
throw new Error(`Invalid value: Argument 'extra-headers' must be a string`);
}
// (possibly) load and parse extra headers from JSON.
if (!value.startsWith('{')) {
// If not a JSON object, assume it's a path to a JSON file.
return JSON.parse(fs.readFileSync(value, 'utf-8'));
}
return JSON.parse(value);
}
/**
* Take yarg's unchecked object value and ensure it's proper throttling settings.
* @param {unknown} value
* @return {LH.ThrottlingSettings|undefined}
*/
function coerceThrottling(value) {
if (value === undefined) return;
if (!isObjectOfUnknownValues(value)) {
throw new Error(`Invalid value: Argument 'throttling' must be an object, specified per-property ('throttling.rttMs', 'throttling.throughputKbps', etc)`);
}
/** @type {Array<keyof LH.ThrottlingSettings>} */
const throttlingKeys = [
'rttMs',
'throughputKbps',
'requestLatencyMs',
'downloadThroughputKbps',
'uploadThroughputKbps',
'cpuSlowdownMultiplier',
];
/** @type {LH.ThrottlingSettings} */
const throttlingSettings = {};
for (const key of throttlingKeys) {
const possibleSetting = value[key];
if (possibleSetting !== undefined && typeof possibleSetting !== 'number') {
throw new Error(`Invalid value: 'throttling.${key}' must be a number`);
}
// Note: this works type-wise because the throttling settings all have the same type.
throttlingSettings[key] = possibleSetting;
}
return throttlingSettings;
}
/**
* Take yarg's unchecked object value and ensure it is a proper LH.screenEmulationSettings.
* @param {unknown} value
* @return {Partial<LH.ScreenEmulationSettings>|undefined}
*/
function coerceScreenEmulation(value) {
if (value === undefined) return;
if (!isObjectOfUnknownValues(value)) {
throw new Error(`Invalid value: Argument 'screenEmulation' must be an object, specified per-property ('screenEmulation.width', 'screenEmulation.deviceScaleFactor', etc)`);
}
/** @type {Array<keyof LH.ScreenEmulationSettings>} */
const keys = ['width', 'height', 'deviceScaleFactor', 'mobile', 'disabled'];
/** @type {Partial<LH.ScreenEmulationSettings>} */
const screenEmulationSettings = {};
for (const key of keys) {
const possibleSetting = value[key];
switch (key) {
case 'width':
case 'height':
case 'deviceScaleFactor':
if (possibleSetting !== undefined && typeof possibleSetting !== 'number') {
throw new Error(`Invalid value: 'screenEmulation.${key}' must be a number`);
}
screenEmulationSettings[key] = possibleSetting;
break;
case 'mobile':
case 'disabled':
// Manually coerce 'true'/'false' strings to booleans since nested property types aren't set.
if (possibleSetting === 'true') {
screenEmulationSettings[key] = true;
} else if (possibleSetting === 'false') {
screenEmulationSettings[key] = false;
} else if (possibleSetting === undefined || typeof possibleSetting === 'boolean') {
screenEmulationSettings[key] = possibleSetting;
} else {
throw new Error(`Invalid value: 'screenEmulation.${key}' must be a boolean`);
}
break;
default:
throw new Error(`Unrecognized screenEmulation option: ${key}`);
}
}
return screenEmulationSettings;
}
export {
getFlags,
getYargsParser,
};

4
node_modules/lighthouse/cli/commands/commands.d.ts generated vendored Normal file
View File

@@ -0,0 +1,4 @@
export { listAudits } from "./list-audits.js";
export { listTraceCategories } from "./list-trace-categories.js";
export { listLocales } from "./list-locales.js";
//# sourceMappingURL=commands.d.ts.map

9
node_modules/lighthouse/cli/commands/commands.js generated vendored Normal file
View File

@@ -0,0 +1,9 @@
/**
* @license Copyright 2016 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
export {listAudits} from './list-audits.js';
export {listTraceCategories} from './list-trace-categories.js';
export {listLocales} from './list-locales.js';

View File

@@ -0,0 +1,2 @@
export function listAudits(): void;
//# sourceMappingURL=list-audits.d.ts.map

15
node_modules/lighthouse/cli/commands/list-audits.js generated vendored Normal file
View File

@@ -0,0 +1,15 @@
/**
* @license Copyright 2016 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import {getAuditList} from '../../core/index.js';
function listAudits() {
const audits = getAuditList().map((i) => i.replace(/\.js$/, ''));
process.stdout.write(JSON.stringify({audits}, null, 2));
process.exit(0);
}
export {listAudits};

View File

@@ -0,0 +1,2 @@
export function listLocales(): void;
//# sourceMappingURL=list-locales.d.ts.map

15
node_modules/lighthouse/cli/commands/list-locales.js generated vendored Normal file
View File

@@ -0,0 +1,15 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import {locales} from '../../shared/localization/locales.js';
function listLocales() {
const localesList = Object.keys(locales);
process.stdout.write(JSON.stringify({locales: localesList}, null, 2));
process.exit(0);
}
export {listLocales};

View File

@@ -0,0 +1,2 @@
export function listTraceCategories(): void;
//# sourceMappingURL=list-trace-categories.d.ts.map

View File

@@ -0,0 +1,14 @@
/**
* @license Copyright 2016 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import {traceCategories} from '../../core/index.js';
function listTraceCategories() {
process.stdout.write(JSON.stringify({traceCategories}));
process.exit(0);
}
export {listTraceCategories};

3
node_modules/lighthouse/cli/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
export {};
//# sourceMappingURL=index.d.ts.map

10
node_modules/lighthouse/cli/index.js generated vendored Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env node
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import {begin} from './bin.js';
await begin();

28
node_modules/lighthouse/cli/printer.d.ts generated vendored Normal file
View File

@@ -0,0 +1,28 @@
/**
* Verify output path to use, either stdout or a file path.
* @param {string} path
* @return {string}
*/
export function checkOutputPath(path: string): string;
/**
* Writes the output.
* @param {string} output
* @param {LH.OutputMode} mode
* @param {string} path
* @return {Promise<void>}
*/
export function write(output: string, mode: LH.OutputMode, path: string): Promise<void>;
/**
* An enumeration of acceptable output modes:
* 'json': JSON formatted results
* 'html': An HTML report
* 'csv': CSV formatted results
* @type {LH.Util.SelfMap<LH.OutputMode>}
*/
export const OutputMode: LH.Util.SelfMap<LH.OutputMode>;
/**
* Returns a list of valid output options.
* @return {Array<string>}
*/
export function getValidOutputOptions(): Array<string>;
//# sourceMappingURL=printer.d.ts.map

99
node_modules/lighthouse/cli/printer.js generated vendored Normal file
View File

@@ -0,0 +1,99 @@
/**
* @license Copyright 2016 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import fs from 'fs';
import log from 'lighthouse-logger';
/**
* An enumeration of acceptable output modes:
* 'json': JSON formatted results
* 'html': An HTML report
* 'csv': CSV formatted results
* @type {LH.Util.SelfMap<LH.OutputMode>}
*/
const OutputMode = {
json: 'json',
html: 'html',
csv: 'csv',
};
/**
* Verify output path to use, either stdout or a file path.
* @param {string} path
* @return {string}
*/
function checkOutputPath(path) {
if (!path) {
log.warn('Printer', 'No output path set; using stdout');
return 'stdout';
}
return path;
}
/**
* Writes the output to stdout.
* @param {string} output
* @return {Promise<void>}
*/
function writeToStdout(output) {
return new Promise(resolve => {
// small delay to avoid race with debug() logs
setTimeout(_ => {
process.stdout.write(`${output}\n`);
resolve();
}, 50);
});
}
/**
* Writes the output to a file.
* @param {string} filePath
* @param {string} output
* @param {LH.OutputMode} outputMode
* @return {Promise<void>}
*/
function writeFile(filePath, output, outputMode) {
return new Promise((resolve, reject) => {
// TODO: make this mkdir to the filePath.
fs.writeFile(filePath, output, (err) => {
if (err) {
return reject(err);
}
log.log('Printer', `${OutputMode[outputMode]} output written to ${filePath}`);
resolve();
});
});
}
/**
* Writes the output.
* @param {string} output
* @param {LH.OutputMode} mode
* @param {string} path
* @return {Promise<void>}
*/
async function write(output, mode, path) {
const outputPath = checkOutputPath(path);
return outputPath === 'stdout' ?
writeToStdout(output) :
writeFile(outputPath, output, mode);
}
/**
* Returns a list of valid output options.
* @return {Array<string>}
*/
function getValidOutputOptions() {
return Object.keys(OutputMode);
}
export {
checkOutputPath,
write,
OutputMode,
getValidOutputOptions,
};

24
node_modules/lighthouse/cli/run.d.ts generated vendored Normal file
View File

@@ -0,0 +1,24 @@
export type ExitError = Error & {
code: string;
friendlyMessage?: string;
};
/**
* exported for testing
* @param {string|Array<string>} flags
* @return {Array<string>}
*/
export function parseChromeFlags(flags?: string | Array<string>): Array<string>;
/**
* @param {LH.RunnerResult} runnerResult
* @param {LH.CliFlags} flags
* @return {Promise<void>}
*/
export function saveResults(runnerResult: LH.RunnerResult, flags: LH.CliFlags): Promise<void>;
/**
* @param {string} url
* @param {LH.CliFlags} flags
* @param {LH.Config|undefined} config
* @return {Promise<LH.RunnerResult|undefined>}
*/
export function runLighthouse(url: string, flags: LH.CliFlags, config: LH.Config | undefined): Promise<LH.RunnerResult | undefined>;
//# sourceMappingURL=run.d.ts.map

284
node_modules/lighthouse/cli/run.js generated vendored Normal file
View File

@@ -0,0 +1,284 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/* eslint-disable no-console */
import path from 'path';
import os from 'os';
import psList from 'ps-list';
import * as ChromeLauncher from 'chrome-launcher';
import yargsParser from 'yargs-parser';
import log from 'lighthouse-logger';
import open from 'open';
import * as Printer from './printer.js';
import lighthouse, {legacyNavigation} from '../core/index.js';
import {getLhrFilenamePrefix} from '../report/generator/file-namer.js';
import * as assetSaver from '../core/lib/asset-saver.js';
import UrlUtils from '../core/lib/url-utils.js';
/** @typedef {Error & {code: string, friendlyMessage?: string}} ExitError */
const _RUNTIME_ERROR_CODE = 1;
const _PROTOCOL_TIMEOUT_EXIT_CODE = 67;
/**
* exported for testing
* @param {string|Array<string>} flags
* @return {Array<string>}
*/
function parseChromeFlags(flags = '') {
// flags will be a string if there is only one chrome-flag parameter:
// i.e. `lighthouse --chrome-flags="--user-agent='My Agent' --headless"`
// flags will be an array if there are multiple chrome-flags parameters
// i.e. `lighthouse --chrome-flags="--user-agent='My Agent'" --chrome-flags="--headless"`
const trimmedFlags = (Array.isArray(flags) ? flags : [flags])
// `child_process.execFile` and other programmatic invocations will pass Lighthouse arguments atomically.
// Many developers aren't aware of this and attempt to pass arguments to LH as they would to a shell `--chromeFlags="--headless --no-sandbox"`.
// In this case, yargs will see `"--headless --no-sandbox"` and treat it as a single argument instead of the intended `--headless --no-sandbox`.
// We remove quotes that surround the entire expression to make this work.
// i.e. `child_process.execFile("lighthouse", ["http://google.com", "--chrome-flags='--headless --no-sandbox'")`
// the following regular expression removes those wrapping quotes:
.map((flagsGroup) => flagsGroup.replace(/^\s*('|")(.+)\1\s*$/, '$2').trim())
.join(' ').trim();
const parsed = yargsParser(trimmedFlags, {
configuration: {'camel-case-expansion': false, 'boolean-negation': false},
});
return Object
.keys(parsed)
// Remove unnecessary _ item provided by yargs,
.filter(key => key !== '_')
// Avoid '=true', then reintroduce quotes
.map(key => {
if (parsed[key] === true) return `--${key}`;
// ChromeLauncher passes flags to Chrome as atomic arguments, so do not double quote
// i.e. `lighthouse --chrome-flags="--user-agent='My Agent'"` becomes `chrome "--user-agent=My Agent"`
// see https://github.com/GoogleChrome/lighthouse/issues/3744
return `--${key}=${parsed[key]}`;
});
}
/**
* Attempts to connect to an instance of Chrome with an open remote-debugging
* port. If none is found, launches a debuggable instance.
* @param {LH.CliFlags} flags
* @return {Promise<ChromeLauncher.LaunchedChrome>}
*/
function getDebuggableChrome(flags) {
if (process.platform === 'darwin' && process.arch === 'x64') {
const cpus = os.cpus();
if (cpus[0].model.includes('Apple')) {
throw new Error(
'Launching Chrome on Mac Silicon (arm64) from an x64 Node installation results in ' +
'Rosetta translating the Chrome binary, even if Chrome is already arm64. This would ' +
'result in huge performance issues. To resolve this, you must run Lighthouse CLI with ' +
'a version of Node built for arm64. You should also confirm that your Chrome install ' +
'says arm64 in chrome://version');
}
}
return ChromeLauncher.launch({
port: flags.port,
ignoreDefaultFlags: flags.chromeIgnoreDefaultFlags,
chromeFlags: parseChromeFlags(flags.chromeFlags),
logLevel: flags.logLevel,
});
}
/** @return {never} */
function printConnectionErrorAndExit() {
console.error('Unable to connect to Chrome');
return process.exit(_RUNTIME_ERROR_CODE);
}
/** @return {never} */
function printProtocolTimeoutErrorAndExit() {
console.error('Debugger protocol timed out while connecting to Chrome.');
return process.exit(_PROTOCOL_TIMEOUT_EXIT_CODE);
}
/**
* @param {ExitError} err
* @return {never}
*/
function printRuntimeErrorAndExit(err) {
console.error('Runtime error encountered:', err.friendlyMessage || err.message);
if (err.stack) {
console.error(err.stack);
}
return process.exit(_RUNTIME_ERROR_CODE);
}
/**
* @param {ExitError} err
* @return {never}
*/
function printErrorAndExit(err) {
if (err.code === 'ECONNREFUSED') {
return printConnectionErrorAndExit();
} else if (err.code === 'CRI_TIMEOUT') {
return printProtocolTimeoutErrorAndExit();
} else {
return printRuntimeErrorAndExit(err);
}
}
/**
* @param {LH.RunnerResult} runnerResult
* @param {LH.CliFlags} flags
* @return {Promise<void>}
*/
async function saveResults(runnerResult, flags) {
const cwd = process.cwd();
if (flags.lanternDataOutputPath) {
const devtoolsLog = runnerResult.artifacts.devtoolsLogs.defaultPass;
await assetSaver.saveLanternNetworkData(devtoolsLog, flags.lanternDataOutputPath);
}
const shouldSaveResults = flags.auditMode || (flags.gatherMode === flags.auditMode);
if (!shouldSaveResults) return;
const {lhr, artifacts, report} = runnerResult;
// Use the output path as the prefix for all generated files.
// If no output path is set, generate a file prefix using the URL and date.
const configuredPath = !flags.outputPath || flags.outputPath === 'stdout' ?
getLhrFilenamePrefix(lhr) :
flags.outputPath.replace(/\.\w{2,4}$/, '');
const resolvedPath = path.resolve(cwd, configuredPath);
if (flags.saveAssets) {
await assetSaver.saveAssets(artifacts, lhr.audits, resolvedPath);
}
for (const outputType of flags.output) {
const extension = outputType;
const output = report[flags.output.indexOf(outputType)];
let outputPath = `${resolvedPath}.report.${extension}`;
// If there was only a single output and the user specified an outputPath, force usage of it.
if (flags.outputPath && flags.output.length === 1) outputPath = flags.outputPath;
await Printer.write(output, outputType, outputPath);
if (outputType === Printer.OutputMode[Printer.OutputMode.html]) {
if (flags.view) {
open(outputPath, {wait: false});
} else {
// eslint-disable-next-line max-len
log.log('CLI', 'Protip: Run lighthouse with `--view` to immediately open the HTML report in your browser');
}
}
}
}
/**
* Attempt to kill the launched Chrome, if defined.
* @param {ChromeLauncher.LaunchedChrome=} launchedChrome
* @return {Promise<void>}
*/
async function potentiallyKillChrome(launchedChrome) {
if (!launchedChrome) return;
/** @type {NodeJS.Timeout} */
let timeout;
const timeoutPromise = new Promise((_, reject) => {
timeout = setTimeout(reject, 5000, new Error('Timed out waiting to kill Chrome'));
});
return Promise.race([
launchedChrome.kill(),
timeoutPromise,
]).catch(async err => {
const runningProcesses = await psList();
if (!runningProcesses.some(proc => proc.pid === launchedChrome.pid)) {
log.warn('CLI', 'Warning: Chrome process could not be killed because it already exited.');
return;
}
throw new Error(`Couldn't quit Chrome process. ${err}`);
}).finally(() => {
clearTimeout(timeout);
});
}
/**
* @param {string} url
* @param {LH.CliFlags} flags
* @param {LH.Config|undefined} config
* @return {Promise<LH.RunnerResult|undefined>}
*/
async function runLighthouse(url, flags, config) {
/** @param {any} reason */
async function handleTheUnhandled(reason) {
process.stderr.write(`Unhandled Rejection. Reason: ${reason}\n`);
await potentiallyKillChrome(launchedChrome).catch(() => {});
setTimeout(_ => {
process.exit(1);
}, 100);
}
process.on('unhandledRejection', handleTheUnhandled);
/** @type {ChromeLauncher.LaunchedChrome|undefined} */
let launchedChrome;
try {
if (url && flags.auditMode && !flags.gatherMode) {
log.warn('CLI', 'URL parameter is ignored if -A flag is used without -G flag');
}
const shouldGather = flags.gatherMode || flags.gatherMode === flags.auditMode;
const shouldUseLocalChrome = UrlUtils.isLikeLocalhost(flags.hostname);
if (shouldGather && shouldUseLocalChrome) {
launchedChrome = await getDebuggableChrome(flags);
flags.port = launchedChrome.port;
}
if (flags.legacyNavigation) {
log.warn('CLI', 'Legacy navigation CLI is deprecated');
flags.channel = 'legacy-navigation-cli';
} else if (!flags.channel) {
flags.channel = 'cli';
}
const runnerResult = flags.legacyNavigation ?
await legacyNavigation(url, flags, config) :
await lighthouse(url, flags, config);
// If in gatherMode only, there will be no runnerResult.
if (runnerResult) {
await saveResults(runnerResult, flags);
}
await potentiallyKillChrome(launchedChrome);
process.removeListener('unhandledRejection', handleTheUnhandled);
// Runtime errors indicate something was *very* wrong with the page result.
// We don't want the user to have to parse the report to figure it out, so we'll still exit
// with an error code after we saved the results.
if (runnerResult?.lhr.runtimeError) {
const {runtimeError} = runnerResult.lhr;
return printErrorAndExit({
name: 'LighthouseError',
friendlyMessage: runtimeError.message,
code: runtimeError.code,
message: runtimeError.message,
});
}
return runnerResult;
} catch (err) {
await potentiallyKillChrome(launchedChrome).catch(() => {});
return printErrorAndExit(err);
}
}
export {
parseChromeFlags,
saveResults,
runLighthouse,
};

5
node_modules/lighthouse/cli/sentry-prompt.d.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
/**
* @return {Promise<boolean>}
*/
export function askPermission(): Promise<boolean>;
//# sourceMappingURL=sentry-prompt.d.ts.map

78
node_modules/lighthouse/cli/sentry-prompt.js generated vendored Normal file
View File

@@ -0,0 +1,78 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import Configstore from 'configstore';
import Confirm from 'enquirer';
import log from 'lighthouse-logger';
const MAXIMUM_WAIT_TIME = 20 * 1000;
// eslint-disable-next-line max-len
const MESSAGE = `${log.reset}We're constantly trying to improve Lighthouse and its reliability.\n ` +
`${log.reset}Learn more: https://github.com/GoogleChrome/lighthouse/blob/main/docs/error-reporting.md \n ` +
` ${log.bold}May we anonymously report runtime exceptions to improve the tool over time?${log.reset} `; // eslint-disable-line max-len
/**
* @return {Promise<boolean>}
*/
function prompt() {
if (!process.stdout.isTTY || process.env.CI) {
// Default non-interactive sessions to false
return Promise.resolve(false);
}
/** @type {NodeJS.Timer|undefined} */
let timeout;
const prompt = new Confirm.Confirm({
name: 'isErrorReportingEnabled',
initial: false,
message: MESSAGE,
actions: {ctrl: {}},
});
const timeoutPromise = new Promise((resolve) => {
timeout = setTimeout(() => {
prompt.close().then(() => {
log.warn('CLI', 'No response to error logging preference, errors will not be reported.');
resolve(false);
});
}, MAXIMUM_WAIT_TIME);
});
return Promise.race([
prompt.run().then(result => {
clearTimeout(/** @type {NodeJS.Timer} */ (timeout));
return result;
}),
timeoutPromise,
]);
}
/**
* @return {Promise<boolean>}
*/
function askPermission() {
return Promise.resolve().then(_ => {
const configstore = new Configstore('lighthouse');
let isErrorReportingEnabled = configstore.get('isErrorReportingEnabled');
if (typeof isErrorReportingEnabled === 'boolean') {
return Promise.resolve(isErrorReportingEnabled);
}
return prompt()
.then(response => {
isErrorReportingEnabled = response;
configstore.set('isErrorReportingEnabled', isErrorReportingEnabled);
return isErrorReportingEnabled;
});
// Error accessing configstore; default to false.
}).catch(_ => false);
}
export {
askPermission,
};

View File

@@ -0,0 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getAssertionReport works (multiple failing) 1`] = `
"X difference at cumulative-layout-shift audit.details.items.length
expected: []
found: [{\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444}]
X difference at cumulative-layout-shift audit.details.blah
expected: 123
found: undefined
found result:
{
\\"id\\": \\"cumulative-layout-shift\\",
\\"title\\": \\"Cumulative Layout Shift\\",
\\"description\\": \\"Cumulative Layout Shift measures the movement of visible elements within the viewport. [Learn more about the Cumulative Layout Shift metric](https://web.dev/cls/).\\",
\\"score\\": 0.8,
\\"scoreDisplayMode\\": \\"numeric\\",
\\"numericValue\\": 0.13570762803819444,
\\"numericUnit\\": \\"unitless\\",
\\"displayValue\\": \\"0.136\\",
\\"details\\": {
\\"type\\": \\"debugdata\\",
\\"items\\": [
{
\\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444
}
]
}
}"
`;
exports[`getAssertionReport works (trivial failing) 1`] = `
"X difference at cumulative-layout-shift audit.details.items.length
expected: []
found: [{\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444}]
found result:
{
\\"id\\": \\"cumulative-layout-shift\\",
\\"title\\": \\"Cumulative Layout Shift\\",
\\"description\\": \\"Cumulative Layout Shift measures the movement of visible elements within the viewport. [Learn more about the Cumulative Layout Shift metric](https://web.dev/cls/).\\",
\\"score\\": 0.8,
\\"scoreDisplayMode\\": \\"numeric\\",
\\"numericValue\\": 0.13570762803819444,
\\"numericUnit\\": \\"unitless\\",
\\"displayValue\\": \\"0.136\\",
\\"details\\": {
\\"type\\": \\"debugdata\\",
\\"items\\": [
{
\\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444
}
]
}
}"
`;
exports[`getAssertionReport works (trivial failing, actual undefined) 1`] = `
"Error: Config did not trigger run of expected audit cumulative-layout-shift-no-exist
X difference at cumulative-layout-shift-no-exist audit
expected: {\\"details\\":{\\"items\\":[]}}
found: undefined
found result:
undefined"
`;

View File

@@ -0,0 +1,12 @@
export default exclusions;
/**
* @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* List of smoke tests excluded per runner. eg: 'cli': ['a11y', 'dbw']
* @type {Record<string, Array<string>>}
*/
declare const exclusions: Record<string, Array<string>>;
//# sourceMappingURL=exclusions.d.ts.map

View File

@@ -0,0 +1,36 @@
/**
* @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* List of smoke tests excluded per runner. eg: 'cli': ['a11y', 'dbw']
* @type {Record<string, Array<string>>}
*/
const exclusions = {
'bundle': [],
'cli': [],
'devtools': [
// Disabled because normal Chrome usage makes DevTools not function on
// these poorly constructed pages
'errors-expired-ssl', 'errors-infinite-loop',
// Disabled because Chrome will follow the redirect first, and Lighthouse will
// only ever see/run the final URL.
'redirects-client-paint-server', 'redirects-multiple-server',
'redirects-single-server', 'redirects-single-client',
'redirects-history-push-state', 'redirects-scripts',
// Disabled because these tests use settings that cannot be fully configured in
// DevTools (e.g. throttling method "provided").
'metrics-tricky-tti', 'metrics-tricky-tti-late-fcp', 'screenshot',
// Disabled because of differences that need further investigation
'byte-efficiency', 'byte-gzip', 'perf-preload',
],
};
// https://github.com/GoogleChrome/lighthouse/issues/14271
for (const array of Object.values(exclusions)) {
array.push('lantern-idle-callback-short');
}
export default exclusions;

View File

@@ -0,0 +1,4 @@
export default smokeTests;
/** @type {ReadonlyArray<Smokehouse.TestDfn>} */
declare const smokeTests: ReadonlyArray<Smokehouse.TestDfn>;
//# sourceMappingURL=core-tests.d.ts.map

View File

@@ -0,0 +1,134 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import a11y from './test-definitions/a11y.js';
import byteEfficiency from './test-definitions/byte-efficiency.js';
import byteGzip from './test-definitions/byte-gzip.js';
import cspAllowAll from './test-definitions/csp-allow-all.js';
import cspBlockAll from './test-definitions/csp-block-all.js';
import dbw from './test-definitions/dobetterweb.js';
import errorsExpiredSsl from './test-definitions/errors-expired-ssl.js';
import errorsIframeExpiredSsl from './test-definitions/errors-iframe-expired-ssl.js';
import errorsInfiniteLoop from './test-definitions/errors-infinite-loop.js';
import formsAutoComplete from './test-definitions/forms-autocomplete.js';
import fpsMax from './test-definitions/fps-max.js';
import fpsMaxPassive from './test-definitions/fps-max-passive.js';
import fpsScaled from './test-definitions/fps-scaled.js';
import issuesMixedContent from './test-definitions/issues-mixed-content.js';
import lanternFetch from './test-definitions/lantern-fetch.js';
import lanternIdleCallbackLong from './test-definitions/lantern-idle-callback-long.js';
import lanternIdleCallbackShort from './test-definitions/lantern-idle-callback-short.js';
import lanternOnline from './test-definitions/lantern-online.js';
import lanternSetTimeout from './test-definitions/lantern-set-timeout.js';
import lanternXhr from './test-definitions/lantern-xhr.js';
import legacyJavascript from './test-definitions/legacy-javascript.js';
import metricsDebugger from './test-definitions/metrics-debugger.js';
import metricsDelayedFcp from './test-definitions/metrics-delayed-fcp.js';
import metricsDelayedLcp from './test-definitions/metrics-delayed-lcp.js';
import metricsTrickyTti from './test-definitions/metrics-tricky-tti.js';
import metricsTrickyTtiLateFcp from './test-definitions/metrics-tricky-tti-late-fcp.js';
import offlineOnlineOnly from './test-definitions/offline-online-only.js';
import offlineReady from './test-definitions/offline-ready.js';
import offlineSwBroken from './test-definitions/offline-sw-broken.js';
import offlineSwSlow from './test-definitions/offline-sw-slow.js';
import oopifRequests from './test-definitions/oopif-requests.js';
import oopifScripts from './test-definitions/oopif-scripts.js';
import perfBudgets from './test-definitions/perf-budgets.js';
import perfDebug from './test-definitions/perf-debug.js';
import perfDiagnosticsAnimations from './test-definitions/perf-diagnostics-animations.js';
import perfDiagnosticsThirdParty from './test-definitions/perf-diagnostics-third-party.js';
import perfDiagnosticsUnsizedImages from './test-definitions/perf-diagnostics-unsized-images.js';
import perfFonts from './test-definitions/perf-fonts.js';
import perfFrameMetrics from './test-definitions/perf-frame-metrics.js';
import perfPreload from './test-definitions/perf-preload.js';
import perfTraceElements from './test-definitions/perf-trace-elements.js';
import pubads from './test-definitions/pubads.js';
import pwaAirhorner from './test-definitions/pwa-airhorner.js';
import pwaCaltrain from './test-definitions/pwa-caltrain.js';
import pwaChromestatus from './test-definitions/pwa-chromestatus.js';
import pwaRocks from './test-definitions/pwa-rocks.js';
import pwaSvgomg from './test-definitions/pwa-svgomg.js';
import redirectsClientPaintServer from './test-definitions/redirects-client-paint-server.js';
import redirectsHistoryPushState from './test-definitions/redirects-history-push-state.js';
import redirectsMultipleServer from './test-definitions/redirects-multiple-server.js';
import redirectsScripts from './test-definitions/redirects-scripts.js';
import redirectsSelf from './test-definitions/redirects-self.js';
import redirectsSingleClient from './test-definitions/redirects-single-client.js';
import redirectsSingleServer from './test-definitions/redirects-single-server.js';
import screenshot from './test-definitions/screenshot.js';
import seoFailing from './test-definitions/seo-failing.js';
import seoPassing from './test-definitions/seo-passing.js';
import seoStatus403 from './test-definitions/seo-status-403.js';
import seoTapTargets from './test-definitions/seo-tap-targets.js';
import sourceMaps from './test-definitions/source-maps.js';
import timing from './test-definitions/timing.js';
/** @type {ReadonlyArray<Smokehouse.TestDfn>} */
const smokeTests = [
a11y,
byteEfficiency,
byteGzip,
cspAllowAll,
cspBlockAll,
dbw,
errorsExpiredSsl,
errorsIframeExpiredSsl,
errorsInfiniteLoop,
formsAutoComplete,
fpsMax,
fpsScaled,
fpsMaxPassive,
issuesMixedContent,
lanternFetch,
lanternIdleCallbackLong,
lanternIdleCallbackShort,
lanternOnline,
lanternSetTimeout,
lanternXhr,
legacyJavascript,
metricsDebugger,
metricsDelayedFcp,
metricsDelayedLcp,
metricsTrickyTti,
metricsTrickyTtiLateFcp,
offlineOnlineOnly,
offlineReady,
offlineSwBroken,
offlineSwSlow,
oopifRequests,
oopifScripts,
perfBudgets,
perfDebug,
perfDiagnosticsAnimations,
perfDiagnosticsThirdParty,
perfDiagnosticsUnsizedImages,
perfFonts,
perfFrameMetrics,
perfPreload,
perfTraceElements,
pubads,
pwaAirhorner,
pwaCaltrain,
pwaChromestatus,
pwaRocks,
pwaSvgomg,
redirectsClientPaintServer,
redirectsHistoryPushState,
redirectsMultipleServer,
redirectsScripts,
redirectsSelf,
redirectsSingleClient,
redirectsSingleServer,
screenshot,
seoFailing,
seoPassing,
seoStatus403,
seoTapTargets,
sourceMaps,
timing,
];
export default smokeTests;

View File

@@ -0,0 +1,16 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* COMPAT: update from the old TestDefn format (array of `expectations` per
* definition) to the new format (single `expectations` per def), doing our best
* generating some unique IDs.
* TODO: remove in Lighthouse 9+ once PubAds (and others?) are updated.
* @see https://github.com/GoogleChrome/lighthouse/issues/11950
* @param {ReadonlyArray<Smokehouse.BackCompatTestDefn>} allTestDefns
* @return {Array<Smokehouse.TestDfn>}
*/
export function updateTestDefnFormat(allTestDefns: ReadonlyArray<Smokehouse.BackCompatTestDefn>): Array<Smokehouse.TestDfn>;
//# sourceMappingURL=back-compat-util.d.ts.map

View File

@@ -0,0 +1,41 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* COMPAT: update from the old TestDefn format (array of `expectations` per
* definition) to the new format (single `expectations` per def), doing our best
* generating some unique IDs.
* TODO: remove in Lighthouse 9+ once PubAds (and others?) are updated.
* @see https://github.com/GoogleChrome/lighthouse/issues/11950
* @param {ReadonlyArray<Smokehouse.BackCompatTestDefn>} allTestDefns
* @return {Array<Smokehouse.TestDfn>}
*/
function updateTestDefnFormat(allTestDefns) {
const expandedTestDefns = allTestDefns.map(testDefn => {
if (Array.isArray(testDefn.expectations)) {
// Create a testDefn per expectation.
return testDefn.expectations.map((expectations, index) => {
return {
...testDefn,
id: `${testDefn.id}-${index}`,
expectations,
};
});
} else {
// New object to make tsc happy.
return {
...testDefn,
expectations: testDefn.expectations,
};
}
});
return expandedTestDefns.flat();
}
export {
updateTestDefnFormat,
};

View File

@@ -0,0 +1,8 @@
/**
* @param {Smokehouse.SmokehouseLibOptions} options
*/
export function smokehouse(options: Smokehouse.SmokehouseLibOptions): Promise<{
success: boolean;
testResults: import("../smokehouse.js").SmokehouseResult[];
}>;
//# sourceMappingURL=lib.d.ts.map

View File

@@ -0,0 +1,48 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Smoke test runner.
* Used to test integrations that run Lighthouse within a browser (i.e. LR, DevTools)
* Supports skipping and modifiying expectations to match the environment.
*/
/* eslint-disable no-console */
import cloneDeep from 'lodash/cloneDeep.js';
import smokeTests from '../core-tests.js';
import {runSmokehouse, getShardedDefinitions} from '../smokehouse.js';
/**
* @param {Smokehouse.SmokehouseLibOptions} options
*/
async function smokehouse(options) {
const {urlFilterRegex, skip, modify, shardArg, ...smokehouseOptions} = options;
const clonedTests = cloneDeep(smokeTests);
const modifiedTests = [];
for (const test of clonedTests) {
if (urlFilterRegex && !test.expectations.lhr.requestedUrl.match(urlFilterRegex)) {
continue;
}
const reasonToSkip = skip && skip(test, test.expectations);
if (reasonToSkip) {
console.log(`skipping ${test.expectations.lhr.requestedUrl}: ${reasonToSkip}`);
continue;
}
modify && modify(test, test.expectations);
modifiedTests.push(test);
}
const shardedTests = getShardedDefinitions(modifiedTests, shardArg);
return runSmokehouse(shardedTests, smokehouseOptions);
}
export {smokehouse};

View File

@@ -0,0 +1,2 @@
export * from "../smokehouse.js";
//# sourceMappingURL=node.d.ts.map

View File

@@ -0,0 +1,12 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview A smokehouse frontend for running within a node process.
*/
// Smokehouse is runnable from within node, so just a no-op for now.
export * from '../smokehouse.js';

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
export {};
//# sourceMappingURL=smokehouse-bin.d.ts.map

View File

@@ -0,0 +1,264 @@
#!/usr/bin/env node
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview A smokehouse frontend for running from the command line. Parse
* flags, start fixture webservers, then run smokehouse.
*/
/* eslint-disable no-console */
import path from 'path';
import fs from 'fs';
import url from 'url';
import cloneDeep from 'lodash/cloneDeep.js';
import yargs from 'yargs';
import * as yargsHelpers from 'yargs/helpers';
import log from 'lighthouse-logger';
import {runSmokehouse, getShardedDefinitions} from '../smokehouse.js';
import {updateTestDefnFormat} from './back-compat-util.js';
import {LH_ROOT} from '../../../../root.js';
import exclusions from '../config/exclusions.js';
import {saveArtifacts} from '../../../../core/lib/asset-saver.js';
import {saveLhr} from '../../../../core/lib/asset-saver.js';
const coreTestDefnsPath =
path.join(LH_ROOT, 'cli/test/smokehouse/core-tests.js');
/**
* Possible Lighthouse runners. Loaded dynamically so e.g. a CLI run isn't
* contingent on having built all the bundles.
*/
const runnerPaths = {
cli: '../lighthouse-runners/cli.js',
bundle: '../lighthouse-runners/bundle.js',
devtools: '../lighthouse-runners/devtools.js',
};
/**
* Determine batches of smoketests to run, based on the `requestedIds`.
* @param {Array<Smokehouse.TestDfn>} allTestDefns
* @param {Array<string>} requestedIds
* @param {Set<string>} excludedTests
* @return {Array<Smokehouse.TestDfn>}
*/
function getDefinitionsToRun(allTestDefns, requestedIds, excludedTests) {
let smokes = [];
const usage = ` ${log.dim}yarn smoke ${allTestDefns.map(t => t.id).join(' ')}${log.reset}\n`;
if (requestedIds.length === 0) {
smokes = [...allTestDefns];
console.log('Running ALL smoketests. Equivalent to:');
console.log(usage);
} else {
smokes = allTestDefns.filter(test => {
// Include all tests that *include* requested id.
// e.g. a requested 'pwa' will match 'pwa-airhorner', 'pwa-caltrain', etc
return requestedIds.some(requestedId => test.id.includes(requestedId));
});
console.log(`Running ONLY smoketests for: ${smokes.map(t => t.id).join(' ')}\n`);
}
const unmatchedIds = requestedIds.filter(requestedId => {
return !allTestDefns.map(t => t.id).some(id => id.includes(requestedId));
});
if (unmatchedIds.length) {
console.log(log.redify(`Smoketests not found for: ${unmatchedIds.join(' ')}`));
console.log(`Check test exclusions (${[...excludedTests].join(' ')})\n`);
console.log(usage);
}
if (!smokes.length) {
throw new Error('no smoketest found to run');
}
return smokes;
}
/**
* Prune the `networkRequests` from the test expectations when `takeNetworkRequestUrls`
* is not defined. Custom servers may not have this method available in-process.
* Also asserts that any expectation with `networkRequests` is run serially. For core
* tests, we don't currently have a good way to map requests to test definitions if
* the tests are run in parallel.
* @param {Array<Smokehouse.TestDfn>} testDefns
* @param {Function|undefined} takeNetworkRequestUrls
* @return {Array<Smokehouse.TestDfn>}
*/
function pruneExpectedNetworkRequests(testDefns, takeNetworkRequestUrls) {
const pruneNetworkRequests = !takeNetworkRequestUrls;
const clonedDefns = cloneDeep(testDefns);
for (const {id, expectations, runSerially} of clonedDefns) {
if (!runSerially && expectations.networkRequests) {
throw new Error(`'${id}' must be set to 'runSerially: true' to assert 'networkRequests'`);
}
if (pruneNetworkRequests && expectations.networkRequests) {
// eslint-disable-next-line max-len
const msg = `'networkRequests' cannot be asserted in test '${id}'. They should only be asserted on tests from an in-process server`;
if (process.env.CI) {
// If we're in CI, we require any networkRequests expectations to be asserted.
throw new Error(msg);
}
console.warn(log.redify('Warning:'),
`${msg}. Pruning expectation: ${JSON.stringify(expectations.networkRequests)}`);
expectations.networkRequests = undefined;
}
}
return clonedDefns;
}
/**
* CLI entry point.
*/
async function begin() {
const y = yargs(yargsHelpers.hideBin(process.argv));
const rawArgv = y
.help('help')
.usage('node $0 [<options>] <test-ids>')
.example('node $0 -j=1 pwa seo', 'run pwa and seo tests serially')
.option('_', {
array: true,
type: 'string',
})
.options({
'debug': {
type: 'boolean',
default: false,
describe: 'Save test artifacts and output verbose logs',
},
'legacy-navigation': {
type: 'boolean',
default: false,
describe: 'Use the legacy navigation runner',
},
'jobs': {
type: 'number',
alias: 'j',
describe: 'Manually set the number of jobs to run at once. `1` runs all tests serially',
},
'retries': {
type: 'number',
describe: 'The number of times to retry failing tests before accepting. Defaults to 0',
},
'runner': {
default: 'cli',
choices: ['cli', 'bundle', 'devtools'],
describe: 'The method of running Lighthouse',
},
'tests-path': {
type: 'string',
describe: 'The path to a set of test definitions to run. Defaults to core smoke tests.',
},
'shard': {
type: 'string',
// eslint-disable-next-line max-len
describe: 'A argument of the form "n/d", which divides the selected tests into d groups and runs the nth group. n and d must be positive integers with 1 ≤ n ≤ d.',
},
'ignore-exclusions': {
type: 'boolean',
default: false,
describe: 'Ignore any smoke test exclusions set.',
},
})
.wrap(y.terminalWidth())
.argv;
// Augmenting yargs type with auto-camelCasing breaks in tsc@4.1.2 and @types/yargs@15.0.11,
// so for now cast to add yarg's camelCase properties to type.
const argv =
/** @type {Awaited<typeof rawArgv> & LH.Util.CamelCasify<Awaited<typeof rawArgv>>} */ (rawArgv);
const jobs = Number.isFinite(argv.jobs) ? argv.jobs : undefined;
const retries = Number.isFinite(argv.retries) ? argv.retries : undefined;
const runnerPath = runnerPaths[/** @type {keyof typeof runnerPaths} */ (argv.runner)];
if (argv.runner === 'bundle') {
console.log('\n✨ Be sure to have recently run this: yarn build-all');
}
const {runLighthouse, setup} = await import(runnerPath);
runLighthouse.runnerName = argv.runner;
// Find test definition file and filter by requestedTestIds.
let testDefnPath = argv.testsPath || coreTestDefnsPath;
testDefnPath = path.resolve(process.cwd(), testDefnPath);
const requestedTestIds = argv._;
const {default: rawTestDefns} = await import(url.pathToFileURL(testDefnPath).href);
const allTestDefns = updateTestDefnFormat(rawTestDefns);
const excludedTests = new Set(exclusions[argv.runner] || []);
const filteredTestDefns = argv.ignoreExclusions ?
allTestDefns : allTestDefns.filter(test => !excludedTests.has(test.id));
const requestedTestDefns = getDefinitionsToRun(filteredTestDefns,
requestedTestIds, excludedTests);
const testDefns = getShardedDefinitions(requestedTestDefns, argv.shard);
let smokehouseResult;
let servers;
let takeNetworkRequestUrls = undefined;
try {
// If running the core tests, spin up the test server.
if (testDefnPath === coreTestDefnsPath) {
const {createServers} = await import('../../fixtures/static-server.js');
servers = await createServers();
takeNetworkRequestUrls = servers[0].takeRequestUrls.bind(servers[0]);
}
const prunedTestDefns = pruneExpectedNetworkRequests(testDefns, takeNetworkRequestUrls);
const options = {
jobs,
retries,
isDebug: argv.debug,
useLegacyNavigation: argv.legacyNavigation,
lighthouseRunner: runLighthouse,
takeNetworkRequestUrls,
setup,
};
smokehouseResult = (await runSmokehouse(prunedTestDefns, options));
} finally {
servers?.forEach(s => s.close());
}
if (!smokehouseResult.success) {
const failedTestResults = smokehouseResult.testResults.filter(r => r.failed);
// Save failed runs to directory. In CI, this is uploaded as an artifact.
const failuresDir = `${LH_ROOT}/.tmp/smokehouse-failures`;
fs.rmSync(failuresDir, {recursive: true, force: true});
fs.mkdirSync(failuresDir);
for (const testResult of failedTestResults) {
for (let i = 0; i < testResult.runs.length; i++) {
const runDir = `${failuresDir}/${i}/${testResult.id}`;
fs.mkdirSync(runDir, {recursive: true});
const run = testResult.runs[i];
await saveArtifacts(run.artifacts, runDir);
await saveLhr(run.lhr, runDir);
fs.writeFileSync(`${runDir}/assertionLog.txt`, run.assertionLog);
fs.writeFileSync(`${runDir}/lighthouseLog.txt`, run.lighthouseLog);
if (run.networkRequests) {
fs.writeFileSync(`${runDir}/networkRequests.txt`, run.networkRequests.join('\n'));
}
}
}
const cmd = `yarn smoke ${failedTestResults.map(r => r.id).join(' ')}`;
console.log(`rerun failures: ${cmd}`);
}
const exitCode = smokehouseResult.success ? 0 : 1;
process.exit(exitCode);
}
await begin();

View File

@@ -0,0 +1,21 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* An extension of Error that includes any stdout or stderr from a child
* process. Based on the error thrown by `child_process.exec()`.
* https://github.com/nodejs/node/blob/3aeae8d81b7b78668c37f7a07a72d94781126d49/lib/child_process.js#L150-L176
*/
export class ChildProcessError extends Error {
/**
* @param {string} message
* @param {string=} stdout
* @param {string=} stderr
*/
constructor(message: string, stdout?: string | undefined, stderr?: string | undefined);
stdout: string;
stderr: string;
}
//# sourceMappingURL=child-process-error.d.ts.map

View File

@@ -0,0 +1,25 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* An extension of Error that includes any stdout or stderr from a child
* process. Based on the error thrown by `child_process.exec()`.
* https://github.com/nodejs/node/blob/3aeae8d81b7b78668c37f7a07a72d94781126d49/lib/child_process.js#L150-L176
*/
class ChildProcessError extends Error {
/**
* @param {string} message
* @param {string=} stdout
* @param {string=} stderr
*/
constructor(message, stdout = '', stderr = '') {
super(message);
this.stdout = stdout;
this.stderr = stderr;
}
}
export {ChildProcessError};

View File

@@ -0,0 +1,78 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* A class that maintains a concurrency pool to coordinate many jobs that should
* only be run `concurrencyLimit` at a time.
* API inspired by http://bluebirdjs.com/docs/api/promise.map.html, but
* independent callers of `concurrentMap()` share the same concurrency limit.
*/
export class ConcurrentMapper {
/**
* Runs callbackfn on `values` in parallel, at a max of `concurrency` at a
* time. Resolves to an array of the results or rejects with the first
* rejected result. Default `concurrency` limit is `Infinity`.
* @template T, U
* @param {Array<T>} values
* @param {(value: T, index: number, array: Array<T>) => Promise<U>} callbackfn
* @param {{concurrency: number}} [options]
* @return {Promise<Array<U>>}
*/
static map<T_1, U_2>(values: T_1[], callbackfn: (value: T_1, index: number, array: T_1[]) => Promise<U_2>, options?: {
concurrency: number;
} | undefined): Promise<U_2[]>;
/** @type {Set<Promise<unknown>>} */
_promisePool: Set<Promise<unknown>>;
/**
* The limits of all currently running jobs. There will be duplicates.
* @type {Array<number>}
*/
_allConcurrencyLimits: Array<number>;
/**
* Returns whether there are fewer running jobs than the minimum current
* concurrency limit and the proposed new `concurrencyLimit`.
* @param {number} concurrencyLimit
*/
_canRunMoreAtLimit(concurrencyLimit: number): boolean;
/**
* Add a job to pool.
* @param {Promise<unknown>} job
* @param {number} concurrencyLimit
*/
_addJob(job: Promise<unknown>, concurrencyLimit: number): void;
/**
* Remove a job from pool.
* @param {Promise<unknown>} job
* @param {number} concurrencyLimit
*/
_removeJob(job: Promise<unknown>, concurrencyLimit: number): void;
/**
* Runs callbackfn on `values` in parallel, at a max of `concurrency` at
* a time across all callers on this instance. Resolves to an array of the
* results (for each caller separately) or rejects with the first rejected
* result. Default `concurrency` limit is `Infinity`.
* @template T, U
* @param {Array<T>} values
* @param {(value: T, index: number, array: Array<T>) => Promise<U>} callbackfn
* @param {{concurrency: number}} [options]
* @return {Promise<Array<U>>}
*/
pooledMap<T, U>(values: T[], callbackfn: (value: T, index: number, array: T[]) => Promise<U>, options?: {
concurrency: number;
} | undefined): Promise<U[]>;
/**
* Runs `fn` concurrent to other operations in the pool, at a max of
* `concurrency` at a time across all callers on this instance. Default
* `concurrency` limit is `Infinity`.
* @template U
* @param {() => Promise<U>} fn
* @param {{concurrency: number}} [options]
* @return {Promise<U>}
*/
runInPool<U_1>(fn: () => Promise<U_1>, options?: {
concurrency: number;
} | undefined): Promise<U_1>;
}
//# sourceMappingURL=concurrent-mapper.d.ts.map

View File

@@ -0,0 +1,125 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* A class that maintains a concurrency pool to coordinate many jobs that should
* only be run `concurrencyLimit` at a time.
* API inspired by http://bluebirdjs.com/docs/api/promise.map.html, but
* independent callers of `concurrentMap()` share the same concurrency limit.
*/
class ConcurrentMapper {
constructor() {
/** @type {Set<Promise<unknown>>} */
this._promisePool = new Set();
/**
* The limits of all currently running jobs. There will be duplicates.
* @type {Array<number>}
*/
this._allConcurrencyLimits = [];
}
/**
* Runs callbackfn on `values` in parallel, at a max of `concurrency` at a
* time. Resolves to an array of the results or rejects with the first
* rejected result. Default `concurrency` limit is `Infinity`.
* @template T, U
* @param {Array<T>} values
* @param {(value: T, index: number, array: Array<T>) => Promise<U>} callbackfn
* @param {{concurrency: number}} [options]
* @return {Promise<Array<U>>}
*/
static async map(values, callbackfn, options) {
const cm = new ConcurrentMapper();
return cm.pooledMap(values, callbackfn, options);
}
/**
* Returns whether there are fewer running jobs than the minimum current
* concurrency limit and the proposed new `concurrencyLimit`.
* @param {number} concurrencyLimit
*/
_canRunMoreAtLimit(concurrencyLimit) {
return this._promisePool.size < concurrencyLimit &&
this._promisePool.size < Math.min(...this._allConcurrencyLimits);
}
/**
* Add a job to pool.
* @param {Promise<unknown>} job
* @param {number} concurrencyLimit
*/
_addJob(job, concurrencyLimit) {
this._promisePool.add(job);
this._allConcurrencyLimits.push(concurrencyLimit);
}
/**
* Remove a job from pool.
* @param {Promise<unknown>} job
* @param {number} concurrencyLimit
*/
_removeJob(job, concurrencyLimit) {
this._promisePool.delete(job);
const limitIndex = this._allConcurrencyLimits.indexOf(concurrencyLimit);
if (limitIndex === -1) {
throw new Error('No current limit found for finishing job');
}
this._allConcurrencyLimits.splice(limitIndex, 1);
}
/**
* Runs callbackfn on `values` in parallel, at a max of `concurrency` at
* a time across all callers on this instance. Resolves to an array of the
* results (for each caller separately) or rejects with the first rejected
* result. Default `concurrency` limit is `Infinity`.
* @template T, U
* @param {Array<T>} values
* @param {(value: T, index: number, array: Array<T>) => Promise<U>} callbackfn
* @param {{concurrency: number}} [options]
* @return {Promise<Array<U>>}
*/
async pooledMap(values, callbackfn, options = {concurrency: Infinity}) {
const {concurrency} = options;
const result = [];
for (let i = 0; i < values.length; i++) {
// Wait until concurrency allows another run.
while (!this._canRunMoreAtLimit(concurrency)) {
// Unconditionally catch since we only care about our own failures
// (caught in the Promise.all below), not other callers.
await Promise.race(this._promisePool).catch(() => {});
}
// innerPromise removes itself from the pool and resolves on return from callback.
const innerPromise = callbackfn(values[i], i, values)
.finally(() => this._removeJob(innerPromise, concurrency));
this._addJob(innerPromise, concurrency);
result.push(innerPromise);
}
return Promise.all(result);
}
/**
* Runs `fn` concurrent to other operations in the pool, at a max of
* `concurrency` at a time across all callers on this instance. Default
* `concurrency` limit is `Infinity`.
* @template U
* @param {() => Promise<U>} fn
* @param {{concurrency: number}} [options]
* @return {Promise<U>}
*/
async runInPool(fn, options = {concurrency: Infinity}) {
// Let pooledMap handle the pool management for the cost of boxing a fake `value`.
const result = await this.pooledMap([''], fn, options);
return result[0];
}
}
export {ConcurrentMapper};

View File

@@ -0,0 +1,33 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* A simple buffered log to use in place of `console`.
*/
export class LocalConsole {
_log: string;
/**
* @param {string} str
*/
log(str: string): void;
/**
* Log but without the ending newline.
* @param {string} str
*/
write(str: string): void;
/**
* @return {string}
*/
getLog(): string;
/**
* Append a stdout and stderr to this log.
* @param {{stdout: string, stderr: string}} stdStrings
*/
adoptStdStrings(stdStrings: {
stdout: string;
stderr: string;
}): void;
}
//# sourceMappingURL=local-console.d.ts.map

View File

@@ -0,0 +1,50 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* A simple buffered log to use in place of `console`.
*/
class LocalConsole {
constructor() {
this._log = '';
}
/**
* @param {string} str
*/
log(str) {
this._log += str + '\n';
}
/**
* Log but without the ending newline.
* @param {string} str
*/
write(str) {
this._log += str;
}
/**
* @return {string}
*/
getLog() {
return this._log;
}
/**
* Append a stdout and stderr to this log.
* @param {{stdout: string, stderr: string}} stdStrings
*/
adoptStdStrings(stdStrings) {
this.write(stdStrings.stdout);
// stderr accrues many empty lines. Don't log unless there's content.
if (/\S/.test(stdStrings.stderr)) {
this.write(stdStrings.stderr);
}
}
}
export {LocalConsole};

View File

@@ -0,0 +1,16 @@
/**
* Launch Chrome and do a full Lighthouse run via the Lighthouse DevTools bundle.
* @param {string} url
* @param {LH.Config=} config
* @param {{isDebug?: boolean, useLegacyNavigation?: boolean}=} testRunnerOptions
* @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts, log: string}>}
*/
export function runLighthouse(url: string, config?: LH.Config | undefined, testRunnerOptions?: {
isDebug?: boolean;
useLegacyNavigation?: boolean;
} | undefined): Promise<{
lhr: LH.Result;
artifacts: LH.Artifacts;
log: string;
}>;
//# sourceMappingURL=bundle.d.ts.map

View File

@@ -0,0 +1,156 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview A runner that launches Chrome and executes Lighthouse via a
* bundle to test that bundling has produced correct and runnable code.
* Currently uses `lighthouse-dt-bundle.js`.
* Runs in a worker to avoid messing up marky's global state.
*/
import fs from 'fs';
import os from 'os';
import {Worker, isMainThread, parentPort, workerData} from 'worker_threads';
import {once} from 'events';
import puppeteer from 'puppeteer-core';
import ChromeLauncher from 'chrome-launcher';
import {CriConnection} from '../../../../core/legacy/gather/connections/cri.js';
import {LH_ROOT} from '../../../../root.js';
import {loadArtifacts, saveArtifacts} from '../../../../core/lib/asset-saver.js';
// This runs only in the worker. The rest runs on the main thread.
if (!isMainThread && parentPort) {
(async () => {
const {url, config, testRunnerOptions} = workerData;
try {
const result = await runBundledLighthouse(url, config, testRunnerOptions);
// Save to assets directory because LighthouseError won't survive postMessage.
const assetsDir = fs.mkdtempSync(os.tmpdir() + '/smoke-bundle-assets-');
await saveArtifacts(result.artifacts, assetsDir);
const value = {
lhr: result.lhr,
assetsDir,
};
parentPort?.postMessage({type: 'result', value});
} catch (err) {
console.error(err);
parentPort?.postMessage({type: 'error', value: err});
}
})();
}
/**
* @param {string} url
* @param {LH.Config|undefined} config
* @param {{isDebug?: boolean, useLegacyNavigation?: boolean}} testRunnerOptions
* @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts}>}
*/
async function runBundledLighthouse(url, config, testRunnerOptions) {
if (isMainThread || !parentPort) {
throw new Error('must be called in worker');
}
const originalBuffer = global.Buffer;
const originalRequire = global.require;
if (typeof globalThis === 'undefined') {
// @ts-expect-error - exposing for loading of dt-bundle.
global.globalThis = global;
}
// Load bundle, which creates a `global.runBundledLighthouse`.
await import(LH_ROOT + '/dist/lighthouse-dt-bundle.js');
global.require = originalRequire;
global.Buffer = originalBuffer;
/** @type {import('../../../../core/index.js')['default']} */
// @ts-expect-error - not worth giving test global an actual type.
const lighthouse = global.runBundledLighthouse;
/** @type {import('../../../../core/index.js')['legacyNavigation']} */
// @ts-expect-error - not worth giving test global an actual type.
const legacyNavigation = global.runBundledLighthouseLegacyNavigation;
// Launch and connect to Chrome.
const launchedChrome = await ChromeLauncher.launch();
const port = launchedChrome.port;
// Run Lighthouse.
try {
const logLevel = testRunnerOptions.isDebug ? 'verbose' : 'info';
let runnerResult;
if (testRunnerOptions.useLegacyNavigation) {
const connection = new CriConnection(port);
runnerResult =
await legacyNavigation(url, {port, logLevel}, config, connection);
} else {
// Puppeteer is not included in the bundle, we must create the page here.
const browser = await puppeteer.connect({browserURL: `http://localhost:${port}`});
const page = await browser.newPage();
runnerResult = await lighthouse(url, {port, logLevel}, config, page);
}
if (!runnerResult) throw new Error('No runnerResult');
return {
lhr: runnerResult.lhr,
artifacts: runnerResult.artifacts,
};
} finally {
// Clean up and return results.
await launchedChrome.kill();
}
}
/**
* Launch Chrome and do a full Lighthouse run via the Lighthouse DevTools bundle.
* @param {string} url
* @param {LH.Config=} config
* @param {{isDebug?: boolean, useLegacyNavigation?: boolean}=} testRunnerOptions
* @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts, log: string}>}
*/
async function runLighthouse(url, config, testRunnerOptions = {}) {
/** @type {string[]} */
const logs = [];
const worker = new Worker(new URL(import.meta.url), {
stdout: true,
stderr: true,
workerData: {url, config, testRunnerOptions},
});
worker.stdout.setEncoding('utf8');
worker.stderr.setEncoding('utf8');
worker.stdout.addListener('data', (data) => {
logs.push(`[STDOUT] ${data}`);
});
worker.stderr.addListener('data', (data) => {
logs.push(`[STDERR] ${data}`);
});
const [workerResponse] = await once(worker, 'message');
const log = logs.join('') + '\n';
if (workerResponse.type === 'error') {
new Error(`Worker returned an error: ${workerResponse.value}\nLog:\n${log}`);
}
const result = workerResponse.value;
if (!result.lhr || !result.assetsDir) {
throw new Error(`invalid response from worker:\n${JSON.stringify(result, null, 2)}`);
}
const artifacts = loadArtifacts(result.assetsDir);
fs.rmSync(result.assetsDir, {recursive: true});
return {
lhr: result.lhr,
artifacts,
log,
};
}
export {
runLighthouse,
};

View File

@@ -0,0 +1,18 @@
/// <reference path="../../../../../../types/internal/lighthouse-logger.d.ts" />
/**
* Launch Chrome and do a full Lighthouse run via the Lighthouse CLI.
* @param {string} url
* @param {LH.Config=} config
* @param {{isDebug?: boolean, useFraggleRock?: boolean}=} testRunnerOptions
* @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts, log: string}>}
*/
export function runLighthouse(url: string, config?: LH.Config | undefined, testRunnerOptions?: {
isDebug?: boolean | undefined;
useFraggleRock?: boolean | undefined;
} | undefined): Promise<{
lhr: LH.Result;
artifacts: LH.Artifacts;
log: string;
}>;
import log from 'lighthouse-logger';
//# sourceMappingURL=cli.d.ts.map

View File

@@ -0,0 +1,134 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview A runner that executes Lighthouse via the Lighthouse CLI to
* test the full pipeline, from parsing arguments on the command line to writing
* results to disk. When complete, reads back the artifacts and LHR and returns
* them.
*/
import {promises as fs} from 'fs';
import {promisify} from 'util';
import {execFile} from 'child_process';
import log from 'lighthouse-logger';
import * as assetSaver from '../../../../core/lib/asset-saver.js';
import {LocalConsole} from '../lib/local-console.js';
import {ChildProcessError} from '../lib/child-process-error.js';
import {LH_ROOT} from '../../../../root.js';
const execFileAsync = promisify(execFile);
/**
* Launch Chrome and do a full Lighthouse run via the Lighthouse CLI.
* @param {string} url
* @param {LH.Config=} config
* @param {{isDebug?: boolean, useFraggleRock?: boolean}=} testRunnerOptions
* @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts, log: string}>}
*/
async function runLighthouse(url, config, testRunnerOptions = {}) {
const {isDebug} = testRunnerOptions;
const tmpDir = `${LH_ROOT}/.tmp/smokehouse`;
await fs.mkdir(tmpDir, {recursive: true});
const tmpPath = await fs.mkdtemp(`${tmpDir}/smokehouse-`);
return internalRun(url, tmpPath, config, testRunnerOptions)
// Wait for internalRun() before removing scratch directory.
.finally(() => !isDebug && fs.rm(tmpPath, {recursive: true, force: true}));
}
/**
* Internal runner.
* @param {string} url
* @param {string} tmpPath
* @param {LH.Config=} config
* @param {{isDebug?: boolean, useLegacyNavigation?: boolean}=} options
* @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts, log: string}>}
*/
async function internalRun(url, tmpPath, config, options) {
const {isDebug = false, useLegacyNavigation = false} = options || {};
const localConsole = new LocalConsole();
const outputPath = `${tmpPath}/smokehouse.report.json`;
const artifactsDirectory = `${tmpPath}/artifacts/`;
const args = [
`${LH_ROOT}/cli/index.js`,
`${url}`,
`--output-path=${outputPath}`,
'--output=json',
`-G=${artifactsDirectory}`,
`-A=${artifactsDirectory}`,
'--port=0',
'--quiet',
];
if (useLegacyNavigation) {
args.push('--legacy-navigation');
}
// Config can be optionally provided.
if (config) {
const configPath = `${tmpPath}/config.json`;
await fs.writeFile(configPath, JSON.stringify(config));
args.push(`--config-path=${configPath}`);
}
const command = 'node';
const env = {...process.env, NODE_ENV: 'test'};
localConsole.log(`${log.dim}$ ${command} ${args.join(' ')} ${log.reset}`);
/** @type {{stdout: string, stderr: string, code?: number}} */
let execResult;
try {
execResult = await execFileAsync(command, args, {env});
} catch (e) {
// exec-thrown errors have stdout, stderr, and exit code from child process.
execResult = e;
}
const exitCode = execResult.code || 0;
if (isDebug) {
localConsole.log(`exit code ${exitCode}`);
localConsole.log(`STDOUT: ${execResult.stdout}`);
localConsole.log(`STDERR: ${execResult.stderr}`);
}
try {
await fs.access(outputPath);
} catch (e) {
throw new ChildProcessError(`Lighthouse run failed to produce a report and exited with ${exitCode}.`, // eslint-disable-line max-len
localConsole.getLog());
}
/** @type {LH.Result} */
const lhr = JSON.parse(await fs.readFile(outputPath, 'utf8'));
const artifacts = assetSaver.loadArtifacts(artifactsDirectory);
// Output has been established as existing, so can log for debug.
if (isDebug) {
localConsole.log(`LHR output available at: ${outputPath}`);
localConsole.log(`Artifacts avaiable in: ${artifactsDirectory}`);
}
// There should either be both an error exitCode and a lhr.runtimeError or neither.
if (Boolean(exitCode) !== Boolean(lhr.runtimeError)) {
const runtimeErrorCode = lhr.runtimeError?.code;
throw new ChildProcessError(`Lighthouse did not exit with an error correctly, exiting with ${exitCode} but with runtimeError '${runtimeErrorCode}'`, // eslint-disable-line max-len
localConsole.getLog());
}
return {
lhr,
artifacts,
log: localConsole.getLog(),
};
}
export {
runLighthouse,
};

View File

@@ -0,0 +1,23 @@
/**
* Launch Chrome and do a full Lighthouse run via DevTools.
* By default, the latest DevTools frontend is used (.tmp/chromium-web-tests/devtools/devtools-frontend)
* unless DEVTOOLS_PATH is set.
* CHROME_PATH determines which Chrome is usedotherwise the default is puppeteer's chrome binary.
* @param {string} url
* @param {LH.Config=} config
* @param {{isDebug?: boolean, useLegacyNavigation?: boolean}=} testRunnerOptions
* @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts, log: string}>}
*/
export function runLighthouse(url: string, config?: LH.Config | undefined, testRunnerOptions?: {
isDebug?: boolean;
useLegacyNavigation?: boolean;
} | undefined): Promise<{
lhr: LH.Result;
artifacts: LH.Artifacts;
log: string;
}>;
/**
* Download/pull latest DevTools, build Lighthouse for DevTools, roll to DevTools, and build DevTools.
*/
export function setup(): Promise<void>;
//# sourceMappingURL=devtools.d.ts.map

View File

@@ -0,0 +1,72 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview A runner that launches Chrome and executes Lighthouse via DevTools.
*/
import fs from 'fs';
import os from 'os';
import {execFileSync} from 'child_process';
import {LH_ROOT} from '../../../../root.js';
import {testUrlFromDevtools} from '../../../../core/scripts/pptr-run-devtools.js';
const devtoolsDir =
process.env.DEVTOOLS_PATH || `${LH_ROOT}/.tmp/chromium-web-tests/devtools/devtools-frontend`;
/**
* Download/pull latest DevTools, build Lighthouse for DevTools, roll to DevTools, and build DevTools.
*/
async function setup() {
if (process.env.CI) return;
process.env.DEVTOOLS_PATH = devtoolsDir;
execFileSync('bash',
['core/test/devtools-tests/download-devtools.sh'],
{stdio: 'inherit'}
);
execFileSync('bash',
['core/test/devtools-tests/roll-devtools.sh'],
{stdio: 'inherit'}
);
}
/**
* Launch Chrome and do a full Lighthouse run via DevTools.
* By default, the latest DevTools frontend is used (.tmp/chromium-web-tests/devtools/devtools-frontend)
* unless DEVTOOLS_PATH is set.
* CHROME_PATH determines which Chrome is usedotherwise the default is puppeteer's chrome binary.
* @param {string} url
* @param {LH.Config=} config
* @param {{isDebug?: boolean, useLegacyNavigation?: boolean}=} testRunnerOptions
* @return {Promise<{lhr: LH.Result, artifacts: LH.Artifacts, log: string}>}
*/
async function runLighthouse(url, config, testRunnerOptions = {}) {
const chromeFlags = [
`--custom-devtools-frontend=file://${devtoolsDir}/out/LighthouseIntegration/gen/front_end`,
];
const {lhr, artifacts, logs} = await testUrlFromDevtools(url, {
config,
chromeFlags,
useLegacyNavigation: testRunnerOptions.useLegacyNavigation,
});
if (testRunnerOptions.isDebug) {
const outputDir = fs.mkdtempSync(os.tmpdir() + '/lh-smoke-cdt-runner-');
fs.writeFileSync(`${outputDir}/lhr.json`, JSON.stringify(lhr));
fs.writeFileSync(`${outputDir}/artifacts.json`, JSON.stringify(artifacts));
console.log(`${url} results saved at ${outputDir}`);
}
const log = logs.join('') + '\n';
return {lhr, artifacts, log};
}
export {
runLighthouse,
setup,
};

208
node_modules/lighthouse/cli/test/smokehouse/readme.md generated vendored Normal file
View File

@@ -0,0 +1,208 @@
# Smokehouse
Smokehouse is the Lighthouse end-to-end/smoke test runner. It takes in a set of URLs (usually pointing to custom-built test sites), runs Lighthouse on them, and compares the results against a set of expectations.
By default this is done using the Lighthouse CLI (to exercise the full pipeline) with the tests listed in [`smokehouse/core-tests.js`](./core-tests.js).
## Options
See [`SmokehouseOptions`](https://github.com/GoogleChrome/lighthouse/blob/main/cli/test/smokehouse/smokehouse.js#L23).
## Test definitions
| Name | Type | Description |
| -------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | The string identifier of the test. |
| `expectations` | `{lhr: Object, artifacts: Object}` | See below. |
| `config` | `LH.Config` (optional) | An optional Lighthouse config. If not specified, the default config is used. |
| `runSerially` | `boolean` (optional) | An optional flag. If set to true, the test won't be run in parallel to other tests. Useful if the test is performance sensitive. |
### Expectations
The smoke test expectations can assert the values of the Lighthouse result (the `lhr`) and gathered `artifacts` for multiple URLs. The URL to be tested is specified in the expectations's `requestedUrl` field.
The expectations are asserted as a subset of the actual results: all values in the expectations must be in the actual results, but not all actual results must be asserted.
Examples can be found in the [core tests](./test-definitions/).
### Special numeric expectations
If checking a number somewhere in the Lighthouse results, numeric comparisons can be used in place of a raw expected number. This allows asserting ranges or categories of numbers where the exact value isn't necessarily important, or to allow for expected variability in a test.
The comparator is specified with a string, and the actual value being tested must be a number. Whitespace may be included in the string for readability.
The following operators are supported:
| Operator | Example |
| -------- | ------------ |
| `>` | `'>0'` |
| `>=` | `'>=5'` |
| `<` | `'<1'` |
| `<=` | `'<=10'` |
| `+/-` | `'100+/-10'` |
| `±` | `'100±10'` |
**Examples**:
| Actual | Expected | Result |
| -- | -- | -- |
| `{timeInMs: 50}` | `{timeInMs: '>0'}` | ✅ PASS |
| `{numericValue: 3969.135}` | `{numericValue: '1000±100'}` | ❌ FAIL |
### Special string expectations
If checking a string somewhere in the Lighthouse results, a regular expression can be used in place of a string literal.
**Examples**:
| Actual | Expected | Result |
| -- | -- | -- |
| `{displayValue: '4.0 s'}` | `{displayValue: /^\d+\.\d+/}` | ✅ PASS |
| `{url: 'http://example.com'}` | `{url: /^https/}` | ❌ FAIL |
### Special array expectations
Individual elements of an array can be asserted by using numeric properties in an object, e.g. asserting the third element in an array is 5: `{2: 5}`.
However, if an array literal is used as the expectation, an extra condition is enforced that the actual array _must_ have the same length as the provided expected array.
Arrays and objects can be checked against a subset of elements using the special `_includes` property. The value of `_includes` _must_ be an array. Each assertion in `_includes` will remove the matching item from consideration for the rest.
Arrays and objects can be asserted to not match any elements using the special `_excludes` property. The value of `_excludes` _must_ be an array. If an `_includes` check is defined before an `_excludes` check, only the element not matched under the previous will be considered.
If an object is checked using `_includes` or `_excludes`, it will be checked against the `Object.entries` array.
**Examples**:
| Actual | Expected | Result |
| -- | -- | -- |
| `[{url: 'http://badssl.com'}, {url: 'http://example.com'}]` | `{1: {url: 'http://example.com'}}` | ✅ PASS |
| `[{timeInMs: 5}, {timeInMs: 15}]` | `{length: 2}` | ✅ PASS |
| `[{timeInMs: 5}, {timeInMs: 15}]` | `{_includes: [{timeInMs: 5}]}` | ✅ PASS |
| `[{timeInMs: 5}, {timeInMs: 15}]` | `{_includes: [{timeInMs: 5}, {timeInMs: 5}]}` | ❌ FAIL |
| `[{timeInMs: 5}, {timeInMs: 15}]` | `{_includes: [{timeInMs: 5}], _excludes: [{timeInMs: 5}]}` | ✅ PASS |
| `[{timeInMs: 5}, {timeInMs: 15}]` | `{_includes: [{timeInMs: 5}], _excludes: [{timeInMs: 15}]}` | ❌ FAIL |
| `[{timeInMs: 5}, {timeInMs: 15}]` | `{_includes: [{timeInMs: 5}], _excludes: [{}]}` | ❌ FAIL |
| `[{timeInMs: 5}, {timeInMs: 15}]` | `[{timeInMs: 5}]` | ❌ FAIL |
| `{'foo': 1}` | `{_includes: [['foo', 1]]}` | ✅ PASS |
| `{'foo': 1, 'bar': 2}` | `{_includes: [['foo', 1]], _excludes: [['bar', 2]]}` | ❌ FAIL |
### Special environment checks
If an expectation requires a minimum version of Chromium, use `_minChromiumVersion: xx.x.x.x` to conditionally ignore that entire object in the expectation.
Can be as specific as you like (`_minChromiumVersion: xx` works too).
**Examples**:
```js
{
artifacts: {
InspectorIssues: {
// Mixed Content issues weren't added to the protocol until M84.
_minChromiumVersion: '84', // The entire `InspectorIssues` is ignored for older Chrome.
mixedContent: [
{
resourceType: 'Image',
resolutionStatus: 'MixedContentWarning',
insecureURL: 'http://www.mixedcontentexamples.com/Content/Test/steveholt.jpg',
mainResourceURL: 'https://www.mixedcontentexamples.com/Test/NonSecureImage',
request: {
url: 'http://www.mixedcontentexamples.com/Content/Test/steveholt.jpg',
},
},
],
},
TraceElements: {
// ... anything here won't be ignored
}
},
```
All pruning checks:
- `_minChromiumVersion`
- `_maxChromiumVersion`
- `_legacyOnly`
- `_fraggleRockOnly`
- `_runner` (set to same value provided to CLI --runner flag, ex: `'devtools'`)
- `_excludeRunner` (set to same value provided to CLI --runner flag, ex: `'devtools'`)
## Pipeline
The different frontends launch smokehouse with a set of tests to run. Smokehouse then coordinates the tests using a particular method of running Lighthouse (CLI, as a bundle, etc).
```
Smokehouse Frontends Lighthouse Runners
+------------+
| |
| bin.js +----+ +--------------+
| | | | |
+------------+ | +-->+ cli.js |
| | | |
+------------+ | +---------------+ | +--------------+
| | | testDefns> | | config> |
| node.js +---------------->+ smokehouse.js +<---------+
| | | | | <lhr | +--------------+
+------------+ | +-------+-------+ | | |
| ^ +-->+ bundle.js |
+------------+ | | | | |
| | | | | +--------------+
| lib.js +----+ v |
| | +--------+--------+ |
+------------+ | | | +--------------+
| report/assert | | | |
| | +-->+ devtools.js |
+-----------------+ | |
+--------------+
```
### Smokehouse frontends
- `frontends/smokehouse-bin.js` - runs smokehouse from the command line
- `lib` - configurable entrypoint to smokehouse, can be bundled to run in a browser environment
- `node.js` - run smokehouse from a node process
### Smokehouse
- `smokehouse.js` - takes a set of smoke-test definitions and runs them via a passed-in runner. Smokehouse is bundleable and can run in a browser as long as runner used is bundleable as well.
### Lighthouse runners
- `lighthouse-runners/cli.js` - the original test runner, exercising the Lighthouse CLI from command-line argument parsing to the results written to disk on completion.
- `lighthouse-runners/bundle.js` - a smoke test runner that operates on an already-bundled version of Lighthouse for end-to-end testing of that version.
- `lighthouse-runners/devtools.js` - a smoke test runner that operates on Lighthouse running from inside DevTools.
## Custom smoke tests (for plugins et al.)
Smokehouse comes with a core set of test definitions, but it can run any set of tests. Custom extensions of Lighthouse (like plugins) can provide their own tests and run them via the same infrastructure. For example:
- have a test site on a public URL or via a local server (e.g. `https://localhost:8080`)
- create a test definition (e.g. in `plugin-tests.js`)
```js
const smokeTests = [{
id: 'pluginTest',
expectations: require('./expectations.js'),
// config: ..., // If left out, uses default LH config
// runSerially: true, // If test is perf-sensitive
};
module.exports = smokeTests;
```
- create a test expectations file (e.g. `expectations.js`)
```js
const expectations = [{
lhr: {
requestedUrl: 'http://localhost:8080/index.html',
finalDisplayedUrl: 'http://localhost:8080/index.html',
audits: {
'preload-as': {
score: 1,
displayValue: /^Found 0 preload requests/,
},
},
},
};
module.exports = expectations;
```
- with `lighthouse` installed as a dependency/peer dependency, run
`yarn smokehouse --tests-path plugin-tests.js`
or
`npx --no-install smokehouse --tests-path plugin-tests.js`

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=report-assert-test.d.ts.map

View File

@@ -0,0 +1,298 @@
/**
* @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/* eslint-disable no-control-regex */
import {readJson} from '../../../core/test/test-utils.js';
import {findDifferences, getAssertionReport} from './report-assert.js';
describe('findDiffersences', () => {
const testCases = {
'works (trivial passing)': {
actual: {},
expected: {},
diffs: null,
},
'works (trivial fail)': {
actual: {},
expected: {a: 1},
diffs: [{path: '.a', actual: undefined, expected: 1}],
},
'works (trivial fail, actual undefined)': {
actual: undefined,
expected: {a: 1},
diffs: [{path: '', actual: undefined, expected: {a: 1}}],
},
'works (trivial fail, nested)': {
actual: {a: {b: 2}},
expected: {a: {b: 1}},
diffs: [{path: '.a.b', actual: 2, expected: 1}],
},
'works (trivial fail, nested actual undefined)': {
actual: {a: undefined},
expected: {a: {b: 1}},
diffs: [{path: '.a', actual: undefined, expected: {b: 1}}],
},
'works (multiple fail 1)': {
actual: {},
expected: {a: 1, b: 2},
diffs: [
{path: '.a', actual: undefined, expected: 1},
{path: '.b', actual: undefined, expected: 2},
],
},
'works (multiple fail 2)': {
actual: {nested: {array: [0, 1, 2]}},
expected: {nested: {array: [2, 1, 0]}},
diffs: [
{path: '.nested.array[0]', actual: 0, expected: 2},
{path: '.nested.array[2]', actual: 2, expected: 0},
],
},
'range (1)': {
actual: {duration: 100},
expected: {duration: '>=100'},
diffs: null,
},
'range (2)': {
actual: {},
expected: {duration: '>=100'},
diffs: [{path: '.duration', actual: undefined, expected: '>=100'}],
},
'range (3)': {
actual: {duration: 100},
expected: {duration: '>100'},
diffs: [{path: '.duration', actual: 100, expected: '>100'}],
},
'range (4)': {
actual: {duration: 100},
expected: {duration: '<100'},
diffs: [{path: '.duration', actual: 100, expected: '<100'}],
},
'array (1)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: {length: 6}},
diffs: null,
},
'array (2)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: {length: '>0'}},
diffs: null,
},
'array (3)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: [0, 1, 2, 3, 4, 5]},
diffs: null,
},
'array (4)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: [0, 1, 2, 3, 4, 5, 6]},
diffs: [
{path: '.prices[6]', actual: undefined, expected: 6},
{path: '.prices.length', actual: [0, 1, 2, 3, 4, 5], expected: [0, 1, 2, 3, 4, 5, 6]},
],
},
'array (5)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: []},
diffs: [{path: '.prices.length', actual: [0, 1, 2, 3, 4, 5], expected: []}],
},
'array (6)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: {'3': '>=3'}},
diffs: null,
},
'array (7)': {
actual: {prices: [0, 1, 2, {nested: 3}, 4, 5]},
expected: {prices: {'3': {nested: '>3'}}},
diffs: [{path: '.prices[3].nested', actual: 3, expected: '>3'}],
},
'_includes (1)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: {_includes: [4]}},
diffs: null,
},
'_includes (2)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: {_includes: [4, 4]}},
diffs: [{path: '.prices', actual: 'Item not found in array', expected: 4}],
},
'_includes (3)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: {_includes: [100]}},
diffs: [{path: '.prices', actual: 'Item not found in array', expected: 100}],
},
'_includes (4)': {
actual: {prices: ['0', '1', '2', '3', '4', '5']},
expected: {prices: {_includes: [/\d/, /\d/, /\d/, /\d/, /\d/, /\d/]}},
diffs: null,
},
'_includes (object)': {
actual: {'0-alpha': 1, '1-beta': 2, '3-gamma': 3},
expected: {_includes: [
['0-alpha', '<2'],
[/[0-9]-beta/, 2],
]},
diffs: null,
},
'_excludes (1)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: {_excludes: [100]}},
diffs: null,
},
'_excludes (2)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: {_excludes: [2]}},
diffs: [{path: '.prices', actual: 2, expected: {
expectedExclusion: 2,
message: 'Expected to not find matching entry via _excludes',
}}],
},
'_excludes (object)': {
actual: {'0-alpha': 1, '1-beta': 2, '3-gamma': 3},
expected: {_excludes: [
[/[0-9]-beta/, 2],
]},
diffs: [{path: '', actual: ['1-beta', 2], expected: {
expectedExclusion: [/[0-9]-beta/, 2],
message: 'Expected to not find matching entry via _excludes',
}}],
},
'_includes and _excludes (1)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: {_includes: [2], _excludes: [2]}},
diffs: null,
},
// Order matters.
'_includes and _excludes (2)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: {_excludes: [2], _includes: [2]}},
diffs: [{path: '.prices', actual: 2, expected: {
expectedExclusion: 2,
message: 'Expected to not find matching entry via _excludes',
}}],
},
'_includes and _excludes (3)': {
actual: {prices: [0, 1, 2, 3, 4, 5]},
expected: {prices: {_includes: [2], _excludes: [2, 1]}},
diffs: [{path: '.prices', actual: 1, expected: {
expectedExclusion: 1,
message: 'Expected to not find matching entry via _excludes',
}}],
},
'_includes and _excludes (object)': {
actual: {'0-alpha': 1, '1-beta': 2, '3-gamma': 3},
expected: {
_includes: [
['0-alpha', '<2'],
],
_excludes: [
[/[0-9]-alpha/, 1],
[/[0-9]-beta/, 2],
],
},
diffs: [{path: '', actual: ['1-beta', 2], expected: {
expectedExclusion: [/[0-9]-beta/, 2],
message: 'Expected to not find matching entry via _excludes',
}}],
},
};
for (const [testName, {actual, expected, diffs}] of Object.entries(testCases)) {
it(testName, () => {
expect(findDifferences('', actual, expected)).toEqual(diffs);
});
}
});
/**
* Removes ANSI codes.
* TODO: should make it so logger can disable these.
* @param {string} text
*/
function clean(text) {
return text
.replace(/\x1B.*?m/g, '')
.replace(/\x1b.*?m/g, '')
.replace(/[✘×]/g, 'X')
.trim();
}
describe('getAssertionReport', () => {
const lhr = readJson('core/test/results/sample_v2.json');
const artifacts = readJson('core/test/results/artifacts/artifacts.json');
it('works (trivial passing)', () => {
const report = getAssertionReport({lhr, artifacts}, {
lhr: {
audits: {},
requestedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
finalDisplayedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
},
});
expect(report).toMatchObject({passed: 3, failed: 0, log: ''});
});
it('works (trivial failing)', () => {
const report = getAssertionReport({lhr, artifacts}, {
lhr: {
audits: {
'cumulative-layout-shift': {
details: {
items: [],
},
},
},
requestedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
finalDisplayedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
},
});
expect(report).toMatchObject({passed: 3, failed: 1});
expect(clean(report.log)).toMatchSnapshot();
});
it('works (trivial failing, actual undefined)', () => {
const report = getAssertionReport({lhr, artifacts}, {
lhr: {
audits: {
'cumulative-layout-shift-no-exist': {
details: {
items: [],
},
},
},
requestedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
finalDisplayedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
},
});
expect(report).toMatchObject({passed: 3, failed: 1});
expect(clean(report.log)).toMatchSnapshot();
});
it('works (multiple failing)', () => {
const report = getAssertionReport({lhr, artifacts}, {
lhr: {
audits: {
'cumulative-layout-shift': {
details: {
items: [],
blah: 123,
},
},
},
requestedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
finalDisplayedUrl: 'http://localhost:10200/dobetterweb/dbw_tester.html',
},
});
expect(report).toMatchObject({passed: 3, failed: 1});
expect(clean(report.log)).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,50 @@
/// <reference path="../../../../../types/internal/lighthouse-logger.d.ts" />
export type Difference = {
path: string;
actual: any;
expected: any;
};
export type Comparison = {
name: string;
actual: any;
expected: any;
equal: boolean;
diffs: Difference[] | null;
};
/**
* 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, useLegacyNavigation?: boolean}=} reportOptions
* @return {{passed: number, failed: number, log: string}}
*/
export function getAssertionReport(actual: {
lhr: LH.Result;
artifacts: LH.Artifacts;
networkRequests?: string[];
}, expected: Smokehouse.ExpectedRunnerResult, reportOptions?: {
runner?: string;
isDebug?: boolean;
useLegacyNavigation?: boolean;
} | undefined): {
passed: number;
failed: number;
log: string;
};
/**
* 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}
*/
export function findDifferences(path: string, actual: any, expected: any): Difference[] | null;
import log from 'lighthouse-logger';
//# sourceMappingURL=report-assert.d.ts.map

View File

@@ -0,0 +1,520 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview An assertion library for comparing smoke-test expectations
* against the results actually collected from Lighthouse.
*/
import cloneDeep from 'lodash/cloneDeep.js';
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, useLegacyNavigation?: boolean}=} reportOptions
*/
function pruneExpectations(localConsole, lhr, expected, reportOptions) {
const isLegacyNavigation = reportOptions?.useLegacyNavigation;
/**
* 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._legacyOnly && !isLegacyNavigation) {
localConsole.log([
`[${key}] marked legacy only but run is Fraggle Rock, pruning expectation:`,
JSON.stringify(value, null, 2),
].join(' '));
remove(key);
} else if (value._fraggleRockOnly && isLegacyNavigation) {
localConsole.log([
`[${key}] marked Fraggle Rock only but run is legacy, pruning expectation:`,
JSON.stringify(value, null, 2),
`Actual channel: ${lhr.configSettings.channel}`,
].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._legacyOnly;
delete obj._fraggleRockOnly;
delete obj._skipInBundled;
delete obj._minChromiumVersion;
delete obj._maxChromiumVersion;
delete obj._runner;
delete obj._excludeRunner;
}
const cloned = cloneDeep(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, useLegacyNavigation?: 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,
};

View File

@@ -0,0 +1,35 @@
export type ChildProcessError = import('./lib/child-process-error.js').ChildProcessError;
export type Run = {
networkRequests: string[] | undefined;
lhr: LH.Result;
artifacts: LH.Artifacts;
lighthouseLog: string;
assertionLog: string;
};
export type SmokehouseResult = {
id: string;
passed: number;
failed: number;
runs: Run[];
};
/**
* Runs the selected smoke tests. Returns whether all assertions pass.
* @param {Array<Smokehouse.TestDfn>} smokeTestDefns
* @param {Smokehouse.SmokehouseOptions} smokehouseOptions
* @return {Promise<{success: boolean, testResults: SmokehouseResult[]}>}
*/
export function runSmokehouse(smokeTestDefns: Array<Smokehouse.TestDfn>, smokehouseOptions: Smokehouse.SmokehouseOptions): Promise<{
success: boolean;
testResults: SmokehouseResult[];
}>;
/**
* Parses the cli `shardArg` flag into `shardNumber/shardTotal`. Splits
* `testDefns` into `shardTotal` shards and returns the `shardNumber`th shard.
* Shards will differ in size by at most 1.
* Shard params must be 1 ≤ shardNumber ≤ shardTotal.
* @param {Array<Smokehouse.TestDfn>} testDefns
* @param {string=} shardArg
* @return {Array<Smokehouse.TestDfn>}
*/
export function getShardedDefinitions(testDefns: Array<Smokehouse.TestDfn>, shardArg?: string | undefined): Array<Smokehouse.TestDfn>;
//# sourceMappingURL=smokehouse.d.ts.map

View File

@@ -0,0 +1,336 @@
/**
* @license Copyright 2016 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview An end-to-end test runner for Lighthouse. Takes a set of smoke
* test definitions and a method of running Lighthouse, returns whether all the
* smoke tests passed.
*/
/* eslint-disable no-console */
/** @typedef {import('./lib/child-process-error.js').ChildProcessError} ChildProcessError */
/**
* @typedef Run
* @property {string[] | undefined} networkRequests
* @property {LH.Result} lhr
* @property {LH.Artifacts} artifacts
* @property {string} lighthouseLog
* @property {string} assertionLog
*/
/**
* @typedef SmokehouseResult
* @property {string} id
* @property {number} passed
* @property {number} failed
* @property {Run[]} runs
*/
import assert from 'assert/strict';
import log from 'lighthouse-logger';
import {runLighthouse as cliLighthouseRunner} from './lighthouse-runners/cli.js';
import {getAssertionReport} from './report-assert.js';
import {LocalConsole} from './lib/local-console.js';
import {ConcurrentMapper} from './lib/concurrent-mapper.js';
// The number of concurrent (`!runSerially`) tests to run if `jobs` isn't set.
const DEFAULT_CONCURRENT_RUNS = 5;
const DEFAULT_RETRIES = 0;
/**
* Runs the selected smoke tests. Returns whether all assertions pass.
* @param {Array<Smokehouse.TestDfn>} smokeTestDefns
* @param {Smokehouse.SmokehouseOptions} smokehouseOptions
* @return {Promise<{success: boolean, testResults: SmokehouseResult[]}>}
*/
async function runSmokehouse(smokeTestDefns, smokehouseOptions) {
const {
isDebug,
useLegacyNavigation,
jobs = DEFAULT_CONCURRENT_RUNS,
retries = DEFAULT_RETRIES,
lighthouseRunner = Object.assign(cliLighthouseRunner, {runnerName: 'cli'}),
takeNetworkRequestUrls,
setup,
} = smokehouseOptions;
assertPositiveInteger('jobs', jobs);
assertNonNegativeInteger('retries', retries);
try {
await setup?.();
} catch (err) {
console.error(log.redify('\nERROR DURING SETUP:'));
console.error(log.redify(err.stack || err));
return {success: false, testResults: []};
}
// Run each testDefn in parallel based on the concurrencyLimit.
const concurrentMapper = new ConcurrentMapper();
const testOptions = {
isDebug,
useLegacyNavigation,
retries,
lighthouseRunner,
takeNetworkRequestUrls,
};
const smokePromises = smokeTestDefns.map(testDefn => {
// If defn is set to `runSerially`, we'll run it in succession with other tests, not parallel.
const concurrency = testDefn.runSerially ? 1 : jobs;
return concurrentMapper.runInPool(() => runSmokeTest(testDefn, testOptions), {concurrency});
});
const testResults = await Promise.all(smokePromises);
// Print final summary.
let passingCount = 0;
let failingCount = 0;
for (const testResult of testResults) {
passingCount += testResult.passed;
failingCount += testResult.failed;
}
if (passingCount) console.log(log.greenify(`${getAssertionLog(passingCount)} passing in total`));
if (failingCount) console.log(log.redify(`${getAssertionLog(failingCount)} failing in total`));
// Print id(s) and fail if there were failing tests.
const failingDefns = testResults.filter(result => result.failed);
if (failingDefns.length) {
const testNames = failingDefns.map(d => d.id).join(', ');
console.log(log.redify(`We have ${failingDefns.length} failing smoketest(s): ${testNames}`));
return {success: false, testResults};
}
return {success: true, testResults};
}
/**
* @param {string} loggableName
* @param {number} value
*/
function assertPositiveInteger(loggableName, value) {
if (!Number.isInteger(value) || value <= 0) {
throw new Error(`${loggableName} must be a positive integer`);
}
}
/**
* @param {string} loggableName
* @param {number} value
*/
function assertNonNegativeInteger(loggableName, value) {
if (!Number.isInteger(value) || value < 0) {
throw new Error(`${loggableName} must be a non-negative integer`);
}
}
/** @param {string} str */
function purpleify(str) {
return `${log.purple}${str}${log.reset}`;
}
/**
* @param {LH.Config=} config
* @return {LH.Config|undefined}
*/
function convertToLegacyConfig(config) {
if (!config) return config;
return {
...config,
passes: [{
passName: 'defaultPass',
pauseAfterFcpMs: config.settings?.pauseAfterFcpMs,
pauseAfterLoadMs: config.settings?.pauseAfterLoadMs,
networkQuietThresholdMs: config.settings?.networkQuietThresholdMs,
cpuQuietThresholdMs: config.settings?.cpuQuietThresholdMs,
blankPage: config.settings?.blankPage,
}],
};
}
/**
* Run Lighthouse in the selected runner.
* @param {Smokehouse.TestDfn} smokeTestDefn
* @param {{isDebug?: boolean, useLegacyNavigation?: boolean, retries: number, lighthouseRunner: Smokehouse.LighthouseRunner, takeNetworkRequestUrls?: () => string[]}} testOptions
* @return {Promise<SmokehouseResult>}
*/
async function runSmokeTest(smokeTestDefn, testOptions) {
const {id, expectations} = smokeTestDefn;
const {
lighthouseRunner,
retries,
isDebug,
useLegacyNavigation,
takeNetworkRequestUrls,
} = testOptions;
const requestedUrl = expectations.lhr.requestedUrl;
console.log(`${purpleify(id)} smoketest starting…`);
// Rerun test until there's a passing result or retries are exhausted to prevent flakes.
/** @type {Run[]} */
const runs = [];
let result;
let report;
const bufferedConsole = new LocalConsole();
bufferedConsole.log(`\n${purpleify(id)}: testing '${requestedUrl}'…`);
for (let i = 0; i <= retries; i++) {
if (i !== 0) {
bufferedConsole.log(` Retrying run (${i} out of ${retries} retries)…`);
}
let config = smokeTestDefn.config;
if (useLegacyNavigation) {
config = convertToLegacyConfig(config);
}
// Run Lighthouse.
try {
result = {
...await lighthouseRunner(requestedUrl, config, {isDebug, useLegacyNavigation}),
networkRequests: takeNetworkRequestUrls ? takeNetworkRequestUrls() : undefined,
};
if (!result.lhr?.audits || !result.artifacts) {
// Something went really wrong and the runner didn't catch it.
throw new Error('lighthouse runner returned a bad result. got lhr:\n' +
JSON.stringify(result.lhr, null, 2));
}
} catch (e) {
// Clear the network requests so that when we retry, we don't see duplicates.
if (takeNetworkRequestUrls) takeNetworkRequestUrls();
logChildProcessError(bufferedConsole, e);
continue; // Retry, if possible.
}
// Assert result.
report = getAssertionReport(result, expectations, {
runner: lighthouseRunner.runnerName,
isDebug,
useLegacyNavigation,
});
runs.push({
...result,
lighthouseLog: result.log,
assertionLog: report.log,
});
if (report.failed) {
bufferedConsole.log(` ${getAssertionLog(report.failed)} failed.`);
continue; // Retry, if possible.
}
break; // Passing result, no need to retry.
}
bufferedConsole.log(` smoketest results:`);
// Write result log if we have one.
if (result) bufferedConsole.write(result.log);
// If there's not an assertion report, just report the whole thing as a single failure.
if (report) bufferedConsole.write(report.log);
const passed = report ? report.passed : 0;
const failed = report ? report.failed : 1;
const correctStr = getAssertionLog(passed);
const colorFn = passed === 0 ? log.redify : log.greenify;
bufferedConsole.log(` Correctly passed ${colorFn(correctStr)}`);
if (failed) {
const failedString = getAssertionLog(failed);
bufferedConsole.log(` Failed ${log.redify(failedString)}`);
}
bufferedConsole.log(`${purpleify(id)} smoketest complete.`);
// Log now so right after finish, but all at once so not interleaved with other tests.
console.log(bufferedConsole.getLog());
return {
id,
passed,
failed,
runs,
};
}
/**
* Logs an error to the console, including stdout and stderr if `err` is a
* `ChildProcessError`.
* @param {LocalConsole} localConsole
* @param {ChildProcessError|Error} err
*/
function logChildProcessError(localConsole, err) {
if ('stdout' in err && 'stderr' in err) {
localConsole.adoptStdStrings(err);
}
localConsole.log(log.redify(err.stack || err.message));
}
/**
* @param {number} count
* @return {string}
*/
function getAssertionLog(count) {
const plural = count === 1 ? '' : 's';
return `${count} assertion${plural}`;
}
/**
* Parses the cli `shardArg` flag into `shardNumber/shardTotal`. Splits
* `testDefns` into `shardTotal` shards and returns the `shardNumber`th shard.
* Shards will differ in size by at most 1.
* Shard params must be 1 ≤ shardNumber ≤ shardTotal.
* @param {Array<Smokehouse.TestDfn>} testDefns
* @param {string=} shardArg
* @return {Array<Smokehouse.TestDfn>}
*/
function getShardedDefinitions(testDefns, shardArg) {
if (!shardArg) return testDefns;
// eslint-disable-next-line max-len
const errorMessage = `'shard' must be of the form 'n/d' and n and d must be positive integers with 1 ≤ n ≤ d. Got '${shardArg}'`;
const match = /^(?<shardNumber>\d+)\/(?<shardTotal>\d+)$/.exec(shardArg);
assert(match?.groups, errorMessage);
const shardNumber = Number(match.groups.shardNumber);
const shardTotal = Number(match.groups.shardTotal);
assert(shardNumber > 0 && Number.isInteger(shardNumber), errorMessage);
assert(shardTotal > 0 && Number.isInteger(shardTotal));
assert(shardNumber <= shardTotal, errorMessage);
// Array is sharded with `Math.ceil(length / shardTotal)` shards first
// and then the remaining `Math.floor(length / shardTotal) shards.
// e.g. `[0, 1, 2, 3]` split into 3 shards is `[[0, 1], [2], [3]]`.
const baseSize = Math.floor(testDefns.length / shardTotal);
const biggerSize = baseSize + 1;
const biggerShardCount = testDefns.length % shardTotal;
// Since we don't have tests for this file, construct all shards so correct
// structure can be asserted.
const shards = [];
let index = 0;
for (let i = 0; i < shardTotal; i++) {
const shardSize = i < biggerShardCount ? biggerSize : baseSize;
shards.push(testDefns.slice(index, index + shardSize));
index += shardSize;
}
assert.strictEqual(shards.length, shardTotal);
assert.deepStrictEqual(shards.flat(), testDefns);
const shardDefns = shards[shardNumber - 1];
console.log(`In this shard (${shardArg}), running: ${shardDefns.map(d => d.id).join(' ')}\n`);
return shardDefns;
}
export {
runSmokehouse,
getShardedDefinitions,
};

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=version-check-test.d.ts.map

View File

@@ -0,0 +1,43 @@
/**
* @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
import {chromiumVersionCheck, compareVersions} from './version-check.js';
describe('version check', () => {
it('compareVersions', async () => {
expect(compareVersions([100, 0, 0, 0], [100, 0, 0, 0])).toBe(0);
expect(compareVersions([101, 0, 0, 0], [100, 0, 0, 0])).toBe(1);
expect(compareVersions([99, 0, 0, 0], [100, 0, 0, 0])).toBe(-1);
expect(compareVersions([100, 0, 10, 0], [100, 0, 10, 0])).toBe(0);
expect(compareVersions([100, 0, 11, 0], [100, 0, 10, 0])).toBe(1);
expect(compareVersions([100, 0, 9, 0], [100, 0, 10, 0])).toBe(-1);
expect(compareVersions([100, 0, 0, 0], [100])).toBe(0);
expect(compareVersions([100, 0, 0, 1], [100])).toBe(1);
expect(compareVersions([99, 0, 0, 0], [100])).toBe(-1);
});
it('chromiumVersionCheck', async () => {
expect(chromiumVersionCheck({version: '100'})).toBe(true);
expect(chromiumVersionCheck({version: '100', min: '100'})).toBe(true);
expect(chromiumVersionCheck({version: '100', max: '100'})).toBe(true);
expect(chromiumVersionCheck({version: '100', min: '101'})).toBe(false);
expect(chromiumVersionCheck({version: '100', max: '99'})).toBe(false);
expect(chromiumVersionCheck({version: '100.0.2331.3'})).toBe(true);
expect(chromiumVersionCheck({version: '100.0.2331.3', min: '100.0.2331.3'})).toBe(true);
expect(chromiumVersionCheck({version: '100.0.2331.3', min: '100.0.0.0'})).toBe(true);
expect(chromiumVersionCheck({version: '100.0.2331.3', max: '100.0.3333.3'})).toBe(true);
expect(chromiumVersionCheck({version: '100.0.2331.3', min: '100.0.2331.2'})).toBe(true);
expect(chromiumVersionCheck({version: '100.0.2331.3', max: '99'})).toBe(false);
expect(chromiumVersionCheck({
version: '100.0.2331.3', min: '100.0.2331.0', max: '100.0.2331.10'})).toBe(true);
expect(chromiumVersionCheck({
version: '100.3.2331.3', min: '100.0.2331.0', max: '100.0.2331.10'})).toBe(false);
});
});

View File

@@ -0,0 +1,15 @@
/**
* Returns false if fails check.
* @param {{version: string, min?: string, max?: string}} opts
*/
export function chromiumVersionCheck(opts: {
version: string;
min?: string | undefined;
max?: string | undefined;
}): boolean;
/**
* @param {number[]} versionA
* @param {number[]} versionB
*/
export function compareVersions(versionA: number[], versionB: number[]): 1 | 0 | -1;
//# sourceMappingURL=version-check.d.ts.map

View File

@@ -0,0 +1,48 @@
/**
* @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Compares chromium version strings: 103.0.5017.0
*/
/**
* @param {string} versionString
* @return {number[]}
*/
function parseVersion(versionString) {
const versionParts = versionString.split('.');
return versionParts.map(Number);
}
/**
* @param {number[]} versionA
* @param {number[]} versionB
*/
function compareVersions(versionA, versionB) {
for (let i = 0; i < versionA.length; i++) {
if ((versionA[i] ?? 0) > (versionB[i] ?? 0)) return 1;
if ((versionA[i] ?? 0) < (versionB[i] ?? 0)) return -1;
}
return 0;
}
/**
* Returns false if fails check.
* @param {{version: string, min?: string, max?: string}} opts
*/
function chromiumVersionCheck(opts) {
const version = parseVersion(opts.version);
const min = opts.min && parseVersion(opts.min);
const max = opts.max && parseVersion(opts.max);
if (min && compareVersions(version, min) === -1) return false;
if (max && compareVersions(version, max) === 1) return false;
return true;
}
export {
chromiumVersionCheck,
compareVersions,
};

27
node_modules/lighthouse/commitlint.config.js generated vendored Normal file
View File

@@ -0,0 +1,27 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
module.exports = {
extends: ['cz'],
rules: {
'body-leading-blank': [1, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 80],
'scope-case': [2, 'always', 'lower-case'],
'scope-empty': [0, 'never'],
'subject-case': [
2,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
],
'subject-empty': [0, 'never'],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
// The scope-enum : defined in the cz-config
// The 'type-enum': defined in the cz-config
},
};

View File

@@ -0,0 +1,10 @@
export default Accesskeys;
declare class Accesskeys extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=accesskeys.d.ts.map

View File

@@ -0,0 +1,44 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures accesskey values are unique.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that evaluates if the accesskey HTML attribute values are unique across all elements. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: '`[accesskey]` values are unique',
/** Title of an accesibility audit that evaluates if the ARIA HTML attributes are misaligned with the aria-role HTML attribute specificed on the element, such mismatches are invalid. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: '`[accesskey]` values are not unique',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'Access keys let users quickly focus a part of the page. For proper ' +
'navigation, each access key must be unique. ' +
'[Learn more about access keys](https://dequeuniversity.com/rules/axe/4.7/accesskeys).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class Accesskeys extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'accesskeys',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default Accesskeys;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default ARIAAllowedAttr;
declare class ARIAAllowedAttr extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-allowed-attr.d.ts.map

View File

@@ -0,0 +1,44 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures ARIA attributes are allowed for an element's role.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that evaluates if the ARIA HTML attributes are misaligned with the aria-role HTML attribute specificed on the element, such mismatches are invalid. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: '`[aria-*]` attributes match their roles',
/** Title of an accesibility audit that evaluates if the ARIA HTML attributes are misaligned with the aria-role HTML attribute specificed on the element, such mismatches are invalid. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: '`[aria-*]` attributes do not match their roles',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'Each ARIA `role` supports a specific subset of `aria-*` attributes. ' +
'Mismatching these invalidates the `aria-*` attributes. [Learn ' +
'how to match ARIA attributes to their roles](https://dequeuniversity.com/rules/axe/4.7/aria-allowed-attr).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class ARIAAllowedAttr extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-allowed-attr',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default ARIAAllowedAttr;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaCommandName;
declare class AriaCommandName extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-command-name.d.ts.map

View File

@@ -0,0 +1,42 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures every ARIA button, link and menuitem element has an accessible name
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accessibility audit that evaluates if important HTML elements have an accessible name. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: '`button`, `link`, and `menuitem` elements have accessible names',
/** Title of an accessibility audit that evaluates if important HTML elements do not have accessible names. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: '`button`, `link`, and `menuitem` elements do not have accessible names.',
/** Description of a Lighthouse audit that tells the user *why* they should have accessible names for HTML elements. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'When an element doesn\'t have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to make command elements more accessible](https://dequeuniversity.com/rules/axe/4.7/aria-command-name).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaCommandName extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-command-name',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaCommandName;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaDialogName;
declare class AriaDialogName extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-dialog-name.d.ts.map

View File

@@ -0,0 +1,45 @@
/**
* @license Copyright 2023 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures every ARIA dialog element has a discernable, accessible name.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accessibility audit that evaluates if ARIA dialog elements have an accessible name. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: 'Elements with `role="dialog"` or `role="alertdialog"` have accessible names.',
/** Title of an accessibility audit that evaluates if ARIA dialog elements do not have accessible names. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: 'Elements with `role="dialog"` or `role="alertdialog"` do not have accessible ' +
'names.',
/** Description of a Lighthouse audit that tells the user *why* they should have accessible names for ARIA dialog elements. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'ARIA dialog elements without accessible names may prevent screen readers users ' +
'from discerning the purpose of these elements. ' +
'[Learn how to make ARIA dialog elements more accessible](https://dequeuniversity.com/rules/axe/4.7/aria-dialog-name).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaDialogName extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-dialog-name',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaDialogName;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaHiddenBody;
declare class AriaHiddenBody extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-hidden-body.d.ts.map

View File

@@ -0,0 +1,42 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures `aria-hidden='true'` is not present on the document body.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that checks if the html <body> element does not have an aria-hidden attribute set on it. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: '`[aria-hidden="true"]` is not present on the document `<body>`',
/** Title of an accesibility audit that checks if the html <body> element does not have an aria-hidden attribute set on it. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: '`[aria-hidden="true"]` is present on the document `<body>`',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'Assistive technologies, like screen readers, work inconsistently when `aria-hidden="true"` is set on the document `<body>`. [Learn how `aria-hidden` affects the document body](https://dequeuniversity.com/rules/axe/4.7/aria-hidden-body).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaHiddenBody extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-hidden-body',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaHiddenBody;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaHiddenFocus;
declare class AriaHiddenFocus extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-hidden-focus.d.ts.map

View File

@@ -0,0 +1,42 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures `aria-hidden` elements do not contain focusable elements.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that checks if all elements that have an aria-hidden attribute do not contain focusable descendent elements. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: '`[aria-hidden="true"]` elements do not contain focusable descendents',
/** Title of an accesibility audit that checks if all elements that have an aria-hidden attribute do not contain focusable descendent elements. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: '`[aria-hidden="true"]` elements contain focusable descendents',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'Focusable descendents within an `[aria-hidden="true"]` element prevent those interactive elements from being available to users of assistive technologies like screen readers. [Learn how `aria-hidden` affects focusable elements](https://dequeuniversity.com/rules/axe/4.7/aria-hidden-focus).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaHiddenFocus extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-hidden-focus',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaHiddenFocus;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaInputFieldName;
declare class AriaInputFieldName extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-input-field-name.d.ts.map

View File

@@ -0,0 +1,42 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures all ARIA input fields have an accessible name.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that checks that all ARIA input fields have an accessible name. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: 'ARIA input fields have accessible names',
/** Title of an accesibility audit that checks that all ARIA input fields have an accessible name. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: 'ARIA input fields do not have accessible names',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'When an input field doesn\'t have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn more about input field labels](https://dequeuniversity.com/rules/axe/4.7/aria-input-field-name).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaInputFieldName extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-input-field-name',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaInputFieldName;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaMeterName;
declare class AriaMeterName extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-meter-name.d.ts.map

View File

@@ -0,0 +1,42 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures every ARIA meter element has an accessible name
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accessibility audit that evaluates if meter HTML elements have an accessible name. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: 'ARIA `meter` elements have accessible names',
/** Title of an accessibility audit that evaluates if meter HTML elements do not have accessible names. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: 'ARIA `meter` elements do not have accessible names.',
/** Description of a Lighthouse audit that tells the user *why* they should have accessible names for HTML 'meter' elements. This is displayed after a user expands the section to see more. No character length limits. 'Learn how...' becomes link text to additional documentation. */
description: 'When a meter element doesn\'t have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `meter` elements](https://dequeuniversity.com/rules/axe/4.7/aria-meter-name).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaMeterName extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-meter-name',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaMeterName;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaProgressbarName;
declare class AriaProgressbarName extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-progressbar-name.d.ts.map

View File

@@ -0,0 +1,42 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures every ARIA progressbar element has an accessible name
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accessibility audit that evaluates if progressbar HTML elements have an accessible name. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: 'ARIA `progressbar` elements have accessible names',
/** Title of an accessibility audit that evaluates if progressbar HTML elements do not have accessible names. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: 'ARIA `progressbar` elements do not have accessible names.',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'When a `progressbar` element doesn\'t have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to label `progressbar` elements](https://dequeuniversity.com/rules/axe/4.7/aria-progressbar-name).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaProgressbarName extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-progressbar-name',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaProgressbarName;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default ARIARequiredAttr;
declare class ARIARequiredAttr extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-required-attr.d.ts.map

View File

@@ -0,0 +1,43 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures elements with ARIA roles have all required ARIA attributes.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that evaluates if all elements with the aria-role attribute have the other corresponding ARIA attributes set as well. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: '`[role]`s have all required `[aria-*]` attributes',
/** Title of an accesibility audit that evaluates if all elements with the aria-role attribute have the other corresponding ARIA attributes set as well. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: '`[role]`s do not have all required `[aria-*]` attributes',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'Some ARIA roles have required attributes that describe the state ' +
'of the element to screen readers. [Learn more about roles and required attributes](https://dequeuniversity.com/rules/axe/4.7/aria-required-attr).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class ARIARequiredAttr extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-required-attr',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default ARIARequiredAttr;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaRequiredChildren;
declare class AriaRequiredChildren extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-required-children.d.ts.map

View File

@@ -0,0 +1,47 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures elements with an ARIA role contain any required children.
* e.g. A parent node with role="list" should contain children with role="listitem".
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that evaluates if the elements with an aria-role that require child elements have the required children. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: 'Elements with an ARIA `[role]` that require children to contain a specific ' +
'`[role]` have all required children.',
/** Title of an accesibility audit that evaluates if the elements with an aria-role that require child elements have the required children. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: 'Elements with an ARIA `[role]` that require children to contain a specific ' +
'`[role]` are missing some or all of those required children.',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'Some ARIA parent roles must contain specific child roles to perform ' +
'their intended accessibility functions. ' +
'[Learn more about roles and required children elements](https://dequeuniversity.com/rules/axe/4.7/aria-required-children).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaRequiredChildren extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-required-children',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaRequiredChildren;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaRequiredParent;
declare class AriaRequiredParent extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-required-parent.d.ts.map

View File

@@ -0,0 +1,45 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures elements with an ARIA role are contained by their required parents.
* e.g. A child node with role="listitem" should be contained by a parent with role="list".
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that evaluates valid aria-role usage. Some ARIA roles require that elements must be a child of specific parent element. This audit checks that when those roles are used, the element with the role is in fact a child of the required parent. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: '`[role]`s are contained by their required parent element',
/** Title of an accesibility audit that evaluates valid aria-role usage. Some ARIA roles require that elements must be a child of specific parent element. This audit checks that when those roles are used, the element with the role is in fact a child of the required parent. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: '`[role]`s are not contained by their required parent element',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'Some ARIA child roles must be contained by specific parent roles to ' +
'properly perform their intended accessibility functions. ' +
'[Learn more about ARIA roles and required parent element](https://dequeuniversity.com/rules/axe/4.7/aria-required-parent).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaRequiredParent extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-required-parent',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaRequiredParent;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaRoles;
declare class AriaRoles extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-roles.d.ts.map

View File

@@ -0,0 +1,44 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures all elements with a role attribute use a valid value.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that evaluates if all elements have valid aria-role HTML attributes. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: '`[role]` values are valid',
/** Title of an accesibility audit that evaluates if all elements have valid aria-role HTML attributes. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: '`[role]` values are not valid',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'ARIA roles must have valid values in order to perform their ' +
'intended accessibility functions. ' +
'[Learn more about valid ARIA roles](https://dequeuniversity.com/rules/axe/4.7/aria-roles).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaRoles extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-roles',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaRoles;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaText;
declare class AriaText extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-text.d.ts.map

View File

@@ -0,0 +1,44 @@
/**
* @license Copyright 2023 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures all elements with `role=text` have no focusable descendents.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that evaluates if elements with `role=text` have no focusable descendents. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: 'Elements with the `role=text` attribute do not have focusable descendents.',
/** Title of an accesibility audit that evaluates if elements with `role=text` have focusable descendents. This title is descriptive of the successful state and is shown to users when no user action is required. */
failureTitle: 'Elements with the `role=text` attribute do have focusable descendents.',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'Adding `role=text` around a text node split by markup enables VoiceOver to treat ' +
'it as one phrase, but the element\'s focusable descendents will not be announced. ' +
'[Learn more about the `role=text` attribute](https://dequeuniversity.com/rules/axe/4.7/aria-text).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaText extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-text',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaText;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaToggleFieldName;
declare class AriaToggleFieldName extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-toggle-field-name.d.ts.map

View File

@@ -0,0 +1,42 @@
/**
* @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures all ARIA toggle fields have an accessible name.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that checks that all ARIA toggle fields have an accessible name. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: 'ARIA toggle fields have accessible names',
/** Title of an accesibility audit that checks that all ARIA toggle fields have an accessible name. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: 'ARIA toggle fields do not have accessible names',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'When a toggle field doesn\'t have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn more about toggle fields](https://dequeuniversity.com/rules/axe/4.7/aria-toggle-field-name).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaToggleFieldName extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-toggle-field-name',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaToggleFieldName;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaTooltipName;
declare class AriaTooltipName extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-tooltip-name.d.ts.map

View File

@@ -0,0 +1,42 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures every ARIA tooltip has an accessible name
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accessibility audit that evaluates if tooltip HTML elements have an accessible name. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: 'ARIA `tooltip` elements have accessible names',
/** Title of an accessibility audit that evaluates if tooltip HTML elements do not have accessible names. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: 'ARIA `tooltip` elements do not have accessible names.',
/** Description of a Lighthouse audit that tells the user *why* they should have accessible names for HTML 'tooltip' elements. This is displayed after a user expands the section to see more. No character length limits. 'Learn how...' becomes link text to additional documentation. */
description: 'When a tooltip element doesn\'t have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn how to name `tooltip` elements](https://dequeuniversity.com/rules/axe/4.7/aria-tooltip-name).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaTooltipName extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-tooltip-name',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaTooltipName;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default AriaTreeitemName;
declare class AriaTreeitemName extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-treeitem-name.d.ts.map

View File

@@ -0,0 +1,42 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures every ARIA treeitem element has an accessible name.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accessibility audit that evaluates if treeitem HTML elements have an accessible name. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: 'ARIA `treeitem` elements have accessible names',
/** Title of an accessibility audit that evaluates if treeitem HTML elements do not have accessible names. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: 'ARIA `treeitem` elements do not have accessible names.',
/** Description of a Lighthouse audit that tells the user *why* they should have accessible names for HTML elements. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'When a `treeitem` element doesn\'t have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn more about labeling `treeitem` elements](https://dequeuniversity.com/rules/axe/4.7/aria-treeitem-name).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class AriaTreeitemName extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-treeitem-name',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default AriaTreeitemName;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default ARIAValidAttr;
declare class ARIAValidAttr extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-valid-attr-value.d.ts.map

View File

@@ -0,0 +1,44 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures all ARIA attributes have valid values.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that evaluates if all elements that have an ARIA HTML attribute have a valid value for that attribute. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: '`[aria-*]` attributes have valid values',
/** Title of an accesibility audit that evaluates if all elements that have an ARIA HTML attribute have a valid value for that attribute. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: '`[aria-*]` attributes do not have valid values',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'Assistive technologies, like screen readers, can\'t interpret ARIA ' +
'attributes with invalid values. [Learn ' +
'more about valid values for ARIA attributes](https://dequeuniversity.com/rules/axe/4.7/aria-valid-attr-value).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class ARIAValidAttr extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-valid-attr-value',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default ARIAValidAttr;
export {UIStrings};

View File

@@ -0,0 +1,10 @@
export default ARIAValidAttr;
declare class ARIAValidAttr extends AxeAudit {
}
export namespace UIStrings {
const title: string;
const failureTitle: string;
const description: string;
}
import AxeAudit from './axe-audit.js';
//# sourceMappingURL=aria-valid-attr.d.ts.map

View File

@@ -0,0 +1,44 @@
/**
* @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Ensures aria-* attributes are valid and not misspelled or non-existent.
* See base class in axe-audit.js for audit() implementation.
*/
import AxeAudit from './axe-audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const UIStrings = {
/** Title of an accesibility audit that evaluates if all elements with ARIA HTML attributes have spelled the name of attribute correctly. This title is descriptive of the successful state and is shown to users when no user action is required. */
title: '`[aria-*]` attributes are valid and not misspelled',
/** Title of an accesibility audit that evaluates if all elements with ARIA HTML attributes have spelled the name of attribute correctly. This title is descriptive of the failing state and is shown to users when there is a failure that needs to be addressed. */
failureTitle: '`[aria-*]` attributes are not valid or misspelled',
/** Description of a Lighthouse audit that tells the user *why* they should try to pass. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
description: 'Assistive technologies, like screen readers, can\'t interpret ARIA ' +
'attributes with invalid names. [Learn ' +
'more about valid ARIA attributes](https://dequeuniversity.com/rules/axe/4.7/aria-valid-attr).',
};
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
class ARIAValidAttr extends AxeAudit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'aria-valid-attr',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
requiredArtifacts: ['Accessibility'],
};
}
}
export default ARIAValidAttr;
export {UIStrings};

Some files were not shown because too many files have changed in this diff Show More