Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,30 @@ public async Task RunJavascriptSuccessfulTest()
MockPage.Verify(x => x.EvaluateAsync<string>(jsExpression, null), Times.Once());
}

[Fact]
public async Task RunJavascriptWithArgumentSuccessfulTest()
{
var jsExpression = "(value) => value.text";
var argument = new { text = "hello" };
var expectedResponse = "hello";

LoggingTestHelper.SetupMock(MockLogger);
MockSingleTestInstanceState.Setup(x => x.GetLogger()).Returns(MockLogger.Object);
MockPage.Setup(x => x.EvaluateAsync<string>(jsExpression, argument)).ReturnsAsync(expectedResponse);

var playwrightTestInfraFunctions = new PlaywrightTestInfraFunctions(
MockTestState.Object,
MockSingleTestInstanceState.Object,
MockFileSystem.Object,
page: MockPage.Object,
testWebProvider: MockTestWebProvider.Object);

var result = await playwrightTestInfraFunctions.RunJavascriptAsync<string>(jsExpression, argument);

Assert.Equal(expectedResponse, result);
MockPage.Verify(x => x.EvaluateAsync<string>(jsExpression, argument), Times.Once());
}

[Fact]
public async Task RouteNetworkRequestTest()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ public interface ITestInfraFunctions
/// <returns>Return value of javascript</returns>
public Task<T> RunJavascriptAsync<T>(string jsExpression);

/// <summary>
/// Runs javascript on the page with a structured argument.
/// </summary>
/// <typeparam name="T">Expected return type</typeparam>
/// <param name="jsExpression">Javascript function to run</param>
/// <param name="argument">Argument serialized by Playwright</param>
/// <returns>Return value of javascript</returns>
public Task<T> RunJavascriptAsync<T>(string jsExpression, object argument);

/// <summary>
/// Triggers a click event on a control
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,15 @@ public async Task<T> RunJavascriptAsync<T>(string jsExpression)
return await Page.EvaluateAsync<T>(jsExpression);
}

public async Task<T> RunJavascriptAsync<T>(string jsExpression, object argument)
{
ValidatePage();

_singleTestInstanceState.GetLogger().LogDebug("Run Javascript: " + jsExpression);

return await Page.EvaluateAsync<T>(jsExpression, argument);
}

public async Task AddScriptContentAsync(string content)
{
ValidatePage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ var mockCanvasControl = class {
static getValue() {
return mockValue;
}

static setValue(data) {
mockValue = data;
return true;
}
}
}

Expand Down Expand Up @@ -168,6 +173,14 @@ var mockAppMagic = class {
return undefined;
}
}

static replicatedContexts = class {
static gallery = class {
static bindingContextAt(index) {
return mockAppMagic.Controls.GlobalContextManager.bindingContext;
}
}
}
}
}
}
Expand All @@ -189,7 +202,11 @@ var OpenAjaxClass = class {

static getAuthoringControlContext() {
return class {
static _replicatedContext = null;
static _replicatedContext = class {
static manager = class {
static managerId = 'gallery';
}
};
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ public async Task SetValue(string javaScript, string controlName, string propert
.Returns(
async (string query) => engine.Evaluate(query).AsBoolean()
);
MockTestInfraFunctions.Setup(m => m.RunJavascriptAsync<bool>(It.IsAny<string>(), It.IsAny<object>()))
.Returns(
async (string query, object argument) => engine.Evaluate($"({query})({JsonConvert.SerializeObject(argument)})").AsBoolean()
);

FormulaValue providerValue = null;
if (value is string)
Expand All @@ -276,7 +280,15 @@ public async Task SetValue(string javaScript, string controlName, string propert
var result = await provider.SetPropertyAsync(new ItemPath { ControlName = controlName, PropertyName = propertyName }, providerValue);

// Assert

Assert.True(result);
if (value is string)
{
Assert.Equal(value, engine.Evaluate("mockValue").AsString());
}
else if (value is bool)
{
Assert.Equal(value, engine.Evaluate("mockValue").AsBoolean());
}
}

/// <summary>
Expand All @@ -287,13 +299,70 @@ public static IEnumerable<object[]> GetSetValueData()
{
// Default Values
yield return new object[] {
Common.MockJavaScript("mockPageType = 'custom';mockValue = 'Hello'", "custom"),
Common.MockJavaScript("mockPageType = 'custom';mockValue = 'Initial'", "custom", interfaceResourceNames: new List<string> { "testengine.provider.mda.PowerAppsTestEngineMDA.js", "testengine.provider.mda.PowerAppsTestEngineMDACustom.js" }),
"TextInput1",
"Text",
"Hello"
};
}

[Fact]
public async Task SetStructuredValuesThroughCustomPage()
{
const string payload = "a\"});globalThis.__injected=true;({\"b";
var engine = new Engine();
engine.Execute(Common.MockJavaScript(
"mockPageType = 'custom';mockValue = null",
"custom",
interfaceResourceNames: new List<string>
{
"testengine.provider.mda.PowerAppsTestEngineMDA.js",
"testengine.provider.mda.PowerAppsTestEngineMDACustom.js"
}));

MockTestState.Setup(m => m.GetTimeout()).Returns(1000);
MockTestState.Setup(m => m.GetDomain()).Returns(String.Empty);
MockSingleTestInstanceState.Setup(m => m.GetLogger()).Returns(MockLogger.Object);
MockTestInfraFunctions = new Mock<ITestInfraFunctions>();
MockTestInfraFunctions
.Setup(m => m.RunJavascriptAsync<bool>(It.IsAny<string>(), It.IsAny<object>()))
.Returns(
async (string query, object argument) =>
engine.Evaluate($"({query})({JsonConvert.SerializeObject(argument)})").AsBoolean());

var provider = new ModelDrivenApplicationProvider(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object);
var galleryPath = new ItemPath
{
ControlName = "TextInput1",
PropertyName = "Text",
ParentControl = new ItemPath
{
ControlName = "Gallery",
PropertyName = "Items",
Index = 0
}
};
var record = RecordValue.NewRecordFromFields(new NamedValue("Text", StringValue.New(payload)));
var tableType = RecordType.Empty().Add("Text", FormulaType.String);
var table = TableValue.NewTable(tableType, record);
var guid = Guid.NewGuid();
var date = new DateTime(2026, 9, 8);

Assert.True(await provider.SetPropertyAsync(galleryPath, record));
Assert.Equal($"{{\"Text\":{JsonConvert.SerializeObject(payload)}}}", engine.Evaluate("JSON.stringify(mockValue)").AsString());

Assert.True(await provider.SetPropertyAsync(galleryPath, table));
Assert.Equal($"[{{\"Text\":{JsonConvert.SerializeObject(payload)}}}]", engine.Evaluate("JSON.stringify(mockValue)").AsString());

Assert.True(await provider.SetPropertyAsync(galleryPath, GuidValue.New(guid)));
Assert.Equal(guid.ToString(), engine.Evaluate("mockValue").AsString());

Assert.True(await provider.SetPropertyAsync(galleryPath, DateValue.NewDateOnly(date)));
Assert.True(engine.Evaluate("typeof mockValue === 'number' && !Number.isNaN(mockValue)").AsBoolean());

Assert.False(engine.Evaluate("globalThis.__injected === true").AsBoolean());
}

[Theory]
[MemberData(nameof(GetDataFromParentData))]
public async Task GetValueFromParent(ItemPath path, string parentJson, object? expected)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ public async Task SetValue(string javaScript, string controlName, string propert
.Returns(
async (string query) => engine.Evaluate(query).AsBoolean()
);
MockTestInfraFunctions.Setup(m => m.RunJavascriptAsync<bool>(It.IsAny<string>(), It.IsAny<object>()))
.Returns(
async (string query, object argument) => engine.Evaluate($"({query})({JsonConvert.SerializeObject(argument)})").AsBoolean()
);

FormulaValue providerValue = null;
if (value is string)
Expand Down Expand Up @@ -422,6 +426,137 @@ public async Task SetValue(string javaScript, string controlName, string propert
Assert.Equal(value, controlValue);
}

[Fact]
public async Task SetRecordValuePassesUntrustedDataAsStructuredArgument()
{
const string payload = "a\"});globalThis.__injected=true;({\"b";
const string expression = "(args) => PowerAppsTestEngine.setPropertyValue(args.itemPath, args.value)";
object capturedArgument = null;

MockSingleTestInstanceState.Setup(m => m.GetLogger()).Returns(MockLogger.Object);
MockTestInfraFunctions
.Setup(m => m.RunJavascriptAsync<bool>(expression, It.IsAny<object>()))
.Callback((string _, object argument) => capturedArgument = argument)
.ReturnsAsync(true);

var provider = new ModelDrivenApplicationProvider(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object);
var record = RecordValue.NewRecordFromFields(new NamedValue(payload, StringValue.New(payload)));

var result = await provider.SetPropertyAsync(
new ItemPath { ControlName = "Dropdown1", PropertyName = payload },
record);

Assert.True(result);
Assert.DoesNotContain(payload, expression);
var arguments = Assert.IsType<Dictionary<string, object?>>(capturedArgument);
var itemPath = Assert.IsType<Dictionary<string, object?>>(arguments["itemPath"]);
var value = Assert.IsAssignableFrom<IDictionary<string, object?>>(arguments["value"]);
Assert.Equal(payload, itemPath["propertyName"]);
Assert.Equal(payload, value[payload]);
MockTestInfraFunctions.Verify(
m => m.RunJavascriptAsync<bool>(expression, It.IsAny<object>()),
Times.Once());
}

[Fact]
public async Task SetEmptyRecordSerializesAsJavaScriptObject()
{
const string expression = "(args) => PowerAppsTestEngine.setPropertyValue(args.itemPath, args.value)";
object capturedArgument = null;

MockSingleTestInstanceState.Setup(m => m.GetLogger()).Returns(MockLogger.Object);
MockTestInfraFunctions
.Setup(m => m.RunJavascriptAsync<bool>(expression, It.IsAny<object>()))
.Callback((string _, object argument) => capturedArgument = argument)
.ReturnsAsync(true);

var provider = new ModelDrivenApplicationProvider(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object);
var emptyRecord = RecordValue.NewRecordFromFields(Array.Empty<NamedValue>());

await provider.SetPropertyAsync(
new ItemPath { ControlName = "Dropdown1", PropertyName = "Selected" },
emptyRecord);

var arguments = Assert.IsType<Dictionary<string, object?>>(capturedArgument);
Assert.IsType<ExpandoObject>(arguments["value"]);

var serializedArgument = SerializePlaywrightArgument(capturedArgument);
Assert.Contains("\"k\":\"value\",\"v\":{\"o\":[]", serializedArgument);
Assert.DoesNotContain("\"k\":\"value\",\"v\":{\"a\":[]", serializedArgument);
}

[Fact]
public async Task SetPropertyPreservesStructuredValueShapes()
{
const string standardExpression = "(args) => PowerAppsTestEngine.setPropertyValue(args.itemPath, args.value)";
const string dateExpression = "(args) => PowerAppsTestEngine.setPropertyValue(args.itemPath, Date.parse(args.value))";
const string payload = "a\"});globalThis.__injected=true;({\"b";
var guid = Guid.NewGuid();
var nestedPath = new ItemPath
{
ControlName = "TableRow",
PropertyName = payload,
ParentControl = new ItemPath
{
ControlName = "Gallery",
PropertyName = "Items",
Index = 0
}
};
var record = RecordValue.NewRecordFromFields(new NamedValue("Text", StringValue.New(payload)));
var tableType = RecordType.Empty().Add("Text", FormulaType.String);
var table = TableValue.NewTable(tableType, record);
var calls = new List<(string Expression, object Argument)>();

MockSingleTestInstanceState.Setup(m => m.GetLogger()).Returns(MockLogger.Object);
MockTestInfraFunctions
.Setup(m => m.RunJavascriptAsync<bool>(It.IsAny<string>(), It.IsAny<object>()))
.Callback((string expression, object argument) => calls.Add((expression, argument)))
.ReturnsAsync(true);

var provider = new ModelDrivenApplicationProvider(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object);

await provider.SetPropertyAsync(nestedPath, StringValue.New(payload));
await provider.SetPropertyAsync(nestedPath, GuidValue.New(guid));
await provider.SetPropertyAsync(nestedPath, record);
await provider.SetPropertyAsync(nestedPath, table);
await provider.SetPropertyAsync(nestedPath, DateValue.NewDateOnly(new DateTime(2026, 9, 8)));

Assert.Equal(5, calls.Count);
Assert.Equal(4, calls.Count(call => call.Expression == standardExpression));
Assert.Single(calls, call => call.Expression == dateExpression);
Assert.All(calls, call =>
{
Assert.DoesNotContain(payload, call.Expression);
var serializedArgument = SerializePlaywrightArgument(call.Argument);
Assert.Contains(payload.Replace("\"", "\\u0022"), serializedArgument);
Assert.Contains("\"k\":\"parentControl\"", serializedArgument);
});

var standardArguments = calls
.Where(call => call.Expression == standardExpression)
.Select(call => Assert.IsType<Dictionary<string, object?>>(call.Argument))
.ToList();
Assert.Equal(payload, standardArguments[0]["value"]);
Assert.Equal(guid, standardArguments[1]["value"]);
Assert.IsType<ExpandoObject>(standardArguments[2]["value"]);
Assert.IsType<List<object?>>(standardArguments[3]["value"]);
}

private static string SerializePlaywrightArgument(object argument)
{
var assembly = typeof(IPage).Assembly;
var converterType = assembly.GetType("Microsoft.Playwright.Transport.Converters.EvaluateArgumentValueConverter");
var visitorType = assembly.GetType("Microsoft.Playwright.Transport.Converters.EvaluateArgumentValueConverter+VisitorInfo");
var handleType = assembly.GetType("Microsoft.Playwright.Core.EvaluateArgumentGuidElement");
var serializeMethod = converterType.GetMethod("Serialize", BindingFlags.NonPublic | BindingFlags.Static);
var handles = Activator.CreateInstance(typeof(List<>).MakeGenericType(handleType));
var visitor = Activator.CreateInstance(visitorType, nonPublic: true);
var serialized = serializeMethod.Invoke(null, new[] { argument, handles, visitor });

return global::System.Text.Json.JsonSerializer.Serialize(serialized, serialized.GetType());
}

/// <summary>
/// Test data for <see cref="SetValue"/>
/// </summary>
Expand Down Expand Up @@ -780,7 +915,7 @@ public async Task SetRecordValue(FormulaValue data)
// Arrange
MockSingleTestInstanceState.Setup(m => m.GetLogger()).Returns(MockLogger.Object);
MockTestInfraFunctions = new Mock<ITestInfraFunctions>();
MockTestInfraFunctions.Setup(m => m.RunJavascriptAsync<string>(It.IsAny<string>()));
MockTestInfraFunctions.Setup(m => m.RunJavascriptAsync<bool>(It.IsAny<string>(), It.IsAny<object>())).ReturnsAsync(true);

var provider = new ModelDrivenApplicationProvider(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockTestState.Object);

Expand Down
Loading
Loading