javascript - Utökar locator playwright med hjälpfunktioner
Jag vill skapa en klass som omsluter Playwrights Locator för att lägga till några hjälpfunktioner.
Beteendet hos locatorn bör vara exakt detsamma som det som tillhandahålls av testningsramverket, förutom de tillagda hjälpfunktionerna.
Men om jag implementerar gränssnittet måste jag implementera alla funktioner manuellt, vilket är något jag inte vill eftersom beteendet inte ändras. Vad är det rätta sättet att göra det på?
Här är lite pseudokod för vad jag vill göra:
// I know I can't extend locator since its an interface but I'm using
// extends to better deliver the concept of what I want to do
export class LocatorWrapper extends Locator {
constructor (...args: any[]) {
super(args);
}
public hoverAndDoubleClick = async () => {
// This function is just a dummy
await this.hover();
await this.click();
await this.click();
}
}
Svar #1
Vad sägs om att titta på möjligheten att använda PageObject-modellen och arv?
Du har en basklass som har en abstrakt egenskap mainSelector som är rot. (Den kan vara string eller Locator enligt ditt val, eller så kan du till och med skapa en konstruktor för den)
export abstract class PageObject {
mainSelector: string;
findLocator(selector: string, options?: Parameters<Page['locator'][1]>) {
return page.locator(selector, options);
}
async shouldBeVisible() {
return this.findLocator(this.mainSelector).waitFor({state: "visible"});
}
async hoverAndDoubleClick() {
// your method
}
// other methods
}
och sedan skulle varje ny PageObject-klass bara utvidga från den.
export class TestComponent extends PageObject {
mainSelector: '.testRoot';
}
så TestComponent kunde använda alla metoder från PageObjects.
Om du har kontexter kan du justera PageObject-klassen för att vara kontextoberoende.
export abstract class PageObject {
constructor(public mainSelector: string, public context: Page | Frame = page) {}
findLocator(selector: string, options?: Parameters<Page['locator']>[1]) {
return this.context.locator(selector, options);
}
}
Detta tillvägagångssätt kan hjälpa till att enkelt omsluta åtgärder från Playwright och manipulera flikskontexter vid behov.
Exempel på detta tillvägagångssätt på Puppeteer
Svar #2
Uppdatera
Efter att ha utforskat dokumentationen insåg jag att det vanligtvis är bättre att skapa en anpassad matchare än att utöka lokaliseringsverktyget.
Idén är att istället för att extrahera information genom att förlänga lokaliseraren, skapa en anpassad funktion att bifoga till playwrights expect som gör samma jobb. Informationen nedan är fortfarande giltig men bör användas i mer specifika användningsfall för att utföra handlingar, snarare än att hämta information för att utvärdera.
Här är dokumentationen
Exempel (verifiera CSS-färg):
❌ Icke-optimal metod
export const extendLocator = (
locator: Locator,
): ExtendedLocator => ({
...locator,
locator: (selector, options) =>
extendLocator(locator.locator(selector, options)),
// Extract CSS property color
getColor: () => locator.evaluate((element) => {
const styles = window.getComputedStyle(element);
return styles.getPropertyValue('color');
});
});
const customLocator = extendLocator(page.getByTestId('my-id'))
expect(await customLocator.getColor()).toBe('red')
✅ Optimal metod
import { expect } from 'playwright'
const extendedExpect = expect.extend({
async toHaveColor(locator, expectedColor) {
return expect(locator).toHaveCSS('color', expectedColor);
},
});
const locator = page.getByTestId('my-id')
await expect(locator).toHaveColor('red');
Gammal version
För att få en utökad version av lokatorn skapade jag ett wrapper-gränssnitt och använde sedan endast lokatorn inuti min Page Object Model.
// Locator extension (for complete extension scroll below)
export interface ExtendedLocator extends Locator {
//...
}
export const extendLocator = (
locator: Locator,
): ExtendedLocator => ({
...locator,
locator: (selector, options) =>
extendLocator(locator.locator(selector, options)),
myUtilityFunction: () => {/* EXEC SOME CODE */}
});
// POM
const baseSelector = 'my-selector'
export class PageObjectModel {
private _locator: ExtendedLocator
public constructor(page: Page){
this._locator = extendLocator(page.locator(baseSelector))
}
public locator = (
...args: Parameters<Locator['locator']>
) => this._locator.locator(...args);
}
// USAGE
test('Should verify a condition', () => {
// get the page from playwright
const page = new PageObjectModel(playwrightPage);
await page.locator('another-selector').myUtilityFunc();
})
För att göra skapandet av POM ännu smidigare kan ett fäste användas. Jag lämnar länken till dokumentationen:
- Armatur
- Sidobjektsmodell
Implementering
Detta är en konfiguration som gör det möjligt att hålla reda på alla funktioner som behöver överskridas för att utöka Locator samt den fullt implementerade funktionen
// This will raise an error if a function that returns a locator
// is not implemented
type ExtendedLocator = {
[key in keyof Locator]: Locator[key] extends (
...args: Parameters<Locator[key]>
) => Locator
? (...args: Parameters<Locator[key]>) => ExtendedLocator
: Locator[key];
} & {
myUtilityFunction: () => void;
};
export const extendLocator = (locator: Locator): ExtendedLocator => {
return {
// Utility functions
myUtilityFunction: () => {}
// Locator plus overridden functions
...locator,
getByTestId: (...args) => extendLocator(locator.getByTestId(...args)),
// Other functions...
};
};
Det är viktigt att notera att detta kan kräva underhåll om nya lokatorer läggs till av API:et. Däremot kommer TypeScript att ge ett fel, och att lägga till dem är enkelt: det räcker med att lägga till newFunction: (...args) => extendLocator(locator.newFunction(...args)).
För att också ge företräde åt den ursprungliga API:ns kod föreslår jag att man placerar hjälpfunktioner före spridningen av lokatorn. På det här sättet kommer konflikter som matchar den ursprungliga API:ns nya funktion att åsidosättas.
