Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 113 additions & 2 deletions plugin-hrm-form/src/___tests__/maskIdentifiers/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,47 @@

/* eslint-disable camelcase */

import { Manager } from '@twilio/flex-ui';
import { Manager, Notifications } from '@twilio/flex-ui';
import each from 'jest-each';

import { maskConversationServiceUserNames } from '../../maskIdentifiers';
import { maskConversationServiceUserNames, maskChannelStringsWithIdentifiers } from '../../maskIdentifiers';
import { getInitializedCan } from '../../permissions/rules';
import { PermissionActions } from '../../permissions/actions';
import { lookupTranslation } from '../../translations';

jest.mock('../../permissions/rules', () => ({
getInitializedCan: jest.fn(),
}));

jest.mock('../../translations', () => ({
lookupTranslation: jest.fn(),
}));

jest.mock('@twilio/flex-ui', () => ({
Manager: {
getInstance: jest.fn(),
},
Notifications: {
registeredNotifications: new Map(),
},
NotificationIds: {
NewChatMessage: 'NewChatMessage',
IncomingTask: 'IncomingTask',
},
DefaultTaskChannels: {
ChatSms: { name: 'ChatSms' },
},
MessageList: {
Content: { remove: jest.fn() },
},
}));

const mockRegisteredNotifications = Notifications.registeredNotifications as Map<string, any>;

const mockLookupTranslation = lookupTranslation as jest.MockedFunction<typeof lookupTranslation>;

const mockGetInitializedCan = getInitializedCan as jest.MockedFunction<typeof getInitializedCan>;
const mockManagerGetInstance = Manager.getInstance as jest.MockedFunction<typeof Manager.getInstance>;

describe('maskConversationServiceUserNames', () => {
let mockManager: any;
Expand Down Expand Up @@ -391,3 +420,85 @@ describe('maskConversationServiceUserNames', () => {
});
});
});

describe('maskChannelStringsWithIdentifiers', () => {
let mockCan: jest.Mock;

const createChannelType = (name = 'default') => ({
name,
templates: {
IncomingTaskCanvas: { firstLine: '' },
TaskListItem: { firstLine: '', secondLine: '' },
CallCanvas: { firstLine: '' },
TaskCanvasHeader: { title: '' },
Supervisor: {
TaskCanvasHeader: { title: '' },
TaskOverviewCanvas: { firstLine: '' },
},
TaskCard: { firstLine: '' },
},
notifications: {
override: {} as Record<string, (notification: any) => void>,
},
});

beforeEach(() => {
mockCan = jest.fn();
mockGetInitializedCan.mockReturnValue(mockCan);
mockManagerGetInstance.mockReturnValue({ strings: { MaskIdentifiers: 'MASKED' } } as any);
mockLookupTranslation.mockReturnValue('Masked Title');
mockRegisteredNotifications.clear();
mockRegisteredNotifications.set('IncomingTask', {});
});

afterEach(() => {
jest.clearAllMocks();
});

describe('when VIEW_IDENTIFIERS permission is denied', () => {
beforeEach(() => {
mockCan.mockImplementation((action: string) => action !== PermissionActions.VIEW_IDENTIFIERS);
});

test('sets notification override for NewChatMessage', () => {
const channelType = createChannelType();
maskChannelStringsWithIdentifiers(channelType as any);
expect(typeof channelType.notifications.override.NewChatMessage).toBe('function');
});

test('notification override handler sets browser title using lookupTranslation', () => {
const channelType = createChannelType();
maskChannelStringsWithIdentifiers(channelType as any);

const notification = { options: { browser: { title: '' } } };
channelType.notifications.override.NewChatMessage(notification);

expect(mockLookupTranslation).toHaveBeenCalledWith('BrowserNotification-ChatMessage-MaskedTitle');
expect(notification.options.browser.title).toBe('Masked Title');
});

test('deletes IncomingTask notification', () => {
const channelType = createChannelType();
maskChannelStringsWithIdentifiers(channelType as any);
expect(mockRegisteredNotifications.has('IncomingTask')).toBe(false);
});
});

describe('when VIEW_IDENTIFIERS permission is granted', () => {
beforeEach(() => {
mockCan.mockReturnValue(true);
});

test('does not set notification override', () => {
const channelType = createChannelType();
maskChannelStringsWithIdentifiers(channelType as any);
expect(channelType.notifications.override.NewChatMessage).toBeUndefined();
});

test('does not delete IncomingTask notification', () => {
const channelType = createChannelType();
maskChannelStringsWithIdentifiers(channelType as any);
expect(mockRegisteredNotifications.has('IncomingTask')).toBe(true);
});
});
});
39 changes: 37 additions & 2 deletions plugin-hrm-form/src/___tests__/translations/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

import { loadTranslations, initLocalization } from '../../translations';
import { Manager } from '@twilio/flex-ui';

import { loadTranslations, initLocalization, lookupTranslation } from '../../translations';

// Mock translation files at the top level
jest.mock('../../translations/en.json', () => ({}), { virtual: true });
jest.mock('../../translations/en-US.json', () => ({}), { virtual: true });
jest.mock('../../translations/en-GB.json', () => ({}), { virtual: true });

jest.mock('@twilio/flex-ui', () => ({
Manager: {
getInstance: jest.fn(),
},
}));

const mockGetAseloFeatureFlags = jest.fn();
const mockGetHrmConfig = jest.fn();
const mockGetDefinitionVersions = jest.fn();
Expand Down Expand Up @@ -160,3 +167,31 @@ describe('Hierarchical Translations', () => {
});
});
});

describe('lookupTranslation', () => {
const mockManagerGetInstance = Manager.getInstance as jest.MockedFunction<typeof Manager.getInstance>;

afterEach(() => {
jest.clearAllMocks();
});

test('returns compiled string for a key that exists in Manager strings', () => {
mockManagerGetInstance.mockReturnValue({ strings: { MyKey: 'Hello World' } } as any);
expect(lookupTranslation('MyKey')).toBe('Hello World');
});

test('falls back to using the code itself as template when key does not exist in Manager strings', () => {
mockManagerGetInstance.mockReturnValue({ strings: {} } as any);
expect(lookupTranslation('FallbackKey')).toBe('FallbackKey');
});

test('passes parameters to the Handlebars template', () => {
mockManagerGetInstance.mockReturnValue({ strings: { Greeting: 'Hello {{name}}!' } } as any);
expect(lookupTranslation('Greeting', { name: 'World' })).toBe('Hello World!');
});

test('uses empty parameters object by default', () => {
mockManagerGetInstance.mockReturnValue({ strings: { SimpleMsg: 'No params here' } } as any);
expect(lookupTranslation('SimpleMsg')).toBe('No params here');
});
});
24 changes: 18 additions & 6 deletions plugin-hrm-form/src/maskIdentifiers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,32 @@
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
import {
Strings,
TaskChannelDefinition,
MessageList,
StateHelper,
AppState,
ConversationHelper,
TaskHelper,
DefaultTaskChannels,
Manager,
AppState,
MessageList,
Notifications,
NotificationIds,
StateHelper,
Strings,
TaskChannelDefinition,
TaskHelper,
} from '@twilio/flex-ui';
// Weird type to pull in, but I can't see how it can be inferred from the public API, so it's this or 'any' xD
import type { ChatProperties } from '@twilio/flex-ui/src/internal-flex-commons/src';

import { getInitializedCan } from '../permissions/rules';
import { PermissionActions } from '../permissions/actions';
import { lookupTranslation } from '../translations';

const maskNotifications = (channelType: TaskChannelDefinition) => {
channelType.notifications.override[NotificationIds.NewChatMessage] = notification => {
notification.options.browser.title = lookupTranslation('BrowserNotification-ChatMessage-MaskedTitle');
};
// Trying to modify this notification doesn't appear to work so remove it
Notifications.registeredNotifications.delete(NotificationIds.IncomingTask);
};

// Mask identifiers in the channel strings
export const maskChannelStringsWithIdentifiers = (channelType: TaskChannelDefinition) => {
Expand Down Expand Up @@ -80,6 +91,7 @@ export const maskChannelStringsWithIdentifiers = (channelType: TaskChannelDefini
TaskCard.firstLine = 'MaskIdentifiers';

Supervisor.TaskOverviewCanvas.firstLine = 'MaskIdentifiers';
maskNotifications(channelType);
};

// Mask identifiers in the manager strings & messaging canvas
Expand Down
3 changes: 2 additions & 1 deletion plugin-hrm-form/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -660,5 +660,6 @@

"Modals-ConfirmDialog-ConfirmButton": "OK",
"Modals-CloseDialog-CancelButton": "Cancel",
"Modals-CloseDialog-DiscardButton": "Discard"
"Modals-CloseDialog-DiscardButton": "Discard",
"BrowserNotification-ChatMessage-MaskedTitle": "New message"
}
8 changes: 8 additions & 0 deletions plugin-hrm-form/src/translations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
import Handlebars from 'handlebars';
import { Manager } from '@twilio/flex-ui';

import { getDefinitionVersions } from '../hrmConfig';

// default language to initialize plugin
Expand Down Expand Up @@ -117,3 +120,8 @@ export const initLocalization = (localizationConfig: LocalizationConfig, helplin

return { translateUI, getMessage };
};

export const lookupTranslation = (code: string, parameters: Record<string, string> = {}): string => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Add unit tests for this new method in the existing unit test file for this module

const { strings } = Manager.getInstance();
return Handlebars.compile(strings[code] ?? code)(parameters);
};
Loading