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