Add more Dialog tests

This caught an issue with <FocusTrap> already.
This commit is contained in:
Ryan Gossiaux
2021-12-26 16:53:44 -08:00
parent a2745e1770
commit 77dd80caa2
3 changed files with 155 additions and 42 deletions

View File

@@ -12,7 +12,12 @@ import { click, Keys, press } from "$lib/test-utils/interactions";
import Transition from "$lib/components/transitions/TransitionRoot.svelte"; import Transition from "$lib/components/transitions/TransitionRoot.svelte";
import { tick } from "svelte"; import { tick } from "svelte";
jest.mock('../../hooks/use-id') let id = 0;
jest.mock('../../hooks/use-id', () => {
return {
useId: jest.fn(() => ++id),
}
})
// @ts-expect-error // @ts-expect-error
global.IntersectionObserver = class FakeIntersectionObserver { global.IntersectionObserver = class FakeIntersectionObserver {
@@ -20,6 +25,7 @@ global.IntersectionObserver = class FakeIntersectionObserver {
disconnect() { } disconnect() { }
} }
beforeEach(() => id = 0)
afterAll(() => jest.restoreAllMocks()) afterAll(() => jest.restoreAllMocks())
describe('Safe guards', () => { describe('Safe guards', () => {
@@ -56,10 +62,7 @@ describe('Safe guards', () => {
] ]
}) })
assertDialog({ assertDialog({ state: DialogState.InvisibleUnmounted })
state: DialogState.InvisibleUnmounted,
attributes: { id: 'headlessui-dialog-1' },
})
}) })
) )
}) })
@@ -355,4 +358,103 @@ describe('Composition', () => {
) )
}) })
describe('Keyboard interactions', () => {
describe('`Escape` key', () => {
it(
'should be possible to close the dialog with Escape',
async () => {
render(
TestRenderer, {
allProps: [
[ManagedDialog, { buttonText: "Trigger", buttonProps: { id: "trigger" } }, [
"Contents",
[TestTabSentinel],
]],
]
})
assertDialog({ state: DialogState.InvisibleUnmounted })
// Open dialog
await click(document.getElementById("trigger"))
// Verify it is open
assertDialog({
state: DialogState.Visible,
attributes: { id: 'headlessui-dialog-1' },
})
// Close dialog
await press(Keys.Escape)
// Verify it is close
assertDialog({ state: DialogState.InvisibleUnmounted })
}
)
it(
'should be possible to close the dialog with Escape, when a field is focused',
suppressConsoleLogs(async () => {
render(
TestRenderer, {
allProps: [
[ManagedDialog, { buttonText: "Trigger", buttonProps: { id: "trigger" } }, [
["Contents"],
[Input, { id: "name" }],
[TestTabSentinel],
]],
]
})
assertDialog({ state: DialogState.InvisibleUnmounted })
// Open dialog
await click(document.getElementById('trigger'))
// Verify it is open
assertDialog({
state: DialogState.Visible,
attributes: { id: 'headlessui-dialog-1' },
})
// Close dialog
await press(Keys.Escape)
// Verify it is close
assertDialog({ state: DialogState.InvisibleUnmounted })
})
)
it(
'should not be possible to close the dialog with Escape, when a field is focused but cancels the event',
async () => {
render(
TestRenderer, {
allProps: [
[ManagedDialog, { buttonText: "Trigger", buttonProps: { id: "trigger" } }, [
["Contents"],
[Input, { id: "name", onKeydown: (e: CustomEvent) => { e.preventDefault(); e.stopPropagation(); } }],
[TestTabSentinel],
]],
]
})
assertDialog({ state: DialogState.InvisibleUnmounted })
// Open dialog
await click(document.getElementById('trigger'))
// Verify it is open
assertDialog({
state: DialogState.Visible,
attributes: { id: 'headlessui-dialog-1' },
})
// Try to close the dialog
await press(Keys.Escape)
// Verify it is still open
assertDialog({ state: DialogState.Visible })
})
})
})

View File

@@ -4,18 +4,25 @@
onChange?: HandlerType; onChange?: HandlerType;
onClose?: HandlerType; onClose?: HandlerType;
onFocus?: HandlerType; onFocus?: HandlerType;
onKeydown?: HandlerType;
} }
type SingleComponent = [SvelteComponent, ComponentProps, TestRendererProps]; type SingleComponent =
| string
| [SvelteComponent, ComponentProps, TestRendererProps];
export type TestRendererProps = export type TestRendererProps =
| undefined | undefined
| string
| SingleComponent | SingleComponent
| SingleComponent[]; | SingleComponent[];
function isSingleComponent( function isSingleComponent(
props: SingleComponent | SingleComponent[] props: SingleComponent | SingleComponent[]
): props is SingleComponent { ): props is SingleComponent {
return Array.isArray(props) && !Array.isArray(props[0]); return (
typeof props === "string" ||
(Array.isArray(props) &&
!Array.isArray(props[0]) &&
typeof props[0] !== "string")
);
} }
</script> </script>
@@ -28,23 +35,22 @@
let onChange: HandlerType = () => {}; let onChange: HandlerType = () => {};
let onClose: HandlerType = () => {}; let onClose: HandlerType = () => {};
let onFocus: HandlerType = () => {}; let onFocus: HandlerType = () => {};
let onKeydown: HandlerType = () => {};
if (allProps && typeof allProps !== "string" && isSingleComponent(allProps)) { if (allProps && typeof allProps !== "string" && isSingleComponent(allProps)) {
({ ({
onChange = onChange, onChange = onChange,
onClose = onClose, onClose = onClose,
onFocus = onFocus, onFocus = onFocus,
onKeydown = onKeydown,
...spreadProps ...spreadProps
} = allProps[1] || {}); } = allProps[1] || {});
} }
</script> </script>
{#if allProps}
{#if isSingleComponent(allProps)}
{#if typeof allProps === "string"} {#if typeof allProps === "string"}
{allProps} {allProps}
{:else if Array.isArray(allProps)}
{#if Array.isArray(allProps[0])}
{#each allProps as childProps}
<svelte:self allProps={childProps} />
{/each}
{:else} {:else}
<svelte:component <svelte:component
this={allProps[0]} this={allProps[0]}
@@ -52,8 +58,14 @@
on:change={onChange} on:change={onChange}
on:close={onClose} on:close={onClose}
on:focus={onFocus} on:focus={onFocus}
on:keydown={onKeydown}
> >
<svelte:self allProps={allProps[2]} /> <svelte:self allProps={allProps[2]} />
</svelte:component> </svelte:component>
{/if} {/if}
{:else}
{#each allProps as childProps}
<svelte:self allProps={childProps} />
{/each}
{/if}
{/if} {/if}

View File

@@ -181,9 +181,9 @@ export async function click(
if (button === MouseButton.Left) { if (button === MouseButton.Left) {
// Cancel in pointerDown cancels mouseDown, mouseUp // Cancel in pointerDown cancels mouseDown, mouseUp
let cancelled = !fireEvent.pointerDown(element, options) let cancelled = !(await fireEvent.pointerDown(element, options))
if (!cancelled) { if (!cancelled) {
fireEvent.mouseDown(element, options) await fireEvent.mouseDown(element, options)
} }
// Ensure to trigger a `focus` event if the element is focusable, or within a focusable element // Ensure to trigger a `focus` event if the element is focusable, or within a focusable element
@@ -196,26 +196,25 @@ export async function click(
next = next.parentElement next = next.parentElement
} }
fireEvent.pointerUp(element, options) await fireEvent.pointerUp(element, options)
if (!cancelled) { if (!cancelled) {
fireEvent.mouseUp(element, options) await fireEvent.mouseUp(element, options)
} }
fireEvent.click(element, options) await fireEvent.click(element, options)
} else if (button === MouseButton.Right) { } else if (button === MouseButton.Right) {
// Cancel in pointerDown cancels mouseDown, mouseUp // Cancel in pointerDown cancels mouseDown, mouseUp
let cancelled = !fireEvent.pointerDown(element, options) let cancelled = !(await fireEvent.pointerDown(element, options))
if (!cancelled) { if (!cancelled) {
fireEvent.mouseDown(element, options) await fireEvent.mouseDown(element, options)
} }
// Only in Firefox: // Only in Firefox:
fireEvent.pointerUp(element, options) await fireEvent.pointerUp(element, options)
if (!cancelled) { if (!cancelled) {
fireEvent.mouseUp(element, options) await fireEvent.mouseUp(element, options)
} }
} }
await tick()
} catch (err: any) { } catch (err: any) {
Error.captureStackTrace(err, click) Error.captureStackTrace(err, click)
throw err throw err
@@ -226,7 +225,7 @@ export async function focus(element: Document | Element | Window | null) {
try { try {
if (element === null) return expect(element).not.toBe(null) if (element === null) return expect(element).not.toBe(null)
fireEvent.focus(element) await fireEvent.focus(element)
await tick() await tick()
} catch (err: any) { } catch (err: any) {
@@ -238,9 +237,9 @@ export async function mouseEnter(element: Document | Element | Window | null) {
try { try {
if (element === null) return expect(element).not.toBe(null) if (element === null) return expect(element).not.toBe(null)
fireEvent.pointerOver(element) await fireEvent.pointerOver(element)
fireEvent.pointerEnter(element) await fireEvent.pointerEnter(element)
fireEvent.mouseOver(element) await fireEvent.mouseOver(element)
await tick() await tick()
} catch (err: any) { } catch (err: any) {
@@ -253,8 +252,8 @@ export async function mouseMove(element: Document | Element | Window | null) {
try { try {
if (element === null) return expect(element).not.toBe(null) if (element === null) return expect(element).not.toBe(null)
fireEvent.pointerMove(element) await fireEvent.pointerMove(element)
fireEvent.mouseMove(element) await fireEvent.mouseMove(element)
await tick() await tick()
} catch (err: any) { } catch (err: any) {
@@ -267,10 +266,10 @@ export async function mouseLeave(element: Document | Element | Window | null) {
try { try {
if (element === null) return expect(element).not.toBe(null) if (element === null) return expect(element).not.toBe(null)
fireEvent.pointerOut(element) await fireEvent.pointerOut(element)
fireEvent.pointerLeave(element) await fireEvent.pointerLeave(element)
fireEvent.mouseOut(element) await fireEvent.mouseOut(element)
fireEvent.mouseLeave(element) await fireEvent.mouseLeave(element)
await tick() await tick()
} catch (err: any) { } catch (err: any) {