feat: make Personal Workspace installable as a PWA

This commit is contained in:
2026-10-08 00:47:03 +09:00
parent aabeb33f41
commit 67afc7bf15
12 changed files with 280 additions and 5 deletions
+103
View File
@@ -0,0 +1,103 @@
import { chromium, expect, test } from "@playwright/test";
test.skip(process.env.TWITTER_LITE_E2E_PRODUCTION !== "1", "PWA requires the production build.");
test("publishes an installable manifest and correctly sized icons before login", async ({
page,
request,
}) => {
await page.goto("/");
await expect(page.locator('link[rel="manifest"]')).toHaveAttribute(
"href",
"/manifest.webmanifest",
);
const response = await request.get("/manifest.webmanifest");
expect(response.ok()).toBe(true);
const manifest = await response.json();
expect(manifest).toMatchObject({
id: "/",
name: "Personal Workspace",
start_url: "/",
scope: "/",
display: "standalone",
});
expect(manifest.icons).toEqual(
expect.arrayContaining([
expect.objectContaining({ sizes: "192x192", purpose: "any" }),
expect.objectContaining({ sizes: "512x512", purpose: "any" }),
expect.objectContaining({ sizes: "512x512", purpose: "maskable" }),
]),
);
for (const icon of manifest.icons) {
const dimensions = await page.evaluate(async (src: string) => {
const image = new Image();
image.src = src;
await image.decode();
return `${image.naturalWidth}x${image.naturalHeight}`;
}, icon.src);
expect(dimensions).toBe(icon.sizes);
}
});
test("passes Chromium installation checks in a regular browser profile", async ({
baseURL,
}, testInfo) => {
// Playwright's default incognito contexts cannot install apps.
const context = await chromium.launchPersistentContext("", {
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,
baseURL,
extraHTTPHeaders: { "Tailscale-User-Login": "[email protected]" },
viewport: testInfo.project.use.viewport,
isMobile: testInfo.project.use.isMobile,
});
try {
const page = await context.newPage();
await page.goto("/");
await page.evaluate(() => navigator.serviceWorker.ready.then(() => true));
const client = await context.newCDPSession(page);
expect((await client.send("Page.getAppManifest")).errors).toEqual([]);
await expect
.poll(async () => (await client.send("Page.getInstallabilityErrors")).installabilityErrors)
.toEqual([]);
} finally {
await context.close();
}
});
test("shows a private offline fallback and retries the original URL online", async ({
page,
context,
baseURL,
}) => {
await page.goto("/");
await expect
.poll(() => page.evaluate(() => navigator.serviceWorker.controller?.scriptURL))
.toBe(new URL("/sw.js", baseURL).href);
await context.setOffline(true);
const destination = "/login?returnTo=%2Fjournal";
await page.goto(destination);
await expect(page).toHaveURL(new URL(destination, baseURL).href);
await expect(page.getByRole("heading", { name: "You’re offline" })).toBeVisible();
expect(
await page.evaluate(() =>
fetch("/api/auth").then(
() => false,
() => true,
),
),
).toBe(true);
const cachedPaths = await page.evaluate(async () => {
const names = await caches.keys();
const requests = await Promise.all(names.map(async (name) => (await caches.open(name)).keys()));
return requests.flat().map((request) => new URL(request.url).pathname);
});
expect(cachedPaths).toEqual(["/offline.html"]);
await context.setOffline(false);
await page.getByRole("button", { name: "Try again" }).click();
await expect(page).toHaveURL(/\/(?:login|setup)(?:\?|$)/);
await expect(page.getByRole("heading", { name: "You’re offline" })).toHaveCount(0);
await expect(page.getByRole("button", { name: /Create account|Sign in/i })).toBeVisible();
});