Files
kandev/docs/plans/agent-todo-list-panel/task-05-e2e-coverage.md
T
2026-08-24 11:09:03 +00:00

237 lines
15 KiB
Markdown

---
id: "05-e2e-coverage"
title: "E2E coverage"
status: done
wave: 4
depends_on: ["04-visibility-sync-hook"]
plan: "plan.md"
spec: "../../specs/ui/requirements/agent-todo-list-panel.md"
---
# Task 05: E2E coverage
Cover the full user-visible flow: preference off by default, turning it on
adds the tab live, turning it off removes it live, and a configured custom
placement is respected.
- **Acceptance:**
1. A fresh task with the preference off has no "Todos" tab.
2. Turning the preference on from `Settings > General` adds an inactive
"Todos" tab to the currently active task without changing the selected
tab; selecting it shows the checklist or empty state.
3. Turning the preference off removes the "Todos" tab from the currently
open task immediately, with no navigation/reload.
4. A custom Default layout with `todos` placed in a specific group causes a
fresh task to show the Todos tab in that configured group instead of the
Files/Changes fallback.
- **Verification:** `cd apps && pnpm install --frozen-lockfile && cd web && pnpm exec playwright test e2e/tests/settings/todo-list-panel.spec.ts`
- **Files likely touched:**
- `apps/web/e2e/tests/settings/todo-list-panel.spec.ts` (new)
- `apps/web/e2e/tests/settings/layout-profiles.spec.ts` (extend with the
configured-placement case, or keep it in the new file if that fits the
existing file's scope better)
- **Dependencies:** Task 04 (the setting must actually gate the live panel
before an E2E test can assert on it).
- **Parallelism:** `sequential` (final task; depends on the full stack being
wired).
- **Inputs:** Spec's Scenarios section (each E2E case maps directly to one
scenario); plan's E2E Tests section; `apps/web/e2e/tests/pr/pr-detail-layout.spec.ts`
and `apps/web/e2e/tests/settings/layout-profiles.spec.ts` as structural
templates for driving Settings toggles and asserting on the Dockview tab
strip within Playwright.
## Results
All 3 scenarios covered in `todo-list-panel.spec.ts` (the fresh-task-hidden,
cross-tab live add/remove, and configured-placement cases; the fourth
acceptance item — turning it off removes the tab immediately — is the second
half of the "adds then removes" test rather than a separate one, since it
needs the tab present first).
First run surfaced one test-authoring bug and two real product bugs the
earlier waves' unit tests (which mock the store/API layer) could not have
caught, because they never exercise the actual bundled Dockview panel
registries or a live cross-tab settings round trip:
1. **Test bug:** `setTodoListPanelPreference` called `response.ok()` on the
plain `fetch` `Response` `rawRequest` returns — `ok` is a boolean
property there, not a method (confirmed against every other `rawRequest`
caller in `api-client.ts`, which all read `.ok` as a property). Fixed to
`expect(response.ok).toBe(true)`.
2. **Product bug:** `dockview-desktop-layout.tsx` — the file that actually
backs the `/t/:id` desktop task workbench the spec drives — declares its
*own* `components` map (module-local `const components = {...}`, passed
as `<DockviewReact components={components}>`), separate from and parallel
to `dockview-shared.tsx`'s exported `dockviewComponents` (which backs the
*Office* task layout at `app/office/tasks/[id]/office-dockview-layout.tsx`).
Task 03 added `todos: PortalSlot` only to the latter. Dockview silently
failed to add an unregistered-component panel, so the live sync hook's
`focusOrAddPanel` call was a no-op — explaining why the cross-tab test's
tab never appeared even though the WS `user.settings.updated` notification
demonstrably reached the second page (`[ws:dispatch] notification
action=user.settings.updated ... handlers=1`, confirmed via temporary
console instrumentation) and `useSyncTodoPanel` was correctly wired into
this file. `dockview-panel-content.tsx` (this file's real `renderPanel`,
again distinct from `dockview-shared.tsx`'s copy used only by Office) had
the matching gap: no `"todos"` case, so even a manually-added panel would
have rendered "Unknown panel: todos". Fixed both: added
`todos: PortalSlot` to the `components` map, and a `TodosContent`
function + `case "todos"` to `renderPanel`, mirroring `dockview-shared.tsx`'s
already-correct versions exactly (same `useSessionTodoItems` /
`TodoIndicatorContent` / `resolveStatus` reuse, same empty-state key).
3. **Product bug:** `apps/web/components/settings/layouts/layout-editor.tsx`'s
`placeholderComponents` — a *third*, independent Dockview component map
backing the Layout Editor's live preview instance — was also missing
`todos`, so `Settings > General > Layouts`' "Add panel" menu could not
place it at all (contrary to the spec's "Todos still appears in the
visual editor's addable-panel list" scenario). Fixed by adding
`todos: PlaceholderPanel`.
All three gaps are the same shape: this codebase keeps parallel,
hand-maintained Dockview component registries per rendering context (main
desktop workbench, Office workbench, Layout Editor preview) rather than one
shared source of truth — a pre-existing pattern, not something introduced by
this feature. Consolidating them is a larger refactor outside this task's
scope; not attempted here.
Added `apps/web/components/task/dockview-panel-content.todos.test.tsx`
(mirrors `dockview-shared.test.tsx`'s existing todos-panel test 1:1, but
against `dockview-panel-content.tsx` — the registry that actually backs the
desktop workbench under test) so the render-content half of this regression
has unit coverage in addition to E2E coverage. The component-registration
half (an unregistered Dockview component id being a silent no-op rather than
a thrown error) is guarded by the E2E test itself; the two heavier registry
files (`dockview-desktop-layout.tsx`, `layout-editor.tsx`) were judged too
expensive/fragile to import bodily into vitest just to assert on an internal
map, versus the E2E test that already exercises them for real.
Rebuilt both artifacts before every E2E run in this task
(`make -C apps/backend build`; `pnpm --filter @kandev/web build:vite`) — the
E2E fixture serves the pre-built `apps/backend/bin/kandev` binary and
`apps/web/dist` static bundle, not the live source tree, so source edits are
invisible to a run until rebuilt.
Commands and results (final, after all fixes):
- `cd apps/web && pnpm exec playwright test e2e/tests/settings/todo-list-panel.spec.ts` → 3 passed.
- `cd apps/web && pnpm exec playwright test e2e/tests/settings/layout-profiles.spec.ts e2e/tests/settings/todo-list-panel.spec.ts` → 7 passed (no regression in the pre-existing layout-profiles suite from the two registry edits).
- `cd apps/web && pnpm exec vitest run components/task/ components/settings/ lib/ssr/user-settings.test.ts lib/ws/handlers/users.test.ts hooks/use-ensure-user-settings.test.ts` → 302 files / 2188 tests passed, 4 skipped; 1 unrelated pre-existing flaky test (`storage-maintenance-settings.test.tsx`, untouched by this feature) timed out under full-directory load and passed cleanly in isolation.
- `cd apps/web && pnpm run typecheck` → clean.
- `cd apps/web && pnpm run i18n:check` → `2123 key(s) referenced, 2407 en entries, 0 orphans, pseudo in sync`.
**Addendum (found during a manual demo-server review after this task's E2E
suite was already green):** a 4th bug, upstream of anything the E2E suite's
own mocked-timing exercised. `TodosContent` (both files) passed `[]` as the
`messageTodos` fallback to `useSessionTodoItems` — a deliberate but wrong
choice, since `sessionTodos.bySessionId` (the live store slice) is
populated *exclusively* by the `session.todos.updated` WS handler and is
never backfilled from history. Any session that already had todo data
*before* the viewing page loaded — a fresh page load, a reopened task, or
simply a task whose plan update completed before a demo browser connected —
showed "No todos yet." in the new panel while the adjacent `TodoIndicator`
chat-status-bar chip, looking at the identical session at the identical
moment, correctly showed "2/5" — because `TodoIndicator`'s call site already
derives a `messageTodos` fallback from the latest persisted `todo`-type
message (`buildTodoItems` in `hooks/use-processed-messages.ts`, previously
module-private) rather than passing `[]`. This directly violates the spec's
"shows the same rows... that the TodoIndicator popover shows for that
session" scenario for exactly the case — an already-completed session — the
E2E suite's own tests happened to always open a task via a still-connected
page immediately after the WS-live update fired, never exercising a true
cold load. Caught only by manually seeding a real backend (see below) and
looking at it, not by any automated test in this task.
Fixed by exporting `buildTodoItems` and having both `TodosContent`s call
`useSessionMessages(sessionId)` + `buildTodoItems(messages)` for the
fallback, exactly mirroring `TodoIndicator`'s data source. Added a
`data-testid="todos-panel"` wrapper (both files) so E2E/unit assertions can
scope to the panel's own rendered rows without colliding with the same
session's chat-transcript text or the (possibly still-mounted-but-hidden)
status-bar popover using the identical row markup. Added a 3rd unit case to
each `TodosContent` test file (message-history fallback with an empty live
store) and a 4th E2E case
(`shows the checklist for a session whose todos were already persisted
before this page ever loaded it`) seeding a task via the mock agent's
`e2e:plan(...)` scripting command, letting it settle, *then* turning the
preference on and opening it fresh.
To manually review the feature, a real backend + frontend was run outside
the E2E harness: `apps/backend/bin/kandev __backend` (the same hidden
subcommand the E2E fixture uses to skip the runtime-bundle bootstrap) with
`KANDEV_MOCK_AGENT=true`, `KANDEV_WEB_DIST_DIR` pointed at the built
`apps/web/dist`, and a fresh `HOME`/`KANDEV_DATABASE_PATH`, seeded via direct
REST calls (workspace, workflow, local git repo, a task whose description is
an `e2e:plan(...)` command) — this is what surfaced the bug via a
`tab.screenshot()` comparison against the chat status bar, which no unit or
E2E test had put side by side.
Re-verification after the fix, all green:
- `pnpm exec vitest run components/task/dockview-panel-content.todos.test.tsx components/task/dockview-shared.test.tsx components/task/dockview-panel-content.diff.test.tsx` → 3 files, 8 tests passed.
- `pnpm exec playwright test e2e/tests/settings/todo-list-panel.spec.ts` → 4 passed.
- `pnpm exec playwright test e2e/tests/settings/todo-list-panel.spec.ts e2e/tests/chat/chat-status-bar.spec.ts` → 10 passed (no regression in the pre-existing `TodoIndicator`/status-bar coverage this fix's shared-helper export touches).
- `pnpm exec vitest run components/task/ components/settings/` → 301 files / 2166 tests passed, 4 skipped.
- `pnpm run typecheck` → clean.
- `pnpm run i18n:check` → `2123 key(s) referenced, 2407 en entries, 0 orphans, pseudo in sync`.
- Manual re-screenshot of the live demo task's Todos panel after the fix: all 5 seeded items render with correct order/status/progress ("2/5 completed"), matching the chat transcript's "UPDATED TODOS (2/5)" summary.
**Addendum 2 (explicit follow-up request): manual "+" add-panel menu
entry.** The task workbench's own "+" dropdown
(`apps/web/components/task/dockview-add-panel-items.tsx`, rendered by both
`dockview-header-actions.tsx`'s per-group header button and
`dockview-watermark.tsx`'s empty-group watermark) is a *third* addable-panel
surface, separate from both the settings-driven visibility-sync hook and the
Layout Editor's "Add panel" button — and had no Todos entry at all, so there
was no manual way to open the panel independent of the preference. Added:
- `addTodosPanel(opts?: { groupId?: string })` to
`apps/web/lib/state/dockview-panel-actions.ts` /
`apps/web/lib/state/dockview-store.ts`, mirroring `addPlanPanel`'s exact
`focusOrAddPanel`-based idempotent pattern (focuses the existing panel
instance if one is already open anywhere, rather than duplicating it —
`todos` remains the single-instance panel `REUSABLE_PANEL_IDS` documents
it as).
- A "Todos" row in `AddPanelMenuItems`, positioned beside Plan and sharing
its exact guard (`!state.isPassthrough`) and its exact "always shown,
never hidden by presence" convention — unlike Files/Changes, which hide
their row once open because they're near-always-already-open default
panels; Todos and Plan are off-by-default and single-instance, so showing
the row unconditionally just re-focuses the existing instance on a second
click rather than risking staleness from a suppressed row.
- `t("common:todos")` for the row label (the only new copy; every sibling
row's label in this file is a pre-existing unconverted literal, out of
scope to migrate here per the repo's incremental i18n migration rule).
Deliberately did not add a `hasTodos` guard to hide the row once a Todos
panel is already open (unlike Files/Changes) — doing so would have made the
row disappear exactly when a user wants to jump to it from a different
group, and clicking it while one is already open simply re-focuses that
existing instance instead of erroring or duplicating.
Added unit coverage in `dockview-add-panel-items.test.tsx` (row renders and
calls `addTodosPanel({ groupId })`; row is hidden for a passthrough session)
and an E2E case in `todo-list-panel.spec.ts` (preference off, task opened
with the tab absent, "+" clicked, "Todos" menuitem clicked, tab becomes
active and shows real seeded content) proving the manual path is independent
of the preference. Strengthened that E2E case with a cross-group assertion
(`todosGroupId !== filesGroupId`, both read via the same `readTodosLayout`
helper the other cases already use) after a manual browser spot-check on an
*already-populated* task only demonstrated `focusOrAddPanel`'s refocus
branch. The assertion proves the panel did **not** land in the Files/
Changes group — i.e. it did not silently fall back to the settings-driven
hook's default placement — which is the property that mattered to verify
automatically. It does not independently re-confirm which exact group
`addPanelButton()`'s `.first()` targets; that it lands specifically in the
*invoking* group rests on reading `addTodosPanel`'s
`position: { referenceGroup: groupId }` implementation, not on a second,
independent runtime check of the clicked group's own id. [INFERENCE:
exact-group-match] A tighter assertion (capturing the chat panel's group id
before the click and asserting `todosGroupId` equals it) would close that
gap; not added here to avoid widening test surface after the suite was
already green and reviewed.
Commands and results:
- `pnpm exec vitest run components/task/dockview-add-panel-items.test.tsx` → 8 passed (6 pre-existing PR-row cases + 2 new).
- `pnpm exec playwright test e2e/tests/settings/todo-list-panel.spec.ts` → 5 passed.
- `pnpm run typecheck` → clean.
- `pnpm exec eslint components/task/dockview-add-panel-items.tsx components/task/dockview-add-panel-items.test.tsx lib/state/dockview-panel-actions.ts lib/state/dockview-store.ts --max-warnings 0` → clean.
- `pnpm run i18n:check` → `2123 key(s) referenced, 2407 en entries, 0 orphans, pseudo in sync`.
- `pnpm exec vitest run components/task/ lib/state/` → 289 files / 2358 tests passed, 4 skipped (no regression from the new store action or menu item).