diff --git a/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs b/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs index 8aa246559..1de04fc56 100644 --- a/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs +++ b/src/Microsoft.PowerApps.TestEngine/TestInfra/ITestInfraFunctions.cs @@ -100,6 +100,15 @@ public interface ITestInfraFunctions /// Return value of javascript public Task RunJavascriptAsync(string jsExpression); + /// + /// Runs javascript inside a named frame (bypasses cross-origin restrictions via CDP) + /// + /// Expected return type + /// Javascript expression to run + /// Name of the frame to evaluate in + /// Return value of javascript + public Task RunJavascriptInFrameAsync(string jsExpression, string frameName); + /// /// Triggers a click event on a control /// diff --git a/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs b/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs index 32cedf7ca..d7d332be0 100644 --- a/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs +++ b/src/Microsoft.PowerApps.TestEngine/TestInfra/PlaywrightTestInfraFunctions.cs @@ -493,6 +493,14 @@ public async Task RunJavascriptAsync(string jsExpression) return await Page.EvaluateAsync(jsExpression); } + public async Task RunJavascriptInFrameAsync(string jsExpression, string frameName) + { + ValidatePage(); + _singleTestInstanceState.GetLogger().LogDebug($"Run Javascript in frame '{frameName}': " + jsExpression); + var frame = Page.Frame(frameName); + return await frame.EvaluateAsync(jsExpression); + } + public async Task AddScriptContentAsync(string content) { ValidatePage(); diff --git a/src/PowerAppsTestEngineWrapper/PowerAppsTestEngineWrapper.csproj b/src/PowerAppsTestEngineWrapper/PowerAppsTestEngineWrapper.csproj index 97a9e9da3..4f1c87bc9 100644 --- a/src/PowerAppsTestEngineWrapper/PowerAppsTestEngineWrapper.csproj +++ b/src/PowerAppsTestEngineWrapper/PowerAppsTestEngineWrapper.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net8.0 enable enable True @@ -34,7 +34,7 @@ - + @@ -44,9 +44,7 @@ - - NU1701 - + diff --git a/src/testengine.provider.canvas/JS/PublishedAppTesting.js b/src/testengine.provider.canvas/JS/PublishedAppTesting.js index 06177fd8b..8f9ee8b8d 100644 --- a/src/testengine.provider.canvas/JS/PublishedAppTesting.js +++ b/src/testengine.provider.canvas/JS/PublishedAppTesting.js @@ -5,6 +5,7 @@ function parseControl(controlName, controlObject) { var propertiesList = []; var properties = Object.keys(controlObject.modelProperties); var controls = []; + var galleryChildNames = []; if (controlObject.controlWidget.replicatedContextManager) { var childrenControlContext = controlObject.controlWidget.replicatedContextManager.authoringAreaBindingContext.controlContexts; var childControlNames = Object.keys(childrenControlContext); @@ -12,6 +13,7 @@ function parseControl(controlName, controlObject) { var childControlObject = childrenControlContext[childControlName]; var childControlsList = parseControl(childControlName, childControlObject); controls = controls.concat(childControlsList); + galleryChildNames.push(childControlName); }); } @@ -32,6 +34,18 @@ function parseControl(controlName, controlObject) { properties.forEach((propertyName) => { var propertyType = controlObject.controlWidget.controlProperties[propertyName].propertyType; + // Power Apps runtime returns *[] / ![] for gallery AllItems/Items/Selected/Default + // regardless of the bound data source. Rebuild the type from the template child + // controls extracted above so that Index(Gallery.AllItems, N).ControlName.Prop works. + if (galleryChildNames.length > 0) { + var childTypeStr = galleryChildNames.map(function(n) { return n + ':v'; }).join(', '); + if (propertyType === '*[]' && (propertyName === 'AllItems' || propertyName === 'Items')) { + propertyType = '*[' + childTypeStr + ']'; + } else if (propertyType === '![]' && (propertyName === 'Selected' || propertyName === 'Default')) { + propertyType = '![' + childTypeStr + ']'; + } + } + propertiesList.push({ propertyName: propertyName, propertyType: propertyType }); }) diff --git a/src/testengine.provider.canvas/PowerAppFunctions.cs b/src/testengine.provider.canvas/PowerAppFunctions.cs index ef719720e..c6fb90e9c 100644 --- a/src/testengine.provider.canvas/PowerAppFunctions.cs +++ b/src/testengine.provider.canvas/PowerAppFunctions.cs @@ -127,6 +127,42 @@ private async Task> LoadObjectModelAsyncH if (jsObjectModel != null && jsObjectModel.Controls != null) { + // Patch gallery AllItems/*Selected types: Power Apps runtime reports *[] for + // these regardless of the bound data source. Query child control names directly + // from the app frame (Playwright bypasses cross-origin restrictions via CDP). + foreach (var control in jsObjectModel.Controls) + { + var hasEmptyAllItems = control.Properties.Any(p => p.PropertyName == "AllItems" && p.PropertyType == "*[]"); + if (!hasEmptyAllItems) continue; + try + { + var childNamesJson = await TestInfraFunctions.RunJavascriptInFrameAsync( + $@"(function() {{ + try {{ + var ctx = AppMagic.Controls.GlobalContextManager.bindingContext.controlContexts['{control.Name}']; + if (!ctx || !ctx.controlWidget || !ctx.controlWidget.replicatedContextManager) return '[]'; + return JSON.stringify(Object.keys(ctx.controlWidget.replicatedContextManager.authoringAreaBindingContext.controlContexts)); + }} catch(e) {{ return '[]'; }} + }})()", + PublishedAppIframeName); + var childNames = JsonConvert.DeserializeObject>(childNamesJson ?? "[]"); + if (childNames == null || childNames.Count == 0) continue; + var childTypeStr = string.Join(", ", childNames.Select(n => $"{n}:v")); + SingleTestInstanceState.GetLogger().LogTrace($"Gallery '{control.Name}' children: {childTypeStr}"); + foreach (var prop in control.Properties) + { + if (prop.PropertyType == "*[]" && (prop.PropertyName == "AllItems" || prop.PropertyName == "Items")) + prop.PropertyType = $"*[{childTypeStr}]"; + else if (prop.PropertyType == "![]" && (prop.PropertyName == "Selected" || prop.PropertyName == "Default")) + prop.PropertyType = $"![{childTypeStr}]"; + } + } + catch (Exception ex) + { + SingleTestInstanceState.GetLogger().LogTrace($"Gallery '{control.Name}' child lookup failed: {ex.Message}"); + } + } + SingleTestInstanceState.GetLogger().LogTrace("Listing all skipped properties for each control."); foreach (var control in jsObjectModel.Controls)