| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <cstring> |
| |
| #include "base/base_paths.h" |
| #include "base/command_line.h" |
| #include "base/debug/debugger.h" |
| #include "base/environment.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/vr/test/xr_browser_test.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "url/gurl.h" |
| |
| namespace vr { |
| |
| constexpr base::TimeDelta XrBrowserTestBase::kPollCheckIntervalShort; |
| constexpr base::TimeDelta XrBrowserTestBase::kPollCheckIntervalLong; |
| constexpr base::TimeDelta XrBrowserTestBase::kPollTimeoutShort; |
| constexpr base::TimeDelta XrBrowserTestBase::kPollTimeoutMedium; |
| constexpr base::TimeDelta XrBrowserTestBase::kPollTimeoutLong; |
| constexpr char XrBrowserTestBase::kOpenXrConfigPathEnvVar[]; |
| constexpr char XrBrowserTestBase::kOpenXrConfigPathVal[]; |
| constexpr char XrBrowserTestBase::kTestFileDir[]; |
| constexpr char XrBrowserTestBase::kSwitchIgnoreRuntimeRequirements[]; |
| const std::vector<std::string> XrBrowserTestBase::kRequiredTestSwitches{ |
| "enable-gpu", "enable-pixel-output-in-tests", |
| "run-through-xr-wrapper-script"}; |
| const std::vector<std::pair<std::string, std::string>> |
| XrBrowserTestBase::kRequiredTestSwitchesWithValues{ |
| std::pair<std::string, std::string>("test-launcher-jobs", "1")}; |
| |
| XrBrowserTestBase::XrBrowserTestBase() : env_(base::Environment::Create()) { |
| enable_features_.push_back(features::kLogJsConsoleMessages); |
| } |
| |
| XrBrowserTestBase::~XrBrowserTestBase() = default; |
| |
| base::FilePath::StringType UTF8ToWideIfNecessary(std::string input) { |
| #if BUILDFLAG(IS_WIN) |
| return base::UTF8ToWide(input); |
| #else |
| return input; |
| #endif // BUILDFLAG(IS_WIN) |
| } |
| |
| std::string WideToUTF8IfNecessary(base::FilePath::StringType input) { |
| #if BUILDFLAG(IS_WIN) |
| return base::WideToUTF8(input); |
| #else |
| return input; |
| #endif // BUILDFLAG(IS_WIN) |
| } |
| |
| // Returns an std::string consisting of the given path relative to the test |
| // executable's path, e.g. if the executable is in out/Debug and the given path |
| // is "test", the returned string should be out/Debug/test. |
| std::string MakeExecutableRelative(const char* path) { |
| base::FilePath executable_path; |
| EXPECT_TRUE( |
| base::PathService::Get(base::BasePathKey::FILE_EXE, &executable_path)); |
| executable_path = executable_path.DirName(); |
| // We need an std::string that is an absolute file path, which requires |
| // platform-specific logic since Windows uses std::wstring instead of |
| // std::string for FilePaths, but SetVar only accepts std::string. |
| return WideToUTF8IfNecessary( |
| base::MakeAbsoluteFilePath( |
| executable_path.Append(base::FilePath(UTF8ToWideIfNecessary(path)))) |
| .value()); |
| } |
| |
| void XrBrowserTestBase::SetUp() { |
| // Check whether the required flags were passed to the test - without these, |
| // we can fail in ways that are non-obvious, so fail more explicitly here if |
| // they aren't present. |
| auto* cmd_line = base::CommandLine::ForCurrentProcess(); |
| for (auto req_switch : kRequiredTestSwitches) { |
| ASSERT_TRUE(cmd_line->HasSwitch(req_switch)) |
| << "Missing switch " << req_switch << " required to run tests properly"; |
| } |
| for (auto req_switch_pair : kRequiredTestSwitchesWithValues) { |
| ASSERT_TRUE(cmd_line->HasSwitch(req_switch_pair.first)) |
| << "Missing switch " << req_switch_pair.first |
| << " required to run tests properly"; |
| ASSERT_TRUE(cmd_line->GetSwitchValueASCII(req_switch_pair.first) == |
| req_switch_pair.second) |
| << "Have required switch " << req_switch_pair.first |
| << ", but not required value " << req_switch_pair.second; |
| } |
| |
| // Get the set of runtime requirements to ignore. |
| if (cmd_line->HasSwitch(kSwitchIgnoreRuntimeRequirements)) { |
| auto reqs = cmd_line->GetSwitchValueASCII(kSwitchIgnoreRuntimeRequirements); |
| if (reqs != "") { |
| for (auto req : base::SplitString( |
| reqs, ",", base::WhitespaceHandling::TRIM_WHITESPACE, |
| base::SplitResult::SPLIT_WANT_NONEMPTY)) { |
| ignored_requirements_.insert(req); |
| } |
| } |
| } |
| |
| // Check whether we meet all runtime requirements for this test. |
| XR_CONDITIONAL_SKIP_PRETEST(runtime_requirements_, ignored_requirements_, |
| &test_skipped_at_startup_) |
| |
| // Set the environment variable to use the mock OpenXR client. |
| // If the kOpenXrConfigPathEnvVar environment variable is set, the OpenXR |
| // loader will look for the OpenXR runtime specified in that json file. The |
| // json file contains the path to the runtime, relative to the json file |
| // itself. Otherwise, the OpenXR loader loads the active OpenXR runtime |
| // installed on the system, which is specified by a registry key. |
| ASSERT_TRUE(env_->SetVar(kOpenXrConfigPathEnvVar, |
| MakeExecutableRelative(kOpenXrConfigPathVal))) |
| << "Failed to set OpenXR JSON location environment variable"; |
| |
| // Set any command line flags that subclasses have set, e.g. enabling features |
| // or specific runtimes. |
| for (const auto& switch_string : append_switches_) { |
| cmd_line->AppendSwitch(switch_string); |
| } |
| |
| for (const auto& blink_feature : enable_blink_features_) { |
| cmd_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, blink_feature); |
| } |
| |
| scoped_feature_list_.InitWithFeatures(enable_features_, disable_features_); |
| |
| InProcessBrowserTest::SetUp(); |
| } |
| |
| void XrBrowserTestBase::TearDown() { |
| if (test_skipped_at_startup_) { |
| // Since we didn't complete startup, no need to do teardown, either. Doing |
| // so can result in hitting a DCHECK. |
| return; |
| } |
| InProcessBrowserTest::TearDown(); |
| } |
| |
| XrBrowserTestBase::RuntimeType XrBrowserTestBase::GetRuntimeType() const { |
| return XrBrowserTestBase::RuntimeType::RUNTIME_NONE; |
| } |
| |
| GURL XrBrowserTestBase::GetUrlForFile(const std::string& test_name) { |
| // GetURL requires that the path start with /. |
| return GetEmbeddedServer()->GetURL(std::string("/") + kTestFileDir + |
| test_name + ".html"); |
| } |
| |
| net::EmbeddedTestServer* XrBrowserTestBase::GetEmbeddedServer() { |
| if (server_ == nullptr) { |
| server_ = std::make_unique<net::EmbeddedTestServer>( |
| net::EmbeddedTestServer::Type::TYPE_HTTPS); |
| // We need to serve from the root in order for the inclusion of the |
| // test harness from //third_party to work. |
| server_->ServeFilesFromSourceDirectory("."); |
| EXPECT_TRUE(server_->Start()) << "Failed to start embedded test server"; |
| } |
| return server_.get(); |
| } |
| |
| content::WebContents* XrBrowserTestBase::GetCurrentWebContents() { |
| return browser()->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| void XrBrowserTestBase::LoadFileAndAwaitInitialization( |
| const std::string& test_name) { |
| GURL url = GetUrlForFile(test_name); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| ASSERT_TRUE(PollJavaScriptBoolean("isInitializationComplete()", |
| kPollTimeoutMedium, |
| GetCurrentWebContents())) |
| << "Timed out waiting for JavaScript test initialization."; |
| |
| #if BUILDFLAG(IS_WIN) |
| // Now that the browser is opened and has focus, keep track of this window so |
| // that we can restore the proper focus after entering each session. This is |
| // required for tests that create multiple sessions to work properly. |
| hwnd_ = GetForegroundWindow(); |
| #endif |
| } |
| |
| void XrBrowserTestBase::RunJavaScriptOrFail( |
| const std::string& js_expression, |
| content::WebContents* web_contents) { |
| if (javascript_failed_) { |
| LogJavaScriptFailure(); |
| return; |
| } |
| |
| ASSERT_TRUE(content::ExecuteScript(web_contents, js_expression)) |
| << "Failed to run given JavaScript: " << js_expression; |
| } |
| |
| bool XrBrowserTestBase::RunJavaScriptAndExtractBoolOrFail( |
| const std::string& js_expression, |
| content::WebContents* web_contents) { |
| if (javascript_failed_) { |
| LogJavaScriptFailure(); |
| return false; |
| } |
| |
| bool result = false; |
| DLOG(INFO) << "Run JavaScript: " << js_expression; |
| EXPECT_TRUE(content::ExecuteScriptAndExtractBool( |
| web_contents, |
| "window.domAutomationController.send(" + js_expression + ")", &result)) |
| << "Failed to run given JavaScript for bool: " << js_expression; |
| return result; |
| } |
| |
| std::string XrBrowserTestBase::RunJavaScriptAndExtractStringOrFail( |
| const std::string& js_expression, |
| content::WebContents* web_contents) { |
| if (javascript_failed_) { |
| LogJavaScriptFailure(); |
| return ""; |
| } |
| |
| std::string result; |
| EXPECT_TRUE(content::ExecuteScriptAndExtractString( |
| web_contents, |
| "window.domAutomationController.send(" + js_expression + ")", &result)) |
| << "Failed to run given JavaScript for string: " << js_expression; |
| return result; |
| } |
| |
| bool XrBrowserTestBase::PollJavaScriptBoolean( |
| const std::string& bool_expression, |
| const base::TimeDelta& timeout, |
| content::WebContents* web_contents) { |
| bool result = false; |
| base::RunLoop wait_loop(base::RunLoop::Type::kNestableTasksAllowed); |
| // Lambda used because otherwise BindRepeating gets confused about which |
| // version of RunJavaScriptAndExtractBoolOrFail to use. |
| BlockOnCondition(base::BindRepeating( |
| [](XrBrowserTestBase* base, std::string expression, |
| content::WebContents* contents) { |
| return base->RunJavaScriptAndExtractBoolOrFail( |
| expression, contents); |
| }, |
| this, bool_expression, web_contents), |
| &result, &wait_loop, base::Time::Now(), timeout); |
| wait_loop.Run(); |
| return result; |
| } |
| |
| void XrBrowserTestBase::PollJavaScriptBooleanOrFail( |
| const std::string& bool_expression, |
| const base::TimeDelta& timeout, |
| content::WebContents* web_contents) { |
| ASSERT_TRUE(PollJavaScriptBoolean(bool_expression, timeout, web_contents)) |
| << "Timed out polling JavaScript boolean expression: " << bool_expression; |
| } |
| |
| void XrBrowserTestBase::BlockOnCondition( |
| base::RepeatingCallback<bool()> condition, |
| bool* result, |
| base::RunLoop* wait_loop, |
| const base::Time& start_time, |
| const base::TimeDelta& timeout, |
| const base::TimeDelta& period) { |
| if (!*result) { |
| *result = condition.Run(); |
| } |
| |
| if (*result) { |
| if (wait_loop->running()) { |
| wait_loop->Quit(); |
| return; |
| } |
| // In the case where the condition is met fast enough that the given |
| // RunLoop hasn't started yet, spin until it's available. |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&XrBrowserTestBase::BlockOnCondition, |
| base::Unretained(this), std::move(condition), |
| base::Unretained(result), base::Unretained(wait_loop), |
| start_time, timeout, period)); |
| return; |
| } |
| |
| if (base::Time::Now() - start_time > timeout && |
| !base::debug::BeingDebugged()) { |
| wait_loop->Quit(); |
| return; |
| } |
| |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&XrBrowserTestBase::BlockOnCondition, |
| base::Unretained(this), std::move(condition), |
| base::Unretained(result), base::Unretained(wait_loop), |
| start_time, timeout, period), |
| period); |
| } |
| |
| void XrBrowserTestBase::WaitOnJavaScriptStep( |
| content::WebContents* web_contents) { |
| // Make sure we aren't trying to wait on a JavaScript test step without the |
| // code to do so. |
| bool code_available = RunJavaScriptAndExtractBoolOrFail( |
| "typeof javascriptDone !== 'undefined'", web_contents); |
| ASSERT_TRUE(code_available) << "Attempted to wait on a JavaScript test step " |
| << "without the code to do so. You either forgot " |
| << "to import webxr_e2e.js or " |
| << "are incorrectly using a C++ function."; |
| |
| // Actually wait for the step to finish. |
| bool success = |
| PollJavaScriptBoolean("javascriptDone", kPollTimeoutLong, web_contents); |
| |
| // Check what state we're in to make sure javascriptDone wasn't called |
| // because the test failed. |
| XrBrowserTestBase::TestStatus test_status = CheckTestStatus(web_contents); |
| if (!success || test_status == XrBrowserTestBase::TestStatus::STATUS_FAILED) { |
| // Failure states: Either polling failed or polling succeeded, but because |
| // the test failed. |
| std::string reason; |
| if (!success) { |
| reason = "Timed out waiting for JavaScript step to finish."; |
| } else { |
| reason = |
| "JavaScript testharness reported failure while waiting for " |
| "JavaScript step to finish"; |
| } |
| |
| std::string result_string = |
| RunJavaScriptAndExtractStringOrFail("resultString", web_contents); |
| if (result_string.empty()) { |
| reason += |
| " Did not obtain specific failure reason from JavaScript " |
| "testharness."; |
| } else { |
| reason += |
| " JavaScript testharness reported failure reason: " + result_string; |
| } |
| // Store that we've failed waiting for a JavaScript step so we can abort |
| // further attempts to run JavaScript, which has the potential to do weird |
| // things and produce non-useful output due to JavaScript code continuing |
| // to run when it's in a known bad state. |
| // This is a workaround for the fact that FAIL() and other gtest macros that |
| // cause test failures only abort the current function. Thus, a failure here |
| // will show up as a test failure, but there's nothing that actually stops |
| // the test from continuing to run since FAIL() is not being called in the |
| // main test body. |
| javascript_failed_ = true; |
| // Newlines to help the failure reason stick out. |
| LOG(ERROR) << "\n\n\nvvvvvvvvvvvvvvvvv Useful Stack vvvvvvvvvvvvvvvvv\n\n"; |
| FAIL() << reason; |
| } |
| |
| // Reset the synchronization boolean. |
| RunJavaScriptOrFail("javascriptDone = false", web_contents); |
| } |
| |
| void XrBrowserTestBase::ExecuteStepAndWait(const std::string& step_function, |
| content::WebContents* web_contents) { |
| RunJavaScriptOrFail(step_function, web_contents); |
| WaitOnJavaScriptStep(web_contents); |
| } |
| |
| XrBrowserTestBase::TestStatus XrBrowserTestBase::CheckTestStatus( |
| content::WebContents* web_contents) { |
| std::string result_string = |
| RunJavaScriptAndExtractStringOrFail("resultString", web_contents); |
| bool test_passed = |
| RunJavaScriptAndExtractBoolOrFail("testPassed", web_contents); |
| if (test_passed) { |
| return XrBrowserTestBase::TestStatus::STATUS_PASSED; |
| } else if (!test_passed && result_string.empty()) { |
| return XrBrowserTestBase::TestStatus::STATUS_RUNNING; |
| } |
| // !test_passed && result_string != "" |
| return XrBrowserTestBase::TestStatus::STATUS_FAILED; |
| } |
| |
| void XrBrowserTestBase::EndTest(content::WebContents* web_contents) { |
| switch (CheckTestStatus(web_contents)) { |
| case XrBrowserTestBase::TestStatus::STATUS_PASSED: |
| break; |
| case XrBrowserTestBase::TestStatus::STATUS_FAILED: |
| FAIL() << "JavaScript testharness failed with reason: " |
| << RunJavaScriptAndExtractStringOrFail("resultString", |
| web_contents); |
| case XrBrowserTestBase::TestStatus::STATUS_RUNNING: |
| FAIL() << "Attempted to end test in C++ without finishing in JavaScript."; |
| default: |
| FAIL() << "Received unknown test status."; |
| } |
| } |
| |
| void XrBrowserTestBase::AssertNoJavaScriptErrors( |
| content::WebContents* web_contents) { |
| if (CheckTestStatus(web_contents) == |
| XrBrowserTestBase::TestStatus::STATUS_FAILED) { |
| FAIL() << "JavaScript testharness failed with reason: " |
| << RunJavaScriptAndExtractStringOrFail("resultString", web_contents); |
| } |
| } |
| |
| void XrBrowserTestBase::RunJavaScriptOrFail(const std::string& js_expression) { |
| RunJavaScriptOrFail(js_expression, GetCurrentWebContents()); |
| } |
| |
| bool XrBrowserTestBase::RunJavaScriptAndExtractBoolOrFail( |
| const std::string& js_expression) { |
| return RunJavaScriptAndExtractBoolOrFail(js_expression, |
| GetCurrentWebContents()); |
| } |
| |
| std::string XrBrowserTestBase::RunJavaScriptAndExtractStringOrFail( |
| const std::string& js_expression) { |
| return RunJavaScriptAndExtractStringOrFail(js_expression, |
| GetCurrentWebContents()); |
| } |
| |
| bool XrBrowserTestBase::PollJavaScriptBoolean( |
| const std::string& bool_expression, |
| const base::TimeDelta& timeout) { |
| return PollJavaScriptBoolean(bool_expression, timeout, |
| GetCurrentWebContents()); |
| } |
| |
| void XrBrowserTestBase::PollJavaScriptBooleanOrFail( |
| const std::string& bool_expression, |
| const base::TimeDelta& timeout) { |
| PollJavaScriptBooleanOrFail(bool_expression, timeout, |
| GetCurrentWebContents()); |
| } |
| |
| void XrBrowserTestBase::WaitOnJavaScriptStep() { |
| WaitOnJavaScriptStep(GetCurrentWebContents()); |
| } |
| |
| void XrBrowserTestBase::ExecuteStepAndWait(const std::string& step_function) { |
| ExecuteStepAndWait(step_function, GetCurrentWebContents()); |
| } |
| |
| void XrBrowserTestBase::EndTest() { |
| EndTest(GetCurrentWebContents()); |
| } |
| |
| void XrBrowserTestBase::AssertNoJavaScriptErrors() { |
| AssertNoJavaScriptErrors(GetCurrentWebContents()); |
| } |
| |
| void XrBrowserTestBase::LogJavaScriptFailure() { |
| LOG(ERROR) << "HEY! LISTEN! Not running requested JavaScript due to previous " |
| "failure. Failures below this are likely garbage. Look for the " |
| "useful stack above."; |
| } |
| |
| } // namespace vr |