/******************************************************************************* * Copyright (c) 2012 BREDEX GmbH. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * BREDEX GmbH - initial API and implementation *******************************************************************************/ package org.eclipse.jubula.rc.common.tester; import java.awt.AWTException; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.HeadlessException; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import javax.imageio.ImageIO; import org.eclipse.jubula.rc.common.driver.ClickOptions; import org.eclipse.jubula.rc.common.driver.IRobot; import org.eclipse.jubula.rc.common.driver.KeyTyper; import org.eclipse.jubula.rc.common.exception.ExecutionEvent; import org.eclipse.jubula.rc.common.exception.OsNotSupportedException; import org.eclipse.jubula.rc.common.exception.RobotException; import org.eclipse.jubula.rc.common.exception.StepExecutionException; import org.eclipse.jubula.rc.common.logger.AutServerLogger; import org.eclipse.jubula.rc.common.tester.interfaces.ITester; import org.eclipse.jubula.rc.common.util.Comparer; import org.eclipse.jubula.rc.common.util.KeyStrokeUtil; import org.eclipse.jubula.rc.common.util.Verifier; import org.eclipse.jubula.toolkit.enums.ValueSets; import org.eclipse.jubula.tools.internal.constants.StringConstants; import org.eclipse.jubula.tools.internal.objects.event.EventFactory; import org.eclipse.jubula.tools.internal.objects.event.TestErrorEvent; import org.eclipse.jubula.tools.internal.utils.ExternalCommandExecutor; import org.eclipse.jubula.tools.internal.utils.ExternalCommandExecutor.MonitorTask; import org.eclipse.jubula.tools.internal.utils.TimeUtil; /** * @author BREDEX GmbH */ public abstract class AbstractApplicationTester implements ITester { /** * String for sequential numbering for screenshots */ public static final String RENAME = "rename"; //$NON-NLS-1$ /** * String for overwriting for screenshots */ public static final String OVERWRITE = "overwrite"; //$NON-NLS-1$ /** * The default format to use when writing images to disk. */ protected static final String DEFAULT_IMAGE_FORMAT = "png"; //$NON-NLS-1$ /** * The string used to separate filename and file extension. */ protected static final String EXTENSION_SEPARATOR = "."; //$NON-NLS-1$ /** * The logging. */ private static AutServerLogger log = new AutServerLogger(AbstractApplicationTester.class); /** * @param text text to type */ public void rcInputText(String text) { getRobot().type(getFocusOwner(), text); } /** * Executes the given command and waits for it to finish. If the * execution does not finish in good time, a timeout will occur. If the * exit code for the execution is not the same as the expected code, the * test step fails. * * @param cmd The command to execute. * @param expectedExitCode The expected exit code of the command. * @param local <code>true</code> if the command should be executed on the * local (client) machine. Otherwise (should run on the server side), * this value should be <code>false</code>. * @param timeout The amount of time (in milliseconds) to wait for the * execution to finish. * @return the output and error from the command */ public String rcExecuteExternalCommand(String cmd, int expectedExitCode, boolean local, int timeout) { if (!local) { MonitorTask mt = new ExternalCommandExecutor().executeCommand( null, cmd, timeout); if (!mt.wasCmdValid()) { throw new StepExecutionException( "Command not found.", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.NO_SUCH_COMMAND)); } String output = mt.getOutput(); if (mt.hasTimeoutOccurred()) { TestErrorEvent event = EventFactory.createActionError( TestErrorEvent.CONFIRMATION_TIMEOUT); event.addProp(TestErrorEvent.Property.COMMAND_LOG_KEY, output); throw new StepExecutionException( "Timeout received before completing execution of script.", //$NON-NLS-1$ event); } int actualExitValue = mt.getExitCode(); if (actualExitValue != expectedExitCode) { TestErrorEvent event = EventFactory.createVerifyFailed( String.valueOf(expectedExitCode), String.valueOf(actualExitValue)); event.addProp(TestErrorEvent.Property.COMMAND_LOG_KEY, output); throw new StepExecutionException( "Verification of exit code failed.", //$NON-NLS-1$ event); } return output; } return null; } /** * {@inheritDoc} */ public void setComponent(Object graphicsComponent) { // Do nothing; Application has no corresponding component } /** * Takes a screenshot and saves the image to disk. * * @param destination * Path and filename for the created image. If the extension is not * ".jpeg" (case-insensitive), ".jpeg" will be appended to the * filename. * @param delay * Amount of time to wait (in milliseconds) before taking the * screenshot. * @param fileAccess * Determines how the file will be created if a file with the * given name and path already exists:<br> * <code>SwingApplicationImplClass.RENAME</code> - * The screenshot will be saved with a sequential integer appended * to the filename.<br> * <code>SwingApplicationImplClass.OVERWRITE</code> - * The screenshot will overwrite the file. * @param scaling * Degree to which the image should be scaled, in percent. A * <code>scaling</code> value of <code>100</code> produces an * unscaled image. This value must be greater than <code>0</code> * and less than or equal to <code>200</code>. * @param createDirs * Determines whether a path will be created if it does not already * exist. A value of <code>true</code> means that all necessary * directories that do not exist will be created automatically. */ public void rcTakeScreenshot(String destination, int delay, String fileAccess, int scaling, boolean createDirs) { // Determine current screen size Toolkit toolkit = Toolkit.getDefaultToolkit(); Dimension screenSize = toolkit.getScreenSize(); // If screen !(resolution%2==0) --> bad scaling int screenWidth = (int)screenSize.getWidth(); int screenHeight = (int)screenSize.getHeight(); if (!(screenWidth % 2 == 0)) { screenWidth = screenWidth - 1; } if (!(screenHeight % 2 == 0)) { screenHeight = screenHeight - 1; } screenSize.setSize(screenWidth, screenHeight); Rectangle screenRect = new Rectangle(screenSize); takeScreenshot(destination, delay, fileAccess, scaling, createDirs, screenRect); } /** * Takes a screenshot and saves the image to disk. * * @param destination * Path and filename for the created image. If the extension is * not ".jpeg" (case-insensitive), ".jpeg" will be appended to * the filename. * @param delay * Amount of time to wait (in milliseconds) before taking the * screenshot. * @param fileAccess * Determines how the file will be created if a file with the * given name and path already exists:<br> * <code>SwingApplicationImplClass.RENAME</code> - The screenshot * will be saved with a sequential integer appended to the * filename.<br> * <code>SwingApplicationImplClass.OVERWRITE</code> - The * screenshot will overwrite the file. * @param scaling * Degree to which the image should be scaled, in percent. A * <code>scaling</code> value of <code>100</code> produces an * unscaled image. This value must be greater than <code>0</code> * and less than or equal to <code>200</code>. * @param createDirs * Determines whether a path will be created if it does not * already exist. A value of <code>true</code> means that all * necessary directories that do not exist will be created * automatically. * @param marginTop * the extra top margin * @param marginRight * the extra right margin * @param marginBottom * the extra bottom margin * @param marginLeft * the extra left margin */ public void rcTakeScreenshotOfActiveWindow(String destination, int delay, String fileAccess, int scaling, boolean createDirs, int marginTop, int marginRight, int marginBottom, int marginLeft) { Rectangle activeWindowBounds = getActiveWindowBounds(); if (activeWindowBounds == null) { throw new StepExecutionException("No active window found", //$NON-NLS-1$ EventFactory .createActionError(TestErrorEvent.NO_ACTIVE_WINDOW)); } int x = activeWindowBounds.x - marginLeft; int y = activeWindowBounds.y - marginTop; int width = activeWindowBounds.width + marginLeft + marginRight; int height = activeWindowBounds.height + marginTop + marginBottom; if (width < 1 || height < 1) { throw new StepExecutionException("Margin parameter lead to negative height or width", //$NON-NLS-1$ EventFactory .createActionError(TestErrorEvent.INVALID_INPUT)); } Rectangle screenRect = new Rectangle(x, y, width, height); takeScreenshot(destination, delay, fileAccess, scaling, createDirs, screenRect); } /** * @return an awt rectangle which represents the absolute active window * bounds; may return null e.g. if no active window could be found */ public abstract Rectangle getActiveWindowBounds(); /** * Takes a screenshot and saves the image to disk, in JPEG format. * * @param destination * Path and filename for the created image. If the extension is not * ".jpeg" (case-insensitive), ".jpeg" will be appended to the * filename. * @param delay * Amount of time to wait (in milliseconds) before taking the * screenshot. * @param fileAccess * Determines how the file will be created if a file with the * given name and path already exists:<br> * <code>SwingApplicationImplClass.RENAME</code> - * The screenshot will be saved with a sequential integer appended * to the filename.<br> * <code>SwingApplicationImplClass.OVERWRITE</code> - * The screenshot will overwrite the file. * @param scaling * Degree to which the image should be scaled, in percent. A * <code>scaling</code> value of <code>100</code> produces an * unscaled image. This value must be greater than <code>0</code> * and less than or equal to <code>200</code>. * @param createDirs * Determines whether a path will be created if it does not already * exist. A value of <code>true</code> means that all necessary * directories that do not exist will be created automatically. * @param screenShotRect * the rectangle to take the screenshot of */ private void takeScreenshot(String destination, int delay, String fileAccess, int scaling, boolean createDirs, Rectangle screenShotRect) { if (scaling <= 0 || scaling > 200) { throw new StepExecutionException( "Invalid scaling factor: Must be between 1 and 200", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.INVALID_PARAM_VALUE)); } double scaleFactor = scaling * 0.01; // Check if file name is valid String outFileName = destination; String imageExtension = getExtension(outFileName); if (imageExtension.length() == 0) { // If not, then we simply append the default extension imageExtension = DEFAULT_IMAGE_FORMAT; outFileName += EXTENSION_SEPARATOR + imageExtension; } // Wait for a user-specified time if (delay > 0) { TimeUtil.delay(delay); } // Create path, if necessary File pic = new File(outFileName); if (pic.getParent() == null) { throw new StepExecutionException( "Invalid file name: specify a file name", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.INVALID_PARAM_VALUE)); } File path = new File(pic.getParent()); if (createDirs && !path.exists() && !path.mkdirs()) { throw new StepExecutionException( "Directory path does not exist and could not be created", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.FILE_IO_ERROR)); } // Rename file if file already exists // FIXME zeb This naming scheme can lead to sorting problems when // filenames have varying numbers of digits (ex. "pic_9" and // "pic_10") if (fileAccess.equals(RENAME)) { String completeExtension = EXTENSION_SEPARATOR + imageExtension.toLowerCase(); int extensionIndex = pic.getName().toLowerCase().lastIndexOf(completeExtension); String fileName = pic.getName().substring(0, extensionIndex); for (int i = 1; pic.exists(); i++) { pic = new File(pic.getParent(), fileName + "_" + i + completeExtension); //$NON-NLS-1$ } } takeScreenshot(screenShotRect, scaleFactor, pic); } /** * Takes a screenshot and saves the image to disk. This method will attempt * to encode the image according to the file extension of the given * output file. If this is not possible (because the encoding type * is not supported), then the default encoding type will be used. If * the default encoding type is used, an appropriate extension will be added * to the filename. * * @param captureRect * Rect to capture in screen coordinates. * @param scaleFactor * Degree to which the image should be scaled, in percent. A * <code>scaleFactor</code> of <code>100</code> produces an * unscaled image. This value must be greater than <code>0</code> * and less than or equal to <code>200</code>. * @param outputFile * Path and filename for the created image. */ public void takeScreenshot( Rectangle captureRect, double scaleFactor, File outputFile) { // Create screenshot java.awt.Robot robot; File out = outputFile; try { robot = new java.awt.Robot(); BufferedImage image = robot.createScreenCapture(captureRect); int scaledWidth = (int) Math.floor(image.getWidth() * scaleFactor); int scaledHeight = (int) Math.floor(image.getHeight() * scaleFactor); BufferedImage imageOut = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB); // Scale it to the new size on-the-fly Graphics2D graphics2D = imageOut.createGraphics(); graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); graphics2D.drawImage(image, 0, 0, scaledWidth, scaledHeight, null); // Save captured image using given format, if supported. String extension = getExtension(out.getName()); if (extension.length() == 0 || !ImageIO.getImageWritersBySuffix(extension).hasNext() || !ImageIO.write(imageOut, extension, out)) { // Otherwise, save using default format out = new File(outputFile.getPath() + EXTENSION_SEPARATOR + DEFAULT_IMAGE_FORMAT); if (!ImageIO.write(imageOut, DEFAULT_IMAGE_FORMAT, out)) { // This should never happen, so log as error if it does. // In this situation, the screenshot will not be saved, but // the test step will still be marked as successful. log.error("Screenshot could not be saved. " + //$NON-NLS-1$ "Default image format (" + DEFAULT_IMAGE_FORMAT //$NON-NLS-1$ + ") is not supported."); //$NON-NLS-1$ } } } catch (AWTException e) { throw new RobotException(e); } catch (IOException e) { throw new StepExecutionException( "Screenshot could not be saved", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.FILE_IO_ERROR)); } } /** * Waits a specified time. * @param timeMilliSec the time to wait in MilliSec */ public void rcWait(int timeMilliSec) { TimeUtil.delay(timeMilliSec); } /** * shows a ConfirmDialog and Pause the Execution of the Test until Window * is closed */ public void rcPause() { throw new ExecutionEvent(ExecutionEvent.PAUSE_EXECUTION); } /** * Does nothing! The restart is implemented in the client but the server * must have an action to execute. */ public void rcRestart() { // nothing } /** * Does nothing! The prepare for shutdown is implemented in the client but * the server must have an action to execute. */ public void rcPrepareForShutdown() { // nothing } /** * Does nothing! The sync shutdown and restart is implemented in the client * but the server must have an action to execute. * * @param timeout * the timeout to use */ public void rcSyncShutdownAndRestart(int timeout) { // nothing } /** * Types the given text without checking location or event confirmation. * * @param text The text to type. */ public void rcNativeInputText(String text) { try { KeyTyper.getInstance().nativeTypeString(text); } catch (AWTException e) { throw new RobotException(e); } } /** * Action to perform a manual test step on server side; opens a window and * wait's for real user interaction * * @param actionToPerform * a textual description of the action to perform in the AUT * @param expectedBehavior * a textual description of the expected behavior * @param timeout * the timeout */ public void rcManualTestStep(String actionToPerform, String expectedBehavior, int timeout) { // empty implementation: implementation can be found in the corresponding // postExecutionCommand } /** * Perform a keystroke specified according <a * href=http://java.sun.com/j2se/1.4.2/docs/api/javax/swing/KeyStroke.html#getKeyStroke(java.lang.String)> * string representation of a keystroke </a>. * This method does not wait for event confirmation, as we have no way of * confirming events on OS-native widgets. * * @param modifierSpec the string representation of the modifiers * @param keySpec the string representation of the key */ public void rcNativeKeyStroke(String modifierSpec, String keySpec) { if (keySpec == null || keySpec.trim().length() == 0) { throw new StepExecutionException("The base key of the key stroke " //$NON-NLS-1$ + "must not be null or empty", //$NON-NLS-1$ EventFactory.createActionError()); } try { KeyTyper typer = KeyTyper.getInstance(); String keyStrokeSpec = keySpec.trim().toUpperCase(); String mod = KeyStrokeUtil.getModifierString(modifierSpec); if (mod.length() > 0) { keyStrokeSpec = mod + " " + keyStrokeSpec; //$NON-NLS-1$ } typer.type(keyStrokeSpec, null, null, null); } catch (AWTException e) { throw new RobotException(e); } } /** * Action to set the value of a variable in the Client. * * @param variable The name of the variable. * @param value The new value for the variable. * @return the new value for the variable. */ public String rcSetValue(String variable, String value) { return value; } /** * @return The Robot instance */ protected abstract IRobot getRobot(); /** * * @param filename A filename for which to find the extension. * @return the file extension for the given filename. For example, * "png" for "example.png". Returns an empty string if the given * name has no extension. This is the case if the given name does * not contain an instance of the extension separator or ends with * the extension separator. For example, "example" or "example.". */ protected String getExtension(String filename) { File file = new File(filename); int extensionIndex = file.getName().lastIndexOf(EXTENSION_SEPARATOR); return extensionIndex == -1 || extensionIndex == file.getName().length() - 1 ? StringConstants.EMPTY : file.getName().substring(extensionIndex + 1); } /** * method to copy a string to the system clipboard * * @param text The text to copy */ public void rcCopyToClipboard(final String text) { StringSelection strSel = new StringSelection(text); try { Toolkit.getDefaultToolkit().getSystemClipboard() .setContents(strSel, null); } catch (IllegalStateException ise) { throw new StepExecutionException( "Clipboard not available.", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.CLIPBOARD_NOT_AVAILABLE)); } catch (HeadlessException he) { throw new StepExecutionException( "Clipboard not available.", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.CLIPBOARD_NOT_AVAILABLE)); } } /** * method to compare a string to the system clipboard * * @param operator * the comparison method * @param text * the text for comparison */ public void rcCheckClipboard(final String operator, final String text) { String content = null; try { content = (String) Toolkit.getDefaultToolkit().getSystemClipboard() .getData(DataFlavor.stringFlavor); } catch (IllegalStateException ise) { throw new StepExecutionException( "Clipboard not available.", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.CLIPBOARD_NOT_AVAILABLE)); } catch (HeadlessException he) { throw new StepExecutionException( "Clipboard not available.", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.CLIPBOARD_NOT_AVAILABLE)); } catch (UnsupportedFlavorException ufe) { throw new StepExecutionException( "Unsupported Clipboard content.", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.CLIPBOARD_UNSUPPORTED_FLAVOR)); } catch (IOException ioe) { throw new StepExecutionException( "Clipboard could not be compared.", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.CLIPBOARD_IO_ERROR)); } Verifier.match(content, text, operator); } /** * method to compare to values * * @param value1 * the first value for comparison * @param comparisonMethod * the comparison method * @param value2 * the second value for comparison */ public void rcCheckValues(final String value1, final String comparisonMethod, final String value2) { Comparer.compare(value1, value2, comparisonMethod); } /** * method to compare to strings * * @param value1 * the first value for comparison * @param operator * the comparison method * @param value2 * the second value for comparison */ public void rcCheckStringValues(final String value1, final String operator, final String value2) { Verifier.match(value1, value2, operator); } /** * Does nothing! The start timer is implemented in the client but the server * must have an action to execute. * @param timerName the name for the timer * @param variableName the variable name to store the current time in millisecs in */ public void rcStartTimer(String timerName, String variableName) { // empty } /** * Does nothing! The read timer is implemented in the client but the server * must have an action to execute. * @param timerName the name for the timer * @param variableName the variable name to store the current time delta in millisecs in */ public void rcReadTimer(String timerName, String variableName) { // empty } /** * activate the AUT * * @param method activation method */ public void rcActivate(String method) { getRobot().activateApplication(method); } /** * clicks into the active window. * * @param count amount of clicks * @param button what mouse button should be used * @param xPos what x position * @param xUnits should x position be pixel or percent values * @param yPos what y position * @param yUnits should y position be pixel or percent values * @throws StepExecutionException error */ public void rcClickDirect(int count, int button, int xPos, String xUnits, int yPos, String yUnits) throws StepExecutionException { Object activeWindow = getActiveWindow(); if (activeWindow != null) { getRobot().click(activeWindow, null, ClickOptions.create() .setClickCount(count) .setConfirmClick(false) .setMouseButton(button), xPos, xUnits.equalsIgnoreCase(ValueSets.Unit.pixel.rcValue()), yPos, yUnits.equalsIgnoreCase(ValueSets.Unit.pixel.rcValue())); } else { throw new StepExecutionException("No active window.", //$NON-NLS-1$ EventFactory.createActionError( TestErrorEvent.NO_ACTIVE_WINDOW)); } } /** * Just a server side method, not usable as action. * * @param keyCode The key code */ public void rcKeyType(int keyCode) { getRobot().keyType(null, keyCode); } /** * Just a server side method, not usable as action. * * note : this action only works if application got focus, * because using defaultToolkit does not work. You have to * use component.getToolKit()s * @param key to set * numlock Num Lock 1 * caplock Caps Lock 2 * scolllock Scroll 3 * @param activated * boolean */ public void rcToggle(int key, boolean activated) { int event = getEventCode(key); if (event != 0) { try { getRobot().keyToggle(getFocusOwner(), event, activated); } catch (UnsupportedOperationException usoe) { throw new StepExecutionException( TestErrorEvent.UNSUPPORTED_OPERATION_ERROR, EventFactory.createActionError( TestErrorEvent.UNSUPPORTED_OPERATION_ERROR)); } catch (OsNotSupportedException e) { throw new StepExecutionException( TestErrorEvent.UNSUPPORTED_OPERATION_ERROR, EventFactory.createActionError( TestErrorEvent.UNSUPPORTED_OPERATION_ERROR)); } } } /** * perform a keystroke * @param modifierSpec the string representation of the modifiers * @param keySpec the string representation of the key */ public abstract void rcKeyStroke(String modifierSpec, String keySpec); /** * * @return the Focus Owner */ protected abstract Object getFocusOwner(); /** * * @param key to set * numlock Num Lock 1 * caplock Caps Lock 2 * scolllock Scroll 3 * @return the toolkit specific eventcode */ protected abstract int getEventCode(int key); /** * * @return The active application window, or <code>null</code> if no * application window is currently active. */ protected abstract Object getActiveWindow(); }