diff --git a/.gitignore b/.gitignore index 67d0e3a1d66..92027547aaf 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,6 @@ flow-tests/**/package.json flow-tests/**/webpack*.js flow-tests/**/tsconfig.json flow-tests/**/pnpm-lock.yaml -flow-tests/**/types.d.ts yarn.lock flow-client/src/main/resources/META-INF/resources/frontend/FlowClient.js 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 ea038c27d6e..c3ac9542258 100644 --- a/flow-client/src/main/java/com/vaadin/client/SystemErrorHandler.java +++ b/flow-client/src/main/java/com/vaadin/client/SystemErrorHandler.java @@ -20,8 +20,6 @@ import com.google.web.bindery.event.shared.UmbrellaException; -import com.google.gwt.core.client.Scheduler; - import com.vaadin.client.bootstrap.ErrorMessage; import elemental.client.Browser; @@ -58,12 +56,8 @@ public SystemErrorHandler(Registry registry) { * message details or null if there are no details */ public void handleSessionExpiredError(String details) { - // Run asynchronously to guarantee that all executions in the Uidl are - // done (#7581) - Scheduler.get() - .scheduleDeferred(() -> handleUnrecoverableError(details, - registry.getApplicationConfiguration() - .getSessionExpiredError())); + handleUnrecoverableError(details, registry.getApplicationConfiguration() + .getSessionExpiredError()); } /** diff --git a/flow-client/src/main/java/com/vaadin/client/WidgetUtil.java b/flow-client/src/main/java/com/vaadin/client/WidgetUtil.java index 5a251769c03..f519085ed3e 100644 --- a/flow-client/src/main/java/com/vaadin/client/WidgetUtil.java +++ b/flow-client/src/main/java/com/vaadin/client/WidgetUtil.java @@ -126,7 +126,7 @@ public static String toPrettyJson(JsonValue json) { * {@code value}. *

* If {@code value} is {@code null} then {@code attribute} is removed, - * otherwise {@code value.toString()} is set as its value. + * otherwise {@code value} is set as its value. * * @param element * the DOM element owning attribute @@ -136,11 +136,11 @@ public static String toPrettyJson(JsonValue json) { * the value to update */ public static void updateAttribute(Element element, String attribute, - Object value) { + String value) { if (value == null) { DomApi.wrap(element).removeAttribute(attribute); } else { - DomApi.wrap(element).setAttribute(attribute, value.toString()); + DomApi.wrap(element).setAttribute(attribute, value); } } @@ -227,7 +227,7 @@ public static native boolean hasJsProperty(Object object, String name) /** * Checks if the given value is explicitly undefined. null * values returns false. - * + * * @param property * the value to be verified * @return true is the value is explicitly undefined, diff --git a/flow-client/src/main/java/com/vaadin/client/flow/binding/SimpleElementBindingStrategy.java b/flow-client/src/main/java/com/vaadin/client/flow/binding/SimpleElementBindingStrategy.java index 45af2f858c1..22c679852dc 100644 --- a/flow-client/src/main/java/com/vaadin/client/flow/binding/SimpleElementBindingStrategy.java +++ b/flow-client/src/main/java/com/vaadin/client/flow/binding/SimpleElementBindingStrategy.java @@ -24,6 +24,7 @@ import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.Scheduler; +import com.vaadin.client.ApplicationConfiguration; import com.vaadin.client.Command; import com.vaadin.client.Console; import com.vaadin.client.ElementUtil; @@ -289,9 +290,9 @@ private native void bindPolymerModelProperties(StateNode node, private native void hookUpPolymerElement(StateNode node, Element element) /*-{ var self = this; - + var originalPropertiesChanged = element._propertiesChanged; - + if (originalPropertiesChanged) { element._propertiesChanged = function (currentProps, changedProps, oldProps) { $entry(function () { @@ -300,16 +301,16 @@ private native void hookUpPolymerElement(StateNode node, Element element) originalPropertiesChanged.apply(this, arguments); }; } - - + + var tree = node.@com.vaadin.client.flow.StateNode::getTree()(); - + var originalReady = element.ready; - + element.ready = function (){ originalReady.apply(this, arguments); @com.vaadin.client.PolymerUtils::fireReadyEvent(*)(element); - + // The _propertiesChanged method which is replaced above for the element // doesn't do anything for items in dom-repeat. // Instead it's called with some meaningful info for the dom-repeat element. @@ -318,7 +319,7 @@ private native void hookUpPolymerElement(StateNode node, Element element) // which changes this method for any dom-repeat instance. var replaceDomRepeatPropertyChange = function(){ var domRepeat = element.root.querySelector('dom-repeat'); - + if ( domRepeat ){ // If the dom-repeat element is in the DOM then // this method should not be executed anymore. The logic below will replace @@ -332,12 +333,12 @@ private native void hookUpPolymerElement(StateNode node, Element element) // if dom-repeat is found => replace _propertiesChanged method in the prototype and mark it as replaced. if ( !domRepeat.constructor.prototype.$propChangedModified){ domRepeat.constructor.prototype.$propChangedModified = true; - + var changed = domRepeat.constructor.prototype._propertiesChanged; - + domRepeat.constructor.prototype._propertiesChanged = function(currentProps, changedProps, oldProps){ changed.apply(this, arguments); - + var props = Object.getOwnPropertyNames(changedProps); var items = "items."; var i; @@ -358,7 +359,7 @@ private native void hookUpPolymerElement(StateNode node, Element element) if( currentPropsItem && currentPropsItem.nodeId ){ var nodeId = currentPropsItem.nodeId; var value = currentPropsItem[propertyName]; - + // this is an attempt to find the template element // which is not available as a context in the protype method var host = this.__dataHost; @@ -369,7 +370,7 @@ private native void hookUpPolymerElement(StateNode node, Element element) while( !host.localName || host.__dataHost ){ host = host.__dataHost; } - + $entry(function () { @SimpleElementBindingStrategy::handleListItemPropertyChange(*)(nodeId, host, propertyName, value, tree); })(); @@ -380,7 +381,7 @@ private native void hookUpPolymerElement(StateNode node, Element element) }; } }; - + // dom-repeat doesn't have to be in DOM even if template has it // such situation happens if there is dom-if e.g. which evaluates to false initially. // in this case dom-repeat is not yet in the DOM tree until dom-if becomes true @@ -395,7 +396,7 @@ private native void hookUpPolymerElement(StateNode node, Element element) element.addEventListener('dom-change',replaceDomRepeatPropertyChange); } } - + }-*/; private static void handleListItemPropertyChange(double nodeId, @@ -629,7 +630,10 @@ private void updateVisibility(JsArray listeners, private void updateVisibility(Element element, NodeMap visibilityData, Boolean visibility) { storeInitialHiddenAttribute(element, visibilityData); - WidgetUtil.updateAttribute(element, HIDDEN_ATTRIBUTE, visibility); + updateAttributeValue( + visibilityData.getNode().getTree().getRegistry() + .getApplicationConfiguration(), + element, HIDDEN_ATTRIBUTE, visibility); } private void restoreInitialHiddenAttribute(Element element, @@ -637,8 +641,10 @@ private void restoreInitialHiddenAttribute(Element element, MapProperty initialVisibility = storeInitialHiddenAttribute(element, visibilityData); if (initialVisibility.hasValue()) { - WidgetUtil.updateAttribute(element, HIDDEN_ATTRIBUTE, - initialVisibility.getValue()); + updateAttributeValue( + visibilityData.getNode().getTree().getRegistry() + .getApplicationConfiguration(), + element, HIDDEN_ATTRIBUTE, initialVisibility.getValue()); } } @@ -733,7 +739,10 @@ private void updateStyleProperty(MapProperty mapProperty, Element element) { private void updateAttribute(MapProperty mapProperty, Element element) { String name = mapProperty.getName(); - WidgetUtil.updateAttribute(element, name, mapProperty.getValue()); + updateAttributeValue( + mapProperty.getMap().getNode().getTree().getRegistry() + .getApplicationConfiguration(), + element, name, mapProperty.getValue()); } private EventRemover bindChildren(BindingContext context) { @@ -1352,6 +1361,35 @@ private EventRemover bindClientCallableMethods(BindingContext context) { (Element) context.htmlNode, context.node); } + private static void updateAttributeValue( + ApplicationConfiguration configuration, Element element, + String attribute, Object value) { + if (value == null || value instanceof String) { + WidgetUtil.updateAttribute(element, attribute, (String) value); + } else { + JsonValue jsonValue = WidgetUtil.crazyJsoCast(value); + if (JsonType.OBJECT.equals(jsonValue.getType())) { + JsonObject object = (JsonObject) jsonValue; + assert object.hasKey( + NodeProperties.URI_ATTRIBUTE) : "Implementation error: JsonObject is recieved as an attribute value for '" + + attribute + "' but it has no " + + NodeProperties.URI_ATTRIBUTE + " key"; + String uri = object.getString(NodeProperties.URI_ATTRIBUTE); + if (configuration.isWebComponentMode()) { + String baseUri = configuration.getServiceUrl(); + baseUri = baseUri.endsWith("/") ? baseUri : baseUri + "/"; + WidgetUtil.updateAttribute(element, attribute, + baseUri + uri); + } else { + WidgetUtil.updateAttribute(element, attribute, uri); + } + } else { + WidgetUtil.updateAttribute(element, attribute, + value.toString()); + } + } + } + private static EventExpression getOrCreateExpression( String expressionString) { if (expressionCache == null) { diff --git a/flow-client/src/main/resources/META-INF/resources/frontend/VaadinDevmodeGizmo.js b/flow-client/src/main/resources/META-INF/resources/frontend/VaadinDevmodeGizmo.js index fa62e4aa112..e6694a96fd1 100644 --- a/flow-client/src/main/resources/META-INF/resources/frontend/VaadinDevmodeGizmo.js +++ b/flow-client/src/main/resources/META-INF/resources/frontend/VaadinDevmodeGizmo.js @@ -344,9 +344,54 @@ class VaadinDevmodeGizmo extends LitElement { this.messages.push(msg); } - demoteNotification() { - if (this.notification) { - this.showMessage(this.notification); + dismissNotification(id) { + const index = this.findNotificationIndex(id); + if (index !== -1 && !this.notifications[index].deleted) { + const notification = this.notifications[index]; + + // user is explicitly dismissing a notification---after that we won't bug them with it + if (notification.dontShowAgain && notification.persistentId && !VaadinDevmodeGizmo.notificationDismissed(notification.persistentId)) { + let dismissed = window.localStorage.getItem(VaadinDevmodeGizmo.DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE); + if (dismissed === null) { + dismissed = notification.persistentId; + } else { + dismissed = dismissed + ',' + notification.persistentId; + } + window.localStorage.setItem(VaadinDevmodeGizmo.DISMISSED_NOTIFICATIONS_IN_LOCAL_STORAGE, dismissed); + } + + notification.deleted = true; + this.showMessage(notification.type, notification.message, notification.details, notification.link); + + const self = this; + // give some time for the animation + setTimeout(() => { + const index = self.findNotificationIndex(id); + if (index != -1) { + this.notifications.splice(index, 1); + this.requestUpdate(); + } + }, this.__transitionDuration); + } + } + + findNotificationIndex(id) { + let index = -1; + this.notifications.some((notification, idx) => { + if (notification.id === id) { + index = idx; + return true; + } + }); + return index; + } + + toggleDontShowAgain(id) { + const index = this.notifications.findIndex(notification => notification.id === id); + if (index !== -1 && !this.notifications[index].deleted) { + const notification = this.notifications[index]; + notification.dontShowAgain = !notification.dontShowAgain; + this.requestUpdate(); } this.showNotification(null); } diff --git a/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtEventHandlerTest.java b/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtEventHandlerTest.java index 6b133627ac3..72dd917d900 100644 --- a/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtEventHandlerTest.java +++ b/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtEventHandlerTest.java @@ -21,6 +21,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import com.vaadin.client.ApplicationConfiguration; import com.vaadin.client.ClientEngineTestBase; import com.vaadin.client.Registry; import com.vaadin.client.WidgetUtil; @@ -66,6 +67,8 @@ protected void gwtSetUp() throws Exception { registry = new Registry() { { set(ConstantPool.class, new ConstantPool()); + set(ApplicationConfiguration.class, + new ApplicationConfiguration()); } }; @@ -138,7 +141,8 @@ public void testClientCallablePromises() { delayTestFinish(100); } - private static native void addThen(Object promise, Consumer callback) + private static native void addThen(Object promise, + Consumer callback) /*-{ promise.then($entry(function(value) { callback.@Consumer::accept(*)(value); diff --git a/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtMultipleBindingTest.java b/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtMultipleBindingTest.java index b6455c78626..73a88166079 100644 --- a/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtMultipleBindingTest.java +++ b/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtMultipleBindingTest.java @@ -15,6 +15,7 @@ */ package com.vaadin.client.flow; +import com.vaadin.client.ApplicationConfiguration; import com.vaadin.client.ClientEngineTestBase; import com.vaadin.client.ExistingElementMap; import com.vaadin.client.Registry; @@ -91,6 +92,8 @@ protected void gwtSetUp() throws Exception { { set(ConstantPool.class, new ConstantPool()); set(ExistingElementMap.class, new ExistingElementMap()); + set(ApplicationConfiguration.class, + new ApplicationConfiguration()); } }; diff --git a/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtPropertyElementBinderTest.java b/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtPropertyElementBinderTest.java index 2cdb5b8e5f6..e263576fb3a 100644 --- a/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtPropertyElementBinderTest.java +++ b/flow-client/src/test-gwt/java/com/vaadin/client/flow/GwtPropertyElementBinderTest.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; +import com.vaadin.client.ApplicationConfiguration; import com.vaadin.client.ClientEngineTestBase; import com.vaadin.client.ExistingElementMap; import com.vaadin.client.InitialPropertiesHandler; @@ -58,6 +59,7 @@ private static class TestRegistry extends Registry { ExistingElementMap existingElementMap) { this.constantPool = constantPool; this.existingElementMap = existingElementMap; + set(ApplicationConfiguration.class, new ApplicationConfiguration()); } @Override diff --git a/flow-server/src/main/java/com/vaadin/flow/component/HasStyle.java b/flow-server/src/main/java/com/vaadin/flow/component/HasStyle.java index e9bc2c30456..33b3859b7f8 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/HasStyle.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/HasStyle.java @@ -63,7 +63,11 @@ default boolean removeClassName(String className) { * null to remove all class names */ default void setClassName(String className) { - getElement().setAttribute("class", className); + if(className == null) { + getElement().removeAttribute("class"); + } else { + getElement().setAttribute("class", className); + } } /** diff --git a/flow-server/src/main/java/com/vaadin/flow/component/Text.java b/flow-server/src/main/java/com/vaadin/flow/component/Text.java index 84878ca38b8..507889e25da 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/Text.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/Text.java @@ -19,6 +19,15 @@ /** * A component which encapsulates the given text in a text node. + *

+ * Text node doesn't support setting any attribute or property so you may not + * use Element API (and {@link Text} doesn't provide any such contract) for + * setting attribute/property. It implies that you may not style this component + * as well. Any attempt to set attribute/property value throws an exception. The + * only available API for a {@link Text} component is set a text. + *

+ * If you need a text component which can be styled then check {@code Span} + * class (from {@code flow-html-components}) module. * * @author Vaadin Ltd * @since 1.0 @@ -59,4 +68,39 @@ public String getText() { return getElement().getText(); } + @Override + protected void set(PropertyDescriptor descriptor, T value) { + throw new UnsupportedOperationException("Cannot set '" + + descriptor.getPropertyName() + "' property to the " + + getClass().getSimpleName() + " component because it doesn't " + + "represent an HTML Element but a text Node on the client side."); + } + + /** + * The method is not supported for the {@link Text} class. + *

+ * Always throws an {@link UnsupportedOperationException}. + * + * @throws UnsupportedOperationException + */ + @Override + public void setId(String id) { + super.setId(id); + } + + /** + * The method is not supported for the {@link Text} class. + *

+ * Always throws an {@link UnsupportedOperationException}. + * + * @throws UnsupportedOperationException + */ + @Override + public void setVisible(boolean visible) { + throw new UnsupportedOperationException("Cannot change " + + getClass().getSimpleName() + + " component visibility because it doesn't " + + "represent an HTML Element but a text Node on the client side."); + } + } diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUI.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUI.java index ee28d9e4a86..11f7a723115 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUI.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUI.java @@ -177,30 +177,23 @@ private boolean renderViewForRoute(Location location) { if (!shouldHandleNavigation(location)) { return false; } - try { - getInternals().setLastHandledNavigation(location); - Optional navigationState = this.getRouter() - .resolveNavigationTarget(location); - if (navigationState.isPresent()) { - // There is a valid route in flow. - return handleNavigation(location, navigationState.get()); - } else { - // When route does not exist, try to navigate to current route - // in order to check if current view can be left before showing - // the error page - if (navigateToPlaceholder(location)) { - return true; - } - - // Route does not exist, and current view does not prevent - // navigation - // thus an error page is shown - handleErrorNavigation(location); + getInternals().setLastHandledNavigation(location); + Optional navigationState = this.getRouter().resolveNavigationTarget(location); + if (navigationState.isPresent()) { + // There is a valid route in flow. + return handleNavigation(location, navigationState.get()); + } else { + // When route does not exist, try to navigate to current route + // in order to check if current view can be left before showing + // the error page + if (navigateToPlaceholder(location)) { + return true; } - } catch (Exception exception) { - return handleExceptionNavigation(location, exception); - } finally { - getInternals().clearLastHandledNavigation(); + + // Route does not exist, and current view does not prevent + // navigation + // thus an error page is shown + handleErrorNavigation(location); } return false; } @@ -224,35 +217,39 @@ private String removeLastSlash(String route) { return route.replaceFirst("/+$", ""); } - private boolean handleNavigation(Location location, - NavigationState navigationState) { - NavigationEvent navigationEvent = new NavigationEvent(getRouter(), - location, this, NavigationTrigger.CLIENT_SIDE); - - NavigationStateRenderer clientNavigationStateRenderer = new NavigationStateRenderer( - navigationState); - - clientNavigationStateRenderer.handle(navigationEvent); - - isUnknownRoute = false; - hasForwardTo = false; - // true if has forwardTo in server-views - if (!getInternals().getActiveRouterTargetsChain().isEmpty() - && getInternals().getActiveRouterTargetsChain().get(0).getClass().getAnnotation(Route.class) != null - && !getInternals().getActiveRouterTargetsChain().get(0).getClass().getAnnotation(Route.class) - .value().contains(getInternals().getActiveViewLocation().getPathWithQueryParameters())) { - // true if the forwardTo target is client-view - isUnknownRoute = !this.getRouter() - .resolveNavigationTarget(new Location(removeFirstSlash(this.getInternals() - .getActiveViewLocation().getPathWithQueryParameters()))).isPresent(); - if (isUnknownRoute) { - forwardToUrl = this.getInternals().getActiveViewLocation().getPathWithQueryParameters(); + private boolean handleNavigation(Location location, NavigationState navigationState) { + try { + NavigationEvent navigationEvent = new NavigationEvent(getRouter(), location, this, + NavigationTrigger.CLIENT_SIDE); + + NavigationStateRenderer clientNavigationStateRenderer = new NavigationStateRenderer(navigationState); + + clientNavigationStateRenderer.handle(navigationEvent); + + isUnknownRoute = false; + hasForwardTo = false; + // true if has forwardTo in server-views + if (!getInternals().getActiveRouterTargetsChain().isEmpty() + && getInternals().getActiveRouterTargetsChain().get(0).getClass().getAnnotation(Route.class) != null + && !getInternals().getActiveRouterTargetsChain().get(0).getClass().getAnnotation(Route.class) + .value().contains(getInternals().getActiveViewLocation().getPathWithQueryParameters())) { + // true if the forwardTo target is client-view + isUnknownRoute = !this.getRouter().resolveNavigationTarget(new Location( + removeFirstSlash(this.getInternals().getActiveViewLocation().getPathWithQueryParameters()))) + .isPresent(); + if (isUnknownRoute) { + forwardToUrl = this.getInternals().getActiveViewLocation().getPathWithQueryParameters(); + } + hasForwardTo = true; } - hasForwardTo = true; - } - adjustPageTitle(); + adjustPageTitle(); - return getInternals().getContinueNavigationAction() != null; + return getInternals().getContinueNavigationAction() != null; + } catch (Exception exception) { + return handleExceptionNavigation(location, exception); + } finally { + getInternals().clearLastHandledNavigation(); + } } private boolean handleExceptionNavigation(Location location, Exception exception) { diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/ElementAttributeMap.java b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/ElementAttributeMap.java index a67cf08bed8..b74d1a5ce2c 100644 --- a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/ElementAttributeMap.java +++ b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/ElementAttributeMap.java @@ -34,6 +34,9 @@ import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.shared.Registration; +import elemental.json.Json; +import elemental.json.JsonObject; + /** * Map for element attribute values. * @@ -65,8 +68,7 @@ public ElementAttributeMap(StateNode node) { * the value */ public void set(String attribute, String value) { - unregisterResource(attribute); - put(attribute, value); + doSet(attribute, value); } /** @@ -103,7 +105,19 @@ public Serializable remove(String attribute) { */ @Override public String get(String attribute) { - return (String) super.get(attribute); + Serializable value = super.get(attribute); + if (value == null || value instanceof String) { + return (String) value; + } else { + // If the value is not a string then current impl only uses + // JsonObject + assert value instanceof JsonObject; + JsonObject object = (JsonObject) value; + // The only object which may be set by the current imlp contains + // "uri" attribute, only this situation is expected here. + assert object.hasKey(NodeProperties.URI_ATTRIBUTE); + return object.getString(NodeProperties.URI_ATTRIBUTE); + } } /** @@ -142,7 +156,11 @@ private void doSetResource(String attribute, } else { targetUri = StreamResourceRegistry.getURI(resource); } - set(attribute, targetUri.toASCIIString()); + JsonObject object = Json.createObject(); + object.put(NodeProperties.URI_ATTRIBUTE, targetUri.toASCIIString()); + // don't use sring as a value, but wrap it into an object to let know + // the client side about specific nature of the value + doSet(attribute, object); } private void ensurePendingRegistrations() { @@ -225,6 +243,11 @@ public void execute() { })); } + private void doSet(String attribute, Serializable value) { + unregisterResource(attribute); + put(attribute, value); + } + private void unsetResource(String attribute) { ensureResourceRegistrations(); StreamRegistration registration = resourceRegistrations.get(attribute); diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeProperties.java b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeProperties.java index 893b51e39c4..dd4008ab675 100644 --- a/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeProperties.java +++ b/flow-server/src/main/java/com/vaadin/flow/internal/nodefeature/NodeProperties.java @@ -87,6 +87,14 @@ public final class NodeProperties { */ public static final String VISIBILITY_HIDDEN_PROPERTY = "hidden"; + /** + * The property in Json object which marks the object as special value + * transmitting URI (not just any string). + *

+ * Used in the {@link ElementAttributeMap}. + */ + public static final String URI_ATTRIBUTE = "uri"; + private NodeProperties() { } } 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 5211755ab4c..0cd47637cfd 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 @@ -898,7 +898,7 @@ private Element createInlineJavaScriptElement( // https://developer.mozilla.org/en/docs/Web/HTML/Element/script Element wrapper = createJavaScriptElement(null, false); wrapper.appendChild( - new DataNode(javaScriptContents, wrapper.baseUri())); + new DataNode(javaScriptContents)); return wrapper; } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/DefaultDeploymentConfiguration.java b/flow-server/src/main/java/com/vaadin/flow/server/DefaultDeploymentConfiguration.java index 01271e2447f..bf9c2d4f82b 100755 --- a/flow-server/src/main/java/com/vaadin/flow/server/DefaultDeploymentConfiguration.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/DefaultDeploymentConfiguration.java @@ -45,37 +45,34 @@ public class DefaultDeploymentConfiguration extends PropertyDeploymentConfiguration { - private static final String SEPARATOR = "\n======================================================================="; - private static final String HEADER = "\n=================== Vaadin DeploymentConfiguration ====================\n"; + public static final String NOT_PRODUCTION_MODE_INFO = "\nVaadin is running in DEBUG MODE.\n" + + "When deploying application for production, remember to disable debug features. See more from https://vaadin.com/docs/"; - public static final String NOT_PRODUCTION_MODE_INFO = " Vaadin is running in DEBUG MODE.\n" - + " When deploying application for production, remember to disable debug features. See more from https://vaadin.com/docs/"; + public static final String NOT_PRODUCTION_MODE_WARNING = "\nWARNING: Vaadin is running in DEBUG MODE with debug features enabled, but with a prebuild frontend bundle (production ready).\n" + + "When deploying application for production, disable debug features by enabling production mode!\n" + + "See more from https://vaadin.com/docs/v14/flow/production/tutorial-production-mode-basic.html"; - public static final String NOT_PRODUCTION_MODE_WARNING = " WARNING: Vaadin is running in DEBUG MODE with debug features enabled, but with a prebuild frontend bundle (production ready).\n" - + " When deploying application for production, disable debug features by enabling production mode!\n" - + " See more from https://vaadin.com/docs/v14/flow/production/tutorial-production-mode-basic.html"; - - public static final String WARNING_V14_BOOTSTRAP = " Using deprecated Vaadin 14 bootstrap mode.\n" - + " Client-side views written in TypeScript are not supported. Vaadin 15+ enables client-side and server-side views.\n" - + " See https://vaadin.com/docs/v15/flow/typescript/starting-the-app.html for more information."; + public static final String WARNING_V14_BOOTSTRAP = "Using deprecated Vaadin 14 bootstrap mode.\n" + + "Client-side views written in TypeScript are not supported. Vaadin 15+ enables client-side and server-side views.\n" + + "See https://vaadin.com/docs/v15/flow/typescript/starting-the-app.html for more information."; // not a warning anymore, but keeping variable name to avoid breaking anything - public static final String WARNING_V15_BOOTSTRAP = "%n Using Vaadin 15+ bootstrap mode.%n %s%n %s"; + public static final String WARNING_V15_BOOTSTRAP = "Using Vaadin 15+ bootstrap mode.%n %s%n %s"; - private static final String DEPLOYMENT_WARNINGS = " Following issues were discovered with deployment configuration:"; + private static final String DEPLOYMENT_WARNINGS = "Following issues were discovered with deployment configuration:"; - public static final String WARNING_XSRF_PROTECTION_DISABLED = " WARNING: Cross-site request forgery protection is disabled!"; + public static final String WARNING_XSRF_PROTECTION_DISABLED = "WARNING: Cross-site request forgery protection is disabled!"; - public static final String WARNING_HEARTBEAT_INTERVAL_NOT_NUMERIC = " WARNING: heartbeatInterval has been set to a non integer value." + public static final String WARNING_HEARTBEAT_INTERVAL_NOT_NUMERIC = "WARNING: heartbeatInterval has been set to a non integer value." + "\n The default of 5min will be used."; - public static final String WARNING_PUSH_MODE_NOT_RECOGNIZED = " WARNING: pushMode has been set to an unrecognized value.\n" - + " The permitted values are \"disabled\", \"manual\",\n" - + " and \"automatic\". The default of \"disabled\" will be used."; + public static final String WARNING_PUSH_MODE_NOT_RECOGNIZED = "WARNING: pushMode has been set to an unrecognized value.\n" + + "The permitted values are \"disabled\", \"manual\",\n" + + "and \"automatic\". The default of \"disabled\" will be used."; - private static final String INDEX_NOT_FOUND = " '%s' is not found from '%s'.%n" - + " Generating a default one in '%s%s'. " - + " Move it to the '%s' folder if you want to customize it."; + private static final String INDEX_NOT_FOUND = "'%s' is not found from '%s'.%n" + + "Generating a default one in '%s%s'. " + + "Move it to the '%s' folder if you want to customize it."; /** * Default value for {@link #getHeartbeatInterval()} = {@value} . @@ -159,18 +156,13 @@ private void logMessages() { Logger logger = LoggerFactory.getLogger(getClass().getName()); if (!warnings.isEmpty()) { - warnings.add(0, HEADER); - warnings.add(1, DEPLOYMENT_WARNINGS); - warnings.add("\n"); + warnings.add(0, DEPLOYMENT_WARNINGS); // merging info messages to warnings for now warnings.addAll(info); - warnings.add(SEPARATOR); if (logger.isWarnEnabled()) { logger.warn(String.join("\n", warnings)); } } else if (!info.isEmpty()) { - info.add(0, HEADER); - info.add(SEPARATOR); if (logger.isInfoEnabled()) { logger.info(String.join("\n", info)); } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java b/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java index e166e2f1e32..f27ccd08ce0 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/PwaRegistry.java @@ -28,6 +28,7 @@ import java.io.InputStreamReader; import java.io.Serializable; import java.io.UncheckedIOException; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.nio.charset.StandardCharsets; @@ -35,13 +36,13 @@ import java.util.List; import java.util.stream.Collectors; -import com.vaadin.flow.server.frontend.FrontendUtils; +import org.slf4j.LoggerFactory; + import com.vaadin.flow.server.startup.ApplicationRouteRegistry; import elemental.json.Json; import elemental.json.JsonArray; import elemental.json.JsonObject; -import org.slf4j.LoggerFactory; /** * Registry for PWA data. @@ -59,6 +60,8 @@ * @since 1.2 */ public class PwaRegistry implements Serializable { + + private static final String META_INF_RESOURCES = "/META-INF/resources"; private static final String HEADLESS_PROPERTY = "java.awt.headless"; private static final String APPLE_STARTUP_IMAGE = "apple-touch-startup-image"; private static final String APPLE_IMAGE_MEDIA = "(device-width: %dpx) and (device-height: %dpx) " @@ -97,18 +100,21 @@ public PwaRegistry(PWA pwa, ServletContext servletContext) // Build pwa elements only if they are enabled if (pwaConfiguration.isEnabled()) { - URL logo = servletContext - .getResource(pwaConfiguration.relIconPath()); - URL offlinePage = servletContext - .getResource(pwaConfiguration.relOfflinePath()); + URL logo = getResourceUrl(servletContext, + pwaConfiguration.relIconPath()); + + URL offlinePage = getResourceUrl(servletContext, + pwaConfiguration.relOfflinePath()); // Load base logo from servlet context if available // fall back to local image if unavailable BufferedImage baseImage = getBaseImage(logo); if (baseImage == null) { - LoggerFactory.getLogger(PwaRegistry.class).error("Image is not found or can't be loaded: " + logo); + LoggerFactory.getLogger(PwaRegistry.class).error( + "Image is not found or can't be loaded: " + logo); } else { - // Pick top-left pixel as fill color if needed for image resizing + // Pick top-left pixel as fill color if needed for image + // resizing int bgColor = baseImage.getRGB(0, 0); // initialize icons @@ -131,6 +137,19 @@ public PwaRegistry(PWA pwa, ServletContext servletContext) } } + private URL getResourceUrl(ServletContext context, String path) + throws MalformedURLException { + URL resourceUrl = context.getResource(path); + if (resourceUrl == null) { + // this is a workaround specific for Spring default static resources + // location: see #8705 + String cpPath = path.startsWith("/") ? META_INF_RESOURCES + path + : META_INF_RESOURCES + "/" + path; + resourceUrl = PwaRegistry.class.getResource(cpPath); + } + return resourceUrl; + } + private List initializeIcons(BufferedImage baseImage, int bgColor) { for (PwaIcon icon : getIconTemplates(pwaConfiguration.getIconPath())) { diff --git a/flow-server/src/main/java/com/vaadin/flow/server/UnsupportedBrowserHandler.java b/flow-server/src/main/java/com/vaadin/flow/server/UnsupportedBrowserHandler.java index c38ecb64ed4..cb6554104f8 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/UnsupportedBrowserHandler.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/UnsupportedBrowserHandler.java @@ -18,6 +18,8 @@ import java.io.IOException; import java.io.Writer; +import com.vaadin.flow.shared.ApplicationConstants; + /** * A {@link RequestHandler} that presents an informative page if the browser in * use is unsupported. @@ -104,6 +106,9 @@ protected void writeBrowserTooOldPage(VaadinRequest request, Writer page = response.getWriter(); WebBrowser browser = VaadinSession.getCurrent().getBrowser(); + response.setContentType( + ApplicationConstants.CONTENT_TYPE_TEXT_HTML_UTF_8); + // @formatter:off page.write( "" 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 e7a901df2bd..b34dd19fbe2 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 @@ -39,6 +39,8 @@ import elemental.json.JsonObject; import elemental.json.impl.JsonUtil; +import java.io.File; + import static com.vaadin.flow.component.internal.JavaScriptBootstrapUI.SERVER_ROUTING; import static com.vaadin.flow.shared.ApplicationConstants.CONTENT_TYPE_TEXT_HTML_UTF_8; import static com.vaadin.flow.shared.ApplicationConstants.CSRF_TOKEN; @@ -172,10 +174,16 @@ private static Document getIndexHtmlDocument(VaadinRequest request) } String frontendDir = FrontendUtils.getProjectFrontendDir( request.getService().getDeploymentConfiguration()); + String indexHtmlFilePath; + if(frontendDir.endsWith(File.separator)) { + indexHtmlFilePath = frontendDir + "index.html"; + } else { + indexHtmlFilePath = frontendDir + File.separatorChar + "index.html"; + } String message = String - .format("Failed to load content of '%1$sindex.html'." - + "It is required to have '%1$sindex.html' file when " - + "using client side bootstrapping.", frontendDir); + .format("Failed to load content of '%1$s'. " + + "It is required to have '%1$s' file when " + + "using client side bootstrapping.", indexHtmlFilePath); throw new IOException(message); } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java index 9a6369cedbb..1c576e68013 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java @@ -281,8 +281,6 @@ static Map getDefaultDevDependencies() { defaults.put("webpack-merge", "4.2.2"); defaults.put("raw-loader", "4.0.0"); - defaults.put("terser", "4.6.7"); - // Forcing chokidar version for now until new babel version is available // check out https://github.com/babel/babel/issues/11488 defaults.put("chokidar", "^3.4.0"); diff --git a/flow-server/src/main/java/com/vaadin/flow/server/startup/NavigationTargetFilter.java b/flow-server/src/main/java/com/vaadin/flow/server/startup/NavigationTargetFilter.java index 6a479994309..d3e330236a0 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/startup/NavigationTargetFilter.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/startup/NavigationTargetFilter.java @@ -27,7 +27,7 @@ * {@link ServiceLoader}. This means that all implementations must have a * zero-argument constructor and the fully qualified name of the implementation * class must be listed on a separate line in a - * META-INF/services/cocom.vaadin.flow.server.startup.NavigationTargetFilter + * META-INF/services/com.vaadin.flow.server.startup.NavigationTargetFilter * file present in the jar file containing the implementation class. * * @author Vaadin Ltd diff --git a/flow-server/src/main/java/com/vaadin/flow/server/startup/ServletDeployer.java b/flow-server/src/main/java/com/vaadin/flow/server/startup/ServletDeployer.java index 237e1a9dd90..c2a75271088 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/startup/ServletDeployer.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/startup/ServletDeployer.java @@ -73,7 +73,6 @@ */ public class ServletDeployer implements ServletContextListener { private static final String SKIPPING_AUTOMATIC_SERVLET_REGISTRATION_BECAUSE = "Skipping automatic servlet registration because"; - private static final String SEPARATOR = "======================================================================="; private enum VaadinServletCreation { NO_CREATION, SERVLET_EXISTS, SERVLET_CREATED; @@ -261,12 +260,11 @@ private void logServletCreation(VaadinServletCreation servletCreation, } /** - * Prints to sysout a notification to the user that the application is to be - * opened in the browser. + * Prints to sysout a notification to the user that the application has been deployed. *

* This method is public so that it can be called in add-ons that map * servlet automatically but don't use this class for that. - * + * * @param servletContext * the deployed servlet context * @param servletAutomaticallyCreated @@ -281,16 +279,14 @@ public static void logAppStartupToConsole(ServletContext servletContext, String contextPath = servletContext.getContextPath(); contextPath = contextPath.isEmpty() ? "/" : contextPath; - String url = String.format("http://localhost:8080%s", contextPath); FrontendUtils.console(FrontendUtils.BRIGHT_BLUE, String.format( - "%n%s%n%n Vaadin application has started in DEBUG MODE and is available by opening %s in the browser.%n%n NOTE: the server HTTP port may vary - see server log output.%n%n%s%n", - SEPARATOR, url, SEPARATOR)); + "Vaadin application has been deployed and started to the context path \"%s\".%n", + contextPath)); } else { // if the user has mapped their own servlet, they will know where to // find it FrontendUtils.console(FrontendUtils.BRIGHT_BLUE, String.format( - "%n%s%n%nVaadin application has started in DEBUG MODE and is available in the browser.%n%n%s%n", - SEPARATOR, SEPARATOR)); + "Vaadin application has been deployed and started.%n")); } } diff --git a/flow-server/src/main/resources/webpack.generated.js b/flow-server/src/main/resources/webpack.generated.js index 3437ecd7c97..3dde9df8372 100644 --- a/flow-server/src/main/resources/webpack.generated.js +++ b/flow-server/src/main/resources/webpack.generated.js @@ -214,8 +214,11 @@ function collectChunks(statsJson, acceptedChunks) { const slimModule = { id: module.id, name: module.name, - source: module.source, + source: module.source }; + if(module.modules) { + slimModule.modules = collectSubModules(module); + } modules.push(slimModule); }); const slimChunk = { @@ -245,27 +248,40 @@ function collectModules(statsJson, acceptedChunks) { statsJson.modules.forEach(function (module) { // Add module if module chunks contain an accepted chunk and the module is generated-flow-imports.js module if (module.chunks.filter(key => acceptedChunks.includes(key)).length > 0 - && (module.name.includes("generated-flow-imports.js") || module.name.includes("generated-flow-imports-fallback.js"))) { - let subModules = []; - // Create sub modules only if they are available - if (module.modules) { - module.modules.forEach(function (module) { - const subModule = { - name: module.name, - source: module.source - }; - subModules.push(subModule); - }); - } + && (module.name.includes("generated-flow-imports.js") || module.name.includes("generated-flow-imports-fallback.js"))) { const slimModule = { id: module.id, name: module.name, - source: module.source, - modules: subModules + source: module.source }; + if(module.modules) { + slimModule.modules = collectSubModules(module); + } modules.push(slimModule); } }); } return modules; } + +/** + * Collect any modules under a module (aka. submodules); + * + * @param module module to get submodules for + */ +function collectSubModules(module) { + let modules = []; + module.modules.forEach(function (submodule) { + if (submodule.source) { + const slimModule = { + name: submodule.name, + source: submodule.source, + }; + if(submodule.id) { + slimModule.id = submodule.id; + } + modules.push(slimModule); + } + }); + return modules; +} diff --git a/flow-server/src/test/java/com/vaadin/flow/component/HasStyleTest.java b/flow-server/src/test/java/com/vaadin/flow/component/HasStyleTest.java index 1047b73a6d7..9c498b8015f 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/HasStyleTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/HasStyleTest.java @@ -90,6 +90,11 @@ public void setClassName() { assertClasses(component); component.setClassName(""); assertClasses(component); + + component.setClassName("removeMe"); + // setting null to classname should remove class name attribute + component.setClassName(null); + assertClasses(component); } @Test diff --git a/flow-server/src/test/java/com/vaadin/flow/server/PwaRegistryTest.java b/flow-server/src/test/java/com/vaadin/flow/server/PwaRegistryTest.java new file mode 100644 index 00000000000..cc45abec010 --- /dev/null +++ b/flow-server/src/test/java/com/vaadin/flow/server/PwaRegistryTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2020 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.flow.server; + +import javax.servlet.ServletContext; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +@PWA(name = "foo", shortName = "bar") +public class PwaRegistryTest { + + @Test + public void pwaIconIsGeneratedBasedOnClasspathIcon_servletContextHasNoResources() + throws IOException { + ServletContext context = Mockito.mock(ServletContext.class); + // PWA annotation has default value for "iconPath" but servlet context + // has no resource for that path, in that case the ClassPath URL will be + // checked which is "META-INF/resources/icons/icon.png" (this path + // available is in the test resources folder). The icon in this path + // differs from the default icon and set of icons will be generated + // based on it + PwaRegistry registry = new PwaRegistry( + PwaRegistryTest.class.getAnnotation(PWA.class), context); + List icons = registry.getIcons(); + // This icon has width 32 and it's generated based on a custom icon (see + // above) + PwaIcon pwaIcon = icons.stream().filter(icon -> icon.getWidth() == 32) + .findFirst().get(); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + pwaIcon.write(stream); + // the default image has 47 on the position 36 + Assert.assertEquals(26, stream.toByteArray()[36]); + } +} diff --git a/flow-server/src/test/java/com/vaadin/flow/server/UnsupportedBrowserHandlerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/UnsupportedBrowserHandlerTest.java index 340ce4e04fc..8d819841c0c 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/UnsupportedBrowserHandlerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/UnsupportedBrowserHandlerTest.java @@ -9,6 +9,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import com.vaadin.flow.shared.ApplicationConstants; import com.vaadin.tests.util.MockDeploymentConfiguration; public class UnsupportedBrowserHandlerTest { @@ -72,6 +73,9 @@ public void testUnsupportedBrowserHandler_tooOldBrowser_returnsUnsupportedBrowse Assert.assertTrue("Unsupported browser page not used", pageCapture.getValue().contains( "I'm sorry, but your browser is not supported")); + + Mockito.verify(response).setContentType( + ApplicationConstants.CONTENT_TYPE_TEXT_HTML_UTF_8); } @Test @@ -94,6 +98,15 @@ public void testUnsupportedBrowserHandler_validBrowserWithForceReloadCookie_does Mockito.verify(writer, Mockito.never()).write(Mockito.anyString()); } + @Test + public void writeBrowserTooOldPage_setContentType() throws IOException { + initMocks(true, true); + handler.writeBrowserTooOldPage(request, response); + + Mockito.verify(response).setContentType( + ApplicationConstants.CONTENT_TYPE_TEXT_HTML_UTF_8); + } + @After public void tearDown() { VaadinSession.setCurrent(null); 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 11caa930571..9a455f1dfe6 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 @@ -21,6 +21,7 @@ import java.io.File; import java.io.IOException; import java.lang.reflect.Method; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -36,6 +37,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.mockito.Mockito; @@ -46,6 +48,7 @@ import com.vaadin.flow.server.DevModeHandler; import com.vaadin.flow.server.MockServletServiceSessionSetup; import com.vaadin.flow.server.VaadinResponse; +import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.VaadinServletRequest; import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.server.frontend.FrontendUtils; @@ -57,9 +60,13 @@ import elemental.json.JsonObject; import static com.vaadin.flow.component.internal.JavaScriptBootstrapUI.SERVER_ROUTING; +import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES; import static com.vaadin.flow.server.DevModeHandlerTest.createStubWebpackTcpListener; +import static com.vaadin.flow.server.frontend.FrontendUtils.INDEX_HTML; import static com.vaadin.flow.server.frontend.NodeUpdateTestUtil.createStubWebpackServer; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class IndexHtmlRequestHandlerTest { private MockServletServiceSessionSetup mocks; @@ -73,6 +80,8 @@ public class IndexHtmlRequestHandlerTest { @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); @Before public void setUp() throws Exception { @@ -103,6 +112,41 @@ public void serveIndexHtml_requestWithRootPath_serveContentFromTemplate() indexHtml.contains(".v-system-error")); } + @Test + public void serveNotFoundIndexHtml_requestWithRootPath_failsWithIOException() + throws IOException { + VaadinServletRequest vaadinServletRequest = createVaadinRequest("/"); + VaadinService vaadinService = vaadinServletRequest.getService(); + + // Finding index.html URL + String indexHtmlPathInProductionMode = VAADIN_SERVLET_RESOURCES + + INDEX_HTML; + URL url = vaadinService.getClassLoader().getResource(indexHtmlPathInProductionMode); + + assertNotNull(url); + File indexHtmlFile = new File(url.getPath()); + File indexHtmlFileTmp = new File(url.getPath() + "_tmp"); + try { + // Renaming file to simulate the absence of index.html + boolean renamed = indexHtmlFile.renameTo(indexHtmlFileTmp); + assertTrue(renamed); + + String expectedError = "Failed to load content of './frontend/index.html'. " + + "It is required to have './frontend/index.html' file " + + "when using client side bootstrapping."; + + exceptionRule.expect(IOException.class); + exceptionRule.expectMessage(expectedError); + + indexHtmlRequestHandler.synchronizedHandleRequest(session, + vaadinServletRequest, response); + } finally { + // Restoring index.html + boolean renamed = indexHtmlFileTmp.renameTo(indexHtmlFile); + assertTrue(renamed); + } + } + @Test public void serveIndexHtml_requestWithRootPath_hasBaseHrefElement() throws IOException { diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java index 07fcb9833b0..6315f1e01ef 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java @@ -180,14 +180,6 @@ public void updateDefaultDependencies_newerVersionsAreNotChanged() .getObject(NodeUpdater.DEV_DEPENDENCIES).getString("webpack")); } - @Test - public void assertTerserVersion() throws IOException { - final JsonObject packageJson = nodeUpdater.getPackageJson(); - nodeUpdater.updateDefaultDependencies(packageJson); - Assert.assertEquals("4.6.7", packageJson - .getObject(NodeUpdater.DEV_DEPENDENCIES).getString("terser")); - } - private String getPolymerVersion(JsonObject object) { JsonObject deps = object.get("dependencies"); String version = deps.getString("@polymer/polymer"); diff --git a/flow-server/src/test/resources/META-INF/resources/icons/icon.png b/flow-server/src/test/resources/META-INF/resources/icons/icon.png new file mode 100644 index 00000000000..e30218e22dc Binary files /dev/null and b/flow-server/src/test/resources/META-INF/resources/icons/icon.png differ diff --git a/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/UnauthenticatedException.java b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/UnauthenticatedException.java new file mode 100644 index 00000000000..3257affeccc --- /dev/null +++ b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/UnauthenticatedException.java @@ -0,0 +1,20 @@ +/* + * Copyright 2000-2020 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.flow.ccdmtest; + +public class UnauthenticatedException extends RuntimeException +{ +} diff --git a/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/UnauthenticatedExceptionHandler.java b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/UnauthenticatedExceptionHandler.java new file mode 100644 index 00000000000..72faed2ccd8 --- /dev/null +++ b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/UnauthenticatedExceptionHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2020 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.flow.ccdmtest; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.ErrorParameter; +import com.vaadin.flow.router.HasErrorParameter; + +import javax.servlet.http.HttpServletResponse; + + +@Tag(Tag.DIV) +public class UnauthenticatedExceptionHandler + extends Component + implements HasErrorParameter +{ + + @Override + public int setErrorParameter(BeforeEnterEvent event, + ErrorParameter + parameter) { + setId("errorView"); + getElement().setText("Tried to navigate to a view without being authenticated"); + return HttpServletResponse.SC_UNAUTHORIZED; + } +} diff --git a/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewThrowsException.java b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewThrowsException.java new file mode 100644 index 00000000000..ef84f2c6999 --- /dev/null +++ b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewThrowsException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2020 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.flow.ccdmtest; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.HasDynamicTitle; +import com.vaadin.flow.router.Route; + +@Route("view-throws-exception") +public class ViewThrowsException extends Div implements HasDynamicTitle { + + public ViewThrowsException() { + Span textField = new Span("You should not see this page, you cannot go back to the main page"); + + add(textField); + } + + @Override + public String getPageTitle() { + // Use backend information + throw new UnauthenticatedException(); + } +} diff --git a/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewWithServerViewButton.java b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewWithServerViewButton.java index 84bc4c9cbc1..cc687eb845a 100644 --- a/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewWithServerViewButton.java +++ b/flow-tests/test-ccdm/src/main/java/com/vaadin/flow/ccdmtest/ViewWithServerViewButton.java @@ -27,6 +27,12 @@ public ViewWithServerViewButton() { NativeButton serverViewButton = new NativeButton("Server view", e -> UI.getCurrent().navigate("serverview")); serverViewButton.setId("serverViewButton"); - add(serverViewButton); + + NativeButton serverViewThrowsExcpetionButton = + new NativeButton("Go to a server view that thorws exception", + e -> UI.getCurrent().navigate(ViewThrowsException.class)); + serverViewThrowsExcpetionButton.setId("serverViewThrowsExcpetionButton"); + + add(serverViewButton, serverViewThrowsExcpetionButton); } } diff --git a/flow-tests/test-ccdm/src/test/java/com/vaadin/flow/ccdmtest/ServerSideNavigationExceptionHandlingIT.java b/flow-tests/test-ccdm/src/test/java/com/vaadin/flow/ccdmtest/ServerSideNavigationExceptionHandlingIT.java new file mode 100644 index 00000000000..7b26ad1da5f --- /dev/null +++ b/flow-tests/test-ccdm/src/test/java/com/vaadin/flow/ccdmtest/ServerSideNavigationExceptionHandlingIT.java @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2020 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.flow.ccdmtest; + +import java.util.List; + +import com.vaadin.flow.testutil.ChromeBrowserTest; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +public class ServerSideNavigationExceptionHandlingIT extends ChromeBrowserTest { + + private void openTestUrl(String url) { + getDriver().get(getRootURL() + "/foo" + url); + waitForDevServer(); + } + + @Test + public void should_showErrorView_when_targetViewThrowsException() { + openVaadinRouter("/"); + + findAnchor("view-with-server-view-button").click(); + + // Navigate to a server-side view that throws exception. + findElement(By.id("serverViewThrowsExcpetionButton")).click(); + + assertView("errorView", "Tried to navigate to a view without being authenticated", "view-throws-exception"); + } + + protected final void openVaadinRouter(String url) { + openTestUrl(url); + + waitForElementPresent(By.id("loadVaadinRouter")); + findElement(By.id("loadVaadinRouter")).click(); + waitForElementPresent(By.id("outlet")); + } + + protected final WebElement findAnchor(String href) { + final List anchors = findElements(By.tagName("a")); + for (WebElement element : anchors) { + if (element.getAttribute("href").endsWith(href)) { + return element; + } + } + + return null; + } + + protected final void assertView(String viewId, String assertViewText, String assertViewRoute) { + waitForElementPresent(By.id(viewId)); + final WebElement serverViewDiv = findElement(By.id(viewId)); + + Assert.assertEquals(assertViewText, serverViewDiv.getText()); + assertCurrentRoute(assertViewRoute); + } + + protected final void assertCurrentRoute(String route) { + final String currentUrl = getDriver().getCurrentUrl(); + Assert.assertTrue(String.format("Expecting route '%s', but url is '%s'", + route, currentUrl), currentUrl.endsWith(route)); + } + + +} diff --git a/flow-tests/test-embedding/embedding-test-assets/src/main/java/com/vaadin/flow/webcomponent/ClientSelectComponent.java b/flow-tests/test-embedding/embedding-test-assets/src/main/java/com/vaadin/flow/webcomponent/ClientSelectComponent.java index 0f3be9e2e97..9b46a85a496 100644 --- a/flow-tests/test-embedding/embedding-test-assets/src/main/java/com/vaadin/flow/webcomponent/ClientSelectComponent.java +++ b/flow-tests/test-embedding/embedding-test-assets/src/main/java/com/vaadin/flow/webcomponent/ClientSelectComponent.java @@ -15,6 +15,7 @@ */ package com.vaadin.flow.webcomponent; +import java.io.ByteArrayInputStream; import java.util.Optional; import com.vaadin.flow.component.AbstractField; @@ -22,8 +23,10 @@ import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.dependency.JsModule; +import com.vaadin.flow.component.html.Anchor; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.server.StreamResource; @JsModule("./src/Dependency.js") public class ClientSelectComponent extends Div { @@ -42,6 +45,12 @@ public ClientSelectComponent() { select.addValueChangeListener(this::setValue); add(select, message); + Anchor getPdf = new Anchor(); + getPdf.setText("Download PDF"); + getPdf.setId("link"); + getPdf.setHref(createPdfStreamResource()); + getPdf.getElement().setAttribute("download", true); + add(getPdf); } @Override @@ -49,6 +58,13 @@ protected void onAttach(AttachEvent attachEvent) { add(new DepElement()); } + private StreamResource createPdfStreamResource() { + StreamResource streamResource = new StreamResource("label.pdf", + () -> new ByteArrayInputStream(new byte[] { 1 })); + streamResource.setContentType("application/pdf"); + return streamResource; + } + private void setValue( AbstractField.ComponentValueChangeEvent event) { String messageText = "No selection"; diff --git a/flow-tests/test-embedding/embedding-test-assets/src/test/java/com/vaadin/flow/webcomponent/WebComponentIT.java b/flow-tests/test-embedding/embedding-test-assets/src/test/java/com/vaadin/flow/webcomponent/WebComponentIT.java index dea5d9f4311..f841d7f6dd1 100644 --- a/flow-tests/test-embedding/embedding-test-assets/src/test/java/com/vaadin/flow/webcomponent/WebComponentIT.java +++ b/flow-tests/test-embedding/embedding-test-assets/src/test/java/com/vaadin/flow/webcomponent/WebComponentIT.java @@ -15,6 +15,7 @@ */ package com.vaadin.flow.webcomponent; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; import org.openqa.selenium.By; @@ -57,8 +58,25 @@ public void indexPageGetsWebComponent_attributeIsReflectedToServer() { select); Assert.assertFalse("Message should not be visible", - noMessage.$("span").first() - .isDisplayed()); + noMessage.$("span").first().isDisplayed()); + } + + @Test + public void downloadLinkHasCorrectBaseURL() { + open(); + + waitForElementVisible(By.id("show-message")); + TestBenchElement showMessage = byId("show-message"); + TestBenchElement link = showMessage.$("a").id("link"); + String href = link.getAttribute("href"); + // self check + Assert.assertTrue(href.startsWith(getRootURL())); + // remove host and port + href = href.substring(getRootURL().length()); + // now the URI should starts with "/vaadin" since this is the URI of + // embedded app + Assert.assertThat(href, + CoreMatchers.startsWith("/vaadin/VAADIN/dynamic/resource/")); } @Test @@ -68,13 +86,13 @@ public void indexPageGetsThemedWebComponent_themeIsApplied() { waitForElementVisible(By.tagName("themed-web-component")); TestBenchElement webComponent = $("themed-web-component").first(); - TestBenchElement themedComponent = webComponent.$("themed-component").first(); + TestBenchElement themedComponent = webComponent.$("themed-component") + .first(); TestBenchElement content = themedComponent.$("div").first(); Assert.assertNotNull("The component which should use theme doesn't " + "contain elements", content); - Assert.assertEquals("rgba(255, 0, 0, 1)", - content.getCssValue("color")); + Assert.assertEquals("rgba(255, 0, 0, 1)", content.getCssValue("color")); } } diff --git a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/RouterSessionExpirationIT.java b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/RouterSessionExpirationIT.java index d900d394688..ce0511d1a63 100644 --- a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/RouterSessionExpirationIT.java +++ b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/RouterSessionExpirationIT.java @@ -18,21 +18,18 @@ import org.junit.Assert; import org.junit.Test; import org.openqa.selenium.By; -import org.openqa.selenium.WebElement; import com.vaadin.flow.testutil.ChromeBrowserTest; public class RouterSessionExpirationIT extends ChromeBrowserTest { - - @Override protected String getTestPath() { return "/view/"; } @Test - public void navigationAfterSessionExpired() { + public void should_HaveANewSessionId_when_NavigationAfterSessionExpired() { openUrl("/new-router-session/NormalView"); navigateToAnotherView(); @@ -45,11 +42,24 @@ public void navigationAfterSessionExpired() { // be a new session Assert.assertNotEquals(sessionId, getSessionId()); sessionId = getSessionId(); - navigateToFirstView(); + navigateToAnotherView(); // session is preserved Assert.assertEquals(sessionId, getSessionId()); } + @Test + public void should_StayOnSessionExpirationView_when_NavigationAfterSessionExpired(){ + openUrl("/new-router-session/NormalView"); + + if (hasClientIssue("7581")) { + return; + } + + navigateToSesssionExpireView(); + + assertTextAvailableInView("ViewWhichInvalidatesSession"); + } + @Test public void navigationAfterInternalError() { openUrl("/new-router-session/NormalView"); @@ -77,7 +87,7 @@ private void navigateToAnotherView() { } private void navigateToSesssionExpireView() { - navigateTo("ViewWhichInvalidatesSession"); + findElement(By.linkText("ViewWhichInvalidatesSession")).click(); } private void navigateToInternalErrorView() { @@ -87,6 +97,12 @@ private void navigateToInternalErrorView() { private void navigateTo(String linkText) { findElement(By.linkText(linkText)).click(); - waitForElementPresent(By.xpath("//strong[text()='" + linkText + "']")); + assertTextAvailableInView(linkText); + + } + + private void assertTextAvailableInView(String linkText) { + Assert.assertNotNull( + findElement(By.xpath("//strong[text()='" + linkText + "']"))); } }