Document the testing process and tighten tester rules#108
Merged
Conversation
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.
…le button + promoted position)
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-keyvalmock placement, ZustandsetStateisolation, jsdom-vs-node env, fake timers,describe/testnaming..claude/agents/qa-tester.md— rewritten with the Playwright conventions:setupCleanVault+waitForTestHooksbootstrap, store-API seeding viawindow.__noteser_test, locator priority, CodeMirror typing,DataTransferdrag-drop, no-flaky-wait rule, artifact citing. Keeps the Obsidian-parity mission and the structured report format.CONTRIBUTING.mdandCLAUDE.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 e2ebehavior 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