Search code examples
javaswinguser-interfacetesting

How do I test Swing GUI in headless environment?


Suppose I want to test this simple JButton extension

package app;

import solution.common.utils.MathConverter;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class JDropdownButton
        extends JButton
        implements ActionListener {
    private JPopupMenu popupMenu;

    public JDropdownButton() {
        this(null, null);
    }

    public JDropdownButton(String text, Icon icon) {
        super(text, icon);
        super.addActionListener(this);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (null == popupMenu || null == popupMenu.getSubElements()) {
            return;
        }

        if (!popupMenu.isVisible()) {
            Point p = getLocationOnScreen();
            popupMenu.setLocation(MathConverter.doubleToInt(p.getX()),
                    MathConverter.doubleToInt(p.getY()) + getHeight());
            popupMenu.setVisible(true);
        }
    }

    public void setDropdownMenu(JPopupMenu menu) {
        popupMenu = menu;
        if (popupMenu != null) {
            popupMenu.setInvoker(this);
        }
    }
}

At this point, I'm only interested in testing if clicking the button shows the popup. If I run this

package app;

import org.junit.jupiter.api.Test;

import javax.swing.*;
import java.awt.event.ActionEvent;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class JDropdownButtonTest {
    @Test
    void actionPerformed_showsPopup() {
        JDropdownButton dropdownButton = new JDropdownButton();
        JPopupMenu popupMenu = new JPopupMenu();
        dropdownButton.setDropdownMenu(popupMenu);
//        JFrame frame = new JFrame();
//        frame.add(dropdownButton);
//        frame.setVisible(true);
        assertFalse(popupMenu.isVisible());
        dropdownButton.doClick();
        assertTrue(popupMenu.isVisible());
    }
}

I get

java.awt.IllegalComponentStateException: component must be showing on the screen to determine its location

    at java.awt.Component.getLocationOnScreen_NoTreeLock(Component.java:2062)
    at java.awt.Component.getLocationOnScreen(Component.java:2036)
    at app.JDropdownButton.actionPerformed(JDropdownButton.java:50)

So it has to be displayed. Since Mockito can't tell a real call from a setup call, this will throw the same exception

    @Test
    void actionPerformed_showsPopup() {
        JDropdownButton dropdownButton = spy(JDropdownButton.class);
        given(dropdownButton.getLocationOnScreen()).willReturn(new Point(0,0));
        JPopupMenu popupMenu = new JPopupMenu();
        dropdownButton.setDropdownMenu(popupMenu);
//        JFrame frame = new JFrame();
//        frame.add(dropdownButton);
//        frame.setVisible(true);
        assertFalse(popupMenu.isVisible());
        dropdownButton.doClick();
        assertTrue(popupMenu.isVisible());
    }

I can uncomment those lines, the test's run fine, but then the CI build on the TeamCity server would fail with a "headless exception" as no graphical environment would be available

So I need a robust GUI test that could be run in a headless environment too

How do I do that? Is updating the CI server's Docker image the only solution?


Solution

  • Not really an answer, more like a workaround. The doReturn() structure avoids an actual method call (I guess because when() returns a mock that "swallows" subsequent calls)

        @Test
        void actionPerformed_showsPopup() {
            JDropdownButton dropdownButton = spy(JDropdownButton.class);
            doReturn(new Point(0,0)).when(dropdownButton).getLocationOnScreen();
            JPopupMenu popupMenu = new JPopupMenu();
            dropdownButton.setDropdownMenu(popupMenu);
            assertFalse(popupMenu.isVisible());
            dropdownButton.doClick();
            assertTrue(popupMenu.isVisible());
        }