Mobile message compose + WidgetConfigDialog SheetForms (Slice 8)
Below md, both the message-compose Dialog and the WidgetConfigDialog
render inside a SheetForm instead of a centered Dialog.
Message compose (UsersPage.impl.tsx): the form body (subject, formatting
toolbar, HTML textarea, preview, attachments) is extracted into a shared
composeBody const consumed by both SheetForm (mobile) and Dialog
(desktop). SheetForm wired with title, onSave=handleSend (which already
closes on success per R4.5), onCancel=closeCompose, isPending,
saveDisabled, saveLabel='Send message'.
WidgetConfigDialog: the draftBody const is shared between branches. The
two-mode flow (list vs draft) maps to dynamic SheetForm props -- list
mode ('Dashboard widgets' / Done / Cancel both close), draft mode
('Add/Edit widget' / Save widget / Cancel=reset back to list). The
inline Back/Save buttons are hidden on mobile (!isMobile) since the
SheetForm footer provides them.
Desktop (md+) is token-identical for both components -- the
isComposeMobile (900px) fullscreen styling on compose is preserved for
the 768-900px band. The large diff (~860 lines) is dominated by
extraction/re-indentation of shared form bodies into consts; the
behavioral delta is ~80 lines.
Tests: 3 new (compose mobile send/subject, WidgetConfigDialog desktop +
mobile titles/Done). 116 tests pass; lint/build green.
Refs openspec/changes/mobile-responsive-parity/ (spec R4, tasks slice 8).
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { WidgetConfigDialog } from "../WidgetConfigDialog";
|
||||
|
||||
// jsdom has no window.matchMedia; default to desktop (matches: false).
|
||||
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;
|
||||
}
|
||||
|
||||
vi.mock("../../hooks/useWidgets", () => ({
|
||||
useWidgetInstances: () => ({ data: [] }),
|
||||
useSaveWidgetInstance: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
useDeleteWidgetInstance: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useServices", () => ({
|
||||
useServiceInstances: () => ({ data: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useSettings", () => ({
|
||||
useTasks: () => ({ data: [] }),
|
||||
}));
|
||||
|
||||
beforeEach(() => setMatchMedia(false));
|
||||
|
||||
describe("WidgetConfigDialog (desktop)", () => {
|
||||
it("renders a Dialog with the dashboard widgets title at md+", () => {
|
||||
render(<WidgetConfigDialog open={true} onClose={() => {}} />);
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Dashboard widgets" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("WidgetConfigDialog (mobile SheetForm — slice 8)", () => {
|
||||
beforeEach(() => setMatchMedia(true));
|
||||
|
||||
it("renders a SheetForm with the dashboard widgets title below md", () => {
|
||||
render(<WidgetConfigDialog open={true} onClose={() => {}} />);
|
||||
expect(screen.getByText("Dashboard widgets")).toBeInTheDocument();
|
||||
// List mode footer: "Done" button closes.
|
||||
expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user