@coveo/quantic 3.37.9 → 3.38.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 (39) hide show
  1. package/force-app/main/default/lwc/quanticFeedback/__tests__/quanticFeedback.test.js +11 -0
  2. package/force-app/main/default/lwc/quanticFeedback/quanticFeedback.html +6 -4
  3. package/force-app/main/default/lwc/quanticFeedback/quanticFeedback.js +8 -0
  4. package/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js +32 -3
  5. package/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.css +1 -8
  6. package/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js +6 -28
  7. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.css +0 -13
  8. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html +120 -93
  9. package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/retryPrompt.html +24 -15
  10. package/force-app/main/default/lwc/quanticGeneratedAnswerCopyToClipboard/__tests__/quanticGeneratedAnswerCopyToClipboard.test.js +125 -0
  11. package/force-app/main/default/lwc/quanticGeneratedAnswerCopyToClipboard/quanticGeneratedAnswerCopyToClipboard.html +1 -1
  12. package/force-app/main/default/lwc/quanticGeneratedAnswerCopyToClipboard/quanticGeneratedAnswerCopyToClipboard.js +20 -1
  13. package/force-app/main/default/lwc/quanticResultHighlightedTextField/quanticResultHighlightedTextField.js +10 -1
  14. package/force-app/main/default/lwc/quanticResultLink/quanticResultLink.js +11 -4
  15. package/force-app/main/default/lwc/quanticResultQuickview/quanticResultQuickview.js +14 -2
  16. package/force-app/main/default/lwc/quanticThreadItem/__tests__/quanticThreadItem.test.js +176 -0
  17. package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.css +64 -0
  18. package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.html +48 -0
  19. package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.js +80 -0
  20. package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.js-meta.xml +5 -0
  21. package/force-app/main/default/lwc/quanticUtils/__tests__/quanticUtils.test.js +30 -0
  22. package/force-app/main/default/lwc/quanticUtils/quanticUtils.js +24 -0
  23. package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +15 -15
  24. package/force-app/main/default/staticresources/coveoheadless/definitions/api/knowledge/stream-answer-api.d.ts +1 -1
  25. package/force-app/main/default/staticresources/coveoheadless/definitions/api/platform-client.d.ts +5 -0
  26. package/force-app/main/default/staticresources/coveoheadless/definitions/app/case-assist-engine/case-assist-engine-configuration.d.ts +1 -1
  27. package/force-app/main/default/staticresources/coveoheadless/definitions/app/insight-engine/insight-engine-configuration.d.ts +1 -1
  28. package/force-app/main/default/staticresources/coveoheadless/definitions/app/recommendation-engine/recommendation-engine-configuration.d.ts +1 -1
  29. package/force-app/main/default/staticresources/coveoheadless/definitions/app/search-engine/search-engine-configuration.d.ts +1 -1
  30. package/force-app/main/default/staticresources/coveoheadless/definitions/features/case-assist-configuration/case-assist-configuration-actions.d.ts +1 -1
  31. package/force-app/main/default/staticresources/coveoheadless/definitions/features/case-assist-configuration/case-assist-configuration-state.d.ts +1 -1
  32. package/force-app/main/default/staticresources/coveoheadless/definitions/features/configuration/configuration-actions.d.ts +1 -1
  33. package/force-app/main/default/staticresources/coveoheadless/definitions/features/configuration/configuration-state.d.ts +1 -1
  34. package/force-app/main/default/staticresources/coveoheadless/definitions/features/generated-answer/generated-answer-request.d.ts +2 -2
  35. package/force-app/main/default/staticresources/coveoheadless/headless.js +17 -17
  36. package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +16 -16
  37. package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +14 -14
  38. package/force-app/main/default/staticresources/dompurify/purify.min.js +2 -2
  39. package/package.json +6 -6
@@ -12,8 +12,27 @@ import {LightningElement, api} from 'lwc';
12
12
  export default class QuanticGeneratedAnswerCopyToClipboard extends LightningElement {
13
13
  /**
14
14
  * The answer to copy
15
+ * @type {string}
15
16
  */
16
- @api answer;
17
+ @api answer = '';
18
+
19
+ /**
20
+ * The size of the copy icon.
21
+ * @api
22
+ * @type {'xx-small' | 'x-small' | 'small' | 'medium' | 'large'}
23
+ */
24
+ @api
25
+ get size() {
26
+ return this._size;
27
+ }
28
+ set size(value) {
29
+ if (['xx-small', 'x-small', 'small', 'medium', 'large'].includes(value)) {
30
+ this._size = value;
31
+ }
32
+ }
33
+
34
+ /** @type {'xx-small' | 'x-small' | 'small' | 'medium' | 'large'} */
35
+ _size = 'xx-small';
17
36
 
18
37
  labels = {
19
38
  copy,
@@ -4,6 +4,7 @@ import {
4
4
  initializeWithHeadless,
5
5
  getHeadlessBundle,
6
6
  } from 'c/quanticHeadlessLoader';
7
+ import {unwrapLockerProxiedObject} from 'c/quanticUtils';
7
8
  import {LightningElement, api} from 'lwc';
8
9
 
9
10
  /** @typedef {import("coveo").Result} Result */
@@ -28,7 +29,13 @@ export default class QuanticResultHighlightedTextField extends LightningElement
28
29
  * @api
29
30
  * @type {Result}
30
31
  */
31
- @api result;
32
+ @api
33
+ get result() {
34
+ return this._result;
35
+ }
36
+ set result(result) {
37
+ this._result = unwrapLockerProxiedObject(result);
38
+ }
32
39
  /**
33
40
  * (Optional) The label to display.
34
41
  * @api
@@ -49,6 +56,8 @@ export default class QuanticResultHighlightedTextField extends LightningElement
49
56
  isInitialized = false;
50
57
  /** @type {boolean} */
51
58
  validated = false;
59
+ /** @type {Result} */
60
+ _result;
52
61
 
53
62
  connectedCallback() {
54
63
  this.validateProps();
@@ -4,7 +4,7 @@ import {
4
4
  getHeadlessBundle,
5
5
  getHeadlessEnginePromise,
6
6
  } from 'c/quanticHeadlessLoader';
7
- import {ResultUtils} from 'c/quanticUtils';
7
+ import {ResultUtils, unwrapLockerProxiedObject} from 'c/quanticUtils';
8
8
  import {NavigationMixin} from 'lightning/navigation';
9
9
  import {LightningElement, api} from 'lwc';
10
10
 
@@ -38,7 +38,13 @@ export default class QuanticResultLink extends NavigationMixin(
38
38
  * @api
39
39
  * @type {Result}
40
40
  */
41
- @api result;
41
+ @api
42
+ get result() {
43
+ return this._result;
44
+ }
45
+ set result(result) {
46
+ this._result = unwrapLockerProxiedObject(result);
47
+ }
42
48
  /**
43
49
  * Where to display the linked URL, as the name for a browsing context (a tab, window, or <iframe>).
44
50
  * The following keywords have special meanings for where to load the URL:
@@ -75,6 +81,8 @@ export default class QuanticResultLink extends NavigationMixin(
75
81
  engine;
76
82
  /** @type {AnyHeadless} */
77
83
  headless;
84
+ /** @type {Result} */
85
+ _result;
78
86
  /** @type {string} */
79
87
  salesforceRecordUrl;
80
88
 
@@ -113,8 +121,7 @@ export default class QuanticResultLink extends NavigationMixin(
113
121
  this.engine = engine;
114
122
  ResultUtils.bindClickEventsOnResult(
115
123
  this.engine,
116
- // Destructuring transforms the Proxy object created by Salesforce to a normal object so no unexpected behaviour will occur with the Headless library.
117
- {...this.result, raw: {...this.result.raw}},
124
+ this.result,
118
125
  this.template,
119
126
  this.headless.buildInteractiveResult
120
127
  );
@@ -8,7 +8,11 @@ import {
8
8
  HeadlessBundleNames,
9
9
  isHeadlessBundle,
10
10
  } from 'c/quanticHeadlessLoader';
11
- import {I18nUtils, getLastFocusableElement} from 'c/quanticUtils';
11
+ import {
12
+ I18nUtils,
13
+ getLastFocusableElement,
14
+ unwrapLockerProxiedObject,
15
+ } from 'c/quanticUtils';
12
16
  import {LightningElement, api, track} from 'lwc';
13
17
 
14
18
  /** @typedef {import("coveo").Result} Result */
@@ -42,7 +46,13 @@ export default class QuanticResultQuickview extends LightningElement {
42
46
  * @api
43
47
  * @type {ResultWithFolding}
44
48
  */
45
- @api result;
49
+ @api
50
+ get result() {
51
+ return this._result;
52
+ }
53
+ set result(result) {
54
+ this._result = unwrapLockerProxiedObject(result);
55
+ }
46
56
  /**
47
57
  * The maximum preview size to retrieve, in bytes. By default, the full preview is retrieved.
48
58
  * @api
@@ -84,6 +94,8 @@ export default class QuanticResultQuickview extends LightningElement {
84
94
 
85
95
  /** @type {Quickview} */
86
96
  quickview;
97
+ /** @type {ResultWithFolding} */
98
+ _result;
87
99
  /** @type {boolean} */
88
100
  isQuickviewOpen = false;
89
101
  /** @type {Function} */
@@ -0,0 +1,176 @@
1
+ // @ts-ignore
2
+ import QuanticThreadItem from '../quanticThreadItem';
3
+ import {buildCreateTestComponent, cleanup, flushPromises} from 'c/testUtils';
4
+
5
+ const selectors = {
6
+ titleButton: '[data-testid="thread-item-title-button"]',
7
+ titleSpan: '[data-testid="thread-item-title-static"]',
8
+ boldTitle: '.slds-text-title_bold',
9
+ contentWrapper: '[data-testid="thread-item-content"]',
10
+ visibleContent: '[data-testid="thread-item-content"] > div:not([hidden])',
11
+ line: '[data-testid="thread-item-line"]',
12
+ dot: '[data-testid="thread-item-dot"]',
13
+ };
14
+
15
+ const createTestComponent = buildCreateTestComponent(
16
+ QuanticThreadItem,
17
+ 'c-quantic-thread-item',
18
+ {
19
+ title: 'Test title',
20
+ }
21
+ );
22
+
23
+ describe('c-quantic-thread-item', () => {
24
+ afterEach(() => {
25
+ cleanup();
26
+ });
27
+
28
+ describe('initial rendering', () => {
29
+ it('renders a button when collapse is enabled', async () => {
30
+ const element = createTestComponent();
31
+ await flushPromises();
32
+
33
+ const button = element.shadowRoot.querySelector(selectors.titleButton);
34
+ expect(button).not.toBeNull();
35
+ });
36
+
37
+ it('renders a span instead of a button when disableCollapse is true', async () => {
38
+ const element = createTestComponent({disableCollapse: true});
39
+ await flushPromises();
40
+
41
+ const button = element.shadowRoot.querySelector(selectors.titleButton);
42
+ const span = element.shadowRoot.querySelector(selectors.titleSpan);
43
+ expect(button).toBeNull();
44
+ expect(span).not.toBeNull();
45
+ });
46
+
47
+ it('renders the timeline line by default', async () => {
48
+ const element = createTestComponent();
49
+ await flushPromises();
50
+
51
+ const line = element.shadowRoot.querySelector(selectors.line);
52
+ expect(line).not.toBeNull();
53
+ });
54
+
55
+ it('hides the timeline line when hideLine is true', async () => {
56
+ const element = createTestComponent({hideLine: true});
57
+ await flushPromises();
58
+
59
+ const line = element.shadowRoot.querySelector(selectors.line);
60
+ expect(line).toBeNull();
61
+ });
62
+ });
63
+
64
+ describe('collapsed state', () => {
65
+ it('does not render the content by default', async () => {
66
+ const element = createTestComponent();
67
+ await flushPromises();
68
+
69
+ const content = element.shadowRoot.querySelector(
70
+ selectors.visibleContent
71
+ );
72
+ expect(content).toBeNull();
73
+ });
74
+
75
+ it('button has aria-expanded set to false when collapsed', async () => {
76
+ const element = createTestComponent();
77
+ await flushPromises();
78
+
79
+ const button = element.shadowRoot.querySelector(selectors.titleButton);
80
+ expect(button.getAttribute('aria-expanded')).toBe('false');
81
+ });
82
+
83
+ it('dot does not have expanded class when collapsed', async () => {
84
+ const element = createTestComponent();
85
+ await flushPromises();
86
+
87
+ const dot = element.shadowRoot.querySelector(selectors.dot);
88
+ expect(dot.className).not.toContain('thread-item__dot--expanded');
89
+ });
90
+ });
91
+
92
+ describe('expanded state', () => {
93
+ it('renders the content when isExpanded is true', async () => {
94
+ const element = createTestComponent({isExpanded: true});
95
+ await flushPromises();
96
+
97
+ const content = element.shadowRoot.querySelector(
98
+ selectors.visibleContent
99
+ );
100
+ expect(content).not.toBeNull();
101
+ });
102
+
103
+ it('button has aria-expanded set to true when expanded', async () => {
104
+ const element = createTestComponent({isExpanded: true});
105
+ await flushPromises();
106
+
107
+ const button = element.shadowRoot.querySelector(selectors.titleButton);
108
+ expect(button.getAttribute('aria-expanded')).toBe('true');
109
+ });
110
+
111
+ it('dot has expanded class when expanded', async () => {
112
+ const element = createTestComponent({isExpanded: true});
113
+ await flushPromises();
114
+
115
+ const dot = element.shadowRoot.querySelector(selectors.dot);
116
+ expect(dot.className).toContain('thread-item__dot--expanded');
117
+ });
118
+ });
119
+
120
+ describe('toggle interaction', () => {
121
+ it('renders the content when the button is clicked while collapsed', async () => {
122
+ const element = createTestComponent();
123
+ await flushPromises();
124
+
125
+ element.shadowRoot.querySelector(selectors.titleButton).click();
126
+ await flushPromises();
127
+
128
+ const content = element.shadowRoot.querySelector(
129
+ selectors.visibleContent
130
+ );
131
+ expect(content).not.toBeNull();
132
+ });
133
+
134
+ it('does not render the content when the button is clicked while expanded', async () => {
135
+ const element = createTestComponent({isExpanded: true});
136
+ await flushPromises();
137
+
138
+ element.shadowRoot.querySelector(selectors.titleButton).click();
139
+ await flushPromises();
140
+
141
+ const content = element.shadowRoot.querySelector(
142
+ selectors.visibleContent
143
+ );
144
+ expect(content).toBeNull();
145
+ });
146
+ });
147
+
148
+ describe('disableCollapse', () => {
149
+ it('forces item to be expanded regardless of isExpanded prop', async () => {
150
+ const element = createTestComponent({
151
+ disableCollapse: true,
152
+ isExpanded: false,
153
+ });
154
+ await flushPromises();
155
+
156
+ const content = element.shadowRoot.querySelector(
157
+ selectors.visibleContent
158
+ );
159
+ expect(content).not.toBeNull();
160
+ });
161
+
162
+ it('renders a bold title when collapse is disabled', async () => {
163
+ const element = createTestComponent({
164
+ disableCollapse: true,
165
+ isExpanded: false,
166
+ });
167
+ await flushPromises();
168
+
169
+ const title = element.shadowRoot.querySelector(selectors.titleSpan);
170
+ expect(title.className).toContain('slds-text-title_bold');
171
+ expect(
172
+ element.shadowRoot.querySelector(selectors.boldTitle)
173
+ ).not.toBeNull();
174
+ });
175
+ });
176
+ });
@@ -0,0 +1,64 @@
1
+ .thread-item {
2
+ list-style: none;
3
+ }
4
+
5
+ .thread-item__dot-column,
6
+ .thread-item__line-column {
7
+ width: 10px;
8
+ }
9
+
10
+ .thread-item__line-column {
11
+ align-items: stretch;
12
+ }
13
+
14
+ .thread-item__dot {
15
+ height: 8px;
16
+ width: 8px;
17
+ border-radius: 50%;
18
+ background-color: var(--lwc-colorBorder, #dddbda);
19
+ }
20
+
21
+ .thread-item__dot--expanded {
22
+ background-color: var(--lwc-colorTextDefault, #080707);
23
+ }
24
+
25
+ .thread-item__line {
26
+ position: relative;
27
+ width: 1px;
28
+ height: 100%;
29
+ background-color: var(--lwc-colorBorder, #dddbda);
30
+ }
31
+
32
+ .thread-item__line::before,
33
+ .thread-item__line::after {
34
+ content: '';
35
+ position: absolute;
36
+ left: 0;
37
+ width: 1px;
38
+ height: 8px;
39
+ background-color: var(--lwc-colorBorder, #dddbda);
40
+ }
41
+
42
+ .thread-item__line::before {
43
+ top: -8px;
44
+ }
45
+
46
+ .thread-item__line::after {
47
+ bottom: -8px;
48
+ }
49
+
50
+ .thread-item__clamped-text {
51
+ display: -webkit-box;
52
+ -webkit-line-clamp: 3;
53
+ -webkit-box-orient: vertical;
54
+ overflow: hidden;
55
+ }
56
+
57
+ .thread-item__title-button {
58
+ width: fit-content;
59
+ border-radius: var(--slds-g-radius-border-2, 4px);
60
+ }
61
+
62
+ .thread-item__title-button:hover {
63
+ background-color: var(--lwc-colorBackgroundRowHover, #f3f2f2);
64
+ }
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <li class="thread-item">
3
+ <div class="slds-grid slds-grid_vertical-align-center">
4
+ <div
5
+ class="slds-grid slds-grid_align-center slds-grid_vertical-align-center slds-shrink-none slds-m-right_x-small thread-item__dot-column"
6
+ >
7
+ <span class={dotClass} data-testid="thread-item-dot"></span>
8
+ </div>
9
+ <div class="slds-col slds-has-flexi-truncate">
10
+ <template lwc:if={disableCollapse}>
11
+ <span class={titleClass} data-testid="thread-item-title-static"
12
+ >{title}</span
13
+ >
14
+ </template>
15
+ <template lwc:else>
16
+ <button
17
+ type="button"
18
+ class={titleButtonClass}
19
+ aria-expanded={isExpanded}
20
+ data-testid="thread-item-title-button"
21
+ onclick={handleTitleClick}
22
+ aria-controls="thread-item-content"
23
+ >
24
+ {title}
25
+ </button>
26
+ </template>
27
+ </div>
28
+ </div>
29
+ <div class="slds-grid">
30
+ <div
31
+ class="slds-grid slds-grid_align-center slds-shrink-none thread-item__line-column slds-m-right_x-small"
32
+ >
33
+ <template lwc:if={shouldDisplayLine}>
34
+ <span class="thread-item__line" data-testid="thread-item-line"></span>
35
+ </template>
36
+ </div>
37
+ <div
38
+ id="thread-item-content"
39
+ class="slds-col slds-p-left_x-small slds-p-vertical_xx-small"
40
+ data-testid="thread-item-content"
41
+ >
42
+ <div hidden={contentHidden}>
43
+ <slot></slot>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </li>
48
+ </template>
@@ -0,0 +1,80 @@
1
+ import {LightningElement, api} from 'lwc';
2
+
3
+ /**
4
+ * The `QuanticThreadItem` component renders a thread item with timeline visuals and collapsible content.
5
+ * @category Internal
6
+ * @example
7
+ * <c-quantic-thread-item title="Step title" hide-line is-expanded></c-quantic-thread-item>
8
+ */
9
+ export default class QuanticThreadItem extends LightningElement {
10
+ /**
11
+ * The title displayed for the thread item.
12
+ * @api
13
+ * @type {string}
14
+ */
15
+ @api title = '';
16
+
17
+ /**
18
+ * Whether the thread item can be expanded or collapsed.
19
+ * @api
20
+ * @type {boolean}
21
+ * @defaultValue `false`
22
+ */
23
+ @api disableCollapse = false;
24
+
25
+ /**
26
+ * Whether the timeline line should be hidden.
27
+ * @api
28
+ * @type {boolean}
29
+ * @defaultValue `false`
30
+ */
31
+ @api hideLine = false;
32
+
33
+ /**
34
+ * Whether the thread item is expanded.
35
+ * @api
36
+ * @type {boolean}
37
+ * @defaultValue `false`
38
+ */
39
+ @api
40
+ get isExpanded() {
41
+ return this._isExpanded;
42
+ }
43
+ set isExpanded(value) {
44
+ this._isExpanded = value;
45
+ }
46
+
47
+ /** @type {boolean} */
48
+ _isExpanded = false;
49
+
50
+ connectedCallback() {
51
+ this._isExpanded = this.disableCollapse ? true : this.isExpanded;
52
+ }
53
+
54
+ handleTitleClick() {
55
+ if (this.disableCollapse) {
56
+ return;
57
+ }
58
+ this._isExpanded = !this._isExpanded;
59
+ }
60
+
61
+ get shouldDisplayLine() {
62
+ return !this.hideLine;
63
+ }
64
+
65
+ get contentHidden() {
66
+ return !this._isExpanded;
67
+ }
68
+
69
+ get titleClass() {
70
+ return `thread-item__clamped-text slds-text-body_regular slds-p-horizontal_x-small slds-p-vertical_xx-small${this._isExpanded ? ' slds-text-title_bold' : ''}`;
71
+ }
72
+
73
+ get titleButtonClass() {
74
+ return `slds-button_reset slds-p-horizontal_x-small slds-p-vertical_xx-small thread-item__title-button thread-item__clamped-text${this._isExpanded ? ' slds-text-title_bold' : ''}`;
75
+ }
76
+
77
+ get dotClass() {
78
+ return `thread-item__dot${this._isExpanded ? ' thread-item__dot--expanded' : ''}`;
79
+ }
80
+ }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <apiVersion>66.0</apiVersion>
4
+ <isExposed>false</isExposed>
5
+ </LightningComponentBundle>
@@ -2,6 +2,7 @@ import {
2
2
  I18nUtils,
3
3
  buildTemplateTextFromResult,
4
4
  copyToClipboard,
5
+ unwrapLockerProxiedObject,
5
6
  } from 'c/quanticUtils';
6
7
 
7
8
  describe('c/quanticUtils', () => {
@@ -200,4 +201,33 @@ describe('c/quanticUtils', () => {
200
201
  );
201
202
  });
202
203
  });
204
+
205
+ describe('unwrapLockerObject', () => {
206
+ it('should deeply clone complex objects while preserving primitive values', () => {
207
+ const original = {
208
+ title: 'Example',
209
+ raw: {
210
+ foo: 'bar',
211
+ tags: ['a', {value: 'b'}],
212
+ },
213
+ score: 42,
214
+ };
215
+
216
+ const unwrapped = unwrapLockerProxiedObject(original);
217
+
218
+ expect(unwrapped).toEqual(original);
219
+ expect(unwrapped).not.toBe(original);
220
+ expect(unwrapped.raw).not.toBe(original.raw);
221
+ expect(unwrapped.raw.tags).not.toBe(original.raw.tags);
222
+ expect(unwrapped.raw.tags[1]).not.toBe(original.raw.tags[1]);
223
+ });
224
+
225
+ it('should return primitive values as-is', () => {
226
+ expect(unwrapLockerProxiedObject(undefined)).toBeUndefined();
227
+ expect(unwrapLockerProxiedObject(null)).toBeNull();
228
+ expect(unwrapLockerProxiedObject('value')).toBe('value');
229
+ expect(unwrapLockerProxiedObject(0)).toBe(0);
230
+ expect(unwrapLockerProxiedObject(false)).toBe(false);
231
+ });
232
+ });
203
233
  });
@@ -259,6 +259,30 @@ export function parseXML(string) {
259
259
  return new window.DOMParser().parseFromString(string, 'text/xml');
260
260
  }
261
261
 
262
+ /**
263
+ * Recursively clones objects to break Locker proxy chains.
264
+ * @param {any} value
265
+ * @returns {any}
266
+ */
267
+ export function unwrapLockerProxiedObject(value) {
268
+ if (value === null || typeof value !== 'object') {
269
+ return value;
270
+ }
271
+
272
+ if (Array.isArray(value)) {
273
+ return value.map((item) => unwrapLockerProxiedObject(item));
274
+ }
275
+
276
+ const unwrappedValue = {};
277
+ for (const key in value) {
278
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
279
+ unwrappedValue[key] = unwrapLockerProxiedObject(value[key]);
280
+ }
281
+ }
282
+
283
+ return unwrappedValue;
284
+ }
285
+
262
286
  /**
263
287
  * Utility class for managing a simple in-memory store.
264
288
  * Supports registering and retrieving facet and sort option data.