diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
new file mode 100644
index 0000000..7fab6b6
--- /dev/null
+++ b/integration-tests/pom.xml
@@ -0,0 +1,83 @@
+
+
+ 4.0.0
+
+ csep
+ root
+ 0.0.1-SNAPSHOT
+
+ integration-tests
+
+
+ 25
+ 25
+ UTF-8
+
+
+
+
+ org.testfx
+ testfx-core
+ 4.0.16-alpha
+
+
+
+
+ org.testfx
+ testfx-junit5
+ 4.0.16-alpha
+
+
+
+
+ org.openjfx
+ javafx-controls
+ 25.0.1
+
+
+
+ org.openjfx
+ javafx-fxml
+ 25.0.1
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.10.1
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.10.1
+
+
+ csep
+ commons
+ 0.0.1-SNAPSHOT
+
+
+ csep
+ client
+ 0.0.1-SNAPSHOT
+
+
+ csep
+ server
+ 0.0.1-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-test
+ 3.5.7
+
+
+ org.springframework.boot
+ spring-boot-test-autoconfigure
+ 3.5.7
+ test
+
+
+
\ No newline at end of file
diff --git a/integration-tests/src/test/java/csep/TestModule.java b/integration-tests/src/test/java/csep/TestModule.java
new file mode 100644
index 0000000..1a16863
--- /dev/null
+++ b/integration-tests/src/test/java/csep/TestModule.java
@@ -0,0 +1,39 @@
+package csep;
+
+import client.scenes.FoodpalApplicationCtrl;
+import client.scenes.MainCtrl;
+import client.scenes.SearchBarCtrl;
+import client.scenes.recipe.IngredientListCtrl;
+import client.scenes.recipe.RecipeStepListCtrl;
+import client.utils.*;
+import com.google.inject.AbstractModule;
+import com.google.inject.Binder;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import commons.Ingredient;
+import commons.Recipe;
+
+import java.nio.file.Path;
+
+public class TestModule implements Module {
+ @Override
+ public void configure(Binder binder) {
+ binder.bind(MainCtrl.class).in(Scopes.SINGLETON);
+ binder.bind(FoodpalApplicationCtrl.class).in(Scopes.SINGLETON);
+ binder.bind(IngredientListCtrl.class).in(Scopes.SINGLETON);
+ binder.bind(RecipeStepListCtrl.class).in(Scopes.SINGLETON);
+ binder.bind(SearchBarCtrl.class).in(Scopes.SINGLETON);
+ binder.bind(LocaleManager.class).in(Scopes.SINGLETON);
+ binder.bind(ServerUtils.class).in(Scopes.SINGLETON);
+ binder.bind(WebSocketUtils.class).in(Scopes.SINGLETON);
+
+ binder.bind(ConfigService.class).toInstance(new ConfigService(Path.of("config.json")));
+ binder.bind(new TypeLiteral>() {}).toInstance(
+ new WebSocketDataService<>()
+ );
+ binder.bind(new TypeLiteral>() {}).toInstance(
+ new WebSocketDataService<>()
+ );
+ }
+}
diff --git a/integration-tests/src/test/java/csep/backlog/BaseTest.java b/integration-tests/src/test/java/csep/backlog/BaseTest.java
new file mode 100644
index 0000000..423264a
--- /dev/null
+++ b/integration-tests/src/test/java/csep/backlog/BaseTest.java
@@ -0,0 +1,74 @@
+package csep.backlog;
+
+import client.MyFXML;
+import client.MyModule;
+import client.UI;
+import com.google.inject.Injector;
+import commons.Recipe;
+import javafx.scene.control.ListView;
+import javafx.scene.input.KeyCode;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static com.google.inject.Guice.createInjector;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+import org.testfx.api.FxToolkit;
+import org.testfx.framework.junit5.ApplicationTest;
+import org.testfx.util.WaitForAsyncUtils;
+import server.Main;
+import server.WebSocketConfig;
+import server.database.IngredientRepository;
+import server.database.RecipeIngredientRepository;
+import server.database.RecipeRepository;
+
+@SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+@ActiveProfiles("test")
+@Import(WebSocketConfig
+ .class)
+public abstract class BaseTest extends ApplicationTest {
+ @Autowired
+ private IngredientRepository ingredientRepository;
+ @Autowired
+ private RecipeRepository recipeRepository;
+ @Autowired
+ private RecipeIngredientRepository recipeIngredientRepository;
+
+ private static final Injector INJECTOR = createInjector(new MyModule());
+ private static final MyFXML FXML = new MyFXML(INJECTOR);
+ @Autowired
+ protected ApplicationContext applicationContext;
+
+ protected T bean(Class type) {
+ return applicationContext.getBean(type);
+ }
+
+ protected T bean(String name, Class type) {
+ return applicationContext.getBean(name, type);
+ }
+ @BeforeEach
+ public void setup() throws Exception {
+ ingredientRepository.deleteAll();
+ recipeRepository.deleteAll();
+
+ recipeIngredientRepository.deleteAll();
+ ingredientRepository.flush();
+ recipeRepository.flush();
+ recipeIngredientRepository.flush();
+ FxToolkit.registerPrimaryStage();
+ FxToolkit.setupApplication(UI::new);
+ WaitForAsyncUtils.waitForFxEvents();
+ }
+ @AfterEach
+ public void tearDown() throws Exception {
+ FxToolkit.cleanupStages();
+ WaitForAsyncUtils.waitForFxEvents();
+ }
+}
+
diff --git a/integration-tests/src/test/java/csep/backlog/basic/CRUDTest.java b/integration-tests/src/test/java/csep/backlog/basic/CRUDTest.java
new file mode 100644
index 0000000..75fa30c
--- /dev/null
+++ b/integration-tests/src/test/java/csep/backlog/basic/CRUDTest.java
@@ -0,0 +1,67 @@
+package csep.backlog.basic;
+
+import commons.Recipe;
+import csep.backlog.BaseTest;
+import javafx.scene.control.ListView;
+import javafx.scene.input.KeyCode;
+import org.junit.jupiter.api.Test;
+import org.testfx.util.WaitForAsyncUtils;
+import server.database.RecipeRepository;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+
+// TODO(1): Testing for error paths
+
+public class CRUDTest extends BaseTest {
+ @Test
+ public void newRecipeOnClickDoesAddOne() {
+ ListView list = lookup("#recipeList").query();
+ // when:
+ clickOn("#addRecipeButton");
+ // then:
+ WaitForAsyncUtils.waitForFxEvents();
+ assertEquals(1L, bean(RecipeRepository.class).count());
+ assertEquals(1, list.getItems().size());
+ }
+ @Test
+ public void deleteRecipeOnClickDoesDeleteOne() {
+ ListView list = lookup("#recipeList").query();
+ // when:
+ clickOn("#addRecipeButton");
+ WaitForAsyncUtils.waitForFxEvents();
+ assertEquals(1, list.getItems().size());
+ WaitForAsyncUtils.waitForFxEvents();
+ clickOn("#removeRecipeButton");
+ // then:
+ WaitForAsyncUtils.waitForFxEvents();
+ assertEquals(0, list.getItems().size());
+ }
+ @Test
+ public void changeRecipeNameDoesChangeName() {
+ WaitForAsyncUtils.waitForFxEvents();
+ ListView list = lookup("#recipeList").query();
+ clickOn("#addRecipeButton");
+ WaitForAsyncUtils.waitForFxEvents();
+ write("TEST_RECIPE");
+ type(KeyCode.ENTER);
+ WaitForAsyncUtils.waitForFxEvents();
+ assertEquals("TEST_RECIPE", list.getItems().get(0).getName());
+ }
+ @Test
+ public void cloneRecipeOnClickDoesCloneOne() {
+ ListView list = lookup("#recipeList").query();
+ clickOn("#addRecipeButton");
+ write("TEST_RECIPE");
+ type(KeyCode.ENTER);
+ WaitForAsyncUtils.waitForFxEvents();
+ clickOn("#cloneRecipeButton");
+ WaitForAsyncUtils.waitForFxEvents();
+ assertEquals(2, list.getItems().size());
+ Recipe orig = list.getItems().get(0);
+ Recipe cloned = list.getItems().get(1);
+ assertNotEquals(orig.getName(), cloned.getName());
+ assertEquals(orig.getPreparationSteps(), cloned.getPreparationSteps());
+ }
+}
diff --git a/integration-tests/src/test/java/csep/backlog/basic/RecipeDetailsTest.java b/integration-tests/src/test/java/csep/backlog/basic/RecipeDetailsTest.java
new file mode 100644
index 0000000..d6d581e
--- /dev/null
+++ b/integration-tests/src/test/java/csep/backlog/basic/RecipeDetailsTest.java
@@ -0,0 +1,4 @@
+package csep.backlog.basic;
+
+public class RecipeDetailsTest {
+}
diff --git a/integration-tests/src/test/resources/application-test.properties b/integration-tests/src/test/resources/application-test.properties
new file mode 100644
index 0000000..2d65ed8
--- /dev/null
+++ b/integration-tests/src/test/resources/application-test.properties
@@ -0,0 +1,18 @@
+spring.datasource.driverClassName=org.h2.Driver
+spring.datasource.username=sa
+spring.datasource.password=
+spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+
+# use one of these alternatives...
+# ... purely in-memory, wiped on restart, but great for testing
+spring.datasource.url=jdbc:h2:mem:testdb
+# ... persisted on disk (in project directory)
+#spring.datasource.url=jdbc:h2:file:./h2-database
+
+# enable DB view on http://localhost:8080/h2-console
+spring.h2.console.enabled=true
+
+# strategy for table (re-)generation
+spring.jpa.hibernate.ddl-auto=create-drop
+# show auto-generated SQL commands
+#spring.jpa.hibernate.show_sql=true
diff --git a/pom.xml b/pom.xml
index 8c0393a..babe7e4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,6 +13,6 @@
commons
client
server
-
-
+ integration-tests
+