package rmblworx.tools.timey.gui; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.isA; import static org.mockito.Matchers.isNull; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.awt.TrayIcon; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.TableView; import javafx.scene.control.TextField; import javafx.scene.input.MouseButton; import org.junit.Before; import org.junit.Test; import org.junit.experimental.categories.Category; import org.loadui.testfx.categories.TestFX; import org.loadui.testfx.utils.FXTestUtils; import org.mockito.ArgumentMatcher; import rmblworx.tools.timey.ITimey; import rmblworx.tools.timey.event.AlarmExpiredEvent; import rmblworx.tools.timey.event.TimeyEvent; import rmblworx.tools.timey.vo.AlarmDescriptor; /* * Copyright 2014-2015 Christian Raue * MIT License http://opensource.org/licenses/mit-license.php */ /** * GUI-Tests für die Alarm-Funktionalität. * @author Christian Raue {@literal <christian.raue@gmail.com>} */ @Category(TestFX.class) public class AlarmControllerTest extends FxmlGuiControllerTest { /** * Container für Elemente. */ private Scene scene; // GUI-Elemente private TableView<Alarm> alarmTable; /** * {@inheritDoc} */ @Override protected final String getFxmlFilename() { return "Alarm.fxml"; } @Before @SuppressWarnings("unchecked") public final void setUp() { scene = stage.getScene(); alarmTable = (TableView<Alarm>) scene.lookup("#alarmTable"); // Tabelle leeren alarmTable.getItems().clear(); } @Test public final void testInitializedFields() throws IllegalAccessException { super.testFxmlInitializedFields(); } /** * Testet das Löschen einzelner Alarme. */ @Test public final void testDeleteAlarm() { final ITimey facade = getController().getGuiHelper().getFacade(); // zwei Alarme anlegen final ObservableList<Alarm> tableData = alarmTable.getItems(); final LocalDateTime now = LocalDateTime.now().withNano(0); final Alarm alarm1 = new Alarm(now.plusSeconds(5), "alarm1"); final Alarm alarm2 = new Alarm(now.plusSeconds(10), "alarm2"); tableData.add(alarm1); tableData.add(alarm2); final Button alarmDeleteButton = (Button) scene.lookup("#alarmDeleteButton"); // Zustand der Schaltflächen testen assertTrue(alarmDeleteButton.isVisible()); assertTrue(alarmDeleteButton.isDisabled()); // zweiten Alarm auswählen Platform.runLater(new Runnable() { public void run() { alarmTable.getSelectionModel().select(alarm2); } }); FXTestUtils.awaitEvents(); // Zustand der Schaltflächen testen assertTrue(alarmDeleteButton.isVisible()); assertFalse(alarmDeleteButton.isDisabled()); // Alarm löschen click(alarmDeleteButton); waitForThreads(); // sicherstellen, dass Alarm gelöscht wurde verify(facade, timeout(WAIT_FOR_EVENT)).removeAlarm(argThat(new ArgumentMatcher<AlarmDescriptor>() { public boolean matches(final Object argument) { return ((AlarmDescriptor) argument).getAlarmtime().getMilliSeconds() == alarm2.getDateTimeInMillis(); } })); // sicherstellen, dass zweiter Alarm gelöscht ist, erster aber nicht assertTrue(tableData.contains(alarm1)); assertFalse(tableData.contains(alarm2)); // sicherstellen, dass erster Alarm ausgewählt ist assertSame(alarm1, alarmTable.getSelectionModel().getSelectedItem()); // Alarm löschen click(alarmDeleteButton); waitForThreads(); // sicherstellen, dass zweiter Alarm auch gelöscht wurde verify(facade, timeout(WAIT_FOR_EVENT)).removeAlarm(argThat(new ArgumentMatcher<AlarmDescriptor>() { public boolean matches(final Object argument) { return ((AlarmDescriptor) argument).getAlarmtime().getMilliSeconds() == alarm1.getDateTimeInMillis(); } })); // sicherstellen, dass keine Alarme mehr existieren assertTrue(tableData.isEmpty()); // sicherstellen, dass kein Alarm mehr ausgewählt ist assertNull(alarmTable.getSelectionModel().getSelectedItem()); // Zustand der Schaltflächen testen assertTrue(alarmDeleteButton.isVisible()); assertTrue(alarmDeleteButton.isDisabled()); } /** * Testet das Löschen aller Alarme. */ @Test public final void testDeleteAllAlarms() { final ITimey facade = getController().getGuiHelper().getFacade(); // zwei Alarme anlegen final ObservableList<Alarm> tableData = alarmTable.getItems(); final LocalDateTime now = LocalDateTime.now().withNano(0); final Alarm alarm1 = new Alarm(now.plusSeconds(5), "alarm1"); final Alarm alarm2 = new Alarm(now.plusSeconds(10), "alarm2"); tableData.add(alarm1); tableData.add(alarm2); FXTestUtils.awaitEvents(); final int alarmsCount = tableData.size(); final Button alarmDeleteAllButton = (Button) scene.lookup("#alarmDeleteAllButton"); final Button alarmDeleteButton = (Button) scene.lookup("#alarmDeleteButton"); // Zustand der Schaltflächen testen assertTrue(alarmDeleteAllButton.isVisible()); assertFalse(alarmDeleteAllButton.isDisabled()); // Schaltfläche nur kurz anklicken click(alarmDeleteAllButton); waitForThreads(); // sicherstellen, dass kein Alarm gelöscht wurde verify(facade, never()).removeAlarm(isA(AlarmDescriptor.class)); assertTrue(tableData.contains(alarm1)); assertTrue(tableData.contains(alarm2)); // Schaltfläche lang genug drücken, aber Maus wegbewegen move(alarmDeleteAllButton); press(MouseButton.PRIMARY); sleep(AlarmController.TIME_TO_PRESS_DELETE_ALL_BUTTON); move(alarmDeleteButton); release(MouseButton.PRIMARY); waitForThreads(); // sicherstellen, dass kein Alarm gelöscht wurde verify(facade, never()).removeAlarm(isA(AlarmDescriptor.class)); assertTrue(tableData.contains(alarm1)); assertTrue(tableData.contains(alarm2)); // Schaltfläche lang genug anklicken, um Löschvorgang auslösen zu können move(alarmDeleteAllButton); press(MouseButton.PRIMARY); sleep(AlarmController.TIME_TO_PRESS_DELETE_ALL_BUTTON); release(MouseButton.PRIMARY); waitForThreads(); // sicherstellen, dass alle Alarme gelöscht wurden verify(facade, timeout(WAIT_FOR_EVENT).times(alarmsCount)).removeAlarm(isA(AlarmDescriptor.class)); assertFalse(tableData.contains(alarm1)); assertFalse(tableData.contains(alarm2)); // Zustand der Schaltflächen testen assertTrue(alarmDeleteAllButton.isVisible()); assertTrue(alarmDeleteAllButton.isDisabled()); } /** * Testet die Darstellung von Alarmen in der Tabelle. */ @Test public final void testAlarmTableRendering() { // Alarm anlegen final ObservableList<Alarm> tableData = alarmTable.getItems(); final Alarm alarm = new Alarm(DateTimeUtil.getLocalDateTimeForString("24.12.2014 12:00:00"), "Test"); tableData.add(alarm); /* * Sicherstellen, dass Zellen die korrekten Objekte enthalten. * Wäre z. B. nicht der Fall, wenn der Name des Alarm-Attributs nicht mit dem Namen der Spalte übereinstimmt. */ final Boolean enabledCellData = (Boolean) alarmTable.getColumns().get(0).getCellData(0); assertEquals(alarm.isEnabled(), enabledCellData); final LocalDateTime dateTimeCellData = (LocalDateTime) alarmTable.getColumns().get(1).getCellData(0); assertNotNull(dateTimeCellData); assertEquals(alarm.getDateTime(), dateTimeCellData); final String descriptionCellData = (String) alarmTable.getColumns().get(2).getCellData(0); assertNotNull(descriptionCellData); assertEquals(alarm.getDescription(), descriptionCellData); } /** * Testet das Bearbeiten eines Alarms. */ @Test public final void testEditAlarm() { final int noonHour = 12; final int onePmHour = 13; // Alarm anlegen final LocalDateTime noon = LocalDateTime.now() .plusYears(1) // +1 Jahr, um Problem mit Zeitpunkt in Vergangenheit zu vermeiden, falls Test nachmittags läuft .withHour(noonHour).withMinute(0).withSecond(0) // 12:00:00 .withNano(0); // Sekundenbruchteil immer auf 0 setzen, da auch per GUI nur ganze Sekunden angegeben werden können final LocalDateTime onePm = noon.withHour(onePmHour); // 13:00:00 am selben Tag final Alarm alarm = new Alarm(noon, "alarm"); alarmTable.getItems().add(alarm); final Button alarmEditButton = (Button) scene.lookup("#alarmEditButton"); // Zustand der Schaltflächen testen assertTrue(alarmEditButton.isVisible()); assertTrue(alarmEditButton.isDisabled()); // Alarm auswählen Platform.runLater(new Runnable() { public void run() { alarmTable.getSelectionModel().select(alarm); } }); FXTestUtils.awaitEvents(); // Zustand der Schaltflächen testen assertTrue(alarmEditButton.isVisible()); assertFalse(alarmEditButton.isDisabled()); // Bearbeiten-Dialog öffnen click(alarmEditButton); waitForThreads(); final Scene dialogScene = ((AlarmController) getController()).getDialogStage().getScene(); // Alarm deaktivieren final CheckBox alarmEnabledCheckbox = (CheckBox) dialogScene.lookup("#alarmEnabledCheckbox"); click(alarmEnabledCheckbox); waitForThreads(); final TextField hoursTextField = (TextField) dialogScene.lookup("#hoursTextField"); /* * Der Versuch, den neuen Wert ins Textfeld per doubleClick(hoursTextField); type(String.valueOf(onePmHour)); einzugeben, würde auf * Travis scheitern und der Feldinhalt würde sich nicht ändern. * Selbst Fokussieren des Feldes per Platform.runLater(... hoursTextField.requestFocus(); ...) würde nicht funktionieren. * Also muss der Wert direkt gesetzt (oder alternativ per Slider geändert) werden. */ Platform.runLater(new Runnable() { public void run() { hoursTextField.setText(String.valueOf(onePmHour)); } }); FXTestUtils.awaitEvents(); // sicherstellen, dass der Wert wirklich geändert wurde assertEquals(String.valueOf(onePmHour), hoursTextField.getText()); final Button alarmSaveButton = (Button) dialogScene.lookup("#alarmSaveButton"); click(alarmSaveButton); waitForThreads(); // sicherstellen, dass Alarm geändert wurde assertFalse(alarm.isEnabled()); assertEquals(onePm, alarm.getDateTime()); // sicherstellen, dass per Fassade alter Alarm gelöscht und neuer angelegt wurde final ITimey facade = getController().getGuiHelper().getFacade(); verify(facade, timeout(WAIT_FOR_EVENT)).removeAlarm(argThat(new ArgumentMatcher<AlarmDescriptor>() { public boolean matches(final Object argument) { return ((AlarmDescriptor) argument).getAlarmtime().getMilliSeconds() == DateTimeUtil.getLocalDateTimeInMillis(noon); } })); verify(facade, timeout(WAIT_FOR_EVENT)).setAlarm(argThat(new ArgumentMatcher<AlarmDescriptor>() { public boolean matches(final Object argument) { return ((AlarmDescriptor) argument).getAlarmtime().getMilliSeconds() == DateTimeUtil.getLocalDateTimeInMillis(onePm); } })); /* * Sicherstellen, dass geänderter Alarm in der Tabelle korrekt angezeigt wird. * Wäre z. B. nicht der Fall, wenn {@code Alarm}-Klasse keine "<Attribut>Property"-Methoden hätte, * siehe http://stackoverflow.com/questions/11065140/javafx-2-1-tableview-refresh-items/24194842#24194842. */ final LocalDateTime dateTimeCellData = (LocalDateTime) alarmTable.getColumns().get(1).getCellData(0); assertNotNull(dateTimeCellData); assertEquals(onePm, dateTimeCellData); // sicherstellen, dass Alarm noch ausgewählt ist assertEquals(alarm, alarmTable.getSelectionModel().getSelectedItem()); // Zustand der Schaltflächen testen assertTrue(alarmEditButton.isVisible()); assertFalse(alarmEditButton.isDisabled()); } /** * Testet, ob die Auswahl eines Alarms erhalten bleibt, wenn sich dessen Position in der Tabelle durch Bearbeiten des Zeitpunktes * verschiebt. */ @Test public final void testKeepAlarmSelectionOnPositionChange() { // Alarme anlegen final ObservableList<Alarm> tableData = alarmTable.getItems(); final LocalDateTime now = LocalDateTime.now().withNano(0); final Alarm selectedAlarm = new Alarm(now.minusYears(1), "ausgewählter Alarm"); final Alarm otherAlarm = new Alarm(now, "anderer Alarm"); tableData.addAll(selectedAlarm, otherAlarm); // einen Alarm auswählen Platform.runLater(new Runnable() { public void run() { alarmTable.getSelectionModel().select(selectedAlarm); } }); FXTestUtils.awaitEvents(); // Alarmzeitpunkt ändern, sodass sich Alarm innerhalb der Tabelle verschiebt Platform.runLater(new Runnable() { public void run() { selectedAlarm.setDateTime(selectedAlarm.getDateTime().plusYears(2)); final AlarmController controller = (AlarmController) getController(); // private Methode controller.refreshTable() aufrufen try { @SuppressWarnings("unchecked") final Class<AlarmController> klass = (Class<AlarmController>) controller.getClass(); final Method refreshTableMethod = klass.getDeclaredMethod("refreshTable"); refreshTableMethod.setAccessible(true); refreshTableMethod.invoke(controller); } catch (final NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { fail(e.getLocalizedMessage()); } } }); FXTestUtils.awaitEvents(); // sicherstellen, dass Auswahl erhalten bleibt assertEquals(1, alarmTable.getSelectionModel().getSelectedIndex()); } /** * Testet, ob die Auswahl eines Alarms erhalten bleibt, wenn nach Eintritt eines Ereignisses die Tabelle neu geladen wird. */ @Test public final void testKeepAlarmSelectionWhileReloadOnEvent() { final AlarmController controller = (AlarmController) getController(); final GuiHelper guiHelper = controller.getGuiHelper(); final ITimey facade = guiHelper.getFacade(); // Alarme anlegen final ObservableList<Alarm> tableData = alarmTable.getItems(); final LocalDateTime now = LocalDateTime.now().withNano(0); final Alarm otherDisabledAlarm = new Alarm(now.minusYears(1), "alarm0", "", false); final Alarm selectedAlarm = new Alarm(now, "alarm1"); final Alarm otherEnabledAlarm = new Alarm(now.plusYears(1), "alarm2"); tableData.addAll(otherDisabledAlarm, selectedAlarm, otherEnabledAlarm); // einen Alarm auswählen Platform.runLater(new Runnable() { public void run() { alarmTable.getSelectionModel().select(selectedAlarm); } }); FXTestUtils.awaitEvents(); // sicherstellen, dass Alarm ausgewählt ist assertEquals(1, alarmTable.getSelectionModel().getSelectedIndex()); // Neuladen der Alarme simulieren, dabei den ausgewählten als deaktiviert markieren final List<AlarmDescriptor> newAlarms = new ArrayList<>(); for (final Alarm alarm : tableData) { Alarm newAlarm; if (alarm == selectedAlarm) { newAlarm = new Alarm(alarm.getDateTime(), alarm.getDescription() + " neu", "", false); } else { newAlarm = alarm; } newAlarms.add(AlarmDescriptorConverter.getAsAlarmDescriptor(newAlarm)); } when(facade.getAllAlarms()).thenReturn(newAlarms); // Ereignis auslösen guiHelper.getMessageHelper().setSuppressMessages(true); controller.handleEvent(new AlarmExpiredEvent(AlarmDescriptorConverter.getAsAlarmDescriptor(selectedAlarm))); waitForThreads(); verify(facade, atLeastOnce()).getAllAlarms(); // sicherstellen, dass Auswahl erhalten bleibt assertEquals(1, alarmTable.getSelectionModel().getSelectedIndex()); } /** * Testet die Verarbeitung eines Ereignisses. */ @Test public final void testHandleEvent() { final AlarmController controller = (AlarmController) getController(); final GuiHelper guiHelper = controller.getGuiHelper(); final Alarm alarm = new Alarm(LocalDateTime.now(), "alarm", "sound", true); final MessageHelper messageHelper = mock(MessageHelper.class); guiHelper.setMessageHelper(messageHelper); final AudioPlayer audioPlayer = mock(AudioPlayer.class); guiHelper.setAudioPlayer(audioPlayer); // Ereignis auslösen controller.handleEvent(new AlarmExpiredEvent(AlarmDescriptorConverter.getAsAlarmDescriptor(alarm))); waitForThreads(); // sicherstellen, dass Ereignis verarbeitet wird verify(messageHelper).showTrayMessageWithFallbackToDialog(anyString(), eq(alarm.getDescription()), isNull(TrayIcon.class), isA(ResourceBundle.class)); verify(audioPlayer).playInThread(isA(ThreadHelper.class), eq(alarm.getSound()), isA(Thread.UncaughtExceptionHandler.class)); } /** * Testet die Verarbeitung eines unwichtigen Ereignisses. */ @Test public final void testIgnoreEvent() { final AlarmController controller = (AlarmController) getController(); final GuiHelper guiHelper = controller.getGuiHelper(); final MessageHelper messageHelper = mock(MessageHelper.class); guiHelper.setMessageHelper(messageHelper); final AudioPlayer audioPlayer = mock(AudioPlayer.class); guiHelper.setAudioPlayer(audioPlayer); // unwichtiges Ereignis auslösen controller.handleEvent(mock(TimeyEvent.class)); waitForThreads(); // sicherstellen, dass Ereignis ignoriert wird verify(messageHelper, never()).showTrayMessageWithFallbackToDialog(anyString(), anyString(), isNull(TrayIcon.class), isA(ResourceBundle.class)); verify(audioPlayer, never()).playInThread(isA(ThreadHelper.class), anyString(), isA(Thread.UncaughtExceptionHandler.class)); } }