@certrev/cert-block 0.5.5 → 1.0.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.
Files changed (190) hide show
  1. package/CHANGELOG.md +164 -26
  2. package/LICENSE +21 -0
  3. package/README.md +415 -54
  4. package/dist/builder/cert-chrome-data.d.ts +26 -8
  5. package/dist/builder/cert-chrome-data.d.ts.map +1 -1
  6. package/dist/builder/cert-chrome-data.js +5 -5
  7. package/dist/builder/cert-chrome-data.js.map +1 -1
  8. package/dist/builder/cert-review-card.d.ts +35 -5
  9. package/dist/builder/cert-review-card.d.ts.map +1 -1
  10. package/dist/builder/cert-review-card.js +115 -34
  11. package/dist/builder/cert-review-card.js.map +1 -1
  12. package/dist/builder/index.d.ts +33 -16
  13. package/dist/builder/index.d.ts.map +1 -1
  14. package/dist/builder/index.js +33 -16
  15. package/dist/builder/index.js.map +1 -1
  16. package/dist/components/CertBadge.d.ts +2 -2
  17. package/dist/components/CertBadge.d.ts.map +1 -1
  18. package/dist/components/CertBadge.js +11 -5
  19. package/dist/components/CertBadge.js.map +1 -1
  20. package/dist/components/CertJsonLd.d.ts.map +1 -1
  21. package/dist/components/CertJsonLd.js.map +1 -1
  22. package/dist/components/CertRevBacklink.d.ts.map +1 -1
  23. package/dist/components/CertRevBacklink.js +3 -2
  24. package/dist/components/CertRevBacklink.js.map +1 -1
  25. package/dist/components/CertReview.d.ts +1 -1
  26. package/dist/components/ExpertBio.d.ts.map +1 -1
  27. package/dist/components/ExpertBio.js +12 -4
  28. package/dist/components/ExpertBio.js.map +1 -1
  29. package/dist/components/escape.d.ts +28 -1
  30. package/dist/components/escape.d.ts.map +1 -1
  31. package/dist/components/escape.js +37 -8
  32. package/dist/components/escape.js.map +1 -1
  33. package/dist/components/format.d.ts +51 -2
  34. package/dist/components/format.d.ts.map +1 -1
  35. package/dist/components/format.js +103 -6
  36. package/dist/components/format.js.map +1 -1
  37. package/dist/components/preview-block.d.ts +1 -1
  38. package/dist/components/preview-block.js +3 -3
  39. package/dist/components/preview-block.js.map +1 -1
  40. package/dist/components/rel.d.ts +24 -0
  41. package/dist/components/rel.d.ts.map +1 -0
  42. package/dist/components/rel.js +24 -0
  43. package/dist/components/rel.js.map +1 -0
  44. package/dist/components/render-cert-block.d.ts +50 -22
  45. package/dist/components/render-cert-block.d.ts.map +1 -1
  46. package/dist/components/render-cert-block.js +146 -80
  47. package/dist/components/render-cert-block.js.map +1 -1
  48. package/dist/components/render-def.d.ts +13 -14
  49. package/dist/components/render-def.d.ts.map +1 -1
  50. package/dist/components/render-def.js +14 -15
  51. package/dist/components/render-def.js.map +1 -1
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +4 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/jsonld/project.d.ts +46 -13
  57. package/dist/jsonld/project.d.ts.map +1 -1
  58. package/dist/jsonld/project.js +139 -56
  59. package/dist/jsonld/project.js.map +1 -1
  60. package/dist/modal/avatar-resize.d.ts +10 -10
  61. package/dist/modal/avatar-resize.js +14 -14
  62. package/dist/modal/avatar-resize.js.map +1 -1
  63. package/dist/modal/cert-modal-view.d.ts +27 -7
  64. package/dist/modal/cert-modal-view.d.ts.map +1 -1
  65. package/dist/modal/cert-modal-view.js +49 -25
  66. package/dist/modal/cert-modal-view.js.map +1 -1
  67. package/dist/modal/certrev-cert-modal.d.ts +5 -5
  68. package/dist/modal/certrev-cert-modal.js +9 -9
  69. package/dist/modal/certrev-cert-modal.js.map +1 -1
  70. package/dist/modal/certrev-cert-modal.styles.d.ts +6 -6
  71. package/dist/modal/certrev-cert-modal.styles.d.ts.map +1 -1
  72. package/dist/modal/certrev-cert-modal.styles.js +8 -8
  73. package/dist/modal/certrev-cert-modal.styles.js.map +1 -1
  74. package/dist/modal/certrev-cert.entry.d.ts +8 -6
  75. package/dist/modal/certrev-cert.entry.d.ts.map +1 -1
  76. package/dist/modal/certrev-cert.entry.js +9 -7
  77. package/dist/modal/certrev-cert.entry.js.map +1 -1
  78. package/dist/modal/certrev-cert.js +6 -6
  79. package/dist/modal/display-strings.d.ts +25 -5
  80. package/dist/modal/display-strings.d.ts.map +1 -1
  81. package/dist/modal/display-strings.js +27 -5
  82. package/dist/modal/display-strings.js.map +1 -1
  83. package/dist/modal/escape.d.ts +26 -3
  84. package/dist/modal/escape.d.ts.map +1 -1
  85. package/dist/modal/escape.js +32 -3
  86. package/dist/modal/escape.js.map +1 -1
  87. package/dist/modal/font-face.d.ts.map +1 -1
  88. package/dist/modal/font-face.js +8 -11
  89. package/dist/modal/font-face.js.map +1 -1
  90. package/dist/modal/ftc-disclosure.d.ts +2 -2
  91. package/dist/modal/ftc-disclosure.js +2 -2
  92. package/dist/modal/ftc-guard.d.ts +44 -9
  93. package/dist/modal/ftc-guard.d.ts.map +1 -1
  94. package/dist/modal/ftc-guard.js +50 -17
  95. package/dist/modal/ftc-guard.js.map +1 -1
  96. package/dist/modal/index.d.ts +1 -1
  97. package/dist/modal/index.js +1 -1
  98. package/dist/modal/interactions.d.ts +2 -2
  99. package/dist/modal/interactions.js +2 -2
  100. package/dist/modal/logo-paths.js +2 -2
  101. package/dist/modal/logo-paths.js.map +1 -1
  102. package/dist/modal/placement.d.ts +4 -3
  103. package/dist/modal/placement.d.ts.map +1 -1
  104. package/dist/modal/placement.js +7 -6
  105. package/dist/modal/placement.js.map +1 -1
  106. package/dist/modal/revalidate.d.ts +2 -3
  107. package/dist/modal/revalidate.d.ts.map +1 -1
  108. package/dist/modal/revalidate.js +2 -3
  109. package/dist/modal/revalidate.js.map +1 -1
  110. package/dist/modal/verify-client.d.ts +2 -2
  111. package/dist/modal/verify-client.js +2 -2
  112. package/dist/verify/cache.d.ts +21 -6
  113. package/dist/verify/cache.d.ts.map +1 -1
  114. package/dist/verify/cache.js +29 -8
  115. package/dist/verify/cache.js.map +1 -1
  116. package/dist/verify/fetch-with-deadline.d.ts +35 -0
  117. package/dist/verify/fetch-with-deadline.d.ts.map +1 -0
  118. package/dist/verify/fetch-with-deadline.js +55 -0
  119. package/dist/verify/fetch-with-deadline.js.map +1 -0
  120. package/dist/verify/get-verified-envelope.d.ts +23 -9
  121. package/dist/verify/get-verified-envelope.d.ts.map +1 -1
  122. package/dist/verify/get-verified-envelope.js +89 -40
  123. package/dist/verify/get-verified-envelope.js.map +1 -1
  124. package/dist/verify/resolve-kid.d.ts +3 -0
  125. package/dist/verify/resolve-kid.d.ts.map +1 -1
  126. package/dist/verify/resolve-kid.js +17 -8
  127. package/dist/verify/resolve-kid.js.map +1 -1
  128. package/dist/webcomponent/certrev-badge.d.ts.map +1 -1
  129. package/dist/webcomponent/certrev-badge.js +24 -2
  130. package/dist/webcomponent/certrev-badge.js.map +1 -1
  131. package/dist/webcomponent/render-badge-html.d.ts +1 -1
  132. package/dist/webcomponent/render-badge-html.d.ts.map +1 -1
  133. package/dist/webcomponent/render-badge-html.js +36 -17
  134. package/dist/webcomponent/render-badge-html.js.map +1 -1
  135. package/package.json +5 -2
  136. package/src/builder/cert-chrome-data.ts +26 -8
  137. package/src/builder/cert-review-card.tsx +147 -38
  138. package/src/builder/index.tsx +34 -17
  139. package/src/components/CertBadge.tsx +14 -9
  140. package/src/components/CertJsonLd.tsx +1 -1
  141. package/src/components/CertRevBacklink.tsx +10 -3
  142. package/src/components/CertReview.tsx +1 -1
  143. package/src/components/ExpertBio.tsx +12 -7
  144. package/src/components/escape.ts +37 -7
  145. package/src/components/format.ts +102 -6
  146. package/src/components/preview-block.ts +3 -3
  147. package/src/components/rel.ts +23 -0
  148. package/src/components/render-cert-block.ts +192 -109
  149. package/src/components/render-def.ts +14 -15
  150. package/src/index.ts +4 -2
  151. package/src/jsonld/project.ts +155 -59
  152. package/src/modal/avatar-resize.ts +14 -14
  153. package/src/modal/cert-modal-view.ts +52 -27
  154. package/src/modal/certrev-cert-modal.styles.ts +8 -8
  155. package/src/modal/certrev-cert-modal.ts +9 -9
  156. package/src/modal/certrev-cert.entry.ts +9 -7
  157. package/src/modal/display-strings.ts +26 -5
  158. package/src/modal/escape.ts +32 -4
  159. package/src/modal/font-face.ts +8 -11
  160. package/src/modal/ftc-disclosure.ts +2 -2
  161. package/src/modal/ftc-guard.ts +73 -18
  162. package/src/modal/index.ts +1 -1
  163. package/src/modal/interactions.ts +2 -2
  164. package/src/modal/logo-paths.ts +2 -2
  165. package/src/modal/placement.ts +7 -6
  166. package/src/modal/revalidate.ts +2 -3
  167. package/src/modal/verify-client.ts +2 -2
  168. package/src/verify/cache.ts +34 -8
  169. package/src/verify/fetch-with-deadline.ts +60 -0
  170. package/src/verify/get-verified-envelope.ts +131 -48
  171. package/src/verify/resolve-kid.ts +22 -6
  172. package/src/webcomponent/certrev-badge.ts +24 -2
  173. package/src/webcomponent/render-badge-html.ts +66 -19
  174. package/src/__tests__/__snapshots__/render-cert-block.test.ts.snap +0 -17
  175. package/src/__tests__/components.test.tsx +0 -206
  176. package/src/__tests__/preview-block.test.ts +0 -84
  177. package/src/__tests__/project.test.ts +0 -128
  178. package/src/__tests__/readme-version.test.ts +0 -51
  179. package/src/__tests__/render-cert-block.test.ts +0 -657
  180. package/src/__tests__/render-def.test.tsx +0 -295
  181. package/src/__tests__/verify.test.ts +0 -239
  182. package/src/__tests__/webcomponent.test.tsx +0 -116
  183. package/src/builder/__tests__/builder.test.tsx +0 -158
  184. package/src/modal/cert-modal-view.test.ts +0 -175
  185. package/src/modal/certrev-cert-modal.test.ts +0 -125
  186. package/src/modal/ftc-guard.test.ts +0 -153
  187. package/src/modal/interactions.test.ts +0 -250
  188. package/src/modal/placement.test.ts +0 -398
  189. package/src/modal/revalidate.test.ts +0 -111
  190. package/src/modal/verify-client.test.ts +0 -164
@@ -1,17 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`renderCertBlock — locked-design snapshots > banner (navy default) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--banner" data-certrev-mode="banner" data-certrev-strings-version="2026-07" style="--ba:#0a1b3f;--ba-fg:#ffffff;--br:14px;--cr-bf:var(--bf,'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif);--surface:#ffffff;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:rgba(10,27,63,.55);--cr-ink-soft:rgba(10,27,63,.75);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);overflow:hidden;font-family:var(--cr-bf);"><div data-certrev-modal-open style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px 22px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);cursor:pointer;"><span style="display:inline-flex;align-items:center;gap:11px;font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;"><svg viewBox="0 0 375 375" width="20" height="20" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg> Expert reviewed</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" aria-label="View certificate" style="display:inline-flex;align-items:center;flex-shrink:0;color:inherit;opacity:.72;text-decoration:none;"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></a></div><div style="padding:22px 24px 18px;"><div style="display:grid;grid-template-columns:1fr 1fr;gap:22px;"><div><div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-35);margin-bottom:11px;">Written by</div><div style="display:flex;align-items:center;gap:12px;"><div style="width:42px;height:42px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:12px;flex-shrink:0;">TE</div><div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">The Editorial Team</div><div style="font-size:13px;color:var(--cr-ink-sub);">Editorial</div></div></div></div><div><div style="font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);margin-bottom:11px;">Reviewed by</div><div class="certrev-cert__rev" role="button" tabindex="0" aria-expanded="false" data-certrev-bio-toggle aria-label="Read more about Jane Doe" style="display:flex;align-items:flex-start;gap:12px;cursor:pointer;border-radius:8px;transition:opacity .15s;"><div style="width:42px;height:42px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:12px;flex-shrink:0;">JD</div><div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">Jane Doe, MD<span class="certrev-cert__caret" style="display:inline-flex;vertical-align:middle;margin-left:4px;color:var(--navy-35);transition:transform .18s ease-out;"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg></span></div><div style="font-size:13px;color:var(--navy-55);line-height:1.45;margin-top:3px;">Credential verified by CertREV</div><div style="font-size:13px;color:var(--navy-55);line-height:1.35;">on Apr 14, 2026</div><div class="certrev-cert__bio" hidden style="display:none;margin-top:8px;font-size:12.5px;line-height:1.5;color:var(--cr-ink-soft);">Board-certified dermatologist in San Francisco specializing in photoprotection, skin-cancer prevention, and evidence-based skincare, with fifteen years of clinical practice.</div></div></div></div></div><div style="height:1px;background:var(--navy-10);margin:16px 0 14px;"></div><div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);"><span>Certified <span style="margin-left:8px;">Apr 14, 2026</span></span><span>Compensated expert</span></div></div></div><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);padding:24px;font-family:var(--cr-bf);"><div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--navy-55);margin-bottom:14px;">Expert memo</div><div style="display:flex;gap:15px;"><div style="width:46px;height:46px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:13px;flex-shrink:0;">JD</div><div style="flex:1;min-width:0;"><div style="font-size:16px;line-height:1.2;color:var(--cr-ink);"><span style="font-weight:700;">Jane Doe</span><span style="color:var(--cr-ink-sub);">, MD</span></div><p style="margin:11px 0 14px;font-size:15.5px;line-height:1.6;color:var(--cr-ink-soft);white-space:pre-line;">Patients ask me almost every week whether the SPF in their foundation is enough on its own. Treat makeup with SPF as a helpful last layer, not the base of your routine.</p></div></div><div style="height:1px;background:var(--navy-10);margin:18px 0 14px;"></div><div style="display:flex;align-items:center;justify-content:space-between;gap:16px 22px;flex-wrap:wrap;"><div style="display:inline-flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);"><svg viewBox="0 0 375 375" width="15" height="15" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg><span>Verified by <span style="font-weight:700;color:var(--navy);">CertREV</span></span></div><div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;"><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;">Jane's Profile</a><span aria-hidden="true" style="font-family:var(--font-mono);font-size:10.5px;color:var(--navy-35);">|</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;">Certificate</a></div></div><div style="font-size:13px;color:var(--navy-55);margin-top:8px;line-height:1.5;">Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.</div></div></div>"`;
4
-
5
- exports[`renderCertBlock — locked-design snapshots > banner (themed, non-navy accent) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--banner" data-certrev-mode="banner" data-certrev-strings-version="2026-07" style="--ba:#E8603C;--ba-fg:#141414;--br:22px;--cr-bf:var(--bf,'Plus Jakarta Sans', system-ui, Helvetica, Arial, sans-serif);--surface:#faf8f4;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:rgba(10,27,63,.55);--cr-ink-soft:rgba(10,27,63,.75);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);overflow:hidden;font-family:var(--cr-bf);"><div data-certrev-modal-open style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px 22px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);cursor:pointer;"><span style="display:inline-flex;align-items:center;gap:11px;font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;"><svg viewBox="0 0 375 375" width="20" height="20" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg> Expert reviewed</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" aria-label="View certificate" style="display:inline-flex;align-items:center;flex-shrink:0;color:inherit;opacity:.72;text-decoration:none;"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></a></div><div style="padding:22px 24px 18px;"><div style="display:grid;grid-template-columns:1fr 1fr;gap:22px;"><div><div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-35);margin-bottom:11px;">Written by</div><div style="display:flex;align-items:center;gap:12px;"><div style="width:42px;height:42px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:12px;flex-shrink:0;">TE</div><div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">The Editorial Team</div><div style="font-size:13px;color:var(--cr-ink-sub);">Editorial</div></div></div></div><div><div style="font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);margin-bottom:11px;">Reviewed by</div><div class="certrev-cert__rev" role="button" tabindex="0" aria-expanded="false" data-certrev-bio-toggle aria-label="Read more about Jane Doe" style="display:flex;align-items:flex-start;gap:12px;cursor:pointer;border-radius:8px;transition:opacity .15s;"><div style="width:42px;height:42px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:12px;flex-shrink:0;">JD</div><div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">Jane Doe, MD<span class="certrev-cert__caret" style="display:inline-flex;vertical-align:middle;margin-left:4px;color:var(--navy-35);transition:transform .18s ease-out;"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg></span></div><div style="font-size:13px;color:var(--navy-55);line-height:1.45;margin-top:3px;">Credential verified by CertREV</div><div style="font-size:13px;color:var(--navy-55);line-height:1.35;">on Apr 14, 2026</div><div class="certrev-cert__bio" hidden style="display:none;margin-top:8px;font-size:12.5px;line-height:1.5;color:var(--cr-ink-soft);">Board-certified dermatologist in San Francisco specializing in photoprotection, skin-cancer prevention, and evidence-based skincare, with fifteen years of clinical practice.</div></div></div></div></div><div style="height:1px;background:var(--navy-10);margin:16px 0 14px;"></div><div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);"><span>Certified <span style="margin-left:8px;">Apr 14, 2026</span></span><span>Compensated expert</span></div></div></div><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);padding:24px;font-family:var(--cr-bf);"><div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--navy-55);margin-bottom:14px;">Expert memo</div><div style="display:flex;gap:15px;"><div style="width:46px;height:46px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:13px;flex-shrink:0;">JD</div><div style="flex:1;min-width:0;"><div style="font-size:16px;line-height:1.2;color:var(--cr-ink);"><span style="font-weight:700;">Jane Doe</span><span style="color:var(--cr-ink-sub);">, MD</span></div><p style="margin:11px 0 14px;font-size:15.5px;line-height:1.6;color:var(--cr-ink-soft);white-space:pre-line;">Patients ask me almost every week whether the SPF in their foundation is enough on its own. Treat makeup with SPF as a helpful last layer, not the base of your routine.</p></div></div><div style="height:1px;background:var(--navy-10);margin:18px 0 14px;"></div><div style="display:flex;align-items:center;justify-content:space-between;gap:16px 22px;flex-wrap:wrap;"><div style="display:inline-flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);"><svg viewBox="0 0 375 375" width="15" height="15" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg><span>Verified by <span style="font-weight:700;color:var(--navy);">CertREV</span></span></div><div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;"><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;">Jane's Profile</a><span aria-hidden="true" style="font-family:var(--font-mono);font-size:10.5px;color:var(--navy-35);">|</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;">Certificate</a></div></div><div style="font-size:13px;color:var(--navy-55);margin-top:8px;line-height:1.5;">Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.</div></div></div>"`;
6
-
7
- exports[`renderCertBlock — locked-design snapshots > banner (v2 brand-ink: barInk + inkColor) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--banner" data-certrev-mode="banner" data-certrev-strings-version="2026-07" style="--ba:#E8603C;--ba-fg:#141414;--br:22px;--cr-bf:var(--bf,'Plus Jakarta Sans', system-ui, Helvetica, Arial, sans-serif);--surface:#faf8f4;--certrev-bar-bg:#000000;--certrev-bar-fg:#ffffff;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--certrev-ink:#1a1a1a;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:color-mix(in srgb,var(--cr-ink) 55%,transparent);--cr-ink-soft:color-mix(in srgb,var(--cr-ink) 75%,transparent);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);overflow:hidden;font-family:var(--cr-bf);"><div data-certrev-modal-open style="display:flex;align-items:center;justify-content:space-between;gap:16px;padding:15px 22px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);cursor:pointer;"><span style="display:inline-flex;align-items:center;gap:11px;font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;"><svg viewBox="0 0 375 375" width="20" height="20" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg> Expert reviewed</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" aria-label="View certificate" style="display:inline-flex;align-items:center;flex-shrink:0;color:inherit;opacity:.72;text-decoration:none;"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></a></div><div style="padding:22px 24px 18px;"><div style="display:grid;grid-template-columns:1fr 1fr;gap:22px;"><div><div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-35);margin-bottom:11px;">Written by</div><div style="display:flex;align-items:center;gap:12px;"><div style="width:42px;height:42px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:12px;flex-shrink:0;">TE</div><div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">The Editorial Team</div><div style="font-size:13px;color:var(--cr-ink-sub);">Editorial</div></div></div></div><div><div style="font-family:var(--font-mono);font-size:10px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);margin-bottom:11px;">Reviewed by</div><div class="certrev-cert__rev" role="button" tabindex="0" aria-expanded="false" data-certrev-bio-toggle aria-label="Read more about Jane Doe" style="display:flex;align-items:flex-start;gap:12px;cursor:pointer;border-radius:8px;transition:opacity .15s;"><div style="width:42px;height:42px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:12px;flex-shrink:0;">JD</div><div><div style="font-size:15px;font-weight:700;color:var(--cr-ink);">Jane Doe, MD<span class="certrev-cert__caret" style="display:inline-flex;vertical-align:middle;margin-left:4px;color:var(--navy-35);transition:transform .18s ease-out;"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg></span></div><div style="font-size:13px;color:var(--navy-55);line-height:1.45;margin-top:3px;">Credential verified by CertREV</div><div style="font-size:13px;color:var(--navy-55);line-height:1.35;">on Apr 14, 2026</div><div class="certrev-cert__bio" hidden style="display:none;margin-top:8px;font-size:12.5px;line-height:1.5;color:var(--cr-ink-soft);">Board-certified dermatologist in San Francisco specializing in photoprotection, skin-cancer prevention, and evidence-based skincare, with fifteen years of clinical practice.</div></div></div></div></div><div style="height:1px;background:var(--navy-10);margin:16px 0 14px;"></div><div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);"><span>Certified <span style="margin-left:8px;">Apr 14, 2026</span></span><span>Compensated expert</span></div></div></div><div style="border:1px solid var(--navy-10);border-radius:calc(var(--br,14px) * 0.7);padding:24px;font-family:var(--cr-bf);"><div style="font-family:var(--font-mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--navy-55);margin-bottom:14px;">Expert memo</div><div style="display:flex;gap:15px;"><div style="width:46px;height:46px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:13px;flex-shrink:0;">JD</div><div style="flex:1;min-width:0;"><div style="font-size:16px;line-height:1.2;color:var(--cr-ink);"><span style="font-weight:700;">Jane Doe</span><span style="color:var(--cr-ink-sub);">, MD</span></div><p style="margin:11px 0 14px;font-size:15.5px;line-height:1.6;color:var(--cr-ink-soft);white-space:pre-line;">Patients ask me almost every week whether the SPF in their foundation is enough on its own. Treat makeup with SPF as a helpful last layer, not the base of your routine.</p></div></div><div style="height:1px;background:var(--navy-10);margin:18px 0 14px;"></div><div style="display:flex;align-items:center;justify-content:space-between;gap:16px 22px;flex-wrap:wrap;"><div style="display:inline-flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy-55);"><svg viewBox="0 0 375 375" width="15" height="15" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg><span>Verified by <span style="font-weight:700;color:var(--navy);">CertREV</span></span></div><div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;"><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;">Jane's Profile</a><span aria-hidden="true" style="font-family:var(--font-mono);font-size:10.5px;color:var(--navy-35);">|</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="font-family:var(--font-mono);font-size:10.5px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--navy);text-decoration:none;cursor:pointer;">Certificate</a></div></div><div style="font-size:13px;color:var(--navy-55);margin-top:8px;line-height:1.5;">Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.</div></div></div>"`;
8
-
9
- exports[`renderCertBlock — locked-design snapshots > floating (navy default) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--floating" data-certrev-mode="floating" data-certrev-strings-version="2026-07" style="--ba:#0a1b3f;--ba-fg:#ffffff;--br:14px;--cr-bf:var(--bf,'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif);--surface:#ffffff;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:rgba(10,27,63,.55);--cr-ink-soft:rgba(10,27,63,.75);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div class="certrev-cert__pill" style="display:inline-flex;align-items:center;gap:14px;padding:12px 22px;border-radius:999px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);box-shadow:0 10px 26px var(--navy-10);"><span style="font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;opacity:.75;">Expert reviewed:</span><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="font-size:14px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:inherit;cursor:pointer;">Profile</a><span style="opacity:.5;">·</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="font-size:14px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:inherit;cursor:pointer;">Certificate</a></div></div>"`;
10
-
11
- exports[`renderCertBlock — locked-design snapshots > floating (themed, non-navy accent) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--floating" data-certrev-mode="floating" data-certrev-strings-version="2026-07" style="--ba:#E8603C;--ba-fg:#141414;--br:22px;--cr-bf:var(--bf,'Plus Jakarta Sans', system-ui, Helvetica, Arial, sans-serif);--surface:#faf8f4;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:rgba(10,27,63,.55);--cr-ink-soft:rgba(10,27,63,.75);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div class="certrev-cert__pill" style="display:inline-flex;align-items:center;gap:14px;padding:12px 22px;border-radius:999px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);box-shadow:0 10px 26px var(--navy-10);"><span style="font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;opacity:.75;">Expert reviewed:</span><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="font-size:14px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:inherit;cursor:pointer;">Profile</a><span style="opacity:.5;">·</span><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="font-size:14px;font-weight:700;text-decoration:underline;text-underline-offset:3px;color:inherit;cursor:pointer;">Certificate</a></div></div>"`;
12
-
13
- exports[`renderCertBlock — locked-design snapshots > sidebar (navy default) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--sidebar" data-certrev-mode="sidebar" data-certrev-strings-version="2026-07" style="--ba:#0a1b3f;--ba-fg:#ffffff;--br:14px;--cr-bf:var(--bf,'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif);--surface:#ffffff;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:rgba(10,27,63,.55);--cr-ink-soft:rgba(10,27,63,.75);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div style="background:var(--surface);border:1px solid var(--navy-10);border-radius:var(--br,14px);overflow:hidden;box-shadow:0 4px 18px var(--navy-06);font-family:var(--cr-bf);"><div style="display:flex;align-items:center;justify-content:center;gap:10px;padding:12px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;"><svg viewBox="0 0 375 375" width="18" height="18" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg> Expert reviewed</div><div style="padding:20px;"><div style="display:flex;align-items:flex-start;gap:13px;"><div style="width:46px;height:46px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:13px;flex-shrink:0;">JD</div><div><div style="font-size:16px;font-weight:700;color:var(--cr-ink);">Jane Doe, MD</div><div style="font-size:12.5px;color:var(--navy-55);line-height:1.45;margin-top:3px;">Credential verified by CertREV</div><div style="font-size:12.5px;color:var(--navy-55);line-height:1.35;">on Apr 14, 2026</div></div></div><p style="font-size:14px;line-height:1.55;color:var(--cr-ink-soft);margin:16px 0 0;">Board-certified dermatologist in San Francisco specializing in photoprotection, skin-cancer prevention, and evidence-based skincare, with fifteen years of clinical practice.</p><blockquote style="margin:16px 0 0;padding-left:16px;border-left:3px solid var(--ba,#0a1b3f);font-size:15px;line-height:1.5;color:var(--cr-ink-soft);white-space:pre-line;">Patients ask me almost every week whether the SPF in their foundation is enough on its own. Treat makeup with SPF as a helpful last layer, not the base of your routine.</blockquote><div style="font-size:12.5px;line-height:1.5;color:var(--navy-55);margin-top:16px;"><span style="font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;">Compensated expert</span> · Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.</div></div><div style="display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--navy-10);"><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;border-right:1px solid var(--navy-10);cursor:pointer;">View Profile</a><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;cursor:pointer;">View Certificate</a></div></div></div>"`;
14
-
15
- exports[`renderCertBlock — locked-design snapshots > sidebar (themed, non-navy accent) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--sidebar" data-certrev-mode="sidebar" data-certrev-strings-version="2026-07" style="--ba:#E8603C;--ba-fg:#141414;--br:22px;--cr-bf:var(--bf,'Plus Jakarta Sans', system-ui, Helvetica, Arial, sans-serif);--surface:#faf8f4;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:rgba(10,27,63,.55);--cr-ink-soft:rgba(10,27,63,.75);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div style="background:var(--surface);border:1px solid var(--navy-10);border-radius:var(--br,14px);overflow:hidden;box-shadow:0 4px 18px var(--navy-06);font-family:var(--cr-bf);"><div style="display:flex;align-items:center;justify-content:center;gap:10px;padding:12px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;"><svg viewBox="0 0 375 375" width="18" height="18" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg> Expert reviewed</div><div style="padding:20px;"><div style="display:flex;align-items:flex-start;gap:13px;"><div style="width:46px;height:46px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:13px;flex-shrink:0;">JD</div><div><div style="font-size:16px;font-weight:700;color:var(--cr-ink);">Jane Doe, MD</div><div style="font-size:12.5px;color:var(--navy-55);line-height:1.45;margin-top:3px;">Credential verified by CertREV</div><div style="font-size:12.5px;color:var(--navy-55);line-height:1.35;">on Apr 14, 2026</div></div></div><p style="font-size:14px;line-height:1.55;color:var(--cr-ink-soft);margin:16px 0 0;">Board-certified dermatologist in San Francisco specializing in photoprotection, skin-cancer prevention, and evidence-based skincare, with fifteen years of clinical practice.</p><blockquote style="margin:16px 0 0;padding-left:16px;border-left:3px solid var(--ba,#0a1b3f);font-size:15px;line-height:1.5;color:var(--cr-ink-soft);white-space:pre-line;">Patients ask me almost every week whether the SPF in their foundation is enough on its own. Treat makeup with SPF as a helpful last layer, not the base of your routine.</blockquote><div style="font-size:12.5px;line-height:1.5;color:var(--navy-55);margin-top:16px;"><span style="font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;">Compensated expert</span> · Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.</div></div><div style="display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--navy-10);"><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;border-right:1px solid var(--navy-10);cursor:pointer;">View Profile</a><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;cursor:pointer;">View Certificate</a></div></div></div>"`;
16
-
17
- exports[`renderCertBlock — locked-design snapshots > sidebar (v2 brand-ink: barInk + inkColor) matches the locked snapshot 1`] = `"<div class="certrev-cert certrev-cert--sidebar" data-certrev-mode="sidebar" data-certrev-strings-version="2026-07" style="--ba:#E8603C;--ba-fg:#141414;--br:22px;--cr-bf:var(--bf,'Plus Jakarta Sans', system-ui, Helvetica, Arial, sans-serif);--surface:#faf8f4;--certrev-bar-bg:#000000;--certrev-bar-fg:#ffffff;--navy:#0a1b3f;--navy-06:rgba(10,27,63,.06);--navy-10:rgba(10,27,63,.10);--navy-35:rgba(10,27,63,.35);--navy-55:rgba(10,27,63,.55);--navy-75:rgba(10,27,63,.75);--stone:#ece9e1;--certrev-ink:#1a1a1a;--cr-ink:var(--certrev-ink,#0a1b3f);--cr-ink-sub:color-mix(in srgb,var(--cr-ink) 55%,transparent);--cr-ink-soft:color-mix(in srgb,var(--cr-ink) 75%,transparent);--font-mono:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;font-family:var(--cr-bf);color:var(--navy)"><div style="background:var(--surface);border:1px solid var(--navy-10);border-radius:var(--br,14px);overflow:hidden;box-shadow:0 4px 18px var(--navy-06);font-family:var(--cr-bf);"><div style="display:flex;align-items:center;justify-content:center;gap:10px;padding:12px;background:var(--certrev-bar-bg,#0a1b3f);color:var(--certrev-bar-fg,#fff);font-family:var(--font-mono);font-size:12px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;"><svg viewBox="0 0 375 375" width="18" height="18" style="flex-shrink:0" aria-label="CertREV"><path fill="currentColor" d="M67.59 221.77L138.77 292.95C145.8 299.98 145.8 311.44 138.77 318.47L123.02 334.22C116 341.25 104.54 341.25 97.5 334.22L26.32 263.04C19.29 256.01 19.29 244.55 26.32 237.52L42.07 221.77C49.1 214.74 60.56 214.74 67.59 221.77Z"></path><path fill="currentColor" d="M96.79 335.08L81.12 319.42C73.8 312.1 73.8 300.23 81.12 292.92L315.88 58.16C323.2 50.85 335.06 50.85 342.38 58.16L358.04 73.83C365.36 81.14 365.36 93.01 358.04 100.33L123.29 335.08C115.97 342.4 104.1 342.4 96.79 335.08Z"></path><path fill="currentColor" d="M203.5 335.14L187.83 319.48C180.52 312.16 180.52 300.29 187.83 292.98L315.88 164.93C323.2 157.62 335.06 157.62 342.38 164.93L358.04 180.6C365.36 187.92 365.36 199.78 358.04 207.1L230 335.14C222.68 342.46 210.82 342.46 203.5 335.14Z"></path><path fill="currentColor" d="M310.21 335.2L294.54 319.54C287.23 312.22 287.23 300.36 294.54 293.04L315.88 271.71C323.2 264.39 335.06 264.39 342.38 271.71L358.04 287.37C365.36 294.69 365.36 306.55 358.04 313.87L336.71 335.2C329.39 342.52 317.53 342.52 310.21 335.2Z"></path></svg> Expert reviewed</div><div style="padding:20px;"><div style="display:flex;align-items:flex-start;gap:13px;"><div style="width:46px;height:46px;border-radius:50%;background:var(--stone);color:var(--cr-ink-sub);display:flex;align-items:center;justify-content:center;font-family:var(--font-mono);font-weight:700;font-size:13px;flex-shrink:0;">JD</div><div><div style="font-size:16px;font-weight:700;color:var(--cr-ink);">Jane Doe, MD</div><div style="font-size:12.5px;color:var(--navy-55);line-height:1.45;margin-top:3px;">Credential verified by CertREV</div><div style="font-size:12.5px;color:var(--navy-55);line-height:1.35;">on Apr 14, 2026</div></div></div><p style="font-size:14px;line-height:1.55;color:var(--cr-ink-soft);margin:16px 0 0;">Board-certified dermatologist in San Francisco specializing in photoprotection, skin-cancer prevention, and evidence-based skincare, with fifteen years of clinical practice.</p><blockquote style="margin:16px 0 0;padding-left:16px;border-left:3px solid var(--ba,#0a1b3f);font-size:15px;line-height:1.5;color:var(--cr-ink-soft);white-space:pre-line;">Patients ask me almost every week whether the SPF in their foundation is enough on its own. Treat makeup with SPF as a helpful last layer, not the base of your routine.</blockquote><div style="font-size:12.5px;line-height:1.5;color:var(--navy-55);margin-top:16px;"><span style="font-family:var(--font-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;">Compensated expert</span> · Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.</div></div><div style="display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--navy-10);"><a href="https://certrev.com/expert/sample" data-certrev-modal-open="expert" rel="noopener" style="text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;border-right:1px solid var(--navy-10);cursor:pointer;">View Profile</a><a href="https://certrev.com/verify/sample" data-certrev-modal-open rel="noopener" style="text-align:center;padding:14px;font-size:14px;font-weight:700;color:var(--navy);text-decoration:none;cursor:pointer;">View Certificate</a></div></div></div>"`;
@@ -1,206 +0,0 @@
1
- /**
2
- * SSR-render tests for the React components, exercised through `react-dom/server`'s
3
- * `renderToStaticMarkup` — the SAME path Hydrogen / Next use to produce the crawlable
4
- * HTML. Rendering server-side here proves three contract properties at once:
5
- * • SSR-safe: a component that touched `useState`/`useEffect`/`window` would throw under
6
- * `renderToStaticMarkup`; passing means the components are render-pure.
7
- * • Escaping: React escapes text + attributes, and our `safeHttpUrl` strips dangerous
8
- * schemes — both asserted against hostile mock facts.
9
- * • Fail-closed: a suppressed verdict / unsafe URL renders nothing.
10
- *
11
- * Facts are mocked via `makeMockPayload` (no crypto, no network — pure render under test).
12
- */
13
-
14
- import { renderToStaticMarkup } from 'react-dom/server'
15
- import { describe, expect, it } from 'vitest'
16
- import { CertBadge } from '../components/CertBadge.js'
17
- import { CertJsonLd } from '../components/CertJsonLd.js'
18
- import { CertRevBacklink } from '../components/CertRevBacklink.js'
19
- import { CertReview } from '../components/CertReview.js'
20
- import { ExpertBio } from '../components/ExpertBio.js'
21
- import { makeMockPayload } from '../contract/fixtures.js'
22
-
23
- const payload = makeMockPayload()
24
-
25
- describe('<CertBadge>', () => {
26
- it('renders the expert name, credentials, certified date, memo, and verify link (full style)', () => {
27
- const html = renderToStaticMarkup(<CertBadge payload={payload} />)
28
- expect(html).toContain('Dr. Jane Doe')
29
- expect(html).toContain('MD, FAAD')
30
- expect(html).toContain('Jun 21, 2026') // certifiedAt, deterministic UTC formatting
31
- expect(html).toContain('retinol claims') // memo
32
- expect(html).toContain('Verify on CertREV')
33
- expect(html).toContain('href="https://certrev.com/verify/cert_fixture_001"')
34
- })
35
-
36
- it('carries an accessible name + the cert id data attribute', () => {
37
- const html = renderToStaticMarkup(<CertBadge payload={payload} />)
38
- expect(html).toContain('aria-label="Content reviewed by Dr. Jane Doe, MD, FAAD"')
39
- expect(html).toContain('data-certrev-cert-id="cert_fixture_001"')
40
- expect(html).toMatch(/^<section/)
41
- })
42
-
43
- it('applies the signed accent color as a CSS custom property + accent border', () => {
44
- const html = renderToStaticMarkup(<CertBadge payload={payload} />)
45
- expect(html).toContain('--certrev-accent:#7c3aed')
46
- expect(html).toContain('border-inline-start-color:#7c3aed')
47
- })
48
-
49
- it('compact style collapses the reviewer block — no memo, no dates, no photo', () => {
50
- const html = renderToStaticMarkup(<CertBadge payload={payload} badgeStyle="compact" />)
51
- expect(html).toContain('certrev-badge--compact')
52
- expect(html).not.toContain('retinol claims')
53
- expect(html).not.toContain('Certified')
54
- expect(html).not.toContain('__photo')
55
- // …but the compliance disclosures persist: a compact claim badge is still a claim face.
56
- expect(html).toContain(
57
- 'Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.',
58
- )
59
- })
60
-
61
- it('carries the always-on compliance baseline: cue + verbatim FTC scope line + strings-version stamp', () => {
62
- const html = renderToStaticMarkup(<CertBadge payload={payload} />)
63
- expect(html).toContain('data-certrev-strings-version="2026-07"')
64
- expect(html).toContain(
65
- 'Independent editorial review of this article’s text and sources. Not a product endorsement, nor medical advice.',
66
- )
67
- expect(html).toContain('data-ftc-line')
68
- // "Compensated expert" (tag-stripped — the cue links "expert" to the reviewer profile).
69
- expect(html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ')).toContain('Compensated expert')
70
- })
71
-
72
- it('honors display flags: showMemo=false hides the memo; showExpertPhoto=false hides the photo', () => {
73
- const p = makeMockPayload({
74
- content: { ...payload.content, display: { ...payload.content.display, showMemo: false, showExpertPhoto: false } },
75
- })
76
- const html = renderToStaticMarkup(<CertBadge payload={p} />)
77
- expect(html).not.toContain('retinol claims')
78
- expect(html).not.toContain('__photo')
79
- })
80
-
81
- it('ESCAPES a hostile display name (React text-escaping; no raw HTML injected)', () => {
82
- const p = makeMockPayload({
83
- content: {
84
- ...payload.content,
85
- expert: { ...payload.content.expert, displayName: '<img src=x onerror=alert(1)>' },
86
- memo: null,
87
- },
88
- })
89
- const html = renderToStaticMarkup(<CertBadge payload={p} />)
90
- expect(html).not.toContain('<img src=x onerror=alert(1)>')
91
- expect(html).toContain('&lt;img src=x onerror=alert(1)&gt;')
92
- })
93
-
94
- it('DROPS a javascript: profile URL (safeHttpUrl) — the name renders unlinked, no href', () => {
95
- const p = makeMockPayload({
96
- content: {
97
- ...payload.content,
98
- // biome-ignore lint/suspicious/noTemplateCurlyInString: literal javascript: scheme string for the XSS-drop assertion
99
- expert: { ...payload.content.expert, profileUrl: 'javascript:alert(document.cookie)' },
100
- },
101
- })
102
- const html = renderToStaticMarkup(<CertBadge payload={p} />)
103
- expect(html).not.toContain('javascript:')
104
- expect(html).not.toContain('__expert-link') // no anchor when the URL is unsafe
105
- expect(html).toContain('Dr. Jane Doe') // name still rendered
106
- })
107
-
108
- it('renders the author byline only when the author is present + distinct from the expert', () => {
109
- const distinct = renderToStaticMarkup(<CertBadge payload={payload} />)
110
- expect(distinct).toContain('Written by Sam Writer, Senior Content Editor')
111
-
112
- const same = makeMockPayload({
113
- content: { ...payload.content, author: { name: 'Dr. Jane Doe', title: null } },
114
- })
115
- expect(renderToStaticMarkup(<CertBadge payload={same} />)).not.toContain('Written by')
116
- })
117
- })
118
-
119
- describe('<ExpertBio>', () => {
120
- it('renders the expert identity block with credentials expanded (abbr + full name)', () => {
121
- const html = renderToStaticMarkup(<ExpertBio payload={payload} />)
122
- expect(html).toMatch(/^<aside/)
123
- expect(html).toContain('Dr. Jane Doe')
124
- expect(html).toContain('<abbr title="Doctor of Medicine">MD</abbr>')
125
- expect(html).toContain('Fellow of the American Academy of Dermatology')
126
- expect(html).toContain('Verified expert reviewer')
127
- })
128
-
129
- it('respects the heading level prop for page-outline slotting', () => {
130
- expect(renderToStaticMarkup(<ExpertBio payload={payload} headingLevel="h2" />)).toContain('<h2')
131
- expect(renderToStaticMarkup(<ExpertBio payload={payload} />)).toContain('<h3') // default
132
- })
133
-
134
- it('omits the photo when showExpertPhoto is false', () => {
135
- const p = makeMockPayload({
136
- content: { ...payload.content, display: { ...payload.content.display, showExpertPhoto: false } },
137
- })
138
- expect(renderToStaticMarkup(<ExpertBio payload={p} />)).not.toContain('__photo')
139
- })
140
- })
141
-
142
- describe('<CertRevBacklink>', () => {
143
- it('renders the live verify link with an accessible label', () => {
144
- const html = renderToStaticMarkup(<CertRevBacklink payload={payload} />)
145
- expect(html).toMatch(/^<a/)
146
- expect(html).toContain('href="https://certrev.com/verify/cert_fixture_001"')
147
- expect(html).toContain('aria-label="Verify this certification on CertREV"')
148
- expect(html).toContain('Verify on CertREV')
149
- })
150
-
151
- it('accepts a custom label', () => {
152
- expect(renderToStaticMarkup(<CertRevBacklink payload={payload} label="Check the certificate" />)).toContain(
153
- 'Check the certificate',
154
- )
155
- })
156
-
157
- it('FAIL-CLOSED: renders nothing when the verify URL is unsafe/absent', () => {
158
- const p = makeMockPayload({ content: { ...payload.content, verifyUrl: 'javascript:alert(1)' } })
159
- expect(renderToStaticMarkup(<CertRevBacklink payload={p} />)).toBe('')
160
- })
161
- })
162
-
163
- describe('<CertJsonLd>', () => {
164
- it('emits an application/ld+json <script> with the projected graph', () => {
165
- const html = renderToStaticMarkup(<CertJsonLd payload={payload} />)
166
- expect(html).toContain('<script type="application/ld+json">')
167
- expect(html).toContain('"@type":"Article"')
168
- expect(html).toContain('"@type":"Review"')
169
- expect(html).toContain('"reviewedBy"')
170
- expect(html).toContain('Dr. Jane Doe')
171
- })
172
-
173
- it('neutralizes < in the serialized body so a hostile memo cannot break out of the tag', () => {
174
- const p = makeMockPayload({
175
- content: { ...payload.content, memo: 'safe </script><script>alert(1)</script>' },
176
- })
177
- const html = renderToStaticMarkup(<CertJsonLd payload={p} />)
178
- // The only literal </script> in the output is the script CLOSE tag, not from the memo.
179
- expect(html.match(/<\/script>/g)?.length).toBe(1)
180
- expect(html).toContain('\\u003c/script')
181
- })
182
-
183
- it('threads pageUrl into the Article @id for host-graph merge', () => {
184
- const html = renderToStaticMarkup(<CertJsonLd payload={payload} pageUrl="https://shop.example.com/a/b" />)
185
- expect(html).toContain('"@id":"https://shop.example.com/a/b#article"')
186
- })
187
- })
188
-
189
- describe('<CertReview> (composite, fail-closed at the component boundary)', () => {
190
- it('renders badge + JSON-LD on a render verdict', () => {
191
- const html = renderToStaticMarkup(<CertReview verdict={{ decision: 'render', payload }} />)
192
- expect(html).toContain('certrev-badge')
193
- expect(html).toContain('application/ld+json')
194
- })
195
-
196
- it('renders NOTHING on a suppress verdict', () => {
197
- const html = renderToStaticMarkup(<CertReview verdict={{ decision: 'suppress', reason: 'revoked' }} />)
198
- expect(html).toBe('')
199
- })
200
-
201
- it('omitJsonLd drops the structured-data script but keeps the badge', () => {
202
- const html = renderToStaticMarkup(<CertReview verdict={{ decision: 'render', payload }} omitJsonLd />)
203
- expect(html).toContain('certrev-badge')
204
- expect(html).not.toContain('application/ld+json')
205
- })
206
- })
@@ -1,84 +0,0 @@
1
- /**
2
- * renderPreviewBlock (POR-10743) — a static, non-delivery, hard-bounded pre-sales preview.
3
- * Asserts: pixel-parity with renderCertBlock, the required expiry bound, the machine markers,
4
- * and the safe-by-default visible label.
5
- */
6
-
7
- import { describe, expect, it } from 'vitest'
8
- import { previewExpiry, renderPreviewBlock } from '../components/preview-block.js'
9
- import type { CertBlockFacts } from '../components/render-cert-block.js'
10
- import { renderCertBlock } from '../components/render-cert-block.js'
11
-
12
- const FACTS: CertBlockFacts = {
13
- authorName: 'Editorial',
14
- reviewerName: 'Dr. Jane Doe',
15
- credential: 'MD',
16
- credentialVerifiedAt: '2026-06-20T00:00:00.000Z',
17
- certifiedAt: '2026-06-20T00:00:00.000Z',
18
- memo: 'Reviewed the claims against current guidance; accurate and safely framed.',
19
- profileUrl: 'https://certrev.com/experts/jane',
20
- certificateUrl: 'https://certrev.com/verify/cert_x',
21
- }
22
-
23
- const EXPIRES = '2026-08-01T00:00:00.000Z'
24
- const input = { mode: 'banner', facts: FACTS } as const
25
-
26
- describe('renderPreviewBlock', () => {
27
- it('REQUIRES expiresAt — a preview must be time-bounded', () => {
28
- expect(() => renderPreviewBlock(input, { expiresAt: '' })).toThrow(/time-bounded/)
29
- expect(() => renderPreviewBlock(input, { expiresAt: ' ' })).toThrow(/time-bounded/)
30
- })
31
-
32
- it('REJECTS an unparseable expiry — the bound must be a real date, not a toothless string', () => {
33
- expect(() => renderPreviewBlock(input, { expiresAt: 'banana' })).toThrow(/parseable date/)
34
- expect(() => renderPreviewBlock(input, { expiresAt: 'someday' })).toThrow(/parseable date/)
35
- // a valid ISO date is accepted
36
- expect(() => renderPreviewBlock(input, { expiresAt: EXPIRES })).not.toThrow()
37
- })
38
-
39
- it('is PIXEL-PARITY — the inner visual is byte-identical to renderCertBlock', () => {
40
- const preview = renderPreviewBlock(input, { expiresAt: EXPIRES, showLabel: false })
41
- expect(preview).toContain(renderCertBlock(input))
42
- })
43
-
44
- it('carries the non-delivery machine markers + the required expiry', () => {
45
- const html = renderPreviewBlock(input, { expiresAt: EXPIRES })
46
- expect(html).toContain('data-certrev-preview="1"')
47
- expect(html).toContain(`data-certrev-preview-expires="${EXPIRES}"`)
48
- // a leading comment banner spells out the non-delivery contract
49
- expect(html).toContain('<!-- CertREV PREVIEW')
50
- expect(html).toContain('Never use as delivery')
51
- })
52
-
53
- it('shows the safe-default visible label; suppressible for a pixel-parity look-demo', () => {
54
- const labeled = renderPreviewBlock(input, { expiresAt: EXPIRES })
55
- expect(labeled).toContain('data-certrev-preview-label')
56
- expect(labeled).toContain('Preview — not a verified certification')
57
-
58
- const bare = renderPreviewBlock(input, { expiresAt: EXPIRES, showLabel: false })
59
- expect(bare).not.toContain('data-certrev-preview-label')
60
- // even bare, the machine marker + expiry remain (never suppressible)
61
- expect(bare).toContain('data-certrev-preview="1"')
62
- expect(bare).toContain(`data-certrev-preview-expires="${EXPIRES}"`)
63
- })
64
-
65
- it('honors a custom label', () => {
66
- const html = renderPreviewBlock(input, { expiresAt: EXPIRES, labelText: 'Sample only' })
67
- expect(html).toContain('Sample only')
68
- })
69
-
70
- it('ESCAPES the caller-supplied label — a malicious labelText cannot inject markup', () => {
71
- const html = renderPreviewBlock(input, {
72
- expiresAt: EXPIRES,
73
- labelText: '<img src=x onerror=alert(1)>',
74
- })
75
- expect(html).not.toContain('<img src=x onerror=alert(1)>')
76
- expect(html).toContain('&lt;img src=x onerror=alert(1)&gt;')
77
- })
78
-
79
- it('previewExpiry extracts the baked expiry, and is null for a non-preview', () => {
80
- const html = renderPreviewBlock(input, { expiresAt: EXPIRES })
81
- expect(previewExpiry(html)).toBe(EXPIRES)
82
- expect(previewExpiry(renderCertBlock(input))).toBeNull()
83
- })
84
- })
@@ -1,128 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { makeMockPayload } from '../contract/fixtures.js'
3
- import {
4
- type JsonLdValue,
5
- projectCertJsonLd,
6
- projectCertJsonLdString,
7
- serializeJsonLdForScript,
8
- } from '../jsonld/project.js'
9
-
10
- function graphOf(value: JsonLdValue | JsonLdValue[]): JsonLdValue[] {
11
- if (Array.isArray(value)) return value
12
- return value['@graph'] as JsonLdValue[]
13
- }
14
- function nodeOfType(value: JsonLdValue | JsonLdValue[], type: string): JsonLdValue {
15
- const n = graphOf(value).find((x) => x['@type'] === type)
16
- if (!n) throw new Error(`no ${type} node`)
17
- return n
18
- }
19
-
20
- describe('projectCertJsonLd', () => {
21
- it('emits a wrapped @graph with Article, Review, Person(expert), Organization', () => {
22
- const g = projectCertJsonLd(makeMockPayload())
23
- expect(Array.isArray(g)).toBe(false)
24
- const graph = graphOf(g)
25
- const types = graph.map((n) => n['@type']).sort()
26
- expect(types).toEqual(['Article', 'Organization', 'Person', 'Review'])
27
- expect((g as JsonLdValue)['@context']).toBe('https://schema.org')
28
- })
29
-
30
- it('Article @id aligns to the page URL (#article) so it MERGES into the host/Yoast node', () => {
31
- const g = projectCertJsonLd(makeMockPayload(), {
32
- pageUrl: 'https://brand.example.com/blogs/skincare/retinol-guide?utm=x',
33
- })
34
- const article = nodeOfType(g, 'Article')
35
- // Fragment + query stripped, #article appended — the Yoast convention.
36
- expect(article['@id']).toBe('https://brand.example.com/blogs/skincare/retinol-guide#article')
37
- })
38
-
39
- it('falls back to subject.canonicalUrls[0] for the Article @id when no pageUrl given', () => {
40
- const article = nodeOfType(projectCertJsonLd(makeMockPayload()), 'Article')
41
- expect(article['@id']).toBe('https://brand.example.com/blogs/skincare/retinol-guide#article')
42
- })
43
-
44
- it('attaches the EXPERT as reviewedBy (the E-E-A-T signal), not as author', () => {
45
- const g = projectCertJsonLd(makeMockPayload())
46
- const article = nodeOfType(g, 'Article')
47
- const person = nodeOfType(g, 'Person')
48
- expect(person.name).toBe('Dr. Jane Doe')
49
- expect((article.reviewedBy as JsonLdValue)['@id']).toBe(person['@id'])
50
- // Distinct author present + attached separately.
51
- expect((article.author as JsonLdValue).name).toBe('Sam Writer')
52
- })
53
-
54
- it('projects credentials as hasCredential + honorificSuffix', () => {
55
- const person = nodeOfType(projectCertJsonLd(makeMockPayload()), 'Person')
56
- expect(person.honorificSuffix).toBe('MD, FAAD')
57
- expect((person.hasCredential as JsonLdValue[]).map((c) => c.alternateName)).toEqual(['MD', 'FAAD'])
58
- })
59
-
60
- it('Review node binds itemReviewed→Article, author→expert, publisher→CertREV org', () => {
61
- const g = projectCertJsonLd(makeMockPayload())
62
- const review = nodeOfType(g, 'Review')
63
- const article = nodeOfType(g, 'Article')
64
- const person = nodeOfType(g, 'Person')
65
- const org = nodeOfType(g, 'Organization')
66
- expect((review.itemReviewed as JsonLdValue)['@id']).toBe(article['@id'])
67
- expect((review.author as JsonLdValue)['@id']).toBe(person['@id'])
68
- expect((review.publisher as JsonLdValue)['@id']).toBe(org['@id'])
69
- expect(review.reviewBody).toContain('retinol claims')
70
- })
71
-
72
- it('CertREV node @ids are namespaced under certrev.com so they cannot collide with host nodes', () => {
73
- const g = projectCertJsonLd(makeMockPayload())
74
- const person = nodeOfType(g, 'Person')
75
- const org = nodeOfType(g, 'Organization')
76
- const review = nodeOfType(g, 'Review')
77
- for (const id of [person['@id'], org['@id'], review['@id']]) {
78
- expect(String(id).startsWith('https://certrev.com/')).toBe(true)
79
- }
80
- })
81
-
82
- it('omits dateModified when contentModifiedAt is null', () => {
83
- const article = nodeOfType(
84
- projectCertJsonLd(makeMockPayload({ content: { ...makeMockPayload().content, contentModifiedAt: null } })),
85
- 'Article',
86
- )
87
- expect('dateModified' in article).toBe(false)
88
- })
89
-
90
- it('does not double-attribute when author === expert', () => {
91
- const p = makeMockPayload()
92
- const sameAuthor = makeMockPayload({
93
- content: { ...p.content, author: { name: p.content.expert.displayName, title: null } },
94
- })
95
- const article = nodeOfType(projectCertJsonLd(sameAuthor), 'Article')
96
- expect('author' in article).toBe(false)
97
- })
98
-
99
- it('is DETERMINISTIC — same facts → byte-identical serialization', () => {
100
- const a = projectCertJsonLdString(makeMockPayload(), { pageUrl: 'https://x.com/p' })
101
- const b = projectCertJsonLdString(makeMockPayload(), { pageUrl: 'https://x.com/p' })
102
- expect(a).toBe(b)
103
- })
104
-
105
- it('wrapGraph:false returns a bare node array for merge-into-existing-graph callers', () => {
106
- const arr = projectCertJsonLd(makeMockPayload(), { wrapGraph: false })
107
- expect(Array.isArray(arr)).toBe(true)
108
- })
109
- })
110
-
111
- describe('serializeJsonLdForScript', () => {
112
- it('neutralizes </script> so the body cannot break out of the <script> tag', () => {
113
- const evilPayload = makeMockPayload({
114
- content: { ...makeMockPayload().content, memo: 'totally safe </script><script>alert(1)</script>' },
115
- })
116
- const out = projectCertJsonLdString(evilPayload)
117
- expect(out).not.toContain('</script>')
118
- expect(out).toContain('\\u003c/script')
119
- // Still valid JSON that round-trips to the original string.
120
- const parsed = JSON.parse(out.replace(/\\u003c/g, '<'))
121
- const review = (parsed['@graph'] as JsonLdValue[]).find((n) => n['@type'] === 'Review')
122
- expect(review?.reviewBody).toContain('</script>')
123
- })
124
-
125
- it('escapes every < (HTML-comment opener defense)', () => {
126
- expect(serializeJsonLdForScript({ a: '<!-- x -->' })).toBe('{"a":"\\u003c!-- x --\\u003e"}')
127
- })
128
- })
@@ -1,51 +0,0 @@
1
- import { readFileSync } from 'node:fs'
2
- import { describe, expect, it } from 'vitest'
3
-
4
- /**
5
- * Both public packages open a `## Version` section with a bare version stamp, and both ship
6
- * README.md inside the published tarball (`files`) — so the stamp is what a consumer reads
7
- * after `npm i`, not just what a browser shows on GitHub.
8
- *
9
- * It has now drifted twice, in both packages:
10
- * - cert-contract 0.5.1 was a docs-only release whose entire purpose was unsticking this
11
- * stamp ("README Version section brought current (0.4.0 / 0.5.0 were missing)").
12
- * - cert-block 0.5.4 bumped package.json and touched nothing else, so the published 0.5.4
13
- * tarball ships a README claiming to be 0.5.3. That is live on npm right now.
14
- *
15
- * Nothing caught either one, because nothing was looking: the stamp is prose, and prose has
16
- * no compiler. A release step nobody can forget beats a release step written down somewhere.
17
- * This is that step.
18
- *
19
- * The two packages word the line differently ("`x.y.z` — see" vs "Current: `x.y.z` — see"),
20
- * so the pattern below matches the first backticked token after the heading rather than a
21
- * fixed sentence. If a README reshapes that section, update the pattern — do not delete the
22
- * assertion; the drift it catches has shipped to npm twice.
23
- */
24
- const PACKAGES = ['cert-block', 'cert-contract'] as const
25
-
26
- describe.each(PACKAGES)('%s README version stamp', (pkgName) => {
27
- const pkgUrl = new URL(`../../../${pkgName}/package.json`, import.meta.url)
28
- const readmeUrl = new URL(`../../../${pkgName}/README.md`, import.meta.url)
29
-
30
- const { version } = JSON.parse(readFileSync(pkgUrl, 'utf8')) as { version: string }
31
- const readme = readFileSync(readmeUrl, 'utf8')
32
-
33
- it('matches package.json — bump both, or neither', () => {
34
- // First backticked token after the `## Version` heading.
35
- const stamp = readme.match(/^## Version\s*\n+[^`\n]*`([^`]+)`/m)
36
-
37
- expect(
38
- stamp,
39
- `${pkgName}: README has no \`## Version\` section opening with a backticked stamp. If the ` +
40
- 'section was renamed or reshaped, update this pattern to match the new shape rather ' +
41
- 'than deleting the assertion.',
42
- ).not.toBeNull()
43
-
44
- expect(
45
- stamp?.[1],
46
- `${pkgName}: README's \`## Version\` stamp is ${stamp?.[1]} but package.json is ${version}. ` +
47
- 'README.md ships inside the npm tarball, so this mismatch gets published to consumers. ' +
48
- `Bump the stamp in packages/${pkgName}/README.md.`,
49
- ).toBe(version)
50
- })
51
- })