From 4155d9f921f4beaa545e0daa546ebd252d883d77 Mon Sep 17 00:00:00 2001 From: Grant Archibald <31553604+Grant-Archibald-MS@users.noreply.github.com> Date: Thu, 6 Jun 2024 11:08:39 -0700 Subject: [PATCH 01/82] Adding namespace checks and support for Power FX #330 --- samples/pause/testPlan.fx.yaml | 2 +- .../TestEngineExtensionCheckerTests.cs | 35 ++- .../Config/TestSettingExtensions.cs | 11 + .../Microsoft.PowerApps.TestEngine.csproj | 2 + .../Modules/TestEngineExtensionChecker.cs | 202 +++++++++++++++++- .../Modules/TestEngineModuleMEFLoader.cs | 4 +- src/PowerAppsTestEngine.sln | 1 + src/testengine.module.pause/PauseFunction.cs | 3 +- .../SampleFunction.cs | 3 +- 9 files changed, 255 insertions(+), 8 deletions(-) diff --git a/samples/pause/testPlan.fx.yaml b/samples/pause/testPlan.fx.yaml index 2fe3d5a53..0946f2bbc 100644 --- a/samples/pause/testPlan.fx.yaml +++ b/samples/pause/testPlan.fx.yaml @@ -9,7 +9,7 @@ testSuite: - testCaseName: Pause testCaseDescription: Pause example testSteps: | - = Pause(); + = TestEngine.Pause(); testSettings: headless: false diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineExtensionCheckerTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineExtensionCheckerTests.cs index 2aa458c60..54be1d693 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineExtensionCheckerTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Modules/TestEngineExtensionCheckerTests.cs @@ -106,6 +106,39 @@ public void IsValid(string usingStatements, string script, bool useTemplate, str Assert.Equal(expected, result); } + + private string _functionTemplate = @" +#r ""Microsoft.PowerFx.Interpreter.dll"" +using Microsoft.PowerFx; +using Microsoft.PowerFx.Types; +using Microsoft.PowerFx.Core.Utils; + +%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(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 + [InlineData("", "", "public class FooFunction : ReflectionFunction { private FooFunction() : base(DPath.Root.Append(new DName(\"TestEngine\")), \"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", false)] // Private constructor - Not allow + [InlineData("", "*", "public class FooFunction : ReflectionFunction { public FooFunction() : base(DPath.Root.Append(new DName(\"Other\")), \"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", false)] // Deny all and not in allow list + [InlineData("Other", "*", "public class FooFunction : ReflectionFunction { public FooFunction() : base(DPath.Root.Append(new DName(\"Other\")), \"Foo\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", true)] // Deny all and in allow list + [InlineData("", "", "public class OtherFunction : ReflectionFunction { public OtherFunction(int someNumber) : base(DPath.Root.Append(new DName(\"TestEngine\")), \"Other\", FormulaType.Blank) {} } public BlankValue Execute() { return FormulaValue.NewBlank(); }", true)] // Allow TestEngine namespace with a parameter + public void ValidPowerFxFunction(string allow, string deny, string code, bool valid) { + // Arrange + var checker = new TestEngineExtensionChecker(MockLogger.Object); + + var assembly = CompileScript(_functionTemplate.Replace("%CODE%",code)); + + var settings = new TestSettingExtensions(); + settings.AllowPowerFxNamespaces.AddRange(allow.Split(',')); + settings.DenyPowerFxNamespaces.AddRange(deny.Split(',')); + + var isValid = checker.VerifyContainsValidNamespacePowerFxFunctions(settings, assembly); + + Assert.Equal(valid, isValid); + } + [Theory] [InlineData(false, true, false, "CN=Test", "CN=Test", 0, 1, true)] [InlineData(false, false, false, "", "", 0, 1, true)] @@ -198,7 +231,7 @@ static X509Certificate2 GenerateSelfSignedCertificate(string subjectName, DateTi } } - private byte[] CompileScript(string script) + public static byte[] CompileScript(string script) { SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(script); ScriptOptions options = ScriptOptions.Default; diff --git a/src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensions.cs b/src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensions.cs index a7ce4572b..9b59cb3d4 100644 --- a/src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensions.cs +++ b/src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensions.cs @@ -40,6 +40,17 @@ public class TestSettingExtensions /// public List DenyNamespaces { get; set; } = new List(); + /// + /// List of allowed PowerFx Namespaces that can be referenced in a Test Engine Module + /// + public List AllowPowerFxNamespaces { get; set; } = new List(); + + /// + /// List of allowed PowerFx Namespaces that deny load unless explict allow is defined + /// + public List DenyPowerFxNamespaces { get; set; } = new List(); + + /// /// Additional optional parameters for extension modules /// diff --git a/src/Microsoft.PowerApps.TestEngine/Microsoft.PowerApps.TestEngine.csproj b/src/Microsoft.PowerApps.TestEngine/Microsoft.PowerApps.TestEngine.csproj index 242f2ff20..1ea65d3bb 100644 --- a/src/Microsoft.PowerApps.TestEngine/Microsoft.PowerApps.TestEngine.csproj +++ b/src/Microsoft.PowerApps.TestEngine/Microsoft.PowerApps.TestEngine.csproj @@ -27,6 +27,7 @@ + @@ -35,6 +36,7 @@ + diff --git a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs index 81d7c90ab..23f22cf95 100644 --- a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs +++ b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs @@ -3,11 +3,23 @@ using System.Security.Cryptography.X509Certificates; using System.Text; +using ICSharpCode.Decompiler; using Microsoft.Extensions.Logging; using Microsoft.PowerApps.TestEngine.Config; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.CSharp; + +using System.Reflection; +using System.Reflection.PortableExecutable; +using System.Reflection.Metadata; +using ModuleDefinition = Mono.Cecil.ModuleDefinition; +using TypeDefinition = Mono.Cecil.TypeDefinition; +using TypeReference = Mono.Cecil.TypeReference; +using MethodBody = Mono.Cecil.Cil.MethodBody; +using System.IO; namespace Microsoft.PowerApps.TestEngine.Modules { @@ -231,6 +243,12 @@ public virtual bool Validate(TestSettingExtensions settings, string file) var valid = true; + if ( !VerifyContainsValidNamespacePowerFxFunctions(settings, contents) ) + { + Logger.LogInformation("Invalid Power FX Namespace"); + valid = false; + } + foreach (var item in found) { // Allow if what was found is shorter and starts with allow value or what was found is a subset of a more specific allow rule @@ -248,14 +266,194 @@ public virtual bool Validate(TestSettingExtensions settings, string file) } } + return valid; } /// - /// Load all the types from the assembly using Intermediate Langiage (IL) mode only + /// Validate that the function only contains PowerFx functions that belong to valid Power Fx namespaces + /// + /// + /// + /// + public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions settings, byte[] assembly) + { + var isValid = true; + using (var stream = new MemoryStream(assembly)) + { + stream.Position = 0; + ModuleDefinition module = ModuleDefinition.ReadModule(stream); + + // Get the source code of the assembly as will be used to check Power FX Namespaces + var code = DecompileModuleToCSharp(assembly); + + foreach (TypeDefinition type in module.GetAllTypes()) + { + if (type.BaseType != null && type.BaseType.Name == "ReflectionFunction") + { + var constructors = type.GetConstructors(); + + if (constructors.Count() != 1) + { + Logger.LogInformation($"No constructor defined for {type.Name}"); + return false; + } + + var constructor = constructors.First(); + + if ( !constructor.HasBody ) + { + Logger.LogInformation($"No body defined for {type.Name}"); + // Needs body for call to base constructor + return false; + } + + var baseCall = constructor.Body.Instructions.FirstOrDefault(i => i.OpCode == OpCodes.Call && i.Operand is MethodReference && ((MethodReference)i.Operand).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; + + if ( baseConstructor.Parameters.Count() < 2 ) + { + // Not enough parameters + Logger.LogInformation($"No not enough parameters for {type.Name}"); + return false; + } + + if (baseConstructor.Parameters[0].ParameterType.FullName != "Microsoft.PowerFx.Core.Utils.DPath" ) + { + // First argument should be Namespace + Logger.LogInformation($"No Power FX Namespace for {type.Name}"); + return false; + } + + // 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) ) + { + // No Power FX Namespace found + Logger.LogInformation($"No Power FX Namespace found for {type.Name}"); + return false; + } + + 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 != "TestEngine") + ) + )) + { + // 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}"); + return false; + } + + if ( !settings.AllowPowerFxNamespaces.Contains(name) && name != "TestEngine" ) + { + Logger.LogInformation($"Not allow Power FX Namespace {name} for {type.Name}"); + // Not in allow list or the Reserved TestEngine namespace + return false; + } + } + } + } + return isValid; + } + + /// + /// Get the declared Power FX Namespace assigned to a Power FX Reflection function + /// + /// The name of the ReflectionFunction to find + /// The decompiled source code to search + /// The DPath Name that has been declared from the code + private string GetPowerFxNamespace(string name, string code) + { + /* + It is assumed that the code will be formatted like the following examples + + public FooFunction() + : base(DPath.Root.Append(new DName("Foo")), "Foo", FormulaType.Blank) { + } + + or + + public OtherFunction(int start) + : base(DPath.Root.Append(new DName("Other")), "Foo", FormulaType.Blank) { + } + + */ + + var lines = code.Split('\n').ToList(); + + var match = lines.Where(l => l.Contains($"public {name}(")).FirstOrDefault(); + + if ( match == null ) + { + return String.Empty; + } + + var index = lines.IndexOf(match); + + // Search for a DName that is Appended to the Root path as functions should be in a Power FX Namespace not the Root + var baseDeclaration = "base(DPath.Root.Append(new DName(\""; + + // Search for the DName + var declaration = lines[index + 1].IndexOf(baseDeclaration); + + if ( declaration >= 0 ) + { + // Found a match + var start = declaration + baseDeclaration.Length; + var end = lines[index + 1].IndexOf("\"", start); + // Extract the Power FX Namespace argument from the declaration + return lines[index + 1].Substring(declaration + baseDeclaration.Length, end - start); + } + + return String.Empty; + } + + private string DecompileModuleToCSharp(byte[] assembly) + { + var fileName = "module.dll"; + using (var module = new MemoryStream(assembly)) + using (var peFile = new PEFile(fileName, module)) + using (var writer = new StringWriter()) + { + var decompilerSettings = new DecompilerSettings() + { + ThrowOnAssemblyResolveErrors = false, + DecompileMemberBodies = true, + UsingDeclarations = true + }; + decompilerSettings.CSharpFormattingOptions.ConstructorBraceStyle = ICSharpCode.Decompiler.CSharp.OutputVisitor.BraceStyle.EndOfLine; + + var resolver = new UniversalAssemblyResolver(this.GetType().Assembly.Location, decompilerSettings.ThrowOnAssemblyResolveErrors, + peFile.DetectTargetFrameworkId(), peFile.DetectRuntimePack(), + decompilerSettings.LoadInMemory ? PEStreamOptions.PrefetchMetadata : PEStreamOptions.Default, + decompilerSettings.ApplyWindowsRuntimeProjections ? MetadataReaderOptions.ApplyWindowsRuntimeProjections : MetadataReaderOptions.None); + var decompiler = new CSharpDecompiler(peFile, resolver, decompilerSettings); + return decompiler.DecompileWholeModuleAsString(); + } + } + + /// + /// Load all the types from the assembly using Intermediate Language (IL) mode only /// /// The byte representation of the assembly - /// The Dependancies, Types and Method calls found in the assembly + /// The Dependencies, Types and Method calls found in the assembly private List LoadTypes(byte[] assembly) { List found = new List(); diff --git a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs index f5461ceff..85fb5586c 100644 --- a/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs +++ b/src/Microsoft.PowerApps.TestEngine/Modules/TestEngineModuleMEFLoader.cs @@ -48,9 +48,9 @@ public AggregateCatalog LoadModules(TestSettingExtensions settings) // Load MEF exports from this assembly match.Add(new AssemblyCatalog(typeof(TestEngine).Assembly)); - foreach (var sourcelocation in settings.Source.InstallSource) + foreach (var sourceLocation in settings.Source.InstallSource) { - string location = sourcelocation; + string location = sourceLocation; if (settings.Source.EnableNuGet) { var nuGetSettings = Settings.LoadDefaultSettings(null); diff --git a/src/PowerAppsTestEngine.sln b/src/PowerAppsTestEngine.sln index 16efe04c3..6805dc987 100644 --- a/src/PowerAppsTestEngine.sln +++ b/src/PowerAppsTestEngine.sln @@ -115,6 +115,7 @@ Global {B91EFA35-C28B-497E-BFDC-8497933393A0} = {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} {2778A59F-773D-414E-A1FF-9C5B5F90E28F} = {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} {8AEAF6BD-38E3-4649-9221-6A67AD1E96EC} = {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} + {D34E437A-6149-46EC-B7DA-FF449E55CEEA} = {63A04DC1-C37E-43E6-8FEA-A480483E11F8} {B3A02421-223D-4E80-A8CE-977B425A6EB2} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} {3D9F90F2-0937-486D-AA0B-BFE425354F4A} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} EndGlobalSection diff --git a/src/testengine.module.pause/PauseFunction.cs b/src/testengine.module.pause/PauseFunction.cs index ce19f7a49..fe789b12a 100644 --- a/src/testengine.module.pause/PauseFunction.cs +++ b/src/testengine.module.pause/PauseFunction.cs @@ -6,6 +6,7 @@ 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 +21,7 @@ public class PauseFunction : ReflectionFunction private readonly ILogger _logger; public PauseFunction(ITestInfraFunctions testInfraFunctions, ITestState testState, ILogger logger) - : base("Pause", FormulaType.Blank) + : base(DPath.Root.Append(new DName("TestEngine")), "Pause", FormulaType.Blank) { _testInfraFunctions = testInfraFunctions; _testState = testState; diff --git a/src/testengine.module.sample/SampleFunction.cs b/src/testengine.module.sample/SampleFunction.cs index 031648a96..f21bd29d7 100644 --- a/src/testengine.module.sample/SampleFunction.cs +++ b/src/testengine.module.sample/SampleFunction.cs @@ -1,11 +1,12 @@ using Microsoft.PowerFx; +using Microsoft.PowerFx.Core.Utils; using Microsoft.PowerFx.Types; namespace testengine.module.sample { public class SampleFunction : ReflectionFunction { - public SampleFunction() : base("Sample", FormulaType.Blank) + public SampleFunction() : base(DPath.Root.Append(new DName("TestEngine")), "Sample", FormulaType.Blank) { } From f1bbdf270c8b52820fcfc3fe5fde088ff93be1c1 Mon Sep 17 00:00:00 2001 From: Grant Archibald <31553604+Grant-Archibald-MS@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:20:47 -0700 Subject: [PATCH 02/82] Adding TestEngine.PlaywrightScript() #335 --- samples/playwrightscript/sample.csx | 16 +++ samples/playwrightscript/testPlan.fx.yaml | 29 ++++ src/PowerAppsTestEngine.sln | 14 ++ .../PlaywrightScriptsFunctionTests.cs | 89 ++++++++++++ .../PlaywrightScriptsModuleTests.cs | 89 ++++++++++++ .../Usings.cs | 1 + ...ngine.module.playwrightscript.tests.csproj | 31 +++++ .../PlaywrightScriptFunction.cs | 130 ++++++++++++++++++ .../PlaywrightScriptModule.cs | 36 +++++ .../testengine.module.playwrightscript.csproj | 28 ++++ 10 files changed, 463 insertions(+) create mode 100644 samples/playwrightscript/sample.csx create mode 100644 samples/playwrightscript/testPlan.fx.yaml create mode 100644 src/testengine.module.playwrightscript.tests/PlaywrightScriptsFunctionTests.cs create mode 100644 src/testengine.module.playwrightscript.tests/PlaywrightScriptsModuleTests.cs create mode 100644 src/testengine.module.playwrightscript.tests/Usings.cs create mode 100644 src/testengine.module.playwrightscript.tests/testengine.module.playwrightscript.tests.csproj create mode 100644 src/testengine.module.playwrightscript/PlaywrightScriptFunction.cs create mode 100644 src/testengine.module.playwrightscript/PlaywrightScriptModule.cs create mode 100644 src/testengine.module.playwrightscript/testengine.module.playwrightscript.csproj diff --git a/samples/playwrightscript/sample.csx b/samples/playwrightscript/sample.csx new file mode 100644 index 000000000..0b75ab9ce --- /dev/null +++ b/samples/playwrightscript/sample.csx @@ -0,0 +1,16 @@ +#r "Microsoft.Playwright.dll" +#r "Microsoft.Extensions.Logging.dll" +using Microsoft.Playwright; +using Microsoft.Extensions.Logging; +using System.Linq; + +public class PlaywrightScript { + public static void Run(IBrowserContext context, ILogger logger) { + var page = context.Pages.First(); + foreach ( var frame in page.Frames ) { + if ( frame.Locator("button:has-text('Button')").CountAsync().Result > 0 ) { + frame.ClickAsync("button:has-text('Button')").Wait(); + } + } + } +} \ No newline at end of file diff --git a/samples/playwrightscript/testPlan.fx.yaml b/samples/playwrightscript/testPlan.fx.yaml new file mode 100644 index 000000000..13590997e --- /dev/null +++ b/samples/playwrightscript/testPlan.fx.yaml @@ -0,0 +1,29 @@ +testSuite: + testSuiteName: testPlan Template + testSuiteDescription: Playwright csx example + persona: User1 + appLogicalName: new_buttonclicker_0a877 + onTestSuiteComplete: Screenshot("playwrightaction_onTestSuiteComplete.png"); + + testCases: + - testCaseName: Run Script + testCaseDescription: CSX example + testSteps: | + = TestEngine.Pause(); + TestEngine.PlaywrightScript("sample.csx"); + TestEngine.Pause(); + +testSettings: + headless: false + locale: "en-US" + recordVideo: true + extensionModules: + enable: true + browserConfigurations: + - browser: Chromium + +environmentVariables: + users: + - personaName: User1 + emailKey: user1Email + passwordKey: user1Password diff --git a/src/PowerAppsTestEngine.sln b/src/PowerAppsTestEngine.sln index 16efe04c3..ba0007b45 100644 --- a/src/PowerAppsTestEngine.sln +++ b/src/PowerAppsTestEngine.sln @@ -46,6 +46,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.pause", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.pause.tests", "testengine.module.pause.tests\testengine.module.pause.tests.csproj", "{3D9F90F2-0937-486D-AA0B-BFE425354F4A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.playwrightscript", "testengine.module.playwrightscript\testengine.module.playwrightscript.csproj", "{FE05DDD2-73C6-44B4-9A1B-7DACD984A428}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.playwrightscript.tests", "testengine.module.playwrightscript.tests\testengine.module.playwrightscript.tests.csproj", "{6E2A1E7D-CDED-49D4-B6C3-BEDD828ED7DB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -100,6 +104,14 @@ Global {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Release|Any CPU.Build.0 = Release|Any CPU + {FE05DDD2-73C6-44B4-9A1B-7DACD984A428}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE05DDD2-73C6-44B4-9A1B-7DACD984A428}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE05DDD2-73C6-44B4-9A1B-7DACD984A428}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE05DDD2-73C6-44B4-9A1B-7DACD984A428}.Release|Any CPU.Build.0 = Release|Any CPU + {6E2A1E7D-CDED-49D4-B6C3-BEDD828ED7DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E2A1E7D-CDED-49D4-B6C3-BEDD828ED7DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E2A1E7D-CDED-49D4-B6C3-BEDD828ED7DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E2A1E7D-CDED-49D4-B6C3-BEDD828ED7DB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -117,6 +129,8 @@ Global {8AEAF6BD-38E3-4649-9221-6A67AD1E96EC} = {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} {B3A02421-223D-4E80-A8CE-977B425A6EB2} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} {3D9F90F2-0937-486D-AA0B-BFE425354F4A} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} + {FE05DDD2-73C6-44B4-9A1B-7DACD984A428} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} + {6E2A1E7D-CDED-49D4-B6C3-BEDD828ED7DB} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7E7B2C01-DDE2-4C5A-96C3-AF474B074331} diff --git a/src/testengine.module.playwrightscript.tests/PlaywrightScriptsFunctionTests.cs b/src/testengine.module.playwrightscript.tests/PlaywrightScriptsFunctionTests.cs new file mode 100644 index 000000000..8b95a8424 --- /dev/null +++ b/src/testengine.module.playwrightscript.tests/PlaywrightScriptsFunctionTests.cs @@ -0,0 +1,89 @@ +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Microsoft.Playwright; +using Moq; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.Extensions.Logging; +using Microsoft.PowerFx.Types; + +namespace testengine.module.browserlocale.tests +{ + public class PlaywrightScriptsFunctionTests + { + private Mock MockTestInfraFunctions; + private Mock MockTestState; + private Mock MockTestWebProvider; + private Mock MockSingleTestInstanceState; + private Mock MockFileSystem; + private Mock MockPage; + private PowerFxConfig TestConfig; + private NetworkRequestMock TestNetworkRequestMock; + private Mock MockLogger; + + public PlaywrightScriptsFunctionTests() + { + MockTestInfraFunctions = new Mock(MockBehavior.Strict); + MockTestState = new Mock(MockBehavior.Strict); + MockTestWebProvider = new Mock(); + MockSingleTestInstanceState = new Mock(MockBehavior.Strict); + MockFileSystem = new Mock(MockBehavior.Strict); + MockPage = new Mock(MockBehavior.Strict); + TestConfig = new PowerFxConfig(); + TestNetworkRequestMock = new NetworkRequestMock(); + MockLogger = new Mock(MockBehavior.Strict); + } + + [Theory] + [InlineData(@"c:\test.csx", @"#r ""Microsoft.Playwright.dll"" +#r ""Microsoft.Extensions.Logging.dll"" +using Microsoft.Playwright; +using Microsoft.Extensions.Logging; + +public class PlaywrightScript +{ + public static void Run(IBrowserContext context, ILogger logger) + { + } +}")] + public void PlaywrightExecute(string file, string code) + { + // Arrange + + var function = new PlaywrightScriptFunction(MockTestInfraFunctions.Object, MockTestState.Object, MockFileSystem.Object, MockLogger.Object); + + MockLogger.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny())); + + MockFileSystem.Setup(x => x.IsValidFilePath(file)).Returns(true); + MockFileSystem.Setup(x => x.ReadAllText(file)).Returns(code); + + MockTestInfraFunctions.Setup(x => x.GetContext()).Returns(new Mock().Object); + + // Act + function.Execute(StringValue.New(file)); + + // Assert + MockLogVerify(LogLevel.Information, "------------------------------\n\n" + + "Executing PlaywrightScript function."); + + MockLogVerify(LogLevel.Debug, "Loading file"); + + MockLogVerify(LogLevel.Information, "Successfully finished executing PlaywrightScript function."); + } + + private void MockLogVerify(LogLevel logLevel, string message) + { + MockLogger.Verify(l => l.Log(It.Is(l => l == logLevel), + It.IsAny(), + It.Is((v, t) => v.ToString() == message), + It.IsAny(), + It.IsAny>()), Times.AtLeastOnce); + } + } +} \ No newline at end of file diff --git a/src/testengine.module.playwrightscript.tests/PlaywrightScriptsModuleTests.cs b/src/testengine.module.playwrightscript.tests/PlaywrightScriptsModuleTests.cs new file mode 100644 index 000000000..296de5fa2 --- /dev/null +++ b/src/testengine.module.playwrightscript.tests/PlaywrightScriptsModuleTests.cs @@ -0,0 +1,89 @@ +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Microsoft.Playwright; +using Moq; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.Extensions.Logging; + +namespace testengine.module.browserlocale.tests +{ + public class PlaywrightScriptsModuleTests + { + private Mock MockTestInfraFunctions; + private Mock MockTestState; + private Mock MockTestWebProvider; + private Mock MockSingleTestInstanceState; + private Mock MockFileSystem; + private Mock MockPage; + private PowerFxConfig TestConfig; + private NetworkRequestMock TestNetworkRequestMock; + private Mock MockLogger; + + public PlaywrightScriptsModuleTests() + { + MockTestInfraFunctions = new Mock(MockBehavior.Strict); + MockTestState = new Mock(MockBehavior.Strict); + MockTestWebProvider = new Mock(); + MockSingleTestInstanceState = new Mock(MockBehavior.Strict); + MockFileSystem = new Mock(MockBehavior.Strict); + MockPage = new Mock(MockBehavior.Strict); + TestConfig = new PowerFxConfig(); + TestNetworkRequestMock = new NetworkRequestMock(); + MockLogger = new Mock(MockBehavior.Strict); + } + + [Fact] + public void ExtendBrowserContextOptionsLocaleUpdate() + { + // Arrange + var module = new PlaywrightScriptModule(); + var options = new BrowserNewContextOptions(); + var settings = new TestSettings() { }; + + // Act + module.ExtendBrowserContextOptions(options, settings); + } + + [Fact] + public void RegisterPowerFxFunction() + { + // Arrange + var module = new PlaywrightScriptModule(); + + 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 + MockLogger.Verify(l => l.Log(It.Is(l => l == LogLevel.Information), + It.IsAny(), + It.Is((v, t) => v.ToString() == "Registered PlaywrightScript()"), + It.IsAny(), + It.IsAny>()), Times.AtLeastOnce); + } + + [Fact] + public async Task RegisterNetworkRoute() + { + // Arrange + var module = new PlaywrightScriptModule(); + + + // Act + await module.RegisterNetworkRoute(MockTestState.Object, MockSingleTestInstanceState.Object, MockFileSystem.Object, MockPage.Object, TestNetworkRequestMock); + + // Assert + } + } +} \ No newline at end of file diff --git a/src/testengine.module.playwrightscript.tests/Usings.cs b/src/testengine.module.playwrightscript.tests/Usings.cs new file mode 100644 index 000000000..8c927eb74 --- /dev/null +++ b/src/testengine.module.playwrightscript.tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/testengine.module.playwrightscript.tests/testengine.module.playwrightscript.tests.csproj b/src/testengine.module.playwrightscript.tests/testengine.module.playwrightscript.tests.csproj new file mode 100644 index 000000000..02f11e54f --- /dev/null +++ b/src/testengine.module.playwrightscript.tests/testengine.module.playwrightscript.tests.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/testengine.module.playwrightscript/PlaywrightScriptFunction.cs b/src/testengine.module.playwrightscript/PlaywrightScriptFunction.cs new file mode 100644 index 000000000..f5b1d01de --- /dev/null +++ b/src/testengine.module.playwrightscript/PlaywrightScriptFunction.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.PowerFx.Types; +using Microsoft.PowerFx; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using System.Reflection; +using Microsoft.PowerFx.Core.Utils; + +namespace testengine.module +{ + /// + /// This will execute CSharp Script (CSX) file passing IBrowserContext and ILogger + /// + public class PlaywrightScriptFunction : ReflectionFunction + { + private readonly ITestInfraFunctions _testInfraFunctions; + private readonly ITestState _testState; + private readonly ILogger _logger; + private readonly IFileSystem _filesystem; + + public PlaywrightScriptFunction(ITestInfraFunctions testInfraFunctions, ITestState testState, IFileSystem filesystem, ILogger logger) + : base(DPath.Root.Append(new DName("TestEngine")), "PlaywrightScript", FormulaType.Blank, FormulaType.String) + { + _testInfraFunctions = testInfraFunctions; + _testState = testState; + _logger = logger; + _filesystem = filesystem; + } + + public BlankValue Execute(StringValue file) + { + _logger.LogInformation("------------------------------\n\n" + + "Executing PlaywrightScript function."); + + if ( !_filesystem.IsValidFilePath(file.Value) ) + { + _logger.LogError("Invalid file"); + throw new ArgumentException("Invalid file"); + } + + _logger.LogDebug("Loading file"); + + var filename = GetFullFile(_testState, file.Value); + var script = _filesystem.ReadAllText(filename); + + byte[] assemblyBinaryContent; + + _logger.LogDebug("Compiling file"); + + ScriptOptions options = ScriptOptions.Default; + var roslynScript = CSharpScript.Create(script, options); + var compilation = roslynScript.GetCompilation(); + + compilation = compilation.WithOptions(compilation.Options + .WithOptimizationLevel(OptimizationLevel.Release) + .WithOutputKind(OutputKind.DynamicallyLinkedLibrary)); + + using (var assemblyStream = new MemoryStream()) + { + var result = compilation.Emit(assemblyStream); + if (!result.Success) + { + var errors = string.Join(Environment.NewLine, result.Diagnostics.Select(x => x)); + throw new Exception("Compilation errors: " + Environment.NewLine + errors); + } + + assemblyBinaryContent = assemblyStream.ToArray(); + } + + GC.Collect(); + + Assembly assembly = Assembly.Load(assemblyBinaryContent); + + _logger.LogDebug("Run script"); + Run(assembly); + + _logger.LogInformation("Successfully finished executing PlaywrightScript function."); + + return FormulaValue.NewBlank(); + } + + private string GetFullFile(ITestState testState, string filename) + { + if (!Path.IsPathRooted(filename)) + { + var testResultDirectory = Path.GetDirectoryName(testState.GetTestConfigFile().FullName); + filename = Path.Combine(testResultDirectory, filename); + } + return filename; + } + + private void Run(Assembly assembly) + { + //Execute the script + var types = assembly.GetTypes(); + + bool found = false; + foreach ( var scriptType in types ) + { + if ( scriptType.Name.Equals("PlaywrightScript") ) + { + found = true; + + var method = scriptType.GetMethod("Run", BindingFlags.Static | BindingFlags.Public); + + var context = _testInfraFunctions.GetContext(); + + if (method == null) + { + _logger.LogError("Static Run Method not found"); + } + + method?.Invoke(null, new object[] { context, _logger }); + } + } + + if ( !found ) { + _logger.LogError("PlaywrightScript class not found"); + } + } + } +} + diff --git a/src/testengine.module.playwrightscript/PlaywrightScriptModule.cs b/src/testengine.module.playwrightscript/PlaywrightScriptModule.cs new file mode 100644 index 000000000..2ac926563 --- /dev/null +++ b/src/testengine.module.playwrightscript/PlaywrightScriptModule.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.PowerFx; +using System.ComponentModel.Composition; +using Microsoft.Extensions.Logging; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Modules; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.Playwright; + +namespace testengine.module +{ + [Export(typeof(ITestEngineModule))] + public class PlaywrightScriptModule : ITestEngineModule + { + public void ExtendBrowserContextOptions(BrowserNewContextOptions options, TestSettings settings) + { + + } + + public void RegisterPowerFxFunction(PowerFxConfig config, ITestInfraFunctions testInfraFunctions, ITestWebProvider testWebProvider, ISingleTestInstanceState singleTestInstanceState, ITestState testState, IFileSystem fileSystem) + { + ILogger logger = singleTestInstanceState.GetLogger(); + config.AddFunction(new PlaywrightScriptFunction(testInfraFunctions, testState, fileSystem, logger)); + logger.LogInformation("Registered PlaywrightScript()"); + } + + public async Task RegisterNetworkRoute(ITestState state, ISingleTestInstanceState singleTestInstanceState, IFileSystem fileSystem, IPage Page, NetworkRequestMock mock) + { + await Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/testengine.module.playwrightscript/testengine.module.playwrightscript.csproj b/src/testengine.module.playwrightscript/testengine.module.playwrightscript.csproj new file mode 100644 index 000000000..bcf997d60 --- /dev/null +++ b/src/testengine.module.playwrightscript/testengine.module.playwrightscript.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + + From 1616e2b7cf477bce00aa2bb8bb7354e259294dd7 Mon Sep 17 00:00:00 2001 From: Grant Archibald <31553604+Grant-Archibald-MS@users.noreply.github.com> Date: Thu, 6 Jun 2024 19:15:15 -0700 Subject: [PATCH 03/82] Adding TestEngine.PlaywrightAction() #337 --- samples/playwrightaction/testPlan.fx.yaml | 27 +++ src/PowerAppsTestEngine.sln | 21 +++ .../PlaywrightActionFunctionTests.cs | 154 ++++++++++++++++++ .../PlaywrightActionFunctionValueTests.cs | 132 +++++++++++++++ .../PlaywrightActionModuleTests.cs | 87 ++++++++++ .../Usings.cs | 1 + ...ngine.module.playwrightaction.tests.csproj | 31 ++++ .../PlaywrightActionFunction.cs | 86 ++++++++++ .../PlaywrightActionModule.cs | 39 +++++ .../PlaywrightActionValueFunction.cs | 119 ++++++++++++++ .../testengine.module.playwrightaction.csproj | 24 +++ .../MockLoggerHelper.cs | 17 ++ .../testengine.module.tests.common.csproj | 14 ++ 13 files changed, 752 insertions(+) create mode 100644 samples/playwrightaction/testPlan.fx.yaml create mode 100644 src/testengine.module.playwrightaction.tests/PlaywrightActionFunctionTests.cs create mode 100644 src/testengine.module.playwrightaction.tests/PlaywrightActionFunctionValueTests.cs create mode 100644 src/testengine.module.playwrightaction.tests/PlaywrightActionModuleTests.cs create mode 100644 src/testengine.module.playwrightaction.tests/Usings.cs create mode 100644 src/testengine.module.playwrightaction.tests/testengine.module.playwrightaction.tests.csproj create mode 100644 src/testengine.module.playwrightaction/PlaywrightActionFunction.cs create mode 100644 src/testengine.module.playwrightaction/PlaywrightActionModule.cs create mode 100644 src/testengine.module.playwrightaction/PlaywrightActionValueFunction.cs create mode 100644 src/testengine.module.playwrightaction/testengine.module.playwrightaction.csproj create mode 100644 src/testengine.module.tests.common/MockLoggerHelper.cs create mode 100644 src/testengine.module.tests.common/testengine.module.tests.common.csproj diff --git a/samples/playwrightaction/testPlan.fx.yaml b/samples/playwrightaction/testPlan.fx.yaml new file mode 100644 index 000000000..ae8047b43 --- /dev/null +++ b/samples/playwrightaction/testPlan.fx.yaml @@ -0,0 +1,27 @@ +testSuite: + testSuiteName: testPlan Template + testSuiteDescription: Playwright action example + persona: User1 + appLogicalName: new_buttonclicker_0a877 + onTestSuiteComplete: Screenshot("playwrightaction_onTestSuiteComplete.png"); + + testCases: + - testCaseName: Run Script + testCaseDescription: Action examples + testSteps: | + = TestEngine.PlaywrightAction("//button", "wait"); + +testSettings: + headless: false + locale: "en-US" + recordVideo: true + extensionModules: + enable: true + browserConfigurations: + - browser: Chromium + +environmentVariables: + users: + - personaName: User1 + emailKey: user1Email + passwordKey: user1Password diff --git a/src/PowerAppsTestEngine.sln b/src/PowerAppsTestEngine.sln index 16efe04c3..80009afac 100644 --- a/src/PowerAppsTestEngine.sln +++ b/src/PowerAppsTestEngine.sln @@ -46,6 +46,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.pause", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.pause.tests", "testengine.module.pause.tests\testengine.module.pause.tests.csproj", "{3D9F90F2-0937-486D-AA0B-BFE425354F4A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.playwrightaction", "testengine.module.playwrightaction\testengine.module.playwrightaction.csproj", "{0C25A43D-15BE-473D-8A21-B32D1019C082}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.playwrightaction.tests", "testengine.module.playwrightaction.tests\testengine.module.playwrightaction.tests.csproj", "{C0D26F6F-A296-4738-A2C9-E2EDA087C764}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "testengine.module.tests.common", "testengine.module.tests.common\testengine.module.tests.common.csproj", "{181900EA-182B-42D4-912B-DD6763D2237D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -100,6 +106,18 @@ Global {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D9F90F2-0937-486D-AA0B-BFE425354F4A}.Release|Any CPU.Build.0 = Release|Any CPU + {0C25A43D-15BE-473D-8A21-B32D1019C082}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C25A43D-15BE-473D-8A21-B32D1019C082}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C25A43D-15BE-473D-8A21-B32D1019C082}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C25A43D-15BE-473D-8A21-B32D1019C082}.Release|Any CPU.Build.0 = Release|Any CPU + {C0D26F6F-A296-4738-A2C9-E2EDA087C764}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0D26F6F-A296-4738-A2C9-E2EDA087C764}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0D26F6F-A296-4738-A2C9-E2EDA087C764}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0D26F6F-A296-4738-A2C9-E2EDA087C764}.Release|Any CPU.Build.0 = Release|Any CPU + {181900EA-182B-42D4-912B-DD6763D2237D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {181900EA-182B-42D4-912B-DD6763D2237D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {181900EA-182B-42D4-912B-DD6763D2237D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {181900EA-182B-42D4-912B-DD6763D2237D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -117,6 +135,9 @@ Global {8AEAF6BD-38E3-4649-9221-6A67AD1E96EC} = {0B61ADD8-5EED-4A2C-99AA-B597EC3EE223} {B3A02421-223D-4E80-A8CE-977B425A6EB2} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} {3D9F90F2-0937-486D-AA0B-BFE425354F4A} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} + {0C25A43D-15BE-473D-8A21-B32D1019C082} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} + {C0D26F6F-A296-4738-A2C9-E2EDA087C764} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} + {181900EA-182B-42D4-912B-DD6763D2237D} = {ACAB614B-304F-48C0-B8B1-8D95F3A9FBC4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7E7B2C01-DDE2-4C5A-96C3-AF474B074331} diff --git a/src/testengine.module.playwrightaction.tests/PlaywrightActionFunctionTests.cs b/src/testengine.module.playwrightaction.tests/PlaywrightActionFunctionTests.cs new file mode 100644 index 000000000..5e2f60c93 --- /dev/null +++ b/src/testengine.module.playwrightaction.tests/PlaywrightActionFunctionTests.cs @@ -0,0 +1,154 @@ +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Microsoft.Playwright; +using Moq; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.Extensions.Logging; +using Microsoft.PowerFx.Types; +using testengine.module.tests.common; +using System.Text.RegularExpressions; + +namespace testengine.module.browserlocale.tests +{ + public class PlaywrightActionFunctionTests + { + private Mock MockTestInfraFunctions; + private Mock MockTestState; + private Mock MockTestWebProvider; + private Mock MockSingleTestInstanceState; + private Mock MockFileSystem; + private Mock MockPage; + private PowerFxConfig TestConfig; + private NetworkRequestMock TestNetworkRequestMock; + private Mock MockLogger; + + public PlaywrightActionFunctionTests() + { + MockTestInfraFunctions = new Mock(MockBehavior.Strict); + MockTestState = new Mock(MockBehavior.Strict); + MockTestWebProvider = new Mock(); + MockSingleTestInstanceState = new Mock(MockBehavior.Strict); + MockFileSystem = new Mock(MockBehavior.Strict); + MockPage = new Mock(MockBehavior.Strict); + TestConfig = new PowerFxConfig(); + TestNetworkRequestMock = new NetworkRequestMock(); + MockLogger = new Mock(MockBehavior.Strict); + } + + private void RunTestScenario(string id) + { + switch (id) { + case "click": + MockTestInfraFunctions.Setup(x => x.ClickAsync("//foo")).Returns(Task.CompletedTask); + break; + case "navigate": + MockPage.Setup(x => x.GotoAsync("https://make.powerapps.com", null)).Returns(Task.FromResult(new Mock().Object)); + break; + case "wait": + MockPage.Setup(x => x.WaitForSelectorAsync("//foo", null)).Returns(Task.FromResult(null)); + break; + case "exists": + case "exists-true": + var mockLocator = new Mock(); + MockPage.Setup(x => x.Locator("//foo", null)).Returns(mockLocator.Object); + mockLocator.Setup(x => x.CountAsync()).Returns(Task.FromResult(id == "exists" ? 0 : 1)); + break; + } + } + + [Theory] + [InlineData("//foo", "click", "click", new string[] { "Click item" }, true)] + [InlineData("https://make.powerapps.com", "navigate", "navigate", new string[] { "Navigate to page" }, true)] + [InlineData("//foo", "wait", "wait", new string[] { "Wait for locator" }, true)] + [InlineData("//foo", "exists", "exists", new string[] { "Check if locator exists", "Exists False" }, false)] + [InlineData("//foo", "exists", "exists-true", new string[] { "Check if locator exists", "Exists True" }, false)] + public void PlaywrightExecute(string locator, string value, string scenario, string[] messages, bool standardEnd) + { + // Arrange + + var function = new PlaywrightActionFunction(MockTestInfraFunctions.Object, MockTestState.Object, MockLogger.Object); + var mockBrowserContext = new Mock(); + + MockLogger.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny())); + + MockPage.Setup(x => x.Url).Returns("http://localhost"); + + MockTestInfraFunctions.Setup(x => x.GetContext()).Returns(mockBrowserContext.Object); + mockBrowserContext.Setup(x => x.Pages).Returns(new List() { MockPage.Object }); + + RunTestScenario(scenario); + + // Act + function.Execute(StringValue.New(locator),StringValue.New(value)); + + // Assert + MockLogger.VerifyMessage(LogLevel.Information, "------------------------------\n\n" + + "Executing PlaywrightAction function."); + + foreach ( var message in messages ) + { + MockLogger.VerifyMessage(LogLevel.Information, message); + } + + if ( standardEnd ) + { + MockLogger.VerifyMessage(LogLevel.Information, "Successfully finished executing PlaywrightAction function."); + } + } + + [Theory] + [InlineData("about:blank",0, "//foo")] + [InlineData("about:blank,https://localhost", 1, "//foo")] + [InlineData("about:blank,https://localhost,https://microsoft.com", 1, "//foo")] + public void WaitPage(string pages, int waitOnPage, string locator) + { + // Arrange + + var function = new PlaywrightActionFunction(MockTestInfraFunctions.Object, MockTestState.Object, MockLogger.Object); + var mockBrowserContext = new Mock(); + + MockLogger.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny())); + + MockTestInfraFunctions.Setup(x => x.GetContext()).Returns(mockBrowserContext.Object); + + var mockPages = new List(); + int index = 0; + foreach (var page in pages.Split(',')) + { + var mockPage = new Mock(); + + if (index <= waitOnPage) + { + mockPage.Setup(m => m.Url).Returns(page); + } + + if ( index == waitOnPage ) + { + mockPage.Setup(x => x.WaitForSelectorAsync(locator, null)).Returns(Task.FromResult(null)); + } + + mockPages.Add(mockPage.Object); + } + + mockBrowserContext.Setup(x => x.Pages).Returns(mockPages); + + // Act + function.Execute(StringValue.New(locator), StringValue.New("wait")); + + // Assert + Mock.VerifyAll(); + } + } +} diff --git a/src/testengine.module.playwrightaction.tests/PlaywrightActionFunctionValueTests.cs b/src/testengine.module.playwrightaction.tests/PlaywrightActionFunctionValueTests.cs new file mode 100644 index 000000000..3623eaa55 --- /dev/null +++ b/src/testengine.module.playwrightaction.tests/PlaywrightActionFunctionValueTests.cs @@ -0,0 +1,132 @@ +using Microsoft.PowerApps.TestEngine.TestInfra; +using Microsoft.PowerFx; +using Microsoft.Playwright; +using Moq; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.Providers; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.Extensions.Logging; +using Microsoft.PowerFx.Types; +using testengine.module.tests.common; +using System.Text.RegularExpressions; + +namespace testengine.module.browserlocale.tests +{ + public class PlaywrightActionValueFunctionTests + { + private Mock MockTestInfraFunctions; + private Mock MockTestState; + private Mock MockTestWebProvider; + private Mock MockSingleTestInstanceState; + private Mock MockFileSystem; + private Mock MockPage; + private PowerFxConfig TestConfig; + private NetworkRequestMock TestNetworkRequestMock; + private Mock MockLogger; + private Mock MockLocator; + + public PlaywrightActionValueFunctionTests() + { + MockTestInfraFunctions = new Mock(MockBehavior.Strict); + MockTestState = new Mock(MockBehavior.Strict); + MockTestWebProvider = new Mock(); + MockSingleTestInstanceState = new Mock(MockBehavior.Strict); + MockFileSystem = new Mock(MockBehavior.Strict); + MockPage = new Mock(MockBehavior.Strict); + TestConfig = new PowerFxConfig(); + TestNetworkRequestMock = new NetworkRequestMock(); + MockLogger = new Mock(MockBehavior.Strict); + MockLocator = new Mock(); + } + + private void RunTestScenario(string id) + { + switch (id) { + case "click-in-iframe": + var mockFrame = new Mock