mirror of
https://github.com/callstack/react-native-testing-library.git
synced 2026-09-18 23:09:04 +08:00
feat: support more tests
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { render } from '..';
|
||||
import { configure, render } from '..';
|
||||
|
||||
beforeEach(() => {
|
||||
configure({ renderer: 'internal' });
|
||||
});
|
||||
|
||||
let isMounted = false;
|
||||
|
||||
@@ -26,14 +30,14 @@ afterEach(() => {
|
||||
|
||||
// This just verifies that by importing RNTL in an environment which supports afterEach (like jest)
|
||||
// we'll get automatic cleanup between tests.
|
||||
test('component is mounted, but not umounted before test ends', () => {
|
||||
test('component is mounted, but not unmounted before test ends', () => {
|
||||
const fn = jest.fn();
|
||||
render(<Test onUnmount={fn} />);
|
||||
expect(isMounted).toEqual(true);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('component is automatically umounted after first test ends', () => {
|
||||
test('component is automatically unmounted after first test ends', () => {
|
||||
expect(isMounted).toEqual(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import { render, screen } from '..';
|
||||
import { configure, render, screen } from '..';
|
||||
|
||||
beforeEach(() => {
|
||||
configure({ renderer: 'internal' });
|
||||
});
|
||||
|
||||
test('screen has the same queries as render result', () => {
|
||||
const result = render(<Text>Mt. Everest</Text>);
|
||||
|
||||
@@ -10,7 +10,7 @@ export type HostTestInstance = ReactTestInstance & { type: string };
|
||||
* @param element The element to check.
|
||||
*/
|
||||
export function isHostElement(element?: ReactTestInstance | null): element is HostTestInstance {
|
||||
return typeof element?.type === 'string';
|
||||
return typeof element?.type === 'string' && element.type !== 'CONTAINER';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,28 +35,31 @@ export function findAll(
|
||||
// Extracted from React Test Renderer
|
||||
// src: https://github.com/facebook/react/blob/8e2bde6f2751aa6335f3cef488c05c3ea08e074a/packages/react-test-renderer/src/ReactTestRenderer.js#L402
|
||||
function findAllInternal(
|
||||
root: ReactTestInstance,
|
||||
node: ReactTestInstance,
|
||||
predicate: (element: ReactTestInstance) => boolean,
|
||||
options?: FindAllOptions,
|
||||
indent: string = '',
|
||||
): HostTestInstance[] {
|
||||
const results: HostTestInstance[] = [];
|
||||
|
||||
//console.log(`${indent} 🟢 findAllInternal`, node.type, node.props);
|
||||
|
||||
// Match descendants first but do not add them to results yet.
|
||||
const matchingDescendants: HostTestInstance[] = [];
|
||||
root.children.forEach((child) => {
|
||||
node.children.forEach((child) => {
|
||||
if (typeof child === 'string') {
|
||||
return;
|
||||
}
|
||||
matchingDescendants.push(...findAllInternal(child, predicate, options));
|
||||
matchingDescendants.push(...findAllInternal(child, predicate, options, indent + ' '));
|
||||
});
|
||||
|
||||
if (
|
||||
// When matchDeepestOnly = true: add current element only if no descendants match
|
||||
(!options?.matchDeepestOnly || matchingDescendants.length === 0) &&
|
||||
isHostElement(root) &&
|
||||
predicate(root)
|
||||
isHostElement(node) &&
|
||||
predicate(node)
|
||||
) {
|
||||
results.push(root);
|
||||
results.push(node);
|
||||
}
|
||||
|
||||
// Add matching descendants after element to preserve original tree walk order.
|
||||
|
||||
@@ -2,7 +2,11 @@ import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
// Note: that must point to root of the /src to reliably replicate default import.
|
||||
import { render } from '../..';
|
||||
import { configure, render } from '../..';
|
||||
|
||||
beforeEach(() => {
|
||||
configure({ renderer: 'internal' });
|
||||
});
|
||||
|
||||
// This is check that RNTL does not extend "expect" by default, until we actually want to expose Jest matchers publically.
|
||||
test('does not extend "expect" by default', () => {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { render, screen } from '../..';
|
||||
import { configure, render, screen } from '../..';
|
||||
import '../extend-expect';
|
||||
|
||||
test('toBeBusy() basic case', () => {
|
||||
beforeEach(() => {
|
||||
configure({ renderer: 'internal' });
|
||||
});
|
||||
|
||||
test.only('toBeBusy() basic case', () => {
|
||||
render(
|
||||
<>
|
||||
<View testID="busy" accessibilityState={{ busy: true }} />
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { TextInput, View } from 'react-native';
|
||||
import { render, screen } from '../..';
|
||||
import { configure, render, screen } from '../..';
|
||||
import '../extend-expect';
|
||||
|
||||
beforeEach(() => {
|
||||
configure({ renderer: 'internal' });
|
||||
});
|
||||
|
||||
test('toHaveDisplayValue() example test', () => {
|
||||
render(<TextInput testID="text-input" value="test" />);
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import { checkHostElement, formatElement } from './utils';
|
||||
export function toBeBusy(this: jest.MatcherContext, element: ReactTestInstance) {
|
||||
checkHostElement(element, toBeBusy, this);
|
||||
|
||||
console.log('🔴 toBeBusy', element.type, element.props);
|
||||
console.log('🔴 - computeAriaBusy', computeAriaBusy(element));
|
||||
|
||||
return {
|
||||
pass: computeAriaBusy(element),
|
||||
message: () => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/* eslint-disable no-console */
|
||||
import * as React from 'react';
|
||||
import { View, Text, Pressable, TouchableOpacity } from 'react-native';
|
||||
import { render, screen } from '../..';
|
||||
import { configure, render, screen } from '../..';
|
||||
|
||||
type ConsoleLogMock = jest.Mock<typeof console.log>;
|
||||
|
||||
beforeEach(() => {
|
||||
configure({ renderer: 'internal' });
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
@@ -148,6 +149,10 @@ describe('disabled state matching', () => {
|
||||
render(<View accessibilityState={{ disabled: true }} />);
|
||||
|
||||
expect(screen.getByA11yState({ disabled: true })).toBeTruthy();
|
||||
expect(screen.queryByA11yState({ disabled: true })).toBeTruthy();
|
||||
|
||||
const x = screen.queryByA11yState({ disabled: false });
|
||||
expect(x).toMatchInlineSnapshot(`null`);
|
||||
expect(screen.queryByA11yState({ disabled: false })).toBeFalsy();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/* eslint-disable no-console */
|
||||
import * as React from 'react';
|
||||
import { Text, TouchableOpacity, View } from 'react-native';
|
||||
import { render, screen } from '../..';
|
||||
import { configure, render, screen } from '../..';
|
||||
|
||||
type ConsoleLogMock = jest.Mock<typeof console.log>;
|
||||
|
||||
beforeEach(() => {
|
||||
configure({ renderer: 'internal' });
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
@@ -30,6 +31,64 @@ const Section = () => (
|
||||
|
||||
test('getByA11yValue, queryByA11yValue, findByA11yValue', async () => {
|
||||
render(<Section />);
|
||||
expect(screen.toJSON()).toMatchInlineSnapshot(`
|
||||
[
|
||||
<Text
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": 60,
|
||||
}
|
||||
}
|
||||
>
|
||||
Title
|
||||
</Text>,
|
||||
<View
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={false}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
{
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": 60,
|
||||
"min": 40,
|
||||
}
|
||||
}
|
||||
>
|
||||
cool text
|
||||
</Text>
|
||||
</View>,
|
||||
]
|
||||
`);
|
||||
|
||||
expect(screen.getByA11yValue({ min: 40 }).props.accessibilityValue).toEqual({
|
||||
min: 40,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { TextInput, View } from 'react-native';
|
||||
import { fireEvent, render, screen } from '../..';
|
||||
import { configure, fireEvent, render, screen } from '../..';
|
||||
import '../../matchers/extend-expect';
|
||||
|
||||
beforeEach(() => {
|
||||
configure({ renderer: 'internal' });
|
||||
});
|
||||
|
||||
const PLACEHOLDER_FRESHNESS = 'Add custom freshness';
|
||||
const PLACEHOLDER_CHEF = 'Who inspected freshness?';
|
||||
const INPUT_FRESHNESS = 'Custom Freshie';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { render, screen } from '../..';
|
||||
import { configure, render, screen } from '../..';
|
||||
|
||||
const BUTTON_HINT = 'click this button';
|
||||
const TEXT_HINT = 'static text';
|
||||
@@ -35,6 +35,54 @@ const Section = () => (
|
||||
test('getByA11yHint, queryByA11yHint, findByA11yHint', async () => {
|
||||
render(<Section />);
|
||||
|
||||
expect(screen.toJSON()).toMatchInlineSnapshot(`
|
||||
[
|
||||
<Text
|
||||
accessibilityHint="static text"
|
||||
>
|
||||
Title
|
||||
</Text>,
|
||||
<View
|
||||
accessibilityHint="click this button"
|
||||
accessibilityState={
|
||||
{
|
||||
"busy": undefined,
|
||||
"checked": undefined,
|
||||
"disabled": undefined,
|
||||
"expanded": undefined,
|
||||
"selected": undefined,
|
||||
}
|
||||
}
|
||||
accessibilityValue={
|
||||
{
|
||||
"max": undefined,
|
||||
"min": undefined,
|
||||
"now": undefined,
|
||||
"text": undefined,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
>
|
||||
<Text
|
||||
accessibilityHint="static text"
|
||||
>
|
||||
Hello
|
||||
</Text>
|
||||
</View>,
|
||||
]
|
||||
`);
|
||||
|
||||
expect(screen.getByA11yHint(BUTTON_HINT).props.accessibilityHint).toEqual(BUTTON_HINT);
|
||||
const button = screen.queryByA11yHint(BUTTON_HINT);
|
||||
expect(button?.props.accessibilityHint).toEqual(BUTTON_HINT);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { TextInput, View } from 'react-native';
|
||||
import { render, screen } from '../..';
|
||||
import { configure, render, screen } from '../..';
|
||||
|
||||
const PLACEHOLDER_FRESHNESS = 'Add custom freshness';
|
||||
const PLACEHOLDER_CHEF = 'Who inspected freshness?';
|
||||
@@ -24,6 +24,10 @@ const Banana = () => (
|
||||
</View>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
configure({ renderer: 'internal' });
|
||||
});
|
||||
|
||||
test('getByPlaceholderText, queryByPlaceholderText', () => {
|
||||
render(<Banana />);
|
||||
const input = screen.getByPlaceholderText(/custom/i);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
View,
|
||||
Switch,
|
||||
} from 'react-native';
|
||||
import { render, screen } from '../..';
|
||||
import { configure, render, screen } from '../..';
|
||||
|
||||
const TEXT_LABEL = 'cool text';
|
||||
|
||||
@@ -42,6 +42,10 @@ const Section = () => (
|
||||
</>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
configure({ renderer: 'internal' });
|
||||
});
|
||||
|
||||
test('getByRole, queryByRole, findByRole', async () => {
|
||||
render(<Section />);
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ test('getByTestId returns only native elements', () => {
|
||||
<View testID="view" />
|
||||
<Button testID="button" title="Button" onPress={jest.fn()} />
|
||||
<MyComponent testID="myComponent" />
|
||||
<View testID="includes-hyphen" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
@@ -43,6 +44,7 @@ test('getByTestId returns only native elements', () => {
|
||||
expect(screen.getByTestId('textInput')).toBeTruthy();
|
||||
expect(screen.getByTestId('view')).toBeTruthy();
|
||||
expect(screen.getByTestId('button')).toBeTruthy();
|
||||
expect(screen.getByTestId('includes-hyphen')).toBeTruthy();
|
||||
|
||||
expect(screen.getAllByTestId('text')).toHaveLength(1);
|
||||
expect(screen.getAllByTestId('textInput')).toHaveLength(1);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Image, Text, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { getDefaultNormalizer, render, screen, within } from '../..';
|
||||
import { configure, getDefaultNormalizer, render, screen, within } from '../..';
|
||||
|
||||
beforeEach(() => {
|
||||
configure({ renderer: 'internal' });
|
||||
});
|
||||
|
||||
test('byText matches simple text', () => {
|
||||
render(<Text testID="text">Hello World</Text>);
|
||||
|
||||
+2
-1
@@ -93,7 +93,8 @@ function buildRenderResult(
|
||||
wrap: (element: React.ReactElement) => JSX.Element,
|
||||
) {
|
||||
const update = updateWithAct(renderer, wrap);
|
||||
const instance = renderer.root;
|
||||
// @ts-expect-error
|
||||
const instance = renderer.container ?? renderer.root;
|
||||
|
||||
const unmount = () => {
|
||||
void act(() => {
|
||||
|
||||
@@ -17,20 +17,36 @@ export class HostElement {
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return 'type' in this.instance ? this.instance.type : 'ROOT';
|
||||
return this.instance.tag === 'INSTANCE' ? this.instance.type : 'CONTAINER';
|
||||
}
|
||||
|
||||
get props(): HostElementProps {
|
||||
return 'props' in this.instance ? this.instance.props : {};
|
||||
return this.instance.tag === 'INSTANCE' ? this.instance.props : {};
|
||||
}
|
||||
|
||||
get children(): HostNode[] {
|
||||
console.log('AAAA', this.instance.children);
|
||||
const result = this.instance.children.map((child) => HostElement.fromInstance(child));
|
||||
console.log('BBBB', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
get parent(): HostElement | null {
|
||||
const parentInstance = this.instance.parent;
|
||||
if (parentInstance == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parentInstance.tag === 'CONTAINER') {
|
||||
return HostElement.fromContainer(parentInstance);
|
||||
}
|
||||
|
||||
if (parentInstance.tag === 'INSTANCE') {
|
||||
return HostElement.fromInstance(parentInstance) as HostElement;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
throw new Error(`Unexpected node type in HostElement.parent: ${this.parentInstance.tag}`);
|
||||
}
|
||||
|
||||
get $$typeof(): Symbol {
|
||||
return Symbol.for('react.test.json');
|
||||
}
|
||||
|
||||
+103
-81
@@ -11,6 +11,7 @@ export type UpdatePayload = unknown;
|
||||
export type Container = {
|
||||
tag: 'CONTAINER';
|
||||
children: Array<Instance | TextInstance>; // Added SuspenseInstance
|
||||
parent: null;
|
||||
createNodeMock: Function;
|
||||
};
|
||||
|
||||
@@ -19,6 +20,7 @@ export type Instance = {
|
||||
type: string;
|
||||
props: Props;
|
||||
children: Array<Instance | TextInstance>;
|
||||
parent: Container | Instance | null;
|
||||
rootContainer: Container;
|
||||
isHidden: boolean;
|
||||
internalHandle: OpaqueHandle;
|
||||
@@ -27,10 +29,13 @@ export type Instance = {
|
||||
export type TextInstance = {
|
||||
tag: 'TEXT';
|
||||
text: string;
|
||||
parent: Container | Instance | null;
|
||||
isHidden: boolean;
|
||||
};
|
||||
|
||||
type HostContext = {
|
||||
instance: Container | Instance;
|
||||
parentInstance: Container | Instance | null;
|
||||
isInsideText: boolean;
|
||||
};
|
||||
|
||||
@@ -101,7 +106,7 @@ const hostConfig = {
|
||||
_hostContext: HostContext,
|
||||
internalHandle: OpaqueHandle,
|
||||
): Instance {
|
||||
// console.log('createInstance', type, props);
|
||||
console.log('🔷 createInstance', type, props);
|
||||
// console.log('- RootContainer:', rootContainer);
|
||||
// console.log('- HostContext:', _hostContext);
|
||||
// console.log('- InternalHandle:', internalHandle);
|
||||
@@ -111,6 +116,7 @@ const hostConfig = {
|
||||
props,
|
||||
isHidden: false,
|
||||
children: [],
|
||||
parent: null,
|
||||
rootContainer,
|
||||
internalHandle,
|
||||
};
|
||||
@@ -132,6 +138,7 @@ const hostConfig = {
|
||||
return {
|
||||
tag: 'TEXT',
|
||||
text,
|
||||
parent: null,
|
||||
isHidden: false,
|
||||
};
|
||||
},
|
||||
@@ -155,12 +162,14 @@ const hostConfig = {
|
||||
* If you don't want to do anything here, you should return `false`.
|
||||
*/
|
||||
finalizeInitialChildren(
|
||||
_instance: Instance,
|
||||
_type: Type,
|
||||
_props: Props,
|
||||
instance: Instance,
|
||||
type: Type,
|
||||
props: Props,
|
||||
_rootContainer: Container,
|
||||
_hostContext: HostContext,
|
||||
): boolean {
|
||||
// console.log('🔷 finalizeInitialChildren', type, props);
|
||||
// console.log('🔷 - instance:', instance);
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -172,13 +181,15 @@ const hostConfig = {
|
||||
* See the meaning of `rootContainer` and `hostContext` in the `createInstance` documentation.
|
||||
*/
|
||||
prepareUpdate(
|
||||
_instance: Instance,
|
||||
_type: Type,
|
||||
_oldProps: Props,
|
||||
_newProps: Props,
|
||||
instance: Instance,
|
||||
type: Type,
|
||||
oldProps: Props,
|
||||
newProps: Props,
|
||||
_rootContainer: Container,
|
||||
_hostContext: HostContext,
|
||||
): UpdatePayload | null {
|
||||
// console.log('🔷 prepareUpdate', type, newProps, oldProps);
|
||||
// console.log('🔷 - instance:', instance);
|
||||
return UPDATE_SIGNAL;
|
||||
},
|
||||
|
||||
@@ -191,8 +202,9 @@ const hostConfig = {
|
||||
*
|
||||
* This method happens **in the render phase**. Do not mutate the tree from it.
|
||||
*/
|
||||
shouldSetTextContent(_type: Type, _props: Props): boolean {
|
||||
shouldSetTextContent(type: Type, props: Props): boolean {
|
||||
// TODO: what should RN do here?
|
||||
//console.log('🔷 shouldSetTextContent', type, props);
|
||||
return false;
|
||||
},
|
||||
|
||||
@@ -203,8 +215,8 @@ const hostConfig = {
|
||||
*
|
||||
* This method happens **in the render phase**. Do not mutate the tree from it.
|
||||
*/
|
||||
getRootHostContext(_rootContainer: Container): HostContext | null {
|
||||
return { isInsideText: false };
|
||||
getRootHostContext(rootContainer: Container): HostContext | null {
|
||||
return { isInsideText: false, instance: rootContainer, parentInstance: null };
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -228,7 +240,11 @@ const hostConfig = {
|
||||
return parentHostContext;
|
||||
}
|
||||
|
||||
return { isInsideText };
|
||||
return {
|
||||
isInsideText,
|
||||
parentInstance: parentHostContext.instance,
|
||||
instance: parentHostContext.instance,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -239,6 +255,7 @@ const hostConfig = {
|
||||
getPublicInstance(instance: Instance | TextInstance): PublicInstance {
|
||||
switch (instance.tag) {
|
||||
case 'INSTANCE': {
|
||||
//console.log('🔷 getPublicInstance', instance);
|
||||
const createNodeMock = instance.rootContainer.createNodeMock;
|
||||
const mockNode = createNodeMock({
|
||||
type: instance.type,
|
||||
@@ -385,92 +402,36 @@ const hostConfig = {
|
||||
*
|
||||
* Although this method currently runs in the commit phase, you still should not mutate any other nodes in it. If you need to do some additional work when a node is definitely connected to the visible tree, look at `commitMount`.
|
||||
*/
|
||||
appendChild(parentInstance: Instance, child: Instance | TextInstance): void {
|
||||
if (__DEV__) {
|
||||
if (!Array.isArray(parentInstance.children)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'An invalid container has been provided. ' +
|
||||
'This may indicate that another renderer is being used in addition to the test renderer. ' +
|
||||
'(For example, ReactDOM.createPortal inside of a ReactTestRenderer tree.) ' +
|
||||
'This is not supported.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const index = parentInstance.children.indexOf(child);
|
||||
if (index !== -1) {
|
||||
parentInstance.children.splice(index, 1);
|
||||
}
|
||||
|
||||
parentInstance.children.push(child);
|
||||
},
|
||||
appendChild,
|
||||
|
||||
/**
|
||||
* Same as `appendChild`, but for when a node is attached to the root container. This is useful if attaching to the root has a slightly different implementation, or if the root container nodes are of a different type than the rest of the tree.
|
||||
*/
|
||||
appendChildToContainer(container: Container, child: Instance | TextInstance): void {
|
||||
const index = container.children.indexOf(child);
|
||||
if (index !== -1) {
|
||||
container.children.splice(index, 1);
|
||||
}
|
||||
|
||||
container.children.push(child);
|
||||
},
|
||||
appendChildToContainer: appendChild,
|
||||
|
||||
/**
|
||||
* This method should mutate the `parentInstance` and place the `child` before `beforeChild` in the list of its children. For example, in the DOM this would translate to a `parentInstance.insertBefore(child, beforeChild)` call.
|
||||
*
|
||||
* Note that React uses this method both for insertions and for reordering nodes. Similar to DOM, it is expected that you can call `insertBefore` to reposition an existing child. Do not mutate any other parts of the tree from it.
|
||||
*/
|
||||
insertBefore(
|
||||
parentInstance: Instance,
|
||||
child: Instance | TextInstance,
|
||||
beforeChild: Instance | TextInstance,
|
||||
): void {
|
||||
const index = parentInstance.children.indexOf(child);
|
||||
if (index !== -1) {
|
||||
parentInstance.children.splice(index, 1);
|
||||
}
|
||||
|
||||
const beforeIndex = parentInstance.children.indexOf(beforeChild);
|
||||
parentInstance.children.splice(beforeIndex, 0, child);
|
||||
},
|
||||
insertBefore,
|
||||
|
||||
/**
|
||||
* Same as `insertBefore`, but for when a node is attached to the root container. This is useful if attaching to the root has a slightly different implementation, or if the root container nodes are of a different type than the rest of the tree.
|
||||
*/
|
||||
insertInContainerBefore(
|
||||
container: Container,
|
||||
child: Instance | TextInstance,
|
||||
beforeChild: Instance | TextInstance,
|
||||
): void {
|
||||
const index = container.children.indexOf(child);
|
||||
if (index !== -1) {
|
||||
container.children.splice(index, 1);
|
||||
}
|
||||
|
||||
const beforeIndex = container.children.indexOf(beforeChild);
|
||||
container.children.splice(beforeIndex, 0, child);
|
||||
},
|
||||
insertInContainerBefore: insertBefore,
|
||||
|
||||
/**
|
||||
* This method should mutate the `parentInstance` to remove the `child` from the list of its children.
|
||||
*
|
||||
* React will only call it for the top-level node that is being removed. It is expected that garbage collection would take care of the whole subtree. You are not expected to traverse the child tree in it.
|
||||
*/
|
||||
removeChild(parentInstance: Instance, child: Instance | TextInstance): void {
|
||||
const index = parentInstance.children.indexOf(child);
|
||||
parentInstance.children.splice(index, 1);
|
||||
},
|
||||
removeChild,
|
||||
|
||||
/**
|
||||
* Same as `removeChild`, but for when a node is detached from the root container. This is useful if attaching to the root has a slightly different implementation, or if the root container nodes are of a different type than the rest of the tree.
|
||||
*/
|
||||
removeChildFromContainer(container: Container, child: Instance | TextInstance): void {
|
||||
const index = container.children.indexOf(child);
|
||||
container.children.splice(index, 1);
|
||||
},
|
||||
removeChildFromContainer: removeChild,
|
||||
|
||||
/**
|
||||
* If you returned `true` from `shouldSetTextContent` for the previous props, but returned `false` from `shouldSetTextContent` for the next props, React will call this method so that you can clear the text content you were managing manually. For example, in the DOM you could set `node.textContent = ''`.
|
||||
@@ -501,12 +462,9 @@ const hostConfig = {
|
||||
*
|
||||
* If you never return `true` from `finalizeInitialChildren`, you can leave it empty.
|
||||
*/
|
||||
commitMount(
|
||||
_instance: Instance,
|
||||
_type: Type,
|
||||
_props: Props,
|
||||
_internalHandle: OpaqueHandle,
|
||||
): void {
|
||||
commitMount(instance: Instance, type: Type, props: Props, _internalHandle: OpaqueHandle): void {
|
||||
console.log('🔷 commitMount', type, props);
|
||||
console.log('🔷 - instance:', instance);
|
||||
// noop
|
||||
},
|
||||
|
||||
@@ -521,10 +479,13 @@ const hostConfig = {
|
||||
type: Type,
|
||||
_prevProps: Props,
|
||||
nextProps: Props,
|
||||
_internalHandle: OpaqueHandle,
|
||||
internalHandle: OpaqueHandle,
|
||||
): void {
|
||||
// console.log('🔷 commitMount', type, nextProps, _prevProps);
|
||||
// console.log('🔷 - instance:', instance);
|
||||
instance.type = type;
|
||||
instance.props = nextProps;
|
||||
instance.internalHandle = internalHandle;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -559,6 +520,9 @@ const hostConfig = {
|
||||
* This method should mutate the `container` root node and remove all children from it.
|
||||
*/
|
||||
clearContainer(container: Container): void {
|
||||
container.children.forEach((child) => {
|
||||
child.parent = null;
|
||||
});
|
||||
container.children.splice(0);
|
||||
},
|
||||
|
||||
@@ -573,3 +537,61 @@ const hostConfig = {
|
||||
};
|
||||
|
||||
export const TestReconciler = createReconciler(hostConfig);
|
||||
|
||||
/**
|
||||
* This method should mutate the `parentInstance` and add the child to its list of children. For example, in the DOM this would translate to a `parentInstance.appendChild(child)` call.
|
||||
*
|
||||
* Although this method currently runs in the commit phase, you still should not mutate any other nodes in it. If you need to do some additional work when a node is definitely connected to the visible tree, look at `commitMount`.
|
||||
*/
|
||||
function appendChild(parentInstance: Container | Instance, child: Instance | TextInstance): void {
|
||||
if (__DEV__) {
|
||||
if (!Array.isArray(parentInstance.children)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'An invalid container has been provided. ' +
|
||||
'This may indicate that another renderer is being used in addition to the test renderer. ' +
|
||||
'(For example, ReactDOM.createPortal inside of a ReactTestRenderer tree.) ' +
|
||||
'This is not supported.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const index = parentInstance.children.indexOf(child);
|
||||
if (index !== -1) {
|
||||
parentInstance.children.splice(index, 1);
|
||||
}
|
||||
|
||||
child.parent = parentInstance;
|
||||
parentInstance.children.push(child);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should mutate the `parentInstance` and place the `child` before `beforeChild` in the list of its children. For example, in the DOM this would translate to a `parentInstance.insertBefore(child, beforeChild)` call.
|
||||
*
|
||||
* Note that React uses this method both for insertions and for reordering nodes. Similar to DOM, it is expected that you can call `insertBefore` to reposition an existing child. Do not mutate any other parts of the tree from it.
|
||||
*/
|
||||
function insertBefore(
|
||||
parentInstance: Container | Instance,
|
||||
child: Instance | TextInstance,
|
||||
beforeChild: Instance | TextInstance,
|
||||
): void {
|
||||
const index = parentInstance.children.indexOf(child);
|
||||
if (index !== -1) {
|
||||
parentInstance.children.splice(index, 1);
|
||||
}
|
||||
|
||||
child.parent = parentInstance;
|
||||
const beforeIndex = parentInstance.children.indexOf(beforeChild);
|
||||
parentInstance.children.splice(beforeIndex, 0, child);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should mutate the `parentInstance` to remove the `child` from the list of its children.
|
||||
*
|
||||
* React will only call it for the top-level node that is being removed. It is expected that garbage collection would take care of the whole subtree. You are not expected to traverse the child tree in it.
|
||||
*/
|
||||
function removeChild(parentInstance: Container | Instance, child: Instance | TextInstance): void {
|
||||
const index = parentInstance.children.indexOf(child);
|
||||
parentInstance.children.splice(index, 1);
|
||||
child.parent = null;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function renderToJson(instance: Container | Instance | TextInstance): Jso
|
||||
}
|
||||
|
||||
const result = {
|
||||
type: 'ROOT',
|
||||
type: 'CONTAINER',
|
||||
props: {},
|
||||
children: renderedChildren,
|
||||
$$typeof: Symbol.for('react.test.json'),
|
||||
@@ -87,3 +87,21 @@ export function renderToJson(instance: Container | Instance | TextInstance): Jso
|
||||
throw new Error(`Unexpected node type in toJSON: ${inst.tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderChildrenToJson(children: Array<Instance | TextInstance> | null) {
|
||||
let result = null;
|
||||
if (children?.length) {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const renderedChild = renderToJson(children[i]);
|
||||
if (renderedChild !== null) {
|
||||
if (result === null) {
|
||||
result = [renderedChild];
|
||||
} else {
|
||||
result.push(renderedChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ test('renders Text', () => {
|
||||
});
|
||||
|
||||
test('throws when rendering string outside of Text', () => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
expect(() => render(<View>Hello</View>)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Text string "Hello" must be rendered inside <Text> component"`,
|
||||
);
|
||||
@@ -28,6 +30,8 @@ test('throws when rendering string outside of Text', () => {
|
||||
expect(() => render(<>Hello</>)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Text string "Hello" must be rendered inside <Text> component"`,
|
||||
);
|
||||
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('implements update()', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { Container, TestReconciler } from './reconciler';
|
||||
import { JsonNode, renderToJson } from './render-to-json';
|
||||
import { JsonNode, renderChildrenToJson, renderToJson } from './render-to-json';
|
||||
import { HostElement, HostNode } from './host-element';
|
||||
|
||||
export type RenderResult = {
|
||||
@@ -15,6 +15,7 @@ export function render(element: ReactElement): RenderResult {
|
||||
let container: Container | null = {
|
||||
tag: 'CONTAINER',
|
||||
children: [],
|
||||
parent: null,
|
||||
createNodeMock: () => null,
|
||||
};
|
||||
|
||||
@@ -25,9 +26,9 @@ export function render(element: ReactElement): RenderResult {
|
||||
false, // isStrictMode
|
||||
null, // concurrentUpdatesByDefaultOverride
|
||||
'id', // identifierPrefix
|
||||
(error) => {
|
||||
(_error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Recoverable Error', error);
|
||||
console.log('Recoverable Error', _error);
|
||||
}, // onRecoverableError
|
||||
null, // transitionCallbacks
|
||||
);
|
||||
@@ -89,21 +90,7 @@ export function render(element: ReactElement): RenderResult {
|
||||
return renderToJson(container.children[1]);
|
||||
}
|
||||
|
||||
let renderedChildren = null;
|
||||
if (container.children?.length) {
|
||||
for (let i = 0; i < container.children.length; i++) {
|
||||
const renderedChild = renderToJson(container.children[i]);
|
||||
if (renderedChild !== null) {
|
||||
if (renderedChildren === null) {
|
||||
renderedChildren = [renderedChild];
|
||||
} else {
|
||||
renderedChildren.push(renderedChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return renderedChildren;
|
||||
return renderChildrenToJson(container.children);
|
||||
};
|
||||
|
||||
const result = {
|
||||
|
||||
@@ -8,9 +8,13 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { createEventLogger, getEventsNames } from '../../../test-utils';
|
||||
import { render, screen } from '../../..';
|
||||
import { configure, render, screen } from '../../..';
|
||||
import { userEvent } from '../..';
|
||||
|
||||
// beforeEach(() => {
|
||||
// configure({ renderer: 'internal' });
|
||||
// });
|
||||
|
||||
describe('userEvent.press with real timers', () => {
|
||||
beforeEach(() => {
|
||||
jest.useRealTimers();
|
||||
|
||||
@@ -9,9 +9,13 @@ import {
|
||||
Button,
|
||||
} from 'react-native';
|
||||
import { createEventLogger, getEventsNames } from '../../../test-utils';
|
||||
import { render, screen } from '../../..';
|
||||
import { configure, render, screen } from '../../..';
|
||||
import { userEvent } from '../..';
|
||||
|
||||
// beforeEach(() => {
|
||||
// configure({ renderer: 'internal' });
|
||||
// });
|
||||
|
||||
describe('userEvent.press with fake timers', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { TextInput, TextInputProps, View } from 'react-native';
|
||||
import { createEventLogger, getEventsNames, lastEventPayload } from '../../../test-utils';
|
||||
import { render, screen } from '../../..';
|
||||
import { configure, render, screen } from '../../..';
|
||||
import { userEvent } from '../..';
|
||||
import '../../../matchers/extend-expect';
|
||||
|
||||
beforeEach(() => {
|
||||
configure({ renderer: 'internal' });
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user