Tester des applications pilotées par modèle

Les applications basées sur des modèles affichent des vues de liste à l’aide d’un composant de grille de données et d’une exécution de formulaire pour modifier les enregistrements. Les classes GridComponent et FormComponent du framework abstraient la structure DOM sous-jacente, donc vous n'avez pas besoin d'écrire des sélecteurs complexes. Ce guide montre comment accéder à une application, interagir avec la grille, ouvrir des enregistrements et vérifier les valeurs des champs de formulaire à l’aide du ModelDrivenAppPage et de ses composants intégrés.

Lancer une application basée sur un modèle

Utilisez skipMakerPortal: true et directUrl pour contourner Power Apps navigation et accéder directement à l’application :

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

const MODEL_DRIVEN_APP_URL = process.env.MODEL_DRIVEN_APP_URL!;

test.beforeEach(async ({ page, context }) => {
  const app = new AppProvider(page, context);

  await app.launch({
    app: 'Northwind Orders',
    type: AppType.ModelDriven,
    mode: AppLaunchMode.Play,
    skipMakerPortal: true,
    directUrl: MODEL_DRIVEN_APP_URL,
  });

  modelDrivenApp = app.getModelDrivenAppPage();
});

Travailler avec la grille

Le GridComponent gère le AG Grid qui alimente les vues de liste pilotées par modèle. Il emploie des attributs [row-index] pour un ciblage fiable des lignes qui fonctionne correctement après le filtrage.

Attendez que la grille se charge

Accédez à l’affichage de liste d’une entité et attendez que les lignes de grille s’affichent avant d’interagir avec elles.

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

Filtrer la grille

Réduisez les résultats de la grille en recherchant toutes les colonnes visibles ou en ciblant une colonne spécifique.

// Filter by keyword (searches across visible columns)
await modelDrivenApp.grid.filterByKeyword('ORD-12345');
await modelDrivenApp.grid.waitForGridLoad();

// Filter by a specific column
await modelDrivenApp.grid.filterByColumn('Order', 'ORD-12345');

Lire les valeurs des cellules

Récupérez la valeur affichée d’une cellule par index de ligne et nom de colonne (nom de schéma ou nom complet).

// By column schema name
const orderNumber = await modelDrivenApp.grid.getCellValue(0, 'nwind_ordernumber');

// By column display name
const status = await modelDrivenApp.grid.getCellValue(0, 'Order Status');

Ouvrir un enregistrement

Ouvrez le formulaire d’un enregistrement par numéro de ligne ou en faisant correspondre une valeur de colonne.

// Open the first record in the grid
await modelDrivenApp.grid.openRecord({ rowNumber: 0 });

// Open a record by column value
await modelDrivenApp.grid.openRecord({
  columnName: 'Order Number',
  columnValue: 'ORD-12345',
});

Sélectionner des lignes

Sélectionnez une ou plusieurs lignes dans la grille ou vérifiez si la grille contient des enregistrements.

// Select one row
await modelDrivenApp.grid.selectRow(0);

// Select multiple rows
await modelDrivenApp.grid.selectRows([0, 1, 2]);

// Check if grid is empty
const isEmpty = await modelDrivenApp.grid.isGridEmpty();

Utiliser des formulaires

Le FormComponent encapsule le runtime de formulaire Dynamics 365 et l’API Xrm FormContext.

Lire les valeurs de champ

Utilisez getAttribute() pour récupérer la valeur actuelle d’un champ de formulaire par son nom de schéma.

const orderNumber = await modelDrivenApp.form.getAttribute('nwind_ordernumber');
const status = await modelDrivenApp.form.getAttribute('nwind_orderstatusid');

Écrire des valeurs de champ

Permet setAttribute() de définir par programmation la valeur d’un champ sur le formulaire.

await modelDrivenApp.form.setAttribute('nwind_ordernumber', 'ORD-99999');
await modelDrivenApp.form.setAttribute('nwind_notes', 'Updated via test');

Enregistrer le formulaire

Enregistrez l’enregistrement et vérifiez que le formulaire n’est plus sale et passe la validation.

await modelDrivenApp.form.save();

// Verify the form saved successfully
expect(await modelDrivenApp.form.isDirty()).toBe(false);
expect(await modelDrivenApp.form.isValid()).toBe(true);

Basculez entre les onglets du formulaire pour accéder aux champs dans différentes sections.

await modelDrivenApp.form.navigateToTab('Summary');
await modelDrivenApp.form.navigateToTab('Details');

Contrôler la visibilité et l’état des champs

Modifiez la visibilité, l’état désactivé ou le niveau requis d’un champ au moment de l’exécution.

await modelDrivenApp.form.setFieldVisibility('nwind_notes', true);
await modelDrivenApp.form.setFieldDisabled('nwind_ordernumber', false);
await modelDrivenApp.form.setFieldRequiredLevel('nwind_customerid', 'required');

Exécuter du code Xrm personnalisé

Exécutez tout code qui utilise l’API Dynamics 365 Xrm directement dans le contexte de formulaire :

const result = await modelDrivenApp.form.execute(async (formContext) => {
  const attr = formContext.getAttribute('nwind_ordernumber');
  return attr?.getValue();
});

console.log(`Order number from Xrm: ${result}`);

Exemple de flux de travail CRUD complet

Ce test illustre un flux de travail de création, de lecture, de mise à jour et de suppression complet sur une application basée sur un modèle.

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

const ENTITY = 'nwind_orders';
const APP_URL = process.env.MODEL_DRIVEN_APP_URL!;

test('should create, read, update, and delete an order', async ({ page, context }) => {
  const app = new AppProvider(page, context);
  await app.launch({
    app: 'Northwind Orders',
    type: AppType.ModelDriven,
    mode: AppLaunchMode.Play,
    skipMakerPortal: true,
    directUrl: APP_URL,
  });
  const mda = app.getModelDrivenAppPage();

  // Step 1: Create a new order record
  await mda.navigateToFormView(ENTITY);
  await page.locator('input[data-id="nwind_ordernumber.fieldControl-text-box-text"]').fill('ORD-TEST-001');
  await page.locator('button[aria-label*="Save"]').first().click();
  await page.waitForTimeout(3000);

  // Step 2: Read the record in the grid
  await mda.navigateToGridView(ENTITY);
  await mda.grid.waitForGridLoad();
  await mda.grid.filterByKeyword('ORD-TEST-001');
  await mda.grid.waitForGridLoad();

  expect(await mda.grid.getRowCount()).toBeGreaterThan(0);
  const cellValue = await mda.grid.getCellValue(0, 'Order Number');
  expect(cellValue).toContain('ORD-TEST-001');

  // Step 3: Update the order number
  await mda.grid.openRecord({ rowNumber: 0 });
  await page.locator('input[data-id="nwind_ordernumber.fieldControl-text-box-text"]').fill('ORD-TEST-001-UPDATED');
  await page.locator('button[aria-label*="Save"]').first().click();
  await page.waitForTimeout(3000);

  // Step 4: Delete the record
  await mda.navigateToGridView(ENTITY);
  await mda.grid.waitForGridLoad();
  await mda.grid.filterByKeyword('ORD-TEST-001-UPDATED');
  await mda.grid.waitForGridLoad();
  await mda.grid.selectRow(0);
  await page.locator('button[aria-label*="Delete"]').first().click();
  // Confirm the deletion dialog
  const dialog = page.locator('[role="dialog"]');
  await dialog.locator('button:has-text("Delete")').click();
});

Utilisez la barre latérale du plan de plan pour accéder à une vue de liste d’entités spécifique.

// Navigate to a specific entity list view via sitemap
const sidebarItem = page.locator('[role="presentation"][title="Orders"]').first();
await sidebarItem.waitFor({ state: 'visible', timeout: 15000 });
await sidebarItem.click();

Dépannage des problèmes de grille

Le tableau suivant répertorie les problèmes courants d’interaction de grille et la façon de les résoudre.

Symptôme Cause probable Résolution
Ligne non trouvée après application du filtrage La grille est toujours en cours de rendu Ajouter await mda.grid.waitForGridLoad() après le filtre
nth-child échec du sélecteur Structure d’en-tête/ligne AG Grid Utiliser le sélecteur [row-index] intégré dans GridComponent
Le clic sur la case à cocher est bloqué par une superposition L’icône CheckMark recouvre le champ de saisie Utilisez { force: true } à l'activation de la case à cocher

Étapes suivantes

Voir également