ROUTED

tcmts.cli.test.e2e.react.ROUTED · #rqtpt47b · term · runs anywhere

An app on the react library: routes from the path, links followed in the app, data read once per key.

export const ROUTED = `// tcm: namespace ui
import { Suspense, use } from "react";
import { match, navigate, refresh, useAppLinks, useData, useLocation, useNavigating } from "#codebase/ui/lib/react";
import { expect } from "vitest";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";

export const reads = { n: 0 };

export async function readNote(id: string): Promise<string> {
  await new Promise((r) => setTimeout(r, 30));
  reads.n++;
  return \`note \${id} (read \${reads.n})\`;
}

export function Note({ id }: { id: string }) {
  const note = useData(\`note:\${id}\`, () => readNote(id), { keep: true });
  return <p>{note.loading && !note.stale ? "loading" : note.value}{note.stale ? " (stale)" : ""}</p>;
}

export function App() {
  useAppLinks();
  const at = useLocation();
  const note = match("/notes/:id", at.path);
  return (
    <main>
      <nav><a href="/">home</a> <a href="/notes/a">a</a> <a href="/notes/b?x=1">b</a></nav>
      <h1>{note ? \`note \${note.id}\` : "home"}</h1>
      {note ? <Note id={note.id} /> : <p>query {at.query.get("q") ?? "none"}</p>}
    </main>
  );
}

export const pages: Record<string, Promise<string>> = {};

export function Page({ path }: { path: string }) {
  return <p>{use((pages[path] ??= new Promise((r) => setTimeout(() => r(\`page \${path}\`), 50))))}</p>;
}

export function Pages() {
  const at = useLocation();
  const navigating = useNavigating();
  return (
    <section>
      {navigating && <i>navigating</i>}
      <Suspense fallback={<p>fallback</p>}><Page path={at.path} /></Suspense>
    </section>
  );
}

export const testMatch = () => {
  expect(match("/notes/:id", "/notes/a%20b")).toEqual({ id: "a b" });
  expect(match("/notes/:id", "/notes")).toBeUndefined();
  expect(match("/notes/:id", "/notes/a/b")).toBeUndefined();
  expect(match("/docs/*", "/docs/a/b")).toEqual({ "*": "a/b" });
  expect(match("/", "/")).toEqual({});
};

export const testRoutesLinksAndData = async () => {
  history.replaceState(null, "", "/?q=hi");
  render(<App />);
  expect(screen.getByRole("heading").textContent).toBe("home");
  expect(screen.getByText("query hi")).toBeTruthy();
  // a link, followed in the app (a reload would end the test)
  fireEvent.click(screen.getByRole("link", { name: "a" }));
  expect(location.pathname).toBe("/notes/a");
  expect(screen.getByRole("heading").textContent).toBe("note a");
  expect(screen.getByText("loading")).toBeTruthy();
  await waitFor(() => expect(screen.getByText("note a (read 1)")).toBeTruthy());
  // the next note: the last one's kept while it's read
  fireEvent.click(screen.getByRole("link", { name: "b" }));
  expect(location.search).toBe("?x=1");
  expect(screen.getByText("note a (read 1) (stale)")).toBeTruthy();
  await waitFor(() => expect(screen.getByText("note b (read 2)")).toBeTruthy());
  // back: read already, so shown at once and not read again
  history.back();
  await waitFor(() => expect(screen.getByRole("heading").textContent).toBe("note a"));
  expect(screen.getByText("note a (read 1)")).toBeTruthy();
  expect(reads.n).toBe(2);
  // read again after a change, keeping what's shown meanwhile
  const again = refresh("note:a");
  expect(screen.getByText("note a (read 1)")).toBeTruthy();
  await again;
  await waitFor(() => expect(screen.getByText("note a (read 3)")).toBeTruthy());
  const length = history.length;
  navigate("/", { replace: true });
  expect(history.length).toBe(length);
  await waitFor(() => expect(screen.getByRole("heading").textContent).toBe("home"));
};

export const testNavigatingKeepsThePageUntilTheNextIsReady = async () => {
  history.replaceState(null, "", "/one");
  await act(async () => void render(<Pages />));
  await waitFor(() => expect(screen.getByText("page /one")).toBeTruthy());
  await act(async () => navigate("/two"));
  // a transition: the page stays, no fallback
  expect(screen.getByText("page /one")).toBeTruthy();
  expect(screen.queryByText("fallback")).toBeNull();
  expect(screen.getByText("navigating")).toBeTruthy();
  await waitFor(() => expect(screen.getByText("page /two")).toBeTruthy());
  expect(screen.queryByText("navigating")).toBeNull();
  // back (to a page read again): a transition too
  delete pages["/one"];
  await act(async () => {
    history.back();
    await new Promise((r) => addEventListener("popstate", r, { once: true }));
    // (it renders just after the event)
    await new Promise((r) => setTimeout(r));
  });
  expect(screen.getByText("navigating")).toBeTruthy();
  expect(screen.getByText("page /two")).toBeTruthy();
  expect(screen.queryByText("fallback")).toBeNull();
  await waitFor(() => expect(screen.getByText("page /one")).toBeTruthy());
};
`

used by