diff --git a/packages/react-dom/src/__tests__/validateDOMNesting-test.internal.js b/packages/react-dom/src/__tests__/validateDOMNesting-test.internal.js deleted file mode 100644 index d68e6ea5a8d5b..0000000000000 --- a/packages/react-dom/src/__tests__/validateDOMNesting-test.internal.js +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * 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 validateDOMNesting; - -// https://html.spec.whatwg.org/multipage/syntax.html#special -const specialTags = [ - 'address', - 'applet', - 'area', - 'article', - 'aside', - 'base', - 'basefont', - 'bgsound', - 'blockquote', - 'body', - 'br', - 'button', - 'caption', - 'center', - 'col', - 'colgroup', - 'dd', - 'details', - 'dir', - 'div', - 'dl', - 'dt', - 'embed', - 'fieldset', - 'figcaption', - 'figure', - 'footer', - 'form', - 'frame', - 'frameset', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'head', - 'header', - 'hgroup', - 'hr', - 'html', - 'iframe', - 'img', - 'input', - 'isindex', - 'li', - 'link', - 'listing', - 'main', - 'marquee', - 'menu', - 'menuitem', - 'meta', - 'nav', - 'noembed', - 'noframes', - 'noscript', - 'object', - 'ol', - 'p', - 'param', - 'plaintext', - 'pre', - 'script', - 'section', - 'select', - 'source', - 'style', - 'summary', - 'table', - 'tbody', - 'td', - 'template', - 'textarea', - 'tfoot', - 'th', - 'thead', - 'title', - 'tr', - 'track', - 'ul', - 'wbr', - 'xmp', -]; - -// https://html.spec.whatwg.org/multipage/syntax.html#formatting -const formattingTags = [ - 'a', - 'b', - 'big', - 'code', - 'em', - 'font', - 'i', - 'nobr', - 's', - 'small', - 'strike', - 'strong', - 'tt', - 'u', -]; - -function isTagStackValid(stack) { - let ancestorInfo = null; - for (let i = 0; i < stack.length; i++) { - if (!validateDOMNesting.isTagValidInContext(stack[i], ancestorInfo)) { - return false; - } - ancestorInfo = validateDOMNesting.updatedAncestorInfo( - ancestorInfo, - stack[i], - null, - ); - } - return true; -} - -describe('validateDOMNesting', () => { - beforeEach(() => { - jest.resetModules(); - - // TODO: can we express this test with only public API? - validateDOMNesting = require('../client/validateDOMNesting').default; - }); - - it('allows any tag with no context', () => { - if (__DEV__) { - // With renderToString (for example), we don't know where we're mounting the - // tag so we must err on the side of leniency. - const allTags = [].concat(specialTags, formattingTags, ['mysterytag']); - allTags.forEach(function(tag) { - expect(validateDOMNesting.isTagValidInContext(tag, null)).toBe(true); - }); - } - }); - - it('allows valid nestings', () => { - if (__DEV__) { - expect(isTagStackValid(['table', 'tbody', 'tr', 'td', 'b'])).toBe(true); - expect(isTagStackValid(['body', 'datalist', 'option'])).toBe(true); - expect(isTagStackValid(['div', 'a', 'object', 'a'])).toBe(true); - expect(isTagStackValid(['div', 'p', 'button', 'p'])).toBe(true); - expect(isTagStackValid(['p', 'svg', 'foreignObject', 'p'])).toBe(true); - expect(isTagStackValid(['html', 'body', 'div'])).toBe(true); - - // Invalid, but not changed by browser parsing so we allow them - expect(isTagStackValid(['div', 'ul', 'ul', 'li'])).toBe(true); - expect(isTagStackValid(['div', 'label', 'div'])).toBe(true); - expect(isTagStackValid(['div', 'ul', 'li', 'section', 'li'])).toBe(true); - expect(isTagStackValid(['div', 'ul', 'li', 'dd', 'li'])).toBe(true); - } - }); - - it('prevents problematic nestings', () => { - if (__DEV__) { - expect(isTagStackValid(['a', 'a'])).toBe(false); - expect(isTagStackValid(['form', 'form'])).toBe(false); - expect(isTagStackValid(['p', 'p'])).toBe(false); - expect(isTagStackValid(['table', 'tr'])).toBe(false); - expect(isTagStackValid(['div', 'ul', 'li', 'div', 'li'])).toBe(false); - expect(isTagStackValid(['div', 'html'])).toBe(false); - expect(isTagStackValid(['body', 'body'])).toBe(false); - expect(isTagStackValid(['svg', 'foreignObject', 'body', 'p'])).toBe( - false, - ); - } - }); -}); diff --git a/packages/react-dom/src/__tests__/validateDOMNesting-test.js b/packages/react-dom/src/__tests__/validateDOMNesting-test.js new file mode 100644 index 0000000000000..415b64ea6808a --- /dev/null +++ b/packages/react-dom/src/__tests__/validateDOMNesting-test.js @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * 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'; + +var React = require('react'); +var ReactDOM = require('react-dom'); + +function normalizeCodeLocInfo(str) { + return str && str.replace(/at .+?:\d+/g, 'at **'); +} + +function expectInvalidNestingWarning(shouldWarn, tagsList, warningsList = []) { + let element = null; + let tags = tagsList; + let warnings = warningsList; + console.error.calls.reset(); + const container = document.createElement(tags.splice(0, 1)); + while (tags.length) { + element = React.createElement(tags.pop(), null, element); + } + ReactDOM.render(element, container); + if (shouldWarn) { + expect(console.error.calls.count()).toEqual(warningsList.length); + while (warnings.length) { + expect( + normalizeCodeLocInfo( + console.error.calls.argsFor(warnings.length - 1)[0], + ), + ).toContain(warnings.pop()); + } + } else { + expect(console.error.calls.count()).toEqual(0); + } +} + +describe('validateDOMNesting', () => { + it('allows valid nestings', () => { + spyOnDev(console, 'error'); + expectInvalidNestingWarning(false, ['table', 'tbody', 'tr', 'td', 'b']); + expectInvalidNestingWarning(false, ['div', 'a', 'object', 'a']); + expectInvalidNestingWarning(false, ['div', 'p', 'button', 'p']); + expectInvalidNestingWarning(false, ['p', 'svg', 'foreignObject', 'p']); + expectInvalidNestingWarning(false, ['html', 'body', 'div']); + + // Invalid, but not changed by browser parsing so we allow them + expectInvalidNestingWarning(false, ['div', 'ul', 'ul', 'li']); + expectInvalidNestingWarning(false, ['div', 'label', 'div']); + expectInvalidNestingWarning(false, ['div', 'ul', 'li', 'section', 'li']); + expectInvalidNestingWarning(false, ['div', 'ul', 'li', 'dd', 'li']); + }); + + it('prevents problematic nestings', () => { + spyOnDev(console, 'error'); + expectInvalidNestingWarning( + true, + ['body', 'datalist', 'option'], + [ + 'render(): Rendering components directly into document.body is discouraged', + ], + ); + expectInvalidNestingWarning( + true, + ['table', 'tr'], + ['validateDOMNesting(...): cannot appear as a child of '], + ); + expectInvalidNestingWarning( + true, + ['p', 'p'], + ['validateDOMNesting(...):

cannot appear as a descendant of

'], + ); + expectInvalidNestingWarning( + true, + ['div', 'ul', 'li', 'div', 'li'], + ['validateDOMNesting(...):

  • cannot appear as a descendant of
  • '], + ); + expectInvalidNestingWarning( + true, + ['div', 'html'], + ['validateDOMNesting(...): cannot appear as a child of
    '], + ); + expectInvalidNestingWarning( + true, + ['body', 'body'], + [ + 'render(): Rendering components directly into document.body is discouraged', + 'validateDOMNesting(...): cannot appear as a child of ', + ], + ); + expectInvalidNestingWarning( + true, + ['svg', 'foreignObject', 'body', 'p'], + [ + 'validateDOMNesting(...): cannot appear as a child of ', + ' is using uppercase HTML', + ], + ); + expectInvalidNestingWarning( + true, + ['a', 'a'], + ['validateDOMNesting(...): cannot appear as a descendant of '], + ); + expectInvalidNestingWarning( + true, + ['form', 'form'], + [ + 'validateDOMNesting(...):
    cannot appear as a descendant of ', + ], + ); + }); +}); diff --git a/packages/react-dom/src/client/validateDOMNesting.js b/packages/react-dom/src/client/validateDOMNesting.js index 8e6165d5e9912..9341bd1e3f591 100644 --- a/packages/react-dom/src/client/validateDOMNesting.js +++ b/packages/react-dom/src/client/validateDOMNesting.js @@ -482,17 +482,6 @@ if (__DEV__) { // TODO: turn this into a named export validateDOMNesting.updatedAncestorInfo = updatedAncestorInfo; - - // For testing - validateDOMNesting.isTagValidInContext = function(tag, ancestorInfo) { - ancestorInfo = ancestorInfo || emptyAncestorInfo; - const parentInfo = ancestorInfo.current; - const parentTag = parentInfo && parentInfo.tag; - return ( - isTagValidWithParent(tag, parentTag) && - !findInvalidAncestorForTag(tag, ancestorInfo) - ); - }; } export default validateDOMNesting;