Skip to content
131 changes: 131 additions & 0 deletions tests/system/src/test/java/test/robot/javafx/stage/StageFocusTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

package test.robot.javafx.stage;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.paint.Color;
import javafx.scene.robot.Robot;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import test.util.Util;
import test.robot.testharness.VisualTestBase;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;


public class StageFocusTest extends VisualTestBase {

static CountDownLatch startupLatch;
static CountDownLatch eventReceivedLatch;

static final int STAGE_SIZE = 200;

static final int STAGE_X = 100;
static final int STAGE_Y = 100;

static final int TIMEOUT = 2000; // ms
static final double TOLERANCE = 0.07;

private Stage theStage = null;

@BeforeAll
public static void setupOnce() throws Exception {
startupLatch = new CountDownLatch(1);
eventReceivedLatch = new CountDownLatch(1);
}

/**
* Checks whether Stage is actually shown when calling show()
*
* Meant as a "canary" test of sorts to ensure other tests relying on
* Stage being actually shown and on foreground work fine.
*/
@Test
public void testStageHasFocusAfterShow() throws InterruptedException {
Util.runAndWait(() -> {
theStage = getStage(false);

Group root = new Group();
Scene scene = new Scene(root, STAGE_SIZE, STAGE_SIZE);
scene.setFill(Color.LIGHTGREEN);
scene.setOnKeyPressed(e -> {
if (e.getCode() == KeyCode.A) {
eventReceivedLatch.countDown();
}
});

theStage.setScene(scene);
theStage.addEventHandler(WindowEvent.WINDOW_SHOWN, e -> {
Platform.runLater(() -> {
theStage.setX(STAGE_X);
theStage.setY(STAGE_Y);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setting Stage (x,y) coordinates in runLater makes the window jump.
it's probably better to move LL98-99 before runLater, to L96

with the robot disabled, the test fails with expected

org.opentest4j.AssertionFailedError: Event received latch timed out! Stage most probably did not have focus after showing. Some tests might fail because of this. If that is the case, try re-running the tests with '--no-daemon' flag in Gradle. ==> expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)

startupLatch.countDown();
});
});
theStage.show();
});

assertTrue(startupLatch.await(TIMEOUT, TimeUnit.MILLISECONDS), "Timeout waiting for test stage to be shown");

// check if isFocused returns true
assertTrue(
theStage.isFocused(),
"Stage.isFocused() returned false! Stage does not have focus after showing. Some tests might fail because of this. " +
"If that is the case, try re-running the tests with '--no-daemon' flag in Gradle."
);

// give UI a bit of time to finish showing transition
// ex. on Windows above latch is set despite the UI still "animating" the show
sleep(500);

// check if window is on top
Util.runAndWait(() -> {
Color color = getColor(STAGE_SIZE / 2, STAGE_SIZE / 2);
assertColorEquals(Color.LIGHTGREEN, color, TOLERANCE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this implementation is a reliable test: the stage in question may be overlapped by another window somewhere in the corner, right?

What would be a reliable test? Strictly speaking, we must check every pixel in the scene, though I wonder if checking each pixel in a grid (maybe 20 x 20, since we don't expect any reasonable window to be less than that) should be enough?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The most reliable way would be to probably scan the whole grid, I agree... I could change this - the stage is not that big after all. Maybe even turn the stage to undecorated to make this more reliable across platforms. I expect the very edge of the Stage to be not exactly the same color, but if we leave ex. 1px margin around the edges it should still be fine. I'll play around with this and update soon.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that too - but why wouldn't the client area, the scene itself, be of different color? The decorations and possible effects should be outside of the stage, shouldn't they?

Copy link
Contributor Author

@lukostyra lukostyra Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turning the scene to undecorated would just make the math easier, without having to count in the window decorations which almost certainly would be of different color.

As for the margin thing, I had to do a bit of history searching and I came back with this old PR: #1242

I think when writing the above my blurry memory recalled one of the attempts at solving this which involved just skipping the 1px margin of the Stage on HiDPI systems. I agree, it should be fine just going through the entire Stage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually a small correction, using VisualTestBase already assumes the Stage we get is undecorated. So no changes need to be made there.

});

// check if we actually have focus and key presses are registered by the app
Util.runAndWait(() -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the stage isn't focused after the sleep the robot's key press might go to some other app entirely. You might want to add an assert after the sleep checking that stage.isFocused() returns true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this and it actually is a bug, isFocused() returns true in this case (when the Stage is not focused). We don't capture the failure of SetForegroundWindow(), so JFX assumes we are in focus. It doesn't necessarily change the outcome of the test, the key press check will then fail, but it would be good for the flag to also reflect that.

I'll add the assertion and file a separate issue for that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could paint the window in some unusual color and check the screen pixels to make sure the window is on top, in addition to being focused?

getRobot().keyPress(KeyCode.A);
});
assertTrue(
eventReceivedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS),
"Event received latch timed out! Stage most probably did not have focus after showing. Some tests might fail because of this. " +
"If that is the case, try re-running the tests with '--no-daemon' flag in Gradle."
);
}
}