Beute L3 Runden 2+3 und Review-Fixes: - Glas-Holo ohne warmes Leuchten, Wanderboden-Fuss, Fokusflug mit Geist-Ansicht, Inhalte im Fokus, Rähmchen mit Art-Farbe beim Anheben - Hinweis-Geometrie je Zarge nur einmal je Layout gebaut; Drehen mit Pointer-Capture, endet auch bei Loslassen ausserhalb (rig-pointer.ts) - Tiefenunschärfe ab 55 fps, Leistungsprobe bei jeder Bewegungsstufe, Context-Loss-Listener aufgeräumt, Trefferboxen als skalierte Einheitsbox Beute L4 Umbauen (/colonies/[id]/hive): Entwurf mit Basis-Schnappschuss beim ersten Schritt, Rückgängig/Wiederholen, Hinweis bei Serverstand- Änderung, Konfliktblatt. Beute L5: - Durchsicht-Abschnitt „Beute“: Aufbau-Änderungen in der Durchsicht, Blatt-Formulare stoppen das Submit-Bubbling durch Portale (sonst speicherte „Speichern“ im Blatt die ganze Durchsicht), Fokus nach Rückgängig, Folgevorschläge beim Speichern - Offline: Beute im Offline-Paket, Warteschlange pending-hive-changes (DB v5), Abarbeiten in Reihenfolge, keine Online-Änderung überholt wartende, Hinweis bei abgelehnter Änderung mit „Warteschlange öffnen“ T7 L1 Diktat: Spracherkennungs-Kapsel (Web Speech API), Einwilligung standardmässig aus, Diktat-Knopf an Freitextfeldern, Seite „Sprache & Datenschutz“; axe-core als exakte devDependency. Backend: NotificationsService liest Empfänger und schreibt in einer Transaktion mit FOR KEY SHARE (paralleles Löschen eines Benutzers führte zu P2003 und verzögerten Vorschlägen; Ursache der wackeligen Endtests). Zwei e2e-Prüfungen gegen geteilten DB-Zustand entschärft. Gates: Frontend lint, typecheck, 2740 Tests, build; Backend lint, typecheck, 4266 Unit-Tests, build; parallele e2e 3x grün (978). Browser-Prüfung 1280/390 px, Tastatur, offline, reduced motion. Refs #19 #20 #21 #22 #27 #32 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
445 lines
19 KiB
TypeScript
445 lines
19 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||
import userEvent from "@testing-library/user-event";
|
||
import { useRef, useState } from "react";
|
||
import { ApiError } from "@/lib/auth/api-client";
|
||
import { applyHiveOps } from "@/lib/hive/ops";
|
||
import { frames, inlineCatalog, part } from "@/lib/hive/test-utils";
|
||
import type { HiveOp, HiveState } from "@/lib/hive/types";
|
||
import { HiveOutline } from "./hive-outline";
|
||
import type { HiveOutlineHandle } from "./hive-outline";
|
||
import { PartSheet } from "./part-sheet";
|
||
import { createHiveViewStore, HiveViewProvider } from "./use-hive-view";
|
||
|
||
const catalog = inlineCatalog();
|
||
|
||
const baseState = (): HiveState => ({
|
||
schemaVersion: 1,
|
||
stack: [
|
||
part("f", "floor"),
|
||
part("b", "box", { frames: frames(10) }),
|
||
part("fe", "feeder", { state: { syrupLiters: 1, fondantKg: 0 } }),
|
||
part("l", "inner-lid", { state: { feedHole: "OPEN" } }),
|
||
part("r", "roof"),
|
||
],
|
||
});
|
||
|
||
interface HarnessProps {
|
||
partId: string;
|
||
initialState?: HiveState;
|
||
submit?: (ops: HiveOp[]) => Promise<unknown>;
|
||
startWithEvaporator?: boolean;
|
||
startWithFrameFeeder?: boolean;
|
||
}
|
||
|
||
/** Applies submitted ops to its own state, like the optimistic cache does. */
|
||
function Harness({ partId, initialState = baseState(), submit, startWithEvaporator, startWithFrameFeeder }: HarnessProps) {
|
||
const [state, setState] = useState(initialState);
|
||
const [open, setOpen] = useState(true);
|
||
const outlineRef = useRef<HiveOutlineHandle>(null);
|
||
const [store] = useState(createHiveViewStore);
|
||
const send = async (ops: HiveOp[]) => {
|
||
await submit?.(ops);
|
||
const result = applyHiveOps(state, ops, catalog);
|
||
if (result.ok) setState(result.state);
|
||
};
|
||
return (
|
||
<HiveViewProvider store={store}>
|
||
<HiveOutline
|
||
ref={outlineRef}
|
||
state={state}
|
||
catalog={catalog}
|
||
canEdit
|
||
announcement=""
|
||
onEditPart={() => {}}
|
||
onEditFrame={() => {}}
|
||
/>
|
||
<PartSheet
|
||
open={open}
|
||
onOpenChange={setOpen}
|
||
state={state}
|
||
catalog={catalog}
|
||
partId={partId}
|
||
submit={send}
|
||
outlineRef={outlineRef}
|
||
startWithEvaporator={startWithEvaporator}
|
||
startWithFrameFeeder={startWithFrameFeeder}
|
||
/>
|
||
</HiveViewProvider>
|
||
);
|
||
}
|
||
|
||
const save = () => screen.getByRole("button", { name: "Speichern" });
|
||
const dialog = () => screen.getByRole("dialog");
|
||
|
||
describe("PartSheet", () => {
|
||
it("is titled with the part's display name", () => {
|
||
render(<Harness partId="fe" />);
|
||
expect(screen.getByRole("dialog", { name: "Futterzarge" })).toBeInTheDocument();
|
||
});
|
||
|
||
it("shows one control per state field", () => {
|
||
render(<Harness partId="fe" />);
|
||
expect(within(dialog()).getByLabelText("Flüssigfutter")).toHaveValue("1");
|
||
expect(within(dialog()).getByLabelText("Futterteig")).toHaveValue("0");
|
||
expect(within(dialog()).getAllByRole("textbox")).toHaveLength(2);
|
||
});
|
||
|
||
it("shows a choice field as a radio group", () => {
|
||
render(<Harness partId="l" />);
|
||
expect(screen.getByRole("radiogroup", { name: "Futterloch" })).toBeInTheDocument();
|
||
expect(screen.getByRole("radio", { name: "offen" })).toBeChecked();
|
||
});
|
||
|
||
it("keeps Speichern disabled until something changed", () => {
|
||
render(<Harness partId="fe" />);
|
||
expect(save()).toBeDisabled();
|
||
});
|
||
|
||
it("does not let its submit bubble into an enclosing form", async () => {
|
||
const outer = vi.fn((event: { preventDefault: () => void }) => event.preventDefault());
|
||
const user = userEvent.setup();
|
||
render(
|
||
<form onSubmit={outer}>
|
||
<Harness partId="fe" submit={vi.fn().mockResolvedValue(undefined)} />
|
||
</form>,
|
||
);
|
||
const field = within(dialog()).getByLabelText("Flüssigfutter");
|
||
await user.clear(field);
|
||
await user.type(field, "3");
|
||
await user.click(save());
|
||
expect(outer).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("saves only the changed level as setState", async () => {
|
||
const submit = vi.fn().mockResolvedValue(undefined);
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="fe" submit={submit} />);
|
||
const field = within(dialog()).getByLabelText("Flüssigfutter");
|
||
await user.clear(field);
|
||
await user.type(field, "3");
|
||
await user.click(save());
|
||
expect(submit).toHaveBeenCalledWith([{ op: "setState", id: "fe", key: "syrupLiters", value: 3 }]);
|
||
});
|
||
|
||
it("takes a German decimal comma", async () => {
|
||
const submit = vi.fn().mockResolvedValue(undefined);
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="fe" submit={submit} />);
|
||
const field = within(dialog()).getByLabelText("Flüssigfutter");
|
||
await user.clear(field);
|
||
await user.type(field, "2,5");
|
||
await user.click(save());
|
||
expect(submit).toHaveBeenCalledWith([{ op: "setState", id: "fe", key: "syrupLiters", value: 2.5 }]);
|
||
});
|
||
|
||
it("blocks saving a level off the step grid and says so", async () => {
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="fe" />);
|
||
const field = within(dialog()).getByLabelText("Flüssigfutter");
|
||
await user.clear(field);
|
||
await user.type(field, "2,3");
|
||
expect(screen.getByText("Nur Schritte von 0,5.")).toBeInTheDocument();
|
||
expect(save()).toBeDisabled();
|
||
});
|
||
|
||
it("blocks saving an empty level", async () => {
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="fe" />);
|
||
await user.clear(within(dialog()).getByLabelText("Flüssigfutter"));
|
||
expect(screen.getByText("Bitte einen Wert angeben.")).toBeInTheDocument();
|
||
expect(save()).toBeDisabled();
|
||
});
|
||
|
||
it("keeps the level at its maximum when more is typed", async () => {
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="fe" />);
|
||
const field = within(dialog()).getByLabelText("Flüssigfutter");
|
||
await user.clear(field);
|
||
await user.type(field, "9");
|
||
expect(field).toHaveValue("3");
|
||
expect(save()).toBeEnabled();
|
||
});
|
||
|
||
it("saves a chosen option of a choice field", async () => {
|
||
const submit = vi.fn().mockResolvedValue(undefined);
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="l" submit={submit} />);
|
||
await user.click(screen.getByRole("radio", { name: "zu" }));
|
||
await user.click(save());
|
||
expect(submit).toHaveBeenCalledWith([{ op: "setState", id: "l", key: "feedHole", value: "CLOSED" }]);
|
||
});
|
||
|
||
it("shows an error inline and stays open when saving fails", async () => {
|
||
const submit = vi.fn().mockRejectedValue(new ApiError(500, "kaputt", null));
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="l" submit={submit} />);
|
||
await user.click(screen.getByRole("radio", { name: "zu" }));
|
||
await user.click(save());
|
||
expect(await screen.findByRole("alert")).toHaveTextContent("Speichern fehlgeschlagen");
|
||
expect(dialog()).toBeInTheDocument();
|
||
});
|
||
|
||
it("closes on a conflict so the conflict sheet can take over", async () => {
|
||
const submit = vi.fn().mockRejectedValue(
|
||
new ApiError(409, "Konflikt", { code: "HIVE_CONFLICT", opIndex: 0, reason: "x", current: {} }),
|
||
);
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="l" submit={submit} />);
|
||
await user.click(screen.getByRole("radio", { name: "zu" }));
|
||
await user.click(save());
|
||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||
});
|
||
|
||
it("returns focus to the edited part, not to the body", async () => {
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="l" />);
|
||
await user.click(screen.getByRole("radio", { name: "zu" }));
|
||
await user.click(save());
|
||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||
await waitFor(() => expect(screen.getByRole("treeitem", { name: /Holzdeckel/ })).toHaveFocus());
|
||
expect(document.body).not.toHaveFocus();
|
||
});
|
||
|
||
describe("floor", () => {
|
||
it("offers Bausperre drin / draußen and inserts the active floor insert type", async () => {
|
||
const submit = vi.fn().mockResolvedValue(undefined);
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="f" submit={submit} />);
|
||
expect(screen.getByRole("radiogroup", { name: "Bausperre" })).toBeInTheDocument();
|
||
expect(screen.getByRole("radio", { name: "draußen" })).toBeChecked();
|
||
await user.click(screen.getByRole("radio", { name: "drin" }));
|
||
await user.click(save());
|
||
expect(submit).toHaveBeenCalledWith([
|
||
{
|
||
op: "addInsert",
|
||
hostId: "f",
|
||
part: { id: expect.any(String), partTypeId: "floor-insert", state: {} },
|
||
},
|
||
]);
|
||
});
|
||
|
||
it("removes an existing Bausperre", async () => {
|
||
const withInsert = baseState();
|
||
withInsert.stack[0] = part("f", "floor", { inserts: [part("fi", "floor-insert")] });
|
||
const submit = vi.fn().mockResolvedValue(undefined);
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="f" initialState={withInsert} submit={submit} />);
|
||
expect(screen.getByRole("radio", { name: "drin" })).toBeChecked();
|
||
await user.click(screen.getByRole("radio", { name: "draußen" }));
|
||
await user.click(save());
|
||
expect(submit).toHaveBeenCalledWith([{ op: "removePart", id: "fi" }]);
|
||
});
|
||
});
|
||
|
||
describe("box", () => {
|
||
it("inserts an evaporator with the chosen acid level", async () => {
|
||
const submit = vi.fn().mockResolvedValue(undefined);
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="b" submit={submit} />);
|
||
expect(screen.queryByLabelText("Säure")).toBeNull();
|
||
await user.click(screen.getByRole("radio", { name: "Verdunster einsetzen" }));
|
||
const acid = within(dialog()).getByLabelText("Säure");
|
||
await user.clear(acid);
|
||
await user.type(acid, "250");
|
||
await user.click(save());
|
||
expect(submit).toHaveBeenCalledWith([
|
||
{
|
||
op: "addInsert",
|
||
hostId: "b",
|
||
part: { id: expect.any(String), partTypeId: "evaporator", state: { acidMl: 250 } },
|
||
},
|
||
]);
|
||
});
|
||
|
||
it("blocks saving an evaporator whose acid is off the step grid", async () => {
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="b" />);
|
||
await user.click(screen.getByRole("radio", { name: "Verdunster einsetzen" }));
|
||
const acid = within(dialog()).getByLabelText("Säure");
|
||
await user.clear(acid);
|
||
await user.type(acid, "255");
|
||
expect(save()).toBeDisabled();
|
||
});
|
||
|
||
it("preselects the evaporator when opened from the outline action", () => {
|
||
render(<Harness partId="b" startWithEvaporator />);
|
||
expect(screen.getByRole("radio", { name: "Verdunster einsetzen" })).toBeChecked();
|
||
expect(within(dialog()).getByLabelText("Säure")).toBeInTheDocument();
|
||
});
|
||
|
||
it("offers no insert when the box already has an evaporator", () => {
|
||
const withEvaporator = baseState();
|
||
withEvaporator.stack[1] = part("b", "box", {
|
||
frames: frames(10),
|
||
inserts: [part("ev", "evaporator", { state: { acidMl: 100 } })],
|
||
});
|
||
render(<Harness partId="b" initialState={withEvaporator} />);
|
||
expect(screen.queryByRole("radio", { name: "Verdunster einsetzen" })).toBeNull();
|
||
});
|
||
|
||
it("moves focus to the new evaporator item after inserting", async () => {
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="b" />);
|
||
await user.click(screen.getByRole("radio", { name: "Verdunster einsetzen" }));
|
||
const acid = within(dialog()).getByLabelText("Säure");
|
||
await user.clear(acid);
|
||
await user.type(acid, "100");
|
||
await user.click(save());
|
||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||
await waitFor(() => expect(screen.getByRole("treeitem", { name: /Verdunster/ })).toHaveFocus());
|
||
expect(document.body).not.toHaveFocus();
|
||
});
|
||
});
|
||
|
||
describe("evaporator", () => {
|
||
const withEvaporator = (): HiveState => {
|
||
const s = baseState();
|
||
s.stack[1] = part("b", "box", {
|
||
frames: frames(10),
|
||
inserts: [part("ev", "evaporator", { state: { acidMl: 100 } })],
|
||
});
|
||
return s;
|
||
};
|
||
|
||
it("edits the acid level", async () => {
|
||
const submit = vi.fn().mockResolvedValue(undefined);
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="ev" initialState={withEvaporator()} submit={submit} />);
|
||
const acid = within(dialog()).getByLabelText("Säure");
|
||
await user.clear(acid);
|
||
await user.type(acid, "200");
|
||
await user.click(save());
|
||
expect(submit).toHaveBeenCalledWith([{ op: "setState", id: "ev", key: "acidMl", value: 200 }]);
|
||
});
|
||
|
||
it("asks before removing, then removes and focuses the box", async () => {
|
||
const submit = vi.fn().mockResolvedValue(undefined);
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="ev" initialState={withEvaporator()} submit={submit} />);
|
||
await user.click(screen.getByRole("button", { name: "Verdunster entfernen" }));
|
||
expect(submit).not.toHaveBeenCalled();
|
||
await user.click(screen.getByRole("button", { name: "Ja, entfernen" }));
|
||
expect(submit).toHaveBeenCalledWith([{ op: "removePart", id: "ev" }]);
|
||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||
await waitFor(() => expect(screen.getByRole("treeitem", { name: /Zarge/ })).toHaveFocus());
|
||
expect(document.body).not.toHaveFocus();
|
||
});
|
||
|
||
it("moves focus into the confirmation, labels it as a group and returns focus on cancel", async () => {
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="ev" initialState={withEvaporator()} submit={vi.fn()} />);
|
||
await user.click(screen.getByRole("button", { name: "Verdunster entfernen" }));
|
||
const group = screen.getByRole("group", { name: "Verdunster wirklich entfernen?" });
|
||
expect(within(group).getByText("Verdunster wirklich entfernen?")).toHaveFocus();
|
||
await user.click(screen.getByRole("button", { name: "Abbrechen" }));
|
||
expect(screen.getByRole("button", { name: "Verdunster entfernen" })).toHaveFocus();
|
||
});
|
||
|
||
it("keeps the evaporator when the confirmation is cancelled", async () => {
|
||
const submit = vi.fn();
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="ev" initialState={withEvaporator()} submit={submit} />);
|
||
await user.click(screen.getByRole("button", { name: "Verdunster entfernen" }));
|
||
await user.click(screen.getByRole("button", { name: "Abbrechen" }));
|
||
expect(submit).not.toHaveBeenCalled();
|
||
expect(screen.getByRole("button", { name: "Verdunster entfernen" })).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe("frame feeder", () => {
|
||
const withPocket = (): HiveState => {
|
||
const s = baseState();
|
||
s.stack[1] = part("b", "box", {
|
||
frames: frames(10).map((f) => (f.slot <= 2 ? { ...f, type: "NONE" as const } : f)),
|
||
inserts: [part("ff", "frame-feeder", { slot: 1, state: { syrupLiters: 0.5 } })],
|
||
});
|
||
return s;
|
||
};
|
||
|
||
it("inserts a frame feeder: setFrames clears the covered slots, addInsert hangs it", async () => {
|
||
const submit = vi.fn().mockResolvedValue(undefined);
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="b" submit={submit} />);
|
||
await user.click(screen.getByRole("radio", { name: "Futtertasche einsetzen" }));
|
||
await user.click(screen.getByRole("radio", { name: "Plätze 3–4" }));
|
||
expect(screen.getByText(/werden zu „leer“/)).toBeInTheDocument();
|
||
const syrup = within(dialog()).getByLabelText("Flüssigfutter");
|
||
await user.clear(syrup);
|
||
await user.type(syrup, "1,5");
|
||
await user.click(save());
|
||
const expectedFrames = frames(10).map((f) => (f.slot === 3 || f.slot === 4 ? { ...f, type: "NONE" } : f));
|
||
expect(submit).toHaveBeenCalledWith([
|
||
{ op: "setFrames", boxId: "b", frames: expectedFrames },
|
||
{
|
||
op: "addInsert",
|
||
hostId: "b",
|
||
part: { id: expect.any(String), partTypeId: "frame-feeder", state: { syrupLiters: 1.5 }, slot: 3 },
|
||
},
|
||
]);
|
||
});
|
||
|
||
it("skips setFrames when the covered slots are already empty", async () => {
|
||
const submit = vi.fn().mockResolvedValue(undefined);
|
||
const user = userEvent.setup();
|
||
const state = baseState();
|
||
state.stack[1] = part("b", "box", {
|
||
frames: frames(10).map((f) => (f.slot <= 2 ? { ...f, type: "NONE" as const } : f)),
|
||
});
|
||
render(<Harness partId="b" initialState={state} submit={submit} />);
|
||
await user.click(screen.getByRole("radio", { name: "Futtertasche einsetzen" }));
|
||
expect(screen.queryByText(/werden zu „leer“/)).toBeNull();
|
||
await user.click(save());
|
||
expect(submit).toHaveBeenCalledWith([
|
||
{ op: "addInsert", hostId: "b", part: expect.objectContaining({ partTypeId: "frame-feeder", slot: 1 }) },
|
||
]);
|
||
});
|
||
|
||
it("offers only positions where the slots are still free", async () => {
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="b" initialState={withPocket()} />);
|
||
await user.click(screen.getByRole("radio", { name: "Futtertasche einsetzen" }));
|
||
expect(screen.queryByRole("radio", { name: "Plätze 1–2" })).toBeNull();
|
||
expect(screen.queryByRole("radio", { name: "Plätze 2–3" })).toBeNull();
|
||
expect(screen.getByRole("radio", { name: "Plätze 3–4" })).toBeInTheDocument();
|
||
});
|
||
|
||
it("preselects the frame feeder when opened from the outline action", () => {
|
||
render(<Harness partId="b" startWithFrameFeeder />);
|
||
expect(screen.getByRole("radio", { name: "Futtertasche einsetzen" })).toBeChecked();
|
||
});
|
||
|
||
it("moves focus to the new pocket item after inserting", async () => {
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="b" />);
|
||
await user.click(screen.getByRole("radio", { name: "Futtertasche einsetzen" }));
|
||
await user.click(save());
|
||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||
await waitFor(() => expect(screen.getByRole("treeitem", { name: /^F1–2 · Futtertasche/ })).toHaveFocus());
|
||
});
|
||
|
||
it("edits the pocket level with a stepper", async () => {
|
||
const submit = vi.fn().mockResolvedValue(undefined);
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="ff" initialState={withPocket()} submit={submit} />);
|
||
expect(screen.getByRole("dialog", { name: "Futtertasche" })).toBeInTheDocument();
|
||
await user.click(within(dialog()).getByRole("button", { name: /Flüssigfutter erhöhen/ }));
|
||
await user.click(save());
|
||
expect(submit).toHaveBeenCalledWith([{ op: "setState", id: "ff", key: "syrupLiters", value: 0.75 }]);
|
||
});
|
||
|
||
it("asks before removing the pocket, then removes it and focuses the box", async () => {
|
||
const submit = vi.fn().mockResolvedValue(undefined);
|
||
const user = userEvent.setup();
|
||
render(<Harness partId="ff" initialState={withPocket()} submit={submit} />);
|
||
await user.click(screen.getByRole("button", { name: "Futtertasche entfernen" }));
|
||
expect(submit).not.toHaveBeenCalled();
|
||
expect(screen.getByRole("group", { name: "Futtertasche wirklich entfernen?" })).toBeInTheDocument();
|
||
await user.click(screen.getByRole("button", { name: "Ja, entfernen" }));
|
||
expect(submit).toHaveBeenCalledWith([{ op: "removePart", id: "ff" }]);
|
||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||
await waitFor(() => expect(screen.getByRole("treeitem", { name: /Zarge/ })).toHaveFocus());
|
||
});
|
||
});
|
||
});
|