// @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 = `
`; 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(); }); }); });