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
Binary file modified docs/public/screenshots/apply-patch-panel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/create-site-modal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/debug-log.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/mail-panel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/setup-wizard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/site-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/screenshots/site-view-wide.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/site-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/stale-site-notice.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/terminal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/screenshots/trac-ticket-panel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 47 additions & 6 deletions scripts/screenshots/capture.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ const outDir = path.join(repoRoot, 'docs', 'public', 'screenshots');
// Every image the same size, every run: a fixed window and DPR 1. Without the
// scale-factor switch a retina display doubles the pixel size of half the
// screenshots and the docs pages render them inconsistently.
//
// A shot can override the width with its own `window` — see `site-view-wide` in
// shots.cjs. Layout that only appears past a breakpoint is invisible to a
// harness with one window size, which is how the content column's width cap
// went unphotographed: at 1200px the window is narrower than the cap, so every
// image looked identical whether the cap was there or not.
const WINDOW = { width: 1200, height: 800 };
const ELECTRON_SWITCHES = ['--force-device-scale-factor=1', '--lang=en-GB'];

Expand Down Expand Up @@ -78,6 +84,13 @@ function expandHome(p) {
return p;
}

async function setWindow(app, bounds) {
await app.evaluate(({ BrowserWindow }, size) => {
const win = BrowserWindow.getAllWindows()[0];
win.setBounds({ x: 40, y: 40, ...size });
}, bounds);
Comment on lines +87 to +91
}

async function launchApp(env) {
const app = await _electron.launch({
// From plain Node, require('electron') resolves to the binary's path —
Expand All @@ -89,19 +102,26 @@ async function launchApp(env) {
env: { ...process.env, TZ: 'UTC', ...env }
});
const page = await app.firstWindow();
await app.evaluate(({ BrowserWindow }, bounds) => {
const win = BrowserWindow.getAllWindows()[0];
win.setBounds({ x: 40, y: 40, ...bounds });
}, WINDOW);
await setWindow(app, WINDOW);
return { app, page };
}

// Freeze CSS animations and transitions, and rewind them to their first frame,
// so a shot is a function of the app's state and nothing else.
//
// Without this, three images changed on every run with no code change at all:
// the "Checking GitHub…" spinner is a CSS animation, and each capture caught it
// at a different angle. That is noise in any diff, and worse than noise in a
// stack of branches — a rebase hits a binary conflict on a file where nothing
// actually changed, and binary conflicts have no resolution but to pick a side.
const SHOT_OPTIONS = { animations: 'disabled' };

async function captureShot(page, shot) {
const file = path.join(outDir, `${shot.slug}.png`);
if (shot.target) {
await shot.target(page).screenshot({ path: file });
await shot.target(page).screenshot({ path: file, ...SHOT_OPTIONS });
} else {
await page.screenshot({ path: file });
await page.screenshot({ path: file, ...SHOT_OPTIONS });
}
console.log(` ✓ ${shot.slug}.png`);
}
Expand All @@ -113,6 +133,18 @@ async function runFixtureTier(selected) {
const { app, page } = await launchApp({ TOOLKIT_USER_DATA_DIR: userDataDir });
try {
for (const shot of selected.filter((s) => s.variant === variant)) {
// Set unconditionally, not only when the shot asks for it: the
// previous shot may have widened the window, and a shot that
// silently inherits another's size is the bug this whole file
// exists to avoid.
//
// Merged over WINDOW rather than substituted for it, for the
// same reason. `setBounds` accepts a partial rectangle, so a
// shot declaring only `{ width: 1600 }` — the natural thing to
// write when only the width matters — would otherwise keep
// whatever height the shot before it left, and `--only=<slug>`
// would produce a different image than a full run.
await setWindow(app, { ...WINDOW, ...shot.window });
// Fresh renderer per shot: open menus and modals from the
// previous shot cannot leak into this one.
await page.reload();
Expand Down Expand Up @@ -164,6 +196,15 @@ async function main() {
const known = shots.filter((s) => s.tier === args.tier).map((s) => s.slug);
throw new Error(`No ${args.tier}-tier shot matches. Known slugs: ${known.join(', ')}`);
}
// A `window` on a live shot does nothing — the maintainer owns the window in
// that tier — and a silently ignored key is the shape of a wasted hour.
const sized = selected.filter((s) => s.tier === 'live' && s.window);
if (sized.length) {
throw new Error(
`Live-tier shots cannot set "window": ${sized.map((s) => s.slug).join(', ')}. ` +
'The maintainer sizes the window in that tier.'
);
}
fs.mkdirSync(outDir, { recursive: true });
console.log(`Capturing ${selected.length} ${args.tier}-tier screenshot(s) into ${path.relative(repoRoot, outDir)}/`);
if (args.tier === 'fixture') await runFixtureTier(selected);
Expand Down
54 changes: 51 additions & 3 deletions scripts/screenshots/shots.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// The declarative list of documentation screenshots.
//
// Each entry is { slug, tier, variant, prepare, target }:
// Each entry is { slug, tier, variant, prepare, target, window }:
// - slug: the output filename, docs/public/screenshots/<slug>.png — docs pages
// reference these names, so renaming one is a docs change too;
// - tier 'fixture': captured fully automatically against seeded state;
Expand All @@ -12,7 +12,13 @@
// if a label changes, the shot fails loudly instead of photographing the
// wrong thing;
// - target (optional): a locator for an element screenshot instead of the
// whole window. Panels read better cropped; whole-window shots orient.
// whole window. Panels read better cropped; whole-window shots orient;
// - window (optional, fixture tier only): { width, height } for this shot
// alone, merged over the harness default of 1200x800. Reach for it when the
// layout worth showing only appears at another size — `site-view-wide` is
// the case, and its comment explains why. Partial is fine: `{ width: 1600 }`
// keeps the default height. The live tier ignores it, because there the
// maintainer owns the window.
//
// Three shots that used to be fixture-tier are live-tier now, joining
// dev-server-running, and moving them back would photograph a screen the 1.0
Expand All @@ -29,13 +35,34 @@
// tier still covers everything else.

/**
* Clicks a site in the sidebar and waits for its view to render.
* Clicks a site in the sidebar and waits for its view to settle.
*
* The wait is not cosmetic. Selecting a site with a linked ticket fires the
* linked-pull-request lookup, which is a network call, and the panel shows a
* spinner until it answers. Without waiting, whether a shot catches the spinner
* or the result is a coin flip — the same run has produced site-view.png
* mid-check and trac-ticket-panel.png already resolved, which is two different
* answers to the same question in one set of docs images.
*
* It also made three PNGs change on every run with no code change at all. In a
* stack of branches that is worse than noise: rebasing hits a binary conflict on
* a file nothing actually changed, and a binary conflict has no resolution
* except to pick a side and re-capture.
*
* Bounded and swallowed rather than awaited indefinitely: a site with no ticket
* never shows the spinner at all, and a harness that hangs because GitHub is
* slow is worse than one that photographs a spinner.
*
* @param {import('playwright-core').Page} page
* @param {string} label
*/
async function selectSite(page, label) {
await page.getByText(label, { exact: true }).first().click();
await page
.getByText('Checking GitHub…')
.first()
.waitFor({ state: 'hidden', timeout: 15000 })
.catch(() => {});
}

/**
Expand Down Expand Up @@ -71,6 +98,27 @@ const shots = [
await page.getByRole('dialog').getByText('Site name').waitFor();
}
},
{
// The content column's width cap, which no other shot can show: at the
// default 1200px window the content area is 855px — the width every
// cropped panel shot here comes out at — which is narrower than the
// 880px cap, so the cap has no effect and the image looks the same with
// or without it. This is the only shot that proves --wpct-content-max-width
// does anything, and the only one that would catch its removal.
//
// Unlike its neighbours it is referenced by no docs page. It ships in
// the VitePress build as a review artifact, deliberately: the cap is
// otherwise unfalsifiable by eye, and test/content-column.test.cjs
// asserts this image exists and was captured at the declared width.
slug: 'site-view-wide',
tier: 'fixture',
variant: 'seeded',
window: { width: 1600, height: 800 },
prepare: async (page) => {
await selectSite(page, 'my-first-patch');
await page.getByRole('button', { name: 'Submit changes' }).waitFor();
}
},
{
slug: 'site-menu',
tier: 'fixture',
Expand Down
10 changes: 8 additions & 2 deletions src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,16 @@
`border-radius` here only rounds the glow around the wrapped action
buttons, which set none of their own. The blocks set their radius inline,
which the glow follows and this does not override.

It reads --wpct-radius-control so the glow keeps the shape of the button
inside it. It was a hand-picked 10px, which was already larger than
anything @wordpress/components draws; once the dev-server button lost its
own oversized radius, a 10px glow around a 2px button read as a rounded
box floating behind a square one.
*/
.next-action-cue {
border-radius: 10px;
box-shadow: 0 0 0 3px rgba(240, 184, 73, 0.45), 0 0 14px 3px rgba(240, 184, 73, 0.30);
border-radius: var(--wpct-radius-control);
box-shadow: 0 0 0 3px rgba(var(--wpct-cue), 0.45), 0 0 14px 3px rgba(var(--wpct-cue), 0.30);
}
/* Visually hidden but read aloud — the spoken half of the next-action cue
(#252), for the live region that names the next step. */
Expand Down
Loading
Loading