testRemindersExample
the reminders example: a local-first PWA built as a static site, in a browser: its data on the device, opened with no server at all, and a new version found and taken by itself, the data kept
import { CHROME, withPage } from "#codebase/tcmts/cli/test/browser"; import { env, exampleDir, staticHost } from "#codebase/tcmts/cli/test/e2e/examples"; import { strip, tcmBin, tempDir } from "#codebase/tcmts/cli/test/e2e/harness"; import { REMINDERS } from "#codebase/tcmts/fixtures"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; export const testRemindersExample = async () => { if (!CHROME) return console.log("skipped: no Chrome"); const dir = path.join(tempDir("examples"), "reminders"); const setup = spawnSync(path.join(exampleDir(REMINDERS), "setup.sh"), [dir], { env: env(), encoding: "utf8", }); const out = strip(setup.stdout + setup.stderr); assert.equal(setup.status, 0, out); // (its own 3, and the react library's 3, mounted at reminders.lib.react) assert.match(out, /6 passed, 0 failed/); assert.doesNotMatch(out, /warning/, "the example's docs link only to what exists"); const tcm = (...args: string[]) => { const r = spawnSync(process.execPath, [tcmBin(), ...args], { cwd: dir, env: env(), encoding: "utf8", }); const said = strip(r.stdout + r.stderr); assert.equal(r.status, 0, said); return said; }; assert.match(tcm("view", "reminders.App"), /renders in the browser/); assert.match(tcm("view", "reminders.saveReminder"), /runs in the browser/); // a static site, under a path of the site it's on const site = path.join(dir, "site"); const built = tcm( "build", "reminders.App", "--static", "site/apps/reminders", "--base", "/apps/reminders", ); assert.match(built, /server\s+nothing \(the app runs entirely in the browser\)/); assert.match( built, /pwa\s+installable; its service worker keeps \d+ file\(s\) and opens it from them first/, ); assert.doesNotMatch(built, /warning/, "its icons are all an installed app needs"); // a page for each list it links to, rendered as it's built assert.match( built, /wrote reminders\.App as a static site into site\/apps\/reminders \(6 page\(s\)/, ); const root = path.join(site, "apps", "reminders"); for (const f of [ "index.html", "today.html", "flagged.html", "completed.html", "404.html", "sw.js", ".nojekyll", ]) assert.ok(fs.existsSync(path.join(root, f)), `${f} is written`); const page = fs.readFileSync(path.join(root, "index.html"), "utf8"); // (its look linked to, under the base: a file of the site) const sheet = /<div id="app"><link rel="stylesheet" href="\/apps\/reminders\/(_app\/assets\/styles-[a-z2-7]{8}\.css)" data-precedence="default"\/><div class="app" data-open="home">.*Scheduled/s.exec( page, )?.[1]; assert.ok(sheet, "its first paint"); assert.ok(fs.existsSync(path.join(root, sheet)), "its stylesheet is written"); assert.match( fs.readFileSync(path.join(root, "flagged.html"), "utf8"), /<h1 class="title">Flagged<\/h1>/, ); assert.doesNotMatch( fs.readFileSync(path.join(root, "404.html"), "utf8"), /<div id="app"><div/, "any other path renders in the browser", ); assert.match(page, /<title>Reminders<\/title>/); assert.match(page, /var __TCM_BASE__ = "\/apps\/reminders"/); assert.match( page, /<link rel="apple-touch-icon" href="\/apps\/reminders\/_app\/assets\/icon-[0-9a-f]{12}\.png">/, ); const linked = /<link rel="manifest" href="([^"]+)">/.exec(page)?.[1] ?? ""; assert.match(linked, /^\/apps\/reminders\/_app\/assets\/manifest-[0-9a-f]{12}\.webmanifest$/); const manifest = JSON.parse(fs.readFileSync(path.join(site, linked), "utf8")) as Record< string, any >; assert.equal(manifest.name, "Reminders"); assert.equal(manifest.start_url, "../../"); assert.equal(manifest.shortcuts[0].url, "../../today"); assert.equal(manifest.display, "standalone"); assert.deepEqual( manifest.icons.map((i: Record<string, string>) => `${i.type} ${i.sizes} ${i.purpose ?? "any"}`), ["image/svg+xml any any", "image/png 512x512 any", "image/png 512x512 maskable"], ); let host = await staticHost(site); const url = `${host.url}/apps/reminders/`; // (what's shown, with what the fields hold: a reminder's title is one) const text = (sel: string) => `(() => { const e = document.querySelector(${JSON.stringify(sel)}); return e ? [e.innerText, ...[...e.querySelectorAll("input")].map((i) => i.value)].join("\\n") : ""; })()`; try { await withPage(url, async (p) => { // what the device starts with, from its own database assert.match( await p.waitFor<string>(text(".list"), (t) => /Add a reminder/.test(t ?? "")), /Today/, ); assert.equal( await p.waitFor<string>( `navigator.serviceWorker.controller ? "kept" : ""`, (v) => v === "kept", ), "kept", "the service worker keeps the app, and serves the page", ); // a reminder in one line: "tomorrow" and "!" are read await p.evaluate( `(() => { const i = document.querySelector(".new input"); Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set.call(i, "Buy milk tomorrow !"); i.dispatchEvent(new Event("input", { bubbles: true })); })()`, ); await p.waitFor<string>(text(".new"), (t) => /Tomorrow/.test(t ?? "")); await p.evaluate(`document.querySelector(".new").requestSubmit()`); await p.evaluate( `[...document.querySelectorAll(".tile")].find((t) => t.innerText.includes("Flagged")).click()`, ); assert.match( await p.waitFor<string>(text(".list"), (t) => /Buy milk/.test(t ?? "")), /Tomorrow/, "flagged, due tomorrow, and the URL says the list", ); assert.equal(await p.evaluate("location.pathname"), "/apps/reminders/flagged"); // no network at all: the app opens anyway, at the list it was on, with what's on the device await host.close(); await p.evaluate("location.reload()"); assert.match( await p.waitFor<string>(text(".list"), (t) => /Buy milk/.test(t ?? "")), /Flagged/, ); // and a list the build never saw, only on the device: the page that renders any path, from the device await p.evaluate(`location.href = "/apps/reminders/lists/reminders"`); assert.match( await p.waitFor<string>(text(".list"), (t) => /Open it in a second window/.test(t ?? "")), /Reminders/, ); await p.evaluate(`location.href = "/apps/reminders/flagged"`); await p.waitFor<string>(text(".list"), (t) => /Buy milk/.test(t ?? "")); // a new version, built into the same directory and back where it was: the app finds it by itself the next // time it's opened, and says so; reloading takes it, the data kept assert.match( tcm("patch", "reminders.SMART", "--old", `name: "All"`, "--new", `name: "Everything"`), /update/, ); tcm("build", "reminders.App", "--static", "site/apps/reminders", "--base", "/apps/reminders"); host = await staticHost(site, host.port); await p.evaluate("location.reload()"); assert.match( await p.waitFor<string>( text(".status"), (t) => /new version is ready/.test(t ?? ""), 30000, ), /Reload/, ); assert.doesNotMatch( await p.evaluate<string>(text(".home")), /Everything/, "until it's reloaded, the page is the one it was", ); await p.evaluate(`document.querySelector(".status button").click()`); assert.match( await p.waitFor<string>(text(".home"), (t) => /Everything/.test(t ?? "")), /Flagged/, ); assert.match( await p.waitFor<string>(text(".list"), (t) => /Buy milk/.test(t ?? "")), /Buy milk/, ); assert.deepEqual(p.errors, []); }); } finally { await host.close(); } }