/
githubmirror
/
preact
Обзор
Документация
Войти
/
githubmirror
/
preact
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
test/browser/lifecycles/componentDidUpdate.test.jsx
436 строк
10 KB
Ryan Christian
test: Switch tests to use `.jsx` file extension (#4925)
29 сен 2025, 16:58
Не верифицирован
29 сен 2025, 16:58
8e3e4ce
Код
Авторство
О чём код?
import { setupRerender } from 'preact/test-utils'; import { createElement, render, Component } from 'preact'; import { setupScratch, teardown } from '../../_util/helpers'; import { vi } from 'vitest'; describe('Lifecycle methods', () => { /** @type {HTMLDivElement} */ let scratch; /** @type {() => void} */ let rerender; beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); }); afterEach(() => { teardown(scratch); }); describe('#componentDidUpdate', () => { it('should be passed previous props and state', () => { /** @type {() => void} */ let updateState; let prevPropsArg; let prevStateArg; let snapshotArg; let curProps; let curState; class Foo extends Component { constructor(props) { super(props); this.state = { value: 0 }; updateState = () => this.setState({ value: this.state.value + 1 }); } static getDerivedStateFromProps(props, state) { // NOTE: Don't do this in real production code! // https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html return { value: state.value + 1 }; } componentDidUpdate(prevProps, prevState, snapshot) { // These object references might be updated later so copy // object so we can assert their values at this snapshot in time prevPropsArg = { ...prevProps }; prevStateArg = { ...prevState }; snapshotArg = snapshot; curProps = { ...this.props }; curState = { ...this.state }; } render() { return <div>{this.state.value}</div>; } } // Expectation: // `prevState` in componentDidUpdate should be // the state before setState and getDerivedStateFromProps was called. // `this.state` in componentDidUpdate should be // the updated state after getDerivedStateFromProps was called. // Initial render // state.value: initialized to 0 in constructor, 0 -> 1 in gDSFP render(<Foo foo="foo" />, scratch); expect(scratch.firstChild.textContent).to.be.equal('1'); expect(prevPropsArg).to.be.undefined; expect(prevStateArg).to.be.undefined; expect(snapshotArg).to.be.undefined; expect(curProps).to.be.undefined; expect(curState).to.be.undefined; // New props // state.value: 1 -> 2 in gDSFP render(<Foo foo="bar" />, scratch); expect(scratch.firstChild.textContent).to.be.equal('2'); expect(prevPropsArg).to.deep.equal({ foo: 'foo' }); expect(prevStateArg).to.deep.equal({ value: 1 }); expect(snapshotArg).to.be.undefined; expect(curProps).to.deep.equal({ foo: 'bar' }); expect(curState).to.deep.equal({ value: 2 }); // New state // state.value: 2 -> 3 in updateState, 3 -> 4 in gDSFP updateState(); rerender(); expect(scratch.firstChild.textContent).to.be.equal('4'); expect(prevPropsArg).to.deep.equal({ foo: 'bar' }); expect(prevStateArg).to.deep.equal({ value: 2 }); expect(snapshotArg).to.be.undefined; expect(curProps).to.deep.equal({ foo: 'bar' }); expect(curState).to.deep.equal({ value: 4 }); }); it('cDU should not be called when sDU returned false', () => { let spy = vi.fn(); let c; class App extends Component { constructor() { super(); c = this; } shouldComponentUpdate() { return false; } componentDidUpdate(prevProps) { spy(prevProps); } } render(<App />, scratch); c.setState({}); rerender(); expect(spy).not.toHaveBeenCalled(); }); it("prevState argument should be the same object if state doesn't change", () => { let changeProps, cduPrevState, cduCurrentState; class PropsProvider extends Component { constructor() { super(); this.state = { value: 0 }; changeProps = this.changeReceiverProps.bind(this); } changeReceiverProps() { let value = (this.state.value + 1) % 2; this.setState({ value }); } render() { return <PropsReceiver value={this.state.value} />; } } class PropsReceiver extends Component { componentDidUpdate(prevProps, prevState) { cduPrevState = prevState; cduCurrentState = this.state; } render({ value }) { return <div>{value}</div>; } } render(<PropsProvider />, scratch); changeProps(); rerender(); expect(cduPrevState).to.equal(cduCurrentState); }); it('prevState argument should be a different object if state does change', () => { let updateState, cduPrevState, cduCurrentState; class Foo extends Component { constructor() { super(); this.state = { value: 0 }; updateState = this.updateState.bind(this); } updateState() { let value = (this.state.value + 1) % 2; this.setState({ value }); } componentDidUpdate(prevProps, prevState) { cduPrevState = prevState; cduCurrentState = this.state; } render() { return <div>{this.state.value}</div>; } } render(<Foo />, scratch); updateState(); rerender(); expect(cduPrevState).to.not.equal(cduCurrentState); }); it("prevProps argument should be the same object if props don't change", () => { let updateState, cduPrevProps, cduCurrentProps; class Foo extends Component { constructor() { super(); this.state = { value: 0 }; updateState = this.updateState.bind(this); } updateState() { let value = (this.state.value + 1) % 2; this.setState({ value }); } componentDidUpdate(prevProps) { cduPrevProps = prevProps; cduCurrentProps = this.props; } render() { return <div>{this.state.value}</div>; } } render(<Foo />, scratch); updateState(); rerender(); expect(cduPrevProps).to.equal(cduCurrentProps); }); it('prevProps argument should be a different object if props do change', () => { let changeProps, cduPrevProps, cduCurrentProps; class PropsProvider extends Component { constructor() { super(); this.state = { value: 0 }; changeProps = this.changeReceiverProps.bind(this); } changeReceiverProps() { let value = (this.state.value + 1) % 2; this.setState({ value }); } render() { return <PropsReceiver value={this.state.value} />; } } class PropsReceiver extends Component { componentDidUpdate(prevProps) { cduPrevProps = prevProps; cduCurrentProps = this.props; } render({ value }) { return <div>{value}</div>; } } render(<PropsProvider />, scratch); changeProps(); rerender(); expect(cduPrevProps).to.not.equal(cduCurrentProps); }); it('is invoked after refs are set', () => { const spy = vi.fn(); let inst; let i = 0; class App extends Component { componentDidUpdate() { expect(spy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledWith(scratch.firstChild); } render() { let ref = null; if (i > 0) { // Add ref after mount (i > 0) ref = spy; } i++; inst = this; return <div ref={ref} />; } } render(<App />, scratch); expect(spy).not.toHaveBeenCalled(); inst.setState({}); rerender(); expect(spy).toHaveBeenCalledOnce(); expect(spy).toHaveBeenCalledWith(scratch.firstChild); }); it('should be called after children are mounted', () => { let log = []; class Inner extends Component { componentDidMount() { log.push('Inner mounted'); // Verify that the component is actually mounted when this // callback is invoked. expect(scratch.querySelector('#inner')).to.not.equal(null); } render() { return <div id="inner" />; } } class Outer extends Component { componentDidUpdate() { log.push('Outer updated'); } render(props) { return props.renderInner ? <Inner /> : <div />; } } render(<Outer renderInner={false} />, scratch); render(<Outer renderInner />, scratch); expect(log).to.deep.equal(['Inner mounted', 'Outer updated']); }); it('should be called after parent DOM elements are updated', () => { let setValue; let outerChildText; class Outer extends Component { constructor(p, c) { super(p, c); this.state = { i: 0 }; setValue = i => this.setState({ i }); } render(props, { i }) { return ( <div> <Inner i={i} {...props} /> <p id="parent-child">Outer: {i}</p> </div> ); } } class Inner extends Component { componentDidUpdate() { // At this point, the parent's <p> tag should've been updated with the latest value outerChildText = scratch.querySelector('#parent-child').textContent; } render(props, { i }) { return <div>Inner: {i}</div>; } } vi.spyOn(Inner.prototype, 'componentDidUpdate'); // Initial render render(<Outer />, scratch); expect(Inner.prototype.componentDidUpdate).not.toHaveBeenCalled(); // Set state with a new i const newValue = 5; setValue(newValue); rerender(); expect(Inner.prototype.componentDidUpdate).toHaveBeenCalled(); expect(outerChildText).to.equal(`Outer: ${newValue.toString()}`); }); it('should not interfere with setState callbacks', () => { let invocation; class Child extends Component { componentDidMount() { this.props.setValue(10); } render() { return <p>Hello world</p>; } } class App extends Component { constructor(props) { super(props); this.state = { show: false, count: null }; } componentDidMount() { // eslint-disable-next-line this.setState({ show: true }); } componentDidUpdate() {} render() { if (this.state.show) { return ( <Child setValue={i => this.setState({ count: i }, () => { invocation = this.state; }) } /> ); } return null; } } render(<App />, scratch); rerender(); expect(invocation.count).to.equal(10); }); }); });