blob: 6760df259746cfe460f2f06c6a7fe601dcb58250 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/autofill_assistant/browser/js_flow_executor.h"
#include "base/callback.h"
#include "base/json/json_reader.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "base/time/tick_clock.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "content/shell/browser/shell.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/switches.h"
namespace autofill_assistant {
namespace {
using ::base::test::RunOnceCallback;
using ::testing::_;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Ne;
using ::testing::NiceMock;
using ::testing::Pair;
using ::testing::Pointee;
using ::testing::Property;
using ::testing::SizeIs;
using ::testing::WithArg;
// Parses |json| as a base::Value. No error handling - this will crash for
// invalid json inputs.
std::unique_ptr<base::Value> UniqueValueFromJson(const std::string& json) {
return std::make_unique<base::Value>(
std::move(*base::JSONReader::Read(json)));
}
class MockJsFlowExecutorDelegate : public JsFlowExecutor::Delegate {
public:
MockJsFlowExecutorDelegate() = default;
~MockJsFlowExecutorDelegate() override = default;
MOCK_METHOD(
void,
RunNativeAction,
(std::unique_ptr<base::Value> native_action,
base::OnceCallback<void(const ClientStatus& result_status,
std::unique_ptr<base::Value> result_value)>
callback),
(override));
};
class JsFlowExecutorTest : public content::ContentBrowserTest {
public:
JsFlowExecutorTest() {}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch("site-per-process");
// Necessary to avoid flakiness or failure due to input arriving
// before the first compositor commit.
command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
// Start a mock server for hosting an OOPIF.
http_server_iframe_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTP);
http_server_iframe_->ServeFilesFromSourceDirectory(
"components/test/data/autofill_assistant/html_iframe");
ASSERT_TRUE(http_server_iframe_->Start(8081));
// Start the main server hosting the test page.
http_server_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTP);
http_server_->ServeFilesFromSourceDirectory(
"components/test/data/autofill_assistant/html");
ASSERT_TRUE(http_server_->Start(8080));
ASSERT_TRUE(NavigateToURL(
shell(),
http_server_->GetURL("/autofill_assistant_target_website.html")));
flow_executor_ = std::make_unique<JsFlowExecutor>(shell()->web_contents(),
&mock_delegate_);
}
// Overload, ignore result value, just return the client status.
ClientStatus RunTest(const std::string& js_flow) {
std::unique_ptr<base::Value> ignored_result;
return RunTest(js_flow, ignored_result);
}
ClientStatus RunTest(const std::string& js_flow,
std::unique_ptr<base::Value>& result_value) {
ClientStatus status;
base::RunLoop run_loop;
flow_executor_->Start(
js_flow, base::BindOnce(&JsFlowExecutorTest::OnFlowFinished,
base::Unretained(this), run_loop.QuitClosure(),
&status, std::ref(result_value)));
run_loop.Run();
return status;
}
void OnFlowFinished(base::OnceClosure done_callback,
ClientStatus* status_output,
std::unique_ptr<base::Value>& result_output,
const ClientStatus& status,
std::unique_ptr<base::Value> result_value) {
*status_output = status;
result_output = std::move(result_value);
std::move(done_callback).Run();
}
protected:
NiceMock<MockJsFlowExecutorDelegate> mock_delegate_;
std::unique_ptr<JsFlowExecutor> flow_executor_;
std::unique_ptr<net::EmbeddedTestServer> http_server_;
std::unique_ptr<net::EmbeddedTestServer> http_server_iframe_;
};
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, SmokeTest) {
EXPECT_THAT(RunTest(std::string()),
Property(&ClientStatus::proto_status, ACTION_APPLIED));
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, InvalidJs) {
EXPECT_THAT(RunTest("Not valid Javascript"),
Property(&ClientStatus::proto_status, UNEXPECTED_JS_ERROR));
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, RunNativeActionWithReturnValue) {
std::unique_ptr<base::Value> native_return_value =
std::make_unique<base::Value>(std::move(*base::JSONReader::Read(
R"(
{
"keyA":12345,
"keyB":"Hello world",
"keyC": ["array", "of", "strings"],
"keyD": {
"keyE": "nested",
"keyF": true,
"keyG": 123.45,
"keyH": null
}
}
)")));
EXPECT_CALL(mock_delegate_, RunNativeAction)
.WillOnce([&](auto value, auto callback) {
EXPECT_EQ(*value, *UniqueValueFromJson(R"(
{"type":"string",
"value":"test"})"));
std::move(callback).Run(ClientStatus(ACTION_APPLIED),
std::move(native_return_value));
});
std::unique_ptr<base::Value> js_return_value;
EXPECT_THAT(RunTest(R"(
let [status, value] = await runNativeAction('test');
if (status != 2) { // ACTION_APPLIED
return status;
}
value.keyA += 3;
value.keyB += '!';
value.keyD.keyF = false;
return value;
)",
js_return_value),
Property(&ClientStatus::proto_status, ACTION_APPLIED));
EXPECT_EQ(*js_return_value, *base::JSONReader::Read(R"(
{
"result": {
"type": "object",
"value": {
"keyA": 12348,
"keyB": "Hello world!",
"keyC": [ "array", "of", "strings" ],
"keyD": {
"keyE": "nested",
"keyF": false,
"keyG": 123.45,
"keyH": null
}
}
}
}
)"));
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, RunMultipleNativeActions) {
EXPECT_CALL(mock_delegate_, RunNativeAction)
.WillOnce([&](auto value, auto callback) {
EXPECT_EQ(*value, *UniqueValueFromJson(R"(
{"type":"string",
"value":"test1"})"));
std::move(callback).Run(ClientStatus(ACTION_APPLIED), nullptr);
})
.WillOnce([&](auto value, auto callback) {
EXPECT_EQ(*value, *UniqueValueFromJson(R"(
{"type":"string",
"value":"test2"})"));
std::move(callback).Run(ClientStatus(OTHER_ACTION_STATUS), nullptr);
});
// Note: the overall flow should report ACTION_APPLIED since the flow
// completed successfully, but the return value should hold
// OTHER_ACTION_STATUS, i.e., 3.
std::unique_ptr<base::Value> result;
EXPECT_THAT(RunTest(R"(
let [status, value] = await runNativeAction('test1');
if (status == 2) { // ACTION_APPLIED
[status, value] = await runNativeAction('test2');
}
return status;
)",
result),
Property(&ClientStatus::proto_status, ACTION_APPLIED));
EXPECT_EQ(*result, *base::JSONReader::Read(R"(
{
"result": {
"description": "3",
"type": "number",
"value": 3
}
}
)"));
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, ReturnInteger) {
std::unique_ptr<base::Value> result;
ClientStatus status = RunTest("return 12345;", result);
EXPECT_EQ(status.proto_status(), ACTION_APPLIED);
EXPECT_EQ(*result, *base::JSONReader::Read(R"(
{
"result": {
"description": "12345",
"type": "number",
"value": 12345
}
}
)"));
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, ReturnString) {
std::unique_ptr<base::Value> result;
ClientStatus status = RunTest("return 'Hello world!';", result);
EXPECT_EQ(status.proto_status(), ACTION_APPLIED);
EXPECT_EQ(*result, *base::JSONReader::Read(R"(
{
"result": {
"type": "string",
"value": "Hello world!"
}
}
)"));
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, ReturnDictionary) {
std::unique_ptr<base::Value> result;
ClientStatus status = RunTest(
R"(
return {
"keyA":12345,
"keyB":"Hello world!",
"keyC": ["array", "of", "strings"],
"keyD": {
"keyE": "nested",
"keyF": true,
"keyG": 123.45,
"keyH": null
}
};
)",
result);
EXPECT_EQ(status.proto_status(), ACTION_APPLIED);
EXPECT_EQ(*result, *base::JSONReader::Read(R"(
{
"result": {
"type": "object",
"value": {
"keyA": 12345,
"keyB": "Hello world!",
"keyC": ["array", "of", "strings"],
"keyD": {
"keyE": "nested",
"keyF": true,
"keyG": 123.45,
"keyH": null
}
}
}
}
)"));
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, ReturnNothing) {
std::unique_ptr<base::Value> result;
ClientStatus status = RunTest("", result);
EXPECT_EQ(status.proto_status(), ACTION_APPLIED);
EXPECT_EQ(*result, *base::JSONReader::Read(R"(
{
"result": {
"type": "undefined"
}
}
)"));
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, ReturnNull) {
std::unique_ptr<base::Value> result;
ClientStatus status = RunTest("return null;", result);
EXPECT_EQ(status.proto_status(), ACTION_APPLIED);
EXPECT_EQ(*result, *base::JSONReader::Read(R"(
{
"result": {
"subtype": "null",
"type": "object",
"value": null
}
}
)"));
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, ExceptionReporting) {
std::unique_ptr<base::Value> result;
ClientStatus status = RunTest("throw new Error('Hello world!');", result);
EXPECT_EQ(status.proto_status(), UNEXPECTED_JS_ERROR);
ASSERT_NE(result, nullptr);
absl::optional<base::Value> exceptionDetails =
result->ExtractKey("exceptionDetails");
ASSERT_NE(exceptionDetails, absl::nullopt);
EXPECT_THAT(exceptionDetails->ExtractKey("text")->GetIfString(),
Pointee(Eq("Uncaught (in promise) Error: Hello world!")));
// We can't currently check the contents of the reported stack frames since
// they depend on the internal wrapper. For now, we simply test that this is
// not empty. For reference, at the time of writing, this was the full output:
// "exceptionDetails": {
// "columnNumber": 0,
// "exception": {
// "className": "Error",
// "description": "Error: Hello world!
// at <anonymous>:13:11
// at <anonymous>:13:41",
// "objectId": "1450023673453216843.4.1",
// "subtype": "error",
// "type": "object"
// },
// "exceptionId": 2,
// "lineNumber": 0,
// "text": "Uncaught (in promise) Error: Hello world!"
// }
EXPECT_THAT(exceptionDetails->ExtractKey("exception"), Ne(absl::nullopt));
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, RunMultipleConsecutiveFlows) {
for (int i = 0; i < 10; ++i) {
std::unique_ptr<base::Value> result;
ClientStatus status =
RunTest(base::StrCat({"return ", base::NumberToString(i)}), result);
EXPECT_EQ(status.proto_status(), ACTION_APPLIED);
EXPECT_EQ(result->ExtractKey("result")->ExtractKey("value")->GetIfInt(), i);
}
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest,
UnserializableRunNativeActionArgument) {
std::unique_ptr<base::Value> result;
EXPECT_CALL(mock_delegate_, RunNativeAction).Times(0);
ClientStatus status = RunTest(
R"(
function foo(){}
// foo cannot be serialized as a JSON object, so this should fail.
let [status, result] = await runNativeAction(foo);
return status;
)",
result);
EXPECT_EQ(result, nullptr);
EXPECT_EQ(status.proto_status(), UNEXPECTED_JS_ERROR);
EXPECT_TRUE(status.details().has_unexpected_error_info());
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, StartWhileAlreadyRunningFails) {
EXPECT_CALL(mock_delegate_, RunNativeAction)
.WillOnce(WithArg<1>([&](auto callback) {
// Starting a second flow while the first one is running should fail.
EXPECT_EQ(RunTest(std::string()).proto_status(), INVALID_ACTION);
// The first flow should be able to finish successfully.
std::move(callback).Run(ClientStatus(ACTION_APPLIED), nullptr);
}));
std::unique_ptr<base::Value> result;
ClientStatus status = RunTest(
R"(
let [status, result] = await runNativeAction('');
return status;
)",
result);
EXPECT_EQ(status.proto_status(), ACTION_APPLIED);
EXPECT_EQ(*result, *base::JSONReader::Read(R"(
{
"result": {
"description": "2",
"type": "number",
"value": 2
}
}
)"));
}
IN_PROC_BROWSER_TEST_F(JsFlowExecutorTest, EnvironmentIsPreservedBetweenRuns) {
EXPECT_EQ(RunTest("globalFlowState.i = 5;").proto_status(), ACTION_APPLIED);
std::unique_ptr<base::Value> result;
EXPECT_EQ(RunTest("return globalFlowState.i;", result).proto_status(),
ACTION_APPLIED);
EXPECT_EQ(*result, *base::JSONReader::Read(R"(
{
"result": {
"description": "5",
"type": "number",
"value": 5
}
}
)"));
}
} // namespace
} // namespace autofill_assistant