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("window.deleteCompleted").Result; + + var result = PollingHelper.Poll( + null, + x => x == null, + getValue, + _testState != null ? 3000 : _testState.GetTimeout(), + _logger, + "Unable to complete delete" + ); + + if (result is bool value) + { + return BooleanValue.New(value); + } + + return BooleanValue.New(false); + } + } +} diff --git a/src/testengine.provider.mda/ModelDrivenApplicationProvider.cs b/src/testengine.provider.mda/ModelDrivenApplicationProvider.cs index d4dee61aa..b0647f9df 100644 --- a/src/testengine.provider.mda/ModelDrivenApplicationProvider.cs +++ b/src/testengine.provider.mda/ModelDrivenApplicationProvider.cs @@ -469,6 +469,8 @@ public async Task SetPropertyAsync(ItemPath itemPath, FormulaValue value) break; case (DateType): return await SetPropertyDateAsync(itemPath, (DateValue)value); + case (DateTimeType): + return await SetPropertyDateTimeAsync(itemPath, (DateTimeValue)value); case (RecordType): return await SetPropertyRecordAsync(itemPath, (RecordValue)value); case (TableType): @@ -539,6 +541,30 @@ public async Task SetPropertyRecordAsync(ItemPath itemPath, RecordValue va } } + public async Task SetPropertyDateTimeAsync(ItemPath itemPath, DateTimeValue value) + { + try + { + ValidateItemPath(itemPath, false); + + var itemPathString = JsonConvert.SerializeObject(itemPath); + var propertyNameString = JsonConvert.SerializeObject(itemPath.PropertyName); + var recordValue = value.GetConvertedValue(null); + + // TODO - Set the Xrm SDK Value and update state for any JS to run + + // Date.parse() parses the date to unix timestamp + var expression = $"PowerAppsTestEngine.setPropertyValue({itemPathString},Date.parse(\"{recordValue}\"))"; + + return await TestInfraFunctions.RunJavascriptAsync(expression); + } + catch (Exception ex) + { + ExceptionHandlingHelper.CheckIfOutDatedPublishedApp(ex, SingleTestInstanceState.GetLogger()); + throw; + } + } + /// /// Convert Power Fx formula value to the string representation /// @@ -756,6 +782,8 @@ public void Setup(PowerFxConfig powerFxConfig, ITestInfraFunctions testInfraFunc powerFxConfig.AddFunction(new SetOptionsFunction(testInfraFunctions, logger)); powerFxConfig.AddFunction(new SetValueJsonFunction(testInfraFunctions, logger)); powerFxConfig.AddFunction(new SaveFormFunction(testInfraFunctions, _testState, logger)); + powerFxConfig.AddFunction(new DeleteRecordFunction(testInfraFunctions, testState, logger)); + } public void ConfigurePowerFx(PowerFxConfig powerFxConfig) diff --git a/src/testengine.provider.mda/PowerAppsTestEngineMDA.js b/src/testengine.provider.mda/PowerAppsTestEngineMDA.js index 8ba6a5519..f0ef7a76b 100644 --- a/src/testengine.provider.mda/PowerAppsTestEngineMDA.js +++ b/src/testengine.provider.mda/PowerAppsTestEngineMDA.js @@ -70,6 +70,8 @@ class PowerAppsTestEngine { return PowerAppsModelDrivenCanvas.getControlProperties(itemPath); case PowerAppsTestEngine.CONSTANTS.EntityRecord: return PowerAppsModelDrivenEntityRecord.getControlProperties(itemPath); + case PowerAppsTestEngine.CONSTANTS.EntityList: + return PowerAppsModelDrivenEntityList.getControlProperties(itemPath); } return JSON.stringify(data); } @@ -83,6 +85,8 @@ class PowerAppsTestEngine { return PowerAppsModelDrivenCanvas.setPropertyValueForControl(item, data); case PowerAppsTestEngine.CONSTANTS.EntityRecord: return PowerAppsModelDrivenEntityRecord.setPropertyValueForControl(item, data); + case PowerAppsTestEngine.CONSTANTS.EntityList: + return PowerAppsModelDrivenEntityList.setPropertyValueForControl(itemPath); } return false; } @@ -95,7 +99,7 @@ class PowerAppsTestEngine { case PowerAppsTestEngine.CONSTANTS.Custom: return PowerAppsModelDrivenCanvas.fetchArrayItemCount(itemPath); case PowerAppsTestEngine.CONSTANTS.EntityList: - return PowerAppsModelDrivenCanvas.fetchArrayItemCount(itemPath); + return PowerAppsModelDrivenEntityList.fetchArrayItemCount(itemPath); case PowerAppsTestEngine.CONSTANTS.EntityRecord: // TODO - Get count of items for name break; @@ -109,6 +113,8 @@ class PowerAppsTestEngine { switch (PowerAppsTestEngine.pageType()) { case PowerAppsTestEngine.CONSTANTS.Custom: return PowerAppsModelDrivenCanvas.selectControl(itemPath); + case PowerAppsTestEngine.CONSTANTS.EntityList: + return PowerAppsModelDrivenEntityList.selectControl(itemPath); case PowerAppsTestEngine.CONSTANTS.EntityRecord: // TODO - Selectitem break; diff --git a/src/testengine.provider.mda/PowerAppsTestEngineMDAEntityList.js b/src/testengine.provider.mda/PowerAppsTestEngineMDAEntityList.js index b727c0373..8a981cba8 100644 --- a/src/testengine.provider.mda/PowerAppsTestEngineMDAEntityList.js +++ b/src/testengine.provider.mda/PowerAppsTestEngineMDAEntityList.js @@ -16,7 +16,214 @@ class PowerAppsModelDrivenEntityList { //TODO: set property types other than columns and individual column properties also // Warning: control object population for the main grid is only for rows currently var propertyTypeString = "*[" + var attributes = new Set(); + var columnInfo = getCurrentXrmStatus().mainGrid.getColumnInfo(); + attributes.add("entityId:s"); + Object.keys(columnInfo).forEach(attribute => { + var columnName = attribute; // see how to reconcile it to displayname since it can have multiword columnInfo[attribute].DisplayName; + var attrType = columnInfo[attribute].Type; + //TODO: for lookup type handle case where no value is present and if value is present then set type + var columnType = PowerAppsModelDrivenEntityList.getAttributeDataType(attrType); + attributes.add(`${columnName}:${columnType}`); + }); + propertyTypeString += Array.from(attributes).join(", "); propertyTypeString += "]"; return JSON.stringify({ Controls: [{ Name: PowerAppsModelDrivenEntityList.CONSTANTS.MainGrid, Properties: [{ PropertyName: PowerAppsModelDrivenEntityList.CONSTANTS.MainGridRowsRecordName, PropertyType: propertyTypeString }] }] }); } + + static fetchArrayItemCount(itemPath) { + if (itemPath.controlName == PowerAppsModelDrivenEntityList.CONSTANTS.MainGrid && itemPath.propertyName == PowerAppsModelDrivenEntityList.CONSTANTS.MainGridRowsRecordName) { + var rowCount = getCurrentXrmStatus().mainGrid.getGrid().getRows().getLength(); + return rowCount; + } + else { + if (itemPath.parentControl && itemPath.parentControl.index === null) { + // Components do not have an item count + throw "Not a gallery, no item count available. Most likely a component"; + } + // TODO: Identify not main grid controls of gallery type and return item count + return 0; + } + } + + static getControlProperties(itemPath) { + var data = []; + // Handle grid row properties + if ( + itemPath.parentControl && + itemPath.parentControl.index !== null && + itemPath.parentControl.controlName == PowerAppsModelDrivenEntityList.CONSTANTS.MainGrid + ) { + // Handle columns (attributes) + if (itemPath.parentControl.propertyName == PowerAppsModelDrivenEntityList.CONSTANTS.MainGridRowsRecordName) { + if (itemPath.propertyName == "entityId") { + // Special case for entityId + var currentGridRow = getCurrentXrmStatus().mainGrid.getGrid().getRows().get(itemPath.parentControl.index); + var idVal = currentGridRow.getData().getEntity().getId(); + idVal = idVal.replace(/{|}/g, ""); + data.push({ Key: itemPath.propertyName, Value: idVal }); + } else { + // Standard attribute/column + var currentGridRow = getCurrentXrmStatus().mainGrid.getGrid().getRows().get(itemPath.parentControl.index); + var currentRowEntity = currentGridRow.getData().getEntity(); + var currentAttribute = currentRowEntity.attributes.getByName(itemPath.propertyName); + var currentAttributeValue = currentAttribute.getValue(); + data.push({ Key: itemPath.propertyName, Value: currentAttributeValue }); + } + } else { + // Non-column properties (e.g., checkbox) + var currentGridRow = getCurrentXrmStatus().mainGrid.getGrid().getRows().get(itemPath.parentControl.index); + + // Example: handle a selection checkbox (commonly used for row selection) + if (itemPath.propertyName === "selected" || itemPath.propertyName === "isSelected") { + // Try to use the API if available + if (typeof currentGridRow.isSelected === "function") { + var isSelected = currentGridRow.isSelected(); + data.push({ Key: itemPath.propertyName, Value: isSelected }); + } else if (typeof currentGridRow.getData === "function" && typeof currentGridRow.getData().isSelected === "function") { + // Fallback: sometimes selection is on the data object + var isSelected = currentGridRow.getData().isSelected(); + data.push({ Key: itemPath.propertyName, Value: isSelected }); + } else { + // If not available, return null or handle as needed + data.push({ Key: itemPath.propertyName, Value: null }); + } + } else { + // Add more non-column property handlers here as needed + data.push({ Key: itemPath.propertyName, Value: null }); + } + } + } else { + // TODO: handle non-grid controls if needed + data.push({ Key: itemPath.propertyName, Value: null }); + } + return JSON.stringify(data); + } + + static getAttributeDataType(attribute) { + // value to get the notation for records based on type of attrtibute, append unknown types as required + var attributeType; + switch (attribute) { + case "integer": + attributeType = "i"; // Integer type + break; + case "boolean": + attributeType = "b"; // Boolean type + break; + case "datetime": + attributeType = "d"; // DateTime type + break; + case "decimal": + attributeType = "n"; // Decimal type + break; + case "string": + case "memo": + attributeType = "s"; // String or memo type + break; + case "lookup": + attributeType = "![id:s, name:s, entityType:s]"; // Lookup type + break; + case "picklist": + attributeType = "p"; // OptionSet type + break; + case "money": + attributeType = "m"; // Currency type + break; + // Add more cases as needed + default: + attributeType = "s"; // Default to 's' for string if no match + break; + } + return attributeType; + //enum AttributeType { + // Boolean = "boolean", + // Unknown = "unknown", + // Customer = "customer", + // // Date and DateTime are treated the same, Date is not a real attribute type + // Date = "date", + // DateTime = "datetime", + // Decimal = "decimal", + // Double = "double", + // Image = "image", + // Integer = "integer", + // Lookup = "lookup", + // ManagedProperty = "managedproperty", + // Memo = "memo", + // Money = "money", + // Owner = "owner", + // PartyList = "partylist", + // PickList = "picklist", + // State = "state", + // Status = "status", + // String = "string", + // UniqueIdentifier = "uniqueidentifier", + // CalendarRules = "calendarrules", + // Virtual = "virtual", + // BigInt = "bigint", + // EntityName = "entityname", + // EntityImage = "entityimage", + // AliasedValue = "aliasedvalue", + // Regarding = "regarding", + // MultiSelectPickList = "multiselectpicklist", + // File = "file", + // NavigationProperty = "navigationproperty", + // RichText = "RichText", + //} + + + } + + static setPropertyValueForControl(itemPath, value) { + var bindingContext = PowerAppsModelDrivenEntityList.getMainGridControls(itemPath); + + var controlContext = bindingContext.controlContexts[itemPath.controlName]; + + if (controlContext) { + if (controlContext.modelProperties[itemPath.propertyName]) { + propertyValue = controlContext.modelProperties[itemPath.propertyName]?.setValue(value); + return true; + } + } + + return false; + } + + + + static selectControl(itemPath) { + // Validate the itemPath object + if (!itemPath || typeof itemPath.index === "undefined" || itemPath.index === null) { + throw new Error("Invalid itemPath: 'index' is required."); + } + + // Get the main grid rows + const mainGrid = getCurrentXrmStatus().mainGrid; + const gridRows = mainGrid.getGrid().getRows(); + + // Validate the index + if (itemPath.index < 0 || itemPath.index >= gridRows.getLength()) { + throw new Error(`Invalid index: ${itemPath.index}. Must be between 0 and ${gridRows.getLength() - 1}.`); + } + + // Select the row at the specified index + const currentGridRow = gridRows.get(itemPath.index); + + + const recordId = currentGridRow.getData().getEntity().getId(); + + if (typeof grid.setSelectedRecordIds === "function") { + grid.setSelectedRecordIds([recordId]); + console.log("Row selected:", recordId); + } else { + console.error("setSelectedRecordIds is not available in this context."); + } + + + // Return the selected row data + const currentRowData = currentGridRow.getData(); + const data = []; + data.push({ Key: "row", Value: currentRowData }); + return JSON.stringify(data); + } + } \ No newline at end of file From acd7211dd990ccf41aca120ecad236722c9d5893 Mon Sep 17 00:00:00 2001 From: v-raghulraja <165115074+v-raghulraja@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:19:44 +0530 Subject: [PATCH 2/5] fixing space issue --- src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs index 988af4c4f..e66359f90 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs +++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/PowerFxEngine.cs @@ -103,7 +103,7 @@ public void Setup(TestSettings settings) 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 SetDOBFieldsFunction(_testWebProvider, Logger)); powerFxConfig.AddFunction(new SelectGridRowCheckboxFunction(_testWebProvider, Logger)); powerFxConfig.AddFunction(new SelectDepartmentOptionsFunction(_testWebProvider, Logger)); From 41d258d01d0f757e6684c1035363da3f30d13d74 Mon Sep 17 00:00:00 2001 From: v-raghulraja <165115074+v-raghulraja@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:23:31 +0530 Subject: [PATCH 3/5] Nunit testing --- .../NavigateToRecordFunctionTests.cs | 143 +++++++++++++++ .../SelectGridRowCheckboxFunctionTests.cs | 105 +++++++++++ .../PowerFx/Functions/SetDOBFieldsFunction.cs | 170 ------------------ .../Functions/SetDOBFieldsFunctionTests.cs | 105 +++++++++++ 4 files changed, 353 insertions(+), 170 deletions(-) create mode 100644 src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/NavigateToRecordFunctionTests.cs create mode 100644 src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectGridRowCheckboxFunctionTests.cs delete mode 100644 src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetDOBFieldsFunction.cs create mode 100644 src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetDOBFieldsFunctionTests.cs diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/NavigateToRecordFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/NavigateToRecordFunctionTests.cs new file mode 100644 index 000000000..2feb50776 --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/NavigateToRecordFunctionTests.cs @@ -0,0 +1,143 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Playwright; +using Microsoft.PowerApps.TestEngine.PowerFx.Functions; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx.Types; +using Moq; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.PowerApps.TestEngine.Tests.PowerFx.Functions +{ + public class NavigateToRecordFunctionTests + { + [Fact] + public async Task ExecuteAsync_NavigatesToExistingRecord_ReturnsTrue() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + var mockPage = new Mock(); + var mockContext = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.GetContext()).Returns(mockContext.Object); + mockContext.Setup(x => x.Pages).Returns(new[] { mockPage.Object }); + + // Simulate entityId found + mockPage.Setup(x => x.EvaluateAsync(It.IsAny(), null)) + .ReturnsAsync("entity123"); + + mockPage.Setup(x => x.EvaluateAsync(It.IsAny(), null)) + .ReturnsAsync(true); + + bool updateModelCalled = false; + Task UpdateModel() { updateModelCalled = true; return Task.CompletedTask; } + + var func = new NavigateToRecordFunction( + mockWebProvider.Object, + UpdateModel, + mockLogger.Object); + + // Act + var result = await func.ExecuteAsync( + FormulaValue.New("account") as StringValue, + FormulaValue.New("entityrecord") as StringValue, + NumberValue.New(1.0)); + + // Assert + Assert.True(((BooleanValue)result).Value); + Assert.True(updateModelCalled); + mockLogger.Verify(l => l.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Navigating to existing record")), + (Exception)null, + It.IsAny>()), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_NavigatesToNewRecord_ReturnsTrue() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + var mockPage = new Mock(); + var mockContext = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.GetContext()).Returns(mockContext.Object); + mockContext.Setup(x => x.Pages).Returns(new[] { mockPage.Object }); + + // Simulate no entityId found + mockPage.Setup(x => x.EvaluateAsync(It.IsAny(), null)) + .ReturnsAsync(string.Empty); + + mockPage.Setup(x => x.EvaluateAsync(It.IsAny(), null)) + .ReturnsAsync(true); + + bool updateModelCalled = false; + Task UpdateModel() { updateModelCalled = true; return Task.CompletedTask; } + + var func = new NavigateToRecordFunction( + mockWebProvider.Object, + UpdateModel, + mockLogger.Object); + + // Act + var result = await func.ExecuteAsync( + FormulaValue.New("contact") as StringValue, + FormulaValue.New("entityrecord") as StringValue, + NumberValue.New(1.0)); + + // Assert + Assert.True(((BooleanValue)result).Value); + Assert.True(updateModelCalled); + mockLogger.Verify(l => l.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("No selected entity found")), + (Exception)null, + It.IsAny>()), Times.Once); + } + + [Fact] + public void Execute_CallsAsyncSynchronously() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + var mockPage = new Mock(); + var mockContext = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.GetContext()).Returns(mockContext.Object); + mockContext.Setup(x => x.Pages).Returns(new[] { mockPage.Object }); + + mockPage.Setup(x => x.EvaluateAsync(It.IsAny(), null)) + .ReturnsAsync("entity456"); + mockPage.Setup(x => x.EvaluateAsync(It.IsAny(), null)) + .ReturnsAsync(true); + + var func = new NavigateToRecordFunction( + mockWebProvider.Object, + () => Task.CompletedTask, + mockLogger.Object); + + // Act + var result = func.Execute( + FormulaValue.New("lead") as StringValue, + FormulaValue.New("entityrecord") as StringValue, + NumberValue.New(1.0)); + + // Assert + Assert.True(((BooleanValue)result).Value); + } + } +} diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectGridRowCheckboxFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectGridRowCheckboxFunctionTests.cs new file mode 100644 index 000000000..c44036989 --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectGridRowCheckboxFunctionTests.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Playwright; +using Microsoft.PowerApps.TestEngine.PowerFx.Functions; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx.Types; +using Moq; +using Xunit; + +namespace Microsoft.PowerApps.TestEngine.Tests.PowerFx.Functions +{ + public class SelectGridRowCheckboxFunctionTests + { + [Fact] + public async Task ExecuteAsync_ValidRowIndex_ExecutesJavaScriptAndReturnsTrue() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + var mockPage = new Mock(); + var mockContext = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.GetContext()).Returns(mockContext.Object); + mockContext.Setup(x => x.Pages).Returns(new[] { mockPage.Object }); + + bool jsCalled = false; + mockPage.Setup(x => x.EvaluateAsync(It.IsAny(), null)) + .Callback(() => jsCalled = true) + .Returns(Task.FromResult((JsonElement?)default)); + + var func = new SelectGridRowCheckboxFunction( + mockWebProvider.Object, + mockLogger.Object); + + // Act + var result = await func.ExecuteAsync(FormulaValue.New(1.0) as NumberValue); + + // Assert + Assert.True(result.Value); + Assert.True(jsCalled); + mockLogger.Verify(l => l.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Executing SelectGridRowCheckboxFunction")), + null, + It.IsAny>()), Times.Once); + } + + [Fact] + public void Execute_CallsAsyncSynchronously() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + var mockPage = new Mock(); + var mockContext = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.GetContext()).Returns(mockContext.Object); + mockContext.Setup(x => x.Pages).Returns(new[] { mockPage.Object }); + + mockPage.Setup(x => x.EvaluateAsync(It.IsAny(), null)) + .Returns(Task.FromResult((JsonElement?)default)); + + var func = new SelectGridRowCheckboxFunction( + mockWebProvider.Object, + mockLogger.Object); + + // Act + var result = func.Execute(FormulaValue.New(0.0) as NumberValue); + + // Assert + Assert.True(result.Value); + } + + [Fact] + public async Task ExecuteAsync_EmptyPages_ThrowsInvalidOperationException() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + var mockContext = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.GetContext()).Returns(mockContext.Object); + mockContext.Setup(x => x.Pages).Returns(Array.Empty()); + + var func = new SelectGridRowCheckboxFunction( + mockWebProvider.Object, + mockLogger.Object); + + // Act & Assert + await Assert.ThrowsAsync(() => + func.ExecuteAsync(FormulaValue.New(0.0) as NumberValue)); + } + } +} diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetDOBFieldsFunction.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetDOBFieldsFunction.cs deleted file mode 100644 index 42b629705..000000000 --- a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetDOBFieldsFunction.cs +++ /dev/null @@ -1,170 +0,0 @@ -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.Tests/PowerFx/Functions/SetDOBFieldsFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetDOBFieldsFunctionTests.cs new file mode 100644 index 000000000..49f830865 --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SetDOBFieldsFunctionTests.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Playwright; +using Microsoft.PowerApps.TestEngine.PowerFx.Functions; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx.Types; +using Moq; +using Xunit; + +namespace Microsoft.PowerApps.TestEngine.Tests.PowerFx.Functions +{ + public class SetDOBFieldsFunctionTests + { + [Fact] + public async Task ExecuteAsync_ValidInputs_ExecutesJavaScriptAndReturnsTrue() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + var mockPage = new Mock(); + var mockContext = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.GetContext()).Returns(mockContext.Object); + mockContext.Setup(x => x.Pages).Returns(new[] { mockPage.Object }); + + bool jsCalled = false; + mockPage.Setup(x => x.EvaluateAsync(It.IsAny(), null)) + .Callback((js, arg) => jsCalled = true) + .Returns(Task.FromResult((JsonElement?)default)); + + var func = new SetDOBFieldsFunction( + mockWebProvider.Object, + mockLogger.Object); + + // Act + var result = await func.ExecuteAsync(FormulaValue.New("2023-01-01") as StringValue, FormulaValue.New("12:00:00") as StringValue); + + // Assert + Assert.True(result.Value); + Assert.True(jsCalled); + mockLogger.Verify(l => l.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Executing SetDOBFieldsFunction")), + null, + It.IsAny>()), Times.Once); + } + + [Fact] + public void Execute_ValidInputs_CallsAsyncSynchronously() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + var mockPage = new Mock(); + var mockContext = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.GetContext()).Returns(mockContext.Object); + mockContext.Setup(x => x.Pages).Returns(new[] { mockPage.Object }); + + mockPage.Setup(x => x.EvaluateAsync(It.IsAny(), null)) + .Returns(Task.FromResult((JsonElement?)default)); + + var func = new SetDOBFieldsFunction( + mockWebProvider.Object, + mockLogger.Object); + + // Act + var result = func.Execute(FormulaValue.New("2023-01-01") as StringValue, FormulaValue.New("12:00:00") as StringValue); + + // Assert + Assert.True(result.Value); + } + + [Fact] + public async Task ExecuteAsync_NoPages_ThrowsInvalidOperationException() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + var mockContext = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.GetContext()).Returns(mockContext.Object); + mockContext.Setup(x => x.Pages).Returns(Array.Empty()); + + var func = new SetDOBFieldsFunction( + mockWebProvider.Object, + mockLogger.Object); + + // Act & Assert + await Assert.ThrowsAsync(() => + func.ExecuteAsync(FormulaValue.New("2023-01-01") as StringValue, FormulaValue.New("12:00:00") as StringValue)); + } + } +} From fb2114da85011698b22a52b30e779afe39121d03 Mon Sep 17 00:00:00 2001 From: v-raghulraja <165115074+v-raghulraja@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:44:08 +0530 Subject: [PATCH 4/5] Department options nunit testing --- .../SelectDepartmentOptionsFunctionTests.cs | 197 ++++++++++++++++++ .../SelectDepartmentOptionsFunction.cs | 3 + 2 files changed, 200 insertions(+) create mode 100644 src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectDepartmentOptionsFunctionTests.cs diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectDepartmentOptionsFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectDepartmentOptionsFunctionTests.cs new file mode 100644 index 000000000..c04caac6f --- /dev/null +++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/SelectDepartmentOptionsFunctionTests.cs @@ -0,0 +1,197 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.PowerFx.Functions; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx.Types; +using System; +using Moq; +using Xunit; + +namespace Microsoft.PowerApps.TestEngine.Tests.PowerFx.Functions +{ + public class SelectDepartmentOptionsFunctionTests + { + [Fact] + public async Task ExecuteAsync_ValidDepartment_CallsSelectDepartmentOptionsAsyncAndLogs() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.SelectDepartmentOptionsAsync(It.IsAny())) + .Returns(Task.FromResult(true)); + + var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object); + var department = StringValue.New("HR"); + + // Act + var result = await func.ExecuteAsync(department); + + // Assert + Assert.True(result.Value); + mockTestInfra.Verify(x => x.SelectDepartmentOptionsAsync("HR"), Times.Once); + mockLogger.Verify( + l => l.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("Executing SelectDepartmentOptionsFunction for department 'HR'.")), + null, + It.IsAny>()), + Times.Once); + mockLogger.Verify( + l => l.Log( + LogLevel.Information, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("SelectDepartmentOptionsFunction execution completed.")), + null, + It.IsAny>()), + Times.Once); + } + + [Fact] + public void Execute_CallsAsyncSynchronously() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.SelectDepartmentOptionsAsync(It.IsAny())) + .Returns(Task.FromResult(true)); + + var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object); + var department = StringValue.New("Finance"); + + // Act + var result = func.Execute(department); + + // Assert + Assert.True(result.Value); + mockTestInfra.Verify(x => x.SelectDepartmentOptionsAsync("Finance"), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_NullTestInfraFunctions_ThrowsNullReferenceException() + { + // Arrange + var mockWebProvider = new Mock(); + 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"); + + // Act & Assert + await Assert.ThrowsAsync(() => func.ExecuteAsync(department)); + } + + [Fact] + public async Task ExecuteAsync_EmptyDepartment_CallsSelectDepartmentOptionsAsyncWithEmptyString() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.SelectDepartmentOptionsAsync(It.IsAny())) + .Returns(Task.FromResult(true)); + + var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object); + var department = StringValue.New(string.Empty); + + // Act + var result = await func.ExecuteAsync(department); + + // Assert + Assert.True(result.Value); + mockTestInfra.Verify(x => x.SelectDepartmentOptionsAsync(string.Empty), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WhitespaceDepartment_CallsSelectDepartmentOptionsAsyncWithWhitespace() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.SelectDepartmentOptionsAsync(It.IsAny())) + .Returns(Task.FromResult(true)); + + var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object); + var department = StringValue.New(" "); + + // Act + var result = await func.ExecuteAsync(department); + + // Assert + Assert.True(result.Value); + mockTestInfra.Verify(x => x.SelectDepartmentOptionsAsync(" "), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_NullDepartment_ThrowsArgumentNullException() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + + var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object); + + // Act & Assert + await Assert.ThrowsAsync(() => func.ExecuteAsync(null)); + } + + [Fact] + public async Task ExecuteAsync_SelectDepartmentOptionsAsyncReturnsFalse_ReturnsTrue() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.SelectDepartmentOptionsAsync(It.IsAny())) + .Returns(Task.FromResult(false)); + + var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object); + var department = StringValue.New("Legal"); + + // Act + var result = await func.ExecuteAsync(department); + + // Assert + // The function always returns true, regardless of the async result + Assert.True(result.Value); + mockTestInfra.Verify(x => x.SelectDepartmentOptionsAsync("Legal"), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_SelectDepartmentOptionsAsyncThrows_PropagatesException() + { + // Arrange + var mockWebProvider = new Mock(); + var mockTestInfra = new Mock(); + var mockLogger = new Mock(); + + mockWebProvider.SetupGet(x => x.TestInfraFunctions).Returns(mockTestInfra.Object); + mockTestInfra.Setup(x => x.SelectDepartmentOptionsAsync(It.IsAny())) + .ThrowsAsync(new InvalidOperationException("Test exception")); + + var func = new SelectDepartmentOptionsFunction(mockWebProvider.Object, mockLogger.Object); + var department = StringValue.New("Admin"); + + // Act & Assert + await Assert.ThrowsAsync(() => func.ExecuteAsync(department)); + } + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDepartmentOptionsFunction.cs b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDepartmentOptionsFunction.cs index 6be01e30c..06ae1be12 100644 --- a/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDepartmentOptionsFunction.cs +++ b/src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/SelectDepartmentOptionsFunction.cs @@ -34,6 +34,9 @@ public BooleanValue Execute(StringValue department) /// 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 } }