@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.
- package/README.md +3 -2
- package/cli/index.mjs +2 -1
- package/env.d.ts +5 -0
- package/package.json +4 -3
- package/scripts/build-edges.js +78 -10
- package/scripts/generate-data.mjs +152 -20
- package/scripts/generate-ontology-data.mjs +184 -0
- package/scripts/generate-ontology-schema.mjs +315 -0
- package/src/__tests__/concept-card.test.ts +1 -1
- package/src/__tests__/concept-detail-interaction.test.ts +40 -18
- package/src/__tests__/concept-formats.test.ts +32 -30
- package/src/__tests__/concept-timeline.test.ts +108 -83
- package/src/__tests__/concept-view.test.ts +15 -2
- package/src/__tests__/dataset-adapter.test.ts +172 -23
- package/src/__tests__/dataset-view.test.ts +6 -5
- package/src/__tests__/designation-registry.test.ts +161 -0
- package/src/__tests__/graph.test.ts +62 -0
- package/src/__tests__/language-detail.test.ts +117 -60
- package/src/__tests__/ontology-registry.test.ts +109 -0
- package/src/__tests__/relationship-categories.test.ts +62 -0
- package/src/__tests__/test-helpers.ts +11 -8
- package/src/adapters/DatasetAdapter.ts +171 -48
- package/src/adapters/model-bridge.ts +277 -0
- package/src/adapters/ontology-registry.ts +75 -0
- package/src/adapters/ontology-schema.ts +100 -0
- package/src/adapters/types.ts +52 -77
- package/src/components/AppSidebar.vue +1 -1
- package/src/components/CitationDisplay.vue +35 -0
- package/src/components/ConceptDetail.vue +334 -93
- package/src/components/ConceptRdfView.vue +397 -0
- package/src/components/ConceptTimeline.vue +56 -52
- package/src/components/GraphPanel.vue +96 -31
- package/src/components/LanguageDetail.vue +45 -37
- package/src/components/NavIcon.vue +1 -0
- package/src/components/NonVerbalRepDisplay.vue +38 -0
- package/src/components/RelationshipList.vue +99 -0
- package/src/config/use-site-config.ts +3 -0
- package/src/data/ontology-schema.json +1551 -0
- package/src/data/taxonomies.json +543 -0
- package/src/graph/GraphEngine.ts +7 -4
- package/src/router/index.ts +5 -0
- package/src/shims/empty.ts +1 -0
- package/src/shims/node-crypto.ts +6 -0
- package/src/shims/node-path.ts +10 -0
- package/src/stores/vocabulary.ts +75 -25
- package/src/style.css +74 -20
- package/src/utils/concept-formats.ts +22 -20
- package/src/utils/concept-helpers.ts +43 -23
- package/src/utils/designation-registry.ts +124 -0
- package/src/utils/relationship-categories.ts +84 -0
- package/src/views/OntologySchemaView.vue +302 -0
- package/src/views/PageView.vue +28 -17
- package/src/views/StatsView.vue +34 -12
- package/vite.config.ts +8 -0
|
@@ -1,23 +1,30 @@
|
|
|
1
|
-
import { describe, it, expect
|
|
2
|
-
import { mount
|
|
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
|
|
4
|
+
import { conceptFromJson } from '../adapters/model-bridge';
|
|
6
5
|
|
|
7
|
-
function
|
|
6
|
+
function makeConceptJson(overrides: Record<string, any> = {}) {
|
|
8
7
|
return {
|
|
9
|
-
'@id': 'https://glossarist.org/test/concept/1
|
|
10
|
-
'@type': 'gl:
|
|
11
|
-
'gl:
|
|
12
|
-
'gl:
|
|
13
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
expect(wrapper.text()).toContain('
|
|
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
|
|
36
|
-
'gl:reviewDate': '2023-05-15',
|
|
42
|
+
const json = makeConceptJson({
|
|
43
|
+
eng: { 'gl:reviewDate': '2023-05-15' },
|
|
37
44
|
});
|
|
38
|
-
const wrapper = mountTimeline(
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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(
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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(
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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(
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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(
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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(
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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(
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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();
|