feat: Beute L3–L5 (3D-Szene, Umbauen, Durchsicht, offline) und Diktat (T7 L1)
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]>
This commit is contained in:
1 parent
ab45d6e24e
commit
1a57de8573
123 files changed
+7639
-355
No files matched your search
@@ -19,20 +19,32 @@ import { colonyFixture, historyItem, sampleState, setupView, systemDto } from ".
|
||||
vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) }));
|
||||
vi.mock("sonner", () => ({ toast: vi.fn() }));
|
||||
vi.mock("@/lib/colonies/api", () => ({ fetchColonies: vi.fn().mockResolvedValue([]) }));
|
||||
vi.mock("@/lib/hive/api", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/lib/hive/api")>()),
|
||||
fetchHive: vi.fn(),
|
||||
fetchHiveSystems: vi.fn(),
|
||||
fetchHiveTemplate: vi.fn(),
|
||||
fetchHiveHistory: vi.fn(),
|
||||
fetchHiveAt: vi.fn(),
|
||||
postHiveChange: vi.fn(),
|
||||
createHive: vi.fn(),
|
||||
vi.mock("@/lib/hive/api", async (importOriginal) => {
|
||||
const fetchHiveSystems = vi.fn();
|
||||
return {
|
||||
...(await importOriginal<typeof import("@/lib/hive/api")>()),
|
||||
fetchHive: vi.fn(),
|
||||
fetchHiveSystems,
|
||||
// The offline-aware variant answers like the plain one here (no offline packs in this spec).
|
||||
fetchHiveSystemsWithFallback: vi.fn(() => fetchHiveSystems()),
|
||||
fetchHiveTemplate: vi.fn(),
|
||||
fetchHiveHistory: vi.fn(),
|
||||
fetchHiveAt: vi.fn(),
|
||||
postHiveChange: vi.fn(),
|
||||
createHive: vi.fn(),
|
||||
};
|
||||
});
|
||||
const pendingHive = vi.hoisted(() => ({ ops: [] as unknown[], count: 0 }));
|
||||
vi.mock("@/components/hive/use-pending-hive", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/components/hive/use-pending-hive")>()),
|
||||
usePendingHiveOps: () => pendingHive.ops,
|
||||
usePendingHiveCount: () => pendingHive.count,
|
||||
}));
|
||||
let permissions: string[] = [];
|
||||
vi.mock("@/lib/auth/auth-context", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/lib/auth/auth-context")>()),
|
||||
useAuth: () => ({ hasPermission: (key: string) => permissions.includes(key) }),
|
||||
useCurrentUserId: () => null,
|
||||
}));
|
||||
const connectivity = vi.hoisted(() => ({ value: "online" as "online" | "offline" }));
|
||||
vi.mock("@/lib/offline/connectivity", async (importOriginal) => ({
|
||||
@@ -64,6 +76,8 @@ describe("HiveCard", () => {
|
||||
beforeEach(() => {
|
||||
permissions = ["colonies:read", "colonies:update"];
|
||||
connectivity.value = "online";
|
||||
pendingHive.ops = [];
|
||||
pendingHive.count = 0;
|
||||
vi.mocked(fetchHive).mockReset().mockResolvedValue(setupView());
|
||||
vi.mocked(fetchHiveSystems).mockReset().mockResolvedValue([systemDto()]);
|
||||
vi.mocked(fetchHiveTemplate).mockReset().mockResolvedValue(templateState());
|
||||
@@ -152,6 +166,14 @@ describe("HiveCard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows changes that wait for network on top of the loaded hive, with a note", async () => {
|
||||
pendingHive.ops = FRAME_EDIT_OPS;
|
||||
pendingHive.count = 1;
|
||||
renderCard();
|
||||
expect(await screen.findByText(/7 Brutwaben/)).toBeInTheDocument();
|
||||
expect(screen.getByText("1 Änderung wartet auf Netz")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the outline in the DOM but visually hidden until „Aufbau als Liste“", async () => {
|
||||
renderCard();
|
||||
const toggle = await screen.findByRole("button", { name: "Aufbau als Liste" });
|
||||
@@ -239,6 +261,32 @@ describe("HiveCard", () => {
|
||||
expect(screen.queryByRole("link", { name: "Umbauen" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("seeds the inspection draft from initialOps (after a remount)", async () => {
|
||||
const onOpsChange = vi.fn();
|
||||
renderCard({ mode: "inspection", onOpsChange, initialOps: FRAME_EDIT_OPS });
|
||||
await screen.findByRole("img", { name: /Beute mit/ });
|
||||
expect(await screen.findByText(/7 Brutwaben/)).toBeInTheDocument();
|
||||
// The outline shows the seeded draft, and seeding is not reported as a new edit.
|
||||
await userEvent.click(screen.getByRole("treeitem", { name: /Zarge 1/ }));
|
||||
expect(screen.getByRole("treeitem", { name: /R5 · Brutwabe/ })).toBeInTheDocument();
|
||||
expect(onOpsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores initialOps that do not apply to the loaded hive, and in view mode", async () => {
|
||||
const broken: HiveOp[] = [{ op: "setFrame", boxId: "gone", slot: 5, type: "BROOD", name: null }];
|
||||
const onOpsChange = vi.fn();
|
||||
const first = renderCard({ mode: "inspection", initialOps: broken, onOpsChange });
|
||||
await screen.findByRole("img", { name: /Beute mit/ });
|
||||
expect(screen.getByText("2 Zargen · 6 Brutwaben · Futter 5 l · Bausperre drin")).toBeInTheDocument();
|
||||
// The form learns that its stored ops were dropped, so its list matches the card.
|
||||
expect(onOpsChange).toHaveBeenCalledWith([]);
|
||||
first.unmount();
|
||||
|
||||
renderCard({ initialOps: FRAME_EDIT_OPS });
|
||||
await screen.findByRole("img", { name: /Beute mit/ });
|
||||
expect(screen.getByText("2 Zargen · 6 Brutwaben · Futter 5 l · Bausperre drin")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the conflict sheet when the hive changed meanwhile", async () => {
|
||||
vi.mocked(postHiveChange).mockRejectedValue(
|
||||
new ApiError(409, "Konflikt", {
|
||||
|
||||
Reference in new issue
Block a user