com.vaadin.v7.tests.components.grid.basicfeatures.escalator.EscalatorSpacerTest.java Source code

Java tutorial

Introduction

Here is the source code for com.vaadin.v7.tests.components.grid.basicfeatures.escalator.EscalatorSpacerTest.java

Source

/*
 * 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));
    }
}