1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include <cstring>
6 
7 #include "base/base_paths.h"
8 #include "base/bind.h"
9 #include "base/callback.h"
10 #include "base/command_line.h"
11 #include "base/debug/debugger.h"
12 #include "base/environment.h"
13 #include "base/files/file_path.h"
14 #include "base/files/file_util.h"
15 #include "base/path_service.h"
16 #include "base/strings/string_split.h"
17 #include "base/strings/utf_string_conversions.h"
18 #include "base/time/time.h"
19 #include "build/build_config.h"
20 #include "chrome/browser/ui/browser.h"
21 #include "chrome/browser/ui/tabs/tab_strip_model.h"
22 #include "chrome/browser/vr/test/xr_browser_test.h"
23 #include "chrome/test/base/in_process_browser_test.h"
24 #include "chrome/test/base/ui_test_utils.h"
25 #include "content/public/browser/web_contents.h"
26 #include "content/public/common/content_features.h"
27 #include "content/public/common/content_switches.h"
28 #include "content/public/test/browser_test_utils.h"
29 #include "url/gurl.h"
30 
31 namespace vr {
32 
33 constexpr base::TimeDelta XrBrowserTestBase::kPollCheckIntervalShort;
34 constexpr base::TimeDelta XrBrowserTestBase::kPollCheckIntervalLong;
35 constexpr base::TimeDelta XrBrowserTestBase::kPollTimeoutShort;
36 constexpr base::TimeDelta XrBrowserTestBase::kPollTimeoutMedium;
37 constexpr base::TimeDelta XrBrowserTestBase::kPollTimeoutLong;
38 constexpr char XrBrowserTestBase::kOpenXrConfigPathEnvVar[];
39 constexpr char XrBrowserTestBase::kOpenXrConfigPathVal[];
40 constexpr char XrBrowserTestBase::kTestFileDir[];
41 constexpr char XrBrowserTestBase::kSwitchIgnoreRuntimeRequirements[];
42 const std::vector<std::string> XrBrowserTestBase::kRequiredTestSwitches{
43     "enable-gpu", "enable-pixel-output-in-tests",
44     "run-through-xr-wrapper-script"};
45 const std::vector<std::pair<std::string, std::string>>
46     XrBrowserTestBase::kRequiredTestSwitchesWithValues{
47         std::pair<std::string, std::string>("test-launcher-jobs", "1")};
48 
XrBrowserTestBase()49 XrBrowserTestBase::XrBrowserTestBase() : env_(base::Environment::Create()) {
50   enable_features_.push_back(features::kLogJsConsoleMessages);
51 }
52 
53 XrBrowserTestBase::~XrBrowserTestBase() = default;
54 
UTF8ToWideIfNecessary(std::string input)55 base::FilePath::StringType UTF8ToWideIfNecessary(std::string input) {
56 #ifdef OS_WIN
57   return base::UTF8ToWide(input);
58 #else
59   return input;
60 #endif  // OS_WIN
61 }
62 
WideToUTF8IfNecessary(base::FilePath::StringType input)63 std::string WideToUTF8IfNecessary(base::FilePath::StringType input) {
64 #ifdef OS_WIN
65   return base::WideToUTF8(input);
66 #else
67   return input;
68 #endif  // OS_Win
69 }
70 
71 // Returns an std::string consisting of the given path relative to the test
72 // executable's path, e.g. if the executable is in out/Debug and the given path
73 // is "test", the returned string should be out/Debug/test.
MakeExecutableRelative(const char * path)74 std::string MakeExecutableRelative(const char* path) {
75   base::FilePath executable_path;
76   EXPECT_TRUE(
77       base::PathService::Get(base::BasePathKey::FILE_EXE, &executable_path));
78   executable_path = executable_path.DirName();
79   // We need an std::string that is an absolute file path, which requires
80   // platform-specific logic since Windows uses std::wstring instead of
81   // std::string for FilePaths, but SetVar only accepts std::string.
82   return WideToUTF8IfNecessary(
83       base::MakeAbsoluteFilePath(
84           executable_path.Append(base::FilePath(UTF8ToWideIfNecessary(path))))
85           .value());
86 }
87 
SetUp()88 void XrBrowserTestBase::SetUp() {
89   // Check whether the required flags were passed to the test - without these,
90   // we can fail in ways that are non-obvious, so fail more explicitly here if
91   // they aren't present.
92   auto* cmd_line = base::CommandLine::ForCurrentProcess();
93   for (auto req_switch : kRequiredTestSwitches) {
94     ASSERT_TRUE(cmd_line->HasSwitch(req_switch))
95         << "Missing switch " << req_switch << " required to run tests properly";
96   }
97   for (auto req_switch_pair : kRequiredTestSwitchesWithValues) {
98     ASSERT_TRUE(cmd_line->HasSwitch(req_switch_pair.first))
99         << "Missing switch " << req_switch_pair.first
100         << " required to run tests properly";
101     ASSERT_TRUE(cmd_line->GetSwitchValueASCII(req_switch_pair.first) ==
102                 req_switch_pair.second)
103         << "Have required switch " << req_switch_pair.first
104         << ", but not required value " << req_switch_pair.second;
105   }
106 
107   // Get the set of runtime requirements to ignore.
108   if (cmd_line->HasSwitch(kSwitchIgnoreRuntimeRequirements)) {
109     auto reqs = cmd_line->GetSwitchValueASCII(kSwitchIgnoreRuntimeRequirements);
110     if (reqs != "") {
111       for (auto req : base::SplitString(
112                reqs, ",", base::WhitespaceHandling::TRIM_WHITESPACE,
113                base::SplitResult::SPLIT_WANT_NONEMPTY)) {
114         ignored_requirements_.insert(req);
115       }
116     }
117   }
118 
119   // Check whether we meet all runtime requirements for this test.
120   XR_CONDITIONAL_SKIP_PRETEST(runtime_requirements_, ignored_requirements_,
121                               &test_skipped_at_startup_)
122 
123   // Set the environment variable to use the mock OpenXR client.
124   // If the kOpenXrConfigPathEnvVar environment variable is set, the OpenXR
125   // loader will look for the OpenXR runtime specified in that json file. The
126   // json file contains the path to the runtime, relative to the json file
127   // itself. Otherwise, the OpenXR loader loads the active OpenXR runtime
128   // installed on the system, which is specified by a registry key.
129   ASSERT_TRUE(env_->SetVar(kOpenXrConfigPathEnvVar,
130                            MakeExecutableRelative(kOpenXrConfigPathVal)))
131       << "Failed to set OpenXR JSON location environment variable";
132 
133   // Set any command line flags that subclasses have set, e.g. enabling features
134   // or specific runtimes.
135   for (const auto& switch_string : append_switches_) {
136     cmd_line->AppendSwitch(switch_string);
137   }
138 
139   for (const auto& blink_feature : enable_blink_features_) {
140     cmd_line->AppendSwitchASCII(switches::kEnableBlinkFeatures, blink_feature);
141   }
142 
143   scoped_feature_list_.InitWithFeatures(enable_features_, disable_features_);
144 
145   InProcessBrowserTest::SetUp();
146 }
147 
TearDown()148 void XrBrowserTestBase::TearDown() {
149   if (test_skipped_at_startup_) {
150     // Since we didn't complete startup, no need to do teardown, either. Doing
151     // so can result in hitting a DCHECK.
152     return;
153   }
154   InProcessBrowserTest::TearDown();
155 }
156 
GetRuntimeType() const157 XrBrowserTestBase::RuntimeType XrBrowserTestBase::GetRuntimeType() const {
158   return XrBrowserTestBase::RuntimeType::RUNTIME_NONE;
159 }
160 
GetUrlForFile(const std::string & test_name)161 GURL XrBrowserTestBase::GetUrlForFile(const std::string& test_name) {
162   // GetURL requires that the path start with /.
163   return GetEmbeddedServer()->GetURL(std::string("/") + kTestFileDir +
164                                      test_name + ".html");
165 }
166 
GetEmbeddedServer()167 net::EmbeddedTestServer* XrBrowserTestBase::GetEmbeddedServer() {
168   if (server_ == nullptr) {
169     server_ = std::make_unique<net::EmbeddedTestServer>(
170         net::EmbeddedTestServer::Type::TYPE_HTTPS);
171     // We need to serve from the root in order for the inclusion of the
172     // test harness from //third_party to work.
173     server_->ServeFilesFromSourceDirectory(".");
174     EXPECT_TRUE(server_->Start()) << "Failed to start embedded test server";
175   }
176   return server_.get();
177 }
178 
GetCurrentWebContents()179 content::WebContents* XrBrowserTestBase::GetCurrentWebContents() {
180   return browser()->tab_strip_model()->GetActiveWebContents();
181 }
182 
LoadFileAndAwaitInitialization(const std::string & test_name)183 void XrBrowserTestBase::LoadFileAndAwaitInitialization(
184     const std::string& test_name) {
185   GURL url = GetUrlForFile(test_name);
186   ui_test_utils::NavigateToURL(browser(), url);
187   ASSERT_TRUE(PollJavaScriptBoolean("isInitializationComplete()",
188                                     kPollTimeoutMedium,
189                                     GetCurrentWebContents()))
190       << "Timed out waiting for JavaScript test initialization.";
191 
192 #if defined(OS_WIN)
193   // Now that the browser is opened and has focus, keep track of this window so
194   // that we can restore the proper focus after entering each session. This is
195   // required for WMR tests that create multiple sessions to work properly.
196   hwnd_ = GetForegroundWindow();
197 #endif
198 }
199 
RunJavaScriptOrFail(const std::string & js_expression,content::WebContents * web_contents)200 void XrBrowserTestBase::RunJavaScriptOrFail(
201     const std::string& js_expression,
202     content::WebContents* web_contents) {
203   if (javascript_failed_) {
204     LogJavaScriptFailure();
205     return;
206   }
207 
208   ASSERT_TRUE(content::ExecuteScript(web_contents, js_expression))
209       << "Failed to run given JavaScript: " << js_expression;
210 }
211 
RunJavaScriptAndExtractBoolOrFail(const std::string & js_expression,content::WebContents * web_contents)212 bool XrBrowserTestBase::RunJavaScriptAndExtractBoolOrFail(
213     const std::string& js_expression,
214     content::WebContents* web_contents) {
215   if (javascript_failed_) {
216     LogJavaScriptFailure();
217     return false;
218   }
219 
220   bool result;
221   DLOG(INFO) << "Run JavaScript: " << js_expression;
222   EXPECT_TRUE(content::ExecuteScriptAndExtractBool(
223       web_contents,
224       "window.domAutomationController.send(" + js_expression + ")", &result))
225       << "Failed to run given JavaScript for bool: " << js_expression;
226   return result;
227 }
228 
RunJavaScriptAndExtractStringOrFail(const std::string & js_expression,content::WebContents * web_contents)229 std::string XrBrowserTestBase::RunJavaScriptAndExtractStringOrFail(
230     const std::string& js_expression,
231     content::WebContents* web_contents) {
232   if (javascript_failed_) {
233     LogJavaScriptFailure();
234     return "";
235   }
236 
237   std::string result;
238   EXPECT_TRUE(content::ExecuteScriptAndExtractString(
239       web_contents,
240       "window.domAutomationController.send(" + js_expression + ")", &result))
241       << "Failed to run given JavaScript for string: " << js_expression;
242   return result;
243 }
244 
PollJavaScriptBoolean(const std::string & bool_expression,const base::TimeDelta & timeout,content::WebContents * web_contents)245 bool XrBrowserTestBase::PollJavaScriptBoolean(
246     const std::string& bool_expression,
247     const base::TimeDelta& timeout,
248     content::WebContents* web_contents) {
249   bool result = false;
250   base::RunLoop wait_loop(base::RunLoop::Type::kNestableTasksAllowed);
251   // Lambda used because otherwise BindRepeating gets confused about which
252   // version of RunJavaScriptAndExtractBoolOrFail to use.
253   BlockOnCondition(base::BindRepeating(
254                        [](XrBrowserTestBase* base, std::string expression,
255                           content::WebContents* contents) {
256                          return base->RunJavaScriptAndExtractBoolOrFail(
257                              expression, contents);
258                        },
259                        this, bool_expression, web_contents),
260                    &result, &wait_loop, base::Time::Now(), timeout);
261   wait_loop.Run();
262   return result;
263 }
264 
PollJavaScriptBooleanOrFail(const std::string & bool_expression,const base::TimeDelta & timeout,content::WebContents * web_contents)265 void XrBrowserTestBase::PollJavaScriptBooleanOrFail(
266     const std::string& bool_expression,
267     const base::TimeDelta& timeout,
268     content::WebContents* web_contents) {
269   ASSERT_TRUE(PollJavaScriptBoolean(bool_expression, timeout, web_contents))
270       << "Timed out polling JavaScript boolean expression: " << bool_expression;
271 }
272 
BlockOnCondition(base::RepeatingCallback<bool ()> condition,bool * result,base::RunLoop * wait_loop,const base::Time & start_time,const base::TimeDelta & timeout,const base::TimeDelta & period)273 void XrBrowserTestBase::BlockOnCondition(
274     base::RepeatingCallback<bool()> condition,
275     bool* result,
276     base::RunLoop* wait_loop,
277     const base::Time& start_time,
278     const base::TimeDelta& timeout,
279     const base::TimeDelta& period) {
280   if (!*result) {
281     *result = condition.Run();
282   }
283 
284   if (*result) {
285     if (wait_loop->running()) {
286       wait_loop->Quit();
287       return;
288     }
289     // In the case where the condition is met fast enough that the given
290     // RunLoop hasn't started yet, spin until it's available.
291     base::ThreadTaskRunnerHandle::Get()->PostTask(
292         FROM_HERE,
293         base::BindOnce(&XrBrowserTestBase::BlockOnCondition,
294                        base::Unretained(this), std::move(condition),
295                        base::Unretained(result), base::Unretained(wait_loop),
296                        start_time, timeout, period));
297     return;
298   }
299 
300   if (base::Time::Now() - start_time > timeout &&
301       !base::debug::BeingDebugged()) {
302     wait_loop->Quit();
303     return;
304   }
305 
306   base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
307       FROM_HERE,
308       base::BindOnce(&XrBrowserTestBase::BlockOnCondition,
309                      base::Unretained(this), std::move(condition),
310                      base::Unretained(result), base::Unretained(wait_loop),
311                      start_time, timeout, period),
312       period);
313 }
314 
WaitOnJavaScriptStep(content::WebContents * web_contents)315 void XrBrowserTestBase::WaitOnJavaScriptStep(
316     content::WebContents* web_contents) {
317   // Make sure we aren't trying to wait on a JavaScript test step without the
318   // code to do so.
319   bool code_available = RunJavaScriptAndExtractBoolOrFail(
320       "typeof javascriptDone !== 'undefined'", web_contents);
321   ASSERT_TRUE(code_available) << "Attempted to wait on a JavaScript test step "
322                               << "without the code to do so. You either forgot "
323                               << "to import webxr_e2e.js or "
324                               << "are incorrectly using a C++ function.";
325 
326   // Actually wait for the step to finish.
327   bool success =
328       PollJavaScriptBoolean("javascriptDone", kPollTimeoutLong, web_contents);
329 
330   // Check what state we're in to make sure javascriptDone wasn't called
331   // because the test failed.
332   XrBrowserTestBase::TestStatus test_status = CheckTestStatus(web_contents);
333   if (!success || test_status == XrBrowserTestBase::TestStatus::STATUS_FAILED) {
334     // Failure states: Either polling failed or polling succeeded, but because
335     // the test failed.
336     std::string reason;
337     if (!success) {
338       reason = "Timed out waiting for JavaScript step to finish.";
339     } else {
340       reason =
341           "JavaScript testharness reported failure while waiting for "
342           "JavaScript step to finish";
343     }
344 
345     std::string result_string =
346         RunJavaScriptAndExtractStringOrFail("resultString", web_contents);
347     if (result_string.empty()) {
348       reason +=
349           " Did not obtain specific failure reason from JavaScript "
350           "testharness.";
351     } else {
352       reason +=
353           " JavaScript testharness reported failure reason: " + result_string;
354     }
355     // Store that we've failed waiting for a JavaScript step so we can abort
356     // further attempts to run JavaScript, which has the potential to do weird
357     // things and produce non-useful output due to JavaScript code continuing
358     // to run when it's in a known bad state.
359     // This is a workaround for the fact that FAIL() and other gtest macros that
360     // cause test failures only abort the current function. Thus, a failure here
361     // will show up as a test failure, but there's nothing that actually stops
362     // the test from continuing to run since FAIL() is not being called in the
363     // main test body.
364     javascript_failed_ = true;
365     // Newlines to help the failure reason stick out.
366     LOG(ERROR) << "\n\n\nvvvvvvvvvvvvvvvvv Useful Stack vvvvvvvvvvvvvvvvv\n\n";
367     FAIL() << reason;
368   }
369 
370   // Reset the synchronization boolean.
371   RunJavaScriptOrFail("javascriptDone = false", web_contents);
372 }
373 
ExecuteStepAndWait(const std::string & step_function,content::WebContents * web_contents)374 void XrBrowserTestBase::ExecuteStepAndWait(const std::string& step_function,
375                                            content::WebContents* web_contents) {
376   RunJavaScriptOrFail(step_function, web_contents);
377   WaitOnJavaScriptStep(web_contents);
378 }
379 
CheckTestStatus(content::WebContents * web_contents)380 XrBrowserTestBase::TestStatus XrBrowserTestBase::CheckTestStatus(
381     content::WebContents* web_contents) {
382   std::string result_string =
383       RunJavaScriptAndExtractStringOrFail("resultString", web_contents);
384   bool test_passed =
385       RunJavaScriptAndExtractBoolOrFail("testPassed", web_contents);
386   if (test_passed) {
387     return XrBrowserTestBase::TestStatus::STATUS_PASSED;
388   } else if (!test_passed && result_string.empty()) {
389     return XrBrowserTestBase::TestStatus::STATUS_RUNNING;
390   }
391   // !test_passed && result_string != ""
392   return XrBrowserTestBase::TestStatus::STATUS_FAILED;
393 }
394 
EndTest(content::WebContents * web_contents)395 void XrBrowserTestBase::EndTest(content::WebContents* web_contents) {
396   switch (CheckTestStatus(web_contents)) {
397     case XrBrowserTestBase::TestStatus::STATUS_PASSED:
398       break;
399     case XrBrowserTestBase::TestStatus::STATUS_FAILED:
400       FAIL() << "JavaScript testharness failed with reason: "
401              << RunJavaScriptAndExtractStringOrFail("resultString",
402                                                     web_contents);
403       break;
404     case XrBrowserTestBase::TestStatus::STATUS_RUNNING:
405       FAIL() << "Attempted to end test in C++ without finishing in JavaScript.";
406       break;
407     default:
408       FAIL() << "Received unknown test status.";
409   }
410 }
411 
AssertNoJavaScriptErrors(content::WebContents * web_contents)412 void XrBrowserTestBase::AssertNoJavaScriptErrors(
413     content::WebContents* web_contents) {
414   if (CheckTestStatus(web_contents) ==
415       XrBrowserTestBase::TestStatus::STATUS_FAILED) {
416     FAIL() << "JavaScript testharness failed with reason: "
417            << RunJavaScriptAndExtractStringOrFail("resultString", web_contents);
418   }
419 }
420 
RunJavaScriptOrFail(const std::string & js_expression)421 void XrBrowserTestBase::RunJavaScriptOrFail(const std::string& js_expression) {
422   RunJavaScriptOrFail(js_expression, GetCurrentWebContents());
423 }
424 
RunJavaScriptAndExtractBoolOrFail(const std::string & js_expression)425 bool XrBrowserTestBase::RunJavaScriptAndExtractBoolOrFail(
426     const std::string& js_expression) {
427   return RunJavaScriptAndExtractBoolOrFail(js_expression,
428                                            GetCurrentWebContents());
429 }
430 
RunJavaScriptAndExtractStringOrFail(const std::string & js_expression)431 std::string XrBrowserTestBase::RunJavaScriptAndExtractStringOrFail(
432     const std::string& js_expression) {
433   return RunJavaScriptAndExtractStringOrFail(js_expression,
434                                              GetCurrentWebContents());
435 }
436 
PollJavaScriptBoolean(const std::string & bool_expression,const base::TimeDelta & timeout)437 bool XrBrowserTestBase::PollJavaScriptBoolean(
438     const std::string& bool_expression,
439     const base::TimeDelta& timeout) {
440   return PollJavaScriptBoolean(bool_expression, timeout,
441                                GetCurrentWebContents());
442 }
443 
PollJavaScriptBooleanOrFail(const std::string & bool_expression,const base::TimeDelta & timeout)444 void XrBrowserTestBase::PollJavaScriptBooleanOrFail(
445     const std::string& bool_expression,
446     const base::TimeDelta& timeout) {
447   PollJavaScriptBooleanOrFail(bool_expression, timeout,
448                               GetCurrentWebContents());
449 }
450 
WaitOnJavaScriptStep()451 void XrBrowserTestBase::WaitOnJavaScriptStep() {
452   WaitOnJavaScriptStep(GetCurrentWebContents());
453 }
454 
ExecuteStepAndWait(const std::string & step_function)455 void XrBrowserTestBase::ExecuteStepAndWait(const std::string& step_function) {
456   ExecuteStepAndWait(step_function, GetCurrentWebContents());
457 }
458 
EndTest()459 void XrBrowserTestBase::EndTest() {
460   EndTest(GetCurrentWebContents());
461 }
462 
AssertNoJavaScriptErrors()463 void XrBrowserTestBase::AssertNoJavaScriptErrors() {
464   AssertNoJavaScriptErrors(GetCurrentWebContents());
465 }
466 
LogJavaScriptFailure()467 void XrBrowserTestBase::LogJavaScriptFailure() {
468   LOG(ERROR) << "HEY! LISTEN! Not running requested JavaScript due to previous "
469                 "failure. Failures below this are likely garbage. Look for the "
470                 "useful stack above.";
471 }
472 
473 }  // namespace vr
474