@glossarist/concept-browser 0.7.44 → 0.7.45
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 +2 -2
- package/scripts/generate-data.mjs +20 -11
- package/scripts/lib/build/image-assets.mjs +190 -0
- package/scripts/lib/build/non-verbal-consumer.mjs +221 -0
- package/src/__tests__/bibliography-adapter.test.ts +79 -0
- package/src/__tests__/content-renderer-nvr-mentions.test.ts +57 -0
- package/src/__tests__/locale.test.ts +46 -0
- package/src/__tests__/model-bridge-entity-refs.test.ts +114 -0
- package/src/__tests__/non-verbal-anchor.test.ts +33 -0
- package/src/__tests__/non-verbal-cross-ref.test.ts +146 -0
- package/src/__tests__/non-verbal-highlight.test.ts +56 -0
- package/src/__tests__/non-verbal-kind.test.ts +77 -0
- package/src/__tests__/non-verbal-list.test.ts +67 -0
- package/src/__tests__/non-verbal-rep-display.test.ts +85 -0
- package/src/__tests__/non-verbal-scroll-guard.test.ts +116 -0
- package/src/__tests__/use-concept-entities.test.ts +76 -0
- package/src/adapters/bibliography-adapter.ts +49 -0
- package/src/adapters/factory.ts +14 -0
- package/src/adapters/model-bridge.ts +51 -0
- package/src/adapters/non-verbal/figure-bridge.ts +101 -0
- package/src/adapters/non-verbal/formula-bridge.ts +48 -0
- package/src/adapters/non-verbal/index.ts +55 -0
- package/src/adapters/non-verbal/kind.ts +46 -0
- package/src/adapters/non-verbal/prefix.ts +67 -0
- package/src/adapters/non-verbal/source-bridge.ts +81 -0
- package/src/adapters/non-verbal/table-bridge.ts +98 -0
- package/src/adapters/non-verbal/types.ts +133 -0
- package/src/adapters/non-verbal-resolver.ts +101 -0
- package/src/components/ConceptDetail.vue +17 -4
- package/src/components/LanguageDetail.vue +0 -3
- package/src/components/NonVerbalRepDisplay.vue +82 -24
- package/src/components/figure/FigureDisplay.vue +132 -0
- package/src/components/figure/FigureImages.vue +111 -0
- package/src/components/figure/figure-image-pick.ts +56 -0
- package/src/components/figure/figure-layout.ts +26 -0
- package/src/components/formula/FormulaDisplay.vue +90 -0
- package/src/components/formula/FormulaExpression.vue +70 -0
- package/src/components/non-verbal/NonVerbalCaption.vue +104 -0
- package/src/components/non-verbal/NonVerbalFallback.vue +69 -0
- package/src/components/non-verbal/NonVerbalList.vue +118 -0
- package/src/components/non-verbal/NonVerbalSources.vue +61 -0
- package/src/components/table/TableDisplay.vue +99 -0
- package/src/components/table/TableMarkup.vue +63 -0
- package/src/components/table/TableStructured.vue +66 -0
- package/src/composables/use-concept-entities.ts +70 -0
- package/src/composables/use-non-verbal-cross-ref.ts +79 -0
- package/src/composables/use-non-verbal-entity.ts +58 -0
- package/src/composables/use-reduced-motion.ts +26 -0
- package/src/composables/use-render-options.ts +30 -33
- package/src/router/index.ts +3 -0
- package/src/router/non-verbal-scroll-guard.ts +56 -0
- package/src/style.css +17 -0
- package/src/utils/content-renderer.ts +76 -64
- package/src/utils/locale.ts +92 -0
- package/src/utils/non-verbal-anchor.ts +51 -0
- package/src/utils/non-verbal-highlight.ts +27 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { conceptFromJson } from '../adapters/model-bridge';
|
|
3
|
+
import type { Concept } from 'glossarist';
|
|
4
|
+
|
|
5
|
+
interface ConceptWithEntityRefs {
|
|
6
|
+
readonly figures: ReadonlyArray<{ entityId: string | null; display: string | null }>;
|
|
7
|
+
readonly tables: ReadonlyArray<{ entityId: string | null; display: string | null }>;
|
|
8
|
+
readonly formulas: ReadonlyArray<{ entityId: string | null; display: string | null }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function refsOf(c: Concept): ConceptWithEntityRefs {
|
|
12
|
+
return c as unknown as ConceptWithEntityRefs;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function conceptWithRefs(): Concept {
|
|
16
|
+
return conceptFromJson({
|
|
17
|
+
'@context': 'https://glossarist.org/ns/context.jsonld',
|
|
18
|
+
'@id': 'https://www.geolexica.org/isotc204/concept/3.1.1.1',
|
|
19
|
+
'@type': 'gl:Concept',
|
|
20
|
+
'gl:identifier': '3.1.1.1',
|
|
21
|
+
'gl:localizedConcept': {
|
|
22
|
+
eng: {
|
|
23
|
+
'@id': 'https://www.geolexica.org/isotc204/concept/3.1.1.1/eng',
|
|
24
|
+
'@type': 'gl:LocalizedConcept',
|
|
25
|
+
'gl:languageCode': 'eng',
|
|
26
|
+
'gl:entryStatus': 'valid',
|
|
27
|
+
'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'entity' }],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
'gl:figureRef': [
|
|
31
|
+
'mixed-reflection',
|
|
32
|
+
{ '@id': '../figure/dispersion-prism' },
|
|
33
|
+
{ '@id': '../figure/standard-wavelengths', 'gl:display': 'Figure 3' },
|
|
34
|
+
],
|
|
35
|
+
'gl:tableRef': [{ '@id': '../table/wavelength-table' }],
|
|
36
|
+
'gl:formulaRef': ['e-mc2'],
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('conceptFromJson — structural entity refs', () => {
|
|
41
|
+
it('extracts bare-string figure refs', () => {
|
|
42
|
+
const c = refsOf(conceptWithRefs());
|
|
43
|
+
const ids = c.figures.map(f => f.entityId);
|
|
44
|
+
expect(ids).toContain('mixed-reflection');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('extracts @id-only figure refs by last path segment', () => {
|
|
48
|
+
const c = refsOf(conceptWithRefs());
|
|
49
|
+
const ids = c.figures.map(f => f.entityId);
|
|
50
|
+
expect(ids).toContain('dispersion-prism');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('preserves gl:display as the reference display override', () => {
|
|
54
|
+
const c = refsOf(conceptWithRefs());
|
|
55
|
+
const withDisplay = c.figures.find(f => f.entityId === 'standard-wavelengths');
|
|
56
|
+
expect(withDisplay?.display).toBe('Figure 3');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('extracts table refs from gl:tableRef', () => {
|
|
60
|
+
const c = refsOf(conceptWithRefs());
|
|
61
|
+
expect(c.tables.map(t => t.entityId)).toContain('wavelength-table');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('extracts formula refs from gl:formulaRef', () => {
|
|
65
|
+
const c = refsOf(conceptWithRefs());
|
|
66
|
+
expect(c.formulas.map(f => f.entityId)).toContain('e-mc2');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns empty arrays when no ref fields are present', () => {
|
|
70
|
+
const c = refsOf(conceptFromJson({
|
|
71
|
+
'@context': 'https://glossarist.org/ns/context.jsonld',
|
|
72
|
+
'@id': 'https://example.org/x/concept/1',
|
|
73
|
+
'@type': 'gl:Concept',
|
|
74
|
+
'gl:identifier': '1',
|
|
75
|
+
'gl:localizedConcept': {
|
|
76
|
+
eng: {
|
|
77
|
+
'@type': 'gl:LocalizedConcept',
|
|
78
|
+
'gl:languageCode': 'eng',
|
|
79
|
+
'gl:entryStatus': 'valid',
|
|
80
|
+
'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'x' }],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
expect(c.figures).toEqual([]);
|
|
85
|
+
expect(c.tables).toEqual([]);
|
|
86
|
+
expect(c.formulas).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('skips malformed entries (empty string, null, missing @id)', () => {
|
|
90
|
+
const c = refsOf(conceptFromJson({
|
|
91
|
+
'@context': 'https://glossarist.org/ns/context.jsonld',
|
|
92
|
+
'@id': 'https://example.org/x/concept/1',
|
|
93
|
+
'@type': 'gl:Concept',
|
|
94
|
+
'gl:identifier': '1',
|
|
95
|
+
'gl:localizedConcept': {
|
|
96
|
+
eng: {
|
|
97
|
+
'@type': 'gl:LocalizedConcept',
|
|
98
|
+
'gl:languageCode': 'eng',
|
|
99
|
+
'gl:entryStatus': 'valid',
|
|
100
|
+
'gl:designation': [{ '@type': 'gl:Expression', 'gl:term': 'x' }],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
'gl:figureRef': [
|
|
104
|
+
'',
|
|
105
|
+
null,
|
|
106
|
+
{},
|
|
107
|
+
{ '@id': '' },
|
|
108
|
+
{ ref: '' },
|
|
109
|
+
' ',
|
|
110
|
+
],
|
|
111
|
+
}));
|
|
112
|
+
expect(c.figures).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
anchorId,
|
|
4
|
+
ANCHOR_KIND_SELECTORS,
|
|
5
|
+
} from '../utils/non-verbal-anchor';
|
|
6
|
+
|
|
7
|
+
describe('non-verbal-anchor', () => {
|
|
8
|
+
describe('anchorId', () => {
|
|
9
|
+
it('joins kind + datasetId + entityId with hyphens', () => {
|
|
10
|
+
expect(anchorId('figure', 'iala-2023', 'mixed-reflection'))
|
|
11
|
+
.toBe('figure-iala-2023-mixed-reflection');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('uses the kind name as prefix (not an alias)', () => {
|
|
15
|
+
expect(anchorId('table', 'iso', 'standard-wavelengths'))
|
|
16
|
+
.toBe('table-iso-standard-wavelengths');
|
|
17
|
+
expect(anchorId('formula', 'iso', 'e-mc2'))
|
|
18
|
+
.toBe('formula-iso-e-mc2');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('ANCHOR_KIND_SELECTORS', () => {
|
|
23
|
+
it('contains one selector per kind', () => {
|
|
24
|
+
expect(ANCHOR_KIND_SELECTORS).toContain('a[href^="#figure-"]');
|
|
25
|
+
expect(ANCHOR_KIND_SELECTORS).toContain('a[href^="#table-"]');
|
|
26
|
+
expect(ANCHOR_KIND_SELECTORS).toContain('a[href^="#formula-"]');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('has exactly three selectors (one per kind)', () => {
|
|
30
|
+
expect(ANCHOR_KIND_SELECTORS.length).toBe(3);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { defineComponent, h } from 'vue';
|
|
3
|
+
import { mount } from '@vue/test-utils';
|
|
4
|
+
import { useNonVerbalCrossRef } from '../composables/use-non-verbal-cross-ref';
|
|
5
|
+
|
|
6
|
+
const Host = defineComponent({
|
|
7
|
+
setup() {
|
|
8
|
+
const { navigateToEntity } = useNonVerbalCrossRef();
|
|
9
|
+
return { navigateToEntity };
|
|
10
|
+
},
|
|
11
|
+
render() {
|
|
12
|
+
return h('div');
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function setHash(href: string) {
|
|
17
|
+
// jsdom-compatible history stub
|
|
18
|
+
Object.defineProperty(window, 'location', {
|
|
19
|
+
configurable: true,
|
|
20
|
+
value: { ...window.location, hash: '' },
|
|
21
|
+
});
|
|
22
|
+
// Some jsdom builds make location readonly; fall back to defining on the prototype chain.
|
|
23
|
+
try {
|
|
24
|
+
window.location.hash = href;
|
|
25
|
+
} catch {
|
|
26
|
+
Object.defineProperty(window.location, 'hash', { configurable: true, value: href });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('useNonVerbalCrossRef', () => {
|
|
31
|
+
let pushSpy: ReturnType<typeof vi.spyOn>;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.useFakeTimers();
|
|
35
|
+
pushSpy = vi.spyOn(window.history, 'pushState').mockImplementation(() => undefined);
|
|
36
|
+
setHash('');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
vi.useRealTimers();
|
|
41
|
+
vi.restoreAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function click(target: HTMLElement): void {
|
|
45
|
+
const ev = new MouseEvent('click', { bubbles: true, cancelable: true });
|
|
46
|
+
target.dispatchEvent(ev);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
it('activates the target when a figure anchor link is clicked', async () => {
|
|
50
|
+
const wrapper = mount(Host, { attachTo: document.body });
|
|
51
|
+
const figure = document.createElement('figure');
|
|
52
|
+
figure.id = 'figure-ds-mixed';
|
|
53
|
+
const scrollSpy = vi.spyOn(figure, 'scrollIntoView').mockImplementation(() => undefined);
|
|
54
|
+
const focusSpy = vi.spyOn(figure, 'focus').mockImplementation(() => undefined);
|
|
55
|
+
document.body.appendChild(figure);
|
|
56
|
+
|
|
57
|
+
const link = document.createElement('a');
|
|
58
|
+
link.href = '#figure-ds-mixed';
|
|
59
|
+
link.textContent = 'Fig 1';
|
|
60
|
+
document.body.appendChild(link);
|
|
61
|
+
|
|
62
|
+
click(link);
|
|
63
|
+
|
|
64
|
+
expect(scrollSpy).toHaveBeenCalled();
|
|
65
|
+
expect(focusSpy).toHaveBeenCalledWith({ preventScroll: true });
|
|
66
|
+
expect(figure.classList.contains('nv-entity--highlighted')).toBe(true);
|
|
67
|
+
expect(pushSpy).toHaveBeenCalledWith(null, '', '#figure-ds-mixed');
|
|
68
|
+
|
|
69
|
+
document.body.removeChild(figure);
|
|
70
|
+
document.body.removeChild(link);
|
|
71
|
+
wrapper.unmount();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('responds to table and formula anchor prefixes', async () => {
|
|
75
|
+
const wrapper = mount(Host, { attachTo: document.body });
|
|
76
|
+
|
|
77
|
+
for (const kind of ['table', 'formula'] as const) {
|
|
78
|
+
const target = document.createElement('div');
|
|
79
|
+
target.id = `${kind}-ds-foo`;
|
|
80
|
+
const scrollSpy = vi.spyOn(target, 'scrollIntoView').mockImplementation(() => undefined);
|
|
81
|
+
document.body.appendChild(target);
|
|
82
|
+
|
|
83
|
+
const link = document.createElement('a');
|
|
84
|
+
link.href = `#${kind}-ds-foo`;
|
|
85
|
+
document.body.appendChild(link);
|
|
86
|
+
|
|
87
|
+
click(link);
|
|
88
|
+
expect(scrollSpy).toHaveBeenCalled();
|
|
89
|
+
vi.mocked(scrollSpy).mockRestore();
|
|
90
|
+
|
|
91
|
+
document.body.removeChild(target);
|
|
92
|
+
document.body.removeChild(link);
|
|
93
|
+
}
|
|
94
|
+
wrapper.unmount();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('ignores clicks on links that are not entity anchors', async () => {
|
|
98
|
+
const wrapper = mount(Host, { attachTo: document.body });
|
|
99
|
+
const otherEl = document.createElement('div');
|
|
100
|
+
otherEl.id = 'something-else';
|
|
101
|
+
const scrollSpy = vi.spyOn(otherEl, 'scrollIntoView').mockImplementation(() => undefined);
|
|
102
|
+
document.body.appendChild(otherEl);
|
|
103
|
+
|
|
104
|
+
const link = document.createElement('a');
|
|
105
|
+
link.href = '#something-else';
|
|
106
|
+
document.body.appendChild(link);
|
|
107
|
+
|
|
108
|
+
click(link);
|
|
109
|
+
expect(scrollSpy).not.toHaveBeenCalled();
|
|
110
|
+
expect(pushSpy).not.toHaveBeenCalled();
|
|
111
|
+
|
|
112
|
+
document.body.removeChild(otherEl);
|
|
113
|
+
document.body.removeChild(link);
|
|
114
|
+
wrapper.unmount();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not activate when the target element is missing', async () => {
|
|
118
|
+
const wrapper = mount(Host, { attachTo: document.body });
|
|
119
|
+
const link = document.createElement('a');
|
|
120
|
+
link.href = '#figure-ds-missing';
|
|
121
|
+
document.body.appendChild(link);
|
|
122
|
+
|
|
123
|
+
expect(() => click(link)).not.toThrow();
|
|
124
|
+
expect(pushSpy).not.toHaveBeenCalled();
|
|
125
|
+
|
|
126
|
+
document.body.removeChild(link);
|
|
127
|
+
wrapper.unmount();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('navigateToEntity activates a known target programmatically', async () => {
|
|
131
|
+
const wrapper = mount(Host, { attachTo: document.body });
|
|
132
|
+
const target = document.createElement('figure');
|
|
133
|
+
target.id = 'figure-ds-prog';
|
|
134
|
+
const scrollSpy = vi.spyOn(target, 'scrollIntoView').mockImplementation(() => undefined);
|
|
135
|
+
document.body.appendChild(target);
|
|
136
|
+
|
|
137
|
+
(wrapper.vm as unknown as { navigateToEntity: (id: string) => void })
|
|
138
|
+
.navigateToEntity('figure-ds-prog');
|
|
139
|
+
|
|
140
|
+
expect(scrollSpy).toHaveBeenCalled();
|
|
141
|
+
expect(target.classList.contains('nv-entity--highlighted')).toBe(true);
|
|
142
|
+
|
|
143
|
+
document.body.removeChild(target);
|
|
144
|
+
wrapper.unmount();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { highlightEntity, scrollToEntity } from '../utils/non-verbal-highlight';
|
|
3
|
+
|
|
4
|
+
describe('non-verbal-highlight', () => {
|
|
5
|
+
let el: HTMLElement;
|
|
6
|
+
let focusSpy: ReturnType<typeof vi.spyOn>;
|
|
7
|
+
let scrollIntoViewSpy: ReturnType<typeof vi.spyOn>;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
el = document.createElement('div');
|
|
11
|
+
focusSpy = vi.spyOn(el, 'focus').mockImplementation(() => undefined);
|
|
12
|
+
scrollIntoViewSpy = vi.spyOn(el, 'scrollIntoView').mockImplementation(() => undefined);
|
|
13
|
+
vi.useFakeTimers();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.useRealTimers();
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('adds the highlight class and tabindex', () => {
|
|
22
|
+
highlightEntity(el);
|
|
23
|
+
expect(el.classList.contains('nv-entity--highlighted')).toBe(true);
|
|
24
|
+
expect(el.getAttribute('tabindex')).toBe('-1');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('focuses the element without scrolling', () => {
|
|
28
|
+
highlightEntity(el);
|
|
29
|
+
expect(focusSpy).toHaveBeenCalledWith({ preventScroll: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('removes the highlight class after the duration', () => {
|
|
33
|
+
highlightEntity(el);
|
|
34
|
+
expect(el.classList.contains('nv-entity--highlighted')).toBe(true);
|
|
35
|
+
vi.advanceTimersByTime(2000);
|
|
36
|
+
expect(el.classList.contains('nv-entity--highlighted')).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('is a no-op for null target', () => {
|
|
40
|
+
expect(() => highlightEntity(null)).not.toThrow();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('scrollToEntity uses smooth behavior when smooth=true', () => {
|
|
44
|
+
scrollToEntity(el, true);
|
|
45
|
+
expect(scrollIntoViewSpy).toHaveBeenCalledWith({ behavior: 'smooth', block: 'start' });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('scrollToEntity uses auto behavior when smooth=false', () => {
|
|
49
|
+
scrollToEntity(el, false);
|
|
50
|
+
expect(scrollIntoViewSpy).toHaveBeenCalledWith({ behavior: 'auto', block: 'start' });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('scrollToEntity is a no-op for null target', () => {
|
|
54
|
+
expect(() => scrollToEntity(null, true)).not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
KIND_TO_DIR,
|
|
4
|
+
KIND_TO_BRIDGE,
|
|
5
|
+
ALL_KINDS,
|
|
6
|
+
MENTION_KIND_TO_ENTITY_KIND,
|
|
7
|
+
entityKindFromMentionKind,
|
|
8
|
+
kindFromType,
|
|
9
|
+
} from '../adapters/non-verbal/kind';
|
|
10
|
+
|
|
11
|
+
describe('non-verbal kind dispatch', () => {
|
|
12
|
+
describe('KIND_TO_DIR', () => {
|
|
13
|
+
it('maps each kind to its plural directory name', () => {
|
|
14
|
+
expect(KIND_TO_DIR.figure).toBe('figures');
|
|
15
|
+
expect(KIND_TO_DIR.table).toBe('tables');
|
|
16
|
+
expect(KIND_TO_DIR.formula).toBe('formulas');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('is frozen (immutable at runtime)', () => {
|
|
20
|
+
expect(Object.isFrozen(KIND_TO_DIR)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('ALL_KINDS', () => {
|
|
25
|
+
it('includes figure, table, formula', () => {
|
|
26
|
+
expect(ALL_KINDS).toContain('figure');
|
|
27
|
+
expect(ALL_KINDS).toContain('table');
|
|
28
|
+
expect(ALL_KINDS).toContain('formula');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('KIND_TO_BRIDGE', () => {
|
|
33
|
+
it('maps every kind to a function', () => {
|
|
34
|
+
for (const kind of ALL_KINDS) {
|
|
35
|
+
expect(typeof KIND_TO_BRIDGE[kind]).toBe('function');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('MENTION_KIND_TO_ENTITY_KIND', () => {
|
|
41
|
+
it('maps parseMention kinds to entity kinds', () => {
|
|
42
|
+
expect(MENTION_KIND_TO_ENTITY_KIND['fig-ref']).toBe('figure');
|
|
43
|
+
expect(MENTION_KIND_TO_ENTITY_KIND['table-ref']).toBe('table');
|
|
44
|
+
expect(MENTION_KIND_TO_ENTITY_KIND['formula-ref']).toBe('formula');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('entityKindFromMentionKind', () => {
|
|
49
|
+
it('returns the entity kind for known mention kinds', () => {
|
|
50
|
+
expect(entityKindFromMentionKind('fig-ref')).toBe('figure');
|
|
51
|
+
expect(entityKindFromMentionKind('table-ref')).toBe('table');
|
|
52
|
+
expect(entityKindFromMentionKind('formula-ref')).toBe('formula');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns null for unknown mention kinds', () => {
|
|
56
|
+
expect(entityKindFromMentionKind('cite-ref')).toBeNull();
|
|
57
|
+
expect(entityKindFromMentionKind('numeric')).toBeNull();
|
|
58
|
+
expect(entityKindFromMentionKind('unknown')).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('kindFromType', () => {
|
|
63
|
+
it('accepts gl: prefix', () => {
|
|
64
|
+
expect(kindFromType('gl:Figure')).toBe('figure');
|
|
65
|
+
expect(kindFromType('gl:Table')).toBe('table');
|
|
66
|
+
expect(kindFromType('gl:Formula')).toBe('formula');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('accepts gloss: prefix', () => {
|
|
70
|
+
expect(kindFromType('gloss:Figure')).toBe('figure');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns null for unknown type', () => {
|
|
74
|
+
expect(kindFromType('gl:Unknown')).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import NonVerbalList from '../components/non-verbal/NonVerbalList.vue';
|
|
4
|
+
import type { StructuralEntityRef } from '../composables/use-concept-entities';
|
|
5
|
+
|
|
6
|
+
function makeRef(overrides: Partial<StructuralEntityRef> = {}): StructuralEntityRef {
|
|
7
|
+
return {
|
|
8
|
+
kind: 'figure',
|
|
9
|
+
entityId: 'mixed-reflection',
|
|
10
|
+
display: null,
|
|
11
|
+
anchor: 'figure-ds-mixed-reflection',
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('NonVerbalList.vue', () => {
|
|
17
|
+
it('renders nothing when refs is empty', () => {
|
|
18
|
+
const wrapper = mount(NonVerbalList, { props: { refs: [] } });
|
|
19
|
+
expect(wrapper.find('.section-label').exists()).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders one group per kind', () => {
|
|
23
|
+
const wrapper = mount(NonVerbalList, {
|
|
24
|
+
props: {
|
|
25
|
+
refs: [
|
|
26
|
+
makeRef({ kind: 'figure', entityId: 'foo', anchor: 'figure-ds-foo' }),
|
|
27
|
+
makeRef({ kind: 'table', entityId: 'bar', anchor: 'table-ds-bar' }),
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
expect(wrapper.text()).toContain('Figures');
|
|
32
|
+
expect(wrapper.text()).toContain('Tables');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('links to the anchor href', () => {
|
|
36
|
+
const wrapper = mount(NonVerbalList, {
|
|
37
|
+
props: {
|
|
38
|
+
refs: [makeRef({ kind: 'figure', entityId: 'mixed', anchor: 'figure-ds-mixed' })],
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const a = wrapper.find('a[href="#figure-ds-mixed"]');
|
|
42
|
+
expect(a.exists()).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('uses display when present, falls back to entityId', () => {
|
|
46
|
+
const wrapper = mount(NonVerbalList, {
|
|
47
|
+
props: {
|
|
48
|
+
refs: [
|
|
49
|
+
makeRef({ entityId: 'mixed', display: 'Figure 3', anchor: 'figure-ds-mixed' }),
|
|
50
|
+
makeRef({ entityId: 'plain', display: null, anchor: 'figure-ds-plain' }),
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
expect(wrapper.text()).toContain('Figure 3');
|
|
55
|
+
expect(wrapper.text()).toContain('plain');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('shows the raw ID as a secondary label when display is present', () => {
|
|
59
|
+
const wrapper = mount(NonVerbalList, {
|
|
60
|
+
props: {
|
|
61
|
+
refs: [makeRef({ entityId: 'dispersion-prism', display: 'Figure 3', anchor: 'figure-ds-x' })],
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const item = wrapper.find('.nv-list__id');
|
|
65
|
+
expect(item.text()).toBe('dispersion-prism');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
4
|
+
import { createRouter, createMemoryHistory } from 'vue-router';
|
|
5
|
+
import type { NonVerbRep } from 'glossarist';
|
|
6
|
+
import NonVerbalRepDisplay from '../components/NonVerbalRepDisplay.vue';
|
|
7
|
+
import { resetFactory } from '../adapters/factory';
|
|
8
|
+
|
|
9
|
+
// glossarist-js's published `.d.ts` declares NonVerbRep's constructor with 0
|
|
10
|
+
// args, but the V3 runtime accepts a plain-data initializer. Cast through
|
|
11
|
+
// `unknown` at this boundary. See TODO.figures/19.
|
|
12
|
+
function makeRep(data: Record<string, unknown>): NonVerbRep {
|
|
13
|
+
return data as unknown as NonVerbRep;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeRouter() {
|
|
17
|
+
return createRouter({
|
|
18
|
+
history: createMemoryHistory(),
|
|
19
|
+
routes: [
|
|
20
|
+
{ path: '/', name: 'home', component: { template: '<div/>' } },
|
|
21
|
+
{ path: '/dataset/:registerId/concept/:conceptId', name: 'concept', component: { template: '<div/>' } },
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('NonVerbalRepDisplay.vue — V3 shape', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
resetFactory();
|
|
29
|
+
setActivePinia(createPinia());
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders nothing when reps is empty', () => {
|
|
33
|
+
const wrapper = mount(NonVerbalRepDisplay, {
|
|
34
|
+
props: { reps: [], locale: 'eng', registerId: 'ds' },
|
|
35
|
+
});
|
|
36
|
+
expect(wrapper.find('.section-label').exists()).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renders a figure per rep', () => {
|
|
40
|
+
const rep = makeRep({
|
|
41
|
+
type: 'photograph',
|
|
42
|
+
images: [{ src: 'fig.png', format: 'png' }],
|
|
43
|
+
alt: { eng: 'A diagram' },
|
|
44
|
+
});
|
|
45
|
+
const wrapper = mount(NonVerbalRepDisplay, {
|
|
46
|
+
props: { reps: [rep], locale: 'eng', registerId: 'ds' },
|
|
47
|
+
});
|
|
48
|
+
expect(wrapper.findAll('figure').length).toBe(1);
|
|
49
|
+
expect(wrapper.text()).toContain('photograph');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renders the localized caption when present', () => {
|
|
53
|
+
const rep = makeRep({
|
|
54
|
+
type: 'photo',
|
|
55
|
+
caption: { eng: 'Figure 1' },
|
|
56
|
+
});
|
|
57
|
+
const wrapper = mount(NonVerbalRepDisplay, {
|
|
58
|
+
props: { reps: [rep], locale: 'eng', registerId: 'ds' },
|
|
59
|
+
});
|
|
60
|
+
expect(wrapper.text()).toContain('Figure 1');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('renders sources when present', async () => {
|
|
64
|
+
const rep = makeRep({
|
|
65
|
+
type: 'photo',
|
|
66
|
+
sources: [
|
|
67
|
+
{
|
|
68
|
+
type: 'authoritative',
|
|
69
|
+
origin: {
|
|
70
|
+
ref: { source: 'ISO 123' },
|
|
71
|
+
locality: { type: 'clause', referenceFrom: '1.2' },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
const router = makeRouter();
|
|
77
|
+
await router.push('/');
|
|
78
|
+
const wrapper = mount(NonVerbalRepDisplay, {
|
|
79
|
+
props: { reps: [rep], locale: 'eng', registerId: 'ds' },
|
|
80
|
+
global: { plugins: [router] },
|
|
81
|
+
});
|
|
82
|
+
expect(wrapper.text()).toContain('Sources');
|
|
83
|
+
expect(wrapper.text()).toContain('ISO 123');
|
|
84
|
+
});
|
|
85
|
+
});
|