package rmblworx.tools.timey.gui.component; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.time.LocalTime; import java.util.List; import java.util.Vector; import javafx.application.Platform; import javafx.geometry.VerticalDirection; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Slider; import javafx.scene.control.TextField; import javafx.scene.input.KeyCode; 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 rmblworx.tools.timey.gui.DateTimeUtil; import rmblworx.tools.timey.gui.JavaFxGuiTest; import com.google.common.base.Predicate; /* * Copyright 2014-2015 Christian Raue * MIT License http://opensource.org/licenses/mit-license.php */ /** * GUI-Tests für die TimePicker-Komponente. * @author Christian Raue {@literal <christian.raue@gmail.com>} */ @Category(TestFX.class) public class TimePickerTest extends JavaFxGuiTest { /** * Container für Elemente. */ private Scene scene; // GUI-Elemente private TimePicker timePicker; private TextField hoursTextField; private TextField minutesTextField; private TextField secondsTextField; private Slider hoursSlider; private Slider minutesSlider; private Slider secondsSlider; /** * @return Elternknoten der GUI-Elemente */ @Override protected final Parent getRootNode() { return WRAP_IN_CONTAINER ? wrapInContainer(new TimePicker()) : new TimePicker(); } /** * {@inheritDoc} */ @Override protected final Object getComponentWithFxmlFields() { return WRAP_IN_CONTAINER ? scene.getRoot().getChildrenUnmodifiable().get(0) : scene.getRoot(); } @Before public final void setUp() { scene = stage.getScene(); timePicker = (TimePicker) getComponentWithFxmlFields(); hoursTextField = (TextField) scene.lookup("#hoursTextField"); minutesTextField = (TextField) scene.lookup("#minutesTextField"); secondsTextField = (TextField) scene.lookup("#secondsTextField"); hoursSlider = (Slider) scene.lookup("#hoursSlider"); minutesSlider = (Slider) scene.lookup("#minutesSlider"); secondsSlider = (Slider) scene.lookup("#secondsSlider"); } @Test public final void testInitializedFields() throws IllegalAccessException { super.testFxmlInitializedFields(); } /** * Testet den Ausgangszustand der Komponente. */ @Test public final void testInitialState() { assertEquals("00", hoursTextField.getText()); assertEquals("00", minutesTextField.getText()); assertEquals("00", secondsTextField.getText()); assertEquals(0L, (long) hoursSlider.getValue()); assertEquals(0L, (long) minutesSlider.getValue()); assertEquals(0L, (long) secondsSlider.getValue()); } /** * Testet die bidirektionale Verbindung zwischen Textfeldern und Slidern. */ @Test public final void testBidirectionalBoundingOfTextFieldsAndSliders() { final List<TextFieldAndSliderValue> testCases = new Vector<>(); testCases.add(new TextFieldAndSliderValue(hoursSlider, 1, hoursTextField, "01")); testCases.add(new TextFieldAndSliderValue(hoursSlider, 23, hoursTextField, "23")); testCases.add(new TextFieldAndSliderValue(minutesSlider, 59, minutesTextField, "59")); testCases.add(new TextFieldAndSliderValue(secondsSlider, 59, secondsTextField, "59")); // abschließend alle Slider zurücksetzen testCases.add(new TextFieldAndSliderValue(hoursSlider, 0, hoursTextField, "00")); testCases.add(new TextFieldAndSliderValue(minutesSlider, 0, minutesTextField, "00")); testCases.add(new TextFieldAndSliderValue(secondsSlider, 0, secondsTextField, "00")); // Wert des Sliders setzen und sicherstellen, dass Textfeld-Inhalt der Erwartung entspricht for (final TextFieldAndSliderValue testCase : testCases) { Platform.runLater(new Runnable() { public void run() { testCase.slider.setValue(testCase.sliderValue); } }); FXTestUtils.awaitEvents(); try { waitUntil(testCase.textField, new Predicate<TextField>() { public boolean apply(final TextField textField) { return testCase.textFieldValue.equals(textField.getText()); } }, 1); } catch (final RuntimeException e) { fail(String.format("test case %d: '%s' doesn't match expected '%s'.", testCases.indexOf(testCase), testCase.textField.getText(), testCase.textFieldValue)); } } // Inhalt des Textfelds setzen und sicherstellen, dass Slider-Wert der Erwartung entspricht for (final TextFieldAndSliderValue testCase : testCases) { Platform.runLater(new Runnable() { public void run() { testCase.textField.setText(testCase.textFieldValue); } }); FXTestUtils.awaitEvents(); try { waitUntil(testCase.slider, new Predicate<Slider>() { public boolean apply(final Slider slider) { return testCase.slider.getValue() == testCase.sliderValue; } }, 1); } catch (final RuntimeException e) { fail(String.format("test case %d: '%.0f' doesn't match expected '%s'.", testCases.indexOf(testCase), testCase.slider.getValue(), testCase.sliderValue)); } } } /** * Testet Wertänderung durch Scrollen innerhalb von Textfeldern. */ @Test public final void testTextFieldScrolling() { /* * Zunächst vom Betriebssystem vorgegebene Scrollrichtung ermitteln. * Kann z. B. unter Mac OS X frei festgelegt werden und ist standardmäßig umgekehrt, ergo verringert hochscrollen den Wert. */ click(hoursTextField); scroll(VerticalDirection.UP); final boolean scrollingUpIncreasesValue = hoursSlider.getValue() == 1.0; if (scrollingUpIncreasesValue) { // ursprünglichen Zustand wiederherstellen scroll(VerticalDirection.DOWN); } // Scrollrichtungen entsprechend definieren final VerticalDirection increaseValue = scrollingUpIncreasesValue ? VerticalDirection.UP : VerticalDirection.DOWN; final VerticalDirection decreaseValue = scrollingUpIncreasesValue ? VerticalDirection.DOWN : VerticalDirection.UP; // nun zum eigentlichen Test ... final TextFieldScrollingData[] testCases = new TextFieldScrollingData[] { new TextFieldScrollingData(minutesTextField, hoursTextField, hoursSlider), new TextFieldScrollingData(secondsTextField, minutesTextField, minutesSlider), new TextFieldScrollingData(hoursTextField, secondsTextField, secondsSlider), }; for (final TextFieldScrollingData testCase : testCases) { click(testCase.focusedTextField); // anderes Feld fokussieren // Scrollen über Feld ohne Fokus hat keine Auswirkung move(testCase.textField); scroll(increaseValue); assertEquals(0.0, testCase.slider.getValue(), 0.0); click(testCase.textField); // richtiges Feld fokussieren scroll(increaseValue); assertEquals(1.0, testCase.slider.getValue(), 0.0); scroll(decreaseValue); assertEquals(0.0, testCase.slider.getValue(), 0.0); } } /** * Testet das Verhalten der Textfelder bei Eingabe ungültiger Werte. */ @Test public final void testTextFieldsInvalidInput() { final List<TextFieldInputAndValue> testCases = new Vector<>(); // zu kurze Eingabe testCases.add(new TextFieldInputAndValue(hoursTextField, "1", "01", true)); testCases.add(new TextFieldInputAndValue(minutesTextField, "1", "01", true)); testCases.add(new TextFieldInputAndValue(secondsTextField, "1", "01", true)); // zu lange Eingabe testCases.add(new TextFieldInputAndValue(hoursTextField, "111", "23", true)); testCases.add(new TextFieldInputAndValue(minutesTextField, "111", "59", true)); testCases.add(new TextFieldInputAndValue(secondsTextField, "111", "59", true)); testCases.add(new TextFieldInputAndValue(hoursTextField, "011", "11", false)); testCases.add(new TextFieldInputAndValue(minutesTextField, "011", "11", false)); testCases.add(new TextFieldInputAndValue(secondsTextField, "011", "11", false)); // überschrittener Maximalwert testCases.add(new TextFieldInputAndValue(hoursTextField, "99", "23", true)); testCases.add(new TextFieldInputAndValue(minutesTextField, "99", "59", true)); testCases.add(new TextFieldInputAndValue(secondsTextField, "99", "59", true)); // keine Zahl testCases.add(new TextFieldInputAndValue(hoursTextField, "x", "00", true)); testCases.add(new TextFieldInputAndValue(minutesTextField, "x", "00", true)); testCases.add(new TextFieldInputAndValue(secondsTextField, "x", "00", true)); for (final InputMode inputMode : new InputMode[] { InputMode.TYPE, InputMode.COPY_PASTE }) { // Inhalt des Textfelds setzen und sicherstellen, dass Inhalt der Erwartung entspricht for (final TextFieldInputAndValue testCase : testCases) { Platform.runLater(new Runnable() { public void run() { testCase.textField.requestFocus(); // Feld fokussieren } }); FXTestUtils.awaitEvents(); assertTrue(testCase.textField.isFocused()); switch (inputMode) { case TYPE: type(testCase.input); break; case COPY_PASTE: Platform.runLater(new Runnable() { public void run() { testCase.textField.setText(testCase.input); } }); FXTestUtils.awaitEvents(); break; default: fail("unbekannter Modus"); } if (testCase.loseFocusAfterwards) { type(KeyCode.TAB); // Feld muss Fokus wieder verlieren assertFalse(testCase.textField.isFocused()); } try { waitUntil(testCase.textField, new Predicate<TextField>() { public boolean apply(final TextField textField) { return testCase.value.equals(textField.getText()); } }, 1); } catch (final RuntimeException e) { fail(String.format("test case %d [%s]: '%s' doesn't match expected '%s'.", testCases.indexOf(testCase), inputMode.toString().toLowerCase(), testCase.textField.getText(), testCase.value)); } finally { // Feldinhalt auf Ausgangswert zurücksetzen Platform.runLater(new Runnable() { public void run() { testCase.textField.setText("00"); } }); FXTestUtils.awaitEvents(); } } } } /** * Testet das Setzen der Zeit. */ @Test public final void testSetTime() { final TimePartsAndTimeValue[] testCases = new TimePartsAndTimeValue[] { new TimePartsAndTimeValue("23", "59", "59", "23:59:59"), new TimePartsAndTimeValue("00", "00", "00", "00:00:00"), }; for (final TimePartsAndTimeValue testCase : testCases) { final LocalTime time = DateTimeUtil.getLocalTimeForString(testCase.timeString); Platform.runLater(new Runnable() { public void run() { timePicker.setValue(time); } }); FXTestUtils.awaitEvents(); assertEquals(testCase.hours, hoursTextField.getText()); assertEquals(testCase.minutes, minutesTextField.getText()); assertEquals(testCase.seconds, secondsTextField.getText()); assertEquals(time, timePicker.getTimeProperty().get()); } } /** * Testet das Abrufen der Zeit. */ @Test public final void testGetTime() { final TimePartsAndTimeValue[] testCases = new TimePartsAndTimeValue[] { new TimePartsAndTimeValue("23", "59", "59", "23:59:59"), new TimePartsAndTimeValue("00", "00", "00", "00:00:00"), }; for (final TimePartsAndTimeValue testCase : testCases) { final LocalTime time = DateTimeUtil.getLocalTimeForString(testCase.timeString); Platform.runLater(new Runnable() { public void run() { hoursTextField.setText(testCase.hours); minutesTextField.setText(testCase.minutes); secondsTextField.setText(testCase.seconds); } }); FXTestUtils.awaitEvents(); assertEquals(time, timePicker.getValue()); assertEquals(time, timePicker.getTimeProperty().get()); } } private enum InputMode { /** * Eingabe per Tippen. */ TYPE, /** * Einfügen per Copy & Paste. */ COPY_PASTE, } private final class TextFieldAndSliderValue { public final Slider slider; public final long sliderValue; public final TextField textField; public final String textFieldValue; public TextFieldAndSliderValue(final Slider slider, final long sliderValue, final TextField textField, final String textFieldValue) { this.slider = slider; this.sliderValue = sliderValue; this.textField = textField; this.textFieldValue = textFieldValue; } } private final class TextFieldScrollingData { public final TextField focusedTextField; public final TextField textField; public final Slider slider; public TextFieldScrollingData(final TextField focusedTextField, final TextField textField, final Slider slider) { this.focusedTextField = focusedTextField; this.textField = textField; this.slider = slider; } } private final class TextFieldInputAndValue { public final TextField textField; public final String input; public final String value; public final boolean loseFocusAfterwards; public TextFieldInputAndValue(final TextField textField, final String input, final String value, final boolean loseFocusAfterwards) { this.textField = textField; this.input = input; this.value = value; this.loseFocusAfterwards = loseFocusAfterwards; } } private final class TimePartsAndTimeValue { public final String hours; public final String minutes; public final String seconds; public final String timeString; public TimePartsAndTimeValue(final String hours, final String minutes, final String seconds, final String timeString) { this.hours = hours; this.minutes = minutes; this.seconds = seconds; this.timeString = timeString; } } }