Pomeranian

Гибкий и эффективный подход к паттерну POM
(Page Object Model)

Андрей Михайлов
команда Monium UI
Яндекс
Цели Pomeranian
- максимально удобное написание тестов;
- позволяет сосредоточиться на бизнес-логике;
- минимум boilerplate;
- нет повторяющегося кода;
- сохраняется возможность денормализации;
- хорошая читаемость тестов;
- хорошая читаемость отчетов Playwright;
- готовность к релизу в open source:
- максимальная гибкость;
- готовность принять коммьюнити;
- грамотная структура;
- тесты;
- документация (WIP).
Десять ноу-хау Pomeranian
- Page object'ы для базовых HTML-элементов
1. Page object'ы для базовых HTML-элементов
export class ElementPO extends PageObject {
/*--- Actions ---*/
click(options?: ClickOptions): Promise<void> {
return this.element.click(options);
}
hover(options?: HoverOptions): Promise<void> {
return this.element.hover(options);
}
waitFor(options?: WaitForOptions): Promise<void> {
return this.element.waitFor(options);
}
// ....
/*--- Filters ---*/
filterByText(hasText: string): this {
return this.derive((locator) => locator.filter({hasText}));
}
// ...
/*--- Assertions ---*/
shouldHaveText(text: string, options?: ToHaveTextOptions): Promise<void> {
return expect(this.element).toHaveText(text, options);
}
// ...
}
class MyPage extends BasePage {
Title = new ElementPO(this, '.Title');
Field = new InputPO(this, '.Field');
SubmitButton = new ElementPO(this, '.Button');
visit () {
// Base URL is baked in
return super.visit('/my-page');
}
}
Базовый класс
Ваш page object
await Page.Title.click();
await Page.Bar.click({timeout: 300);
await Page.Baz.click({force: true);
await Page.Foo.waitFor();
await Page.Bar.waitFor('attached');
await Page.Baz.waitFor('hidden');
await Page.Foo.screenshot('screenshot-name-1');
await Page.Bar.screenshot('screenshot-name-2', {mask: ['#page-footer-wrapper']});
await Page.Baz.screenshot('screenshot-name-3');
Тест
1. Page object'ы для базовых HTML-элементов
import {ElementPO as ElementPOBase} from '@pomeranian/lib-standard';
export class ElementPO extends ElementPOBase {
/*--- Actions ---*/
async screenshot(screenshotName: string, options: LocatorScreenshotOptions = {}) {
const optionsMerged: LocatorScreenshotOptions = {
animations: 'disabled',
...options,
};
const testInfo = this.testInfo;
const screenshot = await this.element.screenshot(optionsMerged);
const testName = getTestName(testInfo);
const projectName = testInfo.project.name;
const sanitizedName = sanitize(screenshotName);
const screenshotPath = `${projectName}-${testName}/${sanitizedName}.png`;
await expect(screenshot).toMatchSnapshot(screenshotPath);
}
}
```
- Обеспечивает конвенцию;
- предотвращает скатывание на процедурный стиль тестах;
- все нужные методы под рукой;
- ненужных методов нет;
- все методы стандартны, команда быстро привыкает к названиям;
- можно добавлять методы, которых нет в Playwright;
- можно добавлять методы, специфичные для вашего приложения.
ElementPO:
* `isInViewport()`
* `hasClass(name: string)`
* `hasStyle(name: string, value?: string)`
InputPO, TextareaPO:
* `setCaretToPosition(pos: number)`
* `selectText(from: number, to: number)`
* `copySelectedTextToClipboard()`
* `append(text: string)`
ImagePO:
* `getAlt()`
* `getSrc()`
* `waitForLoad()`
Десять ноу-хау Pomeranian
- Page object'ы для базовых HTML-элементов
- Централизованная иерархия page object'ов
2. Централизованная иерархия page object'ов
- PO
- Page
- Page
- Page
- Component
- Component
- Element
- Component
- Element
- Element
- Component
const cell =
"[ConnectFailed] Failed to connect to 2a02:6b8:c12:3611:0:4a73:ba36:0 port 3443 after 8 ms: Couldn't connect to server";
await PO.ShardTargetsPage.visit({shard: 'dataui_test_kloof-core_dataui_test_push'});
await PO.ShardTargetsPage.revealTooltipOnCellWithText(cell);
await PO.ShardTargetsPage.screenshot('target-error-hover.png');
export class ShardTargetsPagePO extends EntitesListPO {
target = '[data-qa="shard-targets"]';
/*--- Elements --- */
TooltipTextWithCopy = new ElementPO(this, this.page.locator('.expandable-hover-text__tooltip-container'));
/*--- Actions ---*/
async visit({shard}: {shard: string}): Promise<void> {
const {scopeId} = this.fixtures;
const url = `/projects/${scopeId}/shards/${shard}/view/targets`;
await super.visit(url);
await this.ContentWrapper.waitFor();
}
async revealTooltipOnCellWithText(text: string): Promise<void> {
await this.Cell.filterByText(text).hover();
await this.TooltipTextWithCopy.waitFor();
}
}
export class PO extends PageObject {
/*--- Pages ---*/
ShardTargetsPage = new ShardTargetsPagePO(this);
}
Корневой page object
Page object страницы
Тест
export class EntitiesListPO extendsElementPO {
target = '.ycm-entities-list"';
/*--- Elements --- */
ContentWrapper = new ElementPO(this, '.ycm-entities-list__content-wrapper');
Cell = new ElementPO(this, (parent) => parent.getByRole('cell'));
}
Page object компонента
Десять ноу-хау Pomeranian
- Page object'ы для базовых HTML-элементов
- Централизованная иерархия page object'ов
- Page object'ы знают своего родителя
и корневой PO
3. Page object'ы знают своего родителя и корневой PO
export class PO extends PageObject {
/*--- Pages ---*/
MyPage = new MyPagePO(this);
/*--- Modals ---*/
ConfirmationDialog = new ConfirmationDialogPO(this);
}
Корневой page object
export class MyPagePO extends ElementPO {
/* --- Actions --- */
async restart() {
await this.RestartButton.click();
await this.root.ConfirmationModal.OkButton.click();
}
}
Page object страницы
await PO.MyPage.restart();
Тест
Десять ноу-хау Pomeranian
- Page object'ы для базовых HTML-элементов
- Централизованная иерархия page object'ов
- Page object'ы знают своего родителя и корневой PO
- Page object'ы инициализируются автоматически
4. Page object'ы инициализируются автоматически
import test from '@/tests';
test('Should restart', ({PO}) => {
await PO.MyPage.visit()
await PO.MyPage.restart();
await PO.Toast.shouldHaveText('OK');
});
Ноу-хау от Ильи Исупова (@matumbaman, @SwinX)
4. Page object'ы инициализируются автоматически
import test from '@/tests';
import AvatarPO from "@pomeranian/gravity-ui-kit";
test('Should see avatar', async ({page, BASE_URL}) => {
const avatar = new AvatarPO({page}); // 👈
await page.goto(`${BASE_URL}/service-providers/idm'`);
await avatar.shouldBeVisible();
});
Но можно и вручную:
Десять ноу-хау Pomeranian
- Page object'ы для базовых HTML-элементов
- Централизованная иерархия page object'ов
- Page object'ы знают своего родителя и корневой PO
- Page object'ы инициализируются автоматически
- Assertions внутри page object'ов
5. Assertions внутри page object'ов
import test from '@/tests';
test('Should restart', ({PO}) => {
await PO.MyPage.visit()
await PO.MyPage.restart();
await PO.Toast.shouldHaveText('OK');
});
import {expect} from 'playwright/test';
import {PageObject, type ToHaveTextOptions} from '@pomeranian/core';
export class ElementPO extends PageObject {
/*--- Assertions ---*/
shouldHaveText(text: string, options?: ToHaveTextOptions): Promise<void> {
return expect(this.element).toHaveText(text, options);
}
}
5. Assertions внутри page object'ов
- Лучше читаемость;
- всё под рукой;
- инкапсуляция;
- стандартизация методов;
- больше удобных методов;
- лучше читаемость отчетов
(об этом далее).
Element.shouldBeFocused()
Element.shouldBeOnlyChild()
Element.shouldBeNthChild(2)
Input.shouldHaveSelectedText('foo')
Input.shouldHaveSelectedRange(2, 7)
Input.shouldHaveValidationMessage('Too short')
Link.shouldHaveAnchorTo(Header)
CustomTable
.shouldBeSortedAlphabeticallyByColumn('id')
Недостаток: отсутствие инвертирования условия .not()
Десять ноу-хау Pomeranian
- Page object'ы для базовых HTML-элементов
- Централизованная иерархия page object'ов
- Page object'ы знают своего родителя и корневой PO
- Page object'ы инициализируются автоматически
- Assertions внутри page object'ов
- Работа с коллекциями, выбор элемента из нескольких
6. Работа с коллекциями, выбор элемента из нескольких
DashboardItem = new DasbhoardItemPO(this);
await PO.MyPage.DashboardItem.Title.click() // ❌ error
await PO.MyPage.DashboardItem.first().Title.click();
await PO.MyPage.DashboardItem.nth(2).Title.click();
await PO.MyPage.DashboardItem.withText('Foo').Title.click();
await PO.MyPage.DashboardItem.withAttr('title', 'Foo').Title.click();
MyPagePO:
Тест
Ноу-хау от Макария Шаоряна
(@makar-s)
Десять ноу-хау Pomeranian
- Page object'ы для базовых HTML-элементов
- Централизованная иерархия page object'ов
- Page object'ы знают своего родителя и корневой PO
- Page object'ы инициализируются автоматически
- Assertions внутри page object'ов
- Работа с коллекциями, выбор элемента из нескольких
- Встроенные и переопределяемые селекторы
7. Встроенные и переопределяемые селекторы
import {AvatarPO} from '@pomeranian/gravity-ui-kit';
Ноу-хау от Макария Шаоряна
(@makar-s)
AnyAvatar = new AvatarPO(this);
Будет использовать встроенный селектор `.g-avatar`:
CreatorAvatar = new AvatarPO(this, '[data-qa="creator"]');
ModifierAvatar = new AvatarPO(this, '[data-qa="modifier"]'));
Будут использовать кастомные селекторы:
7. Встроенные и переопределяемые селекторы
Ноу-хау от Макария Шаоряна
(@makar-s)
export class AvatarPO extends PageObject {
target = '.g-avatar';
}
Встроенный селектор объявляется так:
Десять ноу-хау Pomeranian
- Page object'ы для базовых HTML-элементов
- Централизованная иерархия page object'ов
- Page object'ы знают своего родителя и корневой PO
- Page object'ы инициализируются автоматически
- Assertions внутри page object'ов
- Работа с коллекциями, выбор элемента из нескольких
- Встроенные и переопределяемые селекторы
- Автоматический скоупинг селекторов (опциональный)
8. Три способа нацелиться
на элемент
export class MyPagePO extends ElementPO {
target = '.my-page';
Foo = new ElementPO(this, '.foo');
} // ☝️
PO.MyPage.Foo.locator.toString()
export class MyPagePO extends ElementPO {
target = '.my-page';
Bar = new ElementPO(this, (parentLoc) => parentLoc.getByRole('button'));
} // ☝️
PO.MyPage.Baz.locator.toString()
export class MyPagePO extends ElementPO {
target = '.my-page';
Bar = new ElementPO(this, this.page.getByRole('button'));
} // ☝️
PO.MyPage.Bar.locator.toString()
↳ locator('.my-page').locator('.foo')
↳ getByRole('button')
↳ locator('.my-page').getByRole('button')
Строка:
скоупится
Локатор:
не скоупится
Колбэк:
вручную
Десять ноу-хау Pomeranian
- Page object'ы для базовых HTML-элементов
- Централизованная иерархия page object'ов
- Page object'ы знают своего родителя и корневой PO
- Page object'ы инициализируются автоматически
- Assertions внутри page object'ов
- Работа с коллекциями, выбор элемента из нескольких
- Встроенные и переопределяемые селекторы
- Автоматический скоупинг селекторов (опциональный)
- Читаемость тестов и отчетов без `test.step()`
9. Читаемость тестов и отчетов без `test.step()`


Ноу-хау Андрея Волынкина
(@avol-v)
Десять ноу-хау Pomeranian
- Page object'ы для базовых HTML-элементов
- Централизованная иерархия page object'ов
- Page object'ы знают своего родителя и корневой PO
- Page object'ы инициализируются автоматически
- Assertions внутри page object'ов
- Работа с коллекциями, выбор элемента из нескольких
- Встроенные и переопределяемые селекторы
- Автоматический скоупинг селекторов (опциональный)
- Читаемость тестов и отчетов без `test.step()`
- Конвенция именования:
page-object'ы с большой буквы,
методы с маленькой
Pomeranian — гибкий и эффективный подход к паттерну Page Object
By Andrey Mikhaylov (lolmaus)
Pomeranian — гибкий и эффективный подход к паттерну Page Object
- 76