/*******************************************************************************
* Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
* which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
* Oracle - initial API and implementation from Oracle TopLink
******************************************************************************/
package org.eclipse.persistence.tools.workbench.test.scplugin.ui;
// JDK
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.GraphicsEnvironment;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Vector;
import javax.swing.AbstractButton;
import javax.swing.ComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.UIManager;
import javax.swing.plaf.basic.ComboPopup;
import javax.swing.text.JTextComponent;
import junit.framework.Test;
import junit.framework.TestCase;
import org.eclipse.persistence.tools.workbench.framework.resources.ResourceRepository;
import org.eclipse.persistence.tools.workbench.utility.string.StringTools;
/**
* @version 10.1.3
* @author Pascal Filion
*/
public abstract class AbstractPanelTest extends TestCase
{
/**
* This component is used to grab the focus between test for transfering the
* focus on the desired component.
*/
private JComponent focusGrabber;
/**
* The pane to be tested.
*/
private JComponent pane;
/**
* The parent of this <code>AbstractPanelTest</code> if this one is ran
* inside of another one.
*/
final AbstractPanelTest parentTest;
/**
* This <code>Robot</code> is used to simulate a ALT-[mnemonic] in order to
* transfer the focus on a component.
*/
private Robot robot;
/**
* Determines if this test is run as a standalone application.
*/
private boolean standalone;
/**
* The main window used for testing.
*/
protected JFrame window;
/**
* The identifier for components that are an instance of <code>JCheckBox</code>.
*/
public static final int COMPONENT_CHECK_BOX = 1;
/**
* The identifier for components that are an instance of <code>JComboBox</code>.
*/
public static final int COMPONENT_COMBO_BOX = 2;
/**
* The identifier for components that are an instance of <code>JList</code>.
*/
public static final int COMPONENT_LIST = 3;
/**
* The identifier for components that are an instance of <code>JMenu</code>.
*/
public static final int COMPONENT_MENU = 4;
/**
* The identifier for components that are an instance of <code>JRadioButton</code>.
*/
public static final int COMPONENT_RADIO_BUTTON = 5;
/**
* The identifier for components that are an instance of <code>JSpinner</code>.
*/
public static final int COMPONENT_SPINNER = 6;
/**
* The identifier for components that are an instance of <code>JTable</code>.
*/
public static final int COMPONENT_TABLE = 7;
/**
* The identifier for components that are an instance of <code>JTextArea</code>.
*/
public static final int COMPONENT_TEXT_AREA = 8;
/**
* The identifier for components that are an instance of <code>JTextField</code>.
*/
public static final int COMPONENT_TEXT_COMPONENT = 9;
/**
* The identifier for components that are an instance of <code>JTextField</code>.
*/
public static final int COMPONENT_TEXT_FIELD = 10;
/**
* The identifier for components that are an instance of <code>JTree</code>.
*/
public static final int COMPONENT_TREE = 11;
/**
* A constant corresponding to not use any mask over a keyboard key.
*/
public static final int NO_MASK = -1;
/**
* The IP address of Pascal's computer.
*/
static final String PASCAL_COMPUTER_IP_ADDRESS = "138.2.91.79";
/**
* The prefix of the methods that test entering values into fields, which is
* "_testComponentEnabler".
*/
protected static final String TEST_COMPONENT_ENABLER_SIGNATURE = "_testComponentEnabler";
/**
* The prefix of the methods that test entering values into fields, which is
* "_testComponentEntry".
*/
protected static final String TEST_COMPONENT_ENTRY_SIGNATURE = "_testComponentEntry";
/**
* The prefix of the methods that test entering values into fields, which is
* "_testExtra".
*/
protected static final String TEST_EXTRA_SIGNATURE = "_testExtra";
/**
* The prefix of the methods that test the transfer of the focus, which is
* "_testFocusTransfer".
*/
protected static final String TEST_FOCUS_TRANSFER_SIGNATURE = "_testFocusTransfer";
/**
* The prefix of the methods that is requesting another
* <code>AbstractPanelTest</code> to be run within this one, which is
* "_testSubPane".
*/
protected static final String TEST_SUB_PANE_SIGNATURE = "_testSubPane";
/**
* The IP address of Tran's computer.
*/
static final String TRAN_COMPUTER_IP_ADDRESS = "138.2.91.83";
/**
* Allows to change the Look and Feel once before starting the test.
*/
static
{
Toolkit.getDefaultToolkit().setDynamicLayout(true);
// Make sure the locale is French so on French-Canadian keyboard, the
// key mapping is correctly done
try
{
InetAddress inetAddress = InetAddress.getLocalHost();
// Pascal's work computer
if (PASCAL_COMPUTER_IP_ADDRESS.equals(inetAddress.getHostAddress()))
Locale.setDefault(Locale.CANADA_FRENCH);
// Pascal's home computer
else if ("EnzoMatrix".equals(inetAddress.getHostName()))
Locale.setDefault(Locale.CANADA_FRENCH);
}
catch (UnknownHostException e)
{
// Ignore
}
// Set the look and feel
try
{
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
// UIManager.setLookAndFeel("com.sun.java.swing.plaf.motif.MotifLookAndFeel");
// UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
// UIManager.setLookAndFeel("oracle.bali.ewt.olaf.OracleLookAndFeel");
}
catch (Throwable e)
{
e.printStackTrace();
}
}
/**
* Creates a new <code>AbstractPanelTest</code>.
*
* @param parentTest The parent where this <code>TestCase</code> will be ran from
*/
protected AbstractPanelTest(AbstractPanelTest parentTest)
{
this("SubTest", parentTest);
}
/**
* Creates a new <code>AbstractPanelTest</code>.
*
* @param name The name of this class
*/
protected AbstractPanelTest(String name)
{
this(name, null);
}
/**
* Creates a new <code>AbstractPanelTest</code>.
*
* @param name The name of this class
* @param parentTest The parent where this <code>TestCase</code> will be ran from
*/
private AbstractPanelTest(String name, AbstractPanelTest parentTest)
{
super(name);
this.parentTest = parentTest;
}
/**
* Tests the UI by invoking a set of methods.
*
* @throws Throwable
*/
final void _subTestUI() throws Throwable
{
_testFocusTransferByMnemonic();
_testComponentEntry();
_testComponentEnabler();
_testExtra();
_testSubTest();
}
/**
* Retrieves all the methods from the JUnit test class, which is a subclass
* of this class that begin with {@link #TEST_COMPONENT_ENABLER_SIGNATURE}
* and invoke them.
*
* @throws Throwable If any problems occurred during the execution of a method
*/
private void _testComponentEnabler() throws Throwable
{
runMethods(retrieveMethods(TEST_COMPONENT_ENABLER_SIGNATURE));
}
/**
* Retrieves all the methods from the JUnit test class, which is a subclass
* of this class that begin with {@link #TEST_COMPONENT_ENTRY_SIGNATURE}and
* invoke them.
*
* @throws Throwable If any problems occurred during the execution of a method
*/
private void _testComponentEntry() throws Throwable
{
runMethods(retrieveMethods(TEST_COMPONENT_ENTRY_SIGNATURE));
}
/**
* Tests
*
* @param component
* @param componentType
*/
private void _testComponentType(Component component, int componentType)
{
switch (componentType)
{
case COMPONENT_CHECK_BOX:
{
assertTrue("The wrong component received the focus: " + component,
component instanceof JCheckBox);
break;
}
case COMPONENT_COMBO_BOX:
{
component = retrieveComboBox(component);
assertTrue("The wrong component received the focus: " + component,
component instanceof JComboBox);
break;
}
case COMPONENT_LIST:
{
assertTrue("The wrong component received the focus: " + component,
component instanceof JList);
break;
}
case COMPONENT_MENU:
{
assertTrue("The wrong component received the focus: " + component,
component instanceof JMenu);
break;
}
case COMPONENT_SPINNER:
{
component = retrieveSpinner(component);
assertTrue("The wrong component received the focus: " + component,
component instanceof JSpinner);
break;
}
case COMPONENT_RADIO_BUTTON:
{
assertTrue("The wrong component received the focus: ",
component instanceof JRadioButton);
break;
}
case COMPONENT_TABLE:
{
assertTrue("The wrong component received the focus: ",
component instanceof JTable);
break;
}
case COMPONENT_TEXT_COMPONENT:
{
assertTrue("The wrong component received the focus: " + component,
component instanceof JTextComponent);
break;
}
case COMPONENT_TEXT_AREA:
{
assertTrue("The wrong component received the focus: " + component,
component instanceof JTextArea);
break;
}
case COMPONENT_TEXT_FIELD:
{
assertTrue("The wrong component received the focus: " + component,
component instanceof JTextField);
break;
}
case COMPONENT_TREE:
{
assertTrue("The wrong component received the focus: " + component,
component instanceof JTree);
break;
}
default:
{
assertTrue("The component is of an unknown type: " + component, false);
}
}
}
/**
* Tests to make sure a pane does not have duplicate mnemonic. If sub-pane
* can be changed based on certain values, then new sub-pane can be tested
* by overriding {@link #canContinueTestingDuplicateMnemonic()} and perform
* one change at a time and once all the possible changes have been tested,
* <code>false</code> will stop the testing.
*/
private void _testDuplicateMnemonicImp() throws Throwable
{
if (this.pane == null)
this.pane = buildPaneImp();
// Prepare the mnemonic list and add the reserved mnemonic found in the
// menu bar: File | Workbench | Selected | Tools | Window | Help
Hashtable mnemonics = new Hashtable();
registerMnemonic(mnemonics, "FILE_MENU");
// registerMnemonic(mnemonics, "WORKBENCH_MENU");
registerMnemonic(mnemonics, "SELECTED_MENU");
registerMnemonic(mnemonics, "TOOLS_MENU");
registerMnemonic(mnemonics, "WINDOW_MENU");
registerMnemonic(mnemonics, "HELP_MENU");
registerMnemonic(mnemonics, "NAVIGATOR_LABEL");
registerMnemonic(mnemonics, "EDITOR_LABEL");
Hashtable copy = (Hashtable) mnemonics.clone();
// First test with the way the pane first appear on screen
_testDuplicateMnemonicImp(this.pane, copy);
// Dump the duplicate mnemonic into the console
dumpDuplicateMnemonicResults(copy);
// Then ask the subclass to complete the testing by changing any values
// in order for PanelSwitcherAdapter to change their content
while (canContinueTestingDuplicateMnemonic())
{
// Continue with the original mnemonic dictionary
copy = (Hashtable) mnemonics.clone();
// Retest because the content of the pane changed
_testDuplicateMnemonicImp(this.pane, copy);
// Dump the duplicate mnemonic into the console
dumpDuplicateMnemonicResults(copy);
}
}
/**
* Tests to make sure a pane does not have duplicate mnemonic.
*
* @param container
* @param mnemonics
*/
private void _testDuplicateMnemonicImp(Container container, Map mnemonics)
{
Component[] children = container.getComponents();
for (int index = 0; index < children.length; index++)
{
Component component = children[index];
// Retrieve the viewport's view
if (component instanceof JScrollPane)
{
JScrollPane scrollPane = (JScrollPane) component;
component = scrollPane.getViewport().getView();
// Recurse to be safe
_testDuplicateMnemonicImp((Container) component, mnemonics);
}
// Recurse through panel
else if (component instanceof JPanel)
{
_testDuplicateMnemonicImp((Container) component, mnemonics);
}
// The mnemonic is not accessible, it is the responsibility of the
// JUnit test to active the component during this test to make sure it
// is safe to use the mnemonic since something the same mnemonic can be
// shared by more than one component but some are disabled and only one
// is enabled
else if (component.isEnabled() &&
component.isVisible())
{
// JLabel, get the text and mnemonic
if (component instanceof JLabel)
{
JLabel label = (JLabel) component;
storeMnemonic(mnemonics, label.getText(), label.getDisplayedMnemonic());
}
// JButton, get the text and mnemonic
else if (component instanceof AbstractButton)
{
AbstractButton button = (AbstractButton) component;
storeMnemonic(mnemonics, button.getText(), button.getMnemonic());
}
}
}
}
/**
* Retrieves all the methods from the JUnit test class, which is a subclass
* of this class that begin with {@link #TEST_EXTRA_SIGNATURE} and invoke
* them.
*
* @throws Throwable If any problems occurred during the execution of a method
*/
private void _testExtra() throws Throwable
{
runMethods(retrieveMethods(TEST_EXTRA_SIGNATURE));
}
/**
* Tests to see if the mnemonics of a pane are properly transfered.
*
* @throws Throwable
*/
private void _testFocusTransferByMnemonic() throws Throwable
{
runMethods(retrieveMethods(TEST_FOCUS_TRANSFER_SIGNATURE));
}
/**
* Tests
*
* @param component
* @param componentType
* @param labelText
*/
private void _testLabeledByText(Component component,
int componentType,
String key)
{
switch (componentType)
{
case COMPONENT_SPINNER:
{
component = retrieveSpinner(component);
assertEquals(component.getName(), key);
break;
}
case COMPONENT_COMBO_BOX:
{
component = retrieveComboBox(component);
assertEquals(component.getName(), key);
break;
}
default:
{
assertEquals(component.getName(), key);
break;
}
}
}
/**
* Retrieves all the methods from the JUnit test class, which is a subclass
* of this class that begin with {@link #TEST_SUB_PANE_SIGNATURE}and invoke
* them.
*
* @throws Throwable If any problems occurred during the execution of a method
*/
private void _testSubTest() throws Throwable
{
runMethods(retrieveMethods(TEST_SUB_PANE_SIGNATURE));
}
/**
* Acquires the <code>Robot</code> that is responsible to simulate user actions.
*/
private void acquireRobot()
{
try
{
if (this.robot == null)
this.robot = new Robot(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice());
}
catch (Throwable e)
{
fail("Robot could not be acquired: " + e);
}
}
/**
* Determines
*
* @param pane
* @return
*/
protected final boolean areChildrenDisabled(JComponent pane)
{
return checkChildrenEnableState(pane, false);
}
/**
* Determines
*
* @param pane
* @return
*/
protected final boolean areChildrenEnabled(JComponent pane)
{
return checkChildrenEnableState(pane, true);
}
/**
* Creates a component that can receives the focus but will not alter the
* testing.
*/
private void buildFocusGrabber()
{
if (this.parentTest == null)
{
this.focusGrabber = new JButton("Focus Grabber");
this.focusGrabber.setFocusable(true);
this.focusGrabber.setRequestFocusEnabled(true);
}
else
{
this.focusGrabber = this.parentTest.focusGrabber;
}
}
/**
* Creates the pane used for testing its widgets.
*
* @return The <code>JComponent</code> with all its widgets
*/
protected abstract JComponent buildPane() throws Exception;
/**
* Creates the pane to be tested by this JUnit test.
*
* @throws Exception If any problem occurs during the creation of the pane
*/
private JComponent buildPaneImp() throws Exception
{
if (this.pane == null)
{
if (this.parentTest != null)
this.pane = this.parentTest.pane;
else
this.pane = buildPane();
}
return this.pane;
}
/**
* Creates the window that will be used to test different UI features.
*/
protected final void buildWindow()
{
if (this.window == null)
{
if (this.parentTest == null)
this.window = new JFrame(windowTitle());
else
this.window = this.parentTest.window;
}
}
/**
* Tests to make sure a pane does not have duplicate mnemonic.
*
* @param count
* @return
*/
protected boolean canContinueTestingDuplicateMnemonic()
{
return false;
}
/**
* Determines
*
* @param pane
* @return
*/
protected final boolean checkChildrenEnableState(Container pane, boolean enabled)
{
for (int index = pane.getComponentCount(); --index >= 0;)
{
Component component = pane.getComponent(index);
// JPanel are usually not disabled, check its children
if ((component instanceof JPanel) &&
(!checkChildrenEnableState((JPanel) component, enabled)))
{
return false;
}
// Check the scroll pane's view
if (component instanceof JScrollPane)
{
JScrollPane scrollPane = (JScrollPane) component;
component = scrollPane.getViewport().getView();
if (! checkChildrenEnableState((Container) component, enabled))
return false;
}
// At this point, we should be at component that would be disabled
if (pane.getComponent(index).isEnabled() != enabled)
{
return false;
}
}
return true;
}
/**
* Makes sure the focus owner, if it is a text component, has been cleared
* from its default value so that entering a new value will not affect the
* conclusion of the test.
*/
private void clearTextInput() throws Exception
{
// Only need to do CTRL-A to select all the text, typing will clear the
// text automatically
simulateKeyImp(KeyEvent.VK_CONTROL, 'A');
}
/**
* Asserts when there are duplicate mnemonics.
*
* @param mnemonics The table containing the result of the scan
*/
private void dumpDuplicateMnemonicResults(Hashtable mnemonics)
{
StringBuffer dump = new StringBuffer();
for (Iterator iter = mnemonics.entrySet().iterator(); iter.hasNext();)
{
Map.Entry entry = (Map.Entry) iter.next();
Vector duplicate = (Vector) entry.getValue();
if (duplicate.size() > 1)
{
Integer mnemonic = (Integer) entry.getKey();
dump.append(StringTools.CR);
dump.append(new Character((char) mnemonic.intValue()));
dump.append(" is used by multiple labels: ");
dump.append(duplicate);
}
}
if (dump.length() > 0)
{
fail(dump.toString());
}
}
/**
* Executes this JUnit test as a standalone application by using its own
* window.
*
* @param arguments The list of arguments passed by the main method
*/
protected void execute(String[] arguments) throws Exception
{
this.standalone = true;
setUp();
this.window.setVisible(true);
}
/**
* Returns the pane used for testing its widgets.
*
* @return The <code>JComponent</code> with all its widgets
*/
protected final JComponent getPane()
{
return this.pane;
}
/**
* Returns
*
* @return
*/
protected abstract ResourceRepository getResourceRepository();
/**
* Iniatlizes the main window where the page is displayed.
*
* @Throwable Throwable
*/
protected void initializeWindow() throws Exception
{
if (this.parentTest != null)
return;
JPanel panel = new JPanel(new BorderLayout());
panel.add(buildPaneImp(), BorderLayout.CENTER);
if (!this.standalone)
panel.add(this.focusGrabber, BorderLayout.SOUTH);
this.window.getContentPane().add(panel, "Center");
this.window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.window.pack();
this.window.addWindowListener(new WindowAdapter()
{
public void windowClosing(WindowEvent event)
{
try
{
if (AbstractPanelTest.this.standalone)
tearDown();
}
catch (Throwable e)
{
e.printStackTrace();
}
}
});
}
/**
* Translates the given character into the key code. If the character is
* [a-z|A-Z|0-9] then the char is simply converted into an integer.
*
* @param character The character to be converted
* @return The key code of the given character
* @see KeyEvent
*/
private int[] keyCode(int key)
{
boolean french = "fr".equalsIgnoreCase(Locale.getDefault().getLanguage());
// Common keys
switch (key)
{
case '!': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_1 };
case '$': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_4 };
case '%': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_5 };
case '^': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_6 };
case '&': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_7 };
case '*': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_8 };
case '(': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_9 };
case ')': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_0 };
case '_': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_MINUS };
case '+': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_EQUALS };
case ':': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_SEMICOLON };
}
// Mapping French-Canadian keyboard
if (french)
{
switch (key)
{
case '#': return new int[] { NO_MASK, KeyEvent.VK_BACK_QUOTE };
case '@': return new int[] { KeyEvent.VK_ALT_GRAPH, KeyEvent.VK_2 };
case '[': return new int[] { KeyEvent.VK_ALT_GRAPH, KeyEvent.VK_OPEN_BRACKET };
case ']': return new int[] { KeyEvent.VK_ALT_GRAPH, KeyEvent.VK_CLOSE_BRACKET };
case '{': return new int[] { KeyEvent.VK_ALT_GRAPH, KeyEvent.VK_QUOTE };
case '}': return new int[] { KeyEvent.VK_ALT_GRAPH, KeyEvent.VK_BACK_SLASH };
case '|': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_BACK_QUOTE };
case '\\': return new int[] { KeyEvent.VK_ALT_GRAPH, KeyEvent.VK_BACK_QUOTE };
case '\'': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_PERIOD };
case '"': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_2 };
case '<': return new int[] { NO_MASK, KeyEvent.VK_BACK_SLASH };
case '>': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_BACK_SLASH };
case '/': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_3 };
case '?': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_6 };
}
}
// Mapping US-English keyboard
else
{
switch (key)
{
case '#': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_3 };
case '@': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_2 };
case '[': return new int[] { NO_MASK, KeyEvent.VK_OPEN_BRACKET };
case ']': return new int[] { NO_MASK, KeyEvent.VK_CLOSE_BRACKET };
case '{': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_OPEN_BRACKET };
case '}': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_CLOSE_BRACKET };
case '|': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_BACK_SLASH };
case '\\': return new int[] { NO_MASK, KeyEvent.VK_BACK_SLASH };
case '\'': return new int[] { NO_MASK, KeyEvent.VK_QUOTE };
case '"': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_QUOTE };
case '<': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_COMMA };
case '>': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_PERIOD };
case '/': return new int[] { NO_MASK, KeyEvent.VK_SLASH };
case '?': return new int[] { KeyEvent.VK_SHIFT, KeyEvent.VK_SLASH };
}
}
boolean upperCase = Character.isUpperCase((char) key);
return new int[]
{
upperCase ? KeyEvent.VK_SHIFT : NO_MASK,
Character.toUpperCase((char) key)
};
}
/**
* Registers the mnemonic associated with the given key into the given table.
*
* @param table The table where to add the mnemonic along with an empty list
* that will be used to store the duplicate mnemonics
* @param key The key used to retrieve the mnemonic
*/
private void registerMnemonic(Map table, String key)
{
ResourceRepository repository = getResourceRepository();
Vector vector = new Vector();
vector.add(repository.getString(key));
table.put(new Integer(repository.getMnemonic(key)), vector);
}
/**
* Makes sure the focus is not on any components in the tested pane.
*/
private void requestFocus(final Component component) throws Exception
{
component.requestFocus();
sleep();
}
/**
* Makes sure the focus is not on any components in the tested pane.
*/
private void resetFocus() throws Exception
{
requestFocus(this.focusGrabber);
assertTrue(this.focusGrabber.hasFocus());
}
/**
* Retrieves the child component from the given container if one exist that
* has the given key as its name.
*
* @param key The name of the component to look for
* @param container The container to traverse
* @return Either the desired component or <code>null</code> if it could not
* be found within the children of the given container
*/
protected final JComponent retrieveChildComponent(String key, JComponent container)
{
for (int index = 0; index < container.getComponentCount(); index++)
{
JComponent childComponent = (JComponent) container.getComponent(index);
// Directly retrieve the scroll pane's view
if (childComponent instanceof JScrollPane)
{
JScrollPane scrollPane = (JScrollPane) childComponent;
childComponent = (JComponent) scrollPane.getViewport().getView();
}
// The component was found
if (key.equals(childComponent.getName()) &&
!(childComponent instanceof JLabel))
{
return childComponent;
}
// We'll assume only JPanel have children that we are interested to scan
else if (childComponent instanceof JPanel)
{
childComponent = retrieveChildComponent(key, childComponent);
if (childComponent != null)
return childComponent;
}
}
return null;
}
/**
* Editable <code>JComBoxBox</code> does not receive the focus but it's
* editor's component.
*
* @param component The component used to retrieve the combo box: either
* itself or one of its parent
* @return The given component if it is a <code>JComboBox</code> otherwise
* one of its parent
*/
private Component retrieveComboBox(Component component)
{
if (component == null)
return null;
if (component instanceof JComboBox)
return component;
return retrieveComboBox(component.getParent());
}
/**
* Retrieves the value contained by the <code>JTextComponent</code> that has
* the given key as its name.
*
* @param key The name of the component to search
* @return The <code>JTextComponent</code>'s text
*/
protected final String retrieveFieldEntry(String key)
{
JComponent component = retrieveChildComponent(key, this.pane);
_testComponentType(component, COMPONENT_TEXT_COMPONENT);
JTextComponent textComponent = (JTextComponent) component;
String entry = textComponent.getText();
if (entry.length() == 0)
entry = null;
return entry;
}
/**
* Retrieves all the methods where their names start with the given
* signature.
*
* @param testClass The class scanned for methods
* @param signature The prefix of the method's name
* @param testMethods The collection of methods where the methods need to be
* added to
*/
private void retrieveMethods(Class testClass,
String signature,
Collection testMethods)
{
if (AbstractPanelTest.class.equals(testClass))
return;
Method[] methods = testClass.getDeclaredMethods();
for (int index = methods.length; --index >= 0;)
{
Method method = methods[index];
if (method.getName().startsWith(signature) &&
(method.getParameterTypes().length == 0))
{
testMethods.add(method);
}
}
retrieveMethods(testClass.getSuperclass(), signature, testMethods);
}
/**
* Retrieves all the methods where their names start with the given
* signature.
*
* @param signature The prefix of the method's name
* @return A collection of methods where their names start with the given
* signature
*/
private Collection retrieveMethods(String signature)
{
Vector testMethods = new Vector();
retrieveMethods(getClass(), signature, testMethods);
return testMethods;
}
/**
* Editable <code>JSpinner</code> does not receive the focus but it's
* editor's component.
*
* @param component The component used to retrieve the spinner: either
* itself or one of its parent
* @return The given component if it is a <code>JSpinner</code> otherwise one
* of its parent
*/
private JSpinner retrieveSpinner(Component component)
{
if (component == null)
return null;
if (component instanceof JSpinner)
return (JSpinner) component;
return retrieveSpinner(component.getParent());
}
/**
* Retrieves the value contained by the <code>JTextComponent</code> that has
* the given key as its name.
*
* @param key The name of the component to search
* @return The <code>JTextComponent</code>'s text
*/
protected final Number retrieveSpinnerEntry(String key)
{
JComponent component = retrieveChildComponent(key, this.pane);
_testComponentType(component, COMPONENT_SPINNER);
JSpinner spinner = (JSpinner) component;
return (Number) spinner.getValue();
}
/**
* Runs all the methods contained in the given collection. If one fails, then
* the process is stopped.
*
* @param methods The methods to be ran
* @throws Throwable If any problem was encountered in an invoked method
*/
private void runMethods(Collection methods) throws Throwable
{
for (Iterator iter = methods.iterator(); iter.hasNext() ;)
{
try
{
updateWindowState();
resetFocus();
Method method = (Method) iter.next();
method.setAccessible(true);
method.invoke(this, (Object[])null);
}
catch (InvocationTargetException e)
{
throw e.getCause();
}
}
}
/**
* Runs the test from another test case inside of this test case.
*
* @param testCase The test to be run inside of this one
* @throws Throwable If any problem was encountered during the execute of
* another JUnit test as a child of this one
*/
protected final void runTestCase(TestCase testCase) throws Throwable
{
new TestSuiteWrapper(testCase).runBare();
}
/**
* Initializes the necessary for running this test.
*
* @throws Exception If any problem was encountered during the initialization
* (preparation) of this test
*/
protected void setUp() throws Exception
{
buildFocusGrabber();
buildWindow();
initializeWindow();
}
/**
* Simulates
*
* @param item
*/
protected final void simulateComboBoxSelection(Object item)
{
Component focusOwner = this.window.getFocusOwner();
assertTrue(String.valueOf(focusOwner), focusOwner instanceof JComboBox);
JComboBox comboBox = (JComboBox) focusOwner;
comboBox.setSelectedItem(item);
}
/**
* Simulates
*
* @param localizedString
*/
protected final void simulateComboBoxSelectionByRenderer(String localizedString)
{
Component focusOwner = this.window.getFocusOwner();
assertTrue(String.valueOf(focusOwner), focusOwner instanceof JComboBox);
JComboBox comboBox = (JComboBox) focusOwner;
ComboPopup comboPopup = (ComboPopup) comboBox.getAccessibleContext().getAccessibleChild(0);
JList list = comboPopup.getList();
ComboBoxModel model = comboBox.getModel();
int count = model.getSize();
Object selectedItem = model.getSelectedItem();
for (int index = 0; index < count; index++)
{
Object item = model.getElementAt(index);
JLabel label = (JLabel) comboBox.getRenderer().getListCellRendererComponent(list, item, index, item == selectedItem, true);
if (localizedString.equals(label.getText()))
{
comboBox.setSelectedItem(item);
break;
}
}
}
/**
* Simulates
*
* @param input
* @throws Exception
*/
protected final void simulateComboBoxTextInput(String input) throws Exception
{
simulateTextInput(input);
simulateKey(KeyEvent.VK_TAB);
}
/**
* Simulates a key stroke with the given key code. If the given key code
* represents an upper case letter, than the SHIFT key will be simulated as
* well.
*
* @param text The text to be programmatically typed
*/
protected final void simulateFormattedTextInput(String text) throws Exception
{
simulateTextInput(text); // Enter the new value
simulateKey(KeyEvent.VK_TAB); // Push the value into the model
}
/**
* Simulates the given character. If the given key code represents an upper
* case letter, than the SHIFT key will be simulated as well.
* <p>
* Note: The only supported key code right now are [a-z|A-Z|0-9], it seems a
* letter (represented by a Unicode char) can't be easily converted into a
* key code.
*
* @param character The value of the key used to simulate the event
*/
protected final void simulateKey(char character) throws Exception
{
int[] keyCodes = keyCode(character);
simulateKeyImp(keyCodes[0], keyCodes[1]);
}
/**
* Simulates the given key.
*
* @param key The value of the key used to simulate the event
*/
protected final void simulateKey(int key) throws Exception
{
simulateKeyImp(NO_MASK, key);
}
/**
* Simulates a key stroke with the given mnemonic key and maintain the given
* mask pressed during the letter is typed.
*
* @param mask Either <code>KeyEvent.VK_ALT</code>, <code>KeyEvent.VK_CONTROL</code>
* or <code>KeyEvent.VK_SHIFT</code>
* @param letter The keyboard key to simulate
*/
protected final void simulateKey(int mask, int key) throws Exception
{
simulateKeyImp(mask, key);
}
/**
* Simulates a key stroke with the given mnemonic key and maintain the given
* mask pressed during the letter is typed.
*
* @param mask Either <code>KeyEvent.VK_ALT</code>, <code>KeyEvent.VK_CONTROL</code>
* or <code>KeyEvent.VK_SHIFT</code>
* @param letter The keyboard key to simulate
* @param sleep Determines whether this Thread needs to sleep once the
* simulation of the given key is performed
*/
private void simulateKeyImp(int mask, int key) throws Exception
{
simulateKeyImp(mask, key, true);
}
/**
* Simulates a key stroke with the given mnemonic key and maintain the given
* mask pressed during the letter is typed.
*
* @param mask Either <code>KeyEvent.VK_ALT</code>, <code>KeyEvent.VK_CONTROL</code>
* or <code>KeyEvent.VK_SHIFT</code>
* @param letter The keyboard key to simulate
* @param sleep Determines whether this Thread needs to sleep once the
* simulation of the given key is performed
*/
private void simulateKeyImp(final int mask, final int key, boolean sleep) throws Exception
{
acquireRobot();
if (mask != NO_MASK)
this.robot.keyPress(mask);
try
{
this.robot.keyPress(key);
this.robot.keyRelease(key);
}
finally
{
if (mask != NO_MASK)
this.robot.keyRelease(mask);
}
if (sleep)
sleep();
}
/**
* Simulates a key stroke with the given mnemonic key.
*
* @param mnemonicKey The value of the mnemonic used to simulate the event
*/
protected final void simulateMnemonic(int mnemonicKey) throws Exception
{
simulateKeyImp(KeyEvent.VK_ALT, mnemonicKey);
}
/**
* Simulates an ALT-[mnemonic] with the mnemonic key by retrieving it from
* the repository with the given key.
*
* @param repositoryKey The key used to retrieve the mnemonic from the
* repository
*/
protected final void simulateMnemonic(String repositoryKey) throws Exception
{
simulateMnemonic(getResourceRepository().getMnemonic(repositoryKey));
}
/**
* Simulates programmatically the input of the given number.
*
* @param number The value to be entered into a <code>JSpinner</code>
*/
protected final void simulateSpinnerInput(double number) throws Exception
{
simulateSpinnerInput(Double.toString(number));
}
/**
* Simulates programmatically the input of the given number.
*
* @param number The value to be entered into a <code>JSpinner</code>
*/
protected final void simulateSpinnerInput(float number) throws Exception
{
simulateSpinnerInput(Float.toString(number));
}
/**
* Simulates programmatically the input of the given number.
*
* @param number The value to be entered into a <code>JSpinner</code>
*/
protected final void simulateSpinnerInput(int number) throws Exception
{
simulateSpinnerInput(Integer.toString(number));
}
/**
* Simulates programmatically the input of the given number.
*
* @param number The value to be entered into a <code>JSpinner</code>
*/
protected final void simulateSpinnerInput(long number) throws Exception
{
simulateSpinnerInput(Long.toString(number));
}
/**
* Simulates programmatically the input of the given number.
*
* @param number The value to be entered into a <code>JSpinner</code>
*/
protected final void simulateSpinnerInput(short number) throws Exception
{
simulateSpinnerInput(Short.toString(number));
}
/**
* Simulates the given text by simulating every character.
*
* @param text The text to be programmatically typed
*/
protected final void simulateSpinnerInput(String text) throws Exception
{
Component focusOwner = this.window.getFocusOwner();
JSpinner spinner = retrieveSpinner(focusOwner);
assertTrue(String.valueOf(focusOwner), spinner != null);
// For some reason, the focus needs to be transfered
// to the editor's text component
transferFocusToSpinnerEditor(spinner);
// Clear the entry first
clearTextInput();
// Simulate the entry
simulateTextInput(text);
// This will commit the value
simulateKey(KeyEvent.VK_TAB);
}
/**
* Simulates the given text by simulating each letter of the string.
*
* @param text The text to be programmatically typed
*/
protected final void simulateTextInput(String text) throws Exception
{
Component focusOwner = this.window.getFocusOwner();
assertTrue(String.valueOf(focusOwner), focusOwner instanceof JTextComponent);
clearTextInput();
for (int index = 0; index < text.length(); index++)
{
try
{
int[] keyCodes = keyCode(text.charAt(index));
simulateKeyImp(keyCodes[0], keyCodes[1], false);
}
catch (IllegalArgumentException e)
{
fail("The character " + text.charAt(index) + " could not be simulated.");
}
}
sleep();
}
/**
* Runs a <code>Runnable</code> inside of the Event Dispatch Thread so that
* it will do everything until this one is done and this JUnit test can
* continue with its testing thinking the GUI is done with its tasks.
*/
protected final void sleep() throws Exception
{
Thread.sleep(200);
}
/**
* Stores into the given map the given mnemonic and text in order to dump any
* duplicate mnemonics.
*
* @param mnemonics The table of mnemonic associated with label or button text
* @param text The text of either a button or a label
* @param mnemonic The mnemonic to cache
*/
private void storeMnemonic(Map mnemonics,
String text,
int mnemonic)
{
if (mnemonic != '\0')
{
Integer character = new Integer(mnemonic);
if (mnemonics.containsKey(character))
{
Vector duplicate = (Vector) mnemonics.get(character);
duplicate.add(text);
}
else
{
Vector duplicate = new Vector();
mnemonics.put(character, duplicate);
duplicate.add(text);
}
}
}
/**
* Nullified everything that was initialized.
*
* @throws Exception
*/
protected void tearDown() throws Exception
{
this.window.dispose();
this.window = null;
this.focusGrabber = null;
}
/**
* Tests whether the focus was transfered properly using the given mnemonic.
* Once the focus has been transfered, make sure it was transfered onto the
* desired component.
*
* @param mnemonicKey The mnemonic used to trigger the change of focus
* @param labelText The text of the label used to test if the focus was
* transfered from the desired mnemonic assigned from the right label
* @param componentType The type of the component that should receive the
* focus
*/
protected final void testFocusTransferByMnemonic(final int mnemonicKey,
final String key,
final int componentType) throws Exception
{
updateWindowState();
resetFocus();
// Simulate an ALT-mnemonic event
simulateMnemonic(mnemonicKey);
// Test the focus component
Component component = this.window.getFocusOwner();
assertTrue("The focus was not transfered, no component received the focus",
component != null);
assertTrue("The focus was not transfered",
component != this.focusGrabber);
assertFalse("The focus component is a JLabel, this could be because its associated component is disabled",
component instanceof JLabel);
// Make sure the component that received the focus is the proper component
_testComponentType(component, componentType);
// Make sure the component, even though is of the correct type,
// is the expected component
_testLabeledByText(component, componentType, key);
}
/**
* Tests
*
* @param key
* @param componentType
*/
protected final void testFocusTransferByMnemonic(String key, int componentType) throws Exception
{
ResourceRepository repository = getResourceRepository();
testFocusTransferByMnemonic(repository.getMnemonic(key), key, componentType);
}
/**
* Tests the UI panels with several test suites.
*
* @throws Throwable
*/
public final void testUI() throws Throwable
{
_testDuplicateMnemonicImp();
try
{
updateWindowState();
_subTestUI();
}
finally
{
this.window.setVisible(false);
}
}
/**
* Transfer to the first text component contained in the given container.
*
* @param container The container to traverse in order to find a text component
* @return <code>true<code> if a text component was found; <code>false<code>
* otherwise
*/
private boolean transferFocusToSpinnerEditor(Container container) throws Exception
{
Component[] components = container.getComponents();
for (int index = 0; index < components.length; index++)
{
Component component = components[index];
if (components[index] instanceof JTextComponent)
{
requestFocus(components[index]);
return true;
}
else if (component instanceof Container)
{
if (transferFocusToSpinnerEditor((Container) component))
{
return true;
}
}
}
return false;
}
/**
* Makes sure the window is visible and in front of all the other windows.
* Once that is done, reset the focus.
*/
private void updateWindowState() throws Exception
{
EventQueue.invokeAndWait(new Runnable()
{
public void run()
{
if (!AbstractPanelTest.this.window.isVisible())
AbstractPanelTest.this.window.setVisible(true);
AbstractPanelTest.this.window.toFront();
}
});
}
/**
* Returns the title for the main window and the clone window.
*
* @return The title of the main window and close window
*/
protected abstract String windowTitle();
/**
*
*/
private class TestCaseWrapper
{
private String fName;
private TestCase testCase;
private TestCaseWrapper(TestCase testCase, String fName)
{
super();
this.fName = fName;
this.testCase = testCase;
}
private Method retrieveMethod(Class theClass, String methodName) throws Throwable
{
if (theClass == null)
return null;
try
{
Method method = theClass.getDeclaredMethod(methodName, new Class[0]);
if (method != null)
return method;
}
catch (SecurityException e)
{
// Ignore, let scan the superclass
}
catch (NoSuchMethodException e)
{
// Ignore, let scan the superclass
}
return retrieveMethod(theClass.getSuperclass(), methodName);
}
/**
* Runs the bare test sequence.
*
* @exception Throwable if any exception is thrown
*/
void runBare() throws Throwable
{
setUp();
try
{
runMethod(this.fName);
}
finally
{
tearDown();
}
}
private void runMethod(String methodName) throws Throwable
{
assertNotNull(methodName);
Method runMethod = null;
try
{
// use getMethod to get all public inherited
// methods. getDeclaredMethods returns all
// methods of this class but excludes the
// inherited ones.
runMethod = retrieveMethod(this.testCase.getClass(), methodName);
runMethod.setAccessible(true);
}
catch (NoSuchMethodException e)
{
fail("Method \"" + methodName + "\" not found");
}
try
{
runMethod.invoke(this.testCase, (Object[])new Class[0]);
}
catch (InvocationTargetException e)
{
e.fillInStackTrace();
throw e.getTargetException();
}
catch (IllegalAccessException e)
{
e.fillInStackTrace();
throw e;
}
}
private void setUp() throws Throwable
{
runMethod("setUp");
}
private void tearDown() throws Throwable
{
runMethod("tearDown");
}
}
/**
* This <code>TestSuiteWrapper</code> will run all the tests defined in a
* <code>TestCase</code>. {@link AbstractPanelTest#testUI()} will not be ran
* but {@link AbstractPanelTest#_subTestUI()} will be used instead in order
* to use the parent test's window.
*/
private class TestSuiteWrapper
{
/**
* The <code>TestCase</code> to be ran inside of the parent <code>TestCase</code>.
*/
private final TestCase testCase;
/**
* Creates a new <code>TestSuiteWrapper</code>.
*
* @param testCase The <code>TestCase</code> to be ran inside of the
* parent <code>TestCase</code>
*/
private TestSuiteWrapper(TestCase testCase)
{
super();
this.testCase = testCase;
}
/**
* Creates the list of tests to be ran from the given test class. This
* method retrieves all the public test methods at the exception of
* {@link #testUI()}since it has to be run inside of another
* <code>AbstractPanelTest</code>, in that case, {@link #_subTestUI()}
* will be used instead.
*
* @param theClass The class used to retrieve the name of the test methods
* @return The list of names of the test to be run
* @throws Throwable If any introspection calls on a Class failed
*/
private List createTestNames(Class theClass) throws Throwable
{
Class superClass = theClass;
Vector names = new Vector();
while (Test.class.isAssignableFrom(superClass))
{
Method[] methods = superClass.getDeclaredMethods();
for (int index = 0; index < methods.length; index++)
{
Method method = methods[index];
String name = method.getName();
if (names.contains(name))
continue;
// testUI is run inside of another test by _subTestUI()
if (name.equals("_subTestUI"))
{
names.add(name);
}
// Signature: public void testX()
else if (!name.equals("testUI") &&
isPublicTestMethod(method))
{
names.add(name);
}
}
superClass = superClass.getSuperclass();
}
return names;
}
/**
* Determines
*
* @param method
* @return
*/
private boolean isPublicTestMethod(Method method)
{
return isTestMethod(method) &&
Modifier.isPublic(method.getModifiers());
}
/**
* Determines
*
* @param m
* @return
*/
private boolean isTestMethod(Method method)
{
String name = method.getName();
Class[] parameters = method.getParameterTypes();
Class returnType = method.getReturnType();
return parameters.length == 0 &&
name.startsWith("test") &&
returnType.equals(Void.TYPE);
}
/**
* Runs
*
* @throws Throwable
*/
void runBare() throws Throwable
{
Class testClass = this.testCase.getClass();
List tests = createTestNames(testClass);
for (Iterator iter = tests.iterator(); iter.hasNext();)
{
TestCaseWrapper wrapper = new TestCaseWrapper(this.testCase, (String) iter.next());
wrapper.runBare();
}
}
}
}