@windstream/react-shared-components 0.1.94 → 0.1.95

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 (297) hide show
  1. package/README.md +635 -635
  2. package/dist/contentful/index.esm.js +3 -3
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +3 -3
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/core.d.ts +4 -4
  7. package/dist/index.esm.js +5 -13
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/index.js +5 -13
  10. package/dist/index.js.map +1 -1
  11. package/dist/next/index.esm.js +2 -2
  12. package/dist/next/index.esm.js.map +1 -1
  13. package/dist/next/index.js +2 -2
  14. package/dist/next/index.js.map +1 -1
  15. package/dist/styles.css +1 -1
  16. package/dist/utils/index.esm.js +1 -1
  17. package/dist/utils/index.esm.js.map +1 -1
  18. package/dist/utils/index.js +1 -1
  19. package/dist/utils/index.js.map +1 -1
  20. package/package.json +191 -191
  21. package/src/components/accordion/Accordion.stories.tsx +230 -230
  22. package/src/components/accordion/index.test.tsx +270 -270
  23. package/src/components/accordion/index.tsx +70 -70
  24. package/src/components/accordion/types.ts +12 -12
  25. package/src/components/alert-card/AlertCard.stories.tsx +171 -171
  26. package/src/components/alert-card/index.test.tsx +152 -152
  27. package/src/components/alert-card/index.tsx +41 -41
  28. package/src/components/alert-card/types.ts +13 -13
  29. package/src/components/animation-wrapper/index.test.tsx +424 -424
  30. package/src/components/animation-wrapper/index.tsx +129 -129
  31. package/src/components/animation-wrapper/types.ts +11 -11
  32. package/src/components/brand-button/BrandButton.stories.tsx +223 -223
  33. package/src/components/brand-button/helpers.ts +35 -35
  34. package/src/components/brand-button/index.test.tsx +292 -292
  35. package/src/components/brand-button/index.tsx +120 -120
  36. package/src/components/brand-button/types.ts +38 -38
  37. package/src/components/button/Button.stories.tsx +108 -108
  38. package/src/components/button/index.test.tsx +91 -91
  39. package/src/components/button/index.tsx +27 -27
  40. package/src/components/button/types.ts +14 -14
  41. package/src/components/call-button/CallButton.stories.tsx +324 -324
  42. package/src/components/call-button/index.test.tsx +260 -260
  43. package/src/components/call-button/index.tsx +106 -106
  44. package/src/components/call-button/types.ts +16 -16
  45. package/src/components/checkbox/Checkbox.stories.tsx +247 -247
  46. package/src/components/checkbox/index.test.tsx +252 -252
  47. package/src/components/checkbox/index.tsx +197 -197
  48. package/src/components/checkbox/types.ts +27 -27
  49. package/src/components/checklist/Checklist.stories.tsx +150 -150
  50. package/src/components/checklist/index.test.tsx +231 -231
  51. package/src/components/checklist/index.tsx +96 -96
  52. package/src/components/checklist/types.ts +23 -23
  53. package/src/components/collapse/Collapse.stories.tsx +255 -255
  54. package/src/components/collapse/index.test.tsx +277 -277
  55. package/src/components/collapse/index.tsx +47 -47
  56. package/src/components/collapse/types.ts +6 -6
  57. package/src/components/divider/Divider.stories.tsx +205 -205
  58. package/src/components/divider/index.test.tsx +53 -53
  59. package/src/components/divider/index.tsx +22 -22
  60. package/src/components/divider/type.ts +3 -3
  61. package/src/components/image/Image.stories.tsx +113 -113
  62. package/src/components/image/index.test.tsx +174 -174
  63. package/src/components/image/index.tsx +25 -25
  64. package/src/components/image/types.ts +40 -40
  65. package/src/components/input/Input.stories.tsx +325 -325
  66. package/src/components/input/index.test.tsx +348 -348
  67. package/src/components/input/index.tsx +177 -177
  68. package/src/components/input/types.ts +37 -37
  69. package/src/components/link/Link.stories.tsx +163 -163
  70. package/src/components/link/index.test.tsx +199 -199
  71. package/src/components/link/index.tsx +116 -116
  72. package/src/components/link/types.ts +25 -25
  73. package/src/components/list/List.stories.tsx +272 -272
  74. package/src/components/list/index.test.tsx +166 -166
  75. package/src/components/list/index.tsx +88 -88
  76. package/src/components/list/list-item/index.tsx +38 -38
  77. package/src/components/list/list-item/types.ts +13 -13
  78. package/src/components/list/types.ts +29 -29
  79. package/src/components/material-icon/MaterialIcon.stories.tsx +322 -322
  80. package/src/components/material-icon/constants.ts +99 -99
  81. package/src/components/material-icon/index.test.tsx +130 -130
  82. package/src/components/material-icon/index.tsx +47 -47
  83. package/src/components/material-icon/types.ts +31 -31
  84. package/src/components/modal/Modal.stories.tsx +171 -171
  85. package/src/components/modal/index.test.tsx +310 -310
  86. package/src/components/modal/index.tsx +164 -164
  87. package/src/components/modal/types.ts +24 -24
  88. package/src/components/next-image/index.test.tsx +406 -406
  89. package/src/components/next-image/index.tsx +74 -74
  90. package/src/components/next-image/types.ts +1 -1
  91. package/src/components/pagination/index.test.tsx +521 -521
  92. package/src/components/pagination/index.tsx +91 -91
  93. package/src/components/pagination/types.ts +6 -6
  94. package/src/components/radio-button/RadioButton.stories.tsx +307 -307
  95. package/src/components/radio-button/index.test.tsx +151 -151
  96. package/src/components/radio-button/index.tsx +75 -75
  97. package/src/components/radio-button/types.ts +21 -21
  98. package/src/components/see-more/SeeMore.stories.tsx +181 -181
  99. package/src/components/see-more/index.test.tsx +96 -96
  100. package/src/components/see-more/index.tsx +44 -44
  101. package/src/components/see-more/types.ts +4 -4
  102. package/src/components/select/Select.stories.tsx +411 -411
  103. package/src/components/select/index.test.tsx +256 -256
  104. package/src/components/select/index.tsx +155 -155
  105. package/src/components/select/types.ts +36 -36
  106. package/src/components/select-plan-button/SelectPlanButton.stories.tsx +184 -184
  107. package/src/components/select-plan-button/index.test.tsx +173 -173
  108. package/src/components/select-plan-button/index.tsx +63 -63
  109. package/src/components/select-plan-button/types.ts +17 -17
  110. package/src/components/skeleton/Skeleton.stories.tsx +179 -179
  111. package/src/components/skeleton/index.test.tsx +74 -74
  112. package/src/components/skeleton/index.tsx +61 -61
  113. package/src/components/skeleton/types.ts +4 -4
  114. package/src/components/spinner/Spinner.stories.tsx +335 -335
  115. package/src/components/spinner/index.test.tsx +76 -76
  116. package/src/components/spinner/index.tsx +44 -44
  117. package/src/components/spinner/types.ts +5 -5
  118. package/src/components/text/Text.stories.tsx +321 -321
  119. package/src/components/text/index.test.tsx +65 -65
  120. package/src/components/text/index.tsx +25 -25
  121. package/src/components/text/types.ts +45 -45
  122. package/src/components/tooltip/Tooltip.stories.tsx +219 -219
  123. package/src/components/tooltip/index.test.tsx +50 -50
  124. package/src/components/tooltip/index.tsx +74 -74
  125. package/src/components/tooltip/types.ts +7 -7
  126. package/src/components/view-cart-button/ViewCartButton.stories.tsx +252 -252
  127. package/src/components/view-cart-button/index.test.tsx +57 -57
  128. package/src/components/view-cart-button/index.tsx +42 -42
  129. package/src/components/view-cart-button/types.ts +5 -5
  130. package/src/contentful/blocks/accordion/Accordion.stories.mocks.tsx +128 -128
  131. package/src/contentful/blocks/accordion/Accordion.stories.tsx +98 -98
  132. package/src/contentful/blocks/accordion/index.test.tsx +218 -218
  133. package/src/contentful/blocks/accordion/index.tsx +114 -114
  134. package/src/contentful/blocks/accordion/types.ts +34 -34
  135. package/src/contentful/blocks/address-input-banner/index.test.tsx +132 -132
  136. package/src/contentful/blocks/address-input-banner/index.tsx +52 -52
  137. package/src/contentful/blocks/address-input-banner/types.ts +14 -14
  138. package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +287 -287
  139. package/src/contentful/blocks/anchored-bottom-banner/index.tsx +181 -181
  140. package/src/contentful/blocks/anchored-bottom-banner/types.ts +13 -13
  141. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.mocks.tsx +144 -144
  142. package/src/contentful/blocks/blogs-grid/BlogGrid.stories.tsx +157 -157
  143. package/src/contentful/blocks/blogs-grid/index.test.tsx +355 -355
  144. package/src/contentful/blocks/blogs-grid/index.tsx +134 -134
  145. package/src/contentful/blocks/blogs-grid/types.ts +26 -26
  146. package/src/contentful/blocks/blogs-grid-base/index.test.tsx +274 -274
  147. package/src/contentful/blocks/blogs-grid-base/index.tsx +119 -119
  148. package/src/contentful/blocks/blogs-grid-base/types.ts +36 -36
  149. package/src/contentful/blocks/breadcrumbs/BreadcrumbNavigation.stories.tsx +147 -147
  150. package/src/contentful/blocks/breadcrumbs/index.test.tsx +281 -281
  151. package/src/contentful/blocks/breadcrumbs/index.tsx +95 -95
  152. package/src/contentful/blocks/breadcrumbs/types.ts +8 -8
  153. package/src/contentful/blocks/button/Button.stories.tsx +40 -40
  154. package/src/contentful/blocks/button/index.test.tsx +339 -339
  155. package/src/contentful/blocks/button/index.tsx +131 -131
  156. package/src/contentful/blocks/button/types.ts +39 -39
  157. package/src/contentful/blocks/callout/Callout.stories.tsx +23 -23
  158. package/src/contentful/blocks/callout/index.test.tsx +539 -539
  159. package/src/contentful/blocks/callout/index.tsx +277 -277
  160. package/src/contentful/blocks/callout/types.ts +78 -78
  161. package/src/contentful/blocks/cards/Cards.stories.tsx +23 -23
  162. package/src/contentful/blocks/cards/blog-card/index.test.tsx +218 -218
  163. package/src/contentful/blocks/cards/blog-card/index.tsx +129 -129
  164. package/src/contentful/blocks/cards/blog-card/types.ts +34 -34
  165. package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +201 -201
  166. package/src/contentful/blocks/cards/floating-image-card/index.tsx +119 -119
  167. package/src/contentful/blocks/cards/floating-image-card/types.ts +30 -30
  168. package/src/contentful/blocks/cards/full-image-card/index.test.tsx +216 -216
  169. package/src/contentful/blocks/cards/full-image-card/index.tsx +130 -130
  170. package/src/contentful/blocks/cards/full-image-card/types.ts +29 -29
  171. package/src/contentful/blocks/cards/index.test.tsx +39 -39
  172. package/src/contentful/blocks/cards/index.tsx +13 -13
  173. package/src/contentful/blocks/cards/product-card/index.test.tsx +263 -263
  174. package/src/contentful/blocks/cards/product-card/index.tsx +251 -251
  175. package/src/contentful/blocks/cards/product-card/types.ts +28 -28
  176. package/src/contentful/blocks/cards/simple-card/index.test.tsx +364 -364
  177. package/src/contentful/blocks/cards/simple-card/index.tsx +325 -325
  178. package/src/contentful/blocks/cards/simple-card/types.ts +71 -71
  179. package/src/contentful/blocks/cards/testimonial-card/index.test.tsx +180 -180
  180. package/src/contentful/blocks/cards/testimonial-card/index.tsx +90 -90
  181. package/src/contentful/blocks/cards/testimonial-card/types.tsx +12 -12
  182. package/src/contentful/blocks/cards/types.ts +1 -1
  183. package/src/contentful/blocks/carousel/Carousel.stories.tsx +23 -23
  184. package/src/contentful/blocks/carousel/helper.test.tsx +539 -539
  185. package/src/contentful/blocks/carousel/helper.tsx +494 -494
  186. package/src/contentful/blocks/carousel/index.test.tsx +308 -308
  187. package/src/contentful/blocks/carousel/index.tsx +87 -87
  188. package/src/contentful/blocks/carousel/types.test.ts +16 -16
  189. package/src/contentful/blocks/carousel/types.ts +145 -145
  190. package/src/contentful/blocks/cart-retention-banner/index.test.tsx +409 -409
  191. package/src/contentful/blocks/cart-retention-banner/index.tsx +109 -109
  192. package/src/contentful/blocks/cart-retention-banner/types.ts +11 -11
  193. package/src/contentful/blocks/comparison-table/index.test.tsx +114 -114
  194. package/src/contentful/blocks/comparison-table/index.tsx +29 -29
  195. package/src/contentful/blocks/comparison-table/types.ts +6 -6
  196. package/src/contentful/blocks/cookiebanner/index.test.tsx +277 -277
  197. package/src/contentful/blocks/cookiebanner/index.tsx +146 -146
  198. package/src/contentful/blocks/cookiebanner/type.ts +7 -7
  199. package/src/contentful/blocks/cta-callout/CtaCallout.stories.tsx +46 -46
  200. package/src/contentful/blocks/cta-callout/index.test.tsx +244 -244
  201. package/src/contentful/blocks/cta-callout/index.tsx +73 -73
  202. package/src/contentful/blocks/cta-callout/types.ts +26 -26
  203. package/src/contentful/blocks/dynamic-tabs/index.test.tsx +240 -240
  204. package/src/contentful/blocks/dynamic-tabs/index.tsx +204 -204
  205. package/src/contentful/blocks/dynamic-tabs/types.ts +21 -21
  206. package/src/contentful/blocks/email-input-block/index.test.tsx +213 -213
  207. package/src/contentful/blocks/email-input-block/index.tsx +121 -121
  208. package/src/contentful/blocks/email-input-block/types.ts +16 -16
  209. package/src/contentful/blocks/find-kinetic/FindKinetic.stories.tsx +23 -23
  210. package/src/contentful/blocks/find-kinetic/index.test.tsx +269 -269
  211. package/src/contentful/blocks/find-kinetic/index.tsx +138 -138
  212. package/src/contentful/blocks/find-kinetic/types.ts +20 -20
  213. package/src/contentful/blocks/floating-banner/FloatingBanner.stories.tsx +34 -34
  214. package/src/contentful/blocks/floating-banner/index.test.tsx +246 -246
  215. package/src/contentful/blocks/floating-banner/index.tsx +97 -97
  216. package/src/contentful/blocks/floating-banner/types.ts +22 -22
  217. package/src/contentful/blocks/footer/Footer.stories.tsx +317 -317
  218. package/src/contentful/blocks/footer/index.test.tsx +302 -302
  219. package/src/contentful/blocks/footer/index.tsx +91 -91
  220. package/src/contentful/blocks/footer/types.ts +13 -13
  221. package/src/contentful/blocks/image-promo-bar/ImagePromoBar.stories.tsx +23 -23
  222. package/src/contentful/blocks/image-promo-bar/helper.test.tsx +61 -61
  223. package/src/contentful/blocks/image-promo-bar/helper.tsx +28 -28
  224. package/src/contentful/blocks/image-promo-bar/index.test.tsx +467 -467
  225. package/src/contentful/blocks/image-promo-bar/index.tsx +8 -6
  226. package/src/contentful/blocks/image-promo-bar/types.ts +44 -44
  227. package/src/contentful/blocks/image-promo-bar/vimeo-embed.test.tsx +142 -142
  228. package/src/contentful/blocks/image-promo-bar/vimeo-embed.tsx +93 -93
  229. package/src/contentful/blocks/image-promo-bar/youtube-embed.test.tsx +104 -104
  230. package/src/contentful/blocks/image-promo-bar/youtube-embed.tsx +46 -46
  231. package/src/contentful/blocks/modal/constants.ts +53 -53
  232. package/src/contentful/blocks/modal/index.test.tsx +209 -209
  233. package/src/contentful/blocks/modal/index.tsx +108 -108
  234. package/src/contentful/blocks/modal/types.ts +12 -12
  235. package/src/contentful/blocks/navigation/Navigation.stories.mocks.tsx +78 -78
  236. package/src/contentful/blocks/navigation/Navigation.stories.tsx +138 -138
  237. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +208 -208
  238. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +141 -141
  239. package/src/contentful/blocks/navigation/index.test.tsx +924 -924
  240. package/src/contentful/blocks/navigation/index.tsx +569 -569
  241. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +131 -131
  242. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +82 -82
  243. package/src/contentful/blocks/navigation/types.ts +71 -71
  244. package/src/contentful/blocks/primary-hero/PrimaryHero.stories.tsx +23 -23
  245. package/src/contentful/blocks/primary-hero/index.test.tsx +286 -286
  246. package/src/contentful/blocks/primary-hero/index.tsx +239 -239
  247. package/src/contentful/blocks/primary-hero/types.ts +37 -37
  248. package/src/contentful/blocks/search-block/index.test.tsx +268 -268
  249. package/src/contentful/blocks/search-block/index.tsx +90 -90
  250. package/src/contentful/blocks/search-block/types.ts +15 -15
  251. package/src/contentful/blocks/shape-background-wrapper/ShapeBackgroundWrapper.stories.tsx +26 -26
  252. package/src/contentful/blocks/shape-background-wrapper/index.test.tsx +284 -284
  253. package/src/contentful/blocks/shape-background-wrapper/index.tsx +124 -124
  254. package/src/contentful/blocks/shape-background-wrapper/types.ts +36 -36
  255. package/src/contentful/blocks/text/Text.stories.tsx +23 -23
  256. package/src/contentful/blocks/text/index.test.tsx +36 -36
  257. package/src/contentful/blocks/text/index.tsx +12 -12
  258. package/src/contentful/blocks/text/types.ts +1 -1
  259. package/src/contentful/index.test.ts +45 -45
  260. package/src/contentful/index.ts +105 -105
  261. package/src/global-mocks/contentful/to-document.ts +25 -25
  262. package/src/global-mocks/cookie.ts +48 -48
  263. package/src/global-mocks/cx.ts +37 -37
  264. package/src/global-mocks/index.ts +89 -89
  265. package/src/global-mocks/speed-card-bg.ts +27 -27
  266. package/src/global-mocks/utm.ts +49 -49
  267. package/src/hooks/contentful/use-contentful-rich-text.test.tsx +1758 -1758
  268. package/src/hooks/contentful/use-contentful-rich-text.tsx +309 -309
  269. package/src/hooks/contentful/use-processed-check-list.test.tsx +277 -277
  270. package/src/hooks/contentful/use-processed-check-list.ts +63 -63
  271. package/src/hooks/use-body-scroll-lock.test.ts +134 -134
  272. package/src/hooks/use-body-scroll-lock.ts +34 -34
  273. package/src/hooks/use-carousel-swipe.test.ts +393 -393
  274. package/src/hooks/use-carousel-swipe.ts +264 -264
  275. package/src/hooks/use-outside-click.test.ts +142 -142
  276. package/src/hooks/use-outside-click.ts +17 -17
  277. package/src/index.ts +107 -107
  278. package/src/next/index.test.ts +7 -7
  279. package/src/next/index.ts +5 -5
  280. package/src/setupTests.ts +52 -52
  281. package/src/stories/DocsTemplate.tsx +24 -24
  282. package/src/styles/globals.css +343 -343
  283. package/src/types/global.d.ts +9 -9
  284. package/src/types/micro-components.ts +99 -99
  285. package/src/types/utm.ts +49 -49
  286. package/src/utils/contentful/to-document.test.ts +85 -85
  287. package/src/utils/contentful/to-document.ts +24 -24
  288. package/src/utils/cookie.test.ts +180 -180
  289. package/src/utils/cookie.ts +84 -84
  290. package/src/utils/cx.test.ts +90 -90
  291. package/src/utils/cx.ts +49 -49
  292. package/src/utils/index.test.ts +115 -115
  293. package/src/utils/index.ts +41 -41
  294. package/src/utils/speed-card-bg.test.ts +46 -46
  295. package/src/utils/speed-card-bg.ts +24 -24
  296. package/src/utils/utm.test.ts +359 -359
  297. package/src/utils/utm.ts +221 -221
@@ -1,1758 +1,1758 @@
1
- import {
2
- renderContentfulRichText,
3
- renderContentfulRichTextTable,
4
- useContentfulRichText,
5
- } from "./use-contentful-rich-text";
6
-
7
- import {
8
- BLOCKS,
9
- INLINES,
10
- MARKS,
11
- type Document,
12
- } from "@contentful/rich-text-types";
13
- import { render, renderHook, screen } from "@testing-library/react";
14
-
15
- // Mock dependencies
16
- jest.mock("@shared/components/text", () => ({
17
- Text: ({ as: Tag = "span", children, className }: any) => (
18
- <Tag className={className}>{children}</Tag>
19
- ),
20
- }));
21
-
22
- jest.mock("@shared/components/link", () => ({
23
- Link: ({ href, children, target, rel, className }: any) => (
24
- <a href={href} target={target} rel={rel} className={className}>
25
- {children}
26
- </a>
27
- ),
28
- }));
29
-
30
- jest.mock("@shared/components/material-icon", () => ({
31
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
32
- MaterialIcon: ({ name, color, fill }: any) => (
33
- <span data-testid={`icon-${name}`} data-color={color}>
34
- {name}
35
- </span>
36
- ),
37
- }));
38
-
39
- jest.mock("@shared/components/checklist", () => ({
40
- Checklist: ({ items }: any) => (
41
- <ul data-testid="checklist">
42
- {items?.map((item: any, i: number) => (
43
- <li key={i}>{item}</li>
44
- ))}
45
- </ul>
46
- ),
47
- }));
48
-
49
- // Helper to build a Contentful Document
50
- function makeDoc(...content: any[]): Document {
51
- return {
52
- nodeType: BLOCKS.DOCUMENT,
53
- data: {},
54
- content,
55
- };
56
- }
57
-
58
- function makeParagraph(text: string): any {
59
- return {
60
- nodeType: BLOCKS.PARAGRAPH,
61
- data: {},
62
- content: [{ nodeType: "text", value: text, marks: [], data: {} }],
63
- };
64
- }
65
-
66
- function makeHeading(level: number, text: string): any {
67
- const nodeType = `heading-${level}` as any;
68
- return {
69
- nodeType,
70
- data: {},
71
- content: [{ nodeType: "text", value: text, marks: [], data: {} }],
72
- };
73
- }
74
-
75
- function makeMarkedText(text: string, markType: string): any {
76
- return {
77
- nodeType: BLOCKS.PARAGRAPH,
78
- data: {},
79
- content: [
80
- {
81
- nodeType: "text",
82
- value: text,
83
- marks: [{ type: markType }],
84
- data: {},
85
- },
86
- ],
87
- };
88
- }
89
-
90
- function makeHyperlink(url: string, text: string): any {
91
- return {
92
- nodeType: BLOCKS.PARAGRAPH,
93
- data: {},
94
- content: [
95
- {
96
- nodeType: INLINES.HYPERLINK,
97
- data: { uri: url },
98
- content: [{ nodeType: "text", value: text, marks: [], data: {} }],
99
- },
100
- ],
101
- };
102
- }
103
-
104
- function makeList(ordered: boolean, items: string[]): any {
105
- return {
106
- nodeType: ordered ? BLOCKS.OL_LIST : BLOCKS.UL_LIST,
107
- data: {},
108
- content: items.map(item => ({
109
- nodeType: BLOCKS.LIST_ITEM,
110
- data: {},
111
- content: [makeParagraph(item)],
112
- })),
113
- };
114
- }
115
-
116
- function makeBlockquote(text: string): any {
117
- return {
118
- nodeType: BLOCKS.QUOTE,
119
- data: {},
120
- content: [makeParagraph(text)],
121
- };
122
- }
123
-
124
- function makeEmbeddedAsset(url: string | undefined, title?: string): any {
125
- return {
126
- nodeType: BLOCKS.EMBEDDED_ASSET,
127
- data: {
128
- target: url
129
- ? {
130
- fields: {
131
- file: { url },
132
- title: title ?? "Asset",
133
- },
134
- }
135
- : { fields: {} },
136
- },
137
- content: [],
138
- };
139
- }
140
-
141
- function makeEmbeddedEntry(contentTypeId: string, fields: any): any {
142
- return {
143
- nodeType: BLOCKS.EMBEDDED_ENTRY,
144
- data: {
145
- target: {
146
- sys: { contentType: { sys: { id: contentTypeId } } },
147
- fields,
148
- },
149
- },
150
- content: [],
151
- };
152
- }
153
-
154
- function makeInlineEntry(contentTypeId: string, fields: any): any {
155
- return {
156
- nodeType: INLINES.EMBEDDED_ENTRY,
157
- data: {
158
- target: {
159
- sys: { contentType: { sys: { id: contentTypeId } } },
160
- fields,
161
- },
162
- },
163
- content: [],
164
- };
165
- }
166
-
167
- // ──────────────────────────────────────────────
168
- // renderContentfulRichText
169
- // ──────────────────────────────────────────────
170
- describe("renderContentfulRichText", () => {
171
- describe("Null / invalid input", () => {
172
- it("returns null for null doc", () => {
173
- expect(renderContentfulRichText(null)).toBeNull();
174
- });
175
-
176
- it("returns null for undefined doc", () => {
177
- expect(renderContentfulRichText(undefined)).toBeNull();
178
- });
179
-
180
- it("returns null for doc without content array", () => {
181
- const bad = { nodeType: BLOCKS.DOCUMENT, data: {} } as any;
182
- expect(renderContentfulRichText(bad)).toBeNull();
183
- });
184
- });
185
-
186
- describe("Paragraph rendering", () => {
187
- it("renders paragraph with default className 'body1'", () => {
188
- const doc = makeDoc(makeParagraph("Hello world"));
189
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
190
- const div = container.querySelector("div.body1");
191
- expect(div).toBeInTheDocument();
192
- expect(div).toHaveTextContent("Hello world");
193
- });
194
-
195
- it("renders paragraph with custom className", () => {
196
- const doc = makeDoc(makeParagraph("Custom class"));
197
- const { container } = render(
198
- <>{renderContentfulRichText(doc, undefined, "custom-class")}</>
199
- );
200
- expect(container.querySelector("div.custom-class")).toHaveTextContent(
201
- "Custom class"
202
- );
203
- });
204
- });
205
-
206
- describe("Mark rendering", () => {
207
- it("renders bold text", () => {
208
- const doc = makeDoc(makeMarkedText("Bold", MARKS.BOLD));
209
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
210
- const strong = container.querySelector("strong");
211
- expect(strong).toHaveTextContent("Bold");
212
- });
213
-
214
- it("renders italic text", () => {
215
- const doc = makeDoc(makeMarkedText("Italic", MARKS.ITALIC));
216
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
217
- expect(container.querySelector("em")).toHaveTextContent("Italic");
218
- });
219
-
220
- it("renders underline text", () => {
221
- const doc = makeDoc(makeMarkedText("Underline", MARKS.UNDERLINE));
222
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
223
- expect(container.querySelector("u")).toHaveTextContent("Underline");
224
- });
225
-
226
- it("renders code text", () => {
227
- const doc = makeDoc(makeMarkedText("Code", MARKS.CODE));
228
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
229
- expect(container.querySelector("code")).toHaveTextContent("Code");
230
- });
231
- });
232
-
233
- describe("Hyperlink rendering", () => {
234
- it("renders external link with target=_blank when target is not specified", () => {
235
- const doc = makeDoc(makeHyperlink("https://example.com", "Click"));
236
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
237
- const a = container.querySelector("a")!;
238
- expect(a).toHaveAttribute("href", "https://example.com");
239
- expect(a).toHaveAttribute("target", "_blank");
240
- expect(a).toHaveAttribute("rel", "noopener noreferrer");
241
- });
242
-
243
- it("renders internal link with target=_self for relative URLs", () => {
244
- const doc = makeDoc(makeHyperlink("/about", "About"));
245
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
246
- const a = container.querySelector("a")!;
247
- expect(a).toHaveAttribute("target", "_self");
248
- expect(a).not.toHaveAttribute("rel");
249
- });
250
-
251
- it("respects target=true flag to force _blank", () => {
252
- const doc = makeDoc(makeHyperlink("/local", "Local"));
253
- const { container } = render(<>{renderContentfulRichText(doc, true)}</>);
254
- const a = container.querySelector("a")!;
255
- expect(a).toHaveAttribute("target", "_blank");
256
- });
257
-
258
- it("respects target=false flag to force _self", () => {
259
- const doc = makeDoc(makeHyperlink("https://example.com", "External"));
260
- const { container } = render(<>{renderContentfulRichText(doc, false)}</>);
261
- const a = container.querySelector("a")!;
262
- expect(a).toHaveAttribute("target", "_self");
263
- });
264
-
265
- it("applies custom linkClassName", () => {
266
- const doc = makeDoc(makeHyperlink("https://x.com", "Link"));
267
- const { container } = render(
268
- <>{renderContentfulRichText(doc, undefined, "body1", "custom-link")}</>
269
- );
270
- const a = container.querySelector("a")!;
271
- expect(a).toHaveClass("custom-link");
272
- });
273
- });
274
-
275
- describe("Custom options merge", () => {
276
- it("merges custom renderNode options", () => {
277
- const doc = makeDoc(makeParagraph("Custom"));
278
- const customOptions = {
279
- renderNode: {
280
- [BLOCKS.PARAGRAPH]: (_node: any, children: any) => (
281
- <p data-testid="custom-p">{children}</p>
282
- ),
283
- },
284
- };
285
- render(
286
- <>
287
- {renderContentfulRichText(
288
- doc,
289
- undefined,
290
- "body1",
291
- "body1 font-bold",
292
- customOptions
293
- )}
294
- </>
295
- );
296
- // The merged PARAGRAPH comes from the function's own override, not from customOptions
297
- // because the function re-defines BLOCKS.PARAGRAPH after spreading options.renderNode
298
- // The function's PARAGRAPH override wins.
299
- expect(screen.queryByTestId("custom-p")).not.toBeInTheDocument();
300
- });
301
- });
302
-
303
- describe("Inline entry rendering", () => {
304
- it("renders componentCheckList inline entry via defaultOptions", () => {
305
- const doc = makeDoc({
306
- nodeType: BLOCKS.PARAGRAPH,
307
- data: {},
308
- content: [
309
- makeInlineEntry("componentCheckList", { title: "Check Item" }),
310
- ],
311
- });
312
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
313
- expect(container).toHaveTextContent("Check Item");
314
- });
315
-
316
- it("renders unknown inline entry title via defaultOptions", () => {
317
- const doc = makeDoc({
318
- nodeType: BLOCKS.PARAGRAPH,
319
- data: {},
320
- content: [makeInlineEntry("someOtherType", { title: "Fallback" })],
321
- });
322
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
323
- expect(container).toHaveTextContent("Fallback");
324
- });
325
-
326
- it("renders unknown inline entry with missing title as empty", () => {
327
- const doc = makeDoc({
328
- nodeType: BLOCKS.PARAGRAPH,
329
- data: {},
330
- content: [makeInlineEntry("someOtherType", {})],
331
- });
332
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
333
- const spans = container.querySelectorAll("span");
334
- const emptySpan = Array.from(spans).find(s => s.textContent === "");
335
- expect(emptySpan).toBeDefined();
336
- });
337
- });
338
-
339
- describe("Embedded entry rendering via defaultOptions", () => {
340
- it("renders callout block entry", () => {
341
- const doc = makeDoc(
342
- makeEmbeddedEntry("callout", {
343
- title: "Note",
344
- body: "Important info",
345
- })
346
- );
347
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
348
- expect(container.querySelector("aside")).toBeInTheDocument();
349
- expect(container).toHaveTextContent("Note");
350
- });
351
-
352
- it("returns null for unknown block entry type", () => {
353
- const doc = makeDoc(makeEmbeddedEntry("unknownType", { title: "X" }));
354
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
355
- expect(container.querySelector("aside")).not.toBeInTheDocument();
356
- });
357
- });
358
-
359
- describe("Embedded asset via defaultOptions", () => {
360
- it("renders image with protocol-relative URL", () => {
361
- const doc = makeDoc(
362
- makeEmbeddedAsset("//images.ctfl.net/photo.jpg", "Photo")
363
- );
364
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
365
- const img = container.querySelector("img")!;
366
- expect(img).toHaveAttribute("src", "https://images.ctfl.net/photo.jpg");
367
- });
368
-
369
- it("uses description as alt when title is missing", () => {
370
- const node: any = {
371
- nodeType: BLOCKS.EMBEDDED_ASSET,
372
- data: {
373
- target: {
374
- fields: {
375
- file: { url: "https://cdn.com/i.png" },
376
- description: "Desc text",
377
- },
378
- },
379
- },
380
- content: [],
381
- };
382
- const doc = makeDoc(node);
383
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
384
- expect(container.querySelector("img")).toHaveAttribute(
385
- "alt",
386
- "Desc text"
387
- );
388
- });
389
-
390
- it("uses default alt when no title or description", () => {
391
- const node: any = {
392
- nodeType: BLOCKS.EMBEDDED_ASSET,
393
- data: {
394
- target: {
395
- fields: {
396
- file: { url: "https://cdn.com/i.png" },
397
- },
398
- },
399
- },
400
- content: [],
401
- };
402
- const doc = makeDoc(node);
403
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
404
- expect(container.querySelector("img")).toHaveAttribute(
405
- "alt",
406
- "Embedded asset"
407
- );
408
- });
409
-
410
- it("uses target.url fallback when file.url is missing", () => {
411
- const node: any = {
412
- nodeType: BLOCKS.EMBEDDED_ASSET,
413
- data: {
414
- target: {
415
- url: "https://direct.com/img.png",
416
- fields: {},
417
- },
418
- },
419
- content: [],
420
- };
421
- const doc = makeDoc(node);
422
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
423
- expect(container.querySelector("img")).toHaveAttribute(
424
- "src",
425
- "https://direct.com/img.png"
426
- );
427
- });
428
-
429
- it("returns null when no url available", () => {
430
- const doc = makeDoc(makeEmbeddedAsset(undefined));
431
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
432
- expect(container.querySelector("img")).not.toBeInTheDocument();
433
- });
434
- });
435
-
436
- describe("HYPERLINK via defaultOptions in renderContentfulRichText", () => {
437
- it("renders external link with _blank via default HYPERLINK", () => {
438
- // When target param is not set, renderContentfulRichText uses its own HYPERLINK handler
439
- // But defaultOptions.HYPERLINK is still in the spread, though overridden
440
- // This tests renderContentfulRichText's own HYPERLINK handler
441
- const doc = makeDoc(makeHyperlink("https://ext.com", "Ext"));
442
- const { container } = render(
443
- <>{renderContentfulRichText(doc, undefined)}</>
444
- );
445
- const a = container.querySelector("a")!;
446
- expect(a).toHaveAttribute("target", "_blank");
447
- });
448
-
449
- it("renders internal link with _self when target is undefined", () => {
450
- const doc = makeDoc(makeHyperlink("/page", "Page"));
451
- const { container } = render(
452
- <>{renderContentfulRichText(doc, undefined)}</>
453
- );
454
- const a = container.querySelector("a")!;
455
- expect(a).toHaveAttribute("target", "_self");
456
- });
457
-
458
- it("handles hyperlink with missing uri data", () => {
459
- const node: any = {
460
- nodeType: BLOCKS.PARAGRAPH,
461
- data: {},
462
- content: [
463
- {
464
- nodeType: INLINES.HYPERLINK,
465
- data: {},
466
- content: [
467
- { nodeType: "text", value: "No URI", marks: [], data: {} },
468
- ],
469
- },
470
- ],
471
- };
472
- const doc = makeDoc(node);
473
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
474
- expect(container).toHaveTextContent("No URI");
475
- });
476
-
477
- it("handles hyperlink with null data", () => {
478
- const node: any = {
479
- nodeType: BLOCKS.PARAGRAPH,
480
- data: {},
481
- content: [
482
- {
483
- nodeType: INLINES.HYPERLINK,
484
- data: null,
485
- content: [
486
- { nodeType: "text", value: "Null data", marks: [], data: {} },
487
- ],
488
- },
489
- ],
490
- };
491
- const doc = makeDoc(node);
492
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
493
- expect(container).toHaveTextContent("Null data");
494
- });
495
- });
496
-
497
- describe("Superscript and subscript marks", () => {
498
- it("renders superscript text", () => {
499
- const doc = makeDoc(makeMarkedText("Sup", MARKS.SUPERSCRIPT));
500
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
501
- expect(container.querySelector("sup")).toHaveTextContent("Sup");
502
- });
503
-
504
- it("renders subscript text", () => {
505
- const doc = makeDoc(makeMarkedText("Sub", MARKS.SUBSCRIPT));
506
- const { container } = render(<>{renderContentfulRichText(doc)}</>);
507
- expect(container.querySelector("sub")).toHaveTextContent("Sub");
508
- });
509
- });
510
- });
511
-
512
- // ──────────────────────────────────────────────
513
- // useContentfulRichText
514
- // ──────────────────────────────────────────────
515
- describe("useContentfulRichText", () => {
516
- it("returns null for null doc", () => {
517
- const { result } = renderHook(() => useContentfulRichText(null));
518
- expect(result.current).toBeNull();
519
- });
520
-
521
- it("returns null for undefined doc", () => {
522
- const { result } = renderHook(() => useContentfulRichText(undefined));
523
- expect(result.current).toBeNull();
524
- });
525
-
526
- it("returns null for doc without content array", () => {
527
- const bad = { nodeType: BLOCKS.DOCUMENT, data: {} } as any;
528
- const { result } = renderHook(() => useContentfulRichText(bad));
529
- expect(result.current).toBeNull();
530
- });
531
-
532
- it("renders paragraph content", () => {
533
- const doc = makeDoc(makeParagraph("Hook test"));
534
- const { result } = renderHook(() => useContentfulRichText(doc));
535
- const { container } = render(<>{result.current}</>);
536
- expect(container).toHaveTextContent("Hook test");
537
- });
538
-
539
- it("renders headings h1-h6", () => {
540
- const doc = makeDoc(
541
- makeHeading(1, "H1"),
542
- makeHeading(2, "H2"),
543
- makeHeading(3, "H3"),
544
- makeHeading(4, "H4"),
545
- makeHeading(5, "H5"),
546
- makeHeading(6, "H6")
547
- );
548
- const { result } = renderHook(() => useContentfulRichText(doc));
549
- const { container } = render(<>{result.current}</>);
550
- expect(container.querySelector("h1")).toHaveTextContent("H1");
551
- expect(container.querySelector("h2")).toHaveTextContent("H2");
552
- // h3-h6 all render as h3 per defaultOptions
553
- const h3s = container.querySelectorAll("h3");
554
- expect(h3s.length).toBe(4);
555
- });
556
-
557
- it("renders unordered list", () => {
558
- const doc = makeDoc(makeList(false, ["Item A", "Item B"]));
559
- const { result } = renderHook(() => useContentfulRichText(doc));
560
- const { container } = render(<>{result.current}</>);
561
- expect(container.querySelector("ul")).toBeInTheDocument();
562
- expect(container.querySelectorAll("li")).toHaveLength(2);
563
- });
564
-
565
- it("renders ordered list", () => {
566
- const doc = makeDoc(makeList(true, ["First", "Second"]));
567
- const { result } = renderHook(() => useContentfulRichText(doc));
568
- const { container } = render(<>{result.current}</>);
569
- expect(container.querySelector("ol")).toBeInTheDocument();
570
- });
571
-
572
- it("renders blockquote", () => {
573
- const doc = makeDoc(makeBlockquote("Quote text"));
574
- const { result } = renderHook(() => useContentfulRichText(doc));
575
- const { container } = render(<>{result.current}</>);
576
- expect(container.querySelector("blockquote")).toHaveTextContent(
577
- "Quote text"
578
- );
579
- });
580
-
581
- it("renders hyperlink with external target", () => {
582
- const doc = makeDoc(makeHyperlink("https://ext.com", "External"));
583
- const { result } = renderHook(() => useContentfulRichText(doc));
584
- const { container } = render(<>{result.current}</>);
585
- const a = container.querySelector("a")!;
586
- expect(a).toHaveAttribute("target", "_blank");
587
- });
588
-
589
- it("renders hyperlink with internal target", () => {
590
- const doc = makeDoc(makeHyperlink("/page", "Internal"));
591
- const { result } = renderHook(() => useContentfulRichText(doc));
592
- const { container } = render(<>{result.current}</>);
593
- const a = container.querySelector("a")!;
594
- expect(a).not.toHaveAttribute("target");
595
- });
596
-
597
- it("renders embedded asset with image", () => {
598
- const doc = makeDoc(
599
- makeEmbeddedAsset("//images.ctfl.net/photo.jpg", "Photo")
600
- );
601
- const { result } = renderHook(() => useContentfulRichText(doc));
602
- const { container } = render(<>{result.current}</>);
603
- const img = container.querySelector("img")!;
604
- expect(img).toHaveAttribute("src", "https://images.ctfl.net/photo.jpg");
605
- expect(img).toHaveAttribute("alt", "Photo");
606
- });
607
-
608
- it("renders embedded asset with absolute URL", () => {
609
- const doc = makeDoc(makeEmbeddedAsset("https://cdn.example.com/img.png"));
610
- const { result } = renderHook(() => useContentfulRichText(doc));
611
- const { container } = render(<>{result.current}</>);
612
- const img = container.querySelector("img")!;
613
- expect(img).toHaveAttribute("src", "https://cdn.example.com/img.png");
614
- });
615
-
616
- it("returns null for embedded asset without URL", () => {
617
- const doc = makeDoc(makeEmbeddedAsset(undefined));
618
- const { result } = renderHook(() => useContentfulRichText(doc));
619
- const { container } = render(<>{result.current}</>);
620
- expect(container.querySelector("img")).not.toBeInTheDocument();
621
- });
622
-
623
- it("renders embedded asset using target.url when fields.file.url is missing", () => {
624
- const node: any = {
625
- nodeType: BLOCKS.EMBEDDED_ASSET,
626
- data: {
627
- target: {
628
- url: "https://direct.com/image.png",
629
- fields: { title: "Direct" },
630
- },
631
- },
632
- content: [],
633
- };
634
- const doc = makeDoc(node);
635
- const { result } = renderHook(() => useContentfulRichText(doc));
636
- const { container } = render(<>{result.current}</>);
637
- expect(container.querySelector("img")).toHaveAttribute(
638
- "src",
639
- "https://direct.com/image.png"
640
- );
641
- });
642
-
643
- it("renders embedded entry with callout type", () => {
644
- const doc = makeDoc(
645
- makeEmbeddedEntry("callout", { title: "Notice", body: "Important info" })
646
- );
647
- const { result } = renderHook(() => useContentfulRichText(doc));
648
- const { container } = render(<>{result.current}</>);
649
- expect(container.querySelector("aside")).toBeInTheDocument();
650
- expect(container).toHaveTextContent("Notice");
651
- expect(container).toHaveTextContent("Important info");
652
- });
653
-
654
- it("returns null for embedded entry with unknown type", () => {
655
- const doc = makeDoc(makeEmbeddedEntry("unknown", { title: "X" }));
656
- const { result } = renderHook(() => useContentfulRichText(doc));
657
- const { container } = render(<>{result.current}</>);
658
- expect(container.querySelector("aside")).not.toBeInTheDocument();
659
- });
660
-
661
- it("renders inline entry with componentCheckList type", () => {
662
- const doc = makeDoc({
663
- nodeType: BLOCKS.PARAGRAPH,
664
- data: {},
665
- content: [
666
- makeInlineEntry("componentCheckList", { title: "Checklist Item" }),
667
- ],
668
- });
669
- const { result } = renderHook(() => useContentfulRichText(doc));
670
- const { container } = render(<>{result.current}</>);
671
- expect(container).toHaveTextContent("Checklist Item");
672
- });
673
-
674
- it("renders inline entry with unknown type showing title", () => {
675
- const doc = makeDoc({
676
- nodeType: BLOCKS.PARAGRAPH,
677
- data: {},
678
- content: [makeInlineEntry("other", { title: "Other Entry" })],
679
- });
680
- const { result } = renderHook(() => useContentfulRichText(doc));
681
- const { container } = render(<>{result.current}</>);
682
- expect(container).toHaveTextContent("Other Entry");
683
- });
684
-
685
- it("renders inline entry with unknown type and missing title as empty", () => {
686
- const doc = makeDoc({
687
- nodeType: BLOCKS.PARAGRAPH,
688
- data: {},
689
- content: [makeInlineEntry("other", {})],
690
- });
691
- const { result } = renderHook(() => useContentfulRichText(doc));
692
- const { container } = render(<>{result.current}</>);
693
- const spans = container.querySelectorAll("span");
694
- const emptySpan = Array.from(spans).find(s => s.textContent === "");
695
- expect(emptySpan).toBeDefined();
696
- });
697
-
698
- it("merges custom mark options", () => {
699
- const doc = makeDoc(makeMarkedText("Custom bold", MARKS.BOLD));
700
- const customOpts = {
701
- renderMark: {
702
- [MARKS.BOLD]: (text: any) => <b data-testid="custom-bold">{text}</b>,
703
- },
704
- };
705
- const { result } = renderHook(() => useContentfulRichText(doc, customOpts));
706
- render(<>{result.current}</>);
707
- expect(screen.getByTestId("custom-bold")).toHaveTextContent("Custom bold");
708
- });
709
-
710
- it("memoizes result for same doc reference", () => {
711
- const doc = makeDoc(makeParagraph("Memoized"));
712
- const { result, rerender } = renderHook(
713
- ({ d }) => useContentfulRichText(d),
714
- { initialProps: { d: doc } }
715
- );
716
- const first = result.current;
717
- rerender({ d: doc });
718
- expect(result.current).toBe(first);
719
- });
720
-
721
- it("recomputes when doc changes", () => {
722
- const doc1 = makeDoc(makeParagraph("First"));
723
- const doc2 = makeDoc(makeParagraph("Second"));
724
- const { result, rerender } = renderHook(
725
- ({ d }) => useContentfulRichText(d),
726
- { initialProps: { d: doc1 as Document | null } }
727
- );
728
- const first = result.current;
729
- rerender({ d: doc2 });
730
- expect(result.current).not.toBe(first);
731
- });
732
-
733
- it("transitions from null doc to valid doc", () => {
734
- const { result, rerender } = renderHook(
735
- ({ d }) => useContentfulRichText(d),
736
- { initialProps: { d: null as Document | null } }
737
- );
738
- expect(result.current).toBeNull();
739
- rerender({ d: makeDoc(makeParagraph("Now visible")) });
740
- const { container } = render(<>{result.current}</>);
741
- expect(container).toHaveTextContent("Now visible");
742
- });
743
- });
744
-
745
- // ──────────────────────────────────────────────
746
- // renderContentfulRichTextTable
747
- // ──────────────────────────────────────────────
748
- describe("renderContentfulRichTextTable", () => {
749
- it("returns null for null doc", () => {
750
- expect(renderContentfulRichTextTable(null)).toBeNull();
751
- });
752
-
753
- it("returns null for undefined doc", () => {
754
- expect(renderContentfulRichTextTable(undefined)).toBeNull();
755
- });
756
-
757
- it("returns null for doc without content array", () => {
758
- const bad = { nodeType: BLOCKS.DOCUMENT, data: {} } as any;
759
- expect(renderContentfulRichTextTable(bad)).toBeNull();
760
- });
761
-
762
- it("renders table with rows", () => {
763
- const doc = makeDoc({
764
- nodeType: BLOCKS.TABLE,
765
- data: {},
766
- content: [
767
- {
768
- nodeType: BLOCKS.TABLE_ROW,
769
- data: {},
770
- content: [
771
- {
772
- nodeType: BLOCKS.TABLE_HEADER_CELL,
773
- data: {},
774
- content: [makeParagraph("Header")],
775
- },
776
- {
777
- nodeType: BLOCKS.TABLE_HEADER_CELL,
778
- data: {},
779
- content: [makeParagraph("Header 2")],
780
- },
781
- ],
782
- },
783
- {
784
- nodeType: BLOCKS.TABLE_ROW,
785
- data: {},
786
- content: [
787
- {
788
- nodeType: BLOCKS.TABLE_CELL,
789
- data: {},
790
- content: [makeParagraph("Cell 1")],
791
- },
792
- {
793
- nodeType: BLOCKS.TABLE_CELL,
794
- data: {},
795
- content: [makeParagraph("Cell 2")],
796
- },
797
- ],
798
- },
799
- ],
800
- });
801
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
802
- expect(container.querySelector("table")).toBeInTheDocument();
803
- expect(container.querySelectorAll("th")).toHaveLength(2);
804
- expect(container.querySelectorAll("td")).toHaveLength(2);
805
- });
806
-
807
- it("renders check_circle icon for 'yes' cell value", () => {
808
- const doc = makeDoc({
809
- nodeType: BLOCKS.TABLE,
810
- data: {},
811
- content: [
812
- {
813
- nodeType: BLOCKS.TABLE_ROW,
814
- data: {},
815
- content: [
816
- {
817
- nodeType: BLOCKS.TABLE_CELL,
818
- data: {},
819
- content: [makeParagraph("yes")],
820
- },
821
- ],
822
- },
823
- ],
824
- });
825
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
826
- expect(
827
- container.querySelector('[data-testid="icon-check_circle"]')
828
- ).toBeInTheDocument();
829
- });
830
-
831
- it("renders cancel icon for 'no' cell value", () => {
832
- const doc = makeDoc({
833
- nodeType: BLOCKS.TABLE,
834
- data: {},
835
- content: [
836
- {
837
- nodeType: BLOCKS.TABLE_ROW,
838
- data: {},
839
- content: [
840
- {
841
- nodeType: BLOCKS.TABLE_CELL,
842
- data: {},
843
- content: [makeParagraph("no")],
844
- },
845
- ],
846
- },
847
- ],
848
- });
849
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
850
- expect(
851
- container.querySelector('[data-testid="icon-cancel"]')
852
- ).toBeInTheDocument();
853
- });
854
-
855
- it("renders text content for non-yes/no cell values", () => {
856
- const doc = makeDoc({
857
- nodeType: BLOCKS.TABLE,
858
- data: {},
859
- content: [
860
- {
861
- nodeType: BLOCKS.TABLE_ROW,
862
- data: {},
863
- content: [
864
- {
865
- nodeType: BLOCKS.TABLE_CELL,
866
- data: {},
867
- content: [makeParagraph("Custom text")],
868
- },
869
- ],
870
- },
871
- ],
872
- });
873
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
874
- expect(container).toHaveTextContent("Custom text");
875
- expect(
876
- container.querySelector('[data-testid="icon-check_circle"]')
877
- ).not.toBeInTheDocument();
878
- expect(
879
- container.querySelector('[data-testid="icon-cancel"]')
880
- ).not.toBeInTheDocument();
881
- });
882
-
883
- it("renders bold with table-specific class", () => {
884
- const doc = makeDoc(makeMarkedText("Table bold", MARKS.BOLD));
885
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
886
- const strong = container.querySelector("strong")!;
887
- expect(strong).toHaveClass("label4");
888
- expect(strong).toHaveClass("md:label2");
889
- });
890
-
891
- it("renders paragraph as fragment (no wrapper div)", () => {
892
- const doc = makeDoc(makeParagraph("Fragment text"));
893
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
894
- // Should NOT have a div.body3 wrapper like the default options
895
- expect(container.querySelector("div.body3")).not.toBeInTheDocument();
896
- expect(container).toHaveTextContent("Fragment text");
897
- });
898
-
899
- it("renders inline embedded checklist entry from links", () => {
900
- const entryId = "entry-123";
901
- const doc = makeDoc({
902
- nodeType: BLOCKS.PARAGRAPH,
903
- data: {},
904
- content: [
905
- {
906
- nodeType: INLINES.EMBEDDED_ENTRY,
907
- data: { target: { sys: { id: entryId } } },
908
- content: [],
909
- },
910
- ],
911
- });
912
- const links = {
913
- entries: {
914
- inline: [
915
- {
916
- sys: { id: entryId },
917
- __typename: "ComponentCheckList",
918
- list: {
919
- items: [{ checkListTitle: null }],
920
- },
921
- },
922
- ],
923
- },
924
- };
925
- const { container } = render(
926
- <>{renderContentfulRichTextTable(doc, links)}</>
927
- );
928
- expect(
929
- container.querySelector('[data-testid="checklist"]')
930
- ).toBeInTheDocument();
931
- });
932
-
933
- it("renders inline entry title for non-checklist type", () => {
934
- const entryId = "entry-456";
935
- const doc = makeDoc({
936
- nodeType: BLOCKS.PARAGRAPH,
937
- data: {},
938
- content: [
939
- {
940
- nodeType: INLINES.EMBEDDED_ENTRY,
941
- data: { target: { sys: { id: entryId } } },
942
- content: [],
943
- },
944
- ],
945
- });
946
- const links = {
947
- entries: {
948
- inline: [
949
- {
950
- sys: { id: entryId },
951
- __typename: "OtherType",
952
- title: "Other Title",
953
- },
954
- ],
955
- },
956
- };
957
- const { container } = render(
958
- <>{renderContentfulRichTextTable(doc, links)}</>
959
- );
960
- expect(container).toHaveTextContent("Other Title");
961
- });
962
-
963
- it("returns null for inline entry not found in links", () => {
964
- const doc = makeDoc({
965
- nodeType: BLOCKS.PARAGRAPH,
966
- data: {},
967
- content: [
968
- {
969
- nodeType: INLINES.EMBEDDED_ENTRY,
970
- data: { target: { sys: { id: "missing" } } },
971
- content: [],
972
- },
973
- ],
974
- });
975
- const { container } = render(
976
- <>{renderContentfulRichTextTable(doc, { entries: { inline: [] } })}</>
977
- );
978
- // Should render the paragraph wrapper but no entry content
979
- expect(container.textContent).toBe("");
980
- });
981
-
982
- it("renders scrollable table with >2 columns", () => {
983
- const headerRow = {
984
- nodeType: BLOCKS.TABLE_ROW,
985
- data: {},
986
- content: [
987
- {
988
- nodeType: BLOCKS.TABLE_HEADER_CELL,
989
- data: {},
990
- content: [makeParagraph("A")],
991
- },
992
- {
993
- nodeType: BLOCKS.TABLE_HEADER_CELL,
994
- data: {},
995
- content: [makeParagraph("B")],
996
- },
997
- {
998
- nodeType: BLOCKS.TABLE_HEADER_CELL,
999
- data: {},
1000
- content: [makeParagraph("C")],
1001
- },
1002
- ],
1003
- };
1004
- const doc = makeDoc({
1005
- nodeType: BLOCKS.TABLE,
1006
- data: {},
1007
- content: [headerRow],
1008
- });
1009
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1010
- expect(container.querySelector("table")?.className).toContain(
1011
- "min-w-[100.1%]"
1012
- );
1013
- });
1014
-
1015
- it("renders non-scrollable table with <=2 columns", () => {
1016
- const headerRow = {
1017
- nodeType: BLOCKS.TABLE_ROW,
1018
- data: {},
1019
- content: [
1020
- {
1021
- nodeType: BLOCKS.TABLE_HEADER_CELL,
1022
- data: {},
1023
- content: [makeParagraph("A")],
1024
- },
1025
- {
1026
- nodeType: BLOCKS.TABLE_HEADER_CELL,
1027
- data: {},
1028
- content: [makeParagraph("B")],
1029
- },
1030
- ],
1031
- };
1032
- const doc = makeDoc({
1033
- nodeType: BLOCKS.TABLE,
1034
- data: {},
1035
- content: [headerRow],
1036
- });
1037
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1038
- expect(container.querySelector("table")?.className).toContain("min-w-full");
1039
- });
1040
-
1041
- it("renders table cells with non-scrollable layout (node.parent not available)", () => {
1042
- const doc = makeDoc({
1043
- nodeType: BLOCKS.TABLE,
1044
- data: {},
1045
- content: [
1046
- {
1047
- nodeType: BLOCKS.TABLE_ROW,
1048
- data: {},
1049
- content: [
1050
- {
1051
- nodeType: BLOCKS.TABLE_HEADER_CELL,
1052
- data: {},
1053
- content: [makeParagraph("H1")],
1054
- },
1055
- {
1056
- nodeType: BLOCKS.TABLE_HEADER_CELL,
1057
- data: {},
1058
- content: [makeParagraph("H2")],
1059
- },
1060
- {
1061
- nodeType: BLOCKS.TABLE_HEADER_CELL,
1062
- data: {},
1063
- content: [makeParagraph("H3")],
1064
- },
1065
- ],
1066
- },
1067
- {
1068
- nodeType: BLOCKS.TABLE_ROW,
1069
- data: {},
1070
- content: [
1071
- {
1072
- nodeType: BLOCKS.TABLE_CELL,
1073
- data: {},
1074
- content: [makeParagraph("C1")],
1075
- },
1076
- {
1077
- nodeType: BLOCKS.TABLE_CELL,
1078
- data: {},
1079
- content: [makeParagraph("C2")],
1080
- },
1081
- {
1082
- nodeType: BLOCKS.TABLE_CELL,
1083
- data: {},
1084
- content: [makeParagraph("C3")],
1085
- },
1086
- ],
1087
- },
1088
- ],
1089
- });
1090
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1091
- const tds = container.querySelectorAll("td");
1092
- expect(tds.length).toBe(3);
1093
- // node.parent is not set by documentToReactComponents, so isScrollable is always false
1094
- tds.forEach(td => {
1095
- expect(td.className).toContain("w-1/4");
1096
- });
1097
- });
1098
-
1099
- it("renders non-scrollable table cells with w-1/4 for <=2 columns", () => {
1100
- const doc = makeDoc({
1101
- nodeType: BLOCKS.TABLE,
1102
- data: {},
1103
- content: [
1104
- {
1105
- nodeType: BLOCKS.TABLE_ROW,
1106
- data: {},
1107
- content: [
1108
- {
1109
- nodeType: BLOCKS.TABLE_HEADER_CELL,
1110
- data: {},
1111
- content: [makeParagraph("H1")],
1112
- },
1113
- {
1114
- nodeType: BLOCKS.TABLE_HEADER_CELL,
1115
- data: {},
1116
- content: [makeParagraph("H2")],
1117
- },
1118
- ],
1119
- },
1120
- {
1121
- nodeType: BLOCKS.TABLE_ROW,
1122
- data: {},
1123
- content: [
1124
- {
1125
- nodeType: BLOCKS.TABLE_CELL,
1126
- data: {},
1127
- content: [makeParagraph("D1")],
1128
- },
1129
- {
1130
- nodeType: BLOCKS.TABLE_CELL,
1131
- data: {},
1132
- content: [makeParagraph("D2")],
1133
- },
1134
- ],
1135
- },
1136
- ],
1137
- });
1138
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1139
- const tds = container.querySelectorAll("td");
1140
- tds.forEach(td => {
1141
- expect(td.className).toContain("w-1/4");
1142
- });
1143
- });
1144
-
1145
- it("renders checklist with empty items when list has no items", () => {
1146
- const entryId = "entry-empty";
1147
- const doc = makeDoc({
1148
- nodeType: BLOCKS.PARAGRAPH,
1149
- data: {},
1150
- content: [
1151
- {
1152
- nodeType: INLINES.EMBEDDED_ENTRY,
1153
- data: { target: { sys: { id: entryId } } },
1154
- content: [],
1155
- },
1156
- ],
1157
- });
1158
- const links = {
1159
- entries: {
1160
- inline: [
1161
- {
1162
- sys: { id: entryId },
1163
- __typename: "ComponentCheckList",
1164
- list: undefined,
1165
- },
1166
- ],
1167
- },
1168
- };
1169
- const { container } = render(
1170
- <>{renderContentfulRichTextTable(doc, links)}</>
1171
- );
1172
- expect(
1173
- container.querySelector('[data-testid="checklist"]')
1174
- ).toBeInTheDocument();
1175
- });
1176
-
1177
- it("renders non-checklist inline entry with empty title", () => {
1178
- const entryId = "entry-no-title";
1179
- const doc = makeDoc({
1180
- nodeType: BLOCKS.PARAGRAPH,
1181
- data: {},
1182
- content: [
1183
- {
1184
- nodeType: INLINES.EMBEDDED_ENTRY,
1185
- data: { target: { sys: { id: entryId } } },
1186
- content: [],
1187
- },
1188
- ],
1189
- });
1190
- const links = {
1191
- entries: {
1192
- inline: [
1193
- {
1194
- sys: { id: entryId },
1195
- __typename: "OtherType",
1196
- title: "",
1197
- },
1198
- ],
1199
- },
1200
- };
1201
- const { container } = render(
1202
- <>{renderContentfulRichTextTable(doc, links)}</>
1203
- );
1204
- const span = container.querySelector("span");
1205
- expect(span?.textContent).toBe("");
1206
- });
1207
-
1208
- it("renders scrollable table header cells when node.parent has >2 children", () => {
1209
- // Build row with 3 header cells and set parent on each cell
1210
- const headerCells = [
1211
- {
1212
- nodeType: BLOCKS.TABLE_HEADER_CELL,
1213
- data: {},
1214
- content: [makeParagraph("H1")],
1215
- },
1216
- {
1217
- nodeType: BLOCKS.TABLE_HEADER_CELL,
1218
- data: {},
1219
- content: [makeParagraph("H2")],
1220
- },
1221
- {
1222
- nodeType: BLOCKS.TABLE_HEADER_CELL,
1223
- data: {},
1224
- content: [makeParagraph("H3")],
1225
- },
1226
- ];
1227
- const row = {
1228
- nodeType: BLOCKS.TABLE_ROW,
1229
- data: {},
1230
- content: headerCells,
1231
- };
1232
- // Set parent on each cell so isScrollable = true
1233
- headerCells.forEach(cell => {
1234
- (cell as any).parent = row;
1235
- });
1236
- const doc = makeDoc({
1237
- nodeType: BLOCKS.TABLE,
1238
- data: {},
1239
- content: [row],
1240
- });
1241
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1242
- const ths = container.querySelectorAll("th");
1243
- expect(ths.length).toBe(3);
1244
- ths.forEach(th => {
1245
- expect(th.className).toContain("sticky");
1246
- });
1247
- });
1248
-
1249
- it("renders scrollable table data cells when node.parent has >2 children", () => {
1250
- const dataCells = [
1251
- {
1252
- nodeType: BLOCKS.TABLE_CELL,
1253
- data: {},
1254
- content: [makeParagraph("D1")],
1255
- },
1256
- {
1257
- nodeType: BLOCKS.TABLE_CELL,
1258
- data: {},
1259
- content: [makeParagraph("D2")],
1260
- },
1261
- {
1262
- nodeType: BLOCKS.TABLE_CELL,
1263
- data: {},
1264
- content: [makeParagraph("D3")],
1265
- },
1266
- ];
1267
- const row = {
1268
- nodeType: BLOCKS.TABLE_ROW,
1269
- data: {},
1270
- content: dataCells,
1271
- };
1272
- dataCells.forEach(cell => {
1273
- (cell as any).parent = row;
1274
- });
1275
- const doc = makeDoc({
1276
- nodeType: BLOCKS.TABLE,
1277
- data: {},
1278
- content: [row],
1279
- });
1280
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1281
- const tds = container.querySelectorAll("td");
1282
- expect(tds.length).toBe(3);
1283
- tds.forEach(td => {
1284
- expect(td.className).toContain("min-w-[50vw]");
1285
- });
1286
- });
1287
-
1288
- it("renders scrollable table cell with 'yes' value and isScrollable true", () => {
1289
- const dataCells = [
1290
- {
1291
- nodeType: BLOCKS.TABLE_CELL,
1292
- data: {},
1293
- content: [makeParagraph("yes")],
1294
- },
1295
- {
1296
- nodeType: BLOCKS.TABLE_CELL,
1297
- data: {},
1298
- content: [makeParagraph("no")],
1299
- },
1300
- {
1301
- nodeType: BLOCKS.TABLE_CELL,
1302
- data: {},
1303
- content: [makeParagraph("other")],
1304
- },
1305
- ];
1306
- const row = {
1307
- nodeType: BLOCKS.TABLE_ROW,
1308
- data: {},
1309
- content: dataCells,
1310
- };
1311
- dataCells.forEach(cell => {
1312
- (cell as any).parent = row;
1313
- });
1314
- const doc = makeDoc({
1315
- nodeType: BLOCKS.TABLE,
1316
- data: {},
1317
- content: [row],
1318
- });
1319
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1320
- expect(
1321
- container.querySelector('[data-testid="icon-check_circle"]')
1322
- ).toBeInTheDocument();
1323
- expect(
1324
- container.querySelector('[data-testid="icon-cancel"]')
1325
- ).toBeInTheDocument();
1326
- const tds = container.querySelectorAll("td");
1327
- tds.forEach(td => {
1328
- expect(td.className).toContain("min-w-[50vw]");
1329
- });
1330
- });
1331
-
1332
- it("renders cell with empty content array gracefully", () => {
1333
- const doc = makeDoc({
1334
- nodeType: BLOCKS.TABLE,
1335
- data: {},
1336
- content: [
1337
- {
1338
- nodeType: BLOCKS.TABLE_ROW,
1339
- data: {},
1340
- content: [
1341
- {
1342
- nodeType: BLOCKS.TABLE_CELL,
1343
- data: {},
1344
- content: [],
1345
- },
1346
- ],
1347
- },
1348
- ],
1349
- });
1350
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1351
- const td = container.querySelector("td");
1352
- expect(td).toBeInTheDocument();
1353
- });
1354
-
1355
- it("renders superscript mark", () => {
1356
- const doc = makeDoc(makeMarkedText("Sup", MARKS.SUPERSCRIPT));
1357
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1358
- expect(container.querySelector("sup")).toHaveTextContent("Sup");
1359
- });
1360
-
1361
- it("renders subscript mark", () => {
1362
- const doc = makeDoc(makeMarkedText("Sub", MARKS.SUBSCRIPT));
1363
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1364
- expect(container.querySelector("sub")).toHaveTextContent("Sub");
1365
- });
1366
-
1367
- it("renders table when first row content is empty", () => {
1368
- const doc = makeDoc({
1369
- nodeType: BLOCKS.TABLE,
1370
- data: {},
1371
- content: [
1372
- {
1373
- nodeType: BLOCKS.TABLE_ROW,
1374
- data: {},
1375
- content: [],
1376
- },
1377
- ],
1378
- });
1379
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1380
- expect(container.querySelector("table")).toBeInTheDocument();
1381
- expect(container.querySelector("table")?.className).toContain("min-w-full");
1382
- });
1383
-
1384
- it("renders inline entry when links is undefined", () => {
1385
- const doc = makeDoc({
1386
- nodeType: BLOCKS.PARAGRAPH,
1387
- data: {},
1388
- content: [
1389
- {
1390
- nodeType: INLINES.EMBEDDED_ENTRY,
1391
- data: { target: { sys: { id: "any" } } },
1392
- content: [],
1393
- },
1394
- ],
1395
- });
1396
- const { container } = render(
1397
- <>{renderContentfulRichTextTable(doc, undefined)}</>
1398
- );
1399
- expect(container).toBeInTheDocument();
1400
- });
1401
-
1402
- it("renders inline entry when links has no entries", () => {
1403
- const doc = makeDoc({
1404
- nodeType: BLOCKS.PARAGRAPH,
1405
- data: {},
1406
- content: [
1407
- {
1408
- nodeType: INLINES.EMBEDDED_ENTRY,
1409
- data: { target: { sys: { id: "any" } } },
1410
- content: [],
1411
- },
1412
- ],
1413
- });
1414
- const { container } = render(<>{renderContentfulRichTextTable(doc, {})}</>);
1415
- expect(container).toBeInTheDocument();
1416
- });
1417
-
1418
- it("renders inline entry when links.entries has no inline", () => {
1419
- const doc = makeDoc({
1420
- nodeType: BLOCKS.PARAGRAPH,
1421
- data: {},
1422
- content: [
1423
- {
1424
- nodeType: INLINES.EMBEDDED_ENTRY,
1425
- data: { target: { sys: { id: "any" } } },
1426
- content: [],
1427
- },
1428
- ],
1429
- });
1430
- const { container } = render(
1431
- <>{renderContentfulRichTextTable(doc, { entries: {} })}</>
1432
- );
1433
- expect(container).toBeInTheDocument();
1434
- });
1435
-
1436
- it("renders checklist entry with list but no items", () => {
1437
- const entryId = "entry-no-items";
1438
- const doc = makeDoc({
1439
- nodeType: BLOCKS.PARAGRAPH,
1440
- data: {},
1441
- content: [
1442
- {
1443
- nodeType: INLINES.EMBEDDED_ENTRY,
1444
- data: { target: { sys: { id: entryId } } },
1445
- content: [],
1446
- },
1447
- ],
1448
- });
1449
- const links = {
1450
- entries: {
1451
- inline: [
1452
- {
1453
- sys: { id: entryId },
1454
- __typename: "ComponentCheckList",
1455
- list: { items: null },
1456
- },
1457
- ],
1458
- },
1459
- };
1460
- const { container } = render(
1461
- <>{renderContentfulRichTextTable(doc, links)}</>
1462
- );
1463
- expect(
1464
- container.querySelector('[data-testid="checklist"]')
1465
- ).toBeInTheDocument();
1466
- });
1467
-
1468
- it("renders checklist entry with no list property", () => {
1469
- const entryId = "entry-no-list";
1470
- const doc = makeDoc({
1471
- nodeType: BLOCKS.PARAGRAPH,
1472
- data: {},
1473
- content: [
1474
- {
1475
- nodeType: INLINES.EMBEDDED_ENTRY,
1476
- data: { target: { sys: { id: entryId } } },
1477
- content: [],
1478
- },
1479
- ],
1480
- });
1481
- const links = {
1482
- entries: {
1483
- inline: [
1484
- {
1485
- sys: { id: entryId },
1486
- __typename: "ComponentCheckList",
1487
- },
1488
- ],
1489
- },
1490
- };
1491
- const { container } = render(
1492
- <>{renderContentfulRichTextTable(doc, links)}</>
1493
- );
1494
- expect(
1495
- container.querySelector('[data-testid="checklist"]')
1496
- ).toBeInTheDocument();
1497
- });
1498
-
1499
- it("handles hyperlink with missing uri in table", () => {
1500
- const node: any = {
1501
- nodeType: BLOCKS.PARAGRAPH,
1502
- data: {},
1503
- content: [
1504
- {
1505
- nodeType: INLINES.HYPERLINK,
1506
- data: {},
1507
- content: [{ nodeType: "text", value: "NoUri", marks: [], data: {} }],
1508
- },
1509
- ],
1510
- };
1511
- const doc = makeDoc(node);
1512
- const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1513
- expect(container).toHaveTextContent("NoUri");
1514
- });
1515
- });
1516
-
1517
- // ──────────────────────────────────────────────
1518
- // Additional defaultOptions coverage
1519
- // ──────────────────────────────────────────────
1520
- describe("defaultOptions (via useContentfulRichText)", () => {
1521
- it("renders embedded asset with description fallback alt", () => {
1522
- const node: any = {
1523
- nodeType: BLOCKS.EMBEDDED_ASSET,
1524
- data: {
1525
- target: {
1526
- fields: {
1527
- file: { url: "https://cdn.com/img.png" },
1528
- description: "Desc alt",
1529
- },
1530
- },
1531
- },
1532
- content: [],
1533
- };
1534
- const doc = makeDoc(node);
1535
- const { result } = renderHook(() => useContentfulRichText(doc));
1536
- const { container } = render(<>{result.current}</>);
1537
- expect(container.querySelector("img")).toHaveAttribute("alt", "Desc alt");
1538
- });
1539
-
1540
- it("renders embedded asset with default alt when no title or description", () => {
1541
- const node: any = {
1542
- nodeType: BLOCKS.EMBEDDED_ASSET,
1543
- data: {
1544
- target: {
1545
- fields: {
1546
- file: { url: "https://cdn.com/img.png" },
1547
- },
1548
- },
1549
- },
1550
- content: [],
1551
- };
1552
- const doc = makeDoc(node);
1553
- const { result } = renderHook(() => useContentfulRichText(doc));
1554
- const { container } = render(<>{result.current}</>);
1555
- expect(container.querySelector("img")).toHaveAttribute(
1556
- "alt",
1557
- "Embedded asset"
1558
- );
1559
- });
1560
-
1561
- it("renders inline entry with missing fields as empty span", () => {
1562
- const node: any = {
1563
- nodeType: BLOCKS.PARAGRAPH,
1564
- data: {},
1565
- content: [
1566
- {
1567
- nodeType: INLINES.EMBEDDED_ENTRY,
1568
- data: {
1569
- target: {
1570
- sys: { contentType: { sys: { id: "unknown" } } },
1571
- fields: {},
1572
- },
1573
- },
1574
- content: [],
1575
- },
1576
- ],
1577
- };
1578
- const doc = makeDoc(node);
1579
- const { result } = renderHook(() => useContentfulRichText(doc));
1580
- const { container } = render(<>{result.current}</>);
1581
- const spans = container.querySelectorAll("span");
1582
- const emptySpan = Array.from(spans).find(s => s.textContent === "");
1583
- expect(emptySpan).toBeDefined();
1584
- });
1585
-
1586
- // Cover ?. null short-circuit branches in defaultOptions
1587
- it("handles embedded asset with missing target fields gracefully", () => {
1588
- const node: any = {
1589
- nodeType: BLOCKS.EMBEDDED_ASSET,
1590
- data: { target: null },
1591
- content: [],
1592
- };
1593
- const doc = makeDoc(node);
1594
- const { result } = renderHook(() => useContentfulRichText(doc));
1595
- const { container } = render(<>{result.current}</>);
1596
- expect(container.querySelector("img")).not.toBeInTheDocument();
1597
- });
1598
-
1599
- it("handles embedded asset with missing data gracefully", () => {
1600
- const node: any = {
1601
- nodeType: BLOCKS.EMBEDDED_ASSET,
1602
- data: {},
1603
- content: [],
1604
- };
1605
- const doc = makeDoc(node);
1606
- const { result } = renderHook(() => useContentfulRichText(doc));
1607
- const { container } = render(<>{result.current}</>);
1608
- expect(container.querySelector("img")).not.toBeInTheDocument();
1609
- });
1610
-
1611
- it("handles embedded asset with fields but no file", () => {
1612
- const node: any = {
1613
- nodeType: BLOCKS.EMBEDDED_ASSET,
1614
- data: {
1615
- target: {
1616
- fields: { title: "No file" },
1617
- },
1618
- },
1619
- content: [],
1620
- };
1621
- const doc = makeDoc(node);
1622
- const { result } = renderHook(() => useContentfulRichText(doc));
1623
- const { container } = render(<>{result.current}</>);
1624
- expect(container.querySelector("img")).not.toBeInTheDocument();
1625
- });
1626
-
1627
- it("handles embedded entry with missing data target", () => {
1628
- const node: any = {
1629
- nodeType: BLOCKS.EMBEDDED_ENTRY,
1630
- data: { target: null },
1631
- content: [],
1632
- };
1633
- const doc = makeDoc(node);
1634
- const { result } = renderHook(() => useContentfulRichText(doc));
1635
- const { container } = render(<>{result.current}</>);
1636
- expect(container.querySelector("aside")).not.toBeInTheDocument();
1637
- });
1638
-
1639
- it("handles embedded entry with no sys contentType", () => {
1640
- const node: any = {
1641
- nodeType: BLOCKS.EMBEDDED_ENTRY,
1642
- data: {
1643
- target: {
1644
- sys: {},
1645
- fields: { title: "No CT" },
1646
- },
1647
- },
1648
- content: [],
1649
- };
1650
- const doc = makeDoc(node);
1651
- const { result } = renderHook(() => useContentfulRichText(doc));
1652
- const { container } = render(<>{result.current}</>);
1653
- expect(container.querySelector("aside")).not.toBeInTheDocument();
1654
- });
1655
-
1656
- it("handles inline entry with missing data target", () => {
1657
- const node: any = {
1658
- nodeType: BLOCKS.PARAGRAPH,
1659
- data: {},
1660
- content: [
1661
- {
1662
- nodeType: INLINES.EMBEDDED_ENTRY,
1663
- data: { target: null },
1664
- content: [],
1665
- },
1666
- ],
1667
- };
1668
- const doc = makeDoc(node);
1669
- const { result } = renderHook(() => useContentfulRichText(doc));
1670
- const { container } = render(<>{result.current}</>);
1671
- expect(container).toBeInTheDocument();
1672
- });
1673
-
1674
- it("handles inline entry with no sys on target", () => {
1675
- const node: any = {
1676
- nodeType: BLOCKS.PARAGRAPH,
1677
- data: {},
1678
- content: [
1679
- {
1680
- nodeType: INLINES.EMBEDDED_ENTRY,
1681
- data: {
1682
- target: { fields: { title: "No sys" } },
1683
- },
1684
- content: [],
1685
- },
1686
- ],
1687
- };
1688
- const doc = makeDoc(node);
1689
- const { result } = renderHook(() => useContentfulRichText(doc));
1690
- const { container } = render(<>{result.current}</>);
1691
- expect(container).toHaveTextContent("No sys");
1692
- });
1693
-
1694
- it("handles inline entry with no contentType in sys", () => {
1695
- const node: any = {
1696
- nodeType: BLOCKS.PARAGRAPH,
1697
- data: {},
1698
- content: [
1699
- {
1700
- nodeType: INLINES.EMBEDDED_ENTRY,
1701
- data: {
1702
- target: {
1703
- sys: {},
1704
- fields: { title: "No CT" },
1705
- },
1706
- },
1707
- content: [],
1708
- },
1709
- ],
1710
- };
1711
- const doc = makeDoc(node);
1712
- const { result } = renderHook(() => useContentfulRichText(doc));
1713
- const { container } = render(<>{result.current}</>);
1714
- expect(container).toHaveTextContent("No CT");
1715
- });
1716
-
1717
- it("handles inline entry with null fields", () => {
1718
- const node: any = {
1719
- nodeType: BLOCKS.PARAGRAPH,
1720
- data: {},
1721
- content: [
1722
- {
1723
- nodeType: INLINES.EMBEDDED_ENTRY,
1724
- data: {
1725
- target: {
1726
- sys: { contentType: { sys: { id: "other" } } },
1727
- },
1728
- },
1729
- content: [],
1730
- },
1731
- ],
1732
- };
1733
- const doc = makeDoc(node);
1734
- const { result } = renderHook(() => useContentfulRichText(doc));
1735
- const { container } = render(<>{result.current}</>);
1736
- const spans = container.querySelectorAll("span");
1737
- const emptySpan = Array.from(spans).find(s => s.textContent === "");
1738
- expect(emptySpan).toBeDefined();
1739
- });
1740
-
1741
- it("handles hyperlink with missing data", () => {
1742
- const node: any = {
1743
- nodeType: BLOCKS.PARAGRAPH,
1744
- data: {},
1745
- content: [
1746
- {
1747
- nodeType: INLINES.HYPERLINK,
1748
- data: {},
1749
- content: [{ nodeType: "text", value: "Link", marks: [], data: {} }],
1750
- },
1751
- ],
1752
- };
1753
- const doc = makeDoc(node);
1754
- const { result } = renderHook(() => useContentfulRichText(doc));
1755
- const { container } = render(<>{result.current}</>);
1756
- expect(container).toHaveTextContent("Link");
1757
- });
1758
- });
1
+ import {
2
+ renderContentfulRichText,
3
+ renderContentfulRichTextTable,
4
+ useContentfulRichText,
5
+ } from "./use-contentful-rich-text";
6
+
7
+ import {
8
+ BLOCKS,
9
+ INLINES,
10
+ MARKS,
11
+ type Document,
12
+ } from "@contentful/rich-text-types";
13
+ import { render, renderHook, screen } from "@testing-library/react";
14
+
15
+ // Mock dependencies
16
+ jest.mock("@shared/components/text", () => ({
17
+ Text: ({ as: Tag = "span", children, className }: any) => (
18
+ <Tag className={className}>{children}</Tag>
19
+ ),
20
+ }));
21
+
22
+ jest.mock("@shared/components/link", () => ({
23
+ Link: ({ href, children, target, rel, className }: any) => (
24
+ <a href={href} target={target} rel={rel} className={className}>
25
+ {children}
26
+ </a>
27
+ ),
28
+ }));
29
+
30
+ jest.mock("@shared/components/material-icon", () => ({
31
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
32
+ MaterialIcon: ({ name, color, fill }: any) => (
33
+ <span data-testid={`icon-${name}`} data-color={color}>
34
+ {name}
35
+ </span>
36
+ ),
37
+ }));
38
+
39
+ jest.mock("@shared/components/checklist", () => ({
40
+ Checklist: ({ items }: any) => (
41
+ <ul data-testid="checklist">
42
+ {items?.map((item: any, i: number) => (
43
+ <li key={i}>{item}</li>
44
+ ))}
45
+ </ul>
46
+ ),
47
+ }));
48
+
49
+ // Helper to build a Contentful Document
50
+ function makeDoc(...content: any[]): Document {
51
+ return {
52
+ nodeType: BLOCKS.DOCUMENT,
53
+ data: {},
54
+ content,
55
+ };
56
+ }
57
+
58
+ function makeParagraph(text: string): any {
59
+ return {
60
+ nodeType: BLOCKS.PARAGRAPH,
61
+ data: {},
62
+ content: [{ nodeType: "text", value: text, marks: [], data: {} }],
63
+ };
64
+ }
65
+
66
+ function makeHeading(level: number, text: string): any {
67
+ const nodeType = `heading-${level}` as any;
68
+ return {
69
+ nodeType,
70
+ data: {},
71
+ content: [{ nodeType: "text", value: text, marks: [], data: {} }],
72
+ };
73
+ }
74
+
75
+ function makeMarkedText(text: string, markType: string): any {
76
+ return {
77
+ nodeType: BLOCKS.PARAGRAPH,
78
+ data: {},
79
+ content: [
80
+ {
81
+ nodeType: "text",
82
+ value: text,
83
+ marks: [{ type: markType }],
84
+ data: {},
85
+ },
86
+ ],
87
+ };
88
+ }
89
+
90
+ function makeHyperlink(url: string, text: string): any {
91
+ return {
92
+ nodeType: BLOCKS.PARAGRAPH,
93
+ data: {},
94
+ content: [
95
+ {
96
+ nodeType: INLINES.HYPERLINK,
97
+ data: { uri: url },
98
+ content: [{ nodeType: "text", value: text, marks: [], data: {} }],
99
+ },
100
+ ],
101
+ };
102
+ }
103
+
104
+ function makeList(ordered: boolean, items: string[]): any {
105
+ return {
106
+ nodeType: ordered ? BLOCKS.OL_LIST : BLOCKS.UL_LIST,
107
+ data: {},
108
+ content: items.map(item => ({
109
+ nodeType: BLOCKS.LIST_ITEM,
110
+ data: {},
111
+ content: [makeParagraph(item)],
112
+ })),
113
+ };
114
+ }
115
+
116
+ function makeBlockquote(text: string): any {
117
+ return {
118
+ nodeType: BLOCKS.QUOTE,
119
+ data: {},
120
+ content: [makeParagraph(text)],
121
+ };
122
+ }
123
+
124
+ function makeEmbeddedAsset(url: string | undefined, title?: string): any {
125
+ return {
126
+ nodeType: BLOCKS.EMBEDDED_ASSET,
127
+ data: {
128
+ target: url
129
+ ? {
130
+ fields: {
131
+ file: { url },
132
+ title: title ?? "Asset",
133
+ },
134
+ }
135
+ : { fields: {} },
136
+ },
137
+ content: [],
138
+ };
139
+ }
140
+
141
+ function makeEmbeddedEntry(contentTypeId: string, fields: any): any {
142
+ return {
143
+ nodeType: BLOCKS.EMBEDDED_ENTRY,
144
+ data: {
145
+ target: {
146
+ sys: { contentType: { sys: { id: contentTypeId } } },
147
+ fields,
148
+ },
149
+ },
150
+ content: [],
151
+ };
152
+ }
153
+
154
+ function makeInlineEntry(contentTypeId: string, fields: any): any {
155
+ return {
156
+ nodeType: INLINES.EMBEDDED_ENTRY,
157
+ data: {
158
+ target: {
159
+ sys: { contentType: { sys: { id: contentTypeId } } },
160
+ fields,
161
+ },
162
+ },
163
+ content: [],
164
+ };
165
+ }
166
+
167
+ // ──────────────────────────────────────────────
168
+ // renderContentfulRichText
169
+ // ──────────────────────────────────────────────
170
+ describe("renderContentfulRichText", () => {
171
+ describe("Null / invalid input", () => {
172
+ it("returns null for null doc", () => {
173
+ expect(renderContentfulRichText(null)).toBeNull();
174
+ });
175
+
176
+ it("returns null for undefined doc", () => {
177
+ expect(renderContentfulRichText(undefined)).toBeNull();
178
+ });
179
+
180
+ it("returns null for doc without content array", () => {
181
+ const bad = { nodeType: BLOCKS.DOCUMENT, data: {} } as any;
182
+ expect(renderContentfulRichText(bad)).toBeNull();
183
+ });
184
+ });
185
+
186
+ describe("Paragraph rendering", () => {
187
+ it("renders paragraph with default className 'body1'", () => {
188
+ const doc = makeDoc(makeParagraph("Hello world"));
189
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
190
+ const div = container.querySelector("div.body1");
191
+ expect(div).toBeInTheDocument();
192
+ expect(div).toHaveTextContent("Hello world");
193
+ });
194
+
195
+ it("renders paragraph with custom className", () => {
196
+ const doc = makeDoc(makeParagraph("Custom class"));
197
+ const { container } = render(
198
+ <>{renderContentfulRichText(doc, undefined, "custom-class")}</>
199
+ );
200
+ expect(container.querySelector("div.custom-class")).toHaveTextContent(
201
+ "Custom class"
202
+ );
203
+ });
204
+ });
205
+
206
+ describe("Mark rendering", () => {
207
+ it("renders bold text", () => {
208
+ const doc = makeDoc(makeMarkedText("Bold", MARKS.BOLD));
209
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
210
+ const strong = container.querySelector("strong");
211
+ expect(strong).toHaveTextContent("Bold");
212
+ });
213
+
214
+ it("renders italic text", () => {
215
+ const doc = makeDoc(makeMarkedText("Italic", MARKS.ITALIC));
216
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
217
+ expect(container.querySelector("em")).toHaveTextContent("Italic");
218
+ });
219
+
220
+ it("renders underline text", () => {
221
+ const doc = makeDoc(makeMarkedText("Underline", MARKS.UNDERLINE));
222
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
223
+ expect(container.querySelector("u")).toHaveTextContent("Underline");
224
+ });
225
+
226
+ it("renders code text", () => {
227
+ const doc = makeDoc(makeMarkedText("Code", MARKS.CODE));
228
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
229
+ expect(container.querySelector("code")).toHaveTextContent("Code");
230
+ });
231
+ });
232
+
233
+ describe("Hyperlink rendering", () => {
234
+ it("renders external link with target=_blank when target is not specified", () => {
235
+ const doc = makeDoc(makeHyperlink("https://example.com", "Click"));
236
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
237
+ const a = container.querySelector("a")!;
238
+ expect(a).toHaveAttribute("href", "https://example.com");
239
+ expect(a).toHaveAttribute("target", "_blank");
240
+ expect(a).toHaveAttribute("rel", "noopener noreferrer");
241
+ });
242
+
243
+ it("renders internal link with target=_self for relative URLs", () => {
244
+ const doc = makeDoc(makeHyperlink("/about", "About"));
245
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
246
+ const a = container.querySelector("a")!;
247
+ expect(a).toHaveAttribute("target", "_self");
248
+ expect(a).not.toHaveAttribute("rel");
249
+ });
250
+
251
+ it("respects target=true flag to force _blank", () => {
252
+ const doc = makeDoc(makeHyperlink("/local", "Local"));
253
+ const { container } = render(<>{renderContentfulRichText(doc, true)}</>);
254
+ const a = container.querySelector("a")!;
255
+ expect(a).toHaveAttribute("target", "_blank");
256
+ });
257
+
258
+ it("respects target=false flag to force _self", () => {
259
+ const doc = makeDoc(makeHyperlink("https://example.com", "External"));
260
+ const { container } = render(<>{renderContentfulRichText(doc, false)}</>);
261
+ const a = container.querySelector("a")!;
262
+ expect(a).toHaveAttribute("target", "_self");
263
+ });
264
+
265
+ it("applies custom linkClassName", () => {
266
+ const doc = makeDoc(makeHyperlink("https://x.com", "Link"));
267
+ const { container } = render(
268
+ <>{renderContentfulRichText(doc, undefined, "body1", "custom-link")}</>
269
+ );
270
+ const a = container.querySelector("a")!;
271
+ expect(a).toHaveClass("custom-link");
272
+ });
273
+ });
274
+
275
+ describe("Custom options merge", () => {
276
+ it("merges custom renderNode options", () => {
277
+ const doc = makeDoc(makeParagraph("Custom"));
278
+ const customOptions = {
279
+ renderNode: {
280
+ [BLOCKS.PARAGRAPH]: (_node: any, children: any) => (
281
+ <p data-testid="custom-p">{children}</p>
282
+ ),
283
+ },
284
+ };
285
+ render(
286
+ <>
287
+ {renderContentfulRichText(
288
+ doc,
289
+ undefined,
290
+ "body1",
291
+ "body1 font-bold",
292
+ customOptions
293
+ )}
294
+ </>
295
+ );
296
+ // The merged PARAGRAPH comes from the function's own override, not from customOptions
297
+ // because the function re-defines BLOCKS.PARAGRAPH after spreading options.renderNode
298
+ // The function's PARAGRAPH override wins.
299
+ expect(screen.queryByTestId("custom-p")).not.toBeInTheDocument();
300
+ });
301
+ });
302
+
303
+ describe("Inline entry rendering", () => {
304
+ it("renders componentCheckList inline entry via defaultOptions", () => {
305
+ const doc = makeDoc({
306
+ nodeType: BLOCKS.PARAGRAPH,
307
+ data: {},
308
+ content: [
309
+ makeInlineEntry("componentCheckList", { title: "Check Item" }),
310
+ ],
311
+ });
312
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
313
+ expect(container).toHaveTextContent("Check Item");
314
+ });
315
+
316
+ it("renders unknown inline entry title via defaultOptions", () => {
317
+ const doc = makeDoc({
318
+ nodeType: BLOCKS.PARAGRAPH,
319
+ data: {},
320
+ content: [makeInlineEntry("someOtherType", { title: "Fallback" })],
321
+ });
322
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
323
+ expect(container).toHaveTextContent("Fallback");
324
+ });
325
+
326
+ it("renders unknown inline entry with missing title as empty", () => {
327
+ const doc = makeDoc({
328
+ nodeType: BLOCKS.PARAGRAPH,
329
+ data: {},
330
+ content: [makeInlineEntry("someOtherType", {})],
331
+ });
332
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
333
+ const spans = container.querySelectorAll("span");
334
+ const emptySpan = Array.from(spans).find(s => s.textContent === "");
335
+ expect(emptySpan).toBeDefined();
336
+ });
337
+ });
338
+
339
+ describe("Embedded entry rendering via defaultOptions", () => {
340
+ it("renders callout block entry", () => {
341
+ const doc = makeDoc(
342
+ makeEmbeddedEntry("callout", {
343
+ title: "Note",
344
+ body: "Important info",
345
+ })
346
+ );
347
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
348
+ expect(container.querySelector("aside")).toBeInTheDocument();
349
+ expect(container).toHaveTextContent("Note");
350
+ });
351
+
352
+ it("returns null for unknown block entry type", () => {
353
+ const doc = makeDoc(makeEmbeddedEntry("unknownType", { title: "X" }));
354
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
355
+ expect(container.querySelector("aside")).not.toBeInTheDocument();
356
+ });
357
+ });
358
+
359
+ describe("Embedded asset via defaultOptions", () => {
360
+ it("renders image with protocol-relative URL", () => {
361
+ const doc = makeDoc(
362
+ makeEmbeddedAsset("//images.ctfl.net/photo.jpg", "Photo")
363
+ );
364
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
365
+ const img = container.querySelector("img")!;
366
+ expect(img).toHaveAttribute("src", "https://images.ctfl.net/photo.jpg");
367
+ });
368
+
369
+ it("uses description as alt when title is missing", () => {
370
+ const node: any = {
371
+ nodeType: BLOCKS.EMBEDDED_ASSET,
372
+ data: {
373
+ target: {
374
+ fields: {
375
+ file: { url: "https://cdn.com/i.png" },
376
+ description: "Desc text",
377
+ },
378
+ },
379
+ },
380
+ content: [],
381
+ };
382
+ const doc = makeDoc(node);
383
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
384
+ expect(container.querySelector("img")).toHaveAttribute(
385
+ "alt",
386
+ "Desc text"
387
+ );
388
+ });
389
+
390
+ it("uses default alt when no title or description", () => {
391
+ const node: any = {
392
+ nodeType: BLOCKS.EMBEDDED_ASSET,
393
+ data: {
394
+ target: {
395
+ fields: {
396
+ file: { url: "https://cdn.com/i.png" },
397
+ },
398
+ },
399
+ },
400
+ content: [],
401
+ };
402
+ const doc = makeDoc(node);
403
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
404
+ expect(container.querySelector("img")).toHaveAttribute(
405
+ "alt",
406
+ "Embedded asset"
407
+ );
408
+ });
409
+
410
+ it("uses target.url fallback when file.url is missing", () => {
411
+ const node: any = {
412
+ nodeType: BLOCKS.EMBEDDED_ASSET,
413
+ data: {
414
+ target: {
415
+ url: "https://direct.com/img.png",
416
+ fields: {},
417
+ },
418
+ },
419
+ content: [],
420
+ };
421
+ const doc = makeDoc(node);
422
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
423
+ expect(container.querySelector("img")).toHaveAttribute(
424
+ "src",
425
+ "https://direct.com/img.png"
426
+ );
427
+ });
428
+
429
+ it("returns null when no url available", () => {
430
+ const doc = makeDoc(makeEmbeddedAsset(undefined));
431
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
432
+ expect(container.querySelector("img")).not.toBeInTheDocument();
433
+ });
434
+ });
435
+
436
+ describe("HYPERLINK via defaultOptions in renderContentfulRichText", () => {
437
+ it("renders external link with _blank via default HYPERLINK", () => {
438
+ // When target param is not set, renderContentfulRichText uses its own HYPERLINK handler
439
+ // But defaultOptions.HYPERLINK is still in the spread, though overridden
440
+ // This tests renderContentfulRichText's own HYPERLINK handler
441
+ const doc = makeDoc(makeHyperlink("https://ext.com", "Ext"));
442
+ const { container } = render(
443
+ <>{renderContentfulRichText(doc, undefined)}</>
444
+ );
445
+ const a = container.querySelector("a")!;
446
+ expect(a).toHaveAttribute("target", "_blank");
447
+ });
448
+
449
+ it("renders internal link with _self when target is undefined", () => {
450
+ const doc = makeDoc(makeHyperlink("/page", "Page"));
451
+ const { container } = render(
452
+ <>{renderContentfulRichText(doc, undefined)}</>
453
+ );
454
+ const a = container.querySelector("a")!;
455
+ expect(a).toHaveAttribute("target", "_self");
456
+ });
457
+
458
+ it("handles hyperlink with missing uri data", () => {
459
+ const node: any = {
460
+ nodeType: BLOCKS.PARAGRAPH,
461
+ data: {},
462
+ content: [
463
+ {
464
+ nodeType: INLINES.HYPERLINK,
465
+ data: {},
466
+ content: [
467
+ { nodeType: "text", value: "No URI", marks: [], data: {} },
468
+ ],
469
+ },
470
+ ],
471
+ };
472
+ const doc = makeDoc(node);
473
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
474
+ expect(container).toHaveTextContent("No URI");
475
+ });
476
+
477
+ it("handles hyperlink with null data", () => {
478
+ const node: any = {
479
+ nodeType: BLOCKS.PARAGRAPH,
480
+ data: {},
481
+ content: [
482
+ {
483
+ nodeType: INLINES.HYPERLINK,
484
+ data: null,
485
+ content: [
486
+ { nodeType: "text", value: "Null data", marks: [], data: {} },
487
+ ],
488
+ },
489
+ ],
490
+ };
491
+ const doc = makeDoc(node);
492
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
493
+ expect(container).toHaveTextContent("Null data");
494
+ });
495
+ });
496
+
497
+ describe("Superscript and subscript marks", () => {
498
+ it("renders superscript text", () => {
499
+ const doc = makeDoc(makeMarkedText("Sup", MARKS.SUPERSCRIPT));
500
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
501
+ expect(container.querySelector("sup")).toHaveTextContent("Sup");
502
+ });
503
+
504
+ it("renders subscript text", () => {
505
+ const doc = makeDoc(makeMarkedText("Sub", MARKS.SUBSCRIPT));
506
+ const { container } = render(<>{renderContentfulRichText(doc)}</>);
507
+ expect(container.querySelector("sub")).toHaveTextContent("Sub");
508
+ });
509
+ });
510
+ });
511
+
512
+ // ──────────────────────────────────────────────
513
+ // useContentfulRichText
514
+ // ──────────────────────────────────────────────
515
+ describe("useContentfulRichText", () => {
516
+ it("returns null for null doc", () => {
517
+ const { result } = renderHook(() => useContentfulRichText(null));
518
+ expect(result.current).toBeNull();
519
+ });
520
+
521
+ it("returns null for undefined doc", () => {
522
+ const { result } = renderHook(() => useContentfulRichText(undefined));
523
+ expect(result.current).toBeNull();
524
+ });
525
+
526
+ it("returns null for doc without content array", () => {
527
+ const bad = { nodeType: BLOCKS.DOCUMENT, data: {} } as any;
528
+ const { result } = renderHook(() => useContentfulRichText(bad));
529
+ expect(result.current).toBeNull();
530
+ });
531
+
532
+ it("renders paragraph content", () => {
533
+ const doc = makeDoc(makeParagraph("Hook test"));
534
+ const { result } = renderHook(() => useContentfulRichText(doc));
535
+ const { container } = render(<>{result.current}</>);
536
+ expect(container).toHaveTextContent("Hook test");
537
+ });
538
+
539
+ it("renders headings h1-h6", () => {
540
+ const doc = makeDoc(
541
+ makeHeading(1, "H1"),
542
+ makeHeading(2, "H2"),
543
+ makeHeading(3, "H3"),
544
+ makeHeading(4, "H4"),
545
+ makeHeading(5, "H5"),
546
+ makeHeading(6, "H6")
547
+ );
548
+ const { result } = renderHook(() => useContentfulRichText(doc));
549
+ const { container } = render(<>{result.current}</>);
550
+ expect(container.querySelector("h1")).toHaveTextContent("H1");
551
+ expect(container.querySelector("h2")).toHaveTextContent("H2");
552
+ // h3-h6 all render as h3 per defaultOptions
553
+ const h3s = container.querySelectorAll("h3");
554
+ expect(h3s.length).toBe(4);
555
+ });
556
+
557
+ it("renders unordered list", () => {
558
+ const doc = makeDoc(makeList(false, ["Item A", "Item B"]));
559
+ const { result } = renderHook(() => useContentfulRichText(doc));
560
+ const { container } = render(<>{result.current}</>);
561
+ expect(container.querySelector("ul")).toBeInTheDocument();
562
+ expect(container.querySelectorAll("li")).toHaveLength(2);
563
+ });
564
+
565
+ it("renders ordered list", () => {
566
+ const doc = makeDoc(makeList(true, ["First", "Second"]));
567
+ const { result } = renderHook(() => useContentfulRichText(doc));
568
+ const { container } = render(<>{result.current}</>);
569
+ expect(container.querySelector("ol")).toBeInTheDocument();
570
+ });
571
+
572
+ it("renders blockquote", () => {
573
+ const doc = makeDoc(makeBlockquote("Quote text"));
574
+ const { result } = renderHook(() => useContentfulRichText(doc));
575
+ const { container } = render(<>{result.current}</>);
576
+ expect(container.querySelector("blockquote")).toHaveTextContent(
577
+ "Quote text"
578
+ );
579
+ });
580
+
581
+ it("renders hyperlink with external target", () => {
582
+ const doc = makeDoc(makeHyperlink("https://ext.com", "External"));
583
+ const { result } = renderHook(() => useContentfulRichText(doc));
584
+ const { container } = render(<>{result.current}</>);
585
+ const a = container.querySelector("a")!;
586
+ expect(a).toHaveAttribute("target", "_blank");
587
+ });
588
+
589
+ it("renders hyperlink with internal target", () => {
590
+ const doc = makeDoc(makeHyperlink("/page", "Internal"));
591
+ const { result } = renderHook(() => useContentfulRichText(doc));
592
+ const { container } = render(<>{result.current}</>);
593
+ const a = container.querySelector("a")!;
594
+ expect(a).not.toHaveAttribute("target");
595
+ });
596
+
597
+ it("renders embedded asset with image", () => {
598
+ const doc = makeDoc(
599
+ makeEmbeddedAsset("//images.ctfl.net/photo.jpg", "Photo")
600
+ );
601
+ const { result } = renderHook(() => useContentfulRichText(doc));
602
+ const { container } = render(<>{result.current}</>);
603
+ const img = container.querySelector("img")!;
604
+ expect(img).toHaveAttribute("src", "https://images.ctfl.net/photo.jpg");
605
+ expect(img).toHaveAttribute("alt", "Photo");
606
+ });
607
+
608
+ it("renders embedded asset with absolute URL", () => {
609
+ const doc = makeDoc(makeEmbeddedAsset("https://cdn.example.com/img.png"));
610
+ const { result } = renderHook(() => useContentfulRichText(doc));
611
+ const { container } = render(<>{result.current}</>);
612
+ const img = container.querySelector("img")!;
613
+ expect(img).toHaveAttribute("src", "https://cdn.example.com/img.png");
614
+ });
615
+
616
+ it("returns null for embedded asset without URL", () => {
617
+ const doc = makeDoc(makeEmbeddedAsset(undefined));
618
+ const { result } = renderHook(() => useContentfulRichText(doc));
619
+ const { container } = render(<>{result.current}</>);
620
+ expect(container.querySelector("img")).not.toBeInTheDocument();
621
+ });
622
+
623
+ it("renders embedded asset using target.url when fields.file.url is missing", () => {
624
+ const node: any = {
625
+ nodeType: BLOCKS.EMBEDDED_ASSET,
626
+ data: {
627
+ target: {
628
+ url: "https://direct.com/image.png",
629
+ fields: { title: "Direct" },
630
+ },
631
+ },
632
+ content: [],
633
+ };
634
+ const doc = makeDoc(node);
635
+ const { result } = renderHook(() => useContentfulRichText(doc));
636
+ const { container } = render(<>{result.current}</>);
637
+ expect(container.querySelector("img")).toHaveAttribute(
638
+ "src",
639
+ "https://direct.com/image.png"
640
+ );
641
+ });
642
+
643
+ it("renders embedded entry with callout type", () => {
644
+ const doc = makeDoc(
645
+ makeEmbeddedEntry("callout", { title: "Notice", body: "Important info" })
646
+ );
647
+ const { result } = renderHook(() => useContentfulRichText(doc));
648
+ const { container } = render(<>{result.current}</>);
649
+ expect(container.querySelector("aside")).toBeInTheDocument();
650
+ expect(container).toHaveTextContent("Notice");
651
+ expect(container).toHaveTextContent("Important info");
652
+ });
653
+
654
+ it("returns null for embedded entry with unknown type", () => {
655
+ const doc = makeDoc(makeEmbeddedEntry("unknown", { title: "X" }));
656
+ const { result } = renderHook(() => useContentfulRichText(doc));
657
+ const { container } = render(<>{result.current}</>);
658
+ expect(container.querySelector("aside")).not.toBeInTheDocument();
659
+ });
660
+
661
+ it("renders inline entry with componentCheckList type", () => {
662
+ const doc = makeDoc({
663
+ nodeType: BLOCKS.PARAGRAPH,
664
+ data: {},
665
+ content: [
666
+ makeInlineEntry("componentCheckList", { title: "Checklist Item" }),
667
+ ],
668
+ });
669
+ const { result } = renderHook(() => useContentfulRichText(doc));
670
+ const { container } = render(<>{result.current}</>);
671
+ expect(container).toHaveTextContent("Checklist Item");
672
+ });
673
+
674
+ it("renders inline entry with unknown type showing title", () => {
675
+ const doc = makeDoc({
676
+ nodeType: BLOCKS.PARAGRAPH,
677
+ data: {},
678
+ content: [makeInlineEntry("other", { title: "Other Entry" })],
679
+ });
680
+ const { result } = renderHook(() => useContentfulRichText(doc));
681
+ const { container } = render(<>{result.current}</>);
682
+ expect(container).toHaveTextContent("Other Entry");
683
+ });
684
+
685
+ it("renders inline entry with unknown type and missing title as empty", () => {
686
+ const doc = makeDoc({
687
+ nodeType: BLOCKS.PARAGRAPH,
688
+ data: {},
689
+ content: [makeInlineEntry("other", {})],
690
+ });
691
+ const { result } = renderHook(() => useContentfulRichText(doc));
692
+ const { container } = render(<>{result.current}</>);
693
+ const spans = container.querySelectorAll("span");
694
+ const emptySpan = Array.from(spans).find(s => s.textContent === "");
695
+ expect(emptySpan).toBeDefined();
696
+ });
697
+
698
+ it("merges custom mark options", () => {
699
+ const doc = makeDoc(makeMarkedText("Custom bold", MARKS.BOLD));
700
+ const customOpts = {
701
+ renderMark: {
702
+ [MARKS.BOLD]: (text: any) => <b data-testid="custom-bold">{text}</b>,
703
+ },
704
+ };
705
+ const { result } = renderHook(() => useContentfulRichText(doc, customOpts));
706
+ render(<>{result.current}</>);
707
+ expect(screen.getByTestId("custom-bold")).toHaveTextContent("Custom bold");
708
+ });
709
+
710
+ it("memoizes result for same doc reference", () => {
711
+ const doc = makeDoc(makeParagraph("Memoized"));
712
+ const { result, rerender } = renderHook(
713
+ ({ d }) => useContentfulRichText(d),
714
+ { initialProps: { d: doc } }
715
+ );
716
+ const first = result.current;
717
+ rerender({ d: doc });
718
+ expect(result.current).toBe(first);
719
+ });
720
+
721
+ it("recomputes when doc changes", () => {
722
+ const doc1 = makeDoc(makeParagraph("First"));
723
+ const doc2 = makeDoc(makeParagraph("Second"));
724
+ const { result, rerender } = renderHook(
725
+ ({ d }) => useContentfulRichText(d),
726
+ { initialProps: { d: doc1 as Document | null } }
727
+ );
728
+ const first = result.current;
729
+ rerender({ d: doc2 });
730
+ expect(result.current).not.toBe(first);
731
+ });
732
+
733
+ it("transitions from null doc to valid doc", () => {
734
+ const { result, rerender } = renderHook(
735
+ ({ d }) => useContentfulRichText(d),
736
+ { initialProps: { d: null as Document | null } }
737
+ );
738
+ expect(result.current).toBeNull();
739
+ rerender({ d: makeDoc(makeParagraph("Now visible")) });
740
+ const { container } = render(<>{result.current}</>);
741
+ expect(container).toHaveTextContent("Now visible");
742
+ });
743
+ });
744
+
745
+ // ──────────────────────────────────────────────
746
+ // renderContentfulRichTextTable
747
+ // ──────────────────────────────────────────────
748
+ describe("renderContentfulRichTextTable", () => {
749
+ it("returns null for null doc", () => {
750
+ expect(renderContentfulRichTextTable(null)).toBeNull();
751
+ });
752
+
753
+ it("returns null for undefined doc", () => {
754
+ expect(renderContentfulRichTextTable(undefined)).toBeNull();
755
+ });
756
+
757
+ it("returns null for doc without content array", () => {
758
+ const bad = { nodeType: BLOCKS.DOCUMENT, data: {} } as any;
759
+ expect(renderContentfulRichTextTable(bad)).toBeNull();
760
+ });
761
+
762
+ it("renders table with rows", () => {
763
+ const doc = makeDoc({
764
+ nodeType: BLOCKS.TABLE,
765
+ data: {},
766
+ content: [
767
+ {
768
+ nodeType: BLOCKS.TABLE_ROW,
769
+ data: {},
770
+ content: [
771
+ {
772
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
773
+ data: {},
774
+ content: [makeParagraph("Header")],
775
+ },
776
+ {
777
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
778
+ data: {},
779
+ content: [makeParagraph("Header 2")],
780
+ },
781
+ ],
782
+ },
783
+ {
784
+ nodeType: BLOCKS.TABLE_ROW,
785
+ data: {},
786
+ content: [
787
+ {
788
+ nodeType: BLOCKS.TABLE_CELL,
789
+ data: {},
790
+ content: [makeParagraph("Cell 1")],
791
+ },
792
+ {
793
+ nodeType: BLOCKS.TABLE_CELL,
794
+ data: {},
795
+ content: [makeParagraph("Cell 2")],
796
+ },
797
+ ],
798
+ },
799
+ ],
800
+ });
801
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
802
+ expect(container.querySelector("table")).toBeInTheDocument();
803
+ expect(container.querySelectorAll("th")).toHaveLength(2);
804
+ expect(container.querySelectorAll("td")).toHaveLength(2);
805
+ });
806
+
807
+ it("renders check_circle icon for 'yes' cell value", () => {
808
+ const doc = makeDoc({
809
+ nodeType: BLOCKS.TABLE,
810
+ data: {},
811
+ content: [
812
+ {
813
+ nodeType: BLOCKS.TABLE_ROW,
814
+ data: {},
815
+ content: [
816
+ {
817
+ nodeType: BLOCKS.TABLE_CELL,
818
+ data: {},
819
+ content: [makeParagraph("yes")],
820
+ },
821
+ ],
822
+ },
823
+ ],
824
+ });
825
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
826
+ expect(
827
+ container.querySelector('[data-testid="icon-check_circle"]')
828
+ ).toBeInTheDocument();
829
+ });
830
+
831
+ it("renders cancel icon for 'no' cell value", () => {
832
+ const doc = makeDoc({
833
+ nodeType: BLOCKS.TABLE,
834
+ data: {},
835
+ content: [
836
+ {
837
+ nodeType: BLOCKS.TABLE_ROW,
838
+ data: {},
839
+ content: [
840
+ {
841
+ nodeType: BLOCKS.TABLE_CELL,
842
+ data: {},
843
+ content: [makeParagraph("no")],
844
+ },
845
+ ],
846
+ },
847
+ ],
848
+ });
849
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
850
+ expect(
851
+ container.querySelector('[data-testid="icon-cancel"]')
852
+ ).toBeInTheDocument();
853
+ });
854
+
855
+ it("renders text content for non-yes/no cell values", () => {
856
+ const doc = makeDoc({
857
+ nodeType: BLOCKS.TABLE,
858
+ data: {},
859
+ content: [
860
+ {
861
+ nodeType: BLOCKS.TABLE_ROW,
862
+ data: {},
863
+ content: [
864
+ {
865
+ nodeType: BLOCKS.TABLE_CELL,
866
+ data: {},
867
+ content: [makeParagraph("Custom text")],
868
+ },
869
+ ],
870
+ },
871
+ ],
872
+ });
873
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
874
+ expect(container).toHaveTextContent("Custom text");
875
+ expect(
876
+ container.querySelector('[data-testid="icon-check_circle"]')
877
+ ).not.toBeInTheDocument();
878
+ expect(
879
+ container.querySelector('[data-testid="icon-cancel"]')
880
+ ).not.toBeInTheDocument();
881
+ });
882
+
883
+ it("renders bold with table-specific class", () => {
884
+ const doc = makeDoc(makeMarkedText("Table bold", MARKS.BOLD));
885
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
886
+ const strong = container.querySelector("strong")!;
887
+ expect(strong).toHaveClass("label4");
888
+ expect(strong).toHaveClass("md:label2");
889
+ });
890
+
891
+ it("renders paragraph as fragment (no wrapper div)", () => {
892
+ const doc = makeDoc(makeParagraph("Fragment text"));
893
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
894
+ // Should NOT have a div.body3 wrapper like the default options
895
+ expect(container.querySelector("div.body3")).not.toBeInTheDocument();
896
+ expect(container).toHaveTextContent("Fragment text");
897
+ });
898
+
899
+ it("renders inline embedded checklist entry from links", () => {
900
+ const entryId = "entry-123";
901
+ const doc = makeDoc({
902
+ nodeType: BLOCKS.PARAGRAPH,
903
+ data: {},
904
+ content: [
905
+ {
906
+ nodeType: INLINES.EMBEDDED_ENTRY,
907
+ data: { target: { sys: { id: entryId } } },
908
+ content: [],
909
+ },
910
+ ],
911
+ });
912
+ const links = {
913
+ entries: {
914
+ inline: [
915
+ {
916
+ sys: { id: entryId },
917
+ __typename: "ComponentCheckList",
918
+ list: {
919
+ items: [{ checkListTitle: null }],
920
+ },
921
+ },
922
+ ],
923
+ },
924
+ };
925
+ const { container } = render(
926
+ <>{renderContentfulRichTextTable(doc, links)}</>
927
+ );
928
+ expect(
929
+ container.querySelector('[data-testid="checklist"]')
930
+ ).toBeInTheDocument();
931
+ });
932
+
933
+ it("renders inline entry title for non-checklist type", () => {
934
+ const entryId = "entry-456";
935
+ const doc = makeDoc({
936
+ nodeType: BLOCKS.PARAGRAPH,
937
+ data: {},
938
+ content: [
939
+ {
940
+ nodeType: INLINES.EMBEDDED_ENTRY,
941
+ data: { target: { sys: { id: entryId } } },
942
+ content: [],
943
+ },
944
+ ],
945
+ });
946
+ const links = {
947
+ entries: {
948
+ inline: [
949
+ {
950
+ sys: { id: entryId },
951
+ __typename: "OtherType",
952
+ title: "Other Title",
953
+ },
954
+ ],
955
+ },
956
+ };
957
+ const { container } = render(
958
+ <>{renderContentfulRichTextTable(doc, links)}</>
959
+ );
960
+ expect(container).toHaveTextContent("Other Title");
961
+ });
962
+
963
+ it("returns null for inline entry not found in links", () => {
964
+ const doc = makeDoc({
965
+ nodeType: BLOCKS.PARAGRAPH,
966
+ data: {},
967
+ content: [
968
+ {
969
+ nodeType: INLINES.EMBEDDED_ENTRY,
970
+ data: { target: { sys: { id: "missing" } } },
971
+ content: [],
972
+ },
973
+ ],
974
+ });
975
+ const { container } = render(
976
+ <>{renderContentfulRichTextTable(doc, { entries: { inline: [] } })}</>
977
+ );
978
+ // Should render the paragraph wrapper but no entry content
979
+ expect(container.textContent).toBe("");
980
+ });
981
+
982
+ it("renders scrollable table with >2 columns", () => {
983
+ const headerRow = {
984
+ nodeType: BLOCKS.TABLE_ROW,
985
+ data: {},
986
+ content: [
987
+ {
988
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
989
+ data: {},
990
+ content: [makeParagraph("A")],
991
+ },
992
+ {
993
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
994
+ data: {},
995
+ content: [makeParagraph("B")],
996
+ },
997
+ {
998
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
999
+ data: {},
1000
+ content: [makeParagraph("C")],
1001
+ },
1002
+ ],
1003
+ };
1004
+ const doc = makeDoc({
1005
+ nodeType: BLOCKS.TABLE,
1006
+ data: {},
1007
+ content: [headerRow],
1008
+ });
1009
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1010
+ expect(container.querySelector("table")?.className).toContain(
1011
+ "min-w-[100.1%]"
1012
+ );
1013
+ });
1014
+
1015
+ it("renders non-scrollable table with <=2 columns", () => {
1016
+ const headerRow = {
1017
+ nodeType: BLOCKS.TABLE_ROW,
1018
+ data: {},
1019
+ content: [
1020
+ {
1021
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
1022
+ data: {},
1023
+ content: [makeParagraph("A")],
1024
+ },
1025
+ {
1026
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
1027
+ data: {},
1028
+ content: [makeParagraph("B")],
1029
+ },
1030
+ ],
1031
+ };
1032
+ const doc = makeDoc({
1033
+ nodeType: BLOCKS.TABLE,
1034
+ data: {},
1035
+ content: [headerRow],
1036
+ });
1037
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1038
+ expect(container.querySelector("table")?.className).toContain("min-w-full");
1039
+ });
1040
+
1041
+ it("renders table cells with non-scrollable layout (node.parent not available)", () => {
1042
+ const doc = makeDoc({
1043
+ nodeType: BLOCKS.TABLE,
1044
+ data: {},
1045
+ content: [
1046
+ {
1047
+ nodeType: BLOCKS.TABLE_ROW,
1048
+ data: {},
1049
+ content: [
1050
+ {
1051
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
1052
+ data: {},
1053
+ content: [makeParagraph("H1")],
1054
+ },
1055
+ {
1056
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
1057
+ data: {},
1058
+ content: [makeParagraph("H2")],
1059
+ },
1060
+ {
1061
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
1062
+ data: {},
1063
+ content: [makeParagraph("H3")],
1064
+ },
1065
+ ],
1066
+ },
1067
+ {
1068
+ nodeType: BLOCKS.TABLE_ROW,
1069
+ data: {},
1070
+ content: [
1071
+ {
1072
+ nodeType: BLOCKS.TABLE_CELL,
1073
+ data: {},
1074
+ content: [makeParagraph("C1")],
1075
+ },
1076
+ {
1077
+ nodeType: BLOCKS.TABLE_CELL,
1078
+ data: {},
1079
+ content: [makeParagraph("C2")],
1080
+ },
1081
+ {
1082
+ nodeType: BLOCKS.TABLE_CELL,
1083
+ data: {},
1084
+ content: [makeParagraph("C3")],
1085
+ },
1086
+ ],
1087
+ },
1088
+ ],
1089
+ });
1090
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1091
+ const tds = container.querySelectorAll("td");
1092
+ expect(tds.length).toBe(3);
1093
+ // node.parent is not set by documentToReactComponents, so isScrollable is always false
1094
+ tds.forEach(td => {
1095
+ expect(td.className).toContain("w-1/4");
1096
+ });
1097
+ });
1098
+
1099
+ it("renders non-scrollable table cells with w-1/4 for <=2 columns", () => {
1100
+ const doc = makeDoc({
1101
+ nodeType: BLOCKS.TABLE,
1102
+ data: {},
1103
+ content: [
1104
+ {
1105
+ nodeType: BLOCKS.TABLE_ROW,
1106
+ data: {},
1107
+ content: [
1108
+ {
1109
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
1110
+ data: {},
1111
+ content: [makeParagraph("H1")],
1112
+ },
1113
+ {
1114
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
1115
+ data: {},
1116
+ content: [makeParagraph("H2")],
1117
+ },
1118
+ ],
1119
+ },
1120
+ {
1121
+ nodeType: BLOCKS.TABLE_ROW,
1122
+ data: {},
1123
+ content: [
1124
+ {
1125
+ nodeType: BLOCKS.TABLE_CELL,
1126
+ data: {},
1127
+ content: [makeParagraph("D1")],
1128
+ },
1129
+ {
1130
+ nodeType: BLOCKS.TABLE_CELL,
1131
+ data: {},
1132
+ content: [makeParagraph("D2")],
1133
+ },
1134
+ ],
1135
+ },
1136
+ ],
1137
+ });
1138
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1139
+ const tds = container.querySelectorAll("td");
1140
+ tds.forEach(td => {
1141
+ expect(td.className).toContain("w-1/4");
1142
+ });
1143
+ });
1144
+
1145
+ it("renders checklist with empty items when list has no items", () => {
1146
+ const entryId = "entry-empty";
1147
+ const doc = makeDoc({
1148
+ nodeType: BLOCKS.PARAGRAPH,
1149
+ data: {},
1150
+ content: [
1151
+ {
1152
+ nodeType: INLINES.EMBEDDED_ENTRY,
1153
+ data: { target: { sys: { id: entryId } } },
1154
+ content: [],
1155
+ },
1156
+ ],
1157
+ });
1158
+ const links = {
1159
+ entries: {
1160
+ inline: [
1161
+ {
1162
+ sys: { id: entryId },
1163
+ __typename: "ComponentCheckList",
1164
+ list: undefined,
1165
+ },
1166
+ ],
1167
+ },
1168
+ };
1169
+ const { container } = render(
1170
+ <>{renderContentfulRichTextTable(doc, links)}</>
1171
+ );
1172
+ expect(
1173
+ container.querySelector('[data-testid="checklist"]')
1174
+ ).toBeInTheDocument();
1175
+ });
1176
+
1177
+ it("renders non-checklist inline entry with empty title", () => {
1178
+ const entryId = "entry-no-title";
1179
+ const doc = makeDoc({
1180
+ nodeType: BLOCKS.PARAGRAPH,
1181
+ data: {},
1182
+ content: [
1183
+ {
1184
+ nodeType: INLINES.EMBEDDED_ENTRY,
1185
+ data: { target: { sys: { id: entryId } } },
1186
+ content: [],
1187
+ },
1188
+ ],
1189
+ });
1190
+ const links = {
1191
+ entries: {
1192
+ inline: [
1193
+ {
1194
+ sys: { id: entryId },
1195
+ __typename: "OtherType",
1196
+ title: "",
1197
+ },
1198
+ ],
1199
+ },
1200
+ };
1201
+ const { container } = render(
1202
+ <>{renderContentfulRichTextTable(doc, links)}</>
1203
+ );
1204
+ const span = container.querySelector("span");
1205
+ expect(span?.textContent).toBe("");
1206
+ });
1207
+
1208
+ it("renders scrollable table header cells when node.parent has >2 children", () => {
1209
+ // Build row with 3 header cells and set parent on each cell
1210
+ const headerCells = [
1211
+ {
1212
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
1213
+ data: {},
1214
+ content: [makeParagraph("H1")],
1215
+ },
1216
+ {
1217
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
1218
+ data: {},
1219
+ content: [makeParagraph("H2")],
1220
+ },
1221
+ {
1222
+ nodeType: BLOCKS.TABLE_HEADER_CELL,
1223
+ data: {},
1224
+ content: [makeParagraph("H3")],
1225
+ },
1226
+ ];
1227
+ const row = {
1228
+ nodeType: BLOCKS.TABLE_ROW,
1229
+ data: {},
1230
+ content: headerCells,
1231
+ };
1232
+ // Set parent on each cell so isScrollable = true
1233
+ headerCells.forEach(cell => {
1234
+ (cell as any).parent = row;
1235
+ });
1236
+ const doc = makeDoc({
1237
+ nodeType: BLOCKS.TABLE,
1238
+ data: {},
1239
+ content: [row],
1240
+ });
1241
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1242
+ const ths = container.querySelectorAll("th");
1243
+ expect(ths.length).toBe(3);
1244
+ ths.forEach(th => {
1245
+ expect(th.className).toContain("sticky");
1246
+ });
1247
+ });
1248
+
1249
+ it("renders scrollable table data cells when node.parent has >2 children", () => {
1250
+ const dataCells = [
1251
+ {
1252
+ nodeType: BLOCKS.TABLE_CELL,
1253
+ data: {},
1254
+ content: [makeParagraph("D1")],
1255
+ },
1256
+ {
1257
+ nodeType: BLOCKS.TABLE_CELL,
1258
+ data: {},
1259
+ content: [makeParagraph("D2")],
1260
+ },
1261
+ {
1262
+ nodeType: BLOCKS.TABLE_CELL,
1263
+ data: {},
1264
+ content: [makeParagraph("D3")],
1265
+ },
1266
+ ];
1267
+ const row = {
1268
+ nodeType: BLOCKS.TABLE_ROW,
1269
+ data: {},
1270
+ content: dataCells,
1271
+ };
1272
+ dataCells.forEach(cell => {
1273
+ (cell as any).parent = row;
1274
+ });
1275
+ const doc = makeDoc({
1276
+ nodeType: BLOCKS.TABLE,
1277
+ data: {},
1278
+ content: [row],
1279
+ });
1280
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1281
+ const tds = container.querySelectorAll("td");
1282
+ expect(tds.length).toBe(3);
1283
+ tds.forEach(td => {
1284
+ expect(td.className).toContain("min-w-[50vw]");
1285
+ });
1286
+ });
1287
+
1288
+ it("renders scrollable table cell with 'yes' value and isScrollable true", () => {
1289
+ const dataCells = [
1290
+ {
1291
+ nodeType: BLOCKS.TABLE_CELL,
1292
+ data: {},
1293
+ content: [makeParagraph("yes")],
1294
+ },
1295
+ {
1296
+ nodeType: BLOCKS.TABLE_CELL,
1297
+ data: {},
1298
+ content: [makeParagraph("no")],
1299
+ },
1300
+ {
1301
+ nodeType: BLOCKS.TABLE_CELL,
1302
+ data: {},
1303
+ content: [makeParagraph("other")],
1304
+ },
1305
+ ];
1306
+ const row = {
1307
+ nodeType: BLOCKS.TABLE_ROW,
1308
+ data: {},
1309
+ content: dataCells,
1310
+ };
1311
+ dataCells.forEach(cell => {
1312
+ (cell as any).parent = row;
1313
+ });
1314
+ const doc = makeDoc({
1315
+ nodeType: BLOCKS.TABLE,
1316
+ data: {},
1317
+ content: [row],
1318
+ });
1319
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1320
+ expect(
1321
+ container.querySelector('[data-testid="icon-check_circle"]')
1322
+ ).toBeInTheDocument();
1323
+ expect(
1324
+ container.querySelector('[data-testid="icon-cancel"]')
1325
+ ).toBeInTheDocument();
1326
+ const tds = container.querySelectorAll("td");
1327
+ tds.forEach(td => {
1328
+ expect(td.className).toContain("min-w-[50vw]");
1329
+ });
1330
+ });
1331
+
1332
+ it("renders cell with empty content array gracefully", () => {
1333
+ const doc = makeDoc({
1334
+ nodeType: BLOCKS.TABLE,
1335
+ data: {},
1336
+ content: [
1337
+ {
1338
+ nodeType: BLOCKS.TABLE_ROW,
1339
+ data: {},
1340
+ content: [
1341
+ {
1342
+ nodeType: BLOCKS.TABLE_CELL,
1343
+ data: {},
1344
+ content: [],
1345
+ },
1346
+ ],
1347
+ },
1348
+ ],
1349
+ });
1350
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1351
+ const td = container.querySelector("td");
1352
+ expect(td).toBeInTheDocument();
1353
+ });
1354
+
1355
+ it("renders superscript mark", () => {
1356
+ const doc = makeDoc(makeMarkedText("Sup", MARKS.SUPERSCRIPT));
1357
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1358
+ expect(container.querySelector("sup")).toHaveTextContent("Sup");
1359
+ });
1360
+
1361
+ it("renders subscript mark", () => {
1362
+ const doc = makeDoc(makeMarkedText("Sub", MARKS.SUBSCRIPT));
1363
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1364
+ expect(container.querySelector("sub")).toHaveTextContent("Sub");
1365
+ });
1366
+
1367
+ it("renders table when first row content is empty", () => {
1368
+ const doc = makeDoc({
1369
+ nodeType: BLOCKS.TABLE,
1370
+ data: {},
1371
+ content: [
1372
+ {
1373
+ nodeType: BLOCKS.TABLE_ROW,
1374
+ data: {},
1375
+ content: [],
1376
+ },
1377
+ ],
1378
+ });
1379
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1380
+ expect(container.querySelector("table")).toBeInTheDocument();
1381
+ expect(container.querySelector("table")?.className).toContain("min-w-full");
1382
+ });
1383
+
1384
+ it("renders inline entry when links is undefined", () => {
1385
+ const doc = makeDoc({
1386
+ nodeType: BLOCKS.PARAGRAPH,
1387
+ data: {},
1388
+ content: [
1389
+ {
1390
+ nodeType: INLINES.EMBEDDED_ENTRY,
1391
+ data: { target: { sys: { id: "any" } } },
1392
+ content: [],
1393
+ },
1394
+ ],
1395
+ });
1396
+ const { container } = render(
1397
+ <>{renderContentfulRichTextTable(doc, undefined)}</>
1398
+ );
1399
+ expect(container).toBeInTheDocument();
1400
+ });
1401
+
1402
+ it("renders inline entry when links has no entries", () => {
1403
+ const doc = makeDoc({
1404
+ nodeType: BLOCKS.PARAGRAPH,
1405
+ data: {},
1406
+ content: [
1407
+ {
1408
+ nodeType: INLINES.EMBEDDED_ENTRY,
1409
+ data: { target: { sys: { id: "any" } } },
1410
+ content: [],
1411
+ },
1412
+ ],
1413
+ });
1414
+ const { container } = render(<>{renderContentfulRichTextTable(doc, {})}</>);
1415
+ expect(container).toBeInTheDocument();
1416
+ });
1417
+
1418
+ it("renders inline entry when links.entries has no inline", () => {
1419
+ const doc = makeDoc({
1420
+ nodeType: BLOCKS.PARAGRAPH,
1421
+ data: {},
1422
+ content: [
1423
+ {
1424
+ nodeType: INLINES.EMBEDDED_ENTRY,
1425
+ data: { target: { sys: { id: "any" } } },
1426
+ content: [],
1427
+ },
1428
+ ],
1429
+ });
1430
+ const { container } = render(
1431
+ <>{renderContentfulRichTextTable(doc, { entries: {} })}</>
1432
+ );
1433
+ expect(container).toBeInTheDocument();
1434
+ });
1435
+
1436
+ it("renders checklist entry with list but no items", () => {
1437
+ const entryId = "entry-no-items";
1438
+ const doc = makeDoc({
1439
+ nodeType: BLOCKS.PARAGRAPH,
1440
+ data: {},
1441
+ content: [
1442
+ {
1443
+ nodeType: INLINES.EMBEDDED_ENTRY,
1444
+ data: { target: { sys: { id: entryId } } },
1445
+ content: [],
1446
+ },
1447
+ ],
1448
+ });
1449
+ const links = {
1450
+ entries: {
1451
+ inline: [
1452
+ {
1453
+ sys: { id: entryId },
1454
+ __typename: "ComponentCheckList",
1455
+ list: { items: null },
1456
+ },
1457
+ ],
1458
+ },
1459
+ };
1460
+ const { container } = render(
1461
+ <>{renderContentfulRichTextTable(doc, links)}</>
1462
+ );
1463
+ expect(
1464
+ container.querySelector('[data-testid="checklist"]')
1465
+ ).toBeInTheDocument();
1466
+ });
1467
+
1468
+ it("renders checklist entry with no list property", () => {
1469
+ const entryId = "entry-no-list";
1470
+ const doc = makeDoc({
1471
+ nodeType: BLOCKS.PARAGRAPH,
1472
+ data: {},
1473
+ content: [
1474
+ {
1475
+ nodeType: INLINES.EMBEDDED_ENTRY,
1476
+ data: { target: { sys: { id: entryId } } },
1477
+ content: [],
1478
+ },
1479
+ ],
1480
+ });
1481
+ const links = {
1482
+ entries: {
1483
+ inline: [
1484
+ {
1485
+ sys: { id: entryId },
1486
+ __typename: "ComponentCheckList",
1487
+ },
1488
+ ],
1489
+ },
1490
+ };
1491
+ const { container } = render(
1492
+ <>{renderContentfulRichTextTable(doc, links)}</>
1493
+ );
1494
+ expect(
1495
+ container.querySelector('[data-testid="checklist"]')
1496
+ ).toBeInTheDocument();
1497
+ });
1498
+
1499
+ it("handles hyperlink with missing uri in table", () => {
1500
+ const node: any = {
1501
+ nodeType: BLOCKS.PARAGRAPH,
1502
+ data: {},
1503
+ content: [
1504
+ {
1505
+ nodeType: INLINES.HYPERLINK,
1506
+ data: {},
1507
+ content: [{ nodeType: "text", value: "NoUri", marks: [], data: {} }],
1508
+ },
1509
+ ],
1510
+ };
1511
+ const doc = makeDoc(node);
1512
+ const { container } = render(<>{renderContentfulRichTextTable(doc)}</>);
1513
+ expect(container).toHaveTextContent("NoUri");
1514
+ });
1515
+ });
1516
+
1517
+ // ──────────────────────────────────────────────
1518
+ // Additional defaultOptions coverage
1519
+ // ──────────────────────────────────────────────
1520
+ describe("defaultOptions (via useContentfulRichText)", () => {
1521
+ it("renders embedded asset with description fallback alt", () => {
1522
+ const node: any = {
1523
+ nodeType: BLOCKS.EMBEDDED_ASSET,
1524
+ data: {
1525
+ target: {
1526
+ fields: {
1527
+ file: { url: "https://cdn.com/img.png" },
1528
+ description: "Desc alt",
1529
+ },
1530
+ },
1531
+ },
1532
+ content: [],
1533
+ };
1534
+ const doc = makeDoc(node);
1535
+ const { result } = renderHook(() => useContentfulRichText(doc));
1536
+ const { container } = render(<>{result.current}</>);
1537
+ expect(container.querySelector("img")).toHaveAttribute("alt", "Desc alt");
1538
+ });
1539
+
1540
+ it("renders embedded asset with default alt when no title or description", () => {
1541
+ const node: any = {
1542
+ nodeType: BLOCKS.EMBEDDED_ASSET,
1543
+ data: {
1544
+ target: {
1545
+ fields: {
1546
+ file: { url: "https://cdn.com/img.png" },
1547
+ },
1548
+ },
1549
+ },
1550
+ content: [],
1551
+ };
1552
+ const doc = makeDoc(node);
1553
+ const { result } = renderHook(() => useContentfulRichText(doc));
1554
+ const { container } = render(<>{result.current}</>);
1555
+ expect(container.querySelector("img")).toHaveAttribute(
1556
+ "alt",
1557
+ "Embedded asset"
1558
+ );
1559
+ });
1560
+
1561
+ it("renders inline entry with missing fields as empty span", () => {
1562
+ const node: any = {
1563
+ nodeType: BLOCKS.PARAGRAPH,
1564
+ data: {},
1565
+ content: [
1566
+ {
1567
+ nodeType: INLINES.EMBEDDED_ENTRY,
1568
+ data: {
1569
+ target: {
1570
+ sys: { contentType: { sys: { id: "unknown" } } },
1571
+ fields: {},
1572
+ },
1573
+ },
1574
+ content: [],
1575
+ },
1576
+ ],
1577
+ };
1578
+ const doc = makeDoc(node);
1579
+ const { result } = renderHook(() => useContentfulRichText(doc));
1580
+ const { container } = render(<>{result.current}</>);
1581
+ const spans = container.querySelectorAll("span");
1582
+ const emptySpan = Array.from(spans).find(s => s.textContent === "");
1583
+ expect(emptySpan).toBeDefined();
1584
+ });
1585
+
1586
+ // Cover ?. null short-circuit branches in defaultOptions
1587
+ it("handles embedded asset with missing target fields gracefully", () => {
1588
+ const node: any = {
1589
+ nodeType: BLOCKS.EMBEDDED_ASSET,
1590
+ data: { target: null },
1591
+ content: [],
1592
+ };
1593
+ const doc = makeDoc(node);
1594
+ const { result } = renderHook(() => useContentfulRichText(doc));
1595
+ const { container } = render(<>{result.current}</>);
1596
+ expect(container.querySelector("img")).not.toBeInTheDocument();
1597
+ });
1598
+
1599
+ it("handles embedded asset with missing data gracefully", () => {
1600
+ const node: any = {
1601
+ nodeType: BLOCKS.EMBEDDED_ASSET,
1602
+ data: {},
1603
+ content: [],
1604
+ };
1605
+ const doc = makeDoc(node);
1606
+ const { result } = renderHook(() => useContentfulRichText(doc));
1607
+ const { container } = render(<>{result.current}</>);
1608
+ expect(container.querySelector("img")).not.toBeInTheDocument();
1609
+ });
1610
+
1611
+ it("handles embedded asset with fields but no file", () => {
1612
+ const node: any = {
1613
+ nodeType: BLOCKS.EMBEDDED_ASSET,
1614
+ data: {
1615
+ target: {
1616
+ fields: { title: "No file" },
1617
+ },
1618
+ },
1619
+ content: [],
1620
+ };
1621
+ const doc = makeDoc(node);
1622
+ const { result } = renderHook(() => useContentfulRichText(doc));
1623
+ const { container } = render(<>{result.current}</>);
1624
+ expect(container.querySelector("img")).not.toBeInTheDocument();
1625
+ });
1626
+
1627
+ it("handles embedded entry with missing data target", () => {
1628
+ const node: any = {
1629
+ nodeType: BLOCKS.EMBEDDED_ENTRY,
1630
+ data: { target: null },
1631
+ content: [],
1632
+ };
1633
+ const doc = makeDoc(node);
1634
+ const { result } = renderHook(() => useContentfulRichText(doc));
1635
+ const { container } = render(<>{result.current}</>);
1636
+ expect(container.querySelector("aside")).not.toBeInTheDocument();
1637
+ });
1638
+
1639
+ it("handles embedded entry with no sys contentType", () => {
1640
+ const node: any = {
1641
+ nodeType: BLOCKS.EMBEDDED_ENTRY,
1642
+ data: {
1643
+ target: {
1644
+ sys: {},
1645
+ fields: { title: "No CT" },
1646
+ },
1647
+ },
1648
+ content: [],
1649
+ };
1650
+ const doc = makeDoc(node);
1651
+ const { result } = renderHook(() => useContentfulRichText(doc));
1652
+ const { container } = render(<>{result.current}</>);
1653
+ expect(container.querySelector("aside")).not.toBeInTheDocument();
1654
+ });
1655
+
1656
+ it("handles inline entry with missing data target", () => {
1657
+ const node: any = {
1658
+ nodeType: BLOCKS.PARAGRAPH,
1659
+ data: {},
1660
+ content: [
1661
+ {
1662
+ nodeType: INLINES.EMBEDDED_ENTRY,
1663
+ data: { target: null },
1664
+ content: [],
1665
+ },
1666
+ ],
1667
+ };
1668
+ const doc = makeDoc(node);
1669
+ const { result } = renderHook(() => useContentfulRichText(doc));
1670
+ const { container } = render(<>{result.current}</>);
1671
+ expect(container).toBeInTheDocument();
1672
+ });
1673
+
1674
+ it("handles inline entry with no sys on target", () => {
1675
+ const node: any = {
1676
+ nodeType: BLOCKS.PARAGRAPH,
1677
+ data: {},
1678
+ content: [
1679
+ {
1680
+ nodeType: INLINES.EMBEDDED_ENTRY,
1681
+ data: {
1682
+ target: { fields: { title: "No sys" } },
1683
+ },
1684
+ content: [],
1685
+ },
1686
+ ],
1687
+ };
1688
+ const doc = makeDoc(node);
1689
+ const { result } = renderHook(() => useContentfulRichText(doc));
1690
+ const { container } = render(<>{result.current}</>);
1691
+ expect(container).toHaveTextContent("No sys");
1692
+ });
1693
+
1694
+ it("handles inline entry with no contentType in sys", () => {
1695
+ const node: any = {
1696
+ nodeType: BLOCKS.PARAGRAPH,
1697
+ data: {},
1698
+ content: [
1699
+ {
1700
+ nodeType: INLINES.EMBEDDED_ENTRY,
1701
+ data: {
1702
+ target: {
1703
+ sys: {},
1704
+ fields: { title: "No CT" },
1705
+ },
1706
+ },
1707
+ content: [],
1708
+ },
1709
+ ],
1710
+ };
1711
+ const doc = makeDoc(node);
1712
+ const { result } = renderHook(() => useContentfulRichText(doc));
1713
+ const { container } = render(<>{result.current}</>);
1714
+ expect(container).toHaveTextContent("No CT");
1715
+ });
1716
+
1717
+ it("handles inline entry with null fields", () => {
1718
+ const node: any = {
1719
+ nodeType: BLOCKS.PARAGRAPH,
1720
+ data: {},
1721
+ content: [
1722
+ {
1723
+ nodeType: INLINES.EMBEDDED_ENTRY,
1724
+ data: {
1725
+ target: {
1726
+ sys: { contentType: { sys: { id: "other" } } },
1727
+ },
1728
+ },
1729
+ content: [],
1730
+ },
1731
+ ],
1732
+ };
1733
+ const doc = makeDoc(node);
1734
+ const { result } = renderHook(() => useContentfulRichText(doc));
1735
+ const { container } = render(<>{result.current}</>);
1736
+ const spans = container.querySelectorAll("span");
1737
+ const emptySpan = Array.from(spans).find(s => s.textContent === "");
1738
+ expect(emptySpan).toBeDefined();
1739
+ });
1740
+
1741
+ it("handles hyperlink with missing data", () => {
1742
+ const node: any = {
1743
+ nodeType: BLOCKS.PARAGRAPH,
1744
+ data: {},
1745
+ content: [
1746
+ {
1747
+ nodeType: INLINES.HYPERLINK,
1748
+ data: {},
1749
+ content: [{ nodeType: "text", value: "Link", marks: [], data: {} }],
1750
+ },
1751
+ ],
1752
+ };
1753
+ const doc = makeDoc(node);
1754
+ const { result } = renderHook(() => useContentfulRichText(doc));
1755
+ const { container } = render(<>{result.current}</>);
1756
+ expect(container).toHaveTextContent("Link");
1757
+ });
1758
+ });