Skip to content

Commit ef086f4

Browse files
committed
feat: dynamic HostElement prop calculation
1 parent f0e3acb commit ef086f4

File tree

5 files changed

+104
-53
lines changed

5 files changed

+104
-53
lines changed

src/queries/__tests__/test-id.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import { Button, Text, TextInput, View } from 'react-native';
3-
import { render, screen } from '../..';
3+
import { configure, render, screen } from '../..';
44

55
const PLACEHOLDER_FRESHNESS = 'Add custom freshness';
66
const PLACEHOLDER_CHEF = 'Who inspected freshness?';
@@ -24,6 +24,10 @@ const Banana = () => (
2424

2525
const MyComponent = (_props: { testID?: string }) => <Text>My Component</Text>;
2626

27+
beforeEach(() => {
28+
configure({ renderer: 'internal' });
29+
});
30+
2731
test('getByTestId returns only native elements', () => {
2832
render(
2933
<View>

src/renderer/host-element.ts

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,29 @@ type FindOptions = {
1010
const instanceToHostElementMap = new WeakMap<Container | Instance, HostElement>();
1111

1212
export class HostElement {
13-
public type: string;
14-
public props: HostElementProps;
15-
public children: HostNode[];
16-
17-
constructor(type: string, props: HostElementProps, children: HostNode[]) {
18-
this.type = type;
19-
this.props = props;
20-
this.children = children;
13+
private instance: Instance | Container;
14+
15+
constructor(instance: Instance | Container) {
16+
this.instance = instance;
17+
}
18+
19+
get type(): string {
20+
return 'type' in this.instance ? this.instance.type : 'ROOT';
21+
}
22+
23+
get props(): HostElementProps {
24+
return 'props' in this.instance ? this.instance.props : {};
25+
}
26+
27+
get children(): HostNode[] {
28+
console.log('AAAA', this.instance.children);
29+
const result = this.instance.children.map((child) => HostElement.fromInstance(child));
30+
console.log('BBBB', result);
31+
return result;
32+
}
33+
34+
get $$typeof(): Symbol {
35+
return Symbol.for('react.test.json');
2136
}
2237

2338
static fromContainer(container: Container): HostElement {
@@ -26,12 +41,7 @@ export class HostElement {
2641
return hostElement;
2742
}
2843

29-
const result = new HostElement(
30-
'ROOT',
31-
{},
32-
container.children.map((child) => HostElement.fromInstance(child)),
33-
);
34-
44+
const result = new HostElement(container);
3545
instanceToHostElementMap.set(container, result);
3646
return result;
3747
}
@@ -47,19 +57,14 @@ export class HostElement {
4757
return hostElement;
4858
}
4959

50-
const result = new HostElement(
51-
instance.type,
52-
instance.props,
53-
instance.children.map((child) => HostElement.fromInstance(child)),
54-
);
55-
60+
const result = new HostElement(instance);
5661
instanceToHostElementMap.set(instance, result);
5762
return result;
5863
}
5964

6065
default:
6166
// @ts-expect-error
62-
throw new Error(`Unexpected node type in toJSON: ${instance.tag}`);
67+
throw new Error(`Unexpected node type in HostElement.fromInstance: ${instance.tag}`);
6368
}
6469
}
6570

src/renderer/render-to-json.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Instance, TextInstance } from './reconciler';
1+
import { Container, Instance, TextInstance } from './reconciler';
22

33
export type JsonNode = JsonInstance | string;
44

@@ -9,8 +9,8 @@ export type JsonInstance = {
99
$$typeof: Symbol;
1010
};
1111

12-
export function renderToJson(instance: Instance | TextInstance): JsonNode | null {
13-
if (instance.isHidden) {
12+
export function renderToJson(instance: Container | Instance | TextInstance): JsonNode | null {
13+
if (`isHidden` in instance && instance.isHidden) {
1414
// Omit timed out children from output entirely. This seems like the least
1515
// surprising behavior. We could perhaps add a separate API that includes
1616
// them, if it turns out people need it.
@@ -54,6 +54,34 @@ export function renderToJson(instance: Instance | TextInstance): JsonNode | null
5454
return result;
5555
}
5656

57+
case 'CONTAINER': {
58+
let renderedChildren = null;
59+
if (instance.children?.length) {
60+
for (let i = 0; i < instance.children.length; i++) {
61+
const renderedChild = renderToJson(instance.children[i]);
62+
if (renderedChild !== null) {
63+
if (renderedChildren === null) {
64+
renderedChildren = [renderedChild];
65+
} else {
66+
renderedChildren.push(renderedChild);
67+
}
68+
}
69+
}
70+
}
71+
72+
const result = {
73+
type: 'ROOT',
74+
props: {},
75+
children: renderedChildren,
76+
$$typeof: Symbol.for('react.test.json'),
77+
};
78+
// This is needed for JEST to format snapshot as JSX.
79+
Object.defineProperty(result, '$$typeof', {
80+
value: Symbol.for('react.test.json'),
81+
});
82+
return result;
83+
}
84+
5785
default:
5886
// @ts-expect-error
5987
throw new Error(`Unexpected node type in toJSON: ${inst.tag}`);

src/renderer/renderer.test.tsx

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import * as React from 'react';
22
import { View, Text } from 'react-native';
33
import { render } from './renderer';
44

5+
function Passthrough({ children }: { children: React.ReactNode }) {
6+
return children;
7+
}
8+
59
test('renders View', () => {
610
render(<View />);
711
expect(true).toBe(true);
@@ -12,10 +16,18 @@ test('renders Text', () => {
1216
expect(true).toBe(true);
1317
});
1418

15-
test('throws when rendering string inside View', () => {
19+
test('throws when rendering string outside of Text', () => {
1620
expect(() => render(<View>Hello</View>)).toThrowErrorMatchingInlineSnapshot(
1721
`"Text string "Hello" must be rendered inside <Text> component"`,
1822
);
23+
24+
expect(() => render(<Passthrough>Hello</Passthrough>)).toThrowErrorMatchingInlineSnapshot(
25+
`"Text string "Hello" must be rendered inside <Text> component"`,
26+
);
27+
28+
expect(() => render(<>Hello</>)).toThrowErrorMatchingInlineSnapshot(
29+
`"Text string "Hello" must be rendered inside <Text> component"`,
30+
);
1931
});
2032

2133
test('implements update()', () => {
@@ -57,20 +69,9 @@ test('implements unmount()', () => {
5769
test('implements get root()', () => {
5870
const result = render(<View testID="view" />);
5971
expect(result.root).toMatchInlineSnapshot(`
60-
HostElement {
61-
"children": [
62-
HostElement {
63-
"children": [],
64-
"props": {
65-
"children": undefined,
66-
"testID": "view",
67-
},
68-
"type": "View",
69-
},
70-
],
71-
"props": {},
72-
"type": "ROOT",
73-
}
72+
<View
73+
testID="view"
74+
/>
7475
`);
7576
});
7677

src/renderer/renderer.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { ReactElement } from 'react';
22
import { Container, TestReconciler } from './reconciler';
33
import { JsonNode, renderToJson } from './render-to-json';
4-
import { HostElement } from './host-element';
4+
import { HostElement, HostNode } from './host-element';
55

66
export type RenderResult = {
77
update: (element: ReactElement) => void;
88
unmount: () => void;
9-
root: HostElement | null;
9+
container: HostElement | null;
10+
root: HostNode | null;
1011
toJSON: () => JsonNode | JsonNode[] | null;
1112
};
1213

@@ -17,7 +18,7 @@ export function render(element: ReactElement): RenderResult {
1718
createNodeMock: () => null,
1819
};
1920

20-
let rootFiber = TestReconciler.createContainer(
21+
let containerFiber = TestReconciler.createContainer(
2122
container,
2223
0, // 0 = LegacyRoot, 1 = ConcurrentRoot
2324
null, // no hydration callback
@@ -31,7 +32,7 @@ export function render(element: ReactElement): RenderResult {
3132
null, // transitionCallbacks
3233
);
3334

34-
TestReconciler.updateContainer(element, rootFiber, null, () => {
35+
TestReconciler.updateContainer(element, containerFiber, null, () => {
3536
// eslint-disable-next-line no-console
3637
//console.log('Rendered', container?.children);
3738
});
@@ -44,32 +45,32 @@ export function render(element: ReactElement): RenderResult {
4445
// },
4546

4647
const update = (element: ReactElement) => {
47-
if (rootFiber == null || container == null) {
48+
if (containerFiber == null || container == null) {
4849
return;
4950
}
5051

51-
TestReconciler.updateContainer(element, rootFiber, null, () => {
52+
TestReconciler.updateContainer(element, containerFiber, null, () => {
5253
// eslint-disable-next-line no-console
5354
//console.log('Updated', container?.children);
5455
});
5556
};
5657

5758
const unmount = () => {
58-
if (rootFiber == null || container == null) {
59+
if (containerFiber == null || container == null) {
5960
return;
6061
}
6162

62-
TestReconciler.updateContainer(null, rootFiber, null, () => {
63+
TestReconciler.updateContainer(null, containerFiber, null, () => {
6364
// eslint-disable-next-line no-console
6465
//console.log('Unmounted', container?.children);
6566
});
6667

6768
container = null;
68-
rootFiber = null;
69+
containerFiber = null;
6970
};
7071

7172
const toJSON = () => {
72-
if (rootFiber == null || container == null || container.children.length === 0) {
73+
if (containerFiber == null || container == null || container.children.length === 0) {
7374
return null;
7475
}
7576

@@ -109,13 +110,25 @@ export function render(element: ReactElement): RenderResult {
109110
update,
110111
unmount,
111112
toJSON,
112-
get root(): HostElement {
113-
if (rootFiber == null || container == null) {
114-
throw new Error("Can't access .root on unmounted test renderer");
113+
get container(): HostElement {
114+
if (containerFiber == null || container == null) {
115+
throw new Error("Can't access .container on unmounted test renderer");
115116
}
116117

117118
return HostElement.fromContainer(container);
118119
},
120+
121+
get root(): HostNode {
122+
if (containerFiber == null || container == null) {
123+
throw new Error("Can't access .root on unmounted test renderer");
124+
}
125+
126+
if (container.children.length === 0) {
127+
throw new Error("Can't access .root on unmounted test renderer");
128+
}
129+
130+
return HostElement.fromInstance(container.children[0]);
131+
},
119132
};
120133

121134
return result;

0 commit comments

Comments
 (0)