From 508eb7e4dded94083c97839c913c8761085c9c8d Mon Sep 17 00:00:00 2001 From: Josh Story Date: Wed, 22 Mar 2023 21:28:49 -0700 Subject: [PATCH] Add additional tests --- .../src/__tests__/ReactDOMFloat-test.js | 357 ++++++++++++++++-- 1 file changed, 319 insertions(+), 38 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 4e96755c19931..e11e3c979248a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -23,6 +23,7 @@ let ReactDOMClient; let ReactDOMFizzServer; let Suspense; let textCache; +let loadCache; let window; let document; let writable; @@ -34,6 +35,8 @@ let fatalError = undefined; let renderOptions; let waitForAll; let waitForThrow; +let assertLog; +let Scheduler; function resetJSDOM(markup) { // Test Environment @@ -62,12 +65,15 @@ describe('ReactDOMFloat', () => { ReactDOMFizzServer = require('react-dom/server'); Stream = require('stream'); Suspense = React.Suspense; + Scheduler = require('scheduler/unstable_mock'); const InternalTestUtils = require('internal-test-utils'); waitForAll = InternalTestUtils.waitForAll; waitForThrow = InternalTestUtils.waitForThrow; + assertLog = InternalTestUtils.assertLog; textCache = new Map(); + loadCache = new Set(); resetJSDOM('
'); container = document.getElementById('container'); @@ -259,6 +265,54 @@ describe('ReactDOMFloat', () => { ); } + function loadPreloads(hrefs) { + const event = new window.Event('load'); + const nodes = document.querySelectorAll('link[rel="preload"]'); + resolveLoadables(hrefs, nodes, event, href => + Scheduler.log('load preload: ' + href), + ); + } + + function errorPreloads(hrefs) { + const event = new window.Event('error'); + const nodes = document.querySelectorAll('link[rel="preload"]'); + resolveLoadables(hrefs, nodes, event, href => + Scheduler.log('error preload: ' + href), + ); + } + + function loadStylesheets(hrefs) { + const event = new window.Event('load'); + const nodes = document.querySelectorAll('link[rel="stylesheet"]'); + resolveLoadables(hrefs, nodes, event, href => + Scheduler.log('load stylesheet: ' + href), + ); + } + + function errorStylesheets(hrefs) { + const event = new window.Event('error'); + const nodes = document.querySelectorAll('link[rel="stylesheet"]'); + resolveLoadables(hrefs, nodes, event, href => { + Scheduler.log('error stylesheet: ' + href); + }); + } + + function resolveLoadables(hrefs, nodes, event, onLoad) { + const hrefSet = hrefs ? new Set(hrefs) : null; + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if (loadCache.has(node)) { + continue; + } + const href = node.getAttribute('href'); + if (!hrefSet || hrefSet.has(href)) { + loadCache.add(node); + onLoad(href); + node.dispatchEvent(event); + } + } + } + // @gate enableFloat it('can render resources before singletons', async () => { const root = ReactDOMClient.createRoot(document); @@ -1092,14 +1146,10 @@ body { , ); - await act(() => { - const barLink = document.querySelector( - 'link[rel="stylesheet"][href="bar"]', - ); - const event = document.createEvent('Events'); - event.initEvent('error', true, true); - barLink.dispatchEvent(event); - }); + errorStylesheets(['bar']); + assertLog(['error stylesheet: bar']); + + await waitForAll([]); const boundaryTemplateInstance = document.getElementById('B:0'); const suspenseInstance = boundaryTemplateInstance.previousSibling; @@ -1131,6 +1181,13 @@ body { }, }); await waitForAll([]); + // When binding a stylesheet that was SSR'd in a boundary reveal there is a loadingState promise + // We need to use that promise to resolve the suspended commit because we don't know if the load or error + // events have already fired. This requires the load to be awaited for the commit to have a chance to flush + // We could change this by tracking the loadingState's fulfilled status directly on the loadingState similar + // to thenables however this slightly increases the fizz runtime code size. + await loadStylesheets(); + assertLog(['load stylesheet: foo']); expect(getMeaningfulChildren(document)).toEqual( @@ -1667,7 +1724,7 @@ body { -
Hello
+
Goodbye
, @@ -1682,7 +1739,7 @@ body { -
Hello
+
Goodbye
, ); @@ -2682,12 +2739,7 @@ body { React.startTransition(() => { root.render( <> - +
hello
, ); @@ -2698,43 +2750,272 @@ body { , ); - const preload = document.querySelector('link[rel="preload"][as="style"]'); - const loadEvent = document.createEvent('Events'); - loadEvent.initEvent('load', true, true); - preload.dispatchEvent(loadEvent); + loadPreloads(); + assertLog(['load preload: foo']); // We expect that the stylesheet is inserted now but the commit has not happened yet. expect(getMeaningfulChildren(container)).toBe(undefined); expect(getMeaningfulChildren(document.head)).toEqual([ - , + , , ]); - const stylesheet = document.querySelector( - 'link[rel="stylesheet"][data-precedence]', - ); - const loadEvent2 = document.createEvent('Events'); - loadEvent2.initEvent('load', true, true); - stylesheet.dispatchEvent(loadEvent2); + loadStylesheets(); + assertLog(['load stylesheet: foo']); // We expect that the commit finishes synchronously after the stylesheet loads. expect(getMeaningfulChildren(container)).toEqual(
hello
); expect(getMeaningfulChildren(document.head)).toEqual([ - , + , , ]); }); + xit('can delay commit until css resources error', async () => { + // TODO: This test fails and crashes jest. need to figure out why before unskipping. + const root = ReactDOMClient.createRoot(container); + expect(getMeaningfulChildren(container)).toBe(undefined); + React.startTransition(() => { + root.render( + <> + + +
hello
+ , + ); + }); + await waitForAll([]); + expect(getMeaningfulChildren(container)).toBe(undefined); + expect(getMeaningfulChildren(document.head)).toEqual([ + , + , + ]); + + loadPreloads(['foo']); + errorPreloads(['bar']); + assertLog(['load preload: foo', 'error preload: bar']); + + // We expect that the stylesheet is inserted now but the commit has not happened yet. + expect(getMeaningfulChildren(container)).toBe(undefined); + expect(getMeaningfulChildren(document.head)).toEqual([ + , + , + , + , + ]); + + // // Try just this and crash all of Jest + // errorStylesheets(['bar']); + + // // Try this and it fails the test when it shouldn't + // await act(() => { + // errorStylesheets(['bar']); + // }); + + // Try this there is nothing throwing here which is not really surprising since + // the error is bubbling up through some kind of unhandled promise rejection thingy but + // still I thought it was worth confirming + try { + await act(() => { + errorStylesheets(['bar']); + }); + } catch (e) { + console.log(e); + } + + loadStylesheets(['foo']); + assertLog(['load stylesheet: foo', 'error stylesheet: bar']); + + // We expect that the commit finishes synchronously after the stylesheet loads. + expect(getMeaningfulChildren(container)).toEqual(
hello
); + expect(getMeaningfulChildren(document.head)).toEqual([ + , + , + , + , + ]); + }); + + it('assumes stylesheets that load in the shell loaded already', async () => { + await actIntoEmptyDocument(() => { + renderToPipeableStream( + + + + hello + + , + ).pipe(writable); + }); + + let root; + React.startTransition(() => { + root = ReactDOMClient.hydrateRoot( + document, + + + + hello + + , + ); + }); + await waitForAll([]); + expect(getMeaningfulChildren(document)).toEqual( + + + + + hello + , + ); + + React.startTransition(() => { + root.render( + + + + hello2 + + , + ); + }); + await waitForAll([]); + expect(getMeaningfulChildren(document)).toEqual( + + + + + hello2 + , + ); + + React.startTransition(() => { + root.render( + + + + hello3 + + + , + ); + }); + await waitForAll([]); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + hello2 + , + ); + + loadPreloads(); + assertLog(['load preload: bar']); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + hello2 + , + ); + + loadStylesheets(['bar']); + assertLog(['load stylesheet: bar']); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + hello3 + , + ); + }); + + it('can interrupt a suspended commit with a new update', async () => { + function App({children}) { + return ( + + {children} + + ); + } + const root = ReactDOMClient.createRoot(document); + root.render(); + React.startTransition(() => { + root.render( + + hello + + , + ); + }); + await waitForAll([]); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + , + ); + + root.render( + + hello2 + {null} + + , + ); + await waitForAll([]); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + hello2 + , + ); + + loadPreloads(['foo']); + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + hello2 + , + ); + + loadStylesheets(['foo']); + // Even though the foo stylesheet was still inserted as part of the suspense sequence of the first + // commit it does not actually perform the commit because it was cancelled when the higher priority + // update was processed. + expect(getMeaningfulChildren(document)).toEqual( + + + + + + + + hello2 + , + ); + }); + describe('ReactDOM.prefetchDNS(href)', () => { it('creates a dns-prefetch resource when called', async () => { function App({url}) {