diff --git a/packages/react-dom/src/__tests__/ReactDOMViewTransitionClass-test.js b/packages/react-dom/src/__tests__/ReactDOMViewTransitionClass-test.js
new file mode 100644
index 000000000000..236cf84c1d7e
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactDOMViewTransitionClass-test.js
@@ -0,0 +1,529 @@
+/**
+ * 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.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+let React;
+let ReactDOMClient;
+let ViewTransition;
+let act;
+let startTransition;
+let addTransitionType;
+let container;
+
+describe('ReactDOMViewTransitionClass', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ React = require('react');
+ ReactDOMClient = require('react-dom/client');
+ act = require('internal-test-utils').act;
+ ViewTransition = React.ViewTransition;
+ startTransition = React.startTransition;
+ addTransitionType = React.addTransitionType;
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ // Mock document.startViewTransition
+ if (!document.startViewTransition) {
+ document.startViewTransition = function ({update}) {
+ update();
+ return {
+ ready: Promise.resolve(),
+ finished: Promise.resolve(),
+ skipTransition() {},
+ };
+ };
+ }
+
+ // Mock CSS.escape
+ if (typeof CSS === 'undefined') {
+ global.CSS = {escape: str => str};
+ } else if (!CSS.escape) {
+ CSS.escape = str => str;
+ }
+
+ // Mock document.fonts
+ if (!document.fonts) {
+ Object.defineProperty(document, 'fonts', {
+ value: {status: 'loaded', ready: Promise.resolve()},
+ configurable: true,
+ });
+ }
+
+ // Mock Element.prototype.getAnimations
+ if (!Element.prototype.getAnimations) {
+ Element.prototype.getAnimations = function () {
+ return [];
+ };
+ }
+
+ // Mock Element.prototype.animate
+ if (!Element.prototype.animate) {
+ Element.prototype.animate = function () {
+ return {cancel() {}, finished: Promise.resolve()};
+ };
+ }
+
+ // Mock getBoundingClientRect
+ Element.prototype._originalGetBoundingClientRect =
+ Element.prototype.getBoundingClientRect;
+ Element.prototype.getBoundingClientRect = function () {
+ const text = this.textContent || '';
+ return new DOMRect(0, 0, text.length * 10 + 10, 20);
+ };
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ if (Element.prototype._originalGetBoundingClientRect) {
+ Element.prototype.getBoundingClientRect =
+ Element.prototype._originalGetBoundingClientRect;
+ delete Element.prototype._originalGetBoundingClientRect;
+ }
+ });
+
+ // @gate enableViewTransition
+ it('accepts a string as the default class', async () => {
+ function App() {
+ return (
+
+ Hello
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child')).not.toBe(null);
+ });
+
+ // @gate enableViewTransition
+ it('accepts a string as the update class', async () => {
+ function App({text}) {
+ return (
+
+ {text}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child').textContent).toBe('Second');
+ });
+
+ // @gate enableViewTransition
+ it('accepts a string as the enter class', async () => {
+ function App({show}) {
+ if (!show) return null;
+ return (
+
+ Entered
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child').textContent).toBe('Entered');
+ });
+
+ // @gate enableViewTransition
+ it('accepts a string as the exit class', async () => {
+ function App({show}) {
+ if (!show) return null;
+ return (
+
+ Will Exit
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child').textContent).toBe('Will Exit');
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child')).toBe(null);
+ });
+
+ // @gate enableViewTransition
+ it('accepts a string as the share class', async () => {
+ function App({page}) {
+ if (page === 'a') {
+ return (
+
+ Page A
+
+ );
+ }
+ return (
+
+ Page B
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#page-a')).not.toBe(null);
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#page-b')).not.toBe(null);
+ });
+
+ // @gate enableViewTransition
+ it('accepts an object with default key as the class', async () => {
+ function App() {
+ return (
+
+ Object class
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child')).not.toBe(null);
+ });
+
+ // @gate enableViewTransition
+ it('accepts an object with transition type keys', async () => {
+ function App({text}) {
+ return (
+
+ {text}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ await act(() => {
+ startTransition(() => {
+ addTransitionType('nav-forward');
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child').textContent).toBe('Second');
+ });
+
+ // @gate enableViewTransition
+ it('handles "none" class which suppresses the transition', async () => {
+ function App({text}) {
+ return (
+
+ {text}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child').textContent).toBe('Second');
+ });
+
+ // @gate enableViewTransition
+ it('handles "none" in object class which takes precedence', async () => {
+ function App({text}) {
+ return (
+
+ {text}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ // When nav-forward type is active, "none" should take precedence
+ await act(() => {
+ startTransition(() => {
+ addTransitionType('nav-forward');
+ addTransitionType('nav-back');
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child').textContent).toBe('Second');
+ });
+
+ // @gate enableViewTransition
+ it('handles "auto" class which means use default behavior', async () => {
+ function App({text}) {
+ return (
+
+ {text}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child').textContent).toBe('Second');
+ });
+
+ // @gate enableViewTransition
+ it('combines multiple matching transition type classes', async () => {
+ function App({text}) {
+ return (
+
+ {text}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ // When multiple types match, their classes should be combined
+ await act(() => {
+ startTransition(() => {
+ addTransitionType('highlight');
+ addTransitionType('important');
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child').textContent).toBe('Second');
+ });
+
+ // @gate enableViewTransition
+ it('falls back to default when no transition types match', async () => {
+ function App({text}) {
+ return (
+
+ {text}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ // No transition type added, should fall back to default
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child').textContent).toBe('Second');
+ });
+
+ // @gate enableViewTransition
+ it('event class overrides default class', async () => {
+ function App({show}) {
+ if (!show) return null;
+ return (
+
+ Content
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child').textContent).toBe('Content');
+ });
+
+ // @gate enableViewTransition
+ it('event class "auto" nullifies the class', async () => {
+ function App({show}) {
+ if (!show) return null;
+ return (
+
+ Content
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#child').textContent).toBe('Content');
+ });
+
+ // @gate enableViewTransition
+ it('supports different classes for enter, exit, update, and share', async () => {
+ function PageA() {
+ return (
+
+ Page A Content
+
+ );
+ }
+
+ function PageB() {
+ return (
+
+ Page B Content
+
+ );
+ }
+
+ function App({page}) {
+ return page === 'a' ? : ;
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#page-a')).not.toBe(null);
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#page-b')).not.toBe(null);
+ expect(container.querySelector('#page-a')).toBe(null);
+ });
+});
diff --git a/packages/react-dom/src/__tests__/ReactDOMViewTransitionName-test.js b/packages/react-dom/src/__tests__/ReactDOMViewTransitionName-test.js
new file mode 100644
index 000000000000..7bba8e849bef
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactDOMViewTransitionName-test.js
@@ -0,0 +1,320 @@
+/**
+ * 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.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+let React;
+let ReactDOMClient;
+let ViewTransition;
+let act;
+let startTransition;
+let addTransitionType;
+let container;
+
+describe('ReactDOMViewTransitionName', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ React = require('react');
+ ReactDOMClient = require('react-dom/client');
+ act = require('internal-test-utils').act;
+ ViewTransition = React.ViewTransition;
+ startTransition = React.startTransition;
+ addTransitionType = React.addTransitionType;
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ // Mock document.startViewTransition
+ if (!document.startViewTransition) {
+ document.startViewTransition = function ({update}) {
+ update();
+ return {
+ ready: Promise.resolve(),
+ finished: Promise.resolve(),
+ skipTransition() {},
+ };
+ };
+ }
+
+ // Mock CSS.escape
+ if (typeof CSS === 'undefined') {
+ global.CSS = {escape: str => str};
+ } else if (!CSS.escape) {
+ CSS.escape = str => str;
+ }
+
+ // Mock document.fonts
+ if (!document.fonts) {
+ Object.defineProperty(document, 'fonts', {
+ value: {status: 'loaded', ready: Promise.resolve()},
+ configurable: true,
+ });
+ }
+
+ // Mock Element.prototype.getAnimations
+ if (!Element.prototype.getAnimations) {
+ Element.prototype.getAnimations = function () {
+ return [];
+ };
+ }
+
+ // Mock Element.prototype.animate
+ if (!Element.prototype.animate) {
+ Element.prototype.animate = function () {
+ return {cancel() {}, finished: Promise.resolve()};
+ };
+ }
+
+ // Mock getBoundingClientRect
+ Element.prototype._originalGetBoundingClientRect =
+ Element.prototype.getBoundingClientRect;
+ Element.prototype.getBoundingClientRect = function () {
+ const text = this.textContent || '';
+ return new DOMRect(0, 0, text.length * 10 + 10, 20);
+ };
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ if (Element.prototype._originalGetBoundingClientRect) {
+ Element.prototype.getBoundingClientRect =
+ Element.prototype._originalGetBoundingClientRect;
+ delete Element.prototype._originalGetBoundingClientRect;
+ }
+ });
+
+ // @gate enableViewTransition
+ it('renders children without a wrapper element', async () => {
+ function App() {
+ return (
+
+ Hello
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ expect(container.querySelector('#child')).not.toBe(null);
+ expect(container.querySelector('#child').textContent).toBe('Hello');
+ // ViewTransition should not add a wrapper element
+ expect(container.firstChild.id).toBe('child');
+ });
+
+ // @gate enableViewTransition
+ it('uses explicit name prop when provided', async () => {
+ function App() {
+ return (
+
+ Named
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ // The element should be rendered
+ expect(container.querySelector('#child')).not.toBe(null);
+ });
+
+ // @gate enableViewTransition
+ it('generates auto name when name is not provided', async () => {
+ function App() {
+ return (
+
+ Auto
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#auto-named')).not.toBe(null);
+ });
+
+ // @gate enableViewTransition
+ it('generates auto name when name is "auto"', async () => {
+ function App() {
+ return (
+
+ Auto Explicit
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#auto-explicit')).not.toBe(null);
+ });
+
+ // @gate enableViewTransition
+ it('preserves auto name across re-renders', async () => {
+ function App({text}) {
+ return (
+
+ {text}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#stable').textContent).toBe('First');
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#stable').textContent).toBe('Second');
+ });
+
+ // @gate enableViewTransition
+ it('supports multiple ViewTransitions as siblings', async () => {
+ function App() {
+ return (
+
+
+ First
+
+
+ Second
+
+
+ Third
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#first').textContent).toBe('First');
+ expect(container.querySelector('#second').textContent).toBe('Second');
+ expect(container.querySelector('#third').textContent).toBe('Third');
+ });
+
+ // @gate enableViewTransition
+ it('supports nested ViewTransitions', async () => {
+ function App() {
+ return (
+
+
+
+ Nested
+
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#outer')).not.toBe(null);
+ expect(container.querySelector('#inner').textContent).toBe('Nested');
+ });
+
+ // @gate enableViewTransition
+ it('handles conditional rendering inside ViewTransition', async () => {
+ function App({show}) {
+ return (
+
+ {show ? Visible : null}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ expect(container.querySelector('#wrapper').textContent).toBe('');
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#wrapper').textContent).toBe('Visible');
+ });
+
+ // @gate enableViewTransition
+ it('handles ViewTransition mounting and unmounting', async () => {
+ function App({show}) {
+ if (!show) {
+ return Empty
;
+ }
+ return (
+
+ Content
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ expect(container.querySelector('#empty')).not.toBe(null);
+ expect(container.querySelector('#content')).toBe(null);
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#content')).not.toBe(null);
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#content')).toBe(null);
+ expect(container.querySelector('#empty')).not.toBe(null);
+ });
+});
diff --git a/packages/react-dom/src/__tests__/ReactDOMViewTransitionWarnings-test.js b/packages/react-dom/src/__tests__/ReactDOMViewTransitionWarnings-test.js
new file mode 100644
index 000000000000..f79f35e31737
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactDOMViewTransitionWarnings-test.js
@@ -0,0 +1,799 @@
+/**
+ * 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.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+let React;
+let ReactDOMClient;
+let ViewTransition;
+let Suspense;
+let act;
+let startTransition;
+let addTransitionType;
+let container;
+
+describe('ReactDOMViewTransitionWarnings', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ React = require('react');
+ ReactDOMClient = require('react-dom/client');
+ act = require('internal-test-utils').act;
+ ViewTransition = React.ViewTransition;
+ Suspense = React.Suspense;
+ startTransition = React.startTransition;
+ addTransitionType = React.addTransitionType;
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ // Mock document.startViewTransition
+ if (!document.startViewTransition) {
+ document.startViewTransition = function ({update}) {
+ update();
+ return {
+ ready: Promise.resolve(),
+ finished: Promise.resolve(),
+ skipTransition() {},
+ };
+ };
+ }
+
+ // Mock CSS.escape
+ if (typeof CSS === 'undefined') {
+ global.CSS = {escape: str => str};
+ } else if (!CSS.escape) {
+ CSS.escape = str => str;
+ }
+
+ // Mock document.fonts
+ if (!document.fonts) {
+ Object.defineProperty(document, 'fonts', {
+ value: {status: 'loaded', ready: Promise.resolve()},
+ configurable: true,
+ });
+ }
+
+ // Mock Element.prototype.getAnimations
+ if (!Element.prototype.getAnimations) {
+ Element.prototype.getAnimations = function () {
+ return [];
+ };
+ }
+
+ // Mock Element.prototype.animate
+ if (!Element.prototype.animate) {
+ Element.prototype.animate = function () {
+ return {cancel() {}, finished: Promise.resolve()};
+ };
+ }
+
+ // Mock getBoundingClientRect
+ Element.prototype._originalGetBoundingClientRect =
+ Element.prototype.getBoundingClientRect;
+ Element.prototype.getBoundingClientRect = function () {
+ const text = this.textContent || '';
+ return new DOMRect(0, 0, text.length * 10 + 10, 20);
+ };
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ if (Element.prototype._originalGetBoundingClientRect) {
+ Element.prototype.getBoundingClientRect =
+ Element.prototype._originalGetBoundingClientRect;
+ delete Element.prototype._originalGetBoundingClientRect;
+ }
+ });
+
+ // @gate enableViewTransition && __DEV__
+ it('warns when two ViewTransitions have the same name', async () => {
+ function App() {
+ return (
+
+
+ First
+
+
+ Second
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await expect(async () => {
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+ }).toErrorDev([
+ 'There are two components with the same name',
+ 'The existing duplicate has this stack trace',
+ ]);
+ });
+
+ // @gate enableViewTransition && __DEV__
+ it('does not warn for "auto" name duplicates', async () => {
+ function App() {
+ return (
+
+
+ First
+
+
+ Second
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ // Should not produce any warnings
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#first')).not.toBe(null);
+ expect(container.querySelector('#second')).not.toBe(null);
+ });
+
+ // @gate enableViewTransition && __DEV__
+ it('does not warn for unnamed ViewTransitions', async () => {
+ function App() {
+ return (
+
+
+ First
+
+
+ Second
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ // Should not produce any warnings
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#first')).not.toBe(null);
+ expect(container.querySelector('#second')).not.toBe(null);
+ });
+
+ // @gate enableViewTransition && __DEV__
+ it('does not warn when duplicate is unmounted before new one mounts', async () => {
+ function App({page}) {
+ if (page === 'a') {
+ return (
+
+ Page A
+
+ );
+ }
+ return (
+
+ Page B
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#page-a')).not.toBe(null);
+
+ // Switching pages should not warn because old one unmounts
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#page-b')).not.toBe(null);
+ });
+
+ // @gate enableViewTransition && __DEV__
+ it('only warns once per duplicate name', async () => {
+ function App({count}) {
+ const items = [];
+ for (let i = 0; i < count; i++) {
+ items.push(
+
+ {i}
+ ,
+ );
+ }
+ return {items}
;
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await expect(async () => {
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+ }).toErrorDev([
+ 'There are two components with the same name',
+ 'The existing duplicate has this stack trace',
+ ]);
+
+ // Adding more duplicates should not warn again
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+ });
+
+ // @gate enableViewTransition
+ it('supports different unique names without warnings', async () => {
+ function App() {
+ return (
+
+
+
+
+
+ Main
+
+
+
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#header').textContent).toBe('Header');
+ expect(container.querySelector('#main').textContent).toBe('Main');
+ expect(container.querySelector('#footer').textContent).toBe('Footer');
+ });
+});
+
+describe('ReactDOMViewTransitionEdgeCases', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ React = require('react');
+ ReactDOMClient = require('react-dom/client');
+ act = require('internal-test-utils').act;
+ ViewTransition = React.ViewTransition;
+ Suspense = React.Suspense;
+ startTransition = React.startTransition;
+ addTransitionType = React.addTransitionType;
+ container = document.createElement('div');
+ document.body.appendChild(container);
+
+ // Mock document.startViewTransition
+ if (!document.startViewTransition) {
+ document.startViewTransition = function ({update}) {
+ update();
+ return {
+ ready: Promise.resolve(),
+ finished: Promise.resolve(),
+ skipTransition() {},
+ };
+ };
+ }
+
+ // Mock CSS.escape
+ if (typeof CSS === 'undefined') {
+ global.CSS = {escape: str => str};
+ } else if (!CSS.escape) {
+ CSS.escape = str => str;
+ }
+
+ // Mock document.fonts
+ if (!document.fonts) {
+ Object.defineProperty(document, 'fonts', {
+ value: {status: 'loaded', ready: Promise.resolve()},
+ configurable: true,
+ });
+ }
+
+ // Mock Element.prototype.getAnimations
+ if (!Element.prototype.getAnimations) {
+ Element.prototype.getAnimations = function () {
+ return [];
+ };
+ }
+
+ // Mock Element.prototype.animate
+ if (!Element.prototype.animate) {
+ Element.prototype.animate = function () {
+ return {cancel() {}, finished: Promise.resolve()};
+ };
+ }
+
+ // Mock getBoundingClientRect
+ Element.prototype._originalGetBoundingClientRect =
+ Element.prototype.getBoundingClientRect;
+ Element.prototype.getBoundingClientRect = function () {
+ const text = this.textContent || '';
+ return new DOMRect(0, 0, text.length * 10 + 10, 20);
+ };
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ if (Element.prototype._originalGetBoundingClientRect) {
+ Element.prototype.getBoundingClientRect =
+ Element.prototype._originalGetBoundingClientRect;
+ delete Element.prototype._originalGetBoundingClientRect;
+ }
+ });
+
+ // @gate enableViewTransition
+ it('handles rapid mount/unmount cycles', async () => {
+ function App({show}) {
+ if (!show) return null;
+ return (
+
+ Rapid
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+
+ // Rapid mount/unmount
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#rapid')).not.toBe(null);
+ });
+
+ // @gate enableViewTransition
+ it('handles ViewTransition with null children', async () => {
+ function App() {
+ return (
+
+ {null}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ expect(container.querySelector('#wrapper')).not.toBe(null);
+ expect(container.querySelector('#wrapper').textContent).toBe('');
+ });
+
+ // @gate enableViewTransition
+ it('handles ViewTransition with text children', async () => {
+ function App() {
+ return (
+
+ Just text content
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ expect(container.querySelector('#text-child').textContent).toBe(
+ 'Just text content',
+ );
+ });
+
+ // @gate enableViewTransition
+ it('handles ViewTransition with multiple DOM children', async () => {
+ function App() {
+ return (
+
+
+ One
+ Two
+ Three
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ const parent = container.querySelector('#multi-parent');
+ expect(parent.children.length).toBe(3);
+ expect(parent.children[0].textContent).toBe('One');
+ expect(parent.children[1].textContent).toBe('Two');
+ expect(parent.children[2].textContent).toBe('Three');
+ });
+
+ // @gate enableViewTransition
+ it('handles ViewTransition inside a list', async () => {
+ function App({items}) {
+ return (
+
+ {items.map(item => (
+
+ - {item.text}
+
+ ))}
+
+ );
+ }
+
+ const items = [
+ {id: 1, text: 'Apple'},
+ {id: 2, text: 'Banana'},
+ {id: 3, text: 'Cherry'},
+ ];
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#item-1').textContent).toBe('Apple');
+ expect(container.querySelector('#item-2').textContent).toBe('Banana');
+ expect(container.querySelector('#item-3').textContent).toBe('Cherry');
+
+ // Reorder items
+ const reordered = [items[2], items[0], items[1]];
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ const listItems = container.querySelectorAll('li');
+ expect(listItems[0].textContent).toBe('Cherry');
+ expect(listItems[1].textContent).toBe('Apple');
+ expect(listItems[2].textContent).toBe('Banana');
+ });
+
+ // @gate enableViewTransition
+ it('handles adding items to a list with ViewTransitions', async () => {
+ function App({items}) {
+ return (
+
+ {items.map(item => (
+
+ - {item}
+
+ ))}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelectorAll('li').length).toBe(2);
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelectorAll('li').length).toBe(4);
+ expect(container.querySelector('#list-c').textContent).toBe('c');
+ expect(container.querySelector('#list-d').textContent).toBe('d');
+ });
+
+ // @gate enableViewTransition
+ it('handles removing items from a list with ViewTransitions', async () => {
+ function App({items}) {
+ return (
+
+ {items.map(item => (
+
+ - {item}
+
+ ))}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelectorAll('li').length).toBe(3);
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelectorAll('li').length).toBe(1);
+ expect(container.querySelector('#rm-x').textContent).toBe('x');
+ expect(container.querySelector('#rm-y')).toBe(null);
+ expect(container.querySelector('#rm-z')).toBe(null);
+ });
+
+ // @gate enableViewTransition
+ it('handles ViewTransition with Suspense fallback', async () => {
+ let resolve;
+ const promise = new Promise(r => (resolve = r));
+
+ function AsyncChild() {
+ return React.use(promise);
+ }
+
+ function App() {
+ return (
+
+ Loading...}>
+
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ // Should show fallback
+ expect(container.querySelector('#fallback')).not.toBe(null);
+
+ // Resolve the promise
+ await act(async () => {
+ resolve(Loaded!
);
+ });
+
+ expect(container.querySelector('#resolved')).not.toBe(null);
+ expect(container.querySelector('#fallback')).toBe(null);
+ });
+
+ // @gate enableViewTransition
+ it('handles ViewTransition with key change', async () => {
+ function App({id}) {
+ return (
+
+ Card {id}
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#card-1')).not.toBe(null);
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#card-2')).not.toBe(null);
+ expect(container.querySelector('#card-1')).toBe(null);
+ });
+
+ // @gate enableViewTransition
+ it('handles ViewTransition name change on same component', async () => {
+ function App({name}) {
+ return (
+
+ Named transition
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#named')).not.toBe(null);
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#named')).not.toBe(null);
+ });
+
+ // @gate enableViewTransition
+ it('handles deeply nested ViewTransitions', async () => {
+ function App() {
+ return (
+
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#l1')).not.toBe(null);
+ expect(container.querySelector('#l2')).not.toBe(null);
+ expect(container.querySelector('#l3').textContent).toBe('Deep');
+ });
+
+ // @gate enableViewTransition
+ it('handles ViewTransition with Fragment children', async () => {
+ function App() {
+ return (
+
+
+ <>
+ Fragment child 1
+ Fragment child 2
+ >
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ const parent = container.querySelector('#frag-parent');
+ expect(parent.children.length).toBe(2);
+ expect(parent.children[0].textContent).toBe('Fragment child 1');
+ expect(parent.children[1].textContent).toBe('Fragment child 2');
+ });
+
+ // @gate enableViewTransition
+ it('handles ViewTransition with component children', async () => {
+ function Child({text}) {
+ return {text};
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ expect(container.querySelector('#comp-child').textContent).toBe(
+ 'Component child',
+ );
+ });
+
+ // @gate enableViewTransition
+ it('handles ViewTransition with memo component', async () => {
+ const MemoChild = React.memo(function MemoChild({text}) {
+ return {text}
;
+ });
+
+ function App({text}) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ expect(container.querySelector('#memo-child').textContent).toBe(
+ 'Memoized',
+ );
+
+ await act(() => {
+ startTransition(() => {
+ root.render();
+ });
+ });
+
+ expect(container.querySelector('#memo-child').textContent).toBe('Updated');
+ });
+
+ // @gate enableViewTransition
+ it('handles ViewTransition with forwardRef component', async () => {
+ const RefChild = React.forwardRef(function RefChild(props, ref) {
+ return (
+
+ {props.text}
+
+ );
+ });
+
+ function App() {
+ const ref = React.useRef(null);
+ return (
+
+
+
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render();
+ });
+
+ expect(container.querySelector('#ref-child').textContent).toBe('With ref');
+ });
+});