1. WorkbenchTest.java

1.1. Open Modules

  @Test
  void openModule() {
    robot.interact(() -> {
      // Open first
      workbench.openModule(first);
      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(1, workbench.getOpenModules().size());
      InOrder inOrder = inOrder(first);
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Open last
      workbench.openModule(last);
      assertSame(last, workbench.getActiveModule());
      assertSame(moduleNodes[LAST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());
      inOrder = inOrder(first, last);
      inOrder.verify(first).deactivate();
      inOrder.verify(last).init(workbench);
      inOrder.verify(last).activate();
      // Open last again
      workbench.openModule(last);
      assertSame(last, workbench.getActiveModule());
      assertSame(moduleNodes[LAST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());
      verify(last, times(1)).init(workbench);
      verify(last, times(1)).activate();
      verify(last, never()).deactivate();
      // Open first (already initialized)
      workbench.openModule(first);
      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());
      verify(first, times(1)).init(workbench); // no additional init on first
      verify(last, times(1)).init(workbench); // no additional init on last
      inOrder = inOrder(first, last);
      inOrder.verify(last).deactivate();
      inOrder.verify(first).activate();
      verify(first, times(2)).activate();
      // Switch to home screen
      workbench.openAddModulePage();
      assertSame(null, workbench.getActiveModule());
      assertSame(null, workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());
      verify(first, times(1)).init(workbench); // no additional init on first
      verify(last, times(1)).init(workbench); // no additional init on last
      verify(first, times(2)).deactivate();
      // Open second
      workbench.openModule(second);
      assertSame(second, workbench.getActiveModule());
      assertSame(moduleNodes[SECOND_INDEX], workbench.getActiveModuleView());
      assertEquals(3, workbench.getOpenModules().size());
      inOrder = inOrder(second);
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();

      ignoreModuleGetters(first, second, last);
      verifyNoMoreInteractions(first, second, last);
    });
  }

  @Test
  void openModuleInvalid() {
    /* Test if opening a module which has not been passed in the constructor of WorkbenchFxModel
    throws an exception */
    robot.interact(() -> {
      assertThrows(IllegalArgumentException.class,
          () -> workbench.openModule(mock(WorkbenchModule.class)));
    });
  }

1.2. Close Modules

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModuleOne() {
    // open and close module
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.closeModule(first);

      assertSame(null, workbench.getActiveModule());
      assertSame(null, workbench.getActiveModuleView());
      assertEquals(0, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.closeModule(first)
      inOrder.verify(first).deactivate();
      inOrder.verify(first).destroy();

      ignoreModuleGetters(first);
      verifyNoMoreInteractions(first);
    });
  }

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModuleLeft1() {
    robot.interact(() -> {
      // open two modules, close left module
      // right active
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.closeModule(first);

      assertSame(second, workbench.getActiveModule());
      assertSame(moduleNodes[SECOND_INDEX], workbench.getActiveModuleView());
      assertEquals(1, workbench.getOpenModules().size());
      verify(second, never()).deactivate();

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(first)
      inOrder.verify(first, never()).deactivate();
      inOrder.verify(first).destroy();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModuleLeft2() {
    robot.interact(() -> {
      // open two modules, close left module
      // left active
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.openModule(first);
      workbench.closeModule(first);

      assertSame(second, workbench.getActiveModule());
      assertSame(moduleNodes[SECOND_INDEX], workbench.getActiveModuleView());
      assertEquals(1, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.openModule(first)
      inOrder.verify(second).deactivate();
      inOrder.verify(first).activate();
      // Call: workbench.closeModule(first)
      inOrder.verify(first).deactivate();
      inOrder.verify(first).destroy();
      inOrder.verify(second).activate();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModuleRight1() {
    // open two modules, close right module
    // right active
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.closeModule(second);

      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(1, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(second)
      inOrder.verify(second).deactivate();
      inOrder.verify(second).destroy();
      inOrder.verify(first).activate();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModuleRight2() {
    // open two modules, close right module
    // left active
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.openModule(first);
      workbench.closeModule(second);

      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(1, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.openModule(first)
      inOrder.verify(second).deactivate();
      inOrder.verify(first).activate();
      // Call: workbench.closeModule(second)
      inOrder.verify(second, never()).deactivate();
      inOrder.verify(second).destroy();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModuleMiddleActive() {
    // open three modules and close middle module
    // middle active
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.openModule(last);
      workbench.openModule(second);
      workbench.closeModule(second);

      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second, last);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.openModule(last)
      inOrder.verify(second).deactivate();
      inOrder.verify(last).init(workbench);
      inOrder.verify(last).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(last).deactivate();
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(second)
      inOrder.verify(second).deactivate();
      inOrder.verify(second).destroy();
      inOrder.verify(first).activate();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

1.3. Close Modules Interrupted

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModulePreventDestroyActive() {
    // open two modules, close second (active) module
    // destroy() on second module will return false, so the module shouldn't get closed
    when(second.destroy()).thenReturn(false);
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.closeModule(second);

      assertSame(second, workbench.getActiveModule());
      assertSame(moduleNodes[SECOND_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(second)
      // destroy second
      inOrder.verify(second).deactivate();
      inOrder.verify(second).destroy();
      // notice destroy() was unsuccessful, keep focus on second
      inOrder.verify(second).activate();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModulePreventDestroyInactive() {
    // open two modules, close first (inactive) module
    // destroy() on first module will return false, so the module shouldn't get closed
    when(first.destroy()).thenReturn(false);
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.closeModule(first);

      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(second)
      // destroy second
      inOrder.verify(first, never()).deactivate();
      inOrder.verify(first).destroy();
      // notice destroy() was unsuccessful, switch focus to first
      inOrder.verify(second).deactivate();
      inOrder.verify(first).activate();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Example of what happens in case of a closing dialog in the destroy() method of a module with
   * the user confirming the module should get closed. Precondition: openModule tests pass.
   */
  @Test
  void closeModuleDestroyInactiveDialogClose() {
    // open two modules, close first (inactive) module
    // destroy() on first module will return false, so the module shouldn't get closed
    when(first.destroy()).then(invocation -> {
      // dialog opens
      return false;
    });
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.closeModule(first);
      // user confirms yes on dialog: WorkbenchModule#close()
      simulateModuleClose(first);

      assertSame(second, workbench.getActiveModule());
      assertSame(moduleNodes[SECOND_INDEX], workbench.getActiveModuleView());
      assertEquals(1, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(first)
      // attempt to destroy first
      inOrder.verify(first).destroy();
      // destroy() returns false, closeModule() opens first module
      inOrder.verify(second).deactivate();
      inOrder.verify(first).activate();
      // WorkbenchModule#close(), switch to second
      inOrder.verify(first).deactivate();
      inOrder.verify(second).activate();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Internal testing utility method.
   * Ignores calls to the getters of {@link WorkbenchModule}, which enables to safely call
   * {@link Mockito#verifyNoMoreInteractions} during lifecycle order verification tests without
   * having to make assumptions about how many times the getters have been called as well.
   */
  private void ignoreModuleGetters(WorkbenchModule... modules) {
    for (WorkbenchModule module : modules) {
      verify(module, atLeast(0)).getIcon();
      verify(module, atLeast(0)).getName();
      verify(module, atLeast(0)).getWorkbench();
      verify(module, atLeast(0)).getToolbarControlsLeft();
      verify(module, atLeast(0)).getToolbarControlsRight();
    }
  }

  /**
   * Internal testing method which simulates a call to {@link WorkbenchModule#close()}.
   */
  private void simulateModuleClose(WorkbenchModule module) {
    workbench.completeModuleCloseable(module);
  }

  /**
   * Example of what happens in case of a closing dialog in the destroy() method of a module with
   * the user confirming the module should NOT get closed. Precondition: openModule tests pass.
   */
  @Test
  void closeModulePreventDestroyInactiveDialogClose() {
    // open two modules, close first (inactive) module
    // destroy() on first module will return false, so the module shouldn't get closed
    when(first.destroy()).then(invocation -> {
      // dialog opens, user confirms NOT closing module
      return false;
    });

    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.closeModule(first);

      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(first)
      // attempt to destroy first
      inOrder.verify(first, never()).deactivate();
      inOrder.verify(first).destroy();
      // destroy() returns false, switch open module to first
      inOrder.verify(second).deactivate();
      inOrder.verify(first).activate();
      // destroy() returns false, first stays the active module
      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  @Test
  void closeModuleInvalid() {
    robot.interact(() -> {
      // Test for null
      assertThrows(NullPointerException.class, () -> workbench.closeModule(null));
      // Test if closing a module not included in the modules at all throws an exception
      assertThrows(IllegalArgumentException.class,
          () -> workbench.closeModule(mock(WorkbenchModule.class)));
      // Test if closing a module not opened throws an exception
      assertThrows(IllegalArgumentException.class, () -> workbench.closeModule(mockModules[0]));
    });
  }

  @Test
  void closeInactiveModule() {
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.openModule(last);
      workbench.closeModule(second);

      assertSame(last, workbench.getActiveModule());
      assertSame(moduleNodes[LAST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second, last);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.openModule(last)
      inOrder.verify(second).deactivate();
      inOrder.verify(last).init(workbench);
      inOrder.verify(last).activate();
      // Call: workbench.closeModule(second)
      inOrder.verify(second, never()).deactivate();
      inOrder.verify(second).destroy();
      inOrder.verify(last).getWorkbench();
      inOrder.verify(last).getName();
      inOrder.verify(last).getIcon();

      ignoreModuleGetters(first, second, last);
      verifyNoMoreInteractions(first, second, last);
    });
  }

1.4. Close Stage Interrupted

  /**
   * Test for {@link Workbench#setupCleanup()}.
   * Simulates all modules returning {@code true} when
   * {@link WorkbenchModule#destroy()} is being called on them during the cleanup.
   */
  @Test
  void closeStageSuccess() {
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);

      // simulate closing of the stage by pressing the X of the application
      closeStage();

      // all open modules should get closed before the application ends
      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();

      // Effects caused by "Workbench#setupCleanup" -> setOnCloseRequest
      // Implicit Call: workbench.closeModule(first)
      inOrder.verify(first, never()).deactivate();
      inOrder.verify(first).destroy();
      // Implicit Call: workbench.closeModule(second)
      inOrder.verify(second).deactivate();
      inOrder.verify(second).destroy();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);

      assertEquals(0, workbench.getOpenModules().size());
    });
  }

  /**
   * Test for {@link Workbench#setupCleanup()}.
   * Simulates the first (inactive) module returning {@code false} and the second (active) module
   * returning {@code true}, when {@link WorkbenchModule#destroy()} is being called
   * on them during cleanup.
   */
  @Test
  void closeStageFailFirstModule() {
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);

      // make sure closing of the stage gets interrupted, if destroy returns false on a module
      when(first.destroy()).thenReturn(false);

      // simulate closing of the stage like when pressing the X of the application
      closeStage();

      // all open modules should get closed before the application ends
      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();

      // Effects caused by "Workbench#setupCleanup" -> setOnCloseRequest
      // Implicit Call: workbench.closeModule(first)
      inOrder.verify(first, never()).deactivate();
      inOrder.verify(first).destroy(); // returns false
      // Implicit Call: workbench.openModule(first) -> set focus on module that couldn't be closed
      inOrder.verify(second).deactivate();
      inOrder.verify(first).activate();
      // closing should be interrupted

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);

      assertEquals(2, workbench.getOpenModules().size());
    });
  }

  /**
   * Test for {@link Workbench#setupCleanup()}.
   * Simulates the first (inactive) module returning {@code true} and the second (active) module
   * returning {@code false}, when {@link WorkbenchModule#destroy()} is being called on them during
   * cleanup.
   */
  @Test
  void closeStageFailSecondModule() {
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);

      // make sure closing of the stage gets interrupted, if destroy returns false on a module
      when(second.destroy()).thenReturn(false);

      // simulate closing of the stage by pressing the X of the application
      closeStage();

      // all open modules should get closed before the application ends
      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();

      // Effects caused by "Workbench#setupCleanup" -> setOnCloseRequest
      // Implicit Call: workbench.closeModule(first)
      inOrder.verify(first, never()).deactivate();
      inOrder.verify(first).destroy(); // returns true
      // Implicit Call: workbench.closeModule(second)
      inOrder.verify(second).deactivate();
      inOrder.verify(second).destroy(); // returns false
      // second should stay as the active module
      inOrder.verify(second).activate();
      // closing should be interrupted

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);

      assertEquals(1, workbench.getOpenModules().size());
      assertEquals(second, workbench.getOpenModules().get(0));
    });
  }

  /**
   * Test for {@link Workbench#setupCleanup()}.
   * Simulates a special case that caused in bug scenarios to have 2x {@code thenRun} set on
   * {@code moduleCloseable} in {@code stage.setOnCloseRequest}, which lead to 2 dialogs being open
   * instead of one, after the first module has been closed.
   */
  @Test
  void closeStageSpecial1() {
    robot.interact(() -> {
      // Given: 2 Modules open, destroy() on both opens a dialog and returns false.
      //        Pressing yes on the dialog calls WorkbenchModule#close(), pressing no leaves the
      //        module open.
      workbench.openModule(first);
      workbench.openModule(second);
      when(first.destroy()).then(invocationOnMock -> {
        workbench.showDialog(WorkbenchDialog.builder("1", "", WorkbenchDialog.Type.CONFIRMATION)
            .blocking(true).onResult(buttonType -> {
              if (ButtonType.YES.equals(buttonType)) {
                simulateModuleClose(first);
              }
            }).build());
        return false;
      });
      when(second.destroy()).then(invocationOnMock -> {
        workbench.showDialog(WorkbenchDialog.builder("2", "", WorkbenchDialog.Type.CONFIRMATION)
            .blocking(true).onResult(buttonType -> {
              if (ButtonType.YES.equals(buttonType)) {
                simulateModuleClose(second);
              }
            }).build());
        return false;
      });
      assertTrue(isStageOpen());

      // When: Close stage, press No, Close Stage, press yes.
      closeStage();
      assertSame(1, workbench.getBlockingOverlaysShown().size());
      assertSame(2, workbench.getOpenModules().size());

      simulateDialogButtonClick(ButtonType.NO);
      assertSame(0, workbench.getBlockingOverlaysShown().size());
      assertSame(2, workbench.getOpenModules().size());

      closeStage();
      assertSame(1, workbench.getBlockingOverlaysShown().size());
      assertSame(2, workbench.getOpenModules().size());

      simulateDialogButtonClick(ButtonType.YES);
      assertSame(1, workbench.getBlockingOverlaysShown().size());
      assertSame(1, workbench.getOpenModules().size());

      // Then: Only second module is open and 1 dialog is open (closing of second module)
      assertEquals(second, workbench.getOpenModules().get(0));
      assertEquals("2", getShowingDialogControl().getDialog().getTitle());

      // When: Press yes
      simulateDialogButtonClick(ButtonType.YES);

      // Then: No modules and dialogs are open, stage is closed.
      assertSame(0, workbench.getBlockingOverlaysShown().size());
      assertSame(0, workbench.getOpenModules().size());
      assertFalse(isStageOpen());
    });
  }

  /**
   * Test for {@link Workbench#setupCleanup()}.
   * Simulates a special case that caused in bug scenarios for the stage closing process to go on,
   * even though a tab was closed and not the stage itself.
   */
  @Test
  void closeStageSpecial2() {
    robot.interact(() -> {
      // Given: 2 Modules open, destroy() on both opens a dialog and returns false.
      //        Pressing yes on the dialog calls WorkbenchModule#close(), pressing no leaves the
      //        module open.
      workbench.openModule(first);
      workbench.openModule(second);
      when(first.destroy()).then(invocationOnMock -> {
        workbench.showDialog(WorkbenchDialog.builder("1", "", WorkbenchDialog.Type.CONFIRMATION)
            .blocking(true).onResult(buttonType -> {
              if (ButtonType.YES.equals(buttonType)) {
                simulateModuleClose(first);
              }
            }).build());
        return false;
      });
      when(second.destroy()).then(invocationOnMock -> {
        workbench.showDialog(WorkbenchDialog.builder("2", "", WorkbenchDialog.Type.CONFIRMATION)
            .blocking(true).onResult(buttonType -> {
              if (ButtonType.YES.equals(buttonType)) {
                simulateModuleClose(second);
              }
            }).build());
        return false;
      });
      assertTrue(isStageOpen());

      // When: Close stage, press No, Close Tab, Press Yes
      closeStage();
      assertSame(1, workbench.getBlockingOverlaysShown().size());
      assertSame(2, workbench.getOpenModules().size());

      simulateDialogButtonClick(ButtonType.NO);
      assertSame(0, workbench.getBlockingOverlaysShown().size());
      assertSame(2, workbench.getOpenModules().size());

      workbench.closeModule(first); // simulate tab closing
      assertSame(1, workbench.getBlockingOverlaysShown().size());
      assertSame(2, workbench.getOpenModules().size());

      simulateDialogButtonClick(ButtonType.YES);
      assertSame(0, workbench.getBlockingOverlaysShown().size());
      assertSame(1, workbench.getOpenModules().size());

      // Then: Only second module is open and no dialogs are open (stage closing is interrupted)
      assertEquals(second, workbench.getOpenModules().get(0));
      assertTrue(isStageOpen());
    });
  }

  /**
   * Internal utility method for testing.
   * Determines whether the current stage is open or was closed.
   */
  private boolean isStageOpen() {
    return robot.listTargetWindows().size() == 1;
  }

  /**
   * Internal utility method for testing.
   * Simulates closing the stage, which fires a close request to test logic
   * inside of {@link Stage#setOnCloseRequest(EventHandler)}.
   * Using {@link FxRobot#closeCurrentWindow()} would be better, but it only works on Windows
   * because of its implementation, so this approach was chosen as a workaround.
   * @see <a href="https://github.com/TestFX/TestFX/issues/447">
   * closeCurrentWindow() doesn't work headless</a>
   */
  private void closeStage() {
    Stage stage = ((Stage) workbench.getScene().getWindow());
    stage.fireEvent(
        new WindowEvent(
            stage,
            WindowEvent.WINDOW_CLOSE_REQUEST
        )
    );
  }