blob: 1c60a027c5a4d0564e378ba66b27c4223105ec5c [file] [log] [blame]
// Copyright 2017 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.
package org.chromium.chrome.browser.vr;
import android.support.annotation.IntDef;
import android.view.View;
import org.junit.Assert;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.browser.UrlConstants;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.content.browser.test.util.JavaScriptUtils;
import org.chromium.content_public.browser.ContentViewCore;
import org.chromium.content_public.browser.WebContents;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Class containing most of the test framework for WebVR and WebXR testing, which requires
* back-and-forth communication with JavaScript running in the browser. WebVR and WebXR-specific
* functionality can be found in the VrTestFramework and XrTestFramework subclasses.
*
* The general test flow is:
* - Load the HTML file containing the test, which:
* - Loads the WebVR boilerplate code and some test functions
* - Sets up common elements like the canvas and synchronization variable
* - Sets up any steps that need to be triggered by the Java code
* - Check if any VRDisplay objects were found and fail the test if it doesn't
* match what we expect for that test
* - Repeat:
* - Run any necessary Java-side code, e.g. trigger a user action
* - Trigger the next JavaScript test step and wait for it to finish
*
* The JavaScript code will automatically process test results once all
* testharness.js tests are done, just like in layout tests. Once the results
* are processed, the JavaScript code will automatically signal the Java code,
* which can then grab the results and pass/fail the instrumentation test.
*/
public class TestFramework {
public static final int PAGE_LOAD_TIMEOUT_S = 10;
public static final int POLL_CHECK_INTERVAL_SHORT_MS = 50;
public static final int POLL_CHECK_INTERVAL_LONG_MS = 100;
public static final int POLL_TIMEOUT_SHORT_MS = 1000;
public static final int POLL_TIMEOUT_LONG_MS = 10000;
public static final String[] NATIVE_URLS_OF_INTEREST = {UrlConstants.BOOKMARKS_FOLDER_URL,
UrlConstants.BOOKMARKS_UNCATEGORIZED_URL, UrlConstants.BOOKMARKS_URL,
UrlConstants.DOWNLOADS_URL, UrlConstants.NATIVE_HISTORY_URL, UrlConstants.NTP_URL,
UrlConstants.RECENT_TABS_URL};
private static final String TAG = "TestFramework";
static final String TEST_DIR = "chrome/test/data/vr/e2e_test_files";
// Test status enum
private static final int STATUS_RUNNING = 0;
private static final int STATUS_PASSED = 1;
private static final int STATUS_FAILED = 2;
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATUS_RUNNING, STATUS_PASSED, STATUS_FAILED})
private @interface TestStatus {}
private ChromeActivityTestRule mRule;
private WebContents mFirstTabWebContents;
private ContentViewCore mFirstTabCvc;
private View mFirstTabContentView;
/**
* Must be constructed after the rule has been applied (e.g. in whatever method is
* tagged with @Before)
*/
public TestFramework(ChromeActivityTestRule rule) {
mRule = rule;
mFirstTabWebContents = mRule.getWebContents();
mFirstTabCvc = mRule.getActivity().getActivityTab().getContentViewCore();
mFirstTabContentView = mRule.getActivity().getActivityTab().getContentView();
Assert.assertFalse("Test did not start in VR", VrShellDelegate.isInVr());
}
public WebContents getFirstTabWebContents() {
return mFirstTabWebContents;
}
public View getFirstTabContentView() {
return mFirstTabContentView;
}
public ChromeActivityTestRule getRule() {
return mRule;
}
public void simulateRendererKilled() {
final Tab tab = getRule().getActivity().getActivityTab();
ThreadUtils.runOnUiThreadBlocking(() -> tab.simulateRendererKilledForTesting(true));
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return tab.isShowingSadTab();
}
});
}
/**
* Gets the file:// URL to the test file
* @param testName The name of the test whose file will be retrieved.
* @return The file:// URL to the specified test file.
*/
public static String getFileUrlForHtmlTestFile(String testName) {
return "file://" + UrlUtils.getIsolatedTestFilePath(TEST_DIR) + "/html/" + testName
+ ".html";
}
/**
* Gets the path to pass to an EmbeddedTestServer.getURL to load the given HTML test file.
* @param testName The name of the test whose file will be retrieved.
* @param A path that can be passed to EmbeddedTestServer.getURL to load the test file.
*/
public static String getEmbeddedServerPathForHtmlTestFile(String testName) {
return "/" + TEST_DIR + "/html/" + testName + ".html";
}
/**
* Loads the given URL with the given timeout then waits for JavaScript to
* signal that it's ready for testing.
* @param url The URL of the page to load.
* @param timeoutSec The timeout of the page load in seconds.
* @return The return value of ChromeActivityTestRule.loadUrl()
*/
public int loadUrlAndAwaitInitialization(String url, int timeoutSec)
throws InterruptedException {
int result = mRule.loadUrl(url, timeoutSec);
Assert.assertTrue("JavaScript initialization successful",
pollJavaScriptBoolean("isInitializationComplete()", POLL_TIMEOUT_LONG_MS,
mRule.getWebContents()));
return result;
}
/**
* Helper function to run the given JavaScript, return the return value,
* and fail if a timeout/interrupt occurs so we don't have to catch or
* declare exceptions all the time.
* @param js The JavaScript to run.
* @param timeout The timeout in milliseconds before a failure.
* @param webContents The WebContents object to run the JavaScript in.
* @return The return value of the JavaScript.
*/
public static String runJavaScriptOrFail(String js, int timeout, WebContents webContents) {
try {
return JavaScriptUtils.executeJavaScriptAndWaitForResult(
webContents, js, timeout, TimeUnit.MILLISECONDS);
} catch (InterruptedException | TimeoutException e) {
Assert.fail("Fatal interruption or timeout running JavaScript: " + js);
}
return "Not reached";
}
/**
* Retrieves the current status of the JavaScript test and returns an enum corresponding to it.
* @param webContents The WebContents for the tab to check the status in.
* @return A TestStatus integer corresponding to the current state of the JavaScript test
*/
@TestStatus
public static int checkTestStatus(WebContents webContents) {
String resultString =
runJavaScriptOrFail("resultString", POLL_TIMEOUT_SHORT_MS, webContents);
boolean testPassed = Boolean.parseBoolean(
runJavaScriptOrFail("testPassed", POLL_TIMEOUT_SHORT_MS, webContents));
if (testPassed) {
return STATUS_PASSED;
} else if (!testPassed && resultString.equals("\"\"")) {
return STATUS_RUNNING;
} else {
// !testPassed && !resultString.equals("\"\"")
return STATUS_FAILED;
}
}
/**
* Helper function to end the test harness test and assert that it passed,
* setting the failure reason as the description if it didn't.
* @param webContents The WebContents for the tab to check test results in.
*/
public static void endTest(WebContents webContents) {
switch (checkTestStatus(webContents)) {
case STATUS_PASSED:
break;
case STATUS_FAILED:
String resultString =
runJavaScriptOrFail("resultString", POLL_TIMEOUT_SHORT_MS, webContents);
Assert.fail("JavaScript testharness failed with result: " + resultString);
break;
case STATUS_RUNNING:
Assert.fail("Attempted to end test in Java without finishing in JavaScript.");
break;
default:
Assert.fail("Received unknown test status.");
}
}
/**
* Helper function to make sure that the JavaScript test harness did not detect any failures.
* Similar to endTest, but does not fail if the test is still detected as running. This is
* useful because not all tests make use of the test harness' test/assert features (particularly
* simple enter/exit tests), but may still want to ensure that no unexpected JavaScript errors
* were encountered.
* @param webContents The Webcontents for the tab to check for failures in.
*/
public static void assertNoJavaScriptErrors(WebContents webContents) {
Assert.assertNotEquals(checkTestStatus(webContents), STATUS_FAILED);
}
/**
* Polls the provided JavaScript boolean until the timeout is reached or
* the boolean is true.
* @param boolName The name of the JavaScript boolean or expression to poll.
* @param timeoutMs The polling timeout in milliseconds.
* @param webContents The WebContents to run the JavaScript through.
* @return True if the boolean evaluated to true, false if timed out.
*/
public static boolean pollJavaScriptBoolean(
final String boolName, int timeoutMs, final WebContents webContents) {
try {
CriteriaHelper.pollInstrumentationThread(Criteria.equals(true, new Callable<Boolean>() {
@Override
public Boolean call() {
String result = "false";
try {
result = JavaScriptUtils.executeJavaScriptAndWaitForResult(webContents,
boolName, POLL_CHECK_INTERVAL_SHORT_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException | TimeoutException e) {
// Expected to happen regularly, do nothing
}
return Boolean.parseBoolean(result);
}
}), timeoutMs, POLL_CHECK_INTERVAL_LONG_MS);
} catch (AssertionError e) {
Log.d(TAG, "pollJavaScriptBoolean() timed out");
return false;
}
return true;
}
/**
* Waits for a JavaScript step to finish, asserting that the step finished
* instead of timing out.
* @param webContents The WebContents for the tab the JavaScript step is in.
*/
public static void waitOnJavaScriptStep(WebContents webContents) {
// Make sure we aren't trying to wait on a JavaScript test step without the code to do so.
Assert.assertTrue("Attempted to wait on a JavaScript step without the code to do so. You "
+ "either forgot to import webvr_e2e.js or are incorrectly using a Java "
+ "method.",
Boolean.parseBoolean(runJavaScriptOrFail("typeof javascriptDone !== 'undefined'",
POLL_TIMEOUT_SHORT_MS, webContents)));
// Actually wait for the step to finish
boolean success =
pollJavaScriptBoolean("javascriptDone", POLL_TIMEOUT_LONG_MS, webContents);
// Check what state we're in to make sure javascriptDone wasn't called because the test
// failed.
int testStatus = checkTestStatus(webContents);
if (!success || testStatus == STATUS_FAILED) {
// Failure states: Either polling failed or polling succeeded, but because the test
// failed.
String reason;
if (!success) {
reason = "Polling JavaScript boolean javascriptDone timed out.";
} else {
reason = "Polling JavaScript boolean javascriptDone succeeded, but test failed.";
}
String resultString =
runJavaScriptOrFail("resultString", POLL_TIMEOUT_SHORT_MS, webContents);
if (resultString.equals("\"\"")) {
reason += " Did not obtain specific reason from testharness.";
} else {
reason += " Testharness reported failure: " + resultString;
}
Assert.fail(reason);
}
// Reset the synchronization boolean
runJavaScriptOrFail("javascriptDone = false", POLL_TIMEOUT_SHORT_MS, webContents);
}
/**
* Executes a JavaScript step function using the given WebContents.
* @param stepFunction The JavaScript step function to call.
* @param webContents The WebContents for the tab the JavaScript is in.
*/
public static void executeStepAndWait(String stepFunction, WebContents webContents) {
// Run the step and block
JavaScriptUtils.executeJavaScript(webContents, stepFunction);
waitOnJavaScriptStep(webContents);
}
}