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 docs/guide/applying-patches.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
12 changes: 6 additions & 6 deletions docs/guide/creating-a-site.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
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/deep-link-prompt.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/empty-state.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/gutenberg-pull-request-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/gutenberg-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/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 removed docs/public/screenshots/pr-applied.png
Binary file not shown.
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 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/update-incomplete.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion scripts/screenshots/capture.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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, '..', '..');
Expand Down Expand Up @@ -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.
Expand Down
125 changes: 100 additions & 25 deletions scripts/screenshots/fixtures.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');

Expand Down Expand Up @@ -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: {}
Expand All @@ -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
}
Expand All @@ -123,8 +197,9 @@ function buildFixture(variant) {
subject: 'Welcome to WordPress Contributor Day',
from: 'WordPress <wordpress@example.test>',
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: '<p>Your local WordPress site can send mail safely.</p>',
headers: {},
Expand Down Expand Up @@ -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 };
35 changes: 23 additions & 12 deletions scripts/screenshots/shots.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
Expand All @@ -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();
}
},
{
Expand Down
Loading