diff --git a/samples/pause/Pause_testPlan.fx.yaml b/samples/pause/Pause_testPlan.fx.yaml new file mode 100644 index 000000000..82071c911 --- /dev/null +++ b/samples/pause/Pause_testPlan.fx.yaml @@ -0,0 +1,31 @@ +testSuite: + testSuiteName: Pause Function Tests + testSuiteDescription: Verifies that the Pause function works correctly + persona: User1 + appLogicalName: mda_input_controls_app + + testCases: + - testCaseName: Test Pause Function - Non-Headless Mode + testCaseDescription: Tests that Pause function works when headless is false + testSteps: | + = + Screenshot("before_pause.png"); + Pause(); + Screenshot("after_pause.png"); + Assert(true, "Test continued after Pause function"); + +testSettings: + headless: false + browserConfigurations: + - browser: Chromium + channel: msedge + extensionModules: + enable: true + allowPowerFxNamespaces: + - Preview + +environmentVariables: + users: + - personaName: User1 + emailKey: user1Email + passwordKey: NotNeeded diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineExtensionCheckerTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineExtensionCheckerTests.cs index 1dc12af1f..edd353e8e 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineExtensionCheckerTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineExtensionCheckerTests.cs @@ -308,7 +308,7 @@ public void IsValidUserModule(string allow, string deny, string providerNamespac %CODE%"; [Theory] - [InlineData("Test", "", "public class FooFunction : ReflectionFunction { public FooFunction() : base(\"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", false)] // No namespace + [InlineData("Test", "", "public class FooFunction : ReflectionFunction { public FooFunction() : base(\"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", true)] // TestEngine namespace (default for functions without DPath) [InlineData("Test", "", "public class FooFunction : ReflectionFunction { public FooFunction() : base(DPath.Root, \"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", false)] // Root namespace [InlineData("Test", "", "public class FooFunction : ReflectionFunction { public FooFunction() : base(DPath.Root.Append(new DName(\"Test\")), \"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", true)] // Non Root namespace [InlineData("", "", "public class FooFunction : ReflectionFunction { public FooFunction() : base(DPath.Root.Append(new DName(\"TestEngine\")), \"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", true)] // Allow TestEngine namespace diff --git a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs index 9174d4b20..63beb369e 100644 --- a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs +++ b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs @@ -334,6 +334,12 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s stream.Position = 0; ModuleDefinition module = ModuleDefinition.ReadModule(stream); + // Detect if this assembly contains a provider/user/auth implementation + bool assemblyHasProvider = module.GetAllTypes().Any(t => + t.Interfaces.Any(i => i.InterfaceType.FullName == typeof(Providers.ITestWebProvider).FullName) || + t.Interfaces.Any(i => i.InterfaceType.FullName == typeof(Users.IUserManager).FullName) || + t.Interfaces.Any(i => i.InterfaceType.FullName == typeof(Config.IUserCertificateProvider).FullName)); + // Get the source code of the assembly as will be used to check Power FX Namespaces var code = DecompileModuleToCSharp(assembly); @@ -352,6 +358,12 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s { foreach (var name in values) { + // Ignore Preview namespace for provider loading if not explicitly allowed to avoid blocking provider registration in Release builds. + if (name == NAMESPACE_PREVIEW && !settings.AllowPowerFxNamespaces.Contains(NAMESPACE_PREVIEW)) + { + Logger.LogInformation($"Ignoring Preview namespace on provider {type.Name} (not enabled in allow list)."); + continue; + } // Check against deny list using regular expressions if (settings.DenyPowerFxNamespaces.Any(pattern => Regex.IsMatch(name, WildcardToRegex(pattern)))) { @@ -389,32 +401,24 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s Logger.LogInformation($"No constructor defined for {type.Name}. Found {constructors.Count()} expected 1 or more"); return false; } - - var constructor = constructors.Where(c => c.HasBody).FirstOrDefault(); - - if (constructor == null) + var constructor = constructors.FirstOrDefault(c => c.HasBody); + if (constructor == null || !constructor.HasBody) { - Logger.LogInformation($"No constructor with a body"); + Logger.LogInformation($"No constructor with a body for {type.Name}"); + return false; } - - if (!constructor.HasBody) + if (!constructor.IsPublic) { - Logger.LogInformation($"No body defined for {type.Name}"); - // Needs body for call to base constructor + Logger.LogInformation($"Constructor must be public for {type.Name}"); return false; } - - var baseCall = constructor.Body.Instructions?.FirstOrDefault(i => i.OpCode == OpCodes.Call && i.Operand is MethodReference && ((MethodReference)i.Operand).Name == ".ctor"); - + var baseCall = constructor.Body.Instructions?.FirstOrDefault(i => i.OpCode == OpCodes.Call && i.Operand is MethodReference mr && mr.Name == ".ctor"); if (baseCall == null) { Logger.LogInformation($"No base constructor defined for {type.Name}"); - // Unable to find base constructor call return false; } - - MethodReference baseConstructor = (MethodReference)baseCall.Operand; - + var baseConstructor = (MethodReference)baseCall.Operand; if (baseConstructor.Parameters?.Count() < 2) { // Not enough parameters @@ -422,35 +426,48 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s return false; } - if (baseConstructor.Parameters[0].ParameterType.FullName != "Microsoft.PowerFx.Core.Utils.DPath") + string name; + var hasNamespaceParam = baseConstructor.Parameters[0].ParameterType.FullName == "Microsoft.PowerFx.Core.Utils.DPath"; + if (hasNamespaceParam) { - // First argument should be Namespace - Logger.LogInformation($"No Power FX Namespace for {type.Name}"); - return false; + name = GetPowerFxNamespace(type.Name, code); + if (string.IsNullOrEmpty(name)) + { + Logger.LogInformation($"No Power FX Namespace found for {type.Name}"); + return false; + } + } + else + { + // Root/global function (e.g. Pause). Infer via attribute if needed. + bool isPreview = false, isDeprecated = false; + foreach (var ca in type.CustomAttributes) + { + if (ca.AttributeType.FullName == "Microsoft.PowerApps.TestEngine.Modules.TestEngineFunctionAttribute") + { + foreach (var p in ca.Properties) + { + if (p.Name == "IsPreview" && p.Argument.Value is bool pv) isPreview = pv; + if (p.Name == "IsDeprecated" && p.Argument.Value is bool dp) isDeprecated = dp; + } + } + } + name = isPreview ? NAMESPACE_PREVIEW : isDeprecated ? NAMESPACE_DEPRECATED : NAMESPACE_TEST_ENGINE; } - // Use the decompiled code to get the values of the base constructor, specifically look for the namespace - var name = GetPowerFxNamespace(type.Name, code); - - if (string.IsNullOrEmpty(name)) + // Simple optimization: if this is a provider assembly and Preview not enabled, silently skip Preview functions. + if (assemblyHasProvider && name == NAMESPACE_PREVIEW && !settings.AllowPowerFxNamespaces.Contains(NAMESPACE_PREVIEW)) { - // No Power FX Namespace found - Logger.LogInformation($"No Power FX Namespace found for {type.Name}"); - return false; + Logger.LogInformation($"Skipping Preview validation for provider function {type.Name} (Preview not enabled)."); + continue; } if (settings.DenyPowerFxNamespaces.Contains(name)) { - // Deny list match Logger.LogInformation($"Deny Power FX Namespace {name} for {type.Name}"); return false; } - - if ((settings.DenyPowerFxNamespaces.Contains("*") && ( - !settings.AllowPowerFxNamespaces.Contains(name) || - (!settings.AllowPowerFxNamespaces.Contains(name) && name != NAMESPACE_TEST_ENGINE) - ) - )) + if (settings.DenyPowerFxNamespaces.Contains("*") && !settings.AllowPowerFxNamespaces.Contains(name) && name != NAMESPACE_TEST_ENGINE) { // Deny wildcard exists only. Could not find match in allow list and name was not reserved name TestEngine Logger.LogInformation($"Deny Power FX Namespace {name} for {type.Name}"); diff --git a/src/testengine.module.pause.tests/PauseFunctionTests.cs b/src/testengine.module.pause.tests/PauseFunctionTests.cs index 8ac6b45d0..5f89e40c7 100644 --- a/src/testengine.module.pause.tests/PauseFunctionTests.cs +++ b/src/testengine.module.pause.tests/PauseFunctionTests.cs @@ -62,7 +62,7 @@ public void PauseExecute() // Act module.Execute(); - // Assert + // Assert - Should execute successfully MockLogger.Verify(l => l.Log(It.Is(l => l == LogLevel.Information), It.IsAny(), It.Is((v, t) => v.ToString() == "Successfully finished executing Pause function."), @@ -94,7 +94,7 @@ public void SkipExecute() // Act module.Execute(); - // Assert + // Assert - Should skip in headless mode MockLogger.Verify(l => l.Log(It.Is(l => l == LogLevel.Information), It.IsAny(), It.Is((v, t) => v.ToString() == "Skip Pause function as in headless mode."), diff --git a/src/testengine.module.pause.tests/PauseModuleTests.cs b/src/testengine.module.pause.tests/PauseModuleTests.cs index 58bc3b8b2..3b2897460 100644 --- a/src/testengine.module.pause.tests/PauseModuleTests.cs +++ b/src/testengine.module.pause.tests/PauseModuleTests.cs @@ -68,7 +68,41 @@ public void RegisterPowerFxFunction() // Act module.RegisterPowerFxFunction(TestConfig, MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); - // Assert + // Assert - Should skip registration since Preview is not enabled by default + MockLogger.Verify(l => l.Log(It.Is(l => l == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString() == "Skip registering Pause() - Preview namespace not enabled"), + It.IsAny(), + It.IsAny>()), Times.AtLeastOnce); + } + + [Fact] + public void RegisterPowerFxFunctionWithPreviewEnabled() + { + // Arrange + var module = new PauseModule(); + var settings = new TestSettings() + { + ExtensionModules = new TestSettingExtensions() + { + AllowPowerFxNamespaces = new HashSet { "Preview" } + } + }; + + MockTestState.Setup(x => x.GetTestSettings()).Returns(settings); + MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object); + + MockLogger.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny())); + + // Act + module.RegisterPowerFxFunction(TestConfig, MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object); + + // Assert - Should register since Preview is enabled MockLogger.Verify(l => l.Log(It.Is(l => l == LogLevel.Information), It.IsAny(), It.Is((v, t) => v.ToString() == "Registered Pause()"), diff --git a/src/testengine.module.pause/PauseFunction.cs b/src/testengine.module.pause/PauseFunction.cs index 9cfeb81a6..bf0b52c7f 100644 --- a/src/testengine.module.pause/PauseFunction.cs +++ b/src/testengine.module.pause/PauseFunction.cs @@ -5,7 +5,6 @@ using Microsoft.PowerApps.TestEngine.Config; using Microsoft.PowerApps.TestEngine.TestInfra; using Microsoft.PowerFx; -using Microsoft.PowerFx.Core.Utils; using Microsoft.PowerFx.Types; namespace testengine.module @@ -20,7 +19,7 @@ public class PauseFunction : ReflectionFunction private readonly ILogger _logger; public PauseFunction(ITestInfraFunctions testInfraFunctions, ITestState testState, ILogger logger) - : base(DPath.Root.Append(new DName("Preview")), "Pause", FormulaType.Blank) + : base("Pause", FormulaType.Blank) { _testInfraFunctions = testInfraFunctions; _testState = testState; @@ -29,8 +28,7 @@ public PauseFunction(ITestInfraFunctions testInfraFunctions, ITestState testStat public BlankValue Execute() { - _logger.LogInformation("------------------------------\n\n" + - "Executing Pause function."); + _logger.LogInformation("------------------------------\n\nExecuting Pause function."); if (!_testState.GetTestSettings().Headless) { diff --git a/src/testengine.module.pause/PauseModule.cs b/src/testengine.module.pause/PauseModule.cs index c2ec41d6a..767c14bbb 100644 --- a/src/testengine.module.pause/PauseModule.cs +++ b/src/testengine.module.pause/PauseModule.cs @@ -16,21 +16,54 @@ namespace testengine.module [Export(typeof(ITestEngineModule))] public class PauseModule : ITestEngineModule { + /// + /// Indicates whether Preview namespace is enabled in YAML testSettings.extensionModules.allowPowerFxNamespaces. + /// + public virtual bool IsPreviewNamespaceEnabled { get; private set; } = false; + public void ExtendBrowserContextOptions(BrowserNewContextOptions options, TestSettings settings) { - + UpdatePreviewNamespaceProperty(settings); } public void RegisterPowerFxFunction(PowerFxConfig config, ITestInfraFunctions testInfraFunctions, ITestWebProvider testWebProvider, ISingleTestInstanceState singleTestInstanceState, ITestState testState, IFileSystem fileSystem) { + TestSettings testSettings = null; + try + { + if (testState != null) + { + testSettings = testState.GetTestSettings(); + } + } + catch + { + testSettings = null; + } + UpdatePreviewNamespaceProperty(testSettings); + ILogger logger = singleTestInstanceState.GetLogger(); - config.AddFunction(new PauseFunction(testInfraFunctions, testState, logger)); - logger.LogInformation("Registered Pause()"); + + // Only register Pause() function if Preview namespace is enabled + if (IsPreviewNamespaceEnabled) + { + config.AddFunction(new PauseFunction(testInfraFunctions, testState, logger)); + logger.LogInformation("Registered Pause()"); + } + else + { + logger.LogInformation("Skip registering Pause() - Preview namespace not enabled"); + } } public async Task RegisterNetworkRoute(ITestState state, ISingleTestInstanceState singleTestInstanceState, IFileSystem fileSystem, IPage Page, NetworkRequestMock mock) { await Task.CompletedTask; } + + private void UpdatePreviewNamespaceProperty(TestSettings settings) + { + IsPreviewNamespaceEnabled = settings?.ExtensionModules?.AllowPowerFxNamespaces?.Contains("Preview") ?? false; + } } }