@glossarist/concept-browser 0.3.3 → 0.3.7

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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/about-view.test.ts +98 -0
  3. package/src/__tests__/app-footer.test.ts +38 -0
  4. package/src/__tests__/app-header.test.ts +130 -0
  5. package/src/__tests__/app-sidebar.test.ts +159 -0
  6. package/src/__tests__/asciidoc-lite.test.ts +92 -0
  7. package/src/__tests__/concept-card.test.ts +115 -0
  8. package/src/__tests__/concept-detail-interaction.test.ts +251 -0
  9. package/src/__tests__/concept-timeline.test.ts +175 -0
  10. package/src/__tests__/concept-view.test.ts +75 -0
  11. package/src/__tests__/contributors-view.test.ts +103 -0
  12. package/src/__tests__/dataset-view.test.ts +231 -0
  13. package/src/__tests__/format-downloads.test.ts +98 -0
  14. package/src/__tests__/graph-view.test.ts +69 -0
  15. package/src/__tests__/home-interaction.test.ts +157 -0
  16. package/src/__tests__/language-detail.test.ts +146 -0
  17. package/src/__tests__/markdown-lite.test.ts +88 -0
  18. package/src/__tests__/nav-icon.test.ts +48 -0
  19. package/src/__tests__/news-view.test.ts +87 -0
  20. package/src/__tests__/page-view.test.ts +83 -0
  21. package/src/__tests__/plurimath.test.ts +71 -0
  22. package/src/__tests__/resolve-view.test.ts +77 -0
  23. package/src/__tests__/router.test.ts +65 -0
  24. package/src/__tests__/search-bar.test.ts +219 -0
  25. package/src/__tests__/search-view.test.ts +41 -0
  26. package/src/__tests__/stats-view.test.ts +77 -0
  27. package/src/__tests__/test-helpers.ts +168 -0
  28. package/src/__tests__/ui-store.test.ts +100 -0
  29. package/src/__tests__/v-math.test.ts +79 -0
  30. package/src/adapters/DatasetAdapter.ts +17 -15
  31. package/src/adapters/types.ts +1 -1
  32. package/src/components/ConceptDetail.vue +16 -54
  33. package/src/components/ConceptTimeline.vue +1 -8
  34. package/src/components/LanguageDetail.vue +2 -25
  35. package/src/composables/use-render-options.ts +1 -4
  36. package/src/router/index.ts +1 -1
  37. package/src/stores/vocabulary.ts +7 -7
  38. package/src/utils/asciidoc-lite.ts +17 -19
  39. package/src/utils/concept-helpers.ts +34 -0
  40. package/src/utils/escape.ts +7 -0
  41. package/src/utils/markdown-lite.ts +1 -3
  42. package/src/utils/math.ts +2 -11
  43. package/src/utils/plurimath.ts +2 -7
  44. package/src/views/ConceptView.vue +22 -1
  45. package/src/views/DatasetView.vue +7 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.3.3",
3
+ "version": "0.3.7",
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,98 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { mount, flushPromises } from '@vue/test-utils';
3
+ import AboutView from '../views/AboutView.vue';
4
+ import { useVocabularyStore } from '../stores/vocabulary';
5
+ import { createTestRouter, setupPinia, makeManifest, makeAdapterStub } from './test-helpers';
6
+
7
+ const testManifest = makeManifest({
8
+ title: 'Test Dataset',
9
+ description: 'A test dataset for terminology',
10
+ languages: ['eng', 'fra'],
11
+ conceptCount: 100,
12
+ tags: ['terminology', 'iso'],
13
+ sourceRepo: 'https://github.com/glossarist/test-dataset',
14
+ });
15
+
16
+ describe('AboutView', () => {
17
+ let pinia: ReturnType<typeof setupPinia>;
18
+ let router: Awaited<ReturnType<typeof createTestRouter>>;
19
+
20
+ beforeEach(async () => {
21
+ pinia = setupPinia();
22
+ router = await createTestRouter('dataset', '/dataset/test/about');
23
+ const store = useVocabularyStore();
24
+ store.manifests.set('test', testManifest);
25
+ store.datasets.set('test', makeAdapterStub());
26
+ });
27
+
28
+ function mountAbout() {
29
+ return mount(AboutView, {
30
+ global: { plugins: [pinia, router] },
31
+ props: { registerId: 'test' },
32
+ });
33
+ }
34
+
35
+ it('renders About heading', async () => {
36
+ const wrapper = mountAbout();
37
+ await flushPromises();
38
+ expect(wrapper.text()).toContain('About');
39
+ });
40
+
41
+ it('shows description', async () => {
42
+ const wrapper = mountAbout();
43
+ await flushPromises();
44
+ expect(wrapper.text()).toContain('A test dataset for terminology');
45
+ });
46
+
47
+ it('shows owner', async () => {
48
+ const wrapper = mountAbout();
49
+ await flushPromises();
50
+ expect(wrapper.text()).toContain('ISO');
51
+ });
52
+
53
+ it('shows concept count', async () => {
54
+ const wrapper = mountAbout();
55
+ await flushPromises();
56
+ expect(wrapper.text()).toContain('100');
57
+ });
58
+
59
+ it('shows language count', async () => {
60
+ const wrapper = mountAbout();
61
+ await flushPromises();
62
+ expect(wrapper.text()).toContain('English');
63
+ expect(wrapper.text()).toContain('French');
64
+ });
65
+
66
+ it('shows last updated date', async () => {
67
+ const wrapper = mountAbout();
68
+ await flushPromises();
69
+ expect(wrapper.text()).toContain('2025-01-01');
70
+ });
71
+
72
+ it('shows source repo link', async () => {
73
+ const wrapper = mountAbout();
74
+ await flushPromises();
75
+ expect(wrapper.text()).toContain('glossarist/test-dataset');
76
+ });
77
+
78
+ it('shows tags', async () => {
79
+ const wrapper = mountAbout();
80
+ await flushPromises();
81
+ expect(wrapper.text()).toContain('terminology');
82
+ expect(wrapper.text()).toContain('iso');
83
+ });
84
+
85
+ it('renders breadcrumb navigation', async () => {
86
+ const wrapper = mountAbout();
87
+ await flushPromises();
88
+ expect(wrapper.text()).toContain('Home');
89
+ expect(wrapper.text()).toContain('Test Dataset');
90
+ expect(wrapper.text()).toContain('About');
91
+ });
92
+
93
+ it('shows schema version', async () => {
94
+ const wrapper = mountAbout();
95
+ await flushPromises();
96
+ expect(wrapper.text()).toContain('1.0');
97
+ });
98
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import AppFooter from '../components/AppFooter.vue';
4
+ import { createTestRouter, setupPinia } from './test-helpers';
5
+
6
+ describe('AppFooter', () => {
7
+ let pinia: ReturnType<typeof setupPinia>;
8
+ let router: Awaited<ReturnType<typeof createTestRouter>>;
9
+
10
+ beforeEach(async () => {
11
+ pinia = setupPinia();
12
+ router = await createTestRouter('minimal', '/');
13
+ });
14
+
15
+ function mountFooter() {
16
+ return mount(AppFooter, {
17
+ global: { plugins: [pinia, router] },
18
+ });
19
+ }
20
+
21
+ it('renders powered by text', () => {
22
+ const wrapper = mountFooter();
23
+ expect(wrapper.text()).toContain('Built with the');
24
+ expect(wrapper.text()).toContain('Glossarist Concept Browser');
25
+ });
26
+
27
+ it('omits copyright when no config loaded', () => {
28
+ const wrapper = mountFooter();
29
+ expect(wrapper.text()).not.toContain('©');
30
+ });
31
+
32
+ it('links to GitHub repository', () => {
33
+ const wrapper = mountFooter();
34
+ const link = wrapper.findAll('a').find(a => a.text().includes('Glossarist Concept Browser'));
35
+ expect(link).toBeDefined();
36
+ expect(link!.attributes('href')).toContain('github.com');
37
+ });
38
+ });
@@ -0,0 +1,130 @@
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 AppHeader from '../components/AppHeader.vue';
6
+ import { useVocabularyStore } from '../stores/vocabulary';
7
+ import type { Manifest } 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'],
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
+ async function createTestRouter() {
34
+ return createRouter({
35
+ history: createMemoryHistory(),
36
+ routes: [
37
+ { path: '/', name: 'home', component: { template: '<div/>' } },
38
+ { path: '/search', name: 'search', component: { template: '<div/>' } },
39
+ ],
40
+ });
41
+ }
42
+
43
+ describe('AppHeader', () => {
44
+ let pinia: ReturnType<typeof createPinia>;
45
+ let router: Awaited<ReturnType<typeof createTestRouter>>;
46
+
47
+ beforeEach(async () => {
48
+ pinia = createPinia();
49
+ setActivePinia(pinia);
50
+ router = await createTestRouter();
51
+ router.push('/');
52
+ await router.isReady();
53
+ const store = useVocabularyStore();
54
+ store.manifests.set('test', makeManifest());
55
+ store.datasets.set('test', { index: [], getConceptCount: () => 0, getConcepts: () => [] } as any);
56
+ });
57
+
58
+ function mountHeader() {
59
+ return mount(AppHeader, {
60
+ global: { plugins: [pinia, router] },
61
+ });
62
+ }
63
+
64
+ it('renders logo/title text', () => {
65
+ const wrapper = mountHeader();
66
+ expect(wrapper.text()).toContain('Glossarist');
67
+ });
68
+
69
+ it('renders search input', () => {
70
+ const wrapper = mountHeader();
71
+ const input = wrapper.find('input[aria-label="Search concepts"]');
72
+ expect(input.exists()).toBe(true);
73
+ });
74
+
75
+ it('renders mobile hamburger button', () => {
76
+ const wrapper = mountHeader();
77
+ const hamburger = wrapper.find('button[aria-label="Open navigation menu"]');
78
+ expect(hamburger.exists()).toBe(true);
79
+ });
80
+
81
+ it('navigates home on logo click', async () => {
82
+ const wrapper = mountHeader();
83
+ const logoBtn = wrapper.findAll('button').find(b => b.text().includes('Glossarist'));
84
+ expect(logoBtn).toBeDefined();
85
+ await logoBtn!.trigger('click');
86
+ await flushPromises();
87
+ expect(router.currentRoute.value.name).toBe('home');
88
+ });
89
+
90
+ it('navigates to search on form submit with query', async () => {
91
+ const wrapper = mountHeader();
92
+ const input = wrapper.find('input[aria-label="Search concepts"]');
93
+ await input.setValue('road');
94
+ const form = wrapper.find('form');
95
+ await form.trigger('submit');
96
+ await flushPromises();
97
+ expect(router.currentRoute.value.name).toBe('search');
98
+ expect(router.currentRoute.value.query.q).toBe('road');
99
+ });
100
+
101
+ it('does not navigate on empty search', async () => {
102
+ const wrapper = mountHeader();
103
+ const form = wrapper.find('form');
104
+ await form.trigger('submit');
105
+ await flushPromises();
106
+ expect(router.currentRoute.value.name).toBe('home');
107
+ });
108
+
109
+ it('renders theme toggle button', () => {
110
+ const wrapper = mountHeader();
111
+ const themeBtn = wrapper.findAll('button').find(b =>
112
+ b.attributes('aria-label')?.includes('Switch to')
113
+ );
114
+ expect(themeBtn).toBeDefined();
115
+ });
116
+
117
+ it('toggles theme on button click', async () => {
118
+ const wrapper = mountHeader();
119
+ const themeBtn = wrapper.findAll('button').find(b =>
120
+ b.attributes('aria-label')?.includes('Switch to')
121
+ );
122
+ await themeBtn!.trigger('click');
123
+ expect(wrapper.html()).toContain('M12 3v1m0 16v1m9-9h-1');
124
+ });
125
+
126
+ it('shows dataset count', () => {
127
+ const wrapper = mountHeader();
128
+ expect(wrapper.text()).toContain('1 datasets');
129
+ });
130
+ });
@@ -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
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderAsciiDocLite } from '../utils/asciidoc-lite';
3
+
4
+ describe('renderAsciiDocLite', () => {
5
+ it('returns empty string for empty input', () => {
6
+ expect(renderAsciiDocLite('')).toBe('');
7
+ });
8
+
9
+ it('wraps plain text in <p>', () => {
10
+ expect(renderAsciiDocLite('Hello world')).toBe('<p>Hello world</p>');
11
+ });
12
+
13
+ it('creates separate paragraphs on blank lines', () => {
14
+ const result = renderAsciiDocLite('First\n\nSecond');
15
+ expect(result).toContain('<p>First</p>');
16
+ expect(result).toContain('<p>Second</p>');
17
+ });
18
+
19
+ it('renders headings (level + 1, h1 reserved)', () => {
20
+ expect(renderAsciiDocLite('== Heading 2')).toContain('<h3>Heading 2</h3>');
21
+ expect(renderAsciiDocLite('=== Heading 3')).toContain('<h4>Heading 3</h4>');
22
+ expect(renderAsciiDocLite('===== Heading 5')).toContain('<h6>Heading 5</h6>');
23
+ });
24
+
25
+ it('renders bold text', () => {
26
+ expect(renderAsciiDocLite('some *bold* text')).toContain('<strong>bold</strong>');
27
+ });
28
+
29
+ it('renders italic text', () => {
30
+ expect(renderAsciiDocLite('some _italic_ text')).toContain('<em>italic</em>');
31
+ });
32
+
33
+ it('renders monospace text', () => {
34
+ expect(renderAsciiDocLite('use `code` here')).toContain('<code>code</code>');
35
+ });
36
+
37
+ it('renders AsciiDoc links with label', () => {
38
+ const result = renderAsciiDocLite('see https://example.com[label] here');
39
+ expect(result).toContain('<a href="https://example.com"');
40
+ expect(result).toContain('>label</a>');
41
+ });
42
+
43
+ it('renders bare URLs as links', () => {
44
+ const result = renderAsciiDocLite('visit https://example.com now');
45
+ expect(result).toContain('<a href="https://example.com"');
46
+ });
47
+
48
+ it('renders unordered lists', () => {
49
+ const result = renderAsciiDocLite('* item one\n* item two');
50
+ expect(result).toContain('<ul>');
51
+ expect(result).toContain('<li');
52
+ expect(result).toContain('item one');
53
+ expect(result).toContain('item two');
54
+ });
55
+
56
+ it('renders ordered lists', () => {
57
+ const result = renderAsciiDocLite('. first\n. second');
58
+ expect(result).toContain('<ol>');
59
+ expect(result).toContain('first');
60
+ expect(result).toContain('second');
61
+ });
62
+
63
+ it('renders source blocks with ---- delimiter', () => {
64
+ const result = renderAsciiDocLite('----\nlet x = 1;\n----');
65
+ expect(result).toContain('<pre><code>');
66
+ expect(result).toContain('let x = 1;');
67
+ });
68
+
69
+ it('renders source blocks with .... delimiter', () => {
70
+ const result = renderAsciiDocLite('....\nsome text\n....');
71
+ expect(result).toContain('<pre><code>');
72
+ expect(result).toContain('some text');
73
+ });
74
+
75
+ it('escapes HTML in source blocks', () => {
76
+ const result = renderAsciiDocLite('----\n<a href="evil">\n----');
77
+ expect(result).toContain('&lt;a href="evil"&gt;');
78
+ expect(result).not.toContain('<a href="evil">');
79
+ });
80
+
81
+ it('handles multi-line paragraphs', () => {
82
+ const result = renderAsciiDocLite('line one\nline two\n\nnew paragraph');
83
+ expect(result).toContain('<p>line one line two</p>');
84
+ expect(result).toContain('<p>new paragraph</p>');
85
+ });
86
+
87
+ it('handles nested list levels', () => {
88
+ const result = renderAsciiDocLite('* top\n** nested');
89
+ expect(result).toContain('list-level-1');
90
+ expect(result).toContain('list-level-2');
91
+ });
92
+ });
@@ -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', 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
+ });