1 /*
   2  * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.awt.AWTException;
  25 import java.awt.BorderLayout;
  26 import java.awt.Dimension;
  27 import java.awt.GraphicsConfiguration;
  28 import java.awt.GraphicsDevice;
  29 import java.awt.GraphicsEnvironment;
  30 import java.awt.Image;
  31 import java.awt.Insets;
  32 import java.awt.Point;
  33 import java.awt.Rectangle;
  34 import java.awt.Robot;
  35 import java.awt.Toolkit;
  36 import java.awt.Window;
  37 import java.awt.event.ActionEvent;
  38 import java.awt.event.ActionListener;
  39 import java.awt.event.WindowAdapter;
  40 import java.awt.event.WindowEvent;
  41 import java.awt.event.WindowListener;
  42 import java.awt.image.RenderedImage;
  43 import java.io.File;
  44 import java.io.IOException;
  45 import java.lang.reflect.InvocationTargetException;
  46 import java.util.ArrayList;
  47 import java.util.Arrays;
  48 import java.util.Collection;
  49 import java.util.List;
  50 import java.util.Locale;
  51 import java.util.Objects;
  52 import java.util.concurrent.CountDownLatch;
  53 import java.util.concurrent.TimeUnit;
  54 import java.util.concurrent.atomic.AtomicInteger;
  55 
  56 import javax.imageio.ImageIO;
  57 import javax.swing.JButton;
  58 import javax.swing.JComboBox;
  59 import javax.swing.JComponent;
  60 import javax.swing.JDialog;
  61 import javax.swing.JEditorPane;
  62 import javax.swing.JFrame;
  63 import javax.swing.JLabel;
  64 import javax.swing.JOptionPane;
  65 import javax.swing.JPanel;
  66 import javax.swing.JScrollPane;
  67 import javax.swing.JTextArea;
  68 import javax.swing.Timer;
  69 import javax.swing.text.JTextComponent;
  70 import javax.swing.text.html.HTMLEditorKit;
  71 import javax.swing.text.html.StyleSheet;
  72 
  73 import static java.util.Collections.unmodifiableList;
  74 import static javax.swing.SwingUtilities.invokeAndWait;
  75 import static javax.swing.SwingUtilities.isEventDispatchThread;
  76 
  77 /**
  78  * Provides a framework for manual tests to display test instructions and
  79  * Pass/Fail buttons.
  80  * <p>
  81  * Instructions for the user can be either plain text or HTML as supported
  82  * by Swing. If the instructions start with {@code <html>}, the
  83  * instructions are displayed as HTML.
  84  * <p>
  85  * A simple test would look like this:
  86  * <pre>{@code
  87  * public class SampleManualTest {
  88  *     private static final String INSTRUCTIONS =
  89  *             "Click Pass, or click Fail if the test failed.";
  90  *
  91  *     public static void main(String[] args) throws Exception {
  92  *         PassFailJFrame.builder()
  93  *                       .instructions(INSTRUCTIONS)
  94  *                       .testUI(() -> createTestUI())
  95  *                       .build()
  96  *                       .awaitAndCheck();
  97  *     }
  98  *
  99  *     private static Window createTestUI() {
 100  *         JFrame testUI = new JFrame("Test UI");
 101  *         testUI.setSize(250, 150);
 102  *         return testUI;
 103  *     }
 104  * }
 105  * }</pre>
 106  * <p>
 107  * The above example uses the {@link Builder Builder} to set the parameters of
 108  * the instruction frame. It is the recommended way.
 109  * <p>
 110  * The framework will create instruction UI, it will call
 111  * the provided {@code createTestUI} on the Event Dispatch Thread (EDT),
 112  * and it will automatically position the test UI and make it visible.
 113  * <p>
 114  * The {@code Builder.testUI} methods accept interfaces which create one window
 115  * or a list of windows if the test needs multiple windows,
 116  * or directly a single window, an array of windows or a list of windows.
 117  * <p>
 118  * Alternatively, use one of the {@code PassFailJFrame} constructors to
 119  * create an object, then create secondary test UI, register it
 120  * with {@code PassFailJFrame}, position it and make it visible.
 121  * The following sample demonstrates it:
 122  * <pre>{@code
 123  * public class SampleOldManualTest {
 124  *     private static final String INSTRUCTIONS =
 125  *             "Click Pass, or click Fail if the test failed.";
 126  *
 127  *     public static void main(String[] args) throws Exception {
 128  *         PassFailJFrame passFail = new PassFailJFrame(INSTRUCTIONS);
 129  *
 130  *         SwingUtilities.invokeAndWait(() -> createTestUI());
 131  *
 132  *         passFail.awaitAndCheck();
 133  *     }
 134  *
 135  *     private static void createTestUI() {
 136  *         JFrame testUI = new JFrame("Test UI");
 137  *         testUI.setSize(250, 150);
 138  *         PassFailJFrame.addTestWindow(testUI);
 139  *         PassFailJFrame.positionTestWindow(testUI, PassFailJFrame.Position.HORIZONTAL);
 140  *         testUI.setVisible(true);
 141  *     }
 142  * }
 143  * }</pre>
 144  * <p>
 145  * Use methods of the {@code Builder} class or constructors of the
 146  * {@code PassFailJFrame} class to control other parameters:
 147  * <ul>
 148  *     <li>the title of the instruction UI,</li>
 149  *     <li>the timeout of the test,</li>
 150  *     <li>the size of the instruction UI via rows and columns, and</li>
 151  *     <li>to enable screenshots.</li>
 152  * </ul>
 153  */
 154 public final class PassFailJFrame {
 155 
 156     private static final String TITLE = "Test Instruction Frame";
 157     private static final long TEST_TIMEOUT = 5;
 158     private static final int ROWS = 10;
 159     private static final int COLUMNS = 40;
 160 
 161     /**
 162      * Prefix for the user-provided failure reason.
 163      */
 164     private static final String FAILURE_REASON = "Failure Reason:\n";
 165     /**
 166      * The failure reason message when the user didn't provide one.
 167      */
 168     private static final String EMPTY_REASON = "(no reason provided)";
 169 
 170     private static final List<Window> windowList = new ArrayList<>();
 171 
 172     private static final CountDownLatch latch = new CountDownLatch(1);
 173 
 174     private static TimeoutHandler timeoutHandler;
 175 
 176     /**
 177      * The description of why the test fails.
 178      * <p>
 179      * Note: <strong>do not use</strong> this field directly,
 180      * use the {@link #setFailureReason(String) setFailureReason} and
 181      * {@link #getFailureReason() getFailureReason} methods to modify and
 182      * to read its value.
 183      */
 184     private static String failureReason;
 185 
 186     private static final AtomicInteger imgCounter = new AtomicInteger(0);
 187 
 188     private static JFrame frame;
 189 
 190     private static Robot robot;
 191 
 192     public enum Position {HORIZONTAL, VERTICAL, TOP_LEFT_CORNER}
 193 
 194     public PassFailJFrame(String instructions) throws InterruptedException,
 195             InvocationTargetException {
 196         this(instructions, TEST_TIMEOUT);
 197     }
 198 
 199     public PassFailJFrame(String instructions, long testTimeOut) throws
 200             InterruptedException, InvocationTargetException {
 201         this(TITLE, instructions, testTimeOut);
 202     }
 203 
 204     public PassFailJFrame(String title, String instructions,
 205                           long testTimeOut) throws InterruptedException,
 206             InvocationTargetException {
 207         this(title, instructions, testTimeOut, ROWS, COLUMNS);
 208     }
 209 
 210     /**
 211      * Constructs a JFrame with a given title & serves as test instructional
 212      * frame where the user follows the specified test instruction in order
 213      * to test the test case & mark the test pass or fail. If the expected
 214      * result is seen then the user click on the 'Pass' button else click
 215      * on the 'Fail' button and the reason for the failure should be
 216      * specified in the JDialog JTextArea.
 217      *
 218      * @param title        title of the Frame.
 219      * @param instructions the instruction for the tester on how to test
 220      *                     and what is expected (pass) and what is not
 221      *                     expected (fail).
 222      * @param testTimeOut  test timeout where time is specified in minutes.
 223      * @param rows         number of visible rows of the JTextArea where the
 224      *                     instruction is show.
 225      * @param columns      Number of columns of the instructional
 226      *                     JTextArea
 227      * @throws InterruptedException      exception thrown when thread is
 228      *                                   interrupted
 229      * @throws InvocationTargetException if an exception is thrown while
 230      *                                   creating the test instruction frame on
 231      *                                   EDT
 232      */
 233     public PassFailJFrame(String title, String instructions, long testTimeOut,
 234                           int rows, int columns) throws InterruptedException,
 235             InvocationTargetException {
 236         this(title, instructions, testTimeOut, rows, columns, false);
 237     }
 238 
 239     /**
 240      * Constructs a JFrame with a given title & serves as test instructional
 241      * frame where the user follows the specified test instruction in order
 242      * to test the test case & mark the test pass or fail. If the expected
 243      * result is seen then the user click on the 'Pass' button else click
 244      * on the 'Fail' button and the reason for the failure should be
 245      * specified in the JDialog JTextArea.
 246      * <p>
 247      * The test instruction frame also provides a way for the tester to take
 248      * a screenshot (full screen or individual frame) if this feature
 249      * is enabled by passing {@code true} as {@code  enableScreenCapture}
 250      * parameter.
 251      *
 252      * @param title        title of the Frame.
 253      * @param instructions the instruction for the tester on how to test
 254      *                     and what is expected (pass) and what is not
 255      *                     expected (fail).
 256      * @param testTimeOut  test timeout where time is specified in minutes.
 257      * @param rows         number of visible rows of the JTextArea where the
 258      *                     instruction is show.
 259      * @param columns      Number of columns of the instructional
 260      *                     JTextArea
 261      * @param enableScreenCapture if set to true, 'Capture Screen' button & its
 262      *                            associated UIs are added to test instruction
 263      *                            frame
 264      * @throws InterruptedException      exception thrown when thread is
 265      *                                   interrupted
 266      * @throws InvocationTargetException if an exception is thrown while
 267      *                                   creating the test instruction frame on
 268      *                                   EDT
 269      */
 270     public PassFailJFrame(String title, String instructions, long testTimeOut,
 271                           int rows, int columns,
 272                           boolean enableScreenCapture)
 273             throws InterruptedException, InvocationTargetException {
 274         invokeOnEDT(() -> createUI(title, instructions,
 275                                    testTimeOut,
 276                                    rows, columns,
 277                                    enableScreenCapture));
 278     }
 279 
 280     private PassFailJFrame(Builder builder) throws InterruptedException,
 281             InvocationTargetException {
 282         this(builder.title, builder.instructions, builder.testTimeOut,
 283              builder.rows, builder.columns, builder.screenCapture);
 284 
 285         if (builder.windowListCreator != null) {
 286             invokeOnEDT(() ->
 287                     builder.testWindows = builder.windowListCreator.createTestUI());
 288             if (builder.testWindows == null) {
 289                 throw new IllegalStateException("Window list creator returned null list");
 290             }
 291         }
 292 
 293         if (builder.testWindows != null) {
 294             if (builder.testWindows.isEmpty()) {
 295                 throw new IllegalStateException("Window list is empty");
 296             }
 297             addTestWindow(builder.testWindows);
 298             builder.testWindows
 299                    .forEach(w -> w.addWindowListener(windowClosingHandler));
 300 
 301             if (builder.positionWindows != null) {
 302                 positionInstructionFrame(builder.position);
 303                 invokeOnEDT(() -> {
 304                     builder.positionWindows
 305                            .positionTestWindows(unmodifiableList(builder.testWindows),
 306                                                 builder.instructionUIHandler);
 307                 });
 308             } else if (builder.testWindows.size() == 1) {
 309                 Window window = builder.testWindows.get(0);
 310                 positionTestWindow(window, builder.position);
 311             } else {
 312                 positionTestWindow(null, builder.position);
 313             }
 314         }
 315         showAllWindows();
 316     }
 317 
 318     /**
 319      * Performs an operation on EDT. If called on EDT, invokes {@code run}
 320      * directly, otherwise wraps into {@code invokeAndWait}.
 321      *
 322      * @param doRun an operation to run on EDT
 323      * @throws InterruptedException if we're interrupted while waiting for
 324      *              the event dispatching thread to finish executing
 325      *              {@code doRun.run()}
 326      * @throws InvocationTargetException if an exception is thrown while
 327      *              running {@code doRun}
 328      * @see javax.swing.SwingUtilities#invokeAndWait(Runnable)
 329      */
 330     private static void invokeOnEDT(Runnable doRun)
 331             throws InterruptedException, InvocationTargetException {
 332         if (isEventDispatchThread()) {
 333             doRun.run();
 334         } else {
 335             invokeAndWait(doRun);
 336         }
 337     }
 338 
 339     private static void createUI(String title, String instructions,
 340                                  long testTimeOut, int rows, int columns,
 341                                  boolean enableScreenCapture) {
 342         frame = new JFrame(title);
 343         frame.setLayout(new BorderLayout());
 344 
 345         JLabel testTimeoutLabel = new JLabel("", JLabel.CENTER);
 346         timeoutHandler = new TimeoutHandler(testTimeoutLabel, testTimeOut);
 347         frame.add(testTimeoutLabel, BorderLayout.NORTH);
 348 
 349         JTextComponent text = instructions.startsWith("<html>")
 350                               ? configureHTML(instructions, rows, columns)
 351                               : configurePlainText(instructions, rows, columns);
 352         text.setEditable(false);
 353 
 354         frame.add(new JScrollPane(text), BorderLayout.CENTER);
 355 
 356         JButton btnPass = new JButton("Pass");
 357         btnPass.addActionListener((e) -> {
 358             latch.countDown();
 359             timeoutHandler.stop();
 360         });
 361 
 362         JButton btnFail = new JButton("Fail");
 363         btnFail.addActionListener((e) -> {
 364             requestFailureReason();
 365             timeoutHandler.stop();
 366         });
 367 
 368         JPanel buttonsPanel = new JPanel();
 369         buttonsPanel.add(btnPass);
 370         buttonsPanel.add(btnFail);
 371 
 372         if (enableScreenCapture) {
 373             buttonsPanel.add(createCapturePanel());
 374         }
 375 
 376         frame.addWindowListener(windowClosingHandler);
 377 
 378         frame.add(buttonsPanel, BorderLayout.SOUTH);
 379         frame.pack();
 380         frame.setLocationRelativeTo(null);
 381         addTestWindow(frame);
 382     }
 383 
 384     private static JTextComponent configurePlainText(String instructions,
 385                                                      int rows, int columns) {
 386         JTextArea text = new JTextArea(instructions, rows, columns);
 387         text.setLineWrap(true);
 388         text.setWrapStyleWord(true);
 389         return text;
 390     }
 391 
 392     private static JTextComponent configureHTML(String instructions,
 393                                                 int rows, int columns) {
 394         JEditorPane text = new JEditorPane("text/html", instructions);
 395         text.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES,
 396                                Boolean.TRUE);
 397         // Set preferred size as if it were JTextArea
 398         text.setPreferredSize(new JTextArea(rows, columns).getPreferredSize());
 399 
 400         HTMLEditorKit kit = (HTMLEditorKit) text.getEditorKit();
 401         StyleSheet styles = kit.getStyleSheet();
 402         // Reduce the default margins
 403         styles.addRule("ol, ul { margin-left-ltr: 20; margin-left-rtl: 20 }");
 404         // Make the size of code blocks the same as other text
 405         styles.addRule("code { font-size: inherit }");
 406 
 407         return text;
 408     }
 409 
 410 
 411     /**
 412      * Creates a test UI window.
 413      */
 414     @FunctionalInterface
 415     public interface WindowCreator {
 416         /**
 417          * Creates a window for test UI.
 418          * This method is called by the framework on the EDT.
 419          * @return a test UI window
 420          */
 421         Window createTestUI();
 422     }
 423 
 424     /**
 425      * Creates a list of test UI windows.
 426      */
 427     @FunctionalInterface
 428     public interface WindowListCreator {
 429         /**
 430          * Creates one or more windows for test UI.
 431          * This method is called by the framework on the EDT.
 432          * @return a list of test UI windows
 433          */
 434         List<? extends Window> createTestUI();
 435     }
 436 
 437     /**
 438      * Positions test UI windows.
 439      */
 440     @FunctionalInterface
 441     public interface PositionWindows {
 442         /**
 443          * Positions test UI windows.
 444          * This method is called by the framework on the EDT after
 445          * the instruction UI frame was positioned on the screen.
 446          * <p>
 447          * The list of the test windows contains the windows
 448          * that were passed to the framework via the
 449          * {@link Builder#testUI(Window...) testUI(Window...)} method or
 450          * that were created with {@code WindowCreator}
 451          * or {@code WindowListCreator} which were passed via
 452          * {@link Builder#testUI(WindowCreator) testUI(WindowCreator)} or
 453          * {@link Builder#testUI(WindowListCreator) testUI(WindowListCreator)}
 454          * correspondingly.
 455          *
 456          * @param testWindows the list of test windows
 457          * @param instructionUI information about the instruction frame
 458          */
 459         void positionTestWindows(List<? extends Window> testWindows,
 460                                  InstructionUI instructionUI);
 461     }
 462 
 463     /**
 464      * Provides information about the instruction frame.
 465      */
 466     public interface InstructionUI {
 467         /**
 468          * {@return the location of the instruction frame}
 469          */
 470         Point getLocation();
 471 
 472         /**
 473          * {@return the size of the instruction frame}
 474          */
 475         Dimension getSize();
 476 
 477         /**
 478          * {@return the bounds of the instruction frame}
 479          */
 480         Rectangle getBounds();
 481 
 482         /**
 483          * Allows to change the location of the instruction frame.
 484          *
 485          * @param location the new location of the instruction frame
 486          */
 487         void setLocation(Point location);
 488 
 489         /**
 490          * Allows to change the location of the instruction frame.
 491          *
 492          * @param x the <i>x</i> coordinate of the new location
 493          * @param y the <i>y</i> coordinate of the new location
 494          */
 495         void setLocation(int x, int y);
 496 
 497         /**
 498          * Returns the specified position that was used to set
 499          * the initial location of the instruction frame.
 500          *
 501          * @return the specified position
 502          *
 503          * @see Position
 504          */
 505         Position getPosition();
 506     }
 507 
 508 
 509     private static final class TimeoutHandler implements ActionListener {
 510         private final long endTime;
 511 
 512         private final Timer timer;
 513 
 514         private final JLabel label;
 515 
 516         public TimeoutHandler(final JLabel label, final long testTimeOut) {
 517             endTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(testTimeOut);
 518 
 519             this.label = label;
 520 
 521             timer = new Timer(1000, this);
 522             timer.start();
 523             updateTime(testTimeOut);
 524         }
 525 
 526         @Override
 527         public void actionPerformed(ActionEvent e) {
 528             long leftTime = endTime - System.currentTimeMillis();
 529             if (leftTime < 0) {
 530                 timer.stop();
 531                 setFailureReason(FAILURE_REASON
 532                                  + "Timeout - User did not perform testing.");
 533                 latch.countDown();
 534             }
 535             updateTime(leftTime);
 536         }
 537 
 538         private void updateTime(final long leftTime) {
 539             if (leftTime < 0) {
 540                 label.setText("Test timeout: 00:00:00");
 541                 return;
 542             }
 543             long hours = leftTime / 3_600_000;
 544             long minutes = (leftTime - hours * 3_600_000) / 60_000;
 545             long seconds = (leftTime - hours * 3_600_000 - minutes * 60_000) / 1_000;
 546             label.setText(String.format(Locale.ENGLISH,
 547                                         "Test timeout: %02d:%02d:%02d",
 548                                         hours, minutes, seconds));
 549         }
 550 
 551         public void stop() {
 552             timer.stop();
 553         }
 554     }
 555 
 556 
 557     private static final class WindowClosingHandler extends WindowAdapter {
 558         @Override
 559         public void windowClosing(WindowEvent e) {
 560             setFailureReason(FAILURE_REASON
 561                              + "User closed a window");
 562             latch.countDown();
 563         }
 564     }
 565 
 566     private static final WindowListener windowClosingHandler =
 567             new WindowClosingHandler();
 568 
 569 
 570     private static JComponent createCapturePanel() {
 571         JComboBox<CaptureType> screenShortType = new JComboBox<>(CaptureType.values());
 572 
 573         JButton capture = new JButton("ScreenShot");
 574         capture.addActionListener((e) ->
 575                 captureScreen((CaptureType) screenShortType.getSelectedItem()));
 576 
 577         JPanel panel = new JPanel();
 578         panel.add(screenShortType);
 579         panel.add(capture);
 580         return panel;
 581     }
 582 
 583     private enum CaptureType {
 584         FULL_SCREEN("Capture Full Screen"),
 585         WINDOWS("Capture Individual Frame");
 586 
 587         private final String type;
 588         CaptureType(String type) {
 589             this.type = type;
 590         }
 591 
 592         @Override
 593         public String toString() {
 594             return type;
 595         }
 596     }
 597 
 598     private static Robot createRobot() {
 599         if (robot == null) {
 600             try {
 601                 robot = new Robot();
 602             } catch (AWTException e) {
 603                 String errorMsg = "Failed to create an instance of Robot.";
 604                 JOptionPane.showMessageDialog(frame, errorMsg, "Failed",
 605                                               JOptionPane.ERROR_MESSAGE);
 606                 forceFail(errorMsg + e.getMessage());
 607             }
 608         }
 609         return robot;
 610     }
 611 
 612     private static void captureScreen(Rectangle bounds) {
 613         Robot robot = createRobot();
 614 
 615         List<Image> imageList = robot.createMultiResolutionScreenCapture(bounds)
 616                                      .getResolutionVariants();
 617         Image image = imageList.get(imageList.size() - 1);
 618 
 619         File file = new File("CaptureScreen_"
 620                              + imgCounter.incrementAndGet() + ".png");
 621         try {
 622             ImageIO.write((RenderedImage) image, "png", file);
 623         } catch (IOException e) {
 624             throw new RuntimeException(e);
 625         }
 626     }
 627 
 628     private static void captureScreen(CaptureType type) {
 629         switch (type) {
 630             case FULL_SCREEN:
 631                 Arrays.stream(GraphicsEnvironment.getLocalGraphicsEnvironment()
 632                                                  .getScreenDevices())
 633                       .map(GraphicsDevice::getDefaultConfiguration)
 634                       .map(GraphicsConfiguration::getBounds)
 635                       .forEach(PassFailJFrame::captureScreen);
 636                 break;
 637 
 638             case WINDOWS:
 639                 windowList.stream()
 640                           .filter(Window::isShowing)
 641                           .map(Window::getBounds)
 642                           .forEach(PassFailJFrame::captureScreen);
 643                 break;
 644 
 645             default:
 646                 throw new IllegalStateException("Unexpected value of capture type");
 647         }
 648 
 649         JOptionPane.showMessageDialog(frame,
 650                                       "Screen Captured Successfully",
 651                                       "Screen Capture",
 652                                       JOptionPane.INFORMATION_MESSAGE);
 653     }
 654 
 655     /**
 656      * Sets the failure reason which describes why the test fails.
 657      * This method ensures the {@code failureReason} field does not change
 658      * after it's set to a non-{@code null} value.
 659      * @param reason the description of why the test fails
 660      * @throws IllegalArgumentException if the {@code reason} parameter
 661      *         is {@code null}
 662      */
 663     private static synchronized void setFailureReason(final String reason) {
 664         if (reason == null) {
 665             throw new IllegalArgumentException("The failure reason must not be null");
 666         }
 667         if (failureReason == null) {
 668             failureReason = reason;
 669         }
 670     }
 671 
 672     /**
 673      * {@return the description of why the test fails}
 674      */
 675     private static synchronized String getFailureReason() {
 676         return failureReason;
 677     }
 678 
 679     /**
 680      * Wait for the user decision i,e user selects pass or fail button.
 681      * If user does not select pass or fail button then the test waits for
 682      * the specified timeoutMinutes period and the test gets timeout.
 683      * Note: This method should be called from main() thread
 684      *
 685      * @throws InterruptedException      exception thrown when thread is
 686      *                                   interrupted
 687      * @throws InvocationTargetException if an exception is thrown while
 688      *                                   disposing of frames on EDT
 689      */
 690     public void awaitAndCheck() throws InterruptedException, InvocationTargetException {
 691         if (isEventDispatchThread()) {
 692             throw new IllegalStateException("awaitAndCheck() should not be called on EDT");
 693         }
 694         latch.await();
 695         invokeAndWait(PassFailJFrame::disposeWindows);
 696 
 697         String failure = getFailureReason();
 698         if (failure != null) {
 699             throw new RuntimeException(failure);
 700         }
 701 
 702         System.out.println("Test passed!");
 703     }
 704 
 705     /**
 706      * Requests the description of the test failure reason from the tester.
 707      */
 708     private static void requestFailureReason() {
 709         final JDialog dialog = new JDialog(frame, "Test Failure ", true);
 710         dialog.setTitle("Failure reason");
 711         JPanel jPanel = new JPanel(new BorderLayout());
 712         JTextArea jTextArea = new JTextArea(5, 20);
 713 
 714         JButton okButton = new JButton("OK");
 715         okButton.addActionListener((ae) -> {
 716             String text = jTextArea.getText();
 717             setFailureReason(FAILURE_REASON
 718                              + (!text.isEmpty() ? text : EMPTY_REASON));
 719             dialog.setVisible(false);
 720         });
 721 
 722         jPanel.add(new JScrollPane(jTextArea), BorderLayout.CENTER);
 723 
 724         JPanel okayBtnPanel = new JPanel();
 725         okayBtnPanel.add(okButton);
 726 
 727         jPanel.add(okayBtnPanel, BorderLayout.SOUTH);
 728         dialog.add(jPanel);
 729         dialog.setLocationRelativeTo(frame);
 730         dialog.pack();
 731         dialog.setVisible(true);
 732 
 733         // Ensure the test fails even if the dialog is closed
 734         // without clicking the OK button
 735         setFailureReason(FAILURE_REASON + EMPTY_REASON);
 736 
 737         dialog.dispose();
 738         latch.countDown();
 739     }
 740 
 741     /**
 742      * Disposes of all the windows. It disposes of the test instruction frame
 743      * and all other windows added via {@link #addTestWindow(Window)}.
 744      */
 745     private static synchronized void disposeWindows() {
 746         windowList.forEach(Window::dispose);
 747     }
 748 
 749     private static void positionInstructionFrame(final Position position) {
 750         Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 751 
 752         // Get the screen insets to position the frame by taking into
 753         // account the location of taskbar or menu bar on screen.
 754         GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment()
 755                                                       .getDefaultScreenDevice()
 756                                                       .getDefaultConfiguration();
 757         Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc);
 758 
 759         switch (position) {
 760             case HORIZONTAL:
 761                 int newX = ((screenSize.width / 2) - frame.getWidth());
 762                 frame.setLocation((newX + screenInsets.left),
 763                                   (frame.getY() + screenInsets.top));
 764                 break;
 765 
 766             case VERTICAL:
 767                 int newY = ((screenSize.height / 2) - frame.getHeight());
 768                 frame.setLocation((frame.getX() + screenInsets.left),
 769                                   (newY + screenInsets.top));
 770                 break;
 771 
 772             case TOP_LEFT_CORNER:
 773                 frame.setLocation(screenInsets.left, screenInsets.top);
 774                 break;
 775         }
 776         syncLocationToWindowManager();
 777     }
 778 
 779     /**
 780      * Approximately positions the instruction frame relative to the test
 781      * window as specified by the {@code position} parameter. If {@code testWindow}
 782      * is {@code null}, only the instruction frame is positioned according to
 783      * {@code position} parameter.
 784      * <p>This method should be called before making the test window visible
 785      * to avoid flickering.</p>
 786      *
 787      * @param testWindow test window that the test created.
 788      *                   May be {@code null}.
 789      *
 790      * @param position  position must be one of:
 791      *                  <ul>
 792      *                  <li>{@code HORIZONTAL} - the test instruction frame is positioned
 793      *                  such that its right edge aligns with screen's horizontal center
 794      *                  and the test window (if not {@code null}) is placed to the right
 795      *                  of the instruction frame.</li>
 796      *
 797      *                  <li>{@code VERTICAL} - the test instruction frame is positioned
 798      *                  such that its bottom edge aligns with the screen's vertical center
 799      *                  and the test window (if not {@code null}) is placed below the
 800      *                  instruction frame.</li>
 801      *
 802      *                  <li>{@code TOP_LEFT_CORNER} - the test instruction frame is positioned
 803      *                  such that its top left corner is at the top left corner of the screen
 804      *                  and the test window (if not {@code null}) is placed to the right of
 805      *                  the instruction frame.</li>
 806      *                  </ul>
 807      */
 808     public static void positionTestWindow(Window testWindow, Position position) {
 809         positionInstructionFrame(position);
 810 
 811         if (testWindow != null) {
 812             switch (position) {
 813                 case HORIZONTAL:
 814                 case TOP_LEFT_CORNER:
 815                     testWindow.setLocation((frame.getX() + frame.getWidth() + 5),
 816                                            frame.getY());
 817                     break;
 818 
 819                 case VERTICAL:
 820                     testWindow.setLocation(frame.getX(),
 821                                            (frame.getY() + frame.getHeight() + 5));
 822                     break;
 823             }
 824         }
 825 
 826         // make instruction frame visible after updating
 827         // frame & window positions
 828         frame.setVisible(true);
 829     }
 830 
 831     /**
 832      * Ensures the frame location is updated by the window manager
 833      * if it adjusts the frame location after {@code setLocation}.
 834      *
 835      * @see #positionTestWindow
 836      */
 837     private static void syncLocationToWindowManager() {
 838         Toolkit.getDefaultToolkit().sync();
 839         try {
 840             Thread.sleep(500);
 841         } catch (InterruptedException e) {
 842             e.printStackTrace();
 843         }
 844     }
 845 
 846     /**
 847      * Returns the current position and size of the test instruction frame.
 848      * This method can be used in scenarios when custom positioning of
 849      * multiple test windows w.r.t test instruction frame is necessary,
 850      * at test-case level and the desired configuration is not available
 851      * as a {@code Position} option.
 852      *
 853      * @return Rectangle bounds of test instruction frame
 854      * @see #positionTestWindow
 855      *
 856      * @throws InterruptedException      exception thrown when thread is
 857      *                                   interrupted
 858      * @throws InvocationTargetException if an exception is thrown while
 859      *                                   obtaining frame bounds on EDT
 860      */
 861     public static Rectangle getInstructionFrameBounds()
 862             throws InterruptedException, InvocationTargetException {
 863         final Rectangle[] bounds = {null};
 864 
 865         invokeOnEDT(() -> bounds[0] = frame != null ? frame.getBounds() : null);
 866         return bounds[0];
 867     }
 868 
 869     /**
 870      * Add the testWindow to the windowList so that test instruction frame
 871      * and testWindow and any other windows used in this test is disposed
 872      * via disposeWindows().
 873      *
 874      * @param testWindow testWindow that needs to be disposed
 875      */
 876     public static synchronized void addTestWindow(Window testWindow) {
 877         windowList.add(testWindow);
 878     }
 879 
 880     /**
 881      * Adds a collection of test windows to the windowList to be disposed of
 882      * when the test completes.
 883      *
 884      * @param testWindows the collection of test windows to be disposed of
 885      */
 886     public static synchronized void addTestWindow(Collection<? extends Window> testWindows) {
 887         windowList.addAll(testWindows);
 888     }
 889 
 890     /**
 891      * Displays all the windows in {@code windowList}.
 892      *
 893      * @throws InterruptedException if the thread is interrupted while
 894      *              waiting for the event dispatch thread to finish running
 895      *              the {@link #showUI() showUI}
 896      * @throws InvocationTargetException if an exception is thrown while
 897      *              the event dispatch thread executes {@code showUI}
 898      */
 899     private static void showAllWindows()
 900             throws InterruptedException, InvocationTargetException {
 901         invokeOnEDT(PassFailJFrame::showUI);
 902     }
 903 
 904     /**
 905      * Displays all the windows in {@code windowList}; it has to be called on
 906      * the EDT &mdash; use {@link #showAllWindows() showAllWindows} to ensure it.
 907      */
 908     private static synchronized void showUI() {
 909         windowList.forEach(w -> w.setVisible(true));
 910     }
 911 
 912 
 913     /**
 914      * Forcibly pass the test.
 915      * <p>The sample usage:
 916      * <pre><code>
 917      *      PrinterJob pj = PrinterJob.getPrinterJob();
 918      *      if (pj == null || pj.getPrintService() == null) {
 919      *          System.out.println(""Printer not configured or available.");
 920      *          PassFailJFrame.forcePass();
 921      *      }
 922      * </code></pre>
 923      */
 924     public static void forcePass() {
 925         latch.countDown();
 926     }
 927 
 928     /**
 929      *  Forcibly fail the test.
 930      */
 931     public static void forceFail() {
 932         forceFail("forceFail called");
 933     }
 934 
 935     /**
 936      *  Forcibly fail the test and provide a reason.
 937      *
 938      * @param reason the reason why the test is failed
 939      */
 940     public static void forceFail(String reason) {
 941         setFailureReason(FAILURE_REASON + reason);
 942         latch.countDown();
 943     }
 944 
 945     public static final class Builder {
 946         private String title;
 947         private String instructions;
 948         private long testTimeOut;
 949         private int rows;
 950         private int columns;
 951         private boolean screenCapture;
 952 
 953         private List<? extends Window> testWindows;
 954         private WindowListCreator windowListCreator;
 955         private PositionWindows positionWindows;
 956         private InstructionUI instructionUIHandler;
 957 
 958         private Position position;
 959 
 960         public Builder title(String title) {
 961             this.title = title;
 962             return this;
 963         }
 964 
 965         public Builder instructions(String instructions) {
 966             this.instructions = instructions;
 967             return this;
 968         }
 969 
 970         public Builder testTimeOut(long testTimeOut) {
 971             this.testTimeOut = testTimeOut;
 972             return this;
 973         }
 974 
 975         public Builder rows(int rows) {
 976             this.rows = rows;
 977             return this;
 978         }
 979 
 980         public Builder columns(int columns) {
 981             this.columns = columns;
 982             return this;
 983         }
 984 
 985         public Builder screenCapture() {
 986             this.screenCapture = true;
 987             return this;
 988         }
 989 
 990         /**
 991          * Adds a {@code WindowCreator} which the framework will use
 992          * to create the test UI window.
 993          *
 994          * @param windowCreator a {@code WindowCreator}
 995          *              to create the test UI window
 996          * @return this builder
 997          * @throws IllegalArgumentException if {@code windowCreator} is {@code null}
 998          * @throws IllegalStateException if a window creator
 999          *              or a list of test windows is already set
1000          */
1001         public Builder testUI(WindowCreator windowCreator) {
1002             if (windowCreator == null) {
1003                 throw new IllegalArgumentException("The window creator can't be null");
1004             }
1005 
1006             checkWindowsLists();
1007 
1008             this.windowListCreator = () -> List.of(windowCreator.createTestUI());
1009             return this;
1010         }
1011 
1012         /**
1013          * Adds a {@code WindowListCreator} which the framework will use
1014          * to create a list of test UI windows.
1015          *
1016          * @param windowListCreator a {@code WindowListCreator}
1017          *              to create test UI windows
1018          * @return this builder
1019          * @throws IllegalArgumentException if {@code windowListCreator} is {@code null}
1020          * @throws IllegalStateException if a window creator
1021          *              or a list of test windows is already set
1022          */
1023         public Builder testUI(WindowListCreator windowListCreator) {
1024             if (windowListCreator == null) {
1025                 throw new IllegalArgumentException("The window list creator can't be null");
1026             }
1027 
1028             checkWindowsLists();
1029 
1030             this.windowListCreator = windowListCreator;
1031             return this;
1032         }
1033 
1034         /**
1035          * Adds an already created test UI window.
1036          * The window is positioned and shown automatically.
1037          *
1038          * @param window a test UI window
1039          * @return this builder
1040          */
1041         public Builder testUI(Window window) {
1042             return testUI(List.of(window));
1043         }
1044 
1045         /**
1046          * Adds an array of already created test UI windows.
1047          *
1048          * @param windows an array of test UI windows
1049          * @return this builder
1050          */
1051         public Builder testUI(Window... windows) {
1052             return testUI(List.of(windows));
1053         }
1054 
1055         /**
1056          * Adds a list of already created test UI windows.
1057          *
1058          * @param windows a list of test UI windows
1059          * @return this builder
1060          * @throws IllegalArgumentException if {@code windows} is {@code null}
1061          *              or the list contains {@code null}
1062          * @throws IllegalStateException if a window creator
1063          *              or a list of test windows is already set
1064          */
1065         public Builder testUI(List<? extends Window> windows) {
1066             if (windows == null) {
1067                 throw new IllegalArgumentException("The list of windows can't be null");
1068             }
1069             if (windows.stream()
1070                        .anyMatch(Objects::isNull)) {
1071                 throw new IllegalArgumentException("The list of windows can't contain null");
1072             }
1073 
1074             checkWindowsLists();
1075 
1076             this.testWindows = windows;
1077             return this;
1078         }
1079 
1080         /**
1081          * Verifies the state of window list and window creator.
1082          *
1083          * @throws IllegalStateException if a windows list creator
1084          *              or a list of test windows is already set
1085          */
1086         private void checkWindowsLists() {
1087             if (windowListCreator != null) {
1088                 throw new IllegalStateException("Window list creator is already set");
1089             }
1090             if (testWindows != null) {
1091                 throw new IllegalStateException("The list of test windows is already set");
1092             }
1093         }
1094 
1095         public Builder positionTestUI(PositionWindows positionWindows) {
1096             this.positionWindows = positionWindows;
1097             return this;
1098         }
1099 
1100         public Builder position(Position position) {
1101             this.position = position;
1102             return this;
1103         }
1104 
1105         public PassFailJFrame build() throws InterruptedException,
1106                 InvocationTargetException {
1107             validate();
1108             return new PassFailJFrame(this);
1109         }
1110 
1111         private void validate() {
1112             if (title == null) {
1113                 title = TITLE;
1114             }
1115 
1116             if (instructions == null || instructions.isEmpty()) {
1117                 throw new IllegalStateException("Please provide the test " +
1118                         "instructions for this manual test");
1119             }
1120 
1121             if (testTimeOut == 0L) {
1122                 testTimeOut = TEST_TIMEOUT;
1123             }
1124 
1125             if (rows == 0) {
1126                 rows = ROWS;
1127             }
1128 
1129             if (columns == 0) {
1130                 columns = COLUMNS;
1131             }
1132 
1133             if (position == null
1134                 && (testWindows != null || windowListCreator != null)) {
1135 
1136                 position = Position.HORIZONTAL;
1137             }
1138 
1139             if (positionWindows != null) {
1140                 if (testWindows == null && windowListCreator == null) {
1141                     throw new IllegalStateException("To position windows, "
1142                             + "provide an a list of windows to the builder");
1143                 }
1144                 instructionUIHandler = new InstructionUIHandler();
1145             }
1146         }
1147 
1148         private final class InstructionUIHandler implements InstructionUI {
1149             @Override
1150             public Point getLocation() {
1151                 return frame.getLocation();
1152             }
1153 
1154             @Override
1155             public Dimension getSize() {
1156                 return frame.getSize();
1157             }
1158 
1159             @Override
1160             public Rectangle getBounds() {
1161                 return frame.getBounds();
1162             }
1163 
1164             @Override
1165             public void setLocation(Point location) {
1166                 setLocation(location.x, location.y);
1167             }
1168 
1169             @Override
1170             public void setLocation(int x, int y) {
1171                 frame.setLocation(x, y);
1172             }
1173 
1174             @Override
1175             public Position getPosition() {
1176                 return position;
1177             }
1178         }
1179     }
1180 
1181     public static Builder builder() {
1182         return new Builder();
1183     }
1184 }