From 20fb4d7628464f93746567eed6e5d47a763413ef Mon Sep 17 00:00:00 2001 From: Christoph Pirkl <4711730+kaklakariada@users.noreply.github.com> Date: Sun, 14 Apr 2024 16:00:40 +0200 Subject: [PATCH] Improve monthly report (#273) * Add prev/next buttons to monthly report --------- Co-authored-by: kaklakariada --- .github/workflows/build.yml | 7 +++ .vscode/settings.json | 16 +++++- CHANGELOG.md | 6 +- jfxui/build.gradle | 2 +- .../whiterabbit/jfxui/UiActions.java | 12 ++-- .../activities/ActivityPropertyAdapter.java | 10 ++-- .../whiterabbit/jfxui/tray/AwtTrayIcon.java | 15 +++-- .../jfxui/ui/MonthlyProjectReportViewer.java | 57 ++++++++++++++----- .../jfxui/ui/widget/ReportWindow.java | 20 +++++-- .../uistate/widgets/StageStateManager.java | 10 ++-- .../jfxui/JavaFxAppUiTestBase.java | 14 ++--- .../jfxui/MonthlyProjectReportTest.java | 46 +++++++++++++-- .../whiterabbit/jfxui/testutil/TestUtil.java | 36 +++++++++++- .../testutil/model/ApplicationHelper.java | 14 ++--- .../jfxui/testutil/model/JavaFxTable.java | 8 ++- .../model/MonthlyProjectReportWindow.java | 23 +++++++- 16 files changed, 229 insertions(+), 67 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8617c1d1..b3525331 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,6 +77,13 @@ jobs: ./gradlew -status ./gradlew -stop + - name: Archive test reports for ${{ runner.os }} + uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: test-reports-${{ runner.os }} + path: "**/build/reports/tests/*/**" + - name: Archive native package for ${{ runner.os }} uses: actions/upload-artifact@v4 if: ${{ env.DEFAULT_JAVA == matrix.java }} diff --git a/.vscode/settings.json b/.vscode/settings.json index fd2b156e..21ee427e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,12 +5,24 @@ "source.fixAll": "explicit" }, "java.codeGeneration.useBlocks": true, + "java.compile.nullAnalysis.mode": "disabled", + "java.configuration.updateBuildConfiguration": "automatic", + "java.test.config": { + "vmArgs": [ + "-Djava.awt.headless=true", + "-Dtestfx.headless=true", + "-Dtestfx.robot=glass", + "-Dglass.platform=Monocle", + "-Dmonocle.platform=Headless" + ] + }, + "[java]": { + "editor.indentSize": 4 + }, "editor.formatOnSave": true, "editor.formatOnSaveMode": "file", "sonarlint.connectedMode.project": { "connectionId": "itsallcode", "projectKey": "white-rabbit" }, - "java.compile.nullAnalysis.mode": "automatic", - "java.configuration.updateBuildConfiguration": "automatic" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ac539ce..705dd118 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,11 @@ This release requires Java 21. * [#245](https://github.com/itsallcode/white-rabbit/pull/245): Removed webstart deployment. * [#265](https://github.com/itsallcode/white-rabbit/pull/265): Upgraded dependencies, require Java 21. -### Changes / Bugfixes +### New Features + +* [#273](https://github.com/itsallcode/white-rabbit/pull/273): Added buttons to monthly report for jumping to the previous/next month + +### Bugfixes * [#241](https://github.com/itsallcode/white-rabbit/pull/241): Fix automatic interruption dialog popup after resume. * [#233](https://github.com/itsallcode/white-rabbit/pull/233): Upgrade dependencies, use [Gradle versions catalog](https://docs.gradle.org/current/userguide/platforms.html). diff --git a/jfxui/build.gradle b/jfxui/build.gradle index f0e4c87a..e37e62d1 100644 --- a/jfxui/build.gradle +++ b/jfxui/build.gradle @@ -124,7 +124,7 @@ task uiTest(type: Test, group: 'verification') { } forkEvery 5 - jvmArgs '-XX:+HeapDumpOnOutOfMemoryError' + jvmArgs '-XX:+HeapDumpOnOutOfMemoryError', '-XX:+EnableDynamicAgentLoading' if(!project.hasProperty("uiTestsHeadless") || project.property("uiTestsHeadless") != "false") { systemProperty 'java.awt.headless', 'true' diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/UiActions.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/UiActions.java index 3bca0716..992b9a91 100644 --- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/UiActions.java +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/UiActions.java @@ -6,13 +6,18 @@ import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.itsallcode.whiterabbit.api.model.ProjectReport; import org.itsallcode.whiterabbit.jfxui.service.DesktopService; -import org.itsallcode.whiterabbit.jfxui.ui.*; +import org.itsallcode.whiterabbit.jfxui.ui.DailyProjectReportViewer; +import org.itsallcode.whiterabbit.jfxui.ui.MonthlyProjectReportViewer; +import org.itsallcode.whiterabbit.jfxui.ui.PluginManagerViewer; +import org.itsallcode.whiterabbit.jfxui.ui.VacationReportViewer; import org.itsallcode.whiterabbit.logic.Config; import org.itsallcode.whiterabbit.logic.model.MonthIndex; import org.itsallcode.whiterabbit.logic.report.vacation.VacationReport; @@ -133,8 +138,7 @@ public void showMonthlyProjectReport() LOG.warn("No month selected, can't generate project report"); return; } - final ProjectReport report = appService.generateProjectReport(monthIndex.getYearMonth()); - new MonthlyProjectReportViewer(getPrimaryStage(), state.uiState, appService, report).show(); + new MonthlyProjectReportViewer(getPrimaryStage(), state.uiState, appService, monthIndex.getYearMonth()).show(); } public void showPluginManager() diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/table/activities/ActivityPropertyAdapter.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/table/activities/ActivityPropertyAdapter.java index 2e787bbc..8c382c1f 100644 --- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/table/activities/ActivityPropertyAdapter.java +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/table/activities/ActivityPropertyAdapter.java @@ -7,7 +7,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.eclipse.jdt.annotation.NonNull; import org.itsallcode.whiterabbit.jfxui.table.EditListener; import org.itsallcode.whiterabbit.jfxui.table.RecordPropertyAdapter; import org.itsallcode.whiterabbit.logic.model.Activity; @@ -25,7 +24,7 @@ public final class ActivityPropertyAdapter extends RecordPropertyAdapter remainder; final ObjectProperty comment; - private ActivityPropertyAdapter(EditListener editListener, Activity act) + private ActivityPropertyAdapter(final EditListener editListener, final Activity act) { super(dayRecord -> editListener.recordUpdated(dayRecord.getDay())); setRecord(act); @@ -36,7 +35,7 @@ private ActivityPropertyAdapter(EditListener editListener, Activity a update(); } - public void setActivity(Activity activity) + public void setActivity(final Activity activity) { super.setRecord(activity); update(); @@ -50,12 +49,13 @@ public void update() }); } - public static ActivityPropertyAdapter wrap(EditListener editListener, Activity activity) + public static ActivityPropertyAdapter wrap(final EditListener editListener, final Activity activity) { return new ActivityPropertyAdapter(editListener, activity); } - static List<@NonNull ActivityPropertyAdapter> wrap(EditListener editListener, List activities) + static List wrap(final EditListener editListener, + final List activities) { return activities.stream() .map(a -> wrap(editListener, a)) diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/tray/AwtTrayIcon.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/tray/AwtTrayIcon.java index 6110d77f..168871cf 100644 --- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/tray/AwtTrayIcon.java +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/tray/AwtTrayIcon.java @@ -18,7 +18,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.eclipse.jdt.annotation.NonNull; class AwtTrayIcon implements Tray { @@ -27,13 +26,13 @@ class AwtTrayIcon implements Tray private final SystemTray tray; private final TrayIcon trayIcon; - private AwtTrayIcon(SystemTray tray, TrayIcon trayIcon) + private AwtTrayIcon(final SystemTray tray, final TrayIcon trayIcon) { this.tray = tray; this.trayIcon = trayIcon; } - static @NonNull Tray createAwtTray(TrayCallback callback) + static Tray createAwtTray(final TrayCallback callback) { try { @@ -52,7 +51,7 @@ private AwtTrayIcon(SystemTray tray, TrayIcon trayIcon) } } - private static Image loadImage(final String resourceName, Dimension size) + private static Image loadImage(final String resourceName, final Dimension size) { final URL imageUrl = AwtTrayIcon.class.getResource(resourceName); try @@ -67,7 +66,7 @@ private static Image loadImage(final String resourceName, Dimension size) } } - private static PopupMenu createPopupMenu(TrayCallback callback) + private static PopupMenu createPopupMenu(final TrayCallback callback) { final PopupMenu popupMenu = new PopupMenu("White Rabbit Time Recording"); popupMenu.add(menuItem("Show", KeyEvent.VK_S, callback::showMainWindow)); @@ -77,7 +76,7 @@ private static PopupMenu createPopupMenu(TrayCallback callback) return popupMenu; } - private static MenuItem menuItem(String label, int shortcutKey, Runnable action) + private static MenuItem menuItem(final String label, final int shortcutKey, final Runnable action) { final MenuItem menuItem = new MenuItem(label, new MenuShortcut(shortcutKey)); menuItem.addActionListener(event -> action.run()); @@ -85,13 +84,13 @@ private static MenuItem menuItem(String label, int shortcutKey, Runnable action) } @Override - public void setTooltip(String tooltip) + public void setTooltip(final String tooltip) { trayIcon.setToolTip(tooltip); } @Override - public void displayMessage(String caption, String text, MessageType messageType) + public void displayMessage(final String caption, final String text, final MessageType messageType) { SwingUtilities.invokeLater(() -> trayIcon.displayMessage(caption, text, messageType)); } diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/MonthlyProjectReportViewer.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/MonthlyProjectReportViewer.java index 5b206fba..363b61bb 100644 --- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/MonthlyProjectReportViewer.java +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/MonthlyProjectReportViewer.java @@ -1,12 +1,13 @@ package org.itsallcode.whiterabbit.jfxui.ui; import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toList; import java.time.Duration; import java.time.YearMonth; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.itsallcode.whiterabbit.api.model.ProjectReport; import org.itsallcode.whiterabbit.api.model.ProjectReportActivity; import org.itsallcode.whiterabbit.jfxui.table.converter.DurationStringConverter; @@ -19,39 +20,62 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.scene.Node; import javafx.scene.control.TableView; import javafx.stage.Stage; import javafx.util.converter.DefaultStringConverter; public class MonthlyProjectReportViewer { - private final ProjectReport report; + private static final Logger LOG = LogManager.getLogger(MonthlyProjectReportViewer.class); + private final ReportWindow reportWindow; private final UiStateService uiState; private final AppService appService; + private YearMonth yearMonth; - public MonthlyProjectReportViewer(Stage primaryStage, UiStateService uiState, AppService appService, - ProjectReport report) + public MonthlyProjectReportViewer(final Stage primaryStage, final UiStateService uiState, + final AppService appService, final YearMonth yearMonth) { this.uiState = uiState; this.appService = appService; - this.reportWindow = new ReportWindow(primaryStage, uiState, "monthly-project-report", "Monthly Project Report"); - this.report = report; + this.yearMonth = yearMonth; + this.reportWindow = new ReportWindow(primaryStage, uiState, "monthly-project-report", getWindowTitle()); } public void show() { final TableView treeTable = createTreeTable(); - reportWindow.show(treeTable); + updateTable(treeTable); + final Node previousMonthButton = UiWidget.button("prev-month-button", "< Previous Month", + e -> gotoMonth(treeTable, -1)); + final Node nextMonthButton = UiWidget.button("next-month-button", "Next Month >", + e -> gotoMonth(treeTable, +1)); + reportWindow.show(treeTable, previousMonthButton, nextMonthButton); uiState.register(treeTable); } + private void gotoMonth(final TableView treeTable, final int count) + { + yearMonth = yearMonth.plusMonths(count); + LOG.debug("Go {} months to {}", count, yearMonth); + updateTable(treeTable); + reportWindow.updateTitle(getWindowTitle()); + } + + private void updateTable(final TableView treeTable) + { + treeTable.setItems(createRows()); + } + + private String getWindowTitle() + { + return "Monthly Project Report " + yearMonth; + } + private TableView createTreeTable() { - final ObservableList rows = FXCollections.observableList( - report.getProjects().stream() - .map(project -> createRow(report.getMonth(), project)).collect(toList())); - final TableView treeTable = new TableView<>(rows); + final TableView treeTable = new TableView<>(); treeTable.getColumns().addAll(List.of( UiWidget.readOnlyColumn("yearMonth", "Month", new YearMonthStringConverter(), ReportRow::getMonth), @@ -67,7 +91,14 @@ private TableView createTreeTable() return treeTable; } - private ReportRow createRow(YearMonth month, ProjectReportActivity project) + private final ObservableList createRows() + { + final ProjectReport report = appService.generateProjectReport(yearMonth); + return FXCollections.observableList( + report.getProjects().stream().map(project -> createRow(report.getMonth(), project)).toList()); + } + + private ReportRow createRow(final YearMonth month, final ProjectReportActivity project) { return new ReportRow(month, project); } @@ -77,7 +108,7 @@ public static class ReportRow private final YearMonth month; private final ProjectReportActivity project; - private ReportRow(YearMonth month, ProjectReportActivity project) + private ReportRow(final YearMonth month, final ProjectReportActivity project) { this.month = month; this.project = project; diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/widget/ReportWindow.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/widget/ReportWindow.java index 40a32292..eb90ccd4 100644 --- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/widget/ReportWindow.java +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/ui/widget/ReportWindow.java @@ -5,6 +5,8 @@ import java.util.ArrayList; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.itsallcode.whiterabbit.jfxui.ui.UiResources; import org.itsallcode.whiterabbit.jfxui.ui.UiWidget; import org.itsallcode.whiterabbit.jfxui.uistate.UiStateService; @@ -20,13 +22,15 @@ public class ReportWindow { + private static final Logger LOG = LogManager.getLogger(ReportWindow.class); private final Stage primaryStage; private final String windowTitle; private final UiStateService uiState; private final String id; private Stage stage; - public ReportWindow(Stage primaryStage, UiStateService uiState, String id, String windowTitle) + public ReportWindow(final Stage primaryStage, final UiStateService uiState, final String id, + final String windowTitle) { this.primaryStage = primaryStage; this.uiState = uiState; @@ -34,13 +38,14 @@ public ReportWindow(Stage primaryStage, UiStateService uiState, String id, Strin this.windowTitle = windowTitle; } - public void show(Node reportView, Node... toolBarItems) + public void show(final Node reportView, final Node... toolBarItems) { stage = createStage(reportView, toolBarItems); + LOG.debug("Show report window"); stage.show(); } - private Stage createStage(Node reportView, Node... toolBarItems) + private Stage createStage(final Node reportView, final Node... toolBarItems) { final BorderPane pane = new BorderPane(); pane.setTop(createToolBar(toolBarItems)); @@ -49,7 +54,7 @@ private Stage createStage(Node reportView, Node... toolBarItems) return createStage(pane); } - private ToolBar createToolBar(Node... items) + private ToolBar createToolBar(final Node... items) { final Node closeButton = UiWidget.button("close-button", "Close Report", e -> closeReportWindow()); final List allItems = new ArrayList<>(); @@ -77,8 +82,15 @@ private Stage createStage(final Parent root) return newStage; } + public void updateTitle(final String title) + { + LOG.debug("Update window title to '{}'", title); + stage.setTitle(title); + } + private void closeReportWindow() { + LOG.debug("Closing window '{}'", stage.getTitle()); this.stage.close(); } } diff --git a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/uistate/widgets/StageStateManager.java b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/uistate/widgets/StageStateManager.java index bea140cb..b6278d37 100644 --- a/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/uistate/widgets/StageStateManager.java +++ b/jfxui/src/main/java/org/itsallcode/whiterabbit/jfxui/uistate/widgets/StageStateManager.java @@ -12,17 +12,17 @@ class StageStateManager implements WidgetStateManager private static final Logger LOG = LogManager.getLogger(StageStateManager.class); private final DelayedPropertyListener propertyListener; - public StageStateManager(DelayedPropertyListener propertyListener) + public StageStateManager(final DelayedPropertyListener propertyListener) { this.propertyListener = propertyListener; } @Override - public void restore(Stage widget, StageStateModel model) + public void restore(final Stage widget, final StageStateModel model) { if (model.width == null || model.height == null || model.width == 0 || model.height == 0) { - LOG.debug("State not available, don't restore stage {}: {}", model.id, this); + LOG.trace("State not available, don't restore stage {}", model.id); return; } widget.setX(model.x); @@ -32,7 +32,7 @@ public void restore(Stage widget, StageStateModel model) } @Override - public void watch(Stage widget, StageStateModel model) + public void watch(final Stage widget, final StageStateModel model) { propertyListener.register(widget.xProperty(), model::setX); propertyListener.register(widget.yProperty(), model::setY); @@ -41,7 +41,7 @@ public void watch(Stage widget, StageStateModel model) } @Override - public StageStateModel createEmptyModel(String id) + public StageStateModel createEmptyModel(final String id) { return new StageStateModel(id); } diff --git a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/JavaFxAppUiTestBase.java b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/JavaFxAppUiTestBase.java index 2ae19086..46cca8fd 100644 --- a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/JavaFxAppUiTestBase.java +++ b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/JavaFxAppUiTestBase.java @@ -60,7 +60,7 @@ abstract class JavaFxAppUiTestBase private ApplicationHelper applicationHelper; private TimeUtil timeUtil; - protected void setRobot(FxRobot robot) + protected void setRobot(final FxRobot robot) { applicationHelper = ApplicationHelper.create(robot); } @@ -80,12 +80,12 @@ protected void doStart(final Stage stage) doStart(stage, null); } - protected void setInitialTime(Instant initialTime) + protected void setInitialTime(final Instant initialTime) { this.initialTime = initialTime; } - public void setCommandLineArgs(List commandLineArgs) + public void setCommandLineArgs(final List commandLineArgs) { this.commandLineArgs = commandLineArgs; } @@ -117,7 +117,7 @@ protected void doStop() LOG.info("Application shutdown done"); } - private void prepareConfiguration(final ProjectConfig projectConfig, WorkingDirProvider testDirProvider) + private void prepareConfiguration(final ProjectConfig projectConfig, final WorkingDirProvider testDirProvider) { this.dataDir = this.tempDir.resolve("data-dir"); String configFileContent = "data = " + this.dataDir.toString().replace('\\', '/') + "\n"; @@ -200,12 +200,12 @@ private static class TestDirProvider implements WorkingDirProvider { private final Path tempDir; - private TestDirProvider(Path tempDir) + private TestDirProvider(final Path tempDir) { this.tempDir = Objects.requireNonNull(tempDir, "tempDir"); } - public static TestDirProvider create(Path tempDir) + public static TestDirProvider create(final Path tempDir) { final TestDirProvider dirProvider = new TestDirProvider(tempDir); try @@ -215,7 +215,7 @@ public static TestDirProvider create(Path tempDir) } catch (final IOException e) { - throw new UncheckedIOException("Error creating temp directories", e); + throw new UncheckedIOException("Error creating temp directories at " + tempDir, e); } return dirProvider; } diff --git a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/MonthlyProjectReportTest.java b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/MonthlyProjectReportTest.java index f5d60db5..80ba4e72 100644 --- a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/MonthlyProjectReportTest.java +++ b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/MonthlyProjectReportTest.java @@ -2,11 +2,13 @@ import java.time.Duration; import java.time.Instant; +import java.time.YearMonth; import java.util.Locale; import org.itsallcode.whiterabbit.api.model.Project; import org.itsallcode.whiterabbit.jfxui.testutil.model.MonthlyProjectReportWindow; import org.itsallcode.whiterabbit.logic.service.project.ProjectImpl; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.testfx.api.FxRobot; @@ -19,6 +21,7 @@ @ExtendWith(ApplicationExtension.class) class MonthlyProjectReportTest extends JavaFxAppUiTestBase { + private static final Instant INITIAL_TIME = Instant.parse("2007-12-03T10:15:30.20Z"); private static final ProjectImpl PROJECT1 = project("p1", "Project 1"); private static final ProjectImpl PROJECT2 = project("p2", "Project 2"); @@ -28,7 +31,7 @@ class MonthlyProjectReportTest extends JavaFxAppUiTestBase void emptyProjectReport() { time().tickSeparateMinutes(2); - final MonthlyProjectReportWindow report = app().openMonthlyProjectReport(); + final MonthlyProjectReportWindow report = app().openMonthlyProjectReport(YearMonth.of(2007, 12)); report.assertRowCount(0); report.closeViaCloseButton(); @@ -38,7 +41,7 @@ void emptyProjectReport() void closeReportByTypingEscKey() { time().tickSeparateMinutes(2); - final MonthlyProjectReportWindow report = app().openMonthlyProjectReport(); + final MonthlyProjectReportWindow report = app().openMonthlyProjectReport(YearMonth.of(2007, 12)); report.closeViaEscKey(); } @@ -50,18 +53,51 @@ void filledProjectReport() final Project project = new ProjectImpl("p1", "Project 1", null); app().activitiesTable().addRemainderActivity(project, "a1"); - final MonthlyProjectReportWindow report = app().openMonthlyProjectReport(); + final MonthlyProjectReportWindow report = app().openMonthlyProjectReport(YearMonth.of(2007, 12)); report.assertRowCount(1) .assertProject(0, PROJECT1, Duration.ofMinutes(2), "a1"); report.closeViaCloseButton(); } + @Test + void gotoPreviousMonth() + { + time().tickSeparateMinutes(3); + + final Project project = new ProjectImpl("p1", "Project 1", null); + app().activitiesTable().addRemainderActivity(project, "a1"); + + final MonthlyProjectReportWindow report = app().openMonthlyProjectReport(YearMonth.of(2007, 12)); + report.assertRowCount(1); + report.gotoPreviousMonth(); + report.assertWindowTitle("Monthly Project Report 2007-11"); + report.assertRowCount(0); + report.closeViaCloseButton(); + } + + @Test + @Disabled("Second month change test is not working") + void gotoNextMonth() + { + time().tickSeparateMinutes(3); + + final Project project = new ProjectImpl("p1", "Project 1", null); + app().activitiesTable().addRemainderActivity(project, "a1"); + + final MonthlyProjectReportWindow report = app().openMonthlyProjectReport(YearMonth.of(2007, 12)); + report.assertRowCount(1); + report.gotoNextMonth(); + report.assertWindowTitle("Monthly Project Report 2008-01"); + report.assertRowCount(0); + report.closeViaCloseButton(); + } + @Override @Start - void start(Stage stage) + void start(final Stage stage) { setLocale(Locale.GERMANY); - setInitialTime(Instant.parse("2007-12-03T10:15:30.20Z")); + setInitialTime(INITIAL_TIME); doStart(stage, projectConfig(PROJECT1, PROJECT2)); setRobot(robot); } diff --git a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/TestUtil.java b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/TestUtil.java index d8ef47d4..ca7bae35 100644 --- a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/TestUtil.java +++ b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/TestUtil.java @@ -1,9 +1,15 @@ package org.itsallcode.whiterabbit.jfxui.testutil; import java.time.Duration; +import java.time.Instant; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public class TestUtil { + private static final Logger LOG = LogManager.getLogger(TestUtil.class); + private TestUtil() { // Not instantiable @@ -19,7 +25,7 @@ public static void sleepShort() sleep(Duration.ofMillis(100)); } - private static void sleep(Duration duration) + private static void sleep(final Duration duration) { try { @@ -30,4 +36,32 @@ private static void sleep(Duration duration) Thread.currentThread().interrupt(); } } + + public static void retryAssertion(final Duration duration, final Runnable assertion) + { + final Instant start = Instant.now(); + int tries = 0; + while (true) + { + try + { + tries++; + assertion.run(); + return; + } + catch (final AssertionError e) + { + final Duration currentDuration = Duration.between(start, Instant.now()); + final String message = "Assertion failed after " + currentDuration + " / " + tries + " tries: " + + e.getMessage(); + final Duration remaining = currentDuration.minus(duration); + if (remaining.isPositive()) + { + throw new AssertionError(message, e); + } + LOG.warn(message); + sleepLong(); + } + } + } } diff --git a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/model/ApplicationHelper.java b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/model/ApplicationHelper.java index 8994e517..7e0478d2 100644 --- a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/model/ApplicationHelper.java +++ b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/model/ApplicationHelper.java @@ -20,12 +20,12 @@ public class ApplicationHelper { private final FxRobot robot; - private ApplicationHelper(FxRobot robot, Window window) + private ApplicationHelper(final FxRobot robot, final Window window) { this.robot = robot; } - public static ApplicationHelper create(FxRobot robot) + public static ApplicationHelper create(final FxRobot robot) { Objects.requireNonNull(robot, "robot"); return new ApplicationHelper(robot, robot.window(0)); @@ -88,7 +88,7 @@ public PluginManagerWindow openPluginManager() return new PluginManagerWindow(robot, window); } - public void addPresetInterruption(Duration preset) + public void addPresetInterruption(final Duration preset) { final SplitMenuButton splitMenuButton = robot.lookup("#add-interruption-button").query(); final StackPane arrowButton = (StackPane) robot.from(splitMenuButton).lookup(".arrow-button").query(); @@ -110,7 +110,7 @@ public YearMonth getSelectedMonth() return getSelectedMonthComboBox().getValue(); } - public void setSelectedMonth(YearMonth month) + public void setSelectedMonth(final YearMonth month) { JavaFxUtil.runOnFxApplicationThread(() -> getSelectedMonthComboBox().setValue(month)); } @@ -125,7 +125,7 @@ public void gotoPreviousMonth() clickButton("#previous-month-button"); } - private void clickButton(String query) + private void clickButton(final String query) { final Button button = robot.lookup(query).queryButton(); robot.clickOn(button); @@ -146,12 +146,12 @@ public DailyProjectReportWindow openDailyProjectReport() return new DailyProjectReportWindow(robot, window); } - public MonthlyProjectReportWindow openMonthlyProjectReport() + public MonthlyProjectReportWindow openMonthlyProjectReport(final YearMonth yearMonth) { robot.clickOn("#menu_reports"); robot.clickOn("#menuitem_monthly_project_report"); - final Window window = robot.window("Monthly Project Report"); + final Window window = robot.window("Monthly Project Report " + yearMonth); Assertions.assertThat(window).isShowing(); return new MonthlyProjectReportWindow(robot, window); } diff --git a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/model/JavaFxTable.java b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/model/JavaFxTable.java index 15ddc482..4e08a2eb 100644 --- a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/model/JavaFxTable.java +++ b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/model/JavaFxTable.java @@ -16,7 +16,9 @@ import org.testfx.assertions.api.Assertions; import javafx.css.PseudoClass; -import javafx.scene.control.*; +import javafx.scene.control.IndexedCell; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableView; import javafx.scene.control.skin.VirtualFlow; public class JavaFxTable @@ -112,7 +114,7 @@ public void assertContent(final TableRowExpectedContent expectedRowContent) public TableCell cell(final String columnId) { - LOG.debug("Getting row {} / column {}", rowIndex, columnId); + LOG.trace("Getting row {} / column {}", rowIndex, columnId); final IndexedCell row = tableRow(); return row.getChildrenUnmodifiable().stream() .filter(cell -> cell.getId().equals(columnId)) @@ -129,7 +131,7 @@ public IndexedCell tableRow() .findFirst().orElseThrow(); assertThat(virtualFlow.getCellCount()).as("row count of " + virtualFlow).isGreaterThan(rowIndex); final IndexedCell row = JavaFxUtil.runOnFxApplicationThread(() -> virtualFlow.getCell(rowIndex)); - LOG.debug("Got row #{} of {}: {}", rowIndex, virtualFlow, row); + LOG.trace("Got row #{} of {}: {}", rowIndex, virtualFlow, row); return row; } diff --git a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/model/MonthlyProjectReportWindow.java b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/model/MonthlyProjectReportWindow.java index deb0e135..fdd76415 100644 --- a/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/model/MonthlyProjectReportWindow.java +++ b/jfxui/src/uiTest/java/org/itsallcode/whiterabbit/jfxui/testutil/model/MonthlyProjectReportWindow.java @@ -5,18 +5,22 @@ import java.time.Duration; import org.itsallcode.whiterabbit.api.model.Project; +import org.itsallcode.whiterabbit.jfxui.testutil.TestUtil; +import org.itsallcode.whiterabbit.jfxui.ui.MonthlyProjectReportViewer; import org.itsallcode.whiterabbit.jfxui.ui.MonthlyProjectReportViewer.ReportRow; import org.testfx.api.FxRobot; import org.testfx.assertions.api.Assertions; import javafx.scene.input.KeyCode; +import javafx.stage.Stage; import javafx.stage.Window; public class MonthlyProjectReportWindow { private final FxRobot robot; private final Window window; - private final JavaFxTable table; + + private final JavaFxTable table; MonthlyProjectReportWindow(final FxRobot robot, final Window window) { @@ -38,6 +42,23 @@ public void closeViaEscKey() Assertions.assertThat(window).isNotShowing(); } + public void gotoPreviousMonth() + { + robot.clickOn("#prev-month-button"); + } + + public void gotoNextMonth() + { + robot.clickOn("#next-month-button"); + } + + public MonthlyProjectReportWindow assertWindowTitle(final String expectedTitle) + { + final String title = ((Stage) window).getTitle(); + TestUtil.retryAssertion(Duration.ofSeconds(5), () -> assertThat(title).isEqualTo(expectedTitle)); + return this; + } + public MonthlyProjectReportWindow assertRowCount(final int expectedRowCount) { assertThat(table.getRowCount()).isEqualTo(expectedRowCount);