1 // Copyright 2017 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 package org.chromium.chrome.browser.vr;
6 
7 import static org.chromium.chrome.browser.vr.XrTestFramework.PAGE_LOAD_TIMEOUT_S;
8 import static org.chromium.chrome.browser.vr.XrTestFramework.POLL_CHECK_INTERVAL_LONG_MS;
9 import static org.chromium.chrome.browser.vr.XrTestFramework.POLL_CHECK_INTERVAL_SHORT_MS;
10 import static org.chromium.chrome.browser.vr.XrTestFramework.POLL_TIMEOUT_LONG_MS;
11 import static org.chromium.chrome.browser.vr.XrTestFramework.POLL_TIMEOUT_SHORT_MS;
12 import static org.chromium.chrome.test.util.ChromeRestriction.RESTRICTION_TYPE_DEVICE_DAYDREAM;
13 import static org.chromium.chrome.test.util.ChromeRestriction.RESTRICTION_TYPE_SVR;
14 import static org.chromium.chrome.test.util.ChromeRestriction.RESTRICTION_TYPE_VIEWER_DAYDREAM_OR_STANDALONE;
15 import static org.chromium.chrome.test.util.ChromeRestriction.RESTRICTION_TYPE_VR_DON_ENABLED;
16 
17 import android.graphics.Bitmap;
18 import android.graphics.BitmapFactory;
19 import android.graphics.Color;
20 import android.os.Build;
21 import android.support.test.InstrumentationRegistry;
22 import android.support.test.uiautomator.UiDevice;
23 
24 import androidx.test.filters.MediumTest;
25 
26 import org.junit.Assert;
27 import org.junit.Before;
28 import org.junit.Rule;
29 import org.junit.Test;
30 import org.junit.rules.RuleChain;
31 import org.junit.runner.RunWith;
32 
33 import org.chromium.base.test.params.ParameterAnnotations.ClassParameter;
34 import org.chromium.base.test.params.ParameterAnnotations.UseRunnerDelegate;
35 import org.chromium.base.test.params.ParameterSet;
36 import org.chromium.base.test.params.ParameterizedRunner;
37 import org.chromium.base.test.util.CommandLineFlags;
38 import org.chromium.base.test.util.CriteriaHelper;
39 import org.chromium.base.test.util.Restriction;
40 import org.chromium.base.test.util.UrlUtils;
41 import org.chromium.chrome.browser.flags.ChromeFeatureList;
42 import org.chromium.chrome.browser.flags.ChromeSwitches;
43 import org.chromium.chrome.browser.vr.rules.XrActivityRestriction;
44 import org.chromium.chrome.browser.vr.util.NativeUiUtils;
45 import org.chromium.chrome.browser.vr.util.PermissionUtils;
46 import org.chromium.chrome.browser.vr.util.VrTestRuleUtils;
47 import org.chromium.chrome.browser.vr.util.VrTransitionUtils;
48 import org.chromium.chrome.test.ChromeActivityTestRule;
49 import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
50 import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
51 
52 import java.io.File;
53 import java.util.List;
54 import java.util.concurrent.Callable;
55 import java.util.concurrent.CountDownLatch;
56 import java.util.concurrent.TimeUnit;
57 
58 /**
59  * End-to-end tests for transitioning between WebXR's magic window and
60  * presentation modes.
61  */
62 @RunWith(ParameterizedRunner.class)
63 @UseRunnerDelegate(ChromeJUnit4RunnerDelegate.class)
64 @CommandLineFlags.
65 Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE, "enable-features=LogJsConsoleMessages"})
66 public class WebXrVrTransitionTest {
67     @ClassParameter
68     private static List<ParameterSet> sClassParams =
69             VrTestRuleUtils.generateDefaultTestRuleParameters();
70     @Rule
71     public RuleChain mRuleChain;
72 
73     private ChromeActivityTestRule mTestRule;
74     private WebXrVrTestFramework mWebXrVrTestFramework;
75 
WebXrVrTransitionTest(Callable<ChromeActivityTestRule> callable)76     public WebXrVrTransitionTest(Callable<ChromeActivityTestRule> callable) throws Exception {
77         mTestRule = callable.call();
78         mRuleChain = VrTestRuleUtils.wrapRuleInActivityRestrictionRule(mTestRule);
79     }
80 
81     @Before
setUp()82     public void setUp() {
83         mWebXrVrTestFramework = new WebXrVrTestFramework(mTestRule);
84     }
85 
86     /**
87      * Tests that a successful request for an immersive session actually enters VR.
88      */
89     @Test
90     @MediumTest
91             @CommandLineFlags.Add({"enable-features=WebXR"})
92             @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
testRequestSessionEntersVr()93             public void testRequestSessionEntersVr() {
94         testPresentationEntryImpl("generic_webxr_page", mWebXrVrTestFramework);
95     }
96 
testPresentationEntryImpl(String url, WebXrVrTestFramework framework)97     private void testPresentationEntryImpl(String url, WebXrVrTestFramework framework) {
98         framework.loadFileAndAwaitInitialization(url, PAGE_LOAD_TIMEOUT_S);
99         framework.enterSessionWithUserGestureOrFail();
100         Assert.assertTrue("Browser did not enter VR", VrShellDelegate.isInVr());
101 
102         // Verify that we're actually rendering WebXR/VR content and that it's blue (the canvas
103         // is set to blue while presenting). This could be a proper RenderTest, but it's less
104         // overhead to just directly check a pixel.
105         // TODO(https://crbug.com/947252): Run this part unconditionally once the cause of the
106         // flakiness on older devices is fixed.
107         if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
108             CriteriaHelper.pollInstrumentationThread(
109                     ()
110                             -> {
111                         // Creating temporary directories doesn't seem to work, so use a fixed
112                         // location that we know we can write to.
113                         File dumpDirectory = new File(UrlUtils.getIsolatedTestFilePath(
114                                 "chrome/test/data/vr/framebuffer_dumps"));
115                         if (!dumpDirectory.exists() && !dumpDirectory.isDirectory()) {
116                             Assert.assertTrue("Failed to make framebuffer dump directory",
117                                     dumpDirectory.mkdirs());
118                         }
119                         File baseImagePath = new File(dumpDirectory, "dump");
120                         NativeUiUtils.dumpNextFramesFrameBuffers(baseImagePath.getPath());
121                         String filepath = baseImagePath.getPath()
122                                 + NativeUiUtils.FRAME_BUFFER_SUFFIX_WEB_XR_CONTENT + ".png";
123                         BitmapFactory.Options options = new BitmapFactory.Options();
124                         options.inPreferredConfig = Bitmap.Config.ARGB_8888;
125                         Bitmap bitmap = BitmapFactory.decodeFile(filepath, options);
126                         return bitmap != null && Color.BLUE == bitmap.getPixel(0, 0);
127                     },
128                     "Immersive session started, but browser not visibly in VR",
129                     POLL_TIMEOUT_LONG_MS, POLL_CHECK_INTERVAL_LONG_MS);
130         }
131 
132         framework.assertNoJavaScriptErrors();
133     }
134 
135     /**
136      * Tests that WebXR is not exposed if the flag is not on and the page does
137      * not have an origin trial token.
138      */
139     @Test
140     @MediumTest
141     @CommandLineFlags
142             .Add({"disable-features=WebXR"})
143             @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
testWebXrDisabledWithoutFlagSet()144             public void testWebXrDisabledWithoutFlagSet() {
145         apiDisabledWithoutFlagSetImpl(
146                 "test_webxr_disabled_without_flag_set", mWebXrVrTestFramework);
147     }
148 
apiDisabledWithoutFlagSetImpl(String url, WebXrVrTestFramework framework)149     private void apiDisabledWithoutFlagSetImpl(String url, WebXrVrTestFramework framework) {
150         framework.loadFileAndAwaitInitialization(url, PAGE_LOAD_TIMEOUT_S);
151         framework.waitOnJavaScriptStep();
152         framework.endTest();
153     }
154 
155     /**
156      * Tests that the immersive session promise doesn't resolve if the DON flow is
157      * not completed.
158      */
159     @Test
160     @MediumTest
161     @Restriction({RESTRICTION_TYPE_DEVICE_DAYDREAM, RESTRICTION_TYPE_VR_DON_ENABLED})
162     @CommandLineFlags.Add({"enable-features=WebXR"})
163     @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
testPresentationPromiseUnresolvedDuringDon_WebXr()164     public void testPresentationPromiseUnresolvedDuringDon_WebXr() {
165         presentationPromiseUnresolvedDuringDonImpl(
166 
167                 "webxr_test_presentation_promise_unresolved_during_don", mWebXrVrTestFramework);
168     }
169 
presentationPromiseUnresolvedDuringDonImpl( String url, WebXrVrTestFramework framework)170     private void presentationPromiseUnresolvedDuringDonImpl(
171             String url, WebXrVrTestFramework framework) {
172         framework.loadFileAndAwaitInitialization(url, PAGE_LOAD_TIMEOUT_S);
173         framework.enterSessionWithUserGestureAndWait();
174         framework.endTest();
175     }
176 
177     /**
178      * Tests that the immersive session promise is rejected if the DON flow is canceled.
179      */
180     @Test
181     @MediumTest
182     @Restriction({RESTRICTION_TYPE_DEVICE_DAYDREAM, RESTRICTION_TYPE_VR_DON_ENABLED})
183     @CommandLineFlags.Add({"enable-features=WebXR"})
184     @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
testPresentationPromiseRejectedIfDonCanceled_WebXr()185     public void testPresentationPromiseRejectedIfDonCanceled_WebXr() {
186         presentationPromiseRejectedIfDonCanceledImpl(
187 
188                 "webxr_test_presentation_promise_rejected_if_don_canceled", mWebXrVrTestFramework);
189     }
190 
presentationPromiseRejectedIfDonCanceledImpl( String url, WebXrVrTestFramework framework)191     private void presentationPromiseRejectedIfDonCanceledImpl(
192             String url, WebXrVrTestFramework framework) {
193         framework.loadFileAndAwaitInitialization(url, PAGE_LOAD_TIMEOUT_S);
194         final UiDevice uiDevice =
195                 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
196         framework.enterSessionWithUserGesture();
197         // Wait until the DON flow appears to be triggered
198         // TODO(bsheedy): Make this less hacky if there's ever an explicit way to check if the
199         // DON flow is currently active https://crbug.com/758296
200         CriteriaHelper.pollUiThread(() -> {
201             String currentPackageName = uiDevice.getCurrentPackageName();
202             return currentPackageName != null && currentPackageName.equals("com.google.vr.vrcore");
203         }, "DON flow did not start", POLL_TIMEOUT_LONG_MS, POLL_CHECK_INTERVAL_SHORT_MS);
204         uiDevice.pressBack();
205         framework.waitOnJavaScriptStep();
206         framework.endTest();
207     }
208 
209     /**
210      * Tests that the omnibox reappears after exiting an immersive session.
211      */
212     @Test
213     @MediumTest
214             @CommandLineFlags.Add({"enable-features=WebXR"})
215             @Restriction(RESTRICTION_TYPE_SVR)
testControlsVisibleAfterExitingVr_WebXr()216             public void testControlsVisibleAfterExitingVr_WebXr() throws InterruptedException {
217         controlsVisibleAfterExitingVrImpl("generic_webxr_page", mWebXrVrTestFramework);
218     }
219 
controlsVisibleAfterExitingVrImpl(String url, final WebXrVrTestFramework framework)220     private void controlsVisibleAfterExitingVrImpl(String url, final WebXrVrTestFramework framework)
221             throws InterruptedException {
222         framework.loadFileAndAwaitInitialization(url, PAGE_LOAD_TIMEOUT_S);
223         framework.enterSessionWithUserGestureOrFail();
224         VrTransitionUtils.forceExitVr();
225         // The hiding of the controls may only propagate after VR has exited, so give it a chance
226         // to propagate. In the worst case this test will erroneously pass, but should never
227         // erroneously fail, and should only be flaky if omnibox showing is broken.
228         Thread.sleep(100);
229         CriteriaHelper.pollUiThread(()
230                                             -> framework.getRule()
231                                                        .getActivity()
232                                                        .getBrowserControlsManager()
233                                                        .getBrowserControlHiddenRatio()
234                         == 0.0,
235                 "Browser controls did not unhide after exiting VR", POLL_TIMEOUT_SHORT_MS,
236                 POLL_CHECK_INTERVAL_SHORT_MS);
237         framework.assertNoJavaScriptErrors();
238     }
239 
240     /**
241      * Tests that window.requestAnimationFrame stops firing while in a WebXR immersive session, but
242      * resumes afterwards.
243      */
244     @Test
245     @MediumTest
246             @CommandLineFlags.Add({"enable-features=WebXR"})
247             @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
testWindowRafStopsFiringWhilePresenting_WebXr()248             public void testWindowRafStopsFiringWhilePresenting_WebXr()
249             throws InterruptedException {
250         windowRafStopsFiringWhilePresentingImpl(
251 
252                 "webxr_test_window_raf_stops_firing_during_immersive_session",
253                 mWebXrVrTestFramework);
254     }
255 
windowRafStopsFiringWhilePresentingImpl(String url, WebXrVrTestFramework framework)256     private void windowRafStopsFiringWhilePresentingImpl(String url, WebXrVrTestFramework framework)
257             throws InterruptedException {
258         framework.loadFileAndAwaitInitialization(url, PAGE_LOAD_TIMEOUT_S);
259         framework.executeStepAndWait("stepVerifyBeforePresent()");
260         // Pausing of window.rAF is done asynchronously, so wait until that's done.
261         final CountDownLatch vsyncPausedLatch = new CountDownLatch(1);
262         TestVrShellDelegate.getInstance().setVrShellOnVSyncPausedCallback(
263                 () -> { vsyncPausedLatch.countDown(); });
264         framework.enterSessionWithUserGestureOrFail();
265         vsyncPausedLatch.await(POLL_TIMEOUT_SHORT_MS, TimeUnit.MILLISECONDS);
266         framework.executeStepAndWait("stepVerifyDuringPresent()");
267         VrTransitionUtils.forceExitVr();
268         framework.executeStepAndWait("stepVerifyAfterPresent()");
269         framework.endTest();
270     }
271 
272     /**
273      * Tests renderer crashes while in WebXR presentation stay in VR.
274      */
275     @Test
276     @MediumTest
277     @Restriction(RESTRICTION_TYPE_VIEWER_DAYDREAM_OR_STANDALONE)
278             @CommandLineFlags.Add({"enable-features=WebXR"})
testRendererKilledInWebXrStaysInVr()279             public void testRendererKilledInWebXrStaysInVr() throws IllegalArgumentException {
280         rendererKilledInVrStaysInVrImpl("generic_webxr_page", mWebXrVrTestFramework);
281     }
282 
rendererKilledInVrStaysInVrImpl(String url, WebXrVrTestFramework framework)283     private void rendererKilledInVrStaysInVrImpl(String url, WebXrVrTestFramework framework) {
284         framework.loadFileAndAwaitInitialization(url, PAGE_LOAD_TIMEOUT_S);
285         framework.enterSessionWithUserGestureOrFail();
286         framework.simulateRendererKilled();
287         Assert.assertTrue("Browser did not enter VR", VrShellDelegate.isInVr());
288         framework.assertNoJavaScriptErrors();
289     }
290 
291     /**
292      * Tests that window.rAF continues to fire when we have a non-immersive session.
293      */
294     @Test
295     @MediumTest
296             @CommandLineFlags.Add({"enable-features=WebXR"})
297             @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
testWindowRafFiresDuringNonImmersiveSession()298             public void testWindowRafFiresDuringNonImmersiveSession() {
299         mWebXrVrTestFramework.loadFileAndAwaitInitialization(
300                 "test_window_raf_fires_during_non_immersive_session", PAGE_LOAD_TIMEOUT_S);
301         mWebXrVrTestFramework.waitOnJavaScriptStep();
302         mWebXrVrTestFramework.endTest();
303     }
304 
305     /**
306      * Tests that non-immersive sessions stop receiving rAFs during an immersive session, but resume
307      * once the immersive session ends.
308      */
309     @Test
310     @MediumTest
311             @CommandLineFlags.Add({"enable-features=WebXR"})
312             @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
313             @DisableFeatures(ChromeFeatureList.SEND_TAB_TO_SELF)
testNonImmersiveStopsDuringImmersive()314             public void testNonImmersiveStopsDuringImmersive() {
315         mWebXrVrTestFramework.loadFileAndAwaitInitialization(
316                 "test_non_immersive_stops_during_immersive", PAGE_LOAD_TIMEOUT_S);
317         mWebXrVrTestFramework.executeStepAndWait("stepBeforeImmersive()");
318         mWebXrVrTestFramework.enterSessionWithUserGestureOrFail();
319         mWebXrVrTestFramework.executeStepAndWait("stepDuringImmersive()");
320         VrTransitionUtils.forceExitVr();
321         mWebXrVrTestFramework.executeStepAndWait("stepAfterImmersive()");
322         mWebXrVrTestFramework.endTest();
323     }
324 
325     /**
326      * Tests that the "Press app button to exit" toast appears when entering an immersive WebXR for
327      * VR session with Daydream View paired.
328      */
329     @Test
330     @MediumTest
331     @Restriction(RESTRICTION_TYPE_VIEWER_DAYDREAM_OR_STANDALONE)
332             @CommandLineFlags.Add({"enable-features=WebXR"})
333             @XrActivityRestriction({XrActivityRestriction.SupportedActivity.CTA})
testAppButtonExitToast()334             public void testAppButtonExitToast() {
335         mWebXrVrTestFramework.loadFileAndAwaitInitialization(
336                 "generic_webxr_page", PAGE_LOAD_TIMEOUT_S);
337         mWebXrVrTestFramework.enterSessionWithUserGestureOrFail();
338         NativeUiUtils.performActionAndWaitForVisibilityStatus(
339                 UserFriendlyElementName.APP_BUTTON_EXIT_TOAST, true /* visible*/, () -> {});
340     }
341 
342     /**
343      * Tests that a permission prompt dismisses by itself when the page navigates away from
344      * the current page.
345      */
346     @Test
347     @MediumTest
348             @CommandLineFlags.Add({"enable-features=WebXR"})
349             @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
testConsentDialogIsDismissedWhenPageNavigatesAwayInMainFrame()350             public void testConsentDialogIsDismissedWhenPageNavigatesAwayInMainFrame() {
351         mWebXrVrTestFramework.setPermissionPromptAction(
352                 WebXrVrTestFramework.PERMISSION_PROMPT_ACTION_DO_NOTHING);
353         mWebXrVrTestFramework.loadFileAndAwaitInitialization(
354                 "generic_webxr_page", PAGE_LOAD_TIMEOUT_S);
355         mWebXrVrTestFramework.enterSessionWithUserGesture();
356         mWebXrVrTestFramework.runJavaScriptOrFail(
357                 "window.location.href = 'https://google.com'", POLL_TIMEOUT_SHORT_MS);
358         PermissionUtils.waitForPermissionPromptDismissal();
359     }
360 }
361