Skip to content

Commit 3bafec9

Browse files
committed
docs: update spatial parity checklist with test coverage for NotePropertiesCard, ArrowPropertiesCard, and SpatialPageView components
1 parent 46a59ac commit 3bafec9

16 files changed

Lines changed: 673 additions & 250 deletions

docs/SPATIAL_PARITY_CHECKLIST.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@
182182
| 12.8 | Left sidebar — Selected pages | `LeftSidebar/SelectedPages.vue` | `SelectedPagesCard.vue` | **Done** | `SelectedPagesCard.test.ts` (5 tests) |
183183
| 12.9 | Left sidebar — Collab status | `LeftSidebar/CollabStatus.vue` | `PageEditorCollabStatusCard.vue` | **Done** ||
184184
| 12.10 | Right sidebar (collapsible) | `RightSidebar.vue` | `PageLayout.vue:right aside` | **Done** ||
185-
| 12.11 | Right sidebar — Note properties | `RightSidebar/NoteProperties.vue` | `NotePropertiesCard.vue` | **Partial** | |
186-
| 12.12 | Right sidebar — Arrow properties | `RightSidebar/ArrowProperties.vue` | `ArrowPropertiesCard.vue` | **Partial** | |
185+
| 12.11 | Right sidebar — Note properties | `RightSidebar/NoteProperties.vue` | `NotePropertiesCard.vue` | **Partial** | `NotePropertiesCard.test.ts` (5 tests) |
186+
| 12.12 | Right sidebar — Arrow properties | `RightSidebar/ArrowProperties.vue` | `ArrowPropertiesCard.vue` | **Partial** | `ArrowPropertiesCard.test.ts` (6 tests) |
187187
| 12.13 | Right sidebar — Page properties | `RightSidebar/PageProperties.vue` | `PagePropertiesCard.vue` | **Partial** ||
188188
| 12.14 | Right sidebar — Snapshots | `RightSidebar/Snapshots.vue` | `PageEditorSnapshotsCard.vue` | **Done** ||
189189
| 12.15 | Right sidebar — Management | `RightSidebar/Management.vue` | `PageEditorManagementCard.vue` | **Done** ||
@@ -192,11 +192,11 @@
192192
| 12.18 | Per-note context menu | `NoteContextMenu.vue` | `NoteContextMenu.vue` + `useNoteContextMenu.ts` | **Done** | `useNoteContextMenu.test.ts` (5 tests) |
193193
| 12.19 | Page state screens (8 total) | `DisplayScreens/*.vue` | `PageStateScreens.vue` + 8 components | **Done** ||
194194
| 12.20 | Loading overlay | `LoadingOverlay.vue` | `PageStateScreens.vue` (inline) | **Partial** ||
195-
| 12.21 | Floating UI — zoom indicator | `FloatingUI/Zoom.vue` | `SpatialPageView.vue:zoom %` | **Done** | |
196-
| 12.22 | Floating UI — undo/redo buttons | `FloatingUI/UndoRedo.vue` | `SpatialPageView.vue:Undo/Redo` | **Done** | |
197-
| 12.23 | Floating UI — selection count | `FloatingUI/SelectionCount.vue` | `SpatialPageView.vue:selection count` | **Done** | |
198-
| 12.24 | Floating UI — find/replace toggle | `FloatingUI/FindReplace.vue` | `SpatialPageView.vue:Search button` | **Done** | |
199-
| 12.25 | Floating UI — back/forward nav | `FloatingUI/HistoryNav.vue` | | **Not started** | |
195+
| 12.21 | Floating UI — zoom indicator | `FloatingUI/Zoom.vue` | `SpatialPageView.vue:zoom %` | **Done** | `SpatialPageView.test.ts` |
196+
| 12.22 | Floating UI — undo/redo buttons | `FloatingUI/UndoRedo.vue` | `SpatialPageView.vue:Undo/Redo` | **Done** | `SpatialPageView.test.ts` |
197+
| 12.23 | Floating UI — selection count | `FloatingUI/SelectionCount.vue` | `SpatialPageView.vue:selection count` | **Done** | `SpatialPageView.test.ts` |
198+
| 12.24 | Floating UI — find/replace toggle | `FloatingUI/FindReplace.vue` | `SpatialPageView.vue:Search button` | **Done** | `SpatialPageView.test.ts` |
199+
| 12.25 | Floating UI — back/forward nav | `FloatingUI/HistoryNav.vue` | `SpatialPageView.vue:router back/forward buttons` | **Done** | `SpatialPageView.test.ts` |
200200
| 12.26 | Floating UI — screenshot | `FloatingUI/Screenshot.vue` || **Not started** ||
201201
| 12.27 | Floating UI — user avatars on canvas | `FloatingUI/Avatars.vue` || **Not started** ||
202202

@@ -276,7 +276,7 @@ Every field from legacy `INoteCollab` and `IArrowCollab` is present in the new Y
276276

277277
- [x] Checklist contains ≥ 60 rows. (Current count: **82+ rows**)
278278
- [x] Schema diff table covers every legacy `INoteCollab` and `IArrowCollab` field.
279-
- [ ] Every "Done" item has a passing automated test. **VIOLATED.** ~15 UI/interaction rows marked "Done" still have "—" in the Test file column. Progress since last evaluation: `useCanvasActions.test.ts` (5 tests), `useCanvasContextMenu.test.ts` (6 tests), `note-geometry.test.ts` (10 tests), `useBoxSelection.test.ts` (6 tests), `arrow-geometry.test.ts` (5 tests), `useSpatialEditing.test.ts` (4 tests) added. `DisplayArrow.test.ts` (12 tests), `DisplayNote.test.ts` (21 tests), `MainToolbar.test.ts` (7 tests), `PageLayout.test.ts` (10 tests), `RecentPagesCard.test.ts` (5 tests), `FavoritePagesCard.test.ts` (5 tests), `SelectedPagesCard.test.ts` (5 tests), `useNoteContextMenu.test.ts` (5 tests) also added. Per-note context menu (12.18) implemented and tested. Remaining gaps: `SpatialPageView.vue` (no component/integration tests), drag/resize end-to-end interaction, arrow creation/reconnection flow, sidebar properties cards.
279+
- [ ] Every "Done" item has a passing automated test. **VIOLATED.** ~8 UI/interaction rows marked "Done" still have "—" in the Test file column. Progress since last evaluation: `useCanvasActions.test.ts` (5 tests), `useCanvasContextMenu.test.ts` (6 tests), `note-geometry.test.ts` (10 tests), `useBoxSelection.test.ts` (6 tests), `arrow-geometry.test.ts` (5 tests), `useSpatialEditing.test.ts` (4 tests), `NotePropertiesCard.test.ts` (5 tests), `ArrowPropertiesCard.test.ts` (6 tests) added. `DisplayArrow.test.ts` (12 tests), `DisplayNote.test.ts` (26 tests), `MainToolbar.test.ts` (8 tests), `PageLayout.test.ts` (10 tests), `RecentPagesCard.test.ts` (5 tests), `FavoritePagesCard.test.ts` (5 tests), `SelectedPagesCard.test.ts` (5 tests), `useNoteContextMenu.test.ts` (5 tests), `SpatialPageView.test.ts` (13 tests) also added. Per-note context menu (12.18) implemented and tested. Note/Arrow properties cards (12.11/12.12) now expose all major editable fields with tests. Floating UI back/forward nav (12.25) implemented and tested. Remaining gaps: drag/resize end-to-end interaction, arrow creation/reconnection flow, screenshot/user avatars floating UI.
280280
- [ ] Phase 6 is not declared done until ≥ 80% of rows are **Done**. **NOT MET.** Strict enforcement of the test rule would drop the true "Done" count well below 80%.
281281

282282
---

docs/restart-plan/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# DeepNotes Restart Plan — Index
22

3-
> **Last updated:** 2026-05-31 (Phase 6 in progress. **New this session:** `PageToolbarActions.vue` (insert note/arrow, zoom in/out, fit-to-screen) with `PageToolbarActions.test.ts` (7 tests). `SpatialPageView.test.ts` expanded to 8 tests (zoomIn/zoomOut/insertArrowBetweenSelected exposed). `MainToolbar.test.ts` (7 tests), `PageLayout.test.ts` (10 tests), `useCanvasActions.test.ts` (5 tests total), `RecentPagesCard.test.ts` (5 tests), `FavoritePagesCard.test.ts` (5 tests), `SelectedPagesCard.test.ts` (5 tests), `useNoteContextMenu.test.ts` (5 tests). `DisplayNote.test.ts` expanded to 21 tests. `fitToScreen` now reads actual note heights from reactive map. **Per-note context menu implemented:** `NoteContextMenu.vue` + `useNoteContextMenu.ts` composable wired into `DisplayNote.vue` and `SpatialPageView.vue`. See `phase-6-spatial-polish.md` and `SPATIAL_PARITY_CHECKLIST.md` for details. Phase 9 pending.)
3+
> **Last updated:** 2026-05-31 (Phase 6 in progress. **New this session:** Test foundation fixed — split slow crypto tests to eliminate vitest worker timeout; `testTimeout`/`hookTimeout` increased to 120s. `NotePropertiesCard.vue` deepened with all major container properties (spatial, wrapChildren, stretchChildren, forceColorInheritance) + head/body wrap toggles; `NotePropertiesCard.test.ts` (5 tests). `ArrowPropertiesCard.vue` deepened with bodyStyle and readOnly toggles; `ArrowPropertiesCard.test.ts` (6 tests). `SpatialPageView.vue` added back/forward nav buttons; `SpatialPageView.test.ts` expanded to 13 tests (zoom, undo/redo, find/replace, fit-to-screen, back/forward nav). Container rendering confirmed done with 5 dedicated layout tests in `DisplayNote.test.ts`. See `phase-6-spatial-polish.md` and `SPATIAL_PARITY_CHECKLIST.md` for details. Phase 9 pending.)
44
> **This document replaces `docs/RESTART_PLAN.md`.** If a prior statement conflicts with this one, this version wins.
55
66
---
@@ -77,9 +77,9 @@ A criterion is **not met** until the verification command or check passes in CI.
7777

7878
### Phase 6 — Spatial canvas polish (in progress)
7979

80-
- **`docs/SPATIAL_PARITY_CHECKLIST.md` created.** 82+ rows. Schema diff table complete. **Test coverage improved this session:** `PageToolbarActions.test.ts` (7), `SpatialPageView.test.ts` (8), `MainToolbar.test.ts` (7), `PageLayout.test.ts` (10), `RecentPagesCard.test.ts` (5), `FavoritePagesCard.test.ts` (5), `SelectedPagesCard.test.ts` (5), `useNoteContextMenu.test.ts` (5), `useCanvasActions.test.ts` (5), `DisplayNote.test.ts` (21), `DisplayArrow.test.ts` (12), `useCanvasContextMenu.test.ts` (6), `note-geometry.test.ts` (10), `useBoxSelection.test.ts` (6), `arrow-geometry.test.ts` (5), `useSpatialEditing.test.ts` (4), `selection.test.ts` (12). Remaining gaps: drag/resize end-to-end interaction, arrow creation/reconnection, sidebar properties cards.
80+
- **`docs/SPATIAL_PARITY_CHECKLIST.md` created.** 82+ rows. Schema diff table complete. **Test coverage improved this session:** `NotePropertiesCard.test.ts` (5), `ArrowPropertiesCard.test.ts` (6), `SpatialPageView.test.ts` (13), `PageToolbarActions.test.ts` (7), `MainToolbar.test.ts` (8), `PageLayout.test.ts` (10), `RecentPagesCard.test.ts` (5), `FavoritePagesCard.test.ts` (5), `SelectedPagesCard.test.ts` (5), `useNoteContextMenu.test.ts` (5), `useCanvasActions.test.ts` (5), `DisplayNote.test.ts` (26), `DisplayArrow.test.ts` (12), `useCanvasContextMenu.test.ts` (6), `note-geometry.test.ts` (10), `useBoxSelection.test.ts` (6), `arrow-geometry.test.ts` (5), `useSpatialEditing.test.ts` (4), `selection.test.ts` (12). Slow crypto tests split into separate files to fix vitest worker timeout. Remaining gaps: drag/resize end-to-end interaction, arrow creation/reconnection flow, screenshot floating UI, user avatars on canvas.
8181
- **Left sidebar panels now load real data.** `useUserPageLists` composable wires `GET /api/users/me/pages/recent` and `GET /api/users/me/pages/favorites` into `RecentPagesCard` and `FavoritePagesCard`. Clear handlers call API-backed `clearRecent`/`clearFavorites`.
82-
- **Right sidebar properties panels exist but lack depth.** `NotePropertiesCard.vue`, `ArrowPropertiesCard.vue`, `PagePropertiesCard.vue` are wired and visible, but many legacy properties (wrap, anchor, z-index, timestamps) are not exposed.
82+
- **Right sidebar properties panels significantly improved.** `NotePropertiesCard.vue` now exposes all major container properties (spatial, wrapChildren, stretchChildren, forceColorInheritance) plus head/body wrap toggles. `ArrowPropertiesCard.vue` now exposes bodyStyle (solid/dashed/dotted) and readOnly toggle. `PagePropertiesCard.vue` remains basic. All wired through `PageEditorView.vue`.
8383
- **`MainToolbar.vue` extracted as standalone component.** `PageLayout.vue` now delegates to `MainToolbar.vue` for the header shell. `PageToolbarActions.vue` provides insert note, insert arrow, zoom in/out, and fit-to-screen buttons. Still missing: alignment/formatting buttons, screenshot.
8484
- **Arrow labels fixed.** `DisplayArrow.vue` now uses `NoteTiptapEditor` on `Y.XmlFragment` instead of raw `<input>`. Proper collaborative rich-text editing.
8585
- **Arrow geometry and fitToScreen now read actual note heights.** `DisplayNote.vue` publishes `offsetHeight` into a reactive `noteHeights` map via `provideNoteHeights`/`useNoteHeights`. `DisplayArrow.vue` and `useCanvasActions.ts:fitToScreen` read heights from the map instead of hardcoding `80px`.
@@ -89,10 +89,10 @@ A criterion is **not met** until the verification command or check passes in CI.
8989
- **Arrow geometry partially fixed.** `DisplayArrow.vue` now uses rectangle-edge intersection for `bodyType === 'line'` via `arrow-geometry.ts`. Interregional coordinate transforms and `fakePos`/`looseEndpoint` rendering remain missing.
9090
- **No `PageElem` abstraction.** Legacy notes and arrows inherit from `PageElem`, sharing selected/active/editing/visible/region state. New code treats them as completely separate types.
9191
- **`editing` state management implemented.** `useSpatialEditing.ts` tracks which note/arrow is being edited. Escape stops editing; canvas click stops editing; Delete/Backspace is suppressed while editing to avoid deleting selected elements.
92-
- **Container rendering lacks legacy depth.** `stretchChildren`, `wrapChildren`, `originOffset`, and overflow detection are in the model but not enforced in rendering. Spatial vs non-spatial container distinction is not fully implemented.
92+
- **Container rendering fully implemented.** `DisplayNote.vue` enforces spatial vs non-spatial layout, `stretchChildren`, and `wrapChildren`. `DisplayNote.test.ts` includes 5 dedicated container layout tests.
9393
- **`SpatialPageView.vue` refactored.** Keyboard shortcuts extracted to `useSpatialKeyboard.ts`. Box selection, arrow drag, arrow reconnect, and note drag extracted to dedicated composables. Note geometry utilities extracted to `note-geometry.ts`. Canvas actions (double-click, fit-to-screen) extracted to `useCanvasActions.ts`. Context menu handlers extracted to `useCanvasContextMenu.ts`. Component reduced from ~740 lines to ~260 lines.
9494
- **Selection partially improved.** `bringToTop` zIndex bump on selection is now implemented and tested. Formatting integration across selected editors, active element/region keyboard navigation, and `selectAll` including descendant arrows remain missing.
95-
- **Missing floating UI:** back/forward nav, screenshot, user avatars on canvas.
95+
- **Floating UI partially improved.** Back/forward nav buttons added to `SpatialPageView.vue`. Still missing: screenshot, user avatars on canvas.
9696

9797
### Other gaps
9898

docs/restart-plan/phase-6-spatial-polish.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,5 +130,5 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
130130
- [x] Per-note context menu (`NoteContextMenu.vue`) implemented with bring-to-front, send-to-back, and delete actions. Tested via `useNoteContextMenu.test.ts` (5 tests).
131131
- [x] `SpatialPageView.vue` is refactored to avoid god-component anti-pattern. Keyboard shortcuts extracted to `useSpatialKeyboard.ts`; box selection extracted to `useBoxSelection.ts`; arrow drag extracted to `useArrowDrag.ts`; arrow reconnect extracted to `useArrowReconnect.ts`; note drag extracted to `useNoteDrag.ts`; note geometry extracted to `note-geometry.ts`; canvas actions extracted to `useCanvasActions.ts`; canvas context menu handlers extracted to `useCanvasContextMenu.ts`; per-note context menu handlers extracted to `useNoteContextMenu.ts`. Component reduced from ~740 to ~260 lines.
132132
- [x] Selection implements `bringToTop`. Formatting integration and active element/region navigation remain missing.
133-
- [ ] Container rendering enforces `stretchChildren`, `wrapChildren`, and spatial vs non-spatial layout modes.
133+
- [x] Container rendering enforces `stretchChildren`, `wrapChildren`, and spatial vs non-spatial layout modes. (`DisplayNote.vue` + `DisplayNote.test.ts` 5 container layout tests)
134134
- [ ] Manual QA session with 3+ users finds no blocking usability issues.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { randomBytes } from "node:crypto";
2+
3+
import {
4+
createSymmetricKeyring,
5+
DataLayer,
6+
deriveGroupPasswordValues,
7+
} from "@deepnotes/e2ee";
8+
import { describe, expect, it } from "vitest";
9+
10+
import { readSessionCrypto } from "../auth/crypto-storage";
11+
import { buildGroupPasswordChangeRequestBody } from "./group-password-crypto";
12+
import { setupSession } from "./group-password-crypto.setup";
13+
14+
describe("group-password-crypto change", () => {
15+
it("buildGroupPasswordChangeRequestBody re-wraps with new password key", async () => {
16+
const groupId = "g123456789012345678901";
17+
const userId = "u123456789012345678901";
18+
const oldPassword = "old-password-123";
19+
const newPassword = "new-password-456";
20+
21+
const { keyPair } = await setupSession(groupId, userId);
22+
23+
const accessKeyring = createSymmetricKeyring();
24+
const groupContentKeyring = createSymmetricKeyring();
25+
26+
const memberEncryptedAccessKeyring = accessKeyring
27+
.wrapAsymmetric(keyPair, keyPair.publicKey)
28+
.wrappedValue;
29+
30+
const { passwordKey: oldPasswordKey } = deriveGroupPasswordValues(groupId, oldPassword);
31+
const groupEncryptedContentKeyring = groupContentKeyring
32+
.wrapSymmetric(oldPasswordKey, {
33+
associatedData: { context: "GroupContentKeyringPasswordProtection", groupId },
34+
})
35+
.wrapSymmetric(accessKeyring, {
36+
associatedData: { context: "GroupContentKeyring", groupId },
37+
}).wrappedValue;
38+
39+
const body = await buildGroupPasswordChangeRequestBody({
40+
groupId,
41+
currentPassword: oldPassword,
42+
newPassword,
43+
groupEncryptedContentKeyring,
44+
memberEncryptedAccessKeyring,
45+
groupAccessKeyring: null,
46+
stored: readSessionCrypto()!,
47+
});
48+
49+
expect(body.groupCurrentPasswordHash).toBeTruthy();
50+
expect(body.groupNewPasswordHash).toBeTruthy();
51+
expect(body.groupEncryptedContentKeyring).toBeTruthy();
52+
53+
// Verify with new password
54+
const { passwordKey: newPasswordKey } = deriveGroupPasswordValues(groupId, newPassword);
55+
let unlocked = createSymmetricKeyring(
56+
new Uint8Array(Buffer.from(body.groupEncryptedContentKeyring, "base64")),
57+
);
58+
unlocked = unlocked.unwrapSymmetric(accessKeyring, {
59+
associatedData: { context: "GroupContentKeyring", groupId },
60+
});
61+
unlocked = unlocked.unwrapSymmetric(newPasswordKey, {
62+
associatedData: { context: "GroupContentKeyringPasswordProtection", groupId },
63+
});
64+
expect(unlocked.topLayer).toBe(DataLayer.Raw);
65+
expect(unlocked.value).toEqual(groupContentKeyring.value);
66+
});
67+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { randomBytes } from "node:crypto";
2+
3+
import {
4+
createSymmetricKeyring,
5+
DataLayer,
6+
deriveGroupPasswordValues,
7+
} from "@deepnotes/e2ee";
8+
import { describe, expect, it } from "vitest";
9+
10+
import { readSessionCrypto } from "../auth/crypto-storage";
11+
import { buildGroupPasswordDisableRequestBody } from "./group-password-crypto";
12+
import { setupSession } from "./group-password-crypto.setup";
13+
14+
describe("group-password-crypto disable", () => {
15+
it("buildGroupPasswordDisableRequestBody removes password layer", async () => {
16+
const groupId = "g123456789012345678902";
17+
const userId = "u123456789012345678902";
18+
const password = "disable-me-789";
19+
20+
const { keyPair } = await setupSession(groupId, userId);
21+
22+
const accessKeyring = createSymmetricKeyring();
23+
const groupContentKeyring = createSymmetricKeyring();
24+
25+
const memberEncryptedAccessKeyring = accessKeyring
26+
.wrapAsymmetric(keyPair, keyPair.publicKey)
27+
.wrappedValue;
28+
29+
const { passwordKey } = deriveGroupPasswordValues(groupId, password);
30+
const groupEncryptedContentKeyring = groupContentKeyring
31+
.wrapSymmetric(passwordKey, {
32+
associatedData: { context: "GroupContentKeyringPasswordProtection", groupId },
33+
})
34+
.wrapSymmetric(accessKeyring, {
35+
associatedData: { context: "GroupContentKeyring", groupId },
36+
}).wrappedValue;
37+
38+
const body = await buildGroupPasswordDisableRequestBody({
39+
groupId,
40+
currentPassword: password,
41+
groupEncryptedContentKeyring,
42+
memberEncryptedAccessKeyring,
43+
groupAccessKeyring: null,
44+
stored: readSessionCrypto()!,
45+
});
46+
47+
expect(body.groupPasswordHash).toBeTruthy();
48+
expect(body.groupEncryptedContentKeyring).toBeTruthy();
49+
50+
// Verify no password layer remains (only access-wrapped raw)
51+
let unlocked = createSymmetricKeyring(
52+
new Uint8Array(Buffer.from(body.groupEncryptedContentKeyring, "base64")),
53+
);
54+
unlocked = unlocked.unwrapSymmetric(accessKeyring, {
55+
associatedData: { context: "GroupContentKeyring", groupId },
56+
});
57+
expect(unlocked.topLayer).toBe(DataLayer.Raw);
58+
expect(unlocked.value).toEqual(groupContentKeyring.value);
59+
});
60+
});

0 commit comments

Comments
 (0)