Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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');
});
});
}
});
Original file line number Diff line number Diff line change
@@ -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');
});
});
}
});
28 changes: 19 additions & 9 deletions packages/react-native/src/private/setup/setUpDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,11 @@ export default function setUpIntersectionObserver() {
() =>
require('../webapis/intersectionobserver/IntersectionObserver').default,
);

polyfillGlobal(
'IntersectionObserverEntry',
() =>
require('../webapis/intersectionobserver/IntersectionObserverEntry')
.IntersectionObserverEntry_public,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export default function setUpMutationObserver() {

polyfillGlobal(
'MutationRecord',
() => require('../webapis/mutationobserver/MutationRecord').default,
() =>
require('../webapis/mutationobserver/MutationRecord')
.MutationRecord_public,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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",
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<HostInstance>();

const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(<NativeText ref={parentNodeRef}>Some text</NativeText>);
});

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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,15 @@ export function createHTMLCollection<T>(
): HTMLCollection<T> {
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;
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ declare export default class HTMLCollection<+T>
declare export function createHTMLCollection<T>(
elements: ReadonlyArray<T>,
): HTMLCollection<T>;

declare export var HTMLCollection_public: typeof HTMLCollection;
Loading
Loading