--- /dev/null
+/*
+ * Copyright 2000-2016 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.server.data;
+
+import java.lang.reflect.Method;
+import java.util.EventObject;
+import java.util.Objects;
+
+import com.vaadin.event.EventRouter;
+import com.vaadin.shared.Registration;
+
+/**
+ * Abstract data source implementation which takes care of refreshing data from
+ * the underlying data provider.
+ *
+ * @author Vaadin Ltd
+ * @since 8.0
+ *
+ */
+public abstract class AbstractDataSource<T> implements DataSource<T> {
+
+ private EventRouter eventRouter;
+
+ @Override
+ public Registration addDataSourceListener(DataSourceListener listener) {
+ Objects.requireNonNull(listener, "listener cannot be null");
+ addListener(DataChangeEvent.class, listener,
+ DataSourceListener.class.getMethods()[0]);
+ return () -> removeListener(DataChangeEvent.class, listener);
+ }
+
+ @Override
+ public void refreshAll() {
+ fireEvent(new DataChangeEvent(this));
+ }
+
+ /**
+ * Registers a new listener with the specified activation method to listen
+ * events generated by this component. If the activation method does not
+ * have any arguments the event object will not be passed to it when it's
+ * called.
+ *
+ * @param eventType
+ * the type of the listened event. Events of this type or its
+ * subclasses activate the listener.
+ * @param listener
+ * the object instance who owns the activation method.
+ * @param method
+ * the activation method.
+ *
+ */
+ protected void addListener(Class<?> eventType, DataSourceListener listener,
+ Method method) {
+ if (eventRouter == null) {
+ eventRouter = new EventRouter();
+ }
+ eventRouter.addListener(eventType, listener, method);
+ }
+
+ /**
+ * Removes all registered listeners matching the given parameters. Since
+ * this method receives the event type and the listener object as
+ * parameters, it will unregister all <code>object</code>'s methods that are
+ * registered to listen to events of type <code>eventType</code> generated
+ * by this component.
+ *
+ * @param eventType
+ * the exact event type the <code>object</code> listens to.
+ * @param listener
+ * the target object that has registered to listen to events of
+ * type <code>eventType</code> with one or more methods.
+ */
+ protected void removeListener(Class<?> eventType,
+ DataSourceListener listener) {
+ if (eventRouter != null) {
+ eventRouter.removeListener(eventType, listener);
+ }
+ }
+
+ /**
+ * Sends the event to all listeners.
+ *
+ * @param event
+ * the Event to be sent to all listeners.
+ */
+ protected void fireEvent(EventObject event) {
+ if (eventRouter != null) {
+ eventRouter.fireEvent(event);
+ }
+ }
+}
* @param <T>
* data source data type
*/
-public class BackEndDataSource<T> implements DataSource<T> {
+public class BackEndDataSource<T> extends AbstractDataSource<T> {
private Function<Query, Stream<T>> request;
private Function<Query, Integer> sizeCallback;
--- /dev/null
+/*
+ * Copyright 2000-2016 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.server.data;
+
+import java.util.EventObject;
+
+/**
+ * An event fired when the data of a {@code DataSource} changes.
+ *
+ *
+ * @see DataSourceListener
+ *
+ * @author Vaadin Ltd
+ * @since 8.0
+ *
+ */
+public class DataChangeEvent extends EventObject {
+
+ /**
+ * Creates a new {@code DataChangeEvent} event originating from the given
+ * data source.
+ *
+ * @param source
+ * the data source, not null
+ */
+ public DataChangeEvent(DataSource<?> source) {
+ super(source);
+ }
+
+ @Override
+ public DataSource<?> getSource() {
+ return (DataSource<?>) super.getSource();
+ }
+
+}
import com.vaadin.server.AbstractExtension;
import com.vaadin.server.KeyMapper;
import com.vaadin.shared.Range;
+import com.vaadin.shared.Registration;
import com.vaadin.shared.data.DataCommunicatorClientRpc;
import com.vaadin.shared.data.DataCommunicatorConstants;
import com.vaadin.shared.data.DataRequestRpc;
*/
public class DataCommunicator<T> extends AbstractExtension {
+ private Registration dataSourceUpdateRegistration;
+
/**
* Simple implementation of collection data provider communication. All data
* is sent by server automatically and no data is requested by client.
keyMapper = createKeyMapper();
}
+ @Override
+ public void attach() {
+ super.attach();
+ attachDataSourceListener();
+ }
+
+ @Override
+ public void detach() {
+ super.detach();
+ detachDataSourceListener();
+ }
+
/**
* Initially and in the case of a reset all data should be pushed to the
* client.
public void setDataSource(DataSource<T> dataSource) {
Objects.requireNonNull(dataSource, "data source cannot be null");
this.dataSource = dataSource;
+ detachDataSourceListener();
+ if (isAttached()) {
+ attachDataSourceListener();
+ }
reset();
}
+
+ private void attachDataSourceListener() {
+ dataSourceUpdateRegistration = getDataSource()
+ .addDataSourceListener(event -> reset());
+ }
+
+ private void detachDataSourceListener() {
+ if (dataSourceUpdateRegistration != null) {
+ dataSourceUpdateRegistration.remove();
+ dataSourceUpdateRegistration = null;
+ }
+ }
}
import java.util.function.Function;
import java.util.stream.Stream;
+import com.vaadin.shared.Registration;
+
/**
* Minimal DataSource API for communication between the DataProvider and a back
* end service.
*/
int size(Query t);
+ /**
+ * Refreshes all data based on currently available data in the underlying
+ * provider.
+ */
+ void refreshAll();
+
+ /**
+ * Adds a data source listener. The listener is called when some piece of
+ * data is updated.
+ * <p>
+ * The {@link #refreshAll()} method fires {@link DataChangeEvent} each time
+ * when it's called. It allows to update UI components when user changes
+ * something in the underlying data.
+ *
+ * @see #refreshAll()
+ * @param listener
+ * the data change listener, not null
+ * @return a registration for the listener
+ */
+ Registration addDataSourceListener(DataSourceListener listener);
+
/**
* This method creates a new {@link ListDataSource} from a given Collection.
* The ListDataSource creates a protective List copy of all the contents in
--- /dev/null
+/*
+ * Copyright 2000-2016 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.server.data;
+
+import java.io.Serializable;
+
+/**
+ * Interface for listening for a data change events fired by a
+ * {@link DataSource}.
+ *
+ * @author Vaadin Ltd
+ * @since 8.0
+ */
+public interface DataSourceListener extends Serializable {
+
+ /**
+ * Invoked when this listener receives a data change event from a data
+ * source to which it has been added.
+ * <p>
+ * This event is fired when something has changed in the underlying data. It
+ * doesn't allow to distinguish different kind of events
+ * (add/remove/update). It means that the method implementation normally
+ * just reloads the whole data to refresh.
+ *
+ * @param event
+ * the received event, not null
+ */
+ void onDataChange(DataChangeEvent event);
+}
*/
package com.vaadin.server.data;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
-import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;
* @param <T>
* data type
*/
-public class ListDataSource<T> implements DataSource<T> {
+public class ListDataSource<T> extends AbstractDataSource<T> {
- private Function<Query, Stream<T>> request;
- private int size;
+ private Comparator<T> sortOrder;
+ private final Collection<T> backend;
/**
* Constructs a new ListDataSource. This method makes a protective copy of
*/
public ListDataSource(Collection<T> items) {
Objects.requireNonNull(items, "items cannot be null");
- final List<T> backend = new ArrayList<>(items);
- request = query -> backend.stream();
- size = backend.size();
+ backend = items;
+ sortOrder = null;
}
/**
* Chaining constructor for making modified {@link ListDataSource}s. This
* Constructor is used internally for making sorted and filtered variants of
* a base data source with actual data.
+ *
+ * @param items
+ * the backend data from the original list data source
+ * @param sortOrder
+ * a {@link Comparator} providing the needed sorting order
*
- * @param request
- * request for the new data source
*/
- protected ListDataSource(Function<Query, Stream<T>> request) {
- this.request = request;
+ protected ListDataSource(Collection<T> items, Comparator<T> sortOrder) {
+ this(items);
+ this.sortOrder = sortOrder;
}
@Override
public Stream<T> apply(Query query) {
- return request.apply(query);
+ Stream<T> stream = backend.stream();
+ if (sortOrder != null) {
+ stream = stream.sorted(sortOrder);
+ }
+ return stream;
}
/**
* @return new data source with modified sorting
*/
public ListDataSource<T> sortingBy(Comparator<T> sortOrder) {
- return new ListDataSource<>(q -> request.apply(q).sorted(sortOrder));
+ return new ListDataSource<>(backend, sortOrder);
}
/**
*/
@Override
public int size(Query t) {
- return size;
+ return backend.size();
}
+
}
--- /dev/null
+/*
+ * Copyright 2000-2016 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.server.data;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.shared.Registration;
+
+/**
+ * @author Vaadin Ltd
+ *
+ */
+public class AbstractDataSourceTest {
+
+ private static class TestDataSource extends AbstractDataSource<Object> {
+ @Override
+ public Stream<Object> apply(Query t) {
+ return null;
+ }
+
+ @Override
+ public int size(Query t) {
+ return 0;
+ }
+
+ @Override
+ public boolean isInMemory() {
+ return false;
+ }
+ }
+
+ @Test
+ public void refreshAll_notifyListeners() {
+ AbstractDataSource<Object> dataSource = new TestDataSource();
+ AtomicReference<DataChangeEvent> event = new AtomicReference<>();
+ dataSource.addDataSourceListener(ev -> {
+ Assert.assertNull(event.get());
+ event.set(ev);
+ });
+ dataSource.refreshAll();
+ Assert.assertNotNull(event.get());
+ Assert.assertEquals(dataSource, event.get().getSource());
+ }
+
+ @Test
+ public void removeListener_listenerIsNotNotified() {
+ AbstractDataSource<Object> dataSource = new TestDataSource();
+ AtomicReference<DataChangeEvent> event = new AtomicReference<>();
+ Registration registration = dataSource
+ .addDataSourceListener(ev -> event.set(ev));
+ registration.remove();
+ dataSource.refreshAll();
+ Assert.assertNull(event.get());
+ }
+}
--- /dev/null
+/*
+ * Copyright 2000-2016 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.server.data;
+
+import java.util.Collections;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.vaadin.server.MockVaadinSession;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.server.VaadinService;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.shared.Registration;
+import com.vaadin.ui.UI;
+
+/**
+ * @author Vaadin Ltd
+ *
+ */
+public class DataCommunicatorTest {
+
+ private static class TestUI extends UI {
+
+ private final VaadinSession session;
+
+ TestUI(VaadinSession session) {
+ this.session = session;
+ }
+
+ @Override
+ protected void init(VaadinRequest request) {
+ }
+
+ @Override
+ public VaadinSession getSession() {
+ return session;
+ }
+ }
+
+ private static class TestDataSource extends ListDataSource<Object>
+ implements Registration {
+
+ private Registration registration;
+
+ public TestDataSource() {
+ super(Collections.singleton(new Object()));
+ }
+
+ @Override
+ public Registration addDataSourceListener(DataSourceListener listener) {
+ registration = super.addDataSourceListener(listener);
+ return this;
+ }
+
+ @Override
+ public void remove() {
+ registration.remove();
+ registration = null;
+ }
+
+ public boolean isListenerAdded() {
+ return registration != null;
+ }
+
+ }
+
+ private static class TestDataCommunicator extends DataCommunicator<Object> {
+ protected void extend(UI ui) {
+ super.extend(ui);
+ }
+ }
+
+ private MockVaadinSession session = new MockVaadinSession(
+ Mockito.mock(VaadinService.class));
+
+ @Test
+ public void attach_dataSourceListenerIsNotAddedBeforeAttachAndAddedAfter() {
+ session.lock();
+
+ UI ui = new TestUI(session);
+
+ TestDataCommunicator communicator = new TestDataCommunicator();
+
+ TestDataSource dataSource = new TestDataSource();
+ communicator.setDataSource(dataSource);
+
+ Assert.assertFalse(dataSource.isListenerAdded());
+
+ communicator.extend(ui);
+
+ Assert.assertTrue(dataSource.isListenerAdded());
+ }
+
+ @Test
+ public void detach_dataSourceListenerIsRemovedAfterDetach() {
+ session.lock();
+
+ UI ui = new TestUI(session);
+
+ TestDataCommunicator communicator = new TestDataCommunicator();
+
+ TestDataSource dataSource = new TestDataSource();
+ communicator.setDataSource(dataSource);
+
+ communicator.extend(ui);
+
+ Assert.assertTrue(dataSource.isListenerAdded());
+
+ communicator.detach();
+
+ Assert.assertFalse(dataSource.isListenerAdded());
+ }
+
+}
import static org.junit.Assert.assertTrue;
import java.util.Comparator;
+import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
@Test
public void testListContainsAllData() {
+ List<StrBean> list = new LinkedList<>(data);
dataSource.apply(new Query())
.forEach(str -> assertTrue(
"Data source contained values not in original data",
- data.remove(str)));
+ list.remove(str)));
assertTrue("Not all values from original data were in data source",
- data.isEmpty());
+ list.isEmpty());
}
@Test
Assert.assertTrue(prev.getValue().compareTo(cur.getValue()) <= 0);
}
}
+
+ @Test
+ public void refreshAll_changeBeanInstance() {
+ StrBean bean = new StrBean("foo", -1, hashCode());
+ Query query = new Query();
+ int size = dataSource.size(query);
+
+ data.set(0, bean);
+ dataSource.refreshAll();
+
+ List<StrBean> list = dataSource.apply(query)
+ .collect(Collectors.toList());
+ StrBean first = list.get(0);
+ Assert.assertEquals(bean.getValue(), first.getValue());
+ Assert.assertEquals(bean.getRandomNumber(), first.getRandomNumber());
+ Assert.assertEquals(bean.getId(), first.getId());
+
+ Assert.assertEquals(size, dataSource.size(query));
+ }
+
+ @Test
+ public void refreshAll_updateBean() {
+ Query query = new Query();
+ int size = dataSource.size(query);
+
+ StrBean bean = data.get(0);
+ bean.setValue("foo");
+ dataSource.refreshAll();
+
+ List<StrBean> list = dataSource.apply(query)
+ .collect(Collectors.toList());
+ StrBean first = list.get(0);
+ Assert.assertEquals("foo", first.getValue());
+
+ Assert.assertEquals(size, dataSource.size(query));
+ }
+
+ @Test
+ public void refreshAll_sortingBy_changeBeanInstance() {
+ StrBean bean = new StrBean("foo", -1, hashCode());
+ Query query = new Query();
+ int size = dataSource.size(query);
+
+ data.set(0, bean);
+
+ ListDataSource<StrBean> dSource = dataSource
+ .sortingBy(Comparator.comparing(StrBean::getId));
+ dSource.refreshAll();
+
+ List<StrBean> list = dSource.apply(query).collect(Collectors.toList());
+ StrBean first = list.get(0);
+ Assert.assertEquals(bean.getValue(), first.getValue());
+ Assert.assertEquals(bean.getRandomNumber(), first.getRandomNumber());
+ Assert.assertEquals(bean.getId(), first.getId());
+
+ Assert.assertEquals(size, dataSource.size(query));
+ }
+
+ @Test
+ public void refreshAll_addBeanInstance() {
+ StrBean bean = new StrBean("foo", -1, hashCode());
+
+ Query query = new Query();
+ int size = dataSource.size(query);
+
+ data.add(0, bean);
+ dataSource.refreshAll();
+
+ List<StrBean> list = dataSource.apply(query)
+ .collect(Collectors.toList());
+ StrBean first = list.get(0);
+ Assert.assertEquals(bean.getValue(), first.getValue());
+ Assert.assertEquals(bean.getRandomNumber(), first.getRandomNumber());
+ Assert.assertEquals(bean.getId(), first.getId());
+
+ Assert.assertEquals(size + 1, dataSource.size(query));
+ }
+
+ @Test
+ public void refreshAll_removeBeanInstance() {
+ Query query = new Query();
+ int size = dataSource.size(query);
+
+ data.remove(0);
+ dataSource.refreshAll();
+
+ Assert.assertEquals(size - 1, dataSource.size(query));
+ }
}
return randomNumber;
}
+ public void setValue(String value) {
+ this.value = value;
+ }
+
public static List<StrBean> generateRandomBeans(int max) {
List<StrBean> data = new ArrayList<>();
Random r = new Random(13337);
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.LinkedList;
import java.util.List;
import java.util.stream.Stream;
public class AbstractListingTest {
- private final class TestListing extends
- AbstractSingleSelect<String> {
+ private final class TestListing extends AbstractSingleSelect<String> {
protected TestListing() {
setSelectionModel(new SimpleSingleSelection());
@Test
public void testSetItemsWithCollection() {
listing.setItems(items);
+ List<String> list = new LinkedList<>(items);
listing.getDataSource().apply(new Query()).forEach(
str -> Assert.assertTrue("Unexpected item in data source",
- items.remove(str)));
+ list.remove(str)));
Assert.assertTrue("Not all items from list were in data source",
- items.isEmpty());
+ list.isEmpty());
}
@Test
--- /dev/null
+/*
+ * Copyright 2000-2016 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.components.grid.basics;
+
+import java.util.List;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.server.data.DataSource;
+import com.vaadin.server.data.ListDataSource;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Grid;
+
+/**
+ * @author Vaadin Ltd
+ *
+ */
+public class RefreshDataSource extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ Grid<DataObject> grid = new Grid<>();
+ List<DataObject> data = DataObject.generateObjects();
+
+ ListDataSource<DataObject> dataSource = DataSource.create(data);
+ grid.setDataSource(dataSource);
+
+ grid.setDataSource(dataSource);
+ grid.addColumn("Coordinates", DataObject::getCoordinates);
+ addComponent(grid);
+
+ Button update = new Button("Update data",
+ event -> updateData(dataSource, data));
+ update.setId("update");
+ addComponent(update);
+
+ Button add = new Button("Add data", event -> addData(dataSource, data));
+ add.setId("add");
+ addComponent(add);
+
+ Button remove = new Button("Remove data",
+ event -> removeData(dataSource, data));
+ remove.setId("remove");
+ addComponent(remove);
+ }
+
+ private void updateData(DataSource<DataObject> dataSource,
+ List<DataObject> data) {
+ data.get(0).setCoordinates("Updated coordinates");
+ dataSource.refreshAll();
+ }
+
+ private void addData(DataSource<DataObject> dataSource,
+ List<DataObject> data) {
+ DataObject dataObject = new DataObject();
+ dataObject.setCoordinates("Added");
+ data.add(0, dataObject);
+ dataSource.refreshAll();
+ }
+
+ private void removeData(DataSource<DataObject> dataSource,
+ List<DataObject> data) {
+ data.remove(0);
+ dataSource.refreshAll();
+ }
+}
--- /dev/null
+/*
+ * Copyright 2000-2016 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.components.grid.basics;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+/**
+ * @author Vaadin Ltd
+ *
+ */
+public class RefreshDataSourceTest extends MultiBrowserTest {
+
+ @Test
+ public void updateFirstRow() {
+ openTestURL();
+
+ findElement(By.id("update")).click();
+ WebElement first = findElement(By.tagName("td"));
+ Assert.assertEquals(
+ "UI component is not refreshed after update in data",
+ "Updated coordinates", first.getText());
+ }
+
+ @Test
+ public void addFirstRow() {
+ openTestURL();
+
+ findElement(By.id("add")).click();
+ WebElement first = findElement(By.tagName("td"));
+
+ Assert.assertEquals("UI component is not refreshed after add new data",
+ "Added", first.getText());
+ }
+
+ @Test
+ public void removeFirstRow() {
+ openTestURL();
+
+ WebElement first = findElement(By.tagName("td"));
+ String old = first.getText();
+ first = findElement(By.id("remove"));
+ Assert.assertNotEquals("UI component is not refreshed after removal",
+ old, first.getText());
+ }
+
+}
\ No newline at end of file