@@ -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