Freigeben über


Seitenobjektmodell

Das Page Object Model (POM) ist ein Entwurfsmuster, das die Selektoren und Aktionen für eine bestimmte Seite oder Komponente in eine dedizierte Klasse kapselt. Testet Aufrufmethoden für das Seitenobjekt, anstatt unformatierte Locators zu verwenden. Mit diesem Ansatz können Tests besser lesbar und einfacher verwaltet werden, wenn sich die Benutzeroberfläche ändert.

Gründe für die Verwendung von POM für Power Platform-Tests

Power Platform-Apps weisen mehrere Merkmale auf, die POM besonders wertvoll machen:

  • Canvas-Apps verfügen über viele data-control-name Attribute – das Zentralisieren dieser Attribute in einer Klasse bedeutet, dass beim Umbenennen eines Steuerelements nur eine einzige Änderung erforderlich ist.
  • Modellgesteuerte Formularfeldschemanamen können sich ändern, wenn Tabellen geändert werden – durch das Isolieren in einem POM werden die Auswirkungen von Änderungen begrenzt.
  • Allgemeine Aktionen (navigieren Sie zum Katalog, klicken Sie auf "Hinzufügen", speichern Sie Datensatz) werden in vielen Tests wiederholt – POM vermeidet Duplizierung.

Integrierte Toolkit-Seitenobjekte

Das power-platform-playwright-toolkit stellt vorgefertigte Seitenobjekte bereit:

Klasse App-Typ Schlüsselmethoden
CanvasAppPage Leinwand waitForLoad(), getFrame()
ModelDrivenAppPage Modellgesteuert navigateToGridView(), navigateToFormView()
GridComponent Modellgesteuertes App-Raster filterByKeyword(), getCellValue(), openRecord()selectRow()
FormComponent modellgesteuertes App-Formular getAttribute(), setAttribute(), save(), isDirty()
CommandingComponent Modellgesteuerte App-Befehlsleiste clickButton(), clickMoreCommands()

Zugriff auf sie über AppProvider:

const app = new AppProvider(page, context);
await app.launch({ ... });

const mda = app.getModelDrivenAppPage();
// mda.grid, mda.form, mda.commanding are ready to use

Erstellen eines benutzerdefinierten POM für Ihre Canvas-App

Erweitern Sie das Toolkit, indem Sie ein eigenes Seitenobjekt für Ihre spezifische App erstellen:

// 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();
  }
}

Verwenden des POM in Tests

Das folgende Beispiel zeigt, wie Tests das AccountsCanvasPage Seitenobjekt verwenden, um testcode im Fokus des Verhaltens zu halten.

// 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);
  });
});

Erstellen eines benutzerdefinierten POM für modellgesteuerte Entitäten

Schließen Sie das Toolkit ModelDrivenAppPage für eine spezifische Entität ein:

// 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();
  }
}

Ordnerstruktur

Organisieren Sie Seitenobjekte zusammen mit Tests in einer gespiegelten Verzeichnisstruktur:

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-Designrichtlinien

Befolgen Sie diese Richtlinien, um Ihre Seitenobjekte konsistent und einfach zu verwalten.

  • Eine Klasse pro logischer Seite oder Hauptbenutzeroberflächenabschnitt – platzieren Sie die gesamte App nicht in einer Klasse.
  • Locatoren als Getter, nicht als Zeichenfolgen darstellen – das Locator-Objekt bietet bessere Typsicherheit und unterstützt automatisches Warten
  • Setzen Sie waitFor in Aktionsmethoden ein — Aufrufer sollten nicht wissen müssen, wann sie warten müssen
  • Halten Sie Assertionen in Tests, nicht in Seitenobjekten – POMs sollten Aktionen ausführen und Daten zurückgeben; Tests sollten die Erwartungen überprüfen
  • Verwenden Sie beschreibende MethodennamenclickAdd() ist besser als click(), findAccount(name) ist besser als getItem(text)

Nächste Schritte

Siehe auch