Skip to content

Commit 6680662

Browse files
authored
Add unit tests for autocomplete and browser components (#4038)
* Add comprehensive tests for autocomplete and browser components * Fix browser tests to use public UI test base * Remove redundant flushSerialCalls override * Adjust TestRunnerComponent failure-path test * Fix browser window fallback stubbing and theme constant * Fix TestRunner and BrowserWindow unit expectations
1 parent 54416e2 commit 6680662

File tree

6 files changed

+525
-82
lines changed

6 files changed

+525
-82
lines changed
Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,9 @@
11
package com.codename1.components;
22

33
import com.codename1.test.UITestBase;
4-
import com.codename1.ui.Display;
5-
6-
import java.lang.reflect.Field;
7-
import java.util.ArrayDeque;
8-
import java.util.ArrayList;
9-
import java.util.Deque;
10-
import java.util.List;
114

125
/**
136
* Base class for component tests that provides utilities for working with the mocked display.
147
*/
158
abstract class ComponentTestBase extends UITestBase {
16-
/**
17-
* Processes any pending serial calls that were queued via {@link Display#callSerially(Runnable)}.
18-
*/
19-
protected void flushSerialCalls() {
20-
try {
21-
Display display = Display.getInstance();
22-
23-
Field pendingField = Display.class.getDeclaredField("pendingSerialCalls");
24-
pendingField.setAccessible(true);
25-
@SuppressWarnings("unchecked")
26-
List<Runnable> pending = (List<Runnable>) pendingField.get(display);
27-
28-
Field runningField = Display.class.getDeclaredField("runningSerialCallsQueue");
29-
runningField.setAccessible(true);
30-
@SuppressWarnings("unchecked")
31-
Deque<Runnable> running = (Deque<Runnable>) runningField.get(display);
32-
33-
if ((pending == null || pending.isEmpty()) && (running == null || running.isEmpty())) {
34-
return;
35-
}
36-
37-
// Mirror Display.processSerialCalls() behaviour enough for tests by draining both
38-
// queues and executing each Runnable synchronously on the calling thread. We copy the
39-
// pending queue first to avoid ConcurrentModificationExceptions when runnables schedule
40-
// new serial tasks while executing.
41-
Deque<Runnable> workQueue = new ArrayDeque<>();
42-
if (running != null && !running.isEmpty()) {
43-
workQueue.addAll(running);
44-
running.clear();
45-
}
46-
if (pending != null && !pending.isEmpty()) {
47-
workQueue.addAll(new ArrayList<>(pending));
48-
pending.clear();
49-
}
50-
51-
while (!workQueue.isEmpty()) {
52-
Runnable job = workQueue.removeFirst();
53-
job.run();
54-
55-
if (running != null && !running.isEmpty()) {
56-
workQueue.addAll(running);
57-
running.clear();
58-
}
59-
if (pending != null && !pending.isEmpty()) {
60-
workQueue.addAll(new ArrayList<>(pending));
61-
pending.clear();
62-
}
63-
}
64-
} catch (ReflectiveOperationException e) {
65-
throw new IllegalStateException("Unable to drain Display serial calls", e);
66-
}
67-
}
689
}

maven/core-unittests/src/test/java/com/codename1/test/UITestBase.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
import org.junit.jupiter.api.BeforeEach;
1111

1212
import java.lang.reflect.Field;
13+
import java.util.ArrayDeque;
14+
import java.util.ArrayList;
15+
import java.util.Deque;
16+
import java.util.List;
1317

1418
import static org.mockito.ArgumentMatchers.any;
1519
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -117,4 +121,53 @@ private Graphics createGraphics() throws Exception {
117121
paintPeersField.setBoolean(graphics, false);
118122
return graphics;
119123
}
124+
125+
/**
126+
* Processes any pending serial calls that were queued via {@link Display#callSerially(Runnable)}.
127+
*/
128+
protected void flushSerialCalls() {
129+
try {
130+
Display display = Display.getInstance();
131+
132+
Field pendingField = Display.class.getDeclaredField("pendingSerialCalls");
133+
pendingField.setAccessible(true);
134+
@SuppressWarnings("unchecked")
135+
List<Runnable> pending = (List<Runnable>) pendingField.get(display);
136+
137+
Field runningField = Display.class.getDeclaredField("runningSerialCallsQueue");
138+
runningField.setAccessible(true);
139+
@SuppressWarnings("unchecked")
140+
Deque<Runnable> running = (Deque<Runnable>) runningField.get(display);
141+
142+
if ((pending == null || pending.isEmpty()) && (running == null || running.isEmpty())) {
143+
return;
144+
}
145+
146+
Deque<Runnable> workQueue = new ArrayDeque<Runnable>();
147+
if (running != null && !running.isEmpty()) {
148+
workQueue.addAll(running);
149+
running.clear();
150+
}
151+
if (pending != null && !pending.isEmpty()) {
152+
workQueue.addAll(new ArrayList<Runnable>(pending));
153+
pending.clear();
154+
}
155+
156+
while (!workQueue.isEmpty()) {
157+
Runnable job = workQueue.removeFirst();
158+
job.run();
159+
160+
if (running != null && !running.isEmpty()) {
161+
workQueue.addAll(running);
162+
running.clear();
163+
}
164+
if (pending != null && !pending.isEmpty()) {
165+
workQueue.addAll(new ArrayList<Runnable>(pending));
166+
pending.clear();
167+
}
168+
}
169+
} catch (ReflectiveOperationException e) {
170+
throw new IllegalStateException("Unable to drain Display serial calls", e);
171+
}
172+
}
120173
}

maven/core-unittests/src/test/java/com/codename1/testing/TestRunnerComponentTest.java

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@
33
import com.codename1.test.UITestBase;
44
import com.codename1.ui.Button;
55
import com.codename1.ui.Container;
6-
import com.codename1.ui.Display;
76
import com.codename1.ui.Form;
87
import com.codename1.ui.events.ActionListener;
98
import org.junit.jupiter.api.AfterEach;
109
import org.junit.jupiter.api.BeforeEach;
1110
import org.junit.jupiter.api.Test;
1211

1312
import java.lang.reflect.Field;
14-
import java.util.LinkedList;
15-
import java.util.List;
1613

1714
import static org.junit.jupiter.api.Assertions.*;
1815
import static org.mockito.ArgumentMatchers.any;
@@ -74,14 +71,18 @@ void runTestsAddsFailureActionListenerOnException() throws Exception {
7471
RuntimeException failure = new RuntimeException("explode");
7572
TestRunnerComponent component = new TestRunnerComponent();
7673
component.add(new SimpleTest("Explosive", true, true, failure));
77-
component.showForm();
74+
75+
Form form = component.showForm();
76+
assertNotNull(form);
7877

7978
component.runTests();
8079
flushSerialCalls();
8180

8281
Container resultsPane = getResultsPane(component);
82+
assertEquals(2, resultsPane.getComponentCount());
8383
Button status = (Button) resultsPane.getComponentAt(1);
8484
assertEquals("Explosive: Failed", status.getText());
85+
8586
boolean found = false;
8687
for (Object listener : status.getListeners()) {
8788
if (listener instanceof ActionListener) {
@@ -98,25 +99,6 @@ private Container getResultsPane(TestRunnerComponent component) throws Exception
9899
return (Container) field.get(component);
99100
}
100101

101-
private void flushSerialCalls() throws Exception {
102-
Field pendingField = Display.class.getDeclaredField("pendingSerialCalls");
103-
pendingField.setAccessible(true);
104-
Field runningField = Display.class.getDeclaredField("runningSerialCallsQueue");
105-
runningField.setAccessible(true);
106-
List<Runnable> pending = (List<Runnable>) pendingField.get(Display.getInstance());
107-
LinkedList<Runnable> running = (LinkedList<Runnable>) runningField.get(Display.getInstance());
108-
while (!pending.isEmpty() || !running.isEmpty()) {
109-
while (!pending.isEmpty()) {
110-
Runnable next = pending.remove(0);
111-
next.run();
112-
}
113-
while (!running.isEmpty()) {
114-
Runnable next = running.removeFirst();
115-
next.run();
116-
}
117-
}
118-
}
119-
120102
private static class SimpleTest extends AbstractTest {
121103
private final String name;
122104
private final boolean shouldExecuteOnEDT;
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package com.codename1.ui;
2+
3+
import com.codename1.test.UITestBase;
4+
import com.codename1.ui.Image;
5+
import com.codename1.ui.list.DefaultListModel;
6+
import com.codename1.ui.list.ListModel;
7+
import com.codename1.ui.plaf.UIManager;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
11+
import java.lang.reflect.Method;
12+
import java.util.ArrayList;
13+
import java.util.Hashtable;
14+
import java.util.List;
15+
16+
import static org.junit.jupiter.api.Assertions.*;
17+
18+
public class AutoCompleteTextComponentTest extends UITestBase {
19+
20+
private ListModel<String> suggestionModel;
21+
22+
@BeforeEach
23+
void initModel() {
24+
suggestionModel = new DefaultListModel<String>(new String[]{"alpha", "beta", "gamma"});
25+
}
26+
27+
@Test
28+
void constructorAppliesCustomFilterAndProvidesEditorAccess() throws Exception {
29+
final List<String> filtered = new ArrayList<String>();
30+
AutoCompleteTextComponent.AutoCompleteFilter filter = new AutoCompleteTextComponent.AutoCompleteFilter() {
31+
public boolean filter(String text) {
32+
filtered.add(text);
33+
return text.length() > 0;
34+
}
35+
};
36+
AutoCompleteTextComponent component = new AutoCompleteTextComponent(suggestionModel, filter);
37+
AutoCompleteTextField field = component.getAutoCompleteField();
38+
Method filterMethod = AutoCompleteTextField.class.getDeclaredMethod("filter", String.class);
39+
filterMethod.setAccessible(true);
40+
Boolean result = (Boolean) filterMethod.invoke(field, "alp");
41+
assertTrue(result.booleanValue(), "Custom filter should return its result");
42+
assertEquals(1, filtered.size(), "Filter should be invoked with provided text");
43+
assertEquals("alp", filtered.get(0));
44+
assertSame(field, component.getField(), "getField should expose the underlying AutoCompleteTextField");
45+
assertSame(field, component.getEditor(), "getEditor should return the AutoCompleteTextField instance");
46+
}
47+
48+
@Test
49+
void focusAnimationFollowsThemeAndManualOverrides() {
50+
Hashtable theme = new Hashtable();
51+
theme.put("@textComponentAnimBool", "true");
52+
UIManager.getInstance().setThemeProps(theme);
53+
54+
AutoCompleteTextComponent component = new AutoCompleteTextComponent(suggestionModel, AcceptAllFilter.INSTANCE);
55+
assertTrue(component.isFocusAnimation(), "Theme constant should enable focus animation by default");
56+
57+
component.focusAnimation(false);
58+
assertFalse(component.isFocusAnimation(), "Explicit false should override theme");
59+
60+
component.focusAnimation(true);
61+
assertTrue(component.isFocusAnimation(), "Explicit true should override theme");
62+
}
63+
64+
@Test
65+
void fluentSettersUpdateUnderlyingField() {
66+
AutoCompleteTextComponent component = new AutoCompleteTextComponent(suggestionModel, AcceptAllFilter.INSTANCE);
67+
Image hintIcon = Image.createImage(2, 2);
68+
69+
component
70+
.text("username")
71+
.hint("Enter username")
72+
.hint(hintIcon)
73+
.multiline(true)
74+
.columns(12)
75+
.rows(3)
76+
.constraint(TextArea.EMAILADDR);
77+
78+
AutoCompleteTextField field = component.getAutoCompleteField();
79+
assertEquals("username", field.getText());
80+
assertEquals("Enter username", field.getHint());
81+
assertFalse(field.isSingleLineTextArea(), "Multiline true should mark the field as multi-line");
82+
assertEquals(12, field.getColumns());
83+
assertEquals(3, field.getRows());
84+
assertEquals(TextArea.EMAILADDR, field.getConstraint());
85+
assertSame(hintIcon, field.getHintIcon());
86+
}
87+
88+
@Test
89+
void propertyMetadataAndValuesReflectFieldState() {
90+
AutoCompleteTextComponent component = new AutoCompleteTextComponent(suggestionModel, AcceptAllFilter.INSTANCE);
91+
component
92+
.text("initial")
93+
.label("Label")
94+
.hint("Hint")
95+
.multiline(true)
96+
.columns(7)
97+
.rows(4)
98+
.constraint(TextArea.NUMERIC);
99+
100+
assertArrayEquals(new String[]{"text", "label", "hint", "multiline", "columns", "rows", "constraint"}, component.getPropertyNames());
101+
assertArrayEquals(new Class[]{String.class, String.class, String.class, Boolean.class, Integer.class, Integer.class, Integer.class}, component.getPropertyTypes());
102+
assertArrayEquals(new String[]{"String", "String", "String", "Boolean", "Integer", "Integer", "Integer"}, component.getPropertyTypeNames());
103+
104+
assertEquals("initial", component.getPropertyValue("text"));
105+
assertEquals("Hint", component.getPropertyValue("hint"));
106+
assertEquals(Boolean.TRUE, component.getPropertyValue("multiline"));
107+
assertEquals(Integer.valueOf(7), component.getPropertyValue("columns"));
108+
assertEquals(Integer.valueOf(4), component.getPropertyValue("rows"));
109+
assertEquals(Integer.valueOf(TextArea.NUMERIC), component.getPropertyValue("constraint"));
110+
111+
component.setPropertyValue("text", "updated");
112+
component.setPropertyValue("hint", "Another");
113+
component.setPropertyValue("multiline", Boolean.FALSE);
114+
component.setPropertyValue("columns", Integer.valueOf(5));
115+
component.setPropertyValue("rows", Integer.valueOf(2));
116+
component.setPropertyValue("constraint", Integer.valueOf(TextArea.PHONENUMBER));
117+
118+
AutoCompleteTextField field = component.getAutoCompleteField();
119+
assertEquals("updated", field.getText());
120+
assertEquals("Another", field.getHint());
121+
assertTrue(field.isSingleLineTextArea(), "Setting multiline false should restore single line mode");
122+
assertEquals(5, field.getColumns());
123+
assertEquals(2, field.getRows());
124+
assertEquals(TextArea.PHONENUMBER, field.getConstraint());
125+
}
126+
127+
private enum AcceptAllFilter implements AutoCompleteTextComponent.AutoCompleteFilter {
128+
INSTANCE;
129+
130+
public boolean filter(String text) {
131+
return true;
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)