From 97302506ba4a4a7af916e1d5b0e159df53156e84 Mon Sep 17 00:00:00 2001
From: Grant Archibald <31553604+Grant-Archibald-MS@users.noreply.github.com>
Date: Fri, 16 May 2025 22:45:19 -0700
Subject: [PATCH 1/7] Adding Preview.AssertJavaScript
---
samples/javascript-d365-tests/README.md | 92 +++
samples/javascript-d365-tests/RunTests.ps1 | 587 ++++++++++++++++++
samples/javascript-d365-tests/commandBar.js | 84 +++
.../javascript-d365-tests/commandBar.te.yaml | 59 ++
samples/javascript-d365-tests/formScripts.js | 94 +++
.../javascript-d365-tests/formScripts.te.yaml | 50 ++
samples/javascript-d365-tests/mockXrm.js | 181 ++++++
.../javascript-d365-tests/testSettings.yaml | 12 +
samples/javascript-d365-tests/validation.js | 94 +++
.../javascript-d365-tests/validation.te.yaml | 62 ++
samples/javascript-d365-tests/visibility.js | 106 ++++
.../javascript-d365-tests/visibility.te.yaml | 64 ++
...icrosoft.PowerApps.TestEngine.Tests.csproj | 2 +-
.../AssertJavaScriptFunctionTests.cs | 581 +++++++++++++++++
.../PowerFXModel/ControlRecordValueTests.cs | 10 +-
.../Microsoft.PowerApps.TestEngine.csproj | 1 +
.../Functions/AssertJavaScriptFunction.cs | 387 ++++++++++++
.../PowerFx/PowerFxEngine.cs | 13 +-
.../testengine.common.user.tests.csproj | 2 +-
.../testengine.provider.mda.tests.csproj | 2 +-
20 files changed, 2473 insertions(+), 10 deletions(-)
create mode 100644 samples/javascript-d365-tests/README.md
create mode 100644 samples/javascript-d365-tests/RunTests.ps1
create mode 100644 samples/javascript-d365-tests/commandBar.js
create mode 100644 samples/javascript-d365-tests/commandBar.te.yaml
create mode 100644 samples/javascript-d365-tests/formScripts.js
create mode 100644 samples/javascript-d365-tests/formScripts.te.yaml
create mode 100644 samples/javascript-d365-tests/mockXrm.js
create mode 100644 samples/javascript-d365-tests/testSettings.yaml
create mode 100644 samples/javascript-d365-tests/validation.js
create mode 100644 samples/javascript-d365-tests/validation.te.yaml
create mode 100644 samples/javascript-d365-tests/visibility.js
create mode 100644 samples/javascript-d365-tests/visibility.te.yaml
create mode 100644 src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/AssertJavaScriptFunctionTests.cs
create mode 100644 src/Microsoft.PowerApps.TestEngine/PowerFx/Functions/AssertJavaScriptFunction.cs
diff --git a/samples/javascript-d365-tests/README.md b/samples/javascript-d365-tests/README.md
new file mode 100644
index 000000000..f7877d358
--- /dev/null
+++ b/samples/javascript-d365-tests/README.md
@@ -0,0 +1,92 @@
+# JavaScript Testing for Dynamics 365 Client-side Code
+
+This sample demonstrates how to use the PowerApps Test Engine to test JavaScript code commonly used in Dynamics 365 applications, particularly focusing on:
+
+1. Command bar button customizations
+2. Form visibility/display logic
+3. Field validation and business rules
+4. Form event handlers
+
+## Sample Structure
+
+### Core JavaScript Files (D365 Webresource Pattern)
+
+- **mockXrm.js**: Mock implementation of the Xrm client-side object model
+- **commandBar.js**: Command bar button functions
+- **formScripts.js**: Form event handlers
+- **visibility.js**: Field/section/tab visibility control
+- **validation.js**: Data validation functions
+
+### Test Configuration & Scripts
+
+- **commandBar.te.yaml**: Command bar-specific test file
+- **formScripts.te.yaml**: Form script-specific test file
+- **validation.te.yaml**: Validation-specific test file
+- **visibility.te.yaml**: Visibility logic-specific test file
+- **RunTests.ps1**: PowerShell script to execute all tests and generate detailed reports
+- **config.json**: Configuration file for test execution
+
+## How to Run
+
+1. Ensure PowerApps Test Engine is installed
+2. Configure your environment in `config.json` with your tenant ID, environment ID, and URL
+3. Run all tests with detailed reporting: `.\RunTests.ps1`
+4. Optional parameters:
+ - Compile before running: `.\RunTests.ps1 -compile`
+ - Record test interactions: `.\RunTests.ps1 -record`
+ - Specify time threshold: `.\RunTests.ps1 -lastRunTime "2025-05-15 10:00"`
+
+## Understanding the Tests
+
+These tests use the `AssertJavaScript` PowerFx function to execute client-side D365 code against a mock Xrm object. The component test configuration files follow this pattern:
+
+```yaml
+testCaseName: CommandBar_NewButtonAlwaysEnabled
+description: Tests that new button is always enabled
+testSteps: |
+ = Preview.AssertJavaScript({
+ Location="commandBar.js",
+ Setup="mockXrm.js",
+ Run: "isCommandEnabled('new')",
+ Expected: "true"
+ });
+```
+
+Each test specifies:
+- **Location**: JavaScript file containing the function to test
+- **Setup**: JavaScript setup code or path to a file (like mockXrm.js)
+- **Run**: The JavaScript code to execute, directly calling the functions
+- **Expected**: The expected result of the test
+
+The mock framework provides fake implementations of common D365 client-side APIs, allowing you to test form scripts without an actual Dynamics 365 instance.
+
+## D365 JavaScript Structure
+
+The project follows modern Dynamics 365 web resource patterns:
+
+1. **Direct Function Declarations**: All JavaScript is organized as directly callable functions
+ ```javascript
+ function isCommandEnabled(commandName) { ... }
+ ```
+
+2. **Testable Functions**: Functions are designed to be independently testable
+ ```javascript
+ function validatePhoneNumber(phoneNumber) { ... }
+ ```
+
+3. **Separation of Concerns**: Each JavaScript file focuses on a specific area
+ - Command bar customizations
+ - Form event handlers
+ - Field visibility logic
+ - Data validation
+
+## Detailed HTML Reports
+
+The `RunTests.ps1` script generates detailed HTML reports with:
+
+- Pass/fail statistics for each component
+- Health score calculation (passing files / total files × 100)
+- Charts and metrics for test performance
+- Execution time analysis
+
+To view the most recent report, check the `TestResults` folder after running the tests.
diff --git a/samples/javascript-d365-tests/RunTests.ps1 b/samples/javascript-d365-tests/RunTests.ps1
new file mode 100644
index 000000000..daa229a6a
--- /dev/null
+++ b/samples/javascript-d365-tests/RunTests.ps1
@@ -0,0 +1,587 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+# Check for optional command line argument for last run time
+param (
+ [string]$lastRunTime,
+ [switch]$compile,
+ [switch]$record
+)
+
+# Get current directory so we can reset back to it after running the tests
+$currentDirectory = Get-Location
+
+$timeStart = Get-Date
+
+# Check for config.json file
+if (Test-Path -Path .\config.json) {
+ $jsonContent = Get-Content -Path .\config.json -Raw
+ $config = $jsonContent | ConvertFrom-Json
+ $tenantId = $config.tenantId
+ $environmentId = $config.environmentId
+ $environmentUrl = $config.environmentUrl
+ $staticContext = $config.useStaticContext
+ $debugTests = $config.debugTests
+} else {
+ # Default values if no config file is present
+ $tenantId = $null
+ $environmentId = $null
+ $environmentUrl = $null
+ $staticContext = $false
+ $debugTests = $false
+}
+
+# Prompt for required values if they are missing
+if ([string]::IsNullOrEmpty($tenantId)) {
+ $tenantId = Read-Host -Prompt "Enter your tenant ID"
+}
+
+if ([string]::IsNullOrEmpty($environmentId)) {
+ $environmentId = Read-Host -Prompt "Enter your environment ID"
+}
+
+if ([string]::IsNullOrEmpty($environmentUrl)) {
+ $environmentUrl = Read-Host -Prompt "Enter your environment URL (e.g., https://yourorg.crm.dynamics.com)"
+}
+
+# Define the folder path for TestEngine output
+$folderPath = "$env:USERPROFILE\AppData\Local\Temp\Microsoft\TestEngine\TestOutput"
+
+# Set values for flags
+$debugTestValue = $debugTests ? "TRUE" : "FALSE"
+$staticContextValue = $staticContext ? "TRUE" : "FALSE"
+
+# Initialize the dictionary (hash table) for test results
+$dictionary = @{}
+
+# Define the TestData class
+class TestData {
+ [string]$TestFile
+ [int]$PassCount
+ [int]$FailCount
+ [bool]$IsSuccess
+
+ TestData([string]$testFile, [int]$passCount, [int]$failCount, [bool]$isSuccess) {
+ $this.TestFile = $testFile
+ $this.PassCount = $passCount
+ $this.FailCount = $failCount
+ $this.IsSuccess = $isSuccess
+ }
+
+ # Override ToString method for better display
+ [string]ToString() {
+ return "TestFile: $($this.TestFile), PassCount: $($this.PassCount), FailCount: $($this.FailCount), IsSuccess: $($this.IsSuccess)"
+ }
+}
+
+# Function to add or update a key-value pair
+function AddOrUpdate {
+ param (
+ [string]$key,
+ [object]$value
+ )
+
+ if ($dictionary.ContainsKey($key)) {
+ # Update the pass/fail properties if the key exists
+ $dictionary[$key].PassCount += $value.PassCount
+ $dictionary[$key].FailCount += $value.FailCount
+ if (-not $value.IsSuccess) {
+ $dictionary[$key].IsSuccess = $false
+ }
+ Write-Host "Updated key '$key' with value '$($dictionary[$key])'."
+ } else {
+ # Add the key-value pair if the key does not exist
+ $dictionary[$key] = $value
+ Write-Host "Added key '$key' with value '$value'."
+ }
+}
+
+# Function to update test data based on .trx files
+function Update-TestData {
+ param (
+ [string]$folderPath,
+ [datetime]$timeThreshold,
+ [string]$testFile
+ )
+
+ AddOrUpdate -key $testFile -value (New-Object TestData($testFile, 0, 0, $true))
+
+ # Find all folders newer than the specified time
+ $folders = Get-ChildItem -Path $folderPath -Directory | Where-Object { $_.LastWriteTime -gt $timeThreshold }
+
+ # Initialize array to store .trx files
+ $trxFiles = @()
+
+ # Iterate through each folder and find .trx files
+ foreach ($folder in $folders) {
+ $trxFiles += Get-ChildItem -Path $folder.FullName -Filter "*.trx"
+ }
+
+ # Parse each .trx file and update pass/fail counts in TestData
+ foreach ($trxFile in $trxFiles) {
+ $xmlContent = Get-Content -Path $trxFile.FullName -Raw
+ $xml = [xml]::new()
+ $xml.LoadXml($xmlContent)
+
+ # Create a namespace manager
+ $namespaceManager = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
+ $namespaceManager.AddNamespace("ns", "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")
+
+ # Find the Counters element
+ $counters = $xml.SelectSingleNode("//ns:Counters", $namespaceManager)
+
+ # Extract the counter properties and update TestData
+ if ($counters) {
+ $passCount = [int]$counters.passed
+ $failCount = [int]$counters.failed
+ $isSuccess = $failCount -eq 0 -and $passCount -gt 0
+
+ AddOrUpdate -key $testFile -value (New-Object TestData($testFile, $passCount, $failCount, $isSuccess))
+ }
+ }
+}
+
+# Verify Azure CLI context
+if ($tenantId) {
+ $azTenantId = az account show --query tenantId --output tsv 2>$null
+
+ if ($azTenantId -ne $tenantId) {
+ Write-Warning "Azure CLI tenant ID ($azTenantId) does not match specified tenant ID ($tenantId)."
+ $useCliAuth = Read-Host -Prompt "Would you like to log in with the correct tenant ID? (Y/N)"
+
+ if ($useCliAuth -eq "Y" -or $useCliAuth -eq "y") {
+ az login --tenant $tenantId
+ }
+ }
+}
+
+# Build the latest debug version of Test Engine from source if requested
+if ($compile) {
+ Set-Location "$currentDirectory\..\..\src"
+ Write-Host "Compiling the project..."
+ dotnet build
+
+ # Install Playwright if needed
+ Start-Process -FilePath "pwsh" -ArgumentList "-Command `"..\bin\Debug\PowerAppsTestEngine\playwright.ps1 install`"" -Wait
+
+ Set-Location ..\bin\Debug\PowerAppsTestEngine
+} else {
+ # Just navigate to the compiled binary
+ Write-Host "Using pre-compiled binaries..."
+ Set-Location ..\..\bin\Debug\PowerAppsTestEngine
+}
+
+# Find all the *.te.yaml files in the JavaScript tests directory
+$testFiles = Get-ChildItem -Path "$currentDirectory\*.te.yaml" -File
+
+Write-Host "Found $($testFiles.Count) test files to execute."
+
+$totalTests = $testFiles.Count
+$passedTests = 0
+
+# Execute each test file
+foreach ($testFile in $testFiles) {
+ $testFilePath = $testFile.FullName
+ $testFileName = $testFile.Name
+
+ Write-Host "----------------------------------------" -ForegroundColor Yellow
+ Write-Host "Executing test: $testFileName" -ForegroundColor Yellow
+ Write-Host "----------------------------------------" -ForegroundColor Yellow
+
+ $testStart = Get-Date
+
+ dotnet PowerAppsTestEngine.dll -p "powerfx" -i "$testFilePath" -t $tenantId -e $environmentId -d "$environmentUrl" -l "Debug"
+
+ # Update test results data
+ Update-TestData -folderPath $folderPath -timeThreshold $testStart -testFile $testFileName
+}
+
+# Reset the location back to the original directory
+Set-Location $currentDirectory
+
+# Set the timeThreshold if provided as parameter
+if ($lastRunTime) {
+ try {
+ $timeThreshold = [datetime]::ParseExact($lastRunTime, "yyyy-MM-dd HH:mm", $null)
+ } catch {
+ Write-Error "Invalid date format. Please use 'yyyy-MM-DD HH:mm'."
+ $timeThreshold = $timeStart
+ }
+} else {
+ $timeThreshold = $timeStart
+}
+
+# Generate HTML summary report with timestamp
+$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
+$reportPath = "$folderPath\summary_report_$timestamp.html"
+
+# Function to generate HTML table representation of the TestData dictionary
+function Generate-HTMLTable {
+ param (
+ [hashtable]$dictionary,
+ [int]$total
+ )
+
+ # Initialize HTML table
+ $htmlTable = @"
+
Test Execution Results
+
+
+ | Test File |
+ Pass Count |
+ Fail Count |
+ Status |
+
+"@
+
+ $passedFiles = 0
+ # Iterate through the dictionary and add rows to the table
+ foreach ($key in $dictionary.Keys | Sort-Object) {
+ $value = $dictionary[$key]
+ if (-not [string]::IsNullOrEmpty($value)) {
+ $status = $value.IsSuccess ? "Pass" : "Fail"
+ $statusClass = $value.IsSuccess ? "pass" : "fail"
+
+ if ($value.IsSuccess) {
+ $passedFiles++
+ }
+
+ $htmlTable += "| $($value.TestFile) | $($value.PassCount) | $($value.FailCount) | $status |
"
+ }
+ }
+
+ # Close the table
+ $htmlTable += "
"
+
+ $healthPercentage = $total -eq 0 ? "0" : ($passedFiles / $total * 100).ToString("0")
+
+ # Add calculation and formula
+ $mathFormula = @"
+ Test Health Calculation:
+
+"@
+
+ $htmlTable += $mathFormula
+
+ return @{
+ Table = $htmlTable
+ HealthPercentage = $healthPercentage
+ PassedFiles = $passedFiles
+ }
+}
+
+# Find all folders newer than the specified time
+$folders = Get-ChildItem -Path $folderPath -Directory | Where-Object { $_.LastWriteTime -gt $timeThreshold }
+
+# Initialize arrays to store .trx files and test results
+$trxFiles = @()
+$testResults = @()
+
+# Iterate through each folder and find .trx files
+foreach ($folder in $folders) {
+ $trxFiles += Get-ChildItem -Path $folder.FullName -Filter "*.trx"
+}
+
+# Parse each .trx file and count pass and fail tests
+foreach ($trxFile in $trxFiles) {
+ $xmlContent = Get-Content -Path $trxFile.FullName -Raw
+ $xml = [xml]::new()
+ $xml.LoadXml($xmlContent)
+
+ # Create a namespace manager
+ $namespaceManager = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
+ $namespaceManager.AddNamespace("ns", "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")
+
+ # Find the Counters element
+ $counters = $xml.SelectSingleNode("//ns:Counters", $namespaceManager)
+
+ # Extract the counter properties
+ $total = [int]$counters.total
+ $executed = [int]$counters.executed
+ $passed = [int]$counters.passed
+ $failed = [int]$counters.failed
+ $error = [int]$counters.error
+ $timeout = [int]$counters.timeout
+ $aborted = [int]$counters.aborted
+ $inconclusive = [int]$counters.inconclusive
+ $passedButRunAborted = [int]$counters.passedButRunAborted
+ $notRunnable = [int]$counters.notRunnable
+ $notExecuted = [int]$counters.notExecuted
+ $disconnected = [int]$counters.disconnected
+ $warning = [int]$counters.warning
+ $completed = [int]$counters.completed
+ $inProgress = [int]$counters.inProgress
+ $pending = [int]$counters.pending
+
+ $testResults += [PSCustomObject]@{
+ File = $trxFile.FullName
+ Total = $total
+ Executed = $executed
+ Passed = $passed
+ Failed = $failed
+ Error = $error
+ Timeout = $timeout
+ Aborted = $aborted
+ Inconclusive = $inconclusive
+ PassedButRunAborted = $passedButRunAborted
+ NotRunnable = $notRunnable
+ NotExecuted = $notExecuted
+ Disconnected = $disconnected
+ Warning = $warning
+ Completed = $completed
+ InProgress = $inProgress
+ Pending = $pending
+ }
+}
+
+# Calculate totals
+$passCount = ($testResults | Measure-Object -Property Passed -Sum).Sum
+$failCount = ($testResults | Measure-Object -Property Failed -Sum).Sum
+$totalTestCases = $passCount + $failCount
+
+# Generate table and get health stats
+$tableResult = Generate-HTMLTable -dictionary $dictionary -total $testFiles.Count
+$healthPercentage = $tableResult.HealthPercentage
+$healthPercentageValue = [int]$healthPercentage
+$passedFiles = $tableResult.PassedFiles
+
+# Create HTML report
+$htmlReport = @"
+
+
+
+
+
+ D365 JavaScript Test Results
+
+
+
+
+
+
+ D365 JavaScript Test Results
+ Run Date: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
+
+
+
+
Test Files
+
$($testFiles.Count)
+
+
+
Passing Files
+
$passedFiles / $($testFiles.Count)
+
+
+
Test Cases
+
$totalTestCases
+
+
+
Health Score
+
$healthPercentage%
+
+
+
+
+
Test Results Summary
+
+
+
+
+
Test Files Status
+
+
+
+ $($tableResult.Table)
+
+
+
+
Interactive Test Results Table
+
+
+
+
+
+
+"@
+
+# Save HTML report
+$htmlReport | Out-File -FilePath $reportPath -Encoding UTF8
+
+Write-Host "HTML summary report generated at $reportPath"
+Write-Host "Opening report in browser..."
+
+# Open the report in the default browser
+Invoke-Item $reportPath
diff --git a/samples/javascript-d365-tests/commandBar.js b/samples/javascript-d365-tests/commandBar.js
new file mode 100644
index 000000000..f8f739e53
--- /dev/null
+++ b/samples/javascript-d365-tests/commandBar.js
@@ -0,0 +1,84 @@
+/**
+ * commandBar.js - Common command bar functions for Dynamics 365
+ * These functions would be called directly from command bar buttons.
+ */
+
+/**
+ * Determines if the specified command should be enabled based on the current entity state
+ * @param {string} commandName - Name of the command
+ * @returns {boolean} - True if the command should be enabled, false otherwise
+ */
+function isCommandEnabled(commandName) {
+ // Get the current entity state
+ var accountStatus = Xrm.Page.getAttribute("accountstatus");
+ var statusValue = accountStatus ? accountStatus.getValue() : null;
+
+ // Determine which commands should be enabled
+ switch(commandName) {
+ case "new":
+ // New command is always available
+ return true;
+
+ case "activate":
+ // Activate command is only available for inactive records
+ return statusValue === 0; // 0 = Inactive
+
+ case "deactivate":
+ // Deactivate command is only available for active records
+ return statusValue === 1; // 1 = Active
+
+ default:
+ // Unknown command
+ return false;
+ }
+};
+
+/**
+ * Validates if an account can be deactivated based on business rules
+ * @returns {boolean} - True if deactivation is allowed, false otherwise
+ */
+CommandBar.validateBeforeDeactivate = function() {
+ // Check if there are active cases
+ var activeCasesCount = Xrm.Page.getAttribute("activecases_count");
+
+ // Check if there are active cases
+ if (activeCasesCount && activeCasesCount.getValue() > 0) {
+ Xrm.Utility.alertDialog("Cannot deactivate account with active cases.");
+ return false;
+ }
+
+ return true;
+};
+
+/**
+ * Activates the current account record
+ * @returns {boolean} - True if operation was successful
+ */
+CommandBar.activateAccount = function() {
+ // Implementation would call WebAPI to update record
+ var accountStatus = Xrm.Page.getAttribute("accountstatus");
+ if (accountStatus) {
+ accountStatus.setValue(1); // 1 = Active
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Deactivates the current account record
+ * @returns {boolean} - True if operation was successful
+ */
+CommandBar.deactivateAccount = function() {
+ // Check if deactivation is allowed
+ if (!CommandBar.validateBeforeDeactivate()) {
+ return false;
+ }
+
+ // Implementation would call WebAPI to update record
+ var accountStatus = Xrm.Page.getAttribute("accountstatus");
+ if (accountStatus) {
+ accountStatus.setValue(0); // 0 = Inactive
+ return true;
+ }
+ return false;
+};
diff --git a/samples/javascript-d365-tests/commandBar.te.yaml b/samples/javascript-d365-tests/commandBar.te.yaml
new file mode 100644
index 000000000..ae57b02f5
--- /dev/null
+++ b/samples/javascript-d365-tests/commandBar.te.yaml
@@ -0,0 +1,59 @@
+# yaml-embedded-languages: powerfx
+testSuite:
+ testSuiteName: Command Bar Tests
+ testSuiteDescription: Tests for Dynamics 365 command bar functionality using PowerFx format
+ persona: User1
+ appLogicalName: na
+
+ testCases:
+ - testCaseName: CommandBar_NewButtonAlwaysEnabled
+ testCaseDescription: Tests that new button is always enabled
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"commandBar.js", Setup:"mockXrm.js", Run: Join([
+ "isCommandEnabled('new')"
+ ]), Expected: "true" });
+ - testCaseName: CommandBar_ActivateButtonEnabledForInactiveAccount
+ testCaseDescription: Tests that activate button is enabled for inactive accounts
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"commandBar.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('accountstatus').setValue(0)",
+ "isCommandEnabled('activate')"
+ ]), Expected: "true" });
+ - testCaseName: CommandBar_ActivateButtonDisabledForActiveAccount
+ testCaseDescription: Tests that activate button is disabled for active accounts
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"commandBar.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('accountstatus').setValue()",
+ "isCommandEnabled('activate')"
+ ]), Expected: "false" });
+ - testCaseName: CommandBar_DeactivateButtonEnabledForActiveAccount
+ testCaseDescription: Tests that deactivate button is enabled for active accounts
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"commandBar.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('accountstatus').setValue(1)",
+ "isCommandEnabled('deactivate')"
+ ]), Expected: "true" });
+ - testCaseName: CommandBar_DeactivateButtonDisabledForInactiveAccount
+ testCaseDescription: Tests that deactivate button is disabled for inactive accounts
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"commandBar.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('accountstatus').setValue(0)",
+ "isCommandEnabled('deactivate')"
+ ]), Expected: "false" });
+ - testCaseName: CommandBar_DeactivatePreventedWithActiveCases
+ testCaseDescription: Tests that deactivation is prevented when there are active cases
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"commandBar.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('accountstatus').setValue(1)",
+ "Xrm.Page.getAttribute('activecases_count').setValue(3)",
+ "validateBeforeDeactivate()"
+ ]), Expected: "false" });
+
+testSettings:
+ filePath: ./testSettings.yaml
+
+environmentVariables:
+ users:
+ - personaName: User1
+ emailKey: user1Email
+ passwordKey: NotNeeded
\ No newline at end of file
diff --git a/samples/javascript-d365-tests/formScripts.js b/samples/javascript-d365-tests/formScripts.js
new file mode 100644
index 000000000..b5984dddc
--- /dev/null
+++ b/samples/javascript-d365-tests/formScripts.js
@@ -0,0 +1,94 @@
+/**
+ * formScripts.js - Common form script functions for Dynamics 365
+ * These functions would be registered as event handlers for form events.
+ */
+
+/**
+ * Handles the form onLoad event
+ * @param {object} executionContext - The execution context
+ * @returns {boolean} - True if operation was successful
+ */
+function onLoad(executionContext) {
+ // Set default values for new forms
+ var formType = Xrm.Page.ui.getFormType();
+
+ // Form type 1 = Create
+ if (formType === 1) {
+ var countryAttribute = Xrm.Page.getAttribute("address1_country");
+ if (countryAttribute && !countryAttribute.getValue()) {
+ countryAttribute.setValue("United States");
+ }
+ }
+
+ // Set up field event handlers
+ setupFieldEventHandlers();
+
+ return true;
+}
+
+/**
+ * Set up event handlers for various fields
+ * @returns {boolean} - True if operation was successful
+ */
+function setupFieldEventHandlers() {
+ var creditLimitAttribute = Xrm.Page.getAttribute("creditlimit");
+ if (creditLimitAttribute) {
+ creditLimitAttribute.addOnChange(creditLimitOnChange);
+ }
+
+ var customerTypeAttribute = Xrm.Page.getAttribute("customertype");
+ if (customerTypeAttribute) {
+ customerTypeAttribute.addOnChange(customerTypeOnChange);
+ }
+
+ return true;
+}
+
+/**
+ * Handles changes to the credit limit field
+ * @returns {boolean} - True if operation was successful
+ */
+function creditLimitOnChange() {
+ var creditLimit = Xrm.Page.getAttribute("creditlimit").getValue();
+ var creditScoreAttribute = Xrm.Page.getAttribute("creditscore");
+
+ if (creditLimit && creditLimit > 10000) {
+ // For high credit limits, ensure credit score is recorded
+ if (creditScoreAttribute) {
+ creditScoreAttribute.setRequiredLevel("required");
+ }
+ } else {
+ // For normal credit limits, credit score is recommended
+ if (creditScoreAttribute) {
+ creditScoreAttribute.setRequiredLevel("recommended");
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Handles changes to the customer type field
+ * @returns {boolean} - True if operation was successful
+ */
+function customerTypeOnChange() {
+ var customerType = Xrm.Page.getAttribute("customertype").getValue();
+
+ // Show/hide tabs based on customer type
+ var partnerTab = Xrm.Page.ui.tabs.get("partnertab");
+ var consumerTab = Xrm.Page.ui.tabs.get("tab_consumer");
+
+ if (customerType === 2) { // 2 = Partner
+ if (partnerTab) partnerTab.setVisible(true);
+ if (consumerTab) consumerTab.setVisible(false);
+ } else if (customerType === 1) { // 1 = Consumer
+ if (partnerTab) partnerTab.setVisible(false);
+ if (consumerTab) consumerTab.setVisible(true);
+ } else {
+ // Default case - show both tabs
+ if (partnerTab) partnerTab.setVisible(true);
+ if (consumerTab) consumerTab.setVisible(true);
+ }
+
+ return true;
+}
diff --git a/samples/javascript-d365-tests/formScripts.te.yaml b/samples/javascript-d365-tests/formScripts.te.yaml
new file mode 100644
index 000000000..57e978219
--- /dev/null
+++ b/samples/javascript-d365-tests/formScripts.te.yaml
@@ -0,0 +1,50 @@
+# yaml-embedded-languages: powerfx
+testSuite:
+ testSuiteName: Form Script Tests
+ testSuiteDescription: Tests for Dynamics 365 form script functionality using PowerFx format
+ persona: User1
+ appLogicalName: na
+
+ testCases:
+ - testCaseName: FormScripts_DefaultCountryForNewForms
+ testCaseDescription: Tests that default country is set for new forms
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"formScripts.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.ui.formType = 1",
+ "onLoad()",
+ "Xrm.Page.getAttribute('address1_country').getValue() === 'United States'"
+ ]), Expected: "true" });
+ - testCaseName: FormScripts_UnchangedCountryForExistingForms
+ testCaseDescription: Tests that country is not changed for existing forms
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"formScripts.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.ui.formType = 2",
+ "Xrm.Page.getAttribute('address1_country').setValue('Canada')",
+ "onLoad()",
+ "Xrm.Page.getAttribute('address1_country').getValue() === 'Canada'"
+ ]), Expected: "true" });
+ - testCaseName: FormScripts_CreditLimitRequiresScoreForHighLimit
+ testCaseDescription: Tests that credit score is required for high credit limits
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"formScripts.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('creditlimit').setValue(100000)",
+ "creditLimitOnChange()",
+ "Xrm.Page.getControl('creditscore').getRequiredLevel() === 'required'"
+ ]), Expected: "true" });
+ - testCaseName: FormScripts_CustomerTypeShowsPartnerTab
+ testCaseDescription: Tests that partner tab is shown for partner customer type
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"formScripts.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('customertype').setValue(2)",
+ "customerTypeOnChange()",
+ "Xrm.Page.ui.tabs.get('partnertab').getVisible()"
+ ]), Expected: "true" });
+
+testSettings:
+ filePath: ./testSettings.yaml
+
+environmentVariables:
+ users:
+ - personaName: User1
+ emailKey: user1Email
+ passwordKey: NotNeeded
diff --git a/samples/javascript-d365-tests/mockXrm.js b/samples/javascript-d365-tests/mockXrm.js
new file mode 100644
index 000000000..0014d3b7d
--- /dev/null
+++ b/samples/javascript-d365-tests/mockXrm.js
@@ -0,0 +1,181 @@
+/**
+ * mockXrm.js - Mock implementation of Dynamics 365 Xrm object for testing
+ * This file provides a simple mock of the Xrm object to enable testing of client-side scripts
+ */
+
+// Initialize global Xrm object
+var Xrm = {
+ Page: {
+ ui: {
+ formType: 2, // Default to Update form type
+ tabs: {
+ items: {},
+ get: function(tabName) {
+ if (!this.items[tabName]) {
+ this.items[tabName] = {
+ visible: true,
+ setVisible: function(visible) { this.visible = visible; },
+ getVisible: function() { return this.visible; }
+ };
+ }
+ return this.items[tabName];
+ }
+ },
+ sections: {
+ items: {},
+ get: function(sectionName) {
+ if (!this.items[sectionName]) {
+ this.items[sectionName] = {
+ visible: true,
+ setVisible: function(visible) { this.visible = visible; },
+ getVisible: function() { return this.visible; }
+ };
+ }
+ return this.items[sectionName];
+ }
+ },
+ controls: {},
+ getFormType: function() { return this.formType; },
+ setFormType: function(type) { this.formType = type; }
+ },
+
+ data: {
+ entity: {
+ attributes: {}
+ }
+ },
+
+ getAttribute: function(attributeName) {
+ if (!this.attributes) {
+ this.attributes = {};
+ }
+
+ if (!this.attributes[attributeName]) {
+ this.attributes[attributeName] = {
+ value: null,
+ requiredLevel: "none",
+ handlers: [],
+ getValue: function() { return this.value; },
+ setValue: function(value) {
+ this.value = value;
+ // Call registered handlers when value is changed
+ this.handlers.forEach(function(handler) {
+ handler();
+ });
+ },
+ setRequiredLevel: function(level) { this.requiredLevel = level; },
+ getRequiredLevel: function() { return this.requiredLevel; },
+ addOnChange: function(handler) { this.handlers.push(handler); }
+ };
+ }
+
+ return this.attributes[attributeName];
+ },
+
+ getControl: function(controlName) {
+ if (!this.ui.controls[controlName]) {
+ this.ui.controls[controlName] = {
+ visible: true,
+ notification: null,
+ setVisible: function(visible) { this.visible = visible; },
+ getVisible: function() { return this.visible; },
+ setNotification: function(message, id) { this.notification = { message: message, id: id }; },
+ clearNotification: function(id) {
+ if (this.notification && this.notification.id === id) {
+ this.notification = null;
+ }
+ },
+ getNotification: function() { return this.notification; }
+ };
+ }
+
+ return this.ui.controls[controlName];
+ }
+ },
+
+ Utility: {
+ alertDialog: function(message, callback) {
+ console.log("Alert Dialog: " + message);
+ if (callback) callback();
+ return true;
+ },
+ confirmDialog: function(message, callback) {
+ console.log("Confirm Dialog: " + message);
+ if (callback) callback(true); // Always confirm in test
+ return true;
+ }
+ },
+
+ WebApi: {
+ online: true,
+ execute: function(request) {
+ console.log("WebApi.execute called with:", request);
+ // Mock implementation returns a resolved promise
+ return Promise.resolve({
+ ok: true,
+ json: function() {
+ return Promise.resolve({
+ value: "Mock API response"
+ });
+ }
+ });
+ },
+ retrieveMultipleRecords: function(entityName, options, maxPageSize) {
+ console.log("WebApi.retrieveMultipleRecords called for:", entityName);
+ // Mock implementation returns a resolved promise with empty result set
+ return Promise.resolve({
+ entities: []
+ });
+ }
+ }
+};
+
+// Add attributes to support testing
+Xrm.Page.getAttribute("accountstatus").setValue(1); // Default to active
+Xrm.Page.getAttribute("activecases_count").setValue(0); // Default to no active cases
+Xrm.Page.getAttribute("accounttype").setValue(0); // Default to no specific type
+Xrm.Page.getAttribute("industrycode").setValue(0); // Default to no specific industry
+Xrm.Page.getAttribute("creditlimit").setValue(5000); // Default credit limit
+Xrm.Page.getAttribute("creditscore").setValue(650); // Default credit score
+Xrm.Page.getAttribute("customertype").setValue(0); // Default customer type
+
+// Create mock implementation for dialog display used in recommendations
+window.showDialogResponse = true;
+window.showModalDialog = function(url, args, options) {
+ console.log("Show Modal Dialog called with:", args);
+ return window.showDialogResponse;
+};
+
+// Functions for testing
+function resetMockXrm() {
+ Xrm.Page.ui.formType = 2;
+ Xrm.Page.getAttribute("accountstatus").setValue(1);
+ Xrm.Page.getAttribute("activecases_count").setValue(0);
+ Xrm.Page.getAttribute("accounttype").setValue(0);
+ Xrm.Page.getAttribute("industrycode").setValue(0);
+ Xrm.Page.getAttribute("creditlimit").setValue(5000);
+ Xrm.Page.getAttribute("creditscore").setValue(650);
+ Xrm.Page.getAttribute("customertype").setValue(0);
+ window.showDialogResponse = true;
+}
+
+// Add fetch xml support
+Xrm.WebApi.retrieveRecords = function(entityType, options) {
+ console.log("WebApi.retrieveRecords called for:", entityType);
+ // Mock implementation returns a resolved promise with empty result set
+ return Promise.resolve({
+ entities: []
+ });
+};
+
+// Add support for form context rather than just global form
+Xrm.Page.context = {
+ getClientUrl: function() {
+ return "https://mock.crm.dynamics.com";
+ }
+};
+
+// Add form event registration capability
+Xrm.Page.data.entity.addOnSave = function(handler) {
+ // Just store the handler - not implemented for tests
+};
\ No newline at end of file
diff --git a/samples/javascript-d365-tests/testSettings.yaml b/samples/javascript-d365-tests/testSettings.yaml
new file mode 100644
index 000000000..68d4ae69c
--- /dev/null
+++ b/samples/javascript-d365-tests/testSettings.yaml
@@ -0,0 +1,12 @@
+locale: "en-US"
+recordVideo: true
+browserConfigurations:
+ - browser: Chromium
+powerFxTestTypes:
+ - name: TextCollection
+ value: |
+ [{Value: Text}]
+testFunctions:
+ - description: Join lines of text with a semicolon
+ code: |
+ Join(lines: TextCollection): Text = Concat(lines, Value, ";")
\ No newline at end of file
diff --git a/samples/javascript-d365-tests/validation.js b/samples/javascript-d365-tests/validation.js
new file mode 100644
index 000000000..9010563f1
--- /dev/null
+++ b/samples/javascript-d365-tests/validation.js
@@ -0,0 +1,94 @@
+/**
+ * validation.js - Field validation functions for Dynamics 365
+ * These functions would be used to validate form data.
+ */
+
+/**
+ * Validates a phone number format
+ * @param {string} phoneNumber - The phone number to validate
+ * @returns {boolean} - True if valid, false otherwise
+ */
+function validatePhoneNumber(phoneNumber) {
+ if (!phoneNumber) return true; // Empty is valid
+
+ // Simple validation - must be at least 10 digits
+ var digitsOnly = phoneNumber.replace(/\D/g, '');
+ return digitsOnly.length >= 10;
+}
+
+/**
+ * Validates an email address format
+ * @param {string} email - The email to validate
+ * @returns {boolean} - True if valid, false otherwise
+ */
+function validateEmail(email) {
+ if (!email) return true; // Empty is valid
+
+ // Simple validation - must have @ and .
+ return email.includes('@') && email.includes('.') && email.indexOf('@') < email.lastIndexOf('.');
+}
+
+/**
+ * Validates credit limit based on credit score
+ * @param {number} creditLimit - The credit limit amount
+ * @param {number} creditScore - The credit score
+ * @returns {boolean} - True if valid, false otherwise
+ */
+function validateCreditLimit(creditLimit, creditScore) {
+ if (!creditLimit) return true; // Empty is valid
+
+ // Based on credit score, enforce maximum credit limit
+ if (creditScore < 500) {
+ return creditLimit <= 1000;
+ } else if (creditScore < 600) {
+ return creditLimit <= 5000;
+ } else if (creditScore < 700) {
+ return creditLimit <= 25000;
+ } else {
+ // Good credit - no specific validation
+ return true;
+ }
+}
+
+/**
+ * Performs validation for all fields on the account form
+ * @returns {boolean} - True if all validations pass, false otherwise
+ */
+function validateAccountForm() {
+ var isValid = true;
+
+ // Validate phone
+ var phone = Xrm.Page.getAttribute("telephone1").getValue();
+ var phoneControl = Xrm.Page.getControl("telephone1");
+
+ if (!validatePhoneNumber(phone)) {
+ phoneControl.setNotification("Phone number must have at least 10 digits", "phone_validation");
+ isValid = false;
+ } else {
+ phoneControl.clearNotification("phone_validation");
+ }
+
+ // Validate email
+ var email = Xrm.Page.getAttribute("emailaddress1").getValue();
+ var emailControl = Xrm.Page.getControl("emailaddress1");
+
+ if (!validateEmail(email)) {
+ emailControl.setNotification("Please enter a valid email address", "email_validation");
+ isValid = false;
+ } else {
+ emailControl.clearNotification("email_validation");
+ }
+
+ // Validate credit limit
+ var creditLimit = Xrm.Page.getAttribute("creditlimit").getValue();
+ var creditScore = Xrm.Page.getAttribute("creditscore").getValue() || 0;
+ var creditLimitControl = Xrm.Page.getControl("creditlimit");
+
+ if (!validateCreditLimit(creditLimit, creditScore)) {
+ creditLimitControl.setNotification("Credit limit too high for current credit score", "creditlimit_validation");
+ isValid = false; } else {
+ creditLimitControl.clearNotification("creditlimit_validation");
+ }
+
+ return isValid;
+}
diff --git a/samples/javascript-d365-tests/validation.te.yaml b/samples/javascript-d365-tests/validation.te.yaml
new file mode 100644
index 000000000..ecf01537f
--- /dev/null
+++ b/samples/javascript-d365-tests/validation.te.yaml
@@ -0,0 +1,62 @@
+# yaml-embedded-languages: powerfx
+testSuite:
+ testSuiteName: Validation Tests
+ testSuiteDescription: Tests for Dynamics 365 validation functionality using PowerFx format
+ persona: User1
+ appLogicalName: na
+
+ testCases:
+ - testCaseName: Validation_PhoneNumberAcceptsValidFormats
+ testCaseDescription: Tests that phone validation passes for valid formats
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"validation.js", Setup:"mockXrm.js", Run: Join([
+ "validatePhoneNumber('(555) 123-4567')"
+ ]), Expected: "true" });
+ - testCaseName: Validation_PhoneNumberRejectsInvalidFormats
+ testCaseDescription: Tests that phone validation fails for invalid formats
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"validation.js", Setup:"mockXrm.js", Run: Join([
+ "validatePhoneNumber('not-a-phone')"
+ ]), Expected: "false" });
+ - testCaseName: Validation_EmailValidationWithValidEmails
+ testCaseDescription: Tests that email validation passes for valid emails
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"validation.js", Setup:"mockXrm.js", Run: Join([
+ "validateEmail('test@example.com')"
+ ]), Expected: "true" });
+ - testCaseName: Validation_EmailValidationWithInvalidEmails
+ testCaseDescription: Tests that email validation fails for invalid emails
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"validation.js", Setup:"mockXrm.js", Run: Join([
+ "validateEmail('invalid-email')"
+ ]), Expected: "false" });
+ - testCaseName: Validation_CreditLimitValidationBasedOnScore
+ testCaseDescription: Tests that credit limit validation is based on credit score
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"validation.js", Setup:"mockXrm.js", Run: Join([
+ "validateCreditLimit(3000, 550)"
+ ]), Expected: "true" });
+ - testCaseName: Validation_CreditLimitValidationFailsWhenTooHigh
+ testCaseDescription: Tests that credit limit validation fails when limit is too high for score
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"validation.js", Setup:"mockXrm.js", Run: Join([
+ "validateCreditLimit(10000, 450)"
+ ]), Expected: "false" });
+ - testCaseName: Validation_FormValidationSetsNotifications
+ testCaseDescription: Tests that form validation sets notifications for invalid fields
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"validation.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('emailaddress1').setValue('invalid-email')",
+ "Xrm.Page.getAttribute('telephone1').setValue('invalid-phone')",
+ "validateAccountForm()",
+ "Xrm.Page.getControl('emailaddress1').getNotification() !== null && Xrm.Page.getControl('telephone1').getNotification() !== null"
+ ]), Expected: "true" });
+
+testSettings:
+ filePath: ./testSettings.yaml
+
+environmentVariables:
+ users:
+ - personaName: User1
+ emailKey: user1Email
+ passwordKey: NotNeeded
diff --git a/samples/javascript-d365-tests/visibility.js b/samples/javascript-d365-tests/visibility.js
new file mode 100644
index 000000000..5d7c60794
--- /dev/null
+++ b/samples/javascript-d365-tests/visibility.js
@@ -0,0 +1,106 @@
+/**
+ * visibility.js - Form visibility functions for Dynamics 365
+ * These functions are used to control field, section, and tab visibility.
+ */
+
+/**
+ * Updates visibility of fields and sections based on account type
+ * @returns {boolean} - True if operation was successful
+ */
+function updateAccountTypeVisibility() {
+ var accountType = Xrm.Page.getAttribute("accounttype").getValue();
+
+ // Show/hide fields based on account type
+ if (accountType === 1) { // 1 = Customer
+ // Customer-specific fields
+ showHideControl("customerid", true);
+ showHideControl("creditlimit", true);
+ showHideSection("customer_section", true);
+
+ // Hide vendor fields
+ showHideControl("vendorid", false);
+ showHideSection("vendor_section", false);
+ } else if (accountType === 2) { // 2 = Vendor
+ // Vendor-specific fields
+ showHideControl("vendorid", true);
+ showHideControl("vendorrating", true);
+ showHideSection("vendor_section", true);
+
+ // Hide customer fields
+ showHideControl("customerid", false);
+ showHideControl("creditlimit", false);
+ showHideSection("customer_section", false);
+ } else {
+ // Default - hide all special sections
+ showHideControl("customerid", false);
+ showHideControl("vendorid", false);
+ showHideSection("customer_section", false);
+ showHideSection("vendor_section", false);
+ }
+
+ return true;
+}
+
+/**
+ * Updates visibility based on the selected industry
+ * @returns {boolean} - True if operation was successful
+ */
+function updateIndustryBasedVisibility() {
+ var industryCode = Xrm.Page.getAttribute("industrycode").getValue();
+
+ // Show financial tab for financial industry (code 1)
+ if (industryCode === 1) {
+ showHideTab("financial", true);
+ showHideSection("compliance", true);
+ } else {
+ showHideTab("financial", false);
+ showHideSection("compliance", false);
+ }
+
+ return true;
+}
+
+/**
+ * Shows or hides a field control
+ * @param {string} controlName - Name of the control to show/hide
+ * @param {boolean} visible - True to show, false to hide
+ * @returns {boolean} - True if operation was successful
+ */
+function showHideControl(controlName, visible) {
+ var control = Xrm.Page.getControl(controlName);
+ if (control) {
+ control.setVisible(visible);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Shows or hides a section
+ * @param {string} sectionName - Name of the section to show/hide
+ * @param {boolean} visible - True to show, false to hide
+ * @returns {boolean} - True if operation was successful
+ */
+function showHideSection(sectionName, visible) {
+ var section = Xrm.Page.ui.sections.get(sectionName);
+ if (section) {
+ section.setVisible(visible);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Shows or hides a tab
+ * @param {string} tabName - Name of the tab to show/hide
+ * @param {boolean} visible - True to show, false to hide
+ * @returns {boolean} - True if operation was successful
+ */
+function showHideTab(tabName, visible) {
+ var tab = Xrm.Page.ui.tabs.get(tabName);
+ if (tab) {
+ tab.setVisible(visible);
+ return true;
+ }
+ return false;
+}
diff --git a/samples/javascript-d365-tests/visibility.te.yaml b/samples/javascript-d365-tests/visibility.te.yaml
new file mode 100644
index 000000000..19304cf4b
--- /dev/null
+++ b/samples/javascript-d365-tests/visibility.te.yaml
@@ -0,0 +1,64 @@
+# yaml-embedded-languages: powerfx
+testSuite:
+ testSuiteName: Visibility Tests
+ testSuiteDescription: Tests for Dynamics 365 visibility functionality using PowerFx format
+ persona: User1
+ appLogicalName: na
+
+ testCases:
+ - testCaseName: Visibility_CustomerFieldsAndSectionsForCustomerType
+ testCaseDescription: Tests that customer fields and sections are shown for customer account type
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"visibility.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('accounttype').setValue(1)",
+ "updateAccountTypeVisibility()",
+ "Xrm.Page.getControl('customerid').getVisible() && Xrm.Page.ui.sections.get('customer_section').getVisible()"
+ ]), Expected: "true" });
+ - testCaseName: Visibility_VendorFieldsAndSectionsForVendorType
+ testCaseDescription: Tests that vendor fields and sections are shown for vendor account type
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"visibility.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('accounttype').setValue(2)",
+ "updateAccountTypeVisibility()",
+ "Xrm.Page.getControl('vendorid').getVisible() && Xrm.Page.ui.sections.get('vendor_section').getVisible()"
+ ]), Expected: "true" });
+ - testCaseName: Visibility_HideAllSectionsForDefaultType
+ testCaseDescription: Tests that all special sections are hidden for default account type
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"visibility.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('accounttype').setValue(0)",
+ "updateAccountTypeVisibility()",
+ "!Xrm.Page.getControl('customerid').getVisible() && !Xrm.Page.ui.sections.get('customer_section').getVisible() && !Xrm.Page.getControl('vendorid').getVisible() && !Xrm.Page.ui.sections.get('vendor_section').getVisible()"
+ ]), Expected: "true" });
+ - testCaseName: Visibility_FinancialTabForFinancialIndustry
+ testCaseDescription: Tests that financial tab is shown for financial industry
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"visibility.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('industrycode').setValue(1)",
+ "updateIndustryBasedVisibility()",
+ "Xrm.Page.ui.tabs.get('financial').getVisible() && Xrm.Page.ui.sections.get('compliance').getVisible()"
+ ]), Expected: "true" });
+ - testCaseName: Visibility_HideFinancialTabForNonFinancialIndustry
+ testCaseDescription: Tests that financial tab is hidden for non-financial industry
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"visibility.js", Setup:"mockXrm.js", Run: Join([
+ "Xrm.Page.getAttribute('industrycode').setValue(2)",
+ "updateIndustryBasedVisibility()",
+ "!Xrm.Page.ui.tabs.get('financial').getVisible() && !Xrm.Page.ui.sections.get('compliance').getVisible()"
+ ]), Expected: "true" });
+ - testCaseName: Visibility_IndividualControlVisibility
+ testCaseDescription: Tests that individual control visibility function works
+ testSteps: |
+ = Preview.AssertJavaScript({Location:"visibility.js", Setup:"mockXrm.js", Run: Join([
+ "showHideControl('customcontrol', true)",
+ "Xrm.Page.getControl('customcontrol').getVisible()"
+ ]), Expected: "true" });
+
+testSettings:
+ filePath: ./testSettings.yaml
+
+environmentVariables:
+ users:
+ - personaName: User1
+ emailKey: user1Email
+ passwordKey: NotNeeded
diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Microsoft.PowerApps.TestEngine.Tests.csproj b/src/Microsoft.PowerApps.TestEngine.Tests/Microsoft.PowerApps.TestEngine.Tests.csproj
index c06667317..782c6cc2e 100644
--- a/src/Microsoft.PowerApps.TestEngine.Tests/Microsoft.PowerApps.TestEngine.Tests.csproj
+++ b/src/Microsoft.PowerApps.TestEngine.Tests/Microsoft.PowerApps.TestEngine.Tests.csproj
@@ -18,7 +18,7 @@
-
+
diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/AssertJavaScriptFunctionTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/AssertJavaScriptFunctionTests.cs
new file mode 100644
index 000000000..ebef20bbe
--- /dev/null
+++ b/src/Microsoft.PowerApps.TestEngine.Tests/PowerFx/Functions/AssertJavaScriptFunctionTests.cs
@@ -0,0 +1,581 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.PowerApps.TestEngine.Config;
+using Microsoft.PowerApps.TestEngine.PowerFx.Functions;
+using Microsoft.PowerApps.TestEngine.System;
+using Microsoft.PowerApps.TestEngine.Tests.Helpers;
+using Microsoft.PowerFx.Types;
+using Microsoft.Xrm.Sdk;
+using Microsoft.Xrm.Sdk.Query;
+using Moq;
+using Xunit;
+
+namespace Microsoft.PowerApps.TestEngine.Tests.PowerFx.Functions
+{
+ public class AssertJavaScriptFunctionTests : IDisposable
+ {
+ private readonly Mock _mockLogger;
+ private readonly Mock _mockOrgService;
+ private readonly Mock _mockFileSystem;
+ private readonly Mock _mockTestState;
+ private readonly string _testFilePath;
+ private readonly string _testFileContent; public AssertJavaScriptFunctionTests()
+ {
+ _mockLogger = new Mock(MockBehavior.Strict);
+ _mockOrgService = new Mock(MockBehavior.Strict);
+ _mockFileSystem = new Mock(MockBehavior.Strict);
+ _mockTestState = new Mock(MockBehavior.Loose);
+
+ // Create a temporary JavaScript file for testing
+ _testFilePath = Path.GetTempFileName() + ".js";
+ _testFileContent = "// Test JavaScript file\nfunction testFunction() { return 'hello'; }";
+ File.WriteAllText(_testFilePath, _testFileContent);
+ }
+ [Fact]
+ public async Task ExecuteAsync_WithValidRunAndExpected_ReturnsSuccess()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+ var recordFields = new Dictionary
+ {
+ ["Run"] = StringValue.New("'test' + 'success'"),
+ ["Expected"] = StringValue.New("testsuccess")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+ // Act
+ var result = await function.ExecuteAsync(record);
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.True(GetBooleanFieldValue(result, "Success"));
+ Assert.Equal("Assertion passed", GetStringFieldValue(result, "Message"));
+ Assert.Equal(string.Empty, GetStringFieldValue(result, "Details"));
+ }
+ [Fact]
+ public async Task ExecuteAsync_WithMissingRunParameter_ReturnsError()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+ var recordFields = new Dictionary
+ {
+ ["Expected"] = StringValue.New("testsuccess")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record); // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.False(GetBooleanFieldValue(result, "Success"));
+ Assert.Equal("Missing required parameter", GetStringFieldValue(result, "Message"));
+ Assert.Contains("Run", GetStringFieldValue(result, "Details"));
+ }
+ [Fact]
+ public async Task ExecuteAsync_WithMissingExpectedParameter_ReturnsError()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+
+ var recordFields = new Dictionary
+ {
+ ["Run"] = StringValue.New("'test' + 'success'")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.False(GetBooleanFieldValue(result, "Success"));
+ Assert.Equal("Missing required parameter", GetStringFieldValue(result, "Message"));
+ Assert.Contains("Expected", GetStringFieldValue(result, "Details"));
+ }
+ [Fact]
+ public async Task ExecuteAsync_WithInvalidRunCode_ReturnsError()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+
+ var recordFields = new Dictionary
+ {
+ ["Run"] = StringValue.New("invalidFunction()"),
+ ["Expected"] = StringValue.New("result")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.False(GetBooleanFieldValue(result, "Success"));
+ Assert.Equal("Test code execution failed", GetStringFieldValue(result, "Message"));
+ Assert.Contains("Error", GetStringFieldValue(result, "Details"));
+ }
+ [Fact]
+ public async Task ExecuteAsync_WithSetupCode_ExecutesCorrectly()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+ var recordFields = new Dictionary
+ {
+ ["Setup"] = StringValue.New("var x = 10; var y = 5;"),
+ ["Run"] = StringValue.New("x + y"),
+ ["Expected"] = StringValue.New("15")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.True(GetBooleanFieldValue(result, "Success"));
+ }
+ [Fact]
+ public async Task ExecuteAsync_WithInvalidSetupCode_ReturnsError()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+
+ var recordFields = new Dictionary
+ {
+ ["Setup"] = StringValue.New("var x = nonExistentFunction();"),
+ ["Run"] = StringValue.New("10"),
+ ["Expected"] = StringValue.New("10")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.False(GetBooleanFieldValue(result, "Success"));
+ Assert.Equal("Setup code execution failed", GetStringFieldValue(result, "Message"));
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithLocalFile_ExecutesCorrectly()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+
+ // Setup mock file system
+ _mockFileSystem.Setup(fs => fs.FileExists(_testFilePath)).Returns(true); _mockFileSystem.Setup(fs => fs.ReadAllText(_testFilePath)).Returns(_testFileContent);
+
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+
+ var recordFields = new Dictionary
+ {
+ ["Location"] = StringValue.New(_testFilePath),
+ ["Run"] = StringValue.New("testFunction()"),
+ ["Expected"] = StringValue.New("hello")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.True(GetBooleanFieldValue(result, "Success"));
+
+ // Verify file system was called
+ _mockFileSystem.Verify(fs => fs.FileExists(_testFilePath), Times.Once);
+ _mockFileSystem.Verify(fs => fs.ReadAllText(_testFilePath), Times.Once);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithNonExistentFile_ReturnsError()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+
+ // Setup mock file system
+ string nonExistentPath = "C:/nonexistent/file.js";
+ _mockFileSystem.Setup(fs => fs.FileExists(nonExistentPath)).Returns(false);
+
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+
+ var recordFields = new Dictionary
+ {
+ ["Location"] = StringValue.New(nonExistentPath),
+ ["Run"] = StringValue.New("10"),
+ ["Expected"] = StringValue.New("10")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.False(GetBooleanFieldValue(result, "Success"));
+ Assert.Equal("File error", GetStringFieldValue(result, "Message"));
+
+ // Verify file system was called
+ _mockFileSystem.Verify(fs => fs.FileExists(nonExistentPath), Times.Once);
+ _mockFileSystem.Verify(fs => fs.ReadAllText(nonExistentPath), Times.Never);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithWebResource_ExecutesCorrectly()
+ { // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+ // Setup mock org service to return a web resource
+ var webResourceContent = Convert.ToBase64String(global::System.Text.Encoding.UTF8.GetBytes(
+ "function webResourceFunction() { return 'from webresource'; }"));
+
+ var mockEntity = new Entity("webresource");
+ mockEntity["content"] = webResourceContent;
+ var mockEntityCollection = new EntityCollection();
+ mockEntityCollection.Entities.Add(mockEntity); // Create query expression variable for Moq setup
+ _mockOrgService.Setup(s => s.RetrieveMultiple(It.Is(q =>
+ q.EntityName == "webresource" &&
+ q.ColumnSet.Columns.Contains("content") && q.ColumnSet.Columns.Contains("name"))))
+ .Returns(mockEntityCollection);
+
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, _mockOrgService.Object, _mockFileSystem.Object, _mockTestState.Object);
+
+ var recordFields = new Dictionary
+ {
+ ["WebResource"] = StringValue.New("new_testscript.js"),
+ ["Run"] = StringValue.New("webResourceFunction()"),
+ ["Expected"] = StringValue.New("from webresource")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record); // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.True(GetBooleanFieldValue(result, "Success"));
+
+ // Verify service was called with the proper query expression
+ _mockOrgService.Verify(s => s.RetrieveMultiple(It.Is(q =>
+ q.EntityName == "webresource" &&
+ q.ColumnSet.Columns.Contains("content") &&
+ q.ColumnSet.Columns.Contains("name"))), Times.Once);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithNonExistentWebResource_ReturnsError()
+ { // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+
+ // Setup mock org service to return empty collection
+ _mockOrgService.Setup(s => s.RetrieveMultiple(It.Is(q =>
+ q.EntityName == "webresource" &&
+ q.ColumnSet.Columns.Contains("content") && q.ColumnSet.Columns.Contains("name"))))
+ .Returns(new EntityCollection());
+
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, _mockOrgService.Object, _mockFileSystem.Object, _mockTestState.Object);
+
+ var recordFields = new Dictionary
+ {
+ ["WebResource"] = StringValue.New("nonexistent.js"),
+ ["Run"] = StringValue.New("10"),
+ ["Expected"] = StringValue.New("10")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.False(GetBooleanFieldValue(result, "Success"));
+ Assert.Equal("Web resource error", GetStringFieldValue(result, "Message"));
+ }
+ [Fact]
+ public async Task ExecuteAsync_WithFailingAssertion_ReturnsFailure()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+
+ var recordFields = new Dictionary
+ {
+ ["Run"] = StringValue.New("'actual'"),
+ ["Expected"] = StringValue.New("expected")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.False(GetBooleanFieldValue(result, "Success"));
+ Assert.Equal("Assertion failed", GetStringFieldValue(result, "Message"));
+ Assert.Contains("Expected 'expected', got 'actual'", GetStringFieldValue(result, "Details"));
+ }
+ [Fact]
+ public async Task ExecuteAsync_WithComplexJavaScript_ExecutesCorrectly()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+ var complexJs = @"
+ var numbers = [1, 2, 3, 4, 5];
+ var sum = numbers.reduce(function(a, b) { return a + b; }, 0);
+ var doubled = numbers.map(function(n) { return n * 2; });
+ doubled.join(',')
+ ";
+
+ var recordFields = new Dictionary
+ {
+ ["Run"] = StringValue.New(complexJs),
+ ["Expected"] = StringValue.New("2,4,6,8,10")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.True(GetBooleanFieldValue(result, "Success"));
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_FileSystemUsage_VerifyMockCalls()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+
+ string testPath = "C:/test/script.js";
+ string testContent = "function testMock() { return 'mocked content'; }";
+
+ // Setup detailed mock expectations
+ _mockFileSystem.Setup(fs => fs.FileExists(testPath)).Returns(true);
+ _mockFileSystem.Setup(fs => fs.ReadAllText(testPath)).Returns(testContent);
+ _mockFileSystem.Setup(fs => fs.CanAccessFilePath(testPath)).Returns(true);
+
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+
+ var recordFields = new Dictionary
+ {
+ ["Location"] = StringValue.New(testPath),
+ ["Run"] = StringValue.New("testMock()"),
+ ["Expected"] = StringValue.New("mocked content")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.True(GetBooleanFieldValue(result, "Success"));
+
+ // Verify that file system methods were called with expected parameters
+ _mockFileSystem.Verify(fs => fs.FileExists(testPath), Times.Once);
+ _mockFileSystem.Verify(fs => fs.ReadAllText(testPath), Times.Once);
+ // Verify that other file system methods were NOT called
+ // We can't use It.IsAny for methods with optional parameters in expression trees
+ // so we need to specify all parameters explicitly
+ _mockFileSystem.Verify(fs => fs.WriteTextToFile(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
+ _mockFileSystem.Verify(fs => fs.Delete(It.IsAny()), Times.Never);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithFileSystemError_HandlesGracefully()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+
+ string testPath = "C:/test/error.js";
+
+ // Setup mock to throw an exception when reading the file
+ _mockFileSystem.Setup(fs => fs.FileExists(testPath)).Returns(true);
+ _mockFileSystem.Setup(fs => fs.ReadAllText(testPath)).Throws(new IOException("Simulated file error"));
+
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+
+ var recordFields = new Dictionary
+ {
+ ["Location"] = StringValue.New(testPath),
+ ["Run"] = StringValue.New("var x = 10;"),
+ ["Expected"] = StringValue.New("10")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.False(GetBooleanFieldValue(result, "Success"));
+ Assert.Equal("File error", GetStringFieldValue(result, "Message"));
+
+ // Verify that methods were called correctly
+ _mockFileSystem.Verify(fs => fs.FileExists(testPath), Times.Once);
+ _mockFileSystem.Verify(fs => fs.ReadAllText(testPath), Times.Once);
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithRelativePathInSetup_ResolvesCorrectly()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+
+ // Setup mock test state with test config file
+ var mockTestConfigFile = new FileInfo(Path.Combine(Path.GetTempPath(), "test_config.yaml"));
+ _mockTestState.Setup(ts => ts.GetTestConfigFile()).Returns(mockTestConfigFile);
+
+ // Create a test JS file in the same directory as the mock test config
+ var relativeJsFile = "setup_script.js";
+ var fullJsFilePath = Path.Combine(Path.GetTempPath(), relativeJsFile);
+ var setupContent = "var setupVar = 'from_setup_file';";
+
+ try
+ {
+ File.WriteAllText(fullJsFilePath, setupContent);
+
+ // Setup mock file system
+ _mockFileSystem.Setup(fs => fs.FileExists(fullJsFilePath)).Returns(true);
+ _mockFileSystem.Setup(fs => fs.ReadAllText(fullJsFilePath)).Returns(setupContent);
+
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+
+ var recordFields = new Dictionary
+ {
+ ["Setup"] = StringValue.New(relativeJsFile),
+ ["Run"] = StringValue.New("setupVar"),
+ ["Expected"] = StringValue.New("from_setup_file")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.True(GetBooleanFieldValue(result, "Success"));
+
+ // Verify file system was called with the correct full path
+ _mockFileSystem.Verify(fs => fs.FileExists(fullJsFilePath), Times.Once);
+ _mockFileSystem.Verify(fs => fs.ReadAllText(fullJsFilePath), Times.Once);
+ }
+ finally
+ {
+ // Cleanup
+ if (File.Exists(fullJsFilePath))
+ {
+ File.Delete(fullJsFilePath);
+ }
+ }
+ }
+
+ [Fact]
+ public async Task ExecuteAsync_WithJavaScriptFileInSetup_LoadsAndExecutesCorrectly()
+ {
+ // Arrange
+ LoggingTestHelper.SetupMock(_mockLogger);
+
+ // Create a temporary JavaScript file for setup code
+ var setupFilePath = Path.GetTempFileName() + ".js";
+ var setupFileContent = "// Setup JavaScript file\nvar setupVar = 42;";
+ File.WriteAllText(setupFilePath, setupFileContent);
+
+ try
+ {
+ // Setup mock file system
+ _mockFileSystem.Setup(fs => fs.FileExists(setupFilePath)).Returns(true);
+ _mockFileSystem.Setup(fs => fs.ReadAllText(setupFilePath)).Returns(setupFileContent);
+
+ var function = new AssertJavaScriptFunction(_mockLogger.Object, null, _mockFileSystem.Object, _mockTestState.Object);
+
+ var recordFields = new Dictionary
+ {
+ ["Setup"] = StringValue.New(setupFilePath),
+ ["Run"] = StringValue.New("setupVar"),
+ ["Expected"] = StringValue.New("42")
+ };
+ var record = RecordValue.NewRecordFromFields(recordFields.Select(kv =>
+ new NamedValue(kv.Key, kv.Value)).ToArray());
+
+ // Act
+ var result = await function.ExecuteAsync(record);
+
+ // Assert
+ Assert.IsAssignableFrom(result);
+ Assert.True(GetBooleanFieldValue(result, "Success"));
+
+ // Verify file system was called with the correct path
+ _mockFileSystem.Verify(fs => fs.FileExists(setupFilePath), Times.Once);
+ _mockFileSystem.Verify(fs => fs.ReadAllText(setupFilePath), Times.Once);
+ }
+ finally
+ {
+ // Cleanup
+ if (File.Exists(setupFilePath))
+ {
+ File.Delete(setupFilePath);
+ }
+ }
+ }
+
+ // Helper methods to extract values from the record
+ private bool GetBooleanFieldValue(RecordValue record, string fieldName)
+ {
+ foreach (var field in record.Fields)
+ {
+ if (field.Name == fieldName && field.Value is BooleanValue boolVal)
+ {
+ return boolVal.Value;
+ }
+ }
+ throw new ArgumentException($"Field {fieldName} not found or not a boolean value");
+ }
+
+ private string GetStringFieldValue(RecordValue record, string fieldName)
+ {
+ foreach (var field in record.Fields)
+ {
+ if (field.Name == fieldName && field.Value is StringValue strVal)
+ {
+ return strVal.Value;
+ }
+ }
+ throw new ArgumentException($"Field {fieldName} not found or not a string value");
+ }
+
+ // Cleanup temp file after tests
+ public void Dispose()
+ {
+ try
+ {
+ if (File.Exists(_testFilePath))
+ {
+ File.Delete(_testFilePath);
+ }
+ }
+ catch (Exception ex)
+ {
+ // Log exception but don't throw during cleanup
+ Console.WriteLine($"Error in cleanup: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlRecordValueTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlRecordValueTests.cs
index cf8840b0a..cf682984d 100644
--- a/src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlRecordValueTests.cs
+++ b/src/Microsoft.PowerApps.TestEngine.Tests/Provider/PowerFXModel/ControlRecordValueTests.cs
@@ -360,17 +360,17 @@ public static IEnumerable