Skip to content
Snippets Groups Projects
Select Git revision
  • legacy
  • master default protected
  • jdk-17.0.13-ga-legacy
  • jdk-17.0.14+4
  • jdk-17.0.14+3
  • jdk-17.0.14+2
  • jdk-17.0.14+1
  • jdk-17.0.13-ga
  • jdk-17.0.13+11
  • jdk-17.0.13+10
  • jdk-17.0.13+9
  • jdk-17.0.13+8
  • jdk-17.0.13+7
  • jdk-17.0.13+6
  • jdk-17.0.14+0
  • jdk-17.0.13+5
  • jdk-17.0.13+4
  • jdk-17.0.13+3
  • jdk-17.0.13+2
  • jdk-17.0.13+1
  • jdk-17.0.13+0
  • jdk-17.0.12-ga
22 results

PassFailJFrame.java

Blame
  • PassFailJFrame.java 68.70 KiB
    /*
     * 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.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.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.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
     * <i>Pass</i> / <i>Fail</i> buttons. The framework automatically
     * creates a frame to display the instructions, provides buttons
     * to select the test result, and handles test timeout.
     *
     * <p id="timeOutTimer">
     * The instruction UI frame displays a timer at the top which indicates
     * how much time is left. The timer can be paused using the <i>Pause</i>
     * button to the right of the time; the title of the button changes to
     * <i>Resume</i>. To resume the timer, use the <i>Resume</i> button.
     *
     * <p id="instructionText">
     * 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 "<html>"}, the
     * instructions are displayed as HTML, as supported by Swing, which
     * provides richer formatting options.
     * <p>
     * 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.
     *
     * <p id="passFailButtons">
     * At the bottom, the instruction UI frame displays the
     * <i>Pass</i> and <i>Fail</i> buttons. The tester clicks either <i>Pass</i>
     * or <i>Fail</i> button to finish the test. When the tester clicks the
     * <i>Fail</i> button, the framework displays a dialog box prompting for
     * a reason why the test fails. The tester enters the reason and clicks
     * <i>OK</i> to close the dialog and fail the test,
     * or simply closes the dialog to fail the test without providing any reason.
     *
     * <p id="screenCapture">
     * If you enable the screenshot feature, a <i>Screenshot</i> button is
     * added  to the right of the <i>Fail</i> button. The tester can choose either
     * <i>Capture Full Screen</i> (default) or <i>Capture Frames</i> and click the
     * <i>Screenshot</i> button to take a screenshot.
     * If there are multiple screens, screenshots of each screen are created.
     * If the tester selects the <i>Capture Frames</i> mode, screenshots of all
     * the windows or frames registered in the {@code PassFailJFrame} framework
     * are created.
     *
     * <p id="logArea">
     * 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.
     *
     * <p id="awaitTestResult">
     * 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 <i>Pass</i> or <i>Fail</i> button,
     * or until the test times out.
     * <p>
     * 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 <i>Fail</i> button,
     * if the timeout occurs,
     * or if any window or frame is closed.
     * <p>
     * Before returning from {@code awaitAndCheck}, the framework disposes of
     * all the windows and frames.
     *
     * <h2 id="sampleManualTest">Sample Manual Test</h2>
     * A simple test would look like this:
     * {@snippet id='sampleManualTestCode' lang='java':
     * 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(SampleManualTest::createTestUI)
     *                       .build()
     *                       .awaitAndCheck();
     *     }
     *
     *     private static Window createTestUI() {
     *         JFrame testUI = new JFrame("Test UI");
     *         testUI.setSize(250, 150);
     *         return testUI;
     *     }
     * }
     * }
     * <p>
     * The above example uses the {@link Builder Builder} class to set
     * the parameters of the instruction frame.
     * It is <em>the recommended way</em>.
     *
     * <p>
     * The framework will create an instruction UI frame, it will call
     * the provided {@code createTestUI} on the Event Dispatch Thread (<dfn>EDT</dfn>),
     * and it will automatically position the test UI frame and make it visible.
     *
     * <p id="jtregTagsForTest">
     * Add the following jtreg tags before the test class declaration
     * {@snippet :
     * /*
     *  * @test
     *  * @summary Sample manual test
     *  * @library /java/awt/regtesthelpers
     *  * @build PassFailJFrame
     *  * @run main/manual SampleManualTest
     * }
     * and the closing comment tag <code>*&#47;</code>.
     * <p>
     * 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.
     *
     * <h2 id="usingBuilder">Using {@code Builder}</h2>
     * Use methods of the {@link Builder Builder} class to set or change
     * parameters of {@code PassFailJFrame} and its instruction UI:
     * <ul>
     *     <li>{@link Builder#title(String) title} sets
     *         the title of the instruction UI
     *         (the default is {@value #TITLE});</li>
     *     <li>{@link Builder#testTimeOut(long) testTimeOut} sets
     *         the timeout of the test
     *         (the default is {@value #TEST_TIMEOUT});</li>
     *     <li>{@link Builder#rows(int) rows} and
     *         {@link Builder#columns(int) columns} control the size
     *         the text component which displays the instructions
     *         (the default number of rows is the number of lines in the text
     *         of the instructions,
     *         the default number of columns is {@value #COLUMNS});</li>
     *     <li>{@link Builder#logArea() logArea} adds a log area;</li>
     *     <li>{@link Builder#screenCapture() screenCapture}
     *         enables screenshots.</li>
     * </ul>
     *
     * <h3 id="builderTestUI">Using {@code testUI} and {@code splitUI}</h3>
     * The {@code Builder.testUI} methods accept interfaces which create one window
     * or a list of windows if the test needs multiple windows,
     * or directly a single window, an array of windows or a list of windows.
     * <p>
     * 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.
     * <p>
     * Note: <em>support for multiple windows is incomplete</em>.
     *
     * <h2 id="obsoleteSampleTest">Obsolete Sample Test</h2>
     * 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:
     * {@snippet id='obsoleteSampleTestCode' lang='java':
     * public class ObsoleteManualTest {
     *     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(ObsoleteManualTest::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);
     *     }
     * }
     * }
     * <p>
     * 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.
     * <p>
     * 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)}.
     * <p>
     * To avoid the complexity, use the {@link Builder Builder} class
     * which provides a streamlined way to configure and create an
     * instance of {@code PassFailJFrame}.
     * <p>
     * Consider updating tests which use {@code PassFailJFrame} constructors to
     * use the builder pattern.
     */
    public final class PassFailJFrame {
    
        private static final String TITLE = "Test Instruction Frame";
        private static final long TEST_TIMEOUT = 5;
        private static final int ROWS = 10;
        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 didn'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.
         * <p>
         * <b>Note:</b> access to this field has to be synchronized by
         * {@code PassFailJFrame.class}.
         */
        private static final List<Window> windowList = new ArrayList<>();
    
        private static final CountDownLatch latch = new CountDownLatch(1);
    
        private static TimeoutHandlerPanel timeoutHandlerPanel;
    
        /**
         * The description of why the test fails.
         * <p>
         * Note: <strong>do not use</strong> 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 <i>Pass</i> / <i>Fail</i> 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.
         * <p>
         * 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 <i>Pass</i> / <i>Fail</i> 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.
         * <p>
         * 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 <i>Pass</i> / <i>Fail</i> 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.
         * <p>
         * 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 <i>Pass</i> / <i>Fail</i> buttons
         * with the given title, instructions, timeout, number of rows and columns.
         * The screenshot feature is not enabled, if you use this constructor.
         * <p>
         * 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 <i>Pass</i> / <i>Fail</i> 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.
         * <p>
         * 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.
         * <p>
         * Call the {@link #awaitAndCheck() awaitAndCheck} method on the instance
         * of {@code PassFailJFrame} when you set up the testing environment.
         * <p>
         * If the tester clicks the <i>Fail</i> 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 <i>Pass</i> button, the test completes
         * successfully.
         * If the timeout occurs or the instruction frame is closed,
         * the test fails.
         * <p>
         * 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)}.
         * <p>
         * If you enable screenshots by setting the {@code screenCapture}
         * parameter to {@code true}, a <i>Screenshot</i> button is added.
         * Clicking the <i>Screenshot</i> 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());
            timeoutHandlerPanel = new TimeoutHandlerPanel(testTimeOut);
            main.add(timeoutHandlerPanel, BorderLayout.NORTH);
    
            JTextComponent text = instructions.startsWith("<html>")
                                  ? configureHTML(instructions, rows, columns)
                                  : configurePlainText(instructions, rows, columns);
            text.setEditable(false);
    
            JPanel textPanel = new JPanel(new BorderLayout());
            textPanel.setBorder(createEmptyBorder(4, 0, 0, 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();
            buttonsPanel.add(btnPass);
            buttonsPanel.add(btnFail);
    
            if (enableScreenCapture) {
                buttonsPanel.add(createCapturePanel());
            }
    
            if (addLogArea) {
                logArea = new JTextArea(logAreaRows, columns);
                logArea.setEditable(false);
    
                Box buttonsLogPanel = Box.createVerticalBox();
    
                buttonsLogPanel.add(buttonsPanel);
                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(createEmptyBorder(4, 4, 4, 4));
            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;
        }
    
    
        /**
         * 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.
             * <p>
             * 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.
             * <p>
             * 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<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 TimeoutHandlerPanel
                extends JPanel
                implements ActionListener {
    
            private static final String PAUSE_BUTTON_LABEL = "Pause";
            private static final String RESUME_BUTTON_LABEL = "Resume";
    
            private long endTime;
            private long pauseTimeLeft;
    
            private final Timer timer;
    
            private final JLabel label;
            private final JButton button;
    
            public TimeoutHandlerPanel(final long testTimeOut) {
                endTime = System.currentTimeMillis()
                        + TimeUnit.MINUTES.toMillis(testTimeOut);
    
                label =  new JLabel("", JLabel.CENTER);
                button = new JButton(PAUSE_BUTTON_LABEL);
    
                button.setFocusPainted(false);
                button.setFont(new Font(Font.DIALOG, Font.BOLD, 10));
                button.addActionListener(e -> pauseToggle());
    
                setLayout(new BorderLayout());
                add(label, BorderLayout.CENTER);
                add(button, BorderLayout.EAST);
    
                timer = new Timer(1000, this);
                timer.start();
                updateTime(testTimeOut);
            }
    
            @Override
            public void actionPerformed(ActionEvent e) {
                long leftTime = endTime - System.currentTimeMillis();
                if (leftTime < 0) {
                    timer.stop();
                    setFailureReason(FAILURE_REASON
                                     + "Timeout - User did not perform testing.");
                    latch.countDown();
                }
                updateTime(leftTime);
            }
    
            private void updateTime(final long leftTime) {
                if (leftTime < 0) {
                    label.setText("Test timeout: 00:00:00");
                    return;
                }
                long hours = leftTime / 3_600_000;
                long minutes = (leftTime - hours * 3_600_000) / 60_000;
                long seconds = (leftTime - hours * 3_600_000 - minutes * 60_000) / 1_000;
                label.setText(String.format(Locale.ENGLISH,
                                            "Test timeout: %02d:%02d:%02d",
                                            hours, minutes, seconds));
            }
    
    
            private void pauseToggle() {
                if (timer.isRunning()) {
                    pauseTimeLeft = endTime - System.currentTimeMillis();
                    timer.stop();
                    label.setEnabled(false);
                    button.setText(RESUME_BUTTON_LABEL);
                } else {
                    endTime = System.currentTimeMillis() + pauseTimeLeft;
                    updateTime(pauseTimeLeft);
                    timer.start();
                    label.setEnabled(true);
                    button.setText(PAUSE_BUTTON_LABEL);
                }
            }
    
            public void stop() {
                timer.stop();
            }
        }
    
    
        private static final class WindowClosingHandler extends WindowAdapter {
            @Override
            public void windowClosing(WindowEvent e) {
                setFailureReason(FAILURE_REASON
                                 + "User closed a window");
                latch.countDown();
            }
        }
    
        private static final WindowListener windowClosingHandler =
                new WindowClosingHandler();
    
    
        private static JComponent createCapturePanel() {
            JComboBox<CaptureType> screenShortType = new JComboBox<>(CaptureType.values());
    
            JButton capture = new JButton("Screenshot");
            capture.addActionListener((e) ->
                    captureScreen((CaptureType) screenShortType.getSelectedItem()));
    
            JPanel panel = new JPanel();
            panel.add(screenShortType);
            panel.add(capture);
            return panel;
        }
    
        private enum CaptureType {
            FULL_SCREEN("Capture Full Screen"),
            WINDOWS("Capture Frames");
    
            private final String type;
            CaptureType(String type) {
                this.type = type;
            }
    
            @Override
            public String toString() {
                return type;
            }
        }
    
        private static Robot createRobot() {
            if (robot == null) {
                try {
                    robot = new Robot();
                } catch (AWTException e) {
                    String errorMsg = "Failed to create an instance of Robot.";
                    JOptionPane.showMessageDialog(frame, errorMsg, "Failed",
                                                  JOptionPane.ERROR_MESSAGE);
                    forceFail(errorMsg + e.getMessage());
                }
            }
            return robot;
        }
    
        private static void captureScreen(Rectangle bounds) {
            Robot robot = createRobot();
    
            List<Image> imageList = robot.createMultiResolutionScreenCapture(bounds)
                                         .getResolutionVariants();
            Image image = imageList.get(imageList.size() - 1);
    
            File file = new File("CaptureScreen_"
                                 + imgCounter.incrementAndGet() + ".png");
            try {
                ImageIO.write((RenderedImage) image, "png", file);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
        private static void captureScreen(CaptureType type) {
            switch (type) {
                case FULL_SCREEN:
                    Arrays.stream(GraphicsEnvironment.getLocalGraphicsEnvironment()
                                                     .getScreenDevices())
                          .map(GraphicsDevice::getDefaultConfiguration)
                          .map(GraphicsConfiguration::getBounds)
                          .forEach(PassFailJFrame::captureScreen);
                    break;
    
                case WINDOWS:
                    synchronized (PassFailJFrame.class) {
                        windowList.stream()
                                  .filter(Window::isShowing)
                                  .map(Window::getBounds)
                                  .forEach(PassFailJFrame::captureScreen);
                    }
                    break;
    
                default:
                    throw new IllegalStateException("Unexpected value of capture type");
            }
    
            JOptionPane.showMessageDialog(frame,
                                          "Screen Captured Successfully",
                                          "Screen Capture",
                                          JOptionPane.INFORMATION_MESSAGE);
        }
    
        /**
         * Sets the failure reason which describes why the test fails.
         * This method ensures the {@code failureReason} field does not change
         * after it's set to a non-{@code null} value.
         * @param reason the description of why the test fails
         * @throws IllegalArgumentException if the {@code reason} parameter
         *         is {@code null}
         */
        private static synchronized void setFailureReason(final String reason) {
            if (reason == null) {
                throw new IllegalArgumentException("The failure reason must not be null");
            }
            if (failureReason == null) {
                failureReason = reason;
            }
        }
    
        /**
         * {@return the description of why the test fails}
         */
        private static synchronized String getFailureReason() {
            return failureReason;
        }
    
        /**
         * Wait for the user decision i,e user selects pass or fail button.
         * If user does not select pass or fail button then the test waits for
         * the specified timeoutMinutes period and the test gets timeout.
         * Note: This method should be called from main() thread
         *
         * @throws InterruptedException      exception thrown when thread is
         *                                   interrupted
         * @throws InvocationTargetException if an exception is thrown while
         *                                   disposing of frames on EDT
         */
        public void awaitAndCheck() throws InterruptedException, InvocationTargetException {
            if (isEventDispatchThread()) {
                throw new IllegalStateException("awaitAndCheck() should not be called on EDT");
            }
            latch.await();
            invokeAndWait(PassFailJFrame::disposeWindows);
    
            String failure = getFailureReason();
            if (failure != null) {
                throw new RuntimeException(failure);
            }
    
            System.out.println("Test passed!");
        }
    
        /**
         * Requests the description of the test failure reason from the tester.
         */
        private static void requestFailureReason() {
            final JDialog dialog = new JDialog(frame, "Test Failure ", true);
            dialog.setTitle("Failure reason");
            JPanel jPanel = new JPanel(new BorderLayout());
            JTextArea jTextArea = new JTextArea(5, 20);
    
            JButton okButton = new JButton("OK");
            okButton.addActionListener((ae) -> {
                String text = jTextArea.getText();
                setFailureReason(FAILURE_REASON
                                 + (!text.isEmpty() ? text : EMPTY_REASON));
                dialog.setVisible(false);
            });
    
            jPanel.add(new JScrollPane(jTextArea), BorderLayout.CENTER);
    
            JPanel okayBtnPanel = new JPanel();
            okayBtnPanel.add(okButton);
    
            jPanel.add(okayBtnPanel, BorderLayout.SOUTH);
            dialog.add(jPanel);
            dialog.setLocationRelativeTo(frame);
            dialog.pack();
            dialog.setVisible(true);
    
            // Ensure the test fails even if the dialog is closed
            // without clicking the OK button
            setFailureReason(FAILURE_REASON + EMPTY_REASON);
    
            dialog.dispose();
            latch.countDown();
        }
    
        /**
         * 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() {
            windowList.forEach(Window::dispose);
        }
    
        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 + WINDOW_GAP) / 2) - frame.getWidth());
                    frame.setLocation((newX + screenInsets.left),
                                      (frame.getY() + screenInsets.top));
                    break;
    
                case VERTICAL:
                    int newY = (((screenSize.height + WINDOW_GAP) / 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}
         * is {@code null}, only the instruction frame is positioned according to
         * {@code position} parameter.
         * <p>This method should be called before making the test window visible
         * to avoid flickering.</p>
         *
         * @param testWindow test window that the test created.
         *                   May be {@code null}.
         *
         * @param position  position must be one of:
         *                  <ul>
         *                  <li>{@code HORIZONTAL} - the test instruction frame is positioned
         *                  such that its right edge aligns with screen's horizontal center
         *                  and the test window (if not {@code null}) is placed to the right
         *                  of the instruction frame.</li>
         *
         *                  <li>{@code VERTICAL} - the test instruction frame is positioned
         *                  such that its bottom edge aligns with the screen's vertical center
         *                  and the test window (if not {@code null}) is placed below the
         *                  instruction frame.</li>
         *
         *                  <li>{@code TOP_LEFT_CORNER} - the test instruction frame is positioned
         *                  such that its top left corner is at the top left corner of the screen
         *                  and the test window (if not {@code null}) is placed to the right of
         *                  the instruction frame.</li>
         *                  </ul>
         */
        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 &mdash; use {@link #showAllWindows() showAllWindows} to ensure it.
         */
        private static synchronized void showUI() {
            windowList.forEach(w -> w.setVisible(true));
        }
    
    
        /**
         * Forcibly pass the test.
         * <p>The sample usage:
         * <pre><code>
         *      PrinterJob pj = PrinterJob.getPrinterJob();
         *      if (pj == null || pj.getPrintService() == null) {
         *          System.out.println(""Printer not configured or available.");
         *          PassFailJFrame.forcePass();
         *      }
         * </code></pre>
         */
        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.
             * <p>
             * 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.
             * <p>
             * The log area can be controlled by {@link #log(String)},
             * {@link #logClear()} and {@link #logSet(String)}.
             * <p>
             * 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.
             * <p>
             * 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);
            }
    
            private void validate() {
                if (title == null) {
                    title = 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();
        }
    }