@hutusi/amytis 1.12.0 → 1.14.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/CHANGELOG.md +29 -0
- package/GEMINI.md +9 -1
- package/README.md +26 -17
- package/README.zh.md +180 -100
- package/bun.lock +78 -74
- package/content/books/notes-on-thinking/cost-of-certainty.mdx +9 -0
- package/content/books/notes-on-thinking/index.mdx +16 -0
- package/content/books/notes-on-thinking/mental-models.mdx +9 -0
- package/content/books/the-pragmatic-writer/finding-your-voice.mdx +9 -0
- package/content/books/the-pragmatic-writer/index.mdx +18 -0
- package/content/books/the-pragmatic-writer/the-editing-loop.mdx +9 -0
- package/content/books/the-pragmatic-writer/why-writing-matters.mdx +9 -0
- package/content/flows/2026/03/01.md +9 -0
- package/content/flows/2026/03/03.md +9 -0
- package/content/flows/2026/03/05.md +10 -0
- package/content/flows/2026/03/07.md +11 -0
- package/content/posts/images/vibrant-waves.jpg +0 -0
- package/content/posts/welcome-to-amytis.mdx +3 -0
- package/content/series/markdown-showcase/index.mdx +2 -1
- package/content/series/markdown-showcase/mathematical-notation.mdx +8 -4
- package/content/series/markdown-showcase/syntax-highlighting.mdx +9 -5
- package/content/series/markdown-showcase/visuals-and-diagrams.mdx +8 -4
- package/content/{posts → series/markdown-showcase}//344/270/255/346/226/207/346/265/213/350/257/225/346/226/207/347/253/240.mdx +12 -7
- package/content/series/modern-web-dev/index.mdx +4 -2
- package/docs/ARCHITECTURE.md +8 -1
- package/docs/DIGITAL_GARDEN.md +22 -1
- package/package.json +12 -12
- package/public/next-image-export-optimizer-hashes.json +3 -2
- package/scripts/new-flow.ts +1 -0
- package/site.config.example.ts +3 -4
- package/site.config.ts +6 -7
- package/src/app/[slug]/[postSlug]/page.tsx +19 -2
- package/src/app/[slug]/page/[page]/page.tsx +26 -5
- package/src/app/[slug]/page.tsx +28 -8
- package/src/app/all.atom/route.ts +7 -0
- package/src/app/all.xml/route.ts +7 -0
- package/src/app/archive/page.tsx +7 -4
- package/src/app/feed.atom/route.ts +2 -57
- package/src/app/feed.xml/route.ts +2 -64
- package/src/app/flows/[year]/[month]/[day]/page.tsx +13 -0
- package/src/app/flows/feed.atom/route.ts +7 -0
- package/src/app/flows/feed.xml/route.ts +7 -0
- package/src/app/page.tsx +1 -2
- package/src/app/posts/[slug]/page.tsx +28 -9
- package/src/app/posts/feed.atom/route.ts +9 -0
- package/src/app/posts/feed.xml/route.ts +9 -0
- package/src/app/series/[slug]/page.tsx +46 -4
- package/src/components/CuratedSeriesSection.tsx +7 -11
- package/src/components/FeaturedStoriesSection.tsx +1 -1
- package/src/components/FlowCalendarSidebar.tsx +1 -1
- package/src/components/FlowContent.tsx +2 -1
- package/src/components/FlowTimelineEntry.tsx +7 -1
- package/src/components/Footer.tsx +6 -6
- package/src/components/HorizontalScroll.tsx +5 -14
- package/src/components/MarkdownRenderer.test.tsx +6 -0
- package/src/components/MarkdownRenderer.tsx +18 -16
- package/src/components/Navbar.tsx +1 -1
- package/src/components/PostList.tsx +20 -36
- package/src/components/PostSidebar.tsx +1 -1
- package/src/components/RecentNotesSection.tsx +4 -0
- package/src/components/SelectedBooksSection.tsx +65 -25
- package/src/components/SeriesCatalog.tsx +9 -7
- package/src/i18n/translations.ts +2 -0
- package/src/layouts/PostLayout.tsx +1 -1
- package/src/layouts/SimpleLayout.tsx +3 -3
- package/src/lib/feed-utils.ts +158 -18
- package/src/lib/markdown.ts +26 -5
- package/src/lib/urls.ts +9 -4
- package/tests/e2e/mobile/mobile-compat.spec.ts +58 -0
- package/tests/e2e/navigation.test.ts +26 -0
- package/tests/integration/collections.test.ts +17 -2
- package/tests/integration/feed-utils.test.ts +52 -0
- package/tests/integration/flow-title.test.ts +53 -0
- package/tests/integration/markdown-features.test.ts +3 -3
- package/tests/integration/reading-time-headings.test.ts +2 -2
- package/tests/unit/static-params.test.ts +155 -22
- package/tests/unit/urls.test.ts +10 -12
- /package/content/posts/{multilingual-test.mdx → multilingual-test-/344/270/255/346/226/207/351/225/277/346/240/207/351/242/230.mdx"} +0 -0
|
@@ -22,14 +22,24 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { describe, test, expect, mock, beforeAll, beforeEach, afterAll, afterEach } from 'bun:test';
|
|
24
24
|
|
|
25
|
-
// ─── Capture real
|
|
25
|
+
// ─── Capture real modules ─────────────────────────────────────────────────────
|
|
26
26
|
// Static imports are hoisted and resolved before any executable code (including
|
|
27
27
|
// beforeAll / mock.module calls), so this always captures the real module.
|
|
28
28
|
import * as realMarkdown from '../../src/lib/markdown';
|
|
29
|
+
import * as realUrls from '../../src/lib/urls';
|
|
30
|
+
|
|
31
|
+
// `import * as ns` creates a live namespace — its properties update when
|
|
32
|
+
// mock.module() patches the registry. Spread into a plain object here
|
|
33
|
+
// (before any mocking) to get a shallow snapshot of the real exports so
|
|
34
|
+
// restore calls always put back the originals, not the current mock state.
|
|
35
|
+
// Note: nested objects (e.g. RESERVED_ROUTE_SEGMENTS Set) share the same
|
|
36
|
+
// reference, but they are never mutated during tests so this is safe.
|
|
37
|
+
const snapshotUrls = { ...realUrls };
|
|
29
38
|
|
|
30
39
|
let mockedPosts: Array<{ slug: string; series?: string; redirectFrom?: string[] }> = [];
|
|
31
40
|
let mockedNotes: Array<{ slug: string }> = [];
|
|
32
41
|
let mockedSeries: Record<string, Array<{ slug: string }>> = {};
|
|
42
|
+
let mockedSeriesData: Record<string, { redirectFrom?: string[]; title?: string }> = {};
|
|
33
43
|
const originalNodeEnv = process.env.NODE_ENV;
|
|
34
44
|
|
|
35
45
|
// ─── Next.js runtime stubs (module-level — safe) ─────────────────────────────
|
|
@@ -118,7 +128,7 @@ beforeAll(() => {
|
|
|
118
128
|
getBookChapter: () => null,
|
|
119
129
|
getBooksByAuthor: () => [],
|
|
120
130
|
|
|
121
|
-
getSeriesData: () => null,
|
|
131
|
+
getSeriesData: (slug: string) => mockedSeriesData[slug] ?? null,
|
|
122
132
|
getSeriesPosts: () => [],
|
|
123
133
|
getSeriesAuthors: () => [],
|
|
124
134
|
|
|
@@ -136,6 +146,7 @@ beforeEach(() => {
|
|
|
136
146
|
mockedPosts = [];
|
|
137
147
|
mockedNotes = [];
|
|
138
148
|
mockedSeries = {};
|
|
149
|
+
mockedSeriesData = {};
|
|
139
150
|
process.env.NODE_ENV = originalNodeEnv;
|
|
140
151
|
});
|
|
141
152
|
|
|
@@ -143,12 +154,14 @@ afterEach(() => {
|
|
|
143
154
|
mockedPosts = [];
|
|
144
155
|
mockedNotes = [];
|
|
145
156
|
mockedSeries = {};
|
|
157
|
+
mockedSeriesData = {};
|
|
146
158
|
process.env.NODE_ENV = originalNodeEnv;
|
|
147
159
|
});
|
|
148
160
|
|
|
149
|
-
// ─── Restore real
|
|
161
|
+
// ─── Restore real modules ─────────────────────────────────────────────────────
|
|
150
162
|
afterAll(() => {
|
|
151
163
|
mock.module('@/lib/markdown', () => realMarkdown);
|
|
164
|
+
mock.module('@/lib/urls', () => snapshotUrls);
|
|
152
165
|
});
|
|
153
166
|
|
|
154
167
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -232,6 +245,15 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
232
245
|
expect(params).toEqual([{ slug: '_' }]);
|
|
233
246
|
});
|
|
234
247
|
|
|
248
|
+
test('series/[slug] includes redirectFrom slug when series is renamed', async () => {
|
|
249
|
+
mockedSeries = { 'new-name': [] };
|
|
250
|
+
mockedSeriesData = { 'new-name': { redirectFrom: ['/series/old-name'], title: 'New Series' } };
|
|
251
|
+
const { generateStaticParams } = await import('../../src/app/series/[slug]/page');
|
|
252
|
+
const params = await generateStaticParams();
|
|
253
|
+
expect(params).toContainEqual({ slug: 'new-name' });
|
|
254
|
+
expect(params).toContainEqual({ slug: 'old-name' });
|
|
255
|
+
});
|
|
256
|
+
|
|
235
257
|
test('series/[slug]/page/[page] returns [{ slug: "_", page: "2" }]', async () => {
|
|
236
258
|
const { generateStaticParams } = await import('../../src/app/series/[slug]/page/[page]/page');
|
|
237
259
|
const params = await generateStaticParams();
|
|
@@ -296,35 +318,64 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
296
318
|
});
|
|
297
319
|
|
|
298
320
|
describe('autoPaths series routing', () => {
|
|
299
|
-
// autoPaths defaults to
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
321
|
+
// autoPaths defaults to true — series posts are served at /[series-slug]/[post-slug]
|
|
322
|
+
|
|
323
|
+
describe('autoPaths disabled', () => {
|
|
324
|
+
// Override getSeriesAutoPaths to false and use /posts/[slug] as canonical URL.
|
|
325
|
+
beforeEach(() => {
|
|
326
|
+
mock.module('@/lib/urls', () => ({
|
|
327
|
+
...snapshotUrls,
|
|
328
|
+
getSeriesAutoPaths: () => false,
|
|
329
|
+
getPostUrl: (post: { slug: string; series?: string }) => `/posts/${post.slug}`,
|
|
330
|
+
}));
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
afterEach(() => {
|
|
334
|
+
mock.module('@/lib/urls', () => snapshotUrls);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('posts/[slug] includes series posts when autoPaths is disabled', async () => {
|
|
338
|
+
mockedPosts = [{ slug: 'series-post', series: 'my-series' }];
|
|
339
|
+
const { generateStaticParams } = await import('../../src/app/posts/[slug]/page');
|
|
340
|
+
const params = await generateStaticParams();
|
|
341
|
+
expect(params).toContainEqual({ slug: 'series-post' });
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('[slug]/[postSlug] does not include series auto-path params when autoPaths is disabled', async () => {
|
|
345
|
+
mockedSeries = { 'my-series': [{ slug: 'my-post' }] };
|
|
346
|
+
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
347
|
+
const params = await generateStaticParams();
|
|
348
|
+
expect(params).not.toContainEqual({ slug: 'my-series', postSlug: 'my-post' });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('posts/[slug] includes series post when canonical matches /posts/[slug]', async () => {
|
|
352
|
+
mockedPosts = [{ slug: 'my-post', series: 'my-series' }];
|
|
353
|
+
const { generateStaticParams } = await import('../../src/app/posts/[slug]/page');
|
|
354
|
+
const params = await generateStaticParams();
|
|
355
|
+
expect(params).toContainEqual({ slug: 'my-post' });
|
|
356
|
+
});
|
|
306
357
|
});
|
|
307
358
|
|
|
308
|
-
test('[slug]/[postSlug]
|
|
309
|
-
|
|
359
|
+
test('[slug]/[postSlug] includes redirectFrom paths as additional params', async () => {
|
|
360
|
+
mockedPosts = [{ slug: 'my-post', series: 'my-series', redirectFrom: ['/old-prefix/my-post'] }];
|
|
310
361
|
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
311
362
|
const params = await generateStaticParams();
|
|
312
|
-
expect(params).
|
|
363
|
+
expect(params).toContainEqual({ slug: 'old-prefix', postSlug: 'my-post' });
|
|
313
364
|
});
|
|
314
365
|
|
|
315
|
-
test('
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const { generateStaticParams } = await import('../../src/app/posts/[slug]/page');
|
|
366
|
+
test('[slug]/[postSlug] does not include /posts/* redirectFrom when basePath is "posts"', async () => {
|
|
367
|
+
mockedPosts = [{ slug: 'new-name', redirectFrom: ['/posts/old-name'] }];
|
|
368
|
+
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
319
369
|
const params = await generateStaticParams();
|
|
320
|
-
expect(params).toContainEqual({ slug: '
|
|
370
|
+
expect(params).not.toContainEqual({ slug: 'posts', postSlug: 'old-name' });
|
|
321
371
|
});
|
|
322
372
|
|
|
323
|
-
test('[slug]
|
|
324
|
-
mockedPosts = [{ slug: '
|
|
325
|
-
const { generateStaticParams } = await import('../../src/app/[slug]/
|
|
373
|
+
test('posts/[slug] includes redirectFrom slug when post is renamed within /posts/', async () => {
|
|
374
|
+
mockedPosts = [{ slug: 'new-name', redirectFrom: ['/posts/old-name'] }];
|
|
375
|
+
const { generateStaticParams } = await import('../../src/app/posts/[slug]/page');
|
|
326
376
|
const params = await generateStaticParams();
|
|
327
|
-
expect(params).toContainEqual({ slug: '
|
|
377
|
+
expect(params).toContainEqual({ slug: 'new-name' });
|
|
378
|
+
expect(params).toContainEqual({ slug: 'old-name' });
|
|
328
379
|
});
|
|
329
380
|
|
|
330
381
|
test('[slug]/page includes single-segment redirectFrom paths as additional params', async () => {
|
|
@@ -363,6 +414,69 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
363
414
|
const { generateStaticParams } = await import('../../src/app/[slug]/page');
|
|
364
415
|
expect(() => generateStaticParams()).toThrow('[amytis] redirectFrom "/old-slug"');
|
|
365
416
|
});
|
|
417
|
+
|
|
418
|
+
test('[slug]/page throws when redirectFrom alias conflicts with "posts" (RESERVED_ROUTE_SEGMENTS)', async () => {
|
|
419
|
+
mockedPosts = [{ slug: 'my-post', redirectFrom: ['/posts'] }];
|
|
420
|
+
const { generateStaticParams } = await import('../../src/app/[slug]/page');
|
|
421
|
+
expect(() => generateStaticParams()).toThrow('[amytis] redirectFrom "/posts"');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test('[slug]/page includes Unicode single-segment redirectFrom slug as param', async () => {
|
|
425
|
+
mockedPosts = [{ slug: 'my-post', redirectFrom: ['/中文路由'] }];
|
|
426
|
+
const { generateStaticParams } = await import('../../src/app/[slug]/page');
|
|
427
|
+
const params = await generateStaticParams();
|
|
428
|
+
expect(params).toContainEqual({ slug: '中文路由' });
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('autoPaths enabled', () => {
|
|
432
|
+
// Use beforeEach (not beforeAll) so each test starts with a clean base mock,
|
|
433
|
+
// preventing stale state from tests that inline-override the mock.
|
|
434
|
+
beforeEach(() => {
|
|
435
|
+
mock.module('@/lib/urls', () => ({
|
|
436
|
+
...snapshotUrls,
|
|
437
|
+
getSeriesAutoPaths: () => true,
|
|
438
|
+
getSeriesCustomPaths: () => ({}),
|
|
439
|
+
getPostUrl: (post: { slug: string; series?: string }) =>
|
|
440
|
+
post.series ? `/${post.series}/${post.slug}` : `/posts/${post.slug}`,
|
|
441
|
+
validateSeriesAutoPaths: () => {},
|
|
442
|
+
}));
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
afterEach(() => {
|
|
446
|
+
mock.module('@/lib/urls', () => snapshotUrls);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('[slug]/page includes auto-path series slug when autoPaths enabled', async () => {
|
|
450
|
+
mockedSeries = { 'my-series': [{ slug: 'my-post' }] };
|
|
451
|
+
const { generateStaticParams } = await import('../../src/app/[slug]/page');
|
|
452
|
+
const params = await generateStaticParams();
|
|
453
|
+
expect(params).toContainEqual({ slug: 'my-series' });
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test('[slug]/page includes Unicode auto-path series slug when autoPaths enabled', async () => {
|
|
457
|
+
mockedSeries = { '中文系列': [{ slug: 'post-one' }] };
|
|
458
|
+
const { generateStaticParams } = await import('../../src/app/[slug]/page');
|
|
459
|
+
const params = await generateStaticParams();
|
|
460
|
+
expect(params).toContainEqual({ slug: '中文系列' });
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test('[slug]/page uses customPaths prefix for series with override, not the series slug', async () => {
|
|
464
|
+
// Override the base mock to add a customPaths entry for this test.
|
|
465
|
+
mock.module('@/lib/urls', () => ({
|
|
466
|
+
...snapshotUrls,
|
|
467
|
+
getSeriesAutoPaths: () => true,
|
|
468
|
+
getSeriesCustomPaths: () => ({ 'my-series': 'articles' }),
|
|
469
|
+
getPostUrl: (post: { slug: string; series?: string }) =>
|
|
470
|
+
post.series === 'my-series' ? `/articles/${post.slug}` : `/posts/${post.slug}`,
|
|
471
|
+
validateSeriesAutoPaths: () => {},
|
|
472
|
+
}));
|
|
473
|
+
mockedSeries = { 'my-series': [{ slug: 'my-post' }] };
|
|
474
|
+
const { generateStaticParams } = await import('../../src/app/[slug]/page');
|
|
475
|
+
const params = await generateStaticParams();
|
|
476
|
+
expect(params).toContainEqual({ slug: 'articles' });
|
|
477
|
+
expect(params).not.toContainEqual({ slug: 'my-series' });
|
|
478
|
+
});
|
|
479
|
+
});
|
|
366
480
|
});
|
|
367
481
|
|
|
368
482
|
describe('custom path routes', () => {
|
|
@@ -380,6 +494,25 @@ describe('generateStaticParams — placeholder when content is empty', () => {
|
|
|
380
494
|
expect(params).toEqual([{ slug: '_', postSlug: '_' }]);
|
|
381
495
|
});
|
|
382
496
|
|
|
497
|
+
test('[slug]/[postSlug] includes encoded Unicode postSlug variants in non-production', async () => {
|
|
498
|
+
// Use redirectFrom to place a Unicode postSlug at a 2-segment path — no url mock needed.
|
|
499
|
+
mockedPosts = [{ slug: 'my-post', redirectFrom: ['/old-prefix/中文文章'] }];
|
|
500
|
+
process.env.NODE_ENV = 'development';
|
|
501
|
+
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
502
|
+
const params = await generateStaticParams();
|
|
503
|
+
expect(params).toContainEqual({ slug: 'old-prefix', postSlug: '中文文章' });
|
|
504
|
+
expect(params).toContainEqual({ slug: 'old-prefix', postSlug: encodeURIComponent('中文文章') });
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test('[slug]/[postSlug] does not include encoded Unicode postSlug variants in production', async () => {
|
|
508
|
+
mockedPosts = [{ slug: 'my-post', redirectFrom: ['/old-prefix/中文文章'] }];
|
|
509
|
+
process.env.NODE_ENV = 'production';
|
|
510
|
+
const { generateStaticParams } = await import('../../src/app/[slug]/[postSlug]/page');
|
|
511
|
+
const params = await generateStaticParams();
|
|
512
|
+
expect(params).toContainEqual({ slug: 'old-prefix', postSlug: '中文文章' });
|
|
513
|
+
expect(params).not.toContainEqual({ slug: 'old-prefix', postSlug: encodeURIComponent('中文文章') });
|
|
514
|
+
});
|
|
515
|
+
|
|
383
516
|
test('[slug]/page/[page]/page returns placeholder when no custom paths', async () => {
|
|
384
517
|
const { generateStaticParams } = await import('../../src/app/[slug]/page/[page]/page');
|
|
385
518
|
const params = await generateStaticParams();
|
package/tests/unit/urls.test.ts
CHANGED
|
@@ -2,29 +2,27 @@ import { describe, test, expect } from 'bun:test';
|
|
|
2
2
|
import { getPostUrl, getSeriesAutoPaths, validateSeriesAutoPaths } from '../../src/lib/urls';
|
|
3
3
|
|
|
4
4
|
describe('getSeriesAutoPaths', () => {
|
|
5
|
-
test('returns
|
|
6
|
-
expect(getSeriesAutoPaths()).toBe(
|
|
5
|
+
test('returns true by default', () => {
|
|
6
|
+
expect(getSeriesAutoPaths()).toBe(true);
|
|
7
7
|
});
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
describe('getPostUrl — autoPaths
|
|
10
|
+
describe('getPostUrl — autoPaths enabled (default)', () => {
|
|
11
11
|
test('post with no series uses basePath', () => {
|
|
12
12
|
expect(getPostUrl({ slug: 'hello' })).toBe('/posts/hello');
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
test('post with series
|
|
16
|
-
expect(getPostUrl({ slug: 'hello', series: 'my-series' })).toBe('/
|
|
15
|
+
test('post with series uses series slug as prefix when autoPaths is enabled', () => {
|
|
16
|
+
expect(getPostUrl({ slug: 'hello', series: 'my-series' })).toBe('/my-series/hello');
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
describe('validateSeriesAutoPaths — autoPaths
|
|
21
|
-
test('
|
|
22
|
-
|
|
23
|
-
const reserved = ['tags', 'series', 'books', 'flows', 'archive', 'posts'];
|
|
24
|
-
expect(() => validateSeriesAutoPaths(reserved)).not.toThrow();
|
|
20
|
+
describe('validateSeriesAutoPaths — autoPaths enabled (default)', () => {
|
|
21
|
+
test('throws for a reserved route slug', () => {
|
|
22
|
+
expect(() => validateSeriesAutoPaths(['tags'])).toThrow('[amytis]');
|
|
25
23
|
});
|
|
26
24
|
|
|
27
|
-
test('
|
|
28
|
-
expect(() => validateSeriesAutoPaths(['about'], ['about'])).
|
|
25
|
+
test('throws for an extra reserved slug', () => {
|
|
26
|
+
expect(() => validateSeriesAutoPaths(['about'], ['about'])).toThrow('[amytis]');
|
|
29
27
|
});
|
|
30
28
|
});
|