Didn't run this on every single file because it messes up the formatting of the some of the TestRenderer constructions
This commit is contained in:
Ryan Gossiaux
2021-12-28 09:23:42 -10:00
parent 23a98b50ed
commit c99b74c089
21 changed files with 1212 additions and 1033 deletions

View File

@@ -1,4 +1,4 @@
import { Dialog, DialogDescription, DialogOverlay, DialogTitle } from "."
import { Dialog, DialogDescription, DialogOverlay, DialogTitle } from ".";
import TestTabSentinel from "./_TestTabSentinel.svelte";
import ManagedDialog from "./_ManagedDialog.svelte";
import NestedTestComponent from "./_NestedTestComponent.svelte";
@@ -10,95 +10,100 @@ import Div from "$lib/internal/elements/Div.svelte";
import Form from "$lib/internal/elements/Form.svelte";
import P from "$lib/internal/elements/P.svelte";
import Input from "$lib/internal/elements/Input.svelte";
import { assertActiveElement, assertDialog, assertDialogDescription, DialogState, getByText, getDialog, getDialogOverlay, getDialogOverlays, getDialogs } from "$lib/test-utils/accessibility-assertions";
import {
assertActiveElement,
assertDialog,
assertDialogDescription,
DialogState,
getByText,
getDialog,
getDialogOverlay,
getDialogOverlays,
getDialogs,
} from "$lib/test-utils/accessibility-assertions";
import { click, Keys, press } from "$lib/test-utils/interactions";
import Transition from "$lib/components/transitions/TransitionRoot.svelte";
import { tick } from "svelte";
let id = 0;
jest.mock('../../hooks/use-id', () => {
jest.mock("../../hooks/use-id", () => {
return {
useId: jest.fn(() => ++id),
}
})
};
});
// @ts-expect-error
global.IntersectionObserver = class FakeIntersectionObserver {
observe() { }
disconnect() { }
}
observe() {}
disconnect() {}
};
beforeEach(() => id = 0)
afterAll(() => jest.restoreAllMocks())
beforeEach(() => (id = 0));
afterAll(() => jest.restoreAllMocks());
describe('Safe guards', () => {
describe("Safe guards", () => {
it.each([
['DialogOverlay', DialogOverlay],
['DialogTitle', DialogTitle],
["DialogOverlay", DialogOverlay],
["DialogTitle", DialogTitle],
])(
'should error when we are using a <%s /> without a parent <Dialog />',
"should error when we are using a <%s /> without a parent <Dialog />",
suppressConsoleLogs((name, Component) => {
expect(() => render(Component)).toThrowError(
`<${name} /> is missing a parent <Dialog /> component.`
)
expect.hasAssertions()
);
expect.hasAssertions();
})
)
);
it(
'should be possible to render a Dialog without crashing',
"should be possible to render a Dialog without crashing",
suppressConsoleLogs(async () => {
render(
TestRenderer, {
render(TestRenderer, {
allProps: [
Dialog,
{ open: false, onClose: console.log },
[
[Button,
{},
"Trigger"],
[Button, {}, "Trigger"],
[DialogOverlay],
[DialogTitle],
[P, {}, "Contents"],
[DialogDescription]
]
]
})
[DialogDescription],
],
],
});
assertDialog({ state: DialogState.InvisibleUnmounted })
assertDialog({ state: DialogState.InvisibleUnmounted });
})
)
})
);
});
describe('Rendering', () => {
describe('Dialog', () => {
describe("Rendering", () => {
describe("Dialog", () => {
it(
'should complain when the `open` and `onClose` prop are missing',
suppressConsoleLogs(async () => {
expect(() => render(Dialog, { as: "div" })).toThrowErrorMatchingInlineSnapshot(
`"You forgot to provide an \`open\` prop to the \`Dialog\` component."`
)
expect.hasAssertions()
})
)
it(
'should complain when an `open` prop is not a boolean',
"should complain when the `open` and `onClose` prop are missing",
suppressConsoleLogs(async () => {
expect(() =>
render(
TestRenderer, {
allProps: [
Dialog,
{ open: null, onClose: console.log, as: "div" },
]
render(Dialog, { as: "div" })
).toThrowErrorMatchingInlineSnapshot(
`"You forgot to provide an \`open\` prop to the \`Dialog\` component."`
);
expect.hasAssertions();
})
);
it(
"should complain when an `open` prop is not a boolean",
suppressConsoleLogs(async () => {
expect(() =>
render(TestRenderer, {
allProps: [Dialog, { open: null, onClose: console.log, as: "div" }],
})
).toThrowErrorMatchingInlineSnapshot(
`"You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: null"`
)
expect.hasAssertions()
);
expect.hasAssertions();
})
)
);
// TODO: render prop tests!

View File

@@ -2,4 +2,3 @@ export { default as Dialog } from "./Dialog.svelte";
export { default as DialogTitle } from "./DialogTitle.svelte";
export { default as DialogOverlay } from "./DialogOverlay.svelte";
export { default as DialogDescription } from "./../description/Description.svelte";

View File

@@ -2,70 +2,76 @@ import { Disclosure, DisclosureButton, DisclosurePanel } from ".";
import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs";
import { render } from "@testing-library/svelte";
import TestRenderer from "$lib/test-utils/TestRenderer.svelte";
import { assertActiveElement, assertDisclosureButton, assertDisclosurePanel, DisclosureState, getByText, getDisclosureButton, getDisclosurePanel } from "$lib/test-utils/accessibility-assertions";
import {
assertActiveElement,
assertDisclosureButton,
assertDisclosurePanel,
DisclosureState,
getByText,
getDisclosureButton,
getDisclosurePanel,
} from "$lib/test-utils/accessibility-assertions";
import { click, Keys, MouseButton, press } from "$lib/test-utils/interactions";
import { Transition, TransitionChild } from "../transitions";
import TransitionDebug from "./_TransitionDebug.svelte";
let id = 0;
jest.mock('../../hooks/use-id', () => {
jest.mock("../../hooks/use-id", () => {
return {
useId: jest.fn(() => ++id),
}
})
};
});
beforeEach(() => id = 0)
afterAll(() => jest.restoreAllMocks())
beforeEach(() => (id = 0));
afterAll(() => jest.restoreAllMocks());
function nextFrame() {
return new Promise<void>(resolve => {
return new Promise<void>((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
resolve()
})
})
})
resolve();
});
});
});
}
describe('Safe guards', () => {
describe("Safe guards", () => {
it.each([
['DisclosureButton', DisclosureButton],
['DisclosurePanel', DisclosurePanel],
["DisclosureButton", DisclosureButton],
["DisclosurePanel", DisclosurePanel],
])(
'should error when we are using a <%s /> without a parent <Disclosure />',
"should error when we are using a <%s /> without a parent <Disclosure />",
suppressConsoleLogs((name, Component) => {
expect(() => render(Component)).toThrowError(
`<${name} /> is missing a parent <Disclosure /> component.`
)
);
})
)
);
it(
'should be possible to render a Disclosure without crashing',
"should be possible to render a Disclosure without crashing",
suppressConsoleLogs(async () => {
render(
TestRenderer, {
render(TestRenderer, {
allProps: [
Disclosure,
{},
[
[DisclosureButton, {}, "Trigger"],
[DisclosurePanel, {}, "Contents"],
]
]
})
],
],
});
assertDisclosureButton({
state: DisclosureState.InvisibleUnmounted,
attributes: { id: 'headlessui-disclosure-button-1' },
})
assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted })
attributes: { id: "headlessui-disclosure-button-1" },
});
assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted });
})
)
})
);
});
describe('Rendering', () => {
describe("Rendering", () => {
// describe('Disclosure', () => {
// it(
// 'should be possible to render a Disclosure using a render prop',
@@ -242,7 +248,7 @@ describe('Rendering', () => {
// )
// })
describe('DisclosureButton', () => {
describe("DisclosureButton", () => {
// it(
// 'should be possible to render a DisclosureButton using a render prop',
// suppressConsoleLogs(async () => {

View File

@@ -1,9 +1,44 @@
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from ".";
import {
Listbox,
ListboxButton,
ListboxLabel,
ListboxOption,
ListboxOptions,
} from ".";
import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs";
import { render } from "@testing-library/svelte";
import TestRenderer from "$lib/test-utils/TestRenderer.svelte";
import { assertActiveElement, assertActiveListboxOption, assertListbox, assertListboxButton, assertListboxButtonLinkedWithListbox, assertListboxButtonLinkedWithListboxLabel, assertListboxOption, assertNoActiveListboxOption, assertNoSelectedListboxOption, getByText, getListbox, getListboxButton, getListboxButtons, getListboxes, getListboxLabel, getListboxOptions, ListboxState } from "$lib/test-utils/accessibility-assertions";
import { click, focus, Keys, MouseButton, mouseLeave, mouseMove, press, shift, type, word } from "$lib/test-utils/interactions";
import {
assertActiveElement,
assertActiveListboxOption,
assertListbox,
assertListboxButton,
assertListboxButtonLinkedWithListbox,
assertListboxButtonLinkedWithListboxLabel,
assertListboxOption,
assertNoActiveListboxOption,
assertNoSelectedListboxOption,
getByText,
getListbox,
getListboxButton,
getListboxButtons,
getListboxes,
getListboxLabel,
getListboxOptions,
ListboxState,
} from "$lib/test-utils/accessibility-assertions";
import {
click,
focus,
Keys,
MouseButton,
mouseLeave,
mouseMove,
press,
shift,
type,
word,
} from "$lib/test-utils/interactions";
import { Transition } from "../transitions";
import TransitionDebug from "$lib/components/disclosure/_TransitionDebug.svelte";
import ManagedListbox from "./_ManagedListbox.svelte";
@@ -215,9 +250,9 @@ describe('Rendering', () => {
// assertListbox({ state: ListboxState.Visible })
// })
// )
})
});
describe('ListboxButton', () => {
describe("ListboxButton", () => {
// it(
// 'should be possible to render a ListboxButton using a render prop',
// suppressConsoleLogs(async () => {

View File

@@ -2,4 +2,3 @@ export { default as RadioGroup } from "./RadioGroup.svelte";
export { default as RadioGroupOption } from "./RadioGroupOption.svelte";
export { default as RadioGroupLabel } from "../label/Label.svelte";
export { default as RadioGroupDescription } from "../description/Description.svelte";

View File

@@ -1,4 +1,11 @@
import { assertActiveElement, assertFocusable, assertNotFocusable, assertRadioGroupLabel, getByText, getRadioGroupOptions } from "$lib/test-utils/accessibility-assertions";
import {
assertActiveElement,
assertFocusable,
assertNotFocusable,
assertRadioGroupLabel,
getByText,
getRadioGroupOptions,
} from "$lib/test-utils/accessibility-assertions";
import { render } from "@testing-library/svelte";
import { RadioGroup, RadioGroupLabel, RadioGroupOption } from ".";
import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs";

View File

@@ -1,26 +1,29 @@
import { render } from "@testing-library/svelte";
import TestRenderer from "../../test-utils/TestRenderer.svelte";
import { Switch, SwitchDescription, SwitchGroup, SwitchLabel } from ".";
import { assertActiveElement, assertSwitch, getSwitch, getSwitchLabel, 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 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', () => {
it('should be possible to render a Switch without crashing', () => {
describe("Safe guards", () => {
it("should be possible to render a Switch without crashing", () => {
render(TestRenderer, {
allProps: [
Switch,
{ checked: false, onChange: console.log }
]
allProps: [Switch, { checked: false, onChange: console.log }],
});
})
})
});
});
describe('Rendering', () => {
describe("Rendering", () => {
// TODO: handle these render prop (slot prop) tests
// it('should be possible to render an (on) Switch using a render prop', () => {
@@ -43,383 +46,334 @@ describe('Rendering', () => {
// assertSwitch({ state: SwitchState.Off, textContent: 'Off' })
// })
it('should be possible to render an (on) Switch using an `as` prop', () => {
it("should be possible to render an (on) Switch using an `as` prop", () => {
render(TestRenderer, {
allProps: [
Switch,
{ as: "span", checked: true, onChange: console.log },
]
allProps: [Switch, { as: "span", checked: true, onChange: console.log }],
});
assertSwitch({ state: SwitchState.On, tag: 'span' })
})
assertSwitch({ state: SwitchState.On, tag: "span" });
});
it('should be possible to render an (off) Switch using an `as` prop', () => {
it("should be possible to render an (off) Switch using an `as` prop", () => {
render(TestRenderer, {
allProps: [
Switch,
{ as: "span", checked: false, onChange: console.log },
]
allProps: [Switch, { as: "span", checked: false, onChange: console.log }],
});
assertSwitch({ state: SwitchState.Off, tag: 'span' })
})
assertSwitch({ state: SwitchState.Off, tag: "span" });
});
it('should be possible to use the switch contents as the label', () => {
it("should be possible to use the switch contents as the label", () => {
render(TestRenderer, {
allProps: [
Switch,
{ checked: false, onChange: console.log },
[
Span,
{},
"Enable notifications"
]
]
[Span, {}, "Enable notifications"],
],
});
assertSwitch({ state: SwitchState.Off, label: 'Enable notifications' })
})
assertSwitch({ state: SwitchState.Off, label: "Enable notifications" });
});
describe('`type` attribute', () => {
describe("`type` attribute", () => {
it('should set the `type` to "button" by default', async () => {
render(TestRenderer, {
allProps: [
Switch,
{ checked: false, onChange: console.log },
"Trigger"
]
"Trigger",
],
});
expect(getSwitch()).toHaveAttribute('type', 'button')
})
expect(getSwitch()).toHaveAttribute("type", "button");
});
it('should not set the `type` to "button" if it already contains a `type`', async () => {
render(TestRenderer, {
allProps: [
Switch,
{ checked: false, onChange: console.log, type: "submit" },
"Trigger"
]
"Trigger",
],
});
expect(getSwitch()).toHaveAttribute('type', 'submit')
})
expect(getSwitch()).toHaveAttribute("type", "submit");
});
it('should not set the type if the "as" prop is not a "button"', async () => {
render(TestRenderer, {
allProps: [
Switch,
{ checked: false, onChange: console.log, as: "div" },
"Trigger"
]
"Trigger",
],
});
expect(getSwitch()).not.toHaveAttribute('type')
})
})
})
expect(getSwitch()).not.toHaveAttribute("type");
});
});
});
describe('Render composition', () => {
it('should be possible to render a Switch.Group, Switch and Switch.Label', () => {
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"
]
]
]
})
[Switch, { checked: false, onChange: console.log }],
[SwitchLabel, {}, "Enable notifications"],
],
],
});
assertSwitch({ state: SwitchState.Off, label: '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)', () => {
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"]
]
]
})
[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' })
})
assertSwitch({ state: SwitchState.Off, label: "Label B" });
});
it('should be possible to render a Switch.Group, Switch and Switch.Label (after the Switch)', () => {
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"]
]
]
})
[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' })
})
assertSwitch({ state: SwitchState.Off, label: "Label B" });
});
it('should be possible to render a Switch.Group, Switch and Switch.Description (before the Switch)', async () => {
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"],
]
]
})
[SwitchDescription, {}, "This is an important feature"],
[Switch, { checked: false, onChange: console.log }],
],
],
});
assertSwitch({
state: SwitchState.Off,
label: 'Label A',
description: 'This is an important feature',
})
})
})
description: "This is an important feature",
});
});
describe('Keyboard interactions', () => {
describe('`Space` key', () => {
it('should be possible to toggle the Switch with Space', async () => {
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,
{},
]
})
allProps: [ManagedSwitch, {}],
});
// Ensure checkbox is off
assertSwitch({ state: SwitchState.Off })
assertSwitch({ state: SwitchState.Off });
// Focus the switch
getSwitch()?.focus()
getSwitch()?.focus();
// Toggle
await press(Keys.Space)
await press(Keys.Space);
// Ensure state is on
assertSwitch({ state: SwitchState.On })
assertSwitch({ state: SwitchState.On });
// Toggle
await press(Keys.Space)
await press(Keys.Space);
// Ensure state is off
assertSwitch({ state: SwitchState.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()
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 },
]
})
allProps: [ManagedSwitch, { onChange: handleChange }],
});
// Ensure checkbox is off
assertSwitch({ state: SwitchState.Off })
assertSwitch({ state: SwitchState.Off });
// Focus the switch
getSwitch()?.focus()
getSwitch()?.focus();
// Try to toggle
await press(Keys.Enter)
await press(Keys.Enter);
expect(handleChange).not.toHaveBeenCalled()
})
})
expect(handleChange).not.toHaveBeenCalled();
});
});
describe('`Tab` key', () => {
it('should be possible to tab away from the Switch', async () => {
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"]
]
]
})
[Switch, { checked: false, onChange: console.log }],
[Button, { id: "btn" }, "Other element"],
],
],
});
// Ensure checkbox is off
assertSwitch({ state: SwitchState.Off })
assertSwitch({ state: SwitchState.Off });
// Focus the switch
getSwitch()?.focus()
getSwitch()?.focus();
// Expect the switch to be active
assertActiveElement(getSwitch())
assertActiveElement(getSwitch());
// Toggle
await press(Keys.Tab)
await press(Keys.Tab);
// Expect the button to be active
assertActiveElement(document.getElementById('btn'))
})
})
})
assertActiveElement(document.getElementById("btn"));
});
});
});
describe('Mouse interactions', () => {
it('should be possible to toggle the Switch with a click', async () => {
describe("Mouse interactions", () => {
it("should be possible to toggle the Switch with a click", async () => {
render(TestRenderer, {
allProps: [
ManagedSwitch,
{},
]
})
allProps: [ManagedSwitch, {}],
});
// Ensure checkbox is off
assertSwitch({ state: SwitchState.Off })
assertSwitch({ state: SwitchState.Off });
// Toggle
await click(getSwitch())
await click(getSwitch());
// Ensure state is on
assertSwitch({ state: SwitchState.On })
assertSwitch({ state: SwitchState.On });
// Toggle
await click(getSwitch())
await click(getSwitch());
// Ensure state is off
assertSwitch({ state: SwitchState.Off })
})
assertSwitch({ state: SwitchState.Off });
});
it('should be possible to toggle the Switch with a click on the Label', async () => {
it("should be possible to toggle the Switch with a click on the Label", async () => {
render(TestRenderer, {
allProps: [
SwitchGroup,
{},
[
[ManagedSwitch,
{},
],
[SwitchLabel,
{},
"The label"]
]
]
})
[ManagedSwitch, {}],
[SwitchLabel, {}, "The label"],
],
],
});
// Ensure checkbox is off
assertSwitch({ state: SwitchState.Off })
assertSwitch({ state: SwitchState.Off });
// Toggle
await click(getSwitchLabel())
await click(getSwitchLabel());
// Ensure the switch is focused
assertActiveElement(getSwitch())
assertActiveElement(getSwitch());
// Ensure state is on
assertSwitch({ state: SwitchState.On })
assertSwitch({ state: SwitchState.On });
// Toggle
await click(getSwitchLabel())
await click(getSwitchLabel());
// Ensure the switch is focused
assertActiveElement(getSwitch())
assertActiveElement(getSwitch());
// Ensure state is off
assertSwitch({ state: SwitchState.Off })
})
assertSwitch({ state: SwitchState.Off });
});
it('should not be possible to toggle the Switch with a click on the Label (passive)', async () => {
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"]
]
]
})
[ManagedSwitch, {}],
[SwitchLabel, { passive: true }, "The label"],
],
],
});
// Ensure checkbox is off
assertSwitch({ state: SwitchState.Off })
assertSwitch({ state: SwitchState.Off });
// Toggle
await click(getSwitchLabel())
await click(getSwitchLabel());
// Ensure state is still off
assertSwitch({ state: SwitchState.Off })
})
})
assertSwitch({ state: SwitchState.Off });
});
});

View File

@@ -52,7 +52,7 @@ export function useActions(
return {
update(actions: ActionArray) {
if (((actions && actions.length) || 0) != actionReturns.length) {
throw new Error('You must not change the length of an actions array.');
throw new Error("You must not change the length of an actions array.");
}
if (actions) {

View File

@@ -1,4 +1,7 @@
export function portal(element: HTMLElement, target: HTMLElement | null | undefined) {
export function portal(
element: HTMLElement,
target: HTMLElement | null | undefined
) {
if (target) {
target.append(element);
}

View File

@@ -40,46 +40,46 @@ import Strong from "./Strong.svelte";
import Ul from "./Ul.svelte";
const components = {
"a": A,
"address": Address,
"article": Article,
"aside": Aside,
"b": B,
"bdi": Bdi,
"bdo": Bdo,
"blockquote": Blockquote,
"button": Button,
"cite": Cite,
"code": Code,
"data": Data,
"datalist": Datalist,
"dd": Dd,
"dl": Dl,
"dt": Dt,
"div": Div,
"em": Em,
"footer": Footer,
"form": Form,
"h1": H1,
"h2": H2,
"h3": H3,
"h4": H4,
"h5": H5,
"h6": H6,
"header": Header,
"i": I,
"input": Input,
"label": Label,
"li": Li,
"main": Main,
"nav": Nav,
"ol": Ol,
"p": P,
"section": Section,
"span": Span,
"strong": Strong,
"ul": Ul,
}
a: A,
address: Address,
article: Article,
aside: Aside,
b: B,
bdi: Bdi,
bdo: Bdo,
blockquote: Blockquote,
button: Button,
cite: Cite,
code: Code,
data: Data,
datalist: Datalist,
dd: Dd,
dl: Dl,
dt: Dt,
div: Div,
em: Em,
footer: Footer,
form: Form,
h1: H1,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
h6: H6,
header: Header,
i: I,
input: Input,
label: Label,
li: Li,
main: Main,
nav: Nav,
ol: Ol,
p: P,
section: Section,
span: Span,
strong: Strong,
ul: Ul,
};
export type SupportedElement = keyof typeof components;
export type SupportedAs = SupportedElement | SvelteComponent;

File diff suppressed because it is too large Load Diff

View File

@@ -1,59 +1,62 @@
import { tick } from "svelte";
import { fireEvent } from '@testing-library/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 },
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 },
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 },
Home: { key: "Home", keyCode: 36 },
End: { key: "End", keyCode: 35 },
PageUp: { key: 'PageUp', keyCode: 33 },
PageDown: { key: 'PageDown', keyCode: 34 },
PageUp: { key: "PageUp", keyCode: 33 },
PageDown: { key: "PageDown", keyCode: 34 },
Tab: { key: 'Tab', keyCode: 9, charCode: 9 },
}
Tab: { key: "Tab", keyCode: 9, charCode: 9 },
};
export function shift(event: Partial<KeyboardEvent>) {
return { ...event, shiftKey: true }
return { ...event, shiftKey: true };
}
export function word(input: string): Partial<KeyboardEvent>[] {
return input.split('').map(key => ({ key }))
return input.split("").map((key) => ({ key }));
}
let Default = Symbol()
let Ignore = Symbol()
let Default = Symbol();
let Ignore = Symbol();
let cancellations: Record<string | typeof Default, Record<string, Set<string>>> = {
let cancellations: Record<
string | typeof Default,
Record<string, Set<string>>
> = {
[Default]: {
keydown: new Set(['keypress']),
keydown: new Set(["keypress"]),
keypress: new Set([]),
keyup: new Set([]),
},
[Keys.Enter.key!]: {
keydown: new Set(['keypress', 'click']),
keypress: new Set(['click']),
keydown: new Set(["keypress", "click"]),
keypress: new Set(["click"]),
keyup: new Set([]),
},
[Keys.Space.key!]: {
keydown: new Set(['keypress', 'click']),
keydown: new Set(["keypress", "click"]),
keypress: new Set([]),
keyup: new Set(['click']),
keyup: new Set(["click"]),
},
[Keys.Tab.key!]: {
keydown: new Set(['keypress', 'blur', 'focus']),
keydown: new Set(["keypress", "blur", "focus"]),
keypress: new Set([]),
keyup: new Set([]),
},
}
};
let order: Record<
string | typeof Default,
@@ -64,105 +67,115 @@ let order: Record<
> = {
[Default]: [
async function keydown(element, event) {
return await fireEvent.keyDown(element, event)
return await fireEvent.keyDown(element, event);
},
async function keypress(element, event) {
return await fireEvent.keyPress(element, event)
return await fireEvent.keyPress(element, event);
},
async function keyup(element, event) {
return await fireEvent.keyUp(element, event)
return await fireEvent.keyUp(element, event);
},
],
[Keys.Enter.key!]: [
async function keydown(element, event) {
return await fireEvent.keyDown(element, event)
return await fireEvent.keyDown(element, event);
},
async function keypress(element, event) {
return await fireEvent.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
if (element instanceof HTMLButtonElement)
return await fireEvent.click(element, event);
return Ignore;
},
async function keyup(element, event) {
return await fireEvent.keyUp(element, event)
return await fireEvent.keyUp(element, event);
},
],
[Keys.Space.key!]: [
async function keydown(element, event) {
return await fireEvent.keyDown(element, event)
return await fireEvent.keyDown(element, event);
},
async function keypress(element, event) {
return await fireEvent.keyPress(element, event)
return await fireEvent.keyPress(element, event);
},
async function keyup(element, event) {
return await fireEvent.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
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)
return await fireEvent.keyDown(element, event);
},
async function blurAndfocus(_element, event) {
return focusNext(event)
return focusNext(event);
},
async function keyup(element, event) {
return await fireEvent.keyUp(element, event)
return await fireEvent.keyUp(element, event);
},
],
}
};
export async function type(events: Partial<KeyboardEvent>[], element = document.activeElement) {
jest.useFakeTimers()
export async function type(
events: Partial<KeyboardEvent>[],
element = document.activeElement
) {
jest.useFakeTimers();
try {
if (element === null) return expect(element).not.toBe(null)
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]
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 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,
charCode:
event.key?.length === 1 ? event.key?.charCodeAt(0) : undefined,
...event,
})
if (result === Ignore) continue
});
if (result === Ignore) continue;
if (result instanceof Element) {
element = result
element = result;
}
let cancelled = !result
let cancelled = !result;
if (cancelled) {
let skippablesForKey = cancellations[event.key!] ?? cancellations[Default as any]
let skippables = skippablesForKey?.[action.name] ?? new Set()
let skippablesForKey =
cancellations[event.key!] ?? cancellations[Default as any];
let skippables = skippablesForKey?.[action.name] ?? new Set();
for (let skippable of skippables) skip.add(skippable)
for (let skippable of skippables) skip.add(skippable);
}
}
}
// We don't want to actually wait in our tests, so let's advance
jest.runAllTimers()
jest.runAllTimers();
await tick()
await tick();
} catch (err: any) {
Error.captureStackTrace(err, type)
throw err
Error.captureStackTrace(err, type);
throw err;
} finally {
jest.useRealTimers()
jest.useRealTimers();
}
}
export async function press(event: Partial<KeyboardEvent>, element = document.activeElement) {
return type([event], element)
export async function press(
event: Partial<KeyboardEvent>,
element = document.activeElement
) {
return type([event], element);
}
export enum MouseButton {
@@ -175,153 +188,158 @@ export async function click(
button = MouseButton.Left
) {
try {
if (element === null) return expect(element).not.toBe(null)
if (element === null) return expect(element).not.toBe(null);
let options = { button }
let options = { button };
if (button === MouseButton.Left) {
// Cancel in pointerDown cancels mouseDown, mouseUp
let cancelled = !(await fireEvent.pointerDown(element, options))
let cancelled = !(await fireEvent.pointerDown(element, options));
if (!cancelled) {
await 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
let next: HTMLElement | null = element as HTMLElement | null
let next: HTMLElement | null = element as HTMLElement | null;
while (next !== null) {
if (next.matches(focusableSelector)) {
next.focus()
break
next.focus();
break;
}
next = next.parentElement
next = next.parentElement;
}
await fireEvent.pointerUp(element, options)
await fireEvent.pointerUp(element, options);
if (!cancelled) {
await fireEvent.mouseUp(element, options)
await fireEvent.mouseUp(element, options);
}
await fireEvent.click(element, options)
await fireEvent.click(element, options);
} else if (button === MouseButton.Right) {
// Cancel in pointerDown cancels mouseDown, mouseUp
let cancelled = !(await fireEvent.pointerDown(element, options))
let cancelled = !(await fireEvent.pointerDown(element, options));
if (!cancelled) {
await fireEvent.mouseDown(element, options)
await fireEvent.mouseDown(element, options);
}
// Only in Firefox:
await fireEvent.pointerUp(element, options)
await fireEvent.pointerUp(element, options);
if (!cancelled) {
await fireEvent.mouseUp(element, options)
await fireEvent.mouseUp(element, options);
}
}
} catch (err: any) {
Error.captureStackTrace(err, click)
throw err
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)
if (element === null) return expect(element).not.toBe(null);
await fireEvent.focus(element)
await fireEvent.focus(element);
await tick()
await tick();
} catch (err: any) {
Error.captureStackTrace(err, focus)
throw err
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)
if (element === null) return expect(element).not.toBe(null);
await fireEvent.pointerOver(element)
await fireEvent.pointerEnter(element)
await fireEvent.mouseOver(element)
await fireEvent.pointerOver(element);
await fireEvent.pointerEnter(element);
await fireEvent.mouseOver(element);
await tick()
await tick();
} catch (err: any) {
Error.captureStackTrace(err, mouseEnter)
throw err
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)
if (element === null) return expect(element).not.toBe(null);
await fireEvent.pointerMove(element)
await fireEvent.mouseMove(element)
await fireEvent.pointerMove(element);
await fireEvent.mouseMove(element);
await tick()
await tick();
} catch (err: any) {
Error.captureStackTrace(err, mouseMove)
throw err
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)
if (element === null) return expect(element).not.toBe(null);
await fireEvent.pointerOut(element)
await fireEvent.pointerLeave(element)
await fireEvent.mouseOut(element)
await fireEvent.mouseLeave(element)
await fireEvent.pointerOut(element);
await fireEvent.pointerLeave(element);
await fireEvent.mouseOut(element);
await fireEvent.mouseLeave(element);
await tick()
await tick();
} catch (err: any) {
Error.captureStackTrace(err, mouseLeave)
throw err
Error.captureStackTrace(err, mouseLeave);
throw err;
}
}
// ---
function focusNext(event: Partial<KeyboardEvent>) {
let direction = event.shiftKey ? -1 : +1
let focusableElements = getFocusableElements()
let total = focusableElements.length
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
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) next?.focus({ preventScroll: true });
if (next !== document.activeElement) return innerFocusNext(offset + direction)
return next
if (next !== document.activeElement)
return innerFocusNext(offset + direction);
return next;
}
return innerFocusNext()
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])',
"[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'
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'])`
// "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(',')
.join(",");
function getFocusableElements(container = document.body) {
if (!container) return []
return Array.from(container.querySelectorAll(focusableSelector))
if (!container) return [];
return Array.from(container.querySelectorAll(focusableSelector));
}

View File

@@ -1,17 +1,17 @@
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T] &
string
string;
export function suppressConsoleLogs<T extends unknown[]>(
cb: (...args: T) => unknown,
type: FunctionPropertyNames<typeof global.console> = 'error'
type: FunctionPropertyNames<typeof global.console> = "error"
) {
return (...args: T) => {
let spy = jest.spyOn(global.console, type).mockImplementation(jest.fn())
let spy = jest.spyOn(global.console, type).mockImplementation(jest.fn());
return new Promise<unknown>((resolve, reject) => {
Promise.resolve(cb(...args)).then(resolve, reject)
}).finally(() => spy.mockRestore())
}
Promise.resolve(cb(...args)).then(resolve, reject);
}).finally(() => spy.mockRestore());
};
}

View File

@@ -1,8 +1,12 @@
import type { SupportedAs } from "$lib/internal/elements";
export function resolveButtonType(props: { type?: string, as?: SupportedAs }, ref: HTMLElement | null | undefined): string | undefined {
export function resolveButtonType(
props: { type?: string; as?: SupportedAs },
ref: HTMLElement | null | undefined
): string | undefined {
if (props.type) return props.type;
let tag = props.as ?? "button";
if (typeof tag === "string" && tag.toLowerCase() === "button") return "button";
if (typeof tag === "string" && tag.toLowerCase() === "button")
return "button";
if (ref instanceof HTMLButtonElement) return "button";
return undefined;
}

View File

@@ -66,7 +66,7 @@ export function transition(
done?: (reason: Reason) => void
) {
let d = disposables();
let _done = done !== undefined ? once(done) : () => { };
let _done = done !== undefined ? once(done) : () => {};
removeClasses(node, ...entered);
addClasses(node, ...base, ...from);

View File

@@ -5,4 +5,3 @@
>
<slot />
</a>