Module cleanup and tests
CI / ci (push) Failing after 7s

This commit is contained in:
2026-05-24 23:13:45 +02:00
parent 63d83e999a
commit 5dc9b3b8d4
72 changed files with 2545 additions and 1220 deletions
@@ -29,7 +29,6 @@ describe("privacy-settings contract", () => {
it("should export PRIVACY_SETTINGS_DEFAULT with all false and null portrait", () => {
expect(PRIVACY_SETTINGS_DEFAULT).toEqual({
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: null,
});
});
@@ -37,7 +36,6 @@ describe("privacy-settings contract", () => {
it("should export PRIVACY_SETTING_KEYS as frozen array including portrait fallback", () => {
expect(PRIVACY_SETTING_KEYS).toEqual([
"reactionCamEnabled",
"hpReactiveCamStylingEnabled",
"customPortraitFallback",
]);
expect(Object.isFrozen(PRIVACY_SETTING_KEYS)).toBe(true);
@@ -46,7 +44,6 @@ describe("privacy-settings contract", () => {
it("should export FEATURE_NAME_MAP as frozen object", () => {
expect(FEATURE_NAME_MAP).toEqual({
reactionCam: "reactionCamEnabled",
hpReactiveCamStyling: "hpReactiveCamStylingEnabled",
});
expect(Object.isFrozen(FEATURE_NAME_MAP)).toBe(true);
});
@@ -62,19 +59,6 @@ describe("privacy-settings contract", () => {
const result = createPrivacySettings({ reactionCamEnabled: true });
expect(result).toEqual({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: null,
});
});
it("should allow both boolean settings to be overridden", () => {
const result = createPrivacySettings({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: true,
});
expect(result).toEqual({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: true,
customPortraitFallback: null,
});
});
@@ -84,7 +68,6 @@ describe("privacy-settings contract", () => {
const result = createPrivacySettings({ customPortraitFallback: dataURL });
expect(result).toEqual({
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: dataURL,
});
});
@@ -102,7 +85,6 @@ describe("privacy-settings contract", () => {
});
expect(result).toEqual({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: null,
});
});
@@ -112,7 +94,6 @@ describe("privacy-settings contract", () => {
it("should accept valid settings with all false", () => {
const valid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
};
expect(isValidPrivacySettings(valid)).toEqual(valid);
});
@@ -120,7 +101,6 @@ describe("privacy-settings contract", () => {
it("should accept valid settings with all true", () => {
const valid = {
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: true,
};
expect(isValidPrivacySettings(valid)).toEqual(valid);
});
@@ -128,7 +108,6 @@ describe("privacy-settings contract", () => {
it("should accept valid settings with mixed values", () => {
const valid = {
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: false,
};
expect(isValidPrivacySettings(valid)).toEqual(valid);
});
@@ -164,7 +143,6 @@ describe("privacy-settings contract", () => {
it("should throw TypeError for unknown keys", () => {
const invalid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
extraKey: "invalid",
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
@@ -174,7 +152,6 @@ describe("privacy-settings contract", () => {
it("should throw TypeError when reactionCamEnabled is not boolean", () => {
const invalid = {
reactionCamEnabled: "not a boolean",
hpReactiveCamStylingEnabled: false,
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
@@ -185,7 +162,6 @@ describe("privacy-settings contract", () => {
it("should throw TypeError when reactionCamEnabled is a number", () => {
const invalid = {
reactionCamEnabled: 1,
hpReactiveCamStylingEnabled: false,
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
@@ -193,28 +169,6 @@ describe("privacy-settings contract", () => {
);
});
it("should throw TypeError when hpReactiveCamStylingEnabled is not boolean", () => {
const invalid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: "not a boolean",
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
"hpReactiveCamStylingEnabled must be a boolean"
);
});
it("should throw TypeError when hpReactiveCamStylingEnabled is null", () => {
const invalid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: null,
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
"hpReactiveCamStylingEnabled must be a boolean"
);
});
it("should accept settings with only reactionCamEnabled (backward compatible)", () => {
// Backward compatibility: settings without all keys are accepted
const valid = {
@@ -223,14 +177,6 @@ describe("privacy-settings contract", () => {
expect(() => isValidPrivacySettings(valid)).not.toThrow();
});
it("should accept settings with only hpReactiveCamStylingEnabled (backward compatible)", () => {
// Backward compatibility: settings without all keys are accepted
const valid = {
hpReactiveCamStylingEnabled: false,
};
expect(() => isValidPrivacySettings(valid)).not.toThrow();
});
it("should accept empty object (backward compatible)", () => {
// Backward compatibility: empty object is accepted
expect(() => isValidPrivacySettings({})).not.toThrow();
@@ -242,12 +188,6 @@ describe("privacy-settings contract", () => {
expect(validateSettingKey("reactionCamEnabled")).toBe("reactionCamEnabled");
});
it("should accept valid key: hpReactiveCamStylingEnabled", () => {
expect(validateSettingKey("hpReactiveCamStylingEnabled")).toBe(
"hpReactiveCamStylingEnabled"
);
});
it("should throw TypeError for empty string", () => {
expect(() => validateSettingKey("")).toThrow(TypeError);
expect(() => validateSettingKey("")).toThrow(
@@ -332,12 +272,6 @@ describe("privacy-settings contract", () => {
expect(validateFeatureName("reactionCam")).toBe("reactionCam");
});
it("should accept valid feature: hpReactiveCamStyling", () => {
expect(validateFeatureName("hpReactiveCamStyling")).toBe(
"hpReactiveCamStyling"
);
});
it("should throw TypeError for empty string", () => {
expect(() => validateFeatureName("")).toThrow(TypeError);
expect(() => validateFeatureName("")).toThrow(
@@ -399,7 +333,6 @@ describe("privacy-settings contract", () => {
it("should retain existing boolean settings", () => {
expect(PRIVACY_SETTINGS_DEFAULT.reactionCamEnabled).toBe(false);
expect(PRIVACY_SETTINGS_DEFAULT.hpReactiveCamStylingEnabled).toBe(false);
});
});
@@ -410,7 +343,6 @@ describe("privacy-settings contract", () => {
it("should retain existing keys", () => {
expect(PRIVACY_SETTING_KEYS).toContain("reactionCamEnabled");
expect(PRIVACY_SETTING_KEYS).toContain("hpReactiveCamStylingEnabled");
});
});
@@ -443,7 +375,6 @@ describe("privacy-settings contract", () => {
it("should accept valid settings with customPortraitFallback as string", () => {
const valid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: "data:image/png;base64,test",
};
expect(isValidPrivacySettings(valid)).toEqual(valid);
@@ -452,7 +383,6 @@ describe("privacy-settings contract", () => {
it("should accept valid settings with customPortraitFallback as null", () => {
const valid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: null,
};
expect(isValidPrivacySettings(valid)).toEqual(valid);
@@ -462,7 +392,6 @@ describe("privacy-settings contract", () => {
// Backward compatibility - may not have the key
const valid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
};
// This should still work - null/undefined is acceptable
expect(() => isValidPrivacySettings(valid)).not.toThrow();
@@ -471,7 +400,6 @@ describe("privacy-settings contract", () => {
it("should throw TypeError when customPortraitFallback is not string or null", () => {
const invalid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: 123,
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
@@ -483,7 +411,6 @@ describe("privacy-settings contract", () => {
it("should throw TypeError when customPortraitFallback is a boolean", () => {
const invalid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: true,
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
@@ -495,7 +422,6 @@ describe("privacy-settings contract", () => {
it("should throw TypeError when customPortraitFallback is an object", () => {
const invalid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: {},
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
+7 -50
View File
@@ -98,10 +98,10 @@ describe("PlayerPrivacyManager", () => {
});
it("should return saved settings when flag exists", () => {
const savedSettings = { reactionCamEnabled: true, hpReactiveCamStylingEnabled: false, customPortraitFallback: null };
const savedSettings = { reactionCamEnabled: true, customPortraitFallback: null };
adapter.users.get.mockReturnValue({
getFlag: vi.fn((scope, key) => {
if (scope === "video-view-manager") {
if (scope === "scrying-pool") {
return savedSettings[key];
}
return undefined;
@@ -114,7 +114,7 @@ describe("PlayerPrivacyManager", () => {
it("should return partial settings merged with defaults", () => {
adapter.users.get.mockReturnValue({
getFlag: vi.fn((scope, key) => {
if (scope === "video-view-manager" && key === "reactionCamEnabled") {
if (scope === "scrying-pool" && key === "reactionCamEnabled") {
return true;
}
return undefined;
@@ -123,7 +123,6 @@ describe("PlayerPrivacyManager", () => {
const result = manager.getSettings("user1");
expect(result).toEqual({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: null,
});
});
@@ -182,7 +181,7 @@ describe("PlayerPrivacyManager", () => {
adapter.users.get.mockReturnValue(mockUser);
await manager.setSetting("user1", "reactionCamEnabled", true);
expect(mockUser.setFlag).toHaveBeenCalledWith(
"video-view-manager",
"scrying-pool",
"reactionCamEnabled",
true
);
@@ -268,15 +267,6 @@ describe("PlayerPrivacyManager", () => {
expect(manager.isOptedIn("nonexistent", "reactionCam")).toBe(false);
});
it("should work for hpReactiveCamStyling feature", () => {
adapter.users.get.mockReturnValue({
getFlag: vi.fn((scope, key) => {
if (key === "hpReactiveCamStylingEnabled") return true;
return false;
}),
});
expect(manager.isOptedIn("user1", "hpReactiveCamStyling")).toBe(true);
});
});
describe("getAllSettings", () => {
@@ -295,7 +285,6 @@ describe("PlayerPrivacyManager", () => {
id: "user1",
getFlag: vi.fn((scope, key) => {
if (key === "reactionCamEnabled") return true;
if (key === "hpReactiveCamStylingEnabled") return false;
return undefined; // customPortraitFallback and other keys
}),
};
@@ -303,7 +292,6 @@ describe("PlayerPrivacyManager", () => {
id: "user2",
getFlag: vi.fn((scope, key) => {
if (key === "reactionCamEnabled") return false;
if (key === "hpReactiveCamStylingEnabled") return true;
return undefined; // customPortraitFallback and other keys
}),
};
@@ -318,12 +306,10 @@ describe("PlayerPrivacyManager", () => {
expect(result.size).toBe(2);
expect(result.get("user1")).toEqual({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: null,
});
expect(result.get("user2")).toEqual({
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: true,
customPortraitFallback: null,
});
});
@@ -429,34 +415,6 @@ describe("PlayerPrivacyManager", () => {
expect(manager.isOptedIn("player1", "reactionCam")).toBe(true);
});
it("should handle player disabling HP-Reactive Cam Styling", async () => {
const mockUser = {
id: "player1",
getFlag: vi.fn((scope, key) => {
if (key === "hpReactiveCamStylingEnabled") return true;
return false;
}),
setFlag: vi.fn().mockResolvedValue(undefined),
};
adapter.users.get.mockReturnValue(mockUser);
// Initially opted in
expect(manager.isOptedIn("player1", "hpReactiveCamStyling")).toBe(true);
// Disable HP-Reactive Cam Styling
await manager.setSetting("player1", "hpReactiveCamStylingEnabled", false);
// After setting, the mock should return false for hpReactiveCamStylingEnabled
adapter.users.get.mockReturnValue({
id: "player1",
getFlag: vi.fn((scope, key) => {
if (key === "hpReactiveCamStylingEnabled") return false;
return false;
}),
setFlag: vi.fn().mockResolvedValue(undefined),
});
expect(manager.isOptedIn("player1", "hpReactiveCamStyling")).toBe(false);
});
it("should allow GM to view all players' settings", () => {
const gm = { id: "gm1", isGM: true, getFlag: vi.fn(() => false) };
const player1 = {
@@ -465,7 +423,7 @@ describe("PlayerPrivacyManager", () => {
};
const player2 = {
id: "player2",
getFlag: vi.fn((scope, key) => (key === "hpReactiveCamStylingEnabled" ? true : false)),
getFlag: vi.fn((scope, key) => (key === "reactionCamEnabled" ? false : false)),
};
adapter.users.all.mockReturnValue([gm, player1, player2]);
@@ -518,7 +476,7 @@ describe("PlayerPrivacyManager", () => {
).resolves.not.toThrow();
expect(mockUser.setFlag).toHaveBeenCalledWith(
"video-view-manager",
"scrying-pool",
"customPortraitFallback",
dataURL
);
@@ -648,7 +606,7 @@ describe("PlayerPrivacyManager", () => {
await manager.removePortraitFallback("player1");
expect(mockUser.unsetFlag).toHaveBeenCalledWith(
"video-view-manager",
"scrying-pool",
"customPortraitFallback"
);
});
@@ -686,7 +644,6 @@ describe("PlayerPrivacyManager", () => {
getFlag: vi.fn((scope, key) => {
if (key === "customPortraitFallback") return dataURL;
if (key === "reactionCamEnabled") return true;
if (key === "hpReactiveCamStylingEnabled") return false;
return undefined;
}),
};
+1 -1
View File
@@ -815,7 +815,7 @@ describe('ScenePresetManager', () => {
});
expect(mockScene.setFlag).toHaveBeenCalledWith(
'video-view-manager',
'scrying-pool',
'presets',
expect.objectContaining({
_version: 1,
+33 -21
View File
@@ -84,14 +84,15 @@ describe('ScryingPoolController', () => {
// ── AC-2: action() happy path ─────────────────────────────────────────────
describe('action() happy path (AC-2)', () => {
it('stores a PendingOp in _pendingOps keyed by participantId', () => {
it('registers a PendingOp via socketHandler.registerPendingOp with correct shape', () => {
// With self-confirm, _pendingOps is cleared synchronously after action().
// Verify the op was passed to registerPendingOp before being confirmed.
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
expect(controller._pendingOps.has('user-1')).toBe(true);
expect(controller._pendingOps.get('user-1')).toMatchObject({
opId: 'op-1',
userId: 'user-1',
targetState: 'hidden',
});
expect(socketHandler.registerPendingOp).toHaveBeenCalledWith(
expect.objectContaining({ opId: 'op-1', userId: 'user-1', targetState: 'hidden' }),
'scrying-pool.visibility.set',
expect.objectContaining({ opId: 'op-1' })
);
});
it('calls stateStore.setVisibility with the target state (optimistic update)', () => {
@@ -117,18 +118,24 @@ describe('ScryingPoolController', () => {
);
});
it('fires Hooks.callAll scrying-pool:controllerAction with correct payload', () => {
it('fires Hooks.callAll scrying-pool:controllerAction after self-confirm', () => {
// Self-confirm calls _onEcho which fires the hook with source: 'echo'.
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
expect(hooksStub.callAll).toHaveBeenCalledWith(
'scrying-pool:controllerAction',
expect.objectContaining({ participantId: 'user-1', targetState: 'hidden', source: 'ui', opId: 'op-1' })
expect.objectContaining({ participantId: 'user-1', targetState: 'hidden', source: 'echo', opId: 'op-1' })
);
});
it('sets previousState to null-coalesced "never-connected" when participant is new', () => {
it('sets previousState to "active" when participant is new (not yet in matrix)', () => {
// With self-confirm, _pendingOps is cleared synchronously. Verify via registerPendingOp arg.
controller.action('ui', 'new-user', 'hidden', 'op-1', 0);
const op = controller._pendingOps.get('new-user');
expect(op.previousState).toBe('never-connected');
// 'active' is the render-time default for users not in the matrix.
expect(socketHandler.registerPendingOp).toHaveBeenCalledWith(
expect.objectContaining({ previousState: 'active' }),
expect.any(String),
expect.any(Object)
);
});
});
@@ -226,23 +233,30 @@ describe('ScryingPoolController', () => {
return adapter.socket.on.mock.calls[0][1];
}
// Helper: directly register a pending op (bypasses action() self-confirm)
function seedPendingOp(userId, opId, targetState = 'hidden') {
const op = { opId, userId, targetState, previousState: 'active' };
controller._pendingOps.set(userId, op);
socketHandler.registerPendingOp(op, 'scrying-pool.visibility.set', {});
}
it('calls socketHandler.confirmPendingOp with the opId', () => {
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
const echoHandler = getEchoHandler();
seedPendingOp('user-1', 'op-1');
echoHandler({ opId: 'op-1', userId: 'user-1', state: 'hidden', revision: 1 });
expect(socketHandler.confirmPendingOp).toHaveBeenCalledWith('op-1');
});
it('stores the echo revision in _revisions for the userId', () => {
controller.action('ui', 'user-1', 'hidden', 'op-2', 0);
const echoHandler = getEchoHandler();
seedPendingOp('user-1', 'op-2');
echoHandler({ opId: 'op-2', userId: 'user-1', state: 'hidden', revision: 7 });
expect(controller._revisions.get('user-1')).toBe(7);
});
it('calls stateStore.setVisibility with the authoritative state', () => {
controller.action('ui', 'user-1', 'active', 'op-3', 0);
const echoHandler = getEchoHandler();
seedPendingOp('user-1', 'op-3', 'active');
const setSpy = vi.spyOn(stateStore, 'setVisibility');
echoHandler({ opId: 'op-3', userId: 'user-1', state: 'active', revision: 2 });
@@ -251,8 +265,8 @@ describe('ScryingPoolController', () => {
});
it('fires Hooks.callAll scrying-pool:controllerAction with source: echo', () => {
controller.action('ui', 'user-1', 'hidden', 'op-4', 0);
const echoHandler = getEchoHandler();
seedPendingOp('user-1', 'op-4');
echoHandler({ opId: 'op-4', userId: 'user-1', state: 'hidden', revision: 1 });
expect(hooksStub.callAll).toHaveBeenCalledWith(
@@ -262,20 +276,18 @@ describe('ScryingPoolController', () => {
});
it('removes the participant from _pendingOps after echo', () => {
// Register a pending op first
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
const echoHandler = getEchoHandler();
seedPendingOp('user-1', 'op-1');
expect(controller._pendingOps.has('user-1')).toBe(true);
const echoHandler = getEchoHandler();
echoHandler({ opId: 'op-1', userId: 'user-1', state: 'hidden', revision: 1 });
expect(controller._pendingOps.has('user-1')).toBe(false);
});
it('defaults revision to 0 when echo payload omits revision field', () => {
// Register a pending op first (required by new validation)
controller.action('ui', 'user-1', 'hidden', 'op-1', 0);
const echoHandler = getEchoHandler();
seedPendingOp('user-1', 'op-1');
echoHandler({ opId: 'op-1', userId: 'user-1', state: 'hidden' }); // no revision
expect(controller._revisions.get('user-1')).toBe(0);
});
+10 -10
View File
@@ -527,41 +527,41 @@ describe('FoundryAdapter surface delegation', () => {
describe('user flag methods', () => {
it('users.getFlag returns flag value for valid user, scope, and key', () => {
// First set a flag on the GM user
GM_USER.setFlag('video-view-manager', 'testFlag', 'testValue');
const result = adapter.users.getFlag(GM_USER.id, 'video-view-manager', 'testFlag');
GM_USER.setFlag('scrying-pool', 'testFlag', 'testValue');
const result = adapter.users.getFlag(GM_USER.id, 'scrying-pool', 'testFlag');
expect(result).toBe('testValue');
expect(USERS_STUB.get).toHaveBeenCalledWith(GM_USER.id);
});
it('users.getFlag returns null when flag does not exist', () => {
const result = adapter.users.getFlag(GM_USER.id, 'video-view-manager', 'nonExistentFlag');
const result = adapter.users.getFlag(GM_USER.id, 'scrying-pool', 'nonExistentFlag');
expect(result).toBeNull();
});
it('users.getFlag returns null when user does not exist', () => {
const result = adapter.users.getFlag('unknown-user-id', 'video-view-manager', 'testFlag');
const result = adapter.users.getFlag('unknown-user-id', 'scrying-pool', 'testFlag');
expect(result).toBeNull();
expect(USERS_STUB.get).toHaveBeenCalledWith('unknown-user-id');
});
it('users.setFlag sets flag value for valid user', async () => {
const promise = adapter.users.setFlag(PLAYER_USER.id, 'video-view-manager', 'reactionCamEnabled', true);
const promise = adapter.users.setFlag(PLAYER_USER.id, 'scrying-pool', 'reactionCamEnabled', true);
expect(promise).not.toBeNull();
await promise;
expect(USERS_STUB.get).toHaveBeenCalledWith(PLAYER_USER.id);
// Verify the flag was set
expect(PLAYER_USER.getFlag('video-view-manager', 'reactionCamEnabled')).toBe(true);
expect(PLAYER_USER.getFlag('scrying-pool', 'reactionCamEnabled')).toBe(true);
});
it('users.setFlag returns null when user does not exist', () => {
const promise = adapter.users.setFlag('unknown-user-id', 'video-view-manager', 'testFlag', true);
const promise = adapter.users.setFlag('unknown-user-id', 'scrying-pool', 'testFlag', true);
expect(promise).toBeNull();
expect(USERS_STUB.get).toHaveBeenCalledWith('unknown-user-id');
});
it('users.getFlagModule returns module-scoped flag', () => {
GM_USER.setFlag('video-view-manager', 'hpReactiveCamStylingEnabled', false);
const result = adapter.users.getFlagModule(GM_USER.id, 'hpReactiveCamStylingEnabled');
GM_USER.setFlag('scrying-pool', 'reactionCamEnabled', false);
const result = adapter.users.getFlagModule(GM_USER.id, 'reactionCamEnabled');
expect(result).toBe(false);
});
@@ -574,7 +574,7 @@ describe('FoundryAdapter surface delegation', () => {
const promise = adapter.users.setFlagModule(PLAYER_USER.id, 'reactionCamEnabled', true);
expect(promise).not.toBeNull();
await promise;
expect(PLAYER_USER.getFlag('video-view-manager', 'reactionCamEnabled')).toBe(true);
expect(PLAYER_USER.getFlag('scrying-pool', 'reactionCamEnabled')).toBe(true);
});
});
});
@@ -35,10 +35,10 @@ function makeAdapter({
localize: vi.fn((key, data) => {
// Simple mock that returns the key with data substituted
const messages = {
'video-view-manager.notifications.personalHidden': 'GM has hidden your camera. Your portrait is shown to other Participants.',
'video-view-manager.notifications.personalShowed': 'Your camera is now visible to the table.',
'video-view-manager.notifications.gmHid': 'GM hid {name}\'s camera',
'video-view-manager.notifications.gmShowed': 'GM showed {name}\'s camera',
'scrying-pool.notifications.personalHidden': 'GM has hidden your camera. Your portrait is shown to other Participants.',
'scrying-pool.notifications.personalShowed': 'Your camera is now visible to the table.',
'scrying-pool.notifications.gmHid': 'GM hid {name}\'s camera',
'scrying-pool.notifications.gmShowed': 'GM showed {name}\'s camera',
};
let msg = messages[key] ?? key;
if (data?.name) {
+4 -4
View File
@@ -22,10 +22,10 @@ function createMockAdapter(overrides = {}) {
localize: vi.fn((key) => {
// For testing, return strings with placeholders that match ConfirmationBar's .replace() calls
const translations = {
'video-view-manager.presets.confirmation.applied': 'Preset applied — {name}',
'video-view-manager.presets.confirmation.counts': '{hidden} hidden, {visible} visible',
'video-view-manager.presets.confirmation.partial-fail': '(some updates pending)',
'video-view-manager.presets.confirmation.undo': 'Undo preset apply',
'scrying-pool.presets.confirmation.applied': 'Preset applied — {name}',
'scrying-pool.presets.confirmation.counts': '{hidden} hidden, {visible} visible',
'scrying-pool.presets.confirmation.partial-fail': '(some updates pending)',
'scrying-pool.presets.confirmation.undo': 'Undo preset apply',
};
return translations[key] ?? key;
}),
+5 -5
View File
@@ -77,8 +77,8 @@ describe('DirectorsBoard', () => {
it('has DEFAULT_OPTIONS with position', () => {
expect(DirectorsBoard.DEFAULT_OPTIONS.position).toEqual({
width: 400,
height: 300,
width: 420,
height: 480,
});
});
});
@@ -110,7 +110,7 @@ describe('DirectorsBoard', () => {
boardWithOptions._loadPosition();
expect(game.user.getFlag).toHaveBeenCalledWith(
'video-view-manager',
'scrying-pool',
'directorsBoardState'
);
// Position should be merged into options.position (not replaced)
@@ -136,8 +136,8 @@ describe('DirectorsBoard', () => {
expect(boardWithOptions.options.position).toEqual({
left: 100,
top: 200,
width: 400,
height: 300,
width: 420,
height: 480,
});
});
+7 -7
View File
@@ -140,7 +140,7 @@ describe('PresetLoadDialog', () => {
it('should return hasPresets false when no presets exist', async () => {
scenePresetManager.list.mockReturnValue([]);
adapter.i18n.localize = vi.fn((key) => {
if (key === 'video-view-manager.presets.load.emptyMessage') return 'No presets available';
if (key === 'scrying-pool.presets.load.emptyMessage') return 'No presets available';
return key;
});
@@ -163,10 +163,10 @@ describe('PresetLoadDialog', () => {
it('should use i18n for labels', async () => {
adapter.i18n.localize = vi.fn((key) => {
const translations = {
'video-view-manager.presets.load.loadButton': 'Load',
'video-view-manager.presets.load.cancelButton': 'Cancel',
'video-view-manager.presets.load.title': 'Load Preset',
'video-view-manager.presets.load.emptyMessage': 'No presets',
'scrying-pool.presets.load.loadButton': 'Load',
'scrying-pool.presets.load.cancelButton': 'Cancel',
'scrying-pool.presets.load.title': 'Load Preset',
'scrying-pool.presets.load.emptyMessage': 'No presets',
};
return translations[key] || key;
});
@@ -276,7 +276,7 @@ describe('PresetLoadDialog', () => {
it('should show notification on successful load via adapter.notifications', async () => {
adapter.i18n.localize = vi.fn((key) => {
if (key === 'video-view-manager.presets.notifications.applied') return 'Applied preset: {name}';
if (key === 'scrying-pool.presets.notifications.applied') return 'Applied preset: {name}';
return key;
});
@@ -398,7 +398,7 @@ describe('PresetLoadDialog', () => {
it('should use the correct template path', () => {
expect(PresetLoadDialog.PARTS.dialog.template).toBe(
'modules/video-view-manager/templates/preset-load-dialog.hbs'
'modules/scrying-pool/templates/preset-load-dialog.hbs'
);
});
+9 -9
View File
@@ -125,7 +125,7 @@ describe('PresetSaveDialog', () => {
it('should return empty string as defaultName when no presets exist', async () => {
adapter.i18n.localize = vi.fn((key) => {
if (key === 'video-view-manager.presets.save.namePlaceholder') return 'Enter preset name';
if (key === 'scrying-pool.presets.save.namePlaceholder') return 'Enter preset name';
return key;
});
@@ -146,11 +146,11 @@ describe('PresetSaveDialog', () => {
it('should return all i18n labels', async () => {
adapter.i18n.localize = vi.fn((key) => {
const translations = {
'video-view-manager.presets.save.saveButton': 'Save',
'video-view-manager.presets.save.cancelButton': 'Cancel',
'video-view-manager.presets.save.title': 'Save Preset',
'video-view-manager.presets.save.nameLabel': 'Preset Name',
'video-view-manager.presets.save.namePlaceholder': 'Enter preset name',
'scrying-pool.presets.save.saveButton': 'Save',
'scrying-pool.presets.save.cancelButton': 'Cancel',
'scrying-pool.presets.save.title': 'Save Preset',
'scrying-pool.presets.save.nameLabel': 'Preset Name',
'scrying-pool.presets.save.namePlaceholder': 'Enter preset name',
};
return translations[key] || key;
});
@@ -320,7 +320,7 @@ describe('PresetSaveDialog', () => {
scenePresetManager.save = vi.fn().mockResolvedValue({ name: 'My Preset' });
dialog.close = vi.fn().mockResolvedValue({});
adapter.i18n.localize = vi.fn((key) => {
if (key === 'video-view-manager.presets.notifications.saved') return 'Preset {name} saved!';
if (key === 'scrying-pool.presets.notifications.saved') return 'Preset {name} saved!';
return key;
});
@@ -447,7 +447,7 @@ describe('PresetSaveDialog', () => {
it('should use the correct template path', () => {
expect(PresetSaveDialog.PARTS.dialog.template).toBe(
'modules/video-view-manager/templates/preset-save-dialog.hbs'
'modules/scrying-pool/templates/preset-save-dialog.hbs'
);
});
@@ -459,7 +459,7 @@ describe('PresetSaveDialog', () => {
expect(options.classes).toContain('preset-save-dialog');
expect(options.window.title).toBe('Save Scene Preset');
expect(options.window.resizable).toBe(false);
expect(options.position.width).toBe(320);
expect(options.position.width).toBe(360);
});
it('should store references to dependencies', () => {
+5 -5
View File
@@ -88,8 +88,8 @@ describe('ScenePresetPanel', () => {
it('sets aria-label using i18n', () => {
panel.init();
expect(adapter.i18n.localize).toHaveBeenCalledWith('video-view-manager.scenePresetPanel.title');
expect(panel._element.getAttribute('aria-label')).toBe('video-view-manager.scenePresetPanel.title');
expect(adapter.i18n.localize).toHaveBeenCalledWith('scrying-pool.scenePresetPanel.title');
expect(panel._element.getAttribute('aria-label')).toBe('scrying-pool.scenePresetPanel.title');
});
it('sets aria-expanded to false initially', () => {
@@ -267,7 +267,7 @@ describe('ScenePresetPanel', () => {
it('uses i18n for message', () => {
panel._buildEmptyHtml();
expect(adapter.i18n.localize).toHaveBeenCalledWith('video-view-manager.scenePresetPanel.noScene');
expect(adapter.i18n.localize).toHaveBeenCalledWith('scrying-pool.scenePresetPanel.noScene');
});
it('escapes HTML in message', () => {
@@ -450,7 +450,7 @@ describe('ScenePresetPanel', () => {
mockTarget.checked = true;
await panel._onToggleAutoApply(mockTarget);
expect(adapter.notifications.info).toHaveBeenCalledWith(
'video-view-manager.scenePresetPanel.notifications.enabled'
'scrying-pool.scenePresetPanel.notifications.enabled'
);
});
@@ -460,7 +460,7 @@ describe('ScenePresetPanel', () => {
mockTarget.checked = false;
await panel._onToggleAutoApply(mockTarget);
expect(adapter.notifications.info).toHaveBeenCalledWith(
'video-view-manager.scenePresetPanel.notifications.disabled'
'scrying-pool.scenePresetPanel.notifications.disabled'
);
});
+24
View File
@@ -241,6 +241,30 @@ describe('ScryingPoolStrip', () => {
const data = strip.getData();
expect(data.hasStreamAccess).toBe(false);
});
it('includes current user when showGMSelfFeed is true', () => {
adapter.settings = { get: vi.fn(() => true) };
adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
const data = strip.getData();
expect(data.participants.map(p => p.userId)).toContain('u1');
expect(data.participants.map(p => p.userId)).toContain('u2');
});
it('excludes current user when showGMSelfFeed is false', () => {
adapter.settings = { get: vi.fn(() => false) };
adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
const data = strip.getData();
// u1 is the current user (mocked in beforeEach), should be excluded
expect(data.participants.map(p => p.userId)).not.toContain('u1');
expect(data.participants.map(p => p.userId)).toContain('u2');
});
it('includes all users when settings is unavailable (defaults to true)', () => {
// no adapter.settings — fallback to true
adapter.users.all.mockReturnValue([{ id: 'u1' }, { id: 'u2' }]);
const data = strip.getData();
expect(data.participants.length).toBe(2);
});
});
describe('_attachVideoStream() (Story 5.1)', () => {
@@ -31,8 +31,6 @@ function createMockAdapter(overrides = {}) {
'SCRYING_POOL.PrivacyPanel.sectionDescription': 'Control which automation effects can affect your camera.',
'SCRYING_POOL.PrivacyPanel.reactionCamLabel': 'Reaction Cam',
'SCRYING_POOL.PrivacyPanel.reactionCamDescription': 'Automatically show your camera during key moments (combat, rolls, etc.)',
'SCRYING_POOL.PrivacyPanel.hpReactiveCamStylingLabel': 'HP-Reactive Cam Styling',
'SCRYING_POOL.PrivacyPanel.hpReactiveCamStylingDescription': 'Apply visual styling to your camera based on your character\'s HP',
'SCRYING_POOL.PrivacyPanel.toggleOn': 'Enabled',
'SCRYING_POOL.PrivacyPanel.toggleOff': 'Disabled',
'SCRYING_POOL.PrivacyPanel.readOnlyNotice': 'This player\'s privacy settings are read-only',
@@ -130,7 +128,6 @@ describe('PlayerPrivacyPanel', () => {
it('should return context with settings', async () => {
const settings = createPrivacySettings({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: false,
});
playerPrivacyManager.getSettings.mockReturnValue(settings);
adapter.users.current.mockReturnValue({ id: targetUserId });
@@ -140,7 +137,7 @@ describe('PlayerPrivacyPanel', () => {
expect(context.title).toBe('Player Privacy Panel');
expect(context.sectionHeader).toBe('Automation Opt-ins');
expect(context.automationEffects).toBeDefined();
expect(context.automationEffects).toHaveLength(2);
expect(context.automationEffects).toHaveLength(1);
expect(context.isReadOnly).toBe(false);
expect(context.isOwnUser).toBe(true);
});
@@ -156,25 +153,22 @@ describe('PlayerPrivacyPanel', () => {
expect(context.isOwnUser).toBe(false);
});
it('should include both automation effects', async () => {
it('should include the available automation effect', async () => {
const context = await panel._prepareContext();
expect(context.automationEffects).toHaveLength(2);
expect(context.automationEffects).toHaveLength(1);
expect(context.automationEffects[0].key).toBe('reactionCam');
expect(context.automationEffects[1].key).toBe('hpReactiveCamStyling');
});
it('should reflect current settings in context', async () => {
const settings = createPrivacySettings({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: true,
});
playerPrivacyManager.getSettings.mockReturnValue(settings);
const context = await panel._prepareContext();
expect(context.automationEffects[0].enabled).toBe(true);
expect(context.automationEffects[1].enabled).toBe(true);
});
});
@@ -354,13 +348,11 @@ describe('PlayerPrivacyPanel', () => {
it('should clear cached elements', () => {
// Set up some cached values
panel._reactionCamToggle = document.createElement('div');
panel._hpReactiveCamToggle = document.createElement('div');
panel._currentSettings = createPrivacySettings();
panel._onClose();
expect(panel._reactionCamToggle).toBe(null);
expect(panel._hpReactiveCamToggle).toBe(null);
expect(panel._currentSettings).toBe(null);
});
});
+2 -2
View File
@@ -238,7 +238,7 @@ describe('VisibilityBadge', () => {
badge.init();
await badge._setFirstBadgeEncountered();
const mockUser = adapter.users.current();
expect(mockUser.setFlag).toHaveBeenCalledWith('video-view-manager', 'firstBadgeEncounter', true);
expect(mockUser.setFlag).toHaveBeenCalledWith('scrying-pool', 'firstBadgeEncounter', true);
});
});
@@ -390,7 +390,7 @@ describe('FirstEncounterPanel (via VisibilityBadge)', () => {
// Directly call _onGotIt to avoid async click handler timing issues
await panel._onGotIt();
const mockUser = adapter.users.current();
expect(mockUser.setFlag).toHaveBeenCalledWith('video-view-manager', 'firstBadgeEncounter', true);
expect(mockUser.setFlag).toHaveBeenCalledWith('scrying-pool', 'firstBadgeEncounter', true);
});
it('clears timer (no ghost timer after dismissal)', async () => {