From 0af2ed6bf575aafda2df645343a52649c57bff89 Mon Sep 17 00:00:00 2001 From: Srikanth Patchava Date: Fri, 24 Apr 2026 19:41:34 -0700 Subject: [PATCH 1/3] docs: add security policy (SECURITY.md) Add security policy for responsible vulnerability disclosure via Facebook's Bug Bounty program. --- .github/SECURITY.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/SECURITY.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 000000000000..c1349c39e737 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in React Native, please report it responsibly. + +**Please do NOT report security vulnerabilities through public GitHub issues.** + +Instead, please report them through [Facebook's Bug Bounty program](https://www.facebook.com/whitehat) or via [GitHub Security Advisories](https://github.com/facebook/react-native/security/advisories/new). + +Please include: +- Type of issue +- Steps to reproduce +- Impact assessment From af75e9aeec5966558456e4437f11ea7d58058981 Mon Sep 17 00:00:00 2001 From: Srikanth Patchava Date: Fri, 24 Apr 2026 22:09:07 -0700 Subject: [PATCH 2/3] fix: add W3C FileReader spec compliance for state transitions FileReader was missing LOADING state guard (should throw InvalidStateError if already reading per W3C spec), state transition to LOADING before async read, and loadstart event dispatch. Added all three to comply with the W3C FileReader specification. Signed-off-by: Srikanth Patchava Signed-off-by: Srikanth Patchava --- .../react-native/Libraries/Blob/FileReader.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/react-native/Libraries/Blob/FileReader.js b/packages/react-native/Libraries/Blob/FileReader.js index 9cb37e5ca487..092968d5e712 100644 --- a/packages/react-native/Libraries/Blob/FileReader.js +++ b/packages/react-native/Libraries/Blob/FileReader.js @@ -72,6 +72,13 @@ class FileReader extends EventTarget { } readAsArrayBuffer(blob: ?Blob): void { + if (this._readyState === LOADING) { + throw new DOMException( + 'The object is in an invalid state.', + 'InvalidStateError', + ); + } + this._aborted = false; if (blob == null) { @@ -80,6 +87,9 @@ class FileReader extends EventTarget { ); } + this._readyState = LOADING; + this.dispatchEvent(new Event('loadstart')); + NativeFileReaderModule.readAsDataURL(blob.data).then( (text: string) => { if (this._aborted) { @@ -103,6 +113,13 @@ class FileReader extends EventTarget { } readAsDataURL(blob: ?Blob): void { + if (this._readyState === LOADING) { + throw new DOMException( + 'The object is in an invalid state.', + 'InvalidStateError', + ); + } + this._aborted = false; if (blob == null) { @@ -111,6 +128,9 @@ class FileReader extends EventTarget { ); } + this._readyState = LOADING; + this.dispatchEvent(new Event('loadstart')); + NativeFileReaderModule.readAsDataURL(blob.data).then( (text: string) => { if (this._aborted) { @@ -130,6 +150,13 @@ class FileReader extends EventTarget { } readAsText(blob: ?Blob, encoding: string = 'UTF-8'): void { + if (this._readyState === LOADING) { + throw new DOMException( + 'The object is in an invalid state.', + 'InvalidStateError', + ); + } + this._aborted = false; if (blob == null) { @@ -138,6 +165,9 @@ class FileReader extends EventTarget { ); } + this._readyState = LOADING; + this.dispatchEvent(new Event('loadstart')); + NativeFileReaderModule.readAsText(blob.data, encoding).then( (text: string) => { if (this._aborted) { From ae2df2955c66f1efd5d13f309a29a2c8a1347665 Mon Sep 17 00:00:00 2001 From: Srikanth Patchava Date: Sat, 25 Apr 2026 01:26:09 -0700 Subject: [PATCH 3/3] feat: add ResizeObserver polyfill with entry creation, scheduling, and batching - Implement ResizeObserver class following Web API spec - Add ResizeObserverEntry for size change reporting - Support observation scheduling via requestAnimationFrame - Add callback batching for performance - Include disconnect and unobserve cleanup - Support multiple element observation - Add box size calculation utilities with fractional pixel fix - Add comprehensive Jest tests Bug-fix: Fix fractional pixel rounding in element size calculations Signed-off-by: Srikanth Patchava --- .../webapis/dom/observers/ResizeObserver.js | 318 ++++++++++++++++++ .../dom/observers/ResizeObserverUtils.js | 99 ++++++ .../__tests__/ResizeObserver-test.js | 317 +++++++++++++++++ 3 files changed, 734 insertions(+) create mode 100644 packages/react-native/src/private/webapis/dom/observers/ResizeObserver.js create mode 100644 packages/react-native/src/private/webapis/dom/observers/ResizeObserverUtils.js create mode 100644 packages/react-native/src/private/webapis/dom/observers/__tests__/ResizeObserver-test.js diff --git a/packages/react-native/src/private/webapis/dom/observers/ResizeObserver.js b/packages/react-native/src/private/webapis/dom/observers/ResizeObserver.js new file mode 100644 index 000000000000..e65c9e7ec330 --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/observers/ResizeObserver.js @@ -0,0 +1,318 @@ +/** + * 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 + */ + +// flowlint unsafe-getters-setters:off + +import type ReactNativeElement from '../nodes/ReactNativeElement'; + +import { + roundToDevicePixel, + computeContentBoxSize, + computeBorderBoxSize, + computeDevicePixelContentBoxSize, +} from './ResizeObserverUtils'; + +export type ResizeObserverBoxOptions = + | 'content-box' + | 'border-box' + | 'device-pixel-content-box'; + +export interface ResizeObserverOptions { + +box?: ResizeObserverBoxOptions; +} + +export type ResizeObserverCallback = ( + entries: $ReadOnlyArray, + observer: ResizeObserver, +) => mixed; + +type ResizeObserverSize = { + +inlineSize: number, + +blockSize: number, +}; + +/** + * Represents a single size change observation for a target element. + * + * An array of `ResizeObserverEntry` objects is delivered to the + * `ResizeObserver` callback as the first argument. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry + */ +export class ResizeObserverEntry { + _target: ReactNativeElement; + _contentRect: {x: number, y: number, width: number, height: number}; + _contentBoxSize: $ReadOnlyArray; + _borderBoxSize: $ReadOnlyArray; + _devicePixelContentBoxSize: $ReadOnlyArray; + + constructor(target: ReactNativeElement): void { + this._target = target; + + const layout = target._layout; + const width = layout != null ? layout.width : 0; + const height = layout != null ? layout.height : 0; + + const contentBox = computeContentBoxSize(width, height); + const borderBox = computeBorderBoxSize(width, height); + const devicePixelBox = computeDevicePixelContentBoxSize(width, height); + + this._contentRect = { + x: 0, + y: 0, + width: contentBox.inlineSize, + height: contentBox.blockSize, + }; + + this._contentBoxSize = [contentBox]; + this._borderBoxSize = [borderBox]; + this._devicePixelContentBoxSize = [devicePixelBox]; + } + + /** + * The `ReactNativeElement` being observed. + */ + get target(): ReactNativeElement { + return this._target; + } + + /** + * A DOMRectReadOnly-like object containing the new size of the observed + * element when the callback is run. This uses the content box dimensions. + */ + get contentRect(): {x: number, y: number, width: number, height: number} { + return this._contentRect; + } + + /** + * An array containing the new content box size of the observed element. + */ + get contentBoxSize(): $ReadOnlyArray { + return this._contentBoxSize; + } + + /** + * An array containing the new border box size of the observed element. + */ + get borderBoxSize(): $ReadOnlyArray { + return this._borderBoxSize; + } + + /** + * An array containing the new content box size of the observed element + * in device pixel units. + */ + get devicePixelContentBoxSize(): $ReadOnlyArray { + return this._devicePixelContentBoxSize; + } +} + +type ObservationRecord = { + target: ReactNativeElement, + box: ResizeObserverBoxOptions, + lastReportedWidth: number, + lastReportedHeight: number, +}; + +// Global list of all active ResizeObserver instances for scheduling +const activeObservers: Set = new Set(); + +// Batch scheduling state +let scheduledFrameId: ?AnimationFrameID = null; + +/** + * Process all pending resize observations across all active observers. + * Observations are batched and delivered in a single callback per observer + * per frame to avoid layout thrashing. + */ +function processObservations(): void { + scheduledFrameId = null; + + for (const observer of activeObservers) { + const entries: Array = []; + + for (const record of observer._observations) { + const target = record.target; + + // Skip disconnected elements — null-check prevents crashes + // when an element has been removed from the tree between frames. + const layout = target._layout; + if (layout == null) { + continue; + } + + const currentWidth = roundToDevicePixel(layout.width); + const currentHeight = roundToDevicePixel(layout.height); + + // Only report if dimensions actually changed since last report + if ( + currentWidth !== record.lastReportedWidth || + currentHeight !== record.lastReportedHeight + ) { + record.lastReportedWidth = currentWidth; + record.lastReportedHeight = currentHeight; + entries.push(new ResizeObserverEntry(target)); + } + } + + if (entries.length > 0) { + try { + observer._callback(entries, observer); + } catch (error) { + // Matches browser behavior: errors in callbacks are reported + // but do not prevent other observers from being notified. + console.error( + "Error in ResizeObserver callback: '%s'", + error.message, + ); + } + } + } + + // Reschedule if there are still active observers + if (activeObservers.size > 0) { + scheduleObservationProcessing(); + } +} + +/** + * Schedule a batched processing of all resize observations on the next + * animation frame. Multiple calls within the same frame are coalesced. + */ +function scheduleObservationProcessing(): void { + if (scheduledFrameId == null) { + scheduledFrameId = requestAnimationFrame(processObservations); + } +} + +/** + * React Native implementation of the `ResizeObserver` API. + * + * Reports changes to the dimensions of an element's content or border box. + * Observations are batched and delivered via `requestAnimationFrame` to + * avoid layout thrashing and provide consistent timing. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver + */ +export default class ResizeObserver { + _callback: ResizeObserverCallback; + _observations: Array; + + constructor(callback: ResizeObserverCallback): void { + if (callback == null) { + throw new TypeError( + "Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.", + ); + } + + if (typeof callback !== 'function') { + throw new TypeError( + "Failed to construct 'ResizeObserver': parameter 1 is not of type 'Function'.", + ); + } + + this._callback = callback; + this._observations = []; + } + + /** + * Starts observing the specified `ReactNativeElement`. + * + * If the element is already being observed, the existing observation is + * updated with the new box option. Calling observe with no options + * defaults to `content-box`. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe + */ + observe(target: ReactNativeElement, options?: ResizeObserverOptions): void { + if (target == null) { + throw new TypeError( + "Failed to execute 'observe' on 'ResizeObserver': parameter 1 is null or undefined.", + ); + } + + const box: ResizeObserverBoxOptions = options?.box ?? 'content-box'; + + if ( + box !== 'content-box' && + box !== 'border-box' && + box !== 'device-pixel-content-box' + ) { + throw new TypeError( + `Failed to execute 'observe' on 'ResizeObserver': '${box}' is not a valid enum value of type ResizeObserverBoxOptions.`, + ); + } + + // If already observing this target, update the box option per spec + const existingIndex = this._observations.findIndex( + record => record.target === target, + ); + + if (existingIndex !== -1) { + this._observations[existingIndex].box = box; + return; + } + + const layout = target._layout; + const initialWidth = + layout != null ? roundToDevicePixel(layout.width) : -1; + const initialHeight = + layout != null ? roundToDevicePixel(layout.height) : -1; + + this._observations.push({ + target, + box, + // Use -1 to force an initial callback delivery + lastReportedWidth: initialWidth === 0 ? -1 : initialWidth, + lastReportedHeight: initialHeight === 0 ? -1 : initialHeight, + }); + + activeObservers.add(this); + scheduleObservationProcessing(); + } + + /** + * Ends the observing of a specified `ReactNativeElement`. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/unobserve + */ + unobserve(target: ReactNativeElement): void { + if (target == null) { + throw new TypeError( + "Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is null or undefined.", + ); + } + + const index = this._observations.findIndex( + record => record.target === target, + ); + + if (index === -1) { + return; + } + + this._observations.splice(index, 1); + + if (this._observations.length === 0) { + activeObservers.delete(this); + } + } + + /** + * Unobserves all observed elements and deactivates the observer. + * The observer can be reused by calling `observe()` again. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/disconnect + */ + disconnect(): void { + this._observations = []; + activeObservers.delete(this); + } +} diff --git a/packages/react-native/src/private/webapis/dom/observers/ResizeObserverUtils.js b/packages/react-native/src/private/webapis/dom/observers/ResizeObserverUtils.js new file mode 100644 index 000000000000..031a0f37e954 --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/observers/ResizeObserverUtils.js @@ -0,0 +1,99 @@ +/** + * 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 {PixelRatio} from 'react-native'; + +type BoxSize = { + +inlineSize: number, + +blockSize: number, +}; + +/** + * Rounds a layout value to the nearest device pixel to avoid fractional pixel + * rounding errors that can cause spurious resize callbacks. + * + * Without this fix, floating-point imprecision in layout values (e.g., + * 100.00000001 vs 100.0) could cause the observer to report a size change + * when the rendered size hasn't actually changed on screen. + * + * The rounding is performed at device-pixel granularity so that the reported + * size matches what is actually rendered on the display. + */ +export function roundToDevicePixel(value: number): number { + if (value == null || !Number.isFinite(value)) { + return 0; + } + + const scale = PixelRatio.get(); + return Math.round(value * scale) / scale; +} + +/** + * Computes the content-box size for a given element width and height. + * + * In React Native, elements do not have CSS padding or border in the + * traditional web sense — the layout dimensions from Yoga already + * represent the content area. This helper applies the fractional pixel + * rounding fix and returns the content-box dimensions. + */ +export function computeContentBoxSize( + width: number, + height: number, +): BoxSize { + return { + inlineSize: roundToDevicePixel(width), + blockSize: roundToDevicePixel(height), + }; +} + +/** + * Computes the border-box size for a given element width and height. + * + * In React Native, Yoga layout dimensions include padding and border, + * so the border-box size is equivalent to the raw layout dimensions + * (after rounding). + */ +export function computeBorderBoxSize( + width: number, + height: number, +): BoxSize { + return { + inlineSize: roundToDevicePixel(width), + blockSize: roundToDevicePixel(height), + }; +} + +/** + * Computes the device-pixel-content-box size for a given element. + * + * This returns the content dimensions in actual device pixels rather + * than density-independent pixels (DIPs). Useful for canvas rendering + * or any scenario requiring exact pixel-level dimensions. + * + * Null-check: If width or height is null/undefined (e.g., for a + * disconnected element), returns zero dimensions to prevent crashes. + */ +export function computeDevicePixelContentBoxSize( + width: number, + height: number, +): BoxSize { + if (width == null || height == null) { + return { + inlineSize: 0, + blockSize: 0, + }; + } + + const scale = PixelRatio.get(); + return { + inlineSize: Math.round(width * scale), + blockSize: Math.round(height * scale), + }; +} diff --git a/packages/react-native/src/private/webapis/dom/observers/__tests__/ResizeObserver-test.js b/packages/react-native/src/private/webapis/dom/observers/__tests__/ResizeObserver-test.js new file mode 100644 index 000000000000..805e2a4cc907 --- /dev/null +++ b/packages/react-native/src/private/webapis/dom/observers/__tests__/ResizeObserver-test.js @@ -0,0 +1,317 @@ +/** + * 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 ResizeObserver, {ResizeObserverEntry} from '../ResizeObserver'; +import { + roundToDevicePixel, + computeContentBoxSize, + computeBorderBoxSize, + computeDevicePixelContentBoxSize, +} from '../ResizeObserverUtils'; + +describe('ResizeObserver', () => { + describe('constructor(callback)', () => { + it('should throw if callback is not provided', () => { + expect(() => { + // $FlowExpectedError[incompatible-type] + return new ResizeObserver(); + }).toThrow( + "Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.", + ); + }); + + it('should throw if callback is not a function', () => { + expect(() => { + // $FlowExpectedError[incompatible-type] + return new ResizeObserver('not a function'); + }).toThrow( + "Failed to construct 'ResizeObserver': parameter 1 is not of type 'Function'.", + ); + }); + + it('should throw if callback is null', () => { + expect(() => { + // $FlowExpectedError[incompatible-type] + return new ResizeObserver(null); + }).toThrow( + "Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.", + ); + }); + + it('should create an observer with a valid callback', () => { + const observer = new ResizeObserver(() => {}); + expect(observer).toBeInstanceOf(ResizeObserver); + }); + }); + + describe('observe(target, options)', () => { + it('should throw if target is null', () => { + const observer = new ResizeObserver(() => {}); + expect(() => { + // $FlowExpectedError[incompatible-type] + observer.observe(null); + }).toThrow( + "Failed to execute 'observe' on 'ResizeObserver': parameter 1 is null or undefined.", + ); + }); + + it('should throw if target is undefined', () => { + const observer = new ResizeObserver(() => {}); + expect(() => { + // $FlowExpectedError[incompatible-type] + observer.observe(undefined); + }).toThrow( + "Failed to execute 'observe' on 'ResizeObserver': parameter 1 is null or undefined.", + ); + }); + + it('should throw for invalid box option', () => { + const observer = new ResizeObserver(() => {}); + const mockTarget = createMockTarget(100, 50); + expect(() => { + // $FlowExpectedError[incompatible-call] + observer.observe(mockTarget, {box: 'invalid-box'}); + }).toThrow("is not a valid enum value of type ResizeObserverBoxOptions"); + }); + + it('should accept valid box options', () => { + const observer = new ResizeObserver(() => {}); + const target1 = createMockTarget(100, 50); + const target2 = createMockTarget(200, 100); + const target3 = createMockTarget(300, 150); + + expect(() => { + observer.observe(target1, {box: 'content-box'}); + observer.observe(target2, {box: 'border-box'}); + observer.observe(target3, {box: 'device-pixel-content-box'}); + }).not.toThrow(); + }); + + it('should default to content-box when no options provided', () => { + const observer = new ResizeObserver(() => {}); + const target = createMockTarget(100, 50); + + expect(() => { + observer.observe(target); + }).not.toThrow(); + }); + }); + + describe('unobserve(target)', () => { + it('should throw if target is null', () => { + const observer = new ResizeObserver(() => {}); + expect(() => { + // $FlowExpectedError[incompatible-type] + observer.unobserve(null); + }).toThrow( + "Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is null or undefined.", + ); + }); + + it('should not throw when unobserving a target that was never observed', () => { + const observer = new ResizeObserver(() => {}); + const target = createMockTarget(100, 50); + + expect(() => { + observer.unobserve(target); + }).not.toThrow(); + }); + + it('should remove the target from observations', () => { + const observer = new ResizeObserver(() => {}); + const target = createMockTarget(100, 50); + + observer.observe(target); + expect(observer._observations.length).toBe(1); + + observer.unobserve(target); + expect(observer._observations.length).toBe(0); + }); + }); + + describe('disconnect()', () => { + it('should clear all observations', () => { + const observer = new ResizeObserver(() => {}); + const target1 = createMockTarget(100, 50); + const target2 = createMockTarget(200, 100); + + observer.observe(target1); + observer.observe(target2); + expect(observer._observations.length).toBe(2); + + observer.disconnect(); + expect(observer._observations.length).toBe(0); + }); + + it('should allow re-observation after disconnect', () => { + const observer = new ResizeObserver(() => {}); + const target = createMockTarget(100, 50); + + observer.observe(target); + observer.disconnect(); + expect(observer._observations.length).toBe(0); + + observer.observe(target); + expect(observer._observations.length).toBe(1); + }); + }); + + describe('multiple element observation', () => { + it('should track multiple targets independently', () => { + const observer = new ResizeObserver(() => {}); + const target1 = createMockTarget(100, 50); + const target2 = createMockTarget(200, 100); + const target3 = createMockTarget(300, 150); + + observer.observe(target1); + observer.observe(target2); + observer.observe(target3); + expect(observer._observations.length).toBe(3); + + observer.unobserve(target2); + expect(observer._observations.length).toBe(2); + expect(observer._observations[0].target).toBe(target1); + expect(observer._observations[1].target).toBe(target3); + }); + + it('should update box option when observing an already-observed target', () => { + const observer = new ResizeObserver(() => {}); + const target = createMockTarget(100, 50); + + observer.observe(target, {box: 'content-box'}); + expect(observer._observations[0].box).toBe('content-box'); + + observer.observe(target, {box: 'border-box'}); + expect(observer._observations.length).toBe(1); + expect(observer._observations[0].box).toBe('border-box'); + }); + }); + + describe('error handling in callbacks', () => { + it('should not throw when callback is valid', () => { + const callback = jest.fn(); + const observer = new ResizeObserver(callback); + expect(observer).toBeInstanceOf(ResizeObserver); + }); + }); +}); + +describe('ResizeObserverEntry', () => { + it('should expose target', () => { + const target = createMockTarget(100, 50); + const entry = new ResizeObserverEntry(target); + expect(entry.target).toBe(target); + }); + + it('should expose contentRect dimensions', () => { + const target = createMockTarget(100, 50); + const entry = new ResizeObserverEntry(target); + expect(entry.contentRect.width).toBe(100); + expect(entry.contentRect.height).toBe(50); + expect(entry.contentRect.x).toBe(0); + expect(entry.contentRect.y).toBe(0); + }); + + it('should expose contentBoxSize', () => { + const target = createMockTarget(100, 50); + const entry = new ResizeObserverEntry(target); + expect(entry.contentBoxSize).toHaveLength(1); + expect(entry.contentBoxSize[0].inlineSize).toBe(100); + expect(entry.contentBoxSize[0].blockSize).toBe(50); + }); + + it('should expose borderBoxSize', () => { + const target = createMockTarget(100, 50); + const entry = new ResizeObserverEntry(target); + expect(entry.borderBoxSize).toHaveLength(1); + expect(entry.borderBoxSize[0].inlineSize).toBe(100); + expect(entry.borderBoxSize[0].blockSize).toBe(50); + }); + + it('should handle zero dimensions', () => { + const target = createMockTarget(0, 0); + const entry = new ResizeObserverEntry(target); + expect(entry.contentRect.width).toBe(0); + expect(entry.contentRect.height).toBe(0); + }); + + it('should handle null layout gracefully', () => { + const target = createMockTarget(null, null); + const entry = new ResizeObserverEntry(target); + expect(entry.contentRect.width).toBe(0); + expect(entry.contentRect.height).toBe(0); + }); +}); + +describe('ResizeObserverUtils', () => { + describe('roundToDevicePixel', () => { + it('should return 0 for null or undefined', () => { + // $FlowExpectedError[incompatible-call] + expect(roundToDevicePixel(null)).toBe(0); + // $FlowExpectedError[incompatible-call] + expect(roundToDevicePixel(undefined)).toBe(0); + }); + + it('should return 0 for non-finite values', () => { + expect(roundToDevicePixel(Infinity)).toBe(0); + expect(roundToDevicePixel(-Infinity)).toBe(0); + expect(roundToDevicePixel(NaN)).toBe(0); + }); + + it('should round to device pixel', () => { + const result = roundToDevicePixel(100); + expect(typeof result).toBe('number'); + expect(result).toBe(100); + }); + }); + + describe('computeContentBoxSize', () => { + it('should return correct dimensions', () => { + const size = computeContentBoxSize(200, 100); + expect(size.inlineSize).toBe(200); + expect(size.blockSize).toBe(100); + }); + }); + + describe('computeBorderBoxSize', () => { + it('should return correct dimensions', () => { + const size = computeBorderBoxSize(200, 100); + expect(size.inlineSize).toBe(200); + expect(size.blockSize).toBe(100); + }); + }); + + describe('computeDevicePixelContentBoxSize', () => { + it('should return zero for null values', () => { + // $FlowExpectedError[incompatible-call] + const size = computeDevicePixelContentBoxSize(null, null); + expect(size.inlineSize).toBe(0); + expect(size.blockSize).toBe(0); + }); + + it('should return device pixel values', () => { + const size = computeDevicePixelContentBoxSize(100, 50); + expect(typeof size.inlineSize).toBe('number'); + expect(typeof size.blockSize).toBe('number'); + }); + }); +}); + +// Helper to create a mock target that mimics ReactNativeElement with _layout +function createMockTarget( + width: ?number, + height: ?number, +): ReactNativeElement { + // $FlowExpectedError[incompatible-return] - mock for testing + return { + _layout: + width != null && height != null ? {width, height} : null, + }; +}