diff options
author | caalador <mikael.grankvist@gmail.com> | 2017-02-02 14:01:19 +0200 |
---|---|---|
committer | Pekka Hyvönen <pekka@vaadin.com> | 2017-02-02 14:01:19 +0200 |
commit | 59157179b6e5099952f827485e16b67a88831ebf (patch) | |
tree | bef348f7ffb73103e25f69368a8294e48d16ae19 /uitest | |
parent | be2c4684cd43547c2785831a8b0aeda63ccbaebe (diff) | |
download | vaadin-framework-59157179b6e5099952f827485e16b67a88831ebf.tar.gz vaadin-framework-59157179b6e5099952f827485e16b67a88831ebf.zip |
Add scrollTo methods to Grid (#8203) (#8410)
* Add scroll methods to serverside grid (#8203)
Added scrollToTop(), scrollToEnd() and scrollTo(int row)
* Fix scrolling to view of opened details (#8203)
Removed dependency for DetailsManagerConnector from GridConnector.
GridConnector now handles one off listeners.
* Rename detailsRefresh to better show that it's one-off.
Add missing copyright header.
Diffstat (limited to 'uitest')
5 files changed, 763 insertions, 0 deletions
diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/GridDetailsLocation.java b/uitest/src/main/java/com/vaadin/tests/components/grid/GridDetailsLocation.java new file mode 100644 index 0000000000..74e8f44b1f --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/grid/GridDetailsLocation.java @@ -0,0 +1,73 @@ +package com.vaadin.tests.components.grid; + +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.tests.util.Person; +import com.vaadin.tests.util.PersonContainer; +import com.vaadin.ui.Button; +import com.vaadin.ui.CheckBox; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Label; +import com.vaadin.ui.TextField; + +public class GridDetailsLocation extends AbstractTestUI { + + private TextField numberTextField; + private Grid<Person> grid; + private List<Person> testData; + + @Override + protected void setup(VaadinRequest request) { + + grid = new Grid<>(); + testData = new ArrayList<>(PersonContainer.createTestData(1000)); + grid.setItems(testData); + grid.addColumn(item -> item.getFirstName()).setCaption("First Name"); + grid.addColumn(item -> item.getLastName()).setCaption("Last Name"); + + grid.setSelectionMode(Grid.SelectionMode.NONE); + addComponent(grid); + + final CheckBox checkbox = new CheckBox("Details generator"); + checkbox.addValueChangeListener(event -> { + if (checkbox.getValue()) { + grid.setDetailsGenerator(person -> { + Label label = new Label( + person.getFirstName() + " " + person.getLastName()); + // currently the decorator row doesn't change its height + // when the content height is different. + label.setHeight("30px"); + return label; + }); + } else { + grid.setDetailsGenerator(null); + } + }); + addComponent(checkbox); + + numberTextField = new TextField("Row"); + addComponent(numberTextField); + + addComponent(new Button("Toggle and scroll", clickEvent -> { + toggle(); + scrollTo(); + })); + addComponent(new Button("Scroll and toggle", clickEvent -> { + scrollTo(); + toggle(); + })); + } + + private void toggle() { + Person itemId = testData.get(Integer.parseInt(numberTextField.getValue())); + boolean isVisible = grid.isDetailsVisible(itemId); + grid.setDetailsVisible(itemId, !isVisible); + } + + private void scrollTo() { + grid.scrollTo(Integer.parseInt(numberTextField.getValue())); + } +} diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/GridFastAsyncUpdate.java b/uitest/src/main/java/com/vaadin/tests/components/grid/GridFastAsyncUpdate.java new file mode 100644 index 0000000000..3f4391d504 --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/grid/GridFastAsyncUpdate.java @@ -0,0 +1,169 @@ +package com.vaadin.tests.components.grid; + +import java.util.Calendar; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; + +import com.vaadin.annotations.Push; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.Button; +import com.vaadin.ui.Grid; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.VerticalLayout; + +@Push +public class GridFastAsyncUpdate extends AbstractTestUI { + + private final Runnable addRowsTask = new Runnable() { + @Override + public void run() { + System.out.println("Logging..."); + try { + Random random = new Random(); + while (!Thread.currentThread().isInterrupted()) { + Thread.sleep(random.nextInt(100)); + + GridFastAsyncUpdate.this.access(() -> { + ++counter; + Item item = new Item(counter, + (Calendar.getInstance().getTimeInMillis() + - loggingStart), + Level.INFO.toString(), "Message"); + items.add(item); + grid.setItems(items); + + if (grid != null && !scrollLock) { + grid.scrollToEnd(); + } + }); + } + } catch (InterruptedException e) { + System.out.println("logging thread interrupted"); + } + } + }; + + private int counter; + private List<Item> items = new LinkedList<>(); + + private Grid<Item> grid; + private long loggingStart; + private volatile boolean scrollLock = false; + + @Override + protected void setup(VaadinRequest request) { + final VerticalLayout layout = new VerticalLayout(); + layout.setSizeFull(); + layout.setMargin(true); + addComponent(layout); + + HorizontalLayout buttons = new HorizontalLayout(); + layout.addComponent(buttons); + + final ExecutorService logExecutor = Executors.newSingleThreadExecutor(); + + final Button logButton = new Button("Start logging"); + logButton.addClickListener(clickEvent -> { + if ("Start logging".equals(logButton.getCaption())) { + loggingStart = Calendar.getInstance().getTimeInMillis(); + logExecutor.submit(addRowsTask); + logButton.setCaption("Stop logging"); + } else { + System.out.println("Stop logging..."); + try { + logExecutor.shutdownNow(); + } catch (Exception e) { + e.printStackTrace(); + } + logButton.setCaption("Start logging"); + } + }); + buttons.addComponent(logButton); + + final Button scrollButton = new Button("Stop scrolling"); + scrollButton.addClickListener(clickEvent -> { + if (!scrollLock) { + System.out.println("Stop scrolling"); + scrollButton.setCaption("Start scrolling"); + scrollLock = true; + } else { + System.out.println("Start scrolling"); + scrollButton.setCaption("Stop scrolling"); + scrollLock = false; + } + }); + buttons.addComponent(scrollButton); + + grid = new Grid<>(); + grid.addColumn(Item::getSequenceNumber).setCaption(""); + grid.addColumn(Item::getMillis).setCaption(""); + grid.addColumn(Item::getLevel).setCaption(""); + grid.addColumn(Item::getMessage).setCaption(""); + + grid.setWidth("100%"); + grid.setSelectionMode(Grid.SelectionMode.SINGLE); + grid.addSelectionListener(selectionEvent -> { + if (!selectionEvent.getAllSelectedItems().isEmpty()) { + disableScroll(); + } + }); + + layout.addComponent(grid); + layout.setExpandRatio(grid, 1.0f); + } + + protected void disableScroll() { + scrollLock = true; + } + + protected class Item { + Integer sequenceNumber; + Long millis; + String level, message; + + public Item(Integer sequanceNumber, Long millis, String level, + String message) { + this.sequenceNumber = sequanceNumber; + this.millis = millis; + this.level = level; + this.message = message; + } + + public Integer getSequenceNumber() { + return sequenceNumber; + } + + public void setSequenceNumber(Integer sequenceNumber) { + this.sequenceNumber = sequenceNumber; + } + + public Long getMillis() { + return millis; + } + + public void setMillis(Long millis) { + this.millis = millis; + } + + public String getLevel() { + return level; + } + + public void setLevel(String level) { + this.level = level; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } +} diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/GridScrollTo.java b/uitest/src/main/java/com/vaadin/tests/components/grid/GridScrollTo.java new file mode 100644 index 0000000000..9f83436a11 --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/grid/GridScrollTo.java @@ -0,0 +1,67 @@ +package com.vaadin.tests.components.grid; + +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.data.ValueProvider; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.Button; +import com.vaadin.ui.Grid; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.TextField; + +public class GridScrollTo extends AbstractTestUI { + + @Override + protected void setup(VaadinRequest request) { + List<String> data = new ArrayList<>(); + for (int i = 0; i < 200; i++) { + data.add("Name " + i); + } + + Grid<String> grid = new Grid<>(); + grid.setItems(data); + + grid.setSelectionMode(Grid.SelectionMode.NONE); + + grid.addColumn(ValueProvider.identity()).setId("Name") + .setCaption("Name"); + + grid.setDetailsGenerator(item -> { + final HorizontalLayout detailsLayout = new HorizontalLayout(); + detailsLayout.setWidth(100, Unit.PERCENTAGE); + detailsLayout.setHeightUndefined(); + + final Label lbl1 = new Label(item + " details"); + detailsLayout.addComponent(lbl1); + return detailsLayout; + }); + + grid.addItemClickListener(event -> { + final String itemId = event.getItem(); + grid.setDetailsVisible(itemId, !grid.isDetailsVisible(itemId)); + }); + + Button scrollToTop = new Button("Scroll to top", + clickEvent -> grid.scrollToStart()); + scrollToTop.setId("top"); + + Button scrollToEnd = new Button("Scroll to end", + clickEvent -> grid.scrollToEnd()); + scrollToEnd.setId("end"); + + TextField rowField = new TextField("Target row"); + rowField.setId("row-field"); + + Button scrollToRow = new Button("Scroll to row", clickEvent -> grid + .scrollTo(Integer.parseInt(rowField.getValue()))); + scrollToRow.setId("row"); + + addComponent(grid); + + addComponent(new HorizontalLayout(scrollToTop, scrollToEnd, rowField, + scrollToRow)); + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/GridDetailsLocationTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/GridDetailsLocationTest.java new file mode 100644 index 0000000000..e54fe8b0d3 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/GridDetailsLocationTest.java @@ -0,0 +1,303 @@ +package com.vaadin.tests.components.grid; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.StaleElementReferenceException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedCondition; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.testbench.elements.CheckBoxElement; +import com.vaadin.testbench.elements.GridElement; +import com.vaadin.testbench.elements.TextFieldElement; +import com.vaadin.testbench.parallel.TestCategory; +import com.vaadin.tests.tb3.MultiBrowserTest; + +@TestCategory("grid") +public class GridDetailsLocationTest extends MultiBrowserTest { + + private static final int detailsDefaultHeight = 51; + private static final int detailsDefinedHeight = 33; + + private static class Param { + private final int rowIndex; + private final boolean useGenerator; + private final boolean scrollFirstToBottom; + + public Param(int rowIndex, boolean useGenerator, + boolean scrollFirstToBottom) { + this.rowIndex = rowIndex; + this.useGenerator = useGenerator; + this.scrollFirstToBottom = scrollFirstToBottom; + } + + public int getRowIndex() { + return rowIndex; + } + + public boolean useGenerator() { + return useGenerator; + } + + public boolean scrollFirstToBottom() { + return scrollFirstToBottom; + } + + @Override + public String toString() { + return "Param [rowIndex=" + getRowIndex() + ", useGenerator=" + + useGenerator() + ", scrollFirstToBottom=" + + scrollFirstToBottom() + "]"; + } + + } + + public static Collection<Param> parameters() { + List<Param> data = new ArrayList<>(); + + int[] params = new int[] { 0, 500, 999 }; + + for (int rowIndex : params) { + + data.add(new Param(rowIndex, true, false)); + data.add(new Param(rowIndex, true, true)); + } + + return data; + } + + @Before + public void setUp() { + setDebug(true); + } + + @Test + public void toggleAndScroll() throws Throwable { + for (Param param : parameters()) { + try { + openTestURL(); + useGenerator(param.useGenerator()); + scrollToBottom(param.scrollFirstToBottom()); + + // the tested method + toggleAndScroll(param.getRowIndex()); + + verifyLocation(param); + } catch (Throwable t) { + throw new Throwable("" + param, t); + } + } + } + + @Test + public void scrollAndToggle() throws Throwable { + for (Param param : parameters()) { + try { + openTestURL(); + useGenerator(param.useGenerator()); + scrollToBottom(param.scrollFirstToBottom()); + + // the tested method + scrollAndToggle(param.getRowIndex()); + + verifyLocation(param); + + } catch (Throwable t) { + throw new Throwable("" + param, t); + } + } + } + + @Test + public void testDetailsHeightWithGenerator() { + openTestURL(); + useGenerator(true); + toggleAndScroll(5); + + verifyDetailsRowHeight(5, detailsDefinedHeight, 0); + verifyDetailsDecoratorLocation(5, 0, 0); + + toggleAndScroll(0); + + verifyDetailsRowHeight(0, detailsDefinedHeight, 0); + // decorator elements are in DOM in the order they have been added + verifyDetailsDecoratorLocation(0, 0, 1); + + verifyDetailsRowHeight(5, detailsDefinedHeight, 1); + verifyDetailsDecoratorLocation(5, 1, 0); + } + + private void verifyDetailsRowHeight(int rowIndex, int expectedHeight, + int visibleIndexOfSpacer) { + waitForDetailsVisible(); + WebElement details = getDetailsElement(visibleIndexOfSpacer); + Assert.assertEquals("Wrong details row height", expectedHeight, + details.getSize().getHeight()); + } + + private void verifyDetailsDecoratorLocation(int row, + int visibleIndexOfSpacer, int visibleIndexOfDeco) { + WebElement detailsElement = getDetailsElement(visibleIndexOfSpacer); + WebElement detailsDecoElement = getDetailsDecoElement( + visibleIndexOfDeco); + GridElement.GridRowElement rowElement = getGrid().getRow(row); + + Assert.assertEquals( + "Details deco top position does not match row top pos", + rowElement.getLocation().getY(), + detailsDecoElement.getLocation().getY()); + Assert.assertEquals( + "Details deco bottom position does not match details bottom pos", + detailsElement.getLocation().getY() + + detailsElement.getSize().getHeight(), + detailsDecoElement.getLocation().getY() + + detailsDecoElement.getSize().getHeight()); + } + + private void verifyLocation(Param param) { + Assert.assertFalse("Notification was present", + isElementPresent(By.className("v-Notification"))); + + TestBenchElement headerRow = getGrid().getHeaderRow(0); + final int topBoundary = headerRow.getLocation().getX() + + headerRow.getSize().height; + final int bottomBoundary = getGrid().getLocation().getX() + + getGrid().getSize().getHeight() + - getHorizontalScrollbar().getSize().height; + + GridElement.GridRowElement row = getGrid().getRow(param.getRowIndex()); + final int rowTop = row.getLocation().getX(); + + waitForDetailsVisible(); + WebElement details = getDetailsElement(); + final int detailsBottom = details.getLocation().getX() + + details.getSize().getHeight(); + + assertGreaterOrEqual("Row top should be inside grid, gridTop:" + + topBoundary + " rowTop" + rowTop, topBoundary, rowTop); + assertLessThanOrEqual( + "Decorator bottom should be inside grid, gridBottom:" + + bottomBoundary + " decoratorBotton:" + detailsBottom, + detailsBottom, bottomBoundary); + + verifyDetailsRowHeight(param.getRowIndex(), param.useGenerator() + ? detailsDefinedHeight : detailsDefaultHeight, 0); + verifyDetailsDecoratorLocation(param.getRowIndex(), 0, 0); + + Assert.assertFalse("Notification was present", + isElementPresent(By.className("v-Notification"))); + } + + private final By locator = By.className("v-grid-spacer"); + + private WebElement getDetailsElement() { + return getDetailsElement(0); + } + + private WebElement getDetailsElement(int index) { + return findElements(locator).get(index); + } + + private WebElement getDetailsDecoElement(int index) { + return findElements(By.className("v-grid-spacer-deco")).get(index); + } + + private void waitForDetailsVisible() { + waitUntil(new ExpectedCondition<WebElement>() { + + @Override + public WebElement apply(WebDriver driver) { + try { + WebElement detailsElement = getDetailsElement(); + return detailsElement.isDisplayed() + && detailsElement.getSize().getHeight() > 3 + ? detailsElement : null; + } catch (StaleElementReferenceException e) { + return null; + } + } + + @Override + public String toString() { + return "visibility of element located by " + locator; + } + + }, 5); + waitForElementVisible(By.className("v-grid-spacer")); + } + + private void scrollToBottom(boolean scrollFirstToBottom) { + if (scrollFirstToBottom) { + executeScript("arguments[0].scrollTop = 9999999", + getVerticalScrollbar()); + } + } + + private void useGenerator(boolean use) { + CheckBoxElement checkBox = $(CheckBoxElement.class).first(); + boolean isChecked = isCheckedValo(checkBox); + if (use != isChecked) { + clickValo(checkBox); + } + } + + @SuppressWarnings("boxing") + private boolean isCheckedValo(CheckBoxElement checkBoxElement) { + WebElement checkbox = checkBoxElement.findElement(By.tagName("input")); + Object value = executeScript("return arguments[0].checked;", checkbox); + return (Boolean) value; + } + + private void clickValo(CheckBoxElement checkBoxElement) { + checkBoxElement.click(5, 5); + } + + private void scrollAndToggle(int row) { + setRow(row); + getScrollAndToggle().click(); + } + + private void toggleAndScroll(int row) { + setRow(row); + getToggleAndScroll().click(); + } + + private ButtonElement getScrollAndToggle() { + return $(ButtonElement.class).caption("Scroll and toggle").first(); + } + + private ButtonElement getToggleAndScroll() { + return $(ButtonElement.class).caption("Toggle and scroll").first(); + } + + private void setRow(int row) { + $(TextFieldElement.class).first().clear(); + $(TextFieldElement.class).first().sendKeys(String.valueOf(row), + Keys.ENTER, Keys.TAB); + } + + private GridElement getGrid() { + return $(GridElement.class).first(); + } + + private WebElement getVerticalScrollbar() { + WebElement scrollBar = getGrid() + .findElement(By.className("v-grid-scroller-vertical")); + return scrollBar; + } + + private WebElement getHorizontalScrollbar() { + WebElement scrollBar = getGrid() + .findElement(By.className("v-grid-scroller-horizontal")); + return scrollBar; + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrollToTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrollToTest.java new file mode 100644 index 0000000000..8bdb246d81 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrollToTest.java @@ -0,0 +1,151 @@ +package com.vaadin.tests.components.grid; + +import java.util.Optional; + +import org.junit.Assert; +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.testbench.elements.TextFieldElement; +import com.vaadin.testbench.parallel.TestCategory; +import com.vaadin.tests.tb3.SingleBrowserTest; +import com.vaadin.ui.Button; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@TestCategory("grid") +public class GridScrollToTest extends SingleBrowserTest { + + @Test + public void testScrollToEnd() { + openTestURL(); + String finalCellText = "199"; + + assertEquals("Found final element even though should be at top of list", + 0, cellsContaining(finalCellText)); + + $(ButtonElement.class).id("end").click(); + + assertEquals("Could not find final element", 1, + cellsContaining(finalCellText)); + } + + @Test + public void testScrollToRow() { + openTestURL(); + String row = "50"; + + assertEquals("Found row element even though should be at top of list", + 0, cellsContaining(row)); + + $(TextFieldElement.class).id("row-field").setValue(row); + + $(ButtonElement.class).id("row").click(); + + assertEquals("Could not find row element", 1, cellsContaining(row)); + } + + @Test + public void testScrollTop() { + openTestURL(); + String row = "Name 0"; + + assertEquals("Couldn't find first element", 1, cellsContaining(row)); + + getGrid().getVerticalScroller().scroll(800); + + assertEquals( + "Found first element even though we should have scrolled down", + 0, cellsContaining(row)); + + $(ButtonElement.class).id("top").click(); + + assertEquals("Couldn't find first element", 1, cellsContaining(row)); + } + + @Test + public void scrollToLastWithDetailsShowDetails() { + openTestURL(); + + // Scroll to end + $(ButtonElement.class).id("end").click(); + + // Open details + clickCellContaining("199"); + + waitForElementPresent(By.className("v-grid-spacer")); + waitForElementPresent(By.className("v-label")); + assertTrue("Details not visible", getGrid().findElements(By.className("v-label")).stream() + .filter(element -> element.getText().contains("Name 199 details")).findFirst().get().isDisplayed()); + // scroll away + $(ButtonElement.class).id("top").click(); + + assertEquals("Found final element even though should be at top of list", + 0, cellsContaining("199")); + + assertFalse("Found final element details even though should be at top of list", getGrid().findElements(By.className("v-label")).stream() + .filter(element -> element.getText().contains("Name 199 details")).findFirst().isPresent()); + + // Scroll to end + $(ButtonElement.class).id("end").click(); + + assertTrue("Details not visible", getGrid().findElements(By.className("v-label")).stream() + .filter(element -> element.getText().contains("Name 199 details")).findFirst().get().isDisplayed()); + } + + @Test + public void scrollToRowShowsDetails() { + openTestURL(); + + // Scroll to 50 + $(TextFieldElement.class).id("row-field").setValue("50"); + + $(ButtonElement.class).id("row").click(); + + clickCellContaining("50"); + + waitForElementPresent(By.className("v-grid-spacer")); + waitForElementPresent(By.className("v-label")); + assertTrue("Details not visible", getGrid().findElements(By.className("v-label")).stream() + .filter(element -> element.getText().contains("Name 50 details")).findFirst().get().isDisplayed()); + // scroll away + $(ButtonElement.class).id("top").click(); + + assertEquals("Found final element even though should be at top of list", + 0, cellsContaining("50")); + + assertFalse("Found final element details even though should be at top of list", getGrid().findElements(By.className("v-label")).stream() + .filter(element -> element.getText().contains("Name 50 details")).findFirst().isPresent()); + + // Scroll to end + $(ButtonElement.class).id("row").click(); + + assertTrue("Details not visible", getGrid().findElements(By.className("v-label")).stream() + .filter(element -> element.getText().contains("Name 50 details")).findFirst().get().isDisplayed()); + } + + private GridElement getGrid() { + return $(GridElement.class).first(); + } + + private long cellsContaining(String text) { + return getGrid().findElements(By.className("v-grid-cell")).stream() + .filter(element -> element.getText().contains(text)).count(); + } + + private void clickCellContaining(String text) { + Optional<WebElement> first = getGrid() + .findElements(By.className("v-grid-cell")).stream() + .filter(element -> element.getText().contains(text)) + .findFirst(); + if (first.isPresent()) + first.get().click(); + else + Assert.fail("Cell not present"); + } +} |