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 => ;
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 = () => (
hi |
);
render(, scratch);
expect(console.error).to.be.calledOnce;
});
it('missing with ', () => {
const Table = () => (
hi |
);
render(, scratch);
expect(console.error).to.be.calledOnce;
});
it('missing with ', () => {
const Table = () => (
hi |
);
render(, scratch);
expect(console.error).to.be.calledOnce;
});
it('missing with ', () => {
const Table = () => (
hi |
);
render(, scratch);
expect(console.error).to.be.calledOnce;
});
it('missing ', () => {
const Table = () => (
);
render(, scratch);
expect(console.error).to.be.calledOnce;
});
it('missing
with td component', () => {
const Cell = ({ children }) => {children} | ;
const Table = () => (
);
render(, scratch);
expect(console.error).to.be.calledOnce;
});
it('missing
with th component', () => {
const Cell = ({ children }) => {children} | ;
const Table = () => (
);
render(, scratch);
expect(console.error).to.be.calledOnce;
});
it('Should accept instead of | in ', () => {
const Table = () => (
);
render(, scratch);
expect(console.error).to.not.be.called;
});
it('Accepts well formed table with TD components', () => {
const Cell = ({ children }) => {children} | ;
const Table = () => (
);
render(, scratch);
expect(console.error).to.not.be.called;
});
it('Accepts well formed table', () => {
const Table = () => (
);
render(, scratch);
expect(console.error).to.not.be.called;
});
it('Accepts minimal well formed table', () => {
const Table = () => (
);
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;
});
});
});
|