Skip to content

feat(demo): dogfood dj-navigate + dj-hook with a playwright regression guard (#1742)#1744

Merged
johnrtipton merged 2 commits into
mainfrom
feat/demo-dogfood-nav-hooks-1742
Jun 6, 2026
Merged

feat(demo): dogfood dj-navigate + dj-hook with a playwright regression guard (#1742)#1744
johnrtipton merged 2 commits into
mainfrom
feat/demo-dogfood-nav-hooks-1742

Conversation

@johnrtipton

Copy link
Copy Markdown
Contributor

Summary

The entire v1.0.2 navigation arc (#1733 zero-wiring route map, #1737 SSR→hydration flash parity, #1738 DjustHooks/dj-hook) was driven by a downstream consumer because examples/demo_project never exercised these paths end-to-end. This PR adds an in-house dogfood flow + a playwright regression guard so a future regression in nav/hydration/hooks red-bars CI internally.

Closes #1742.

What was added

New views/templates (reused nothing — 2 new minimal LiveViews):

  • NavDemoPageAView / NavDemoPageBView (plain djust.LiveView subclasses) at /demos/nav-a/ and /demos/nav-b/, linked by dj-navigate.
  • demos/nav_demo_base.html registers the DemoWidget dj-hook once in the persistent shell; nav_demo_a.html / nav_demo_b.html carry a <canvas dj-hook="DemoWidget" dj-update="ignore">.

The #1733 dogfood (verified locally): the SPA cross-view flow needed NO live_session(), NO get_route_map_script(), and NO context processor. The route map auto-derives from the URLconf and auto-emits via {% djust_client_config %} (already in the demo base <head>). Confirmed window.djust._routeMap contained both /demos/nav-a/NavDemoPageAView and /demos/nav-b/NavDemoPageBView on a live server.

The #1738 dj-hook API (verified against python/djust/static/djust/src/19-hooks.js): window.DjustHooks.DemoWidget = { mounted(), destroyed() }, this.el is the DOM element, registry merges window.DjustHooks. dj-update="ignore" keeps VDOM patches off the canvas.

The 3 playwright assertions (tests/playwright/test_nav_hooks.py)

  1. feat(nav): auto-wire dj-navigate route map (zero-wiring SPA nav) #1733 — no full reload: a window.__nav_sentinel set on Page A survives the dj-navigate click to B AND location.pathname changes to /demos/nav-b/. (A full reload would wipe the sentinel.)
  2. Initial SSR render skips the comment/whitespace normalization that render_with_diff applies → first-hydration morphChildren re-render (flash) #1737 — no hydration flash: a MutationObserver on the [dj-view] root records zero direct-child remove/re-add during first-load hydration.
  3. docs: how to integrate client-side libraries (e.g. Chart.js) via client hooks — inline <script> doesn't run on dj-navigate #1738 — hook survives nav: the mounted() marker is set on initial load AND a fresh mounted() fires on the SPA patch-inserted Page B widget (mount count increments).

Local dogfood-pass evidence (#1060)

Ran the demo server exactly as the CI playwright job does (uvicorn demo_project.asgi:application on :8002) and ran the new test against it:

🔍 #1738 hook on A: {'flag': True, 'count': 1, 'elMarked': True}
🔍 #1737 observed [dj-view] root: True, child churn: 0
🔍 pathname after nav: /demos/nav-b/, sentinel after: 1780783255691 (unchanged)
🔍 #1738 hook on B after SPA nav: {'count': 2, 'elMarked': True}
✅ PASS: SPA sentinel survived, no hydration flash, hook survived nav

Gate-off self-test (#1468): forcing a full reload instead of the SPA nav makes the test FAIL with exit 1 (sentinel after: None) — the #1733 assertion is non-tautological.

GET /demos/nav-a/ and GET /demos/nav-b/ both return 200. manage.py check on the demo is clean for the new files (no new T012/T015/T016/V0xx; T016 stays silent because the URLconf-derived route map is non-empty).

CI wiring

Added .venv/bin/python tests/playwright/test_nav_hooks.py to the playwright-tests job in .github/workflows/test.yml, after the test_draft_mode.py line.

Two-commit shape

  1. feat(demo): ... — demo views/templates/urls + hook JS + playwright test + test.yml wiring (no CHANGELOG).
  2. docs(changelog): ... — CHANGELOG only.

🤖 Generated with Claude Code

johnrtipton and others added 2 commits June 6, 2026 18:02
…ght guard (#1742)

Adds an in-house dogfood of the v1.0.2 navigation arc so a future regression
red-bars CI internally instead of surfacing downstream:

- Two plain djust.LiveView pages (NavDemoPageAView / NavDemoPageBView) linked
  by dj-navigate. The route map auto-derives from the URLconf (#1733) and
  auto-emits via {% djust_client_config %} — NO live_session(),
  get_route_map_script(), or context processor needed.
- A DemoWidget dj-hook (canvas, dj-update="ignore") registered once in the
  persistent shell, following the verified 19-hooks.js contract
  (DjustHooks registry, mounted()/destroyed(), this.el). Mounts on Page A
  and re-mounts on the SPA patch-inserted Page B widget (#1738).
- tests/playwright/test_nav_hooks.py asserts: SPA nav (window sentinel
  survives + pathname changes, no full reload — #1733); no first-hydration
  wholesale [dj-view] child replacement via MutationObserver (#1737); hook
  mounted() fires on initial load AND survives SPA nav (#1738).
- Wires the new test into the playwright-tests job in test.yml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@johnrtipton

Copy link
Copy Markdown
Contributor Author

Stage 11 Adversarial Review — PR #1744 (feat #1742: dogfood dj-navigate + dj-hook, playwright guard)

Verdict: 🟢 APPROVE. The guard's three assertions are non-tautological — empirically gate-off-proven for #1733 and #1738, and code-traced + functionally-probed for #1737. No framework runtime touched. demo-checks clean, all CI green. Two 🟡 hardening notes (neither blocking).

Reviewed at branch HEAD 0fb6157a, git rev-list --count HEAD..origin/main = 0 (not stale). Exactly the 2 expected commits.

Pre-review gates

Per-assertion non-tautology verdict (the load-bearing deliverable)

I ran the demo on :8002 (chromium available locally) and did real gate-off tests.

#1733 SPA-nav sentinel — 🟢 NON-TAUTOLOGICAL (empirically proven).
The sentinel window.__nav_sentinel is set once via page.evaluate after load and is never re-set after navigation (test_nav_hooks.py:56, read again at :131). I gated off by replacing dj-navigate="/demos/nav-b/" with a plain href (full reload) and re-ran:

#1733: window sentinel did not survive navigation — full page reload (before=…279, after=None)

Assertion FAILED exactly as designed (:135). On the real dj-navigate it survives. Non-tautological.

#1738 hook-survives-nav — 🟢 NON-TAUTOLOGICAL (empirically proven; count check is load-bearing).
The Page-B check asserts mounted_b["count"] > mount_count_before (test_nav_hooks.py:149), i.e. mounted() must re-fire on the patch-inserted #demo-widget-b. The implementer correctly gave A and B distinct IDs (#demo-widget-a/#demo-widget-b) → distinct _getHookElId (19-hooks.js:72, fresh per DOM element) → updateHooks treats B as new → mounted() fires + count increments (19-hooks.js:243-255). In my href gate-off, Page B's elMarked was still True (full render mounts it) but count stayed 1, was 1 → the COUNT assertion FAILED. Note: the elMarked check (:154) IS tautological on a full reload (a marker persists), but the count check is what carries the assertion — and it's sound. Demo hook matches the real 19-hooks.js contract (window.DjustHooks.X = {mounted, destroyed}, this.el, dj-update="ignore").

#1737 no-hydration-flash MutationObserver — 🟢 NON-TAUTOLOGICAL for the real symptom (timing analyzed + functionally probed). (This was the subtle one.)

Flakiness — 🟡 low risk, non-blocking

Ran 3 consecutive clean passes locally (3/3 PASS). Navigation uses a proper wait_for_function(pathname==B, 8s) (:120). The two asyncio.sleep (1.5s hydration :81, 1.0s post-nav :128) are fixed waits, not condition waits — generous headroom but a theoretical flake on a heavily-loaded runner. Suggest replacing the 1.5s hydration sleep with wait_for_function('() => window.__demoWidgetMounted'). Consistent with the existing sibling playwright tests; acceptable as-is.

Other findings

Empirical results summary

🤖 Generated with Claude Code

@johnrtipton johnrtipton merged commit d451800 into main Jun 6, 2026
13 checks passed
@johnrtipton johnrtipton deleted the feat/demo-dogfood-nav-hooks-1742 branch June 6, 2026 22:18
@johnrtipton

Copy link
Copy Markdown
Contributor Author

Retrospective — PR #1744 (pipeline-drain v1.0.2-3)

Task: v1.0.2-3 — #1742 (Action Tracker #290): dogfood dj-navigate + a client-hook in the demo with a playwright regression guard.

Quality: 5/5

The guard is empirically non-tautological across all 3 invariants, and the dogfood proved #1733's zero-wiring claim in-house.

What went well

What didn't / follow-up

Process finding

A regression guard's worth = its non-tautology. The reviewer didn't just read the assertions; it gate-off-tested each (href-swap, forced-flash) to prove each would FAIL if the regression returned. For the subtle #1737 timing assertion, it verified the observer is installed early enough to see the real (async WS-mount) flash window. That empirical-canary discipline (#252) is what separates a guard from green-theater.

Verified

RETRO_COMPLETE

johnrtipton added a commit that referenced this pull request Jun 6, 2026
PR #1743 (#1741 test pollution) + PR #1744 (#1742 demo dogfood). Guard
hardening tracked in #1745.

Audit-bypass-reason: docs-only status update via pipeline-drain skill (no retro needed)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tech-debt: dogfood dj-navigate cross-view flow + a client-hook (3rd-party lib) in the demo so demo-checks catches nav/hydration/hook regressions

1 participant