8294156: Allow PassFailJFrame.Builder to create test UI
Reviewed-by: azvegint, prr
This commit is contained in:
parent
14090ef603
commit
42b9ac8a07
@ -29,19 +29,23 @@ import java.awt.GraphicsDevice;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.awt.Image;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.Robot;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.Window;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowListener;
|
||||
import java.awt.image.RenderedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@ -63,9 +67,83 @@ import javax.swing.text.JTextComponent;
|
||||
import javax.swing.text.html.HTMLEditorKit;
|
||||
import javax.swing.text.html.StyleSheet;
|
||||
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
import static javax.swing.SwingUtilities.invokeAndWait;
|
||||
import static javax.swing.SwingUtilities.isEventDispatchThread;
|
||||
|
||||
/**
|
||||
* Provides a framework for manual tests to display test instructions and
|
||||
* Pass/Fail buttons.
|
||||
* <p>
|
||||
* Instructions for the user can be either plain text or HTML as supported
|
||||
* by Swing. If the instructions start with {@code <html>}, the
|
||||
* instructions are displayed as HTML.
|
||||
* <p>
|
||||
* A simple test would look like this:
|
||||
* <pre>{@code
|
||||
* public class SampleManualTest {
|
||||
* private static final String INSTRUCTIONS =
|
||||
* "Click Pass, or click Fail if the test failed.";
|
||||
*
|
||||
* public static void main(String[] args) throws Exception {
|
||||
* PassFailJFrame.builder()
|
||||
* .instructions(INSTRUCTIONS)
|
||||
* .testUI(() -> createTestUI())
|
||||
* .build()
|
||||
* .awaitAndCheck();
|
||||
* }
|
||||
*
|
||||
* private static List<Window> createTestUI() {
|
||||
* JFrame testUI = new JFrame("Test UI");
|
||||
* testUI.setSize(250, 150);
|
||||
* return List.of(testUI);
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
* <p>
|
||||
* The above example uses the {@link Builder Builder} to set the parameters of
|
||||
* the instruction frame. It is the recommended way.
|
||||
* <p>
|
||||
* The framework will create instruction UI, it will call
|
||||
* the provided {@code createTestUI} on the Event Dispatch Thread (EDT),
|
||||
* and it will automatically position the test UI and make it visible.
|
||||
* <p>
|
||||
* Alternatively, use one of the {@code PassFailJFrame} constructors to
|
||||
* create an object, then create secondary test UI, register it
|
||||
* with {@code PassFailJFrame}, position it and make it visible.
|
||||
* The following sample demonstrates it:
|
||||
* <pre>{@code
|
||||
* public class SampleOldManualTest {
|
||||
* private static final String INSTRUCTIONS =
|
||||
* "Click Pass, or click Fail if the test failed.";
|
||||
*
|
||||
* public static void main(String[] args) throws Exception {
|
||||
* PassFailJFrame passFail = new PassFailJFrame(INSTRUCTIONS);
|
||||
*
|
||||
* SwingUtilities.invokeAndWait(() -> createTestUI());
|
||||
*
|
||||
* passFail.awaitAndCheck();
|
||||
* }
|
||||
*
|
||||
* private static void createTestUI() {
|
||||
* JFrame testUI = new JFrame("Test UI");
|
||||
* testUI.setSize(250, 150);
|
||||
* PassFailJFrame.addTestWindow(testUI);
|
||||
* PassFailJFrame.positionTestWindow(testUI, PassFailJFrame.Position.HORIZONTAL);
|
||||
* testUI.setVisible(true);
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
* <p>
|
||||
* Use methods of the {@code Builder} class or constructors of the
|
||||
* {@code PassFailJFrame} class to control other parameters:
|
||||
* <ul>
|
||||
* <li>the title of the instruction UI,</li>
|
||||
* <li>the timeout of the test,</li>
|
||||
* <li>the size of the instruction UI via rows and columns, and</li>
|
||||
* <li>to enable screenshots.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class PassFailJFrame {
|
||||
|
||||
private static final String TITLE = "Test Instruction Frame";
|
||||
@ -172,21 +250,67 @@ public class PassFailJFrame {
|
||||
*/
|
||||
public PassFailJFrame(String title, String instructions, long testTimeOut,
|
||||
int rows, int columns,
|
||||
boolean enableScreenCapture) throws InterruptedException,
|
||||
InvocationTargetException {
|
||||
if (isEventDispatchThread()) {
|
||||
createUI(title, instructions, testTimeOut, rows, columns,
|
||||
enableScreenCapture);
|
||||
} else {
|
||||
invokeAndWait(() -> createUI(title, instructions, testTimeOut,
|
||||
rows, columns, enableScreenCapture));
|
||||
}
|
||||
boolean enableScreenCapture)
|
||||
throws InterruptedException, InvocationTargetException {
|
||||
invokeOnEDT(() -> createUI(title, instructions,
|
||||
testTimeOut,
|
||||
rows, columns,
|
||||
enableScreenCapture));
|
||||
}
|
||||
|
||||
private PassFailJFrame(Builder builder) throws InterruptedException,
|
||||
InvocationTargetException {
|
||||
this(builder.title, builder.instructions, builder.testTimeOut,
|
||||
builder.rows, builder.columns, builder.screenCapture);
|
||||
builder.rows, builder.columns, builder.screenCapture);
|
||||
|
||||
if (builder.windowCreator != null) {
|
||||
invokeOnEDT(() ->
|
||||
builder.testWindows = builder.windowCreator.createTestUI());
|
||||
}
|
||||
|
||||
if (builder.testWindows != null) {
|
||||
addTestWindow(builder.testWindows);
|
||||
builder.testWindows
|
||||
.forEach(w -> w.addWindowListener(windowClosingHandler));
|
||||
|
||||
if (builder.positionWindows != null) {
|
||||
positionInstructionFrame(builder.position);
|
||||
invokeOnEDT(() -> {
|
||||
builder.positionWindows
|
||||
.positionTestWindows(unmodifiableList(builder.testWindows),
|
||||
builder.instructionUIHandler);
|
||||
|
||||
windowList.forEach(w -> w.setVisible(true));
|
||||
});
|
||||
} else if (builder.testWindows.size() == 1) {
|
||||
Window window = builder.testWindows.get(0);
|
||||
positionTestWindow(window, builder.position);
|
||||
window.setVisible(true);
|
||||
} else {
|
||||
positionTestWindow(null, builder.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an operation on EDT. If called on EDT, invokes {@code run}
|
||||
* directly, otherwise wraps into {@code invokeAndWait}.
|
||||
*
|
||||
* @param doRun an operation to run on EDT
|
||||
* @throws InterruptedException if we're interrupted while waiting for
|
||||
* the event dispatching thread to finish executing
|
||||
* {@code doRun.run()}
|
||||
* @throws InvocationTargetException if an exception is thrown while
|
||||
* running {@code doRun}
|
||||
* @see javax.swing.SwingUtilities#invokeAndWait(Runnable)
|
||||
*/
|
||||
private static void invokeOnEDT(Runnable doRun)
|
||||
throws InterruptedException, InvocationTargetException {
|
||||
if (isEventDispatchThread()) {
|
||||
doRun.run();
|
||||
} else {
|
||||
invokeAndWait(doRun);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createUI(String title, String instructions,
|
||||
@ -241,16 +365,7 @@ public class PassFailJFrame {
|
||||
buttonsPanel.add(createCapturePanel());
|
||||
}
|
||||
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
super.windowClosing(e);
|
||||
testFailedReason = FAILURE_REASON
|
||||
+ "User closed the instruction Frame";
|
||||
failed = true;
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
frame.addWindowListener(windowClosingHandler);
|
||||
|
||||
frame.add(buttonsPanel, BorderLayout.SOUTH);
|
||||
frame.pack();
|
||||
@ -284,6 +399,101 @@ public class PassFailJFrame {
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates one or more windows for test UI.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface WindowCreator {
|
||||
/**
|
||||
* Creates one or more windows for test UI.
|
||||
* This method is called by the framework on the EDT.
|
||||
* @return a list of windows.
|
||||
*/
|
||||
List<? extends Window> createTestUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions test UI windows.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface PositionWindows {
|
||||
/**
|
||||
* Positions test UI windows.
|
||||
* This method is called by the framework on the EDT after
|
||||
* the instruction UI frame was positioned on the screen.
|
||||
* <p>
|
||||
* The list of the test windows contains the windows
|
||||
* that were passed to the framework via
|
||||
* {@link Builder#testUI(WindowCreator) testUI} method.
|
||||
*
|
||||
* @param testWindows the list of test windows
|
||||
* @param instructionUI information about the instruction frame
|
||||
*/
|
||||
void positionTestWindows(List<? extends Window> testWindows,
|
||||
InstructionUI instructionUI);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides information about the instruction frame.
|
||||
*/
|
||||
public interface InstructionUI {
|
||||
/**
|
||||
* {@return the location of the instruction frame}
|
||||
*/
|
||||
Point getLocation();
|
||||
|
||||
/**
|
||||
* {@return the size of the instruction frame}
|
||||
*/
|
||||
Dimension getSize();
|
||||
|
||||
/**
|
||||
* {@return the bounds of the instruction frame}
|
||||
*/
|
||||
Rectangle getBounds();
|
||||
|
||||
/**
|
||||
* Allows to change the location of the instruction frame.
|
||||
*
|
||||
* @param location the new location of the instruction frame
|
||||
*/
|
||||
void setLocation(Point location);
|
||||
|
||||
/**
|
||||
* Allows to change the location of the instruction frame.
|
||||
*
|
||||
* @param x the <i>x</i> coordinate of the new location
|
||||
* @param y the <i>y</i> coordinate of the new location
|
||||
*/
|
||||
void setLocation(int x, int y);
|
||||
|
||||
/**
|
||||
* Returns the specified position that was used to set
|
||||
* the initial location of the instruction frame.
|
||||
*
|
||||
* @return the specified position
|
||||
*
|
||||
* @see Position
|
||||
*/
|
||||
Position getPosition();
|
||||
}
|
||||
|
||||
|
||||
private static final class WindowClosingHandler extends WindowAdapter {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
testFailedReason = FAILURE_REASON
|
||||
+ "User closed a window";
|
||||
failed = true;
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
private static final WindowListener windowClosingHandler =
|
||||
new WindowClosingHandler();
|
||||
|
||||
|
||||
private static JComponent createCapturePanel() {
|
||||
JComboBox<CaptureType> screenShortType = new JComboBox<>(CaptureType.values());
|
||||
|
||||
@ -409,13 +619,11 @@ public class PassFailJFrame {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose all the window(s) i,e both the test instruction frame and
|
||||
* the window(s) that is added via addTestWindow(Window testWindow)
|
||||
* Disposes of all the windows. It disposes of the test instruction frame
|
||||
* and all other windows added via {@link #addTestWindow(Window)}.
|
||||
*/
|
||||
private static synchronized void disposeWindows() {
|
||||
for (Window win : windowList) {
|
||||
win.dispose();
|
||||
}
|
||||
windowList.forEach(Window::dispose);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -450,6 +658,36 @@ public class PassFailJFrame {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
private static void positionInstructionFrame(final Position position) {
|
||||
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
|
||||
|
||||
// Get the screen insets to position the frame by taking into
|
||||
// account the location of taskbar or menu bar on screen.
|
||||
GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment()
|
||||
.getDefaultScreenDevice()
|
||||
.getDefaultConfiguration();
|
||||
Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc);
|
||||
|
||||
switch (position) {
|
||||
case HORIZONTAL:
|
||||
int newX = ((screenSize.width / 2) - frame.getWidth());
|
||||
frame.setLocation((newX + screenInsets.left),
|
||||
(frame.getY() + screenInsets.top));
|
||||
break;
|
||||
|
||||
case VERTICAL:
|
||||
int newY = ((screenSize.height / 2) - frame.getHeight());
|
||||
frame.setLocation((frame.getX() + screenInsets.left),
|
||||
(newY + screenInsets.top));
|
||||
break;
|
||||
|
||||
case TOP_LEFT_CORNER:
|
||||
frame.setLocation(screenInsets.left, screenInsets.top);
|
||||
break;
|
||||
}
|
||||
syncLocationToWindowManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximately positions the instruction frame relative to the test
|
||||
* window as specified by the {@code position} parameter. If {@code testWindow}
|
||||
@ -480,40 +718,23 @@ public class PassFailJFrame {
|
||||
* </ul>
|
||||
*/
|
||||
public static void positionTestWindow(Window testWindow, Position position) {
|
||||
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
|
||||
positionInstructionFrame(position);
|
||||
|
||||
// Get the screen insets to position the frame by taking into
|
||||
// account the location of taskbar/menubars on screen.
|
||||
GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment()
|
||||
.getDefaultScreenDevice().getDefaultConfiguration();
|
||||
Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc);
|
||||
if (testWindow != null) {
|
||||
switch (position) {
|
||||
case HORIZONTAL:
|
||||
case TOP_LEFT_CORNER:
|
||||
testWindow.setLocation((frame.getX() + frame.getWidth() + 5),
|
||||
frame.getY());
|
||||
break;
|
||||
|
||||
if (position.equals(Position.HORIZONTAL)) {
|
||||
int newX = ((screenSize.width / 2) - frame.getWidth());
|
||||
frame.setLocation((newX + screenInsets.left),
|
||||
(frame.getY() + screenInsets.top));
|
||||
syncLocationToWindowManager();
|
||||
if (testWindow != null) {
|
||||
testWindow.setLocation((frame.getX() + frame.getWidth() + 5),
|
||||
frame.getY());
|
||||
}
|
||||
} else if (position.equals(Position.VERTICAL)) {
|
||||
int newY = ((screenSize.height / 2) - frame.getHeight());
|
||||
frame.setLocation((frame.getX() + screenInsets.left),
|
||||
(newY + screenInsets.top));
|
||||
syncLocationToWindowManager();
|
||||
if (testWindow != null) {
|
||||
testWindow.setLocation(frame.getX(),
|
||||
(frame.getY() + frame.getHeight() + 5));
|
||||
}
|
||||
} else if (position.equals(Position.TOP_LEFT_CORNER)) {
|
||||
frame.setLocation(screenInsets.left, screenInsets.top);
|
||||
syncLocationToWindowManager();
|
||||
if (testWindow != null) {
|
||||
testWindow.setLocation((frame.getX() + frame.getWidth() + 5),
|
||||
frame.getY());
|
||||
case VERTICAL:
|
||||
testWindow.setLocation(frame.getX(),
|
||||
(frame.getY() + frame.getHeight() + 5));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// make instruction frame visible after updating
|
||||
// frame & window positions
|
||||
frame.setVisible(true);
|
||||
@ -553,13 +774,7 @@ public class PassFailJFrame {
|
||||
throws InterruptedException, InvocationTargetException {
|
||||
final Rectangle[] bounds = {null};
|
||||
|
||||
if (isEventDispatchThread()) {
|
||||
bounds[0] = frame != null ? frame.getBounds() : null;
|
||||
} else {
|
||||
invokeAndWait(() -> {
|
||||
bounds[0] = frame != null ? frame.getBounds() : null;
|
||||
});
|
||||
}
|
||||
invokeOnEDT(() -> bounds[0] = frame != null ? frame.getBounds() : null);
|
||||
return bounds[0];
|
||||
}
|
||||
|
||||
@ -574,6 +789,16 @@ public class PassFailJFrame {
|
||||
windowList.add(testWindow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a collection of test windows to the windowList to be disposed of
|
||||
* when the test completes.
|
||||
*
|
||||
* @param testWindows the collection of test windows to be disposed of
|
||||
*/
|
||||
public static synchronized void addTestWindow(Collection<? extends Window> testWindows) {
|
||||
windowList.addAll(testWindows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcibly pass the test.
|
||||
* <p>The sample usage:
|
||||
@ -607,13 +832,20 @@ public class PassFailJFrame {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
public static final class Builder {
|
||||
private String title;
|
||||
private String instructions;
|
||||
private long testTimeOut;
|
||||
private int rows;
|
||||
private int columns;
|
||||
private boolean screenCapture = false;
|
||||
private boolean screenCapture;
|
||||
|
||||
private List<? extends Window> testWindows;
|
||||
private WindowCreator windowCreator;
|
||||
private PositionWindows positionWindows;
|
||||
private InstructionUI instructionUIHandler;
|
||||
|
||||
private Position position;
|
||||
|
||||
public Builder title(String title) {
|
||||
this.title = title;
|
||||
@ -645,6 +877,51 @@ public class PassFailJFrame {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder testUI(Window window) {
|
||||
return testUI(List.of(window));
|
||||
}
|
||||
|
||||
public Builder testUI(Window... windows) {
|
||||
return testUI(List.of(windows));
|
||||
}
|
||||
|
||||
public Builder testUI(List<Window> windows) {
|
||||
if (windows == null) {
|
||||
throw new IllegalArgumentException("The list of windows can't be null");
|
||||
}
|
||||
if (windows.stream()
|
||||
.anyMatch(Objects::isNull)) {
|
||||
throw new IllegalArgumentException("The windows list can't contain null");
|
||||
}
|
||||
|
||||
if (windowCreator != null) {
|
||||
throw new IllegalStateException("windowCreator is already set");
|
||||
}
|
||||
this.testWindows = windows;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder testUI(WindowCreator windowCreator) {
|
||||
if (windowCreator == null) {
|
||||
throw new IllegalArgumentException("The window creator can't be null");
|
||||
}
|
||||
if (testWindows != null) {
|
||||
throw new IllegalStateException("testWindows are already set");
|
||||
}
|
||||
this.windowCreator = windowCreator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder positionTestUI(PositionWindows positionWindows) {
|
||||
this.positionWindows = positionWindows;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder position(Position position) {
|
||||
this.position = position;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PassFailJFrame build() throws InterruptedException,
|
||||
InvocationTargetException {
|
||||
validate();
|
||||
@ -652,26 +929,76 @@ public class PassFailJFrame {
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
if (this.title == null) {
|
||||
this.title = TITLE;
|
||||
if (title == null) {
|
||||
title = TITLE;
|
||||
}
|
||||
|
||||
if (this.instructions == null || this.instructions.length() == 0) {
|
||||
throw new RuntimeException("Please provide the test " +
|
||||
"instruction for this manual test");
|
||||
if (instructions == null || instructions.isEmpty()) {
|
||||
throw new IllegalStateException("Please provide the test " +
|
||||
"instructions for this manual test");
|
||||
}
|
||||
|
||||
if (this.testTimeOut == 0L) {
|
||||
this.testTimeOut = TEST_TIMEOUT;
|
||||
if (testTimeOut == 0L) {
|
||||
testTimeOut = TEST_TIMEOUT;
|
||||
}
|
||||
|
||||
if (this.rows == 0) {
|
||||
this.rows = ROWS;
|
||||
if (rows == 0) {
|
||||
rows = ROWS;
|
||||
}
|
||||
|
||||
if (this.columns == 0) {
|
||||
this.columns = COLUMNS;
|
||||
if (columns == 0) {
|
||||
columns = COLUMNS;
|
||||
}
|
||||
|
||||
if (position == null
|
||||
&& (testWindows != null || windowCreator != null)) {
|
||||
|
||||
position = Position.HORIZONTAL;
|
||||
}
|
||||
|
||||
if (positionWindows != null) {
|
||||
if (testWindows == null && windowCreator == null) {
|
||||
throw new IllegalStateException("To position windows, "
|
||||
+ "provide an a list of windows to the builder");
|
||||
}
|
||||
instructionUIHandler = new InstructionUIHandler();
|
||||
}
|
||||
}
|
||||
|
||||
private final class InstructionUIHandler implements InstructionUI {
|
||||
@Override
|
||||
public Point getLocation() {
|
||||
return frame.getLocation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getSize() {
|
||||
return frame.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rectangle getBounds() {
|
||||
return frame.getBounds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocation(Point location) {
|
||||
setLocation(location.x, location.y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocation(int x, int y) {
|
||||
frame.setLocation(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position getPosition() {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user