Mastering Dynamics 365 UI Automation with Playwright

By | February 6, 2026

Mastering Dynamics 365 UI Automation with Playwright

Automating the Microsoft Dynamics 365 (D365) user interface is notoriously difficult. Developers often struggle with dynamic iFrames, deeply nested DOM hierarchies, and fragile XPath selectors that break after every minor CRM update. Traditional Selenium-based frameworks frequently suffer from “flakiness,” failing before completing a single stable execution.

However, Playwright has redefined the standard for D365 testing. With native auto-waiting, resilient locator strategies, and authentication state persistence. Playwright allows you to bypass repeated MFA challenges and build a scalable automation suite.

Why Use Playwright for Dynamics 365?

Before jumping into the code, here is why Playwright outperforms traditional tools for enterprise CRM environments:

  • Native Auto-Waiting: Automatically waits for elements to be actionable, eliminating sleep() or pause() commands.
  • Resilient Selectors: Uses data-id, ARIA roles, and placeholders instead of brittle XPaths.
  • Session Reuse: Log in once via MFA and reuse that state across hundreds of tests.
  • TypeScript Support: Provides strong typing for complex Dataverse entity structures.

Prerequisites

  • Node.js: Latest LTS version.
  • VS Code: The recommended IDE for Playwright.
  • Dataverse Access: A D365 environment with permissions to create/edit records.

Step 1. Initialize Your Project

Create a new folder for your project, open it in VS Code, and run below command in your terminal.

npm init playwright@latest

Step 2. Select the following options when prompted:

  • TypeScript: Yes (Strongly recommended for D365 to handle complex data types).
  • Install Browsers: Yes.
  • GitHub Actions: Select based on your CI/CD needs.
  • Create tests file: Yes.

Next, install dotenv. This is essential for security, ensuring credentials are never hardcoded in scripts.

npm install dotenv

Step 3. Configuration & Authentication strategy

The primary blocker in D365 automation is the login process. Repeated login attempts during testing can trigger MFA limits or block your account.

We will resolve this using Global Setup. We will log in once, save the browser’s “state” to a JSON file, and inject that state into every subsequent test.

 A: Configure the Environment Variables

Instead of repeating credentials, we will use a .env file. This allows greater control over the application ID and execution modes.

  1. Create a file named “.env” in your root directory.
  2. Add the following properties:

Properties

# .env

# 1. URL: Your base Dynamics 365 URL

CRM_URL=https://your-org.crm.dynamics.com/

# 2. CREDENTIALS

CRM_USERNAME=automation.user@your-org.onmicrosoft.com

CRM_PASSWORD=YourRealPasswordHere

# 3. APP ID:

APP_ID= 00000000-0000-0000-0000-000000000000

# This is crucial for D365 debugging as it makes the fast execution visible

SLOW_MO=1000

Warning: Open your. gitignore file and ensure .env is listed on a new line. Never commit this file to GitHub or any source control.

B: Create the Global Login Script

Create a new file: tests/auth.setup.ts.

This script will launch the browser, enter credentials, and pause to allow you to manually approve the MFA notification on your mobile device. Once approved, it saves the session state.

TypeScript

// tests/auth.setup.ts

import { test as setup } from '@playwright/test';

import dotenv from 'dotenv';

dotenv.config();

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {

// 1. Navigate to CRM

await page.goto(process.env.CRM_URL!);

// 2. Perform Login

await page.getByPlaceholder('Email, phone, or Skype').fill(process.env.CRM_USERNAME!);

await page.getByRole('button', { name: 'Next' }).click();

await page.getByPlaceholder('Password').fill(process.env.CRM_PASSWORD!);

await page.getByRole('button', { name: 'Sign in' }).click();

// 3. MFA & "Stay Signed In" Handling

// The script waits here for you to approve MFA on your device.

try {

await page.getByRole('button', { name: 'Yes' }).click({ timeout: 15000 });

} catch (e) {

console.log("MFA Prompt did not appear or was manually handled.");

}

// 4. Verification & Save

// Ensure we are truly inside D365 before saving the state

await page.waitForURL(/dynamics.com/);

// Save cookies and storage tokens to a JSON file

await page.context().storageState({ path: authFile });

console.log("Auth state saved to " + authFile);

});

C: Update Playwright Configuration

Now, configure playwright.config.ts to utilize the environment variables and the new authentication setup.

TypeScript

// playwright.config.ts

import { defineConfig, devices } from '@playwright/test';

import dotenv from 'dotenv';

dotenv.config();

export default defineConfig({

testDir: './tests',

fullyParallel: true,

use: {

baseURL: process.env.CRM_URL,

trace: 'on-first-retry',

// Control visibility via .env

headless: process.env.HEADLESS === 'true',

// Slow down operations for debugging visibility

launchOptions: {

slowMo: parseInt(process.env.SLOW_MO || '0'),

},

},

projects: [

// 1. Setup Project: Runs first to generate the auth file

{

name: 'setup',

testMatch: /.*\.setup\.ts/

},

// 2. Main Project: Uses the generated auth file

{

name: 'chromium',

use: {

...devices['Desktop Chrome'],

// INJECT SAVED SESSION HERE:

storageState: 'playwright/.auth/user.json',

},

},

],

});

Step 4. Test Code

In D365, lets create now one contact record using the below code:

Create tests/contact.spec.ts:

TypeScript:

import { test, expect } from '@playwright/test';

test('Create New Contact in D365', async ({ page }) => {

const orgUrl = process.env.CRM_URL;

const appId = process.env.APP_ID;

// 1. Direct Navigation

await page.goto(`${orgUrl}/main.aspx?appid=${appId}&pagetype=entitylist&etn=contact`);

// 2. Click "New"

const newButton = page.locator('button[data-id*="NewRecord"]');

await newButton.waitFor({ state: 'visible'});

await newButton.click();

// 3. Wait for Form Load

const firstNameInput = page.locator('[data-id*="firstname"] input');

await firstNameInput.waitFor({ state: 'visible'});

// 4. Fill Form

await firstNameInput.click(); // Focus first to trigger D365 JS events

await firstNameInput.fill('Doe');

// Last Name

await page.locator('[data-id*="lastname"] input').fill('John');

// Job Title

await page.locator('[data-id*="jobtitle"] input').fill('QA Engineer');

// Email (Logical name is usually 'emailaddress1')

await page.locator('[data-id*="emailaddress1"] input').fill('jd34@eng.com');

// 5. Save

await page.getByRole('menuitem', { name: /Save/i }).first().click();

// 6. Verify

// Assert that the "Saved" status appears

await expect(page.getByText('Saved')).toBeVisible({ timeout: 5000 });

});

Step 5. Execution Guide

A: Authenticate (Run once per session)

Run the setup project to handle the login.

npx playwright test –project=setup –headed

Action: When the browser opens, manually approve the MFA notification on your phone. The browser will close automatically, and user.json will be generated.

B: Run the Tests

Run your main test suite. Playwright will inject the session from Step 1, skipping the login screen entirely.

# Run all tests

npx playwright test –project=chromium –headed

# OR run a specific test file

npx playwright test tests/contact.spec.ts –project=chromium –headed

Conclusion

Playwright makes Dynamics 365 automation practical and reliable by removing the common pain points of flaky waits, unstable selectors and repeated MFA logins.

By combining environment-based configuration, saved authentication state and robust locator strategies, you can build stable end-to-end CRM tests that run consistently and are easy to maintain. This approach provides a strong foundation for scalable, enterprise-grade Dynamics 365 UI automation. There are many advanced techniques offered in built like video recording, smart upsert logic and bulk data creations.

FAQs

  1. How do I handle MFA in Dynamics 365 automation?

The most efficient way to handle MFA in Playwright is using a Global Setup routine. By logging in manually once and saving the storageState to a JSON file, Playwright can inject those cookies into all subsequent browser contexts, effectively skipping the MFA prompt for the duration of the session.

  1. Why are XPaths bad for Dynamics 365 testing?

Dynamics 365 uses a highly dynamic DOM where IDs and paths change frequently. Instead of XPaths, use Playwright locators based on data-id or ARIA roles. These are tied to the logical structure of the CRM and remain stable even if the UI layout shifts slightly.

  1. Can Playwright automate Dynamics 365 iFrames?

Yes. Unlike Selenium, which requires manual frame switching, Playwright’s FrameLocator allows you to interact with elements inside iFrames seamlessly. However, navigating directly to specific App IDs and entity types via URL (as shown in Step 5) often bypasses unnecessary frame-related complexity.

  1. How do I make Playwright tests faster in D365?

To optimize speed, use headless: true in production and reuse authentication states. Additionally, navigating directly to the entity record page via URL query parameters (e.g., &pagetype=entitylist&etn=contact) reduces the time spent clicking through the CRM site map.

Category: Dynamics 365 playwright Technical Unified Interface(UCI) Tags:

About Sam Kumar

Sam Kumar is the Vice President of Marketing at Inogic, a Microsoft Gold ISV Partner renowned for its innovative apps for Dynamics 365 CRM and Power Apps. With a rich history in Dynamics 365 and Power Platform development, Sam leads a team of certified CRM developers dedicated to pioneering cutting-edge technologies with Copilot and Azure AI the latest additions. Passionate about transforming the CRM industry, Sam’s insights and leadership drive Inogic’s mission to change the “Dynamics” of CRM.