Step 11.1: installable PWA
CI / check (push) Successful in 6m11s
Details
CI / check (push) Successful in 6m11s
Details
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6b56af7d1e
commit
feb84aecaa
|
|
@ -32,3 +32,7 @@ jobs:
|
|||
- run: pnpm typecheck
|
||||
- run: pnpm lint
|
||||
- run: pnpm coverage
|
||||
# Browser tests against the production build (Phase 11: PWA and offline).
|
||||
- run: pnpm --filter @deadhead/web build
|
||||
- run: pnpm --filter @deadhead/web exec playwright install --with-deps chromium
|
||||
- run: pnpm --filter @deadhead/web e2e
|
||||
|
|
|
|||
|
|
@ -6,3 +6,6 @@ coverage/
|
|||
.env.local
|
||||
.DS_Store
|
||||
.pnpm-store/
|
||||
# Playwright output
|
||||
apps/web/test-results/
|
||||
apps/web/playwright-report/
|
||||
|
|
|
|||
|
|
@ -1240,3 +1240,11 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
- **Why:** after the deal a fresh player couldn't fuel the Courier and the Clipper from Earth at once (~921 cr against ~890). At 2.1 the pair costs ~645 cr (Courier 161, Clipper 484), and the scripted fresh-account check now flies both together with no top-up.
|
||||
- **2.1, not 2:** Jupiter Cloud Platform is 2, and Earth must stay the most expensive launch (GDD §launch cost, `locations.test.ts`). Only Earth's value changed.
|
||||
- **Effects:** every departure from Earth costs 30 % less fuel; payouts don't use the multiplier, so the difference is profit, and the deal loan is paid off sooner. Moon launches (0.5) stay 4.2× cheaper than Earth. The Step 3.x fuel hand calculation now uses 2.1.
|
||||
|
||||
## Step 11.1
|
||||
|
||||
- **Installability is tested with Chrome's own check**: Lighthouse removed its PWA category in version 12, so the planned "Lighthouse PWA audit" can't run. The Playwright test calls the DevTools `Page.getInstallabilityErrors`, which is what that audit reported, and expects an empty list.
|
||||
- **Browser tests run against `vite preview` of the real build**, with no API server; installability and caching don't need one. They live in `apps/web/e2e/*.e2e.ts` (not picked up by Vitest) and Phase 12's end-to-end tests can reuse the setup.
|
||||
- **Icons are generated placeholders** (the Courier sprite on `#080d1d`), with the maskable icon keeping the ship inside the 80 % safe zone. Replace by putting a logo through `scripts/icons.mjs`.
|
||||
- **Precache is ~6.7 MB**, mostly the @2x map art the CR-003 amendment asks for. Only woff2 fonts are cached (every supporting browser uses them before woff).
|
||||
- **Auto-update**: a new build's service worker takes over on the next load without a prompt; there is no player data in the cache to lose.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe("installable PWA (Step 11.1)", () => {
|
||||
test("Chrome reports no installability errors", async ({ page, context }) => {
|
||||
await page.goto("/");
|
||||
await page.evaluate(() => navigator.serviceWorker.ready.then(() => undefined));
|
||||
// The check Lighthouse's PWA audit used (Lighthouse 12+ no longer has one).
|
||||
const cdp = await context.newCDPSession(page);
|
||||
const { installabilityErrors } = await cdp.send("Page.getInstallabilityErrors");
|
||||
expect(installabilityErrors).toEqual([]);
|
||||
});
|
||||
|
||||
test("has a standalone manifest with the game's name and icons", async ({ request }) => {
|
||||
const manifest = await (await request.get("/manifest.webmanifest")).json();
|
||||
expect(manifest).toMatchObject({
|
||||
name: "Deadhead: Space Freight Tycoon",
|
||||
display: "standalone",
|
||||
theme_color: "#080d1d",
|
||||
});
|
||||
const sizes = manifest.icons.map((i: { sizes: string; purpose?: string }) => i.sizes);
|
||||
expect(sizes).toEqual(expect.arrayContaining(["192x192", "512x512"]));
|
||||
expect(manifest.icons.some((i: { purpose?: string }) => i.purpose === "maskable")).toBe(true);
|
||||
for (const icon of manifest.icons) expect((await request.get(icon.src)).ok()).toBe(true);
|
||||
});
|
||||
|
||||
test("precaches the app shell, fonts and generated map art, not the source art", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.evaluate(() => navigator.serviceWorker.ready.then(() => undefined));
|
||||
// Wait for the install to finish filling the precache.
|
||||
await expect
|
||||
.poll(async () =>
|
||||
page.evaluate(async () => {
|
||||
const urls: string[] = [];
|
||||
for (const name of await caches.keys()) {
|
||||
for (const r of await (await caches.open(name)).keys()) urls.push(r.url);
|
||||
}
|
||||
return urls;
|
||||
}),
|
||||
)
|
||||
.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("/index.html"),
|
||||
expect.stringMatching(/\/assets\/map-gen\/body-earth@2x\.png/),
|
||||
expect.stringMatching(/\/assets\/.*chakra-petch.*\.woff2/),
|
||||
expect.stringMatching(/\/assets\/.*barlow.*\.woff2/),
|
||||
]),
|
||||
);
|
||||
const urls = await page.evaluate(async () => {
|
||||
const all: string[] = [];
|
||||
for (const name of await caches.keys()) {
|
||||
for (const r of await (await caches.open(name)).keys()) all.push(r.url);
|
||||
}
|
||||
return all;
|
||||
});
|
||||
expect(urls.filter((u) => u.includes("/assets/map/"))).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -3,6 +3,9 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#080d1d" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png" />
|
||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
||||
<title>Deadhead: Space Freight Tycoon</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
"build": "node scripts/map-art.mjs --if-available && vite build",
|
||||
"map-art": "node scripts/map-art.mjs",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"icons": "node scripts/icons.mjs",
|
||||
"e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deadhead/content": "workspace:*",
|
||||
|
|
@ -25,6 +27,7 @@
|
|||
"zustand": "^5.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.63.0",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.3",
|
||||
|
|
@ -35,6 +38,8 @@
|
|||
"jsdom": "^30.1.1",
|
||||
"sharp": "^0.35.5",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"vite": "^8.3.2"
|
||||
"vite": "^8.3.2",
|
||||
"vite-plugin-pwa": "^2.0.0",
|
||||
"workbox-window": "^7.4.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const PORT = 4173;
|
||||
|
||||
/**
|
||||
* Browser tests against the production build (Phase 11): service worker,
|
||||
* install and offline behaviour can't be tested in jsdom. Run `pnpm build`
|
||||
* first; `pnpm e2e` serves dist/ with `vite preview`.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "e2e",
|
||||
testMatch: "**/*.e2e.ts",
|
||||
fullyParallel: false,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
use: { baseURL: `http://127.0.0.1:${PORT}`, ...devices["Desktop Chrome"] },
|
||||
webServer: {
|
||||
command: `pnpm exec vite preview --port ${PORT} --strictPort --host 127.0.0.1`,
|
||||
url: `http://127.0.0.1:${PORT}`,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
|
|
@ -0,0 +1,50 @@
|
|||
// Generates the PWA and browser icons (Step 11.1) from the Courier sprite on the
|
||||
// app's space background. Placeholder art until the user supplies a logo.
|
||||
//
|
||||
// node scripts/icons.mjs writes public/icons/*.png
|
||||
import { mkdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import sharp from "sharp";
|
||||
|
||||
const webRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const outDir = join(webRoot, "public/icons");
|
||||
/** --color-space-900 in src/index.css: the theme and background color. */
|
||||
export const BACKGROUND = "#080d1d";
|
||||
|
||||
/**
|
||||
* Each icon: size in px and how much of it the ship fills. Maskable icons keep
|
||||
* the ship inside the central 80 % safe zone, so they fill less.
|
||||
*/
|
||||
export const ICONS = [
|
||||
{ file: "icon-192.png", size: 192, fill: 0.78 },
|
||||
{ file: "icon-512.png", size: 512, fill: 0.78 },
|
||||
{ file: "maskable-512.png", size: 512, fill: 0.56 },
|
||||
{ file: "apple-touch-icon.png", size: 180, fill: 0.7 },
|
||||
{ file: "favicon-32.png", size: 32, fill: 0.9 },
|
||||
];
|
||||
|
||||
sharp.cache(false);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const source = readFileSync(join(webRoot, "public/assets/map/ship-courier.png"));
|
||||
const trimmed = await sharp(source).trim().toBuffer();
|
||||
|
||||
for (const { file, size, fill } of ICONS) {
|
||||
const box = Math.round(size * fill);
|
||||
const ship = await sharp(trimmed)
|
||||
.resize(box, box, { fit: "inside" })
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
await sharp({
|
||||
create: { width: size, height: size, channels: 4, background: BACKGROUND },
|
||||
})
|
||||
.composite([
|
||||
{
|
||||
input: ship.data,
|
||||
left: Math.round((size - ship.info.width) / 2),
|
||||
top: Math.round((size - ship.info.height) / 2),
|
||||
},
|
||||
])
|
||||
.png({ compressionLevel: 9 })
|
||||
.toFile(join(outDir, file));
|
||||
}
|
||||
console.log(`icons: wrote ${ICONS.length} files to ${outDir}`);
|
||||
|
|
@ -5,5 +5,5 @@
|
|||
"jsx": "react-jsx",
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src", "test", "vite.config.ts", "vitest.config.ts"]
|
||||
"include": ["src", "test", "e2e", "vite.config.ts", "vitest.config.ts", "playwright.config.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,45 @@ import { readFileSync } from "node:fs";
|
|||
import { rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
|
||||
/** --color-space-900: the app's background, used for the splash and browser chrome. */
|
||||
const THEME_COLOR = "#080d1d";
|
||||
|
||||
/**
|
||||
* Installable PWA (Step 11.1): manifest, and a service worker that precaches the
|
||||
* app shell, the self-hosted fonts (CR-005) and the generated map art (CR-003).
|
||||
* API calls are never cached; offline behaviour is Step 11.2.
|
||||
*/
|
||||
const pwa = VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
injectRegister: "auto",
|
||||
includeAssets: ["icons/favicon-32.png", "icons/apple-touch-icon.png"],
|
||||
manifest: {
|
||||
name: "Deadhead: Space Freight Tycoon",
|
||||
short_name: "Deadhead",
|
||||
description: "Run a freight company across the solar system.",
|
||||
theme_color: THEME_COLOR,
|
||||
background_color: THEME_COLOR,
|
||||
display: "standalone",
|
||||
orientation: "any",
|
||||
start_url: "/",
|
||||
scope: "/",
|
||||
icons: [
|
||||
{ src: "/icons/icon-192.png", sizes: "192x192", type: "image/png" },
|
||||
{ src: "/icons/icon-512.png", sizes: "512x512", type: "image/png" },
|
||||
{ src: "/icons/maskable-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
globPatterns: ["**/*.{js,css,html,woff2,png,webp,svg}"],
|
||||
// The source art is only a build input (decision 0.5.37); never precache it.
|
||||
globIgnores: ["assets/map/**"],
|
||||
navigateFallback: "/index.html",
|
||||
navigateFallbackDenylist: [/^\/api\//],
|
||||
cleanupOutdatedCaches: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* The user's source map art (public/assets/map) is large and only an input to
|
||||
|
|
@ -45,7 +84,7 @@ function buildInfo() {
|
|||
|
||||
export default defineConfig({
|
||||
define: buildInfo(),
|
||||
plugins: [react(), tailwindcss(), omitSourceArt()],
|
||||
plugins: [react(), tailwindcss(), omitSourceArt(), pwa],
|
||||
server: {
|
||||
// Same-domain API in development too (decision 0.5.20). The server listens on
|
||||
// IPv4 only, so target 127.0.0.1 rather than localhost.
|
||||
|
|
|
|||
|
|
@ -648,16 +648,16 @@ Steps 10M.1–10M.5 are specified in `memory-bank/change-requests/CR-013-veteran
|
|||
|
||||
### Step 11.1 — Installable PWA
|
||||
- Add `vite-plugin-pwa` with a manifest (name "Deadhead: Space Freight Tycoon", icons, theme color, standalone display) and a service worker that caches the app shell.
|
||||
- **Test:** A Lighthouse PWA audit in CI reports the app as installable. Manual check: the app installs to a phone home screen and launches standalone.
|
||||
- **Test:** A Lighthouse PWA audit in CI reports the app as installable. *(As built: Lighthouse 12+ has no PWA category, so a Playwright test asks Chrome directly — `Page.getInstallabilityErrors`, the check that audit used — and expects none.)* Manual check: the app installs to a phone home screen and launches standalone.
|
||||
- **Amended by CR-003:** The service worker precaches the generated map assets in `public/assets/map-gen/` (not the source art in `public/assets/map/`, which the build omits).
|
||||
- **Amended by CR-005:** The self-hosted font files (`@fontsource` Chakra Petch and Barlow, bundled into `dist/assets`) are part of the offline cache.
|
||||
|
||||
### Step 11.2 — Offline shell behavior
|
||||
- When offline, the app loads from cache and shows the last known state with a clear "Offline — showing last update" banner. In-flight progress bars keep advancing from cached dispatch records. Actions are disabled until back online.
|
||||
- **Test:** Playwright test with network disabled after first load: the app renders, the offline banner shows, progress bars advance, and action buttons are disabled.
|
||||
- **Amended by CR-011:** Settings that need the server (rename, backup, restore, new company, developer tools) are disabled while offline; display and help settings still work.
|
||||
|
||||
---
|
||||
- **Amended by CR-011:** Settings that need the server (rename, backup, restore, new company, developer tools) are disabled while offline; display and help settings still work.
|
||||
|
||||
## Phase 12 — Balance Harness and End-to-End Tests
|
||||
|
||||
|
|
|
|||
|
|
@ -296,3 +296,8 @@ Spec: `memory-bank/change-requests/CR-013-veterans-clipper.md` (received as "CR-
|
|||
**Phase 10M complete — waiting for review.**
|
||||
|
||||
- [x] **Follow-up: Earth launch cost 3 → 2.1** (user decision). Fixes the post-deal fuel shortfall: the fresh-account check (deal, Courier and Clipper in flight together, Clipper Priority delivery unlocking goal 4) passes with no developer top-up. Fuel hand-calculation test updated.
|
||||
|
||||
## Phase 11 — PWA
|
||||
|
||||
- [x] **Step 11.1 — Installable PWA.** `vite-plugin-pwa` 2.0 (generateSW, auto-update): manifest "Deadhead: Space Freight Tycoon" / "Deadhead", standalone, theme and background `#080d1d`, icons 192, 512 and maskable 512 (plus favicon and Apple touch icon in `index.html`). The service worker precaches the app shell, the self-hosted font files (woff2) and all 25 generated map images (64 entries, ~6.7 MB); source art in `assets/map/` is excluded; `/api` is never served from the cache. Icons come from `scripts/icons.mjs` (`pnpm --filter @deadhead/web icons`): the Courier sprite on the space background, placeholder until there is a logo. Playwright is set up for browser tests against the production build (`apps/web/playwright.config.ts`, `e2e/*.e2e.ts`, `pnpm --filter @deadhead/web e2e`), and CI builds the web app, installs Chromium and runs them. Tests (`e2e/install.e2e.ts`): Chrome reports no installability errors; the manifest has the name, standalone display, both icon sizes and a maskable icon, all served; the precache holds index.html, map-gen art and both fonts, and no source art. **Manual check for the user:** install to a phone home screen and launch standalone.
|
||||
- [ ] Step 11.2 — Offline shell behavior
|
||||
|
|
|
|||
3278
pnpm-lock.yaml
3278
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue