2eb649eceb
Below md, the Users directory and the three Backups tables render as MobileCardRow cards: - UsersPage: display name primary; username/activity/email fields. Each card carries a selection checkbox (44px via mobile-touch-target) in the actions slot with stopPropagation so toggling selection does not open the drawer; card-body tap still opens the drawer. - BackupAlertsTable: alert message primary; severity/type/created fields; Acknowledge action preserved in actions slot. - BackupJobsTable: job name primary; source/schedule/last-status fields (joins latestRuns into a JobCardRow). - BackupRunsTable: run job_id primary; status/duration/size/started fields; status-filter Select renders above both layouts (preserved on mobile). Desktop (md+) is byte-for-byte identical for all four components -- the UsersPage diff is dominated by re-indenting the existing Table into the isMobile ternary else branch. Fix from Slice 5 review: MobileCardRow now renders the clickable card as <div role=button tabIndex=0> with Enter/Space keyboard handling instead of <button>, so nesting a Radix Checkbox (which renders a <button>) in the actions slot produces valid HTML. The desktop-parity argument for <button>-in-<button> did not hold (desktop rows are <tr>, not buttons). Cross-cutting: useIsMobile hardened with typeof window.matchMedia guard (safe in real browsers; only changes jsdom crash -> false). The file-local 900px compose hook was renamed useComposeViewport to avoid collision with the shared 768px useIsMobile. Tests: BackupJobsTable test file added (was untested), UsersPage mobile selection round-trip + stopPropagation, mobile card render across all four components. 105 tests pass; lint/build green. Refs openspec/changes/mobile-responsive-parity/ (spec R3, tasks slice 5).
107 lines
3.0 KiB
TypeScript
107 lines
3.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import BackupAlertsTable from "../BackupAlertsTable";
|
|
import type { BackupAlert } from "../../types/backups";
|
|
|
|
function alert(overrides: Partial<BackupAlert> = {}): BackupAlert {
|
|
return {
|
|
id: "a1",
|
|
job_id: "job-1",
|
|
run_id: null,
|
|
alert_type: "failed_status",
|
|
severity: "warning",
|
|
message: "Run failed",
|
|
acknowledged: false,
|
|
resolved_at: null,
|
|
created_at: 1_700_000_000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("BackupAlertsTable", () => {
|
|
it("maps alert severity onto Badge variants per design §2.3", () => {
|
|
render(
|
|
<BackupAlertsTable
|
|
alerts={[
|
|
alert({ id: "c", severity: "critical" }),
|
|
alert({ id: "w", severity: "warning" }),
|
|
]}
|
|
onAcknowledge={vi.fn()}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("critical").getAttribute("data-variant")).toBe(
|
|
"destructive",
|
|
);
|
|
expect(screen.getByText("warning").getAttribute("data-variant")).toBe(
|
|
"warning",
|
|
);
|
|
});
|
|
|
|
it("calls onAcknowledge with the alert id when the button is clicked", async () => {
|
|
const onAcknowledge = vi.fn();
|
|
render(
|
|
<BackupAlertsTable
|
|
alerts={[alert({ id: "ack-me" })]}
|
|
onAcknowledge={onAcknowledge}
|
|
/>,
|
|
);
|
|
await userEvent.click(screen.getByRole("button", { name: "Acknowledge" }));
|
|
expect(onAcknowledge).toHaveBeenCalledTimes(1);
|
|
expect(onAcknowledge).toHaveBeenCalledWith("ack-me");
|
|
});
|
|
|
|
it("hides the acknowledge button for already-acknowledged alerts", () => {
|
|
render(
|
|
<BackupAlertsTable
|
|
alerts={[alert({ id: "done", acknowledged: true })]}
|
|
onAcknowledge={vi.fn()}
|
|
/>,
|
|
);
|
|
expect(screen.queryByRole("button", { name: "Acknowledge" })).toBeNull();
|
|
});
|
|
});
|
|
|
|
// jsdom lacks matchMedia; default to desktop so existing tests are unaffected.
|
|
function setMatchMedia(matches: boolean) {
|
|
window.matchMedia = ((query: string) => ({
|
|
matches: query.includes("768") ? matches : false,
|
|
media: query,
|
|
onchange: null,
|
|
addEventListener: () => {},
|
|
removeEventListener: () => {},
|
|
addListener: () => {},
|
|
removeListener: () => {},
|
|
dispatchEvent: () => false,
|
|
})) as unknown as typeof window.matchMedia;
|
|
}
|
|
|
|
beforeEach(() => setMatchMedia(false));
|
|
|
|
describe("BackupAlertsTable (mobile card layout — slice 5)", () => {
|
|
it("renders cards with message as primary below md", () => {
|
|
setMatchMedia(true);
|
|
render(
|
|
<BackupAlertsTable
|
|
alerts={[alert({ id: "m1", message: "Disk full" })]}
|
|
onAcknowledge={vi.fn()}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("Disk full")).toBeInTheDocument();
|
|
expect(screen.getAllByText("Severity")).toHaveLength(1);
|
|
});
|
|
|
|
it("renders acknowledge action on card below md", async () => {
|
|
setMatchMedia(true);
|
|
const onAck = vi.fn();
|
|
render(
|
|
<BackupAlertsTable
|
|
alerts={[alert({ id: "a1", acknowledged: false })]}
|
|
onAcknowledge={onAck}
|
|
/>,
|
|
);
|
|
await userEvent.click(screen.getByRole("button", { name: "Ack" }));
|
|
expect(onAck).toHaveBeenCalledWith("a1");
|
|
});
|
|
});
|