Created
May 5, 2024 02:12
-
-
Save antischematic/62a678ba1e3a810ccc20668d11f8117d to your computer and use it in GitHub Desktop.
This file contains 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 { 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