Files
scrying-pool/tests/unit/ui/player/PlayerPrivacyPanel.test.js
uberwald 5dc9b3b8d4
CI / ci (push) Failing after 7s
Module cleanup and tests
2026-05-24 23:13:45 +02:00

360 lines
12 KiB
JavaScript

/**
* Tests for PlayerPrivacyPanel.
* @module tests/unit/ui/player/PlayerPrivacyPanel.test
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { PlayerPrivacyPanel } from '../../../../src/ui/player/PlayerPrivacyPanel.js';
import { createPrivacySettings } from '../../../../src/contracts/privacy-settings.js';
// Test helper: create a mock FoundryAdapter surface
function createMockAdapter(overrides = {}) {
return {
users: {
get: vi.fn(() => null),
all: vi.fn(() => []),
current: vi.fn(() => ({ id: 'test-user' })),
...overrides.users,
},
notifications: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
...overrides.notifications,
},
i18n: {
localize: vi.fn((key) => {
// Simple mock for i18n - return known strings or the key
const translations = {
'SCRYING_POOL.PrivacyPanel.title': 'Player Privacy Panel',
'SCRYING_POOL.PrivacyPanel.sectionHeader': 'Automation Opt-ins',
'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.toggleOn': 'Enabled',
'SCRYING_POOL.PrivacyPanel.toggleOff': 'Disabled',
'SCRYING_POOL.PrivacyPanel.readOnlyNotice': 'This player\'s privacy settings are read-only',
'SCRYING_POOL.PrivacyPanel.savedNotification': 'Privacy settings saved',
'SCRYING_POOL.PrivacyPanel.saveError': 'Failed to save privacy settings',
};
return translations[key] ?? key;
}),
...overrides.i18n,
},
...overrides,
};
}
// Test helper: create a mock PlayerPrivacyManager surface
function createMockPlayerPrivacyManager(overrides = {}) {
return {
getSettings: vi.fn(() => createPrivacySettings()),
setSetting: vi.fn().mockResolvedValue(undefined),
isOptedIn: vi.fn(() => false),
getAllSettings: vi.fn(() => new Map()),
onChange: vi.fn(() => vi.fn()), // Returns unsubscribe function
teardown: vi.fn(),
...overrides,
};
}
describe('PlayerPrivacyPanel', () => {
let adapter;
let playerPrivacyManager;
let panel;
const targetUserId = 'test-user-1';
beforeEach(() => {
adapter = createMockAdapter();
playerPrivacyManager = createMockPlayerPrivacyManager();
panel = new PlayerPrivacyPanel(adapter, playerPrivacyManager, targetUserId);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('constructor', () => {
it('should construct with valid arguments', () => {
expect(() =>
new PlayerPrivacyPanel(adapter, playerPrivacyManager, targetUserId)
).not.toThrow();
});
it('should throw TypeError for null adapter', () => {
expect(() =>
new PlayerPrivacyPanel(null, playerPrivacyManager, targetUserId)
).toThrow(TypeError);
expect(() =>
new PlayerPrivacyPanel(null, playerPrivacyManager, targetUserId)
).toThrow('adapter argument is required');
});
it('should throw TypeError for non-object adapter', () => {
expect(() =>
new PlayerPrivacyPanel('not an object', playerPrivacyManager, targetUserId)
).toThrow(TypeError);
expect(() =>
new PlayerPrivacyPanel('not an object', playerPrivacyManager, targetUserId)
).toThrow('adapter argument is required');
});
it('should throw TypeError for null playerPrivacyManager', () => {
expect(() =>
new PlayerPrivacyPanel(adapter, null, targetUserId)
).toThrow(TypeError);
expect(() =>
new PlayerPrivacyPanel(adapter, null, targetUserId)
).toThrow('playerPrivacyManager argument is required');
});
it('should throw TypeError for empty targetUserId', () => {
expect(() =>
new PlayerPrivacyPanel(adapter, playerPrivacyManager, '')
).toThrow(TypeError);
expect(() =>
new PlayerPrivacyPanel(adapter, playerPrivacyManager, '')
).toThrow('targetUserId must be a non-empty string');
});
it('should store dependencies', () => {
expect(panel._adapter).toBe(adapter);
expect(panel._playerPrivacyManager).toBe(playerPrivacyManager);
expect(panel._targetUserId).toBe(targetUserId);
});
});
describe('_prepareContext', () => {
it('should return context with settings', async () => {
const settings = createPrivacySettings({
reactionCamEnabled: true,
});
playerPrivacyManager.getSettings.mockReturnValue(settings);
adapter.users.current.mockReturnValue({ id: targetUserId });
const context = await panel._prepareContext();
expect(context.title).toBe('Player Privacy Panel');
expect(context.sectionHeader).toBe('Automation Opt-ins');
expect(context.automationEffects).toBeDefined();
expect(context.automationEffects).toHaveLength(1);
expect(context.isReadOnly).toBe(false);
expect(context.isOwnUser).toBe(true);
});
it('should mark as read-only when viewing another user', async () => {
const otherUserId = 'other-user';
panel = new PlayerPrivacyPanel(adapter, playerPrivacyManager, otherUserId);
adapter.users.current.mockReturnValue({ id: targetUserId });
const context = await panel._prepareContext();
expect(context.isReadOnly).toBe(true);
expect(context.isOwnUser).toBe(false);
});
it('should include the available automation effect', async () => {
const context = await panel._prepareContext();
expect(context.automationEffects).toHaveLength(1);
expect(context.automationEffects[0].key).toBe('reactionCam');
});
it('should reflect current settings in context', async () => {
const settings = createPrivacySettings({
reactionCamEnabled: true,
});
playerPrivacyManager.getSettings.mockReturnValue(settings);
const context = await panel._prepareContext();
expect(context.automationEffects[0].enabled).toBe(true);
});
});
describe('_isReadOnlyMode', () => {
it('should return false when viewing own user', () => {
adapter.users.current.mockReturnValue({ id: targetUserId });
expect(panel._isReadOnlyMode()).toBe(false);
});
it('should return true when viewing another user', () => {
const otherUserId = 'other-user';
panel = new PlayerPrivacyPanel(adapter, playerPrivacyManager, otherUserId);
adapter.users.current.mockReturnValue({ id: targetUserId });
expect(panel._isReadOnlyMode()).toBe(true);
});
it('should return true when current user is null', () => {
adapter.users.current.mockReturnValue(null);
expect(panel._isReadOnlyMode()).toBe(true);
});
it('should return true when current user has no id', () => {
adapter.users.current.mockReturnValue({});
expect(panel._isReadOnlyMode()).toBe(true);
});
});
describe('_onToggleChange', () => {
beforeEach(() => {
panel._currentSettings = createPrivacySettings();
playerPrivacyManager.setSetting.mockClear();
});
it('should revert change in read-only mode', async () => {
const otherUserId = 'other-user';
panel = new PlayerPrivacyPanel(adapter, playerPrivacyManager, otherUserId);
panel._currentSettings = createPrivacySettings();
adapter.users.current.mockReturnValue({ id: targetUserId });
const checkbox = {
checked: true,
type: 'checkbox',
getAttribute: () => 'reactionCamEnabled',
};
const event = { target: checkbox, preventDefault: vi.fn(), stopPropagation: vi.fn() };
await panel._onToggleChange(event);
// Should revert the change
expect(checkbox.checked).toBe(false);
expect(playerPrivacyManager.setSetting).not.toHaveBeenCalled();
});
it('should update setting for own user', async () => {
adapter.users.current.mockReturnValue({ id: targetUserId });
playerPrivacyManager.setSetting.mockResolvedValue(undefined);
const checkbox = {
checked: true,
type: 'checkbox',
getAttribute: () => 'reactionCamEnabled',
};
const event = { target: checkbox, preventDefault: vi.fn(), stopPropagation: vi.fn() };
await panel._onToggleChange(event);
expect(playerPrivacyManager.setSetting).toHaveBeenCalledWith(
targetUserId,
'reactionCamEnabled',
true
);
});
it('should handle missing data-setting attribute', async () => {
adapter.users.current.mockReturnValue({ id: targetUserId });
const checkbox = {
checked: true,
type: 'checkbox',
getAttribute: () => null,
};
const event = { target: checkbox, preventDefault: vi.fn(), stopPropagation: vi.fn() };
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
await panel._onToggleChange(event);
expect(consoleWarnSpy).toHaveBeenCalledWith(
expect.stringContaining('missing data-setting')
);
consoleWarnSpy.mockRestore();
});
it('should handle non-checkbox target', async () => {
const checkbox = {
checked: true,
type: 'radio', // Not checkbox
getAttribute: () => 'reactionCamEnabled',
};
const event = { target: checkbox, preventDefault: vi.fn(), stopPropagation: vi.fn() };
await panel._onToggleChange(event);
expect(playerPrivacyManager.setSetting).not.toHaveBeenCalled();
});
it('should show success notification on successful update', async () => {
adapter.users.current.mockReturnValue({ id: targetUserId });
playerPrivacyManager.setSetting.mockResolvedValue(undefined);
const checkbox = {
checked: true,
type: 'checkbox',
getAttribute: () => 'reactionCamEnabled',
};
const event = { target: checkbox, preventDefault: vi.fn(), stopPropagation: vi.fn() };
await panel._onToggleChange(event);
expect(adapter.notifications.info).toHaveBeenCalledWith(
'Privacy settings saved'
);
});
it('should revert checkbox and show error on TypeError', async () => {
adapter.users.current.mockReturnValue({ id: targetUserId });
playerPrivacyManager.setSetting.mockRejectedValue(
new TypeError('Invalid key')
);
const checkbox = {
checked: true,
type: 'checkbox',
getAttribute: () => 'reactionCamEnabled',
};
const event = { target: checkbox, preventDefault: vi.fn(), stopPropagation: vi.fn() };
await panel._onToggleChange(event);
// Should revert the change
expect(checkbox.checked).toBe(false);
expect(adapter.notifications.error).toHaveBeenCalledWith('Invalid key');
});
it('should revert checkbox and show generic error on other errors', async () => {
adapter.users.current.mockReturnValue({ id: targetUserId });
playerPrivacyManager.setSetting.mockRejectedValue(
new Error('Some other error')
);
const checkbox = {
checked: true,
type: 'checkbox',
getAttribute: () => 'reactionCamEnabled',
};
const event = { target: checkbox, preventDefault: vi.fn(), stopPropagation: vi.fn() };
await panel._onToggleChange(event);
// Should revert the change
expect(checkbox.checked).toBe(false);
expect(adapter.notifications.error).toHaveBeenCalledWith(
'Failed to save privacy settings'
);
});
});
describe('_onClose', () => {
it('should clear cached elements', () => {
// Set up some cached values
panel._reactionCamToggle = document.createElement('div');
panel._currentSettings = createPrivacySettings();
panel._onClose();
expect(panel._reactionCamToggle).toBe(null);
expect(panel._currentSettings).toBe(null);
});
});
});