From d96eecbc810c0a43644f1764e419b44cc92ba61b Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 13 Dec 2024 15:44:04 -0500 Subject: [PATCH 1/4] Remove enableComponentStackLocations --- packages/shared/ReactComponentStackFrame.js | 68 +++------ packages/shared/ReactFeatureFlags.js | 2 +- .../__tests__/describeComponentFrame-test.js | 137 ------------------ .../forks/ReactFeatureFlags.native-fb.js | 1 - .../forks/ReactFeatureFlags.native-oss.js | 1 - .../forks/ReactFeatureFlags.test-renderer.js | 1 - ...actFeatureFlags.test-renderer.native-fb.js | 1 - .../ReactFeatureFlags.test-renderer.www.js | 1 - .../shared/forks/ReactFeatureFlags.www.js | 2 - 9 files changed, 22 insertions(+), 192 deletions(-) delete mode 100644 packages/shared/__tests__/describeComponentFrame-test.js diff --git a/packages/shared/ReactComponentStackFrame.js b/packages/shared/ReactComponentStackFrame.js index 96f69617e607b..a276d87154d52 100644 --- a/packages/shared/ReactComponentStackFrame.js +++ b/packages/shared/ReactComponentStackFrame.js @@ -9,8 +9,6 @@ import type {LazyComponent} from 'react/src/ReactLazy'; -import {enableComponentStackLocations} from 'shared/ReactFeatureFlags'; - import { REACT_SUSPENSE_TYPE, REACT_SUSPENSE_LIST_TYPE, @@ -28,30 +26,26 @@ import DefaultPrepareStackTrace from 'shared/DefaultPrepareStackTrace'; let prefix; let suffix; export function describeBuiltInComponentFrame(name: string): string { - if (enableComponentStackLocations) { - if (prefix === undefined) { - // Extract the VM specific prefix used by each line. - try { - throw Error(); - } catch (x) { - const match = x.stack.trim().match(/\n( *(at )?)/); - prefix = (match && match[1]) || ''; - suffix = - x.stack.indexOf('\n at') > -1 - ? // V8 - ' ()' - : // JSC/Spidermonkey - x.stack.indexOf('@') > -1 - ? '@unknown:0:0' - : // Other - ''; - } + if (prefix === undefined) { + // Extract the VM specific prefix used by each line. + try { + throw Error(); + } catch (x) { + const match = x.stack.trim().match(/\n( *(at )?)/); + prefix = (match && match[1]) || ''; + suffix = + x.stack.indexOf('\n at') > -1 + ? // V8 + ' ()' + : // JSC/Spidermonkey + x.stack.indexOf('@') > -1 + ? '@unknown:0:0' + : // Other + ''; } - // We use the prefix to ensure our stacks line up with native stack frames. - return '\n' + prefix + name + suffix; - } else { - return describeComponentFrame(name); } + // We use the prefix to ensure our stacks line up with native stack frames. + return '\n' + prefix + name + suffix; } export function describeDebugInfoFrame(name: string, env: ?string): string { @@ -296,28 +290,12 @@ export function describeNativeComponentFrame( return syntheticFrame; } -function describeComponentFrame(name: null | string) { - return '\n in ' + (name || 'Unknown'); -} - export function describeClassComponentFrame(ctor: Function): string { - if (enableComponentStackLocations) { - return describeNativeComponentFrame(ctor, true); - } else { - return describeFunctionComponentFrame(ctor); - } + return describeNativeComponentFrame(ctor, true); } export function describeFunctionComponentFrame(fn: Function): string { - if (enableComponentStackLocations) { - return describeNativeComponentFrame(fn, false); - } else { - if (!fn) { - return ''; - } - const name = fn.displayName || fn.name || null; - return describeComponentFrame(name); - } + return describeNativeComponentFrame(fn, false); } function shouldConstruct(Component: Function) { @@ -334,11 +312,7 @@ export function describeUnknownElementTypeFrameInDEV(type: any): string { return ''; } if (typeof type === 'function') { - if (enableComponentStackLocations) { - return describeNativeComponentFrame(type, shouldConstruct(type)); - } else { - return describeFunctionComponentFrame(type); - } + return describeNativeComponentFrame(type, shouldConstruct(type)); } if (typeof type === 'string') { return describeBuiltInComponentFrame(type); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index ccf9310ba5805..d82b3570118d9 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -13,7 +13,7 @@ // Flags that can likely be deleted or landed without consequences // ----------------------------------------------------------------------------- -export const enableComponentStackLocations = true; +// None // ----------------------------------------------------------------------------- // Killswitch diff --git a/packages/shared/__tests__/describeComponentFrame-test.js b/packages/shared/__tests__/describeComponentFrame-test.js deleted file mode 100644 index 6fc5bef2c9945..0000000000000 --- a/packages/shared/__tests__/describeComponentFrame-test.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @emails react-core - */ - -'use strict'; - -let React; -let ReactDOMClient; -let act; -let jsxDEV; - -describe('Component stack trace displaying', () => { - beforeEach(() => { - React = require('react'); - ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; - jsxDEV = require('react/jsx-dev-runtime').jsxDEV; - }); - - // @gate !enableComponentStackLocations - // @gate __DEV__ - it('should provide filenames in stack traces', async () => { - class Component extends React.Component { - render() { - return [a, b]; - } - } - - spyOnDev(console, 'error'); - const container = document.createElement('div'); - const fileNames = { - '': '', - '/': '', - '\\': '', - Foo: 'Foo', - 'Bar/Foo': 'Foo', - 'Bar\\Foo': 'Foo', - 'Baz/Bar/Foo': 'Foo', - 'Baz\\Bar\\Foo': 'Foo', - - 'Foo.js': 'Foo.js', - 'Foo.jsx': 'Foo.jsx', - '/Foo.js': 'Foo.js', - '/Foo.jsx': 'Foo.jsx', - '\\Foo.js': 'Foo.js', - '\\Foo.jsx': 'Foo.jsx', - 'Bar/Foo.js': 'Foo.js', - 'Bar/Foo.jsx': 'Foo.jsx', - 'Bar\\Foo.js': 'Foo.js', - 'Bar\\Foo.jsx': 'Foo.jsx', - '/Bar/Foo.js': 'Foo.js', - '/Bar/Foo.jsx': 'Foo.jsx', - '\\Bar\\Foo.js': 'Foo.js', - '\\Bar\\Foo.jsx': 'Foo.jsx', - 'Bar/Baz/Foo.js': 'Foo.js', - 'Bar/Baz/Foo.jsx': 'Foo.jsx', - 'Bar\\Baz\\Foo.js': 'Foo.js', - 'Bar\\Baz\\Foo.jsx': 'Foo.jsx', - '/Bar/Baz/Foo.js': 'Foo.js', - '/Bar/Baz/Foo.jsx': 'Foo.jsx', - '\\Bar\\Baz\\Foo.js': 'Foo.js', - '\\Bar\\Baz\\Foo.jsx': 'Foo.jsx', - 'C:\\funny long (path)/Foo.js': 'Foo.js', - 'C:\\funny long (path)/Foo.jsx': 'Foo.jsx', - - 'index.js': 'index.js', - 'index.jsx': 'index.jsx', - '/index.js': 'index.js', - '/index.jsx': 'index.jsx', - '\\index.js': 'index.js', - '\\index.jsx': 'index.jsx', - 'Bar/index.js': 'Bar/index.js', - 'Bar/index.jsx': 'Bar/index.jsx', - 'Bar\\index.js': 'Bar/index.js', - 'Bar\\index.jsx': 'Bar/index.jsx', - '/Bar/index.js': 'Bar/index.js', - '/Bar/index.jsx': 'Bar/index.jsx', - '\\Bar\\index.js': 'Bar/index.js', - '\\Bar\\index.jsx': 'Bar/index.jsx', - 'Bar/Baz/index.js': 'Baz/index.js', - 'Bar/Baz/index.jsx': 'Baz/index.jsx', - 'Bar\\Baz\\index.js': 'Baz/index.js', - 'Bar\\Baz\\index.jsx': 'Baz/index.jsx', - '/Bar/Baz/index.js': 'Baz/index.js', - '/Bar/Baz/index.jsx': 'Baz/index.jsx', - '\\Bar\\Baz\\index.js': 'Baz/index.js', - '\\Bar\\Baz\\index.jsx': 'Baz/index.jsx', - 'C:\\funny long (path)/index.js': 'funny long (path)/index.js', - 'C:\\funny long (path)/index.jsx': 'funny long (path)/index.jsx', - }; - - const root = ReactDOMClient.createRoot(container); - - let i = 0; - for (const fileName in fileNames) { - Component.displayName = 'Component ' + i; - - await act(() => { - root.render( - // Intentionally inlining a manual jsxDEV() instead of relying on the - // compiler so that we can pass a custom source location. - jsxDEV( - Component, - {}, - undefined, - false, - {fileName, lineNumber: i}, - this, - ), - ); - }); - - i++; - } - if (__DEV__) { - i = 0; - expect(console.error).toHaveBeenCalledTimes( - Object.keys(fileNames).length, - ); - for (const fileName in fileNames) { - if (!fileNames.hasOwnProperty(fileName)) { - continue; - } - const args = console.error.mock.calls[i]; - const stack = args[args.length - 1]; - const expected = fileNames[fileName]; - expect(stack).toContain(`at ${expected}:`); - i++; - } - } - }); -}); diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 0d13fe48058af..f2b80a9b9f0f2 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -45,7 +45,6 @@ export const disableTextareaChildren = false; export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableCache = true; -export const enableComponentStackLocations = true; export const enableCPUSuspense = true; export const enableCreateEventHandleAPI = false; export const enableDebugTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index a3ac1e88c2a97..e5d233c61181a 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -32,7 +32,6 @@ export const disableTextareaChildren = false; export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableCache = true; -export const enableComponentStackLocations = true; export const enableCPUSuspense = false; export const enableCreateEventHandleAPI = false; export const enableDebugTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 7d3502cb1cac0..fc147e0d12329 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -40,7 +40,6 @@ export const enableUseMemoCacheHook = true; export const enableNoCloningMemoCache = false; export const enableUseEffectEventHook = false; export const favorSafetyOverHydrationPerf = true; -export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; export const enableMoveBefore = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 92034467fd72b..8cdfc474647d1 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -24,7 +24,6 @@ export const disableTextareaChildren = false; export const enableAsyncDebugInfo = false; export const enableAsyncIterableChildren = false; export const enableCache = true; -export const enableComponentStackLocations = true; export const enableCPUSuspense = true; export const enableCreateEventHandleAPI = false; export const enableDebugTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index b093ebf81b202..960c41199128c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -42,7 +42,6 @@ export const enableUseMemoCacheHook = true; export const enableNoCloningMemoCache = false; export const enableUseEffectEventHook = false; export const favorSafetyOverHydrationPerf = true; -export const enableComponentStackLocations = true; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; export const enableMoveBefore = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 8abe581e7989c..903b75e6e3577 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -99,8 +99,6 @@ export const enableSuspenseCallback = true; export const enableLegacyHidden = true; -export const enableComponentStackLocations = true; - export const disableTextareaChildren = __EXPERIMENTAL__; export const enableFizzExternalRuntime = true; From 995beaf96525e8b3db6d75c342118fe4a28d4c0c Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Fri, 13 Dec 2024 15:51:19 -0500 Subject: [PATCH 2/4] Remove enableFilterEmptyStringAttributesDOM --- .../src/client/ReactDOMComponent.js | 144 ++++++------ .../src/server/ReactFizzConfigDOM.js | 75 +++---- .../src/__tests__/ReactDOMComponent-test.js | 208 +++++++++--------- .../src/__tests__/ReactDOMFloat-test.js | 4 +- ...eactDOMServerIntegrationAttributes-test.js | 16 +- packages/shared/ReactFeatureFlags.js | 5 - .../forks/ReactFeatureFlags.native-fb.js | 1 - .../forks/ReactFeatureFlags.native-oss.js | 1 - .../forks/ReactFeatureFlags.test-renderer.js | 1 - ...actFeatureFlags.test-renderer.native-fb.js | 1 - .../ReactFeatureFlags.test-renderer.www.js | 1 - .../shared/forks/ReactFeatureFlags.www.js | 1 - 12 files changed, 209 insertions(+), 249 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index ac95c91c596a8..05892c930e1ca 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -63,10 +63,7 @@ import {validateProperties as validateInputProperties} from '../shared/ReactDOMN import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; import sanitizeURL from '../shared/sanitizeURL'; -import { - enableTrustedTypesIntegration, - enableFilterEmptyStringAttributesDOM, -} from 'shared/ReactFeatureFlags'; +import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags'; import { mediaEventTypes, listenToNonDelegatedEvent, @@ -400,35 +397,33 @@ function setProp( // fallthrough case 'src': case 'href': { - if (enableFilterEmptyStringAttributesDOM) { - if ( - value === '' && - // is fine for "reload" links. - !(tag === 'a' && key === 'href') - ) { - if (__DEV__) { - if (key === 'src') { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - key, - key, - ); - } else { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - key, - key, - ); - } + if ( + value === '' && + // is fine for "reload" links. + !(tag === 'a' && key === 'href') + ) { + if (__DEV__) { + if (key === 'src') { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + key, + key, + ); + } else { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + key, + key, + ); } - domElement.removeAttribute(key); - break; } + domElement.removeAttribute(key); + break; } if ( value == null || @@ -2489,53 +2484,52 @@ function diffHydratedGenericElement( // fallthrough case 'src': case 'href': - if (enableFilterEmptyStringAttributesDOM) { - if ( - value === '' && - // is fine for "reload" links. - !(tag === 'a' && propKey === 'href') && - !(tag === 'object' && propKey === 'data') - ) { - if (__DEV__) { - if (propKey === 'src') { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - propKey, - propKey, - ); - } else { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - propKey, - propKey, - ); - } + if ( + value === '' && + // is fine for "reload" links. + !(tag === 'a' && propKey === 'href') && + !(tag === 'object' && propKey === 'data') + ) { + if (__DEV__) { + if (propKey === 'src') { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + propKey, + propKey, + ); + } else { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + propKey, + propKey, + ); } - hydrateSanitizedAttribute( - domElement, - propKey, - propKey, - null, - extraAttributes, - serverDifferences, - ); - continue; } + hydrateSanitizedAttribute( + domElement, + propKey, + propKey, + null, + extraAttributes, + serverDifferences, + ); + continue; + } else { + hydrateSanitizedAttribute( + domElement, + propKey, + propKey, + value, + extraAttributes, + serverDifferences, + ); + continue; } - hydrateSanitizedAttribute( - domElement, - propKey, - propKey, - value, - extraAttributes, - serverDifferences, - ); - continue; case 'action': case 'formAction': { const serverValue = domElement.getAttribute(propKey); diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 1351a28db3aab..fe2e713d8a3ba 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -27,10 +27,7 @@ import { import {Children} from 'react'; -import { - enableFilterEmptyStringAttributesDOM, - enableFizzExternalRuntime, -} from 'shared/ReactFeatureFlags'; +import {enableFizzExternalRuntime} from 'shared/ReactFeatureFlags'; import type { Destination, @@ -1210,30 +1207,28 @@ function pushAttribute( } case 'src': case 'href': { - if (enableFilterEmptyStringAttributesDOM) { - if (value === '') { - if (__DEV__) { - if (name === 'src') { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - name, - name, - ); - } else { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - name, - name, - ); - } + if (value === '') { + if (__DEV__) { + if (name === 'src') { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + name, + name, + ); + } else { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + name, + name, + ); } - return; } + return; } } // Fall through to the last case which shouldn't remove empty strings. @@ -1633,19 +1628,17 @@ function pushStartObject( checkAttributeStringCoercion(propValue, 'data'); } const sanitizedValue = sanitizeURL('' + propValue); - if (enableFilterEmptyStringAttributesDOM) { - if (sanitizedValue === '') { - if (__DEV__) { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - propKey, - propKey, - ); - } - break; + if (sanitizedValue === '') { + if (__DEV__) { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + propKey, + propKey, + ); } + break; } target.push( attributeSeparator, @@ -3615,11 +3608,7 @@ export function pushStartInstance( // Fast track very common tags break; case 'a': - if (enableFilterEmptyStringAttributesDOM) { - return pushStartAnchor(target, props); - } else { - break; - } + return pushStartAnchor(target, props); case 'g': case 'p': case 'li': diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index d37a4ecba6dc6..ce71a6334ee64 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -587,133 +587,131 @@ describe('ReactDOMComponent', () => { expect(node.hasAttribute('data-foo')).toBe(false); }); - if (ReactFeatureFlags.enableFilterEmptyStringAttributesDOM) { - it('should not add an empty src attribute', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'An empty string ("") was passed to the src attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to src instead of an empty string.', - ); - const node = container.firstChild; - expect(node.hasAttribute('src')).toBe(false); - + it('should not add an empty src attribute', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { await act(() => { - root.render(); + root.render(); }); - expect(node.hasAttribute('src')).toBe(true); + }).toErrorDev( + 'An empty string ("") was passed to the src attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to src instead of an empty string.', + ); + const node = container.firstChild; + expect(node.hasAttribute('src')).toBe(false); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'An empty string ("") was passed to the src attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to src instead of an empty string.', - ); - expect(node.hasAttribute('src')).toBe(false); + await act(() => { + root.render(); }); + expect(node.hasAttribute('src')).toBe(true); - it('should not add an empty href attribute', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'An empty string ("") was passed to the href attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to href instead of an empty string.', - ); - const node = container.firstChild; - expect(node.hasAttribute('href')).toBe(false); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( + 'An empty string ("") was passed to the src attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to src instead of an empty string.', + ); + expect(node.hasAttribute('src')).toBe(false); + }); + it('should not add an empty href attribute', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { await act(() => { - root.render(); + root.render(); }); - expect(node.hasAttribute('href')).toBe(true); + }).toErrorDev( + 'An empty string ("") was passed to the href attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to href instead of an empty string.', + ); + const node = container.firstChild; + expect(node.hasAttribute('href')).toBe(false); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'An empty string ("") was passed to the href attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to href instead of an empty string.', - ); - expect(node.hasAttribute('href')).toBe(false); + await act(() => { + root.render(); }); + expect(node.hasAttribute('href')).toBe(true); - it('should allow an empty href attribute on anchors', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); + await expect(async () => { await act(() => { - root.render(); + root.render(); }); - const node = container.firstChild; - expect(node.getAttribute('href')).toBe(''); + }).toErrorDev( + 'An empty string ("") was passed to the href attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to href instead of an empty string.', + ); + expect(node.hasAttribute('href')).toBe(false); + }); + + it('should allow an empty href attribute on anchors', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); }); + const node = container.firstChild; + expect(node.getAttribute('href')).toBe(''); + }); - it('should allow an empty action attribute', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(
); - }); - const node = container.firstChild; - expect(node.getAttribute('action')).toBe(''); + it('should allow an empty action attribute', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + const node = container.firstChild; + expect(node.getAttribute('action')).toBe(''); - await act(() => { - root.render(); - }); - expect(node.hasAttribute('action')).toBe(true); + await act(() => { + root.render(); + }); + expect(node.hasAttribute('action')).toBe(true); - await act(() => { - root.render(); - }); - expect(node.getAttribute('action')).toBe(''); + await act(() => { + root.render(); }); + expect(node.getAttribute('action')).toBe(''); + }); - it('allows empty string of a formAction to override the default of a parent', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - -