blob: ecf8cd186263e543755e3f9c2a7048cf54e96985 [file] [log] [blame]
/** @module golden/test_util
* @description
*
* <p>
* A general set of useful functions for tests and demos,
* e.g. reducing boilerplate.
* </p>
*/
/**
* 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 {string} Name of the element to test, e.g. 'foo-sk'.
* @return {Function} 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(elementName) {
let element;
afterEach(() => {
if (element) {
document.body.removeChild(element);
element = null;
}
});
return (finishSetupCallbackFn) => {
element = document.createElement(elementName);
if (finishSetupCallbackFn !== undefined) {
finishSetupCallbackFn(element);
}
document.body.appendChild(element);
return element;
};
}
/**
* Returns a promise that will resolve when the given DOM event is caught at the
* document's body element, 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 {string} Name of event to catch.
* @param timeoutMillis {number} How long to wait for the event before rejecting
* the returned promise.
* @return {Promise} A promise that will resolve to the caught event.
*/
export function eventPromise(event, timeoutMillis = 5000) {
const eventCaughtCallback = (resolve, _, e) => resolve(e);
const timeoutCallback = (_, reject) => reject(new Error(`timed out after ${timeoutMillis} ms `
+ `while waiting to catch event "${event}"`));
return buildEventPromise(
event, timeoutMillis, eventCaughtCallback, timeoutCallback,
);
}
/**
* Returns a promise that will resolve if the given DOM event is *not* caught at
* the document's body element 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 {string} Name of event to catch.
* @param timeoutMillis {number} How long to wait for the event before rejecting
* the returned promise.
* @return {Promise} A promise that will resolve to the caught event.
*/
export function noEventPromise(event, timeoutMillis = 200) {
const eventCaughtCallback = (_, reject) => reject(new Error(`event "${event}" was caught when none was expected`));
const timeoutCallback = (resolve, _) => resolve();
return buildEventPromise(
event, timeoutMillis, eventCaughtCallback, timeoutCallback,
);
}
/**
* Helper function to construct promises based on DOM events.
*
* @param event {string} Name of event to add a listener for at document.body.
* @param timeoutMillis {number} Milliseconds to wait before timing out.
* @param eventCaughtCallback {Function} 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 {Promise} A promise that will resolve or reject based exclusively on
* what the callback functions do with the resolve and reject parameters.
*/
function buildEventPromise(
event, timeoutMillis, eventCaughtCallback, timeoutCallback,
) {
// 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.body before returning.
return new Promise((resolve, reject) => {
let timeout;
const handler = (e) => {
document.body.removeEventListener(event, handler);
clearTimeout(timeout);
eventCaughtCallback(resolve, reject, e);
};
// 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 = setTimeout(() => {
document.body.removeEventListener(event, handler);
timeoutCallback(resolve, reject);
}, timeoutMillis);
}
document.body.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) {
expect(window.location.search).to.equal(expected);
}