import { createElement, render, createRef, Component, Fragment } from 'preact'; import { setupScratch, teardown, serializeHtml } from '../../../test/_util/helpers'; import './fakeDevTools'; import 'preact/debug'; import * as PropTypes from 'prop-types'; // eslint-disable-next-line no-duplicate-imports import { resetPropWarnings } from 'preact/debug'; const h = createElement; /** @jsx createElement */ describe('debug', () => { let scratch; let errors = []; let warnings = []; beforeEach(() => { errors = []; warnings = []; scratch = setupScratch(); sinon.stub(console, 'error').callsFake(e => errors.push(e)); sinon.stub(console, 'warn').callsFake(w => warnings.push(w)); }); afterEach(() => { /** @type {*} */ (console.error).restore(); console.warn.restore(); teardown(scratch); }); it('should initialize devtools', () => { expect(window.__PREACT_DEVTOOLS__.attachPreact).to.have.been.called; }); it('should print an error on rendering on undefined parent', () => { let fn = () => render(
, undefined); expect(fn).to.throw(/render/); }); it('should print an error on rendering on invalid parent', () => { let fn = () => render(
, 6); expect(fn).to.throw(/valid HTML node/); expect(fn).to.throw(/
{ const App = () =>
; let fn = () => render(, 6); expect(fn).to.throw(/ render(, {}); expect(fn).to.throw(/ render(, 'badroot'); expect(fn).to.throw(/ { class App extends Component { render() { return
; } } let fn = () => render(, 6); expect(fn).to.throw(/ { let fn = () => render(h(undefined), scratch); expect(fn).to.throw(/createElement/); }); it('should print an error on invalid object component', () => { let fn = () => render(h({}), scratch); expect(fn).to.throw(/createElement/); }); it('should print an error when component is an array', () => { let fn = () => render(h([
]), scratch); expect(fn).to.throw(/createElement/); }); it('should print an error on double jsx conversion', () => { let Foo =
; let fn = () => render(h(), scratch); expect(fn).to.throw(/JSX twice/); }); it('should add __source to the vnode in debug mode.', () => { const vnode = h('div', { __source: { fileName: 'div.jsx', lineNumber: 3 } }); expect(vnode.__source).to.deep.equal({ fileName: 'div.jsx', lineNumber: 3 }); expect(vnode.props.__source).to.be.undefined; }); it('should add __self to the vnode in debug mode.', () => { const vnode = h('div', { __self: {} }); expect(vnode.__self).to.deep.equal({}); expect(vnode.props.__self).to.be.undefined; }); it('should warn when accessing certain attributes', () => { const vnode = h('div', null); // Push into an array to avoid empty statements being dead code eliminated const res = []; res.push(vnode); res.push(vnode.attributes); expect(console.warn).to.be.calledOnce; expect(console.warn.args[0]).to.match(/use vnode.props/); res.push(vnode.nodeName); expect(console.warn).to.be.calledTwice; expect(console.warn.args[1]).to.match(/use vnode.type/); res.push(vnode.children); expect(console.warn).to.be.calledThrice; expect(console.warn.args[2]).to.match(/use vnode.props.children/); // Should only warn once res.push(vnode.attributes); expect(console.warn).to.be.calledThrice; res.push(vnode.nodeName); expect(console.warn).to.be.calledThrice; res.push(vnode.children); expect(console.warn).to.be.calledThrice; vnode.attributes = {}; expect(console.warn.args[3]).to.match(/use vnode.props/); vnode.nodeName = ''; expect(console.warn.args[4]).to.match(/use vnode.type/); vnode.children = []; expect(console.warn.args[5]).to.match(/use vnode.props.children/); // Should only warn once vnode.attributes = {}; expect(console.warn.args.length).to.equal(6); vnode.nodeName = ''; expect(console.warn.args.length).to.equal(6); vnode.children = []; expect(console.warn.args.length).to.equal(6); // Mark res as used, otherwise it will be dead code eliminated expect(res.length).to.equal(7); }); it('should warn when calling setState inside the constructor', () => { class Foo extends Component { constructor(props) { super(props); this.setState({ foo: true }); } render() { return
foo
; } } render(, scratch); expect(console.warn).to.be.calledOnce; expect(console.warn.args[0]).to.match(/no-op/); }); it('should NOT warn when calling setState inside the cWM', () => { class Foo extends Component { componentWillMount() { this.setState({ foo: true }); } render() { return
foo
; } } render(, scratch); expect(console.warn).to.not.be.called; }); it('should warn when calling setState on an unmounted Component', () => { let setState; class Foo extends Component { constructor(props) { super(props); setState = () => this.setState({ foo: true }); } render() { return
foo
; } } render(, scratch); expect(console.warn).to.not.be.called; render(null, scratch); setState(); expect(console.warn).to.be.calledOnce; expect(console.warn.args[0]).to.match(/no-op/); }); it('should warn when calling forceUpdate inside the constructor', () => { class Foo extends Component { constructor(props) { super(props); this.forceUpdate(); } render() { return
foo
; } } render(, scratch); expect(console.warn).to.be.calledOnce; expect(console.warn.args[0]).to.match(/no-op/); }); it('should warn when calling forceUpdate on an unmounted Component', () => { let forceUpdate; class Foo extends Component { constructor(props) { super(props); forceUpdate = () => this.forceUpdate(); } render() { return
foo
; } } render(, scratch); forceUpdate(); expect(console.warn).to.not.be.called; render(null, scratch); forceUpdate(); expect(console.warn).to.be.calledOnce; expect(console.warn.args[0]).to.match(/no-op/); }); it('should print an error when child is a plain object', () => { let fn = () => render(
{{}}
, scratch); expect(fn).to.throw(/not valid/); }); it('should print an error on invalid refs', () => { let fn = () => render(
, scratch); expect(fn).to.throw(/createRef/); }); it('should not print for null as a handler', () => { let fn = () => render(
, scratch); expect(fn).not.to.throw(); }); it('should not print for undefined as a handler', () => { let fn = () => render(
, scratch); expect(fn).not.to.throw(); }); it('should not print for attributes starting with on for Components', () => { const Comp = () =>

online

; let fn = () => render(, scratch); expect(fn).not.to.throw(); }); it('should print an error on invalid handler', () => { let fn = () => render(
, scratch); expect(fn).to.throw(/"onclick" property should be a function/); }); it('should NOT print an error on valid refs', () => { let noop = () => {}; render(
, scratch); let ref = createRef(); render(
, scratch); expect(console.error).to.not.be.called; }); describe('duplicate keys', () => { const List = props =>
    {props.children}
; const ListItem = props =>
  • {props.children}
  • ; it('should print an error on duplicate keys with DOM nodes', () => { render(
    , scratch ); expect(console.error).to.be.calledOnce; }); it('should allow distinct object keys', () => { const A = { is: 'A' }; const B = { is: 'B' }; render(
    , scratch ); expect(console.error).not.to.be.called; }); it('should print an error for duplicate object keys', () => { const A = { is: 'A' }; render(
    , scratch ); expect(console.error).to.be.calledOnce; }); it('should print an error on duplicate keys with Components', () => { function App() { return ( a b d d ); } render(, scratch); expect(console.error).to.be.calledOnce; }); it('should print an error on duplicate keys with Fragments', () => { function App() { return ( a b {/* Should be okay to duplicate keys since these are inside a Fragment */} c d e f
    sibling
    ); } render(, scratch); expect(console.error).to.be.calledTwice; }); }); describe('table markup', () => { it('missing ///', () => { const Table = () => ( ); render(
    hi
    , scratch); expect(console.error).to.be.calledOnce; }); it('missing
    with ', () => { const Table = () => ( ); render(
    hi
    , scratch); expect(console.error).to.be.calledOnce; }); it('missing
    with ', () => { const Table = () => ( ); render(
    hi
    , scratch); expect(console.error).to.be.calledOnce; }); it('missing
    with ', () => { const Table = () => ( ); render(
    hi
    , scratch); expect(console.error).to.be.calledOnce; }); it('missing ', () => { const Table = () => (
    Hi
    ); render(, scratch); expect(console.error).to.be.calledOnce; }); it('missing with td component', () => { const Cell = ({ children }) => ; const Table = () => (
    {children}
    Hi
    ); render(, scratch); expect(console.error).to.be.calledOnce; }); it('missing with th component', () => { const Cell = ({ children }) => ; const Table = () => (
    {children}
    Hi
    ); render(, scratch); expect(console.error).to.be.calledOnce; }); it('Should accept ', () => { const Table = () => (
    instead of in
    Hi
    ); render(, scratch); expect(console.error).to.not.be.called; }); it('Accepts well formed table with TD components', () => { const Cell = ({ children }) => ; const Table = () => (
    {children}
    Body
    Head
    Body
    ); render(, scratch); expect(console.error).to.not.be.called; }); it('Accepts well formed table', () => { const Table = () => (
    Head
    Body
    Body
    ); render(, scratch); expect(console.error).to.not.be.called; }); it('Accepts minimal well formed table', () => { const Table = () => (
    Head
    Body
    ); render(, scratch); expect(console.error).to.not.be.called; }); }); describe('PropTypes', () => { beforeEach(() => { resetPropWarnings(); }); it("should fail if props don't match prop-types", () => { function Foo(props) { return

    {props.text}

    ; } Foo.propTypes = { text: PropTypes.string.isRequired }; render(, scratch); expect(console.error).to.be.calledOnce; // The message here may change when the "prop-types" library is updated, // but we check it exactly to make sure all parameters were supplied // correctly. expect(console.error).to.have.been.calledOnceWith( sinon.match( /^Failed prop type: Invalid prop `text` of type `number` supplied to `Foo`, expected `string`\.\n {2}in Foo \(at (.*)[/\\]debug[/\\]test[/\\]browser[/\\]debug\.test\.js:[0-9]+\)$/m ) ); }); it('should only log a given prop type error once', () => { function Foo(props) { return

    {props.text}

    ; } Foo.propTypes = { text: PropTypes.string.isRequired, count: PropTypes.number }; // Trigger the same error twice. The error should only be logged // once. render(, scratch); render(, scratch); expect(console.error).to.be.calledOnce; // Trigger a different error. This should result in a new log // message. console.error.resetHistory(); render(, scratch); expect(console.error).to.be.calledOnce; }); it('should render with error logged when validator gets signal and throws exception', () => { function Baz(props) { return

    {props.unhappy}

    ; } Baz.propTypes = { unhappy: function alwaysThrows(obj, key) { if (obj[key] === 'signal') throw Error('got prop'); } }; render(, scratch); expect(console.error).to.be.calledOnce; expect(errors[0].includes('got prop')).to.equal(true); expect(serializeHtml(scratch)).to.equal('

    signal

    '); }); it('should not print to console when types are correct', () => { function Bar(props) { return

    {props.text}

    ; } Bar.propTypes = { text: PropTypes.string.isRequired }; render(, scratch); expect(console.error).to.not.be.called; }); }); });