mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
* feat(usage): stream reports for live agent accounts Resolve account scope from the running harness environment without invoking lifecycle hooks. Keep the client aggregate result while delivering report updates as each fetch settles; the app presentation follows separately. * fix(usage): retain account scope for open agent sessions Capture the launch environment once per session, independently of lazy child-process startup or exit. Use a session UUID for registry identity. Update remaining global discovery fixtures to the scoped SDK contract. * test(usage): update browser fixtures for scoped report streams * feat(usage): let a usage card show window rows without pins UsageCard takes a required pinnable prop. When false, a window row is a plain bar: no Pressable, hover highlight, or pin glyph. The settings Usage list passes true. * feat(usage): show the agent's usage in the context window popover Reports stream into the query cache as each update arrives, for the host list and for a new per-agent query keyed by agent. The context window popover fetches its agent's reports while open and renders one unpinnable card per report as it lands, a loading sentence before the first, the request error as a sentence, and nothing on hosts without usage reports. * feat(usage): hide the Refresh button on popover usage cards UsageCard takes a required refreshable prop. The popover passes false because its content takes no pointer events, so the button could not be pressed; the card then shows its freshness inline. The settings Usage list keeps the button. * feat(usage): make the sidebar Usage summary opt-in The sidebar Usage item starts hidden on every layout, not only on compact ones, so resolveSidebarNavItems drops its compact input. While the summary is off, the Usage list renders window rows without pins; saved pins stay in preferences and return when it is turned on. Specs that read the summary now turn it on first. * docs(glossary): describe the popover usage card and the opt-in sidebar summary * fix(usage): show a popover request error without retrying A daemon error for an agent's usage, such as an unknown agent, does not heal on retry, and reopening the popover fetches again. Retrying held the loading sentence for about seven seconds before the error. * test(usage): cover agent usage in the context window popover The usage fixture records agentId, streams reports held behind promises, and can switch host support between reloads. The meter spec walks loading, streaming, a report problem, a request error and a host without usage reports, on desktop and compact in both themes. * fix(usage): keep the plain context window tooltip on hosts without usage The popover widened to fit usage cards on every host. Hosts that cannot report usage now keep the tooltip's default padding and width, as before, and mount no usage section. * fix(usage): let a report's Refresh replace it in the agent's list too Refresh wrote the refreshed report only into the host's list, so a card rendered from an agent's list kept the old report. Both lists now sit under one host key and Refresh replaces the report in each that holds it. * feat(usage): open the context window details in a sheet on compact screens Wide screens keep the tooltip. On compact screens pressing the meter opens a content-sized bottom sheet titled Context window. Both render one ContextWindowDetails body; only the sheet's usage cards have Refresh, since the tooltip cannot be pressed. Pins stay off in both. * test(usage): cover the compact context window sheet On the compact viewport the meter opens the sheet; its cards have Refresh, a Refresh replaces the card, and Close dismisses the sheet. The glossary names the sheet. * feat(ui): give tooltips and the context window sheet accessible roles Tooltip content is role=tooltip, and the context window sheet is a dialog named Context window, so assistive tech and tests find them by role. The agent usage message drops its test ID; it is found by its text. * test(usage): reshape the context window and Usage switch specs as journeys The context window spec runs one tooltip journey and one sheet journey per theme, with no layout branches. Its fixture script, meter actions and report builders move to a context-window helper, and it finds the meter, tooltip, sheet and Refresh buttons by role. The theme comes from the system colour scheme. The Usage switch test uses named helpers for the Usage icon and the Summary in sidebar switch. * fix(usage): say an agent's usage request failed instead of showing older reports A failed request keeps whatever reports the query held: those from an earlier open, or those that streamed in before the failure. The context window details showed them as if they were the agent's current usage. The request error now wins over held reports. * test(usage): let a fixture clean up the context window spec's agent The tooltip and sheet tests no longer wrap their steps in try/finally; a spec-local fixture seeds the agent and removes it after the test. * fix(usage): keep a refreshed report when the list it is in finishes Pressing Refresh on one card while another source was still streaming put the pre-refresh report back: the finished list replaced the query's data with the copies the request had fetched when it started. A report now keeps whichever copy was fetched later, both when a streamed copy arrives and when the finished list replaces the streamed one. * fix(usage): show only the login a resumed agent runs under now An agent resumed under another login keeps its ID, so its usage query kept the previous login's reports: reopening the context window showed them while the new login's reports loaded, then both side by side until the request finished. The agent's usage is now dropped as soon as its tooltip or sheet closes, so each open loads the login the agent runs under at that moment. * fix(usage): stop a closed agent usage request from writing reports Closing the context window details cancels the agent's usage request and drops reports still on their way, so reopening after a resume under another login sends a new request instead of joining the old one.
493 lines
18 KiB
TypeScript
493 lines
18 KiB
TypeScript
import { tmpdir } from "node:os";
|
|
import { randomUUID } from "node:crypto";
|
|
import path from "node:path";
|
|
import type { Page } from "@playwright/test";
|
|
import type { UsageReportEntry } from "@getpaseo/protocol/messages";
|
|
import { expect, test } from "../support/fixtures";
|
|
import { gotoAppShell } from "../support/helpers/app";
|
|
import { addConnectedHostAndReload } from "../support/helpers/hosts";
|
|
import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon";
|
|
import { getServerId } from "../support/helpers/server-id";
|
|
import { seedSidebarFooterPreferences } from "../support/helpers/sidebar-nav-settings";
|
|
import {
|
|
installUsageReportsFixture,
|
|
type UsageListRequest,
|
|
type UsageReportsFixture,
|
|
} from "../support/helpers/usage-reports";
|
|
import {
|
|
installLoginUsage,
|
|
installCodexWindowUsage,
|
|
expectCodexReportedWindows,
|
|
openUsage,
|
|
refreshLoginUsage,
|
|
hoverUsageWindow,
|
|
} from "../support/helpers/usage-login";
|
|
import {
|
|
claudeAndCodexReports,
|
|
expectPinnedUsage,
|
|
pinRow,
|
|
openUsageOptions,
|
|
showUsageAs,
|
|
refreshAllUsage,
|
|
togglePin,
|
|
usageItem,
|
|
} from "../support/helpers/usage-sidebar-item";
|
|
|
|
const emptyHome = path.join(tmpdir(), `paseo-usage-empty-${randomUUID()}`);
|
|
test.use({
|
|
e2eDaemonEnvironment: {
|
|
HOME: emptyHome,
|
|
USERPROFILE: emptyHome,
|
|
CODEX_HOME: emptyHome,
|
|
CLAUDE_CONFIG_DIR: emptyHome,
|
|
XDG_DATA_HOME: emptyHome,
|
|
PI_CODING_AGENT_DIR: emptyHome,
|
|
COPILOT_TOKEN: "",
|
|
GITHUB_TOKEN: "",
|
|
GITHUB_PAT: "",
|
|
CURSOR_ACCESS_TOKEN: "",
|
|
CURSOR_TOKEN: "",
|
|
GROK_API_KEY: "",
|
|
GROK_TOKEN: "",
|
|
KIMI_TOKEN: "",
|
|
KIMI_API_KEY: "",
|
|
KIMI_CODE_HOME: emptyHome,
|
|
MINIMAX_API_KEY: "",
|
|
ZAI_API_KEY: "",
|
|
GLM_API_KEY: "",
|
|
},
|
|
});
|
|
|
|
// These tests read the sidebar summary and pin windows, which both need the Usage item on.
|
|
test.beforeEach(async ({ page }) => {
|
|
await seedSidebarFooterPreferences(page, [{ key: "usage", visible: true }]);
|
|
});
|
|
|
|
function forcedRefreshes(usage: UsageReportsFixture): UsageListRequest[] {
|
|
return usage.listRequests().filter((request) => request.forceRefresh);
|
|
}
|
|
|
|
// Two hours reads "2h ago" for an hour, so the assertion cannot race the clock.
|
|
function twoHoursAgo(): string {
|
|
return new Date(Date.now() - 2 * 60 * 60_000).toISOString();
|
|
}
|
|
|
|
/** Set PASEO_QA_SCREENSHOT_DIR to keep a QA screenshot. */
|
|
async function qaScreenshot(page: Page, name: string) {
|
|
const directory = process.env.PASEO_QA_SCREENSHOT_DIR;
|
|
if (!directory) return;
|
|
await page.waitForTimeout(600);
|
|
await page.addStyleTag({ content: ".__expo_fast_refresh { display: none !important; }" });
|
|
await page.screenshot({ path: path.join(directory, `${name}.png`) });
|
|
}
|
|
|
|
function hostFilter(page: Page) {
|
|
return page.locator('[data-testid="usage-host-filter-trigger"]:visible');
|
|
}
|
|
|
|
function weeklyReport(sourceId: string, usedPct: number): UsageReportEntry {
|
|
return {
|
|
id: `${sourceId}:a`,
|
|
account: {},
|
|
fetchedAt: twoHoursAgo(),
|
|
sourceId,
|
|
sourceLabel: `${sourceId} plan`,
|
|
report: { status: "available", windows: [{ id: "weekly", label: "Weekly", usedPct }] },
|
|
};
|
|
}
|
|
|
|
test.describe("usage screen", () => {
|
|
test("opens from the sidebar on the host's reports, with no host filter for one host", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(120_000);
|
|
const serverId = getServerId();
|
|
const usage = await installUsageReportsFixture(page, {
|
|
lists: [
|
|
[
|
|
{
|
|
id: "alpha:a",
|
|
account: {},
|
|
fetchedAt: twoHoursAgo(),
|
|
sourceId: "alpha",
|
|
sourceLabel: "Alpha plan",
|
|
report: {
|
|
status: "available",
|
|
windows: [{ id: "weekly", label: "Weekly", usedPct: 31 }],
|
|
},
|
|
},
|
|
{
|
|
id: "beta:b",
|
|
account: {},
|
|
fetchedAt: "2026-01-01T00:00:00.000Z",
|
|
sourceId: "beta",
|
|
sourceLabel: "Beta plan",
|
|
report: {
|
|
status: "unavailable",
|
|
problem: { kind: "no_quota", detail: "No active coding plan" },
|
|
},
|
|
},
|
|
],
|
|
],
|
|
});
|
|
|
|
await gotoAppShell(page);
|
|
await page.locator('[data-testid="sidebar-usage"]:visible').first().click();
|
|
await expect(page).toHaveURL(/\/usage$/);
|
|
await usage.waitForListRequests(1);
|
|
|
|
const group = page.getByTestId(`usage-host-${serverId}`);
|
|
await expect(group.getByText("Alpha plan", { exact: true })).toBeVisible({ timeout: 10_000 });
|
|
await expect(hostFilter(page)).toHaveCount(0);
|
|
await qaScreenshot(page, "phase7-usage-screen-one-host");
|
|
await expect(group.getByText("31%")).toBeVisible();
|
|
await expect(group.getByText("Beta plan", { exact: true })).toBeVisible();
|
|
await expect(group.getByText("Unavailable", { exact: true })).toBeVisible();
|
|
|
|
await group.getByTestId("usage-refresh").first().hover();
|
|
await expect(page.getByTestId("usage-freshness-tooltip")).toHaveText("Updated 2h ago");
|
|
await expect(group.getByTestId("usage-freshness")).toHaveCount(0);
|
|
});
|
|
|
|
test("refreshes one report from its card", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const serverId = getServerId();
|
|
const beta: UsageReportEntry = {
|
|
id: "beta:b",
|
|
account: {},
|
|
fetchedAt: twoHoursAgo(),
|
|
sourceId: "beta",
|
|
sourceLabel: "Beta plan",
|
|
report: {
|
|
status: "available",
|
|
windows: [{ id: "weekly", label: "Weekly", usedPct: 12 }],
|
|
},
|
|
};
|
|
const alpha = (usedPct: number, fetchedAt: string): UsageReportEntry => ({
|
|
id: "alpha:a",
|
|
account: {},
|
|
fetchedAt,
|
|
sourceId: "alpha",
|
|
sourceLabel: "Alpha plan",
|
|
report: {
|
|
status: "available",
|
|
windows: [{ id: "weekly", label: "Weekly", usedPct }],
|
|
},
|
|
});
|
|
// The sidebar summary and the screen each load reports; only a card's Refresh forces one.
|
|
const usage = await installUsageReportsFixture(page, {
|
|
lists: [
|
|
(request) =>
|
|
request.forceRefresh
|
|
? [alpha(58, new Date().toISOString())]
|
|
: [alpha(31, twoHoursAgo()), beta],
|
|
],
|
|
});
|
|
|
|
await gotoAppShell(page);
|
|
await page.locator('[data-testid="sidebar-usage"]:visible').first().click();
|
|
const group = page.getByTestId(`usage-host-${serverId}`);
|
|
await expect(group.getByText("31%")).toBeVisible({ timeout: 10_000 });
|
|
|
|
const alphaRefresh = group.getByTestId("usage-refresh").first();
|
|
await alphaRefresh.click();
|
|
await expect
|
|
.poll(() => forcedRefreshes(usage))
|
|
.toEqual([{ forceRefresh: true, reportIds: ["alpha:a"] }]);
|
|
await expect(group.getByText("58%")).toBeVisible();
|
|
await expect(group.getByText("12%")).toBeVisible();
|
|
|
|
await group.getByTestId("usage-refresh").nth(1).hover();
|
|
await expect(page.getByTestId("usage-freshness-tooltip")).toHaveText("Updated 2h ago");
|
|
await alphaRefresh.hover();
|
|
await expect(page.getByTestId("usage-freshness-tooltip")).toHaveText("Updated just now");
|
|
});
|
|
|
|
test("shows the host once it connects after a cold load on a phone", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const serverId = getServerId();
|
|
const usage = await installUsageReportsFixture(page, {
|
|
lists: [
|
|
[
|
|
{
|
|
id: "alpha:a",
|
|
account: {},
|
|
fetchedAt: twoHoursAgo(),
|
|
sourceId: "alpha",
|
|
sourceLabel: "Alpha plan",
|
|
report: { status: "available", windows: [] },
|
|
},
|
|
],
|
|
],
|
|
});
|
|
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await page.goto("/usage");
|
|
await usage.waitForListRequests(1);
|
|
|
|
const group = page.getByTestId(`usage-host-${serverId}`);
|
|
await expect(group.getByText("Alpha plan", { exact: true })).toBeVisible({ timeout: 10_000 });
|
|
await expect(group.getByTestId("usage-freshness")).toHaveText("Updated 2h ago");
|
|
});
|
|
|
|
test("tells the user to update a host without usage support", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const serverId = getServerId();
|
|
const usage = await installUsageReportsFixture(page, { usageSupported: false });
|
|
|
|
await gotoAppShell(page);
|
|
// Without reports the footer has no Usage item; its Usage icon opens the screen.
|
|
await page.locator('[data-testid="sidebar-usage-icon"]:visible').first().click();
|
|
|
|
await expect(
|
|
page.getByTestId(`usage-host-${serverId}`).getByText(/^Update .+ to see usage$/),
|
|
).toBeVisible({ timeout: 10_000 });
|
|
await qaScreenshot(page, "phase7-usage-update-host");
|
|
await expect(page.getByTestId("usage-options-toggle")).toHaveCount(0);
|
|
await expect(page.getByTestId("usage-refresh-all")).toHaveCount(0);
|
|
expect(usage.listRequests()).toHaveLength(0);
|
|
});
|
|
|
|
test("a host picked on the Usage screen is the sidebar's host too, after a reload", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(420_000);
|
|
const primaryServerId = getServerId();
|
|
const secondary = await startIsolatedHostDaemon(
|
|
`srv_usage_${randomUUID().replaceAll("-", "").slice(0, 12)}`,
|
|
);
|
|
try {
|
|
await installUsageReportsFixture(page, { lists: [[weeklyReport("alpha", 31)]] });
|
|
await installUsageReportsFixture(page, {
|
|
port: secondary.port,
|
|
lists: [[weeklyReport("beta", 12)]],
|
|
});
|
|
await gotoAppShell(page);
|
|
await addConnectedHostAndReload(page, {
|
|
serverId: secondary.serverId,
|
|
label: "Secondary box",
|
|
port: secondary.port,
|
|
});
|
|
|
|
// Nothing picked and no workspace open: the first host.
|
|
await expectPinnedUsage(page, ["31% Weekly"]);
|
|
await usageItem(page).click();
|
|
await expect(page.getByTestId(`usage-host-${primaryServerId}`)).toBeVisible();
|
|
|
|
await hostFilter(page).click();
|
|
await page.getByTestId(`usage-host-filter-item-${secondary.serverId}`).click();
|
|
await expect(
|
|
page.getByTestId(`usage-host-${secondary.serverId}`).getByText("12%"),
|
|
).toBeVisible({ timeout: 30_000 });
|
|
await expect(hostFilter(page)).toContainText("Secondary box");
|
|
await qaScreenshot(page, "usage-screen-picked-host");
|
|
await expectPinnedUsage(page, ["12% Weekly"]);
|
|
|
|
// The e2e seed resets the host list on every load, so reopening re-adds the second host.
|
|
await addConnectedHostAndReload(page, {
|
|
serverId: secondary.serverId,
|
|
label: "Secondary box",
|
|
port: secondary.port,
|
|
});
|
|
await expectPinnedUsage(page, ["12% Weekly"]);
|
|
await expect(
|
|
page.getByTestId(`usage-host-${secondary.serverId}`).getByText("12%"),
|
|
).toBeVisible({ timeout: 30_000 });
|
|
} finally {
|
|
await secondary.close().catch(() => undefined);
|
|
}
|
|
});
|
|
});
|
|
|
|
const loginWindows: UsageReportEntry["report"] = {
|
|
status: "available",
|
|
windows: [
|
|
{ id: "five_hour", label: "Session", shortLabel: "5h", summary: true, usedPct: 31 },
|
|
{ id: "weekly", label: "Weekly", shortLabel: "wk", summary: true, usedPct: 54 },
|
|
],
|
|
};
|
|
|
|
test("expired login refreshes to windows with visible pin toggles", async ({ page }) => {
|
|
const fixture = await installLoginUsage({
|
|
status: "unavailable",
|
|
problem: {
|
|
kind: "expired",
|
|
expiresAt: new Date(Date.now() - 3_600_000).toISOString(),
|
|
refreshedBy: "claude",
|
|
},
|
|
});
|
|
try {
|
|
await gotoAppShell(page);
|
|
await openUsage(page);
|
|
await expect(
|
|
page.getByText("Login expired 1h ago. Run claude to refresh it.", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(page.getByText("Unavailable", { exact: true })).toBeVisible();
|
|
await qaScreenshot(page, "usage-expired-login");
|
|
await fixture.setReport(loginWindows);
|
|
await refreshLoginUsage(page);
|
|
await expect(
|
|
page.getByText("Login expired 1h ago. Run claude to refresh it.", { exact: true }),
|
|
).toHaveCount(0);
|
|
await expectPinnedUsage(page, ["31% 5h", "54% wk"]);
|
|
const row = page.getByRole("checkbox", { name: /^Pin Claude Weekly, / });
|
|
await expect(row).toBeChecked();
|
|
await expect(row.getByTestId("usage-pin-glyph-pinned")).toHaveCSS("opacity", "1");
|
|
await hoverUsageWindow(page, "Weekly");
|
|
await expect(page.getByText("Unpin", { exact: true })).toBeVisible();
|
|
await expect(row.getByTestId("usage-pin-glyph-pinned")).toHaveCSS("opacity", "1");
|
|
await expect(row.getByTestId("usage-pin-glyph-pinned").locator("svg")).not.toHaveAttribute(
|
|
"fill",
|
|
"none",
|
|
);
|
|
await qaScreenshot(page, "usage-pin-hover");
|
|
await togglePin(page.getByTestId("usage-report-login-journey:account"), "Claude", "Weekly");
|
|
await expect(row.getByTestId("usage-pin-glyph-unpinned")).toHaveCSS("opacity", "1");
|
|
await expect(row.getByTestId("usage-pin-glyph-unpinned").locator("svg")).toHaveAttribute(
|
|
"fill",
|
|
"none",
|
|
);
|
|
await hoverUsageWindow(page, "Weekly");
|
|
await expect(page.getByText("Pin", { exact: true })).toBeVisible();
|
|
await expectPinnedUsage(page, ["31% 5h"]);
|
|
await qaScreenshot(page, "usage-pin-unselected");
|
|
await togglePin(page.getByTestId("usage-report-login-journey:account"), "Claude", "Weekly");
|
|
await expect(row.getByTestId("usage-pin-glyph-pinned")).toHaveCSS("opacity", "1");
|
|
await expectPinnedUsage(page, ["31% 5h", "54% wk"]);
|
|
} finally {
|
|
await fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
test("compact usage rows always show the pin glyph", async ({ page }) => {
|
|
const fixture = await installLoginUsage(loginWindows);
|
|
try {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await gotoAppShell(page);
|
|
await openUsage(page);
|
|
const card = page.getByTestId("usage-report-login-journey:account");
|
|
await expect(card.getByTestId("usage-pin-glyph-pinned")).toHaveCount(2);
|
|
await expect(card.getByTestId("usage-pin-glyph-pinned").nth(0)).toHaveCSS("opacity", "1");
|
|
await expect(card.getByTestId("usage-pin-glyph-pinned").nth(1)).toHaveCSS("opacity", "1");
|
|
await qaScreenshot(page, "usage-pin-compact");
|
|
} finally {
|
|
await fixture.cleanup();
|
|
}
|
|
});
|
|
|
|
for (const shape of ["seven-day-only", "Spark"] as const) {
|
|
test(`Codex ${shape} windows use provider durations on the card and sidebar`, async ({
|
|
page,
|
|
}) => {
|
|
const reset = Math.floor(Date.now() / 1000) + 5 * 86400;
|
|
const source = await installCodexWindowUsage({
|
|
rate_limit: {
|
|
primary_window: { used_percent: 11, limit_window_seconds: 604800, reset_at: reset },
|
|
secondary_window: null,
|
|
},
|
|
additional_rate_limits:
|
|
shape === "Spark"
|
|
? [
|
|
{
|
|
limit_name: "GPT-5.3-Codex-Spark",
|
|
metered_feature: "codex_bengalfox",
|
|
rate_limit: {
|
|
primary_window: {
|
|
used_percent: 0,
|
|
limit_window_seconds: 18000,
|
|
reset_at: Math.floor(Date.now() / 1000) + 3600,
|
|
},
|
|
secondary_window: {
|
|
used_percent: 0,
|
|
limit_window_seconds: 604800,
|
|
reset_at: reset,
|
|
},
|
|
},
|
|
},
|
|
]
|
|
: [],
|
|
});
|
|
try {
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
await gotoAppShell(page);
|
|
await expect(usageItem(page)).toBeVisible();
|
|
await usageItem(page).click();
|
|
await qaScreenshot(page, `codex-${shape}-card-and-summary`);
|
|
await expectCodexReportedWindows(page, shape);
|
|
await qaScreenshot(page, `codex-${shape}-pins`);
|
|
} finally {
|
|
await source.cleanup();
|
|
}
|
|
});
|
|
}
|
|
|
|
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(`inline Usage Settings ${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 page.setViewportSize(viewport);
|
|
await page.goto("/usage");
|
|
const screen = page.getByTestId(`usage-host-${getServerId()}`);
|
|
await expect(screen.getByText("Claude", { exact: true })).toBeVisible({ timeout: 30_000 });
|
|
const settings = screen.getByRole("button", { name: "Settings", exact: true });
|
|
const chevron = settings.locator("svg").locator("../..");
|
|
await expect(settings).toHaveAccessibleName("Settings");
|
|
await expect(settings).toHaveAttribute("aria-expanded", "false");
|
|
await expect(chevron).toHaveCSS("transform", "none");
|
|
await expect(screen.getByTestId("usage-display-used")).toHaveCount(0);
|
|
await expect(
|
|
pinRow(screen, "Claude", "Weekly").getByTestId("usage-pin-glyph-pinned"),
|
|
).toHaveCSS("opacity", "1");
|
|
await captureSettingsState(page, testInfo, `${size}-${theme}-collapsed`);
|
|
|
|
await togglePin(screen, "Claude", "Weekly");
|
|
await page.mouse.move(0, viewport.height - 1);
|
|
await expect(
|
|
pinRow(screen, "Claude", "Weekly").getByTestId("usage-pin-glyph-unpinned"),
|
|
).toHaveCSS("opacity", "1");
|
|
await openUsageOptions(page);
|
|
await expect(settings).toHaveAttribute("aria-expanded", "true");
|
|
await expect(chevron).toHaveCSS("transform", "matrix(0, 1, -1, 0, 0, 0)");
|
|
await expect(screen.getByText("Pinned windows show in the sidebar footer")).toBeVisible();
|
|
await expect(screen.getByTestId("usage-display-used")).toHaveAttribute(
|
|
"aria-selected",
|
|
"true",
|
|
);
|
|
await settings.blur();
|
|
await page.mouse.move(0, viewport.height - 1);
|
|
await captureSettingsState(page, testInfo, `${size}-${theme}-expanded`);
|
|
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 settings.click();
|
|
await expect(settings).toHaveAttribute("aria-expanded", "false");
|
|
await expect(screen.getByTestId("usage-display-used")).toHaveCount(0);
|
|
await refreshAllUsage(page);
|
|
await expect.poll(() => forcedRefreshes(usage)).toEqual([{ forceRefresh: true }]);
|
|
});
|
|
}
|
|
}
|
|
|
|
async function captureSettingsState(
|
|
page: Page,
|
|
testInfo: import("@playwright/test").TestInfo,
|
|
name: string,
|
|
) {
|
|
await qaScreenshot(page, name);
|
|
await testInfo.attach(name, { body: await page.screenshot(), contentType: "image/png" });
|
|
}
|