Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,7 @@ jobs:
- run: yarn --cwd aio lint
# Run PWA-score tests
# (Run before unit and e2e tests, which destroy the `dist/` directory.)
# Temporarily lowering the min required PWA score to avoid flakes on CI.
# TODO(gkalpak): Re-enable once https://github.com/angular/angular/issues/29910 is resolved.
# - run: yarn --cwd aio test-pwa-score-localhost $CI_AIO_MIN_PWA_SCORE
- run: yarn --cwd aio test-pwa-score-localhost 70
- run: yarn --cwd aio test-pwa-score-localhost $CI_AIO_MIN_PWA_SCORE
# Check the bundle sizes.
# (Run before unit and e2e tests, which destroy the `dist/` directory.)
- run: yarn --cwd aio payload-size
Expand Down Expand Up @@ -287,10 +284,7 @@ jobs:
- run: yarn --cwd aio build-local --progress=false
# Run PWA-score tests
# (Run before unit and e2e tests, which destroy the `dist/` directory.)
# Temporarily lowering the min required PWA score to avoid flakes on CI.
# TODO(gkalpak): Re-enable once https://github.com/angular/angular/issues/29910 is resolved.
# - run: yarn --cwd aio test-pwa-score-localhost $CI_AIO_MIN_PWA_SCORE
- run: yarn --cwd aio test-pwa-score-localhost 70
- run: yarn --cwd aio test-pwa-score-localhost $CI_AIO_MIN_PWA_SCORE
# Run unit tests
- run: yarn --cwd aio test --progress=false --watch=false
# Run e2e tests
Expand Down
102 changes: 60 additions & 42 deletions aio/scripts/test-pwa-score.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

/**
* Usage:
* node scripts/test-pwa-score <url> <min-score> [<log-file>]
* ```sh
* node scripts/test-pwa-score <url> <min-score> [<log-file>]
* ```
*
* Fails if the score is below `<min-score>`.
* If `<log-file>` is defined, the full results will be logged there.
Expand All @@ -14,12 +16,12 @@
const chromeLauncher = require('chrome-launcher');
const lighthouse = require('lighthouse');
const printer = require('lighthouse/lighthouse-cli/printer');
const config = require('lighthouse/lighthouse-core/config/default-config.js');
const logger = require('lighthouse-logger');

// Constants
const CHROME_LAUNCH_OPTS = {};
const LIGHTHOUSE_FLAGS = {logLevel: 'info'};
const LONG_WAIT_FOR_SW_DELAY = 5000;
const SKIPPED_HTTPS_AUDITS = ['redirects-http'];
const VIEWER_URL = 'https://googlechrome.github.io/lighthouse/viewer/';

Expand All @@ -32,22 +34,31 @@ if (process.env.CI) {
_main(process.argv.slice(2));

// Functions - Definitions
function _main(args) {
async function _main(args) {
const {url, minScore, logFile} = parseInput(args);
const isOnHttp = /^http:/.test(url);
const isOnLocalhost = /\/\/localhost\b/.test(url);
const config = {extends: 'lighthouse:default'};

console.log(`Running PWA audit for '${url}'...`);

if (isOnHttp) {
skipHttpsAudits(config);
}
// If testing on HTTP, skip HTTPS-specific tests.
// (Note: Browsers special-case localhost and run ServiceWorker even on HTTP.)
if (isOnHttp) skipHttpsAudits(config);

// If testing on localhost, where the server has less optimizations (e.g. no file compression),
// wait longer for the ServiceWorker to be registered, so Lighthouse can reliably detect it.
if (isOnLocalhost) waitLongerForSw(config);

logger.setLevel(LIGHTHOUSE_FLAGS.logLevel);

launchChromeAndRunLighthouse(url, LIGHTHOUSE_FLAGS, config).
then(results => processResults(results, logFile)).
then(score => evaluateScore(minScore, score)).
catch(onError);
try {
const results = await launchChromeAndRunLighthouse(url, LIGHTHOUSE_FLAGS, config);
const score = await processResults(results, logFile);
evaluateScore(minScore, score);
} catch (err) {
onError(err);
}
}

function evaluateScore(expectedScore, actualScore) {
Expand All @@ -60,13 +71,15 @@ function evaluateScore(expectedScore, actualScore) {
}
}

function launchChromeAndRunLighthouse(url, flags, config) {
return chromeLauncher.launch(CHROME_LAUNCH_OPTS).then(chrome => {
flags.port = chrome.port;
return lighthouse(url, flags, config).
then(results => chrome.kill().then(() => results)).
catch(err => chrome.kill().then(() => { throw err; }, () => { throw err; }));
});
async function launchChromeAndRunLighthouse(url, flags, config) {
const chrome = await chromeLauncher.launch(CHROME_LAUNCH_OPTS);
flags.port = chrome.port;

try {
return await lighthouse(url, flags, config);
} finally {
await chrome.kill();
}
}

function onError(err) {
Expand All @@ -88,36 +101,41 @@ function parseInput(args) {
return {url, minScore, logFile};
}

function processResults(results, logFile) {
async function processResults(results, logFile) {
const lhVersion = results.lhr.lighthouseVersion;
const categories = results.lhr.categories;
const report = results.report;

return Promise.resolve().
then(() => {
if (logFile) {
console.log(`Saving results in '${logFile}'...`);
console.log(`(LightHouse viewer: ${VIEWER_URL})`);

return printer.write(report, printer.OutputMode.json, logFile);
}
}).
then(() => {
const categoryData = Object.keys(categories).map(name => categories[name]);
const maxTitleLen = Math.max(...categoryData.map(({title}) => title.length));

console.log('\nLighthouse version:', results.lhr.lighthouseVersion);

console.log('\nAudit scores:');
categoryData.forEach(({title, score}) => {
const paddedTitle = `${title}:`.padEnd(maxTitleLen + 1);
const paddedScore = (score * 100).toFixed(0).padStart(3);
console.log(` - ${paddedTitle} ${paddedScore} / 100`);
});
}).
then(() => categories.pwa.score * 100);
if (logFile) {
console.log(`\nSaving results in '${logFile}'...`);
console.log(`(LightHouse viewer: ${VIEWER_URL})`);

await printer.write(report, printer.OutputMode.json, logFile);
}

const categoryData = Object.keys(categories).map(name => categories[name]);
const maxTitleLen = Math.max(...categoryData.map(({title}) => title.length));

console.log(`\nLighthouse version: ${lhVersion}`);

console.log('\nAudit scores:');
categoryData.forEach(({title, score}) => {
const paddedTitle = `${title}:`.padEnd(maxTitleLen + 1);
const paddedScore = (score * 100).toFixed(0).padStart(3);
console.log(` - ${paddedTitle} ${paddedScore} / 100`);
});

return categories.pwa.score * 100;
}

function skipHttpsAudits(config) {
console.info(`Skipping HTTPS-related audits (${SKIPPED_HTTPS_AUDITS.join(', ')})...`);
config.settings.skipAudits = SKIPPED_HTTPS_AUDITS;
const settings = config.settings || (config.settings = {});
settings.skipAudits = SKIPPED_HTTPS_AUDITS;
}

function waitLongerForSw(config) {
console.info(`Will wait longer for ServiceWorker (${LONG_WAIT_FOR_SW_DELAY}ms)...`);
const passes = config.passes || (config.passes = []);
passes.push({passName: 'defaultPass', pauseAfterLoadMs: LONG_WAIT_FOR_SW_DELAY});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What other implications are there here? I'd expect local scores to be artificially inflated because in these 5s the app loads completely.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, there are no other implications. (The score is 100% anyway as long as the SW is detected in time and the failing tests when it is not detected are boolean (does it have a SW? does it work offline? etc).)

Other categories (e.g. performance) don't seem to be inflated due to this setting (e.g. performance is still ~45% on localhost). This makes sense, because pauseAfterLoadMs does not affect performance-specific metrics (such as first-meaningful-paint).

}