カスタム ページは、モデル駆動型アプリ内に埋め込まれたキャンバス アプリです。 モデル駆動型アプリ シェル内の iframe でレンダリングされます。 テストするには、モデル駆動型アプリに移動し、サイトマップからカスタム ページを選択し、すべてのコントロール操作を内部 iframe にスコープする必要があります。
モデル駆動型アプリでのカスタム ページ テストのしくみ
カスタム ページが読み込まれると、モデル駆動型アプリ シェルは Dynamics 365 ドメインに残ります。 カスタムページキャンバスランタイムは内部に読み込まれます。
iframe[name="fullscreen-app-host"]
これは、スタンドアロンのキャンバス アプリで使用されるのと同じ iframe です。 フレーム ロケーターを取得すると、すべてのキャンバス アプリのテスト パターンが適用されます。
カスタム ページに移動する
-
AppProviderを使用してモデル駆動型アプリを起動します。 - サイトマップでカスタム ページ項目を選択します。
- キャンバス ランタイムが初期化されるまで待ちます。
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`
),
});