diff --git a/Jenkinsfile b/Jenkinsfile index 05ef34791..a9db9d369 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -125,8 +125,10 @@ pipeline { sh "docker-compose ${env.COMPOSE_PARAMS} pull db" sh "docker-compose ${env.COMPOSE_PARAMS} up -d" + def modules = sh(script: 'node test/cypress/docker/find/find.js', returnStdout: true).trim() + echo "E2E MODULES: ${modules}" image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") { - sh 'sh test/cypress/cypressParallel.sh 1' + sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'" } } } @@ -183,3 +185,4 @@ pipeline { } } } + diff --git a/package.json b/package.json index 28eb6931e..beb15ade3 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "axios": "^1.4.0", "chromium": "^3.0.3", "croppie": "^2.6.5", + "es-module-lexer": "^1.6.0", + "fast-glob": "^3.3.3", "moment": "^2.30.1", "pinia": "^2.1.3", "quasar": "^2.17.7", @@ -50,6 +52,7 @@ "@quasar/app-vite": "^2.0.8", "@quasar/quasar-app-extension-qcalendar": "^4.0.2", "@quasar/quasar-app-extension-testing-unit-vitest": "^0.4.0", + "@vue/compiler-sfc": "^3.5.13", "@vue/test-utils": "^2.4.4", "autoprefixer": "^10.4.14", "cypress": "^14.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99ec0544e..538af33b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,12 @@ dependencies: croppie: specifier: ^2.6.5 version: 2.6.5 + es-module-lexer: + specifier: ^1.6.0 + version: 1.6.0 + fast-glob: + specifier: ^3.3.3 + version: 3.3.3 moment: specifier: ^2.30.1 version: 2.30.1 @@ -70,6 +76,9 @@ devDependencies: '@quasar/quasar-app-extension-testing-unit-vitest': specifier: ^0.4.0 version: 0.4.0(@vue/test-utils@2.4.6)(quasar@2.17.7)(typescript@5.7.3)(vite@6.2.0)(vitest@0.34.6)(vue@3.5.13) + '@vue/compiler-sfc': + specifier: ^3.5.13 + version: 3.5.13 '@vue/test-utils': specifier: ^2.4.4 version: 2.4.6 @@ -1575,12 +1584,10 @@ packages: dependencies: '@nodelib/fs.stat': 2.0.5 run-parallel: 1.2.0 - dev: true /@nodelib/fs.stat@2.0.5: resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} engines: {node: '>= 8'} - dev: true /@nodelib/fs.walk@1.2.8: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} @@ -1588,7 +1595,6 @@ packages: dependencies: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.0 - dev: true /@one-ini/wasm@0.1.1: resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} @@ -4382,6 +4388,10 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + /es-module-lexer@1.6.0: + resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + dev: false + /es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -4954,7 +4964,6 @@ packages: glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.8 - dev: true /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -4972,7 +4981,6 @@ packages: resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==} dependencies: reusify: 1.0.4 - dev: true /fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -5299,7 +5307,6 @@ packages: engines: {node: '>= 6'} dependencies: is-glob: 4.0.3 - dev: true /glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} @@ -6463,7 +6470,6 @@ packages: /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - dev: true /merge@2.1.1: resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} @@ -7317,7 +7323,6 @@ packages: /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true /quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} @@ -7571,7 +7576,6 @@ packages: /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true /rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -7663,7 +7667,6 @@ packages: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: queue-microtask: 1.2.3 - dev: true /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} diff --git a/test/cypress/cypressParallel.sh b/test/cypress/cypressParallel.sh deleted file mode 100644 index 8ef26bcde..000000000 --- a/test/cypress/cypressParallel.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -find 'test/cypress/integration' \ - -mindepth 1 \ - -maxdepth 1 \ - -type d | \ -xargs -P "$1" -I {} sh -c ' - echo "🔷 {}" && - xvfb-run -a cypress run \ - --headless \ - --spec "{}" \ - --quiet \ - > /dev/null -' -wait diff --git a/test/cypress/docker/cypressParallel.sh b/test/cypress/docker/cypressParallel.sh new file mode 100644 index 000000000..8e253f1e3 --- /dev/null +++ b/test/cypress/docker/cypressParallel.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +echo $2 +if [ -z "$2" ]; then + TEST_DIRS=$(find 'test/cypress/integration' -mindepth 1 -maxdepth 1 -type d) +else + TEST_DIRS=$2 +fi + +echo $TEST_DIRS x$1 + +echo "$TEST_DIRS" | xargs -P "$1" -I {} sh -c ' + echo "🔷 {}" && + xvfb-run -a cypress run \ + --headless \ + --spec "{}" \ + --quiet \ + > /dev/null +' +wait diff --git a/test/cypress/docker/find/find-imports.js b/test/cypress/docker/find/find-imports.js new file mode 100644 index 000000000..39c3ac3eb --- /dev/null +++ b/test/cypress/docker/find/find-imports.js @@ -0,0 +1,52 @@ +import fs from 'fs'; +import { parse } from 'es-module-lexer'; +import { parse as vueParse } from '@vue/compiler-sfc'; +import glob from 'fast-glob'; +import { resolveImportPath, toRelative } from './resolve-import-path.js'; + +const ROUTER_MODULES = 'src/router/modules/'; +const files = await glob(['src/**/*.{vue,js,ts}'], { absolute: true }); +const vueFiles = new Map(); + +export async function findImports(targetFile, visited = new Set(), identation = '') { + if (visited.has(targetFile)) return []; // Avoid infinite loops + visited.add(targetFile); + + const usageFiles = files + .filter((file) => { + let content = fs.readFileSync(file, 'utf8'); + if (file.endsWith('.vue')) { + if (vueFiles.has(file)) { + content = vueFiles.get(file); + } else { + const { descriptor } = vueParse(content); + content = descriptor?.scriptSetup?.content ?? ''; + vueFiles.set(file, content); + } + } + if (!content.trim()) return false; + + return parse(content)[0].some((imp) => { + if (!imp?.n) return false; + return resolveImportPath(imp.n, targetFile) === targetFile; + }); + }) + .map((file) => toRelative(file)); + + let fullTree = [...usageFiles]; + for (const file of usageFiles) { + if (file.startsWith(ROUTER_MODULES)) { + continue; + } + fullTree = [ + ...fullTree, + ...(await findImports(file, visited, identation + ' ')), + ]; + } + + return getUniques(fullTree); // Remove duplicates +} + +function getUniques(array) { + return Array.from(new Set(array)); +} diff --git a/test/cypress/docker/find/find.js b/test/cypress/docker/find/find.js new file mode 100644 index 000000000..ae463f333 --- /dev/null +++ b/test/cypress/docker/find/find.js @@ -0,0 +1,35 @@ +import { execSync } from 'child_process'; +import { findImports } from './find-imports.js'; +import { getModules } from './get-modules.js'; +const E2E_PATH = 'test/cypress/integration'; +const FINDED_PATHS = ['src', E2E_PATH]; + +function getGitDiff(options) { + const TARGET_BRANCH = options[2] || 'dev'; + const diff = execSync(`git diff --name-only origin/${TARGET_BRANCH}`, { + encoding: 'utf-8', + }); + return diff.split('\n'); +} + +async function getChangedModules() { + let changedModules = new Set(); + const changes = getGitDiff(process.argv); + for (const change of changes) { + if (!FINDED_PATHS.some((prefix) => change.startsWith(prefix))) return ''; + const changedArray = [ + ...changedModules, + ...new Set(getModules(await findImports(change))), + ]; + if (change.startsWith(E2E_PATH)) changedArray.push(change); + changedModules = new Set(changedArray); + } + return [...changedModules].join(' '); +} + +getChangedModules() + .then((modules) => console.log(modules)) // is return + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/test/cypress/docker/find/get-modules.js b/test/cypress/docker/find/get-modules.js new file mode 100644 index 000000000..4ac9ec8c4 --- /dev/null +++ b/test/cypress/docker/find/get-modules.js @@ -0,0 +1,13 @@ +export function getModules(files) { + const CYPRESS_PREFIX = 'test/cypress/integration/'; + const CYPRESS_SUFIX = '/**/*.spec.js'; + const modules = []; + for (const file of files) { + if (file.startsWith('src/page')) { + modules.push( + CYPRESS_PREFIX + file.split('/')[2].toLowerCase() + CYPRESS_SUFIX, + ); + } + } + return modules; +} diff --git a/test/cypress/docker/find/resolve-import-path.js b/test/cypress/docker/find/resolve-import-path.js new file mode 100644 index 000000000..38c225fd2 --- /dev/null +++ b/test/cypress/docker/find/resolve-import-path.js @@ -0,0 +1,34 @@ +import fs from 'fs'; +import path from 'path'; +const rootDir = process.cwd(); +const config = JSON.parse(fs.readFileSync('jsconfig.json', 'utf-8')); +const { paths, baseUrl } = config.compilerOptions; + +function resolveImportPath(importPath, fileBase) { + if (!importPath) return null; + importPath = jsConfigPaths(importPath); + const fileDir = path.dirname(fileBase); + if (importPath.startsWith('.') || importPath.startsWith('/')) { + return path.relative(rootDir, path.resolve(fileDir, importPath)); + } + + return importPath; +} +function toRelative(file) { + return path.relative(rootDir, file); +} + +function jsConfigPaths(importPath) { + for (const [aliasPattern, [target]] of Object.entries(paths)) { + const alias = aliasPattern.replace('/*', ''); + const targetBase = target.replace('/*', ''); + + if (importPath.startsWith(alias)) { + const rest = importPath.slice(alias.length); + return path.resolve(baseUrl, targetBase + rest); + } + } + return importPath; +} + +export { resolveImportPath, toRelative }; diff --git a/test/cypress/run.sh b/test/cypress/docker/run.sh similarity index 89% rename from test/cypress/run.sh rename to test/cypress/docker/run.sh index 0f8c59902..6de127790 100755 --- a/test/cypress/run.sh +++ b/test/cypress/docker/run.sh @@ -35,6 +35,8 @@ docker build -f ./docs/Dockerfile.dev -t lilium-dev . # END IMAGES docker-compose -p e2e --project-directory . -f test/cypress/docker-compose.yml up -d +files=$(node test/cypress/docker/find/find.js) +echo $files docker run -it --rm \ -v "$(pwd)":/app \ @@ -42,6 +44,6 @@ docker run -it --rm \ -e CI \ -e TZ \ lilium-dev \ - bash -c 'sh test/cypress/cypressParallel.sh 2' + bash -c "sh test/cypress/docker/cypressParallel.sh 2 $files" cleanup diff --git a/test/cypress/summary.sh b/test/cypress/docker/summary.sh similarity index 100% rename from test/cypress/summary.sh rename to test/cypress/docker/summary.sh diff --git a/test/cypress/integration/outLogin/login.spec.js b/test/cypress/integration/login/login.spec.js similarity index 100% rename from test/cypress/integration/outLogin/login.spec.js rename to test/cypress/integration/login/login.spec.js diff --git a/test/cypress/integration/outLogin/logout.spec.js b/test/cypress/integration/login/logout.spec.js similarity index 100% rename from test/cypress/integration/outLogin/logout.spec.js rename to test/cypress/integration/login/logout.spec.js diff --git a/test/cypress/integration/outLogin/recoverPassword.spec.js b/test/cypress/integration/login/recoverPassword.spec.js similarity index 100% rename from test/cypress/integration/outLogin/recoverPassword.spec.js rename to test/cypress/integration/login/recoverPassword.spec.js diff --git a/test/cypress/integration/outLogin/twoFactor.spec.js b/test/cypress/integration/login/twoFactor.spec.js similarity index 100% rename from test/cypress/integration/outLogin/twoFactor.spec.js rename to test/cypress/integration/login/twoFactor.spec.js diff --git a/test/cypress/integration/Order/orderCatalog.spec.js b/test/cypress/integration/order/orderCatalog.spec.js similarity index 100% rename from test/cypress/integration/Order/orderCatalog.spec.js rename to test/cypress/integration/order/orderCatalog.spec.js diff --git a/test/cypress/integration/Supplier/SupplierBalance.spec.js b/test/cypress/integration/supplier/SupplierBalance.spec.js similarity index 100% rename from test/cypress/integration/Supplier/SupplierBalance.spec.js rename to test/cypress/integration/supplier/SupplierBalance.spec.js