The selected Space used to live in sessionStorage and ride along invisibly with every collection route. It now lives in the URL: collection pages exist both globally and beneath /spaces/:spaceUid, and the Space context derives its scope from the current location. - Add a Space route branch (Home, Explore, Calendar, Attachments) guarded by a SpaceRoute that shows loading, unavailable, and retry states. - Resolve collection routes through one matchPath-based helper built from the route constants; build Space-scoped paths with buildCollectionPath. - Fetch the route Space directly, seeded from the switcher list so pages are not held behind a second round-trip. - Turn the Space switcher into plain links that preserve the current collection view and query. - Drop the remembered-Space machinery: clearSelectedSpace/selectMemos, the compose-placement policy, routeSupportsCollectionScope, and the quick-find switchToAll flag are all implied by the URL now. - Return from memo details to the full origin URL, including its Space. - Add space.unavailable and space.back-to-memos strings; align the Azerbaijani catalog with English.
117 lines
4.7 KiB
TypeScript
117 lines
4.7 KiB
TypeScript
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { createMemoryRouter, MemoryRouter, RouterProvider, useLocation } from "react-router-dom";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
type MemoFilter,
|
|
MemoFilterProvider,
|
|
parseFilterQuery,
|
|
stringifyFilters,
|
|
useMemoFilterContext,
|
|
} from "@/contexts/MemoFilterContext";
|
|
import { BUILTIN_TASKS_VIEW_ID } from "@/lib/memo-views";
|
|
|
|
const expression = 'tags.exists(t, t.contains("50%, café & C++"))\n || space == null';
|
|
|
|
const Harness = () => {
|
|
const { filters, setFilters, setMemoView, memoView } = useMemoFilterContext();
|
|
const location = useLocation();
|
|
return (
|
|
<div>
|
|
<output data-testid="filters">{JSON.stringify(filters)}</output>
|
|
<output data-testid="memoView">{memoView}</output>
|
|
<output data-testid="url">{location.search}</output>
|
|
<button type="button" onClick={() => setFilters([{ factor: "celSearch", value: expression }])}>
|
|
Search CEL
|
|
</button>
|
|
<button type="button" onClick={() => setFilters([{ factor: "contentSearch", value: "plan" }])}>
|
|
Search plan
|
|
</button>
|
|
<button type="button" onClick={() => setMemoView(BUILTIN_TASKS_VIEW_ID)}>
|
|
Select Tasks
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
describe("MemoFilterProvider", () => {
|
|
it("restores CEL after URL synchronization, navigation history, and remount", async () => {
|
|
const routes = [
|
|
{
|
|
path: "*",
|
|
element: (
|
|
<MemoFilterProvider>
|
|
<Harness />
|
|
</MemoFilterProvider>
|
|
),
|
|
},
|
|
];
|
|
const router = createMemoryRouter(routes, { initialEntries: ["/inbox", "/explore"], initialIndex: 1 });
|
|
const rendered = render(<RouterProvider router={router} />);
|
|
fireEvent.click(screen.getByRole("button", { name: "Search CEL" }));
|
|
const expected: MemoFilter[] = [{ factor: "celSearch", value: expression }];
|
|
await waitFor(() => expect(parseFilterQuery(new URLSearchParams(router.state.location.search).get("filter"))).toEqual(expected));
|
|
const savedUrl = `/explore${router.state.location.search}`;
|
|
await act(async () => {
|
|
await router.navigate(-1);
|
|
});
|
|
await waitFor(() => expect(screen.getByTestId("filters")).toHaveTextContent("[]"));
|
|
await act(async () => {
|
|
await router.navigate(1);
|
|
});
|
|
await waitFor(() => expect(screen.getByTestId("filters")).toHaveTextContent(JSON.stringify(expected)));
|
|
rendered.unmount();
|
|
render(<RouterProvider router={createMemoryRouter(routes, { initialEntries: [savedUrl] })} />);
|
|
expect(screen.getByTestId("filters")).toHaveTextContent(JSON.stringify(expected));
|
|
expect(new URLSearchParams(router.state.location.search).get("filter")).toBe(stringifyFilters(expected));
|
|
});
|
|
it("preserves the Space origin when navigation removes collection filters", async () => {
|
|
const origin = "/spaces/product?filter=tagSearch%3Awork";
|
|
const router = createMemoryRouter(
|
|
[
|
|
{
|
|
path: "*",
|
|
element: (
|
|
<MemoFilterProvider>
|
|
<Harness />
|
|
</MemoFilterProvider>
|
|
),
|
|
},
|
|
],
|
|
{ initialEntries: [origin] },
|
|
);
|
|
render(<RouterProvider router={router} />);
|
|
fireEvent.click(screen.getByRole("button", { name: "Select Tasks" }));
|
|
const state = { from: origin, fromScope: "preserve" };
|
|
await act(() => router.navigate("/memos/1", { state }));
|
|
await waitFor(() => expect(screen.getByTestId("filters")).toHaveTextContent("[]"));
|
|
expect(router.state.location.search).toBe("");
|
|
expect(router.state.location.state).toEqual(state);
|
|
await act(() => router.navigate(-1));
|
|
expect(router.state.location.pathname + router.state.location.search).toBe(origin);
|
|
expect(screen.getByTestId("filters")).toHaveTextContent('"work"');
|
|
await act(() => router.navigate("/spaces/research?filter=tagSearch%3Awork"));
|
|
expect(screen.getByTestId("memoView")).toHaveTextContent(BUILTIN_TASKS_VIEW_ID);
|
|
});
|
|
|
|
it("keeps encoded values containing colons intact", () => {
|
|
expect(parseFilterQuery("contentSearch:https://example.com:8080/path")).toEqual([
|
|
{ factor: "contentSearch", value: "https://example.com:8080/path" },
|
|
]);
|
|
});
|
|
|
|
it("keeps search filters when selecting a view", () => {
|
|
render(
|
|
<MemoryRouter>
|
|
<MemoFilterProvider>
|
|
<Harness />
|
|
</MemoFilterProvider>
|
|
</MemoryRouter>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Search plan" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Select Tasks" }));
|
|
|
|
expect(screen.getByTestId("filters")).toHaveTextContent('[{"factor":"contentSearch","value":"plan"}]');
|
|
expect(screen.getByTestId("memoView")).toHaveTextContent(BUILTIN_TASKS_VIEW_ID);
|
|
});
|
|
});
|