@adobe/spacecat-shared-tokowaka-client 1.1.0 → 1.2.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 +7 -0
- package/README.md +144 -19
- package/package.json +1 -1
- package/src/index.d.ts +81 -15
- package/src/index.js +314 -260
- package/src/mappers/base-mapper.js +1 -1
- package/src/mappers/faq-mapper.js +18 -23
- package/src/utils/custom-html-utils.js +1 -0
- package/src/utils/patch-utils.js +12 -25
- package/src/utils/s3-utils.js +100 -7
- package/test/index.test.js +808 -990
- package/test/mappers/base-mapper.test.js +72 -88
- package/test/mappers/faq-mapper.test.js +61 -97
- package/test/utils/html-utils.test.js +10 -12
- package/test/utils/patch-utils.test.js +204 -235
- package/test/utils/s3-utils.test.js +140 -0
- package/test/utils/site-utils.test.js +80 -0
- package/test/utils/suggestion-utils.test.js +187 -0
|
@@ -227,34 +227,30 @@ describe('BaseOpportunityMapper', () => {
|
|
|
227
227
|
|
|
228
228
|
it('should remove patches by suggestion IDs using default implementation', () => {
|
|
229
229
|
const config = {
|
|
230
|
-
|
|
231
|
-
baseURL: 'https://example.com',
|
|
230
|
+
url: 'https://example.com/page1',
|
|
232
231
|
version: '1.0',
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
value: 'value-1',
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
opportunityId: 'opp-test',
|
|
245
|
-
suggestionId: 'sugg-2',
|
|
246
|
-
op: 'replace',
|
|
247
|
-
value: 'value-2',
|
|
248
|
-
},
|
|
249
|
-
],
|
|
232
|
+
forceFail: false,
|
|
233
|
+
prerender: true,
|
|
234
|
+
patches: [
|
|
235
|
+
{
|
|
236
|
+
opportunityId: 'opp-test',
|
|
237
|
+
suggestionId: 'sugg-1',
|
|
238
|
+
op: 'replace',
|
|
239
|
+
value: 'value-1',
|
|
250
240
|
},
|
|
251
|
-
|
|
241
|
+
{
|
|
242
|
+
opportunityId: 'opp-test',
|
|
243
|
+
suggestionId: 'sugg-2',
|
|
244
|
+
op: 'replace',
|
|
245
|
+
value: 'value-2',
|
|
246
|
+
},
|
|
247
|
+
],
|
|
252
248
|
};
|
|
253
249
|
|
|
254
250
|
const result = testMapper.rollbackPatches(config, ['sugg-1'], 'opp-test');
|
|
255
251
|
|
|
256
|
-
expect(result.
|
|
257
|
-
expect(result.
|
|
252
|
+
expect(result.patches).to.have.lengthOf(1);
|
|
253
|
+
expect(result.patches[0].suggestionId).to.equal('sugg-2');
|
|
258
254
|
expect(result.removedCount).to.equal(1);
|
|
259
255
|
});
|
|
260
256
|
|
|
@@ -268,101 +264,89 @@ describe('BaseOpportunityMapper', () => {
|
|
|
268
264
|
|
|
269
265
|
it('should remove patches for multiple suggestion IDs', () => {
|
|
270
266
|
const config = {
|
|
271
|
-
|
|
272
|
-
baseURL: 'https://example.com',
|
|
267
|
+
url: 'https://example.com/page1',
|
|
273
268
|
version: '1.0',
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
value: 'value-2',
|
|
289
|
-
},
|
|
290
|
-
{
|
|
291
|
-
opportunityId: 'opp-test',
|
|
292
|
-
suggestionId: 'sugg-3',
|
|
293
|
-
op: 'replace',
|
|
294
|
-
value: 'value-3',
|
|
295
|
-
},
|
|
296
|
-
],
|
|
269
|
+
forceFail: false,
|
|
270
|
+
prerender: true,
|
|
271
|
+
patches: [
|
|
272
|
+
{
|
|
273
|
+
opportunityId: 'opp-test',
|
|
274
|
+
suggestionId: 'sugg-1',
|
|
275
|
+
op: 'replace',
|
|
276
|
+
value: 'value-1',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
opportunityId: 'opp-test',
|
|
280
|
+
suggestionId: 'sugg-2',
|
|
281
|
+
op: 'replace',
|
|
282
|
+
value: 'value-2',
|
|
297
283
|
},
|
|
298
|
-
|
|
284
|
+
{
|
|
285
|
+
opportunityId: 'opp-test',
|
|
286
|
+
suggestionId: 'sugg-3',
|
|
287
|
+
op: 'replace',
|
|
288
|
+
value: 'value-3',
|
|
289
|
+
},
|
|
290
|
+
],
|
|
299
291
|
};
|
|
300
292
|
|
|
301
293
|
const result = testMapper.rollbackPatches(config, ['sugg-1', 'sugg-3'], 'opp-test');
|
|
302
294
|
|
|
303
|
-
expect(result.
|
|
304
|
-
expect(result.
|
|
295
|
+
expect(result.patches).to.have.lengthOf(1);
|
|
296
|
+
expect(result.patches[0].suggestionId).to.equal('sugg-2');
|
|
305
297
|
expect(result.removedCount).to.equal(2);
|
|
306
298
|
});
|
|
307
299
|
|
|
308
300
|
it('should remove URL path when all patches are removed', () => {
|
|
309
301
|
const config = {
|
|
310
|
-
|
|
311
|
-
baseURL: 'https://example.com',
|
|
302
|
+
url: 'https://example.com/page1',
|
|
312
303
|
version: '1.0',
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
value: 'value-1',
|
|
322
|
-
},
|
|
323
|
-
],
|
|
304
|
+
forceFail: false,
|
|
305
|
+
prerender: true,
|
|
306
|
+
patches: [
|
|
307
|
+
{
|
|
308
|
+
opportunityId: 'opp-test',
|
|
309
|
+
suggestionId: 'sugg-1',
|
|
310
|
+
op: 'replace',
|
|
311
|
+
value: 'value-1',
|
|
324
312
|
},
|
|
325
|
-
|
|
313
|
+
],
|
|
326
314
|
};
|
|
327
315
|
|
|
328
316
|
const result = testMapper.rollbackPatches(config, ['sugg-1'], 'opp-test');
|
|
329
317
|
|
|
330
|
-
//
|
|
331
|
-
expect(result.
|
|
318
|
+
// All patches removed, patches array should be empty
|
|
319
|
+
expect(result.patches).to.have.lengthOf(0);
|
|
332
320
|
expect(result.removedCount).to.equal(1);
|
|
333
321
|
});
|
|
334
322
|
|
|
335
323
|
it('should preserve patches from other opportunities', () => {
|
|
336
324
|
const config = {
|
|
337
|
-
|
|
338
|
-
baseURL: 'https://example.com',
|
|
325
|
+
url: 'https://example.com/page1',
|
|
339
326
|
version: '1.0',
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
value: 'other-value',
|
|
355
|
-
},
|
|
356
|
-
],
|
|
327
|
+
forceFail: false,
|
|
328
|
+
prerender: true,
|
|
329
|
+
patches: [
|
|
330
|
+
{
|
|
331
|
+
opportunityId: 'opp-test',
|
|
332
|
+
suggestionId: 'sugg-1',
|
|
333
|
+
op: 'replace',
|
|
334
|
+
value: 'test-value',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
opportunityId: 'opp-other',
|
|
338
|
+
suggestionId: 'sugg-2',
|
|
339
|
+
op: 'replace',
|
|
340
|
+
value: 'other-value',
|
|
357
341
|
},
|
|
358
|
-
|
|
342
|
+
],
|
|
359
343
|
};
|
|
360
344
|
|
|
361
345
|
// Default implementation removes by suggestionId regardless of opportunity
|
|
362
346
|
const result = testMapper.rollbackPatches(config, ['sugg-1'], 'opp-test');
|
|
363
347
|
|
|
364
|
-
expect(result.
|
|
365
|
-
expect(result.
|
|
348
|
+
expect(result.patches).to.have.lengthOf(1);
|
|
349
|
+
expect(result.patches[0].suggestionId).to.equal('sugg-2');
|
|
366
350
|
expect(result.removedCount).to.equal(1);
|
|
367
351
|
});
|
|
368
352
|
});
|
|
@@ -1325,132 +1325,96 @@ Overall, Bulk positions itself as a better choice for sports nutrition through i
|
|
|
1325
1325
|
describe('rollbackPatches', () => {
|
|
1326
1326
|
it('should remove FAQ heading when last FAQ suggestion is rolled back', () => {
|
|
1327
1327
|
const config = {
|
|
1328
|
-
|
|
1329
|
-
baseURL: 'https://example.com',
|
|
1328
|
+
url: 'https://example.com/page1',
|
|
1330
1329
|
version: '1.0',
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
value: { type: 'element', tagName: 'h2', children: [{ type: 'text', value: 'FAQs' }] },
|
|
1341
|
-
},
|
|
1342
|
-
{
|
|
1343
|
-
opportunityId: 'opp-faq',
|
|
1344
|
-
suggestionId: 'sugg-1',
|
|
1345
|
-
op: 'appendChild',
|
|
1346
|
-
value: { type: 'element', tagName: 'div' },
|
|
1347
|
-
},
|
|
1348
|
-
],
|
|
1330
|
+
forceFail: false,
|
|
1331
|
+
prerender: true,
|
|
1332
|
+
patches: [
|
|
1333
|
+
{
|
|
1334
|
+
opportunityId: 'opp-faq',
|
|
1335
|
+
// FAQ heading patch (no suggestionId)
|
|
1336
|
+
op: 'appendChild',
|
|
1337
|
+
selector: 'body',
|
|
1338
|
+
value: { type: 'element', tagName: 'h2', children: [{ type: 'text', value: 'FAQs' }] },
|
|
1349
1339
|
},
|
|
1350
|
-
|
|
1340
|
+
{
|
|
1341
|
+
opportunityId: 'opp-faq',
|
|
1342
|
+
suggestionId: 'sugg-1',
|
|
1343
|
+
op: 'appendChild',
|
|
1344
|
+
value: { type: 'element', tagName: 'div' },
|
|
1345
|
+
},
|
|
1346
|
+
],
|
|
1351
1347
|
};
|
|
1352
1348
|
|
|
1353
1349
|
const result = mapper.rollbackPatches(config, ['sugg-1'], 'opp-faq');
|
|
1354
1350
|
|
|
1355
1351
|
// Both FAQ item and heading should be removed
|
|
1356
|
-
expect(result.
|
|
1352
|
+
expect(result.patches).to.have.lengthOf(0);
|
|
1357
1353
|
expect(result.removedCount).to.equal(2);
|
|
1358
1354
|
});
|
|
1359
1355
|
|
|
1360
1356
|
it('should keep FAQ heading when other FAQ suggestions remain', () => {
|
|
1361
1357
|
const config = {
|
|
1362
|
-
|
|
1363
|
-
baseURL: 'https://example.com',
|
|
1358
|
+
url: 'https://example.com/page1',
|
|
1364
1359
|
version: '1.0',
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
value: { type: 'element', tagName: 'h2', children: [{ type: 'text', value: 'FAQs' }] },
|
|
1375
|
-
},
|
|
1376
|
-
{
|
|
1377
|
-
opportunityId: 'opp-faq',
|
|
1378
|
-
suggestionId: 'sugg-1',
|
|
1379
|
-
op: 'appendChild',
|
|
1380
|
-
value: { type: 'element', tagName: 'div' },
|
|
1381
|
-
},
|
|
1382
|
-
{
|
|
1383
|
-
opportunityId: 'opp-faq',
|
|
1384
|
-
suggestionId: 'sugg-2',
|
|
1385
|
-
op: 'appendChild',
|
|
1386
|
-
value: { type: 'element', tagName: 'div' },
|
|
1387
|
-
},
|
|
1388
|
-
],
|
|
1360
|
+
forceFail: false,
|
|
1361
|
+
prerender: true,
|
|
1362
|
+
patches: [
|
|
1363
|
+
{
|
|
1364
|
+
opportunityId: 'opp-faq',
|
|
1365
|
+
// FAQ heading patch
|
|
1366
|
+
op: 'appendChild',
|
|
1367
|
+
selector: 'body',
|
|
1368
|
+
value: { type: 'element', tagName: 'h2', children: [{ type: 'text', value: 'FAQs' }] },
|
|
1389
1369
|
},
|
|
1390
|
-
|
|
1370
|
+
{
|
|
1371
|
+
opportunityId: 'opp-faq',
|
|
1372
|
+
suggestionId: 'sugg-1',
|
|
1373
|
+
op: 'appendChild',
|
|
1374
|
+
value: { type: 'element', tagName: 'div' },
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
opportunityId: 'opp-faq',
|
|
1378
|
+
suggestionId: 'sugg-2',
|
|
1379
|
+
op: 'appendChild',
|
|
1380
|
+
value: { type: 'element', tagName: 'div' },
|
|
1381
|
+
},
|
|
1382
|
+
],
|
|
1391
1383
|
};
|
|
1392
1384
|
|
|
1393
1385
|
const result = mapper.rollbackPatches(config, ['sugg-1'], 'opp-faq');
|
|
1394
1386
|
|
|
1395
1387
|
// Only sugg-1 removed, heading and sugg-2 remain
|
|
1396
|
-
expect(result.
|
|
1397
|
-
expect(result.
|
|
1398
|
-
expect(result.
|
|
1388
|
+
expect(result.patches).to.have.lengthOf(2);
|
|
1389
|
+
expect(result.patches[0]).to.not.have.property('suggestionId'); // Heading
|
|
1390
|
+
expect(result.patches[1].suggestionId).to.equal('sugg-2');
|
|
1399
1391
|
expect(result.removedCount).to.equal(1);
|
|
1400
1392
|
});
|
|
1401
1393
|
|
|
1402
1394
|
it('should handle multiple URLs independently', () => {
|
|
1395
|
+
// Note: With the new per-URL architecture, each URL has its own config
|
|
1396
|
+
// This test validates that rollback works correctly for a single URL config
|
|
1403
1397
|
const config = {
|
|
1404
|
-
|
|
1405
|
-
baseURL: 'https://example.com',
|
|
1398
|
+
url: 'https://example.com/page1',
|
|
1406
1399
|
version: '1.0',
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
value: 'FAQ1',
|
|
1417
|
-
},
|
|
1418
|
-
],
|
|
1419
|
-
},
|
|
1420
|
-
'/page2': {
|
|
1421
|
-
prerender: true,
|
|
1422
|
-
patches: [
|
|
1423
|
-
{ opportunityId: 'opp-faq', op: 'appendChild', value: 'FAQs' },
|
|
1424
|
-
{
|
|
1425
|
-
opportunityId: 'opp-faq',
|
|
1426
|
-
suggestionId: 'sugg-2',
|
|
1427
|
-
op: 'appendChild',
|
|
1428
|
-
value: 'FAQ2',
|
|
1429
|
-
},
|
|
1430
|
-
{
|
|
1431
|
-
opportunityId: 'opp-faq',
|
|
1432
|
-
suggestionId: 'sugg-3',
|
|
1433
|
-
op: 'appendChild',
|
|
1434
|
-
value: 'FAQ3',
|
|
1435
|
-
},
|
|
1436
|
-
],
|
|
1400
|
+
forceFail: false,
|
|
1401
|
+
prerender: true,
|
|
1402
|
+
patches: [
|
|
1403
|
+
{ opportunityId: 'opp-faq', op: 'appendChild', value: 'FAQs' },
|
|
1404
|
+
{
|
|
1405
|
+
opportunityId: 'opp-faq',
|
|
1406
|
+
suggestionId: 'sugg-1',
|
|
1407
|
+
op: 'appendChild',
|
|
1408
|
+
value: 'FAQ1',
|
|
1437
1409
|
},
|
|
1438
|
-
|
|
1410
|
+
],
|
|
1439
1411
|
};
|
|
1440
1412
|
|
|
1441
|
-
|
|
1442
|
-
// Remove sugg-2 from page2 (should keep heading because sugg-3 remains)
|
|
1443
|
-
const result = mapper.rollbackPatches(config, ['sugg-1', 'sugg-2'], 'opp-faq');
|
|
1444
|
-
|
|
1445
|
-
// page1 completely removed
|
|
1446
|
-
expect(result.tokowakaOptimizations).to.not.have.property('/page1');
|
|
1447
|
-
|
|
1448
|
-
// page2 still has heading + sugg-3
|
|
1449
|
-
expect(result.tokowakaOptimizations['/page2'].patches).to.have.lengthOf(2);
|
|
1450
|
-
expect(result.tokowakaOptimizations['/page2'].patches[0]).to.not.have.property('suggestionId');
|
|
1451
|
-
expect(result.tokowakaOptimizations['/page2'].patches[1].suggestionId).to.equal('sugg-3');
|
|
1413
|
+
const result = mapper.rollbackPatches(config, ['sugg-1'], 'opp-faq');
|
|
1452
1414
|
|
|
1453
|
-
|
|
1415
|
+
// All patches removed (heading + FAQ item)
|
|
1416
|
+
expect(result.patches).to.have.lengthOf(0);
|
|
1417
|
+
expect(result.removedCount).to.equal(2);
|
|
1454
1418
|
});
|
|
1455
1419
|
|
|
1456
1420
|
it('should handle null/undefined config gracefully', () => {
|
|
@@ -39,7 +39,6 @@ describe('HTML Utils', () => {
|
|
|
39
39
|
try {
|
|
40
40
|
await fetchHtmlWithWarmup(
|
|
41
41
|
'',
|
|
42
|
-
'api-key',
|
|
43
42
|
'host',
|
|
44
43
|
'edge-url',
|
|
45
44
|
log,
|
|
@@ -51,51 +50,51 @@ describe('HTML Utils', () => {
|
|
|
51
50
|
}
|
|
52
51
|
});
|
|
53
52
|
|
|
54
|
-
it('should throw error when
|
|
53
|
+
it('should throw error when forwardedHost is missing', async () => {
|
|
55
54
|
try {
|
|
56
55
|
await fetchHtmlWithWarmup(
|
|
57
56
|
'https://example.com/page',
|
|
57
|
+
'api-key',
|
|
58
58
|
'',
|
|
59
|
-
'host',
|
|
60
59
|
'edge-url',
|
|
61
60
|
log,
|
|
62
61
|
false,
|
|
63
62
|
);
|
|
64
63
|
expect.fail('Should have thrown error');
|
|
65
64
|
} catch (error) {
|
|
66
|
-
expect(error.message).to.equal('
|
|
65
|
+
expect(error.message).to.equal('Forwarded host is required for fetching HTML');
|
|
67
66
|
}
|
|
68
67
|
});
|
|
69
68
|
|
|
70
|
-
it('should throw error when
|
|
69
|
+
it('should throw error when tokowakaEdgeUrl is missing', async () => {
|
|
71
70
|
try {
|
|
72
71
|
await fetchHtmlWithWarmup(
|
|
73
72
|
'https://example.com/page',
|
|
74
73
|
'api-key',
|
|
74
|
+
'host',
|
|
75
75
|
'',
|
|
76
|
-
'edge-url',
|
|
77
76
|
log,
|
|
78
77
|
false,
|
|
79
78
|
);
|
|
80
79
|
expect.fail('Should have thrown error');
|
|
81
80
|
} catch (error) {
|
|
82
|
-
expect(error.message).to.equal('
|
|
81
|
+
expect(error.message).to.equal('TOKOWAKA_EDGE_URL is not configured');
|
|
83
82
|
}
|
|
84
83
|
});
|
|
85
84
|
|
|
86
|
-
it('should throw error when
|
|
85
|
+
it('should throw error when apiKey is missing', async () => {
|
|
87
86
|
try {
|
|
88
87
|
await fetchHtmlWithWarmup(
|
|
89
88
|
'https://example.com/page',
|
|
90
|
-
'api-key',
|
|
91
|
-
'host',
|
|
92
89
|
'',
|
|
90
|
+
'host',
|
|
91
|
+
'edge-url',
|
|
93
92
|
log,
|
|
94
93
|
false,
|
|
95
94
|
);
|
|
96
95
|
expect.fail('Should have thrown error');
|
|
97
96
|
} catch (error) {
|
|
98
|
-
expect(error.message).to.equal('
|
|
97
|
+
expect(error.message).to.equal('Tokowaka API key is required for fetching HTML');
|
|
99
98
|
}
|
|
100
99
|
});
|
|
101
100
|
|
|
@@ -273,7 +272,6 @@ describe('HTML Utils', () => {
|
|
|
273
272
|
// This tests the defensive 'throw lastError' fallback
|
|
274
273
|
await fetchHtmlWithWarmup(
|
|
275
274
|
'https://example.com/page',
|
|
276
|
-
'api-key',
|
|
277
275
|
'host',
|
|
278
276
|
'https://edge.example.com',
|
|
279
277
|
log,
|