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>
244 lines
9.1 KiB
JavaScript
244 lines
9.1 KiB
JavaScript
// @ts-nocheck
|
|
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
import { AVTileAdapter } from '../../../../src/ui/shared/AVTileAdapter.js';
|
|
import { createFoundryAdapterMock } from '../../../helpers/foundryAdapterMock.js';
|
|
|
|
describe('AVTileAdapter', () => {
|
|
let adapter;
|
|
let avAdapter;
|
|
|
|
beforeEach(() => {
|
|
document.body.innerHTML = `
|
|
<div class="camera-view" data-user-id="user-1"></div>
|
|
<div class="camera-view" data-user-id="user-2"></div>
|
|
`;
|
|
adapter = createFoundryAdapterMock();
|
|
avAdapter = new AVTileAdapter(adapter);
|
|
});
|
|
|
|
afterEach(() => {
|
|
avAdapter.disconnect();
|
|
});
|
|
|
|
describe('constructor', () => {
|
|
it('stores adapter reference without side effects', () => {
|
|
expect(avAdapter._adapter).toBe(adapter);
|
|
expect(avAdapter._observers).toBeInstanceOf(Map);
|
|
expect(avAdapter._observers.size).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('mount()', () => {
|
|
it('appends element to the matching AV tile', () => {
|
|
const el = document.createElement('div');
|
|
el.dataset.spRole = 'lock-overlay';
|
|
avAdapter.mount('user-1', el);
|
|
const tile = document.querySelector('[data-user-id="user-1"]');
|
|
expect(tile.contains(el)).toBe(true);
|
|
});
|
|
|
|
it('marks mounted element with data-sp-mount attribute', () => {
|
|
const el = document.createElement('div');
|
|
el.dataset.spRole = 'lock-overlay';
|
|
avAdapter.mount('user-1', el);
|
|
expect(el.dataset.spMount).toBe('1');
|
|
});
|
|
|
|
it('warns and does not throw when tile not found', () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
const el = document.createElement('div');
|
|
expect(() => avAdapter.mount('unknown-user', el)).not.toThrow();
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
'[ScryingPool] AVTileAdapter.mount: tile not found for',
|
|
'unknown-user'
|
|
);
|
|
warnSpy.mockRestore();
|
|
});
|
|
|
|
it('is idempotent: calling twice with same element does not duplicate', () => {
|
|
const el = document.createElement('div');
|
|
el.dataset.spRole = 'lock-overlay';
|
|
avAdapter.mount('user-1', el);
|
|
avAdapter.mount('user-1', el);
|
|
const tile = document.querySelector('[data-user-id="user-1"]');
|
|
expect(tile.querySelectorAll('[data-sp-role="lock-overlay"]').length).toBe(1);
|
|
});
|
|
|
|
it('replaces existing element with same data-sp-role', () => {
|
|
const el1 = document.createElement('div');
|
|
el1.dataset.spRole = 'lock-overlay';
|
|
el1.textContent = 'first';
|
|
avAdapter.mount('user-1', el1);
|
|
|
|
const el2 = document.createElement('div');
|
|
el2.dataset.spRole = 'lock-overlay';
|
|
el2.textContent = 'second';
|
|
avAdapter.mount('user-1', el2);
|
|
|
|
const tile = document.querySelector('[data-user-id="user-1"]');
|
|
const overlays = tile.querySelectorAll('[data-sp-role="lock-overlay"]');
|
|
expect(overlays.length).toBe(1);
|
|
expect(overlays[0].textContent).toBe('second');
|
|
});
|
|
|
|
it('appends multiple elements with different roles', () => {
|
|
const el1 = document.createElement('div');
|
|
el1.dataset.spRole = 'lock-overlay';
|
|
const el2 = document.createElement('div');
|
|
el2.dataset.spRole = 'portrait-fallback';
|
|
avAdapter.mount('user-1', el1);
|
|
avAdapter.mount('user-1', el2);
|
|
const tile = document.querySelector('[data-user-id="user-1"]');
|
|
expect(tile.querySelectorAll('[data-sp-mount]').length).toBe(2);
|
|
});
|
|
|
|
it('does not affect other tiles', () => {
|
|
const el = document.createElement('div');
|
|
el.dataset.spRole = 'lock-overlay';
|
|
avAdapter.mount('user-1', el);
|
|
const tile2 = document.querySelector('[data-user-id="user-2"]');
|
|
expect(tile2.querySelectorAll('[data-sp-mount]').length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('unmount()', () => {
|
|
it('removes all data-sp-mount children from tile', () => {
|
|
const el1 = document.createElement('div');
|
|
el1.dataset.spRole = 'lock-overlay';
|
|
const el2 = document.createElement('div');
|
|
el2.dataset.spRole = 'portrait-fallback';
|
|
avAdapter.mount('user-1', el1);
|
|
avAdapter.mount('user-1', el2);
|
|
avAdapter.unmount('user-1');
|
|
const tile = document.querySelector('[data-user-id="user-1"]');
|
|
expect(tile.querySelectorAll('[data-sp-mount]').length).toBe(0);
|
|
});
|
|
|
|
it('does not remove non-managed children', () => {
|
|
const native = document.createElement('video');
|
|
document.querySelector('[data-user-id="user-1"]').appendChild(native);
|
|
avAdapter.unmount('user-1');
|
|
const tile = document.querySelector('[data-user-id="user-1"]');
|
|
expect(tile.contains(native)).toBe(true);
|
|
});
|
|
|
|
it('is no-op if tile not found', () => {
|
|
expect(() => avAdapter.unmount('unknown-user')).not.toThrow();
|
|
});
|
|
|
|
it('does not affect other tiles', () => {
|
|
const el1 = document.createElement('div');
|
|
el1.dataset.spRole = 'lock-overlay';
|
|
const el2 = document.createElement('div');
|
|
el2.dataset.spRole = 'lock-overlay';
|
|
avAdapter.mount('user-1', el1);
|
|
avAdapter.mount('user-2', el2);
|
|
avAdapter.unmount('user-1');
|
|
const tile2 = document.querySelector('[data-user-id="user-2"]');
|
|
expect(tile2.querySelectorAll('[data-sp-mount]').length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('setStateClass()', () => {
|
|
it('adds sp-state-{stateName} class to tile', () => {
|
|
avAdapter.setStateClass('user-1', 'hidden');
|
|
const tile = document.querySelector('[data-user-id="user-1"]');
|
|
expect(tile.classList.contains('sp-state-hidden')).toBe(true);
|
|
});
|
|
|
|
it('removes all previous sp-state-* classes before adding new', () => {
|
|
const tile = document.querySelector('[data-user-id="user-1"]');
|
|
tile.classList.add('sp-state-active');
|
|
tile.classList.add('sp-state-pending');
|
|
avAdapter.setStateClass('user-1', 'hidden');
|
|
expect(tile.classList.contains('sp-state-active')).toBe(false);
|
|
expect(tile.classList.contains('sp-state-pending')).toBe(false);
|
|
expect(tile.classList.contains('sp-state-hidden')).toBe(true);
|
|
});
|
|
|
|
it('clears all sp-state-* classes when stateName is null', () => {
|
|
const tile = document.querySelector('[data-user-id="user-1"]');
|
|
tile.classList.add('sp-state-active');
|
|
avAdapter.setStateClass('user-1', null);
|
|
expect(tile.classList.contains('sp-state-active')).toBe(false);
|
|
});
|
|
|
|
it('warns and does not throw when tile not found', () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
expect(() => avAdapter.setStateClass('unknown-user', 'hidden')).not.toThrow();
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
'[ScryingPool] AVTileAdapter.setStateClass: tile not found for',
|
|
'unknown-user'
|
|
);
|
|
warnSpy.mockRestore();
|
|
});
|
|
|
|
it('does not affect non-sp-* classes on tile', () => {
|
|
const tile = document.querySelector('[data-user-id="user-1"]');
|
|
tile.classList.add('camera-view');
|
|
tile.classList.add('some-other-class');
|
|
avAdapter.setStateClass('user-1', 'hidden');
|
|
expect(tile.classList.contains('camera-view')).toBe(true);
|
|
expect(tile.classList.contains('some-other-class')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('onTileRerender()', () => {
|
|
it('calls callback when tile children change', async () => {
|
|
const cb = vi.fn();
|
|
avAdapter.onTileRerender('user-1', cb);
|
|
const tile = document.querySelector('[data-user-id="user-1"]');
|
|
tile.appendChild(document.createElement('span'));
|
|
await new Promise(resolve => setTimeout(resolve, 20));
|
|
expect(cb).toHaveBeenCalledWith(tile);
|
|
});
|
|
|
|
it('replaces existing observer when called again for same userId', async () => {
|
|
const cb1 = vi.fn();
|
|
const cb2 = vi.fn();
|
|
avAdapter.onTileRerender('user-1', cb1);
|
|
avAdapter.onTileRerender('user-1', cb2);
|
|
// Only one observer should be active per userId
|
|
expect(avAdapter._observers.size).toBe(1);
|
|
});
|
|
|
|
it('stores one observer per userId', () => {
|
|
avAdapter.onTileRerender('user-1', vi.fn());
|
|
avAdapter.onTileRerender('user-2', vi.fn());
|
|
expect(avAdapter._observers.size).toBe(2);
|
|
});
|
|
|
|
it('is no-op if tile not found', () => {
|
|
expect(() => avAdapter.onTileRerender('unknown-user', vi.fn())).not.toThrow();
|
|
expect(avAdapter._observers.size).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('disconnect()', () => {
|
|
it('stops observers from firing after disconnect', async () => {
|
|
const cb = vi.fn();
|
|
avAdapter.onTileRerender('user-1', cb);
|
|
avAdapter.disconnect();
|
|
const tile = document.querySelector('[data-user-id="user-1"]');
|
|
tile.appendChild(document.createElement('span'));
|
|
await new Promise(resolve => setTimeout(resolve, 20));
|
|
expect(cb).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('clears observer map', () => {
|
|
avAdapter.onTileRerender('user-1', vi.fn());
|
|
avAdapter.onTileRerender('user-2', vi.fn());
|
|
avAdapter.disconnect();
|
|
expect(avAdapter._observers.size).toBe(0);
|
|
});
|
|
|
|
it('is safe to call multiple times', () => {
|
|
avAdapter.onTileRerender('user-1', vi.fn());
|
|
expect(() => {
|
|
avAdapter.disconnect();
|
|
avAdapter.disconnect();
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
});
|