diff --git a/.eslintrc.json b/.eslintrc.json
index 61f2b36cfa7d13fab652eb8c1893237d748fc3c5..5731eb32b6d0461f11f5112c2a5a65ecb9b216a4 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -25,7 +25,10 @@
         "@typescript-eslint/ban-ts-comment": "off",
         "indent": [
             "warn",
-            4
+            4,
+            {
+                "SwitchCase": 1
+            }
         ],
         "vue/html-indent": [
             "warn",
diff --git a/.gitignore b/.gitignore
index 21762d4dec8b02f7a2421752af696c881c3891cf..e23b545651fd18d2cde0232e72b6858a6ee4ba44 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,4 +25,6 @@ public/preview.zip
 *.njsproj
 *.sln
 *.sw?
-
+/test-results/
+/playwright-report/
+/playwright/.cache/
diff --git a/electron/electron.js b/electron/electron.js
index ac2ea051c4c09226dfc03567965b544cd17f4cd8..812f53a5756f89b98f7038a8e9389d762075e999 100644
--- a/electron/electron.js
+++ b/electron/electron.js
@@ -10,6 +10,8 @@ const { cleanPreview } = require('./components/preview');
 const path = require('path');
 const { autoUpdater } = require('electron-updater');
 
+const headless = process.argv.includes('--headless=true');
+
 let mainWindow;
 let splashWindow;
 // Open file with editor, on windows : using argv | on macOS using open-file event (see below)
@@ -31,7 +33,7 @@ autoUpdater.checkForUpdatesAndNotify();
 // This method will be called when Electron has finished initialization and is ready to create browser windows.
 app.whenReady().then(() => {
     mainWindow = createMainWindow();
-    splashWindow = createSplashWindow();
+    if(!headless) splashWindow = createSplashWindow();
 
     setupIpcListener(mainWindow);
 
@@ -40,8 +42,11 @@ app.whenReady().then(() => {
         waitEvent(mainWindow, 'ready-to-show'),
         wait(200)
     ]).then(async () => {
-        splashWindow.destroy();
-        mainWindow.show();
+        if(!headless) {
+            splashWindow.destroy();
+            mainWindow.show();
+        }
+
         if (filepath) {
             mainWindow.webContents.send('epocProjectPicked', JSON.stringify({name: null, modified: null, filepath: filepath, workdir: null}));
         }
diff --git a/package-lock.json b/package-lock.json
index 2b58056e18f6398f29180fb47dd97f1d0c0e8430..04444a36697c5957aa7446aa754ad8d8c31b5a75 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,6 +31,8 @@
         "vuedraggable": "^4.1.0"
       },
       "devDependencies": {
+        "@playwright/test": "^1.38.1",
+        "@types/node": "^20.8.4",
         "@typescript-eslint/eslint-plugin": "^5.46.1",
         "@typescript-eslint/parser": "^5.46.1",
         "@vitejs/plugin-vue": "^4.0.0",
@@ -981,6 +983,21 @@
         "node": ">=14"
       }
     },
+    "node_modules/@playwright/test": {
+      "version": "1.38.1",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
+      "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
+      "dev": true,
+      "dependencies": {
+        "playwright": "1.38.1"
+      },
+      "bin": {
+        "playwright": "cli.js"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
     "node_modules/@popperjs/core": {
       "version": "2.11.8",
       "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -1390,9 +1407,12 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "20.3.2",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz",
-      "integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw=="
+      "version": "20.8.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.4.tgz",
+      "integrity": "sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A==",
+      "dependencies": {
+        "undici-types": "~5.25.1"
+      }
     },
     "node_modules/@types/normalize-package-data": {
       "version": "2.4.1",
@@ -9356,6 +9376,36 @@
         "node": ">=8"
       }
     },
+    "node_modules/playwright": {
+      "version": "1.38.1",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
+      "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
+      "dev": true,
+      "dependencies": {
+        "playwright-core": "1.38.1"
+      },
+      "bin": {
+        "playwright": "cli.js"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "optionalDependencies": {
+        "fsevents": "2.3.2"
+      }
+    },
+    "node_modules/playwright-core": {
+      "version": "1.38.1",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
+      "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
+      "dev": true,
+      "bin": {
+        "playwright-core": "cli.js"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
     "node_modules/plist": {
       "version": "3.0.6",
       "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz",
@@ -11195,6 +11245,11 @@
         "through": "^2.3.8"
       }
     },
+    "node_modules/undici-types": {
+      "version": "5.25.3",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
+      "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA=="
+    },
     "node_modules/universalify": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -13803,6 +13858,15 @@
       "dev": true,
       "optional": true
     },
+    "@playwright/test": {
+      "version": "1.38.1",
+      "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
+      "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
+      "dev": true,
+      "requires": {
+        "playwright": "1.38.1"
+      }
+    },
     "@popperjs/core": {
       "version": "2.11.8",
       "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -14161,9 +14225,12 @@
       "dev": true
     },
     "@types/node": {
-      "version": "20.3.2",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz",
-      "integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw=="
+      "version": "20.8.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.4.tgz",
+      "integrity": "sha512-ZVPnqU58giiCjSxjVUESDtdPk4QR5WQhhINbc9UBrKLU68MX5BF6kbQzTrkwbolyr0X8ChBpXfavr5mZFKZQ5A==",
+      "requires": {
+        "undici-types": "~5.25.1"
+      }
     },
     "@types/normalize-package-data": {
       "version": "2.4.1",
@@ -20130,6 +20197,22 @@
         "find-up": "^3.0.0"
       }
     },
+    "playwright": {
+      "version": "1.38.1",
+      "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
+      "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
+      "dev": true,
+      "requires": {
+        "fsevents": "2.3.2",
+        "playwright-core": "1.38.1"
+      }
+    },
+    "playwright-core": {
+      "version": "1.38.1",
+      "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
+      "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
+      "dev": true
+    },
     "plist": {
       "version": "3.0.6",
       "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz",
@@ -21504,6 +21587,11 @@
         "through": "^2.3.8"
       }
     },
+    "undici-types": {
+      "version": "5.25.3",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
+      "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA=="
+    },
     "universalify": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
diff --git a/package.json b/package.json
index ebedd7e0b2b052a7068a8897c1714622e02bb2b2..b430d66509369bb0140f1fe89be3733646e59b11 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
     "test": "vitest",
     "coverage": "vitest --coverage",
     "wdio": "wdio run ./wdio.conf.ts",
+    "e2e": "npx playwright test --workers=1",
     "lint": "npx eslint ./src ./test ./electron/* --ext .ts,.vue,.js",
     "updatePreview": "set -o allexport; source .env; set +o allexport && curl --location --output public/preview.zip --header \"PRIVATE-TOKEN: $GITLAB_TOKEN\" $GITLAB_URL",
     "debug-mac": "'/Applications/ePoc Editor.app/Contents/MacOS/ePoc Editor' --remote-debugging-port=8315"
@@ -47,6 +48,8 @@
     "vuedraggable": "^4.1.0"
   },
   "devDependencies": {
+    "@playwright/test": "^1.38.1",
+    "@types/node": "^20.8.4",
     "@typescript-eslint/eslint-plugin": "^5.46.1",
     "@typescript-eslint/parser": "^5.46.1",
     "@vitejs/plugin-vue": "^4.0.0",
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..03234a936e1d87fede64468b60435eba09439106
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,78 @@
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+  testDir: './tests',
+  /* Run tests in files in parallel */
+  fullyParallel: false,
+  /* Fail the build on CI if you accidentally left test.only in the source code. */
+  forbidOnly: !!process.env.CI,
+  /* Retry on CI only */
+  retries: process.env.CI ? 2 : 0,
+  /* Opt out of parallel tests on CI. */
+  workers: process.env.CI ? 1 : undefined,
+  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+  reporter: 'html',
+  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+  use: {
+    /* Base URL to use in actions like `await page.goto('/')`. */
+    // baseURL: 'http://127.0.0.1:3000',
+
+    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+    trace: 'on-first-retry',
+    headless: true,
+  },
+
+  /* Configure projects for major browsers */
+  projects: [
+    {
+      name: 'chromium',
+      use: { ...devices['Desktop Chrome'] },
+    },
+
+    // {
+    //   name: 'firefox',
+    //   use: { ...devices['Desktop Firefox'] },
+    // },
+    //
+    // {
+    //   name: 'webkit',
+    //   use: { ...devices['Desktop Safari'] },
+    // },
+
+    /* Test against mobile viewports. */
+    // {
+    //   name: 'Mobile Chrome',
+    //   use: { ...devices['Pixel 5'] },
+    // },
+    // {
+    //   name: 'Mobile Safari',
+    //   use: { ...devices['iPhone 12'] },
+    // },
+
+    /* Test against branded browsers. */
+    // {
+    //   name: 'Microsoft Edge',
+    //   use: { ...devices['Desktop Edge'], channel: 'msedge' },
+    // },
+    // {
+    //   name: 'Google Chrome',
+    //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },
+    // },
+  ],
+
+  /* Run your local dev server before starting the tests */
+  // webServer: {
+  //   command: 'npm run start',
+  //   url: 'http://127.0.0.1:3000',
+  //   reuseExistingServer: !process.env.CI,
+  // },
+});
diff --git a/src/features/ePocFlow/nodes/ActivityNode.vue b/src/features/ePocFlow/nodes/ActivityNode.vue
index 8baeacf6782b5ef4a9c8409b3c9e80f5e92042ab..582ca0a5401968a3e795627d68c24460cd637c56 100644
--- a/src/features/ePocFlow/nodes/ActivityNode.vue
+++ b/src/features/ePocFlow/nodes/ActivityNode.vue
@@ -21,7 +21,7 @@ const props = defineProps<{
     }
 }>();
 
-const { findNode, edges } = useVueFlow({ id: 'main' });
+const { findNode, edges, nodes } = useVueFlow({ id: 'main' });
 
 const currentNode = computed(() => findNode(props.id));
 
@@ -70,12 +70,18 @@ const connectable = computed(() => {
 
 const connectedBadges = computed(() => getConnectedBadges(currentNode.value.data.contentId));
 
+const activityIndex = computed(() => {
+    const activities = nodes.value.filter((node) => node.type === 'activity');
+    return activities.findIndex(activity => activity.id === currentNode.value.id) + 1;
+});
+
 </script>
 
 <template>
     <div>
         <div 
             ref="page"
+            :data-testid="`activity-${activityIndex}`"
             class="container"
             @click.exact="openPageForm(currentNode.id, currentNode.data.formType)"
             @click.meta="closeFormPanel"
@@ -89,6 +95,7 @@ const connectedBadges = computed(() => getConnectedBadges(currentNode.value.data
             <!--suppress JSUnresolvedReference -->
             <p class="node-title" :class="{ 'active': editorStore.openedElementId ? editorStore.openedElementId === props.id : false }">{{ currentNode.data.formValues?.title || 'Activité' }}</p>
             <Handle
+                :data-testid="`target-activity-${activityIndex}`"
                 :class="{ 'not-connected': !isTarget }"
                 type="target"
                 :position="Position.Left"
@@ -99,6 +106,7 @@ const connectedBadges = computed(() => getConnectedBadges(currentNode.value.data
                 <small>{{ connectedBadges.length }}</small>
             </div>
             <DraggableNode
+                :parent-test-id="`activity-${activityIndex}`"
                 :node-id="id"
                 :contents="data.elements"
                 type="activity"
@@ -107,6 +115,7 @@ const connectedBadges = computed(() => getConnectedBadges(currentNode.value.data
             />
         </div>
         <Handle
+            :data-testid="`source-activity-${activityIndex}`"
             type="source"
             :class="{ 'not-connected': connectable }"
             :position="Position.Right"
diff --git a/src/features/ePocFlow/nodes/AddChapterNode.vue b/src/features/ePocFlow/nodes/AddChapterNode.vue
index 048942c4f33d0baedfe64b77daac35916c1293e4..44ff37717e197b7a31ce97dfa27cfda8cf6f0d10 100644
--- a/src/features/ePocFlow/nodes/AddChapterNode.vue
+++ b/src/features/ePocFlow/nodes/AddChapterNode.vue
@@ -14,7 +14,7 @@ function onClick() {
 
 <template>
     <div class="add-chapter" @mousedown="onClick()">
-        <button class="add-btn"><i class="icon-plus"></i></button>
+        <button data-testid="add-chapter" class="add-btn"><i class="icon-plus"></i></button>
     </div>
 </template>
 
diff --git a/src/features/ePocFlow/nodes/ChapterNode.vue b/src/features/ePocFlow/nodes/ChapterNode.vue
index 2b04b65cc79bae769629be6d83c3cab2fc83c7db..4ea926d308935787f4d5c9bb822731f0b5b04a47 100644
--- a/src/features/ePocFlow/nodes/ChapterNode.vue
+++ b/src/features/ePocFlow/nodes/ChapterNode.vue
@@ -21,14 +21,17 @@ const { findNode, nodes, edges } = useVueFlow({ id: 'main' });
 
 const currentNode = findNode(props.id);
 
-const subtitle = computed(() => {
+const chapterIndex = computed(() => {
     const chapters = nodes.value.filter(node => node.type === 'chapter');
+    return chapters.findIndex(chapter => chapter.id === currentNode.id) + 1;
+});
+
+const subtitle = computed(() => {
     const epocNode = findNode('1');
     const chapterParameter = epocNode?.data?.formValues?.chapterParameter || 'Chapitre';
     const label = chapterParameter.length > 8 ? chapterParameter.substring(0, 7) + '...' : chapterParameter;
-    const chapterIndex = chapters.findIndex(chapter => chapter.id === currentNode.id) + 1;
 
-    return `${label} ${chapterIndex}`;
+    return `${label} ${chapterIndex.value}`;
 });
 
 const isSource = computed(() => getConnectedEdges([currentNode], edges.value).some((edge) => edge.sourceNode.id === props.id));
@@ -75,6 +78,7 @@ const connectedBadges = computed(() => getConnectedBadges(currentNode.data.conte
             <small>{{ connectedBadges.length }}</small>
         </div>
         <ContentButton
+            :data-testid="`chapter-${chapterIndex}`"
             :icon="currentNode.data.action.icon"
             :is-draggable="false"
             :class-list="classList"
@@ -87,6 +91,7 @@ const connectedBadges = computed(() => getConnectedBadges(currentNode.data.conte
     </div>
     <!-- ! mousedown.stop important in vue-flow v1.16.4 on non draggable node -->
     <Handle
+        :data-testid="`source-chapter-${chapterIndex}`"
         type="source"
         :position="Position.Right"
         :connectable="!isSource && !editorStore.selectNodeMode"
diff --git a/src/features/ePocFlow/nodes/PageNode.vue b/src/features/ePocFlow/nodes/PageNode.vue
index f6697e803297d1eb22a7033853a255751601f840..084f6c163d2a762c51bcde845569676337f1f3a1 100644
--- a/src/features/ePocFlow/nodes/PageNode.vue
+++ b/src/features/ePocFlow/nodes/PageNode.vue
@@ -22,7 +22,7 @@ const props = defineProps<{
     }
 }>();
 
-const { findNode, edges } = useVueFlow({ id: 'main' });
+const { findNode, edges, nodes } = useVueFlow({ id: 'main' });
 
 const currentNode = computed(() => findNode(props.id));
 
@@ -72,12 +72,18 @@ const connectable = computed(() => {
 
 const connectedBadges = computed(() => getConnectedBadges(currentNode.value.data.contentId));
 
+const pageIndex = computed(() => {
+    const pages = nodes.value.filter((node) => node.type === 'page');
+    return pages.findIndex((page: any) => page.id === currentNode.value.id) + 1;
+});
+
 </script>
 
 <template>
     <div>
-        <div 
+        <div
             ref="page"
+            :data-testid="`page-${pageIndex}`"
             class="container"
             @click.exact="openPageForm(currentNode.id, currentNode.data.formType)"
             @click.meta="closeFormPanel"
@@ -91,6 +97,7 @@ const connectedBadges = computed(() => getConnectedBadges(currentNode.value.data
             <!--suppress JSUnresolvedReference -->
             <p class="node-title" :class="{ 'active': editorStore.openedElementId ? editorStore.openedElementId === props.id : false }">{{ currentNode.data.formValues?.title || 'Page' }}</p>
             <Handle
+                :data-testid="`target-page-${pageIndex}`"
                 :class="{ 'not-connected': !isTarget }"
                 type="target"
                 :position="Position.Left"
@@ -101,6 +108,7 @@ const connectedBadges = computed(() => getConnectedBadges(currentNode.value.data
                 <small>{{ connectedBadges.length }}</small>
             </div>
             <DraggableNode
+                :parent-test-id="`page-${pageIndex}`"
                 :node-id="id"
                 :contents="data.elements"
                 type="page"
@@ -109,6 +117,7 @@ const connectedBadges = computed(() => getConnectedBadges(currentNode.value.data
             />
         </div>
         <Handle
+            :data-testid="`source-page-${pageIndex}`"
             type="source"
             :class="{ 'not-connected': connectable }"
             :position="Position.Right"
diff --git a/src/features/ePocFlow/nodes/content/DraggableNode.vue b/src/features/ePocFlow/nodes/content/DraggableNode.vue
index 46aaa9f9f69f7d7ff90f7e1f1ee44b8099fd81f7..4b482457063078e66dc6ddcd72ed86c4e084f251 100644
--- a/src/features/ePocFlow/nodes/content/DraggableNode.vue
+++ b/src/features/ePocFlow/nodes/content/DraggableNode.vue
@@ -17,6 +17,7 @@ const props = defineProps<{
     nodeId: string;
     contents: NodeElement[];
     type: 'page' | 'activity';
+    parentTestId: string;
 }>();
 
 const emit = defineEmits<{
@@ -129,6 +130,7 @@ function onContextMenu(contentId: string) {
                     <small>{{ getConnectedBadges(element.contentId).length }}</small>
                 </div>
                 <ContentButton
+                    :data-testid="`${parentTestId}-${index}`"
                     :icon="element.action.icon"
                     :is-draggable="!isCondition && !editorStore.selectNodeMode"
                     :is-active="editorStore.openedElementId ? editorStore.openedElementId === element.id : false"
diff --git a/src/features/ePocFlow/nodes/ePocNode.vue b/src/features/ePocFlow/nodes/ePocNode.vue
index 4482bcfab61f4cde0286f217508c48289ff6e1eb..3d981dbc2b80bfefe5388b5207b2fd1a4b6ea901 100644
--- a/src/features/ePocFlow/nodes/ePocNode.vue
+++ b/src/features/ePocFlow/nodes/ePocNode.vue
@@ -44,7 +44,8 @@ function onContextMenu() {
 
 <template>
     <div>
-        <ContentButton 
+        <ContentButton
+            data-testid="epoc-node"
             :icon="currentNode.data.action.icon"
             :is-draggable="false"
             :class-list="classList"
diff --git a/src/features/sideBar/components/SideActions.vue b/src/features/sideBar/components/SideActions.vue
index be2a1d9fb15412b47fc12c8b13cc2085ee985c10..9ff7cc01840bc6dbebee8bf15cfe69c911e327b1 100644
--- a/src/features/sideBar/components/SideActions.vue
+++ b/src/features/sideBar/components/SideActions.vue
@@ -66,6 +66,7 @@ function showTemplateMenu() {
                 <ContentButton
                     :key="index"
                     v-tippy="{content: element.tooltip, placement: 'right', arrow : true, arrowType : 'round', animation : 'fade'}"
+                    :data-testid="`${element.type}-content`"
                     :icon="element.icon"
                     :is-draggable="true"
                     :class-list="{ 'btn-content-blue': true }"
@@ -78,13 +79,15 @@ function showTemplateMenu() {
         <!--suppress VueUnrecognizedDirective -->
         <ContentButton
             v-tippy="{content: questionContent.tooltip, placement: 'right', arrow : true, arrowType : 'round', animation : 'fade'}"
+            data-testid="questions-menu"
             :icon="questionContent.icon"
             :is-draggable="false"
             :class-list="classList(questionContent)"
             :is-active="editorStore.questionMenu"
+            @mouseup.stop
             @click="showQuestionsMenu"
         />
-        <div v-if="editorStore.questionMenu" class="floating-menu" @click.stop>
+        <div v-if="editorStore.questionMenu" data-testid="floating-menu" class="floating-menu" @click.stop>
             <div class="arrow-wrapper">
                 <div class="arrow">
                 </div>
@@ -103,6 +106,7 @@ function showTemplateMenu() {
                         <ContentButton
                             :key="index"
                             v-tippy="{content: element.label, placement: 'right', arrow : true, arrowType : 'round', animation : 'fade'}"
+                            :data-testid="`${element.type}-content`"
                             :icon="element.icon"
                             :class-list="{ 'btn-content-blue': true }"
                             :is-draggable="true"
diff --git a/tests/data/form/contentForm.data.ts b/tests/data/form/contentForm.data.ts
new file mode 100644
index 0000000000000000000000000000000000000000..21aaf1202e53449a26163995cfd883979ceea84b
--- /dev/null
+++ b/tests/data/form/contentForm.data.ts
@@ -0,0 +1,30 @@
+import { TestForm } from '@/tests/types';
+
+const textForm: TestForm = {
+    type: 'text',
+    inputs: [
+        {
+            label: 'Résumé',
+            value: 'Résumé test',
+            type: 'html'
+        },
+    ]
+};
+
+const videoForm: TestForm = {
+    type: 'video',
+    inputs: [
+        {
+            label: 'Résumé',
+            value: 'Résumé test',
+            type: 'html'
+        }
+    ]
+};
+
+const audioForm: TestForm = {
+    type: 'audio',
+    inputs: []
+};
+
+export const contentForms = [textForm, videoForm, audioForm];
\ No newline at end of file
diff --git a/tests/data/form/index.ts b/tests/data/form/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..68c607b49f12329352d70af4b538d9d1dbd9e76a
--- /dev/null
+++ b/tests/data/form/index.ts
@@ -0,0 +1,5 @@
+import { nodeForms } from './nodeForm.data';
+import { contentForms } from '@/tests/data/form/contentForm.data';
+import { questionForms } from '@/tests/data/form/questionForm.data';
+
+export const forms = [...nodeForms, ...contentForms, ...questionForms];
\ No newline at end of file
diff --git a/tests/data/form/nodeForm.data.ts b/tests/data/form/nodeForm.data.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e17a93a0d9e868d4608a62831f2e1fa8e0889b63
--- /dev/null
+++ b/tests/data/form/nodeForm.data.ts
@@ -0,0 +1,102 @@
+import { TestForm } from '@/tests/types';
+
+const epocForm: TestForm = {
+    type: 'epoc',
+    inputs: [
+        {
+            label: 'ID de l\'ePoc',
+            value: 'TestID',
+            type: 'text'
+        },
+        {
+            label: 'Edition',
+            value: '2023',
+            type: 'text'
+        },
+        {
+            label: 'Titre',
+            value: 'ePoc test',
+            type: 'text'
+        },
+        {
+            label: 'Présentation',
+            value: 'Cet ePoc à été généré automatiquement',
+            type: 'html'
+        },
+        {
+            label: 'Nombre de badge pour obtenir l\'attestation',
+            value: '5',
+            type: 'score'
+        }
+    ]
+};
+
+const chapterForm: TestForm = {
+    type: 'chapter',
+    inputs: [
+        {
+            label: 'Titre',
+            value: 'Chapitre test',
+            type: 'text'
+        }
+    ]
+};
+
+const pageForm: TestForm = {
+    type: 'page',
+    inputs: [
+        {
+            label: 'Titre',
+            value: 'Titre test',
+            type: 'text'
+        },
+        {
+            label: 'Sous-titre',
+            value: 'Sous-titre test',
+            type: 'text'
+        },
+        {
+            label: 'Caché dans la table des matières',
+            value: true,
+            type: 'checkbox'
+        },
+        {
+            label: 'Ne s\'affiche qu\'a certaines conditions',
+            value: true,
+            type: 'checkbox'
+        }
+    ]
+};
+
+const activityForm: TestForm = {
+    type: 'activity',
+    inputs: [
+        {
+            label: 'Titre',
+            value: 'Titre test',
+            type: 'text'
+        },
+        {
+            label: 'Sous-titre',
+            value: 'Sous-titre test',
+            type: 'text'
+        },
+        {
+            label: 'Résumé',
+            value: 'Résumé test',
+            type: 'textarea'
+        },
+        {
+            label: 'Caché dans la table des matières',
+            value: true,
+            type: 'checkbox'
+        },
+        {
+            label: 'Ne s\'affiche qu\'a certaines conditions',
+            value: true,
+            type: 'checkbox'
+        }
+    ]
+};
+
+export const nodeForms = [epocForm, chapterForm, pageForm, activityForm];
\ No newline at end of file
diff --git a/tests/data/form/questionForm.data.ts b/tests/data/form/questionForm.data.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cd58cb2af980a19839fe78569b271e14cf9c5ec0
--- /dev/null
+++ b/tests/data/form/questionForm.data.ts
@@ -0,0 +1,108 @@
+import { TestForm } from '@/tests/types';
+
+const choiceForm: TestForm = {
+    type: 'choice',
+    inputs: [
+        {
+            label: 'Score',
+            value: '5',
+            type: 'score'
+        },
+        {
+            label: 'Question',
+            value: 'Question test',
+            type: 'textarea'
+        },
+        {
+            label: 'Consigne',
+            value: 'Consigne test',
+            type: 'textarea'
+        },
+    ]
+};
+
+const dragAndDropForm: TestForm = {
+    type: 'drag-and-drop',
+    inputs: [
+        {
+            label: 'Score',
+            value: '5',
+            type: 'score'
+        },
+        {
+            label: 'Question',
+            value: 'Question test',
+            type: 'textarea'
+        },
+        {
+            label: 'Consigne',
+            value: 'Consigne test',
+            type: 'textarea'
+        }
+    ]
+};
+
+const reorderForm: TestForm = {
+    type: 'reorder',
+    inputs: [
+        {
+            label: 'Score',
+            value: '5',
+            type: 'score'
+        },
+        {
+            label: 'Question',
+            value: 'Question test',
+            type: 'textarea'
+        },
+        {
+            label: 'Consigne',
+            value: 'Consigne test',
+            type: 'textarea'
+        }
+    ]
+};
+
+const swipeForm: TestForm = {
+    type: 'swipe',
+    inputs: [
+        {
+            label: 'Score',
+            value: '5',
+            type: 'score'
+        },
+        {
+            label: 'Question',
+            value: 'Question test',
+            type: 'textarea'
+        },
+        {
+            label: 'Consigne',
+            value: 'Consigne test',
+            type: 'textarea'
+        }
+    ]
+};
+
+const dropdownListForm: TestForm = {
+    type: 'dropdown-list',
+    inputs: [
+        {
+            label: 'Score',
+            value: '5',
+            type: 'score'
+        },
+        {
+            label: 'Question',
+            value: 'Question test',
+            type: 'textarea'
+        },
+        {
+            label: 'Consigne',
+            value: 'Consigne test',
+            type: 'textarea'
+        }
+    ]
+};
+
+export const questionForms = [choiceForm, dragAndDropForm, reorderForm, swipeForm, dropdownListForm];
\ No newline at end of file
diff --git a/tests/data/graph.data.ts b/tests/data/graph.data.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0a3888431e60629abf3f4c87c49c8d1471e470f6
--- /dev/null
+++ b/tests/data/graph.data.ts
@@ -0,0 +1,58 @@
+import { TestNode } from '../types';
+
+let chapterIndex = 0;
+let pageIndex = 0;
+let activityIndex = 0;
+
+/*
+    This array is used to create an ePoc within the tests.
+    The nodes are placed following the order of the array, and connected to the previous node.
+ */
+export const nodes: TestNode[] = [
+    {
+        type: 'chapter',
+        index: ++chapterIndex
+    },
+    {
+        type: 'page',
+        index: ++pageIndex,
+        contents: [
+            { type: 'text' }
+        ]
+    },
+    {
+        type: 'page',
+        index: ++pageIndex,
+        contents: [
+            { type: 'video' }
+        ]
+    },
+    {
+        type: 'activity',
+        index: ++activityIndex,
+        contents: [
+            { type: 'choice' },
+            { type: 'drag-and-drop' }
+        ]
+    },
+    {
+        type: 'chapter',
+        index: ++chapterIndex
+    },
+    {
+        type: 'page',
+        index: ++pageIndex,
+        contents: [
+            { type: 'audio' }
+        ]
+    },
+    {
+        type: 'activity',
+        index: ++activityIndex,
+        contents: [
+            { type: 'reorder' },
+            { type: 'swipe' },
+            { type: 'dropdown-list' }
+        ]
+    }
+];
\ No newline at end of file
diff --git a/tests/data/index.ts b/tests/data/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7677a859b0740b6adf1adeb026a3815291e9ecec
--- /dev/null
+++ b/tests/data/index.ts
@@ -0,0 +1,2 @@
+export * from './graph.data';
+export * from './form';
\ No newline at end of file
diff --git a/tests/main.spec.ts b/tests/main.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9adae74e078ef87907016f46887dba6ab34bcc9c
--- /dev/null
+++ b/tests/main.spec.ts
@@ -0,0 +1,71 @@
+import { test } from '@playwright/test';
+const { _electron: electron } = require('playwright');
+
+import { sleep, addChapter, createLinkedNode, fillForm, addContentToNode } from '@/tests/utils';
+import { forms, nodes } from '@/tests/data';
+
+let electronApp;
+let window;
+
+test.describe('Create a new ePoc', () => {
+    test.beforeAll(async () => {
+        electronApp = await electron.launch({ args: ['electron/electron.js', '--headless=true']});
+        // electronApp = await electron.launch({ args: ['electron/electron.js']});
+        
+        window = await new Promise((resolve) => {
+            electronApp.on('window', page => {
+                if (page.url().includes('index.html')) {
+                    resolve(page);
+                }
+            });
+        });
+    });
+
+    test.afterAll(async () => {
+        // await sleep(2000);
+        await electronApp.close();
+    });
+
+    test('Create a new project', (async () => {
+        await window.getByText('Créer un nouveau projet').click();
+    }));
+
+    test.describe('Creating the graph', () => {
+        for(let i = 0; i < nodes.length; i++) {
+            test(`Add ${nodes[i].type} ${nodes[i].index}`, (async () => {
+                if(nodes[i].type === 'chapter') {
+                    await addChapter(window);
+                } else {
+                    await createLinkedNode(window, nodes[i - 1], nodes[i]);
+                    if(nodes[i].contents.length > 1) {
+                        for(let j = 1; j < nodes[i].contents.length; j++) {
+                            await addContentToNode(window, nodes[i].contents[j].type, nodes[i], j);
+                        }
+                    }
+                }
+            }));
+        }
+    });
+
+    test.describe('Filling forms', () => {
+        test('Filling ePoc form', async () => {
+            const epocForm = forms.find(form => form.type === 'epoc');
+            await fillForm(window, epocForm, 'epoc-node');
+        });
+        
+        for(const node of nodes) {
+            test(`Filling ${node.type}-${node.index} form`, async () => {
+                const testId = `${node.type}-${node.index}`;
+                const form = forms.find(form => form.type === node.type);
+                await fillForm(window, form, testId);
+                
+                if(node.type === 'page' || node.type === 'activity') {
+                    for(const [index,content] of node.contents.entries()) {
+                        const contentForm = forms.find(form => form.type === content.type);
+                        await fillForm(window, contentForm, `${testId}-${index}`);
+                    }
+                }
+            });
+        }
+    });
+});
\ No newline at end of file
diff --git a/tests/types/index.ts b/tests/types/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..822d7263a863f31a10a08cf7ac88b3c55a5a3f45
--- /dev/null
+++ b/tests/types/index.ts
@@ -0,0 +1 @@
+export * from './test.type'
\ No newline at end of file
diff --git a/tests/types/test.type.ts b/tests/types/test.type.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2567d4879a5466eade00b4f16b5bc2860807a1c7
--- /dev/null
+++ b/tests/types/test.type.ts
@@ -0,0 +1,27 @@
+export type PageContent = 'text' | 'video' | 'audio'
+
+const questions = ['choice', 'drag-and-drop', 'reorder', 'swipe', 'dropdown-list'] as const;
+export type Question = typeof questions[number];
+
+export function isQuestion(value) {
+    return questions.includes(value);
+}
+
+export type Content = PageContent | Question
+
+export interface TestNode {
+    type: 'chapter' | 'page' | 'activity';
+    index: number;
+    contents?: { type: Content }[];
+}
+
+export interface TestInput {
+    label: string;
+    value: string | boolean;
+    type: 'text' | 'html' | 'checkbox' | 'score' | 'textarea';
+}
+
+export interface TestForm {
+    type: 'page' | 'epoc' | 'activity' | 'text' | 'audio' | 'video' | 'chapter' | 'choice' | 'drag-and-drop' | 'reorder' | 'swipe' | 'dropdown-list';
+    inputs: TestInput[];
+}
\ No newline at end of file
diff --git a/tests/utils/form.ts b/tests/utils/form.ts
new file mode 100644
index 0000000000000000000000000000000000000000..77bae28606d7dbfdc3296626c245de55e498332d
--- /dev/null
+++ b/tests/utils/form.ts
@@ -0,0 +1,47 @@
+import { TestForm } from '@/tests/types';
+
+async function openForm(window, testId: string) {
+    const element = await window.getByTestId(testId);
+    const elementBox = await element.boundingBox();
+    
+    const clickLocation = {
+        x: elementBox.x + 10,
+        y: elementBox.y + 10
+    };
+    await window.mouse.click(clickLocation.x, clickLocation.y);
+}
+
+export async function fillForm(window, form: TestForm, testId: string) {
+    await openForm(window, testId);
+    
+    //TODO: detect form type
+    
+    for(const input of form.inputs) {
+        switch(input.type) {
+            case 'html': {
+                const label = await window.getByText(input.label);
+                await label.click();
+                await label.pressSequentially(input.value as string);
+                break;
+            }
+        
+            case 'checkbox': {
+                const checkbox = await window.getByLabel(input.label);
+                if(input.value) await checkbox.check();
+                break;
+            }
+            
+            case 'score': {
+                const score = await window.getByLabel(input.label);
+                await score.type(input.value as string);
+                break;
+            }
+            
+            default: {
+                const inputLabel = await window.getByLabel(input.label, { exact: true });
+                await inputLabel.fill(input.value as string);
+                break;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/utils/graph.ts b/tests/utils/graph.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c5cd7038b678877407ee90c592c5baf658de91ed
--- /dev/null
+++ b/tests/utils/graph.ts
@@ -0,0 +1,89 @@
+import { TestNode, isQuestion } from '../types';
+
+export async function sleep(ms) {
+    return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+export async function dragAndDrop(window, source, dropLocation) {
+    await source.hover();
+    await window.mouse.down();
+    await window.mouse.move(dropLocation.x, dropLocation.y);
+    await window.mouse.up();
+}
+
+export async function dragAndDropTo(window, source, target) {
+    await window.mouse.move(source.x, source.y);
+    await window.mouse.down();
+    await window.mouse.move(target.x, target.y);
+    await window.mouse.up();
+}
+
+export async function addChapter(window) {
+    await window.getByTestId('add-chapter').click();
+}
+
+export async function createLinkedNode(window, sourceNode: TestNode, newNode: TestNode ) {
+    const questionMenu = await window.getByTestId('questions-menu');
+    
+    if(newNode.type === 'activity') await questionMenu.click();
+    
+    const sideAction = await window.getByTestId(`${newNode.contents[0].type}-content`);
+    
+    const sourceNodeBox = await window.getByTestId(`${sourceNode.type}-${sourceNode.index}`).boundingBox();
+
+    const dropLocation = {
+        x: sourceNodeBox.x + sourceNodeBox.width + 100,
+        y: sourceNodeBox.y + sourceNodeBox.height / 2
+    };
+
+    await dragAndDrop(window, sideAction, dropLocation);
+
+    await linkNodes(window, sourceNode, newNode);
+}
+
+/*
+    This function is used to add content to a page.
+    ! Doesn't work for the moment, not sure i can make it work.
+ */
+export async function addContentToNode(window, type, node: TestNode, pos: number) {
+    const questionMenu = await window.getByTestId('questions-menu');
+    
+    if(isQuestion(type)) await questionMenu.click();
+    
+    const sideAction = await window.getByTestId(`${type}-content`);
+    
+    const contentBox = await window.getByTestId(`${node.type}-${node.index}-${pos-1}`).boundingBox();
+    
+    const dropLocation = {
+        x: contentBox.x + 10,
+        y: contentBox.y + contentBox.height - 10
+    };
+    
+
+    await dragAndDrop(window, sideAction, dropLocation);
+    
+    const floatingMenu = await window.getByTestId('floating-menu');
+    if(await floatingMenu.isVisible()) await questionMenu.click();
+}
+
+export async function linkNodes(window, sourceNode: TestNode, targetNode: TestNode) {
+    const sourceHandle = await window.getByTestId(`source-${sourceNode.type}-${sourceNode.index}`);
+    const targetHandle = await window.getByTestId(`target-${targetNode.type}-${targetNode.index}`);
+
+    const sourceBox = await sourceHandle.boundingBox();
+    const targetBox = await targetHandle.boundingBox();
+
+    const sourceLocation = {
+        x: sourceBox.x + sourceBox.width / 2,
+        y: sourceBox.y + sourceBox.height / 2
+    };
+
+    const targetLocation = {
+        x: targetBox.x + targetBox.width / 2,
+        y: targetBox.y + targetBox.height / 2
+    };
+
+    if(!sourceHandle || !targetHandle) return;
+
+    await dragAndDropTo(window, sourceLocation, targetLocation);
+}
\ No newline at end of file
diff --git a/tests/utils/index.ts b/tests/utils/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3e04cbbc47c0f58ebcf14e8f06998c2b71e33065
--- /dev/null
+++ b/tests/utils/index.ts
@@ -0,0 +1,2 @@
+export * from './graph';
+export * from './form';
\ No newline at end of file