Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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([]);
});
});