-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(gui): Usage — five cards, caveat on the coverage card, year heatmap behind a disclosure #3395
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import { afterEach, beforeEach, expect, jest, test } from "bun:test"; | ||
| import { Window } from "happy-dom"; | ||
| import { act } from "react"; | ||
| import { createRoot, type Root } from "react-dom/client"; | ||
| import { LanguageProvider } from "../src/i18n/provider"; | ||
| import { en } from "../src/i18n/en"; | ||
| import { Tooltip } from "../src/ui"; | ||
| import { IconInfo } from "../src/icons"; | ||
|
|
||
| /** | ||
| * The Usage coverage card's caveat is a Tooltip whose child is an icon plus an sr-only | ||
| * name (060). This renders that exact shape and pins what the plan promised: the trigger | ||
| * is a focusable button, it has the accessible name, and focusing it shows the caveat. | ||
| */ | ||
| const globals = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const; | ||
| let previousGlobals: Record<(typeof globals)[number], unknown>; | ||
| let testWindow: Window; | ||
| let root: Root | null = null; | ||
|
|
||
| beforeEach(() => { | ||
| previousGlobals = Object.fromEntries(globals.map(key => [key, Reflect.get(globalThis, key)])) as typeof previousGlobals; | ||
| testWindow = new Window({ url: "http://localhost/#usage" }); | ||
| Object.defineProperties(globalThis, { | ||
| document: { configurable: true, value: testWindow.document }, | ||
| window: { configurable: true, value: testWindow }, | ||
| navigator: { configurable: true, value: testWindow.navigator }, | ||
| localStorage: { configurable: true, value: testWindow.localStorage }, | ||
| }); | ||
| (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; | ||
| jest.useFakeTimers(); | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| if (root) { await act(async () => { root!.unmount(); }); root = null; } | ||
| jest.useRealTimers(); | ||
| testWindow.close(); | ||
| for (const key of globals) Object.defineProperty(globalThis, key, { configurable: true, value: previousGlobals[key] }); | ||
| }); | ||
|
|
||
| test("the coverage caveat tooltip is a focusable, named trigger that shows the caveat on focus", async () => { | ||
| const container = testWindow.document.createElement("div"); | ||
| testWindow.document.body.appendChild(container); | ||
| root = createRoot(container as unknown as HTMLElement); | ||
| await act(async () => { | ||
| root!.render( | ||
| <LanguageProvider> | ||
| <Tooltip content={en["usage.subtitle"]} side="top" maxWidth={360}> | ||
| <IconInfo width={13} height={13} aria-hidden="true" /> | ||
| <span className="sr-only">{en["usage.subtitleAria"]}</span> | ||
| </Tooltip> | ||
| </LanguageProvider>, | ||
| ); | ||
| }); | ||
| const trigger = container.querySelector<HTMLButtonElement>("button.ocx-tooltip")!; | ||
| expect(trigger).not.toBeNull(); | ||
| expect(trigger.tagName).toBe("BUTTON"); | ||
| expect(trigger.hasAttribute("disabled")).toBe(false); | ||
| // Accessible name comes from the visually-hidden span, not a title attribute. | ||
| expect(trigger.textContent).toContain(en["usage.subtitleAria"]); | ||
| expect(trigger.getAttribute("title")).toBeNull(); | ||
| expect(container.textContent).not.toContain(en["usage.subtitle"]); | ||
|
|
||
| await act(async () => { | ||
| trigger.dispatchEvent(new testWindow.FocusEvent("focus", { bubbles: false })); | ||
| trigger.dispatchEvent(new testWindow.FocusEvent("focusin", { bubbles: true })); | ||
| }); | ||
| await act(async () => { jest.advanceTimersByTime(300); }); | ||
| const tipId = trigger.getAttribute("aria-describedby"); | ||
| expect(tipId).toBeTruthy(); | ||
| const tip = testWindow.document.getElementById(tipId!); | ||
| expect(tip?.textContent).toContain(en["usage.subtitle"]); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { readFileSync } from "node:fs"; | ||
| import { join } from "node:path"; | ||
|
|
||
| const src = readFileSync(join(import.meta.dir, "../src/pages/Usage.tsx"), "utf8"); | ||
| const css = readFileSync(join(import.meta.dir, "../src/styles.css"), "utf8"); | ||
|
|
||
| /** | ||
| * devlog/_plan/260904_dashboard_minimal/060_usage.md: five summary cards (the "active days" | ||
| * card said nothing a 7/30-day range with a heatmap did not), the counting caveat on the | ||
| * coverage card as a focusable info button (not a paragraph, not a title attribute), the | ||
| * cost figure at body weight beside its caveat, and the year heatmap behind a closed | ||
| * disclosure for 30d while the seven-day bars stay inline. | ||
| */ | ||
| describe("Usage page minimal shape", () => { | ||
| test("five cards, no active-days card, no subtitle paragraph", () => { | ||
| expect(src).toContain('className="usage-cards usage-cards-5"'); | ||
| expect(src).not.toContain("usage.card.activeDays"); | ||
| expect(src).not.toContain("activeDays"); | ||
| expect(src).not.toContain('<p className="page-sub">{t("usage.subtitle")}</p>'); | ||
| }); | ||
|
|
||
| test("the counting caveat is a focusable tooltip on the coverage card with an accessible name", () => { | ||
| expect(src).toContain('<Tooltip content={t("usage.subtitle")}'); | ||
| expect(src).toContain('{t("usage.subtitleAria")}'); | ||
| expect(src).not.toMatch(/title=\{t\("usage\.subtitle"\)\}/); | ||
| }); | ||
|
Comment on lines
+23
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift Add a rendered-behavior test for accessibility and disclosure behavior. These assertions only search Also applies to: 34-41 🤖 Prompt for AI Agents |
||
|
|
||
| test("the cost figure is body weight, not a stat value", () => { | ||
| expect(src).toContain('className="mono text-control usage-cost-value"'); | ||
| expect(src).not.toContain('className="stat-value mono usage-cost-value"'); | ||
| }); | ||
|
|
||
| test("30d heatmap sits in a closed details whose toggle re-pins the scroll; 7d bars stay inline", () => { | ||
| expect(src).toContain('<details className="panel usage-heatmap-details" style={{ marginTop: 16 }} onToggle={pinRight}>'); | ||
| expect(src).toContain("const pinRight = useCallback("); | ||
| expect(src).not.toMatch(/<details className="panel usage-heatmap-details"[^>]*\bopen\b/); | ||
| // Seven-day bars are returned before the details branch. | ||
| expect(src.indexOf('if (range === "7d") {')).toBeLessThan(src.indexOf('<details className="panel usage-heatmap-details"')); | ||
| expect(css).toContain(".usage-heatmap-details > summary::-webkit-details-marker { display: none; }"); | ||
| expect(css).toContain(".usage-cards-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }"); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the 30-day and available-history views, replacing the previous
<h3>with a plain<summary>removes “Daily activity” from the document heading outline, so screen-reader users navigating by headings can no longer discover this subsection. Keep the native disclosure control, but include heading semantics in its summary (for example, by nesting the existing heading in it).AGENTS.md reference: gui/AGENTS.md:L33-L33
Useful? React with 👍 / 👎.