@server/next 0.28.5 → 0.28.7
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/package.json +1 -1
- package/src/jsx/jsx-runtime.js +40 -24
- package/src/jsx/jsx-runtime.test.jsx +347 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.7",
|
|
4
4
|
"description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
|
|
5
5
|
"homepage": "https://server-js.com/",
|
|
6
6
|
"repository": "https://github.com/franciscop/server-next.git",
|
package/src/jsx/jsx-runtime.js
CHANGED
|
@@ -9,7 +9,7 @@ const entities = {
|
|
|
9
9
|
const encode = (str = "") => {
|
|
10
10
|
if (typeof str === "number") str = String(str);
|
|
11
11
|
if (typeof str !== "string") return "";
|
|
12
|
-
return str.replace(/[&<>"]/g, (tag) => entities[tag]);
|
|
12
|
+
return str.replace(/[&<>"']/g, (tag) => entities[tag]);
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const SELFCLOSE = new Set(
|
|
@@ -20,39 +20,52 @@ const REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
|
|
|
20
20
|
|
|
21
21
|
const altAttrs = {
|
|
22
22
|
classname: "class",
|
|
23
|
+
htmlfor: "for",
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
// valid primitives only
|
|
26
|
-
const isValidChild = (child) =>
|
|
27
|
-
|
|
28
|
-
// CSS helpers
|
|
29
|
-
const escapeCSS = (value) => String(value).replace(/[<>&"'`]/g, "\\$&");
|
|
26
|
+
// valid primitives only — null, undefined, false, true are all skipped
|
|
27
|
+
const isValidChild = (child) =>
|
|
28
|
+
child != null && child !== false && child !== true;
|
|
30
29
|
|
|
31
30
|
const minifyCss = (str) =>
|
|
32
31
|
str
|
|
32
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
33
33
|
.replace(/\s+/g, " ")
|
|
34
|
-
.replace(
|
|
34
|
+
.replace(/\s*([{}:;,>~+])\s*/g, "$1")
|
|
35
|
+
.replace(/;}/g, "}")
|
|
36
|
+
.replace(/<\/style>/gi, "<\\/style>")
|
|
35
37
|
.trim();
|
|
36
38
|
|
|
37
|
-
// React element detection (safe for
|
|
39
|
+
// React element detection (safe for custom objects too)
|
|
38
40
|
const isReactElement = (val) =>
|
|
39
41
|
val !== null && typeof val === "object" && "type" in val && "props" in val;
|
|
40
42
|
|
|
41
|
-
//
|
|
43
|
+
// Marker for raw (pre-escaped) content that bypasses HTML encoding
|
|
44
|
+
const RAW = Symbol("raw");
|
|
45
|
+
const raw = (str) => ({ [RAW]: String(str) });
|
|
46
|
+
|
|
42
47
|
const renderChild = (child) => {
|
|
43
|
-
if (child == null || child === false) return "";
|
|
48
|
+
if (child == null || child === false || child === true) return "";
|
|
44
49
|
|
|
45
50
|
if (Array.isArray(child)) {
|
|
46
51
|
return child.map(renderChild).join("");
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
if (typeof child === "function") {
|
|
50
|
-
|
|
55
|
+
const result = child();
|
|
56
|
+
// Strings returned by functions are pre-rendered HTML — don't re-encode them
|
|
57
|
+
if (result == null || result === false || result === true) return "";
|
|
58
|
+
if (typeof result === "string") return result;
|
|
59
|
+
if (typeof result === "number") return String(result);
|
|
60
|
+
return renderChild(result);
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
if (typeof child === "string") return child;
|
|
63
|
+
if (typeof child === "string") return encode(child);
|
|
54
64
|
if (typeof child === "number") return String(child);
|
|
55
65
|
|
|
66
|
+
// Raw marker bypasses encoding (used by script, style, dangerouslySetInnerHTML)
|
|
67
|
+
if (typeof child === "object" && RAW in child) return child[RAW];
|
|
68
|
+
|
|
56
69
|
if (isReactElement(child)) {
|
|
57
70
|
return jsx(child.type, child.props || {})();
|
|
58
71
|
}
|
|
@@ -62,7 +75,7 @@ const renderChild = (child) => {
|
|
|
62
75
|
};
|
|
63
76
|
|
|
64
77
|
const jsx = (tag, { children, ...props } = {}) => {
|
|
65
|
-
//
|
|
78
|
+
// Fragment (Symbol-safe)
|
|
66
79
|
if (tag === REACT_FRAGMENT_TYPE || tag === Fragment) {
|
|
67
80
|
return () => renderChild(children);
|
|
68
81
|
}
|
|
@@ -81,21 +94,24 @@ const jsx = (tag, { children, ...props } = {}) => {
|
|
|
81
94
|
return () => renderChild(tag.render({ children, ...props }, null));
|
|
82
95
|
}
|
|
83
96
|
|
|
84
|
-
// script
|
|
85
|
-
if (tag === "script" && children) {
|
|
86
|
-
const
|
|
87
|
-
|
|
97
|
+
// script: raw content (JS must not be HTML-encoded)
|
|
98
|
+
if (tag === "script" && children != null) {
|
|
99
|
+
const parts = Array.isArray(children) ? children : [children];
|
|
100
|
+
const content = parts
|
|
101
|
+
.filter(isValidChild)
|
|
102
|
+
.map((c) => (typeof c === "string" ? c : typeof c === "number" ? String(c) : ""))
|
|
103
|
+
.join("");
|
|
104
|
+
children = raw(content);
|
|
88
105
|
}
|
|
89
106
|
|
|
90
|
-
// style
|
|
107
|
+
// style: minified raw content
|
|
91
108
|
if (tag === "style" && typeof children === "string") {
|
|
92
|
-
|
|
93
|
-
children = () => src;
|
|
109
|
+
children = raw(minifyCss(children));
|
|
94
110
|
}
|
|
95
111
|
|
|
96
112
|
// dangerouslySetInnerHTML
|
|
97
113
|
if (props?.dangerouslySetInnerHTML) {
|
|
98
|
-
children = (
|
|
114
|
+
children = raw(props.dangerouslySetInnerHTML.__html ?? "");
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
// normalize children
|
|
@@ -104,7 +120,7 @@ const jsx = (tag, { children, ...props } = {}) => {
|
|
|
104
120
|
.map(renderChild)
|
|
105
121
|
.join("");
|
|
106
122
|
|
|
107
|
-
//
|
|
123
|
+
// safety gate (prevents Symbol/unknown-type crash)
|
|
108
124
|
if (!tag || typeof tag !== "string") {
|
|
109
125
|
return () => flatChildren;
|
|
110
126
|
}
|
|
@@ -120,7 +136,7 @@ const jsx = (tag, { children, ...props } = {}) => {
|
|
|
120
136
|
if (v === true) return key;
|
|
121
137
|
|
|
122
138
|
const value =
|
|
123
|
-
typeof v === "string" || typeof v === "number" ? encode(v) : "";
|
|
139
|
+
typeof v === "string" || typeof v === "number" ? encode(String(v)) : "";
|
|
124
140
|
|
|
125
141
|
return `${key}="${value}"`;
|
|
126
142
|
})
|
|
@@ -137,6 +153,6 @@ const jsx = (tag, { children, ...props } = {}) => {
|
|
|
137
153
|
return () => `${doctype}<${tag}${attrStr}>${flatChildren}</${tag}>`;
|
|
138
154
|
};
|
|
139
155
|
|
|
140
|
-
const Fragment = "";
|
|
156
|
+
const Fragment = Symbol.for("react.fragment");
|
|
141
157
|
|
|
142
158
|
export { jsx, jsx as jsxs, jsx as jsxDEV, Fragment };
|
|
@@ -56,6 +56,7 @@ describe("jsx", () => {
|
|
|
56
56
|
it("can render a pre with indented text", () => {
|
|
57
57
|
expect(<pre>{` indented`}</pre>).toRender("<pre> indented</pre>");
|
|
58
58
|
});
|
|
59
|
+
|
|
59
60
|
it("can render a pre inside a div", () => {
|
|
60
61
|
expect(
|
|
61
62
|
<div>
|
|
@@ -63,6 +64,189 @@ describe("jsx", () => {
|
|
|
63
64
|
</div>,
|
|
64
65
|
).toRender("<div><pre>line1\nline2</pre></div>");
|
|
65
66
|
});
|
|
67
|
+
|
|
68
|
+
it("does NOT render the <pre>", () => {
|
|
69
|
+
expect(<code>{"<pre>"}</code>).toRender("<code><pre></code>");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("text encoding", () => {
|
|
74
|
+
it("encodes < and > to prevent XSS", () => {
|
|
75
|
+
expect(<div>{"<script>alert(1)</script>"}</div>).toRender(
|
|
76
|
+
"<div><script>alert(1)</script></div>",
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("encodes & in text content", () => {
|
|
81
|
+
expect(<div>{"a & b"}</div>).toRender("<div>a & b</div>");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('encodes " in text content', () => {
|
|
85
|
+
expect(<div>{'say "hi"'}</div>).toRender("<div>say "hi"</div>");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("encodes ' in text content", () => {
|
|
89
|
+
expect(<div>{"it's"}</div>).toRender("<div>it's</div>");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("renders 0 as a child", () => {
|
|
93
|
+
expect(<div>{0}</div>).toRender("<div>0</div>");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("renders positive integers", () => {
|
|
97
|
+
expect(<div>{42}</div>).toRender("<div>42</div>");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("renders floats", () => {
|
|
101
|
+
expect(<div>{3.14}</div>).toRender("<div>3.14</div>");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("renders negative numbers", () => {
|
|
105
|
+
expect(<div>{-1}</div>).toRender("<div>-1</div>");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("renders nothing for null", () => {
|
|
109
|
+
expect(<div>{null}</div>).toRender("<div></div>");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("renders nothing for undefined", () => {
|
|
113
|
+
expect(<div>{undefined}</div>).toRender("<div></div>");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("renders nothing for false", () => {
|
|
117
|
+
expect(<div>{false}</div>).toRender("<div></div>");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("renders nothing for true", () => {
|
|
121
|
+
expect(<div>{true}</div>).toRender("<div></div>");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("renders conditional content", () => {
|
|
125
|
+
const show = true;
|
|
126
|
+
expect(<div>{show && <span>yes</span>}</div>).toRender(
|
|
127
|
+
"<div><span>yes</span></div>",
|
|
128
|
+
);
|
|
129
|
+
const hide = false;
|
|
130
|
+
expect(<div>{hide && <span>no</span>}</div>).toRender("<div></div>");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("renders multiple children in order", () => {
|
|
134
|
+
expect(
|
|
135
|
+
<ul>
|
|
136
|
+
<li>a</li>
|
|
137
|
+
<li>b</li>
|
|
138
|
+
<li>c</li>
|
|
139
|
+
</ul>,
|
|
140
|
+
).toRender("<ul><li>a</li><li>b</li><li>c</li></ul>");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("renders array children", () => {
|
|
144
|
+
const items = ["x", "y", "z"];
|
|
145
|
+
expect(<span>{items.map((i) => <b key={i}>{i}</b>)}</span>).toRender(
|
|
146
|
+
"<span><b>x</b><b>y</b><b>z</b></span>",
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("treats string returned from a function child as raw HTML", () => {
|
|
151
|
+
const getHtml = () => "<b>bold</b>";
|
|
152
|
+
expect(<div>{getHtml}</div>).toRender("<div><b>bold</b></div>");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("attribute encoding", () => {
|
|
157
|
+
it("encodes < and > in attribute values", () => {
|
|
158
|
+
expect(<div title="<b>hi</b>"></div>).toRender(
|
|
159
|
+
'<div title="<b>hi</b>"></div>',
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("encodes & in attribute values", () => {
|
|
164
|
+
expect(<div title="a & b"></div>).toRender('<div title="a & b"></div>');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('encodes " in attribute values', () => {
|
|
168
|
+
expect(<div title={`say "hi"`}></div>).toRender(
|
|
169
|
+
'<div title="say "hi""></div>',
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("encodes ' in attribute values", () => {
|
|
174
|
+
expect(<div title={"it's"}></div>).toRender(
|
|
175
|
+
'<div title="it's"></div>',
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("maps className to class", () => {
|
|
180
|
+
expect(<div className="foo bar"></div>).toRender(
|
|
181
|
+
'<div class="foo bar"></div>',
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("maps htmlFor to for", () => {
|
|
186
|
+
expect(<label htmlFor="email"></label>).toRender(
|
|
187
|
+
'<label for="email"></label>',
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("supports data-* attributes", () => {
|
|
192
|
+
expect(<div data-id="42" data-name="test"></div>).toRender(
|
|
193
|
+
'<div data-id="42" data-name="test"></div>',
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("supports aria-* attributes", () => {
|
|
198
|
+
expect(<button aria-label="close" aria-expanded={false}></button>).toRender(
|
|
199
|
+
'<button aria-label="close"></button>',
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("supports numeric attribute values", () => {
|
|
204
|
+
expect(<input tabIndex={3} />).toRender('<input tabIndex="3" />');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("strips function-valued attributes", () => {
|
|
208
|
+
expect(<div onFoo={() => {}} ref={() => {}}></div>).toRender("<div></div>");
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("self-closing tags", () => {
|
|
213
|
+
it("renders br", () => {
|
|
214
|
+
expect(<br />).toRender("<br />");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("renders hr", () => {
|
|
218
|
+
expect(<hr />).toRender("<hr />");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("renders img with attributes", () => {
|
|
222
|
+
expect(<img src="cat.png" alt="A cat" />).toRender(
|
|
223
|
+
'<img src="cat.png" alt="A cat" />',
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("renders input with type and name", () => {
|
|
228
|
+
expect(<input type="text" name="q" />).toRender(
|
|
229
|
+
'<input type="text" name="q" />',
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("renders link tag", () => {
|
|
234
|
+
expect(<link rel="stylesheet" href="style.css" />).toRender(
|
|
235
|
+
'<link rel="stylesheet" href="style.css" />',
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("renders meta tag", () => {
|
|
240
|
+
expect(<meta name="description" content="hello" />).toRender(
|
|
241
|
+
'<meta name="description" content="hello" />',
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("renders source tag", () => {
|
|
246
|
+
expect(<source src="video.mp4" type="video/mp4" />).toRender(
|
|
247
|
+
'<source src="video.mp4" type="video/mp4" />',
|
|
248
|
+
);
|
|
249
|
+
});
|
|
66
250
|
});
|
|
67
251
|
|
|
68
252
|
describe("fragments", () => {
|
|
@@ -88,6 +272,169 @@ describe("fragments", () => {
|
|
|
88
272
|
it("handles empty fragments", () => {
|
|
89
273
|
expect(<></>).toRender("");
|
|
90
274
|
});
|
|
275
|
+
|
|
276
|
+
it("encodes text inside a fragment", () => {
|
|
277
|
+
expect(<>{"<b>"}</>).toRender("<b>");
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("script tag", () => {
|
|
282
|
+
it("renders script content without encoding", () => {
|
|
283
|
+
expect(<script>{"const x = 1 < 2 && 3 > 0;"}</script>).toRender(
|
|
284
|
+
"<script>const x = 1 < 2 && 3 > 0;</script>",
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("renders script with src attribute and no children", () => {
|
|
289
|
+
expect(<script src="app.js"></script>).toRender(
|
|
290
|
+
'<script src="app.js"></script>',
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("renders empty script tag", () => {
|
|
295
|
+
expect(<script></script>).toRender("<script></script>");
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("style tag", () => {
|
|
300
|
+
it("renders style content", () => {
|
|
301
|
+
expect(<style>{"body { color: red; }"}</style>).toRender(
|
|
302
|
+
"<style>body{color:red}</style>",
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("minifies whitespace in style content", () => {
|
|
307
|
+
expect(
|
|
308
|
+
<style>{"body {\n color: red;\n margin: 0;\n}"}</style>,
|
|
309
|
+
).toRender("<style>body{color:red;margin:0}</style>");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("strips CSS comments", () => {
|
|
313
|
+
expect(
|
|
314
|
+
<style>{"/* reset */ body { margin: 0; } /* end */"}</style>,
|
|
315
|
+
).toRender("<style>body{margin:0}</style>");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("preserves child combinator in selectors", () => {
|
|
319
|
+
expect(
|
|
320
|
+
<style>{":not(pre) > code { background: none; }"}</style>,
|
|
321
|
+
).toRender("<style>:not(pre)>code{background:none}</style>");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("preserves sibling combinators in selectors", () => {
|
|
325
|
+
expect(
|
|
326
|
+
<style>{"h2 + p { margin: 0; } h2 ~ p { color: red; }"}</style>,
|
|
327
|
+
).toRender("<style>h2+p{margin:0}h2~p{color:red}</style>");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("removes spaces around braces, colons, and semicolons", () => {
|
|
331
|
+
expect(
|
|
332
|
+
<style>{"a { color : red ; font-size : 1em ; }"}</style>,
|
|
333
|
+
).toRender("<style>a{color:red;font-size:1em}</style>");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("removes trailing semicolon before closing brace", () => {
|
|
337
|
+
expect(
|
|
338
|
+
<style>{"p { margin: 0; padding: 0; }"}</style>,
|
|
339
|
+
).toRender("<style>p{margin:0;padding:0}</style>");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("does not break </style> injection", () => {
|
|
343
|
+
expect(
|
|
344
|
+
<style>{"a { content: '</style>'; }"}</style>,
|
|
345
|
+
).toRender("<style>a{content:'<\\/style>'}</style>");
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("dangerouslySetInnerHTML", () => {
|
|
350
|
+
it("renders raw HTML without encoding", () => {
|
|
351
|
+
expect(
|
|
352
|
+
<div dangerouslySetInnerHTML={{ __html: "<b>bold</b>" }}></div>,
|
|
353
|
+
).toRender("<div><b>bold</b></div>");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("does not double-encode HTML entities", () => {
|
|
357
|
+
expect(
|
|
358
|
+
<div dangerouslySetInnerHTML={{ __html: "& <" }}></div>,
|
|
359
|
+
).toRender("<div>& <</div>");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("preserves script tags in raw HTML", () => {
|
|
363
|
+
expect(
|
|
364
|
+
<div
|
|
365
|
+
dangerouslySetInnerHTML={{ __html: "<script>alert(1)</script>" }}
|
|
366
|
+
></div>,
|
|
367
|
+
).toRender("<div><script>alert(1)</script></div>");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("does not render dangerouslySetInnerHTML as an attribute", () => {
|
|
371
|
+
expect(
|
|
372
|
+
<div dangerouslySetInnerHTML={{ __html: "hi" }}></div>,
|
|
373
|
+
).toRender("<div>hi</div>");
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe("function components", () => {
|
|
378
|
+
it("renders a simple function component", () => {
|
|
379
|
+
const Greeting = ({ name }) => <p>Hello, {name}!</p>;
|
|
380
|
+
expect(<Greeting name="world" />).toRender("<p>Hello, world!</p>");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("renders a component with children", () => {
|
|
384
|
+
const Box = ({ children }) => <div className="box">{children}</div>;
|
|
385
|
+
expect(
|
|
386
|
+
<Box>
|
|
387
|
+
<span>inside</span>
|
|
388
|
+
</Box>,
|
|
389
|
+
).toRender('<div class="box"><span>inside</span></div>');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("renders nested components", () => {
|
|
393
|
+
const Inner = () => <em>inner</em>;
|
|
394
|
+
const Outer = () => (
|
|
395
|
+
<div>
|
|
396
|
+
<Inner />
|
|
397
|
+
</div>
|
|
398
|
+
);
|
|
399
|
+
expect(<Outer />).toRender("<div><em>inner</em></div>");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("renders a component returning null", () => {
|
|
403
|
+
const Empty = () => null;
|
|
404
|
+
expect(<Empty />).toRender("");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("renders a component returning a string", () => {
|
|
408
|
+
const Text = () => "hello";
|
|
409
|
+
expect(<Text />).toRender("hello");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("encodes text returned by a component", () => {
|
|
413
|
+
const Unsafe = () => "<script>alert(1)</script>";
|
|
414
|
+
expect(<Unsafe />).toRender(
|
|
415
|
+
"<script>alert(1)</script>",
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
describe("forwardRef-like components", () => {
|
|
421
|
+
it("renders an object with a render function", () => {
|
|
422
|
+
const Button = { render: ({ children }) => <button>{children}</button> };
|
|
423
|
+
expect(<Button>click me</Button>).toRender(
|
|
424
|
+
"<button>click me</button>",
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("passes props to render function", () => {
|
|
429
|
+
const Input = {
|
|
430
|
+
render: ({ type, placeholder }) => (
|
|
431
|
+
<input type={type} placeholder={placeholder} />
|
|
432
|
+
),
|
|
433
|
+
};
|
|
434
|
+
expect(<Input type="email" placeholder="you@example.com" />).toRender(
|
|
435
|
+
'<input type="email" placeholder="you@example.com" />',
|
|
436
|
+
);
|
|
437
|
+
});
|
|
91
438
|
});
|
|
92
439
|
|
|
93
440
|
describe("React element interop", () => {
|