UNPKG

@serenity-js/playwright

Version:

Adapter that integrates @serenity-js/web with Playwright, enabling Serenity/JS reporting and using the Screenplay Pattern to write component and end-to-end test scenarios

164 lines (130 loc) 5.07 kB
import { f, LogicError } from '@serenity-js/core'; import type { PageElement, RootLocator, Selector } from '@serenity-js/web'; import { ByCss, ByCssContainingText, ByDeepCss, ById, ByTagName, ByXPath, Locator } from '@serenity-js/web'; import type * as playwright from 'playwright-core'; import { SerenitySelectorEngines } from '../../../selector-engines'; import { promised } from '../../promised'; import { PlaywrightPageElement } from '../PlaywrightPageElement'; import type { PlaywrightRootLocator } from './PlaywrightRootLocator'; /** * Playwright-specific implementation of [`Locator`](https://serenity-js.org/api/web/class/Locator/). * * @group Models */ export class PlaywrightLocator extends Locator<playwright.Locator, string> { constructor( parent: RootLocator<playwright.Locator>, selector: Selector, ) { super(parent, selector); } // todo: refactor; replace with a map and some more generic lookup mechanism protected nativeSelector(): string { if (this.selector instanceof ByCss) { return `:light(${ this.selector.value })`; } if (this.selector instanceof ByDeepCss) { return this.selector.value; } if (this.selector instanceof ByCssContainingText) { return `:light(${ this.selector.value }):has-text("${ this.selector.text }")`; } if (this.selector instanceof ById) { return `id=${ this.selector.value }`; } if (this.selector instanceof ByTagName) { return `:light(${ this.selector.value })`; } if (this.selector instanceof ByXPath) { return `xpath=${ this.selector.value }`; } throw new LogicError(f`${ this.selector } is not supported by ${ this.constructor.name }`); } async isPresent(): Promise<boolean> { try { const parentPresent = await this.parent.isPresent(); if (! parentPresent) { return false; } const parent = await this.parent.nativeElement(); await parent.locator(this.nativeSelector()).first().waitFor({ state: 'attached', timeout: 250 }); return true; } catch (error) { if (error.name === 'TimeoutError') { return false; } throw error; } } async nativeElement(): Promise<playwright.Locator> { const parent = await this.parent.nativeElement(); return promised(parent.locator(this.nativeSelector())); } async allNativeElements(): Promise<Array<playwright.Locator>> { const parent = await this.parent.nativeElement(); if (! parent) { return []; } return promised(parent.locator(this.nativeSelector()).all()); } of(parent: PlaywrightRootLocator): Locator<playwright.Locator, string> { return new PlaywrightLocator(parent, this.selector); } closestTo(child: PlaywrightLocator): Locator<playwright.Locator, string> { return new PlaywrightParentElementLocator(this.parent, this.selector, child); } locate(child: PlaywrightLocator): Locator<playwright.Locator, string> { return new PlaywrightLocator(this, child.selector); } element(): PageElement<playwright.Locator> { return new PlaywrightPageElement(this); } async allElements(): Promise<Array<PageElement<playwright.Locator>>> { const elements = await this.allNativeElements(); return elements.map(childElement => new PlaywrightPageElement( new PlaywrightExistingElementLocator( this.parent as PlaywrightRootLocator, this.selector, childElement, ) ) ); } } /** * @internal */ export class PlaywrightExistingElementLocator extends PlaywrightLocator { constructor( parent: RootLocator<playwright.Locator>, selector: Selector, private readonly existingNativeElement: playwright.Locator, ) { super(parent, selector); } async nativeElement(): Promise<playwright.Locator> { return this.existingNativeElement; } async allNativeElements(): Promise<Array<playwright.Locator>> { return [ this.existingNativeElement ]; } } class PlaywrightParentElementLocator extends PlaywrightLocator { constructor( parent: RootLocator<playwright.Locator>, selector: Selector, private readonly child: PlaywrightLocator ) { super(parent, selector); } override async nativeElement(): Promise<playwright.Locator> { const cssSelector = this.asCssSelector(this.selector); const child = await this.child.nativeElement(); return child.locator(`${ SerenitySelectorEngines.engineIdOf('closest') }=${ cssSelector.value }`) } async allNativeElements(): Promise<Array<playwright.Locator>> { return [ await this.nativeElement() ]; } }