test: Kürzel-Verdrahtung zwischen Layout und Seiten absichern
CI / backend (push) Successful in 2m2s
CI / frontend (push) Canceled after 2m41s
Images bauen / build (backend) (push) Successful in 3m54s
Images bauen / build (frontend) (push) Successful in 4m32s

Sechs Tests für usePendingAction und useSearchFocus. Der Store wird vor dem
Test zurückgesetzt statt danach – nach dem Test hängen die Komponenten noch am
Store und React meldet ein Update außerhalb von act().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
Jonas Menzel
2026-09-09 21:58:20 +02:00
co-authored by Claude Opus 5
parent d63cc0af65
commit ba02e17776
@@ -0,0 +1,108 @@
/** Tests des Bindeglieds zwischen globalen Kürzeln und den Seiten. */
import { useRef } from "react";
import { act, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { usePendingAction, useSearchFocus } from "@/hooks/usePageShortcuts";
import { useUiStore } from "@/store/ui";
beforeEach(() => {
// Der Store lebt global sonst tropft eine Absicht in den nächsten Test.
// Bewusst vor dem Test und nicht danach: nach dem Test hängen die Komponenten
// noch am Store und React meldet ein Update außerhalb von act().
useUiStore.setState({ pendingAction: null, searchFocusRequest: 0 });
});
function Seite({ run }: { run: () => void }) {
const suchfeld = useRef<HTMLInputElement>(null);
usePendingAction("new-transaction", run);
useSearchFocus(suchfeld);
return <input ref={suchfeld} aria-label="Suche" defaultValue="Vorbelegung" />;
}
describe("usePendingAction", () => {
it("führt die Absicht aus, sobald sie hinterlegt wird", async () => {
const oeffnen = vi.fn();
render(<Seite run={oeffnen} />);
expect(oeffnen).not.toHaveBeenCalled();
await act(async () => {
useUiStore.getState().requestAction("new-transaction");
});
await waitFor(() => expect(oeffnen).toHaveBeenCalledOnce());
// Die Absicht ist danach verbraucht.
expect(useUiStore.getState().pendingAction).toBeNull();
});
it("ignoriert eine Absicht, die einer anderen Seite gilt", async () => {
const oeffnen = vi.fn();
render(<Seite run={oeffnen} />);
await act(async () => {
useUiStore.getState().requestAction("new-recurrence");
});
await waitFor(() => {
expect(useUiStore.getState().pendingAction).toBe("new-recurrence");
});
expect(oeffnen).not.toHaveBeenCalled();
});
it("führt dieselbe Absicht nicht zweimal aus", async () => {
const oeffnen = vi.fn();
const { rerender } = render(<Seite run={oeffnen} />);
await act(async () => {
useUiStore.getState().requestAction("new-transaction");
});
await waitFor(() => expect(oeffnen).toHaveBeenCalledOnce());
rerender(<Seite run={oeffnen} />);
expect(oeffnen).toHaveBeenCalledOnce();
});
});
describe("useSearchFocus", () => {
it("fokussiert und markiert das Suchfeld auf Anfrage", async () => {
render(<Seite run={vi.fn()} />);
const feld = screen.getByLabelText("Suche") as HTMLInputElement;
expect(feld).not.toHaveFocus();
await act(async () => {
useUiStore.getState().requestSearchFocus();
});
await waitFor(() => expect(feld).toHaveFocus());
// Der vorhandene Text ist markiert, damit Tippen ihn ersetzt.
expect(feld.selectionStart).toBe(0);
expect(feld.selectionEnd).toBe("Vorbelegung".length);
});
it("greift beim ersten Rendern noch nicht", () => {
render(<Seite run={vi.fn()} />);
expect(screen.getByLabelText("Suche")).not.toHaveFocus();
});
it("wirkt auch bei mehrfacher Anfrage", async () => {
render(<Seite run={vi.fn()} />);
const feld = screen.getByLabelText("Suche");
await act(async () => {
useUiStore.getState().requestSearchFocus();
});
await waitFor(() => expect(feld).toHaveFocus());
await act(async () => feld.blur());
await act(async () => {
useUiStore.getState().requestSearchFocus();
});
await waitFor(() => expect(feld).toHaveFocus());
});
});