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/3] 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/3] 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 b968786ac26beeb641e8b5be70a6fe264223bc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 18 Jun 2026 03:48:40 -0700 Subject: [PATCH 3/3] 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';