The "all" / "preserve" origin scope existed to decide whether returning
from a memo detail should clear the remembered Space. With the Space now
part of the origin URL itself, nothing branches on it any more; it was only
computed, serialized into router state as fromScope, and threaded through
props as parentScope.
Router state is now just { from }, createMemoNavigationState takes one
argument, and resolveMemoDetailOrigin / resolveMemoParentPage return the
origin page as a string. The archived-memo fallback to /archived stays.
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 };
|
|
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);
|
|
});
|
|
});
|