Skip to content

Commit 34401e9

Browse files
committed
Refine Robolectric UI test bootstrap
1 parent 9b85c43 commit 34401e9

File tree

1 file changed

+68
-83
lines changed

1 file changed

+68
-83
lines changed

scripts/templates/HelloCodenameOneUiTest.java.tmpl

Lines changed: 68 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,22 @@ public class @MAIN_NAME@UiTest {
4444

4545
private static final long STARTUP_TIMEOUT_MS = 30_000L;
4646
private static final long LAYOUT_TIMEOUT_MS = 5_000L;
47+
private static final long RENDER_TIMEOUT_MS = 10_000L;
4748
private static final long EDT_TIMEOUT_MS = 10_000L;
4849

4950
private ActivityController<@MAIN_NAME@Stub> controller;
5051
private @MAIN_NAME@Stub activity;
52+
private View contentView;
5153

5254
@Before
5355
public void setUp() throws Exception {
5456
controller = Robolectric.buildActivity(@[email protected]);
55-
activity = controller.setup().get();
56-
bootstrapCodenameOneApp();
57+
activity = controller.create().start().resume().visible().get();
58+
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
59+
60+
contentView = activity.findViewById(android.R.id.content);
61+
assertNotNull("Activity content view should be available after onResume", contentView);
62+
5763
waitForCodenameOneForm();
5864
}
5965

@@ -63,15 +69,13 @@ public class @MAIN_NAME@UiTest {
6369
controller.pause();
6470
controller.stop();
6571
controller.destroy();
72+
controller = null;
6673
}
6774
}
6875

6976
@Test
7077
public void mainFormScreenshotContainsRenderedContent() throws Exception {
71-
View decorView = activity.getWindow().getDecorView();
72-
assertNotNull("Activity decor view should be available", decorView);
73-
74-
Bitmap androidScreenshot = captureAndroidScreenshot(decorView);
78+
Bitmap androidScreenshot = waitForAndroidScreenshot(contentView);
7579
assertTrue("Android screenshot width should be positive", androidScreenshot.getWidth() > 0);
7680
assertTrue("Android screenshot height should be positive", androidScreenshot.getHeight() > 0);
7781
assertTrue(
@@ -82,7 +86,7 @@ public class @MAIN_NAME@UiTest {
8286
assertTrue("Android screenshot file should exist", androidScreenshotFile.isFile());
8387
assertTrue("Android screenshot file should not be empty", androidScreenshotFile.length() > 0L);
8488

85-
Image codenameOneScreenshot = captureCodenameOneScreenshot();
89+
Image codenameOneScreenshot = waitForCodenameOneScreenshot();
8690
assertNotNull("Codename One screenshot should be available", codenameOneScreenshot);
8791
assertTrue("Codename One screenshot width should be positive", codenameOneScreenshot.getWidth() > 0);
8892
assertTrue("Codename One screenshot height should be positive", codenameOneScreenshot.getHeight() > 0);
@@ -97,7 +101,27 @@ public class @MAIN_NAME@UiTest {
97101
assertTrue("Codename One screenshot file should not be empty", codenameOneScreenshotFile.length() > 0L);
98102
}
99103

100-
private Bitmap captureAndroidScreenshot(View decorView) throws Exception {
104+
private Bitmap waitForAndroidScreenshot(View viewFromOnResume) throws Exception {
105+
View decorView = viewFromOnResume.getRootView();
106+
long deadline = SystemClock.uptimeMillis() + RENDER_TIMEOUT_MS;
107+
Bitmap lastCapture = null;
108+
109+
while (SystemClock.uptimeMillis() < deadline) {
110+
Bitmap candidate = attemptAndroidScreenshot(decorView);
111+
lastCapture = candidate;
112+
if (candidate != null && hasRenderableContent(candidate)) {
113+
return candidate;
114+
}
115+
SystemClock.sleep(32L);
116+
}
117+
118+
if (lastCapture != null) {
119+
return lastCapture;
120+
}
121+
throw new AssertionError("Timed out waiting for Android screenshot to contain rendered content");
122+
}
123+
124+
private Bitmap attemptAndroidScreenshot(View decorView) throws Exception {
101125
ensureViewHasLayout(decorView);
102126
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
103127
flushCodenameOneGraphics();
@@ -112,7 +136,30 @@ public class @MAIN_NAME@UiTest {
112136
return captureBitmap(decorView);
113137
}
114138

139+
private Image waitForCodenameOneScreenshot() throws Exception {
140+
long deadline = SystemClock.uptimeMillis() + RENDER_TIMEOUT_MS;
141+
Image lastCapture = null;
142+
143+
while (SystemClock.uptimeMillis() < deadline) {
144+
flushCodenameOneGraphics();
145+
Image screenshot = captureCodenameOneScreenshot();
146+
lastCapture = screenshot;
147+
if (screenshot != null && hasRenderableContent(screenshot)) {
148+
return screenshot;
149+
}
150+
SystemClock.sleep(32L);
151+
}
152+
153+
if (lastCapture != null) {
154+
return lastCapture;
155+
}
156+
throw new AssertionError("Timed out waiting for Codename One screenshot to contain rendered content");
157+
}
158+
115159
private static Bitmap captureBitmap(View view) {
160+
if (view.getWidth() <= 0 || view.getHeight() <= 0) {
161+
return Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
162+
}
116163
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
117164
Canvas canvas = new Canvas(bitmap);
118165
view.draw(canvas);
@@ -146,6 +193,9 @@ public class @MAIN_NAME@UiTest {
146193
}
147194

148195
private static boolean hasRenderableContent(Image screenshot) throws Exception {
196+
if (!Display.isInitialized()) {
197+
return false;
198+
}
149199
int width = screenshot.getWidth();
150200
int height = screenshot.getHeight();
151201
int[] pixels = callSeriallyAndWait(screenshot::getRGB);
@@ -210,12 +260,12 @@ public class @MAIN_NAME@UiTest {
210260
return outputDir;
211261
}
212262

213-
private static void waitForCodenameOneForm() {
263+
private static void waitForCodenameOneForm() throws Exception {
214264
long deadline = SystemClock.uptimeMillis() + STARTUP_TIMEOUT_MS;
215265
while (SystemClock.uptimeMillis() < deadline) {
216266
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
217267
if (Display.isInitialized()) {
218-
Form current = Display.getInstance().getCurrent();
268+
Form current = callSeriallyAndWait(() -> Display.getInstance().getCurrent());
219269
if (current != null) {
220270
return;
221271
}
@@ -225,37 +275,10 @@ public class @MAIN_NAME@UiTest {
225275
throw new AssertionError("Timed out waiting for Codename One main form to be displayed");
226276
}
227277

228-
private void bootstrapCodenameOneApp() throws Exception {
229-
runOnMainSync(() -> AndroidImplementation.startContext(activity));
230-
if (!Display.isInitialized()) {
231-
runOnMainSync(() -> Display.init(activity));
232-
}
278+
private static Image captureCodenameOneScreenshot() throws Exception {
233279
if (!Display.isInitialized()) {
234-
throw new AssertionError("Codename One Display failed to initialize");
235-
}
236-
callSeriallyAndWait(() -> {
237-
@MAIN_NAME@ app = @[email protected]();
238-
boolean needsInit = isStubFirstStart(app);
239-
if (app == null) {
240-
app = new @MAIN_NAME@();
241-
assignStubAppInstance(app);
242-
needsInit = true;
243-
}
244-
if (needsInit) {
245-
app.init(activity);
246-
}
247-
app.start();
248-
markStubFirstTimeConsumed();
249280
return null;
250-
});
251-
}
252-
253-
private void runOnMainSync(Runnable runnable) {
254-
activity.runOnUiThread(runnable);
255-
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
256-
}
257-
258-
private static Image captureCodenameOneScreenshot() throws Exception {
281+
}
259282
return callSeriallyAndWait(() -> Display.getInstance().captureScreen());
260283
}
261284

@@ -341,6 +364,9 @@ public class @MAIN_NAME@UiTest {
341364
}
342365

343366
private static void flushCodenameOneGraphics() throws Exception {
367+
if (!Display.isInitialized()) {
368+
return;
369+
}
344370
callSeriallyAndWait(() -> {
345371
Form current = Display.getInstance().getCurrent();
346372
if (current != null) {
@@ -372,51 +398,10 @@ public class @MAIN_NAME@UiTest {
372398
}
373399
}
374400

375-
private static void assignStubAppInstance(@MAIN_NAME@ app) {
376-
try {
377-
Field appField = @[email protected]("i");
378-
appField.setAccessible(true);
379-
appField.set(null, app);
380-
} catch (NoSuchFieldException | IllegalAccessException e) {
381-
throw new RuntimeException("Unable to assign Codename One application instance", e);
382-
}
383-
}
384-
385-
private boolean isStubFirstStart(@MAIN_NAME@ appInstance) {
386-
try {
387-
Field firstTimeField = @[email protected]("firstTime");
388-
firstTimeField.setAccessible(true);
389-
boolean firstTimeValue;
390-
if (java.lang.reflect.Modifier.isStatic(firstTimeField.getModifiers())) {
391-
firstTimeValue = firstTimeField.getBoolean(null);
392-
} else {
393-
firstTimeValue = firstTimeField.getBoolean(activity);
394-
}
395-
return firstTimeValue || appInstance == null;
396-
} catch (NoSuchFieldException ignored) {
397-
return appInstance == null;
398-
} catch (IllegalAccessException e) {
399-
throw new RuntimeException("Unable to inspect Codename One stub firstTime flag", e);
400-
}
401-
}
402-
403-
private void markStubFirstTimeConsumed() {
404-
try {
405-
Field firstTimeField = @[email protected]("firstTime");
406-
firstTimeField.setAccessible(true);
407-
if (java.lang.reflect.Modifier.isStatic(firstTimeField.getModifiers())) {
408-
firstTimeField.setBoolean(null, false);
409-
} else {
410-
firstTimeField.setBoolean(activity, false);
411-
}
412-
} catch (NoSuchFieldException ignored) {
413-
// Field absent in some stub variants; nothing to update.
414-
} catch (IllegalAccessException e) {
415-
throw new RuntimeException("Unable to update Codename One stub firstTime flag", e);
416-
}
417-
}
418-
419401
private static <T> T callSeriallyAndWait(Callable<T> callable) throws Exception {
402+
if (!Display.isInitialized()) {
403+
throw new IllegalStateException("Codename One Display must be initialized before invoking EDT work");
404+
}
420405
CountDownLatch latch = new CountDownLatch(1);
421406
AtomicReference<T> result = new AtomicReference<>();
422407
AtomicReference<Throwable> error = new AtomicReference<>();

0 commit comments

Comments
 (0)