// @ts-nocheck import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { createFoundryAdapterMock } from '../../helpers/foundryAdapterMock.js'; // Mock ScryingPoolStrip before it's imported so Application global isn't needed vi.mock('../../../src/ui/gm/ScryingPoolStrip.js', () => ({ ScryingPoolStrip: vi.fn().mockImplementation(() => ({ render: vi.fn().mockResolvedValue(undefined), close: vi.fn(), rendered: false, })), })); import { RoleRenderer } from '../../../src/ui/RoleRenderer.js'; function makeAVTileAdapter() { return { mount: vi.fn(), unmount: vi.fn(), setStateClass: vi.fn(), disconnect: vi.fn(), onTileRerender: vi.fn(), }; } function makeStateStore() { const states = new Map(); return { getState: vi.fn(userId => states.get(userId) ?? 'active'), _states: states, }; } function makeController() { return { action: vi.fn(), getRevision: vi.fn(() => 0), hasPendingOp: vi.fn(() => false), }; } describe('RoleRenderer', () => { let adapter; let avTileAdapter; let stateStore; let controller; let renderer; let hooksStub; beforeEach(() => { hooksStub = { on: vi.fn(), off: vi.fn(), once: vi.fn(), callAll: vi.fn() }; vi.stubGlobal('Hooks', hooksStub); vi.stubGlobal('game', { webrtc: {}, user: { setFlag: vi.fn(), getFlag: vi.fn(() => null) } }); adapter = createFoundryAdapterMock(); avTileAdapter = makeAVTileAdapter(); stateStore = makeStateStore(); controller = makeController(); renderer = new RoleRenderer(stateStore, controller, avTileAdapter, adapter); }); afterEach(() => { vi.unstubAllGlobals(); vi.clearAllMocks(); }); describe('constructor', () => { it('stores all injected dependencies without side effects', () => { expect(renderer._stateStore).toBe(stateStore); expect(renderer._controller).toBe(controller); expect(renderer._avTileAdapter).toBe(avTileAdapter); expect(renderer._adapter).toBe(adapter); }); it('does not register any Hooks in constructor', () => { expect(hooksStub.on).not.toHaveBeenCalled(); }); it('_strip is null before openStrip()', () => { expect(renderer._strip).toBeNull(); }); }); describe('init()', () => { it('registers scrying-pool:stateChanged hook', () => { renderer.init(); expect(hooksStub.on).toHaveBeenCalledWith( 'scrying-pool:stateChanged', expect.any(Function) ); }); it('registers scrying-pool:controllerAction hook', () => { renderer.init(); expect(hooksStub.on).toHaveBeenCalledWith( 'scrying-pool:controllerAction', expect.any(Function) ); }); it('registers updateUser hook', () => { renderer.init(); expect(hooksStub.on).toHaveBeenCalledWith('updateUser', expect.any(Function)); }); }); describe('_applyAVTileState()', () => { it('calls setStateClass on avTileAdapter with userId and state', () => { renderer._applyAVTileState('user-1', 'active'); expect(avTileAdapter.setStateClass).toHaveBeenCalledWith('user-1', 'active'); }); it('mounts lock-overlay element when state is hidden', () => { renderer._applyAVTileState('user-1', 'hidden'); expect(avTileAdapter.mount).toHaveBeenCalled(); const el = avTileAdapter.mount.mock.calls[0][1]; expect(el.dataset.spRole).toBe('lock-overlay'); }); it('unmounts lock-overlay when state transitions away from hidden', () => { renderer._applyAVTileState('user-1', 'active'); expect(avTileAdapter.unmount).toHaveBeenCalledWith('user-1'); }); it('mounts portrait-fallback when state is never-connected', () => { renderer._applyAVTileState('user-1', 'never-connected'); expect(avTileAdapter.mount).toHaveBeenCalled(); const el = avTileAdapter.mount.mock.calls[0][1]; expect(el.dataset.spRole).toBe('portrait-fallback'); }); it('mounts portrait-fallback when state is cam-lost', () => { renderer._applyAVTileState('user-1', 'cam-lost'); expect(avTileAdapter.mount).toHaveBeenCalled(); const el = avTileAdapter.mount.mock.calls[0][1]; expect(el.dataset.spRole).toBe('portrait-fallback'); }); it('unmounts portrait-fallback when state is not camera-absent', () => { renderer._applyAVTileState('user-1', 'active'); expect(avTileAdapter.unmount).toHaveBeenCalled(); }); it('does not mount any overlay for active state', () => { renderer._applyAVTileState('user-1', 'active'); expect(avTileAdapter.mount).not.toHaveBeenCalled(); }); }); describe('stateChanged hook handler', () => { it('calls _applyAVTileState when scrying-pool:stateChanged fires', () => { renderer.init(); const spy = vi.spyOn(renderer, '_applyAVTileState'); const handler = hooksStub.on.mock.calls.find( c => c[0] === 'scrying-pool:stateChanged' )[1]; handler({ userId: 'user-1', state: 'hidden' }); expect(spy).toHaveBeenCalledWith('user-1', 'hidden'); }); it('handles bulk matrix payload gracefully', () => { renderer.init(); const handler = hooksStub.on.mock.calls.find( c => c[0] === 'scrying-pool:stateChanged' )[1]; // bulk payload has no userId expect(() => handler({ matrix: {}, timestamp: Date.now(), revision: 1 })).not.toThrow(); }); }); describe('openStrip()', () => { it('constructs ScryingPoolStrip lazily on first call', async () => { const { ScryingPoolStrip } = await import('../../../src/ui/gm/ScryingPoolStrip.js'); vi.clearAllMocks(); renderer.openStrip(); expect(ScryingPoolStrip).toHaveBeenCalledOnce(); }); it('reuses existing strip instance on second call', async () => { const { ScryingPoolStrip } = await import('../../../src/ui/gm/ScryingPoolStrip.js'); vi.clearAllMocks(); renderer.openStrip(); renderer.openStrip(); expect(ScryingPoolStrip).toHaveBeenCalledOnce(); }); it('calls render on the strip', () => { renderer.openStrip(); expect(renderer._strip.render).toHaveBeenCalled(); }); }); describe('closeStrip()', () => { it('calls close on the strip if it exists', () => { renderer.openStrip(); const strip = renderer._strip; renderer.closeStrip(); expect(strip.close).toHaveBeenCalled(); }); it('is no-op if strip is not open', () => { expect(() => renderer.closeStrip()).not.toThrow(); }); }); });