Page Object Model (POM) は、特定のページまたはコンポーネントのセレクターとアクションを専用クラスにカプセル化するデザイン パターンです。 テストでは、生ロケーターを使用する代わりに、ページ オブジェクトのメソッドを呼び出します。 この方法により、UI が変更されたときにテストの読みやすく、保守が容易になります。
Power Platform テストに POM を使用する理由
Power Platform アプリには、POM を特に価値のあるものにするいくつかの特性があります。
-
キャンバス アプリには多くの
data-control-name属性 があります。クラスで一元化すると、コントロールの名前を変更するだけで 1 つの変更が必要になります。 - モデル駆動型フォーム フィールド スキーマ名 は、テーブルが変更された場合に変更される可能性があります。POM でテーブルを分離すると、変更の影響が制限されます。
- 一般的なアクション (ギャラリーに移動し、[追加] をクリックしてレコードを保存) は、多くのテストで繰り返されます。POM は重複を回避します。
ツールキット内蔵のページオブジェクト
power-platform-playwright-toolkitには、既製のページ オブジェクトが用意されています。
| クラス | アプリの種類 | 主要なメソッド |
|---|---|---|
CanvasAppPage |
Canvas |
waitForLoad()、getFrame() |
ModelDrivenAppPage |
モデル駆動型 |
navigateToGridView()、navigateToFormView() |
GridComponent |
モデル駆動型アプリ グリッド |
filterByKeyword()、getCellValue()、openRecord()、selectRow() |
FormComponent |
モデル駆動型アプリ フォーム |
getAttribute()、setAttribute()、save()、isDirty() |
CommandingComponent |
モデル駆動型アプリのコマンド バー |
clickButton()、clickMoreCommands() |
AppProviderを使用してアクセスします。
const app = new AppProvider(page, context);
await app.launch({ ... });
const mda = app.getModelDrivenAppPage();
// mda.grid, mda.form, mda.commanding are ready to use
キャンバス アプリ用のカスタム POM を作成する
特定のアプリ用に独自のページ オブジェクトを作成して、ツールキットを拡張します。
// pages/accounts/AccountsCanvasPage.ts
import { Page, FrameLocator, expect } from '@playwright/test';
export class AccountsCanvasPage {
private readonly frame: FrameLocator;
constructor(private readonly page: Page) {
this.frame = page.frameLocator('iframe[name="fullscreen-app-host"]');
}
// --- Locators ---
get gallery() {
return this.frame.locator('[data-control-name="Gallery1"]');
}
get addButton() {
return this.frame.locator('[data-control-name="IconButton_Add1"] [role="button"]');
}
get saveButton() {
return this.frame.locator('[data-control-name="IconButton_Accept1"] [role="button"]');
}
get accountNameInput() {
return this.frame.locator('input[aria-label="Account Name"]');
}
get phoneInput() {
return this.frame.locator('input[aria-label="Main Phone"]');
}
// --- Actions ---
async waitForLoad(): Promise<void> {
await this.gallery
.locator('[data-control-part="gallery-item"]')
.first()
.waitFor({ state: 'visible', timeout: 60000 });
}
async clickAdd(): Promise<void> {
await this.addButton.waitFor({ state: 'visible' });
await this.addButton.click();
}
async fillAccountForm(accountName: string, phone: string): Promise<void> {
await this.accountNameInput.fill(accountName);
await this.phoneInput.fill(phone);
}
async save(): Promise<void> {
await this.saveButton.click();
}
async findAccount(name: string) {
return this.gallery
.locator('[data-control-part="gallery-item"]')
.filter({
has: this.frame
.locator('[data-control-name="Title1"]')
.getByText(name, { exact: true }),
});
}
async expectAccountVisible(name: string): Promise<void> {
const item = await this.findAccount(name);
await expect(item).toBeVisible({ timeout: 30000 });
}
async getItemCount(): Promise<number> {
return this.gallery.locator('[data-control-part="gallery-item"]').count();
}
}
テストで POM を使用する
次の例は、テストが AccountsCanvasPage ページ オブジェクトを使用して、テスト コードが動作に重点を置いた状態を維持する方法を示しています。
// tests/accounts/accounts.test.ts
import { test, expect } from '@playwright/test';
import { AppProvider, AppType, AppLaunchMode, buildCanvasAppUrlFromEnv } from 'power-platform-playwright-toolkit';
import { AccountsCanvasPage } from '../../pages/accounts/AccountsCanvasPage';
test.describe('Accounts canvas app', () => {
let accountsPage: AccountsCanvasPage;
test.beforeEach(async ({ page, context }) => {
const app = new AppProvider(page, context);
await app.launch({
app: 'Accounts App',
type: AppType.Canvas,
mode: AppLaunchMode.Play,
skipMakerPortal: true,
directUrl: buildCanvasAppUrlFromEnv(),
});
accountsPage = new AccountsCanvasPage(page);
await accountsPage.waitForLoad();
});
test('should display accounts', async () => {
const count = await accountsPage.getItemCount();
expect(count).toBeGreaterThan(0);
});
test('should create a new account', async () => {
const name = `Test Account ${Date.now()}`;
await accountsPage.clickAdd();
await accountsPage.fillAccountForm(name, '555-9000');
await accountsPage.save();
await accountsPage.expectAccountVisible(name);
});
});
モデル駆動型エンティティのカスタム POM を作成する
特定のエンティティのツールキットの ModelDrivenAppPage をラップします。
// pages/orders/OrdersPage.ts
import { Page, expect } from '@playwright/test';
import { ModelDrivenAppPage } from 'power-platform-playwright-toolkit';
const ENTITY = 'nwind_orders';
const ORDER_NUMBER_FIELD = 'nwind_ordernumber';
const STATUS_FIELD = 'nwind_orderstatusid';
export class OrdersPage {
constructor(
private readonly page: Page,
private readonly mda: ModelDrivenAppPage,
) {}
async navigateToList(): Promise<void> {
await this.mda.navigateToGridView(ENTITY);
await this.mda.grid.waitForGridLoad();
}
async filterByOrderNumber(orderNumber: string): Promise<void> {
await this.mda.grid.filterByKeyword(orderNumber);
await this.mda.grid.waitForGridLoad();
}
async openFirstOrder(): Promise<void> {
await this.mda.grid.openRecord({ rowNumber: 0 });
}
async getOrderNumber(): Promise<string | null> {
return this.mda.form.getAttribute(ORDER_NUMBER_FIELD);
}
async setOrderNumber(value: string): Promise<void> {
await this.mda.form.setAttribute(ORDER_NUMBER_FIELD, value);
}
async saveOrder(): Promise<void> {
await this.mda.form.save();
expect(await this.mda.form.isDirty()).toBe(false);
}
async deleteFirstOrder(): Promise<void> {
await this.mda.grid.selectRow(0);
await this.page.locator('button[aria-label*="Delete"]').first().click();
const dialog = this.page.locator('[role="dialog"]');
await dialog.locator('button:has-text("Delete")').click();
}
}
フォルダー構造
ミラー化されたディレクトリ構造で、ページ オブジェクトをテストと共に整理します。
packages/e2e-tests/
├── pages/
│ ├── accounts/
│ │ └── AccountsCanvasPage.ts
│ ├── orders/
│ │ └── OrdersPage.ts
│ └── northwind/
│ ├── NorthwindCanvasAppPage.ts
│ └── CustomPage.page.ts
├── tests/
│ ├── accounts/
│ │ └── accounts.test.ts
│ └── northwind/
│ ├── canvas/
│ └── mda/
└── playwright.config.ts
POM 設計ガイドライン
次のガイドラインに従って、ページ オブジェクトの一貫性と保守を容易にします。
- 論理ページまたは主要 UI セクションごとに 1 つのクラス - アプリ全体を 1 つのクラスに配置しないでください
- ロケーターを文字列ではなくゲッターとして公開することにより、ロケーターオブジェクトはより優れた型安全性と自動待機を提供します。
-
アクション メソッド内に
waitForを配置します 。呼び出し元は、待機するタイミングを知る必要はありません - ページ オブジェクトではなく、テストでアサーションを保持します 。POM はアクションを実行し、データを返す必要があります。テストで期待値を確認する必要がある
-
わかりやすいメソッド名を使用する —
clickAdd()はclick()よりも優れているfindAccount(name)getItem(text)