@coveo/quantic 3.39.1 → 3.41.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 (48) hide show
  1. package/force-app/main/default/classes/HeadlessControllerTest.cls +1 -0
  2. package/force-app/main/default/classes/InsightControllerTest.cls +1 -0
  3. package/force-app/main/default/classes/RecommendationsControllerTest.cls +1 -1
  4. package/force-app/main/default/classes/SampleTokenProvider.cls +1 -0
  5. package/force-app/main/default/classes/SampleTokenProviderTest.cls +1 -0
  6. package/force-app/main/default/labels/CustomLabels.labels-meta.xml +77 -0
  7. package/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js +54 -0
  8. package/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js +26 -3
  9. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.css +5 -0
  10. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html +3 -1
  11. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/__tests__/quanticGeneratedAnswerBody.test.js +341 -0
  12. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/quanticGeneratedAnswerBody.js +148 -0
  13. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/quanticGeneratedAnswerBody.js-meta.xml +5 -0
  14. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/answer.css +3 -0
  15. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/answer.html +53 -0
  16. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/cannotAnswer.html +7 -0
  17. package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/error.html +7 -0
  18. package/force-app/main/default/lwc/quanticGeneratedAnswerContent/__tests__/quanticGeneratedAnswerContent.test.js +269 -0
  19. package/force-app/main/default/lwc/quanticGeneratedAnswerContent/quanticGeneratedAnswerContent.js +136 -0
  20. package/force-app/main/default/lwc/quanticGeneratedAnswerContent/templates/generatedMarkdownContent.css +10 -0
  21. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/__tests__/quanticGeneratedAnswerStreamOfThought.test.js +348 -0
  22. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.css +17 -0
  23. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js +163 -0
  24. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js-meta.xml +5 -0
  25. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/collapsedSummary.css +1 -0
  26. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/collapsedSummary.html +32 -0
  27. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/streamOfThought.css +1 -0
  28. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/streamOfThought.html +65 -0
  29. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/__tests__/quanticGeneratedAnswerThread.test.js +285 -0
  30. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.css +47 -0
  31. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.html +67 -0
  32. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.js +93 -0
  33. package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.js-meta.xml +5 -0
  34. package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.css +0 -4
  35. package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.html +1 -1
  36. package/force-app/main/default/lwc/quanticUtils/__tests__/markdownUtils.test.js +38 -0
  37. package/force-app/main/default/lwc/quanticUtils/markdownUtils.js +18 -0
  38. package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +6 -6
  39. package/force-app/main/default/staticresources/coveoheadless/definitions/api/commerce/common/pagination.d.ts +1 -1
  40. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/core/generated-answer/headless-core-generated-answer.d.ts +1 -0
  41. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/core/generated-answer/headless-core-interactive-citation.d.ts +3 -3
  42. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/generated-answer/interactive-citation-analytics-client.d.ts +3 -0
  43. package/force-app/main/default/staticresources/coveoheadless/definitions/features/analytics/analytics-utils.d.ts +3 -2
  44. package/force-app/main/default/staticresources/coveoheadless/definitions/features/generated-answer/generated-answer-analytics-actions.d.ts +2 -0
  45. package/force-app/main/default/staticresources/coveoheadless/headless.js +17 -17
  46. package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +7 -7
  47. package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +14 -14
  48. package/package.json +2 -2
@@ -0,0 +1,148 @@
1
+ import couldNotGenerateAnAnswer from '@salesforce/label/c.quantic_CouldNotGenerateAnAnswer';
2
+ import generatedAnswerErrorTurnLimitReached from '@salesforce/label/c.quantic_GeneratedAnswerErrorTurnLimitReached';
3
+ import genericErrorTitle from '@salesforce/label/c.quantic_GenericErrorTitle';
4
+ import thisAnswerWasHelpful from '@salesforce/label/c.quantic_ThisAnswerWasHelpful';
5
+ import thisAnswerWasNotHelpful from '@salesforce/label/c.quantic_ThisAnswerWasNotHelpful';
6
+ import {LightningElement, api} from 'lwc';
7
+ // @ts-ignore
8
+ import answerTemplate from './templates/answer.html';
9
+ // @ts-ignore
10
+ import cannotAnswerTemplate from './templates/cannotAnswer.html';
11
+ // @ts-ignore
12
+ import errorTemplate from './templates/error.html';
13
+
14
+ /** @typedef {import("@coveo/headless").GeneratedAnswerBase} GeneratedAnswerBase */
15
+
16
+ const FEEDBACK_NEUTRAL_STATE = 'neutral';
17
+ const FEEDBACK_LIKED_STATE = 'liked';
18
+ const FEEDBACK_DISLIKED_STATE = 'disliked';
19
+
20
+ /**
21
+ * The `QuanticGeneratedAnswerBody` component renders a single generated answer unit.
22
+ * @category Internal
23
+ * @fires CustomEvent#quantic__generatedanswerlike
24
+ * @fires CustomEvent#quantic__generatedanswerdislike
25
+ * @fires CustomEvent#quantic__generatedanswercopy
26
+ * @fires CustomEvent#quantic__citationhover
27
+ */
28
+ export default class QuanticGeneratedAnswerBody extends LightningElement {
29
+ /**
30
+ * The ID of the engine instance the component registers to.
31
+ * @api
32
+ * @type {string}
33
+ */
34
+ @api engineId;
35
+ /**
36
+ * The generated answer object to render.
37
+ * @api
38
+ * @type {GeneratedAnswerBase}
39
+ */
40
+ @api generatedAnswer;
41
+ /**
42
+ * Whether to disable citation anchoring.
43
+ * @api
44
+ * @type {boolean}
45
+ */
46
+ @api disableCitationAnchoring = false;
47
+
48
+ labels = {
49
+ couldNotGenerateAnAnswer,
50
+ generatedAnswerErrorTurnLimitReached,
51
+ genericErrorTitle,
52
+ thisAnswerWasHelpful,
53
+ thisAnswerWasNotHelpful,
54
+ };
55
+
56
+ get answer() {
57
+ return this.generatedAnswer?.answer;
58
+ }
59
+
60
+ get citations() {
61
+ return this.generatedAnswer?.citations || [];
62
+ }
63
+
64
+ get answerId() {
65
+ return this.generatedAnswer?.answerId;
66
+ }
67
+
68
+ get answerContentFormat() {
69
+ return this.generatedAnswer?.answerContentFormat;
70
+ }
71
+
72
+ get isStreaming() {
73
+ return !!this.generatedAnswer?.isStreaming;
74
+ }
75
+
76
+ get hasError() {
77
+ return !!this.generatedAnswer?.error?.code;
78
+ }
79
+
80
+ get cannotAnswer() {
81
+ return !!this.generatedAnswer?.cannotAnswer;
82
+ }
83
+
84
+ get errorMessage() {
85
+ if (this.generatedAnswer?.error?.isSseTurnLimitReachedError?.()) {
86
+ return this.labels.generatedAnswerErrorTurnLimitReached;
87
+ }
88
+ return this.labels.genericErrorTitle;
89
+ }
90
+
91
+ get computedFeedbackState() {
92
+ if (this.generatedAnswer?.liked) {
93
+ return FEEDBACK_LIKED_STATE;
94
+ }
95
+ if (this.generatedAnswer?.disliked) {
96
+ return FEEDBACK_DISLIKED_STATE;
97
+ }
98
+ return FEEDBACK_NEUTRAL_STATE;
99
+ }
100
+
101
+ get shouldShowCitations() {
102
+ return this.citations.length > 0 && !this.isStreaming;
103
+ }
104
+
105
+ get shouldShowActions() {
106
+ return Boolean(this.answer) && !this.isStreaming;
107
+ }
108
+
109
+ handleLike(event) {
110
+ event.stopPropagation();
111
+ this.dispatchAnswerInteractionEvent('quantic__generatedanswerlike');
112
+ }
113
+
114
+ handleDislike(event) {
115
+ event.stopPropagation();
116
+ this.dispatchAnswerInteractionEvent('quantic__generatedanswerdislike');
117
+ }
118
+
119
+ handleCitationHover = (citationId, citationHoverTimeMs) => {
120
+ this.dispatchAnswerInteractionEvent('quantic__citationhover', {
121
+ citationId,
122
+ citationHoverTimeMs,
123
+ });
124
+ };
125
+
126
+ dispatchAnswerInteractionEvent(eventName, payload = {}) {
127
+ this.dispatchEvent(
128
+ new CustomEvent(eventName, {
129
+ detail: {
130
+ answerId: this.answerId,
131
+ ...payload,
132
+ },
133
+ bubbles: true,
134
+ composed: true,
135
+ })
136
+ );
137
+ }
138
+
139
+ render() {
140
+ if (this.hasError) {
141
+ return errorTemplate;
142
+ }
143
+ if (this.cannotAnswer) {
144
+ return cannotAnswerTemplate;
145
+ }
146
+ return answerTemplate;
147
+ }
148
+ }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <apiVersion>65.0</apiVersion>
4
+ <isExposed>false</isExposed>
5
+ </LightningComponentBundle>
@@ -0,0 +1,3 @@
1
+ .generated-answer__body {
2
+ overflow-wrap: break-word;
3
+ }
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <section
3
+ data-testid="generated-answer-body"
4
+ class="generated-answer__body"
5
+ >
6
+ <c-quantic-generated-answer-content
7
+ answer-content-format={answerContentFormat}
8
+ answer={answer}
9
+ is-streaming={isStreaming}
10
+ >
11
+ </c-quantic-generated-answer-content>
12
+ <div class="slds-grid slds-grid_vertical">
13
+ <template lwc:if={shouldShowCitations}>
14
+ <div class="slds-size_1-of-1 slds-var-m-top_x-small">
15
+ <c-quantic-source-citations
16
+ data-testid="generated-answer-body__citations"
17
+ engine-id={engineId}
18
+ citations={citations}
19
+ citation-hover-handler={handleCitationHover}
20
+ disable-citation-anchoring={disableCitationAnchoring}
21
+ ></c-quantic-source-citations>
22
+ </div>
23
+ </template>
24
+ <template lwc:if={shouldShowActions}>
25
+ <div
26
+ data-testid="generated-answer-body__actions"
27
+ class="slds-size_1-of-1 slds-grid slds-grid_vertical-align-center slds-var-m-top_x-small slds-grid_align-start"
28
+ >
29
+ <c-quantic-feedback
30
+ state={computedFeedbackState}
31
+ onquantic__like={handleLike}
32
+ onquantic__dislike={handleDislike}
33
+ like-icon-name="utility:like"
34
+ like-label={labels.thisAnswerWasHelpful}
35
+ dislike-icon-name="utility:dislike"
36
+ dislike-label={labels.thisAnswerWasNotHelpful}
37
+ size="x-small"
38
+ question=""
39
+ hide-explain-why-button
40
+ hide-labels
41
+ ></c-quantic-feedback>
42
+ <c-quantic-generated-answer-copy-to-clipboard
43
+ data-testid="generated-answer-body__copy-to-clipboard"
44
+ answer={answer}
45
+ answer-id={answerId}
46
+ size="x-small"
47
+ class="slds-var-m-horizontal_xx-small"
48
+ ></c-quantic-generated-answer-copy-to-clipboard>
49
+ </div>
50
+ </template>
51
+ </div>
52
+ </section>
53
+ </template>
@@ -0,0 +1,7 @@
1
+ <template>
2
+ <section
3
+ data-testid="generated-answer-body__no-answer-message"
4
+ >
5
+ <div>{labels.couldNotGenerateAnAnswer}</div>
6
+ </section>
7
+ </template>
@@ -0,0 +1,7 @@
1
+ <template>
2
+ <section
3
+ data-testid="generated-answer-body__error"
4
+ >
5
+ <p>{errorMessage}</p>
6
+ </section>
7
+ </template>
@@ -1,8 +1,11 @@
1
+ /* eslint-disable no-import-assign */
1
2
  // @ts-ignore
2
3
  import QuanticGeneratedAnswerContent from 'c/quanticGeneratedAnswerContent';
3
4
  import {createElement} from 'lwc';
4
5
  import {loadMarkdownDependencies} from 'c/quanticUtils';
6
+ import * as mockHeadlessLoader from 'c/quanticHeadlessLoader';
5
7
 
8
+ jest.mock('c/quanticHeadlessLoader');
6
9
  jest.mock('c/quanticUtils', () => ({
7
10
  loadMarkdownDependencies: jest.fn(
8
11
  () =>
@@ -11,6 +14,9 @@ jest.mock('c/quanticUtils', () => ({
11
14
  })
12
15
  ),
13
16
  transformMarkdownToHtml: jest.fn((value) => value),
17
+ LinkUtils: {
18
+ bindAnalyticsToLink: jest.fn(() => jest.fn()),
19
+ },
14
20
  }));
15
21
 
16
22
  const mockMarkedUse = jest.fn();
@@ -23,14 +29,47 @@ global.marked = {
23
29
  const SELECTORS = {
24
30
  textAnswerContainer: 'span.generated-answer-content__answer',
25
31
  markdownAnswerContainer: 'div.generated-answer-content__answer',
32
+ inlineLink: 'a[data-answer-inline-link]',
33
+ inlineLinkIconContainer: 'span.slds-icon_container',
34
+ inlineLinkIconSvg: 'svg.slds-icon',
26
35
  };
27
36
 
37
+ const exampleEngineId = 'example-engine-id';
38
+ const exampleAnswerId = 'example-answer-id';
39
+
28
40
  const defaultOptions = {
29
41
  isStreaming: false,
30
42
  answerContentFormat: 'text/plain',
31
43
  answer: '',
32
44
  };
33
45
 
46
+ const exampleEngine = {id: 'dummy-engine'};
47
+ let isInitialized = false;
48
+
49
+ const mockBuildInteractiveGeneratedAnswerInlineLink = jest.fn(() => ({
50
+ select: jest.fn(),
51
+ beginDelayedSelect: jest.fn(),
52
+ cancelPendingSelect: jest.fn(),
53
+ }));
54
+
55
+ function prepareHeadlessState() {
56
+ // @ts-ignore
57
+ mockHeadlessLoader.getHeadlessBundle = () => ({
58
+ buildInteractiveGeneratedAnswerInlineLink:
59
+ mockBuildInteractiveGeneratedAnswerInlineLink,
60
+ });
61
+ }
62
+
63
+ function mockSuccessfulHeadlessInitialization() {
64
+ // @ts-ignore
65
+ mockHeadlessLoader.initializeWithHeadless = (element, _, initialize) => {
66
+ if (element instanceof QuanticGeneratedAnswerContent && !isInitialized) {
67
+ isInitialized = true;
68
+ initialize(exampleEngine);
69
+ }
70
+ };
71
+ }
72
+
34
73
  function createTestComponent(options = defaultOptions) {
35
74
  const element = createElement('c-quantic-generated-answer-content', {
36
75
  is: QuanticGeneratedAnswerContent,
@@ -43,6 +82,20 @@ function createTestComponent(options = defaultOptions) {
43
82
  return element;
44
83
  }
45
84
 
85
+ /**
86
+ * Builds an HTML string containing anchor tags with the data-answer-inline-link
87
+ * attribute, which is what the component's processInlineLinks method targets.
88
+ * @param {Array<{href: string, text: string}>} links
89
+ * @returns {string}
90
+ */
91
+ function buildAnswerWithInlineLinks(links) {
92
+ return links
93
+ .map(
94
+ ({href, text}) => `<a href="${href}" data-answer-inline-link>${text}</a>`
95
+ )
96
+ .join(' ');
97
+ }
98
+
46
99
  // Helper function to wait until the microtask queue is empty.
47
100
  function flushPromises() {
48
101
  // eslint-disable-next-line @lwc/lwc/no-async-operation
@@ -56,6 +109,7 @@ describe('c-quantic-generated-answer-content', () => {
56
109
  document.body.removeChild(document.body.firstChild);
57
110
  }
58
111
  jest.clearAllMocks();
112
+ isInitialized = false;
59
113
  }
60
114
 
61
115
  afterEach(() => {
@@ -148,4 +202,219 @@ describe('c-quantic-generated-answer-content', () => {
148
202
  expect(loadMarkdownDependencies).toHaveBeenCalled();
149
203
  });
150
204
  });
205
+
206
+ describe('inline links in a markdown answer', () => {
207
+ const exampleLinks = [
208
+ {href: 'https://example.com/1', text: 'Link one'},
209
+ {href: 'https://example.com/2', text: 'Link two'},
210
+ ];
211
+
212
+ beforeEach(() => {
213
+ mockSuccessfulHeadlessInitialization();
214
+ prepareHeadlessState();
215
+ });
216
+
217
+ describe('when the answer contains anchors with data-answer-inline-link', () => {
218
+ it('should set target="_blank" and append an icon span to each inline link anchor', async () => {
219
+ const {LinkUtils} = jest.requireMock('c/quanticUtils');
220
+ LinkUtils.bindAnalyticsToLink.mockReturnValue(jest.fn());
221
+
222
+ const element = createTestComponent({
223
+ ...defaultOptions,
224
+ engineId: exampleEngineId,
225
+ answerId: exampleAnswerId,
226
+ answerContentFormat: 'text/markdown',
227
+ answer: buildAnswerWithInlineLinks(exampleLinks),
228
+ });
229
+ await flushPromises();
230
+
231
+ const answerContainer = element.shadowRoot.querySelector(
232
+ SELECTORS.markdownAnswerContainer
233
+ );
234
+ const anchors = answerContainer.querySelectorAll(SELECTORS.inlineLink);
235
+
236
+ expect(anchors).toHaveLength(exampleLinks.length);
237
+ anchors.forEach((anchor) => {
238
+ expect(anchor.target).toBe('_blank');
239
+ });
240
+ anchors.forEach((anchor) => {
241
+ const iconSpan = anchor.querySelector(
242
+ SELECTORS.inlineLinkIconContainer
243
+ );
244
+ expect(iconSpan).not.toBeNull();
245
+ expect(iconSpan.classList).toContain('slds-icon-utility-new_window');
246
+ expect(iconSpan.classList).toContain('slds-current-color');
247
+ expect(
248
+ iconSpan.querySelector(SELECTORS.inlineLinkIconSvg)
249
+ ).not.toBeNull();
250
+ });
251
+ });
252
+
253
+ it('should call buildInteractiveGeneratedAnswerInlineLink with the correct parameters for each inline link', async () => {
254
+ const {LinkUtils} = jest.requireMock('c/quanticUtils');
255
+ LinkUtils.bindAnalyticsToLink.mockReturnValue(jest.fn());
256
+
257
+ createTestComponent({
258
+ ...defaultOptions,
259
+ engineId: exampleEngineId,
260
+ answerId: exampleAnswerId,
261
+ answerContentFormat: 'text/markdown',
262
+ answer: buildAnswerWithInlineLinks(exampleLinks),
263
+ });
264
+ await flushPromises();
265
+
266
+ expect(
267
+ mockBuildInteractiveGeneratedAnswerInlineLink
268
+ ).toHaveBeenCalledTimes(exampleLinks.length);
269
+ expect(LinkUtils.bindAnalyticsToLink).toHaveBeenCalledTimes(
270
+ exampleLinks.length
271
+ );
272
+
273
+ exampleLinks.forEach((anchor, index) => {
274
+ expect(
275
+ mockBuildInteractiveGeneratedAnswerInlineLink
276
+ ).toHaveBeenNthCalledWith(index + 1, exampleEngine, {
277
+ options: {
278
+ link: {
279
+ linkURL: anchor.href,
280
+ linkText: anchor.text,
281
+ },
282
+ answerId: exampleAnswerId,
283
+ },
284
+ });
285
+ });
286
+ });
287
+
288
+ it('should call LinkUtils.bindAnalyticsToLink for each inline link with the anchor and its controller', async () => {
289
+ const {LinkUtils} = jest.requireMock('c/quanticUtils');
290
+ const mockUnbind = jest.fn();
291
+ LinkUtils.bindAnalyticsToLink.mockReturnValue(mockUnbind);
292
+
293
+ const element = createTestComponent({
294
+ ...defaultOptions,
295
+ engineId: exampleEngineId,
296
+ answerId: exampleAnswerId,
297
+ answerContentFormat: 'text/markdown',
298
+ answer: buildAnswerWithInlineLinks(exampleLinks),
299
+ });
300
+ await flushPromises();
301
+
302
+ expect(LinkUtils.bindAnalyticsToLink).toHaveBeenCalledTimes(
303
+ exampleLinks.length
304
+ );
305
+
306
+ const answerContainer = element.shadowRoot.querySelector(
307
+ SELECTORS.markdownAnswerContainer
308
+ );
309
+ const anchors = Array.from(
310
+ answerContainer.querySelectorAll(SELECTORS.inlineLink)
311
+ );
312
+
313
+ anchors.forEach((anchor, index) => {
314
+ expect(LinkUtils.bindAnalyticsToLink).toHaveBeenNthCalledWith(
315
+ index + 1,
316
+ anchor,
317
+ expect.objectContaining({
318
+ select: expect.any(Function),
319
+ beginDelayedSelect: expect.any(Function),
320
+ cancelPendingSelect: expect.any(Function),
321
+ })
322
+ );
323
+ });
324
+ });
325
+ });
326
+
327
+ describe('when the answer does not contain anchors with data-answer-inline-link', () => {
328
+ it('should not call buildInteractiveGeneratedAnswerInlineLink and LinkUtils.bindAnalyticsToLink', async () => {
329
+ const {LinkUtils} = jest.requireMock('c/quanticUtils');
330
+
331
+ createTestComponent({
332
+ ...defaultOptions,
333
+ engineId: exampleEngineId,
334
+ answerId: exampleAnswerId,
335
+ answerContentFormat: 'text/markdown',
336
+ answer: '<p>No inline links here</p>',
337
+ });
338
+ await flushPromises();
339
+
340
+ expect(
341
+ mockBuildInteractiveGeneratedAnswerInlineLink
342
+ ).not.toHaveBeenCalled();
343
+ expect(LinkUtils.bindAnalyticsToLink).not.toHaveBeenCalled();
344
+ });
345
+ });
346
+
347
+ describe('when the headless bundle does not expose buildInteractiveGeneratedAnswerInlineLink', () => {
348
+ it('should not throw and should not call LinkUtils.bindAnalyticsToLink', async () => {
349
+ // @ts-ignore
350
+ mockHeadlessLoader.getHeadlessBundle = () => ({});
351
+ const {LinkUtils} = jest.requireMock('c/quanticUtils');
352
+
353
+ expect(() => {
354
+ createTestComponent({
355
+ ...defaultOptions,
356
+ engineId: exampleEngineId,
357
+ answerId: exampleAnswerId,
358
+ answerContentFormat: 'text/markdown',
359
+ answer: buildAnswerWithInlineLinks(exampleLinks),
360
+ });
361
+ }).not.toThrow();
362
+
363
+ await flushPromises();
364
+
365
+ expect(LinkUtils.bindAnalyticsToLink).not.toHaveBeenCalled();
366
+ });
367
+ });
368
+
369
+ describe('when the answer is updated', () => {
370
+ it('should clean up previous inline link bindings before processing the new answer', async () => {
371
+ const {LinkUtils} = jest.requireMock('c/quanticUtils');
372
+ const mockUnbind = jest.fn();
373
+ LinkUtils.bindAnalyticsToLink.mockReturnValue(mockUnbind);
374
+
375
+ const element = createTestComponent({
376
+ ...defaultOptions,
377
+ engineId: exampleEngineId,
378
+ answerId: exampleAnswerId,
379
+ answerContentFormat: 'text/markdown',
380
+ answer: buildAnswerWithInlineLinks(exampleLinks),
381
+ });
382
+ await flushPromises();
383
+
384
+ expect(mockUnbind).not.toHaveBeenCalled();
385
+
386
+ const updatedLinks = [
387
+ {href: 'https://example.com/3', text: 'Link three'},
388
+ ];
389
+ element.answer = buildAnswerWithInlineLinks(updatedLinks);
390
+ await flushPromises();
391
+
392
+ expect(mockUnbind).toHaveBeenCalledTimes(exampleLinks.length);
393
+ });
394
+ });
395
+
396
+ describe('when the component is disconnected', () => {
397
+ it('should call the unbind function for each inline link binding', async () => {
398
+ const {LinkUtils} = jest.requireMock('c/quanticUtils');
399
+ const mockUnbind = jest.fn();
400
+ LinkUtils.bindAnalyticsToLink.mockReturnValue(mockUnbind);
401
+
402
+ const element = createTestComponent({
403
+ ...defaultOptions,
404
+ engineId: exampleEngineId,
405
+ answerId: exampleAnswerId,
406
+ answerContentFormat: 'text/markdown',
407
+ answer: buildAnswerWithInlineLinks(exampleLinks),
408
+ });
409
+ await flushPromises();
410
+
411
+ expect(mockUnbind).not.toHaveBeenCalled();
412
+
413
+ document.body.removeChild(element);
414
+ await flushPromises();
415
+
416
+ expect(mockUnbind).toHaveBeenCalledTimes(exampleLinks.length);
417
+ });
418
+ });
419
+ });
151
420
  });