@coveo/quantic 3.10.0-pre.faaf12d7a7 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +52 -12
  2. package/force-app/main/default/labels/CustomLabels.labels-meta.xml +7 -0
  3. package/force-app/main/default/lwc/quanticCaseAssistInterface/quanticCaseAssistInterface.js +8 -0
  4. package/force-app/main/default/lwc/quanticCategoryFacet/__tests__/quanticCategoryFacet.test.js +108 -1
  5. package/force-app/main/default/lwc/quanticCategoryFacet/quanticCategoryFacet.js +22 -0
  6. package/force-app/main/default/lwc/quanticDateFacet/__tests__/quanticDateFacet.test.js +107 -1
  7. package/force-app/main/default/lwc/quanticDateFacet/quanticDateFacet.js +22 -0
  8. package/force-app/main/default/lwc/quanticFacet/__tests__/quanticFacet.test.js +106 -1
  9. package/force-app/main/default/lwc/quanticFacet/quanticFacet.js +21 -1
  10. package/force-app/main/default/lwc/quanticFeedbackModalQna/__tests__/quanticFeedbackModalQna.test.js +1 -1
  11. package/force-app/main/default/lwc/quanticFeedbackModalQna/quanticFeedbackModalQna.js +1 -1
  12. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.css +0 -1
  13. package/force-app/main/default/lwc/quanticGeneratedAnswerContent/templates/generatedMarkdownContent.css +1 -0
  14. package/force-app/main/default/lwc/quanticInsightInterface/quanticInsightInterface.js +8 -0
  15. package/force-app/main/default/lwc/quanticNotifications/__tests__/quanticNotifications.test.js +112 -4
  16. package/force-app/main/default/lwc/quanticNotifications/quanticNotifications.css +8 -0
  17. package/force-app/main/default/lwc/quanticNotifications/quanticNotifications.html +20 -7
  18. package/force-app/main/default/lwc/quanticNotifications/quanticNotifications.js +57 -18
  19. package/force-app/main/default/lwc/quanticNumericFacet/__tests__/quanticNumericFacet.test.js +107 -1
  20. package/force-app/main/default/lwc/quanticNumericFacet/quanticNumericFacet.js +22 -0
  21. package/force-app/main/default/lwc/quanticRecommendationInterface/quanticRecommendationInterface.js +8 -0
  22. package/force-app/main/default/lwc/quanticSearchInterface/quanticSearchInterface.js +8 -0
  23. package/force-app/main/default/lwc/quanticTimeframeFacet/__tests__/quanticTimeframeFacet.test.js +107 -1
  24. package/force-app/main/default/lwc/quanticTimeframeFacet/quanticTimeframeFacet.js +22 -0
  25. package/force-app/main/default/lwc/quanticUserActionsToggle/__tests__/quanticUserActionsToggle.test.js +18 -0
  26. package/force-app/main/default/lwc/quanticUserActionsToggle/e2e/data.ts +32 -0
  27. package/force-app/main/default/lwc/quanticUserActionsToggle/e2e/fixture.ts +32 -0
  28. package/force-app/main/default/lwc/quanticUserActionsToggle/e2e/pageObject.ts +71 -0
  29. package/force-app/main/default/lwc/quanticUserActionsToggle/e2e/quanticUserActionsToggle.e2e.ts +39 -0
  30. package/force-app/main/default/staticresources/coveobueno/browser/bueno.js +3 -18
  31. package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +9 -6
  32. package/force-app/main/default/staticresources/coveoheadless/definitions/api/commerce/common/request.d.ts +1 -1
  33. package/force-app/main/default/staticresources/coveoheadless/definitions/api/commerce/recommendations/recommendations-request.d.ts +1 -1
  34. package/force-app/main/default/staticresources/coveoheadless/definitions/api/commerce/search/query-suggest/query-suggest-request.d.ts +1 -1
  35. package/force-app/main/default/staticresources/coveoheadless/definitions/api/knowledge/stream-answer-api.d.ts +1 -1
  36. package/force-app/main/default/staticresources/coveoheadless/definitions/api/service/case-assist/get-case-classifications/get-case-classifications-request.d.ts +1 -1
  37. package/force-app/main/default/staticresources/coveoheadless/definitions/api/service/case-assist/get-document-suggestions/get-document-suggestions-request.d.ts +1 -1
  38. package/force-app/main/default/staticresources/coveoheadless/definitions/api/service/insight/get-interface/get-interface-config-request.d.ts +1 -1
  39. package/force-app/main/default/staticresources/coveoheadless/definitions/api/service/insight/query/query-request.d.ts +2 -2
  40. package/force-app/main/default/staticresources/coveoheadless/definitions/api/service/insight/user-actions/user-actions-request.d.ts +1 -1
  41. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-engine/commerce-engine.ssr.d.ts +4 -13
  42. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-ssr-engine/common.d.ts +8 -3
  43. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-ssr-engine/factories/build-factory.d.ts +19 -0
  44. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-ssr-engine/factories/hydrated-state-factory.d.ts +4 -0
  45. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-ssr-engine/factories/recommendation-hydrated-state-factory.d.ts +3 -0
  46. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-ssr-engine/factories/recommendation-static-state-factory.d.ts +3 -0
  47. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-ssr-engine/factories/static-state-factory.d.ts +4 -0
  48. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-ssr-engine/types/build.d.ts +13 -0
  49. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-ssr-engine/types/common.d.ts +180 -5
  50. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-ssr-engine/types/core-engine.d.ts +29 -10
  51. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-ssr-engine/types/fetch-static-state.d.ts +15 -0
  52. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-ssr-engine/types/from-build-result.d.ts +20 -0
  53. package/force-app/main/default/staticresources/coveoheadless/definitions/app/commerce-ssr-engine/types/hydrate-static-state.d.ts +18 -0
  54. package/force-app/main/default/staticresources/coveoheadless/definitions/app/logger.d.ts +1 -1
  55. package/force-app/main/default/staticresources/coveoheadless/definitions/app/navigatorContextProvider.d.ts +17 -0
  56. package/force-app/main/default/staticresources/coveoheadless/definitions/app/ssr-engine/types/common.d.ts +6 -0
  57. package/force-app/main/default/staticresources/coveoheadless/definitions/app/ssr-engine/types/from-build-result.d.ts +1 -0
  58. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.d.ts +1 -1
  59. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/field-suggestions/headless-field-suggestions-generator.ssr.d.ts +2 -2
  60. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/instant-products/headless-instant-products.ssr.d.ts +3 -3
  61. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/product-view/headless-product-view.ssr.d.ts +2 -2
  62. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.d.ts +2 -2
  63. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/recommendations/headless-recommendations.ssr.d.ts +10 -4
  64. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/standalone-search-box/headless-standalone-search-box.ssr.d.ts +2 -2
  65. package/force-app/main/default/staticresources/coveoheadless/definitions/features/analytics/search-action-cause.d.ts +0 -82
  66. package/force-app/main/default/staticresources/coveoheadless/definitions/features/facets/facet-set/facet-set-analytics-actions.d.ts +0 -1
  67. package/force-app/main/default/staticresources/coveoheadless/definitions/features/history/history-analytics-actions.d.ts +0 -3
  68. package/force-app/main/default/staticresources/coveoheadless/definitions/features/search/legacy/search-request.d.ts +1 -1
  69. package/force-app/main/default/staticresources/coveoheadless/definitions/features/search/search-actions.d.ts +2 -2
  70. package/force-app/main/default/staticresources/coveoheadless/definitions/features/search/search-request.d.ts +1 -1
  71. package/force-app/main/default/staticresources/coveoheadless/definitions/features/static-filter-set/static-filter-set-actions.d.ts +0 -4
  72. package/force-app/main/default/staticresources/coveoheadless/definitions/ssr-commerce.index.d.ts +4 -4
  73. package/force-app/main/default/staticresources/coveoheadless/definitions/test/mock-engine-v2.d.ts +1 -1
  74. package/force-app/main/default/staticresources/coveoheadless/definitions/utils/facet-utils.d.ts +1 -1
  75. package/force-app/main/default/staticresources/coveoheadless/definitions/utils/utils.d.ts +4 -0
  76. package/force-app/main/default/staticresources/coveoheadless/headless.js +11 -8
  77. package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +10 -7
  78. package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +9 -6
  79. package/force-app/main/default/staticresources/dompurify/purify.min.js +1339 -2
  80. package/package.json +23 -19
@@ -15,6 +15,7 @@ import {
15
15
  initializeWithHeadless,
16
16
  registerToStore,
17
17
  getHeadlessBundle,
18
+ getBueno,
18
19
  } from 'c/quanticHeadlessLoader';
19
20
  import {
20
21
  I18nUtils,
@@ -177,7 +178,6 @@ export default class QuanticFacet extends LightningElement {
177
178
  * @type {DependsOn}
178
179
  */
179
180
  @api dependsOn;
180
-
181
181
  /**
182
182
  * Whether the facet is collapsed.
183
183
  * @api
@@ -315,6 +315,7 @@ export default class QuanticFacet extends LightningElement {
315
315
  element: this.template.host,
316
316
  });
317
317
  if (this.dependsOn) {
318
+ this.validateDependsOnProperty();
318
319
  this.initFacetConditionManager(engine);
319
320
  }
320
321
  };
@@ -342,6 +343,25 @@ export default class QuanticFacet extends LightningElement {
342
343
  });
343
344
  this.dispatchEvent(renderFacetEvent);
344
345
  }
346
+ validateDependsOnProperty() {
347
+ if (this.dependsOn) {
348
+ getBueno(this).then(() => {
349
+ const {parentFacetId, expectedValue} = this.dependsOn;
350
+ if (!Bueno.isString(parentFacetId)) {
351
+ console.error(
352
+ `The ${this.field} ${this.template.host.localName} requires dependsOn.parentFacetId to be a valid string.`
353
+ );
354
+ this.setInitializationError();
355
+ }
356
+ if (expectedValue && !Bueno.isString(expectedValue)) {
357
+ console.error(
358
+ `The ${this.field} ${this.template.host.localName} requires dependsOn.expectedValue to be a valid string.`
359
+ );
360
+ this.setInitializationError();
361
+ }
362
+ });
363
+ }
364
+ }
345
365
 
346
366
  initFacetConditionManager(engine) {
347
367
  this.facetConditionsManager = this.headless.buildFacetConditionsManager(
@@ -193,7 +193,7 @@ describe('c-quantic-feedback-modal-qna', () => {
193
193
 
194
194
  expect(functionsMocks.handleSubmit).toHaveBeenCalledWith(
195
195
  expect.objectContaining({
196
- correctTopicValue: 'yes',
196
+ correctTopic: 'yes',
197
197
  hallucinationFree: 'yes',
198
198
  documented: 'yes',
199
199
  readable: 'yes',
@@ -168,7 +168,7 @@ export default class QuanticFeedbackModalQna extends LightningModal {
168
168
  return;
169
169
  }
170
170
  this.handleSubmit({
171
- correctTopicValue: this.correctTopicValue,
171
+ correctTopic: this.correctTopicValue,
172
172
  hallucinationFree: this.hallucinationFreeValue,
173
173
  documented: this.documentedValue,
174
174
  readable: this.readableValue,
@@ -6,7 +6,6 @@
6
6
  }
7
7
 
8
8
  .generated-answer__answer {
9
- white-space: pre-wrap;
10
9
  overflow: hidden;
11
10
  position: relative;
12
11
  }
@@ -100,6 +100,7 @@ blockquote {
100
100
  }
101
101
 
102
102
  .scrollable-table {
103
+ margin-top: var(--lwc-spacingLarge, 1.5rem);
103
104
  margin-bottom: var(--lwc-spacingLarge, 1.5rem);
104
105
  display: inline-block;
105
106
  border: 1px solid;
@@ -80,6 +80,14 @@ export default class QuanticInsightInterface extends LightningElement {
80
80
  ...(document.referrer && {
81
81
  originLevel3: document.referrer.substring(0, 256),
82
82
  }),
83
+ analyticsClientMiddleware: (_event, payload) => {
84
+ if (!payload.customData) {
85
+ payload.customData = {};
86
+ }
87
+ payload.customData.coveoQuanticVersion =
88
+ window.coveoQuanticVersion;
89
+ return payload;
90
+ },
83
91
  },
84
92
  ...rest,
85
93
  },
@@ -20,17 +20,30 @@ const exampleEngine = {
20
20
  id: 'mock engine',
21
21
  };
22
22
 
23
+ const mockSearchStatusState = {
24
+ hasResults: true,
25
+ hasError: false,
26
+ };
27
+
23
28
  const functionsMocks = {
24
29
  buildNotifyTrigger: jest.fn(() => ({
25
30
  state: notificationsState,
26
31
  subscribe: functionsMocks.subscribe,
27
32
  })),
28
33
  dispatchMessage: jest.fn(() => {}),
29
- subscribe: jest.fn((cb) => {
30
- cb();
34
+ buildSearchStatus: jest.fn(() => ({
35
+ state: mockSearchStatusState,
36
+ subscribe: jest.fn((callback) => {
37
+ functionsMocks.subscribe.callback = callback;
38
+ return functionsMocks.unsubscribeSearchStatus;
39
+ }),
40
+ })),
41
+ subscribe: jest.fn((callback) => {
42
+ callback();
31
43
  return functionsMocks.unsubscribe;
32
44
  }),
33
45
  unsubscribe: jest.fn(() => {}),
46
+ unsubscribeSearchStatus: jest.fn(() => {}),
34
47
  };
35
48
 
36
49
  // @ts-ignore
@@ -43,6 +56,7 @@ AriaLiveRegion.mockImplementation(() => {
43
56
  const selectors = {
44
57
  notifications: '[data-test="notification"]',
45
58
  initializationError: 'c-quantic-component-error',
59
+ notificationCloseButton: 'lightning-button-icon',
46
60
  };
47
61
 
48
62
  const defaultOptions = {
@@ -68,6 +82,7 @@ function prepareHeadlessState() {
68
82
  mockHeadlessLoader.getHeadlessBundle = () => {
69
83
  return {
70
84
  buildNotifyTrigger: functionsMocks.buildNotifyTrigger,
85
+ buildSearchStatus: functionsMocks.buildSearchStatus,
71
86
  };
72
87
  };
73
88
  }
@@ -145,7 +160,7 @@ describe('c-quantic-notifications', () => {
145
160
  });
146
161
 
147
162
  describe('component initialization', () => {
148
- it('should build the controller with the proper paramters', async () => {
163
+ it('should build the notifyTrigger and searchStatus controllers with the proper parameters', async () => {
149
164
  createTestComponent();
150
165
  await flushPromises();
151
166
 
@@ -153,9 +168,13 @@ describe('c-quantic-notifications', () => {
153
168
  expect(functionsMocks.buildNotifyTrigger).toHaveBeenCalledWith(
154
169
  exampleEngine
155
170
  );
171
+ expect(functionsMocks.buildSearchStatus).toHaveBeenCalledTimes(1);
172
+ expect(functionsMocks.buildSearchStatus).toHaveBeenCalledWith(
173
+ exampleEngine
174
+ );
156
175
  });
157
176
 
158
- it('should subscribe to the headless state changes', async () => {
177
+ it('should subscribe to the headless notify trigger state changes', async () => {
159
178
  createTestComponent();
160
179
  await flushPromises();
161
180
 
@@ -223,5 +242,94 @@ describe('c-quantic-notifications', () => {
223
242
  expect(notifications.length).toEqual(0);
224
243
  });
225
244
  });
245
+
246
+ describe('when there is an error following a search', () => {
247
+ it('should not render the notifications component', async () => {
248
+ const element = createTestComponent();
249
+ mockSearchStatusState.hasError = true;
250
+ functionsMocks.subscribe.callback();
251
+ await flushPromises();
252
+
253
+ const notifications = element.shadowRoot.querySelectorAll(
254
+ selectors.notifications
255
+ );
256
+
257
+ expect(notifications.length).toEqual(0);
258
+ });
259
+ });
260
+ });
261
+
262
+ describe('when clicking on a notification close button', () => {
263
+ it('should properly dismiss that notification', async () => {
264
+ const element = createTestComponent();
265
+ await flushPromises();
266
+
267
+ const notificationsBeforeClose = element.shadowRoot.querySelectorAll(
268
+ selectors.notifications
269
+ );
270
+
271
+ const firstNotificationCloseButton =
272
+ notificationsBeforeClose[0].querySelector(
273
+ selectors.notificationCloseButton
274
+ );
275
+ firstNotificationCloseButton.click();
276
+ await flushPromises();
277
+
278
+ const notificationsAfterClose = element.shadowRoot.querySelectorAll(
279
+ selectors.notifications
280
+ );
281
+
282
+ expect(notificationsAfterClose.length).toEqual(
283
+ exampleNotifications.length - 1
284
+ );
285
+ expect(notificationsAfterClose[0].textContent).toEqual(
286
+ exampleNotifications[1]
287
+ );
288
+ });
289
+
290
+ describe('when triggering another search with the same query', () => {
291
+ it('should reset the visibility of the notifications', async () => {
292
+ const element = createTestComponent();
293
+ await flushPromises();
294
+
295
+ const notificationsBeforeClose = element.shadowRoot.querySelectorAll(
296
+ selectors.notifications
297
+ );
298
+
299
+ expect(notificationsBeforeClose.length).toEqual(
300
+ exampleNotifications.length
301
+ );
302
+
303
+ const firstNotificationCloseButton =
304
+ notificationsBeforeClose[0].querySelector(
305
+ selectors.notificationCloseButton
306
+ );
307
+ firstNotificationCloseButton.click();
308
+ await flushPromises();
309
+
310
+ const notificationsAfterClose = element.shadowRoot.querySelectorAll(
311
+ selectors.notifications
312
+ );
313
+
314
+ expect(notificationsAfterClose.length).toEqual(
315
+ exampleNotifications.length - 1
316
+ );
317
+
318
+ mockSearchStatusState.hasError = false;
319
+ functionsMocks.subscribe.callback();
320
+ await flushPromises();
321
+
322
+ const notificationsAfterSearch = element.shadowRoot.querySelectorAll(
323
+ selectors.notifications
324
+ );
325
+
326
+ expect(notificationsAfterSearch.length).toEqual(
327
+ exampleNotifications.length
328
+ );
329
+ notificationsAfterSearch.forEach((notification, index) => {
330
+ expect(notification.textContent).toEqual(exampleNotifications[index]);
331
+ });
332
+ });
333
+ });
226
334
  });
227
335
  });
@@ -1,7 +1,15 @@
1
+ :host {
2
+ --slds-c-toast-sizing-min-width: 100%;
3
+ }
4
+
1
5
  .notification {
2
6
  background-color: var(---lwc-colorBackgroundAlt, white);
3
7
  border: 1px solid var(--lwc-colorBackgroundToast, rgb(116, 116, 116));
4
8
  margin-right: 0;
5
9
  margin-left: 0;
6
10
  width: 100%;
11
+ }
12
+
13
+ .notification-container {
14
+ z-index: 2;
7
15
  }
@@ -5,15 +5,28 @@
5
5
  </template>
6
6
  <template lwc:else>
7
7
  <template for:each={notifications} for:item="notification">
8
- <div key={notification.id} data-test="notification" class="slds-notify_container slds-is-relative">
9
- <div data-cy="notification" class="slds-notify slds-notify_toast notification" role="status">
10
- <lightning-icon icon-name="utility:info_alt" alternative-text="info" title="info"
11
- size="small"></lightning-icon>
12
- <div class="slds-notify__content slds-var-m-left_small">
13
- {notification.value}
8
+ <template lwc:if={notification.visible}>
9
+ <div key={notification.id} data-test="notification" class="slds-notify_container slds-is-relative notification-container">
10
+ <div data-cy="notification" class="slds-notify slds-notify_toast notification" role="status">
11
+ <lightning-icon icon-name="utility:info_alt" alternative-text="info" title="info"
12
+ size="small"></lightning-icon>
13
+ <div class="slds-notify__content slds-var-m-left_small">
14
+ {notification.value}
15
+ </div>
16
+ <div class="slds-notify__close">
17
+ <lightning-button-icon
18
+ icon-name="utility:close"
19
+ alternative-text={labels.closeNotification}
20
+ size="small"
21
+ variant="bare"
22
+ title={labels.closeNotification}
23
+ onclick={handleNotificationClose}
24
+ data-id={notification.id}
25
+ ></lightning-button-icon>
26
+ </div>
14
27
  </div>
15
28
  </div>
16
- </div>
29
+ </template>
17
30
  </template>
18
31
  </template>
19
32
  </template>
@@ -3,12 +3,13 @@ import {
3
3
  initializeWithHeadless,
4
4
  getHeadlessBundle,
5
5
  } from 'c/quanticHeadlessLoader';
6
+ import closeNotification from '@salesforce/label/c.quantic_CloseNotification';
6
7
  import {AriaLiveRegion} from 'c/quanticUtils';
7
8
  import {LightningElement, api} from 'lwc';
8
9
 
9
10
  /** @typedef {import("coveo").SearchEngine} SearchEngine */
11
+ /** @typedef {import("coveo").SearchStatus} SearchStatus */
10
12
  /** @typedef {import("coveo").NotifyTrigger} NotifyTrigger */
11
- /** @typedef {import("coveo").NotifyTriggerState} NotifyTriggerState */
12
13
 
13
14
  /**
14
15
  * The `QuanticNotifications` component is responsible for displaying notifications generated by the Coveo Search API (see [Trigger](https://docs.coveo.com/en/1458)).
@@ -27,12 +28,22 @@ export default class QuanticNotifications extends LightningElement {
27
28
 
28
29
  /** @type {NotifyTrigger} */
29
30
  notifyTrigger;
30
- /** @type {NotifyTriggerState} */
31
- notifyTriggerState;
32
31
  /** @type {boolean} */
33
32
  hasInitializationError = false;
34
33
  /** @type {import('c/quanticUtils').AriaLiveUtils} */
35
34
  ariaLiveNotificationsRegion;
35
+ /** @type {Array} */
36
+ notifications = [];
37
+ /** @type {Function} */
38
+ unsubscribe;
39
+ /** @type {Function} */
40
+ unsubscribeSearchStatus;
41
+ /** @type {SearchStatus} */
42
+ searchStatus;
43
+
44
+ labels = {
45
+ closeNotification,
46
+ };
36
47
 
37
48
  connectedCallback() {
38
49
  registerComponentForInit(this, this.engineId);
@@ -48,32 +59,60 @@ export default class QuanticNotifications extends LightningElement {
48
59
  initialize = (engine) => {
49
60
  this.headless = getHeadlessBundle(this.engineId);
50
61
  this.notifyTrigger = this.headless.buildNotifyTrigger(engine);
62
+ this.searchStatus = this.headless.buildSearchStatus(engine);
51
63
  this.ariaLiveNotificationsRegion = AriaLiveRegion('notifications', this);
52
64
  this.unsubscribe = this.notifyTrigger.subscribe(() => this.updateState());
65
+ this.unsubscribeSearchStatus = this.searchStatus.subscribe(() =>
66
+ this.handleSearchStatusChange()
67
+ );
53
68
  };
54
69
 
55
70
  disconnectedCallback() {
56
71
  this.unsubscribe?.();
72
+ this.unsubscribeSearchStatus?.();
57
73
  }
58
74
 
59
75
  updateState() {
60
- this.notifyTriggerState = this.notifyTrigger.state;
61
- this.ariaLiveNotificationsRegion.dispatchMessage(
62
- this.notifyTriggerState?.notifications.reduce(
63
- (value, notification, index) => {
64
- return `${value} Notification ${index + 1}: ${notification}`;
65
- },
66
- ''
67
- )
68
- );
69
- }
70
-
71
- get notifications() {
72
- return (
73
- this.notifyTriggerState?.notifications.map((notification, index) => ({
76
+ this.notifications =
77
+ this.notifyTrigger?.state?.notifications.map((notification, index) => ({
74
78
  value: notification,
75
79
  id: index,
76
- })) ?? []
80
+ visible: true,
81
+ })) ?? [];
82
+
83
+ this.dispatchAriaLiveNotificationsRegionMessage();
84
+ }
85
+
86
+ handleSearchStatusChange() {
87
+ if (!this.searchStatus?.state.isLoading) {
88
+ if (this.searchStatus?.state?.hasError) {
89
+ this.notifications = [];
90
+ } else {
91
+ this.notifications =
92
+ this.notifications?.map((notification) => ({
93
+ ...notification,
94
+ visible: true,
95
+ })) ?? [];
96
+ this.dispatchAriaLiveNotificationsRegionMessage();
97
+ }
98
+ }
99
+ }
100
+
101
+ handleNotificationClose(event) {
102
+ const currentNotificationId = event.currentTarget.dataset.id;
103
+ this.notifications = this.notifications.map((notification) => {
104
+ if (notification.id.toString() === currentNotificationId) {
105
+ return {...notification, visible: false};
106
+ }
107
+ return notification;
108
+ });
109
+ }
110
+
111
+ dispatchAriaLiveNotificationsRegionMessage() {
112
+ this.ariaLiveNotificationsRegion.dispatchMessage(
113
+ this.notifications.reduce((value, notification) => {
114
+ return `${value} Notification ${notification.id + 1}: ${notification.value}`;
115
+ }, '')
77
116
  );
78
117
  }
79
118
 
@@ -12,17 +12,22 @@ jest.mock('c/quanticUtils', () => ({
12
12
  NUMERICFACETS: 'numericFacets',
13
13
  },
14
14
  },
15
+ I18nUtils: {
16
+ format: jest.fn(),
17
+ },
15
18
  }));
16
19
  jest.mock('c/quanticHeadlessLoader');
17
20
 
18
21
  const selectors = {
19
22
  facetContent: '[data-test="facet-content"]',
23
+ componentError: 'c-quantic-component-error',
20
24
  };
21
25
 
22
26
  const exampleFacetId = 'example facet id';
27
+ const exampleField = 'exampleField';
23
28
  const exampleFilterId = `${exampleFacetId}_input`;
24
29
  const defaultOptions = {
25
- field: 'example field',
30
+ field: exampleField,
26
31
  };
27
32
  const numericFacetControllerMock = {
28
33
  subscribe: jest.fn((callback) => callback()),
@@ -31,6 +36,8 @@ const numericFacetControllerMock = {
31
36
  values: [],
32
37
  },
33
38
  };
39
+ const parentFacetIdError = `The ${exampleField} c-quantic-numeric-facet requires dependsOn.parentFacetId to be a valid string.`;
40
+ const expectedValueError = `The ${exampleField} c-quantic-numeric-facet requires dependsOn.expectedValue to be a valid string.`;
34
41
 
35
42
  function createTestComponent(options = defaultOptions) {
36
43
  prepareHeadlessState();
@@ -87,6 +94,21 @@ const exampleEngine = {
87
94
  };
88
95
  let isInitialized = false;
89
96
 
97
+ function mockBueno() {
98
+ // @ts-ignore
99
+ mockHeadlessLoader.getBueno = () => {
100
+ // @ts-ignore
101
+ global.Bueno = {
102
+ isString: jest
103
+ .fn()
104
+ .mockImplementation(
105
+ (value) => Object.prototype.toString.call(value) === '[object String]'
106
+ ),
107
+ };
108
+ return new Promise((resolve) => resolve());
109
+ };
110
+ }
111
+
90
112
  function mockSuccessfulHeadlessInitialization() {
91
113
  // @ts-ignore
92
114
  mockHeadlessLoader.initializeWithHeadless = (element, _, initialize) => {
@@ -109,6 +131,7 @@ function cleanup() {
109
131
  describe('c-quantic-numeric-facet', () => {
110
132
  beforeAll(() => {
111
133
  mockSuccessfulHeadlessInitialization();
134
+ mockBueno();
112
135
  });
113
136
 
114
137
  afterEach(() => {
@@ -214,6 +237,89 @@ describe('c-quantic-numeric-facet', () => {
214
237
  });
215
238
  });
216
239
 
240
+ describe('validation of the dependsOn property', () => {
241
+ let consoleError;
242
+ beforeAll(() => {
243
+ consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
244
+ });
245
+ describe('when dependsOn.parentFacetId is not provided', () => {
246
+ it('should display the error component', async () => {
247
+ const invalidFacetDependency = {
248
+ expectedValue: 'txt',
249
+ };
250
+ const element = createTestComponent({
251
+ ...defaultOptions,
252
+ dependsOn: invalidFacetDependency,
253
+ });
254
+ await flushPromises();
255
+
256
+ const componentError = element.shadowRoot.querySelector(
257
+ selectors.componentError
258
+ );
259
+ const facetContent = element.shadowRoot.querySelector(
260
+ selectors.facetContent
261
+ );
262
+
263
+ expect(consoleError).toHaveBeenCalledTimes(1);
264
+ expect(consoleError).toHaveBeenCalledWith(parentFacetIdError);
265
+ expect(componentError).not.toBeNull();
266
+ expect(facetContent).toBeNull();
267
+ });
268
+ });
269
+
270
+ describe('when dependsOn.parentFacetId is not a string', () => {
271
+ it('should display the error component', async () => {
272
+ const invalidFacetDependency = {
273
+ parentFacetId: 1,
274
+ expectedValue: 'txt',
275
+ };
276
+ const element = createTestComponent({
277
+ ...defaultOptions,
278
+ dependsOn: invalidFacetDependency,
279
+ });
280
+ await flushPromises();
281
+
282
+ const componentError = element.shadowRoot.querySelector(
283
+ selectors.componentError
284
+ );
285
+ const facetContent = element.shadowRoot.querySelector(
286
+ selectors.facetContent
287
+ );
288
+
289
+ expect(consoleError).toHaveBeenCalledTimes(1);
290
+ expect(consoleError).toHaveBeenCalledWith(parentFacetIdError);
291
+ expect(componentError).not.toBeNull();
292
+ expect(facetContent).toBeNull();
293
+ });
294
+ });
295
+
296
+ describe('when dependsOn.expectedValue is not a string', () => {
297
+ it('should display the error component', async () => {
298
+ const invalidFacetDependency = {
299
+ parentFacetId: 'filetype',
300
+ expectedValue: 2,
301
+ };
302
+ const element = createTestComponent({
303
+ ...defaultOptions,
304
+ dependsOn: invalidFacetDependency,
305
+ });
306
+ await flushPromises();
307
+
308
+ const componentError = element.shadowRoot.querySelector(
309
+ selectors.componentError
310
+ );
311
+ const facetContent = element.shadowRoot.querySelector(
312
+ selectors.facetContent
313
+ );
314
+
315
+ expect(consoleError).toHaveBeenCalledTimes(1);
316
+ expect(consoleError).toHaveBeenCalledWith(expectedValueError);
317
+ expect(componentError).not.toBeNull();
318
+ expect(facetContent).toBeNull();
319
+ });
320
+ });
321
+ });
322
+
217
323
  describe('when the component is disconnected', () => {
218
324
  it('should make the condition manager stop watching the facet', async () => {
219
325
  const exampleFacetDependency = {
@@ -18,6 +18,7 @@ import {
18
18
  getHeadlessBindings,
19
19
  registerToStore,
20
20
  getHeadlessBundle,
21
+ getBueno,
21
22
  } from 'c/quanticHeadlessLoader';
22
23
  import {
23
24
  I18nUtils,
@@ -264,6 +265,7 @@ export default class QuanticNumericFacet extends LightningElement {
264
265
  * @param {SearchEngine} engine
265
266
  */
266
267
  initialize = (engine) => {
268
+ this.validateDependsOnProperty();
267
269
  this.headless = getHeadlessBundle(this.engineId);
268
270
  this.searchStatus = this.headless.buildSearchStatus(engine);
269
271
  this.unsubscribeSearchStatus = this.searchStatus.subscribe(() =>
@@ -288,6 +290,26 @@ export default class QuanticNumericFacet extends LightningElement {
288
290
  });
289
291
  };
290
292
 
293
+ validateDependsOnProperty() {
294
+ if (this.dependsOn) {
295
+ getBueno(this).then(() => {
296
+ const {parentFacetId, expectedValue} = this.dependsOn;
297
+ if (!Bueno.isString(parentFacetId)) {
298
+ console.error(
299
+ `The ${this.field} ${this.template.host.localName} requires dependsOn.parentFacetId to be a valid string.`
300
+ );
301
+ this.setInitializationError();
302
+ }
303
+ if (expectedValue && !Bueno.isString(expectedValue)) {
304
+ console.error(
305
+ `The ${this.field} ${this.template.host.localName} requires dependsOn.expectedValue to be a valid string.`
306
+ );
307
+ this.setInitializationError();
308
+ }
309
+ });
310
+ }
311
+ }
312
+
291
313
  /**
292
314
  * @param {import("coveo").SearchEngine} engine
293
315
  */
@@ -82,6 +82,14 @@ export default class QuanticRecommendationInterface extends LightningElement {
82
82
  ...(document.referrer && {
83
83
  originLevel3: document.referrer.substring(0, 256),
84
84
  }),
85
+ analyticsClientMiddleware: (_event, payload) => {
86
+ if (!payload.customData) {
87
+ payload.customData = {};
88
+ }
89
+ payload.customData.coveoQuanticVersion =
90
+ window.coveoQuanticVersion;
91
+ return payload;
92
+ },
85
93
  },
86
94
  ...rest,
87
95
  },
@@ -107,6 +107,14 @@ export default class QuanticSearchInterface extends LightningElement {
107
107
  ...(document.referrer && {
108
108
  originLevel3: document.referrer.substring(0, 256),
109
109
  }),
110
+ analyticsClientMiddleware: (_event, payload) => {
111
+ if (!payload.customData) {
112
+ payload.customData = {};
113
+ }
114
+ payload.customData.coveoQuanticVersion =
115
+ window.coveoQuanticVersion;
116
+ return payload;
117
+ },
110
118
  },
111
119
  ...rest,
112
120
  },