@glossarist/concept-browser 0.3.4 → 0.4.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.
Files changed (81) hide show
  1. package/README.md +3 -2
  2. package/cli/index.mjs +2 -1
  3. package/env.d.ts +5 -0
  4. package/package.json +4 -3
  5. package/scripts/build-edges.js +78 -10
  6. package/scripts/generate-data.mjs +152 -20
  7. package/scripts/generate-ontology-data.mjs +184 -0
  8. package/scripts/generate-ontology-schema.mjs +315 -0
  9. package/src/__tests__/about-view.test.ts +98 -0
  10. package/src/__tests__/app-footer.test.ts +38 -0
  11. package/src/__tests__/app-header.test.ts +130 -0
  12. package/src/__tests__/app-sidebar.test.ts +159 -0
  13. package/src/__tests__/asciidoc-lite.test.ts +1 -1
  14. package/src/__tests__/concept-card.test.ts +115 -0
  15. package/src/__tests__/concept-detail-interaction.test.ts +273 -0
  16. package/src/__tests__/concept-formats.test.ts +32 -30
  17. package/src/__tests__/concept-timeline.test.ts +200 -0
  18. package/src/__tests__/concept-view.test.ts +88 -0
  19. package/src/__tests__/contributors-view.test.ts +103 -0
  20. package/src/__tests__/dataset-adapter.test.ts +172 -23
  21. package/src/__tests__/dataset-view.test.ts +232 -0
  22. package/src/__tests__/designation-registry.test.ts +161 -0
  23. package/src/__tests__/format-downloads.test.ts +98 -0
  24. package/src/__tests__/graph-view.test.ts +69 -0
  25. package/src/__tests__/graph.test.ts +62 -0
  26. package/src/__tests__/home-interaction.test.ts +157 -0
  27. package/src/__tests__/language-detail.test.ts +203 -0
  28. package/src/__tests__/nav-icon.test.ts +48 -0
  29. package/src/__tests__/news-view.test.ts +87 -0
  30. package/src/__tests__/ontology-registry.test.ts +109 -0
  31. package/src/__tests__/page-view.test.ts +83 -0
  32. package/src/__tests__/relationship-categories.test.ts +62 -0
  33. package/src/__tests__/resolve-view.test.ts +77 -0
  34. package/src/__tests__/router.test.ts +65 -0
  35. package/src/__tests__/search-bar.test.ts +219 -0
  36. package/src/__tests__/search-view.test.ts +41 -0
  37. package/src/__tests__/stats-view.test.ts +77 -0
  38. package/src/__tests__/test-helpers.ts +171 -0
  39. package/src/__tests__/ui-store.test.ts +100 -0
  40. package/src/__tests__/v-math.test.ts +8 -7
  41. package/src/adapters/DatasetAdapter.ts +188 -63
  42. package/src/adapters/model-bridge.ts +277 -0
  43. package/src/adapters/ontology-registry.ts +75 -0
  44. package/src/adapters/ontology-schema.ts +100 -0
  45. package/src/adapters/types.ts +53 -78
  46. package/src/components/AppSidebar.vue +1 -1
  47. package/src/components/CitationDisplay.vue +35 -0
  48. package/src/components/ConceptDetail.vue +349 -146
  49. package/src/components/ConceptRdfView.vue +397 -0
  50. package/src/components/ConceptTimeline.vue +57 -60
  51. package/src/components/GraphPanel.vue +96 -31
  52. package/src/components/LanguageDetail.vue +46 -61
  53. package/src/components/NavIcon.vue +1 -0
  54. package/src/components/NonVerbalRepDisplay.vue +38 -0
  55. package/src/components/RelationshipList.vue +99 -0
  56. package/src/composables/use-render-options.ts +1 -4
  57. package/src/config/use-site-config.ts +3 -0
  58. package/src/data/ontology-schema.json +1551 -0
  59. package/src/data/taxonomies.json +543 -0
  60. package/src/graph/GraphEngine.ts +7 -4
  61. package/src/router/index.ts +6 -1
  62. package/src/shims/empty.ts +1 -0
  63. package/src/shims/node-crypto.ts +6 -0
  64. package/src/shims/node-path.ts +10 -0
  65. package/src/stores/vocabulary.ts +82 -32
  66. package/src/style.css +74 -20
  67. package/src/utils/asciidoc-lite.ts +17 -19
  68. package/src/utils/concept-formats.ts +22 -20
  69. package/src/utils/concept-helpers.ts +54 -0
  70. package/src/utils/designation-registry.ts +124 -0
  71. package/src/utils/escape.ts +7 -0
  72. package/src/utils/markdown-lite.ts +1 -3
  73. package/src/utils/math.ts +2 -11
  74. package/src/utils/plurimath.ts +2 -7
  75. package/src/utils/relationship-categories.ts +84 -0
  76. package/src/views/ConceptView.vue +22 -1
  77. package/src/views/DatasetView.vue +7 -2
  78. package/src/views/OntologySchemaView.vue +302 -0
  79. package/src/views/PageView.vue +28 -17
  80. package/src/views/StatsView.vue +34 -12
  81. package/vite.config.ts +8 -0
@@ -0,0 +1,161 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Expression, Abbreviation } from 'glossarist';
3
+ import {
4
+ designationTypeInfo,
5
+ normativeStatusInfo,
6
+ abbreviationDetails,
7
+ grammarBadges,
8
+ pronunciationLabel,
9
+ pronunciationTooltip,
10
+ termTypeInfo,
11
+ sourceStatusInfo,
12
+ sourceTypeInfo,
13
+ } from '../utils/designation-registry';
14
+
15
+ describe('designationTypeInfo', () => {
16
+ it('returns info for expression', () => {
17
+ const d = Expression.fromJSON({ type: 'expression', designation: 'test' });
18
+ const info = designationTypeInfo(d);
19
+ expect(info.label).toBe('expression');
20
+ expect(info.color).toContain('sky');
21
+ });
22
+
23
+ it('returns info for abbreviation', () => {
24
+ const d = Abbreviation.fromJSON({ type: 'abbreviation', designation: 'ISO' });
25
+ const info = designationTypeInfo(d);
26
+ expect(info.label).toBe('abbreviation');
27
+ expect(info.color).toContain('amber');
28
+ });
29
+
30
+ it('returns info for symbol with broader hierarchy', () => {
31
+ const d = { type: 'letter_symbol', designation: 'x' } as any;
32
+ const info = designationTypeInfo(d);
33
+ expect(info.label).toBe('letter symbol');
34
+ });
35
+
36
+ it('returns fallback for unknown type', () => {
37
+ const d = { type: 'custom', designation: 'x' } as any;
38
+ const info = designationTypeInfo(d);
39
+ expect(info.label).toBe('custom');
40
+ });
41
+ });
42
+
43
+ describe('normativeStatusInfo', () => {
44
+ it('returns preferred', () => {
45
+ const info = normativeStatusInfo('preferred');
46
+ expect(info.label).toBe('preferred');
47
+ expect(info.color).toContain('emerald');
48
+ });
49
+
50
+ it('returns deprecated', () => {
51
+ const info = normativeStatusInfo('deprecated');
52
+ expect(info.label).toBe('deprecated');
53
+ expect(info.color).toContain('red');
54
+ });
55
+
56
+ it('returns empty for null', () => {
57
+ const info = normativeStatusInfo(null);
58
+ expect(info.label).toBe('');
59
+ });
60
+ });
61
+
62
+ describe('sourceStatusInfo', () => {
63
+ it('returns identical status', () => {
64
+ const info = sourceStatusInfo('identical');
65
+ expect(info.label).toBe('identical');
66
+ });
67
+
68
+ it('returns modified status', () => {
69
+ const info = sourceStatusInfo('modified');
70
+ expect(info.label).toBe('modified');
71
+ expect(info.definition).toBeTruthy();
72
+ });
73
+
74
+ it('returns empty for null', () => {
75
+ const info = sourceStatusInfo(null);
76
+ expect(info.label).toBe('');
77
+ });
78
+ });
79
+
80
+ describe('sourceTypeInfo', () => {
81
+ it('returns authoritative', () => {
82
+ const info = sourceTypeInfo('authoritative');
83
+ expect(info.label).toBe('authoritative');
84
+ expect(info.color).toContain('purple');
85
+ });
86
+
87
+ it('returns lineage', () => {
88
+ const info = sourceTypeInfo('lineage');
89
+ expect(info.label).toBe('lineage');
90
+ expect(info.color).toContain('blue');
91
+ });
92
+ });
93
+
94
+ describe('termTypeInfo', () => {
95
+ it('returns term type with definition', () => {
96
+ const info = termTypeInfo('acronym');
97
+ expect(info.label).toBe('acronym');
98
+ expect(info.definition).toBeTruthy();
99
+ expect(info.category).toBe('abbreviation');
100
+ });
101
+
102
+ it('returns empty for null', () => {
103
+ const info = termTypeInfo(null);
104
+ expect(info.label).toBe('');
105
+ });
106
+ });
107
+
108
+ describe('abbreviationDetails', () => {
109
+ it('identifies acronym', () => {
110
+ const d = Abbreviation.fromJSON({ type: 'abbreviation', designation: 'ISO', acronym: true });
111
+ expect(abbreviationDetails(d)).toContain('acronym');
112
+ expect(abbreviationDetails(d)).not.toContain('initialism');
113
+ });
114
+
115
+ it('identifies initialism', () => {
116
+ const d = Abbreviation.fromJSON({ type: 'abbreviation', designation: 'UN', initialism: true });
117
+ expect(abbreviationDetails(d)).toContain('initialism');
118
+ });
119
+
120
+ it('identifies truncation', () => {
121
+ const d = Abbreviation.fromJSON({ type: 'abbreviation', designation: 'info', truncation: true });
122
+ expect(abbreviationDetails(d)).toContain('truncation');
123
+ });
124
+ });
125
+
126
+ describe('grammarBadges', () => {
127
+ it('returns gender badge with ontology label', () => {
128
+ const d = Expression.fromJSON({ type: 'expression', designation: 'test', grammar_info: [{ gender: 'f' }] });
129
+ const badges = grammarBadges((d as any).grammarInfo[0]);
130
+ expect(badges).toEqual([{ label: 'feminine', definition: 'Feminine grammatical gender.' }]);
131
+ });
132
+
133
+ it('returns gender and number badges', () => {
134
+ const d = Expression.fromJSON({ type: 'expression', designation: 'test', grammar_info: [{ gender: 'm', number: 'singular' }] });
135
+ const badges = grammarBadges((d as any).grammarInfo[0]);
136
+ expect(badges[0].label).toBe('masculine');
137
+ expect(badges[1].label).toBe('singular');
138
+ });
139
+ });
140
+
141
+ describe('pronunciationLabel', () => {
142
+ it('shows content with system', () => {
143
+ const p = { content: '/tɛst/', system: 'IPA', language: null, script: null, country: null } as any;
144
+ expect(pronunciationLabel(p)).toBe('/tɛst/ (IPA)');
145
+ });
146
+
147
+ it('shows content only', () => {
148
+ const p = { content: '/t/', system: null, language: null, script: null, country: null } as any;
149
+ expect(pronunciationLabel(p)).toBe('/t/');
150
+ });
151
+ });
152
+
153
+ describe('pronunciationTooltip', () => {
154
+ it('includes all metadata', () => {
155
+ const p = { content: '/t/', system: 'IPA', language: 'en', script: 'Latn', country: 'US' } as any;
156
+ const tip = pronunciationTooltip(p);
157
+ expect(tip).toContain('Language: en');
158
+ expect(tip).toContain('System: IPA');
159
+ expect(tip).toContain('Country: US');
160
+ });
161
+ });
@@ -0,0 +1,98 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import { createPinia, setActivePinia } from 'pinia';
4
+ import { createRouter, createMemoryHistory } from 'vue-router';
5
+ import FormatDownloads from '../components/FormatDownloads.vue';
6
+
7
+ async function createTestRouter() {
8
+ return createRouter({
9
+ history: createMemoryHistory(),
10
+ routes: [
11
+ { path: '/', name: 'home', component: { template: '<div/>' } },
12
+ ],
13
+ });
14
+ }
15
+
16
+ describe('FormatDownloads', () => {
17
+ it('renders download links for known formats', async () => {
18
+ const pinia = createPinia();
19
+ setActivePinia(pinia);
20
+ const router = await createTestRouter();
21
+ const wrapper = mount(FormatDownloads, {
22
+ global: { plugins: [pinia, router] },
23
+ props: { registerId: 'test', conceptId: '3.1.1.1', formats: ['ttl', 'jsonld'] },
24
+ });
25
+ expect(wrapper.text()).toContain('Downloads');
26
+ expect(wrapper.text()).toContain('Turtle RDF');
27
+ expect(wrapper.text()).toContain('JSON-LD');
28
+ });
29
+
30
+ it('generates correct URLs', async () => {
31
+ const pinia = createPinia();
32
+ setActivePinia(pinia);
33
+ const router = await createTestRouter();
34
+ const wrapper = mount(FormatDownloads, {
35
+ global: { plugins: [pinia, router] },
36
+ props: { registerId: 'test', conceptId: '3.1.1.1', formats: ['ttl'] },
37
+ });
38
+ const link = wrapper.find('a');
39
+ expect(link.attributes('href')).toBe('/data/test/concepts/3.1.1.1.ttl');
40
+ });
41
+
42
+ it('filters unknown formats', async () => {
43
+ const pinia = createPinia();
44
+ setActivePinia(pinia);
45
+ const router = await createTestRouter();
46
+ const wrapper = mount(FormatDownloads, {
47
+ global: { plugins: [pinia, router] },
48
+ props: { registerId: 'test', conceptId: '1', formats: ['ttl', 'unknown'] },
49
+ });
50
+ const links = wrapper.findAll('a');
51
+ expect(links.length).toBe(1);
52
+ });
53
+
54
+ it('renders nothing when all formats are unknown', async () => {
55
+ const pinia = createPinia();
56
+ setActivePinia(pinia);
57
+ const router = await createTestRouter();
58
+ const wrapper = mount(FormatDownloads, {
59
+ global: { plugins: [pinia, router] },
60
+ props: { registerId: 'test', conceptId: '1', formats: ['unknown'] },
61
+ });
62
+ expect(wrapper.find('a').exists()).toBe(false);
63
+ });
64
+
65
+ it('renders nothing when formats array is empty', async () => {
66
+ const pinia = createPinia();
67
+ setActivePinia(pinia);
68
+ const router = await createTestRouter();
69
+ const wrapper = mount(FormatDownloads, {
70
+ global: { plugins: [pinia, router] },
71
+ props: { registerId: 'test', conceptId: '1', formats: [] },
72
+ });
73
+ expect(wrapper.find('a').exists()).toBe(false);
74
+ });
75
+
76
+ it('sets download attribute on links', async () => {
77
+ const pinia = createPinia();
78
+ setActivePinia(pinia);
79
+ const router = await createTestRouter();
80
+ const wrapper = mount(FormatDownloads, {
81
+ global: { plugins: [pinia, router] },
82
+ props: { registerId: 'test', conceptId: '3.1.1.1', formats: ['jsonld'] },
83
+ });
84
+ const link = wrapper.find('a');
85
+ expect(link.attributes('download')).toBe('3.1.1.1.jsonld');
86
+ });
87
+
88
+ it('shows TBX-XML format', async () => {
89
+ const pinia = createPinia();
90
+ setActivePinia(pinia);
91
+ const router = await createTestRouter();
92
+ const wrapper = mount(FormatDownloads, {
93
+ global: { plugins: [pinia, router] },
94
+ props: { registerId: 'test', conceptId: '1', formats: ['tbx'] },
95
+ });
96
+ expect(wrapper.text()).toContain('TBX-XML');
97
+ });
98
+ });
@@ -0,0 +1,69 @@
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 GraphView from '../views/GraphView.vue';
6
+ import { useVocabularyStore } from '../stores/vocabulary';
7
+
8
+ async function createTestRouter() {
9
+ const router = createRouter({
10
+ history: createMemoryHistory(),
11
+ routes: [
12
+ { path: '/', name: 'home', component: { template: '<div/>' } },
13
+ { path: '/graph', name: 'graph', component: { template: '<div/>' } },
14
+ ],
15
+ });
16
+ router.push('/graph');
17
+ await router.isReady();
18
+ return router;
19
+ }
20
+
21
+ describe('GraphView', () => {
22
+ let pinia: ReturnType<typeof createPinia>;
23
+ let router: Awaited<ReturnType<typeof createTestRouter>>;
24
+
25
+ beforeEach(async () => {
26
+ pinia = createPinia();
27
+ setActivePinia(pinia);
28
+ router = await createTestRouter();
29
+ });
30
+
31
+ function mountGraph() {
32
+ return mount(GraphView, {
33
+ global: {
34
+ plugins: [pinia, router],
35
+ stubs: { GraphPanel: true },
36
+ },
37
+ });
38
+ }
39
+
40
+ it('renders breadcrumb navigation', () => {
41
+ const wrapper = mountGraph();
42
+ expect(wrapper.text()).toContain('Home');
43
+ expect(wrapper.text()).toContain('Graph View');
44
+ });
45
+
46
+ it('shows loading spinner initially', () => {
47
+ const wrapper = mountGraph();
48
+ expect(wrapper.find('.animate-spin').exists()).toBe(true);
49
+ expect(wrapper.text()).toContain('Loading graph data');
50
+ });
51
+
52
+ it('renders GraphPanel after loading', async () => {
53
+ const wrapper = mountGraph();
54
+ await flushPromises();
55
+ expect(wrapper.findComponent({ name: 'GraphPanel' }).exists()).toBe(true);
56
+ });
57
+
58
+ it('shows edge count after loading', async () => {
59
+ const wrapper = mountGraph();
60
+ await flushPromises();
61
+ expect(wrapper.text()).toContain('edges');
62
+ });
63
+
64
+ it('has full-height container', () => {
65
+ const wrapper = mountGraph();
66
+ const container = wrapper.find('.flex.flex-col');
67
+ expect(container.exists()).toBe(true);
68
+ });
69
+ });
@@ -91,6 +91,37 @@ describe('GraphEngine', () => {
91
91
  expect(g.edgeCount).toBe(1);
92
92
  });
93
93
 
94
+ it('keeps separate edges for different languages', () => {
95
+ const g = new GraphEngine();
96
+ g.addEdge({ source: 'uri:a', target: 'uri:b', type: 'references', register: 'test', lang: 'eng' });
97
+ g.addEdge({ source: 'uri:a', target: 'uri:b', type: 'references', register: 'test', lang: 'fra' });
98
+ expect(g.edgeCount).toBe(2);
99
+ });
100
+
101
+ it('deduplicates edges with same source+target+type+lang', () => {
102
+ const g = new GraphEngine();
103
+ g.addEdge({ source: 'uri:a', target: 'uri:b', type: 'references', register: 'test', lang: 'eng' });
104
+ g.addEdge({ source: 'uri:a', target: 'uri:b', type: 'references', register: 'test', lang: 'eng' });
105
+ expect(g.edgeCount).toBe(1);
106
+ });
107
+
108
+ it('creates domain stub with correct fields', () => {
109
+ const g = new GraphEngine();
110
+ g.addEdge({
111
+ source: 'https://glossarist.org/isotc211/concept/3',
112
+ target: 'https://glossarist.org/isotc211/domain/iso-19105',
113
+ type: 'domain',
114
+ label: 'ISO 19105',
115
+ register: 'isotc211',
116
+ lang: 'eng',
117
+ });
118
+ const domainNode = g.getNode('https://glossarist.org/isotc211/domain/iso-19105');
119
+ expect(domainNode?.register).toBe('isotc211');
120
+ expect(domainNode?.nodeType).toBe('domain');
121
+ expect(domainNode?.status).toBe('domain');
122
+ expect(domainNode?.loaded).toBe(false);
123
+ });
124
+
94
125
  it('extracts register from URI for stub nodes', () => {
95
126
  const g = new GraphEngine();
96
127
  g.addEdge({
@@ -172,6 +203,37 @@ describe('GraphEngine', () => {
172
203
  const sub = g.getSubgraph('uri:a', 5);
173
204
  expect(sub.nodes.length).toBe(2);
174
205
  });
206
+
207
+ it('does not traverse past domain nodes in getSubgraph', () => {
208
+ const g = new GraphEngine();
209
+ g.addNode(makeNode('https://glossarist.org/test/concept/a', 'a'));
210
+ g.addNode(makeNode('https://glossarist.org/test/concept/b', 'b'));
211
+ g.addNode(makeNode('https://glossarist.org/test/concept/c', 'c'));
212
+ g.addNode(makeNode('https://glossarist.org/test/concept/d', 'd'));
213
+
214
+ g.addEdge({
215
+ source: 'https://glossarist.org/test/concept/a',
216
+ target: 'https://glossarist.org/test/domain/iso-12345',
217
+ type: 'domain', register: 'test', label: 'ISO 12345', lang: 'eng',
218
+ });
219
+ g.addEdge({
220
+ source: 'https://glossarist.org/test/concept/b',
221
+ target: 'https://glossarist.org/test/domain/iso-12345',
222
+ type: 'domain', register: 'test', label: 'ISO 12345', lang: 'eng',
223
+ });
224
+ g.addEdge({
225
+ source: 'https://glossarist.org/test/concept/c',
226
+ target: 'https://glossarist.org/test/domain/iso-12345',
227
+ type: 'domain', register: 'test', label: 'ISO 12345', lang: 'eng',
228
+ });
229
+
230
+ const sub = g.getSubgraph('https://glossarist.org/test/concept/a', 3);
231
+ const nodeUris = sub.nodes.map(n => n.uri);
232
+ expect(nodeUris).toContain('https://glossarist.org/test/concept/a');
233
+ expect(nodeUris).toContain('https://glossarist.org/test/domain/iso-12345');
234
+ expect(nodeUris).not.toContain('https://glossarist.org/test/concept/b');
235
+ expect(nodeUris).not.toContain('https://glossarist.org/test/concept/c');
236
+ });
175
237
  });
176
238
 
177
239
  describe('getAllNodes', () => {
@@ -0,0 +1,157 @@
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 HomeView from '../views/HomeView.vue';
6
+ import { useVocabularyStore } from '../stores/vocabulary';
7
+ import type { Manifest } from '../adapters/types';
8
+
9
+ function makeManifest(overrides: Partial<Manifest> = {}): Manifest {
10
+ return {
11
+ id: 'test',
12
+ datasetUri: 'https://glossarist.org/test/concept',
13
+ title: 'Test Dataset',
14
+ description: 'A test dataset',
15
+ owner: 'ISO',
16
+ baseUrl: '/data/test',
17
+ languages: ['eng', 'fra'],
18
+ conceptCount: 100,
19
+ conceptUrlTemplate: '/data/test/concepts/{id}.json',
20
+ indexUrl: '/data/test/index.json',
21
+ contextUrl: '/data/test/context.json',
22
+ uriBase: 'https://glossarist.org',
23
+ status: 'published',
24
+ schemaVersion: '1.0',
25
+ tags: [],
26
+ lastUpdated: '2025-01-01',
27
+ sourceRepo: 'https://example.com/repo',
28
+ chunkSize: 1000,
29
+ color: '#3366ff',
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ async function createTestRouter() {
35
+ const router = createRouter({
36
+ history: createMemoryHistory(),
37
+ routes: [
38
+ { path: '/', name: 'home', component: { template: '<div/>' } },
39
+ { path: '/dataset/:registerId', name: 'dataset', component: { template: '<div/>' } },
40
+ { path: '/search', name: 'search', component: { template: '<div/>' } },
41
+ { path: '/graph', name: 'graph', component: { template: '<div/>' } },
42
+ { path: '/dataset/:registerId/concept/:conceptId', name: 'concept', component: { template: '<div/>' } },
43
+ ],
44
+ });
45
+ router.push('/');
46
+ await router.isReady();
47
+ return router;
48
+ }
49
+
50
+ describe('HomeView interactions', () => {
51
+ let pinia: ReturnType<typeof createPinia>;
52
+ let router: Awaited<ReturnType<typeof createTestRouter>>;
53
+
54
+ beforeEach(async () => {
55
+ pinia = createPinia();
56
+ setActivePinia(pinia);
57
+ router = await createTestRouter();
58
+ });
59
+
60
+ function mountHome() {
61
+ return mount(HomeView, {
62
+ global: { plugins: [pinia, router] },
63
+ });
64
+ }
65
+
66
+ it('renders the welcome hero section', () => {
67
+ const wrapper = mountHome();
68
+ expect(wrapper.find('h1').text()).toContain('Glossarist');
69
+ });
70
+
71
+ it('renders Search, Graph View, and Surprise Me buttons', () => {
72
+ const wrapper = mountHome();
73
+ const buttons = wrapper.findAll('button');
74
+ const texts = buttons.map(b => b.text());
75
+ expect(texts.some(t => t.includes('Search'))).toBe(true);
76
+ expect(texts.some(t => t.includes('Graph View'))).toBe(true);
77
+ expect(texts.some(t => t.includes('Surprise Me'))).toBe(true);
78
+ });
79
+
80
+ it('navigates to search on Search button click', async () => {
81
+ const wrapper = mountHome();
82
+ const searchBtn = wrapper.findAll('button').find(b => b.text().includes('Search'));
83
+ await searchBtn!.trigger('click');
84
+ await flushPromises();
85
+ expect(router.currentRoute.value.name).toBe('search');
86
+ });
87
+
88
+ it('navigates to graph on Graph View button click', async () => {
89
+ const wrapper = mountHome();
90
+ const graphBtn = wrapper.findAll('button').find(b => b.text().includes('Graph View'));
91
+ await graphBtn!.trigger('click');
92
+ await flushPromises();
93
+ expect(router.currentRoute.value.name).toBe('graph');
94
+ });
95
+
96
+ it('shows stats counters', () => {
97
+ const store = useVocabularyStore();
98
+ store.manifests.set('test', makeManifest());
99
+ store.datasets.set('test', { index: [], getConceptCount: () => 0 } as any);
100
+
101
+ const wrapper = mountHome();
102
+ expect(wrapper.text()).toContain('Datasets');
103
+ expect(wrapper.text()).toContain('Concepts');
104
+ expect(wrapper.text()).toContain('Languages');
105
+ });
106
+
107
+ it('shows a CTA card for a single dataset', () => {
108
+ const store = useVocabularyStore();
109
+ store.manifests.set('test', makeManifest());
110
+ store.datasets.set('test', { index: [], getConceptCount: () => 0 } as any);
111
+
112
+ const wrapper = mountHome();
113
+ expect(wrapper.text()).toContain('Test Dataset');
114
+ expect(wrapper.text()).toContain('Browse concepts');
115
+ });
116
+
117
+ it('navigates to dataset on CTA click', async () => {
118
+ const store = useVocabularyStore();
119
+ store.manifests.set('test', makeManifest());
120
+ store.datasets.set('test', { index: [], getConceptCount: () => 0 } as any);
121
+
122
+ const wrapper = mountHome();
123
+ const cta = wrapper.findAll('button').find(b => b.text().includes('Browse concepts'));
124
+ await cta!.trigger('click');
125
+ await flushPromises();
126
+ expect(router.currentRoute.value.name).toBe('dataset');
127
+ expect(router.currentRoute.value.params.registerId).toBe('test');
128
+ });
129
+
130
+ it('shows dataset cards for multiple datasets', () => {
131
+ const store = useVocabularyStore();
132
+ store.manifests.set('ds1', makeManifest({ id: 'ds1', title: 'Dataset One' }));
133
+ store.manifests.set('ds2', makeManifest({ id: 'ds2', title: 'Dataset Two' }));
134
+ store.datasets.set('ds1', { index: [], getConceptCount: () => 0 } as any);
135
+ store.datasets.set('ds2', { index: [], getConceptCount: () => 0 } as any);
136
+
137
+ const wrapper = mountHome();
138
+ expect(wrapper.text()).toContain('Dataset One');
139
+ expect(wrapper.text()).toContain('Dataset Two');
140
+ expect(wrapper.text()).toContain('Available Datasets');
141
+ });
142
+
143
+ it('navigates to dataset on card click', async () => {
144
+ const store = useVocabularyStore();
145
+ store.manifests.set('ds1', makeManifest({ id: 'ds1', title: 'Dataset One' }));
146
+ store.manifests.set('ds2', makeManifest({ id: 'ds2', title: 'Dataset Two' }));
147
+ store.datasets.set('ds1', { index: [], getConceptCount: () => 0 } as any);
148
+ store.datasets.set('ds2', { index: [], getConceptCount: () => 0 } as any);
149
+
150
+ const wrapper = mountHome();
151
+ const card = wrapper.findAll('button').find(b => b.text().includes('Dataset One'));
152
+ await card!.trigger('click');
153
+ await flushPromises();
154
+ expect(router.currentRoute.value.name).toBe('dataset');
155
+ expect(router.currentRoute.value.params.registerId).toBe('ds1');
156
+ });
157
+ });