/* * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import java.awt.AWTException; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.GraphicsConfiguration; 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.ActionEvent; import java.awt.event.ActionListener; 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.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import javax.imageio.ImageIO; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JEditorPane; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTextArea; import javax.swing.Timer; import javax.swing.border.Border; 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.BorderFactory.createEmptyBorder; import static javax.swing.SwingUtilities.invokeAndWait; import static javax.swing.SwingUtilities.isEventDispatchThread; /** * A framework for manual tests to display test instructions and * Pass / Fail buttons. The framework automatically * creates a frame to display the instructions, provides buttons * to select the test result, and handles test timeout. * *
* The instruction UI frame displays a timer at the top which indicates * how much time is left. The timer can be paused using the Pause * button to the right of the time; the title of the button changes to * Resume. To resume the timer, use the Resume button. * *
* In the center, the instruction UI frame displays instructions for the * tester. The instructions can be either plain text or HTML. If the * text of the instructions starts with {@code ""}, the * instructions are displayed as HTML, as supported by Swing, which * provides richer formatting options. *
* The instructions are displayed in a text component with word-wrapping * so that there's no horizontal scroll bar. If the text doesn't fit, a * vertical scroll bar is shown. Use {@code rows} and {@code columns} * parameters to change the size of this text component. * If possible, choose the number of rows and columns so that * the instructions fit and no scroll bars are shown. * *
* At the bottom, the instruction UI frame displays the * Pass and Fail buttons. The tester clicks either Pass * or Fail button to finish the test. When the tester clicks the * Fail button, the framework displays a dialog box prompting for * a reason why the test fails. The tester enters the reason and clicks * OK to close the dialog and fail the test, * or simply closes the dialog to fail the test without providing any reason. * *
* If you enable the screenshot feature, a Screenshot button is * added to the right of the Fail button. The tester can choose either * Capture Full Screen (default) or Capture Frames and click the * Screenshot button to take a screenshot. * If there are multiple screens, screenshots of each screen are created. * If the tester selects the Capture Frames mode, screenshots of all * the windows or frames registered in the {@code PassFailJFrame} framework * are created. * *
* If you enable a log area, the instruction UI frame adds a text component * to display log messages below the buttons. * Use {@link #log(String) log}, {@link #logSet(String) logSet} * and {@link #logClear() logClear} static methods of {@code PassFailJFrame} * to add or clear messages from the log area. * *
* After you create an instance of {@code PassFailJFrame}, call the * {@link #awaitAndCheck() awaitAndCheck} method to stop the current thread * (usually the main thread) and wait until the tester clicks * either Pass or Fail button, * or until the test times out. *
* The call to the {@code awaitAndCheck} method is usually the last * statement in the {@code main} method of your test. * If the test fails, an exception is thrown to signal the failure to jtreg. * The test fails if the tester clicks the Fail button, * if the timeout occurs, * or if any window or frame is closed. *
* Before returning from {@code awaitAndCheck}, the framework disposes of * all the windows and frames. * *
* The above example uses the {@link Builder Builder} class to set * the parameters of the instruction frame. * It is the recommended way. * *
* The framework will create an instruction UI frame, it will call * the provided {@code createTestUI} on the Event Dispatch Thread (EDT), * and it will automatically position the test UI frame and make it visible. * *
* Add the following jtreg tags before the test class declaration *
* /*
* * @test
* * @summary Sample manual test
* * @library /java/awt/regtesthelpers
* * @build PassFailJFrame
* * @run main/manual SampleManualTest
* */
*
* * The {@code @library} tag points to the location of the * {@code PassFailJFrame} class in the source code; * the {@code @build} tag makes jtreg compile the {@code PassFailJFrame} class, * and finally the {@code @run} tag specifies it is a manual * test and the class to run. *
* Don't forget to update the name of the class to run in the {@code @run} tag. * *
* For simple test UI, use {@link Builder#splitUI(PanelCreator) splitUI}, * or explicitly * {@link Builder#splitUIRight(PanelCreator) splitUIRight} or * {@link Builder#splitUIBottom(PanelCreator) splitUIBottom} with * a {@link PanelCreator PanelCreator}. * The framework will call the provided * {@code createUIPanel} method to create the component with test UI and * will place it as the right or bottom component in a split pane * along with instruction UI. *
* Note: support for multiple windows is incomplete. * *
* This sample uses {@link #PassFailJFrame(String) a constructor} of * {@code PassFailJFrame} to create its instance, * there are several overloads provided which allow changing other parameters. *
* When you use the constructors, you have to explicitly create * your test UI window on EDT. After you create the window, * you need to register it with the framework using * {@link #addTestWindow(Window) addTestWindow} * to ensure the window is disposed of when the test completes. * Before showing the window, you have to call * {@link #positionTestWindow(Window, Position) positionTestWindow} * to position the test window near the instruction UI frame provided * by the framework. And finally you have to explicitly show the test UI * window by calling {@code setVisible(true)}. *
* To avoid the complexity, use the {@link Builder Builder} class * which provides a streamlined way to configure and create an * instance of {@code PassFailJFrame}. *
* Consider updating tests which use {@code PassFailJFrame} constructors to * use the builder pattern. */ public final class PassFailJFrame { /** A default title for the instruction frame. */ private static final String TITLE = "Test Instructions"; /** A default test timeout. */ private static final long TEST_TIMEOUT = 5; /** A default number of rows for displaying the test instructions. */ private static final int ROWS = 10; /** A default number of columns for displaying the test instructions. */ private static final int COLUMNS = 40; /** * A gap between windows. */ public static final int WINDOW_GAP = 8; /** * Prefix for the user-provided failure reason. */ private static final String FAILURE_REASON = "Failure Reason:\n"; /** * The failure reason message when the user doesn't provide one. */ private static final String EMPTY_REASON = "(no reason provided)"; /** * List of windows or frames managed by the {@code PassFailJFrame} * framework. These windows are automatically disposed of when the * test is finished. *
* Note: access to this field has to be synchronized by
* {@code PassFailJFrame.class}.
*/
private static final List
* Note: do not use this field directly,
* use the {@link #setFailureReason(String) setFailureReason} and
* {@link #getFailureReason() getFailureReason} methods to modify and
* to read its value.
*/
private static String failureReason;
private static final AtomicInteger imgCounter = new AtomicInteger(0);
private static JFrame frame;
private static Robot robot;
private static JTextArea logArea;
public enum Position {HORIZONTAL, VERTICAL, TOP_LEFT_CORNER}
/**
* Constructs a frame which displays test instructions and
* the Pass / Fail buttons with the given instructions, and
* the default timeout of {@value #TEST_TIMEOUT} minutes,
* the default title of {@value #TITLE} and
* the default values of {@value #ROWS} and {@value #COLUMNS}
* for rows and columns.
*
* See {@link #PassFailJFrame(String,String,long,int,int,boolean)} for
* more details.
*
* @param instructions the instructions for the tester
*
* @throws InterruptedException if the current thread is interrupted
* while waiting for EDT to finish creating UI components
* @throws InvocationTargetException if an exception is thrown while
* creating UI components on EDT
*/
public PassFailJFrame(String instructions)
throws InterruptedException, InvocationTargetException {
this(instructions, TEST_TIMEOUT);
}
/**
* Constructs a frame which displays test instructions and
* the Pass / Fail buttons
* with the given instructions and timeout as well as
* the default title of {@value #TITLE}
* and the default values of {@value #ROWS} and {@value #COLUMNS}
* for rows and columns.
*
* See {@link #PassFailJFrame(String,String,long,int,int,boolean)} for
* more details.
*
* @param instructions the instructions for the tester
* @param testTimeOut the test timeout in minutes
*
* @throws InterruptedException if the current thread is interrupted
* while waiting for EDT to finish creating UI components
* @throws InvocationTargetException if an exception is thrown while
* creating UI components on EDT
*/
public PassFailJFrame(String instructions, long testTimeOut)
throws InterruptedException, InvocationTargetException {
this(TITLE, instructions, testTimeOut);
}
/**
* Constructs a frame which displays test instructions and
* the Pass / Fail buttons
* with the given title, instructions and timeout as well as
* the default values of {@value #ROWS} and {@value #COLUMNS}
* for rows and columns.
* The screenshot feature is not enabled, if you use this constructor.
*
* See {@link #PassFailJFrame(String,String,long,int,int,boolean)} for
* more details.
*
* @param title the title of the instruction frame
* @param instructions the instructions for the tester
* @param testTimeOut the test timeout in minutes
*
* @throws InterruptedException if the current thread is interrupted
* while waiting for EDT to finish creating UI components
* @throws InvocationTargetException if an exception is thrown while
* creating UI components on EDT
*/
public PassFailJFrame(String title, String instructions,
long testTimeOut)
throws InterruptedException, InvocationTargetException {
this(title, instructions, testTimeOut, ROWS, COLUMNS);
}
/**
* Constructs a frame which displays test instructions and
* the Pass / Fail buttons
* with the given title, instructions, timeout, number of rows and columns.
* The screenshot feature is not enabled, if you use this constructor.
*
* See {@link #PassFailJFrame(String,String,long,int,int,boolean)} for
* more details.
*
* @param title the title of the instruction frame
* @param instructions the instructions for the tester
* @param testTimeOut the test timeout in minutes
* @param rows the number of rows for the text component
* which displays test instructions
* @param columns the number of columns for the text component
* which displays test instructions
*
* @throws InterruptedException if the current thread is interrupted
* while waiting for EDT to finish creating UI components
* @throws InvocationTargetException if an exception is thrown while
* creating UI components on EDT
*/
public PassFailJFrame(String title, String instructions,
long testTimeOut,
int rows, int columns)
throws InterruptedException, InvocationTargetException {
this(title, instructions, testTimeOut, rows, columns, false);
}
/**
* Constructs a frame which displays test instructions and
* the Pass / Fail buttons
* as well as supporting UI components with the given title, instructions,
* timeout, number of rows and columns,
* and screen capture functionality.
* All the UI components are created on the EDT, so it is safe to call
* the constructor on the main thread.
*
* After you create a test UI window, register the window using
* {@link #addTestWindow(Window) addTestWindow} for disposal, and
* position it close to the instruction frame using
* {@link #positionTestWindow(Window, Position) positionTestWindow}.
* As the last step, make your test UI window visible.
*
* Call the {@link #awaitAndCheck() awaitAndCheck} method on the instance
* of {@code PassFailJFrame} when you set up the testing environment.
*
* If the tester clicks the Fail button, a dialog prompting for
* a description of the problem is displayed, and then an exception
* is thrown which fails the test.
* If the tester clicks the Pass button, the test completes
* successfully.
* If the timeout occurs or the instruction frame is closed,
* the test fails.
*
* The {@code rows} and {@code columns} parameters control
* the size of a text component which displays the instructions.
* The preferred size of the instructions is calculated by
* creating {@code new JTextArea(rows, columns)}.
*
* If you enable screenshots by setting the {@code screenCapture}
* parameter to {@code true}, a Screenshot button is added.
* Clicking the Screenshot button takes screenshots of
* all the monitors or all the windows registered with
* {@code PassFailJFrame}.
*
* @param title the title of the instruction frame
* @param instructions the instructions for the tester
* @param testTimeOut the test timeout in minutes
* @param rows the number of rows for the text component
* which displays test instructions
* @param columns the number of columns for the text component
* which displays test instructions
* @param screenCapture if set to {@code true}, enables screen capture
* functionality
*
* @throws InterruptedException if the current thread is interrupted
* while waiting for EDT to finish creating UI components
* @throws InvocationTargetException if an exception is thrown while
* creating UI components on EDT
*
* @see JTextArea#JTextArea(int,int) JTextArea(int rows, int columns)
* @see Builder Builder
*/
public PassFailJFrame(String title, String instructions,
long testTimeOut,
int rows, int columns,
boolean screenCapture)
throws InterruptedException, InvocationTargetException {
invokeOnEDT(() -> createUI(title, instructions,
testTimeOut,
rows, columns,
screenCapture));
}
/**
* Configures {@code PassFailJFrame} using the builder.
* It creates test UI specified using {@code testUI} or {@code splitUI}
* methods on EDT.
* @param builder the builder with the parameters
* @throws InterruptedException if the current thread is interrupted while
* waiting for EDT to complete a task
* @throws InvocationTargetException if an exception is thrown while
* running a task on EDT
*/
private PassFailJFrame(final Builder builder)
throws InterruptedException, InvocationTargetException {
invokeOnEDT(() -> createUI(builder));
if (!builder.splitUI && builder.panelCreator != null) {
JComponent content = builder.panelCreator.createUIPanel();
String title = content.getName();
if (title == null) {
title = "Test UI";
}
JDialog dialog = new JDialog(frame, title, false);
dialog.addWindowListener(windowClosingHandler);
dialog.add(content, BorderLayout.CENTER);
dialog.pack();
addTestWindow(dialog);
positionTestWindow(dialog, builder.position);
}
if (builder.windowListCreator != null) {
invokeOnEDT(() ->
builder.testWindows = builder.windowListCreator.createTestUI());
if (builder.testWindows == null) {
throw new IllegalStateException("Window list creator returned null list");
}
}
if (builder.testWindows != null) {
if (builder.testWindows.isEmpty()) {
throw new IllegalStateException("Window list is empty");
}
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));
} else {
Window window = builder.testWindows.get(0);
positionTestWindow(window, builder.position);
}
}
showAllWindows();
}
/**
* 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);
}
}
/**
* Does the same as {@link #invokeOnEDT(Runnable)}, but does not throw
* any checked exceptions.
*
* @param doRun an operation to run on EDT
*/
private static void invokeOnEDTUncheckedException(Runnable doRun) {
try {
invokeOnEDT(doRun);
} catch (InterruptedException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
private static void createUI(String title, String instructions,
long testTimeOut, int rows, int columns,
boolean enableScreenCapture) {
frame = new JFrame(title);
frame.setLayout(new BorderLayout());
frame.addWindowListener(windowClosingHandler);
frame.add(createInstructionUIPanel(instructions,
testTimeOut,
rows, columns,
enableScreenCapture,
false, 0),
BorderLayout.CENTER);
frame.pack();
frame.setLocationRelativeTo(null);
addTestWindow(frame);
}
private static void createUI(Builder builder) {
frame = new JFrame(builder.title);
frame.setLayout(new BorderLayout());
frame.addWindowListener(windowClosingHandler);
JComponent instructionUI =
createInstructionUIPanel(builder.instructions,
builder.testTimeOut,
builder.rows, builder.columns,
builder.screenCapture,
builder.addLogArea,
builder.logAreaRows);
if (builder.splitUI) {
JSplitPane splitPane = new JSplitPane(
builder.splitUIOrientation,
instructionUI,
builder.panelCreator.createUIPanel());
frame.add(splitPane, BorderLayout.CENTER);
} else {
frame.add(instructionUI, BorderLayout.CENTER);
}
frame.pack();
frame.setLocationRelativeTo(null);
addTestWindow(frame);
}
private static JComponent createInstructionUIPanel(String instructions,
long testTimeOut,
int rows, int columns,
boolean enableScreenCapture,
boolean addLogArea,
int logAreaRows) {
JPanel main = new JPanel(new BorderLayout());
main.setBorder(createFrameBorder());
timeoutHandlerPanel = new TimeoutHandlerPanel(testTimeOut);
main.add(timeoutHandlerPanel, BorderLayout.NORTH);
JTextComponent text = instructions.startsWith("")
? configureHTML(instructions, rows, columns)
: configurePlainText(instructions, rows, columns);
text.setEditable(false);
JPanel textPanel = new JPanel(new BorderLayout());
textPanel.setBorder(createEmptyBorder(GAP, 0, GAP, 0));
textPanel.add(new JScrollPane(text), BorderLayout.CENTER);
main.add(textPanel, BorderLayout.CENTER);
JButton btnPass = new JButton("Pass");
btnPass.addActionListener((e) -> {
latch.countDown();
timeoutHandlerPanel.stop();
});
JButton btnFail = new JButton("Fail");
btnFail.addActionListener((e) -> {
requestFailureReason();
timeoutHandlerPanel.stop();
});
JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.CENTER,
GAP, 0));
buttonsPanel.add(btnPass);
buttonsPanel.add(btnFail);
if (enableScreenCapture) {
buttonsPanel.add(createCapturePanel());
}
if (addLogArea) {
logArea = new JTextArea(logAreaRows, columns);
logArea.setEditable(false);
logArea.setBorder(createTextBorder());
Box buttonsLogPanel = Box.createVerticalBox();
buttonsLogPanel.add(buttonsPanel);
buttonsLogPanel.add(Box.createVerticalStrut(GAP));
buttonsLogPanel.add(new JScrollPane(logArea));
main.add(buttonsLogPanel, BorderLayout.SOUTH);
} else {
main.add(buttonsPanel, BorderLayout.SOUTH);
}
main.setMinimumSize(main.getPreferredSize());
return main;
}
private static JTextComponent configurePlainText(String instructions,
int rows, int columns) {
JTextArea text = new JTextArea(instructions, rows, columns);
text.setLineWrap(true);
text.setWrapStyleWord(true);
text.setBorder(createTextBorder());
return text;
}
private static JTextComponent configureHTML(String instructions,
int rows, int columns) {
JEditorPane text = new JEditorPane("text/html", instructions);
text.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES,
Boolean.TRUE);
// Set preferred size as if it were JTextArea
text.setPreferredSize(new JTextArea(rows, columns).getPreferredSize());
HTMLEditorKit kit = (HTMLEditorKit) text.getEditorKit();
StyleSheet styles = kit.getStyleSheet();
// Reduce the default margins
styles.addRule("ol, ul { margin-left-ltr: 20; margin-left-rtl: 20 }");
// Make the size of code blocks the same as other text
styles.addRule("code { font-size: inherit }");
return text;
}
/** A default gap between components. */
private static final int GAP = 4;
/**
* Creates a default border for frames or dialogs.
* It uses the default gap of {@value GAP}.
*
* @return the border for frames and dialogs
*/
private static Border createFrameBorder() {
return createEmptyBorder(GAP, GAP, GAP, GAP);
}
/**
* Creates a border set to text area.
* It uses the default gap of {@value GAP}.
*
* @return the border for text area
*/
private static Border createTextBorder() {
return createEmptyBorder(GAP, GAP, GAP, GAP);
}
/**
* Creates a test UI window.
*/
@FunctionalInterface
public interface WindowCreator {
/**
* Creates a window for test UI.
* This method is called by the framework on the EDT.
* @return a test UI window
*/
Window createTestUI();
}
/**
* Creates a list of test UI windows.
*/
@FunctionalInterface
public interface WindowListCreator {
/**
* Creates one or more windows for test UI.
* This method is called by the framework on the EDT.
* @return a list of test UI windows
*/
List extends Window> createTestUI();
}
/**
* Creates a component (panel) with test UI
* to be hosted in a split pane or a frame.
*/
@FunctionalInterface
public interface PanelCreator {
/**
* Creates a component which hosts test UI. This component
* is placed into a split pane or into a frame to display the UI.
*
* This method is called by the framework on the EDT.
* @return a component (panel) with test UI
*/
JComponent createUIPanel();
}
/**
* 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.
*
* The list of the test windows contains the windows
* that were passed to the framework via the
* {@link Builder#testUI(Window...) testUI(Window...)} method or
* that were created with {@code WindowCreator}
* or {@code WindowListCreator} which were passed via
* {@link Builder#testUI(WindowCreator) testUI(WindowCreator)} or
* {@link Builder#testUI(WindowListCreator) testUI(WindowListCreator)}
* correspondingly.
*
* @param testWindows the list of test windows
* @param instructionUI information about the instruction frame
*/
void positionTestWindows(List This method should be called before making the test window visible
* to avoid flickering. The sample usage:
*
* The log area can be controlled by {@link #log(String)},
* {@link #logClear()} and {@link #logSet(String)}.
*
* @return this builder
*/
public Builder logArea() {
this.addLogArea = true;
return this;
}
/**
* Adds a log area below the "Pass", "Fail" buttons.
*
* The log area can be controlled by {@link #log(String)},
* {@link #logClear()} and {@link #logSet(String)}.
*
* The number of columns is taken from the number of
* columns in the instructional JTextArea.
*
* @param rows of the log area
* @return this builder
*/
public Builder logArea(int rows) {
this.addLogArea = true;
this.logAreaRows = rows;
return this;
}
/**
* Adds a {@code WindowCreator} which the framework will use
* to create the test UI window.
*
* @param windowCreator a {@code WindowCreator}
* to create the test UI window
* @return this builder
* @throws IllegalArgumentException if {@code windowCreator} is {@code null}
* @throws IllegalStateException if a window creator
* or a list of test windows is already set
*/
public Builder testUI(WindowCreator windowCreator) {
if (windowCreator == null) {
throw new IllegalArgumentException("The window creator can't be null");
}
checkWindowsLists();
this.windowListCreator = () -> List.of(windowCreator.createTestUI());
return this;
}
/**
* Adds an implementation of {@link PositionWindows PositionWindows}
* which the framework will use to position multiple test UI windows.
*
* @param positionWindows an implementation of {@code PositionWindows}
* to position multiple test UI windows
* @return this builder
* @throws IllegalArgumentException if the {@code positionWindows}
* parameter is {@code null}
* @throws IllegalStateException if the {@code positionWindows} field
* is already set
*/
public Builder positionTestUI(PositionWindows positionWindows) {
if (positionWindows == null) {
throw new IllegalArgumentException("positionWindows parameter can't be null");
}
if (this.positionWindows != null) {
throw new IllegalStateException("PositionWindows is already set");
}
this.positionWindows = positionWindows;
return this;
}
/**
* Positions the test UI windows in a row to the right of
* the instruction frame. The top of the windows is aligned to
* that of the instruction frame.
*
* @return this builder
*/
public Builder positionTestUIRightRow() {
return position(Position.HORIZONTAL)
.positionTestUI(WindowLayouts::rightOneRow);
}
/**
* Positions the test UI windows in a column to the right of
* the instruction frame. The top of the first window is aligned to
* that of the instruction frame.
*
* @return this builder
*/
public Builder positionTestUIRightColumn() {
return position(Position.HORIZONTAL)
.positionTestUI(WindowLayouts::rightOneColumn);
}
/**
* Positions the test UI windows in a column to the right of
* the instruction frame centering the stack of the windows.
*
* @return this builder
*/
public Builder positionTestUIRightColumnCentered() {
return position(Position.HORIZONTAL)
.positionTestUI(WindowLayouts::rightOneColumnCentered);
}
/**
* Positions the test UI windows in a row to the bottom of
* the instruction frame. The left of the first window is aligned to
* that of the instruction frame.
*
* @return this builder
*/
public Builder positionTestUIBottomRow() {
return position(Position.VERTICAL)
.positionTestUI(WindowLayouts::bottomOneRow);
}
/**
* Positions the test UI windows in a row to the bottom of
* the instruction frame centering the row of the windows.
*
* @return this builder
*/
public Builder positionTestUIBottomRowCentered() {
return position(Position.VERTICAL)
.positionTestUI(WindowLayouts::bottomOneRowCentered);
}
/**
* Positions the test UI windows in a column to the bottom of
* the instruction frame. The left of the first window is aligned to
* that of the instruction frame.
*
* @return this builder
*/
public Builder positionTestUIBottomColumn() {
return position(Position.VERTICAL)
.positionTestUI(WindowLayouts::bottomOneColumn);
}
/**
* Adds a {@code WindowListCreator} which the framework will use
* to create a list of test UI windows.
*
* @param windowListCreator a {@code WindowListCreator}
* to create test UI windows
* @return this builder
* @throws IllegalArgumentException if {@code windowListCreator} is {@code null}
* @throws IllegalStateException if a window creator
* or a list of test windows is already set
*/
public Builder testUI(WindowListCreator windowListCreator) {
if (windowListCreator == null) {
throw new IllegalArgumentException("The window list creator can't be null");
}
checkWindowsLists();
this.windowListCreator = windowListCreator;
return this;
}
/**
* Adds an already created test UI window.
* The window is positioned and shown automatically.
*
* @param window a test UI window
* @return this builder
*/
public Builder testUI(Window window) {
return testUI(List.of(window));
}
/**
* Adds an array of already created test UI windows.
*
* @param windows an array of test UI windows
* @return this builder
*/
public Builder testUI(Window... windows) {
return testUI(List.of(windows));
}
/**
* Adds a list of already created test UI windows.
*
* @param windows a list of test UI windows
* @return this builder
* @throws IllegalArgumentException if {@code windows} is {@code null}
* or the list contains {@code null}
* @throws IllegalStateException if a window creator
* or a list of test windows is already set
*/
public Builder testUI(List extends 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 list of windows can't contain null");
}
checkWindowsLists();
this.testWindows = windows;
return this;
}
/**
* Verifies the state of window list and window creator.
*
* @throws IllegalStateException if a windows list creator
* or a list of test windows is already set
*/
private void checkWindowsLists() {
if (windowListCreator != null) {
throw new IllegalStateException("Window list creator is already set");
}
if (testWindows != null) {
throw new IllegalStateException("The list of test windows is already set");
}
}
/**
* Adds a {@code PanelCreator} which the framework will use
* to create a component and place it into a dialog.
*
* @param panelCreator a {@code PanelCreator} to create a component
* with test UI
* @return this builder
* @throws IllegalStateException if split UI was enabled using
* a {@code splitUI} method
*/
public Builder testUI(PanelCreator panelCreator) {
if (splitUI) {
throw new IllegalStateException("Can't combine splitUI and "
+ "testUI with panelCreator");
}
this.panelCreator = panelCreator;
return this;
}
/**
* Adds a {@code PanelCreator} which the framework will use
* to create a component with test UI and display it in a split pane.
*
* By default, horizontal orientation is used,
* and test UI is displayed to the right of the instruction UI.
*
* @param panelCreator a {@code PanelCreator} to create a component
* with test UI
* @return this builder
*
* @throws IllegalStateException if a {@code PanelCreator} is
* already set
* @throws IllegalArgumentException if {panelCreator} is {@code null}
*/
public Builder splitUI(PanelCreator panelCreator) {
return splitUIRight(panelCreator);
}
/**
* Adds a {@code PanelCreator} which the framework will use
* to create a component with test UI and display it
* to the right of instruction UI.
*
* @param panelCreator a {@code PanelCreator} to create a component
* with test UI
* @return this builder
*
* @throws IllegalStateException if a {@code PanelCreator} is
* already set
* @throws IllegalArgumentException if {panelCreator} is {@code null}
*/
public Builder splitUIRight(PanelCreator panelCreator) {
return splitUI(panelCreator, JSplitPane.HORIZONTAL_SPLIT);
}
/**
* Adds a {@code PanelCreator} which the framework will use
* to create a component with test UI and display it
* in the bottom of instruction UI.
*
* @param panelCreator a {@code PanelCreator} to create a component
* with test UI
* @return this builder
*
* @throws IllegalStateException if a {@code PanelCreator} is
* already set
* @throws IllegalArgumentException if {panelCreator} is {@code null}
*/
public Builder splitUIBottom(PanelCreator panelCreator) {
return splitUI(panelCreator, JSplitPane.VERTICAL_SPLIT);
}
/**
* Enables split UI and stores the orientation of the split pane.
*
* @param panelCreator a {@code PanelCreator} to create a component
* with test UI
* @param splitUIOrientation orientation of the split pane
* @return this builder
*
* @throws IllegalStateException if a {@code PanelCreator} is
* already set
* @throws IllegalArgumentException if {panelCreator} is {@code null}
*/
private Builder splitUI(PanelCreator panelCreator,
int splitUIOrientation) {
if (panelCreator == null) {
throw new IllegalArgumentException("A PanelCreator cannot be null");
}
if (this.panelCreator != null) {
throw new IllegalStateException("A PanelCreator is already set");
}
splitUI = true;
this.splitUIOrientation = splitUIOrientation;
this.panelCreator = panelCreator;
return this;
}
public Builder position(Position position) {
this.position = position;
return this;
}
public PassFailJFrame build() throws InterruptedException,
InvocationTargetException {
validate();
return new PassFailJFrame(this);
}
/**
* Returns the file name of the test, if the {@code test.file} property
* is defined, concatenated with {@code " - "} which serves as a prefix
* to the default instruction frame title;
* or an empty string if the {@code test.file} property is not defined.
*
* @return the prefix to the default title:
* either the file name of the test or an empty string
*
* @see jtreg
* test-specific system properties and environment variables
*/
private static String getTestFileNamePrefix() {
String testFile = System.getProperty("test.file");
if (testFile == null) {
return "";
}
return Paths.get(testFile).getFileName().toString()
+ " - ";
}
/**
* Validates the state of the builder and
* expands parameters that have no assigned values
* to their default values.
*
* @throws IllegalStateException if no instructions are provided,
* or if {@code PositionWindows} implementation is
* provided but neither window creator nor
* test window list are set
*/
private void validate() {
if (title == null) {
title = getTestFileNamePrefix() + TITLE;
}
if (instructions == null || instructions.isEmpty()) {
throw new IllegalStateException("Please provide the test " +
"instructions for this manual test");
}
if (testTimeOut == 0L) {
testTimeOut = TEST_TIMEOUT;
}
if (rows == 0) {
rows = getDefaultRows();
}
if (columns == 0) {
columns = COLUMNS;
}
if (position == null
&& (testWindows != null || windowListCreator != null
|| (!splitUI && panelCreator != null))) {
position = Position.HORIZONTAL;
}
if (positionWindows != null) {
if (testWindows == null && windowListCreator == null) {
throw new IllegalStateException("To position windows, "
+ "provide 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;
}
}
}
/**
* Creates a builder for configuring {@code PassFailJFrame}.
*
* @return the builder for configuring {@code PassFailJFrame}
*/
public static Builder builder() {
return new Builder();
}
}
*
*/
public static void positionTestWindow(Window testWindow, Position position) {
positionInstructionFrame(position);
if (testWindow != null) {
switch (position) {
case HORIZONTAL:
case TOP_LEFT_CORNER:
testWindow.setLocation((frame.getX() + frame.getWidth() + WINDOW_GAP),
frame.getY());
break;
case VERTICAL:
testWindow.setLocation(frame.getX(),
(frame.getY() + frame.getHeight() + WINDOW_GAP));
break;
}
}
// make instruction frame visible after updating
// frame & window positions
frame.setVisible(true);
}
/**
* Ensures the frame location is updated by the window manager
* if it adjusts the frame location after {@code setLocation}.
*
* @see #positionTestWindow
*/
private static void syncLocationToWindowManager() {
Toolkit.getDefaultToolkit().sync();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* Returns the current position and size of the test instruction frame.
* This method can be used in scenarios when custom positioning of
* multiple test windows w.r.t test instruction frame is necessary,
* at test-case level and the desired configuration is not available
* as a {@code Position} option.
*
* @return Rectangle bounds of test instruction frame
* @see #positionTestWindow
*
* @throws InterruptedException exception thrown when thread is
* interrupted
* @throws InvocationTargetException if an exception is thrown while
* obtaining frame bounds on EDT
*/
public static Rectangle getInstructionFrameBounds()
throws InterruptedException, InvocationTargetException {
final Rectangle[] bounds = {null};
invokeOnEDT(() -> bounds[0] = frame != null ? frame.getBounds() : null);
return bounds[0];
}
/**
* Add the testWindow to the windowList so that test instruction frame
* and testWindow and any other windows used in this test is disposed
* via disposeWindows().
*
* @param testWindow testWindow that needs to be disposed
*/
public static synchronized void addTestWindow(Window testWindow) {
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);
}
/**
* Displays all the windows in {@code windowList}.
*
* @throws InterruptedException if the thread is interrupted while
* waiting for the event dispatch thread to finish running
* the {@link #showUI() showUI}
* @throws InvocationTargetException if an exception is thrown while
* the event dispatch thread executes {@code showUI}
*/
private static void showAllWindows()
throws InterruptedException, InvocationTargetException {
invokeOnEDT(PassFailJFrame::showUI);
}
/**
* Displays all the windows in {@code windowList}; it has to be called on
* the EDT — use {@link #showAllWindows() showAllWindows} to ensure it.
*/
private static synchronized void showUI() {
windowList.forEach(w -> w.setVisible(true));
}
/**
* Forcibly pass the test.
*
*/
public static void forcePass() {
latch.countDown();
}
/**
* Forcibly fail the test.
*/
public static void forceFail() {
forceFail("forceFail called");
}
/**
* Forcibly fail the test and provide a reason.
*
* @param reason the reason why the test is failed
*/
public static void forceFail(String reason) {
setFailureReason(FAILURE_REASON + reason);
latch.countDown();
}
/**
* Adds a {@code message} to the log area, if enabled by
* {@link Builder#logArea() logArea()} or
* {@link Builder#logArea(int) logArea(int)}.
*
* @param message the message to log
*/
public static void log(String message) {
System.out.println("PassFailJFrame: " + message);
invokeOnEDTUncheckedException(() -> logArea.append(message + "\n"));
}
/**
* Clears the log area, if enabled by
* {@link Builder#logArea() logArea()} or
* {@link Builder#logArea(int) logArea(int)}.
*/
public static void logClear() {
System.out.println("\nPassFailJFrame: log cleared\n");
invokeOnEDTUncheckedException(() -> logArea.setText(""));
}
/**
* Replaces the log area content with provided {@code text}, if enabled by
* {@link Builder#logArea() logArea()} or
* {@link Builder#logArea(int) logArea(int)}.
*
* @param text new text for the log area
*/
public static void logSet(String text) {
System.out.println("\nPassFailJFrame: log set to:\n" + text + "\n");
invokeOnEDTUncheckedException(() -> logArea.setText(text));
}
public static final class Builder {
private String title;
private String instructions;
private long testTimeOut;
private int rows;
private int columns;
private boolean screenCapture;
private boolean addLogArea;
private int logAreaRows = 10;
private List extends Window> testWindows;
private WindowListCreator windowListCreator;
private PanelCreator panelCreator;
private boolean splitUI;
private int splitUIOrientation;
private PositionWindows positionWindows;
private InstructionUI instructionUIHandler;
private Position position;
/**
* A private constructor for the builder,
* it should not be created directly.
* Use {@code PassFailJFrame.builder()} method instead.
*/
private Builder() {
}
public Builder title(String title) {
this.title = title;
return this;
}
public Builder instructions(String instructions) {
this.instructions = instructions;
return this;
}
public Builder testTimeOut(long testTimeOut) {
this.testTimeOut = testTimeOut;
return this;
}
/**
* Sets the number of rows for displaying the instruction text.
* The default value is the number of lines in the text plus 1:
* {@code ((int) instructions.lines().count() + 1)}.
*
* @param rows the number of rows for instruction text
* @return this builder
*/
public Builder rows(int rows) {
this.rows = rows;
return this;
}
private int getDefaultRows() {
return (int) instructions.lines().count() + 1;
}
/**
* Adds a certain number of rows for displaying the instruction text.
*
* @param rowsAdd the number of rows to add to the number of rows
* @return this builder
* @see #rows
*/
public Builder rowsAdd(int rowsAdd) {
if (rows == 0) {
rows = getDefaultRows();
}
rows += rowsAdd;
return this;
}
/**
* Sets the number of columns for displaying the instruction text.
*
* @param columns the number of columns for instruction text
* @return this builder
*/
public Builder columns(int columns) {
this.columns = columns;
return this;
}
public Builder screenCapture() {
this.screenCapture = true;
return this;
}
/**
* Adds a log area below the "Pass", "Fail" buttons.
*
* PrinterJob pj = PrinterJob.getPrinterJob();
* if (pj == null || pj.getPrintService() == null) {
* System.out.println(""Printer not configured or available.");
* PassFailJFrame.forcePass();
* }
*