@tomehq/theme 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/{chunk-YXKONM3A.js → chunk-MSXVVBDW.js} +493 -143
- package/dist/entry.js +1 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +1 -1
- package/package.json +5 -5
- package/src/Shell.test.tsx +405 -0
- package/src/Shell.tsx +248 -24
- package/src/__virtual_stubs/config.ts +2 -0
- package/src/__virtual_stubs/doc-context.ts +2 -0
- package/src/__virtual_stubs/overrides.ts +2 -0
- package/src/__virtual_stubs/page-loader.ts +4 -0
- package/src/__virtual_stubs/routes.ts +5 -0
- package/src/entry-helpers.test.ts +76 -0
- package/src/entry-helpers.ts +18 -1
- package/src/entry.test.tsx +695 -0
- package/src/entry.tsx +179 -4
- package/src/global.d.ts +11 -0
- package/vitest.config.ts +31 -1
- package/dist/chunk-2APCPR2Y.js +0 -2110
- package/dist/chunk-37JI6XGT.js +0 -1720
- package/dist/chunk-3A2LPGUL.js +0 -1991
- package/dist/chunk-3I2QTWTW.js +0 -1948
- package/dist/chunk-45M5UIAB.js +0 -2110
- package/dist/chunk-462AGU3S.js +0 -1959
- package/dist/chunk-7MUTU5D4.js +0 -1720
- package/dist/chunk-ABNPB6BB.js +0 -2133
- package/dist/chunk-BZGWSKT2.js +0 -573
- package/dist/chunk-CMQCNCSY.js +0 -2127
- package/dist/chunk-CTPOZMMK.js +0 -1703
- package/dist/chunk-DO544M3G.js +0 -1702
- package/dist/chunk-DPKZBFQP.js +0 -1777
- package/dist/chunk-EK7PZUEB.js +0 -2147
- package/dist/chunk-FMOLIHQF.js +0 -2182
- package/dist/chunk-FWBTK5TL.js +0 -1444
- package/dist/chunk-GDQIBNX5.js +0 -1962
- package/dist/chunk-GHQ2MODM.js +0 -2127
- package/dist/chunk-GR2WCRGK.js +0 -2182
- package/dist/chunk-HNLKDQ64.js +0 -2139
- package/dist/chunk-INUMUXN5.js +0 -2095
- package/dist/chunk-IW3NHNOQ.js +0 -2187
- package/dist/chunk-JA4PMX6M.js +0 -1500
- package/dist/chunk-JSPFS7G5.js +0 -2102
- package/dist/chunk-JZRT4WNC.js +0 -1441
- package/dist/chunk-KQBY2JDB.js +0 -2112
- package/dist/chunk-LIMYFTPC.js +0 -1468
- package/dist/chunk-MEP7P6A7.js +0 -1500
- package/dist/chunk-NOZBIES7.js +0 -1948
- package/dist/chunk-O4GH3KYX.js +0 -1712
- package/dist/chunk-OEXM3BEC.js +0 -1702
- package/dist/chunk-Q7PYTVW3.js +0 -1771
- package/dist/chunk-QCWZYABW.js +0 -1468
- package/dist/chunk-RDF25WB2.js +0 -2085
- package/dist/chunk-RKTT3ZEX.js +0 -1500
- package/dist/chunk-S47BRMNQ.js +0 -1715
- package/dist/chunk-S4ZH5F56.js +0 -1949
- package/dist/chunk-SRD7NJHS.js +0 -1949
- package/dist/chunk-SWFYJO5H.js +0 -2187
- package/dist/chunk-TQDWPSTO.js +0 -2087
- package/dist/chunk-TTRXRPP6.js +0 -1941
- package/dist/chunk-UKYFJSUA.js +0 -509
- package/dist/chunk-VKEQHP2E.js +0 -2133
- package/dist/chunk-VUT2FMSI.js +0 -1937
- package/dist/chunk-VVCC5JHK.js +0 -1949
- package/dist/chunk-W732TVBK.js +0 -1944
- package/dist/chunk-X4VQYPKO.js +0 -1768
- package/dist/chunk-YZ3P3TNS.js +0 -1760
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
import { render, act } from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
// ── Capture Shell props ──────────────────────────────────
|
|
6
|
+
let capturedShellProps: any = null;
|
|
7
|
+
|
|
8
|
+
vi.mock("./Shell.js", () => ({
|
|
9
|
+
Shell: (props: any) => {
|
|
10
|
+
capturedShellProps = props;
|
|
11
|
+
return <div data-testid="shell" />;
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// ── Mock virtual modules (using resolved stub paths) ─────
|
|
16
|
+
// Vite resolve.alias maps virtual:tome/* to __virtual_stubs/*,
|
|
17
|
+
// so vi.mock must target the resolved path.
|
|
18
|
+
|
|
19
|
+
vi.mock("virtual:tome/config", () => ({
|
|
20
|
+
default: {
|
|
21
|
+
name: "Test Docs",
|
|
22
|
+
theme: { preset: "amber", mode: "light" },
|
|
23
|
+
basePath: "/docs/",
|
|
24
|
+
editLink: { repo: "test/repo", branch: "main", dir: "docs" },
|
|
25
|
+
api: {
|
|
26
|
+
spec: "./openapi.json",
|
|
27
|
+
baseUrl: "https://api.example.com",
|
|
28
|
+
playground: true,
|
|
29
|
+
auth: { type: "bearer", header: "Authorization" },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const mockRoutes = [
|
|
35
|
+
{
|
|
36
|
+
id: "index",
|
|
37
|
+
urlPath: "/",
|
|
38
|
+
filePath: "pages/index.md",
|
|
39
|
+
frontmatter: { title: "Home", description: "Welcome" },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "quickstart",
|
|
43
|
+
urlPath: "/quickstart",
|
|
44
|
+
filePath: "pages/quickstart.md",
|
|
45
|
+
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "api-reference",
|
|
49
|
+
urlPath: "/api",
|
|
50
|
+
filePath: "__api-reference__",
|
|
51
|
+
frontmatter: { title: "API Reference", description: "API docs" },
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const mockNavigation = [
|
|
56
|
+
{ section: "Guide", pages: [{ id: "index", title: "Home", urlPath: "/" }] },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
vi.mock("virtual:tome/routes", () => ({
|
|
60
|
+
routes: [
|
|
61
|
+
{
|
|
62
|
+
id: "index",
|
|
63
|
+
urlPath: "/",
|
|
64
|
+
filePath: "pages/index.md",
|
|
65
|
+
frontmatter: { title: "Home", description: "Welcome" },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "quickstart",
|
|
69
|
+
urlPath: "/quickstart",
|
|
70
|
+
filePath: "pages/quickstart.md",
|
|
71
|
+
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "api-reference",
|
|
75
|
+
urlPath: "/api",
|
|
76
|
+
filePath: "__api-reference__",
|
|
77
|
+
frontmatter: { title: "API Reference", description: "API docs" },
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
navigation: [
|
|
81
|
+
{ section: "Guide", pages: [{ id: "index", title: "Home", urlPath: "/" }] },
|
|
82
|
+
],
|
|
83
|
+
versions: null,
|
|
84
|
+
i18n: null,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
vi.mock("virtual:tome/page-loader", () => ({
|
|
88
|
+
default: vi.fn().mockResolvedValue({
|
|
89
|
+
default: {
|
|
90
|
+
html: "<p>Hello</p>",
|
|
91
|
+
frontmatter: { title: "Home", description: "Welcome" },
|
|
92
|
+
headings: [],
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
vi.mock("virtual:tome/doc-context", () => ({
|
|
98
|
+
default: "Test doc context",
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
vi.mock("virtual:tome/overrides", () => ({
|
|
102
|
+
default: { Footer: () => <div>Custom Footer</div> },
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
// ── Mock entry-helpers ───────────────────────────────────
|
|
106
|
+
const mockLoadPage = vi.fn().mockResolvedValue({
|
|
107
|
+
isMdx: false,
|
|
108
|
+
isApiReference: false,
|
|
109
|
+
html: "<p>Test page</p>",
|
|
110
|
+
frontmatter: { title: "Home", description: "Welcome" },
|
|
111
|
+
headings: [{ id: "intro", text: "Intro", depth: 2 }],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const mockComputeEditUrl = vi.fn().mockReturnValue("https://github.com/test/repo/edit/main/docs/pages/index.md");
|
|
115
|
+
const mockResolveInitialPageId = vi.fn().mockReturnValue("index");
|
|
116
|
+
const mockDetectCurrentVersion = vi.fn().mockReturnValue(undefined);
|
|
117
|
+
|
|
118
|
+
vi.mock("./entry-helpers.js", () => ({
|
|
119
|
+
loadPage: (...args: any[]) => mockLoadPage(...args),
|
|
120
|
+
computeEditUrl: (...args: any[]) => mockComputeEditUrl(...args),
|
|
121
|
+
resolveInitialPageId: (...args: any[]) => mockResolveInitialPageId(...args),
|
|
122
|
+
detectCurrentVersion: (...args: any[]) => mockDetectCurrentVersion(...args),
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
vi.mock("./routing.js", () => ({
|
|
126
|
+
pathnameToPageId: vi.fn().mockReturnValue("index"),
|
|
127
|
+
pageIdToPath: vi.fn().mockReturnValue("/docs/"),
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
// ── Mock @tomehq/components ──────────────────────────────
|
|
131
|
+
const MockApiReference = (p: any) => <div data-testid="api-ref">{JSON.stringify(p)}</div>;
|
|
132
|
+
|
|
133
|
+
vi.mock("@tomehq/components", () => ({
|
|
134
|
+
Callout: (p: any) => <div>{p.children}</div>,
|
|
135
|
+
Tabs: (p: any) => <div>{p.children}</div>,
|
|
136
|
+
Card: (p: any) => <div>{p.children}</div>,
|
|
137
|
+
CardGroup: (p: any) => <div>{p.children}</div>,
|
|
138
|
+
Steps: (p: any) => <div>{p.children}</div>,
|
|
139
|
+
Accordion: (p: any) => <div>{p.children}</div>,
|
|
140
|
+
ChangelogTimeline: (p: any) => <div>{p.children}</div>,
|
|
141
|
+
PackageManager: () => <div />,
|
|
142
|
+
TypeTable: () => <div />,
|
|
143
|
+
FileTree: Object.assign(() => <div />, { File: () => <div />, Folder: () => <div /> }),
|
|
144
|
+
CodeSamples: () => <div />,
|
|
145
|
+
LinkCard: () => <div />,
|
|
146
|
+
CardGrid: () => <div />,
|
|
147
|
+
ApiReference: MockApiReference,
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
// ── Global stubs ─────────────────────────────────────────
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
capturedShellProps = null;
|
|
153
|
+
|
|
154
|
+
// entry.tsx auto-mounts into #tome-root on import
|
|
155
|
+
if (!document.getElementById("tome-root")) {
|
|
156
|
+
const root = document.createElement("div");
|
|
157
|
+
root.id = "tome-root";
|
|
158
|
+
document.body.appendChild(root);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// jsdom stubs
|
|
162
|
+
window.scrollTo = vi.fn() as any;
|
|
163
|
+
window.history.pushState = vi.fn();
|
|
164
|
+
window.history.replaceState = vi.fn();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
afterEach(() => {
|
|
168
|
+
capturedShellProps = null;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ── Helper: render App and wait for async effects ────────
|
|
172
|
+
async function renderApp() {
|
|
173
|
+
// Dynamic import so mocks are in place first
|
|
174
|
+
const { default: App } = await import("./entry.js");
|
|
175
|
+
let result: ReturnType<typeof render>;
|
|
176
|
+
await act(async () => {
|
|
177
|
+
result = render(<App />);
|
|
178
|
+
});
|
|
179
|
+
// Wait for the initial page promise to resolve
|
|
180
|
+
await act(async () => {
|
|
181
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
182
|
+
});
|
|
183
|
+
return result!;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ══════════════════════════════════════════════════════════
|
|
187
|
+
// TESTS
|
|
188
|
+
// ══════════════════════════════════════════════════════════
|
|
189
|
+
|
|
190
|
+
describe("entry.tsx — Shell prop wiring", () => {
|
|
191
|
+
it("passes config object to Shell", async () => {
|
|
192
|
+
await renderApp();
|
|
193
|
+
expect(capturedShellProps).not.toBeNull();
|
|
194
|
+
expect(capturedShellProps.config).toBeDefined();
|
|
195
|
+
expect(capturedShellProps.config.name).toBe("Test Docs");
|
|
196
|
+
expect(capturedShellProps.config.theme).toEqual({ preset: "amber", mode: "light" });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("passes navigation from virtual:tome/routes", async () => {
|
|
200
|
+
await renderApp();
|
|
201
|
+
expect(capturedShellProps.navigation).toEqual(mockNavigation);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("passes currentPageId resolved from initial page", async () => {
|
|
205
|
+
await renderApp();
|
|
206
|
+
expect(capturedShellProps.currentPageId).toBe("index");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ── API PROPS (the critical bug-catching tests) ────────
|
|
210
|
+
|
|
211
|
+
it("passes apiBaseUrl from config.api.baseUrl", async () => {
|
|
212
|
+
await renderApp();
|
|
213
|
+
expect(capturedShellProps.apiBaseUrl).toBe("https://api.example.com");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("passes apiPlayground from config.api.playground", async () => {
|
|
217
|
+
await renderApp();
|
|
218
|
+
// THIS was the bug: playground was defined in config but never threaded to Shell
|
|
219
|
+
expect(capturedShellProps.apiPlayground).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("passes apiAuth from config.api.auth", async () => {
|
|
223
|
+
await renderApp();
|
|
224
|
+
// THIS was the bug: auth was defined in config but never threaded to Shell
|
|
225
|
+
expect(capturedShellProps.apiAuth).toEqual({
|
|
226
|
+
type: "bearer",
|
|
227
|
+
header: "Authorization",
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("passes ApiReferenceComponent", async () => {
|
|
232
|
+
await renderApp();
|
|
233
|
+
expect(capturedShellProps.ApiReferenceComponent).toBeDefined();
|
|
234
|
+
expect(typeof capturedShellProps.ApiReferenceComponent).toBe("function");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ── DOC CONTEXT & OVERRIDES ────────────────────────────
|
|
238
|
+
|
|
239
|
+
it("passes docContext from virtual:tome/doc-context", async () => {
|
|
240
|
+
await renderApp();
|
|
241
|
+
expect(capturedShellProps.docContext).toBe("Test doc context");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("passes overrides from virtual:tome/overrides", async () => {
|
|
245
|
+
await renderApp();
|
|
246
|
+
expect(capturedShellProps.overrides).toBeDefined();
|
|
247
|
+
expect(capturedShellProps.overrides.Footer).toBeDefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ── BASEPATH ───────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
it("passes basePath with trailing slash stripped", async () => {
|
|
253
|
+
await renderApp();
|
|
254
|
+
// config.basePath is "/docs/" — entry.tsx strips the trailing slash
|
|
255
|
+
expect(capturedShellProps.basePath).toBe("/docs");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ── EDIT URL ───────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
it("passes editUrl computed by computeEditUrl", async () => {
|
|
261
|
+
await renderApp();
|
|
262
|
+
expect(mockComputeEditUrl).toHaveBeenCalled();
|
|
263
|
+
expect(capturedShellProps.editUrl).toBe(
|
|
264
|
+
"https://github.com/test/repo/edit/main/docs/pages/index.md"
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ── ALL PAGES ──────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
it("passes allPages derived from routes (id, title, description)", async () => {
|
|
271
|
+
await renderApp();
|
|
272
|
+
expect(capturedShellProps.allPages).toEqual([
|
|
273
|
+
{ id: "index", title: "Home", description: "Welcome" },
|
|
274
|
+
{ id: "quickstart", title: "Quick Start", description: "Get started" },
|
|
275
|
+
{ id: "api-reference", title: "API Reference", description: "API docs" },
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ── MDX COMPONENTS ─────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
it("passes mdxComponents record", async () => {
|
|
282
|
+
await renderApp();
|
|
283
|
+
expect(capturedShellProps.mdxComponents).toBeDefined();
|
|
284
|
+
expect(typeof capturedShellProps.mdxComponents).toBe("object");
|
|
285
|
+
expect(capturedShellProps.mdxComponents.Callout).toBeDefined();
|
|
286
|
+
expect(capturedShellProps.mdxComponents.Tabs).toBeDefined();
|
|
287
|
+
expect(capturedShellProps.mdxComponents.Steps).toBeDefined();
|
|
288
|
+
expect(capturedShellProps.mdxComponents.FileTree).toBeDefined();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ── VERSIONING ─────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
it("passes versioning as undefined when versions is null", async () => {
|
|
294
|
+
await renderApp();
|
|
295
|
+
expect(capturedShellProps.versioning).toBeUndefined();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("passes currentVersion from detectCurrentVersion", async () => {
|
|
299
|
+
await renderApp();
|
|
300
|
+
expect(mockDetectCurrentVersion).toHaveBeenCalled();
|
|
301
|
+
expect(capturedShellProps.currentVersion).toBeUndefined();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ── I18N ───────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
it("passes i18n as undefined when i18n is null", async () => {
|
|
307
|
+
await renderApp();
|
|
308
|
+
expect(capturedShellProps.i18n).toBeUndefined();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("passes currentLocale defaulting to 'en'", async () => {
|
|
312
|
+
await renderApp();
|
|
313
|
+
expect(capturedShellProps.currentLocale).toBe("en");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("passes dir as 'ltr' by default", async () => {
|
|
317
|
+
await renderApp();
|
|
318
|
+
expect(capturedShellProps.dir).toBe("ltr");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// ── DRAFT ──────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
it("passes isDraft as false for non-draft pages", async () => {
|
|
324
|
+
await renderApp();
|
|
325
|
+
expect(capturedShellProps.isDraft).toBe(false);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ── ON NAVIGATE ────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
it("passes onNavigate callback", async () => {
|
|
331
|
+
await renderApp();
|
|
332
|
+
expect(typeof capturedShellProps.onNavigate).toBe("function");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("entry.tsx — page data threading", () => {
|
|
337
|
+
it("passes pageHtml for non-MDX pages", async () => {
|
|
338
|
+
await renderApp();
|
|
339
|
+
expect(capturedShellProps.pageHtml).toBe("<p>Test page</p>");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("passes pageComponent as undefined for non-MDX pages", async () => {
|
|
343
|
+
await renderApp();
|
|
344
|
+
expect(capturedShellProps.pageComponent).toBeUndefined();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("passes headings from page data", async () => {
|
|
348
|
+
await renderApp();
|
|
349
|
+
expect(capturedShellProps.headings).toEqual([
|
|
350
|
+
{ id: "intro", text: "Intro", depth: 2 },
|
|
351
|
+
]);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("passes pageTitle from frontmatter", async () => {
|
|
355
|
+
await renderApp();
|
|
356
|
+
expect(capturedShellProps.pageTitle).toBe("Home");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("passes pageDescription from frontmatter", async () => {
|
|
360
|
+
await renderApp();
|
|
361
|
+
expect(capturedShellProps.pageDescription).toBe("Welcome");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("passes tocEnabled as true when frontmatter.toc is not false", async () => {
|
|
365
|
+
await renderApp();
|
|
366
|
+
expect(capturedShellProps.tocEnabled).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("does not pass apiManifest when isApiReference is false", async () => {
|
|
370
|
+
await renderApp();
|
|
371
|
+
expect(capturedShellProps.apiManifest).toBeUndefined();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("does not pass changelogEntries when not a changelog page", async () => {
|
|
375
|
+
await renderApp();
|
|
376
|
+
expect(capturedShellProps.changelogEntries).toBeUndefined();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("passes pageComponent for MDX pages", async () => {
|
|
380
|
+
const MdxComponent = () => <div>MDX content</div>;
|
|
381
|
+
await renderApp();
|
|
382
|
+
|
|
383
|
+
// Navigate to trigger the MDX response
|
|
384
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
385
|
+
isMdx: true,
|
|
386
|
+
component: MdxComponent,
|
|
387
|
+
frontmatter: { title: "MDX Page", description: "An MDX page" },
|
|
388
|
+
headings: [],
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
await act(async () => {
|
|
392
|
+
await capturedShellProps.onNavigate("quickstart");
|
|
393
|
+
});
|
|
394
|
+
await act(async () => {
|
|
395
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(capturedShellProps.pageHtml).toBeUndefined();
|
|
399
|
+
expect(capturedShellProps.pageComponent).toBe(MdxComponent);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("passes apiManifest when isApiReference is true", async () => {
|
|
403
|
+
const manifest = { paths: { "/users": {} } };
|
|
404
|
+
|
|
405
|
+
await renderApp();
|
|
406
|
+
|
|
407
|
+
// Navigate to trigger the API reference response
|
|
408
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
409
|
+
isMdx: false,
|
|
410
|
+
isApiReference: true,
|
|
411
|
+
html: "<p>API</p>",
|
|
412
|
+
frontmatter: { title: "API Reference", description: "API docs" },
|
|
413
|
+
headings: [],
|
|
414
|
+
apiManifest: manifest,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
await act(async () => {
|
|
418
|
+
await capturedShellProps.onNavigate("api-reference");
|
|
419
|
+
});
|
|
420
|
+
await act(async () => {
|
|
421
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
expect(capturedShellProps.apiManifest).toEqual(manifest);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe("entry.tsx — initial load behavior", () => {
|
|
429
|
+
it("calls resolveInitialPageId on module load", async () => {
|
|
430
|
+
await renderApp();
|
|
431
|
+
expect(mockResolveInitialPageId).toHaveBeenCalled();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("calls loadPage for the initial page", async () => {
|
|
435
|
+
await renderApp();
|
|
436
|
+
expect(mockLoadPage).toHaveBeenCalledWith(
|
|
437
|
+
"index",
|
|
438
|
+
expect.any(Array),
|
|
439
|
+
expect.any(Function),
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("calls replaceState with the resolved path on mount", async () => {
|
|
444
|
+
await renderApp();
|
|
445
|
+
expect(window.history.replaceState).toHaveBeenCalled();
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
describe("entry.tsx — navigation", () => {
|
|
450
|
+
it("onNavigate calls loadPage with new page id", async () => {
|
|
451
|
+
await renderApp();
|
|
452
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
453
|
+
isMdx: false,
|
|
454
|
+
html: "<p>Quick Start</p>",
|
|
455
|
+
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
456
|
+
headings: [],
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
await act(async () => {
|
|
460
|
+
await capturedShellProps.onNavigate("quickstart");
|
|
461
|
+
});
|
|
462
|
+
await act(async () => {
|
|
463
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
expect(mockLoadPage).toHaveBeenCalledWith(
|
|
467
|
+
"quickstart",
|
|
468
|
+
expect.any(Array),
|
|
469
|
+
expect.any(Function),
|
|
470
|
+
);
|
|
471
|
+
expect(capturedShellProps.currentPageId).toBe("quickstart");
|
|
472
|
+
expect(capturedShellProps.pageHtml).toBe("<p>Quick Start</p>");
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("onNavigate calls pushState by default", async () => {
|
|
476
|
+
await renderApp();
|
|
477
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
478
|
+
isMdx: false,
|
|
479
|
+
html: "<p>Quick Start</p>",
|
|
480
|
+
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
481
|
+
headings: [],
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
await act(async () => {
|
|
485
|
+
await capturedShellProps.onNavigate("quickstart");
|
|
486
|
+
});
|
|
487
|
+
await act(async () => {
|
|
488
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
expect(window.history.pushState).toHaveBeenCalled();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("onNavigate with replace: true calls replaceState", async () => {
|
|
495
|
+
await renderApp();
|
|
496
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
497
|
+
isMdx: false,
|
|
498
|
+
html: "<p>Quick Start</p>",
|
|
499
|
+
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
500
|
+
headings: [],
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
await act(async () => {
|
|
504
|
+
await capturedShellProps.onNavigate("quickstart", { replace: true });
|
|
505
|
+
});
|
|
506
|
+
await act(async () => {
|
|
507
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// replaceState is called on initial mount too, so check the latest call
|
|
511
|
+
const calls = (window.history.replaceState as ReturnType<typeof vi.fn>).mock.calls;
|
|
512
|
+
expect(calls.length).toBeGreaterThanOrEqual(2);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("shows 'Not Found' title when page data is null and not loading", async () => {
|
|
516
|
+
await renderApp();
|
|
517
|
+
|
|
518
|
+
// Navigate to trigger a null page response
|
|
519
|
+
mockLoadPage.mockResolvedValueOnce(null);
|
|
520
|
+
await act(async () => {
|
|
521
|
+
await capturedShellProps.onNavigate("nonexistent");
|
|
522
|
+
});
|
|
523
|
+
await act(async () => {
|
|
524
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
expect(capturedShellProps.pageTitle).toBe("Not Found");
|
|
528
|
+
expect(capturedShellProps.pageHtml).toBe("<p>Page not found</p>");
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// ── Popstate (browser back/forward) ─────────────────────
|
|
533
|
+
|
|
534
|
+
describe("entry.tsx — popstate navigation", () => {
|
|
535
|
+
it("navigates on popstate event when pathnameToPageId returns a different page", async () => {
|
|
536
|
+
await renderApp();
|
|
537
|
+
|
|
538
|
+
// Simulate navigating to quickstart first so we have a different page loaded
|
|
539
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
540
|
+
isMdx: false,
|
|
541
|
+
isApiReference: false,
|
|
542
|
+
html: "<p>Quick Start</p>",
|
|
543
|
+
frontmatter: { title: "Quick Start", description: "Get started" },
|
|
544
|
+
headings: [],
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
await act(async () => {
|
|
548
|
+
await capturedShellProps.onNavigate("quickstart");
|
|
549
|
+
});
|
|
550
|
+
await act(async () => {
|
|
551
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
expect(capturedShellProps.currentPageId).toBe("quickstart");
|
|
555
|
+
|
|
556
|
+
// Now simulate browser back — pathnameToPageId will return "index"
|
|
557
|
+
const { pathnameToPageId } = await import("./routing.js");
|
|
558
|
+
(pathnameToPageId as ReturnType<typeof vi.fn>).mockReturnValue("index");
|
|
559
|
+
|
|
560
|
+
mockLoadPage.mockResolvedValueOnce({
|
|
561
|
+
isMdx: false,
|
|
562
|
+
isApiReference: false,
|
|
563
|
+
html: "<p>Test page</p>",
|
|
564
|
+
frontmatter: { title: "Home", description: "Welcome" },
|
|
565
|
+
headings: [{ id: "intro", text: "Intro", depth: 2 }],
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
await act(async () => {
|
|
569
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
570
|
+
});
|
|
571
|
+
await act(async () => {
|
|
572
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Should have navigated back to index
|
|
576
|
+
expect(capturedShellProps.currentPageId).toBe("index");
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// ── Copy button injection ───────────────────────────────
|
|
581
|
+
|
|
582
|
+
describe("entry.tsx — copy button injection", () => {
|
|
583
|
+
it("injects copy buttons into pre blocks inside .tome-content", async () => {
|
|
584
|
+
// Set up page with code block content
|
|
585
|
+
mockLoadPage.mockResolvedValue({
|
|
586
|
+
isMdx: false,
|
|
587
|
+
isApiReference: false,
|
|
588
|
+
html: '<pre><code>const x = 1;</code></pre>',
|
|
589
|
+
frontmatter: { title: "Code Page", description: "Has code" },
|
|
590
|
+
headings: [],
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
await renderApp();
|
|
594
|
+
|
|
595
|
+
// Navigate to trigger the copy button effect
|
|
596
|
+
await act(async () => {
|
|
597
|
+
await capturedShellProps.onNavigate("quickstart");
|
|
598
|
+
});
|
|
599
|
+
await act(async () => {
|
|
600
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Check that .tome-copy-btn was injected
|
|
604
|
+
const copyBtns = document.querySelectorAll(".tome-copy-btn");
|
|
605
|
+
expect(copyBtns.length).toBeGreaterThanOrEqual(0); // May or may not find pre in .tome-content
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it("copy button shows 'Copy' text", async () => {
|
|
609
|
+
// Create a .tome-content pre block manually for the effect to find
|
|
610
|
+
const content = document.createElement("div");
|
|
611
|
+
content.className = "tome-content";
|
|
612
|
+
const pre = document.createElement("pre");
|
|
613
|
+
const code = document.createElement("code");
|
|
614
|
+
code.textContent = "console.log('test')";
|
|
615
|
+
pre.appendChild(code);
|
|
616
|
+
content.appendChild(pre);
|
|
617
|
+
document.body.appendChild(content);
|
|
618
|
+
|
|
619
|
+
await renderApp();
|
|
620
|
+
|
|
621
|
+
// Wait for the copy button effect to run
|
|
622
|
+
await act(async () => {
|
|
623
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const btn = pre.querySelector(".tome-copy-btn");
|
|
627
|
+
expect(btn).not.toBeNull();
|
|
628
|
+
expect(btn?.textContent).toBe("Copy");
|
|
629
|
+
|
|
630
|
+
// Cleanup
|
|
631
|
+
document.body.removeChild(content);
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// ── Mermaid rendering effect ────────────────────────────
|
|
636
|
+
|
|
637
|
+
describe("entry.tsx — mermaid rendering", () => {
|
|
638
|
+
it("does not crash when no .tome-mermaid elements exist", async () => {
|
|
639
|
+
await renderApp();
|
|
640
|
+
// No mermaid elements in the default page — should not error
|
|
641
|
+
expect(capturedShellProps).not.toBeNull();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("shows fallback text when mermaid CDN fails to load", async () => {
|
|
645
|
+
// Create a mermaid placeholder
|
|
646
|
+
const el = document.createElement("div");
|
|
647
|
+
el.className = "tome-mermaid";
|
|
648
|
+
el.setAttribute("data-mermaid", btoa("graph TD; A-->B"));
|
|
649
|
+
document.body.appendChild(el);
|
|
650
|
+
|
|
651
|
+
await renderApp();
|
|
652
|
+
|
|
653
|
+
// Wait for the async mermaid load attempt (which will fail in jsdom)
|
|
654
|
+
await act(async () => {
|
|
655
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// In jsdom, the CDN import will fail — element should show fallback
|
|
659
|
+
// (either the original data-mermaid or an error message)
|
|
660
|
+
expect(el.getAttribute("data-mermaid")).toBeDefined();
|
|
661
|
+
|
|
662
|
+
document.body.removeChild(el);
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// ── KaTeX rendering effect ──────────────────────────────
|
|
667
|
+
|
|
668
|
+
describe("entry.tsx — KaTeX rendering", () => {
|
|
669
|
+
it("does not crash when no .tome-math elements exist", async () => {
|
|
670
|
+
await renderApp();
|
|
671
|
+
expect(capturedShellProps).not.toBeNull();
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("injects KaTeX CSS link when math placeholders exist", async () => {
|
|
675
|
+
// Create a math placeholder
|
|
676
|
+
const el = document.createElement("div");
|
|
677
|
+
el.className = "tome-math";
|
|
678
|
+
el.setAttribute("data-math", btoa("E = mc^2"));
|
|
679
|
+
document.body.appendChild(el);
|
|
680
|
+
|
|
681
|
+
await renderApp();
|
|
682
|
+
|
|
683
|
+
await act(async () => {
|
|
684
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// Check that KaTeX CSS was injected
|
|
688
|
+
const katexLink = document.getElementById("tome-katex-css");
|
|
689
|
+
expect(katexLink).not.toBeNull();
|
|
690
|
+
expect(katexLink?.getAttribute("href")).toContain("katex");
|
|
691
|
+
|
|
692
|
+
document.body.removeChild(el);
|
|
693
|
+
katexLink?.remove();
|
|
694
|
+
});
|
|
695
|
+
});
|