diff --git a/.gitignore b/.gitignore index d831c00..4698d60 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ /.project /node_modules /webpack.generated.js +/webpack.config.js /node /package.json /package-lock.json +/frontend/index.html +/frontend/generated +/error-screenshots diff --git a/pom.xml b/pom.xml index 4e1e21f..478eb80 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ Simple Timer for Vaadin Flow - 14.11.12 + 14.11.13 1.8 1.8 UTF-8 @@ -143,6 +143,34 @@ jar test + + com.vaadin + vaadin-testbench + test + + + com.vaadin + license-checker + test + + + org.hamcrest + hamcrest-library + 1.3 + test + + + io.github.bonigarcia + webdrivermanager + 5.9.1 + test + + + com.flowingcode.vaadin.test + testbench-rpc + 1.3.0 + test + @@ -238,6 +266,89 @@ + + it + + + + org.eclipse.jetty + jetty-maven-plugin + ${jetty.version} + + 0 + + jar + + ${project.artifactId} + 8081 + + + + start-jetty + pre-integration-test + + start + + + + stop-jetty + post-integration-test + + stop + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.22.2 + + + + integration-test + verify + + + + + false + true + + + + ${webdriver.chrome.driver} + + + + + + maven-resources-plugin + 3.1.0 + + + + copy-test-to-classes + process-test-classes + + copy-resources + + + ${project.build.outputDirectory} + + + ${project.build.testOutputDirectory} + + + + + + + + + + directory diff --git a/src/main/java/com/flowingcode/vaadin/addons/simpletimer/SimpleTimer.java b/src/main/java/com/flowingcode/vaadin/addons/simpletimer/SimpleTimer.java index 45302ab..02a9b07 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/simpletimer/SimpleTimer.java +++ b/src/main/java/com/flowingcode/vaadin/addons/simpletimer/SimpleTimer.java @@ -109,6 +109,10 @@ public void setDoubleDigitHours(final boolean doubleDigitHours) { getElement().setProperty("doubleDigitHours", doubleDigitHours); } + public void setTargetId(final String targetId) { + getElement().setProperty("targetId", targetId); + } + /** Starts or stops the timer if it is already started */ public void start() { getElement().callJsFunction("start"); @@ -167,8 +171,8 @@ public CompletableFuture getCurrentTimeAsync() { * @return this registration, for chaining */ public Registration addCurrentTimeChangeListener( - PropertyChangeListener listener, long period, TimeUnit periodUnit) { - int millis = (int) Math.min(periodUnit.toMillis(period), Integer.MAX_VALUE); + PropertyChangeListener listener, final long period, final TimeUnit periodUnit) { + final int millis = (int) Math.min(periodUnit.toMillis(period), Integer.MAX_VALUE); if (listener == null) { listener = ev -> {}; } @@ -202,7 +206,7 @@ public boolean isVisible() { } @Override - public void setVisible(boolean visible) { + public void setVisible(final boolean visible) { getStyle().set(DISPLAY, visible ? INLINE : "none"); } } diff --git a/src/main/resources/META-INF/frontend/simple-timer/simple-timer.js b/src/main/resources/META-INF/frontend/simple-timer/simple-timer.js index 4965118..568035e 100644 --- a/src/main/resources/META-INF/frontend/simple-timer/simple-timer.js +++ b/src/main/resources/META-INF/frontend/simple-timer/simple-timer.js @@ -110,27 +110,30 @@ Polymer({ type: Boolean, value: false }, + targetId: { + type: String, + value: null, + }, /** * Time the timer has spent running since it was started */ - _elapsedTime: { + _elapsed: { type: Number, value: 0 }, _formattedTime: { type: String, - value: '0' + value: '0', + observer: "_updateTarget", } }, ready: function() { - if (this.countUp) { - this.set('currentTime', 0); - } else { - this.set('currentTime', this.startTime); - } - this.set('_formattedTime', this._formatTime(this.currentTime.toString())); + if (this.currentTime===undefined) { + this.set('currentTime', this.countUp ? 0 : this.startTime); + } + this.set('_formattedTime', this._formatTime(this.currentTime.toString())); }, start: function() { @@ -199,5 +202,10 @@ Polymer({ hours = hours.toString().padStart(2, '0'); } return (this.hours ? hours + ':' : '') + (this.minutes || this.hours ? minutes + ':' : '') + seconds + (this.fractions ? ('.' + timeString[1].substring(0,2)) : '') - } + }, + _updateTarget: function(newValue, oldValue){ + if (document.getElementById(this.targetId)) { + document.getElementById(this.targetId).innerText = newValue; + } + } }); \ No newline at end of file diff --git a/src/test/java/com/flowingcode/addons/simpletimer/integration/AbstractViewTest.java b/src/test/java/com/flowingcode/addons/simpletimer/integration/AbstractViewTest.java new file mode 100644 index 0000000..fff79d5 --- /dev/null +++ b/src/test/java/com/flowingcode/addons/simpletimer/integration/AbstractViewTest.java @@ -0,0 +1,106 @@ +/*- + * #%L + * Template Add-on + * %% + * Copyright (C) 2024 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +package com.flowingcode.addons.simpletimer.integration; + +import com.vaadin.testbench.ScreenshotOnFailureRule; +import com.vaadin.testbench.TestBench; +import com.vaadin.testbench.parallel.ParallelTest; +import io.github.bonigarcia.wdm.WebDriverManager; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.openqa.selenium.chrome.ChromeDriver; + +/** + * Base class for ITs + * + *

The tests use Chrome driver (see pom.xml for integration-tests profile) to run integration + * tests on a headless Chrome. If a property {@code test.use .hub} is set to true, {@code + * AbstractViewTest} will assume that the TestBench test is running in a CI environment. In order to + * keep the this class light, it makes certain assumptions about the CI environment (such as + * available environment variables). It is not advisable to use this class as a base class for you + * own TestBench tests. + * + *

To learn more about TestBench, visit Vaadin TestBench. + */ +public abstract class AbstractViewTest extends ParallelTest { + private static final int SERVER_PORT = 8080; + + private final String route; + + @Rule public ScreenshotOnFailureRule rule = new ScreenshotOnFailureRule(this, true); + + public AbstractViewTest() { + this(""); + } + + protected AbstractViewTest(String route) { + this.route = route; + } + + @BeforeClass + public static void setupClass() { + WebDriverManager.chromedriver().setup(); + } + + @Override + @Before + public void setup() throws Exception { + if (isUsingHub()) { + super.setup(); + } else { + setDriver(TestBench.createDriver(new ChromeDriver())); + } + getDriver().get(getURL(route)); + } + + /** + * Returns deployment host name concatenated with route. + * + * @return URL to route + */ + private static String getURL(String route) { + return String.format("http://%s:%d/%s", getDeploymentHostname(), SERVER_PORT, route); + } + + /** Property set to true when running on a test hub. */ + private static final String USE_HUB_PROPERTY = "test.use.hub"; + + /** + * Returns whether we are using a test hub. This means that the starter is running tests in + * Vaadin's CI environment, and uses TestBench to connect to the testing hub. + * + * @return whether we are using a test hub + */ + private static boolean isUsingHub() { + return Boolean.TRUE.toString().equals(System.getProperty(USE_HUB_PROPERTY)); + } + + /** + * If running on CI, get the host name from environment variable HOSTNAME + * + * @return the host name + */ + private static String getDeploymentHostname() { + return isUsingHub() ? System.getenv("HOSTNAME") : "localhost"; + } +} \ No newline at end of file diff --git a/src/test/java/com/flowingcode/addons/simpletimer/integration/IntegrationCallables.java b/src/test/java/com/flowingcode/addons/simpletimer/integration/IntegrationCallables.java new file mode 100644 index 0000000..5513764 --- /dev/null +++ b/src/test/java/com/flowingcode/addons/simpletimer/integration/IntegrationCallables.java @@ -0,0 +1,29 @@ +package com.flowingcode.addons.simpletimer.integration; + +public interface IntegrationCallables { + + void setStartTime(Integer startTime); + + void setEndTime(Integer endTime); + + void start(); + + void pause(); + + void reset(); + + boolean isRunning(); + + void openDialog(); + + void closeDialog(); + // BigDecimal getCurrentTime(); + // + // CompletableFuture getCurrentTimeAsync(); + // + // Registration addCurrentTimeChangeListener(PropertyChangeListener listener, long period, + // TimeUnit periodUnit); + // + // Registration addTimerEndEvent(ComponentEventListener listener); + +} diff --git a/src/test/java/com/flowingcode/addons/simpletimer/integration/IntegrationView.java b/src/test/java/com/flowingcode/addons/simpletimer/integration/IntegrationView.java new file mode 100644 index 0000000..60f7bcd --- /dev/null +++ b/src/test/java/com/flowingcode/addons/simpletimer/integration/IntegrationView.java @@ -0,0 +1,77 @@ +package com.flowingcode.addons.simpletimer.integration; + +import com.flowingcode.vaadin.addons.simpletimer.SimpleTimer; +import com.vaadin.flow.component.ClientCallable; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Label; +import com.vaadin.flow.router.Route; + +@Route("/it") +public class IntegrationView extends Div implements IntegrationCallables { + + private final SimpleTimer timer = new SimpleTimer(); + + private Dialog dialog; + + private final Label timerTarget; + + public IntegrationView() { + add(timer); + timerTarget = new Label(); + timerTarget.setId("timerTarget"); + timer.setTargetId("timerTarget"); + } + + @Override + @ClientCallable + public void openDialog() { + if (dialog == null) { + dialog = new Dialog(timerTarget); + } + dialog.open(); + } + + @Override + @ClientCallable + public void closeDialog() { + dialog.close(); + } + + @Override + @ClientCallable + public void setStartTime(final Integer startTime) { + timer.setStartTime(startTime); + } + + @Override + @ClientCallable + public void setEndTime(final Integer endTime) { + timer.setEndTime(endTime); + } + + @Override + @ClientCallable + public void start() { + timer.start(); + } + + @Override + @ClientCallable + public void pause() { + timer.pause(); + } + + @Override + @ClientCallable + public void reset() { + timer.reset(); + } + + @Override + @ClientCallable + public boolean isRunning() { + return timer.isRunning(); + } + +} diff --git a/src/test/java/com/flowingcode/addons/simpletimer/integration/SimpleIT.java b/src/test/java/com/flowingcode/addons/simpletimer/integration/SimpleIT.java new file mode 100644 index 0000000..e9fae66 --- /dev/null +++ b/src/test/java/com/flowingcode/addons/simpletimer/integration/SimpleIT.java @@ -0,0 +1,90 @@ +package com.flowingcode.addons.simpletimer.integration; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import com.flowingcode.vaadin.testbench.rpc.HasRpcSupport; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class SimpleIT extends AbstractViewTest implements HasRpcSupport { + + IntegrationCallables $server = createCallableProxy(IntegrationCallables.class); + + public SimpleIT() { + super("it"); + } + + private static void sleep(final long millis) { + try { + Thread.sleep(millis); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private Double currentTime() { + return $(SimpleTimerElement.class).first().currentTime(); + } + + @Test + public void countDown() { + assertThat(currentTime(), nullValue()); + assertFalse($server.isRunning()); + + $server.setStartTime(1); + assertThat(currentTime(), equalTo(1.0)); + + $server.start(); + assertTrue($server.isRunning()); + final double t0 = currentTime(); + final double t1 = currentTime(); + assertThat(t0, lessThan(1.0)); + assertThat(t1, lessThan(t0)); + } + + @Test + public void countUp() { + assertThat(currentTime(), nullValue()); + assertFalse($server.isRunning()); + + $server.setEndTime(1); + assertThat(currentTime(), equalTo(0.0)); + + $server.start(); + assertTrue($server.isRunning()); + final double t0 = currentTime(); + final double t1 = currentTime(); + assertThat(t0, greaterThan(0.0)); + assertThat(t1, greaterThan(t0)); + } + + @Test + public void countUpInDialog() { + $server.openDialog(); + $server.setEndTime(100); + assertThat(currentTime(), equalTo(0.0)); + + final long w0 = System.nanoTime(); + $server.start(); + final double t0 = currentTime(); + assertThat(t0, greaterThan(0.0)); + final long w1 = System.nanoTime(); + + $server.closeDialog(); + sleep(0); + $server.openDialog(); + + final long w2 = System.nanoTime(); + // double delta = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - now) / 1000.0; + final double t1 = currentTime(); + final long w3 = System.nanoTime(); + // assertThat(t1, greaterThan(t0 + delta)); + System.out.println(TimeUnit.NANOSECONDS.toMillis(w3 - w0) / 1000.0); + System.out.println(TimeUnit.NANOSECONDS.toMillis(w2 - w1) / 1000.0); + System.out.println(t1 - t0); + System.out.println(t0); + System.out.println(t1); + } + +} diff --git a/src/test/java/com/flowingcode/addons/simpletimer/integration/SimpleTimerElement.java b/src/test/java/com/flowingcode/addons/simpletimer/integration/SimpleTimerElement.java new file mode 100644 index 0000000..a84b5b2 --- /dev/null +++ b/src/test/java/com/flowingcode/addons/simpletimer/integration/SimpleTimerElement.java @@ -0,0 +1,15 @@ +package com.flowingcode.addons.simpletimer.integration; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.elementsbase.Element; +import java.util.Optional; + +@Element("simple-timer") +public class SimpleTimerElement extends TestBenchElement { + + Double currentTime() { + Number time = (Number) executeScript("return arguments[0].currentTime", this); + return Optional.ofNullable(time).map(Number::doubleValue).orElse(null); + } + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/simpletimer/SimpletimerDemo.java b/src/test/java/com/flowingcode/vaadin/addons/simpletimer/SimpletimerDemo.java index adb133a..4be6692 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/simpletimer/SimpletimerDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/simpletimer/SimpletimerDemo.java @@ -75,7 +75,7 @@ public SimpletimerDemo() { } update(); }); - final Button start = new Button("Start/Stop", e -> timer.start()); + final Button start = new Button("Start", e -> timer.start()); final Button stop = new Button("Stop", e -> timer.pause()); final Button reset = new Button( diff --git a/src/test/java/com/flowingcode/vaadin/addons/simpletimer/SimpletimerDemo2.java b/src/test/java/com/flowingcode/vaadin/addons/simpletimer/SimpletimerDemo2.java new file mode 100644 index 0000000..c6a3ba5 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/simpletimer/SimpletimerDemo2.java @@ -0,0 +1,144 @@ +/*- + * #%L + * Simple Timer Addon + * %% + * Copyright (C) 2019 - 2020 Flowing Code + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.flowingcode.vaadin.addons.simpletimer; + +import com.flowingcode.vaadin.addons.demo.DemoSource; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.dialog.Dialog; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Label; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import java.math.BigDecimal; + +@SuppressWarnings("serial") +@PageTitle("Simple Timer Demo") +@DemoSource +@Route(value = "simple-timer/simple-timer2", layout = SimpletimerDemoView.class) +public class SimpletimerDemo2 extends Div { + + private final SimpleTimer timer = new SimpleTimer(); + + private boolean countUpMode; + private BigDecimal time = new BigDecimal(60); + + public SimpletimerDemo2() { + setSizeFull(); + timer.setWidth("100px"); + timer.setHeight("50px"); + timer.getStyle().set("font-size", "40px"); + timer.setStartTime(60); + + final Span timerTitle = new Span("Simple Count Up Timer"); + + final TextField startTime = + new TextField("Start Time", e -> { + time = new BigDecimal(e.getValue()); + update(); + }); + final Checkbox countUp = new Checkbox("Count Up", false); + countUp.addValueChangeListener( + e -> { + countUpMode = countUp.getValue(); + if (countUpMode) { + startTime.setLabel("End Time"); + timerTitle.setText("Simple Count Up Timer"); + } else { + startTime.setLabel("Start Time"); + timerTitle.setText("Simple Countdown Timer"); + } + update(); + }); + final Button start = new Button("Start/Stop", e -> timer.start()); + final Button stop = new Button("Stop", e -> timer.pause()); + final Button reset = + new Button( + "Reset", + e -> { + timer.reset(); + }); + final Button running = + new Button( + "Current Time", + e -> + timer + .getCurrentTimeAsync() + .thenAccept( + time -> + Notification.show( + time.toPlainString() + + (timer.isRunning() ? "" : " (Not Running)")))); + final Checkbox fractions = new Checkbox("Fractions", true); + fractions.addValueChangeListener(e -> timer.setFractions(e.getValue())); + final Checkbox minutes = new Checkbox("Minutes", e -> timer.setMinutes(e.getValue())); + final Checkbox hours = new Checkbox("Hours", e -> timer.setHours(e.getValue())); + final Checkbox doubleDigitHours = + new Checkbox("Double digit hours", e -> timer.setDoubleDigitHours(e.getValue())); + final Checkbox visible = + new Checkbox( + "Visible", + e -> { + if (e.isFromClient()) { + timer.setVisible(!timer.isVisible()); + } + }); + visible.setValue(true); + + timer.addTimerEndEvent(e -> Notification.show("Timer Ended")); + + timer.setVisible(false); + final HorizontalLayout topLayout = new HorizontalLayout(timerTitle, timer); + topLayout.setAlignItems(Alignment.CENTER); + + final HorizontalLayout options = + new HorizontalLayout(countUp, fractions, minutes, hours, visible, doubleDigitHours); + options.setAlignItems(Alignment.CENTER); + options.getStyle().set("flex-wrap", "wrap"); + + final HorizontalLayout bottomLayout = new HorizontalLayout(start, stop, reset, running); + bottomLayout.setAlignItems(Alignment.BASELINE); + + add(new VerticalLayout(topLayout, startTime, options, bottomLayout)); + + final Label timerTarget = new Label(); + timerTarget.setId("timerTarget"); + + final Dialog dlg = new Dialog(timerTarget); + dlg.setModal(false); + dlg.setCloseOnOutsideClick(false); + dlg.add(new Button("Close", ev -> dlg.close())); + add(new Button("OPEN", ev -> dlg.open())); + } + + private void update() { + if (countUpMode) { + timer.setEndTime(time); + } else { + timer.setStartTime(time); + } + } +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/simpletimer/SimpletimerDemoView.java b/src/test/java/com/flowingcode/vaadin/addons/simpletimer/SimpletimerDemoView.java index 9367ae6..30549be 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/simpletimer/SimpletimerDemoView.java +++ b/src/test/java/com/flowingcode/vaadin/addons/simpletimer/SimpletimerDemoView.java @@ -33,6 +33,7 @@ public class SimpletimerDemoView extends TabbedDemo { public SimpletimerDemoView() { addDemo(SimpletimerDemo.class); + addDemo(SimpletimerDemo2.class); setSizeFull(); } } diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 4e575ad..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * This file has been autogenerated as it didn't exist or was made for an older incompatible version. - * This file can be used for manual configuration will not be modified if the flowDefaults constant exists. - */ -const merge = require('webpack-merge'); -const flowDefaults = require('./webpack.generated.js'); - -module.exports = merge(flowDefaults, { - -}); - -/** - * This file can be used to configure the flow plugin defaults. - * - * // Add a custom plugin - * flowDefaults.plugins.push(new MyPlugin()); - * - * // Update the rules to also transpile `.mjs` files - * if (!flowDefaults.module.rules[0].test) { - * throw "Unexpected structure in generated webpack config"; - * } - * flowDefaults.module.rules[0].test = /\.m?js$/ - * - * // Include a custom JS in the entry point in addition to generated-flow-imports.js - * if (typeof flowDefaults.entry.index != "string") { - * throw "Unexpected structure in generated webpack config"; - * } - * flowDefaults.entry.index = [flowDefaults.entry.index, "myCustomFile.js"]; - * - * or add new configuration in the merge block. - * - * module.exports = merge(flowDefaults, { - * mode: 'development', - * devtool: 'inline-source-map' - * }); - * - */