@windstream/react-shared-components 0.1.93 → 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.
- package/dist/contentful/index.esm.js +2 -2
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +3 -3
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils/index.esm.js +1 -1
- package/dist/utils/index.js +1 -1
- package/package.json +14 -8
- package/src/components/accordion/index.test.tsx +270 -0
- package/src/components/alert-card/index.test.tsx +152 -0
- package/src/components/animation-wrapper/index.test.tsx +424 -0
- package/src/components/brand-button/index.test.tsx +292 -0
- package/src/components/button/index.test.tsx +91 -0
- package/src/components/call-button/index.test.tsx +260 -0
- package/src/components/checkbox/index.test.tsx +252 -0
- package/src/components/checklist/index.test.tsx +231 -0
- package/src/components/checklist/index.tsx +64 -29
- package/src/components/checklist/types.ts +7 -1
- package/src/components/collapse/index.test.tsx +277 -0
- package/src/components/collapse/index.tsx +1 -0
- package/src/components/divider/index.test.tsx +53 -0
- package/src/components/image/index.test.tsx +174 -0
- package/src/components/input/index.test.tsx +348 -0
- package/src/components/link/index.test.tsx +199 -0
- package/src/components/list/index.test.tsx +166 -0
- package/src/components/material-icon/index.test.tsx +130 -0
- package/src/components/modal/index.test.tsx +310 -0
- package/src/components/next-image/index.test.tsx +406 -0
- package/src/components/pagination/index.test.tsx +521 -0
- package/src/components/radio-button/index.test.tsx +151 -0
- package/src/components/see-more/index.test.tsx +96 -0
- package/src/components/select/index.test.tsx +256 -0
- package/src/components/select-plan-button/index.test.tsx +173 -0
- package/src/components/skeleton/index.test.tsx +74 -0
- package/src/components/spinner/index.test.tsx +76 -0
- package/src/components/text/index.test.tsx +65 -0
- package/src/components/tooltip/index.test.tsx +50 -0
- package/src/components/view-cart-button/index.test.tsx +57 -0
- package/src/contentful/blocks/accordion/index.test.tsx +218 -0
- package/src/contentful/blocks/accordion/index.tsx +3 -1
- package/src/contentful/blocks/address-input-banner/index.test.tsx +132 -0
- package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +287 -0
- package/src/contentful/blocks/blogs-grid/BlogGrid.stories.tsx +5 -4
- package/src/contentful/blocks/blogs-grid/index.test.tsx +355 -0
- package/src/contentful/blocks/blogs-grid-base/index.test.tsx +274 -0
- package/src/contentful/blocks/breadcrumbs/index.test.tsx +281 -0
- package/src/contentful/blocks/button/index.test.tsx +339 -0
- package/src/contentful/blocks/callout/index.test.tsx +539 -0
- package/src/contentful/blocks/cards/blog-card/index.test.tsx +218 -0
- package/src/contentful/blocks/cards/floating-image-card/index.test.tsx +201 -0
- package/src/contentful/blocks/cards/full-image-card/index.test.tsx +216 -0
- package/src/contentful/blocks/cards/index.test.tsx +39 -0
- package/src/contentful/blocks/cards/product-card/index.test.tsx +263 -0
- package/src/contentful/blocks/cards/simple-card/index.test.tsx +364 -0
- package/src/contentful/blocks/cards/simple-card/index.tsx +1 -1
- package/src/contentful/blocks/cards/testimonial-card/index.test.tsx +180 -0
- package/src/contentful/blocks/carousel/helper.test.tsx +539 -0
- package/src/contentful/blocks/carousel/index.test.tsx +308 -0
- package/src/contentful/blocks/carousel/types.test.ts +16 -0
- package/src/contentful/blocks/cart-retention-banner/index.test.tsx +409 -0
- package/src/contentful/blocks/cart-retention-banner/index.tsx +4 -4
- package/src/contentful/blocks/comparison-table/index.test.tsx +114 -0
- package/src/contentful/blocks/cookiebanner/index.test.tsx +277 -0
- package/src/contentful/blocks/cta-callout/index.test.tsx +244 -0
- package/src/contentful/blocks/dynamic-tabs/index.test.tsx +240 -0
- package/src/contentful/blocks/email-input-block/index.test.tsx +213 -0
- package/src/contentful/blocks/email-input-block/index.tsx +40 -35
- package/src/contentful/blocks/find-kinetic/index.test.tsx +269 -0
- package/src/contentful/blocks/floating-banner/index.test.tsx +246 -0
- package/src/contentful/blocks/footer/index.test.tsx +302 -0
- package/src/contentful/blocks/image-promo-bar/helper.test.tsx +61 -0
- package/src/contentful/blocks/image-promo-bar/index.test.tsx +467 -0
- package/src/contentful/blocks/image-promo-bar/index.tsx +248 -246
- package/src/contentful/blocks/image-promo-bar/vimeo-embed.test.tsx +142 -0
- package/src/contentful/blocks/image-promo-bar/youtube-embed.test.tsx +104 -0
- package/src/contentful/blocks/modal/index.test.tsx +209 -0
- package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +208 -0
- package/src/contentful/blocks/navigation/index.test.tsx +924 -0
- package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +131 -0
- package/src/contentful/blocks/primary-hero/index.test.tsx +286 -0
- package/src/contentful/blocks/primary-hero/index.tsx +7 -4
- package/src/contentful/blocks/search-block/index.test.tsx +268 -0
- package/src/contentful/blocks/shape-background-wrapper/index.test.tsx +284 -0
- package/src/contentful/blocks/text/index.test.tsx +36 -0
- package/src/contentful/index.test.ts +45 -0
- package/src/global-mocks/contentful/to-document.ts +25 -0
- package/src/global-mocks/cookie.ts +48 -0
- package/src/global-mocks/cx.ts +37 -0
- package/src/global-mocks/index.ts +89 -0
- package/src/global-mocks/speed-card-bg.ts +27 -0
- package/src/global-mocks/utm.ts +49 -0
- package/src/hooks/contentful/use-contentful-rich-text.test.tsx +1758 -0
- package/src/hooks/contentful/use-contentful-rich-text.tsx +1 -1
- package/src/hooks/contentful/use-processed-check-list.test.tsx +277 -0
- package/src/hooks/use-body-scroll-lock.test.ts +134 -0
- package/src/hooks/use-carousel-swipe.test.ts +393 -0
- package/src/hooks/use-outside-click.test.ts +142 -0
- package/src/index.ts +1 -1
- package/src/next/index.test.ts +7 -0
- package/src/setupTests.ts +17 -11
- package/src/utils/contentful/to-document.test.ts +85 -0
- package/src/utils/cookie.test.ts +180 -0
- package/src/utils/cx.test.ts +90 -0
- package/src/utils/index.test.ts +115 -0
- package/src/utils/speed-card-bg.test.ts +46 -0
- package/src/utils/utm.test.ts +359 -0
|
@@ -0,0 +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
|
+
});
|