@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.
- 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__/about-view.test.ts +98 -0
- package/src/__tests__/app-footer.test.ts +38 -0
- package/src/__tests__/app-header.test.ts +130 -0
- package/src/__tests__/app-sidebar.test.ts +159 -0
- package/src/__tests__/asciidoc-lite.test.ts +1 -1
- package/src/__tests__/concept-card.test.ts +115 -0
- package/src/__tests__/concept-detail-interaction.test.ts +273 -0
- package/src/__tests__/concept-formats.test.ts +32 -30
- package/src/__tests__/concept-timeline.test.ts +200 -0
- package/src/__tests__/concept-view.test.ts +88 -0
- package/src/__tests__/contributors-view.test.ts +103 -0
- package/src/__tests__/dataset-adapter.test.ts +172 -23
- package/src/__tests__/dataset-view.test.ts +232 -0
- package/src/__tests__/designation-registry.test.ts +161 -0
- package/src/__tests__/format-downloads.test.ts +98 -0
- package/src/__tests__/graph-view.test.ts +69 -0
- package/src/__tests__/graph.test.ts +62 -0
- package/src/__tests__/home-interaction.test.ts +157 -0
- package/src/__tests__/language-detail.test.ts +203 -0
- package/src/__tests__/nav-icon.test.ts +48 -0
- package/src/__tests__/news-view.test.ts +87 -0
- package/src/__tests__/ontology-registry.test.ts +109 -0
- package/src/__tests__/page-view.test.ts +83 -0
- package/src/__tests__/relationship-categories.test.ts +62 -0
- package/src/__tests__/resolve-view.test.ts +77 -0
- package/src/__tests__/router.test.ts +65 -0
- package/src/__tests__/search-bar.test.ts +219 -0
- package/src/__tests__/search-view.test.ts +41 -0
- package/src/__tests__/stats-view.test.ts +77 -0
- package/src/__tests__/test-helpers.ts +171 -0
- package/src/__tests__/ui-store.test.ts +100 -0
- package/src/__tests__/v-math.test.ts +8 -7
- package/src/adapters/DatasetAdapter.ts +188 -63
- 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 +53 -78
- package/src/components/AppSidebar.vue +1 -1
- package/src/components/CitationDisplay.vue +35 -0
- package/src/components/ConceptDetail.vue +349 -146
- package/src/components/ConceptRdfView.vue +397 -0
- package/src/components/ConceptTimeline.vue +57 -60
- package/src/components/GraphPanel.vue +96 -31
- package/src/components/LanguageDetail.vue +46 -61
- package/src/components/NavIcon.vue +1 -0
- package/src/components/NonVerbalRepDisplay.vue +38 -0
- package/src/components/RelationshipList.vue +99 -0
- package/src/composables/use-render-options.ts +1 -4
- 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 +6 -1
- 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 +82 -32
- package/src/style.css +74 -20
- package/src/utils/asciidoc-lite.ts +17 -19
- package/src/utils/concept-formats.ts +22 -20
- package/src/utils/concept-helpers.ts +54 -0
- package/src/utils/designation-registry.ts +124 -0
- package/src/utils/escape.ts +7 -0
- package/src/utils/markdown-lite.ts +1 -3
- package/src/utils/math.ts +2 -11
- package/src/utils/plurimath.ts +2 -7
- package/src/utils/relationship-categories.ts +84 -0
- package/src/views/ConceptView.vue +22 -1
- package/src/views/DatasetView.vue +7 -2
- 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,203 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import LanguageDetail from '../components/LanguageDetail.vue';
|
|
4
|
+
import { useVocabularyStore } from '../stores/vocabulary';
|
|
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
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('LanguageDetail', () => {
|
|
26
|
+
let pinia: ReturnType<typeof setupPinia>;
|
|
27
|
+
let router: Awaited<ReturnType<typeof createTestRouter>>;
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
pinia = setupPinia();
|
|
31
|
+
router = await createTestRouter('dataset', '/');
|
|
32
|
+
const store = useVocabularyStore();
|
|
33
|
+
store.manifests.set('test', makeManifest({ languages: ['eng', 'fra'] }));
|
|
34
|
+
store.datasets.set('test', makeAdapterStub());
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function mountDetail(conceptJson: Record<string, any>, activeLang = 'eng') {
|
|
38
|
+
const concept = conceptFromJson(conceptJson);
|
|
39
|
+
return mount(LanguageDetail, {
|
|
40
|
+
global: { plugins: [pinia, router], directives: { math: () => {} } },
|
|
41
|
+
props: { concept, activeLang },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
it('renders language selector buttons', () => {
|
|
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);
|
|
57
|
+
expect(wrapper.text()).toContain('English');
|
|
58
|
+
expect(wrapper.text()).toContain('French');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('highlights active language button', () => {
|
|
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');
|
|
73
|
+
const buttons = wrapper.findAll('button').filter(b => b.text().includes('English'));
|
|
74
|
+
expect(buttons[0].classes()).toContain('bg-ink-800');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('emits update:activeLang on language click', async () => {
|
|
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');
|
|
89
|
+
const fraBtn = wrapper.findAll('button').find(b => b.text().includes('French'));
|
|
90
|
+
expect(fraBtn).toBeDefined();
|
|
91
|
+
await fraBtn!.trigger('click');
|
|
92
|
+
expect(wrapper.emitted('update:activeLang')?.[0]).toEqual(['fra']);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('shows entry status badge', () => {
|
|
96
|
+
const json = makeConceptJson({
|
|
97
|
+
eng: { 'gl:entryStatus': 'valid' },
|
|
98
|
+
});
|
|
99
|
+
const wrapper = mountDetail(json);
|
|
100
|
+
expect(wrapper.text()).toContain('valid');
|
|
101
|
+
});
|
|
102
|
+
|
|
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
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const wrapper = mountDetail(json);
|
|
112
|
+
expect(wrapper.text()).toContain('road');
|
|
113
|
+
expect(wrapper.text()).toContain('expression');
|
|
114
|
+
expect(wrapper.text()).toContain('preferred');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('shows definition', () => {
|
|
118
|
+
const json = makeConceptJson({
|
|
119
|
+
eng: {
|
|
120
|
+
'gl:definition': [{ '@type': 'gl:Definition', 'gl:content': 'A paved surface for vehicles.' }],
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
const wrapper = mountDetail(json);
|
|
124
|
+
expect(wrapper.text()).toContain('Definition');
|
|
125
|
+
expect(wrapper.text()).toContain('paved surface');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('shows notes', () => {
|
|
129
|
+
const json = makeConceptJson({
|
|
130
|
+
eng: {
|
|
131
|
+
'gl:notes': [{ '@type': 'gl:Note', 'gl:content': 'This is a note.' }],
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const wrapper = mountDetail(json);
|
|
135
|
+
expect(wrapper.text()).toContain('Notes');
|
|
136
|
+
expect(wrapper.text()).toContain('This is a note');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('shows examples', () => {
|
|
140
|
+
const json = makeConceptJson({
|
|
141
|
+
eng: {
|
|
142
|
+
'gl:examples': [{ '@type': 'gl:Example', 'gl:content': 'A highway is a road.' }],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
const wrapper = mountDetail(json);
|
|
146
|
+
expect(wrapper.text()).toContain('Examples');
|
|
147
|
+
expect(wrapper.text()).toContain('A highway is a road');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('shows sources', () => {
|
|
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
|
+
},
|
|
155
|
+
});
|
|
156
|
+
const wrapper = mountDetail(json);
|
|
157
|
+
expect(wrapper.text()).toContain('Sources');
|
|
158
|
+
expect(wrapper.text()).toContain('ISO 7010');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('shows term-only state for language without definition', () => {
|
|
162
|
+
const json = makeConceptJson({
|
|
163
|
+
eng: {
|
|
164
|
+
'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'test', 'gl:normativeStatus': 'preferred' }],
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
const wrapper = mountDetail(json);
|
|
168
|
+
expect(wrapper.text()).toContain('Term only in English');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('shows no data message for missing language', () => {
|
|
172
|
+
const json = makeConceptJson({ eng: {} });
|
|
173
|
+
const wrapper = mountDetail(json, 'zho');
|
|
174
|
+
expect(wrapper.text()).toContain('No data available');
|
|
175
|
+
});
|
|
176
|
+
|
|
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
|
+
},
|
|
185
|
+
});
|
|
186
|
+
const wrapper = mountDetail(json);
|
|
187
|
+
expect(wrapper.text()).toContain('symbol');
|
|
188
|
+
expect(wrapper.text()).toContain('abbreviation');
|
|
189
|
+
});
|
|
190
|
+
|
|
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
|
+
},
|
|
198
|
+
});
|
|
199
|
+
const wrapper = mountDetail(json);
|
|
200
|
+
expect(wrapper.text()).toContain('feminine');
|
|
201
|
+
expect(wrapper.text()).toContain('singular');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import NavIcon from '../components/NavIcon.vue';
|
|
4
|
+
|
|
5
|
+
describe('NavIcon', () => {
|
|
6
|
+
it('renders an SVG element', () => {
|
|
7
|
+
const wrapper = mount(NavIcon, { props: { name: 'home' } });
|
|
8
|
+
expect(wrapper.find('svg').exists()).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('renders home icon path', () => {
|
|
12
|
+
const wrapper = mount(NavIcon, { props: { name: 'home' } });
|
|
13
|
+
const path = wrapper.find('svg path');
|
|
14
|
+
expect(path.exists()).toBe(true);
|
|
15
|
+
expect(path.attributes('d')).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders search icon', () => {
|
|
19
|
+
const wrapper = mount(NavIcon, { props: { name: 'search' } });
|
|
20
|
+
expect(wrapper.find('svg').exists()).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('renders graph icon', () => {
|
|
24
|
+
const wrapper = mount(NavIcon, { props: { name: 'graph' } });
|
|
25
|
+
expect(wrapper.find('svg').exists()).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('falls back to info icon for unknown name', () => {
|
|
29
|
+
const wrapper = mount(NavIcon, { props: { name: 'nonexistent' } });
|
|
30
|
+
expect(wrapper.find('svg').exists()).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('applies correct CSS classes', () => {
|
|
34
|
+
const wrapper = mount(NavIcon, { props: { name: 'home' } });
|
|
35
|
+
const svg = wrapper.find('svg');
|
|
36
|
+
expect(svg.classes()).toContain('w-4');
|
|
37
|
+
expect(svg.classes()).toContain('h-4');
|
|
38
|
+
expect(svg.classes()).toContain('text-ink-400');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders all known icon types', () => {
|
|
42
|
+
const icons = ['home', 'search', 'graph', 'newspaper', 'users', 'info', 'chart', 'list'];
|
|
43
|
+
for (const name of icons) {
|
|
44
|
+
const wrapper = mount(NavIcon, { props: { name } });
|
|
45
|
+
expect(wrapper.find('svg').exists()).toBe(true);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { mount, flushPromises } from '@vue/test-utils';
|
|
3
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
4
|
+
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
5
|
+
import NewsView from '../views/NewsView.vue';
|
|
6
|
+
|
|
7
|
+
async function createTestRouter() {
|
|
8
|
+
const router = createRouter({
|
|
9
|
+
history: createMemoryHistory(),
|
|
10
|
+
routes: [
|
|
11
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
12
|
+
{ path: '/news', name: 'news', component: { template: '<div/>' } },
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
router.push('/news');
|
|
16
|
+
await router.isReady();
|
|
17
|
+
return router;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('NewsView', () => {
|
|
21
|
+
let pinia: ReturnType<typeof createPinia>;
|
|
22
|
+
let router: Awaited<ReturnType<typeof createTestRouter>>;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
pinia = createPinia();
|
|
26
|
+
setActivePinia(pinia);
|
|
27
|
+
router = await createTestRouter();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function mountNews() {
|
|
31
|
+
return mount(NewsView, {
|
|
32
|
+
global: { plugins: [pinia, router] },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it('renders breadcrumb navigation', () => {
|
|
37
|
+
const wrapper = mountNews();
|
|
38
|
+
expect(wrapper.text()).toContain('Home');
|
|
39
|
+
expect(wrapper.text()).toContain('News');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('renders News heading', () => {
|
|
43
|
+
const wrapper = mountNews();
|
|
44
|
+
expect(wrapper.text()).toContain('News');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('shows loading skeleton initially', () => {
|
|
48
|
+
const wrapper = mountNews();
|
|
49
|
+
expect(wrapper.find('.animate-pulse').exists()).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('shows empty state when no posts', async () => {
|
|
53
|
+
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve([]) });
|
|
54
|
+
const wrapper = mountNews();
|
|
55
|
+
await flushPromises();
|
|
56
|
+
expect(wrapper.text()).toContain('No news posts yet');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('shows error state when fetch fails', async () => {
|
|
60
|
+
globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
61
|
+
const wrapper = mountNews();
|
|
62
|
+
await flushPromises();
|
|
63
|
+
expect(wrapper.text()).toContain('Failed to load news posts');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders posts when available', async () => {
|
|
67
|
+
const posts = [
|
|
68
|
+
{ slug: 'test-post', title: 'Test Post', date: '2025-01-15', categories: ['release'], file: '/news/test-post.adoc', excerpt: 'A test excerpt.' },
|
|
69
|
+
];
|
|
70
|
+
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(posts) });
|
|
71
|
+
const wrapper = mountNews();
|
|
72
|
+
await flushPromises();
|
|
73
|
+
expect(wrapper.text()).toContain('Test Post');
|
|
74
|
+
expect(wrapper.text()).toContain('release');
|
|
75
|
+
expect(wrapper.text()).toContain('A test excerpt.');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('formats dates correctly', async () => {
|
|
79
|
+
const posts = [
|
|
80
|
+
{ slug: 'dated', title: 'Dated', date: '2025-03-15', categories: [], file: '/news/dated.adoc', excerpt: '' },
|
|
81
|
+
];
|
|
82
|
+
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(posts) });
|
|
83
|
+
const wrapper = mountNews();
|
|
84
|
+
await flushPromises();
|
|
85
|
+
expect(wrapper.text()).toContain('March 15, 2025');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,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,83 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { mount, flushPromises } from '@vue/test-utils';
|
|
3
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
4
|
+
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
5
|
+
import PageView from '../views/PageView.vue';
|
|
6
|
+
|
|
7
|
+
async function createTestRouter(path = '/pages/test-page') {
|
|
8
|
+
const router = createRouter({
|
|
9
|
+
history: createMemoryHistory(),
|
|
10
|
+
routes: [
|
|
11
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
12
|
+
{ path: '/pages/:slug', name: 'page', component: { template: '<div/>' } },
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
router.push(path);
|
|
16
|
+
await router.isReady();
|
|
17
|
+
return router;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('PageView', () => {
|
|
21
|
+
let pinia: ReturnType<typeof createPinia>;
|
|
22
|
+
let router: Awaited<ReturnType<typeof createTestRouter>>;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
pinia = createPinia();
|
|
26
|
+
setActivePinia(pinia);
|
|
27
|
+
router = await createTestRouter();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function mountPage() {
|
|
31
|
+
return mount(PageView, {
|
|
32
|
+
global: { plugins: [pinia, router] },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it('shows loading skeleton initially', () => {
|
|
37
|
+
const wrapper = mountPage();
|
|
38
|
+
expect(wrapper.find('.animate-pulse').exists()).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders breadcrumb navigation', () => {
|
|
42
|
+
const wrapper = mountPage();
|
|
43
|
+
expect(wrapper.text()).toContain('Home');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('shows page not found when fetch fails', async () => {
|
|
47
|
+
globalThis.fetch = vi.fn().mockResolvedValue({ ok: false });
|
|
48
|
+
const wrapper = mountPage();
|
|
49
|
+
await flushPromises();
|
|
50
|
+
expect(wrapper.text()).toContain('Page Not Found');
|
|
51
|
+
expect(wrapper.text()).toContain('test-page');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('shows go home link on not found', async () => {
|
|
55
|
+
globalThis.fetch = vi.fn().mockResolvedValue({ ok: false });
|
|
56
|
+
const wrapper = mountPage();
|
|
57
|
+
await flushPromises();
|
|
58
|
+
const homeLink = wrapper.findAll('a').find(a => a.text() === 'Go Home');
|
|
59
|
+
expect(homeLink).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('renders page content when loaded', async () => {
|
|
63
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
64
|
+
ok: true,
|
|
65
|
+
json: () => Promise.resolve({ title: 'About Us', html: '<p>Hello world</p>' }),
|
|
66
|
+
});
|
|
67
|
+
const wrapper = mountPage();
|
|
68
|
+
await flushPromises();
|
|
69
|
+
expect(wrapper.text()).toContain('About Us');
|
|
70
|
+
expect(wrapper.text()).toContain('Hello world');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('renders page title in breadcrumb', async () => {
|
|
74
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
75
|
+
ok: true,
|
|
76
|
+
json: () => Promise.resolve({ title: 'About Us', html: '<p>Content</p>' }),
|
|
77
|
+
});
|
|
78
|
+
const wrapper = mountPage();
|
|
79
|
+
await flushPromises();
|
|
80
|
+
const breadcrumb = wrapper.find('nav[aria-label="Breadcrumb"]');
|
|
81
|
+
expect(breadcrumb.text()).toContain('About Us');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,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
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { mount, flushPromises } from '@vue/test-utils';
|
|
3
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
4
|
+
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
5
|
+
import { resetFactory, getFactory } from '../adapters/factory';
|
|
6
|
+
import ResolveView from '../views/ResolveView.vue';
|
|
7
|
+
|
|
8
|
+
const TEST_URI = 'https://glossarist.org/test/concept/1';
|
|
9
|
+
|
|
10
|
+
async function createTestRouter(uri = TEST_URI) {
|
|
11
|
+
const router = createRouter({
|
|
12
|
+
history: createMemoryHistory(),
|
|
13
|
+
routes: [
|
|
14
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
15
|
+
{ path: '/dataset/:registerId/concept/:conceptId', name: 'concept', component: { template: '<div/>' } },
|
|
16
|
+
{ path: '/resolve/:uri(.*)', name: 'resolve', component: { template: '<div/>' } },
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
router.push(`/resolve/${encodeURIComponent(uri)}`);
|
|
20
|
+
await router.isReady();
|
|
21
|
+
return router;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('ResolveView', () => {
|
|
25
|
+
let pinia: ReturnType<typeof createPinia>;
|
|
26
|
+
let router: Awaited<ReturnType<typeof createTestRouter>>;
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
resetFactory();
|
|
30
|
+
pinia = createPinia();
|
|
31
|
+
setActivePinia(pinia);
|
|
32
|
+
router = await createTestRouter();
|
|
33
|
+
// Pre-seed the factory with a stub adapter so discoverDatasets is skipped
|
|
34
|
+
const factory = getFactory();
|
|
35
|
+
const adapter = { registerId: 'test', manifest: null };
|
|
36
|
+
(factory as any).adapters.set('test', adapter);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function mountResolve() {
|
|
40
|
+
return mount(ResolveView, {
|
|
41
|
+
global: { plugins: [pinia, router] },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
it('shows resolving message while loading', () => {
|
|
46
|
+
const wrapper = mountResolve();
|
|
47
|
+
expect(wrapper.text()).toContain('Resolving...');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('shows error when concept not found', async () => {
|
|
51
|
+
const wrapper = mountResolve();
|
|
52
|
+
await flushPromises();
|
|
53
|
+
expect(wrapper.text()).toContain('Concept not found');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('displays the URI being resolved', async () => {
|
|
57
|
+
const wrapper = mountResolve();
|
|
58
|
+
await flushPromises();
|
|
59
|
+
expect(wrapper.text()).toContain('glossarist.org/test/concept/1');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('shows return to home link on error', async () => {
|
|
63
|
+
const wrapper = mountResolve();
|
|
64
|
+
await flushPromises();
|
|
65
|
+
const link = wrapper.findAll('a').find(a => a.text().includes('Return to home'));
|
|
66
|
+
expect(link).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('resolves the URI via the factory resolver', async () => {
|
|
70
|
+
const factory = getFactory();
|
|
71
|
+
factory.resolver.registerDataset('test', ['https://glossarist.org/test/*']);
|
|
72
|
+
const resolution = factory.resolve(TEST_URI);
|
|
73
|
+
expect(resolution.type).toBe('internal');
|
|
74
|
+
expect(resolution).toHaveProperty('registerId', 'test');
|
|
75
|
+
expect(resolution).toHaveProperty('conceptId', '1');
|
|
76
|
+
});
|
|
77
|
+
});
|