From ac3b88c4ba6a76bd81973171eb6ac9a075cbcdde Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Wed, 25 Feb 2026 15:49:23 -0800 Subject: [PATCH] Add Fantom integration tests Summary: Add new Fantom integration tests for TouchableOpacity, achieving 40.54% line coverage for TouchableOpacity.js (from 0%). Tests cover: - Basic rendering and structure (accessible="true", rn-view output) - Style prop application (width, height, backgroundColor) - activeOpacity prop (default opacity behavior, custom style opacity) - onPress event handling via click dispatch - Disabled state behavior (prevents press, sets accessibilityState) - Children rendering - Ref instance validation (ReactNativeElement, RN:View tag name) Changelog: [Internal] Differential Revision: D94360684 --- .../__tests__/TouchableOpacity-itest.js | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-itest.js diff --git a/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-itest.js b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-itest.js new file mode 100644 index 000000000000..cdb2cd4211cf --- /dev/null +++ b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-itest.js @@ -0,0 +1,215 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {HostInstance} from 'react-native'; + +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {createRef} from 'react'; +import {Text, TouchableOpacity} from 'react-native'; +import ensureInstance from 'react-native/src/private/__tests__/utilities/ensureInstance'; +import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; + +describe('', () => { + describe('props', () => { + describe('rendering', () => { + it('renders as a view with accessible="true"', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( + , + ); + }); + }); + + describe('style', () => { + it('applies style props', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect( + root + .getRenderedOutput({ + props: ['width', 'height', 'backgroundColor'], + }) + .toJSX(), + ).toEqual( + , + ); + }); + }); + + describe('activeOpacity', () => { + it('does not render explicit opacity when using default', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + // Default opacity of 1 is not rendered as a prop + expect(root.getRenderedOutput({props: ['opacity']}).toJSX()).toEqual( + , + ); + }); + + it('renders with custom style opacity', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(root.getRenderedOutput({props: ['opacity']}).toJSX()).toEqual( + , + ); + }); + }); + + describe('onPress', () => { + it('triggers callback when the element is pressed', () => { + const elementRef = createRef(); + const onPressCallback = jest.fn(); + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const element = ensureInstance(elementRef.current, ReactNativeElement); + Fantom.dispatchNativeEvent(element, 'click'); + + expect(onPressCallback).toHaveBeenCalledTimes(1); + }); + }); + + describe('disabled', () => { + it('cannot be pressed when disabled', () => { + const elementRef = createRef(); + const onPressCallback = jest.fn(); + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const element = ensureInstance(elementRef.current, ReactNativeElement); + Fantom.dispatchNativeEvent(element, 'click'); + + expect(onPressCallback).toHaveBeenCalledTimes(0); + }); + + it('sets accessibilityState disabled to true', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + }); + + describe('children', () => { + it('renders children inside the touchable', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + Press me + , + ); + }); + + const element = ensureInstance( + root.document.documentElement.firstElementChild, + ReactNativeElement, + ); + expect(element.childNodes.length).toBe(1); + + expect(root.getRenderedOutput({props: ['accessible']}).toJSX()).toEqual( + + Press me + , + ); + }); + }); + }); + + describe('ref', () => { + describe('instance', () => { + it('is an element node', () => { + const elementRef = createRef(); + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(elementRef.current).toBeInstanceOf(ReactNativeElement); + }); + + it('uses the "RN:View" tag name', () => { + const elementRef = createRef(); + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const element = ensureInstance(elementRef.current, ReactNativeElement); + expect(element.tagName).toBe('RN:View'); + }); + }); + }); +});