import { test } from 'node:test'; import assert from 'node:assert/strict'; import { configureCreditKeyboardAccess } from './creditKeyboard.js'; import { setLocale } from './i18n/index.js'; function fakeElement() { const attributes = new Map(); const listeners = new Map(); return { dataset: {}, focused: false, id: '', setAttribute(name, value) { attributes.set(name, String(value)); }, getAttribute(name) { return attributes.get(name) ?? null; }, addEventListener(type, listener) { if (!listeners.has(type)) listeners.set(type, []); listeners.get(type).push(listener); }, dispatch(type, event = {}) { const dispatched = { currentTarget: this, target: this, defaultPrevented: false, propagationStopped: false, preventDefault() { this.defaultPrevented = true; }, stopPropagation() { this.propagationStopped = true; }, ...event, }; for (const listener of listeners.get(type) || []) listener(dispatched); return dispatched; }, click() { this.onclick?.(); this.dispatch('click'); }, focus() { this.focused = true; }, listenerCount(type) { return listeners.get(type)?.length || 0; }, }; } function fixture() { const expand = fakeElement(); const close = fakeElement(); const lightbox = fakeElement(); lightbox.querySelector = () => close; lightbox.contains = (target) => target === lightbox || target === close; const overlay = fakeElement(); lightbox.parentElement = overlay; const root = { querySelector(selector) { if (selector === '#cesium-credits .cesium-credit-expand-link') return expand; if (selector === '.cesium-credit-lightbox') return lightbox; return null; }, }; return { close, expand, lightbox, overlay, root }; } test('Cesium Data attribution and close controls join the keyboard tab order', () => { const f = fixture(); assert.equal(configureCreditKeyboardAccess(f.root), true); assert.equal(f.expand.getAttribute('role'), 'button'); assert.equal(f.expand.getAttribute('tabindex'), '0'); assert.equal(f.expand.getAttribute('aria-haspopup'), 'dialog'); assert.equal(f.expand.getAttribute('aria-expanded'), 'false'); assert.equal(f.close.getAttribute('role'), 'button'); assert.equal(f.close.getAttribute('tabindex'), '0'); assert.equal(f.close.getAttribute('aria-label'), 'Close data attribution'); }); test('Enter opens and closes attribution on initial keydown', () => { const f = fixture(); let opened = 0; let closed = 0; f.expand.onclick = () => { opened++; }; f.close.onclick = () => { closed++; }; configureCreditKeyboardAccess(f.root); const openEvent = f.expand.dispatch('keydown', { key: 'Enter' }); assert.equal(openEvent.defaultPrevented, true); assert.equal(opened, 1); assert.equal(f.expand.getAttribute('aria-expanded'), 'true'); assert.equal(f.close.focused, true); const closeEvent = f.close.dispatch('keydown', { key: 'Enter' }); assert.equal(closeEvent.defaultPrevented, true); assert.equal(closed, 1); assert.equal(f.expand.getAttribute('aria-expanded'), 'false'); assert.equal(f.expand.focused, true); }); test('Space opens and closes attribution only on key release', () => { const f = fixture(); let opened = 0; let closed = 0; f.expand.onclick = () => { opened++; }; f.close.onclick = () => { closed++; }; configureCreditKeyboardAccess(f.root); const openDown = f.expand.dispatch('keydown', { key: ' ' }); assert.equal(openDown.defaultPrevented, true); assert.equal(opened, 0); assert.equal(f.expand.getAttribute('aria-expanded'), 'false'); const openUp = f.expand.dispatch('keyup', { key: ' ' }); assert.equal(openUp.defaultPrevented, true); assert.equal(opened, 1); assert.equal(f.expand.getAttribute('aria-expanded'), 'true'); assert.equal(f.close.focused, true); const closeDown = f.close.dispatch('keydown', { key: ' ' }); assert.equal(closeDown.defaultPrevented, true); assert.equal(closed, 0); const closeUp = f.close.dispatch('keyup', { key: ' ' }); assert.equal(closeUp.defaultPrevented, true); assert.equal(closed, 1); assert.equal(f.expand.getAttribute('aria-expanded'), 'false'); assert.equal(f.expand.focused, true); }); test('held activation keys do not repeat open or close actions', () => { const f = fixture(); let opened = 0; f.expand.onclick = () => { opened++; }; configureCreditKeyboardAccess(f.root); f.expand.dispatch('keydown', { key: ' ' }); const repeated = f.expand.dispatch('keydown', { key: ' ', repeat: true }); assert.equal(repeated.defaultPrevented, true); assert.equal(opened, 0); f.expand.dispatch('keyup', { key: ' ' }); assert.equal(opened, 1); f.expand.dispatch('keyup', { key: ' ' }); assert.equal(opened, 1); const g = fixture(); let enterOpened = 0; g.expand.onclick = () => { enterOpened++; }; configureCreditKeyboardAccess(g.root); g.expand.dispatch('keydown', { key: 'Enter' }); g.expand.dispatch('keydown', { key: 'Enter', repeat: true }); assert.equal(enterOpened, 1); }); test('losing focus during a Space hold cancels release activation', () => { const f = fixture(); let opened = 0; f.expand.onclick = () => { opened++; }; configureCreditKeyboardAccess(f.root); f.expand.dispatch('keydown', { key: ' ' }); f.expand.dispatch('blur'); const release = f.expand.dispatch('keyup', { key: ' ' }); assert.equal(release.defaultPrevented, true); assert.equal(opened, 0); assert.equal(f.expand.getAttribute('aria-expanded'), 'false'); }); test('Escape and backdrop dismissal close attribution and restore its disclosure', () => { const f = fixture(); let closed = 0; f.close.onclick = () => { closed++; }; configureCreditKeyboardAccess(f.root); f.expand.click(); const escape = f.lightbox.dispatch('keydown', { key: 'Escape', target: f.close }); assert.equal(escape.defaultPrevented, true); assert.equal(escape.propagationStopped, true); assert.equal(closed, 1); assert.equal(f.expand.getAttribute('aria-expanded'), 'false'); assert.equal(f.expand.focused, true); f.expand.focused = false; f.expand.click(); f.overlay.dispatch('click', { target: f.overlay }); assert.equal(f.expand.getAttribute('aria-expanded'), 'false'); assert.equal(f.expand.focused, true); }); test('credit keyboard setup is idempotent and safely declines incomplete markup', () => { const f = fixture(); assert.equal(configureCreditKeyboardAccess(f.root), true); assert.equal(configureCreditKeyboardAccess(f.root), true); assert.equal(f.expand.listenerCount('keydown'), 1); assert.equal(f.expand.listenerCount('keyup'), 1); assert.equal(f.expand.listenerCount('blur'), 1); assert.equal(f.close.listenerCount('keydown'), 1); assert.equal(f.close.listenerCount('keyup'), 1); assert.equal(f.close.listenerCount('blur'), 1); assert.equal(f.lightbox.listenerCount('keydown'), 1); assert.equal(f.overlay.listenerCount('click'), 1); assert.equal(configureCreditKeyboardAccess({ querySelector: () => null }), false); }); function textFixture() { const f = fixture(); const title = fakeElement(); title.textContent = 'Data provided by:'; f.expand.textContent = 'Data attribution'; f.lightbox.querySelector = (selector) => selector === '.cesium-credit-lightbox-title' ? title : f.close; return { ...f, title }; } test('English keeps Cesium attribution texts', () => { setLocale('en'); const f = textFixture(); configureCreditKeyboardAccess(f.root); assert.equal(f.expand.textContent, 'Data attribution'); assert.equal(f.title.textContent, 'Data provided by:'); }); for (const [locale, link, title] of [ ['ru', 'Источники данных', 'Данные предоставлены:'], ['es', 'Atribución de datos', 'Datos proporcionados por:'], ]) test(`${locale}: Cesium attribution link and lightbox title are translated`, () => { setLocale(locale); try { const f = textFixture(); configureCreditKeyboardAccess(f.root); assert.equal(f.expand.textContent, link); assert.equal(f.title.textContent, title); } finally { setLocale('en'); } });