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