@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.
- package/force-app/main/default/classes/HeadlessControllerTest.cls +1 -0
- package/force-app/main/default/classes/InsightControllerTest.cls +1 -0
- package/force-app/main/default/classes/RecommendationsControllerTest.cls +1 -1
- package/force-app/main/default/classes/SampleTokenProvider.cls +1 -0
- package/force-app/main/default/classes/SampleTokenProviderTest.cls +1 -0
- package/force-app/main/default/labels/CustomLabels.labels-meta.xml +77 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js +54 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js +26 -3
- package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.css +5 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html +3 -1
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/__tests__/quanticGeneratedAnswerBody.test.js +341 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/quanticGeneratedAnswerBody.js +148 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/quanticGeneratedAnswerBody.js-meta.xml +5 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/answer.css +3 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/answer.html +53 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/cannotAnswer.html +7 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerBody/templates/error.html +7 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerContent/__tests__/quanticGeneratedAnswerContent.test.js +269 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerContent/quanticGeneratedAnswerContent.js +136 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerContent/templates/generatedMarkdownContent.css +10 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/__tests__/quanticGeneratedAnswerStreamOfThought.test.js +348 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.css +17 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js +163 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js-meta.xml +5 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/collapsedSummary.css +1 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/collapsedSummary.html +32 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/streamOfThought.css +1 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/templates/streamOfThought.html +65 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerThread/__tests__/quanticGeneratedAnswerThread.test.js +285 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.css +47 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.html +67 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.js +93 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerThread/quanticGeneratedAnswerThread.js-meta.xml +5 -0
- package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.css +0 -4
- package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.html +1 -1
- package/force-app/main/default/lwc/quanticUtils/__tests__/markdownUtils.test.js +38 -0
- package/force-app/main/default/lwc/quanticUtils/markdownUtils.js +18 -0
- package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +6 -6
- package/force-app/main/default/staticresources/coveoheadless/definitions/api/commerce/common/pagination.d.ts +1 -1
- package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/core/generated-answer/headless-core-generated-answer.d.ts +1 -0
- package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/core/generated-answer/headless-core-interactive-citation.d.ts +3 -3
- package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/generated-answer/interactive-citation-analytics-client.d.ts +3 -0
- package/force-app/main/default/staticresources/coveoheadless/definitions/features/analytics/analytics-utils.d.ts +3 -2
- package/force-app/main/default/staticresources/coveoheadless/definitions/features/generated-answer/generated-answer-analytics-actions.d.ts +2 -0
- package/force-app/main/default/staticresources/coveoheadless/headless.js +17 -17
- package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +7 -7
- package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +14 -14
- 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,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>
|
|
@@ -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
|
});
|