Skip to content

Document the testing process and tighten tester rules#108

Merged
thetechjon merged 23 commits into
devfrom
claude/testing-process-tester-rules-olTVy
Jun 7, 2026
Merged

Document the testing process and tighten tester rules#108
thetechjon merged 23 commits into
devfrom
claude/testing-process-tester-rules-olTVy

Conversation

@ipapakonstantinou
Copy link
Copy Markdown
Owner

What

Adds a single source of truth for how testing works in noteser and the rules every tester (human or subagent) follows, then rewrites the two test-running subagent definitions to defer to it.

  • docs/testing.md (new) — the three test layers (Jest unit, Playwright E2E/parity, live sync harness), commands, per-layer conventions, 10 binding rules, and a copy-paste templates appendix (pure util / store+fake-timers / hook+mock / component+userEvent; canonical parity spec / CodeMirror editor / tasks query block / GitHub-vault seeding / DataTransfer drag-drop).
  • .claude/agents/tester.md — rewritten with the concrete Jest conventions: boundary-only mocking, idb-keyval mock placement, Zustand setState isolation, jsdom-vs-node env, fake timers, describe/test naming.
  • .claude/agents/qa-tester.md — rewritten with the Playwright conventions: setupCleanVault + waitForTestHooks bootstrap, store-API seeding via window.__noteser_test, locator priority, CodeMirror typing, DataTransfer drag-drop, no-flaky-wait rule, artifact citing. Keeps the Obsidian-parity mission and the structured report format.
  • Cross-links from CONTRIBUTING.md and CLAUDE.md.

Why

The testing conventions were established in practice across ~165 unit tests and ~80 parity specs but never written down. The two tester subagents each carried a partial, drifting copy. This centralizes the process and makes the rules explicit and grounded in the actual codebase patterns.

How tested

Docs-only change — no code touched. Existing npm test / npm run e2e behavior is unchanged. Templates are modelled line-for-line on existing tests (toastStore.test.ts, useGitHubSync.test.ts, settingsPrimitives.test.tsx, tasks-query-block.spec.ts, live-preview-headings.spec.ts, drag-note-into-folder.spec.ts).

https://claude.ai/code/session_01Rf9F7stxAk2WxNavwsH4Ku


Generated by Claude Code

ipapakonstantinou and others added 23 commits May 30, 2026 17:50
20 commits since the last main promotion (2026-05-25). Brings the
following to prod (noteser.app):

Launch-day fixes (today):
- iOS keyboard lift via visualViewport on the mobile formatting toolbar
- Feature-tour attachments self-heal in AttachmentImage on IDB miss
- Chrome Android autofill row suppressed via autocomplete=off on CM
- 'Coming from Obsidian?' CTA copy restored

Welcome demo GIFs (today + earlier this week):
- 5 GIFs embedded in WelcomePane: connect flow, git interface,
  calendar / daily notes, task queries, iPhone layout
- Rebrand: 'Your second brain in the browser' hero

Stale-branch finish-and-merge (today):
- Vault change-passphrase flow (Phase B addendum) + e2e parity spec
- ContextMenu hook-order fix (Gist publish flow)
- Zipball pull lazyload test coverage + post-merge content-length stub
- GitHub sync hardening plan doc
5 commits since main 7d182a4:

- security(git-proxy): close the only proxy route without origin +
  rate-limit guards. Net effect: noteser.app is no longer an open
  anonymising CORS proxy to GitHub. Per the external review received
  this evening; correctly bounded as infra-abuse / bandwidth-amp risk
  (not token theft, not SSRF).

- Mobile toolbar journey (build, tune, remove): added the Obsidian-
  parity pill with 8 actions, tuned the VisualViewport lift twice
  trying to clear the iOS accessory pill, then removed the toolbar
  render entirely per Jon. iOS Safari's input-accessory pill cannot
  be hidden from a web app; stacking our own bar above it was strictly
  worse. Component file retained for a future native wrap (#26 Tauri).
- Static-source guard now pins direct .innerHTML setter assignments
  alongside the existing dangerouslySetInnerHTML / rehype-raw bans.
  ESLint rule path was attempted but FlatCompat + next/core-web-vitals
  silently dropped the custom rule blocks; the markdownXssGuard test
  has the same blast radius and runs in CI.

- SECURITY.md gains an Audit log section with the git-proxy fix and
  this expansion documented. Append-only, gives future reviewers a
  paper trail.
docs/roadmap.md now reflects the 2026-05-25 to 2026-05-30 stretch and
the launch GTM thread (r/ObsidianMD ban, r/PKMS approval, queued
venues). Last refresh was 2026-05-23 and was stale on everything.
HSTS includeSubDomains + drop X-Powered-By: Next.js. Two-line change
to next.config.mjs, no app-level impact.
Adds @vercel/analytics 2.0.1 and mounts <Analytics /> in layout.tsx
so prod starts recording referrer + visitor data. Dashboard toggle
already flipped on by Jon (only he has admin). Free tier 2,500
events/month on Hobby — comfortably covers a launch-week burst.
Three drops in one promote:

1. PR #37 (ded-furby) — README contributing section
2. PR #36 (MFA-G) — lenient "done today" task query setting
3. Vercel Analytics custom events: note-created, sync-configured,
   sync-success (per-session dedup via sessionStorage)
4. Buttondown-backed /api/subscribe + EmailSignup component on
   WelcomePane and Settings → About. BUTTONDOWN_API_KEY already in
   Vercel env for production/preview/development.

All 153 test suites passing locally; CI green on the underlying PRs.
…play

Plugin platform v1 in full:
  - manifest validator + Worker bridge + capability-mediated PluginCtx
  - command palette / sidebar Plugins tab / code-block renderer adapters
  - Settings → Plugins URL-paste installer with SHA-256 integrity check
  - persistence in IndexedDB + bootstrap-on-reload
  - @noteser/plugin-sdk package staged at packages/
  - noteser-word-count reference plugin hosted at /plugins/
  - /help/plugins docs page
  - awesome-noteser curated list at github.com/thetechjon/awesome-noteser

Two production-bug fixes since the merge to dev:
  - useShallow on the plugin renderer selector to stop a Zustand v5
    infinite-loop that white-screened the dev preview
  - wireActiveNoteTracker subscribes to workspace + note + folder
    stores and pushes activeNoteChanged with FULL content so plugins
    actually see the current note

About panel now shows the real version + short git SHA.

Plugin platform end-to-end verified on dev: noteser-word-count
installed from URL, rendered the live word count of the active note,
updated when switching notes.
…derer

Second reference plugin exercises the code-block renderer surface in
production. Claims five fence languages: note / info / tip / warn /
danger. Optional first-line override "title: My title".

Shared VNode mapping extracted to src/plugins/PluginVNode.tsx. Adds
the VNodeCallout shape with curated colors. PluginsPanel and
PluginCodeBlock delegate through one mapper — one audit surface.

Install on prod:
  https://noteser.app/plugins/noteser-callout/manifest.json

Awesome-noteser updated with the new entry.
Promote dev → main: leaf-model sidebar + calendar week + many UX fixes (52 commits)
* fix(preview): nested undone task no longer struck through under done parent

Reading-mode put text-decoration: line-through on the outer <li>, so the
strike line painted across every descendant text — including an undone
nested sub-task — even when the descendant set text-decoration: none
(modern browsers still draw the ancestor's strike across the descendant's
box). The ListItem renderer now splits a done item's React children into
its own content and any nested <ul>/<ol> sub-lists, wrapping ONLY the own
content in a .preview-task-done-line span sibling of the nested list.
Sibling boundary is the only reliable separator: the strike line stops at
the span's box edge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(editor): explicit drawSelection() so selection layer can't disappear

@uiw/react-codemirror's basicSetup currently enables drawSelection() by
default, so the .cm-selectionBackground layer renders and obsidianTheme
paints it with var(--obsidian-highlight). A future basicSetup change that
drops drawSelection from defaults would silently regress to the native
::selection path — on past builds that combination produced a near-
invisible selection. Add drawSelection() to the extensions array
explicitly so the layer is guaranteed regardless of basicSetup, and add a
static source-text regression test that pins the import + call site.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(plugins): manifest-preview modal with capability prose (#43)

Show users what they are about to install before they click Install.
The modal now fetches + validates the manifest itself, so fetch /
schema errors surface inline instead of bouncing back to the Plugins
panel with a one-line toast. Preview pane lists every requested
capability (surfaces + permissions) with a human-readable description
sourced from manifest.ts.

Adds optional `description` + `homepage` fields to the manifest schema
(validated; homepage gated to https / localhost). Schema tests cover
both. Modal tests cover loading / preview / error / Install / Cancel
paths and the legacy pre-fetched-record entry shape.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(editor): make CodeMirror selection visible in every theme

Split `--obsidian-highlight` into two tokens: a hover surface and a
new `--obsidian-selection` painted into `.cm-selectionBackground` and
`::selection`. Each preset now declares its own selection colour, all
clearing >= 2:1 against the editor background and >= 4.5:1 against
the editor text. Regression test pins the contrast floor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(plugins): scan vault for in-vault manifest.json (#43)

Adds a "Scan vault for plugins" button to Settings → Plugins that walks
the live note store for notes titled `manifest.json` whose body parses
as a valid plugin manifest. Each candidate renders inline with name +
version + path-in-vault and an Install button that hands the assembled
InstalledPluginRecord to the existing plugin-install-confirm modal —
same confirmation screen the URL flow uses.

Composition with the install flow: kept the modal's data contract
(`{ record }`) unchanged. The vault path fetches the bundle via the new
fetchPluginFromManifest installer helper, then opens the modal with the
record. No new modal data fields, no in-vault bundle parsing, no
duplication of the permissions UI.

* feat(sidebar): drag-to-pin gesture on mobile note rows

Horizontal pointer-drag past 60px on a note row toggles its pinned state,
matching the iOS Mail / Things swipe idiom. Pure threshold logic lives in
src/utils/swipePin.ts; SwipePinRow wraps each row so the hook keeps stable
identity across parent renders. Vertical-dominant motion releases to the
list scroll so the gesture cannot fight the off-canvas drawer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(pwa): brand install banner, move iOS hint to Settings → About

Voice fix per launch-week copy rules: "Install noteser" replaces the
generic "Install" button + "Install Noteser for offline use." banner.
iOS Safari has no install API, so the manual Share → Add to Home Screen
instructions move from a screen-stealing inline tip to a dedicated panel
in Settings → About, surfaced only to non-standalone iOS Safari sessions.

Smoke test on public/manifest.json asserts the shape Chrome / iOS rely
on (name, 192 / 512 icons, maskable 512, theme matches obsidian palette,
icon files exist on disk).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* perf(boot): yield to main between plugin loads + during migration

iOS Safari's watchdog kills any task whose main-thread work exceeds its
window — long synchronous boot work (plugin bootstrap, legacy data
migration) can blow past it on a cold launch. New bootTrace.ts wraps
performance.mark / measure and exposes yieldToMain + forEachWithYield
helpers built on the Scheduler API with a setTimeout fallback.

Two hot paths now yield:

- bootstrapInstalledPlugins fans the SubtleCrypto integrity hashes out
  via Promise.all (instead of awaiting them serially) and yields between
  worker spawns. Cuts the dominant cost when several plugins are
  installed.
- migrateOldData becomes async and chunks the old-format note/folder
  map() loops through forEachWithYield so a legacy vault with hundreds
  of notes does not block first paint.

Both paths are marked + measured; dev mode logs durations under
[boot] so further tuning has a baseline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Settings About: drop em dashes + Obsidian-style framing, add noteser.app link

Voice rule violations on the About panel: em dash separator and 'Obsidian-style markdown note-taking' (no Obsidian in neutral copy). Rewrite as a plain sentence. Also add a direct link to noteser.app alongside the existing noteser.thetechjon.com docs link.

* feat(settings): in-modal search for finding settings by keyword

Adds a sticky search input at the top of the Settings modal. Typing
filters the catalog by label, description, category, and keyword
(case-insensitive substring); matches render grouped by their original
panel with a Go to setting jump button. Empty state reads No settings
match "<query>". Esc clears a non-empty query without closing the
modal; / refocuses from elsewhere in the modal.

Approach: external catalog (settingsCatalog.ts) listing every setting's
metadata. Panels keep their existing per-store render code; the catalog
is consumed only by the search UI. Adds 54 catalogued entries across
the existing 15 panels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(e2e): Playwright visual regression for editor selection across themes

Mounts the real editor in each of the four built-in themes (default,
light, sepia, solarized-dark), selects a known paragraph, and snapshots
the .cm-editor region. Closes the jsdom blind spot in the existing
themeSelectionContrast unit test, which pins the token value but cannot
verify the rendered selection layer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(help): GitBook-style /help chrome with day/night toggle

Redesign the /help layout (chrome only — articles untouched) to read like
GitBook: 720px content column, larger heading hierarchy, generous
line-height, calm sidebar with a left-border accent for the active
topic, and an optional "On this page" rail derived from h2/h3 of the
markdown body.

Add a /help-scoped theme toggle (sun/moon) that persists under
`noteser-help-theme` and defaults to dark. The main app theme is
unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(editor): list-cycle parks caret after the new marker so user can type

The Mod+Alt+Shift+L cycle command was dispatching a whole-line replace
without setting a selection. CodeMirror's default behaviour anchors the
caret to the LEFT of the inserted text, which placed the caret in front
of '- [ ] ' instead of after it. The user had to press End before
typing their task text.

Park the caret at the end of the rewritten line when the cycle was
fired with a single empty cursor on a single line. Multi-line and
range selections still skip the explicit selection (no good single
answer for them).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(editor): single Mod+L shortcut, caret on the right after convert

Per user feedback, fold the Alt+L task-insert behaviour into Mod+L
(the existing toggle-checkbox shortcut), and drop Alt+L. The unified
binding does:

- On an existing task line: flip done/undone (unchanged).
- On a plain/bullet/ordered line: convert to a task and park the caret
  at the end of the rewritten line so the user can type the task body
  immediately. Previously the caret was anchored to the LEFT of the
  newly inserted '- [ ] '.

Caret-placement only fires for a single empty cursor on a single line;
range and multi-line selections retain the previous behaviour to avoid
inventing a 'correct' answer for them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(ux): clickable breadcrumb reveals in tree + gist auth copies code

Two small UX wins:

1. The EditorHeader breadcrumb is now interactive. Click a folder
   segment to expand its chain in the file tree, set it active, and
   switch the sidebar to the Files view. Clicking the note title
   reveals the parent folder. The sidebar opens automatically if it
   was collapsed.

2. The 'Open GitHub to authorise' button on the Publish-as-gist scope
   flow now copies the device-flow user_code to the clipboard before
   opening the verification page, so the user just pastes the code on
   GitHub instead of switching tabs back to read it. Label updated to
   'Copy code and open GitHub' so the behaviour is discoverable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(tree): Select for Compare + Compare with Selected on note rows

VS Code-style two-step compare flow on the file tree right-click menu.
"Select for Compare" marks a note (italic + accent border in the tree);
"Compare with Selected" opens a new read-only side-by-side diff tab
between the marked note and a different right-clicked note. Esc clears
the pending mark from anywhere. The compare tab reuses diffByLine for
hunks and lives as a new 'compare' tab kind on the workspace store, so
MergeEditorView's apply logic stays untouched.

* Tab title: 'Your second brain in the browser' (drop Collaborative framing)

Noteser is not real-time collab. The new title positions the product
honestly: a second brain that lives in your browser, syncing to your
own repo or local folder.

* feat(editor): vertical splits + 3-pane workspace (Obsidian parity)

Adds a Split down action alongside the existing Split right, raises the
pane cap from 2 to 3, and replaces the implicit "left/right pane[0/1]"
arrangement with an explicit LayoutNode tree. The tree models a leaf
(one pane) or a binary split (horizontal/vertical, per-divider ratio)
so the renderer can describe any 3-pane arrangement — three columns,
three rows, or an L-shape (a horizontal split with a vertical split
nested on one side).

- store: LayoutNode + MAX_PANES + splitTabDown + setLayoutRatio. Split
  paths share a single helper that enforces the 3-pane cap (the 4th
  attempt moves the tab into the newest pane, in-place, so the source
  pane isn't compacted away). closeTab / moveTab / pruneStaleTabs /
  closeAllMergeTabs reconcile the layout when panes appear or vanish.
- Editor.tsx: walks the LayoutNode and renders a draggable divider per
  split (col-resize for horizontal, row-resize for vertical). The drag
  writes back into the layout tree via setLayoutRatio.
- TabBar.tsx: right-click context menu with "Split right", "Split down"
  and "Close tab"; tab strip's bottom border lights up purple on the
  focused pane.
- Pane.tsx: drop zones for both right- and bottom-edge tab drags.
- persist v2 → v3 migration derives a horizontal-cascade layout from
  the legacy flat panes[] array; reconciliation drops layout leaves
  pointing at panes that no longer exist.

Tests cover splitTabDown, the 4th-split rejection, pane re-collapse on
close, layout-ratio clamping, and the v1→v3 / v2→v3 / idempotent /
broken-layout migration paths.

* fix(sidebar): stop click propagation in context menu so Move-to-folder submenu stays open

The sidebar root has onClick={closeContextMenu}; without
stopPropagation on the context menu container, ANY click inside the
menu bubbled up and immediately closed the menu — including the
Move-to-folder and AI submenu toggles. Items that should close the
menu already call onClose() directly.

* style(editor): trim redundant comment in tab context-menu dismiss handler

The early-return on the closest() check already telegraphs intent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Drop the Settings cog from the right-side activity ribbon

* fix(sidebar): mirror drag MIMEs so left↔right tab drag actually works

Cross-sidebar drag was broken in both directions because:
- Left tabs dragged via PinnedMiniStrip only set SIDEBAR_PANEL_DRAG_MIME,
  which the right drop zones filter out (they only accept
  RIGHT_TAB_DRAG_MIME).
- Right tabs dragged via RightMiniStrip only set RIGHT_TAB_DRAG_MIME,
  which the left drop zones reject (they accept TAB_DRAG_MIME or
  SIDEBAR_PANEL_DRAG_MIME only).

Set BOTH MIMEs on every tab drag. The drop-side filters then accept
the drag; the drop handlers already detect the source side and route
through moveTabAcrossSidebars so the cross-side move executes
atomically.

Closes the regression where dragging Source Control from the left
sidebar to the right sidebar silently did nothing.

* feat(sidebar): keyboard-resizable stacked groups (a11y parity)

The vertical GroupResizeHandle between stacked sidebar sections was
mouse-drag only, while the horizontal column SidebarResizeHandle already
supported arrow-key resize. Closes that gap (backlog #36): the separator
is now focusable with ArrowUp/ArrowDown to nudge the boundary (Shift for
a coarser step), Home/End to push it to either extreme, and the standard
aria-valuenow/min/max range so screen readers announce the resize.

Shared the clamp arithmetic (resolvePair) between the drag and keyboard
paths so they can't drift apart. Adds groupResizeHandle.test.tsx (10
tests) covering drag, keyboard, clamping, reset, and the ARIA contract.

* chore(lint): land XSS-sink bans as real ESLint rules

The flat ESLint config was on FlatCompat-wrapped next/core-web-vitals,
which was believed to silently drop custom rule blocks — so the raw-HTML
XSS guards lived only as static-source Jest scans (markdownXssGuard).

Turns out the drop isn't inherent to the next preset: a custom rules
object placed at the TOP LEVEL of the exported flat-config array (after
the compat.extends spread, not nested inside it) lands cleanly. Added
no-restricted-syntax bans for dangerouslySetInnerHTML and `.innerHTML =`
plus a no-restricted-imports ban for rehype-raw, scoped to skip
__tests__ (tests legitimately build DOM fixtures, matching the Jest
guard's own __tests__ exclusion).

Verified all three rules fire on a planted production-file violation and
that the clean repo still lints green. The Jest guards stay as the belt;
these are the suspenders — same regressions now caught live on lint /
in-editor, before the suite runs.

* docs(research): testing best-practices guide for QA/testers

Practical, noteser-shaped testing guide: the test layers we have and
what each is for, where the bugs historically live (sync/merge,
hydration races, editor keymaps), how to write good tests against the
Zustand stores + CodeMirror surfaces, flake discipline (no wall-clock
asserts), a coverage strategy that finds blind spots without chasing a
percentage, and an exploratory QA-sweep checklist.

* test(qa): consolidated coverage sweep from QA agents (+325 tests)

Adds the genuinely-new test files produced by the two background QA
agents, rebased onto current dev (which already carries the sidebar
keyboard-resize feature + XSS-sink lint rules):

Core logic (agent A):
- githubSyncGaps.test.ts — sync classifier branches, dual-SHA tracking,
  notePath/folder-path with &/'/leading-dots, MIME guessing, first-clone
  shell path, zero-churn on unedited notes
- lineDiffEdgeCases.test.ts — diffByLine + threeWayMerge boundaries
  (empty/single-line ancestor, all-insert/all-delete, interleaved hunks)
- markdownTableGaps.test.ts — findTableBounds/cell-range/divider edges

Editor + sidebar (agent B):
- editorKeymapCommands.test.ts — checkbox toggle, list cycle, empty-
  checkbox regex, table Tab wiring roundtrip
- markdownTableEdgeCases.test.ts — header-only/single-column/divider-row
  Tab + Shift-Tab navigation
- sidebarStackEdgeCases.test.tsx — empty/hidden-group states, resize-
  handle mount/unmount around collapsed groups, group actions

Excludes the agents' groupResizeHandle.test.tsx (its 7 skip tests
documented the keyboard-a11y gap that is already CLOSED on dev by the
merged feature; dev's own groupResizeHandle.test.tsx covers it). No
jest.config change needed — dev already excludes /.claude/worktrees/.

Verified against current dev: typecheck clean, full suite 2357 passed
(17 skipped), 0 regressions.

* Repair 8 stale top-level E2E specs (#67) (#81)

* test(e2e): repair stale new-features specs after sidebar + ribbon rework (#67)

Three specs in `e2e/new-features.spec.ts` asserted against UI contracts
that drifted under the 2026-06-04 sidebar leaf-model refactor and the
ribbon trim to four quick-launch actions. App behaviour is unchanged.

- vsg1 (source-control crash regression): switched the activation from
  `getByTestId('sidebar-tab-source-control').click()` (the activity-bar
  pinned testid was renamed to `sidebar-pinned-tab-<id>`) to driving
  `setGroupActiveTab` via the testHooks store bridge. The fake-token
  auto-pull was racing the click and detaching the button mid-action —
  going through the store sidesteps that without weakening the React-#185
  regression guarantee. Also persisted `noteser-settings` at v3 with
  source-control seeded directly into a sidebar group so the
  SourceControlPanel mounts; the v0→v3 migration was collapsing the
  default two-group layout into a single calendar-only group, leaving
  the panel unmounted.

- b3e7 (ribbon order persistence): the previous assertion expected
  `ribbon-item-random-note` in the default ribbon, but
  `resolveRibbonOrder` only knows about new-note, daily-note,
  command-palette, templates. The unknown id was silently dropped,
  taking the test with it. Reasserted against the actual default set
  and reordered with `templates` first.

- z9o3 (`![[Title]]` embed renders): `setPreviewMode(true)` was being
  clobbered by `workspaceStore.openNote`'s deferred async import that
  re-aligns the global preview flag with `notesOpenInPreviewMode` on
  first-note-open. Set `setNotesOpenInPreviewMode(true)` first so the
  deferred import lands in the already-correct branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(e2e): repair source-control panel test against per-repo vault switch (#67)

`source-control panel groups changes by folder` (`gutter-and-scm.spec.ts:119`)
was failing because the seeded GitHub session triggered
`useAutoSync.runPullOnly` → `runPull`, which calls `switchVault(repo)` to
the per-repo IDB key (`notesKey(repo)`). That switch abandoned the four
notes the test had just added against the unscoped base key, so the
panel correctly reported "clean" with zero pending entries — but the
seeded fixtures were gone.

Disable `autoSyncOnStart` via the testHooks bridge before configuring
the fake session. The startup pull is a side effect we don't want this
test to model. Also scoped the `2026-W11.md` / `README.md` / etc.
text assertions to the source-control panel root, because
`PropertiesPanel` now surfaces the active note's `gitPath` under the
`properties-git-path` testid — strict-mode would otherwise hit the
two-match guard on `README.md`.

App behaviour unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(e2e): repair nav-and-pin specs against welcome-tab + draggable-row click swallowing (#67)

Two unrelated drifts wedged four specs in `e2e/nav-and-pin.spec.ts`:

1. The default Welcome tab. `page.tsx` opens a `welcome`-kind tab on
   first load when `onboardingShown` is false. The active-tab strip
   selector (`.border-t-obsidianAccentPurple span.truncate`) then
   resolved to "Welcome", not "Alpha". Suppressed via
   `noteser-settings.state.onboardingShown = true` in `beforeEach`,
   plus seeded `sidebarGroups` at v3 with `files` active so the
   FolderTree mounts (the v0→v3 migration was collapsing the default
   two-group layout to a single calendar-only group, leaving the note
   rows unrendered).

2. Playwright's mouse-event simulation drops the synthesised `click`
   on every `draggable=true` element in this Chromium build (chromium
   -1223 via `@playwright/test` 1.60). `Locator.click()` and
   `page.mouse.click()` both fire `mousedown`/`mouseup` but no `click`
   on the FIRST click in any pair — only the second one in a tight
   pair gets through. That made single-click assertions never see a
   tab open at all, and `twoRealClicks` resolved to a single effective
   click. Real users don't hit this — their hardware doesn't trip the
   same drag heuristic — so this is a test-fidelity issue, not an app
   bug. Added a `singleClickRow` helper that calls
   `HTMLElement.click()` directly via `evaluate`, dispatching a real
   bubbling click event the React handler in `FolderTree` listens
   for. `twoRealClicks` now routes through it; `Locator.dblclick()`
   is left alone because it still works (its three-event burst
   bypasses the drag suppression). Updated specs at lines 102, 244,
   313 to use the helper for every single-click step.

App behaviour unchanged. No source files touched.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: ipapakonstantinou <johnypapakonstantinou@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* CI: add dependency audit + scoped Playwright gate, cover sync push paths

- Add `npm audit --audit-level=high` to the check job so a known
  high/critical advisory fails the build.
- Add an E2E job running a new `e2e:ci` script. It uses
  playwright.config.ci.ts, which scopes the gate to the stable
  top-level specs and excludes the experimental e2e/parity/** specs
  (matching CLAUDE.md's "graduation is manual" policy). Uploads the
  Playwright report as an artifact.
- Extend useGitHubSync.test.ts with runSync push-path coverage: the
  happy commit path (records sha, writes path updates back), push
  failure (retryable error, guard released), a locked vault (opens the
  unlock modal), an exhausted token (Reconnect, not Retry), and the
  conflict branch (applies non-conflicts, opens merge tabs, no push).

* docs: add competitive analysis + queue improvement backlog

- Add docs/competitive-analysis.md: compact market benchmark (closest
  competitors, prioritized feature gaps, differentiation/risks, top
  recommendations).
- Queue 12 pending orchestrator items derived from the analysis and the
  code-quality review (offline/PWA, rate-limit hardening, AI RAG, graph
  view, properties UI, imports, web clipper, positioning, plus tech-debt
  refactors and an a11y/perf pass) so they can be picked up by an agent.

* CI: make E2E job report-only pending stale-spec repair (#67)

The first real CI run of the new E2E job surfaced 8 pre-existing
top-level specs that have bit-rotted against the current UI (not app
regressions). Mark the job continue-on-error so the suite still runs
and uploads its report but cannot block PRs until the specs are
repaired. Tracked in #67.

* docs: track improvement backlog as GitHub issues (#68-#79)

Move the 12 improvement tasks out of the orchestrator queue and into
GitHub issues so agents in other sessions can discover and claim them.
Restore queue.json to its prior state; point competitive-analysis.md at
the issues instead.

* ci: flip E2E gate to blocking now that #67 specs are repaired

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Jon Don <thetechjon@gmail.com>
* Harden git-proxy CORS preflight to echo only allowed origins (#28)

The OPTIONS handler echoed Access-Control-Allow-Origin: * regardless of
the calling origin. It now mirrors the GET/POST origin guard: validate via
isOriginAllowed, echo the specific origin with Vary: Origin on success, and
refuse with a bare 403 otherwise. The GET/POST success response echoes the
validated origin too instead of *.

https://claude.ai/code/session_014wU9oA8DyaDEpaH8K8Rptk

* Add regression test for the 'N notes loading…' sidebar banner (#30)

Locks in the progressive-clone shell banner in FolderTree: shows with the
shell count while notes stream in, hidden when all loaded, hidden for an
empty vault.

https://claude.ai/code/session_014wU9oA8DyaDEpaH8K8Rptk

* Let the user rename the trash folder (#27)

New vault-scoped trashFolderName setting (default .trash) surfaced as a
sanitized text input in Settings → General. The synthetic trash sidebar row
reads the configured name for its label, aria-labels, and data attribute.
Renaming is cosmetic: the row keeps its fixed TRASH_FOLDER_ID identity, so
trashed notes stay trashed and recoverable across the rename.

https://claude.ai/code/session_014wU9oA8DyaDEpaH8K8Rptk

* Add 'Open in GitHub' link to the Source Control panel header (#34)

When a syncRepo is configured, the Source Control panel header now shows
owner/name plus a small external-link button that opens the repo on GitHub
(repo root for main/master, /tree/<branch> otherwise). Hidden when no repo
is connected.

https://claude.ai/code/session_014wU9oA8DyaDEpaH8K8Rptk

---------

Co-authored-by: Claude <noreply@anthropic.com>
Document the full testing process in docs/testing.md as the single source
of truth across the three layers (Jest unit, Playwright E2E/parity, live
sync harness) and the binding rules every tester follows.

Rewrite the two test-running subagent definitions to defer to that doc and
encode the concrete conventions:
- tester.md: boundary-only mocking, idb-keyval mock placement, Zustand
  setState isolation, jsdom-vs-node env, fake timers, describe/test naming.
- qa-tester.md: setupCleanVault + waitForTestHooks bootstrap, store-API
  seeding via window.__noteser_test, locator priority, CodeMirror typing,
  DataTransfer drag-drop, no-flaky-wait rule, artifact citing.

Cross-link from CONTRIBUTING.md and CLAUDE.md.
Append a templates appendix to docs/testing.md: runnable skeletons for unit
(pure util, store with fake timers, hook with mocked module, component with
userEvent) and E2E (canonical parity spec, CodeMirror editor, tasks query
block, GitHub-vault seeding, DataTransfer drag-drop), each modelled on an
existing test.
@thetechjon thetechjon merged commit 249601c into dev Jun 7, 2026
3 checks passed
@thetechjon thetechjon deleted the claude/testing-process-tester-rules-olTVy branch June 7, 2026 05:20
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.

3 participants