From 38acabfa6089ab8ac469c12b5f55022fb96935e5 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 23 Aug 2021 16:46:06 -0300 Subject: added web vendors --- .../lifecycles/getDerivedStateFromProps.test.js | 419 +++++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 preact/test/browser/lifecycles/getDerivedStateFromProps.test.js (limited to 'preact/test/browser/lifecycles/getDerivedStateFromProps.test.js') diff --git a/preact/test/browser/lifecycles/getDerivedStateFromProps.test.js b/preact/test/browser/lifecycles/getDerivedStateFromProps.test.js new file mode 100644 index 0000000..0fa1394 --- /dev/null +++ b/preact/test/browser/lifecycles/getDerivedStateFromProps.test.js @@ -0,0 +1,419 @@ +import { setupRerender } from 'preact/test-utils'; +import { createElement, render, Component } from 'preact'; +import { setupScratch, teardown } from '../../_util/helpers'; + +/** @jsx createElement */ + +describe('Lifecycle methods', () => { + /** @type {HTMLDivElement} */ + let scratch; + + /** @type {() => void} */ + let rerender; + + beforeEach(() => { + scratch = setupScratch(); + rerender = setupRerender(); + }); + + afterEach(() => { + teardown(scratch); + }); + + describe('static getDerivedStateFromProps', () => { + it('should set initial state with value returned from getDerivedStateFromProps', () => { + class Foo extends Component { + static getDerivedStateFromProps(props) { + return { + foo: props.foo, + bar: 'bar' + }; + } + render() { + return
; + } + } + + render(, scratch); + expect(scratch.firstChild.className).to.be.equal('foo bar'); + }); + + it('should update initial state with value returned from getDerivedStateFromProps', () => { + class Foo extends Component { + constructor(props, context) { + super(props, context); + this.state = { + foo: 'foo', + bar: 'bar' + }; + } + static getDerivedStateFromProps(props, state) { + return { + foo: `not-${state.foo}` + }; + } + render() { + return
; + } + } + + render(, scratch); + expect(scratch.firstChild.className).to.equal('not-foo bar'); + }); + + it("should update the instance's state with the value returned from getDerivedStateFromProps when props change", () => { + class Foo extends Component { + constructor(props, context) { + super(props, context); + this.state = { + value: 'initial' + }; + } + static getDerivedStateFromProps(props) { + if (props.update) { + return { + value: 'updated' + }; + } + + return null; + } + componentDidMount() {} + componentDidUpdate() {} + render() { + return
; + } + } + + sinon.spy(Foo, 'getDerivedStateFromProps'); + sinon.spy(Foo.prototype, 'componentDidMount'); + sinon.spy(Foo.prototype, 'componentDidUpdate'); + + render(, scratch); + expect(scratch.firstChild.className).to.equal('initial'); + expect(Foo.getDerivedStateFromProps).to.have.callCount(1); + expect(Foo.prototype.componentDidMount).to.have.callCount(1); // verify mount occurred + expect(Foo.prototype.componentDidUpdate).to.have.callCount(0); + + render(, scratch); + expect(scratch.firstChild.className).to.equal('updated'); + expect(Foo.getDerivedStateFromProps).to.have.callCount(2); + expect(Foo.prototype.componentDidMount).to.have.callCount(1); + expect(Foo.prototype.componentDidUpdate).to.have.callCount(1); // verify update occurred + }); + + it("should update the instance's state with the value returned from getDerivedStateFromProps when state changes", () => { + class Foo extends Component { + constructor(props, context) { + super(props, context); + this.state = { + value: 'initial' + }; + } + static getDerivedStateFromProps(props, state) { + // Don't change state for call that happens after the constructor + if (state.value === 'initial') { + return null; + } + + return { + value: state.value + ' derived' + }; + } + componentDidMount() { + // eslint-disable-next-line react/no-did-mount-set-state + this.setState({ value: 'updated' }); + } + render() { + return
; + } + } + + sinon.spy(Foo, 'getDerivedStateFromProps'); + + render(, scratch); + expect(scratch.firstChild.className).to.equal('initial'); + expect(Foo.getDerivedStateFromProps).to.have.been.calledOnce; + + rerender(); // call rerender to handle cDM setState call + expect(scratch.firstChild.className).to.equal('updated derived'); + expect(Foo.getDerivedStateFromProps).to.have.been.calledTwice; + }); + + it('should NOT modify state if null is returned', () => { + class Foo extends Component { + constructor(props, context) { + super(props, context); + this.state = { + foo: 'foo', + bar: 'bar' + }; + } + static getDerivedStateFromProps() { + return null; + } + render() { + return
; + } + } + + sinon.spy(Foo, 'getDerivedStateFromProps'); + + render(, scratch); + expect(scratch.firstChild.className).to.equal('foo bar'); + expect(Foo.getDerivedStateFromProps).to.have.been.called; + }); + + // NOTE: Difference from React + // React v16.3.2 warns if undefined if returned from getDerivedStateFromProps + it('should NOT modify state if undefined is returned', () => { + class Foo extends Component { + constructor(props, context) { + super(props, context); + this.state = { + foo: 'foo', + bar: 'bar' + }; + } + static getDerivedStateFromProps() {} + render() { + return
; + } + } + + sinon.spy(Foo, 'getDerivedStateFromProps'); + + render(, scratch); + expect(scratch.firstChild.className).to.equal('foo bar'); + expect(Foo.getDerivedStateFromProps).to.have.been.called; + }); + + it('should NOT invoke deprecated lifecycles (cWM/cWRP) if new static gDSFP is present', () => { + class Foo extends Component { + static getDerivedStateFromProps() {} + componentWillMount() {} + componentWillReceiveProps() {} + render() { + return
; + } + } + + sinon.spy(Foo, 'getDerivedStateFromProps'); + sinon.spy(Foo.prototype, 'componentWillMount'); + sinon.spy(Foo.prototype, 'componentWillReceiveProps'); + + render(, scratch); + expect(Foo.getDerivedStateFromProps).to.have.been.called; + expect(Foo.prototype.componentWillMount).to.not.have.been.called; + expect(Foo.prototype.componentWillReceiveProps).to.not.have.been.called; + }); + + it('is not called if neither state nor props have changed', () => { + let logs = []; + let childRef; + + class Parent extends Component { + constructor(props) { + super(props); + this.state = { parentRenders: 0 }; + } + + static getDerivedStateFromProps(props, state) { + logs.push('parent getDerivedStateFromProps'); + return state.parentRenders + 1; + } + + render() { + logs.push('parent render'); + return ; + } + } + + class Child extends Component { + constructor(props) { + super(props); + childRef = this; + } + render() { + logs.push('child render'); + return this.props.parentRenders; + } + } + + render(, scratch); + expect(logs).to.deep.equal([ + 'parent getDerivedStateFromProps', + 'parent render', + 'child render' + ]); + + logs = []; + childRef.setState({}); + rerender(); + expect(logs).to.deep.equal(['child render']); + }); + + // TODO: Investigate this test: + // [should not override state with stale values if prevState is spread within getDerivedStateFromProps](https://github.com/facebook/react/blob/25dda90c1ecb0c662ab06e2c80c1ee31e0ae9d36/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js#L1035) + + it('should be passed next props and state', () => { + /** @type {() => void} */ + let updateState; + + let propsArg; + let stateArg; + + class Foo extends Component { + constructor(props) { + super(props); + this.state = { + value: 0 + }; + updateState = () => + this.setState({ + value: this.state.value + 1 + }); + } + static getDerivedStateFromProps(props, state) { + // These object references might be updated later so copy + // object so we can assert their values at this snapshot in time + propsArg = { ...props }; + stateArg = { ...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 + }; + } + render() { + return
{this.state.value}
; + } + } + + // Initial render + // state.value: initialized to 0 in constructor, 0 -> 1 in gDSFP + render(, scratch); + + let element = scratch.firstChild; + expect(element.textContent).to.be.equal('1'); + expect(propsArg).to.deep.equal({ + foo: 'foo' + }); + expect(stateArg).to.deep.equal({ + value: 0 + }); + + // New Props + // state.value: 1 -> 2 in gDSFP + render(, scratch); + expect(element.textContent).to.be.equal('2'); + expect(propsArg).to.deep.equal({ + foo: 'bar' + }); + expect(stateArg).to.deep.equal({ + value: 1 + }); + + // New state + // state.value: 2 -> 3 in updateState, 3 -> 4 in gDSFP + updateState(); + rerender(); + expect(element.textContent).to.be.equal('4'); + expect(propsArg).to.deep.equal({ + foo: 'bar' + }); + expect(stateArg).to.deep.equal({ + value: 3 + }); + + // New Props (see #1446) + // 4 -> 5 in gDSFP + render(, scratch); + expect(element.textContent).to.be.equal('5'); + expect(stateArg).to.deep.equal({ + value: 4 + }); + + // New Props (see #1446) + // 5 -> 6 in gDSFP + render(, scratch); + expect(element.textContent).to.be.equal('6'); + expect(stateArg).to.deep.equal({ + value: 5 + }); + }); + + // From preactjs/preact#1170 + it('should NOT mutate state on mount, only create new versions', () => { + const stateConstant = {}; + let componentState; + + class Stateful extends Component { + static getDerivedStateFromProps() { + return { key: 'value' }; + } + + constructor() { + super(...arguments); + this.state = stateConstant; + } + + componentDidMount() { + componentState = this.state; + } + + render() { + return
; + } + } + + render(, scratch); + + // Verify captured object references didn't get mutated + expect(componentState).to.deep.equal({ key: 'value' }); + expect(stateConstant).to.deep.equal({}); + }); + + it('should NOT mutate state on update, only create new versions', () => { + const initialState = {}; + const capturedStates = []; + + let setState; + + class Stateful extends Component { + static getDerivedStateFromProps(props, state) { + return { value: (state.value || 0) + 1 }; + } + + constructor() { + super(...arguments); + this.state = initialState; + capturedStates.push(this.state); + + setState = this.setState.bind(this); + } + + componentDidMount() { + capturedStates.push(this.state); + } + + componentDidUpdate() { + capturedStates.push(this.state); + } + + render() { + return
; + } + } + + render(, scratch); + + setState({ value: 10 }); + rerender(); + + // Verify captured object references didn't get mutated + expect(capturedStates).to.deep.equal([{}, { value: 1 }, { value: 11 }]); + }); + }); +}); -- cgit v1.2.3