Fix Story 1.3: StateStore spec compliance and minor cleanup
Critical Fix: - StateStore now uses global Hooks.callAll directly (per spec) - Removed hooks parameter from StateStore constructor - Updated module.js to pass only adapter.settings - Updated tests to stub globalThis.Hooks Minor Cleanup: - Fixed misleading warning in SocketHandler.registerPendingOp - Added clarifying comment for setMatrix _revision behavior Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
* tests/unit/foundry/FoundryAdapter.test.js
|
||||
*
|
||||
* Story 1.2 — WebRTC spike: FoundryAdapter probe tests.
|
||||
* Story 1.3 — Data layer: constructor(game), surface delegation tests.
|
||||
*
|
||||
* OQ-1 spike result: css-fallback (FoundryVTT v14, 2026-05-21)
|
||||
* track.enabled = false does NOT stop inbound WebRTC bandwidth.
|
||||
@@ -11,7 +12,18 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { FoundryAdapter } from '../../../src/foundry/FoundryAdapter.js';
|
||||
import { createFoundryAdapterMock } from '../../helpers/foundryAdapterMock.js';
|
||||
import {
|
||||
GAME_STUB,
|
||||
SETTINGS_STUB,
|
||||
SOCKET_STUB,
|
||||
USERS_STUB,
|
||||
SCENES_STUB,
|
||||
HOOKS_STUB,
|
||||
UI_STUB,
|
||||
GM_USER,
|
||||
PLAYER_USER,
|
||||
ACTIVE_SCENE,
|
||||
} from '../../fixtures/foundry-adapter.js';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -184,12 +196,26 @@ describe('FoundryAdapter.buildWebRTCSurface', () => {
|
||||
// ─── Constructor + interface shape parity ─────────────────────────────────────
|
||||
|
||||
describe('FoundryAdapter constructor', () => {
|
||||
it('is side-effect-free — instantiates without accessing game.*', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('Hooks', HOOKS_STUB);
|
||||
vi.stubGlobal('ui', UI_STUB);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('is side-effect-free — instantiates without accessing game.* (no game arg)', () => {
|
||||
expect(() => new FoundryAdapter()).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts game as constructor arg and stores it', () => {
|
||||
const adapter = new FoundryAdapter(GAME_STUB);
|
||||
expect(adapter._game).toBe(GAME_STUB);
|
||||
});
|
||||
|
||||
it('has webrtc = null by default (css-fallback path)', () => {
|
||||
const adapter = new FoundryAdapter();
|
||||
const adapter = new FoundryAdapter(GAME_STUB);
|
||||
expect(adapter.webrtc).toBeNull();
|
||||
});
|
||||
|
||||
@@ -199,26 +225,154 @@ describe('FoundryAdapter constructor', () => {
|
||||
expect(typeof FoundryAdapter.SETTING_WEBRTC_MODE).toBe('string');
|
||||
expect(FoundryAdapter.SETTING_WEBRTC_MODE).toBe('webrtcMode');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interface shape parity with canonical mock', () => {
|
||||
it('canonical mock webrtc default matches FoundryAdapter default (null)', () => {
|
||||
const mock = createFoundryAdapterMock();
|
||||
const adapter = new FoundryAdapter();
|
||||
expect(mock.webrtc).toBe(adapter.webrtc); // both null
|
||||
});
|
||||
|
||||
it('canonical mock can override webrtc to simulate track-disable surface', () => {
|
||||
const mockSurface = { disableTrack: vi.fn(), enableTrack: vi.fn() };
|
||||
const mock = createFoundryAdapterMock({ webrtc: mockSurface });
|
||||
expect(mock.webrtc).toBe(mockSurface);
|
||||
expect(typeof mock.webrtc.disableTrack).toBe('function');
|
||||
expect(typeof mock.webrtc.enableTrack).toBe('function');
|
||||
});
|
||||
|
||||
it('buildWebRTCSurface returns object with disableTrack and enableTrack', () => {
|
||||
const surface = FoundryAdapter.buildWebRTCSurface(makeGameWebrtc());
|
||||
expect(typeof surface.disableTrack).toBe('function');
|
||||
expect(typeof surface.enableTrack).toBe('function');
|
||||
it('exposes all 6 surfaces: settings, socket, users, scenes, notifications, hooks', () => {
|
||||
const adapter = new FoundryAdapter(GAME_STUB);
|
||||
expect(typeof adapter.settings).toBe('object');
|
||||
expect(typeof adapter.socket).toBe('object');
|
||||
expect(typeof adapter.users).toBe('object');
|
||||
expect(typeof adapter.scenes).toBe('object');
|
||||
expect(typeof adapter.notifications).toBe('object');
|
||||
expect(typeof adapter.hooks).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Surface delegation ────────────────────────────────────────────────────────
|
||||
|
||||
describe('FoundryAdapter surface delegation', () => {
|
||||
let adapter;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('Hooks', HOOKS_STUB);
|
||||
vi.stubGlobal('ui', UI_STUB);
|
||||
vi.clearAllMocks();
|
||||
adapter = new FoundryAdapter(GAME_STUB);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('settings surface', () => {
|
||||
it('settings.register delegates to game.settings.register with namespace', () => {
|
||||
adapter.settings.register('visibilityMatrix', { default: {} });
|
||||
expect(SETTINGS_STUB.register).toHaveBeenCalledWith('scrying-pool', 'visibilityMatrix', { default: {} });
|
||||
});
|
||||
|
||||
it('settings.get delegates to game.settings.get with namespace', () => {
|
||||
SETTINGS_STUB.get.mockReturnValue({ userId1: 'active' });
|
||||
const result = adapter.settings.get('visibilityMatrix');
|
||||
expect(SETTINGS_STUB.get).toHaveBeenCalledWith('scrying-pool', 'visibilityMatrix');
|
||||
expect(result).toEqual({ userId1: 'active' });
|
||||
});
|
||||
|
||||
it('settings.set delegates to game.settings.set with namespace', async () => {
|
||||
SETTINGS_STUB.set.mockResolvedValue({ userId1: 'hidden' });
|
||||
await adapter.settings.set('visibilityMatrix', { userId1: 'hidden' });
|
||||
expect(SETTINGS_STUB.set).toHaveBeenCalledWith('scrying-pool', 'visibilityMatrix', { userId1: 'hidden' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('socket surface', () => {
|
||||
it('socket.emit delegates to game.socket.emit', () => {
|
||||
adapter.socket.emit('scrying-pool.visibility.set', { opId: 'op-1' });
|
||||
expect(SOCKET_STUB.emit).toHaveBeenCalledWith('scrying-pool.visibility.set', { opId: 'op-1' });
|
||||
});
|
||||
|
||||
it('socket.on delegates to game.socket.on', () => {
|
||||
const handler = vi.fn();
|
||||
adapter.socket.on('scrying-pool.visibility.set', handler);
|
||||
expect(SOCKET_STUB.on).toHaveBeenCalledWith('scrying-pool.visibility.set', handler);
|
||||
});
|
||||
|
||||
it('socket.off delegates to game.socket.off', () => {
|
||||
const handler = vi.fn();
|
||||
adapter.socket.off('scrying-pool.visibility.set', handler);
|
||||
expect(SOCKET_STUB.off).toHaveBeenCalledWith('scrying-pool.visibility.set', handler);
|
||||
});
|
||||
});
|
||||
|
||||
describe('users surface', () => {
|
||||
it('users.get returns user by id', () => {
|
||||
const result = adapter.users.get(GM_USER.id);
|
||||
expect(USERS_STUB.get).toHaveBeenCalledWith(GM_USER.id);
|
||||
expect(result).toBe(GM_USER);
|
||||
});
|
||||
|
||||
it('users.get returns null for unknown id', () => {
|
||||
expect(adapter.users.get('unknown-id')).toBeNull();
|
||||
});
|
||||
|
||||
it('users.all returns array from game.users iteration', () => {
|
||||
const result = adapter.users.all();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result).toContain(GM_USER);
|
||||
expect(result).toContain(PLAYER_USER);
|
||||
});
|
||||
|
||||
it('users.isGM(id) checks a specific user', () => {
|
||||
expect(adapter.users.isGM(GM_USER.id)).toBe(true);
|
||||
expect(adapter.users.isGM(PLAYER_USER.id)).toBe(false);
|
||||
});
|
||||
|
||||
it('users.isGM() with no arg checks current user', () => {
|
||||
expect(adapter.users.isGM()).toBe(true); // USER_STUB is GM
|
||||
});
|
||||
|
||||
it('users.current() returns game.user', () => {
|
||||
expect(adapter.users.current()).toEqual({ id: GM_USER.id, name: GM_USER.name, isGM: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('scenes surface', () => {
|
||||
it('scenes.current() returns game.scenes.active', () => {
|
||||
expect(adapter.scenes.current()).toBe(ACTIVE_SCENE);
|
||||
});
|
||||
|
||||
it('scenes.get(id) delegates to game.scenes.get', () => {
|
||||
const result = adapter.scenes.get(ACTIVE_SCENE.id);
|
||||
expect(SCENES_STUB.get).toHaveBeenCalledWith(ACTIVE_SCENE.id);
|
||||
expect(result).toBe(ACTIVE_SCENE);
|
||||
});
|
||||
|
||||
it('scenes.get returns null for unknown id', () => {
|
||||
expect(adapter.scenes.get('no-such-scene')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('notifications surface', () => {
|
||||
it('notifications.info delegates to ui.notifications.info', () => {
|
||||
adapter.notifications.info('Test info');
|
||||
expect(UI_STUB.notifications.info).toHaveBeenCalledWith('Test info');
|
||||
});
|
||||
|
||||
it('notifications.warn delegates to ui.notifications.warn', () => {
|
||||
adapter.notifications.warn('Test warn');
|
||||
expect(UI_STUB.notifications.warn).toHaveBeenCalledWith('Test warn');
|
||||
});
|
||||
|
||||
it('notifications.error delegates to ui.notifications.error', () => {
|
||||
adapter.notifications.error('Test error');
|
||||
expect(UI_STUB.notifications.error).toHaveBeenCalledWith('Test error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hooks surface', () => {
|
||||
it('hooks.on delegates to Hooks.on', () => {
|
||||
const handler = vi.fn();
|
||||
adapter.hooks.on('ready', handler);
|
||||
expect(HOOKS_STUB.on).toHaveBeenCalledWith('ready', handler);
|
||||
});
|
||||
|
||||
it('hooks.once delegates to Hooks.once', () => {
|
||||
const handler = vi.fn();
|
||||
adapter.hooks.once('ready', handler);
|
||||
expect(HOOKS_STUB.once).toHaveBeenCalledWith('ready', handler);
|
||||
});
|
||||
|
||||
it('hooks.off delegates to Hooks.off', () => {
|
||||
const handler = vi.fn();
|
||||
adapter.hooks.off('ready', handler);
|
||||
expect(HOOKS_STUB.off).toHaveBeenCalledWith('ready', handler);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user