mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-22 17:22:47 +00:00
test(app): retain separate initial theme journeys
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<Locator>;
|
||||
type Shot = (state: string) => Promise<void>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user