@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,200 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import ConceptTimeline from '../components/ConceptTimeline.vue';
|
|
4
|
+
import { conceptFromJson } from '../adapters/model-bridge';
|
|
5
|
+
|
|
6
|
+
function makeConceptJson(overrides: Record<string, any> = {}) {
|
|
7
|
+
return {
|
|
8
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
9
|
+
'@type': 'gl:Concept',
|
|
10
|
+
'gl:identifier': '1',
|
|
11
|
+
'gl:localizedConcept': {
|
|
12
|
+
eng: {
|
|
13
|
+
'@type': 'gl:LocalizedConcept',
|
|
14
|
+
'gl:languageCode': 'eng',
|
|
15
|
+
'gl:entryStatus': 'valid',
|
|
16
|
+
...overrides.eng,
|
|
17
|
+
},
|
|
18
|
+
...(overrides.otherLangs || {}),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function mountTimeline(conceptJson: Record<string, any>, activeLang = 'eng', languageOrder?: string[]) {
|
|
24
|
+
const concept = conceptFromJson(conceptJson);
|
|
25
|
+
return mount(ConceptTimeline, {
|
|
26
|
+
props: {
|
|
27
|
+
concept,
|
|
28
|
+
activeLang,
|
|
29
|
+
languageOrder,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('ConceptTimeline', () => {
|
|
35
|
+
it('shows review metadata even without history entries', () => {
|
|
36
|
+
const wrapper = mountTimeline(makeConceptJson());
|
|
37
|
+
expect(wrapper.text()).toContain('Review Details');
|
|
38
|
+
expect(wrapper.text()).toContain('valid');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders review date as timeline entry', () => {
|
|
42
|
+
const json = makeConceptJson({
|
|
43
|
+
eng: { 'gl:reviewDate': '2023-05-15' },
|
|
44
|
+
});
|
|
45
|
+
const wrapper = mountTimeline(json);
|
|
46
|
+
expect(wrapper.text()).toContain('Review initiated');
|
|
47
|
+
expect(wrapper.text()).toContain('2023');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('renders review decision event', () => {
|
|
51
|
+
const json = makeConceptJson({
|
|
52
|
+
eng: {
|
|
53
|
+
'gl:reviewDecisionEvent': 'Accepted',
|
|
54
|
+
'gl:reviewDate': '2023-05-15',
|
|
55
|
+
'gl:reviewDecisionDate': '2023-06-01',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const wrapper = mountTimeline(json);
|
|
59
|
+
expect(wrapper.text()).toContain('Accepted');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('renders gl:dates entries', () => {
|
|
63
|
+
const json = makeConceptJson({
|
|
64
|
+
eng: {
|
|
65
|
+
'gl:dates': [
|
|
66
|
+
{ 'gl:dateType': 'accepted', 'gl:date': '2020-01-01' },
|
|
67
|
+
{ 'gl:dateType': 'amended', 'gl:date': '2022-06-15' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const wrapper = mountTimeline(json);
|
|
72
|
+
expect(wrapper.text()).toContain('Concept accepted');
|
|
73
|
+
expect(wrapper.text()).toContain('Definition amended');
|
|
74
|
+
expect(wrapper.text()).toContain('2020');
|
|
75
|
+
expect(wrapper.text()).toContain('2022');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('sorts entries by date ascending', () => {
|
|
79
|
+
const json = makeConceptJson({
|
|
80
|
+
eng: {
|
|
81
|
+
'gl:dates': [
|
|
82
|
+
{ 'gl:dateType': 'amended', 'gl:date': '2022-06-15' },
|
|
83
|
+
{ 'gl:dateType': 'accepted', 'gl:date': '2020-01-01' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
const wrapper = mountTimeline(json);
|
|
88
|
+
const texts = wrapper.text();
|
|
89
|
+
const acceptedIdx = texts.indexOf('Concept accepted');
|
|
90
|
+
const amendedIdx = texts.indexOf('Definition amended');
|
|
91
|
+
expect(acceptedIdx).toBeLessThan(amendedIdx);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('shows language selector when multiple languages have history', () => {
|
|
95
|
+
const json = makeConceptJson({
|
|
96
|
+
eng: { 'gl:reviewDate': '2023-01-01' },
|
|
97
|
+
otherLangs: {
|
|
98
|
+
fra: {
|
|
99
|
+
'@type': 'gl:LocalizedConcept',
|
|
100
|
+
'gl:languageCode': 'fra',
|
|
101
|
+
'gl:reviewDate': '2023-02-01',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const wrapper = mountTimeline(json, 'eng', ['eng', 'fra']);
|
|
106
|
+
expect(wrapper.text()).toContain('French');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('does not show language selector for single-language history', () => {
|
|
110
|
+
const json = makeConceptJson({
|
|
111
|
+
eng: { 'gl:reviewDate': '2023-01-01' },
|
|
112
|
+
});
|
|
113
|
+
const wrapper = mountTimeline(json);
|
|
114
|
+
const buttons = wrapper.findAll('button');
|
|
115
|
+
const langButtons = buttons.filter(b => b.text().includes('French'));
|
|
116
|
+
expect(langButtons.length).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('emits update:activeLang on language button click', async () => {
|
|
120
|
+
const json = makeConceptJson({
|
|
121
|
+
eng: { 'gl:reviewDate': '2023-01-01' },
|
|
122
|
+
otherLangs: {
|
|
123
|
+
fra: {
|
|
124
|
+
'@type': 'gl:LocalizedConcept',
|
|
125
|
+
'gl:languageCode': 'fra',
|
|
126
|
+
'gl:reviewDate': '2023-02-01',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
const wrapper = mountTimeline(json, 'eng', ['eng', 'fra']);
|
|
131
|
+
const fraBtn = wrapper.findAll('button').find(b => b.text().includes('French'));
|
|
132
|
+
expect(fraBtn).toBeDefined();
|
|
133
|
+
await fraBtn!.trigger('click');
|
|
134
|
+
expect(wrapper.emitted('update:activeLang')?.[0]).toEqual(['fra']);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('shows review metadata when present', () => {
|
|
138
|
+
const json = makeConceptJson({
|
|
139
|
+
eng: {
|
|
140
|
+
'gl:reviewDate': '2023-01-01',
|
|
141
|
+
'gl:reviewStatus': 'final',
|
|
142
|
+
'gl:reviewDecision': 'accepted',
|
|
143
|
+
'gl:entryStatus': 'valid',
|
|
144
|
+
'gl:release': '3',
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
const wrapper = mountTimeline(json);
|
|
148
|
+
expect(wrapper.text()).toContain('Review Details');
|
|
149
|
+
expect(wrapper.text()).toContain('final');
|
|
150
|
+
expect(wrapper.text()).toContain('accepted');
|
|
151
|
+
expect(wrapper.text()).toContain('valid');
|
|
152
|
+
expect(wrapper.text()).toContain('3');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('groups by year when more than 3 entries', () => {
|
|
156
|
+
const json = makeConceptJson({
|
|
157
|
+
eng: {
|
|
158
|
+
'gl:dates': [
|
|
159
|
+
{ 'gl:dateType': 'accepted', 'gl:date': '2019-03-01' },
|
|
160
|
+
{ 'gl:dateType': 'amended', 'gl:date': '2020-06-15' },
|
|
161
|
+
{ 'gl:dateType': 'amended', 'gl:date': '2021-09-20' },
|
|
162
|
+
{ 'gl:dateType': 'published', 'gl:date': '2023-01-10' },
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
const wrapper = mountTimeline(json);
|
|
167
|
+
expect(wrapper.text()).toContain('2019');
|
|
168
|
+
expect(wrapper.text()).toContain('2020');
|
|
169
|
+
expect(wrapper.text()).toContain('2021');
|
|
170
|
+
expect(wrapper.text()).toContain('2023');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('uses simple layout for 3 or fewer entries', () => {
|
|
174
|
+
const json = makeConceptJson({
|
|
175
|
+
eng: {
|
|
176
|
+
'gl:dates': [
|
|
177
|
+
{ 'gl:dateType': 'accepted', 'gl:date': '2020-01-01' },
|
|
178
|
+
{ 'gl:dateType': 'amended', 'gl:date': '2022-06-15' },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const wrapper = mountTimeline(json);
|
|
183
|
+
expect(wrapper.text()).toContain('Concept accepted');
|
|
184
|
+
expect(wrapper.text()).toContain('Definition amended');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('deduplicates review date if it matches a gl:date entry', () => {
|
|
188
|
+
const json = makeConceptJson({
|
|
189
|
+
eng: {
|
|
190
|
+
'gl:dates': [
|
|
191
|
+
{ 'gl:dateType': 'review', 'gl:date': '2023-05-15' },
|
|
192
|
+
],
|
|
193
|
+
'gl:reviewDate': '2023-05-15',
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
const wrapper = mountTimeline(json);
|
|
197
|
+
const reviewCount = wrapper.text().split('Review initiated').length - 1;
|
|
198
|
+
expect(reviewCount).toBeLessThanOrEqual(1);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { mount, flushPromises } from '@vue/test-utils';
|
|
3
|
+
import ConceptView from '../views/ConceptView.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
|
+
describe('ConceptView', () => {
|
|
9
|
+
let pinia: ReturnType<typeof setupPinia>;
|
|
10
|
+
let router: Awaited<ReturnType<typeof createTestRouter>>;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
pinia = setupPinia();
|
|
14
|
+
router = await createTestRouter('dataset', '/');
|
|
15
|
+
const store = useVocabularyStore();
|
|
16
|
+
store.manifests.set('test', makeManifest());
|
|
17
|
+
store.datasets.set('test', makeAdapterStub());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function mountConceptView(registerId = 'test', conceptId = '1') {
|
|
21
|
+
return mount(ConceptView, {
|
|
22
|
+
global: {
|
|
23
|
+
plugins: [pinia, router],
|
|
24
|
+
stubs: { ConceptDetail: true },
|
|
25
|
+
},
|
|
26
|
+
props: { registerId, conceptId },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
it('shows loading skeleton initially', () => {
|
|
31
|
+
const wrapper = mountConceptView();
|
|
32
|
+
expect(wrapper.find('.skeleton').exists()).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('shows error when fetchConcept returns null', async () => {
|
|
36
|
+
const wrapper = mountConceptView();
|
|
37
|
+
await flushPromises();
|
|
38
|
+
expect(wrapper.text()).toContain('Failed to load concept');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('shows retry button on error', async () => {
|
|
42
|
+
const wrapper = mountConceptView();
|
|
43
|
+
await flushPromises();
|
|
44
|
+
const retryBtn = wrapper.findAll('button').find(b => b.text() === 'Retry');
|
|
45
|
+
expect(retryBtn).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('shows back to dataset link on error', async () => {
|
|
49
|
+
const wrapper = mountConceptView();
|
|
50
|
+
await flushPromises();
|
|
51
|
+
const link = wrapper.findAll('a').find(a => a.text().includes('Back to dataset'));
|
|
52
|
+
expect(link).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('renders ConceptDetail when concept loads', async () => {
|
|
56
|
+
const concept = conceptFromJson({
|
|
57
|
+
'@id': 'https://glossarist.org/test/concept/1',
|
|
58
|
+
'@type': 'gl:Concept',
|
|
59
|
+
'gl:identifier': '1',
|
|
60
|
+
'gl:localizedConcept': {
|
|
61
|
+
eng: {
|
|
62
|
+
'@type': 'gl:LocalizedConcept',
|
|
63
|
+
'gl:languageCode': 'eng',
|
|
64
|
+
'gl:entryStatus': 'valid',
|
|
65
|
+
'gl:designation': [
|
|
66
|
+
{ '@type': 'gl:Expression', 'gl:term': 'test', 'gl:normativeStatus': 'preferred' },
|
|
67
|
+
],
|
|
68
|
+
'gl:definition': [{ '@type': 'gl:DetailedDefinition', 'gl:content': 'A test concept.' }],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
const store = useVocabularyStore();
|
|
73
|
+
store.datasets.set('test', makeAdapterStub({ fetchConcept: () => Promise.resolve(concept) }));
|
|
74
|
+
|
|
75
|
+
const wrapper = mountConceptView();
|
|
76
|
+
await flushPromises();
|
|
77
|
+
expect(wrapper.findComponent({ name: 'ConceptDetail' }).exists()).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('shows error when dataset not found', async () => {
|
|
81
|
+
const store = useVocabularyStore();
|
|
82
|
+
store.datasets.delete('test');
|
|
83
|
+
store.manifests.delete('test');
|
|
84
|
+
const wrapper = mountConceptView();
|
|
85
|
+
await flushPromises();
|
|
86
|
+
expect(wrapper.text()).toContain('Failed to load concept');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
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 ContributorsView from '../views/ContributorsView.vue';
|
|
6
|
+
import { useSiteConfig } from '../config/use-site-config';
|
|
7
|
+
|
|
8
|
+
// Mock useSiteConfig so we can inject contributor data
|
|
9
|
+
vi.mock('../config/use-site-config', () => {
|
|
10
|
+
let _config: any = {};
|
|
11
|
+
return {
|
|
12
|
+
useSiteConfig: () => ({ config: { value: _config } }),
|
|
13
|
+
__setConfig: (c: any) => { _config = c; },
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
async function createTestRouter() {
|
|
18
|
+
const router = createRouter({
|
|
19
|
+
history: createMemoryHistory(),
|
|
20
|
+
routes: [
|
|
21
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
22
|
+
{ path: '/contributors', name: 'contributors', component: { template: '<div/>' } },
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
router.push('/contributors');
|
|
26
|
+
await router.isReady();
|
|
27
|
+
return router;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('ContributorsView', () => {
|
|
31
|
+
let pinia: ReturnType<typeof createPinia>;
|
|
32
|
+
let router: Awaited<ReturnType<typeof createTestRouter>>;
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
pinia = createPinia();
|
|
36
|
+
setActivePinia(pinia);
|
|
37
|
+
router = await createTestRouter();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function mountContributors() {
|
|
41
|
+
return mount(ContributorsView, {
|
|
42
|
+
global: { plugins: [pinia, router] },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
it('renders breadcrumb navigation', () => {
|
|
47
|
+
const wrapper = mountContributors();
|
|
48
|
+
expect(wrapper.text()).toContain('Home');
|
|
49
|
+
expect(wrapper.text()).toContain('Contributors');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renders Contributors heading', () => {
|
|
53
|
+
const wrapper = mountContributors();
|
|
54
|
+
expect(wrapper.text()).toContain('Contributors');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('shows empty state when no contributors', () => {
|
|
58
|
+
const wrapper = mountContributors();
|
|
59
|
+
expect(wrapper.text()).toContain('No contributor information available');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('shows contributors table when data present', async () => {
|
|
63
|
+
const { __setConfig } = await import('../config/use-site-config') as any;
|
|
64
|
+
__setConfig({
|
|
65
|
+
contributors: [
|
|
66
|
+
{ name: 'Jane Doe', role: 'Editor', organization: 'ISO', email: 'jane@example.com', url: 'https://example.com' },
|
|
67
|
+
{ name: 'John Smith', role: 'Author', organization: 'IEC' },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
const wrapper = mountContributors();
|
|
71
|
+
expect(wrapper.text()).toContain('Jane Doe');
|
|
72
|
+
expect(wrapper.text()).toContain('John Smith');
|
|
73
|
+
expect(wrapper.text()).toContain('Editor');
|
|
74
|
+
expect(wrapper.text()).toContain('ISO');
|
|
75
|
+
expect(wrapper.text()).toContain('jane@example.com');
|
|
76
|
+
expect(wrapper.text()).toContain('Author');
|
|
77
|
+
expect(wrapper.text()).toContain('IEC');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('links contributor name when URL present', async () => {
|
|
81
|
+
const { __setConfig } = await import('../config/use-site-config') as any;
|
|
82
|
+
__setConfig({
|
|
83
|
+
contributors: [
|
|
84
|
+
{ name: 'Jane Doe', url: 'https://example.com' },
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
const wrapper = mountContributors();
|
|
88
|
+
const link = wrapper.findAll('a').find(a => a.text() === 'Jane Doe');
|
|
89
|
+
expect(link).toBeDefined();
|
|
90
|
+
expect(link!.attributes('href')).toBe('https://example.com');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('shows dash for missing role/org', async () => {
|
|
94
|
+
const { __setConfig } = await import('../config/use-site-config') as any;
|
|
95
|
+
__setConfig({
|
|
96
|
+
contributors: [{ name: 'Anonymous' }],
|
|
97
|
+
});
|
|
98
|
+
const wrapper = mountContributors();
|
|
99
|
+
const cells = wrapper.findAll('td');
|
|
100
|
+
const dashCount = cells.filter(c => c.text() === '—');
|
|
101
|
+
expect(dashCount.length).toBeGreaterThanOrEqual(2);
|
|
102
|
+
});
|
|
103
|
+
});
|