package com.vaadin.tests.components; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.Function; import com.vaadin.annotations.Theme; import com.vaadin.event.FieldEvents.BlurEvent; import com.vaadin.event.FieldEvents.BlurListener; import com.vaadin.event.FieldEvents.BlurNotifier; import com.vaadin.event.FieldEvents.FocusEvent; import com.vaadin.event.FieldEvents.FocusListener; import com.vaadin.event.FieldEvents.FocusNotifier; import com.vaadin.server.Resource; import com.vaadin.server.ThemeResource; import com.vaadin.server.VaadinRequest; import com.vaadin.shared.Registration; import com.vaadin.tests.util.Log; import com.vaadin.tests.util.LoremIpsum; import com.vaadin.ui.AbstractComponent; import com.vaadin.ui.MenuBar; import com.vaadin.ui.MenuBar.MenuItem; import com.vaadin.v7.ui.themes.BaseTheme; @Theme("tests-components") public abstract class AbstractComponentTest<T extends AbstractComponent> extends AbstractComponentTestCase<T> implements FocusListener, BlurListener { protected static final String TEXT_SHORT = "Short"; protected static final String TEXT_MEDIUM = "This is a semi-long text that might wrap."; protected static final String TEXT_LONG = "This is a long text. " + LoremIpsum.get(500); protected static final String TEXT_VERY_LONG = "This is a very, very long text. " + LoremIpsum.get(5000); private static final Resource SELECTED_ICON = new ThemeResource( "../runo/icons/16/ok.png"); private static final LinkedHashMap<String, String> sizeOptions = new LinkedHashMap<>(); static { sizeOptions.put("auto", null); sizeOptions.put("50%", "50%"); sizeOptions.put("100%", "100%"); for (int w = 200; w < 1000; w += 100) { sizeOptions.put(w + "px", w + "px"); } } // Menu related private MenuItem mainMenu; private MenuBar menu; private MenuItem settingsMenu; private T component; // Used to determine if a menuItem should be selected and the other // unselected on click private Set<MenuItem> parentOfSelectableMenuItem = new HashSet<>(); /** * Maps the category name to a menu item */ private Map<String, MenuItem> categoryToMenuItem = new HashMap<>(); private Map<MenuItem, String> menuItemToCategory = new HashMap<>(); // Logging private Log log; protected static final String CATEGORY_STATE = "State"; protected static final String CATEGORY_SIZE = "Size"; protected static final String CATEGORY_SELECTION = "Selection"; protected static final String CATEGORY_LISTENERS = "Listeners"; protected static final String CATEGORY_FEATURES = "Features"; protected static final String CATEGORY_ACTIONS = "Actions"; protected static final String CATEGORY_DECORATIONS = "Decorations"; @Override protected final void setup(VaadinRequest request) { // Create menu here so it appears before the components addComponent(createMainMenu()); getLayout().setSizeFull(); createLog(); super.setup(request); // Create menu actions and trigger default actions createActions(); // Clear initialization log messages log.clear(); } private MenuBar createMainMenu() { menu = new MenuBar(); menu.setId("menu"); mainMenu = menu.addItem("Component", null); settingsMenu = menu.addItem("Settings", null); populateSettingsMenu(settingsMenu); return menu; } /** * Override to add items to the "settings" menu. * * NOTE, Call super class first to preserve current order. If you override * this in a class and another class overrides it you might break tests * because the wrong items will be selected. * * @param settingsMenu */ protected void populateSettingsMenu(MenuItem settingsMenu) { MenuItem showEventLog = settingsMenu.addItem("Show event log", new MenuBar.Command() { @Override public void menuSelected(MenuItem selectedItem) { boolean selected = !isSelected(selectedItem); setLogVisible(selected); setSelected(selectedItem, selected); } }); setSelected(showEventLog, true); settingsMenu.addItem("Clear log", new MenuBar.Command() { @Override public void menuSelected(MenuItem selectedItem) { log.clear(); } }); MenuItem layoutSize = settingsMenu.addItem("Parent layout size", null); MenuItem layoutWidth = layoutSize.addItem("Width", null); MenuItem layoutHeight = layoutSize.addItem("Height", null); for (final String name : sizeOptions.keySet()) { layoutWidth.addItem(name, new MenuBar.Command() { @Override public void menuSelected(MenuItem selectedItem) { getTestComponents().get(0).getParent() .setWidth(sizeOptions.get(name)); log("Parent layout width set to " + name); } }); layoutHeight.addItem(name, new MenuBar.Command() { @Override public void menuSelected(MenuItem selectedItem) { getTestComponents().get(0).getParent() .setHeight(sizeOptions.get(name)); log("Parent layout height set to " + name); } }); } } protected void setLogVisible(boolean visible) { // This is only to be screenshot-compatible with Vaadin 6, where // invisible components cause spacing if (visible) { log.removeStyleName("displaynone"); log.setCaption((String) log.getData()); } else { log.addStyleName("displaynone"); log.setCaption(null); } } private void createLog() { log = new Log(5).setNumberLogRows(true); log.setData(log.getCaption()); log.setStyleName(BaseTheme.CLIP); getLayout().addComponent(log, 1); } /** * By default initializes just one instance of {@link #getTestClass()} using * {@link #constructComponent()}. */ @Override protected void initializeComponents() { component = constructComponent(); component.setId("testComponent"); addTestComponent(component); } public T getComponent() { return component; } @Override protected void addTestComponent(T c) { super.addTestComponent(c); getLayout().setExpandRatio(c, 1); } /** * Construct the component that is to be tested. This method uses a no-arg * constructor by default. Override to customize. * * @return Instance of the component that is to be tested. * @throws IllegalAccessException * @throws InstantiationException */ protected T constructComponent() { try { return getTestClass().newInstance(); } catch (Exception e) { throw new RuntimeException( "Failed to instantiate " + getTestClass(), e); } } /** * Create actions for the component. Remember to call super.createActions() * when overriding. */ protected void createActions() { createBooleanAction("Enabled", CATEGORY_STATE, true, enabledCommand); createBooleanAction("Readonly", CATEGORY_STATE, false, readonlyCommand); createBooleanAction("Visible", CATEGORY_STATE, true, visibleCommand); createBooleanAction("Error indicator", CATEGORY_STATE, false, errorIndicatorCommand); createLocaleSelect(CATEGORY_STATE); createErrorMessageSelect(CATEGORY_DECORATIONS); createDescriptionSelect(CATEGORY_DECORATIONS); createCaptionSelect(CATEGORY_DECORATIONS); createIconSelect(CATEGORY_DECORATIONS); createWidthAndHeightActions(CATEGORY_SIZE); createStyleNameSelect(CATEGORY_DECORATIONS); createFocusActions(); } protected Command<T, Boolean> focusListenerCommand = new Command<T, Boolean>() { private Registration focusListenerRegistration; @Override public void execute(T c, Boolean value, Object data) { FocusNotifier focusNotifier = (FocusNotifier) c; if (value) { focusListenerRegistration = focusNotifier .addFocusListener(AbstractComponentTest.this); } else if (focusListenerRegistration != null) { focusListenerRegistration.remove(); } } }; protected Command<T, Boolean> blurListenerCommand = new Command<T, Boolean>() { private Registration blurListenerRegistration; @Override public void execute(T c, Boolean value, Object data) { BlurNotifier bn = (BlurNotifier) c; if (value) { blurListenerRegistration = bn .addBlurListener(AbstractComponentTest.this); } else if (blurListenerRegistration != null) { blurListenerRegistration.remove(); } } }; protected void createFocusListener(String category) { createBooleanAction("Focus listener", category, false, focusListenerCommand); } protected void createBlurListener(String category) { createBooleanAction("Blur listener", category, false, blurListenerCommand); } private void createFocusActions() { if (FocusNotifier.class.isAssignableFrom(getTestClass())) { createFocusListener(CATEGORY_LISTENERS); } if (BlurNotifier.class.isAssignableFrom(getTestClass())) { createBlurListener(CATEGORY_LISTENERS); } if (Focusable.class.isAssignableFrom(getTestClass())) { LinkedHashMap<String, Integer> tabIndexes = new LinkedHashMap<>(); tabIndexes.put("0", 0); tabIndexes.put("-1", -1); tabIndexes.put("10", 10); createSelectAction("Tab index", "State", tabIndexes, "0", new Command<T, Integer>() { @Override public void execute(T c, Integer tabIndex, Object data) { ((Focusable) c).setTabIndex(tabIndex); } }); createClickAction("Set focus", "State", new Command<T, Void>() { @Override public void execute(T c, Void value, Object data) { ((Focusable) c).focus(); } }, null); } } private void createStyleNameSelect(String category) { LinkedHashMap<String, String> options = new LinkedHashMap<>(); options.put("-", null); options.put("Light blue background (background-lightblue)", "background-lightblue"); options.put("1px red border (border-red-1px)", "border-red-1px"); options.put("2px blue border (border-blue-2px)", "border-blue-2px"); createComponentStyleNames(options); createSelectAction("Style name", category, options, "-", styleNameCommand); } protected void createComponentStyleNames( LinkedHashMap<String, String> options) { } private void createErrorMessageSelect(String category) { LinkedHashMap<String, String> options = new LinkedHashMap<>(); options.put("-", null); options.put(TEXT_SHORT, TEXT_SHORT); options.put("Medium", TEXT_MEDIUM); options.put("Long", TEXT_LONG); options.put("Very long", TEXT_VERY_LONG); createSelectAction("Error message", category, options, "-", errorMessageCommand); } private void createDescriptionSelect(String category) { LinkedHashMap<String, String> options = new LinkedHashMap<>(); options.put("-", null); options.put(TEXT_SHORT, TEXT_SHORT); options.put("Medium", TEXT_MEDIUM); options.put("Long", TEXT_LONG); options.put("Very long", TEXT_VERY_LONG); createSelectAction("Description / tooltip", category, options, "-", descriptionCommand); } private void createCaptionSelect(String category) { createSelectAction("Caption", category, createCaptionOptions(), "Short", captionCommand); } protected LinkedHashMap<String, String> createCaptionOptions() { LinkedHashMap<String, String> options = new LinkedHashMap<>(); options.put("-", null); options.put("Short", TEXT_SHORT); options.put("Medium", TEXT_MEDIUM); options.put("Long", TEXT_LONG); options.put("Very long", TEXT_VERY_LONG); return options; } private void createWidthAndHeightActions(String category) { String widthCategory = "Width"; String heightCategory = "Height"; createCategory(widthCategory, category); createCategory(heightCategory, category); for (String name : sizeOptions.keySet()) { createClickAction(name, widthCategory, widthCommand, sizeOptions.get(name)); createClickAction(name, heightCategory, heightCommand, sizeOptions.get(name)); } // Default to undefined size for (T c : getTestComponents()) { c.setWidth(null); c.setHeight(null); } } private void createIconSelect(String category) { LinkedHashMap<String, Resource> options = new LinkedHashMap<>(); options.put("-", null); options.put("16x16", ICON_16_USER_PNG_CACHEABLE); options.put("32x32", ICON_32_ATTENTION_PNG_CACHEABLE); options.put("64x64", ICON_64_EMAIL_REPLY_PNG_CACHEABLE); createSelectAction("Icon", category, options, "-", iconCommand, null); } private void createLocaleSelect(String category) { LinkedHashMap<String, Locale> options = new LinkedHashMap<>(); options.put("-", null); options.put("fi_FI", new Locale("fi", "FI")); options.put("en_US", Locale.US); options.put("zh_CN", Locale.SIMPLIFIED_CHINESE); options.put("fr_FR", Locale.FRANCE); createSelectAction("Locale", category, options, "en_US", localeCommand, null); } protected void createBooleanAction(String caption, String category, boolean initialState, final Command<T, Boolean> command) { createBooleanAction(caption, category, initialState, command, null); } protected <DATATYPE> void createBooleanAction(String caption, String category, boolean initialState, final Command<T, Boolean> command, Object data) { MenuItem categoryItem = getCategoryMenuItem(category); MenuItem item = categoryItem.addItem(caption, menuBooleanCommand(command, data)); setSelected(item, initialState); doCommand(caption, command, initialState, data); } protected <DATATYPE> void createClickAction(String caption, String category, final Command<T, DATATYPE> command, DATATYPE value) { createClickAction(caption, category, command, value, null); } protected <DATATYPE> void createClickAction(String caption, String category, final Command<T, DATATYPE> command, DATATYPE value, Object data) { MenuItem categoryItem = getCategoryMenuItem(category); categoryItem.addItem(caption, menuClickCommand(command, value, data)); } private MenuItem getCategoryMenuItem(String category) { if (category == null) { return getCategoryMenuItem("Misc"); } MenuItem item = categoryToMenuItem.get(category); if (item != null) { return item; } return createCategory(category, null); } /** * Creates category named "category" with id "categoryId" in parent category * "parentCategory". Each categoryId must be globally unique. * * @param category * @param categoryId * @param parentCategory * @return */ protected MenuItem createCategory(String category, String parentCategory) { if (hasCategory(category)) { return categoryToMenuItem.get(category); } MenuItem item; if (parentCategory == null) { item = mainMenu.addItem(category, null); } else { item = getCategoryMenuItem(parentCategory).addItem(category, null); } categoryToMenuItem.put(category, item); menuItemToCategory.put(item, category); return item; } protected boolean hasCategory(String categoryId) { return categoryToMenuItem.containsKey(categoryId); } protected void removeCategory(String categoryId) { if (!hasCategory(categoryId)) { throw new IllegalArgumentException( "Category '" + categoryId + "' does not exist"); } MenuItem item = getCategoryMenuItem(categoryId); Object[] children = item.getChildren().toArray(); for (Object child : children) { if (menuItemToCategory.containsKey(child)) { removeCategory(menuItemToCategory.get(child)); } } // Detach from parent item.getParent().removeChild(item); // Clean mappings categoryToMenuItem.remove(categoryId); menuItemToCategory.remove(item); } private MenuBar.Command menuBooleanCommand( final com.vaadin.tests.components.ComponentTestCase.Command<T, Boolean> booleanCommand, final Object data) { return new MenuBar.Command() { @Override public void menuSelected(MenuItem selectedItem) { boolean selected = !isSelected(selectedItem); doCommand(getText(selectedItem), booleanCommand, selected, data); setSelected(selectedItem, selected); } }; } private <DATATYPE> MenuBar.Command menuClickCommand( final com.vaadin.tests.components.ComponentTestCase.Command<T, DATATYPE> command, final DATATYPE value, final Object data) { return new MenuBar.Command() { @Override public void menuSelected(MenuItem selectedItem) { doCommand(getText(selectedItem), command, value, data); } }; } protected void setSelected(MenuItem item, boolean selected) { if (selected) { item.setIcon(SELECTED_ICON); } else { item.setIcon(null); } } protected boolean isSelected(MenuItem item) { return item.getIcon() != null; } private <VALUETYPE> MenuBar.Command singleSelectMenuCommand( final com.vaadin.tests.components.ComponentTestCase.Command<T, VALUETYPE> cmd, final VALUETYPE object, final Object data) { return new MenuBar.Command() { @Override public void menuSelected(MenuItem selectedItem) { doCommand(getText(selectedItem), cmd, object, data); if (parentOfSelectableMenuItem .contains(selectedItem.getParent())) { unselectChildren(selectedItem.getParent()); setSelected(selectedItem, true); } } }; } /** * Unselect all child menu items * * @param parent */ protected void unselectChildren(MenuItem parent) { List<MenuItem> children = parent.getChildren(); if (children == null) { return; } for (MenuItem child : children) { setSelected(child, false); } } protected String getText(MenuItem item) { String path = ""; MenuItem parent = item.getParent(); while (!isCategory(parent)) { path = parent.getText() + "/" + path; parent = parent.getParent(); } return path + "/" + item.getText(); } private boolean isCategory(MenuItem item) { return item.getParent() == mainMenu; } protected <TYPE> void createSelectAction(String caption, String category, LinkedHashMap<String, TYPE> options, String initialValue, com.vaadin.tests.components.ComponentTestCase.Command<T, TYPE> command) { createSelectAction(caption, category, options, initialValue, command, null); } protected <TYPE extends Enum<TYPE>> void createSelectAction(String caption, String category, Class<TYPE> enumType, TYPE initialValue, com.vaadin.tests.components.ComponentTestCase.Command<T, TYPE> command) { LinkedHashMap<String, TYPE> options = new LinkedHashMap<>(); for (TYPE value : EnumSet.allOf(enumType)) { options.put(value.toString(), value); } createSelectAction(caption, category, options, initialValue.toString(), command); } protected <TYPE> void createMultiClickAction(String caption, String category, LinkedHashMap<String, TYPE> options, com.vaadin.tests.components.ComponentTestCase.Command<T, TYPE> command, Object data) { MenuItem categoryItem = getCategoryMenuItem(category); MenuItem mainItem = categoryItem.addItem(caption, null); for (String option : options.keySet()) { MenuBar.Command cmd = menuClickCommand(command, options.get(option), data); mainItem.addItem(option, cmd); } } protected <TYPE> void createMultiToggleAction(String caption, String category, LinkedHashMap<String, TYPE> options, com.vaadin.tests.components.ComponentTestCase.Command<T, Boolean> command, boolean defaultValue) { LinkedHashMap<String, Boolean> defaultValues = new LinkedHashMap<>(); for (String option : options.keySet()) { defaultValues.put(option, defaultValue); } createMultiToggleAction(caption, category, options, command, defaultValues); } protected <TYPE> void createMultiToggleAction(String caption, String category, LinkedHashMap<String, TYPE> options, com.vaadin.tests.components.ComponentTestCase.Command<T, Boolean> command, LinkedHashMap<String, Boolean> defaultValues) { createCategory(caption, category); for (String option : options.keySet()) { createBooleanAction(option, caption, defaultValues.get(option), command, options.get(option)); } } protected <TYPE> void createSelectAction(String caption, String category, LinkedHashMap<String, TYPE> options, String initialValue, com.vaadin.tests.components.ComponentTestCase.Command<T, TYPE> command, Object data) { MenuItem parentItem = getCategoryMenuItem(category); MenuItem mainItem = parentItem.addItem(caption, null); parentOfSelectableMenuItem.add(mainItem); for (String option : options.keySet()) { MenuBar.Command cmd = singleSelectMenuCommand(command, options.get(option), data); MenuItem item = mainItem.addItem(option, cmd); if (option.equals(initialValue)) { cmd.menuSelected(item); } } } protected void createListenerAction(String caption, String category, Function<T, Registration> addListener) { createBooleanAction(caption, category, false, new Command<T, Boolean>() { Registration registration; @Override public void execute(T c, Boolean enabled, Object data) { if (enabled) { registration = addListener.apply(c); } else if (registration != null) { registration.remove(); registration = null; } } }); } protected LinkedHashMap<String, Integer> createIntegerOptions(int max) { LinkedHashMap<String, Integer> options = new LinkedHashMap<>(); for (int i = 0; i <= 9 && i <= max; i++) { options.put(String.valueOf(i), i); } for (int i = 10; i <= max; i *= 10) { options.put(String.valueOf(i), i); if (2 * i <= max) { options.put(String.valueOf(2 * i), 2 * i); } if (5 * i <= max) { options.put(String.valueOf(5 * i), 5 * i); } } return options; } protected LinkedHashMap<String, Double> createDoubleOptions(double max) { LinkedHashMap<String, Double> options = new LinkedHashMap<>(); for (double d = 0; d <= max && d < 10; d += 0.5) { options.put(String.valueOf(d), d); } for (double d = 10; d <= max; d *= 10) { options.put(String.valueOf(d), d); if (2.5 * d <= max) { options.put(String.valueOf(2 * d), 2 * d); } if (5 * d <= max) { options.put(String.valueOf(5 * d), 5 * d); } } return options; } protected LinkedHashMap<String, Resource> createIconOptions( boolean cacheable) { LinkedHashMap<String, Resource> options = new LinkedHashMap<>(); options.put("-", null); if (cacheable) { options.put("16x16", ICON_16_USER_PNG_CACHEABLE); options.put("32x32", ICON_32_ATTENTION_PNG_CACHEABLE); options.put("64x64", ICON_64_EMAIL_REPLY_PNG_CACHEABLE); } else { options.put("16x16", ICON_16_USER_PNG_UNCACHEABLE); options.put("32x32", ICON_32_ATTENTION_PNG_UNCACHEABLE); options.put("64x64", ICON_64_EMAIL_REPLY_PNG_UNCACHEABLE); } return options; } protected void log(String msg) { log.log(msg); } protected boolean hasLog() { return log != null; } @Override protected <VALUET> void doCommand(String commandName, AbstractComponentTestCase.Command<T, VALUET> command, VALUET value, Object data) { if (hasLog()) { log("Command: " + commandName + "(" + value + ")"); } super.doCommand(commandName, command, value, data); } @Override public void focus(FocusEvent event) { log(event.getClass().getSimpleName()); } @Override public void blur(BlurEvent event) { log(event.getClass().getSimpleName()); } }