import { getByTestId, prettyDOM, render as testRender } from '@testing-library/react' import React, { Fragment, createRef, type ElementType, type Ref } from 'react' import { suppressConsoleLogs } from '../test-utils/suppress-console-logs' import type { Expand, Props } from '../types' import { RenderFeatures, render, type PropsForFeatures } from './render' function contents(id = 'wrapper') { return prettyDOM(getByTestId(document.body, id), undefined, { highlight: false, }) } describe('Default functionality', () => { let slot = {} function Dummy( props: Props & Partial<{ a: any; b: any; c: any }> ) { return (
{render({ ourProps: {}, theirProps: props, slot, defaultTag: 'div', name: 'Dummy', })}
) } function DummyWithClassName( props: Props & Partial<{ className: string | (() => string) }> ) { return (
{render({ ourProps: {}, theirProps: props, slot, defaultTag: 'div', name: 'Dummy', })}
) } it('should be possible to render a dummy component', () => { testRender() expect(contents()).toMatchSnapshot() }) it('should be possible to merge classes when rendering', () => { testRender(
) expect(contents('wrapper-with-class')).toMatchSnapshot() }) it('should be possible to merge class fns when rendering', () => { testRender( 'test-inner'}> ) expect(contents('wrapper-with-class')).toMatchSnapshot() }) it('should be possible to render a dummy component with some children as a callback', () => { expect.assertions(2) testRender( {(data) => { expect(data).toBe(slot) return Contents }} ) expect(contents()).toMatchSnapshot() }) it('should be possible to add a ref with a different name', () => { let ref = createRef() function MyComponent({ innerRef, ...props }: Props }>) { return
} function OtherDummy(props: Props) { return (
{render({ ourProps: { ref }, theirProps: props, slot, defaultTag: 'div', name: 'OtherDummy', })}
) } testRender( Contents ) expect(contents()).toMatchSnapshot() }) it('should be possible to passthrough props to a dummy component', () => { testRender() expect(contents()).toMatchSnapshot() }) it('should be possible to change the underlying DOM node using the `as` prop', () => { testRender() expect(contents()).toMatchSnapshot() }) it('should be possible to change the underlying DOM node using the `as` prop and still have a function as children', () => { testRender({() => Contents}) expect(contents()).toMatchSnapshot() }) it('should be possible to render the children only when the `as` prop is set to Fragment', () => { testRender(Contents) expect(contents()).toMatchSnapshot() }) it('should forward all the props to the first child when using an as={Fragment}', () => { testRender( {() => Contents} ) expect(contents()).toMatchSnapshot() }) it('should forward boolean values from `slot` as data attributes', () => { function Dummy( props: Props & Partial<{ a: any; b: any; c: any }> ) { return (
{render({ ourProps: {}, theirProps: props, slot: { a: true, b: false, c: true }, defaultTag: 'div', name: 'Dummy', })}
) } testRender({() => Contents}) expect(contents()).toMatchSnapshot() }) it('should prefer user provided data attributes over the ones we set automatically', () => { function Dummy( props: Props & Partial<{ a: any; b: any; c: any }> ) { return (
{render({ ourProps: {}, theirProps: props, slot: { accept: true }, defaultTag: 'div', name: 'Dummy', })}
) } testRender({() => Contents}) expect(contents()).toMatchSnapshot() }) 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 Contents A Contents B ) }).toThrow( new Error( [ 'Passing props on "Fragment"!', '', 'The current component 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( Contents A Contents B ) expect(contents()).toMatchSnapshot() }) 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 Contents ) }).toThrow( new Error( [ 'Passing props on "Fragment"!', '', 'The current component 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: (props: any) => React.JSX.Element) { it('should be possible to render a `static` dummy component (show = true)', () => { testRender( Contents ) expect(contents()).toMatchSnapshot() }) it('should be possible to render a `static` dummy component (show = false)', () => { testRender( Contents ) expect(contents()).toMatchSnapshot() }) } // 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 = RenderFeatures.Static function Dummy( props: Expand & PropsForFeatures> & { show: boolean } ) { let { show, ...rest } = props return (
{render({ ourProps: {}, theirProps: rest, slot, defaultTag: 'div', features: EnabledFeatures, visible: show, name: 'Dummy', })}
) } testStaticFeature(Dummy) }) // --- function testRenderStrategyFeature(Dummy: (props: any) => React.JSX.Element) { describe('Unmount render strategy', () => { it('should be possible to render an `unmount` dummy component (show = true)', () => { testRender( Contents ) expect(contents()).toMatchSnapshot() }) it('should be possible to render an `unmount` dummy component (show = false)', () => { testRender( Contents ) // No contents, because we unmounted! expect(contents()).toMatchSnapshot() }) }) describe('Hidden render strategy', () => { it('should be possible to render an `unmount={false}` dummy component (show = true)', () => { testRender( Contents ) expect(contents()).toMatchSnapshot() }) it('should be possible to render an `unmount={false}` dummy component (show = false)', () => { testRender( Contents ) // We do have contents, but it is marked as hidden! expect(contents()).toMatchSnapshot() }) }) } describe('Features.RenderStrategy', () => { let slot = {} let EnabledFeatures = RenderFeatures.RenderStrategy function Dummy( props: Expand & PropsForFeatures> & { show: boolean } ) { let { show, ...rest } = props return (
{render({ ourProps: {}, theirProps: rest, slot, defaultTag: 'div', features: EnabledFeatures, visible: show, name: 'Dummy', })}
) } 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 = RenderFeatures.Static | RenderFeatures.RenderStrategy function Dummy( props: Expand & PropsForFeatures> & { show: boolean } ) { let { show, ...rest } = props return (
{render({ ourProps: {}, theirProps: rest, slot, defaultTag: 'div', features: EnabledFeatures, visible: show, name: '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) })