From 28db04bfa55ff6ee17c87694614a7ef564c1257a Mon Sep 17 00:00:00 2001
From: v-raghulraja <165115074+v-raghulraja@users.noreply.github.com>
Date: Wed, 11 Jun 2025 13:48:51 +0530
Subject: [PATCH 1/5] Enable MDA Entity List Page Support in PowerApps Test
Engine
---
samples/entitylist/testPlan.fx.yaml | 118 ++++++++++
samples/entitylist/testSettings.yaml | 34 +++
.../PowerFx/Functions/SetDOBFieldsFunction.cs | 170 ++++++++++++++
.../Functions/NavigateToRecordFunction.cs | 104 +++++++++
.../SelectDepartmentOptionsFunction.cs | 43 ++++
.../SelectGridRowCheckboxFunction.cs | 55 +++++
.../PowerFx/Functions/SetDOBFieldsFunction.cs | 170 ++++++++++++++
.../PowerFx/PowerFxEngine.cs | 4 +
.../Providers/PowerFxModel/MDATypeMapping.cs | 1 +
.../TestInfra/ITestInfraFunctions.cs | 7 +
.../TestInfra/PlaywrightTestInfraFunctions.cs | 32 +++
.../DeleteRecordFunction.cs | 75 +++++++
.../ModelDrivenApplicationProvider.cs | 28 +++
.../PowerAppsTestEngineMDA.js | 8 +-
.../PowerAppsTestEngineMDAEntityList.js | 207 ++++++++++++++++++
15 files changed, 1055 insertions(+), 1 deletion(-)
create mode 100644 samples/entitylist/testPlan.fx.yaml
create mode 100644 samples/entitylist/testSettings.yaml
create mode 100644 src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetDOBFieldsFunction.cs
create mode 100644 src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/NavigateToRecordFunction.cs
create mode 100644 src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDepartmentOptionsFunction.cs
create mode 100644 src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectGridRowCheckboxFunction.cs
create mode 100644 src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SetDOBFieldsFunction.cs
create mode 100644 src/testengine.provider.mda/DeleteRecordFunction.cs
diff --git a/samples/entitylist/testPlan.fx.yaml b/samples/entitylist/testPlan.fx.yaml
new file mode 100644
index 000000000..192a9cd1b
--- /dev/null
+++ b/samples/entitylist/testPlan.fx.yaml
@@ -0,0 +1,118 @@
+testSuite:
+ testSuiteName: Employee Entity Form Automation
+ testSuiteDescription: Verifies that the Employee Entity controls work correctly.
+ persona: User1
+ appLogicalName: Entity_controls_app
+
+ testCases:
+
+ # 1. Insert Employee
+ - testCaseName: Insert Employee
+ testCaseDescription: Inserts a new employee record and asserts the table contains 4 records after insertion.
+ testSteps: |
+ Collect(
+ cr693_employee5,
+ {
+ cr693_employeename: "Ra Ra",
+ cr693_empoyeeid: "E006",
+ cr693_dob: DateTime(1991,05,15,00,0,0)
+ }
+ );
+ Assert(CountRows(cr693_employee5) = 4, "The employee table should contain 4 records after insertion");
+
+ # 2. Sort Employee IDs
+ - testCaseName: Sort Employee IDs
+ testCaseDescription: Sorts the employee list by Employee ID in descending order.
+ testSteps: |
+ Sort(cr693_employee5, cr693_empoyeeid, SortOrder.Descending);
+
+ # 3. Delete Employee with ID E006
+ - testCaseName: Delete Employee with ID E006
+ testCaseDescription: Deletes the employee record with ID 'E006' and asserts it no longer exists.
+ testSteps: |
+ Refresh(cr693_employee5);
+ Remove(
+ cr693_employee5,
+ LookUp(cr693_employee5, cr693_empoyeeid = "E007")
+ );
+ Assert(
+ IsBlank(LookUp(cr693_employee5, cr693_empoyeeid = "E007")),
+ "The record with employee ID 'E006' should no longer exist in the employee table"
+ );
+
+ # 4. Update Employee
+ - testCaseName: Update Employee
+ testCaseDescription: Updates the name of the first employee record and asserts the update.
+ testSteps: |
+ Patch(
+ cr693_employee5,
+ First(cr693_employee5),
+ {
+ cr693_employeename: "RR2"
+ }
+ );
+ Assert(First(cr693_employee5).cr693_employeename = "RR2", "The employee name should be updated to 'RR2'");
+
+ # 5. Verify Employee Record Count
+ - testCaseName: Verify Employee Record Count
+ testCaseDescription: Asserts that the employee table displays exactly 3 employee records.
+ testSteps: |
+ Assert(CountRows(cr693_employee5) = 3, "Checking if Table displays correct number of items");
+
+ # 6. Verify Employee List Filtering by Name
+ - testCaseName: Verify Employee List Filtering by Name
+ testCaseDescription: Asserts that filtering the employee list by name returns 1 record starting with 'uu'.
+ testSteps: |
+ Assert(CountRows(Filter(cr693_employee5, StartsWith(cr693_employeename, "uu"))) = 1, "The employee list should contain 1 record with a name starting with 'Test'");
+
+ # 7. Verify Employee Name Field
+ - testCaseName: Verify Employee Name Field
+ testCaseDescription: Asserts that the Employee Name for ID 'E001' is 'Alice Smith'.
+ testSteps: |
+ Assert(LookUp(cr693_employee5, cr693_empoyeeid = "E001").cr693_employeename = "RR2", "The Employee Name for ID 'E001' should be 'RR2'");
+
+ # 8. Click New Record button on the Command Bar Button
+ - testCaseName: Click New Record button on the Command Bar Button
+ testCaseDescription: Automates clicking the New Record button, entering details, and saving the new employee.
+ testSteps: |
+ Assert(NavigateToRecord("cr693_employee5", "entityrecord", 1));
+ SetProperty(cr693_empoyeeid.Text, "E007");
+ SetProperty(cr693_employeename.Text, "RR Test_Create");
+ SetDOBFields("04/09/1976", "11:30 PM");
+ SelectDepartmentOptions("IT");
+ CommandBarAction(SaveAndClose());
+ Assert(NavigateToRecord("cr693_employee5", "entitylist", 1));
+
+ # 9. Test SelectGridRowCheckbox
+ - testCaseName: Test SelectGridRowCheckbox
+ testCaseDescription: Selects the checkbox of the first grid row.
+ testSteps: |
+ SelectGridRowCheckbox(1);
+
+ # 10. Click Edit Record button on the Command Bar Button
+ - testCaseName: Click Edit Record button on the Command Bar Button
+ testCaseDescription: Automates editing the first record, updating all fields, and saving.
+ testSteps: |
+ Assert(NavigateToRecord("cr693_employee5", "entityrecord", 1));
+ SetProperty(cr693_employeename.Text, "John 20 Updated");
+ SetProperty(cr693_empoyeeid.Text, "E020 Updated");
+ SetDOBFields("04/09/1976", "12:30 PM");
+ SelectDepartmentOptions("Finance");
+ CommandBarAction(SaveAndClose());
+ Assert(NavigateToRecord("cr693_employee5", "entitylist", 1));
+
+ # 11. Delete Record
+ - testCaseName: Delete Record
+ testCaseDescription: Automates deleting the first record from the list and verifies navigation.
+ testSteps: |
+ SelectGridRowCheckbox(1);
+ Assert(NavigateToRecord("cr693_employee5", "entityrecord", 1));
+ DeleteRecord();
+ Assert(NavigateToRecord("cr693_employee5", "entitylist", 1));
+
+testSettings:
+ filePath: ./testSettings.yaml
+environmentVariables:
+ users:
+ - personaName: User1
+ emailKey: user1Email
diff --git a/samples/entitylist/testSettings.yaml b/samples/entitylist/testSettings.yaml
new file mode 100644
index 000000000..ab936e141
--- /dev/null
+++ b/samples/entitylist/testSettings.yaml
@@ -0,0 +1,34 @@
+locale: "en-US"
+headless: false
+recordVideo: true
+extensionModules:
+ enable: true
+ parameters:
+ enableDataverseFunctions: true
+ allowPowerFxNamespaces:
+ - Preview
+timeout: 1200000
+browserConfigurations:
+ - browser: Chromium
+ channel: msedge
+
+testFunctions:
+ - description: Get Identifier of save comamnd bar item
+ code: |
+ SaveForm(): Boolean = Preview.SaveForm();
+ - description: Get Identifier of New command bar item
+ code: |
+ NewRecord(): Text = "New";
+ - description: Get Identifier of save and close command bar item
+ code: |
+ SaveAndClose(): Text = "Save & Close";
+ - description: Save and close the form using Document Object Model (DOM) selector for command bar
+ code: |
+ CommandBarAction(name: Text): Void =
+ Preview.PlaywrightAction(Concatenate("//*[@aria-label='", name, "']"), "click");
+
+ - description: Delete the current record using Power Fx control from MDA
+ code: |
+ DeleteRecord(): Boolean = Preview.DeleteRecord();
+
+
diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetDOBFieldsFunction.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetDOBFieldsFunction.cs
new file mode 100644
index 000000000..42b629705
--- /dev/null
+++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetDOBFieldsFunction.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.PowerApps.TestEngine.Providers;
+using Microsoft.PowerApps.TestEngine.TestInfra;
+using Microsoft.PowerFx;
+using Microsoft.PowerFx.Types;
+using Microsoft.Xrm.Sdk;
+
+namespace Microsoft.PowerApps.TestEngine.PowerFx.Functions
+{
+ ///
+ /// Sets the Date of Birth and Time of Birth fields using aria-label selectors.
+ ///
+ public class SetDOBFieldsFunction : ReflectionFunction
+ {
+ private readonly ITestWebProvider _testWebProvider;
+ private readonly ILogger _logger;
+
+ public SetDOBFieldsFunction(ITestWebProvider testWebProvider, ILogger logger)
+ : base("SetDOBFields", FormulaType.Boolean, FormulaType.String, FormulaType.String)
+ {
+ _testWebProvider = testWebProvider;
+ _logger = logger;
+ }
+
+ public BooleanValue Execute(StringValue dateValue, StringValue timeValue)
+ {
+ return ExecuteAsync(dateValue, timeValue).Result;
+ }
+
+ public async Task ExecuteAsync(StringValue dateValue, StringValue timeValue)
+ {
+ _logger.LogInformation($"Executing SetDOBFieldsFunction with provided values: date={dateValue.Value}, time={timeValue.Value}");
+
+ // Default time to 08:00 AM if not provided
+ var time = string.IsNullOrWhiteSpace(timeValue.Value) ? "08:00 AM" : timeValue.Value;
+
+ var js = $@"
+ (async function() {{
+ function waitForElement(selector, timeout = 5000) {{
+ return new Promise((resolve, reject) => {{
+ const interval = 100;
+ let elapsed = 0;
+ const check = () => {{
+ const el = document.querySelector(selector);
+ if (el) return resolve(el);
+ elapsed += interval;
+ if (elapsed >= timeout) return reject('Timeout: ' + selector);
+ setTimeout(check, interval);
+ }};
+ check();
+ }});
+ }}
+
+ const dateStr = '{dateValue.Value}';
+ const timeStr = '{time.Replace("'", "\\'")}';
+ console.log('SetDOBFieldsFunction JS: dateStr=', dateStr, 'timeStr=', timeStr);
+ const [month, day, year] = dateStr.split('/').map(part => parseInt(part, 10));
+ const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
+ 'July', 'August', 'September', 'October', 'November', 'December'];
+ const monthName = monthNames[month - 1];
+
+ try {{
+ const dateInput = await waitForElement(""[aria-label='Date of DOB']"");
+ const calendarIcon = dateInput.parentElement.querySelector('svg');
+ if (!calendarIcon) {{
+ console.log('Calendar icon not found.');
+ return;
+ }}
+
+ // Step 1: Open calendar
+ calendarIcon.dispatchEvent(new MouseEvent('click', {{ bubbles: true }}));
+ console.log('Calendar opened.');
+
+ // Step 2: Switch to year picker
+ const yearSwitchBtn = await waitForElement(""button[aria-label*='select to switch to year picker']"");
+ yearSwitchBtn.click();
+ console.log('Switched to year picker.');
+
+ // Step 3: Navigate to correct year
+ async function selectYear() {{
+ let yearBtn = Array.from(document.querySelectorAll('button'))
+ .find(btn => btn.textContent.trim() === String(year));
+ let attempts = 0;
+ while (!yearBtn && attempts < 15) {{
+ const prevYearBtn = document.querySelector(""button[title*='Navigate to previous year']"");
+ if (prevYearBtn) {{
+ prevYearBtn.click();
+ await new Promise(r => setTimeout(r, 200));
+ }}
+ yearBtn = Array.from(document.querySelectorAll('button'))
+ .find(btn => btn.textContent.trim() === String(year));
+ attempts++;
+ }}
+ if (yearBtn) {{
+ yearBtn.click();
+ console.log(`Year ${{year}} selected.`);
+ }} else {{
+ console.warn('Year button not found.');
+ return false;
+ }}
+ return true;
+ }}
+ if (!await selectYear()) return;
+
+ // Step 4: Select month
+ const monthBtn = Array.from(document.querySelectorAll(""button[role='gridcell']""))
+ .find(btn => btn.textContent.trim().toLowerCase().startsWith(monthName.slice(0, 3).toLowerCase()));
+ if (monthBtn) {{
+ monthBtn.click();
+ console.log(`Month ${{monthName}} selected.`);
+ }} else {{
+ console.warn(`Month ${{monthName}} not found.`);
+ return;
+ }}
+
+ // Step 5: Select day
+ await new Promise(r => setTimeout(r, 400));
+ const dayBtn = Array.from(document.querySelectorAll(""td button[aria-label]""))
+ .find(btn => btn.getAttribute('aria-label')?.includes(`${{day}}, ${{monthName}}, ${{year}}`));
+ if (dayBtn) {{
+ dayBtn.click();
+ console.log(`Day ${{day}} selected.`);
+ }} else {{
+ console.warn(`Day ${{day}} not found.`);
+ return;
+ }}
+
+ // Step 6: Set time
+ await new Promise(r => setTimeout(r, 600));
+ const timeInput = await waitForElement(""[aria-label='Time of DOB']"");
+ timeInput.focus();
+ timeInput.value = timeStr;
+ timeInput.setAttribute('value', timeStr);
+ [
+ 'focus', 'keydown', 'keypress', 'input', 'keyup', 'change', 'blur', 'focusout', 'click', 'paste',
+ 'compositionstart', 'compositionupdate', 'compositionend'
+ ].forEach(eventType => {{
+ timeInput.dispatchEvent(new Event(eventType, {{ bubbles: true }}));
+ }});
+ setTimeout(() => {{
+ if (timeInput.value !== timeStr) {{
+ timeInput.value = timeStr;
+ timeInput.setAttribute('value', timeStr);
+ [
+ 'focus', 'keydown', 'keypress', 'input', 'keyup', 'change', 'blur', 'focusout', 'click', 'paste',
+ 'compositionstart', 'compositionupdate', 'compositionend'
+ ].forEach(eventType => {{
+ timeInput.dispatchEvent(new Event(eventType, {{ bubbles: true }}));
+ }});
+ console.log('Retried setting time to:', timeStr);
+ }}
+ }}, 300);
+ console.log(`Time set to ${{timeStr}}.`);
+ }} catch (e) {{
+ console.warn('SetDOBFieldsFunction error:', e);
+ }}
+ }})();
+ ";
+
+ var page = _testWebProvider.TestInfraFunctions.GetContext().Pages.First();
+ await page.EvaluateAsync(js);
+
+ _logger.LogInformation("SetDOBFieldsFunction execution completed.");
+ return FormulaValue.New(true);
+ }
+ }
+}
diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/NavigateToRecordFunction.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/NavigateToRecordFunction.cs
new file mode 100644
index 000000000..afb25f377
--- /dev/null
+++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/NavigateToRecordFunction.cs
@@ -0,0 +1,104 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.PowerApps.TestEngine.Providers;
+using Microsoft.PowerApps.TestEngine.TestInfra;
+using Microsoft.PowerFx;
+using Microsoft.PowerFx.Core.Utils;
+using Microsoft.PowerFx.Types;
+using Newtonsoft.Json.Linq;
+
+
+
+namespace Microsoft.PowerApps.TestEngine.PowerFx.Functions
+{
+ ///
+ /// Navigates to the selected entity record in the grid, or to the new record page if none selected.
+ ///
+ public class NavigateToRecordFunction : ReflectionFunction
+ {
+ private readonly ITestWebProvider _testWebProvider;
+ private readonly Func _updateModelFunction;
+ private readonly ILogger _logger;
+
+
+ public NavigateToRecordFunction(ITestWebProvider testWebProvider, Func updateModelFunction, ILogger logger)
+ : base("NavigateToRecord", FormulaType.Boolean, FormulaType.String, FormulaType.String, FormulaType.Number)
+ {
+ _testWebProvider = testWebProvider;
+ _updateModelFunction = updateModelFunction;
+ _logger = logger;
+ }
+
+ public BooleanValue Execute(
+ StringValue entityName,
+ StringValue entityPage,
+ NumberValue target)
+ {
+ return ExecuteAsync(entityName, entityPage, target).Result;
+ }
+
+ public async Task ExecuteAsync(
+ StringValue entityName,
+ StringValue entityPage,
+ NumberValue target)
+ {
+ _logger.LogInformation("Executing NavigateToRecordFunction: extracting selected entityId from grid.");
+
+ // Extract the selected entityId from the grid
+ var jsExtractId = @"
+ (function() {
+ const selectedRows = document.querySelectorAll(""div[role='row'][aria-label*='deselect']"");
+ for (let row of selectedRows) {
+ const link = row.querySelector(""a[aria-label][href*='etn=" + entityName.Value + @"']"");
+ if (link) {
+ const url = new URL(link.href, window.location.origin);
+ const entityId = url.searchParams.get('id');
+ if (entityId) {
+ return entityId;
+ }
+ }
+ }
+ return '';
+ })();
+ ";
+
+ var page = _testWebProvider.TestInfraFunctions.GetContext().Pages.First();
+ var entityId = await page.EvaluateAsync(jsExtractId);
+
+ var pageInput = new JObject
+ {
+ ["pageType"] = entityPage.Value,
+ ["entityName"] = entityName.Value
+ };
+
+ if (!string.IsNullOrEmpty(entityId))
+ {
+ pageInput["entityId"] = entityId;
+ _logger.LogInformation($"Navigating to existing record with entityId: {entityId}");
+ }
+ else
+ {
+ _logger.LogInformation("No selected entity found. Navigating to new record page.");
+ }
+
+ var navigationOptions = new JObject
+ {
+ ["target"] = (int)target.Value
+ };
+
+ var jsNavigate = $@"
+ Xrm.Navigation.navigateTo({pageInput}, {navigationOptions})
+ .then(function() {{ return true; }}, function(error) {{ return false; }});
+ ";
+
+ var navResult = await page.EvaluateAsync(jsNavigate);
+ _logger.LogInformation($"Navigation result: {navResult}");
+
+ // Ensure Power Fx model is updated after navigation
+ await _updateModelFunction();
+
+ return FormulaValue.New(navResult);
+ }
+ }
+}
diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDepartmentOptionsFunction.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDepartmentOptionsFunction.cs
new file mode 100644
index 000000000..6be01e30c
--- /dev/null
+++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDepartmentOptionsFunction.cs
@@ -0,0 +1,43 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Playwright;
+using Microsoft.PowerApps.TestEngine.Providers;
+using Microsoft.PowerApps.TestEngine.TestInfra;
+using Microsoft.PowerFx;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.PowerApps.TestEngine.PowerFx.Functions
+{
+ ///
+ /// Power Fx function to select a department option in the Department dropdown.
+ ///
+ public class SelectDepartmentOptionsFunction : ReflectionFunction
+ {
+ private readonly ITestWebProvider _testWebProvider;
+ private readonly ILogger _logger;
+
+ public SelectDepartmentOptionsFunction(ITestWebProvider testWebProvider, ILogger logger)
+ : base("SelectDepartmentOptions", FormulaType.Boolean, FormulaType.String)
+ {
+ _testWebProvider = testWebProvider;
+ _logger = logger;
+ }
+
+ public BooleanValue Execute(StringValue department)
+ {
+ return ExecuteAsync(department).Result;
+ }
+
+ ///
+ /// Asynchronously selects a department option in the Department dropdown.
+ ///
+ public async Task ExecuteAsync(StringValue department)
+ {
+ _logger.LogInformation($"Executing SelectDepartmentOptionsFunction for department '{department.Value}'.");
+ await _testWebProvider.TestInfraFunctions.SelectDepartmentOptionsAsync(department.Value);
+ _logger.LogInformation("SelectDepartmentOptionsFunction execution completed.");
+ return FormulaValue.New(true);
+ }
+ }
+}
diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectGridRowCheckboxFunction.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectGridRowCheckboxFunction.cs
new file mode 100644
index 000000000..90139229c
--- /dev/null
+++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectGridRowCheckboxFunction.cs
@@ -0,0 +1,55 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.PowerApps.TestEngine.Providers;
+using Microsoft.PowerApps.TestEngine.TestInfra;
+using Microsoft.PowerFx;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.PowerApps.TestEngine.PowerFx.Functions
+{
+ ///
+ /// Selects or deselects a checkbox in a grid row based on the row index.
+ ///
+ public class SelectGridRowCheckboxFunction : ReflectionFunction
+ {
+ private readonly ITestWebProvider _testWebProvider;
+ private readonly ILogger _logger;
+
+ public SelectGridRowCheckboxFunction(ITestWebProvider testWebProvider, ILogger logger)
+ : base("SelectGridRowCheckbox", FormulaType.Boolean, FormulaType.Number)
+ {
+ _testWebProvider = testWebProvider;
+ _logger = logger;
+ }
+
+ public BooleanValue Execute(NumberValue rowIndex)
+ {
+ return ExecuteAsync(rowIndex).Result;
+ }
+
+ public async Task ExecuteAsync(NumberValue rowIndex)
+ {
+ _logger.LogInformation($"Executing SelectGridRowCheckboxFunction for row index {rowIndex.Value}.");
+
+ var js = $@"
+ (function() {{
+ var checkboxes = document.querySelectorAll(""input[type='checkbox'][aria-label='select or deselect the row']"");
+ var idx = {rowIndex.Value} - 1;
+ if (idx >= 0 && checkboxes.length > idx) {{
+ checkboxes[idx].click();
+ console.log('Checkbox in row ' + (idx + 1) + ' clicked.');
+ }} else {{
+ console.log('Row index ' + idx + ' is out of bounds. Only ' + checkboxes.length + ' checkbox(es) found.');
+ }}
+ }})();
+ ";
+
+ var page = _testWebProvider.TestInfraFunctions.GetContext().Pages.First();
+ await page.EvaluateAsync(js);
+
+ _logger.LogInformation("SelectGridRowCheckboxFunction execution completed.");
+ return FormulaValue.New(true);
+ }
+ }
+}
diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SetDOBFieldsFunction.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SetDOBFieldsFunction.cs
new file mode 100644
index 000000000..42b629705
--- /dev/null
+++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SetDOBFieldsFunction.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.PowerApps.TestEngine.Providers;
+using Microsoft.PowerApps.TestEngine.TestInfra;
+using Microsoft.PowerFx;
+using Microsoft.PowerFx.Types;
+using Microsoft.Xrm.Sdk;
+
+namespace Microsoft.PowerApps.TestEngine.PowerFx.Functions
+{
+ ///
+ /// Sets the Date of Birth and Time of Birth fields using aria-label selectors.
+ ///
+ public class SetDOBFieldsFunction : ReflectionFunction
+ {
+ private readonly ITestWebProvider _testWebProvider;
+ private readonly ILogger _logger;
+
+ public SetDOBFieldsFunction(ITestWebProvider testWebProvider, ILogger logger)
+ : base("SetDOBFields", FormulaType.Boolean, FormulaType.String, FormulaType.String)
+ {
+ _testWebProvider = testWebProvider;
+ _logger = logger;
+ }
+
+ public BooleanValue Execute(StringValue dateValue, StringValue timeValue)
+ {
+ return ExecuteAsync(dateValue, timeValue).Result;
+ }
+
+ public async Task ExecuteAsync(StringValue dateValue, StringValue timeValue)
+ {
+ _logger.LogInformation($"Executing SetDOBFieldsFunction with provided values: date={dateValue.Value}, time={timeValue.Value}");
+
+ // Default time to 08:00 AM if not provided
+ var time = string.IsNullOrWhiteSpace(timeValue.Value) ? "08:00 AM" : timeValue.Value;
+
+ var js = $@"
+ (async function() {{
+ function waitForElement(selector, timeout = 5000) {{
+ return new Promise((resolve, reject) => {{
+ const interval = 100;
+ let elapsed = 0;
+ const check = () => {{
+ const el = document.querySelector(selector);
+ if (el) return resolve(el);
+ elapsed += interval;
+ if (elapsed >= timeout) return reject('Timeout: ' + selector);
+ setTimeout(check, interval);
+ }};
+ check();
+ }});
+ }}
+
+ const dateStr = '{dateValue.Value}';
+ const timeStr = '{time.Replace("'", "\\'")}';
+ console.log('SetDOBFieldsFunction JS: dateStr=', dateStr, 'timeStr=', timeStr);
+ const [month, day, year] = dateStr.split('/').map(part => parseInt(part, 10));
+ const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
+ 'July', 'August', 'September', 'October', 'November', 'December'];
+ const monthName = monthNames[month - 1];
+
+ try {{
+ const dateInput = await waitForElement(""[aria-label='Date of DOB']"");
+ const calendarIcon = dateInput.parentElement.querySelector('svg');
+ if (!calendarIcon) {{
+ console.log('Calendar icon not found.');
+ return;
+ }}
+
+ // Step 1: Open calendar
+ calendarIcon.dispatchEvent(new MouseEvent('click', {{ bubbles: true }}));
+ console.log('Calendar opened.');
+
+ // Step 2: Switch to year picker
+ const yearSwitchBtn = await waitForElement(""button[aria-label*='select to switch to year picker']"");
+ yearSwitchBtn.click();
+ console.log('Switched to year picker.');
+
+ // Step 3: Navigate to correct year
+ async function selectYear() {{
+ let yearBtn = Array.from(document.querySelectorAll('button'))
+ .find(btn => btn.textContent.trim() === String(year));
+ let attempts = 0;
+ while (!yearBtn && attempts < 15) {{
+ const prevYearBtn = document.querySelector(""button[title*='Navigate to previous year']"");
+ if (prevYearBtn) {{
+ prevYearBtn.click();
+ await new Promise(r => setTimeout(r, 200));
+ }}
+ yearBtn = Array.from(document.querySelectorAll('button'))
+ .find(btn => btn.textContent.trim() === String(year));
+ attempts++;
+ }}
+ if (yearBtn) {{
+ yearBtn.click();
+ console.log(`Year ${{year}} selected.`);
+ }} else {{
+ console.warn('Year button not found.');
+ return false;
+ }}
+ return true;
+ }}
+ if (!await selectYear()) return;
+
+ // Step 4: Select month
+ const monthBtn = Array.from(document.querySelectorAll(""button[role='gridcell']""))
+ .find(btn => btn.textContent.trim().toLowerCase().startsWith(monthName.slice(0, 3).toLowerCase()));
+ if (monthBtn) {{
+ monthBtn.click();
+ console.log(`Month ${{monthName}} selected.`);
+ }} else {{
+ console.warn(`Month ${{monthName}} not found.`);
+ return;
+ }}
+
+ // Step 5: Select day
+ await new Promise(r => setTimeout(r, 400));
+ const dayBtn = Array.from(document.querySelectorAll(""td button[aria-label]""))
+ .find(btn => btn.getAttribute('aria-label')?.includes(`${{day}}, ${{monthName}}, ${{year}}`));
+ if (dayBtn) {{
+ dayBtn.click();
+ console.log(`Day ${{day}} selected.`);
+ }} else {{
+ console.warn(`Day ${{day}} not found.`);
+ return;
+ }}
+
+ // Step 6: Set time
+ await new Promise(r => setTimeout(r, 600));
+ const timeInput = await waitForElement(""[aria-label='Time of DOB']"");
+ timeInput.focus();
+ timeInput.value = timeStr;
+ timeInput.setAttribute('value', timeStr);
+ [
+ 'focus', 'keydown', 'keypress', 'input', 'keyup', 'change', 'blur', 'focusout', 'click', 'paste',
+ 'compositionstart', 'compositionupdate', 'compositionend'
+ ].forEach(eventType => {{
+ timeInput.dispatchEvent(new Event(eventType, {{ bubbles: true }}));
+ }});
+ setTimeout(() => {{
+ if (timeInput.value !== timeStr) {{
+ timeInput.value = timeStr;
+ timeInput.setAttribute('value', timeStr);
+ [
+ 'focus', 'keydown', 'keypress', 'input', 'keyup', 'change', 'blur', 'focusout', 'click', 'paste',
+ 'compositionstart', 'compositionupdate', 'compositionend'
+ ].forEach(eventType => {{
+ timeInput.dispatchEvent(new Event(eventType, {{ bubbles: true }}));
+ }});
+ console.log('Retried setting time to:', timeStr);
+ }}
+ }}, 300);
+ console.log(`Time set to ${{timeStr}}.`);
+ }} catch (e) {{
+ console.warn('SetDOBFieldsFunction error:', e);
+ }}
+ }})();
+ ";
+
+ var page = _testWebProvider.TestInfraFunctions.GetContext().Pages.First();
+ await page.EvaluateAsync(js);
+
+ _logger.LogInformation("SetDOBFieldsFunction execution completed.");
+ return FormulaValue.New(true);
+ }
+ }
+}
diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs
index dc6e97270..988af4c4f 100644
--- a/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs
+++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs
@@ -102,6 +102,10 @@ public void Setup(TestSettings settings)
powerFxConfig.AddFunction(new AssertNotErrorFunction(Logger));
powerFxConfig.AddFunction(new SetPropertyFunction(_testWebProvider, Logger));
powerFxConfig.AddFunction(new IsMatchFunction(Logger));
+ powerFxConfig.AddFunction(new NavigateToRecordFunction(_testWebProvider, async () => await UpdatePowerFxModelAsync(), Logger));
+ powerFxConfig.AddFunction(new SetDOBFieldsFunction(_testWebProvider, Logger));
+ powerFxConfig.AddFunction(new SelectGridRowCheckboxFunction(_testWebProvider, Logger));
+ powerFxConfig.AddFunction(new SelectDepartmentOptionsFunction(_testWebProvider, Logger));
if (settings != null && settings.ExtensionModules != null && settings.ExtensionModules.Enable)
{
diff --git a/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/MDATypeMapping.cs b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/MDATypeMapping.cs
index 27418f271..e5dbc4b03 100644
--- a/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/MDATypeMapping.cs
+++ b/src/Microsoft.PowerApps.TestEngine/Providers/PowerFxModel/MDATypeMapping.cs
@@ -33,6 +33,7 @@ public MDATypeMapping()
typeMappings.Add("m", FormulaType.Decimal);
typeMappings.Add("v", FormulaType.UntypedObject);
typeMappings.Add("i", FormulaType.String);
+ typeMappings.Add("p", FormulaType.Unknown);
}
///
diff --git a/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs b/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs
index 8aa246559..cf603543a 100644
--- a/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs
+++ b/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs
@@ -107,5 +107,12 @@ public interface ITestInfraFunctions
/// The physical file path for image file
///
public Task TriggerControlClickEvent(string controlName, string filePath);
+
+ ///
+ /// Selects a department option in the user interface.
+ ///
+ /// The department value to select.
+ /// True if the department option was successfully selected; otherwise, false.
+ public Task SelectDepartmentOptionsAsync(string value);
}
}
diff --git a/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs b/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs
index 5ada989e8..b62d1216f 100644
--- a/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs
+++ b/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs
@@ -513,5 +513,37 @@ public async Task TriggerControlClickEvent(string controlName, string file
}
return false;
}
+
+ ///
+ /// Selects a single department option in the Department dropdown by its name.
+ /// Only one department can be selected at a time.
+ ///
+ /// The name of the department to select.
+ /// True if the option was successfully selected, otherwise false.
+ public async Task SelectDepartmentOptionsAsync(string departmentName)
+ {
+ if (string.IsNullOrEmpty(departmentName))
+ {
+ _singleTestInstanceState.GetLogger().LogError("Department name cannot be null or empty.");
+ throw new ArgumentException("Department name must be provided.", nameof(departmentName));
+ }
+
+ try
+ {
+ ValidatePage();
+
+ // Open the Department dropdown
+ await Page.GetByLabel("Department", new() { Exact = true }).ClickAsync();
+ // Select the specified department
+ await Page.GetByRole(AriaRole.Option, new() { Name = departmentName }).ClickAsync();
+
+ return true; // Indicate success
+ }
+ catch (Exception ex)
+ {
+ _singleTestInstanceState.GetLogger().LogError($"Error occurred while selecting Department option: {departmentName}. Exception: {ex.Message}");
+ return false; // Indicate failure
+ }
+ }
}
}
diff --git a/src/testengine.provider.mda/DeleteRecordFunction.cs b/src/testengine.provider.mda/DeleteRecordFunction.cs
new file mode 100644
index 000000000..1fd3b1d9b
--- /dev/null
+++ b/src/testengine.provider.mda/DeleteRecordFunction.cs
@@ -0,0 +1,75 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.PowerApps.TestEngine.Config;
+using Microsoft.PowerApps.TestEngine.Helpers;
+using Microsoft.PowerApps.TestEngine.TestInfra;
+using Microsoft.PowerFx;
+using Microsoft.PowerFx.Core.Utils;
+using Microsoft.PowerFx.Types;
+using System.Threading.Tasks;
+
+namespace testengine.provider.mda
+{
+ ///
+ /// This will wait for the current record to be deleted.
+ ///
+ public class DeleteRecordFunction : ReflectionFunction
+ {
+ private readonly ITestInfraFunctions _testInfraFunctions;
+ private readonly ITestState? _testState;
+ private readonly ILogger _logger;
+
+ public DeleteRecordFunction(ITestInfraFunctions testInfraFunctions, ITestState? testState, ILogger logger)
+ : base(DPath.Root.Append(new DName("Preview")), "DeleteRecord", BooleanType.Boolean)
+ {
+ _testInfraFunctions = testInfraFunctions;
+ _testState = testState;
+ _logger = logger;
+ }
+
+ ///
+ /// Attempt to delete the current record
+ ///
+ /// True if record successfully deleted.
+ public BooleanValue Execute()
+ {
+ _logger.LogInformation("Starting Delete Record");
+ return ExecuteAsync().Result;
+ }
+
+ public async Task ExecuteAsync()
+ {
+
+ await _testInfraFunctions.RunJavascriptAsync(
+ @"window.deleteCompleted = null;
+ var entityName = Xrm.Page.data.entity.getEntityName && Xrm.Page.data.entity.getEntityName();
+ var entityId = Xrm.Page.data.entity.getId && Xrm.Page.data.entity.getId();
+ if (entityName && entityId) {
+ Xrm.WebApi.deleteRecord(entityName, entityId.replace(/[{}]/g, ''))
+ .then(function() { window.deleteCompleted = true; })
+ .catch(function() { window.deleteCompleted = false; });
+ } else {
+ window.deleteCompleted = false;
+ }"
+ );
+
+
+ var getValue = () => _testInfraFunctions.RunJavascriptAsync
public async Task ExecuteAsync(StringValue department)
{
+ if (department == null)
+ throw new ArgumentNullException(nameof(department));
+
_logger.LogInformation($"Executing SelectDepartmentOptionsFunction for department '{department.Value}'.");
await _testWebProvider.TestInfraFunctions.SelectDepartmentOptionsAsync(department.Value);
_logger.LogInformation("SelectDepartmentOptionsFunction execution completed.");
From af87034f18e8991e1bdbac95b2487f0b8ba46acb Mon Sep 17 00:00:00 2001
From: v-raghulraja <165115074+v-raghulraja@users.noreply.github.com>
Date: Fri, 18 Jul 2025 22:37:00 +0530
Subject: [PATCH 5/5] Changed the SelectDepartment name to SelectDropdown
---
samples/entitylist/testPlan.fx.yaml | 4 +-
...s => SelectDropdownOptionFunctionTests.cs} | 84 +++++++++----------
...ion.cs => SelectDropdownOptionFunction.cs} | 22 ++---
.../PowerFx/PowerFxEngine.cs | 2 +-
.../TestInfra/ITestInfraFunctions.cs | 8 +-
.../TestInfra/PlaywrightTestInfraFunctions.cs | 23 +++--
6 files changed, 71 insertions(+), 72 deletions(-)
rename src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/{SelectDepartmentOptionsFunctionTests.cs => SelectDropdownOptionFunctionTests.cs} (59%)
rename src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/{SelectDepartmentOptionsFunction.cs => SelectDropdownOptionFunction.cs} (51%)
diff --git a/samples/entitylist/testPlan.fx.yaml b/samples/entitylist/testPlan.fx.yaml
index 192a9cd1b..5faf15dfb 100644
--- a/samples/entitylist/testPlan.fx.yaml
+++ b/samples/entitylist/testPlan.fx.yaml
@@ -79,7 +79,7 @@ testSuite:
SetProperty(cr693_empoyeeid.Text, "E007");
SetProperty(cr693_employeename.Text, "RR Test_Create");
SetDOBFields("04/09/1976", "11:30 PM");
- SelectDepartmentOptions("IT");
+ SelectDropdownOption("IT");
CommandBarAction(SaveAndClose());
Assert(NavigateToRecord("cr693_employee5", "entitylist", 1));
@@ -97,7 +97,7 @@ testSuite:
SetProperty(cr693_employeename.Text, "John 20 Updated");
SetProperty(cr693_empoyeeid.Text, "E020 Updated");
SetDOBFields("04/09/1976", "12:30 PM");
- SelectDepartmentOptions("Finance");
+ SelectDropdownOption("Finance");
CommandBarAction(SaveAndClose());
Assert(NavigateToRecord("cr693_employee5", "entitylist", 1));
diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectDepartmentOptionsFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectDropdownOptionFunctionTests.cs
similarity index 59%
rename from src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectDepartmentOptionsFunctionTests.cs
rename to src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectDropdownOptionFunctionTests.cs
index c04caac6f..6003d99f1 100644
--- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectDepartmentOptionsFunctionTests.cs
+++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectDropdownOptionFunctionTests.cs
@@ -10,10 +10,10 @@
namespace Microsoft.PowerApps.TestEngine.Tests.PowerFx.Functions
{
- public class SelectDepartmentOptionsFunctionTests
+ public class SelectDropdownOptionFunctionTests
{
[Fact]
- public async Task ExecuteAsync_ValidDepartment_CallsSelectDepartmentOptionsAsyncAndLogs()
+ public async Task ExecuteAsync_ValidDropdown_CallsOptionAsyncAndLogs()
{
// Arrange
var mockWebProvider = new Mock();
@@ -21,23 +21,23 @@ public async Task ExecuteAsync_ValidDepartment_CallsSelectDepartmentOptionsAsync
var mockLogger = new Mock();
mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object);
- mockTestInfra.Setup(x => x.SelectDepartmentOptionsAsync(It.IsAny()))
+ mockTestInfra.Setup(x => x.SelectDropdownOptionAsync(It.IsAny()))
.Returns(Task.FromResult(true));
- var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object);
- var department = StringValue.New("HR");
+ var func = new SelectDropdownOptionFunction(mockWebProvider.Object, mockLogger.Object);
+ var dropdownOption = StringValue.New("HR");
// Act
- var result = await func.ExecuteAsync(department);
+ var result = await func.ExecuteAsync(dropdownOption);
// Assert
Assert.True(result.Value);
- mockTestInfra.Verify(x => x.SelectDepartmentOptionsAsync("HR"), Times.Once);
+ mockTestInfra.Verify(x => x.SelectDropdownOptionAsync("HR"), Times.Once);
mockLogger.Verify(
l => l.Log(
LogLevel.Information,
It.IsAny(),
- It.Is((v, t) => v.ToString().Contains("Executing SelectDepartmentOptionsFunction for department 'HR'.")),
+ It.Is((v, t) => v.ToString().Contains("Executing SelectDropdownOptionFunction for dropdown 'HR'.")),
null,
It.IsAny>()),
Times.Once);
@@ -45,7 +45,7 @@ public async Task ExecuteAsync_ValidDepartment_CallsSelectDepartmentOptionsAsync
l => l.Log(
LogLevel.Information,
It.IsAny(),
- It.Is((v, t) => v.ToString().Contains("SelectDepartmentOptionsFunction execution completed.")),
+ It.Is((v, t) => v.ToString().Contains("SelectDropdownOptionFunction execution completed.")),
null,
It.IsAny>()),
Times.Once);
@@ -60,18 +60,18 @@ public void Execute_CallsAsyncSynchronously()
var mockLogger = new Mock();
mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object);
- mockTestInfra.Setup(x => x.SelectDepartmentOptionsAsync(It.IsAny()))
+ mockTestInfra.Setup(x => x.SelectDropdownOptionAsync(It.IsAny()))
.Returns(Task.FromResult(true));
- var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object);
- var department = StringValue.New("Finance");
+ var func = new SelectDropdownOptionFunction(mockWebProvider.Object, mockLogger.Object);
+ var dropdownOption = StringValue.New("Finance");
// Act
- var result = func.Execute(department);
+ var result = func.Execute(dropdownOption);
// Assert
Assert.True(result.Value);
- mockTestInfra.Verify(x => x.SelectDepartmentOptionsAsync("Finance"), Times.Once);
+ mockTestInfra.Verify(x => x.SelectDropdownOptionAsync("Finance"), Times.Once);
}
[Fact]
@@ -82,15 +82,15 @@ public async Task ExecuteAsync_NullTestInfraFunctions_ThrowsNullReferenceExcepti
var mockLogger = new Mock();
mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns((ITestInfraFunctions)null);
- var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object);
- var department = StringValue.New("IT");
+ var func = new SelectDropdownOptionFunction(mockWebProvider.Object, mockLogger.Object);
+ var dropdownOption = StringValue.New("IT");
// Act & Assert
- await Assert.ThrowsAsync(() => func.ExecuteAsync(department));
+ await Assert.ThrowsAsync(() => func.ExecuteAsync(dropdownOption));
}
[Fact]
- public async Task ExecuteAsync_EmptyDepartment_CallsSelectDepartmentOptionsAsyncWithEmptyString()
+ public async Task ExecuteAsync_EmptyDropdown_CallsSelectDropdownOptionAsyncWithEmptyString()
{
// Arrange
var mockWebProvider = new Mock();
@@ -98,22 +98,22 @@ public async Task ExecuteAsync_EmptyDepartment_CallsSelectDepartmentOptionsAsync
var mockLogger = new Mock();
mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object);
- mockTestInfra.Setup(x => x.SelectDepartmentOptionsAsync(It.IsAny()))
+ mockTestInfra.Setup(x => x.SelectDropdownOptionAsync(It.IsAny()))
.Returns(Task.FromResult(true));
- var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object);
- var department = StringValue.New(string.Empty);
+ var func = new SelectDropdownOptionFunction(mockWebProvider.Object, mockLogger.Object);
+ var dropdownOption = StringValue.New(string.Empty);
// Act
- var result = await func.ExecuteAsync(department);
+ var result = await func.ExecuteAsync(dropdownOption);
// Assert
Assert.True(result.Value);
- mockTestInfra.Verify(x => x.SelectDepartmentOptionsAsync(string.Empty), Times.Once);
+ mockTestInfra.Verify(x => x.SelectDropdownOptionAsync(string.Empty), Times.Once);
}
[Fact]
- public async Task ExecuteAsync_WhitespaceDepartment_CallsSelectDepartmentOptionsAsyncWithWhitespace()
+ public async Task ExecuteAsync_WhitespaceDropdown_CallsSelectDropdownOptionAsyncWithWhitespace()
{
// Arrange
var mockWebProvider = new Mock();
@@ -121,22 +121,22 @@ public async Task ExecuteAsync_WhitespaceDepartment_CallsSelectDepartmentOptions
var mockLogger = new Mock();
mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object);
- mockTestInfra.Setup(x => x.SelectDepartmentOptionsAsync(It.IsAny()))
+ mockTestInfra.Setup(x => x.SelectDropdownOptionAsync(It.IsAny()))
.Returns(Task.FromResult(true));
- var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object);
- var department = StringValue.New(" ");
+ var func = new SelectDropdownOptionFunction(mockWebProvider.Object, mockLogger.Object);
+ var dropdownOption = StringValue.New(" ");
// Act
- var result = await func.ExecuteAsync(department);
+ var result = await func.ExecuteAsync(dropdownOption);
// Assert
Assert.True(result.Value);
- mockTestInfra.Verify(x => x.SelectDepartmentOptionsAsync(" "), Times.Once);
+ mockTestInfra.Verify(x => x.SelectDropdownOptionAsync(" "), Times.Once);
}
[Fact]
- public async Task ExecuteAsync_NullDepartment_ThrowsArgumentNullException()
+ public async Task ExecuteAsync_NullDropdown_ThrowsArgumentNullException()
{
// Arrange
var mockWebProvider = new Mock();
@@ -145,14 +145,14 @@ public async Task ExecuteAsync_NullDepartment_ThrowsArgumentNullException()
mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object);
- var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object);
+ var func = new SelectDropdownOptionFunction(mockWebProvider.Object, mockLogger.Object);
// Act & Assert
await Assert.ThrowsAsync(() => func.ExecuteAsync(null));
}
[Fact]
- public async Task ExecuteAsync_SelectDepartmentOptionsAsyncReturnsFalse_ReturnsTrue()
+ public async Task ExecuteAsync_SelectDropdownOptionAsyncReturnsFalse_ReturnsTrue()
{
// Arrange
var mockWebProvider = new Mock();
@@ -160,23 +160,23 @@ public async Task ExecuteAsync_SelectDepartmentOptionsAsyncReturnsFalse_ReturnsT
var mockLogger = new Mock();
mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object);
- mockTestInfra.Setup(x => x.SelectDepartmentOptionsAsync(It.IsAny()))
+ mockTestInfra.Setup(x => x.SelectDropdownOptionAsync(It.IsAny()))
.Returns(Task.FromResult(false));
- var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object);
- var department = StringValue.New("Legal");
+ var func = new SelectDropdownOptionFunction(mockWebProvider.Object, mockLogger.Object);
+ var dropdownOption = StringValue.New("Legal");
// Act
- var result = await func.ExecuteAsync(department);
+ var result = await func.ExecuteAsync(dropdownOption);
// Assert
// The function always returns true, regardless of the async result
Assert.True(result.Value);
- mockTestInfra.Verify(x => x.SelectDepartmentOptionsAsync("Legal"), Times.Once);
+ mockTestInfra.Verify(x => x.SelectDropdownOptionAsync("Legal"), Times.Once);
}
[Fact]
- public async Task ExecuteAsync_SelectDepartmentOptionsAsyncThrows_PropagatesException()
+ public async Task ExecuteAsync_SelectDropdownOptionAsyncThrows_PropagatesException()
{
// Arrange
var mockWebProvider = new Mock();
@@ -184,14 +184,14 @@ public async Task ExecuteAsync_SelectDepartmentOptionsAsyncThrows_PropagatesExce
var mockLogger = new Mock();
mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object);
- mockTestInfra.Setup(x => x.SelectDepartmentOptionsAsync(It.IsAny()))
+ mockTestInfra.Setup(x => x.SelectDropdownOptionAsync(It.IsAny()))
.ThrowsAsync(new InvalidOperationException("Test exception"));
- var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object);
- var department = StringValue.New("Admin");
+ var func = new SelectDropdownOptionFunction(mockWebProvider.Object, mockLogger.Object);
+ var dropdownOption = StringValue.New("Admin");
// Act & Assert
- await Assert.ThrowsAsync(() => func.ExecuteAsync(department));
+ await Assert.ThrowsAsync(() => func.ExecuteAsync(dropdownOption));
}
}
}
diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDepartmentOptionsFunction.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDropdownOptionFunction.cs
similarity index 51%
rename from src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDepartmentOptionsFunction.cs
rename to src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDropdownOptionFunction.cs
index 06ae1be12..5df5ecd3c 100644
--- a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDepartmentOptionsFunction.cs
+++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDropdownOptionFunction.cs
@@ -10,15 +10,15 @@
namespace Microsoft.PowerApps.TestEngine.PowerFx.Functions
{
///
- /// Power Fx function to select a department option in the Department dropdown.
+ /// Power Fx function to select an option in a dropdown.
///
- public class SelectDepartmentOptionsFunction : ReflectionFunction
+ public class SelectDropdownOptionFunction : ReflectionFunction
{
private readonly ITestWebProvider _testWebProvider;
private readonly ILogger _logger;
- public SelectDepartmentOptionsFunction(ITestWebProvider testWebProvider, ILogger logger)
- : base("SelectDepartmentOptions", FormulaType.Boolean, FormulaType.String)
+ public SelectDropdownOptionFunction(ITestWebProvider testWebProvider, ILogger logger)
+ : base("SelectDropdownOption", FormulaType.Boolean, FormulaType.String)
{
_testWebProvider = testWebProvider;
_logger = logger;
@@ -30,16 +30,16 @@ public BooleanValue Execute(StringValue department)
}
///
- /// Asynchronously selects a department option in the Department dropdown.
+ /// Asynchronously selects a dropdown option in the dropdown.
///
- public async Task ExecuteAsync(StringValue department)
+ public async Task ExecuteAsync(StringValue dropdownOption)
{
- if (department == null)
- throw new ArgumentNullException(nameof(department));
+ if (dropdownOption == null)
+ throw new ArgumentNullException(nameof(dropdownOption));
- _logger.LogInformation($"Executing SelectDepartmentOptionsFunction for department '{department.Value}'.");
- await _testWebProvider.TestInfraFunctions.SelectDepartmentOptionsAsync(department.Value);
- _logger.LogInformation("SelectDepartmentOptionsFunction execution completed.");
+ _logger.LogInformation($"Executing SelectDropdownOptionFunction for dropdown '{dropdownOption.Value}'.");
+ await _testWebProvider.TestInfraFunctions.SelectDropdownOptionAsync(dropdownOption.Value);
+ _logger.LogInformation("SelectDropdownOptionFunction execution completed.");
return FormulaValue.New(true);
}
}
diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs
index e66359f90..b992d3439 100644
--- a/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs
+++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs
@@ -105,7 +105,7 @@ public void Setup(TestSettings settings)
powerFxConfig.AddFunction(new NavigateToRecordFunction(_testWebProvider, async () => await UpdatePowerFxModelAsync(), Logger));
powerFxConfig.AddFunction(new SetDOBFieldsFunction(_testWebProvider, Logger));
powerFxConfig.AddFunction(new SelectGridRowCheckboxFunction(_testWebProvider, Logger));
- powerFxConfig.AddFunction(new SelectDepartmentOptionsFunction(_testWebProvider, Logger));
+ powerFxConfig.AddFunction(new SelectDropdownOptionFunction(_testWebProvider, Logger));
if (settings != null && settings.ExtensionModules != null && settings.ExtensionModules.Enable)
{
diff --git a/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs b/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs
index cf603543a..ce5273324 100644
--- a/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs
+++ b/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs
@@ -109,10 +109,10 @@ public interface ITestInfraFunctions
public Task TriggerControlClickEvent(string controlName, string filePath);
///
- /// Selects a department option in the user interface.
+ /// Selects a dropdown option in the user interface.
///
- /// The department value to select.
- /// True if the department option was successfully selected; otherwise, false.
- public Task SelectDepartmentOptionsAsync(string value);
+ /// The dropdown value to select.
+ /// True if the dropdown option was successfully selected; otherwise, false.
+ public Task SelectDropdownOptionAsync(string value);
}
}
diff --git a/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs b/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs
index b62d1216f..a283aa1be 100644
--- a/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs
+++ b/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs
@@ -515,33 +515,32 @@ public async Task TriggerControlClickEvent(string controlName, string file
}
///
- /// Selects a single department option in the Department dropdown by its name.
- /// Only one department can be selected at a time.
+ /// Selects an option in the given dropdown by its name.
///
- /// The name of the department to select.
- /// True if the option was successfully selected, otherwise false.
- public async Task SelectDepartmentOptionsAsync(string departmentName)
+ /// The name of the dropdown option to select.
+ /// True if the option was successfully selected, otherwise false.
+ public async Task SelectDropdownOptionAsync(string value)
{
- if (string.IsNullOrEmpty(departmentName))
+ if (string.IsNullOrEmpty(value))
{
- _singleTestInstanceState.GetLogger().LogError("Department name cannot be null or empty.");
- throw new ArgumentException("Department name must be provided.", nameof(departmentName));
+ _singleTestInstanceState.GetLogger().LogError("Dropdown option value cannot be null or empty.");
+ throw new ArgumentException("Dropdown option value must be provided.", nameof(value));
}
try
{
ValidatePage();
- // Open the Department dropdown
+ // Open the dropdown
await Page.GetByLabel("Department", new() { Exact = true }).ClickAsync();
- // Select the specified department
- await Page.GetByRole(AriaRole.Option, new() { Name = departmentName }).ClickAsync();
+ // Select the specified option
+ await Page.GetByRole(AriaRole.Option, new() { Name = value }).ClickAsync();
return true; // Indicate success
}
catch (Exception ex)
{
- _singleTestInstanceState.GetLogger().LogError($"Error occurred while selecting Department option: {departmentName}. Exception: {ex.Message}");
+ _singleTestInstanceState.GetLogger().LogError($"Error occurred while selecting dropdown option: {value}. Exception: {ex.Message}");
return false; // Indicate failure
}
}