Skip to content

Commit 785ca96

Browse files
committed
Add saved site creation APIs
1 parent 3b76840 commit 785ca96

9 files changed

Lines changed: 709 additions & 85 deletions

File tree

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

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ test('playgroundSites.list() returns the active site', async ({ website }) => {
2020
const active = sites.find((s: any) => s.isActive);
2121
expect(active).toBeTruthy();
2222
expect(active.slug).toBeTruthy();
23-
expect(active.storage).toBe('temporary');
23+
// Storage-specific behavior is covered below. This test only checks that
24+
// the list exposes the active Playground through the public API shape.
25+
expect(['opfs', 'local-fs', 'temporary']).toContain(active.storage);
2426
});
2527

2628
test('playgroundSites.saveInBrowser() persists a temporary site', async ({
@@ -32,7 +34,7 @@ test('playgroundSites.saveInBrowser() persists a temporary site', async ({
3234
`This test relies on OPFS which isn't available in Playwright's flavor of ${browserName}.`
3335
);
3436

35-
await website.goto('./');
37+
await website.goto('./?storage=temp');
3638
await website.page.waitForFunction(() =>
3739
Boolean((window as any).playgroundSites?.getClient())
3840
);
@@ -44,6 +46,87 @@ test('playgroundSites.saveInBrowser() persists a temporary site', async ({
4446
expect(result.storage).toBe('opfs');
4547
});
4648

49+
test('playgroundSites.autosaveTemporarySite() persists without disrupting the tab', async ({
50+
website,
51+
browserName,
52+
}) => {
53+
test.skip(
54+
browserName !== 'chromium',
55+
`This test relies on OPFS which isn't available in Playwright's flavor of ${browserName}.`
56+
);
57+
58+
await website.goto('./?storage=temp');
59+
await website.page.waitForFunction(() =>
60+
Boolean((window as any).playgroundSites?.getClient())
61+
);
62+
63+
const result = await website.page.evaluate(async () => {
64+
const api = (window as any).playgroundSites;
65+
const beforeUrl = window.location.href;
66+
const beforeClient = api.getClient();
67+
const beforeActive = api.list().find((s: any) => s.isActive);
68+
const saveResult = await api.autosaveTemporarySite();
69+
const afterActive = api.list().find((s: any) => s.isActive);
70+
return {
71+
beforeSlug: beforeActive?.slug,
72+
saveResult,
73+
afterActive,
74+
sameClient: beforeClient === api.getClient(),
75+
sameUrl: window.location.href === beforeUrl,
76+
};
77+
});
78+
79+
// Autosave should promote the active temporary site to browser storage
80+
// without changing the visible URL or replacing the running Playground.
81+
expect(result.saveResult.slug).toBe(result.beforeSlug);
82+
expect(result.saveResult.storage).toBe('opfs');
83+
expect(result.afterActive.storage).toBe('opfs');
84+
expect(result.afterActive.persistence).toBe('autosave');
85+
expect(result.sameClient).toBe(true);
86+
expect(result.sameUrl).toBe(true);
87+
});
88+
89+
test('playgroundSites.createNewSavedSite() creates an explicit OPFS site by default', async ({
90+
website,
91+
browserName,
92+
}) => {
93+
test.skip(
94+
browserName !== 'chromium',
95+
`This test relies on OPFS which isn't available in Playwright's flavor of ${browserName}.`
96+
);
97+
98+
await website.goto('./?storage=temp');
99+
await website.page.waitForFunction(() =>
100+
Boolean((window as any).playgroundSites?.getClient())
101+
);
102+
103+
const result = await website.page.evaluate(async () => {
104+
const api = (window as any).playgroundSites;
105+
const beforeUrl = window.location.href;
106+
const slug = await api.createNewSavedSite(
107+
'api-created-site',
108+
{ phpVersion: '8.3' },
109+
{ updateUrl: false }
110+
);
111+
const active = api.list().find((s: any) => s.isActive);
112+
return {
113+
slug,
114+
active,
115+
sameUrl: window.location.href === beforeUrl,
116+
hasClient: api.getClient() != null,
117+
};
118+
});
119+
120+
// The new API creates a saved site record, boots it, and marks it as an
121+
// explicit save unless the caller opts into autosave persistence.
122+
expect(result.slug).toBe('api-created-site');
123+
expect(result.active.slug).toBe('api-created-site');
124+
expect(result.active.storage).toBe('opfs');
125+
expect(result.active.persistence).toBe('explicit');
126+
expect(result.hasClient).toBe(true);
127+
expect(result.sameUrl).toBe(true);
128+
});
129+
47130
test('playgroundSites.rename() renames a saved site', async ({
48131
website,
49132
browserName,
@@ -60,8 +143,8 @@ test('playgroundSites.rename() renames a saved site', async ({
60143

61144
const newName = await website.page.evaluate(async () => {
62145
const api = (window as any).playgroundSites;
63-
await api.saveInBrowser();
64146
const name = 'Renamed Via API';
147+
await api.saveInBrowser();
65148
await api.rename(name);
66149
const sites = api.list();
67150
const active = sites.find((s: any) => s.isActive);

packages/playground/website/src/lib/state/playground-identity.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getAutosaveFingerprintFromURL,
55
getAutosaveFingerprintFromSite,
66
getRuntimeBootFingerprint,
7+
getSetupUrlFromUrl,
78
} from './playground-identity';
89

910
describe('getAutosaveFingerprintFromURL', () => {
@@ -100,6 +101,22 @@ describe('getAutosaveFingerprintFromURL', () => {
100101
});
101102
});
102103

104+
describe('getSetupUrlFromUrl', () => {
105+
it('keeps setup query params and drops routing and UI params', () => {
106+
const setupUrl = getSetupUrlFromUrl(
107+
new URL(
108+
'https://playground.test/?php=8.3&php-extension=https://example.com/ext.json&plugin=a&plugin=b' +
109+
'&site-slug=demo&storage=temp&random=abc&modal=save-site' +
110+
'&can-save=no#blueprint'
111+
)
112+
);
113+
114+
expect(setupUrl.toString()).toBe(
115+
'https://playground.test/?php=8.3&php-extension=https%3A%2F%2Fexample.com%2Fext.json&plugin=a&plugin=b#blueprint'
116+
);
117+
});
118+
});
119+
103120
describe('getRuntimeBootFingerprint', () => {
104121
const runtimeConfiguration: RuntimeConfiguration = {
105122
phpVersion: '8.3',

packages/playground/website/src/lib/state/playground-identity.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const SETUP_QUERY_PARAMS = new Set([
1616
'name',
1717
'networking',
1818
'php',
19+
'php-extension',
1920
'plugin',
2021
'theme',
2122
'url',
@@ -35,6 +36,25 @@ export function getAutosaveFingerprintFromURL(url: URL) {
3536
});
3637
}
3738

39+
/**
40+
* Returns a URL containing only query parameters that define Playground setup.
41+
*
42+
* New saved sites use this so routing, UI, and lifecycle query parameters from
43+
* the current page do not leak into persisted site metadata. Keeping this on
44+
* the same setup-param list as autosave fingerprints gives future setup query
45+
* params one place to register.
46+
*/
47+
export function getSetupUrlFromUrl(url: URL) {
48+
const setupUrl = new URL(url.href);
49+
setupUrl.search = '';
50+
for (const [key, value] of url.searchParams.entries()) {
51+
if (SETUP_QUERY_PARAMS.has(key)) {
52+
setupUrl.searchParams.append(key, value);
53+
}
54+
}
55+
return setupUrl;
56+
}
57+
3858
/**
3959
* Returns the autosave fingerprint originally associated with a site.
4060
*

0 commit comments

Comments
 (0)