| /** @module infra-sk/test_util |
| * @description |
| * |
| * <p> |
| * A general set of useful functions for tests and demos, |
| * e.g. reducing boilerplate. |
| * </p> |
| */ |
| |
| import { expect } from 'chai'; |
| |
| /** |
| * Takes a DOM element name (e.g. 'my-component-sk') and returns a factory |
| * function that can be used to obtain new instances of that element. |
| * |
| * The element returned by the factory function is attached to document.body, |
| * and an afterEach() hook is set to automatically remove the element from the |
| * DOM after each test. |
| * |
| * The returned factory function optionally takes a callback function that will |
| * be called with the newly instantiated element before it is attached to the |
| * DOM, giving client code a chance to finish setting up the element before e.g. |
| * the element's connecetedCallback() method is invoked. |
| * |
| * Sample usage: |
| * |
| * describe('my-component-sk', () => { |
| * const newInstance = setUpElementUnderTest('my-component-sk'); |
| * |
| * it('should be correctly instantiated', () => { |
| * const myComponentSk = newInstance((el) => { |
| * // This is called before attaching the element to the DOM. |
| * el.setAttribute('hello', 'world'); |
| * }); |
| * |
| * expect(myComponentSk.parentElement).to.equal(document.body); |
| * expect(myComponentSk.getAttribute('hello')).to.equal('world'); |
| * }); |
| * }); |
| * |
| * @param elementName Name of the element to test, e.g. 'foo-sk'. |
| * @return A factory function that optionally takes a callback which is invoked |
| * with the newly instantiated element before it is attached to the DOM. |
| */ |
| export function setUpElementUnderTest<T extends HTMLElement>(elementName: string): (finishSetupCallback?: (instance: T) => void) => T { |
| let element: T | null; |
| |
| afterEach(() => { |
| if (element) { |
| document.body.removeChild(element); |
| element = null; |
| } |
| }); |
| |
| return (finishSetupCallbackFn?: (instance: T) => void) => { |
| element = document.createElement(elementName) as T; |
| if (finishSetupCallbackFn) { |
| finishSetupCallbackFn(element); |
| } |
| document.body.appendChild(element); |
| return element; |
| }; |
| } |
| |
| /** |
| * Returns a promise that will resolve when the given DOM event is caught at |
| * document, or reject if the event isn't caught within the given |
| * amount of time. |
| * |
| * Sample usage: |
| * |
| * // Code under test. |
| * function doSomethingThatTriggersCustomEvent() { |
| * ... |
| * this.dispatchEvent( |
| * new CustomEvent('my-event', {detail: 'hello world', bubbles: true}); |
| * } |
| * |
| * // Test. |
| * it('should trigger a custom event', async () => { |
| * const myEvent = eventPromise('my-event'); |
| * doSomethingThatTriggersCustomEvent(); |
| * const ev = await myEvent; |
| * expect(ev.detail).to.equal('hello world'); |
| * }); |
| * |
| * Set timeoutMillis = 0 to skip call to setTimeout(). This is necessary on |
| * tests that simulate the passing of time with sinon.useFakeTimers(), which |
| * could trigger the timeout before the promise has a chance to catch the event. |
| * |
| * @param event Name of event to catch. |
| * @param timeoutMillis How long to wait for the event before rejecting the |
| * returned promise. |
| * @return A promise that will resolve to the caught event. |
| */ |
| export function eventPromise<T extends Event>(event: string, timeoutMillis = 5000) { |
| const eventCaughtCallback = (resolve: (event: T) => void, _: any, e: T) => resolve(e); |
| const timeoutCallback = |
| (_: any, reject: (reason: any) => void) => reject(new Error( |
| `timed out after ${timeoutMillis} ms while waiting to catch event "${event}"`)); |
| return buildEventPromise<T>(event, timeoutMillis, eventCaughtCallback, timeoutCallback); |
| } |
| |
| /** |
| * Returns a promise that will resolve if the given DOM event is *not* caught at |
| * document after the given amount of time, or reject if the |
| * event is caught. |
| * |
| * Useful for testing code that emits an event based on some condition. |
| * |
| * Sample usage: |
| * |
| * // Code under test. |
| * function maybeTriggerCustomEvent(condition) { |
| * if (condition) { |
| * this.dispatchEvent( |
| * new CustomEvent('my-event', {detail: 'hello world', bubbles: true}); |
| * } else { |
| * // Do nothing. |
| * } |
| * } |
| * |
| * // Test. |
| * it('should not trigger a custom event', async () => { |
| * const noEvent = noEventPromise('my-event'); |
| * maybeTriggerCustomEvent(false); |
| * await noEvent; |
| * }); |
| * |
| * Set timeoutMillis = 0 to skip call to setTimeout(). This is necessary on |
| * tests that simulate the passing of time with sinon.useFakeTimers(), which |
| * could trigger the timeout before the promise has a chance to catch the event. |
| * |
| * @param event Name of event to catch. |
| * @param timeoutMillis How long to wait for the event before rejecting the |
| * returned promise. |
| * @return A promise that will resolve to the caught event. |
| */ |
| export function noEventPromise(event: string, timeoutMillis = 200) { |
| const eventCaughtCallback = |
| (_: any, reject: (reason: any) => void) => |
| reject(new Error(`event "${event}" was caught when none was expected`)); |
| const timeoutCallback = (resolve: () => void) => resolve(); |
| return buildEventPromise<void>( |
| event, |
| timeoutMillis, |
| eventCaughtCallback, |
| timeoutCallback); |
| } |
| |
| /** |
| * Helper function to construct promises based on DOM events. |
| * |
| * @param event Name of event to add a listener for at document. |
| * @param timeoutMillis Milliseconds to wait before timing out. |
| * @param eventCaughtCallback Called when the event is caught with parameters |
| * (resolve, reject, event), where resolve and reject are the functions |
| * passed to the promise's executor function, and event is the Event object |
| * that was caught. |
| * @param timeoutCallback Called after timeoutMillis if no event is caught, |
| * with arguments (resolve, reject) as passed to eventCaughtCallback. |
| * @return A promise that will resolve or reject based exclusively on what the |
| * callback functions do with the resolve and reject parameters. |
| */ |
| function buildEventPromise<T extends Event | void>( |
| event: string, |
| timeoutMillis: number, |
| eventCaughtCallback: (resolve: (value?: T | PromiseLike<T> | undefined) => void, |
| reject: (reason?: any) => void, event: T) => void, |
| timeoutCallback: (resolve: (value?: T | PromiseLike<T> | undefined) => void, |
| reject: (reason?: any) => void) => void, |
| ) { |
| // The executor function passed as a constructor argument to the Promise |
| // object is executed immediately. This guarantees that the event handler |
| // is added to document before returning. |
| return new Promise<T>((resolve, reject) => { |
| let timeout: number; |
| |
| const handler = (e: Event) => { |
| document.removeEventListener(event, handler); |
| window.clearTimeout(timeout); |
| eventCaughtCallback(resolve, reject, e as T); |
| }; |
| |
| // Skip setTimeout() call with timeoutMillis = 0. Useful when faking time in |
| // tests with sinon.useFakeTimers(). See |
| // https://sinonjs.org/releases/v7.5.0/fake-timers/. |
| if (timeoutMillis !== 0) { |
| timeout = window.setTimeout(() => { |
| document.removeEventListener(event, handler); |
| timeoutCallback(resolve, reject); |
| }, timeoutMillis); |
| } |
| document.addEventListener(event, handler); |
| }); |
| } |
| |
| /** |
| * Asserts that there the given string exactly matches the current query string |
| * of the url bar. For example, expectQueryStringToEqual('?foo=bar&foo=orange'); |
| */ |
| export function expectQueryStringToEqual(expected: string) { |
| expect(window.location.search).to.equal(expected); |
| } |