Skip to content

Commit f6f6dc7

Browse files
Fix for #274 - NPE if no editor can be created for a given mimetype (#275)
* cleanup * handling a race condition in JeddictBrain with no proper content returned from the LLM * fixing bug #274 - NPE if no editor can be created for a given mimetype * content to reproduce #374 * Added copyright to DummyChatModelListener * cleanup
1 parent 3e43376 commit f6f6dc7

7 files changed

Lines changed: 260 additions & 29 deletions

File tree

src/main/java/io/github/jeddict/ai/agent/AbstractTool.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ public void addListener(final JeddictBrainListener listener) {
6262
listeners.add(listener);
6363
}
6464

65+
public void removeListener(final JeddictBrainListener listener) {
66+
if (listener == null) {
67+
throw new IllegalArgumentException("listener can not be null");
68+
}
69+
listeners.remove(listener);
70+
}
71+
6572
public void checkPath(final String path) throws ToolExecutionException {
6673
//
6774
// NOTE: we can not use toRealPath here because we want to check even
@@ -78,14 +85,7 @@ public void checkPath(final String path) throws ToolExecutionException {
7885
"trying to reach a file outside the project folder");
7986
}
8087
}
81-
82-
public void removeListener(final JeddictBrainListener listener) {
83-
if (listener == null) {
84-
throw new IllegalArgumentException("listener can not be null");
85-
}
86-
listeners.remove(listener);
87-
}
88-
88+
8989
public Path fullPath(final String path) {
9090
return basepath.resolve(path).normalize();
9191
}

src/main/java/io/github/jeddict/ai/lang/JeddictBrain.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -368,15 +368,19 @@ public void onEvent(AiServiceCompletedEvent e) {
368368
// the toString() of the object so that the listener can
369369
// rely on receive always the same object
370370
//
371-
final Object obj = e.result().get();
372-
373-
final ChatResponse result = (obj instanceof ChatResponse)
374-
? (ChatResponse)obj
375-
: ChatResponse.builder().aiMessage(AiMessage.from(String.valueOf(obj))).build();
376-
LOG.finest(() ->
377-
"%s\n%s".formatted(String.valueOf(e.eventClass()), String.valueOf(result))
378-
);
379-
on(listeners).loop((l) -> l.onChatCompleted(result));
371+
e.result().ifPresentOrElse(obj -> {
372+
final ChatResponse result = (obj instanceof ChatResponse)
373+
? (ChatResponse)obj
374+
: ChatResponse.builder().aiMessage(AiMessage.from(String.valueOf(obj))).build();
375+
LOG.finest(() ->
376+
"%s\n%s".formatted(String.valueOf(e.eventClass()), String.valueOf(result))
377+
);
378+
on(listeners).loop((l) -> l.onChatCompleted(result));
379+
}, () -> {
380+
LOG.finest(() ->
381+
"no result from the model..."
382+
);
383+
});
380384
}
381385
},
382386
new AiServiceErrorListener() {

src/main/java/io/github/jeddict/ai/util/EditorUtil.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public static String updateEditors(
119119
if (firstPane != null) {
120120
firstPane.scrollRectToVisible(firstPane.getBounds());
121121
}
122-
122+
123123
assistantChat.revalidate();
124124
assistantChat.repaint();
125125
List<FileObject> context = new ArrayList<>();
@@ -642,14 +642,32 @@ public static NbEditorKit createEditorKit(String mimeType) {
642642
public static JEditorPane createInMemoryEditorCopy(String mimeType) {
643643
JEditorPane mirrorEditor;
644644
try {
645-
String ext = getExtension(mimeType);
645+
final String ext = getExtension(mimeType);
646+
final FileObject root =
647+
org.openide.filesystems.FileUtil.createMemoryFileSystem().getRoot();
646648

647-
FileObject fo = org.openide.filesystems.FileUtil.createMemoryFileSystem()
648-
.getRoot()
649-
.createData("code-preview." + ext);
649+
FileObject fo = root.createData("code-preview." + ext);
650650

651651
EditorCookie ec = fo.getLookup().lookup(EditorCookie.class);
652652

653+
if (ec == null) {
654+
LOG.info(() -> "no editor to display '" + ext + "', falling back to txt");
655+
fo.delete();
656+
fo = root.createData("code-previre.txt");
657+
ec = fo.getLookup().lookup(EditorCookie.class);
658+
}
659+
660+
//
661+
// if here ec is still null (potentially it may happen if there is
662+
// no editor associated to .txt, although unlikely) there is nothing
663+
// we can do more...
664+
//
665+
if (ec == null) {
666+
final String msg = "no editors to display '" + ext + "' content have been found in NetBeans";
667+
LOG.severe(msg);
668+
throw new IllegalStateException(msg);
669+
}
670+
653671
try {
654672
ec.open();
655673

src/test/java/io/github/jeddict/ai/agent/AbstractToolTest.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55
import io.github.jeddict.ai.test.DummyTool;
66
import io.github.jeddict.ai.lang.DummyJeddictBrainListener;
77
import java.io.IOException;
8-
import java.nio.file.Paths;
98
import java.util.function.UnaryOperator;
109
import org.junit.jupiter.api.Test;
1110

1211
import static org.assertj.core.api.BDDAssertions.then;
1312
import static org.assertj.core.api.BDDAssertions.thenThrownBy;
14-
import org.junit.jupiter.api.BeforeEach;
1513

1614
public class AbstractToolTest extends TestBase {
1715

@@ -57,10 +55,10 @@ public void set_and_get_humanInTheMiddle() throws IOException {
5755
public void fires_onProgress_events() throws IOException {
5856
//
5957
// When a tool send an onProgress event, we want to start a new thread.
60-
// This is because when stremaing, content does not necessarily ends with
58+
// This is because when stremaing, content does not necessarily ends with
6159
// a \n and the output of the tool may not go to a new line.
6260
//
63-
61+
6462
// given
6563
final DummyTool tool = new DummyTool(projectDir);
6664
final DummyJeddictBrainListener listener = new DummyJeddictBrainListener();
@@ -72,8 +70,8 @@ public void fires_onProgress_events() throws IOException {
7270
// then
7371
then(listener.collector).hasSize(1);
7472
then(listener.collector.get(0)).asString().isEqualTo("(onProgress,\na message)");
75-
76-
73+
74+
7775
}
7876

7977
@Test

src/test/java/io/github/jeddict/ai/lang/JeddictBrainTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ public void get_new_pair_programmer() {
360360
* non interactive. Both must receive events.
361361
*/
362362
@Test
363-
public void interactive_and_not_interactove_agents_receive_events() {
363+
public void interactive_and_not_interactive_agents_receive_events() {
364364
final DummyJeddictBrainListener listener = new DummyJeddictBrainListener();
365365
final JeddictBrain brain = new JeddictBrain(false);
366366

src/test/java/io/github/jeddict/ai/test/DummyChatModelListener.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
/**
2+
* Copyright 2025 the original author or authors from the Jeddict project (https://jeddict.github.io/).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
116
package io.github.jeddict.ai.test;
217

318
import dev.langchain4j.model.chat.listener.ChatModelListener;
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
2+
To write a JUnit test for your JavaFX application using **TestFX** (which provides `FxRobot`), you'll need to:
3+
1. **Load the FXML file** into a `Parent` node.
4+
2. **Inject the controller** (if needed).
5+
3. **Use `FxRobot`** to interact with UI controls (e.g., setting text in `TextArea`).
6+
7+
---
8+
9+
### **Step 1: Add TestFX to Your Project**
10+
Ensure you have **TestFX** in your dependencies (Maven/Gradle):
11+
12+
#### **Maven (`pom.xml`)**
13+
```xml
14+
<dependency>
15+
<groupId>org.testfx</groupId>
16+
<artifactId>testfx-core</artifactId>
17+
<version>4.0.20</version>
18+
<scope>test</scope>
19+
</dependency>
20+
```
21+
22+
#### **Gradle (`build.gradle`)**
23+
```groovy
24+
testImplementation 'org.testfx:testfx-core:4.0.20'
25+
```
26+
27+
---
28+
29+
### **Step 2: Write the JUnit Test**
30+
Here's a complete example using **TestFX** to set text in `userPromptTextArea`:
31+
32+
```java
33+
import javafx.fxml.FXMLLoader;
34+
import javafx.scene.Parent;
35+
import javafx.scene.Scene;
36+
import javafx.stage.Stage;
37+
import org.junit.jupiter.api.BeforeEach;
38+
import org.junit.jupiter.api.Test;
39+
import org.testfx.api.FxRobot;
40+
import org.testfx.framework.junit5.ApplicationTest;
41+
import org.testfx.util.WaitForAsyncUtils;
42+
43+
import java.io.IOException;
44+
45+
import static org.testfx.api.FxAssert.verifyThat;
46+
import static org.testfx.matcher.control.TextAreaMatcher.hasText;
47+
48+
public class MainControllerTest extends ApplicationTest {
49+
50+
private Parent root;
51+
52+
@Override
53+
public void start(Stage stage) throws Exception {
54+
// Load the FXML file
55+
FXMLLoader loader = new FXMLLoader(getClass().getResource("/ste/ai/toolify/MainView.fxml"));
56+
root = loader.load();
57+
stage.setScene(new Scene(root));
58+
stage.show();
59+
}
60+
61+
@BeforeEach
62+
public void setUp() {
63+
// Load the FXML before each test
64+
try {
65+
FXMLLoader loader = new FXMLLoader(getClass().getResource("/ste/ai/toolify/MainView.fxml"));
66+
root = loader.load();
67+
} catch (IOException e) {
68+
throw new RuntimeException("Failed to load FXML", e);
69+
}
70+
}
71+
72+
@Test
73+
public void testSetUserPromptTextAreaText(FxRobot robot) {
74+
// Wait for the UI to load
75+
WaitForAsyncUtils.waitForFxRobot(() -> {
76+
// Find the TextArea by FXML ID
77+
robot.clickOn("#userPromptTextArea");
78+
79+
// Wait for focus (optional, but ensures the TextArea is ready)
80+
WaitForAsyncUtils.waitForFxRobot(() -> {
81+
// Set text in the TextArea
82+
robot.write("Hello, this is a test prompt!");
83+
});
84+
85+
// Verify the text was set
86+
verifyThat("#userPromptTextArea", hasText("Hello, this is a test prompt!"));
87+
});
88+
}
89+
}
90+
```
91+
92+
---
93+
94+
### **Key Points**
95+
1. **`FxRobot`** is used to interact with UI controls.
96+
2. **`robot.clickOn("#userPromptTextArea")`** ensures the `TextArea` is focused.
97+
3. **`robot.write("...")`** sets the text.
98+
4. **`verifyThat("#userPromptTextArea", hasText("..."))`** checks if the text was set correctly.
99+
100+
---
101+
102+
### **Alternative: Directly Setting Text (Without Clicking)**
103+
If you don't need to simulate user interaction, you can directly set the text using `FxRobot`'s `write()` method:
104+
105+
```java
106+
@Test
107+
public void testSetUserPromptTextAreaTextDirectly(FxRobot robot) {
108+
robot.write("#userPromptTextArea", "Hello, this is a test prompt!");
109+
verifyThat("#userPromptTextArea", hasText("Hello, this is a test prompt!"));
110+
}
111+
```
112+
113+
---
114+
115+
### **Handling Controllers (If Needed)**
116+
If your `MainController` has logic that modifies the `TextArea` after loading, you may need to **inject the controller** into the `FXMLLoader`:
117+
118+
```java
119+
@BeforeEach
120+
public void setUp() {
121+
FXMLLoader loader = new FXMLLoader(getClass().getResource("/ste/ai/toolify/MainView.fxml"));
122+
loader.setController(new MainController()); // Inject controller if needed
123+
try {
124+
root = loader.load();
125+
} catch (IOException e) {
126+
throw new RuntimeException("Failed to load FXML", e);
127+
}
128+
}
129+
```
130+
131+
---
132+
133+
### **Full Example with Error Handling**
134+
```java
135+
import javafx.fxml.FXMLLoader;
136+
import javafx.scene.Parent;
137+
import javafx.scene.Scene;
138+
import javafx.stage.Stage;
139+
import org.junit.jupiter.api.BeforeEach;
140+
import org.junit.jupiter.api.Test;
141+
import org.testfx.api.FxRobot;
142+
import org.testfx.framework.junit5.ApplicationTest;
143+
import org.testfx.util.WaitForAsyncUtils;
144+
145+
import java.io.IOException;
146+
147+
import static org.testfx.api.FxAssert.verifyThat;
148+
import static org.testfx.matcher.control.TextAreaMatcher.hasText;
149+
150+
public class MainControllerTest extends ApplicationTest {
151+
152+
private Parent root;
153+
154+
@Override
155+
public void start(Stage stage) throws Exception {
156+
FXMLLoader loader = new FXMLLoader(getClass().getResource("/ste/ai/toolify/MainView.fxml"));
157+
root = loader.load();
158+
stage.setScene(new Scene(root));
159+
stage.show();
160+
}
161+
162+
@BeforeEach
163+
public void setUp() {
164+
try {
165+
FXMLLoader loader = new FXMLLoader(getClass().getResource("/ste/ai/toolify/MainView.fxml"));
166+
root = loader.load();
167+
} catch (IOException e) {
168+
throw new RuntimeException("Failed to load FXML", e);
169+
}
170+
}
171+
172+
@Test
173+
public void testSetUserPromptTextAreaText(FxRobot robot) {
174+
WaitForAsyncUtils.waitForFxRobot(() -> {
175+
robot.write("#userPromptTextArea", "Hello, this is a test prompt!");
176+
verifyThat("#userPromptTextArea", hasText("Hello, this is a test prompt!"));
177+
});
178+
}
179+
}
180+
```
181+
182+
---
183+
184+
### **Troubleshooting**
185+
1. **If the `TextArea` is not found**, ensure:
186+
- The FXML path is correct (`/ste/ai/toolify/MainView.fxml`).
187+
- The `fx:id` (`userPromptTextArea`) matches the FXML.
188+
2. **If the test hangs**, use `WaitForAsyncUtils` to ensure the UI loads.
189+
3. **If the test fails with `NoSuchElementException`**, verify that the `TextArea` is visible and interactive.
190+
191+
---
192+
193+
### **Final Notes**
194+
- **TestFX** is the standard way to test JavaFX UI interactions.
195+
- **`FxRobot`** provides methods like `clickOn()`, `write()`, and `verifyThat()` for easy UI testing.
196+
- Always ensure the FXML is loaded before interacting with controls.

0 commit comments

Comments
 (0)