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 package org.chromium.content.browser.androidoverlay;
5 import android.annotation.TargetApi;
6 import android.graphics.Bitmap;
7 import android.graphics.Canvas;
8 import android.graphics.Color;
9 import android.graphics.Rect;
10 import android.os.Build;
11 import android.support.test.InstrumentationRegistry;
12 import android.view.Surface;
13 
14 import androidx.test.filters.MediumTest;
15 
16 import org.junit.Assert;
17 import org.junit.Before;
18 import org.junit.Rule;
19 import org.junit.Test;
20 import org.junit.runner.RunWith;
21 
22 import org.chromium.base.test.BaseJUnit4ClassRunner;
23 import org.chromium.base.test.util.Feature;
24 import org.chromium.base.test.util.MinAndroidSdkLevel;
25 import org.chromium.base.test.util.UrlUtils;
26 import org.chromium.content.browser.RenderCoordinatesImpl;
27 import org.chromium.content.browser.androidoverlay.DialogOverlayImplTestRule.Client;
28 import org.chromium.content_public.browser.test.util.TestThreadUtils;
29 
30 import java.util.concurrent.Callable;
31 
32 /**
33  * Pixel tests for DialogOverlayImpl.  These use UiAutomation, so they only run in JB or above.
34  */
35 @RunWith(BaseJUnit4ClassRunner.class)
36 @MinAndroidSdkLevel(Build.VERSION_CODES.JELLY_BEAN_MR2)
37 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
38 public class DialogOverlayImplPixelTest {
39     // Color that we'll fill the overlay with.
40 
41     @Rule
42     public DialogOverlayImplTestRule mActivityTestRule =
43             new DialogOverlayImplTestRule(TEST_PAGE_DATA_URL);
44 
45     private static final int OVERLAY_FILL_COLOR = Color.BLUE;
46 
47     // CSS coordinates of a div that we'll try to cover with an overlay.
48     private static final int DIV_X_CSS = 10;
49     private static final int DIV_Y_CSS = 20;
50     private static final int DIV_WIDTH_CSS = 300;
51     private static final int DIV_HEIGHT_CSS = 200;
52 
53     // Provide a solid-color div that's positioned / sized by DIV_*_CSS.
54     private static final String TEST_PAGE_STYLE = "<style>"
55             + "div {"
56             + "left: " + DIV_X_CSS + "px;"
57             + "top: " + DIV_Y_CSS + "px;"
58             + "width: " + DIV_WIDTH_CSS + "px;"
59             + "height: " + DIV_HEIGHT_CSS + "px;"
60             + "position: absolute;"
61             + "background: red;"
62             + "}"
63             + "</style>";
64     private static final String TEST_PAGE_DATA_URL = UrlUtils.encodeHtmlDataUri(
65             "<html>" + TEST_PAGE_STYLE + "<body><div></div></body></html>");
66 
67     // Number of retries for various race-prone operations.
68     private static final int NUM_RETRIES = 10;
69 
70     // Delay (msec) between retries.
71     private static final int RETRY_DELAY = 50;
72 
73     // Number of rows and columns that we consider as optional due to rounding and blending diffs.
74     private static final int FUZZY_PIXELS = 1;
75 
76     // DIV_*_CSS converted to screen pixels.
77     int mDivXPx;
78     int mDivYPx;
79     int mDivWidthPx;
80     int mDivHeightPx;
81 
82     // Target area boundaries.
83     // We allow a range because of page / device scaling.  The size of the div and the size of the
84     // area of overlap can be off by a pixel in either direction.  The div can be blended around the
85     // edge, while the overlay position can round differently.
86     int mTargetAreaMinPx;
87     int mTargetAreaMaxPx;
88 
89     // Maximum status bar height that we'll work with.  This just lets us restrict the area of the
90     // screenshot that we inspect, since it's slow.  This should also include the URL bar.
91     private static final int mStatusBarMaxHeightPx = 300;
92 
93     // Area of interest that contains the div, since the whole image is big.
94     Rect mAreaOfInterestPx;
95 
96     // Screenshot of the test page, before we do anything.
97     Bitmap mInitialScreenshot;
98 
99     RenderCoordinatesImpl mCoordinates;
100 
101     @Before
setUp()102     public void setUp() {
103         takeScreenshotOfBackground();
104         mCoordinates = mActivityTestRule.getRenderCoordinates();
105     }
106 
107     // Take a screenshot via UiAutomation, which captures all overlays.
takeScreenshot()108     Bitmap takeScreenshot() {
109         return InstrumentationRegistry.getInstrumentation().getUiAutomation().takeScreenshot();
110     }
111 
112     // Fill |surface| with OVERLAY_FILL_COLOR and return a screenshot.  Note that we have no idea
113     // how long it takes before the image posts, so the screenshot might not reflect it.  Be
114     // prepared to retry.  Note that we always draw the same thing, so it's okay if a retry gets a
115     // screenshot of a previous surface; they're identical.
fillSurface(Surface surface)116     Bitmap fillSurface(Surface surface) {
117         Canvas canvas = surface.lockCanvas(null);
118         canvas.drawColor(OVERLAY_FILL_COLOR);
119         surface.unlockCanvasAndPost(canvas);
120         return takeScreenshot();
121     }
122 
convertCSSToScreenPixels(int css)123     int convertCSSToScreenPixels(int css) {
124         return (int) (css * mCoordinates.getPageScaleFactor()
125                 * mCoordinates.getDeviceScaleFactor());
126     }
127 
128     // Since ContentShell makes our solid color div have some textured background, we have to be
129     // somewhat lenient here.  Plus, sometimes the edges of the div are blended.
isApproximatelyRed(int color)130     boolean isApproximatelyRed(int color) {
131         int r = Color.red(color);
132         return r > 100 && Color.green(color) < r && Color.blue(color) < r;
133     }
134 
135     // Take a screenshot, and wait until we get one that has the background div in it.
takeScreenshotOfBackground()136     void takeScreenshotOfBackground() {
137         mAreaOfInterestPx = new Rect();
138         for (int retries = 0; retries < NUM_RETRIES; retries++) {
139             // Compute the div position in screen pixels.  We recompute these since they sometimes
140             // take a while to settle, also.
141             mDivXPx = convertCSSToScreenPixels(DIV_X_CSS);
142             mDivYPx = convertCSSToScreenPixels(DIV_Y_CSS);
143             mDivWidthPx = convertCSSToScreenPixels(DIV_WIDTH_CSS);
144             mDivHeightPx = convertCSSToScreenPixels(DIV_HEIGHT_CSS);
145 
146             // Allow one edge on each side to be non-overlapping or misdetected.
147             mTargetAreaMaxPx = mDivWidthPx * mDivHeightPx;
148             mTargetAreaMinPx = (mDivWidthPx - FUZZY_PIXELS) * (mDivHeightPx - FUZZY_PIXELS);
149 
150             // Don't read the whole bitmap.  It's quite big.  Assume that the status bar is only at
151             // the top, and that it's at most mStatusBarMaxHeightPx px tall.  We also allow a bit of
152             // room on each side for rounding issues.  Setting these too large just slows down the
153             // test, without affecting the result.
154             mAreaOfInterestPx.left = mDivXPx - FUZZY_PIXELS;
155             mAreaOfInterestPx.top = mDivYPx - FUZZY_PIXELS;
156             mAreaOfInterestPx.right = mDivXPx + mDivWidthPx - 1 + FUZZY_PIXELS;
157             mAreaOfInterestPx.bottom = mDivYPx + mDivHeightPx + mStatusBarMaxHeightPx;
158 
159             mInitialScreenshot = takeScreenshot();
160 
161             int area = 0;
162             for (int ry = mAreaOfInterestPx.top; ry <= mAreaOfInterestPx.bottom; ry++) {
163                 for (int rx = mAreaOfInterestPx.left; rx <= mAreaOfInterestPx.right; rx++) {
164                     if (isApproximatelyRed(mInitialScreenshot.getPixel(rx, ry))) area++;
165                 }
166             }
167 
168             // It's okay if we have some randomly colored other pixels.
169             if (area >= mTargetAreaMinPx) return;
170 
171             try {
172                 Thread.sleep(RETRY_DELAY);
173             } catch (Exception e) {
174             }
175         }
176 
177         Assert.assertTrue(false);
178     }
179 
180     // Count how many pixels in the div are covered by OVERLAY_FILL_COLOR in |overlayScreenshot|,
181     // and return it.
countDivPixelsCoveredByOverlay(Bitmap overlayScreenshot)182     int countDivPixelsCoveredByOverlay(Bitmap overlayScreenshot) {
183         // Find pixels that changed from the source color to the target color.  This should avoid
184         // issues like changes in the status bar, unless we're really unlucky.  It assumes that the
185         // div is actually the expected size; coloring the entire page red would fool this.
186         int area = 0;
187         for (int ry = mAreaOfInterestPx.top; ry <= mAreaOfInterestPx.bottom; ry++) {
188             for (int rx = mAreaOfInterestPx.left; rx <= mAreaOfInterestPx.right; rx++) {
189                 if (isApproximatelyRed(mInitialScreenshot.getPixel(rx, ry))
190                         && overlayScreenshot.getPixel(rx, ry) == OVERLAY_FILL_COLOR) {
191                     area++;
192                 }
193             }
194         }
195 
196         return area;
197     }
198 
199     // Assert that |surface| exactly covers the target div on the page.  Note that we assume that
200     // you have not drawn anything to |surface| yet, so that we can still see the div.
assertDivIsExactlyCovered(Surface surface)201     void assertDivIsExactlyCovered(Surface surface) {
202         // Draw two colors, and count as the area the ones that change between screenshots.  This
203         // lets us notice if the status bar is occluding something, even if it happens to be the
204         // same color.
205         int area = 0;
206         int targetArea = mDivWidthPx * mDivHeightPx;
207         for (int retries = 0; retries < NUM_RETRIES; retries++) {
208             // We fill the overlay every time, in case a resize was pending.  Eventually, we should
209             // reach a steady-state where the surface is resized, and this (or a previous) filled-in
210             // surface is on the screen.
211             Bitmap overlayScreenshot = fillSurface(surface);
212             area = countDivPixelsCoveredByOverlay(overlayScreenshot);
213             if (area >= mTargetAreaMinPx && area <= mTargetAreaMaxPx) return;
214 
215             // There are several reasons this can fail besides being broken.  We don't know how long
216             // it takes for fillSurface()'s output to make it to the display.  We also don't know
217             // how long scheduleLayout() takes.  Just try a few times, since the whole thing should
218             // take only a frame or two to settle.
219             try {
220                 Thread.sleep(RETRY_DELAY);
221             } catch (Exception e) {
222             }
223         }
224 
225         // Assert so that we get a helpful message in the log.
226         Assert.assertEquals(targetArea, area);
227     }
228 
229     // Wait for |overlay| to become ready, get its surface, and return it.
waitForSurface(DialogOverlayImpl overlay)230     Surface waitForSurface(DialogOverlayImpl overlay) throws Exception {
231         Assert.assertNotNull(overlay);
232         final Client.Event event = mActivityTestRule.getClient().nextEvent();
233         Assert.assertTrue(event.surfaceKey > 0);
234         return TestThreadUtils.runOnUiThreadBlocking(new Callable<Surface>() {
235             @Override
236             public Surface call() {
237                 return DialogOverlayImplJni.get().lookupSurfaceForTesting((int) event.surfaceKey);
238             }
239         });
240     }
241 
242     @Test
243     @MediumTest
244     @Feature({"AndroidOverlay"})
245     public void testInitialPosition() throws Exception {
246         // Test that the initial position supplied for the overlay covers the <div> we created.
247         final DialogOverlayImpl overlay =
248                 mActivityTestRule.createOverlay(mDivXPx, mDivYPx, mDivWidthPx, mDivHeightPx);
249         Surface surface = waitForSurface(overlay);
250 
251         assertDivIsExactlyCovered(surface);
252     }
253 
254     @Test
255     @MediumTest
256     @Feature({"AndroidOverlay"})
257     public void testScheduleLayout() throws Exception {
258         // Test that scheduleLayout() moves the overlay to cover the <div>.
259         final DialogOverlayImpl overlay = mActivityTestRule.createOverlay(0, 0, 10, 10);
260         Surface surface = waitForSurface(overlay);
261 
262         final org.chromium.gfx.mojom.Rect rect = new org.chromium.gfx.mojom.Rect();
263         rect.x = mDivXPx;
264         rect.y = mDivYPx;
265         rect.width = mDivWidthPx;
266         rect.height = mDivHeightPx;
267 
268         TestThreadUtils.runOnUiThreadBlocking(() -> { overlay.scheduleLayout(rect); });
269 
270         assertDivIsExactlyCovered(surface);
271     }
272 }
273