Modelo de objetos de página

El modelo de objetos de página (POM) es un patrón de diseño que encapsula los selectores y las acciones de una página o componente específicos en una clase dedicada. Prueba llamar a métodos en el objeto de página en lugar de usar localizadores sin procesar. Este enfoque hace que las pruebas sean más legibles y fáciles de mantener cuando cambia la interfaz de usuario.

¿Por qué usar POM para pruebas de Power Platform?

Las aplicaciones de Power Platform tienen varias características que hacen que POM sea especialmente valioso:

  • Las aplicaciones de lienzo tienen muchos data-control-name atributos — centralizarlos en una clase significa que cambiar el nombre de un control solo requiere una modificación.
  • Los nombres de esquema de campo de formulario controlados por modelos pueden cambiar si se modifican las tablas; aislarlos en un POM limita el impacto de los cambios.
  • Las acciones comunes (navegar a la galería, hacer clic en Agregar, guardar registro) se repiten en muchas pruebas: POM evita la duplicación.

Objetos de página integrados del kit de herramientas

power-platform-playwright-toolkit proporciona objetos de página listos:

Class Tipo de aplicación Métodos clave
CanvasAppPage Canvas waitForLoad(), getFrame()
ModelDrivenAppPage Dirigido por modelos navigateToGridView(), navigateToFormView()
GridComponent cuadrícula de aplicaciones controladas por modelos filterByKeyword(), getCellValue(), , openRecord(), selectRow()
FormComponent Formulario de app impulsada por modelos getAttribute(), setAttribute(), , save(), isDirty()
CommandingComponent Barra de comandos de la aplicación controlada por modelos clickButton(), clickMoreCommands()

Acceda a ellos a través de AppProvider:

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

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

Crea un POM personalizado para tu aplicación de canvas

Amplíe el kit de herramientas mediante la creación de su propio objeto de página para su aplicación específica:

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

Uso de POM en pruebas

En el ejemplo siguiente se muestra cómo las pruebas consumen el objeto de página para mantener el AccountsCanvasPage código de prueba centrado en el comportamiento.

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

Creación de un POM personalizado para entidades controladas por modelos

Encapsular el kit de ModelDrivenAppPage herramientas para una entidad específica:

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

Estructura de carpetas

Organice los objetos de página junto con las pruebas en una estructura de directorio reflejada:

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

Directrices de diseño POM

Siga estas instrucciones para mantener los objetos de página coherentes y fáciles de mantener.

  • Una clase por página lógica o sección de interfaz de usuario principal : no coloque toda la aplicación en una clase.
  • Exponer localizadores como captadores, no cadenas : el objeto de localizador proporciona una mejor seguridad de tipos y espera automática.
  • Colocar waitFor dentro de los métodos de acción — quienes llaman no deben saber cuándo esperar
  • Mantener aserciones en pruebas, no objetos de página : las POM deben realizar acciones y devolver datos; las pruebas deben comprobar las expectativas
  • Usar nombres de método descriptivos: clickAdd() es mejor que click(), findAccount(name) es mejor que getItem(text).

Pasos siguientes

Consulte también