
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.
- Create a file named “.env” in your root directory.
- 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
- 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.
- 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.
- 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.
- 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.