diff --git a/packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts b/packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts new file mode 100644 index 000000000..2c513bc41 --- /dev/null +++ b/packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts @@ -0,0 +1,55 @@ +import { expect, test } from "../fixtures" +import { openSidebar, withSession } from "../actions" +import { pawworkSidebarSelector } from "../selectors" + +test("pawwork sidebar merges pin into the status slot with a stable title baseline", async ({ + page, + sdk, + gotoSession, +}) => { + const stamp = Date.now() + await withSession(sdk, `i150 regression ${stamp}`, async (session) => { + await gotoSession(session.id) + await openSidebar(page) + + const sidebar = page.locator(pawworkSidebarSelector).first() + const row = sidebar.locator(`[data-session-id="${session.id}"]`).first() + const title = row.locator("span", { hasText: `i150 regression ${stamp}` }).first() + + const leftOf = async (locator: typeof title) => { + const rect = await locator.evaluate((el) => el.getBoundingClientRect()) + return Math.round(rect.left) + } + + // Baseline: unpinned, no hover. + await page.mouse.move(0, 0) + await expect(row).toBeVisible() + const baseline = await leftOf(title) + + // Hover must not shift the title horizontally (action slot grows on the right). + await row.hover() + expect(await leftOf(title)).toBe(baseline) + + // Pin via row menu; the row rerenders in the pinned section. + await row.locator('[data-action="session-row-menu"]').click() + await page.getByRole("menuitem", { name: /pin session/i }).click() + const pinnedRow = sidebar + .locator(`[data-component="pawwork-sidebar-pinned"] [data-session-id="${session.id}"]`) + .first() + await expect(pinnedRow).toBeVisible() + + const pinnedTitle = pinnedRow.locator("span", { hasText: `i150 regression ${stamp}` }).first() + await page.mouse.move(0, 0) + expect(await leftOf(pinnedTitle)).toBe(baseline) + await pinnedRow.hover() + expect(await leftOf(pinnedTitle)).toBe(baseline) + + // Pin button should occupy the row's leading slot, not a separate column. + const pinButton = pinnedRow.locator('[data-action="pawwork-session-pin"][data-pinned="true"]').first() + await expect(pinButton).toBeVisible() + const pinRect = await pinButton.evaluate((el) => el.getBoundingClientRect()) + const rowRect = await pinnedRow.evaluate((el) => el.getBoundingClientRect()) + expect(Math.round(pinRect.left - rowRect.left)).toBe(8) + expect(Math.round(pinRect.width)).toBe(24) + }) +}) diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index bfe33e453..e9fc147ad 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -96,6 +96,7 @@ const SessionRow = (props: { warmPress: () => void warmFocus: () => void titleContent?: JSX.Element + leadingSlot?: JSX.Element }): JSX.Element => { const title = () => sessionTitle(props.session.title) const indicator = () => { @@ -103,7 +104,7 @@ const SessionRow = (props: { if (props.hasPermissions()) return
if (props.hasError()) return
if (props.unseenCount() > 0) return
- return null + return props.leadingSlot ?? null } return ( @@ -195,6 +196,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { warmPress={() => warm(2, "high")} warmFocus={() => warm(2, "high")} titleContent={props.titleContent?.({ session: props.session, title: () => sessionTitle(props.session.title) ?? "" })} + leadingSlot={!props.level && props.leadingSlot ? props.leadingSlot(props.session) : undefined} /> ) @@ -206,9 +208,6 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => { style={{ "padding-left": `${8 + (props.level ?? 0) * 16}px` }} >
- -
{props.leadingSlot?.(props.session)}
-