Add more Dialog tests
This caught an issue with <FocusTrap> already.
This commit is contained in:
@@ -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 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -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,32 +35,37 @@
|
|||||||
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 typeof allProps === "string"}
|
{#if allProps}
|
||||||
{allProps}
|
{#if isSingleComponent(allProps)}
|
||||||
{:else if Array.isArray(allProps)}
|
{#if typeof allProps === "string"}
|
||||||
{#if Array.isArray(allProps[0])}
|
{allProps}
|
||||||
|
{:else}
|
||||||
|
<svelte:component
|
||||||
|
this={allProps[0]}
|
||||||
|
{...spreadProps}
|
||||||
|
on:change={onChange}
|
||||||
|
on:close={onClose}
|
||||||
|
on:focus={onFocus}
|
||||||
|
on:keydown={onKeydown}
|
||||||
|
>
|
||||||
|
<svelte:self allProps={allProps[2]} />
|
||||||
|
</svelte:component>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
{#each allProps as childProps}
|
{#each allProps as childProps}
|
||||||
<svelte:self allProps={childProps} />
|
<svelte:self allProps={childProps} />
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
|
||||||
<svelte:component
|
|
||||||
this={allProps[0]}
|
|
||||||
{...spreadProps}
|
|
||||||
on:change={onChange}
|
|
||||||
on:close={onClose}
|
|
||||||
on:focus={onFocus}
|
|
||||||
>
|
|
||||||
<svelte:self allProps={allProps[2]} />
|
|
||||||
</svelte:component>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user