collab-presence 0010: unified draggable overlay menu
One circular FAB (peer-count badge) replaces the top-right overlay row: draggable anywhere (pointer capture, 4px click-vs-drag threshold — the comment-pin pattern), position persisted in localStorage, panel opens toward screen center and stacks composable sections — roster, source chip, view-only pill, follow row, comments, chrome toggle. Renders at z-50 above everything (decided: covers wx dialogs; trivially dismissed via click-away/ Esc/drag). CommentLayer portals its bar + list panel into the menu's comments slot; pins/popovers/composer/click-catcher stay canvas-anchored. The FAB is the chrome-hidden survivor. e2e specs updated with an openOverlayMenu helper (panel is closed by default, click-away closes it). NOTE: Playwright suites updated but not executed here (no local WASM artifacts) — verify on CI / next session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A42xfPFNdfsUt9eowkC9eM
This commit is contained in:
parent
250dc3dbae
commit
f4d3c3f146
11 changed files with 410 additions and 141 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { test, expect, type Browser, type Page } from '@playwright/test';
|
||||
import { openOverlayMenu } from './overlay-menu';
|
||||
|
||||
/**
|
||||
* Figma-like "hide UI" toggle e2e (desktop): Cmd/Ctrl+\ and the floating
|
||||
|
|
@ -85,7 +86,9 @@ test('Ctrl+\\ hides every non-canvas UI element; the canvas reclaims the viewpor
|
|||
);
|
||||
// shell overlays follow the toggle…
|
||||
await expect(page.getByText(/console \(/)).toHaveCount(0);
|
||||
// …but the toggle button itself stays reachable
|
||||
// …but the toggle stays reachable inside the overlay menu (0010) — the
|
||||
// FAB is the canvas-only survivor.
|
||||
await openOverlayMenu(page);
|
||||
await expect(page.locator('[data-testid="chrome-toggle"]')).toBeVisible();
|
||||
|
||||
await page.screenshot({ path: 'test-results/web-chrome-hidden.png', scale: 'css' });
|
||||
|
|
@ -94,7 +97,8 @@ test('Ctrl+\\ hides every non-canvas UI element; the canvas reclaims the viewpor
|
|||
test('the floating button restores EXACTLY the pre-hide chrome (no over-shown panes)', async () => {
|
||||
test.setTimeout(120_000);
|
||||
|
||||
// still hidden from the previous test — restore via the button
|
||||
// still hidden from the previous test — restore via the button (in the menu)
|
||||
await openOverlayMenu(page);
|
||||
await page.locator('[data-testid="chrome-toggle"]').click();
|
||||
|
||||
await expect.poll(() => visibleMenuTitles(page), { timeout: 15000 }).toBeGreaterThan(0);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { openOverlayMenu } from './overlay-menu';
|
||||
|
||||
/**
|
||||
* Comment pins vs. canvas resize (collab-presence 0005 regression): the DOM
|
||||
|
|
@ -28,6 +29,7 @@ async function bootAs(page: Page, user: string): Promise<void> {
|
|||
intervals: [1000],
|
||||
})
|
||||
.toMatch(TITLE);
|
||||
await openOverlayMenu(page); // the comment bar lives in the overlay menu (0010)
|
||||
await expect(page.getByTestId('comment-bar-toggle')).toBeVisible({ timeout: 30000 });
|
||||
await page.getByTestId('comment-bar-toggle').click();
|
||||
await expect(page.getByTestId('comment-mode-toggle')).toBeVisible();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { openOverlayMenu } from './overlay-menu';
|
||||
|
||||
/**
|
||||
* Figma-like comments e2e (collab-presence 0005): two tabs of the real React
|
||||
|
|
@ -28,6 +29,7 @@ async function bootAs(page: Page, user: string): Promise<void> {
|
|||
.toMatch(TITLE);
|
||||
// The comment controls mount once the collab session + bridge are up; the
|
||||
// action buttons live inside the expandable bar — open it for the test.
|
||||
await openOverlayMenu(page); // the comment bar lives in the overlay menu (0010)
|
||||
await expect(page.getByTestId('comment-bar-toggle')).toBeVisible({ timeout: 30000 });
|
||||
await page.getByTestId('comment-bar-toggle').click();
|
||||
await expect(page.getByTestId('comment-mode-toggle')).toBeVisible();
|
||||
|
|
@ -96,7 +98,9 @@ test('comment lifecycle across two tabs: create → reply → resolve → delete
|
|||
|
||||
// Resolved pins hide from the default (unresolved) pin set in both tabs…
|
||||
await expect(page.getByTestId('comment-pin')).toHaveCount(0, { timeout: 20000 });
|
||||
// …but stay reachable via the panel's "resolved" filter.
|
||||
// …but stay reachable via the panel's "resolved" filter. (The popover
|
||||
// click above closed the overlay menu — click-away — so re-open it.)
|
||||
await openOverlayMenu(page);
|
||||
await page.getByTestId('comment-panel-toggle').click();
|
||||
await page.getByTestId('comment-show-resolved').check();
|
||||
await expect(page.getByTestId('comment-panel-item')).toHaveCount(1);
|
||||
|
|
@ -106,6 +110,7 @@ test('comment lifecycle across two tabs: create → reply → resolve → delete
|
|||
await expect(page.getByTestId('comment-popover')).toBeVisible();
|
||||
await page.getByTestId('comment-delete-thread').click();
|
||||
|
||||
await openOverlayMenu(page); // the delete click closed the menu (click-away)
|
||||
await expect(page.getByTestId('comment-panel-item')).toHaveCount(0, { timeout: 20000 });
|
||||
await expect(pageB.getByTestId('comment-pin')).toHaveCount(0, { timeout: 20000 });
|
||||
await pageB.close();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { openOverlayMenu } from './overlay-menu';
|
||||
|
||||
/**
|
||||
* Follow-user e2e (collab-presence 0008): two tabs on the demo BOARD share
|
||||
|
|
@ -81,7 +82,9 @@ test('B follows A: viewport mirrors, then local input breaks the follow', async
|
|||
})
|
||||
.toBe(true);
|
||||
|
||||
// B sees alice in the roster and clicks her avatar → follow.
|
||||
// B sees alice in the roster (inside the overlay menu, 0010) and clicks
|
||||
// her avatar → follow.
|
||||
await openOverlayMenu(b);
|
||||
const avatar = b.locator('[data-presence-user="alice"]');
|
||||
await expect(avatar).toBeVisible({ timeout: 30000 });
|
||||
await avatar.click();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { test, expect, devices, type Page, type Browser } from '@playwright/test';
|
||||
import { openOverlayMenu } from './overlay-menu';
|
||||
|
||||
/**
|
||||
* Mobile canvas-only mode e2e (features/mobile) — runs under the
|
||||
|
|
@ -310,7 +311,9 @@ test('editor is canvas-only: no menubar, GL canvas fills the viewport', async ()
|
|||
});
|
||||
|
||||
test('the floating toggle brings the full UI up and back off (Figma-like hide-UI)', async () => {
|
||||
// Canvas-only is the mobile DEFAULT, but the toggle must stay reachable.
|
||||
// Canvas-only is the mobile DEFAULT, but the toggle must stay reachable —
|
||||
// inside the overlay menu (0010); the FAB is the persistent control.
|
||||
await openOverlayMenu(page);
|
||||
const toggle = page.locator('[data-testid="chrome-toggle"]');
|
||||
await expect(toggle, 'toggle button present in canvas-only mode').toBeVisible();
|
||||
|
||||
|
|
|
|||
16
tests/web/overlay-menu.ts
Normal file
16
tests/web/overlay-menu.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Open the overlay menu (collab-presence 0010) if its panel isn't already
|
||||
* open. The roster, source chip, view-only pill, comment bar and chrome
|
||||
* toggle live inside the panel; the FAB (with the peer-count badge) is the
|
||||
* only overlay control always on screen. Any canvas click closes the panel
|
||||
* (click-away), so specs re-open it before each in-panel interaction.
|
||||
*/
|
||||
export async function openOverlayMenu(page: Page): Promise<void> {
|
||||
const fab = page.getByTestId('overlay-menu-fab');
|
||||
await expect(fab).toBeVisible({ timeout: 30000 });
|
||||
if (await page.getByTestId('overlay-menu-panel').count()) return;
|
||||
await fab.click();
|
||||
await expect(page.getByTestId('overlay-menu-panel')).toBeVisible();
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { openOverlayMenu } from './overlay-menu';
|
||||
|
||||
/**
|
||||
* Presence roster e2e (collab-presence 0001): two tabs of the real React app on
|
||||
|
|
@ -42,11 +43,14 @@ test('two tabs on one file see each other in the roster; leaving removes promptl
|
|||
|
||||
await bootAs(page, 'alice');
|
||||
|
||||
// Before anyone else joins, alice has no roster chip at all.
|
||||
// Before anyone else joins, alice has no roster section in the overlay
|
||||
// menu (0010) at all.
|
||||
await openOverlayMenu(page);
|
||||
await expect(page.getByTestId('presence-roster')).toHaveCount(0);
|
||||
|
||||
const pageB = await context.newPage();
|
||||
await bootAs(pageB, 'bob');
|
||||
await openOverlayMenu(pageB);
|
||||
|
||||
// Each side shows the other user (avatar keyed by slug), not itself.
|
||||
await expect(page.locator('[data-presence-user="bob"]')).toBeVisible({ timeout: 30000 });
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { test, expect, type Browser, type Page } from '@playwright/test';
|
||||
import { openOverlayMenu } from './overlay-menu';
|
||||
|
||||
/**
|
||||
* Read-only viewer e2e (read-only-viewer): `?readonly=1` boots the pcbnew
|
||||
|
|
@ -118,6 +119,8 @@ test('viewer boots locked: chrome-less, nothing selectable, hotkey edits inert,
|
|||
// "View only" pill is the one read-only affordance; Ctrl+\ stays inert.
|
||||
await expect.poll(() => visibleMenuTitles(viewer), { timeout: 15000 }).toBe(0);
|
||||
await expect(viewer.getByText(/console \(/)).toHaveCount(0);
|
||||
// The pill + (absent) chrome toggle live inside the overlay menu (0010).
|
||||
await openOverlayMenu(viewer);
|
||||
await expect(viewer.locator('[data-testid="chrome-toggle"]')).toHaveCount(0);
|
||||
await expect(viewer.getByTestId('view-only-pill')).toBeVisible();
|
||||
await viewer.keyboard.press('Control+\\');
|
||||
|
|
@ -136,6 +139,7 @@ test('viewer boots locked: chrome-less, nothing selectable, hotkey edits inert,
|
|||
expect(await visibleMenuTitles(viewer)).toBe(0);
|
||||
|
||||
// The writer beside it keeps the full UI (positive control for the above).
|
||||
await openOverlayMenu(writer);
|
||||
await expect(writer.locator('[data-testid="chrome-toggle"]')).toBeVisible();
|
||||
await expect(writer.getByTestId('view-only-pill')).toHaveCount(0);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue