diff --git a/vaadin-testbench-core-junit5/src/test/java/com/vaadin/testbench/commands/TestBenchCommandExecutorTest.java b/vaadin-testbench-core-junit5/src/test/java/com/vaadin/testbench/commands/TestBenchCommandExecutorTest.java index 188865b82..289fedfa3 100644 --- a/vaadin-testbench-core-junit5/src/test/java/com/vaadin/testbench/commands/TestBenchCommandExecutorTest.java +++ b/vaadin-testbench-core-junit5/src/test/java/com/vaadin/testbench/commands/TestBenchCommandExecutorTest.java @@ -11,6 +11,7 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.time.Duration; import java.util.Arrays; import org.junit.jupiter.api.Assertions; @@ -213,9 +214,7 @@ private WebDriver mockScreenshotDriver(int nrScreenshotsGrabbed, "cursor-bottom-edge-off.png"); Mockito.when(driver.getScreenshotAs(OutputType.BYTES)) .thenReturn(screenshotBytes); - Mockito.when( - driver.executeScript(Mockito.contains("window.Vaadin.Flow"))) - .thenReturn(Boolean.TRUE); + mockWaitForVaadin(driver); if (expectGetCapabilities) { Capabilities mockedCapabilities = Mockito.mock(Capabilities.class); Mockito.when(mockedCapabilities.getBrowserName()) @@ -283,6 +282,7 @@ public void testTotalTimeSpentServicingRequests() { private FirefoxDriver mockJSExecutor(boolean forcesSync) { FirefoxDriver jse = Mockito.mock(FirefoxDriver.class); + mockWaitForVaadin(jse); Mockito.when(jse .executeScript(Mockito.contains("window.Vaadin.Flow.client"))) .thenReturn(Boolean.TRUE); @@ -290,4 +290,20 @@ private FirefoxDriver mockJSExecutor(boolean forcesSync) { .thenReturn(Arrays.asList(1000L, 2000L, 3000L)); return jse; } + + private void mockWaitForVaadin(RemoteWebDriver driver) { + WebDriver.Options options = Mockito.mock(WebDriver.Options.class); + WebDriver.Timeouts timeouts = Mockito.mock(WebDriver.Timeouts.class); + Mockito.when(driver.manage()).thenReturn(options); + Mockito.when(options.timeouts()).thenReturn(timeouts); + Mockito.when(timeouts.getScriptTimeout()) + .thenReturn(Duration.ofSeconds(30)); + Mockito.when(timeouts.scriptTimeout(Mockito.any(Duration.class))) + .thenReturn(timeouts); + // Phase 1: sync check returns true immediately + Mockito.when(driver.executeScript(Mockito.contains("whenReady"))) + .thenReturn(Boolean.TRUE); + Mockito.when(driver.executeAsyncScript(Mockito.anyString())) + .thenReturn(null); + } } diff --git a/vaadin-testbench-core/src/test/java/com/vaadin/testbench/commands/TestBenchCommandExecutorTest.java b/vaadin-testbench-core/src/test/java/com/vaadin/testbench/commands/TestBenchCommandExecutorTest.java index 68ea1514b..67581644c 100644 --- a/vaadin-testbench-core/src/test/java/com/vaadin/testbench/commands/TestBenchCommandExecutorTest.java +++ b/vaadin-testbench-core/src/test/java/com/vaadin/testbench/commands/TestBenchCommandExecutorTest.java @@ -11,6 +11,7 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.time.Duration; import java.util.Arrays; import org.junit.Before; @@ -216,9 +217,7 @@ private WebDriver mockScreenshotDriver(int nrScreenshotsGrabbed, "cursor-bottom-edge-off.png"); Mockito.when(driver.getScreenshotAs(OutputType.BYTES)) .thenReturn(screenshotBytes); - Mockito.when( - driver.executeScript(Mockito.contains("window.Vaadin.Flow"))) - .thenReturn(Boolean.TRUE); + mockWaitForVaadin(driver); if (expectGetCapabilities) { Capabilities mockedCapabilities = Mockito.mock(Capabilities.class); Mockito.when(mockedCapabilities.getBrowserName()) @@ -286,6 +285,7 @@ public void testTotalTimeSpentServicingRequests() { private FirefoxDriver mockJSExecutor(boolean forcesSync) { FirefoxDriver jse = Mockito.mock(FirefoxDriver.class); + mockWaitForVaadin(jse); Mockito.when(jse .executeScript(Mockito.contains("window.Vaadin.Flow.client"))) .thenReturn(Boolean.TRUE); @@ -293,4 +293,20 @@ private FirefoxDriver mockJSExecutor(boolean forcesSync) { .thenReturn(Arrays.asList(1000L, 2000L, 3000L)); return jse; } + + private void mockWaitForVaadin(RemoteWebDriver driver) { + WebDriver.Options options = Mockito.mock(WebDriver.Options.class); + WebDriver.Timeouts timeouts = Mockito.mock(WebDriver.Timeouts.class); + Mockito.when(driver.manage()).thenReturn(options); + Mockito.when(options.timeouts()).thenReturn(timeouts); + Mockito.when(timeouts.getScriptTimeout()) + .thenReturn(Duration.ofSeconds(30)); + Mockito.when(timeouts.scriptTimeout(Mockito.any(Duration.class))) + .thenReturn(timeouts); + // Phase 1: sync check returns true immediately + Mockito.when(driver.executeScript(Mockito.contains("whenReady"))) + .thenReturn(Boolean.TRUE); + Mockito.when(driver.executeAsyncScript(Mockito.anyString())) + .thenReturn(null); + } } diff --git a/vaadin-testbench-integration-tests-junit5/src/test/java/com/vaadin/tests/WaitForVaadinIT.java b/vaadin-testbench-integration-tests-junit5/src/test/java/com/vaadin/tests/WaitForVaadinIT.java index 75d0ff20c..3460027ba 100644 --- a/vaadin-testbench-integration-tests-junit5/src/test/java/com/vaadin/tests/WaitForVaadinIT.java +++ b/vaadin-testbench-integration-tests-junit5/src/test/java/com/vaadin/tests/WaitForVaadinIT.java @@ -8,10 +8,7 @@ */ package com.vaadin.tests; -import java.lang.reflect.Field; - import org.junit.jupiter.api.Assertions; -import org.openqa.selenium.JavascriptExecutor; import com.vaadin.flow.component.Component; import com.vaadin.testUI.PageObjectView; @@ -44,11 +41,11 @@ public void waitForVaadin_activeConnector_waits() { @BrowserTest public void waitForVaadin_activeConnector_waitsUtilReady() { openTestURL(); - assertDevServerIsNotLoaded(); getCommandExecutor().executeScript( - "window.Vaadin.Flow.clients[\"blocker\"] = {isActive: () => true};"); - setWaitForVaadinLoopHook(500, - "window.Vaadin.Flow.clients[\"blocker\"] = {isActive: () => false};"); + "window.Vaadin.Flow.clients[\"blocker\"] = {isActive: () => true};" + + "setTimeout(function() {" + + " window.Vaadin.Flow.clients[\"blocker\"] = {isActive: () => false};" + + "}, 500);"); getCommandExecutor().waitForVaadin(); assertClientIsActive(); } @@ -61,41 +58,24 @@ public void waitForVaadin_noConnectors_returnsImmediately() { assertExecutionNoLonger(() -> getCommandExecutor().waitForVaadin()); } - @BrowserTest - public void waitForVaadin_noFlow_returnsImmediately() { - openTestURL(); - - getCommandExecutor().executeScript("window.Vaadin.Flow = undefined;"); - assertExecutionNoLonger(() -> getCommandExecutor().waitForVaadin()); - } - @BrowserTest public void waitForVaadin_devModeNotReady_waits() { openTestURL(); - getCommandExecutor().executeScript( - "window.Vaadin = {Flow: {devServerIsNotLoaded: true}};"); + getCommandExecutor() + .executeScript("window.Vaadin.Flow.whenReady = false;"); assertExecutionBlocked(() -> getCommandExecutor().waitForVaadin()); } @BrowserTest public void waitForVaadin_devModeNotReady_waitsUntilReady() { openTestURL(); - assertDevServerIsNotLoaded(); getCommandExecutor().executeScript( - "window.Vaadin = {Flow: {devServerIsNotLoaded: true}};"); - setWaitForVaadinLoopHook(500, - "window.Vaadin.Flow.devServerIsNotLoaded = false;"); + "window._savedWhenReady = window.Vaadin.Flow.whenReady;" + + "window.Vaadin.Flow.whenReady = false;" + + "setTimeout(function() {" + + " window.Vaadin.Flow.whenReady = window._savedWhenReady;" + + "}, 500);"); getCommandExecutor().waitForVaadin(); - assertDevServerIsNotLoaded(); - } - - private void assertDevServerIsNotLoaded() { - Object devServerIsNotLoaded = executeScript( - "return window.Vaadin.Flow.devServerIsNotLoaded;"); - Assertions.assertTrue( - devServerIsNotLoaded == null - || devServerIsNotLoaded == Boolean.FALSE, - "devServerIsNotLoaded should be null or false"); } private void assertClientIsActive() { @@ -123,28 +103,4 @@ private void assertExecutionBlocked(Runnable command) { "Unexpected execution time, waiting time = " + timeout); } - private void setWaitForVaadinLoopHook(long timeout, - String scriptToRunAfterTimeout) { - long systemCurrentTimeMillis = System.currentTimeMillis(); - setWaitForVaadinLoopHook(() -> { - if (System.currentTimeMillis() - - systemCurrentTimeMillis > timeout) { - ((JavascriptExecutor) getCommandExecutor().getDriver() - .getWrappedDriver()) - .executeScript(scriptToRunAfterTimeout); - setWaitForVaadinLoopHook(null); - } - }); - } - - private void setWaitForVaadinLoopHook(Runnable action) { - try { - Field field = getCommandExecutor().getClass() - .getDeclaredField("waitForVaadinLoopHook"); - field.setAccessible(true); - field.set(getCommandExecutor(), action); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } } diff --git a/vaadin-testbench-integration-tests/src/test/java/com/vaadin/tests/WaitForVaadinIT.java b/vaadin-testbench-integration-tests/src/test/java/com/vaadin/tests/WaitForVaadinIT.java index 7dbb88a5e..f45e5d223 100644 --- a/vaadin-testbench-integration-tests/src/test/java/com/vaadin/tests/WaitForVaadinIT.java +++ b/vaadin-testbench-integration-tests/src/test/java/com/vaadin/tests/WaitForVaadinIT.java @@ -8,11 +8,8 @@ */ package com.vaadin.tests; -import java.lang.reflect.Field; - import org.junit.Assert; import org.junit.Test; -import org.openqa.selenium.JavascriptExecutor; import com.vaadin.flow.component.Component; import com.vaadin.testUI.PageObjectView; @@ -44,11 +41,11 @@ public void waitForVaadin_activeConnector_waits() { @Test public void waitForVaadin_activeConnector_waitsUtilReady() { openTestURL(); - assertDevServerIsNotLoaded(); getCommandExecutor().executeScript( - "window.Vaadin.Flow.clients[\"blocker\"] = {isActive: () => true};"); - setWaitForVaadinLoopHook(500, - "window.Vaadin.Flow.clients[\"blocker\"] = {isActive: () => false};"); + "window.Vaadin.Flow.clients[\"blocker\"] = {isActive: () => true};" + + "setTimeout(function() {" + + " window.Vaadin.Flow.clients[\"blocker\"] = {isActive: () => false};" + + "}, 500);"); getCommandExecutor().waitForVaadin(); assertClientIsActive(); } @@ -61,41 +58,24 @@ public void waitForVaadin_noConnectors_returnsImmediately() { assertExecutionNoLonger(() -> getCommandExecutor().waitForVaadin()); } - @Test - public void waitForVaadin_noFlow_returnsImmediately() { - openTestURL(); - - getCommandExecutor().executeScript("window.Vaadin.Flow = undefined;"); - assertExecutionNoLonger(() -> getCommandExecutor().waitForVaadin()); - } - @Test public void waitForVaadin_devModeNotReady_waits() { openTestURL(); - - getCommandExecutor().executeScript( - "window.Vaadin = {Flow: {devServerIsNotLoaded: true}};"); + getCommandExecutor() + .executeScript("window.Vaadin.Flow.whenReady = false;"); assertExecutionBlocked(() -> getCommandExecutor().waitForVaadin()); } @Test public void waitForVaadin_devModeNotReady_waitsUntilReady() { openTestURL(); - assertDevServerIsNotLoaded(); getCommandExecutor().executeScript( - "window.Vaadin = {Flow: {devServerIsNotLoaded: true}};"); - setWaitForVaadinLoopHook(500, - "window.Vaadin.Flow.devServerIsNotLoaded = false;"); + "window._savedWhenReady = window.Vaadin.Flow.whenReady;" + + "window.Vaadin.Flow.whenReady = false;" + + "setTimeout(function() {" + + " window.Vaadin.Flow.whenReady = window._savedWhenReady;" + + "}, 500);"); getCommandExecutor().waitForVaadin(); - assertDevServerIsNotLoaded(); - } - - private void assertDevServerIsNotLoaded() { - Object devServerIsNotLoaded = executeScript( - "return window.Vaadin.Flow.devServerIsNotLoaded;"); - Assert.assertTrue("devServerIsNotLoaded should be null or false", - devServerIsNotLoaded == null - || devServerIsNotLoaded == Boolean.FALSE); } private void assertClientIsActive() { @@ -125,28 +105,4 @@ private void assertExecutionBlocked(Runnable command) { timeout >= BLOCKING_EXECUTION_TIMEOUT); } - private void setWaitForVaadinLoopHook(long timeout, - String scriptToRunAfterTimeout) { - long systemCurrentTimeMillis = System.currentTimeMillis(); - setWaitForVaadinLoopHook(() -> { - if (System.currentTimeMillis() - - systemCurrentTimeMillis > timeout) { - ((JavascriptExecutor) getCommandExecutor().getDriver() - .getWrappedDriver()) - .executeScript(scriptToRunAfterTimeout); - setWaitForVaadinLoopHook(null); - } - }); - } - - private void setWaitForVaadinLoopHook(Runnable action) { - try { - Field field = getCommandExecutor().getClass() - .getDeclaredField("waitForVaadinLoopHook"); - field.setAccessible(true); - field.set(getCommandExecutor(), action); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } } diff --git a/vaadin-testbench-shared/src/main/java/com/vaadin/testbench/commands/TestBenchCommandExecutor.java b/vaadin-testbench-shared/src/main/java/com/vaadin/testbench/commands/TestBenchCommandExecutor.java index 6f7327d61..32aff2314 100644 --- a/vaadin-testbench-shared/src/main/java/com/vaadin/testbench/commands/TestBenchCommandExecutor.java +++ b/vaadin-testbench-shared/src/main/java/com/vaadin/testbench/commands/TestBenchCommandExecutor.java @@ -13,12 +13,14 @@ import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; +import java.time.Duration; import java.util.List; import org.openqa.selenium.Dimension; import org.openqa.selenium.HasCapabilities; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.Point; +import org.openqa.selenium.ScriptTimeoutException; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.HttpCommandExecutor; @@ -48,27 +50,16 @@ private static Logger getLogger() { private boolean enableWaitForVaadin = true; private boolean autoScrollIntoView = true; // @formatter:off - String WAIT_FOR_VAADIN_SCRIPT = - "if (document.readyState != 'complete') {" - + " return false;" - + "}" - + "if (window.Vaadin && window.Vaadin.Flow && window.Vaadin.Flow.devServerIsNotLoaded) {" - + " return false;" - + "} else if (window.Vaadin && window.Vaadin.Flow && window.Vaadin.Flow.clients) {" - + " var clients = window.Vaadin.Flow.clients;" - + " for (var client in clients) {" - + " if (clients[client].isActive()) {" - + " return false;" - + " }" - + " }" - + " return true;" - + "} else {" - + " return true;" - + "}"; + private static final String WHEN_READY_CHECK_SCRIPT = + "return typeof window.Vaadin !== 'undefined'" + + " && typeof window.Vaadin.Flow !== 'undefined'" + + " && typeof window.Vaadin.Flow.whenReady === 'function'"; + + private static final String WAIT_FOR_VAADIN_ASYNC_SCRIPT = + "var callback = arguments[arguments.length - 1];" + + "window.Vaadin.Flow.whenReady(callback);"; // @formatter:on - - // A hook for testing purposes - private Runnable waitForVaadinLoopHook; + private static final long WAIT_FOR_VAADIN_TIMEOUT_MS = 40000; public TestBenchCommandExecutor(ImageComparison imageComparison, ReferenceNameGenerator referenceNameGenerator) { @@ -109,31 +100,75 @@ public String getRemoteControlName() { /** * Block until Vaadin reports it has finished processing server messages. + *

+ * First waits for the dev server to start (if needed), then makes a single + * async call to {@code Flow.whenReady} which handles all remaining + * readiness checks. */ public void waitForVaadin() { if (!enableWaitForVaadin) { - // wait for vaadin is disabled, just return. return; } - long timeoutTime = System.currentTimeMillis() + 40000; - Boolean finished = false; - while (System.currentTimeMillis() < timeoutTime && !finished) { - if (waitForVaadinLoopHook != null) { - waitForVaadinLoopHook.run(); + // Must use the wrapped driver here to avoid calling waitForVaadin + // again + WebDriver wrappedDriver = getDriver().getWrappedDriver(); + long deadline = System.currentTimeMillis() + WAIT_FOR_VAADIN_TIMEOUT_MS; + + if (!waitForDevServer(wrappedDriver, deadline)) { + return; + } + + // Single async call — whenReady handles all readiness checks + Duration originalTimeout = wrappedDriver.manage().timeouts() + .getScriptTimeout(); + try { + long remaining = deadline - System.currentTimeMillis(); + if (remaining <= 0) { + return; + } + wrappedDriver.manage().timeouts() + .scriptTimeout(Duration.ofMillis(remaining)); + ((JavascriptExecutor) wrappedDriver) + .executeAsyncScript(WAIT_FOR_VAADIN_ASYNC_SCRIPT); + } catch (ScriptTimeoutException e) { + // Silent timeout + } finally { + wrappedDriver.manage().timeouts().scriptTimeout(originalTimeout); + } + } + + /** + * Polls until {@code Vaadin.Flow.whenReady} is a function, indicating the + * dev server has started and Flow is loaded. Uses Java-side polling so it + * survives page reloads during dev server startup. + * + * @return {@code true} if whenReady became available, {@code false} if the + * deadline was reached + */ + private boolean waitForDevServer(WebDriver wrappedDriver, long deadline) { + while (System.currentTimeMillis() < deadline) { + try { + Boolean ready = (Boolean) ((JavascriptExecutor) wrappedDriver) + .executeScript(WHEN_READY_CHECK_SCRIPT); + if (Boolean.TRUE.equals(ready)) { + return true; + } + } catch (Exception e) { + // Page may be reloading, continue polling + } + long remaining = deadline - System.currentTimeMillis(); + if (remaining <= 0) { + return false; } - // Must use the wrapped driver here to avoid calling waitForVaadin - // again - finished = (Boolean) ((JavascriptExecutor) getDriver() - .getWrappedDriver()).executeScript(WAIT_FOR_VAADIN_SCRIPT); - if (finished == null) { - // This should never happen but according to - // https://dev.vaadin.com/ticket/19703, it happens - getLogger().debug( - "waitForVaadin returned null, this should never happen"); - finished = false; + try { + Thread.sleep(Math.min(500, remaining)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; } } + return false; } @Override