Keep usage providers in place after refresh (#6947)

* fix(app): preserve usage provider order across refreshes

* test(usage): keep refresh and scrolling journeys stable

Usage reports can follow the long fixture card, so a fixed wheel distance
can scroll past its final window. Target the named windows with wheel
input while preserving the visibility checks. Assert refreshed provider
order through visible names and keep viewport setup in journey helpers.

Co-authored-by: Sleroq <hireme@sleroq.link>

---------

Co-authored-by: Sleroq <hireme@sleroq.link>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-15 21:16:52 +02:00
committed by GitHub
co-authored by Sleroq
parent ec7446add4
commit 208e323035
5 changed files with 92 additions and 10 deletions
@@ -100,6 +100,40 @@ function weeklyReport(sourceId: string, usedPct: number): UsageReportEntry {
};
}
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%", { 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,
@@ -152,6 +186,21 @@ test.describe("usage modal", () => {
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();
@@ -30,7 +30,7 @@ import {
openUsageFromIcon,
openUsageFromItem,
pinRow,
scrollUsage,
scrollUsageToWindow,
setSummaryInSidebar,
openUsageOptions,
showUsageAs,
@@ -146,7 +146,9 @@ test.describe("Usage item", () => {
compact: { viewport: COMPACT, showSidebar: openCompactSidebar },
};
for (const [size, { viewport, showSidebar }] of Object.entries(layouts)) {
test(`long usage reports scroll to the final window on ${size}`, async ({ page }) => {
test(`long usage reports scroll between their first and last windows on ${size}`, async ({
page,
}) => {
test.setTimeout(120_000);
const source = await installTallUsageSource();
try {
@@ -161,10 +163,10 @@ test.describe("Usage item", () => {
await waitForSettledPosition(content);
await expect(content.getByText("Window 20", { exact: true })).not.toBeInViewport();
await qaScreenshot(page, `${size}-usage-scroll-top`);
await scrollUsage(page, 5_000);
await scrollUsageToWindow(page, "Window 20");
await expect(content.getByText("Window 20", { exact: true })).toBeInViewport();
await qaScreenshot(page, `${size}-usage-scroll-bottom`);
await scrollUsage(page, -5_000);
await scrollUsageToWindow(page, "Window 1");
await expect(content.getByText("Window 20", { exact: true })).not.toBeInViewport();
} finally {
await source.cleanup();
@@ -212,6 +212,14 @@ export async function scrollUsage(page: Page, deltaY: number): Promise<void> {
await page.mouse.wheel(0, deltaY);
}
/** Reach a window with the wheel even when other reports follow its card. */
export async function scrollUsageToWindow(page: Page, label: string): Promise<void> {
const target = await usageModal(page).getByText(label, { exact: true }).boundingBox();
const viewport = page.viewportSize();
if (!target || !viewport) throw new Error("Usage window must exist before scrolling to it.");
await scrollUsage(page, target.y + target.height / 2 - viewport.height / 2);
}
/** The modal's own Close button. */
export async function closeUsage(page: Page): Promise<void> {
await visible(page, "usage-modal").getByRole("button", { name: "Close", exact: true }).click();
+16
View File
@@ -257,6 +257,22 @@ describe("settleReports", () => {
expect(settleReports([refreshed, beta], [alpha, beta])).toEqual([refreshed, beta]);
});
it("keeps the displayed order when providers finish a refresh in reverse order", () => {
const refreshed = entry({ sourceId: "alpha", planLabel: "New" });
const streamed = upsertReport(upsertReport([alpha, beta], beta), refreshed);
expect(settleReports(streamed, [beta, refreshed])).toEqual([refreshed, beta]);
});
it("drops missing reports and appends new ones without moving surviving reports", () => {
const gamma = entry({ sourceId: "gamma" });
const delta = entry({ sourceId: "delta" });
expect(settleReports([alpha, beta, gamma], [delta, gamma, alpha])).toEqual([
alpha,
gamma,
delta,
]);
});
it("takes the finished list as is when nothing was shown yet", () => {
expect(settleReports(undefined, [alpha, beta])).toEqual([alpha, beta]);
});
+13 -6
View File
@@ -82,17 +82,24 @@ export function upsertReport(
}
/**
* The list a finished request leaves: its reports, dropping any the host no longer has, except
* that a copy on screen fetched after the request's copy stays.
* Keep displayed reports in place: the finished list is in fetch-completion order, which can
* change on every refresh. Drop missing reports, append new ones, and keep newer fetched copies.
*/
export function settleReports(
shown: readonly UsageReportEntry[] | undefined,
finished: readonly UsageReportEntry[],
): UsageReportEntry[] {
return finished.map((report) => {
const copy = shown?.find((entry) => entry.id === report.id);
return copy ? laterFetched(copy, report) : report;
});
const remaining = new Map(finished.map((report) => [report.id, report]));
const settled: UsageReportEntry[] = [];
for (const report of shown ?? []) {
const arriving = remaining.get(report.id);
if (!arriving) {
continue;
}
settled.push(laterFetched(report, arriving));
remaining.delete(report.id);
}
return [...settled, ...remaining.values()];
}
export interface UsageQueryState {