Playwright Test interoperability: Reusing Playwright native locators within SerenityJS? #2155
-
Hey @jan-molak , Is it possible to reuse playwrights "native" locators (from BackgroundWe have two teams in one project: developers and testers. Developers are already using Playwright and testers are thinking about switching to SerenityJS. There are several Playwright E2E tests in the project that were written by devs. Page object models were used in these tests. It would be great if we could continue to use the existing page object models with Playwright syntax within SerenityJS. This would allow both sides to share code and use their preferred testing tools at the same time. ´ |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
Hey, @jase88! Sure! Serenity/JS is designed to make it easy for you to enhance existing test suites to improve reporting, introduce the actor model and follow the Screenplay Pattern. I've written a detailed guide on this topic: "Extending Playwright Test with Serenity/JS". In short, to introduce Serenity/JS to an existing Playwright Test suite, you need to:
- import { describe, it, test } from '@playwright/test'
+ import { describe, it, test } from '@serenity-js/playwright-test' Now, to answer your original question:
One of the great things about the Serenity/JS Playwright Test fixtures is that the default This means that you can start introducing actors even to an existing Playwright Test scenario. Using native Playwright APIsConsider the following example, written using native Playwright Test APIs: import { describe, it, expect } from '@serenity-js/playwright-test' // Note that I use Serenity/JS APIs here
describe('Todo App', () => {
it('should allow me to add todo items using plain Playwright Test APIs', async ({ page }) => {
await page.goto('https://todo-app.serenity-js.org/#/');
await page.locator('.new-todo').fill('Walk the dog');
await page.locator('.new-todo').press('Enter');
await expect(page.locator('.view label')).toHaveText([
'Walk the dog'
])
})
}) Using Page Object pattern with native Playwright APIsLet's now make it more relevant by introducing a Page Object, as described in the Playwring documentation. A scenario using a import { describe, it, expect } from '@serenity-js/playwright-test'
import { Page, Locator } from '@playwright/test';
export class TodoApp {
private readonly page: Page
private readonly newTodoInputBox: Locator
public readonly recordedItems: Locator
constructor(page: Page) {
this.page = page
this.newTodoInputBox = page.locator('.new-todo')
this.recordedItems = page.locator('.view label')
}
async goto() {
await this.page.goto('https://todo-app.serenity-js.org/#/');
}
async recordItem(name: string) {
await this.newTodoInputBox.fill(name);
await this.newTodoInputBox.press('Enter');
}
}
describe('Todo App', () => {
it('should allow me to add todo items using the Page Object pattern', async ({ page }) => {
const app = new TodoApp(page);
await app.goto();
await app.recordItem('Walk the dog')
await expect(app.recordedItems).toHaveText([
'Walk the dog'
])
})
}) Tip Note that Playwright's description of Page Objects, where each page object represents a whole web page, is based on the original design proposed by the Selenium team back in 2009 (see Page Object Model). If you want to use page objects, I'd suggest checking Martin Fowler's alternative, or my alternative of Lean Page Objects, also described in detail in "BDD in Action, Second Edition". Introducing actorsApart from import { describe, it, expect } from '@serenity-js/playwright-test'
import { Page, Locator } from '@playwright/test';
import { Navigate } from '@serenity-js/web';
describe('Todo App', () => {
it('should allow me to add todo items using Playwright Test and Serenity/JS APIs', async ({ page, actor }) => {
await actor.attemptsTo(
Navigate.to('https://todo-app.serenity-js.org/#/'), // await page.goto('https://todo-app.serenity-js.org/#/');
)
await page.locator('.new-todo').fill('Walk the dog');
await page.locator('.new-todo').press('Enter');
await expect(page.locator('.view label')).toHaveText([
'Walk the dog'
])
})
}) As I mentioned earlier, you can use const newTodoInputBox = PageElement.from(page.locator('.new-todo')) So if we carried on with refactoring the original Playwright Test scenario, we might arrive at something similar to the below example: import { describe, expect, it } from '@serenity-js/playwright-test';
import { Enter, Key, Navigate, PageElement, Press } from '@serenity-js/web';
describe('Todo App', () => {
it('should allow me to add todo items using Playwright Test and Serenity/JS APIs', async ({ page, actor }) => {
const newTodoInputBox = () => PageElement.from(page.locator('.new-todo'))
.describedAs('new todo input box')
await actor.attemptsTo(
Navigate.to('https://todo-app.serenity-js.org/#/'), // await page.goto('https://todo-app.serenity-js.org/#/');
Enter.theValue('Walk the dog').into(newTodoInputBox()), // await page.locator('.new-todo').fill('Walk the dog');
Press.the(Key.Enter).in(newTodoInputBox()), // await page.locator('.new-todo').press('Enter');
)
await expect(page.locator('.view label')).toHaveText([
'Walk the dog'
])
})
}) While you could use const recordedItems = async () => PageElements.of(await page.locator('.view label').all()) as unknown as PageElements<Array<Locator>> Instead, for those occasions where you need to reference a collection of elements, I'd suggest using const recordedItems = () => PageElements.located(By.css('.view label'))
.describedAs('recorded items') Tip If you feel that Serenity/JS should have a mechanism like With this, the finished scenario could look like this: import { describe, expect, it } from '@serenity-js/playwright-test';
import { contain, Ensure } from '@serenity-js/assertions';
import { By, Enter, Key, Navigate, PageElement, PageElements, Press, Text } from '@serenity-js/web';
describe('Todo App', () => {
it('should allow me to add todo items using Playwright Test and Serenity/JS APIs', async ({ page, actor }) => {
const newTodoInputBox = () => PageElement.from(page.locator('.new-todo'))
.describedAs('new todo input box')
const recordedItems = () => PageElements.located(By.css('.view label'))
.describedAs('recorded items')
await actor.attemptsTo(
Navigate.to('https://todo-app.serenity-js.org/#/'),
Enter.theValue('Walk the dog').into(newTodoInputBox()),
Press.the(Key.Enter).in(newTodoInputBox()),
Ensure.that(Text.ofAll(recordedItems()), contain('Walk the dog')),
)
})
}) Page Objects with Serenity/JSLet's now go back to your original question of how to introduce Serenity/JS APIs to existing page objects import { describe, it, expect } from '@serenity-js/playwright-test'
import { Page, Locator } from '@playwright/test';
export class TodoApp {
private readonly page: Page
private readonly newTodoInputBox: Locator
public readonly recordedItems: Locator
constructor(page: Page) {
this.page = page
this.newTodoInputBox = page.locator('.new-todo')
this.recordedItems = page.locator('.view label')
}
async goto() {
await this.page.goto('https://todo-app.serenity-js.org/#/');
}
async recordItem(name: string) {
await this.newTodoInputBox.fill(name);
await this.newTodoInputBox.press('Enter');
}
} We could:
With this approach, we'd arrive at a page object implementation like this: class TodoApp {
private static readonly newTodoInputBox = () =>
PageElement.located(By.css('.new-todo'))
.describedAs('new todo input box')
private static readonly recordedItems = () =>
PageElements.located(By.css('.view label'))
.describedAs('recorded items')
static goto = () =>
Task.where(`#actor opens the Todo App`,
Navigate.to('https://todo-app.serenity-js.org/#/'),
)
static recordItem = (name: string) =>
Task.where(`#actor records a todo item called "${ name }"`,
Enter.theValue(name).into(this.newTodoInputBox()),
Press.the(Key.Enter).in(this.newTodoInputBox()),
)
static recordedItemNames = () =>
Text.ofAll(this.recordedItems())
} In this version, note that:
With our new page object, the implementation becomes simply: import { describe, expect, it } from '@serenity-js/playwright-test';
import { contain, Ensure } from '@serenity-js/assertions';
import { By, Enter, Key, Navigate, PageElement, PageElements, Press, Text } from '@serenity-js/web';
import { Task } from '@serenity-js/core';
class TodoApp {
private static readonly newTodoInputBox = () =>
PageElement.located(By.css('.new-todo'))
.describedAs('new todo input box')
private static readonly recordedItems = () =>
PageElements.located(By.css('.view label'))
.describedAs('recorded items')
static goto = () =>
Task.where(`#actor opens the Todo App`,
Navigate.to('https://todo-app.serenity-js.org/#/'),
)
static recordItem = (name: string) =>
Task.where(`#actor records a todo item called "${ name }"`,
Enter.theValue(name).into(this.newTodoInputBox()),
Press.the(Key.Enter).in(this.newTodoInputBox()),
)
static recordedItemNames = () =>
Text.ofAll(this.recordedItems())
}
describe('Todo App', () => {
it('should allow me to add todo items using the Page Object pattern using Serenity/JS', async ({ actor }) => {
await actor.attemptsTo(
TodoApp.goto(),
TodoApp.recordItem('Walk the dog'),
Ensure.that(TodoApp.recordedItemNames(), contain('Walk the dog')),
)
})
}) If you'd like to explore this some more, have a look at the examples in the Serenity/JS Playwright Test project template Let me know if the above explanation helps your team! |
Beta Was this translation helpful? Give feedback.
Hey, @jase88!
Sure! Serenity/JS is designed to make it easy for you to enhance existing test suites to improve reporting, introduce the actor model and follow the Screenplay Pattern. I've written a detailed guide on this topic: "Extending Playwright Test with Serenity/JS".
In short, to introduce Serenity/JS to an existing Playwright Test suite, you need to:
playwright.config.ts
andpackage.json
(see this project template for reference)