7918792f4e
- Delete src/ui/shared/ParticipantCard.js (duplicate of boardUtils.js with conflicting implementations) - Delete tests/unit/ui/shared/ParticipantCard.test.js (tests for deleted file) - Add directorsBoard to global declarations in ScryingPoolStrip.js to fix lint errors Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
594 lines
23 KiB
JavaScript
594 lines
23 KiB
JavaScript
// @ts-nocheck
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { VisibilityBadge } from '../../../../src/ui/player/VisibilityBadge.js';
|
|
import { createFoundryAdapterMock } from '../../../helpers/foundryAdapterMock.js';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeAdapter({ userId = 'user-player', isGM = false, firstBadgeEncountered = false } = {}) {
|
|
const mockUser = {
|
|
id: userId,
|
|
getFlag: vi.fn().mockReturnValue(firstBadgeEncountered),
|
|
setFlag: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
return createFoundryAdapterMock({
|
|
users: {
|
|
current: () => mockUser,
|
|
isGM: () => isGM,
|
|
get: () => mockUser,
|
|
all: () => [mockUser],
|
|
},
|
|
});
|
|
}
|
|
|
|
function makeAVTileAdapter() {
|
|
return {
|
|
mount: vi.fn(),
|
|
unmount: vi.fn(),
|
|
setStateClass: vi.fn(),
|
|
onTileRerender: vi.fn(),
|
|
disconnect: vi.fn(),
|
|
};
|
|
}
|
|
|
|
function makeStateStore(initialState = 'active') {
|
|
return {
|
|
getState: vi.fn().mockReturnValue(initialState),
|
|
};
|
|
}
|
|
|
|
function makeController() {
|
|
return {
|
|
hasPendingOp: vi.fn().mockReturnValue(false),
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// VisibilityBadge
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('VisibilityBadge', () => {
|
|
let adapter;
|
|
let avTileAdapter;
|
|
let stateStore;
|
|
let controller;
|
|
let hookHandlers;
|
|
|
|
beforeEach(() => {
|
|
document.body.innerHTML = `<div class="camera-view" data-user-id="user-player"></div>`;
|
|
hookHandlers = {};
|
|
vi.stubGlobal('Hooks', {
|
|
on: vi.fn((event, handler) => { hookHandlers[event] = handler; }),
|
|
once: vi.fn(),
|
|
off: vi.fn(),
|
|
callAll: vi.fn(),
|
|
});
|
|
adapter = makeAdapter();
|
|
avTileAdapter = makeAVTileAdapter();
|
|
stateStore = makeStateStore();
|
|
controller = makeController();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('stores deps without side effects', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
expect(badge._stateStore).toBe(stateStore);
|
|
expect(badge._controller).toBe(controller);
|
|
expect(badge._avTileAdapter).toBe(avTileAdapter);
|
|
expect(badge._adapter).toBe(adapter);
|
|
expect(Hooks.on).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('init()', () => {
|
|
it('resolves currentUserId from adapter.users.current()', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
expect(badge._currentUserId).toBe('user-player');
|
|
});
|
|
|
|
it('subscribes to scrying-pool:stateChanged hook', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
expect(Hooks.on).toHaveBeenCalledWith('scrying-pool:stateChanged', expect.any(Function));
|
|
});
|
|
|
|
it('mounts initial badge via avTileAdapter', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
expect(avTileAdapter.mount).toHaveBeenCalledWith('user-player', expect.any(HTMLElement));
|
|
});
|
|
|
|
it('registers onTileRerender callback', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
expect(avTileAdapter.onTileRerender).toHaveBeenCalledWith('user-player', expect.any(Function));
|
|
});
|
|
|
|
it('no-ops when no currentUserId (null user)', () => {
|
|
const noUserAdapter = createFoundryAdapterMock({
|
|
users: { current: () => null, isGM: () => false, get: () => null, all: () => [] },
|
|
});
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, noUserAdapter);
|
|
badge.init();
|
|
expect(badge._currentUserId).toBeNull();
|
|
expect(avTileAdapter.mount).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('_createBadgeElement()', () => {
|
|
it('creates element with correct class and data-sp-role', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
const el = badge._createBadgeElement('hidden');
|
|
expect(el.className).toBe('sp-visibility-badge');
|
|
expect(el.dataset.spRole).toBe('visibility-badge');
|
|
});
|
|
|
|
it('sets role="status" and aria-live="polite"', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
const el = badge._createBadgeElement('hidden');
|
|
expect(el.getAttribute('role')).toBe('status');
|
|
expect(el.getAttribute('aria-live')).toBe('polite');
|
|
});
|
|
|
|
it('sets aria-label with state label', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
const el = badge._createBadgeElement('hidden');
|
|
expect(el.getAttribute('aria-label')).toBe('Camera visibility: Hidden from table');
|
|
});
|
|
|
|
it('sets aria-label to "Active" for active state', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
const el = badge._createBadgeElement('active');
|
|
expect(el.getAttribute('aria-label')).toBe('Camera visibility: Active');
|
|
});
|
|
|
|
it('renders all state labels correctly', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
const cases = [
|
|
['hidden', 'Hidden from table'],
|
|
['self-muted', 'Camera paused'],
|
|
['offline', 'Not connected'],
|
|
['cam-lost', 'Camera unavailable'],
|
|
['reconnecting', 'Rejoining view'],
|
|
['never-connected', 'Not yet connected'],
|
|
['ghost', 'Leaving'],
|
|
];
|
|
for (const [state, expectedLabel] of cases) {
|
|
const el = badge._createBadgeElement(state);
|
|
const span = el.querySelector('.sp-visibility-badge__label');
|
|
expect(span?.textContent).toBe(expectedLabel);
|
|
}
|
|
});
|
|
|
|
it('renders no label text for active state', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
const el = badge._createBadgeElement('active');
|
|
const span = el.querySelector('.sp-visibility-badge__label');
|
|
expect(span?.textContent ?? '').toBe('');
|
|
});
|
|
});
|
|
|
|
describe('_onStateChanged()', () => {
|
|
it('ignores events for other users', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
avTileAdapter.mount.mockClear();
|
|
badge._onStateChanged({ userId: 'other-user', state: 'hidden' });
|
|
expect(avTileAdapter.mount).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('updates badge element and re-mounts for own user', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
avTileAdapter.mount.mockClear();
|
|
badge._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
expect(avTileAdapter.mount).toHaveBeenCalledWith('user-player', expect.any(HTMLElement));
|
|
});
|
|
|
|
it('updates badge aria-label on state change', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
badge._onStateChanged({ userId: 'user-player', state: 'offline' });
|
|
expect(badge._badgeEl.getAttribute('aria-label')).toBe('Camera visibility: Not connected');
|
|
});
|
|
|
|
it('triggers FirstEncounterPanel when firstBadgeEncountered is false', () => {
|
|
// firstBadgeEncountered = false (default makeAdapter)
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
badge._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
// Panel should be in the DOM
|
|
expect(document.querySelector('.sp-first-encounter-panel')).not.toBeNull();
|
|
});
|
|
|
|
it('does NOT trigger FirstEncounterPanel when already encountered', () => {
|
|
const encounteredAdapter = makeAdapter({ firstBadgeEncountered: true });
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, encounteredAdapter);
|
|
badge.init();
|
|
badge._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
expect(document.querySelector('.sp-first-encounter-panel')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('_getFirstBadgeEncountered()', () => {
|
|
it('returns flag value from adapter user', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
expect(badge._getFirstBadgeEncountered()).toBe(false);
|
|
});
|
|
|
|
it('returns true when flag is set', () => {
|
|
const encAdapter = makeAdapter({ firstBadgeEncountered: true });
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, encAdapter);
|
|
badge.init();
|
|
expect(badge._getFirstBadgeEncountered()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('_setFirstBadgeEncountered()', () => {
|
|
it('calls setFlag on current user', async () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
await badge._setFirstBadgeEncountered();
|
|
const mockUser = adapter.users.current();
|
|
expect(mockUser.setFlag).toHaveBeenCalledWith('video-view-manager', 'firstBadgeEncounter', true);
|
|
});
|
|
});
|
|
|
|
describe('badge click handler', () => {
|
|
it('opens VisibilityDetailsPanel on badge click', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
badge._badgeEl.click();
|
|
const dialog = document.querySelector('dialog.sp-visibility-details-panel');
|
|
expect(dialog).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('teardown()', () => {
|
|
it('calls avTileAdapter.disconnect()', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
badge.teardown();
|
|
expect(avTileAdapter.disconnect).toHaveBeenCalled();
|
|
});
|
|
|
|
it('unsubscribes from Hooks', () => {
|
|
const badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
badge.teardown();
|
|
expect(Hooks.off).toHaveBeenCalledWith('scrying-pool:stateChanged', expect.any(Function));
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// FirstEncounterPanel
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('FirstEncounterPanel (via VisibilityBadge)', () => {
|
|
let adapter;
|
|
let avTileAdapter;
|
|
let stateStore;
|
|
let controller;
|
|
let badge;
|
|
|
|
beforeEach(() => {
|
|
document.body.innerHTML = `<div class="camera-view" data-user-id="user-player"></div>`;
|
|
vi.stubGlobal('Hooks', {
|
|
on: vi.fn(),
|
|
once: vi.fn(),
|
|
off: vi.fn(),
|
|
callAll: vi.fn(),
|
|
});
|
|
adapter = makeAdapter({ firstBadgeEncountered: false });
|
|
avTileAdapter = makeAVTileAdapter();
|
|
stateStore = makeStateStore();
|
|
controller = makeController();
|
|
badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
// Trigger a state change to show the panel
|
|
badge._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe('show()', () => {
|
|
it('appends panel to DOM with role="dialog"', () => {
|
|
const panel = document.querySelector('.sp-first-encounter-panel');
|
|
expect(panel).not.toBeNull();
|
|
expect(panel.getAttribute('role')).toBe('dialog');
|
|
});
|
|
|
|
it('sets aria-modal="false"', () => {
|
|
const panel = document.querySelector('.sp-first-encounter-panel');
|
|
expect(panel.getAttribute('aria-modal')).toBe('false');
|
|
});
|
|
|
|
it('contains a "Got it" button', () => {
|
|
const btn = document.querySelector('.sp-first-encounter-panel [data-action="got-it"]');
|
|
expect(btn).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('10s auto-collapse timer', () => {
|
|
it('collapses panel after 10s and shows chip', () => {
|
|
vi.useFakeTimers();
|
|
// Re-trigger with fake timers
|
|
document.body.innerHTML = `<div class="camera-view" data-user-id="user-player"></div>`;
|
|
const b = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
b.init();
|
|
b._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
expect(document.querySelector('.sp-first-encounter-panel')).not.toBeNull();
|
|
vi.advanceTimersByTime(10_001); // fires collapse timer
|
|
vi.advanceTimersByTime(301); // fires 300ms transition timer
|
|
expect(document.querySelector('.sp-first-encounter-panel')).toBeNull();
|
|
expect(document.querySelector('.sp-visibility-chip')).not.toBeNull();
|
|
});
|
|
|
|
it('pauses timer on mouseenter and resumes on mouseleave', () => {
|
|
vi.useFakeTimers();
|
|
document.body.innerHTML = `<div class="camera-view" data-user-id="user-player"></div>`;
|
|
const b = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
b.init();
|
|
b._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
const panel = document.querySelector('.sp-first-encounter-panel');
|
|
panel.dispatchEvent(new Event('mouseenter'));
|
|
vi.advanceTimersByTime(10_001);
|
|
// Should NOT have collapsed because timer was paused
|
|
expect(document.querySelector('.sp-first-encounter-panel')).not.toBeNull();
|
|
panel.dispatchEvent(new Event('mouseleave'));
|
|
vi.advanceTimersByTime(10_001);
|
|
vi.advanceTimersByTime(301);
|
|
expect(document.querySelector('.sp-first-encounter-panel')).toBeNull();
|
|
});
|
|
|
|
it('pauses timer on focusin and resumes on focusout', () => {
|
|
vi.useFakeTimers();
|
|
document.body.innerHTML = `<div class="camera-view" data-user-id="user-player"></div>`;
|
|
const b = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
b.init();
|
|
b._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
const panel = document.querySelector('.sp-first-encounter-panel');
|
|
panel.dispatchEvent(new Event('focusin'));
|
|
vi.advanceTimersByTime(10_001);
|
|
expect(document.querySelector('.sp-first-encounter-panel')).not.toBeNull();
|
|
panel.dispatchEvent(new Event('focusout'));
|
|
vi.advanceTimersByTime(10_001);
|
|
vi.advanceTimersByTime(301);
|
|
expect(document.querySelector('.sp-first-encounter-panel')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('"Got it" button', () => {
|
|
it('dismisses panel from DOM', async () => {
|
|
document.body.innerHTML = `<div class="camera-view" data-user-id="user-player"></div>`;
|
|
const b = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
b.init();
|
|
b._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
const panel = b._firstEncounterPanel;
|
|
// Directly call _onGotIt to avoid async click handler timing issues
|
|
await panel._onGotIt();
|
|
expect(document.querySelector('.sp-first-encounter-panel')).toBeNull();
|
|
});
|
|
|
|
it('calls setFirstBadgeEncountered', async () => {
|
|
const b = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
b.init();
|
|
b._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
const panel = b._firstEncounterPanel;
|
|
// Directly call _onGotIt to avoid async click handler timing issues
|
|
await panel._onGotIt();
|
|
const mockUser = adapter.users.current();
|
|
expect(mockUser.setFlag).toHaveBeenCalledWith('video-view-manager', 'firstBadgeEncounter', true);
|
|
});
|
|
|
|
it('clears timer (no ghost timer after dismissal)', async () => {
|
|
vi.useFakeTimers();
|
|
document.body.innerHTML = `<div class="camera-view" data-user-id="user-player"></div>`;
|
|
const b = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
b.init();
|
|
b._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
const panel = b._firstEncounterPanel;
|
|
// Directly call _onGotIt to avoid async click handler timing issues
|
|
await panel._onGotIt();
|
|
// After dismissal, advancing time should NOT cause errors or chip to appear
|
|
expect(() => vi.advanceTimersByTime(15_000)).not.toThrow();
|
|
expect(document.querySelector('.sp-visibility-chip')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('chip (collapsed state)', () => {
|
|
it('chip is focusable with role="button"', () => {
|
|
vi.useFakeTimers();
|
|
document.body.innerHTML = `<div class="camera-view" data-user-id="user-player"></div>`;
|
|
const b = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
b.init();
|
|
b._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
vi.advanceTimersByTime(10_001);
|
|
vi.advanceTimersByTime(301);
|
|
const chip = document.querySelector('.sp-visibility-chip');
|
|
expect(chip.getAttribute('role')).toBe('button');
|
|
expect(chip.getAttribute('tabindex')).toBe('0');
|
|
});
|
|
|
|
it('chip click opens VisibilityDetailsPanel', () => {
|
|
vi.useFakeTimers();
|
|
document.body.innerHTML = `<div class="camera-view" data-user-id="user-player"></div>`;
|
|
const b = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
b.init();
|
|
b._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
vi.advanceTimersByTime(10_001);
|
|
vi.advanceTimersByTime(301);
|
|
const chip = document.querySelector('.sp-visibility-chip');
|
|
chip.click();
|
|
expect(document.querySelector('dialog.sp-visibility-details-panel')).not.toBeNull();
|
|
});
|
|
|
|
it('chip Enter keydown opens VisibilityDetailsPanel', () => {
|
|
vi.useFakeTimers();
|
|
document.body.innerHTML = `<div class="camera-view" data-user-id="user-player"></div>`;
|
|
const b = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
b.init();
|
|
b._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
vi.advanceTimersByTime(10_001);
|
|
vi.advanceTimersByTime(301);
|
|
const chip = document.querySelector('.sp-visibility-chip');
|
|
chip.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
expect(document.querySelector('dialog.sp-visibility-details-panel')).not.toBeNull();
|
|
});
|
|
|
|
it('chip Space keydown opens VisibilityDetailsPanel', () => {
|
|
vi.useFakeTimers();
|
|
document.body.innerHTML = `<div class="camera-view" data-user-id="user-player"></div>`;
|
|
const b = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
b.init();
|
|
b._onStateChanged({ userId: 'user-player', state: 'hidden' });
|
|
vi.advanceTimersByTime(10_001);
|
|
vi.advanceTimersByTime(301);
|
|
const chip = document.querySelector('.sp-visibility-chip');
|
|
chip.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));
|
|
expect(document.querySelector('dialog.sp-visibility-details-panel')).not.toBeNull();
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// VisibilityDetailsPanel
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('VisibilityDetailsPanel (via VisibilityBadge)', () => {
|
|
let adapter;
|
|
let avTileAdapter;
|
|
let stateStore;
|
|
let controller;
|
|
let badge;
|
|
|
|
beforeEach(() => {
|
|
document.body.innerHTML = `<div class="camera-view" data-user-id="user-player"></div>`;
|
|
vi.stubGlobal('Hooks', {
|
|
on: vi.fn(),
|
|
once: vi.fn(),
|
|
off: vi.fn(),
|
|
callAll: vi.fn(),
|
|
});
|
|
adapter = makeAdapter({ firstBadgeEncountered: true });
|
|
avTileAdapter = makeAVTileAdapter();
|
|
stateStore = makeStateStore('hidden');
|
|
controller = makeController();
|
|
badge = new VisibilityBadge(stateStore, controller, avTileAdapter, adapter);
|
|
badge.init();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
// Clean up any open dialogs
|
|
document.querySelectorAll('dialog').forEach(d => d.remove());
|
|
});
|
|
|
|
describe('show()', () => {
|
|
it('creates a <dialog> element with correct class', () => {
|
|
badge._badgeEl.click();
|
|
const dialog = document.querySelector('dialog.sp-visibility-details-panel');
|
|
expect(dialog).not.toBeNull();
|
|
});
|
|
|
|
it('sets aria-modal="true"', () => {
|
|
badge._badgeEl.click();
|
|
const dialog = document.querySelector('dialog');
|
|
expect(dialog.getAttribute('aria-modal')).toBe('true');
|
|
});
|
|
|
|
it('appends dialog to document.body', () => {
|
|
badge._badgeEl.click();
|
|
const dialog = document.querySelector('dialog');
|
|
expect(dialog.parentNode).toBe(document.body);
|
|
});
|
|
|
|
it('calls showModal()', () => {
|
|
const showModalSpy = vi.spyOn(HTMLDialogElement.prototype, 'showModal').mockImplementation(() => {});
|
|
badge._badgeEl.click();
|
|
expect(showModalSpy).toHaveBeenCalled();
|
|
showModalSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('content', () => {
|
|
it('contains a "Close" button', () => {
|
|
badge._badgeEl.click();
|
|
const btn = document.querySelector('dialog [data-action="close-details"]');
|
|
expect(btn).not.toBeNull();
|
|
});
|
|
|
|
it('shows state explanation text', () => {
|
|
badge._badgeEl.click();
|
|
const dialog = document.querySelector('dialog');
|
|
expect(dialog.textContent).toContain('Hidden from table');
|
|
});
|
|
|
|
it('suppresses audience list and shows reassurance when state is hidden', () => {
|
|
badge._badgeEl.click();
|
|
const dialog = document.querySelector('dialog');
|
|
expect(dialog.textContent).toContain('Other players cannot see your feed');
|
|
});
|
|
|
|
it('shows stale data indicator when controller is null', () => {
|
|
const b = new VisibilityBadge(stateStore, null, avTileAdapter, adapter);
|
|
b.init();
|
|
b._badgeEl.click();
|
|
const dialog = document.querySelector('dialog');
|
|
expect(dialog.textContent).toContain('Data may be outdated');
|
|
});
|
|
});
|
|
|
|
describe('close handlers', () => {
|
|
it('"Close" button closes the dialog', () => {
|
|
const closeSpy = vi.spyOn(HTMLDialogElement.prototype, 'close').mockImplementation(function () {
|
|
this.dispatchEvent(new Event('close'));
|
|
});
|
|
badge._badgeEl.click();
|
|
const btn = document.querySelector('[data-action="close-details"]');
|
|
btn.click();
|
|
expect(closeSpy).toHaveBeenCalled();
|
|
closeSpy.mockRestore();
|
|
});
|
|
|
|
it('backdrop click closes dialog', () => {
|
|
const closeSpy = vi.spyOn(HTMLDialogElement.prototype, 'close').mockImplementation(function () {
|
|
this.dispatchEvent(new Event('close'));
|
|
});
|
|
badge._badgeEl.click();
|
|
const dialog = document.querySelector('dialog');
|
|
dialog.dispatchEvent(new MouseEvent('click', { bubbles: false }));
|
|
// event.target === dialog triggers close
|
|
expect(closeSpy).toHaveBeenCalled();
|
|
closeSpy.mockRestore();
|
|
});
|
|
|
|
it('removes dialog from DOM on close', () => {
|
|
badge._badgeEl.click();
|
|
const dialog = document.querySelector('dialog');
|
|
dialog.dispatchEvent(new Event('close'));
|
|
expect(document.querySelector('dialog')).toBeNull();
|
|
});
|
|
|
|
it('returns focus to trigger element on close', () => {
|
|
const focusSpy = vi.spyOn(badge._badgeEl, 'focus');
|
|
badge._badgeEl.click();
|
|
const dialog = document.querySelector('dialog');
|
|
dialog.dispatchEvent(new Event('close'));
|
|
expect(focusSpy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|