source: josm/trunk/test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java@ 14332

Last change on this file since 14332 was 14332, checked in by Don-vip, 6 years ago

fix #16841 - MergeLayerActionTest, DownloadWmsAlongTrackActionTest: fix for non-headless mode by properly mocking dialogs (patch by ris)

  • Property svn:eol-style set to native
File size: 8.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.testutils.mockers;
3
4import static org.junit.Assert.fail;
5
6import java.awt.Component;
7import java.awt.GraphicsEnvironment;
8import java.util.Arrays;
9import java.util.Map;
10import java.util.Optional;
11import java.util.WeakHashMap;
12
13import org.openstreetmap.josm.TestUtils;
14import org.openstreetmap.josm.gui.ExtendedDialog;
15import org.openstreetmap.josm.tools.Logging;
16
17import mockit.Deencapsulation;
18import mockit.Invocation;
19import mockit.Mock;
20
21/**
22 * MockUp for {@link ExtendedDialog} allowing a test to pre-seed uses of {@link ExtendedDialog}
23 * with mock "responses". This works best with {@link ExtendedDialog}s which have their contents set
24 * through {@link ExtendedDialog#setContent(String)} as simple strings. In such a case, responses can
25 * be defined through a mapping from content {@link String}s to button indexes ({@link Integer}s) or
26 * button labels ({@link String}s). Example:
27 *
28 * <pre>
29 * new ExtendedDialogMocker(ImmutableMap.&lt;String, Object&gt;builder()
30 * .put("JOSM version 8,001 required for plugin baz_plugin.", "Download Plugin")
31 * .put("JOSM version 7,001 required for plugin dummy_plugin.", "Cancel")
32 * .put("Are you sure you want to do foo bar?", ExtendedDialog.DialogClosedOtherwise)
33 * .build()
34 * );
35 * </pre>
36 *
37 * Testing examples with more complicated contents would require overriding
38 * {@link #getString(ExtendedDialog)} or even {@link #getMockResult(ExtendedDialog)} with custom logic.
39 * The class is implemented as a number of small methods with the main aim being to allow overriding of
40 * only the parts necessary for a particular case.
41 *
42 * The default {@link #getMockResult(ExtendedDialog)} will raise an
43 * {@link AssertionError} on an {@link ExtendedDialog} activation without a
44 * 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 */
49public class ExtendedDialogMocker extends BaseDialogMockUp<ExtendedDialog> {
50 /**
51 * Because we're unable to add fields to the mocked class, we need to use this external global
52 * mapping to be able to keep a note of the most recently set simple String contents of each
53 * {@link ExtendedDialog} instance - {@link ExtendedDialog} doesn't store this information
54 * itself, instead converting it directly into the embedded {@link Component}.
55 */
56 protected final Map<ExtendedDialog, String> simpleStringContentMemo = new WeakHashMap<>();
57
58 /**
59 * Construct an {@link ExtendedDialogMocker} with an empty {@link #mockResultMap}.
60 */
61 public ExtendedDialogMocker() {
62 this(null);
63 }
64
65 /**
66 * Construct an {@link ExtendedDialogMocker} with the provided {@link #mockResultMap}.
67 * @param mockResultMap mapping of {@link ExtendedDialog} string contents to
68 * result button label or integer index.
69 */
70 public ExtendedDialogMocker(final Map<String, Object> mockResultMap) {
71 super(mockResultMap);
72 if (GraphicsEnvironment.isHeadless()) {
73 new WindowMocker();
74 }
75 }
76
77 protected int getButtonPositionFromLabel(final ExtendedDialog instance, final String label) {
78 final String[] bTexts = Deencapsulation.getField(instance, "bTexts");
79 final int position = Arrays.asList(bTexts).indexOf(label);
80 if (position == -1) {
81 fail("Unable to find button labeled \"" + label + "\". Instead found: " + Arrays.toString(bTexts));
82 }
83 return position;
84 }
85
86 protected String getString(final ExtendedDialog instance) {
87 return Optional.ofNullable(this.simpleStringContentMemo.get(instance))
88 .orElseGet(() -> instance.toString());
89 }
90
91 protected int getMockResult(final ExtendedDialog instance) {
92 final String stringContent = this.getString(instance);
93 final Object result = this.getMockResultMap().get(stringContent);
94
95 if (result == null) {
96 fail(
97 "Unexpected ExtendedDialog content: " + stringContent
98 );
99 } else if (result instanceof Integer) {
100 return (Integer) result;
101 } else if (result instanceof String) {
102 // buttons are numbered with 1-based indexing
103 return 1 + this.getButtonPositionFromLabel(instance, (String) result);
104 }
105
106 throw new IllegalArgumentException(
107 "ExtendedDialog contents mapped to unsupported type of Object: " + result
108 );
109 }
110
111 /**
112 * Target for overriding, similar to {@link #getMockResult} except with the implication it will only
113 * be invoked once per dialog display, therefore ideal opportunity to perform any mutating actions,
114 * e.g. making a selection on a widget.
115 * @param instance dialog instance
116 */
117 protected void act(final ExtendedDialog instance) {
118 // Override in sub-classes
119 }
120
121 protected Object[] getInvocationLogEntry(final ExtendedDialog instance, final int mockResult) {
122 return new Object[] {
123 mockResult,
124 this.getString(instance),
125 instance.getTitle()
126 };
127 }
128
129 /**
130 * A convenience method to access {@link ExtendedDialog#content} without exception-catching boilerplate
131 * @param instance dialog instance
132 * @return dialog content component
133 */
134 protected Component getContent(final ExtendedDialog instance) {
135 try {
136 return (Component) TestUtils.getPrivateField(instance, "content");
137 } catch (ReflectiveOperationException e) {
138 throw new RuntimeException(e);
139 }
140 }
141
142 @Mock
143 private void setupDialog(final Invocation invocation) {
144 if (!GraphicsEnvironment.isHeadless()) {
145 invocation.proceed();
146 }
147 // else do nothing - WindowMocker-ed Windows doesn't work well enough for some of the
148 // component constructions
149 }
150
151 @Mock
152 private void setVisible(final Invocation invocation, final boolean value) {
153 if (value == true) {
154 try {
155 final ExtendedDialog instance = invocation.getInvokedInstance();
156 this.act(instance);
157 final int mockResult = this.getMockResult(instance);
158 // TODO check validity of mockResult?
159 Deencapsulation.setField(instance, "result", mockResult);
160 Logging.info(
161 "{0} answering {1} to ExtendedDialog with content {2}",
162 this.getClass().getName(),
163 mockResult,
164 this.getString(instance)
165 );
166 this.getInvocationLogInternal().add(this.getInvocationLogEntry(instance, mockResult));
167 } catch (AssertionError e) {
168 // in case this exception gets ignored by the calling thread we want to signify this failure
169 // in the invocation log. it's hard to know what to add to the log in these cases as it's
170 // probably unsafe to call getInvocationLogEntry, so add the exception on its own.
171 this.getInvocationLogInternal().add(new Object[] {e});
172 throw e;
173 }
174 }
175 }
176
177 @Mock
178 private ExtendedDialog setContent(final Invocation invocation, final String message) {
179 final ExtendedDialog retval = invocation.proceed(message);
180 // must set this *after* the regular invocation else that will fall through to
181 // setContent(Component, boolean) which would overwrite it (with null)
182 this.simpleStringContentMemo.put((ExtendedDialog) invocation.getInvokedInstance(), message);
183 return retval;
184 }
185
186 @Mock
187 private ExtendedDialog setContent(final Invocation invocation, final Component content, final boolean placeContentInScrollPane) {
188 this.simpleStringContentMemo.put((ExtendedDialog) invocation.getInvokedInstance(), null);
189 return invocation.proceed(content, placeContentInScrollPane);
190 }
191}
Note: See TracBrowser for help on using the repository browser.