/** Eliminate redundant modifier keys surrounding keystrokes or keystrings. */ private void removeExtraModifiers() { setStatus("Removing extra modifiers"); for (int i = 0; i < steps.size(); i++) { Step step = (Step) steps.get(i); if (isKey(step, ANY_KEY, PRESS)) { Event se = (Event) step; String cs = se.getAttribute(XMLConstants.TAG_KEYCODE); int code = AWT.getKeyCode(cs); boolean remove = false; boolean foundKeyStroke = false; if (AWT.isModifier(code)) { for (int j = i + 1; j < steps.size(); j++) { Step next = (Step) steps.get(j); if (isKey(next, cs, RELEASE)) { if (foundKeyStroke) { steps.remove(j); remove = true; } break; } else if (isKeyStroke(next, ANY_KEY) || isKeyString(next)) { foundKeyStroke = true; remove = true; } else if (!isKey(next, ANY_KEY, EITHER)) { break; } } } if (remove) { steps.remove(i--); } } } }
/** Remove keypress events preceding and following ActionMap actions. */ private void removeShortcutModifierKeyPresses() { int current = 0; int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); String modifier = AWT.getKeyCode(AWT.maskToKeyCode(mask)); while (current < steps.size()) { Step step = (Step) steps.get(current); if (isKey(step, modifier, PRESS)) { Log.debug("Found possible extraneous modifier"); int keyDown = current; Action action = null; while (++current < steps.size()) { step = (Step) steps.get(current); if (step instanceof Action) { if ("actionActionMap".equals(((Action) step).getMethodName())) { action = (Action) step; continue; } } else if (isKey(step, modifier, RELEASE)) { if (action != null) { Log.debug("Removing extraneous shortcut modifier"); steps.remove(current); steps.remove(keyDown); current = keyDown - 1; } } break; } } ++current; } }
/** Provides shared UI- and action-related constants. */ public interface AWTConstants { int MULTI_CLICK_INTERVAL = 250; // a guess /** Number of pixels traversed before a drag starts. */ // OSX 10(1.3.1), 5(1.4.1) // Linux/X11: delay+16 // NOTE: could maybe install a drag gesture recognizer, but that's kinda // complex for what you get out of it. int DRAG_THRESHOLD = Platform.isWindows() || Platform.isMacintosh() ? 10 : 16; int BUTTON_MASK = (InputEvent.BUTTON1_MASK | InputEvent.BUTTON2_MASK | InputEvent.BUTTON3_MASK); int POPUP_MASK = AWT.getPopupMask(); String POPUP_MODIFIER = AWT.getMouseModifiers(POPUP_MASK); boolean POPUP_ON_PRESS = AWT.getPopupOnPress(); int TERTIARY_MASK = AWT.getTertiaryMask(); String TERTIARY_MODIFIER = AWT.getMouseModifiers(TERTIARY_MASK); int MENU_SHORTCUT_MASK = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask(); String MENU_SHORTCUT_MODIFIER = AWT.getKeyModifiers(MENU_SHORTCUT_MASK); String MENU_SHORTCUT_STRING = MENU_SHORTCUT_MASK == InputEvent.ALT_MASK ? "alt " : MENU_SHORTCUT_MASK == InputEvent.META_MASK ? "meta " : MENU_SHORTCUT_MASK == InputEvent.SHIFT_MASK ? "shift " : "control "; String MENU_SHORTCUT_KEYCODE = AWT.getKeyCode(AWT.maskToKeyCode(MENU_SHORTCUT_MASK)); }
/** Parse clicks to cancel the recording if we get a click that's not in the JList (or ESC). */ protected boolean parseClick(AWTEvent event) { if (isFinished()) { return false; } // FIXME add key-based activation/termination? boolean consumed = true; if (combo == null) { combo = getComboBox(event); listener = new ActionListener() { public void actionPerformed(ActionEvent ev) { index = combo.getSelectedIndex(); if (!combo.isPopupVisible()) { combo.removeActionListener(listener); setFinished(true); } } }; combo.addActionListener(listener); setStatus("Waiting for selection"); } else if (event.getID() == KeyEvent.KEY_RELEASED && (((KeyEvent) event).getKeyCode() == KeyEvent.VK_SPACE || ((KeyEvent) event).getKeyCode() == KeyEvent.VK_ENTER)) { index = combo.getSelectedIndex(); setFinished(true); } // Cancel via click somewhere else else if (event.getID() == MouseEvent.MOUSE_PRESSED && !AWT.isOnPopup((Component) event.getSource()) && combo != getComboBox(event)) { setFinished(true); consumed = false; } // Cancel via ESC key else if (event.getID() == KeyEvent.KEY_RELEASED && ((KeyEvent) event).getKeyCode() == KeyEvent.VK_ESCAPE) { setStatus("Selection canceled"); setFinished(true); } else { Log.debug("Event ignored"); } if (list == null && combo.isPopupVisible()) list = tester.findComboList(combo); if (isFinished()) { combo.removeActionListener(listener); listener = null; } return consumed; }
/** Return the semantic recorder for the given component. */ private SemanticRecorder getSemanticRecorder(Component comp) { // FIXME extract into AWT.getLAFParent? // Account for LAF implementations that use a JButton on top // of the combo box if ((comp instanceof JButton) && (comp.getParent() instanceof JComboBox)) { comp = comp.getParent(); } // Account for LAF components of JInternalFrame else if (AWT.isInternalFrameDecoration(comp)) { while (!(comp instanceof JInternalFrame)) comp = comp.getParent(); } return getSemanticRecorder(comp.getClass()); }
/** Assert the currently active window has the specified name */ protected static void assertWindowname(String name) throws InterruptedException, ComponentNotFoundException { logger.fine("Expecting window name: " + name); Window w = null; for (int i = 0; i < 100; i++) { guiSleep(); w = AWT.getActiveWindow(); if (w == null) continue; if (name.equals(w.getName())) return; Thread.sleep(100); } throw new ComponentNotFoundException( "Window name not found: " + name + (w != null ? (" (currently focused: " + w.getName() + ")") : "")); }
/** Gives focus to a {@linkplain Component} by its name */ protected static boolean focusByName(final String name) throws ComponentNotFoundException, MultipleComponentsFoundException { Component c = findByName(name); if (c.hasFocus()) return true; // try ordinary method if (c.requestFocusInWindow()) { while (!c.hasFocus()) guiSleep(); return true; } // press tab until we have the correct focus for (int i = 0; i < 25 /* TODO proper number */; i++) { tester.keyStroke('\t'); guiSleep(); if (name.equals(AWT.getActiveWindow().getFocusOwner().getName())) return true; } // failed ... logger.warning("Could not give focus to component: " + name); return true; }
/** Make screenshot taking part of unit tests */ @Test public static void testScreenshots() throws Exception { File shotdir = FileUtils.createTempDir("jgridstart-screenshots-"); try { doScreenshots(shotdir); } catch (Throwable e) { // on error, output final screenshot as base64 on debug log File errorshot = new File(shotdir, "error.png"); saveScreenshot(errorshot); Thread.sleep(500); FileInputStream in = new FileInputStream(errorshot); byte[] data = new byte[(int) errorshot.length()]; in.read(data, 0, data.length); // need to log in chunks because logger doesn't seem to be able to support >4096 chars String basedata = new String(Base64.encode(data)); logger.finest("Interactive UI testing failed, last screenshot (base64 encoded):"); logger.finest("=== BEGIN PNG ==="); int pos = 0; while (pos < basedata.length()) { int len = 1024; if (pos + len < basedata.length()) logger.finest(basedata.substring(pos, pos + len)); else logger.finest(basedata.substring(pos)); pos += len; } logger.finest("=== END PNG ==="); // destroy window Window mainwnd = AWT.getActiveWindow(); if (mainwnd != null && mainwnd.isVisible()) mainwnd.dispose(); // pass on error if (e instanceof Exception) throw (Exception) e; else if (e instanceof Error) throw (Error) e; else throw new Exception("Unknown throwable: ", e); } finally { // remove screenshot directory again FileUtils.recursiveDelete(shotdir); } }
/** Eliminate redundant key press/release events surrounding a keytyped event. */ private void coalesceKeyEvents() { setStatus("Coalescing key events"); for (int i = 0; i < steps.size(); i++) { Step step = (Step) steps.get(i); if (isKey(step, ANY_KEY, PRESS)) { // In the case of modifiers, remove only if the presence of // the key down/up is redundant. Event se = (Event) step; String cs = se.getAttribute(XMLConstants.TAG_KEYCODE); int code = AWT.getKeyCode(cs); // OSX option modifier should be ignored, since it is used to // generate input method events. boolean isOSXOption = Platform.isOSX() && code == KeyEvent.VK_ALT; if (AWT.isModifier(code) && !isOSXOption) continue; // In the case of non-modifier keys, walk the steps until we // find the key release, then optionally replace the key press // with a keystroke, or remove it if the keystroke was already // recorded. This sorts out jumbled key press/release events. boolean foundKeyStroke = false; boolean foundRelease = false; for (int j = i + 1; j < steps.size(); j++) { Step next = (Step) steps.get(j); // If we find the release, remove it and this if (isKey(next, cs, RELEASE)) { foundRelease = true; String target = ((Event) next).getComponentID(); steps.remove(j); steps.remove(i); // Add a keystroke only if we didn't find any key // input between press and release (except on OSX, // where the option key generates input method events // which aren't recorded). if (!foundKeyStroke && !isOSXOption) { String mods = se.getAttribute(XMLConstants.TAG_MODIFIERS); String[] args = (mods == null || "0".equals(mods) ? new String[] {target, cs} : new String[] {target, cs, mods}); Step typed = new Action(getResolver(), null, "actionKeyStroke", args); steps.add(i, typed); setStatus("Insert artifical " + typed); } else { setStatus("Removed redundant key events (" + cs + ")"); --i; } break; } else if (isKeyStroke(next, ANY_KEY) || isKeyString(next)) { foundKeyStroke = true; // If it's a numpad keycode, use the numpad // keycode instead of the resulting numeric character // keystroke. if (cs.startsWith("VK_NUMPAD")) { foundKeyStroke = false; steps.remove(j--); } } } // We don't like standalone key presses if (!foundRelease) { setStatus("Removed extraneous key press (" + cs + ")"); steps.remove(i--); } } } }
public static void doScreenshots(File shotdir) throws Exception { shotdir.mkdirs(); String prefix = "jgridstart-screenshot-"; // setup temporary environment logger.info("Setting up jGridstart interactive screenshot and testing environment"); File tmphome = FileUtils.createTempDir("jgridstart-home"); Window mainwnd = null; try { System.setProperty("jgridstart.ca.provider", "LocalCA"); System.setProperty("jgridstart.ca.local.hold", "true"); System.setProperty("user.home", tmphome.getCanonicalPath()); // create standard gui nl.nikhef.jgridstart.gui.Main.main(new String[] {}); LogHelper.setupLogging(true); // move mouse here since closing window may give up focus later Thread.sleep(2000); guiSleep(); mainwnd = AWT.getActiveWindow(); assertNotNull(mainwnd); tester.mouseMove(mainwnd.getComponents()[0]); assertWindowname("jgridstart-main-window"); /* * Request new */ logger.info("Interactive testing scenario: Request New"); // start screen saveScreenshot(new File(shotdir, prefix + "newrequest01.png")); // new request wizard guiSleep(); tester.key(new Integer('N'), InputEvent.CTRL_MASK); guiSleep(); saveScreenshot(new File(shotdir, prefix + "newrequest02.png")); // enter details guiSleep(); assertWindowname("jgridstart-requestwizard-0"); focusByName("givenname"); keyString("John\t"); keyString("Doe\t"); keyString("[email protected]\t"); keyString("N\t"); keyString(password + "\t"); keyString(password + "\t"); // wait for submission tester.key(new Integer('N'), InputEvent.ALT_MASK); guiSleep(); saveScreenshot(new File(shotdir, prefix + "newrequest03.png")); assertWindowname("jgridstart-requestwizard-1"); waitEnabled(JButton.class, "Next"); // verification form System.setProperty("wizard.show.help1", "true"); // simulate help btn1 pressed tester.key(new Integer('N'), InputEvent.ALT_MASK); guiSleep(); guiSleep(); saveScreenshot(new File(shotdir, prefix + "newrequest04.png")); assertWindowname("jgridstart-requestwizard-2"); // form display JButton btn = (JButton) new BasicFinder() .find( new Matcher() { public boolean matches(Component c) { return c instanceof JButton && ((JButton) c).getText().startsWith("display form"); } }); btn.doClick(); waitEnabled(JButton.class, "Close"); guiSleep(); saveScreenshot(new File(shotdir, prefix + "newrequest05.png")); assertWindowname("jgridstart-verification-form"); tester.key(new Integer('C'), InputEvent.ALT_MASK); // close wizard guiSleep(); assertWindowname("jgridstart-requestwizard-2"); tester.key(new Integer('C'), InputEvent.ALT_MASK); guiSleep(); saveScreenshot(new File(shotdir, prefix + "newrequest06.png")); assertWindowname("jgridstart-main-window"); // enable certificate in LocalCA and refresh pane System.setProperty("jgridstart.ca.local.hold", "false"); tester.key(KeyEvent.VK_F5); Thread.sleep(1000); guiSleep(); saveScreenshot(new File(shotdir, prefix + "newrequest07.png")); assertWindowname("jgridstart-main-window"); // show request wizard again tester.key(new Integer('A'), InputEvent.ALT_MASK); tester.key('R'); guiSleep(); guiSleep(); saveScreenshot(new File(shotdir, prefix + "newrequest08.png")); assertWindowname("jgridstart-requestwizard-2"); // install step tester.key(new Integer('N'), InputEvent.ALT_MASK); Thread.sleep(1000); guiSleep(); saveScreenshot(new File(shotdir, prefix + "newrequest09.png")); assertWindowname("jgridstart-requestwizard-3"); // show final screen tester.key(new Integer('N'), InputEvent.ALT_MASK); guiSleep(); saveScreenshot(new File(shotdir, prefix + "newrequest10.png")); assertWindowname("jgridstart-requestwizard-4"); // exit wizard tester.key(new Integer('C'), InputEvent.ALT_MASK); // save final screenshot guiSleep(); saveScreenshot(new File(shotdir, prefix + "newrequest11.png")); assertWindowname("jgridstart-main-window"); guiSleep(); /* * Renewal */ logger.info("Interactive testing scenario: Renewal"); System.setProperty("jgridstart.ca.local.hold", "true"); // forget password so we certainly get the password dialog PasswordCache.getInstance().clear(); // start screen guiSleep(); saveScreenshot(new File(shotdir, prefix + "renew01.png")); assertWindowname("jgridstart-main-window"); // personal details tester.key(new Integer('A'), InputEvent.ALT_MASK); tester.key('W'); Thread.sleep(500); guiSleep(); saveScreenshot(new File(shotdir, prefix + "renew02.png")); assertWindowname("jgridstart-requestwizard-0"); focusByName("email"); keyString("\t"); keyString(password + "\t"); keyString(password + "\t"); keyString(password + "\t"); // wait for submission screen tester.key(new Integer('N'), InputEvent.ALT_MASK); // renew03.png used to be a password dialog, which was removed // submit page guiSleep(); saveScreenshot(new File(shotdir, prefix + "renew04.png")); assertWindowname("jgridstart-requestwizard-1"); waitEnabled(JButton.class, "Next"); // wait for approval page tester.key(new Integer('N'), InputEvent.ALT_MASK); guiSleep(); saveScreenshot(new File(shotdir, prefix + "renew05.png")); assertWindowname("jgridstart-requestwizard-2"); // close wizard guiSleep(); tester.key(new Integer('C'), InputEvent.ALT_MASK); guiSleep(); saveScreenshot(new File(shotdir, prefix + "renew06.png")); assertWindowname("jgridstart-main-window"); // enable certificate in LocalCA and refresh pane System.setProperty("jgridstart.ca.local.hold", "false"); tester.key(KeyEvent.VK_F5); Thread.sleep(1000); guiSleep(); saveScreenshot(new File(shotdir, prefix + "renew07.png")); assertWindowname("jgridstart-main-window"); // show request wizard again tester.key(new Integer('A'), InputEvent.ALT_MASK); tester.key('R'); waitEnabled(JButton.class, "Next"); Thread.sleep(500); guiSleep(); saveScreenshot(new File(shotdir, prefix + "renew08.png")); assertWindowname("jgridstart-requestwizard-2"); // install step tester.key(new Integer('N'), InputEvent.ALT_MASK); Thread.sleep(1000); guiSleep(); saveScreenshot(new File(shotdir, prefix + "renew09.png")); assertWindowname("jgridstart-requestwizard-3"); // exit wizard tester.key(new Integer('C'), InputEvent.ALT_MASK); // save final screenshot guiSleep(); saveScreenshot(new File(shotdir, prefix + "renew10.png")); assertWindowname("jgridstart-main-window"); guiSleep(); /* * Import/export */ logger.info("Interactive testing scenario: Import/Export"); // forget password so we certainly get the password dialog PasswordCache.getInstance().clear(); // starting screenshot (multiple certificates) guiSleep(); saveScreenshot(new File(shotdir, prefix + "importexport01.png")); assertWindowname("jgridstart-main-window"); // export dialog tester.key(new Integer('E'), InputEvent.CTRL_MASK); waitEnabled(JButton.class, "Export"); guiSleep(); saveScreenshot(new File(shotdir, prefix + "importexport02.png")); assertWindowname("jgridstart-export-file-dialog"); // enter name and do export tester.keyString("jgridstart_test_certificate.p12\n"); Thread.sleep(2000); saveScreenshot(new File(shotdir, prefix + "importexport03.png")); assertWindowname("jgridstart-password-entry-decrypt"); tester.keyString(password + "\n"); guiSleep(); assertWindowname("jgridstart-main-window"); // forget password so we certainly get the password dialog PasswordCache.getInstance().clear(); // import dialog tester.key(new Integer('I'), InputEvent.CTRL_MASK); waitEnabled(JButton.class, "Import"); guiSleep(); saveScreenshot(new File(shotdir, prefix + "importexport04.png")); assertWindowname("jgridstart-import-file-dialog"); guiSleep(); // enter name and do import tester.keyString("jgridstart_test_certificate.p12\n"); Thread.sleep(1000); saveScreenshot(new File(shotdir, prefix + "importexport05.png")); assertWindowname("jgridstart-password-entry-decrypt"); keyString(password + "\n"); guiSleep(); /* * Certificate details */ logger.info("Interactive testing scenario: Certificate details"); // certificate details view mainwnd.setSize(750, 480); System.setProperty("view.showdetails", "true"); URLLauncherCertificate.performAction("viewlist(false)", mainwnd); tester.key(KeyEvent.VK_F5); Thread.sleep(500); guiSleep(); saveScreenshot(new File(shotdir, prefix + "viewdetails01.png")); assertWindowname("jgridstart-main-window"); /* * Exit! */ logger.info("Interactive testing finished"); /* Quit does a {@link System.exit}, which JUnit doesn't like. The * error it gives is something like: * [junit] Test <testclass> FAILED (crashed) * So we leave it to the calling function to dispose of the window. */ // tester.key(new Integer('Q'), InputEvent.CTRL_MASK); } finally { guiSleep(); Thread.sleep(500); // for screenshot to complete ... FileUtils.recursiveDelete(tmphome); } // exit! return; }