360 lines
12 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|