blob: 54559e5748d574ec565b25047d7db552f307aeeb [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* These helpers are designed to be used when testing components or other code that renders into the DOM.
* By using these helpers we ensure the DOM is correctly cleaned between test runs.
*
* Note that `resetTestDOM` is automatically run before each test (see `test_setup.ts`).
**/
import type * as NodeText from '../../../../front_end/ui/components/node_text/node_text.js';
const {assert} = chai;
const TEST_CONTAINER_ID = '__devtools-test-container-id';
interface RenderOptions {
allowMultipleChildren?: boolean;
}
/**
* Renders a given element into the DOM. By default it will error if it finds an element already rendered but this can be controlled via the options.
**/
export const renderElementIntoDOM = (element: HTMLElement, renderOptions: RenderOptions = {}) => {
const container = document.getElementById(TEST_CONTAINER_ID);
if (!container) {
assert.fail(`renderIntoDOM expected to find ${TEST_CONTAINER_ID}`);
return;
}
const allowMultipleChildren = Boolean(renderOptions.allowMultipleChildren);
if (container.childNodes.length !== 0 && !allowMultipleChildren) {
assert.fail('renderIntoDOM expects the container to be empty');
return;
}
container.appendChild(element);
return element;
};
/**
* Completely cleans out the test DOM to ensure it's empty for the next test run.
* This is run automatically between tests - you should not be manually calling this yourself.
**/
export const resetTestDOM = () => {
const previousContainer = document.getElementById(TEST_CONTAINER_ID);
if (previousContainer) {
previousContainer.remove();
}
const newContainer = document.createElement('div');
newContainer.id = TEST_CONTAINER_ID;
document.body.appendChild(newContainer);
};
/**
* An easy way to assert the component's shadowRoot exists so you're able to assert on its contents.
*/
export function assertShadowRoot(shadowRoot: ShadowRoot|null): asserts shadowRoot is ShadowRoot {
assert.instanceOf(shadowRoot, ShadowRoot);
}
type Constructor<T> = {
new (...args: unknown[]): T,
};
/**
* Asserts that `element` is of type `T`.
*/
export function assertElement<T extends Element>(
element: Element|null, elementClass: Constructor<T>): asserts element is T {
assert.instanceOf(element, elementClass);
}
/**
* Asserts that all emenents of `nodeList` are at least of type `T`.
*/
export function assertElements<T extends Element>(
nodeList: NodeListOf<Element>, elementClass: Constructor<T>): asserts nodeList is NodeListOf<T> {
nodeList.forEach(e => assert.instanceOf(e, elementClass));
}
export function getElementWithinComponent<T extends HTMLElement, V extends Element>(
component: T, selector: string, elementClass: Constructor<V>) {
assertShadowRoot(component.shadowRoot);
const element = component.shadowRoot.querySelector(selector);
assertElement(element, elementClass);
return element;
}
export function getElementsWithinComponent<T extends HTMLElement, V extends Element>(
component: T, selector: string, elementClass: Constructor<V>) {
assertShadowRoot(component.shadowRoot);
const elements = component.shadowRoot.querySelectorAll(selector);
assertElements(elements, elementClass);
return elements;
}
/* Waits for the given element to have a scrollLeft property of at least desiredScrollLeft */
export function waitForScrollLeft<T extends Element>(element: T, desiredScrollLeft: number): Promise<void> {
let lastScrollLeft = -1;
let scrollLeftTimeout: number|null = null;
const timeBetweenPolls = 50;
return new Promise(resolve => {
const pollForScrollLeft = () => {
const newScrollLeft = element.scrollLeft;
// If we get the same scroll value twice in a row, and it's at least what
// we want, we're done!
if (lastScrollLeft === newScrollLeft && newScrollLeft >= desiredScrollLeft) {
if (scrollLeftTimeout) {
window.clearTimeout(scrollLeftTimeout);
}
resolve();
return;
}
lastScrollLeft = newScrollLeft;
scrollLeftTimeout = window.setTimeout(pollForScrollLeft, timeBetweenPolls);
};
window.setTimeout(pollForScrollLeft, timeBetweenPolls);
});
}
/**
* Dispatches a mouse click event.
*/
export function dispatchClickEvent<T extends Element>(element: T, options: MouseEventInit = {}) {
const clickEvent = new MouseEvent('click', options);
element.dispatchEvent(clickEvent);
}
export function dispatchFocusEvent<T extends Element>(element: T, options: FocusEventInit = {}) {
const focusEvent = new FocusEvent('focus', options);
element.dispatchEvent(focusEvent);
}
export function dispatchFocusOutEvent<T extends Element>(element: T, options: FocusEventInit = {}) {
const focusEvent = new FocusEvent('focusout', options);
element.dispatchEvent(focusEvent);
}
/**
* Dispatches a keydown event. Errors if the event was not dispatched successfully.
*/
export function dispatchKeyDownEvent<T extends Element>(element: T, options: KeyboardEventInit = {}) {
const clickEvent = new KeyboardEvent('keydown', options);
const success = element.dispatchEvent(clickEvent);
if (!success) {
assert.fail('Failed to trigger keydown event successfully.');
}
}
export function dispatchInputEvent<T extends Element>(element: T, options: InputEventInit = {}) {
const inputEvent = new InputEvent('input', options);
element.dispatchEvent(inputEvent);
}
/**
* Dispatches a mouse over event.
*/
export function dispatchMouseOverEvent<T extends Element>(element: T, options: MouseEventInit = {}) {
const moveEvent = new MouseEvent('mouseover', options);
element.dispatchEvent(moveEvent);
}
/**
* Dispatches a mouse out event.
*/
export function dispatchMouseOutEvent<T extends Element>(element: T, options: MouseEventInit = {}) {
const moveEvent = new MouseEvent('mouseout', options);
element.dispatchEvent(moveEvent);
}
/**
* Dispatches a mouse move event.
*/
export function dispatchMouseMoveEvent<T extends Element>(element: T, options: MouseEventInit = {}) {
const moveEvent = new MouseEvent('mousemove', options);
element.dispatchEvent(moveEvent);
}
/**
* Dispatches a mouse leave event.
*/
export function dispatchMouseLeaveEvent<T extends Element>(element: T, options: MouseEventInit = {}) {
const leaveEvent = new MouseEvent('mouseleave', options);
element.dispatchEvent(leaveEvent);
}
/**
* Dispatches a clipboard copy event.
*/
export function dispatchCopyEvent<T extends Element>(element: T, options: ClipboardEventInit = {}) {
const copyEvent = new ClipboardEvent('copy', options);
element.dispatchEvent(copyEvent);
}
/**
* Dispatches a clipboard paste event.
*/
export function dispatchPasteEvent<T extends Element>(element: T, options: ClipboardEventInit = {}) {
const pasteEvent = new ClipboardEvent('paste', options);
element.dispatchEvent(pasteEvent);
}
/**
* Listens to an event of an element and returns a Promise that resolves to the
* specified event type.
*/
export function getEventPromise<T extends Event>(element: HTMLElement, eventName: string): Promise<T> {
return new Promise<T>(resolve => {
element.addEventListener(eventName, (event: Event) => {
resolve(event as T);
}, {once: true});
});
}
export async function doubleRaf() {
return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
}
export async function raf() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
/**
* It's useful to use innerHTML in the tests to have full confidence in the
* renderer output, but LitHtml uses comment nodes to split dynamic from
* static parts of a template, and we don't want our tests full of noise
* from those.
*/
export function stripLitHtmlCommentNodes(text: string) {
/**
* LitHtml comments take the form of:
* <!--?lit$1234?--> or:
* <!--?-->
* And this regex matches both.
*/
return text.replaceAll(/<!--\?(lit\$[0-9]+\$)?-->/g, '');
}
/**
* Returns an array of textContents.
* Multiple consecutive newLine and space characters are removed.
*/
export function getCleanTextContentFromElements(el: ShadowRoot|HTMLElement, selector: string): string[] {
const elements = Array.from(el.querySelectorAll(selector));
return elements.map(element => {
return element.textContent ? element.textContent.trim().replace(/[ \n]{2,}/g, '') : '';
});
}
export function assertNodeTextContent(component: NodeText.NodeText.NodeText, expectedContent: string) {
assertShadowRoot(component.shadowRoot);
const content = Array.from(component.shadowRoot.querySelectorAll('span')).map(span => span.textContent).join('');
assert.strictEqual(content, expectedContent);
}