/* * 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.v7.tests.components.grid.basicfeatures.escalator; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.Before; import org.junit.ComparisonFailure; import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.Keys; import org.openqa.selenium.WebElement; import com.vaadin.client.WidgetUtil; import com.vaadin.shared.Range; import com.vaadin.testbench.TestBenchElement; import com.vaadin.testbench.elements.NotificationElement; import com.vaadin.testbench.parallel.BrowserUtil; import com.vaadin.v7.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest; @SuppressWarnings("boxing") public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { //@formatter:off // separate strings made so that eclipse can show the concatenated string by hovering the mouse over the constant // translate3d(0px, 40px, 123px); // translate3d(24px, 15.251px, 0); // translate(0, 40px); private final static String TRANSLATE_VALUE_REGEX = "translate(?:3d|)" // "translate" or "translate3d" + "\\(" // literal "(" + "(" // start capturing the x argument + "[0-9]+" // the integer part of the value + "(?:" // start of the subpixel part of the value + "\\.[0-9]" // if we have a period, there must be at least one number after it + "[0-9]*" // any amount of accuracy afterwards is fine + ")?" // the subpixel part is optional + ")" + "(?:px)?" // we don't care if the values are suffixed by "px" or not. + ", " + "(" // start capturing the y argument + "[0-9]+" // the integer part of the value + "(?:" // start of the subpixel part of the value + "\\.[0-9]" // if we have a period, there must be at least one number after it + "[0-9]*" // any amount of accuracy afterwards is fine + ")?" // the subpixel part is optional + ")" + "(?:px)?" // we don't care if the values are suffixed by "px" or not. + "(?:, .*?)?" // the possible z argument, uninteresting (translate doesn't have one, translate3d does) + "\\)" // literal ")" + ";?"; // optional ending semicolon // 40px; // 12.34px private final static String PIXEL_VALUE_REGEX = "(" // capture the pixel value + "[0-9]+" // the pixel argument + "(?:" // start of the subpixel part of the value + "\\.[0-9]" // if we have a period, there must be at least one number after it + "[0-9]*" // any amount of accuracy afterwards is fine + ")?" // the subpixel part is optional + ")" + "(?:px)?" // optional "px" string + ";?"; // optional semicolon //@formatter:on // also matches "-webkit-transform"; private final static Pattern TRANSFORM_CSS_PATTERN = Pattern .compile("transform: (.*?);"); private final static Pattern TOP_CSS_PATTERN = Pattern.compile( "top: ([0-9]+(?:\\.[0-9]+)?(?:px)?);?", Pattern.CASE_INSENSITIVE); private final static Pattern LEFT_CSS_PATTERN = Pattern.compile( "left: ([0-9]+(?:\\.[0-9]+)?(?:px)?);?", Pattern.CASE_INSENSITIVE); private final static Pattern TRANSLATE_VALUE_PATTERN = Pattern .compile(TRANSLATE_VALUE_REGEX); private final static Pattern PIXEL_VALUE_PATTERN = Pattern .compile(PIXEL_VALUE_REGEX, Pattern.CASE_INSENSITIVE); @Before public void before() { setDebug(true); openTestURL(); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, "Set 20px default height"); populate(); } @Test public void openVisibleSpacer() { assertFalse("No spacers should be shown at the start", spacersAreFoundInDom()); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); assertNotNull("Spacer should be shown after setting it", getSpacer(1)); } @Test public void closeVisibleSpacer() { selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); selectMenuPath(FEATURES, SPACERS, ROW_1, REMOVE); assertNull("Spacer should not exist after removing it", getSpacer(1)); } @Test public void spacerPushesVisibleRowsDown() { double oldTop = getElementTop(getBodyRow(2)); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); double newTop = getElementTop(getBodyRow(2)); assertGreater("Row below a spacer was not pushed down", newTop, oldTop); } @Test public void addingRowAboveSpacerPushesItDown() { selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_ROWS); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); double oldTop = getElementTop(getSpacer(1)); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); double newTop = getElementTop(getSpacer(2)); assertGreater("Spacer should've been pushed down (oldTop: " + oldTop + ", newTop: " + newTop + ")", newTop, oldTop); } @Test public void addingRowBelowSpacerDoesNotPushItDown() { selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_ROWS); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); double oldTop = getElementTop(getSpacer(1)); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_END); double newTop = getElementTop(getSpacer(1)); assertEquals("Spacer should've not been pushed down", newTop, oldTop, WidgetUtil.PIXEL_EPSILON); } @Test public void addingRowBelowSpacerIsActuallyRenderedBelowWhenEscalatorIsEmpty() { selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_ROWS); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); double spacerTop = getElementTop(getSpacer(1)); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_END); double rowTop = getElementTop(getBodyRow(2)); assertEquals("Next row should've been rendered below the spacer", spacerTop + 100, rowTop, WidgetUtil.PIXEL_EPSILON); } @Test public void addSpacerAtBottomThenScrollThere() { selectMenuPath(FEATURES, SPACERS, ROW_99, SET_100PX); scrollVerticallyTo(999999); assertFalse("Did not expect a notification", $(NotificationElement.class).exists()); } @Test public void scrollToBottomThenAddSpacerThere() { scrollVerticallyTo(999999); long oldBottomScrollTop = getScrollTop(); selectMenuPath(FEATURES, SPACERS, ROW_99, SET_100PX); assertEquals( "Adding a spacer underneath the current viewport should " + "not scroll anywhere", oldBottomScrollTop, getScrollTop()); assertFalse("Got an unexpected notification", $(NotificationElement.class).exists()); scrollVerticallyTo(999999); assertFalse("Got an unexpected notification", $(NotificationElement.class).exists()); assertGreater("Adding a spacer should've made the scrollbar scroll " + "further", getScrollTop(), oldBottomScrollTop); } @Test public void removingRowAboveSpacerMovesSpacerUp() { selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); WebElement spacer = getSpacer(1); double originalElementTop = getElementTop(spacer); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ONE_ROW_FROM_BEGINNING); assertLessThan("spacer should've moved up", getElementTop(spacer), originalElementTop); assertNull("No spacer for row 1 should be found after removing the " + "top row", getSpacer(1)); } @Test public void removingSpacedRowRemovesSpacer() { selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); assertTrue("Spacer should've been found in the DOM", spacersAreFoundInDom()); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ONE_ROW_FROM_BEGINNING); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ONE_ROW_FROM_BEGINNING); assertFalse("No spacers should be in the DOM after removing " + "associated spacer", spacersAreFoundInDom()); } @Test public void spacersAreFixedInViewport_firstFreezeThenScroll() { selectMenuPath(FEATURES, FROZEN_COLUMNS, FREEZE_1_COLUMN); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); assertEquals( "Spacer's left position should've been 0 at the " + "beginning", 0d, getElementLeft(getSpacer(1)), WidgetUtil.PIXEL_EPSILON); int scrollTo = 10; scrollHorizontallyTo(scrollTo); assertEquals( "Spacer's left position should've been " + scrollTo + " after scrolling " + scrollTo + "px", scrollTo, getElementLeft(getSpacer(1)), WidgetUtil.PIXEL_EPSILON); } @Test public void spacersAreFixedInViewport_firstScrollThenFreeze() { selectMenuPath(FEATURES, FROZEN_COLUMNS, FREEZE_1_COLUMN); int scrollTo = 10; scrollHorizontallyTo(scrollTo); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); assertEquals( "Spacer's left position should've been " + scrollTo + " after scrolling " + scrollTo + "px", scrollTo, getElementLeft(getSpacer(1)), WidgetUtil.PIXEL_EPSILON); } @Test public void addingMinusOneSpacerDoesNotScrollWhenScrolledAtTop() { scrollVerticallyTo(5); selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, SET_100PX); assertEquals( "No scroll adjustment should've happened when adding the -1 spacer", 5, getScrollTop()); } @Test public void removingMinusOneSpacerScrolls() { scrollVerticallyTo(5); selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, SET_100PX); selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, REMOVE); assertEquals("Scroll adjustment should've happened when removing the " + "-1 spacer", 0, getScrollTop()); } @Test public void scrollToRowWorksProperlyWithSpacers() throws Exception { selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, SET_100PX); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); /* * we check for row -2 instead of -1, because escalator has the one row * buffered underneath the footer */ selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_75); Thread.sleep(500); assertEquals("Row 75: 0,75", getBodyCell(-2, 0).getText()); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_25); Thread.sleep(500); try { assertEquals("Row 25: 0,25", getBodyCell(0, 0).getText()); } catch (ComparisonFailure retryForIE10andIE11) { /* * This seems to be some kind of subpixel/off-by-one-pixel error. * Everything's scrolled correctly, but Escalator still loads one * row above to the DOM, underneath the header. It's there, but it's * not visible. We'll allow for that one pixel error. */ assertEquals("Row 24: 0,24", getBodyCell(0, 0).getText()); } } @Test public void scrollToSpacerFromAbove() throws Exception { selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX); selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING); // Browsers might vary with a few pixels. Range allowableScrollRange = Range.between(765, 780); int scrollTop = (int) getScrollTop(); assertTrue("Scroll position was not " + allowableScrollRange + ", but " + scrollTop, allowableScrollRange.contains(scrollTop)); } @Test public void scrollToSpacerFromBelow() throws Exception { selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX); scrollVerticallyTo(999999); selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING); // Browsers might vary with a few pixels. Range allowableScrollRange = Range.between(1015, 1025); int scrollTop = (int) getScrollTop(); assertTrue("Scroll position was not " + allowableScrollRange + ", but " + scrollTop, allowableScrollRange.contains(scrollTop)); } @Test public void scrollToSpacerAlreadyInViewport() throws Exception { selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX); scrollVerticallyTo(1000); selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING); assertEquals(getScrollTop(), 1000); } @Test public void scrollToRowAndSpacerFromAbove() throws Exception { selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX); selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_SPACERBELOW_ANY_0PADDING); // Browsers might vary with a few pixels. Range allowableScrollRange = Range.between(765, 780); int scrollTop = (int) getScrollTop(); assertTrue("Scroll position was not " + allowableScrollRange + ", but " + scrollTop, allowableScrollRange.contains(scrollTop)); } @Test public void scrollToRowAndSpacerFromBelow() throws Exception { selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX); scrollVerticallyTo(999999); selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_SPACERBELOW_ANY_0PADDING); // Browsers might vary with a few pixels. Range allowableScrollRange = Range.between(995, 1005); int scrollTop = (int) getScrollTop(); assertTrue("Scroll position was not " + allowableScrollRange + ", but " + scrollTop, allowableScrollRange.contains(scrollTop)); } @Test public void scrollToRowAndSpacerAlreadyInViewport() throws Exception { selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX); scrollVerticallyTo(950); selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_SPACERBELOW_ANY_0PADDING); assertEquals(getScrollTop(), 950); } @Test public void domCanBeSortedWithFocusInSpacer() throws InterruptedException { // Firefox behaves badly with focus-related tests - skip it. if (BrowserUtil.isFirefox(super.getDesiredCapabilities())) { return; } selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); WebElement inputElement = getEscalator() .findElement(By.tagName("input")); inputElement.click(); scrollVerticallyTo(30); // Sleep needed because of all the JS we're doing, and to let // the DOM reordering to take place. Thread.sleep(500); assertFalse("Error message detected", $(NotificationElement.class).exists()); } @Test public void spacersAreInsertedInCorrectDomPosition() { selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); WebElement tbody = getEscalator().findElement(By.tagName("tbody")); WebElement spacer = getChild(tbody, 2); String cssClass = spacer.getAttribute("class"); assertTrue( "element index 2 was not a spacer (class=\"" + cssClass + "\")", cssClass.contains("-spacer")); } @Test public void spacersAreInCorrectDomPositionAfterScroll() { selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); scrollVerticallyTo(32); // roughly one row's worth WebElement tbody = getEscalator().findElement(By.tagName("tbody")); WebElement spacer = getChild(tbody, 1); String cssClass = spacer.getAttribute("class"); assertTrue( "element index 1 was not a spacer (class=\"" + cssClass + "\")", cssClass.contains("-spacer")); } @Test public void spacerScrolledIntoViewGetsFocus() { selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER); selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX); selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING); tryToTabIntoFocusUpdaterElement(); assertEquals("input", getFocusedElement().getTagName()); } @Test public void spacerScrolledOutOfViewDoesNotGetFocus() { selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING); tryToTabIntoFocusUpdaterElement(); assertNotEquals("input", getFocusedElement().getTagName()); } @Test public void spacerOpenedInViewGetsFocus() { selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); tryToTabIntoFocusUpdaterElement(); WebElement focusedElement = getFocusedElement(); assertEquals("input", focusedElement.getTagName()); } @Test public void spacerOpenedOutOfViewDoesNotGetFocus() { selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER); selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX); tryToTabIntoFocusUpdaterElement(); assertNotEquals("input", getFocusedElement().getTagName()); } @Test public void spacerOpenedInViewAndScrolledOutAndBackAgainGetsFocus() { selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER); selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_50); selectMenuPath(FEATURES, SPACERS, ROW_1, SCROLL_HERE_ANY_0PADDING); tryToTabIntoFocusUpdaterElement(); assertEquals("input", getFocusedElement().getTagName()); } @Test public void spacerOpenedOutOfViewAndScrolledInAndBackAgainDoesNotGetFocus() { selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER); selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX); selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING); selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_0); tryToTabIntoFocusUpdaterElement(); assertNotEquals("input", getFocusedElement().getTagName()); } private void tryToTabIntoFocusUpdaterElement() { ((TestBenchElement) findElement(By.className("gwt-MenuBar"))).focus(); WebElement focusedElement = getFocusedElement(); focusedElement.sendKeys(Keys.TAB); } private WebElement getChild(WebElement parent, int childIndex) { return (WebElement) executeScript( "return arguments[0].children[" + childIndex + "];", parent); } private static double[] getElementDimensions(WebElement element) { /* * we need to parse the style attribute, since using getCssValue gets a * normalized value that is harder to parse. */ String style = element.getAttribute("style"); String transform = getTransformFromStyle(style); if (transform != null) { return getTranslateValues(transform); } double[] result = new double[] { -1, -1 }; String left = getLeftFromStyle(style); if (left != null) { result[0] = getPixelValue(left); } String top = getTopFromStyle(style); if (top != null) { result[1] = getPixelValue(top); } if (result[0] != -1 && result[1] != -1) { return result; } else { throw new IllegalArgumentException("Could not parse the position " + "information from the CSS \"" + style + "\""); } } private static double getElementTop(WebElement element) { return getElementDimensions(element)[1]; } private static double getElementLeft(WebElement element) { return getElementDimensions(element)[0]; } private static String getTransformFromStyle(String style) { return getFromStyle(TRANSFORM_CSS_PATTERN, style); } private static String getTopFromStyle(String style) { return getFromStyle(TOP_CSS_PATTERN, style); } private static String getLeftFromStyle(String style) { return getFromStyle(LEFT_CSS_PATTERN, style); } private static String getFromStyle(Pattern pattern, String style) { Matcher matcher = pattern.matcher(style); if (matcher.find()) { assertEquals("wrong amount of groups matched in " + style, 1, matcher.groupCount()); return matcher.group(1); } else { return null; } } /** * @return {@code [0] == x}, {@code [1] == y} */ private static double[] getTranslateValues(String translate) { Matcher matcher = TRANSLATE_VALUE_PATTERN.matcher(translate); assertTrue("no matches for " + translate + " against " + TRANSLATE_VALUE_PATTERN, matcher.find()); assertEquals("wrong amout of groups matched in " + translate, 2, matcher.groupCount()); return new double[] { Double.parseDouble(matcher.group(1)), Double.parseDouble(matcher.group(2)) }; } private static double getPixelValue(String top) { Matcher matcher = PIXEL_VALUE_PATTERN.matcher(top); assertTrue( "no matches for \"" + top + "\" against " + PIXEL_VALUE_PATTERN, matcher.find()); assertEquals("wrong amount of groups matched in " + top, 1, matcher.groupCount()); return Double.parseDouble(matcher.group(1)); } }