@@ -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;
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -815,7 +815,7 @@ describe('ScenePresetManager', () => {
|
||||
});
|
||||
|
||||
expect(mockScene.setFlag).toHaveBeenCalledWith(
|
||||
'video-view-manager',
|
||||
'scrying-pool',
|
||||
'presets',
|
||||
expect.objectContaining({
|
||||
_version: 1,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user