Skip to content
Merged
11 changes: 11 additions & 0 deletions packages/app/e2e/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,17 @@ export async function openRightPanel(page: Page) {
return panel
}

/**
* Returns the right-panel shell tab strip. The Tabs.List is portalled into the
* titlebar (see <Titlebar> #pawwork-titlebar-tabs) so queries scoped to the
* complementary right panel no longer find it. Use this helper instead of
* `rightPanel.getByRole("tablist")` — it stays correct whether the tabs render
* portalled (desktop) or inline.
*/
export function rightPanelTabList(page: Page) {
return page.locator('[data-scope="right-panel"][data-component="tabs"]').getByRole("tablist").first()
}

export async function closeSidebar(page: Page) {
if (await isSidebarClosed(page)) return

Expand Down
37 changes: 24 additions & 13 deletions packages/app/e2e/commands/panels.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { cleanupSession, openSidebar } from "../actions"
import { cleanupSession, openSidebar, rightPanelTabList } from "../actions"
import { pawworkSessionNewSelector, promptSelector, titlebarRightSelector } from "../selectors"
import { modKey } from "../utils"

Expand All @@ -14,7 +14,7 @@ test("desktop right-panel tabs switch between review and files within a unified
await rightToggle.click()
await expect(rightPanel).toHaveAttribute("aria-hidden", "false")

const shellTabList = rightPanel.getByRole("tablist").first()
const shellTabList = rightPanelTabList(page)
const reviewTab = shellTabList.getByRole("tab", { name: "Review", exact: true })
const filesTab = shellTabList.getByRole("tab", { name: "Files", exact: true })
await expect(shellTabList.getByRole("tab", { name: "Status", exact: true })).toBeVisible()
Expand Down Expand Up @@ -121,7 +121,7 @@ test("desktop session keeps a single right-panel toggle and icon-first utility t
await page.keyboard.press(`${modKey}+Shift+R`)
await expect(rightPanel).toHaveAttribute("aria-hidden", "false")

const shellTabList = rightPanel.getByRole("tablist").first()
const shellTabList = rightPanelTabList(page)
await expect(shellTabList.locator('[data-component="icon"]')).toHaveCount(4)

const widths = await shellTabList.locator('[data-slot="tabs-trigger"]').evaluateAll((els) =>
Expand All @@ -139,7 +139,7 @@ test("desktop right-panel shell tabs keep the sidepanel chrome contract", async
await page.keyboard.press(`${modKey}+Shift+R`)
await expect(rightPanel).toHaveAttribute("aria-hidden", "false")

const shellTabList = rightPanel.getByRole("tablist").first()
const shellTabList = rightPanelTabList(page)
const statusWrapper = shellTabList.locator('[data-slot="tabs-trigger-wrapper"]').first()

const wrapperStyles = await statusWrapper.evaluate((el) => {
Expand Down Expand Up @@ -185,7 +185,7 @@ test("desktop right-panel uses the design icon set for utility tabs", async ({ p
await page.keyboard.press(`${modKey}+Shift+R`)
await expect(rightPanel).toHaveAttribute("aria-hidden", "false")

const shellTabList = rightPanel.getByRole("tablist").first()
const shellTabList = rightPanelTabList(page)
const icons = await shellTabList.locator('[data-slot="tabs-trigger"] [data-slot="icon-svg"]').evaluateAll((els) =>
els.map((el) => el.innerHTML),
)
Expand All @@ -203,12 +203,16 @@ test("desktop review root shows a simple toolbar before opening files", async ({
await page.keyboard.press(`${modKey}+Shift+R`)
await expect(rightPanel).toHaveAttribute("aria-hidden", "false")

const shellTabList = rightPanel.getByRole("tablist").first()
const shellTabList = rightPanelTabList(page)
const reviewTab = shellTabList.getByRole("tab", { name: "Review", exact: true })
await reviewTab.click()
await expect(reviewTab).toHaveAttribute("aria-selected", "true")

await expect(rightPanel.getByRole("tablist")).toHaveCount(2)
// Shell tabs are now portalled into the titlebar; only the inner review
// tablist remains inside the complementary panel region.
await expect(rightPanel.getByRole("tablist")).toHaveCount(1)
// The portalled shell tablist is still present and visible.
await expect(shellTabList).toBeVisible()

const openFile = rightPanel.getByRole("button", { name: /^Open file$/i }).first()
await expect(openFile).toBeVisible()
Expand All @@ -222,22 +226,29 @@ test("desktop right-panel collapses shell tab labels below the compact threshold
await page.keyboard.press(`${modKey}+Shift+R`)
await expect(rightPanel).toHaveAttribute("aria-hidden", "false")

const shellTabList = rightPanel.getByRole("tablist").first()
// Shell tabs are portalled into the titlebar; the slot's width — and thus the
// label-collapse container queries — is driven by --right-panel-width set on
// [data-component="desktop-shell"] (layout.tsx). documentElement-level
// overrides lose the cascade to the inline style on desktop-shell, so set the
// var directly on that element.
const shellTabList = rightPanelTabList(page)
const tabLabels = () =>
shellTabList
.locator('[data-slot="tabs-trigger"]')
.evaluateAll((els) => els.map((el) => el.textContent?.trim() ?? ""))

await expect.poll(tabLabels).toEqual(["", "", "", ""])

await rightPanel.evaluate((el) => {
;(el as HTMLElement).style.width = "400px"
await page.evaluate(() => {
const shell = document.querySelector('[data-component="desktop-shell"]') as HTMLElement | null
shell?.style.setProperty("--right-panel-width", "400px")
})

await expect.poll(tabLabels).toEqual(["Status", "Files", "Review", "Terminal"])

await rightPanel.evaluate((el) => {
;(el as HTMLElement).style.width = "320px"
await page.evaluate(() => {
const shell = document.querySelector('[data-component="desktop-shell"]') as HTMLElement | null
shell?.style.setProperty("--right-panel-width", "320px")
})

await expect.poll(tabLabels).toEqual(["", "", "", ""])
Expand Down Expand Up @@ -291,7 +302,7 @@ test("legacy changes side-panel state restores into the review tab", async ({ pa
await gotoSession()

const rightPanel = page.locator("#right-panel")
const shellTabList = rightPanel.getByRole("tablist").first()
const shellTabList = rightPanelTabList(page)

await expect(rightPanel).toHaveAttribute("aria-hidden", "false")
await expect(shellTabList.getByRole("tab", { name: "Review", exact: true })).toHaveAttribute("aria-selected", "true")
Expand Down
8 changes: 5 additions & 3 deletions packages/app/e2e/files/file-tree.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { openRightPanel, withSession } from "../actions"
import { openRightPanel, rightPanelTabList, withSession } from "../actions"

// Historical context: before the right-panel-polish PR (#52), the Review tab
// carried a sibling vertical file-tree pane (#file-tree-panel) that surfaced
Expand All @@ -13,8 +13,10 @@ test("@smoke review tab no longer renders the legacy file-tree sub-panel", async
await withSession(project.sdk, `e2e review layout smoke ${Date.now()}`, async (session) => {
await project.gotoSession(session.id)

const rightPanel = await openRightPanel(page)
const shellTabList = rightPanel.getByRole("tablist")
await openRightPanel(page)
// Tabs.List is portalled into the titlebar — use the scope-aware helper instead
// of scoping to the complementary region.
const shellTabList = rightPanelTabList(page)
await shellTabList.locator("button").last().click()
await page.getByRole("menuitem", { name: "Review" }).click()

Expand Down
9 changes: 6 additions & 3 deletions packages/app/e2e/inputs/select-review-filter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
* IconButtons toggle aria-pressed correctly.
*/
import type { Page } from "@playwright/test"
import { openRightPanel, withSession } from "../actions"
import { openRightPanel, rightPanelTabList, withSession } from "../actions"
import { test, expect } from "../fixtures"
import { bodyText } from "../prompt/mock"

async function openReviewPanel(page: Page) {
const panel = await openRightPanel(page)
const tabList = panel.getByRole("tablist").first()
await openRightPanel(page)
// Tabs.List is portalled into the titlebar (see #pawwork-titlebar-tabs), so it
// no longer lives inside the complementary right-panel region — query via the
// data-scope helper instead of `panel.getByRole("tablist")`.
const tabList = rightPanelTabList(page)
const reviewTab = tabList.getByRole("tab", { name: "Review", exact: true })

if (await reviewTab.isVisible().catch(() => false)) {
Expand Down
4 changes: 2 additions & 2 deletions packages/app/e2e/prompt/prompt-slash-terminal.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { runPromptSlash, waitTerminalFocusIdle } from "../actions"
import { runPromptSlash, waitTerminalFocusIdle, rightPanelTabList } from "../actions"
import { promptSelector, terminalSelector } from "../selectors"

test("/terminal opens the right-panel terminal tab", async ({ page, gotoSession }) => {
Expand All @@ -8,7 +8,7 @@ test("/terminal opens the right-panel terminal tab", async ({ page, gotoSession
const prompt = page.locator(promptSelector)
const terminal = page.locator(terminalSelector)
const rightPanel = page.locator("#right-panel")
const shellTabList = rightPanel.getByRole("tablist").first()
const shellTabList = rightPanelTabList(page)
const terminalTab = shellTabList.getByRole("tab", { name: "Terminal", exact: true })
const embeddedTerminalTabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]')

Expand Down
5 changes: 5 additions & 0 deletions packages/app/e2e/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export const projectWorkspacesToggleSelector = (slug: string) =>
`[data-action="project-workspaces-toggle"][data-project="${slug}"]`

export const titlebarRightSelector = "#pawwork-titlebar-right"
// Right-panel shell tabs are portalled into the titlebar so the tab strip reads
// as window chrome instead of a second toolbar (see <Titlebar> #pawwork-titlebar-tabs).
// Scoping by data-scope (stamped on the slot) keeps test queries resilient to
// portal-vs-inline rendering.
export const rightPanelTabsScopeSelector = '[data-scope="right-panel"]'
export const sidebarNavMobileSelector = '[data-component="sidebar-nav-mobile"]'

export const popoverBodySelector = '[data-slot="popover-body"]'
Expand Down
8 changes: 4 additions & 4 deletions packages/app/e2e/session/session-artifacts.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "../fixtures"
import { openRightPanel, waitSessionIdle } from "../actions"
import { openRightPanel, rightPanelTabList, waitSessionIdle } from "../actions"
import { bodyText } from "../prompt/mock"

test("added files stay quiet until the user opens the Files tab", async ({ page, llm, project }) => {
Expand Down Expand Up @@ -50,11 +50,11 @@ test("added files stay quiet until the user opens the Files tab", async ({ page,
const rightPanel = page.locator('[data-component="right-panel"]')
await expect(rightPanel).toHaveAttribute("aria-hidden", "true")

const panel = await openRightPanel(page)
await panel.getByRole("button", { name: "Add tab" }).click()
await openRightPanel(page)
const shellTabList = rightPanelTabList(page)
await shellTabList.getByRole("button", { name: "Add tab" }).click()
await page.getByRole("menuitem", { name: "Files" }).click()

const shellTabList = panel.getByRole("tablist").first()
const filesTab = shellTabList.getByRole("tab", { name: "Files", exact: true })
await expect(filesTab).toHaveAttribute("aria-selected", "true")
await expect(page.locator('[data-artifact-file="artifact-report.md"]')).toBeVisible({ timeout: 30000 })
Expand Down
8 changes: 5 additions & 3 deletions packages/app/e2e/session/session-composer-dock.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
closeSettingsPanel,
openSettings,
openRightPanel,
rightPanelTabList,
seedSessionQuestion,
} from "../actions"
import {
Expand Down Expand Up @@ -951,9 +952,10 @@ test("todo updates do not switch an open right panel to status", async ({ page,
const rightPanel = await openRightPanel(page)
await expect(rightPanel).toHaveAttribute("aria-hidden", "false")

await rightPanel.getByRole("button", { name: "Add tab" }).click()
const shellTabList = rightPanelTabList(page)
await shellTabList.getByRole("button", { name: "Add tab" }).click()
await page.getByRole("menuitem", { name: "Files" }).click()
const filesTab = rightPanel.getByRole("tab", { name: "Files", exact: true }).first()
const filesTab = shellTabList.getByRole("tab", { name: "Files", exact: true })
await expect(filesTab).toHaveAttribute("aria-selected", "true")

await e2eUpdateTodos(
Expand Down Expand Up @@ -991,7 +993,7 @@ test("todo updates remain visible in the status panel", async ({ page, project }

const rightPanel = await openRightPanel(page)
await expect(rightPanel).toHaveAttribute("aria-hidden", "false")
const statusTab = rightPanel.getByRole("tab", { name: "Status", exact: true }).first()
const statusTab = rightPanelTabList(page).getByRole("tab", { name: "Status", exact: true })
await statusTab.click()
await expect(statusTab).toHaveAttribute("aria-selected", "true")

Expand Down
6 changes: 3 additions & 3 deletions packages/app/e2e/session/session-review.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readFile } from "node:fs/promises"
import { waitSessionIdle, withSession } from "../actions"
import { waitSessionIdle, withSession, rightPanelTabList } from "../actions"
import { test, expect } from "../fixtures"
import { bodyText } from "../prompt/mock"
import { titlebarRightSelector } from "../selectors"
Expand Down Expand Up @@ -108,14 +108,14 @@ async function patchWithMock(
async function show(page: Parameters<typeof test>[0]["page"]) {
const rightToggle = page.locator(`${titlebarRightSelector} button`).first()
const rightPanel = page.locator("#right-panel")
const shellTabList = rightPanel.getByRole("tablist").first()
const shellTabList = rightPanelTabList(page)
const reviewTab = shellTabList.getByRole("tab", { name: "Review", exact: true })

await expect(rightToggle).toBeVisible()
if ((await rightPanel.getAttribute("aria-hidden")) === "true") await rightToggle.click()
await expect(rightPanel).toHaveAttribute("aria-hidden", "false")
if ((await reviewTab.count()) === 0) {
await rightPanel.getByRole("button", { name: "Add tab" }).click()
await shellTabList.getByRole("button", { name: "Add tab" }).click()
await page.getByRole("menuitem", { name: /Review/ }).click()
}
await reviewTab.click()
Expand Down
Loading