import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { createRouter, createMemoryHistory } from 'vue-router';
import ConceptDetail from '../components/ConceptDetail.vue';
import { useVocabularyStore } from '../stores/vocabulary';
import type { Manifest } from '../adapters/types';
import { conceptFromJson } from '../adapters/model-bridge';
// Prevent the 2.7MB Opal runtime from loading in tests
vi.mock('../utils/plurimath', () => ({
loadPlurimath: () => new Promise(() => {}),
mathToHtml: () => 'x',
renderToMathML: () => null,
}));
import { vMath } from '../directives/v-math';
function makeManifest(): Manifest {
return {
id: 'test',
datasetUri: 'https://glossarist.org/test/concept',
title: 'Test Dataset',
description: 'A test dataset',
owner: 'ISO',
baseUrl: '/data/test',
languages: ['eng', 'fra'],
conceptCount: 1,
conceptUrlTemplate: '/data/test/concepts/{id}.json',
indexUrl: '/data/test/index.json',
contextUrl: '/data/test/context.json',
uriBase: 'https://glossarist.org',
status: 'published',
schemaVersion: '1.0',
tags: [],
lastUpdated: '2025-01-01',
sourceRepo: 'https://example.com/repo',
chunkSize: 1000,
languageOrder: ['eng', 'fra'],
color: '#3366ff',
};
}
function makeConceptJson(overrides: Record = {}) {
return {
'@context': 'https://glossarist.org/context',
'@id': 'https://glossarist.org/test/concept/1',
'@type': 'gl:Concept',
'gl:identifier': '1',
'gl:localizedConcept': {
eng: {
'@id': 'https://glossarist.org/test/concept/1/eng',
'@type': 'gl:LocalizedConcept',
'gl:languageCode': 'eng',
'gl:entryStatus': 'valid',
'gl:designation': [
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': 'test term' },
{ '@type': 'gl:Symbol', 'gl:normativeStatus': 'admitted', 'gl:term': 'stem:[x]' },
],
'gl:definition': [
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'a definition with *italic* text' },
],
'gl:notes': [
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'a note' },
],
'gl:examples': [
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'an example' },
],
'gl:source': [{ '@type': 'gl:Source', 'gl:sourceType': 'authoritative' }],
},
fra: {
'@id': 'https://glossarist.org/test/concept/1/fra',
'@type': 'gl:LocalizedConcept',
'gl:languageCode': 'fra',
'gl:entryStatus': 'valid',
'gl:designation': [
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': 'terme de test' },
],
'gl:definition': [
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'une définition' },
],
},
},
...overrides,
};
}
async function createTestRouter() {
return createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', name: 'home', component: { template: '' } },
{ path: '/dataset/:registerId', name: 'dataset', component: { template: '' } },
{ path: '/dataset/:registerId/concept/:conceptId', name: 'concept', component: { template: '' } },
],
});
}
describe('ConceptDetail interactions', () => {
let pinia: ReturnType;
let router: Awaited>;
let store: ReturnType;
beforeEach(async () => {
pinia = createPinia();
setActivePinia(pinia);
router = await createTestRouter();
router.push('/');
await router.isReady();
store = useVocabularyStore();
store.manifests.set('test', makeManifest());
store.datasets.set('test', { index: [], getConceptCount: () => 0, getConcepts: () => [], getConceptPosition: () => -1, getIndexEntry: () => undefined } as any);
});
function mountDetail(conceptJson: Record = makeConceptJson()) {
const concept = conceptFromJson(conceptJson);
return mount(ConceptDetail, {
global: {
plugins: [pinia, router],
directives: { math: vMath },
},
props: {
concept,
manifest: makeManifest(),
edges: [],
registerId: 'test',
adjacent: { prev: null, next: null },
},
});
}
it('renders the primary term in the header', () => {
const wrapper = mountDetail();
expect(wrapper.find('h1').html()).toContain('test term');
});
async function switchToDefinition(wrapper: ReturnType) {
const tabs = wrapper.findAll('button[role="tab"]');
const defTab = tabs.find(t => t.text().includes('Definition'));
if (defTab) {
await defTab.trigger('click');
await flushPromises();
}
}
it('renders concept ID badge', () => {
const wrapper = mountDetail();
expect(wrapper.text()).toContain('1');
});
it('renders language sections for eng and fra', async () => {
const wrapper = mountDetail();
await switchToDefinition(wrapper);
expect(wrapper.text()).toContain('English');
expect(wrapper.text()).toContain('French');
});
it('renders italic text in definition', async () => {
const wrapper = mountDetail();
await switchToDefinition(wrapper);
expect(wrapper.html()).toContain('italic');
});
it('renders stem: notation as math-pending placeholder', async () => {
const wrapper = mountDetail();
await switchToDefinition(wrapper);
expect(wrapper.html()).toContain('math-pending');
expect(wrapper.html()).toContain('data-expr="x"');
});
it('renders notes section', async () => {
const wrapper = mountDetail();
await switchToDefinition(wrapper);
expect(wrapper.text()).toContain('Note 1');
expect(wrapper.text()).toContain('a note');
});
it('renders examples section', async () => {
const wrapper = mountDetail();
await switchToDefinition(wrapper);
expect(wrapper.text()).toContain('Example 1');
expect(wrapper.text()).toContain('an example');
});
it('renders scoped examples nested inside a note', async () => {
const json = makeConceptJson() as Record;
json['gl:localizedConcept'].eng['gl:notes'] = [
{
'@type': 'gl:DetailedDefinition',
'gl:content': 'resistance depends on dimensions and material',
'gl:examples': [
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'copper resistivity ≈ 1.68e-8 Ω·m at 20 °C' },
{ '@type': 'gl:DetailedDefinition', 'gl:content': '1 m of 1 mm² copper wire ≈ 0.017 Ω' },
],
},
];
const wrapper = mountDetail(json);
await switchToDefinition(wrapper);
expect(wrapper.text()).toContain('resistance depends');
expect(wrapper.text()).toContain('Example 1');
expect(wrapper.text()).toContain('Example 2');
expect(wrapper.text()).toContain('copper resistivity');
expect(wrapper.text()).toContain('0.017 Ω');
});
it('renders designation types as badges', async () => {
const wrapper = mountDetail();
await switchToDefinition(wrapper);
expect(wrapper.text()).toContain('symbol');
});
it('collapses non-eng languages when 6+ languages present', async () => {
const json = makeConceptJson() as Record;
for (const lang of ['deu', 'spa', 'kor', 'jpn']) {
json['gl:localizedConcept'][lang] = {
'@id': `https://glossarist.org/test/concept/1/${lang}`,
'@type': 'gl:LocalizedConcept',
'gl:languageCode': lang,
'gl:designation': [
{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': `term-${lang}` },
],
'gl:definition': [
{ '@type': 'gl:DetailedDefinition', 'gl:content': `def-${lang}` },
],
};
}
const wrapper = mountDetail(json);
await switchToDefinition(wrapper);
await flushPromises();
expect(wrapper.text()).toContain('6 languages');
});
it('toggles language section on click', async () => {
const wrapper = mountDetail();
await switchToDefinition(wrapper);
const buttons = wrapper.findAll('button');
const fraButton = buttons.find(b => b.text().includes('French'));
expect(fraButton).toBeDefined();
await fraButton!.trigger('click');
await fraButton!.trigger('click');
});
it('switches between definition and history tabs', async () => {
const wrapper = mountDetail();
await switchToDefinition(wrapper);
expect(wrapper.text()).toContain('a definition with');
const tabs = wrapper.findAll('button[role="tab"]');
const historyTab = tabs.find(t => t.text().includes('History'));
expect(historyTab).toBeDefined();
await historyTab!.trigger('click');
await flushPromises();
expect(wrapper.text()).not.toContain('a definition with');
});
it('renders cross-reference link and navigates on click', async () => {
const json = makeConceptJson();
json['gl:localizedConcept'].eng['gl:definition'] = [
{ '@type': 'gl:DetailedDefinition', 'gl:content': 'see {{urn:iso:std:iso:14812:3.1.1.1,entity}} here' },
];
// Register the URI pattern via factory so it resolves as internal
const { getFactory } = await import('../adapters/factory');
const factory = getFactory();
factory.uriRouter.registerDataset('test', '', '', ['https://glossarist.org/test/concept/*']);
const wrapper = mountDetail(json);
await switchToDefinition(wrapper);
await flushPromises();
const xref = wrapper.find('.xref-link');
if (xref.exists()) {
await xref.trigger('click');
await flushPromises();
expect(router.currentRoute.value.name).toBe('concept');
}
});
it('renders entry status badge', () => {
const wrapper = mountDetail();
expect(wrapper.text()).toContain('valid');
});
it('renders the language quick-jump sidebar with all languages', async () => {
const wrapper = mountDetail();
await switchToDefinition(wrapper);
expect(wrapper.text()).toContain('Languages (2)');
});
});