feat(web): responsive Vocabularies master/detail (drawer on narrow) (#58)

This commit is contained in:
2026-06-09 15:12:45 +02:00
parent b5756e16b5
commit 80c2aad298
2 changed files with 82 additions and 6 deletions
+57
View File
@@ -0,0 +1,57 @@
import { afterEach, expect, test, vi } from "vitest";
import { screen, within } from "@testing-library/react";
import { Route, Routes } from "react-router-dom";
import { renderApp } from "../test/render";
import { VocabulariesPage } from "./vocabularies-page";
import { VocabularyTerms } from "./vocabulary-terms";
import { SelectVocabularyPrompt } from "./select-vocabulary-prompt";
function setViewport(wide: boolean) {
Object.defineProperty(window, "matchMedia", {
value: (query: string): MediaQueryList =>
({
matches: wide && query === "(min-width: 1024px)",
media: query,
onchange: null,
addEventListener: () => {},
removeEventListener: () => {},
addListener: () => {},
removeListener: () => {},
dispatchEvent: () => false,
}) as MediaQueryList,
writable: true,
});
}
afterEach(() => vi.restoreAllMocks());
function tree() {
return (
<Routes>
<Route path="/vocabularies" element={<VocabulariesPage />}>
<Route index element={<SelectVocabularyPrompt />} />
<Route path=":id" element={<VocabularyTerms />} />
</Route>
</Routes>
);
}
test("narrow: a selected vocabulary's detail renders in a portaled drawer", async () => {
setViewport(false);
renderApp(tree(), { route: "/vocabularies/v-material" });
const body = within(document.body);
expect(
await body.findByRole("button", { name: /close detail/i }, { timeout: 5000 }),
).toBeInTheDocument();
});
test("wide: a selected vocabulary renders inline, with no detail drawer", async () => {
setViewport(true);
renderApp(tree(), { route: "/vocabularies/v-material" });
// VocabularyTerms renders its "Terms" caption inline in the right pane.
await screen.findByText(/terms/i);
expect(screen.queryByRole("button", { name: /close detail/i })).toBeNull();
});