diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index e1798a84c..9475fb3f6 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -35,9 +35,13 @@ test('resetToDefaults() resets config to defaults', () => { test('resetToDefaults() resets internal config to defaults', () => { configureInternal({ - hostComponentNames: { text: 'A', textInput: 'A' }, + hostComponentNames: { text: 'A', textInput: 'A', switch: 'A' }, + }); + expect(getConfig().hostComponentNames).toEqual({ + text: 'A', + textInput: 'A', + switch: 'A', }); - expect(getConfig().hostComponentNames).toEqual({ text: 'A', textInput: 'A' }); resetToDefaults(); expect(getConfig().hostComponentNames).toBe(undefined); diff --git a/src/__tests__/host-component-names.test.tsx b/src/__tests__/host-component-names.test.tsx index 1df43f267..434fdc36c 100644 --- a/src/__tests__/host-component-names.test.tsx +++ b/src/__tests__/host-component-names.test.tsx @@ -17,12 +17,17 @@ beforeEach(() => { describe('getHostComponentNames', () => { test('returns host component names from internal config', () => { configureInternal({ - hostComponentNames: { text: 'banana', textInput: 'banana' }, + hostComponentNames: { + text: 'banana', + textInput: 'banana', + switch: 'banana', + }, }); expect(getHostComponentNames()).toEqual({ text: 'banana', textInput: 'banana', + switch: 'banana', }); }); @@ -34,6 +39,7 @@ describe('getHostComponentNames', () => { expect(hostComponentNames).toEqual({ text: 'Text', textInput: 'TextInput', + switch: 'RCTSwitch', }); expect(getConfig().hostComponentNames).toBe(hostComponentNames); }); @@ -61,12 +67,17 @@ describe('configureHostComponentNamesIfNeeded', () => { expect(getConfig().hostComponentNames).toEqual({ text: 'Text', textInput: 'TextInput', + switch: 'RCTSwitch', }); }); test('does not update internal config when host component names are already configured', () => { configureInternal({ - hostComponentNames: { text: 'banana', textInput: 'banana' }, + hostComponentNames: { + text: 'banana', + textInput: 'banana', + switch: 'banana', + }, }); configureHostComponentNamesIfNeeded(); @@ -74,6 +85,7 @@ describe('configureHostComponentNamesIfNeeded', () => { expect(getConfig().hostComponentNames).toEqual({ text: 'banana', textInput: 'banana', + switch: 'banana', }); }); diff --git a/src/__tests__/react-native-api.test.tsx b/src/__tests__/react-native-api.test.tsx index 0a1383dab..65f30c9f4 100644 --- a/src/__tests__/react-native-api.test.tsx +++ b/src/__tests__/react-native-api.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { View, Text, TextInput } from 'react-native'; +import { View, Text, TextInput, Switch } from 'react-native'; import { render } from '..'; /** @@ -19,7 +19,6 @@ test('React Native API assumption: renders single host element', () => { test('React Native API assumption: renders single host element', () => { const view = render(Hello); - expect(view.getByText('Hello')).toBe(view.getByTestId('test')); expect(view.toJSON()).toMatchInlineSnapshot(` renders single host element', ( ); - expect(view.getByText(/Hello/)).toBe(view.getByTestId('test')); - expect(view.getByText('Before')).toBe(view.getByTestId('before')); - expect(view.getByText('Deeply nested')).toBe( - view.getByTestId('deeplyNested') - ); expect(view.toJSON()).toMatchInlineSnapshot(` renders single host element', () placeholder="Placeholder" /> ); - expect(view.getByPlaceholderText('Placeholder')).toBe( - view.getByTestId('test') - ); expect(view.toJSON()).toMatchInlineSnapshot(` renders single host element', () /> `); }); + +test('React Native API assumption: with nested Text renders single host element', () => { + const view = render( + + Hello + + ); + + expect(view.toJSON()).toMatchInlineSnapshot(` + + + Hello + + + `); +}); + +test('React Native API assumption: renders single host element', () => { + const view = render( + + ); + + expect(view.toJSON()).toMatchInlineSnapshot(` + + `); +}); diff --git a/src/config.ts b/src/config.ts index 3528391e3..5788f4fba 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,6 +23,7 @@ export type ConfigAliasOptions = { export type HostComponentNames = { text: string; textInput: string; + switch: string; }; export type InternalConfig = Config & { diff --git a/src/helpers/__tests__/component-tree.test.tsx b/src/helpers/__tests__/component-tree.test.tsx index 869a76131..e275a58be 100644 --- a/src/helpers/__tests__/component-tree.test.tsx +++ b/src/helpers/__tests__/component-tree.test.tsx @@ -4,11 +4,8 @@ import { render } from '../..'; import { getHostChildren, getHostParent, - getHostSelf, getHostSelves, getHostSiblings, - getCompositeParentOfType, - isHostElementForType, } from '../component-tree'; function ZeroHostChildren() { @@ -106,72 +103,6 @@ describe('getHostChildren()', () => { }); }); -describe('getHostSelf()', () => { - it('returns passed element for host components', () => { - const view = render( - - - - - - - ); - - const hostSubject = view.getByTestId('subject'); - expect(getHostSelf(hostSubject)).toEqual(hostSubject); - - const hostSibling = view.getByTestId('sibling'); - expect(getHostSelf(hostSibling)).toEqual(hostSibling); - - const hostParent = view.getByTestId('parent'); - expect(getHostSelf(hostParent)).toEqual(hostParent); - - const hostGrandparent = view.getByTestId('grandparent'); - expect(getHostSelf(hostGrandparent)).toEqual(hostGrandparent); - }); - - it('returns single host child for React Native composite components', () => { - const view = render( - - Text - - - ); - - const compositeText = view.UNSAFE_getByType(Text); - const hostText = view.getByTestId('text'); - expect(getHostSelf(compositeText)).toEqual(hostText); - - const compositeTextInput = view.UNSAFE_getByType(TextInput); - const hostTextInput = view.getByTestId('textInput'); - expect(getHostSelf(compositeTextInput)).toEqual(hostTextInput); - }); - - it('throws on non-single host children elements for custom composite components', () => { - const view = render( - - - - - ); - - const zeroCompositeComponent = view.UNSAFE_getByType(ZeroHostChildren); - expect(() => getHostSelf(zeroCompositeComponent)).toThrow( - 'Expected exactly one host element, but found none.' - ); - - const multipleCompositeComponent = - view.UNSAFE_getByType(MultipleHostChildren); - expect(() => getHostSelf(multipleCompositeComponent)).toThrow( - 'Expected exactly one host element, but found 3.' - ); - }); -}); - describe('getHostSelves()', () => { it('returns passed element for host components', () => { const view = render( @@ -293,38 +224,3 @@ describe('getHostSiblings()', () => { ]); }); }); - -test('getCompositeParentOfType', () => { - const root = render( - - - - ); - const hostView = root.getByTestId('view'); - const hostText = root.getByTestId('text'); - - const compositeView = getCompositeParentOfType(hostView, View); - // We get the corresponding composite component (same testID), but not the host - expect(compositeView?.type).toBe(View); - expect(compositeView?.props.testID).toBe('view'); - const compositeText = getCompositeParentOfType(hostText, Text); - expect(compositeText?.type).toBe(Text); - expect(compositeText?.props.testID).toBe('text'); - - // Checks parent type - expect(getCompositeParentOfType(hostText, View)).toBeNull(); - expect(getCompositeParentOfType(hostView, Text)).toBeNull(); - - // Ignores itself, stops if ancestor is host - expect(getCompositeParentOfType(compositeText!, Text)).toBeNull(); - expect(getCompositeParentOfType(compositeView!, View)).toBeNull(); -}); - -test('isHostElementForType', () => { - const view = render(); - const hostComponent = view.getByTestId('test'); - const compositeComponent = getCompositeParentOfType(hostComponent, View); - expect(isHostElementForType(hostComponent, View)).toBe(true); - expect(isHostElementForType(hostComponent, Text)).toBe(false); - expect(isHostElementForType(compositeComponent!, View)).toBe(false); -}); diff --git a/src/helpers/accessiblity.ts b/src/helpers/accessiblity.ts index 91a9bc246..6fa73f49f 100644 --- a/src/helpers/accessiblity.ts +++ b/src/helpers/accessiblity.ts @@ -2,12 +2,10 @@ import { AccessibilityState, AccessibilityValue, StyleSheet, - Switch, - Text, - TextInput, } from 'react-native'; import { ReactTestInstance } from 'react-test-renderer'; -import { getHostSiblings, isHostElementForType } from './component-tree'; +import { getConfig } from '../config'; +import { getHostSiblings } from './component-tree'; type IsInaccessibleOptions = { cache?: WeakMap; @@ -101,9 +99,11 @@ export function isAccessibilityElement( return element.props.accessible; } + const hostComponentNames = getConfig().hostComponentNames; + return ( - isHostElementForType(element, Text) || - isHostElementForType(element, TextInput) || - isHostElementForType(element, Switch) + element?.type === hostComponentNames?.text || + element?.type === hostComponentNames?.textInput || + element?.type === hostComponentNames?.switch ); } diff --git a/src/helpers/component-tree.ts b/src/helpers/component-tree.ts index 325f49cbd..3d3ea03eb 100644 --- a/src/helpers/component-tree.ts +++ b/src/helpers/component-tree.ts @@ -4,7 +4,7 @@ import { ReactTestInstance } from 'react-test-renderer'; * Checks if the given element is a host element. * @param element The element to check. */ -export function isHostElement(element?: ReactTestInstance | null): boolean { +export function isHostElement(element?: ReactTestInstance | null) { return typeof element?.type === 'string'; } @@ -59,32 +59,6 @@ export function getHostChildren( return hostChildren; } -/** - * Return a single host element that represent the passed host or composite element. - * - * @param element The element start traversing from. - * @throws Error if the passed element is a composite element and has no host children or has more than one host child. - * @returns If the passed element is a host element, it will return itself, if the passed element is a composite - * element, it will return a single host descendant. - */ -export function getHostSelf( - element: ReactTestInstance | null -): ReactTestInstance { - const hostSelves = getHostSelves(element); - - if (hostSelves.length === 0) { - throw new Error(`Expected exactly one host element, but found none.`); - } - - if (hostSelves.length > 1) { - throw new Error( - `Expected exactly one host element, but found ${hostSelves.length}.` - ); - } - - return hostSelves[0]; -} - /** * Return the array of host elements that represent the passed element. * @@ -113,37 +87,3 @@ export function getHostSiblings( (sibling) => !hostSelves.includes(sibling) ); } - -export function getCompositeParentOfType( - element: ReactTestInstance, - type: React.ComponentType -) { - let current = element.parent; - - while (!isHostElement(current)) { - // We're at the root of the tree - if (!current) { - return null; - } - - if (current.type === type) { - return current; - } - current = current.parent; - } - - return null; -} - -/** - * Note: this function should be generally used for core React Native types like `View`, `Text`, `TextInput`, etc. - */ -export function isHostElementForType( - element: ReactTestInstance, - type: React.ComponentType -) { - // Not a host element - if (!isHostElement(element)) return false; - - return getCompositeParentOfType(element, type) !== null; -} diff --git a/src/helpers/host-component-names.tsx b/src/helpers/host-component-names.tsx index ac91cc71a..61e31db0d 100644 --- a/src/helpers/host-component-names.tsx +++ b/src/helpers/host-component-names.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { ReactTestInstance } from 'react-test-renderer'; -import { Text, TextInput, View } from 'react-native'; +import { Switch, Text, TextInput, View } from 'react-native'; import { configureInternal, getConfig, HostComponentNames } from '../config'; import { renderWithAct } from '../render-act'; @@ -33,12 +33,14 @@ function detectHostComponentNames(): HostComponentNames { Hello + ); return { text: getByTestId(renderer.root, 'text').type as string, textInput: getByTestId(renderer.root, 'textInput').type as string, + switch: getByTestId(renderer.root, 'switch').type as string, }; } catch (error) { const errorMessage =