次の方法で共有


モデル駆動型アプリでカスタム ページをテストする

カスタム ページは、モデル駆動型アプリ内に埋め込まれたキャンバス アプリです。 モデル駆動型アプリ シェル内の iframe でレンダリングされます。 テストするには、モデル駆動型アプリに移動し、サイトマップからカスタム ページを選択し、すべてのコントロール操作を内部 iframe にスコープする必要があります。

モデル駆動型アプリでのカスタム ページ テストのしくみ

カスタム ページが読み込まれると、モデル駆動型アプリ シェルは Dynamics 365 ドメインに残ります。 カスタムページキャンバスランタイムは内部に読み込まれます。

iframe[name="fullscreen-app-host"]

これは、スタンドアロンのキャンバス アプリで使用されるのと同じ iframe です。 フレーム ロケーターを取得すると、すべてのキャンバス アプリのテスト パターンが適用されます。

  1. AppProviderを使用してモデル駆動型アプリを起動します。
  2. サイトマップでカスタム ページ項目を選択します。
  3. キャンバス ランタイムが初期化されるまで待ちます。
import { test, expect } from '@playwright/test';
import { AppProvider, AppType, AppLaunchMode } from 'power-platform-playwright-toolkit';

const MODEL_DRIVEN_APP_URL = process.env.MODEL_DRIVEN_APP_URL!;
const CUSTOM_PAGE_NAME = process.env.CUSTOM_PAGE_NAME ?? 'AccountsCustomPage';

test.beforeAll(async ({ browser }) => {
  const context = await browser.newContext({ storageState: mdaStorageStatePath });
  const page = await context.newPage();

  const app = new AppProvider(page, context);
  await app.launch({
    app: 'My App',
    type: AppType.ModelDriven,
    mode: AppLaunchMode.Play,
    skipMakerPortal: true,
    directUrl: MODEL_DRIVEN_APP_URL,
  });

  // Navigate to the custom page via the sitemap
  const sidebarItem = page
    .locator(`[role="presentation"][title="${CUSTOM_PAGE_NAME}"]`)
    .first();
  await sidebarItem.waitFor({ state: 'visible', timeout: 30000 });
  await sidebarItem.click();
  await page.waitForTimeout(3000);
});

カスタム ページ コントロールを操作する

カスタム ページに移動した後、スコープ ロケーターをキャンバス iframe に移動します。

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

// Wait for gallery to appear
await canvasFrame
  .locator('[data-control-part="gallery-item"]')
  .first()
  .waitFor({ state: 'visible', timeout: 30000 });

カスタムページコントロール内のボタンをクリック

data-control-name属性を使用してキャンバス iframe 内の特定のボタン コントロールをターゲットにし、内部[role="button"]要素を見つけてクリック アクションをトリガーします。

await canvasFrame.locator('[data-control-name="IconButton_Accept1"] [role="button"]').click();
await canvasFrame.locator('[data-control-name="IconButton_Edit1"] [role="button"]').click();

ユーザー設定ページのフォームフィールドに入力を行う。

キャンバス iframe 内の aria-label 属性で入力フィールドを検索し、 fill メソッドを使用して値を入力します。

const accountNameInput = canvasFrame.locator('input[aria-label="Account Name"]');
await accountNameInput.fill('Contoso Ltd');

ギャラリー内の特定の項目を検索するには、 Title1などの子コントロールのテキスト コンテンツを照合して、ギャラリー項目の一覧をフィルター処理します。

const galleryItem = canvasFrame
  .locator('[role="listitem"][data-control-part="gallery-item"]')
  .filter({
    has: canvasFrame
      .locator('[data-control-name="Title1"]')
      .getByText('Contoso Ltd', { exact: true }),
  });

await galleryItem.waitFor({ state: 'visible', timeout: 30000 });

保存後にカスタム ページを更新する

Dataverse によってサポートされるカスタム ページに新しいレコードを保存すると、完全な再読み込みをトリガーしない限り、ギャラリーは自動的に更新されません。 推奨される方法は、アプリのルートに移動して戻る方法です。

// Navigate to app root to force gallery refresh
await page.goto(MODEL_DRIVEN_APP_URL, { waitUntil: 'load', timeout: 60000 });
await page.locator('[role="menuitem"]').first().waitFor({ state: 'visible', timeout: 30000 });

// Navigate back to the custom page
const sidebarItem = page.locator(`[role="presentation"][title="${CUSTOM_PAGE_NAME}"]`).first();
await sidebarItem.waitFor({ state: 'visible', timeout: 30000 });
await sidebarItem.click();

// Wait for the new record to appear in the gallery
const specificItem = page
  .locator('[data-control-part="gallery-item"]')
  .filter({ has: page.locator('[data-control-name="Title1"]').getByText(accountName, { exact: true }) });
await specificItem.waitFor({ state: 'visible', timeout: 60000 });

完全なテスト例: レコードを作成して確認する

次の例では、ナビゲーション、フォーム入力、保存、ギャラリー検証を、アカウント レコードを作成してカスタム ページ ギャラリーに表示されることを確認する 1 つのエンド ツー エンド テストに結合します。

test('should create an account and verify it in the gallery', async ({ page }) => {
  const ACCOUNT_NAME = `Test Account ${Date.now()}`;
  const canvasFrame = page.frameLocator('iframe[name="fullscreen-app-host"]');

  // Click New record
  await page.locator('[title="New record"]').click();

  // Fill the form
  await canvasFrame.locator('input[aria-label="Account Name"]').fill(ACCOUNT_NAME);
  await canvasFrame.locator('input[aria-label="Main Phone"]').fill('555-1234');

  // Save
  await canvasFrame
    .locator('[data-control-name="IconButton_Accept1"] [role="button"]')
    .click();

  // Refresh and verify
  await page.waitForTimeout(5000); // wait for Dataverse write
  await page.goto(MODEL_DRIVEN_APP_URL, { waitUntil: 'load', timeout: 60000 });
  await page.locator('[role="menuitem"]').first().waitFor({ timeout: 30000 });
  await page.locator(`[role="presentation"][title="${CUSTOM_PAGE_NAME}"]`).first().click();

  await expect(
    page
      .locator('[data-control-part="gallery-item"]')
      .filter({ has: page.locator('[data-control-name="Title1"]').getByText(ACCOUNT_NAME, { exact: true }) })
  ).toBeVisible({ timeout: 60000 });
});

カスタム ページの認証

カスタム ページは、Dynamics 365 ドメインで実行されます。 モデル駆動型アプリのストレージ状態を使用します。

test.use({
  storageState: path.join(
    path.dirname(getStorageStatePath(process.env.MS_AUTH_EMAIL!)),
    `state-mda-${process.env.MS_AUTH_EMAIL}.json`
  ),
});

次のステップ

こちらも参照ください