@adobe/spacecat-shared-tokowaka-client 1.11.1 → 1.12.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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [@adobe/spacecat-shared-tokowaka-client-v1.12.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.11.2...@adobe/spacecat-shared-tokowaka-client-v1.12.0) (2026-03-19)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* **AGENTCOM-466:** switch commerce enrichment mapper to semantic HTML ([#1435](https://github.com/adobe/spacecat-shared/issues/1435)) ([05a8fd9](https://github.com/adobe/spacecat-shared/commit/05a8fd9c8574edcd59317abcebb2ba0b3c3e038c))
|
|
6
|
+
|
|
7
|
+
## [@adobe/spacecat-shared-tokowaka-client-v1.11.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.11.1...@adobe/spacecat-shared-tokowaka-client-v1.11.2) (2026-03-15)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **deps:** update external fixes ([#1440](https://github.com/adobe/spacecat-shared/issues/1440)) ([d1a583a](https://github.com/adobe/spacecat-shared/commit/d1a583aca6a68378debbc01e3a3e8796f1f228bf))
|
|
12
|
+
|
|
1
13
|
## [@adobe/spacecat-shared-tokowaka-client-v1.11.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.11.0...@adobe/spacecat-shared-tokowaka-client-v1.11.1) (2026-03-10)
|
|
2
14
|
|
|
3
15
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/spacecat-shared-tokowaka-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Tokowaka Client for SpaceCat - Edge optimization config management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@adobe/spacecat-shared-utils": "1.81.1",
|
|
38
|
-
"@aws-sdk/client-cloudfront": "3.
|
|
39
|
-
"@aws-sdk/client-s3": "3.
|
|
38
|
+
"@aws-sdk/client-cloudfront": "3.1009.0",
|
|
39
|
+
"@aws-sdk/client-s3": "3.1009.0",
|
|
40
40
|
"hast-util-from-html": "2.0.3",
|
|
41
41
|
"mdast-util-from-markdown": "2.0.3",
|
|
42
42
|
"mdast-util-to-hast": "13.2.1",
|
|
@@ -13,19 +13,112 @@
|
|
|
13
13
|
import { hasText } from '@adobe/spacecat-shared-utils';
|
|
14
14
|
import { TARGET_USER_AGENTS_CATEGORIES } from '../constants.js';
|
|
15
15
|
import BaseOpportunityMapper from './base-mapper.js';
|
|
16
|
+
import { htmlToHast } from '../utils/html-utils.js';
|
|
16
17
|
|
|
17
18
|
const EXCLUDED_FIELDS = new Set([
|
|
18
19
|
'rationale',
|
|
19
20
|
]);
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// Fields rendered in fixed order at the top of the article.
|
|
23
|
+
// Each entry: CSS class, display label, [source keys in priority order]
|
|
24
|
+
const ORDERED_FIELDS = [
|
|
25
|
+
{ cls: 'category', sources: ['facts.facets.category_path', 'category'] },
|
|
26
|
+
{ cls: 'description', sources: ['pdp.description_plain', 'description'] },
|
|
27
|
+
{ cls: 'features', sources: ['pdp.feature_bullets'] },
|
|
28
|
+
{ cls: 'variants', sources: ['facts.variants.summary'] },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function escapeHtml(str) {
|
|
32
|
+
return String(str)
|
|
33
|
+
.replace(/&/g, '&')
|
|
34
|
+
.replace(/</g, '<')
|
|
35
|
+
.replace(/>/g, '>')
|
|
36
|
+
.replace(/"/g, '"');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sanitizeClassName(key) {
|
|
40
|
+
return key
|
|
41
|
+
.replace(/^(pdp\.|facts\.facets\.|facts\.variants\.|facts\.attributes\.)/, '')
|
|
42
|
+
.replace(/[^a-z0-9-]/gi, '-')
|
|
43
|
+
.replace(/-+/g, '-')
|
|
44
|
+
.replace(/^-|-$/g, '')
|
|
45
|
+
.toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderCategoryPath(value) {
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
return escapeHtml(value.join(' \u203A '));
|
|
51
|
+
}
|
|
52
|
+
return escapeHtml(String(value));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderList(items, cls) {
|
|
56
|
+
const lis = items.map((item) => `<li>${escapeHtml(String(item))}</li>`).join('');
|
|
57
|
+
return `<ul class="${cls}">${lis}</ul>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderValue(key, value, cls) {
|
|
61
|
+
if (value == null) return '';
|
|
62
|
+
|
|
63
|
+
if (key === 'facts.facets.category_path' || key === 'category') {
|
|
64
|
+
return `<p class="${cls}">${renderCategoryPath(value)}</p>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
if (value.length === 0) return '';
|
|
69
|
+
return renderList(value, cls);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (typeof value === 'object') {
|
|
73
|
+
const entries = Object.entries(value).filter(([, v]) => v != null && v !== '');
|
|
74
|
+
if (entries.length === 0) return '';
|
|
75
|
+
const items = entries.map(([k, v]) => `${escapeHtml(k)}: ${escapeHtml(String(v))}`);
|
|
76
|
+
return renderList(items, cls);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const str = String(value);
|
|
80
|
+
if (!str) return '';
|
|
81
|
+
return `<p class="${cls}">${escapeHtml(str)}</p>`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function enrichmentToHtml(enrichmentData) {
|
|
85
|
+
const sku = enrichmentData.sku || '';
|
|
86
|
+
const consumed = new Set(['sku', ...EXCLUDED_FIELDS]);
|
|
87
|
+
const parts = [];
|
|
88
|
+
|
|
89
|
+
// Render ordered fields first
|
|
90
|
+
for (const { cls, sources } of ORDERED_FIELDS) {
|
|
91
|
+
let matched = false;
|
|
92
|
+
for (const sourceKey of sources) {
|
|
93
|
+
if (!matched && sourceKey in enrichmentData && enrichmentData[sourceKey] != null) {
|
|
94
|
+
const html = renderValue(sourceKey, enrichmentData[sourceKey], cls);
|
|
95
|
+
if (html) {
|
|
96
|
+
parts.push(html);
|
|
97
|
+
matched = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Consume all sources in the group so alternatives don't appear in remaining fields
|
|
102
|
+
for (const sourceKey of sources) {
|
|
103
|
+
consumed.add(sourceKey);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Render remaining fields in document order
|
|
23
108
|
for (const [key, value] of Object.entries(enrichmentData)) {
|
|
24
|
-
if (
|
|
25
|
-
|
|
109
|
+
if (consumed.has(key)) {
|
|
110
|
+
// eslint-disable-next-line no-continue
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const cls = sanitizeClassName(key);
|
|
114
|
+
const html = renderValue(key, value, cls);
|
|
115
|
+
if (html) {
|
|
116
|
+
parts.push(html);
|
|
26
117
|
}
|
|
27
118
|
}
|
|
28
|
-
|
|
119
|
+
|
|
120
|
+
const skuAttr = sku ? ` data-sku="${escapeHtml(sku)}"` : '';
|
|
121
|
+
return `<div data-enrichment="spacecat"${skuAttr}><article>${parts.join('')}</article></div>`;
|
|
29
122
|
}
|
|
30
123
|
|
|
31
124
|
export default class CommercePageEnrichmentMapper extends BaseOpportunityMapper {
|
|
@@ -76,17 +169,16 @@ export default class CommercePageEnrichmentMapper extends BaseOpportunityMapper
|
|
|
76
169
|
|
|
77
170
|
const data = suggestion.getData();
|
|
78
171
|
const enrichmentData = JSON.parse(data.patchValue);
|
|
79
|
-
const
|
|
172
|
+
const html = enrichmentToHtml(enrichmentData);
|
|
173
|
+
const value = htmlToHast(html);
|
|
80
174
|
|
|
81
175
|
patches.push({
|
|
82
176
|
...this.createBasePatch(suggestion, opportunityId),
|
|
83
177
|
op: 'appendChild',
|
|
84
|
-
selector: '
|
|
178
|
+
selector: 'body',
|
|
85
179
|
value,
|
|
86
|
-
valueFormat: '
|
|
180
|
+
valueFormat: 'hast',
|
|
87
181
|
target: TARGET_USER_AGENTS_CATEGORIES.AI_BOTS,
|
|
88
|
-
tag: 'script',
|
|
89
|
-
attrs: { type: 'application/json' },
|
|
90
182
|
});
|
|
91
183
|
});
|
|
92
184
|
|
|
@@ -16,6 +16,43 @@ import { expect } from 'chai';
|
|
|
16
16
|
import sinon from 'sinon';
|
|
17
17
|
import CommercePageEnrichmentMapper from '../../src/mappers/commerce-page-enrichment-mapper.js';
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Recursively finds the first HAST element node matching a predicate.
|
|
21
|
+
*/
|
|
22
|
+
function findNode(node, predicate) {
|
|
23
|
+
if (predicate(node)) return node;
|
|
24
|
+
if (node.children) {
|
|
25
|
+
for (const child of node.children) {
|
|
26
|
+
const found = findNode(child, predicate);
|
|
27
|
+
if (found) return found;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Collects all HAST element nodes matching a predicate.
|
|
35
|
+
*/
|
|
36
|
+
function findAllNodes(node, predicate) {
|
|
37
|
+
const results = [];
|
|
38
|
+
if (predicate(node)) results.push(node);
|
|
39
|
+
if (node.children) {
|
|
40
|
+
for (const child of node.children) {
|
|
41
|
+
results.push(...findAllNodes(child, predicate));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extracts concatenated text content from a HAST subtree.
|
|
49
|
+
*/
|
|
50
|
+
function textContent(node) {
|
|
51
|
+
if (node.type === 'text') return node.value;
|
|
52
|
+
if (node.children) return node.children.map(textContent).join('');
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
|
|
19
56
|
describe('CommercePageEnrichmentMapper', () => {
|
|
20
57
|
let mapper;
|
|
21
58
|
let log;
|
|
@@ -194,7 +231,7 @@ describe('CommercePageEnrichmentMapper', () => {
|
|
|
194
231
|
};
|
|
195
232
|
}
|
|
196
233
|
|
|
197
|
-
it('should produce a
|
|
234
|
+
it('should produce a HAST patch appended to body', () => {
|
|
198
235
|
const suggestion = makeSuggestion({
|
|
199
236
|
patchValue: JSON.stringify({
|
|
200
237
|
sku: 'HT5695',
|
|
@@ -215,18 +252,51 @@ describe('CommercePageEnrichmentMapper', () => {
|
|
|
215
252
|
const patch = patches[0];
|
|
216
253
|
|
|
217
254
|
expect(patch.op).to.equal('appendChild');
|
|
218
|
-
expect(patch.selector).to.equal('
|
|
219
|
-
expect(patch.valueFormat).to.equal('
|
|
255
|
+
expect(patch.selector).to.equal('body');
|
|
256
|
+
expect(patch.valueFormat).to.equal('hast');
|
|
220
257
|
expect(patch.target).to.equal('ai-bots');
|
|
221
|
-
expect(patch.tag).to.equal('script');
|
|
222
|
-
expect(patch.attrs).to.deep.equal({ type: 'application/json' });
|
|
223
258
|
expect(patch.opportunityId).to.equal(opportunityId);
|
|
224
259
|
expect(patch.suggestionId).to.equal(suggestionId);
|
|
225
260
|
expect(patch.prerenderRequired).to.be.true;
|
|
226
261
|
expect(patch.lastUpdated).to.be.a('number');
|
|
262
|
+
|
|
263
|
+
// HAST root
|
|
264
|
+
expect(patch.value).to.be.an('object');
|
|
265
|
+
expect(patch.value.type).to.equal('root');
|
|
266
|
+
expect(patch.value.children).to.be.an('array');
|
|
227
267
|
});
|
|
228
268
|
|
|
229
|
-
it('should
|
|
269
|
+
it('should produce HAST with correct wrapper and article structure', () => {
|
|
270
|
+
const suggestion = makeSuggestion({
|
|
271
|
+
patchValue: JSON.stringify({
|
|
272
|
+
sku: 'HT5695',
|
|
273
|
+
name: 'Seat Cover Set',
|
|
274
|
+
brand: 'Lovesac',
|
|
275
|
+
'pdp.description_plain': 'A great product.',
|
|
276
|
+
}),
|
|
277
|
+
url: 'https://www.lovesac.com/products/seat-cover-set',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const patches = mapper.suggestionsToPatches(
|
|
281
|
+
'/products/seat-cover-set',
|
|
282
|
+
[suggestion],
|
|
283
|
+
opportunityId,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const { value } = patches[0];
|
|
287
|
+
|
|
288
|
+
// Outer div with data-enrichment="spacecat" and data-sku
|
|
289
|
+
const wrapper = findNode(value, (n) => n.tagName === 'div'
|
|
290
|
+
&& n.properties?.dataEnrichment === 'spacecat');
|
|
291
|
+
expect(wrapper).to.exist;
|
|
292
|
+
expect(wrapper.properties.dataSku).to.equal('HT5695');
|
|
293
|
+
|
|
294
|
+
// Article inside the wrapper
|
|
295
|
+
const article = findNode(wrapper, (n) => n.tagName === 'article');
|
|
296
|
+
expect(article).to.exist;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should render enrichment fields as semantic HTML elements', () => {
|
|
230
300
|
const suggestion = makeSuggestion({
|
|
231
301
|
patchValue: JSON.stringify({
|
|
232
302
|
sku: 'HT5695',
|
|
@@ -247,13 +317,66 @@ describe('CommercePageEnrichmentMapper', () => {
|
|
|
247
317
|
);
|
|
248
318
|
|
|
249
319
|
const { value } = patches[0];
|
|
250
|
-
|
|
251
|
-
expect(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
expect(
|
|
256
|
-
|
|
320
|
+
const article = findNode(value, (n) => n.tagName === 'article');
|
|
321
|
+
expect(article).to.exist;
|
|
322
|
+
|
|
323
|
+
// String fields → <p> elements
|
|
324
|
+
const paragraphs = findAllNodes(article, (n) => n.tagName === 'p');
|
|
325
|
+
expect(paragraphs.length).to.be.greaterThan(0);
|
|
326
|
+
|
|
327
|
+
// Category path → rendered with › separator
|
|
328
|
+
const categoryP = paragraphs.find((p) => p.properties?.className?.includes('category'));
|
|
329
|
+
expect(categoryP).to.exist;
|
|
330
|
+
expect(textContent(categoryP)).to.include('›');
|
|
331
|
+
|
|
332
|
+
// Description → <p class="description">
|
|
333
|
+
const descP = paragraphs.find((p) => p.properties?.className?.includes('description'));
|
|
334
|
+
expect(descP).to.exist;
|
|
335
|
+
expect(textContent(descP)).to.equal('A great product.');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should render array fields as unordered lists', () => {
|
|
339
|
+
const suggestion = makeSuggestion({
|
|
340
|
+
patchValue: JSON.stringify({
|
|
341
|
+
sku: 'HT5695',
|
|
342
|
+
'pdp.feature_bullets': ['Includes 4 Seats', 'StealthTech eligible'],
|
|
343
|
+
}),
|
|
344
|
+
url: 'https://example.com/page',
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
|
|
348
|
+
const { value } = patches[0];
|
|
349
|
+
|
|
350
|
+
const ul = findNode(value, (n) => n.tagName === 'ul'
|
|
351
|
+
&& n.properties?.className?.includes('features'));
|
|
352
|
+
expect(ul).to.exist;
|
|
353
|
+
|
|
354
|
+
const lis = ul.children.filter((c) => c.tagName === 'li');
|
|
355
|
+
expect(lis).to.have.length(2);
|
|
356
|
+
expect(textContent(lis[0])).to.equal('Includes 4 Seats');
|
|
357
|
+
expect(textContent(lis[1])).to.equal('StealthTech eligible');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should render object fields as lists of key-value entries', () => {
|
|
361
|
+
const suggestion = makeSuggestion({
|
|
362
|
+
patchValue: JSON.stringify({
|
|
363
|
+
sku: 'HT5695',
|
|
364
|
+
variants: { color: 'Blue', size: 'Large' },
|
|
365
|
+
}),
|
|
366
|
+
url: 'https://example.com/page',
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
|
|
370
|
+
const { value } = patches[0];
|
|
371
|
+
|
|
372
|
+
const ul = findNode(value, (n) => n.tagName === 'ul'
|
|
373
|
+
&& n.properties?.className?.includes('variants'));
|
|
374
|
+
expect(ul).to.exist;
|
|
375
|
+
|
|
376
|
+
const lis = ul.children.filter((c) => c.tagName === 'li');
|
|
377
|
+
expect(lis).to.have.length(2);
|
|
378
|
+
expect(textContent(lis[0])).to.include('color');
|
|
379
|
+
expect(textContent(lis[0])).to.include('Blue');
|
|
257
380
|
});
|
|
258
381
|
|
|
259
382
|
it('should handle minimal enrichment data (only sku)', () => {
|
|
@@ -269,10 +392,16 @@ describe('CommercePageEnrichmentMapper', () => {
|
|
|
269
392
|
);
|
|
270
393
|
|
|
271
394
|
expect(patches).to.have.length(1);
|
|
272
|
-
|
|
395
|
+
const { value } = patches[0];
|
|
396
|
+
expect(value.type).to.equal('root');
|
|
397
|
+
|
|
398
|
+
const wrapper = findNode(value, (n) => n.tagName === 'div'
|
|
399
|
+
&& n.properties?.dataEnrichment === 'spacecat');
|
|
400
|
+
expect(wrapper).to.exist;
|
|
401
|
+
expect(wrapper.properties.dataSku).to.equal('MINIMAL');
|
|
273
402
|
});
|
|
274
403
|
|
|
275
|
-
it('should
|
|
404
|
+
it('should render all enrichment field types correctly', () => {
|
|
276
405
|
const enrichment = {
|
|
277
406
|
sku: '4Seats5Sides',
|
|
278
407
|
name: '4 Seats + 5 Sides Sactional',
|
|
@@ -305,7 +434,25 @@ describe('CommercePageEnrichmentMapper', () => {
|
|
|
305
434
|
);
|
|
306
435
|
|
|
307
436
|
expect(patches).to.have.length(1);
|
|
308
|
-
|
|
437
|
+
const { value } = patches[0];
|
|
438
|
+
const article = findNode(value, (n) => n.tagName === 'article');
|
|
439
|
+
expect(article).to.exist;
|
|
440
|
+
|
|
441
|
+
// Ordered fields rendered: category_path used (not category), description, features, variants
|
|
442
|
+
const allText = textContent(article);
|
|
443
|
+
expect(allText).to.include('Furniture');
|
|
444
|
+
expect(allText).to.include('›');
|
|
445
|
+
expect(allText).to.include('A modular sofa configuration.');
|
|
446
|
+
expect(allText).to.include('Includes 4 Seats');
|
|
447
|
+
expect(allText).to.include('Multiple fabric options');
|
|
448
|
+
|
|
449
|
+
// Remaining fields rendered
|
|
450
|
+
expect(allText).to.include('4 Seats + 5 Sides Sactional');
|
|
451
|
+
expect(allText).to.include('Lovesac');
|
|
452
|
+
expect(allText).to.include('Corded Velvet');
|
|
453
|
+
expect(allText).to.include('Grey');
|
|
454
|
+
expect(allText).to.include('homeowners');
|
|
455
|
+
expect(allText).to.include('modular couch');
|
|
309
456
|
});
|
|
310
457
|
|
|
311
458
|
it('should skip ineligible suggestions and log warning', () => {
|
|
@@ -357,6 +504,7 @@ describe('CommercePageEnrichmentMapper', () => {
|
|
|
357
504
|
patchValue: JSON.stringify({
|
|
358
505
|
sku: 'HT5695',
|
|
359
506
|
rationale: 'This should not appear in output',
|
|
507
|
+
brand: 'Lovesac',
|
|
360
508
|
}),
|
|
361
509
|
url: 'https://example.com/page',
|
|
362
510
|
});
|
|
@@ -367,8 +515,9 @@ describe('CommercePageEnrichmentMapper', () => {
|
|
|
367
515
|
opportunityId,
|
|
368
516
|
);
|
|
369
517
|
|
|
370
|
-
|
|
371
|
-
expect(
|
|
518
|
+
const allText = textContent(patches[0].value);
|
|
519
|
+
expect(allText).to.not.include('This should not appear in output');
|
|
520
|
+
expect(allText).to.include('Lovesac');
|
|
372
521
|
});
|
|
373
522
|
|
|
374
523
|
it('should exclude null values from output', () => {
|
|
@@ -387,7 +536,146 @@ describe('CommercePageEnrichmentMapper', () => {
|
|
|
387
536
|
opportunityId,
|
|
388
537
|
);
|
|
389
538
|
|
|
390
|
-
|
|
539
|
+
const allText = textContent(patches[0].value);
|
|
540
|
+
expect(allText).to.include('Lovesac');
|
|
541
|
+
// null name should not produce any element
|
|
542
|
+
const article = findNode(patches[0].value, (n) => n.tagName === 'article');
|
|
543
|
+
const nameP = findAllNodes(article, (n) => n.tagName === 'p'
|
|
544
|
+
&& n.properties?.className?.includes('name'));
|
|
545
|
+
expect(nameP).to.have.length(0);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('should render string category as plain text with › not needed', () => {
|
|
549
|
+
const suggestion = makeSuggestion({
|
|
550
|
+
patchValue: JSON.stringify({
|
|
551
|
+
sku: 'TEST',
|
|
552
|
+
category: 'Electronics',
|
|
553
|
+
}),
|
|
554
|
+
url: 'https://example.com/page',
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
|
|
558
|
+
const article = findNode(patches[0].value, (n) => n.tagName === 'article');
|
|
559
|
+
const categoryP = findNode(article, (n) => n.tagName === 'p'
|
|
560
|
+
&& n.properties?.className?.includes('category'));
|
|
561
|
+
expect(categoryP).to.exist;
|
|
562
|
+
expect(textContent(categoryP)).to.equal('Electronics');
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('should skip empty arrays and empty objects', () => {
|
|
566
|
+
const suggestion = makeSuggestion({
|
|
567
|
+
patchValue: JSON.stringify({
|
|
568
|
+
sku: 'TEST',
|
|
569
|
+
'pdp.feature_bullets': [],
|
|
570
|
+
variants: {},
|
|
571
|
+
brand: 'TestBrand',
|
|
572
|
+
}),
|
|
573
|
+
url: 'https://example.com/page',
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
|
|
577
|
+
const article = findNode(patches[0].value, (n) => n.tagName === 'article');
|
|
578
|
+
const allText = textContent(article);
|
|
579
|
+
expect(allText).to.include('TestBrand');
|
|
580
|
+
|
|
581
|
+
// Empty array and empty object should not produce elements
|
|
582
|
+
const featureUl = findNode(article, (n) => n.tagName === 'ul'
|
|
583
|
+
&& n.properties?.className?.includes('features'));
|
|
584
|
+
expect(featureUl).to.not.exist;
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('should skip empty string values', () => {
|
|
588
|
+
const suggestion = makeSuggestion({
|
|
589
|
+
patchValue: JSON.stringify({
|
|
590
|
+
sku: 'TEST',
|
|
591
|
+
name: '',
|
|
592
|
+
brand: 'Visible',
|
|
593
|
+
}),
|
|
594
|
+
url: 'https://example.com/page',
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
|
|
598
|
+
const article = findNode(patches[0].value, (n) => n.tagName === 'article');
|
|
599
|
+
const allText = textContent(article);
|
|
600
|
+
expect(allText).to.include('Visible');
|
|
601
|
+
|
|
602
|
+
const nameP = findAllNodes(article, (n) => n.tagName === 'p'
|
|
603
|
+
&& n.properties?.className?.includes('name'));
|
|
604
|
+
expect(nameP).to.have.length(0);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('should filter null and empty entries from object values', () => {
|
|
608
|
+
const suggestion = makeSuggestion({
|
|
609
|
+
patchValue: JSON.stringify({
|
|
610
|
+
sku: 'TEST',
|
|
611
|
+
variants: { color: 'Blue', size: null, weight: '' },
|
|
612
|
+
}),
|
|
613
|
+
url: 'https://example.com/page',
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
|
|
617
|
+
const ul = findNode(patches[0].value, (n) => n.tagName === 'ul'
|
|
618
|
+
&& n.properties?.className?.includes('variants'));
|
|
619
|
+
expect(ul).to.exist;
|
|
620
|
+
const lis = ul.children.filter((c) => c.tagName === 'li');
|
|
621
|
+
expect(lis).to.have.length(1);
|
|
622
|
+
expect(textContent(lis[0])).to.include('Blue');
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('should produce empty article when object has only null entries', () => {
|
|
626
|
+
const suggestion = makeSuggestion({
|
|
627
|
+
patchValue: JSON.stringify({
|
|
628
|
+
sku: 'TEST',
|
|
629
|
+
variants: { color: null, size: '' },
|
|
630
|
+
}),
|
|
631
|
+
url: 'https://example.com/page',
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
|
|
635
|
+
const ul = findNode(patches[0].value, (n) => n.tagName === 'ul'
|
|
636
|
+
&& n.properties?.className?.includes('variants'));
|
|
637
|
+
expect(ul).to.not.exist;
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('should handle enrichment with no sku', () => {
|
|
641
|
+
const suggestion = makeSuggestion({
|
|
642
|
+
patchValue: JSON.stringify({
|
|
643
|
+
brand: 'NoBrand',
|
|
644
|
+
}),
|
|
645
|
+
url: 'https://example.com/page',
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
|
|
649
|
+
const wrapper = findNode(patches[0].value, (n) => n.tagName === 'div'
|
|
650
|
+
&& n.properties?.dataEnrichment === 'spacecat');
|
|
651
|
+
expect(wrapper).to.exist;
|
|
652
|
+
expect(wrapper.properties.dataSku).to.be.undefined;
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('should use ordered fields priority (category_path over category)', () => {
|
|
656
|
+
const suggestion = makeSuggestion({
|
|
657
|
+
patchValue: JSON.stringify({
|
|
658
|
+
sku: 'TEST',
|
|
659
|
+
category: 'Simple Category',
|
|
660
|
+
'facts.facets.category_path': ['Home', 'Furniture'],
|
|
661
|
+
}),
|
|
662
|
+
url: 'https://example.com/page',
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
|
|
666
|
+
const { value } = patches[0];
|
|
667
|
+
const article = findNode(value, (n) => n.tagName === 'article');
|
|
668
|
+
|
|
669
|
+
// category_path should be used (has priority), not the flat category
|
|
670
|
+
const categoryP = findNode(article, (n) => n.tagName === 'p'
|
|
671
|
+
&& n.properties?.className?.includes('category'));
|
|
672
|
+
expect(categoryP).to.exist;
|
|
673
|
+
expect(textContent(categoryP)).to.include('Home');
|
|
674
|
+
expect(textContent(categoryP)).to.include('›');
|
|
675
|
+
|
|
676
|
+
// Flat 'category' should NOT appear since category_path was consumed
|
|
677
|
+
const allText = textContent(article);
|
|
678
|
+
expect(allText).to.not.include('Simple Category');
|
|
391
679
|
});
|
|
392
680
|
});
|
|
393
681
|
});
|