diff --git a/package-lock.json b/package-lock.json index 8761edede7d..84640416b6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2022,12 +2022,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", - "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.9.tgz", + "integrity": "sha512-z88xeGxnzehn2sqZ8UdGQEvYErF1odv2CftxInpSYJt6uHuPe9YjahKZITGs3l5LeI9d2ROG+obuDAoSlqbNfQ==", "dependencies": { - "@babel/highlight": "^7.25.7", + "@babel/highlight": "^7.25.9", "picocolors": "^1.0.0" }, "engines": { @@ -2525,19 +2524,17 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", - "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", - "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "engines": { "node": ">=6.9.0" } @@ -2582,12 +2579,11 @@ } }, "node_modules/@babel/highlight": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", - "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.9", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -2597,12 +2593,11 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", - "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz", + "integrity": "sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==", "dependencies": { - "@babel/types": "^7.25.7" + "@babel/types": "^7.25.9" }, "bin": { "parser": "bin/babel-parser.js" @@ -4539,32 +4534,30 @@ } }, "node_modules/@babel/template": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", - "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.7", - "@babel/parser": "^7.25.7", - "@babel/types": "^7.25.7" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", - "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.7", - "@babel/generator": "^7.25.7", - "@babel/parser": "^7.25.7", - "@babel/template": "^7.25.7", - "@babel/types": "^7.25.7", + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -4573,13 +4566,12 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", - "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.9.tgz", + "integrity": "sha512-omlUGkr5EaoIJrhLf9CJ0TvjBRpd9+AXRG//0GEQ9THSo8wPiTlbpy1/Ow8ZTrbXpjd9FHXfbFQx32I04ht0FA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.25.7", + "@babel/types": "^7.25.9", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -4593,7 +4585,6 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, - "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -4602,14 +4593,12 @@ } }, "node_modules/@babel/types": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", - "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz", + "integrity": "sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ==", "dependencies": { - "@babel/helper-string-parser": "^7.25.7", - "@babel/helper-validator-identifier": "^7.25.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -50496,6 +50485,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, "engines": { "node": ">=4" } diff --git a/packages/quantic/cypress/e2e/default-1/notifications/notifications.cypress.ts b/packages/quantic/cypress/e2e/default-1/notifications/notifications.cypress.ts index b0158daa091..f64e44fe7dd 100644 --- a/packages/quantic/cypress/e2e/default-1/notifications/notifications.cypress.ts +++ b/packages/quantic/cypress/e2e/default-1/notifications/notifications.cypress.ts @@ -1,41 +1,59 @@ import {configure} from '../../../page-objects/configurator'; +import {performSearch} from '../../../page-objects/actions/action-perform-search'; import { getQueryAlias, interceptSearch, mockSearchWithNotifyTrigger, } from '../../../page-objects/search'; import {NotificationsExpectations as Expect} from './notifications-expectations'; - +import { + useCaseParamTest, + useCaseEnum, + InsightInterfaceExpectations as InsightInterfaceExpect, +} from '../../../page-objects/use-case'; const exampleNotifications = ['Notification one', 'Notification two']; +interface NotificationsOptions { + useCase: string; +} + describe('quantic-notifications', () => { const pageUrl = 's/quantic-notifications'; - function visitNotifications() { + function visitNotifications(options: Partial) { interceptSearch(); cy.visit(pageUrl); - configure(); + configure(options); + if (options.useCase === useCaseEnum.insight) { + InsightInterfaceExpect.isInitialized(); + performSearch(); + } } - describe('when no notification is fired by the pipeline trigger', () => { - it('should not render any notification', () => { - visitNotifications(); + useCaseParamTest.forEach((param) => { + describe(param.label, () => { + describe('when no notification is fired by the pipeline trigger', () => { + it('should not render any notification', () => { + visitNotifications({useCase: param.useCase}); + mockSearchWithNotifyTrigger(param.useCase, []); - cy.wait(getQueryAlias()); - Expect.displayNotifications(false); - }); - }); + cy.wait(getQueryAlias(param.useCase)); + Expect.displayNotifications(false); + }); + }); - describe('when some notifications are fired by the pipeline trigger', () => { - it('should render the notifications', () => { - visitNotifications(); - mockSearchWithNotifyTrigger('search', exampleNotifications); + describe('when some notifications are fired by the pipeline trigger', () => { + it('should render the notifications', () => { + mockSearchWithNotifyTrigger(param.useCase, exampleNotifications); + visitNotifications({useCase: param.useCase}); - cy.wait(getQueryAlias()); - Expect.displayNotifications(true); - Expect.logQueryPipelineTriggersNotification(exampleNotifications); - exampleNotifications.forEach((notification, index) => { - Expect.notificationContains(index, notification); + cy.wait(getQueryAlias(param.useCase)); + Expect.displayNotifications(true); + Expect.logQueryPipelineTriggersNotification(exampleNotifications); + exampleNotifications.forEach((notification, index) => { + Expect.notificationContains(index, notification); + }); + }); }); }); }); diff --git a/packages/quantic/cypress/page-objects/search.ts b/packages/quantic/cypress/page-objects/search.ts index ddec07b06e4..d55f7c943b4 100644 --- a/packages/quantic/cypress/page-objects/search.ts +++ b/packages/quantic/cypress/page-objects/search.ts @@ -645,15 +645,20 @@ export function mockSearchWithNotifyTrigger( useCase: string, notifications: string[] ) { + const InterceptAliasesToUse = + useCase === useCaseEnum.insight + ? InterceptAliases.Insight + : InterceptAliases.Search; + cy.intercept(getRoute(useCase), (req) => { req.continue((res) => { - res.body.triggers = notifications.map((notification) => ({ + res.body.triggers = notifications?.map((notification) => ({ type: 'notify', content: notification, })); res.send(); }); - }).as(InterceptAliases.Search.substring(1)); + }).as(InterceptAliasesToUse.substring(1)); } export function mockQuerySuggestions(suggestions: string[]) { diff --git a/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.html b/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.html index dbe325ec081..a2220d9c6bd 100644 --- a/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.html +++ b/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.html @@ -3,11 +3,12 @@
+
- + - + \ No newline at end of file diff --git a/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.js b/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.js index 1bc1e63e32e..75daa7f1688 100644 --- a/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.js +++ b/packages/quantic/force-app/examples/main/lwc/exampleQuanticNotifications/exampleQuanticNotifications.js @@ -8,7 +8,15 @@ export default class ExampleQuanticNotifications extends LightningElement { pageTitle = 'Quantic Notifications'; pageDescription = 'component is responsible for displaying notifications generated by the Coveo Search API.'; - options = []; + options = [ + { + attribute: 'useCase', + label: 'Use Case', + description: + 'Define which use case to test. Possible values are: search, insights', + defaultValue: 'search', + }, + ]; get notConfigured() { return !this.isConfigured; diff --git a/packages/quantic/force-app/main/default/lwc/quanticNotifications/__tests__/quanticNotifications.test.js b/packages/quantic/force-app/main/default/lwc/quanticNotifications/__tests__/quanticNotifications.test.js new file mode 100644 index 00000000000..bd8e14e7d3b --- /dev/null +++ b/packages/quantic/force-app/main/default/lwc/quanticNotifications/__tests__/quanticNotifications.test.js @@ -0,0 +1,227 @@ +/* eslint-disable no-import-assign */ +// @ts-ignore +import QuanticNotifications from 'c/quanticNotifications'; +// @ts-ignore +import {createElement} from 'lwc'; +import * as mockHeadlessLoader from 'c/quanticHeadlessLoader'; +import {AriaLiveRegion} from 'c/quanticUtils'; + +jest.mock('c/quanticHeadlessLoader'); +jest.mock('c/quanticUtils'); + +const exampleNotifications = ['notification1', 'notification2']; + +let notificationsState = { + notifications: exampleNotifications, +}; +let isInitialized = false; + +const exampleEngine = { + id: 'mock engine', +}; + +const functionsMocks = { + buildNotifyTrigger: jest.fn(() => ({ + state: notificationsState, + subscribe: functionsMocks.subscribe, + })), + dispatchMessage: jest.fn(() => {}), + subscribe: jest.fn((cb) => { + cb(); + return functionsMocks.unsubscribe; + }), + unsubscribe: jest.fn(() => {}), +}; + +// @ts-ignore +AriaLiveRegion.mockImplementation(() => { + return { + dispatchMessage: functionsMocks.dispatchMessage, + }; +}); + +const selectors = { + notifications: '[data-test="notification"]', + initializationError: 'c-quantic-component-error', +}; + +const defaultOptions = { + engineId: 'exampleEngineId', +}; + +function createTestComponent(options = defaultOptions) { + prepareHeadlessState(); + + const element = createElement('c-quantic-notifications', { + is: QuanticNotifications, + }); + for (const [key, value] of Object.entries(options)) { + element[key] = value; + } + + document.body.appendChild(element); + return element; +} + +function prepareHeadlessState() { + // @ts-ignore + mockHeadlessLoader.getHeadlessBundle = () => { + return { + buildNotifyTrigger: functionsMocks.buildNotifyTrigger, + }; + }; +} + +// Helper function to wait until the microtask queue is empty. +function flushPromises() { + // eslint-disable-next-line @lwc/lwc/no-async-operation + return new Promise((resolve) => setTimeout(resolve, 0)); +} + +function mockSuccessfulHeadlessInitialization() { + // @ts-ignore + mockHeadlessLoader.initializeWithHeadless = (element, _, initialize) => { + if (element instanceof QuanticNotifications && !isInitialized) { + isInitialized = true; + initialize(exampleEngine); + } + }; +} + +function mockErroneousHeadlessInitialization() { + // @ts-ignore + mockHeadlessLoader.initializeWithHeadless = (element) => { + if (element instanceof QuanticNotifications) { + element.setInitializationError(); + } + }; +} + +function cleanup() { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + jest.clearAllMocks(); + isInitialized = false; +} + +describe('c-quantic-notifications', () => { + beforeAll(() => { + mockSuccessfulHeadlessInitialization(); + }); + + afterEach(() => { + cleanup(); + notificationsState = { + notifications: exampleNotifications, + }; + }); + + describe('when an error occurs during initialization', () => { + beforeEach(() => { + mockErroneousHeadlessInitialization(); + }); + + afterAll(() => { + mockSuccessfulHeadlessInitialization(); + }); + + it('should display the initialization error component', async () => { + const element = createTestComponent(); + await flushPromises(); + + const initializationError = element.shadowRoot.querySelector( + selectors.initializationError + ); + + const notification = element.shadowRoot.querySelector( + selectors.notifications + ); + + expect(initializationError).not.toBeNull(); + expect(notification).toBeNull(); + }); + }); + + describe('component initialization', () => { + it('should build the controller with the proper paramters', async () => { + createTestComponent(); + await flushPromises(); + + expect(functionsMocks.buildNotifyTrigger).toHaveBeenCalledTimes(1); + expect(functionsMocks.buildNotifyTrigger).toHaveBeenCalledWith( + exampleEngine + ); + }); + + it('should subscribe to the headless state changes', async () => { + createTestComponent(); + await flushPromises(); + + expect(functionsMocks.subscribe).toHaveBeenCalledTimes(1); + }); + + it('should call AriaLiveRegion with the right parameters', async () => { + await createTestComponent(); + await flushPromises(); + + expect(AriaLiveRegion).toHaveBeenCalledTimes(1); + expect(AriaLiveRegion).toHaveBeenCalledWith( + 'notifications', + expect.anything() + ); + }); + }); + + describe('when the component is initialized successfully', () => { + describe('when some notifications are present in the state', () => { + it('should render the notifications component', async () => { + const element = createTestComponent(); + await flushPromises(); + + const notifications = element.shadowRoot.querySelectorAll( + selectors.notifications + ); + + expect(notifications).not.toBeNull(); + expect(notifications.length).toBe(exampleNotifications.length); + notifications.forEach((notification, index) => { + expect(notification.textContent).toEqual(exampleNotifications[index]); + }); + }); + + it('should call dispatchMessage with the correct message', async () => { + await createTestComponent(); + await flushPromises(); + + const expectedMessage = + ' Notification 1: notification1 Notification 2: notification2'; + + expect(functionsMocks.dispatchMessage).toHaveBeenCalledTimes(1); + expect(functionsMocks.dispatchMessage).toHaveBeenCalledWith( + expectedMessage + ); + }); + }); + + describe('when no notifications are present in the state', () => { + beforeEach(() => { + notificationsState = { + notifications: [], + }; + }); + + it('should not render the notifications component', async () => { + const element = createTestComponent(); + await flushPromises(); + + const notifications = element.shadowRoot.querySelectorAll( + selectors.notifications + ); + + expect(notifications.length).toEqual(0); + }); + }); + }); +}); diff --git a/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html b/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html index 7a8742ef624..6b1621ec078 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html +++ b/packages/quantic/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html @@ -5,7 +5,7 @@