source: josm/trunk/test/unit/org/openstreetmap/josm/testutils/mockers/JOptionPaneSimpleMocker.java@ 17360

Last change on this file since 17360 was 17275, checked in by Don-vip, 3 years ago

see #16567 - upgrade almost all tests to JUnit 5, except those depending on WiremockRule

See https://github.com/tomakehurst/wiremock/issues/684

  • Property svn:eol-style set to native
File size: 13.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.testutils.mockers;
3
4import static org.junit.jupiter.api.Assertions.fail;
5
6import java.awt.Component;
7import java.util.Arrays;
8import java.util.Map;
9import java.util.WeakHashMap;
10
11import javax.swing.Icon;
12import javax.swing.JOptionPane;
13
14import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil.MessagePanel;
15import org.openstreetmap.josm.tools.Logging;
16
17import mockit.Invocation;
18import mockit.Mock;
19import mockit.MockUp;
20
21/**
22 * MockUp for {@link JOptionPane} allowing a test to pre-seed uses of {@link JOptionPane}'s
23 * {@code showInputDialog(...)}, {@code showMessageDialog(...)} and {@code showConfirmDialog(...)}
24 * with mock "responses". This works best with calls which use simple string-based {@code message}
25 * parameters. In such a case, responses can be defined through a mapping from content {@link String}s
26 * to button integer codes ({@link Integer}s) in the case of {@code showConfirmDialog(...)} calls or
27 * arbitrary Objects ( but probably {@link String}s) in the case of {@code showInputDialog(...)} calls.
28 * {@code showMessageDialog(...)} calls' contents should be mapped to {@link JOptionPane#OK_OPTION}.
29 * Example:
30 *
31 * <pre>
32 * new JOptionPaneSimpleMocker(ImmutableMap.of(
33 * "Number of tags to delete", "17", // a showInputDialog(...) call
34 * "Please select the row to edit.", JOptionPane.OK_OPTION, // a showMessageDialog(...) call
35 * "Do you want to save foo bar?", JOptionPane.CANCEL_OPTION // a showConfirmDialog(...) call
36 * ));
37 * </pre>
38 *
39 * Testing examples with more complicated contents would require overriding
40 * {@link #getStringFromMessage(Object)} or even {@link #getMockResultForMessage(Object)} with custom logic.
41 * The class is implemented as a number of small methods with the main aim being to allow overriding of
42 * only the parts necessary for a particular case.
43 *
44 * The default {@link #getMockResultForMessage(Object)} will raise an
45 * {@link junit.framework.AssertionFailedError} on an activation without a matching mapping entry or if
46 * the mapped result value is invalid for the call.
47 *
48 * The public {@link #getMockResultMap()} method returns the modifiable result map to allow for situations
49 * where the desired result might need to be changed mid-test.
50 *
51 * This class should also work with dialogs shown using
52 * {@link org.openstreetmap.josm.gui.ConditionalOptionPaneUtil}.
53 *
54 * NOTE that this class does NOT handle {@code showOptionDialog(...)} calls or direct {@link JOptionPane}
55 * instantiations. These are probably too flexible to be universally mocked with a "Simple" interface and
56 * are probably best handled with case-specific mockers.
57 */
58public class JOptionPaneSimpleMocker extends BaseDialogMockUp<JOptionPane> {
59
60 private boolean isPermittedResult(int option, int value) {
61 if (option == JOptionPane.YES_NO_OPTION) {
62 return value == JOptionPane.YES_OPTION || value == JOptionPane.NO_OPTION || value == JOptionPane.CLOSED_OPTION;
63 } else if (option == JOptionPane.YES_NO_CANCEL_OPTION) {
64 return value == JOptionPane.YES_OPTION || value == JOptionPane.NO_OPTION
65 || value == JOptionPane.CANCEL_OPTION || value == JOptionPane.CLOSED_OPTION;
66 } else if (option == JOptionPane.OK_CANCEL_OPTION) {
67 return value == JOptionPane.OK_OPTION || value == JOptionPane.CANCEL_OPTION || value == JOptionPane.CLOSED_OPTION;
68 } else if (option == JOptionPane.DEFAULT_OPTION) {
69 // it's hard to know much about DEFAULT_OPTION, so we can't really police anything here, so
70 // including all known options
71 return value == JOptionPane.OK_OPTION || value == JOptionPane.CANCEL_OPTION || value == JOptionPane.CLOSED_OPTION
72 || value == JOptionPane.YES_OPTION || value == JOptionPane.NO_OPTION;
73 } else {
74 return false;
75 }
76 }
77
78 protected final MessagePanelMocker messagePanelMocker;
79
80 /**
81 * Construct a {@link JOptionPaneSimpleMocker} with an empty {@link #mockResultMap}.
82 */
83 public JOptionPaneSimpleMocker() {
84 this(null);
85 }
86
87 /**
88 * Construct an {@link JOptionPaneSimpleMocker} with the provided {@link #mockResultMap} and a
89 * default {@link MessagePanelMocker}.
90 * @param mockResultMap mapping of {@link JOptionPaneSimpleMocker} {@code message} string to
91 * result Object.
92 */
93 public JOptionPaneSimpleMocker(
94 final Map<String, Object> mockResultMap
95 ) {
96 this(mockResultMap, null);
97 }
98
99 /**
100 * Construct an {@link JOptionPaneSimpleMocker} with the provided {@link #mockResultMap} and the
101 * provided {@link MessagePanelMocker} instance.
102 * @param mockResultMap mapping of {@link JOptionPaneSimpleMocker} {@code message} string to
103 * result Object.
104 * @param messagePanelMocker {@link MessagePanelMocker} instace to use for {@link org.openstreetmap.josm.gui.ConditionalOptionPaneUtil}
105 * message-string retrieval.
106 */
107 public JOptionPaneSimpleMocker(
108 final Map<String, Object> mockResultMap,
109 final MessagePanelMocker messagePanelMocker
110 ) {
111 super(mockResultMap);
112 this.messagePanelMocker = messagePanelMocker != null ? messagePanelMocker : new MessagePanelMocker();
113 }
114
115 protected String getStringFromOriginalMessage(final Object originalMessage) {
116 return originalMessage.toString();
117 }
118
119 protected String getStringFromMessage(final Object message) {
120 final Object originalMessage = message instanceof MessagePanel ?
121 this.messagePanelMocker.getOriginalMessage((MessagePanel) message) : message;
122 return this.getStringFromOriginalMessage(originalMessage);
123 }
124
125 protected Object getMockResultForMessage(final Object message) {
126 final String messageString = this.getStringFromMessage(message);
127 if (!this.getMockResultMap().containsKey(messageString)) {
128 fail("Unexpected JOptionPane message string: " + messageString);
129 }
130 return this.getMockResultMap().get(messageString);
131 }
132
133 /**
134 * Target for overriding, similar to {@link #getMockResultForMessage} except with the implication it
135 * will only be invoked once per dialog display, therefore ideal opportunity to perform any mutating
136 * actions, e.g. making a selection on a widget.
137 * @param message message
138 */
139 protected void act(final Object message) {
140 // Override in sub-classes
141 }
142
143 protected Object[] getInvocationLogEntry(
144 final Object message,
145 final String title,
146 final Integer optionType,
147 final Integer messageType,
148 final Icon icon,
149 final Object[] selectionValues,
150 final Object initialSelectionValue,
151 final Object mockResult
152 ) {
153 return new Object[] {
154 mockResult,
155 this.getStringFromMessage(message),
156 title
157 };
158 }
159
160 @Mock
161 protected Object showInputDialog(
162 final Component parentComponent,
163 final Object message,
164 final String title,
165 final int messageType,
166 final Icon icon,
167 final Object[] selectionValues,
168 final Object initialSelectionValue
169 ) {
170 try {
171 this.act(message);
172 final Object result = this.getMockResultForMessage(message);
173 if (selectionValues == null) {
174 if (!(result instanceof String)) {
175 fail(String.format(
176 "Only valid result type for showInputDialog with null selectionValues is String: received %s",
177 result
178 ));
179 }
180 } else {
181 if (!Arrays.asList(selectionValues).contains(result)) {
182 fail(String.format(
183 "Result for showInputDialog not present in selectionValues: %s",
184 result
185 ));
186 }
187 }
188
189 Logging.info(
190 "{0} answering {1} to showInputDialog with message {2}",
191 this.getClass().getName(),
192 result,
193 this.getStringFromMessage(message)
194 );
195
196 this.getInvocationLogInternal().add(this.getInvocationLogEntry(
197 message,
198 title,
199 null,
200 messageType,
201 icon,
202 selectionValues,
203 initialSelectionValue,
204 result
205 ));
206
207 return result;
208 } catch (AssertionError e) {
209 // in case this exception gets ignored by the calling thread we want to signify this failure
210 // in the invocation log. it's hard to know what to add to the log in these cases as it's
211 // probably unsafe to call getInvocationLogEntry, so add the exception on its own.
212 this.getInvocationLogInternal().add(new Object[] {e});
213 throw e;
214 }
215 }
216
217 @Mock
218 protected void showMessageDialog(
219 final Component parentComponent,
220 final Object message,
221 final String title,
222 final int messageType,
223 final Icon icon
224 ) {
225 try {
226 this.act(message);
227 // why look up a "result" for a message dialog which can only have one possible result? it's
228 // a good opportunity to assert its contents
229 final Object result = this.getMockResultForMessage(message);
230 if (!(result instanceof Integer && (int) result == JOptionPane.OK_OPTION)) {
231 fail(String.format(
232 "Only valid result for showMessageDialog is %d: received %s",
233 JOptionPane.OK_OPTION,
234 result
235 ));
236 }
237
238 Logging.info(
239 "{0} answering {1} to showMessageDialog with message {2}",
240 this.getClass().getName(),
241 result,
242 this.getStringFromMessage(message)
243 );
244
245 this.getInvocationLogInternal().add(this.getInvocationLogEntry(
246 message,
247 title,
248 null,
249 messageType,
250 icon,
251 null,
252 null,
253 JOptionPane.OK_OPTION
254 ));
255 } catch (AssertionError e) {
256 // in case this exception gets ignored by the calling thread we want to signify this failure
257 // in the invocation log. it's hard to know what to add to the log in these cases as it's
258 // probably unsafe to call getInvocationLogEntry, so add the exception on its own.
259 this.getInvocationLogInternal().add(new Object[] {e});
260 throw e;
261 }
262 }
263
264 @Mock
265 protected int showConfirmDialog(
266 final Component parentComponent,
267 final Object message,
268 final String title,
269 final int optionType,
270 final int messageType,
271 final Icon icon
272 ) {
273 try {
274 this.act(message);
275 final Object result = this.getMockResultForMessage(message);
276 if (!(result instanceof Integer && isPermittedResult(optionType, (int) result))) {
277 fail(String.format(
278 "Invalid result for showConfirmDialog with optionType %d: %s",
279 optionType,
280 result
281 ));
282 }
283
284 Logging.info(
285 "{0} answering {1} to showConfirmDialog with message {2}",
286 this.getClass().getName(),
287 result,
288 this.getStringFromMessage(message)
289 );
290
291 this.getInvocationLogInternal().add(this.getInvocationLogEntry(
292 message,
293 title,
294 optionType,
295 messageType,
296 icon,
297 null,
298 null,
299 result
300 ));
301
302 return (int) result;
303 } catch (AssertionError e) {
304 // in case this exception gets ignored by the calling thread we want to signify this failure
305 // in the invocation log. it's hard to know what to add to the log in these cases as it's
306 // probably unsafe to call getInvocationLogEntry, so add the exception on its own.
307 this.getInvocationLogInternal().add(new Object[] {e});
308 throw e;
309 }
310 }
311
312 /**
313 * MockUp for {@link MessagePanel} to allow mocking to work with ConditionalOptionPaneUtil dialogs
314 */
315 public static class MessagePanelMocker extends MockUp<MessagePanel> {
316 protected final Map<MessagePanel, Object> originalMessageMemo = new WeakHashMap<>();
317
318 @Mock
319 private void $init(
320 final Invocation invocation,
321 final Object message,
322 final boolean displayImmediateOption
323 ) {
324 this.originalMessageMemo.put(
325 (MessagePanel) invocation.getInvokedInstance(),
326 message
327 );
328 invocation.proceed();
329 }
330
331 /**
332 * Returns the original message.
333 * @param instance message panel
334 * @return the original message
335 */
336 public Object getOriginalMessage(final MessagePanel instance) {
337 return this.originalMessageMemo.get(instance);
338 }
339
340 /* TODO also allow mocking of getNotShowAgain() */
341 }
342}
Note: See TracBrowser for help on using the repository browser.