1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.testutils.mockers;
|
---|
3 |
|
---|
4 | import static org.junit.jupiter.api.Assertions.fail;
|
---|
5 |
|
---|
6 | import java.awt.Component;
|
---|
7 | import java.util.Arrays;
|
---|
8 | import java.util.Map;
|
---|
9 | import java.util.OptionalInt;
|
---|
10 | import java.util.stream.IntStream;
|
---|
11 |
|
---|
12 | import javax.swing.Icon;
|
---|
13 | import javax.swing.JOptionPane;
|
---|
14 |
|
---|
15 | import org.openstreetmap.josm.gui.HelpAwareOptionPane;
|
---|
16 | import org.openstreetmap.josm.tools.Logging;
|
---|
17 |
|
---|
18 | import mockit.Mock;
|
---|
19 |
|
---|
20 | /**
|
---|
21 | * MockUp for {@link HelpAwareOptionPane} allowing a test to pre-seed uses of
|
---|
22 | * {@link HelpAwareOptionPane} with mock "responses". This works best with
|
---|
23 | * calls to {@link HelpAwareOptionPane#showOptionDialog(Component, Object, String, int, Icon,
|
---|
24 | * HelpAwareOptionPane.ButtonSpec[], HelpAwareOptionPane.ButtonSpec, String)} which use simple string-based
|
---|
25 | * {@code msg} parameters. In such a case, responses can be defined through a mapping from content
|
---|
26 | * {@link String}s to button indexes ({@link Integer}s) or button labels ({@link String}s). Example:
|
---|
27 | *
|
---|
28 | * <pre>
|
---|
29 | * new HelpAwareOptionPaneMocker(ImmutableMap.<String, Object>builder()
|
---|
30 | * .put("Do you want to save foo bar?", 2)
|
---|
31 | * .put("Please restart JOSM to activate the downloaded plugins.", "OK")
|
---|
32 | * .build()
|
---|
33 | * );
|
---|
34 | * </pre>
|
---|
35 | *
|
---|
36 | * Testing examples with more complicated contents would require overriding
|
---|
37 | * {@link #getStringFromMessage(Object)} or even {@link #getMockResultForMessage(Object)} with custom
|
---|
38 | * logic. The class is implemented as a number of small methods with the main aim being to allow overriding
|
---|
39 | * of only the parts necessary for a particular case.
|
---|
40 | *
|
---|
41 | * The default {@link #getMockResultForMessage(Object)} will raise an
|
---|
42 | * {@link AssertionError} on an {@link #showOptionDialog(Component, Object, String,
|
---|
43 | * int, Icon, HelpAwareOptionPane.ButtonSpec[], HelpAwareOptionPane.ButtonSpec, String)}
|
---|
44 | * activation without a matching mapping entry or if the named button doesn't exist.
|
---|
45 | *
|
---|
46 | * The public {@link #getMockResultMap()} method returns the modifiable result map to allow for situations
|
---|
47 | * where the desired result might need to be changed mid-test.
|
---|
48 | */
|
---|
49 | public class HelpAwareOptionPaneMocker extends BaseDialogMockUp<HelpAwareOptionPane> {
|
---|
50 | /**
|
---|
51 | * Construct a {@link HelpAwareOptionPaneMocker} with an empty {@link #mockResultMap}.
|
---|
52 | */
|
---|
53 | public HelpAwareOptionPaneMocker() {
|
---|
54 | this(null);
|
---|
55 | }
|
---|
56 |
|
---|
57 | /**
|
---|
58 | * Construct an {@link HelpAwareOptionPane} with the provided {@link #mockResultMap}.
|
---|
59 | * @param mockResultMap mapping of {@link HelpAwareOptionPane} {@code msg} string to
|
---|
60 | * result button label or integer index.
|
---|
61 | */
|
---|
62 | public HelpAwareOptionPaneMocker(
|
---|
63 | final Map<String, Object> mockResultMap
|
---|
64 | ) {
|
---|
65 | super(mockResultMap);
|
---|
66 | }
|
---|
67 |
|
---|
68 | protected String getStringFromMessage(final Object message) {
|
---|
69 | return message.toString();
|
---|
70 | }
|
---|
71 |
|
---|
72 | protected Object getMockResultForMessage(final Object message) {
|
---|
73 | final String messageString = this.getStringFromMessage(message);
|
---|
74 | if (!this.getMockResultMap().containsKey(messageString)) {
|
---|
75 | fail("Unexpected HelpAwareOptionPane message string: " + messageString);
|
---|
76 | }
|
---|
77 | return this.getMockResultMap().get(messageString);
|
---|
78 | }
|
---|
79 |
|
---|
80 | /**
|
---|
81 | * Target for overriding, similar to {@link #getMockResultForMessage} except with the implication it
|
---|
82 | * will only be invoked once per dialog display, therefore ideal opportunity to perform any mutating
|
---|
83 | * actions, e.g. making a selection on a widget.
|
---|
84 | * @param message message
|
---|
85 | */
|
---|
86 | protected void act(final Object message) {
|
---|
87 | // Override in sub-classes
|
---|
88 | }
|
---|
89 |
|
---|
90 | protected int getButtonPositionFromLabel(
|
---|
91 | final HelpAwareOptionPane.ButtonSpec[] options,
|
---|
92 | final String label
|
---|
93 | ) {
|
---|
94 | if (options == null) {
|
---|
95 | if (!label.equals("OK")) {
|
---|
96 | fail(String.format(
|
---|
97 | "Only valid result for HelpAwareOptionPane with options = null is \"OK\": received %s",
|
---|
98 | label
|
---|
99 | ));
|
---|
100 | }
|
---|
101 | return JOptionPane.OK_OPTION;
|
---|
102 | } else {
|
---|
103 | final OptionalInt optIndex = IntStream.range(0, options.length)
|
---|
104 | .filter(i -> options[i].text.equals(label))
|
---|
105 | .findFirst();
|
---|
106 | if (!optIndex.isPresent()) {
|
---|
107 | fail(String.format(
|
---|
108 | "Unable to find button labeled \"%s\". Instead found %s",
|
---|
109 | label,
|
---|
110 | Arrays.toString(Arrays.stream(options).map((buttonSpec) -> buttonSpec.text).toArray())
|
---|
111 | ));
|
---|
112 | }
|
---|
113 | return optIndex.getAsInt();
|
---|
114 | }
|
---|
115 | }
|
---|
116 |
|
---|
117 | protected Object[] getInvocationLogEntry(
|
---|
118 | final Object msg,
|
---|
119 | final String title,
|
---|
120 | final Integer messageType,
|
---|
121 | final Icon icon,
|
---|
122 | final HelpAwareOptionPane.ButtonSpec[] options,
|
---|
123 | final HelpAwareOptionPane.ButtonSpec defaultOption,
|
---|
124 | final String helpTopic,
|
---|
125 | final Integer mockResult
|
---|
126 | ) {
|
---|
127 | return new Object[] {
|
---|
128 | mockResult,
|
---|
129 | this.getStringFromMessage(msg),
|
---|
130 | title
|
---|
131 | };
|
---|
132 | }
|
---|
133 |
|
---|
134 | @Mock
|
---|
135 | protected int showOptionDialog(
|
---|
136 | final Component parentComponent,
|
---|
137 | final Object msg,
|
---|
138 | final String title,
|
---|
139 | final int messageType,
|
---|
140 | final Icon icon,
|
---|
141 | final HelpAwareOptionPane.ButtonSpec[] options,
|
---|
142 | final HelpAwareOptionPane.ButtonSpec defaultOption,
|
---|
143 | final String helpTopic
|
---|
144 | ) {
|
---|
145 | try {
|
---|
146 | this.act(msg);
|
---|
147 | final Object result = this.getMockResultForMessage(msg);
|
---|
148 |
|
---|
149 | if (result == null) {
|
---|
150 | fail(
|
---|
151 | "Invalid result for HelpAwareOptionPane: null (HelpAwareOptionPane returns"
|
---|
152 | + "JOptionPane.OK_OPTION for closed windows if that was the intent)"
|
---|
153 | );
|
---|
154 | }
|
---|
155 |
|
---|
156 | Integer retval = null;
|
---|
157 | if (result instanceof String) {
|
---|
158 | retval = this.getButtonPositionFromLabel(options, (String) result);
|
---|
159 | } else if (result instanceof Integer) {
|
---|
160 | retval = (Integer) result;
|
---|
161 | } else {
|
---|
162 | throw new IllegalArgumentException(
|
---|
163 | "HelpAwareOptionPane message mapped to unsupported type of Object: " + result
|
---|
164 | );
|
---|
165 | }
|
---|
166 |
|
---|
167 | // check the returned integer for validity
|
---|
168 | if (retval < 0) {
|
---|
169 | fail(String.format(
|
---|
170 | "Invalid result for HelpAwareOptionPane: %s (HelpAwareOptionPane returns "
|
---|
171 | + "JOptionPane.OK_OPTION for closed windows if that was the intent)",
|
---|
172 | retval
|
---|
173 | ));
|
---|
174 | } else if (retval > (options == null ? 0 : options.length-1)) {
|
---|
175 | fail(String.format(
|
---|
176 | "Invalid result for HelpAwareOptionPane: %s (in call with options = %s)",
|
---|
177 | retval,
|
---|
178 | Arrays.asList(options)
|
---|
179 | ));
|
---|
180 | }
|
---|
181 |
|
---|
182 | Logging.info(
|
---|
183 | "{0} answering {1} to HelpAwareOptionPane with message {2}",
|
---|
184 | this.getClass().getName(),
|
---|
185 | retval,
|
---|
186 | this.getStringFromMessage(msg)
|
---|
187 | );
|
---|
188 |
|
---|
189 | this.getInvocationLogInternal().add(this.getInvocationLogEntry(
|
---|
190 | msg,
|
---|
191 | title,
|
---|
192 | messageType,
|
---|
193 | icon,
|
---|
194 | options,
|
---|
195 | defaultOption,
|
---|
196 | helpTopic,
|
---|
197 | retval
|
---|
198 | ));
|
---|
199 |
|
---|
200 | return retval;
|
---|
201 | } catch (AssertionError e) {
|
---|
202 | // in case this exception gets ignored by the calling thread we want to signify this failure
|
---|
203 | // in the invocation log. it's hard to know what to add to the log in these cases as it's
|
---|
204 | // probably unsafe to call getInvocationLogEntry, so add the exception on its own.
|
---|
205 | this.getInvocationLogInternal().add(new Object[] {e});
|
---|
206 | throw e;
|
---|
207 | }
|
---|
208 | }
|
---|
209 | }
|
---|