@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,159 @@
|
|
|
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 AppSidebar from '../components/AppSidebar.vue';
|
|
6
|
+
import { useVocabularyStore } from '../stores/vocabulary';
|
|
7
|
+
import { useUiStore } from '../stores/ui';
|
|
8
|
+
import type { Manifest } from '../adapters/types';
|
|
9
|
+
|
|
10
|
+
function makeManifest(id = 'test'): Manifest {
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
datasetUri: `https://glossarist.org/${id}/concept`,
|
|
14
|
+
title: `Test ${id}`,
|
|
15
|
+
description: 'A test dataset',
|
|
16
|
+
owner: 'ISO',
|
|
17
|
+
baseUrl: `/data/${id}`,
|
|
18
|
+
languages: ['eng'],
|
|
19
|
+
conceptCount: 50,
|
|
20
|
+
conceptUrlTemplate: `/data/${id}/concepts/{id}.json`,
|
|
21
|
+
indexUrl: `/data/${id}/index.json`,
|
|
22
|
+
contextUrl: `/data/${id}/context.json`,
|
|
23
|
+
uriBase: 'https://glossarist.org',
|
|
24
|
+
status: 'published',
|
|
25
|
+
schemaVersion: '1.0',
|
|
26
|
+
tags: [],
|
|
27
|
+
lastUpdated: '2025-01-01',
|
|
28
|
+
sourceRepo: 'https://example.com/repo',
|
|
29
|
+
chunkSize: 1000,
|
|
30
|
+
color: '#3366ff',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function createTestRouter(initialPath = '/') {
|
|
35
|
+
const router = createRouter({
|
|
36
|
+
history: createMemoryHistory(),
|
|
37
|
+
routes: [
|
|
38
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
39
|
+
{ path: '/dataset/:registerId', name: 'dataset', component: { template: '<div/>' } },
|
|
40
|
+
{ path: '/dataset/:registerId/concept/:conceptId', name: 'concept', component: { template: '<div/>' } },
|
|
41
|
+
{ path: '/search', name: 'search', component: { template: '<div/>' } },
|
|
42
|
+
{ path: '/graph', name: 'graph', component: { template: '<div/>' } },
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
router.push(initialPath);
|
|
46
|
+
await router.isReady();
|
|
47
|
+
return router;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('AppSidebar', () => {
|
|
51
|
+
let pinia: ReturnType<typeof createPinia>;
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
pinia = createPinia();
|
|
55
|
+
setActivePinia(pinia);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function seedStore(datasets: string[] = ['test']) {
|
|
59
|
+
const store = useVocabularyStore();
|
|
60
|
+
for (const id of datasets) {
|
|
61
|
+
store.manifests.set(id, makeManifest(id));
|
|
62
|
+
store.datasets.set(id, { index: [], getConceptCount: () => 0, getConcepts: () => [] } as any);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function mountSidebar(initialPath = '/') {
|
|
67
|
+
const router = await createTestRouter(initialPath);
|
|
68
|
+
return mount(AppSidebar, {
|
|
69
|
+
global: { plugins: [pinia, router], stubs: { NavIcon: true } },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
it('renders navigation section', async () => {
|
|
74
|
+
seedStore();
|
|
75
|
+
const wrapper = await mountSidebar();
|
|
76
|
+
expect(wrapper.text()).toContain('Navigation');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('renders dataset entries in sidebar', async () => {
|
|
80
|
+
seedStore(['iso1', 'iso2']);
|
|
81
|
+
const wrapper = await mountSidebar();
|
|
82
|
+
expect(wrapper.text()).toContain('Test iso1');
|
|
83
|
+
expect(wrapper.text()).toContain('Test iso2');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('shows concept count for loaded datasets', async () => {
|
|
87
|
+
seedStore();
|
|
88
|
+
const wrapper = await mountSidebar();
|
|
89
|
+
expect(wrapper.text()).toContain('50 concepts');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('navigates to dataset on click', async () => {
|
|
93
|
+
seedStore();
|
|
94
|
+
const router = await createTestRouter('/');
|
|
95
|
+
const wrapper = mount(AppSidebar, {
|
|
96
|
+
global: { plugins: [pinia, router], stubs: { NavIcon: true } },
|
|
97
|
+
});
|
|
98
|
+
const dsBtn = wrapper.findAll('button').find(b => b.text().includes('Test test'));
|
|
99
|
+
expect(dsBtn).toBeDefined();
|
|
100
|
+
await dsBtn!.trigger('click');
|
|
101
|
+
await flushPromises();
|
|
102
|
+
expect(router.currentRoute.value.name).toBe('dataset');
|
|
103
|
+
expect(router.currentRoute.value.params.registerId).toBe('test');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('closes sidebar on dataset click (mobile)', async () => {
|
|
107
|
+
seedStore();
|
|
108
|
+
const wrapper = await mountSidebar();
|
|
109
|
+
const ui = useUiStore();
|
|
110
|
+
ui.sidebarOpen = true;
|
|
111
|
+
const dsBtn = wrapper.findAll('button').find(b => b.text().includes('Test test'));
|
|
112
|
+
await dsBtn!.trigger('click');
|
|
113
|
+
expect(ui.sidebarOpen).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('shows mobile backdrop when sidebar open', async () => {
|
|
117
|
+
seedStore();
|
|
118
|
+
const wrapper = await mountSidebar();
|
|
119
|
+
const ui = useUiStore();
|
|
120
|
+
ui.sidebarOpen = true;
|
|
121
|
+
await wrapper.vm.$nextTick();
|
|
122
|
+
const backdrop = wrapper.find('.fixed.inset-0');
|
|
123
|
+
expect(backdrop.exists()).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('hides mobile backdrop when sidebar closed', async () => {
|
|
127
|
+
seedStore();
|
|
128
|
+
const wrapper = await mountSidebar();
|
|
129
|
+
const ui = useUiStore();
|
|
130
|
+
ui.sidebarOpen = false;
|
|
131
|
+
await wrapper.vm.$nextTick();
|
|
132
|
+
const backdrop = wrapper.find('.fixed.inset-0');
|
|
133
|
+
expect(backdrop.exists()).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('closes sidebar on backdrop click', async () => {
|
|
137
|
+
seedStore();
|
|
138
|
+
const wrapper = await mountSidebar();
|
|
139
|
+
const ui = useUiStore();
|
|
140
|
+
ui.sidebarOpen = true;
|
|
141
|
+
await wrapper.vm.$nextTick();
|
|
142
|
+
const backdrop = wrapper.find('.fixed.inset-0');
|
|
143
|
+
await backdrop.trigger('click');
|
|
144
|
+
expect(ui.sidebarOpen).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('shows dataset-level nav when on dataset route', async () => {
|
|
148
|
+
seedStore();
|
|
149
|
+
const wrapper = await mountSidebar('/dataset/test');
|
|
150
|
+
// The dataset nav section shows when currentManifest exists
|
|
151
|
+
expect(wrapper.text()).toContain('Test test');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('shows powered by link', async () => {
|
|
155
|
+
seedStore();
|
|
156
|
+
const wrapper = await mountSidebar();
|
|
157
|
+
expect(wrapper.text()).toContain('Glossarist Concept Browser');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -74,7 +74,7 @@ describe('renderAsciiDocLite', () => {
|
|
|
74
74
|
|
|
75
75
|
it('escapes HTML in source blocks', () => {
|
|
76
76
|
const result = renderAsciiDocLite('----\n<a href="evil">\n----');
|
|
77
|
-
expect(result).toContain('<a href
|
|
77
|
+
expect(result).toContain('<a href="evil">');
|
|
78
78
|
expect(result).not.toContain('<a href="evil">');
|
|
79
79
|
});
|
|
80
80
|
|
|
@@ -0,0 +1,115 @@
|
|
|
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 ConceptCard from '../components/ConceptCard.vue';
|
|
6
|
+
import { useVocabularyStore } from '../stores/vocabulary';
|
|
7
|
+
import type { Manifest, ConceptSummary } from '../adapters/types';
|
|
8
|
+
|
|
9
|
+
function makeManifest(): Manifest {
|
|
10
|
+
return {
|
|
11
|
+
id: 'test',
|
|
12
|
+
datasetUri: 'https://glossarist.org/test/concept',
|
|
13
|
+
title: 'Test Dataset',
|
|
14
|
+
description: 'A test dataset',
|
|
15
|
+
owner: 'ISO',
|
|
16
|
+
baseUrl: '/data/test',
|
|
17
|
+
languages: ['eng', 'fra'],
|
|
18
|
+
conceptCount: 10,
|
|
19
|
+
conceptUrlTemplate: '/data/test/concepts/{id}.json',
|
|
20
|
+
indexUrl: '/data/test/index.json',
|
|
21
|
+
contextUrl: '/data/test/context.json',
|
|
22
|
+
uriBase: 'https://glossarist.org',
|
|
23
|
+
status: 'published',
|
|
24
|
+
schemaVersion: '1.0',
|
|
25
|
+
tags: [],
|
|
26
|
+
lastUpdated: '2025-01-01',
|
|
27
|
+
sourceRepo: 'https://example.com/repo',
|
|
28
|
+
chunkSize: 1000,
|
|
29
|
+
color: '#3366ff',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeEntry(overrides: Partial<ConceptSummary> = {}): ConceptSummary {
|
|
34
|
+
return { id: '3.1.1.1', designations: { eng: 'test term' }, eng: 'test term', status: 'valid', ...overrides };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function createTestRouter() {
|
|
38
|
+
return createRouter({
|
|
39
|
+
history: createMemoryHistory(),
|
|
40
|
+
routes: [
|
|
41
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
42
|
+
{ path: '/dataset/:registerId', name: 'dataset', component: { template: '<div/>' } },
|
|
43
|
+
{ path: '/dataset/:registerId/concept/:conceptId', name: 'concept', component: { template: '<div/>' } },
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('ConceptCard', () => {
|
|
49
|
+
let pinia: ReturnType<typeof createPinia>;
|
|
50
|
+
let router: Awaited<ReturnType<typeof createTestRouter>>;
|
|
51
|
+
|
|
52
|
+
beforeEach(async () => {
|
|
53
|
+
pinia = createPinia();
|
|
54
|
+
setActivePinia(pinia);
|
|
55
|
+
router = await createTestRouter();
|
|
56
|
+
router.push('/');
|
|
57
|
+
await router.isReady();
|
|
58
|
+
const store = useVocabularyStore();
|
|
59
|
+
store.manifests.set('test', makeManifest());
|
|
60
|
+
store.datasets.set('test', { index: [], getConceptCount: () => 0, getConcepts: () => [] } as any);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function mountCard(entry = makeEntry(), registerId = 'test') {
|
|
64
|
+
return mount(ConceptCard, {
|
|
65
|
+
global: { plugins: [pinia, router] },
|
|
66
|
+
props: { entry, registerId },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
it('renders the English term as title', () => {
|
|
71
|
+
const wrapper = mountCard();
|
|
72
|
+
expect(wrapper.text()).toContain('test term');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('renders the concept ID', () => {
|
|
76
|
+
const wrapper = mountCard();
|
|
77
|
+
expect(wrapper.text()).toContain('3.1.1.1');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('falls back to ID when no English term', () => {
|
|
81
|
+
const wrapper = mountCard(makeEntry({ eng: '' }));
|
|
82
|
+
expect(wrapper.text()).toContain('3.1.1.1');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('shows valid status badge', () => {
|
|
86
|
+
const wrapper = mountCard();
|
|
87
|
+
expect(wrapper.text()).toContain('valid');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('shows superseded status with reduced opacity', () => {
|
|
91
|
+
const wrapper = mountCard(makeEntry({ status: 'superseded' }));
|
|
92
|
+
expect(wrapper.text()).toContain('superseded');
|
|
93
|
+
expect(wrapper.find('button').classes()).toContain('opacity-70');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('shows withdrawn status with reduced opacity', () => {
|
|
97
|
+
const wrapper = mountCard(makeEntry({ status: 'withdrawn' }));
|
|
98
|
+
expect(wrapper.text()).toContain('withdrawn');
|
|
99
|
+
expect(wrapper.find('button').classes()).toContain('opacity-70');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('navigates to concept page on click', async () => {
|
|
103
|
+
const wrapper = mountCard();
|
|
104
|
+
await wrapper.find('button').trigger('click');
|
|
105
|
+
await flushPromises();
|
|
106
|
+
expect(router.currentRoute.value.name).toBe('concept');
|
|
107
|
+
expect(router.currentRoute.value.params.registerId).toBe('test');
|
|
108
|
+
expect(router.currentRoute.value.params.conceptId).toBe('3.1.1.1');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('shows language count from manifest', () => {
|
|
112
|
+
const wrapper = mountCard();
|
|
113
|
+
expect(wrapper.text()).toContain('2 lang');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,273 @@
|
|
|
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 ConceptDetail from '../components/ConceptDetail.vue';
|
|
6
|
+
import { useVocabularyStore } from '../stores/vocabulary';
|
|
7
|
+
import type { Manifest } from '../adapters/types';
|
|
8
|
+
import { conceptFromJson } from '../adapters/model-bridge';
|
|
9
|
+
// Prevent the 2.7MB Opal runtime from loading in tests
|
|
10
|
+
vi.mock('../utils/plurimath', () => ({
|
|
11
|
+
loadPlurimath: () => new Promise(() => {}),
|
|
12
|
+
mathToHtml: () => '<code class="math-fallback">x</code>',
|
|
13
|
+
renderToMathML: () => null,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
import { vMath } from '../directives/v-math';
|
|
17
|
+
|
|
18
|
+
function makeManifest(): Manifest {
|
|
19
|
+
return {
|
|
20
|
+
id: 'test',
|
|
21
|
+
datasetUri: 'https://glossarist.org/test/concept',
|
|
22
|
+
title: 'Test Dataset',
|
|
23
|
+
description: 'A test dataset',
|
|
24
|
+
owner: 'ISO',
|
|
25
|
+
baseUrl: '/data/test',
|
|
26
|
+
languages: ['eng', 'fra'],
|
|
27
|
+
conceptCount: 1,
|
|
28
|
+
conceptUrlTemplate: '/data/test/concepts/{id}.json',
|
|
29
|
+
indexUrl: '/data/test/index.json',
|
|
30
|
+
contextUrl: '/data/test/context.json',
|
|
31
|
+
uriBase: 'https://glossarist.org',
|
|
32
|
+
status: 'published',
|
|
33
|
+
schemaVersion: '1.0',
|
|
34
|
+
tags: [],
|
|
35
|
+
lastUpdated: '2025-01-01',
|
|
36
|
+
sourceRepo: 'https://example.com/repo',
|
|
37
|
+
chunkSize: 1000,
|
|
38
|
+
languageOrder: ['eng', 'fra'],
|
|
39
|
+
color: '#3366ff',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeConceptJson(overrides: Record<string, any> = {}) {
|
|
44
|
+
return {
|
|
45
|
+
'@context': 'https://glossarist.org/context',
|
|
46
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
47
|
+
'@type': 'gl:Concept',
|
|
48
|
+
'gl:identifier': '1',
|
|
49
|
+
'gl:localizedConcept': {
|
|
50
|
+
eng: {
|
|
51
|
+
'@id': 'https://glossarist.org/test/concept/1/eng',
|
|
52
|
+
'@type': 'gl:LocalizedConcept',
|
|
53
|
+
'gl:languageCode': 'eng',
|
|
54
|
+
'gl:entryStatus': 'valid',
|
|
55
|
+
'gl:designation': [
|
|
56
|
+
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': 'test term' },
|
|
57
|
+
{ '@type': 'gl:Symbol', 'gl:normativeStatus': 'admitted', 'gl:term': 'stem:[x]' },
|
|
58
|
+
],
|
|
59
|
+
'gl:definition': [
|
|
60
|
+
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'a definition with *italic* text' },
|
|
61
|
+
],
|
|
62
|
+
'gl:notes': [
|
|
63
|
+
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'a note' },
|
|
64
|
+
],
|
|
65
|
+
'gl:examples': [
|
|
66
|
+
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'an example' },
|
|
67
|
+
],
|
|
68
|
+
'gl:source': [{ '@type': 'gl:Source', 'gl:sourceType': 'authoritative' }],
|
|
69
|
+
},
|
|
70
|
+
fra: {
|
|
71
|
+
'@id': 'https://glossarist.org/test/concept/1/fra',
|
|
72
|
+
'@type': 'gl:LocalizedConcept',
|
|
73
|
+
'gl:languageCode': 'fra',
|
|
74
|
+
'gl:entryStatus': 'valid',
|
|
75
|
+
'gl:designation': [
|
|
76
|
+
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': 'terme de test' },
|
|
77
|
+
],
|
|
78
|
+
'gl:definition': [
|
|
79
|
+
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'une définition' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
...overrides,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function createTestRouter() {
|
|
88
|
+
return createRouter({
|
|
89
|
+
history: createMemoryHistory(),
|
|
90
|
+
routes: [
|
|
91
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
92
|
+
{ path: '/dataset/:registerId', name: 'dataset', component: { template: '<div/>' } },
|
|
93
|
+
{ path: '/dataset/:registerId/concept/:conceptId', name: 'concept', component: { template: '<div/>' } },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe('ConceptDetail interactions', () => {
|
|
99
|
+
let pinia: ReturnType<typeof createPinia>;
|
|
100
|
+
let router: Awaited<ReturnType<typeof createTestRouter>>;
|
|
101
|
+
let store: ReturnType<typeof useVocabularyStore>;
|
|
102
|
+
|
|
103
|
+
beforeEach(async () => {
|
|
104
|
+
pinia = createPinia();
|
|
105
|
+
setActivePinia(pinia);
|
|
106
|
+
router = await createTestRouter();
|
|
107
|
+
router.push('/');
|
|
108
|
+
await router.isReady();
|
|
109
|
+
store = useVocabularyStore();
|
|
110
|
+
store.manifests.set('test', makeManifest());
|
|
111
|
+
store.datasets.set('test', { index: [], getConceptCount: () => 0, getConcepts: () => [], getConceptPosition: () => -1, getIndexEntry: () => undefined } as any);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
function mountDetail(conceptJson: Record<string, any> = makeConceptJson()) {
|
|
115
|
+
const concept = conceptFromJson(conceptJson);
|
|
116
|
+
return mount(ConceptDetail, {
|
|
117
|
+
global: {
|
|
118
|
+
plugins: [pinia, router],
|
|
119
|
+
directives: { math: vMath },
|
|
120
|
+
},
|
|
121
|
+
props: {
|
|
122
|
+
concept,
|
|
123
|
+
manifest: makeManifest(),
|
|
124
|
+
edges: [],
|
|
125
|
+
registerId: 'test',
|
|
126
|
+
adjacent: { prev: null, next: null },
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
it('renders the primary term in the header', () => {
|
|
132
|
+
const wrapper = mountDetail();
|
|
133
|
+
expect(wrapper.find('h1').html()).toContain('test term');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
async function switchToDefinition(wrapper: ReturnType<typeof mountDetail>) {
|
|
137
|
+
const tabs = wrapper.findAll('button[role="tab"]');
|
|
138
|
+
const defTab = tabs.find(t => t.text().includes('Definition'));
|
|
139
|
+
if (defTab) {
|
|
140
|
+
await defTab.trigger('click');
|
|
141
|
+
await flushPromises();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
it('renders concept ID badge', () => {
|
|
146
|
+
const wrapper = mountDetail();
|
|
147
|
+
expect(wrapper.text()).toContain('1');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('renders language sections for eng and fra', async () => {
|
|
151
|
+
const wrapper = mountDetail();
|
|
152
|
+
await switchToDefinition(wrapper);
|
|
153
|
+
expect(wrapper.text()).toContain('English');
|
|
154
|
+
expect(wrapper.text()).toContain('French');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('renders italic text in definition', async () => {
|
|
158
|
+
const wrapper = mountDetail();
|
|
159
|
+
await switchToDefinition(wrapper);
|
|
160
|
+
expect(wrapper.html()).toContain('<em>italic</em>');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('renders stem: notation as math-pending placeholder', async () => {
|
|
164
|
+
const wrapper = mountDetail();
|
|
165
|
+
await switchToDefinition(wrapper);
|
|
166
|
+
expect(wrapper.html()).toContain('math-pending');
|
|
167
|
+
expect(wrapper.html()).toContain('data-expr="x"');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('renders notes section', async () => {
|
|
171
|
+
const wrapper = mountDetail();
|
|
172
|
+
await switchToDefinition(wrapper);
|
|
173
|
+
expect(wrapper.text()).toContain('Note 1');
|
|
174
|
+
expect(wrapper.text()).toContain('a note');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('renders examples section', async () => {
|
|
178
|
+
const wrapper = mountDetail();
|
|
179
|
+
await switchToDefinition(wrapper);
|
|
180
|
+
expect(wrapper.text()).toContain('Example 1');
|
|
181
|
+
expect(wrapper.text()).toContain('an example');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('renders designation types as badges', async () => {
|
|
185
|
+
const wrapper = mountDetail();
|
|
186
|
+
await switchToDefinition(wrapper);
|
|
187
|
+
expect(wrapper.text()).toContain('symbol');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('collapses non-eng languages when 6+ languages present', async () => {
|
|
191
|
+
const json = makeConceptJson() as Record<string, any>;
|
|
192
|
+
for (const lang of ['deu', 'spa', 'kor', 'jpn']) {
|
|
193
|
+
json['gl:localizedConcept'][lang] = {
|
|
194
|
+
'@id': `https://glossarist.org/test/concept/1/${lang}`,
|
|
195
|
+
'@type': 'gl:LocalizedConcept',
|
|
196
|
+
'gl:languageCode': lang,
|
|
197
|
+
'gl:designation': [
|
|
198
|
+
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': `term-${lang}` },
|
|
199
|
+
],
|
|
200
|
+
'gl:definition': [
|
|
201
|
+
{ '@type': 'gl:DetailedDefinition', 'gl:content': `def-${lang}` },
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const wrapper = mountDetail(json);
|
|
206
|
+
await switchToDefinition(wrapper);
|
|
207
|
+
await flushPromises();
|
|
208
|
+
expect(wrapper.text()).toContain('6 languages');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('toggles language section on click', async () => {
|
|
212
|
+
const wrapper = mountDetail();
|
|
213
|
+
await switchToDefinition(wrapper);
|
|
214
|
+
const buttons = wrapper.findAll('button');
|
|
215
|
+
const fraButton = buttons.find(b => b.text().includes('French'));
|
|
216
|
+
expect(fraButton).toBeDefined();
|
|
217
|
+
|
|
218
|
+
await fraButton!.trigger('click');
|
|
219
|
+
await fraButton!.trigger('click');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('switches between definition and history tabs', async () => {
|
|
223
|
+
const wrapper = mountDetail();
|
|
224
|
+
await switchToDefinition(wrapper);
|
|
225
|
+
expect(wrapper.text()).toContain('a definition with');
|
|
226
|
+
|
|
227
|
+
const tabs = wrapper.findAll('button[role="tab"]');
|
|
228
|
+
const historyTab = tabs.find(t => t.text().includes('History'));
|
|
229
|
+
expect(historyTab).toBeDefined();
|
|
230
|
+
await historyTab!.trigger('click');
|
|
231
|
+
await flushPromises();
|
|
232
|
+
|
|
233
|
+
expect(wrapper.text()).not.toContain('a definition with');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('renders cross-reference link and navigates on click', async () => {
|
|
237
|
+
const json = makeConceptJson();
|
|
238
|
+
json['gl:localizedConcept'].eng['gl:definition'] = [
|
|
239
|
+
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'see {{urn:iso:std:iso:14812:3.1.1.1,entity}} here' },
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
// Register the URI pattern via factory so it resolves as internal
|
|
243
|
+
const { getFactory } = await import('../adapters/factory');
|
|
244
|
+
const factory = getFactory();
|
|
245
|
+
factory.router.registerDataset('test', '/data/test', {
|
|
246
|
+
...makeManifest(),
|
|
247
|
+
uriBase: 'https://glossarist.org',
|
|
248
|
+
});
|
|
249
|
+
factory.resolver.registerDataset('test', ['https://glossarist.org/test/concept/*']);
|
|
250
|
+
|
|
251
|
+
const wrapper = mountDetail(json);
|
|
252
|
+
await switchToDefinition(wrapper);
|
|
253
|
+
await flushPromises();
|
|
254
|
+
|
|
255
|
+
const xref = wrapper.find('.xref-link');
|
|
256
|
+
if (xref.exists()) {
|
|
257
|
+
await xref.trigger('click');
|
|
258
|
+
await flushPromises();
|
|
259
|
+
expect(router.currentRoute.value.name).toBe('concept');
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('renders entry status badge', () => {
|
|
264
|
+
const wrapper = mountDetail();
|
|
265
|
+
expect(wrapper.text()).toContain('valid');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('renders the language quick-jump sidebar with all languages', async () => {
|
|
269
|
+
const wrapper = mountDetail();
|
|
270
|
+
await switchToDefinition(wrapper);
|
|
271
|
+
expect(wrapper.text()).toContain('Languages (2)');
|
|
272
|
+
});
|
|
273
|
+
});
|
|
@@ -1,37 +1,33 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { FORMAT_REGISTRY, conceptToTurtle, conceptToSkosJsonLd } from '../utils/concept-formats';
|
|
3
|
-
import
|
|
3
|
+
import { Concept } from 'glossarist';
|
|
4
4
|
|
|
5
|
-
function makeConcept(overrides:
|
|
6
|
-
return {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'gl:identifier': '1',
|
|
11
|
-
'gl:localizedConcept': {
|
|
5
|
+
function makeConcept(overrides: Record<string, unknown> = {}): Concept {
|
|
6
|
+
return Concept.fromJSON({
|
|
7
|
+
id: '1',
|
|
8
|
+
uri: 'https://glossarist.org/test/concept/1',
|
|
9
|
+
localizations: {
|
|
12
10
|
eng: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'admitted', 'gl:term': 'alt term' },
|
|
11
|
+
language_code: 'eng',
|
|
12
|
+
entry_status: 'valid',
|
|
13
|
+
terms: [
|
|
14
|
+
{ type: 'expression', designation: 'test term', normative_status: 'preferred' },
|
|
15
|
+
{ type: 'expression', designation: 'alt term', normative_status: 'admitted' },
|
|
19
16
|
],
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
definition: [{ content: 'a definition' }],
|
|
18
|
+
notes: [{ content: 'a note' }],
|
|
22
19
|
},
|
|
23
20
|
deu: {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': 'Testbegriff' },
|
|
21
|
+
language_code: 'deu',
|
|
22
|
+
entry_status: 'valid',
|
|
23
|
+
terms: [
|
|
24
|
+
{ type: 'expression', designation: 'Testbegriff', normative_status: 'preferred' },
|
|
29
25
|
],
|
|
30
|
-
|
|
26
|
+
definition: [{ content: 'eine Definition' }],
|
|
31
27
|
},
|
|
32
28
|
},
|
|
33
29
|
...overrides,
|
|
34
|
-
};
|
|
30
|
+
});
|
|
35
31
|
}
|
|
36
32
|
|
|
37
33
|
describe('FORMAT_REGISTRY', () => {
|
|
@@ -64,17 +60,24 @@ describe('conceptToTurtle', () => {
|
|
|
64
60
|
});
|
|
65
61
|
|
|
66
62
|
it('escapes special characters in Turtle', () => {
|
|
67
|
-
const concept =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
const concept = Concept.fromJSON({
|
|
64
|
+
id: '1',
|
|
65
|
+
uri: 'https://glossarist.org/test/concept/1',
|
|
66
|
+
localizations: {
|
|
67
|
+
eng: {
|
|
68
|
+
language_code: 'eng',
|
|
69
|
+
terms: [{ type: 'expression', designation: 'test', normative_status: 'preferred' }],
|
|
70
|
+
definition: [{ content: 'has "quotes" and \\backslash' }],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
71
74
|
const ttl = conceptToTurtle(concept);
|
|
72
75
|
expect(ttl).toContain('\\"quotes\\"');
|
|
73
76
|
expect(ttl).toContain('\\\\backslash');
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
it('handles empty concept gracefully', () => {
|
|
77
|
-
const ttl = conceptToTurtle({}
|
|
80
|
+
const ttl = conceptToTurtle(Concept.fromJSON({}));
|
|
78
81
|
expect(ttl).toContain('a skos:Concept');
|
|
79
82
|
expect(ttl).toContain('skos:notation ""');
|
|
80
83
|
});
|
|
@@ -100,8 +103,7 @@ describe('conceptToSkosJsonLd', () => {
|
|
|
100
103
|
});
|
|
101
104
|
|
|
102
105
|
it('omits empty language maps', () => {
|
|
103
|
-
const concept =
|
|
104
|
-
concept['gl:localizedConcept'] = {};
|
|
106
|
+
const concept = Concept.fromJSON({ id: '1', uri: 'https://glossarist.org/test/concept/1' });
|
|
105
107
|
const parsed = JSON.parse(conceptToSkosJsonLd(concept));
|
|
106
108
|
expect(parsed['skos:prefLabel']).toBeUndefined();
|
|
107
109
|
expect(parsed['skos:definition']).toBeUndefined();
|