Merge branch 'testing-client' into 'main'

added IngredientControllerTest

Closes #77

See merge request cse1105/2025-2026/teams/csep-team-76!82
This commit is contained in:
Aysegul Aydinlik 2026-01-22 15:17:03 +01:00
commit 14a4e157ea
2 changed files with 177 additions and 1 deletions

View file

@ -23,7 +23,7 @@ public class IngredientController {
private final RestTemplate restTemplate = new RestTemplate(); // Simplified REST client
@FXML
private void handleDeleteIngredient(ActionEvent event) {
public void handleDeleteIngredient(ActionEvent event) {
// Get selected ingredient
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
if (selectedIngredient == null) {

View file

@ -0,0 +1,176 @@
package client.Ingredient;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import commons.Ingredient;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
import javafx.stage.Window;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.github.tomakehurst.wiremock.client.WireMock.delete;
import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
import static com.github.tomakehurst.wiremock.client.WireMock.okJson;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
@EnabledIfEnvironmentVariable(named = "DISPLAY", matches = ".+")
@WireMockTest(httpPort = 8080)
class IngredientControllerMockTest {
private IngredientController controller;
private ListView<Ingredient> ingredientListView;
// starting javaFX and allow use of listview and alert usage
@BeforeAll
static void initJavaFx() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
try {
Platform.startup(latch::countDown);
} catch (IllegalStateException alreadyStarted) {
latch.countDown();
}
assertTrue(latch.await(3, TimeUnit.SECONDS), "JavaFX Platform failed to start");
Platform.setImplicitExit(false);
}
//inject fxml fields and create controller + mock UI
@BeforeEach
void setup() throws Exception {
controller = new IngredientController();
ingredientListView = new ListView<>();
ingredientListView.setItems(FXCollections.observableArrayList(
new Ingredient("Bread", 1, 2, 3),
new Ingredient("Cheese", 2, 2, 2),
new Ingredient("Ham", 3, 3, 3)
));
setPrivateField(controller, "ingredientListView", ingredientListView);
setPrivateField(controller, "deleteButton", new Button("Delete"));
}
// pick ingredient -> backend says not in use -> fake delete ingredient
@Test
void deleteIngredientWhenNotUsedCallsUsageThenDeleteAndClearsList() throws Exception {
Ingredient selected = ingredientListView.getItems().get(0);
ingredientListView.getSelectionModel().select(selected);
stubFor(get(urlEqualTo("/api/ingredients/" + selected.getId() + "/usage"))
.willReturn(okJson("{\"ingredientId\":" + selected.getId() + ",\"usedInRecipes\":0}")));
stubFor(delete(urlEqualTo("/api/ingredients/" + selected.getId()))
.willReturn(ok()));
// safe close for show and wait, run controller on JavaFX
try (DialogCloser closer = startDialogCloser()) {
runOnFxThreadAndWait(() -> controller.handleDeleteIngredient(new ActionEvent()));
}
verify(getRequestedFor(urlEqualTo("/api/ingredients/" + selected.getId() + "/usage")));
verify(deleteRequestedFor(urlEqualTo("/api/ingredients/" + selected.getId())));
assertEquals(0, ingredientListView.getItems().size());
}
//select ingredient -> if used backend says it and show warning -> safety delete but shouldn't happen
@Test
void deleteIngredientWhenUsedShowsWarningAndDoesNotDeleteIfDialogClosed() throws Exception {
Ingredient selected = ingredientListView.getItems().get(1);
ingredientListView.getSelectionModel().select(selected);
stubFor(get(urlEqualTo("/api/ingredients/" + selected.getId() + "/usage"))
.willReturn(okJson("{\"ingredientId\":" + selected.getId() + ",\"usedInRecipes\":2}")));
stubFor(delete(urlEqualTo("/api/ingredients/" + selected.getId()))
.willReturn(ok()));
//safe close as if user selected cancel
try (DialogCloser closer = startDialogCloser()) {
runOnFxThreadAndWait(() -> controller.handleDeleteIngredient(new ActionEvent()));
}
// check usage but not delete
verify(getRequestedFor(urlEqualTo("/api/ingredients/" + selected.getId() + "/usage")));
verify(0, deleteRequestedFor(urlEqualTo("/api/ingredients/" + selected.getId())));
assertEquals(3, ingredientListView.getItems().size());
}
// fxml helper
private static void setPrivateField(Object target, String fieldName, Object value) throws Exception {
Field f = target.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(target, value);
}
//controller on JavaFX
private static void runOnFxThreadAndWait(Runnable action) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
try {
action.run();
} finally {
latch.countDown();
}
});
assertTrue(latch.await(8, TimeUnit.SECONDS), "FX action timed out");
}
// safe close so that show and wait doesn't wait forever
private static DialogCloser startDialogCloser() {
AtomicBoolean running = new AtomicBoolean(true);
Thread t = new Thread(() -> {
while (running.get()) {
try {
Thread.sleep(50);
} catch (InterruptedException ignored) {
}
Platform.runLater(() -> {
for (Window w : Window.getWindows()) {
if (w instanceof Stage stage && stage.isShowing()) {
stage.close();
}
}
});
}
}, "javafx-dialog-closer");
t.setDaemon(true);
t.start();
return new DialogCloser(running);
}
// dialog closer
private static final class DialogCloser implements AutoCloseable {
private final AtomicBoolean running;
private DialogCloser(AtomicBoolean running) {
this.running = running;
}
@Override
public void close() {
running.set(false);
}
}
}