diff --git a/README.md b/README.md index 3462c91..c75b215 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ reproduced locally with one command. | Console and network | JavaScript errors, theme assets that 404, libraries loaded from third-party CDNs. | | Layout | Pages that scroll sideways at phone, tablet or desktop width, with the elements sticking out. | | Accessibility | [axe](https://github.com/dequelabs/axe-core) violations rated critical or serious, in light and dark mode. Keyboard basics: skip link, visible focus, menus that open and close with the keyboard. | -| Performance | [Lighthouse](https://github.com/GoogleChrome/lighthouse) scores for the homepage and a post, against a budget per theme. | +| Performance | [Lighthouse](https://github.com/GoogleChrome/lighthouse) scores for the homepage and a post, against a budget per theme. Ghost's own apps (Portal, search, comments) are blocked during the run, so the score is the theme's. | | Visual changes | On pull requests: every template at phone, tablet and desktop widths, in light and dark mode, before and after the change. Anything that looks different waits for a reviewer's approval. | Problems inside what Ghost renders itself (Portal, search, comments, the diff --git a/test/theme.test.mjs b/test/theme.test.mjs index d099ab1..313e175 100644 --- a/test/theme.test.mjs +++ b/test/theme.test.mjs @@ -1,4 +1,4 @@ -import { test } from 'node:test'; +import { mock, test } from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from 'node:fs'; @@ -7,6 +7,11 @@ import { join } from 'node:path'; import yauzl from 'yauzl'; import { packageTheme } from '../src/theme.mjs'; +// packageTheme prints "── Zip: …". Plain text on stdout next to the test +// runner's own messages makes `node --test` fail now and then with "Unable to +// deserialize cloned data", so keep it quiet. +mock.method(console, 'log', () => {}); + function config(dir, zip = {}) { return { dir, name: 'my-theme', pkg: {}, zip: { command: null, path: null, ...zip } }; } diff --git a/tests/lib/ghost.mjs b/tests/lib/ghost.mjs index 34673cb..92745d5 100644 --- a/tests/lib/ghost.mjs +++ b/tests/lib/ghost.mjs @@ -32,6 +32,9 @@ export function isHtml(response) { /** Same-origin paths served by Ghost itself rather than the theme. */ const GHOST_PATHS = ['/public/', '/ghost/', '/members/']; +/** Ghost's apps (Portal, search, comments, …), as a Lighthouse `blockedUrlPatterns` entry. */ +export const GHOST_APPS = 'https://cdn.jsdelivr.net/ghost/*'; + /** * Third-party origins Ghost loads on its own: its apps from jsDelivr, fonts * from Bunny Fonts when a site picks a custom font, avatars from Gravatar. diff --git a/tests/lighthouse.spec.mjs b/tests/lighthouse.spec.mjs index 7920822..257bd5f 100644 --- a/tests/lighthouse.spec.mjs +++ b/tests/lighthouse.spec.mjs @@ -1,12 +1,18 @@ // Lighthouse (mobile preset) for pages marked `lighthouse: true`, first profile // only. Runs on its own, in one worker, after the other checks so scores aren't // skewed by parallel load. Scores are attached and printed by the runner as a table. +// +// Ghost's own apps (Portal, search, comments) are blocked. The theme can't +// change them, and on CI runners, with a slow CPU and a fast network, +// Lighthouse's simulation counted Portal's half a megabyte towards the largest +// paint: Pod lost 15 to 20 points that had nothing to do with the theme. import { createServer } from 'node:net'; import { chromium } from '@playwright/test'; import lighthouse from 'lighthouse'; import { test, expect } from './fixtures.mjs'; import { runtime, LIGHTHOUSE } from './lib/runtime.mjs'; +import { GHOST_APPS } from './lib/ghost.mjs'; /** Lighthouse drives Chromium over a debugging port; ask the OS for a free one. */ function freePort() { @@ -34,6 +40,7 @@ for (const entry of runtime.manifest.pages.filter((p) => p.lighthouse)) { output: 'html', logLevel: 'error', onlyCategories: Object.keys(runtime.budgets), + blockedUrlPatterns: [GHOST_APPS], }); const scores = Object.fromEntries( Object.entries(result.lhr.categories).map(([id, category]) => [id, category.score]),