@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
|
@@ -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
|
+
});
|
|
@@ -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', () => {
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { mount
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
3
|
import LanguageDetail from '../components/LanguageDetail.vue';
|
|
4
4
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
5
|
-
import
|
|
6
|
-
import { createTestRouter, setupPinia, makeManifest,
|
|
5
|
+
import { conceptFromJson } from '../adapters/model-bridge';
|
|
6
|
+
import { createTestRouter, setupPinia, makeManifest, makeAdapterStub } from './test-helpers';
|
|
7
|
+
|
|
8
|
+
function makeConceptJson(overrides: Record<string, any> = {}) {
|
|
9
|
+
return {
|
|
10
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
11
|
+
'@type': 'gl:Concept',
|
|
12
|
+
'gl:identifier': '1',
|
|
13
|
+
'gl:localizedConcept': {
|
|
14
|
+
eng: {
|
|
15
|
+
'@type': 'gl:LocalizedConcept',
|
|
16
|
+
'gl:languageCode': 'eng',
|
|
17
|
+
'gl:entryStatus': 'valid',
|
|
18
|
+
...overrides.eng,
|
|
19
|
+
},
|
|
20
|
+
...(overrides.otherLangs || {}),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
7
24
|
|
|
8
25
|
describe('LanguageDetail', () => {
|
|
9
26
|
let pinia: ReturnType<typeof setupPinia>;
|
|
@@ -17,33 +34,58 @@ describe('LanguageDetail', () => {
|
|
|
17
34
|
store.datasets.set('test', makeAdapterStub());
|
|
18
35
|
});
|
|
19
36
|
|
|
20
|
-
function mountDetail(
|
|
37
|
+
function mountDetail(conceptJson: Record<string, any>, activeLang = 'eng') {
|
|
38
|
+
const concept = conceptFromJson(conceptJson);
|
|
21
39
|
return mount(LanguageDetail, {
|
|
22
40
|
global: { plugins: [pinia, router], directives: { math: () => {} } },
|
|
23
|
-
props: {
|
|
41
|
+
props: { concept, activeLang },
|
|
24
42
|
});
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
it('renders language selector buttons', () => {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
const json = makeConceptJson({
|
|
47
|
+
eng: {},
|
|
48
|
+
otherLangs: {
|
|
49
|
+
fra: {
|
|
50
|
+
'@type': 'gl:LocalizedConcept',
|
|
51
|
+
'gl:languageCode': 'fra',
|
|
52
|
+
'gl:entryStatus': 'valid',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const wrapper = mountDetail(json);
|
|
31
57
|
expect(wrapper.text()).toContain('English');
|
|
32
58
|
expect(wrapper.text()).toContain('French');
|
|
33
59
|
});
|
|
34
60
|
|
|
35
61
|
it('highlights active language button', () => {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
const json = makeConceptJson({
|
|
63
|
+
eng: {},
|
|
64
|
+
otherLangs: {
|
|
65
|
+
fra: {
|
|
66
|
+
'@type': 'gl:LocalizedConcept',
|
|
67
|
+
'gl:languageCode': 'fra',
|
|
68
|
+
'gl:entryStatus': 'valid',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
const wrapper = mountDetail(json, 'eng');
|
|
39
73
|
const buttons = wrapper.findAll('button').filter(b => b.text().includes('English'));
|
|
40
74
|
expect(buttons[0].classes()).toContain('bg-ink-800');
|
|
41
75
|
});
|
|
42
76
|
|
|
43
77
|
it('emits update:activeLang on language click', async () => {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
78
|
+
const json = makeConceptJson({
|
|
79
|
+
eng: {},
|
|
80
|
+
otherLangs: {
|
|
81
|
+
fra: {
|
|
82
|
+
'@type': 'gl:LocalizedConcept',
|
|
83
|
+
'gl:languageCode': 'fra',
|
|
84
|
+
'gl:entryStatus': 'valid',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const wrapper = mountDetail(json, 'eng');
|
|
47
89
|
const fraBtn = wrapper.findAll('button').find(b => b.text().includes('French'));
|
|
48
90
|
expect(fraBtn).toBeDefined();
|
|
49
91
|
await fraBtn!.trigger('click');
|
|
@@ -51,96 +93,111 @@ describe('LanguageDetail', () => {
|
|
|
51
93
|
});
|
|
52
94
|
|
|
53
95
|
it('shows entry status badge', () => {
|
|
54
|
-
const
|
|
55
|
-
|
|
96
|
+
const json = makeConceptJson({
|
|
97
|
+
eng: { 'gl:entryStatus': 'valid' },
|
|
98
|
+
});
|
|
99
|
+
const wrapper = mountDetail(json);
|
|
56
100
|
expect(wrapper.text()).toContain('valid');
|
|
57
101
|
});
|
|
58
102
|
|
|
59
|
-
it('shows designations', () => {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
103
|
+
it('shows designations with ontology labels', () => {
|
|
104
|
+
const json = makeConceptJson({
|
|
105
|
+
eng: {
|
|
106
|
+
'gl:designation': [
|
|
107
|
+
{ '@type': 'gl:Expression', 'gl:term': 'road', 'gl:normativeStatus': 'preferred' },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
64
110
|
});
|
|
65
|
-
const wrapper = mountDetail(
|
|
111
|
+
const wrapper = mountDetail(json);
|
|
66
112
|
expect(wrapper.text()).toContain('road');
|
|
67
|
-
expect(wrapper.text()).toContain('
|
|
68
|
-
expect(wrapper.text()).toContain('
|
|
113
|
+
expect(wrapper.text()).toContain('expression');
|
|
114
|
+
expect(wrapper.text()).toContain('preferred');
|
|
69
115
|
});
|
|
70
116
|
|
|
71
117
|
it('shows definition', () => {
|
|
72
|
-
const
|
|
73
|
-
|
|
118
|
+
const json = makeConceptJson({
|
|
119
|
+
eng: {
|
|
120
|
+
'gl:definition': [{ '@type': 'gl:Definition', 'gl:content': 'A paved surface for vehicles.' }],
|
|
121
|
+
},
|
|
74
122
|
});
|
|
75
|
-
const wrapper = mountDetail(
|
|
123
|
+
const wrapper = mountDetail(json);
|
|
76
124
|
expect(wrapper.text()).toContain('Definition');
|
|
77
125
|
expect(wrapper.text()).toContain('paved surface');
|
|
78
126
|
});
|
|
79
127
|
|
|
80
128
|
it('shows notes', () => {
|
|
81
|
-
const
|
|
82
|
-
|
|
129
|
+
const json = makeConceptJson({
|
|
130
|
+
eng: {
|
|
131
|
+
'gl:notes': [{ '@type': 'gl:Note', 'gl:content': 'This is a note.' }],
|
|
132
|
+
},
|
|
83
133
|
});
|
|
84
|
-
const wrapper = mountDetail(
|
|
134
|
+
const wrapper = mountDetail(json);
|
|
85
135
|
expect(wrapper.text()).toContain('Notes');
|
|
86
136
|
expect(wrapper.text()).toContain('This is a note');
|
|
87
137
|
});
|
|
88
138
|
|
|
89
139
|
it('shows examples', () => {
|
|
90
|
-
const
|
|
91
|
-
|
|
140
|
+
const json = makeConceptJson({
|
|
141
|
+
eng: {
|
|
142
|
+
'gl:examples': [{ '@type': 'gl:Example', 'gl:content': 'A highway is a road.' }],
|
|
143
|
+
},
|
|
92
144
|
});
|
|
93
|
-
const wrapper = mountDetail(
|
|
145
|
+
const wrapper = mountDetail(json);
|
|
94
146
|
expect(wrapper.text()).toContain('Examples');
|
|
95
147
|
expect(wrapper.text()).toContain('A highway is a road');
|
|
96
148
|
});
|
|
97
149
|
|
|
98
150
|
it('shows sources', () => {
|
|
99
|
-
const
|
|
100
|
-
|
|
151
|
+
const json = makeConceptJson({
|
|
152
|
+
eng: {
|
|
153
|
+
'gl:source': [{ '@type': 'gl:Source', 'gl:sourceType': 'authoritative', 'gl:origin': { '@type': 'gl:Origin', 'gl:ref': { '@type': 'gl:Ref', 'gl:source': 'ISO 7010' } } }],
|
|
154
|
+
},
|
|
101
155
|
});
|
|
102
|
-
const wrapper = mountDetail(
|
|
156
|
+
const wrapper = mountDetail(json);
|
|
103
157
|
expect(wrapper.text()).toContain('Sources');
|
|
104
158
|
expect(wrapper.text()).toContain('ISO 7010');
|
|
105
159
|
});
|
|
106
160
|
|
|
107
161
|
it('shows term-only state for language without definition', () => {
|
|
108
|
-
const
|
|
109
|
-
|
|
162
|
+
const json = makeConceptJson({
|
|
163
|
+
eng: {
|
|
164
|
+
'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'test', 'gl:normativeStatus': 'preferred' }],
|
|
165
|
+
},
|
|
110
166
|
});
|
|
111
|
-
|
|
112
|
-
delete (eng as any)['gl:notes'];
|
|
113
|
-
delete (eng as any)['gl:examples'];
|
|
114
|
-
const wrapper = mountDetail({ eng });
|
|
167
|
+
const wrapper = mountDetail(json);
|
|
115
168
|
expect(wrapper.text()).toContain('Term only in English');
|
|
116
169
|
});
|
|
117
170
|
|
|
118
171
|
it('shows no data message for missing language', () => {
|
|
119
|
-
const
|
|
120
|
-
const wrapper = mountDetail(
|
|
172
|
+
const json = makeConceptJson({ eng: {} });
|
|
173
|
+
const wrapper = mountDetail(json, 'zho');
|
|
121
174
|
expect(wrapper.text()).toContain('No data available');
|
|
122
175
|
});
|
|
123
176
|
|
|
124
|
-
it('shows designation type badges', () => {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
177
|
+
it('shows designation type badges with ontology labels', () => {
|
|
178
|
+
const json = makeConceptJson({
|
|
179
|
+
eng: {
|
|
180
|
+
'gl:designation': [
|
|
181
|
+
{ '@type': 'gl:Symbol', 'gl:term': 'H₂O', 'gl:normativeStatus': 'preferred' },
|
|
182
|
+
{ '@type': 'gl:Abbreviation', 'gl:term': 'abbr', 'gl:normativeStatus': 'admitted' },
|
|
183
|
+
],
|
|
184
|
+
},
|
|
130
185
|
});
|
|
131
|
-
const wrapper = mountDetail(
|
|
132
|
-
expect(wrapper.text()).toContain('
|
|
133
|
-
expect(wrapper.text()).toContain('
|
|
186
|
+
const wrapper = mountDetail(json);
|
|
187
|
+
expect(wrapper.text()).toContain('symbol');
|
|
188
|
+
expect(wrapper.text()).toContain('abbreviation');
|
|
134
189
|
});
|
|
135
190
|
|
|
136
|
-
it('shows
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
191
|
+
it('shows grammar info with ontology labels when present', () => {
|
|
192
|
+
const json = makeConceptJson({
|
|
193
|
+
eng: {
|
|
194
|
+
'gl:designation': [
|
|
195
|
+
{ '@type': 'gl:Expression', 'gl:term': 'route', 'gl:normativeStatus': 'preferred', 'gl:grammarInfo': [{ 'gl:gender': 'f', 'gl:number': 'singular' }] },
|
|
196
|
+
],
|
|
197
|
+
},
|
|
141
198
|
});
|
|
142
|
-
const wrapper = mountDetail(
|
|
143
|
-
expect(wrapper.text()).toContain('
|
|
199
|
+
const wrapper = mountDetail(json);
|
|
200
|
+
expect(wrapper.text()).toContain('feminine');
|
|
144
201
|
expect(wrapper.text()).toContain('singular');
|
|
145
202
|
});
|
|
146
203
|
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ontology } from '../adapters/ontology-registry';
|
|
3
|
+
|
|
4
|
+
describe('OntologyRegistry', () => {
|
|
5
|
+
it('loads all 10 taxonomies', () => {
|
|
6
|
+
expect(ontology.getAll('conceptStatus').length).toBe(7);
|
|
7
|
+
expect(ontology.getAll('entryStatus').length).toBe(4);
|
|
8
|
+
expect(ontology.getAll('normativeStatus').length).toBe(4);
|
|
9
|
+
expect(ontology.getAll('sourceType').length).toBe(2);
|
|
10
|
+
expect(ontology.getAll('sourceStatus').length).toBe(10);
|
|
11
|
+
expect(ontology.getAll('relationshipType').length).toBe(14);
|
|
12
|
+
expect(ontology.getAll('designationType').length).toBe(5);
|
|
13
|
+
expect(ontology.getAll('termType').length).toBe(24);
|
|
14
|
+
expect(ontology.getAll('grammarGender').length).toBe(4);
|
|
15
|
+
expect(ontology.getAll('grammarNumber').length).toBe(3);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns correct labels for conceptStatus', () => {
|
|
19
|
+
expect(ontology.getLabel('conceptStatus', 'valid')).toBe('valid');
|
|
20
|
+
expect(ontology.getLabel('conceptStatus', 'superseded')).toBe('superseded');
|
|
21
|
+
expect(ontology.getLabel('conceptStatus', 'not_valid')).toBe('not valid');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns correct labels for grammarGender', () => {
|
|
25
|
+
expect(ontology.getLabel('grammarGender', 'm')).toBe('masculine');
|
|
26
|
+
expect(ontology.getLabel('grammarGender', 'f')).toBe('feminine');
|
|
27
|
+
expect(ontology.getLabel('grammarGender', 'n')).toBe('neuter');
|
|
28
|
+
expect(ontology.getLabel('grammarGender', 'c')).toBe('common');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns altLabel for grammarGender', () => {
|
|
32
|
+
expect(ontology.getAltLabel('grammarGender', 'm')).toBe('m');
|
|
33
|
+
expect(ontology.getAltLabel('grammarGender', 'f')).toBe('f');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns definitions for every taxonomy value', () => {
|
|
37
|
+
for (const status of ['draft', 'submitted', 'not_valid', 'invalid', 'valid', 'superseded', 'retired']) {
|
|
38
|
+
expect(ontology.getDefinition('conceptStatus', status)).toBeTruthy();
|
|
39
|
+
}
|
|
40
|
+
for (const gender of ['m', 'f', 'n', 'c']) {
|
|
41
|
+
expect(ontology.getDefinition('grammarGender', gender)).toBeTruthy();
|
|
42
|
+
}
|
|
43
|
+
for (const status of ['identical', 'similar', 'modified', 'restyle', 'context_added', 'generalisation', 'specialisation', 'unspecified', 'related', 'not_equal']) {
|
|
44
|
+
expect(ontology.getDefinition('sourceStatus', status)).toBeTruthy();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns correct hierarchy for designationType', () => {
|
|
49
|
+
expect(ontology.getBroader('designationType', 'abbreviation')).toBe('expression');
|
|
50
|
+
expect(ontology.getBroader('designationType', 'letter_symbol')).toBe('symbol');
|
|
51
|
+
expect(ontology.getBroader('designationType', 'graphical_symbol')).toBe('symbol');
|
|
52
|
+
expect(ontology.getBroader('designationType', 'expression')).toBeNull();
|
|
53
|
+
expect(ontology.getBroader('designationType', 'symbol')).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns narrower for designationType', () => {
|
|
57
|
+
const childrenOfExpression = ontology.getNarrower('designationType', 'expression');
|
|
58
|
+
expect(childrenOfExpression.map(c => c.id)).toContain('abbreviation');
|
|
59
|
+
|
|
60
|
+
const childrenOfSymbol = ontology.getNarrower('designationType', 'symbol');
|
|
61
|
+
expect(childrenOfSymbol.map(c => c.id)).toEqual(expect.arrayContaining(['letter_symbol', 'graphical_symbol']));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns broader for termType', () => {
|
|
65
|
+
expect(ontology.getBroader('termType', 'acronym')).toBe('abbreviation');
|
|
66
|
+
expect(ontology.getBroader('termType', 'initialism')).toBe('abbreviation');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns null for unknown taxonomy/value', () => {
|
|
70
|
+
expect(ontology.getConcept('conceptStatus', 'nonexistent')).toBeNull();
|
|
71
|
+
expect(ontology.getLabel('conceptStatus', 'nonexistent')).toBe('nonexistent');
|
|
72
|
+
expect(ontology.getDefinition('conceptStatus', 'nonexistent')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns scheme IRIs', () => {
|
|
76
|
+
expect(ontology.getScheme('conceptStatus')).toContain('status');
|
|
77
|
+
expect(ontology.getScheme('grammarGender')).toContain('gender');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('has() checks work', () => {
|
|
81
|
+
expect(ontology.has('conceptStatus', 'valid')).toBe(true);
|
|
82
|
+
expect(ontology.has('conceptStatus', 'nonexistent')).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('normativeStatus includes correct labels', () => {
|
|
86
|
+
expect(ontology.getLabel('normativeStatus', 'preferred')).toBe('preferred');
|
|
87
|
+
expect(ontology.getLabel('normativeStatus', 'admitted')).toBe('admitted');
|
|
88
|
+
expect(ontology.getLabel('normativeStatus', 'deprecated')).toBe('deprecated');
|
|
89
|
+
expect(ontology.getLabel('normativeStatus', 'superseded')).toBe('superseded');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('relationshipType has all glossarist-specific types', () => {
|
|
93
|
+
const types = ontology.getAll('relationshipType').map(c => c.id);
|
|
94
|
+
expect(types).toContain('deprecates');
|
|
95
|
+
expect(types).toContain('supersedes');
|
|
96
|
+
expect(types).toContain('superseded_by');
|
|
97
|
+
expect(types).toContain('compare');
|
|
98
|
+
expect(types).toContain('contrast');
|
|
99
|
+
expect(types).toContain('homograph');
|
|
100
|
+
expect(types).toContain('false_friend');
|
|
101
|
+
expect(types).toContain('abbreviated_form_for');
|
|
102
|
+
expect(types).toContain('short_form_for');
|
|
103
|
+
expect(types).toContain('sequentially_related');
|
|
104
|
+
expect(types).toContain('spatially_related');
|
|
105
|
+
expect(types).toContain('temporally_related');
|
|
106
|
+
expect(types).toContain('related_concept_broader');
|
|
107
|
+
expect(types).toContain('related_concept_narrower');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { categorizeRelationship, relationshipLabel, RELATIONSHIP_CATEGORIES } from '../utils/relationship-categories';
|
|
3
|
+
|
|
4
|
+
describe('RELATIONSHIP_CATEGORIES', () => {
|
|
5
|
+
it('has expected categories', () => {
|
|
6
|
+
const ids = RELATIONSHIP_CATEGORIES.map(c => c.id);
|
|
7
|
+
expect(ids).toContain('hierarchical');
|
|
8
|
+
expect(ids).toContain('mapping');
|
|
9
|
+
expect(ids).toContain('associative');
|
|
10
|
+
expect(ids).toContain('lifecycle');
|
|
11
|
+
expect(ids).toContain('comparative');
|
|
12
|
+
expect(ids).toContain('spatiotemporal');
|
|
13
|
+
expect(ids).toContain('lexical');
|
|
14
|
+
expect(ids).toContain('designation');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('each category has required fields', () => {
|
|
18
|
+
for (const cat of RELATIONSHIP_CATEGORIES) {
|
|
19
|
+
expect(cat.id).toBeTruthy();
|
|
20
|
+
expect(cat.label).toBeTruthy();
|
|
21
|
+
expect(cat.types.length).toBeGreaterThan(0);
|
|
22
|
+
expect(cat.color).toBeTruthy();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('categorizeRelationship', () => {
|
|
28
|
+
it('categorizes broader as hierarchical', () => {
|
|
29
|
+
expect(categorizeRelationship('broader').id).toBe('hierarchical');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('categorizes equivalent as mapping', () => {
|
|
33
|
+
expect(categorizeRelationship('equivalent').id).toBe('mapping');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('categorizes references as associative', () => {
|
|
37
|
+
expect(categorizeRelationship('references').id).toBe('associative');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('categorizes supersedes as lifecycle', () => {
|
|
41
|
+
expect(categorizeRelationship('supersedes').id).toBe('lifecycle');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('categorizes compare as comparative', () => {
|
|
45
|
+
expect(categorizeRelationship('compare').id).toBe('comparative');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns other for unknown type', () => {
|
|
49
|
+
expect(categorizeRelationship('unknown_type').id).toBe('other');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('relationshipLabel', () => {
|
|
54
|
+
it('formats snake_case as title case', () => {
|
|
55
|
+
expect(relationshipLabel('broader_generic')).toBe('Broader Generic');
|
|
56
|
+
expect(relationshipLabel('related_concept')).toBe('Related Concept');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('handles single word', () => {
|
|
60
|
+
expect(relationshipLabel('broader')).toBe('Broader');
|
|
61
|
+
});
|
|
62
|
+
});
|