1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.webkit.cts; 18 19 import static org.junit.Assert.assertEquals; 20 21 import android.webkit.JavascriptInterface; 22 import android.webkit.ServiceWorkerClient; 23 import android.webkit.ServiceWorkerController; 24 import android.webkit.WebResourceRequest; 25 import android.webkit.WebResourceResponse; 26 import android.webkit.WebView; 27 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient; 28 29 import androidx.test.ext.junit.rules.ActivityScenarioRule; 30 import androidx.test.ext.junit.runners.AndroidJUnit4; 31 import androidx.test.filters.MediumTest; 32 33 import com.android.compatibility.common.util.NullWebViewUtils; 34 import com.android.compatibility.common.util.PollingCheck; 35 36 import org.junit.After; 37 import org.junit.Assume; 38 import org.junit.Before; 39 import org.junit.Rule; 40 import org.junit.Test; 41 import org.junit.runner.RunWith; 42 43 import java.io.ByteArrayInputStream; 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.concurrent.Callable; 47 48 @MediumTest 49 @RunWith(AndroidJUnit4.class) 50 public class ServiceWorkerClientTest extends SharedWebViewTest { 51 52 // The BASE_URL does not matter since the tests will intercept the load, but it should be https 53 // for the Service Worker registration to succeed. 54 private static final String BASE_URL = "https://www.example.com/"; 55 private static final String INDEX_URL = BASE_URL + "index.html"; 56 private static final String SW_URL = BASE_URL + "sw.js"; 57 private static final String FETCH_URL = BASE_URL + "fetch.html"; 58 59 private static final String JS_INTERFACE_NAME = "Android"; 60 private static final int POLLING_TIMEOUT = 60 * 1000; 61 62 // static HTML page always injected instead of the url loaded. 63 private static final String INDEX_RAW_HTML = 64 "<!DOCTYPE html>\n" 65 + "<html>\n" 66 + " <body>\n" 67 + " <script>\n" 68 + " navigator.serviceWorker.register('sw.js').then(function(reg) {\n" 69 + " " + JS_INTERFACE_NAME + ".registrationSuccess();\n" 70 + " }).catch(function(err) {\n" 71 + " console.error(err);\n" 72 + " });\n" 73 + " </script>\n" 74 + " </body>\n" 75 + "</html>\n"; 76 private static final String SW_RAW_HTML = "fetch('fetch.html');"; 77 private static final String SW_UNREGISTER_RAW_JS = 78 "navigator.serviceWorker.getRegistration().then(function(r) {" 79 + " r.unregister().then(function(success) {" 80 + " if (success) " + JS_INTERFACE_NAME + ".unregisterSuccess();" 81 + " else console.error('unregister() was not successful');" 82 + " });" 83 + "}).catch(function(err) {" 84 + " console.error(err);" 85 + "});"; 86 87 @Rule 88 public ActivityScenarioRule mActivityScenarioRule = 89 new ActivityScenarioRule(WebViewCtsActivity.class); 90 91 private JavascriptStatusReceiver mJavascriptStatusReceiver; 92 private WebViewOnUiThread mOnUiThread; 93 94 // Both this test and WebViewOnUiThread need to override some of the methods on WebViewClient, 95 // so this test subclasses the WebViewClient from WebViewOnUiThread. 96 private static class InterceptClient extends WaitForLoadedClient { 97 InterceptClient(WebViewOnUiThread webViewOnUiThread)98 public InterceptClient(WebViewOnUiThread webViewOnUiThread) throws Exception { 99 super(webViewOnUiThread); 100 } 101 102 @Override shouldInterceptRequest(WebView view, WebResourceRequest request)103 public WebResourceResponse shouldInterceptRequest(WebView view, 104 WebResourceRequest request) { 105 // Only return content for INDEX_URL, deny all other requests. 106 try { 107 if (request.getUrl().toString().equals(INDEX_URL)) { 108 return new WebResourceResponse("text/html", "utf-8", 109 new ByteArrayInputStream(INDEX_RAW_HTML.getBytes("UTF-8"))); 110 } 111 } catch(java.io.UnsupportedEncodingException e) {} 112 return new WebResourceResponse("text/html", "UTF-8", null); 113 } 114 } 115 116 public static class InterceptServiceWorkerClient extends ServiceWorkerClient { 117 private List<WebResourceRequest> mInterceptedRequests = new ArrayList<WebResourceRequest>(); 118 119 @Override shouldInterceptRequest(WebResourceRequest request)120 public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { 121 // Records intercepted requests and only return content for SW_URL. 122 mInterceptedRequests.add(request); 123 try { 124 if (request.getUrl().toString().equals(SW_URL)) { 125 return new WebResourceResponse("application/javascript", "utf-8", 126 new ByteArrayInputStream(SW_RAW_HTML.getBytes("UTF-8"))); 127 } 128 } catch(java.io.UnsupportedEncodingException e) {} 129 return new WebResourceResponse("text/html", "UTF-8", null); 130 } 131 getInterceptedRequests()132 List<WebResourceRequest> getInterceptedRequests() { 133 return mInterceptedRequests; 134 } 135 } 136 137 @Before setUp()138 public void setUp() throws Exception { 139 WebView webview = getTestEnvironment().getWebView(); 140 if (webview == null) return; 141 mOnUiThread = new WebViewOnUiThread(webview); 142 mOnUiThread.getSettings().setJavaScriptEnabled(true); 143 144 mJavascriptStatusReceiver = new JavascriptStatusReceiver(); 145 mOnUiThread.addJavascriptInterface(mJavascriptStatusReceiver, JS_INTERFACE_NAME); 146 mOnUiThread.setWebViewClient(new InterceptClient(mOnUiThread)); 147 } 148 149 @After tearDown()150 public void tearDown() throws Exception { 151 if (mOnUiThread != null) { 152 mOnUiThread.cleanUp(); 153 ServiceWorkerController.getInstance().setServiceWorkerClient(null); 154 } 155 } 156 157 @Override createTestEnvironment()158 protected SharedWebViewTestEnvironment createTestEnvironment() { 159 Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable()); 160 161 SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder(); 162 163 mActivityScenarioRule 164 .getScenario() 165 .onActivity( 166 activity -> { 167 WebView webView = ((WebViewCtsActivity) activity).getWebView(); 168 builder.setHostAppInvoker( 169 SharedWebViewTestEnvironment.createHostAppInvoker( 170 activity)) 171 .setContext(activity) 172 .setWebView(webView); 173 }); 174 175 return builder.build(); 176 } 177 178 /** 179 * This should remain functionally equivalent to 180 * androidx.webkit.ServiceWorkerClientCompatTest#testServiceWorkerClientInterceptCallback. 181 * Modifications to this test should be reflected in that test as necessary. See 182 * http://go/modifying-webview-cts. 183 */ 184 // Test correct invocation of shouldInterceptRequest for Service Workers. 185 @Test testServiceWorkerClientInterceptCallback()186 public void testServiceWorkerClientInterceptCallback() throws Exception { 187 final InterceptServiceWorkerClient mInterceptServiceWorkerClient = 188 new InterceptServiceWorkerClient(); 189 ServiceWorkerController swController = ServiceWorkerController.getInstance(); 190 swController.setServiceWorkerClient(mInterceptServiceWorkerClient); 191 192 mOnUiThread.loadUrlAndWaitForCompletion(INDEX_URL); 193 194 Callable<Boolean> registrationSuccess = new Callable<Boolean>() { 195 @Override 196 public Boolean call() { 197 return mJavascriptStatusReceiver.mRegistrationSuccess; 198 } 199 }; 200 PollingCheck.check("JS could not register Service Worker", POLLING_TIMEOUT, 201 registrationSuccess); 202 203 Callable<Boolean> receivedRequest = new Callable<Boolean>() { 204 @Override 205 public Boolean call() { 206 return mInterceptServiceWorkerClient.getInterceptedRequests().size() >= 2; 207 } 208 }; 209 PollingCheck.check("Service Worker intercept callbacks not invoked", POLLING_TIMEOUT, 210 receivedRequest); 211 212 List<WebResourceRequest> requests = mInterceptServiceWorkerClient.getInterceptedRequests(); 213 assertEquals(2, requests.size()); 214 assertEquals(SW_URL, requests.get(0).getUrl().toString()); 215 assertEquals(FETCH_URL, requests.get(1).getUrl().toString()); 216 217 // Clean-up, make sure to unregister the Service Worker. 218 mOnUiThread.evaluateJavascript(SW_UNREGISTER_RAW_JS, null); 219 Callable<Boolean> unregisterSuccess = new Callable<Boolean>() { 220 @Override 221 public Boolean call() { 222 return mJavascriptStatusReceiver.mUnregisterSuccess; 223 } 224 }; 225 PollingCheck.check("JS could not unregister Service Worker", POLLING_TIMEOUT, 226 unregisterSuccess); 227 } 228 229 /** 230 * This should remain functionally equivalent to 231 * androidx.webkit.ServiceWorkerClientCompatTest#testSetNullServiceWorkerClient. 232 * Modifications to this test should be reflected in that test as necessary. See 233 * http://go/modifying-webview-cts. 234 */ 235 // Test setting a null ServiceWorkerClient. 236 @Test testSetNullServiceWorkerClient()237 public void testSetNullServiceWorkerClient() throws Exception { 238 ServiceWorkerController swController = ServiceWorkerController.getInstance(); 239 swController.setServiceWorkerClient(null); 240 mOnUiThread.loadUrlAndWaitForCompletion(INDEX_URL); 241 242 Callable<Boolean> registrationFailure = 243 () -> !mJavascriptStatusReceiver.mRegistrationSuccess; 244 PollingCheck.check("JS unexpectedly registered the Service Worker", POLLING_TIMEOUT, 245 registrationFailure); 246 } 247 248 // Object added to the page via AddJavascriptInterface() that is used by the test Javascript to 249 // notify back to Java if the Service Worker registration was successful. 250 public final static class JavascriptStatusReceiver { 251 public volatile boolean mRegistrationSuccess = false; 252 public volatile boolean mUnregisterSuccess = false; 253 254 @JavascriptInterface registrationSuccess()255 public void registrationSuccess() { 256 mRegistrationSuccess = true; 257 } 258 259 @JavascriptInterface unregisterSuccess()260 public void unregisterSuccess() { 261 mUnregisterSuccess = true; 262 } 263 } 264 } 265 266