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:
commit
14a4e157ea
2 changed files with 177 additions and 1 deletions
|
|
@ -23,7 +23,7 @@ public class IngredientController {
|
||||||
private final RestTemplate restTemplate = new RestTemplate(); // Simplified REST client
|
private final RestTemplate restTemplate = new RestTemplate(); // Simplified REST client
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void handleDeleteIngredient(ActionEvent event) {
|
public void handleDeleteIngredient(ActionEvent event) {
|
||||||
// Get selected ingredient
|
// Get selected ingredient
|
||||||
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
|
Ingredient selectedIngredient = ingredientListView.getSelectionModel().getSelectedItem();
|
||||||
if (selectedIngredient == null) {
|
if (selectedIngredient == null) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue