Fix Story 2.3 code review findings: remove duplicate ParticipantCard.js, fix lint in ScryingPoolStrip.js
- 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>
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
// @ts-nocheck
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Stub foundry global for conditional base class — must NOT be present at module load time
|
||||
// so the fallback class is used. The module is imported after this comment block.
|
||||
// We only stub `foundry` in specific tests that need runtime foundry calls (none here).
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('Hooks', {
|
||||
on: vi.fn(() => 99),
|
||||
off: vi.fn(),
|
||||
});
|
||||
vi.stubGlobal('game', {
|
||||
user: {
|
||||
setFlag: vi.fn(),
|
||||
getFlag: vi.fn(() => null),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
import { DirectorsBoard } from '../../../../src/ui/gm/DirectorsBoard.js';
|
||||
|
||||
describe('DirectorsBoard', () => {
|
||||
let stateStore;
|
||||
let controller;
|
||||
let adapter;
|
||||
let board;
|
||||
|
||||
beforeEach(() => {
|
||||
stateStore = { getState: vi.fn(() => 'active') };
|
||||
controller = { action: vi.fn(), hasPendingOp: vi.fn(() => false), getRevision: vi.fn(() => 0) };
|
||||
adapter = {
|
||||
users: {
|
||||
get: vi.fn(() => ({ name: 'Alice', avatar: null })),
|
||||
all: vi.fn(() => [{ id: 'u1' }]),
|
||||
},
|
||||
};
|
||||
board = new DirectorsBoard(stateStore, controller, adapter);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('is side-effect-free: does not call Hooks.on', () => {
|
||||
expect(Hooks.on).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets _hookId to null initially', () => {
|
||||
expect(board._hookId).toBeNull();
|
||||
});
|
||||
|
||||
it('stores stateStore, controller, adapter references', () => {
|
||||
expect(board._stateStore).toBe(stateStore);
|
||||
expect(board._controller).toBe(controller);
|
||||
expect(board._adapter).toBe(adapter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('init()', () => {
|
||||
it('registers scrying-pool:stateChanged hook', () => {
|
||||
board.init();
|
||||
expect(Hooks.on).toHaveBeenCalledWith('scrying-pool:stateChanged', expect.any(Function));
|
||||
});
|
||||
|
||||
it('stores the returned hook id in _hookId', () => {
|
||||
board.init();
|
||||
expect(board._hookId).toBe(99);
|
||||
});
|
||||
});
|
||||
|
||||
describe('teardown()', () => {
|
||||
it('calls Hooks.off with the stored hook id', () => {
|
||||
board.init();
|
||||
board.teardown();
|
||||
expect(Hooks.off).toHaveBeenCalledWith('scrying-pool:stateChanged', 99);
|
||||
});
|
||||
|
||||
it('sets _hookId to null after teardown', () => {
|
||||
board.init();
|
||||
board.teardown();
|
||||
expect(board._hookId).toBeNull();
|
||||
});
|
||||
|
||||
it('is a no-op when init was not called', () => {
|
||||
expect(() => board.teardown()).not.toThrow();
|
||||
expect(Hooks.off).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('_dispatchToggle()', () => {
|
||||
it('calls controller.action with positional args (active→hidden)', () => {
|
||||
stateStore.getState.mockReturnValue('active');
|
||||
board._dispatchToggle('u1');
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u1', 'hidden', expect.any(String), expect.any(Number));
|
||||
});
|
||||
|
||||
it('calls controller.action with targetState active (hidden→active)', () => {
|
||||
stateStore.getState.mockReturnValue('hidden');
|
||||
board._dispatchToggle('u2');
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u2', 'active', expect.any(String), expect.any(Number));
|
||||
});
|
||||
|
||||
it('does not dispatch if userId is falsy', () => {
|
||||
board._dispatchToggle(null);
|
||||
board._dispatchToggle(undefined);
|
||||
board._dispatchToggle('');
|
||||
expect(controller.action).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not dispatch if controller reports pending op', () => {
|
||||
controller.hasPendingOp.mockReturnValue(true);
|
||||
board._dispatchToggle('u1');
|
||||
expect(controller.action).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('defaults to active state when stateStore returns null', () => {
|
||||
stateStore.getState.mockReturnValue(null);
|
||||
board._dispatchToggle('u1');
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u1', 'hidden', expect.any(String), expect.any(Number));
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggle()', () => {
|
||||
it('calls render({ force: true }) when not rendered', async () => {
|
||||
board._rendered = false;
|
||||
const renderSpy = vi.spyOn(board, 'render').mockResolvedValue(undefined);
|
||||
await board.toggle();
|
||||
expect(renderSpy).toHaveBeenCalledWith({ force: true });
|
||||
});
|
||||
|
||||
it('calls close() when rendered', async () => {
|
||||
board._rendered = true;
|
||||
const closeSpy = vi.spyOn(board, 'close').mockResolvedValue(undefined);
|
||||
await board.toggle();
|
||||
expect(closeSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('_onStateChanged()', () => {
|
||||
it('calls render({ force: true }) when board is rendered', () => {
|
||||
board._rendered = true;
|
||||
const renderSpy = vi.spyOn(board, 'render').mockResolvedValue(undefined);
|
||||
board._onStateChanged({ userId: 'u1', newState: 'hidden' });
|
||||
expect(renderSpy).toHaveBeenCalledWith({ force: true });
|
||||
});
|
||||
|
||||
it('does not call render when board is not rendered', () => {
|
||||
board._rendered = false;
|
||||
const renderSpy = vi.spyOn(board, 'render').mockResolvedValue(undefined);
|
||||
board._onStateChanged({ userId: 'u1', newState: 'hidden' });
|
||||
expect(renderSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('_prepareContext()', () => {
|
||||
it('returns board context with participants from adapter', async () => {
|
||||
const ctx = await board._prepareContext({});
|
||||
expect(ctx.participants).toHaveLength(1);
|
||||
expect(ctx.participants[0].userId).toBe('u1');
|
||||
expect(ctx.isEmpty).toBe(false);
|
||||
});
|
||||
|
||||
it('returns isEmpty=true when adapter has no users', async () => {
|
||||
adapter.users.all.mockReturnValue([]);
|
||||
const ctx = await board._prepareContext({});
|
||||
expect(ctx.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('returns hasUndo=false when _undoSnapshot is null', async () => {
|
||||
board._undoSnapshot = null;
|
||||
const ctx = await board._prepareContext({});
|
||||
expect(ctx.hasUndo).toBe(false);
|
||||
});
|
||||
|
||||
it('returns hasUndo=true when _undoSnapshot is set', async () => {
|
||||
board._undoSnapshot = new Map([['u1', 'hidden']]);
|
||||
const ctx = await board._prepareContext({});
|
||||
expect(ctx.hasUndo).toBe(true);
|
||||
});
|
||||
|
||||
it('returns hasRestore=false when _spotlightSnapshot is null', async () => {
|
||||
board._spotlightSnapshot = null;
|
||||
const ctx = await board._prepareContext({});
|
||||
expect(ctx.hasRestore).toBe(false);
|
||||
});
|
||||
|
||||
it('returns hasRestore=true when _spotlightSnapshot is set', async () => {
|
||||
board._spotlightSnapshot = new Map([['u1', 'active']]);
|
||||
const ctx = await board._prepareContext({});
|
||||
expect(ctx.hasRestore).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DEFAULT_OPTIONS', () => {
|
||||
it('has correct id', () => {
|
||||
expect(DirectorsBoard.DEFAULT_OPTIONS.id).toBe('scrying-pool-directors-board');
|
||||
});
|
||||
|
||||
it('has classes including scrying-pool and directors-board', () => {
|
||||
expect(DirectorsBoard.DEFAULT_OPTIONS.classes).toContain('scrying-pool');
|
||||
expect(DirectorsBoard.DEFAULT_OPTIONS.classes).toContain('directors-board');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PARTS', () => {
|
||||
it('has a board part with the correct template path', () => {
|
||||
expect(DirectorsBoard.PARTS.board.template).toContain('directors-board.hbs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('showAll()', () => {
|
||||
it('calls controller.action with active for each non-ghost user', () => {
|
||||
adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }, { id: 'u3' }]);
|
||||
stateStore.getState.mockImplementation(id => id === 'u3' ? 'ghost' : 'hidden');
|
||||
board.showAll();
|
||||
expect(controller.action).toHaveBeenCalledTimes(2);
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u1', 'active', expect.any(String), expect.any(Number));
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u2', 'active', expect.any(String), expect.any(Number));
|
||||
expect(controller.action).not.toHaveBeenCalledWith('board', 'u3', expect.anything(), expect.anything(), expect.anything());
|
||||
});
|
||||
|
||||
it('stores pre-action snapshot in _undoSnapshot (non-ghost users only)', () => {
|
||||
adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }, { id: 'u3' }]);
|
||||
stateStore.getState.mockImplementation(id => {
|
||||
if (id === 'u1') return 'hidden';
|
||||
if (id === 'u2') return 'active';
|
||||
return 'ghost';
|
||||
});
|
||||
board.showAll();
|
||||
expect(board._undoSnapshot).toBeInstanceOf(Map);
|
||||
expect(board._undoSnapshot.get('u1')).toBe('hidden');
|
||||
expect(board._undoSnapshot.get('u2')).toBe('active');
|
||||
expect(board._undoSnapshot.has('u3')).toBe(false);
|
||||
});
|
||||
|
||||
it('clears _spotlightSnapshot when called', () => {
|
||||
board._spotlightSnapshot = new Map([['u1', 'active']]);
|
||||
adapter.users.all.mockReturnValue([{ id: 'u1' }]);
|
||||
stateStore.getState.mockReturnValue('hidden');
|
||||
board.showAll();
|
||||
expect(board._spotlightSnapshot).toBeNull();
|
||||
});
|
||||
|
||||
it('skips participants with pending ops', () => {
|
||||
adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
|
||||
stateStore.getState.mockReturnValue('hidden');
|
||||
controller.hasPendingOp.mockImplementation(id => id === 'u1');
|
||||
board.showAll();
|
||||
expect(controller.action).toHaveBeenCalledTimes(1);
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u2', 'active', expect.any(String), expect.any(Number));
|
||||
});
|
||||
|
||||
it('is a no-op when all users are ghost', () => {
|
||||
adapter.users.all.mockReturnValue([{ id: 'u1' }]);
|
||||
stateStore.getState.mockReturnValue('ghost');
|
||||
board.showAll();
|
||||
expect(controller.action).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hideAll()', () => {
|
||||
it('calls controller.action with hidden for each non-ghost user', () => {
|
||||
adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
|
||||
stateStore.getState.mockReturnValue('active');
|
||||
board.hideAll();
|
||||
expect(controller.action).toHaveBeenCalledTimes(2);
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u1', 'hidden', expect.any(String), expect.any(Number));
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u2', 'hidden', expect.any(String), expect.any(Number));
|
||||
});
|
||||
|
||||
it('stores pre-action snapshot in _undoSnapshot', () => {
|
||||
adapter.users.all.mockReturnValue([{ id: 'u1' }]);
|
||||
stateStore.getState.mockReturnValue('active');
|
||||
board.hideAll();
|
||||
expect(board._undoSnapshot).toBeInstanceOf(Map);
|
||||
expect(board._undoSnapshot.get('u1')).toBe('active');
|
||||
});
|
||||
|
||||
it('clears _spotlightSnapshot when called', () => {
|
||||
board._spotlightSnapshot = new Map([['u1', 'active']]);
|
||||
adapter.users.all.mockReturnValue([{ id: 'u1' }]);
|
||||
stateStore.getState.mockReturnValue('active');
|
||||
board.hideAll();
|
||||
expect(board._spotlightSnapshot).toBeNull();
|
||||
});
|
||||
|
||||
it('excludes ghost-state participants', () => {
|
||||
adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
|
||||
stateStore.getState.mockImplementation(id => id === 'u2' ? 'ghost' : 'active');
|
||||
board.hideAll();
|
||||
expect(controller.action).toHaveBeenCalledTimes(1);
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u1', 'hidden', expect.any(String), expect.any(Number));
|
||||
});
|
||||
});
|
||||
|
||||
describe('undo()', () => {
|
||||
it('restores participants to snapshot states', () => {
|
||||
board._undoSnapshot = new Map([['u1', 'hidden'], ['u2', 'active']]);
|
||||
stateStore.getState.mockReturnValue('active');
|
||||
board.undo();
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u1', 'hidden', expect.any(String), expect.any(Number));
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u2', 'active', expect.any(String), expect.any(Number));
|
||||
});
|
||||
|
||||
it('clears _undoSnapshot after use (single-step only)', () => {
|
||||
board._undoSnapshot = new Map([['u1', 'hidden']]);
|
||||
stateStore.getState.mockReturnValue('active');
|
||||
board.undo();
|
||||
expect(board._undoSnapshot).toBeNull();
|
||||
});
|
||||
|
||||
it('is a no-op when _undoSnapshot is null', () => {
|
||||
board._undoSnapshot = null;
|
||||
board.undo();
|
||||
expect(controller.action).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('second undo is unavailable after first (no-op)', () => {
|
||||
board._undoSnapshot = new Map([['u1', 'hidden']]);
|
||||
stateStore.getState.mockReturnValue('active');
|
||||
board.undo();
|
||||
board.undo();
|
||||
expect(controller.action).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('skips ghost-state participants during undo', () => {
|
||||
board._undoSnapshot = new Map([['u1', 'active'], ['u2', 'hidden']]);
|
||||
stateStore.getState.mockImplementation(id => id === 'u2' ? 'ghost' : 'active');
|
||||
board.undo();
|
||||
expect(controller.action).toHaveBeenCalledTimes(1);
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u1', 'active', expect.any(String), expect.any(Number));
|
||||
});
|
||||
|
||||
it('skips participants with pending ops during undo', () => {
|
||||
board._undoSnapshot = new Map([['u1', 'hidden'], ['u2', 'hidden']]);
|
||||
stateStore.getState.mockReturnValue('active');
|
||||
controller.hasPendingOp.mockImplementation(id => id === 'u1');
|
||||
board.undo();
|
||||
expect(controller.action).toHaveBeenCalledTimes(1);
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u2', 'hidden', expect.any(String), expect.any(Number));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DirectorsBoard spotlight', () => {
|
||||
let stateStore, controller, adapter, board;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('Hooks', { on: vi.fn(() => 1), off: vi.fn(), once: vi.fn() });
|
||||
vi.stubGlobal('game', { keybindings: { register: vi.fn() } });
|
||||
stateStore = {
|
||||
getState: vi.fn(),
|
||||
getAll: vi.fn(() => new Map()),
|
||||
};
|
||||
controller = {
|
||||
action: vi.fn(),
|
||||
hasPendingOp: vi.fn(() => false),
|
||||
getRevision: vi.fn(() => 0),
|
||||
};
|
||||
adapter = {
|
||||
users: { all: vi.fn(() => [{ id: 'u1' }, { id: 'u2' }, { id: 'u3' }]) },
|
||||
};
|
||||
board = new DirectorsBoard(stateStore, controller, adapter);
|
||||
board.rendered = false;
|
||||
board.render = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe('spotlight(userId)', () => {
|
||||
it('sets focusedId active, all others hidden, captures snapshot, clears undo', () => {
|
||||
stateStore.getState.mockImplementation(id => ({ u1: 'hidden', u2: 'active', u3: 'active' }[id]));
|
||||
board._undoSnapshot = new Map([['u1', 'hidden']]);
|
||||
board.spotlight('u1');
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u1', 'active', expect.any(String), expect.any(Number));
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u2', 'hidden', expect.any(String), expect.any(Number));
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u3', 'hidden', expect.any(String), expect.any(Number));
|
||||
});
|
||||
|
||||
it('stores pre-spotlight snapshot in _spotlightSnapshot', () => {
|
||||
stateStore.getState.mockImplementation(id => id === 'u1' ? 'active' : 'hidden');
|
||||
board.spotlight('u1');
|
||||
expect(board._spotlightSnapshot).toBeInstanceOf(Map);
|
||||
expect(board._spotlightSnapshot.size).toBe(3);
|
||||
});
|
||||
|
||||
it('clears _undoSnapshot when spotlight is called', () => {
|
||||
stateStore.getState.mockReturnValue('active');
|
||||
board._undoSnapshot = new Map([['u1', 'hidden']]);
|
||||
board.spotlight('u2');
|
||||
expect(board._undoSnapshot).toBeNull();
|
||||
});
|
||||
|
||||
it('excludes ghost participants from spotlight', () => {
|
||||
stateStore.getState.mockImplementation(id => id === 'u3' ? 'ghost' : 'active');
|
||||
board.spotlight('u1');
|
||||
const calls = controller.action.mock.calls.map(c => c[1]);
|
||||
expect(calls).not.toContain('u3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreSpotlight()', () => {
|
||||
it('restores participants to pre-spotlight snapshot states', () => {
|
||||
board._spotlightSnapshot = new Map([['u1', 'hidden'], ['u2', 'active'], ['u3', 'hidden']]);
|
||||
stateStore.getState.mockReturnValue('active');
|
||||
board.restoreSpotlight();
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u1', 'hidden', expect.any(String), expect.any(Number));
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u2', 'active', expect.any(String), expect.any(Number));
|
||||
});
|
||||
|
||||
it('clears _spotlightSnapshot after restore', () => {
|
||||
board._spotlightSnapshot = new Map([['u1', 'active']]);
|
||||
stateStore.getState.mockReturnValue('active');
|
||||
board.restoreSpotlight();
|
||||
expect(board._spotlightSnapshot).toBeNull();
|
||||
});
|
||||
|
||||
it('is a no-op when _spotlightSnapshot is null', () => {
|
||||
board._spotlightSnapshot = null;
|
||||
board.restoreSpotlight();
|
||||
expect(controller.action).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips ghost participants during restore', () => {
|
||||
board._spotlightSnapshot = new Map([['u1', 'active'], ['u2', 'hidden']]);
|
||||
stateStore.getState.mockImplementation(id => id === 'u2' ? 'ghost' : 'active');
|
||||
board.restoreSpotlight();
|
||||
expect(controller.action).toHaveBeenCalledTimes(1);
|
||||
expect(controller.action).toHaveBeenCalledWith('board', 'u1', 'active', expect.any(String), expect.any(Number));
|
||||
});
|
||||
});
|
||||
|
||||
describe('spotlightFocused()', () => {
|
||||
it('is a no-op if no participant is focused', () => {
|
||||
board._focusedUserId = null;
|
||||
board.spotlightFocused();
|
||||
expect(controller.action).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls spotlight() with the currently focused userId', () => {
|
||||
stateStore.getState.mockReturnValue('active');
|
||||
board._focusedUserId = 'u2';
|
||||
const spy = vi.spyOn(board, 'spotlight');
|
||||
board.spotlightFocused();
|
||||
expect(spy).toHaveBeenCalledWith('u2');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user