Skip to content

Instantly share code, notes, and snippets.

@antischematic
Created May 5, 2024 02:12
Show Gist options
  • Save antischematic/62a678ba1e3a810ccc20668d11f8117d to your computer and use it in GitHub Desktop.
Save antischematic/62a678ba1e3a810ccc20668d11f8117d to your computer and use it in GitHub Desktop.
import { ComponentHarness, HarnessEnvironment, TestElement, ComponentHarnessConstructor, ElementDimensions, TestKey, ModifierKeys} from "@angular/cdk/testing"
import { Locator, Page, test, expect as baseExpect } from "@playwright/test"
/**
* An Angular framework stabilizer function that takes a callback and calls it when the application
* is stable, passing a boolean indicating if any work was done.
*/
declare interface FrameworkStabilizer {
(callback: (didWork: boolean) => void): void;
}
declare global {
interface Window {
/**
* These hooks are exposed by Angular to register a callback for when the application is stable
* (no more pending tasks).
*
* For the implementation, see: https://github.com/
* angular/angular/blob/main/packages/platform-browser/src/browser/testability.ts#L30-L49
*/
frameworkStabilizers: FrameworkStabilizer[];
}
}
async function whenStable() {
await Promise.all(window.frameworkStabilizers.map(stabilizer => new Promise(stabilizer)))
}
class PlaywrightTestElement implements TestElement {
async blur(): Promise<void> {
await this.locator.blur()
await this.forceStabilize()
}
async focus(): Promise<void> {
await this.locator.focus()
await this.forceStabilize()
}
async clear(): Promise<void> {
await this.locator.clear()
await this.forceStabilize()
}
async click(): Promise<void> {
await this.locator.click()
await this.forceStabilize()
}
async mouseAway(): Promise<void> {
await this.locator.page().mouse.move(-Number.MAX_SAFE_INTEGER, -Number.MAX_SAFE_INTEGER)
await this.forceStabilize()
}
async dispatchEvent(name: string, event: any): Promise<void> {
await this.locator.dispatchEvent(name, event)
await this.forceStabilize()
}
async getAttribute(qualifiedName: string): Promise<string | null> {
return this.locator.getAttribute(qualifiedName)
}
async getCssValue(key: string): Promise<string> {
return this.locator.evaluate((element) => window.getComputedStyle(element).getPropertyValue(key))
}
async getDimensions(): Promise<ElementDimensions> {
return this.locator.evaluate((element) => element.getBoundingClientRect())
}
async getProperty(name: string): Promise<any> {
const handle = await this.locator.elementHandle()
return handle?.getProperty(name) ?? null
}
async hasClass(name: string): Promise<boolean> {
return this.locator.evaluate(element => element.classList.contains(name))
}
async hover(): Promise<void> {
await this.locator.hover()
await this.forceStabilize()
}
async isFocused(): Promise<boolean> {
return this.locator.evaluate(node => document.activeElement === node)
}
async matchesSelector(selector: string): Promise<boolean> {
return this.locator.evaluate(element => element.matches(selector))
}
async rightClick(): Promise<void> {
await this.locator.click({ button: "right" })
await this.forceStabilize()
}
async selectOptions(...optionIndices: number[]) {
await this.locator.selectOption(optionIndices.map(index => ({ index })))
await this.forceStabilize()
}
async sendKeys(...keys: (string | TestKey)[]): Promise<void>
async sendKeys(modifiers: ModifierKeys, ...keys: (string | TestKey)[]): Promise<void>
async sendKeys(modifiers: ModifierKeys | string | TestKey, ...keys: (string | TestKey)[]): Promise<void> {
await this.locator.pressSequentially(keys.join(''))
await this.forceStabilize()
}
async setContenteditableValue(value: any): Promise<void> {
await this.locator.fill(value)
await this.forceStabilize()
}
async setInputValue(value: any): Promise<void> {
await this.locator.fill(value)
await this.forceStabilize()
}
async text(): Promise<string> {
return this.locator.innerText()
}
constructor(public locator: Locator, private forceStabilize: () => Promise<void>) {}
}
class PlaywrightHarnessEnvironment extends HarnessEnvironment<Locator> {
static getHarnessForPage<T extends ComponentHarness>(page: Page, harness: ComponentHarnessConstructor<T>): Promise<T> {
return new this(page.locator('css=body')).getHarness(harness)
}
protected constructor(private locator: Locator) {
super(locator)
}
async forceStabilize() {
await this.locator.evaluate(whenStable)
}
async waitForTasksOutsideAngular() {
await Promise.resolve()
}
getDocumentRoot() {
return this.locator.page().locator('css=body')
}
createTestElement(locator: Locator): TestElement {
return new PlaywrightTestElement(locator, () => this.forceStabilize())
}
createEnvironment(locator: Locator) {
return new PlaywrightHarnessEnvironment(locator)
}
async getAllRawElements(selector: string & { target: string }): Promise<Locator[]> {
let result: Locator
switch (selector.target) {
case "byRole":
const { role, options } = JSON.parse(selector)
result = this.locator.getByRole(role, options)
if (options.selector) {
result = result.and(this.locator.locator(`css=${options.selector}`))
}
break
default:
result = this.locator.locator(`css=${selector}`)
break
}
const count = await result.count()
const locators = [] as Locator[]
for (let i = 0; i < count; i++) {
locators.push(result.nth(i))
}
return locators
}
}
function byRole(role: string, options: {} = {}) {
const payload = "The byRole selector is not supported by this environment"
Object.defineProperty(payload, "target", { value: "byRole", enumerable: true })
Object.defineProperty(payload, "data", { value: JSON.stringify({ role, options }) })
return payload
}
function getLocator(element: TestElement) {
if (element instanceof PlaywrightTestElement) {
return element.locator
}
throw new Error
}
class ChildHarness extends ComponentHarness {
static hostSelector = "child"
}
class TestHarness extends ComponentHarness {
static hostSelector = "test"
getChild = this.locatorFor(byRole("button"))
getChildHarness = this.locatorFor(ChildHarness)
}
const expect = baseExpect.extend({
toBeInViewport: async (element: Promise<TestElement> | TestElement) => {
await baseExpect(getLocator(await element)).toBeInViewport()
return {
message: () => "",
pass: true
}
}
})
test('hello', async ({ page }) => {
const harness = await PlaywrightHarnessEnvironment.getHarnessForPage(page, TestHarness)
expect(harness.host()).toBeInViewport()
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment