Skip to content

Commit

Permalink
Detect hierarchy changes not sent to the client (#18317)
Browse files Browse the repository at this point in the history
Change-Id: I77b420738738a42ff50e2a509e4ac4072b1b6e1f
  • Loading branch information
Legioth authored and Vaadin Code Review committed Dec 2, 2015
1 parent 907e689 commit 3cf5700
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 4 deletions.
Expand Up @@ -26,6 +26,8 @@
import com.vaadin.server.ClientConnector;
import com.vaadin.server.LegacyCommunicationManager;
import com.vaadin.server.PaintException;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinService;
import com.vaadin.ui.UI;

import elemental.json.Json;
Expand Down Expand Up @@ -87,6 +89,22 @@ public void write(UI ui, Writer writer, Set<String> stateUpdateConnectors)
}
}
}
// Dummy assert just for conditionally storing away data that will be
// used by the real assert later on
assert storeSentHierarchy(hierarchyInfo);

writer.write(JsonUtil.stringify(hierarchyInfo));
}

private boolean storeSentHierarchy(JsonObject hierarchyInfo) {
VaadinRequest request = VaadinService.getCurrentRequest();
if (request != null) {
request.setAttribute(ConnectorHierarchyWriter.class.getName()
+ ".hierarchyInfo", hierarchyInfo);
}

// Always true, we're just setting up for another assert
return true;
}

}
52 changes: 52 additions & 0 deletions server/src/com/vaadin/ui/ConnectorTracker.java
Expand Up @@ -37,6 +37,9 @@
import com.vaadin.server.GlobalResourceHandler;
import com.vaadin.server.LegacyCommunicationManager;
import com.vaadin.server.StreamVariable;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinService;
import com.vaadin.server.communication.ConnectorHierarchyWriter;

import elemental.json.Json;
import elemental.json.JsonException;
Expand Down Expand Up @@ -326,6 +329,13 @@ assert isHierarchyComplete() : "The connector hierarchy is corrupted. "
.isConnectorVisibleToClient(connector)) {
uninitializedConnectors.add(connector);
diffStates.remove(connector);

assert isRemovalSentToClient(connector) : "Connector "
+ connector
+ " (id = "
+ connector.getConnectorId()
+ ") is no longer visible to the client, but no corresponding hierarchy change is being sent.";

if (getLogger().isLoggable(Level.FINE)) {
getLogger()
.log(Level.FINE,
Expand All @@ -338,6 +348,48 @@ assert isHierarchyComplete() : "The connector hierarchy is corrupted. "
cleanStreamVariables();
}

private boolean isRemovalSentToClient(ClientConnector connector) {
VaadinRequest request = VaadinService.getCurrentRequest();
if (request == null) {
// Probably run from a unit test without normal request handling
return true;
}

String attributeName = ConnectorHierarchyWriter.class.getName()
+ ".hierarchyInfo";
Object hierarchyInfoObj = request.getAttribute(attributeName);
if (hierarchyInfoObj instanceof JsonObject) {
JsonObject hierachyInfo = (JsonObject) hierarchyInfoObj;

ClientConnector firstVisibleParent = findFirstVisibleParent(connector);
if (firstVisibleParent == null) {
// Connector is detached, not our business
return true;
}

if (!hierachyInfo.hasKey(firstVisibleParent.getConnectorId())) {
return false;
}
} else {
getLogger().warning(
"Request attribute " + attributeName
+ " is not a JsonObject");
}

return true;
}

private ClientConnector findFirstVisibleParent(ClientConnector connector) {
while (connector != null) {
connector = connector.getParent();
if (LegacyCommunicationManager
.isConnectorVisibleToClient(connector)) {
return connector;
}
}
return null;
}

private void removeUnregisteredConnectors() {
GlobalResourceHandler globalResourceHandler = uI.getSession()
.getGlobalResourceHandler(false);
Expand Down
@@ -0,0 +1,70 @@
/*
* Copyright 2000-2014 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.tests.application;

import com.vaadin.server.VaadinRequest;
import com.vaadin.tests.components.AbstractTestUI;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Component;
import com.vaadin.ui.CssLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.SelectiveRenderer;

public class MissingHierarchyDetection extends AbstractTestUI {

private boolean isChildRendered = true;
private BrokenCssLayout layout = new BrokenCssLayout();

public class BrokenCssLayout extends CssLayout implements SelectiveRenderer {
public BrokenCssLayout() {
setCaption("Broken layout");
Label label = new Label("Child component");
label.setId("label");
addComponent(label);
}

@Override
public boolean isRendered(Component childComponent) {
return isChildRendered;
}
}

@Override
protected void setup(VaadinRequest request) {
addComponent(layout);
addComponent(new Button("Toggle properly", new Button.ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
toggle(true);
}
}));
addComponent(new Button("Toggle improperly",
new Button.ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
toggle(false);
}
}));
}

private void toggle(boolean properly) {
isChildRendered = !isChildRendered;
if (properly) {
layout.markAsDirtyRecursive();
}
}
}
@@ -0,0 +1,50 @@
/*
* Copyright 2000-2014 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.tests.application;

import org.junit.Assert;
import org.junit.Test;
import org.openqa.selenium.By;

import com.vaadin.testbench.elements.ButtonElement;
import com.vaadin.testbench.elements.LabelElement;
import com.vaadin.tests.tb3.SingleBrowserTest;

public class MissingHierarchyDetectionTest extends SingleBrowserTest {
@Test
public void testMissingHierarchyDetection() {
openTestURL();

Assert.assertTrue(isElementPresent(By.id("label")));

ButtonElement toggleProperly = $(ButtonElement.class).caption(
"Toggle properly").first();

toggleProperly.click();
assertNoSystemNotifications();
Assert.assertFalse(isElementPresent(By.id("label")));

toggleProperly.click();
assertNoSystemNotifications();
Assert.assertTrue(isElementPresent(LabelElement.class));

ButtonElement toggleInproperly = $(ButtonElement.class).caption(
"Toggle improperly").first();
toggleInproperly.click();

assertSystemNotification();
}
}
32 changes: 28 additions & 4 deletions uitest/src/com/vaadin/tests/tb3/AbstractTB3Test.java
Expand Up @@ -1120,12 +1120,36 @@ protected void assertElementNotPresent(By by) {
* "?debug" as exceptions are otherwise not shown as notifications.
*/
protected void assertNoErrorNotifications() {
Assert.assertTrue(
"Debug window must be open to be able to see error notifications",
isDebugWindowOpen());
Assert.assertFalse(
"Error notification with client side exception is shown",
isElementPresent(By.className("v-Notification-error")));
isNotificationPresent("error"));
}

/**
* Asserts that no system notifications are shown.
*/
protected void assertNoSystemNotifications() {
Assert.assertFalse(
"Error notification with system error exception is shown",
isNotificationPresent("system"));
}

/**
* Asserts that a system notification is shown.
*/
protected void assertSystemNotification() {
Assert.assertTrue(
"Error notification with system error exception is not shown",
isNotificationPresent("system"));
}

private boolean isNotificationPresent(String type) {
if ("error".equals(type)) {
Assert.assertTrue(
"Debug window must be open to be able to see error notifications",
isDebugWindowOpen());
}
return isElementPresent(By.className("v-Notification-" + type));
}

private boolean isDebugWindowOpen() {
Expand Down

0 comments on commit 3cf5700

Please sign in to comment.