Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions apps/desktop-tauri/src/components/CodexAccountsMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,35 @@ describe("CodexAccountsMenu", () => {
expect((fills[1] as HTMLElement).style.width).toBe("70%");
});

it("renders a usage bar from a weekly-only snapshot (primaryWindow: null)", async () => {
const weeklyOnly: CodexAccountUsageSnapshot = {
email: "weekly@example.com",
providerAccountId: null,
plan: "pro",
allowed: true,
limitReached: false,
primaryWindow: null,
secondaryWindow: {
usedPercent: 42,
resetAt: null,
limitWindowSeconds: 604800,
},
credits: null,
updatedAt: "2024-01-01T00:00:00Z",
};
const { container } = renderMenu(false, {
accounts: [account("1", { source: "ambient" }), account("2")],
snapshots: { "1": weeklyOnly },
});
await screen.findByText("user-1@example.com");

const fills = container.querySelectorAll(
".codex-menu-accounts__bar-fill",
);
expect(fills.length).toBe(1);
expect((fills[0] as HTMLElement).style.width).toBe("42%");
});

it("switches an account and kicks a provider refresh", async () => {
renderMenu(false, {
accounts: [account("1", { source: "ambient" }), account("2")],
Expand Down
11 changes: 9 additions & 2 deletions apps/desktop-tauri/src/components/CodexAccountsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,15 @@ export default function CodexAccountsMenu({ hideEmail }: { hideEmail: boolean })
<ul className="codex-menu-accounts__list">
{accounts.map((account) => {
const snapshot = snapshots[account.id];
const pct = snapshot?.primaryWindow
? Math.round(snapshot.primaryWindow.usedPercent)
// Prefer the primary (session) window, but accounts whose backend
// only returns a weekly window have primaryWindow: null — fall back
// to the next filled window in canonical order (primary →
// secondary; the account-snapshot bridge carries no tertiary or
// extra rate windows) so the usage bar still renders.
const usageWindow =
snapshot?.primaryWindow ?? snapshot?.secondaryWindow ?? null;
const pct = usageWindow
? Math.round(usageWindow.usedPercent)
: null;
const label =
account.nickname ??
Expand Down
25 changes: 25 additions & 0 deletions apps/desktop-tauri/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2484,6 +2484,31 @@ body:has(.tray-panel-reveal) {
color: var(--provider-status-error);
}

/* CodexAccountsSection cards sit in the fixed 720px settings window next to a
3-button actions row (~242px). A long account email has no break
opportunities, so without containment it overflows the info column and
paints over the actions. Ellipsize the info column/title instead, and pin
the actions row in place with flex-shrink: 0 + nowrap. */
.codex-accounts-card .credential-card__info {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.codex-accounts-card .credential-card__info strong {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.codex-accounts-card .credential-card__actions {
flex-shrink: 0;
flex-wrap: nowrap;
white-space: nowrap;
}

.provider-detail-pace__stage {
font-size: 0.86rem;
font-weight: 600;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { readFileSync } from "node:fs";
import { act, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type {
Expand Down Expand Up @@ -139,4 +140,54 @@ describe("CodexAccountsSection", () => {
});
expect(tauriMocks.codexAccountRestartDesktop).toHaveBeenCalledTimes(1);
});
});

// Layout containment cannot be asserted via jsdom (vitest runs with
// `css: false`, so styles.css is never applied and computed styles are
// empty). Assert the stylesheet rules directly instead: these are the exact
// properties that keep a long account email from painting over the actions
// row at the fixed 720px settings window. import.meta.dirname (not .url)
// survives vitest's jsdom transform as the real on-disk directory.
if (!import.meta.dirname) {
throw new Error("import.meta.dirname unavailable to vitest runner");
}
const stylesSource = readFileSync(
`${import.meta.dirname}/../../../../../styles.css`,
"utf8",
);

function ruleBlock(source: string, selector: string): string {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = source.match(new RegExp(`${escaped}\\s*\\{([^}]*)\\}`));
expect(match).not.toBeNull();
return match![1];
}

describe("CodexAccountsSection containment styles", () => {
it("ellipsizes the info column and pins the actions row inside the card", () => {
const info = ruleBlock(
stylesSource,
".codex-accounts-card .credential-card__info",
);
expect(info).toContain("min-width: 0");
expect(info).toContain("overflow: hidden");
expect(info).toContain("text-overflow: ellipsis");
expect(info).toContain("white-space: nowrap");

const title = ruleBlock(
stylesSource,
".codex-accounts-card .credential-card__info strong",
);
expect(title).toContain("max-width: 100%");
expect(title).toContain("overflow: hidden");
expect(title).toContain("text-overflow: ellipsis");
expect(title).toContain("white-space: nowrap");

const actions = ruleBlock(
stylesSource,
".codex-accounts-card .credential-card__actions",
);
expect(actions).toContain("flex-shrink: 0");
expect(actions).toContain("nowrap");
});
});
16 changes: 16 additions & 0 deletions apps/desktop-tauri/src/test/node-builtins.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Minimal ambient declarations for the Node builtins used by test files.
// This workspace does not depend on @types/node; declaring only the touched
// signatures keeps tsc honest without pulling the full Node type surface.
// Delete this file if @types/node is ever added (the real declarations
// supersede these).
declare module "node:fs" {
export function readFileSync(path: string, encoding: "utf8"): string;
}

// Node 20.11+ / vite-node inject import.meta.dirname|filename; those types
// also come from @types/node, so declare the touched member here. Note:
// import.meta.url is NOT reliable under vitest's jsdom transform (it can
// resolve relative URLs against the dev-server origin) — use dirname instead.
interface ImportMeta {
readonly dirname?: string;
}
Loading