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