feat(web): sidebar group polish — even hover geometry, folder open/close icons, group pinning (#35)

Co-authored-by: Alice <alice@prismshadow.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-07-23 00:35:41 +08:00
committed by GitHub
parent 78a8b35793
commit 829ca57a43
6 changed files with 234 additions and 31 deletions
+42
View File
@@ -8,6 +8,8 @@
* "临时工作区" group, a named directory groups under its basename, and that group header's
* "+" pre-fills the draft's Workspace (via router state, applied once per navigation — a
* manual change made afterwards survives a reload instead of being re-overridden);
* - the group header's hover pin toggle lifts a group above the unpinned ones in its mode and
* persists per Project (order re-checked after a reload);
* - after switching the sidebar to agent mode (toggle persisted in localStorage), the agent
* group header's "+" creates a draft scoped to that group's Agent (explicitly set via router
* state, overriding the cache).
@@ -205,6 +207,46 @@ test("draft: pick model/approval -> reload restores them -> send creates the ses
await expect(page.getByLabel("Workspace")).toContainText(parentLabel);
await expect(page.getByLabel("Workspace")).not.toContainText(wsLabel);
// —— Pinning: the header's hover pin toggle lifts a group above the others in its mode and
// persists per Project (localStorage penguin.sidebarPinnedGroups.<projectId>); order is
// asserted geometrically, like layout.spec does for the login language buttons. ——
const tempLabel = page.getByText("临时工作区", { exact: true });
const namedLabel = page.getByText(wsLabel, { exact: true });
const yOf = async (locator) => (await locator.boundingBox())?.y ?? -1;
// Unpinned baseline: the merged temp group sits below the named group.
await expect(tempLabel).toBeVisible();
expect(await yOf(tempLabel)).toBeGreaterThan(await yOf(namedLabel));
const tempHeader = tempLabel.locator("xpath=ancestor::div[contains(@class,'items-center')][1]");
await tempHeader.hover();
await tempHeader.getByRole("button", { name: "置顶分组" }).click();
await expect
.poll(() =>
page.evaluate((k) => localStorage.getItem(k), `penguin.sidebarPinnedGroups.${projectId}`),
)
.toContain("temp-workspaces");
await expect.poll(async () => (await yOf(tempLabel)) < (await yOf(namedLabel))).toBe(true);
// Pinned state and order survive a reload. The pin button's accessible name is STATIC
// ("置顶分组"); the state lives in aria-pressed alone (review: a name swapping to 取消置顶
// alongside aria-pressed announces the state twice in conflicting ways).
await page.reload();
await expect(tempLabel).toBeVisible();
await expect.poll(async () => (await yOf(tempLabel)) < (await yOf(namedLabel))).toBe(true);
const tempPin = tempHeader.getByRole("button", { name: "置顶分组" });
await expect(tempPin).toHaveAttribute("aria-pressed", "true");
// The pinned pin doubles as the always-visible indicator. toBeVisible() ignores opacity,
// so assert computed opacity directly: pinned = 1 with the mouse elsewhere; an unpinned
// header's pin stays hover-gated at 0. Park the mouse first — after the reorder the named
// header sits where the temp header was clicked, which would otherwise hover-reveal it.
await page.mouse.move(640, 500);
await expect.poll(() => tempPin.evaluate((el) => getComputedStyle(el).opacity)).toBe("1");
await expect
.poll(() =>
wsHeader
.getByRole("button", { name: "置顶分组" })
.evaluate((el) => getComputedStyle(el).opacity),
)
.toBe("0");
// —— Switch the sidebar to agent mode via the section-header toggle (persists in localStorage) ——
await page.getByRole("button", { name: "按 Agent 分组" }).click();
await expect