8294156: Allow PassFailJFrame.Builder to create test UI

Reviewed-by: azvegint, prr
This commit is contained in:
Alexey Ivanov 2023-10-25 11:31:44 +00:00
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();
}
}