diff --git a/docs/guide/applying-patches.md b/docs/guide/applying-patches.md index 7adfb146..1028d05f 100644 --- a/docs/guide/applying-patches.md +++ b/docs/guide/applying-patches.md @@ -6,7 +6,7 @@ On a WordPress Core site, the panel accepts pull requests, local `.diff` / `.pat The **Apply a patch or PR** panel lets you test someone else's work before adding your own. Pull requests and patch files take different paths: a PR becomes its own checkout with the author's commits, while a `.diff`/`.patch` file is applied on top of the branch you are already using. Your own changes are kept with their branch. -![The Apply a patch or PR panel on a Core site, with a field for a pull request URL and a link to choose a patch file](/screenshots/apply-patch-panel.png) +![The Apply a patch or PR card on a Core site, with two tabs, Pull request and Diff, and under the first a field for a pull request URL or number and an Apply PR button](/screenshots/apply-patch-panel.png) On a Gutenberg site, the corresponding card is focused on pull requests: diff --git a/docs/guide/creating-a-site.md b/docs/guide/creating-a-site.md index b8a5e086..970107fe 100644 --- a/docs/guide/creating-a-site.md +++ b/docs/guide/creating-a-site.md @@ -6,19 +6,19 @@ You do not need one, though, for every work item. A site holds as many Core tick When the app starts with no sites, the main area shows a short prompt to create your first one. -![The app before any site exists: an empty main area and the Create a site button at the bottom of the sidebar](/screenshots/empty-state.png) +![The app before any site exists: No sites, Create your first site to begin contributing, and a Create site button in the middle of the window](/screenshots/empty-state.png) ## Start the creation flow -Click **Create a site** at the bottom of the sidebar. A dialog opens with three fields. +Click **Create site** in the middle of the window when there are no sites yet, or **Create new site** at the top of the sites list once there are. A dialog opens with three fields. -![The Create a site dialog, with a Site name text field, a Contribute to choice between WordPress Core and Gutenberg with a line under each, and a Site location folder picker](/screenshots/create-site-modal.png) +![The Create site dialog, with a Site name text field, a Project choice between WordPress Core and Gutenberg, with lines under it that describe the one selected and say it cannot be changed later, and a Location folder picker](/screenshots/create-site-modal.png) - **Site name** — the label shown in the sidebar. It also determines the folder name: spaces and characters that are not valid in file names become hyphens, so a site named `My WordPress site` lives in a folder called `My-WordPress-site`. -- **Contribute to** — which project this site is a checkout of. **WordPress Core** clones `wordpress-develop` and works from [Trac tickets](./trac-tickets), patches and pull requests; **Gutenberg** clones the block editor's repository, builds it and runs it as a plugin in a stock WordPress, and works from [GitHub issues](./gutenberg-issues) and pull requests. The choice decides what the site clones and how it builds and runs, and it cannot be changed afterwards: create another site for the other project. -- **Site location** — the parent folder where the site will be created. The app adds a new directory inside it for the project; it does not clone into the folder you pick directly. +- **Project** — which project this site is a checkout of. **WordPress Core** clones `wordpress-develop` and works from [Trac tickets](./trac-tickets), patches and pull requests; **Gutenberg** clones the block editor's repository, builds it and runs it as a plugin in a stock WordPress, and works from [GitHub issues](./gutenberg-issues) and pull requests. The choice decides what the site clones and how it builds and runs, and it cannot be changed afterwards: create another site for the other project. +- **Location** — the parent folder where the site will be created. The app adds a new directory inside it for the project; it does not clone into the folder you pick directly. -Click **Create site** (or press Enter) to start. **Cancel** or Escape closes the dialog without creating anything. +Click **Create site** (or press Enter) to start. The close button or Escape closes the dialog without creating anything. ## What happens during setup diff --git a/docs/public/screenshots/apply-patch-panel.png b/docs/public/screenshots/apply-patch-panel.png index ec2acdd7..9104563d 100644 Binary files a/docs/public/screenshots/apply-patch-panel.png and b/docs/public/screenshots/apply-patch-panel.png differ diff --git a/docs/public/screenshots/create-site-modal.png b/docs/public/screenshots/create-site-modal.png index 91d5b688..f46eb34e 100644 Binary files a/docs/public/screenshots/create-site-modal.png and b/docs/public/screenshots/create-site-modal.png differ diff --git a/docs/public/screenshots/debug-log.png b/docs/public/screenshots/debug-log.png index 9dd14acf..8da890ca 100644 Binary files a/docs/public/screenshots/debug-log.png and b/docs/public/screenshots/debug-log.png differ diff --git a/docs/public/screenshots/deep-link-prompt.png b/docs/public/screenshots/deep-link-prompt.png index e6c7fa96..a472b0ea 100644 Binary files a/docs/public/screenshots/deep-link-prompt.png and b/docs/public/screenshots/deep-link-prompt.png differ diff --git a/docs/public/screenshots/empty-state.png b/docs/public/screenshots/empty-state.png index 8f9bfa51..a9759e8d 100644 Binary files a/docs/public/screenshots/empty-state.png and b/docs/public/screenshots/empty-state.png differ diff --git a/docs/public/screenshots/gutenberg-pull-request-panel.png b/docs/public/screenshots/gutenberg-pull-request-panel.png index fe50e3cf..6d07e378 100644 Binary files a/docs/public/screenshots/gutenberg-pull-request-panel.png and b/docs/public/screenshots/gutenberg-pull-request-panel.png differ diff --git a/docs/public/screenshots/gutenberg-site-view.png b/docs/public/screenshots/gutenberg-site-view.png index 7e742b63..40cb2cd3 100644 Binary files a/docs/public/screenshots/gutenberg-site-view.png and b/docs/public/screenshots/gutenberg-site-view.png differ diff --git a/docs/public/screenshots/mail-panel.png b/docs/public/screenshots/mail-panel.png index 932c7496..de872ccb 100644 Binary files a/docs/public/screenshots/mail-panel.png and b/docs/public/screenshots/mail-panel.png differ diff --git a/docs/public/screenshots/pr-applied.png b/docs/public/screenshots/pr-applied.png deleted file mode 100644 index 18d70c40..00000000 Binary files a/docs/public/screenshots/pr-applied.png and /dev/null differ diff --git a/docs/public/screenshots/site-menu.png b/docs/public/screenshots/site-menu.png index a04f9aa5..2190657d 100644 Binary files a/docs/public/screenshots/site-menu.png and b/docs/public/screenshots/site-menu.png differ diff --git a/docs/public/screenshots/stale-site-notice.png b/docs/public/screenshots/stale-site-notice.png index f24a5c2c..dc2bdc44 100644 Binary files a/docs/public/screenshots/stale-site-notice.png and b/docs/public/screenshots/stale-site-notice.png differ diff --git a/docs/public/screenshots/terminal.png b/docs/public/screenshots/terminal.png index a8aff078..80be93f5 100644 Binary files a/docs/public/screenshots/terminal.png and b/docs/public/screenshots/terminal.png differ diff --git a/docs/public/screenshots/update-incomplete.png b/docs/public/screenshots/update-incomplete.png index 96b60e6e..8061fa61 100644 Binary files a/docs/public/screenshots/update-incomplete.png and b/docs/public/screenshots/update-incomplete.png differ diff --git a/scripts/screenshots/capture.cjs b/scripts/screenshots/capture.cjs index 06df5749..5eb0e763 100644 --- a/scripts/screenshots/capture.cjs +++ b/scripts/screenshots/capture.cjs @@ -32,7 +32,7 @@ const os = require('os'); const path = require('path'); const readline = require('readline'); const { _electron } = require('playwright-core'); -const { buildFixture, cleanFixtureSites } = require('./fixtures.cjs'); +const { buildFixture, cleanFixtureSites, standInForTheOutside } = require('./fixtures.cjs'); const { shots } = require('./shots.cjs'); const repoRoot = path.resolve(__dirname, '..', '..'); @@ -114,6 +114,13 @@ async function runFixtureTier(selected) { const { userDataDir } = buildFixture(variant); const { app, page } = await launchApp({ TOOLKIT_USER_DATA_DIR: userDataDir }); try { + // A picture is of where things come to rest: a tab's underline + // caught half way to its tab, or a menu half open, is of neither + // state. With motion reduced the design system jumps. + await page.emulateMedia({ reducedMotion: 'reduce' }); + // And of the app, not of the network that day, with no server + // started in a folder that holds no WordPress. + await standInForTheOutside(app); for (const shot of selected.filter((s) => s.variant === variant)) { // Fresh renderer per shot: open menus and modals from the // previous shot cannot leak into this one. diff --git a/scripts/screenshots/fixtures.cjs b/scripts/screenshots/fixtures.cjs index f6bc1d39..70aa7c0d 100644 --- a/scripts/screenshots/fixtures.cjs +++ b/scripts/screenshots/fixtures.cjs @@ -9,9 +9,11 @@ // redacts logs; nothing redacts a screenshot, so the fixture path is the // mitigation. // -// The fake sites are empty directories (plus a canned debug.log or build marker): the renderer -// tolerates a site whose git metadata cannot be read — it shows the site with -// no snapshot date rather than crashing — so no real clone is needed. +// The fake sites are empty directories (plus a canned debug.log or build +// marker): the renderer tolerates a site whose git metadata cannot be read, +// and what it shows of a site's age comes from the seeded settings, so no +// real clone is needed. Nothing a shot does reaches the network or starts a +// process in them: see standInForTheOutside. const fs = require('fs'); const os = require('os'); @@ -24,9 +26,90 @@ const FIXTURE_ROOT = ? path.join(os.tmpdir(), 'wpct-docs-fixture') : '/tmp/wpct-docs-fixture'; +// When the fixture's sites were made and how old their code is, counted back +// from the day the shots are taken and not written down: a date written down +// is fresh for a fortnight and then every site in every picture wears the +// notice that its code is old. The amber dot needs `staleSite` more than 14 +// days behind and the others well inside that. The hour is fixed, so two +// runs on one UTC day draw the same pixels; the code's date is at midnight, +// since the app counts its age in whole days from it, and a later hour would +// make "126 days old" a day less for part of every day. +const DAY_MS = 24 * 60 * 60 * 1000; +function daysAgo(days, hour) { + const day = new Date(Date.now() - days * DAY_MS); + day.setUTCHours(hour, 0, 0, 0); + return day.toISOString(); +} +const FRESH = { createdAt: daysAgo(2, 10), trunkDate: daysAgo(2, 0) }; +const STALE = { createdAt: daysAgo(126, 10), trunkDate: daysAgo(126, 0) }; + +// The ticket the fixture's ready site is linked to. +const LINKED_TICKET = '60000'; + +// What GitHub is said to answer when a ticket's card asks which pull requests +// cite it, as the main process hands it on. The card asks as soon as its site +// is opened; left to the network, a shot would show whatever GitHub says that +// day, or the card still asking. +const LINKED_PULL_REQUESTS = [ + { number: 13245, url: 'https://github.com/WordPress/wordpress-develop/pull/13245', title: 'Docs: correct the default', state: 'open', commitDate: daysAgo(6, 12), updatedAt: daysAgo(5, 15) }, + { number: 13012, url: 'https://github.com/WordPress/wordpress-develop/pull/13012', title: 'Docs: list the values', state: 'closed', updatedAt: daysAgo(40, 12) } +]; + +// Where the mail server is said to be listening, when a shot says it is. +const SMTP_PORT = 1025; + +/** + * Answers for the app what would otherwise be asked of the network or start + * a process, so that a fixture shot is the same picture whatever the network + * says, and no shot starts a server in a folder that holds no WordPress. + * Installed in the main process once per launch, before the window is + * reloaded for the first shot. + * + * The dev server and what it runs first are answered and never run: the + * script is "started" and never ends, and the server's start is accepted + * and never gives an address, so the window stays at starting. Of what + * comes with a server, debug.log's tail is real, and its shot counts on it: + * the window starts the tail itself, before the server. The mail server is + * not, since the server's start is what brings it up: see + * `sayMailServerStarted`. + * + * @param {import('playwright-core').ElectronApplication} app + */ +async function standInForTheOutside(app) { + await app.evaluate(({ ipcMain }, [ticket, prs]) => { + const replace = (channel, handler) => { + ipcMain.removeHandler(channel); + ipcMain.handle(channel, handler); + }; + replace('git:list-ticket-patches', () => ({ ok: true, ticket, prs: { status: 'ok', items: prs, rankComplete: true } })); + replace('npm:run-script', async () => ({ runId: 'docs-fixture' })); + replace('playground:start', async () => ({ ok: true })); + }, [LINKED_TICKET, LINKED_PULL_REQUESTS]); +} + +/** + * Says to the window what the main process says when a site's mail server + * has started, which the stood-in dev server never makes it say. + * + * @param {import('playwright-core').ElectronApplication} app + * @param {string} sitePath + */ +async function sayMailServerStarted(app, sitePath) { + await app.evaluate(({ BrowserWindow }, payload) => { + for (const win of BrowserWindow.getAllWindows()) win.webContents.send('smtp:started', payload); + }, { sitePath, port: SMTP_PORT }); +} + +// What an earlier run left in debug.log, stamped the way PHP stamps it and +// dated the day before the shots: after its site was made, like the mail. +function phpStamp(iso) { + const [, year, month, day, time] = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso); + const name = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][Number(month) - 1]; + return `[${day}-${name}-${year} ${time} UTC]`; +} const DEBUG_LOG_LINES = [ - '[10-Aug-2026 09:12:44 UTC] PHP Notice: Undefined variable $post in /wordpress/wp-content/themes/twentytwentyfive/functions.php on line 112', - '[10-Aug-2026 09:12:45 UTC] PHP Deprecated: Function get_page_by_title is deprecated since version 6.2.0! Use WP_Query instead.', + `${phpStamp(daysAgo(1, 9).replace('09:00:00', '09:12:44'))} PHP Notice: Undefined variable $post in /wordpress/wp-content/themes/twentytwentyfive/functions.php on line 112`, + `${phpStamp(daysAgo(1, 9).replace('09:00:00', '09:12:45'))} PHP Deprecated: Function get_page_by_title is deprecated since version 6.2.0! Use WP_Query instead.`, '' ].join('\n'); @@ -56,11 +139,10 @@ function buildFixture(variant) { siteMeta: { [gutenbergSite]: { initialized: true, - createdAt: '2026-09-16T10:00:00.000Z', + ...FRESH, label: 'my-gutenberg-fix', projectType: 'gutenberg', - skipInitWizard: true, - trunkDate: '2026-09-16T09:00:00.000Z' + skipInitWizard: true } }, preferences: {} @@ -80,39 +162,31 @@ function buildFixture(variant) { fs.mkdirSync(path.join(readySite, 'build', 'wp-content'), { recursive: true }); fs.writeFileSync(path.join(readySite, 'build', 'wp-content', 'debug.log'), DEBUG_LOG_LINES); - // Dates are fixed, not computed from "now": the amber staleness dot needs - // staleSite to be more than 14 days behind, and the other two to be fresh - // enough not to be flagged. Retaking the screenshots years from now flips - // the fresh sites amber too — bump these dates when that happens. writeSettings(userDataDir, { sites: [wizardSite, readySite, staleSite, incompleteSite], siteMeta: { [wizardSite]: { initialized: true, - createdAt: '2026-09-16T10:00:00.000Z', - label: 'wordpress-develop', - trunkDate: '2026-09-16T09:00:00.000Z' + ...FRESH, + label: 'wordpress-develop' }, [readySite]: { initialized: true, - createdAt: '2026-09-16T10:00:00.000Z', + ...FRESH, label: 'my-first-patch', - trunkDate: '2026-09-16T09:00:00.000Z', skipInitWizard: true, - tracTicket: '60000' + tracTicket: LINKED_TICKET }, [staleSite]: { initialized: true, - createdAt: '2026-06-01T10:00:00.000Z', + ...STALE, label: 'older-site', - trunkDate: '2026-06-01T09:00:00.000Z', skipInitWizard: true }, [incompleteSite]: { initialized: true, - createdAt: '2026-09-16T10:00:00.000Z', + ...FRESH, label: 'needs-rebuild', - trunkDate: '2026-09-16T09:00:00.000Z', skipInitWizard: true, updateIncomplete: true } @@ -123,8 +197,9 @@ function buildFixture(variant) { subject: 'Welcome to WordPress Contributor Day', from: 'WordPress ', to: 'contributor@example.test', - date: '2026-08-10T09:30:00.000Z', - sentAt: '2026-08-10T09:30:00.000Z', + // After the site it came from was made. + date: daysAgo(1, 9), + sentAt: daysAgo(1, 9), text: 'Your local WordPress site can send mail safely.', html: '

Your local WordPress site can send mail safely.

', headers: {}, @@ -152,4 +227,4 @@ function cleanFixtureSites() { fs.rmSync(FIXTURE_ROOT, { recursive: true, force: true }); } -module.exports = { buildFixture, cleanFixtureSites, FIXTURE_ROOT }; +module.exports = { standInForTheOutside, sayMailServerStarted, buildFixture, cleanFixtureSites, FIXTURE_ROOT }; diff --git a/scripts/screenshots/shots.cjs b/scripts/screenshots/shots.cjs index d6e0889c..921b4fe9 100644 --- a/scripts/screenshots/shots.cjs +++ b/scripts/screenshots/shots.cjs @@ -32,6 +32,8 @@ // Where the app's controls and cards are is written down once, for the journeys // and for these shots alike. const ui = require('../../tests/e2e/helpers/ui.cjs'); +const path = require('path'); +const { FIXTURE_ROOT, sayMailServerStarted } = require('./fixtures.cjs'); /** * Clicks a site in the sidebar and waits for its view to render. @@ -91,6 +93,9 @@ const shots = [ variant: 'seeded', prepare: async (page) => { await selectSite(page, 'my-first-patch'); + // The card under the menu has asked which pull requests cite the + // ticket; the picture is taken with the answer in it. + await page.getByText('Docs: correct the default').waitFor(); await ui.siteMenuButton(page).click(); await ui.updateTrunkMenuItem(page).waitFor(); } @@ -149,19 +154,19 @@ const shots = [ tier: 'fixture', variant: 'debug', target: (page) => ui.tray(page, 'Logs'), - prepare: async (page, app) => { + prepare: async (page) => { await selectSite(page, 'my-first-patch'); await ui.openTray(page, 'Logs'); - // Starting a real dev session is what makes the renderer attach the - // debug-log tail, but this fixture deliberately is not a WordPress clone. - // Keep both long-running processes pending so the screenshot exercises - // the real tail without publishing their inevitable fixture failures. - await app.evaluate(({ ipcMain }) => { - ipcMain.removeHandler('npm:run-script'); - ipcMain.handle('npm:run-script', async () => ({ runId: 'docs-debug-log' })); - ipcMain.removeHandler('playground:start'); - ipcMain.handle('playground:start', async () => ({ ok: true })); - }); + // Taller than it opens, so that the file's lines are all in the + // picture and the first is not cut by the tabs. + await page.getByRole('separator', { name: 'Resize tray', exact: true }).focus(); + for (let presses = 0; presses < 3; presses++) await page.keyboard.press('ArrowUp'); + // Starting the dev server is what makes the renderer attach the + // debug-log tail, which it does before it asks for the server, and + // the tail is not stood in for. The server and the script it runs + // first are (see standInForTheOutside): this fixture deliberately + // is not a WordPress clone, and the picture is of the real tail + // without their inevitable failures in it. await ui.startDevServerButton(page).click(); await page.getByRole('tab', { name: /debug\.log/i }).filter({ visible: true }).click(); await page.getByText('Undefined variable $post', { exact: false }).filter({ visible: true }).first().waitFor(); @@ -173,11 +178,17 @@ const shots = [ tier: 'fixture', variant: 'seeded', target: (page) => ui.tray(page, 'Email'), - prepare: async (page) => { + prepare: async (page, app) => { await selectSite(page, 'my-first-patch'); + // Starting the server is what loads the list. The server is + // stood in for, so the mail server it would bring up is said to + // have started, where a real one listens on a port of its own + // choosing that is different in every picture. await ui.startDevServerButton(page).click(); await ui.openTray(page, 'Email'); await page.getByText('Welcome to WordPress Contributor Day').filter({ visible: true }).waitFor(); + await sayMailServerStarted(app, path.join(FIXTURE_ROOT, 'my-first-patch')); + await page.getByText(/^SMTP listening on /).filter({ visible: true }).waitFor(); } }, {