Last active
November 2, 2020 16:13
-
-
Save mainfraame/e9013f498755c3c4fa1389df956303f6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {isEqual} from "lodash"; | |
import {outdent} from "outdent"; | |
import {act} from "react-dom/test-utils"; | |
import {fireEvent} from "@testing-library/react"; | |
import {dblClick} from "@testing-library/user-event"; | |
export type JSDOMElementProps = { | |
id?: string; | |
root?: string; | |
selector?: string; | |
}; | |
export class JSDOMElement<T = HTMLDivElement> { | |
component: string; | |
_element: T; | |
_id: string; | |
_rootSelector: string; | |
_selector: string; | |
constructor(props: JSDOMElementProps) { | |
const root = props.root ? `${props.root} ` : ""; | |
const selector = | |
props.selector || (props.id ? `[data-test-id="${props.id}"]` : ""); | |
this._id = props.id; | |
this._rootSelector = root; | |
this._selector = selector; | |
} | |
private element(): T { | |
if ( | |
// ensure the element is still valid | |
this._element?.isConnected && | |
this._element["__proto__"] | |
) { | |
return this._element; | |
} | |
this._element = document.querySelector(this.selector()); | |
return this._element; | |
} | |
hasClass(className: string): boolean { | |
return this.element().classList.contains(className); | |
} | |
blur(): void { | |
act(() => { | |
fireEvent.blur(this.element()); | |
}); | |
} | |
click(): void { | |
act(() => { | |
this.element().click(); | |
}); | |
} | |
dblClick(): void { | |
act(() => { | |
dblClick(this.element()); | |
}); | |
} | |
focus(): void { | |
act(() => { | |
fireEvent.focus(this.element()); | |
}); | |
} | |
getAttribute<T>(attr: string): T { | |
return this.element().getAttribute(attr) as T; | |
} | |
getDataAttrs(pick?: string[]): { [index: string]: any } { | |
const dataset = this.element().dataset; | |
return Object.keys(dataset) | |
.filter((key) => !pick || pick.includes(key)) | |
.reduce( | |
(acc, key) => ({ | |
...acc, | |
[key]: dataset[key], | |
}), | |
{} | |
); | |
} | |
getDataAttr<T>(attr: string): T { | |
return this.element().dataset[attr] as T; | |
} | |
getStyle(styleProp: string): string { | |
return this.element().style[styleProp]; | |
} | |
getValue(): any { | |
return this.element().value; | |
} | |
hasAttribute(attr: string): boolean { | |
return this.element().hasAttribute(attr); | |
} | |
isDisabled(): boolean { | |
return !!this.hasAttribute("disabled") === true; | |
} | |
isEnabled(): boolean { | |
return !this.isDisabled(); | |
} | |
isInDom(): boolean { | |
return !!this.element(); | |
} | |
innerText(): string { | |
// for some reason, i've found that some third party components will | |
// have their innerText only available in the textContent property | |
return (this.element().innerText || this.element().textContent) | |
.trim() | |
.replace(/\n/g, ""); | |
} | |
isReadOnly(): boolean { | |
return !!this.getAttribute("readOnly"); | |
} | |
isValid(): boolean { | |
// use a data attribute or use the native way to check input validity | |
return this.getDataAttr("valid"); | |
} | |
keyDown(key: string): void { | |
act(() => { | |
fireEvent.keyDown(this.element(), {key}); | |
}); | |
} | |
mouseDown(): void { | |
act(() => { | |
fireEvent.mouseDown(this.element()); | |
}); | |
} | |
mouseEnter(): void { | |
act(() => { | |
fireEvent.mouseEnter(this.element()); | |
}); | |
} | |
mouseLeave(): void { | |
act(() => { | |
fireEvent.mouseLeave(this.element()); | |
}); | |
} | |
mouseOver(): void { | |
act(() => { | |
fireEvent.mouseOver(this.element()); | |
}); | |
} | |
mouseUp(): void { | |
act(() => { | |
fireEvent.mouseUp(this.element()); | |
}); | |
} | |
outerHTML(): string { | |
return this.element().outerHTML.trim(); | |
} | |
scrollTo(coordinates: { top?: number; left?: number }): void { | |
const $el = this.element(); | |
if (typeof coordinates.top === "number") { | |
act(() => { | |
$el.scrollTop = coordinates.top; | |
}); | |
} | |
if (typeof coordinates.left === "number") { | |
act(() => { | |
$el.scrollLeft = coordinates.left; | |
}); | |
} | |
act(() => { | |
fireEvent.scroll(this.element()); | |
}); | |
} | |
selector(selector?: string, trim?: boolean): string { | |
return `${this._rootSelector}${this._selector}${ | |
selector ? `${trim || !this._selector ? "" : " "}${selector}` : "" | |
}`; | |
} | |
setValue(value: string): void { | |
if (isEqual(value, this.getValue())) { | |
return; | |
} | |
act(() => { | |
fireEvent.change(this.element(), { | |
bubbles: true, | |
currentTarget: { | |
value: value, | |
rawValue: value, | |
}, | |
target: { | |
value: value, | |
rawValue: value, | |
}, | |
}); | |
}); | |
} | |
waitForAttribute(attr: string, value: any, timeout?: number): Promise<void> { | |
return waitForCondition(() => this.getAttribute(attr) === value, { | |
timeout, | |
msg: outdent` | |
waited for attribute ${attr} to have value ${value} | |
selector: ${this.selector()} | |
`, | |
}); | |
} | |
waitForDataAttr(attr: string, value: any, timeout?: number): Promise<void> { | |
return waitForCondition(() => isEqual(this.getDataAttr(attr), value), { | |
timeout, | |
msg: outdent` | |
waited for data attribute ${attr} to have value ${value} | |
latest value for data attribute is ${this.getDataAttr(attr)} | |
selector: ${this.selector()} | |
`, | |
}); | |
} | |
waitForClass(className: string, timeout?: number): Promise<void> { | |
return waitForCondition( | |
() => this.element().classList.contains(className), | |
{ | |
timeout, | |
msg: outdent` | |
waited for the element to have the "${className}" | |
selector: ${this.selector()} | |
`, | |
} | |
); | |
} | |
waitForNoClass(className: string, timeout?: number): Promise<void> { | |
return waitForCondition( | |
() => !this.element().classList.contains(className), | |
{ | |
timeout, | |
msg: outdent` | |
waited for the element to not have the "${className}" | |
selector: ${this.selector()} | |
`, | |
} | |
); | |
} | |
async waitForDataAttrChange(attr: string, startValue: any, timeout?: number): Promise<void> { | |
const original = | |
startValue === undefined ? this.getDataAttr(attr) : startValue; | |
await waitForCondition(() => !isEqual(this.getDataAttr(attr), original), { | |
timeout, | |
msg: outdent` | |
waited for data attribute ${attr} value to change from ${original} | |
selector: ${this.selector()} | |
`, | |
}); | |
} | |
waitForDisabled(): Promise<void> { | |
return this.waitForDataAttr("disabled", true); | |
} | |
waitForEnabled(timeout?: number): Promise<void> { | |
return this.waitForDataAttr("disabled", false, timeout); | |
} | |
async waitForInDom(timeout?: number): Promise<void> { | |
return waitForCondition(() => !!this.element(), { | |
timeout, | |
msg: outdent` | |
waited for element to appear in dom | |
selector: ${this.selector()} | |
`, | |
}); | |
} | |
async waitForInnerText(timeout?: number): Promise<void> { | |
await waitForCondition(() => !!this.innerText(), { | |
timeout, | |
msg: outdent` | |
waited for element's inner text to be rendered | |
selector: ${this.selector()} | |
`, | |
}); | |
} | |
async waitForInnerTextChange(timeout?: number): Promise<void> { | |
const original = this.innerText(); | |
await waitForCondition(() => this.innerText() !== original, { | |
timeout, | |
msg: outdent` | |
waited for element's inner text to change | |
original text: ${original} | |
selector: ${this.selector()} | |
`, | |
}); | |
} | |
async waitForInnerTextChangeFrom(origText, timeout?: number): Promise<void> { | |
await waitForCondition(() => this.innerText() !== origText, { | |
timeout, | |
msg: outdent` | |
waited for element to appear inner text to change | |
original text: ${origText} | |
selector: ${this.selector()} | |
`, | |
}); | |
} | |
async waitForInvalid(): Promise<void> { | |
await this.waitForDataAttr("valid", false); | |
} | |
async waitForNotInDom(timeout?: number): Promise<void> { | |
await act(async () => { | |
await waitForCondition( | |
() => document.querySelector(this.selector()) === null, | |
{ | |
timeout, | |
msg: outdent` | |
waited for element to not be in the dom | |
selector: ${this.selector()} | |
`, | |
} | |
); | |
}); | |
} | |
async waitForOuterHTMLChange(action: () => void, timeout?: number): Promise<void> { | |
const original = this.outerHTML(); | |
action(); | |
await waitForCondition(() => this.outerHTML() !== original, { | |
timeout, | |
msg: outdent` | |
waited for element's outerHTML to change | |
original text: ${original} | |
selector: ${this.selector()} | |
`, | |
}); | |
} | |
async waitForStyle(style: string, value: any, timeout?: number): Promise<void> { | |
await waitForCondition(() => this.getStyle(style) === value, { | |
timeout, | |
msg: outdent` | |
waited for element style | |
selector: ${this.selector()} | |
property: ${style} | |
value: ${value} | |
`, | |
}); | |
} | |
waitForValid(): Promise<void> { | |
return this.waitForDataAttr("valid", true); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment