Files
headlessui/packages/@headlessui-react/src/utils/render.test.tsx
T
Robin Malfait 3e19aa5c97 Properly merge incoming props (#1265)
* rename inconsistent `passThroughProps` and `passthroughProps` to more
concise `incomingProps`

This is going to make a bit more sense in the next commits of this
branch, hold on!

* split props into `propsWeControl` and `propsTheyControl`

This will allow us to merge the props with a bit more control. Instead
of overriding every prop from the user' props with our props, we can now
merge event listeners.

* update `render` API to accept `propsWeControl` and `propsTheyControl`

* improve the merge logic

This will essentially do the exact same thing we were doing before:
```js
let props = { ...propsTheyControl, ...propsWeControl }
```

But instead of overriding everything, we will merge the event listener
related props like `onClick`, `onKeyDown`, ...

* fix typo in tests

* simplify naming

- Rename `propsWeControl` to `ourProps`
- Rename `propsTheyControl` to `theirProps`

* update changelog
2022-03-22 17:32:11 +01:00

489 lines
12 KiB
TypeScript

import React, { ElementType, createRef, Ref, Fragment } from 'react'
import { render as testRender, prettyDOM, getByTestId } from '@testing-library/react'
import { suppressConsoleLogs } from '../test-utils/suppress-console-logs'
import { render, Features, PropsForFeatures } from './render'
import { Props, Expand } from '../types'
function contents() {
return prettyDOM(getByTestId(document.body, 'wrapper'), undefined, {
highlight: false,
})
}
describe('Default functionality', () => {
let slot = {}
function Dummy<TTag extends ElementType = 'div'>(
props: Props<TTag> & Partial<{ a: any; b: any; c: any }>
) {
return (
<div data-testid="wrapper">
{render({
ourProps: {},
theirProps: props,
slot,
defaultTag: 'div',
name: 'Dummy',
})}
</div>
)
}
it('should be possible to render a dummy component', () => {
testRender(<Dummy />)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<div />
</div>"
`)
})
it('should be possible to render a dummy component with some children as a callback', () => {
expect.assertions(2)
testRender(
<Dummy>
{(data) => {
expect(data).toBe(slot)
return <span>Contents</span>
}}
</Dummy>
)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<div>
<span>
Contents
</span>
</div>
</div>"
`)
})
it('should be possible to add a ref with a different name', () => {
let ref = createRef()
function MyComponent<T extends ElementType = 'div'>({
innerRef,
...props
}: Props<T> & { innerRef: Ref<HTMLDivElement> }) {
return <div ref={innerRef} {...props} />
}
function OtherDummy<TTag extends ElementType = 'div'>(props: Props<TTag>) {
return (
<div data-testid="wrapper">
{render({
ourProps: { ref },
theirProps: props,
slot,
defaultTag: 'div',
name: 'OtherDummy',
})}
</div>
)
}
testRender(
<OtherDummy as={MyComponent} refName="potato">
Contents
</OtherDummy>
)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<div
potato=\\"[object Object]\\"
>
Contents
</div>
</div>"
`)
})
it('should be possible to passthrough props to a dummy component', () => {
testRender(<Dummy a={1} b={2} c={3} />)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<div
a=\\"1\\"
b=\\"2\\"
c=\\"3\\"
/>
</div>"
`)
})
it('should be possible to change the underlying DOM node using the `as` prop', () => {
testRender(<Dummy as="button" />)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<button />
</div>"
`)
})
it('should be possible to change the underlying DOM node using the `as` prop and still have a function as children', () => {
testRender(<Dummy as="button">{() => <span>Contents</span>}</Dummy>)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<button>
<span>
Contents
</span>
</button>
</div>"
`)
})
it('should be possible to render the children only when the `as` prop is set to Fragment', () => {
testRender(<Dummy as={Fragment}>Contents</Dummy>)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
Contents
</div>"
`)
})
it('should forward all the props to the first child when using an as={Fragment}', () => {
testRender(
<Dummy as={Fragment} a={1} b={1}>
{() => <span>Contents</span>}
</Dummy>
)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<span
a=\\"1\\"
b=\\"1\\"
>
Contents
</span>
</div>"
`)
})
it(
'should error when we are rendering a Fragment with multiple children',
suppressConsoleLogs(() => {
expect.assertions(1)
return expect(() => {
testRender(
// @ts-expect-error className cannot be applied to a Fragment
<Dummy as={Fragment} className="p-12">
<span>Contents A</span>
<span>Contents B</span>
</Dummy>
)
}).toThrowError(
new Error(
[
'Passing props on "Fragment"!',
'',
'The current component <Dummy /> is rendering a "Fragment".',
'However we need to passthrough the following props:',
' - className',
'',
'You can apply a few solutions:',
' - Add an `as="..."` prop, to ensure that we render an actual element instead of a "Fragment".',
' - Render a single element as the child so that we can forward the props onto that element.',
].join('\n')
)
)
})
)
it("should not error when we are rendering a Fragment with multiple children when we don't passthrough additional props", () => {
testRender(
<Dummy as={Fragment}>
<span>Contents A</span>
<span>Contents B</span>
</Dummy>
)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<span>
Contents A
</span>
<span>
Contents B
</span>
</div>"
`)
})
it(
'should error when we are applying props to a Fragment when we do not have a dedicated element',
suppressConsoleLogs(() => {
expect.assertions(1)
return expect(() => {
testRender(
// @ts-expect-error className cannot be applied to a Fragment
<Dummy as={Fragment} className="p-12">
Contents
</Dummy>
)
}).toThrowError(
new Error(
[
'Passing props on "Fragment"!',
'',
'The current component <Dummy /> is rendering a "Fragment".',
'However we need to passthrough the following props:',
' - className',
'',
'You can apply a few solutions:',
' - Add an `as="..."` prop, to ensure that we render an actual element instead of a "Fragment".',
' - Render a single element as the child so that we can forward the props onto that element.',
].join('\n')
)
)
})
)
})
// ---
function testStaticFeature(Dummy: Function) {
it('should be possible to render a `static` dummy component (show = true)', () => {
testRender(
<Dummy show={true} static>
Contents
</Dummy>
)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<div>
Contents
</div>
</div>"
`)
})
it('should be possible to render a `static` dummy component (show = false)', () => {
testRender(
<Dummy show={false} static>
Contents
</Dummy>
)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<div>
Contents
</div>
</div>"
`)
})
}
// With the `static` keyword, the user is always in control. When we internally decide to show the
// component or hide it then it won't have any effect. This is useful for when you want to wrap your
// component in a Transition for example so that the Transition component can control the
// showing/hiding based on the `show` prop AND the state of the transition.
describe('Features.Static', () => {
let slot = {}
let EnabledFeatures = Features.Static
function Dummy<TTag extends ElementType = 'div'>(
props: Expand<Props<TTag> & PropsForFeatures<typeof EnabledFeatures>> & { show: boolean }
) {
let { show, ...rest } = props
return (
<div data-testid="wrapper">
{render({
ourProps: {},
theirProps: rest,
slot,
defaultTag: 'div',
features: EnabledFeatures,
visible: show,
name: 'Dummy',
})}
</div>
)
}
testStaticFeature(Dummy)
})
// ---
function testRenderStrategyFeature(Dummy: Function) {
describe('Unmount render strategy', () => {
it('should be possible to render an `unmount` dummy component (show = true)', () => {
testRender(
<Dummy show={true} unmount>
Contents
</Dummy>
)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<div>
Contents
</div>
</div>"
`)
})
it('should be possible to render an `unmount` dummy component (show = false)', () => {
testRender(
<Dummy show={false} unmount>
Contents
</Dummy>
)
// No contents, because we unmounted!
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
/>"
`)
})
})
describe('Hidden render strategy', () => {
it('should be possible to render an `unmount={false}` dummy component (show = true)', () => {
testRender(
<Dummy show={true} unmount={false}>
Contents
</Dummy>
)
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<div>
Contents
</div>
</div>"
`)
})
it('should be possible to render an `unmount={false}` dummy component (show = false)', () => {
testRender(
<Dummy show={false} unmount={false}>
Contents
</Dummy>
)
// We do have contents, but it is marked as hidden!
expect(contents()).toMatchInlineSnapshot(`
"<div
data-testid=\\"wrapper\\"
>
<div
hidden=\\"\\"
style=\\"display: none;\\"
>
Contents
</div>
</div>"
`)
})
})
}
describe('Features.RenderStrategy', () => {
let slot = {}
let EnabledFeatures = Features.RenderStrategy
function Dummy<TTag extends ElementType = 'div'>(
props: Expand<Props<TTag> & PropsForFeatures<typeof EnabledFeatures>> & { show: boolean }
) {
let { show, ...rest } = props
return (
<div data-testid="wrapper">
{render({
ourProps: {},
theirProps: rest,
slot,
defaultTag: 'div',
features: EnabledFeatures,
visible: show,
name: 'Dummy',
})}
</div>
)
}
testRenderStrategyFeature(Dummy)
})
// ---
// This should enable the `static` and `unmount` features. However they can't be used together!
describe('Features.Static | Features.RenderStrategy', () => {
let slot = {}
let EnabledFeatures = Features.Static | Features.RenderStrategy
function Dummy<TTag extends ElementType = 'div'>(
props: Expand<Props<TTag> & PropsForFeatures<typeof EnabledFeatures>> & { show: boolean }
) {
let { show, ...rest } = props
return (
<div data-testid="wrapper">
{render({
ourProps: {},
theirProps: rest,
slot,
defaultTag: 'div',
features: EnabledFeatures,
visible: show,
name: 'Dummy',
})}
</div>
)
}
// TODO: Can we "legit" test this? 🤔
it('should result in a typescript error', () => {
testRender(
// @ts-expect-error static & unmount together are incompatible
<Dummy show={false} static unmount>
Contents
</Dummy>
)
})
// To avoid duplication, and to make sure that the features tested in isolation can also be
// re-used when they are combined.
testStaticFeature(Dummy)
testRenderStrategyFeature(Dummy)
})