diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7afb95a852..2880a3e0d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -407,7 +407,7 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # Balance contiguous file groups using CI durations and the trimmed # inventory. Required check names and worker counts stay unchanged. - PWTEST_SHARD_WEIGHTS: "183:185:181:202" + PWTEST_SHARD_WEIGHTS: "187:183:184:201" - name: Upload test artifacts uses: actions/upload-artifact@v4 diff --git a/packages/app/e2e/browser/context-window-meter.spec.ts b/packages/app/e2e/browser/context-window-meter.spec.ts index b3c5f4baab..e432aa52d2 100644 --- a/packages/app/e2e/browser/context-window-meter.spec.ts +++ b/packages/app/e2e/browser/context-window-meter.spec.ts @@ -173,14 +173,6 @@ for (const layout of [ }); } -async function captureContextThemes(page: Page, surface: string, state: string) { - for (const theme of ["light", "dark"] as const) { - await page.emulateMedia({ colorScheme: theme }); - await expect(contextWindowDetails(page)).toBeVisible(); - await qaScreenshot(page, `${surface}-${theme}-${state}`); - } -} - type OpenDetails = (page: Page) => Promise; type Shot = (state: string) => Promise; @@ -249,138 +241,140 @@ async function expectOnlyContextWindowWithoutUsage( await shot("unsupported"); } -test("the context window hover card shows the agent's usage in both themes", async ({ - page, - agent, -}) => { - test.setTimeout(240_000); - const [claude] = claudeAndCodexReports(); - const usage = await scriptAgentUsage(page); - await page.emulateMedia({ colorScheme: "light" }); - await page.setViewportSize(DESKTOP); - await openAgent(page, agent); - const shot: Shot = (state) => captureContextThemes(page, "hover-card-desktop", state); +for (const theme of ["light", "dark"] as const) { + test(`the context window hover card shows the agent's usage (${theme})`, async ({ + page, + agent, + }) => { + test.setTimeout(240_000); + const [claude] = claudeAndCodexReports(); + const usage = await scriptAgentUsage(page); + await page.emulateMedia({ colorScheme: theme }); + await page.setViewportSize(DESKTOP); + await openAgent(page, agent); + const shot: Shot = (state) => qaScreenshot(page, `hover-card-desktop-${theme}-${state}`); - await test.step("reports stream in one card at a time, each with Refresh", async () => { - const card = await expectCardsToStreamIn(page, usage, hoverContextWindowMeter, shot); - await expectRefreshButtons(card, 2); - await shot("ready"); - expect(usage.agentRequests()).toEqual([agent.agentId]); - }); - - await test.step("the card stays open while the pointer moves onto it", async () => { - const card = contextWindowDetails(page); - await moveOntoContextWindowDetails(page, card); - await page.waitForTimeout(300); - await expect(card).toBeVisible(); - }); - - await test.step("Refresh keeps the card open and replaces it with the new report", async () => { - const refreshed = gate(); - usage.answerNext({ stream: [refreshed.promise, expiredLogin(onWorkLogin(claude!))] }); - const card = contextWindowDetails(page); - await refreshUsageCard(card, "Claude"); - await expect(card.getByRole("button", { name: "Refresh Claude" })).toBeDisabled(); - // Past the card's close grace, with the pointer still on the card. - await page.waitForTimeout(500); - await expect(card).toBeVisible(); - refreshed.open(); - await expect(card.getByText(LOGIN_EXPIRED)).toBeVisible(); - expect(usage.refreshedAgents()).toEqual([agent.agentId]); - }); - - await test.step("after a resume under another login, only that login shows", async () => { - // Refresh can retain focus; return to the composer before testing pointer dismissal. - await composerLocator(page).focus(); - // Closed while the old login's report is still on its way. - await leaveContextWindowMeter(page); - const slowSource = gate(); - usage.answerNext({ stream: [slowSource.promise, onWorkLogin(claude!)] }); - const closedEarly = await hoverContextWindowMeter(page); - await expect(closedEarly.getByText("Loading usage...", { exact: true })).toBeVisible(); - await leaveContextWindowMeter(page); - - const resumed = gate(); - const finished = gate(); - usage.answerNext({ - stream: [resumed.promise, onPersonalLogin(claude!), finished.promise], + await test.step("reports stream in one card at a time, each with Refresh", async () => { + const card = await expectCardsToStreamIn(page, usage, hoverContextWindowMeter, shot); + await expectRefreshButtons(card, 2); + await shot("ready"); + expect(usage.agentRequests()).toEqual([agent.agentId]); }); - const card = await hoverContextWindowMeter(page); - await expect(card.getByText("Loading usage...", { exact: true })).toBeVisible(); - await expect(card.getByText("work@example.com", { exact: true })).toHaveCount(0); - // The old login's report lands during the new request, before the new login's. - slowSource.open(); - resumed.open(); - await expect( - usageCard(card, "claude:personal").getByText("personal@example.com", { exact: true }), - ).toBeVisible(); - await expect(card.getByText("work@example.com", { exact: true })).toHaveCount(0); - finished.open(); + await test.step("the card stays open while the pointer moves onto it", async () => { + const card = contextWindowDetails(page); + await moveOntoContextWindowDetails(page, card); + await page.waitForTimeout(300); + await expect(card).toBeVisible(); + }); + + await test.step("Refresh keeps the card open and replaces it with the new report", async () => { + const refreshed = gate(); + usage.answerNext({ stream: [refreshed.promise, expiredLogin(onWorkLogin(claude!))] }); + const card = contextWindowDetails(page); + await refreshUsageCard(card, "Claude"); + await expect(card.getByRole("button", { name: "Refresh Claude" })).toBeDisabled(); + // Past the card's close grace, with the pointer still on the card. + await page.waitForTimeout(500); + await expect(card).toBeVisible(); + refreshed.open(); + await expect(card.getByText(LOGIN_EXPIRED)).toBeVisible(); + expect(usage.refreshedAgents()).toEqual([agent.agentId]); + }); + + await test.step("after a resume under another login, only that login shows", async () => { + // Refresh can retain focus; return to the composer before testing pointer dismissal. + await composerLocator(page).focus(); + // Closed while the old login's report is still on its way. + await leaveContextWindowMeter(page); + const slowSource = gate(); + usage.answerNext({ stream: [slowSource.promise, onWorkLogin(claude!)] }); + const closedEarly = await hoverContextWindowMeter(page); + await expect(closedEarly.getByText("Loading usage...", { exact: true })).toBeVisible(); + await leaveContextWindowMeter(page); + + const resumed = gate(); + const finished = gate(); + usage.answerNext({ + stream: [resumed.promise, onPersonalLogin(claude!), finished.promise], + }); + const card = await hoverContextWindowMeter(page); + await expect(card.getByText("Loading usage...", { exact: true })).toBeVisible(); + await expect(card.getByText("work@example.com", { exact: true })).toHaveCount(0); + + // The old login's report lands during the new request, before the new login's. + slowSource.open(); + resumed.open(); + await expect( + usageCard(card, "claude:personal").getByText("personal@example.com", { exact: true }), + ).toBeVisible(); + await expect(card.getByText("work@example.com", { exact: true })).toHaveCount(0); + finished.open(); + }); + + await test.step("a report with a problem shows it on the card", async () => { + usage.answerNext([expiredLogin(onWorkLogin(claude!))]); + await reloadAgent(page); + const card = await hoverContextWindowMeter(page); + await expect(card.getByText(LOGIN_EXPIRED)).toBeVisible(); + await shot("problem"); + }); + + await test.step("a failed request says so in a sentence", async () => { + await expectFailedRequestSentence(page, usage, hoverContextWindowMeter, shot); + }); + + await test.step("a host without usage reports shows only the context window", async () => { + await expectOnlyContextWindowWithoutUsage(page, usage, hoverContextWindowMeter, shot); + // One request per open that had usage; this one sends none. + expect(usage.agentRequests()).toHaveLength(6); + }); }); - await test.step("a report with a problem shows it on the card", async () => { - usage.answerNext([expiredLogin(onWorkLogin(claude!))]); - await reloadAgent(page); - const card = await hoverContextWindowMeter(page); - await expect(card.getByText(LOGIN_EXPIRED)).toBeVisible(); - await shot("problem"); - }); + test(`the context window sheet shows the agent's usage and refreshes it (${theme})`, async ({ + page, + agent, + }) => { + test.setTimeout(240_000); + const [claude] = claudeAndCodexReports(); + const usage = await scriptAgentUsage(page); + await page.emulateMedia({ colorScheme: theme }); + await page.setViewportSize(COMPACT); + await openAgent(page, agent); + const shot: Shot = (state) => qaScreenshot(page, `sheet-compact-${theme}-${state}`); - await test.step("a failed request says so in a sentence", async () => { - await expectFailedRequestSentence(page, usage, hoverContextWindowMeter, shot); - }); + await test.step("reports stream in one card at a time, each with Refresh", async () => { + const sheet = await expectCardsToStreamIn(page, usage, pressContextWindowMeter, shot); + await expectRefreshButtons(sheet, 2); + // Without hover, each card prints its freshness instead of a Refresh tooltip. + await expect(sheet.getByText("Updated just now", { exact: true })).toHaveCount(2); + await shot("ready"); + expect(usage.agentRequests()).toEqual([agent.agentId]); + }); - await test.step("a host without usage reports shows only the context window", async () => { - await expectOnlyContextWindowWithoutUsage(page, usage, hoverContextWindowMeter, shot); - // One request per open that had usage; this one sends none. - expect(usage.agentRequests()).toHaveLength(6); - }); -}); + await test.step("Refresh replaces the card with the source's new report", async () => { + usage.answerNext([expiredLogin(onWorkLogin(claude!))]); + const sheet = contextWindowDetails(page); + await refreshUsageCard(sheet, "Claude"); + await expect(sheet.getByText(LOGIN_EXPIRED)).toBeVisible(); + expect(usage.refreshedAgents()).toEqual([agent.agentId]); + await shot("problem"); + }); -test("the context window sheet shows the agent's usage and refreshes it in both themes", async ({ - page, - agent, -}) => { - test.setTimeout(240_000); - const [claude] = claudeAndCodexReports(); - const usage = await scriptAgentUsage(page); - await page.emulateMedia({ colorScheme: "light" }); - await page.setViewportSize(COMPACT); - await openAgent(page, agent); - const shot: Shot = (state) => captureContextThemes(page, "sheet-compact", state); + await test.step("Close dismisses the sheet", async () => { + await closeContextWindowSheet(page); + }); - await test.step("reports stream in one card at a time, each with Refresh", async () => { - const sheet = await expectCardsToStreamIn(page, usage, pressContextWindowMeter, shot); - await expectRefreshButtons(sheet, 2); - // Without hover, each card prints its freshness instead of a Refresh tooltip. - await expect(sheet.getByText("Updated just now", { exact: true })).toHaveCount(2); - await shot("ready"); - expect(usage.agentRequests()).toEqual([agent.agentId]); - }); + await test.step("a failed request says so in a sentence", async () => { + await expectFailedRequestSentence(page, usage, pressContextWindowMeter, shot); + }); - await test.step("Refresh replaces the card with the source's new report", async () => { - usage.answerNext([expiredLogin(onWorkLogin(claude!))]); - const sheet = contextWindowDetails(page); - await refreshUsageCard(sheet, "Claude"); - await expect(sheet.getByText(LOGIN_EXPIRED)).toBeVisible(); - expect(usage.refreshedAgents()).toEqual([agent.agentId]); - await shot("problem"); + await test.step("a host without usage reports shows only the context window", async () => { + await expectOnlyContextWindowWithoutUsage(page, usage, pressContextWindowMeter, shot); + expect(usage.agentRequests()).toHaveLength(3); + }); }); - - await test.step("Close dismisses the sheet", async () => { - await closeContextWindowSheet(page); - }); - - await test.step("a failed request says so in a sentence", async () => { - await expectFailedRequestSentence(page, usage, pressContextWindowMeter, shot); - }); - - await test.step("a host without usage reports shows only the context window", async () => { - await expectOnlyContextWindowWithoutUsage(page, usage, pressContextWindowMeter, shot); - expect(usage.agentRequests()).toHaveLength(3); - }); -}); +} test("context details support keyboard entry and dismiss when the viewport moves", async ({ page, diff --git a/packages/app/e2e/browser/usage-modal.spec.ts b/packages/app/e2e/browser/usage-modal.spec.ts index 723cb827b7..a923971d09 100644 --- a/packages/app/e2e/browser/usage-modal.spec.ts +++ b/packages/app/e2e/browser/usage-modal.spec.ts @@ -477,59 +477,49 @@ for (const shape of ["seven-day-only", "Spark"] as const) { }); } -for (const [size, viewport] of Object.entries({ - desktop: { width: 1440, height: 900 }, - compact: { width: 390, height: 844 }, -})) { - test(`Usage Settings cog ${size} in both themes`, async ({ page }, testInfo) => { - test.setTimeout(120_000); - await page.addInitScript(() => { - const key = "@paseo:app-settings"; - const current = JSON.parse(localStorage.getItem(key) ?? "{}"); - localStorage.setItem(key, JSON.stringify({ ...current, theme: "auto" })); - }); - await page.emulateMedia({ colorScheme: "light" }); - const usage = await installUsageReportsFixture(page, { - lists: [() => claudeAndCodexReports()], - }); - await page.setViewportSize(viewport); - await gotoAppShell(page); - if (size === "compact") await openCompactSidebar(page); - await openUsageFromIcon(page); - const screen = page.getByTestId(`usage-host-${getServerId()}`); - await expect(screen.getByText("Claude", { exact: true })).toBeVisible({ timeout: 30_000 }); - - for (const theme of ["light", "dark"] as const) { - await test.step(`the cog and its settings work in ${theme}`, async () => { - await page.emulateMedia({ colorScheme: theme }); - const settings = page.getByTestId("usage-options-menu"); - await expect(settings).toHaveAccessibleName("Settings"); - await expect(page.getByTestId("usage-display-used")).toHaveCount(0); - await captureSettingsState(page, testInfo, `${size}-${theme}-cog`); - - await openUsageOptions(page); - await expectUsageOptionsFitContent(page); - await expect(page.getByText("Pinned windows show in the sidebar footer")).toBeVisible(); - await expect(page.getByTestId("usage-display-used")).toHaveAttribute( - "aria-selected", - "true", - ); - await captureSettingsState(page, testInfo, `${size}-${theme}-settings`); - await showUsageAs(page, "remaining"); - await expect(screen.getByText("69% left")).toBeVisible(); - await showUsageAs(page, "used"); - await expect(pinRow(screen, "Claude", "Session")).toHaveAccessibleName( - /^Pin Claude Session, 31% · resets /, - ); - await openUsageOptions(page); - await closeUsageOptions(page); +for (const theme of ["light", "dark"] as const) { + for (const [size, viewport] of Object.entries({ + desktop: { width: 1440, height: 900 }, + compact: { width: 390, height: 844 }, + })) { + test(`Usage Settings cog ${size} ${theme}`, async ({ page }, testInfo) => { + test.setTimeout(120_000); + await page.addInitScript((value) => { + const key = "@paseo:app-settings"; + const current = JSON.parse(localStorage.getItem(key) ?? "{}"); + localStorage.setItem(key, JSON.stringify({ ...current, theme: value })); + }, theme); + const usage = await installUsageReportsFixture(page, { + lists: [() => claudeAndCodexReports()], }); - } - await test.step("refresh still works after changing the display settings", async () => { + await page.setViewportSize(viewport); + await gotoAppShell(page); + if (size === "compact") await openCompactSidebar(page); + await openUsageFromIcon(page); + const screen = page.getByTestId(`usage-host-${getServerId()}`); + await expect(screen.getByText("Claude", { exact: true })).toBeVisible({ timeout: 30_000 }); + const settings = page.getByTestId("usage-options-menu"); + await expect(settings).toHaveAccessibleName("Settings"); + await expect(page.getByTestId("usage-display-used")).toHaveCount(0); + await captureSettingsState(page, testInfo, `${size}-${theme}-cog`); + + await openUsageOptions(page); + await expectUsageOptionsFitContent(page); + await expect(page.getByText("Pinned windows show in the sidebar footer")).toBeVisible(); + await expect(page.getByTestId("usage-display-used")).toHaveAttribute("aria-selected", "true"); + await captureSettingsState(page, testInfo, `${size}-${theme}-settings`); + await showUsageAs(page, "remaining"); + await expect(screen.getByText("69% left")).toBeVisible(); + await showUsageAs(page, "used"); + await expect(pinRow(screen, "Claude", "Session")).toHaveAccessibleName( + /^Pin Claude Session, 31% · resets /, + ); + await openUsageOptions(page); + await closeUsageOptions(page); await refreshAllUsage(page); await expect.poll(() => forcedRefreshes(usage)).toEqual([{ forceRefresh: true }]); }); - }); + } } async function captureSettingsState(