@adobe/spacecat-shared-tokowaka-client 1.0.2 → 1.0.3

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,1324 @@
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 FaqMapper from '../../src/mappers/faq-mapper.js';
17
+
18
+ describe('FaqMapper', () => {
19
+ let mapper;
20
+ let log;
21
+
22
+ beforeEach(() => {
23
+ log = {
24
+ debug: () => {},
25
+ info: () => {},
26
+ warn: () => {},
27
+ error: () => {},
28
+ };
29
+ mapper = new FaqMapper(log);
30
+ });
31
+
32
+ describe('getOpportunityType', () => {
33
+ it('should return faq', () => {
34
+ expect(mapper.getOpportunityType()).to.equal('faq');
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 FAQ suggestion', () => {
46
+ const suggestion = {
47
+ getData: () => ({
48
+ item: {
49
+ question: 'Is this valid?',
50
+ answer: 'Yes, it is.',
51
+ },
52
+ url: 'https://www.example.com/page',
53
+ shouldOptimize: true,
54
+ transformRules: {
55
+ action: 'appendChild',
56
+ selector: 'main',
57
+ },
58
+ }),
59
+ };
60
+
61
+ const result = mapper.canDeploy(suggestion);
62
+
63
+ expect(result).to.deep.equal({ eligible: true });
64
+ });
65
+
66
+ it('should return ineligible when item.question/answer is missing', () => {
67
+ const suggestion = {
68
+ getData: () => ({
69
+ shouldOptimize: true,
70
+ transformRules: {
71
+ action: 'appendChild',
72
+ selector: 'main',
73
+ },
74
+ }),
75
+ };
76
+
77
+ const result = mapper.canDeploy(suggestion);
78
+
79
+ expect(result).to.deep.equal({
80
+ eligible: false,
81
+ reason: 'item.question and item.answer are required',
82
+ });
83
+ });
84
+
85
+ it('should return ineligible when transformRules are missing', () => {
86
+ const suggestion = {
87
+ getData: () => ({
88
+ shouldOptimize: true,
89
+ item: {
90
+ question: 'Is this valid?',
91
+ answer: 'Yes, it is.',
92
+ },
93
+ }),
94
+ };
95
+
96
+ const result = mapper.canDeploy(suggestion);
97
+
98
+ expect(result).to.deep.equal({
99
+ eligible: false,
100
+ reason: 'transformRules is required',
101
+ });
102
+ });
103
+
104
+ it('should return ineligible when transformRules action is invalid', () => {
105
+ const suggestion = {
106
+ getData: () => ({
107
+ shouldOptimize: true,
108
+ item: {
109
+ question: 'Question?',
110
+ answer: 'Answer.',
111
+ },
112
+ transformRules: {
113
+ action: 'replace',
114
+ selector: 'main',
115
+ },
116
+ }),
117
+ };
118
+
119
+ const result = mapper.canDeploy(suggestion);
120
+
121
+ expect(result).to.deep.equal({
122
+ eligible: false,
123
+ reason: 'transformRules.action must be insertAfter, insertBefore, or appendChild',
124
+ });
125
+ });
126
+
127
+ it('should return ineligible when transformRules selector is missing', () => {
128
+ const suggestion = {
129
+ getData: () => ({
130
+ shouldOptimize: true,
131
+ item: {
132
+ question: 'Question?',
133
+ answer: 'Answer.',
134
+ },
135
+ transformRules: {
136
+ action: 'appendChild',
137
+ },
138
+ }),
139
+ };
140
+
141
+ const result = mapper.canDeploy(suggestion);
142
+
143
+ expect(result).to.deep.equal({
144
+ eligible: false,
145
+ reason: 'transformRules.selector is required',
146
+ });
147
+ });
148
+
149
+ it('should return ineligible when data is null', () => {
150
+ const suggestion = {
151
+ getData: () => null,
152
+ };
153
+
154
+ const result = mapper.canDeploy(suggestion);
155
+
156
+ expect(result).to.deep.equal({
157
+ eligible: false,
158
+ reason: 'shouldOptimize flag is not true',
159
+ });
160
+ });
161
+
162
+ it('should accept insertAfter as valid action', () => {
163
+ const suggestion = {
164
+ getData: () => ({
165
+ item: {
166
+ question: 'Question?',
167
+ answer: 'Answer.',
168
+ },
169
+ url: 'https://www.example.com/page',
170
+ shouldOptimize: true,
171
+ transformRules: {
172
+ action: 'insertAfter',
173
+ selector: 'main',
174
+ },
175
+ }),
176
+ };
177
+
178
+ const result = mapper.canDeploy(suggestion);
179
+
180
+ expect(result).to.deep.equal({ eligible: true });
181
+ });
182
+
183
+ it('should accept insertBefore as valid action', () => {
184
+ const suggestion = {
185
+ getData: () => ({
186
+ item: {
187
+ question: 'Question?',
188
+ answer: 'Answer.',
189
+ },
190
+ url: 'https://www.example.com/page',
191
+ shouldOptimize: true,
192
+ transformRules: {
193
+ action: 'insertBefore',
194
+ selector: 'main',
195
+ },
196
+ }),
197
+ };
198
+
199
+ const result = mapper.canDeploy(suggestion);
200
+
201
+ expect(result).to.deep.equal({ eligible: true });
202
+ });
203
+
204
+ it('should return ineligible when URL is invalid', () => {
205
+ const suggestion = {
206
+ getData: () => ({
207
+ item: {
208
+ question: 'Question?',
209
+ answer: 'Answer.',
210
+ },
211
+ url: 'not-a-valid-url',
212
+ shouldOptimize: true,
213
+ transformRules: {
214
+ action: 'appendChild',
215
+ selector: 'main',
216
+ },
217
+ }),
218
+ };
219
+
220
+ const result = mapper.canDeploy(suggestion);
221
+ expect(result.eligible).to.be.false;
222
+ expect(result.reason).to.include('not a valid URL');
223
+ });
224
+
225
+ it('should return ineligible when shouldOptimize is false', () => {
226
+ const suggestion = {
227
+ getData: () => ({
228
+ item: {
229
+ question: 'Question?',
230
+ answer: 'Answer.',
231
+ },
232
+ url: 'https://www.example.com/page',
233
+ shouldOptimize: false,
234
+ transformRules: {
235
+ action: 'appendChild',
236
+ selector: 'main',
237
+ },
238
+ }),
239
+ };
240
+
241
+ const result = mapper.canDeploy(suggestion);
242
+ expect(result).to.deep.equal({
243
+ eligible: false,
244
+ reason: 'shouldOptimize flag is not true',
245
+ });
246
+ });
247
+
248
+ it('should return eligible when shouldOptimize is true', () => {
249
+ const suggestion = {
250
+ getData: () => ({
251
+ item: {
252
+ question: 'Question?',
253
+ answer: 'Answer.',
254
+ },
255
+ url: 'https://www.example.com/page',
256
+ shouldOptimize: true,
257
+ transformRules: {
258
+ action: 'appendChild',
259
+ selector: 'main',
260
+ },
261
+ }),
262
+ };
263
+
264
+ const result = mapper.canDeploy(suggestion);
265
+ expect(result).to.deep.equal({ eligible: true });
266
+ });
267
+ });
268
+
269
+ describe('suggestionsToPatches', () => {
270
+ it('should create patch with HAST value from markdown', () => {
271
+ const suggestion = {
272
+ getId: () => 'sugg-faq-123',
273
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
274
+ getData: () => ({
275
+ item: {
276
+ question: 'Is Bulk better than myprotein?',
277
+ answer: 'Yes, because of **better value**.',
278
+ },
279
+ url: 'https://www.example.com/page',
280
+ headingText: 'FAQs',
281
+ shouldOptimize: true,
282
+ transformRules: {
283
+ action: 'appendChild',
284
+ selector: 'main',
285
+ },
286
+ }),
287
+ };
288
+
289
+ const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-faq-123', null);
290
+
291
+ // Should create 2 patches: heading + FAQ
292
+ expect(patches).to.be.an('array');
293
+ expect(patches.length).to.equal(2);
294
+
295
+ // First patch: heading (no suggestionId)
296
+ const headingPatch = patches[0];
297
+ expect(headingPatch.opportunityId).to.equal('opp-faq-123');
298
+ expect(headingPatch.suggestionId).to.be.undefined;
299
+ expect(headingPatch.op).to.equal('appendChild');
300
+ expect(headingPatch.selector).to.equal('main');
301
+ expect(headingPatch.value.tagName).to.equal('h2');
302
+ expect(headingPatch.value.children[0].value).to.equal('FAQs');
303
+
304
+ // Second patch: FAQ item
305
+ const faqPatch = patches[1];
306
+ expect(faqPatch.opportunityId).to.equal('opp-faq-123');
307
+ expect(faqPatch.suggestionId).to.equal('sugg-faq-123');
308
+ expect(faqPatch.op).to.equal('appendChild');
309
+ expect(faqPatch.selector).to.equal('main');
310
+ expect(faqPatch.valueFormat).to.equal('hast');
311
+ expect(faqPatch.prerenderRequired).to.be.true;
312
+ expect(faqPatch.lastUpdated).to.be.a('number');
313
+
314
+ // Verify FAQ HAST structure: <div><h3>question</h3>answer</div>
315
+ expect(faqPatch.value).to.be.an('object');
316
+ expect(faqPatch.value.type).to.equal('element');
317
+ expect(faqPatch.value.tagName).to.equal('div');
318
+ expect(faqPatch.value.children).to.be.an('array');
319
+ expect(faqPatch.value.children[0].tagName).to.equal('h3');
320
+ });
321
+
322
+ it('should convert FAQ markdown with headings and lists to HAST', () => {
323
+ const suggestion1 = {
324
+ getId: () => 'sugg-faq-1',
325
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
326
+ getData: () => ({
327
+ item: {
328
+ question: 'Is Bulk better than myprotein?',
329
+ answer: `Bulk offers several advantages:
330
+
331
+ 1. **Better Value for Money**: High-quality products at competitive prices.
332
+ 2. **Wider Selection**: Products for diverse fitness goals.
333
+ 3. **Unique Product Ranges**: Simplified product choices.`,
334
+ },
335
+ url: 'https://www.example.com/page',
336
+ headingText: 'FAQs',
337
+ shouldOptimize: true,
338
+ transformRules: {
339
+ action: 'appendChild',
340
+ selector: 'main',
341
+ },
342
+ }),
343
+ };
344
+
345
+ const patches = mapper.suggestionsToPatches('/page', [suggestion1], 'opp-faq-complex', null);
346
+
347
+ expect(patches.length).to.equal(2); // heading + FAQ
348
+
349
+ // Check heading patch
350
+ expect(patches[0].value.tagName).to.equal('h2');
351
+
352
+ // Check FAQ patch
353
+ const faqPatch = patches[1];
354
+ expect(faqPatch).to.exist;
355
+ expect(faqPatch.value.type).to.equal('element');
356
+ expect(faqPatch.value.tagName).to.equal('div');
357
+ expect(faqPatch.value.children).to.be.an('array');
358
+
359
+ // Verify structure: div > [h3, ...answer content]
360
+ const h3 = faqPatch.value.children[0];
361
+ expect(h3.tagName).to.equal('h3');
362
+ expect(h3.children[0].value).to.equal('Is Bulk better than myprotein?');
363
+
364
+ // The rest should be answer content (paragraph, list, etc.)
365
+ expect(faqPatch.value.children.length).to.be.greaterThan(1);
366
+ });
367
+
368
+ it('should handle markdown with bold text', () => {
369
+ const suggestion = {
370
+ getId: () => 'sugg-faq-bold',
371
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
372
+ getData: () => ({
373
+ item: {
374
+ question: 'Question?',
375
+ answer: 'This is **bold** text.',
376
+ },
377
+ url: 'https://www.example.com/page',
378
+ headingText: 'FAQs',
379
+ shouldOptimize: true,
380
+ transformRules: {
381
+ action: 'appendChild',
382
+ selector: 'main',
383
+ },
384
+ }),
385
+ };
386
+
387
+ const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-faq-bold', null);
388
+
389
+ expect(patches.length).to.equal(2); // heading + FAQ
390
+
391
+ // Check FAQ patch (second one)
392
+ const faqPatch = patches[1];
393
+ expect(faqPatch).to.exist;
394
+ expect(faqPatch.value.type).to.equal('element');
395
+ expect(faqPatch.value.tagName).to.equal('div');
396
+
397
+ // Structure: div > [h3, ...answer]
398
+ expect(faqPatch.value.children[0].tagName).to.equal('h3');
399
+
400
+ // Find the paragraph in the answer
401
+ const paragraph = faqPatch.value.children.find((child) => child.type === 'element' && child.tagName === 'p');
402
+ expect(paragraph).to.exist;
403
+ expect(paragraph.children).to.be.an('array');
404
+
405
+ // Should contain strong elements
406
+ const hasStrong = paragraph.children.some((child) => child.type === 'element' && child.tagName === 'strong');
407
+ expect(hasStrong).to.be.true;
408
+ });
409
+
410
+ it('should return null when item.question/answer is missing', () => {
411
+ const suggestion = {
412
+ getId: () => 'sugg-invalid',
413
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
414
+ getData: () => ({
415
+ transformRules: {
416
+ action: 'appendChild',
417
+ selector: 'main',
418
+ },
419
+ }),
420
+ };
421
+
422
+ const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-invalid', null);
423
+
424
+ expect(patches).to.be.an('array');
425
+ expect(patches.length).to.equal(0);
426
+ });
427
+
428
+ it('should return null when transformRules are incomplete', () => {
429
+ const suggestion = {
430
+ getId: () => 'sugg-invalid-2',
431
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
432
+ getData: () => ({
433
+ item: {
434
+ question: 'Question?',
435
+ answer: 'Answer.',
436
+ },
437
+ transformRules: {
438
+ selector: 'main',
439
+ },
440
+ }),
441
+ };
442
+
443
+ const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-invalid-2', null);
444
+
445
+ expect(patches).to.be.an('array');
446
+ expect(patches.length).to.equal(0);
447
+ });
448
+
449
+ it('should handle markdown parsing errors gracefully', () => {
450
+ let errorCount = 0;
451
+ const errorLog = {
452
+ debug: () => {},
453
+ info: () => {},
454
+ warn: () => {},
455
+ error: () => { errorCount += 1; },
456
+ };
457
+
458
+ const errorMapper = new FaqMapper(errorLog);
459
+
460
+ // Override buildFaqItemHast to throw an error
461
+ const originalBuildFaqItemHast = errorMapper.buildFaqItemHast;
462
+ errorMapper.buildFaqItemHast = () => {
463
+ throw new Error('Markdown parsing failed');
464
+ };
465
+
466
+ const suggestion = {
467
+ getId: () => 'sugg-error',
468
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
469
+ getData: () => ({
470
+ item: {
471
+ question: 'Question?',
472
+ answer: 'Answer.',
473
+ },
474
+ url: 'https://www.example.com/page',
475
+ headingText: 'FAQs',
476
+ shouldOptimize: true,
477
+ transformRules: {
478
+ action: 'appendChild',
479
+ selector: 'main',
480
+ },
481
+ }),
482
+ };
483
+
484
+ const patches = errorMapper.suggestionsToPatches('/page', [suggestion], 'opp-error', null);
485
+
486
+ // Should only create heading patch, FAQ item should fail gracefully
487
+ expect(patches).to.be.an('array');
488
+ expect(patches.length).to.equal(1); // Only heading
489
+ expect(patches[0].value.tagName).to.equal('h2'); // Heading patch
490
+ expect(errorCount).to.be.greaterThan(0);
491
+
492
+ // Restore original method
493
+ errorMapper.buildFaqItemHast = originalBuildFaqItemHast;
494
+ });
495
+
496
+ it('should handle real-world FAQ example from user', () => {
497
+ const question1 = 'Is Bulk better than myprotein?';
498
+ const answer1 = `Bulk offers several advantages over MyProtein in sports nutrition:
499
+
500
+ 1. **Better Value for Money**: Bulk provides high-quality products at competitive prices, highlighting products like their Pure Whey Protein™, Europe's best value whey protein.
501
+ 2. **Wider Selection**: Bulk's range of products caters to diverse fitness goals, including weight loss, muscle building, and performance improvement, making it broader than MyProtein.
502
+ 3. **Unique Product Ranges**: Bulk simplifies product choices with four distinct ranges—Pure Series™, Complete Series™, Pro Series™, and Active Foods™.
503
+ 4. **Customer Satisfaction**: Bulk emphasizes strong customer service and boasts a higher Trustpilot rating compared to MyProtein, indicating better customer trust.
504
+ 5. **Superior Product Formulation**: Popular products, such as Elevate™ pre-workout and Complete Greens™, are noted for their quality and pricing compared to MyProtein's offerings.
505
+
506
+ Overall, Bulk positions itself as a better choice for sports nutrition through its focus on value, variety, innovation, and customer satisfaction.`;
507
+
508
+ const suggestion = {
509
+ getId: () => 'sugg-faq-real',
510
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
511
+ getData: () => ({
512
+ item: {
513
+ question: question1,
514
+ answer: answer1,
515
+ },
516
+ headingText: 'FAQs',
517
+ shouldOptimize: true,
518
+ transformRules: {
519
+ action: 'appendChild',
520
+ selector: 'main',
521
+ },
522
+ url: 'https://www.bulk.com/uk',
523
+ }),
524
+ };
525
+
526
+ const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-faq-real', null);
527
+
528
+ expect(patches.length).to.equal(2); // heading + FAQ
529
+
530
+ // Check heading patch
531
+ expect(patches[0].value.tagName).to.equal('h2');
532
+
533
+ // Check FAQ patch
534
+ const faqPatch = patches[1];
535
+ expect(faqPatch).to.exist;
536
+ expect(faqPatch.op).to.equal('appendChild');
537
+ expect(faqPatch.selector).to.equal('main');
538
+ expect(faqPatch.valueFormat).to.equal('hast');
539
+ expect(faqPatch.value.type).to.equal('element');
540
+ expect(faqPatch.value.tagName).to.equal('div');
541
+ expect(faqPatch.value.children).to.be.an('array');
542
+
543
+ // Verify structure: div > [h3, ...answer content]
544
+ expect(faqPatch.value.children[0].tagName).to.equal('h3');
545
+ });
546
+
547
+ it('should create individual patches for multiple FAQ suggestions', () => {
548
+ const suggestions = [
549
+ {
550
+ getId: () => 'sugg-faq-1',
551
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
552
+ getData: () => ({
553
+ url: 'https://www.example.com/page',
554
+ headingText: 'FAQs',
555
+ shouldOptimize: true,
556
+ item: {
557
+ question: 'What is your return policy?',
558
+ answer: 'You can return items within 30 days.',
559
+ },
560
+ transformRules: {
561
+ action: 'appendChild',
562
+ selector: 'main',
563
+ },
564
+ }),
565
+ },
566
+ {
567
+ getId: () => 'sugg-faq-2',
568
+ getUpdatedAt: () => '2025-01-15T11:00:00.000Z',
569
+ getData: () => ({
570
+ url: 'https://www.example.com/page',
571
+ headingText: 'FAQs',
572
+ shouldOptimize: true,
573
+ item: {
574
+ question: 'Do you ship internationally?',
575
+ answer: 'Yes, we ship to over 100 countries.',
576
+ },
577
+ transformRules: {
578
+ action: 'appendChild',
579
+ selector: 'main',
580
+ },
581
+ }),
582
+ },
583
+ ];
584
+
585
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-faq-123', null);
586
+
587
+ expect(patches).to.be.an('array');
588
+ expect(patches.length).to.equal(3); // heading + 2 FAQs
589
+
590
+ // First patch: heading (no suggestionId)
591
+ const headingPatch = patches[0];
592
+ expect(headingPatch.opportunityId).to.equal('opp-faq-123');
593
+ expect(headingPatch.suggestionId).to.be.undefined;
594
+ expect(headingPatch.op).to.equal('appendChild');
595
+ expect(headingPatch.selector).to.equal('main');
596
+ expect(headingPatch.value.tagName).to.equal('h2');
597
+
598
+ // Second patch: first FAQ
599
+ const firstFaqPatch = patches[1];
600
+ expect(firstFaqPatch.opportunityId).to.equal('opp-faq-123');
601
+ expect(firstFaqPatch.suggestionId).to.equal('sugg-faq-1');
602
+ expect(firstFaqPatch.op).to.equal('appendChild');
603
+ expect(firstFaqPatch.selector).to.equal('main');
604
+ expect(firstFaqPatch.value.tagName).to.equal('div');
605
+
606
+ // Third patch: second FAQ
607
+ const secondFaqPatch = patches[2];
608
+ expect(secondFaqPatch.opportunityId).to.equal('opp-faq-123');
609
+ expect(secondFaqPatch.suggestionId).to.equal('sugg-faq-2');
610
+ expect(secondFaqPatch.op).to.equal('appendChild');
611
+ expect(secondFaqPatch.selector).to.equal('main');
612
+ expect(secondFaqPatch.value.tagName).to.equal('div');
613
+
614
+ // Verify HAST contains both questions
615
+ const hastString1 = JSON.stringify(firstFaqPatch.value);
616
+ const hastString2 = JSON.stringify(secondFaqPatch.value);
617
+ expect(hastString1).to.include('What is your return policy?');
618
+ expect(hastString2).to.include('Do you ship internationally?');
619
+ });
620
+
621
+ it('should handle single FAQ suggestion', () => {
622
+ const suggestions = [
623
+ {
624
+ getId: () => 'sugg-faq-1',
625
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
626
+ getData: () => ({
627
+ url: 'https://www.example.com/page',
628
+ headingText: 'FAQs',
629
+ shouldOptimize: true,
630
+ item: {
631
+ question: 'What is your return policy?',
632
+ answer: 'You can return items within 30 days.',
633
+ },
634
+ transformRules: {
635
+ action: 'appendChild',
636
+ selector: 'main',
637
+ },
638
+ }),
639
+ },
640
+ ];
641
+
642
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-faq-123', null);
643
+
644
+ expect(patches).to.be.an('array');
645
+ expect(patches.length).to.equal(2); // heading + 1 FAQ
646
+ expect(patches[0].suggestionId).to.be.undefined; // heading
647
+ expect(patches[1].suggestionId).to.equal('sugg-faq-1'); // FAQ
648
+ });
649
+
650
+ it('should filter out ineligible suggestions', () => {
651
+ const suggestions = [
652
+ {
653
+ getId: () => 'sugg-faq-1',
654
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
655
+ getData: () => ({
656
+ url: 'https://www.example.com/page',
657
+ headingText: 'FAQs',
658
+ shouldOptimize: true,
659
+ item: {
660
+ question: 'Valid question?',
661
+ answer: 'Valid answer.',
662
+ },
663
+ transformRules: {
664
+ action: 'appendChild',
665
+ selector: 'main',
666
+ },
667
+ }),
668
+ },
669
+ {
670
+ getId: () => 'sugg-faq-2',
671
+ getUpdatedAt: () => '2025-01-15T11:00:00.000Z',
672
+ getData: () => ({
673
+ url: 'https://www.example.com/page',
674
+ shouldOptimize: true,
675
+ // Missing item - should be filtered out
676
+ transformRules: {
677
+ action: 'appendChild',
678
+ selector: 'main',
679
+ },
680
+ }),
681
+ },
682
+ ];
683
+
684
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-faq-123', null);
685
+
686
+ expect(patches.length).to.equal(2); // heading + 1 valid FAQ
687
+ expect(patches[0].suggestionId).to.be.undefined; // heading
688
+ expect(patches[1].suggestionId).to.equal('sugg-faq-1'); // FAQ
689
+ });
690
+
691
+ it('should return empty array when all suggestions are ineligible', () => {
692
+ const suggestions = [
693
+ {
694
+ getId: () => 'sugg-faq-1',
695
+ getData: () => ({
696
+ // Missing transformRules
697
+ item: {
698
+ question: 'Question?',
699
+ answer: 'Answer.',
700
+ },
701
+ }),
702
+ },
703
+ ];
704
+
705
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-faq-123', null);
706
+
707
+ expect(patches).to.be.an('array');
708
+ expect(patches.length).to.equal(0);
709
+ });
710
+
711
+ it('should return empty array for empty suggestions', () => {
712
+ const patches = mapper.suggestionsToPatches('/page', [], 'opp-faq-123', null);
713
+ expect(patches).to.be.an('array');
714
+ expect(patches.length).to.equal(0);
715
+ });
716
+
717
+ it('should handle suggestions with invalid URLs in allOpportunitySuggestions', () => {
718
+ const suggestions = [
719
+ {
720
+ getId: () => 'sugg-faq-new',
721
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
722
+ getData: () => ({
723
+ url: 'https://www.example.com/page',
724
+ headingText: 'FAQs',
725
+ shouldOptimize: true,
726
+ item: {
727
+ question: 'Q?',
728
+ answer: 'A.',
729
+ },
730
+ transformRules: {
731
+ action: 'appendChild',
732
+ selector: 'main',
733
+ },
734
+ }),
735
+ },
736
+ ];
737
+
738
+ const allOpportunitySuggestions = [
739
+ {
740
+ getId: () => 'sugg-deployed-1',
741
+ getData: () => ({
742
+ url: 'invalid-url', // Invalid URL should be filtered out
743
+ item: {
744
+ question: 'Old Q?',
745
+ answer: 'Old A.',
746
+ },
747
+ tokowakaDeployed: 1704884400000,
748
+ transformRules: {
749
+ action: 'appendChild',
750
+ selector: 'main',
751
+ },
752
+ }),
753
+ },
754
+ ];
755
+
756
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-faq-123', allOpportunitySuggestions);
757
+
758
+ expect(patches).to.be.an('array');
759
+ expect(patches.length).to.equal(2); // heading + FAQ
760
+ expect(patches[0].suggestionId).to.be.undefined; // heading
761
+ expect(patches[1].suggestionId).to.equal('sugg-faq-new'); // FAQ
762
+ });
763
+
764
+ it('should use correct updatedAt timestamp for each patch', () => {
765
+ const suggestions = [
766
+ {
767
+ getId: () => 'sugg-faq-1',
768
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
769
+ getData: () => ({
770
+ url: 'https://www.example.com/page',
771
+ headingText: 'FAQs',
772
+ shouldOptimize: true,
773
+ item: {
774
+ question: 'Q1?',
775
+ answer: 'A1',
776
+ },
777
+ transformRules: {
778
+ action: 'appendChild',
779
+ selector: 'main',
780
+ },
781
+ }),
782
+ },
783
+ {
784
+ getId: () => 'sugg-faq-2',
785
+ getUpdatedAt: () => '2025-01-15T12:00:00.000Z',
786
+ getData: () => ({
787
+ url: 'https://www.example.com/page',
788
+ headingText: 'FAQs',
789
+ shouldOptimize: true,
790
+ item: {
791
+ question: 'Q2?',
792
+ answer: 'A2',
793
+ },
794
+ transformRules: {
795
+ action: 'appendChild',
796
+ selector: 'main',
797
+ },
798
+ }),
799
+ },
800
+ ];
801
+
802
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-faq-123', null);
803
+
804
+ const expectedTimestamp1 = new Date('2025-01-15T10:00:00.000Z').getTime();
805
+ const expectedTimestamp2 = new Date('2025-01-15T12:00:00.000Z').getTime();
806
+
807
+ expect(patches.length).to.equal(3); // heading + 2 FAQs
808
+ // Heading uses the most recent timestamp from suggestions (12:00:00)
809
+ expect(patches[0].lastUpdated).to.equal(expectedTimestamp2);
810
+ // FAQ patches use their respective suggestion timestamps
811
+ expect(patches[1].lastUpdated).to.equal(expectedTimestamp1);
812
+ expect(patches[2].lastUpdated).to.equal(expectedTimestamp2);
813
+ });
814
+
815
+ it('should use Date.now() when getUpdatedAt returns null', () => {
816
+ const suggestions = [
817
+ {
818
+ getId: () => 'sugg-faq-1',
819
+ getUpdatedAt: () => null, // No updatedAt
820
+ getData: () => ({
821
+ url: 'https://www.example.com/page',
822
+ headingText: 'FAQs',
823
+ shouldOptimize: true,
824
+ item: {
825
+ question: 'Q1?',
826
+ answer: 'A1',
827
+ },
828
+ transformRules: {
829
+ action: 'appendChild',
830
+ selector: 'main',
831
+ },
832
+ }),
833
+ },
834
+ ];
835
+
836
+ const beforeTime = Date.now();
837
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-faq-123', null);
838
+ const afterTime = Date.now();
839
+
840
+ expect(patches.length).to.equal(2); // heading + FAQ
841
+ // Both heading and FAQ should use Date.now()
842
+ expect(patches[0].lastUpdated).to.be.at.least(beforeTime);
843
+ expect(patches[0].lastUpdated).to.be.at.most(afterTime);
844
+ expect(patches[1].lastUpdated).to.be.at.least(beforeTime);
845
+ expect(patches[1].lastUpdated).to.be.at.most(afterTime);
846
+ });
847
+
848
+ it('should handle invalid date strings by using Date.now()', () => {
849
+ const suggestions = [
850
+ {
851
+ getId: () => 'sugg-faq-invalid',
852
+ getUpdatedAt: () => 'invalid-date-string', // Invalid date
853
+ getData: () => ({
854
+ url: 'https://www.example.com/page',
855
+ headingText: 'FAQs',
856
+ shouldOptimize: true,
857
+ item: {
858
+ question: 'Q1?',
859
+ answer: 'A1',
860
+ },
861
+ transformRules: {
862
+ action: 'appendChild',
863
+ selector: 'main',
864
+ },
865
+ }),
866
+ },
867
+ ];
868
+
869
+ const beforeTime = Date.now();
870
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-faq-invalid', null);
871
+ const afterTime = Date.now();
872
+
873
+ expect(patches.length).to.equal(2); // heading + FAQ
874
+ // Both heading and FAQ should fallback to Date.now() for invalid dates
875
+ expect(patches[0].lastUpdated).to.be.at.least(beforeTime);
876
+ expect(patches[0].lastUpdated).to.be.at.most(afterTime);
877
+ expect(patches[1].lastUpdated).to.be.at.least(beforeTime);
878
+ expect(patches[1].lastUpdated).to.be.at.most(afterTime);
879
+ });
880
+
881
+ it('should handle real-world FAQ structure from user example', () => {
882
+ const suggestion = {
883
+ getId: () => '5ea1c4b1-dd5a-42e5-ad97-35cf8cc03cb9',
884
+ getUpdatedAt: () => '2025-11-05T17:02:37.741Z',
885
+ getData: () => ({
886
+ topic: 'modifier pdf',
887
+ transformRules: {
888
+ action: 'appendChild',
889
+ selector: 'main',
890
+ },
891
+ item: {
892
+ answerSuitabilityReason: 'The answer provides clear instructions...',
893
+ questionRelevanceReason: 'The question is directly related...',
894
+ question: 'Comment modifier un PDF déjà existant ?',
895
+ answer: 'Pour modifier un PDF existant avec Adobe Acrobat, vous pouvez utiliser soit l\'éditeur en ligne...',
896
+ sources: [
897
+ 'https://www.adobe.com/in/acrobat/features/modify-pdfs.html',
898
+ ],
899
+ },
900
+ headingText: 'FAQs',
901
+ shouldOptimize: true,
902
+ url: 'https://www.adobe.com/fr/acrobat/online/pdf-editor.html',
903
+ }),
904
+ };
905
+
906
+ const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-faq-123', null);
907
+
908
+ expect(patches).to.be.an('array');
909
+ expect(patches.length).to.equal(2); // heading + FAQ
910
+
911
+ // Check heading
912
+ expect(patches[0].value.tagName).to.equal('h2');
913
+
914
+ // Check FAQ
915
+ const faqPatch = patches[1];
916
+ expect(faqPatch.op).to.equal('appendChild');
917
+ expect(faqPatch.selector).to.equal('main');
918
+ expect(faqPatch.valueFormat).to.equal('hast');
919
+
920
+ const hastString = JSON.stringify(faqPatch.value);
921
+ expect(hastString).to.include('Comment modifier un PDF');
922
+ });
923
+
924
+ it('should handle existing config parameter', () => {
925
+ const suggestions = [
926
+ {
927
+ getId: () => 'sugg-faq-new',
928
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
929
+ getData: () => ({
930
+ url: 'https://www.example.com/page',
931
+ headingText: 'FAQs',
932
+ shouldOptimize: true,
933
+ item: {
934
+ question: 'New question?',
935
+ answer: 'New answer.',
936
+ },
937
+ transformRules: {
938
+ action: 'appendChild',
939
+ selector: 'main',
940
+ },
941
+ }),
942
+ },
943
+ ];
944
+
945
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-faq-123', null);
946
+
947
+ expect(patches).to.be.an('array');
948
+ expect(patches.length).to.equal(2); // heading + FAQ
949
+ });
950
+
951
+ it('should handle existing config with no existing patches for URL', () => {
952
+ const suggestions = [
953
+ {
954
+ getId: () => 'sugg-faq-new',
955
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
956
+ getData: () => ({
957
+ url: 'https://www.example.com/page',
958
+ headingText: 'FAQs',
959
+ shouldOptimize: true,
960
+ item: {
961
+ question: 'New question?',
962
+ answer: 'New answer.',
963
+ },
964
+ transformRules: {
965
+ action: 'appendChild',
966
+ selector: 'main',
967
+ },
968
+ }),
969
+ },
970
+ ];
971
+
972
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-faq-123', null);
973
+
974
+ expect(patches).to.be.an('array');
975
+ expect(patches.length).to.equal(2); // heading + FAQ
976
+ });
977
+
978
+ it('should handle error when checking existing config', () => {
979
+ const suggestions = [
980
+ {
981
+ getId: () => 'sugg-faq-new',
982
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
983
+ getData: () => ({
984
+ url: 'https://www.example.com/page',
985
+ headingText: 'FAQs',
986
+ shouldOptimize: true,
987
+ item: {
988
+ question: 'New question?',
989
+ answer: 'New answer.',
990
+ },
991
+ transformRules: {
992
+ action: 'appendChild',
993
+ selector: 'main',
994
+ },
995
+ }),
996
+ },
997
+ ];
998
+
999
+ // Should handle error gracefully and still create patch
1000
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-faq-123', null);
1001
+
1002
+ expect(patches).to.be.an('array');
1003
+ expect(patches.length).to.equal(2); // heading + FAQ
1004
+ });
1005
+
1006
+ it('should handle null URL when checking existing config', () => {
1007
+ const suggestions = [
1008
+ {
1009
+ getId: () => 'sugg-faq-new',
1010
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
1011
+ getData: () => ({
1012
+ url: 'https://www.example.com/page',
1013
+ headingText: 'FAQs',
1014
+ shouldOptimize: true,
1015
+ item: {
1016
+ question: 'New question?',
1017
+ answer: 'New answer.',
1018
+ },
1019
+ transformRules: {
1020
+ action: 'appendChild',
1021
+ selector: 'main',
1022
+ },
1023
+ }),
1024
+ },
1025
+ ];
1026
+
1027
+ const patches = mapper.suggestionsToPatches('/page', suggestions, 'opp-faq-123', null);
1028
+
1029
+ expect(patches).to.be.an('array');
1030
+ expect(patches.length).to.equal(2); // heading + FAQ
1031
+ });
1032
+
1033
+ it('should handle markdown to HAST conversion errors in suggestionsToPatches', () => {
1034
+ let errorCount = 0;
1035
+ const errorLog = {
1036
+ debug: () => {},
1037
+ info: () => {},
1038
+ warn: () => {},
1039
+ error: () => { errorCount += 1; },
1040
+ };
1041
+
1042
+ const errorMapper = new FaqMapper(errorLog);
1043
+
1044
+ // Override buildFaqItemHast to throw an error
1045
+ const originalBuildFaqItemHast = errorMapper.buildFaqItemHast;
1046
+ errorMapper.buildFaqItemHast = () => {
1047
+ throw new Error('Markdown parsing failed');
1048
+ };
1049
+
1050
+ const suggestions = [
1051
+ {
1052
+ getId: () => 'sugg-faq-error',
1053
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
1054
+ getData: () => ({
1055
+ url: 'https://www.example.com/page',
1056
+ headingText: 'FAQs',
1057
+ shouldOptimize: true,
1058
+ item: {
1059
+ question: 'Question?',
1060
+ answer: 'Answer.',
1061
+ },
1062
+ transformRules: {
1063
+ action: 'appendChild',
1064
+ selector: 'main',
1065
+ },
1066
+ }),
1067
+ },
1068
+ ];
1069
+
1070
+ const patches = errorMapper.suggestionsToPatches('/page', suggestions, 'opp-faq-error', null);
1071
+
1072
+ expect(patches).to.be.an('array');
1073
+ expect(patches.length).to.equal(1); // Only heading, FAQ item fails
1074
+ expect(patches[0].value.tagName).to.equal('h2'); // Heading patch
1075
+ expect(errorCount).to.be.greaterThan(0);
1076
+
1077
+ // Restore original method
1078
+ errorMapper.buildFaqItemHast = originalBuildFaqItemHast;
1079
+ });
1080
+
1081
+ it('should handle existing config with urlOptimizations but no patches', () => {
1082
+ const suggestion = {
1083
+ getId: () => 'sugg-faq-1',
1084
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
1085
+ getData: () => ({
1086
+ url: 'https://www.example.com/page',
1087
+ headingText: 'FAQs',
1088
+ shouldOptimize: true,
1089
+ item: {
1090
+ question: 'Question?',
1091
+ answer: 'Answer.',
1092
+ },
1093
+ transformRules: {
1094
+ action: 'appendChild',
1095
+ selector: 'main',
1096
+ },
1097
+ }),
1098
+ };
1099
+
1100
+ // Config with urlOptimizations but no patches array
1101
+ const existingConfig = {
1102
+ tokowakaOptimizations: {
1103
+ '/page': {
1104
+ prerender: true,
1105
+ // No patches array
1106
+ },
1107
+ },
1108
+ };
1109
+
1110
+ const patches = mapper.suggestionsToPatches('/page', [suggestion], 'opp-faq-123', existingConfig);
1111
+
1112
+ expect(patches).to.be.an('array');
1113
+ expect(patches.length).to.equal(2); // heading + FAQ (both created)
1114
+ expect(patches[0].value.tagName).to.equal('h2'); // Heading
1115
+ expect(patches[1].value.tagName).to.equal('div'); // FAQ
1116
+ });
1117
+ });
1118
+
1119
+ describe('canDeploy - new format', () => {
1120
+ it('should accept new format with item.question and item.answer', () => {
1121
+ const suggestion = {
1122
+ getData: () => ({
1123
+ item: {
1124
+ question: 'Is this the new format?',
1125
+ answer: 'Yes, it is.',
1126
+ },
1127
+ url: 'https://www.example.com/page',
1128
+ shouldOptimize: true,
1129
+ transformRules: {
1130
+ action: 'appendChild',
1131
+ selector: 'main',
1132
+ },
1133
+ }),
1134
+ };
1135
+
1136
+ const result = mapper.canDeploy(suggestion);
1137
+ expect(result).to.deep.equal({ eligible: true });
1138
+ });
1139
+
1140
+ it('should reject when item.question/answer is missing', () => {
1141
+ const suggestion = {
1142
+ getData: () => ({
1143
+ shouldOptimize: true,
1144
+ transformRules: {
1145
+ action: 'appendChild',
1146
+ selector: 'main',
1147
+ },
1148
+ }),
1149
+ };
1150
+
1151
+ const result = mapper.canDeploy(suggestion);
1152
+ expect(result.eligible).to.be.false;
1153
+ expect(result.reason).to.include('item.question and item.answer are required');
1154
+ });
1155
+ });
1156
+
1157
+ describe('tokowakaDeployed filtering', () => {
1158
+ it('should always create heading patch even when FAQ already deployed for URL', () => {
1159
+ const newSuggestion = {
1160
+ getId: () => 'sugg-new-1',
1161
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
1162
+ getData: () => ({
1163
+ url: 'https://www.example.com/page',
1164
+ headingText: 'FAQs',
1165
+ shouldOptimize: true,
1166
+ item: {
1167
+ question: 'New question?',
1168
+ answer: 'New answer.',
1169
+ },
1170
+ transformRules: {
1171
+ action: 'appendChild',
1172
+ selector: 'main',
1173
+ },
1174
+ }),
1175
+ };
1176
+
1177
+ // Mock existingConfig with heading already present
1178
+ const existingConfig = {
1179
+ tokowakaOptimizations: {
1180
+ '/page': {
1181
+ patches: [
1182
+ {
1183
+ opportunityId: 'opp-faq-123',
1184
+ lastUpdated: new Date('2025-01-01T00:00:00.000Z').getTime(),
1185
+ // No suggestionId = heading patch
1186
+ op: 'appendChild',
1187
+ selector: 'main',
1188
+ value: { type: 'element', tagName: 'h2', children: [{ type: 'text', value: 'FAQs' }] },
1189
+ },
1190
+ ],
1191
+ },
1192
+ },
1193
+ };
1194
+
1195
+ const patches = mapper.suggestionsToPatches(
1196
+ '/page',
1197
+ [newSuggestion],
1198
+ 'opp-faq-123',
1199
+ existingConfig,
1200
+ );
1201
+
1202
+ expect(patches).to.be.an('array');
1203
+ expect(patches.length).to.equal(2); // Heading + FAQ (always create heading)
1204
+
1205
+ // First patch: heading with updated timestamp
1206
+ expect(patches[0].suggestionId).to.be.undefined;
1207
+ expect(patches[0].value.tagName).to.equal('h2');
1208
+ expect(patches[0].lastUpdated).to.equal(new Date('2025-01-15T10:00:00.000Z').getTime());
1209
+
1210
+ // Second patch: FAQ
1211
+ expect(patches[1].suggestionId).to.equal('sugg-new-1');
1212
+ expect(patches[1].value.tagName).to.equal('div');
1213
+ expect(patches[1].selector).to.equal('main');
1214
+ expect(patches[1].op).to.equal('appendChild');
1215
+ });
1216
+
1217
+ it('should create heading when no patches exist for URL', () => {
1218
+ const newSuggestion = {
1219
+ getId: () => 'sugg-new-1',
1220
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
1221
+ getData: () => ({
1222
+ url: 'https://www.example.com/page',
1223
+ headingText: 'FAQs',
1224
+ item: {
1225
+ question: 'New question?',
1226
+ answer: 'New answer.',
1227
+ },
1228
+ shouldOptimize: true,
1229
+ transformRules: {
1230
+ action: 'appendChild',
1231
+ selector: 'main',
1232
+ },
1233
+ }),
1234
+ };
1235
+
1236
+ // Empty config - no patches
1237
+ const existingConfig = null;
1238
+
1239
+ const patches = mapper.suggestionsToPatches(
1240
+ '/page',
1241
+ [newSuggestion],
1242
+ 'opp-faq-123',
1243
+ existingConfig,
1244
+ );
1245
+
1246
+ expect(patches).to.be.an('array');
1247
+ expect(patches.length).to.equal(2); // heading + FAQ
1248
+
1249
+ // First should be heading
1250
+ expect(patches[0].suggestionId).to.be.undefined;
1251
+ expect(patches[0].value.tagName).to.equal('h2');
1252
+
1253
+ // Second should be FAQ
1254
+ expect(patches[1].suggestionId).to.equal('sugg-new-1');
1255
+ expect(patches[1].value.tagName).to.equal('div');
1256
+ expect(patches[1].selector).to.equal('main');
1257
+ });
1258
+
1259
+ it('should work without existing config parameter', () => {
1260
+ const newSuggestion = {
1261
+ getId: () => 'sugg-new-1',
1262
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
1263
+ getData: () => ({
1264
+ url: 'https://www.example.com/page',
1265
+ headingText: 'FAQs',
1266
+ shouldOptimize: true,
1267
+ item: {
1268
+ question: 'New question?',
1269
+ answer: 'New answer.',
1270
+ },
1271
+ transformRules: {
1272
+ action: 'appendChild',
1273
+ selector: 'main',
1274
+ },
1275
+ }),
1276
+ };
1277
+
1278
+ const patches = mapper.suggestionsToPatches(
1279
+ '/page',
1280
+ [newSuggestion],
1281
+ 'opp-faq-123',
1282
+ null, // No existing config
1283
+ );
1284
+
1285
+ expect(patches).to.be.an('array');
1286
+ expect(patches.length).to.equal(2); // heading + FAQ
1287
+ expect(patches[0].suggestionId).to.be.undefined; // heading
1288
+ expect(patches[1].suggestionId).to.equal('sugg-new-1'); // FAQ
1289
+ });
1290
+
1291
+ it('should handle invalid existing config', () => {
1292
+ const newSuggestion = {
1293
+ getId: () => 'sugg-new-1',
1294
+ getUpdatedAt: () => '2025-01-15T10:00:00.000Z',
1295
+ getData: () => ({
1296
+ url: 'https://www.example.com/page',
1297
+ headingText: 'FAQs',
1298
+ shouldOptimize: true,
1299
+ item: {
1300
+ question: 'New question?',
1301
+ answer: 'New answer.',
1302
+ },
1303
+ transformRules: {
1304
+ action: 'appendChild',
1305
+ selector: 'main',
1306
+ },
1307
+ }),
1308
+ };
1309
+
1310
+ // Pass invalid config
1311
+ const patches = mapper.suggestionsToPatches(
1312
+ '/page',
1313
+ [newSuggestion],
1314
+ 'opp-faq-123',
1315
+ 'not-valid-config',
1316
+ );
1317
+
1318
+ expect(patches).to.be.an('array');
1319
+ expect(patches.length).to.equal(2); // heading + FAQ
1320
+ expect(patches[0].suggestionId).to.be.undefined; // heading
1321
+ expect(patches[1].suggestionId).to.equal('sugg-new-1'); // FAQ
1322
+ });
1323
+ });
1324
+ });