@glossarist/concept-browser 0.3.4 → 0.3.7

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.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/about-view.test.ts +98 -0
  3. package/src/__tests__/app-footer.test.ts +38 -0
  4. package/src/__tests__/app-header.test.ts +130 -0
  5. package/src/__tests__/app-sidebar.test.ts +159 -0
  6. package/src/__tests__/asciidoc-lite.test.ts +1 -1
  7. package/src/__tests__/concept-card.test.ts +115 -0
  8. package/src/__tests__/concept-detail-interaction.test.ts +251 -0
  9. package/src/__tests__/concept-timeline.test.ts +175 -0
  10. package/src/__tests__/concept-view.test.ts +75 -0
  11. package/src/__tests__/contributors-view.test.ts +103 -0
  12. package/src/__tests__/dataset-view.test.ts +231 -0
  13. package/src/__tests__/format-downloads.test.ts +98 -0
  14. package/src/__tests__/graph-view.test.ts +69 -0
  15. package/src/__tests__/home-interaction.test.ts +157 -0
  16. package/src/__tests__/language-detail.test.ts +146 -0
  17. package/src/__tests__/nav-icon.test.ts +48 -0
  18. package/src/__tests__/news-view.test.ts +87 -0
  19. package/src/__tests__/page-view.test.ts +83 -0
  20. package/src/__tests__/resolve-view.test.ts +77 -0
  21. package/src/__tests__/router.test.ts +65 -0
  22. package/src/__tests__/search-bar.test.ts +219 -0
  23. package/src/__tests__/search-view.test.ts +41 -0
  24. package/src/__tests__/stats-view.test.ts +77 -0
  25. package/src/__tests__/test-helpers.ts +168 -0
  26. package/src/__tests__/ui-store.test.ts +100 -0
  27. package/src/__tests__/v-math.test.ts +8 -7
  28. package/src/adapters/DatasetAdapter.ts +17 -15
  29. package/src/adapters/types.ts +1 -1
  30. package/src/components/ConceptDetail.vue +16 -54
  31. package/src/components/ConceptTimeline.vue +1 -8
  32. package/src/components/LanguageDetail.vue +2 -25
  33. package/src/composables/use-render-options.ts +1 -4
  34. package/src/router/index.ts +1 -1
  35. package/src/stores/vocabulary.ts +7 -7
  36. package/src/utils/asciidoc-lite.ts +17 -19
  37. package/src/utils/concept-helpers.ts +34 -0
  38. package/src/utils/escape.ts +7 -0
  39. package/src/utils/markdown-lite.ts +1 -3
  40. package/src/utils/math.ts +2 -11
  41. package/src/utils/plurimath.ts +2 -7
  42. package/src/views/ConceptView.vue +22 -1
  43. package/src/views/DatasetView.vue +7 -2
@@ -0,0 +1,146 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { mount, flushPromises } from '@vue/test-utils';
3
+ import LanguageDetail from '../components/LanguageDetail.vue';
4
+ import { useVocabularyStore } from '../stores/vocabulary';
5
+ import type { LocalizedConcept } from '../adapters/types';
6
+ import { createTestRouter, setupPinia, makeManifest, makeLocalizedConcept, makeAdapterStub } from './test-helpers';
7
+
8
+ describe('LanguageDetail', () => {
9
+ let pinia: ReturnType<typeof setupPinia>;
10
+ let router: Awaited<ReturnType<typeof createTestRouter>>;
11
+
12
+ beforeEach(async () => {
13
+ pinia = setupPinia();
14
+ router = await createTestRouter('dataset', '/');
15
+ const store = useVocabularyStore();
16
+ store.manifests.set('test', makeManifest({ languages: ['eng', 'fra'] }));
17
+ store.datasets.set('test', makeAdapterStub());
18
+ });
19
+
20
+ function mountDetail(lcs: Record<string, LocalizedConcept>, activeLang = 'eng') {
21
+ return mount(LanguageDetail, {
22
+ global: { plugins: [pinia, router], directives: { math: () => {} } },
23
+ props: { localizedConcepts: lcs, activeLang },
24
+ });
25
+ }
26
+
27
+ it('renders language selector buttons', () => {
28
+ const eng = makeLocalizedConcept();
29
+ const fra = makeLocalizedConcept({ '@id': '.../fra', 'gl:languageCode': 'fra' });
30
+ const wrapper = mountDetail({ eng, fra });
31
+ expect(wrapper.text()).toContain('English');
32
+ expect(wrapper.text()).toContain('French');
33
+ });
34
+
35
+ it('highlights active language button', () => {
36
+ const eng = makeLocalizedConcept();
37
+ const fra = makeLocalizedConcept({ '@id': '.../fra', 'gl:languageCode': 'fra' });
38
+ const wrapper = mountDetail({ eng, fra }, 'eng');
39
+ const buttons = wrapper.findAll('button').filter(b => b.text().includes('English'));
40
+ expect(buttons[0].classes()).toContain('bg-ink-800');
41
+ });
42
+
43
+ it('emits update:activeLang on language click', async () => {
44
+ const eng = makeLocalizedConcept();
45
+ const fra = makeLocalizedConcept({ '@id': '.../fra', 'gl:languageCode': 'fra' });
46
+ const wrapper = mountDetail({ eng, fra }, 'eng');
47
+ const fraBtn = wrapper.findAll('button').find(b => b.text().includes('French'));
48
+ expect(fraBtn).toBeDefined();
49
+ await fraBtn!.trigger('click');
50
+ expect(wrapper.emitted('update:activeLang')?.[0]).toEqual(['fra']);
51
+ });
52
+
53
+ it('shows entry status badge', () => {
54
+ const eng = makeLocalizedConcept({ 'gl:entryStatus': 'valid' });
55
+ const wrapper = mountDetail({ eng });
56
+ expect(wrapper.text()).toContain('valid');
57
+ });
58
+
59
+ it('shows designations', () => {
60
+ const eng = makeLocalizedConcept({
61
+ 'gl:designation': [
62
+ { '@type': 'gl:Expression', 'gl:term': 'road', 'gl:normativeStatus': 'preferred' },
63
+ ],
64
+ });
65
+ const wrapper = mountDetail({ eng });
66
+ expect(wrapper.text()).toContain('road');
67
+ expect(wrapper.text()).toContain('Expression');
68
+ expect(wrapper.text()).toContain('Preferred');
69
+ });
70
+
71
+ it('shows definition', () => {
72
+ const eng = makeLocalizedConcept({
73
+ 'gl:definition': [{ '@type': 'gl:Definition', 'gl:content': 'A paved surface for vehicles.' }],
74
+ });
75
+ const wrapper = mountDetail({ eng });
76
+ expect(wrapper.text()).toContain('Definition');
77
+ expect(wrapper.text()).toContain('paved surface');
78
+ });
79
+
80
+ it('shows notes', () => {
81
+ const eng = makeLocalizedConcept({
82
+ 'gl:notes': [{ '@type': 'gl:Note', 'gl:content': 'This is a note.' }],
83
+ });
84
+ const wrapper = mountDetail({ eng });
85
+ expect(wrapper.text()).toContain('Notes');
86
+ expect(wrapper.text()).toContain('This is a note');
87
+ });
88
+
89
+ it('shows examples', () => {
90
+ const eng = makeLocalizedConcept({
91
+ 'gl:examples': [{ '@type': 'gl:Example', 'gl:content': 'A highway is a road.' }],
92
+ });
93
+ const wrapper = mountDetail({ eng });
94
+ expect(wrapper.text()).toContain('Examples');
95
+ expect(wrapper.text()).toContain('A highway is a road');
96
+ });
97
+
98
+ it('shows sources', () => {
99
+ const eng = makeLocalizedConcept({
100
+ 'gl:source': [{ '@type': 'gl:Source', 'gl:sourceType': 'authoritative', 'gl:origin': { '@type': 'gl:Origin', 'gl:ref': 'ISO 7010' } }],
101
+ });
102
+ const wrapper = mountDetail({ eng });
103
+ expect(wrapper.text()).toContain('Sources');
104
+ expect(wrapper.text()).toContain('ISO 7010');
105
+ });
106
+
107
+ it('shows term-only state for language without definition', () => {
108
+ const eng = makeLocalizedConcept({
109
+ 'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'test', 'gl:normativeStatus': 'preferred' }],
110
+ });
111
+ delete (eng as any)['gl:definition'];
112
+ delete (eng as any)['gl:notes'];
113
+ delete (eng as any)['gl:examples'];
114
+ const wrapper = mountDetail({ eng });
115
+ expect(wrapper.text()).toContain('Term only in English');
116
+ });
117
+
118
+ it('shows no data message for missing language', () => {
119
+ const eng = makeLocalizedConcept();
120
+ const wrapper = mountDetail({ eng }, 'zho');
121
+ expect(wrapper.text()).toContain('No data available');
122
+ });
123
+
124
+ it('shows designation type badges', () => {
125
+ const eng = makeLocalizedConcept({
126
+ 'gl:designation': [
127
+ { '@type': 'gl:Symbol', 'gl:term': 'H₂O', 'gl:normativeStatus': 'preferred' },
128
+ { '@type': 'gl:Abbreviation', 'gl:term': 'abbr', 'gl:normativeStatus': 'admitted' },
129
+ ],
130
+ });
131
+ const wrapper = mountDetail({ eng });
132
+ expect(wrapper.text()).toContain('Symbol');
133
+ expect(wrapper.text()).toContain('Abbreviation');
134
+ });
135
+
136
+ it('shows gender and plurality when present', () => {
137
+ const eng = makeLocalizedConcept({
138
+ 'gl:designation': [
139
+ { '@type': 'gl:Expression', 'gl:term': 'route', 'gl:normativeStatus': 'preferred', 'gl:gender': 'f', 'gl:plurality': 'singular' },
140
+ ],
141
+ });
142
+ const wrapper = mountDetail({ eng });
143
+ expect(wrapper.text()).toContain('f');
144
+ expect(wrapper.text()).toContain('singular');
145
+ });
146
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import NavIcon from '../components/NavIcon.vue';
4
+
5
+ describe('NavIcon', () => {
6
+ it('renders an SVG element', () => {
7
+ const wrapper = mount(NavIcon, { props: { name: 'home' } });
8
+ expect(wrapper.find('svg').exists()).toBe(true);
9
+ });
10
+
11
+ it('renders home icon path', () => {
12
+ const wrapper = mount(NavIcon, { props: { name: 'home' } });
13
+ const path = wrapper.find('svg path');
14
+ expect(path.exists()).toBe(true);
15
+ expect(path.attributes('d')).toBeTruthy();
16
+ });
17
+
18
+ it('renders search icon', () => {
19
+ const wrapper = mount(NavIcon, { props: { name: 'search' } });
20
+ expect(wrapper.find('svg').exists()).toBe(true);
21
+ });
22
+
23
+ it('renders graph icon', () => {
24
+ const wrapper = mount(NavIcon, { props: { name: 'graph' } });
25
+ expect(wrapper.find('svg').exists()).toBe(true);
26
+ });
27
+
28
+ it('falls back to info icon for unknown name', () => {
29
+ const wrapper = mount(NavIcon, { props: { name: 'nonexistent' } });
30
+ expect(wrapper.find('svg').exists()).toBe(true);
31
+ });
32
+
33
+ it('applies correct CSS classes', () => {
34
+ const wrapper = mount(NavIcon, { props: { name: 'home' } });
35
+ const svg = wrapper.find('svg');
36
+ expect(svg.classes()).toContain('w-4');
37
+ expect(svg.classes()).toContain('h-4');
38
+ expect(svg.classes()).toContain('text-ink-400');
39
+ });
40
+
41
+ it('renders all known icon types', () => {
42
+ const icons = ['home', 'search', 'graph', 'newspaper', 'users', 'info', 'chart', 'list'];
43
+ for (const name of icons) {
44
+ const wrapper = mount(NavIcon, { props: { name } });
45
+ expect(wrapper.find('svg').exists()).toBe(true);
46
+ }
47
+ });
48
+ });
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { mount, flushPromises } from '@vue/test-utils';
3
+ import { createPinia, setActivePinia } from 'pinia';
4
+ import { createRouter, createMemoryHistory } from 'vue-router';
5
+ import NewsView from '../views/NewsView.vue';
6
+
7
+ async function createTestRouter() {
8
+ const router = createRouter({
9
+ history: createMemoryHistory(),
10
+ routes: [
11
+ { path: '/', name: 'home', component: { template: '<div/>' } },
12
+ { path: '/news', name: 'news', component: { template: '<div/>' } },
13
+ ],
14
+ });
15
+ router.push('/news');
16
+ await router.isReady();
17
+ return router;
18
+ }
19
+
20
+ describe('NewsView', () => {
21
+ let pinia: ReturnType<typeof createPinia>;
22
+ let router: Awaited<ReturnType<typeof createTestRouter>>;
23
+
24
+ beforeEach(async () => {
25
+ pinia = createPinia();
26
+ setActivePinia(pinia);
27
+ router = await createTestRouter();
28
+ });
29
+
30
+ function mountNews() {
31
+ return mount(NewsView, {
32
+ global: { plugins: [pinia, router] },
33
+ });
34
+ }
35
+
36
+ it('renders breadcrumb navigation', () => {
37
+ const wrapper = mountNews();
38
+ expect(wrapper.text()).toContain('Home');
39
+ expect(wrapper.text()).toContain('News');
40
+ });
41
+
42
+ it('renders News heading', () => {
43
+ const wrapper = mountNews();
44
+ expect(wrapper.text()).toContain('News');
45
+ });
46
+
47
+ it('shows loading skeleton initially', () => {
48
+ const wrapper = mountNews();
49
+ expect(wrapper.find('.animate-pulse').exists()).toBe(true);
50
+ });
51
+
52
+ it('shows empty state when no posts', async () => {
53
+ globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve([]) });
54
+ const wrapper = mountNews();
55
+ await flushPromises();
56
+ expect(wrapper.text()).toContain('No news posts yet');
57
+ });
58
+
59
+ it('shows error state when fetch fails', async () => {
60
+ globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
61
+ const wrapper = mountNews();
62
+ await flushPromises();
63
+ expect(wrapper.text()).toContain('Failed to load news posts');
64
+ });
65
+
66
+ it('renders posts when available', async () => {
67
+ const posts = [
68
+ { slug: 'test-post', title: 'Test Post', date: '2025-01-15', categories: ['release'], file: '/news/test-post.adoc', excerpt: 'A test excerpt.' },
69
+ ];
70
+ globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(posts) });
71
+ const wrapper = mountNews();
72
+ await flushPromises();
73
+ expect(wrapper.text()).toContain('Test Post');
74
+ expect(wrapper.text()).toContain('release');
75
+ expect(wrapper.text()).toContain('A test excerpt.');
76
+ });
77
+
78
+ it('formats dates correctly', async () => {
79
+ const posts = [
80
+ { slug: 'dated', title: 'Dated', date: '2025-03-15', categories: [], file: '/news/dated.adoc', excerpt: '' },
81
+ ];
82
+ globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(posts) });
83
+ const wrapper = mountNews();
84
+ await flushPromises();
85
+ expect(wrapper.text()).toContain('March 15, 2025');
86
+ });
87
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { mount, flushPromises } from '@vue/test-utils';
3
+ import { createPinia, setActivePinia } from 'pinia';
4
+ import { createRouter, createMemoryHistory } from 'vue-router';
5
+ import PageView from '../views/PageView.vue';
6
+
7
+ async function createTestRouter(path = '/pages/test-page') {
8
+ const router = createRouter({
9
+ history: createMemoryHistory(),
10
+ routes: [
11
+ { path: '/', name: 'home', component: { template: '<div/>' } },
12
+ { path: '/pages/:slug', name: 'page', component: { template: '<div/>' } },
13
+ ],
14
+ });
15
+ router.push(path);
16
+ await router.isReady();
17
+ return router;
18
+ }
19
+
20
+ describe('PageView', () => {
21
+ let pinia: ReturnType<typeof createPinia>;
22
+ let router: Awaited<ReturnType<typeof createTestRouter>>;
23
+
24
+ beforeEach(async () => {
25
+ pinia = createPinia();
26
+ setActivePinia(pinia);
27
+ router = await createTestRouter();
28
+ });
29
+
30
+ function mountPage() {
31
+ return mount(PageView, {
32
+ global: { plugins: [pinia, router] },
33
+ });
34
+ }
35
+
36
+ it('shows loading skeleton initially', () => {
37
+ const wrapper = mountPage();
38
+ expect(wrapper.find('.animate-pulse').exists()).toBe(true);
39
+ });
40
+
41
+ it('renders breadcrumb navigation', () => {
42
+ const wrapper = mountPage();
43
+ expect(wrapper.text()).toContain('Home');
44
+ });
45
+
46
+ it('shows page not found when fetch fails', async () => {
47
+ globalThis.fetch = vi.fn().mockResolvedValue({ ok: false });
48
+ const wrapper = mountPage();
49
+ await flushPromises();
50
+ expect(wrapper.text()).toContain('Page Not Found');
51
+ expect(wrapper.text()).toContain('test-page');
52
+ });
53
+
54
+ it('shows go home link on not found', async () => {
55
+ globalThis.fetch = vi.fn().mockResolvedValue({ ok: false });
56
+ const wrapper = mountPage();
57
+ await flushPromises();
58
+ const homeLink = wrapper.findAll('a').find(a => a.text() === 'Go Home');
59
+ expect(homeLink).toBeDefined();
60
+ });
61
+
62
+ it('renders page content when loaded', async () => {
63
+ globalThis.fetch = vi.fn().mockResolvedValue({
64
+ ok: true,
65
+ json: () => Promise.resolve({ title: 'About Us', html: '<p>Hello world</p>' }),
66
+ });
67
+ const wrapper = mountPage();
68
+ await flushPromises();
69
+ expect(wrapper.text()).toContain('About Us');
70
+ expect(wrapper.text()).toContain('Hello world');
71
+ });
72
+
73
+ it('renders page title in breadcrumb', async () => {
74
+ globalThis.fetch = vi.fn().mockResolvedValue({
75
+ ok: true,
76
+ json: () => Promise.resolve({ title: 'About Us', html: '<p>Content</p>' }),
77
+ });
78
+ const wrapper = mountPage();
79
+ await flushPromises();
80
+ const breadcrumb = wrapper.find('nav[aria-label="Breadcrumb"]');
81
+ expect(breadcrumb.text()).toContain('About Us');
82
+ });
83
+ });
@@ -0,0 +1,77 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { mount, flushPromises } from '@vue/test-utils';
3
+ import { createPinia, setActivePinia } from 'pinia';
4
+ import { createRouter, createMemoryHistory } from 'vue-router';
5
+ import { resetFactory, getFactory } from '../adapters/factory';
6
+ import ResolveView from '../views/ResolveView.vue';
7
+
8
+ const TEST_URI = 'https://glossarist.org/test/concept/1';
9
+
10
+ async function createTestRouter(uri = TEST_URI) {
11
+ const router = createRouter({
12
+ history: createMemoryHistory(),
13
+ routes: [
14
+ { path: '/', name: 'home', component: { template: '<div/>' } },
15
+ { path: '/dataset/:registerId/concept/:conceptId', name: 'concept', component: { template: '<div/>' } },
16
+ { path: '/resolve/:uri(.*)', name: 'resolve', component: { template: '<div/>' } },
17
+ ],
18
+ });
19
+ router.push(`/resolve/${encodeURIComponent(uri)}`);
20
+ await router.isReady();
21
+ return router;
22
+ }
23
+
24
+ describe('ResolveView', () => {
25
+ let pinia: ReturnType<typeof createPinia>;
26
+ let router: Awaited<ReturnType<typeof createTestRouter>>;
27
+
28
+ beforeEach(async () => {
29
+ resetFactory();
30
+ pinia = createPinia();
31
+ setActivePinia(pinia);
32
+ router = await createTestRouter();
33
+ // Pre-seed the factory with a stub adapter so discoverDatasets is skipped
34
+ const factory = getFactory();
35
+ const adapter = { registerId: 'test', manifest: null };
36
+ (factory as any).adapters.set('test', adapter);
37
+ });
38
+
39
+ function mountResolve() {
40
+ return mount(ResolveView, {
41
+ global: { plugins: [pinia, router] },
42
+ });
43
+ }
44
+
45
+ it('shows resolving message while loading', () => {
46
+ const wrapper = mountResolve();
47
+ expect(wrapper.text()).toContain('Resolving...');
48
+ });
49
+
50
+ it('shows error when concept not found', async () => {
51
+ const wrapper = mountResolve();
52
+ await flushPromises();
53
+ expect(wrapper.text()).toContain('Concept not found');
54
+ });
55
+
56
+ it('displays the URI being resolved', async () => {
57
+ const wrapper = mountResolve();
58
+ await flushPromises();
59
+ expect(wrapper.text()).toContain('glossarist.org/test/concept/1');
60
+ });
61
+
62
+ it('shows return to home link on error', async () => {
63
+ const wrapper = mountResolve();
64
+ await flushPromises();
65
+ const link = wrapper.findAll('a').find(a => a.text().includes('Return to home'));
66
+ expect(link).toBeDefined();
67
+ });
68
+
69
+ it('resolves the URI via the factory resolver', async () => {
70
+ const factory = getFactory();
71
+ factory.resolver.registerDataset('test', ['https://glossarist.org/test/*']);
72
+ const resolution = factory.resolve(TEST_URI);
73
+ expect(resolution.type).toBe('internal');
74
+ expect(resolution).toHaveProperty('registerId', 'test');
75
+ expect(resolution).toHaveProperty('conceptId', '1');
76
+ });
77
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { routes } from '../router/index';
3
+
4
+ // We just need the route definitions, not a full router instance
5
+ const routeMap = new Map(routes.map(r => [r.name as string, r]));
6
+
7
+ describe('Router route definitions', () => {
8
+ it('defines all expected routes', () => {
9
+ const names = routes.map(r => r.name).filter(Boolean);
10
+ expect(names).toContain('home');
11
+ expect(names).toContain('dataset');
12
+ expect(names).toContain('concept');
13
+ expect(names).toContain('stats');
14
+ expect(names).toContain('about');
15
+ expect(names).toContain('search');
16
+ expect(names).toContain('graph');
17
+ expect(names).toContain('resolve');
18
+ });
19
+
20
+ it('uses correct path patterns', () => {
21
+ expect(routeMap.get('home')!.path).toBe('/');
22
+ expect(routeMap.get('dataset')!.path).toBe('/dataset/:registerId');
23
+ expect(routeMap.get('concept')!.path).toBe('/dataset/:registerId/concept/:conceptId');
24
+ expect(routeMap.get('stats')!.path).toBe('/dataset/:registerId/stats');
25
+ expect(routeMap.get('about')!.path).toBe('/dataset/:registerId/about');
26
+ expect(routeMap.get('search')!.path).toBe('/search');
27
+ expect(routeMap.get('graph')!.path).toBe('/graph');
28
+ expect(routeMap.get('resolve')!.path).toBe('/resolve/:uri(.*)');
29
+ });
30
+
31
+ it('dataset and concept routes use props: true', () => {
32
+ expect(routeMap.get('dataset')!.props).toBe(true);
33
+ expect(routeMap.get('concept')!.props).toBe(true);
34
+ expect(routeMap.get('stats')!.props).toBe(true);
35
+ expect(routeMap.get('about')!.props).toBe(true);
36
+ });
37
+
38
+ it('resolve route accepts any URI with wildcard', () => {
39
+ const resolve = routeMap.get('resolve')!;
40
+ expect(resolve.path).toContain(':uri(.*)');
41
+ });
42
+
43
+ it('uses lazy-loaded components for all routes', () => {
44
+ for (const route of routes) {
45
+ if (route.component && typeof route.component === 'function') {
46
+ // Dynamic import returns a function
47
+ expect(route.component).toBeTypeOf('function');
48
+ }
49
+ }
50
+ });
51
+
52
+ it('defines dataset-page catch-all after specific routes', () => {
53
+ const names = routes.map(r => r.name);
54
+ const dsIdx = names.indexOf('dataset');
55
+ const dsPageIdx = names.indexOf('dataset-page');
56
+ expect(dsIdx).toBeGreaterThan(-1);
57
+ expect(dsPageIdx).toBeGreaterThan(dsIdx);
58
+ });
59
+
60
+ it('defines page slug catch-all as last route', () => {
61
+ const last = routes[routes.length - 1];
62
+ expect(last.name).toBe('page');
63
+ expect(last.path).toBe('/:slug');
64
+ });
65
+ });