/*
* 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.screenshotbrowser;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.vaadin.event.ShortcutAction.KeyCode;
import com.vaadin.event.ShortcutListener;
import com.vaadin.server.ExternalResource;
import com.vaadin.server.FileResource;
import com.vaadin.server.VaadinRequest;
import com.vaadin.ui.BrowserFrame;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.CustomComponent;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.v7.data.Property.ValueChangeEvent;
import com.vaadin.v7.data.Property.ValueChangeListener;
import com.vaadin.v7.data.util.BeanItemContainer;
import com.vaadin.v7.ui.Table;
public class ScreenshotBrowser extends UI {
private static final File screenshotDir = findScreenshotDir();
/*-
* Groups:
* 1 - test class
* 2 - test method
* 3 - platform
* 4 - browser name
* 5 - browser version
* 6 - additional qualifiers
* 7 - identifier
*/
private static final Pattern screenshotNamePattern = Pattern
.compile("(.+?)-(.+?)_(.+?)_(.+?)_(.+?)(_.+)?_(.+?)\\.png\\.html");
public static enum Action {
ACCEPT {
@Override
public void apply(File screenshotFile) {
File targetFile = new File(getReferenceDir(),
screenshotFile.getName());
// Delete previous file as well as any alternatives
if (targetFile.exists()) {
for (int i = 1; true; i++) {
File alternative = getAlternative(targetFile, i);
if (alternative.exists()) {
alternative.delete();
} else {
break;
}
}
targetFile.delete();
}
screenshotFile.renameTo(targetFile);
}
},
IGNORE {
@Override
public void apply(File screenshotFile) {
screenshotFile.delete();
}
},
ALTERNATIVE {
@Override
public void apply(File screenshotFile) {
File baseFile = new File(getReferenceDir(),
screenshotFile.getName());
// Iterate until we find the first alternative id not yet used
File targetFile = baseFile;
int alternativeNumber = 1;
while (targetFile.exists()) {
targetFile = getAlternative(baseFile, alternativeNumber++);
}
screenshotFile.renameTo(targetFile);
}
};
public void commit(File htmlFile) {
String screenshotName = htmlFile.getName().substring(0,
htmlFile.getName().length() - ".html".length());
apply(new File(htmlFile.getParentFile(), screenshotName));
htmlFile.delete();
}
private static File getReferenceDir() {
return new File(screenshotDir, "reference");
}
private static File getAlternative(File baseFile,
int alternativeNumber) {
assert alternativeNumber >= 1;
String alternativeName = baseFile.getName().replaceFirst("\\.png",
"_" + alternativeNumber + ".png");
return new File(baseFile.getParentFile(), alternativeName);
}
protected abstract void apply(File screenshotFile);
}
public static class ComparisonFailure {
private final Matcher matcher;
private final File file;
private Action action;
public ComparisonFailure(File file) {
this.file = file;
matcher = screenshotNamePattern.matcher(file.getName());
if (!matcher.matches()) {
throw new RuntimeException("Could not parse screenshot name "
+ file.getAbsolutePath());
}
}
public File getFile() {
return file;
}
public String getName() {
return matcher.group();
}
public String getTestClass() {
return matcher.group(1);
}
public String getTestMethod() {
return matcher.group(2);
}
public String getBrowser() {
return matcher.group(4) + " " + matcher.group(5);
}
public String getQualifiers() {
return matcher.group(6);
}
public String getIdentifier() {
return matcher.group(7);
}
public void setAction(Action action) {
this.action = action;
}
public Action getAction() {
return action;
}
}
private class Viewer extends CustomComponent {
private BrowserFrame preview = new BrowserFrame();
private VerticalLayout left = new VerticalLayout();
private HorizontalLayout root = new HorizontalLayout(left, preview);
private Collection<ComparisonFailure> items;
public Viewer() {
preview.setWidth("1500px");
preview.setHeight("100%");
left.setMargin(true);
left.setSpacing(true);
left.setSizeFull();
left.addComponent(
createActionButton("Accept changes", 'j', Action.ACCEPT));
left.addComponent(
createActionButton("Ignore changes", 'k', Action.IGNORE));
left.addComponent(createActionButton("Use as alternative", 'l',
Action.ALTERNATIVE));
left.addComponent(
new Button("Clear action", createSetActionListener(null)));
left.addComponent(createSpacer());
left.addComponent(
new Button("Commit actions", new Button.ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
commitActions();
}
}));
left.addComponent(createSpacer());
left.addComponent(new Button("Refresh", new Button.ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
refreshTableContainer();
}
}));
Label expandSpacer = createSpacer();
left.addComponent(expandSpacer);
left.setExpandRatio(expandSpacer, 1);
left.addComponent(new Label(
"Press the j, k or l keys to quickly select an action for the selected item."));
root.setExpandRatio(left, 1);
root.setSizeFull();
setCompositionRoot(root);
setHeight("850px");
setWidth("100%");
}
private Button createActionButton(String caption, char shortcut,
Action action) {
Button button = new Button(
caption + " <strong>" + shortcut + "</strong>",
createSetActionListener(action));
button.setCaptionAsHtml(true);
return button;
}
private Label createSpacer() {
// Poor man's spacer, non-breaking space
return new Label("\u00a0");
}
private ClickListener createSetActionListener(final Action action) {
return new ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
setActions(action);
}
};
}
public void setActions(final Action action) {
for (ComparisonFailure comparisonFailure : items) {
comparisonFailure.setAction(action);
}
table.refreshRowCache();
}
public void setItems(Collection<ComparisonFailure> items) {
this.items = items;
if (items.size() == 1) {
ComparisonFailure failure = items.iterator().next();
preview.setSource(new FileResource(failure.getFile()));
} else {
preview.setSource(new ExternalResource("about:blank"));
}
}
}
private final Table table = new Table();
private final Viewer viewer = new Viewer();
@Override
protected void init(VaadinRequest request) {
table.setWidth("100%");
table.setHeight("100%");
table.setMultiSelect(true);
table.addValueChangeListener(new ValueChangeListener() {
@Override
public void valueChange(ValueChangeEvent event) {
@SuppressWarnings("unchecked")
Collection<ComparisonFailure> selectedRows = (Collection<ComparisonFailure>) table
.getValue();
viewer.setItems(selectedRows);
}
});
table.addShortcutListener(
createShortcutListener(KeyCode.J, Action.ACCEPT));
table.addShortcutListener(
createShortcutListener(KeyCode.K, Action.IGNORE));
table.addShortcutListener(
createShortcutListener(KeyCode.L, Action.ALTERNATIVE));
refreshTableContainer();
VerticalLayout mainLayout = new VerticalLayout(table, viewer);
mainLayout.setExpandRatio(table, 1);
mainLayout.setSizeFull();
setSizeFull();
setContent(mainLayout);
table.focus();
}
private void commitActions() {
for (ComparisonFailure comparisonFailure : getContainer()
.getItemIds()) {
Action action = comparisonFailure.getAction();
if (action != null) {
action.commit(comparisonFailure.getFile());
}
}
refreshTableContainer();
}
private ShortcutListener createShortcutListener(int keyCode,
final Action action) {
return new ShortcutListener(action.toString(), keyCode, null) {
@Override
public void handleAction(Object sender, Object target) {
viewer.setActions(action);
selectNextWithoutAction();
}
};
}
private void selectNextWithoutAction() {
Collection<?> selected = (Collection<?>) table.getValue();
BeanItemContainer<ComparisonFailure> container = getContainer();
// Find where to start
ComparisonFailure candidate;
if (selected == null || selected.isEmpty()) {
candidate = container.firstItemId();
} else {
candidate = (ComparisonFailure) selected.iterator().next();
}
// Find first one without action
while (candidate != null && candidate.getAction() != null) {
candidate = container.nextItemId(candidate);
}
// Select it
if (candidate == null) {
table.setValue(Collections.emptySet());
} else {
table.setValue(Collections.singleton(candidate));
}
}
@SuppressWarnings("unchecked")
private BeanItemContainer<ComparisonFailure> getContainer() {
return (BeanItemContainer<ComparisonFailure>) table
.getContainerDataSource();
}
private void refreshTableContainer() {
File errorsDir = new File(screenshotDir, "errors");
File[] failures = errorsDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".html");
}
});
BeanItemContainer<ComparisonFailure> container = new BeanItemContainer<>(
ComparisonFailure.class);
for (File failure : failures) {
container.addBean(new ComparisonFailure(failure));
}
table.setContainerDataSource(container);
table.setVisibleColumns("testClass", "testMethod", "browser",
"qualifiers", "identifier", "action");
if (container.size() > 0) {
table.select(container.firstItemId());
}
}
private static File findScreenshotDir() {
File propertiesFile = new File(
"../work/eclipse-run-selected-test.properties");
if (!propertiesFile.exists()) {
throw new RuntimeException(
"File " + propertiesFile.getAbsolutePath() + " not found.");
}
FileInputStream in = null;
try {
in = new FileInputStream(propertiesFile);
Properties properties = new Properties();
properties.load(in);
String screenShotDirName = properties
.getProperty("com.vaadin.testbench.screenshot.directory");
if (screenShotDirName == null
|| screenShotDirName.startsWith("<")) {
throw new RuntimeException(
"com.vaadin.testbench.screenshot.directory has not been configred in "
+ propertiesFile.getAbsolutePath());
}
File screenshotDir = new File(screenShotDirName);
if (!screenshotDir.isDirectory()) {
throw new RuntimeException(screenshotDir.getAbsolutePath()
+ " is not a directory");
}
return screenshotDir;
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}