Skip to content

Commit bed7498

Browse files
authored
Add comprehensive unit tests for core UI components (#3987)
1 parent 92201b4 commit bed7498

File tree

8 files changed

+732
-0
lines changed

8 files changed

+732
-0
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.codename1.components;
2+
3+
import com.codename1.media.Media;
4+
import com.codename1.media.MediaRecorderBuilder;
5+
import com.codename1.ui.Button;
6+
import com.codename1.ui.Label;
7+
import com.codename1.ui.events.ActionEvent;
8+
import com.codename1.ui.events.ActionListener;
9+
10+
import org.junit.jupiter.api.BeforeEach;
11+
import org.junit.jupiter.api.Test;
12+
13+
import java.lang.reflect.Field;
14+
import java.util.Collection;
15+
import java.util.concurrent.atomic.AtomicInteger;
16+
17+
import static org.junit.jupiter.api.Assertions.*;
18+
import static org.mockito.ArgumentMatchers.any;
19+
import static org.mockito.ArgumentMatchers.anyString;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.verify;
22+
import static org.mockito.Mockito.when;
23+
24+
class AudioRecorderComponentTest extends ComponentTestBase {
25+
26+
private Media media;
27+
28+
@BeforeEach
29+
void prepareMediaMocks() throws Exception {
30+
when(implementation.getAvailableRecordingMimeTypes()).thenReturn(new String[]{"audio/wav"});
31+
media = mock(Media.class);
32+
when(implementation.createMediaRecorder(any(MediaRecorderBuilder.class))).thenReturn(media);
33+
when(implementation.createMediaRecorder(anyString(), anyString())).thenReturn(media);
34+
}
35+
36+
private AudioRecorderComponent createRecorder(boolean redirect) {
37+
MediaRecorderBuilder builder = new MediaRecorderBuilder();
38+
builder.path("/tmp/record.m4a");
39+
builder.redirectToAudioBuffer(redirect);
40+
AudioRecorderComponent component = new AudioRecorderComponent(builder);
41+
flushSerialCalls();
42+
return component;
43+
}
44+
45+
@Test
46+
void initializationQueuesAndAppliesPausedState() {
47+
AudioRecorderComponent recorder = createRecorder(false);
48+
assertEquals(AudioRecorderComponent.RecorderState.Paused, recorder.getState());
49+
assertTrue(recorder.getComponentCount() > 0, "Recorder UI should be constructed after initialization");
50+
}
51+
52+
@Test
53+
void recordAndPauseActionsUpdateMediaState() throws Exception {
54+
AudioRecorderComponent recorder = createRecorder(false);
55+
Button recordButton = getPrivateButton(recorder, "record");
56+
fireButtonAction(recordButton);
57+
assertEquals(AudioRecorderComponent.RecorderState.Recording, recorder.getState());
58+
verify(media).play();
59+
60+
Button pauseButton = getPrivateButton(recorder, "pause");
61+
fireButtonAction(pauseButton);
62+
assertEquals(AudioRecorderComponent.RecorderState.Paused, recorder.getState());
63+
verify(media).pause();
64+
}
65+
66+
@Test
67+
void doneActionRedirectAcceptsRecordingAndNotifiesListeners() throws Exception {
68+
AudioRecorderComponent recorder = createRecorder(true);
69+
Button recordButton = getPrivateButton(recorder, "record");
70+
fireButtonAction(recordButton);
71+
72+
AtomicInteger eventCount = new AtomicInteger();
73+
recorder.addActionListener(evt -> eventCount.incrementAndGet());
74+
75+
Button doneButton = getPrivateButton(recorder, "done");
76+
fireButtonAction(doneButton);
77+
78+
verify(media).cleanup();
79+
assertEquals(AudioRecorderComponent.RecorderState.Accepted, recorder.getState());
80+
assertEquals(1, eventCount.get(), "AudioRecorderComponent only fires action events when the recording is accepted");
81+
}
82+
83+
@Test
84+
void animateUpdatesRecordingTime() throws Exception {
85+
AudioRecorderComponent recorder = createRecorder(false);
86+
setPrivateField(recorder, "state", AudioRecorderComponent.RecorderState.Recording);
87+
setPrivateField(recorder, "recordingLength", 61005L);
88+
setPrivateField(recorder, "lastRecordingStartTime", 0L);
89+
Label recordingTime = getPrivateField(recorder, "recordingTime", Label.class);
90+
boolean animating = recorder.animate();
91+
assertTrue(animating);
92+
assertEquals("01:01.5", recordingTime.getText());
93+
}
94+
95+
private Button getPrivateButton(AudioRecorderComponent recorder, String fieldName) throws Exception {
96+
return getPrivateField(recorder, fieldName, Button.class);
97+
}
98+
99+
@SuppressWarnings("unchecked")
100+
private void fireButtonAction(Button button) {
101+
Collection listeners = button.getListeners();
102+
for (Object listener : listeners) {
103+
((ActionListener) listener).actionPerformed(new ActionEvent(button));
104+
}
105+
flushSerialCalls();
106+
}
107+
108+
@SuppressWarnings("unchecked")
109+
private <T> T getPrivateField(Object target, String name, Class<T> type) throws Exception {
110+
Field field = target.getClass().getDeclaredField(name);
111+
field.setAccessible(true);
112+
return (T) field.get(target);
113+
}
114+
115+
private void setPrivateField(Object target, String name, Object value) throws Exception {
116+
Field field = target.getClass().getDeclaredField(name);
117+
field.setAccessible(true);
118+
field.set(target, value);
119+
}
120+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.codename1.components;
2+
3+
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;
11+
12+
/**
13+
* Base class for component tests that provides utilities for working with the mocked display.
14+
*/
15+
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+
}
68+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package com.codename1.components;
2+
3+
import com.codename1.ui.Button;
4+
import com.codename1.ui.Component;
5+
import com.codename1.ui.Container;
6+
import com.codename1.ui.FontImage;
7+
8+
import org.junit.jupiter.api.AfterEach;
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Test;
11+
12+
import java.lang.reflect.Field;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
16+
import static org.junit.jupiter.api.Assertions.*;
17+
import static org.mockito.Mockito.when;
18+
19+
class FloatingActionButtonTest extends ComponentTestBase {
20+
21+
private boolean originalAutoSizing;
22+
private float originalDefaultSize;
23+
24+
@BeforeEach
25+
void captureDefaults() {
26+
originalAutoSizing = FloatingActionButton.isAutoSizing();
27+
originalDefaultSize = FloatingActionButton.getIconDefaultSize();
28+
when(implementation.isPortrait()).thenReturn(true);
29+
}
30+
31+
@AfterEach
32+
void restoreDefaults() {
33+
FloatingActionButton.setAutoSizing(originalAutoSizing);
34+
FloatingActionButton.setIconDefaultSize(originalDefaultSize);
35+
}
36+
37+
@Test
38+
void autoSizingUsesIconDimensions() {
39+
FloatingActionButton.setAutoSizing(true);
40+
FloatingActionButton fab = new FloatingActionButton(FontImage.MATERIAL_ADD, null, FloatingActionButton.getIconDefaultSize());
41+
int expectedWidth = fab.getIcon().getWidth() * 11 / 4;
42+
int expectedHeight = fab.getIcon().getHeight() * 11 / 4;
43+
assertEquals(expectedWidth, fab.getPreferredSize().getWidth());
44+
assertEquals(expectedHeight, fab.getPreferredSize().getHeight());
45+
}
46+
47+
@Test
48+
void createSubFabStoresButtonsInMenu() throws Exception {
49+
FloatingActionButton fab = new FloatingActionButton(FontImage.MATERIAL_ADD, null, FloatingActionButton.getIconDefaultSize());
50+
FloatingActionButton first = fab.createSubFAB(FontImage.MATERIAL_CAMERA, "Camera");
51+
FloatingActionButton second = fab.createSubFAB(FontImage.MATERIAL_CHAT, "Chat");
52+
53+
List<FloatingActionButton> subMenu = getSubMenu(fab);
54+
assertEquals(2, subMenu.size());
55+
assertSame(first, subMenu.get(0));
56+
assertSame(second, subMenu.get(1));
57+
}
58+
59+
@Test
60+
void popupContentCreatesTextActionsThatTriggerSubFab() throws Exception {
61+
FloatingActionButton fab = new FloatingActionButton(FontImage.MATERIAL_ADD, null, FloatingActionButton.getIconDefaultSize());
62+
fab.setFloatingActionTextUIID("PopupText");
63+
fab.setWidth(120);
64+
65+
TrackingSubFab subFab = new TrackingSubFab(FontImage.MATERIAL_EMAIL, "Send");
66+
List<FloatingActionButton> subs = new ArrayList<>();
67+
subs.add(subFab);
68+
setSubMenu(fab, subs);
69+
70+
Container content = fab.createPopupContent(subs);
71+
assertEquals(1, content.getComponentCount());
72+
Button textButton = findButtonByText(content, "Send");
73+
assertNotNull(textButton);
74+
assertEquals("PopupText", textButton.getUIID());
75+
76+
for (Object listenerObj : textButton.getListeners()) {
77+
((com.codename1.ui.events.ActionListener) listenerObj).actionPerformed(new com.codename1.ui.events.ActionEvent(textButton));
78+
}
79+
80+
assertTrue(subFab.pressedCalled);
81+
assertTrue(subFab.releasedCalled);
82+
}
83+
84+
@Test
85+
void badgeCreationKeepsTextAndUiid() {
86+
FloatingActionButton badge = FloatingActionButton.createBadge("3");
87+
assertEquals("Badge", badge.getUIID());
88+
badge.setText("7");
89+
assertEquals("7", badge.getText());
90+
}
91+
92+
private Button findButtonByText(Component component, String text) {
93+
if (component instanceof Button) {
94+
Button button = (Button) component;
95+
if (text.equals(button.getText())) {
96+
return button;
97+
}
98+
}
99+
if (component instanceof Container) {
100+
Container container = (Container) component;
101+
for (Component child : container) {
102+
Button result = findButtonByText(child, text);
103+
if (result != null) {
104+
return result;
105+
}
106+
}
107+
}
108+
return null;
109+
}
110+
111+
@SuppressWarnings("unchecked")
112+
private List<FloatingActionButton> getSubMenu(FloatingActionButton fab) throws Exception {
113+
Field field = FloatingActionButton.class.getDeclaredField("subMenu");
114+
field.setAccessible(true);
115+
return (List<FloatingActionButton>) field.get(fab);
116+
}
117+
118+
private void setSubMenu(FloatingActionButton fab, List<FloatingActionButton> subMenu) throws Exception {
119+
Field field = FloatingActionButton.class.getDeclaredField("subMenu");
120+
field.setAccessible(true);
121+
field.set(fab, subMenu);
122+
}
123+
124+
private static class TrackingSubFab extends FloatingActionButton {
125+
boolean pressedCalled;
126+
boolean releasedCalled;
127+
128+
TrackingSubFab(char icon, String text) {
129+
super(icon, text, 2.8f);
130+
}
131+
132+
@Override
133+
public void pressed() {
134+
pressedCalled = true;
135+
super.pressed();
136+
}
137+
138+
@Override
139+
public void released() {
140+
releasedCalled = true;
141+
super.released();
142+
}
143+
}
144+
}

0 commit comments

Comments
 (0)