diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2e7ac7c..0ffe505c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,9 +59,6 @@ jobs: - name: Check du discipline (no gobj on host, no make-el shim) run: bb scripts/check_du_discipline.bb - - name: Check barebuild boundary (BareDOM <-> BareBuild) - run: bb scripts/check-barebuild-boundary.bb - - name: Build ESM library run: npm run build @@ -124,52 +121,3 @@ jobs: run: npm test env: CHROMIUM_FLAGS: "--no-sandbox" - - demo-e2e: - name: Demo app E2E (Playwright) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Setup Java - uses: actions/setup-java@v5 - with: - distribution: 'temurin' - java-version: '21' - - - uses: actions/setup-node@v6 - with: - node-version: '22' - - - uses: DeLaGuardo/setup-clojure@13 - with: - cli: 'latest' - - - name: Setup Babashka - run: | - curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install - chmod +x install - sudo ./install --dir /usr/local/bin - bb --version - - - run: npm ci - - # barebuild-* is not published to npm yet, so build the ESM library and - # install a local pack OVER the demo app's registry pin (same approach as - # `bb smoke-build`). Once barebuild-* ships, the demo's ^3.x pin resolves - # from the registry and these two steps collapse to a plain `npm install`. - - name: Build ESM library (for the local pack) - run: npx shadow-cljs release lib - - - name: Pack and install the library into the demo app (over the pin) - run: | - TGZ="$(npm pack --silent | tail -1)" - echo "packed $TGZ" - cd barebuild/demo-app - npm install "$GITHUB_WORKSPACE/$TGZ" - - - name: Install Playwright chromium + OS deps - run: cd barebuild/demo-app && npx playwright install --with-deps chromium - - - name: Run demo-app read-path E2E - run: cd barebuild/demo-app && bb e2e diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db0d393f..6ad056c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,15 +25,8 @@ jobs: run: | TAG="${GITHUB_REF_NAME}" VERSION="${TAG#v}" - # Dist-tag from any semver pre-release identifier so a pre-release NEVER moves - # `latest`: 4.0.0-alpha.0 → alpha, 4.0.0-rc.1 → rc, 3.5.0 → latest (no identifier). - DIST_TAG=$(printf '%s' "$VERSION" | sed -n 's/^[0-9.]*-\([A-Za-z]*\).*/\1/p') - [ -z "$DIST_TAG" ] && DIST_TAG=latest - PRERELEASE=false; [ "$DIST_TAG" = latest ] || PRERELEASE=true echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "dist_tag=${DIST_TAG}" >> "$GITHUB_OUTPUT" - echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT" # Added this step to fix UnsupportedClassVersionError - name: Setup Java @@ -84,15 +77,11 @@ jobs: - name: Publish to NPM run: | npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} - npm publish --access public --tag "${{ steps.version.outputs.dist_tag }}" + npm publish --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Build and Publish to Clojars - # Pre-releases (alpha) are npm/ESM-only — skip the Clojure jar. (The jar is for - # source-consuming CLJS apps, not the alpha audience; skipping also avoids - # re-deploying an unbumped jar version, which Clojars rejects.) - if: ${{ steps.version.outputs.prerelease == 'false' }} run: | clojure -T:build jar clojure -X:deploy @@ -106,6 +95,5 @@ jobs: tag_name: ${{ steps.version.outputs.tag }} name: "v${{ steps.version.outputs.version }}" generate_release_notes: true - prerelease: ${{ steps.version.outputs.prerelease == 'true' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CLAUDE.md b/CLAUDE.md index 35e4dd8e..3048dfd6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -202,7 +202,7 @@ Components **must** use shared utility modules — never reimplement locally: - **`component/register!`** — element registration from declarative options map - **`du/setv!`** / **`du/getv`** — host-element instance-field storage (refs, model cache, handlers). The trace-recorder hook lives here; `gobj/set` / `gobj/get` on `el` is forbidden and enforced by `bb scripts/check_du_discipline.bb`. Use `gobj` only on non-host JS objects. - **`du/set-attr!`** / **`du/remove-attr!`** — attribute writes. Goes through the trace recorder. **Exception:** per-frame writes inside `requestAnimationFrame` loops (e.g. `animate!` → `render-*!`) use **`du/set-attr-untraced!`** / **`du/remove-attr-untraced!`** to keep the recorder readable. Even after host-attribution + rate-limiting, a high-fanout animation can emit 60+ records/sec under distinct attribute keys; the untraced variants do the native DOM write without firing the hook. Use only in hot paths, with a one-line `;; Hot path: rAF-driven` comment so the intent is greppable. References: `x_liquid_glass/render-satellites!`, `x_soft_body/render-path!`, `x_liquid_dock/animate!`. -- **`du/setv-untraced!`** — instance-field write without firing the trace hook. Reserved for **bookkeeping with no diagnostic display value**, of two kinds. (1) **Animation bookkeeping** stamped every frame: the canonical trio is **`k-raf`** (requestAnimationFrame id), **`k-last-frame`** (previous frame timestamp), and **`k-time`** (accumulated animation time) — the recorder seeing 60+ writes/sec of these adds noise without adding signal. (2) **Transient non-displayable handles** whose *value* is opaque and uninterpretable in a time-travel trace (and may not be structured-cloneable) — e.g. an in-flight **`AbortController`** (`barebuild-data`'s `k-abort`), where the *meaningful* lifecycle it tracks is already traced through a sibling state field + dispatched event. In both cases add a one-line greppable rationale at the field (mirroring the `;; Hot path: rAF-driven` attr-write convention) so the deviation reads as intentional. Use the normal `du/setv!` for actual UI state (selection, hover, pressed, active-source, …). +- **`du/setv-untraced!`** — instance-field write without firing the trace hook. Reserved for **bookkeeping with no diagnostic display value**, of two kinds. (1) **Animation bookkeeping** stamped every frame: the canonical trio is **`k-raf`** (requestAnimationFrame id), **`k-last-frame`** (previous frame timestamp), and **`k-time`** (accumulated animation time) — the recorder seeing 60+ writes/sec of these adds noise without adding signal. (2) **Transient non-displayable handles** whose *value* is opaque and uninterpretable in a time-travel trace (and may not be structured-cloneable) — e.g. an in-flight **`AbortController`** (`k-abort`), where the *meaningful* lifecycle it tracks is already traced through a sibling state field + dispatched event. In both cases add a one-line greppable rationale at the field (mirroring the `;; Hot path: rAF-driven` attr-write convention) so the deviation reads as intentional. Use the normal `du/setv!` for actual UI state (selection, hover, pressed, active-source, …). - **`du/has-attr?`** / **`du/get-attr`** — attribute reads in `read-model` - **`du/dispatch!`** / **`du/dispatch-cancelable!`** — event dispatch - **`du/install-properties!`** — install property accessors from `model/property-api` (Tier 0; see _Property accessor tiers_ above) diff --git a/README.md b/README.md index f6ffba42..8d91b45c 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ All adapters are auto-generated from the same Custom Elements Manifest, so addin ## Components -**103 UI web components across 11 categories** — from foundational UI controls to morphing animations, organic effects, and scroll-driven storytelling — plus **3 [BareBuild](./barebuild/docs/read-side.md) orchestration elements** (read-side: router / route / data). +**103 UI web components across 11 categories** — from foundational UI controls to morphing animations, organic effects, and scroll-driven storytelling. | Category | Count | Examples | |----------|------:|----------| @@ -118,7 +118,6 @@ All adapters are auto-generated from the same Custom Elements Manifest, so addin | **Effects** | 12 | Liquid Glass · Confetti · Neural Glow · Metaball Cursor · Organic Shape | | **Scroll** | 5 | Scroll · Scroll Parallax · Scroll Stack · Scroll Story · Scroll Timeline | | **Utility** | 2 | i18n · i18n Provider | -| **Orchestration** | 3 | BareBuild Router · Route · Data — read-side; not adapter-wrapped | See [**docs/components.md**](./docs/components.md) for the full per-component catalogue with one-line descriptions and links to each component's API documentation. diff --git a/barebuild/bb.edn b/barebuild/bb.edn deleted file mode 100644 index e5b08882..00000000 --- a/barebuild/bb.edn +++ /dev/null @@ -1,16 +0,0 @@ -{:paths ["cli" "test"] - :tasks - {new {:doc "Scaffold a new BareBuild app: bb new [parent-dir]" - :requires ([barebuild-new]) - :task (apply barebuild-new/-main *command-line-args*)} - test {:doc "Run BareBuild platform tests (fast; no shadow compile)" - :requires ([barebuild-new-test]) - :task (barebuild-new-test/-main)} - smoke-build {:doc "E2E: scaffold an app and compile it against a local pack of barebuild-*" - :requires ([smoke-build]) - :task (smoke-build/-main)} - demo-e2e {:doc "Run the demo-app Playwright e2e from the platform root" - ;; :dir keeps this boundary-clean (no parent-relative path); runs - ;; the demo-app's own `bb e2e`, whose playwright.config boots `bb serve`. - :task (shell {:dir "demo-app"} "bb" "e2e")}}} - diff --git a/barebuild/cli/barebuild_new.clj b/barebuild/cli/barebuild_new.clj deleted file mode 100644 index 72321968..00000000 --- a/barebuild/cli/barebuild_new.clj +++ /dev/null @@ -1,74 +0,0 @@ -(ns barebuild-new - "Scaffold a BareBuild read-side app from templates/app. - - Run from the barebuild/ directory: `bb new [parent-dir]`. - Babashka built-ins only (no external deps), modelled on scripts/release.clj. - - NB: the ns is barebuild-rooted (not `barebuild.cli.new`) on purpose — bb.edn - lives in barebuild/ and its :paths are relative to it, so a `barebuild.*` ns - would need `:paths [\"..\"]`, and the boundary check forbids any `..` here." - (:require [babashka.fs :as fs] - [clojure.java.io :as io] - [clojure.string :as str])) - -(def ^:private token "APP_NAME") -;; Relative to the barebuild/ cwd (bb runs tasks from the bb.edn dir). Avoids any -;; parent-relative path (boundary-check rule) and fragile *file* resolution. -(def ^:private default-template-dir "templates/app") - -(defn- die [& msg] - (binding [*out* *err*] - (println (str "barebuild new: " (apply str msg)))) - (System/exit 1)) - -(defn valid-name? - "An app name usable as both an npm package name and a CLJS ns segment: - hyphen-separated runs of lowercase letters / digits, starting with a letter. - Hyphens must be internal separators — no leading, trailing, or doubled hyphen - (those make an invalid npm name and an ugly munged dir like 'my_app_')." - [s] - (boolean (and (string? s) (re-matches #"[a-z][a-z0-9]*(-[a-z0-9]+)*" s)))) - -(defn- munge-dir - "ClojureScript munges '-' to '_' in file paths; the ns itself keeps the hyphen." - [app-name] - (str/replace app-name "-" "_")) - -(defn scaffold! - "Copy template-dir → target-dir, substituting the APP_NAME token in file - CONTENTS (hyphen name, e.g. my-app) and in PATH segments (munged dir name, - e.g. my_app). Returns the number of files written." - [{:keys [app-name template-dir target-dir]}] - (let [munged (munge-dir app-name) - files (filter #(.isFile ^java.io.File %) (file-seq (io/file template-dir)))] - ;; Template files are UTF-8 text by contract — this slurp/replace/spit round-trip - ;; would corrupt a binary asset. None exist today; add a byte-copy branch here if - ;; one ever does (e.g. a favicon). - (doseq [^java.io.File f files] - (let [rel (str (fs/relativize template-dir (.getPath f))) - out-file (fs/path target-dir (str/replace rel token munged)) - content (str/replace (slurp f) token app-name)] - (fs/create-dirs (fs/parent out-file)) - (spit (fs/file out-file) content))) - (count files))) - -(defn -main [& args] - (let [[app-name parent-dir] args] - (when-not (valid-name? app-name) - (die "invalid app name " (pr-str app-name) - " — hyphen-separated lowercase letters/digits, starting with a letter" - " (e.g. my-app). No leading, trailing, or doubled hyphen.\n" - " usage: bb new [parent-dir]")) - (when-not (fs/exists? default-template-dir) - (die "template dir not found: " default-template-dir - " (run this from the barebuild/ directory)")) - (let [target (str (fs/path (or parent-dir (str (fs/cwd))) app-name))] - (when (fs/exists? target) - (die "target already exists: " target)) - (let [n (scaffold! {:app-name app-name - :template-dir default-template-dir - :target-dir target})] - (println (str "✓ created '" app-name "' (" n " files) → " target)) - (println " next:") - (println (str " cd " target)) - (println " bb dev # installs deps + serves http://localhost:8000"))))) diff --git a/barebuild/cli/smoke_build.clj b/barebuild/cli/smoke_build.clj deleted file mode 100644 index 9e988a52..00000000 --- a/barebuild/cli/smoke_build.clj +++ /dev/null @@ -1,50 +0,0 @@ -(ns smoke-build - "End-to-end release smoke: prove a scaffolded app actually COMPILES against the - real, published-shape barebuild-* ESM — the one thing the fast `bb test` and - the dev demos don't exercise. Run from barebuild/: `bb smoke-build`. - - Flow: build dist (release lib) → npm pack the repo → scaffold an app to a temp - dir → npm install the local tarball OVER the registry pin (no published version - has barebuild-* yet) → shadow-cljs release app → assert public/js/main.js." - (:require [babashka.process :refer [shell]] - [babashka.fs :as fs] - [cheshire.core :as json] - [barebuild-new :as scaffold])) - -(defn- repo-root - "The repo root = the parent of the barebuild/ cwd. fs/parent (not a literal - parent-relative path, which the boundary check forbids in barebuild/ code)." - [] - (str (fs/parent (fs/cwd)))) - -(defn -main [] - (let [root (repo-root) - tmp (str (fs/create-temp-dir {:prefix "bb-smoke-"})) - app-dir (str (fs/path tmp "smoke-app")) - tgz-abs (atom nil)] - (try - (println "→ build dist (release lib)") - (shell {:dir root} "npx" "shadow-cljs" "release" "lib") - - (println "→ npm pack") - (let [out (:out (shell {:dir root :out :string} "npm" "pack" "--json" "--ignore-scripts")) - tgz (-> (json/parse-string out true) first :filename)] - (reset! tgz-abs (str (fs/path root tgz))) - (println " packed" @tgz-abs)) - - (println "→ scaffold smoke-app") - (scaffold/scaffold! {:app-name "smoke-app" :template-dir "templates/app" :target-dir app-dir}) - - (println "→ npm install (local tarball over the ^3.4.0 pin) + shadow build") - (shell {:dir app-dir} "npm" "install" @tgz-abs "shadow-cljs@^3.4.7") - (shell {:dir app-dir} "npx" "shadow-cljs" "release" "app") - - (let [main-js (fs/path app-dir "public" "js" "main.js")] - (if (fs/exists? main-js) - (println (str "✓ smoke-build PASS — scaffolded app compiled against barebuild-* (" - (fs/size main-js) " bytes main.js)")) - (do (println "✗ smoke-build FAIL — no public/js/main.js emitted") - (System/exit 1)))) - (finally - (fs/delete-tree tmp) - (when @tgz-abs (fs/delete-if-exists @tgz-abs)))))) diff --git a/barebuild/demo-app/.gitignore b/barebuild/demo-app/.gitignore deleted file mode 100644 index d90e4a07..00000000 --- a/barebuild/demo-app/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -/node_modules -/.shadow-cljs -/public/js -/test-results -/playwright-report -# Generated fresh each install (CI installs a locally-packed @vanelsas/baredom -# tarball over the pin); never commit a lockfile pinned to a local file: path. -/package-lock.json diff --git a/barebuild/demo-app/README.md b/barebuild/demo-app/README.md deleted file mode 100644 index f9efbaad..00000000 --- a/barebuild/demo-app/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# BareBuild Tasks — Phase 4 demo - -The Phase 4 **read-only E2E demo** for [BareBuild](https://github.com/vanelsas/baredom): -a believable Tasks product built only from the three V1 orchestration elements -(``, ``, ``) plus shipped -BareDOM `x-*` components — native web components, no framework runtime, no virtual -DOM. - -It is also the **write-side telemetry harness**. The read paths work out of the -box; the create/update/delete paths are deliberately left as clearly-marked stub -seams for you to hand-wire (see _Write side_ below). What you write there is the -input that designs the V1.1 `` / `` / -`` contracts — for the candidate shapes see -[`../docs/write-side-sketch.md`](../docs/write-side-sketch.md). - -> **Build status.** This is being built in reviewed steps. This commit ships the -> single `/tasks` board route reading the live `/api/tasks` endpoint from the -> Babashka backend (`server/serve.clj`). Later steps add a `/tasks/:id` detail -> route, a `/settings` route, `x-form` write surfaces, and the e2e harness. - -## Run - -> **One-time setup (until `@vanelsas/baredom` ≥ 3.4.0 is published to npm).** The -> `barebuild-*` orchestration components are not on the registry yet, so this app's -> `^3.4.0` pin cannot resolve from a plain `npm install`. Seed it with a local pack -> first — the exact thing the CI `demo-e2e` job does. From the **repo root**: -> -> ```sh -> npx shadow-cljs release lib # build dist/ -> TGZ=$(npm pack) # → vanelsas-baredom-.tgz -> npm --prefix barebuild/demo-app install "$PWD/$TGZ" # install it over the pin -> ``` -> -> After this, `bb serve` / `bb e2e` work. (A `package-lock.json` is generated by that -> install and is intentionally git-ignored — it would otherwise pin a machine-local -> `file:` path. Once the registry version exists, the pin resolves normally and this -> step disappears.) - -```sh -bb serve # build the app, then serve public/ + the JSON API on http://localhost:3000 -``` - -Open , click **Tasks** — the route activates, fetches -`/api/tasks` from the backend, and renders the result into an ``. - -For hot-reload development use `bb dev` (shadow-cljs on http://localhost:8000), -but note the `/api/tasks` fetch needs the backend — run `bb serve` to see data load. - -## How it works - -- `public/index.html` declares the UI as markup: a `` with - ``s and dormant `` brokers. -- `src/demo_app/core.cljs` registers the components and wires the read side by - hand: set the broker's `src` when the route activates, read the value from - `event.detail.state`, render it. See the comments there and - [the read-side guide](../docs/read-side.md). - -> **Why `querySelector` here?** The app legitimately reaches into its *own* markup -> to grab handles. (The library's internal rule against `querySelector` is about -> *components* never finding their collaborators by selector — a different concern.) -> This hand-wiring is the deliberate V1 starting point; the declarative wiring -> element (``) is V1.1, designed from what this glue teaches us. - -## Write side (telemetry seam) - -The backend already exposes the write endpoints (POST/PUT/PATCH/DELETE — see -`server/serve.clj`). The write *surfaces* (`x-form` in a `New task` modal, the -detail edit/delete, the settings save) ship **built but unwired**: the five -submit→fetch→DOM→invalidate seams live in -[`src/demo_app/write_side.cljs`](src/demo_app/write_side.cljs) and just show a toast -until you fill them. Write what feels natural, then tell us how it differed — that -telemetry is what designs the V1.1 coordination elements. - -- The candidate element shapes (a **prior** to compare against, *not* a spec to - copy): [`../docs/write-side-sketch.md`](../docs/write-side-sketch.md). -- Where the telemetry is recorded (the **evidence** that gates V1.1): - [`../docs/write-side-design-notes.md`](../docs/write-side-design-notes.md). - -## Requirements - -Needs `@vanelsas/baredom` **≥ 3.4.0** (the release that introduces the `barebuild-*` -orchestration components). Pinned in `package.json`. CI installs a locally-packed -tarball over this pin until `barebuild-*` ships to npm. - -## Build - -```sh -bb build # release build to public/js -``` diff --git a/barebuild/demo-app/bb.edn b/barebuild/demo-app/bb.edn deleted file mode 100644 index 81f5b099..00000000 --- a/barebuild/demo-app/bb.edn +++ /dev/null @@ -1,18 +0,0 @@ -{:paths ["server"] - :tasks - {dev {:doc "Install deps and start the dev server on http://localhost:8000" - :task (do (shell "npm install") - (shell "npx shadow-cljs watch app"))} - build {:doc "Production build to public/js" - :task (do (shell "npm install") - (shell "npx shadow-cljs release app"))} - serve {:doc "Build the app, then serve public/ + JSON API on http://localhost:3000" - :requires ([serve]) - :task (do (shell "npm install") - (shell "npx shadow-cljs release app") - (apply serve/-main *command-line-args*))} - e2e {:doc "Run the Playwright read-path smoke. playwright.config boots `bb serve`." - :task (do (shell "npm install") - ;; Browsers for a portable / CI run; cached after first install. - (shell "npx playwright install chromium") - (shell "npx playwright test"))}}} diff --git a/barebuild/demo-app/data/settings.edn b/barebuild/demo-app/data/settings.edn deleted file mode 100644 index 14761ecc..00000000 --- a/barebuild/demo-app/data/settings.edn +++ /dev/null @@ -1,5 +0,0 @@ -;; Seed settings for the demo backend (server/serve.clj). One JSON object served -;; at GET /api/settings; PUT /api/settings merges. Ephemeral (reset on restart). -{:theme "system" - :page-size 25 - :default-status "todo"} diff --git a/barebuild/demo-app/data/tasks.edn b/barebuild/demo-app/data/tasks.edn deleted file mode 100644 index 5a28fd43..00000000 --- a/barebuild/demo-app/data/tasks.edn +++ /dev/null @@ -1,6 +0,0 @@ -;; Seed task data for the demo backend (server/serve.clj). Loaded into an -;; in-memory atom at startup; mutations are ephemeral (reset on restart). -[{:id 1 :title "Draft the read-side guide" :status "done" :assignee "Ada" :due "2026-06-02"} - {:id 2 :title "Wire the board route" :status "doing" :assignee "Alan" :due "2026-06-05"} - {:id 3 :title "Sketch the detail view" :status "todo" :assignee "Grace" :due "2026-06-09"} - {:id 4 :title "Seed the demo backend" :status "todo" :assignee "Ada" :due "2026-06-11"}] diff --git a/barebuild/demo-app/e2e/read-paths.spec.ts b/barebuild/demo-app/e2e/read-paths.spec.ts deleted file mode 100644 index 4bcbb9c7..00000000 --- a/barebuild/demo-app/e2e/read-paths.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -// Read-path E2E smoke for the BareBuild Tasks demo. Drives the real stack -// (built app + `bb serve` JSON API on one origin) and asserts the DOM is a -// projection of server state, that filtering never refetches, and that the -// route's slotted body survives navigation (the identity-preservation invariant). -// -// The WRITE paths (create/update/delete in src/demo_app/write_side.cljs) are wired -// live, but these read-path assertions never trigger them, so the suite stays -// deterministic against the seed data. - -type Task = { id: number; title: string; status: string; assignee: string; due: string }; - -const rowTitles = (page: Page) => - page.$$eval('#tasks-table x-table-row', (rows) => - rows.slice(1).map((r) => r.querySelector('x-table-cell:nth-child(2)')?.textContent ?? '')); - -test('home route is visible at /', async ({ page }) => { - await page.goto('/'); - await expect(page.locator('barebuild-route[path="/"] h1')).toBeVisible(); -}); - -test('board renders rows that match GET /api/tasks', async ({ page, request }) => { - const serverTasks: Task[] = await (await request.get('/api/tasks')).json(); - - await page.goto('/tasks'); - await page.waitForSelector('#tasks-table x-table-row'); - - // 1 header row + one row per task. - await expect(page.locator('#tasks-table x-table-row')).toHaveCount(serverTasks.length + 1); - expect(await rowTitles(page)).toEqual(serverTasks.map((t) => t.title)); -}); - -test('stats reflect status counts', async ({ page, request }) => { - const serverTasks: Task[] = await (await request.get('/api/tasks')).json(); - const count = (s: string) => String(serverTasks.filter((t) => t.status === s).length); - - await page.goto('/tasks'); - await page.waitForSelector('#tasks-table x-table-row'); - - for (const status of ['todo', 'doing', 'done']) { - await expect(page.locator(`x-stat[data-status="${status}"]`)).toHaveAttribute('value', count(status)); - } -}); - -test('status filter options are data-driven from view/statuses', async ({ page }) => { - await page.goto('/tasks'); - await page.waitForSelector('#tasks-table x-table-row'); - - // The