package abbot.tester.swt;
import java.util.ArrayList;
import java.util.List;
import junit.framework.TestCase;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import abbot.Log;
import abbot.finder.matchers.swt.ClassMatcher;
import abbot.finder.matchers.swt.CompositeMatcher;
import abbot.finder.matchers.swt.TextMatcher;
import abbot.finder.swt.BasicFinder;
import abbot.finder.swt.Matcher;
import abbot.finder.swt.MultipleWidgetsFoundException;
import abbot.finder.swt.WidgetFinder;
import abbot.finder.swt.WidgetNotFoundException;
import abbot.script.Condition;
import abbot.swt.utilities.ExceptionHelper;
import abbot.swt.utilities.ScreenCapture;
/** An abstract class to facilitate the testing of dialogs in eclipse.
*
* Currently, this class only supports dialogs which have a title(Shell.getText()).
*
* protected abstract void invokeDialog() throws Throwable;
* protected abstract void doTestDialog() throws Throwable;
* protected abstract void doCloseDialog( boolean ok ) throws Throwable;
*
* Nesting of AbstractDialogTesters IS supported. All dialogs should
* be launched from their respective invokeDialog methods.
* e.g.
*
* AbstractDialogTester one = AbstractDialogTester(firstTitle,_display) {
* invokeDialog() throws Throwable {
* //invoke first dialog
* }
*
* doTestDialog() throws Throwable {
* AbstractDialogTester two = new AbstractDialogTester(secondTitle,_display) {
* protected void invokeDialog) throws Throwable {
* //invoke second dialog
* }
* protected void doTestDialog() throws Throwable { ... }
* protected void doCloseDialog(boolean ok) throws Throwable { //close second dialog }
* };
* //maybe run some tests on first dialog before launching second.
* two.runDialog();
* //maybe run some tests on first dialog after closing second.
* }
*
* doCloseDialog(boolean ok) throws Throwable { //close first dialog }
* }
* one.runDialog(); //run all of the above code.
*
*
*/
public abstract class AbstractDialogTester {
/* @todo: add functionality to set close timeout. */
/* @todo: override toString methods in anonymous Conditions, for better
* exception reporting.
*/
/* @todo: use logging. */
// string used in screenshot filename when we force close a dialog
private static final String FORCE_CLOSE_DIALOG = "force_close_dialog_";
// string used in screenshot filename when we encounter a failure testing a dialog
private static final String FAILURE_TESTING_DIALOG = "failure_testing_dialog_";
public static final int DEFAULT_TIMEOUT_MINUTES = 5;
public static final int DEFAULT_TIMEOUT_CLOSE_SECONDS = 25;
public static final int DEFAULT_TIMEOUT_INVOKEFAILURE_OR_SHELLSHOWING_SECONDS = 60;
protected Display _display;
private String _title;
protected Shell _dialogShell;
private int _timeoutMinutes;
private int _timeoutCloseSeconds;
private int _timeoutForInvokeFailureOrShellShowingSeconds;
private volatile boolean _done = false; // volatile since accessed by 2 threads
protected final WidgetFinder _finder = BasicFinder.getDefault();
protected final ShellTester _shellTester = new ShellTester();
protected final ButtonTester _buttonTester = new ButtonTester();
public AbstractDialogTester(String title, Display display) {
this(title, display, DEFAULT_TIMEOUT_MINUTES);
}
public AbstractDialogTester(String title, Display display,
int timeoutMinutes) {
_display = display;
_title = title;
_timeoutMinutes = timeoutMinutes;
_timeoutCloseSeconds = DEFAULT_TIMEOUT_CLOSE_SECONDS;
_timeoutForInvokeFailureOrShellShowingSeconds = DEFAULT_TIMEOUT_INVOKEFAILURE_OR_SHELLSHOWING_SECONDS;
}
protected void clickButton(Composite root, String label)
throws WidgetNotFoundException, MultipleWidgetsFoundException {
clickButton(root, makeClassAndTextMatcher(Button.class, label));
}
protected void clickButton(String label) throws WidgetNotFoundException,
MultipleWidgetsFoundException {
clickButton(_dialogShell, makeClassAndTextMatcher(Button.class, label));
}
protected void clickButton(final Composite root, final Matcher m)
throws WidgetNotFoundException, MultipleWidgetsFoundException {
final Composite searchRoot = (root == null) ? _dialogShell : root;
final Button btn = (Button) _finder.find(searchRoot, m);
final String label = _buttonTester.getText(btn);
Log.debug(_title + ": Clicking Button..." + label);
final ButtonTester tester = ButtonTester.getButtonTester();
Robot.wait(new Condition() {
public boolean test() {
return tester.getEnabled(btn);
}
//@Override
public String toString() {
return label + " to be enabled";
}
});
_buttonTester.actionClick(btn);
}
protected static Matcher makeClassAndTextMatcher(Class clazz, String text) {
TextMatcher textMatcher = new TextMatcher(text);
ClassMatcher classMatcher = new ClassMatcher(clazz);
CompositeMatcher compMatcher = new CompositeMatcher(new Matcher[] {
textMatcher, classMatcher });
return compMatcher;
}
/**
* This wait is for the Condition that waits on doCloseDialog().
*/
public void setTimeoutCloseSeconds(int timeoutSeconds) {
_timeoutCloseSeconds = timeoutSeconds;
}
/**
* This wait is for the Condition that waits on doTestDialog()
*/
public void setTimeoutMinutes(int timeoutMinutes) {
_timeoutMinutes = timeoutMinutes;
}
/**
* This wait is for the Condition that waits on invokeDialog()
*/
public void setTimeoutForInvokeFailureOrShellShowingSeconds(
int timeoutSeconds) {
_timeoutForInvokeFailureOrShellShowingSeconds = timeoutSeconds;
}
/**
* This method should ONLY invoke the dialog under test.
*
* @throws Throwable
*/
protected abstract void invokeDialog() throws Throwable;
/**
* This method will launch, test, and close the dialog.
*/
public void runDialog() {
//final List<Throwable> throwables = new ArrayList<Throwable>();
final List throwables = new ArrayList();
final TestDialogThread th = new TestDialogThread(_title
+ " TestDialogThread") {
//@Override
public void run() {
try {
Robot.delay(1000); // This pause helps prevent a deadlock with invokeDialog() when the use a find.
waitForInvokeDialogFailureOrShellShowing(_title, this); // a condition which will short-circuit on the shell showing or an invokeDialog failure
if (!_invokeDialogFailed) {
waitForDialogShowing(); //wait for dialog to be ready for test, overridden by awt-swt dialog tester
testDialog();
}
} catch (Throwable t) {
Log.log(t);
throwables.add(t);
} finally {
_done = true;
}
}
};
th.start();
try {
invokeDialog();
} catch (Throwable t) {
Log.log(t);
th.setInvokeFailed();
AssertionError ae = new AssertionError("Failed to invoke Dialog: "
+ _title);
ae.initCause(t);
throw ae;
}
// wait for the dialog to be done
// before timing out, poll each second
// put this wait in a try so we can carry on
// if something goes wrong while we're waiting
try {
Robot.wait(new Condition() {
public boolean test() {
return _done;
}
//@Override
public String toString() {
return _title
+ " did not complete its doTestDialog() method within "
+ _timeoutMinutes + " minutes.";
}
}, _timeoutMinutes * 60000, 1000); // poll every second
} catch (Throwable t) {
Log.log(t);
throwables.add(t);
// try to close the dialog
attemptCloseDialog(throwables);
} finally {
// if there was an error, rethrow it
if (!throwables.isEmpty()) {
AssertionError ae = new AssertionError(
"Failed while testing dialog: " + _title);
ae.initCause(ExceptionHelper.chainThrowables(throwables));
throw ae;
}
}
}
/**
* Convenience wait for a shell to be displayed. This method is like
* waitForFrameShowing, only searches for top-level shells
*/
public static void waitForShellShowing(final String title) {
final WidgetTester w = new WidgetTester();
Robot.wait(new Condition() {
public boolean test() {
return w.assertDecorationsShowing(title, true);
}
public String toString() {
return title + " to show";
}
}, 60000);
}
/*************************************************************************
* Similar to a JUnit test tearDown method, this method is responsible
* for backing out any changes (if the test requires) and closing
* the dialog.
*
* @param boolean ok representing whether everything is ok up to this point or not
*
* - if everything is ok, subclass implementations should close the dialog normally
* e.g. by clicking OK or Finish, etc.
*
* - if everything is NOT ok at this point, subclass implementations should close
* the dialog however they can
* e.g. by clicking cancel, pressing the ESC key, etc.
*
* @throws Throwable
************************************************************************/
protected abstract void doCloseDialog(boolean ok) throws Throwable;
/**
* All 'testing' or dialog manipulation should be done in this method.
*
* @throws Throwable
*/
protected abstract void doTestDialog() throws Throwable;
protected final void testDialog() throws Throwable {
//List<Throwable> throwables = new ArrayList<Throwable>();
List throwables = new ArrayList();
try {
doTestDialog();
} catch (Throwable t) {
// we will rethrow this after closing the dialog
t.printStackTrace();
throwables.add(t);
} finally {
attemptCloseDialog(throwables);
// now package the throwables for rethrowing
if (!throwables.isEmpty()) {
throw ExceptionHelper.chainThrowables(throwables);
}
}
}
//private void attemptCloseDialog( List<Throwable> throwables)
private void attemptCloseDialog(List throwables) {
// Try to gracefully exit
try {
// take a screenshot if there was a failure testing this dialog
if (!throwables.isEmpty()) {
try {
Log
.warn("Taking screen capture b/c of failure while testing the "
+ _title + " dialog.");
//@todo: clean title for filename
ScreenCapture.createScreenCapture(FAILURE_TESTING_DIALOG);
} catch (Throwable t) {
// log error and carry on
Log.log(t);
}
}
// pass in a boolean here indicating whether
// or not there was an error up to this point
doCloseDialog(throwables.isEmpty());
} catch (Throwable t) {
// we will chain this and rethrow it after closing the dialog
Log.log(t);
throwables.add(t);
} finally {
try {
// give the dialog time to close before closing it with brute
// force.
Robot.wait(new Condition() {
public boolean test() {
return (_dialogShell != null && _dialogShell
.isDisposed());
}
//@Override
public String toString() {
return _title + " failed to close within "
+ _timeoutCloseSeconds + " seconds.";
}
}, _timeoutCloseSeconds * 1000, 1000);
} catch (Throwable t) {
Log.log(t);
throwables.add(t);
} finally {
// if the dialog is still around try a brute force close
if (_dialogShell != null && !_dialogShell.isDisposed()) {
Log.warn("trying to brute force close dialog with title: "
+ _title);
// take a screen shot just before brute force closing the
// dialog
// put in a try catch to make sure we continue and close the
// dialog
// even if something goes wrong during the screencap
try {
Log
.warn("Taking screen capture b/c of failure while closing the "
+ _title + " dialog.");
ScreenCapture.createScreenCapture(FORCE_CLOSE_DIALOG);
} catch (Throwable t) {
// log error and carry on
Log.log(t);
}
if (_dialogShell.getDisplay() != null) {
Robot.syncExec(_dialogShell.getDisplay(), this,
new Runnable() {
public void run() {
if (!_dialogShell.isDisposed())
_dialogShell.close();
}
});
}
}
}
}
}
protected void waitForDialogShowing() throws WidgetNotFoundException,
MultipleWidgetsFoundException {
Robot.waitForIdle(_display); //@todo: can we get rid of this waitForIdle?
waitForShellShowing(_title);
_dialogShell = (Shell) _finder.find(makeClassAndTextMatcher(
Shell.class, _title));
TestCase.assertNotNull("Dialog shell is showing, but was not found: "
+ _title, _dialogShell);
_shellTester.actionFocus(_dialogShell);
}
private void waitForInvokeDialogFailureOrShellShowing(final String title,
final TestDialogThread runner) {
final WidgetTester w = new WidgetTester();
Robot.wait(new Condition() {
public boolean test() {
if (runner != null && runner._invokeDialogFailed) {
return true;
} else {
return w.assertDecorationsShowing(title, true);
}
}
//@Override
public String toString() {
return title + " to show";
}
}, _timeoutForInvokeFailureOrShellShowingSeconds * 1000, 1000);
}
private class TestDialogThread extends Thread {
protected boolean _invokeDialogFailed = false;
public TestDialogThread(String name) {
super(name);
}
public void setInvokeFailed() {
System.err.println("invokeDialog() FAILED for: " + _title);
_invokeDialogFailed = true;
}
}
}