@adobe/spacecat-shared-tokowaka-client 1.2.4 → 1.3.1

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.
@@ -0,0 +1,616 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ /* eslint-env mocha */
14
+
15
+ import { expect } from 'chai';
16
+ import TocMapper from '../../src/mappers/toc-mapper.js';
17
+
18
+ describe('TocMapper', () => {
19
+ let mapper;
20
+ let log;
21
+
22
+ beforeEach(() => {
23
+ log = {
24
+ debug: () => {},
25
+ info: () => {},
26
+ warn: () => {},
27
+ error: () => {},
28
+ };
29
+ mapper = new TocMapper(log);
30
+ });
31
+
32
+ describe('getOpportunityType', () => {
33
+ it('should return toc', () => {
34
+ expect(mapper.getOpportunityType()).to.equal('toc');
35
+ });
36
+ });
37
+
38
+ describe('requiresPrerender', () => {
39
+ it('should return true', () => {
40
+ expect(mapper.requiresPrerender()).to.be.true;
41
+ });
42
+ });
43
+
44
+ describe('canDeploy', () => {
45
+ it('should return eligible for valid toc suggestion with insertAfter', () => {
46
+ const suggestion = {
47
+ getData: () => ({
48
+ checkType: 'toc',
49
+ transformRules: {
50
+ action: 'insertAfter',
51
+ selector: 'h1#main-heading',
52
+ valueFormat: 'hast',
53
+ value: {
54
+ type: 'root',
55
+ children: [
56
+ {
57
+ type: 'element',
58
+ tagName: 'nav',
59
+ properties: { className: ['toc'] },
60
+ children: [],
61
+ },
62
+ ],
63
+ },
64
+ },
65
+ }),
66
+ };
67
+
68
+ const result = mapper.canDeploy(suggestion);
69
+
70
+ expect(result).to.deep.equal({ eligible: true });
71
+ });
72
+
73
+ it('should return eligible for valid toc suggestion with insertBefore', () => {
74
+ const suggestion = {
75
+ getData: () => ({
76
+ checkType: 'toc',
77
+ transformRules: {
78
+ action: 'insertBefore',
79
+ selector: 'main',
80
+ valueFormat: 'hast',
81
+ value: {
82
+ type: 'root',
83
+ children: [
84
+ {
85
+ type: 'element',
86
+ tagName: 'nav',
87
+ properties: { className: ['toc'] },
88
+ children: [],
89
+ },
90
+ ],
91
+ },
92
+ },
93
+ }),
94
+ };
95
+
96
+ const result = mapper.canDeploy(suggestion);
97
+
98
+ expect(result).to.deep.equal({ eligible: true });
99
+ });
100
+
101
+ it('should return ineligible for non-toc checkType', () => {
102
+ const suggestion = {
103
+ getData: () => ({
104
+ checkType: 'heading-empty',
105
+ transformRules: {
106
+ action: 'insertAfter',
107
+ selector: 'h1',
108
+ valueFormat: 'hast',
109
+ value: {},
110
+ },
111
+ }),
112
+ };
113
+
114
+ const result = mapper.canDeploy(suggestion);
115
+
116
+ expect(result).to.deep.equal({
117
+ eligible: false,
118
+ reason: 'Only toc checkType can be deployed. This suggestion has checkType: heading-empty',
119
+ });
120
+ });
121
+
122
+ it('should return ineligible when checkType is missing', () => {
123
+ const suggestion = {
124
+ getData: () => ({
125
+ transformRules: {
126
+ action: 'insertAfter',
127
+ selector: 'h1',
128
+ valueFormat: 'hast',
129
+ value: {},
130
+ },
131
+ }),
132
+ };
133
+
134
+ const result = mapper.canDeploy(suggestion);
135
+
136
+ expect(result).to.deep.equal({
137
+ eligible: false,
138
+ reason: 'Only toc checkType can be deployed. This suggestion has checkType: undefined',
139
+ });
140
+ });
141
+
142
+ it('should return ineligible when data is null', () => {
143
+ const suggestion = {
144
+ getData: () => null,
145
+ };
146
+
147
+ const result = mapper.canDeploy(suggestion);
148
+
149
+ expect(result).to.deep.equal({
150
+ eligible: false,
151
+ reason: 'Only toc checkType can be deployed. This suggestion has checkType: undefined',
152
+ });
153
+ });
154
+
155
+ it('should return ineligible when transformRules.selector is missing', () => {
156
+ const suggestion = {
157
+ getData: () => ({
158
+ checkType: 'toc',
159
+ transformRules: {
160
+ action: 'insertAfter',
161
+ valueFormat: 'hast',
162
+ value: {},
163
+ },
164
+ }),
165
+ };
166
+
167
+ const result = mapper.canDeploy(suggestion);
168
+
169
+ expect(result).to.deep.equal({
170
+ eligible: false,
171
+ reason: 'transformRules.selector is required',
172
+ });
173
+ });
174
+
175
+ it('should return ineligible when transformRules.selector is empty string', () => {
176
+ const suggestion = {
177
+ getData: () => ({
178
+ checkType: 'toc',
179
+ transformRules: {
180
+ action: 'insertAfter',
181
+ selector: '',
182
+ valueFormat: 'hast',
183
+ value: {},
184
+ },
185
+ }),
186
+ };
187
+
188
+ const result = mapper.canDeploy(suggestion);
189
+
190
+ expect(result).to.deep.equal({
191
+ eligible: false,
192
+ reason: 'transformRules.selector is required',
193
+ });
194
+ });
195
+
196
+ it('should return ineligible when transformRules.value is missing', () => {
197
+ const suggestion = {
198
+ getData: () => ({
199
+ checkType: 'toc',
200
+ transformRules: {
201
+ action: 'insertAfter',
202
+ selector: 'h1',
203
+ valueFormat: 'hast',
204
+ },
205
+ }),
206
+ };
207
+
208
+ const result = mapper.canDeploy(suggestion);
209
+
210
+ expect(result).to.deep.equal({
211
+ eligible: false,
212
+ reason: 'transformRules.value is required',
213
+ });
214
+ });
215
+
216
+ it('should return ineligible when transformRules.valueFormat is not hast', () => {
217
+ const suggestion = {
218
+ getData: () => ({
219
+ checkType: 'toc',
220
+ transformRules: {
221
+ action: 'insertAfter',
222
+ selector: 'h1',
223
+ valueFormat: 'text',
224
+ value: 'some text',
225
+ },
226
+ }),
227
+ };
228
+
229
+ const result = mapper.canDeploy(suggestion);
230
+
231
+ expect(result).to.deep.equal({
232
+ eligible: false,
233
+ reason: 'transformRules.valueFormat must be hast for toc',
234
+ });
235
+ });
236
+
237
+ it('should return ineligible when transformRules.valueFormat is missing', () => {
238
+ const suggestion = {
239
+ getData: () => ({
240
+ checkType: 'toc',
241
+ transformRules: {
242
+ action: 'insertAfter',
243
+ selector: 'h1',
244
+ value: {},
245
+ },
246
+ }),
247
+ };
248
+
249
+ const result = mapper.canDeploy(suggestion);
250
+
251
+ expect(result).to.deep.equal({
252
+ eligible: false,
253
+ reason: 'transformRules.valueFormat must be hast for toc',
254
+ });
255
+ });
256
+
257
+ it('should return ineligible when action is invalid', () => {
258
+ const suggestion = {
259
+ getData: () => ({
260
+ checkType: 'toc',
261
+ transformRules: {
262
+ action: 'replace',
263
+ selector: 'h1',
264
+ valueFormat: 'hast',
265
+ value: {},
266
+ },
267
+ }),
268
+ };
269
+
270
+ const result = mapper.canDeploy(suggestion);
271
+
272
+ expect(result).to.deep.equal({
273
+ eligible: false,
274
+ reason: 'transformRules.action must be one of insertBefore, insertAfter for toc',
275
+ });
276
+ });
277
+
278
+ it('should return ineligible when action is missing', () => {
279
+ const suggestion = {
280
+ getData: () => ({
281
+ checkType: 'toc',
282
+ transformRules: {
283
+ selector: 'h1',
284
+ valueFormat: 'hast',
285
+ value: {},
286
+ },
287
+ }),
288
+ };
289
+
290
+ const result = mapper.canDeploy(suggestion);
291
+
292
+ expect(result).to.deep.equal({
293
+ eligible: false,
294
+ reason: 'transformRules.action must be one of insertBefore, insertAfter for toc',
295
+ });
296
+ });
297
+ });
298
+
299
+ describe('suggestionsToPatches', () => {
300
+ it('should create patch for valid toc suggestion with insertAfter', () => {
301
+ const tocValue = {
302
+ type: 'root',
303
+ children: [
304
+ {
305
+ type: 'element',
306
+ tagName: 'nav',
307
+ properties: { className: ['toc'] },
308
+ children: [
309
+ {
310
+ type: 'element',
311
+ tagName: 'ul',
312
+ children: [
313
+ {
314
+ type: 'element',
315
+ tagName: 'li',
316
+ children: [
317
+ {
318
+ type: 'element',
319
+ tagName: 'a',
320
+ properties: { href: '#section1' },
321
+ children: [{ type: 'text', value: 'Section 1' }],
322
+ },
323
+ ],
324
+ },
325
+ ],
326
+ },
327
+ ],
328
+ },
329
+ ],
330
+ };
331
+
332
+ const suggestion = {
333
+ getId: () => 'sugg-toc-1',
334
+ getUpdatedAt: () => '2025-12-09T10:00:00.000Z',
335
+ getData: () => ({
336
+ checkType: 'toc',
337
+ recommendedAction: 'Add a Table of Contents to the page',
338
+ transformRules: {
339
+ action: 'insertAfter',
340
+ selector: 'h1#main-heading',
341
+ valueFormat: 'hast',
342
+ value: tocValue,
343
+ scrapedAt: '2025-12-06T06:27:04.663Z',
344
+ },
345
+ }),
346
+ };
347
+
348
+ const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-toc-1');
349
+ expect(patches.length).to.equal(1);
350
+ const patch = patches[0];
351
+
352
+ expect(patch).to.deep.include({
353
+ op: 'insertAfter',
354
+ selector: 'h1#main-heading',
355
+ value: tocValue,
356
+ valueFormat: 'hast',
357
+ opportunityId: 'opp-toc-1',
358
+ suggestionId: 'sugg-toc-1',
359
+ prerenderRequired: true,
360
+ });
361
+ expect(patch.lastUpdated).to.be.a('number');
362
+ expect(patch.target).to.equal('ai-bots');
363
+ });
364
+
365
+ it('should create patch for valid toc suggestion with insertBefore', () => {
366
+ const tocValue = {
367
+ type: 'root',
368
+ children: [
369
+ {
370
+ type: 'element',
371
+ tagName: 'nav',
372
+ properties: { className: ['toc'] },
373
+ children: [],
374
+ },
375
+ ],
376
+ };
377
+
378
+ const suggestion = {
379
+ getId: () => 'sugg-toc-2',
380
+ getUpdatedAt: () => '2025-12-09T10:00:00.000Z',
381
+ getData: () => ({
382
+ checkType: 'toc',
383
+ transformRules: {
384
+ action: 'insertBefore',
385
+ selector: 'main',
386
+ valueFormat: 'hast',
387
+ value: tocValue,
388
+ },
389
+ }),
390
+ };
391
+
392
+ const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-toc-2');
393
+ expect(patches.length).to.equal(1);
394
+ const patch = patches[0];
395
+
396
+ expect(patch).to.deep.include({
397
+ op: 'insertBefore',
398
+ selector: 'main',
399
+ value: tocValue,
400
+ valueFormat: 'hast',
401
+ opportunityId: 'opp-toc-2',
402
+ suggestionId: 'sugg-toc-2',
403
+ prerenderRequired: true,
404
+ });
405
+ expect(patch.lastUpdated).to.be.a('number');
406
+ });
407
+
408
+ it('should create patch with complex nested toc structure', () => {
409
+ const complexTocValue = {
410
+ type: 'root',
411
+ children: [
412
+ {
413
+ type: 'element',
414
+ tagName: 'nav',
415
+ properties: { className: ['toc'] },
416
+ children: [
417
+ {
418
+ type: 'element',
419
+ tagName: 'ul',
420
+ children: [
421
+ {
422
+ type: 'element',
423
+ tagName: 'li',
424
+ children: [
425
+ {
426
+ type: 'element',
427
+ tagName: 'a',
428
+ properties: {
429
+ 'data-selector': 'h1#main-title',
430
+ href: '#',
431
+ },
432
+ children: [{ type: 'text', value: 'Main Title' }],
433
+ },
434
+ ],
435
+ },
436
+ {
437
+ type: 'element',
438
+ tagName: 'li',
439
+ properties: { className: ['toc-sub'] },
440
+ children: [
441
+ {
442
+ type: 'element',
443
+ tagName: 'a',
444
+ properties: {
445
+ 'data-selector': 'h2#section-1',
446
+ href: '#',
447
+ },
448
+ children: [{ type: 'text', value: 'Section 1' }],
449
+ },
450
+ ],
451
+ },
452
+ ],
453
+ },
454
+ ],
455
+ },
456
+ ],
457
+ };
458
+
459
+ const suggestion = {
460
+ getId: () => 'sugg-toc-3',
461
+ getUpdatedAt: () => '2025-12-09T10:00:00.000Z',
462
+ getData: () => ({
463
+ checkType: 'toc',
464
+ transformRules: {
465
+ action: 'insertAfter',
466
+ selector: 'h1#heartfelt-birthday-wishes-for-brothers',
467
+ valueFormat: 'hast',
468
+ value: complexTocValue,
469
+ },
470
+ }),
471
+ };
472
+
473
+ const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-toc-3');
474
+ expect(patches.length).to.equal(1);
475
+ const patch = patches[0];
476
+
477
+ expect(patch.value).to.deep.equal(complexTocValue);
478
+ expect(patch.op).to.equal('insertAfter');
479
+ });
480
+
481
+ it('should return empty array for invalid toc suggestion', () => {
482
+ const suggestion = {
483
+ getId: () => 'sugg-invalid',
484
+ getData: () => ({
485
+ checkType: 'toc',
486
+ transformRules: {
487
+ action: 'replace', // Invalid action
488
+ selector: 'h1',
489
+ valueFormat: 'hast',
490
+ value: {},
491
+ },
492
+ }),
493
+ };
494
+
495
+ const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-invalid');
496
+ expect(patches.length).to.equal(0);
497
+ });
498
+
499
+ it('should return empty array when transformRules is missing', () => {
500
+ const suggestion = {
501
+ getId: () => 'sugg-no-rules',
502
+ getData: () => ({
503
+ checkType: 'toc',
504
+ }),
505
+ };
506
+
507
+ const patches = mapper.suggestionsToPatches('/path', [suggestion], 'opp-no-rules');
508
+ expect(patches.length).to.equal(0);
509
+ });
510
+
511
+ it('should log warning for invalid suggestion', () => {
512
+ let warnMessage = '';
513
+ const warnLog = {
514
+ debug: () => {},
515
+ info: () => {},
516
+ warn: (msg) => { warnMessage = msg; },
517
+ error: () => {},
518
+ };
519
+ const warnMapper = new TocMapper(warnLog);
520
+
521
+ const suggestion = {
522
+ getId: () => 'sugg-warn',
523
+ getData: () => ({
524
+ checkType: 'toc',
525
+ transformRules: {
526
+ action: 'replace', // Invalid
527
+ selector: 'h1',
528
+ valueFormat: 'hast',
529
+ value: {},
530
+ },
531
+ }),
532
+ };
533
+
534
+ const patches = warnMapper.suggestionsToPatches('/path', [suggestion], 'opp-warn');
535
+
536
+ expect(patches.length).to.equal(0);
537
+ expect(warnMessage).to.include('cannot be deployed');
538
+ expect(warnMessage).to.include('sugg-warn');
539
+ });
540
+
541
+ it('should handle multiple suggestions', () => {
542
+ const tocValue1 = { type: 'root', children: [] };
543
+ const tocValue2 = { type: 'root', children: [] };
544
+
545
+ const suggestions = [
546
+ {
547
+ getId: () => 'sugg-multi-1',
548
+ getUpdatedAt: () => '2025-12-09T10:00:00.000Z',
549
+ getData: () => ({
550
+ checkType: 'toc',
551
+ transformRules: {
552
+ action: 'insertAfter',
553
+ selector: 'h1',
554
+ valueFormat: 'hast',
555
+ value: tocValue1,
556
+ },
557
+ }),
558
+ },
559
+ {
560
+ getId: () => 'sugg-multi-2',
561
+ getUpdatedAt: () => '2025-12-09T10:00:00.000Z',
562
+ getData: () => ({
563
+ checkType: 'toc',
564
+ transformRules: {
565
+ action: 'insertBefore',
566
+ selector: 'main',
567
+ valueFormat: 'hast',
568
+ value: tocValue2,
569
+ },
570
+ }),
571
+ },
572
+ ];
573
+
574
+ const patches = mapper.suggestionsToPatches('/path', suggestions, 'opp-multi');
575
+ expect(patches.length).to.equal(2);
576
+ expect(patches[0].suggestionId).to.equal('sugg-multi-1');
577
+ expect(patches[1].suggestionId).to.equal('sugg-multi-2');
578
+ });
579
+
580
+ it('should filter out invalid suggestions from multiple suggestions', () => {
581
+ const tocValue = { type: 'root', children: [] };
582
+
583
+ const suggestions = [
584
+ {
585
+ getId: () => 'sugg-valid',
586
+ getUpdatedAt: () => '2025-12-09T10:00:00.000Z',
587
+ getData: () => ({
588
+ checkType: 'toc',
589
+ transformRules: {
590
+ action: 'insertAfter',
591
+ selector: 'h1',
592
+ valueFormat: 'hast',
593
+ value: tocValue,
594
+ },
595
+ }),
596
+ },
597
+ {
598
+ getId: () => 'sugg-invalid',
599
+ getData: () => ({
600
+ checkType: 'toc',
601
+ transformRules: {
602
+ action: 'replace', // Invalid
603
+ selector: 'h1',
604
+ valueFormat: 'hast',
605
+ value: tocValue,
606
+ },
607
+ }),
608
+ },
609
+ ];
610
+
611
+ const patches = mapper.suggestionsToPatches('/path', suggestions, 'opp-mixed');
612
+ expect(patches.length).to.equal(1);
613
+ expect(patches[0].suggestionId).to.equal('sugg-valid');
614
+ });
615
+ });
616
+ });