@glossarist/concept-browser 0.3.7 → 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 (54) 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__/concept-card.test.ts +1 -1
  10. package/src/__tests__/concept-detail-interaction.test.ts +40 -18
  11. package/src/__tests__/concept-formats.test.ts +32 -30
  12. package/src/__tests__/concept-timeline.test.ts +108 -83
  13. package/src/__tests__/concept-view.test.ts +15 -2
  14. package/src/__tests__/dataset-adapter.test.ts +172 -23
  15. package/src/__tests__/dataset-view.test.ts +6 -5
  16. package/src/__tests__/designation-registry.test.ts +161 -0
  17. package/src/__tests__/graph.test.ts +62 -0
  18. package/src/__tests__/language-detail.test.ts +117 -60
  19. package/src/__tests__/ontology-registry.test.ts +109 -0
  20. package/src/__tests__/relationship-categories.test.ts +62 -0
  21. package/src/__tests__/test-helpers.ts +11 -8
  22. package/src/adapters/DatasetAdapter.ts +171 -48
  23. package/src/adapters/model-bridge.ts +277 -0
  24. package/src/adapters/ontology-registry.ts +75 -0
  25. package/src/adapters/ontology-schema.ts +100 -0
  26. package/src/adapters/types.ts +52 -77
  27. package/src/components/AppSidebar.vue +1 -1
  28. package/src/components/CitationDisplay.vue +35 -0
  29. package/src/components/ConceptDetail.vue +334 -93
  30. package/src/components/ConceptRdfView.vue +397 -0
  31. package/src/components/ConceptTimeline.vue +56 -52
  32. package/src/components/GraphPanel.vue +96 -31
  33. package/src/components/LanguageDetail.vue +45 -37
  34. package/src/components/NavIcon.vue +1 -0
  35. package/src/components/NonVerbalRepDisplay.vue +38 -0
  36. package/src/components/RelationshipList.vue +99 -0
  37. package/src/config/use-site-config.ts +3 -0
  38. package/src/data/ontology-schema.json +1551 -0
  39. package/src/data/taxonomies.json +543 -0
  40. package/src/graph/GraphEngine.ts +7 -4
  41. package/src/router/index.ts +5 -0
  42. package/src/shims/empty.ts +1 -0
  43. package/src/shims/node-crypto.ts +6 -0
  44. package/src/shims/node-path.ts +10 -0
  45. package/src/stores/vocabulary.ts +75 -25
  46. package/src/style.css +74 -20
  47. package/src/utils/concept-formats.ts +22 -20
  48. package/src/utils/concept-helpers.ts +43 -23
  49. package/src/utils/designation-registry.ts +124 -0
  50. package/src/utils/relationship-categories.ts +84 -0
  51. package/src/views/OntologySchemaView.vue +302 -0
  52. package/src/views/PageView.vue +28 -17
  53. package/src/views/StatsView.vue +34 -12
  54. package/vite.config.ts +8 -0
@@ -1,23 +1,30 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { mount, flushPromises } from '@vue/test-utils';
3
- import { createPinia, setActivePinia } from 'pinia';
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
4
3
  import ConceptTimeline from '../components/ConceptTimeline.vue';
5
- import type { LocalizedConcept } from '../adapters/types';
4
+ import { conceptFromJson } from '../adapters/model-bridge';
6
5
 
7
- function makeLocalizedConcept(overrides: Partial<LocalizedConcept> = {}): LocalizedConcept {
6
+ function makeConceptJson(overrides: Record<string, any> = {}) {
8
7
  return {
9
- '@id': 'https://glossarist.org/test/concept/1/eng',
10
- '@type': 'gl:LocalizedConcept',
11
- 'gl:languageCode': 'eng',
12
- 'gl:entryStatus': 'valid',
13
- ...overrides,
8
+ '@id': 'https://glossarist.org/test/concept/1',
9
+ '@type': 'gl:Concept',
10
+ 'gl:identifier': '1',
11
+ 'gl:localizedConcept': {
12
+ eng: {
13
+ '@type': 'gl:LocalizedConcept',
14
+ 'gl:languageCode': 'eng',
15
+ 'gl:entryStatus': 'valid',
16
+ ...overrides.eng,
17
+ },
18
+ ...(overrides.otherLangs || {}),
19
+ },
14
20
  };
15
21
  }
16
22
 
17
- function mountTimeline(lcs: Record<string, LocalizedConcept>, activeLang = 'eng', languageOrder?: string[]) {
23
+ function mountTimeline(conceptJson: Record<string, any>, activeLang = 'eng', languageOrder?: string[]) {
24
+ const concept = conceptFromJson(conceptJson);
18
25
  return mount(ConceptTimeline, {
19
26
  props: {
20
- localizedConcepts: lcs,
27
+ concept,
21
28
  activeLang,
22
29
  languageOrder,
23
30
  },
@@ -25,40 +32,43 @@ function mountTimeline(lcs: Record<string, LocalizedConcept>, activeLang = 'eng'
25
32
  }
26
33
 
27
34
  describe('ConceptTimeline', () => {
28
- it('shows empty state when no history data', () => {
29
- const lc = makeLocalizedConcept({ 'gl:entryStatus': undefined });
30
- const wrapper = mountTimeline({ eng: lc });
31
- expect(wrapper.text()).toContain('No history data available');
35
+ it('shows review metadata even without history entries', () => {
36
+ const wrapper = mountTimeline(makeConceptJson());
37
+ expect(wrapper.text()).toContain('Review Details');
38
+ expect(wrapper.text()).toContain('valid');
32
39
  });
33
40
 
34
41
  it('renders review date as timeline entry', () => {
35
- const lc = makeLocalizedConcept({
36
- 'gl:reviewDate': '2023-05-15',
42
+ const json = makeConceptJson({
43
+ eng: { 'gl:reviewDate': '2023-05-15' },
37
44
  });
38
- const wrapper = mountTimeline({ eng: lc });
45
+ const wrapper = mountTimeline(json);
39
46
  expect(wrapper.text()).toContain('Review initiated');
40
47
  expect(wrapper.text()).toContain('2023');
41
48
  });
42
49
 
43
50
  it('renders review decision event', () => {
44
- const lc = makeLocalizedConcept({
45
- 'gl:reviewDecisionEvent': 'Accepted',
46
- 'gl:reviewDate': '2023-05-15',
47
- 'gl:reviewDecisionDate': '2023-06-01',
51
+ const json = makeConceptJson({
52
+ eng: {
53
+ 'gl:reviewDecisionEvent': 'Accepted',
54
+ 'gl:reviewDate': '2023-05-15',
55
+ 'gl:reviewDecisionDate': '2023-06-01',
56
+ },
48
57
  });
49
- const wrapper = mountTimeline({ eng: lc });
50
- // The review event banner
58
+ const wrapper = mountTimeline(json);
51
59
  expect(wrapper.text()).toContain('Accepted');
52
60
  });
53
61
 
54
62
  it('renders gl:dates entries', () => {
55
- const lc = makeLocalizedConcept({
56
- 'gl:dates': [
57
- { 'gl:dateType': 'accepted', 'gl:date': '2020-01-01' },
58
- { 'gl:dateType': 'amended', 'gl:date': '2022-06-15' },
59
- ],
63
+ const json = makeConceptJson({
64
+ eng: {
65
+ 'gl:dates': [
66
+ { 'gl:dateType': 'accepted', 'gl:date': '2020-01-01' },
67
+ { 'gl:dateType': 'amended', 'gl:date': '2022-06-15' },
68
+ ],
69
+ },
60
70
  });
61
- const wrapper = mountTimeline({ eng: lc });
71
+ const wrapper = mountTimeline(json);
62
72
  expect(wrapper.text()).toContain('Concept accepted');
63
73
  expect(wrapper.text()).toContain('Definition amended');
64
74
  expect(wrapper.text()).toContain('2020');
@@ -66,48 +76,58 @@ describe('ConceptTimeline', () => {
66
76
  });
67
77
 
68
78
  it('sorts entries by date ascending', () => {
69
- const lc = makeLocalizedConcept({
70
- 'gl:dates': [
71
- { 'gl:dateType': 'amended', 'gl:date': '2022-06-15' },
72
- { 'gl:dateType': 'accepted', 'gl:date': '2020-01-01' },
73
- ],
79
+ const json = makeConceptJson({
80
+ eng: {
81
+ 'gl:dates': [
82
+ { 'gl:dateType': 'amended', 'gl:date': '2022-06-15' },
83
+ { 'gl:dateType': 'accepted', 'gl:date': '2020-01-01' },
84
+ ],
85
+ },
74
86
  });
75
- const wrapper = mountTimeline({ eng: lc });
87
+ const wrapper = mountTimeline(json);
76
88
  const texts = wrapper.text();
77
- // "accepted" entry should appear before "amended" in the rendered output
78
89
  const acceptedIdx = texts.indexOf('Concept accepted');
79
90
  const amendedIdx = texts.indexOf('Definition amended');
80
91
  expect(acceptedIdx).toBeLessThan(amendedIdx);
81
92
  });
82
93
 
83
94
  it('shows language selector when multiple languages have history', () => {
84
- const eng = makeLocalizedConcept({ 'gl:reviewDate': '2023-01-01' });
85
- const fra = makeLocalizedConcept({
86
- '@id': 'https://glossarist.org/test/concept/1/fra',
87
- 'gl:languageCode': 'fra',
88
- 'gl:reviewDate': '2023-02-01',
95
+ const json = makeConceptJson({
96
+ eng: { 'gl:reviewDate': '2023-01-01' },
97
+ otherLangs: {
98
+ fra: {
99
+ '@type': 'gl:LocalizedConcept',
100
+ 'gl:languageCode': 'fra',
101
+ 'gl:reviewDate': '2023-02-01',
102
+ },
103
+ },
89
104
  });
90
- const wrapper = mountTimeline({ eng, fra }, 'eng', ['eng', 'fra']);
105
+ const wrapper = mountTimeline(json, 'eng', ['eng', 'fra']);
91
106
  expect(wrapper.text()).toContain('French');
92
107
  });
93
108
 
94
109
  it('does not show language selector for single-language history', () => {
95
- const eng = makeLocalizedConcept({ 'gl:reviewDate': '2023-01-01' });
96
- const wrapper = mountTimeline({ eng });
97
- // Should not have multiple language buttons
110
+ const json = makeConceptJson({
111
+ eng: { 'gl:reviewDate': '2023-01-01' },
112
+ });
113
+ const wrapper = mountTimeline(json);
98
114
  const buttons = wrapper.findAll('button');
99
115
  const langButtons = buttons.filter(b => b.text().includes('French'));
100
116
  expect(langButtons.length).toBe(0);
101
117
  });
102
118
 
103
119
  it('emits update:activeLang on language button click', async () => {
104
- const eng = makeLocalizedConcept({ 'gl:reviewDate': '2023-01-01' });
105
- const fra = makeLocalizedConcept({
106
- '@id': 'https://glossarist.org/test/concept/1/fra',
107
- 'gl:languageCode': 'fra',
108
- 'gl:reviewDate': '2023-02-01',
120
+ const json = makeConceptJson({
121
+ eng: { 'gl:reviewDate': '2023-01-01' },
122
+ otherLangs: {
123
+ fra: {
124
+ '@type': 'gl:LocalizedConcept',
125
+ 'gl:languageCode': 'fra',
126
+ 'gl:reviewDate': '2023-02-01',
127
+ },
128
+ },
109
129
  });
110
- const wrapper = mountTimeline({ eng, fra }, 'eng', ['eng', 'fra']);
130
+ const wrapper = mountTimeline(json, 'eng', ['eng', 'fra']);
111
131
  const fraBtn = wrapper.findAll('button').find(b => b.text().includes('French'));
112
132
  expect(fraBtn).toBeDefined();
113
133
  await fraBtn!.trigger('click');
@@ -115,14 +135,16 @@ describe('ConceptTimeline', () => {
115
135
  });
116
136
 
117
137
  it('shows review metadata when present', () => {
118
- const lc = makeLocalizedConcept({
119
- 'gl:reviewDate': '2023-01-01',
120
- 'gl:reviewStatus': 'final',
121
- 'gl:reviewDecision': 'accepted',
122
- 'gl:entryStatus': 'valid',
123
- 'gl:release': 3,
138
+ const json = makeConceptJson({
139
+ eng: {
140
+ 'gl:reviewDate': '2023-01-01',
141
+ 'gl:reviewStatus': 'final',
142
+ 'gl:reviewDecision': 'accepted',
143
+ 'gl:entryStatus': 'valid',
144
+ 'gl:release': '3',
145
+ },
124
146
  });
125
- const wrapper = mountTimeline({ eng: lc });
147
+ const wrapper = mountTimeline(json);
126
148
  expect(wrapper.text()).toContain('Review Details');
127
149
  expect(wrapper.text()).toContain('final');
128
150
  expect(wrapper.text()).toContain('accepted');
@@ -131,16 +153,17 @@ describe('ConceptTimeline', () => {
131
153
  });
132
154
 
133
155
  it('groups by year when more than 3 entries', () => {
134
- const lc = makeLocalizedConcept({
135
- 'gl:dates': [
136
- { 'gl:dateType': 'accepted', 'gl:date': '2019-03-01' },
137
- { 'gl:dateType': 'amended', 'gl:date': '2020-06-15' },
138
- { 'gl:dateType': 'amended', 'gl:date': '2021-09-20' },
139
- { 'gl:dateType': 'published', 'gl:date': '2023-01-10' },
140
- ],
156
+ const json = makeConceptJson({
157
+ eng: {
158
+ 'gl:dates': [
159
+ { 'gl:dateType': 'accepted', 'gl:date': '2019-03-01' },
160
+ { 'gl:dateType': 'amended', 'gl:date': '2020-06-15' },
161
+ { 'gl:dateType': 'amended', 'gl:date': '2021-09-20' },
162
+ { 'gl:dateType': 'published', 'gl:date': '2023-01-10' },
163
+ ],
164
+ },
141
165
  });
142
- const wrapper = mountTimeline({ eng: lc });
143
- // Year markers should be rendered
166
+ const wrapper = mountTimeline(json);
144
167
  expect(wrapper.text()).toContain('2019');
145
168
  expect(wrapper.text()).toContain('2020');
146
169
  expect(wrapper.text()).toContain('2021');
@@ -148,27 +171,29 @@ describe('ConceptTimeline', () => {
148
171
  });
149
172
 
150
173
  it('uses simple layout for 3 or fewer entries', () => {
151
- const lc = makeLocalizedConcept({
152
- 'gl:dates': [
153
- { 'gl:dateType': 'accepted', 'gl:date': '2020-01-01' },
154
- { 'gl:dateType': 'amended', 'gl:date': '2022-06-15' },
155
- ],
174
+ const json = makeConceptJson({
175
+ eng: {
176
+ 'gl:dates': [
177
+ { 'gl:dateType': 'accepted', 'gl:date': '2020-01-01' },
178
+ { 'gl:dateType': 'amended', 'gl:date': '2022-06-15' },
179
+ ],
180
+ },
156
181
  });
157
- const wrapper = mountTimeline({ eng: lc });
158
- // Should have entries but no year grouping markers
182
+ const wrapper = mountTimeline(json);
159
183
  expect(wrapper.text()).toContain('Concept accepted');
160
184
  expect(wrapper.text()).toContain('Definition amended');
161
185
  });
162
186
 
163
187
  it('deduplicates review date if it matches a gl:date entry', () => {
164
- const lc = makeLocalizedConcept({
165
- 'gl:dates': [
166
- { 'gl:dateType': 'review', 'gl:date': '2023-05-15' },
167
- ],
168
- 'gl:reviewDate': '2023-05-15',
188
+ const json = makeConceptJson({
189
+ eng: {
190
+ 'gl:dates': [
191
+ { 'gl:dateType': 'review', 'gl:date': '2023-05-15' },
192
+ ],
193
+ 'gl:reviewDate': '2023-05-15',
194
+ },
169
195
  });
170
- const wrapper = mountTimeline({ eng: lc });
171
- // Should only have one entry for that date, not two "review" entries
196
+ const wrapper = mountTimeline(json);
172
197
  const reviewCount = wrapper.text().split('Review initiated').length - 1;
173
198
  expect(reviewCount).toBeLessThanOrEqual(1);
174
199
  });
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { mount, flushPromises } from '@vue/test-utils';
3
3
  import ConceptView from '../views/ConceptView.vue';
4
4
  import { useVocabularyStore } from '../stores/vocabulary';
5
+ import { conceptFromJson } from '../adapters/model-bridge';
5
6
  import { createTestRouter, setupPinia, makeManifest, makeAdapterStub } from './test-helpers';
6
7
 
7
8
  describe('ConceptView', () => {
@@ -52,10 +53,22 @@ describe('ConceptView', () => {
52
53
  });
53
54
 
54
55
  it('renders ConceptDetail when concept loads', async () => {
55
- const concept = {
56
+ const concept = conceptFromJson({
56
57
  '@id': 'https://glossarist.org/test/concept/1',
57
58
  '@type': 'gl:Concept',
58
- };
59
+ 'gl:identifier': '1',
60
+ 'gl:localizedConcept': {
61
+ eng: {
62
+ '@type': 'gl:LocalizedConcept',
63
+ 'gl:languageCode': 'eng',
64
+ 'gl:entryStatus': 'valid',
65
+ 'gl:designation': [
66
+ { '@type': 'gl:Expression', 'gl:term': 'test', 'gl:normativeStatus': 'preferred' },
67
+ ],
68
+ 'gl:definition': [{ '@type': 'gl:DetailedDefinition', 'gl:content': 'A test concept.' }],
69
+ },
70
+ },
71
+ });
59
72
  const store = useVocabularyStore();
60
73
  store.datasets.set('test', makeAdapterStub({ fetchConcept: () => Promise.resolve(concept) }));
61
74
 
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { DatasetAdapter } from '../adapters/DatasetAdapter';
3
+ import { conceptFromJson } from '../adapters/model-bridge';
3
4
 
4
5
  // Mock fetch globally
5
6
  const mockFetch = vi.fn();
@@ -98,13 +99,13 @@ describe('DatasetAdapter', () => {
98
99
  mockFetch.mockReturnValue(mockJsonResponse(concept));
99
100
 
100
101
  const result = await adapter.fetchConcept('103-01-02');
101
- expect(result['gl:identifier']).toBe('103-01-02');
102
+ expect(result.id).toBe('103-01-02');
102
103
  expect(mockFetch).toHaveBeenCalledWith('/data/test/concepts/103-01-02.json');
103
104
 
104
105
  // Second call should use cache
105
106
  mockFetch.mockReset();
106
107
  const cached = await adapter.fetchConcept('103-01-02');
107
- expect(cached['gl:identifier']).toBe('103-01-02');
108
+ expect(cached.id).toBe('103-01-02');
108
109
  expect(mockFetch).not.toHaveBeenCalled();
109
110
  });
110
111
 
@@ -195,12 +196,56 @@ describe('DatasetAdapter', () => {
195
196
  const hits = adapter.search('xyznotfound');
196
197
  expect(hits.length).toBe(0);
197
198
  });
199
+
200
+ it('ranks exact matches above starts-with above contains', async () => {
201
+ const index = {
202
+ registerId: 'test',
203
+ schemaVersion: '1.0.0',
204
+ conceptCount: 3,
205
+ chunkSize: 500,
206
+ chunks: [],
207
+ concepts: [
208
+ { id: '1', designations: { eng: 'mass' }, eng: 'mass', status: 'valid' },
209
+ { id: '2', designations: { eng: 'mass flow rate' }, eng: 'mass flow rate', status: 'valid' },
210
+ { id: '3', designations: { eng: 'center of mass' }, eng: 'center of mass', status: 'valid' },
211
+ ],
212
+ };
213
+ mockFetch.mockReturnValue(mockJsonResponse(index));
214
+ await adapter.loadIndex();
215
+
216
+ const hits = adapter.search('mass');
217
+ // Exact match first, then starts-with, then contains
218
+ expect(hits[0].conceptId).toBe('1');
219
+ expect(hits[1].conceptId).toBe('2');
220
+ expect(hits[2].conceptId).toBe('3');
221
+ });
222
+
223
+ it('ranks ID exact match highest', async () => {
224
+ const index = {
225
+ registerId: 'test',
226
+ schemaVersion: '1.0.0',
227
+ conceptCount: 2,
228
+ chunkSize: 500,
229
+ chunks: [],
230
+ concepts: [
231
+ { id: '102-01-01', designations: { eng: 'field' }, eng: 'field', status: 'valid' },
232
+ { id: '102-01-02', designations: { eng: 'electromagnetic field' }, eng: 'electromagnetic field', status: 'valid' },
233
+ ],
234
+ };
235
+ mockFetch.mockReturnValue(mockJsonResponse(index));
236
+ await adapter.loadIndex();
237
+
238
+ const hits = adapter.search('102-01-01');
239
+ expect(hits[0].conceptId).toBe('102-01-01');
240
+ expect(hits[0].matchField).toBe('id');
241
+ });
198
242
  });
199
243
 
200
244
  describe('extractEdges', () => {
201
245
  it('extracts cross-reference edges from gl:references', () => {
202
- const concept = {
246
+ const concept = conceptFromJson({
203
247
  '@id': 'https://glossarist.org/test/concept/102-01-01',
248
+ '@type': 'gl:Concept',
204
249
  'gl:localizedConcept': {
205
250
  eng: {
206
251
  'gl:references': [
@@ -209,18 +254,38 @@ describe('DatasetAdapter', () => {
209
254
  ],
210
255
  },
211
256
  },
212
- };
257
+ });
213
258
 
214
- const edges = adapter.extractEdges(concept as any);
259
+ const edges = adapter.extractEdges(concept);
215
260
  expect(edges.length).toBe(2);
216
261
  expect(edges[0].target).toBe('https://glossarist.org/iev/concept/103-01-02');
217
262
  expect(edges[0].type).toBe('references');
218
263
  expect(edges[0].label).toBe('functional');
219
264
  });
220
265
 
266
+ it('tags reference edges with language', () => {
267
+ const concept = conceptFromJson({
268
+ '@id': 'https://glossarist.org/test/concept/1',
269
+ '@type': 'gl:Concept',
270
+ 'gl:localizedConcept': {
271
+ eng: { 'gl:references': [
272
+ { '@id': 'https://glossarist.org/test/concept/2', 'gl:term': 'other' },
273
+ ]},
274
+ fra: { 'gl:references': [
275
+ { '@id': 'https://glossarist.org/test/concept/3', 'gl:term': 'autre' },
276
+ ]},
277
+ },
278
+ });
279
+ const edges = adapter.extractEdges(concept);
280
+ expect(edges.length).toBe(2);
281
+ expect(edges.find(e => e.lang === 'eng')?.target).toContain('/concept/2');
282
+ expect(edges.find(e => e.lang === 'fra')?.target).toContain('/concept/3');
283
+ });
284
+
221
285
  it('skips self-references', () => {
222
- const concept = {
286
+ const concept = conceptFromJson({
223
287
  '@id': 'https://glossarist.org/test/concept/102-01-01',
288
+ '@type': 'gl:Concept',
224
289
  'gl:localizedConcept': {
225
290
  eng: {
226
291
  'gl:references': [
@@ -228,37 +293,40 @@ describe('DatasetAdapter', () => {
228
293
  ],
229
294
  },
230
295
  },
231
- };
296
+ });
232
297
 
233
- const edges = adapter.extractEdges(concept as any);
298
+ const edges = adapter.extractEdges(concept);
234
299
  expect(edges.length).toBe(0);
235
300
  });
236
301
 
237
302
  it('handles concepts with no references', () => {
238
- const concept = {
303
+ const concept = conceptFromJson({
239
304
  '@id': 'https://glossarist.org/test/concept/102-01-01',
305
+ '@type': 'gl:Concept',
240
306
  'gl:localizedConcept': {
241
307
  eng: {},
242
308
  },
243
- };
309
+ });
244
310
 
245
- const edges = adapter.extractEdges(concept as any);
311
+ const edges = adapter.extractEdges(concept);
246
312
  expect(edges.length).toBe(0);
247
313
  });
248
314
 
249
315
  it('handles empty localizedConcept', () => {
250
- const concept = {
316
+ const concept = conceptFromJson({
251
317
  '@id': 'https://glossarist.org/test/concept/102-01-01',
318
+ '@type': 'gl:Concept',
252
319
  'gl:localizedConcept': {},
253
- };
320
+ });
254
321
 
255
- const edges = adapter.extractEdges(concept as any);
322
+ const edges = adapter.extractEdges(concept);
256
323
  expect(edges.length).toBe(0);
257
324
  });
258
325
 
259
326
  it('collects references from multiple languages without duplication', () => {
260
- const concept = {
327
+ const concept = conceptFromJson({
261
328
  '@id': 'https://glossarist.org/test/concept/102-01-01',
329
+ '@type': 'gl:Concept',
262
330
  'gl:localizedConcept': {
263
331
  eng: {
264
332
  'gl:references': [
@@ -271,16 +339,17 @@ describe('DatasetAdapter', () => {
271
339
  ],
272
340
  },
273
341
  },
274
- };
342
+ });
275
343
 
276
344
  // Same target from two languages — both edges are kept (different labels)
277
- const edges = adapter.extractEdges(concept as any);
345
+ const edges = adapter.extractEdges(concept);
278
346
  expect(edges.length).toBe(2);
279
347
  });
280
348
 
281
349
  it('extracts inline IEV cross-references from gl:references', () => {
282
- const concept = {
350
+ const concept = conceptFromJson({
283
351
  '@id': 'https://glossarist.org/test/concept/112-01-01',
352
+ '@type': 'gl:Concept',
284
353
  'gl:localizedConcept': {
285
354
  eng: {
286
355
  'gl:references': [
@@ -289,9 +358,9 @@ describe('DatasetAdapter', () => {
289
358
  ],
290
359
  },
291
360
  },
292
- };
361
+ });
293
362
 
294
- const edges = adapter.extractEdges(concept as any);
363
+ const edges = adapter.extractEdges(concept);
295
364
  expect(edges.length).toBe(2);
296
365
  expect(edges[0].target).toBe('https://glossarist.org/iev/concept/102-02-18');
297
366
  expect(edges[0].label).toBe('scalar');
@@ -300,8 +369,9 @@ describe('DatasetAdapter', () => {
300
369
  });
301
370
 
302
371
  it('extracts inline URN cross-references from gl:references', () => {
303
- const concept = {
372
+ const concept = conceptFromJson({
304
373
  '@id': 'https://glossarist.org/test/concept/3.1.1.1',
374
+ '@type': 'gl:Concept',
305
375
  'gl:localizedConcept': {
306
376
  eng: {
307
377
  'gl:references': [
@@ -309,15 +379,94 @@ describe('DatasetAdapter', () => {
309
379
  ],
310
380
  },
311
381
  },
312
- };
382
+ });
313
383
 
314
- const edges = adapter.extractEdges(concept as any);
384
+ const edges = adapter.extractEdges(concept);
315
385
  expect(edges.length).toBe(1);
316
386
  expect(edges[0].target).toBe('https://glossarist.org/isotc204/concept/3.1.1.6');
317
387
  expect(edges[0].label).toBe('entity');
318
388
  });
319
389
  });
320
390
 
391
+ describe('extractDomainEdges', () => {
392
+ it('extracts domain edges from gl:domain field per language', () => {
393
+ const concept = conceptFromJson({
394
+ '@id': 'https://glossarist.org/test/concept/3',
395
+ '@type': 'gl:Concept',
396
+ 'gl:localizedConcept': {
397
+ eng: { 'gl:domain': 'geometry' },
398
+ fra: { 'gl:domain': 'géométrie' },
399
+ },
400
+ });
401
+ const edges = adapter.extractDomainEdges(concept);
402
+ expect(edges.length).toBe(2);
403
+ expect(edges.every(e => e.type === 'domain')).toBe(true);
404
+ expect(edges.find(e => e.lang === 'eng')?.target).toContain('/domain/geometry');
405
+ expect(edges.find(e => e.lang === 'fra')?.target).toContain('/domain/gomtrie');
406
+ expect(edges.find(e => e.lang === 'eng')?.label).toBe('geometry');
407
+ expect(edges.find(e => e.lang === 'fra')?.label).toBe('géométrie');
408
+ });
409
+
410
+ it('handles same domain across languages', () => {
411
+ const concept = conceptFromJson({
412
+ '@id': 'https://glossarist.org/test/concept/1',
413
+ '@type': 'gl:Concept',
414
+ 'gl:localizedConcept': {
415
+ eng: { 'gl:domain': 'metadata' },
416
+ fra: { 'gl:domain': 'metadata' },
417
+ },
418
+ });
419
+ const edges = adapter.extractDomainEdges(concept);
420
+ expect(edges.length).toBe(2);
421
+ expect(edges[0].target).toBe(edges[1].target);
422
+ expect(edges[0].target).toContain('/domain/metadata');
423
+ });
424
+
425
+ it('skips concepts without gl:domain', () => {
426
+ const concept = conceptFromJson({
427
+ '@id': 'https://glossarist.org/test/concept/1',
428
+ '@type': 'gl:Concept',
429
+ 'gl:localizedConcept': { eng: {} },
430
+ });
431
+ const edges = adapter.extractDomainEdges(concept);
432
+ expect(edges.length).toBe(0);
433
+ });
434
+
435
+ it('handles empty localizedConcept', () => {
436
+ const concept = conceptFromJson({
437
+ '@id': 'https://glossarist.org/test/concept/1',
438
+ '@type': 'gl:Concept',
439
+ 'gl:localizedConcept': {},
440
+ });
441
+ const edges = adapter.extractDomainEdges(concept);
442
+ expect(edges.length).toBe(0);
443
+ });
444
+ });
445
+
446
+ describe('loadDomainNodes', () => {
447
+ it('loads domain nodes from domain-nodes.json', async () => {
448
+ mockFetch.mockReturnValue(mockJsonResponse({
449
+ registerId: 'test',
450
+ domainNodes: [
451
+ { uri: 'https://glossarist.org/test/domain/iso-19107', label: 'ISO 19107', registerId: 'test', conceptCount: 147 },
452
+ ],
453
+ }));
454
+ const nodes = await adapter.loadDomainNodes();
455
+ expect(nodes.length).toBe(1);
456
+ expect(nodes[0].nodeType).toBe('domain');
457
+ expect(nodes[0].status).toBe('domain');
458
+ expect(nodes[0].loaded).toBe(true);
459
+ expect(nodes[0].designations.eng).toBe('ISO 19107');
460
+ expect(mockFetch).toHaveBeenCalledWith('/data/test/domain-nodes.json');
461
+ });
462
+
463
+ it('returns empty array on fetch failure', async () => {
464
+ mockFetch.mockReturnValue(Promise.resolve({ ok: false, status: 404 } as Response));
465
+ const nodes = await adapter.loadDomainNodes();
466
+ expect(nodes).toEqual([]);
467
+ });
468
+ });
469
+
321
470
  describe('getLanguages', () => {
322
471
  it('returns languages from manifest', async () => {
323
472
  const manifest = {
@@ -34,6 +34,7 @@ function makeManifest(overrides: Partial<Manifest> = {}): Manifest {
34
34
  function makeConcepts(count: number): ConceptSummary[] {
35
35
  return Array.from({ length: count }, (_, i) => ({
36
36
  id: String(i + 1),
37
+ designations: { eng: `term ${i + 1}` },
37
38
  eng: `term ${i + 1}`,
38
39
  status: i % 10 === 0 ? 'superseded' : 'valid',
39
40
  }));
@@ -147,9 +148,9 @@ describe('DatasetView', () => {
147
148
 
148
149
  it('filters concepts by term', async () => {
149
150
  const concepts: ConceptSummary[] = [
150
- { id: '1', eng: 'road network', status: 'valid' },
151
- { id: '2', eng: 'bridge design', status: 'valid' },
152
- { id: '3', eng: 'road user', status: 'valid' },
151
+ { id: '1', designations: { eng: 'road network' }, eng: 'road network', status: 'valid' },
152
+ { id: '2', designations: { eng: 'bridge design' }, eng: 'bridge design', status: 'valid' },
153
+ { id: '3', designations: { eng: 'road user' }, eng: 'road user', status: 'valid' },
153
154
  ];
154
155
  const wrapper = mountDataset(concepts);
155
156
  await flushPromises();
@@ -163,8 +164,8 @@ describe('DatasetView', () => {
163
164
 
164
165
  it('filters concepts by ID', async () => {
165
166
  const concepts: ConceptSummary[] = [
166
- { id: '3.1.1.1', eng: 'term one', status: 'valid' },
167
- { id: '3.1.1.2', eng: 'term two', status: 'valid' },
167
+ { id: '3.1.1.1', designations: { eng: 'term one' }, eng: 'term one', status: 'valid' },
168
+ { id: '3.1.1.2', designations: { eng: 'term two' }, eng: 'term two', status: 'valid' },
168
169
  ];
169
170
  const wrapper = mountDataset(concepts);
170
171
  await flushPromises();