diff options
author | Tatu Lund <tatu@vaadin.com> | 2019-12-02 11:14:58 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-12-02 11:14:58 +0200 |
commit | d389e77e9f2c7a38143732daf697252495b95e7c (patch) | |
tree | a8a9f8b346a75a9210f5bc65a5dc3a42469fac0a | |
parent | 7955d2c9aaa50b9f1d35176ba47fcdbe976cef86 (diff) | |
parent | 252ef116342a6b0363b48d157de33a932d44752f (diff) | |
download | vaadin-framework-d389e77e9f2c7a38143732daf697252495b95e7c.tar.gz vaadin-framework-d389e77e9f2c7a38143732daf697252495b95e7c.zip |
Merge branch 'master' into fix11576
43 files changed, 1050 insertions, 109 deletions
diff --git a/README-TESTS.md b/README-TESTS.md index d79dc8c1ed..d9fef4071d 100755 --- a/README-TESTS.md +++ b/README-TESTS.md @@ -53,7 +53,7 @@ We're going to do the same as with the command-line approach - launch the `mvn j Before running any tests from your IDE you need to 1. copy `uitest/eclipse-run-selected-test.properties` to `work/eclipse-run-selected-test.properties` 2. edit `work/eclipse-run-selected-test.properties` - 1. Define `com.vaadin.testbench.screenshot.directory` as the directory where you checked out the screenshots repository (this directory contains the “references” subdirectory) + 1. Define `com.vaadin.testbench.screenshot.directory` as the `uitest` repository (this directory contains the “reference-screenshots” subdirectory) 2. Set `com.vaadin.testbench.deployment.url=http://localhost:8888/` 3. Set `com.vaadin.testbench.runLocally=chrome` to only run tests on Chrome. On Ubuntu you can then install Chrome driver easily: `sudo apt install chromium-chromedriver` diff --git a/client/src/main/java/com/vaadin/client/communication/MessageHandler.java b/client/src/main/java/com/vaadin/client/communication/MessageHandler.java index a622bf1614..8e16293423 100644 --- a/client/src/main/java/com/vaadin/client/communication/MessageHandler.java +++ b/client/src/main/java/com/vaadin/client/communication/MessageHandler.java @@ -193,6 +193,7 @@ public class MessageHandler { private int lastSeenServerSyncId = UNDEFINED_SYNC_ID; private ApplicationConnection connection; + private boolean resyncInProgress; /** * Data structure holding information about pending UIDL messages. @@ -258,7 +259,17 @@ public class MessageHandler { protected void handleJSON(final ValueMap json) { final int serverId = getServerId(json); - if (isResynchronize(json) && !isNextExpectedMessage(serverId)) { + boolean hasResynchronize = isResynchronize(json); + + if (!hasResynchronize && resyncInProgress) { + Logger.getLogger(MessageHandler.class.getName()) + .warning("Dropping the response of a request before a resync request."); + return; + } + + resyncInProgress = false; + + if (hasResynchronize && !isNextExpectedMessage(serverId)) { // Resynchronize request. We must remove any old pending // messages and ensure this is handled next. Otherwise we // would keep waiting for an older message forever (if this @@ -321,7 +332,7 @@ public class MessageHandler { int serverNextExpected = json .getInt(ApplicationConstants.CLIENT_TO_SERVER_ID); getMessageSender().setClientToServerMessageId(serverNextExpected, - isResynchronize(json)); + hasResynchronize); } if (serverId != -1) { @@ -1822,4 +1833,7 @@ public class MessageHandler { } }-*/; + public void onResynchronize() { + resyncInProgress = true; + } } diff --git a/client/src/main/java/com/vaadin/client/communication/MessageSender.java b/client/src/main/java/com/vaadin/client/communication/MessageSender.java index 5864ee5ee7..cde6657d1a 100644 --- a/client/src/main/java/com/vaadin/client/communication/MessageSender.java +++ b/client/src/main/java/com/vaadin/client/communication/MessageSender.java @@ -349,6 +349,7 @@ public class MessageSender { * state from the server */ public void resynchronize() { + getMessageHandler().onResynchronize(); getLogger().info("Resynchronizing from server"); JsonObject resyncParam = Json.createObject(); resyncParam.put(ApplicationConstants.RESYNCHRONIZE_ID, true); diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/MultiSelectionModelConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/MultiSelectionModelConnector.java index 7bbc66d271..de003e6daf 100644 --- a/client/src/main/java/com/vaadin/client/connectors/grid/MultiSelectionModelConnector.java +++ b/client/src/main/java/com/vaadin/client/connectors/grid/MultiSelectionModelConnector.java @@ -250,7 +250,7 @@ public class MultiSelectionModelConnector @Override protected boolean isSelected(JsonObject item) { - return getState().allSelected || super.isSelected(item); + return super.isSelected(item); } /** diff --git a/client/src/main/java/com/vaadin/client/ui/VComboBox.java b/client/src/main/java/com/vaadin/client/ui/VComboBox.java index 81453bfade..438234d30b 100644 --- a/client/src/main/java/com/vaadin/client/ui/VComboBox.java +++ b/client/src/main/java/com/vaadin/client/ui/VComboBox.java @@ -28,6 +28,7 @@ import java.util.logging.Logger; import com.google.gwt.animation.client.AnimationScheduler; import com.google.gwt.aria.client.Roles; import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.Scheduler; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.NativeEvent; @@ -427,10 +428,15 @@ public class VComboBox extends Composite implements Field, KeyDownHandler, // Add TT anchor point getElement().setId("VAADIN_COMBOBOX_OPTIONLIST"); - leftPosition = getDesiredLeftPosition(); - topPosition = getDesiredTopPosition(); + // Set the default position if the popup isn't already visible, + // the setPopupPositionAndShow call later on can deal with any + // adjustments that might be needed + if (!popup.isShowing()) { + leftPosition = getDesiredLeftPosition(); + topPosition = getDesiredTopPosition(); - setPopupPosition(leftPosition, topPosition); + setPopupPosition(leftPosition, topPosition); + } int nullOffset = getNullSelectionItemShouldBeVisible() ? 1 : 0; boolean firstPage = currentPage == 0; @@ -893,16 +899,24 @@ public class VComboBox extends Composite implements Field, KeyDownHandler, } } + if (offsetWidth + menuMarginBorderPaddingWidth + + left < VComboBox.this.getAbsoluteLeft() + + VComboBox.this.getOffsetWidth()) { + // Popup doesn't reach all the way to the end of the input + // field, filtering may have changed the popup width. + left = VComboBox.this.getAbsoluteLeft(); + } if (offsetWidth + menuMarginBorderPaddingWidth + left > Window .getClientWidth()) { + // Popup doesn't fit the view, needs to be opened to the left + // instead. left = VComboBox.this.getAbsoluteLeft() + VComboBox.this.getOffsetWidth() - offsetWidth - (int) menuMarginBorderPaddingWidth; - if (left < 0) { - left = 0; - menu.setWidth(Window.getClientWidth() + "px"); - - } + } + if (left < 0) { + left = 0; + menu.setWidth(Window.getClientWidth() + "px"); } setPopupPosition(left, top); @@ -1850,7 +1864,8 @@ public class VComboBox extends Composite implements Field, KeyDownHandler, if (event.getTypeInt() == Event.ONPASTE) { if (textInputEnabled && connector.isEnabled() && !connector.isReadOnly()) { - filterOptions(currentPage); + Scheduler.get() + .scheduleDeferred(() -> filterOptions(currentPage)); } } } diff --git a/client/src/main/java/com/vaadin/client/ui/VTwinColSelect.java b/client/src/main/java/com/vaadin/client/ui/VTwinColSelect.java index 6cbdb944ec..8618f9b52d 100644 --- a/client/src/main/java/com/vaadin/client/ui/VTwinColSelect.java +++ b/client/src/main/java/com/vaadin/client/ui/VTwinColSelect.java @@ -25,6 +25,7 @@ import java.util.Set; import java.util.function.BiConsumer; import java.util.stream.Collectors; +import com.google.gwt.core.client.Scheduler; import com.google.gwt.dom.client.Style.Overflow; import com.google.gwt.dom.client.Style.Position; import com.google.gwt.event.dom.client.ClickEvent; @@ -37,6 +38,7 @@ import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.MouseDownEvent; import com.google.gwt.event.dom.client.MouseDownHandler; import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Element; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTML; @@ -80,6 +82,7 @@ public class VTwinColSelect extends Composite implements MultiSelectWidget, private static final int VISIBLE_COUNT = 10; private static final int DEFAULT_COLUMN_COUNT = 10; + private static int scheduledScrollToItem = -1; private final DoubleClickListBox optionsListBox; @@ -327,15 +330,28 @@ public class VTwinColSelect extends Composite implements MultiSelectWidget, private static void updateListBox(ListBox listBox, List<JsonObject> options) { + List<String> selected = new ArrayList<String>(); + // Retain right visible selection, see #11287 + for (int i = 0; i < listBox.getItemCount(); ++i) { + if (listBox.isItemSelected(i)) { + selected.add(listBox.getItemText(i)); + } + } for (int i = 0; i < options.size(); i++) { final JsonObject item = options.get(i); // reuse existing option if possible + String caption = MultiSelectWidget.getCaption(item); if (i < listBox.getItemCount()) { - listBox.setItemText(i, MultiSelectWidget.getCaption(item)); + listBox.setItemText(i, caption); listBox.setValue(i, MultiSelectWidget.getKey(item)); } else { - listBox.addItem(MultiSelectWidget.getCaption(item), - MultiSelectWidget.getKey(item)); + listBox.addItem(caption, MultiSelectWidget.getKey(item)); + } + boolean isSelected = selected.contains(caption); + listBox.setItemSelected(i, isSelected); + if (isSelected) { + // Ensure that last selected item is visible + scrollToView(listBox,i); } } // remove extra @@ -344,6 +360,19 @@ public class VTwinColSelect extends Composite implements MultiSelectWidget, } } + private static void scrollToView(ListBox listBox, int i) { + if (scheduledScrollToItem == -1) { + scheduledScrollToItem = i; + Scheduler.get().scheduleDeferred(() -> { + Element el = (Element) listBox.getElement().getChild(scheduledScrollToItem); + el.scrollIntoView(); + scheduledScrollToItem = -1; + }); + } else { + scheduledScrollToItem = i; + } + } + private static boolean[] getSelectionBitmap(ListBox listBox) { final boolean[] selectedIndexes = new boolean[listBox.getItemCount()]; for (int i = 0; i < listBox.getItemCount(); i++) { diff --git a/client/src/main/java/com/vaadin/client/ui/treegrid/TreeGridConnector.java b/client/src/main/java/com/vaadin/client/ui/treegrid/TreeGridConnector.java index c220057508..253e03a80c 100644 --- a/client/src/main/java/com/vaadin/client/ui/treegrid/TreeGridConnector.java +++ b/client/src/main/java/com/vaadin/client/ui/treegrid/TreeGridConnector.java @@ -288,7 +288,7 @@ public class TreeGridConnector extends GridConnector { */ private void setCollapsed(int rowIndex, boolean collapsed, boolean userOriginated) { - if (isAwaitingRowChange()) { + if (isAwaitingRowChange() || !getState().enabled) { return; } if (collapsed) { diff --git a/client/src/main/java/com/vaadin/client/widget/escalator/ScrollbarBundle.java b/client/src/main/java/com/vaadin/client/widget/escalator/ScrollbarBundle.java index d0ae71e4d4..8042e1b264 100644 --- a/client/src/main/java/com/vaadin/client/widget/escalator/ScrollbarBundle.java +++ b/client/src/main/java/com/vaadin/client/widget/escalator/ScrollbarBundle.java @@ -587,10 +587,12 @@ public abstract class ScrollbarBundle implements DeferredWorker { * @return the new scroll position in pixels */ public final double getScrollPos() { - assert internalGetScrollPos() == toInt32( - scrollPos) : "calculated scroll position (" + scrollPos + int internalScrollPos = internalGetScrollPos(); + assert Math.abs(internalScrollPos + - toInt32(scrollPos)) <= 1 : "calculated scroll position (" + + scrollPos + ") did not match the DOM element scroll position (" - + internalGetScrollPos() + ")"; + + internalScrollPos + ")"; return scrollPos; } diff --git a/client/src/main/java/com/vaadin/client/widgets/Escalator.java b/client/src/main/java/com/vaadin/client/widgets/Escalator.java index 5c3bf593fb..e98ae2c578 100644 --- a/client/src/main/java/com/vaadin/client/widgets/Escalator.java +++ b/client/src/main/java/com/vaadin/client/widgets/Escalator.java @@ -5059,9 +5059,10 @@ public class Escalator extends Widget break; case END: // target row at the bottom of the viewport - newTopRowLogicalIndex = Math.min( - lastVisibleIndexIfScrollingDown + 1, getRowCount() - 1) + newTopRowLogicalIndex = lastVisibleIndexIfScrollingDown + 1 - visualRangeLength + 1; + newTopRowLogicalIndex = ensureTopRowLogicalIndexSanity( + newTopRowLogicalIndex); if ((newTopRowLogicalIndex > oldTopRowLogicalIndex) && (newTopRowLogicalIndex - oldTopRowLogicalIndex < visualRangeLength)) { @@ -5078,10 +5079,8 @@ public class Escalator extends Widget // target row at the middle of the viewport, padding has to be // zero or we never would have reached this far newTopRowLogicalIndex = targetRowIndex - visualRangeLength / 2; - // ensure we don't attempt to go beyond the bottom row - if (newTopRowLogicalIndex + visualRangeLength > getRowCount()) { - newTopRowLogicalIndex = getRowCount() - visualRangeLength; - } + newTopRowLogicalIndex = ensureTopRowLogicalIndexSanity( + newTopRowLogicalIndex); if (newTopRowLogicalIndex < oldTopRowLogicalIndex) { logicalTargetIndex = newTopRowLogicalIndex; } else if (newTopRowLogicalIndex > oldTopRowLogicalIndex) { @@ -5102,8 +5101,9 @@ public class Escalator extends Widget case START: // target row at the top of the viewport, include buffer // row if there is room for one - newTopRowLogicalIndex = Math - .max(firstVisibleIndexIfScrollingUp - 1, 0); + newTopRowLogicalIndex = firstVisibleIndexIfScrollingUp - 1; + newTopRowLogicalIndex = ensureTopRowLogicalIndexSanity( + newTopRowLogicalIndex); if (getVisibleRowRange().contains(newTopRowLogicalIndex)) { logicalTargetIndex = oldTopRowLogicalIndex + visualRangeLength; @@ -5141,6 +5141,24 @@ public class Escalator extends Widget } /** + * Modifies the proposed top row logical index to fit within the logical + * range and to not leave gaps if it is avoidable. + * + * @param proposedTopRowLogicalIndex + * @return an adjusted index, or the original if no changes were + * necessary + */ + private int ensureTopRowLogicalIndexSanity( + int proposedTopRowLogicalIndex) { + int newTopRowLogicalIndex = Math.max(proposedTopRowLogicalIndex, 0); + int visualRangeLength = visualRowOrder.size(); + if (newTopRowLogicalIndex + visualRangeLength > getRowCount()) { + newTopRowLogicalIndex = getRowCount() - visualRangeLength; + } + return newTopRowLogicalIndex; + } + + /** * Checks that scrolling is allowed and resets the scroll position if * it's not. * diff --git a/client/src/main/java/com/vaadin/client/widgets/Grid.java b/client/src/main/java/com/vaadin/client/widgets/Grid.java index 2cbeb9cd21..977935c9bf 100755 --- a/client/src/main/java/com/vaadin/client/widgets/Grid.java +++ b/client/src/main/java/com/vaadin/client/widgets/Grid.java @@ -7809,8 +7809,11 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, } } - assert cell != null : "received " + eventType - + "-event with a null cell target"; + if (cell == null) { + getLogger().log(Level.WARNING, + "received " + eventType + "-event with a null cell target"); + return; + } eventCell.set(cell, getSectionFromContainer(container)); GridEvent<T> gridEvent = new GridEvent<>(event, eventCell); diff --git a/compatibility-client/src/main/java/com/vaadin/v7/client/ui/VScrollTable.java b/compatibility-client/src/main/java/com/vaadin/v7/client/ui/VScrollTable.java index 088c8a2bd6..36b1468bbd 100644 --- a/compatibility-client/src/main/java/com/vaadin/v7/client/ui/VScrollTable.java +++ b/compatibility-client/src/main/java/com/vaadin/v7/client/ui/VScrollTable.java @@ -6348,15 +6348,15 @@ public class VScrollTable extends FlowPanel if (targetCellOrRowFound) { setRowFocus(this); - ensureFocus(); if (dragmode != 0 && (event .getButton() == NativeEvent.BUTTON_LEFT)) { + ensureFocus(); startRowDrag(event, type, targetTdOrTr); } else if (event.getCtrlKey() || event.getShiftKey() || event.getMetaKey() && isMultiSelectModeDefault()) { - + ensureFocus(); // Prevent default text selection in Firefox event.preventDefault(); diff --git a/documentation/addons/addons-maven.asciidoc b/documentation/addons/addons-maven.asciidoc index 7a1cfe99f3..8161894a47 100644 --- a/documentation/addons/addons-maven.asciidoc +++ b/documentation/addons/addons-maven.asciidoc @@ -280,7 +280,7 @@ It is also useful if you maintain your source code in GitHub or a similar servic For production, especially in intranet applications, you should normally use the `local` or `fetch` modes. This ensures that separating the availability of the Vaadin CDN service from availability of the application server does not add an extra point of failure. -_They can be be used for production_ if your application is intended as globally available, you want to gain the global delivery benefit of the Vaadin CDN, and the availability tradeoff is not significant. +_They can be used for production_ if your application is intended as globally available, you want to gain the global delivery benefit of the Vaadin CDN, and the availability tradeoff is not significant. ==== === Serving Remotely Compiled Widget Set Locally diff --git a/server/src/main/java/com/vaadin/data/BeanValidationBinder.java b/server/src/main/java/com/vaadin/data/BeanValidationBinder.java index e98ce96b37..de67f922bf 100644 --- a/server/src/main/java/com/vaadin/data/BeanValidationBinder.java +++ b/server/src/main/java/com/vaadin/data/BeanValidationBinder.java @@ -49,7 +49,25 @@ public class BeanValidationBinder<BEAN> extends Binder<BEAN> { * the bean type to use, not <code>null</code> */ public BeanValidationBinder(Class<BEAN> beanType) { - super(beanType); + this(beanType,false); + } + + /** + * Creates a new binder that uses reflection based on the provided bean type + * to resolve bean properties. It assumes that JSR-303 bean validation + * implementation is present on the classpath. If there is no such + * implementation available then {@link Binder} class should be used instead + * (this constructor will throw an exception). Otherwise + * {@link BeanValidator} is added to each binding that is defined using a + * property name. + * + * @param beanType + * the bean type to use, not {@code null} + * @param scanNestedDefinitions + * if {@code true}, scan for nested property definitions as well + */ + public BeanValidationBinder(Class<BEAN> beanType, boolean scanNestedDefinitions) { + super(beanType, scanNestedDefinitions); if (!BeanUtil.checkBeanValidationAvailable()) { throw new IllegalStateException(BeanValidationBinder.class .getSimpleName() diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index 22ef7d7782..cd7b834571 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -226,6 +226,30 @@ public class Binder<BEAN> implements Serializable { * @since 8.4 */ public Setter<BEAN, TARGET> getSetter(); + + /** + * Enable or disable asRequired validator. + * The validator is enabled by default. + * + * @see #asRequired(String) + * @see #asRequired(ErrorMessageProvider) + * + * @param asRequiredEnabled + * {@code false} if asRequired validator should + * be disabled, {@code true} otherwise (default) + */ + public void setAsRequiredEnabled(boolean asRequiredEnabled); + + /** + * Returns whether asRequired validator is currently enabled or not + * + * @see #asRequired(String) + * @see #asRequired(ErrorMessageProvider) + * + * @return {@code false} if asRequired validator is disabled + * {@code true} otherwise (default) + */ + public boolean isAsRequiredEnabled(); } /** @@ -781,6 +805,8 @@ public class Binder<BEAN> implements Serializable { */ private Converter<FIELDVALUE, ?> converterValidatorChain; + private boolean asRequiredSet; + /** * Creates a new binding builder associated with the given field. * Initializes the builder with the given converter chain and status @@ -816,7 +842,7 @@ public class Binder<BEAN> implements Serializable { getBinder().bindings.add(binding); if (getBinder().getBean() != null) { - binding.initFieldValue(getBinder().getBean()); + binding.initFieldValue(getBinder().getBean(), true); } if (setter == null) { binding.getField().setReadOnly(true); @@ -917,8 +943,14 @@ public class Binder<BEAN> implements Serializable { public BindingBuilder<BEAN, TARGET> asRequired( Validator<TARGET> customRequiredValidator) { checkUnbound(); + this.asRequiredSet = true; field.setRequiredIndicatorVisible(true); - return withValidator(customRequiredValidator); + return withValidator((value, context) -> { + if (!field.isRequiredIndicatorVisible()) + return ValidationResult.ok(); + else + return customRequiredValidator.apply(value, context); + }); } /** @@ -1020,12 +1052,15 @@ public class Binder<BEAN> implements Serializable { */ private final Converter<FIELDVALUE, TARGET> converterValidatorChain; + private boolean asRequiredSet; + public BindingImpl(BindingBuilderImpl<BEAN, FIELDVALUE, TARGET> builder, ValueProvider<BEAN, TARGET> getter, Setter<BEAN, TARGET> setter) { this.binder = builder.getBinder(); this.field = builder.field; this.statusHandler = builder.statusHandler; + this.asRequiredSet = builder.asRequiredSet; converterValidatorChain = ((Converter<FIELDVALUE, TARGET>) builder.converterValidatorChain); onValueChange = getField() @@ -1146,20 +1181,33 @@ public class Binder<BEAN> implements Serializable { * * @param bean * the bean to fetch the property value from + * @param writeBackChangedValues + * <code>true</code> if the bean value should be updated if + * the value is different after converting to and from the + * presentation value; <code>false</code> to avoid updating + * the bean value */ - private void initFieldValue(BEAN bean) { + private void initFieldValue(BEAN bean, boolean writeBackChangedValues) { assert bean != null; assert onValueChange != null; valueInit = true; try { - getField().setValue(convertDataToFieldType(bean)); + TARGET originalValue = getter.apply(bean); + convertAndSetFieldValue(originalValue); + + if (writeBackChangedValues && setter != null) { + doConversion().ifOk(convertedValue -> { + if (!Objects.equals(originalValue, convertedValue)) { + setter.accept(bean, convertedValue); + } + }); + } } finally { valueInit = false; } } - private FIELDVALUE convertDataToFieldType(BEAN bean) { - TARGET target = getter.apply(bean); + private FIELDVALUE convertToFieldType(TARGET target) { ValueContext valueContext = createValueContext(); return converterValidatorChain.convertToPresentation(target, valueContext); @@ -1218,7 +1266,31 @@ public class Binder<BEAN> implements Serializable { @Override public void read(BEAN bean) { - getField().setValue(convertDataToFieldType(bean)); + convertAndSetFieldValue(getter.apply(bean)); + } + + private void convertAndSetFieldValue(TARGET modelValue) { + FIELDVALUE convertedValue = convertToFieldType(modelValue); + try { + getField().setValue(convertedValue); + } catch (RuntimeException e) { + /* + * Add an additional hint to the exception for the typical case + * with a field that doesn't accept null values. The non-null + * empty value is used as a heuristic to determine that the + * field doesn't accept null rather than throwing for some other + * reason. + */ + if (convertedValue == null && getField().getEmptyValue() != null) { + throw new IllegalStateException(String.format( + "A field of type %s didn't accept a null value." + + " If null values are expected, then configure a null representation for the binding.", + field.getClass().getName()), e); + } else { + // Otherwise, let the original exception speak for itself + throw e; + } + } } @Override @@ -1245,6 +1317,24 @@ public class Binder<BEAN> implements Serializable { public Setter<BEAN, TARGET> getSetter() { return setter; } + + @Override + public void setAsRequiredEnabled(boolean asRequiredEnabled) { + if (!asRequiredSet) { + throw new IllegalStateException( + "Unable to toggle asRequired validation since " + + "asRequired has not been set."); + } + if (asRequiredEnabled != isAsRequiredEnabled()) { + field.setRequiredIndicatorVisible(asRequiredEnabled); + validate(); + } + } + + @Override + public boolean isAsRequiredEnabled() { + return field.isRequiredIndicatorVisible(); + } } /** @@ -1639,6 +1729,10 @@ public class Binder<BEAN> implements Serializable { * Any change made in the fields also runs validation for the field * {@link Binding} and bean level validation for this binder (bean level * validators are added using {@link Binder#withValidator(Validator)}. + * <p> + * After updating each field, the value is read back from the field and the + * bean's property value is updated if it has been changed from the original + * value by the field or a converter. * * @see #readBean(Object) * @see #writeBean(Object) @@ -1658,7 +1752,7 @@ public class Binder<BEAN> implements Serializable { } else { doRemoveBean(false); this.bean = bean; - getBindings().forEach(b -> b.initFieldValue(bean)); + getBindings().forEach(b -> b.initFieldValue(bean, true)); // if there has been field value change listeners that trigger // validation, need to make sure the validation errors are cleared getValidationStatusHandler().statusChange( @@ -1706,7 +1800,7 @@ public class Binder<BEAN> implements Serializable { // we unbind a binding in valueChangeListener of another // field. if (binding.getField() != null) - binding.initFieldValue(bean); + binding.initFieldValue(bean, false); }); getValidationStatusHandler().statusChange( BinderValidationStatus.createUnresolvedStatus(this)); @@ -1746,6 +1840,23 @@ public class Binder<BEAN> implements Serializable { } /** + * Writes successfully converted and validated changes from the bound fields + * to the bean even if there are other fields with non-validated changes. + * + * @see #writeBean(Object) + * @see #writeBeanIfValid(Object) + * @see #readBean(Object) + * @see #setBean(Object) + * + * @param bean + * the object to which to write the field values, not + * {@code null} + */ + public void writeBeanAsDraft(BEAN bean) { + doWriteDraft(bean, new ArrayList<>(bindings)); + } + + /** * Writes changes from the bound fields to the given bean if all validators * (binding and bean level) pass. * <p> @@ -1833,6 +1944,23 @@ public class Binder<BEAN> implements Serializable { } /** + * Writes the successfully converted and validated field values into the + * given bean. + * + * @param bean + * the bean to write field values into + * @param bindings + * the set of bindings to write to the bean + */ + @SuppressWarnings({ "unchecked" }) + private void doWriteDraft(BEAN bean, Collection<Binding<BEAN, ?>> bindings) { + Objects.requireNonNull(bean, "bean cannot be null"); + + bindings.forEach(binding -> ((BindingImpl<BEAN, ?, ?>) binding) + .writeFieldValue(bean)); + } + + /** * Restores the state of the bean from the given values. This method is used * together with {@link #getBeanState(Object, Collection)} to provide a way * to revert changes in case the bean validation fails after save. diff --git a/server/src/main/java/com/vaadin/server/VaadinPortletService.java b/server/src/main/java/com/vaadin/server/VaadinPortletService.java index 1b804ad18c..0b2d28acc6 100644 --- a/server/src/main/java/com/vaadin/server/VaadinPortletService.java +++ b/server/src/main/java/com/vaadin/server/VaadinPortletService.java @@ -22,6 +22,8 @@ import java.io.File; import java.io.InputStream; import java.net.URL; import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -366,4 +368,40 @@ public class VaadinPortletService extends VaadinService { getWrappedPortletSession(wrappedSession).removeAttribute( getSessionAttributeName(), PortletSession.APPLICATION_SCOPE); } + + @Override + protected void setSessionLock(WrappedSession wrappedSession, Lock lock) { + if (wrappedSession == null) { + throw new IllegalArgumentException( + "Can't set a lock for a null session"); + } + Object currentSessionLock = getWrappedPortletSession(wrappedSession) + .getAttribute(getLockAttributeName(), + PortletSession.APPLICATION_SCOPE); + assert (currentSessionLock == null + || currentSessionLock == lock) : "Changing the lock for a session is not allowed"; + + getWrappedPortletSession(wrappedSession).setAttribute( + getLockAttributeName(), lock, + PortletSession.APPLICATION_SCOPE); + } + + @Override + protected Lock getSessionLock(WrappedSession wrappedSession) { + Object lock = getWrappedPortletSession(wrappedSession) + .getAttribute(getLockAttributeName(), + PortletSession.APPLICATION_SCOPE); + + if (lock instanceof ReentrantLock) { + return (ReentrantLock) lock; + } + + if (lock == null) { + return null; + } + + throw new RuntimeException( + "Something else than a ReentrantLock was stored in the " + + getLockAttributeName() + " in the session"); + } } diff --git a/server/src/main/java/com/vaadin/server/VaadinService.java b/server/src/main/java/com/vaadin/server/VaadinService.java index 1e67db708b..9503db113f 100644 --- a/server/src/main/java/com/vaadin/server/VaadinService.java +++ b/server/src/main/java/com/vaadin/server/VaadinService.java @@ -622,7 +622,7 @@ public abstract class VaadinService implements Serializable { * @param lock * The lock object */ - private void setSessionLock(WrappedSession wrappedSession, Lock lock) { + protected void setSessionLock(WrappedSession wrappedSession, Lock lock) { if (wrappedSession == null) { throw new IllegalArgumentException( "Can't set a lock for a null session"); @@ -640,7 +640,7 @@ public abstract class VaadinService implements Serializable { * * @return The attribute name for the lock */ - private String getLockAttributeName() { + protected String getLockAttributeName() { return getServiceName() + ".lock"; } diff --git a/server/src/main/java/com/vaadin/server/VaadinServlet.java b/server/src/main/java/com/vaadin/server/VaadinServlet.java index b0d270accb..794cfdd1d6 100644 --- a/server/src/main/java/com/vaadin/server/VaadinServlet.java +++ b/server/src/main/java/com/vaadin/server/VaadinServlet.java @@ -28,11 +28,13 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Serializable; +import java.io.UnsupportedEncodingException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; +import java.net.URLDecoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -46,6 +48,7 @@ import java.util.Map; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; @@ -759,6 +762,13 @@ public class VaadinServlet extends HttpServlet implements Constants { private static boolean scssCompileWarWarningEmitted = false; /** + * Pattern for matching request paths that start with /VAADIN/, multiple + * slashes allowed on either side. + */ + private static Pattern staticFileRequestPathPatternVaadin = Pattern + .compile("^/+VAADIN/.*"); + + /** * Returns the default theme. Must never return null. * * @return @@ -1348,16 +1358,44 @@ public class VaadinServlet extends HttpServlet implements Constants { * @since 8.0 */ protected String getStaticFilePath(HttpServletRequest request) { - String pathInfo = request.getPathInfo(); - if (pathInfo == null) { + if (request.getPathInfo() == null) { return null; } + String decodedPath = null; + String contextPath = null; + try { + // pathInfo should be already decoded, but some containers do not + // decode it, hence we use getRequestURI instead. + decodedPath = URLDecoder.decode(request.getRequestURI(), + StandardCharsets.UTF_8.name()); + contextPath = URLDecoder.decode(request.getContextPath(), + StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("An error occurred during decoding URL.", + e); + } + // Possible context path needs to be removed + String filePath = decodedPath.substring(contextPath.length()); + String servletPath = request.getServletPath(); + // Possible servlet path needs to be removed + if (!servletPath.isEmpty() && !servletPath.equals("/VAADIN") + && filePath.startsWith(servletPath)) { + filePath = filePath.substring(servletPath.length()); + } // Servlet mapped as /* serves at /VAADIN // Servlet mapped as /foo/bar/* serves at /foo/bar/VAADIN - if (pathInfo.startsWith("/VAADIN/")) { - return pathInfo; + + // Matches request paths /VAADIN/*, //VAADIN/* etc. + if (staticFileRequestPathPatternVaadin.matcher(filePath).matches()) { + // Remove any extra slashes from the beginning, + // later occurrences don't interfere + while (filePath.startsWith("//")) { + filePath = filePath.substring(1); + } + return filePath; } - String servletPrefixedPath = request.getServletPath() + pathInfo; + + String servletPrefixedPath = servletPath + filePath; // Servlet mapped as /VAADIN/* if (servletPrefixedPath.startsWith("/VAADIN/")) { return servletPrefixedPath; diff --git a/server/src/main/java/com/vaadin/ui/Composite.java b/server/src/main/java/com/vaadin/ui/Composite.java index 8c9379f3f7..0d3418c4b1 100644 --- a/server/src/main/java/com/vaadin/ui/Composite.java +++ b/server/src/main/java/com/vaadin/ui/Composite.java @@ -216,6 +216,16 @@ public class Composite extends AbstractComponent implements HasComponents { } @Override + public void setEnabled(boolean enabled) { + getRootOrThrow().setEnabled(enabled); + } + + @Override + public boolean isEnabled() { + return getRootOrThrow().isEnabled(); + } + + @Override public float getWidth() { return getRootOrThrow().getWidth(); } diff --git a/server/src/main/java/com/vaadin/ui/CustomLayout.java b/server/src/main/java/com/vaadin/ui/CustomLayout.java index 7764846065..4fd331c462 100644 --- a/server/src/main/java/com/vaadin/ui/CustomLayout.java +++ b/server/src/main/java/com/vaadin/ui/CustomLayout.java @@ -292,6 +292,9 @@ public class CustomLayout extends AbstractLayout implements LegacyComponent { /** * Set the contents of the template used to draw the custom layout. * + * Note: setTemplateContents can be applied only before CustomLayout + * instance has been attached. + * * @param templateContents */ public void setTemplateContents(String templateContents) { diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java index 8d19d89a7e..1ac71ff984 100644 --- a/server/src/main/java/com/vaadin/ui/Grid.java +++ b/server/src/main/java/com/vaadin/ui/Grid.java @@ -4390,7 +4390,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, Objects.requireNonNull(destination, "ScrollDestination can not be null"); - if (row > getDataCommunicator().getDataProviderSize()) { + if (row >= getDataCommunicator().getDataProviderSize()) { throw new IllegalArgumentException("Row outside dataProvider size"); } diff --git a/server/src/main/java/com/vaadin/ui/MultiSelect.java b/server/src/main/java/com/vaadin/ui/MultiSelect.java index d2cd7def59..0e75e8377d 100644 --- a/server/src/main/java/com/vaadin/ui/MultiSelect.java +++ b/server/src/main/java/com/vaadin/ui/MultiSelect.java @@ -86,7 +86,8 @@ public interface MultiSelect<T> extends HasValue<Set<T>> { * If all the added items were already selected and the removed items were * not selected, this is a NO-OP. * <p> - * Duplicate items (in both add & remove sets) are ignored. + * Duplicate items (in both add & remove sets) are ignored and removed from + * the sets. * * @param addedItems * the items to add, not {@code null} diff --git a/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java b/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java index f51744c96e..0ef1d1a5f7 100644 --- a/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java +++ b/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java @@ -15,7 +15,24 @@ */ package com.vaadin.ui.components.grid; -import com.vaadin.data.provider.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.vaadin.data.provider.DataCommunicator; +import com.vaadin.data.provider.DataProvider; +import com.vaadin.data.provider.HierarchicalDataProvider; +import com.vaadin.data.provider.HierarchicalQuery; +import com.vaadin.data.provider.Query; import com.vaadin.event.selection.MultiSelectionEvent; import com.vaadin.event.selection.MultiSelectionListener; import com.vaadin.shared.Registration; @@ -23,11 +40,6 @@ import com.vaadin.shared.data.selection.GridMultiSelectServerRpc; import com.vaadin.shared.ui.grid.MultiSelectionModelState; import com.vaadin.ui.MultiSelect; -import java.util.*; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; - /** * Multiselection model for grid. * <p> @@ -134,8 +146,7 @@ public class MultiSelectionModelImpl<T> extends AbstractSelectionModel<T> @Override public boolean isSelected(T item) { - return isAllSelected() - || selectionContainsId(getGrid().getDataProvider().getId(item)); + return selectionContainsId(getGrid().getDataProvider().getId(item)); } /** diff --git a/server/src/main/java/com/vaadin/ui/renderers/TextRenderer.java b/server/src/main/java/com/vaadin/ui/renderers/TextRenderer.java index 94fbef613d..5c074d774c 100644 --- a/server/src/main/java/com/vaadin/ui/renderers/TextRenderer.java +++ b/server/src/main/java/com/vaadin/ui/renderers/TextRenderer.java @@ -52,9 +52,12 @@ public class TextRenderer extends AbstractRenderer<Object, Object> { public JsonValue encode(Object value) { if (value == null) { return super.encode(null); - } else { - return Json.create(value.toString()); } + String stringValue = value.toString(); + if (stringValue == null) { + return super.encode(null); + } + return Json.create(stringValue); } @Override diff --git a/server/src/main/java/com/vaadin/ui/themes/ValoTheme.java b/server/src/main/java/com/vaadin/ui/themes/ValoTheme.java index 0f5a09c1b9..3ec3b66c85 100644 --- a/server/src/main/java/com/vaadin/ui/themes/ValoTheme.java +++ b/server/src/main/java/com/vaadin/ui/themes/ValoTheme.java @@ -100,13 +100,13 @@ public class ValoTheme { /** * Success notification style. Adds a border around the notification and an - * icon next to the title. Can be combined with any other Label style. + * icon next to the title. Can be combined with any other Notification style. */ public static final String NOTIFICATION_SUCCESS = "success"; /** * Failure notification style. Adds a border around the notification and an - * icon next to the title. Can be combined with any other Label style. + * icon next to the title. Can be combined with any other Notification style. */ public static final String NOTIFICATION_FAILURE = "failure"; @@ -117,6 +117,12 @@ public class ValoTheme { */ public static final String NOTIFICATION_CRITICAL_ERROR = "critical-error"; + /** + * Styles the notification to be dark variant. Can be combined with any + * other Notification style. + */ + public static final String NOTIFICATION_DARK = "dark"; + /*************************************************************************** * * Label styles diff --git a/server/src/test/java/com/vaadin/data/BinderComponentTest.java b/server/src/test/java/com/vaadin/data/BinderComponentTest.java index 776816be5e..ab5122cc76 100644 --- a/server/src/test/java/com/vaadin/data/BinderComponentTest.java +++ b/server/src/test/java/com/vaadin/data/BinderComponentTest.java @@ -66,17 +66,13 @@ public class BinderComponentTest private <T> void testFieldNullRepresentation(T initialValue, HasValue<T> field) { - binder.bind(field, t -> null, (str, val) -> { - assertEquals("Value update with initial value failed.", - initialValue, field.getValue()); - }); + binder.bind(field, t -> null, (str, val) -> {}); field.setValue(initialValue); assertEquals("Initial value of field unexpected", initialValue, field.getValue()); binder.setBean(item); assertEquals("Null representation for field failed", field.getEmptyValue(), field.getValue()); - field.setValue(initialValue); } } diff --git a/server/src/test/java/com/vaadin/data/BinderTest.java b/server/src/test/java/com/vaadin/data/BinderTest.java index f4515dc211..b3fe5a8194 100644 --- a/server/src/test/java/com/vaadin/data/BinderTest.java +++ b/server/src/test/java/com/vaadin/data/BinderTest.java @@ -8,16 +8,20 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.util.Locale; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.ExpectedException; import com.vaadin.data.Binder.Binding; import com.vaadin.data.Binder.BindingBuilder; @@ -31,9 +35,17 @@ import com.vaadin.tests.data.bean.Person; import com.vaadin.tests.data.bean.Sex; import com.vaadin.ui.TextField; import org.apache.commons.lang.StringUtils; +import org.hamcrest.CoreMatchers; public class BinderTest extends BinderTestBase<Binder<Person>, Person> { + @Rule + /* + * transient to avoid interfering with serialization tests that capture a + * test instance in a closure + */ + public transient ExpectedException exceptionRule = ExpectedException.none(); + @Before public void setUp() { binder = new Binder<>(); @@ -258,6 +270,39 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { } @Test + public void save_bound_beanAsDraft() { + Binder<Person> binder = new Binder<>(); + binder.forField(nameField) + .withValidator((value,context) -> { + if (value.equals("Mike")) return ValidationResult.ok(); + else return ValidationResult.error("value must be Mike"); + }) + .bind(Person::getFirstName, Person::setFirstName); + binder.forField(ageField) + .withConverter(new StringToIntegerConverter("")) + .bind(Person::getAge, Person::setAge); + + Person person = new Person(); + + String fieldValue = "John"; + nameField.setValue(fieldValue); + + int age = 10; + ageField.setValue("10"); + + person.setFirstName("Mark"); + + binder.writeBeanAsDraft(person); + + // name is not written to draft as validation / conversion + // does not pass + assertNotEquals(fieldValue, person.getFirstName()); + // age is written to draft even if firstname validation + // fails + assertEquals(age, person.getAge()); + } + + @Test public void load_bound_fieldValueIsUpdated() { binder.bind(nameField, Person::getFirstName, Person::setFirstName); @@ -333,7 +378,7 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { binder.setBean(namelessPerson); assertTrue(nullTextField.isEmpty()); - assertEquals(null, namelessPerson.getFirstName()); + assertEquals("null", namelessPerson.getFirstName()); // Change value, see that textfield is not empty and bean is updated. nullTextField.setValue(""); @@ -428,13 +473,13 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { TextField textField = new TextField(); assertFalse(textField.isRequiredIndicatorVisible()); - BindingBuilder<Person, String> binding = binder.forField(textField); + BindingBuilder<Person, String> bindingBuilder = binder.forField(textField); assertFalse(textField.isRequiredIndicatorVisible()); - binding.asRequired("foobar"); + bindingBuilder.asRequired("foobar"); assertTrue(textField.isRequiredIndicatorVisible()); - binding.bind(Person::getFirstName, Person::setFirstName); + Binding<Person, String> binding = bindingBuilder.bind(Person::getFirstName, Person::setFirstName); binder.setBean(item); assertNull(textField.getErrorMessage()); @@ -446,6 +491,9 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { textField.setValue("value"); assertNull(textField.getErrorMessage()); assertTrue(textField.isRequiredIndicatorVisible()); + + binding.setAsRequiredEnabled(false); + assertFalse(textField.isRequiredIndicatorVisible()); } @Test @@ -525,7 +573,7 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { binding.bind(Person::getFirstName, Person::setFirstName); binder.setBean(item); assertNull(textField.getErrorMessage()); - assertEquals(0, invokes.get()); + assertEquals(1, invokes.get()); textField.setValue(" "); ErrorMessage errorMessage = textField.getErrorMessage(); @@ -533,7 +581,7 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { assertEquals("Input is required.", errorMessage.getFormattedHtmlMessage()); // validation is done for all changed bindings once. - assertEquals(1, invokes.get()); + assertEquals(2, invokes.get()); textField.setValue("value"); assertNull(textField.getErrorMessage()); @@ -582,7 +630,7 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { binder.setBean(item); assertNull(textField.getErrorMessage()); - assertEquals(0, invokes.get()); + assertEquals(1, invokes.get()); textField.setValue(" "); ErrorMessage errorMessage = textField.getErrorMessage(); @@ -590,7 +638,7 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { assertEquals("Input required.", errorMessage.getFormattedHtmlMessage()); // validation is done for all changed bindings once. - assertEquals(1, invokes.get()); + assertEquals(2, invokes.get()); textField.setValue("value"); assertNull(textField.getErrorMessage()); @@ -1099,12 +1147,12 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { binder.setBean(item); ageField.setValue("3"); - Assert.assertEquals(infoMessage, + assertEquals(infoMessage, ageField.getComponentError().getFormattedHtmlMessage()); - Assert.assertEquals(ErrorLevel.INFO, + assertEquals(ErrorLevel.INFO, ageField.getComponentError().getErrorLevel()); - Assert.assertEquals(3, item.getAge()); + assertEquals(3, item.getAge()); } @Test @@ -1246,4 +1294,118 @@ public class BinderTest extends BinderTestBase<Binder<Person>, Person> { nameField.setValue("Foo"); } + + @Test + public void nonSymetricValue_setBean_writtenToBean() { + binder.bind(nameField, Person::getLastName, Person::setLastName); + + assertNull(item.getLastName()); + + binder.setBean(item); + + assertEquals("", item.getLastName()); + } + + @Test + public void nonSymmetricValue_readBean_beanNotTouched() { + binder.bind(nameField, Person::getLastName, Person::setLastName); + binder.addValueChangeListener( + event -> fail("No value change event should be fired")); + + assertNull(item.getLastName()); + + binder.readBean(item); + + assertNull(item.getLastName()); + } + + @Test + public void symetricValue_setBean_beanNotUpdated() { + binder.bind(nameField, Person::getFirstName, Person::setFirstName); + + binder.setBean(new Person() { + @Override + public String getFirstName() { + return "First"; + } + + @Override + public void setFirstName(String firstName) { + fail("Setter should not be called"); + } + }); + } + + @Test + public void nullRejetingField_nullValue_wrappedExceptionMentionsNullRepresentation() { + TextField field = createNullAnd42RejectingFieldWithEmptyValue(""); + + Binder<AtomicReference<Integer>> binder = createIntegerConverterBinder( + field); + + exceptionRule.expect(IllegalStateException.class); + exceptionRule.expectMessage("null representation"); + exceptionRule.expectCause(CoreMatchers.isA(NullPointerException.class)); + + binder.readBean(new AtomicReference<>()); + } + + + @Test + public void nullRejetingField_otherRejectedValue_originalExceptionIsThrown() { + TextField field = createNullAnd42RejectingFieldWithEmptyValue(""); + + Binder<AtomicReference<Integer>> binder = createIntegerConverterBinder( + field); + + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage("42"); + + binder.readBean(new AtomicReference<>(Integer.valueOf(42))); + } + + @Test(expected = NullPointerException.class) + public void nullAcceptingField_nullValue_originalExceptionIsThrown() { + /* + * Edge case with a field that throws for null but has null as the empty + * value. This is most likely the case if the field doesn't explicitly + * reject null values but is instead somehow broken so that any value is + * rejected. + */ + TextField field = createNullAnd42RejectingFieldWithEmptyValue(null); + + Binder<AtomicReference<Integer>> binder = createIntegerConverterBinder( + field); + + binder.readBean(new AtomicReference<>(null)); + } + + private TextField createNullAnd42RejectingFieldWithEmptyValue( + String emptyValue) { + return new TextField() { + @Override + public void setValue(String value) { + if (value == null) { + throw new NullPointerException("Null value"); + } else if ("42".equals(value)) { + throw new IllegalArgumentException("42 is not allowed"); + } + super.setValue(value); + } + + @Override + public String getEmptyValue() { + return emptyValue; + } + }; + } + + private Binder<AtomicReference<Integer>> createIntegerConverterBinder( + TextField field) { + Binder<AtomicReference<Integer>> binder = new Binder<>(); + binder.forField(field) + .withConverter(new StringToIntegerConverter("Must have number")) + .bind(AtomicReference::get, AtomicReference::set); + return binder; + } } diff --git a/server/src/test/java/com/vaadin/server/VaadinPortletServiceTest.java b/server/src/test/java/com/vaadin/server/VaadinPortletServiceTest.java index 2d7da64d20..2280e7d4f5 100644 --- a/server/src/test/java/com/vaadin/server/VaadinPortletServiceTest.java +++ b/server/src/test/java/com/vaadin/server/VaadinPortletServiceTest.java @@ -9,6 +9,8 @@ import static org.mockito.Mockito.when; import java.util.concurrent.locks.ReentrantLock; +import javax.portlet.PortletSession; + import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; @@ -185,9 +187,9 @@ public class VaadinPortletServiceTest { ReentrantLock mockLock = Mockito.mock(ReentrantLock.class); when(mockLock.isHeldByCurrentThread()).thenReturn(true); - WrappedSession emptyWrappedSession = Mockito + WrappedPortletSession emptyWrappedSession = Mockito .mock(WrappedPortletSession.class); - when(emptyWrappedSession.getAttribute("null.lock")) + when(emptyWrappedSession.getAttribute("null.lock",PortletSession.APPLICATION_SCOPE)) .thenReturn(mockLock); VaadinRequest requestWithUIIDSet = Mockito .mock(VaadinRequest.class); diff --git a/server/src/test/java/com/vaadin/server/VaadinServletTest.java b/server/src/test/java/com/vaadin/server/VaadinServletTest.java index f1490208d4..652dc30665 100644 --- a/server/src/test/java/com/vaadin/server/VaadinServletTest.java +++ b/server/src/test/java/com/vaadin/server/VaadinServletTest.java @@ -116,6 +116,8 @@ public class VaadinServletTest { HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getServletPath()).thenReturn(servletPath); Mockito.when(request.getPathInfo()).thenReturn(pathInfo); + Mockito.when(request.getRequestURI()).thenReturn("/context"+pathInfo); + Mockito.when(request.getContextPath()).thenReturn("/context"); return request; } } diff --git a/test/servlet-containers/generic/build.xml b/test/servlet-containers/generic/build.xml index 61f915c833..e5af1c85bf 100644 --- a/test/servlet-containers/generic/build.xml +++ b/test/servlet-containers/generic/build.xml @@ -39,10 +39,10 @@ <target name="clean-testbench-errors"><!--todo remove when have got rid of screenshots--> <fail unless="com.vaadin.testbench.screenshot.directory" message="Define screenshot directory using -Dcom.vaadin.testbench.screenshot.directory" /> - <mkdir dir="${com.vaadin.testbench.screenshot.directory}/errors" /> + <mkdir dir="${com.vaadin.testbench.screenshot.directory}/error-screenshots" /> <delete> <fileset - dir="${com.vaadin.testbench.screenshot.directory}/errors"> + dir="${com.vaadin.testbench.screenshot.directory}/error-screenshots"> <include name="*" /> </fileset> </delete> diff --git a/test/spring-boot-subcontext/src/test/java/com/example/VaadinSpringBootSmokeIT.java b/test/spring-boot-subcontext/src/test/java/com/example/VaadinSpringBootSmokeIT.java index f6f3db6243..bbd0bc628f 100644 --- a/test/spring-boot-subcontext/src/test/java/com/example/VaadinSpringBootSmokeIT.java +++ b/test/spring-boot-subcontext/src/test/java/com/example/VaadinSpringBootSmokeIT.java @@ -40,12 +40,26 @@ public class VaadinSpringBootSmokeIT extends TestBenchTestCase { @Test public void testPageLoadsAndButtonWorks() { getDriver().navigate() + .to("http://localhost:" + port + DemoApplication.CONTEXT); + runSmokeTest(); + } + + @Test + public void testPageLoadsAndButtonWorksWithExtraSlash() { + getDriver().navigate() .to("http://localhost:" + port + "/" + DemoApplication.CONTEXT); runSmokeTest(); } @Test public void testSubPathPageLoadsAndButtonWorks() { + getDriver().navigate().to("http://localhost:" + port + + DemoApplication.CONTEXT + "/" + SubPathUI.SUBPATH); + runSmokeTest(); + } + + @Test + public void testSubPathPageLoadsAndButtonWorksWithExtraSlash() { getDriver().navigate().to("http://localhost:" + port + "/" + DemoApplication.CONTEXT + "/" + SubPathUI.SUBPATH); runSmokeTest(); diff --git a/uitest/eclipse-run-selected-test.properties b/uitest/eclipse-run-selected-test.properties index 0315b46a6d..1d257d1773 100644 --- a/uitest/eclipse-run-selected-test.properties +++ b/uitest/eclipse-run-selected-test.properties @@ -10,7 +10,7 @@ # Location of the screenshot directory. This is mutually exclusive with the folder settings for XVFB testing. # This is the directory that contains the "references" directory -com.vaadin.testbench.screenshot.directory=<enter the full path to the screenshots directory, parent of "references" directory> +com.vaadin.testbench.screenshot.directory=<enter the full path to the "uitest" directory, parent of "reference-screenshots" directory> # Deployment url to use for testing. Context path must be / diff --git a/uitest/src/main/java/com/vaadin/screenshotbrowser/ScreenshotBrowser.java b/uitest/src/main/java/com/vaadin/screenshotbrowser/ScreenshotBrowser.java index 089a981b1f..37916493fa 100644 --- a/uitest/src/main/java/com/vaadin/screenshotbrowser/ScreenshotBrowser.java +++ b/uitest/src/main/java/com/vaadin/screenshotbrowser/ScreenshotBrowser.java @@ -9,6 +9,10 @@ import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.DirectoryFileFilter; +import org.apache.commons.io.filefilter.SuffixFileFilter; + import com.vaadin.event.ShortcutAction.KeyCode; import com.vaadin.event.ShortcutListener; import com.vaadin.server.ExternalResource; @@ -35,11 +39,11 @@ public class ScreenshotBrowser extends UI { * 3 - platform * 4 - browser name * 5 - browser version - * 6 - additional qualifiers - * 7 - identifier + * 6 - identifier + * 7 - additional identifiers */ private static final Pattern screenshotNamePattern = Pattern - .compile("(.+?)-(.+?)_(.+?)_(.+?)_(.+?)(_.+)?_(.+?)\\.png\\.html"); + .compile("(.+?)-(.+?)_(.+?)_(.+?)_(.*?)_(.+?)(_.+)?\\.png\\.html"); public static enum Action { ACCEPT { @@ -97,7 +101,7 @@ public class ScreenshotBrowser extends UI { } private static File getReferenceDir() { - return new File(screenshotDir, "reference"); + return new File(screenshotDir, "reference-screenshots"); } private static File getAlternative(File baseFile, @@ -146,12 +150,12 @@ public class ScreenshotBrowser extends UI { return matcher.group(4) + " " + matcher.group(5); } - public String getQualifiers() { - return matcher.group(6); - } - public String getIdentifier() { - return matcher.group(7); + String additional = matcher.group(7); + if (additional != null) { + return matcher.group(6) + additional; + } + return matcher.group(6); } public void setAction(Action action) { @@ -176,7 +180,8 @@ public class ScreenshotBrowser extends UI { left.setMargin(true); left.setSpacing(true); - left.setSizeFull(); + left.setSizeUndefined(); + left.setWidth("270px"); left.addComponent( createActionButton("Accept changes", 'j', Action.ACCEPT)); @@ -199,15 +204,17 @@ public class ScreenshotBrowser extends UI { left.addComponent(expandSpacer); left.setExpandRatio(expandSpacer, 1); - left.addComponent(new Label( - "Press the j, k or l keys to quickly select an action for the selected item.")); + Label instructions = new Label( + "Press the j, k or l keys to quickly select an action for the selected item."); + instructions.setWidth("100%"); + left.addComponent(instructions); root.setExpandRatio(left, 1); root.setSizeFull(); setCompositionRoot(root); setHeight("850px"); - setWidth("100%"); + setWidth("1800px"); } private Button createActionButton(String caption, char shortcut, @@ -216,6 +223,11 @@ public class ScreenshotBrowser extends UI { caption + " <strong>" + shortcut + "</strong>", createSetActionListener(action)); button.setCaptionAsHtml(true); + if (!Action.IGNORE.equals(action)) { + // other actions disabled for now since the functionality was + // designed for a different directory structure, needs reworking + button.setEnabled(false); + } return button; } @@ -253,7 +265,7 @@ public class ScreenshotBrowser extends UI { @Override protected void init(VaadinRequest request) { table.setWidth("100%"); - table.setHeight("100%"); + table.setPageLength(10); table.setMultiSelect(true); table.addValueChangeListener(event -> { @@ -274,9 +286,8 @@ public class ScreenshotBrowser extends UI { refreshTableContainer(); - VerticalLayout mainLayout = new VerticalLayout(table, viewer); - mainLayout.setExpandRatio(table, 1); - mainLayout.setSizeFull(); + VerticalLayout mainLayout = new VerticalLayout(viewer, table); + mainLayout.setSizeUndefined(); setSizeFull(); setContent(mainLayout); @@ -339,10 +350,10 @@ public class ScreenshotBrowser extends UI { } private void refreshTableContainer() { - File errorsDir = new File(screenshotDir, "errors"); + File errorsDir = new File(screenshotDir, "error-screenshots"); - File[] failures = errorsDir - .listFiles((dir, name) -> name.endsWith(".html")); + Collection<File> failures = FileUtils.listFiles(errorsDir, + new SuffixFileFilter(".html"), DirectoryFileFilter.DIRECTORY); BeanItemContainer<ComparisonFailure> container = new BeanItemContainer<>( ComparisonFailure.class); @@ -352,7 +363,7 @@ public class ScreenshotBrowser extends UI { table.setContainerDataSource(container); table.setVisibleColumns("testClass", "testMethod", "browser", - "qualifiers", "identifier", "action"); + "identifier", "action"); if (container.size() > 0) { table.select(container.firstItemId()); } diff --git a/uitest/src/main/java/com/vaadin/tests/components/combobox/ComboBoxPasteFilter.java b/uitest/src/main/java/com/vaadin/tests/components/combobox/ComboBoxPasteFilter.java new file mode 100644 index 0000000000..ba36c69d73 --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/combobox/ComboBoxPasteFilter.java @@ -0,0 +1,66 @@ +package com.vaadin.tests.components.combobox; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.vaadin.data.provider.ListDataProvider; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.ComboBox; +import com.vaadin.ui.Label; + +public class ComboBoxPasteFilter extends AbstractTestUI { + + private static final String WORDS = "loutish offer popcorn bitter buzz " + + "change mass boy erect aquatic donkey gentle colorful zippy " + + "soup pocket bathe fear supreme pan present knife quartz shy " + + "conscious tested thumb snow evasive reason dusty bridge giddy " + + "smooth bomb endurable tiger red gun fix regret quizzical income " + + "careless owe sleet loss silent serious play consider messy " + + "retire reduce shaky shiny low suggest preach bleach drunk " + + "talk instruct peck hungry improve meat chop title encourage " + + "marry island romantic fabulous kneel guarantee dock complain " + + "mate tour intend geese hole swing mine superb level slip " + + "spoon sky live nine open playground guard possible hate " + + "spotless apparatus bow"; + + @Override + protected void setup(VaadinRequest request) { + ComboBox<String> box = new ComboBox<String>("Paste a word from below"); + Collection<String> massiveData = massiveData(); + box.setDataProvider(new ListDataProvider<String>(massiveData)); + addComponent(box); + + Label label = new Label(WORDS); + label.setSizeFull(); + addComponent(label); + } + + private Collection<String> massiveData() { + return find(WORDS, "([\\w]+)"); + } + + public List<String> find(String text, String patternStr) { + Pattern pattern = Pattern.compile(patternStr); + Matcher matcher = pattern.matcher(text); + List<String> result = new ArrayList<>(); + while (matcher.find()) { + result.add(matcher.group()); + } + return result; + } + + @Override + protected Integer getTicketNumber() { + return 11779; + } + + @Override + protected String getTestDescription() { + return "ComboBox should filter regardless of whether the value is " + + "pasted with keyboard or with mouse"; + } +} diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/GridSelectAllFiltering.java b/uitest/src/main/java/com/vaadin/tests/components/grid/GridSelectAllFiltering.java new file mode 100644 index 0000000000..3c85e9c435 --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/grid/GridSelectAllFiltering.java @@ -0,0 +1,71 @@ +package com.vaadin.tests.components.grid; + +import java.util.Iterator; +import java.util.Set; + +import com.vaadin.data.provider.DataProvider; +import com.vaadin.data.provider.ListDataProvider; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.data.bean.Person; +import com.vaadin.ui.Button; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.SelectionMode; +import com.vaadin.ui.renderers.NumberRenderer; + +public class GridSelectAllFiltering extends SimpleGridUI { + private String filterText = "Johannes"; + + @Override + protected void setup(VaadinRequest request) { + Grid<Person> grid = new Grid<>(); + grid.setSelectionMode(SelectionMode.MULTI); + grid.setHeightByRows(3); + grid.addColumn(Person::getFirstName); + grid.addColumn(Person::getAge, new NumberRenderer()); + + ListDataProvider<Person> dataProvider = DataProvider + .ofCollection(createPersons()); + dataProvider.setFilter(person -> { + if (person.getFirstName().contains(filterText)) { + return false; + } + return true; + }); + grid.setDataProvider(dataProvider); + + Button toggleButton = new Button("Toggle filter", e -> { + if ("Johannes".equals(filterText)) { + filterText = "Galileo"; + } else { + filterText = "Johannes"; + } + dataProvider.refreshAll(); + }); + toggleButton.setId("toggle"); + + Button checkButton = new Button("Check selection", e -> { + Set<Person> selected = grid.getSelectedItems(); + Iterator<Person> i = selected.iterator(); + log("selected " + selected.size() + + (i.hasNext() ? ": " + i.next().getFirstName() : "") + + (i.hasNext() ? ", " + i.next().getFirstName() : "") + + (i.hasNext() ? ", " + i.next().getFirstName() : "") + + (i.hasNext() ? "... " : "")); + }); + checkButton.setId("check"); + + addComponents(grid, toggleButton, checkButton); + } + + @Override + protected Integer getTicketNumber() { + return 11479; + } + + @Override + protected String getTestDescription() { + return "Selecting all does not select items that have been " + + "filtered out, they should not be shown selected " + + "after the filter changes."; + } +} diff --git a/uitest/src/main/java/com/vaadin/tests/components/tree/TreeBasicFeatures.java b/uitest/src/main/java/com/vaadin/tests/components/tree/TreeBasicFeatures.java index f429a6102c..98709f00fa 100644 --- a/uitest/src/main/java/com/vaadin/tests/components/tree/TreeBasicFeatures.java +++ b/uitest/src/main/java/com/vaadin/tests/components/tree/TreeBasicFeatures.java @@ -115,6 +115,15 @@ public class TreeBasicFeatures extends AbstractTestUIWithLog { ? t -> "level" + t.getDepth() : t -> null)) .setCheckable(true); + MenuItem enabled = componentMenu.addItem("Enabled", + menuItem -> tree.setEnabled(!tree.isEnabled())); + enabled.setCheckable(true); + enabled.setChecked(true); + + componentMenu.addItem("Expand 0 | 0", + menuItem -> tree.expand(new HierarchicalTestBean(null, 0, 0))); + componentMenu.addItem("Collapse 0 | 0", menuItem -> tree + .collapse(new HierarchicalTestBean(null, 0, 0))); return menu; } diff --git a/uitest/src/main/java/com/vaadin/tests/components/tree/TreeInitiallyDisabled.java b/uitest/src/main/java/com/vaadin/tests/components/tree/TreeInitiallyDisabled.java new file mode 100644 index 0000000000..ca4a6bdb70 --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/tree/TreeInitiallyDisabled.java @@ -0,0 +1,51 @@ +package com.vaadin.tests.components.tree; + +import com.vaadin.data.TreeData; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.Button; +import com.vaadin.ui.Tree; + +public class TreeInitiallyDisabled extends AbstractTestUI { + + @Override + protected void setup(VaadinRequest request) { + Tree<String> tree = new Tree<>(); + TreeData<String> treeData = new TreeData<>(); + String parent1 = "Parent 1"; + treeData.addItem(null, parent1); + treeData.addItem(parent1, "Child 1.1"); + treeData.addItem(parent1, "Child 1.2"); + treeData.addItem(parent1, "Child 1.3"); + String parent2 = "Parent 2"; + treeData.addItem(null, parent2); + treeData.addItem(parent2, "Child 2.1"); + treeData.addItem(parent2, "Child 2.2"); + treeData.addItem(parent2, "Child 2.3"); + String parent3 = "Parent 3"; + treeData.addItem(null, parent3); + treeData.addItem(parent3, "Child 3.1"); + treeData.addItem(parent3, "Child 3.2"); + treeData.addItem(parent3, "Child 3.3"); + tree.setTreeData(treeData); + + Button button = new Button("Toggle enabled/disabled"); + button.addClickListener(event -> { + tree.setEnabled(!tree.isEnabled()); + }); + + addComponents(tree, button); + + tree.setEnabled(false); + } + + @Override + protected Integer getTicketNumber() { + return 11831; + } + + @Override + protected String getTestDescription() { + return "Initially disabled Tree should have disabled styles."; + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrollDestinationTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrollDestinationTest.java index 67a831557d..302ea741a4 100644 --- a/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrollDestinationTest.java +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrollDestinationTest.java @@ -15,10 +15,12 @@ import com.vaadin.testbench.TestBenchElement; import com.vaadin.testbench.elements.ButtonElement; import com.vaadin.testbench.elements.GridElement; import com.vaadin.testbench.elements.NativeSelectElement; +import com.vaadin.testbench.elements.TextFieldElement; import com.vaadin.tests.tb3.SingleBrowserTest; public class GridScrollDestinationTest extends SingleBrowserTest { + private TextFieldElement textField; private ButtonElement button; private GridElement grid; private TestBenchElement header; @@ -28,6 +30,7 @@ public class GridScrollDestinationTest extends SingleBrowserTest { public void setup() throws Exception { super.setup(); openTestURL(); + textField = $(TextFieldElement.class).first(); button = $(ButtonElement.class).first(); grid = $(GridElement.class).first(); header = grid.getHeader(); @@ -110,6 +113,28 @@ public class GridScrollDestinationTest extends SingleBrowserTest { rows = grid.getBody().findElements(By.className("v-grid-row")); row = rows.get(6); assertEquals("50", row.getText()); + + // scroll to beginning using scroll destination + textField.setValue("0"); + button.click(); + + // expect to be scrolled all the way up + rows = grid.getBody().findElements(By.className("v-grid-row")); + row = rows.get(0); + assertEquals("0", row.getText()); + + assertElementAtTop(row); + + // scroll to end using scroll destination + textField.setValue("99"); + button.click(); + + // expect to be scrolled all the way down + rows = grid.getBody().findElements(By.className("v-grid-row")); + row = rows.get(rows.size() - 1); + assertEquals("99", row.getText()); + + assertElementAtBottom(row); } @Test @@ -168,6 +193,29 @@ public class GridScrollDestinationTest extends SingleBrowserTest { assertEquals("50", row.getText()); assertElementAtBottom(row); + + // scroll to beginning using scroll destination + textField.setValue("0"); + button.click(); + button.click(); + + // expect to be scrolled all the way up + rows = grid.getBody().findElements(By.className("v-grid-row")); + row = rows.get(0); + assertEquals("0", row.getText()); + + assertElementAtTop(row); + + // scroll to end using scroll destination + textField.setValue("99"); + button.click(); + + // expect to be scrolled all the way down + rows = grid.getBody().findElements(By.className("v-grid-row")); + row = rows.get(rows.size() - 1); + assertEquals("99", row.getText()); + + assertElementAtBottom(row); } @Test @@ -226,6 +274,28 @@ public class GridScrollDestinationTest extends SingleBrowserTest { assertEquals("50", row.getText()); assertElementAtTop(row); + + // scroll to beginning using scroll destination + textField.setValue("0"); + button.click(); + + // expect to be scrolled all the way up + rows = grid.getBody().findElements(By.className("v-grid-row")); + row = rows.get(0); + assertEquals("0", row.getText()); + + assertElementAtTop(row); + + // scroll to end using scroll destination + textField.setValue("99"); + button.click(); + + // expect to be scrolled all the way down + rows = grid.getBody().findElements(By.className("v-grid-row")); + row = rows.get(rows.size() - 1); + assertEquals("99", row.getText()); + + assertElementAtBottom(row); } @Test @@ -287,5 +357,27 @@ public class GridScrollDestinationTest extends SingleBrowserTest { assertEquals("50", row.getText()); assertElementAtMiddle(row); + + // scroll to beginning using scroll destination + textField.setValue("0"); + button.click(); + + // expect to be scrolled all the way up + rows = grid.getBody().findElements(By.className("v-grid-row")); + row = rows.get(0); + assertEquals("0", row.getText()); + + assertElementAtTop(row); + + // scroll to end using scroll destination + textField.setValue("99"); + button.click(); + + // expect to be scrolled all the way down + rows = grid.getBody().findElements(By.className("v-grid-row")); + row = rows.get(rows.size() - 1); + assertEquals("99", row.getText()); + + assertElementAtBottom(row); } } diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/GridSelectAllFilteringTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/GridSelectAllFilteringTest.java new file mode 100644 index 0000000000..37c7d6281c --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/GridSelectAllFilteringTest.java @@ -0,0 +1,62 @@ +package com.vaadin.tests.components.grid; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.openqa.selenium.WebElement; + +import com.vaadin.testbench.By; +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.testbench.elements.GridElement; +import com.vaadin.tests.tb3.MultiBrowserTest; + +public class GridSelectAllFilteringTest extends MultiBrowserTest { + + @Test + public void checkSelectAll() { + openTestURL(); + + GridElement grid = $(GridElement.class).first(); + ButtonElement toggleButton = $(ButtonElement.class).id("toggle"); + ButtonElement checkButton = $(ButtonElement.class).id("check"); + + // ensure no initial selection + checkButton.click(); + assertEquals("Unexpected log entry,", "1. selected 0", getLogRow(0)); + assertEquals("Unexpected amount of visually selected rows,", 0, + grid.findElements(By.className("v-grid-row-selected")).size()); + + // select all + WebElement selectAllCheckbox = grid + .findElement(By.className("v-grid-select-all-checkbox")); + selectAllCheckbox.click(); + + // ensure only the two visible rows get selected + checkButton.click(); + assertEquals("Unexpected log entry,", + "2. selected 2: Nicolaus Copernicus, Galileo Galilei", + getLogRow(0)); + assertEquals("Unexpected amount of visually selected rows,", 2, + grid.findElements(By.className("v-grid-row-selected")).size()); + + // toggle filter + toggleButton.click(); + + // ensure selection did not change but only one selected row is visible + checkButton.click(); + assertEquals("Unexpected log entry,", + "3. selected 2: Nicolaus Copernicus, Galileo Galilei", + getLogRow(0)); + assertEquals("Unexpected amount of visually selected rows,", 1, + grid.findElements(By.className("v-grid-row-selected")).size()); + + // remove all selections + selectAllCheckbox.click(); + + // ensure all selections got removed whether they were visible or not + checkButton.click(); + assertEquals("Unexpected log entry,", "4. selected 0", getLogRow(0)); + assertEquals("Unexpected amount of visually selected rows,", 0, + grid.findElements(By.className("v-grid-row-selected")).size()); + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/tree/TreeBasicFeaturesTest.java b/uitest/src/test/java/com/vaadin/tests/components/tree/TreeBasicFeaturesTest.java index 4e133015b3..bc8771aa91 100644 --- a/uitest/src/test/java/com/vaadin/tests/components/tree/TreeBasicFeaturesTest.java +++ b/uitest/src/test/java/com/vaadin/tests/components/tree/TreeBasicFeaturesTest.java @@ -252,4 +252,26 @@ public class TreeBasicFeaturesTest extends MultiBrowserTest { $(TreeElement.class).first().getItem(0).showTooltip(); assertEquals("", "0 | 0", getTooltipElement().getText()); } + + @Test + public void tree_disable_enable_expand_collapse() { + TreeElement tree = $(TreeElement.class).first(); + selectMenuPath("Component", "Enabled"); + assertTrue(tree.hasClassName("v-disabled")); + // ensure expanding doesn't work + tree.expand(0); + assertEquals("0 | 1", tree.getItem(1).getText()); + selectMenuPath("Component", "Enabled"); + assertFalse(tree.hasClassName("v-disabled")); + // ensure expanding and collapsing works again + tree.expand(0); + assertEquals("1 | 0", tree.getItem(1).getText()); + tree.collapse(0); + assertEquals("0 | 1", tree.getItem(1).getText()); + // same test for server-side expanding and collapsing + selectMenuPath("Component", "Expand 0 | 0"); + assertEquals("1 | 0", tree.getItem(1).getText()); + selectMenuPath("Component", "Collapse 0 | 0"); + assertEquals("0 | 1", tree.getItem(1).getText()); + } } diff --git a/uitest/src/test/java/com/vaadin/tests/components/tree/TreeInitiallyDisabledTest.java b/uitest/src/test/java/com/vaadin/tests/components/tree/TreeInitiallyDisabledTest.java new file mode 100644 index 0000000000..93cb1633f1 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/tree/TreeInitiallyDisabledTest.java @@ -0,0 +1,18 @@ +package com.vaadin.tests.components.tree; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.vaadin.testbench.elements.TreeElement; +import com.vaadin.tests.tb3.SingleBrowserTest; + +public class TreeInitiallyDisabledTest extends SingleBrowserTest { + + @Test + public void checkDisabledStyleAdded() { + openTestURL(); + TreeElement tree = $(TreeElement.class).first(); + assertTrue(tree.hasClassName("v-disabled")); + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeaturesTest.java b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeaturesTest.java index 39d40438fc..04ca0003b9 100644 --- a/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeaturesTest.java +++ b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBasicFeaturesTest.java @@ -348,6 +348,30 @@ public class TreeGridBasicFeaturesTest extends MultiBrowserTest { assertEquals("Not expanded", "1 | 0", grid.getCell(1, 0).getText()); } + @Test + public void disable_enable_expand_collapse() { + TreeGridElement treeGrid = $(TreeGridElement.class).first(); + selectMenuPath("Component", "State", "Enabled"); + assertTrue(treeGrid.hasClassName("v-disabled")); + // ensure expanding doesn't work + treeGrid.expandWithClick(0); + assertCellTexts(1, 0, new String[] { "0 | 1" }); + selectMenuPath("Component", "State", "Enabled"); + assertFalse(treeGrid.hasClassName("v-disabled")); + // ensure expanding and collapsing works again + treeGrid.expandWithClick(0); + assertCellTexts(1, 0, new String[] { "1 | 0" }); + treeGrid.collapseWithClick(0); + assertCellTexts(1, 0, new String[] { "0 | 1" }); + // same test for server-side expanding and collapsing + selectMenuPath("Component", "Features", "Server-side expand", + "Expand 0 | 0"); + assertCellTexts(1, 0, new String[] { "1 | 0" }); + selectMenuPath("Component", "Features", "Server-side collapse", + "Collapse 0 | 0"); + assertCellTexts(1, 0, new String[] { "0 | 1" }); + } + private void assertCellTexts(int startRowIndex, int cellIndex, String[] cellTexts) { int index = startRowIndex; diff --git a/uitest/src/test/java/com/vaadin/tests/tb3/PrivateTB3Configuration.java b/uitest/src/test/java/com/vaadin/tests/tb3/PrivateTB3Configuration.java index 1f4d113021..0390931968 100644 --- a/uitest/src/test/java/com/vaadin/tests/tb3/PrivateTB3Configuration.java +++ b/uitest/src/test/java/com/vaadin/tests/tb3/PrivateTB3Configuration.java @@ -80,8 +80,9 @@ public abstract class PrivateTB3Configuration extends ScreenshotTB3Test { String dir = System.getProperty(SCREENSHOT_DIRECTORY, properties.getProperty(SCREENSHOT_DIRECTORY)); if (dir != null && !dir.isEmpty()) { - String reference = Paths.get(dir, "reference").toString(); - String errors = Paths.get(dir, "errors").toString(); + String reference = Paths.get(dir, "reference-screenshots") + .toString(); + String errors = Paths.get(dir, "error-screenshots").toString(); Parameters.setScreenshotReferenceDirectory(reference); Parameters.setScreenshotErrorDirectory(errors); } else { diff --git a/uitest/src/test/java/com/vaadin/tests/tb3/VaadinBrowserFactory.java b/uitest/src/test/java/com/vaadin/tests/tb3/VaadinBrowserFactory.java index e113e7783b..82db9898d2 100644 --- a/uitest/src/test/java/com/vaadin/tests/tb3/VaadinBrowserFactory.java +++ b/uitest/src/test/java/com/vaadin/tests/tb3/VaadinBrowserFactory.java @@ -43,7 +43,7 @@ public class VaadinBrowserFactory extends DefaultBrowserFactory { case PHANTOMJS: return create(browser, "1", Platform.LINUX); case CHROME: - return create(browser, "40", Platform.VISTA); + return create(browser, "", Platform.ANY); case FIREFOX: default: DesiredCapabilities dc = create(Browser.FIREFOX, "45", |