Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion test/theme.test.mjs
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 } };
}
Expand Down
3 changes: 3 additions & 0 deletions tests/lib/ghost.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions tests/lighthouse.spec.mjs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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]),
Expand Down
Loading