{"id":43987,"date":"2026-03-11T17:15:12","date_gmt":"2026-03-11T11:45:12","guid":{"rendered":"https:\/\/www.inogic.com\/blog\/?p=43987"},"modified":"2026-03-11T17:15:12","modified_gmt":"2026-03-11T11:45:12","slug":"mastering-dynamics-365-ui-automation-with-playwright-part-2","status":"publish","type":"post","link":"https:\/\/www.inogic.com\/blog\/2026\/03\/mastering-dynamics-365-ui-automation-with-playwright-part-2\/","title":{"rendered":"Mastering Dynamics 365 UI Automation with Playwright &#8211; Part 2"},"content":{"rendered":"<p><img decoding=\"async\" class=\"alignnone size-full wp-image-43993\" style=\"border: 1px solid #000000; padding: 1px; margin: 1px;\" src=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/03\/EXTEND-YOUR-CRM-AGENT-WITH-REST-API-TOOLS-A-PRACTICAL-GUIDE-1.png\" alt=\"Mastering Dynamics 365 UI Automation with Playwright \u2013 Part 2\" width=\"2100\" height=\"1200\" srcset=\"https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/03\/EXTEND-YOUR-CRM-AGENT-WITH-REST-API-TOOLS-A-PRACTICAL-GUIDE-1.png 2100w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/03\/EXTEND-YOUR-CRM-AGENT-WITH-REST-API-TOOLS-A-PRACTICAL-GUIDE-1-300x171.png 300w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/03\/EXTEND-YOUR-CRM-AGENT-WITH-REST-API-TOOLS-A-PRACTICAL-GUIDE-1-1024x585.png 1024w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/03\/EXTEND-YOUR-CRM-AGENT-WITH-REST-API-TOOLS-A-PRACTICAL-GUIDE-1-768x439.png 768w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/03\/EXTEND-YOUR-CRM-AGENT-WITH-REST-API-TOOLS-A-PRACTICAL-GUIDE-1-1536x878.png 1536w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/03\/EXTEND-YOUR-CRM-AGENT-WITH-REST-API-TOOLS-A-PRACTICAL-GUIDE-1-2048x1170.png 2048w, https:\/\/www.inogic.com\/blog\/wp-content\/uploads\/2026\/03\/EXTEND-YOUR-CRM-AGENT-WITH-REST-API-TOOLS-A-PRACTICAL-GUIDE-1-660x377.png 660w\" sizes=\"(max-width: 2100px) 100vw, 2100px\" \/><\/p>\n<p>In the previous <a href=\"https:\/\/www.inogic.com\/blog\/2026\/02\/mastering-dynamics-365-ui-automation-with-playwright\/\" target=\"_blank\" rel=\"noopener\">blog<\/a>, we tackled the hurdle of bypassing MFA using saved authentication states. While getting past the login screen is a victory, the real challenge in Dynamics 365 (D365) lies in the UI interaction with a few fields.<\/p>\n<p>Unlike standard web forms, D365 relies heavily on asynchronous components. If your script doesn&#8217;t respect the interaction sequence of the CRM, your automation will be brittle and prone to false negatives.<\/p>\n<p>In a recent project, we deployed a suite of 50 test cases to a CI\/CD pipeline. Locally, everything passed. But in the cloud, nearly 30% failed. The culprit wasn&#8217;t the code logic; it was the Lookup fields.<\/p>\n<p>The automation was &#8220;typing&#8221; data faster than the CRM&#8217;s background search could populate the suggestion menu. Because the script moved to the next step before the record was actually linked, the form was submitted with empty fields. This is a common pitfall: treating a complex CRM component like a simple text box. To fix this, we have to align our code with the component&#8217;s internal event triggers.<\/p>\n<h3>1. Handling Complex D365 Components<\/h3>\n<p><strong>A. The Lookup Interaction Sequence<\/strong><\/p>\n<p>A Lookup field (like Account or Contact) is a multi-step process. You must click to focus, type to trigger the search event, and then select the result from the dynamically generated list explicitly.<\/p>\n<p>Let us continue with the same example of creating a \u201cContact\u201d through Playwright. Using Lookup and Option set fields. The handling of these fields is different as whenever you click on these fields the main form focus changes to the lookup panel or option set panel.<\/p>\n<p><strong>Use Case:<\/strong><\/p>\n<ol>\n<li>Click on \u201cParent Customer\u201d lookup field<\/li>\n<li>Change the focus to lookup panel<\/li>\n<li>Select the 1<sup>st<\/sup> value from the lookup list<\/li>\n<\/ol>\n<p>TypeScript<\/p>\n<pre class=\"lang:css gutter:true start:1\">const accountInput = page.locator('[data-id*=\"parentcustomerid\"] input');\u00a0await accountInput.click();\u00a0\u00a0\u00a0\u00a0\u00a0 \u00a0\/\/ 1. Focus the elementawait accountInput.fill('Innovation'); \/\/ 2. Trigger the search event\/\/Select the specific result from the floating menuawait page.locator('li[data-id*=\"parentcustomerid\"]').first().click();<\/pre>\n<p><strong>B. Interacting with Option Sets<\/strong><\/p>\n<p>Option Sets (Dropdowns) in D365 are often &#8220;lazy-loaded.&#8221; The options aren&#8217;t available in the DOM until the parent element is clicked. A direct .selectOption() call will usually fail here because the element is technically hidden.<\/p>\n<p><strong>Use Case:<\/strong><\/p>\n<ol>\n<li>Click on \u201cPreferred Contact Method\u201d Option set field<\/li>\n<li>Change the focus to Option set panel<\/li>\n<li>Select the specific option set value.<\/li>\n<\/ol>\n<p>TypeScript<\/p>\n<pre class=\"lang:css gutter:true start:1\">\/\/ Example: Selecting Preferred Contact Methodawait page.locator('[data-id*=\"preferredcontactmethodcode\"]').click();await page.getByRole('option', { name: 'Email' }).click();<\/pre>\n<h3>2. Implementing a Video Audit Trail<\/h3>\n<p>When a test fails in a headless environment (like a GitHub Action or Azure DevOps), log files rarely tell the whole story. You need to see exactly what the UI looked like at the moment of failure.<\/p>\n<p>When I was going through the video recordings, I noticed that the same video was getting overridden by next run recorded video. Hence it was not capturing the all versions of Video that recorded only latest could be seen. To enable the feature that let Playwright store the video recording evidences in a separate file for later tracking.<\/p>\n<p>Make sure video capture node is available in Playwright \u201cConfig.ts\u201d file.<\/p>\n<p>TypeScript<\/p>\n<pre class=\"lang:css gutter:true start:1\">use: {\r\n\r\nvideo: 'on',\r\n\r\n},<\/pre>\n<p>By default, Playwright may overwrite videos or delete them after a second run. Using an afterEach hook with a unique timestamp ensures you have a permanent audit trail for every execution.<\/p>\n<p>TypeScript<\/p>\n<pre class=\"lang:css gutter:true start:1\">import fs from 'fs';\r\n\r\nimport path from 'path';\r\n\r\n\u00a0\r\n\r\ntest.afterEach(async ({ page }, testInfo) =&gt; {\r\n\r\nconst video = page.video();\r\n\r\nif (video) {\r\n\r\n\/\/ Format: Test_Name_YYYY-MM-DD.webm\r\n\r\nconst timestamp = new Date().toISOString().replace(\/[:.]\/g, '-');\r\n\r\nconst fileName = `${testInfo.title.replace(\/\\s+\/g, '_')}_${timestamp}.webm`;\r\n\r\nconst historyFolder = '.\/video-history';\r\n\r\n\u00a0\r\n\r\nif (!fs.existsSync(historyFolder)) {\r\n\r\nfs.mkdirSync(historyFolder, { recursive: true });\r\n\r\n}\r\n\u00a0\r\n\r\nconst videoPath = path.join(historyFolder, fileName);\r\n\r\nawait video.saveAs(videoPath);\r\n\r\n}\r\n\r\nawait page.close();\r\n\r\n});<\/pre>\n<p>Using the above code existing Playwright uses existing build-in \u2018fs\u2019 (File System) Node.js module it checks if folder exist then create the file. After each test it runs get the video, add timestamps for each video to save separately. After test finishes you will find file created with unique filename and video saved separately.<\/p>\n<h3><strong>Conclusion:<\/strong><\/h3>\n<p>Enterprise-grade automation isn&#8217;t about how fast you can click buttons; it&#8217;s about how reliably you can handle the state of the application. By respecting the component sequence for Lookups and maintaining a strict video history, you reduce &#8220;flaky&#8221; tests and provide clear evidence when bugs actually occur.<\/p>\n<h3>FAQs<\/h3>\n<ol>\n<li><strong> How do you automate Microsoft Dynamics 365 UI using Playwright?<\/strong><br \/>\nYou can automate Dynamics 365 UI with Playwright by interacting with CRM elements using locators, handling asynchronous components properly, and following the correct interaction sequence for complex fields such as lookups and option sets.<\/li>\n<li><strong> Why do Playwright tests fail in CI\/CD but pass locally in Dynamics 365?<\/strong><br \/>\nTests may fail in CI\/CD environments because Dynamics 365 uses asynchronous UI components. If scripts run faster than the UI loads or background processes complete, the automation may miss elements or submit incomplete data.<\/li>\n<li><strong> How do you handle Lookup fields in Dynamics 365 using Playwright?<\/strong><br \/>\nTo handle lookup fields, the script should click the field, type the search value to trigger the lookup query, and explicitly select a result from the dynamically generated suggestion list.<\/li>\n<li><strong> How can you select Option Set values in Dynamics 365 automation?<\/strong><br \/>\nOption Set values should be selected by first clicking the dropdown to load the options into the DOM and then selecting the desired option using Playwright locators.<\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>In the previous blog, we tackled the hurdle of bypassing MFA using saved authentication states. While getting past the login screen is a victory, the real challenge in Dynamics 365 (D365) lies in the UI interaction with a few fields. Unlike standard web forms, D365 relies heavily on asynchronous components. If your script doesn&#8217;t respect\u2026 <span class=\"read-more\"><a href=\"https:\/\/www.inogic.com\/blog\/2026\/03\/mastering-dynamics-365-ui-automation-with-playwright-part-2\/\">Read More &raquo;<\/a><\/span><\/p>\n","protected":false},"author":15,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[16,2361],"tags":[3319],"class_list":["post-43987","post","type-post","status-publish","format-standard","hentry","category-dynamics-365","category-technical","tag-playwright"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/posts\/43987","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/users\/15"}],"replies":[{"embeddable":true,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/comments?post=43987"}],"version-history":[{"count":0,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/posts\/43987\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/media?parent=43987"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/categories?post=43987"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.inogic.com\/blog\/wp-json\/wp\/v2\/tags?post=43987"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}