From 20060906c9e86ca2f499458e6e956d7b59756c52 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 2 Nov 2016 19:29:54 +0000 Subject: [PATCH] Add tests for scheduling inside callbacks --- scripts/fiber/tests-passing.txt | 12 + .../ReactIncrementalScheduling-test.js | 449 ++++++++++++++++++ 2 files changed, 461 insertions(+) diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 995e16d47ef1c..d503949718040 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -816,6 +816,18 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js * splits deferred work on multiple roots * works on deferred roots in the order they were scheduled * handles interleaved deferred and animation work +* performs animation work in animation callback +* schedules deferred work in animation callback +* schedules deferred work and performs animation work in animation callback +* performs animation work and schedules deferred work in animation callback +* performs deferred work in deferred callback if it has time +* schedules deferred work in deferred callback if it runs out of time +* performs animated work in deferred callback if it has time +* performs animated work and deferred work in deferred callback if it has time +* performs deferred and animated work work in deferred callback if it has time +* FIXME: schedules animated work as if it was deferred in deferred callback if it runs out of time +* schedules animated work and deferred work in deferred callback if it runs out of time +* schedules deferred work and animated work in deferred callback if it runs out of time src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js * can update child nodes of a host instance diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js index 0bcbec226786d..4aa28976e8193 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalScheduling-test.js @@ -614,4 +614,453 @@ describe('ReactIncrementalScheduling', () => { expect(ReactNoop.getChildren('b')).toEqual([span('b:3')]); expect(ReactNoop.getChildren('c')).toEqual([span('c:3')]); }); + + it('performs animation work in animation callback', () => { + class Foo extends React.Component { + componentDidMount() { + // Animation work that will get performed during animation callback + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'b'); + }); + } + render() { + return ; + } + } + + // Schedule animation work + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'a'); + }); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(true); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + + // Flushing animation work should flush animation work scheduled inside it + ReactNoop.flushAnimationPri(); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + }); + + it('schedules deferred work in animation callback', () => { + class Foo extends React.Component { + componentDidMount() { + // Deferred work that will get scheduled during animation callback + ReactNoop.renderToRootWithID(, 'b'); + } + render() { + return ; + } + } + + // Schedule animation work + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'a'); + }); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(true); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + + // Flushing animation work should not flush the deferred work + ReactNoop.flushAnimationPri(); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flush the deferred work + ReactNoop.flushDeferredPri(); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + }); + + it('schedules deferred work and performs animation work in animation callback', () => { + let hasScheduled = false; + class Foo extends React.Component { + componentDidMount() { + // Deferred work that will get scheduled during animation callback + ReactNoop.renderToRootWithID(, 'b'); + // Animation work that will get performed during animation callback + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'c'); + }); + // Deferred work that will get scheduled during animation callback + ReactNoop.renderToRootWithID(, 'd'); + hasScheduled = true; + } + render() { + return ; + } + } + + // Schedule animation work + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'a'); + }); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(true); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + + // Flushing animation work should flush animation work scheduled inside it + ReactNoop.flushAnimationPri(); + expect(hasScheduled).toBe(true); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([]); + expect(ReactNoop.getChildren('c')).toEqual([span('c:1')]); + expect(ReactNoop.getChildren('d')).toEqual([]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flush the deferred work + ReactNoop.flushDeferredPri(); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.getChildren('c')).toEqual([span('c:1')]); + expect(ReactNoop.getChildren('d')).toEqual([span('d:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + }); + + it('performs animation work and schedules deferred work in animation callback', () => { + let hasScheduled = false; + class Foo extends React.Component { + componentDidMount() { + // Animation work that will get performed during animation callback + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'b'); + }); + // Deferred work that will get scheduled during animation callback + ReactNoop.renderToRootWithID(, 'c'); + // Animation work that will get performed during animation callback + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'd'); + }); + hasScheduled = true; + } + render() { + return ; + } + } + + // Schedule animation work + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'a'); + }); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(true); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + + // Flushing animation work should flush animation work scheduled inside it + ReactNoop.flushAnimationPri(); + expect(hasScheduled).toBe(true); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.getChildren('c')).toEqual([]); + expect(ReactNoop.getChildren('d')).toEqual([span('d:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flush the deferred work + ReactNoop.flushDeferredPri(); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.getChildren('c')).toEqual([span('c:1')]); + expect(ReactNoop.getChildren('d')).toEqual([span('d:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + }); + + it('performs deferred work in deferred callback if it has time', () => { + class Foo extends React.Component { + componentDidMount() { + // Deferred work that will get performed during deferred callback + ReactNoop.renderToRootWithID(, 'b'); + } + render() { + return ; + } + } + + // Schedule deferred work + ReactNoop.renderToRootWithID(, 'a'); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flushing deferred work should flush deferred work scheduled inside it + ReactNoop.flushDeferredPri(); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + }); + + it('schedules deferred work in deferred callback if it runs out of time', () => { + let hasScheduled = false; + class Foo extends React.Component { + componentDidMount() { + // Deferred work that will get scheduled during deferred callback + ReactNoop.renderToRootWithID(, 'b'); + hasScheduled = true; + } + render() { + return ; + } + } + + // Schedule deferred work + ReactNoop.renderToRootWithID(, 'a'); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flush just enough deferred work to schedule more deferred work + ReactNoop.flushDeferredPri(20); + expect(hasScheduled).toBe(true); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flush the rest of the deferred work + ReactNoop.flushDeferredPri(15); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + }); + + it('performs animated work in deferred callback if it has time', () => { + class Foo extends React.Component { + componentDidMount() { + // Animated work that will get performed during deferred callback + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'b'); + }); + } + render() { + return ; + } + } + + // Schedule deferred work + ReactNoop.renderToRootWithID(, 'a'); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flushing deferred work should flush animated work scheduled inside it + ReactNoop.flushDeferredPri(); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + }); + + it('performs animated work and deferred work in deferred callback if it has time', () => { + class Foo extends React.Component { + componentDidMount() { + // Deferred work that will get performed during deferred callback + ReactNoop.renderToRootWithID(, 'b'); + // Animation work that will get performed during deferred callback + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'c'); + }); + // Deferred work that will get performed during deferred callback + ReactNoop.renderToRootWithID(, 'd'); + } + render() { + return ; + } + } + + // Schedule deferred work + ReactNoop.renderToRootWithID(, 'a'); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flushing deferred work should flush both deferred and animated work scheduled inside it + ReactNoop.flushDeferredPri(); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.getChildren('c')).toEqual([span('c:1')]); + expect(ReactNoop.getChildren('d')).toEqual([span('d:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + }); + + it('performs deferred and animated work work in deferred callback if it has time', () => { + class Foo extends React.Component { + componentDidMount() { + // Animation work that will get performed during deferred callback + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'b'); + }); + // Deferred work that will get performed during deferred callback + ReactNoop.renderToRootWithID(, 'c'); + // Animation work that will get performed during deferred callback + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'd'); + }); + } + render() { + return ; + } + } + + // Schedule deferred work + ReactNoop.renderToRootWithID(, 'a'); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flushing deferred work should flush both deferred and animated work scheduled inside it + ReactNoop.flushDeferredPri(); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.getChildren('c')).toEqual([span('c:1')]); + expect(ReactNoop.getChildren('d')).toEqual([span('d:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + }); + + // This test documents the existing behavior. Desired behavior: + // it('schedules animated work in deferred callback if it runs out of time', () => { + it('FIXME: schedules animated work as if it was deferred in deferred callback if it runs out of time', () => { + let hasScheduled = false; + class Foo extends React.Component { + componentDidMount() { + // Animated work that will get scheduled during deferred callback + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'b'); + }); + hasScheduled = true; + } + render() { + return ; + } + } + + // Schedule deferred work + ReactNoop.renderToRootWithID(, 'a'); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flush just enough deferred work to schedule animated work + ReactNoop.flushDeferredPri(20); + expect(hasScheduled).toBe(true); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([]); + // FIXME: I would expect these to be flipped because animation work was scheduled. + // expect(ReactNoop.hasScheduledAnimationCallback()).toBe(true); + // expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flush the rest of work in an animated callback. + // Technically we didn't schedule this callback. This test is valid because we should have scheduled it. + ReactNoop.flushAnimationPri(); + // FIXME: we should have flushed everything here. + // expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + // expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + // expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + // expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // FIXME: it should be unnecessary to flush the deferred work: + ReactNoop.flushDeferredPri(); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + }); + + it('schedules animated work and deferred work in deferred callback if it runs out of time', () => { + let isScheduled = false; + class Foo extends React.Component { + componentDidMount() { + // Deferred work that will get performed during deferred callback + ReactNoop.renderToRootWithID(, 'b'); + // Animation work that will get performed during deferred callback + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'c'); + }); + // Deferred work that will get performed during deferred callback + ReactNoop.renderToRootWithID(, 'd'); + isScheduled = true; + } + render() { + return ; + } + } + + // Schedule deferred work + ReactNoop.renderToRootWithID(, 'a'); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flushing deferred work should schedule both deferred and animated work + ReactNoop.flushDeferredPri(20); + expect(isScheduled).toBe(true); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([]); + expect(ReactNoop.getChildren('c')).toEqual([]); + expect(ReactNoop.getChildren('d')).toEqual([]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flush the rest of the work + ReactNoop.flushDeferredPri(); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.getChildren('c')).toEqual([span('c:1')]); + expect(ReactNoop.getChildren('d')).toEqual([span('d:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + }); + + it('schedules deferred work and animated work in deferred callback if it runs out of time', () => { + let isScheduled = false; + class Foo extends React.Component { + componentDidMount() { + // Animation work that will get performed during deferred callback + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'b'); + }); + // Deferred work that will get performed during deferred callback + ReactNoop.renderToRootWithID(, 'c'); + // Animation work that will get performed during deferred callback + ReactNoop.performAnimationWork(() => { + ReactNoop.renderToRootWithID(, 'd'); + }); + isScheduled = true; + } + render() { + return ; + } + } + + // Schedule deferred work + ReactNoop.renderToRootWithID(, 'a'); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flushing deferred work should schedule both deferred and animated work + ReactNoop.flushDeferredPri(20); + expect(isScheduled).toBe(true); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([]); + expect(ReactNoop.getChildren('c')).toEqual([]); + expect(ReactNoop.getChildren('d')).toEqual([]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(true); + + // Flush the rest of the work + ReactNoop.flushDeferredPri(); + expect(ReactNoop.getChildren('a')).toEqual([span('a:1')]); + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]); + expect(ReactNoop.getChildren('c')).toEqual([span('c:1')]); + expect(ReactNoop.getChildren('d')).toEqual([span('d:1')]); + expect(ReactNoop.hasScheduledAnimationCallback()).toBe(false); + expect(ReactNoop.hasScheduledDeferredCallback()).toBe(false); + }); });