From 7fadbd2b73175f00514479650c75cca801b44b48 Mon Sep 17 00:00:00 2001 From: Vaadin Bot Date: Tue, 12 Nov 2024 09:14:04 +0100 Subject: [PATCH] fix: reconnect web components after session expiration (#20407) (CP: 24.5) (#20439) * fix: reconnect web components after session expiration (#20407) * fix: reconnect web components after session expiration After session expiration, Flow client in webcomponent mode send a GET request to the server to re-initialize itself with a valid session cookie. However, the XHR call is done with the withCredentials flag set to false, making the browser ignore the Set-Cookie header in the response. This change forces the withCredential flag to true for resync request so that the new cookie can be handled by the browser and reused in the subsequent request that re-intitializes the embedded component. If PUSH is enabled, it also restores the connection after resynchornization request to make sure pending invocation queue, and especially the webcomponent connected events, can be flushed correctly and sent to the server. Also temporarily suspends hearbeat during resynchronization request to prevent issue with concurrent requests, potentially causing duplicated session expiration handling on the client. Fixes #19620 * add tests * fix pom files --------- Co-authored-by: Marco Collovati --- .../com/vaadin/client/DefaultRegistry.java | 3 +- .../com/vaadin/client/SystemErrorHandler.java | 70 ++++++-- .../AtmospherePushConnection.java | 2 +- .../DefaultConnectionStateHandler.java | 7 +- .../client/communication/Heartbeat.java | 19 ++- .../client/communication/MessageSender.java | 33 +++- .../client/communication/ServerRpcQueue.java | 1 + .../client/gwt/elemental/js/util/Xhr.java | 18 +++ .../WebComponentBootstrapHandler.java | 2 + .../communication/WebComponentProvider.java | 2 +- flow-tests/test-frontend/pom.xml | 3 + .../.gitignore | 1 + .../pom.xml | 105 ++++++++++++ .../src/main/frontend/index.html | 23 +++ .../src/main/frontend/web-component.html | 10 ++ .../java/com/vaadin/viteapp/LoginForm.java | 57 +++++++ .../com/vaadin/viteapp/LoginFormExporter.java | 20 +++ .../java/com/vaadin/viteapp/UserService.java | 27 ++++ .../src/main/webapp/basic-component.html | 13 ++ .../com/vaadin/viteapp/BasicComponentIT.java | 152 ++++++++++++++++++ .../.gitignore | 1 + .../pom.xml | 105 ++++++++++++ .../src/main/frontend/index.html | 23 +++ .../src/main/frontend/web-component.html | 10 ++ .../java/com/vaadin/viteapp/LoginForm.java | 57 +++++++ .../com/vaadin/viteapp/LoginFormExporter.java | 20 +++ .../java/com/vaadin/viteapp/UserService.java | 27 ++++ .../src/main/webapp/basic-component.html | 13 ++ .../com/vaadin/viteapp/BasicComponentIT.java | 152 ++++++++++++++++++ .../.gitignore | 1 + .../pom.xml | 105 ++++++++++++ .../src/main/frontend/index.html | 23 +++ .../src/main/frontend/web-component.html | 10 ++ .../java/com/vaadin/viteapp/LoginForm.java | 57 +++++++ .../com/vaadin/viteapp/LoginFormExporter.java | 20 +++ .../java/com/vaadin/viteapp/UserService.java | 27 ++++ .../src/main/webapp/basic-component.html | 13 ++ .../com/vaadin/viteapp/BasicComponentIT.java | 152 ++++++++++++++++++ scripts/computeMatrix.js | 3 + 39 files changed, 1364 insertions(+), 23 deletions(-) create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/.gitignore create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/pom.xml create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/frontend/index.html create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/frontend/web-component.html create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/java/com/vaadin/viteapp/LoginForm.java create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/java/com/vaadin/viteapp/LoginFormExporter.java create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/java/com/vaadin/viteapp/UserService.java create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/webapp/basic-component.html create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/test/java/com/vaadin/viteapp/BasicComponentIT.java create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/.gitignore create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/pom.xml create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/frontend/index.html create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/frontend/web-component.html create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/java/com/vaadin/viteapp/LoginForm.java create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/java/com/vaadin/viteapp/LoginFormExporter.java create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/java/com/vaadin/viteapp/UserService.java create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/webapp/basic-component.html create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/test/java/com/vaadin/viteapp/BasicComponentIT.java create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/.gitignore create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/pom.xml create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/frontend/index.html create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/frontend/web-component.html create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/java/com/vaadin/viteapp/LoginForm.java create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/java/com/vaadin/viteapp/LoginFormExporter.java create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/java/com/vaadin/viteapp/UserService.java create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/webapp/basic-component.html create mode 100644 flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/test/java/com/vaadin/viteapp/BasicComponentIT.java diff --git a/flow-client/src/main/java/com/vaadin/client/DefaultRegistry.java b/flow-client/src/main/java/com/vaadin/client/DefaultRegistry.java index 174577059a3..8e1b92736c3 100644 --- a/flow-client/src/main/java/com/vaadin/client/DefaultRegistry.java +++ b/flow-client/src/main/java/com/vaadin/client/DefaultRegistry.java @@ -79,7 +79,8 @@ public DefaultRegistry(ApplicationConnection connection, set(InitialPropertiesHandler.class, new InitialPropertiesHandler(this)); // Classes with dependencies, in correct order - set(Heartbeat.class, new Heartbeat(this)); + Supplier heartbeatSupplier = () -> new Heartbeat(this); + set(Heartbeat.class, heartbeatSupplier); set(ConnectionStateHandler.class, new DefaultConnectionStateHandler(this)); set(XhrConnection.class, new XhrConnection(this)); diff --git a/flow-client/src/main/java/com/vaadin/client/SystemErrorHandler.java b/flow-client/src/main/java/com/vaadin/client/SystemErrorHandler.java index 2a70a11ebe1..5eb85691cc0 100644 --- a/flow-client/src/main/java/com/vaadin/client/SystemErrorHandler.java +++ b/flow-client/src/main/java/com/vaadin/client/SystemErrorHandler.java @@ -23,6 +23,7 @@ import com.google.gwt.core.client.Scheduler; import com.google.gwt.xhr.client.XMLHttpRequest; + import com.vaadin.client.bootstrap.ErrorMessage; import com.vaadin.client.communication.MessageHandler; import com.vaadin.client.gwt.elemental.js.util.Xhr; @@ -144,47 +145,96 @@ public void handleUnrecoverableError(String caption, String message, } } + private boolean resyncInProgress = false; + /** * Send GET async request to acquire new JSESSIONID, browser will set cookie * automatically based on Set-Cookie response header. */ private void resynchronizeSession() { + if (resyncInProgress) { + Console.debug( + "Web components resynchronization already in progress"); + return; + } + resyncInProgress = true; String serviceUrl = registry.getApplicationConfiguration() .getServiceUrl() + "web-component/web-component-bootstrap.js"; + + // Stop heart beat to prevent requests during resynchronization + registry.getHeartbeat().setInterval(-1); + if (registry.getPushConfiguration().isPushEnabled()) { + registry.getMessageSender().setPushEnabled(false, false); + } + String sessionResyncUri = SharedUtil.addGetParameter(serviceUrl, ApplicationConstants.REQUEST_TYPE_PARAMETER, ApplicationConstants.REQUEST_TYPE_WEBCOMPONENT_RESYNC); - Xhr.get(sessionResyncUri, new Xhr.Callback() { + Xhr.getWithCredentials(sessionResyncUri, new Xhr.Callback() { @Override public void onFail(XMLHttpRequest xhr, Exception exception) { + registry.getHeartbeat().setInterval(registry + .getApplicationConfiguration().getHeartbeatInterval()); handleError(exception); } @Override public void onSuccess(XMLHttpRequest xhr) { - Console.log( "Received xhr HTTP session resynchronization message: " + xhr.getResponseText()); - registry.reset(); - registry.getUILifecycle().setState(UILifecycle.UIState.RUNNING); + // Make sure heartbeat has not been restarted. This is + // especially important if the uiId gets reset after session + // expiration, to prevent multiple heartbeats requests for + // different ui + registry.getHeartbeat().setInterval(-1); + int uiId = registry.getApplicationConfiguration().getUIId(); ValueMap json = MessageHandler .parseWrappedJson(xhr.getResponseText()); + int newUiId = json.getInt(ApplicationConstants.UI_ID); + if (newUiId != uiId) { + Console.debug("UI ID switched from " + uiId + " to " + + newUiId + " after resynchronization"); + registry.getApplicationConfiguration().setUIId(newUiId); + } + registry.reset(); + + registry.getUILifecycle().setState(UILifecycle.UIState.RUNNING); registry.getMessageHandler().handleMessage(json); - registry.getApplicationConfiguration() - .setUIId(json.getInt(ApplicationConstants.UI_ID)); - Scheduler.get().scheduleDeferred(() -> Arrays - .stream(registry.getApplicationConfiguration() - .getExportedWebComponents()) - .forEach(SystemErrorHandler.this::recreateNodes)); + boolean pushEnabled = registry.getPushConfiguration() + .isPushEnabled(); + if (pushEnabled) { + // PUSH connection might have been closed in response to + // sever session expiration. If PUSH is required, reconnect + // before recreating web components to make sure the + // connected events can be propagated to the server. + // PUSH reconnection is deferred to allow current request + // to complete and process the Set-Cookie header. + Scheduler.get().scheduleDeferred(() -> { + Console.debug("Re-establish PUSH connection"); + registry.getMessageSender().setPushEnabled(true); + Scheduler.get().scheduleDeferred( + () -> recreateWebComponents()); + }); + } else { + Scheduler.get() + .scheduleDeferred(() -> recreateWebComponents()); + } } }); } + private void recreateWebComponents() { + Arrays.stream(registry.getApplicationConfiguration() + .getExportedWebComponents()) + .forEach(SystemErrorHandler.this::recreateNodes); + resyncInProgress = false; + } + private native void recreateNodes(String elementName) /*-{ var elements = document.getElementsByTagName(elementName); diff --git a/flow-client/src/main/java/com/vaadin/client/communication/AtmospherePushConnection.java b/flow-client/src/main/java/com/vaadin/client/communication/AtmospherePushConnection.java index 46a512efd35..c40ab4f904d 100644 --- a/flow-client/src/main/java/com/vaadin/client/communication/AtmospherePushConnection.java +++ b/flow-client/src/main/java/com/vaadin/client/communication/AtmospherePushConnection.java @@ -186,7 +186,6 @@ public AtmospherePushConnection(Registry registry) { } else { config.setStringValue(key, value); } - }); String pushServletMapping = getPushConfiguration() @@ -686,6 +685,7 @@ protected final native AtmosphereConfiguration createConfig() fallbackTransport: 'long-polling', contentType: 'application/json; charset=UTF-8', reconnectInterval: 5000, + withCredentials: true, maxWebsocketErrorRetries: 12, timeout: -1, maxReconnectOnClose: 10000000, diff --git a/flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java b/flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java index 5be7f20f33d..2847c46bf21 100644 --- a/flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java +++ b/flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java @@ -534,8 +534,11 @@ private void pauseHeartbeats() { } private void resumeHeartbeats() { - registry.getHeartbeat().setInterval( - registry.getApplicationConfiguration().getHeartbeatInterval()); + // Resume heart beat only if it was not terminated (interval == -1) + if (registry.getHeartbeat().getInterval() >= 0) { + registry.getHeartbeat().setInterval(registry + .getApplicationConfiguration().getHeartbeatInterval()); + } } private boolean redirectIfRefreshToken(String message) { diff --git a/flow-client/src/main/java/com/vaadin/client/communication/Heartbeat.java b/flow-client/src/main/java/com/vaadin/client/communication/Heartbeat.java index d3ee979787a..745bc681780 100644 --- a/flow-client/src/main/java/com/vaadin/client/communication/Heartbeat.java +++ b/flow-client/src/main/java/com/vaadin/client/communication/Heartbeat.java @@ -17,6 +17,7 @@ import com.google.gwt.user.client.Timer; import com.google.gwt.xhr.client.XMLHttpRequest; + import com.vaadin.client.Console; import com.vaadin.client.Registry; import com.vaadin.client.gwt.elemental.js.util.Xhr; @@ -74,8 +75,13 @@ public Heartbeat(Registry registry) { */ public void send() { timer.cancel(); + if (interval < 0) { + Console.debug("Heartbeat terminated, skipping request"); + return; + } Console.debug("Sending heartbeat request..."); + Xhr.post(uri, null, "text/plain; charset=utf-8", new Xhr.Callback() { @Override @@ -86,12 +92,19 @@ public void onSuccess(XMLHttpRequest xhr) { @Override public void onFail(XMLHttpRequest xhr, Exception e) { - // Handler should stop the application if heartbeat should no // longer be sent if (e == null) { - registry.getConnectionStateHandler() - .heartbeatInvalidStatusCode(xhr); + // Heartbeat has been terminated before response processing. + // Most likely a session expiration happened, and it has + // already been handled by another component. + if (interval < 0) { + Console.debug( + "Heartbeat terminated, ignoring failure."); + } else { + registry.getConnectionStateHandler() + .heartbeatInvalidStatusCode(xhr); + } } else { registry.getConnectionStateHandler().heartbeatException(xhr, e); diff --git a/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java b/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java index 5f1c311d6ba..dcfe798bc8e 100644 --- a/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java +++ b/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java @@ -16,8 +16,9 @@ package com.vaadin.client.communication; import com.google.gwt.core.client.GWT; -import com.vaadin.client.Console; + import com.vaadin.client.ConnectionIndicator; +import com.vaadin.client.Console; import com.vaadin.client.Registry; import com.vaadin.flow.shared.ApplicationConstants; @@ -91,10 +92,14 @@ public void sendInvocationsToServer() { return; } - if (registry.getRequestResponseTracker().hasActiveRequest() - || (push != null && !push.isActive())) { + boolean hasActiveRequest = registry.getRequestResponseTracker() + .hasActiveRequest(); + if (hasActiveRequest || (push != null && !push.isActive())) { // There is an active request or push is enabled but not active // -> send when current request completes or push becomes active + Console.debug("Postpone sending invocations to server because of " + + (hasActiveRequest ? "active request" + : "PUSH not active")); } else { doSendInvocationsToServer(); } @@ -200,9 +205,11 @@ public void send(final JsonObject payload) { // server after a reconnection. // Reference will be cleaned up once the server confirms it has // seen this message + Console.debug("send PUSH"); pushPendingMessage = payload; push.push(payload); } else { + Console.log("send XHR"); registry.getXhrConnection().send(payload); } } @@ -215,7 +222,22 @@ public void send(final JsonObject payload) { * false to disable the push connection. */ public void setPushEnabled(boolean enabled) { - if (enabled && push == null) { + setPushEnabled(enabled, true); + } + + /** + * Sets the status for the push connection. + * + * @param enabled + * true to enable the push connection; + * false to disable the push connection. + * @param reEnableIfNeeded + * true if push should be re-enabled after + * disconnection if configuration changed; false to + * prevent reconnection. + */ + public void setPushEnabled(boolean enabled, boolean reEnableIfNeeded) { + if (enabled && (push == null || !push.isActive())) { push = pushConnectionFactory.create(registry); } else if (!enabled && push != null && push.isActive()) { push.disconnect(() -> { @@ -225,7 +247,8 @@ public void setPushEnabled(boolean enabled) { * old connection to disconnect, now is the right time to open a * new connection */ - if (registry.getPushConfiguration().isPushEnabled()) { + if (reEnableIfNeeded + && registry.getPushConfiguration().isPushEnabled()) { setPushEnabled(true); } diff --git a/flow-client/src/main/java/com/vaadin/client/communication/ServerRpcQueue.java b/flow-client/src/main/java/com/vaadin/client/communication/ServerRpcQueue.java index be8f0ea706f..93f31598a05 100644 --- a/flow-client/src/main/java/com/vaadin/client/communication/ServerRpcQueue.java +++ b/flow-client/src/main/java/com/vaadin/client/communication/ServerRpcQueue.java @@ -16,6 +16,7 @@ package com.vaadin.client.communication; import com.google.gwt.core.client.Scheduler; + import com.vaadin.client.Console; import com.vaadin.client.Registry; diff --git a/flow-client/src/main/java/com/vaadin/client/gwt/elemental/js/util/Xhr.java b/flow-client/src/main/java/com/vaadin/client/gwt/elemental/js/util/Xhr.java index 59e4c609141..ef8811e319f 100644 --- a/flow-client/src/main/java/com/vaadin/client/gwt/elemental/js/util/Xhr.java +++ b/flow-client/src/main/java/com/vaadin/client/gwt/elemental/js/util/Xhr.java @@ -18,6 +18,7 @@ import com.google.gwt.core.client.JavaScriptException; import com.google.gwt.xhr.client.ReadyStateChangeHandler; import com.google.gwt.xhr.client.XMLHttpRequest; + import com.vaadin.client.Console; import elemental.client.Browser; @@ -90,6 +91,23 @@ public static XMLHttpRequest get(String url, Callback callback) { return request(create(), "GET", url, callback); } + /** + * Send a GET request to the url including credentials in XHR, + * and dispatch updates to the callback. + * + * @param url + * the URL + * @param callback + * the callback to be notified + * @return a reference to the sent XmlHttpRequest + */ + public static XMLHttpRequest getWithCredentials(String url, + Callback callback) { + XMLHttpRequest request = create(); + request.setWithCredentials(true); + return request(request, "GET", url, callback); + } + /** * Send a GET request to the url and dispatch updates to the * callback. diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentBootstrapHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentBootstrapHandler.java index 120cce42620..11a0fdb92c9 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentBootstrapHandler.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentBootstrapHandler.java @@ -629,6 +629,8 @@ protected boolean handleWebComponentResyncRequest(BootstrapContext context, json.put(ApplicationConstants.UI_ID, context.getUI().getUIId()); json.put(ApplicationConstants.UIDL_SECURITY_TOKEN_ID, context.getUI().getCsrfToken()); + json.put(ApplicationConstants.UIDL_PUSH_ID, + context.getUI().getSession().getPushId()); String responseString = "for(;;);[" + JsonUtil.stringify(json) + "]"; try { diff --git a/flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentProvider.java b/flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentProvider.java index 5da1a5e719d..b2cd0f0f80a 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentProvider.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentProvider.java @@ -229,7 +229,7 @@ protected String bootstrapNpm(boolean productionMode) { const delay = 200; const poll = async () => { try { - const response = await fetch(bootstrapSrc, { method: 'HEAD', headers: { 'X-DevModePoll': 'true' } }); + const response = await fetch(bootstrapSrc, { method: 'HEAD', credentials: 'include', headers: { 'X-DevModePoll': 'true' } }); if (response.headers.has('X-DevModePending')) { setTimeout(poll, delay); } else { diff --git a/flow-tests/test-frontend/pom.xml b/flow-tests/test-frontend/pom.xml index bee3599b998..39fc1f4a4d7 100644 --- a/flow-tests/test-frontend/pom.xml +++ b/flow-tests/test-frontend/pom.xml @@ -46,6 +46,9 @@ vite-embedded-webcomponent-resync + vite-embedded-webcomponent-resync-ws + vite-embedded-webcomponent-resync-wsxhr + vite-embedded-webcomponent-resync-longpolling test-npm/pom-production.xml test-npm test-pnpm/pom-production.xml diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/.gitignore b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/.gitignore new file mode 100644 index 00000000000..3dee652a777 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/.gitignore @@ -0,0 +1 @@ +!**/index.html diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/pom.xml b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/pom.xml new file mode 100644 index 00000000000..49f6c738fb2 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + + com.vaadin + test-frontend + 24.5-SNAPSHOT + + vite-embedded-webcomponent-resync-long-polling + Vite embedded app - session resync with long polling PUSH + war + + + + + com.vaadin + flow-bom + ${project.version} + pom + import + + + + + + + + com.vaadin + vaadin-dev-server + ${project.version} + + + com.vaadin + flow-test-lumo + ${project.version} + + + + org.eclipse.jetty + jetty-server + ${jetty.version} + + + org.eclipse.jetty.ee10 + jetty-ee10-webapp + ${jetty.version} + + + org.eclipse.jetty + jetty-http + ${jetty.version} + + + org.eclipse.jetty.ee10 + jetty-ee10-annotations + ${jetty.version} + + + org.eclipse.jetty + jetty-util + ${jetty.version} + + + org.eclipse.jetty.ee10 + jetty-ee10-servlet + ${jetty.version} + + + org.eclipse.jetty.ee10.websocket + jetty-ee10-websocket-jakarta-server + ${jetty.version} + + + + + org.eclipse.jetty.ee10 + jetty-ee10-plus + ${jetty.version} + + + jakarta.annotation + jakarta.annotation-api + + + + + + + + + com.vaadin + flow-maven-plugin + + + + prepare-frontend + + + + + + + diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/frontend/index.html b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/frontend/index.html new file mode 100644 index 00000000000..d36e593475c --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ + diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/frontend/web-component.html b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/frontend/web-component.html new file mode 100644 index 00000000000..fd598bbbadf --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/frontend/web-component.html @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/java/com/vaadin/viteapp/LoginForm.java b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/java/com/vaadin/viteapp/LoginForm.java new file mode 100644 index 00000000000..79484427374 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/java/com/vaadin/viteapp/LoginForm.java @@ -0,0 +1,57 @@ +package com.vaadin.viteapp; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Input; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.function.SerializableRunnable; + +public class LoginForm extends Div { + private Input userName = new Input(); + private Input password = new Input(); + private Div errorMsg = new Div(); + private String userLabel; + private String pwdLabel; + private Div layout = new Div(); + private List loginListeners = new CopyOnWriteArrayList<>(); + + public LoginForm() { + updateForm(); + + add(layout); + + NativeButton login = new NativeButton("Login", event -> login()); + add(login, errorMsg); + } + + public void setUserNameLabel(String userNameLabelString) { + userLabel = userNameLabelString; + updateForm(); + } + + public void setPasswordLabel(String pwd) { + pwdLabel = pwd; + updateForm(); + } + + public void updateForm() { + layout.removeAll(); + layout.add(new Span(userLabel), userName); + layout.add(new Span(pwdLabel), password); + } + + private void login() { + Optional authToken = UserService.getInstance() + .authenticate(userName.getValue(), password.getValue()); + if (authToken.isPresent()) { + errorMsg.setText("Authentication success"); + } else { + errorMsg.setText("Authentication failure"); + } + } + +} diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/java/com/vaadin/viteapp/LoginFormExporter.java b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/java/com/vaadin/viteapp/LoginFormExporter.java new file mode 100644 index 00000000000..c630bf04f06 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/java/com/vaadin/viteapp/LoginFormExporter.java @@ -0,0 +1,20 @@ +package com.vaadin.viteapp; + +import com.vaadin.flow.component.WebComponentExporter; +import com.vaadin.flow.component.page.Push; +import com.vaadin.flow.component.webcomponent.WebComponent; +import com.vaadin.flow.shared.ui.Transport; + +@Push(transport = Transport.LONG_POLLING) +public class LoginFormExporter extends WebComponentExporter { + public LoginFormExporter() { + super("login-form"); + addProperty("userlbl", "").onChange(LoginForm::setUserNameLabel); + addProperty("pwdlbl", "").onChange(LoginForm::setPasswordLabel); + } + + @Override + protected void configureInstance(WebComponent webComponent, + LoginForm form) { + } +} diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/java/com/vaadin/viteapp/UserService.java b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/java/com/vaadin/viteapp/UserService.java new file mode 100644 index 00000000000..a74e2ec4ba6 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/java/com/vaadin/viteapp/UserService.java @@ -0,0 +1,27 @@ +package com.vaadin.viteapp; + +import java.util.Optional; + +public final class UserService { + + private static final UserService INSTANCE = new UserService(); + + private UserService() { + } + + public static UserService getInstance() { + return INSTANCE; + } + + public String getName(Object authToken) { + return "Joe"; + } + + public Optional authenticate(String user, String passwd) { + if ("admin".equals(user) && "admin".equals(passwd)) { + return Optional.of(new Object()); + } else { + return Optional.empty(); + } + } +} diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/webapp/basic-component.html b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/webapp/basic-component.html new file mode 100644 index 00000000000..ab6da8a6997 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/main/webapp/basic-component.html @@ -0,0 +1,13 @@ + + + + + + + +
+ +
+ + + diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/test/java/com/vaadin/viteapp/BasicComponentIT.java b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/test/java/com/vaadin/viteapp/BasicComponentIT.java new file mode 100644 index 00000000000..071c7382da2 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling/src/test/java/com/vaadin/viteapp/BasicComponentIT.java @@ -0,0 +1,152 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.viteapp; + +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionListener; + +import java.io.File; + +import org.eclipse.jetty.ee10.webapp.WebAppContext; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.Keys; +import org.openqa.selenium.StaleElementReferenceException; + +import com.vaadin.flow.server.InitParameters; +import com.vaadin.flow.testutil.ChromeDeviceTest; + +public class BasicComponentIT extends ChromeDeviceTest { + + private static final String HOTDEPLOY_PROPERTY = "vaadin." + + InitParameters.FRONTEND_HOTDEPLOY; + + private Server server; + + private WebAppContext context; + + private String hotdeploy; + + protected HttpSession session; + + @Before + public void init() throws Exception { + setup(8888); + getDriver().get(getRootURL()); + waitForDevServer(); + getDriver().get(getRootURL() + "/basic-component.html"); + } + + @Test + public void session_resynced_webcomponent_is_active() throws Exception { + waitForWebComponent("login-form"); + // check if web component works + clickButton(); + Assert.assertEquals("Authentication failure", + getAuthenticationResult()); + + // simulate expired session by invalidating current session + session.invalidate(); + waitForWebComponent("login-form"); + + // init request to resynchronize expired session and recreate components + clickButton(); + + try { + // it seems WebDriver needs also sync to new session + setUsername(""); + } catch (StaleElementReferenceException ex) { + // NOP + } + + // check if web component works again + setUsername("admin"); + setPassword("admin"); + clickButton(); + Assert.assertEquals("Authentication success", + getAuthenticationResult()); + } + + private void clickButton() { + waitUntil(d -> $("login-form").first().$("button").first()).click(); + } + + private String getAuthenticationResult() { + return $("login-form").first().$("div").last().getText(); + } + + private void setUsername(String value) { + $("login-form").first().$("input").first().sendKeys(value + Keys.TAB); + } + + private void setPassword(String value) { + $("login-form").first().$("input").last().sendKeys(value + Keys.TAB); + } + + @Override + public void checkIfServerAvailable() { + // NOP + } + + public void setup(int port) throws Exception { + hotdeploy = System.getProperty(HOTDEPLOY_PROPERTY); + System.setProperty(HOTDEPLOY_PROPERTY, "true"); + server = new Server(); + try (ServerConnector connector = new ServerConnector(server)) { + connector.setPort(port); + server.setConnectors(new ServerConnector[] { connector }); + } + + File[] warDirs = new File("target") + .listFiles(file -> file.getName().matches( + "vite-embedded-webcomponent-resync-.*-SNAPSHOT\\.war")); + String warfile = "target/" + warDirs[0].getName(); + + context = new WebAppContext(warfile, "/"); + + // store session id to be able to invalidate it during test + context.getSessionHandler().addEventListener(new HttpSessionListener() { + @Override + public void sessionCreated(HttpSessionEvent httpSessionEvent) { + session = httpSessionEvent.getSession(); + } + }); + + server.setHandler(context); + server.start(); + } + + @After + public void shutdown() throws Exception { + try { + context.stop(); + context.destroy(); + context = null; + } finally { + server.stop(); + if (hotdeploy == null) { + System.clearProperty(HOTDEPLOY_PROPERTY); + } else { + System.setProperty(HOTDEPLOY_PROPERTY, hotdeploy); + } + } + } +} diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/.gitignore b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/.gitignore new file mode 100644 index 00000000000..3dee652a777 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/.gitignore @@ -0,0 +1 @@ +!**/index.html diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/pom.xml b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/pom.xml new file mode 100644 index 00000000000..3c292227f42 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + + com.vaadin + test-frontend + 24.5-SNAPSHOT + + vite-embedded-webcomponent-resync-ws + Vite embedded app - session resync with websocket PUSH + war + + + + + com.vaadin + flow-bom + ${project.version} + pom + import + + + + + + + + com.vaadin + vaadin-dev-server + ${project.version} + + + com.vaadin + flow-test-lumo + ${project.version} + + + + org.eclipse.jetty + jetty-server + ${jetty.version} + + + org.eclipse.jetty.ee10 + jetty-ee10-webapp + ${jetty.version} + + + org.eclipse.jetty + jetty-http + ${jetty.version} + + + org.eclipse.jetty.ee10 + jetty-ee10-annotations + ${jetty.version} + + + org.eclipse.jetty + jetty-util + ${jetty.version} + + + org.eclipse.jetty.ee10 + jetty-ee10-servlet + ${jetty.version} + + + org.eclipse.jetty.ee10.websocket + jetty-ee10-websocket-jakarta-server + ${jetty.version} + + + + + org.eclipse.jetty.ee10 + jetty-ee10-plus + ${jetty.version} + + + jakarta.annotation + jakarta.annotation-api + + + + + + + + + com.vaadin + flow-maven-plugin + + + + prepare-frontend + + + + + + + diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/frontend/index.html b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/frontend/index.html new file mode 100644 index 00000000000..d36e593475c --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ + diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/frontend/web-component.html b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/frontend/web-component.html new file mode 100644 index 00000000000..fd598bbbadf --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/frontend/web-component.html @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/java/com/vaadin/viteapp/LoginForm.java b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/java/com/vaadin/viteapp/LoginForm.java new file mode 100644 index 00000000000..79484427374 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/java/com/vaadin/viteapp/LoginForm.java @@ -0,0 +1,57 @@ +package com.vaadin.viteapp; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Input; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.function.SerializableRunnable; + +public class LoginForm extends Div { + private Input userName = new Input(); + private Input password = new Input(); + private Div errorMsg = new Div(); + private String userLabel; + private String pwdLabel; + private Div layout = new Div(); + private List loginListeners = new CopyOnWriteArrayList<>(); + + public LoginForm() { + updateForm(); + + add(layout); + + NativeButton login = new NativeButton("Login", event -> login()); + add(login, errorMsg); + } + + public void setUserNameLabel(String userNameLabelString) { + userLabel = userNameLabelString; + updateForm(); + } + + public void setPasswordLabel(String pwd) { + pwdLabel = pwd; + updateForm(); + } + + public void updateForm() { + layout.removeAll(); + layout.add(new Span(userLabel), userName); + layout.add(new Span(pwdLabel), password); + } + + private void login() { + Optional authToken = UserService.getInstance() + .authenticate(userName.getValue(), password.getValue()); + if (authToken.isPresent()) { + errorMsg.setText("Authentication success"); + } else { + errorMsg.setText("Authentication failure"); + } + } + +} diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/java/com/vaadin/viteapp/LoginFormExporter.java b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/java/com/vaadin/viteapp/LoginFormExporter.java new file mode 100644 index 00000000000..542a10ea7f4 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/java/com/vaadin/viteapp/LoginFormExporter.java @@ -0,0 +1,20 @@ +package com.vaadin.viteapp; + +import com.vaadin.flow.component.WebComponentExporter; +import com.vaadin.flow.component.page.Push; +import com.vaadin.flow.component.webcomponent.WebComponent; +import com.vaadin.flow.shared.ui.Transport; + +@Push(transport = Transport.WEBSOCKET) +public class LoginFormExporter extends WebComponentExporter { + public LoginFormExporter() { + super("login-form"); + addProperty("userlbl", "").onChange(LoginForm::setUserNameLabel); + addProperty("pwdlbl", "").onChange(LoginForm::setPasswordLabel); + } + + @Override + protected void configureInstance(WebComponent webComponent, + LoginForm form) { + } +} diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/java/com/vaadin/viteapp/UserService.java b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/java/com/vaadin/viteapp/UserService.java new file mode 100644 index 00000000000..a74e2ec4ba6 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/java/com/vaadin/viteapp/UserService.java @@ -0,0 +1,27 @@ +package com.vaadin.viteapp; + +import java.util.Optional; + +public final class UserService { + + private static final UserService INSTANCE = new UserService(); + + private UserService() { + } + + public static UserService getInstance() { + return INSTANCE; + } + + public String getName(Object authToken) { + return "Joe"; + } + + public Optional authenticate(String user, String passwd) { + if ("admin".equals(user) && "admin".equals(passwd)) { + return Optional.of(new Object()); + } else { + return Optional.empty(); + } + } +} diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/webapp/basic-component.html b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/webapp/basic-component.html new file mode 100644 index 00000000000..ab6da8a6997 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/main/webapp/basic-component.html @@ -0,0 +1,13 @@ + + + + + + + +
+ +
+ + + diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/test/java/com/vaadin/viteapp/BasicComponentIT.java b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/test/java/com/vaadin/viteapp/BasicComponentIT.java new file mode 100644 index 00000000000..071c7382da2 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws/src/test/java/com/vaadin/viteapp/BasicComponentIT.java @@ -0,0 +1,152 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.viteapp; + +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionListener; + +import java.io.File; + +import org.eclipse.jetty.ee10.webapp.WebAppContext; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.Keys; +import org.openqa.selenium.StaleElementReferenceException; + +import com.vaadin.flow.server.InitParameters; +import com.vaadin.flow.testutil.ChromeDeviceTest; + +public class BasicComponentIT extends ChromeDeviceTest { + + private static final String HOTDEPLOY_PROPERTY = "vaadin." + + InitParameters.FRONTEND_HOTDEPLOY; + + private Server server; + + private WebAppContext context; + + private String hotdeploy; + + protected HttpSession session; + + @Before + public void init() throws Exception { + setup(8888); + getDriver().get(getRootURL()); + waitForDevServer(); + getDriver().get(getRootURL() + "/basic-component.html"); + } + + @Test + public void session_resynced_webcomponent_is_active() throws Exception { + waitForWebComponent("login-form"); + // check if web component works + clickButton(); + Assert.assertEquals("Authentication failure", + getAuthenticationResult()); + + // simulate expired session by invalidating current session + session.invalidate(); + waitForWebComponent("login-form"); + + // init request to resynchronize expired session and recreate components + clickButton(); + + try { + // it seems WebDriver needs also sync to new session + setUsername(""); + } catch (StaleElementReferenceException ex) { + // NOP + } + + // check if web component works again + setUsername("admin"); + setPassword("admin"); + clickButton(); + Assert.assertEquals("Authentication success", + getAuthenticationResult()); + } + + private void clickButton() { + waitUntil(d -> $("login-form").first().$("button").first()).click(); + } + + private String getAuthenticationResult() { + return $("login-form").first().$("div").last().getText(); + } + + private void setUsername(String value) { + $("login-form").first().$("input").first().sendKeys(value + Keys.TAB); + } + + private void setPassword(String value) { + $("login-form").first().$("input").last().sendKeys(value + Keys.TAB); + } + + @Override + public void checkIfServerAvailable() { + // NOP + } + + public void setup(int port) throws Exception { + hotdeploy = System.getProperty(HOTDEPLOY_PROPERTY); + System.setProperty(HOTDEPLOY_PROPERTY, "true"); + server = new Server(); + try (ServerConnector connector = new ServerConnector(server)) { + connector.setPort(port); + server.setConnectors(new ServerConnector[] { connector }); + } + + File[] warDirs = new File("target") + .listFiles(file -> file.getName().matches( + "vite-embedded-webcomponent-resync-.*-SNAPSHOT\\.war")); + String warfile = "target/" + warDirs[0].getName(); + + context = new WebAppContext(warfile, "/"); + + // store session id to be able to invalidate it during test + context.getSessionHandler().addEventListener(new HttpSessionListener() { + @Override + public void sessionCreated(HttpSessionEvent httpSessionEvent) { + session = httpSessionEvent.getSession(); + } + }); + + server.setHandler(context); + server.start(); + } + + @After + public void shutdown() throws Exception { + try { + context.stop(); + context.destroy(); + context = null; + } finally { + server.stop(); + if (hotdeploy == null) { + System.clearProperty(HOTDEPLOY_PROPERTY); + } else { + System.setProperty(HOTDEPLOY_PROPERTY, hotdeploy); + } + } + } +} diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/.gitignore b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/.gitignore new file mode 100644 index 00000000000..3dee652a777 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/.gitignore @@ -0,0 +1 @@ +!**/index.html diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/pom.xml b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/pom.xml new file mode 100644 index 00000000000..53ebfffd4b3 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + + com.vaadin + test-frontend + 24.5-SNAPSHOT + + vite-embedded-webcomponent-resync-wsxhr + Vite embedded app - session resync with websocket XHR PUSH + war + + + + + com.vaadin + flow-bom + ${project.version} + pom + import + + + + + + + + com.vaadin + vaadin-dev-server + ${project.version} + + + com.vaadin + flow-test-lumo + ${project.version} + + + + org.eclipse.jetty + jetty-server + ${jetty.version} + + + org.eclipse.jetty.ee10 + jetty-ee10-webapp + ${jetty.version} + + + org.eclipse.jetty + jetty-http + ${jetty.version} + + + org.eclipse.jetty.ee10 + jetty-ee10-annotations + ${jetty.version} + + + org.eclipse.jetty + jetty-util + ${jetty.version} + + + org.eclipse.jetty.ee10 + jetty-ee10-servlet + ${jetty.version} + + + org.eclipse.jetty.ee10.websocket + jetty-ee10-websocket-jakarta-server + ${jetty.version} + + + + + org.eclipse.jetty.ee10 + jetty-ee10-plus + ${jetty.version} + + + jakarta.annotation + jakarta.annotation-api + + + + + + + + + com.vaadin + flow-maven-plugin + + + + prepare-frontend + + + + + + + diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/frontend/index.html b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/frontend/index.html new file mode 100644 index 00000000000..d36e593475c --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/frontend/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +
+ + diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/frontend/web-component.html b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/frontend/web-component.html new file mode 100644 index 00000000000..fd598bbbadf --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/frontend/web-component.html @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/java/com/vaadin/viteapp/LoginForm.java b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/java/com/vaadin/viteapp/LoginForm.java new file mode 100644 index 00000000000..79484427374 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/java/com/vaadin/viteapp/LoginForm.java @@ -0,0 +1,57 @@ +package com.vaadin.viteapp; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Input; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.function.SerializableRunnable; + +public class LoginForm extends Div { + private Input userName = new Input(); + private Input password = new Input(); + private Div errorMsg = new Div(); + private String userLabel; + private String pwdLabel; + private Div layout = new Div(); + private List loginListeners = new CopyOnWriteArrayList<>(); + + public LoginForm() { + updateForm(); + + add(layout); + + NativeButton login = new NativeButton("Login", event -> login()); + add(login, errorMsg); + } + + public void setUserNameLabel(String userNameLabelString) { + userLabel = userNameLabelString; + updateForm(); + } + + public void setPasswordLabel(String pwd) { + pwdLabel = pwd; + updateForm(); + } + + public void updateForm() { + layout.removeAll(); + layout.add(new Span(userLabel), userName); + layout.add(new Span(pwdLabel), password); + } + + private void login() { + Optional authToken = UserService.getInstance() + .authenticate(userName.getValue(), password.getValue()); + if (authToken.isPresent()) { + errorMsg.setText("Authentication success"); + } else { + errorMsg.setText("Authentication failure"); + } + } + +} diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/java/com/vaadin/viteapp/LoginFormExporter.java b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/java/com/vaadin/viteapp/LoginFormExporter.java new file mode 100644 index 00000000000..8123c8c80fb --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/java/com/vaadin/viteapp/LoginFormExporter.java @@ -0,0 +1,20 @@ +package com.vaadin.viteapp; + +import com.vaadin.flow.component.WebComponentExporter; +import com.vaadin.flow.component.page.Push; +import com.vaadin.flow.component.webcomponent.WebComponent; +import com.vaadin.flow.shared.ui.Transport; + +@Push(transport = Transport.WEBSOCKET_XHR) +public class LoginFormExporter extends WebComponentExporter { + public LoginFormExporter() { + super("login-form"); + addProperty("userlbl", "").onChange(LoginForm::setUserNameLabel); + addProperty("pwdlbl", "").onChange(LoginForm::setPasswordLabel); + } + + @Override + protected void configureInstance(WebComponent webComponent, + LoginForm form) { + } +} diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/java/com/vaadin/viteapp/UserService.java b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/java/com/vaadin/viteapp/UserService.java new file mode 100644 index 00000000000..a74e2ec4ba6 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/java/com/vaadin/viteapp/UserService.java @@ -0,0 +1,27 @@ +package com.vaadin.viteapp; + +import java.util.Optional; + +public final class UserService { + + private static final UserService INSTANCE = new UserService(); + + private UserService() { + } + + public static UserService getInstance() { + return INSTANCE; + } + + public String getName(Object authToken) { + return "Joe"; + } + + public Optional authenticate(String user, String passwd) { + if ("admin".equals(user) && "admin".equals(passwd)) { + return Optional.of(new Object()); + } else { + return Optional.empty(); + } + } +} diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/webapp/basic-component.html b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/webapp/basic-component.html new file mode 100644 index 00000000000..ab6da8a6997 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/main/webapp/basic-component.html @@ -0,0 +1,13 @@ + + + + + + + +
+ +
+ + + diff --git a/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/test/java/com/vaadin/viteapp/BasicComponentIT.java b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/test/java/com/vaadin/viteapp/BasicComponentIT.java new file mode 100644 index 00000000000..071c7382da2 --- /dev/null +++ b/flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr/src/test/java/com/vaadin/viteapp/BasicComponentIT.java @@ -0,0 +1,152 @@ +/* + * Copyright 2000-2024 Vaadin Ltd. + * + * 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. + */ +package com.vaadin.viteapp; + +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionListener; + +import java.io.File; + +import org.eclipse.jetty.ee10.webapp.WebAppContext; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.Keys; +import org.openqa.selenium.StaleElementReferenceException; + +import com.vaadin.flow.server.InitParameters; +import com.vaadin.flow.testutil.ChromeDeviceTest; + +public class BasicComponentIT extends ChromeDeviceTest { + + private static final String HOTDEPLOY_PROPERTY = "vaadin." + + InitParameters.FRONTEND_HOTDEPLOY; + + private Server server; + + private WebAppContext context; + + private String hotdeploy; + + protected HttpSession session; + + @Before + public void init() throws Exception { + setup(8888); + getDriver().get(getRootURL()); + waitForDevServer(); + getDriver().get(getRootURL() + "/basic-component.html"); + } + + @Test + public void session_resynced_webcomponent_is_active() throws Exception { + waitForWebComponent("login-form"); + // check if web component works + clickButton(); + Assert.assertEquals("Authentication failure", + getAuthenticationResult()); + + // simulate expired session by invalidating current session + session.invalidate(); + waitForWebComponent("login-form"); + + // init request to resynchronize expired session and recreate components + clickButton(); + + try { + // it seems WebDriver needs also sync to new session + setUsername(""); + } catch (StaleElementReferenceException ex) { + // NOP + } + + // check if web component works again + setUsername("admin"); + setPassword("admin"); + clickButton(); + Assert.assertEquals("Authentication success", + getAuthenticationResult()); + } + + private void clickButton() { + waitUntil(d -> $("login-form").first().$("button").first()).click(); + } + + private String getAuthenticationResult() { + return $("login-form").first().$("div").last().getText(); + } + + private void setUsername(String value) { + $("login-form").first().$("input").first().sendKeys(value + Keys.TAB); + } + + private void setPassword(String value) { + $("login-form").first().$("input").last().sendKeys(value + Keys.TAB); + } + + @Override + public void checkIfServerAvailable() { + // NOP + } + + public void setup(int port) throws Exception { + hotdeploy = System.getProperty(HOTDEPLOY_PROPERTY); + System.setProperty(HOTDEPLOY_PROPERTY, "true"); + server = new Server(); + try (ServerConnector connector = new ServerConnector(server)) { + connector.setPort(port); + server.setConnectors(new ServerConnector[] { connector }); + } + + File[] warDirs = new File("target") + .listFiles(file -> file.getName().matches( + "vite-embedded-webcomponent-resync-.*-SNAPSHOT\\.war")); + String warfile = "target/" + warDirs[0].getName(); + + context = new WebAppContext(warfile, "/"); + + // store session id to be able to invalidate it during test + context.getSessionHandler().addEventListener(new HttpSessionListener() { + @Override + public void sessionCreated(HttpSessionEvent httpSessionEvent) { + session = httpSessionEvent.getSession(); + } + }); + + server.setHandler(context); + server.start(); + } + + @After + public void shutdown() throws Exception { + try { + context.stop(); + context.destroy(); + context = null; + } finally { + server.stop(); + if (hotdeploy == null) { + System.clearProperty(HOTDEPLOY_PROPERTY); + } else { + System.setProperty(HOTDEPLOY_PROPERTY, hotdeploy); + } + } + } +} diff --git a/scripts/computeMatrix.js b/scripts/computeMatrix.js index e81a61d5a83..f271bf98fd2 100755 --- a/scripts/computeMatrix.js +++ b/scripts/computeMatrix.js @@ -132,6 +132,9 @@ const moduleWeights = { 'flow-plugins/flow-maven-plugin': { weight: 4 }, 'flow-plugins/flow-gradle-plugin': { weight: 4 }, 'flow-tests/test-frontend/vite-embedded-webcomponent-resync': { weight: 4 }, + 'flow-tests/test-frontend/vite-embedded-webcomponent-resync-ws': { weight: 4 }, + 'flow-tests/test-frontend/vite-embedded-webcomponent-resync-wsxhr': { weight: 4 }, + 'flow-tests/test-frontend/vite-embedded-webcomponent-resync-longpolling': { weight: 4 }, 'flow-tests/test-npm-only-features/test-npm-no-buildmojo': { weight: 4 }, 'flow-tests/test-ccdm/pom-production.xml': { weight: 3 }, 'flow-tests/test-application-theme/test-theme-component-live-reload': { weight: 3 },