Add remaining Switch tests
This commit is contained in:
11
src/lib/components/switch/_ManagedSwitch.svelte
Normal file
11
src/lib/components/switch/_ManagedSwitch.svelte
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Switch from "./Switch.svelte";
|
||||||
|
|
||||||
|
// This component is only for use in tests
|
||||||
|
export let initialChecked = false;
|
||||||
|
let state = initialChecked;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Switch checked={state} on:change={(e) => (state = e.detail)} on:change>
|
||||||
|
<slot />
|
||||||
|
</Switch>
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
import { render } from "@testing-library/svelte";
|
import { render } from "@testing-library/svelte";
|
||||||
import TestRenderer from "../../test-utils/TestRenderer.svelte";
|
import TestRenderer from "../../test-utils/TestRenderer.svelte";
|
||||||
import { Switch } from ".";
|
import { Switch, SwitchDescription, SwitchGroup, SwitchLabel } from ".";
|
||||||
import { assertSwitch, getSwitch, SwitchState } from "$lib/test-utils/accessibility-assertions";
|
import { assertActiveElement, assertSwitch, getSwitch, getSwitchLabel, SwitchState } from "$lib/test-utils/accessibility-assertions";
|
||||||
|
import Button from "$lib/internal/elements/Button.svelte";
|
||||||
|
import Div from "$lib/internal/elements/Div.svelte";
|
||||||
import Span from "$lib/internal/elements/Span.svelte";
|
import Span from "$lib/internal/elements/Span.svelte";
|
||||||
|
import ManagedSwitch from "./_ManagedSwitch.svelte";
|
||||||
|
import { click, Keys, press } from "$lib/test-utils/interactions";
|
||||||
jest.mock('../../hooks/use-id')
|
jest.mock('../../hooks/use-id')
|
||||||
|
|
||||||
describe('Safe guards', () => {
|
describe('Safe guards', () => {
|
||||||
@@ -112,3 +116,310 @@ describe('Rendering', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Render composition', () => {
|
||||||
|
it('should be possible to render a Switch.Group, Switch and Switch.Label', () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
SwitchGroup,
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
[Switch,
|
||||||
|
{ checked: false, onChange: console.log }
|
||||||
|
],
|
||||||
|
[SwitchLabel,
|
||||||
|
{},
|
||||||
|
"Enable notifications"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
assertSwitch({ state: SwitchState.Off, label: 'Enable notifications' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be possible to render a Switch.Group, Switch and Switch.Label (before the Switch)', () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
SwitchGroup,
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
[SwitchLabel,
|
||||||
|
{},
|
||||||
|
"Label B"],
|
||||||
|
[Switch,
|
||||||
|
{ checked: false, onChange: console.log },
|
||||||
|
"Label A"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Warning! Using aria-label or aria-labelledby will hide any descendant content from assistive
|
||||||
|
// technologies.
|
||||||
|
//
|
||||||
|
// Thus: Label A should not be part of the "label" in this case
|
||||||
|
assertSwitch({ state: SwitchState.Off, label: 'Label B' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be possible to render a Switch.Group, Switch and Switch.Label (after the Switch)', () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
SwitchGroup,
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
[Switch,
|
||||||
|
{ checked: false, onChange: console.log },
|
||||||
|
"Label A"],
|
||||||
|
[SwitchLabel,
|
||||||
|
{},
|
||||||
|
"Label B"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Warning! Using aria-label or aria-labelledby will hide any descendant content from assistive
|
||||||
|
// technologies.
|
||||||
|
//
|
||||||
|
// Thus: Label A should not be part of the "label" in this case
|
||||||
|
assertSwitch({ state: SwitchState.Off, label: 'Label B' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be possible to render a Switch.Group, Switch and Switch.Description (before the Switch)', async () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
SwitchGroup,
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
[SwitchDescription,
|
||||||
|
{},
|
||||||
|
"This is an important feature"],
|
||||||
|
[Switch,
|
||||||
|
{ checked: false, onChange: console.log }],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
assertSwitch({ state: SwitchState.Off, description: 'This is an important feature' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be possible to render a Switch.Group, Switch and Switch.Description (after the Switch)', () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
SwitchGroup,
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
[Switch,
|
||||||
|
{ checked: false, onChange: console.log }],
|
||||||
|
[SwitchDescription,
|
||||||
|
{},
|
||||||
|
"This is an important feature"],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
assertSwitch({ state: SwitchState.Off, description: 'This is an important feature' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be possible to render a Switch.Group, Switch, Switch.Label and Switch.Description', () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
SwitchGroup,
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
[SwitchLabel,
|
||||||
|
{},
|
||||||
|
"Label A"],
|
||||||
|
[Switch,
|
||||||
|
{ checked: false, onChange: console.log }],
|
||||||
|
[SwitchDescription,
|
||||||
|
{},
|
||||||
|
"This is an important feature"],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
assertSwitch({
|
||||||
|
state: SwitchState.Off,
|
||||||
|
label: 'Label A',
|
||||||
|
description: 'This is an important feature',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Keyboard interactions', () => {
|
||||||
|
describe('`Space` key', () => {
|
||||||
|
it('should be possible to toggle the Switch with Space', async () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
ManagedSwitch,
|
||||||
|
{},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure checkbox is off
|
||||||
|
assertSwitch({ state: SwitchState.Off })
|
||||||
|
|
||||||
|
// Focus the switch
|
||||||
|
getSwitch()?.focus()
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
await press(Keys.Space)
|
||||||
|
|
||||||
|
// Ensure state is on
|
||||||
|
assertSwitch({ state: SwitchState.On })
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
await press(Keys.Space)
|
||||||
|
|
||||||
|
// Ensure state is off
|
||||||
|
assertSwitch({ state: SwitchState.Off })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('`Enter` key', () => {
|
||||||
|
it('should not be possible to use Enter to toggle the Switch', async () => {
|
||||||
|
let handleChange = jest.fn()
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
ManagedSwitch,
|
||||||
|
{ onChange: handleChange },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Ensure checkbox is off
|
||||||
|
assertSwitch({ state: SwitchState.Off })
|
||||||
|
|
||||||
|
// Focus the switch
|
||||||
|
getSwitch()?.focus()
|
||||||
|
|
||||||
|
// Try to toggle
|
||||||
|
await press(Keys.Enter)
|
||||||
|
|
||||||
|
expect(handleChange).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('`Tab` key', () => {
|
||||||
|
it('should be possible to tab away from the Switch', async () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
Div,
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
[Switch,
|
||||||
|
{ checked: false, onChange: console.log }],
|
||||||
|
[Button, { id: "btn" }, "Other element"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure checkbox is off
|
||||||
|
assertSwitch({ state: SwitchState.Off })
|
||||||
|
|
||||||
|
// Focus the switch
|
||||||
|
getSwitch()?.focus()
|
||||||
|
|
||||||
|
// Expect the switch to be active
|
||||||
|
assertActiveElement(getSwitch())
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
await press(Keys.Tab)
|
||||||
|
|
||||||
|
// Expect the button to be active
|
||||||
|
assertActiveElement(document.getElementById('btn'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Mouse interactions', () => {
|
||||||
|
it('should be possible to toggle the Switch with a click', async () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
ManagedSwitch,
|
||||||
|
{},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure checkbox is off
|
||||||
|
assertSwitch({ state: SwitchState.Off })
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
await click(getSwitch())
|
||||||
|
|
||||||
|
// Ensure state is on
|
||||||
|
assertSwitch({ state: SwitchState.On })
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
await click(getSwitch())
|
||||||
|
|
||||||
|
// Ensure state is off
|
||||||
|
assertSwitch({ state: SwitchState.Off })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be possible to toggle the Switch with a click on the Label', async () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
SwitchGroup,
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
[ManagedSwitch,
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
[SwitchLabel,
|
||||||
|
{},
|
||||||
|
"The label"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Ensure checkbox is off
|
||||||
|
assertSwitch({ state: SwitchState.Off })
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
await click(getSwitchLabel())
|
||||||
|
|
||||||
|
// Ensure the switch is focused
|
||||||
|
assertActiveElement(getSwitch())
|
||||||
|
|
||||||
|
// Ensure state is on
|
||||||
|
assertSwitch({ state: SwitchState.On })
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
await click(getSwitchLabel())
|
||||||
|
|
||||||
|
// Ensure the switch is focused
|
||||||
|
assertActiveElement(getSwitch())
|
||||||
|
|
||||||
|
// Ensure state is off
|
||||||
|
assertSwitch({ state: SwitchState.Off })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not be possible to toggle the Switch with a click on the Label (passive)', async () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
SwitchGroup,
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
[ManagedSwitch,
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
[SwitchLabel,
|
||||||
|
{ passive: true },
|
||||||
|
"The label"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure checkbox is off
|
||||||
|
assertSwitch({ state: SwitchState.Off })
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
await click(getSwitchLabel())
|
||||||
|
|
||||||
|
// Ensure state is still off
|
||||||
|
assertSwitch({ state: SwitchState.Off })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
328
src/lib/test-utils/interactions.ts
Normal file
328
src/lib/test-utils/interactions.ts
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
import { tick } from "svelte";
|
||||||
|
import { fireEvent } from '@testing-library/svelte'
|
||||||
|
|
||||||
|
export let Keys: Record<string, Partial<KeyboardEvent>> = {
|
||||||
|
Space: { key: ' ', keyCode: 32, charCode: 32 },
|
||||||
|
Enter: { key: 'Enter', keyCode: 13, charCode: 13 },
|
||||||
|
Escape: { key: 'Escape', keyCode: 27, charCode: 27 },
|
||||||
|
Backspace: { key: 'Backspace', keyCode: 8 },
|
||||||
|
|
||||||
|
ArrowLeft: { key: 'ArrowLeft', keyCode: 37 },
|
||||||
|
ArrowUp: { key: 'ArrowUp', keyCode: 38 },
|
||||||
|
ArrowRight: { key: 'ArrowRight', keyCode: 39 },
|
||||||
|
ArrowDown: { key: 'ArrowDown', keyCode: 40 },
|
||||||
|
|
||||||
|
Home: { key: 'Home', keyCode: 36 },
|
||||||
|
End: { key: 'End', keyCode: 35 },
|
||||||
|
|
||||||
|
PageUp: { key: 'PageUp', keyCode: 33 },
|
||||||
|
PageDown: { key: 'PageDown', keyCode: 34 },
|
||||||
|
|
||||||
|
Tab: { key: 'Tab', keyCode: 9, charCode: 9 },
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shift(event: Partial<KeyboardEvent>) {
|
||||||
|
return { ...event, shiftKey: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function word(input: string): Partial<KeyboardEvent>[] {
|
||||||
|
return input.split('').map(key => ({ key }))
|
||||||
|
}
|
||||||
|
|
||||||
|
let Default = Symbol()
|
||||||
|
let Ignore = Symbol()
|
||||||
|
|
||||||
|
let cancellations: Record<string | typeof Default, Record<string, Set<string>>> = {
|
||||||
|
[Default]: {
|
||||||
|
keydown: new Set(['keypress']),
|
||||||
|
keypress: new Set([]),
|
||||||
|
keyup: new Set([]),
|
||||||
|
},
|
||||||
|
[Keys.Enter.key!]: {
|
||||||
|
keydown: new Set(['keypress', 'click']),
|
||||||
|
keypress: new Set(['click']),
|
||||||
|
keyup: new Set([]),
|
||||||
|
},
|
||||||
|
[Keys.Space.key!]: {
|
||||||
|
keydown: new Set(['keypress', 'click']),
|
||||||
|
keypress: new Set([]),
|
||||||
|
keyup: new Set(['click']),
|
||||||
|
},
|
||||||
|
[Keys.Tab.key!]: {
|
||||||
|
keydown: new Set(['keypress', 'blur', 'focus']),
|
||||||
|
keypress: new Set([]),
|
||||||
|
keyup: new Set([]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let order: Record<
|
||||||
|
string | typeof Default,
|
||||||
|
((
|
||||||
|
element: Element,
|
||||||
|
event: Partial<KeyboardEvent | MouseEvent>
|
||||||
|
) => Promise<boolean | symbol> | typeof Ignore | Promise<Element>)[]
|
||||||
|
> = {
|
||||||
|
[Default]: [
|
||||||
|
async function keydown(element, event) {
|
||||||
|
return await fireEvent.keyDown(element, event)
|
||||||
|
},
|
||||||
|
async function keypress(element, event) {
|
||||||
|
return await fireEvent.keyPress(element, event)
|
||||||
|
},
|
||||||
|
async function keyup(element, event) {
|
||||||
|
return await fireEvent.keyUp(element, event)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[Keys.Enter.key!]: [
|
||||||
|
async function keydown(element, event) {
|
||||||
|
return await fireEvent.keyDown(element, event)
|
||||||
|
},
|
||||||
|
async function keypress(element, event) {
|
||||||
|
return await fireEvent.keyPress(element, event)
|
||||||
|
},
|
||||||
|
async function click(element, event) {
|
||||||
|
if (element instanceof HTMLButtonElement) return await fireEvent.click(element, event)
|
||||||
|
return Ignore
|
||||||
|
},
|
||||||
|
async function keyup(element, event) {
|
||||||
|
return await fireEvent.keyUp(element, event)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[Keys.Space.key!]: [
|
||||||
|
async function keydown(element, event) {
|
||||||
|
return await fireEvent.keyDown(element, event)
|
||||||
|
},
|
||||||
|
async function keypress(element, event) {
|
||||||
|
return await fireEvent.keyPress(element, event)
|
||||||
|
},
|
||||||
|
async function keyup(element, event) {
|
||||||
|
return await fireEvent.keyUp(element, event)
|
||||||
|
},
|
||||||
|
async function click(element, event) {
|
||||||
|
if (element instanceof HTMLButtonElement) return await fireEvent.click(element, event)
|
||||||
|
return Ignore
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[Keys.Tab.key!]: [
|
||||||
|
async function keydown(element, event) {
|
||||||
|
return await fireEvent.keyDown(element, event)
|
||||||
|
},
|
||||||
|
async function blurAndfocus(_element, event) {
|
||||||
|
return focusNext(event)
|
||||||
|
},
|
||||||
|
async function keyup(element, event) {
|
||||||
|
return await fireEvent.keyUp(element, event)
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function type(events: Partial<KeyboardEvent>[], element = document.activeElement) {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (element === null) return expect(element).not.toBe(null)
|
||||||
|
|
||||||
|
for (let event of events) {
|
||||||
|
let skip = new Set()
|
||||||
|
let actions = order[event.key!] ?? order[Default as any]
|
||||||
|
for (let action of actions) {
|
||||||
|
let checks = action.name.split('And')
|
||||||
|
if (checks.some(check => skip.has(check))) continue
|
||||||
|
|
||||||
|
let result: boolean | typeof Ignore | Element = await action(element, {
|
||||||
|
type: action.name,
|
||||||
|
charCode: event.key?.length === 1 ? event.key?.charCodeAt(0) : undefined,
|
||||||
|
...event,
|
||||||
|
})
|
||||||
|
if (result === Ignore) continue
|
||||||
|
if (result instanceof Element) {
|
||||||
|
element = result
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = !result
|
||||||
|
if (cancelled) {
|
||||||
|
let skippablesForKey = cancellations[event.key!] ?? cancellations[Default as any]
|
||||||
|
let skippables = skippablesForKey?.[action.name] ?? new Set()
|
||||||
|
|
||||||
|
for (let skippable of skippables) skip.add(skippable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want to actually wait in our tests, so let's advance
|
||||||
|
jest.runAllTimers()
|
||||||
|
|
||||||
|
await tick()
|
||||||
|
} catch (err: any) {
|
||||||
|
Error.captureStackTrace(err, type)
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
jest.useRealTimers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function press(event: Partial<KeyboardEvent>, element = document.activeElement) {
|
||||||
|
return type([event], element)
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MouseButton {
|
||||||
|
Left = 0,
|
||||||
|
Right = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function click(
|
||||||
|
element: Document | Element | Window | null,
|
||||||
|
button = MouseButton.Left
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (element === null) return expect(element).not.toBe(null)
|
||||||
|
|
||||||
|
let options = { button }
|
||||||
|
|
||||||
|
if (button === MouseButton.Left) {
|
||||||
|
// Cancel in pointerDown cancels mouseDown, mouseUp
|
||||||
|
let cancelled = !fireEvent.pointerDown(element, options)
|
||||||
|
if (!cancelled) {
|
||||||
|
fireEvent.mouseDown(element, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure to trigger a `focus` event if the element is focusable, or within a focusable element
|
||||||
|
let next: HTMLElement | null = element as HTMLElement | null
|
||||||
|
while (next !== null) {
|
||||||
|
if (next.matches(focusableSelector)) {
|
||||||
|
next.focus()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
next = next.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent.pointerUp(element, options)
|
||||||
|
if (!cancelled) {
|
||||||
|
fireEvent.mouseUp(element, options)
|
||||||
|
}
|
||||||
|
fireEvent.click(element, options)
|
||||||
|
} else if (button === MouseButton.Right) {
|
||||||
|
// Cancel in pointerDown cancels mouseDown, mouseUp
|
||||||
|
let cancelled = !fireEvent.pointerDown(element, options)
|
||||||
|
if (!cancelled) {
|
||||||
|
fireEvent.mouseDown(element, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only in Firefox:
|
||||||
|
fireEvent.pointerUp(element, options)
|
||||||
|
if (!cancelled) {
|
||||||
|
fireEvent.mouseUp(element, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await tick()
|
||||||
|
} catch (err: any) {
|
||||||
|
Error.captureStackTrace(err, click)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function focus(element: Document | Element | Window | null) {
|
||||||
|
try {
|
||||||
|
if (element === null) return expect(element).not.toBe(null)
|
||||||
|
|
||||||
|
fireEvent.focus(element)
|
||||||
|
|
||||||
|
await tick()
|
||||||
|
} catch (err: any) {
|
||||||
|
Error.captureStackTrace(err, focus)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 tick()
|
||||||
|
} catch (err: any) {
|
||||||
|
Error.captureStackTrace(err, mouseEnter)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 tick()
|
||||||
|
} catch (err: any) {
|
||||||
|
Error.captureStackTrace(err, mouseMove)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 tick()
|
||||||
|
} catch (err: any) {
|
||||||
|
Error.captureStackTrace(err, mouseLeave)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
function focusNext(event: Partial<KeyboardEvent>) {
|
||||||
|
let direction = event.shiftKey ? -1 : +1
|
||||||
|
let focusableElements = getFocusableElements()
|
||||||
|
let total = focusableElements.length
|
||||||
|
|
||||||
|
function innerFocusNext(offset = 0): Element {
|
||||||
|
let currentIdx = focusableElements.indexOf(document.activeElement as HTMLElement)
|
||||||
|
let next = focusableElements[(currentIdx + total + direction + offset) % total] as HTMLElement
|
||||||
|
|
||||||
|
if (next) next?.focus({ preventScroll: true })
|
||||||
|
|
||||||
|
if (next !== document.activeElement) return innerFocusNext(offset + direction)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerFocusNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credit:
|
||||||
|
// - https://stackoverflow.com/a/30753870
|
||||||
|
let focusableSelector = [
|
||||||
|
'[contentEditable=true]',
|
||||||
|
'[tabindex]',
|
||||||
|
'a[href]',
|
||||||
|
'area[href]',
|
||||||
|
'button:not([disabled])',
|
||||||
|
'iframe',
|
||||||
|
'input:not([disabled])',
|
||||||
|
'select:not([disabled])',
|
||||||
|
'textarea:not([disabled])',
|
||||||
|
]
|
||||||
|
.map(
|
||||||
|
process.env.NODE_ENV === 'test'
|
||||||
|
? // TODO: Remove this once JSDOM fixes the issue where an element that is
|
||||||
|
// "hidden" can be the document.activeElement, because this is not possible
|
||||||
|
// in real browsers.
|
||||||
|
selector => `${selector}:not([tabindex='-1']):not([style*='display: none'])`
|
||||||
|
: selector => `${selector}:not([tabindex='-1'])`
|
||||||
|
)
|
||||||
|
.join(',')
|
||||||
|
|
||||||
|
function getFocusableElements(container = document.body) {
|
||||||
|
if (!container) return []
|
||||||
|
return Array.from(container.querySelectorAll(focusableSelector))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user