Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions apps/loopover-ui/eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,10 @@ export default tseslint.config(
// because they flagged 24 pre-existing files. #9588 fixed them: purity, refs and static-components are
// now clean and back at error, where the recommended preset puts them.
//
// set-state-in-effect stays at warn for ONE remaining site: docs-toc reads its headings out of the
// rendered DOM. Sourcing them from the compiled MDX `toc` instead is the real fix, but the component
// lives in the docs LAYOUT while that data lives in the child route -- and it also serves docs pages
// that are not MDX at all and so have no compiled toc. That is its own change, tracked in #9872;
// every other call site in this app is already clean.
"react-hooks/set-state-in-effect": "warn",
// set-state-in-effect is at ERROR as of #9872, which removed the last site: docs-toc built its rail
// by scanning the rendered DOM and reading the result into state from an effect. It now renders the
// page's compiled MDX `toc`, so there is nothing to discover and nothing to set.
"react-hooks/set-state-in-effect": "error",
"no-restricted-imports": [
"error",
{
Expand Down
2 changes: 1 addition & 1 deletion apps/loopover-ui/src/components/site/docs-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function DocsPage({
<p className="mt-3 max-w-2xl text-token-sm text-muted-foreground">{description}</p>
)}
</header>
<div className="space-y-5 text-token-base leading-token-relaxed text-foreground/85 [&_h2]:mt-10 [&_h2]:mb-2 [&_h2]:text-token-lg [&_h2]:font-medium [&_h2]:text-foreground [&_h3]:mt-6 [&_h3]:mb-1.5 [&_h3]:text-token-sm [&_h3]:font-medium [&_h3]:text-foreground [&_p]:text-muted-foreground [&_strong]:text-foreground [&_ul]:list-disc [&_ul]:pl-6 [&_ul]:text-muted-foreground [&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:text-muted-foreground [&_li]:mt-1 [&_p>a]:text-mint [&_li>a]:text-mint [&_p>a:hover]:underline [&_li>a:hover]:underline [&_code]:rounded [&_code]:bg-accent/40 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-token-xs">
<div className="space-y-5 text-token-base leading-token-relaxed text-foreground/85 [&_h2]:scroll-mt-20 [&_h3]:scroll-mt-20 [&_h2]:mt-10 [&_h2]:mb-2 [&_h2]:text-token-lg [&_h2]:font-medium [&_h2]:text-foreground [&_h3]:mt-6 [&_h3]:mb-1.5 [&_h3]:text-token-sm [&_h3]:font-medium [&_h3]:text-foreground [&_p]:text-muted-foreground [&_strong]:text-foreground [&_ul]:list-disc [&_ul]:pl-6 [&_ul]:text-muted-foreground [&_ol]:list-decimal [&_ol]:pl-6 [&_ol]:text-muted-foreground [&_li]:mt-1 [&_p>a]:text-mint [&_li>a]:text-mint [&_p>a:hover]:underline [&_li>a:hover]:underline [&_code]:rounded [&_code]:bg-accent/40 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-token-xs">
{children}
</div>
<DocsPrevNext />
Expand Down
103 changes: 103 additions & 0 deletions apps/loopover-ui/src/components/site/docs-toc.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

import { DocsToc, type TocHeading } from "./docs-toc";

// #9872: the rail used to scan the rendered `article.prose-docs` for h2/h3 and read the result into state
// from an effect. It renders the page's COMPILED toc now, so these drive it the way the app does -- by
// handing it items -- and pin the properties the DOM scrape used to provide implicitly.

vi.mock("@tanstack/react-router", () => ({
useLocation: () => ({ pathname: "/docs/example" }),
useChildMatches: () => [],
}));

const items: TocHeading[] = [
{ id: "first", text: "First section", level: 2 },
{ id: "second", text: "Second section", level: 2 },
{ id: "nested", text: "Nested", level: 3 },
];

beforeEach(() => {
window.localStorage.clear();
vi.stubGlobal(
"IntersectionObserver",
class {
observe() {}
disconnect() {}
unobserve() {}
takeRecords() {
return [];
}
root = null;
rootMargin = "";
thresholds = [];
},
);
});

describe("DocsToc (#9872)", () => {
it("renders one link per compiled toc entry, anchored to its id", () => {
render(<DocsToc items={items} />);
expect(screen.getByRole("link", { name: "First section" }).getAttribute("href")).toBe("#first");
expect(screen.getByRole("link", { name: "Nested" }).getAttribute("href")).toBe("#nested");
});

it("renders a heading's inline markup instead of flattening it", () => {
// The concrete gain over the DOM scrape, which read `textContent`: 29 of this repo's docs headings
// carry inline code, and the rail used to show them as bare text.
render(
<DocsToc
items={[
{ id: "a", text: <code>wantedPaths</code>, level: 3 },
{ id: "b", text: "Plain", level: 2 },
]}
/>,
);
expect(screen.getByRole("link", { name: "wantedPaths" }).querySelector("code")).not.toBeNull();
});

it("indents depth-3 entries and leaves depth-2 flush", () => {
render(<DocsToc items={items} />);
expect(screen.getByRole("link", { name: "Nested" }).closest("li")?.className).toContain("pl-3");
expect(
screen.getByRole("link", { name: "First section" }).closest("li")?.className,
).not.toContain("pl-3");
});

it("renders nothing for a page with fewer than two headings — a one-item rail is noise", () => {
const { container } = render(<DocsToc items={[items[0]!]} />);
expect(container.firstChild).toBeNull();
expect(render(<DocsToc items={[]} />).container.firstChild).toBeNull();
});

it("marks the remembered section current on first render, without an effect", () => {
// Restored during render from localStorage rather than set from an effect, which is what lets
// react-hooks/set-state-in-effect go back to `error`.
window.localStorage.setItem("docs-toc:v2:/docs/example", "second");
render(<DocsToc items={items} />);
expect(screen.getByRole("link", { name: "Second section" }).getAttribute("aria-current")).toBe(
"location",
);
expect(
screen.getByRole("link", { name: "First section" }).getAttribute("aria-current"),
).toBeNull();
});

it("ignores a remembered section that is not on this page", () => {
window.localStorage.setItem("docs-toc:v2:/docs/example", "from-a-different-page");
render(<DocsToc items={items} />);
for (const item of items)
expect(
screen.getByRole("link", { name: String(item.text) }).getAttribute("aria-current"),
).toBeNull();
});

it("survives localStorage throwing (Safari private mode)", () => {
const getItem = vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
throw new Error("denied");
});
expect(() => render(<DocsToc items={items} />)).not.toThrow();
getItem.mockRestore();
});
});
Loading
Loading