Mobile Users + Backups tables: stacked cards + selection (Slice 5)

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).
This commit is contained in:
Developer
2026-06-26 13:24:19 +00:00
parent 2076ab76fa
commit 2eb649eceb
10 changed files with 606 additions and 186 deletions
@@ -9,8 +9,10 @@ import type {
UserDirectoryResponse,
} from "../../types";
// jsdom has no window.matchMedia; MUI `useMediaQuery` (still used by the
// compose dialog, slice 6b) must not blow up during render. Stub to "desktop".
// jsdom has no window.matchMedia; the shared `useIsMobile` hook and the
// compose dialog viewport hook must not blow up during render. Stub to
// "desktop" (matches: false) by default; the slice-5 describe block flips it
// to mobile for card-layout assertions.
beforeEach(() => {
if (!window.matchMedia) {
window.matchMedia = ((query: string) => ({
@@ -283,3 +285,61 @@ describe("UsersPage (slice 6b — compose dialog formatting actions)", () => {
expect(body.value).toContain("<strong>");
});
});
describe("UsersPage (mobile card layout — slice 5)", () => {
beforeEach(() => {
window.matchMedia = ((query: string) => ({
matches: query.includes("768"),
media: query,
onchange: null,
addEventListener: () => {},
removeEventListener: () => {},
addListener: () => {},
removeListener: () => {},
dispatchEvent: () => false,
})) as unknown as typeof window.matchMedia;
});
it("renders user cards with display name as primary below md", () => {
users = [
userFixture({ jellyfin_id: "u1", display_name: "Alice" }),
userFixture({
jellyfin_id: "u2",
username: "bob",
display_name: "Bob",
}),
];
render(
<TooltipProvider>
<UsersPage />
</TooltipProvider>,
);
expect(screen.getByText("Alice")).toBeInTheDocument();
expect(screen.getByText("Bob")).toBeInTheDocument();
// Activity field label should appear per card.
expect(screen.getAllByText("Activity")).toHaveLength(2);
});
it("toggles selection from the card checkbox without opening the drawer", async () => {
users = [userFixture({ jellyfin_id: "u1", display_name: "Alice" })];
render(
<TooltipProvider>
<UsersPage />
</TooltipProvider>,
);
const checkbox = screen.getByRole("checkbox", {
name: /Select Alice/i,
});
expect(checkbox).toHaveAttribute("data-state", "unchecked");
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("data-state", "checked");
// Drawer stays closed: the session-panel stub only renders when the
// drawer opens via a card-body tap, not via the checkbox.
expect(screen.queryByTestId("session-panel-stub")).not.toBeInTheDocument();
});
});