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