Pomeranian

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

Андрей Михайлов

команда Monium UI

Яндекс

Цели Pomeranian

  • максимально удобное написание тестов;
  • позволяет сосредоточиться на бизнес-логике;
  • минимум boilerplate;
  • нет повторяющегося кода;
  • сохраняется возможность денормализации;
  • хорошая читаемость тестов;
  • хорошая читаемость отчетов Playwright;
  • готовность к релизу в open source:
  • максимальная гибкость;
  • готовность принять коммьюнити;
  • грамотная структура;
  • тесты;
  • документация (WIP).

Десять ноу-хау Pomeranian

  1. 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

  1. Page object'ы для базовых HTML-элементов
  2. Централизованная иерархия page object'ов

2. Централизованная иерархия page object'ов

  • PO
    • Page
    • Page
    • Page
      • Component
        • Component
        • Element
      • Component
      • Element
      • Element
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

  1. Page object'ы для базовых HTML-элементов
  2. Централизованная иерархия page object'ов
  3. 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

  1. Page object'ы для базовых HTML-элементов
  2. Централизованная иерархия page object'ов
  3. Page object'ы знают своего родителя и корневой PO
  4. 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

  1. Page object'ы для базовых HTML-элементов
  2. Централизованная иерархия page object'ов
  3. Page object'ы знают своего родителя и корневой PO
  4. Page object'ы инициализируются автоматически
  5. 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

  1. Page object'ы для базовых HTML-элементов
  2. Централизованная иерархия page object'ов
  3. Page object'ы знают своего родителя и корневой PO
  4. Page object'ы инициализируются автоматически
  5. Assertions внутри page object'ов
  6. Работа с коллекциями, выбор элемента из нескольких

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

  1. Page object'ы для базовых HTML-элементов
  2. Централизованная иерархия page object'ов
  3. Page object'ы знают своего родителя и корневой PO
  4. Page object'ы инициализируются автоматически
  5. Assertions внутри page object'ов
  6. Работа с коллекциями, выбор элемента из нескольких
  7. Встроенные и переопределяемые селекторы

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

  1. Page object'ы для базовых HTML-элементов
  2. Централизованная иерархия page object'ов
  3. Page object'ы знают своего родителя и корневой PO
  4. Page object'ы инициализируются автоматически
  5. Assertions внутри page object'ов
  6. Работа с коллекциями, выбор элемента из нескольких
  7. Встроенные и переопределяемые селекторы
  8. Автоматический скоупинг селекторов (опциональный)

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

  1. Page object'ы для базовых HTML-элементов
  2. Централизованная иерархия page object'ов
  3. Page object'ы знают своего родителя и корневой PO
  4. Page object'ы инициализируются автоматически
  5. Assertions внутри page object'ов
  6. Работа с коллекциями, выбор элемента из нескольких
  7. Встроенные и переопределяемые селекторы
  8. Автоматический скоупинг селекторов (опциональный)
  9. Читаемость тестов и отчетов без `test.step()`

9. Читаемость тестов и отчетов без `test.step()`

Ноу-хау Андрея Волынкина

(@avol-v)

Десять ноу-хау Pomeranian

  1. Page object'ы для базовых HTML-элементов
  2. Централизованная иерархия page object'ов
  3. Page object'ы знают своего родителя и корневой PO
  4. Page object'ы инициализируются автоматически
  5. Assertions внутри page object'ов
  6. Работа с коллекциями, выбор элемента из нескольких
  7. Встроенные и переопределяемые селекторы
  8. Автоматический скоупинг селекторов (опциональный)
  9. Читаемость тестов и отчетов без `test.step()`
  10. Конвенция именования:
    page-object'ы с большой буквы,
    методы с маленькой

Pomeranian — гибкий и эффективный подход к паттерну Page Object

By Andrey Mikhaylov (lolmaus)

Pomeranian — гибкий и эффективный подход к паттерну Page Object

  • 76