1 // Copyright 2013 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.
5 package org.chromium.android_webview.test;
7 import android.test.suitebuilder.annotation.SmallTest;
8 import android.view.View;
9 import android.view.ViewGroup.LayoutParams;
10 import android.widget.LinearLayout;
12 import org.chromium.android_webview.AwContents;
13 import org.chromium.android_webview.AwContentsClient;
14 import org.chromium.android_webview.AwLayoutSizer;
15 import org.chromium.android_webview.test.util.CommonResources;
16 import org.chromium.base.test.util.Feature;
17 import org.chromium.content.browser.test.util.CallbackHelper;
18 import org.chromium.ui.gfx.DeviceDisplayInfo;
20 import java.util.concurrent.atomic.AtomicReference;
23 * Tests for certain edge cases related to integrating with the Android view system.
25 public class AndroidViewIntegrationTest extends AwTestBase {
26 private static final int CONTENT_SIZE_CHANGE_STABILITY_TIMEOUT_MS = 1000;
28 private static class OnContentSizeChangedHelper extends CallbackHelper {
32 public int getWidth() {
33 assert getCallCount() > 0;
37 public int getHeight() {
38 assert getCallCount() > 0;
42 public void onContentSizeChanged(int widthCss, int heightCss) {
49 private OnContentSizeChangedHelper mOnContentSizeChangedHelper =
50 new OnContentSizeChangedHelper();
51 private CallbackHelper mOnPageScaleChangedHelper = new CallbackHelper();
53 private class TestAwLayoutSizer extends AwLayoutSizer {
55 public void onContentSizeChanged(int widthCss, int heightCss) {
56 super.onContentSizeChanged(widthCss, heightCss);
57 if (mOnContentSizeChangedHelper != null)
58 mOnContentSizeChangedHelper.onContentSizeChanged(widthCss, heightCss);
62 public void onPageScaleChanged(float pageScaleFactor) {
63 super.onPageScaleChanged(pageScaleFactor);
64 if (mOnPageScaleChangedHelper != null)
65 mOnPageScaleChangedHelper.notifyCalled();
70 protected TestDependencyFactory createTestDependencyFactory() {
71 return new TestDependencyFactory() {
73 public AwLayoutSizer createLayoutSizer() {
74 return new TestAwLayoutSizer();
79 final LinearLayout.LayoutParams mWrapContentLayoutParams =
80 new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
82 private AwTestContainerView createCustomTestContainerViewOnMainSync(
83 final AwContentsClient awContentsClient, final int visibility) throws Exception {
84 final AtomicReference<AwTestContainerView> testContainerView =
85 new AtomicReference<AwTestContainerView>();
86 getInstrumentation().runOnMainSync(new Runnable() {
89 testContainerView.set(createAwTestContainerView(awContentsClient));
90 testContainerView.get().setLayoutParams(mWrapContentLayoutParams);
91 testContainerView.get().setVisibility(visibility);
94 return testContainerView.get();
97 private AwTestContainerView createDetachedTestContainerViewOnMainSync(
98 final AwContentsClient awContentsClient) {
99 final AtomicReference<AwTestContainerView> testContainerView =
100 new AtomicReference<AwTestContainerView>();
101 getInstrumentation().runOnMainSync(new Runnable() {
104 testContainerView.set(createDetachedAwTestContainerView(awContentsClient));
107 return testContainerView.get();
110 private void assertZeroHeight(final AwTestContainerView testContainerView) throws Throwable {
111 // Make sure the test isn't broken by the view having a non-zero height.
112 getInstrumentation().runOnMainSync(new Runnable() {
115 assertEquals(0, testContainerView.getHeight());
120 private int getRootLayoutWidthOnMainThread() throws Exception {
121 final AtomicReference<Integer> width = new AtomicReference<Integer>();
122 getInstrumentation().runOnMainSync(new Runnable() {
125 width.set(Integer.valueOf(getActivity().getRootLayoutWidth()));
132 * This checks for issues related to loading content into a 0x0 view.
134 * A 0x0 sized view is common if the WebView is set to wrap_content and newly created. The
135 * expected behavior is for the WebView to expand after some content is loaded.
136 * In Chromium it would be valid to not load or render content into a WebContents with a 0x0
137 * view (since the user can't see it anyway) and only do so after the view's size is non-zero.
138 * Such behavior is unacceptable for the WebView and this test is to ensure that such behavior
139 * is not re-introduced.
142 @Feature({"AndroidWebView"})
143 public void testZeroByZeroViewLoadsContent() throws Throwable {
144 final TestAwContentsClient contentsClient = new TestAwContentsClient();
145 final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync(
146 contentsClient, View.VISIBLE);
147 assertZeroHeight(testContainerView);
149 final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount();
150 final int pageScaleChangeCallCount = mOnPageScaleChangedHelper.getCallCount();
151 loadUrlAsync(testContainerView.getAwContents(), CommonResources.ABOUT_HTML);
152 mOnPageScaleChangedHelper.waitForCallback(pageScaleChangeCallCount);
153 mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount);
154 assertTrue(mOnContentSizeChangedHelper.getHeight() > 0);
158 * Check that a content size change notification is issued when the view is invisible.
160 * This makes sure that any optimizations related to the view's visibility don't inhibit
161 * the ability to load pages. Many applications keep the WebView hidden when it's loading.
164 @Feature({"AndroidWebView"})
165 public void testInvisibleViewLoadsContent() throws Throwable {
166 final TestAwContentsClient contentsClient = new TestAwContentsClient();
167 final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync(
168 contentsClient, View.INVISIBLE);
169 assertZeroHeight(testContainerView);
171 final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount();
172 final int pageScaleChangeCallCount = mOnPageScaleChangedHelper.getCallCount();
173 loadUrlAsync(testContainerView.getAwContents(), CommonResources.ABOUT_HTML);
174 mOnPageScaleChangedHelper.waitForCallback(pageScaleChangeCallCount);
175 mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount);
176 assertTrue(mOnContentSizeChangedHelper.getHeight() > 0);
178 getInstrumentation().runOnMainSync(new Runnable() {
181 assertEquals(View.INVISIBLE, testContainerView.getVisibility());
187 * Check that a content size change notification is sent even if the WebView is off screen.
190 @Feature({"AndroidWebView"})
191 public void testDisconnectedViewLoadsContent() throws Throwable {
192 final TestAwContentsClient contentsClient = new TestAwContentsClient();
193 final AwTestContainerView testContainerView =
194 createDetachedTestContainerViewOnMainSync(contentsClient);
195 assertZeroHeight(testContainerView);
197 final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount();
198 final int pageScaleChangeCallCount = mOnPageScaleChangedHelper.getCallCount();
199 loadUrlAsync(testContainerView.getAwContents(), CommonResources.ABOUT_HTML);
200 mOnPageScaleChangedHelper.waitForCallback(pageScaleChangeCallCount);
201 mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount);
202 assertTrue(mOnContentSizeChangedHelper.getHeight() > 0);
205 private String makeHtmlPageOfSize(int widthCss, int heightCss, boolean heightPercent) {
206 String content = "<div class=\"normal\">a</div>";
208 content += "<div class=\"heightPercent\"></div>";
209 return CommonResources.makeHtmlPageFrom(
210 "<style type=\"text/css\">" +
211 "body { margin:0px; padding:0px; } " +
213 "width:" + widthCss + "px; " +
214 "height:" + heightCss + "px; " +
215 "background-color: red; " +
217 ".heightPercent { " +
219 "background-color: blue; " +
221 "</style>", content);
224 private void waitForContentSizeToChangeTo(OnContentSizeChangedHelper helper, int callCount,
225 int widthCss, int heightCss) throws Exception {
226 final int maxSizeChangeNotificationsToWaitFor = 5;
227 for (int i = 1; i <= maxSizeChangeNotificationsToWaitFor; i++) {
228 helper.waitForCallback(callCount, i);
229 if ((heightCss == -1 || helper.getHeight() == heightCss) &&
230 (widthCss == -1 || helper.getWidth() == widthCss)) {
233 // This means that we hit the max number of iterations but the expected contents size
235 assertTrue(i != maxSizeChangeNotificationsToWaitFor);
239 private void loadPageOfSizeAndWaitForSizeChange(AwContents awContents,
240 OnContentSizeChangedHelper helper, int widthCss, int heightCss,
241 boolean heightPercent) throws Exception {
243 final String htmlData = makeHtmlPageOfSize(widthCss, heightCss, heightPercent);
244 final int contentSizeChangeCallCount = helper.getCallCount();
245 loadDataAsync(awContents, htmlData, "text/html", false);
247 waitForContentSizeToChangeTo(helper, contentSizeChangeCallCount, widthCss, heightCss);
251 @Feature({"AndroidWebView"})
252 public void testSizeUpdateWhenDetached() throws Throwable {
253 final TestAwContentsClient contentsClient = new TestAwContentsClient();
254 final AwTestContainerView testContainerView = createDetachedTestContainerViewOnMainSync(
256 assertZeroHeight(testContainerView);
258 final int contentWidthCss = 142;
259 final int contentHeightCss = 180;
261 loadPageOfSizeAndWaitForSizeChange(testContainerView.getAwContents(),
262 mOnContentSizeChangedHelper, contentWidthCss, contentHeightCss, false);
265 public void waitForNoLayoutsPending() throws InterruptedException {
266 // This is to make sure that there are no more pending size change notifications. Ideally
267 // we'd assert that the renderer is idle (has no pending layout passes) but that would
268 // require quite a bit of plumbing, so we just wait a bit and make sure the size hadn't
270 Thread.sleep(CONTENT_SIZE_CHANGE_STABILITY_TIMEOUT_MS);
274 @Feature({"AndroidWebView"})
275 public void testAbsolutePositionContributesToContentSize() throws Throwable {
276 final TestAwContentsClient contentsClient = new TestAwContentsClient();
277 final AwTestContainerView testContainerView = createDetachedTestContainerViewOnMainSync(
279 assertZeroHeight(testContainerView);
281 final int widthCss = 142;
282 final int heightCss = 180;
284 final String htmlData = CommonResources.makeHtmlPageFrom(
285 "<style type=\"text/css\">" +
286 "body { margin:0px; padding:0px; } " +
288 "position: absolute; " +
289 "width:" + widthCss + "px; " +
290 "height:" + heightCss + "px; " +
291 "background-color: red; " +
293 "</style>", "<div>a</div>");
295 final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount();
296 loadDataAsync(testContainerView.getAwContents(), htmlData, "text/html", false);
298 waitForContentSizeToChangeTo(mOnContentSizeChangedHelper, contentSizeChangeCallCount,
299 widthCss, heightCss);
303 @Feature({"AndroidWebView"})
304 public void testViewSizedCorrectlyInWrapContentMode() throws Throwable {
305 final TestAwContentsClient contentsClient = new TestAwContentsClient();
306 final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync(
307 contentsClient, View.VISIBLE);
308 assertZeroHeight(testContainerView);
310 final double deviceDIPScale =
311 DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
313 final int contentWidthCss = 142;
314 final int contentHeightCss = 180;
316 // In wrap-content mode the AwLayoutSizer will size the view to be as wide as the parent
318 final int expectedWidthCss =
319 (int) Math.ceil(getRootLayoutWidthOnMainThread() / deviceDIPScale);
320 final int expectedHeightCss = contentHeightCss;
322 loadPageOfSizeAndWaitForSizeChange(testContainerView.getAwContents(),
323 mOnContentSizeChangedHelper, expectedWidthCss, expectedHeightCss, false);
325 waitForNoLayoutsPending();
326 assertEquals(expectedWidthCss, mOnContentSizeChangedHelper.getWidth());
327 assertEquals(expectedHeightCss, mOnContentSizeChangedHelper.getHeight());
331 @Feature({"AndroidWebView"})
332 public void testViewSizedCorrectlyInWrapContentModeWithDynamicContents() throws Throwable {
333 final TestAwContentsClient contentsClient = new TestAwContentsClient();
334 final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync(
335 contentsClient, View.VISIBLE);
336 assertZeroHeight(testContainerView);
338 final double deviceDIPScale =
339 DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
341 final int contentWidthCss = 142;
342 final int contentHeightCss = 180;
344 final int expectedWidthCss =
345 (int) Math.ceil(getRootLayoutWidthOnMainThread() / deviceDIPScale);
346 final int expectedHeightCss = contentHeightCss;
348 loadPageOfSizeAndWaitForSizeChange(testContainerView.getAwContents(),
349 mOnContentSizeChangedHelper, expectedWidthCss, contentHeightCss, true);
351 waitForNoLayoutsPending();
352 assertEquals(expectedWidthCss, mOnContentSizeChangedHelper.getWidth());
353 assertEquals(expectedHeightCss, mOnContentSizeChangedHelper.getHeight());
357 @Feature({"AndroidWebView"})
358 public void testReceivingSizeAfterLoadUpdatesLayout() throws Throwable {
359 final TestAwContentsClient contentsClient = new TestAwContentsClient();
360 final AwTestContainerView testContainerView = createDetachedTestContainerViewOnMainSync(
362 final AwContents awContents = testContainerView.getAwContents();
364 final double deviceDIPScale =
365 DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
366 final int physicalWidth = 600;
367 final int spanWidth = 42;
368 final int expectedWidthCss =
369 (int) Math.ceil(physicalWidth / deviceDIPScale);
371 StringBuilder htmlBuilder = new StringBuilder("<html><body style='margin:0px;'>");
372 final String spanBlock =
373 "<span style='width: " + spanWidth + "px; display: inline-block;'>a</span>";
374 for (int i = 0; i < 10; ++i) {
375 htmlBuilder.append(spanBlock);
377 htmlBuilder.append("</body></html>");
379 int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount();
380 loadDataAsync(awContents, htmlBuilder.toString(), "text/html", false);
381 // Because we're loading the contents into a detached WebView its layout size is 0x0 and as
382 // a result of that the paragraph will be formated such that each word is on a separate
384 waitForContentSizeToChangeTo(mOnContentSizeChangedHelper, contentSizeChangeCallCount,
387 final int narrowLayoutHeight = mOnContentSizeChangedHelper.getHeight();
389 contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount();
390 getInstrumentation().runOnMainSync(new Runnable() {
393 testContainerView.onSizeChanged(physicalWidth, 0, 0, 0);
396 mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount);
398 // As a result of calling the onSizeChanged method the layout size should be updated to
399 // match the width of the webview and the text we previously loaded should reflow making the
400 // contents width match the WebView width.
401 assertEquals(expectedWidthCss, mOnContentSizeChangedHelper.getWidth());
402 assertTrue(mOnContentSizeChangedHelper.getHeight() < narrowLayoutHeight);
403 assertTrue(mOnContentSizeChangedHelper.getHeight() > 0);