diff --git a/web/e2e/pane-full-page-host.spec.ts b/web/e2e/pane-full-page-host.spec.ts index 0f690b8b..02115550 100644 --- a/web/e2e/pane-full-page-host.spec.ts +++ b/web/e2e/pane-full-page-host.spec.ts @@ -339,6 +339,56 @@ test.describe('full-page pane host (PLAN-2154 Phase 2 / TASK-2174)', () => { await expect.poll(handleDisplay, { timeout: 3000 }).not.toBe('none'); }); + test('the Rich/Markdown mode toggle renders on the peeking side and flips in ONE gesture (BUG-2263 follow-up)', async ({ + page, + fixture, + request, + }) => { + await page.setViewportSize(DESKTOP); + await browserLogin(page); + + const master = await seedDocWithContent(fixture, request, 'FP mode-toggle master', 'Master body paragraph.'); + const related = await seedDoc(fixture, request, 'FP mode-toggle related'); + await seedRelatedLink(fixture, request, master.slug, related.id); + const relatedRef = await itemRef(fixture, request, related.slug); + + await page.goto(fullPageUrl(fixture, master.slug)); + const masterCol = page.locator('.item-page-host > .item-page'); + const masterEditor = masterCol.locator('.editor-wrapper .ProseMirror'); + const masterToggle = masterCol.locator('.editor-mode-toggle'); + await expect(masterEditor).toHaveAttribute('contenteditable', 'true'); + await expect(masterToggle).toBeVisible(); + + // Open the pane → the master stays active (DR-2); the pane is a read-only + // preview. NEW (BUG-2263 follow-up): the peeking PANE shows the mode toggle — + // previously it was hidden on the peeking side. + await page + .locator('.relationship-group', { hasText: 'Related' }) + .locator('a.link-target', { hasText: 'FP mode-toggle related' }) + .click(); + const pane = page.locator('.item-pane'); + await expect(pane).toBeVisible(); + await expect.poll(() => openItemParam(page)).toBe(relatedRef); + const paneEditor = pane.locator('.editor-wrapper .ProseMirror'); + await expect(pane.locator('.editor-mode-toggle')).toBeVisible(); + await expect(paneEditor).toHaveAttribute('contenteditable', 'false'); + + // Click into the pane → the MASTER becomes the peeking side; its toggle stays. + await paneEditor.click(); + await expect(masterEditor).toHaveAttribute('contenteditable', 'false'); + await expect(masterToggle).toBeVisible(); + + // ONE GESTURE: click the peeking master's "Markdown" button. Its onclick bails + // on `if (peeking) return`, so a successful flip PROVES the pointerdown + // activator un-peeked the master FIRST, in the same gesture. + await masterCol.locator('.editor-mode-toggle .mode-btn', { hasText: 'Markdown' }).click(); + await expect(masterCol.locator('.editor-mode-toggle .mode-btn', { hasText: 'Markdown' })).toHaveClass(/active/); + // Master is now in raw markdown mode: the ProseMirror unmounted, and the pane + // is the frozen side — exactly one typeable editor throughout. + await expect(masterEditor).toHaveCount(0); + await expect(paneEditor).toHaveAttribute('contenteditable', 'false'); + }); + test('a cold-loaded `?item=` is stripped — never mounts a pane on the master', async ({ page, fixture, diff --git a/web/src/lib/components/items/ItemDetail.svelte b/web/src/lib/components/items/ItemDetail.svelte index b617ef1a..79efbfcf 100644 --- a/web/src/lib/components/items/ItemDetail.svelte +++ b/web/src/lib/components/items/ItemDetail.svelte @@ -4235,12 +4235,14 @@ Editor, EditorBubbleMenu, provider, collabKey and SSE stay persistent across an A→B item switch (the no-{#key} perf premise). -->
- - {#if !peeking} +
- {/if} {#if rawMode} {#key item.id} -{#if !peeking} - -{/if} + + {#if canEdit} diff --git a/web/src/lib/components/items/masterFreeze/masterFreeze.svelte.test.ts b/web/src/lib/components/items/masterFreeze/masterFreeze.svelte.test.ts index df73df07..2ea2121c 100644 --- a/web/src/lib/components/items/masterFreeze/masterFreeze.svelte.test.ts +++ b/web/src/lib/components/items/masterFreeze/masterFreeze.svelte.test.ts @@ -76,11 +76,14 @@ describe('invisible master/pane freeze wiring (BUG-2263)', () => { expect(text(r, 'mutationsEnabled')).toBe('false'); // CONTENT bucket — frozen: the editor is not typeable, the raw editor is - // read-only, the bubble/link chrome + provider-lifecycle mode toggle hide. + // read-only, and the selection bubble/link chrome is gone (it only appears on + // a selection, which requires activation). expect(text(r, 'editor-editable')).toBe('false'); expect(text(r, 'raw-readonly')).toBe('true'); expect(present(r, 'editor-mutation-ui')).toBe(false); - expect(present(r, 'mode-toggle')).toBe(false); + // The Rich/Markdown toggle now renders on the peeking side too (BUG-2263 + // follow-up): a click activates the side before the guarded provider flip. + expect(present(r, 'mode-toggle')).toBe(true); // REST bucket — INVISIBLE: fields interactive, children/timeline not frozen. expect(text(r, 'field-readonly')).toBe('false'); @@ -112,7 +115,9 @@ describe('invisible master/pane freeze wiring (BUG-2263)', () => { const r = render({ canEdit: true, peeking: false }); expect(text(r, 'mutationsEnabled')).toBe('true'); - // Content surfaces are the ONLY difference from the peeking case. + // The editor's own state (typeable, raw not read-only) + its selection chrome + // are the ONLY differences from the peeking case. The mode toggle now renders + // in BOTH states (asserted true here and in the peeking case above). expect(text(r, 'editor-editable')).toBe('true'); expect(text(r, 'raw-readonly')).toBe('false'); expect(present(r, 'editor-mutation-ui')).toBe(true);