diff --git a/flow-client/src/test/frontend/FlowTests.ts b/flow-client/src/test/frontend/FlowTests.ts index 8f19e326138..d874a75ef44 100644 --- a/flow-client/src/test/frontend/FlowTests.ts +++ b/flow-client/src/test/frontend/FlowTests.ts @@ -14,6 +14,21 @@ import sinon from 'sinon'; const $wnd = window as any; const flowRoot = window.document.body as any; +/** + * Stubs window.Vaadin.Flow.ready and whenReady with sentinel values so the + * preservation test can verify that constructing a Flow instance does not + * overwrite definitions from the inline bootstrap script. The real + * implementations live in flow-server's whenReady.js and are not tested here. + */ +const READY_SENTINEL = () => {}; +const WHEN_READY_SENTINEL = () => {}; +function stubWhenReady() { + $wnd.Vaadin = $wnd.Vaadin || {}; + $wnd.Vaadin.Flow = $wnd.Vaadin.Flow || {}; + $wnd.Vaadin.Flow.ready = READY_SENTINEL; + $wnd.Vaadin.Flow.whenReady = WHEN_READY_SENTINEL; +} + const stubVaadinPushSrc = '/src/test/frontend/stubVaadinPush.js'; let server: MockXhrServer; // A `changes` array that adds a div with 'Foo' text to body @@ -657,6 +672,13 @@ describe('Flow', () => { expect($wnd.Vaadin.connectionState.state).to.equal(ConnectionState.RECONNECTING); }); + it('should preserve ready and whenReady defined by inline script after construction', () => { + stubWhenReady(); + new Flow({ imports: () => {} }); + expect($wnd.Vaadin.Flow.ready).to.equal(READY_SENTINEL); + expect($wnd.Vaadin.Flow.whenReady).to.equal(WHEN_READY_SENTINEL); + }); + it('should pre-attach container element on every navigation', async () => { stubServerRemoteFunction('foobar-12345'); mockInitResponse('foobar-12345'); diff --git a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java index 3df1c010367..1860bd0806e 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/BootstrapHandler.java @@ -133,6 +133,7 @@ public class BootstrapHandler extends SynchronizedRequestHandler { + "/client.nocache.js"; private static final String BOOTSTRAP_JS = readResource( "BootstrapHandler.js"); + public static final String WHEN_READY_JS = readResource("whenReady.js"); private static final String CSS_TYPE_ATTRIBUTE_VALUE = "text/css"; private static final String CAPTION = "caption"; @@ -1194,6 +1195,7 @@ private String getBootstrapJS(ObjectNode initialUIDL, result = result.replace("{{GWT_STAT_EVENTS}}", ""); } + result = result.replace("{{WHEN_READY}}", WHEN_READY_JS); result = result.replace("{{APP_ID}}", context.getAppId()); result = result.replace("{{CONFIG_JSON}}", appConfigString); // {{INITIAL_UIDL}} should be the last replaced so that it may have diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java index e33908e7485..35b6ac57e7b 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java @@ -583,8 +583,10 @@ private void addInitialFlow(ObjectNode initialJson, Document indexDocument, indexDocument.head(), request); Element elm = new Element(SCRIPT); elm.attr(SCRIPT_INITIAL, ""); - elm.appendChild(new DataNode("window.Vaadin = window.Vaadin || {};" + // - "window.Vaadin.TypeScript= " + initialJson.toString() + ";")); + elm.appendChild(new DataNode("window.Vaadin = window.Vaadin || {};" + + "window.Vaadin.Flow = window.Vaadin.Flow || {};" + + BootstrapHandler.WHEN_READY_JS + "window.Vaadin.TypeScript= " + + initialJson.toString() + ";")); indexDocument.head().insertChildren(0, elm); } diff --git a/flow-server/src/main/resources/com/vaadin/flow/server/BootstrapHandler.js b/flow-server/src/main/resources/com/vaadin/flow/server/BootstrapHandler.js index e45a36b9250..3705a873a84 100644 --- a/flow-server/src/main/resources/com/vaadin/flow/server/BootstrapHandler.js +++ b/flow-server/src/main/resources/com/vaadin/flow/server/BootstrapHandler.js @@ -42,6 +42,10 @@ window.Vaadin = window.Vaadin || {}; window.Vaadin.Flow = window.Vaadin.Flow || {}; + if (!window.Vaadin.Flow.whenReady) { + {{WHEN_READY}} + } + /** * Triggers a CSS animation on an element by adding a class, then * removes the class when the animation ends. diff --git a/flow-server/src/main/resources/com/vaadin/flow/server/whenReady.js b/flow-server/src/main/resources/com/vaadin/flow/server/whenReady.js new file mode 100644 index 00000000000..fd933d07d3e --- /dev/null +++ b/flow-server/src/main/resources/com/vaadin/flow/server/whenReady.js @@ -0,0 +1,28 @@ +window.Vaadin.Flow.ready = async function ({ timeout = 30000 } = {}) { + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + const deadline = Date.now() + timeout; + + const isIdle = () => { + if (document.readyState !== 'complete') return false; + if (window.Vaadin.Flow.devServerIsNotLoaded) return false; + const clients = window.Vaadin.Flow.clients; + if (!clients) return false; + // Flow has not bootstrapped until at least one client with isActive is registered + const probes = Object.values(clients).filter((c) => typeof c.isActive === 'function'); + if (probes.length === 0) return false; + return probes.every((c) => !c.isActive()); + }; + + while (!isIdle()) { + if (Date.now() >= deadline) { + throw new Error('Vaadin.Flow.ready timed out after ' + timeout + 'ms'); + } + await sleep(50); + } +}; + +window.Vaadin.Flow.whenReady = function (callback) { + window.Vaadin.Flow.ready() + .catch((e) => console.warn(e.message)) + .then(callback); +}; diff --git a/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java index 86c5dfff8cc..cb7c4d44b27 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java @@ -397,9 +397,13 @@ public void should_not_add_initialUidl_when_not_includeInitialBootstrapUidl() Element initialUidlScript = findScript(scripts, INITIAL_UIDL_SEARCH_STRING); - assertEquals( - "window.Vaadin = window.Vaadin || {};window.Vaadin.TypeScript= {};", - initialUidlScript.childNode(0).toString()); + String scriptContent = initialUidlScript.childNode(0).toString(); + assertTrue( + scriptContent.startsWith("window.Vaadin = window.Vaadin || {};" + + "window.Vaadin.Flow = window.Vaadin.Flow || {};" + + "window.Vaadin.Flow.ready = ")); + assertTrue(scriptContent.contains("window.Vaadin.Flow.whenReady = ")); + assertTrue(scriptContent.endsWith("window.Vaadin.TypeScript= {};")); assertEquals("", initialUidlScript.attr("initial")); } @@ -448,9 +452,13 @@ public void should_not_initialize_UI_and_add_initialUidl_when_invalid_route() Elements scripts = document.head().getElementsByTag("script"); Element initialUidlScript = findScript(scripts, INITIAL_UIDL_SEARCH_STRING); - assertEquals( - "window.Vaadin = window.Vaadin || {};window.Vaadin.TypeScript= {};", - initialUidlScript.childNode(0).toString()); + String scriptContent = initialUidlScript.childNode(0).toString(); + assertTrue( + scriptContent.startsWith("window.Vaadin = window.Vaadin || {};" + + "window.Vaadin.Flow = window.Vaadin.Flow || {};" + + "window.Vaadin.Flow.ready = ")); + assertTrue(scriptContent.contains("window.Vaadin.Flow.whenReady = ")); + assertTrue(scriptContent.endsWith("window.Vaadin.TypeScript= {};")); assertEquals("", initialUidlScript.attr("initial")); assertNull(UI.getCurrent()); } diff --git a/vaadin-dev-server/src/main/resources/com/vaadin/base/devserver/dev-mode-not-ready.html b/vaadin-dev-server/src/main/resources/com/vaadin/base/devserver/dev-mode-not-ready.html index 4df7614405b..171e56c3205 100644 --- a/vaadin-dev-server/src/main/resources/com/vaadin/base/devserver/dev-mode-not-ready.html +++ b/vaadin-dev-server/src/main/resources/com/vaadin/base/devserver/dev-mode-not-ready.html @@ -49,7 +49,7 @@