// FE-MAP-GHOST-001 to 004: the assistant's suggested places as map pins, built once for both renderers.
import { describe, expect, it, vi } from 'vitest';
import { GHOST_HOT_CLASS, buildGhostElement, mountGhosts, setGhostHot } from './assistantGhostMarker';
const GHOST = { id: 'c1', lat: 41.9, lng: 12.48, name: 'Pantheon', color: '#ff0000', icon: 'Landmark' };
describe('assistantGhostMarker', () => {
it('FE-MAP-GHOST-001: a name never becomes markup, and the colour goes in as a CSS variable', () => {
const el = buildGhostElement({ ...GHOST, name: '
' });
expect(el.querySelector('img')).toBeNull();
expect(el.getAttribute('aria-label')).toBe('
');
expect(el.style.getPropertyValue('--ghost-color')).toBe('#ff0000');
expect(el.querySelector('.trek-ghost-face svg')).not.toBeNull();
});
it('FE-MAP-GHOST-002: the pointer reports the hover, a click or Enter reports the ghost without reaching the map', () => {
const onHover = vi.fn();
const onClick = vi.fn();
const removed = vi.fn();
const map = document.createElement('div');
const mapClick = vi.fn();
map.addEventListener('click', mapClick);
const mounted = mountGhosts(
[GHOST],
(el) => {
map.appendChild(el);
return { remove: removed };
},
{ current: { onHover, onClick } }
);
const el = mounted.elements.get('c1');
el?.dispatchEvent(new Event('pointerenter'));
el?.dispatchEvent(new Event('pointerleave'));
el?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
el?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
expect(onHover.mock.calls).toEqual([['c1'], [null]]);
expect(onClick).toHaveBeenCalledTimes(2);
expect(mapClick).not.toHaveBeenCalled();
mounted.dispose();
expect(removed).toHaveBeenCalledOnce();
expect(mounted.elements.size).toBe(0);
});
it('FE-MAP-GHOST-003: only the hovered ghost is marked', () => {
const a = document.createElement('div');
const b = document.createElement('div');
const elements = new Map([
['a', a],
['b', b],
]);
setGhostHot(elements, 'b');
expect([a.classList.contains(GHOST_HOT_CLASS), b.classList.contains(GHOST_HOT_CLASS)]).toEqual([false, true]);
setGhostHot(elements, null);
expect(b.classList.contains(GHOST_HOT_CLASS)).toBe(false);
});
it('FE-MAP-GHOST-004: a spot a card marks is a dot without an icon, and every ghost of one change lights by its call', () => {
const point = buildGhostElement({ id: 'c1:via:0', ref: 'c1', lat: 41.9, lng: 12.5, name: 'Via', variant: 'point' });
expect(point.classList.contains('trek-ghost--point')).toBe(true);
expect(point.querySelector('.trek-ghost-face svg')).toBeNull();
const onHover = vi.fn();
const onClick = vi.fn();
const ghosts = [
{ id: 'c1:stop:0', ref: 'c1', lat: 41.9, lng: 12.5, name: 'Stop', variant: 'pin' as const },
{ id: 'c1:via:0', ref: 'c1', lat: 41.91, lng: 12.5, name: 'Via', variant: 'point' as const },
{ id: 'c2', lat: 41.92, lng: 12.5, name: 'Other' },
];
const mounted = mountGhosts(ghosts, () => ({ remove: vi.fn() }), { current: { onHover, onClick } });
mounted.elements.get('c1:via:0')?.dispatchEvent(new Event('pointerenter'));
mounted.elements.get('c1:stop:0')?.dispatchEvent(new MouseEvent('click'));
expect(onHover).toHaveBeenCalledWith('c1');
expect(onClick).toHaveBeenCalledWith('c1');
setGhostHot(mounted.elements, 'c1');
const hot = [...mounted.elements.values()].map((el) => el.classList.contains(GHOST_HOT_CLASS));
expect(hot).toEqual([true, true, false]);
mounted.dispose();
});
});