/* eslint-disable import/export */ import { cleanup, render } from "@testing-library/react"; import * as React from "react"; import { afterEach } from "vitest"; import { template, useSlot, OverrideNode, type CreateTemplate, type Slot, type SlotChildren, } from "../src"; afterEach(() => { cleanup(); }); describe("Default slot", () => { type Props = { children?: SlotChildren | Slot<"named">>; }; function DefaultSlotTest({ children }: Props) { const { slot } = useSlot(children); return slot.default(
Default Content
, { prop: "test" }); } const defaultTestTemplate = template as CreateTemplate; test("renders default templates, functions, elements with unspecified react-slot attribute, react-slot='default', regular react nodes", () => { const { asFragment } = render(
Default Template #1
{() =>
Default Template #2
}
{() => { return
Function
; }}
Should not be rendered
No react-slot attribute
react-slots=default
JSXText {"string"} {42}
, ); expect(asFragment()).toMatchInlineSnapshot(`
Default Template #1
Default Template #2
Function
No react-slot attribute
react-slots=default
JSXTextstring42
`); }); test("Calls child function with the right argument", () => { const { asFragment } = render( {({ prop }) =>
Function arg: {prop}
} {({ prop }) =>
Template function arg: {prop}
}
, ); expect(asFragment()).toMatchInlineSnapshot(`
Function arg: test
Template function arg: test
`); }); test("Filters out null, undefined, boolean, keeps 0", () => { const { asFragment } = render( {null} {undefined}
Just something to divide the content
{true} {false} {0}
, ); expect(asFragment()).toMatchInlineSnapshot(`
Just something to divide the content
0
`); }); }); test("`hasSlot` keeps track of whether content is provided", () => { type Props = { children?: SlotChildren>; }; function HasSlotTest({ children }: Props) { const { hasSlot } = useSlot(children); return <>{hasSlot.default ? "true" : "false"}; } const hasSlotTestTemplate = template as CreateTemplate; const { asFragment, rerender } = render( {null} {undefined} {true} {false} Should not count , ); expect(asFragment()).toMatchInlineSnapshot(` false `); rerender( {null} {undefined} {true} {false} {0} , ); expect(asFragment()).toMatchInlineSnapshot(` true `); rerender( Should count , ); expect(asFragment()).toMatchInlineSnapshot(` true `); // JSXText rerender( ); expect(asFragment()).toMatchInlineSnapshot(` true `); rerender(); expect(asFragment()).toMatchInlineSnapshot(` false `); }); describe("Template component", () => { type Props = { children?: SlotChildren< Slot<{ foo: string }> | Slot<"named", { bar: string }> >; }; function TemplateComponentTest({ children }: Props) { const { slot } = useSlot(children); return ( <>
{slot.default("default content", { foo: "foo" })}
{slot.named("default content", { bar: "bar" })}
); } const testTemplate = template as CreateTemplate; test("child function is called with right arguments", () => { const { asFragment } = render( {({ foo }) => foo} {({ bar }) => bar} , ); expect(asFragment()).toMatchInlineSnapshot(`
foo
bar
`); }); test("renders children when not a function", () => { const { asFragment } = render( default template Named template , ); expect(asFragment()).toMatchInlineSnapshot(`
default template
Named template
`); }); test("renders `as` element with correct props when `as` is intrinsic element name", () => { const { asFragment } = render( {({ foo }) => foo} , ); expect(asFragment()).toMatchInlineSnapshot(`
foo
`); }); test("renders `as` element with correct props when `as` is a custom component", () => { function CustomComp({ prop, children, }: { prop: number; children: React.ReactNode; }) { return ( <>
{children}
{prop}
); } const { asFragment } = render( {({ foo }) => foo} A react node , ); expect(asFragment()).toMatchInlineSnapshot(`
foo
42
A react node
69
`); }); test("renders nothing when no children provided", () => { const { asFragment } = render( , ); expect(asFragment()).toMatchInlineSnapshot(`
default content
`); }); test("renders 0", () => { const { asFragment } = render( 0 , ); expect(asFragment()).toMatchInlineSnapshot(`
default content
0
`); }); test("child function is called with an empty object when no props are provided", () => { function Test({ children }: any) { const { slot } = useSlot(children); return slot.default(); } render( {(prop: {}) => { expect(prop).toEqual({}); return null; }} {(prop: {}) => { expect(prop).toEqual({}); return null; }} , ); }); test("renders an element with a key if specified", () => { type Props = { children?: SlotChildren>; }; function SlotTest({ children }: Props) { const { slot } = useSlot(children); const defaultElement = slot.default(); expect(defaultElement?.props.children[0].key).toBe(".$1"); const namedElement = slot.named(); expect(namedElement?.props.children[0].key).toBe(".$2"); return defaultElement; } render( , ); }); }); describe("Slot function", () => { test("renders provided content when called with no arguments", () => { function SlotTest({ children }: any) { const { slot } = useSlot(children); return slot.default(); } const { asFragment } = render(foo); expect(asFragment()).toMatchInlineSnapshot(` foo `); }); test("renders nothing when no arguments are provided and content is not provided", () => { function SlotTest({ children }: any) { const { slot } = useSlot(children); return slot.default(); } const { asFragment } = render(); expect(asFragment()).toMatchInlineSnapshot(""); }); test("renders default content when no content is provided", () => { function SlotTest({ children }: any) { const { slot } = useSlot(children); return slot.default("default content"); } const { asFragment } = render(); expect(asFragment()).toMatchInlineSnapshot(` default content `); }); test("renders 0 when provided as default content", () => { function SlotTest({ children }: any) { const { slot } = useSlot(children); return slot.default(0); } const { asFragment } = render(); expect(asFragment()).toMatchInlineSnapshot(` 0 `); }); test("calls template function with props", () => { function SlotTest({ children }: any) { const { slot } = useSlot(children); return slot.default(null, { prop: "knock " }); } const { asFragment } = render( {({ prop }: any) => prop} {({ prop }: any) => prop} , ); expect(asFragment()).toMatchInlineSnapshot(` knock knock `); }); test("renders an element with a key when provided with props. Props won't include key when executed", () => { type Props = { children: SlotChildren>; }; function SlotTest({ children }: Props) { const { slot } = useSlot(children); const element = slot.default(null, { prop: "foo", key: 1 }); expect(element?.key).toBe("1"); return element; } render( {(props) => { expect(props).toEqual({ prop: "foo" }); return null; }} , ); }); test("renders an element with a key when provided as third argument", () => { type Props = { children?: SlotChildren>; }; function SlotTest({ children }: Props) { const { slot } = useSlot(children); const element = slot.default(null, { prop: "foo" }, 1); expect(element?.key).toBe("1"); return element; } render(); }); test("renders an element with the first key when provided with props as well as third argument", () => { type Props = { children?: SlotChildren>; }; function SlotTest({ children }: Props) { const { slot } = useSlot(children); const element = slot.default(null, { prop: "foo", key: 1 }, 2); expect(element?.key).toBe("1"); return element; } render(); }); test("can render the same slot multiple times and provide different props", () => { type Props = { children?: SlotChildren>; }; function SlotTest({ children }: Props) { const { slot } = useSlot(children); return ( <> {slot.default(null, { prop: "foo" })} {slot.default(null, { prop: "bar" })} ); } const { asFragment } = render( {({ prop }) =>
{prop}
}
, ); expect(asFragment()).toMatchInlineSnapshot(`
foo
bar
`); }); test("accepts slot-name prop that can be used to specify slot for child", () => { function Child({ children }: any) { const { slot } = useSlot(children); return slot.foo(); } function Parent({ children }: any) { const { slot } = useSlot(children); return {slot.default(null, { "slot-name": "foo" })}; } const { asFragment } = render(
content
, ); expect(asFragment()).toMatchInlineSnapshot(`
content
`); }); }); describe("Template as slot function", () => { type ChildProps = { children: SlotChildren | Slot<"named">>; }; const childTemplate = template as CreateTemplate; function Child({ children }: ChildProps) { const { slot } = useSlot(children); return ( <>
{slot.default( "child's default content", { foo: "foo", bar: "bar" }, 1, )}
{slot.named(<>child's default content)}
); } type TemplateAsSlotTestProps = { children: SlotChildren< | Slot<{ foo: string; bar: string; baz: string }> | Slot<"named", { baz?: string }> >; }; function TemplateAsSlotTest({ children }: TemplateAsSlotTestProps) { const { slot } = useSlot(children); return ( Own default content ); } test("renders parent's content into child's slot", () => { const { asFragment } = render( Rendered from parent Rendered from parent , ); expect(asFragment()).toMatchInlineSnapshot(`
Rendered from parent
`); }); test("merges child slot's props and own props. Own props overrides child props. Keys are omitted", () => { render( {(props: any) => { expect(props).toEqual({ foo: "overridden foo", bar: "bar", baz: "baz", }); return null; }} , ); }); test("renders child slot's default content when no children is provided", () => { const { asFragment } = render( // testing the named slot only , ); expect(asFragment()).toMatchInlineSnapshot(`
child's default content
`); }); test("renders own children even if child slot specified default content", () => { const { asFragment } = render( // testing the default slot only , ); expect(asFragment()).toMatchInlineSnapshot(`
Own default content
`); }); test("respects parent template's keys, it's own keys and child slot's keys", () => { function Child({ children }: any) { const { slot } = useSlot(children); const element = slot.default(null, null, 1); expect(element?.key).toBe("1"); expect(element?.props.children[0].key).toBe(".$2"); expect(element?.props.children[0].props.children[0].key).toBe(".$3"); return element; } function KeyTest({ children }: any) { const { slot } = useSlot(children); return ( ); } render( , ); }); }); describe("OverrideNode", () => { test("is ignored when it has no props", () => { function Parent({ children }: any) { const { slot } = useSlot(children); return (
); } function Child({ children }: any) { const { slot } = useSlot(children); return (
{slot.name(fallback content)}
); } const { asFragment: asFragment1 } = render(
, ); const { asFragment: asFragment2 } = render(); const { asFragment: asFragment3 } = render(content); const { asFragment: asFragment4 } = render(); expect(asFragment1()).toMatchInlineSnapshot(`
`); expect(asFragment2()).toMatchInlineSnapshot(`
fallback content
`); expect(asFragment3()).toMatchInlineSnapshot(`
content
`); expect(asFragment4()).toMatchInlineSnapshot(`
fallback content
`); }); test("enforces node type", () => { { function Child({ children }: any) { const { slot } = useSlot(children); return (
{slot.name( , )}
); } expect(() => render()).toThrow(); expect(() => render( , ), ).not.toThrow(); expect(() => render(string)).toThrow(); } { function Child({ children }: any) { const { slot } = useSlot(children); return (
{slot.default( fallback content , )}
); } const { asFragment } = render( This is kept removed {42} {true} , ); expect(asFragment()).toMatchInlineSnapshot(`
This is kept 42
`); } { function Child({ children }: any) { const { slot } = useSlot(children); return (
{slot.default(
, )}
); } const { asFragment } = render( , ); expect(asFragment()).toMatchInlineSnapshot(`
`); const { asFragment: asFragment2 } = render(); expect(asFragment2()).toMatchInlineSnapshot(`
`); } }); test("overrides node when the node prop is specified, and content matches allowedNodes", () => { function Child({ children }: any) { const { slot } = useSlot(children); return ( <> {slot.default( { return
{node}
; }} />, )} ); } const { asFragment } = render(
foo
ignored 42
`); }); test("overrides all nodes when the node prop is specified and allowedNodes is not", () => { function Child({ children }: any) { const { slot } = useSlot(children); return ( <> {slot.default( { return
{node}
; }} />, )} ); } const { asFragment } = render(
foo
included
42
`); }); test("node function is called only for nodes that get rendered", () => { function Child({ children, nodeFn }: any) { const { slot } = useSlot(children); return ( <> {slot.default( Fallback content, )} ); } function Parent({ children, childNodeFn, nodeFn }: any) { const { slot } = useSlot(children); return ( Fallback content 2 Fallback content 3 ); } render( { expect(node).toBe("Fallback content"); }} >, ); render( { expect(node).toBe("Content"); }} > Content , ); let callCount = 0; render( { expect(node).toBe("Fallback content 2"); return {node}; }} childNodeFn={(node: any) => { let expectedContent = [ Fallback content 2, "Fallback content 3", ]; expect(node).toEqual(expectedContent[callCount++]); return node; }} >, ); }); test("overrides props for elements specified by allowedNodes, ignores primitives", () => { function Child({ children }: any) { const { slot } = useSlot(children); return ( <> {slot.default( Fallback , )} ); } { const { asFragment } = render(); expect(asFragment()).toMatchInlineSnapshot(` Fallback `); } { const { asFragment } = render(
content
, ); expect(asFragment()).toMatchInlineSnapshot(`
content
`); } }); test("prop functions are only called for nodes that get rendered", () => { function Child({ children, propsOverride }: any) { const { slot } = useSlot(children); return ( <> {slot.default( Fallback 1 , )} ); } function Parent({ children, childPropsOverride, propsOverride }: any) { const { slot } = useSlot(children); return (
Fallback 2
Fallback 3
); } render( { expect(props).toEqual({ className: "fallback-1", children: "Fallback 1", }); }} >, ); render( { expect(props).toEqual({ id: "test-id", }); }} >
, ); { let callCount = 0; render( { expect(props).toEqual({ children: "Fallback 2" }); return { className: "added-class" }; }} childPropsOverride={(props: any) => { let expectedProps = [ { className: "added-class", children: "Fallback 2" }, { children: "Fallback 3" }, ]; expect(props).toEqual(expectedProps[callCount++]); }} >, ); } { let callCount = 0; render( { expect(props).toEqual({ children: "Fallback 2" }); return { className: "added-class" }; }} childPropsOverride={(props: any) => { let expectedProps = [ { className: "added-class", children: "Fallback 2" }, { children: "Fallback 3" }, ]; expect(props).toEqual(expectedProps[callCount++]); }} >, ); } { render( { expect(props).toEqual({ children: "Content" }); return { className: "added-class" }; }} childPropsOverride={(props: any) => { let expectedProps = { className: "added-class", children: "Content", }; expect(props).toEqual(expectedProps); }} >
Content
, ); } }); test("prop function's return value is merged to the rest of the props", () => { function Child({ children, propsOverride }: any) { const { slot } = useSlot(children); return ( <> {slot.default( Fallback 1 , )} ); } const { asFragment } = render( ({ id: "added-id" })} />, ); expect(asFragment()).toMatchInlineSnapshot(` Fallback 1 `); }); test("can be chained", () => { function Child({ children }: any) { const { slot } = useSlot(children); return slot.default([ { if (React.isValidElement(node)) { return React.createElement("span", node.props); } return node; }} >, , ]); } function Parent({ children }: any) { const { slot } = useSlot(children); return (
{node}
}> Parent's default content
({ id: "parent-added" })}>
); } { const { asFragment } = render(
content
, ); expect(asFragment()).toMatchInlineSnapshot(` content `); } { const { asFragment } = render(); expect(asFragment()).toMatchInlineSnapshot(` Parent's default content `); } { const { asFragment } = render(Content); expect(asFragment()).toMatchInlineSnapshot(` Content `); } }); test("maintains key equality of fallback content when reordered", () => { function Child({ children, defaultContent }: any) { const { slot } = useSlot(children); return slot.default(defaultContent); } const { asFragment, rerender, getByText } = render( { return [
{node.props.children} override 1
,
{node.props.children} override 2
, ]; }} > {[ { id: "fallback-1", content: "Fallback 1" }, { id: "fallback-2", content: "Fallback 2" }, ].map(({ id, content }) => (
{content}
))} , "added-class" }}> {[ { id: "fallback-3", content: "Fallback 3" }, { id: "fallback-4", content: "Fallback 4" }, ].map(({ id, content }) => (
{content}
))}
,
Fallback 5
, ]} >
, ); expect(asFragment()).toMatchInlineSnapshot(`
Fallback 1 override 1
Fallback 1 override 2
Fallback 2 override 1
Fallback 2 override 2
Fallback 3
Fallback 4
Fallback 5
`); let oneOne = getByText("Fallback 1 override 1"); let oneTwo = getByText("Fallback 1 override 2"); let twoOne = getByText("Fallback 2 override 1"); let twoTwo = getByText("Fallback 2 override 2"); let three = getByText("Fallback 3"); let four = getByText("Fallback 4"); let five = getByText("Fallback 5"); rerender( // OverrideNode's swapped; objects to map swapped; Third Override's key changed "added-class" }}> {[ { id: "fallback-4", content: "Fallback 4" }, { id: "fallback-3", content: "Fallback 3" }, ].map(({ id, content }) => (
{content}
))} , { return [
{node.props.children} override 1
,
{node.props.children} override 2
, ]; }} > {[ { id: "fallback-2", content: "Fallback 2" }, { id: "fallback-1", content: "Fallback 1" }, ].map(({ id, content }) => (
{content}
))}
,
Fallback 5
, ]} >
, ); expect(oneOne).toBe(getByText("Fallback 1 override 1")); expect(oneTwo).toBe(getByText("Fallback 1 override 2")); expect(twoOne).toBe(getByText("Fallback 2 override 1")); expect(twoTwo).toBe(getByText("Fallback 2 override 2")); expect(three).toBe(getByText("Fallback 3")); expect(four).toBe(getByText("Fallback 4")); expect(five).not.toBe(getByText("Fallback 5")); }); test("maintains key equality of provided content when reordered", () => { function Child({ children, defaultFallback, fooFallback, isReversed, }: any) { const { slot } = useSlot(children); return isReversed ? [ slot.foo(fooFallback, null, 2), slot.default(defaultFallback, null, 1), ] : [ slot.default(defaultFallback, null, 1), slot.foo(fooFallback, null, 2), ]; } const { asFragment, getByText, rerender } = render( [
{node.props.children} override 1
,
{node.props.children} override 2
, ]} /> } fooFallback={ "added-class" }} /> } > Default 1 Default 2 Foo 1 Foo 2.1 Foo 2.2
, ); expect(asFragment()).toMatchInlineSnapshot(`
Default 1 override 1
Default 1 override 2
Default 2 override 1
Default 2 override 2
Foo 1 Foo 2.1 Foo 2.2
`); const default1_1 = getByText("Default 1 override 1"); const default1_2 = getByText("Default 1 override 2"); const default2_1 = getByText("Default 2 override 1"); const default2_2 = getByText("Default 2 override 2"); const foo1 = getByText("Foo 1"); const foo2_1 = getByText("Foo 2.1"); const foo2_2 = getByText("Foo 2.2"); rerender( // Swapped the slot placements; // Swapped array items in node override; // Swapped content; // swapped children of template.foo [
{node.props.children} override 2
,
{node.props.children} override 1
, ]} /> } fooFallback={ "added-class" }} /> } > Foo 2.2 Foo 2.1 Foo 1 Default 2 Default 1
, ); expect(default1_1).toBe(getByText("Default 1 override 1")); expect(default1_2).toBe(getByText("Default 1 override 2")); expect(default2_1).toBe(getByText("Default 2 override 1")); expect(default2_2).toBe(getByText("Default 2 override 2")); expect(foo1).toBe(getByText("Foo 1")); expect(foo2_1).toBe(getByText("Foo 2.1")); expect(foo2_2).toBe(getByText("Foo 2.2")); }); });