Cartesian Plane Lesson 16 Page 10: Testing the Line Properties Panel GUI, LPPTestDialog Utility

LinePropertiesPanel, GUI Testing

On this page we will begin to develop tests for the LinePropertiesPanel class.

Testing the LinePropertiesPanel gets kind of tedious. There are a lot of GUI components to keep track, lots of logic deferred to the Event Dispatch Thread, and some involved assertions. In an effort to simplify things, I decided to make a utility class just to encapsulate and interact with the GUI: class LPPTestDialog. While this class will contain some assertions, mainly related to identifying GUI components, most of the assertions will reside in the main JUnit test classes. We’ll start by describing the utility class; the actual JUnit tests will begin on the next page.

GitHub repository: Cartesian Plane Part 16

Previous lesson: Cartesian Plane Lesson 16 Page 9: Completing the LinePropertiesPanel

The LPPTestDialog Class

The LPPTestDialog class is a subclass of JDialog. It’s also implemented as a singleton, so it has a private constructor and a class method to return the class’s sole instance. It lives in a special test package, com.acmemail.judah.cartesian_plane.test_utils.lp_plane. All of its fields are given over to tracking its singleton, and the components of the LinePropertiesPanel; here’s a list, followed by some notes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class LPPTestDialog extends JDialog
{
    private static LPPTestDialog        dialog          = null;
    
    private final LinePropertiesPanel   propertiesPanel = 
        new LinePropertiesPanel();
    private final List<PRadioButton<LinePropertySet>>      
        radioButtons;
    private final JPanel                propCompPanel;
    private final JSpinner              strokeSpinner;
    private final SpinnerNumberModel    strokeModel;
    private final JLabel                strokeLabel;
    private final JSpinner              lengthSpinner;
    private final SpinnerNumberModel    lengthModel;
    private final JLabel                lengthLabel;
    private final JSpinner              spacingSpinner;
    private final SpinnerNumberModel    spacingModel;
    private final JLabel                spacingLabel;
    private final JButton               colorButton;
    private final JTextField            colorField;
    private final JCheckBox             drawCheckBox;
    
    private final JButton               applyButton;
    private final JButton               resetButton;
    private final JButton               closeButton;
    
    // These are volatile variables for use in lambdas. They are used
    // in method return statements, after which their values are no
    // longer predictable.
    private boolean     tempBoolean;
    private CompConfig  tempConfig;
    private Object      tempObj;
    // ...
}
  • Line 3: This class’s singleton.
  • Lines 5,6: The LinePropertiesPanel, the subject of our test and the LPPTestDialog’s content pane.
  • Lines 7,8: A list of the radio buttons on the left side of the LinePropertiesPanel.
  • Line 9: The panel that contains the components directly related to displaying and modifying property values. This includes the spinners, the color components and the draw check box; it does not include the radio buttons, or the Apply, Reset or Close buttons. In class LinePropertiesPanel it is implemented as the nested class PropertiesPanel.
  • Lines 10-18: The spinners, spinner models and accompanying labels associated with the stroke, length and spacing properties.
  • Lines 19,20: The button and text field associated with the color property.
  • Line 21: The check box associated with the draw property.
  • Lines 23-25: The Apply, Reset and Close buttons.
  • Lines 30-32: Temporary variables for the convenience of methods that use lambdas to obtain and return a value. Recall that a local variable cannot be assigned a value in a lambda (or any anonymous class). These fields are used as temporary substitutes; as soon as the value of one of these fields is returned, the field no longer has a predictable state. An example of their use is in the isSelected(AbstractButton button) method, shown below.
  • Line 31: See description of the inner class CompConfig, below.
public boolean isSelected( AbstractButton button )
{
    if ( SwingUtilities.isEventDispatchThread() )
        tempBoolean = button.isSelected();
    else
        GUIUtils.schedEDTAndWait( 
            () -> tempBoolean = button.isSelected()
        );
    return tempBoolean;
}

To initialize the fields we have a variety of helper methods. One is the method getPropCompPanel. This method locates the PropertiesPanel (field propCompPanel) which organizes the right hand side of the LinePropertiesPanel. Our GUI consists of lots of panels, and the one we want is the one that contains the spinners. So this method finds a JSpinner then gets its parent. The annotated code follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private JPanel getPropCompPanel()
{
    Predicate<JComponent>   pred    = c -> (c instanceof JSpinner);
    JComponent              comp    =
        ComponentFinder.find( propertiesPanel, pred );
    
    assertNotNull( comp );
    assertTrue( comp instanceof JSpinner );
    Container               parent  = comp.getParent();
    assertNotNull( parent );
    assertTrue( parent instanceof JPanel );
    return (JPanel)parent;
}
  • Line 3: Predicate asserting that a component is a JSpinner.
  • Lines 4,5: Gets the first component in the LinePropertiesPanel that satisfies the predicate.
  • Lines 7,8: Sanity check; verifies that the operation worked as expected.
  • Line 9: Gets the parent container of the JSpinner.
  • Lines 10,11: Sanity check; verifies that the parent exists and is the expected type.

Note: We’re going to have to get even more creative looking for components that can’t easily be found with ComponentFinder. We might consider giving the prominent components unique names, then we could just use ComponentFinder to locate the component with a given name.

From the GUI’s point of view, it’s hard to distinguish between the stroke, length and spacing spinners, and there’s no way to ask ComponentFinder to “find and return all the JSpinners.” So instead we traverse the propCompPanel’s list of components, and identify anything that’s a JSpinner. When we’re finished we have a list of JSpinners, and we assume the first is for control of the stroke property, the second for control of the length property, and the third for control of the spacing property. The method that does this is parseSpinners; here’s the annotated code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private List<JSpinner> parseSpinners()
{
    List<JSpinner>  allSpinners   = 
        Stream.of( propCompPanel.getComponents() )
            .filter( c -> (c instanceof JSpinner) )
            .map( c -> (JSpinner)c )
            .toList();
    assertEquals( 3, allSpinners.size() );
    return allSpinners;
}
  • Line 4: Streams all the component children of the PropertiesPanel; note that getComponents returns an array of type Component.
  • Line 5: Discards any component that is not a JSpinner.
  • Line 6: Casts the component to JSpinner.
  • Line 7: Puts all the spinners in a list, which is assigned to allSpinners at line 3.
  • Line 8: Sanity check; if everything is working properly, we should have found exactly three spinners.

The helper method parseJButton( String text ) finds the JButton with the given text. We’ve seen this method before. It utilizes a ComponentFinder class method to get a predicate to identify the target button. Lines 7 and 8 constitute a sanity check to verify that the operation worked. Here’s the code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private JButton parseJButton( String text )
{
    Predicate<JComponent>   pred    = 
        ComponentFinder.getButtonPredicate( text );
    JComponent              comp    =
        ComponentFinder.find( propertiesPanel , pred );
    assertNotNull( comp );
    assertTrue( comp instanceof JButton, text );
    return (JButton)comp;
}

Method parseRButton( String text ) is used to find a PRadioButton<LinePropertySet> with the given text. It’s very similar to ParseJButton, but there’s no ComponentFinder utility to get an appropriate predicate, so we have to write our own; we’re looking for a component that’s a PRadioButton and has the given text:
    Predicate isButton =
        c -> (c instanceof PRadioButton<?>);
    Predicate hasText =
        c -> ((PRadioButton<?>)c).getText().equals( text );
    Predicate pred =
        isButton.and( hasText );

Note 1: Recall that the and in isButton.and( hasText ) is a default method in the Predicate interface. It forms the logical-and (&&) of isButton and hasText.

Note 2: The code for this method makes use of PRadioButton<?> when what we would really like to have is PRadioButton<LinePropertySet>. This is because of type erasure, an issue we encountered previously.

The complete code for the method follows below. The assertions at lines 11, 12 and 14 are sanity checks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
private PRadioButton<LinePropertySet> parseRButton( String text )
{
    Predicate<JComponent>   isButton    =
        c -> (c instanceof PRadioButton<?>);
    Predicate<JComponent>   hasText     =
        c -> ((PRadioButton<?>)c).getText().equals( text );
    Predicate<JComponent>   pred        =
        isButton.and( hasText );
    JComponent  comp    =
        ComponentFinder.find( propertiesPanel, pred );
    assertNotNull( comp );
    assertTrue( comp instanceof PRadioButton<?> );
    PRadioButton<?> testButton  = (PRadioButton<?>)comp;
    assertTrue( testButton.get() instanceof LinePropertySet );
    PRadioButton<LinePropertySet>   button  =
        (PRadioButton<LinePropertySet>)testButton;
    return button;
}

The helper method parseRButtons() just calls parseRButton( String text ) four times, passing the labels for each of the radio buttons on the left side of the LinePropertiesPanel. Ultimately it returns a list containing all the buttons. It looks like this:

1
2
3
4
5
6
7
8
private List<PRadioButton<LinePropertySet>> parseRButtons()
{
    List<PRadioButton<LinePropertySet>> buttons =
        Stream.of( "Axes", "Major Tics", "Minor Tics", "Grid Lines" )
            .map( this::parseRButton )
            .toList();
    return buttons;
}

Method parseJLabel( String text ) is little different from parseJButton and parseRButton. You can find the code in the GitHub repository.

Finding the JTextField associated with the color property is another tricky task. The JSpinners all incorporate JTextFields so we can’t just ask ComponentFinder for the first JTextField that it finds. Instead we will look for the JTextField that is a direct child of the PropertiesPanel (field propCompPanel). You can find the code immediately below. Note that we are using the findFirst method of the Stream class; recall that this method returns an Optional, so findFirst is immediately chained to the orElse method of the Optional class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private JTextField parseColorField()
{
    JTextField  textField   = 
        Stream.of( propCompPanel.getComponents() )
            .filter( c -> (c instanceof JTextField) )
            .map( c -> (JTextField)c )
            .findFirst().orElse( null );
    assertNotNull( textField );
    return textField;
}

Inner Class CompConfig

CompConfig is a simple class that encapsulates the current configuration of the components on the right hand side of the LinePropertiesPanel. When the main test code wants to validate the state of these components it will call the getAllProperties method which will return a CompConfig object. Note that this is a public class with a private constructor; the only way to get an instance of this class is to call the getAllProperties method. This method is guaranteed to invoke the constructor only on the EDT, so the constructor itself doesn’t have to worry about what thread it’s running on.

The encapsulated data includes the enabled states of all the significant fields, and the values of the spinners, color text field and the draw check box. The class consists of nothing but final public fields, and a constructor that initializes them. A sample of the code in this class follows below; the complete code can be found in the GitHub repository.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CompConfig
{
    public final boolean    strokeSpinnerEnabled;
    public final boolean    strokeLabelEnabled;
    public final boolean    lengthSpinnerEnabled;
    public final boolean    lengthLabelEnabled;
    // ...    
    public final float      stroke;
    public final float      length;
    // ...
    private CompConfig()
    {
        strokeSpinnerEnabled = strokeSpinner.isEnabled();
        strokeLabelEnabled = strokeLabel.isEnabled();
        lengthSpinnerEnabled = lengthSpinner.isEnabled();
        lengthLabelEnabled = lengthLabel.isEnabled();
        // ...        
        stroke = strokeModel.getNumber().floatValue();
        length = lengthModel.getNumber().floatValue();
        // ...
    }
}

Other Helper Methods

The LPPTestDialog class has three helper methods in addition to the ones that are used to initialize the class’s fields. One of them, synchRightEDT, is purely for the convenience of the public method synchRight, and we will discuss it later. The remaining two, colorValue and setColor, are for dealing with the color text field. The code is shown below. Note that setColor goes out of its way to ensure that it is executing on the the EDT; also, after setting the value of the color text field, it activates the text field’s action listeners, which then updates the associated feedback component.

private static Color colorValue( String strColor )
{
    Color   color   = null;
    try
    {
        int intColor    = Integer.decode( strColor ) & 0xFFFFFF;
        color = new Color( intColor );
    }
    catch ( NumberFormatException exc )
    {
    }
    
    return color;
}

private void setColor( Color color )
{
    int     rgb     = color.getRGB() & 0xffffff;
    String  strRGB  = String.format( "0x%06x", rgb );
    if ( SwingUtilities.isEventDispatchThread() )
    {
        colorField.setText( strRGB );
        colorField.postActionEvent();
    }
    else
        GUIUtils.schedEDTAndWait( () -> {
            colorField.setText( strRGB );
            colorField.postActionEvent();
        });
}

Instantiation

As previously noted, LPPTestDialog is implemented as a singleton. Its constructor is private, and the only way to get the sole instance is by calling the public class method getDialog. This method assumes that it is not being executed on the EDT; it looks like this:

public static LPPTestDialog getDialog()
{
    if ( dialog == null )
        GUIUtils.schedEDTAndWait( () ->
            dialog = new LPPTestDialog()
        );
    return dialog;
}

The LPPTestDialog constructor expends most of its energy locating the various components of the LinePropertiesDialog and initializing the object’s fields. Only the last three lines have anything to do with composing the actual GUI. Here’s the code for the constructor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private LPPTestDialog()
{
    propCompPanel = getPropCompPanel();
    radioButtons = parseRButtons();
    
    List<JSpinner>  allSpinners = parseSpinners();
    strokeSpinner = allSpinners.get( 0 );
    strokeModel = (SpinnerNumberModel)strokeSpinner.getModel();
    strokeLabel = parseJLabel( "Stroke" );
    lengthSpinner = allSpinners.get( 1 );
    lengthModel = (SpinnerNumberModel)lengthSpinner.getModel();
    lengthLabel = parseJLabel( "Length" );
    spacingSpinner = allSpinners.get( 2 );
    spacingModel = (SpinnerNumberModel)spacingSpinner.getModel();
    spacingLabel = parseJLabel( "Spacing" );
    
    colorButton = parseJButton( "Color" );
    colorField = parseColorField();
    drawCheckBox = parseJCheckBox();
    
    applyButton = parseJButton( "Apply" );
    resetButton = parseJButton( "Reset" );
    closeButton = parseJButton( "Close" );
    
    setTitle( "Line Properties Panel Test Dialog" );
    setContentPane( propertiesPanel );
    pack();
}

Public Methods

apply(), reset(), close(), doClick(AbstractButton button)
The LPPTestDialog has three public methods for clicking the control buttons, apply(), reset() and close(). It has one method, doClick(AbstractButton), that can be used to click any button. The doClick method ensures that it is executed on the EDT. Here’s the code for the apply and doClick methods; the complete code can be found in the GitHub repository.

public void apply()
{
    doClick( applyButton );
}
public void doClick( AbstractButton button )
{
    if ( SwingUtilities.isEventDispatchThread() )
        button.doClick();
    else
        GUIUtils.schedEDTAndWait( () -> button.doClick() ); 
}

setDialogVisible( boolean visible ), isDialogVisible()
We have methods for setting and for testing the visibility of the LPPTestDialog. The code for these methods follows. Note that the methods goes out of their way to make sure all the sensitive work is performed on the EDT.

public void setDialogVisible( boolean visible )
{
    if ( SwingUtilities.isEventDispatchThread() )
        setVisible( visible );
    else
        GUIUtils.schedEDTAndWait( 
            () -> setVisible( visible )
        );
}

public boolean isDialogVisible()
{
    if ( SwingUtilities.isEventDispatchThread() )
        tempBoolean = isVisible();
    else
        GUIUtils.schedEDTAndWait( 
            () -> tempBoolean = isVisible()
        );
    return tempBoolean;
}

Digression: My first thought was not to write a method named isDialogVisisble(), but to override isVisible() in the dialog class:

@Override
public boolean isVisible()
{
    if ( SwingUtilities.isEventDispatchThread() )
        tempBoolean = super.isVisible();
    else
        GUIUtils.schedEDTAndWait(
            () -> tempBoolean = super.isVisible()
        );
}

It turns out, however, that pack() invokes isVisible() multiple times, and, at least sometimes, on some versions of Windows, the first time it’s called isEventDispatchThread() returns false, and GUIUtils.schedEDTAndWait(…) crashes with an InvocationTargetException. (I’ve been advised that this does not occur on MacOS).

The work around was to give the method a unique name, isDialogVisible. For symmetry, rather than override setVisible, I decided on a unique method name for that as well, setDialogVisible.

synchRight(LinePropertySet set)
Method synchRight, and its private helper method synchRightEDT, are called by a JUnitTest method when it wants to change the values reflected by the components on the right hand side of the LinePropertiesDialog without changing the LinePropertySet in the corresponding radio button. For example:

  1. Select a radio button.
  2. Verify that the values of the associated LinePropertySet are reflected in the components on the right hand side of the LinePropertiesPanel.
  3. Assign new values to the components on the right hand side of the LinePropertiesPanel.
  4. Push the Apply button.
  5. Verify that the new values of the components on the right hand side of the LinePropertiesPanel are copied into the radio button’s LinePropertySet.
  6. Verify that the new values of the LinePropertySet are used to update the PropertyManager.

Note that the public synchRight method assures that the private synchRightEDT method is called on the EDT, then the private method does all the heavy lifting. Here’s the code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void synchRight( LinePropertySet set )
{
    if ( SwingUtilities.isEventDispatchThread() )
        synchRightEDT( set );
    else
        GUIUtils.schedEDTAndWait( () -> synchRightEDT( set ) );
}

private void synchRightEDT( LinePropertySet set )
{
    if ( set.hasStroke() )
        strokeSpinner.setValue( set.getStroke() );
    if ( set.hasLength() )
        lengthSpinner.setValue( set.getLength() );
    if ( set.hasSpacing() )
        spacingSpinner.setValue( set.getSpacing() );
    if ( set.hasColor() )
        setColor( set.getColor() );
    if ( set.hasDraw() )
        drawCheckBox.setSelected( set.getDraw() );
}

getAllProperties()
The getAllProperties method is called when a JUnitTest method wants to examine the values of the components on the right hand side of the LinePropertiesPanel. For example:

  1. Select a radio button.
  2. Get the values of the components on the right hand side of the LinePropertiesPanel.
  3. Verify the component values match the values of the properties stored in the radio button’s LinePropertySet.

All this method has to do is instantiate and return a CompConfig object; the CompConfig constructor does all the data gathering (see CompConfig, above).

public CompConfig getAllProperties()
{
    if ( SwingUtilities.isEventDispatchThread() )
        tempConfig = new CompConfig();
    else
        GUIUtils.schedEDTAndWait( () -> 
            tempConfig = new CompConfig()
        );
    
    return tempConfig;
}

isSelected(AbstractButton button), getRadioButtons()
The isSelected method returns true if a given button is selected. It makes sure that the testing is performed on the EDT; we already saw this method, above. Method getRadioButtons returns the list of radio buttons (field radioButtons). It looks like this:

public List<PRadioButton<LinePropertySet>> getRadioButtons()
{
    return radioButtons;
}

Summary

On this page we began testing of the LinePropertiesPanel by creating a utility class, LPPTestDialog, to manage interaction with GUI components. It performs all the necessary GUI hierarchy traversal, and any other tasks related to the EDT. This will allow us to limit our actual JUnit test classes to the major testing issues, such as verifying that values in a particular LineProperySet object (from the left side of the LinePropertiesPanel) agree with the component values on the right side of the panel.

On the next page we will write the JUnit test for validating the functional performance of the LinePropertiesPanel.

    Next: Functional Testing of the LinePropertiesPanel