edvin / tornadofx-guide

TornadoFX Guide
133 stars 67 forks source link

Can't open wizard from TestFX #104

Closed Bendat closed 5 years ago

Bendat commented 5 years ago

Hi,

I'm trying to write a test for my tornadofx application using TestFx. I'm trying to open a wizard by pressing a button, however I'm receiving the following exception:

--- Exception in Async Thread ---
kotlin.KotlinNullPointerException: null
    tornadofx.FX$Companion.getPrimaryStage(FX.kt:116)
    tornadofx.UIComponent.getCurrentWindow(Component.kt:351)
    tornadofx.UIComponent.openWindow$default(Component.kt:697)
    a.b.c.ProcessesView$root$1$1$1$1$1$1$1.invoke(ProcessesView.kt:34)
    a.b.c.gui.views.ProcessesView$root$1$1$1$1$1$1$1.invoke(ProcessesView.kt:17)
    tornadofx.Component.find(Component.kt:100)
    tornadofx.Component.find$default(Component.kt:100)
    a.b.c.ProcessesView$root$1$1$1$1$1$1.run(ProcessesView.kt:30)
    com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
    java.security.AccessController.doPrivileged(Native Method)
    com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
    com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)

The test is setup up as follows:

class ExploratoryProcessesViewAutomationTest : ApplicationTest(), Loggable {
    lateinit var stage: Stage
    val manager = ProcessManager.FXProcessManager()
    lateinit var list: ProcessesView

    override fun init() {
        FxToolkit.registerPrimaryStage()
    }

    override fun start(stage: Stage) {
        super.start(stage)
        stage.initStyle(StageStyle.UNIFIED)
        list = ProcessesView(manager)
        stage.scene = Scene(list.root)
        stage.show()
    }

    override fun stop() {
        FxToolkit.hideStage()
    }

    // Tests
}

As an aside, onDock is not called either. It can be run on it's own using the intellij plugin just fine.

I'd appreciate any guidance

ahinchman1 commented 5 years ago

Inside of overrride fun init() {...} I would try a @Before annotation for your setup and and @After for the teardown.

Your message is saying that the stage it's trying to get in registerPrimaryStage is not initialized. This means that the view was probably not added to the stage. For example:

class Test: ApplicationTest() {

    lateinit var primaryStage: Stage
    val view = TestView()

    @Before
    fun setupApplication() {
        primaryStage = FxToolkit.registerPrimaryStage()
        val fragment = find<Editor>(CatScheduleScope())

        view.root.add(fragment.root)

        // Stage objects must be constructed and modified on the JavaFX App Thread
        interact {
            primaryStage.scene = Scene(view.root)
            primaryStage.show()
            primaryStage.toFront()
        }
    }
    ...  
} 

Feel free to refer to this blurb on TestFX and TornadoFX for referencing! I know I had a tough time with this starting out and it feels like the set up for everyone is different.

Bendat commented 5 years ago

H and Thanks @ahinchman1 however this solution didn't work for me.

What did work was to wrap components being tested in an App type. The Install Wizard looks like

class InstallWizard : Wizard() {
    override val canFinish = allPagesComplete
    override val canGoNext = currentPageComplete

    private val controller = InstallWizardController()
    val model by inject<InstallWizardViewModel>(params = mapOf("controller" to controller))

    init {
        add(RegistrationPage::class)
        add(SetupPage::class)
    }
}

Next I created a wrapper for it:

class WizardApp : App(InstallWizard::class) {
    val root by inject<InstallWizard>()
}

Then in TestFX I load this app with setupFixture as follows (Groovy, Spock)

lass SetupPageSpec extends ApplicationSpec {
    WizardApp app = null
    Stage stage
    Button finish
    Button next
    String password
    File testDir = new File(new File(System.getProperty("user.home")), "sample-test")
    private def faker = new Faker()

    @Override
    void start(Stage stage) {
        app = new WizardApp()
        FxToolkit.registerPrimaryStage()
        FxToolkit.setupFixture {
            this.stage = new Stage(StageStyle.UNIFIED)
            app.start(stage)
            stage.show()
        }
    }

Which allows dependency injection to work, but loses the ability to run a single test across multiple methods (the fixture is torn down after each method this way)