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);
63 public void onPageScaleChanged(float pageScaleFactor) {
64 super.onPageScaleChanged(pageScaleFactor);
65 if (mOnPageScaleChangedHelper != null) {
66 mOnPageScaleChangedHelper.notifyCalled();
72 protected TestDependencyFactory createTestDependencyFactory() {
73 return new TestDependencyFactory() {
75 public AwLayoutSizer createLayoutSizer() {
76 return new TestAwLayoutSizer();
81 final LinearLayout.LayoutParams mWrapContentLayoutParams =
82 new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
84 private AwTestContainerView createCustomTestContainerViewOnMainSync(
85 final AwContentsClient awContentsClient, final int visibility) throws Exception {
86 final AtomicReference<AwTestContainerView> testContainerView =
87 new AtomicReference<AwTestContainerView>();
88 getInstrumentation().runOnMainSync(new Runnable() {
91 testContainerView.set(createAwTestContainerView(awContentsClient));
92 testContainerView.get().setLayoutParams(mWrapContentLayoutParams);
93 testContainerView.get().setVisibility(visibility);
96 return testContainerView.get();
99 private AwTestContainerView createDetachedTestContainerViewOnMainSync(
100 final AwContentsClient awContentsClient) {
101 final AtomicReference<AwTestContainerView> testContainerView =
102 new AtomicReference<AwTestContainerView>();
103 getInstrumentation().runOnMainSync(new Runnable() {
106 testContainerView.set(createDetachedAwTestContainerView(awContentsClient));
109 return testContainerView.get();
112 private void assertZeroHeight(final AwTestContainerView testContainerView) throws Throwable {
113 // Make sure the test isn't broken by the view having a non-zero height.
114 getInstrumentation().runOnMainSync(new Runnable() {
117 assertEquals(0, testContainerView.getHeight());
122 private int getRootLayoutWidthOnMainThread() throws Exception {
123 final AtomicReference<Integer> width = new AtomicReference<Integer>();
124 getInstrumentation().runOnMainSync(new Runnable() {
127 width.set(Integer.valueOf(getActivity().getRootLayoutWidth()));
134 * This checks for issues related to loading content into a 0x0 view.
136 * A 0x0 sized view is common if the WebView is set to wrap_content and newly created. The
137 * expected behavior is for the WebView to expand after some content is loaded.
138 * In Chromium it would be valid to not load or render content into a WebContents with a 0x0
139 * view (since the user can't see it anyway) and only do so after the view's size is non-zero.
140 * Such behavior is unacceptable for the WebView and this test is to ensure that such behavior
141 * is not re-introduced.
144 @Feature({"AndroidWebView"})
145 public void testZeroByZeroViewLoadsContent() throws Throwable {
146 final TestAwContentsClient contentsClient = new TestAwContentsClient();
147 final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync(
148 contentsClient, View.VISIBLE);
149 assertZeroHeight(testContainerView);
151 final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount();
152 final int pageScaleChangeCallCount = mOnPageScaleChangedHelper.getCallCount();
153 loadUrlAsync(testContainerView.getAwContents(), CommonResources.ABOUT_HTML);
154 mOnPageScaleChangedHelper.waitForCallback(pageScaleChangeCallCount);
155 mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount);
156 assertTrue(mOnContentSizeChangedHelper.getHeight() > 0);
160 * Check that a content size change notification is issued when the view is invisible.
162 * This makes sure that any optimizations related to the view's visibility don't inhibit
163 * the ability to load pages. Many applications keep the WebView hidden when it's loading.
166 @Feature({"AndroidWebView"})
167 public void testInvisibleViewLoadsContent() throws Throwable {
168 final TestAwContentsClient contentsClient = new TestAwContentsClient();
169 final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync(
170 contentsClient, View.INVISIBLE);
171 assertZeroHeight(testContainerView);
173 final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount();
174 final int pageScaleChangeCallCount = mOnPageScaleChangedHelper.getCallCount();
175 loadUrlAsync(testContainerView.getAwContents(), CommonResources.ABOUT_HTML);
176 mOnPageScaleChangedHelper.waitForCallback(pageScaleChangeCallCount);
177 mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount);
178 assertTrue(mOnContentSizeChangedHelper.getHeight() > 0);
180 getInstrumentation().runOnMainSync(new Runnable() {
183 assertEquals(View.INVISIBLE, testContainerView.getVisibility());
189 * Check that a content size change notification is sent even if the WebView is off screen.
192 @Feature({"AndroidWebView"})
193 public void testDisconnectedViewLoadsContent() throws Throwable {
194 final TestAwContentsClient contentsClient = new TestAwContentsClient();
195 final AwTestContainerView testContainerView =
196 createDetachedTestContainerViewOnMainSync(contentsClient);
197 assertZeroHeight(testContainerView);
199 final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount();
200 final int pageScaleChangeCallCount = mOnPageScaleChangedHelper.getCallCount();
201 loadUrlAsync(testContainerView.getAwContents(), CommonResources.ABOUT_HTML);
202 mOnPageScaleChangedHelper.waitForCallback(pageScaleChangeCallCount);
203 mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount);
204 assertTrue(mOnContentSizeChangedHelper.getHeight() > 0);
207 private String makeHtmlPageOfSize(int widthCss, int heightCss, boolean heightPercent) {
208 String content = "<div class=\"normal\">a</div>";
209 if (heightPercent) content += "<div class=\"heightPercent\"></div>";
210 return CommonResources.makeHtmlPageFrom(
211 "<style type=\"text/css\">" +
212 "body { margin:0px; padding:0px; } " +
214 "width:" + widthCss + "px; " +
215 "height:" + heightCss + "px; " +
216 "background-color: red; " +
218 ".heightPercent { " +
220 "background-color: blue; " +
222 "</style>", content);
225 private void waitForContentSizeToChangeTo(OnContentSizeChangedHelper helper, int callCount,
226 int widthCss, int heightCss) throws Exception {
227 final int maxSizeChangeNotificationsToWaitFor = 5;
228 for (int i = 1; i <= maxSizeChangeNotificationsToWaitFor; i++) {
229 helper.waitForCallback(callCount, i);
230 if ((heightCss == -1 || helper.getHeight() == heightCss) &&
231 (widthCss == -1 || helper.getWidth() == widthCss)) {
234 // This means that we hit the max number of iterations but the expected contents size
236 assertTrue(i != maxSizeChangeNotificationsToWaitFor);
240 private void loadPageOfSizeAndWaitForSizeChange(AwContents awContents,
241 OnContentSizeChangedHelper helper, int widthCss, int heightCss,
242 boolean heightPercent) throws Exception {
244 final String htmlData = makeHtmlPageOfSize(widthCss, heightCss, heightPercent);
245 final int contentSizeChangeCallCount = helper.getCallCount();
246 loadDataAsync(awContents, htmlData, "text/html", false);
248 waitForContentSizeToChangeTo(helper, contentSizeChangeCallCount, widthCss, heightCss);
252 @Feature({"AndroidWebView"})
253 public void testSizeUpdateWhenDetached() throws Throwable {
254 final TestAwContentsClient contentsClient = new TestAwContentsClient();
255 final AwTestContainerView testContainerView = createDetachedTestContainerViewOnMainSync(
257 assertZeroHeight(testContainerView);
259 final int contentWidthCss = 142;
260 final int contentHeightCss = 180;
262 loadPageOfSizeAndWaitForSizeChange(testContainerView.getAwContents(),
263 mOnContentSizeChangedHelper, contentWidthCss, contentHeightCss, false);
266 public void waitForNoLayoutsPending() throws InterruptedException {
267 // This is to make sure that there are no more pending size change notifications. Ideally
268 // we'd assert that the renderer is idle (has no pending layout passes) but that would
269 // require quite a bit of plumbing, so we just wait a bit and make sure the size hadn't
271 Thread.sleep(CONTENT_SIZE_CHANGE_STABILITY_TIMEOUT_MS);
275 @Feature({"AndroidWebView"})
276 public void testAbsolutePositionContributesToContentSize() throws Throwable {
277 final TestAwContentsClient contentsClient = new TestAwContentsClient();
278 final AwTestContainerView testContainerView = createDetachedTestContainerViewOnMainSync(
280 assertZeroHeight(testContainerView);
282 final int widthCss = 142;
283 final int heightCss = 180;
285 final String htmlData = CommonResources.makeHtmlPageFrom(
286 "<style type=\"text/css\">" +
287 " body { margin:0px; padding:0px; } " +
289 " position: absolute; " +
290 " width:" + widthCss + "px; " +
291 " height:" + heightCss + "px; " +
292 " background-color: red; " +
294 "</style>", "<div>a</div>");
296 final int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount();
297 loadDataAsync(testContainerView.getAwContents(), htmlData, "text/html", false);
299 waitForContentSizeToChangeTo(mOnContentSizeChangedHelper, contentSizeChangeCallCount,
300 widthCss, heightCss);
304 @Feature({"AndroidWebView"})
305 public void testViewSizedCorrectlyInWrapContentMode() throws Throwable {
306 final TestAwContentsClient contentsClient = new TestAwContentsClient();
307 final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync(
308 contentsClient, View.VISIBLE);
309 assertZeroHeight(testContainerView);
311 final double deviceDIPScale =
312 DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
314 final int contentWidthCss = 142;
315 final int contentHeightCss = 180;
317 // In wrap-content mode the AwLayoutSizer will size the view to be as wide as the parent
319 final int expectedWidthCss =
320 (int) Math.ceil(getRootLayoutWidthOnMainThread() / deviceDIPScale);
321 final int expectedHeightCss = contentHeightCss;
323 loadPageOfSizeAndWaitForSizeChange(testContainerView.getAwContents(),
324 mOnContentSizeChangedHelper, expectedWidthCss, expectedHeightCss, false);
326 waitForNoLayoutsPending();
327 assertEquals(expectedWidthCss, mOnContentSizeChangedHelper.getWidth());
328 assertEquals(expectedHeightCss, mOnContentSizeChangedHelper.getHeight());
332 @Feature({"AndroidWebView"})
333 public void testViewSizedCorrectlyInWrapContentModeWithDynamicContents() throws Throwable {
334 final TestAwContentsClient contentsClient = new TestAwContentsClient();
335 final AwTestContainerView testContainerView = createCustomTestContainerViewOnMainSync(
336 contentsClient, View.VISIBLE);
337 assertZeroHeight(testContainerView);
339 final double deviceDIPScale =
340 DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
342 final int contentWidthCss = 142;
343 final int contentHeightCss = 180;
345 final int expectedWidthCss =
346 (int) Math.ceil(getRootLayoutWidthOnMainThread() / deviceDIPScale);
347 final int expectedHeightCss = contentHeightCss;
349 loadPageOfSizeAndWaitForSizeChange(testContainerView.getAwContents(),
350 mOnContentSizeChangedHelper, expectedWidthCss, contentHeightCss, true);
352 waitForNoLayoutsPending();
353 assertEquals(expectedWidthCss, mOnContentSizeChangedHelper.getWidth());
354 assertEquals(expectedHeightCss, mOnContentSizeChangedHelper.getHeight());
358 @Feature({"AndroidWebView"})
359 public void testReceivingSizeAfterLoadUpdatesLayout() throws Throwable {
360 final TestAwContentsClient contentsClient = new TestAwContentsClient();
361 final AwTestContainerView testContainerView = createDetachedTestContainerViewOnMainSync(
363 final AwContents awContents = testContainerView.getAwContents();
365 final double deviceDIPScale =
366 DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
367 final int physicalWidth = 600;
368 final int spanWidth = 42;
369 final int expectedWidthCss =
370 (int) Math.ceil(physicalWidth / deviceDIPScale);
372 StringBuilder htmlBuilder = new StringBuilder("<html><body style='margin:0px;'>");
373 final String spanBlock =
374 "<span style='width: " + spanWidth + "px; display: inline-block;'>a</span>";
375 for (int i = 0; i < 10; ++i) {
376 htmlBuilder.append(spanBlock);
378 htmlBuilder.append("</body></html>");
380 int contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount();
381 loadDataAsync(awContents, htmlBuilder.toString(), "text/html", false);
382 // Because we're loading the contents into a detached WebView its layout size is 0x0 and as
383 // a result of that the paragraph will be formated such that each word is on a separate
385 waitForContentSizeToChangeTo(mOnContentSizeChangedHelper, contentSizeChangeCallCount,
388 final int narrowLayoutHeight = mOnContentSizeChangedHelper.getHeight();
390 contentSizeChangeCallCount = mOnContentSizeChangedHelper.getCallCount();
391 getInstrumentation().runOnMainSync(new Runnable() {
394 testContainerView.onSizeChanged(physicalWidth, 0, 0, 0);
397 mOnContentSizeChangedHelper.waitForCallback(contentSizeChangeCallCount);
399 // As a result of calling the onSizeChanged method the layout size should be updated to
400 // match the width of the webview and the text we previously loaded should reflow making the
401 // contents width match the WebView width.
402 assertEquals(expectedWidthCss, mOnContentSizeChangedHelper.getWidth());
403 assertTrue(mOnContentSizeChangedHelper.getHeight() < narrowLayoutHeight);
404 assertTrue(mOnContentSizeChangedHelper.getHeight() > 0);