22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Martijn Schrage
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export function clearLog(): void;
4 |
5 | export function resetInstanceIdCounters(): void;
6 |
7 | export class VisualizerProvider extends React.Component<{}, {}> {}
8 |
9 | export class Log extends React.Component<{}, {}> {}
10 |
11 | export interface TraceProps {
12 | trace: (msg: string) => void,
13 | LifecyclePanel : () => JSX.Element
14 | }
15 |
16 | // Diff / Omit from https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-311923766
17 | type Diff =
18 | ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];
19 |
20 | type Omit = Pick>
21 |
22 | // Simpler TypeScript 2.8+ definition of Omit (disabled for now to support lower TypeScript versions)
23 | // export type Omit = Pick>;
24 |
25 | // Due to https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20796, we cannot use traceLifecycle as a decorator
26 | // in TypeScript, so just do `const TracedComponent = traceLifecycle(ComponentToTrace)` instead.
27 | export function traceLifecycle
16 |
17 | ## Usage
18 |
19 | The easiest way to get started is to
20 | open the [CodeSandbox playground](https://codesandbox.io/s/github/Oblosys/react-lifecycle-visualizer/tree/master/examples/parent-child-demo?file=/src/samples/New.js) and edit the sample components in `src/samples`. (For a better view of the log, press the 'Open in New Window' button in the top-right corner.)
21 |
22 | The panel shows the new React 16.3 lifecycle methods, unless the component defines at least one legacy method and no new methods. On a component that has both legacy and new methods, React ignores the legacy methods, so the panel shows the new methods.
23 |
24 | Though technically not lifecycle methods, `setState` & `render` are also traced. A single `setState(update, [callback])` call may generate up to three log entries:
25 |
26 | 1. `'setState'` for the call itself.
27 | 2. If `update` is a function instead of an object, `'setState:update fn'` is logged when that function is evaluated.
28 | 3. If a `callback` function is provided, `'setState:callback'` is logged when it's called.
29 |
30 | To save space, the lifecycle panel only contains `setState`, which gets highlighted on any of the three events above.
31 |
32 |
33 | ## Run the demo locally
34 |
35 | To run a local copy of the CodeSandbox demo, simply clone the repo, and run `npm install` & `npm start`:
36 |
37 | ```
38 | git clone git@github.com:Oblosys/react-lifecycle-visualizer.git
39 | cd react-lifecycle-visualizer
40 | npm install
41 | npm start
42 | ```
43 |
44 | The demo runs on http://localhost:8000/.
45 |
46 |
47 | ## Using the npm package
48 |
49 | ```sh
50 | $ npm i react-lifecycle-visualizer
51 | ```
52 |
53 | #### Setup
54 |
55 | To set up tracing, wrap the root or some other ancestor component in a `` and include the `` component somewhere. For example:
56 |
57 | ```jsx
58 | import { Log, VisualizerProvider } from 'react-lifecycle-visualizer';
59 |
60 | ReactDom.render(
61 |
62 |
63 |
64 |
65 |
66 | ,
67 | document.getElementById('root')
68 | );
69 | ```
70 |
71 | If you're using a WebPack dev-server with hot reloading, you can include a call to `resetInstanceIdCounters` in the module where you set up hot reloading:
72 |
73 | ```jsx
74 | import { resetInstanceIdCounters } from 'react-lifecycle-visualizer';
75 | ..
76 | resetInstanceIdCounters(); // reset instance counters on hot reload
77 | ..
78 | ```
79 |
80 | This isn't strictly necessary, but without it, instance counters will keep increasing on each hot reload, making the log less readable.
81 |
82 | #### Tracing components
83 |
84 | To trace a component (e.g. `ComponentToTrace`,) apply the `traceLifecycle` HOC to it. This is most easily done with a decorator.
85 |
86 | ```jsx
87 | import { traceLifecycle } from 'react-lifecycle-visualizer';
88 | ..
89 | @traceLifecycle
90 | class ComponentToTrace extends React.Component {
91 | ..
92 | render() {
93 | return (
94 | ..
95 |
96 | ..
97 | );
98 | }
99 | }
100 | ```
101 |
102 | Alternatively, apply `traceLifecycle` directly to the class, like this:
103 |
104 | ```jsx
105 | const ComponentToTrace = traceLifecycle(class ComponentToTrace extends React.Component {...});
106 | ```
107 |
108 | or
109 |
110 | ```jsx
111 | class ComponentToTraceOrg extends React.Component {...}
112 | const ComponentToTrace = traceLifecycle(ComponentToTraceOrg);
113 | ```
114 |
115 | #### Traced component props: `LifecyclePanel` and `trace`
116 |
117 | The traced component receives two additional props: `LifecyclePanel` and `trace`. The `LifecyclePanel` prop is a component that can be included in the rendering with `` to display the lifecycle methods of the traced component.
118 |
119 | ```jsx
120 | render() {
121 | return (
122 | ..
123 |
124 | ..
125 | );
126 | }
127 | ```
128 |
129 | The `trace` prop is a function of type `(msg: string) => void` that can be used to log custom messages:
130 |
131 | ```jsx
132 | componentDidUpdate(prevProps, prevState) {
133 | this.props.trace('prevProps: ' + JSON.stringify(prevProps));
134 | }
135 | ```
136 |
137 | In the constructor we can use `this.props.trace` after the call to `super`, or access `trace` on the `props` parameter:
138 |
139 | ```jsx
140 | constructor(props) {
141 | props.trace('before super(props)');
142 | super(props);
143 | this.props.trace('after super(props)');
144 | }
145 | ```
146 |
147 | In the static `getDerivedStateFromProps` we cannot use `this` to refer to the component instance, but we can access `trace` on the `nextProps` parameter:
148 |
149 | ```jsx
150 | static getDerivedStateFromProps(nextProps, prevState) {
151 | nextProps.trace('nextProps: ' + JSON.stringify(nextProps));
152 | ..
153 | }
154 | ```
155 |
156 | ## TypeScript
157 |
158 | There's no need to install additional TypeScript typings, as these are already included in the package. The interface `TraceProps` declares the `trace` and `LifecyclePanel` props. Its definition is
159 |
160 | ```typescript
161 | export interface TraceProps {
162 | trace: (msg: string) => void,
163 | LifecyclePanel : React.SFC
164 | }
165 | ```
166 |
167 | With the exception of tracing a component, the TypeScript setup is the same as the JavaScript setup above. Here's an example of a traced component in TypeScript:
168 |
169 |
170 | ```jsx
171 | import { traceLifecycle, TraceProps } from 'react-lifecycle-visualizer';
172 | ..
173 | interface ComponentToTraceProps extends TraceProps {}; // add trace & LifecyclePanel props
174 | interface ComponentToTraceState {}
175 |
176 | class ComponentToTrace extends React.Component {
177 | constructor(props: ComponentToTraceProps, context?: any) {
178 | props.trace('before super(props)');
179 | super(props, context);
180 | this.props.trace('after super(props)');
181 | }
182 |
183 | static getDerivedStateFromProps(nextProps : ComponentToTraceProps, nextState: ComponentToTraceState) {
184 | nextProps.trace('deriving');
185 | return null;
186 | }
187 |
188 | render() {
189 | return ;
190 | }
191 | }
192 |
193 | ```
194 |
195 | The only difference is that we cannot use `traceLifecycle` as a decorator in TypeScript, because it changes the signature of the parameter class (see this [issue](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20796)). Instead, we simply apply it as a function:
196 |
197 | ```tsx
198 | const TracedComponent = traceLifecycle(ComponentToTrace);
199 | ```
200 |
--------------------------------------------------------------------------------
/src/traceLifecycle.jsx:
--------------------------------------------------------------------------------
1 | /* eslint max-classes-per-file: 0, react/jsx-props-no-spreading: 0, react/static-property-placement: 0 */
2 | import React, { Component } from 'react';
3 | import hoistStatics from 'hoist-non-react-statics';
4 |
5 | import * as constants from './constants';
6 | import * as ActionCreators from './redux/actionCreators';
7 | import LifecyclePanel from './components/LifecyclePanel';
8 | import { MConstructor, MShouldUpdate, MRender, MDidMount,
9 | MDidUpdate, MWillUnmount, MSetState, MGetDerivedState, MGetSnapshot,
10 | MWillMount, MWillReceiveProps, MWillUpdate,
11 | MUnsafeWillMount, MUnsafeWillReceiveProps, MUnsafeWillUpdate} from './constants';
12 | import { store as lifecycleVisualizerStore } from './redux/VisualizerProvider';
13 |
14 | const instanceIdCounters = {};
15 |
16 | export const resetInstanceIdCounters = () => {
17 | Object.keys(instanceIdCounters).forEach((k) => delete instanceIdCounters[k]);
18 | };
19 |
20 | const mkInstanceId = (componentName) => {
21 | if (!Object.prototype.hasOwnProperty.call(instanceIdCounters, componentName)) {
22 | instanceIdCounters[componentName] = 0;
23 | }
24 | instanceIdCounters[componentName] += 1;
25 | return instanceIdCounters[componentName];
26 | };
27 |
28 | export default function traceLifecycle(ComponentToTrace) {
29 | const componentToTraceName = ComponentToTrace.displayName || ComponentToTrace.name || 'Component';
30 |
31 | const superMethods = Object.getOwnPropertyNames(ComponentToTrace.prototype).concat(
32 | ComponentToTrace.getDerivedStateFromProps ? [MGetDerivedState] : []
33 | );
34 |
35 | const isLegacy = // component is legacy if it includes one of the legacy methods and no new methods.
36 | superMethods.some((member) => constants.lifecycleMethodNamesLegacyOnly.includes(member)) &&
37 | superMethods.every((member) => !constants.lifecycleMethodNamesNewOnly.includes(member));
38 |
39 | const implementedMethods = [...superMethods, MSetState];
40 |
41 | class TracedComponent extends ComponentToTrace {
42 | constructor(props, context) {
43 | props.trace(MConstructor);
44 | super(props, context);
45 | if (!isLegacy && typeof this.state === 'undefined') {
46 | this.state = {};
47 | // Initialize state if it is undefined, otherwise the addition of getDerivedStateFromProps will cause a warning.
48 | }
49 | }
50 |
51 | componentWillMount() {
52 | this.props.trace(MWillMount);
53 | if (super.componentWillMount) {
54 | super.componentWillMount();
55 | }
56 | }
57 |
58 | UNSAFE_componentWillMount() { // eslint-disable-line camelcase
59 | this.props.trace(MWillMount); // trace it as 'componentWillMount' for brevity
60 | if (super.UNSAFE_componentWillMount) {
61 | super.UNSAFE_componentWillMount();
62 | }
63 | }
64 |
65 | static getDerivedStateFromProps(nextProps, prevState) {
66 | nextProps.trace(MGetDerivedState);
67 | return ComponentToTrace.getDerivedStateFromProps
68 | ? ComponentToTrace.getDerivedStateFromProps(nextProps, prevState)
69 | : null;
70 | }
71 |
72 | componentDidMount() {
73 | this.props.trace(MDidMount);
74 | if (super.componentDidMount) {
75 | super.componentDidMount();
76 | }
77 | }
78 |
79 | componentWillUnmount() {
80 | this.props.trace(MWillUnmount);
81 | if (super.componentWillUnmount) {
82 | super.componentWillUnmount();
83 | }
84 | }
85 |
86 | componentWillReceiveProps(...args) {
87 | this.props.trace(MWillReceiveProps);
88 | if (super.componentWillReceiveProps) {
89 | super.componentWillReceiveProps(...args);
90 | }
91 | }
92 |
93 | UNSAFE_componentWillReceiveProps(...args) { // eslint-disable-line camelcase
94 | this.props.trace(MWillReceiveProps); // trace it as 'componentWillReceiveProps' for brevity
95 | if (super.UNSAFE_componentWillReceiveProps) {
96 | super.UNSAFE_componentWillReceiveProps(...args);
97 | }
98 | }
99 |
100 | shouldComponentUpdate(...args) {
101 | this.props.trace(MShouldUpdate);
102 | return super.shouldComponentUpdate
103 | ? super.shouldComponentUpdate(...args)
104 | : true;
105 | }
106 |
107 | componentWillUpdate(...args) {
108 | this.props.trace(MWillUpdate);
109 | if (super.componentWillUpdate) {
110 | super.componentWillUpdate(...args);
111 | }
112 | }
113 |
114 | UNSAFE_componentWillUpdate(...args) { // eslint-disable-line camelcase
115 | this.props.trace(MWillUpdate); // trace it as 'componentWillUpdate' for brevity
116 | if (super.UNSAFE_componentWillUpdate) {
117 | super.UNSAFE_componentWillUpdate(...args);
118 | }
119 | }
120 |
121 | render() {
122 | if (super.render) {
123 | this.props.trace(MRender);
124 | return super.render();
125 | }
126 | return undefined; // There's no super.render, which will trigger a React error
127 | }
128 |
129 | getSnapshotBeforeUpdate(...args) {
130 | this.props.trace(MGetSnapshot);
131 | return super.getSnapshotBeforeUpdate
132 | ? super.getSnapshotBeforeUpdate(...args)
133 | : null;
134 | }
135 |
136 | componentDidUpdate(...args) {
137 | this.props.trace(MDidUpdate);
138 | if (super.componentDidUpdate) {
139 | super.componentDidUpdate(...args);
140 | }
141 | }
142 |
143 | setState(updater, callback) {
144 | this.props.trace(MSetState);
145 |
146 | // Unlike the lifecycle methods we only trace the update function and callback when they are actually defined.
147 | const tracingUpdater = typeof updater !== 'function' ? updater : (...args) => {
148 | this.props.trace(MSetState + ':update fn');
149 | return updater(...args);
150 | };
151 |
152 | const tracingCallback = !callback ? undefined : (...args) => {
153 | this.props.trace(MSetState + ':callback');
154 | callback(...args);
155 | };
156 | super.setState(tracingUpdater, tracingCallback);
157 | }
158 |
159 | static displayName = componentToTraceName;
160 | }
161 |
162 | class TracingComponent extends Component {
163 | constructor(props, context) {
164 | super(props, context);
165 |
166 | const instanceId = mkInstanceId(ComponentToTrace.name);
167 |
168 | // eslint-disable-next-line react/no-unstable-nested-components
169 | const WrappedLifecyclePanel = () => (
170 |
176 | );
177 | this.LifecyclePanel = WrappedLifecyclePanel;
178 |
179 | this.trace = (methodName) => {
180 | // Just dispatch on lifecycleVisualizerStore directly, rather than introducing complexity by using context.
181 | lifecycleVisualizerStore.dispatch(
182 | ActionCreators.trace(componentToTraceName, instanceId, methodName)
183 | );
184 | };
185 | }
186 |
187 | render() {
188 | return ;
189 | }
190 |
191 | static displayName = `traceLifecycle(${componentToTraceName})`;
192 | }
193 |
194 | // Removing the inappropriate methods is simpler than adding appropriate methods to prototype.
195 | if (isLegacy) {
196 | delete TracedComponent.getDerivedStateFromProps;
197 | delete TracedComponent.prototype.getSnapshotBeforeUpdate;
198 |
199 | // Only keep the tracer method corresponding to the implemented super method, unless neither the old or the
200 | // UNSAFE_ method is implemented, in which case we keep the UNSAFE_ method.
201 | // NOTE: This allows both the old method and the UNSAFE_ version to be traced, but this is correct, as React calls
202 | // both.
203 | const deleteOldOrUnsafe = (method, unsafeMethod) => {
204 | if (!superMethods.includes(method)) {
205 | delete TracedComponent.prototype[method];
206 | } else if (!superMethods.includes(unsafeMethod)) {
207 | delete TracedComponent.prototype[unsafeMethod];
208 | }
209 | };
210 |
211 | deleteOldOrUnsafe(MWillMount, MUnsafeWillMount);
212 | deleteOldOrUnsafe(MWillReceiveProps, MUnsafeWillReceiveProps);
213 | deleteOldOrUnsafe(MWillUpdate, MUnsafeWillUpdate);
214 | } else {
215 | delete TracedComponent.prototype.componentWillMount;
216 | delete TracedComponent.prototype.componentWillReceiveProps;
217 | delete TracedComponent.prototype.componentWillUpdate;
218 | delete TracedComponent.prototype.UNSAFE_componentWillMount;
219 | delete TracedComponent.prototype.UNSAFE_componentWillReceiveProps;
220 | delete TracedComponent.prototype.UNSAFE_componentWillUpdate;
221 | }
222 |
223 | return hoistStatics(TracingComponent, ComponentToTrace);
224 | }
225 |
--------------------------------------------------------------------------------
/test/integration.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { act, render, screen, within } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 |
5 | import { clearLog, resetInstanceIdCounters } from '../src';
6 |
7 | import { Wrapper } from './Wrapper';
8 | import TracedChild from './TracedChild';
9 | import TracedLegacyChild from './TracedLegacyChild';
10 | import TracedLegacyUnsafeChild from './TracedLegacyUnsafeChild';
11 |
12 | const nNewLifecyclePanelMethods = 9; // Non-legacy panel has 9 lifecycle methods
13 | const nLegacyLifecyclePanelMethods = 10; // Legacy panel has 10 lifecycle methods
14 |
15 | // Return array of length `n` which is 'true' at index `i` and 'false' everywhere else.
16 | const booleanStringListOnlyTrueAt = (n, i) => Array.from({length: n}, (_undefined, ix) => `${ix === i}`);
17 |
18 | const formatLogEntries = (instanceName, logMethods) => logMethods.map((e, i) =>
19 | ('' + i).padStart(2) + ` ${instanceName}: ` + e // NOTE: padding assumes <=100 entries
20 | );
21 |
22 | const setupUser = () => userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
23 |
24 | describe('traceLifecycle', () => {
25 | it('preserves static properties', () => {
26 | expect(TracedChild.staticProperty).toBe('a static property');
27 | });
28 | });
29 |
30 | describe('LifecyclePanel', () => {
31 | it('shows which methods are implemented', () => {
32 | render(}/>);
33 | const methods = [...screen.getByTestId('lifecycle-panel').querySelectorAll('.lifecycle-method')];
34 |
35 | methods.forEach((node) => {
36 | expect(node).toHaveAttribute('data-is-implemented', 'true');
37 | });
38 | });
39 |
40 | it('shows new methods for non-legacy component', () => {
41 | render(}/>);
42 | const methods = [...screen.getByTestId('lifecycle-panel').querySelectorAll('.lifecycle-method')];
43 |
44 | expect(methods).toHaveLength(nNewLifecyclePanelMethods);
45 | });
46 |
47 | it('shows legacy methods for legacy component', () => {
48 | /* eslint-disable no-console */
49 | // Disable console.warn to suppress React warnings about using legacy methods (emitted once per method).
50 | const consoleWarn = console.warn;
51 | console.warn = () => {};
52 | render(}/>);
53 | console.warn = consoleWarn;
54 | /* eslint-enable no-console */
55 |
56 | const methods = [...screen.getByTestId('lifecycle-panel').querySelectorAll('.lifecycle-method')];
57 |
58 | expect(methods).toHaveLength(nLegacyLifecyclePanelMethods);
59 | });
60 | });
61 |
62 | describe('Log', () => {
63 | it('sequentially highlights log entries', () => {
64 | render(}/>);
65 | act(() => jest.runOnlyPendingTimers()); // log entries are generated asynchronously, so run timers once
66 |
67 | const entries = [...screen.getByTestId('log-entries').querySelectorAll('.entry')];
68 | const nLogEntries = entries.length;
69 |
70 | expect(nLogEntries).toBeGreaterThan(0);
71 |
72 | for (let i = 0; i < nLogEntries; i++) {
73 | expect(entries.map((node) => node.getAttribute('data-is-highlighted'))).toEqual(
74 | booleanStringListOnlyTrueAt(nLogEntries, i)
75 | );
76 | act(() => jest.runOnlyPendingTimers()); // not necessary for last iteration, but harmless
77 | }
78 | });
79 |
80 | it('highlights the corresponding panel method', async () => {
81 | const user = setupUser();
82 | render(}/>);
83 | const logEntries = within(screen.getByTestId('log-entries'));
84 | const panel = within(screen.getByTestId('lifecycle-panel'));
85 | act(() => jest.runOnlyPendingTimers()); // log entries are generated asynchronously, so run timers once
86 |
87 | expect(panel.getByText('render')).toHaveAttribute('data-is-highlighted', 'false');
88 | await user.hover(logEntries.getByText('4 Child-1: render'));
89 | expect(panel.getByText('render')).toHaveAttribute('data-is-highlighted', 'true');
90 |
91 | expect(panel.getByText('constructor')).toHaveAttribute('data-is-highlighted', 'false');
92 | await user.hover(logEntries.getByText('0 Child-1: constructor'));
93 | expect(panel.getByText('constructor')).toHaveAttribute('data-is-highlighted', 'true');
94 | });
95 |
96 | it('logs all new lifecycle methods', async () => {
97 | const user = setupUser();
98 | render(}/>); // Mount TracedChild
99 | await user.click(screen.getByTestId('prop-value-checkbox')); // Update TracedChild prop
100 | await user.click(screen.getByTestId('state-update-button')); // Update TracedChild state
101 | await user.click(screen.getByTestId('show-child-checkbox')); // Unmount TracedChild
102 | act(() => jest.runOnlyPendingTimers());
103 |
104 | const expectedLogEntries = [
105 | // Mount TracedChild
106 | 'constructor',
107 | 'custom:constructor',
108 | 'static getDerivedStateFromProps',
109 | 'custom:getDerivedStateFromProps',
110 | 'render',
111 | 'custom:render',
112 | 'componentDidMount',
113 | 'custom:componentDidMount',
114 |
115 | // Update TracedChild prop
116 | 'static getDerivedStateFromProps',
117 | 'custom:getDerivedStateFromProps',
118 | 'shouldComponentUpdate',
119 | 'custom:shouldComponentUpdate',
120 | 'render',
121 | 'custom:render',
122 | 'getSnapshotBeforeUpdate',
123 | 'custom:getSnapshotBeforeUpdate',
124 | 'componentDidUpdate',
125 | 'custom:componentDidUpdate',
126 |
127 | // Update TracedChild state
128 | 'setState',
129 | 'setState:update fn',
130 | 'custom:setState update fn',
131 | 'static getDerivedStateFromProps',
132 | 'custom:getDerivedStateFromProps',
133 | 'shouldComponentUpdate',
134 | 'custom:shouldComponentUpdate',
135 | 'render',
136 | 'custom:render',
137 | 'getSnapshotBeforeUpdate',
138 | 'custom:getSnapshotBeforeUpdate',
139 | 'componentDidUpdate',
140 | 'custom:componentDidUpdate',
141 | 'setState:callback',
142 | 'custom:setState callback',
143 |
144 | // Unmount TracedChild
145 | 'componentWillUnmount',
146 | 'custom:componentWillUnmount',
147 | ];
148 |
149 | const entries = [...screen.getByTestId('log-entries').querySelectorAll('.entry')];
150 | expect(entries.map((node) => node.textContent))
151 | .toEqual(formatLogEntries('Child-1', expectedLogEntries)
152 | );
153 | });
154 |
155 | it('logs all legacy lifecycle methods', async () => {
156 | const user = setupUser();
157 |
158 | /* eslint-disable no-console */
159 | // Disable console.warn to suppress React warnings about using legacy methods.
160 | const consoleWarn = console.warn;
161 | console.warn = () => {};
162 | render(}/>); // Mount TracedLegacyChild
163 | console.warn = consoleWarn;
164 | /* eslint-enable no-console */
165 |
166 | await user.click(screen.getByTestId('prop-value-checkbox')); // Update TracedLegacyChild prop
167 | await user.click(screen.getByTestId('state-update-button')); // Update TracedLegacyChild state
168 | await user.click(screen.getByTestId('show-child-checkbox')); // Unmount TracedLegacyChild
169 | act(() => jest.runOnlyPendingTimers());
170 |
171 | const expectedLogEntries = [
172 | // Mount TracedLegacyChild
173 | 'constructor',
174 | 'custom:constructor',
175 | 'componentWillMount',
176 | 'custom:componentWillMount',
177 | 'render',
178 | 'custom:render',
179 | 'componentDidMount',
180 | 'custom:componentDidMount',
181 |
182 | // Update TracedLegacyChild prop
183 | 'componentWillReceiveProps',
184 | 'custom:componentWillReceiveProps',
185 | 'shouldComponentUpdate',
186 | 'custom:shouldComponentUpdate',
187 | 'componentWillUpdate',
188 | 'custom:componentWillUpdate',
189 | 'render',
190 | 'custom:render',
191 | 'componentDidUpdate',
192 | 'custom:componentDidUpdate',
193 |
194 | // Update TracedLegacyChild state
195 | 'setState',
196 | 'setState:update fn',
197 | 'custom:setState update fn',
198 | 'shouldComponentUpdate',
199 | 'custom:shouldComponentUpdate',
200 | 'componentWillUpdate',
201 | 'custom:componentWillUpdate',
202 | 'render',
203 | 'custom:render',
204 | 'componentDidUpdate',
205 | 'custom:componentDidUpdate',
206 | 'setState:callback',
207 | 'custom:setState callback',
208 |
209 | // Unmount TracedLegacyChild
210 | 'componentWillUnmount',
211 | 'custom:componentWillUnmount',
212 | ];
213 |
214 | const entries = [...screen.getByTestId('log-entries').querySelectorAll('.entry')];
215 | expect(entries.map((node) => node.textContent))
216 | .toEqual(formatLogEntries('LegacyChild-1', expectedLogEntries)
217 | );
218 | });
219 |
220 | it('logs all legacy UNSAFE_ lifecycle methods', async () => {
221 | const user = setupUser();
222 | // Mount TracedLegacyUnsafeChild
223 | render(}/>);
224 | await user.click(screen.getByTestId('prop-value-checkbox')); // Update TracedLegacyUnsafeChild prop
225 | act(() => jest.runOnlyPendingTimers());
226 |
227 | const expectedLogEntries = [
228 | // Mount TracedLegacyUnsafeChild
229 | 'constructor',
230 | 'componentWillMount',
231 | 'custom:UNSAFE_componentWillMount',
232 | 'render',
233 | 'componentDidMount',
234 |
235 | // Update TracedLegacyUnsafeChild prop
236 | 'componentWillReceiveProps',
237 | 'custom:UNSAFE_componentWillReceiveProps',
238 | 'shouldComponentUpdate',
239 | 'componentWillUpdate',
240 | 'custom:UNSAFE_componentWillUpdate',
241 | 'render',
242 | 'componentDidUpdate',
243 | ];
244 |
245 | const entries = [...screen.getByTestId('log-entries').querySelectorAll('.entry')];
246 | expect(entries.map((node) => node.textContent))
247 | .toEqual(formatLogEntries('LegacyUnsafeChild-1', expectedLogEntries)
248 | );
249 | });
250 |
251 | it('is cleared by clearLog()', () => {
252 | render(}/>);
253 | act(() => jest.runOnlyPendingTimers());
254 |
255 | const entries = [...screen.getByTestId('log-entries').querySelectorAll('.entry')];
256 | expect(entries).not.toHaveLength(0);
257 |
258 | act(() => clearLog());
259 |
260 | const clearedEntries = [...screen.getByTestId('log-entries').querySelectorAll('.entry')];
261 | expect(clearedEntries).toHaveLength(0);
262 | });
263 | });
264 |
265 | describe('instanceId counter', () => {
266 | it('starts at 1', () => {
267 | render(}/>);
268 | act(() => jest.runOnlyPendingTimers());
269 |
270 | const entries = [...screen.getByTestId('log-entries').querySelectorAll('.entry')];
271 | expect(entries[0]).toHaveTextContent(/^ ?\d+ Child-1/);
272 | });
273 |
274 | it('increments on remount', async () => {
275 | const user = setupUser();
276 | render(}/>); // Mount TracedChild
277 | await user.click(screen.getByTestId('show-child-checkbox')); // Unmount TracedChild
278 | act(() => jest.runOnlyPendingTimers());
279 | act(() => clearLog());
280 | await user.click(screen.getByTestId('show-child-checkbox')); // Mount TracedChild
281 | act(() => jest.runOnlyPendingTimers());
282 |
283 | const entries = [...screen.getByTestId('log-entries').querySelectorAll('.entry')];
284 | expect(entries[0]).toHaveTextContent(/^ ?\d+ Child-2/);
285 | });
286 |
287 | it('is reset by resetInstanceIdCounters', async () => {
288 | const user = setupUser();
289 | render(}/>); // Mount TracedChild
290 | await user.click(screen.getByTestId('show-child-checkbox')); // Unmount TracedChild
291 | act(() => jest.runOnlyPendingTimers());
292 | act(() => clearLog());
293 |
294 | resetInstanceIdCounters();
295 |
296 | await user.click(screen.getByTestId('show-child-checkbox')); // Mount TracedChild
297 | act(() => jest.runOnlyPendingTimers());
298 |
299 | const entries = [...screen.getByTestId('log-entries').querySelectorAll('.entry')];
300 | expect(entries[0]).toHaveTextContent(/^ ?\d+ Child-1/);
301 | });
302 | });
303 |
--------------------------------------------------------------------------------