Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding TestEngine.PlaywrightAction() #338

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/PowerFX/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ There are several specifically defined functions for the test framework.
- [Pause](./Pause.md)
- [Select](./Select.md)
- [SetProperty](./SetProperty.md)
- [TestEngine.PlaywrightAction](./TestEngine.PlaywrightAction.md)
- [Wait](./Wait.md)

## Naming
Expand All @@ -23,6 +24,14 @@ Here are some guidelines for naming your functions in Power Fx:

By following these naming standards, your Power Fx code will be easier to read and maintain, and other developers will be able to understand your code more easily.

### Use Namespaces

Namespaces should be used for Power Fx functions in the Power Apps Test Engine for several reasons. First, using namespaces ensures that there is no clash with built-in functions, which can cause confusion and errors. By using namespaces, Power Fx functions can be organized and grouped together in a clear and concise manner.

Additionally, namespaces make it clear that these Power Fx functions belong to the Test Engine, and are not part of the larger Power Apps ecosystem. This helps to avoid confusion and ensures that the functions are used appropriately within the context of the Test Engine.

Overall, using namespaces for Power Fx functions in the Power Apps Test Engine is a best practice that helps to ensure clarity, organization, and consistency in the testing process.

### Using Descriptive Names

Using descriptive names is important because it makes it easier for others (and yourself) to understand what the function or service does. A good name should be concise but also convey the function's or service's purpose. For example, instead of naming a function "Calculate," you could name it "CalculateTotalCost" to make it clear what the function is doing.
Expand Down
36 changes: 36 additions & 0 deletions docs/PowerFX/TestEngine.PlaywrightAction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# TestEngine.PlaywrightAction

` TestEngine.PlaywrightAction(Locator, Action)`

` TestEngine.PlaywrightAction(Url, Action)`

This use the locators or Url to apply an action to the current web page.

## Locators

When selecting actions that require a locator you can make use of [CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) or XPath queries.

Locators for web pages are based on Playwright locators. More information on locators is available from [Playwright documentation](https://playwright.dev/docs/other-locators).

Playwright also supports experimental React and vue base selectors that can be useful for selecting elements on code first extensions like PCF controls within a Power App.

## Actions

The following actions are supported

| Action | Description |
|----------|----------------------------------------|
| click | Select matching locator items |
| exists | Returns True or False is locator exist |
| navigate | Navigate to the url |
| wait | Wait for locator items to exist |

## Examples

` TestEngine.PlaywrightAction("//button", "click")`

` Assert(TestEngine.PlaywrightAction("//button", "exists") = true)`

` TestEngine.PlaywrightAction("https://www.microsoft.com", "navigate")`

` TestEngine.PlaywrightAction("//button", "wait")`
27 changes: 27 additions & 0 deletions samples/playwrightaction/testPlan.fx.yaml
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions src/PowerAppsTestEngine.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ITestInfraFunctions> MockTestInfraFunctions;
private Mock<ITestState> MockTestState;
private Mock<ITestWebProvider> MockTestWebProvider;
private Mock<ISingleTestInstanceState> MockSingleTestInstanceState;
private Mock<IFileSystem> MockFileSystem;
private Mock<IPage> MockPage;
private PowerFxConfig TestConfig;
private NetworkRequestMock TestNetworkRequestMock;
private Mock<ILogger> MockLogger;

public PlaywrightActionFunctionTests()
{
MockTestInfraFunctions = new Mock<ITestInfraFunctions>(MockBehavior.Strict);
MockTestState = new Mock<ITestState>(MockBehavior.Strict);
MockTestWebProvider = new Mock<ITestWebProvider>();
MockSingleTestInstanceState = new Mock<ISingleTestInstanceState>(MockBehavior.Strict);
MockFileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
MockPage = new Mock<IPage>(MockBehavior.Strict);
TestConfig = new PowerFxConfig();
TestNetworkRequestMock = new NetworkRequestMock();
MockLogger = new Mock<ILogger>(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<IResponse>().Object));
break;
case "wait":
MockPage.Setup(x => x.WaitForSelectorAsync("//foo", null)).Returns(Task.FromResult<IElementHandle?>(null));
break;
case "exists":
case "exists-true":
var mockLocator = new Mock<ILocator>();
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<IBrowserContext>();

MockLogger.Setup(x => x.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));

MockPage.Setup(x => x.Url).Returns("http://localhost");

MockTestInfraFunctions.Setup(x => x.GetContext()).Returns(mockBrowserContext.Object);
mockBrowserContext.Setup(x => x.Pages).Returns(new List<IPage>() { 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<IBrowserContext>();

MockLogger.Setup(x => x.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));

MockTestInfraFunctions.Setup(x => x.GetContext()).Returns(mockBrowserContext.Object);

var mockPages = new List<IPage>();
int index = 0;
foreach (var page in pages.Split(','))
{
var mockPage = new Mock<IPage>();

if (index <= waitOnPage)
{
mockPage.Setup(m => m.Url).Returns(page);
}

if ( index == waitOnPage )
{
mockPage.Setup(x => x.WaitForSelectorAsync(locator, null)).Returns(Task.FromResult<IElementHandle?>(null));
}

mockPages.Add(mockPage.Object);
}

mockBrowserContext.Setup(x => x.Pages).Returns(mockPages);

// Act
function.Execute(StringValue.New(locator), StringValue.New("wait"));

// Assert
Mock.VerifyAll();
}
}
}
Loading
Loading