@coveo/quantic 3.42.0 → 3.43.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 (27) hide show
  1. package/force-app/main/default/labels/CustomLabels.labels-meta.xml +21 -0
  2. package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/agentData.ts +248 -0
  3. package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/fixture-agent.ts +77 -0
  4. package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/fixture.ts +65 -59
  5. package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/pageObject.ts +158 -13
  6. package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/quanticGeneratedAnswer.e2e.ts +1 -5
  7. package/force-app/main/default/lwc/quanticGeneratedAnswer/e2e/quanticGeneratedAnswerAgent.e2e.ts +233 -0
  8. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/conversation.html +1 -1
  9. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/__tests__/quanticGeneratedAnswerFollowUpInput.test.js +242 -10
  10. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.css +2 -2
  11. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.html +16 -1
  12. package/force-app/main/default/lwc/quanticGeneratedAnswerFollowUpInput/quanticGeneratedAnswerFollowUpInput.js +47 -4
  13. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/__tests__/quanticGeneratedAnswerStreamOfThought.test.js +95 -41
  14. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/__tests__/stepsUtils.test.js +118 -0
  15. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/quanticGeneratedAnswerStreamOfThought.js +21 -35
  16. package/force-app/main/default/lwc/quanticGeneratedAnswerStreamOfThought/stepsUtils.js +66 -0
  17. package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +15 -15
  18. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/product-listing/facets/headless-product-listing-facet-options.d.ts +4 -39
  19. package/force-app/main/default/staticresources/coveoheadless/definitions/controllers/commerce/search/facets/headless-search-facet-options.d.ts +4 -39
  20. package/force-app/main/default/staticresources/coveoheadless/definitions/features/commerce/facets/facet-set/facet-set-selector.d.ts +3 -9
  21. package/force-app/main/default/staticresources/coveoheadless/definitions/features/facets/facet-search-set/category/category-facet-search-state-selector.d.ts +1 -27
  22. package/force-app/main/default/staticresources/coveoheadless/definitions/features/facets/facet-search-set/specific/specific-facet-search-state-selector.d.ts +1 -27
  23. package/force-app/main/default/staticresources/coveoheadless/headless.js +17 -17
  24. package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +17 -17
  25. package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +15 -15
  26. package/force-app/main/default/staticresources/dompurify/purify.min.js +2 -2
  27. package/package.json +5 -5
@@ -11,8 +11,19 @@ jest.mock(
11
11
  () => ({default: 'Ask follow-up'}),
12
12
  {virtual: true}
13
13
  );
14
+ jest.mock(
15
+ '@salesforce/label/c.quantic_FollowUpInputTooLong',
16
+ () => ({
17
+ default: 'Your question is too long. Please use {{0}} characters or fewer.',
18
+ }),
19
+ {virtual: true}
20
+ );
14
21
  jest.mock('c/quanticUtils', () => ({
15
22
  keys: {ENTER: 'Enter', ESC: 'Escape'},
23
+ I18nUtils: {
24
+ format: (label, ...args) =>
25
+ label.replace(/{{(\d+)}}/g, (_match, index) => args[index]),
26
+ },
16
27
  }));
17
28
 
18
29
  const selectors = {
@@ -20,9 +31,13 @@ const selectors = {
20
31
  submitButton: 'lightning-button-icon',
21
32
  container: '.follow-up-input__container',
22
33
  expander: '.follow-up-input__expander',
34
+ counter: '.follow-up-input__counter',
35
+ validationMessage: '[role="alert"]',
23
36
  };
24
37
 
25
38
  const expandedClass = 'follow-up-input__expander--expanded';
39
+ const errorClass = 'follow-up-input__container--error';
40
+ const MAX_LENGTH = 300;
26
41
 
27
42
  const createTestComponent = buildCreateTestComponent(
28
43
  QuanticGeneratedAnswerFollowUpInput,
@@ -76,6 +91,21 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
76
91
  expect(handler.mock.calls[0][0].detail.value).toBe('another question');
77
92
  });
78
93
 
94
+ it('should dispatch the trimmed value when the input has leading and trailing spaces', async () => {
95
+ const element = createTestComponent();
96
+ await flushPromises();
97
+
98
+ const handler = jest.fn();
99
+ element.addEventListener('quantic__submitfollowup', handler);
100
+
101
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
102
+ textarea.value = ' another question ';
103
+ element.shadowRoot.querySelector(selectors.submitButton).click();
104
+
105
+ expect(handler).toHaveBeenCalledTimes(1);
106
+ expect(handler.mock.calls[0][0].detail.value).toBe('another question');
107
+ });
108
+
79
109
  it('should clear the textarea after a successful submit', async () => {
80
110
  const element = createTestComponent();
81
111
  await flushPromises();
@@ -149,7 +179,7 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
149
179
  });
150
180
 
151
181
  describe('expandable input behaviour', () => {
152
- it('should expand and add focused class on focus', async () => {
182
+ it('should expand on focus', async () => {
153
183
  const element = createTestComponent();
154
184
  await flushPromises();
155
185
 
@@ -158,14 +188,10 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
158
188
  await flushPromises();
159
189
 
160
190
  const expander = element.shadowRoot.querySelector(selectors.expander);
161
- const container = element.shadowRoot.querySelector(selectors.container);
162
191
  expect(expander.classList).toContain(expandedClass);
163
- expect(container.classList).toContain(
164
- 'follow-up-input__container--focused'
165
- );
166
192
  });
167
193
 
168
- it('should collapse and remove focused class on blur', async () => {
194
+ it('should collapse on blur', async () => {
169
195
  const element = createTestComponent();
170
196
  await flushPromises();
171
197
 
@@ -177,11 +203,7 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
177
203
  await flushPromises();
178
204
 
179
205
  const expander = element.shadowRoot.querySelector(selectors.expander);
180
- const container = element.shadowRoot.querySelector(selectors.container);
181
206
  expect(expander.classList).not.toContain(expandedClass);
182
- expect(container.classList).not.toContain(
183
- 'follow-up-input__container--focused'
184
- );
185
207
  });
186
208
 
187
209
  it('should sync replica text on input', async () => {
@@ -231,4 +253,214 @@ describe('c-quantic-generated-answer-follow-up-input', () => {
231
253
  expect(textarea.blur).toHaveBeenCalled();
232
254
  });
233
255
  });
256
+
257
+ describe('character limit validation', () => {
258
+ it('should display the character counter starting at 0 / 300', async () => {
259
+ const element = createTestComponent();
260
+ await flushPromises();
261
+
262
+ const counter = element.shadowRoot.querySelector(selectors.counter);
263
+ expect(counter.textContent.trim()).toBe('0 / 300');
264
+ });
265
+
266
+ it('should update the character counter as the user types', async () => {
267
+ const element = createTestComponent();
268
+ await flushPromises();
269
+
270
+ const textTest = 'hello';
271
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
272
+ textarea.value = textTest;
273
+ textarea.dispatchEvent(new CustomEvent('input'));
274
+ await flushPromises();
275
+
276
+ const counter = element.shadowRoot.querySelector(selectors.counter);
277
+ expect(counter.textContent.trim()).toBe(`${textTest.length} / 300`);
278
+ });
279
+
280
+ it('should not count leading and trailing spaces in the character counter', async () => {
281
+ const element = createTestComponent();
282
+ await flushPromises();
283
+
284
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
285
+ textarea.value = ' hello ';
286
+ textarea.dispatchEvent(new CustomEvent('input'));
287
+ await flushPromises();
288
+
289
+ const counter = element.shadowRoot.querySelector(selectors.counter);
290
+ expect(counter.textContent.trim()).toBe('5 / 300');
291
+ });
292
+
293
+ it('should not flag the input as over the limit when trailing spaces push the raw length past the limit', async () => {
294
+ const element = createTestComponent();
295
+ await flushPromises();
296
+
297
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
298
+ textarea.value = `${'a'.repeat(MAX_LENGTH)} `;
299
+ textarea.dispatchEvent(new CustomEvent('input'));
300
+ await flushPromises();
301
+
302
+ const counter = element.shadowRoot.querySelector(selectors.counter);
303
+ expect(counter.textContent.trim()).toBe('300 / 300');
304
+
305
+ const container = element.shadowRoot.querySelector(selectors.container);
306
+ expect(container.classList).not.toContain(errorClass);
307
+
308
+ const message = element.shadowRoot.querySelector(
309
+ selectors.validationMessage
310
+ );
311
+ expect(message).toBeNull();
312
+ });
313
+
314
+ it('should show the exceeded count when over the limit', async () => {
315
+ const element = createTestComponent();
316
+ await flushPromises();
317
+
318
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
319
+ textarea.value = 'a'.repeat(MAX_LENGTH + 26);
320
+ textarea.dispatchEvent(new CustomEvent('input'));
321
+ await flushPromises();
322
+
323
+ const counter = element.shadowRoot.querySelector(selectors.counter);
324
+ expect(counter.textContent.trim()).toBe('326 / 300');
325
+ });
326
+
327
+ it('should put the input container in an error state and display a validation message when over the limit', async () => {
328
+ const element = createTestComponent();
329
+ await flushPromises();
330
+
331
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
332
+ textarea.value = 'a'.repeat(MAX_LENGTH + 1);
333
+ textarea.dispatchEvent(new CustomEvent('input'));
334
+ await flushPromises();
335
+
336
+ const container = element.shadowRoot.querySelector(selectors.container);
337
+ expect(container.classList).toContain(errorClass);
338
+ const message = element.shadowRoot.querySelector(
339
+ selectors.validationMessage
340
+ );
341
+ expect(message).not.toBeNull();
342
+ expect(message.textContent.trim()).toBe(
343
+ 'Your question is too long. Please use 300 characters or fewer.'
344
+ );
345
+ });
346
+
347
+ it('should not display a validation message when within the limit', async () => {
348
+ const element = createTestComponent();
349
+ await flushPromises();
350
+
351
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
352
+ textarea.value = 'a'.repeat(MAX_LENGTH);
353
+ textarea.dispatchEvent(new CustomEvent('input'));
354
+ await flushPromises();
355
+
356
+ const message = element.shadowRoot.querySelector(
357
+ selectors.validationMessage
358
+ );
359
+ expect(message).toBeNull();
360
+ });
361
+
362
+ it('should associate the textarea with the validation message via aria-describedby when over the limit', async () => {
363
+ const element = createTestComponent();
364
+ await flushPromises();
365
+
366
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
367
+ expect(textarea.getAttribute('aria-describedby')).toBeNull();
368
+
369
+ textarea.value = 'a'.repeat(MAX_LENGTH + 1);
370
+ textarea.dispatchEvent(new CustomEvent('input'));
371
+ await flushPromises();
372
+
373
+ const message = element.shadowRoot.querySelector(
374
+ selectors.validationMessage
375
+ );
376
+ expect(message.id).toBeTruthy();
377
+ expect(textarea.getAttribute('aria-describedby')).toBe(message.id);
378
+ });
379
+
380
+ it('should remove aria-describedby once the value is back within the limit', async () => {
381
+ const element = createTestComponent();
382
+ await flushPromises();
383
+
384
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
385
+ textarea.value = 'a'.repeat(MAX_LENGTH + 1);
386
+ textarea.dispatchEvent(new CustomEvent('input'));
387
+ await flushPromises();
388
+
389
+ expect(textarea.getAttribute('aria-describedby')).not.toBeNull();
390
+
391
+ textarea.value = 'a'.repeat(MAX_LENGTH);
392
+ textarea.dispatchEvent(new CustomEvent('input'));
393
+ await flushPromises();
394
+
395
+ expect(textarea.getAttribute('aria-describedby')).toBeNull();
396
+ });
397
+
398
+ it('should disable the submit button when over the limit', async () => {
399
+ const element = createTestComponent();
400
+ await flushPromises();
401
+
402
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
403
+ textarea.value = 'a'.repeat(MAX_LENGTH + 1);
404
+ textarea.dispatchEvent(new CustomEvent('input'));
405
+ await flushPromises();
406
+
407
+ const submitButton = element.shadowRoot.querySelector(
408
+ selectors.submitButton
409
+ );
410
+ expect(submitButton.disabled).toBe(true);
411
+ });
412
+
413
+ it('should not dispatch the event when over the limit', async () => {
414
+ const element = createTestComponent();
415
+ await flushPromises();
416
+
417
+ const handler = jest.fn();
418
+ element.addEventListener('quantic__submitfollowup', handler);
419
+
420
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
421
+ textarea.value = 'a'.repeat(MAX_LENGTH + 1);
422
+ textarea.dispatchEvent(new CustomEvent('input'));
423
+ await flushPromises();
424
+
425
+ textarea.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
426
+
427
+ expect(handler).not.toHaveBeenCalled();
428
+ });
429
+
430
+ it('should clear the error state once the value is back within the limit', async () => {
431
+ const element = createTestComponent();
432
+ await flushPromises();
433
+
434
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
435
+ textarea.value = 'a'.repeat(MAX_LENGTH + 1);
436
+ textarea.dispatchEvent(new CustomEvent('input'));
437
+ await flushPromises();
438
+
439
+ let container = element.shadowRoot.querySelector(selectors.container);
440
+ expect(container.classList).toContain(errorClass);
441
+
442
+ textarea.value = 'a'.repeat(MAX_LENGTH);
443
+ textarea.dispatchEvent(new CustomEvent('input'));
444
+ await flushPromises();
445
+
446
+ container = element.shadowRoot.querySelector(selectors.container);
447
+ expect(container.classList).not.toContain(errorClass);
448
+ });
449
+
450
+ it('should reset the character counter after a successful submit', async () => {
451
+ const element = createTestComponent();
452
+ await flushPromises();
453
+
454
+ const textarea = element.shadowRoot.querySelector(selectors.textarea);
455
+ textarea.value = 'a question';
456
+ textarea.dispatchEvent(new CustomEvent('input'));
457
+ await flushPromises();
458
+
459
+ textarea.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
460
+ await flushPromises();
461
+
462
+ const counter = element.shadowRoot.querySelector(selectors.counter);
463
+ expect(counter.textContent.trim()).toBe('0 / 300');
464
+ });
465
+ });
234
466
  });
@@ -2,8 +2,8 @@
2
2
  min-height: calc(2.5rem + 2px);
3
3
  }
4
4
 
5
- .follow-up-input__container--focused {
6
- border-color: var(--lwc-brandAccessible, #0176d3);
5
+ .follow-up-input__container--error {
6
+ border-color: var(--lwc-colorBorderError, #ea001e);
7
7
  }
8
8
 
9
9
  .follow-up-input__expander {
@@ -7,6 +7,8 @@
7
7
  rows="1"
8
8
  placeholder={labels.askFollowUp}
9
9
  aria-label={labels.askFollowUp}
10
+ aria-invalid={textareaAriaInvalid}
11
+ aria-describedby={textareaAriaDescribedBy}
10
12
  onfocus={handleFocus}
11
13
  onblur={handleBlur}
12
14
  oninput={handleInput}
@@ -14,7 +16,7 @@
14
16
  ></textarea>
15
17
  </div>
16
18
  <lightning-button-icon
17
- disabled={submitButtonDisabled}
19
+ disabled={isSubmitPrevented}
18
20
  onclick={handleSubmitFollowUp}
19
21
  icon-name="utility:arrowup"
20
22
  variant="brand"
@@ -27,4 +29,17 @@
27
29
  <label>{labels.submitFollowUp}</label>
28
30
  </lightning-button-icon>
29
31
  </div>
32
+ <div
33
+ class="follow-up-input__footer slds-grid slds-p-horizontal_x-small slds-p-top_xx-small"
34
+ >
35
+ <template lwc:if={isOverCharacterLimit}>
36
+ <span
37
+ id="follow-up-validation-message"
38
+ class="slds-text-body_small slds-text-color_error"
39
+ role="alert"
40
+ >{validationMessage}
41
+ </span>
42
+ </template>
43
+ <span class={characterCounterClasses}>{characterCounterText}</span>
44
+ </div>
30
45
  </template>
@@ -1,8 +1,14 @@
1
1
  import submitFollowUp from '@salesforce/label/c.quantic_SubmitFollowUp';
2
2
  import askFollowUp from '@salesforce/label/c.quantic_AskFollowUp';
3
- import {keys} from 'c/quanticUtils';
3
+ import followUpInputTooLong from '@salesforce/label/c.quantic_FollowUpInputTooLong';
4
+ import {I18nUtils, keys} from 'c/quanticUtils';
4
5
  import {api, LightningElement} from 'lwc';
5
6
 
7
+ /**
8
+ * Maximum number of characters allowed in a follow-up question.
9
+ */
10
+ const MAX_FOLLOW_UP_QUESTION_LENGTH = 300;
11
+
6
12
  /**
7
13
  * The `QuanticGeneratedAnswerFollowUpInput` component allows users to submit follow-up questions related to a generated answer.
8
14
  * @fires CustomEvent#quantic__submitfollowup
@@ -14,6 +20,7 @@ export default class QuanticGeneratedAnswerFollowUpInput extends LightningElemen
14
20
  labels = {
15
21
  submitFollowUp,
16
22
  askFollowUp,
23
+ followUpInputTooLong,
17
24
  };
18
25
 
19
26
  /**
@@ -27,6 +34,9 @@ export default class QuanticGeneratedAnswerFollowUpInput extends LightningElemen
27
34
  /** @type {boolean} */
28
35
  _focused = false;
29
36
 
37
+ /** @type {number} */
38
+ characterCount = 0;
39
+
30
40
  handleKeyDown(event) {
31
41
  // Let the browser commit IME text before handling shortcuts like Enter during composition.
32
42
  if (event.isComposing || event.keyCode === 229) {
@@ -47,13 +57,14 @@ export default class QuanticGeneratedAnswerFollowUpInput extends LightningElemen
47
57
 
48
58
  handleSubmitFollowUp() {
49
59
  if (
50
- this.submitButtonDisabled ||
60
+ this.isSubmitPrevented ||
51
61
  this.refs.askFollowUpInput.value.trim() === ''
52
62
  ) {
53
63
  return;
54
64
  }
55
65
  this.sendSubmitFollowUpEvent();
56
66
  this.refs.askFollowUpInput.value = '';
67
+ this.characterCount = 0;
57
68
  this.syncTextWithReplica();
58
69
  this.collapse();
59
70
  }
@@ -70,6 +81,7 @@ export default class QuanticGeneratedAnswerFollowUpInput extends LightningElemen
70
81
  }
71
82
 
72
83
  handleInput() {
84
+ this.characterCount = this.refs.askFollowUpInput.value.trim().length;
73
85
  this.expand();
74
86
  this.syncTextWithReplica();
75
87
  }
@@ -105,13 +117,44 @@ export default class QuanticGeneratedAnswerFollowUpInput extends LightningElemen
105
117
  bubbles: true,
106
118
  composed: true,
107
119
  detail: {
108
- value: this.refs.askFollowUpInput.value,
120
+ value: this.refs.askFollowUpInput.value.trim(),
109
121
  },
110
122
  })
111
123
  );
112
124
  }
113
125
 
114
126
  get inputContainerClasses() {
115
- return `follow-up-input__container slds-is-relative slds-grid slds-box slds-p-around_none ${this._focused ? 'follow-up-input__container--focused' : ''}`;
127
+ return `follow-up-input__container slds-is-relative slds-grid slds-box slds-p-around_none ${this.isOverCharacterLimit ? 'follow-up-input__container--error' : ''}`;
128
+ }
129
+
130
+ get isOverCharacterLimit() {
131
+ return this.characterCount > MAX_FOLLOW_UP_QUESTION_LENGTH;
132
+ }
133
+
134
+ get isSubmitPrevented() {
135
+ return this.submitButtonDisabled || this.isOverCharacterLimit;
136
+ }
137
+
138
+ get textareaAriaInvalid() {
139
+ return this.isOverCharacterLimit ? 'true' : null;
140
+ }
141
+
142
+ get textareaAriaDescribedBy() {
143
+ return this.isOverCharacterLimit ? 'follow-up-validation-message' : null;
144
+ }
145
+
146
+ get characterCounterText() {
147
+ return `${this.characterCount} / ${MAX_FOLLOW_UP_QUESTION_LENGTH}`;
148
+ }
149
+
150
+ get characterCounterClasses() {
151
+ return `follow-up-input__counter slds-col_bump-left slds-text-body_small ${this.isOverCharacterLimit ? 'slds-text-color_error' : 'slds-text-color_weak'}`;
152
+ }
153
+
154
+ get validationMessage() {
155
+ return I18nUtils.format(
156
+ this.labels.followUpInputTooLong,
157
+ MAX_FOLLOW_UP_QUESTION_LENGTH
158
+ );
116
159
  }
117
160
  }
@@ -1,7 +1,21 @@
1
- import {resolveSteps} from '../quanticGeneratedAnswerStreamOfThought.js';
2
1
  import {buildCreateTestComponent, cleanup, flushPromises} from 'c/testUtils';
3
2
  import QuanticGeneratedAnswerStreamOfThought from '../quanticGeneratedAnswerStreamOfThought';
4
3
 
4
+ jest.mock(
5
+ '@salesforce/label/c.quantic_AgentGenerationStepSearchQuery',
6
+ () => ({default: 'Searching for {{0}}.'}),
7
+ {
8
+ virtual: true,
9
+ }
10
+ );
11
+ jest.mock(
12
+ '@salesforce/label/c.quantic_AgentGenerationStepSearchQueryCompleted',
13
+ () => ({default: 'Searched for {{0}}'}),
14
+ {
15
+ virtual: true,
16
+ }
17
+ );
18
+
5
19
  const selectors = {
6
20
  stepItem: '[data-testid="step-item"]',
7
21
  spinner: '[data-testid="spinner"]',
@@ -20,46 +34,6 @@ const createTestComponent = buildCreateTestComponent(
20
34
  {agentSteps: [], isStreaming: false}
21
35
  );
22
36
 
23
- describe('#resolveSteps', () => {
24
- it('should return empty array for empty input', () => {
25
- expect(resolveSteps([])).toEqual([]);
26
- });
27
-
28
- it('should resolve a full sequence correctly', () => {
29
- expect(
30
- resolveSteps([
31
- {name: 'thinking', status: 'completed', startedAt: 0},
32
- {name: 'searching', status: 'completed', startedAt: 10},
33
- {name: 'thinking', status: 'completed', startedAt: 20},
34
- {name: 'answering', status: 'active', startedAt: 30},
35
- ])
36
- ).toEqual([
37
- {name: 'thinking-before-search', status: 'completed'},
38
- {name: 'searching', status: 'completed'},
39
- {name: 'thinking-after-search', status: 'completed'},
40
- {name: 'answering', status: 'active'},
41
- ]);
42
- });
43
-
44
- it('should handle repeated searching steps', () => {
45
- expect(
46
- resolveSteps([
47
- {name: 'thinking', status: 'completed', startedAt: 0},
48
- {name: 'searching', status: 'completed', startedAt: 10},
49
- {name: 'thinking', status: 'completed', startedAt: 20},
50
- {name: 'searching', status: 'completed', startedAt: 30},
51
- {name: 'thinking', status: 'active', startedAt: 40},
52
- ])
53
- ).toEqual([
54
- {name: 'thinking-before-search', status: 'completed'},
55
- {name: 'searching', status: 'completed'},
56
- {name: 'thinking-after-search', status: 'completed'},
57
- {name: 'searching', status: 'completed'},
58
- {name: 'thinking-after-search', status: 'active'},
59
- ]);
60
- });
61
- });
62
-
63
37
  describe('quantic generated answer stream of thought component', () => {
64
38
  afterEach(() => {
65
39
  cleanup();
@@ -152,6 +126,86 @@ describe('quantic generated answer stream of thought component', () => {
152
126
  expect(collapseButton).toBeNull();
153
127
  expect(collapsedSummary).toBeNull();
154
128
  });
129
+
130
+ it('should render a searching-with-query step when toolCall has a query', async () => {
131
+ const element = createTestComponent({
132
+ agentSteps: [
133
+ {
134
+ name: 'searching',
135
+ status: 'active',
136
+ startedAt: 0,
137
+ toolCalls: [
138
+ {
139
+ toolCallName: 'search',
140
+ toolCallId: 'tc-1',
141
+ startedAt: 0,
142
+ status: 'active',
143
+ type: 'search',
144
+ toolCallArgs: {q: 'how to reset password'},
145
+ },
146
+ ],
147
+ },
148
+ ],
149
+ isStreaming: true,
150
+ });
151
+ await flushPromises();
152
+
153
+ const stepItem = element.shadowRoot.querySelector(selectors.stepItem);
154
+ expect(stepItem.getAttribute('data-step-name')).toBe(
155
+ 'searching-with-query'
156
+ );
157
+
158
+ const stepLabel = element.shadowRoot.querySelector(selectors.stepLabel);
159
+ expect(stepLabel.textContent).toContain('how to reset password');
160
+ });
161
+
162
+ it('should expand multiple search toolCalls into separate step items', async () => {
163
+ const element = createTestComponent({
164
+ agentSteps: [
165
+ {
166
+ name: 'searching',
167
+ status: 'completed',
168
+ startedAt: 0,
169
+ toolCalls: [
170
+ {
171
+ toolCallName: 'search',
172
+ toolCallId: 'tc-1',
173
+ startedAt: 0,
174
+ finishedAt: 1,
175
+ status: 'completed',
176
+ type: 'search',
177
+ toolCallArgs: {q: 'What is Quantic'},
178
+ },
179
+ {
180
+ toolCallName: 'search',
181
+ toolCallId: 'tc-2',
182
+ startedAt: 1,
183
+ finishedAt: 2,
184
+ status: 'completed',
185
+ type: 'search',
186
+ toolCallArgs: {q: 'What is Atomic'},
187
+ },
188
+ ],
189
+ },
190
+ ],
191
+ isStreaming: true,
192
+ });
193
+ await flushPromises();
194
+
195
+ const stepItems = element.shadowRoot.querySelectorAll(selectors.stepItem);
196
+ expect(stepItems).toHaveLength(2);
197
+ stepItems.forEach((item) =>
198
+ expect(item.getAttribute('data-step-name')).toBe('searching-with-query')
199
+ );
200
+
201
+ const stepLabelsText = Array.from(
202
+ element.shadowRoot.querySelectorAll(selectors.stepLabel)
203
+ )
204
+ .map((el) => el.textContent ?? '')
205
+ .join(' ');
206
+ expect(stepLabelsText).toContain('What is Quantic');
207
+ expect(stepLabelsText).toContain('What is Atomic');
208
+ });
155
209
  });
156
210
 
157
211
  describe('after streaming completes', () => {