|
9 | 9 |
|
10 | 10 | 'use strict';
|
11 | 11 |
|
| 12 | +import {createEventTarget} from 'dom-event-testing-library'; |
| 13 | + |
12 | 14 | let React;
|
13 | 15 | let ReactDOM;
|
14 | 16 | let ReactDOMServer;
|
15 | 17 | let Scheduler;
|
16 | 18 | let Suspense;
|
| 19 | +let usePress; |
17 | 20 |
|
18 | 21 | function dispatchMouseHoverEvent(to, from) {
|
19 | 22 | if (!to) {
|
@@ -92,11 +95,16 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
92 | 95 | beforeEach(() => {
|
93 | 96 | jest.resetModuleRegistry();
|
94 | 97 |
|
| 98 | + let ReactFeatureFlags = require('shared/ReactFeatureFlags'); |
| 99 | + ReactFeatureFlags.enableDeprecatedFlareAPI = true; |
| 100 | + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; |
| 101 | + |
95 | 102 | React = require('react');
|
96 | 103 | ReactDOM = require('react-dom');
|
97 | 104 | ReactDOMServer = require('react-dom/server');
|
98 | 105 | Scheduler = require('scheduler');
|
99 | 106 | Suspense = React.Suspense;
|
| 107 | + usePress = require('react-interactions/events/press').usePress; |
100 | 108 | });
|
101 | 109 |
|
102 | 110 | if (!__EXPERIMENTAL__) {
|
@@ -342,6 +350,241 @@ describe('ReactDOMServerSelectiveHydration', () => {
|
342 | 350 | document.body.removeChild(container);
|
343 | 351 | });
|
344 | 352 |
|
| 353 | + it('hydrates the target boundary synchronously during a click (flare)', async () => { |
| 354 | + function Child({text}) { |
| 355 | + Scheduler.unstable_yieldValue(text); |
| 356 | + const listener = usePress({ |
| 357 | + onPress() { |
| 358 | + Scheduler.unstable_yieldValue('Clicked ' + text); |
| 359 | + }, |
| 360 | + }); |
| 361 | + |
| 362 | + return <span DEPRECATED_flareListeners={listener}>{text}</span>; |
| 363 | + } |
| 364 | + |
| 365 | + function App() { |
| 366 | + Scheduler.unstable_yieldValue('App'); |
| 367 | + return ( |
| 368 | + <div> |
| 369 | + <Suspense fallback="Loading..."> |
| 370 | + <Child text="A" /> |
| 371 | + </Suspense> |
| 372 | + <Suspense fallback="Loading..."> |
| 373 | + <Child text="B" /> |
| 374 | + </Suspense> |
| 375 | + </div> |
| 376 | + ); |
| 377 | + } |
| 378 | + |
| 379 | + let finalHTML = ReactDOMServer.renderToString(<App />); |
| 380 | + |
| 381 | + expect(Scheduler).toHaveYielded(['App', 'A', 'B']); |
| 382 | + |
| 383 | + let container = document.createElement('div'); |
| 384 | + // We need this to be in the document since we'll dispatch events on it. |
| 385 | + document.body.appendChild(container); |
| 386 | + |
| 387 | + container.innerHTML = finalHTML; |
| 388 | + |
| 389 | + let root = ReactDOM.createRoot(container, {hydrate: true}); |
| 390 | + root.render(<App />); |
| 391 | + |
| 392 | + // Nothing has been hydrated so far. |
| 393 | + expect(Scheduler).toHaveYielded([]); |
| 394 | + |
| 395 | + let span = container.getElementsByTagName('span')[1]; |
| 396 | + |
| 397 | + let target = createEventTarget(span); |
| 398 | + |
| 399 | + // This should synchronously hydrate the root App and the second suspense |
| 400 | + // boundary. |
| 401 | + let preventDefault = jest.fn(); |
| 402 | + target.virtualclick({preventDefault}); |
| 403 | + |
| 404 | + // The event should have been canceled because we called preventDefault. |
| 405 | + expect(preventDefault).toHaveBeenCalled(); |
| 406 | + |
| 407 | + // We rendered App, B and then invoked the event without rendering A. |
| 408 | + expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']); |
| 409 | + |
| 410 | + // After continuing the scheduler, we finally hydrate A. |
| 411 | + expect(Scheduler).toFlushAndYield(['A']); |
| 412 | + |
| 413 | + document.body.removeChild(container); |
| 414 | + }); |
| 415 | + |
| 416 | + it('hydrates at higher pri if sync did not work first time (flare)', async () => { |
| 417 | + let suspend = false; |
| 418 | + let resolve; |
| 419 | + let promise = new Promise(resolvePromise => (resolve = resolvePromise)); |
| 420 | + |
| 421 | + function Child({text}) { |
| 422 | + if ((text === 'A' || text === 'D') && suspend) { |
| 423 | + throw promise; |
| 424 | + } |
| 425 | + Scheduler.unstable_yieldValue(text); |
| 426 | + |
| 427 | + const listener = usePress({ |
| 428 | + onPress() { |
| 429 | + Scheduler.unstable_yieldValue('Clicked ' + text); |
| 430 | + }, |
| 431 | + }); |
| 432 | + return <span DEPRECATED_flareListeners={listener}>{text}</span>; |
| 433 | + } |
| 434 | + |
| 435 | + function App() { |
| 436 | + Scheduler.unstable_yieldValue('App'); |
| 437 | + return ( |
| 438 | + <div> |
| 439 | + <Suspense fallback="Loading..."> |
| 440 | + <Child text="A" /> |
| 441 | + </Suspense> |
| 442 | + <Suspense fallback="Loading..."> |
| 443 | + <Child text="B" /> |
| 444 | + </Suspense> |
| 445 | + <Suspense fallback="Loading..."> |
| 446 | + <Child text="C" /> |
| 447 | + </Suspense> |
| 448 | + <Suspense fallback="Loading..."> |
| 449 | + <Child text="D" /> |
| 450 | + </Suspense> |
| 451 | + </div> |
| 452 | + ); |
| 453 | + } |
| 454 | + |
| 455 | + let finalHTML = ReactDOMServer.renderToString(<App />); |
| 456 | + |
| 457 | + expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']); |
| 458 | + |
| 459 | + let container = document.createElement('div'); |
| 460 | + // We need this to be in the document since we'll dispatch events on it. |
| 461 | + document.body.appendChild(container); |
| 462 | + |
| 463 | + container.innerHTML = finalHTML; |
| 464 | + |
| 465 | + let spanD = container.getElementsByTagName('span')[3]; |
| 466 | + |
| 467 | + suspend = true; |
| 468 | + |
| 469 | + // A and D will be suspended. We'll click on D which should take |
| 470 | + // priority, after we unsuspend. |
| 471 | + let root = ReactDOM.createRoot(container, {hydrate: true}); |
| 472 | + root.render(<App />); |
| 473 | + |
| 474 | + // Nothing has been hydrated so far. |
| 475 | + expect(Scheduler).toHaveYielded([]); |
| 476 | + |
| 477 | + // This click target cannot be hydrated yet because it's suspended. |
| 478 | + let result = dispatchClickEvent(spanD); |
| 479 | + |
| 480 | + expect(Scheduler).toHaveYielded(['App']); |
| 481 | + |
| 482 | + expect(result).toBe(true); |
| 483 | + |
| 484 | + // Continuing rendering will render B next. |
| 485 | + expect(Scheduler).toFlushAndYield(['B', 'C']); |
| 486 | + |
| 487 | + suspend = false; |
| 488 | + resolve(); |
| 489 | + await promise; |
| 490 | + |
| 491 | + // After the click, we should prioritize D and the Click first, |
| 492 | + // and only after that render A and C. |
| 493 | + expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']); |
| 494 | + |
| 495 | + document.body.removeChild(container); |
| 496 | + }); |
| 497 | + |
| 498 | + it('hydrates at higher pri for secondary discrete events (flare)', async () => { |
| 499 | + let suspend = false; |
| 500 | + let resolve; |
| 501 | + let promise = new Promise(resolvePromise => (resolve = resolvePromise)); |
| 502 | + |
| 503 | + function Child({text}) { |
| 504 | + if ((text === 'A' || text === 'D') && suspend) { |
| 505 | + throw promise; |
| 506 | + } |
| 507 | + Scheduler.unstable_yieldValue(text); |
| 508 | + |
| 509 | + const listener = usePress({ |
| 510 | + onPress() { |
| 511 | + Scheduler.unstable_yieldValue('Clicked ' + text); |
| 512 | + }, |
| 513 | + }); |
| 514 | + return <span DEPRECATED_flareListeners={listener}>{text}</span>; |
| 515 | + } |
| 516 | + |
| 517 | + function App() { |
| 518 | + Scheduler.unstable_yieldValue('App'); |
| 519 | + return ( |
| 520 | + <div> |
| 521 | + <Suspense fallback="Loading..."> |
| 522 | + <Child text="A" /> |
| 523 | + </Suspense> |
| 524 | + <Suspense fallback="Loading..."> |
| 525 | + <Child text="B" /> |
| 526 | + </Suspense> |
| 527 | + <Suspense fallback="Loading..."> |
| 528 | + <Child text="C" /> |
| 529 | + </Suspense> |
| 530 | + <Suspense fallback="Loading..."> |
| 531 | + <Child text="D" /> |
| 532 | + </Suspense> |
| 533 | + </div> |
| 534 | + ); |
| 535 | + } |
| 536 | + |
| 537 | + let finalHTML = ReactDOMServer.renderToString(<App />); |
| 538 | + |
| 539 | + expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']); |
| 540 | + |
| 541 | + let container = document.createElement('div'); |
| 542 | + // We need this to be in the document since we'll dispatch events on it. |
| 543 | + document.body.appendChild(container); |
| 544 | + |
| 545 | + container.innerHTML = finalHTML; |
| 546 | + |
| 547 | + let spanA = container.getElementsByTagName('span')[0]; |
| 548 | + let spanC = container.getElementsByTagName('span')[2]; |
| 549 | + let spanD = container.getElementsByTagName('span')[3]; |
| 550 | + |
| 551 | + suspend = true; |
| 552 | + |
| 553 | + // A and D will be suspended. We'll click on D which should take |
| 554 | + // priority, after we unsuspend. |
| 555 | + let root = ReactDOM.createRoot(container, {hydrate: true}); |
| 556 | + root.render(<App />); |
| 557 | + |
| 558 | + // Nothing has been hydrated so far. |
| 559 | + expect(Scheduler).toHaveYielded([]); |
| 560 | + |
| 561 | + // This click target cannot be hydrated yet because the first is Suspended. |
| 562 | + dispatchClickEvent(spanA); |
| 563 | + dispatchClickEvent(spanC); |
| 564 | + dispatchClickEvent(spanD); |
| 565 | + |
| 566 | + expect(Scheduler).toHaveYielded(['App']); |
| 567 | + |
| 568 | + suspend = false; |
| 569 | + resolve(); |
| 570 | + await promise; |
| 571 | + |
| 572 | + // We should prioritize hydrating A, C and D first since we clicked in |
| 573 | + // them. Only after they're done will we hydrate B. |
| 574 | + expect(Scheduler).toFlushAndYield([ |
| 575 | + 'A', |
| 576 | + 'Clicked A', |
| 577 | + 'C', |
| 578 | + 'Clicked C', |
| 579 | + 'D', |
| 580 | + 'Clicked D', |
| 581 | + // B should render last since it wasn't clicked. |
| 582 | + 'B', |
| 583 | + ]); |
| 584 | + |
| 585 | + document.body.removeChild(container); |
| 586 | + }); |
| 587 | + |
345 | 588 | it('hydrates the last target as higher priority for continuous events', async () => {
|
346 | 589 | let suspend = false;
|
347 | 590 | let resolve;
|
|
0 commit comments