@glossarist/concept-browser 0.7.139 → 0.7.140
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/package.json +1 -1
- package/scripts/__tests__/standards-index.test.ts +121 -0
- package/scripts/generate-data.js +51 -3
- package/scripts/generate-data.ts +37 -3
- package/src/__tests__/app-footer.test.ts +36 -0
- package/src/__tests__/app-header.test.ts +32 -5
- package/src/__tests__/composables/use-same-standard-terms.test.ts +397 -0
- package/src/__tests__/concept-view.test.ts +103 -0
- package/src/__tests__/dataset-adapter.test.ts +150 -0
- package/src/__tests__/dataset-view.test.ts +41 -1
- package/src/__tests__/model-bridge-citation.test.ts +20 -0
- package/src/__tests__/same-standard-pane.test.ts +174 -0
- package/src/__tests__/scripts/generate-data.test.ts +74 -0
- package/src/__tests__/search-across-datasets.test.ts +102 -0
- package/src/__tests__/search-bar.test.ts +304 -0
- package/src/__tests__/search-results.test.ts +54 -0
- package/src/__tests__/search-utils.test.ts +64 -1
- package/src/__tests__/search-view.test.ts +194 -2
- package/src/__tests__/source-standard.test.ts +70 -0
- package/src/__tests__/ui-store.test.ts +26 -0
- package/src/__tests__/vocabulary-layered.test.ts +7 -7
- package/src/adapters/DatasetAdapter.ts +16 -8
- package/src/adapters/jsonld-types.ts +1 -0
- package/src/adapters/model-bridge.ts +1 -0
- package/src/adapters/standards-index.ts +7 -0
- package/src/adapters/types.ts +1 -0
- package/src/components/AppFooter.vue +7 -0
- package/src/components/AppHeader.vue +6 -5
- package/src/components/SameStandardPane.vue +70 -0
- package/src/components/SearchBar.vue +73 -18
- package/src/components/SearchResults.vue +46 -5
- package/src/components/site/SiteFooter.astro +6 -0
- package/src/composables/use-same-standard-terms.ts +106 -0
- package/src/config/types.ts +11 -0
- package/src/config/use-site-config.ts +1 -0
- package/src/i18n/locales/eng.yml +10 -1
- package/src/i18n/locales/fra.yml +10 -1
- package/src/stores/ui.ts +16 -1
- package/src/stores/vocabulary.ts +27 -31
- package/src/utils/search.ts +14 -1
- package/src/utils/source-standard.ts +34 -0
- package/src/views/ConceptView.vue +43 -12
- package/src/views/DatasetView.vue +25 -5
- package/src/views/SearchView.vue +9 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.140",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
describe('generate-data standards index', () => {
|
|
9
|
+
it('emits unique authoritative standards per register and preserves source stats', () => {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), 'generate-data-standards-'));
|
|
11
|
+
const conceptsDir = join(root, 'source', 'concepts');
|
|
12
|
+
const outputDir = join(root, 'public', 'data', 'fixture');
|
|
13
|
+
const configPath = join(root, 'site-config.yml');
|
|
14
|
+
const generator = resolve('scripts/generate-data.ts');
|
|
15
|
+
const tsx = resolve('node_modules/tsx/dist/loader.mjs');
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
mkdirSync(conceptsDir, { recursive: true });
|
|
19
|
+
writeFileSync(configPath, JSON.stringify({
|
|
20
|
+
id: 'fixture', title: 'Fixture', uri_base: 'https://example.test',
|
|
21
|
+
datasets: [{ id: 'fixture', local_path: 'source', languages: ['eng', 'fra'] }],
|
|
22
|
+
}));
|
|
23
|
+
const writeConcept = (id: string, sources: unknown[], engSources: unknown[] = []) => {
|
|
24
|
+
writeFileSync(join(conceptsDir, `${id}.yaml`), JSON.stringify({
|
|
25
|
+
termid: id,
|
|
26
|
+
_sources: sources,
|
|
27
|
+
eng: {
|
|
28
|
+
terms: [{ designation: `Fixture term ${id}` }],
|
|
29
|
+
sources: engSources,
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
};
|
|
33
|
+
writeConcept('1', [
|
|
34
|
+
{ type: 'authoritative', origin: { ref: { source: 'ISO', id: '19165-2:2020', clause: '3.1' } } },
|
|
35
|
+
{ type: 'authoritative', origin: { ref: { source: 'ISO', id: '19165-2:2020', clause: '4.2' } } },
|
|
36
|
+
{ type: 'authoritative', origin: { ref: { source: 'IEC', id: '17000:2020' } } },
|
|
37
|
+
{ type: 'lineage', origin: { ref: { source: 'ISO', id: '704:2022' } } },
|
|
38
|
+
], [
|
|
39
|
+
{ type: 'authoritative', origin: { ref: { source: 'iso', id: '19165-2:2020', clause: '5.1' } } },
|
|
40
|
+
]);
|
|
41
|
+
writeConcept('2', [
|
|
42
|
+
{ type: 'authoritative', origin: { ref: { source: ' ISO ', id: ' 19165-2:2020 ', clause: '6.3' } } },
|
|
43
|
+
]);
|
|
44
|
+
writeConcept('3', [
|
|
45
|
+
{ type: 'lineage', origin: { ref: { source: 'ISO', id: '999:2020' } } },
|
|
46
|
+
], [
|
|
47
|
+
{ type: 'authoritative', origin: { ref: { source: 'ISO', id: '1087:2019' } } },
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
execFileSync(process.execPath, ['--import', tsx, generator], {
|
|
51
|
+
cwd: root,
|
|
52
|
+
env: { ...process.env, SITE_CONFIG: configPath },
|
|
53
|
+
stdio: 'pipe',
|
|
54
|
+
timeout: 30_000,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const standards = JSON.parse(readFileSync(join(outputDir, 'standards.json'), 'utf8'));
|
|
58
|
+
expect(standards).toEqual({
|
|
59
|
+
'iso 19165-2:2020': { label: 'ISO 19165-2:2020', conceptIds: ['1', '2'] },
|
|
60
|
+
'iec 17000:2020': { label: 'IEC 17000:2020', conceptIds: ['1'] },
|
|
61
|
+
'iso 1087:2019': { label: 'ISO 1087:2019', conceptIds: ['3'] },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const stats = JSON.parse(readFileSync(join(outputDir, 'stats.json'), 'utf8'));
|
|
65
|
+
expect(stats.sourceCount).toBe(4);
|
|
66
|
+
expect(stats.sources).toEqual(expect.arrayContaining([
|
|
67
|
+
expect.objectContaining({ ref: 'ISO', conceptCount: 2, conceptIds: ['1', '3'] }),
|
|
68
|
+
expect.objectContaining({ ref: 'IEC', conceptCount: 1, conceptIds: ['1'] }),
|
|
69
|
+
]));
|
|
70
|
+
} finally {
|
|
71
|
+
rmSync(root, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}, 60_000);
|
|
74
|
+
|
|
75
|
+
it('includes authoritative standards from languages absent from the dataset config', () => {
|
|
76
|
+
const root = mkdtempSync(join(tmpdir(), 'generate-data-unconfigured-language-'));
|
|
77
|
+
const conceptsDir = join(root, 'source', 'concepts');
|
|
78
|
+
const configPath = join(root, 'site-config.yml');
|
|
79
|
+
try {
|
|
80
|
+
mkdirSync(conceptsDir, { recursive: true });
|
|
81
|
+
writeFileSync(configPath, JSON.stringify({
|
|
82
|
+
id: 'fixture', title: 'Fixture', uri_base: 'https://example.test',
|
|
83
|
+
datasets: [{ id: 'fixture', local_path: 'source', languages: ['eng'] }],
|
|
84
|
+
}));
|
|
85
|
+
writeFileSync(join(conceptsDir, '1.yaml'), JSON.stringify({
|
|
86
|
+
termid: '1',
|
|
87
|
+
eng: {
|
|
88
|
+
terms: [{ designation: 'Term' }],
|
|
89
|
+
sources: [{ type: 'authoritative', origin: { ref: { source: 'ISO' } } }],
|
|
90
|
+
},
|
|
91
|
+
deu: {
|
|
92
|
+
terms: [{ designation: 'Begriff' }],
|
|
93
|
+
sources: [
|
|
94
|
+
{ type: 'authoritative', origin: { ref: { source: 'DIN', id: '1' } } },
|
|
95
|
+
{ type: 'lineage', origin: { ref: { source: 'DIN', id: '2' } } },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
execFileSync(process.execPath, [
|
|
100
|
+
'--import', resolve('node_modules/tsx/dist/loader.mjs'), resolve('scripts/generate-data.ts'),
|
|
101
|
+
], {
|
|
102
|
+
cwd: root,
|
|
103
|
+
env: { ...process.env, SITE_CONFIG: configPath },
|
|
104
|
+
stdio: 'pipe',
|
|
105
|
+
timeout: 30_000,
|
|
106
|
+
});
|
|
107
|
+
const outputDir = join(root, 'public', 'data', 'fixture');
|
|
108
|
+
expect(JSON.parse(readFileSync(join(outputDir, 'standards.json'), 'utf8'))).toEqual({
|
|
109
|
+
iso: { label: 'ISO', conceptIds: ['1'] },
|
|
110
|
+
'din 1': { label: 'DIN 1', conceptIds: ['1'] },
|
|
111
|
+
});
|
|
112
|
+
const stats = JSON.parse(readFileSync(join(outputDir, 'stats.json'), 'utf8'));
|
|
113
|
+
expect(stats.sourceCount).toBe(1);
|
|
114
|
+
expect(stats.sources).toEqual([
|
|
115
|
+
{ ref: 'ISO', types: ['authoritative'], conceptCount: 1, conceptIds: ['1'] },
|
|
116
|
+
]);
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(root, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
}, 60_000);
|
|
121
|
+
});
|
package/scripts/generate-data.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// scripts/generate-data.ts
|
|
2
2
|
import fs2 from "fs";
|
|
3
3
|
import path3 from "path";
|
|
4
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
4
5
|
import yaml4 from "js-yaml";
|
|
5
6
|
import {
|
|
6
7
|
naturalSort,
|
|
@@ -89,6 +90,28 @@ function loadSiteConfig(args = []) {
|
|
|
89
90
|
return { config: config2, configPath };
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
// src/utils/source-standard.ts
|
|
94
|
+
function standardLabel(ref) {
|
|
95
|
+
const parts = [ref?.source, ref?.id].map((part) => part?.trim().replace(/\s+/g, " ") ?? "").filter(Boolean);
|
|
96
|
+
return parts.join(" ") || null;
|
|
97
|
+
}
|
|
98
|
+
function standardKey(ref) {
|
|
99
|
+
return standardLabel(ref)?.toLowerCase() ?? null;
|
|
100
|
+
}
|
|
101
|
+
function collectStandardKeys(concept) {
|
|
102
|
+
const sources = [
|
|
103
|
+
...concept.sources,
|
|
104
|
+
...concept.languages.flatMap((lang) => concept.localization(lang)?.sources ?? [])
|
|
105
|
+
];
|
|
106
|
+
const keys = /* @__PURE__ */ new Set();
|
|
107
|
+
for (const source of sources) {
|
|
108
|
+
if (source.type !== "authoritative") continue;
|
|
109
|
+
const key = standardKey(source.origin?.ref);
|
|
110
|
+
if (key !== null) keys.add(key);
|
|
111
|
+
}
|
|
112
|
+
return [...keys];
|
|
113
|
+
}
|
|
114
|
+
|
|
92
115
|
// scripts/lib/concept-groups.ts
|
|
93
116
|
function getGroups(conceptYaml) {
|
|
94
117
|
if (conceptYaml.eng && conceptYaml.eng.groups) return conceptYaml.eng.groups;
|
|
@@ -1275,7 +1298,26 @@ async function processDataset(dir, register, opts) {
|
|
|
1275
1298
|
withPlurality: 0
|
|
1276
1299
|
}
|
|
1277
1300
|
};
|
|
1301
|
+
const standards = /* @__PURE__ */ new Map();
|
|
1278
1302
|
const STATS_PROCESSORS = [
|
|
1303
|
+
function collectStandards(cy, termid) {
|
|
1304
|
+
const concept = {
|
|
1305
|
+
sources: cy._sources || [],
|
|
1306
|
+
languages: Object.keys(cy).filter((lang) => !lang.startsWith("_") && cy[lang] && typeof cy[lang] === "object" && !Array.isArray(cy[lang])),
|
|
1307
|
+
localization: (lang) => cy[lang]
|
|
1308
|
+
};
|
|
1309
|
+
const sources = [
|
|
1310
|
+
...concept.sources,
|
|
1311
|
+
...concept.languages.flatMap((lang) => concept.localization(lang).sources || [])
|
|
1312
|
+
];
|
|
1313
|
+
for (const key of collectStandardKeys(concept)) {
|
|
1314
|
+
if (!standards.has(key)) {
|
|
1315
|
+
const source = sources.find((source2) => source2.type === "authoritative" && standardKey(source2.origin?.ref) === key);
|
|
1316
|
+
standards.set(key, { label: standardLabel(source.origin.ref), conceptIds: [] });
|
|
1317
|
+
}
|
|
1318
|
+
standards.get(key).conceptIds.push(termid);
|
|
1319
|
+
}
|
|
1320
|
+
},
|
|
1279
1321
|
function collectSources(cy, termid, s) {
|
|
1280
1322
|
const refs = /* @__PURE__ */ new Set();
|
|
1281
1323
|
const allSources = [
|
|
@@ -1393,12 +1435,17 @@ async function processDataset(dir, register, opts) {
|
|
|
1393
1435
|
const chunk = concepts.slice(i, i + CHUNK_SIZE);
|
|
1394
1436
|
const chunkIndex = Math.floor(i / CHUNK_SIZE);
|
|
1395
1437
|
const chunkFile = `index-${String(chunkIndex).padStart(4, "0")}.json`;
|
|
1396
|
-
|
|
1438
|
+
const chunkData = {
|
|
1397
1439
|
registerId: register,
|
|
1398
1440
|
chunkIndex,
|
|
1399
1441
|
concepts: chunk
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1442
|
+
};
|
|
1443
|
+
const chunkContent = JSON.stringify(chunkData);
|
|
1444
|
+
const chunkPath = path3.join(DATA, register, "chunks", chunkFile);
|
|
1445
|
+
fs2.mkdirSync(path3.dirname(chunkPath), { recursive: true });
|
|
1446
|
+
fs2.writeFileSync(chunkPath, chunkContent);
|
|
1447
|
+
const hash = createHash2("sha256").update(chunkContent).digest("hex").slice(0, 12);
|
|
1448
|
+
chunks.push({ file: chunkFile, count: chunk.length, hash });
|
|
1402
1449
|
}
|
|
1403
1450
|
const summary = concepts.map((c) => ({
|
|
1404
1451
|
id: c.id,
|
|
@@ -1490,6 +1537,7 @@ async function processDataset(dir, register, opts) {
|
|
|
1490
1537
|
conceptIds: s.conceptIds
|
|
1491
1538
|
})).sort((a, b) => b.conceptCount - a.conceptCount);
|
|
1492
1539
|
const totalRelationships = Object.values(stats.relTypeCounts).reduce((a, b) => a + b, 0);
|
|
1540
|
+
writeJson(path3.join(DATA, register, "standards.json"), Object.fromEntries(standards));
|
|
1493
1541
|
writeJson(path3.join(DATA, register, "stats.json"), {
|
|
1494
1542
|
sourceCount: sourceStats.length,
|
|
1495
1543
|
sources: sourceStats,
|
package/scripts/generate-data.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import yaml from 'js-yaml';
|
|
4
5
|
import {
|
|
5
6
|
naturalSort,
|
|
@@ -15,6 +16,8 @@ import {
|
|
|
15
16
|
type Concept,
|
|
16
17
|
} from 'glossarist';
|
|
17
18
|
import { loadSiteConfig } from './load-site-config';
|
|
19
|
+
import { collectStandardKeys, standardKey, standardLabel } from '../src/utils/source-standard';
|
|
20
|
+
import type { StandardsIndex } from '../src/adapters/standards-index';
|
|
18
21
|
import { getGroups } from './lib/concept-groups';
|
|
19
22
|
import { consumeDatasetEntities } from './lib/build/non-verbal-consumer';
|
|
20
23
|
import { copyImageAssets } from './lib/build/image-assets';
|
|
@@ -740,7 +743,31 @@ async function processDataset(dir, register, opts) {
|
|
|
740
743
|
},
|
|
741
744
|
};
|
|
742
745
|
|
|
746
|
+
const standards = new Map<string, StandardsIndex[string]>();
|
|
747
|
+
|
|
743
748
|
const STATS_PROCESSORS = [
|
|
749
|
+
function collectStandards(cy, termid) {
|
|
750
|
+
// Harmonized metadata uses underscore-prefixed fields; object-valued
|
|
751
|
+
// language entries include every localization, even if not configured.
|
|
752
|
+
const concept = {
|
|
753
|
+
sources: cy._sources || [],
|
|
754
|
+
languages: Object.keys(cy).filter(lang =>
|
|
755
|
+
!lang.startsWith('_') && cy[lang] && typeof cy[lang] === 'object' && !Array.isArray(cy[lang])),
|
|
756
|
+
localization: (lang: string) => cy[lang],
|
|
757
|
+
};
|
|
758
|
+
const sources = [
|
|
759
|
+
...concept.sources,
|
|
760
|
+
...concept.languages.flatMap(lang => concept.localization(lang).sources || []),
|
|
761
|
+
];
|
|
762
|
+
for (const key of collectStandardKeys(concept)) {
|
|
763
|
+
if (!standards.has(key)) {
|
|
764
|
+
const source = sources.find(source =>
|
|
765
|
+
source.type === 'authoritative' && standardKey(source.origin?.ref) === key);
|
|
766
|
+
standards.set(key, { label: standardLabel(source.origin.ref)!, conceptIds: [] });
|
|
767
|
+
}
|
|
768
|
+
standards.get(key)!.conceptIds.push(termid);
|
|
769
|
+
}
|
|
770
|
+
},
|
|
744
771
|
function collectSources(cy, termid, s) {
|
|
745
772
|
const refs = new Set();
|
|
746
773
|
const allSources = [
|
|
@@ -879,12 +906,17 @@ async function processDataset(dir, register, opts) {
|
|
|
879
906
|
const chunk = concepts.slice(i, i + CHUNK_SIZE);
|
|
880
907
|
const chunkIndex = Math.floor(i / CHUNK_SIZE);
|
|
881
908
|
const chunkFile = `index-${String(chunkIndex).padStart(4, '0')}.json`;
|
|
882
|
-
|
|
909
|
+
const chunkData = {
|
|
883
910
|
registerId: register,
|
|
884
911
|
chunkIndex,
|
|
885
912
|
concepts: chunk,
|
|
886
|
-
}
|
|
887
|
-
|
|
913
|
+
};
|
|
914
|
+
const chunkContent = JSON.stringify(chunkData);
|
|
915
|
+
const chunkPath = path.join(DATA, register, 'chunks', chunkFile);
|
|
916
|
+
fs.mkdirSync(path.dirname(chunkPath), { recursive: true });
|
|
917
|
+
fs.writeFileSync(chunkPath, chunkContent);
|
|
918
|
+
const hash = createHash('sha256').update(chunkContent).digest('hex').slice(0, 12);
|
|
919
|
+
chunks.push({ file: chunkFile, count: chunk.length, hash });
|
|
888
920
|
}
|
|
889
921
|
|
|
890
922
|
const summary = concepts.map(c => ({
|
|
@@ -993,6 +1025,8 @@ async function processDataset(dir, register, opts) {
|
|
|
993
1025
|
.sort((a, b) => b.conceptCount - a.conceptCount);
|
|
994
1026
|
const totalRelationships = Object.values(stats.relTypeCounts).reduce((a: number, b: number) => a + b, 0);
|
|
995
1027
|
|
|
1028
|
+
writeJson(path.join(DATA, register, 'standards.json'), Object.fromEntries(standards));
|
|
1029
|
+
|
|
996
1030
|
writeJson(path.join(DATA, register, 'stats.json'), {
|
|
997
1031
|
sourceCount: sourceStats.length,
|
|
998
1032
|
sources: sourceStats,
|
|
@@ -35,4 +35,40 @@ describe('AppFooter', () => {
|
|
|
35
35
|
expect(link).toBeDefined();
|
|
36
36
|
expect(link!.attributes('href')).toBe('https://glossarist.org');
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
it('omits hosted by when branding.hostedBy is absent', () => {
|
|
40
|
+
const wrapper = mountFooter();
|
|
41
|
+
expect(wrapper.text()).not.toContain('Hosted by');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders hosted by with logo when branding.hostedBy is configured', async () => {
|
|
45
|
+
const inline = document.createElement('script');
|
|
46
|
+
inline.id = 'site-config-json';
|
|
47
|
+
inline.type = 'application/json';
|
|
48
|
+
inline.textContent = JSON.stringify({
|
|
49
|
+
title: 'Test Site',
|
|
50
|
+
branding: {
|
|
51
|
+
hostedBy: {
|
|
52
|
+
name: 'Enosema Foundation',
|
|
53
|
+
url: 'https://www.enosema.org',
|
|
54
|
+
logo: 'https://www.enosema.org/logos/logo-text-white.svg',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
document.head.appendChild(inline);
|
|
59
|
+
|
|
60
|
+
const { useSiteConfig } = await import('../config/use-site-config');
|
|
61
|
+
await useSiteConfig().loadConfig();
|
|
62
|
+
|
|
63
|
+
const wrapper = mountFooter();
|
|
64
|
+
expect(wrapper.text()).toContain('Hosted by');
|
|
65
|
+
const link = wrapper.findAll('a').find(a => a.attributes('href') === 'https://www.enosema.org');
|
|
66
|
+
expect(link).toBeDefined();
|
|
67
|
+
expect(link!.attributes('href')).toBe('https://www.enosema.org');
|
|
68
|
+
const img = link!.find('img');
|
|
69
|
+
expect(img.attributes('src')).toBe('https://www.enosema.org/logos/logo-text-white.svg');
|
|
70
|
+
expect(img.attributes('alt')).toBe('Enosema Foundation');
|
|
71
|
+
inline.remove();
|
|
72
|
+
});
|
|
73
|
+
|
|
38
74
|
});
|
|
@@ -3,6 +3,8 @@ import { mount, flushPromises } from '@vue/test-utils';
|
|
|
3
3
|
import { createPinia, setActivePinia } from 'pinia';
|
|
4
4
|
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
5
5
|
import AppHeader from '../components/AppHeader.vue';
|
|
6
|
+
import { useUiStore } from '../stores/ui';
|
|
7
|
+
import { locale } from '../i18n';
|
|
6
8
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
7
9
|
import type { Manifest } from '../adapters/types';
|
|
8
10
|
|
|
@@ -45,6 +47,7 @@ describe('AppHeader', () => {
|
|
|
45
47
|
let router: Awaited<ReturnType<typeof createTestRouter>>;
|
|
46
48
|
|
|
47
49
|
beforeEach(async () => {
|
|
50
|
+
locale.value = 'eng';
|
|
48
51
|
pinia = createPinia();
|
|
49
52
|
setActivePinia(pinia);
|
|
50
53
|
router = await createTestRouter();
|
|
@@ -66,10 +69,28 @@ describe('AppHeader', () => {
|
|
|
66
69
|
expect(wrapper.text()).toContain('Glossarist');
|
|
67
70
|
});
|
|
68
71
|
|
|
69
|
-
it('renders search
|
|
72
|
+
it('renders quick search on the home route', () => {
|
|
70
73
|
const wrapper = mountHeader();
|
|
71
|
-
const input = wrapper.find('input[aria-label="
|
|
74
|
+
const input = wrapper.find('input[aria-label="Quick search"]');
|
|
72
75
|
expect(input.exists()).toBe(true);
|
|
76
|
+
expect(input.attributes('placeholder')).toBe('Quick search...');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('hides quick search on the search route and restores it elsewhere', async () => {
|
|
80
|
+
await router.push('/search?q=road');
|
|
81
|
+
const wrapper = mountHeader();
|
|
82
|
+
expect(wrapper.find('form').exists()).toBe(false);
|
|
83
|
+
|
|
84
|
+
await router.push('/');
|
|
85
|
+
expect(wrapper.find('form').exists()).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('labels quick search in French', () => {
|
|
89
|
+
locale.value = 'fra';
|
|
90
|
+
const wrapper = mountHeader();
|
|
91
|
+
const input = wrapper.get('input');
|
|
92
|
+
expect(input.attributes('aria-label')).toBe('Recherche rapide');
|
|
93
|
+
expect(input.attributes('placeholder')).toBe('Recherche rapide…');
|
|
73
94
|
});
|
|
74
95
|
|
|
75
96
|
it('renders mobile hamburger button', () => {
|
|
@@ -89,13 +110,19 @@ describe('AppHeader', () => {
|
|
|
89
110
|
|
|
90
111
|
it('navigates to search on form submit with query', async () => {
|
|
91
112
|
const wrapper = mountHeader();
|
|
92
|
-
const input = wrapper.find('input[aria-label="
|
|
93
|
-
await input.setValue('road');
|
|
113
|
+
const input = wrapper.find('input[aria-label="Quick search"]');
|
|
114
|
+
await input.setValue(' road & rail ');
|
|
94
115
|
const form = wrapper.find('form');
|
|
95
116
|
await form.trigger('submit');
|
|
96
117
|
await flushPromises();
|
|
97
118
|
expect(router.currentRoute.value.name).toBe('search');
|
|
98
|
-
expect(router.currentRoute.value.query
|
|
119
|
+
expect(router.currentRoute.value.query).toEqual({ q: 'road & rail' });
|
|
120
|
+
expect(router.currentRoute.value.path).toBe('/search');
|
|
121
|
+
expect(useUiStore().searchQuery).toBe('');
|
|
122
|
+
expect(wrapper.find('form').exists()).toBe(false);
|
|
123
|
+
|
|
124
|
+
await router.push('/');
|
|
125
|
+
expect(wrapper.get('input').element.value).toBe('');
|
|
99
126
|
});
|
|
100
127
|
|
|
101
128
|
it('does not navigate on empty search', async () => {
|