fix(team-members): preserve detail and load states (SH-326)

This commit is contained in:
Codex Review Integration 2026-09-16 23:28:41 -03:00
parent e323b0b2ce
commit 633fcdc52c
5 changed files with 88 additions and 8 deletions

View file

@ -204,6 +204,12 @@ async function openWorkOrderPage(page: Page, mode: "default" | "empty" | "error"
});
}
async function expectWorkOrderPageReady(page: Page) {
await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({
timeout: 90_000,
});
}
async function expectStableScreenshot(page: Page, name: string) {
await page.waitForTimeout(250);
await page.evaluate(
@ -217,15 +223,18 @@ async function expectStableScreenshot(page: Page, name: string) {
}
test.describe("Work Orders deterministic pixel regression", () => {
test.setTimeout(120_000);
test("list", async ({ page }) => {
await openWorkOrderPage(page);
await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible();
await expectWorkOrderPageReady(page);
await expect(page.getByText("WO-501").first()).toBeVisible();
await expectStableScreenshot(page, "wo-list.png");
});
test("filters", async ({ page }) => {
await openWorkOrderPage(page);
await expectWorkOrderPageReady(page);
await page.getByRole("button", { name: "Advanced Filters" }).click();
await expect(page.getByRole("dialog", { name: "Advanced Filters" })).toBeVisible();
await expectStableScreenshot(page, "wo-filters.png");
@ -233,6 +242,7 @@ test.describe("Work Orders deterministic pixel regression", () => {
test("new", async ({ page }) => {
await openWorkOrderPage(page);
await expectWorkOrderPageReady(page);
await page.getByRole("button", { name: "New WO" }).click();
await expect(page.getByRole("heading", { name: "Type & schedule" })).toBeVisible();
await expectStableScreenshot(page, "wo-new.png");
@ -240,6 +250,7 @@ test.describe("Work Orders deterministic pixel regression", () => {
test("detail", async ({ page }) => {
await openWorkOrderPage(page);
await expectWorkOrderPageReady(page);
const row = page.locator("#wo-row-1");
await row.hover();
await row.getByRole("button", { name: "View details" }).click();
@ -249,7 +260,7 @@ test.describe("Work Orders deterministic pixel regression", () => {
test("empty", async ({ page }) => {
await openWorkOrderPage(page, "empty");
await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible();
await expectWorkOrderPageReady(page);
await page.getByLabel("Search work orders").fill("zz");
await expect(page.getByText("No work orders match your search")).toBeVisible();
await expectStableScreenshot(page, "wo-empty.png");
@ -257,7 +268,7 @@ test.describe("Work Orders deterministic pixel regression", () => {
test("error", async ({ page }) => {
await openWorkOrderPage(page, "error");
await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible();
await expectWorkOrderPageReady(page);
const alert = page.getByRole("main").getByRole("alert");
await expect(alert).toBeVisible();
await expect(alert).toContainText(/server error/i);
@ -267,6 +278,7 @@ test.describe("Work Orders deterministic pixel regression", () => {
test("mobile", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await openWorkOrderPage(page);
await expectWorkOrderPageReady(page);
await expect(page.getByText("WO-501").first()).toBeVisible();
await expectStableScreenshot(page, "wo-mobile.png");

View file

@ -1,6 +1,7 @@
import {
useEffect,
useMemo,
useRef,
useState,
type Dispatch,
type ReactNode,
@ -264,14 +265,20 @@ function useTeamMemberEditor(
const [form, setForm] = useState<TeamMemberForm>(emptyForm);
const [submitted, setSubmitted] = useState(false);
const updateTeamMember = useUpdateTeamMember();
const currentMemberRef = useRef(currentMember);
useEffect(() => {
if (currentMember) {
setForm(formFromMember(currentMember));
currentMemberRef.current = currentMember;
}, [currentMember]);
useEffect(() => {
const member = currentMemberRef.current;
if (member) {
setForm(formFromMember(member));
setEditing(false);
setSubmitted(false);
}
}, [currentMember]);
}, [currentMember?.id]);
useEffect(() => {
setEditing(false);
@ -351,7 +358,11 @@ function useTeamMemberEditor(
toggleArea,
togglePermission,
save,
cancel: () => setEditing(false),
cancel: () => {
if (currentMember) setForm(formFromMember(currentMember));
setSubmitted(false);
setEditing(false);
},
beginEdit: () => setEditing(true),
};
}

View file

@ -22,11 +22,13 @@ function memberStatusLabel(member: TeamMemberListItem) {
export function TeamMembersTable({
rows,
isLoading,
hasError,
tab,
onOpen,
}: {
rows: TeamMemberListItem[];
isLoading: boolean;
hasError: boolean;
tab: "active" | "inactive";
onOpen: (member: TeamMemberListItem) => void;
}) {
@ -52,6 +54,15 @@ export function TeamMembersTable({
<CircularProgress size={28} />
</TableCell>
</TableRow>
) : hasError ? (
<TableRow>
<TableCell colSpan={8} align="center" sx={{ py: 8 }}>
<Typography sx={{ fontWeight: 600 }}>Unable to load team members</Typography>
<Typography variant="body2" color="text.secondary">
Check your connection and try again.
</Typography>
</TableCell>
</TableRow>
) : rows.length === 0 ? (
<TableRow>
<TableCell colSpan={8} align="center" sx={{ py: 8 }}>

View file

@ -85,7 +85,13 @@ export default function TeamMembersListPage() {
)}
<Box sx={{ flex: 1, overflow: "auto", p: 2 }}>
<TeamMembersTable rows={rows} isLoading={isLoading} tab={tab} onOpen={setSelectedMember} />
<TeamMembersTable
rows={rows}
isLoading={isLoading}
hasError={Boolean(error)}
tab={tab}
onOpen={setSelectedMember}
/>
</Box>
<Stack

View file

@ -118,4 +118,44 @@ describe("TeamMemberDetailDrawer", () => {
),
);
});
it("keeps edits when the detail query refreshes", async () => {
const user = userEvent.setup();
const { rerender } = renderWithProviders(
<TeamMemberDetailDrawer member={member} onClose={vi.fn()} />,
{ withAuth: false },
);
await user.click(screen.getByRole("button", { name: "Edit" }));
const name = screen.getByRole("textbox", { name: "Name" });
await user.clear(name);
await user.type(name, "Taylor Updated");
vi.mocked(useTeamMemberDetail).mockReturnValue({
data: { ...detail, phone: "555-0101" },
isLoading: false,
error: null,
} as unknown as ReturnType<typeof useTeamMemberDetail>);
rerender(<TeamMemberDetailDrawer member={member} onClose={vi.fn()} />);
expect(screen.getByRole("textbox", { name: "Name" })).toHaveValue("Taylor Updated");
expect(screen.getByRole("textbox", { name: "Phone" })).toHaveValue("555-0100");
});
it("restores the saved detail when editing is cancelled", async () => {
const user = userEvent.setup();
renderWithProviders(<TeamMemberDetailDrawer member={member} onClose={vi.fn()} />, {
withAuth: false,
});
await user.click(screen.getByRole("button", { name: "Edit" }));
const name = screen.getByRole("textbox", { name: "Name" });
await user.clear(name);
await user.type(name, "Discarded draft");
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(screen.getByRole("button", { name: "Edit" })).toBeInTheDocument();
expect(screen.getAllByText("Taylor Dispatcher")).not.toHaveLength(0);
expect(screen.queryByDisplayValue("Discarded draft")).not.toBeInTheDocument();
});
});