diff --git a/packages/react-native/src/private/setup/__tests__/setUpDefaultReactNativeEnvironment-Globals-IntersectionObserver-itest.js b/packages/react-native/src/private/setup/__tests__/setUpDefaultReactNativeEnvironment-Globals-IntersectionObserver-itest.js new file mode 100644 index 000000000000..2b8e03447d33 --- /dev/null +++ b/packages/react-native/src/private/setup/__tests__/setUpDefaultReactNativeEnvironment-Globals-IntersectionObserver-itest.js @@ -0,0 +1,44 @@ +/** + * 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. + * + * @fantom_flags enableIntersectionObserverByDefault:* + * @flow strict-local + * @format + * @oncall react_native + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags'; + +declare var IntersectionObserverEntry: unknown; + +// TODO: Merge into `setUpDefaultReactNativeEnvironment-Globals-itest.js` once +// the `enableIntersectionObserverByDefault` feature flag is cleaned up and the +// IntersectionObserver globals are exposed unconditionally. +describe('setUpDefaultReactNativeEnvironment (IntersectionObserver globals)', () => { + if (ReactNativeFeatureFlags.enableIntersectionObserverByDefault()) { + describe('when enableIntersectionObserverByDefault is enabled', () => { + it('should provide IntersectionObserver', () => { + expect(typeof IntersectionObserver).toBe('function'); + }); + + it('should provide IntersectionObserverEntry', () => { + expect(typeof IntersectionObserverEntry).toBe('function'); + }); + }); + } else { + describe('when enableIntersectionObserverByDefault is disabled', () => { + it('should not provide IntersectionObserver', () => { + expect(typeof IntersectionObserver).toBe('undefined'); + }); + + it('should not provide IntersectionObserverEntry', () => { + expect(typeof IntersectionObserverEntry).toBe('undefined'); + }); + }); + } +}); diff --git a/packages/react-native/src/private/setup/__tests__/setUpDefaultReactNativeEnvironment-Globals-MutationObserver-itest.js b/packages/react-native/src/private/setup/__tests__/setUpDefaultReactNativeEnvironment-Globals-MutationObserver-itest.js new file mode 100644 index 000000000000..12cab7d5bf3f --- /dev/null +++ b/packages/react-native/src/private/setup/__tests__/setUpDefaultReactNativeEnvironment-Globals-MutationObserver-itest.js @@ -0,0 +1,44 @@ +/** + * 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. + * + * @fantom_flags enableMutationObserverByDefault:* + * @flow strict-local + * @format + * @oncall react_native + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags'; + +declare var MutationRecord: unknown; + +// TODO: Merge into `setUpDefaultReactNativeEnvironment-Globals-itest.js` once +// the `enableMutationObserverByDefault` feature flag is cleaned up and the +// MutationObserver globals are exposed unconditionally. +describe('setUpDefaultReactNativeEnvironment (MutationObserver globals)', () => { + if (ReactNativeFeatureFlags.enableMutationObserverByDefault()) { + describe('when enableMutationObserverByDefault is enabled', () => { + it('should provide MutationObserver', () => { + expect(typeof MutationObserver).toBe('function'); + }); + + it('should provide MutationRecord', () => { + expect(typeof MutationRecord).toBe('function'); + }); + }); + } else { + describe('when enableMutationObserverByDefault is disabled', () => { + it('should not provide MutationObserver', () => { + expect(typeof MutationObserver).toBe('undefined'); + }); + + it('should not provide MutationRecord', () => { + expect(typeof MutationRecord).toBe('undefined'); + }); + }); + } +}); diff --git a/packages/react-native/src/private/setup/setUpDOM.js b/packages/react-native/src/private/setup/setUpDOM.js index 24f2c295deba..00949f51725c 100644 --- a/packages/react-native/src/private/setup/setUpDOM.js +++ b/packages/react-native/src/private/setup/setUpDOM.js @@ -31,47 +31,57 @@ export default function setUpDOM() { polyfillGlobal( 'DOMRectList', - () => require('../webapis/geometry/DOMRectList').default, + () => require('../webapis/geometry/DOMRectList').DOMRectList_public, ); polyfillGlobal( 'HTMLCollection', - () => require('../webapis/dom/oldstylecollections/HTMLCollection').default, + () => + require('../webapis/dom/oldstylecollections/HTMLCollection') + .HTMLCollection_public, ); polyfillGlobal( 'NodeList', - () => require('../webapis/dom/oldstylecollections/NodeList').default, + () => + require('../webapis/dom/oldstylecollections/NodeList').NodeList_public, ); polyfillGlobal( 'Node', - () => require('../webapis/dom/nodes/ReadOnlyNode').default, + () => require('../webapis/dom/nodes/ReadOnlyNode').ReadOnlyNode_public, ); polyfillGlobal( 'Document', - () => require('../webapis/dom/nodes/ReactNativeDocument').default, + () => + require('../webapis/dom/nodes/ReactNativeDocument') + .ReactNativeDocument_public, ); polyfillGlobal( 'CharacterData', - () => require('../webapis/dom/nodes/ReadOnlyCharacterData').default, + () => + require('../webapis/dom/nodes/ReadOnlyCharacterData') + .ReadOnlyCharacterData_public, ); polyfillGlobal( 'Text', - () => require('../webapis/dom/nodes/ReadOnlyText').default, + () => require('../webapis/dom/nodes/ReadOnlyText').ReadOnlyText_public, ); polyfillGlobal( 'Element', - () => require('../webapis/dom/nodes/ReadOnlyElement').default, + () => + require('../webapis/dom/nodes/ReadOnlyElement').ReadOnlyElement_public, ); polyfillGlobal( 'HTMLElement', - () => require('../webapis/dom/nodes/ReactNativeElement').default, + () => + require('../webapis/dom/nodes/ReactNativeElement') + .ReactNativeElement_public, ); polyfillGlobal('Event', () => require('../webapis/dom/events/Event').default); diff --git a/packages/react-native/src/private/setup/setUpIntersectionObserver.js b/packages/react-native/src/private/setup/setUpIntersectionObserver.js index 773a6d9f6d82..7275dbc8293a 100644 --- a/packages/react-native/src/private/setup/setUpIntersectionObserver.js +++ b/packages/react-native/src/private/setup/setUpIntersectionObserver.js @@ -24,4 +24,11 @@ export default function setUpIntersectionObserver() { () => require('../webapis/intersectionobserver/IntersectionObserver').default, ); + + polyfillGlobal( + 'IntersectionObserverEntry', + () => + require('../webapis/intersectionobserver/IntersectionObserverEntry') + .IntersectionObserverEntry_public, + ); } diff --git a/packages/react-native/src/private/setup/setUpMutationObserver.js b/packages/react-native/src/private/setup/setUpMutationObserver.js index e3f03b59172e..c0a4817e8ff0 100644 --- a/packages/react-native/src/private/setup/setUpMutationObserver.js +++ b/packages/react-native/src/private/setup/setUpMutationObserver.js @@ -26,6 +26,8 @@ export default function setUpMutationObserver() { polyfillGlobal( 'MutationRecord', - () => require('../webapis/mutationobserver/MutationRecord').default, + () => + require('../webapis/mutationobserver/MutationRecord') + .MutationRecord_public, ); } diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js index 2c28e4a22f49..7ecad0a58565 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeDocument.js @@ -139,3 +139,14 @@ export function createReactNativeDocument( const document = new ReactNativeDocument(rootTag, instanceHandle); return document; } + +export const ReactNativeDocument_public: typeof ReactNativeDocument = + // $FlowExpectedError[incompatible-type] + function Document() { + throw new TypeError( + "Failed to construct 'Document': Nodes cannot be imperatively created in React Native", + ); + }; + +// $FlowExpectedError[prop-missing] +ReactNativeDocument_public.prototype = ReactNativeDocument.prototype; diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js index 0cd9e0f18016..df933174b370 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js @@ -292,3 +292,14 @@ function replaceConstructorWithoutSuper( export default replaceConstructorWithoutSuper( ReactNativeElement, ) as typeof ReactNativeElement; + +export const ReactNativeElement_public: typeof ReactNativeElement = + // $FlowExpectedError[incompatible-type] + function HTMLElement() { + throw new TypeError( + "Failed to construct 'HTMLElement': Nodes cannot be imperatively created in React Native", + ); + }; + +// $FlowExpectedError[prop-missing] +ReactNativeElement_public.prototype = ReactNativeElement.prototype; diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js index 0544349cf8c5..20fcfe94ce43 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyCharacterData.js @@ -70,3 +70,14 @@ export default class ReadOnlyCharacterData extends ReadOnlyNode { return data.slice(offset, offset + adjustedCount); } } + +export const ReadOnlyCharacterData_public: typeof ReadOnlyCharacterData = + // $FlowExpectedError[incompatible-type] + function CharacterData() { + throw new TypeError( + "Failed to construct 'CharacterData': Illegal constructor", + ); + }; + +// $FlowExpectedError[prop-missing] +ReadOnlyCharacterData_public.prototype = ReadOnlyCharacterData.prototype; diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyElement.js b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyElement.js index 1f0d82c2d600..1502a6fa1874 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyElement.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyElement.js @@ -245,3 +245,12 @@ export function getBoundingClientRect( // Empty rect if any of the above failed return new DOMRect(0, 0, 0, 0); } + +export const ReadOnlyElement_public: typeof ReadOnlyElement = + // $FlowExpectedError[incompatible-type] + function Element() { + throw new TypeError("Failed to construct 'Element': Illegal constructor"); + }; + +// $FlowExpectedError[prop-missing] +ReadOnlyElement_public.prototype = ReadOnlyElement.prototype; diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js index 03082500c2c8..9da614f16a44 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyNode.js @@ -371,6 +371,21 @@ export default replaceConstructorWithoutSuper( ReadOnlyNode, ) as typeof ReadOnlyNode; +export const ReadOnlyNode_public: typeof ReadOnlyNode = + // $FlowExpectedError[incompatible-type] + function Node() { + throw new TypeError("Failed to construct 'Node': Illegal constructor"); + }; + +// $FlowExpectedError[prop-missing] +ReadOnlyNode_public.prototype = ReadOnlyNode.prototype; +// Copy static properties (ELEMENT_NODE, DOCUMENT_NODE, TEXT_NODE, +// DOCUMENT_POSITION_*, etc.) so that callers accessing them via the public +// constructor (e.g. `Node.ELEMENT_NODE`) still work. +// $FlowFixMe[unsafe-object-assign] +// $FlowFixMe[not-an-object] +Object.assign(ReadOnlyNode_public, ReadOnlyNode); + // Temporary type until we ship ReadOnlyNode extending EventTarget ungated. export type ReadOnlyNodeWithEventTarget = ReadOnlyNode & EventTarget; diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyText.js b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyText.js index 58cc2dc69dbe..01dcbbae5955 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyText.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReadOnlyText.js @@ -28,3 +28,14 @@ export default class ReadOnlyText extends ReadOnlyCharacterData { return ReadOnlyNode.TEXT_NODE; } } + +export const ReadOnlyText_public: typeof ReadOnlyText = + // $FlowExpectedError[incompatible-type] + function Text() { + throw new TypeError( + "Failed to construct 'Text': Nodes cannot be imperatively created in React Native", + ); + }; + +// $FlowExpectedError[prop-missing] +ReadOnlyText_public.prototype = ReadOnlyText.prototype; diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js index 7db55b0e5636..8aeab00fd9f9 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeDocument-itest.js @@ -308,4 +308,12 @@ describe('ReactNativeDocument', () => { expect(document.getElementById('baz')).toBe(null); }); }); + + describe('global constructor', () => { + it('throws when called', () => { + expect(() => new Document()).toThrow( + "Failed to construct 'Document': Nodes cannot be imperatively created in React Native", + ); + }); + }); }); diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js index 196348fee7c1..6bd94995fda4 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReactNativeElement-itest.js @@ -1631,4 +1631,18 @@ describe('ReactNativeElement', () => { }); }); }); + + describe('global constructors', () => { + it('throws when constructing HTMLElement', () => { + expect(() => new HTMLElement()).toThrow( + "Failed to construct 'HTMLElement': Nodes cannot be imperatively created in React Native", + ); + }); + + it('throws when constructing Element', () => { + expect(() => new Element()).toThrow( + "Failed to construct 'Element': Illegal constructor", + ); + }); + }); }); diff --git a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReadOnlyText-itest.js b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReadOnlyText-itest.js index 71beb78fde3a..8f7b258ad920 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReadOnlyText-itest.js +++ b/packages/react-native/src/private/webapis/dom/nodes/__tests__/ReadOnlyText-itest.js @@ -331,4 +331,69 @@ describe('ReadOnlyText', () => { }); }); }); + + describe('global constructors', () => { + it('throws when constructing Text', () => { + expect(() => new Text()).toThrow( + "Failed to construct 'Text': Nodes cannot be imperatively created in React Native", + ); + }); + + it('throws when constructing CharacterData', () => { + expect(() => new CharacterData()).toThrow( + "Failed to construct 'CharacterData': Illegal constructor", + ); + }); + + it('throws when constructing Node', () => { + expect(() => new Node()).toThrow( + "Failed to construct 'Node': Illegal constructor", + ); + }); + + it('public stubs preserve `instanceof` against real instances', () => { + // The public stubs alias their prototype to the real class so that + // `instanceof` against the global still works for instances obtained + // from refs/observer callbacks. + const parentNodeRef = createRef(); + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(Some text); + }); + + const parentNode = ensureReadOnlyNode(parentNodeRef.current); + const textNode = parentNode.childNodes[0]; + + expect(textNode instanceof Node).toBe(true); + expect(textNode instanceof CharacterData).toBe(true); + expect(textNode instanceof Text).toBe(true); + }); + }); + + describe('Node static constants on the global', () => { + // The public `Node` stub also exposes the node-type and document-position + // constants from the spec so callers can read them off the global. + it('exposes node-type constants', () => { + expect(Node.ELEMENT_NODE).toBe(1); + expect(Node.ATTRIBUTE_NODE).toBe(2); + expect(Node.TEXT_NODE).toBe(3); + expect(Node.CDATA_SECTION_NODE).toBe(4); + expect(Node.PROCESSING_INSTRUCTION_NODE).toBe(7); + expect(Node.COMMENT_NODE).toBe(8); + expect(Node.DOCUMENT_NODE).toBe(9); + expect(Node.DOCUMENT_TYPE_NODE).toBe(10); + expect(Node.DOCUMENT_FRAGMENT_NODE).toBe(11); + }); + + it('exposes document-position constants', () => { + expect(Node.DOCUMENT_POSITION_DISCONNECTED).toBe(1); + expect(Node.DOCUMENT_POSITION_PRECEDING).toBe(2); + expect(Node.DOCUMENT_POSITION_FOLLOWING).toBe(4); + expect(Node.DOCUMENT_POSITION_CONTAINS).toBe(8); + expect(Node.DOCUMENT_POSITION_CONTAINED_BY).toBe(16); + expect(Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC).toBe(32); + }); + }); }); diff --git a/packages/react-native/src/private/webapis/dom/oldstylecollections/HTMLCollection.js b/packages/react-native/src/private/webapis/dom/oldstylecollections/HTMLCollection.js index 2688b799af6c..4295da55838d 100644 --- a/packages/react-native/src/private/webapis/dom/oldstylecollections/HTMLCollection.js +++ b/packages/react-native/src/private/webapis/dom/oldstylecollections/HTMLCollection.js @@ -86,3 +86,15 @@ export function createHTMLCollection( ): HTMLCollection { return new HTMLCollection(elements); } + +export const HTMLCollection_public: typeof HTMLCollection = + /* eslint-disable no-shadow */ + // $FlowExpectedError[incompatible-type] + function HTMLCollection() { + throw new TypeError( + "Failed to construct 'HTMLCollection': Illegal constructor", + ); + }; + +// $FlowExpectedError[prop-missing] +HTMLCollection_public.prototype = HTMLCollection.prototype; diff --git a/packages/react-native/src/private/webapis/dom/oldstylecollections/HTMLCollection.js.flow b/packages/react-native/src/private/webapis/dom/oldstylecollections/HTMLCollection.js.flow index b024dfc9c68a..eacb632c8523 100644 --- a/packages/react-native/src/private/webapis/dom/oldstylecollections/HTMLCollection.js.flow +++ b/packages/react-native/src/private/webapis/dom/oldstylecollections/HTMLCollection.js.flow @@ -27,3 +27,5 @@ declare export default class HTMLCollection<+T> declare export function createHTMLCollection( elements: ReadonlyArray, ): HTMLCollection; + +declare export var HTMLCollection_public: typeof HTMLCollection; diff --git a/packages/react-native/src/private/webapis/dom/oldstylecollections/NodeList.js b/packages/react-native/src/private/webapis/dom/oldstylecollections/NodeList.js index 4d19d1659ce8..dc9cc4664534 100644 --- a/packages/react-native/src/private/webapis/dom/oldstylecollections/NodeList.js +++ b/packages/react-native/src/private/webapis/dom/oldstylecollections/NodeList.js @@ -108,3 +108,13 @@ setPlatformObject(NodeList); export function createNodeList(elements: ReadonlyArray): NodeList { return new NodeList(elements); } + +export const NodeList_public: typeof NodeList = + /* eslint-disable no-shadow */ + // $FlowExpectedError[incompatible-type] + function NodeList() { + throw new TypeError("Failed to construct 'NodeList': Illegal constructor"); + }; + +// $FlowExpectedError[prop-missing] +NodeList_public.prototype = NodeList.prototype; diff --git a/packages/react-native/src/private/webapis/dom/oldstylecollections/NodeList.js.flow b/packages/react-native/src/private/webapis/dom/oldstylecollections/NodeList.js.flow index ee11d7060e90..e268ade1bd04 100644 --- a/packages/react-native/src/private/webapis/dom/oldstylecollections/NodeList.js.flow +++ b/packages/react-native/src/private/webapis/dom/oldstylecollections/NodeList.js.flow @@ -32,3 +32,5 @@ declare export default class NodeList<+T> implements Iterable, ArrayLike { declare export function createNodeList( elements: ReadonlyArray, ): NodeList; + +declare export var NodeList_public: typeof NodeList; diff --git a/packages/react-native/src/private/webapis/dom/oldstylecollections/__tests__/HTMLCollection-test.js b/packages/react-native/src/private/webapis/dom/oldstylecollections/__tests__/HTMLCollection-itest.js similarity index 78% rename from packages/react-native/src/private/webapis/dom/oldstylecollections/__tests__/HTMLCollection-test.js rename to packages/react-native/src/private/webapis/dom/oldstylecollections/__tests__/HTMLCollection-itest.js index cc281e40a521..63fceb3374fd 100644 --- a/packages/react-native/src/private/webapis/dom/oldstylecollections/__tests__/HTMLCollection-test.js +++ b/packages/react-native/src/private/webapis/dom/oldstylecollections/__tests__/HTMLCollection-itest.js @@ -8,6 +8,8 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + import {createHTMLCollection} from '../HTMLCollection'; describe('HTMLCollection', () => { @@ -37,15 +39,23 @@ describe('HTMLCollection', () => { const collection = createHTMLCollection(['a', 'b', 'c']); - expect(() => { + let writeIndexError; + try { collection[0] = 'replacement'; - }).toThrow(TypeError); + } catch (e) { + writeIndexError = e; + } + expect(writeIndexError).toBeInstanceOf(TypeError); expect(collection[0]).toBe('a'); - expect(() => { + let writeLengthError; + try { // $FlowExpectedError[cannot-write] collection.length = 100; - }).toThrow(TypeError); + } catch (e) { + writeLengthError = e; + } + expect(writeLengthError).toBeInstanceOf(TypeError); expect(collection.length).toBe(3); }); @@ -76,4 +86,12 @@ describe('HTMLCollection', () => { expect(collection.item(3)).toBe(null); }); }); + + describe('global constructor', () => { + it('throws when called', () => { + expect(() => new HTMLCollection()).toThrow( + "Failed to construct 'HTMLCollection': Illegal constructor", + ); + }); + }); }); diff --git a/packages/react-native/src/private/webapis/dom/oldstylecollections/__tests__/NodeList-test.js b/packages/react-native/src/private/webapis/dom/oldstylecollections/__tests__/NodeList-itest.js similarity index 85% rename from packages/react-native/src/private/webapis/dom/oldstylecollections/__tests__/NodeList-test.js rename to packages/react-native/src/private/webapis/dom/oldstylecollections/__tests__/NodeList-itest.js index 377ee8c3777e..fc7710f028a6 100644 --- a/packages/react-native/src/private/webapis/dom/oldstylecollections/__tests__/NodeList-test.js +++ b/packages/react-native/src/private/webapis/dom/oldstylecollections/__tests__/NodeList-itest.js @@ -8,6 +8,8 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + import {createNodeList} from '../NodeList'; describe('NodeList', () => { @@ -46,15 +48,23 @@ describe('NodeList', () => { const collection = createNodeList(['a', 'b', 'c']); - expect(() => { + let writeIndexError; + try { collection[0] = 'replacement'; - }).toThrow(TypeError); + } catch (e) { + writeIndexError = e; + } + expect(writeIndexError).toBeInstanceOf(TypeError); expect(collection[0]).toBe('a'); - expect(() => { + let writeLengthError; + try { // $FlowExpectedError[cannot-write] collection.length = 100; - }).toThrow(TypeError); + } catch (e) { + writeLengthError = e; + } + expect(writeLengthError).toBeInstanceOf(TypeError); expect(collection.length).toBe(3); }); @@ -83,7 +93,7 @@ describe('NodeList', () => { expect(keys.next()).toEqual({value: 0, done: false}); expect(keys.next()).toEqual({value: 1, done: false}); expect(keys.next()).toEqual({value: 2, done: false}); - expect(keys.next()).toEqual({done: true}); + expect(keys.next()).toEqual({value: undefined, done: true}); let i = 0; for (const key of collection.keys()) { @@ -101,7 +111,7 @@ describe('NodeList', () => { expect(values.next()).toEqual({value: 'a', done: false}); expect(values.next()).toEqual({value: 'b', done: false}); expect(values.next()).toEqual({value: 'c', done: false}); - expect(values.next()).toEqual({done: true}); + expect(values.next()).toEqual({value: undefined, done: true}); let i = 0; for (const value of collection.values()) { @@ -119,7 +129,7 @@ describe('NodeList', () => { expect(entries.next()).toEqual({value: [0, 'a'], done: false}); expect(entries.next()).toEqual({value: [1, 'b'], done: false}); expect(entries.next()).toEqual({value: [2, 'c'], done: false}); - expect(entries.next()).toEqual({done: true}); + expect(entries.next()).toEqual({value: undefined, done: true}); let i = 0; for (const entry of collection.entries()) { @@ -157,4 +167,12 @@ describe('NodeList', () => { }, explicitThis); }); }); + + describe('global constructor', () => { + it('throws when called', () => { + expect(() => new NodeList()).toThrow( + "Failed to construct 'NodeList': Illegal constructor", + ); + }); + }); }); diff --git a/packages/react-native/src/private/webapis/geometry/DOMRectList.js b/packages/react-native/src/private/webapis/geometry/DOMRectList.js index e56ba2f053a3..b0ca8dbee689 100644 --- a/packages/react-native/src/private/webapis/geometry/DOMRectList.js +++ b/packages/react-native/src/private/webapis/geometry/DOMRectList.js @@ -77,3 +77,15 @@ export function createDOMRectList( ): DOMRectList { return new DOMRectList(elements); } + +export const DOMRectList_public: typeof DOMRectList = + /* eslint-disable no-shadow */ + // $FlowExpectedError[incompatible-type] + function DOMRectList() { + throw new TypeError( + "Failed to construct 'DOMRectList': Illegal constructor", + ); + }; + +// $FlowExpectedError[prop-missing] +DOMRectList_public.prototype = DOMRectList.prototype; diff --git a/packages/react-native/src/private/webapis/geometry/DOMRectList.js.flow b/packages/react-native/src/private/webapis/geometry/DOMRectList.js.flow index 1e31dc9093c8..7a291e0e4908 100644 --- a/packages/react-native/src/private/webapis/geometry/DOMRectList.js.flow +++ b/packages/react-native/src/private/webapis/geometry/DOMRectList.js.flow @@ -25,3 +25,5 @@ declare export default class DOMRectList declare export function createDOMRectList( domRects: ReadonlyArray, ): DOMRectList; + +declare export var DOMRectList_public: typeof DOMRectList; diff --git a/packages/react-native/src/private/webapis/geometry/__tests__/DOMRectList-test.js b/packages/react-native/src/private/webapis/geometry/__tests__/DOMRectList-itest.js similarity index 80% rename from packages/react-native/src/private/webapis/geometry/__tests__/DOMRectList-test.js rename to packages/react-native/src/private/webapis/geometry/__tests__/DOMRectList-itest.js index a7c999f01ae9..f1a2932ac225 100644 --- a/packages/react-native/src/private/webapis/geometry/__tests__/DOMRectList-test.js +++ b/packages/react-native/src/private/webapis/geometry/__tests__/DOMRectList-itest.js @@ -8,6 +8,8 @@ * @format */ +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + import {createDOMRectList} from '../DOMRectList'; import DOMRectReadOnly from '../DOMRectReadOnly'; @@ -42,15 +44,23 @@ describe('DOMRectList', () => { const collection = createDOMRectList([domRectA, domRectB, domRectC]); - expect(() => { + let writeIndexError; + try { collection[0] = new DOMRectReadOnly(); - }).toThrow(TypeError); + } catch (e) { + writeIndexError = e; + } + expect(writeIndexError).toBeInstanceOf(TypeError); expect(collection[0]).toBe(domRectA); - expect(() => { + let writeLengthError; + try { // $FlowExpectedError[cannot-write] collection.length = 100; - }).toThrow(TypeError); + } catch (e) { + writeLengthError = e; + } + expect(writeLengthError).toBeInstanceOf(TypeError); expect(collection.length).toBe(3); }); @@ -81,4 +91,12 @@ describe('DOMRectList', () => { expect(collection.item(3)).toBe(null); }); }); + + describe('global constructor', () => { + it('throws when called', () => { + expect(() => new DOMRectList()).toThrow( + "Failed to construct 'DOMRectList': Illegal constructor", + ); + }); + }); }); diff --git a/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserverEntry.js b/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserverEntry.js index baf77a2d3035..2b7bfacfeb32 100644 --- a/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserverEntry.js +++ b/packages/react-native/src/private/webapis/intersectionobserver/IntersectionObserverEntry.js @@ -168,3 +168,16 @@ export function createIntersectionObserverEntry( ): IntersectionObserverEntry { return new IntersectionObserverEntry(entry, target); } + +export const IntersectionObserverEntry_public: typeof IntersectionObserverEntry = + /* eslint-disable no-shadow */ + // $FlowExpectedError[incompatible-type] + function IntersectionObserverEntry() { + throw new TypeError( + "Failed to construct 'IntersectionObserverEntry': Illegal constructor", + ); + }; + +// $FlowExpectedError[prop-missing] +IntersectionObserverEntry_public.prototype = + IntersectionObserverEntry.prototype; diff --git a/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js b/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js index 9799c589affc..7440f670a96a 100644 --- a/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js +++ b/packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js @@ -12,6 +12,7 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import type {HostInstance} from 'react-native'; import type IntersectionObserverType from 'react-native/src/private/webapis/intersectionobserver/IntersectionObserver'; +import type IntersectionObserverEntryType from 'react-native/src/private/webapis/intersectionobserver/IntersectionObserverEntry'; import ensureInstance from '../../../__tests__/utilities/ensureInstance'; import {createShadowNodeReferenceCountingRef} from '../../../__tests__/utilities/ShadowNodeReferenceCounter'; @@ -22,9 +23,9 @@ import {ScrollView, View} from 'react-native'; import setUpIntersectionObserver from 'react-native/src/private/setup/setUpIntersectionObserver'; import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; import DOMRectReadOnly from 'react-native/src/private/webapis/geometry/DOMRectReadOnly'; -import IntersectionObserverEntry from 'react-native/src/private/webapis/intersectionobserver/IntersectionObserverEntry'; declare const IntersectionObserver: Class; +declare const IntersectionObserverEntry: Class; setUpIntersectionObserver(); @@ -4277,4 +4278,18 @@ describe('IntersectionObserver', () => { }); }); }); + + describe('IntersectionObserverEntry global constructor', () => { + it('throws when called', () => { + expect( + () => + // The public stub throws regardless of arguments; the real class + // requires two so Flow needs a suppression here. + // $FlowExpectedError[incompatible-type] + new IntersectionObserverEntry(), + ).toThrow( + "Failed to construct 'IntersectionObserverEntry': Illegal constructor", + ); + }); + }); }); diff --git a/packages/react-native/src/private/webapis/mutationobserver/MutationRecord.js b/packages/react-native/src/private/webapis/mutationobserver/MutationRecord.js index 01da2b1fce9b..d440c95c7177 100644 --- a/packages/react-native/src/private/webapis/mutationobserver/MutationRecord.js +++ b/packages/react-native/src/private/webapis/mutationobserver/MutationRecord.js @@ -83,3 +83,15 @@ export function createMutationRecord( ): MutationRecord { return new MutationRecord(entry); } + +export const MutationRecord_public: typeof MutationRecord = + /* eslint-disable no-shadow */ + // $FlowExpectedError[incompatible-type] + function MutationRecord() { + throw new TypeError( + "Failed to construct 'MutationRecord': Illegal constructor", + ); + }; + +// $FlowExpectedError[prop-missing] +MutationRecord_public.prototype = MutationRecord.prototype; diff --git a/packages/react-native/src/private/webapis/mutationobserver/__tests__/MutationObserver-itest.js b/packages/react-native/src/private/webapis/mutationobserver/__tests__/MutationObserver-itest.js index 5cd58243420b..718f03c374cf 100644 --- a/packages/react-native/src/private/webapis/mutationobserver/__tests__/MutationObserver-itest.js +++ b/packages/react-native/src/private/webapis/mutationobserver/__tests__/MutationObserver-itest.js @@ -928,4 +928,16 @@ describe('MutationObserver', () => { }).not.toThrow(); }); }); + + describe('MutationRecord global constructor', () => { + it('throws when called', () => { + expect( + () => + // The public stub throws regardless of arguments; the real class + // requires one so Flow needs a suppression here. + // $FlowExpectedError[incompatible-type] + new MutationRecord(), + ).toThrow("Failed to construct 'MutationRecord': Illegal constructor"); + }); + }); });