Capacidades de prueba avanzadas

Además de las pruebas funcionales, Playwright proporciona compatibilidad integrada para pruebas de regresión visual, simulación de solicitudes de red y auditoría de accesibilidad. En este artículo se muestra cómo aplicar cada una a las pruebas de aplicaciones de Power Platform.

Pruebas de comparación visual

La aserción toHaveScreenshot() de Playwright toma una captura de pantalla de referencia en la primera ejecución y compara las ejecuciones posteriores con ella. Las diferencias de nivel de píxel producen un error en la prueba.

Captura de una aplicación lienzo de referencia

En el ejemplo siguiente se lanza una aplicación de lienzo y se obtiene una captura de pantalla del control de la galería, para establecer una base visual para una comparación futura.

import { test, expect } from '@playwright/test';
import { AppProvider, AppType, AppLaunchMode, buildCanvasAppUrlFromEnv } from 'power-platform-playwright-toolkit';

test('gallery matches visual baseline', async ({ page, context }) => {
  const app = new AppProvider(page, context);
  await app.launch({
    app: 'Orders App',
    type: AppType.Canvas,
    mode: AppLaunchMode.Play,
    skipMakerPortal: true,
    directUrl: buildCanvasAppUrlFromEnv(),
  });

  const canvasFrame = page.frameLocator('iframe[name="fullscreen-app-host"]');
  await canvasFrame
    .locator('[data-control-part="gallery-item"]')
    .first()
    .waitFor({ state: 'visible', timeout: 60000 });

  // Capture the canvas frame only (not the model-driven app shell chrome)
  const galleryLocator = canvasFrame.locator('[data-control-name="Gallery1"]');
  await expect(galleryLocator).toHaveScreenshot('orders-gallery.png');
});

Nota:

En la primera ejecución, Playwright escribe la captura de pantalla inicial en tests/__screenshots__/. Confirme estos archivos en el control de código fuente. Las ejecuciones posteriores se diferencian de ellas.

Actualizar líneas base

Cuando la interfaz de usuario cambie intencionadamente, actualice las líneas base:

npx playwright test --update-snapshots

Configuración de umbrales de captura de pantalla

Permita una pequeña diferencia de píxeles para dar cabida a la representación de fuentes entre entornos:

// playwright.config.ts
export default defineConfig({
  expect: {
    toHaveScreenshot: {
      maxDiffPixelRatio: 0.01,  // allow 1% pixel difference
      threshold: 0.2,           // per-pixel color difference threshold
    },
  },
});

Comparación de vistas de aplicaciones controladas por modelos

En el caso de las aplicaciones basadas en modelos, debe definir el ámbito de la captura de pantalla para evitar capturar marcas de tiempo dinámicas:

test('order grid matches visual baseline', async ({ page, context }) => {
  const app = new AppProvider(page, context);
  await app.launch({ ... });
  const mda = app.getModelDrivenAppPage();

  await mda.navigateToGridView('nwind_orders');
  await mda.grid.waitForGridLoad();

  // Capture only the grid container, not the full page
  const grid = page.locator('[ref="eBodyContainer"]');
  await expect(grid).toHaveScreenshot('orders-grid.png', {
    mask: [page.locator('[col-id="modifiedon"]')],  // mask dynamic columns
  });
});

Simulación de solicitudes de red

Playwright page.route() intercepta las solicitudes HTTP. Úselo para simular respuestas de api de Dataverse, simular condiciones de error o acelerar las pruebas que no necesitan datos activos.

Simulación de una respuesta de WebApi de Dataverse

En el ejemplo siguiente se intercepta una llamada a Dataverse WebAPI y se devuelve una respuesta JSON simulada, por lo que puede probar el comportamiento de la aplicación sin depender de datos activos.

test('gallery shows mocked orders', async ({ page, context }) => {
  // Intercept Dataverse API calls before launching the app
  await page.route('**/api/data/v9.2/nwind_orders*', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        value: [
          { nwind_ordernumber: 'ORD-MOCK-001', nwind_name: 'Mocked Order 1' },
          { nwind_ordernumber: 'ORD-MOCK-002', nwind_name: 'Mocked Order 2' },
        ],
      }),
    });
  });

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

  const canvasFrame = page.frameLocator('iframe[name="fullscreen-app-host"]');
  await expect(
    canvasFrame.locator('[data-control-part="gallery-item"]').first()
  ).toBeVisible({ timeout: 30000 });

  // Verify mocked data appears
  await expect(
    canvasFrame.locator('[data-control-name="Title1"]').getByText('Mocked Order 1')
  ).toBeVisible();
});

Simulación de un error de API

En el ejemplo siguiente se simula un error de servidor devolviendo un código de estado 500, por lo que puede comprobar que la aplicación muestra el estado de error adecuado.

test('shows error state when API fails', async ({ page, context }) => {
  await page.route('**/api/data/v9.2/nwind_orders*', (route) => {
    route.fulfill({ status: 500, body: 'Internal Server Error' });
  });

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

  const canvasFrame = page.frameLocator('iframe[name="fullscreen-app-host"]');

  // Verify the app shows an error or empty state
  await expect(canvasFrame.locator('[data-control-name="ErrorLabel1"]')).toBeVisible();
});

Interceptar y observar solicitudes (sin simular)

En el ejemplo siguiente se escuchan las solicitudes POST salientes a la API de Dataverse sin modificarlas, por lo que puede comprobar que la aplicación envía las solicitudes esperadas cuando se produce una acción del usuario.

test('save triggers a POST to Dataverse', async ({ page, context }) => {
  const apiRequests: string[] = [];

  page.on('request', (req) => {
    if (req.url().includes('/api/data/v9.2/') && req.method() === 'POST') {
      apiRequests.push(req.url());
    }
  });

  // ... perform save action ...

  expect(apiRequests.some((url) => url.includes('nwind_orders'))).toBe(true);
});

Tip

Combina la observación de solicitudes con el suministro ficticio para validar que se envía la consulta OData correcta a Dataverse. Este enfoque es útil para comprobar que los filtros y las expansións se construyen correctamente.

Pruebas de accesibilidad

Playwright se integra con axe-core a través del paquete @axe-core/playwright para verificar el cumplimiento de las Directrices de accesibilidad de contenido web (WCAG).

Instalación de axe-core para Playwright

Ejecute el siguiente comando para agregar el paquete axe-core Playwright como una dependencia de desarrollo en el proyecto de prueba.

cd packages/e2e-tests
npm install --save-dev @axe-core/playwright

Auditar una aplicación de lienzo para detectar infracciones de accesibilidad

En el ejemplo siguiente se inicia una aplicación de lienzo y se ejecuta una auditoría axe-core limitada a las reglas de Nivel A y AA, de WCAG 2.0. La prueba falla si encuentra alguna infracción.

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { AppProvider, AppType, AppLaunchMode, buildCanvasAppUrlFromEnv } from 'power-platform-playwright-toolkit';

test('canvas app has no critical accessibility violations', async ({ page, context }) => {
  const app = new AppProvider(page, context);
  await app.launch({
    app: 'Orders App',
    type: AppType.Canvas,
    mode: AppLaunchMode.Play,
    skipMakerPortal: true,
    directUrl: buildCanvasAppUrlFromEnv(),
  });

  const canvasFrame = page.frameLocator('iframe[name="fullscreen-app-host"]');
  await canvasFrame
    .locator('[data-control-part="gallery-item"]')
    .first()
    .waitFor({ state: 'visible', timeout: 60000 });

  // Audit the canvas iframe content
  const frame = page.frame({ name: 'fullscreen-app-host' });
  if (!frame) throw new Error('Canvas frame not found');

  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa'])
    .include('iframe[name="fullscreen-app-host"]')
    .analyze();

  expect(results.violations).toEqual([]);
});

Auditar un formulario de una aplicación controlada por modelos

En el ejemplo siguiente se abre un registro en una aplicación controlada por modelos y se ejecuta una auditoría de accesibilidad. Filtra los resultados solo a infracciones críticas y graves.

test('order form has no accessibility violations', async ({ page, context }) => {
  const app = new AppProvider(page, context);
  await app.launch({ ... });
  const mda = app.getModelDrivenAppPage();

  await mda.navigateToGridView('nwind_orders');
  await mda.grid.waitForGridLoad();
  await mda.grid.openRecord({ rowNumber: 0 });

  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa'])
    .exclude('.ms-Spinner')  // exclude loading spinners
    .analyze();

  // Filter to critical and serious violations only
  const critical = results.violations.filter(
    (v) => v.impact === 'critical' || v.impact === 'serious'
  );

  expect(critical).toEqual([]);
});

Notificar infracciones de accesibilidad

Para obtener una salida legible cuando se encuentran infracciones, dé formato a la salida de la prueba.

if (results.violations.length > 0) {
  const summary = results.violations
    .map((v) => `[${v.impact}] ${v.id}: ${v.description}`)
    .join('\n');

  throw new Error(`Accessibility violations found:\n${summary}`);
}

Exclusión de infracciones conocidas

Si la aplicación tiene infracciones conocidas que aceptas o realizas un seguimiento por separado:

const results = await new AxeBuilder({ page })
  .withTags(['wcag2a', 'wcag2aa'])
  .disableRules(['color-contrast'])  // Known issue tracked in #123
  .analyze();

Importante

Deshabilitar las reglas debe ser temporal. Realice un seguimiento de cada regla deshabilitada con una referencia de elemento de trabajo para que pueda ser corregida.

Combinar funcionalidades

Puede combinar pruebas visuales, de red y de accesibilidad en el mismo archivo de prueba. Un patrón común es un conjunto de pruebas de humo que ejecuta los tres.

test.describe('Canvas app smoke tests', () => {
  test('loads successfully (visual)', async ({ page, context }) => {
    // ... launch app ...
    await expect(canvasFrame.locator('[data-control-name="Gallery1"]'))
      .toHaveScreenshot('gallery-baseline.png');
  });

  test('Dataverse API is called on load (network)', async ({ page, context }) => {
    const calls: string[] = [];
    page.on('request', (req) => {
      if (req.url().includes('/api/data/')) calls.push(req.url());
    });
    // ... launch app ...
    expect(calls.length).toBeGreaterThan(0);
  });

  test('has no accessibility violations (a11y)', async ({ page, context }) => {
    // ... launch app ...
    const results = await new AxeBuilder({ page }).withTags(['wcag2aa']).analyze();
    expect(results.violations.filter((v) => v.impact === 'critical')).toEqual([]);
  });
});

Pasos siguientes

Consulte también