mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 21:23:04 +00:00
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.
746 lines
28 KiB
TypeScript
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);
|
|
}
|