/
githubmirror
/
preact
Обзор
Документация
Войти
/
githubmirror
/
preact
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
test/browser/placeholders.test.jsx
481 строка
11 KB
Ryan Christian
test: Switch tests to use `.jsx` file extension (#4925)
29 сен 2025, 16:58
Не верифицирован
29 сен 2025, 16:58
8e3e4ce
Код
Авторство
О чём код?
import { createElement, Component, render, createRef, Fragment } from 'preact'; import { setupRerender } from 'preact/test-utils'; import { setupScratch, teardown } from '../_util/helpers'; import { logCall, clearLog, getLog } from '../_util/logCall'; import { div } from '../_util/dom'; import { vi } from 'vitest'; describe('null placeholders', () => { /** @type {HTMLDivElement} */ let scratch; /** @type {() => void} */ let rerender; /** @type {string[]} */ let ops; function createNullable(name) { return function Nullable(props) { return props.show ? name : null; }; } /** * @param {string} name * @returns {[import('preact').ComponentClass, import('preact').RefObject<{ toggle(): void }>]} */ function createStatefulNullable(name) { let ref = createRef(); class Nullable extends Component { constructor(props) { super(props); this.state = { show: props.initialShow || true }; ref.current = this; } toggle() { this.setState({ show: !this.state.show }); } componentDidUpdate() { ops.push(`Update ${name}`); } componentDidMount() { ops.push(`Mount ${name}`); } componentWillUnmount() { ops.push(`Unmount ${name}`); } render() { return this.state.show ? <div>{name}</div> : null; } } return [Nullable, ref]; } let resetAppendChild; let resetInsertBefore; let resetRemoveText; let resetRemove; beforeAll(() => { resetAppendChild = logCall(Element.prototype, 'appendChild'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); resetRemove = logCall(Element.prototype, 'remove'); resetRemoveText = logCall(Text.prototype, 'remove'); }); afterAll(() => { resetAppendChild(); resetInsertBefore(); resetRemoveText(); resetRemove(); }); beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); ops = []; }); afterEach(() => { teardown(scratch); clearLog(); }); it('should treat undefined as a hole', () => { let Bar = () => <div>bar</div>; function Foo(props) { let sibling; if (props.condition) { sibling = <Bar />; } return ( <div> <div>Hello</div> {sibling} </div> ); } render(<Foo condition />, scratch); expect(scratch.innerHTML).to.equal( '<div><div>Hello</div><div>bar</div></div>' ); clearLog(); render(<Foo />, scratch); expect(scratch.innerHTML).to.equal('<div><div>Hello</div></div>'); expect(getLog()).to.deep.equal(['<div>bar.remove()']); }); it('should preserve state of Components when using null or booleans as placeholders', () => { // Must be the same class for all children in <App /> for this test to be valid class Stateful extends Component { constructor(props) { super(props); if (typeof props.ref === 'function') { props.ref(this); } else if (props.ref) { props.ref.current = this; } this.state = { count: 0 }; } increment() { this.setState({ count: this.state.count + 1 }); } componentDidUpdate() { ops.push(`Update ${this.props.name}`); } componentDidMount() { ops.push(`Mount ${this.props.name}`); } componentWillUnmount() { ops.push(`Unmount ${this.props.name}`); } render() { return ( <div> {this.props.name}: {this.state.count} </div> ); } } const s1ref = createRef(); const s2ref = createRef(); const s3ref = createRef(); function App({ first = null, second = false }) { return [first, second, <Stateful name="third" ref={s3ref} />]; } // Mount third stateful - Initial render render(<App />, scratch); expect(scratch.innerHTML).to.equal('<div>third: 0</div>'); expect(ops).to.deep.equal(['Mount third'], 'mount third'); // Update third stateful ops = []; s3ref.current.increment(); rerender(); expect(scratch.innerHTML).to.equal('<div>third: 1</div>'); expect(ops).to.deep.equal(['Update third'], 'update third'); // Mount first stateful ops = []; render(<App first={<Stateful name="first" ref={s1ref} />} />, scratch); expect(scratch.innerHTML).to.equal( '<div>first: 0</div><div>third: 1</div>' ); expect(ops).to.deep.equal(['Mount first', 'Update third'], 'mount first'); // Update first stateful ops = []; s1ref.current.increment(); s3ref.current.increment(); rerender(); expect(scratch.innerHTML).to.equal( '<div>first: 1</div><div>third: 2</div>' ); expect(ops).to.deep.equal(['Update first', 'Update third'], 'update first'); // Mount second stateful ops = []; render( <App first={<Stateful name="first" ref={s1ref} />} second={<Stateful name="second" ref={s2ref} />} />, scratch ); expect(scratch.innerHTML).to.equal( '<div>first: 1</div><div>second: 0</div><div>third: 2</div>' ); expect(ops).to.deep.equal( ['Update first', 'Mount second', 'Update third'], 'mount second' ); // Update second stateful ops = []; s1ref.current.increment(); s2ref.current.increment(); s3ref.current.increment(); rerender(); expect(scratch.innerHTML).to.equal( '<div>first: 2</div><div>second: 1</div><div>third: 3</div>' ); expect(ops).to.deep.equal( ['Update first', 'Update second', 'Update third'], 'update second' ); }); it('should efficiently replace self-updating null placeholders', () => { // These Nullable components replace themselves with null without the parent re-rendering const [Nullable, ref] = createStatefulNullable('Nullable'); const [Nullable2, ref2] = createStatefulNullable('Nullable2'); function App() { return ( <div> <div>1</div> <Nullable /> <div>3</div> <Nullable2 /> </div> ); } render(<App />, scratch); expect(scratch.innerHTML).to.equal( div([div(1), div('Nullable'), div(3), div('Nullable2')]) ); clearLog(); ref2.current.toggle(); ref.current.toggle(); rerender(); expect(scratch.innerHTML).to.equal(div([div(1), div(3)])); expect(getLog()).to.deep.equal([ '<div>Nullable2.remove()', '<div>Nullable.remove()' ]); clearLog(); ref2.current.toggle(); ref.current.toggle(); rerender(); expect(scratch.innerHTML).to.equal( div([div(1), div('Nullable'), div(3), div('Nullable2')]) ); expect(getLog()).to.deep.equal([ '<div>.appendChild(#text)', '<div>13.appendChild(<div>Nullable2)', '<div>.appendChild(#text)', '<div>13Nullable2.insertBefore(<div>Nullable, <div>3)' ]); }); // See preactjs/preact#2350 it('should efficiently replace null placeholders in parent rerenders (#2350)', () => { // This Nullable only changes when it's parent rerenders const Nullable1 = createNullable('Nullable 1'); const Nullable2 = createNullable('Nullable 2'); /** @type {() => void} */ let toggle; class App extends Component { constructor(props) { super(props); this.state = { show: false }; toggle = () => this.setState({ show: !this.state.show }); } render() { return ( <div> <div>{this.state.show.toString()}</div> <Nullable1 show={this.state.show} /> <div>the middle</div> <Nullable2 show={this.state.show} /> </div> ); } } render(<App />, scratch); expect(scratch.innerHTML).to.equal(div([div('false'), div('the middle')])); clearLog(); toggle(); rerender(); expect(scratch.innerHTML).to.equal( div([div('true'), 'Nullable 1', div('the middle'), 'Nullable 2']) ); expect(getLog()).to.deep.equal([ '<div>truethe middle.insertBefore(#text, <div>the middle)', '<div>trueNullable 1the middle.appendChild(#text)' ]); clearLog(); toggle(); rerender(); expect(scratch.innerHTML).to.equal(div([div('false'), div('the middle')])); expect(getLog()).to.deep.equal([ '#text.remove()', // '<div>falsethe middleNullable 2.appendChild(<div>the middle)', '#text.remove()' ]); }); it('when set through the children prop (#4074)', () => { /** @type {(state: { value: boolean }) => void} */ let setState; const Iframe = () => { // Using a div here to make debugging tests in devtools a little less // noisy. The dom ops still assert that the iframe isn't moved. // // return <iframe src="https://codesandbox.io/s/runtime-silence-no4zx" />; return <div>Iframe</div>; }; const Test2 = () => <div>Test2</div>; const Test3 = () => <div>Test3</div>; class App extends Component { constructor(props) { super(props); this.state = { value: true }; setState = this.setState.bind(this); } render(props, state) { return ( <div> <Test2 /> {state.value && <Test3 />} {props.children} </div> ); } } render( <App> <Iframe /> </App>, scratch ); expect(scratch.innerHTML).to.equal( '<div><div>Test2</div><div>Test3</div><div>Iframe</div></div>' ); clearLog(); setState({ value: false }); rerender(); expect(scratch.innerHTML).to.equal( '<div><div>Test2</div><div>Iframe</div></div>' ); expect(getLog()).to.deep.equal(['<div>Test3.remove()']); }); it('when set through the children prop when removing a Fragment with multiple DOM children (#4074)', () => { /** @type {(state: { value: boolean }) => void} */ let setState; const Iframe = () => { // Using a div here to make debugging tests in devtools a little less // noisy. The dom ops still assert that the iframe isn't moved. // // return <iframe src="https://codesandbox.io/s/runtime-silence-no4zx" />; return <div>Iframe</div>; }; const Test2 = () => <div>Test2</div>; const Test34 = () => ( <Fragment> <div>Test3</div> <div>Test4</div> </Fragment> ); class App extends Component { constructor(props) { super(props); this.state = { value: true }; setState = this.setState.bind(this); } render(props, state) { return ( <div> <Test2 /> {state.value && <Test34 />} {props.children} </div> ); } } render( <App> <Iframe /> </App>, scratch ); expect(scratch.innerHTML).to.equal( '<div><div>Test2</div><div>Test3</div><div>Test4</div><div>Iframe</div></div>' ); clearLog(); setState({ value: false }); rerender(); expect(scratch.innerHTML).to.equal( '<div><div>Test2</div><div>Iframe</div></div>' ); expect(getLog()).to.deep.equal([ '<div>Test3.remove()', '<div>Test4.remove()' ]); }); it('should only call unmount once when removing placeholders (#4104)', () => { /** @type {(state: { value: boolean }) => void} */ let setState; const Iframe = () => { // Using a div here to make debugging tests in devtools a little less // noisy. The dom ops still assert that the iframe isn't moved. // // return <iframe src="https://codesandbox.io/s/runtime-silence-no4zx" />; return <div>Iframe</div>; }; const Test2 = () => <div>Test2</div>; const ref = vi.fn(); class App extends Component { constructor(props) { super(props); this.state = { value: true }; setState = this.setState.bind(this); } render(props, state) { return ( <div> <Test2 /> {state.value && <div ref={ref}>Test3</div>} {props.children} </div> ); } } render( <App> <Iframe /> </App>, scratch ); expect(scratch.innerHTML).to.equal( '<div><div>Test2</div><div>Test3</div><div>Iframe</div></div>' ); expect(ref).toHaveBeenCalledTimes(1); ref.mockClear(); clearLog(); setState({ value: false }); rerender(); expect(scratch.innerHTML).to.equal( '<div><div>Test2</div><div>Iframe</div></div>' ); expect(getLog()).to.deep.equal(['<div>Test3.remove()']); expect(ref).toHaveBeenCalledTimes(1); }); });