@hutusi/amytis 1.15.0 → 1.17.0
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/.claude/rules/immersive-reading.md +21 -0
- package/.claude/rules/rst.md +13 -0
- package/CHANGELOG.md +42 -0
- package/CLAUDE.md +89 -219
- package/bun.lock +185 -547
- package/content/books/sample-book/index.mdx +3 -0
- package/content/posts/code-block-features-showcase.mdx +223 -0
- package/docs/ALERTS.md +112 -0
- package/docs/ARCHITECTURE.md +298 -5
- package/docs/CODE-BLOCKS.md +238 -0
- package/docs/CONTRIBUTING.md +25 -0
- package/docs/DIGITAL_GARDEN.md +1 -1
- package/docs/guides/README.md +11 -0
- package/docs/guides/importing-vuepress-books.md +237 -0
- package/eslint.config.mjs +18 -6
- package/package.json +42 -20
- package/scripts/generate-code-group-icons.ts +79 -0
- package/scripts/render-rst.py +207 -3
- package/scripts/sync-vuepress-book.ts +710 -0
- package/site.config.example.ts +3 -3
- package/site.config.ts +3 -3
- package/src/app/[slug]/layout.tsx +30 -0
- package/src/app/books/[slug]/{[chapter] → [...chapter]}/page.tsx +32 -10
- package/src/app/books/[slug]/layout.tsx +24 -0
- package/src/app/books/[slug]/page.tsx +85 -34
- package/src/app/globals.css +570 -123
- package/src/app/page.tsx +7 -1
- package/src/app/posts/layout.tsx +20 -0
- package/src/app/series/[slug]/page.tsx +33 -9
- package/src/app/sitemap.ts +3 -3
- package/src/components/ArticleCopyCleaner.tsx +64 -0
- package/src/components/BookMobileNav.tsx +44 -50
- package/src/components/BookReadingShell.tsx +145 -0
- package/src/components/BookSidebar.tsx +0 -0
- package/src/components/CodeBlock.test.tsx +93 -8
- package/src/components/CodeBlock.tsx +39 -101
- package/src/components/CodeBlockToolbar.tsx +88 -0
- package/src/components/CodeGroup.tsx +81 -0
- package/src/components/CoverImage.tsx +1 -0
- package/src/components/CuratedSeriesSection.tsx +28 -10
- package/src/components/ExternalLinkIcon.tsx +15 -0
- package/src/components/FeaturedStoriesSection.tsx +44 -23
- package/src/components/Footer.tsx +1 -1
- package/src/components/GithubAlert.tsx +97 -0
- package/src/components/ImmersiveReader.tsx +130 -0
- package/src/components/ImmersiveReaderTopBar.tsx +106 -0
- package/src/components/ImmersiveReadingFlagHandler.tsx +40 -0
- package/src/components/ImmersiveReadingPrefsPopover.tsx +249 -0
- package/src/components/ImmersiveReadingProvider.tsx +168 -0
- package/src/components/ImmersiveSeriesSidebar.tsx +143 -0
- package/src/components/ImmersiveToggleButton.tsx +45 -0
- package/src/components/MarkdownRenderer.test.tsx +14 -4
- package/src/components/MarkdownRenderer.tsx +175 -23
- package/src/components/Mermaid.tsx +32 -1
- package/src/components/Navbar.tsx +3 -1
- package/src/components/PostList.tsx +1 -1
- package/src/components/PostNavigation.tsx +13 -2
- package/src/components/PostReadingShell.tsx +68 -0
- package/src/components/PostSidebar.tsx +13 -2
- package/src/components/ReadingProgressBar.tsx +1 -1
- package/src/components/RstRenderer.test.tsx +15 -15
- package/src/components/RstRenderer.tsx +37 -2
- package/src/components/Search.tsx +18 -4
- package/src/components/SelectedBooksSection.tsx +27 -8
- package/src/components/SeriesCatalog.tsx +1 -1
- package/src/components/ShareBar.tsx +5 -0
- package/src/components/TocPanel.tsx +10 -2
- package/src/hooks/useActiveHeading.ts +35 -13
- package/src/hooks/useSidebarAutoScroll.ts +31 -7
- package/src/i18n/translations.ts +44 -0
- package/src/layouts/BookLayout.tsx +62 -74
- package/src/layouts/PostLayout.tsx +154 -111
- package/src/lib/code-group-icons.test.ts +78 -0
- package/src/lib/code-group-icons.ts +148 -0
- package/src/lib/immersive-reading-prefs.ts +104 -0
- package/src/lib/markdown.test.ts +56 -13
- package/src/lib/markdown.ts +217 -57
- package/src/lib/normalize-vuepress-math.ts +118 -0
- package/src/lib/rehype-fence-meta.ts +22 -0
- package/src/lib/remark-book-chapter-links.ts +106 -0
- package/src/lib/remark-code-group.ts +54 -0
- package/src/lib/remark-github-alerts.test.ts +83 -0
- package/src/lib/remark-github-alerts.ts +65 -0
- package/src/lib/remark-vuepress-containers.ts +130 -0
- package/src/lib/rst-renderer.ts +19 -7
- package/src/lib/rst.test.ts +212 -2
- package/src/lib/rst.ts +217 -13
- package/src/lib/scroll-utils.ts +44 -6
- package/src/lib/shiki-rst.ts +185 -0
- package/src/lib/shiki.test.ts +153 -0
- package/src/lib/shiki.ts +292 -0
- package/src/lib/shuffle.ts +15 -1
- package/src/lib/sort.ts +15 -0
- package/src/lib/urls.ts +62 -0
- package/src/test-utils/render.ts +23 -0
- package/tests/fixtures/sync-vuepress-book/docs/.vuepress/config.js +43 -0
- package/tests/fixtures/sync-vuepress-book/docs/intro/welcome.md +7 -0
- package/tests/fixtures/sync-vuepress-book/docs/maths/linear/assets/diagram.png +1 -0
- package/tests/fixtures/sync-vuepress-book/docs/maths/linear/matrices.md +7 -0
- package/tests/fixtures/sync-vuepress-book/docs/maths/linear/vectors.md +9 -0
- package/tests/helpers/env.ts +19 -0
- package/tests/integration/book-chapter-links.test.ts +107 -0
- package/tests/integration/book-index-cta.test.ts +87 -0
- package/tests/integration/books-nested-toc.test.ts +176 -0
- package/tests/integration/books.test.ts +3 -2
- package/tests/integration/code-block-features.test.ts +188 -0
- package/tests/integration/code-group.test.ts +183 -0
- package/tests/integration/code-notation.test.ts +97 -0
- package/tests/integration/github-alerts.test.ts +82 -0
- package/tests/integration/markdown-external-links.test.ts +103 -0
- package/tests/integration/normalize-vuepress-math.test.ts +149 -0
- package/tests/integration/reading-time-headings.test.ts +8 -6
- package/tests/integration/series-draft.test.ts +6 -13
- package/tests/integration/series-index-cta.test.ts +88 -0
- package/tests/integration/sync-vuepress-book.test.ts +443 -0
- package/tests/integration/vuepress-containers.test.ts +107 -0
- package/tests/tooling/new-post.test.ts +1 -1
- package/tests/unit/immersive-reading-prefs.test.ts +144 -0
- package/tests/unit/static-params.test.ts +32 -19
- package/vercel.json +7 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_PREFS,
|
|
4
|
+
STORAGE_KEY,
|
|
5
|
+
readStoredPrefs,
|
|
6
|
+
writeStoredPrefs,
|
|
7
|
+
type StoredPrefs,
|
|
8
|
+
} from '../../src/lib/immersive-reading-prefs';
|
|
9
|
+
|
|
10
|
+
// Minimal in-memory mock matching the Storage interface subsets the helpers
|
|
11
|
+
// accept. Per-test instance keeps state isolated and avoids touching
|
|
12
|
+
// globalThis.localStorage. Optional setItem override lets us simulate
|
|
13
|
+
// private-browsing / quota-exceeded throws.
|
|
14
|
+
function makeMockStorage(
|
|
15
|
+
initial?: Record<string, string>,
|
|
16
|
+
opts: { setItemThrows?: boolean } = {},
|
|
17
|
+
) {
|
|
18
|
+
const store: Record<string, string> = { ...(initial ?? {}) };
|
|
19
|
+
return {
|
|
20
|
+
getItem(key: string): string | null {
|
|
21
|
+
return Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null;
|
|
22
|
+
},
|
|
23
|
+
setItem(key: string, value: string): void {
|
|
24
|
+
if (opts.setItemThrows) throw new Error('quota exceeded');
|
|
25
|
+
store[key] = value;
|
|
26
|
+
},
|
|
27
|
+
_store: store,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('readStoredPrefs', () => {
|
|
32
|
+
test('returns defaults when storage is empty', () => {
|
|
33
|
+
expect(readStoredPrefs(makeMockStorage())).toEqual(DEFAULT_PREFS);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('returns defaults when no storage is available', () => {
|
|
37
|
+
// Passing a storage whose getItem always returns null mirrors the
|
|
38
|
+
// "globalThis.localStorage missing" path. Production callers can also
|
|
39
|
+
// pass undefined and rely on the lazy default — that path is exercised
|
|
40
|
+
// by the provider in the browser, not by unit tests.
|
|
41
|
+
const empty = { getItem: () => null };
|
|
42
|
+
expect(readStoredPrefs(empty)).toEqual(DEFAULT_PREFS);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('returns defaults when stored JSON is invalid', () => {
|
|
46
|
+
expect(readStoredPrefs(makeMockStorage({ [STORAGE_KEY]: 'not-json{' }))).toEqual(DEFAULT_PREFS);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('returns defaults when stored value is not an object', () => {
|
|
50
|
+
expect(readStoredPrefs(makeMockStorage({ [STORAGE_KEY]: '"a string"' }))).toEqual(DEFAULT_PREFS);
|
|
51
|
+
expect(readStoredPrefs(makeMockStorage({ [STORAGE_KEY]: 'null' }))).toEqual(DEFAULT_PREFS);
|
|
52
|
+
expect(readStoredPrefs(makeMockStorage({ [STORAGE_KEY]: '42' }))).toEqual(DEFAULT_PREFS);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('returns defaults when storage entry is missing for the key', () => {
|
|
56
|
+
expect(readStoredPrefs(makeMockStorage({ 'unrelated-key': 'whatever' }))).toEqual(DEFAULT_PREFS);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('round-trips a fully valid prefs blob', () => {
|
|
60
|
+
const blob: StoredPrefs = {
|
|
61
|
+
fontSize: 'xl',
|
|
62
|
+
readingTheme: 'sepia',
|
|
63
|
+
columnWidth: 'narrow',
|
|
64
|
+
sidebarOpen: false,
|
|
65
|
+
};
|
|
66
|
+
expect(readStoredPrefs(makeMockStorage({ [STORAGE_KEY]: JSON.stringify(blob) }))).toEqual(blob);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// The schema-drift case the helpers exist for: one corrupt key must not
|
|
70
|
+
// discard the whole blob — other valid keys still apply, the bad one
|
|
71
|
+
// falls back to its default.
|
|
72
|
+
test('per-key fallback: bad fontSize, others survive', () => {
|
|
73
|
+
const stored = JSON.stringify({
|
|
74
|
+
fontSize: 'banana',
|
|
75
|
+
readingTheme: 'dark',
|
|
76
|
+
columnWidth: 'full',
|
|
77
|
+
sidebarOpen: false,
|
|
78
|
+
});
|
|
79
|
+
expect(readStoredPrefs(makeMockStorage({ [STORAGE_KEY]: stored }))).toEqual({
|
|
80
|
+
fontSize: DEFAULT_PREFS.fontSize, // fell back
|
|
81
|
+
readingTheme: 'dark',
|
|
82
|
+
columnWidth: 'full',
|
|
83
|
+
sidebarOpen: false,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('per-key fallback: bad readingTheme + columnWidth, others survive', () => {
|
|
88
|
+
const stored = JSON.stringify({
|
|
89
|
+
fontSize: 's',
|
|
90
|
+
readingTheme: 'neon',
|
|
91
|
+
columnWidth: 'ultra-wide',
|
|
92
|
+
sidebarOpen: true,
|
|
93
|
+
});
|
|
94
|
+
expect(readStoredPrefs(makeMockStorage({ [STORAGE_KEY]: stored }))).toEqual({
|
|
95
|
+
fontSize: 's',
|
|
96
|
+
readingTheme: DEFAULT_PREFS.readingTheme,
|
|
97
|
+
columnWidth: DEFAULT_PREFS.columnWidth,
|
|
98
|
+
sidebarOpen: true,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('sidebarOpen is strict-boolean — string "true" does not count', () => {
|
|
103
|
+
const stored = JSON.stringify({
|
|
104
|
+
fontSize: 'm',
|
|
105
|
+
readingTheme: 'auto',
|
|
106
|
+
columnWidth: 'wide',
|
|
107
|
+
sidebarOpen: 'true',
|
|
108
|
+
});
|
|
109
|
+
expect(readStoredPrefs(makeMockStorage({ [STORAGE_KEY]: stored })).sidebarOpen).toBe(
|
|
110
|
+
DEFAULT_PREFS.sidebarOpen,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('returns defaults when all keys are missing from a valid object', () => {
|
|
115
|
+
expect(readStoredPrefs(makeMockStorage({ [STORAGE_KEY]: '{}' }))).toEqual(DEFAULT_PREFS);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('writeStoredPrefs', () => {
|
|
120
|
+
test('writes the full blob under STORAGE_KEY as JSON', () => {
|
|
121
|
+
const storage = makeMockStorage();
|
|
122
|
+
const blob: StoredPrefs = {
|
|
123
|
+
fontSize: 'l',
|
|
124
|
+
readingTheme: 'dark',
|
|
125
|
+
columnWidth: 'medium',
|
|
126
|
+
sidebarOpen: false,
|
|
127
|
+
};
|
|
128
|
+
writeStoredPrefs(blob, storage);
|
|
129
|
+
expect(JSON.parse(storage._store[STORAGE_KEY])).toEqual(blob);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('swallows throws (private browsing / quota exceeded)', () => {
|
|
133
|
+
const storage = makeMockStorage(undefined, { setItemThrows: true });
|
|
134
|
+
// Must not crash — the production caller relies on this silence so the
|
|
135
|
+
// reader still works in private browsing.
|
|
136
|
+
expect(() => writeStoredPrefs(DEFAULT_PREFS, storage)).not.toThrow();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('does nothing when no storage is available', () => {
|
|
140
|
+
// Same as above but exercising the no-storage path. The "no storage"
|
|
141
|
+
// call site in production happens during SSR.
|
|
142
|
+
expect(() => writeStoredPrefs(DEFAULT_PREFS, undefined)).not.toThrow();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* • afterAll restores the real module so any subsequent tests see real data.
|
|
22
22
|
*/
|
|
23
23
|
import { describe, test, expect, mock, beforeAll, beforeEach, afterAll, afterEach } from 'bun:test';
|
|
24
|
+
import { setEnvVar, restoreEnvVar } from '../helpers/env';
|
|
24
25
|
|
|
25
26
|
// ─── Capture real modules ─────────────────────────────────────────────────────
|
|
26
27
|
// Static imports are hoisted and resolved before any executable code (including
|
|
@@ -36,7 +37,18 @@ import * as realUrls from '../../src/lib/urls';
|
|
|
36
37
|
// reference, but they are never mutated during tests so this is safe.
|
|
37
38
|
const snapshotUrls = { ...realUrls };
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
// Mock-post shape: only `slug` is required; the named fields are the ones
|
|
41
|
+
// production code paths read. Extra fields (full Post shape) are allowed via
|
|
42
|
+
// the index signature so individual tests can pass realistic fixtures without
|
|
43
|
+
// every property having to be enumerated here.
|
|
44
|
+
let mockedPosts: Array<{
|
|
45
|
+
slug: string;
|
|
46
|
+
series?: string;
|
|
47
|
+
redirectFrom?: string[];
|
|
48
|
+
draft?: boolean;
|
|
49
|
+
title?: string;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}> = [];
|
|
40
52
|
let mockedNotes: Array<{ slug: string }> = [];
|
|
41
53
|
let mockedSeries: Record<string, Array<{ slug: string }>> = {};
|
|
42
54
|
let mockedSeriesData: Record<string, { redirectFrom?: string[]; title?: string }> = {};
|
|
@@ -148,7 +160,7 @@ beforeEach(() => {
|
|
|
148
160
|
mockedNotes = [];
|
|
149
161
|
mockedSeries = {};
|
|
150
162
|
mockedSeriesData = {};
|
|
151
|
-
|
|
163
|
+
restoreEnvVar('NODE_ENV', originalNodeEnv);
|
|
152
164
|
});
|
|
153
165
|
|
|
154
166
|
afterEach(() => {
|
|
@@ -156,7 +168,7 @@ afterEach(() => {
|
|
|
156
168
|
mockedNotes = [];
|
|
157
169
|
mockedSeries = {};
|
|
158
170
|
mockedSeriesData = {};
|
|
159
|
-
|
|
171
|
+
restoreEnvVar('NODE_ENV', originalNodeEnv);
|
|
160
172
|
});
|
|
161
173
|
|
|
162
174
|
// ─── Restore real modules ─────────────────────────────────────────────────────
|
|
@@ -201,7 +213,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
201
213
|
|
|
202
214
|
test('notes/[slug] includes raw and encoded Unicode slug in non-production', async () => {
|
|
203
215
|
mockedNotes = [{ slug: '推理模型' }];
|
|
204
|
-
|
|
216
|
+
setEnvVar('NODE_ENV', 'development');
|
|
205
217
|
const { generateStaticParams } = await import('../../src/app/notes/[slug]/page');
|
|
206
218
|
const params = generateStaticParams();
|
|
207
219
|
expect(params).toContainEqual({ slug: '推理模型' });
|
|
@@ -210,7 +222,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
210
222
|
|
|
211
223
|
test('notes/[slug] includes only raw Unicode slug in production', async () => {
|
|
212
224
|
mockedNotes = [{ slug: '推理模型' }];
|
|
213
|
-
|
|
225
|
+
setEnvVar('NODE_ENV', 'production');
|
|
214
226
|
const { generateStaticParams } = await import('../../src/app/notes/[slug]/page');
|
|
215
227
|
const params = generateStaticParams();
|
|
216
228
|
expect(params).toContainEqual({ slug: '推理模型' });
|
|
@@ -232,10 +244,10 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
232
244
|
expect(params).toEqual([{ slug: '_' }]);
|
|
233
245
|
});
|
|
234
246
|
|
|
235
|
-
test('books/[slug]/[chapter] returns [{ slug: "_", chapter: "_" }]', async () => {
|
|
236
|
-
const { generateStaticParams } = await import('../../src/app/books/[slug]/[chapter]/page');
|
|
247
|
+
test('books/[slug]/[...chapter] returns [{ slug: "_", chapter: ["_"] }]', async () => {
|
|
248
|
+
const { generateStaticParams } = await import('../../src/app/books/[slug]/[...chapter]/page');
|
|
237
249
|
const params = await generateStaticParams();
|
|
238
|
-
expect(params).toEqual([{ slug: '_', chapter: '_' }]);
|
|
250
|
+
expect(params).toEqual([{ slug: '_', chapter: ['_'] }]);
|
|
239
251
|
});
|
|
240
252
|
});
|
|
241
253
|
|
|
@@ -257,7 +269,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
257
269
|
|
|
258
270
|
test('series/[slug] includes raw and encoded Unicode slug in non-production', async () => {
|
|
259
271
|
mockedSeries = { '软件构架设计': [] };
|
|
260
|
-
|
|
272
|
+
setEnvVar('NODE_ENV', 'development');
|
|
261
273
|
const { generateStaticParams } = await import('../../src/app/series/[slug]/page');
|
|
262
274
|
const params = await generateStaticParams();
|
|
263
275
|
expect(params).toContainEqual({ slug: '软件构架设计' });
|
|
@@ -266,7 +278,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
266
278
|
|
|
267
279
|
test('series/[slug] includes only raw Unicode slug in production', async () => {
|
|
268
280
|
mockedSeries = { '软件构架设计': [] };
|
|
269
|
-
|
|
281
|
+
setEnvVar('NODE_ENV', 'production');
|
|
270
282
|
const { generateStaticParams } = await import('../../src/app/series/[slug]/page');
|
|
271
283
|
const params = await generateStaticParams();
|
|
272
284
|
expect(params).toContainEqual({ slug: '软件构架设计' });
|
|
@@ -281,7 +293,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
281
293
|
|
|
282
294
|
test('series/[slug]/page/[page] includes encoded Unicode slug in non-production', async () => {
|
|
283
295
|
mockedSeries = { '软件构架设计': Array.from({ length: 6 }, (_, i) => ({ slug: `p${i + 1}` })) };
|
|
284
|
-
|
|
296
|
+
setEnvVar('NODE_ENV', 'development');
|
|
285
297
|
const { generateStaticParams } = await import('../../src/app/series/[slug]/page/[page]/page');
|
|
286
298
|
const params = await generateStaticParams();
|
|
287
299
|
expect(params).toContainEqual({ slug: '软件构架设计', page: '2' });
|
|
@@ -361,7 +373,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
361
373
|
|
|
362
374
|
test('posts/[slug] includes raw and encoded Unicode slug in non-production', async () => {
|
|
363
375
|
mockedPosts = [{ slug: '中文测试文章' }];
|
|
364
|
-
|
|
376
|
+
setEnvVar('NODE_ENV', 'development');
|
|
365
377
|
const { generateStaticParams } = await import('../../src/app/posts/[slug]/page');
|
|
366
378
|
const params = await generateStaticParams();
|
|
367
379
|
|
|
@@ -371,7 +383,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
371
383
|
|
|
372
384
|
test('posts/[slug] includes only raw Unicode slug in production', async () => {
|
|
373
385
|
mockedPosts = [{ slug: '中文测试文章' }];
|
|
374
|
-
|
|
386
|
+
setEnvVar('NODE_ENV', 'production');
|
|
375
387
|
const { generateStaticParams } = await import('../../src/app/posts/[slug]/page');
|
|
376
388
|
const params = await generateStaticParams();
|
|
377
389
|
|
|
@@ -485,7 +497,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
485
497
|
|
|
486
498
|
test('[slug]/page does not include single-segment redirectFrom for draft posts in production', async () => {
|
|
487
499
|
mockedPosts = [{ slug: 'my-post', draft: true, redirectFrom: ['/old-slug'] }];
|
|
488
|
-
|
|
500
|
+
setEnvVar('NODE_ENV', 'production');
|
|
489
501
|
const { generateStaticParams } = await import('../../src/app/[slug]/page');
|
|
490
502
|
const params = await generateStaticParams();
|
|
491
503
|
expect(params).not.toContainEqual({ slug: 'old-slug' });
|
|
@@ -588,7 +600,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
588
600
|
test('[slug]/[postSlug] includes encoded Unicode postSlug variants in non-production', async () => {
|
|
589
601
|
// Use redirectFrom to place a Unicode postSlug at a 2-segment path — no url mock needed.
|
|
590
602
|
mockedPosts = [{ slug: 'my-post', redirectFrom: ['/old-prefix/中文文章'] }];
|
|
591
|
-
|
|
603
|
+
setEnvVar('NODE_ENV', 'development');
|
|
592
604
|
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
593
605
|
const params = await generateStaticParams();
|
|
594
606
|
expect(params).toContainEqual({ slug: 'old-prefix', postSlug: '中文文章' });
|
|
@@ -597,7 +609,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
597
609
|
|
|
598
610
|
test('[slug]/[postSlug] includes encoded Unicode prefix variants in non-production', async () => {
|
|
599
611
|
mockedSeries = { '软件构架设计': [{ slug: 'architecture-post' }] };
|
|
600
|
-
|
|
612
|
+
setEnvVar('NODE_ENV', 'development');
|
|
601
613
|
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
602
614
|
const params = await generateStaticParams();
|
|
603
615
|
expect(params).toContainEqual({ slug: '软件构架设计', postSlug: 'architecture-post' });
|
|
@@ -606,7 +618,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
606
618
|
|
|
607
619
|
test('[slug]/[postSlug] includes encoded Unicode prefix and postSlug variants together in non-production', async () => {
|
|
608
620
|
mockedSeries = { '软件构架设计': [{ slug: '中文文章' }] };
|
|
609
|
-
|
|
621
|
+
setEnvVar('NODE_ENV', 'development');
|
|
610
622
|
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
611
623
|
const params = await generateStaticParams();
|
|
612
624
|
expect(params).toContainEqual({ slug: '软件构架设计', postSlug: '中文文章' });
|
|
@@ -617,7 +629,7 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
617
629
|
|
|
618
630
|
test('[slug]/[postSlug] does not include encoded Unicode postSlug variants in production', async () => {
|
|
619
631
|
mockedPosts = [{ slug: 'my-post', redirectFrom: ['/old-prefix/中文文章'] }];
|
|
620
|
-
|
|
632
|
+
setEnvVar('NODE_ENV', 'production');
|
|
621
633
|
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
622
634
|
const params = await generateStaticParams();
|
|
623
635
|
expect(params).toContainEqual({ slug: 'old-prefix', postSlug: '中文文章' });
|
|
@@ -641,7 +653,8 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
641
653
|
imageBaseSlug: 'posts',
|
|
642
654
|
category: 'Test',
|
|
643
655
|
tags: [],
|
|
644
|
-
|
|
656
|
+
readingMinutes: 1,
|
|
657
|
+
wordCount: 0,
|
|
645
658
|
}];
|
|
646
659
|
|
|
647
660
|
const page = await import('../../src/app/[slug]/[postSlug]/page');
|