次の方法で共有


ページ オブジェクト モデル

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)

次のステップ

こちらも参照ください