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

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());
};
}