Search code examples
javajavafxauthenticationtestfx

TestFx - Can't access login dialog


I have a simple javafx application, where, right after stage.show(), I'm calling login dialog. When I run tests, they does not start to do their work until the login dialog is filled and confirmed manually. For test purposes, I tried to display another dialog after clicking on a button on stage and there were no problems with manipulating it through testFx. The only problem is with the initial login dialog. Is there a way to workaround this behavior, or am I doing something wrong?

Test scenario:

MainApp.java

package cz.mono.monofx;

import cz.mono.monofx.fxml.LoginDialog;
import cz.mono.monofx.fxml.ScreensController;
import java.util.Optional;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.util.Pair;


public class MainApp extends Application {

    public static final String SCREEN1 = "scene";
    private final String screen1fxml = "/fxml/Scene.fxml";
    public static final String SCREEN2 = "scene2";
    private final String screen2fxml = "/fxml/Scene2.fxml";

    @Override
    public void start(Stage stage) throws Exception {

        ScreensController mainContainer = new ScreensController();
        mainContainer.loadScreen(SCREEN1, screen1fxml);
        mainContainer.loadScreen(SCREEN2, screen2fxml);

        mainContainer.setScreen(SCREEN1);

        Group root = new Group(); 
        root.getChildren().addAll(mainContainer);

        Scene scene = new Scene(root);
        scene.getStylesheets().add("/styles/Styles.css");

        stage.setTitle("Aplipikacka");
        stage.setScene(scene);
        stage.show();

        LoginDialog login = new LoginDialog();
        Optional <Pair<String, String>> result = login.getResult();
    }

    /**
     * The main() method is ignored in correctly deployed JavaFX application.
     * main() serves only as fallback in case the application can not be
     * launched through deployment artifacts, e.g., in IDEs with limited FX
     * support. NetBeans ignores main().
     *
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

}

TestFxBase.java

    package cz.mono.monofx.test;

import cz.mono.monofx.MainApp;
import java.util.concurrent.TimeoutException;
import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.stage.Stage;
import org.junit.After;
import org.junit.Before;
import org.testfx.api.FxToolkit;
import org.testfx.framework.junit.ApplicationTest;

public class TestFxBase extends ApplicationTest {

    @Before
    public void setUpClass() throws Exception {
        ApplicationTest.launch(MainApp.class);
    }

    @After
    public void afterEachTest() throws TimeoutException {
        FxToolkit.hideStage();
        release(new KeyCode[]{});
        release(new MouseButton[]{});
    }

    //Helper method to retrieve javafx components
    public <T extends Node> T find(String query) {
        return (T) lookup(query).queryAll().iterator().next();
    } 

    @Override
    public void start(Stage stage) {
        stage.show();
    }
}

SimpleTest.java

    package cz.mono.monofx.test;

import static javafx.scene.input.KeyCode.TAB;
import org.junit.Test;

public class ValidationTest extends TestFxBase {

    @Test
    public void verifyLogin() {
        clickOn("#dialogButton");
        sleep(1000);
        type(TAB);
        sleep(1000);
        type(TAB);
        sleep(1000);
    }
}

LoginDialog.java

package cz.mono.monofx.fxml;

import java.util.Optional;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ButtonBar.ButtonData;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.util.Pair;

public class LoginDialog {

    private Dialog<Pair<String, String>> dialog;
    private TextField username;
    private PasswordField password;
    private Optional<Pair<String, String>> result;

    public LoginDialog() {
        dialog = new Dialog<>();
        dialog.setTitle("Login");
        dialog.setHeaderText("Provide correct login informations.");

        username = new TextField();
        username.setPrefSize(150, 30);
        username.setPromptText("username");
        username.setId("username");

        //Request focus on username by default
        Platform.runLater(()-> username.requestFocus());

        password = new PasswordField();
        password.setPrefSize(150, 30);
        password.setPromptText("password");
        password.setId("password");

        ButtonType loginButtonType = new ButtonType("Login", ButtonData.OK_DONE);
        dialog.getDialogPane().getButtonTypes().addAll(loginButtonType, ButtonType.CANCEL);

        Node loginButton = dialog.getDialogPane().lookupButton(loginButtonType);
        loginButton.setId("loginButton");
        BooleanBinding bb = Bindings.createBooleanBinding(()-> username.getText().isEmpty() || password.getText().isEmpty(), username.textProperty(), password.textProperty());
        loginButton.disableProperty().bind(bb);

        GridPane grid = new GridPane();
        grid.setVgap(10);
        grid.setHgap(10);
        grid.setPadding(new Insets(20, 150, 10, 10));

        grid.add(new Label("Username: "), 0, 0);
        grid.add(username, 0, 1);
        grid.add(new Label("Password"), 1, 0);
        grid.add(password, 1, 1);

        dialog.getDialogPane().setContent(grid);

        // Convert the result to a username-password-pair when the login button is clicked.
        dialog.setResultConverter(dialogButton -> {
            if (dialogButton == loginButtonType) {
                return new Pair<>(username.getText(), password.getText());
            }
            return null;
        });

        result = dialog.showAndWait();
        result.ifPresent(usernamePassword -> {
            System.out.println("Username=" + usernamePassword.getKey() + ", Password=" + usernamePassword.getValue());
        });
        if (!result.isPresent()) {
            System.exit(0);
        };

    }

    /**
     * @return the result
     */
    public Optional<Pair<String, String>> getResult() {
        return result;
    }
}

Solution

  • Let me show one of possible working solutions. I used it on my project successfully.

    The main idea is to have the start application method as minimal as possible:

    public class MainApp extends Application {
        @Override
        public void start(Stage stage) throws Exception {
            stage.setScene(new Scene(new MainPane(stage)));
            stage.show();
        }
    

    All the logic are moved to MainPane class:

    class MainPane extends BorderPane {
        MainPane(final Stage stage) {
            stage.setTitle("Aplipikacka");
    
            // open the login dialog only when the stage is opened too.
            stage.setOnShown(event -> Platform.runLater(this::showLoginDialog));
        }
    
        private void showLoginDialog() {
            LoginDialog login = new LoginDialog();
            Optional<Pair<String, String>> result = login.getResult();
            // TODO finish here
        }
    }
    

    Because textFx provides own stage where you should inject your scene manually do the next:

    public class TestFxBase extends ApplicationTest {
        @Override
        public void start(Stage stage) {
            stage.setScene(new Scene(new MainPane(stage)));
            stage.show();
        }
    }
    

    Finally improve a bit the test:

    public class MainAppFIT extends TestFxBase {
        @Test
        public void verifyLogin() {
            // given started application and opened login dialog
            sleep(500);
    
            // when
            write("HelloWorld");
            type(TAB);
            write("password");
            clickOn("#loginButton");
    
            // then
            // TODO finish here with verification of actual result
        }
    }
    

    As result I am able to see how does textfx robot click/type as defined in test steps.

    BTW, `MainAppFIT' - FIT means functional integration test.