From 77dd80caa2303992475a0b231594d7d42bcfda89 Mon Sep 17 00:00:00 2001 From: Ryan Gossiaux Date: Sun, 26 Dec 2021 16:53:44 -0800 Subject: [PATCH] Add more Dialog tests This caught an issue with already. --- src/lib/components/dialog/dialog.test.ts | 112 ++++++++++++++++++++++- src/lib/test-utils/TestRenderer.svelte | 46 ++++++---- src/lib/test-utils/interactions.ts | 39 ++++---- 3 files changed, 155 insertions(+), 42 deletions(-) diff --git a/src/lib/components/dialog/dialog.test.ts b/src/lib/components/dialog/dialog.test.ts index ea8f2ff..3e799f2 100644 --- a/src/lib/components/dialog/dialog.test.ts +++ b/src/lib/components/dialog/dialog.test.ts @@ -12,7 +12,12 @@ import { click, Keys, press } from "$lib/test-utils/interactions"; import Transition from "$lib/components/transitions/TransitionRoot.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 global.IntersectionObserver = class FakeIntersectionObserver { @@ -20,6 +25,7 @@ global.IntersectionObserver = class FakeIntersectionObserver { disconnect() { } } +beforeEach(() => id = 0) afterAll(() => jest.restoreAllMocks()) describe('Safe guards', () => { @@ -56,10 +62,7 @@ describe('Safe guards', () => { ] }) - assertDialog({ - state: DialogState.InvisibleUnmounted, - attributes: { id: 'headlessui-dialog-1' }, - }) + assertDialog({ state: DialogState.InvisibleUnmounted }) }) ) }) @@ -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 }) + }) + }) +}) diff --git a/src/lib/test-utils/TestRenderer.svelte b/src/lib/test-utils/TestRenderer.svelte index 06d013f..33adca9 100644 --- a/src/lib/test-utils/TestRenderer.svelte +++ b/src/lib/test-utils/TestRenderer.svelte @@ -4,18 +4,25 @@ onChange?: HandlerType; onClose?: HandlerType; onFocus?: HandlerType; + onKeydown?: HandlerType; } - type SingleComponent = [SvelteComponent, ComponentProps, TestRendererProps]; + type SingleComponent = + | string + | [SvelteComponent, ComponentProps, TestRendererProps]; export type TestRendererProps = | undefined - | string | SingleComponent | SingleComponent[]; function isSingleComponent( props: SingleComponent | 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") + ); } @@ -28,32 +35,37 @@ let onChange: HandlerType = () => {}; let onClose: HandlerType = () => {}; let onFocus: HandlerType = () => {}; + let onKeydown: HandlerType = () => {}; if (allProps && typeof allProps !== "string" && isSingleComponent(allProps)) { ({ onChange = onChange, onClose = onClose, onFocus = onFocus, + onKeydown = onKeydown, ...spreadProps } = allProps[1] || {}); } -{#if typeof allProps === "string"} - {allProps} -{:else if Array.isArray(allProps)} - {#if Array.isArray(allProps[0])} +{#if allProps} + {#if isSingleComponent(allProps)} + {#if typeof allProps === "string"} + {allProps} + {:else} + + + + {/if} + {:else} {#each allProps as childProps} {/each} - {:else} - - - {/if} {/if} diff --git a/src/lib/test-utils/interactions.ts b/src/lib/test-utils/interactions.ts index 9bc0ce0..72b5f1b 100644 --- a/src/lib/test-utils/interactions.ts +++ b/src/lib/test-utils/interactions.ts @@ -181,9 +181,9 @@ export async function click( if (button === MouseButton.Left) { // Cancel in pointerDown cancels mouseDown, mouseUp - let cancelled = !fireEvent.pointerDown(element, options) + let cancelled = !(await fireEvent.pointerDown(element, options)) 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 @@ -196,26 +196,25 @@ export async function click( next = next.parentElement } - fireEvent.pointerUp(element, options) + await fireEvent.pointerUp(element, options) 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) { // Cancel in pointerDown cancels mouseDown, mouseUp - let cancelled = !fireEvent.pointerDown(element, options) + let cancelled = !(await fireEvent.pointerDown(element, options)) if (!cancelled) { - fireEvent.mouseDown(element, options) + await fireEvent.mouseDown(element, options) } // Only in Firefox: - fireEvent.pointerUp(element, options) + await fireEvent.pointerUp(element, options) if (!cancelled) { - fireEvent.mouseUp(element, options) + await fireEvent.mouseUp(element, options) } } - await tick() } catch (err: any) { Error.captureStackTrace(err, click) throw err @@ -226,7 +225,7 @@ export async function focus(element: Document | Element | Window | null) { try { if (element === null) return expect(element).not.toBe(null) - fireEvent.focus(element) + await fireEvent.focus(element) await tick() } catch (err: any) { @@ -238,9 +237,9 @@ export async function mouseEnter(element: Document | Element | Window | null) { try { if (element === null) return expect(element).not.toBe(null) - fireEvent.pointerOver(element) - fireEvent.pointerEnter(element) - fireEvent.mouseOver(element) + await fireEvent.pointerOver(element) + await fireEvent.pointerEnter(element) + await fireEvent.mouseOver(element) await tick() } catch (err: any) { @@ -253,8 +252,8 @@ export async function mouseMove(element: Document | Element | Window | null) { try { if (element === null) return expect(element).not.toBe(null) - fireEvent.pointerMove(element) - fireEvent.mouseMove(element) + await fireEvent.pointerMove(element) + await fireEvent.mouseMove(element) await tick() } catch (err: any) { @@ -267,10 +266,10 @@ export async function mouseLeave(element: Document | Element | Window | null) { try { if (element === null) return expect(element).not.toBe(null) - fireEvent.pointerOut(element) - fireEvent.pointerLeave(element) - fireEvent.mouseOut(element) - fireEvent.mouseLeave(element) + await fireEvent.pointerOut(element) + await fireEvent.pointerLeave(element) + await fireEvent.mouseOut(element) + await fireEvent.mouseLeave(element) await tick() } catch (err: any) {