@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { DatasetAdapter } from '../adapters/DatasetAdapter';
|
|
3
|
+
import { conceptFromJson } from '../adapters/model-bridge';
|
|
3
4
|
|
|
4
5
|
// Mock fetch globally
|
|
5
6
|
const mockFetch = vi.fn();
|
|
@@ -98,13 +99,13 @@ describe('DatasetAdapter', () => {
|
|
|
98
99
|
mockFetch.mockReturnValue(mockJsonResponse(concept));
|
|
99
100
|
|
|
100
101
|
const result = await adapter.fetchConcept('103-01-02');
|
|
101
|
-
expect(result
|
|
102
|
+
expect(result.id).toBe('103-01-02');
|
|
102
103
|
expect(mockFetch).toHaveBeenCalledWith('/data/test/concepts/103-01-02.json');
|
|
103
104
|
|
|
104
105
|
// Second call should use cache
|
|
105
106
|
mockFetch.mockReset();
|
|
106
107
|
const cached = await adapter.fetchConcept('103-01-02');
|
|
107
|
-
expect(cached
|
|
108
|
+
expect(cached.id).toBe('103-01-02');
|
|
108
109
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
109
110
|
});
|
|
110
111
|
|
|
@@ -195,12 +196,56 @@ describe('DatasetAdapter', () => {
|
|
|
195
196
|
const hits = adapter.search('xyznotfound');
|
|
196
197
|
expect(hits.length).toBe(0);
|
|
197
198
|
});
|
|
199
|
+
|
|
200
|
+
it('ranks exact matches above starts-with above contains', async () => {
|
|
201
|
+
const index = {
|
|
202
|
+
registerId: 'test',
|
|
203
|
+
schemaVersion: '1.0.0',
|
|
204
|
+
conceptCount: 3,
|
|
205
|
+
chunkSize: 500,
|
|
206
|
+
chunks: [],
|
|
207
|
+
concepts: [
|
|
208
|
+
{ id: '1', designations: { eng: 'mass' }, eng: 'mass', status: 'valid' },
|
|
209
|
+
{ id: '2', designations: { eng: 'mass flow rate' }, eng: 'mass flow rate', status: 'valid' },
|
|
210
|
+
{ id: '3', designations: { eng: 'center of mass' }, eng: 'center of mass', status: 'valid' },
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
mockFetch.mockReturnValue(mockJsonResponse(index));
|
|
214
|
+
await adapter.loadIndex();
|
|
215
|
+
|
|
216
|
+
const hits = adapter.search('mass');
|
|
217
|
+
// Exact match first, then starts-with, then contains
|
|
218
|
+
expect(hits[0].conceptId).toBe('1');
|
|
219
|
+
expect(hits[1].conceptId).toBe('2');
|
|
220
|
+
expect(hits[2].conceptId).toBe('3');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('ranks ID exact match highest', async () => {
|
|
224
|
+
const index = {
|
|
225
|
+
registerId: 'test',
|
|
226
|
+
schemaVersion: '1.0.0',
|
|
227
|
+
conceptCount: 2,
|
|
228
|
+
chunkSize: 500,
|
|
229
|
+
chunks: [],
|
|
230
|
+
concepts: [
|
|
231
|
+
{ id: '102-01-01', designations: { eng: 'field' }, eng: 'field', status: 'valid' },
|
|
232
|
+
{ id: '102-01-02', designations: { eng: 'electromagnetic field' }, eng: 'electromagnetic field', status: 'valid' },
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
mockFetch.mockReturnValue(mockJsonResponse(index));
|
|
236
|
+
await adapter.loadIndex();
|
|
237
|
+
|
|
238
|
+
const hits = adapter.search('102-01-01');
|
|
239
|
+
expect(hits[0].conceptId).toBe('102-01-01');
|
|
240
|
+
expect(hits[0].matchField).toBe('id');
|
|
241
|
+
});
|
|
198
242
|
});
|
|
199
243
|
|
|
200
244
|
describe('extractEdges', () => {
|
|
201
245
|
it('extracts cross-reference edges from gl:references', () => {
|
|
202
|
-
const concept = {
|
|
246
|
+
const concept = conceptFromJson({
|
|
203
247
|
'@id': 'https://glossarist.org/test/concept/102-01-01',
|
|
248
|
+
'@type': 'gl:Concept',
|
|
204
249
|
'gl:localizedConcept': {
|
|
205
250
|
eng: {
|
|
206
251
|
'gl:references': [
|
|
@@ -209,18 +254,38 @@ describe('DatasetAdapter', () => {
|
|
|
209
254
|
],
|
|
210
255
|
},
|
|
211
256
|
},
|
|
212
|
-
};
|
|
257
|
+
});
|
|
213
258
|
|
|
214
|
-
const edges = adapter.extractEdges(concept
|
|
259
|
+
const edges = adapter.extractEdges(concept);
|
|
215
260
|
expect(edges.length).toBe(2);
|
|
216
261
|
expect(edges[0].target).toBe('https://glossarist.org/iev/concept/103-01-02');
|
|
217
262
|
expect(edges[0].type).toBe('references');
|
|
218
263
|
expect(edges[0].label).toBe('functional');
|
|
219
264
|
});
|
|
220
265
|
|
|
266
|
+
it('tags reference edges with language', () => {
|
|
267
|
+
const concept = conceptFromJson({
|
|
268
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
269
|
+
'@type': 'gl:Concept',
|
|
270
|
+
'gl:localizedConcept': {
|
|
271
|
+
eng: { 'gl:references': [
|
|
272
|
+
{ '@id': 'https://glossarist.org/test/concept/2', 'gl:term': 'other' },
|
|
273
|
+
]},
|
|
274
|
+
fra: { 'gl:references': [
|
|
275
|
+
{ '@id': 'https://glossarist.org/test/concept/3', 'gl:term': 'autre' },
|
|
276
|
+
]},
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const edges = adapter.extractEdges(concept);
|
|
280
|
+
expect(edges.length).toBe(2);
|
|
281
|
+
expect(edges.find(e => e.lang === 'eng')?.target).toContain('/concept/2');
|
|
282
|
+
expect(edges.find(e => e.lang === 'fra')?.target).toContain('/concept/3');
|
|
283
|
+
});
|
|
284
|
+
|
|
221
285
|
it('skips self-references', () => {
|
|
222
|
-
const concept = {
|
|
286
|
+
const concept = conceptFromJson({
|
|
223
287
|
'@id': 'https://glossarist.org/test/concept/102-01-01',
|
|
288
|
+
'@type': 'gl:Concept',
|
|
224
289
|
'gl:localizedConcept': {
|
|
225
290
|
eng: {
|
|
226
291
|
'gl:references': [
|
|
@@ -228,37 +293,40 @@ describe('DatasetAdapter', () => {
|
|
|
228
293
|
],
|
|
229
294
|
},
|
|
230
295
|
},
|
|
231
|
-
};
|
|
296
|
+
});
|
|
232
297
|
|
|
233
|
-
const edges = adapter.extractEdges(concept
|
|
298
|
+
const edges = adapter.extractEdges(concept);
|
|
234
299
|
expect(edges.length).toBe(0);
|
|
235
300
|
});
|
|
236
301
|
|
|
237
302
|
it('handles concepts with no references', () => {
|
|
238
|
-
const concept = {
|
|
303
|
+
const concept = conceptFromJson({
|
|
239
304
|
'@id': 'https://glossarist.org/test/concept/102-01-01',
|
|
305
|
+
'@type': 'gl:Concept',
|
|
240
306
|
'gl:localizedConcept': {
|
|
241
307
|
eng: {},
|
|
242
308
|
},
|
|
243
|
-
};
|
|
309
|
+
});
|
|
244
310
|
|
|
245
|
-
const edges = adapter.extractEdges(concept
|
|
311
|
+
const edges = adapter.extractEdges(concept);
|
|
246
312
|
expect(edges.length).toBe(0);
|
|
247
313
|
});
|
|
248
314
|
|
|
249
315
|
it('handles empty localizedConcept', () => {
|
|
250
|
-
const concept = {
|
|
316
|
+
const concept = conceptFromJson({
|
|
251
317
|
'@id': 'https://glossarist.org/test/concept/102-01-01',
|
|
318
|
+
'@type': 'gl:Concept',
|
|
252
319
|
'gl:localizedConcept': {},
|
|
253
|
-
};
|
|
320
|
+
});
|
|
254
321
|
|
|
255
|
-
const edges = adapter.extractEdges(concept
|
|
322
|
+
const edges = adapter.extractEdges(concept);
|
|
256
323
|
expect(edges.length).toBe(0);
|
|
257
324
|
});
|
|
258
325
|
|
|
259
326
|
it('collects references from multiple languages without duplication', () => {
|
|
260
|
-
const concept = {
|
|
327
|
+
const concept = conceptFromJson({
|
|
261
328
|
'@id': 'https://glossarist.org/test/concept/102-01-01',
|
|
329
|
+
'@type': 'gl:Concept',
|
|
262
330
|
'gl:localizedConcept': {
|
|
263
331
|
eng: {
|
|
264
332
|
'gl:references': [
|
|
@@ -271,16 +339,17 @@ describe('DatasetAdapter', () => {
|
|
|
271
339
|
],
|
|
272
340
|
},
|
|
273
341
|
},
|
|
274
|
-
};
|
|
342
|
+
});
|
|
275
343
|
|
|
276
344
|
// Same target from two languages — both edges are kept (different labels)
|
|
277
|
-
const edges = adapter.extractEdges(concept
|
|
345
|
+
const edges = adapter.extractEdges(concept);
|
|
278
346
|
expect(edges.length).toBe(2);
|
|
279
347
|
});
|
|
280
348
|
|
|
281
349
|
it('extracts inline IEV cross-references from gl:references', () => {
|
|
282
|
-
const concept = {
|
|
350
|
+
const concept = conceptFromJson({
|
|
283
351
|
'@id': 'https://glossarist.org/test/concept/112-01-01',
|
|
352
|
+
'@type': 'gl:Concept',
|
|
284
353
|
'gl:localizedConcept': {
|
|
285
354
|
eng: {
|
|
286
355
|
'gl:references': [
|
|
@@ -289,9 +358,9 @@ describe('DatasetAdapter', () => {
|
|
|
289
358
|
],
|
|
290
359
|
},
|
|
291
360
|
},
|
|
292
|
-
};
|
|
361
|
+
});
|
|
293
362
|
|
|
294
|
-
const edges = adapter.extractEdges(concept
|
|
363
|
+
const edges = adapter.extractEdges(concept);
|
|
295
364
|
expect(edges.length).toBe(2);
|
|
296
365
|
expect(edges[0].target).toBe('https://glossarist.org/iev/concept/102-02-18');
|
|
297
366
|
expect(edges[0].label).toBe('scalar');
|
|
@@ -300,8 +369,9 @@ describe('DatasetAdapter', () => {
|
|
|
300
369
|
});
|
|
301
370
|
|
|
302
371
|
it('extracts inline URN cross-references from gl:references', () => {
|
|
303
|
-
const concept = {
|
|
372
|
+
const concept = conceptFromJson({
|
|
304
373
|
'@id': 'https://glossarist.org/test/concept/3.1.1.1',
|
|
374
|
+
'@type': 'gl:Concept',
|
|
305
375
|
'gl:localizedConcept': {
|
|
306
376
|
eng: {
|
|
307
377
|
'gl:references': [
|
|
@@ -309,15 +379,94 @@ describe('DatasetAdapter', () => {
|
|
|
309
379
|
],
|
|
310
380
|
},
|
|
311
381
|
},
|
|
312
|
-
};
|
|
382
|
+
});
|
|
313
383
|
|
|
314
|
-
const edges = adapter.extractEdges(concept
|
|
384
|
+
const edges = adapter.extractEdges(concept);
|
|
315
385
|
expect(edges.length).toBe(1);
|
|
316
386
|
expect(edges[0].target).toBe('https://glossarist.org/isotc204/concept/3.1.1.6');
|
|
317
387
|
expect(edges[0].label).toBe('entity');
|
|
318
388
|
});
|
|
319
389
|
});
|
|
320
390
|
|
|
391
|
+
describe('extractDomainEdges', () => {
|
|
392
|
+
it('extracts domain edges from gl:domain field per language', () => {
|
|
393
|
+
const concept = conceptFromJson({
|
|
394
|
+
'@id': 'https://glossarist.org/test/concept/3',
|
|
395
|
+
'@type': 'gl:Concept',
|
|
396
|
+
'gl:localizedConcept': {
|
|
397
|
+
eng: { 'gl:domain': 'geometry' },
|
|
398
|
+
fra: { 'gl:domain': 'géométrie' },
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
const edges = adapter.extractDomainEdges(concept);
|
|
402
|
+
expect(edges.length).toBe(2);
|
|
403
|
+
expect(edges.every(e => e.type === 'domain')).toBe(true);
|
|
404
|
+
expect(edges.find(e => e.lang === 'eng')?.target).toContain('/domain/geometry');
|
|
405
|
+
expect(edges.find(e => e.lang === 'fra')?.target).toContain('/domain/gomtrie');
|
|
406
|
+
expect(edges.find(e => e.lang === 'eng')?.label).toBe('geometry');
|
|
407
|
+
expect(edges.find(e => e.lang === 'fra')?.label).toBe('géométrie');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('handles same domain across languages', () => {
|
|
411
|
+
const concept = conceptFromJson({
|
|
412
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
413
|
+
'@type': 'gl:Concept',
|
|
414
|
+
'gl:localizedConcept': {
|
|
415
|
+
eng: { 'gl:domain': 'metadata' },
|
|
416
|
+
fra: { 'gl:domain': 'metadata' },
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
const edges = adapter.extractDomainEdges(concept);
|
|
420
|
+
expect(edges.length).toBe(2);
|
|
421
|
+
expect(edges[0].target).toBe(edges[1].target);
|
|
422
|
+
expect(edges[0].target).toContain('/domain/metadata');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('skips concepts without gl:domain', () => {
|
|
426
|
+
const concept = conceptFromJson({
|
|
427
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
428
|
+
'@type': 'gl:Concept',
|
|
429
|
+
'gl:localizedConcept': { eng: {} },
|
|
430
|
+
});
|
|
431
|
+
const edges = adapter.extractDomainEdges(concept);
|
|
432
|
+
expect(edges.length).toBe(0);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('handles empty localizedConcept', () => {
|
|
436
|
+
const concept = conceptFromJson({
|
|
437
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
438
|
+
'@type': 'gl:Concept',
|
|
439
|
+
'gl:localizedConcept': {},
|
|
440
|
+
});
|
|
441
|
+
const edges = adapter.extractDomainEdges(concept);
|
|
442
|
+
expect(edges.length).toBe(0);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
describe('loadDomainNodes', () => {
|
|
447
|
+
it('loads domain nodes from domain-nodes.json', async () => {
|
|
448
|
+
mockFetch.mockReturnValue(mockJsonResponse({
|
|
449
|
+
registerId: 'test',
|
|
450
|
+
domainNodes: [
|
|
451
|
+
{ uri: 'https://glossarist.org/test/domain/iso-19107', label: 'ISO 19107', registerId: 'test', conceptCount: 147 },
|
|
452
|
+
],
|
|
453
|
+
}));
|
|
454
|
+
const nodes = await adapter.loadDomainNodes();
|
|
455
|
+
expect(nodes.length).toBe(1);
|
|
456
|
+
expect(nodes[0].nodeType).toBe('domain');
|
|
457
|
+
expect(nodes[0].status).toBe('domain');
|
|
458
|
+
expect(nodes[0].loaded).toBe(true);
|
|
459
|
+
expect(nodes[0].designations.eng).toBe('ISO 19107');
|
|
460
|
+
expect(mockFetch).toHaveBeenCalledWith('/data/test/domain-nodes.json');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('returns empty array on fetch failure', async () => {
|
|
464
|
+
mockFetch.mockReturnValue(Promise.resolve({ ok: false, status: 404 } as Response));
|
|
465
|
+
const nodes = await adapter.loadDomainNodes();
|
|
466
|
+
expect(nodes).toEqual([]);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
321
470
|
describe('getLanguages', () => {
|
|
322
471
|
it('returns languages from manifest', async () => {
|
|
323
472
|
const manifest = {
|
|
@@ -0,0 +1,232 @@
|
|
|
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 DatasetView from '../views/DatasetView.vue';
|
|
6
|
+
import { useVocabularyStore } from '../stores/vocabulary';
|
|
7
|
+
import type { Manifest, ConceptSummary } from '../adapters/types';
|
|
8
|
+
|
|
9
|
+
function makeManifest(overrides: Partial<Manifest> = {}): 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: 100,
|
|
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
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeConcepts(count: number): ConceptSummary[] {
|
|
35
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
36
|
+
id: String(i + 1),
|
|
37
|
+
designations: { eng: `term ${i + 1}` },
|
|
38
|
+
eng: `term ${i + 1}`,
|
|
39
|
+
status: i % 10 === 0 ? 'superseded' : 'valid',
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeAdapter(concepts: ConceptSummary[] = []) {
|
|
44
|
+
const dense = concepts.filter(Boolean);
|
|
45
|
+
return {
|
|
46
|
+
registerId: 'test',
|
|
47
|
+
index: dense,
|
|
48
|
+
manifest: null,
|
|
49
|
+
getConceptCount: () => dense.length,
|
|
50
|
+
getConcepts: () => dense,
|
|
51
|
+
isRangeLoaded: () => true,
|
|
52
|
+
ensureChunksForRange: async () => {},
|
|
53
|
+
ensureAllChunksLoaded: async () => {},
|
|
54
|
+
getAdjacentConcepts: () => ({ prev: null, next: null }),
|
|
55
|
+
} as any;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function createTestRouter() {
|
|
59
|
+
const router = createRouter({
|
|
60
|
+
history: createMemoryHistory(),
|
|
61
|
+
routes: [
|
|
62
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
63
|
+
{ path: '/dataset/:registerId', name: 'dataset', component: { template: '<div/>' } },
|
|
64
|
+
{ path: '/dataset/:registerId/concept/:conceptId', name: 'concept', component: { template: '<div/>' } },
|
|
65
|
+
{ path: '/dataset/:registerId/stats', name: 'stats', component: { template: '<div/>' } },
|
|
66
|
+
{ path: '/dataset/:registerId/about', name: 'about', component: { template: '<div/>' } },
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
router.push('/dataset/test');
|
|
70
|
+
await router.isReady();
|
|
71
|
+
return router;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('DatasetView', () => {
|
|
75
|
+
let pinia: ReturnType<typeof createPinia>;
|
|
76
|
+
let router: Awaited<ReturnType<typeof createTestRouter>>;
|
|
77
|
+
|
|
78
|
+
beforeEach(async () => {
|
|
79
|
+
pinia = createPinia();
|
|
80
|
+
setActivePinia(pinia);
|
|
81
|
+
router = await createTestRouter();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
function mountDataset(concepts: ConceptSummary[] = [], manifestOverrides: Partial<Manifest> = {}) {
|
|
85
|
+
const store = useVocabularyStore();
|
|
86
|
+
const manifest = makeManifest(manifestOverrides);
|
|
87
|
+
store.manifests.set('test', manifest);
|
|
88
|
+
store.datasets.set('test', makeAdapter(concepts));
|
|
89
|
+
return mount(DatasetView, {
|
|
90
|
+
global: { plugins: [pinia, router] },
|
|
91
|
+
props: { registerId: 'test' },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
it('renders dataset title and description', async () => {
|
|
96
|
+
const wrapper = mountDataset(makeConcepts(10));
|
|
97
|
+
await flushPromises();
|
|
98
|
+
expect(wrapper.text()).toContain('Test Dataset');
|
|
99
|
+
expect(wrapper.text()).toContain('A test dataset');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('shows concept count badge', async () => {
|
|
103
|
+
const wrapper = mountDataset(makeConcepts(100));
|
|
104
|
+
await flushPromises();
|
|
105
|
+
expect(wrapper.text()).toContain('100 concepts');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('shows language count badge', async () => {
|
|
109
|
+
const wrapper = mountDataset(makeConcepts(10));
|
|
110
|
+
await flushPromises();
|
|
111
|
+
expect(wrapper.text()).toContain('2 languages');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('shows owner badge', async () => {
|
|
115
|
+
const wrapper = mountDataset(makeConcepts(10));
|
|
116
|
+
await flushPromises();
|
|
117
|
+
expect(wrapper.text()).toContain('ISO');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('shows concept cards for loaded concepts', async () => {
|
|
121
|
+
const wrapper = mountDataset(makeConcepts(3));
|
|
122
|
+
await flushPromises();
|
|
123
|
+
expect(wrapper.text()).toContain('term 1');
|
|
124
|
+
expect(wrapper.text()).toContain('term 2');
|
|
125
|
+
expect(wrapper.text()).toContain('term 3');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('paginates at 50 concepts per page', async () => {
|
|
129
|
+
const wrapper = mountDataset(makeConcepts(120));
|
|
130
|
+
await flushPromises();
|
|
131
|
+
// Should show 50 on first page
|
|
132
|
+
expect(wrapper.text()).toContain('term 1');
|
|
133
|
+
expect(wrapper.text()).not.toContain('term 51');
|
|
134
|
+
// Should show pagination
|
|
135
|
+
expect(wrapper.text()).toContain('Prev');
|
|
136
|
+
expect(wrapper.text()).toContain('Next');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('shows correct page count in pagination', async () => {
|
|
140
|
+
const wrapper = mountDataset(makeConcepts(120));
|
|
141
|
+
await flushPromises();
|
|
142
|
+
// 120 / 50 = 3 pages (ceil)
|
|
143
|
+
const pageButtons = wrapper.findAll('button').filter(b => /^\d+$/.test(b.text().trim()));
|
|
144
|
+
const pageNumbers = pageButtons.map(b => parseInt(b.text().trim()));
|
|
145
|
+
expect(pageNumbers).toContain(1);
|
|
146
|
+
expect(pageNumbers).toContain(3);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('filters concepts by term', async () => {
|
|
150
|
+
const concepts: ConceptSummary[] = [
|
|
151
|
+
{ id: '1', designations: { eng: 'road network' }, eng: 'road network', status: 'valid' },
|
|
152
|
+
{ id: '2', designations: { eng: 'bridge design' }, eng: 'bridge design', status: 'valid' },
|
|
153
|
+
{ id: '3', designations: { eng: 'road user' }, eng: 'road user', status: 'valid' },
|
|
154
|
+
];
|
|
155
|
+
const wrapper = mountDataset(concepts);
|
|
156
|
+
await flushPromises();
|
|
157
|
+
const input = wrapper.find('input[aria-label="Filter concepts"]');
|
|
158
|
+
await input.setValue('road');
|
|
159
|
+
await flushPromises();
|
|
160
|
+
expect(wrapper.text()).toContain('road network');
|
|
161
|
+
expect(wrapper.text()).toContain('road user');
|
|
162
|
+
expect(wrapper.text()).not.toContain('bridge design');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('filters concepts by ID', async () => {
|
|
166
|
+
const concepts: ConceptSummary[] = [
|
|
167
|
+
{ id: '3.1.1.1', designations: { eng: 'term one' }, eng: 'term one', status: 'valid' },
|
|
168
|
+
{ id: '3.1.1.2', designations: { eng: 'term two' }, eng: 'term two', status: 'valid' },
|
|
169
|
+
];
|
|
170
|
+
const wrapper = mountDataset(concepts);
|
|
171
|
+
await flushPromises();
|
|
172
|
+
const input = wrapper.find('input[aria-label="Filter concepts"]');
|
|
173
|
+
await input.setValue('3.1.1.1');
|
|
174
|
+
await flushPromises();
|
|
175
|
+
expect(wrapper.text()).toContain('term one');
|
|
176
|
+
expect(wrapper.text()).not.toContain('term two');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('shows empty state when filter matches nothing', async () => {
|
|
180
|
+
const wrapper = mountDataset(makeConcepts(5));
|
|
181
|
+
await flushPromises();
|
|
182
|
+
const input = wrapper.find('input[aria-label="Filter concepts"]');
|
|
183
|
+
await input.setValue('zzzznonexistent');
|
|
184
|
+
await flushPromises();
|
|
185
|
+
expect(wrapper.text()).toContain('No concepts match your filter');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('shows clear filter button on empty state', async () => {
|
|
189
|
+
const wrapper = mountDataset(makeConcepts(5));
|
|
190
|
+
await flushPromises();
|
|
191
|
+
const input = wrapper.find('input[aria-label="Filter concepts"]');
|
|
192
|
+
await input.setValue('zzzznonexistent');
|
|
193
|
+
await flushPromises();
|
|
194
|
+
expect(wrapper.text()).toContain('Clear filter');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('shows links to stats and about pages', async () => {
|
|
198
|
+
const wrapper = mountDataset(makeConcepts(5));
|
|
199
|
+
await flushPromises();
|
|
200
|
+
expect(wrapper.text()).toContain('Statistics');
|
|
201
|
+
expect(wrapper.text()).toContain('About');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('shows bulk downloads when manifest has bulkFormats', async () => {
|
|
205
|
+
const wrapper = mountDataset(makeConcepts(5), {
|
|
206
|
+
bulkFormats: [
|
|
207
|
+
{ file: 'all.ttl', format: 'turtle', size: 1024 },
|
|
208
|
+
{ file: 'all.jsonld', format: 'jsonld', size: 2048 },
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
await flushPromises();
|
|
212
|
+
expect(wrapper.text()).toContain('Download');
|
|
213
|
+
expect(wrapper.text()).toContain('1.0 KB');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('shows 0 of N concepts in filter count', async () => {
|
|
217
|
+
const wrapper = mountDataset(makeConcepts(10));
|
|
218
|
+
await flushPromises();
|
|
219
|
+
const input = wrapper.find('input[aria-label="Filter concepts"]');
|
|
220
|
+
await input.setValue('term 5');
|
|
221
|
+
await flushPromises();
|
|
222
|
+
expect(wrapper.text()).toContain('of 10 concepts');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('disables Prev on first page', async () => {
|
|
226
|
+
const wrapper = mountDataset(makeConcepts(120));
|
|
227
|
+
await flushPromises();
|
|
228
|
+
const prevBtn = wrapper.findAll('button').find(b => b.text().includes('Prev'));
|
|
229
|
+
expect(prevBtn).toBeDefined();
|
|
230
|
+
expect(prevBtn!.attributes('disabled')).toBeDefined();
|
|
231
|
+
});
|
|
232
|
+
});
|