Files
paseo/packages/app/e2e/browser/usage-modal.spec.ts
T
Mohamed Boudra e12feb633f feat(usage): show when each limit resets, and make usage rows easier to read (#8162)
The reset countdown floored to one unit, so a weekly window read "resets 2d"
from 2d 23h down to 2d 0h and looked stuck for a day; the session window had
the same problem with hours. Countdowns now use two units ("2d 23h", "4h 59m"),
and every row also shows the clock time it resets ("7:20 AM", "Sat 8:00 AM",
"Oct 31, 3:00 PM"), so users can plan work around a reset without hovering.

Rows now put the window name and percent on one line, the bar under it, and the
reset on its own full-width line, so the exact time fits at phone width in
longer languages. The pinned-row background is gone; the pin glyph, drawn in
the foreground colour when pinned, is the only pinned signal. Bars are 6px.
2026-10-29 11:24:34 +07:00

746 lines
28 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, openSettings } from "../support/helpers/app";
import { addConnectedHostAndReload } from "../support/helpers/hosts";
import { startIsolatedHostDaemon } from "../support/helpers/isolated-host-daemon";
import { openSettingsSection } from "../support/helpers/settings";
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,
expectLoginUsageFreshness,
installCodexWindowUsage,
expectCodexReportedWindows,
refreshLoginUsage,
hoverUsageWindow,
} from "../support/helpers/usage-login";
import {
claudeAndCodexReports,
expectPinnedUsage,
openCompactSidebar,
openUsageFromIcon,
openUsageFromItem,
pinRow,
openUsageOptions,
closeUsageOptions,
expectUsageOptionsFitContent,
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 }] },
};
}
function installReversedUsageRefresh(page: Page) {
const initial = [weeklyReport("opencode", 31), weeklyReport("openai", 12)];
const refreshed = [weeklyReport("openai", 22), weeklyReport("opencode", 41)];
return installUsageReportsFixture(page, {
lists: [(request) => (request.forceRefresh ? refreshed : initial)],
});
}
async function openUsageSummaryAtWidth(page: Page, width: number) {
await page.setViewportSize({ width, height: 900 });
await gotoAppShell(page);
if (width === 390) await openCompactSidebar(page);
await expectPinnedUsage(page, ["31% Weekly", "12% Weekly"]);
await openUsageFromItem(page);
}
async function expectUsageProviderOrder(page: Page, summary: string[]) {
const group = page.getByTestId(`usage-host-${getServerId()}`);
await expect(group.getByText(/^(opencode|openai) plan$/)).toHaveText([
"opencode plan",
"openai plan",
]);
await expectPinnedUsage(page, summary);
}
async function refreshUsageAndWaitForReports(page: Page, usage: UsageReportsFixture) {
const requestCount = usage.listRequests().length;
await refreshAllUsage(page);
await usage.waitForListRequests(requestCount + 1);
const group = page.getByTestId(`usage-host-${getServerId()}`);
await expect(group.getByText("41% used", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Refresh all", exact: true })).toBeEnabled();
}
test.describe("usage modal", () => {
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 openUsageFromItem(page);
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, "usage-modal-one-host");
await expect(group.getByText("31% used")).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);
});
for (const width of [1440, 390]) {
test(`keeps sidebar and modal provider order across a reversed refresh at width ${width}`, async ({
page,
}) => {
const usage = await installReversedUsageRefresh(page);
await openUsageSummaryAtWidth(page, width);
await expectUsageProviderOrder(page, ["31% Weekly", "12% Weekly"]);
await qaScreenshot(page, `usage-order-${width}-before-refresh`);
await refreshUsageAndWaitForReports(page, usage);
await expectUsageProviderOrder(page, ["41% Weekly", "22% Weekly"]);
await qaScreenshot(page, `usage-order-${width}-after-refresh`);
});
}
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 modal 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 openUsageFromItem(page);
const group = page.getByTestId(`usage-host-${serverId}`);
await expect(group.getByText("31% used")).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% used")).toBeVisible();
await expect(group.getByText("12% used")).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 gotoAppShell(page);
await openCompactSidebar(page);
await openUsageFromIcon(page);
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 Usage.
await openUsageFromIcon(page);
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-menu")).toHaveCount(0);
await expect(page.getByTestId("usage-refresh-all")).toHaveCount(0);
expect(usage.listRequests()).toHaveLength(0);
});
test("a host picked in the Usage modal 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 openUsageFromItem(page);
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% used"),
).toBeVisible({ timeout: 30_000 });
await expect(hostFilter(page)).toContainText("Secondary box");
await qaScreenshot(page, "usage-modal-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 openUsageFromItem(page);
await expect(
page.getByTestId(`usage-host-${secondary.serverId}`).getByText("12% used"),
).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 openUsageFromIcon(page);
await expect(
page.getByText("Claude: 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("Claude: 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 openCompactSidebar(page);
await openUsageFromIcon(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 openUsageFromItem(page);
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(`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 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% used · resets /,
);
await openUsageOptions(page);
await closeUsageOptions(page);
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" });
}
test("an account with all logins failing shows each harness and remedy, then only usage when one succeeds", async ({
page,
}) => {
const fixture = await installLoginUsage([
{ harness: "Codex", report: { status: "error", error: "Usage API returned 500. Try again." } },
{
harness: "OpenCode",
report: {
status: "unavailable",
problem: { kind: "rejected", status: 401, refreshedBy: "opencode" },
},
},
{
harness: "Pi",
report: { status: "unavailable", problem: { kind: "no_quota", detail: "No active plan" } },
},
{
harness: "OMP",
report: {
status: "unavailable",
problem: { kind: "rejected", status: 403, refreshedBy: "omp" },
},
},
]);
try {
await gotoAppShell(page);
await openUsageFromIcon(page);
await expectLoginErrors(page, [
"Codex: Usage API returned 500. Try again.",
"OpenCode: Login rejected (HTTP 401). Run opencode to refresh it.",
"Pi: No active plan",
"OMP: Login rejected (HTTP 403). Run omp to refresh it.",
]);
await fixture.setReport([
{
harness: "Codex",
report: { status: "error", error: "Usage API returned 500. Try again." },
},
{
harness: "OpenCode",
report: { status: "available", windows: [{ id: "weekly", label: "Weekly", usedPct: 42 }] },
},
]);
await refreshLoginUsage(page);
await expectLoginUsageOnly(page, "42% used");
} finally {
await fixture.cleanup();
}
});
async function expectLoginErrors(page: Page, lines: string[]) {
const card = page.getByTestId("usage-report-login-journey:account");
await expect(card.getByTestId("usage-login-error")).toHaveText(lines.join("\n"));
}
async function expectLoginUsageOnly(page: Page, percentage: string) {
const card = page.getByTestId("usage-report-login-journey:account");
await expect(card.getByText(new RegExp(`^${percentage}(?:$| ·)`))).toBeVisible();
await expect(card.getByTestId("usage-login-error")).toHaveCount(0);
}
for (const compact of [false, true]) {
test(`failed polls retain usage and successful freshness (${compact ? "footer" : "tooltip"})`, async ({
page,
}) => {
const fixture = await installLoginUsage({ status: "error", error: "Initial failure" });
try {
await page.clock.install();
await page.setViewportSize(
compact ? { width: 390, height: 844 } : { width: 1440, height: 900 },
);
await gotoAppShell(page);
if (compact) await openCompactSidebar(page);
await openUsageFromIcon(page);
await expectLoginErrors(page, ["Claude: Initial failure"]);
await test.step("developer errors do not interrupt freshness inspection", async () => {
await page.evaluate(() => console.error("E2E: developer toast while Usage is open"));
await expectLoginUsageFreshness(page, "No successful update", compact);
});
await fixture.setReport({
status: "available",
windows: [{ id: "weekly", label: "Weekly", usedPct: 42, resetsAt: "2000-01-01T00:00:00Z" }],
});
// Move only the browser clock; real daemon fetches continue using the host clock.
await page.clock.setFixedTime(new Date(Date.now() + 3_900_000));
await refreshLoginUsage(page);
await expectLoginUsageOnly(page, "42% used");
await advanceUsageClock(page, 60_000);
await expectLoginUsageFreshness(page, "Updated 1h ago", compact);
await fixture.setReport({ status: "error", error: "Rate limited" });
await refreshLoginUsage(page);
await expectRetainedLoginUsage(page, "Claude: Rate limited");
await expectLoginUsageFreshness(page, "Last updated 1h ago", compact);
await refreshLoginUsage(page);
await expectRetainedLoginUsage(page, "Claude: Rate limited");
await expectLoginUsageFreshness(page, "Last updated 1h ago", compact);
await fixture.setReport({
status: "unavailable",
problem: { kind: "no_quota", detail: "No active quota" },
});
await refreshLoginUsage(page);
await expectRetainedLoginUsage(page, "Claude: No active quota");
await expectLoginUsageFreshness(page, "Last updated 1h ago", compact);
await fixture.setReport({
status: "available",
windows: [{ id: "weekly", label: "Weekly", usedPct: 43 }],
});
await page.clock.setFixedTime(new Date(Date.now() + 150_000));
await refreshLoginUsage(page);
await expectLoginUsageOnly(page, "43% used");
await advanceUsageClock(page, 60_000);
await expect(page.getByText("42% used", { exact: true })).toHaveCount(0);
await expectLoginUsageFreshness(page, "Updated 2m ago", compact);
} finally {
await fixture.cleanup();
}
});
}
async function expectRetainedLoginUsage(page: Page, message: string) {
const card = page.getByTestId("usage-report-login-journey:account");
await expect(card.getByText(/^42% used(?:$| ·)/)).toBeVisible();
await expect(card.getByTestId("usage-login-error")).toHaveText(message);
await expect(card.getByText("Reset time passed", { exact: false })).toBeVisible();
}
async function switchToJapanese(page: Page) {
await page.getByRole("button", { name: "Close", exact: true }).click();
await openSettings(page);
await openSettingsSection(page, "general");
await page.getByRole("button", { name: "System", exact: true }).click();
await page.getByRole("menuitem", { name: "日本語 - Japanese", exact: true }).click();
await page.getByRole("button", { name: "戻る", exact: true }).click();
}
async function openJapaneseUsage(page: Page) {
await page.getByRole("button", { name: "使用状況", exact: true }).click();
await expect(page.getByTestId("usage-modal").first()).toBeVisible();
}
test("usage percentages state their meaning and follow the selected language", async ({ page }) => {
test.setTimeout(120_000);
const login = await installLoginUsage({
status: "available",
windows: [{ id: "session", label: "Session", usedPct: 70, summary: true }],
});
try {
await gotoAppShell(page);
await openUsageFromIcon(page);
await expect(page.getByText("70% used", { exact: true })).toBeVisible();
await switchToJapanese(page);
await expect(usageItem(page)).toHaveAccessibleName(/Claude セッション 70% 使用済み/);
await openJapaneseUsage(page);
await expect(page.getByText("70% 使用済み", { exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "すべて更新", exact: true })).toBeVisible();
await page.getByRole("button", { name: "設定", exact: true }).last().click();
await expect(page.getByText("割合", { exact: true })).toBeVisible();
await expect(page.getByText("使用済み", { exact: true })).toBeVisible();
await expect(page.getByText("残り", { exact: true })).toBeVisible();
} finally {
await login.cleanup();
}
});
test("open usage clocks advance without refreshing or reopening", async ({ page }) => {
test.setTimeout(120_000);
const now = new Date();
const reset = new Date(now.getTime() + 30.5 * 60_000).toISOString();
const login = await installLoginUsage({
status: "available",
windows: [
{ id: "session", label: "Session", usedPct: 42, resetsAt: reset },
{ id: "weekly", label: "Weekly", usedPct: 80, runsOutAt: reset, shortfallPct: 10 },
],
balances: [{ id: "credits", label: "Credits", unit: "usd", remaining: 12, resetsAt: reset }],
});
try {
await openCompactUsageWithClock(page, now);
const card = page.getByTestId("usage-report-login-journey:account");
await expect(card.getByTestId("usage-freshness")).toHaveText("Updated just now");
await expect(card.getByText("resets in 30m", { exact: false }).first()).toBeVisible();
// The shared minute clock is wall-aligned, and the daemon timestamps use real time.
// After two browser minutes the latest displayed tick is one or two minutes old.
await advanceUsageClock(page, 120_000);
await expect(card.getByTestId("usage-freshness")).toHaveText(/^Updated [12]m ago$/);
await expect(card.getByText(/resets in (28|29)m/).first()).toBeVisible();
await expect(card.getByText(/runs out in (28|29)m/)).toBeVisible();
await expect(card.getByTestId("provider-usage-balance-credits-reset")).toHaveText(
/^resets in (28|29)m\s·\s/,
);
} finally {
await login.cleanup();
}
});
async function openCompactUsageWithClock(page: Page, now: Date) {
await page.setViewportSize({ width: 390, height: 844 });
await page.clock.install({ time: now });
await gotoAppShell(page);
await openCompactSidebar(page);
await openUsageFromIcon(page);
}
async function advanceUsageClock(page: Page, milliseconds: number) {
await page.clock.fastForward(milliseconds);
}