Skip to content

Commit 570e4f7

Browse files
committed
Add default autosave restore UX
1 parent 6fa19db commit 570e4f7

10 files changed

Lines changed: 633 additions & 105 deletions

File tree

packages/docs/site/docs/developers/06-apis/query-api/01-index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ You can go ahead and try it out. The Playground will automatically install the t
3838
| `multisite` | `no` | Enables the WordPress multisite mode. Accepts `yes` or `no`. |
3939
| `import-site` | | Imports site files and database from a ZIP file specified by a URL. |
4040
| `import-wxr` | | Imports site content from a WXR file specified by a URL. It uses the WordPress Importer plugin, so the default admin user must be logged in. |
41-
| `site-slug` | | Selects which site to load from browser storage. If the specified site does not exist, the user will be prompted to save a new site with the specified slug. |
41+
| `site-slug` | | Selects which site to load from browser storage. If the specified site does not exist, Playground creates a new browser-saved site with the specified slug unless temporary storage is requested. |
42+
| `storage` | | Controls whether the Playground is saved by default. Use `storage=temp` to create an unsaved temporary Playground that is reset when the page is refreshed or closed. |
4243
| `language` | `en_US` | Sets the locale for the WordPress instance. This must be used in combination with `networking=yes` otherwise WordPress won't be able to download translations. |
4344
| `core-pr` | | Installs a specific https://github.com/WordPress/wordpress-develop core PR. Accepts the PR number. For example, `core-pr=6883`. |
4445
| `gutenberg-pr` | | Installs a specific https://github.com/WordPress/gutenberg PR. Accepts the PR number. For example, `gutenberg-pr=65337`. |

packages/playground/website/playwright/e2e/opfs.spec.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ async function createTestWordPressZip(markerContent: string): Promise<Buffer> {
2121
// level, so tests would interfere with each other's saved sites if run in parallel.
2222
test.describe.configure({ mode: 'serial' });
2323

24+
/**
25+
* Returns a setup URL that must create a temporary Playground for this test.
26+
*
27+
* The random value keeps the setup fingerprint distinct from any saved site
28+
* another serial OPFS test may have created earlier in the same browser.
29+
*/
30+
function getTemporaryPlaygroundUrl(hash = '') {
31+
return `./?storage=temp&random=${Math.random().toString(36).slice(2)}${hash}`;
32+
}
33+
2434
/**
2535
* Helper function to handle the save site modal flow
2636
*/
@@ -33,6 +43,10 @@ async function saveSiteViaModal(
3343
) {
3444
const { customName, storageType = 'opfs' } = options || {};
3545

46+
// The site manager remembers the last selected tab. The save notice only
47+
// lives on the Settings tab, so select it before looking for the button.
48+
await page.getByRole('tab', { name: 'Settings' }).click();
49+
3650
// Click the "Save site locally" button in the temporary site notice to open the modal.
3751
// This button is in the site manager panel and triggers the save flow via SitePersistButton.
3852
const saveButton = page.getByRole('button', { name: 'Save site locally' });
@@ -77,7 +91,7 @@ test('should switch between sites', async ({ website, browserName }) => {
7791
`This test relies on OPFS which isn't available in Playwright's flavor of ${browserName}.`
7892
);
7993

80-
await website.goto('./');
94+
await website.goto(getTemporaryPlaygroundUrl());
8195

8296
await website.ensureSiteManagerIsOpen();
8397

@@ -129,7 +143,9 @@ test('should preserve PHP constants when saving a temporary site to OPFS', async
129143
},
130144
],
131145
};
132-
await website.goto(`./#${JSON.stringify(blueprint)}`);
146+
await website.goto(
147+
getTemporaryPlaygroundUrl(`#${JSON.stringify(blueprint)}`)
148+
);
133149

134150
await website.ensureSiteManagerIsOpen();
135151

@@ -180,7 +196,7 @@ test('should rename a saved Playground and persist after reload', async ({
180196
`This test relies on OPFS which isn't available in Playwright's flavor of ${browserName}.`
181197
);
182198

183-
await website.goto('./');
199+
await website.goto(getTemporaryPlaygroundUrl());
184200
await website.ensureSiteManagerIsOpen();
185201

186202
// Save the temporary site to OPFS so rename is available
@@ -238,7 +254,7 @@ test('should show save site modal with correct elements', async ({
238254
`This test relies on OPFS which isn't available in Playwright's flavor of ${browserName}.`
239255
);
240256

241-
await website.goto('./');
257+
await website.goto(getTemporaryPlaygroundUrl());
242258
await website.ensureSiteManagerIsOpen();
243259

244260
// Click the Save button in the site manager panel
@@ -282,7 +298,7 @@ test('should close save site modal without saving', async ({
282298
`This test relies on OPFS which isn't available in Playwright's flavor of ${browserName}.`
283299
);
284300

285-
await website.goto('./');
301+
await website.goto(getTemporaryPlaygroundUrl());
286302
await website.ensureSiteManagerIsOpen();
287303

288304
// Open the modal
@@ -328,7 +344,7 @@ test('should have playground name input text selected by default', async ({
328344
`This test relies on OPFS which isn't available in Playwright's flavor of ${browserName}.`
329345
);
330346

331-
await website.goto('./');
347+
await website.goto(getTemporaryPlaygroundUrl());
332348
await website.ensureSiteManagerIsOpen();
333349

334350
// Open the modal
@@ -363,7 +379,7 @@ test('should save site with custom name', async ({ website, browserName }) => {
363379
`This test relies on OPFS which isn't available in Playwright's flavor of ${browserName}.`
364380
);
365381

366-
await website.goto('./');
382+
await website.goto(getTemporaryPlaygroundUrl());
367383
await website.ensureSiteManagerIsOpen();
368384

369385
const customName = 'My Custom Playground Name';
@@ -396,7 +412,7 @@ test('should not persist save site modal through page refresh', async ({
396412
`This test relies on OPFS which isn't available in Playwright's flavor of ${browserName}.`
397413
);
398414

399-
await website.goto('./');
415+
await website.goto(getTemporaryPlaygroundUrl());
400416
await website.ensureSiteManagerIsOpen();
401417

402418
// Open the save modal
@@ -433,7 +449,7 @@ test('should display OPFS storage option as selected by default', async ({
433449
`This test relies on OPFS which isn't available in Playwright's flavor of ${browserName}.`
434450
);
435451

436-
await website.goto('./');
452+
await website.goto(getTemporaryPlaygroundUrl());
437453
await website.ensureSiteManagerIsOpen();
438454

439455
// Open the save modal
@@ -477,7 +493,9 @@ test('should import ZIP into temporary site when a saved site exists', async ({
477493
},
478494
],
479495
};
480-
await website.goto(`./#${JSON.stringify(blueprint)}`);
496+
await website.goto(
497+
getTemporaryPlaygroundUrl(`#${JSON.stringify(blueprint)}`)
498+
);
481499

482500
// Verify the marker is present
483501
await expect(wordpress.locator('body')).toContainText(savedSiteMarker);
@@ -564,7 +582,9 @@ test('should create temporary site when importing ZIP while on a saved site with
564582
},
565583
],
566584
};
567-
await website.goto(`./#${JSON.stringify(blueprint)}`);
585+
await website.goto(
586+
getTemporaryPlaygroundUrl(`#${JSON.stringify(blueprint)}`)
587+
);
568588
await expect(wordpress.locator('body')).toContainText(savedSiteMarker);
569589

570590
await website.ensureSiteManagerIsOpen();
@@ -670,9 +690,10 @@ test.describe('Missing site modal', () => {
670690
// Clear all storage to ensure clean state
671691
await context.clearCookies();
672692

673-
// Use a unique slug that definitely doesn't exist
693+
// Use a unique temporary slug so the missing-site prompt is expected.
694+
// Missing saved-site URLs create a new autosaved site by default.
674695
const uniqueSlug = `missing-modal-test-${Date.now()}`;
675-
await website.goto(`./?site-slug=${uniqueSlug}`);
696+
await website.goto(`./?storage=temp&site-slug=${uniqueSlug}`);
676697

677698
// The modal should appear early, even before WordPress fully loads
678699
await expect(
@@ -697,7 +718,7 @@ test.describe('Missing site modal', () => {
697718
await context.clearCookies();
698719

699720
const uniqueSlug = `dismiss-modal-test-${Date.now()}`;
700-
await website.goto(`./?site-slug=${uniqueSlug}`);
721+
await website.goto(`./?storage=temp&site-slug=${uniqueSlug}`);
701722

702723
// Wait for modal
703724
const dialog = website.page.getByRole('dialog', {

packages/playground/website/playwright/e2e/query-api.spec.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const LatestSupportedWordPressVersion = Object.keys(
1717

1818
test('should load PHP 8.3 by default', async ({ website, wordpress }) => {
1919
// Navigate to the website
20-
await website.goto('./?url=/phpinfo.php');
20+
await website.goto('./?storage=temp&url=/phpinfo.php');
2121
await expect(wordpress.locator('h1.p').first()).toContainText(
2222
'PHP Version 8.3'
2323
);
@@ -157,7 +157,7 @@ test('should load WordPress latest by default', async ({
157157
website,
158158
wordpress,
159159
}) => {
160-
await website.goto('./?url=/wp-admin/');
160+
await website.goto('./?storage=temp&url=/wp-admin/');
161161

162162
const expectedBodyClass =
163163
'version-' + LatestSupportedWordPressVersion.replace('.', '-');
@@ -170,7 +170,7 @@ test('should load WordPress 6.3 when requested', async ({
170170
website,
171171
wordpress,
172172
}) => {
173-
await website.goto('./?wp=6.3&url=/wp-admin/');
173+
await website.goto('./?storage=temp&wp=6.3&url=/wp-admin/');
174174
await expect(wordpress.locator(`body.branch-6-3`)).toContainText(
175175
'Dashboard'
176176
);
@@ -180,7 +180,9 @@ test('should disable networking when requested', async ({
180180
website,
181181
wordpress,
182182
}) => {
183-
await website.goto('./?networking=no&url=/wp-admin/plugin-install.php');
183+
await website.goto(
184+
'./?storage=temp&networking=no&url=/wp-admin/plugin-install.php'
185+
);
184186
await expect(wordpress.locator('.notice.error')).toContainText(
185187
'Network access is an experimental, opt-in feature'
186188
);
@@ -190,12 +192,16 @@ test('should enable networking when requested', async ({
190192
website,
191193
wordpress,
192194
}) => {
193-
await website.goto('./?networking=yes&url=/wp-admin/plugin-install.php');
195+
await website.goto(
196+
'./?storage=temp&networking=yes&url=/wp-admin/plugin-install.php'
197+
);
194198
await expect(wordpress.locator('body')).toContainText('Install Now');
195199
});
196200

197201
test('should install the specified plugin', async ({ website, wordpress }) => {
198-
await website.goto('./?plugin=gutenberg&url=/wp-admin/plugins.php');
202+
await website.goto(
203+
'./?storage=temp&plugin=gutenberg&url=/wp-admin/plugins.php'
204+
);
199205
await expect(wordpress.locator('#deactivate-gutenberg')).toContainText(
200206
'Deactivate'
201207
);
@@ -205,23 +211,23 @@ test('should login the user in by default if no login query parameter is provide
205211
website,
206212
wordpress,
207213
}) => {
208-
await website.goto('./?url=/wp-admin/');
214+
await website.goto('./?storage=temp&url=/wp-admin/');
209215
await expect(wordpress.locator('body')).toContainText('Dashboard');
210216
});
211217

212218
test('should login the user in if the login query parameter is set to yes', async ({
213219
website,
214220
wordpress,
215221
}) => {
216-
await website.goto('./?login=yes&url=/wp-admin/');
222+
await website.goto('./?storage=temp&login=yes&url=/wp-admin/');
217223
await expect(wordpress.locator('body')).toContainText('Dashboard');
218224
});
219225

220226
test('should not login the user in if the login query parameter is set to no', async ({
221227
website,
222228
wordpress,
223229
}) => {
224-
await website.goto('./?login=no&url=/wp-admin/');
230+
await website.goto('./?storage=temp&login=no&url=/wp-admin/');
225231
await expect(wordpress.locator('input[type="submit"]')).toContainText(
226232
'Log In'
227233
);
@@ -232,7 +238,7 @@ test('should not login the user in if the login query parameter is set to no', a
232238
['/wp-admin/post.php?post=1&action=edit', 'should redirect to post editor'],
233239
].forEach(([path, description]) => {
234240
test(description, async ({ website, wordpress }) => {
235-
await website.goto(`./?url=${encodeURIComponent(path)}`);
241+
await website.goto(`./?storage=temp&url=${encodeURIComponent(path)}`);
236242
expect(
237243
await wordpress
238244
.locator('body')
@@ -251,7 +257,7 @@ test('should translate WP-admin to Spanish using the language query parameter',
251257
`It's unclear why this test fails on Safari. The root cause of the failure is unknown as the feature ` +
252258
`seems to be working in manual testing.`
253259
);
254-
await website.goto('./?language=es_ES&url=/wp-admin/');
260+
await website.goto('./?storage=temp&language=es_ES&url=/wp-admin/');
255261
await expect(wordpress.locator('body')).toContainText('Escritorio');
256262
});
257263

@@ -318,7 +324,7 @@ test('should retain encoded control characters in the URL', async ({
318324
// most wp-admin pages enforce a redirect to a sanitized (broken)
319325
// version of the URL.
320326
await website.goto(
321-
`./?url=${encodeURIComponent(
327+
`./?storage=temp&url=${encodeURIComponent(
322328
path
323329
)}&plugin=html-api-debugger#${JSON.stringify(blueprint)}`
324330
);
@@ -398,6 +404,7 @@ async function gotoPHPOnlyPlayground(
398404
) {
399405
const query = new URLSearchParams({
400406
php: '8.3',
407+
storage: 'temp',
401408
...queryParams,
402409
});
403410
const blueprint: Blueprint = {

packages/playground/website/playwright/e2e/website-ui.spec.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -585,21 +585,23 @@ test.describe('Database panel', () => {
585585

586586
// Test saving playgrounds by default and when the "can-save" URL parameter is set to "no".
587587
test.describe('Save Status Indicator', () => {
588-
test('should show "Unsaved Playground" status for temporary playgrounds', async ({
588+
test('should show "Unsaved" status for temporary playgrounds', async ({
589589
website,
590590
}) => {
591-
await website.goto('./');
591+
await website.goto('./?storage=temp');
592592
await website.ensureSiteManagerIsClosed();
593593

594-
const indicator = website.page.getByText('Unsaved Playground');
594+
const indicator = website.page.getByRole('button', {
595+
name: 'Unsaved',
596+
});
595597
await expect(indicator).toBeVisible();
596598
await expect(indicator).toHaveCount(1);
597599
});
598600

599601
test('should see save playground message in the Site Manager', async ({
600602
website,
601603
}) => {
602-
await website.goto('./');
604+
await website.goto('./?storage=temp');
603605
await website.ensureSiteManagerIsOpen();
604606

605607
const indicator = website.page.getByText(
@@ -610,13 +612,15 @@ test.describe('Save Status Indicator', () => {
610612
await expect(indicator).toHaveCount(1);
611613
});
612614

613-
test('should not show "Unsaved Playground" status when "can-save=no" is set', async ({
615+
test('should not show "Unsaved" status when "can-save=no" is set', async ({
614616
website,
615617
}) => {
616618
await website.goto('./?can-save=no');
617619
await website.ensureSiteManagerIsClosed();
618620

619-
const indicator = website.page.getByText('Unsaved Playground');
621+
const indicator = website.page.getByRole('button', {
622+
name: 'Unsaved',
623+
});
620624
await expect(indicator).toHaveCount(0);
621625
});
622626

0 commit comments

Comments
 (0)