com.googlecode.fightinglayoutbugs.DetectElementsWithInvisibleFocus.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.fightinglayoutbugs.DetectElementsWithInvisibleFocus.java

Source

/*
 * Copyright 2009-2012 Michael Tamm
 *
 * 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.googlecode.fightinglayoutbugs;

import com.google.gson.Gson;
import com.googlecode.fightinglayoutbugs.helpers.RectangularRegion;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openqa.selenium.*;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;

/**
 * <p>
 * Detects if there are elements on the analyzed web page, which
 * take the focus (when the user presses the TAB key several times)
 * but do not change their visual appearance when they got the focus.
 * </p><p>
 * This is actually a usability problem, because the user does not
 * see, which element is currently focused.
 * </p><p>
 * Attention: This detector is very slow, because it needs to take
 * a screenshot after each simulated press on the TAB key.
 * </p>
 */
public class DetectElementsWithInvisibleFocus extends AbstractLayoutBugDetector {

    private static final Log LOG = LogFactory.getLog(DetectElementsWithInvisibleFocus.class);
    private static final RectangularRegion NOT_DISPLAYED = new RectangularRegion(0, 0, 0, 0);

    @Override
    public Collection<LayoutBug> findLayoutBugsIn(@Nonnull WebPage webPage) {
        Collection<LayoutBug> result = new ArrayList<LayoutBug>();
        // 1.) Focus first focusable element ...
        FocusedElement focusedElement1 = focusFirstElement(webPage);
        if (focusedElement1 != null) {
            Set<WebElement> visitedElements = new HashSet<WebElement>();
            visitedElements.add(focusedElement1.element);
            Screenshot screenshot1 = webPage.takeScreenshot();
            List<RectangularRegion> focusOrder = new ArrayList<RectangularRegion>();
            focusOrder.add(focusedElement1.region);
            // 2.) Focus next elements and compare screenshots (restrict detection to first 99 focusable elements) ...
            for (int i = 2; i <= 99; ++i) {
                FocusedElement focusedElement2 = focusNextElement(focusedElement1, webPage, visitedElements);
                if (focusedElement2 == null) {
                    break;
                }
                focusOrder.add(focusedElement2.region);
                Screenshot screenshot2 = webPage.takeScreenshot();
                if (i == 2 && focusedElement1.hasInvisibleFocus(screenshot1, screenshot2)) {
                    result.add(createLayoutBug(focusedElement1, focusOrder, webPage, screenshot1));
                }
                if (focusedElement2.hasInvisibleFocus(screenshot2, screenshot1)) {
                    result.add(createLayoutBug(focusedElement2, focusOrder, webPage, screenshot2));
                }
                screenshot1 = screenshot2;
                focusedElement1 = focusedElement2;
            }
        }
        return result;
    }

    @Nullable
    private FocusedElement focusFirstElement(WebPage webPage) {
        WebElement firstFocusedWebElement = getFocusedWebElement(webPage);
        if (firstFocusedWebElement == null) {
            // Try to focus first element ...
            try {
                WebDriver driver = webPage.getDriver();
                WebElement bodyElement = driver.findElement(By.tagName("body"));
                bodyElement.sendKeys(Keys.TAB);
            } catch (Exception e) {
                LOG.warn("Failed to focus first element.", e);
            }
            firstFocusedWebElement = getFocusedWebElement(webPage);
        } else if ("body".equals(firstFocusedWebElement.getTagName().toLowerCase())) {
            firstFocusedWebElement.sendKeys(Keys.TAB);
            firstFocusedWebElement = getFocusedWebElement(webPage);
        }
        if (firstFocusedWebElement != null && !"body".equals(firstFocusedWebElement.getTagName().toLowerCase())) {
            return toFocusedElement(firstFocusedWebElement, webPage);
        } else {
            return null;
        }
    }

    private WebElement getFocusedWebElement(WebPage webPage) {
        return (WebElement) webPage.executeJavaScript("return document.activeElement;");
    }

    private FocusedElement toFocusedElement(@Nonnull WebElement activeElement, WebPage webPage) {
        // I don't trust WebDriver, that's why I determine the offset, width and height with jQuery too ...
        Map temp = new Gson()
                .fromJson(
                        (String) webPage.executeJavaScript(
                                "var $element = jQuery(arguments[0]);\n" + "var offset = $element.offset();\n"
                                        + "var $temp = $element.clone(false).wrap('<div></div>').parent();\n"
                                        + "try {\n" + "    return JSON.stringify({\n" + "        x: offset.left,\n"
                                        + "        y: offset.top,\n" + "        w: $element.width(),\n"
                                        + "        h: $element.height(),\n" + "        html: $temp.html()\n"
                                        + "    });\n" + "} finally {\n" + "    $temp.remove();\n" + "}",
                                activeElement),
                        Map.class);
        int x = ((Number) temp.get("x")).intValue();
        int y = ((Number) temp.get("y")).intValue();
        int w = ((Number) temp.get("w")).intValue();
        int h = ((Number) temp.get("h")).intValue();
        String html = (String) temp.get("html");
        if (activeElement.isDisplayed()) {
            Point location = activeElement.getLocation();
            Dimension size = activeElement.getSize();
            int x1 = Math.min(location.getX(), x);
            int y1 = Math.min(location.getY(), y);
            int x2 = Math.max(location.getX() + size.getWidth(), x + w) - 1;
            int y2 = Math.max(location.getY() + size.getHeight(), y + h) - 1;
            return new FocusedElement(activeElement, new RectangularRegion(x1, y1, x2, y2), html);
        } else {
            return new FocusedElement(activeElement, NOT_DISPLAYED, html);
        }
    }

    @Nullable
    private FocusedElement focusNextElement(FocusedElement focusedElement, WebPage webPage,
            Collection<WebElement> visitedElements) {
        focusedElement.element.sendKeys(Keys.TAB);
        final WebElement focusedWebElement = getFocusedWebElement(webPage);
        if (focusedWebElement != null && !visitedElements.contains(focusedWebElement)
                && !"body".equals(focusedWebElement.getTagName().toLowerCase())) {
            visitedElements.add(focusedWebElement);
            return toFocusedElement(focusedWebElement, webPage);
        } else {
            return null;
        }
    }

    private LayoutBug createLayoutBug(FocusedElement focusedElement, List<RectangularRegion> focusOrder,
            WebPage webPage, Screenshot screenshotWithFocus) {
        return createLayoutBug(
                "Detected element with invisible focus -- i.e. the element does not change its appearance when it gets the focus.\n"
                        + "- Element: " + focusedElement.html.replace("\n", "\n           ") + "\n" + "- Region: "
                        + focusedElement.region,
                webPage, screenshotWithFocus, new InvisibleFocusMarker(focusedElement, focusOrder));
    }

    private static class FocusedElement {
        private final WebElement element;
        private final RectangularRegion region;
        private final String html;

        private FocusedElement(WebElement element, RectangularRegion region, String html) {
            this.element = element;
            this.region = region;
            this.html = html;
        }

        private boolean hasInvisibleFocus(Screenshot screenshotWithFocus, Screenshot screenshotWithoutFocus) {
            // Ignore text input fields, they should have a blinking cursor ...
            if (isTextInputField(element)) {
                return false;
            }
            // Ignore elements, which are not displayed ...
            if (!isDisplayed()) {
                return false;
            }
            // To prevent false alarms we extend the region to analyze by 4 pixels in each direction ...
            RectangularRegion regionToAnalyze = addBorder(region, 4, screenshotWithFocus);
            ScreenshotRegion screenshotRegionWithFocus = new ScreenshotRegion(screenshotWithFocus, regionToAnalyze);
            ScreenshotRegion screenshotRegionWithoutFocus = new ScreenshotRegion(screenshotWithoutFocus,
                    regionToAnalyze);
            return screenshotRegionWithFocus.equals(screenshotRegionWithoutFocus);
        }

        private boolean isDisplayed() {
            return region != NOT_DISPLAYED;
        }

        private boolean isTextInputField(WebElement webElement) {
            String tagName = webElement.getTagName().toLowerCase();
            if ("input".equals(tagName)) {
                String typeAttribute = webElement.getAttribute("type");
                return (typeAttribute == null || typeAttribute.isEmpty() || "text".equals(typeAttribute)
                        || "password".equals(typeAttribute));
            } else {
                return "textarea".equals(tagName);
            }
        }

        private RectangularRegion addBorder(RectangularRegion region, int border, Screenshot screenshot) {
            final int x1 = Math.max(0, region.x1 - border);
            final int y1 = Math.max(0, region.y1 - border);
            final int x2 = Math.max(screenshot.width - 1, region.x2 + border);
            final int y2 = Math.max(screenshot.height - 1, region.y2 + border);
            return new RectangularRegion(x1, y1, x2, y2);
        }
    }

    private static class InvisibleFocusMarker implements Marker {
        private final FocusedElement focusedElement;
        private final List<RectangularRegion> focusOrder;

        public InvisibleFocusMarker(FocusedElement data, List<RectangularRegion> focusOrder) {
            this.focusedElement = data;
            this.focusOrder = focusOrder;
        }

        @Override
        public void mark(int[][] screenshot) {
            final int w = screenshot.length;
            final int h = screenshot[0].length;
            // TODO: add numbers to indicate focus order
            for (int x = focusedElement.region.x1; x <= focusedElement.region.x2 && x < w; ++x) {
                for (int y = focusedElement.region.y1; y <= focusedElement.region.y2 && y < h; ++y) {
                    if ((x + y) % 2 == 0) {
                        screenshot[x][y] = 0xFF0000;
                    }
                }
            }
            // TODO: fade out unimportant areas
        }
    }
}