@adobe/spacecat-shared-tokowaka-client 1.14.0 → 1.15.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,9 @@
1
+ ## [@adobe/spacecat-shared-tokowaka-client-v1.15.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.14.0...@adobe/spacecat-shared-tokowaka-client-v1.15.0) (2026-05-04)
2
+
3
+ ### Features
4
+
5
+ * **AGENTCOM-634:** PDP enrichment does not correctly format on the AI visibility tool checker ([#1578](https://github.com/adobe/spacecat-shared/issues/1578)) ([b6140db](https://github.com/adobe/spacecat-shared/commit/b6140db8406ab2259dd3e91acdd31d91680e64ad))
6
+
1
7
  ## [@adobe/spacecat-shared-tokowaka-client-v1.14.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-tokowaka-client-v1.13.5...@adobe/spacecat-shared-tokowaka-client-v1.14.0) (2026-05-04)
2
8
 
3
9
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-tokowaka-client",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "Tokowaka Client for SpaceCat - Edge optimization config management",
5
5
  "type": "module",
6
6
  "engines": {
@@ -19,6 +19,18 @@ const EXCLUDED_FIELDS = new Set([
19
19
  'rationale',
20
20
  ]);
21
21
 
22
+ // Array-typed keys rendered as <ul> that should NOT get a wrapping <section> + <h3>.
23
+ const HEADING_EXCLUDED_KEYS = new Set([
24
+ 'facts.feature_bullets',
25
+ 'facts.description_plain',
26
+ ]);
27
+
28
+ function deriveHeadingLabel(key) {
29
+ const lastSegment = key.split('.').pop();
30
+ const spaced = lastSegment.replace(/_/g, ' ');
31
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
32
+ }
33
+
22
34
  // Fields rendered in fixed order at the top of the article.
23
35
  // Each entry: CSS class, display label, [source keys in priority order]
24
36
  const ORDERED_FIELDS = [
@@ -121,7 +133,12 @@ function enrichmentToHtml(enrichmentData) {
121
133
  const cls = sanitizeClassName(key);
122
134
  const html = renderValue(key, value, cls);
123
135
  if (html) {
124
- parts.push(html);
136
+ if (Array.isArray(value) && !HEADING_EXCLUDED_KEYS.has(key)) {
137
+ const label = escapeHtml(deriveHeadingLabel(key));
138
+ parts.push(`<section class="${cls}"><h3 class="${cls}">${label}</h3>${html}</section>`);
139
+ } else {
140
+ parts.push(html);
141
+ }
125
142
  }
126
143
  }
127
144
 
@@ -660,6 +660,146 @@ describe('CommercePageEnrichmentMapper', () => {
660
660
  expect(wrapper.properties.dataSku).to.be.undefined;
661
661
  });
662
662
 
663
+ it('should wrap each qualifying array field in a <section> with derived <h3>', () => {
664
+ const suggestion = makeSuggestion({
665
+ patchValue: JSON.stringify({
666
+ sku: 'BigOne_1935',
667
+ 'facts.attributes.color_family': ['Black', 'Blue'],
668
+ 'facts.attributes.fabric': ['Velvet'],
669
+ 'facts.attributes.fabric_type': ['Charcoal Wombat Phur'],
670
+ 'facts.options': ['Cover', 'Insert'],
671
+ 'facts.configurations': ['BigOne Insert & Cover'],
672
+ }),
673
+ url: 'https://www.lovesac.com/products/bigone',
674
+ });
675
+
676
+ const patches = mapper.suggestionsToPatches('/products/bigone', [suggestion], opportunityId);
677
+ const article = findNode(patches[0].value, (n) => n.tagName === 'article');
678
+
679
+ const sections = findAllNodes(article, (n) => n.tagName === 'section');
680
+ expect(sections).to.have.length(5);
681
+
682
+ const expected = [
683
+ { cls: 'color-family', label: 'Color family' },
684
+ { cls: 'fabric', label: 'Fabric' },
685
+ { cls: 'fabric-type', label: 'Fabric type' },
686
+ { cls: 'facts-options', label: 'Options' },
687
+ { cls: 'facts-configurations', label: 'Configurations' },
688
+ ];
689
+ for (const { cls, label } of expected) {
690
+ const section = sections.find((s) => s.properties?.className?.includes(cls));
691
+ expect(section, `section.${cls}`).to.exist;
692
+
693
+ const h3 = findNode(section, (n) => n.tagName === 'h3');
694
+ expect(h3, `h3 in section.${cls}`).to.exist;
695
+ expect(h3.properties.className).to.include(cls);
696
+ expect(textContent(h3)).to.equal(label);
697
+
698
+ const ul = findNode(section, (n) => n.tagName === 'ul');
699
+ expect(ul, `ul in section.${cls}`).to.exist;
700
+ expect(ul.properties.className).to.include(cls);
701
+ }
702
+ });
703
+
704
+ it('should place <h3> immediately before <ul> inside the <section>', () => {
705
+ const suggestion = makeSuggestion({
706
+ patchValue: JSON.stringify({
707
+ sku: 'TEST',
708
+ 'facts.attributes.fabric': ['Velvet', 'Phur'],
709
+ }),
710
+ url: 'https://example.com/page',
711
+ });
712
+
713
+ const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
714
+ const section = findNode(patches[0].value, (n) => n.tagName === 'section'
715
+ && n.properties?.className?.includes('fabric'));
716
+ expect(section).to.exist;
717
+
718
+ const elementChildren = section.children.filter((c) => c.type === 'element');
719
+ expect(elementChildren).to.have.length(2);
720
+ expect(elementChildren[0].tagName).to.equal('h3');
721
+ expect(elementChildren[1].tagName).to.equal('ul');
722
+ });
723
+
724
+ it('should NOT wrap excluded array keys (facts.feature_bullets, facts.description_plain)', () => {
725
+ const suggestion = makeSuggestion({
726
+ patchValue: JSON.stringify({
727
+ sku: 'TEST',
728
+ 'facts.feature_bullets': ['Bullet one', 'Bullet two'],
729
+ 'facts.description_plain': 'A plain description.',
730
+ }),
731
+ url: 'https://example.com/page',
732
+ });
733
+
734
+ const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
735
+ const article = findNode(patches[0].value, (n) => n.tagName === 'article');
736
+
737
+ expect(findAllNodes(article, (n) => n.tagName === 'section')).to.have.length(0);
738
+ expect(findAllNodes(article, (n) => n.tagName === 'h3')).to.have.length(0);
739
+
740
+ const ul = findNode(article, (n) => n.tagName === 'ul'
741
+ && n.properties?.className?.includes('facts-feature-bullets'));
742
+ expect(ul).to.exist;
743
+ });
744
+
745
+ it('should NOT wrap empty arrays in a <section>', () => {
746
+ const suggestion = makeSuggestion({
747
+ patchValue: JSON.stringify({
748
+ sku: 'TEST',
749
+ 'facts.attributes.fabric': [],
750
+ }),
751
+ url: 'https://example.com/page',
752
+ });
753
+
754
+ const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
755
+ const article = findNode(patches[0].value, (n) => n.tagName === 'article');
756
+
757
+ expect(findAllNodes(article, (n) => n.tagName === 'section')).to.have.length(0);
758
+ expect(findAllNodes(article, (n) => n.tagName === 'h3')).to.have.length(0);
759
+ expect(findAllNodes(article, (n) => n.tagName === 'ul'
760
+ && n.properties?.className?.includes('fabric'))).to.have.length(0);
761
+ });
762
+
763
+ it('should NOT wrap non-array values (string, object) in a <section>', () => {
764
+ const suggestion = makeSuggestion({
765
+ patchValue: JSON.stringify({
766
+ sku: 'TEST',
767
+ brand: 'Lovesac',
768
+ custom_attrs: { color: 'Blue', size: 'L' },
769
+ }),
770
+ url: 'https://example.com/page',
771
+ });
772
+
773
+ const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
774
+ const article = findNode(patches[0].value, (n) => n.tagName === 'article');
775
+
776
+ expect(findAllNodes(article, (n) => n.tagName === 'section')).to.have.length(0);
777
+ expect(findAllNodes(article, (n) => n.tagName === 'h3')).to.have.length(0);
778
+
779
+ const brandP = findNode(article, (n) => n.tagName === 'p'
780
+ && n.properties?.className?.includes('brand'));
781
+ expect(brandP).to.exist;
782
+
783
+ const customUl = findNode(article, (n) => n.tagName === 'ul'
784
+ && n.properties?.className?.includes('custom-attrs'));
785
+ expect(customUl).to.exist;
786
+ });
787
+
788
+ it('should derive heading labels with only the first character uppercased', () => {
789
+ const suggestion = makeSuggestion({
790
+ patchValue: JSON.stringify({
791
+ sku: 'TEST',
792
+ 'facts.attributes.fabric_type': ['Wombat Phur'],
793
+ }),
794
+ url: 'https://example.com/page',
795
+ });
796
+
797
+ const patches = mapper.suggestionsToPatches('/page', [suggestion], opportunityId);
798
+ const h3 = findNode(patches[0].value, (n) => n.tagName === 'h3');
799
+ expect(h3).to.exist;
800
+ expect(textContent(h3)).to.equal('Fabric type');
801
+ });
802
+
663
803
  it('should use ordered fields priority (category_path over category)', () => {
664
804
  const suggestion = makeSuggestion({
665
805
  patchValue: JSON.stringify({