From 14c37f4bf4da4ff9675c3ee9b29a642020730e1c Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 25 Sep 2018 08:34:07 -0700 Subject: [PATCH] Added new (failing) suspense+interaction tests --- .../__tests__/ReactProfiler-test.internal.js | 634 ++++++++++++++---- .../ReactProfilerDOM-test.internal.js | 174 +++++ 2 files changed, 690 insertions(+), 118 deletions(-) create mode 100644 packages/react/src/__tests__/ReactProfilerDOM-test.internal.js diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index bfb4e19bf3650..802aa31c0dd22 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -46,7 +46,9 @@ function loadModules({ if (useNoopRenderer) { ReactNoop = require('react-noop-renderer'); + ReactTestRenderer = null; } else { + ReactNoop = null; ReactTestRenderer = require('react-test-renderer'); ReactTestRenderer.unstable_setNowImplementation(mockNow); } @@ -1229,11 +1231,13 @@ describe('Profiler', () => { const getWorkForReactThreads = mockFn => mockFn.mock.calls.filter(([interactions, threadID]) => threadID > 0); - beforeEach(() => { + function loadModulesForTracing(params) { jest.resetModules(); loadModules({ + enableSuspense: true, enableSchedulerTracing: true, + ...params, }); throwInOnInteractionScheduledWorkCompleted = false; @@ -1273,7 +1277,9 @@ describe('Profiler', () => { onWorkStarted, onWorkStopped, }); - }); + } + + beforeEach(() => loadModulesForTracing()); describe('error handling', () => { it('should cover errors thrown in onWorkScheduled', () => { @@ -1404,6 +1410,34 @@ describe('Profiler', () => { }); }); + it('should properly trace work scheduled during the begin render phase', () => { + const callback = jest.fn(); + let wrapped; + const Component = jest.fn(() => { + wrapped = SchedulerTracing.unstable_wrap(callback); + return null; + }); + + let interaction; + SchedulerTracing.unstable_trace('event', mockNow(), () => { + const interactions = SchedulerTracing.unstable_getCurrent(); + expect(interactions.size).toBe(1); + interaction = Array.from(interactions)[0]; + ReactTestRenderer.create(); + }); + + expect(Component).toHaveBeenCalledTimes(1); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + expect(callback).not.toHaveBeenCalled(); + + wrapped(); + expect(callback).toHaveBeenCalledTimes(1); + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); + }); + it('should associate traced events with their subsequent commits', () => { let instance = null; @@ -2115,24 +2149,12 @@ describe('Profiler', () => { ]); }); - it('traces both the temporary placeholder and the finished render for an interaction', async () => { - jest.resetModules(); - - loadModules({ - useNoopRenderer: true, - enableSuspense: true, - enableSchedulerTracing: true, - }); - - // Re-register since we've reloaded modules - SchedulerTracing.unstable_subscribe({ - onInteractionScheduledWorkCompleted, - onInteractionTraced, - onWorkCanceled, - onWorkScheduled, - onWorkStarted, - onWorkStopped, - }); + describe('suspense', () => { + let AsyncText; + let Text; + let TextResource; + let cache; + let resourcePromise; function awaitableAdvanceTimers(ms) { jest.advanceTimersByTime(ms); @@ -2142,124 +2164,500 @@ describe('Profiler', () => { }); } - const SimpleCacheProvider = require('simple-cache-provider'); - let cache; - function invalidateCache() { - cache = SimpleCacheProvider.createCache(invalidateCache); + function yieldForRenderer(value) { + if (ReactNoop) { + ReactNoop.yield(value); + } else { + ReactTestRenderer.unstable_yield(value); + } } - invalidateCache(); - const TextResource = SimpleCacheProvider.createResource( - ([text, ms = 0]) => { - return new Promise((resolve, reject) => { + + beforeEach(() => { + const SimpleCacheProvider = require('simple-cache-provider'); + function invalidateCache() { + cache = SimpleCacheProvider.createCache(invalidateCache); + } + invalidateCache(); + + resourcePromise = null; + + TextResource = SimpleCacheProvider.createResource(([text, ms = 0]) => { + resourcePromise = new Promise((resolve, reject) => setTimeout(() => { - ReactNoop.yield(`Promise resolved [${text}]`); + yieldForRenderer(`Promise resolved [${text}]`); resolve(text); - }, ms); - }); - }, - ([text, ms]) => text, - ); + }, ms), + ); + return resourcePromise; + }, ([text, ms]) => text); + + AsyncText = ({ms, text}) => { + try { + TextResource.read(cache, [text, ms]); + yieldForRenderer(`AsyncText [${text}]`); + return text; + } catch (promise) { + if (typeof promise.then === 'function') { + yieldForRenderer(`Suspend [${text}]`); + } else { + yieldForRenderer(`Error [${text}]`); + } + throw promise; + } + }; - function Text(props) { - ReactNoop.yield(props.text); - return ; - } + Text = ({text}) => { + yieldForRenderer(`Text [${text}]`); + return text; + }; + }); - function span(prop) { - return {type: 'span', children: [], prop}; - } + it('traces both the temporary placeholder and the finished render for an interaction', async () => { + loadModulesForTracing({useNoopRenderer: true}); + + const interaction = { + id: 0, + name: 'initial render', + timestamp: mockNow(), + }; + + const onRender = jest.fn(); + SchedulerTracing.unstable_trace(interaction.name, mockNow(), () => { + ReactNoop.render( + + }> + + + + , + ); + }); + + expect(onInteractionTraced).toHaveBeenCalledTimes(1); + expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( + interaction, + ); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0); + expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); + + expect(ReactNoop.flush()).toEqual([ + 'Suspend [Async]', + 'Text [Loading...]', + 'Text [Sync]', + ]); + // The update hasn't expired yet, so we commit nothing. + expect(ReactNoop.getChildren()).toEqual([]); + expect(onRender).not.toHaveBeenCalled(); + + // Advance both React's virtual time and Jest's timers by enough to expire + // the update, but not by enough to flush the suspending promise. + ReactNoop.expire(10000); + await awaitableAdvanceTimers(10000); + // No additional rendering work is required, since we already prepared + // the placeholder. + expect(ReactNoop.flushExpired()).toEqual([]); + // Should have committed the placeholder. + expect(ReactNoop.getChildren()).toEqual([ + {text: 'Loading...'}, + {text: 'Sync'}, + ]); + expect(onRender).toHaveBeenCalledTimes(1); + + let call = onRender.mock.calls[0]; + expect(call[0]).toEqual('test-profiler'); + expect(call[6]).toMatchInteractions( + ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], + ); + + expect(onInteractionTraced).toHaveBeenCalledTimes(1); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + + // Once the promise resolves, we render the suspended view + await awaitableAdvanceTimers(10000); + expect(ReactNoop.flush()).toEqual([ + 'Promise resolved [Async]', + 'AsyncText [Async]', + ]); + expect(ReactNoop.getChildren()).toEqual([ + {text: 'Async'}, + {text: 'Sync'}, + ]); + expect(onRender).toHaveBeenCalledTimes(2); + + call = onRender.mock.calls[1]; + expect(call[0]).toEqual('test-profiler'); + expect(call[6]).toMatchInteractions( + ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], + ); + + expect(onInteractionTraced).toHaveBeenCalledTimes(1); + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); + }); + + it('does not prematurely complete for suspended sync renders', async () => { + const interaction = { + id: 0, + name: 'initial render', + timestamp: mockNow(), + }; + + const onRender = jest.fn(); + SchedulerTracing.unstable_trace( + interaction.name, + interaction.timestamp, + () => { + ReactTestRenderer.create( + + }> + + + , + ); + }, + ); + + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + + jest.runAllTimers(); + await resourcePromise; + + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); + }); + + it('traces cascading work after suspended sync renders', async () => { + let wrappedCascadingFn; + class AsyncComponentWithCascadingWork extends React.Component { + state = { + hasMounted: false, + }; + + componentDidMount() { + wrappedCascadingFn = SchedulerTracing.unstable_wrap(() => { + this.setState({hasMounted: true}); + }); + } - function AsyncText(props) { - const text = props.text; - try { - TextResource.read(cache, [props.text, props.ms]); - ReactNoop.yield(text); - return ; - } catch (promise) { - if (typeof promise.then === 'function') { - ReactNoop.yield(`Suspend! [${text}]`); - } else { - ReactNoop.yield(`Error! [${text}]`); + render() { + const {ms, text} = this.props; + TextResource.read(cache, [text, ms]); + return {this.state.hasMounted}; } - throw promise; } - } - const interaction = { - id: 0, - name: 'initial render', - timestamp: mockNow(), - }; + const interaction = { + id: 0, + name: 'initial render', + timestamp: mockNow(), + }; - const onRender = jest.fn(); - SchedulerTracing.unstable_trace(interaction.name, mockNow(), () => { - ReactNoop.render( - - }> - - - - , + const onRender = jest.fn(); + SchedulerTracing.unstable_trace( + interaction.name, + interaction.timestamp, + () => { + ReactTestRenderer.create( + + }> + + + , + ); + }, ); + + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + + jest.runAllTimers(); + await resourcePromise; + + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + + wrappedCascadingFn(); + + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); }); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( - interaction, - ); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(getWorkForReactThreads(onWorkStarted)).toHaveLength(0); - expect(getWorkForReactThreads(onWorkStopped)).toHaveLength(0); + it('does not prematurely complete for suspended renders that have exceeded their deadline', async () => { + const interaction = { + id: 0, + name: 'initial render', + timestamp: mockNow(), + }; - expect(ReactNoop.flush()).toEqual([ - 'Suspend! [Async]', - 'Loading...', - 'Sync', - ]); - // The update hasn't expired yet, so we commit nothing. - expect(ReactNoop.getChildren()).toEqual([]); - expect(onRender).not.toHaveBeenCalled(); + const onRender = jest.fn(); + let renderer; + SchedulerTracing.unstable_trace( + interaction.name, + interaction.timestamp, + () => { + renderer = ReactTestRenderer.create( + + }> + + + , + { + unstable_isAsync: true, + }, + ); + }, + ); - // Advance both React's virtual time and Jest's timers by enough to expire - // the update, but not by enough to flush the suspending promise. - ReactNoop.expire(10000); - await awaitableAdvanceTimers(10000); - // No additional rendering work is required, since we already prepared - // the placeholder. - expect(ReactNoop.flushExpired()).toEqual([]); - // Should have committed the placeholder. - expect(ReactNoop.getChildren()).toEqual([ - span('Loading...'), - span('Sync'), - ]); - expect(onRender).toHaveBeenCalledTimes(1); + advanceTimeBy(1500); + await awaitableAdvanceTimers(1500); - let call = onRender.mock.calls[0]; - expect(call[0]).toEqual('test-profiler'); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], - ); + expect(renderer).toFlushAll(['Suspend [loaded]', 'Text [loading]']); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + advanceTimeBy(2500); + await awaitableAdvanceTimers(2500); - // Once the promise resolves, we render the suspended view - await awaitableAdvanceTimers(10000); - expect(ReactNoop.flush()).toEqual(['Promise resolved [Async]', 'Async']); - expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]); - expect(onRender).toHaveBeenCalledTimes(2); + expect(renderer).toFlushAll(['AsyncText [loaded]']); + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); + }); - call = onRender.mock.calls[1]; - expect(call[0]).toEqual('test-profiler'); - expect(call[6]).toMatchInteractions( - ReactFeatureFlags.enableSchedulerTracing ? [interaction] : [], - ); + it('decrements interaction count correctly if suspense loads before placeholder is shown', async () => { + const interaction = { + id: 0, + name: 'initial render', + timestamp: mockNow(), + }; - expect(onInteractionTraced).toHaveBeenCalledTimes(1); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect( - onInteractionScheduledWorkCompleted, - ).toHaveBeenLastNotifiedOfInteraction(interaction); + const onRender = jest.fn(); + let renderer; + SchedulerTracing.unstable_trace( + interaction.name, + interaction.timestamp, + () => { + renderer = ReactTestRenderer.create( + + }> + + + , + {unstable_isAsync: true}, + ); + }, + ); + renderer.unstable_flushAll(); + + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1000); + await resourcePromise; + renderer.unstable_flushAll(); + + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); + }); + + it('handles high-pri renderers between suspended and resolved (sync) trees', async () => { + const initialRenderInteraction = { + id: 0, + name: 'initial render', + timestamp: mockNow(), + }; + + const onRender = jest.fn(); + let renderer; + SchedulerTracing.unstable_trace( + initialRenderInteraction.name, + initialRenderInteraction.timestamp, + () => { + renderer = ReactTestRenderer.create( + + }> + + + + , + ); + }, + ); + expect(renderer.toJSON()).toEqual(['loading', 'initial']); + + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + expect(onRender).toHaveBeenCalledTimes(2); // Sync null commit, placeholder commit + expect(onRender.mock.calls[0][6]).toMatchInteractions([ + initialRenderInteraction, + ]); + onRender.mockClear(); + + const highPriUpdateInteraction = { + id: 1, + name: 'hiPriUpdate', + timestamp: mockNow(), + }; + + const originalPromise = resourcePromise; + + renderer.unstable_flushSync(() => { + SchedulerTracing.unstable_trace( + highPriUpdateInteraction.name, + highPriUpdateInteraction.timestamp, + () => { + renderer.update( + + }> + + + + , + ); + }, + ); + }); + expect(renderer.toJSON()).toEqual(['loading', 'updated']); + + expect(onRender).toHaveBeenCalledTimes(2); // Sync null commit, placeholder commit + expect(onRender.mock.calls[0][6]).toMatchInteractions([ + initialRenderInteraction, + highPriUpdateInteraction, + ]); + onRender.mockClear(); + + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + + advanceTimeBy(1000); + jest.advanceTimersByTime(1000); + await originalPromise; + expect(renderer.toJSON()).toEqual(['loaded', 'updated']); + + expect(onRender).toHaveBeenCalledTimes(2); + expect(onRender.mock.calls[0][6]).toMatchInteractions([ + initialRenderInteraction, + highPriUpdateInteraction, + ]); + + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); + expect( + onInteractionScheduledWorkCompleted.mock.calls[0][0], + ).toMatchInteraction(initialRenderInteraction); + expect( + onInteractionScheduledWorkCompleted.mock.calls[1][0], + ).toMatchInteraction(highPriUpdateInteraction); + }); + + it('handles high-pri renderers between suspended and resolved (async) trees', async () => { + const initialRenderInteraction = { + id: 0, + name: 'initial render', + timestamp: mockNow(), + }; + + const onRender = jest.fn(); + let renderer; + SchedulerTracing.unstable_trace( + initialRenderInteraction.name, + initialRenderInteraction.timestamp, + () => { + renderer = ReactTestRenderer.create( + + }> + + + + , + {unstable_isAsync: true}, + ); + }, + ); + expect(renderer).toFlushAll([ + 'Suspend [loaded]', + 'Text [loading]', + 'Text [initial]', + ]); + + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + expect(onRender).not.toHaveBeenCalled(); + + advanceTimeBy(500); + jest.advanceTimersByTime(500); + + const highPriUpdateInteraction = { + id: 1, + name: 'hiPriUpdate', + timestamp: mockNow(), + }; + + const originalPromise = resourcePromise; + + renderer.unstable_flushSync(() => { + SchedulerTracing.unstable_trace( + highPriUpdateInteraction.name, + highPriUpdateInteraction.timestamp, + () => { + renderer.update( + + }> + + + + , + ); + }, + ); + }); + expect(renderer.toJSON()).toEqual(['loading', 'updated']); + + expect(onRender).toHaveBeenCalledTimes(1); + expect(onRender.mock.calls[0][6]).toMatchInteractions([ + highPriUpdateInteraction, + ]); + onRender.mockClear(); + + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(highPriUpdateInteraction); + + advanceTimeBy(500); + jest.advanceTimersByTime(500); + await originalPromise; + expect(renderer).toFlushAll(['AsyncText [loaded]']); + expect(renderer.toJSON()).toEqual(['loaded', 'updated']); + + expect(onRender).toHaveBeenCalledTimes(1); + expect(onRender.mock.calls[0][6]).toMatchInteractions([ + initialRenderInteraction, + ]); + + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(2); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(initialRenderInteraction); + }); }); }); }); diff --git a/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js b/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js new file mode 100644 index 0000000000000..07a7c54da63d8 --- /dev/null +++ b/packages/react/src/__tests__/ReactProfilerDOM-test.internal.js @@ -0,0 +1,174 @@ +/** + * Copyright (c) Facebook, Inc. and its 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 ReactFeatureFlags; +let ReactDOM; +let SchedulerTracing; +let SimpleCacheProvider; + +function initEnvForAsyncTesting() { + // Boilerplate copied from ReactDOMRoot-test + // TODO pull this into helper method, reduce repetition. + const originalDateNow = Date.now; + global.Date.now = function() { + return originalDateNow(); + }; + global.requestAnimationFrame = function(cb) { + return setTimeout(() => { + cb(Date.now()); + }); + }; + const originalAddEventListener = global.addEventListener; + let postMessageCallback; + global.addEventListener = function(eventName, callback, useCapture) { + if (eventName === 'message') { + postMessageCallback = callback; + } else { + originalAddEventListener(eventName, callback, useCapture); + } + }; + global.postMessage = function(messageKey, targetOrigin) { + const postMessageEvent = {source: window, data: messageKey}; + if (postMessageCallback) { + postMessageCallback(postMessageEvent); + } + }; +} + +function loadModules() { + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffects = false; + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + ReactFeatureFlags.enableProfilerTimer = true; + ReactFeatureFlags.enableSchedulerTracing = true; + ReactFeatureFlags.enableSuspense = true; + + React = require('react'); + SchedulerTracing = require('scheduler/tracing'); + ReactDOM = require('react-dom'); + SimpleCacheProvider = require('simple-cache-provider'); +} + +describe('ProfilerDOM', () => { + let TextResource; + let cache; + let resourcePromise; + let onInteractionScheduledWorkCompleted; + let onInteractionTraced; + + beforeEach(() => { + initEnvForAsyncTesting(); + loadModules(); + + onInteractionScheduledWorkCompleted = jest.fn(); + onInteractionTraced = jest.fn(); + + // Verify interaction subscriber methods are called as expected. + SchedulerTracing.unstable_subscribe({ + onInteractionScheduledWorkCompleted, + onInteractionTraced, + onWorkCanceled: () => {}, + onWorkScheduled: () => {}, + onWorkStarted: () => {}, + onWorkStopped: () => {}, + }); + + cache = SimpleCacheProvider.createCache(() => {}); + + resourcePromise = null; + + TextResource = SimpleCacheProvider.createResource(([text, ms = 0]) => { + resourcePromise = new Promise( + SchedulerTracing.unstable_wrap((resolve, reject) => { + setTimeout( + SchedulerTracing.unstable_wrap(() => { + resolve(text); + }), + ms, + ); + }), + ); + return resourcePromise; + }, ([text, ms]) => text); + }); + + const AsyncText = ({ms, text}) => { + TextResource.read(cache, [text, ms]); + return text; + }; + + const Text = ({text}) => text; + + it('should correctly trace interactions for async roots', async done => { + let batch, element, interaction; + + SchedulerTracing.unstable_trace('initial_event', performance.now(), () => { + const interactions = SchedulerTracing.unstable_getCurrent(); + expect(interactions.size).toBe(1); + interaction = Array.from(interactions)[0]; + + element = document.createElement('div'); + const root = ReactDOM.unstable_createRoot(element); + batch = root.createBatch(); + batch.render( + }> + + , + ); + batch.then( + SchedulerTracing.unstable_wrap(() => { + batch.commit(); + + expect(element.textContent).toBe('Loading...'); + expect(onInteractionTraced).toHaveBeenCalledTimes(1); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + + resourcePromise.then( + SchedulerTracing.unstable_wrap(() => { + jest.runAllTimers(); + + expect(element.textContent).toBe('Text'); + expect(onInteractionTraced).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).not.toHaveBeenCalled(); + + // Evaluate in an unwrapped callback, + // Because trace/wrap won't decrement the count within the wrapped callback. + setImmediate(() => { + expect(onInteractionTraced).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); + + expect(interaction.__count).toBe(0); + + done(); + }); + }), + ); + }), + ); + }); + + expect(onInteractionTraced).toHaveBeenCalledTimes(1); + expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( + interaction, + ); + expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled(); + + jest.runAllTimers(); + }); +});