/* * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import java.awt.AWTException; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.GraphicsConfiguration; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Image; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.Robot; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import javax.imageio.ImageIO; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JEditorPane; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTextArea; import javax.swing.Timer; import javax.swing.border.Border; import javax.swing.text.JTextComponent; import javax.swing.text.html.HTMLEditorKit; import javax.swing.text.html.StyleSheet; import static java.util.Collections.unmodifiableList; import static javax.swing.BorderFactory.createEmptyBorder; import static javax.swing.SwingUtilities.invokeAndWait; import static javax.swing.SwingUtilities.isEventDispatchThread; /** * A framework for manual tests to display test instructions and * Pass / Fail buttons. The framework automatically * creates a frame to display the instructions, provides buttons * to select the test result, and handles test timeout. * *

* The instruction UI frame displays a timer at the top which indicates * how much time is left. The timer can be paused using the Pause * button to the right of the time; the title of the button changes to * Resume. To resume the timer, use the Resume button. * *

* In the center, the instruction UI frame displays instructions for the * tester. The instructions can be either plain text or HTML. If the * text of the instructions starts with {@code ""}, the * instructions are displayed as HTML, as supported by Swing, which * provides richer formatting options. *

* The instructions are displayed in a text component with word-wrapping * so that there's no horizontal scroll bar. If the text doesn't fit, a * vertical scroll bar is shown. Use {@code rows} and {@code columns} * parameters to change the size of this text component. * If possible, choose the number of rows and columns so that * the instructions fit and no scroll bars are shown. * *

* At the bottom, the instruction UI frame displays the * Pass and Fail buttons. The tester clicks either Pass * or Fail button to finish the test. When the tester clicks the * Fail button, the framework displays a dialog box prompting for * a reason why the test fails. The tester enters the reason and clicks * OK to close the dialog and fail the test, * or simply closes the dialog to fail the test without providing any reason. * *

* If you enable the screenshot feature, a Screenshot button is * added to the right of the Fail button. The tester can choose either * Capture Full Screen (default) or Capture Frames and click the * Screenshot button to take a screenshot. * If there are multiple screens, screenshots of each screen are created. * If the tester selects the Capture Frames mode, screenshots of all * the windows or frames registered in the {@code PassFailJFrame} framework * are created. * *

* If you enable a log area, the instruction UI frame adds a text component * to display log messages below the buttons. * Use {@link #log(String) log}, {@link #logSet(String) logSet} * and {@link #logClear() logClear} static methods of {@code PassFailJFrame} * to add or clear messages from the log area. * *

* After you create an instance of {@code PassFailJFrame}, call the * {@link #awaitAndCheck() awaitAndCheck} method to stop the current thread * (usually the main thread) and wait until the tester clicks * either Pass or Fail button, * or until the test times out. *

* The call to the {@code awaitAndCheck} method is usually the last * statement in the {@code main} method of your test. * If the test fails, an exception is thrown to signal the failure to jtreg. * The test fails if the tester clicks the Fail button, * if the timeout occurs, * or if any window or frame is closed. *

* Before returning from {@code awaitAndCheck}, the framework disposes of * all the windows and frames. * *

Sample Manual Test

* A simple test would look like this: * {@snippet id='sampleManualTestCode' lang='java': * public class SampleManualTest { * private static final String INSTRUCTIONS = * "Click Pass, or click Fail if the test failed."; * * public static void main(String[] args) throws Exception { * PassFailJFrame.builder() * .instructions(INSTRUCTIONS) * .testUI(SampleManualTest::createTestUI) * .build() * .awaitAndCheck(); * } * * private static Window createTestUI() { * JFrame testUI = new JFrame("Test UI"); * testUI.setSize(250, 150); * return testUI; * } * } * } *

* The above example uses the {@link Builder Builder} class to set * the parameters of the instruction frame. * It is the recommended way. * *

* The framework will create an instruction UI frame, it will call * the provided {@code createTestUI} on the Event Dispatch Thread (EDT), * and it will automatically position the test UI frame and make it visible. * *

* Add the following jtreg tags before the test class declaration *


 * /*
 *  * @test
 *  * @summary Sample manual test
 *  * @library /java/awt/regtesthelpers
 *  * @build PassFailJFrame
 *  * @run main/manual SampleManualTest
 *  */
 * 
*

* The {@code @library} tag points to the location of the * {@code PassFailJFrame} class in the source code; * the {@code @build} tag makes jtreg compile the {@code PassFailJFrame} class, * and finally the {@code @run} tag specifies it is a manual * test and the class to run. *

* Don't forget to update the name of the class to run in the {@code @run} tag. * *

Using {@code Builder}

* Use methods of the {@link Builder Builder} class to set or change * parameters of {@code PassFailJFrame} and its instruction UI: * * *

Using {@code testUI} and {@code splitUI}

* The {@code Builder.testUI} methods accept interfaces which create one window * or a list of windows if the test needs multiple windows, * or directly a single window, an array of windows or a list of windows. *

* For simple test UI, use {@link Builder#splitUI(PanelCreator) splitUI}, * or explicitly * {@link Builder#splitUIRight(PanelCreator) splitUIRight} or * {@link Builder#splitUIBottom(PanelCreator) splitUIBottom} with * a {@link PanelCreator PanelCreator}. * The framework will call the provided * {@code createUIPanel} method to create the component with test UI and * will place it as the right or bottom component in a split pane * along with instruction UI. *

* Note: support for multiple windows is incomplete. * *

Obsolete Sample Test

* Alternatively, use one of the {@code PassFailJFrame} constructors to * create an object, then create secondary test UI, register it * with {@code PassFailJFrame}, position it and make it visible. * The following sample demonstrates it: * {@snippet id='obsoleteSampleTestCode' lang='java': * public class ObsoleteManualTest { * private static final String INSTRUCTIONS = * "Click Pass, or click Fail if the test failed."; * * public static void main(String[] args) throws Exception { * PassFailJFrame passFail = new PassFailJFrame(INSTRUCTIONS); * * SwingUtilities.invokeAndWait(ObsoleteManualTest::createTestUI); * * passFail.awaitAndCheck(); * } * * private static void createTestUI() { * JFrame testUI = new JFrame("Test UI"); * testUI.setSize(250, 150); * PassFailJFrame.addTestWindow(testUI); * PassFailJFrame.positionTestWindow(testUI, PassFailJFrame.Position.HORIZONTAL); * testUI.setVisible(true); * } * } * } *

* This sample uses {@link #PassFailJFrame(String) a constructor} of * {@code PassFailJFrame} to create its instance, * there are several overloads provided which allow changing other parameters. *

* When you use the constructors, you have to explicitly create * your test UI window on EDT. After you create the window, * you need to register it with the framework using * {@link #addTestWindow(Window) addTestWindow} * to ensure the window is disposed of when the test completes. * Before showing the window, you have to call * {@link #positionTestWindow(Window, Position) positionTestWindow} * to position the test window near the instruction UI frame provided * by the framework. And finally you have to explicitly show the test UI * window by calling {@code setVisible(true)}. *

* To avoid the complexity, use the {@link Builder Builder} class * which provides a streamlined way to configure and create an * instance of {@code PassFailJFrame}. *

* Consider updating tests which use {@code PassFailJFrame} constructors to * use the builder pattern. */ public final class PassFailJFrame { /** A default title for the instruction frame. */ private static final String TITLE = "Test Instructions"; /** A default test timeout. */ private static final long TEST_TIMEOUT = 5; /** A default number of rows for displaying the test instructions. */ private static final int ROWS = 10; /** A default number of columns for displaying the test instructions. */ private static final int COLUMNS = 40; /** * A gap between windows. */ public static final int WINDOW_GAP = 8; /** * Prefix for the user-provided failure reason. */ private static final String FAILURE_REASON = "Failure Reason:\n"; /** * The failure reason message when the user doesn't provide one. */ private static final String EMPTY_REASON = "(no reason provided)"; /** * List of windows or frames managed by the {@code PassFailJFrame} * framework. These windows are automatically disposed of when the * test is finished. *

* Note: access to this field has to be synchronized by * {@code PassFailJFrame.class}. */ private static final List windowList = new ArrayList<>(); private static final CountDownLatch latch = new CountDownLatch(1); private static TimeoutHandlerPanel timeoutHandlerPanel; /** * The description of why the test fails. *

* Note: do not use this field directly, * use the {@link #setFailureReason(String) setFailureReason} and * {@link #getFailureReason() getFailureReason} methods to modify and * to read its value. */ private static String failureReason; private static final AtomicInteger imgCounter = new AtomicInteger(0); private static JFrame frame; private static Robot robot; private static JTextArea logArea; public enum Position {HORIZONTAL, VERTICAL, TOP_LEFT_CORNER} /** * Constructs a frame which displays test instructions and * the Pass / Fail buttons with the given instructions, and * the default timeout of {@value #TEST_TIMEOUT} minutes, * the default title of {@value #TITLE} and * the default values of {@value #ROWS} and {@value #COLUMNS} * for rows and columns. *

* See {@link #PassFailJFrame(String,String,long,int,int,boolean)} for * more details. * * @param instructions the instructions for the tester * * @throws InterruptedException if the current thread is interrupted * while waiting for EDT to finish creating UI components * @throws InvocationTargetException if an exception is thrown while * creating UI components on EDT */ public PassFailJFrame(String instructions) throws InterruptedException, InvocationTargetException { this(instructions, TEST_TIMEOUT); } /** * Constructs a frame which displays test instructions and * the Pass / Fail buttons * with the given instructions and timeout as well as * the default title of {@value #TITLE} * and the default values of {@value #ROWS} and {@value #COLUMNS} * for rows and columns. *

* See {@link #PassFailJFrame(String,String,long,int,int,boolean)} for * more details. * * @param instructions the instructions for the tester * @param testTimeOut the test timeout in minutes * * @throws InterruptedException if the current thread is interrupted * while waiting for EDT to finish creating UI components * @throws InvocationTargetException if an exception is thrown while * creating UI components on EDT */ public PassFailJFrame(String instructions, long testTimeOut) throws InterruptedException, InvocationTargetException { this(TITLE, instructions, testTimeOut); } /** * Constructs a frame which displays test instructions and * the Pass / Fail buttons * with the given title, instructions and timeout as well as * the default values of {@value #ROWS} and {@value #COLUMNS} * for rows and columns. * The screenshot feature is not enabled, if you use this constructor. *

* See {@link #PassFailJFrame(String,String,long,int,int,boolean)} for * more details. * * @param title the title of the instruction frame * @param instructions the instructions for the tester * @param testTimeOut the test timeout in minutes * * @throws InterruptedException if the current thread is interrupted * while waiting for EDT to finish creating UI components * @throws InvocationTargetException if an exception is thrown while * creating UI components on EDT */ public PassFailJFrame(String title, String instructions, long testTimeOut) throws InterruptedException, InvocationTargetException { this(title, instructions, testTimeOut, ROWS, COLUMNS); } /** * Constructs a frame which displays test instructions and * the Pass / Fail buttons * with the given title, instructions, timeout, number of rows and columns. * The screenshot feature is not enabled, if you use this constructor. *

* See {@link #PassFailJFrame(String,String,long,int,int,boolean)} for * more details. * * @param title the title of the instruction frame * @param instructions the instructions for the tester * @param testTimeOut the test timeout in minutes * @param rows the number of rows for the text component * which displays test instructions * @param columns the number of columns for the text component * which displays test instructions * * @throws InterruptedException if the current thread is interrupted * while waiting for EDT to finish creating UI components * @throws InvocationTargetException if an exception is thrown while * creating UI components on EDT */ public PassFailJFrame(String title, String instructions, long testTimeOut, int rows, int columns) throws InterruptedException, InvocationTargetException { this(title, instructions, testTimeOut, rows, columns, false); } /** * Constructs a frame which displays test instructions and * the Pass / Fail buttons * as well as supporting UI components with the given title, instructions, * timeout, number of rows and columns, * and screen capture functionality. * All the UI components are created on the EDT, so it is safe to call * the constructor on the main thread. *

* After you create a test UI window, register the window using * {@link #addTestWindow(Window) addTestWindow} for disposal, and * position it close to the instruction frame using * {@link #positionTestWindow(Window, Position) positionTestWindow}. * As the last step, make your test UI window visible. *

* Call the {@link #awaitAndCheck() awaitAndCheck} method on the instance * of {@code PassFailJFrame} when you set up the testing environment. *

* If the tester clicks the Fail button, a dialog prompting for * a description of the problem is displayed, and then an exception * is thrown which fails the test. * If the tester clicks the Pass button, the test completes * successfully. * If the timeout occurs or the instruction frame is closed, * the test fails. *

* The {@code rows} and {@code columns} parameters control * the size of a text component which displays the instructions. * The preferred size of the instructions is calculated by * creating {@code new JTextArea(rows, columns)}. *

* If you enable screenshots by setting the {@code screenCapture} * parameter to {@code true}, a Screenshot button is added. * Clicking the Screenshot button takes screenshots of * all the monitors or all the windows registered with * {@code PassFailJFrame}. * * @param title the title of the instruction frame * @param instructions the instructions for the tester * @param testTimeOut the test timeout in minutes * @param rows the number of rows for the text component * which displays test instructions * @param columns the number of columns for the text component * which displays test instructions * @param screenCapture if set to {@code true}, enables screen capture * functionality * * @throws InterruptedException if the current thread is interrupted * while waiting for EDT to finish creating UI components * @throws InvocationTargetException if an exception is thrown while * creating UI components on EDT * * @see JTextArea#JTextArea(int,int) JTextArea(int rows, int columns) * @see Builder Builder */ public PassFailJFrame(String title, String instructions, long testTimeOut, int rows, int columns, boolean screenCapture) throws InterruptedException, InvocationTargetException { invokeOnEDT(() -> createUI(title, instructions, testTimeOut, rows, columns, screenCapture)); } /** * Configures {@code PassFailJFrame} using the builder. * It creates test UI specified using {@code testUI} or {@code splitUI} * methods on EDT. * @param builder the builder with the parameters * @throws InterruptedException if the current thread is interrupted while * waiting for EDT to complete a task * @throws InvocationTargetException if an exception is thrown while * running a task on EDT */ private PassFailJFrame(final Builder builder) throws InterruptedException, InvocationTargetException { invokeOnEDT(() -> createUI(builder)); if (!builder.splitUI && builder.panelCreator != null) { JComponent content = builder.panelCreator.createUIPanel(); String title = content.getName(); if (title == null) { title = "Test UI"; } JDialog dialog = new JDialog(frame, title, false); dialog.addWindowListener(windowClosingHandler); dialog.add(content, BorderLayout.CENTER); dialog.pack(); addTestWindow(dialog); positionTestWindow(dialog, builder.position); } if (builder.windowListCreator != null) { invokeOnEDT(() -> builder.testWindows = builder.windowListCreator.createTestUI()); if (builder.testWindows == null) { throw new IllegalStateException("Window list creator returned null list"); } } if (builder.testWindows != null) { if (builder.testWindows.isEmpty()) { throw new IllegalStateException("Window list is empty"); } addTestWindow(builder.testWindows); builder.testWindows .forEach(w -> w.addWindowListener(windowClosingHandler)); if (builder.positionWindows != null) { positionInstructionFrame(builder.position); invokeOnEDT(() -> builder.positionWindows .positionTestWindows(unmodifiableList(builder.testWindows), builder.instructionUIHandler)); } else { Window window = builder.testWindows.get(0); positionTestWindow(window, builder.position); } } showAllWindows(); } /** * Performs an operation on EDT. If called on EDT, invokes {@code run} * directly, otherwise wraps into {@code invokeAndWait}. * * @param doRun an operation to run on EDT * @throws InterruptedException if we're interrupted while waiting for * the event dispatching thread to finish executing * {@code doRun.run()} * @throws InvocationTargetException if an exception is thrown while * running {@code doRun} * @see javax.swing.SwingUtilities#invokeAndWait(Runnable) */ private static void invokeOnEDT(Runnable doRun) throws InterruptedException, InvocationTargetException { if (isEventDispatchThread()) { doRun.run(); } else { invokeAndWait(doRun); } } /** * Does the same as {@link #invokeOnEDT(Runnable)}, but does not throw * any checked exceptions. * * @param doRun an operation to run on EDT */ private static void invokeOnEDTUncheckedException(Runnable doRun) { try { invokeOnEDT(doRun); } catch (InterruptedException | InvocationTargetException e) { throw new RuntimeException(e); } } private static void createUI(String title, String instructions, long testTimeOut, int rows, int columns, boolean enableScreenCapture) { frame = new JFrame(title); frame.setLayout(new BorderLayout()); frame.addWindowListener(windowClosingHandler); frame.add(createInstructionUIPanel(instructions, testTimeOut, rows, columns, enableScreenCapture, false, 0), BorderLayout.CENTER); frame.pack(); frame.setLocationRelativeTo(null); addTestWindow(frame); } private static void createUI(Builder builder) { frame = new JFrame(builder.title); frame.setLayout(new BorderLayout()); frame.addWindowListener(windowClosingHandler); JComponent instructionUI = createInstructionUIPanel(builder.instructions, builder.testTimeOut, builder.rows, builder.columns, builder.screenCapture, builder.addLogArea, builder.logAreaRows); if (builder.splitUI) { JSplitPane splitPane = new JSplitPane( builder.splitUIOrientation, instructionUI, builder.panelCreator.createUIPanel()); frame.add(splitPane, BorderLayout.CENTER); } else { frame.add(instructionUI, BorderLayout.CENTER); } frame.pack(); frame.setLocationRelativeTo(null); addTestWindow(frame); } private static JComponent createInstructionUIPanel(String instructions, long testTimeOut, int rows, int columns, boolean enableScreenCapture, boolean addLogArea, int logAreaRows) { JPanel main = new JPanel(new BorderLayout()); main.setBorder(createFrameBorder()); timeoutHandlerPanel = new TimeoutHandlerPanel(testTimeOut); main.add(timeoutHandlerPanel, BorderLayout.NORTH); JTextComponent text = instructions.startsWith("") ? configureHTML(instructions, rows, columns) : configurePlainText(instructions, rows, columns); text.setEditable(false); JPanel textPanel = new JPanel(new BorderLayout()); textPanel.setBorder(createEmptyBorder(GAP, 0, GAP, 0)); textPanel.add(new JScrollPane(text), BorderLayout.CENTER); main.add(textPanel, BorderLayout.CENTER); JButton btnPass = new JButton("Pass"); btnPass.addActionListener((e) -> { latch.countDown(); timeoutHandlerPanel.stop(); }); JButton btnFail = new JButton("Fail"); btnFail.addActionListener((e) -> { requestFailureReason(); timeoutHandlerPanel.stop(); }); JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, GAP, 0)); buttonsPanel.add(btnPass); buttonsPanel.add(btnFail); if (enableScreenCapture) { buttonsPanel.add(createCapturePanel()); } if (addLogArea) { logArea = new JTextArea(logAreaRows, columns); logArea.setEditable(false); logArea.setBorder(createTextBorder()); Box buttonsLogPanel = Box.createVerticalBox(); buttonsLogPanel.add(buttonsPanel); buttonsLogPanel.add(Box.createVerticalStrut(GAP)); buttonsLogPanel.add(new JScrollPane(logArea)); main.add(buttonsLogPanel, BorderLayout.SOUTH); } else { main.add(buttonsPanel, BorderLayout.SOUTH); } main.setMinimumSize(main.getPreferredSize()); return main; } private static JTextComponent configurePlainText(String instructions, int rows, int columns) { JTextArea text = new JTextArea(instructions, rows, columns); text.setLineWrap(true); text.setWrapStyleWord(true); text.setBorder(createTextBorder()); return text; } private static JTextComponent configureHTML(String instructions, int rows, int columns) { JEditorPane text = new JEditorPane("text/html", instructions); text.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); // Set preferred size as if it were JTextArea text.setPreferredSize(new JTextArea(rows, columns).getPreferredSize()); HTMLEditorKit kit = (HTMLEditorKit) text.getEditorKit(); StyleSheet styles = kit.getStyleSheet(); // Reduce the default margins styles.addRule("ol, ul { margin-left-ltr: 20; margin-left-rtl: 20 }"); // Make the size of code blocks the same as other text styles.addRule("code { font-size: inherit }"); return text; } /** A default gap between components. */ private static final int GAP = 4; /** * Creates a default border for frames or dialogs. * It uses the default gap of {@value GAP}. * * @return the border for frames and dialogs */ private static Border createFrameBorder() { return createEmptyBorder(GAP, GAP, GAP, GAP); } /** * Creates a border set to text area. * It uses the default gap of {@value GAP}. * * @return the border for text area */ private static Border createTextBorder() { return createEmptyBorder(GAP, GAP, GAP, GAP); } /** * Creates a test UI window. */ @FunctionalInterface public interface WindowCreator { /** * Creates a window for test UI. * This method is called by the framework on the EDT. * @return a test UI window */ Window createTestUI(); } /** * Creates a list of test UI windows. */ @FunctionalInterface public interface WindowListCreator { /** * Creates one or more windows for test UI. * This method is called by the framework on the EDT. * @return a list of test UI windows */ List createTestUI(); } /** * Creates a component (panel) with test UI * to be hosted in a split pane or a frame. */ @FunctionalInterface public interface PanelCreator { /** * Creates a component which hosts test UI. This component * is placed into a split pane or into a frame to display the UI. *

* This method is called by the framework on the EDT. * @return a component (panel) with test UI */ JComponent createUIPanel(); } /** * Positions test UI windows. */ @FunctionalInterface public interface PositionWindows { /** * Positions test UI windows. * This method is called by the framework on the EDT after * the instruction UI frame was positioned on the screen. *

* The list of the test windows contains the windows * that were passed to the framework via the * {@link Builder#testUI(Window...) testUI(Window...)} method or * that were created with {@code WindowCreator} * or {@code WindowListCreator} which were passed via * {@link Builder#testUI(WindowCreator) testUI(WindowCreator)} or * {@link Builder#testUI(WindowListCreator) testUI(WindowListCreator)} * correspondingly. * * @param testWindows the list of test windows * @param instructionUI information about the instruction frame */ void positionTestWindows(List testWindows, InstructionUI instructionUI); } /** * Provides information about the instruction frame. */ public interface InstructionUI { /** * {@return the location of the instruction frame} */ Point getLocation(); /** * {@return the size of the instruction frame} */ Dimension getSize(); /** * {@return the bounds of the instruction frame} */ Rectangle getBounds(); /** * Allows to change the location of the instruction frame. * * @param location the new location of the instruction frame */ void setLocation(Point location); /** * Allows to change the location of the instruction frame. * * @param x the x coordinate of the new location * @param y the y coordinate of the new location */ void setLocation(int x, int y); /** * Returns the specified position that was used to set * the initial location of the instruction frame. * * @return the specified position * * @see Position */ Position getPosition(); } private static final class TimeoutHandlerPanel extends JPanel implements ActionListener { private static final String PAUSE_BUTTON_LABEL = "Pause"; private static final String RESUME_BUTTON_LABEL = "Resume"; private long endTime; private long pauseTimeLeft; private final Timer timer; private final JLabel label; private final JButton button; public TimeoutHandlerPanel(final long testTimeOut) { endTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(testTimeOut); label = new JLabel("", JLabel.CENTER); button = new JButton(PAUSE_BUTTON_LABEL); button.setFocusPainted(false); button.setFont(new Font(Font.DIALOG, Font.BOLD, 10)); button.addActionListener(e -> pauseToggle()); setLayout(new BorderLayout()); add(label, BorderLayout.CENTER); add(button, BorderLayout.EAST); timer = new Timer(1000, this); timer.start(); updateTime(testTimeOut); } @Override public void actionPerformed(ActionEvent e) { long leftTime = endTime - System.currentTimeMillis(); if (leftTime < 0) { timer.stop(); setFailureReason(FAILURE_REASON + "Timeout - User did not perform testing."); latch.countDown(); } updateTime(leftTime); } private void updateTime(final long leftTime) { if (leftTime < 0) { label.setText("Test timeout: 00:00:00"); return; } long hours = leftTime / 3_600_000; long minutes = (leftTime - hours * 3_600_000) / 60_000; long seconds = (leftTime - hours * 3_600_000 - minutes * 60_000) / 1_000; label.setText(String.format(Locale.ENGLISH, "Test timeout: %02d:%02d:%02d", hours, minutes, seconds)); } private void pauseToggle() { if (timer.isRunning()) { pauseTimeLeft = endTime - System.currentTimeMillis(); timer.stop(); label.setEnabled(false); button.setText(RESUME_BUTTON_LABEL); } else { endTime = System.currentTimeMillis() + pauseTimeLeft; updateTime(pauseTimeLeft); timer.start(); label.setEnabled(true); button.setText(PAUSE_BUTTON_LABEL); } } public void stop() { timer.stop(); } } private static final class WindowClosingHandler extends WindowAdapter { @Override public void windowClosing(WindowEvent e) { setFailureReason(FAILURE_REASON + "User closed a window"); latch.countDown(); } } private static final WindowListener windowClosingHandler = new WindowClosingHandler(); private static JComponent createCapturePanel() { JComboBox screenShortType = new JComboBox<>(CaptureType.values()); JButton capture = new JButton("Screenshot"); capture.addActionListener((e) -> captureScreen((CaptureType) screenShortType.getSelectedItem())); JPanel panel = new JPanel(); panel.add(screenShortType); panel.add(capture); return panel; } private enum CaptureType { FULL_SCREEN("Capture Full Screen"), WINDOWS("Capture Frames"); private final String type; CaptureType(String type) { this.type = type; } @Override public String toString() { return type; } } private static Robot createRobot() { if (robot == null) { try { robot = new Robot(); } catch (AWTException e) { String errorMsg = "Failed to create an instance of Robot."; JOptionPane.showMessageDialog(frame, errorMsg, "Failed", JOptionPane.ERROR_MESSAGE); forceFail(errorMsg + e.getMessage()); } } return robot; } private static void captureScreen(Rectangle bounds) { Robot robot = createRobot(); List imageList = robot.createMultiResolutionScreenCapture(bounds) .getResolutionVariants(); Image image = imageList.get(imageList.size() - 1); File file = new File("CaptureScreen_" + imgCounter.incrementAndGet() + ".png"); try { ImageIO.write((RenderedImage) image, "png", file); } catch (IOException e) { throw new RuntimeException(e); } } private static void captureScreen(CaptureType type) { switch (type) { case FULL_SCREEN: Arrays.stream(GraphicsEnvironment.getLocalGraphicsEnvironment() .getScreenDevices()) .map(GraphicsDevice::getDefaultConfiguration) .map(GraphicsConfiguration::getBounds) .forEach(PassFailJFrame::captureScreen); break; case WINDOWS: synchronized (PassFailJFrame.class) { windowList.stream() .filter(Window::isShowing) .map(Window::getBounds) .forEach(PassFailJFrame::captureScreen); } break; default: throw new IllegalStateException("Unexpected value of capture type"); } JOptionPane.showMessageDialog(frame, "Screen Captured Successfully", "Screen Capture", JOptionPane.INFORMATION_MESSAGE); } /** * Sets the failure reason which describes why the test fails. * This method ensures the {@code failureReason} field does not change * after it's set to a non-{@code null} value. * @param reason the description of why the test fails * @throws IllegalArgumentException if the {@code reason} parameter * is {@code null} */ private static synchronized void setFailureReason(final String reason) { if (reason == null) { throw new IllegalArgumentException("The failure reason must not be null"); } if (failureReason == null) { failureReason = reason; } } /** * {@return the description of why the test fails} */ private static synchronized String getFailureReason() { return failureReason; } /** * Wait for the user decision i,e user selects pass or fail button. * If user does not select pass or fail button then the test waits for * the specified timeoutMinutes period and the test gets timeout. * Note: This method should be called from main() thread * * @throws InterruptedException exception thrown when thread is * interrupted * @throws InvocationTargetException if an exception is thrown while * disposing of frames on EDT */ public void awaitAndCheck() throws InterruptedException, InvocationTargetException { if (isEventDispatchThread()) { throw new IllegalStateException("awaitAndCheck() should not be called on EDT"); } latch.await(); invokeAndWait(PassFailJFrame::disposeWindows); String failure = getFailureReason(); if (failure != null) { throw new RuntimeException(failure); } System.out.println("Test passed!"); } /** * Requests the description of the test failure reason from the tester. */ private static void requestFailureReason() { final JDialog dialog = new JDialog(frame, "Failure reason", true); JTextArea reason = new JTextArea(5, 20); reason.setBorder(createTextBorder()); JButton okButton = new JButton("OK"); okButton.addActionListener((ae) -> { String text = reason.getText(); setFailureReason(FAILURE_REASON + (!text.isEmpty() ? text : EMPTY_REASON)); dialog.setVisible(false); }); JPanel okayBtnPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, GAP, 0)); okayBtnPanel.setBorder(createEmptyBorder(GAP, 0, 0, 0)); okayBtnPanel.add(okButton); JPanel main = new JPanel(new BorderLayout()); main.setBorder(createFrameBorder()); main.add(new JScrollPane(reason), BorderLayout.CENTER); main.add(okayBtnPanel, BorderLayout.SOUTH); dialog.add(main); dialog.setLocationRelativeTo(frame); dialog.pack(); dialog.setVisible(true); // Ensure the test fails even if the dialog is closed // without clicking the OK button setFailureReason(FAILURE_REASON + EMPTY_REASON); dialog.dispose(); latch.countDown(); } /** * Disposes of all the windows. It disposes of the test instruction frame * and all other windows added via {@link #addTestWindow(Window)}. */ private static synchronized void disposeWindows() { windowList.forEach(Window::dispose); } private static void positionInstructionFrame(final Position position) { Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); // Get the screen insets to position the frame by taking into // account the location of taskbar or menu bar on screen. GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment() .getDefaultScreenDevice() .getDefaultConfiguration(); Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc); switch (position) { case HORIZONTAL: int newX = (((screenSize.width + WINDOW_GAP) / 2) - frame.getWidth()); frame.setLocation((newX + screenInsets.left), (frame.getY() + screenInsets.top)); break; case VERTICAL: int newY = (((screenSize.height + WINDOW_GAP) / 2) - frame.getHeight()); frame.setLocation((frame.getX() + screenInsets.left), (newY + screenInsets.top)); break; case TOP_LEFT_CORNER: frame.setLocation(screenInsets.left, screenInsets.top); break; } syncLocationToWindowManager(); } /** * Approximately positions the instruction frame relative to the test * window as specified by the {@code position} parameter. If {@code testWindow} * is {@code null}, only the instruction frame is positioned according to * {@code position} parameter. *

This method should be called before making the test window visible * to avoid flickering.

* * @param testWindow test window that the test created. * May be {@code null}. * * @param position position must be one of: * */ public static void positionTestWindow(Window testWindow, Position position) { positionInstructionFrame(position); if (testWindow != null) { switch (position) { case HORIZONTAL: case TOP_LEFT_CORNER: testWindow.setLocation((frame.getX() + frame.getWidth() + WINDOW_GAP), frame.getY()); break; case VERTICAL: testWindow.setLocation(frame.getX(), (frame.getY() + frame.getHeight() + WINDOW_GAP)); break; } } // make instruction frame visible after updating // frame & window positions frame.setVisible(true); } /** * Ensures the frame location is updated by the window manager * if it adjusts the frame location after {@code setLocation}. * * @see #positionTestWindow */ private static void syncLocationToWindowManager() { Toolkit.getDefaultToolkit().sync(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } /** * Returns the current position and size of the test instruction frame. * This method can be used in scenarios when custom positioning of * multiple test windows w.r.t test instruction frame is necessary, * at test-case level and the desired configuration is not available * as a {@code Position} option. * * @return Rectangle bounds of test instruction frame * @see #positionTestWindow * * @throws InterruptedException exception thrown when thread is * interrupted * @throws InvocationTargetException if an exception is thrown while * obtaining frame bounds on EDT */ public static Rectangle getInstructionFrameBounds() throws InterruptedException, InvocationTargetException { final Rectangle[] bounds = {null}; invokeOnEDT(() -> bounds[0] = frame != null ? frame.getBounds() : null); return bounds[0]; } /** * Add the testWindow to the windowList so that test instruction frame * and testWindow and any other windows used in this test is disposed * via disposeWindows(). * * @param testWindow testWindow that needs to be disposed */ public static synchronized void addTestWindow(Window testWindow) { windowList.add(testWindow); } /** * Adds a collection of test windows to the windowList to be disposed of * when the test completes. * * @param testWindows the collection of test windows to be disposed of */ public static synchronized void addTestWindow(Collection testWindows) { windowList.addAll(testWindows); } /** * Displays all the windows in {@code windowList}. * * @throws InterruptedException if the thread is interrupted while * waiting for the event dispatch thread to finish running * the {@link #showUI() showUI} * @throws InvocationTargetException if an exception is thrown while * the event dispatch thread executes {@code showUI} */ private static void showAllWindows() throws InterruptedException, InvocationTargetException { invokeOnEDT(PassFailJFrame::showUI); } /** * Displays all the windows in {@code windowList}; it has to be called on * the EDT — use {@link #showAllWindows() showAllWindows} to ensure it. */ private static synchronized void showUI() { windowList.forEach(w -> w.setVisible(true)); } /** * Forcibly pass the test. *

The sample usage: *


     *      PrinterJob pj = PrinterJob.getPrinterJob();
     *      if (pj == null || pj.getPrintService() == null) {
     *          System.out.println(""Printer not configured or available.");
     *          PassFailJFrame.forcePass();
     *      }
     * 
*/ public static void forcePass() { latch.countDown(); } /** * Forcibly fail the test. */ public static void forceFail() { forceFail("forceFail called"); } /** * Forcibly fail the test and provide a reason. * * @param reason the reason why the test is failed */ public static void forceFail(String reason) { setFailureReason(FAILURE_REASON + reason); latch.countDown(); } /** * Adds a {@code message} to the log area, if enabled by * {@link Builder#logArea() logArea()} or * {@link Builder#logArea(int) logArea(int)}. * * @param message the message to log */ public static void log(String message) { System.out.println("PassFailJFrame: " + message); invokeOnEDTUncheckedException(() -> logArea.append(message + "\n")); } /** * Clears the log area, if enabled by * {@link Builder#logArea() logArea()} or * {@link Builder#logArea(int) logArea(int)}. */ public static void logClear() { System.out.println("\nPassFailJFrame: log cleared\n"); invokeOnEDTUncheckedException(() -> logArea.setText("")); } /** * Replaces the log area content with provided {@code text}, if enabled by * {@link Builder#logArea() logArea()} or * {@link Builder#logArea(int) logArea(int)}. * * @param text new text for the log area */ public static void logSet(String text) { System.out.println("\nPassFailJFrame: log set to:\n" + text + "\n"); invokeOnEDTUncheckedException(() -> logArea.setText(text)); } public static final class Builder { private String title; private String instructions; private long testTimeOut; private int rows; private int columns; private boolean screenCapture; private boolean addLogArea; private int logAreaRows = 10; private List testWindows; private WindowListCreator windowListCreator; private PanelCreator panelCreator; private boolean splitUI; private int splitUIOrientation; private PositionWindows positionWindows; private InstructionUI instructionUIHandler; private Position position; /** * A private constructor for the builder, * it should not be created directly. * Use {@code PassFailJFrame.builder()} method instead. */ private Builder() { } public Builder title(String title) { this.title = title; return this; } public Builder instructions(String instructions) { this.instructions = instructions; return this; } public Builder testTimeOut(long testTimeOut) { this.testTimeOut = testTimeOut; return this; } /** * Sets the number of rows for displaying the instruction text. * The default value is the number of lines in the text plus 1: * {@code ((int) instructions.lines().count() + 1)}. * * @param rows the number of rows for instruction text * @return this builder */ public Builder rows(int rows) { this.rows = rows; return this; } private int getDefaultRows() { return (int) instructions.lines().count() + 1; } /** * Adds a certain number of rows for displaying the instruction text. * * @param rowsAdd the number of rows to add to the number of rows * @return this builder * @see #rows */ public Builder rowsAdd(int rowsAdd) { if (rows == 0) { rows = getDefaultRows(); } rows += rowsAdd; return this; } /** * Sets the number of columns for displaying the instruction text. * * @param columns the number of columns for instruction text * @return this builder */ public Builder columns(int columns) { this.columns = columns; return this; } public Builder screenCapture() { this.screenCapture = true; return this; } /** * Adds a log area below the "Pass", "Fail" buttons. *

* The log area can be controlled by {@link #log(String)}, * {@link #logClear()} and {@link #logSet(String)}. * * @return this builder */ public Builder logArea() { this.addLogArea = true; return this; } /** * Adds a log area below the "Pass", "Fail" buttons. *

* The log area can be controlled by {@link #log(String)}, * {@link #logClear()} and {@link #logSet(String)}. *

* The number of columns is taken from the number of * columns in the instructional JTextArea. * * @param rows of the log area * @return this builder */ public Builder logArea(int rows) { this.addLogArea = true; this.logAreaRows = rows; return this; } /** * Adds a {@code WindowCreator} which the framework will use * to create the test UI window. * * @param windowCreator a {@code WindowCreator} * to create the test UI window * @return this builder * @throws IllegalArgumentException if {@code windowCreator} is {@code null} * @throws IllegalStateException if a window creator * or a list of test windows is already set */ public Builder testUI(WindowCreator windowCreator) { if (windowCreator == null) { throw new IllegalArgumentException("The window creator can't be null"); } checkWindowsLists(); this.windowListCreator = () -> List.of(windowCreator.createTestUI()); return this; } /** * Adds an implementation of {@link PositionWindows PositionWindows} * which the framework will use to position multiple test UI windows. * * @param positionWindows an implementation of {@code PositionWindows} * to position multiple test UI windows * @return this builder * @throws IllegalArgumentException if the {@code positionWindows} * parameter is {@code null} * @throws IllegalStateException if the {@code positionWindows} field * is already set */ public Builder positionTestUI(PositionWindows positionWindows) { if (positionWindows == null) { throw new IllegalArgumentException("positionWindows parameter can't be null"); } if (this.positionWindows != null) { throw new IllegalStateException("PositionWindows is already set"); } this.positionWindows = positionWindows; return this; } /** * Positions the test UI windows in a row to the right of * the instruction frame. The top of the windows is aligned to * that of the instruction frame. * * @return this builder */ public Builder positionTestUIRightRow() { return position(Position.HORIZONTAL) .positionTestUI(WindowLayouts::rightOneRow); } /** * Positions the test UI windows in a column to the right of * the instruction frame. The top of the first window is aligned to * that of the instruction frame. * * @return this builder */ public Builder positionTestUIRightColumn() { return position(Position.HORIZONTAL) .positionTestUI(WindowLayouts::rightOneColumn); } /** * Positions the test UI windows in a column to the right of * the instruction frame centering the stack of the windows. * * @return this builder */ public Builder positionTestUIRightColumnCentered() { return position(Position.HORIZONTAL) .positionTestUI(WindowLayouts::rightOneColumnCentered); } /** * Positions the test UI windows in a row to the bottom of * the instruction frame. The left of the first window is aligned to * that of the instruction frame. * * @return this builder */ public Builder positionTestUIBottomRow() { return position(Position.VERTICAL) .positionTestUI(WindowLayouts::bottomOneRow); } /** * Positions the test UI windows in a row to the bottom of * the instruction frame centering the row of the windows. * * @return this builder */ public Builder positionTestUIBottomRowCentered() { return position(Position.VERTICAL) .positionTestUI(WindowLayouts::bottomOneRowCentered); } /** * Positions the test UI windows in a column to the bottom of * the instruction frame. The left of the first window is aligned to * that of the instruction frame. * * @return this builder */ public Builder positionTestUIBottomColumn() { return position(Position.VERTICAL) .positionTestUI(WindowLayouts::bottomOneColumn); } /** * Adds a {@code WindowListCreator} which the framework will use * to create a list of test UI windows. * * @param windowListCreator a {@code WindowListCreator} * to create test UI windows * @return this builder * @throws IllegalArgumentException if {@code windowListCreator} is {@code null} * @throws IllegalStateException if a window creator * or a list of test windows is already set */ public Builder testUI(WindowListCreator windowListCreator) { if (windowListCreator == null) { throw new IllegalArgumentException("The window list creator can't be null"); } checkWindowsLists(); this.windowListCreator = windowListCreator; return this; } /** * Adds an already created test UI window. * The window is positioned and shown automatically. * * @param window a test UI window * @return this builder */ public Builder testUI(Window window) { return testUI(List.of(window)); } /** * Adds an array of already created test UI windows. * * @param windows an array of test UI windows * @return this builder */ public Builder testUI(Window... windows) { return testUI(List.of(windows)); } /** * Adds a list of already created test UI windows. * * @param windows a list of test UI windows * @return this builder * @throws IllegalArgumentException if {@code windows} is {@code null} * or the list contains {@code null} * @throws IllegalStateException if a window creator * or a list of test windows is already set */ public Builder testUI(List windows) { if (windows == null) { throw new IllegalArgumentException("The list of windows can't be null"); } if (windows.stream() .anyMatch(Objects::isNull)) { throw new IllegalArgumentException("The list of windows can't contain null"); } checkWindowsLists(); this.testWindows = windows; return this; } /** * Verifies the state of window list and window creator. * * @throws IllegalStateException if a windows list creator * or a list of test windows is already set */ private void checkWindowsLists() { if (windowListCreator != null) { throw new IllegalStateException("Window list creator is already set"); } if (testWindows != null) { throw new IllegalStateException("The list of test windows is already set"); } } /** * Adds a {@code PanelCreator} which the framework will use * to create a component and place it into a dialog. * * @param panelCreator a {@code PanelCreator} to create a component * with test UI * @return this builder * @throws IllegalStateException if split UI was enabled using * a {@code splitUI} method */ public Builder testUI(PanelCreator panelCreator) { if (splitUI) { throw new IllegalStateException("Can't combine splitUI and " + "testUI with panelCreator"); } this.panelCreator = panelCreator; return this; } /** * Adds a {@code PanelCreator} which the framework will use * to create a component with test UI and display it in a split pane. *

* By default, horizontal orientation is used, * and test UI is displayed to the right of the instruction UI. * * @param panelCreator a {@code PanelCreator} to create a component * with test UI * @return this builder * * @throws IllegalStateException if a {@code PanelCreator} is * already set * @throws IllegalArgumentException if {panelCreator} is {@code null} */ public Builder splitUI(PanelCreator panelCreator) { return splitUIRight(panelCreator); } /** * Adds a {@code PanelCreator} which the framework will use * to create a component with test UI and display it * to the right of instruction UI. * * @param panelCreator a {@code PanelCreator} to create a component * with test UI * @return this builder * * @throws IllegalStateException if a {@code PanelCreator} is * already set * @throws IllegalArgumentException if {panelCreator} is {@code null} */ public Builder splitUIRight(PanelCreator panelCreator) { return splitUI(panelCreator, JSplitPane.HORIZONTAL_SPLIT); } /** * Adds a {@code PanelCreator} which the framework will use * to create a component with test UI and display it * in the bottom of instruction UI. * * @param panelCreator a {@code PanelCreator} to create a component * with test UI * @return this builder * * @throws IllegalStateException if a {@code PanelCreator} is * already set * @throws IllegalArgumentException if {panelCreator} is {@code null} */ public Builder splitUIBottom(PanelCreator panelCreator) { return splitUI(panelCreator, JSplitPane.VERTICAL_SPLIT); } /** * Enables split UI and stores the orientation of the split pane. * * @param panelCreator a {@code PanelCreator} to create a component * with test UI * @param splitUIOrientation orientation of the split pane * @return this builder * * @throws IllegalStateException if a {@code PanelCreator} is * already set * @throws IllegalArgumentException if {panelCreator} is {@code null} */ private Builder splitUI(PanelCreator panelCreator, int splitUIOrientation) { if (panelCreator == null) { throw new IllegalArgumentException("A PanelCreator cannot be null"); } if (this.panelCreator != null) { throw new IllegalStateException("A PanelCreator is already set"); } splitUI = true; this.splitUIOrientation = splitUIOrientation; this.panelCreator = panelCreator; return this; } public Builder position(Position position) { this.position = position; return this; } public PassFailJFrame build() throws InterruptedException, InvocationTargetException { validate(); return new PassFailJFrame(this); } /** * Returns the file name of the test, if the {@code test.file} property * is defined, concatenated with {@code " - "} which serves as a prefix * to the default instruction frame title; * or an empty string if the {@code test.file} property is not defined. * * @return the prefix to the default title: * either the file name of the test or an empty string * * @see jtreg * test-specific system properties and environment variables */ private static String getTestFileNamePrefix() { String testFile = System.getProperty("test.file"); if (testFile == null) { return ""; } return Paths.get(testFile).getFileName().toString() + " - "; } /** * Validates the state of the builder and * expands parameters that have no assigned values * to their default values. * * @throws IllegalStateException if no instructions are provided, * or if {@code PositionWindows} implementation is * provided but neither window creator nor * test window list are set */ private void validate() { if (title == null) { title = getTestFileNamePrefix() + TITLE; } if (instructions == null || instructions.isEmpty()) { throw new IllegalStateException("Please provide the test " + "instructions for this manual test"); } if (testTimeOut == 0L) { testTimeOut = TEST_TIMEOUT; } if (rows == 0) { rows = getDefaultRows(); } if (columns == 0) { columns = COLUMNS; } if (position == null && (testWindows != null || windowListCreator != null || (!splitUI && panelCreator != null))) { position = Position.HORIZONTAL; } if (positionWindows != null) { if (testWindows == null && windowListCreator == null) { throw new IllegalStateException("To position windows, " + "provide a list of windows to the builder"); } instructionUIHandler = new InstructionUIHandler(); } } private final class InstructionUIHandler implements InstructionUI { @Override public Point getLocation() { return frame.getLocation(); } @Override public Dimension getSize() { return frame.getSize(); } @Override public Rectangle getBounds() { return frame.getBounds(); } @Override public void setLocation(Point location) { setLocation(location.x, location.y); } @Override public void setLocation(int x, int y) { frame.setLocation(x, y); } @Override public Position getPosition() { return position; } } } /** * Creates a builder for configuring {@code PassFailJFrame}. * * @return the builder for configuring {@code PassFailJFrame} */ public static Builder builder() { return new Builder(); } }