← back to Crazy News Channel Shadowman

screenrecord/DEBUG-REPORT.md

171 lines

# PANDEMONIUM-24 (`crazy-news-channel/index.html`) — screenrecord debug report

Target: `file:///Users/macstudio3/Projects/crazy-news-channel/index.html`
Method: Playwright chromium, 5 runs × distinct click-order combinations, video-recorded,
every action appended to `screenrecord/debug-log.jsonl`. Two full harness executions were
run (see "Harness notes" below) plus one targeted follow-up check; all three are preserved
in the append-only log.

## Highest-leverage fix (do this first)

**`renderStories()` does a full `#storyGrid.innerHTML` rebuild on every escalation tick,
and the tick loop fires this on almost every one of the ~40 staggered background stories
advancing — which detaches a story card's `<a data-open-id>` out from under an in-flight
click.** Confirmed twice, independently, across two separate full 5-run executions
(`run3`, both passes), as a hard `Element is not attached to the DOM` exception — not a
timeout, not ambiguous:

```
run3 (pass A): "open story 'unit4' (for escape test)" -> Element is not attached to the DOM
run3 (pass B): "open story card 'sci-migratory-solar-birds'" -> Element is not attached to the DOM
```

The app already knows this pattern is bad: `updateLiveUpdateBlock()` (used for the *article*
view) is explicitly written to patch one block in place "so no full re-render, no stolen
focus/scroll position" — but `renderStories()` for the *grid* view got no equivalent
treatment, even though the grid is the surface with the most clickable targets and the most
frequent churn (any 1 of 40 stories advancing rebuilds the whole grid). A real visitor whose
click lands during one of these rebuilds can have the click silently swallowed or, per this
evidence, throw the browser-native "node is no longer in the tree" condition. Fix: diff-patch
individual `.story-card` nodes on escalation (or debounce/batch `renderStories()` calls),
the same way the article's live-update block already does.

Secondary, softer evidence of the same race: a cluster of `scrollIntoViewIfNeeded` timeouts
on story-card links throughout runs 0/1/2/4 that did **not** turn out to be this bug on
closer inspection (see below) — but the mechanism (grid replaced out from under a pending
locator) is the same failure class, just resolved by Playwright's retry before hard-erroring.

## What did NOT turn out to be an app defect (important for an honest report)

Chasing down every apparent failure, the large majority of `ok:false` rows across both
passes trace to **three harness bugs in my own test script**, not the application. Naming
them explicitly so they aren't miscounted as PANDEMONIUM-24 defects:

1. **Category filter left non-"all" before later story-card clicks.** `catOrder` walked
   forward ends on `"uncategorized"` (last element of the array); run0 and run2's scripted
   sequences never reset the mood filter back to `"all"` before subsequently trying to open
   specific story cards by id (e.g. `tuesdays`, a politics story). The app correctly hid
   those cards — Playwright correctly couldn't find/scroll to them. Not a bug: my test
   clicked a story that its own prior action had filtered out of the DOM.
2. **Unsafe fallback in the admin-delete test helper.** When the "delete the story I just
   created" locator (matched by headline text) found zero rows — legitimately, e.g. because
   `resetToDefaults()` had already wiped it — my harness fell back to
   `.admin-story-list [data-delete-id]:first()`, which deletes an **arbitrary, unrelated
   story** (in run1, pass B, this silently deleted the real `tuesdays` seed story). All
   subsequent `tuesdays` click failures in that run trace to my harness deleting the wrong
   row, not to the app.
3. **Admin panel not opened before hitting its controls.** The run3 seeded-shuffle block
   and the run4 explicit sequence both called `blockAdminCreate`/`blockAdminReset` without
   first calling `blockAdminOpen()` — clicking/filling controls inside a `hidden` `<section>`
   times out by construction. Confirmed by inspecting the timeline: e.g. pass B run3,
   `admin reset to defaults` failed at exactly 10.000s after the prior action with **no
   `open admin panel` action anywhere before it** in that run.

None of the three above are PANDEMONIUM-24 bugs — they're recorded in the log and named
here so the next reload of this harness (or a human) doesn't re-report them as app defects.

## Confirmed CLEAN: the Escape-key priority / admin+article both-open case

The user's brief specifically called out Escape "from article, from admin," and the app's
own source comment flags this as a fragile case: *"Escape: closes whichever 'layer' is
currently open — admin panel takes priority over the article view **if somehow both are
open**."* Neither of the two full 5-run passes actually exercised the both-open case (run4's
"errored-first" branch fired instead, since prior runs already had errors queued), so I ran
a small targeted follow-up (`run: "X-escape-priority-verify"` in the log,
`rec/escape-priority-verify/escape-priority-verify.mp4`):

1. Open admin panel → `adminHidden:false`.
2. Click a story card while admin stays open → **both `adminHidden:false` AND
   `articleHidden:false` simultaneously** (confirmed: the admin panel and the article view
   are NOT mutually exclusive in this app — they're separate DOM siblings, so this
   combined state is real and reachable by a normal user, e.g. open Admin, then click a
   headline link without closing it first).
3. Escape #1 → admin closes (`adminHidden:true`), article stays open (`articleHidden:false`),
   focus lands on `#adminToggleBtn`. **Matches the documented priority exactly.**
4. Escape #2 → article closes (`articleHidden:true`, hash cleared), focus correctly restored
   to the `<a>` link that opened it (`restoreGridFocus`), not lost to `<body>`.

No state leak, no focus trap. This is a genuinely well-built piece of the app — worth
noting since the source comment reads defensively ("if somehow both are open") as if the
author wasn't fully sure it would hold up; it does.

## Full action tally

- Pass A (initial fixed harness, tight 2.5–4s timeouts): 170 actions, runs 0–4, log at
  `screenrecord/_pass-A/debug-log.jsonl`.
- Pass B (generous 6–10s timeouts, to separate real races from host-load noise — this
  machine showed a load average of ~48 during pass A): 170 actions, runs 0–4, log at
  `screenrecord/debug-log.jsonl` (same file also holds the escape-priority follow-up).
- The same `run3` "Element is not attached to the DOM" failure reproduced in **both**
  passes despite the 2.5×–4× more generous timeout in pass B — that consistency, not the
  raw failure count, is what elevates it from "flaky test" to "confirmed app race."
- An aborted zero-th attempt (killed mid-run due to a harness bug that produced 30s
  cascading timeouts) is preserved for reference at `screenrecord/_aborted-attempt0/`.

## Order-dependence verdict

Per-order breakdown of what actually surfaced:

| Run | Order strategy | Real app issue? | Notes |
|---|---|---|---|
| run0 | DOM order | No | 1 harness bug (category left filtered) |
| run1 | Reverse | No | 1 harness bug (bad admin-delete fallback wiped the wrong story) |
| run2 | Toggles/klaxon-first | No | 1 harness bug (category left filtered) — but note klaxon rapid on/off/on/off ×4 + sound-toggle interplay was clean, no siren/interval leak observed (`state.klaxonAudioTimer` correctly cleared each toggle) |
| run3 | Seeded shuffle (seed 3) | **Yes — the DOM-detach race** | Reproduced in both passes; also this run's shuffle happened to skip `blockAdminOpen()` (harness bug, separate issue) |
| run4 | Errored-first replay | No | Inherited run3-era harness bugs (category-filter, admin-not-opened) by re-testing the same broken selectors; klaxon toggle at the very end was clean |

The DOM-detach race is best characterized as **session-duration/timing-dependent rather
than strictly click-order-dependent**: `state.elapsedSec` (and therefore how many of the 40
staggered stories have ticked) advances on a real 1-second wall clock via `setInterval`
regardless of what order the test clicks things in. It surfaced in `run3` in both passes
most likely because run3 is far enough into each session (3 prior full browser
contexts... no — each run is a fresh context, so more precisely: run3's own scripted
sequence takes long enough, combined with dense escalation activity from many
concurrently-staggered stories, to land a click during an active rebuild window). Any
click order that takes long enough before reaching the story grid — admin-heavy or
category-heavy sequences — increases exposure to this window.

## Cross-reload debug trail (how to reproduce the confirmed defect)

1. Load `index.html` fresh (any category, any prior state — doesn't matter, this is a
   background-timer race, not a state leak).
2. Let ~50–90 seconds of wall-clock elapse with the tab in foreground (or programmatically
   drive several unrelated interactions first) so multiple of the 40 background stories are
   actively escalating.
3. Click (or Playwright-locate-then-click) any `[data-open-id]` story-card link right as an
   escalation tick lands — `tick()` runs every 1000ms and calls `renderStories()` whenever
   `anyStoryAdvanced` is true, which given 40 staggered stories is most seconds in the first
   ~60–90s of a session.
4. Expected: click opens the article. Observed (intermittently, evidenced twice): the click
   target is destroyed mid-action (`Element is not attached to the DOM`) because
   `#storyGrid.innerHTML` was just fully replaced.

## Artifacts

- `screenrecord/debug-log.jsonl` — pass B (170 actions) + the escape-priority follow-up,
  append-only, canonical for future reloads.
- `screenrecord/_pass-A/debug-log.jsonl` — pass A (170 actions), kept for the cross-pass
  reproduction evidence cited above.
- `screenrecord/_aborted-attempt0/` — killed zeroth attempt (harness-timeout cascade), kept
  for completeness, not analyzed further.
- Recordings (`.webm` originals + `.mp4` converted via ffmpeg, pass B):
  - `screenrecord/rec/run0/run0.mp4` — DOM order
  - `screenrecord/rec/run1/run1.mp4` — reverse order
  - `screenrecord/rec/run2/run2.mp4` — toggles-first
  - `screenrecord/rec/run3/run3.mp4` — seeded shuffle (contains the confirmed detach race)
  - `screenrecord/rec/run4/run4.mp4` — errored-first replay
  - `screenrecord/rec/escape-priority-verify/escape-priority-verify.mp4` — targeted
    admin+article-both-open Escape-priority confirmation (clean pass)
- `scripts/screenrecord.js` — the harness itself (kept in the repo per the "everything is
  gitified" standing rule).

## Coverage note (bounded, not skipped silently)

The channel renders 40 breaking-story cards + 4 feature cards (44 total `[data-open-id]`
targets). Each run tested a spread sample of 4 (first / ~1/3 / ~2/3 / last of the current
DOM order) rather than all 44, to bound runtime — logged explicitly in the JSONL
(`"skipped 40 of 44 story cards..."`) each run. All 8 category chips, the klaxon toggle, the
sound-toggle checkbox, the full admin CRUD surface (create w/ validation, delete, reset), the
admin open/close toggle, and both Escape-key exit paths were exercised in every run, not
sampled.