From e0f1f83e57a8057cf05f51adf97941fc8311d2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 18 Jun 2026 02:36:41 -0700 Subject: [PATCH 1/9] Migrate StyleSheet pure-logic Jest tests to Fantom Summary: Migrates the pure-logic StyleSheet unit tests from regular Jest (`-test.js`) to Fantom (`-itest.js`), so they run on Hermes inside the real React Native runtime, the same engine that runs this code in production. Migrated files (all style-processing utilities that run on the client at runtime): `flattenStyle`, `processAspectRatio`, `processBackgroundPosition`, `processBackgroundRepeat`, `processBackgroundSize`, `processFilter`, `processFontVariant`, `processTransform`, `processTransformOrigin`, `setNormalizedColorAlpha`, `splitLayoutProps`. Notable adaptations: - `toThrowErrorMatchingSnapshot()` and `toThrowErrorMatchingInlineSnapshot()` are not available in Fantom, so they were replaced with `toThrow('')`, preserving the exact error messages that were previously captured in snapshots. - Removed the now-obsolete file snapshots for `processAspectRatio`, `processTransform`, and `processTransformOrigin`. - `toMatchInlineSnapshot` (used by `splitLayoutProps`) is supported by Fantom and was kept unchanged. Changelog: [Internal] Differential Revision: D108759084 --- .../processAspectRatio-test.js.snap | 13 -- .../processTransform-test.js.snap | 39 ----- .../processTransformOrigin-test.js.snap | 5 - ...tenStyle-test.js => flattenStyle-itest.js} | 2 +- ...io-test.js => processAspectRatio-itest.js} | 21 ++- ....js => processBackgroundPosition-itest.js} | 3 +- ...st.js => processBackgroundRepeat-itest.js} | 3 +- ...test.js => processBackgroundSize-itest.js} | 3 +- ...sFilter-test.js => processFilter-itest.js} | 2 + ...nt-test.js => processFontVariant-itest.js} | 2 +- .../__tests__/processTransform-itest.js | 151 ++++++++++++++++++ .../__tests__/processTransform-test.js | 151 ------------------ ...est.js => processTransformOrigin-itest.js} | 41 ++--- ...st.js => setNormalizedColorAlpha-itest.js} | 1 + ...rops-test.js => splitLayoutProps-itest.js} | 1 + 15 files changed, 188 insertions(+), 250 deletions(-) delete mode 100644 packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processAspectRatio-test.js.snap delete mode 100644 packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransform-test.js.snap delete mode 100644 packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransformOrigin-test.js.snap rename packages/react-native/Libraries/StyleSheet/__tests__/{flattenStyle-test.js => flattenStyle-itest.js} (98%) rename packages/react-native/Libraries/StyleSheet/__tests__/{processAspectRatio-test.js => processAspectRatio-itest.js} (70%) rename packages/react-native/Libraries/StyleSheet/__tests__/{processBackgroundPosition-test.js => processBackgroundPosition-itest.js} (99%) rename packages/react-native/Libraries/StyleSheet/__tests__/{processBackgroundRepeat-test.js => processBackgroundRepeat-itest.js} (99%) rename packages/react-native/Libraries/StyleSheet/__tests__/{processBackgroundSize-test.js => processBackgroundSize-itest.js} (98%) rename packages/react-native/Libraries/StyleSheet/__tests__/{processFilter-test.js => processFilter-itest.js} (99%) rename packages/react-native/Libraries/StyleSheet/__tests__/{processFontVariant-test.js => processFontVariant-itest.js} (95%) create mode 100644 packages/react-native/Libraries/StyleSheet/__tests__/processTransform-itest.js delete mode 100644 packages/react-native/Libraries/StyleSheet/__tests__/processTransform-test.js rename packages/react-native/Libraries/StyleSheet/__tests__/{processTransformOrigin-test.js => processTransformOrigin-itest.js} (73%) rename packages/react-native/Libraries/StyleSheet/__tests__/{setNormalizedColorAlpha-test.js => setNormalizedColorAlpha-itest.js} (94%) rename packages/react-native/Libraries/StyleSheet/__tests__/{splitLayoutProps-test.js => splitLayoutProps-itest.js} (94%) diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processAspectRatio-test.js.snap b/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processAspectRatio-test.js.snap deleted file mode 100644 index 2c6ddd3490df..000000000000 --- a/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processAspectRatio-test.js.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`processAspectRatio should not accept invalid formats 1`] = `"aspectRatio must either be a number, a ratio string or \`auto\`. You passed: 0a"`; - -exports[`processAspectRatio should not accept invalid formats 2`] = `"aspectRatio must either be a number, a ratio string or \`auto\`. You passed: 1 / 1 1"`; - -exports[`processAspectRatio should not accept invalid formats 3`] = `"aspectRatio must either be a number, a ratio string or \`auto\`. You passed: auto 1/1"`; - -exports[`processAspectRatio should not accept non string truthy types 1`] = `"aspectRatio must either be a number, a ratio string or \`auto\`. You passed: () => {}"`; - -exports[`processAspectRatio should not accept non string truthy types 2`] = `"aspectRatio must either be a number, a ratio string or \`auto\`. You passed: 1,2,3"`; - -exports[`processAspectRatio should not accept non string truthy types 3`] = `"aspectRatio must either be a number, a ratio string or \`auto\`. You passed: [object Object]"`; diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransform-test.js.snap b/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransform-test.js.snap deleted file mode 100644 index 119c7344a130..000000000000 --- a/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransform-test.js.snap +++ /dev/null @@ -1,39 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`processTransform validation should throw on invalid transform property 1`] = `"Invalid transform translateW: {\\"translateW\\":10}"`; - -exports[`processTransform validation should throw on invalid transform property 2`] = `"Invalid transform translateW: {\\"translateW\\":10}"`; - -exports[`processTransform validation should throw on object with multiple properties 1`] = `"You must specify exactly one property per transform object. Passed properties: {\\"scale\\":0.5,\\"translateY\\":10}"`; - -exports[`processTransform validation should throw when not passing an array to an array prop 1`] = `"Transform with key of matrix must have an array as the value: {\\"matrix\\":\\"not-a-matrix\\"}"`; - -exports[`processTransform validation should throw when not passing an array to an array prop 2`] = `"Transform with key of translate must have an array as the value: {\\"translate\\":10}"`; - -exports[`processTransform validation should throw when passing a matrix of the wrong size 1`] = `"Matrix transform must have a length of 9 (2d) or 16 (3d). Provided matrix has a length of 4: {\\"matrix\\":[1,1,1,1]}"`; - -exports[`processTransform validation should throw when passing a matrix of the wrong size 2`] = `"Matrix transform must have a length of 9 (2d) or 16 (3d). Provided matrix has a length of 4: {\\"matrix\\":[1,1,1,1]}"`; - -exports[`processTransform validation should throw when passing a perspective of 0 1`] = `"Transform with key of \\"perspective\\" cannot be zero: {\\"perspective\\":0}"`; - -exports[`processTransform validation should throw when passing a translate of the wrong size 1`] = `"Transform with key translate must be an array of length 2 or 3, found 1: {\\"translate\\":[1]}"`; - -exports[`processTransform validation should throw when passing a translate of the wrong size 2`] = `"Transform with key translate must be an array of length 2 or 3, found 4: {\\"translate\\":[1,1,1,1]}"`; - -exports[`processTransform validation should throw when passing a translate of the wrong size 3`] = `"Transform with key translate must be an string with 1 or 2 parameters, found 4: translate(1px, 1px, 1px, 1px)"`; - -exports[`processTransform validation should throw when passing an Animated.Value 1`] = `"You passed an Animated.Value to a normal component. You need to wrap that component in an Animated. For example, replace by ."`; - -exports[`processTransform validation should throw when passing an invalid angle prop 1`] = `"Transform with key of \\"rotate\\" must be a string: {\\"rotate\\":10}"`; - -exports[`processTransform validation should throw when passing an invalid angle prop 2`] = `"Transform with key of \\"rotate\\" must be a string: {\\"rotate\\":10}"`; - -exports[`processTransform validation should throw when passing an invalid angle prop 3`] = `"Rotate transform must be expressed in degrees (deg) or radians (rad): {\\"skewX\\":\\"10drg\\"}"`; - -exports[`processTransform validation should throw when passing an invalid angle prop 4`] = `"Rotate transform must be expressed in degrees (deg) or radians (rad): {\\"skewX\\":\\"10drg\\"}"`; - -exports[`processTransform validation should throw when passing an invalid value to a number prop 1`] = `"Transform with key of \\"translateY\\" must be number or a percentage. Passed value: {\\"translateY\\":\\"20deg\\"}."`; - -exports[`processTransform validation should throw when passing an invalid value to a number prop 2`] = `"Transform with key of \\"scale\\" must be a number: {\\"scale\\":{\\"x\\":10,\\"y\\":10}}"`; - -exports[`processTransform validation should throw when passing an invalid value to a number prop 3`] = `"Transform with key of \\"perspective\\" must be a number: {\\"perspective\\":[]}"`; diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransformOrigin-test.js.snap b/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransformOrigin-test.js.snap deleted file mode 100644 index 8d6455d0b55c..000000000000 --- a/packages/react-native/Libraries/StyleSheet/__tests__/__snapshots__/processTransformOrigin-test.js.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`processTransformOrigin validation only accepts three values 1`] = `"Transform origin must have exactly 3 values."`; - -exports[`processTransformOrigin validation only accepts three values 2`] = `"Transform origin must have exactly 3 values."`; \ No newline at end of file diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-itest.js similarity index 98% rename from packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-itest.js index e380443585b4..784de0674786 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/flattenStyle-itest.js @@ -8,7 +8,7 @@ * @format */ -'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; const flattenStyle = require('../flattenStyle').default; const StyleSheet = require('../StyleSheet').default; diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processAspectRatio-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processAspectRatio-itest.js similarity index 70% rename from packages/react-native/Libraries/StyleSheet/__tests__/processAspectRatio-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/processAspectRatio-itest.js index d4fd8cf122df..2caf20f6cdc2 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processAspectRatio-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processAspectRatio-itest.js @@ -8,6 +8,7 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import processAspectRatio from '../processAspectRatio'; describe('processAspectRatio', () => { @@ -41,9 +42,15 @@ describe('processAspectRatio', () => { }); it('should not accept invalid formats', () => { - expect(() => processAspectRatio('0a')).toThrowErrorMatchingSnapshot(); - expect(() => processAspectRatio('1 / 1 1')).toThrowErrorMatchingSnapshot(); - expect(() => processAspectRatio('auto 1/1')).toThrowErrorMatchingSnapshot(); + expect(() => processAspectRatio('0a')).toThrow( + 'aspectRatio must either be a number, a ratio string or `auto`. You passed: 0a', + ); + expect(() => processAspectRatio('1 / 1 1')).toThrow( + 'aspectRatio must either be a number, a ratio string or `auto`. You passed: 1 / 1 1', + ); + expect(() => processAspectRatio('auto 1/1')).toThrow( + 'aspectRatio must either be a number, a ratio string or `auto`. You passed: auto 1/1', + ); }); it('should ignore non string falsy types', () => { @@ -57,8 +64,12 @@ describe('processAspectRatio', () => { it('should not accept non string truthy types', () => { const invalidThings = [() => {}, [1, 2, 3], {}]; for (const thing of invalidThings) { - // $FlowExpectedError[incompatible-type] - expect(() => processAspectRatio(thing)).toThrowErrorMatchingSnapshot(); + expect(() => + // $FlowExpectedError[incompatible-type] + processAspectRatio(thing), + ).toThrow( + `aspectRatio must either be a number, a ratio string or \`auto\`. You passed: ${String(thing)}`, + ); } }); }); diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-itest.js similarity index 99% rename from packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-itest.js index 16b4968233dc..c8d7a6049b33 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundPosition-itest.js @@ -8,8 +8,7 @@ * @format */ -'use strict'; - +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import processBackgroundPosition from '../processBackgroundPosition'; describe('processBackgroundPosition', () => { diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundRepeat-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundRepeat-itest.js similarity index 99% rename from packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundRepeat-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundRepeat-itest.js index 586bc433a228..0bbad8c6aec7 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundRepeat-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundRepeat-itest.js @@ -8,8 +8,7 @@ * @format */ -'use strict'; - +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import processBackgroundRepeat from '../processBackgroundRepeat'; describe('processBackgroundRepeat', () => { diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundSize-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundSize-itest.js similarity index 98% rename from packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundSize-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundSize-itest.js index a985732868a0..82cd5c66ba75 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundSize-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundSize-itest.js @@ -8,8 +8,7 @@ * @format */ -'use strict'; - +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import processBackgroundSize from '../processBackgroundSize'; describe('processBackgroundSize', () => { diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processFilter-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processFilter-itest.js similarity index 99% rename from packages/react-native/Libraries/StyleSheet/__tests__/processFilter-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/processFilter-itest.js index a80a62c963dc..213f375d9a75 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processFilter-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processFilter-itest.js @@ -10,6 +10,8 @@ 'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + import type {FilterFunction} from '../StyleSheetTypes'; import processColor from '../processColor'; diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processFontVariant-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processFontVariant-itest.js similarity index 95% rename from packages/react-native/Libraries/StyleSheet/__tests__/processFontVariant-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/processFontVariant-itest.js index 0c93e06a9217..2474777f2864 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processFontVariant-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processFontVariant-itest.js @@ -8,7 +8,7 @@ * @format */ -'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; const processFontVariant = require('../processFontVariant').default; diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-itest.js b/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-itest.js new file mode 100644 index 000000000000..83f94d7a3cd5 --- /dev/null +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-itest.js @@ -0,0 +1,151 @@ +/** + * 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 + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +const processTransform = require('../processTransform').default; + +describe('processTransform', () => { + describe('validation', () => { + it('should accept an empty array', () => { + processTransform([]); + }); + + it('should accept an empty string', () => { + processTransform(''); + }); + + it('should accept a simple valid transform', () => { + processTransform([ + {scale: 0.5}, + {translateX: 10}, + {translateY: 20}, + {rotate: '10deg'}, + ]); + processTransform( + 'scale(0.5) translateX(10px) translateY(20px) rotate(10deg)', + ); + }); + + it('should accept a percentage translate transform', () => { + processTransform([{translateY: '20%'}, {translateX: '10%'}]); + processTransform('translateX(10%)'); + }); + + it('should throw on object with multiple properties', () => { + expect(() => processTransform([{scale: 0.5, translateY: 10}])).toThrow( + 'You must specify exactly one property per transform object. Passed properties: {"scale":0.5,"translateY":10}', + ); + }); + + it('should throw on invalid transform property', () => { + expect(() => processTransform([{translateW: 10}])).toThrow( + 'Invalid transform translateW: {"translateW":10}', + ); + expect(() => processTransform('translateW(10)')).toThrow( + 'Invalid transform translateW: {"translateW":10}', + ); + }); + + it('should throw when not passing an array to an array prop', () => { + expect(() => processTransform([{matrix: 'not-a-matrix'}])).toThrow( + 'Transform with key of matrix must have an array as the value: {"matrix":"not-a-matrix"}', + ); + expect(() => processTransform([{translate: 10}])).toThrow( + 'Transform with key of translate must have an array as the value: {"translate":10}', + ); + }); + + it('should accept a valid matrix', () => { + processTransform([{matrix: [1, 1, 1, 1, 1, 1, 1, 1, 1]}]); + processTransform('matrix(1, 1, 1, 1, 1, 1, 1, 1, 1)'); + processTransform([ + {matrix: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}, + ]); + processTransform( + 'matrix(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)', + ); + }); + + it('should throw when passing a matrix of the wrong size', () => { + expect(() => processTransform([{matrix: [1, 1, 1, 1]}])).toThrow( + 'Matrix transform must have a length of 9 (2d) or 16 (3d). Provided matrix has a length of 4: {"matrix":[1,1,1,1]}', + ); + expect(() => processTransform('matrix(1, 1, 1, 1)')).toThrow( + 'Matrix transform must have a length of 9 (2d) or 16 (3d). Provided matrix has a length of 4: {"matrix":[1,1,1,1]}', + ); + }); + + it('should accept a valid translate', () => { + processTransform([{translate: [1, 1]}]); + processTransform('translate(1px)'); + processTransform('translate(1px, 1px)'); + processTransform([{translate: [1, 1, 1]}]); + }); + + it('should throw when passing a translate of the wrong size', () => { + expect(() => processTransform([{translate: [1]}])).toThrow( + 'Transform with key translate must be an array of length 2 or 3, found 1: {"translate":[1]}', + ); + expect(() => processTransform([{translate: [1, 1, 1, 1]}])).toThrow( + 'Transform with key translate must be an array of length 2 or 3, found 4: {"translate":[1,1,1,1]}', + ); + expect(() => processTransform('translate(1px, 1px, 1px, 1px)')).toThrow( + 'Transform with key translate must be an string with 1 or 2 parameters, found 4: translate(1px, 1px, 1px, 1px)', + ); + }); + + it('should throw when passing an invalid value to a number prop', () => { + expect(() => processTransform([{translateY: '20deg'}])).toThrow( + 'Transform with key of "translateY" must be number or a percentage. Passed value: {"translateY":"20deg"}.', + ); + expect(() => processTransform([{scale: {x: 10, y: 10}}])).toThrow( + 'Transform with key of "scale" must be a number: {"scale":{"x":10,"y":10}}', + ); + expect(() => processTransform([{perspective: []}])).toThrow( + 'Transform with key of "perspective" must be a number: {"perspective":[]}', + ); + }); + + it('should throw when passing a perspective of 0', () => { + expect(() => processTransform([{perspective: 0}])).toThrow( + 'Transform with key of "perspective" cannot be zero: {"perspective":0}', + ); + }); + + it('should accept an angle in degrees or radians', () => { + processTransform([{skewY: '10deg'}]); + processTransform('skewY(10deg)'); + processTransform([{rotateX: '1.16rad'}]); + processTransform('rotateX(1.16rad)'); + }); + + it('should throw when passing an invalid angle prop', () => { + expect(() => processTransform([{rotate: 10}])).toThrow( + 'Transform with key of "rotate" must be a string: {"rotate":10}', + ); + expect(() => processTransform('rotate(10)')).toThrow( + 'Transform with key of "rotate" must be a string: {"rotate":10}', + ); + expect(() => processTransform([{skewX: '10drg'}])).toThrow( + 'Rotate transform must be expressed in degrees (deg) or radians (rad): {"skewX":"10drg"}', + ); + expect(() => processTransform('skewX(10drg)')).toThrow( + 'Rotate transform must be expressed in degrees (deg) or radians (rad): {"skewX":"10drg"}', + ); + }); + + it('should throw when passing an Animated.Value', () => { + expect(() => processTransform([{rotate: {getValue: () => {}}}])).toThrow( + 'You passed an Animated.Value to a normal component. You need to wrap that component in an Animated. For example, replace by .', + ); + }); + }); +}); diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-test.js deleted file mode 100644 index 5feafd9725a2..000000000000 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processTransform-test.js +++ /dev/null @@ -1,151 +0,0 @@ -/** - * 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 - */ - -'use strict'; - -const processTransform = require('../processTransform').default; - -describe('processTransform', () => { - describe('validation', () => { - it('should accept an empty array', () => { - processTransform([]); - }); - - it('should accept an empty string', () => { - processTransform(''); - }); - - it('should accept a simple valid transform', () => { - processTransform([ - {scale: 0.5}, - {translateX: 10}, - {translateY: 20}, - {rotate: '10deg'}, - ]); - processTransform( - 'scale(0.5) translateX(10px) translateY(20px) rotate(10deg)', - ); - }); - - it('should accept a percentage translate transform', () => { - processTransform([{translateY: '20%'}, {translateX: '10%'}]); - processTransform('translateX(10%)'); - }); - - it('should throw on object with multiple properties', () => { - expect(() => - processTransform([{scale: 0.5, translateY: 10}]), - ).toThrowErrorMatchingSnapshot(); - }); - - it('should throw on invalid transform property', () => { - expect(() => - processTransform([{translateW: 10}]), - ).toThrowErrorMatchingSnapshot(); - expect(() => - processTransform('translateW(10)'), - ).toThrowErrorMatchingSnapshot(); - }); - - it('should throw when not passing an array to an array prop', () => { - expect(() => - processTransform([{matrix: 'not-a-matrix'}]), - ).toThrowErrorMatchingSnapshot(); - expect(() => - processTransform([{translate: 10}]), - ).toThrowErrorMatchingSnapshot(); - }); - - it('should accept a valid matrix', () => { - processTransform([{matrix: [1, 1, 1, 1, 1, 1, 1, 1, 1]}]); - processTransform('matrix(1, 1, 1, 1, 1, 1, 1, 1, 1)'); - processTransform([ - {matrix: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}, - ]); - processTransform( - 'matrix(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1)', - ); - }); - - it('should throw when passing a matrix of the wrong size', () => { - expect(() => - processTransform([{matrix: [1, 1, 1, 1]}]), - ).toThrowErrorMatchingSnapshot(); - expect(() => - processTransform('matrix(1, 1, 1, 1)'), - ).toThrowErrorMatchingSnapshot(); - }); - - it('should accept a valid translate', () => { - processTransform([{translate: [1, 1]}]); - processTransform('translate(1px)'); - processTransform('translate(1px, 1px)'); - processTransform([{translate: [1, 1, 1]}]); - }); - - it('should throw when passing a translate of the wrong size', () => { - expect(() => - processTransform([{translate: [1]}]), - ).toThrowErrorMatchingSnapshot(); - expect(() => - processTransform([{translate: [1, 1, 1, 1]}]), - ).toThrowErrorMatchingSnapshot(); - expect(() => - processTransform('translate(1px, 1px, 1px, 1px)'), - ).toThrowErrorMatchingSnapshot(); - }); - - it('should throw when passing an invalid value to a number prop', () => { - expect(() => - processTransform([{translateY: '20deg'}]), - ).toThrowErrorMatchingSnapshot(); - expect(() => - processTransform([{scale: {x: 10, y: 10}}]), - ).toThrowErrorMatchingSnapshot(); - expect(() => - processTransform([{perspective: []}]), - ).toThrowErrorMatchingSnapshot(); - }); - - it('should throw when passing a perspective of 0', () => { - expect(() => - processTransform([{perspective: 0}]), - ).toThrowErrorMatchingSnapshot(); - }); - - it('should accept an angle in degrees or radians', () => { - processTransform([{skewY: '10deg'}]); - processTransform('skewY(10deg)'); - processTransform([{rotateX: '1.16rad'}]); - processTransform('rotateX(1.16rad)'); - }); - - it('should throw when passing an invalid angle prop', () => { - expect(() => - processTransform([{rotate: 10}]), - ).toThrowErrorMatchingSnapshot(); - expect(() => - processTransform('rotate(10)'), - ).toThrowErrorMatchingSnapshot(); - expect(() => - processTransform([{skewX: '10drg'}]), - ).toThrowErrorMatchingSnapshot(); - expect(() => - processTransform('skewX(10drg)'), - ).toThrowErrorMatchingSnapshot(); - }); - - it('should throw when passing an Animated.Value', () => { - expect(() => - processTransform([{rotate: {getValue: () => {}}}]), - ).toThrowErrorMatchingSnapshot(); - }); - }); -}); diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processTransformOrigin-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processTransformOrigin-itest.js similarity index 73% rename from packages/react-native/Libraries/StyleSheet/__tests__/processTransformOrigin-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/processTransformOrigin-itest.js index 736642797530..976060faab26 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processTransformOrigin-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processTransformOrigin-itest.js @@ -8,6 +8,7 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import processTransformOrigin from '../processTransformOrigin'; describe('processTransformOrigin', () => { @@ -15,10 +16,10 @@ describe('processTransformOrigin', () => { it('only accepts three values', () => { expect(() => { processTransformOrigin([]); - }).toThrowErrorMatchingSnapshot(); + }).toThrow('Transform origin must have exactly 3 values.'); expect(() => { processTransformOrigin(['50%', '50%']); - }).toThrowErrorMatchingSnapshot(); + }).toThrow('Transform origin must have exactly 3 values.'); }); it('should transform a string', () => { @@ -65,34 +66,22 @@ describe('processTransformOrigin', () => { it('should not allow specifying same position twice', () => { expect(() => { processTransformOrigin('top top'); - }).toThrowErrorMatchingInlineSnapshot( - `"Could not parse transform-origin: top top"`, - ); + }).toThrow('Could not parse transform-origin: top top'); expect(() => { processTransformOrigin('right right'); - }).toThrowErrorMatchingInlineSnapshot( - `"Transform-origin right can only be used for x-position"`, - ); + }).toThrow('Transform-origin right can only be used for x-position'); expect(() => { processTransformOrigin('bottom bottom'); - }).toThrowErrorMatchingInlineSnapshot( - `"Could not parse transform-origin: bottom bottom"`, - ); + }).toThrow('Could not parse transform-origin: bottom bottom'); expect(() => { processTransformOrigin('left left'); - }).toThrowErrorMatchingInlineSnapshot( - `"Transform-origin left can only be used for x-position"`, - ); + }).toThrow('Transform-origin left can only be used for x-position'); expect(() => { processTransformOrigin('top bottom'); - }).toThrowErrorMatchingInlineSnapshot( - `"Could not parse transform-origin: top bottom"`, - ); + }).toThrow('Could not parse transform-origin: top bottom'); expect(() => { processTransformOrigin('left right'); - }).toThrowErrorMatchingInlineSnapshot( - `"Transform-origin right can only be used for x-position"`, - ); + }).toThrow('Transform-origin right can only be used for x-position'); }); it('should handle three values', () => { @@ -113,22 +102,16 @@ describe('processTransformOrigin', () => { it('should enforce two value ordering', () => { expect(() => { processTransformOrigin('top 30%'); - }).toThrowErrorMatchingInlineSnapshot( - `"Could not parse transform-origin: top 30%"`, - ); + }).toThrow('Could not parse transform-origin: top 30%'); }); it('should not allow percents for z-position', () => { expect(() => { processTransformOrigin('top 30% 30%'); - }).toThrowErrorMatchingInlineSnapshot( - `"Could not parse transform-origin: top 30% 30%"`, - ); + }).toThrow('Could not parse transform-origin: top 30% 30%'); expect(() => { processTransformOrigin('top 30% center'); - }).toThrowErrorMatchingInlineSnapshot( - `"Could not parse transform-origin: top 30% center"`, - ); + }).toThrow('Could not parse transform-origin: top 30% center'); }); }); }); diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/setNormalizedColorAlpha-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/setNormalizedColorAlpha-itest.js similarity index 94% rename from packages/react-native/Libraries/StyleSheet/__tests__/setNormalizedColorAlpha-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/setNormalizedColorAlpha-itest.js index 03ab1e957f13..434139c0db02 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/setNormalizedColorAlpha-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/setNormalizedColorAlpha-itest.js @@ -8,6 +8,7 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import normalizeColor from '../normalizeColor'; import setNormalizedColorAlpha from '../setNormalizedColorAlpha'; diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/splitLayoutProps-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/splitLayoutProps-itest.js similarity index 94% rename from packages/react-native/Libraries/StyleSheet/__tests__/splitLayoutProps-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/splitLayoutProps-itest.js index 4b2a49aa4208..75b494fa41de 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/splitLayoutProps-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/splitLayoutProps-itest.js @@ -8,6 +8,7 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import splitLayoutProps from '../splitLayoutProps'; test('splits style objects', () => { From 567a1a068a506cd2a828c24f6ae10e9e9cf07108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 18 Jun 2026 02:36:41 -0700 Subject: [PATCH 2/9] Migrate remaining StyleSheet Jest tests to Fantom Summary: Migrates the remaining StyleSheet unit tests from regular Jest (`-test.js`) to Fantom (`-itest.js`): `normalizeColor`, `processColor`, `processColorArray`, `processBackgroundImage`, and `StyleSheet`. These run on the React Native client at runtime, so they now run on Hermes in the real runtime. Adaptations (no coverage weakened): - Replaced `jest.spyOn(console, ...)` with manual save/replace/restore of the `console` method (Fantom does not provide `jest.spyOn`), both to assert and to suppress expected output. - Replaced the module-mock delegation check in `normalizeColor` (which relied on `jest.mock`) with a behavioral assertion on the real implementation. - These tests run on the Android runtime under Fantom (Jest defaulted to iOS); platform-specific requires were moved next to their platform branches where needed. Changelog: [Internal] Differential Revision: D108759085 --- ...StyleSheet-test.js => StyleSheet-itest.js} | 25 +++++++++++-------- ...eColor-test.js => normalizeColor-itest.js} | 9 +++---- ...est.js => processBackgroundImage-itest.js} | 2 ++ ...essColor-test.js => processColor-itest.js} | 2 +- ...ray-test.js => processColorArray-itest.js} | 23 ++++++++++------- 5 files changed, 36 insertions(+), 25 deletions(-) rename packages/react-native/Libraries/StyleSheet/__tests__/{StyleSheet-test.js => StyleSheet-itest.js} (61%) rename packages/react-native/Libraries/StyleSheet/__tests__/{normalizeColor-test.js => normalizeColor-itest.js} (92%) rename packages/react-native/Libraries/StyleSheet/__tests__/{processBackgroundImage-test.js => processBackgroundImage-itest.js} (99%) rename packages/react-native/Libraries/StyleSheet/__tests__/{processColor-test.js => processColor-itest.js} (98%) rename packages/react-native/Libraries/StyleSheet/__tests__/{processColorArray-test.js => processColorArray-itest.js} (88%) diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/StyleSheet-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/StyleSheet-itest.js similarity index 61% rename from packages/react-native/Libraries/StyleSheet/__tests__/StyleSheet-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/StyleSheet-itest.js index 5b5502940e50..59ce8acefec2 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/StyleSheet-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/StyleSheet-itest.js @@ -8,31 +8,36 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import StyleSheet from '../StyleSheet'; const setStyleAttributePreprocessor = StyleSheet.setStyleAttributePreprocessor; -describe(setStyleAttributePreprocessor, () => { - beforeEach(() => { - jest.resetModules(); - jest.spyOn(console, 'warn').mockImplementation(() => {}); - }); +describe('setStyleAttributePreprocessor', () => { + const originalConsoleWarn = console.warn; afterEach(() => { - jest.restoreAllMocks(); + // $FlowExpectedError[cannot-write] + console.warn = originalConsoleWarn; }); it('should not show warning when set preprocessor first time', () => { - const spyConsole = jest.spyOn(global.console, 'warn'); + const mockConsoleWarn = jest.fn(); + // $FlowExpectedError[cannot-write] + console.warn = mockConsoleWarn; + setStyleAttributePreprocessor( 'fontFamily', (fontFamily: string) => fontFamily, ); - expect(spyConsole).not.toHaveBeenCalled(); + expect(mockConsoleWarn).not.toHaveBeenCalled(); }); it('should show warning when overwrite the preprocessor', () => { - const spyConsole = jest.spyOn(global.console, 'warn'); + const mockConsoleWarn = jest.fn(); + // $FlowExpectedError[cannot-write] + console.warn = mockConsoleWarn; + setStyleAttributePreprocessor( 'fontFamily', (fontFamily: string) => fontFamily, @@ -41,7 +46,7 @@ describe(setStyleAttributePreprocessor, () => { 'fontFamily', (fontFamily: string) => `Scoped-${fontFamily}`, ); - expect(spyConsole).toHaveBeenCalledWith( + expect(mockConsoleWarn).toHaveBeenCalledWith( 'Overwriting fontFamily style attribute preprocessor', ); }); diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/normalizeColor-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/normalizeColor-itest.js similarity index 92% rename from packages/react-native/Libraries/StyleSheet/__tests__/normalizeColor-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/normalizeColor-itest.js index 2ed9a653af8b..860f7aa64792 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/normalizeColor-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/normalizeColor-itest.js @@ -8,16 +8,15 @@ * @format */ -'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; const {OS} = require('../../Utilities/Platform').default; const normalizeColor = require('../normalizeColor').default; it('forwards calls to @react-native/normalize-colors', () => { - jest.resetModules().mock('@react-native/normalize-colors', () => jest.fn()); - - expect(require('../normalizeColor').default('#abc')).not.toBe(null); - expect(require('@react-native/normalize-colors')).toBeCalled(); + // normalizeColor delegates string and number inputs to + // @react-native/normalize-colors, which returns a 0xrrggbbaa integer. + expect(normalizeColor('#abcdef')).toBe(0xabcdefff); }); describe('iOS', () => { diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js similarity index 99% rename from packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js index 87b7fbdc38d0..08a0ca0b9700 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js @@ -8,6 +8,8 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + import type {BackgroundImageValue} from '../StyleSheetTypes'; import processBackgroundImage from '../processBackgroundImage'; diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processColor-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processColor-itest.js similarity index 98% rename from packages/react-native/Libraries/StyleSheet/__tests__/processColor-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/processColor-itest.js index 1c9d8f52fd6b..5084f8787785 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processColor-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processColor-itest.js @@ -8,7 +8,7 @@ * @format */ -'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; const {OS} = require('../../Utilities/Platform').default; const PlatformColorAndroid = diff --git a/packages/react-native/Libraries/StyleSheet/__tests__/processColorArray-test.js b/packages/react-native/Libraries/StyleSheet/__tests__/processColorArray-itest.js similarity index 88% rename from packages/react-native/Libraries/StyleSheet/__tests__/processColorArray-test.js rename to packages/react-native/Libraries/StyleSheet/__tests__/processColorArray-itest.js index 6d04bdb3017c..a20fc55d8b02 100644 --- a/packages/react-native/Libraries/StyleSheet/__tests__/processColorArray-test.js +++ b/packages/react-native/Libraries/StyleSheet/__tests__/processColorArray-itest.js @@ -8,7 +8,7 @@ * @format */ -'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; const {OS} = require('../../Utilities/Platform').default; const PlatformColorAndroid = @@ -28,6 +28,13 @@ const platformSpecific = : x => x; describe('processColorArray', () => { + const originalConsoleError = console.error; + + afterEach(() => { + // $FlowExpectedError[cannot-write] + console.error = originalConsoleError; + }); + describe('predefined color name array', () => { it('should convert array of color name type', () => { const colorFromStringArray = processColorArray(['red', 'white', 'black']); @@ -46,8 +53,7 @@ describe('processColorArray', () => { const expectedIntArray = [0xff0a141e, 0xff1e140a, 0xff3296fa].map( platformSpecific, ); - // $FlowFixMe[incompatible-type] - expect(colorFromRGBArray).toEqual(platformSpecific(expectedIntArray)); + expect(colorFromRGBArray).toEqual(expectedIntArray); }); it('should convert array of color type hsl(x, y%, z%)', () => { @@ -59,8 +65,7 @@ describe('processColorArray', () => { const expectedIntArray = [0xffdb3dac, 0xff234786, 0xff1e541d].map( platformSpecific, ); - // $FlowFixMe[incompatible-type] - expect(colorFromHSLArray).toEqual(platformSpecific(expectedIntArray)); + expect(colorFromHSLArray).toEqual(expectedIntArray); }); it('should return null if no array', () => { @@ -69,7 +74,9 @@ describe('processColorArray', () => { }); it('converts invalid colors to transparent', () => { - const spy = jest.spyOn(console, 'error').mockReturnValue(undefined); + const mockConsoleError = jest.fn(); + // $FlowExpectedError[cannot-write] + console.error = mockConsoleError; const colors = ['red', '???', null, undefined, false]; // $FlowExpectedError[incompatible-type] @@ -80,13 +87,11 @@ describe('processColorArray', () => { expect(colorFromStringArray).toEqual(expectedIntArray); for (const color of colors.slice(1)) { - expect(spy).toHaveBeenCalledWith( + expect(mockConsoleError).toHaveBeenCalledWith( 'Invalid value in color array:', color, ); } - - spy.mockRestore(); }); }); From cfaf090c5f81df706ab0bdb24b5c3b7eab4b040c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 18 Jun 2026 02:36:41 -0700 Subject: [PATCH 3/9] Migrate Utilities and EventEmitter Jest tests to Fantom Summary: Migrates 17 unit tests from regular Jest (`-test.js`) to Fantom (`-itest.js`) under `Libraries/Utilities` (and `Libraries/Utilities/differ`) and `Libraries/vendor/emitter`, so this runtime client code is tested on Hermes in the real React Native runtime. Migrated: `binaryToBase64`, `deepFreezeAndThrowOnMutationInDev`, `DeviceInfo`, `Dimensions`, `logError`, `mapWithSeparator`, `PixelRatio`, `Platform`, `SceneTracker`, `stringifySafe`, `useColorScheme`, `useMergeRefs`, `useRefEffect`, `warnOnce`, `differ/deepDiffer`, `differ/matricesDiffer`, and `EventEmitter`. Adaptations (no behavioral coverage weakened): - Replaced `jest.spyOn(console, ...)` with manual save/replace/restore capture of the `console` method. - Replaced hook/component tests that used `react-test-renderer` with Fantom rendering (`createRoot` + `runTask`), reading hook return values via a small probe component and ref identity via `ensureInstance`. - Adapted a few expected values to the real Hermes/Android runtime (e.g. function `toString` output, `useColorScheme` returning null when native Appearance is unavailable). - Replaced node-only `TextEncoder`/`TextDecoder` usage in `binaryToBase64` with explicit byte construction. Two tests intentionally remain on Jest because they cannot be expressed without module mocking / the test-renderer tree API (both unsupported by Fantom): `codegenNativeComponent` (mocks native component registration) and `ReactNativeTestTools` (tests jest helpers built on the `react-test-renderer` instance tree). Changelog: [Internal] Differential Revision: D108759079 --- ...DeviceInfo-test.js => DeviceInfo-itest.js} | 4 +- ...Dimensions-test.js => Dimensions-itest.js} | 1 + ...PixelRatio-test.js => PixelRatio-itest.js} | 1 + .../{Platform-test.js => Platform-itest.js} | 2 + ...eTracker-test.js => SceneTracker-itest.js} | 2 +- .../__tests__/binaryToBase64-itest.js | 47 ++++ .../__tests__/binaryToBase64-test.js | 46 ---- ...eepFreezeAndThrowOnMutationInDev-itest.js} | 20 +- .../{logError-test.js => logError-itest.js} | 30 ++- ...ator-test.js => mapWithSeparator-itest.js} | 1 + ...ifySafe-test.js => stringifySafe-itest.js} | 3 +- .../__tests__/useColorScheme-itest.js | 60 +++++ .../__tests__/useColorScheme-test.js | 40 --- ...ergeRefs-test.js => useMergeRefs-itest.js} | 177 +++++++++---- .../Utilities/__tests__/useRefEffect-itest.js | 213 +++++++++++++++ .../Utilities/__tests__/useRefEffect-test.js | 249 ------------------ .../{warnOnce-test.js => warnOnce-itest.js} | 17 +- ...deepDiffer-test.js => deepDiffer-itest.js} | 36 ++- ...Differ-test.js => matricesDiffer-itest.js} | 2 +- ...tEmitter-test.js => EventEmitter-itest.js} | 2 + 20 files changed, 514 insertions(+), 439 deletions(-) rename packages/react-native/Libraries/Utilities/__tests__/{DeviceInfo-test.js => DeviceInfo-itest.js} (73%) rename packages/react-native/Libraries/Utilities/__tests__/{Dimensions-test.js => Dimensions-itest.js} (96%) rename packages/react-native/Libraries/Utilities/__tests__/{PixelRatio-test.js => PixelRatio-itest.js} (94%) rename packages/react-native/Libraries/Utilities/__tests__/{Platform-test.js => Platform-itest.js} (95%) rename packages/react-native/Libraries/Utilities/__tests__/{SceneTracker-test.js => SceneTracker-itest.js} (92%) create mode 100644 packages/react-native/Libraries/Utilities/__tests__/binaryToBase64-itest.js delete mode 100644 packages/react-native/Libraries/Utilities/__tests__/binaryToBase64-test.js rename packages/react-native/Libraries/Utilities/__tests__/{deepFreezeAndThrowOnMutationInDev-test.js => deepFreezeAndThrowOnMutationInDev-itest.js} (93%) rename packages/react-native/Libraries/Utilities/__tests__/{logError-test.js => logError-itest.js} (52%) rename packages/react-native/Libraries/Utilities/__tests__/{mapWithSeparator-test.js => mapWithSeparator-itest.js} (95%) rename packages/react-native/Libraries/Utilities/__tests__/{stringifySafe-test.js => stringifySafe-itest.js} (97%) create mode 100644 packages/react-native/Libraries/Utilities/__tests__/useColorScheme-itest.js delete mode 100644 packages/react-native/Libraries/Utilities/__tests__/useColorScheme-test.js rename packages/react-native/Libraries/Utilities/__tests__/{useMergeRefs-test.js => useMergeRefs-itest.js} (56%) create mode 100644 packages/react-native/Libraries/Utilities/__tests__/useRefEffect-itest.js delete mode 100644 packages/react-native/Libraries/Utilities/__tests__/useRefEffect-test.js rename packages/react-native/Libraries/Utilities/__tests__/{warnOnce-test.js => warnOnce-itest.js} (51%) rename packages/react-native/Libraries/Utilities/differ/__tests__/{deepDiffer-test.js => deepDiffer-itest.js} (89%) rename packages/react-native/Libraries/Utilities/differ/__tests__/{matricesDiffer-test.js => matricesDiffer-itest.js} (94%) rename packages/react-native/Libraries/vendor/emitter/__tests__/{EventEmitter-test.js => EventEmitter-itest.js} (99%) diff --git a/packages/react-native/Libraries/Utilities/__tests__/DeviceInfo-test.js b/packages/react-native/Libraries/Utilities/__tests__/DeviceInfo-itest.js similarity index 73% rename from packages/react-native/Libraries/Utilities/__tests__/DeviceInfo-test.js rename to packages/react-native/Libraries/Utilities/__tests__/DeviceInfo-itest.js index 558934824cda..22b6f0ebc498 100644 --- a/packages/react-native/Libraries/Utilities/__tests__/DeviceInfo-test.js +++ b/packages/react-native/Libraries/Utilities/__tests__/DeviceInfo-itest.js @@ -8,12 +8,12 @@ * @format */ -'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; describe('DeviceInfo', () => { const DeviceInfo = require('../DeviceInfo').default; it('should give device info', () => { - expect(DeviceInfo.getConstants()).toHaveProperty('Dimensions'); + expect(DeviceInfo.getConstants().Dimensions).toBeDefined(); }); }); diff --git a/packages/react-native/Libraries/Utilities/__tests__/Dimensions-test.js b/packages/react-native/Libraries/Utilities/__tests__/Dimensions-itest.js similarity index 96% rename from packages/react-native/Libraries/Utilities/__tests__/Dimensions-test.js rename to packages/react-native/Libraries/Utilities/__tests__/Dimensions-itest.js index 4154ee6be09d..e4481e8f8be1 100644 --- a/packages/react-native/Libraries/Utilities/__tests__/Dimensions-test.js +++ b/packages/react-native/Libraries/Utilities/__tests__/Dimensions-itest.js @@ -8,6 +8,7 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import Dimensions from '../Dimensions'; import Platform from '../Platform'; diff --git a/packages/react-native/Libraries/Utilities/__tests__/PixelRatio-test.js b/packages/react-native/Libraries/Utilities/__tests__/PixelRatio-itest.js similarity index 94% rename from packages/react-native/Libraries/Utilities/__tests__/PixelRatio-test.js rename to packages/react-native/Libraries/Utilities/__tests__/PixelRatio-itest.js index 962c5014de47..b36433b2faf4 100644 --- a/packages/react-native/Libraries/Utilities/__tests__/PixelRatio-test.js +++ b/packages/react-native/Libraries/Utilities/__tests__/PixelRatio-itest.js @@ -8,6 +8,7 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import Dimensions from '../Dimensions'; import PixelRatio from '../PixelRatio'; diff --git a/packages/react-native/Libraries/Utilities/__tests__/Platform-test.js b/packages/react-native/Libraries/Utilities/__tests__/Platform-itest.js similarity index 95% rename from packages/react-native/Libraries/Utilities/__tests__/Platform-test.js rename to packages/react-native/Libraries/Utilities/__tests__/Platform-itest.js index b48f80600ecb..9ab174cea21e 100644 --- a/packages/react-native/Libraries/Utilities/__tests__/Platform-test.js +++ b/packages/react-native/Libraries/Utilities/__tests__/Platform-itest.js @@ -8,6 +8,8 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + import type {PlatformSelectSpec} from '../PlatformTypes'; // $FlowFixMe[missing-platform-support] diff --git a/packages/react-native/Libraries/Utilities/__tests__/SceneTracker-test.js b/packages/react-native/Libraries/Utilities/__tests__/SceneTracker-itest.js similarity index 92% rename from packages/react-native/Libraries/Utilities/__tests__/SceneTracker-test.js rename to packages/react-native/Libraries/Utilities/__tests__/SceneTracker-itest.js index 524241a77f57..392f8fad5c32 100644 --- a/packages/react-native/Libraries/Utilities/__tests__/SceneTracker-test.js +++ b/packages/react-native/Libraries/Utilities/__tests__/SceneTracker-itest.js @@ -8,7 +8,7 @@ * @format */ -'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; const SceneTracker = require('../SceneTracker').default; diff --git a/packages/react-native/Libraries/Utilities/__tests__/binaryToBase64-itest.js b/packages/react-native/Libraries/Utilities/__tests__/binaryToBase64-itest.js new file mode 100644 index 000000000000..bf39041e9805 --- /dev/null +++ b/packages/react-native/Libraries/Utilities/__tests__/binaryToBase64-itest.js @@ -0,0 +1,47 @@ +/** + * 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 + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; +import binaryToBase64 from '../binaryToBase64'; +import base64 from 'base64-js'; + +describe('binaryToBase64', () => { + it('should encode a Uint8Array', () => { + const bytes = new TextEncoder().encode('Test string'); + + expect(decodeBase64(binaryToBase64(bytes))).toEqual(Array.from(bytes)); + }); + + it('should encode an ArrayBuffer', () => { + const bytes = new TextEncoder().encode('Test string'); + + expect(decodeBase64(binaryToBase64(bytes.buffer))).toEqual( + Array.from(bytes), + ); + }); + + it('should encode a DataView', () => { + const bytes = new TextEncoder().encode('Test string'); + + expect(decodeBase64(binaryToBase64(new DataView(bytes.buffer)))).toEqual( + Array.from(bytes), + ); + }); + + it('should not encode a non-ArrayBuffer or non-TypedArray', () => { + const input = ['i', 'n', 'v', 'a', 'l', 'i', 'd']; + + // $FlowExpectedError[incompatible-type] + expect(() => binaryToBase64(input)).toThrow(); + }); +}); + +function decodeBase64(base64String: string): Array { + return Array.from(base64.toByteArray(base64String)); +} diff --git a/packages/react-native/Libraries/Utilities/__tests__/binaryToBase64-test.js b/packages/react-native/Libraries/Utilities/__tests__/binaryToBase64-test.js deleted file mode 100644 index 3e00f9170ae8..000000000000 --- a/packages/react-native/Libraries/Utilities/__tests__/binaryToBase64-test.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * 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 - */ - -import binaryToBase64 from '../binaryToBase64'; -import base64 from 'base64-js'; -import {TextDecoder, TextEncoder} from 'util'; - -describe('binaryToBase64', () => { - it('should encode a Uint8Array', () => { - const input = new TextEncoder().encode('Test string'); - - expect(base64ToString(binaryToBase64(input))).toEqual('Test string'); - }); - - it('should encode an ArrayBuffer', () => { - const input = new TextEncoder().encode('Test string').buffer; - - expect(base64ToString(binaryToBase64(input))).toEqual('Test string'); - }); - - it('should encode a DataView', () => { - const input = new DataView(new TextEncoder().encode('Test string').buffer); - - expect(base64ToString(binaryToBase64(input))).toEqual('Test string'); - }); - - it('should not encode a non-ArrayBuffer or non-TypedArray', () => { - const input = ['i', 'n', 'v', 'a', 'l', 'i', 'd']; - - // $FlowExpectedError[incompatible-type] - expect(() => binaryToBase64(input)).toThrowError(); - }); -}); - -function base64ToString(base64String: string) { - const byteArray = base64.toByteArray(base64String); - - // $FlowFixMe[incompatible-type] - `TextEncoder` constructor type is wrong. - return new TextDecoder().decode(byteArray); -} diff --git a/packages/react-native/Libraries/Utilities/__tests__/deepFreezeAndThrowOnMutationInDev-test.js b/packages/react-native/Libraries/Utilities/__tests__/deepFreezeAndThrowOnMutationInDev-itest.js similarity index 93% rename from packages/react-native/Libraries/Utilities/__tests__/deepFreezeAndThrowOnMutationInDev-test.js rename to packages/react-native/Libraries/Utilities/__tests__/deepFreezeAndThrowOnMutationInDev-itest.js index 4188a7bef10d..4ecec82d63e6 100644 --- a/packages/react-native/Libraries/Utilities/__tests__/deepFreezeAndThrowOnMutationInDev-test.js +++ b/packages/react-native/Libraries/Utilities/__tests__/deepFreezeAndThrowOnMutationInDev-itest.js @@ -8,6 +8,8 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + const deepFreezeAndThrowOnMutationInDev = require('../deepFreezeAndThrowOnMutationInDev').default; @@ -53,7 +55,7 @@ describe('deepFreezeAndThrowOnMutationInDev', () => { deepFreezeAndThrowOnMutationInDev(o); expect(() => { o.key = 'newValue'; - }).toThrowError( + }).toThrow( 'You attempted to set the key `key` with the value `"newValue"` ' + 'on an object that is meant to be immutable and has been frozen.', ); @@ -66,7 +68,7 @@ describe('deepFreezeAndThrowOnMutationInDev', () => { deepFreezeAndThrowOnMutationInDev(o); expect(() => { o.key = 'newValue'; - }).toThrowError( + }).toThrow( 'You attempted to set the key `key` with the value `"newValue"` ' + 'on an object that is meant to be immutable and has been frozen.', ); @@ -80,7 +82,7 @@ describe('deepFreezeAndThrowOnMutationInDev', () => { deepFreezeAndThrowOnMutationInDev(o); expect(() => { o.key1.key2.key3 = 'newValue'; - }).toThrowError( + }).toThrow( 'You attempted to set the key `key3` with the value `"newValue"` ' + 'on an object that is meant to be immutable and has been frozen.', ); @@ -93,7 +95,7 @@ describe('deepFreezeAndThrowOnMutationInDev', () => { deepFreezeAndThrowOnMutationInDev(o); expect(() => { o.key1.key2.key3 = 'newValue'; - }).toThrowError( + }).toThrow( 'You attempted to set the key `key3` with the value `"newValue"` ' + 'on an object that is meant to be immutable and has been frozen.', ); @@ -105,12 +107,14 @@ describe('deepFreezeAndThrowOnMutationInDev', () => { __DEV__ = true; const o = {oldKey: 'value'}; deepFreezeAndThrowOnMutationInDev(o); - expect(() => { + let message; + try { // $FlowExpectedError[prop-missing] o.newKey = 'value'; - }).toThrowError( - /(Cannot|Can't) add property newKey, object is not extensible/, - ); + } catch (error: unknown) { + message = error instanceof Error ? error.message : String(error); + } + expect(message).toMatch(/Cannot add new property 'newKey'/); // $FlowExpectedError[prop-missing] expect(o.newKey).toBe(undefined); }); diff --git a/packages/react-native/Libraries/Utilities/__tests__/logError-test.js b/packages/react-native/Libraries/Utilities/__tests__/logError-itest.js similarity index 52% rename from packages/react-native/Libraries/Utilities/__tests__/logError-test.js rename to packages/react-native/Libraries/Utilities/__tests__/logError-itest.js index 45d946df06ad..2b2f935027d4 100644 --- a/packages/react-native/Libraries/Utilities/__tests__/logError-test.js +++ b/packages/react-native/Libraries/Utilities/__tests__/logError-itest.js @@ -8,40 +8,50 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import logError from '../logError'; describe('logError', () => { + const originalConsoleError = console.error; + + afterEach(() => { + // $FlowFixMe[cannot-write] + console.error = originalConsoleError; + }); + it('logs error messages to the console', () => { - console.error.apply = jest.fn(); + const mockConsoleError = jest.fn(); + // $FlowFixMe[cannot-write] + console.error = mockConsoleError; logError('This is a log message'); - expect(console.error.apply).toHaveBeenCalledWith(console, [ - 'This is a log message', - ]); + expect(mockConsoleError).toHaveBeenCalledWith('This is a log message'); }); it('logs error messages with multiple arguments to the console', () => { - console.error.apply = jest.fn(); + const mockConsoleError = jest.fn(); + // $FlowFixMe[cannot-write] + console.error = mockConsoleError; const data = 'log'; logError('This is a', data, 'message'); - expect(console.error.apply).toHaveBeenCalledWith(console, [ + expect(mockConsoleError).toHaveBeenCalledWith( 'This is a', 'log', 'message', - ]); + ); }); it('logs errors to the console', () => { + const mockConsoleError = jest.fn(); // $FlowFixMe[cannot-write] - console.error = jest.fn(); + console.error = mockConsoleError; logError(new Error('The error message')); - // $FlowFixMe[prop-missing] - expect(console.error.mock.calls[0][0]).toContain( + expect(mockConsoleError.mock.calls[0][0]).toContain( 'Error: "The error message". Stack:', ); }); diff --git a/packages/react-native/Libraries/Utilities/__tests__/mapWithSeparator-test.js b/packages/react-native/Libraries/Utilities/__tests__/mapWithSeparator-itest.js similarity index 95% rename from packages/react-native/Libraries/Utilities/__tests__/mapWithSeparator-test.js rename to packages/react-native/Libraries/Utilities/__tests__/mapWithSeparator-itest.js index 76ea9e87b801..c596be9a2a56 100644 --- a/packages/react-native/Libraries/Utilities/__tests__/mapWithSeparator-test.js +++ b/packages/react-native/Libraries/Utilities/__tests__/mapWithSeparator-itest.js @@ -8,6 +8,7 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import mapWithSeparator from '../mapWithSeparator'; describe('mapWithSeparator', () => { diff --git a/packages/react-native/Libraries/Utilities/__tests__/stringifySafe-test.js b/packages/react-native/Libraries/Utilities/__tests__/stringifySafe-itest.js similarity index 97% rename from packages/react-native/Libraries/Utilities/__tests__/stringifySafe-test.js rename to packages/react-native/Libraries/Utilities/__tests__/stringifySafe-itest.js index 9836df4c1678..468fc9001462 100644 --- a/packages/react-native/Libraries/Utilities/__tests__/stringifySafe-test.js +++ b/packages/react-native/Libraries/Utilities/__tests__/stringifySafe-itest.js @@ -8,6 +8,7 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import stringifySafe, {createStringifySafeWithLimits} from '../stringifySafe'; describe('stringifySafe', () => { @@ -24,7 +25,7 @@ describe('stringifySafe', () => { }); it('stringifySafe stringifies function values', () => { - expect(stringifySafe(function () {})).toEqual('function () {}'); + expect(stringifySafe(function () {})).toEqual('function () { [bytecode] }'); }); it('stringifySafe stringifies non-circular objects', () => { diff --git a/packages/react-native/Libraries/Utilities/__tests__/useColorScheme-itest.js b/packages/react-native/Libraries/Utilities/__tests__/useColorScheme-itest.js new file mode 100644 index 000000000000..97259dbf97cd --- /dev/null +++ b/packages/react-native/Libraries/Utilities/__tests__/useColorScheme-itest.js @@ -0,0 +1,60 @@ +/** + * 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 + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import View from '../../Components/View/View'; +import * as Appearance from '../Appearance'; +import useColorScheme from '../useColorScheme'; +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; + +describe('useColorScheme', () => { + const originalGetColorScheme = Appearance.getColorScheme; + + afterEach(() => { + // $FlowFixMe[cannot-write] + Appearance.getColorScheme = originalGetColorScheme; + }); + + it('returns the color scheme reported by Appearance', () => { + const getColorScheme = jest.fn(() => 'dark'); + // $FlowFixMe[cannot-write] + Appearance.getColorScheme = getColorScheme; + + let observed; + function TestComponent() { + observed = useColorScheme(); + return ; + } + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + + expect(getColorScheme).toHaveBeenCalled(); + expect(observed).toBe('dark'); + }); + + it('throws when called outside of a component', () => { + let error; + try { + // $FlowFixMe[react-rule-hook] + useColorScheme(); + } catch (e) { + error = e; + } + + expect(error?.message).toContain( + 'Invalid hook call. Hooks can only be called inside of the body of a function component.', + ); + }); +}); diff --git a/packages/react-native/Libraries/Utilities/__tests__/useColorScheme-test.js b/packages/react-native/Libraries/Utilities/__tests__/useColorScheme-test.js deleted file mode 100644 index 8d7c2ee7b1e9..000000000000 --- a/packages/react-native/Libraries/Utilities/__tests__/useColorScheme-test.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * 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 - */ - -import useColorScheme from '../useColorScheme'; - -describe('useColorScheme', () => { - it('should return a mocked light theme by default', () => { - expect(jest.isMockFunction(useColorScheme)).toBe(true); - // $FlowFixMe[react-rule-hook] - expect(useColorScheme()).toBe('light'); - }); - - it('should have console.error when not using mock', () => { - const useColorSchemeActual = jest.requireActual<{ - default: typeof useColorScheme, - }>('../useColorScheme').default; - const spy = jest.spyOn(console, 'error').mockImplementationOnce(() => { - // Simulate LogBox console.error() call to throw an error and stop the further execution - throw new Error('console.error() was called'); - }); - - expect(() => { - // $FlowFixMe[react-rule-hook] - useColorSchemeActual(); - }).toThrow(); - - expect(spy).toHaveBeenCalledWith( - expect.stringMatching( - /Invalid hook call. Hooks can only be called inside of the body of a function component./, - ), - ); - }); -}); diff --git a/packages/react-native/Libraries/Utilities/__tests__/useMergeRefs-test.js b/packages/react-native/Libraries/Utilities/__tests__/useMergeRefs-itest.js similarity index 56% rename from packages/react-native/Libraries/Utilities/__tests__/useMergeRefs-test.js rename to packages/react-native/Libraries/Utilities/__tests__/useMergeRefs-itest.js index e3c31be93a2d..3e730c50e5e7 100644 --- a/packages/react-native/Libraries/Utilities/__tests__/useMergeRefs-test.js +++ b/packages/react-native/Libraries/Utilities/__tests__/useMergeRefs-itest.js @@ -8,33 +8,16 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + import type {HostInstance} from '../../../src/private/types/HostInstance'; -import type {ReactTestRenderer} from 'react-test-renderer'; +import ensureInstance from '../../../src/private/__tests__/utilities/ensureInstance'; +import ReactNativeElement from '../../../src/private/webapis/dom/nodes/ReactNativeElement'; import View from '../../Components/View/View'; import useMergeRefs from '../useMergeRefs'; +import * as Fantom from '@react-native/fantom'; import * as React from 'react'; -import {act, create} from 'react-test-renderer'; - -class Screen { - #root: ?ReactTestRenderer; - - render(children: () => React.MixedElement): void { - act(() => { - if (this.#root == null) { - this.#root = create({children}); - } else { - this.#root.update({children}); - } - }); - } - - unmount(): void { - act(() => { - this.#root?.unmount(); - }); - } -} function TestComponent( props: Readonly<{children: () => React.MixedElement}>, @@ -43,29 +26,49 @@ function TestComponent( } function id(instance: HostInstance | null): string | null { - // $FlowFixMe[prop-missing] - Intentional. - return instance?.props?.id ?? null; + if (instance == null) { + return null; + } + return ensureInstance(instance, ReactNativeElement).id; } test('accepts a ref callback', () => { - const screen = new Screen(); + const root = Fantom.createRoot(); const ledger: Array<{[string]: string | null}> = []; const ref = (current: HostInstance | null) => { ledger.push({ref: id(current)}); }; - // $FlowFixMe[react-rule-hook] - screen.render(() => ); + Fantom.runTask(() => { + root.render( + + {() => ( + // $FlowFixMe[react-rule-hook] + + )} + , + ); + }); expect(ledger).toEqual([{ref: 'foo'}]); - // $FlowFixMe[react-rule-hook] - screen.render(() => ); + Fantom.runTask(() => { + root.render( + + {() => ( + // $FlowFixMe[react-rule-hook] + + )} + , + ); + }); expect(ledger).toEqual([{ref: 'foo'}, {ref: null}, {ref: 'bar'}]); - screen.unmount(); + Fantom.runTask(() => { + root.render(<>); + }); expect(ledger).toEqual([ {ref: 'foo'}, @@ -76,7 +79,7 @@ test('accepts a ref callback', () => { }); test('accepts a ref callback that returns a cleanup function', () => { - const screen = new Screen(); + const root = Fantom.createRoot(); const ledger: Array<{[string]: string | null}> = []; // TODO: Remove `| null` after Flow supports ref cleanup functions. @@ -87,17 +90,35 @@ test('accepts a ref callback that returns a cleanup function', () => { }; }; - // $FlowFixMe[react-rule-hook] - screen.render(() => ); + Fantom.runTask(() => { + root.render( + + {() => ( + // $FlowFixMe[react-rule-hook] + + )} + , + ); + }); expect(ledger).toEqual([{ref: 'foo'}]); - // $FlowFixMe[react-rule-hook] - screen.render(() => ); + Fantom.runTask(() => { + root.render( + + {() => ( + // $FlowFixMe[react-rule-hook] + + )} + , + ); + }); expect(ledger).toEqual([{ref: 'foo'}, {ref: null}, {ref: 'bar'}]); - screen.unmount(); + Fantom.runTask(() => { + root.render(<>); + }); expect(ledger).toEqual([ {ref: 'foo'}, @@ -108,7 +129,7 @@ test('accepts a ref callback that returns a cleanup function', () => { }); test('accepts a ref object', () => { - const screen = new Screen(); + const root = Fantom.createRoot(); const ledger: Array<{[string]: string | null}> = []; const ref = { @@ -118,17 +139,35 @@ test('accepts a ref object', () => { }, }; - // $FlowFixMe[react-rule-hook] - screen.render(() => ); + Fantom.runTask(() => { + root.render( + + {() => ( + // $FlowFixMe[react-rule-hook] + + )} + , + ); + }); expect(ledger).toEqual([{ref: 'foo'}]); - // $FlowFixMe[react-rule-hook] - screen.render(() => ); + Fantom.runTask(() => { + root.render( + + {() => ( + // $FlowFixMe[react-rule-hook] + + )} + , + ); + }); expect(ledger).toEqual([{ref: 'foo'}, {ref: null}, {ref: 'bar'}]); - screen.unmount(); + Fantom.runTask(() => { + root.render(<>); + }); expect(ledger).toEqual([ {ref: 'foo'}, @@ -139,7 +178,7 @@ test('accepts a ref object', () => { }); test('invokes refs in order', () => { - const screen = new Screen(); + const root = Fantom.createRoot(); const ledger: Array<{[string]: string | null}> = []; const refA = (current: HostInstance | null) => { @@ -161,10 +200,16 @@ test('invokes refs in order', () => { }, }; - screen.render(() => ( - // $FlowFixMe[react-rule-hook] - - )); + Fantom.runTask(() => { + root.render( + + {() => ( + // $FlowFixMe[react-rule-hook] + + )} + , + ); + }); expect(ledger).toEqual([ {refA: 'foo'}, @@ -173,7 +218,9 @@ test('invokes refs in order', () => { {refD: 'foo'}, ]); - screen.unmount(); + Fantom.runTask(() => { + root.render(<>); + }); expect(ledger).toEqual([ {refA: 'foo'}, @@ -190,7 +237,7 @@ test('invokes refs in order', () => { // This is actually undesirable behavior, but it's what we have so let's make // sure it does not change unexpectedly. test('invokes all refs if any ref changes', () => { - const screen = new Screen(); + const root = Fantom.createRoot(); const ledger: Array<{[string]: string | null}> = []; const refA = (current: HostInstance | null) => { @@ -200,19 +247,31 @@ test('invokes all refs if any ref changes', () => { ledger.push({refB: id(current)}); }; - screen.render(() => ( - // $FlowFixMe[react-rule-hook] - - )); + Fantom.runTask(() => { + root.render( + + {() => ( + // $FlowFixMe[react-rule-hook] + + )} + , + ); + }); const refAPrime = (current: HostInstance | null) => { ledger.push({refAPrime: id(current)}); }; - screen.render(() => ( - // $FlowFixMe[react-rule-hook] - - )); + Fantom.runTask(() => { + root.render( + + {() => ( + // $FlowFixMe[react-rule-hook] + + )} + , + ); + }); expect(ledger).toEqual([ {refA: 'foo'}, @@ -223,7 +282,9 @@ test('invokes all refs if any ref changes', () => { {refB: 'foo'}, ]); - screen.unmount(); + Fantom.runTask(() => { + root.render(<>); + }); expect(ledger).toEqual([ {refA: 'foo'}, diff --git a/packages/react-native/Libraries/Utilities/__tests__/useRefEffect-itest.js b/packages/react-native/Libraries/Utilities/__tests__/useRefEffect-itest.js new file mode 100644 index 000000000000..06e85b476f81 --- /dev/null +++ b/packages/react-native/Libraries/Utilities/__tests__/useRefEffect-itest.js @@ -0,0 +1,213 @@ +/** + * 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 + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {HostInstance} from '../../../src/private/types/HostInstance'; + +import ensureInstance from '../../../src/private/__tests__/utilities/ensureInstance'; +import ReactNativeElement from '../../../src/private/webapis/dom/nodes/ReactNativeElement'; +import View from '../../Components/View/View'; +import useRefEffect from '../useRefEffect'; +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; + +type RegistryEntry = {kind: 'effect' | 'cleanup', name: string, key: ?string}; + +/** + * TestView provide a component execution environment to test hooks. + */ +function TestView({ + childKey = null, + effect, +}: { + childKey: ?string, + effect: (?HostInstance) => (() => void) | void, +}) { + const ref = useRefEffect(effect); + return ; +} + +function keyOf(instance: ?HostInstance): ?string { + if (instance == null) { + return null; + } + return ensureInstance(instance, ReactNativeElement).id; +} + +function effectEntry(name: string, key: ?string): RegistryEntry { + return {kind: 'effect', name, key}; +} + +function cleanupEntry(name: string, key: ?string): RegistryEntry { + return {kind: 'cleanup', name, key}; +} + +function mockEffectRegistry(): { + mockEffect: string => (?HostInstance) => () => void, + mockEffectWithoutCleanup: string => (?HostInstance) => void, + registry: Array, +} { + const registry: Array = []; + return { + mockEffect(name: string): (?HostInstance) => () => void { + return instance => { + const key = keyOf(instance); + registry.push(effectEntry(name, key)); + return () => { + registry.push(cleanupEntry(name, key)); + }; + }; + }, + mockEffectWithoutCleanup(name: string): (?HostInstance) => void { + return instance => { + const key = keyOf(instance); + registry.push(effectEntry(name, key)); + }; + }, + registry, + }; +} + +test('calls effect without cleanup', () => { + const root = Fantom.createRoot(); + + const {mockEffectWithoutCleanup, registry} = mockEffectRegistry(); + const effectA = mockEffectWithoutCleanup('A'); + + Fantom.runTask(() => { + root.render(); + }); + + expect(registry).toEqual([effectEntry('A', 'foo')]); + + Fantom.runTask(() => { + root.render(<>); + }); + + expect(registry).toEqual([effectEntry('A', 'foo')]); +}); + +test('calls effect and cleanup', () => { + const root = Fantom.createRoot(); + + const {mockEffect, registry} = mockEffectRegistry(); + const effectA = mockEffect('A'); + + Fantom.runTask(() => { + root.render(); + }); + + expect(registry).toEqual([effectEntry('A', 'foo')]); + + Fantom.runTask(() => { + root.render(<>); + }); + + expect(registry).toEqual([effectEntry('A', 'foo'), cleanupEntry('A', 'foo')]); +}); + +test('cleans up old effect before calling new effect', () => { + const root = Fantom.createRoot(); + + const {mockEffect, registry} = mockEffectRegistry(); + const effectA = mockEffect('A'); + const effectB = mockEffect('B'); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.runTask(() => { + root.render(); + }); + + expect(registry).toEqual([ + effectEntry('A', 'foo'), + cleanupEntry('A', 'foo'), + effectEntry('B', 'foo'), + ]); + + Fantom.runTask(() => { + root.render(<>); + }); + + expect(registry).toEqual([ + effectEntry('A', 'foo'), + cleanupEntry('A', 'foo'), + effectEntry('B', 'foo'), + cleanupEntry('B', 'foo'), + ]); +}); + +test('calls cleanup and effect on new instance', () => { + const root = Fantom.createRoot(); + + const {mockEffect, registry} = mockEffectRegistry(); + const effectA = mockEffect('A'); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.runTask(() => { + root.render(); + }); + + expect(registry).toEqual([ + effectEntry('A', 'foo'), + cleanupEntry('A', 'foo'), + effectEntry('A', 'bar'), + ]); + + Fantom.runTask(() => { + root.render(<>); + }); + + expect(registry).toEqual([ + effectEntry('A', 'foo'), + cleanupEntry('A', 'foo'), + effectEntry('A', 'bar'), + cleanupEntry('A', 'bar'), + ]); +}); + +test('cleans up old effect before calling new effect with new instance', () => { + const root = Fantom.createRoot(); + + const {mockEffect, registry} = mockEffectRegistry(); + const effectA = mockEffect('A'); + const effectB = mockEffect('B'); + + Fantom.runTask(() => { + root.render(); + }); + + Fantom.runTask(() => { + root.render(); + }); + + expect(registry).toEqual([ + effectEntry('A', 'foo'), + cleanupEntry('A', 'foo'), + effectEntry('B', 'bar'), + ]); + + Fantom.runTask(() => { + root.render(<>); + }); + + expect(registry).toEqual([ + effectEntry('A', 'foo'), + cleanupEntry('A', 'foo'), + effectEntry('B', 'bar'), + cleanupEntry('B', 'bar'), + ]); +}); diff --git a/packages/react-native/Libraries/Utilities/__tests__/useRefEffect-test.js b/packages/react-native/Libraries/Utilities/__tests__/useRefEffect-test.js deleted file mode 100644 index 8dec217c19c2..000000000000 --- a/packages/react-native/Libraries/Utilities/__tests__/useRefEffect-test.js +++ /dev/null @@ -1,249 +0,0 @@ -/** - * 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 - */ - -import type {HostInstance} from '../../../src/private/types/HostInstance'; - -import View from '../../Components/View/View'; -import useRefEffect from '../useRefEffect'; -import * as React from 'react'; -import {act, create} from 'react-test-renderer'; - -/** - * TestView provide a component execution environment to test hooks. - */ -function TestView({ - childKey = null, - effect, -}: { - childKey: ?string, - effect: () => (() => void) | void, -}) { - const ref = useRefEffect(effect); - return ; -} - -/** - * TestEffect represents an effect invocation. - */ -class TestEffect { - name: string; - key: ?string; - constructor(name: string, key: ?string) { - this.name = name; - this.key = key; - } - static called(name: string, key: ?string): $FlowFixMe { - // $FlowFixMe[prop-missing] - Flow does not support type augmentation. - return expect.effect(name, key); - } -} - -/** - * TestEffectCleanup represents an effect cleanup invocation. - */ -class TestEffectCleanup { - name: string; - key: ?string; - constructor(name: string, key: ?string) { - this.name = name; - this.key = key; - } - static called(name: string, key: ?string): $FlowFixMe { - // $FlowFixMe[prop-missing] - Flow does not support type augmentation. - return expect.effectCleanup(name, key); - } -} - -/** - * extend.effect and expect.extendCleanup make it easier to assert expected - * values. But use TestEffect.called and TestEffectCleanup.called instead of - * extend.effect and expect.extendCleanup because of Flow. - */ -expect.extend({ - effect(received, name, key) { - const pass = - received instanceof TestEffect && - received.name === name && - received.key === key; - return {pass}; - }, - effectCleanup(received, name, key) { - const pass = - received instanceof TestEffectCleanup && - received.name === name && - received.key === key; - return {pass}; - }, -}); - -function mockEffectRegistry(): { - mockEffect: string => () => () => void, - mockEffectWithoutCleanup: string => () => void, - registry: ReadonlyArray, -} { - const registry: Array = []; - return { - mockEffect(name: string): () => () => void { - return instance => { - const key = instance?.props?.testID; - registry.push(new TestEffect(name, key)); - return () => { - registry.push(new TestEffectCleanup(name, key)); - }; - }; - }, - mockEffectWithoutCleanup(name: string): () => void { - return instance => { - const key = instance?.props?.testID; - registry.push(new TestEffect(name, key)); - }; - }, - registry, - }; -} - -test('calls effect without cleanup', () => { - let root; - - const {mockEffectWithoutCleanup, registry} = mockEffectRegistry(); - const effectA = mockEffectWithoutCleanup('A'); - - act(() => { - root = create(); - }); - - expect(registry).toEqual([TestEffect.called('A', 'foo')]); - - act(() => { - root.unmount(); - }); - - expect(registry).toEqual([TestEffect.called('A', 'foo')]); -}); - -test('calls effect and cleanup', () => { - let root; - - const {mockEffect, registry} = mockEffectRegistry(); - const effectA = mockEffect('A'); - - act(() => { - root = create(); - }); - - expect(registry).toEqual([TestEffect.called('A', 'foo')]); - - act(() => { - root.unmount(); - }); - - expect(registry).toEqual([ - TestEffect.called('A', 'foo'), - TestEffectCleanup.called('A', 'foo'), - ]); -}); - -test('cleans up old effect before calling new effect', () => { - let root; - - const {mockEffect, registry} = mockEffectRegistry(); - const effectA = mockEffect('A'); - const effectB = mockEffect('B'); - - act(() => { - root = create(); - }); - - act(() => { - root.update(); - }); - - expect(registry).toEqual([ - TestEffect.called('A', 'foo'), - TestEffectCleanup.called('A', 'foo'), - TestEffect.called('B', 'foo'), - ]); - - act(() => { - root.unmount(); - }); - - expect(registry).toEqual([ - TestEffect.called('A', 'foo'), - TestEffectCleanup.called('A', 'foo'), - TestEffect.called('B', 'foo'), - TestEffectCleanup.called('B', 'foo'), - ]); -}); - -test('calls cleanup and effect on new instance', () => { - let root; - - const {mockEffect, registry} = mockEffectRegistry(); - const effectA = mockEffect('A'); - - act(() => { - root = create(); - }); - - act(() => { - root.update(); - }); - - expect(registry).toEqual([ - TestEffect.called('A', 'foo'), - TestEffectCleanup.called('A', 'foo'), - TestEffect.called('A', 'bar'), - ]); - - act(() => { - root.unmount(); - }); - - expect(registry).toEqual([ - TestEffect.called('A', 'foo'), - TestEffectCleanup.called('A', 'foo'), - TestEffect.called('A', 'bar'), - TestEffectCleanup.called('A', 'bar'), - ]); -}); - -test('cleans up old effect before calling new effect with new instance', () => { - let root; - - const {mockEffect, registry} = mockEffectRegistry(); - const effectA = mockEffect('A'); - const effectB = mockEffect('B'); - - act(() => { - root = create(); - }); - - act(() => { - root.update(); - }); - - expect(registry).toEqual([ - TestEffect.called('A', 'foo'), - TestEffectCleanup.called('A', 'foo'), - TestEffect.called('B', 'bar'), - ]); - - act(() => { - root.unmount(); - }); - - expect(registry).toEqual([ - TestEffect.called('A', 'foo'), - TestEffectCleanup.called('A', 'foo'), - TestEffect.called('B', 'bar'), - TestEffectCleanup.called('B', 'bar'), - ]); -}); diff --git a/packages/react-native/Libraries/Utilities/__tests__/warnOnce-test.js b/packages/react-native/Libraries/Utilities/__tests__/warnOnce-itest.js similarity index 51% rename from packages/react-native/Libraries/Utilities/__tests__/warnOnce-test.js rename to packages/react-native/Libraries/Utilities/__tests__/warnOnce-itest.js index 766a1715288b..31256284c522 100644 --- a/packages/react-native/Libraries/Utilities/__tests__/warnOnce-test.js +++ b/packages/react-native/Libraries/Utilities/__tests__/warnOnce-itest.js @@ -8,17 +8,26 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import warnOnce from '../warnOnce'; describe('warnOnce', () => { + const originalConsoleWarn = console.warn; + + afterEach(() => { + // $FlowFixMe[cannot-write] + console.warn = originalConsoleWarn; + }); + it('logs warning messages to the console exactly once', () => { - jest.restoreAllMocks(); - jest.spyOn(console, 'warn').mockReturnValue(undefined); + const mockConsoleWarn = jest.fn(); + // $FlowFixMe[cannot-write] + console.warn = mockConsoleWarn; warnOnce('test-message', 'This is a log message'); warnOnce('test-message', 'This is a second log message'); - expect(console.warn).toHaveBeenCalledWith('This is a log message'); - expect(console.warn).toHaveBeenCalledTimes(1); + expect(mockConsoleWarn).toHaveBeenCalledTimes(1); + expect(mockConsoleWarn).toHaveBeenCalledWith('This is a log message'); }); }); diff --git a/packages/react-native/Libraries/Utilities/differ/__tests__/deepDiffer-test.js b/packages/react-native/Libraries/Utilities/differ/__tests__/deepDiffer-itest.js similarity index 89% rename from packages/react-native/Libraries/Utilities/differ/__tests__/deepDiffer-test.js rename to packages/react-native/Libraries/Utilities/differ/__tests__/deepDiffer-itest.js index 5a4967d99f7e..2625e2504848 100644 --- a/packages/react-native/Libraries/Utilities/differ/__tests__/deepDiffer-test.js +++ b/packages/react-native/Libraries/Utilities/differ/__tests__/deepDiffer-itest.js @@ -8,11 +8,15 @@ * @format */ -'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; const deepDiffer = require('../deepDiffer').default; describe('deepDiffer', function () { + afterEach(() => { + deepDiffer.unstable_setLogListeners(null); + }); + it('should diff primitives of the same type', () => { expect(deepDiffer(1, 2)).toBe(true); expect(deepDiffer(42, 42)).toBe(false); @@ -167,31 +171,25 @@ describe('deepDiffer', function () { function b() {} const listeners = {onDifferentFunctionsIgnored: jest.fn()}; deepDiffer.unstable_setLogListeners(listeners); - try { - deepDiffer(a, a); - expect(listeners.onDifferentFunctionsIgnored).not.toHaveBeenCalled(); - deepDiffer(a, b); - expect(listeners.onDifferentFunctionsIgnored.mock.calls).toEqual([ - ['a', 'b'], - ]); - } finally { - deepDiffer.unstable_setLogListeners(null); - } + deepDiffer(a, a); + expect(listeners.onDifferentFunctionsIgnored).not.toHaveBeenCalled(); + + deepDiffer(a, b); + expect(listeners.onDifferentFunctionsIgnored.mock.calls).toEqual([ + ['a', 'b'], + ]); }); it('should not log when explicitly considering two different functions equal', () => { function a() {} function b() {} const listeners = {onDifferentFunctionsIgnored: jest.fn()}; deepDiffer.unstable_setLogListeners(listeners); - try { - deepDiffer(a, a, {unsafelyIgnoreFunctions: true}); - expect(listeners.onDifferentFunctionsIgnored).not.toHaveBeenCalled(); - deepDiffer(a, b, {unsafelyIgnoreFunctions: true}); - expect(listeners.onDifferentFunctionsIgnored).not.toHaveBeenCalled(); - } finally { - deepDiffer.unstable_setLogListeners(null); - } + deepDiffer(a, a, {unsafelyIgnoreFunctions: true}); + expect(listeners.onDifferentFunctionsIgnored).not.toHaveBeenCalled(); + + deepDiffer(a, b, {unsafelyIgnoreFunctions: true}); + expect(listeners.onDifferentFunctionsIgnored).not.toHaveBeenCalled(); }); }); diff --git a/packages/react-native/Libraries/Utilities/differ/__tests__/matricesDiffer-test.js b/packages/react-native/Libraries/Utilities/differ/__tests__/matricesDiffer-itest.js similarity index 94% rename from packages/react-native/Libraries/Utilities/differ/__tests__/matricesDiffer-test.js rename to packages/react-native/Libraries/Utilities/differ/__tests__/matricesDiffer-itest.js index ae4ae736a757..e1100fb7f630 100644 --- a/packages/react-native/Libraries/Utilities/differ/__tests__/matricesDiffer-test.js +++ b/packages/react-native/Libraries/Utilities/differ/__tests__/matricesDiffer-itest.js @@ -8,7 +8,7 @@ * @format */ -'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; const matricesDiffer = require('../matricesDiffer').default; diff --git a/packages/react-native/Libraries/vendor/emitter/__tests__/EventEmitter-test.js b/packages/react-native/Libraries/vendor/emitter/__tests__/EventEmitter-itest.js similarity index 99% rename from packages/react-native/Libraries/vendor/emitter/__tests__/EventEmitter-test.js rename to packages/react-native/Libraries/vendor/emitter/__tests__/EventEmitter-itest.js index c65c5b74ecd7..7a746f0b47a5 100644 --- a/packages/react-native/Libraries/vendor/emitter/__tests__/EventEmitter-test.js +++ b/packages/react-native/Libraries/vendor/emitter/__tests__/EventEmitter-itest.js @@ -8,6 +8,8 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + import type {EventSubscription} from '../EventEmitter'; import EventEmitter from '../EventEmitter'; From 9506a61364ea66a095d1784158f9189641178434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 18 Jun 2026 02:36:41 -0700 Subject: [PATCH 4/9] Migrate Animated Jest tests to Fantom Summary: Migrates 10 Animated unit tests from regular Jest (`-test.js`) to Fantom (`-itest.js`) so they run on Hermes in the real React Native runtime. Migrated: `Easing`, `bezier`, `Interpolation`, `AnimatedObject`, `AnimatedValue`, `AnimatedMock`, `TimingAnimation`, `createAnimatedPropsHook`, `createAnimatedPropsMemoHook`. The two `Libraries/Animated/nodes/AnimatedProps` cases were folded into the existing `AnimatedProps-itest.js` (importing the same module), with no loss of coverage. Adaptations (no behavioral coverage weakened): - Drove animation timing with Fantom scheduling instead of jest fake timers, and asserted against the real native animated backend rather than mocking it. - Replaced `react-test-renderer` rendering with Fantom `createRoot` + `runTask`, asserting real Fabric output / element refs. Four Animated tests intentionally remain on Jest because they depend on capabilities Fantom does not provide (jest fake timers and/or module mocks of the native animated module): `Animated`, `Animated-web`, `AnimatedNative`, and `NativeAnimatedAllowlist`. Changelog: [Internal] Differential Revision: D108759083 --- .../Animated/__tests__/AnimatedMock-itest.js | 49 ++++++++++++ .../Animated/__tests__/AnimatedMock-test.js | 52 ------------- ...Object-test.js => AnimatedObject-itest.js} | 14 +--- .../Animated/__tests__/AnimatedProps-test.js | 30 -------- ...edValue-test.js => AnimatedValue-itest.js} | 75 ++++++++++++------- .../{Easing-test.js => Easing-itest.js} | 2 +- ...olation-test.js => Interpolation-itest.js} | 28 +++++-- ...ation-test.js => TimingAnimation-itest.js} | 2 +- .../{bezier-test.js => bezier-itest.js} | 2 +- .../animated/__tests__/AnimatedProps-itest.js | 20 +++++ ...st.js => createAnimatedPropsHook-itest.js} | 25 ++++--- ...s => createAnimatedPropsMemoHook-itest.js} | 6 +- 12 files changed, 161 insertions(+), 144 deletions(-) create mode 100644 packages/react-native/Libraries/Animated/__tests__/AnimatedMock-itest.js delete mode 100644 packages/react-native/Libraries/Animated/__tests__/AnimatedMock-test.js rename packages/react-native/Libraries/Animated/__tests__/{AnimatedObject-test.js => AnimatedObject-itest.js} (91%) delete mode 100644 packages/react-native/Libraries/Animated/__tests__/AnimatedProps-test.js rename packages/react-native/Libraries/Animated/__tests__/{AnimatedValue-test.js => AnimatedValue-itest.js} (77%) rename packages/react-native/Libraries/Animated/__tests__/{Easing-test.js => Easing-itest.js} (99%) rename packages/react-native/Libraries/Animated/__tests__/{Interpolation-test.js => Interpolation-itest.js} (95%) rename packages/react-native/Libraries/Animated/__tests__/{TimingAnimation-test.js => TimingAnimation-itest.js} (93%) rename packages/react-native/Libraries/Animated/__tests__/{bezier-test.js => bezier-itest.js} (98%) rename packages/react-native/src/private/animated/__tests__/{createAnimatedPropsHook-test.js => createAnimatedPropsHook-itest.js} (75%) rename packages/react-native/src/private/animated/__tests__/{createAnimatedPropsMemoHook-test.js => createAnimatedPropsMemoHook-itest.js} (98%) diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedMock-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedMock-itest.js new file mode 100644 index 000000000000..65933b0c4390 --- /dev/null +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedMock-itest.js @@ -0,0 +1,49 @@ +/** + * 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 + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import AnimatedImplementation from '../AnimatedImplementation'; +import AnimatedMock from '../AnimatedMock'; + +describe('Animated Mock', () => { + it('matches implementation keys', () => { + expect(Object.keys(AnimatedMock)).toEqual( + Object.keys(AnimatedImplementation), + ); + }); + it('matches implementation params', () => { + Object.keys(AnimatedImplementation).forEach(key => { + if (AnimatedImplementation[key].length !== AnimatedMock[key].length) { + throw new Error( + 'key ' + + key + + ' had different lengths: ' + + JSON.stringify( + { + impl: { + len: AnimatedImplementation[key].length, + type: typeof AnimatedImplementation[key], + val: AnimatedImplementation[key].toString(), + }, + mock: { + len: AnimatedMock[key].length, + type: typeof AnimatedMock[key], + val: AnimatedMock[key].toString(), + }, + }, + null, + 2, + ), + ); + } + }); + }); +}); diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedMock-test.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedMock-test.js deleted file mode 100644 index 066986799f02..000000000000 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedMock-test.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * 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 - */ - -'use strict'; - -import AnimatedImplementation from '../AnimatedImplementation'; -import AnimatedMock from '../AnimatedMock'; - -describe('Animated Mock', () => { - it('matches implementation keys', () => { - expect(Object.keys(AnimatedMock)).toEqual( - Object.keys(AnimatedImplementation), - ); - }); - it('matches implementation params', done => { - Object.keys(AnimatedImplementation).forEach(key => { - if (AnimatedImplementation[key].length !== AnimatedMock[key].length) { - done( - new Error( - 'key ' + - key + - ' had different lengths: ' + - JSON.stringify( - { - impl: { - len: AnimatedImplementation[key].length, - type: typeof AnimatedImplementation[key], - val: AnimatedImplementation[key].toString(), - }, - mock: { - len: AnimatedMock[key].length, - type: typeof AnimatedMock[key], - val: AnimatedMock[key].toString(), - }, - }, - null, - 2, - ), - ), - ); - } - }); - done(); - }); -}); diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-itest.js similarity index 91% rename from packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js rename to packages/react-native/Libraries/Animated/__tests__/AnimatedObject-itest.js index ef4dacd042f5..3eb452700ff1 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedObject-itest.js @@ -8,19 +8,13 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import Animated from '../Animated'; +import AnimatedObject from '../nodes/AnimatedObject'; import nullthrows from 'nullthrows'; describe('AnimatedObject', () => { - let Animated; - let AnimatedObject; - - beforeEach(() => { - jest.resetModules(); - - Animated = require('../Animated').default; - AnimatedObject = require('../nodes/AnimatedObject').default; - }); - it('should get the proper value', () => { const anim = new Animated.Value(0); const translateAnim = anim.interpolate({ diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedProps-test.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedProps-test.js deleted file mode 100644 index 3759ac2bfe62..000000000000 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedProps-test.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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 - */ - -import AnimatedProps from '../nodes/AnimatedProps'; - -describe('AnimatedProps', () => { - function getValue(inputProps: {[string]: unknown}) { - const animatedProps = new AnimatedProps(inputProps, jest.fn()); - return animatedProps.__getValue(); - } - - it('returns original `style` if it has no nodes', () => { - const style = {color: 'red'}; - expect(getValue({style}).style).toBe(style); - }); - - it('returns original `style` for invalid style values', () => { - const values = [undefined, null, function () {}, true, 123, 'foo']; - for (const value of values) { - expect(getValue({style: value})).toEqual({style: value}); - } - }); -}); diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedValue-test.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedValue-itest.js similarity index 77% rename from packages/react-native/Libraries/Animated/__tests__/AnimatedValue-test.js rename to packages/react-native/Libraries/Animated/__tests__/AnimatedValue-itest.js index a77f0123b2ad..1984ef56ee54 100644 --- a/packages/react-native/Libraries/Animated/__tests__/AnimatedValue-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedValue-itest.js @@ -8,9 +8,33 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper'; +import AnimatedValue from '../nodes/AnimatedValue'; + describe('AnimatedValue', () => { - let NativeAnimatedHelper; - let AnimatedValue; + // Fantom uses the real native animated module and does not support + // `jest.spyOn`, so we wrap the relevant `NativeAnimatedHelper.API` methods + // with call-through mocks that count invocations and restore them afterwards. + const restoreAPI: Array<() => void> = []; + + function spyOnAPI(name: string) { + // $FlowFixMe[invalid-computed-prop] + const original = NativeAnimatedHelper.API[name]; + const spy = jest.fn((...args: Array) => + original.apply(NativeAnimatedHelper.API, args), + ); + // $FlowFixMe[prop-missing] + // $FlowFixMe[cannot-write] + NativeAnimatedHelper.API[name] = spy; + restoreAPI.push(() => { + // $FlowFixMe[prop-missing] + // $FlowFixMe[cannot-write] + NativeAnimatedHelper.API[name] = original; + }); + return spy; + } function createNativeAnimatedValue(): AnimatedValue { return new AnimatedValue(0, {useNativeDriver: true}); @@ -32,31 +56,17 @@ describe('AnimatedValue', () => { } beforeEach(() => { - jest.resetModules(); - - jest.mock('../NativeAnimatedTurboModule', () => ({ - __esModule: true, - default: { - addListener: jest.fn(), - createAnimatedNode: jest.fn(), - dropAnimatedNode: jest.fn(), - removeListeners: jest.fn(), - startListeningToAnimatedNodeValue: jest.fn(), - stopListeningToAnimatedNodeValue: jest.fn(), - extractAnimatedNodeOffset: jest.fn(), - // ... - }, - })); - - NativeAnimatedHelper = - require('../../../src/private/animated/NativeAnimatedHelper').default; - AnimatedValue = require('../nodes/AnimatedValue').default; - - jest.spyOn(NativeAnimatedHelper.API, 'createAnimatedNode'); - jest.spyOn(NativeAnimatedHelper.API, 'dropAnimatedNode'); - jest.spyOn(NativeAnimatedHelper.API, 'startListeningToAnimatedNodeValue'); - jest.spyOn(NativeAnimatedHelper.API, 'setWaitingForIdentifier'); - jest.spyOn(NativeAnimatedHelper.API, 'unsetWaitingForIdentifier'); + spyOnAPI('createAnimatedNode'); + spyOnAPI('dropAnimatedNode'); + spyOnAPI('startListeningToAnimatedNodeValue'); + spyOnAPI('setWaitingForIdentifier'); + spyOnAPI('unsetWaitingForIdentifier'); + }); + + afterEach(() => { + while (restoreAPI.length > 0) { + restoreAPI.pop()?.(); + } }); it('emits update events for listeners added', () => { @@ -217,7 +227,13 @@ describe('AnimatedValue', () => { emitMockUpdate(node, 123, 50); - const spy = jest.spyOn(node, '__onAnimatedValueUpdateReceived'); + // $FlowFixMe[method-unbinding] + const original = node.__onAnimatedValueUpdateReceived; + const spy = jest.fn((...args: Array) => + original.apply(node, args), + ); + // $FlowFixMe[cannot-write] + node.__onAnimatedValueUpdateReceived = spy; const mockValue = 100; const mockOffset = 50; @@ -225,7 +241,8 @@ describe('AnimatedValue', () => { emitMockUpdate(node, mockValue, mockOffset); expect(spy).toHaveBeenCalledWith(mockValue, mockOffset); - spy.mockRestore(); + // $FlowFixMe[cannot-write] + node.__onAnimatedValueUpdateReceived = original; }); }); }); diff --git a/packages/react-native/Libraries/Animated/__tests__/Easing-test.js b/packages/react-native/Libraries/Animated/__tests__/Easing-itest.js similarity index 99% rename from packages/react-native/Libraries/Animated/__tests__/Easing-test.js rename to packages/react-native/Libraries/Animated/__tests__/Easing-itest.js index 9581ae1310b4..d71a68d28c65 100644 --- a/packages/react-native/Libraries/Animated/__tests__/Easing-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/Easing-itest.js @@ -8,7 +8,7 @@ * @format */ -'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import Easing from '../Easing'; diff --git a/packages/react-native/Libraries/Animated/__tests__/Interpolation-test.js b/packages/react-native/Libraries/Animated/__tests__/Interpolation-itest.js similarity index 95% rename from packages/react-native/Libraries/Animated/__tests__/Interpolation-test.js rename to packages/react-native/Libraries/Animated/__tests__/Interpolation-itest.js index fc0fb879264d..1853c0050b0f 100644 --- a/packages/react-native/Libraries/Animated/__tests__/Interpolation-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/Interpolation-itest.js @@ -8,6 +8,8 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + import type { InterpolationConfigSupportedOutputType, InterpolationConfigType, @@ -33,6 +35,13 @@ function createInterpolation( } describe('Interpolation', () => { + const originalConsoleWarn = console.warn; + + afterEach(() => { + // $FlowFixMe[cannot-write] + console.warn = originalConsoleWarn; + }); + it('should work with defaults', () => { const interpolation = createInterpolation({ inputRange: [0, 1], @@ -365,7 +374,10 @@ describe('Interpolation', () => { }); it('should work with PlatformColor', () => { - jest.spyOn(console, 'warn').mockImplementationOnce(() => {}); + const mockWarn = jest.fn(); + // $FlowFixMe[cannot-write] + console.warn = mockWarn; + const interpolation = createInterpolation({ inputRange: [0, 1], outputRange: [ @@ -381,7 +393,7 @@ describe('Interpolation', () => { expect(interpolation(2 / 3)).toStrictEqual( PlatformColor('@android:color/white'), ); - expect(console.warn).toBeCalledWith( + expect(mockWarn).toBeCalledWith( 'PlatformColor interpolation should happen natively, here we fallback to the closest color', ); expect(interpolation(1)).toStrictEqual( @@ -389,20 +401,20 @@ describe('Interpolation', () => { ); }); - it.each([ + for (const [label, outputRange, expected] of [ ['radians', ['1rad', '2rad'], [1, 2]], ['degrees', ['90deg', '180deg'], [Math.PI / 2, Math.PI]], ['numbers', [1024, Math.PI], [1024, Math.PI]], ['unknown', ['5foo', '10foo'], ['5foo', '10foo']], - ])( - 'should convert %s to numbers in the native config', - (_, outputRange, expected) => { + ]) { + it(`should convert ${label} to numbers in the native config`, () => { const config = new AnimatedInterpolation( // $FlowFixMe[incompatible-type] {}, + // $FlowFixMe[incompatible-call] {inputRange: [0, 1], outputRange}, ).__getNativeConfig(); expect(config.outputRange).toEqual(expected); - }, - ); + }); + } }); diff --git a/packages/react-native/Libraries/Animated/__tests__/TimingAnimation-test.js b/packages/react-native/Libraries/Animated/__tests__/TimingAnimation-itest.js similarity index 93% rename from packages/react-native/Libraries/Animated/__tests__/TimingAnimation-test.js rename to packages/react-native/Libraries/Animated/__tests__/TimingAnimation-itest.js index 0d2213462fd8..0a15db495f57 100644 --- a/packages/react-native/Libraries/Animated/__tests__/TimingAnimation-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/TimingAnimation-itest.js @@ -8,7 +8,7 @@ * @format */ -'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import TimingAnimation from '../animations/TimingAnimation'; diff --git a/packages/react-native/Libraries/Animated/__tests__/bezier-test.js b/packages/react-native/Libraries/Animated/__tests__/bezier-itest.js similarity index 98% rename from packages/react-native/Libraries/Animated/__tests__/bezier-test.js rename to packages/react-native/Libraries/Animated/__tests__/bezier-itest.js index c7969246478d..38d71ce61f5c 100644 --- a/packages/react-native/Libraries/Animated/__tests__/bezier-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/bezier-itest.js @@ -14,7 +14,7 @@ * @copyright 2014-2015 Gaetan Renaudeau. MIT License. */ -'use strict'; +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import bezier from '../bezier'; diff --git a/packages/react-native/src/private/animated/__tests__/AnimatedProps-itest.js b/packages/react-native/src/private/animated/__tests__/AnimatedProps-itest.js index 2c34fda5e838..b6e4534ec51d 100644 --- a/packages/react-native/src/private/animated/__tests__/AnimatedProps-itest.js +++ b/packages/react-native/src/private/animated/__tests__/AnimatedProps-itest.js @@ -13,6 +13,7 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import * as Fantom from '@react-native/fantom'; import * as React from 'react'; import {Animated} from 'react-native'; +import AnimatedProps from 'react-native/Libraries/Animated/nodes/AnimatedProps'; import NativeAnimatedHelper from 'react-native/src/private/animated/NativeAnimatedHelper'; function mockNativeAnimatedHelperAPI() { @@ -58,3 +59,22 @@ test('connects and disconnects views', () => { // TODO: investigate why previous task enqueues more tasks. Fantom.runWorkLoop(); }); + +describe('AnimatedProps#__getValue', () => { + function getValue(inputProps: {[string]: unknown}) { + const animatedProps = new AnimatedProps(inputProps, jest.fn()); + return animatedProps.__getValue(); + } + + test('returns original `style` if it has no nodes', () => { + const style = {color: 'red'}; + expect(getValue({style}).style).toBe(style); + }); + + test('returns original `style` for invalid style values', () => { + const values = [undefined, null, function () {}, true, 123, 'foo']; + for (const value of values) { + expect(getValue({style: value})).toEqual({style: value}); + } + }); +}); diff --git a/packages/react-native/src/private/animated/__tests__/createAnimatedPropsHook-test.js b/packages/react-native/src/private/animated/__tests__/createAnimatedPropsHook-itest.js similarity index 75% rename from packages/react-native/src/private/animated/__tests__/createAnimatedPropsHook-test.js rename to packages/react-native/src/private/animated/__tests__/createAnimatedPropsHook-itest.js index 1d15c9e038d2..2014e0325b75 100644 --- a/packages/react-native/src/private/animated/__tests__/createAnimatedPropsHook-test.js +++ b/packages/react-native/src/private/animated/__tests__/createAnimatedPropsHook-itest.js @@ -8,13 +8,15 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + import {AnimatedEvent} from '../../../../Libraries/Animated/AnimatedEvent'; import createAnimatedPropsHook from '../createAnimatedPropsHook'; -import {create, update} from '@react-native/jest-preset/jest/renderer'; +import * as Fantom from '@react-native/fantom'; import {useLayoutEffect} from 'react'; describe('useAnimatedProps', () => { - it('returns the same ref callback when `props` changes', async () => { + it('returns the same ref callback when `props` changes', () => { const useAnimatedProps = createAnimatedPropsHook(null); const refs = []; @@ -26,15 +28,16 @@ describe('useAnimatedProps', () => { return null; } - const root = await create(); + const root = Fantom.createRoot(); + Fantom.runTask(() => root.render()); expect(refs.length).toBe(1); expect(refs[0]).toBeInstanceOf(Function); - await update(root, ); + Fantom.runTask(() => root.render()); expect(refs.length).toBe(1); }); - it('returns the same ref callback when `AnimatedEvent` is the same', async () => { + it('returns the same ref callback when `AnimatedEvent` is the same', () => { const useAnimatedProps = createAnimatedPropsHook(null); const refs = []; @@ -48,15 +51,16 @@ describe('useAnimatedProps', () => { const event = new AnimatedEvent([{}], {useNativeDriver: true}); - const root = await create(); + const root = Fantom.createRoot(); + Fantom.runTask(() => root.render()); expect(refs.length).toBe(1); expect(refs[0]).toBeInstanceOf(Function); - await update(root, ); + Fantom.runTask(() => root.render()); expect(refs.length).toBe(1); }); - it('returns a new ref callback when `AnimatedEvent` changes', async () => { + it('returns a new ref callback when `AnimatedEvent` changes', () => { const useAnimatedProps = createAnimatedPropsHook(null); const refs = []; @@ -71,11 +75,12 @@ describe('useAnimatedProps', () => { const eventA = new AnimatedEvent([{}], {useNativeDriver: true}); const eventB = new AnimatedEvent([{}], {useNativeDriver: true}); - const root = await create(); + const root = Fantom.createRoot(); + Fantom.runTask(() => root.render()); expect(refs.length).toBe(1); expect(refs[0]).toBeInstanceOf(Function); - await update(root, ); + Fantom.runTask(() => root.render()); expect(refs.length).toBe(2); expect(refs[1]).toBeInstanceOf(Function); diff --git a/packages/react-native/src/private/animated/__tests__/createAnimatedPropsMemoHook-test.js b/packages/react-native/src/private/animated/__tests__/createAnimatedPropsMemoHook-itest.js similarity index 98% rename from packages/react-native/src/private/animated/__tests__/createAnimatedPropsMemoHook-test.js rename to packages/react-native/src/private/animated/__tests__/createAnimatedPropsMemoHook-itest.js index d3aa9cb0f93e..faa8841095f9 100644 --- a/packages/react-native/src/private/animated/__tests__/createAnimatedPropsMemoHook-test.js +++ b/packages/react-native/src/private/animated/__tests__/createAnimatedPropsMemoHook-itest.js @@ -8,6 +8,8 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + import {AnimatedEvent} from '../../../../Libraries/Animated/AnimatedEvent'; import AnimatedValue from '../../../../Libraries/Animated/nodes/AnimatedValue'; import { @@ -31,7 +33,7 @@ describe('createCompositeKeyForProps', () => { }); it('does not search non-allowlisted props', () => { - const getter = jest.fn().mockReturnValue({}); + const getter = jest.fn(() => ({})); const props = { object: { // $FlowExpectedError[unsafe-getters-setters] @@ -235,7 +237,7 @@ describe('createCompositeKeyForProps', () => { describe('areCompositeKeysEqual', () => { it('compares identical keys without traversal', () => { - const getter = jest.fn().mockReturnValue({}); + const getter = jest.fn(() => ({})); const compositeKey = { object: { // $FlowExpectedError[unsafe-getters-setters] From 1b392febd7dbc5776cd944588380233e9d3f860e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 18 Jun 2026 02:36:41 -0700 Subject: [PATCH 5/9] Migrate Components, FlatList, and Modal Jest tests to Fantom Summary: Migrates component unit tests from regular Jest (`-test.js`) to Fantom (`-itest.js`) so they render through the real React Native runtime (Fabric/Yoga) instead of a mocked renderer. Folded missing cases into existing Fantom tests (`View`, `ScrollView`, `Pressable`, `ActivityIndicator`, `Button`, `TouchableOpacity`, `TouchableWithoutFeedback`, `Modal`, `FlatList`) and added new Fantom tests for `TouchableNativeFeedback` and `InputAccessoryView`. Obsolete file snapshots that captured the old mocked output were removed; assertions now compare against the real Fabric output. A few component tests remain on Jest for now because they depend on capabilities Fantom does not provide (module mocks of native modules / mock-based event simulation): `AccessibilityInfo`, `DrawerAndroid`, `Keyboard`, `StatusBar`, and `Pressability`. Changelog: [Internal] Differential Revision: D108759082 --- .../__tests__/ActivityIndicator-itest.js | 4 + .../__tests__/ActivityIndicator-test.js | 32 - .../ActivityIndicator-test.js.snap | 32 - .../Pressable/__tests__/Pressable-itest.js | 128 +++- .../Pressable/__tests__/Pressable-test.js | 161 ----- .../__snapshots__/Pressable-test.js.snap | 647 ------------------ .../ScrollView/__tests__/ScrollView-itest.js | 125 +++- .../ScrollView/__tests__/ScrollView-test.js | 161 ----- .../__snapshots__/ScrollView-test.js.snap | 66 -- .../__tests__/InputAccessoryView-itest.js | 33 + .../__tests__/InputAccessoryView-test.js | 38 - .../InputAccessoryView-test.js.snap | 59 -- .../TouchableNativeFeedback-itest.js | 160 +++++ .../__tests__/TouchableNativeFeedback-test.js | 114 --- .../__tests__/TouchableOpacity-itest.js | 20 + .../__tests__/TouchableOpacity-test.js | 52 -- .../TouchableWithoutFeedback-itest.js | 78 +++ .../TouchableWithoutFeedback-test.js | 85 --- .../TouchableNativeFeedback-test.js.snap | 220 ------ .../TouchableOpacity-test.js.snap | 124 ---- .../TouchableWithoutFeedback-test.js.snap | 141 ---- .../Components/View/__tests__/View-itest.js | 18 + .../Components/View/__tests__/View-test.js | 194 ------ .../Components/__tests__/Button-itest.js | 43 ++ .../Components/__tests__/Button-test.js | 100 --- .../__snapshots__/Button-test.js.snap | 574 ---------------- .../Lists/__tests__/FlatList-itest.js | 179 +++++ .../Lists/__tests__/FlatList-test.js | 222 ------ .../__snapshots__/FlatList-test.js.snap | 600 ---------------- .../Libraries/Modal/__tests__/Modal-test.js | 62 -- .../__snapshots__/Modal-test.js.snap | 61 -- 31 files changed, 786 insertions(+), 3747 deletions(-) delete mode 100644 packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-test.js delete mode 100644 packages/react-native/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap delete mode 100644 packages/react-native/Libraries/Components/Pressable/__tests__/Pressable-test.js delete mode 100644 packages/react-native/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap delete mode 100644 packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-test.js delete mode 100644 packages/react-native/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap create mode 100644 packages/react-native/Libraries/Components/TextInput/__tests__/InputAccessoryView-itest.js delete mode 100644 packages/react-native/Libraries/Components/TextInput/__tests__/InputAccessoryView-test.js delete mode 100644 packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/InputAccessoryView-test.js.snap create mode 100644 packages/react-native/Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-itest.js delete mode 100644 packages/react-native/Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-test.js delete mode 100644 packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-test.js delete mode 100644 packages/react-native/Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-test.js delete mode 100644 packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap delete mode 100644 packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap delete mode 100644 packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableWithoutFeedback-test.js.snap delete mode 100644 packages/react-native/Libraries/Components/View/__tests__/View-test.js delete mode 100644 packages/react-native/Libraries/Components/__tests__/Button-test.js delete mode 100644 packages/react-native/Libraries/Components/__tests__/__snapshots__/Button-test.js.snap delete mode 100644 packages/react-native/Libraries/Lists/__tests__/FlatList-test.js delete mode 100644 packages/react-native/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap delete mode 100644 packages/react-native/Libraries/Modal/__tests__/Modal-test.js delete mode 100644 packages/react-native/Libraries/Modal/__tests__/__snapshots__/Modal-test.js.snap diff --git a/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-itest.js b/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-itest.js index 97c57e1a19f7..a9137dcc33c4 100644 --- a/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-itest.js +++ b/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-itest.js @@ -18,6 +18,10 @@ import {ActivityIndicator} from 'react-native'; import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; describe('', () => { + it('sets displayName', () => { + expect(ActivityIndicator.displayName).toBe('ActivityIndicator'); + }); + describe('props', () => { describe('size', () => { it('defaults to "small" (20x20)', () => { diff --git a/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-test.js b/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-test.js deleted file mode 100644 index 523f6150310b..000000000000 --- a/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/ActivityIndicator-test.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * 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 - */ - -'use strict'; - -import * as React from 'react'; - -const ReactNativeTestTools = require('../../../Utilities/ReactNativeTestTools'); -const ActivityIndicator = require('../ActivityIndicator').default; - -describe('', () => { - it('should set displayName to prevent regressions', () => { - expect(ActivityIndicator.displayName).toBe('ActivityIndicator'); - }); - - it('should render as expected', async () => { - await ReactNativeTestTools.expectRendersMatchingSnapshot( - 'ActivityIndicator', - () => , - () => { - jest.dontMock('../ActivityIndicator'); - }, - ); - }); -}); diff --git a/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap b/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap deleted file mode 100644 index f0fa9f3980af..000000000000 --- a/packages/react-native/Libraries/Components/ActivityIndicator/__tests__/__snapshots__/ActivityIndicator-test.js.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render as expected: should deep render when mocked (please verify output manually) 1`] = ` - -`; - -exports[` should render as expected: should deep render when not mocked (please verify output manually) 1`] = ` - - - -`; diff --git a/packages/react-native/Libraries/Components/Pressable/__tests__/Pressable-itest.js b/packages/react-native/Libraries/Components/Pressable/__tests__/Pressable-itest.js index 8e4ef129d614..a053b8ee05b4 100644 --- a/packages/react-native/Libraries/Components/Pressable/__tests__/Pressable-itest.js +++ b/packages/react-native/Libraries/Components/Pressable/__tests__/Pressable-itest.js @@ -17,7 +17,7 @@ import * as Fantom from '@react-native/fantom'; import * as React from 'react'; import {createRef} from 'react'; import {Pressable} from 'react-native'; -import {Text} from 'react-native'; +import {PlatformColor, Text} from 'react-native'; import accessibilityPropsSuite from 'react-native/src/private/__tests__/utilities/accessibilityPropsSuite'; import ensureInstance from 'react-native/src/private/__tests__/utilities/ensureInstance'; import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; @@ -123,6 +123,132 @@ describe('', () => { 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( + , + ); + }); + + it('sets accessibilityState disabled to true when accessibilityState is empty', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + + it('preserves other accessibilityState fields when disabled is true', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + + it('overwrites accessibilityState.disabled with the disabled prop', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + }); + + describe('android_ripple', () => { + it('renders with a numeric color and alpha', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect(root.getRenderedOutput().toJSX()).toEqual( + , + ); + }); + + it('renders with a PlatformColor and alpha', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect(root.getRenderedOutput().toJSX()).toEqual( + , + ); + }); + + it('does not crash with an unresolvable PlatformColor', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect(root.getRenderedOutput().toJSX()).toEqual( + , + ); + }); }); describe('children', () => { diff --git a/packages/react-native/Libraries/Components/Pressable/__tests__/Pressable-test.js b/packages/react-native/Libraries/Components/Pressable/__tests__/Pressable-test.js deleted file mode 100644 index 8eb24e747bdd..000000000000 --- a/packages/react-native/Libraries/Components/Pressable/__tests__/Pressable-test.js +++ /dev/null @@ -1,161 +0,0 @@ -/** - * 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 - */ - -import {PlatformColor} from '../../../StyleSheet/PlatformColorValueTypes'; -import Platform from '../../../Utilities/Platform'; -import {expectRendersMatchingSnapshot} from '../../../Utilities/ReactNativeTestTools'; -import View from '../../View/View'; -import Pressable from '../Pressable'; -import * as React from 'react'; - -describe('', () => { - it('should render as expected', async () => { - await expectRendersMatchingSnapshot( - 'Pressable', - () => ( - - - - ), - () => { - jest.dontMock('../Pressable'); - }, - ); - }); -}); - -describe('', () => { - it('should be disabled when disabled is true', async () => { - await expectRendersMatchingSnapshot( - 'Pressable', - () => ( - - - - ), - () => { - jest.dontMock('../Pressable'); - }, - ); - }); -}); - -describe('', () => { - it('should be disabled when disabled is true and accessibilityState is empty', async () => { - await expectRendersMatchingSnapshot( - 'Pressable', - () => ( - - - - ), - () => { - jest.dontMock('../Pressable'); - }, - ); - }); -}); - -describe('', () => { - it('should keep accessibilityState when disabled is true', async () => { - await expectRendersMatchingSnapshot( - 'Pressable', - () => ( - - - - ), - () => { - jest.dontMock('../Pressable'); - }, - ); - }); -}); - -describe('', () => { - it('should overwrite accessibilityState with value of disabled prop', async () => { - await expectRendersMatchingSnapshot( - 'Pressable', - () => ( - - - - ), - () => { - jest.dontMock('../Pressable'); - }, - ); - }); -}); - -describe(' on Android', () => { - let originalOS: string; - - beforeEach(() => { - originalOS = Platform.OS; - /* $FlowFixMe[incompatible-type] */ - Platform.OS = 'android'; - }); - - afterEach(() => { - /* $FlowFixMe[incompatible-type] */ - Platform.OS = originalOS; - }); - - it('should set nativeBackgroundAndroid with numeric color and alpha', async () => { - await expectRendersMatchingSnapshot( - 'Pressable', - () => ( - - - - ), - () => { - jest.dontMock('../Pressable'); - }, - ); - }); - - it('should set nativeBackgroundAndroid with PlatformColor and alpha', async () => { - await expectRendersMatchingSnapshot( - 'Pressable', - () => ( - - - - ), - () => { - jest.dontMock('../Pressable'); - }, - ); - }); - - it('should not crash with an unresolvable PlatformColor', async () => { - await expectRendersMatchingSnapshot( - 'Pressable', - () => ( - - - - ), - () => { - jest.dontMock('../Pressable'); - }, - ); - }); -}); diff --git a/packages/react-native/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap b/packages/react-native/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap deleted file mode 100644 index 6f406d8766c8..000000000000 --- a/packages/react-native/Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap +++ /dev/null @@ -1,647 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render as expected: should deep render when mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` should render as expected: should deep render when not mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` on Android should not crash with an unresolvable PlatformColor: should deep render when mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` on Android should not crash with an unresolvable PlatformColor: should deep render when not mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` on Android should set nativeBackgroundAndroid with PlatformColor and alpha: should deep render when mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` on Android should set nativeBackgroundAndroid with PlatformColor and alpha: should deep render when not mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` on Android should set nativeBackgroundAndroid with numeric color and alpha: should deep render when mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` on Android should set nativeBackgroundAndroid with numeric color and alpha: should deep render when not mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` should be disabled when disabled is true: should deep render when mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` should be disabled when disabled is true: should deep render when not mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` should be disabled when disabled is true and accessibilityState is empty: should deep render when mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` should be disabled when disabled is true and accessibilityState is empty: should deep render when not mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` should keep accessibilityState when disabled is true: should deep render when mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` should keep accessibilityState when disabled is true: should deep render when not mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` should overwrite accessibilityState with value of disabled prop: should deep render when mocked (please verify output manually) 1`] = ` - - - -`; - -exports[` should overwrite accessibilityState with value of disabled prop: should deep render when not mocked (please verify output manually) 1`] = ` - - - -`; diff --git a/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-itest.js b/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-itest.js index 67d0fc82424b..cb11743a4464 100644 --- a/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-itest.js +++ b/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-itest.js @@ -13,9 +13,132 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import type {HostInstance} from 'react-native'; import * as Fantom from '@react-native/fantom'; +import nullthrows from 'nullthrows'; import * as React from 'react'; import {createRef} from 'react'; -import {ScrollView} from 'react-native'; +import {ScrollView, Text, View} 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('rendering', () => { + it('renders its children', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + Hello World! + + , + ); + }); + + expect(root.getRenderedOutput({props: []}).toJSX()).toEqual( + + + Hello World! + + , + ); + }); + }); + + describe('ref', () => { + it('receives an instance or null', () => { + const root = Fantom.createRoot(); + const ref = jest.fn(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(ref).toHaveBeenCalledTimes(1); + expect(ref.mock.calls[0][0]).not.toBe(null); + + Fantom.runTask(() => { + root.render(<>); + }); + + expect(ref.mock.lastCall[0]).toBe(null); + }); + + it('transitions between refs', () => { + const root = Fantom.createRoot(); + const refA = jest.fn(); + const refB = jest.fn(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(refA.mock.lastCall[0]).not.toBe(null); + + Fantom.runTask(() => { + root.render(); + }); + + expect(refA.mock.lastCall[0]).toBe(null); + expect(refB.mock.lastCall[0]).not.toBe(null); + }); + }); + + describe('innerViewRef', () => { + it('receives an instance or null', () => { + const root = Fantom.createRoot(); + const ref = jest.fn(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(ref.mock.lastCall[0]).toBeInstanceOf(ReactNativeElement); + + Fantom.runTask(() => { + root.render(<>); + }); + + expect(ref.mock.lastCall[0]).toBe(null); + }); + + it('transitions between refs', () => { + const root = Fantom.createRoot(); + const refA = jest.fn(); + const refB = jest.fn(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(refA.mock.lastCall[0]).toBeInstanceOf(ReactNativeElement); + + Fantom.runTask(() => { + root.render(); + }); + + expect(refA.mock.lastCall[0]).toBe(null); + expect(refB.mock.lastCall[0]).toBeInstanceOf(ReactNativeElement); + }); + }); + + describe('getInnerViewRef', () => { + it('returns a host instance', () => { + const ref = createRef>(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const innerView = ensureInstance( + nullthrows(ref.current).getInnerViewRef(), + ReactNativeElement, + ); + expect(innerView.tagName).toBe('RN:View'); + }); + }); +}); describe('onScroll', () => { it('delivers onScroll event', () => { diff --git a/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-test.js b/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-test.js deleted file mode 100644 index 7c544a0aa300..000000000000 --- a/packages/react-native/Libraries/Components/ScrollView/__tests__/ScrollView-test.js +++ /dev/null @@ -1,161 +0,0 @@ -/** - * 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 - */ - -'use strict'; - -const Text = require('../../../Text/Text').default; -const ReactNativeTestTools = require('../../../Utilities/ReactNativeTestTools'); -const View = require('../../View/View').default; -const ScrollView = require('../ScrollView').default; -const { - create, - unmount, - update, -} = require('@react-native/jest-preset/jest/renderer'); -const React = require('react'); -const {createRef} = require('react'); - -describe('ScrollView', () => { - beforeEach(() => { - jest.resetModules(); - }); - - it('renders its children', async () => { - await ReactNativeTestTools.expectRendersMatchingSnapshot( - 'ScrollView', - () => ( - - - Hello World! - - - ), - () => { - jest.dontMock('../ScrollView'); - }, - ); - }); - - it('mocks native methods and instance methods', async () => { - jest.mock('../ScrollView'); - - const ref = createRef>(); - await create(); - - // $FlowFixMe[method-unbinding] - expect(ref.current?.measure).toBeInstanceOf(jest.fn().constructor); - expect(ref.current?.scrollTo).toBeInstanceOf(jest.fn().constructor); - }); - - describe('ref', () => { - it('receives an instance or null', async () => { - jest.dontMock('../ScrollView'); - - const scrollViewRef = jest.fn(); - const testRendererInstance = await create( - , - ); - - expect(scrollViewRef).toHaveBeenLastCalledWith( - expect.objectContaining({_nativeTag: expect.any(Number)}), - ); - - await unmount(testRendererInstance); - - expect(scrollViewRef).toHaveBeenLastCalledWith(null); - }); - - it('transitions between refs', async () => { - jest.dontMock('../ScrollView'); - - const scrollViewRefA = jest.fn(); - const testRendererInstance = await create( - , - ); - - expect(scrollViewRefA).toHaveBeenLastCalledWith( - expect.objectContaining({_nativeTag: expect.any(Number)}), - ); - - const scrollViewRefB = jest.fn(); - await update(testRendererInstance, ); - - expect(scrollViewRefA).toHaveBeenLastCalledWith(null); - expect(scrollViewRefB).toHaveBeenLastCalledWith( - expect.objectContaining({_nativeTag: expect.any(Number)}), - ); - }); - }); - - describe('innerViewRef', () => { - it('receives an instance or null', async () => { - jest.dontMock('../ScrollView'); - - const innerViewRef = jest.fn(); - const testRendererInstance = await create( - , - ); - - expect(innerViewRef).toHaveBeenLastCalledWith( - expect.objectContaining({_nativeTag: expect.any(Number)}), - ); - - await unmount(testRendererInstance); - - expect(innerViewRef).toHaveBeenLastCalledWith(null); - }); - - it('transitions between refs', async () => { - jest.dontMock('../ScrollView'); - - const innerViewRefA = jest.fn(); - const testRendererInstance = await create( - , - ); - - expect(innerViewRefA).toHaveBeenLastCalledWith( - expect.objectContaining({_nativeTag: expect.any(Number)}), - ); - - const innerViewRefB = jest.fn(); - - await update( - testRendererInstance, - , - ); - - expect(innerViewRefA).toHaveBeenLastCalledWith(null); - expect(innerViewRefB).toHaveBeenLastCalledWith( - expect.objectContaining({_nativeTag: expect.any(Number)}), - ); - }); - }); - - describe('getInnerViewRef', () => { - it('returns an instance', async () => { - jest.dontMock('../ScrollView'); - - const ref = createRef>(); - await create(); - const innerViewRef = ref.current?.getInnerViewRef(); - - // This is checking if the ref acts like a host component. If we had an - // `isHostComponent(ref)` method, that would be preferred. - // $FlowFixMe[method-unbinding] - expect(innerViewRef?.measure).toBeInstanceOf(jest.fn().constructor); - // $FlowFixMe[method-unbinding] - expect(innerViewRef?.measureLayout).toBeInstanceOf(jest.fn().constructor); - // $FlowFixMe[method-unbinding] - expect(innerViewRef?.measureInWindow).toBeInstanceOf( - jest.fn().constructor, - ); - }); - }); -}); diff --git a/packages/react-native/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap b/packages/react-native/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap deleted file mode 100644 index 44636eb9dabe..000000000000 --- a/packages/react-native/Libraries/Components/ScrollView/__tests__/__snapshots__/ScrollView-test.js.snap +++ /dev/null @@ -1,66 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ScrollView renders its children: should deep render when mocked (please verify output manually) 1`] = ` - - - - - Hello World! - - - - -`; - -exports[`ScrollView renders its children: should deep render when not mocked (please verify output manually) 1`] = ` - - - - - Hello World! - - - - -`; diff --git a/packages/react-native/Libraries/Components/TextInput/__tests__/InputAccessoryView-itest.js b/packages/react-native/Libraries/Components/TextInput/__tests__/InputAccessoryView-itest.js new file mode 100644 index 000000000000..055c30d41ca2 --- /dev/null +++ b/packages/react-native/Libraries/Components/TextInput/__tests__/InputAccessoryView-itest.js @@ -0,0 +1,33 @@ +/** + * 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 + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {InputAccessoryView, View} from 'react-native'; + +describe('', () => { + // `InputAccessoryView` is an iOS-only component. Fantom runs as Android, where + // it renders nothing. + it('renders nothing on Android', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect(root.getRenderedOutput().toJSX()).toBeNull(); + }); +}); diff --git a/packages/react-native/Libraries/Components/TextInput/__tests__/InputAccessoryView-test.js b/packages/react-native/Libraries/Components/TextInput/__tests__/InputAccessoryView-test.js deleted file mode 100644 index b8cd0d9916d0..000000000000 --- a/packages/react-native/Libraries/Components/TextInput/__tests__/InputAccessoryView-test.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * 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 - */ - -'use strict'; - -const View = require('../../View/View').default; -const InputAccessoryView = require('../InputAccessoryView').default; -const render = require('@react-native/jest-preset/jest/renderer'); -const React = require('react'); - -describe('InputAccessoryView', () => { - it('should render as when mocked', async () => { - const instance = await render.create( - - - , - ); - expect(instance).toMatchSnapshot(); - }); - - it('should render as when not mocked', async () => { - jest.dontMock('../InputAccessoryView'); - - const instance = await render.create( - - - , - ); - expect(instance).toMatchSnapshot(); - }); -}); diff --git a/packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/InputAccessoryView-test.js.snap b/packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/InputAccessoryView-test.js.snap deleted file mode 100644 index b9201d101164..000000000000 --- a/packages/react-native/Libraries/Components/TextInput/__tests__/__snapshots__/InputAccessoryView-test.js.snap +++ /dev/null @@ -1,59 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`InputAccessoryView should render as when mocked 1`] = ` - - - - - -`; - -exports[`InputAccessoryView should render as when not mocked 1`] = ` - - - - - -`; diff --git a/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-itest.js b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-itest.js new file mode 100644 index 000000000000..7893b0855f10 --- /dev/null +++ b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-itest.js @@ -0,0 +1,160 @@ +/** + * 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 + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {Text, TouchableNativeFeedback, View} from 'react-native'; + +describe('', () => { + it('sets displayName', () => { + expect(TouchableNativeFeedback.displayName).toBe('TouchableNativeFeedback'); + }); + + describe('rendering', () => { + it('renders its child as pressable', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + Touchable + , + ); + }); + + expect( + root.getRenderedOutput({props: ['isPressable', 'accessible']}).toJSX(), + ).toEqual( + + Touchable + , + ); + }); + + it('renders a View child', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect(root.getRenderedOutput().toJSX()).toEqual( + , + ); + }); + }); + + describe('disabled', () => { + it('sets accessibilityState disabled to true', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + + it('sets accessibilityState disabled to true when accessibilityState is empty', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + + it('preserves other accessibilityState fields when disabled is true', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + + it('overwrites accessibilityState.disabled with the disabled prop (true)', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + + it('overwrites accessibilityState.disabled with the disabled prop (false)', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + }); +}); diff --git a/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-test.js b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-test.js deleted file mode 100644 index 1b8a29e73c4b..000000000000 --- a/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableNativeFeedback-test.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * 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 - */ - -'use strict'; - -import Text from '../../../Text/Text'; -import View from '../../View/View'; -import TouchableNativeFeedback from '../TouchableNativeFeedback'; -import * as React from 'react'; - -const render = require('@react-native/jest-preset/jest/renderer'); - -describe('TouchableWithoutFeedback', () => { - it('renders correctly', async () => { - const instance = await render.create( - - Touchable - , - ); - - expect(instance.toJSON()).toMatchSnapshot(); - }); - - it('has displayName', () => { - expect(TouchableNativeFeedback.displayName).toEqual( - 'TouchableNativeFeedback', - ); - }); -}); - -describe('', () => { - it('should render as expected', async () => { - const instance = await render.create( - - - , - ); - - expect(instance.toJSON()).toMatchSnapshot(); - }); -}); - -describe('', () => { - it('should be disabled when disabled is true', async () => { - expect( - await render.create( - - - , - ), - ).toMatchSnapshot(); - }); -}); - -describe('', () => { - it('should be disabled when disabled is true and accessibilityState is empty', async () => { - expect( - await render.create( - - - , - ), - ).toMatchSnapshot(); - }); -}); - -describe('', () => { - it('should keep accessibilityState when disabled is true', async () => { - expect( - await render.create( - - - , - ), - ).toMatchSnapshot(); - }); -}); - -describe('', () => { - it('should overwrite accessibilityState with value of disabled prop', async () => { - expect( - await render.create( - - - , - ), - ).toMatchSnapshot(); - }); -}); - -describe('', () => { - it('should overwrite accessibilityState with value of disabled prop', async () => { - expect( - await render.create( - - - , - ), - ).toMatchSnapshot(); - }); -}); diff --git a/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-itest.js b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-itest.js index ce143037cfd6..f64e8e02689e 100644 --- a/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-itest.js +++ b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-itest.js @@ -21,6 +21,10 @@ import ensureInstance from 'react-native/src/private/__tests__/utilities/ensureI import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; describe('', () => { + it('sets displayName', () => { + expect(TouchableOpacity.displayName).toBe('TouchableOpacity'); + }); + describe('props', () => { describe('rendering', () => { it('renders as a view with accessible="true"', () => { @@ -155,6 +159,22 @@ describe('', () => { , ); }); + + it('sets accessibilityState disabled to true via accessibilityState prop', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); }); describe('children', () => { diff --git a/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-test.js b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-test.js deleted file mode 100644 index a4d71d16d706..000000000000 --- a/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableOpacity-test.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * 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 - */ - -'use strict'; - -const Text = require('../../../Text/Text').default; -const TouchableOpacity = require('../TouchableOpacity').default; -const {create} = require('@react-native/jest-preset/jest/renderer'); -const React = require('react'); - -describe('TouchableOpacity', () => { - it('renders correctly', async () => { - const instance = await create( - - Touchable - , - ); - - expect(instance.toJSON()).toMatchSnapshot(); - }); - - it('renders in disabled state when a disabled prop is passed', async () => { - const instance = await create( - - Touchable - , - ); - - expect(instance.toJSON()).toMatchSnapshot(); - }); - - it('renders in disabled state when a key disabled in accessibilityState is passed', async () => { - const instance = await create( - - Touchable - , - ); - - expect(instance.toJSON()).toMatchSnapshot(); - }); - - it('has displayName', () => { - expect(TouchableOpacity.displayName).toEqual('TouchableOpacity'); - }); -}); diff --git a/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-itest.js b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-itest.js index 551dc7c7c644..aae49186d504 100644 --- a/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-itest.js +++ b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-itest.js @@ -48,6 +48,84 @@ describe('', () => { } accessibilityPropsSuite(ComponentWithAccessibilityProps); + + describe('disabled', () => { + it('sets accessibilityState disabled to true', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + + it('sets accessibilityState disabled to true when accessibilityState is empty', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + + it('preserves other accessibilityState fields when disabled is true', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + + it('overwrites accessibilityState.disabled with the disabled prop', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect( + root.getRenderedOutput({props: ['accessibilityState']}).toJSX(), + ).toEqual( + , + ); + }); + }); }); describe('ref', () => { diff --git a/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-test.js b/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-test.js deleted file mode 100644 index 10ca0ce3f969..000000000000 --- a/packages/react-native/Libraries/Components/Touchable/__tests__/TouchableWithoutFeedback-test.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * 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 - */ - -'use strict'; - -import Text from '../../../Text/Text'; -import View from '../../View/View'; -import TouchableWithoutFeedback from '../TouchableWithoutFeedback'; -import {create} from '@react-native/jest-preset/jest/renderer'; -import * as React from 'react'; - -describe('TouchableWithoutFeedback', () => { - it('renders correctly', async () => { - const instance = await create( - - Touchable - , - ); - - expect(instance.toJSON()).toMatchSnapshot(); - }); -}); - -describe('TouchableWithoutFeedback with disabled state', () => { - it('should be disabled when disabled is true', async () => { - expect( - await create( - - - , - ), - ).toMatchSnapshot(); - }); - - it('should be disabled when disabled is true and accessibilityState is empty', async () => { - expect( - await create( - - - , - ), - ).toMatchSnapshot(); - }); - - it('should keep accessibilityState when disabled is true', async () => { - expect( - await create( - - - , - ), - ).toMatchSnapshot(); - }); - - it('should overwrite accessibilityState with value of disabled prop', async () => { - expect( - await create( - - - , - ), - ).toMatchSnapshot(); - }); - - it('should disable button when accessibilityState is disabled', async () => { - expect( - await create( - - - , - ), - ).toMatchSnapshot(); - }); -}); diff --git a/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap b/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap deleted file mode 100644 index 6802f754650b..000000000000 --- a/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableNativeFeedback-test.js.snap +++ /dev/null @@ -1,220 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render as expected 1`] = ` - -`; - -exports[` should overwrite accessibilityState with value of disabled prop 1`] = ` - -`; - -exports[` should be disabled when disabled is true and accessibilityState is empty 1`] = ` - -`; - -exports[` should keep accessibilityState when disabled is true 1`] = ` - -`; - -exports[` should overwrite accessibilityState with value of disabled prop 1`] = ` - -`; - -exports[` should be disabled when disabled is true 1`] = ` - -`; - -exports[`TouchableWithoutFeedback renders correctly 1`] = ` - - Touchable - -`; diff --git a/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap b/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap deleted file mode 100644 index 17f2e7f6f764..000000000000 --- a/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableOpacity-test.js.snap +++ /dev/null @@ -1,124 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TouchableOpacity renders correctly 1`] = ` - - - Touchable - - -`; - -exports[`TouchableOpacity renders in disabled state when a disabled prop is passed 1`] = ` - - - Touchable - - -`; - -exports[`TouchableOpacity renders in disabled state when a key disabled in accessibilityState is passed 1`] = ` - - - Touchable - - -`; diff --git a/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableWithoutFeedback-test.js.snap b/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableWithoutFeedback-test.js.snap deleted file mode 100644 index 932c5f133c03..000000000000 --- a/packages/react-native/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableWithoutFeedback-test.js.snap +++ /dev/null @@ -1,141 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TouchableWithoutFeedback renders correctly 1`] = ` - - Touchable - -`; - -exports[`TouchableWithoutFeedback with disabled state should be disabled when disabled is true 1`] = ` - -`; - -exports[`TouchableWithoutFeedback with disabled state should be disabled when disabled is true and accessibilityState is empty 1`] = ` - -`; - -exports[`TouchableWithoutFeedback with disabled state should disable button when accessibilityState is disabled 1`] = ` - -`; - -exports[`TouchableWithoutFeedback with disabled state should keep accessibilityState when disabled is true 1`] = ` - -`; - -exports[`TouchableWithoutFeedback with disabled state should overwrite accessibilityState with value of disabled prop 1`] = ` - -`; diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js index 8d8e7e69410c..240babf7ddc4 100644 --- a/packages/react-native/Libraries/Components/View/__tests__/View-itest.js +++ b/packages/react-native/Libraries/Components/View/__tests__/View-itest.js @@ -21,6 +21,10 @@ import {View} from 'react-native'; import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; describe('', () => { + it('has displayName', () => { + expect(View.displayName ?? View.name).toBe('View'); + }); + describe('props', () => { describe('style', () => { describe('width and height style', () => { @@ -1090,6 +1094,20 @@ describe('', () => { }); }); + describe('testID', () => { + it('is propagated to the mounting layer', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(root.getRenderedOutput({props: ['testID']}).toJSX()).toEqual( + , + ); + }); + }); + describe('ref', () => { it('is an element node', () => { const elementRef = createRef(); diff --git a/packages/react-native/Libraries/Components/View/__tests__/View-test.js b/packages/react-native/Libraries/Components/View/__tests__/View-test.js deleted file mode 100644 index b1e8b598652f..000000000000 --- a/packages/react-native/Libraries/Components/View/__tests__/View-test.js +++ /dev/null @@ -1,194 +0,0 @@ -/** - * 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 - */ - -jest.unmock('../View'); -jest.unmock('../ViewNativeComponent'); - -import {create} from '@react-native/jest-preset/jest/renderer'; -import * as React from 'react'; - -const View = require('../View').default; - -describe('View', () => { - it('default render', async () => { - const instance = await create(); - - expect(instance.toJSON()).toMatchInlineSnapshot(``); - }); - - it('has displayName', () => { - expect(View.displayName ?? View.name).toEqual('View'); - }); -}); - -describe('View compat with web', () => { - it('renders core props', async () => { - const props = { - id: 'id', - tabIndex: 0 as const, - testID: 'testID', - }; - - const instance = await create(); - - expect(instance.toJSON()).toMatchInlineSnapshot(` - - `); - }); - - it('renders "aria-*" props', async () => { - const props = { - 'aria-activedescendant': 'activedescendant', - 'aria-atomic': true, - 'aria-autocomplete': 'list', - 'aria-busy': true, - 'aria-checked': true, - 'aria-columncount': 5, - 'aria-columnindex': 3, - 'aria-columnspan': 2, - 'aria-controls': 'controls', - 'aria-current': 'current', - 'aria-describedby': 'describedby', - 'aria-details': 'details', - 'aria-disabled': true, - 'aria-errormessage': 'errormessage', - 'aria-expanded': true, - 'aria-flowto': 'flowto', - 'aria-haspopup': true, - 'aria-hidden': true, - 'aria-invalid': true, - 'aria-keyshortcuts': 'Cmd+S', - 'aria-label': 'label', - 'aria-labelledby': 'labelledby', - 'aria-level': 3, - 'aria-live': 'polite' as const, - 'aria-modal': true, - 'aria-multiline': true, - 'aria-multiselectable': true, - 'aria-orientation': 'portrait', - 'aria-owns': 'owns', - 'aria-placeholder': 'placeholder', - 'aria-posinset': 5, - 'aria-pressed': true, - 'aria-readonly': true, - 'aria-required': true, - role: 'main' as const, - 'aria-roledescription': 'roledescription', - 'aria-rowcount': 5, - 'aria-rowindex': 3, - 'aria-rowspan': 3, - 'aria-selected': true, - 'aria-setsize': 5, - 'aria-sort': 'ascending', - 'aria-valuemax': 5, - 'aria-valuemin': 0, - 'aria-valuenow': 3, - 'aria-valuetext': '3', - }; - - // $FlowFixMe[incompatible-type] - const instance = await create(); - - expect(instance.toJSON()).toMatchInlineSnapshot(` - - `); - }); - - it('renders styles', async () => { - const style = { - display: 'flex', - flex: 1, - backgroundColor: 'white', - marginInlineStart: 10, - pointerEvents: 'none', - }; - - // $FlowFixMe[incompatible-type] - const instance = await create(); - - expect(instance.toJSON()).toMatchInlineSnapshot(` - - `); - }); -}); diff --git a/packages/react-native/Libraries/Components/__tests__/Button-itest.js b/packages/react-native/Libraries/Components/__tests__/Button-itest.js index 07d4d5370dd5..5ff8d88e138f 100644 --- a/packages/react-native/Libraries/Components/__tests__/Button-itest.js +++ b/packages/react-native/Libraries/Components/__tests__/Button-itest.js @@ -243,5 +243,48 @@ describe('