From 58238bf2bacd9dfa5f94b17bb13a96c35f657174 Mon Sep 17 00:00:00 2001 From: Neo Nie Date: Sat, 13 Aug 2022 00:13:31 +0100 Subject: [PATCH] add tests --- example/components/RFCSelect.tsx | 9 +- src/__fixtures__/RFCSelect.tsx | 53 ++++++ src/__fixtures__/SimpleField.tsx | 69 ++++++++ src/__tests__/rfc.test.tsx | 285 +++++++++++++++++++++++++++++++ src/__tests__/simple.test.tsx | 181 ++++++++++++++++++++ src/__tests__/ssr.test.tsx | 50 ++++++ src/rfc/SlotsManager.ts | 4 +- src/rfc/index.tsx | 14 +- src/rfc/utils.ts | 2 +- src/simple/index.tsx | 9 +- 10 files changed, 666 insertions(+), 10 deletions(-) create mode 100644 src/__fixtures__/RFCSelect.tsx create mode 100644 src/__fixtures__/SimpleField.tsx create mode 100644 src/__tests__/rfc.test.tsx create mode 100644 src/__tests__/simple.test.tsx diff --git a/example/components/RFCSelect.tsx b/example/components/RFCSelect.tsx index fe9c338..23a0c39 100644 --- a/example/components/RFCSelect.tsx +++ b/example/components/RFCSelect.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useRef, useState } from 'react' import { createHost, createSlot } from 'create-slots/rfc' import { Item } from './RFCItem' @@ -8,12 +8,13 @@ const SelectDivider = createSlot('hr') export const Select = (props: React.ComponentProps<'ul'>) => { const [selected, setSelected] = useState(null) + const indexRef = useRef(0) return (
Selected: {selected}
{createHost(props.children, (slots) => { - let index = 0 + indexRef.current = 0 return (
    {slots.map((slot) => { @@ -23,11 +24,11 @@ export const Select = (props: React.ComponentProps<'ul'>) => { return ( { setSelected(itemProps.value) diff --git a/src/__fixtures__/RFCSelect.tsx b/src/__fixtures__/RFCSelect.tsx new file mode 100644 index 0000000..277f2ff --- /dev/null +++ b/src/__fixtures__/RFCSelect.tsx @@ -0,0 +1,53 @@ +import * as React from 'react' + +import { createHost, createSlot } from '../rfc' + +const Divider = (props: React.ComponentPropsWithoutRef<'hr'>) => ( +
    +) + +const SelectItem = createSlot('li') +const SelectDivider = createSlot(Divider) + +export const Select = (props: React.ComponentPropsWithoutRef<'ul'>) => { + const [selected, setSelected] = React.useState() + const indexRef = React.useRef(0) + + return ( +
    +
    Selected: {selected?.props.value ?? ''}
    + {createHost(props.children, (slots) => { + indexRef.current = 0 + return ( +
      + {slots.map((slot) => { + if (slot.type === SelectItem) { + const itemProps = slot.props + + return ( +
    • { + setSelected(slot) + }, + }} + /> + ) + } + + return slot + })} +
    + ) + })} +
    + ) +} + +Select.Item = SelectItem +Select.Divider = SelectDivider diff --git a/src/__fixtures__/SimpleField.tsx b/src/__fixtures__/SimpleField.tsx new file mode 100644 index 0000000..bf2d20b --- /dev/null +++ b/src/__fixtures__/SimpleField.tsx @@ -0,0 +1,69 @@ +import * as React from 'react' + +import { createHost, createSlot } from '../simple' + +const Description = (props: React.ComponentPropsWithoutRef<'span'>) => ( + +) + +const FieldLabel = createSlot('label') +const FieldInput = createSlot('input') +const FieldDescription = createSlot(Description) +const FieldIcon = createSlot('span') + +// const createFill =

    (name: P) => { +// const FillComponent = React.forwardRef((props, ref) => { +// const Slots = useSlots() +// const [originalProps] = React.useState(() => Slots.getProps(name)) + +// React.useEffect(() => Slots.update(name, { ...props, ref })) +// React.useEffect( +// () => () => { +// originalProps ? Slots.update(name, originalProps) : Slots.unmount(name) +// }, +// [Slots, originalProps] +// ) + +// return null +// }) as unknown as typeof SlotComponents[P] +// return FillComponent +// } + +type FieldProps = React.ComponentPropsWithoutRef<'div'> + +export const Field = (props: FieldProps) => { + const id = ':r0:' + const descriptionId = ':r1:' + + return createHost(props.children, (Slots) => { + const labelProps = Slots.getProps(FieldLabel) + const inputProps = Slots.getProps(FieldInput) + const descriptionProps = Slots.getProps(FieldDescription) + const iconProps = Slots.getProps(FieldIcon) + return ( +

    + {labelProps &&
    + ) + }) +} + +Field.Label = FieldLabel +Field.Input = FieldInput +Field.Description = FieldDescription +Field.Icon = FieldIcon diff --git a/src/__tests__/rfc.test.tsx b/src/__tests__/rfc.test.tsx new file mode 100644 index 0000000..edfa319 --- /dev/null +++ b/src/__tests__/rfc.test.tsx @@ -0,0 +1,285 @@ +import * as React from 'react' +import { render, screen, fireEvent } from '@testing-library/react' + +import { create } from '../__fixtures__/utils' +import { Select } from '../__fixtures__/RFCSelect' + +test('render slots', () => { + const instance = create( + + ) + expect(instance).toMatchInlineSnapshot(` +
    +
    + Selected: +
    +
      +
    • + Foo +
    • +
      +
    • + Bar +
    • +
    +
    +`) + + // insert item + instance.update( + + ) + expect(instance).toMatchInlineSnapshot(` +
    +
    + Selected: +
    +
      +
    • + Foo +
    • +
      +
    • + Baz +
    • +
      +
    • + Bar +
    • +
    +
    +`) + + // remove item + instance.update( + + ) + expect(instance).toMatchInlineSnapshot(` +
    +
    + Selected: +
    +
      +
    • + Foo +
    • +
      +
    • + Bar +
    • +
    +
    +`) + + // update item + instance.update( + + ) + expect(instance).toMatchInlineSnapshot(` +
    +
    + Selected: +
    +
      +
    • + FooFoo +
    • +
      +
    • + Bar +
    • +
    +
    +`) + + // nested slots + instance.update( + + FooFoo + + Bar + + + + Bar + + ) + + expect(instance).toMatchInlineSnapshot(` +
    +
    + Selected: +
    +
      +
    • +
      +
      + Selected: +
      +
        +
      • + FooFoo +
      • +
        +
      • + Bar +
      • +
      +
      +
    • +
      +
    • + Bar +
    • +
    +
    +`) +}) + +test('ref', () => { + const ref = { current: null } + render( + + ) + + expect(ref.current).toMatchInlineSnapshot(` +
  • + Foo +
  • +`) +}) + +test('interaction', () => { + render( + + ) + + fireEvent.click(screen.getAllByRole('listitem')[0]) + expect(screen.getByText('Selected: foo')).not.toBeNull() + + fireEvent.click(screen.getAllByRole('listitem')[1]) + expect(screen.getByText('Selected: bar')).not.toBeNull() +}) + +test('dev warning', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation() + render( + + ) + expect(warn).toHaveBeenCalledTimes(0) + + // @ts-ignore + process.env.NODE_ENV = 'development' + render( + + ) + expect(warn).toHaveBeenCalledTimes(1) + expect(warn).toHaveBeenCalledWith( + 'Unwrapped children found in "Host", either wrap them in subcomponents or remove' + ) +}) diff --git a/src/__tests__/simple.test.tsx b/src/__tests__/simple.test.tsx new file mode 100644 index 0000000..fe8e939 --- /dev/null +++ b/src/__tests__/simple.test.tsx @@ -0,0 +1,181 @@ +import * as React from 'react' +import { render } from '@testing-library/react' + +import { create } from '../__fixtures__/utils' +import { Field } from '../__fixtures__/SimpleField' + +test('render slots', () => { + const instance = create( + + Label + + - + Description + + ) + expect(instance).toMatchInlineSnapshot(` +
    + + +
    + + - + + + Description + +
    +
    + `) + + // arbitrary order + instance.update( + + - + + Description + Label + + ) + expect(instance).toMatchInlineSnapshot(` +
    + + +
    + + - + + + Description + +
    +
    + `) + + // dynamic content + instance.update( + + Label + + + ) + + expect(instance).toMatchInlineSnapshot(` +
    + + +
    + `) + + // nested slots + instance.update( + + Label + + + + Label + + + + + ) + + expect(instance).toMatchInlineSnapshot(` +
    + + +
    + +
    + + +
    +
    +
    +
    + `) +}) + +test('ref', () => { + const ref = { current: null } + render( + + Label + + - + Description + + ) + + expect(ref.current).toMatchInlineSnapshot(` + +`) +}) + +test('dev warning', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation() + render( + + Label + Input + + ) + expect(warn).toHaveBeenCalledTimes(0) + + // @ts-ignore + process.env.NODE_ENV = 'development' + render( + + Label + Input + + ) + expect(warn).toHaveBeenCalledTimes(1) + expect(warn).toHaveBeenCalledWith( + 'Unwrapped children found in "Host", either wrap them in subcomponents or remove' + ) +}) diff --git a/src/__tests__/ssr.test.tsx b/src/__tests__/ssr.test.tsx index 3673bd7..0d570d9 100644 --- a/src/__tests__/ssr.test.tsx +++ b/src/__tests__/ssr.test.tsx @@ -8,6 +8,8 @@ import { renderToStaticMarkup } from 'react-dom/server' import { Field } from '../__fixtures__/Field' import { StaticField } from '../__fixtures__/StaticField' import { Select } from '../__fixtures__/Select' +import { Select as RFCSelect } from '../__fixtures__/RFCSelect' +import { Field as SimpleField } from '../__fixtures__/SimpleField' test('default SSR', () => { const markup = renderToStaticMarkup( @@ -91,3 +93,51 @@ test('list SSR', () => { `"
    Selected:
    • Foo

    • Bar
    "` ) }) + +test('rfc SSR', () => { + const markup = renderToStaticMarkup( + + Foo + + Bar + + ) + expect(markup).toMatchInlineSnapshot( + `"
    Selected:
    • Foo

    • Bar
    "` + ) +}) + +test('simple SSR', () => { + const markup = renderToStaticMarkup( + + Label + + + + Label + + + + + ) + + expect(markup).toMatchInlineSnapshot( + `"
    "` + ) + + // arbitrary order + const markup1 = renderToStaticMarkup( + + + + Label + + + + Label + + + ) + + expect(markup1).toEqual(markup) +}) diff --git a/src/rfc/SlotsManager.ts b/src/rfc/SlotsManager.ts index 3c934c3..edf7160 100644 --- a/src/rfc/SlotsManager.ts +++ b/src/rfc/SlotsManager.ts @@ -1,5 +1,7 @@ import * as React from 'react' +import { SlotElement } from './utils' + type Key = React.Key export const createSlotsManager = (onChange: (key: Key) => void) => { @@ -23,7 +25,7 @@ export const createSlotsManager = (onChange: (key: Key) => void) => { return itemMap.has(key) }, get() { - return Array.from(itemMap.values()) + return Array.from(itemMap.values()) as SlotElement[] }, } } diff --git a/src/rfc/index.tsx b/src/rfc/index.tsx index 392cf46..eb1172a 100644 --- a/src/rfc/index.tsx +++ b/src/rfc/index.tsx @@ -1,13 +1,15 @@ import * as React from 'react' import { createSlotsContext, getComponentName } from '../utils' +import { DevChildren } from '../DevChildren' import { ScanContext, ScanProvider } from '../ScanContext' import { createSlotsManager } from './SlotsManager' +import { SlotElement } from './utils' -export * from '../utils' +export * from './utils' type Slots = ReturnType -type Callback = (Slots: React.ReactElement[]) => JSX.Element | null +type Callback = (slots: SlotElement[]) => JSX.Element | null const SlotsContext = createSlotsContext(undefined) @@ -38,7 +40,13 @@ export const Host = ({ return ( <> - {children} + + {process.env.NODE_ENV === 'development' ? ( + {children} + ) : ( + children + )} + diff --git a/src/rfc/utils.ts b/src/rfc/utils.ts index 523cc6a..c15934d 100644 --- a/src/rfc/utils.ts +++ b/src/rfc/utils.ts @@ -1,6 +1,6 @@ import * as React from 'react' -type SlotElement = React.ReactElement< +export type SlotElement = React.ReactElement< React.ComponentPropsWithRef, T > & { diff --git a/src/simple/index.tsx b/src/simple/index.tsx index 80e023a..91a693e 100644 --- a/src/simple/index.tsx +++ b/src/simple/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { createSlotsContext, getComponentName } from '../utils' +import { DevChildren } from '../DevChildren' import { createSlotsManager } from './SlotsManager' type Slots = ReturnType @@ -27,7 +28,13 @@ export const Host = ({ return ( <> - {children} + + {process.env.NODE_ENV === 'development' ? ( + {children} + ) : ( + children + )} + )