Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
export const getAssignee = message => message?.conversation?.assignee_id;
|
||||
export const isConversationUnassigned = message => !getAssignee(message);
|
||||
export const isConversationAssignedToMe = (message, currentUserId) =>
|
||||
getAssignee(message) === currentUserId;
|
||||
export const isMessageFromCurrentUser = (message, currentUserId) =>
|
||||
message?.sender?.id === currentUserId;
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions';
|
||||
import { getUserPermissions } from 'dashboard/helper/permissionsHelper';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
class AudioNotificationStore {
|
||||
constructor(store) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
hasUnreadConversation = () => {
|
||||
const mineConversation = this.store.getters.getMineChats({
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
});
|
||||
|
||||
return mineConversation.some(conv => conv.unread_count > 0);
|
||||
};
|
||||
|
||||
isMessageFromPendingConversation = (message = {}) => {
|
||||
const { conversation_id: conversationId } = message || {};
|
||||
if (!conversationId) return false;
|
||||
|
||||
const activeConversation =
|
||||
this.store.getters.getConversationById(conversationId);
|
||||
return activeConversation?.status === wootConstants.STATUS_TYPE.PENDING;
|
||||
};
|
||||
|
||||
isMessageFromCurrentConversation = message => {
|
||||
return this.store.getters.getSelectedChat?.id === message.conversation_id;
|
||||
};
|
||||
|
||||
hasConversationPermission = user => {
|
||||
const currentAccountId = this.store.getters.getCurrentAccountId;
|
||||
// Get the user permissions for the current account
|
||||
const userPermissions = getUserPermissions(user, currentAccountId);
|
||||
// Check if the user has the required permissions
|
||||
const hasRequiredPermission = [...ROLES, ...CONVERSATION_PERMISSIONS].some(
|
||||
permission => userPermissions.includes(permission)
|
||||
);
|
||||
return hasRequiredPermission;
|
||||
};
|
||||
}
|
||||
|
||||
export default AudioNotificationStore;
|
||||
@@ -0,0 +1,217 @@
|
||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||
import { showBadgeOnFavicon } from './faviconHelper';
|
||||
import { initFaviconSwitcher } from './faviconHelper';
|
||||
|
||||
import { EVENT_TYPES } from 'dashboard/routes/dashboard/settings/profile/constants.js';
|
||||
import GlobalStore from 'dashboard/store';
|
||||
import AudioNotificationStore from './AudioNotificationStore';
|
||||
import {
|
||||
isConversationAssignedToMe,
|
||||
isConversationUnassigned,
|
||||
isMessageFromCurrentUser,
|
||||
} from './AudioMessageHelper';
|
||||
import WindowVisibilityHelper from './WindowVisibilityHelper';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
const NOTIFICATION_TIME = 30000;
|
||||
const ALERT_DURATION = 10000;
|
||||
const ALERT_PATH_PREFIX = '/audio/dashboard/';
|
||||
const DEFAULT_TONE = 'ding';
|
||||
const DEFAULT_ALERT_TYPE = ['none'];
|
||||
|
||||
export class DashboardAudioNotificationHelper {
|
||||
constructor(store) {
|
||||
if (!store) {
|
||||
throw new Error('store is required');
|
||||
}
|
||||
this.store = new AudioNotificationStore(store);
|
||||
|
||||
this.notificationConfig = {
|
||||
audioAlertType: DEFAULT_ALERT_TYPE,
|
||||
playAlertOnlyWhenHidden: true,
|
||||
alertIfUnreadConversationExist: false,
|
||||
};
|
||||
|
||||
this.recurringNotificationTimer = null;
|
||||
|
||||
this.audioConfig = {
|
||||
audio: null,
|
||||
tone: DEFAULT_TONE,
|
||||
hasSentSoundPermissionsRequest: false,
|
||||
};
|
||||
|
||||
this.currentUser = null;
|
||||
}
|
||||
|
||||
intializeAudio = () => {
|
||||
const resourceUrl = `${ALERT_PATH_PREFIX}${this.audioConfig.tone}.mp3`;
|
||||
this.audioConfig.audio = new Audio(resourceUrl);
|
||||
return this.audioConfig.audio.load();
|
||||
};
|
||||
|
||||
playAudioAlert = async () => {
|
||||
try {
|
||||
await this.audioConfig.audio.play();
|
||||
} catch (error) {
|
||||
if (
|
||||
error.name === 'NotAllowedError' &&
|
||||
!this.hasSentSoundPermissionsRequest
|
||||
) {
|
||||
this.hasSentSoundPermissionsRequest = true;
|
||||
useAlert(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.SOUND_PERMISSION_ERROR',
|
||||
{ usei18n: true, duration: ALERT_DURATION }
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
set = ({
|
||||
currentUser,
|
||||
alwaysPlayAudioAlert,
|
||||
alertIfUnreadConversationExist,
|
||||
audioAlertType = DEFAULT_ALERT_TYPE,
|
||||
audioAlertTone = DEFAULT_TONE,
|
||||
}) => {
|
||||
this.notificationConfig = {
|
||||
...this.notificationConfig,
|
||||
audioAlertType: audioAlertType.split('+').filter(Boolean),
|
||||
playAlertOnlyWhenHidden: !alwaysPlayAudioAlert,
|
||||
alertIfUnreadConversationExist: alertIfUnreadConversationExist,
|
||||
};
|
||||
|
||||
this.currentUser = currentUser;
|
||||
|
||||
const previousAudioTone = this.audioConfig.tone;
|
||||
this.audioConfig = {
|
||||
...this.audioConfig,
|
||||
tone: audioAlertTone,
|
||||
};
|
||||
|
||||
if (previousAudioTone !== audioAlertTone) {
|
||||
this.intializeAudio();
|
||||
}
|
||||
|
||||
initFaviconSwitcher();
|
||||
this.clearRecurringTimer();
|
||||
this.playAudioEvery30Seconds();
|
||||
};
|
||||
|
||||
shouldPlayAlert = () => {
|
||||
if (this.notificationConfig.playAlertOnlyWhenHidden) {
|
||||
return !WindowVisibilityHelper.isWindowVisible();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
executeRecurringNotification = () => {
|
||||
if (this.store.hasUnreadConversation() && this.shouldPlayAlert()) {
|
||||
this.playAudioAlert();
|
||||
showBadgeOnFavicon();
|
||||
}
|
||||
this.resetRecurringTimer();
|
||||
};
|
||||
|
||||
clearRecurringTimer = () => {
|
||||
if (this.recurringNotificationTimer) {
|
||||
clearTimeout(this.recurringNotificationTimer);
|
||||
}
|
||||
};
|
||||
|
||||
resetRecurringTimer = () => {
|
||||
this.clearRecurringTimer();
|
||||
this.recurringNotificationTimer = setTimeout(
|
||||
this.executeRecurringNotification,
|
||||
NOTIFICATION_TIME
|
||||
);
|
||||
};
|
||||
|
||||
playAudioEvery30Seconds = () => {
|
||||
const { audioAlertType, alertIfUnreadConversationExist } =
|
||||
this.notificationConfig;
|
||||
|
||||
// Audio alert is disabled dismiss the timer
|
||||
if (audioAlertType.includes('none')) return;
|
||||
|
||||
// If unread conversation flag is disabled, dismiss the timer
|
||||
if (!alertIfUnreadConversationExist) return;
|
||||
|
||||
this.resetRecurringTimer();
|
||||
};
|
||||
|
||||
shouldNotifyOnMessage = message => {
|
||||
const { audioAlertType } = this.notificationConfig;
|
||||
if (audioAlertType.includes('none')) return false;
|
||||
if (audioAlertType.includes('all')) return true;
|
||||
|
||||
const assignedToMe = isConversationAssignedToMe(
|
||||
message,
|
||||
this.currentUser.id
|
||||
);
|
||||
const isUnassigned = isConversationUnassigned(message);
|
||||
|
||||
const shouldPlayAudio = [];
|
||||
|
||||
if (
|
||||
audioAlertType.includes(EVENT_TYPES.ASSIGNED) ||
|
||||
audioAlertType.includes('mine')
|
||||
) {
|
||||
shouldPlayAudio.push(assignedToMe);
|
||||
}
|
||||
if (audioAlertType.includes(EVENT_TYPES.UNASSIGNED)) {
|
||||
shouldPlayAudio.push(isUnassigned);
|
||||
}
|
||||
if (audioAlertType.includes(EVENT_TYPES.NOTME)) {
|
||||
shouldPlayAudio.push(!isUnassigned && !assignedToMe);
|
||||
}
|
||||
|
||||
return shouldPlayAudio.some(Boolean);
|
||||
};
|
||||
|
||||
onNewMessage = message => {
|
||||
// If the user does not have the permission to view the conversation, then dismiss the alert
|
||||
// FIX ME: There shouldn't be a new message if the user has no access to the conversation.
|
||||
if (!this.store.hasConversationPermission(this.currentUser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the conversation status is pending, then dismiss the alert
|
||||
// This case is common for all audio event types
|
||||
if (this.store.isMessageFromPendingConversation(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the message is sent by the current user then dismiss the alert
|
||||
if (isMessageFromCurrentUser(message, this.currentUser.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.shouldNotifyOnMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the message type is not incoming or private, then dismiss the alert
|
||||
const { message_type: messageType, private: isPrivate } = message;
|
||||
if (messageType !== MESSAGE_TYPE.INCOMING && !isPrivate) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (WindowVisibilityHelper.isWindowVisible()) {
|
||||
// If the user looking at the conversation, then dismiss the alert
|
||||
if (this.store.isMessageFromCurrentConversation(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user has disabled alerts when active on the dashboard, the dismiss the alert
|
||||
if (this.notificationConfig.playAlertOnlyWhenHidden) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.playAudioAlert();
|
||||
showBadgeOnFavicon();
|
||||
this.playAudioEvery30Seconds();
|
||||
};
|
||||
}
|
||||
|
||||
export default new DashboardAudioNotificationHelper(GlobalStore);
|
||||
@@ -0,0 +1,21 @@
|
||||
export class WindowVisibilityHelper {
|
||||
constructor() {
|
||||
this.isVisible = true;
|
||||
this.initializeEvent();
|
||||
}
|
||||
|
||||
initializeEvent = () => {
|
||||
window.addEventListener('blur', () => {
|
||||
this.isVisible = false;
|
||||
});
|
||||
window.addEventListener('focus', () => {
|
||||
this.isVisible = true;
|
||||
});
|
||||
};
|
||||
|
||||
isWindowVisible() {
|
||||
return !document.hidden && this.isVisible;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WindowVisibilityHelper();
|
||||
@@ -0,0 +1,21 @@
|
||||
export const showBadgeOnFavicon = () => {
|
||||
const favicons = document.querySelectorAll('.favicon');
|
||||
|
||||
favicons.forEach(favicon => {
|
||||
const newFileName = `/favicon-badge-${favicon.sizes[[0]]}.png`;
|
||||
favicon.href = newFileName;
|
||||
});
|
||||
};
|
||||
|
||||
export const initFaviconSwitcher = () => {
|
||||
const favicons = document.querySelectorAll('.favicon');
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
favicons.forEach(favicon => {
|
||||
const oldFileName = `/favicon-${favicon.sizes[[0]]}.png`;
|
||||
favicon.href = oldFileName;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
getAssignee,
|
||||
isConversationUnassigned,
|
||||
isConversationAssignedToMe,
|
||||
isMessageFromCurrentUser,
|
||||
} from '../AudioMessageHelper';
|
||||
|
||||
describe('getAssignee', () => {
|
||||
it('should return assignee_id when present', () => {
|
||||
const message = { conversation: { assignee_id: 1 } };
|
||||
expect(getAssignee(message)).toBe(1);
|
||||
});
|
||||
|
||||
it('should return undefined when no assignee_id', () => {
|
||||
const message = { conversation: null };
|
||||
expect(getAssignee(message)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle null message', () => {
|
||||
expect(getAssignee(null)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConversationUnassigned', () => {
|
||||
it('should return true when no assignee', () => {
|
||||
const message = { conversation: { assignee_id: null } };
|
||||
expect(isConversationUnassigned(message)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when has assignee', () => {
|
||||
const message = { conversation: { assignee_id: 1 } };
|
||||
expect(isConversationUnassigned(message)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null message', () => {
|
||||
expect(isConversationUnassigned(null)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConversationAssignedToMe', () => {
|
||||
const currentUserId = 1;
|
||||
|
||||
it('should return true when assigned to current user', () => {
|
||||
const message = { conversation: { assignee_id: 1 } };
|
||||
expect(isConversationAssignedToMe(message, currentUserId)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when assigned to different user', () => {
|
||||
const message = { conversation: { assignee_id: 2 } };
|
||||
expect(isConversationAssignedToMe(message, currentUserId)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when unassigned', () => {
|
||||
const message = { conversation: { assignee_id: null } };
|
||||
expect(isConversationAssignedToMe(message, currentUserId)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null message', () => {
|
||||
expect(isConversationAssignedToMe(null, currentUserId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMessageFromCurrentUser', () => {
|
||||
const currentUserId = 1;
|
||||
|
||||
it('should return true when message is from current user', () => {
|
||||
const message = { sender: { id: 1 } };
|
||||
expect(isMessageFromCurrentUser(message, currentUserId)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when message is from different user', () => {
|
||||
const message = { sender: { id: 2 } };
|
||||
expect(isMessageFromCurrentUser(message, currentUserId)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null message', () => {
|
||||
expect(isMessageFromCurrentUser(null, currentUserId)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
import AudioNotificationStore from '../AudioNotificationStore';
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions';
|
||||
import { getUserPermissions } from 'dashboard/helper/permissionsHelper';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
|
||||
vi.mock('dashboard/helper/permissionsHelper', () => ({
|
||||
getUserPermissions: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('AudioNotificationStore', () => {
|
||||
let store;
|
||||
let audioNotificationStore;
|
||||
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
getters: {
|
||||
getMineChats: vi.fn(),
|
||||
getSelectedChat: null,
|
||||
getCurrentAccountId: 1,
|
||||
getConversationById: vi.fn(),
|
||||
},
|
||||
};
|
||||
audioNotificationStore = new AudioNotificationStore(store);
|
||||
});
|
||||
|
||||
describe('hasUnreadConversation', () => {
|
||||
it('should return true when there are unread conversations', () => {
|
||||
store.getters.getMineChats.mockReturnValue([
|
||||
{ id: 1, unread_count: 2 },
|
||||
{ id: 2, unread_count: 0 },
|
||||
]);
|
||||
|
||||
expect(audioNotificationStore.hasUnreadConversation()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when there are no unread conversations', () => {
|
||||
store.getters.getMineChats.mockReturnValue([
|
||||
{ id: 1, unread_count: 0 },
|
||||
{ id: 2, unread_count: 0 },
|
||||
]);
|
||||
|
||||
expect(audioNotificationStore.hasUnreadConversation()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when there are no conversations', () => {
|
||||
store.getters.getMineChats.mockReturnValue([]);
|
||||
|
||||
expect(audioNotificationStore.hasUnreadConversation()).toBe(false);
|
||||
});
|
||||
|
||||
it('should call getMineChats with correct parameters', () => {
|
||||
store.getters.getMineChats.mockReturnValue([]);
|
||||
audioNotificationStore.hasUnreadConversation();
|
||||
|
||||
expect(store.getters.getMineChats).toHaveBeenCalledWith({
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMessageFromPendingConversation', () => {
|
||||
it('should return true when conversation status is pending', () => {
|
||||
store.getters.getConversationById.mockReturnValue({
|
||||
id: 123,
|
||||
status: wootConstants.STATUS_TYPE.PENDING,
|
||||
});
|
||||
const message = { conversation_id: 123 };
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromPendingConversation(message)
|
||||
).toBe(true);
|
||||
expect(store.getters.getConversationById).toHaveBeenCalledWith(123);
|
||||
});
|
||||
|
||||
it('should return false when conversation status is not pending', () => {
|
||||
store.getters.getConversationById.mockReturnValue({
|
||||
id: 123,
|
||||
status: wootConstants.STATUS_TYPE.OPEN,
|
||||
});
|
||||
const message = { conversation_id: 123 };
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromPendingConversation(message)
|
||||
).toBe(false);
|
||||
expect(store.getters.getConversationById).toHaveBeenCalledWith(123);
|
||||
});
|
||||
|
||||
it('should return false when conversation is not found', () => {
|
||||
store.getters.getConversationById.mockReturnValue(null);
|
||||
const message = { conversation_id: 123 };
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromPendingConversation(message)
|
||||
).toBe(false);
|
||||
expect(store.getters.getConversationById).toHaveBeenCalledWith(123);
|
||||
});
|
||||
|
||||
it('should return false when message has no conversation_id', () => {
|
||||
const message = {};
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromPendingConversation(message)
|
||||
).toBe(false);
|
||||
expect(store.getters.getConversationById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false when message is null or undefined', () => {
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromPendingConversation(null)
|
||||
).toBe(false);
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromPendingConversation(undefined)
|
||||
).toBe(false);
|
||||
expect(store.getters.getConversationById).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMessageFromCurrentConversation', () => {
|
||||
it('should return true when message is from selected chat', () => {
|
||||
store.getters.getSelectedChat = { id: 6179 };
|
||||
const message = { conversation_id: 6179 };
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromCurrentConversation(message)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when message is from different chat', () => {
|
||||
store.getters.getSelectedChat = { id: 6179 };
|
||||
const message = { conversation_id: 1337 };
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromCurrentConversation(message)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no chat is selected', () => {
|
||||
store.getters.getSelectedChat = null;
|
||||
const message = { conversation_id: 6179 };
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromCurrentConversation(message)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasConversationPermission', () => {
|
||||
const mockUser = { id: 'user123' };
|
||||
|
||||
beforeEach(() => {
|
||||
getUserPermissions.mockReset();
|
||||
});
|
||||
|
||||
it('should return true when user has a required role', () => {
|
||||
getUserPermissions.mockReturnValue([ROLES[0]]);
|
||||
|
||||
expect(audioNotificationStore.hasConversationPermission(mockUser)).toBe(
|
||||
true
|
||||
);
|
||||
expect(getUserPermissions).toHaveBeenCalledWith(mockUser, 1);
|
||||
});
|
||||
|
||||
it('should return true when user has a conversation permission', () => {
|
||||
getUserPermissions.mockReturnValue([CONVERSATION_PERMISSIONS[0]]);
|
||||
|
||||
expect(audioNotificationStore.hasConversationPermission(mockUser)).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when user has no required permissions', () => {
|
||||
getUserPermissions.mockReturnValue(['some-other-permission']);
|
||||
|
||||
expect(audioNotificationStore.hasConversationPermission(mockUser)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when user has no permissions', () => {
|
||||
getUserPermissions.mockReturnValue([]);
|
||||
|
||||
expect(audioNotificationStore.hasConversationPermission(mockUser)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { WindowVisibilityHelper } from '../WindowVisibilityHelper';
|
||||
|
||||
describe('WindowVisibilityHelper', () => {
|
||||
let blurCallback;
|
||||
let focusCallback;
|
||||
let windowEventListeners;
|
||||
let documentHiddenValue = false;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.resetAllMocks();
|
||||
|
||||
// Reset event listeners before each test
|
||||
windowEventListeners = {};
|
||||
|
||||
// Mock window.addEventListener
|
||||
window.addEventListener = vi.fn((event, callback) => {
|
||||
windowEventListeners[event] = callback;
|
||||
if (event === 'blur') blurCallback = callback;
|
||||
if (event === 'focus') focusCallback = callback;
|
||||
});
|
||||
|
||||
// Mock document.hidden with a getter that returns our controlled value
|
||||
Object.defineProperty(document, 'hidden', {
|
||||
configurable: true,
|
||||
get: () => documentHiddenValue,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
documentHiddenValue = false;
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should add blur and focus event listeners', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
expect(helper.isVisible).toBe(true);
|
||||
|
||||
expect(window.addEventListener).toHaveBeenCalledTimes(2);
|
||||
expect(window.addEventListener).toHaveBeenCalledWith(
|
||||
'blur',
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(window.addEventListener).toHaveBeenCalledWith(
|
||||
'focus',
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('window events', () => {
|
||||
it('should set isVisible to false on blur', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
blurCallback();
|
||||
expect(helper.isVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('should set isVisible to true on focus', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
blurCallback(); // First blur the window
|
||||
focusCallback(); // Then focus it
|
||||
expect(helper.isVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle multiple blur/focus events', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
|
||||
blurCallback();
|
||||
expect(helper.isVisible).toBe(false);
|
||||
|
||||
focusCallback();
|
||||
expect(helper.isVisible).toBe(true);
|
||||
|
||||
blurCallback();
|
||||
expect(helper.isVisible).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isWindowVisible', () => {
|
||||
it('should return true when document is visible and window is focused', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
documentHiddenValue = false;
|
||||
helper.isVisible = true;
|
||||
|
||||
expect(helper.isWindowVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when document is hidden', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
documentHiddenValue = true;
|
||||
helper.isVisible = true;
|
||||
|
||||
expect(helper.isWindowVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when window is not focused', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
documentHiddenValue = false;
|
||||
helper.isVisible = false;
|
||||
|
||||
expect(helper.isWindowVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when both document is hidden and window is not focused', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
documentHiddenValue = true;
|
||||
helper.isVisible = false;
|
||||
|
||||
expect(helper.isWindowVisible()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user