Skip to content

Commit 6047534

Browse files
feat: publicTestComponentTypes option (#33)
1 parent bb44860 commit 6047534

3 files changed

Lines changed: 141 additions & 94 deletions

File tree

src/__tests__/renderer.test.tsx

Lines changed: 123 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { beforeEach, expect, test } from "@jest/globals";
1+
import { beforeEach, describe, expect, test } from "@jest/globals";
22
import { createElement } from "react";
33

44
import { createRoot } from "../renderer";
@@ -20,104 +20,140 @@ test("basic renderer usage", async () => {
2020
`);
2121
});
2222

23-
test("render with single allowed text component", async () => {
24-
const renderer = createRoot({
25-
textComponents: ["Text"],
26-
});
27-
await renderWithAct(renderer, createElement("Text", null, "Hello!"));
28-
expect(renderer.container).toMatchInlineSnapshot(`
29-
<>
30-
<Text>
31-
Hello!
32-
</Text>
33-
</>
34-
`);
23+
describe("textComponentTypes", () => {
24+
test("single allowed text component", async () => {
25+
const renderer = createRoot({
26+
textComponentTypes: ["Text"],
27+
});
28+
await renderWithAct(renderer, createElement("Text", null, "Hello!"));
29+
expect(renderer.container).toMatchInlineSnapshot(`
30+
<>
31+
<Text>
32+
Hello!
33+
</Text>
34+
</>
35+
`);
3536

36-
await renderWithAct(
37-
renderer,
38-
<div>
39-
<hr />
40-
</div>,
41-
);
42-
expect(renderer.container).toMatchInlineSnapshot(`
43-
<>
37+
await renderWithAct(
38+
renderer,
4439
<div>
4540
<hr />
46-
</div>
47-
</>
48-
`);
41+
</div>,
42+
);
43+
expect(renderer.container).toMatchInlineSnapshot(`
44+
<>
45+
<div>
46+
<hr />
47+
</div>
48+
</>
49+
`);
4950

50-
await expect(() =>
51-
renderWithAct(renderer, <div>Hello!</div>),
52-
).rejects.toThrowErrorMatchingInlineSnapshot(
53-
`"Invariant Violation: Text strings must be rendered within a <Text> component. Detected attempt to render "Hello!" string within a <div> component."`,
54-
);
55-
});
51+
await expect(() =>
52+
renderWithAct(renderer, <div>Hello!</div>),
53+
).rejects.toThrowErrorMatchingInlineSnapshot(
54+
`"Invariant Violation: Text strings must be rendered within a <Text> component. Detected attempt to render "Hello!" string within a <div> component."`,
55+
);
56+
});
5657

57-
test("render with two allowed text components", async () => {
58-
const renderer = createRoot({
59-
textComponents: ["A", "B"],
58+
test("two allowed text components", async () => {
59+
const renderer = createRoot({
60+
textComponentTypes: ["A", "B"],
61+
});
62+
await renderWithAct(
63+
renderer,
64+
<div>
65+
{createElement("A", null, "Hello!")}
66+
{createElement("B", null, "Hi!")}
67+
</div>,
68+
);
69+
expect(renderer.container).toMatchInlineSnapshot(`
70+
<>
71+
<div>
72+
<A>
73+
Hello!
74+
</A>
75+
<B>
76+
Hi!
77+
</B>
78+
</div>
79+
</>
80+
`);
81+
82+
await expect(() =>
83+
renderWithAct(renderer, createElement("X", null, "Hello!")),
84+
).rejects.toThrowErrorMatchingInlineSnapshot(
85+
`"Invariant Violation: Text strings must be rendered within a <A> or <B> component. Detected attempt to render "Hello!" string within a <X> component."`,
86+
);
6087
});
61-
await renderWithAct(
62-
renderer,
63-
<div>
64-
{createElement("A", null, "Hello!")}
65-
{createElement("B", null, "Hi!")}
66-
</div>,
67-
);
68-
expect(renderer.container).toMatchInlineSnapshot(`
69-
<>
88+
89+
test("multiple allowed text components", async () => {
90+
const renderer = createRoot({
91+
textComponentTypes: ["A", "B", "C"],
92+
});
93+
await renderWithAct(
94+
renderer,
7095
<div>
71-
<A>
72-
Hello!
73-
</A>
74-
<B>
75-
Hi!
76-
</B>
77-
</div>
78-
</>
79-
`);
96+
{createElement("A", null, "Hello!")}
97+
{createElement("B", null, "Hi!")}
98+
{createElement("C", null, "Hola!")}
99+
</div>,
100+
);
101+
expect(renderer.container).toMatchInlineSnapshot(`
102+
<>
103+
<div>
104+
<A>
105+
Hello!
106+
</A>
107+
<B>
108+
Hi!
109+
</B>
110+
<C>
111+
Hola!
112+
</C>
113+
</div>
114+
</>
115+
`);
80116

81-
await expect(() =>
82-
renderWithAct(renderer, createElement("X", null, "Hello!")),
83-
).rejects.toThrowErrorMatchingInlineSnapshot(
84-
`"Invariant Violation: Text strings must be rendered within a <A> or <B> component. Detected attempt to render "Hello!" string within a <X> component."`,
85-
);
86-
});
117+
await expect(() =>
118+
renderWithAct(renderer, createElement("X", null, "Hello!")),
119+
).rejects.toThrowErrorMatchingInlineSnapshot(
120+
`"Invariant Violation: Text strings must be rendered within a <A>, <B>, or <C> component. Detected attempt to render "Hello!" string within a <X> component."`,
121+
);
122+
});
87123

88-
test("render with multiple allowed text components", async () => {
89-
const renderer = createRoot({
90-
textComponents: ["A", "B", "C"],
124+
test("error message uses textComponentTypes when publicTextComponentTypes is not set", async () => {
125+
const renderer = createRoot({
126+
textComponentTypes: ["RCTText", "RCTVirtualText"],
127+
});
128+
129+
await expect(() =>
130+
renderWithAct(renderer, <div>Hello!</div>),
131+
).rejects.toThrowErrorMatchingInlineSnapshot(
132+
`"Invariant Violation: Text strings must be rendered within a <RCTText> or <RCTVirtualText> component. Detected attempt to render "Hello!" string within a <div> component."`,
133+
);
91134
});
92-
await renderWithAct(
93-
renderer,
94-
<div>
95-
{createElement("A", null, "Hello!")}
96-
{createElement("B", null, "Hi!")}
97-
{createElement("C", null, "Hola!")}
98-
</div>,
99-
);
100-
expect(renderer.container).toMatchInlineSnapshot(`
101-
<>
102-
<div>
103-
<A>
135+
136+
test("error message uses publicTextComponentTypes when set", async () => {
137+
const renderer = createRoot({
138+
textComponentTypes: ["RCTText", "RCTVirtualText"],
139+
publicTextComponentTypes: ["Text"],
140+
});
141+
142+
await renderWithAct(renderer, createElement("RCTText", null, "Hello!"));
143+
expect(renderer.container).toMatchInlineSnapshot(`
144+
<>
145+
<RCTText>
104146
Hello!
105-
</A>
106-
<B>
107-
Hi!
108-
</B>
109-
<C>
110-
Hola!
111-
</C>
112-
</div>
113-
</>
114-
`);
147+
</RCTText>
148+
</>
149+
`);
115150

116-
await expect(() =>
117-
renderWithAct(renderer, createElement("X", null, "Hello!")),
118-
).rejects.toThrowErrorMatchingInlineSnapshot(
119-
`"Invariant Violation: Text strings must be rendered within a <A>, <B>, or <C> component. Detected attempt to render "Hello!" string within a <X> component."`,
120-
);
151+
await expect(() =>
152+
renderWithAct(renderer, <div>Hello!</div>),
153+
).rejects.toThrowErrorMatchingInlineSnapshot(
154+
`"Invariant Violation: Text strings must be rendered within a <Text> component. Detected attempt to render "Hello!" string within a <div> component."`,
155+
);
156+
});
121157
});
122158

123159
function NullComponent() {

src/reconciler.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export type Type = string;
1010
export type Props = Record<string, unknown>;
1111

1212
type ReconcilerConfig = {
13-
textComponents?: string[];
13+
textComponentTypes?: string[];
14+
publicTextComponentTypes?: string[];
1415
createNodeMock: (element: ReactElement) => object;
1516
};
1617

@@ -166,10 +167,13 @@ const hostConfig: ReactReconciler.HostConfig<
166167
hostContext: HostContext,
167168
_internalHandle: Fiber,
168169
): TextInstance {
169-
if (rootContainer.config.textComponents && !hostContext.isInsideText) {
170+
if (rootContainer.config.textComponentTypes && !hostContext.isInsideText) {
171+
const componentTypes =
172+
rootContainer.config.publicTextComponentTypes ?? rootContainer.config.textComponentTypes;
173+
170174
throw new Error(
171175
`Invariant Violation: Text strings must be rendered within a ${formatComponentList(
172-
rootContainer.config.textComponents,
176+
componentTypes,
173177
)} component. Detected attempt to render "${text}" string within a <${
174178
hostContext.type
175179
}> component.`,
@@ -291,7 +295,7 @@ const hostConfig: ReactReconciler.HostConfig<
291295
* This method happens **in the render phase**. Do not mutate the tree from it.
292296
*/
293297
getChildHostContext(parentHostContext: HostContext, type: Type): HostContext {
294-
const isInsideText = Boolean(parentHostContext.config.textComponents?.includes(type));
298+
const isInsideText = Boolean(parentHostContext.config.textComponentTypes?.includes(type));
295299
return { ...parentHostContext, type: type, isInsideText };
296300
},
297301

src/renderer.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,14 @@ const defaultOnRecoverableError = (error: unknown, errorInfo: ErrorInfo) => {
2727
* Options for configuring the test renderer root.
2828
*/
2929
export type RootOptions = {
30-
/** Types of valid text components. */
31-
textComponents?: string[];
30+
/** Types of host components that are allowed to contain text nodes. Trying to render text outside of these components will throw an error. */
31+
textComponentTypes?: string[];
32+
33+
/**
34+
* Host component types to display to users in the error message when they try to render text outside of `textComponentTypes`.
35+
* Defaults to `textComponentTypes`, but you may want to override the components mentioned in the error message.
36+
*/
37+
publicTextComponentTypes?: string[];
3238

3339
/** Function to create mock nodes for refs. */
3440
createNodeMock?: (element: ReactElement) => object;
@@ -82,7 +88,8 @@ export function createRoot(options?: RootOptions): Root {
8288
children: [],
8389
isHidden: false,
8490
config: {
85-
textComponents: options?.textComponents,
91+
textComponentTypes: options?.textComponentTypes,
92+
publicTextComponentTypes: options?.publicTextComponentTypes,
8693
createNodeMock: options?.createNodeMock ?? defaultCreateMockNode,
8794
},
8895
};

0 commit comments

Comments
 (0)