mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 13:28:57 +00:00
fix(web): render the Rich/Markdown mode toggle on the peeking side too (BUG-2263 follow-up) (#988)
The invisible-freeze work (PR #987) left ONE surface still gated on `!peeking`: the editor's Rich⇄Markdown mode toggle. It was hidden on the passive preview because it's a provider-LIFECYCLE control — switching to Markdown nulls collabKey and DESTROYS the retained collab provider, which retain-alive (D2) forbids WHILE peeking. But under focus-follows-editing a click on the toggle fires the host's pointerdown-capture activator FIRST, flipping activePane to that side (peeking=false) before the click's onclick runs. So by the time the flip executes, the side is already ACTIVE and tearing down its own provider is normal active-side behavior — the "teardown while peeking" the gate feared can't happen via a click. The onclick's existing `if (peeking) return` guards (plus the `|| peeking` mid-flush rechecks) remain as the backstop for a re-peek DURING the async flush (e.g. the user clicks the other side mid-flip). So drop the `{#if !peeking}` render gate — the toggle now renders on both sides like every other invisible-freeze surface. Verified in the browser: opening the pane shows the toggle on the peeking preview; clicking the peeking master's "Markdown" button activates it and flips to raw mode in ONE gesture, ProseMirror unmounts cleanly, exactly one typeable editor throughout, zero console errors. Tests: FreezeProbe renders mode-toggle unconditionally; masterFreeze asserts it present while peeking; new host e2e opens the pane, confirms the toggle on the peeking side, and asserts the one-gesture flip (a successful flip proves activation preceded the guarded onclick). Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
This commit is contained in:
@@ -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=<the master itself>` is stripped — never mounts a pane on the master', async ({
|
||||
page,
|
||||
fixture,
|
||||
|
||||
@@ -4235,12 +4235,14 @@
|
||||
Editor, EditorBubbleMenu, provider, collabKey and SSE stay
|
||||
persistent across an A→B item switch (the no-{#key} perf premise). -->
|
||||
<div class="content-panel">
|
||||
<!-- Master-freeze (TASK-2172): the Rich⇄Markdown toggle is a
|
||||
provider-LIFECYCLE control — switching to Markdown flushes,
|
||||
sets rawMode, nulls collabKey and DESTROYS the retained
|
||||
provider (violating retain-alive). Hide it entirely while
|
||||
peeking; the onclick guards below are belt-and-suspenders. -->
|
||||
{#if !peeking}
|
||||
<!-- BUG-2263 invisible freeze: the Rich⇄Markdown toggle renders on
|
||||
BOTH sides. It IS a provider-LIFECYCLE control (Markdown flushes,
|
||||
nulls collabKey, DESTROYS the retained provider), but a click
|
||||
activates THIS side first (pointerdown → activePane flip →
|
||||
peeking=false) before the flip runs, so the teardown only ever
|
||||
happens on the now-ACTIVE side — never while peeking. The onclick
|
||||
`peeking` guards below stay as the backstop for a re-peek DURING the
|
||||
async flush (e.g. the user clicks the other side mid-flip). -->
|
||||
<div class="editor-mode-toggle">
|
||||
<button
|
||||
class="mode-btn"
|
||||
@@ -4435,7 +4437,6 @@
|
||||
title="Raw markdown editor"
|
||||
>Markdown</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if rawMode}
|
||||
{#key item.id}
|
||||
<!-- Master-freeze (TASK-2172, HT-2176 Option A): a peeking
|
||||
|
||||
@@ -52,13 +52,13 @@
|
||||
<div data-testid="child-frozen">{false}</div>
|
||||
<div data-testid="timeline-frozen">{false}</div>
|
||||
|
||||
<!-- The Rich⇄Markdown mode toggle is a provider-LIFECYCLE control (switching to
|
||||
Markdown destroys the retained collab provider), so it stays hidden on the
|
||||
passive preview (`!peeking`); it reappears the instant you click in (which
|
||||
you must do to edit content anyway). The one accepted visible exception. -->
|
||||
{#if !peeking}
|
||||
<button data-testid="mode-toggle">Rich / Markdown</button>
|
||||
{/if}
|
||||
<!-- The Rich⇄Markdown mode toggle now renders on BOTH sides (BUG-2263 follow-up).
|
||||
It IS a provider-LIFECYCLE control (Markdown destroys the retained provider),
|
||||
but a click activates THIS side first (pointerdown → peeking=false) before the
|
||||
guarded flip runs, so the teardown only ever happens on the now-active side. So
|
||||
it renders unconditionally like the other invisible-freeze surfaces; the flip's
|
||||
`if (peeking) return` onclick guards are the backstop for a re-peek mid-flush. -->
|
||||
<button data-testid="mode-toggle">Rich / Markdown</button>
|
||||
|
||||
<!-- REST mutation UI — gated on `canEdit` alone, present on both sides. -->
|
||||
{#if canEdit}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user