-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Separate debug tests into multiple files and fix some bugs in debug #2047
Changes from 11 commits
2270b2b
989f10f
e5f750c
f408eec
e66d37e
2a66c26
e3c2308
06e7f6c
cc2e5c8
e4dd728
b0d6228
96ab769
51057f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,8 @@ import { getDisplayName } from './devtools/custom'; | |
import { options, Component } from 'preact'; | ||
import { ELEMENT_NODE, DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE } from './constants'; | ||
|
||
const isWeakMapSupported = typeof WeakMap === 'function'; | ||
|
||
function getClosestDomNodeParent(parent) { | ||
if (!parent) return {}; | ||
if (typeof parent.type === 'function') { | ||
|
@@ -19,7 +21,11 @@ export function initDebug() { | |
let oldCatchError = options._catchError; | ||
let oldRoot = options._root; | ||
let oldHook = options._hook; | ||
const warnedComponents = { useEffect: {}, useLayoutEffect: {}, lazyPropTypes: {} }; | ||
const warnedComponents = !isWeakMapSupported ? null : ({ | ||
useEffect: new WeakMap(), | ||
useLayoutEffect: new WeakMap(), | ||
lazyPropTypes: new WeakMap() | ||
}); | ||
|
||
options._catchError = (error, vnode, oldVNode) => { | ||
let component = vnode && vnode._component; | ||
|
@@ -58,7 +64,7 @@ export function initDebug() { | |
} | ||
if (!isValid) throw new Error(` | ||
Expected a valid HTML node as a second argument to render. | ||
Received ${parentNode} instead: render(<${vnode.type.name || vnode.type} />, ${parentNode}); | ||
Received ${parentNode} instead: render(<${getDisplayName(vnode)} />, ${parentNode}); | ||
`); | ||
|
||
if (oldRoot) oldRoot(vnode, parentNode); | ||
|
@@ -147,12 +153,12 @@ export function initDebug() { | |
|
||
// Check prop-types if available | ||
if (typeof vnode.type==='function' && vnode.type.propTypes) { | ||
if (vnode.type.displayName === 'Lazy' && !warnedComponents.lazyPropTypes[vnode.type]) { | ||
if (vnode.type.displayName === 'Lazy' && warnedComponents && !warnedComponents.lazyPropTypes.has(vnode.type)) { | ||
const m = 'PropTypes are not supported on lazy(). Use propTypes on the wrapped component itself. '; | ||
try { | ||
const lazyVNode = vnode.type(); | ||
warnedComponents.lazyPropTypes[vnode.type] = true; | ||
console.warn(m + 'Component wrapped in lazy() is ' + (lazyVNode.type.displayName || lazyVNode.type.name)); | ||
warnedComponents.lazyPropTypes.set(vnode.type, true); | ||
console.warn(m + 'Component wrapped in lazy() is ' + getDisplayName(lazyVNode)); | ||
} | ||
catch (promise) { | ||
console.warn(m + 'We will log the wrapped component\'s name once it is loaded.'); | ||
|
@@ -223,8 +229,6 @@ export function initDebug() { | |
}); | ||
} | ||
|
||
if (oldDiffed) oldDiffed(vnode); | ||
|
||
if (vnode._component && vnode._component.__hooks) { | ||
let hooks = vnode._component.__hooks; | ||
if (Array.isArray(hooks._list)) { | ||
|
@@ -239,8 +243,8 @@ export function initDebug() { | |
} | ||
if (Array.isArray(hooks._pendingEffects)) { | ||
hooks._pendingEffects.forEach((effect) => { | ||
if ((!effect._args || !Array.isArray(effect._args)) && !warnedComponents.useEffect[vnode.type]) { | ||
warnedComponents.useEffect[vnode.type] = true; | ||
if (!Array.isArray(effect._args) && warnedComponents && !warnedComponents.useEffect.has(vnode.type)) { | ||
warnedComponents.useEffect.set(vnode.type, true); | ||
console.warn('You should provide an array of arguments as the second argument to the "useEffect" hook.\n\n' + | ||
'Not doing so will invoke this effect on every render.\n\n' + | ||
'This effect can be found in the render of ' + getDisplayName(vnode) + '.'); | ||
|
@@ -249,8 +253,8 @@ export function initDebug() { | |
} | ||
if (Array.isArray(hooks._pendingLayoutEffects)) { | ||
hooks._pendingLayoutEffects.forEach((layoutEffect) => { | ||
if ((!layoutEffect._args || !Array.isArray(layoutEffect._args)) && !warnedComponents.useLayoutEffect[vnode.type]) { | ||
warnedComponents.useLayoutEffect[vnode.type] = true; | ||
if (!Array.isArray(layoutEffect._args) && warnedComponents && !warnedComponents.useLayoutEffect.has(vnode.type)) { | ||
warnedComponents.useLayoutEffect.set(vnode.type, true); | ||
console.warn('You should provide an array of arguments as the second argument to the "useLayoutEffect" hook.\n\n' + | ||
'Not doing so will invoke this effect on every render.\n\n' + | ||
'This effect can be found in the render of ' + getDisplayName(vnode) + '.'); | ||
|
@@ -259,6 +263,8 @@ export function initDebug() { | |
} | ||
} | ||
|
||
if (oldDiffed) oldDiffed(vnode); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hooks use the This test passed previously because before each test we would delete the diffed option which deleted the hooks option and we never set it back. So the diffed hook option never ran in the old debug tests. Now it does in the new There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh that's a good catch 👍 |
||
|
||
if (vnode._children != null) { | ||
const keys = []; | ||
for (let i = 0; i < vnode._children.length; i++) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { createElement, render } from 'preact/compat'; | ||
import { initDebug } from '../../src/debug'; | ||
import { setupScratch, teardown } from '../../../test/_util/helpers'; | ||
|
||
/** @jsx createElement */ | ||
|
||
initDebug(); | ||
|
||
describe('debug with compat', () => { | ||
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(() => { | ||
(console.error).restore(); | ||
console.warn.restore(); | ||
teardown(scratch); | ||
}); | ||
|
||
// TODO: This test only passed before because the old test first rendered | ||
// a string ref and expected an error. Then it rerendered a vnode with the | ||
// same string ref and did not expect an error. The rerender didn't throw | ||
// an error because the diff saw the oldVNode and the newVNode as having | ||
// the same ref ("a") and so it didn't try to apply it. It had nothing to do | ||
// with the "$$typeof" property being present which the old test seems to imply | ||
it.skip('should not print an error on string refs', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preact core doesn't support string refs and compat doesn't add support for it. No one has complained yet so should we leave this feature out for now and only worry about bringing it back if someone needs it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We deprecated string refs with X. I was the one who removed it :D We got 2 users who noticed so far, but they could easily update by switching to a function callback or the |
||
let fn = () => render(<div ref="a" />, scratch); | ||
expect(fn).to.not.throw(); | ||
expect(console.error).to.not.be.called; | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { createElement, render, Component } from 'preact'; | ||
import { | ||
useState, | ||
useEffect, | ||
useLayoutEffect, | ||
useMemo, | ||
useCallback | ||
} from 'preact/hooks'; | ||
import { initDebug } from '../../src/debug'; | ||
import { act } from 'preact/test-utils'; | ||
import { setupScratch, teardown } from '../../../test/_util/helpers'; | ||
|
||
/** @jsx createElement */ | ||
|
||
initDebug(); | ||
|
||
describe('debug with hooks', () => { | ||
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(() => { | ||
(console.error).restore(); | ||
console.warn.restore(); | ||
teardown(scratch); | ||
}); | ||
|
||
// TODO: Fix this test. It only passed before because App was the first component | ||
// into render so currentComponent in hooks/index.js wasn't set yet. However, | ||
// any children under App wouldn't have thrown the error if they did what App | ||
// did because currentComponent would be set to App. | ||
// In other words, hooks never clear currentComponent so once it is set, it won't | ||
// be unset | ||
it.skip('should throw an error when using a hook outside a render', () => { | ||
const Foo = props => props.children; | ||
class App extends Component { | ||
componentWillMount() { | ||
useState(); | ||
} | ||
|
||
render() { | ||
return <p>test</p>; | ||
} | ||
} | ||
const fn = () => | ||
act(() => | ||
render( | ||
<Foo> | ||
<App /> | ||
</Foo>, | ||
scratch | ||
) | ||
); | ||
expect(fn).to.throw(/Hook can only be invoked from render/); | ||
}); | ||
|
||
// TODO: Fix this test. It only passed before because render was never called. | ||
// Once render is called, currentComponent is set and never unset so calls to | ||
// hooks outside of components would still work. | ||
it.skip('should throw an error when invoked outside of a component', () => { | ||
function Foo(props) { | ||
useEffect(() => {}); // Pretend to use a hook | ||
return props.children; | ||
} | ||
|
||
const fn = () => | ||
act(() => { | ||
render(<Foo>Hello!</Foo>, scratch); | ||
useState(); | ||
}); | ||
expect(fn).to.throw(/Hook can only be invoked from render/); | ||
}); | ||
|
||
it('should warn for argumentless useEffect hooks', () => { | ||
const App = () => { | ||
const [state] = useState('test'); | ||
useEffect(() => 'test'); | ||
return <p>{state}</p>; | ||
}; | ||
render(<App />, scratch); | ||
expect(warnings[0]).to.match(/You should provide an array of arguments/); | ||
render(<App />, scratch); | ||
expect(warnings[1]).to.be.undefined; | ||
}); | ||
|
||
it('should warn for argumentless useLayoutEffect hooks', () => { | ||
const App = () => { | ||
const [state] = useState('test'); | ||
useLayoutEffect(() => 'test'); | ||
return <p>{state}</p>; | ||
}; | ||
render(<App />, scratch); | ||
expect(warnings[0]).to.match(/You should provide an array of arguments/); | ||
render(<App />, scratch); | ||
expect(warnings[1]).to.be.undefined; | ||
}); | ||
|
||
it('should not warn for argumented effect hooks', () => { | ||
const App = () => { | ||
const [state] = useState('test'); | ||
useLayoutEffect(() => 'test', []); | ||
useEffect(() => 'test', [state]); | ||
return <p>{state}</p>; | ||
}; | ||
const fn = () => act(() => render(<App />, scratch)); | ||
expect(fn).to.not.throw(); | ||
}); | ||
|
||
it('should warn for useless useMemo calls', () => { | ||
const App = () => { | ||
const [people] = useState([40, 20, 60, 80]); | ||
const retiredPeople = useMemo(() => people.filter(x => x >= 60)); | ||
const cb = useCallback(() => () => 'test'); | ||
return <p onClick={cb}>{retiredPeople.map(x => x)}</p>; | ||
}; | ||
render(<App />, scratch); | ||
expect(warnings.length).to.equal(2); | ||
}); | ||
|
||
it('should warn when non-array args is passed', () => { | ||
const App = () => { | ||
const foo = useMemo(() => 'foo', 12); | ||
return <p>{foo}</p>; | ||
}; | ||
render(<App />, scratch); | ||
expect(warnings[0]).to.match(/without passing arguments/); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, we used an object to store a map of component instances to booleans indicating if we had warned on them already. Setting a function as a property on an object calls
.toString
on the function and uses that as the key. This behavior doesn't work if multiple instances of the same constructor function have warnings cuz each instance uses will have the same constructor function string value.The previous tests passed because we cleared and reset the options before each tests meaning these object got reset for each test. However, in the new separated tests, debug is only initialized once, and the lazy prop types tests started failing cuz all Lazy components share the same constructor function and so different instances of lazy in different tests wouldn't warn when we expected them to.
Using a WeakMap fixes this problem since each component instance (regardless of whether they share the same constructor function) now has its own key/value pair in the WeakMap.
IE11 has basic support for WeakMaps so I think its okay to use them in debug.