diff --git a/.commitlintrc.json b/.commitlintrc.json index 55f118d12e..d1b71dd705 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -2,6 +2,7 @@ "extends": ["@commitlint/config-conventional"], "rules": { "type-enum": [2, "always", ["ci", "chore", "docs", "feat", "fix", "perf", "refactor", "revert", "test"]], - "body-max-line-length": [2, "always", 500] + "body-max-line-length": [2, "always", 500], + "header-max-length": [2, "always", 200] } } diff --git a/.github/actions/upload-test-artifacts/action.yaml b/.github/actions/upload-test-artifacts/action.yaml index 63f604bb3a..29d79afaf1 100644 --- a/.github/actions/upload-test-artifacts/action.yaml +++ b/.github/actions/upload-test-artifacts/action.yaml @@ -37,3 +37,10 @@ runs: name: playwright-test-results-${{ inputs.browser }}-${{ inputs.run-id }} path: test-results/ retention-days: 2 + + - name: Upload Connection Test Data + uses: actions/upload-artifact@v4 + with: + name: connection-test-data-${{ inputs.browser }}-${{ inputs.run-id }} + path: e2e/fixtures/connectionsTestCases.json + retention-days: 2 diff --git a/.github/workflows/build_test_and_release.yml b/.github/workflows/build_test_and_release.yml index 57c585b58f..405ea54272 100644 --- a/.github/workflows/build_test_and_release.yml +++ b/.github/workflows/build_test_and_release.yml @@ -142,9 +142,10 @@ jobs: - name: Run Playwright tests env: TESTS_JWT_AUTH_TOKEN: ${{ secrets.TESTS_JWT_AUTH_TOKEN }} + VITE_DESCOPE_PROJECT_ID: ${{ secrets.VITE_DESCOPE_PROJECT_ID }} VITE_DISPLAY_CHATBOT: true CI: true - run: npx playwright test --project=${{ matrix.browser }} + run: npx playwright test ${{ env.TEST_FILE || '' }} --project=${{ matrix.browser }} - name: Upload test artifacts if: always() @@ -157,7 +158,6 @@ jobs: name: 🎨 Visual Regression Tests needs: setup runs-on: ubuntu-latest - if: false # Temporarily disabled timeout-minutes: 30 continue-on-error: true @@ -206,6 +206,7 @@ jobs: - name: Run visual regression tests env: TESTS_JWT_AUTH_TOKEN: ${{ secrets.TESTS_JWT_AUTH_TOKEN }} + VITE_DESCOPE_PROJECT_ID: ${{ secrets.VITE_DESCOPE_PROJECT_ID }} VITE_DISPLAY_CHATBOT: true run: npx playwright test project/connections/buttonPresence.visual.spec.ts --project="Visual Regression" --workers=1 @@ -213,7 +214,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: visual-regression-results + name: visual-regression-results-${{ github.run_id }} path: | test-results/ playwright-report/ @@ -253,7 +254,6 @@ jobs: run: | git submodule update --remote npm run type-check - npm run generate:connection-test-data npm run lint:ci npm run build echo '!dist' >> .gitignore @@ -261,7 +261,7 @@ jobs: - name: Upload Built Files uses: actions/upload-artifact@v4 with: - name: built-files + name: built-files-${{ github.run_id }} path: dist release: @@ -281,7 +281,7 @@ jobs: - uses: actions/download-artifact@v4 with: - name: built-files + name: built-files-${{ github.run_id }} path: ./dist - name: Create ZIP of the dist directory diff --git a/.gitignore b/.gitignore index 18e7ebdf51..ed97095241 100644 --- a/.gitignore +++ b/.gitignore @@ -25,10 +25,18 @@ dist-ssr .vscode/mcp.json .env + +# Playwright Test Results /test-results/ /playwright-report/ -/blob-report/ /playwright/.cache/ +/blob-report/ + +# Playwright Test Results Docker Visual Regression +.cache/dconf +.config/google-chrome +.pki + # Sentry Config File @@ -57,3 +65,7 @@ directory_contents.json # ACT Run github workflows locally .vars .secrets + +# npm +.npm-docker-cache +.npm diff --git a/.husky/pre-commit b/.husky/pre-commit index 9fb2b1fa65..7fb2d12433 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -8,7 +8,6 @@ if [ -z "$all_staged_files" ]; then exit 0 fi - # =========================================== # Lint-staged Check # =========================================== diff --git a/package.json b/package.json index 89da1c1af5..69d5bffea2 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,12 @@ "fetch-templates": "node scripts/fetchTemplates", "clear-playwright-cache": "rm -rf ~/.cache/ms-playwright playwright-report .playwright", "generate-interfaces-manifest": "node scripts/generateInterfacesManifest", - "generate:connection-test-data": "if [ \"$VITE_RUN_VISUAL_REGRESSION_TESTS\" = \"true\" ]; then tsx scripts/generateConnectionTestData.ts; fi", + "generate:connection-test-data": "tsx scripts/generateConnectionTestData.ts", "dev": "vite", "generate-project-files-index": "node generateProjectFilesIndex.js && eslint --fix src/assets/templates/index.ts", "lint": "eslint", "lint:fix": "npm run lint -- --fix", "lint:ci": "npm run lint -- --rule 'prettier/prettier: off'", - "pretest:e2e": "npm run generate:connection-test-data", - "pretest:e2e:ui": "npm run generate:connection-test-data", "postinstall": "husky", "prepare": "husky install", "preview": "vite preview", @@ -25,7 +23,7 @@ "storybook": "storybook dev", "storybook-docs": "storybook dev --docs", "tailwind-config-viewer": "tailwind-config-viewer -o --config tailwind.config.cjs", - "test:e2e": "playwright test", + "test:e2e": "npm run generate:connection-test-data && playwright test", "test:e2e:rate-limited": "E2E_RATE_LIMIT_DELAY=2000 playwright test", "test:e2e:rate-limited:serial": "E2E_RATE_LIMIT_DELAY=2000 playwright test --workers=1 --project=Chrome", "test:e2e:by-name": "node scripts/runTestByName.mjs", @@ -42,30 +40,18 @@ "test:e2e:safari": "playwright test --project=Safari --reporter=line", "test:e2e:edge": "playwright test --project=Edge --reporter=line", "test:e2e:visual": "npm run generate:connection-test-data && playwright test --project=\"Visual Regression\"", - "test:e2e:visual:update": "npm run test:e2e:visual -- --update-snapshots", - "test:e2e:visual:serial": "npm run test:e2e:visual -- --workers=1", - "test:e2e:visual:serial:update": "npm run test:e2e:visual:serial -- --update-snapshots", + "test:e2e:visual:update": "npm run generate:connection-test-data && playwright test --project=\"Visual Regression\" --update-snapshots", + "test:e2e:visual:serial": "npm run generate:connection-test-data && playwright test --project=\"Visual Regression\" --workers=1", + "test:e2e:visual:serial:update": "npm run generate:connection-test-data && playwright test --project=\"Visual Regression\" --workers=1 --update-snapshots", + "test:e2e:visual:docker": "node scripts/e2e/docker-visual.mjs", "test:e2e:report": "npx playwright show-report", - "test:e2e:act": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Chrome\" --container-options \"--privileged\"", - "test:e2e:act:logs": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Chrome\" --container-options \"--privileged\" --verbose", - "test:e2e:act:logs:keep": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Chrome\" --container-options \"--privileged\" --verbose --reuse", - "test:e2e:act:debug": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Chrome\" --container-options \"--privileged\" --debug", - "test:e2e:act:debug:keep": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Chrome\" --container-options \"--privileged\" --debug --reuse", - "test:e2e:act:chrome": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Chrome\" --container-options \"--privileged\"", - "test:e2e:act:firefox": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Firefox\" --container-options \"--privileged\"", - "test:e2e:act:safari": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Safari\" --container-options \"--privileged\"", - "test:e2e:act:edge": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Edge\" --container-options \"--privileged\"", - "test:e2e:act:chrome:logs": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Chrome\" --container-options \"--privileged\" --verbose", - "test:e2e:act:firefox:logs": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Firefox\" --container-options \"--privileged\" --verbose", - "test:e2e:act:safari:logs": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Safari\" --container-options \"--privileged\" --verbose", - "test:e2e:act:edge:logs": "act --workflows .github/workflows/build_test_and_release.yml --job test --secret-file .secrets --var-file .vars --platform ubuntu-latest=catthehacker/ubuntu:act-latest --matrix \"browser:Edge\" --container-options \"--privileged\" --verbose", - "test:e2e:ui": "npx playwright test --ui", + "test:e2e:act": "node scripts/e2e/act.mjs", + "test:e2e:ui": "npm run generate:connection-test-data && npx playwright test --ui", "test": "vitest", "test:run": "vitest run", "test:ui": "vitest --ui", "tsc": "tsc", - "pretype-check": "npm run generate:connection-test-data", - "type-check": "tsc --pretty --noEmit", + "type-check": "npm run generate:connection-test-data && tsc --pretty --noEmit", "validate-rules": "node scripts/validateRuleConsistency.mjs" }, "dependencies": { @@ -231,4 +217,4 @@ "rimraf": "^4.0.0", "micromatch": "^4.0.8" } -} +} \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts index d2c2c6a261..e56edb9a16 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,11 +1,10 @@ import { PlaywrightTestOptions, defineConfig, devices } from "@playwright/test"; import dotenv from "dotenv"; +import path from "path"; + +const visualRegression = process.env.DOCKER_VISUAL_REGRESSION === "true"; dotenv.config(); -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ const extraHTTPHeaders: PlaywrightTestOptions["extraHTTPHeaders"] | undefined = process.env.TESTS_JWT_AUTH_TOKEN ? { Authorization: `Bearer ${process.env.TESTS_JWT_AUTH_TOKEN}` } @@ -17,22 +16,49 @@ const previewPort = process.env.VITE_PREVIEW_PORT ? parseInt(process.env.VITE_LOCAL_PORT) : 8000; -/** - * See https://playwright.dev/docs/test-configuration. - */ +const snapshotDir = process.env.SNAPSHOT_DIR; + +const getSnapshotPath = (): string => { + if (snapshotDir) { + const absoluteSnapshotDir = path.isAbsolute(snapshotDir) + ? snapshotDir + : path.resolve(process.cwd(), snapshotDir); + + return `${absoluteSnapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}`; + } + + return "{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}"; +}; + +const visualRegressionConfig = { + browserName: "chromium" as const, + timezoneId: "UTC", + locale: "en-US", + colorScheme: "light" as const, + reducedMotion: "reduce" as const, + video: "on" as const, + trace: "on" as const, + screenshot: "on" as const, + launchOptions: { + args: [ + "--disable-gpu", + "--disable-dev-shm-usage", + "--disable-accelerated-2d-canvas", + "--no-sandbox", + "--disable-setuid-sandbox", + "--font-render-hinting=none", + "--disable-lcd-text", + "--disable-font-subpixel-positioning", + ], + }, + ...devices["Desktop Chrome"], +}; + export default defineConfig({ - /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, + workers: 1, - workers: process.env.CI ? 1 : 2, - - /* Configure projects for major browsers */ projects: [ - // { - // name: "chromium", - // use: { ...devices["Desktop Chrome"] }, - // }, - { name: "Firefox", use: { ...devices["Desktop Firefox"] }, @@ -45,15 +71,6 @@ export default defineConfig({ testIgnore: /.*\.visual\.spec\.ts/, }, - // { - // name: "Mobile Chrome", - // use: { ...devices["Pixel 5"] }, - // }, - // { - // name: "Mobile Safari", - // use: { ...devices["iPhone 12"] }, - // }, - { name: "Edge", use: { ...devices["Desktop Edge"], channel: "msedge" }, @@ -67,10 +84,7 @@ export default defineConfig({ { name: "Visual Regression", testMatch: /.*\.visual\.spec\.ts/, - use: { - ...devices["Desktop Chrome"], - channel: "chrome", - }, + use: visualRegressionConfig, }, ], @@ -94,15 +108,14 @@ export default defineConfig({ timeout: 5000, /* Threshold for visual comparison (0-1, where 0 is exact match) */ toHaveScreenshot: { - maxDiffPixels: 100, - maxDiffPixelRatio: 0.01, + maxDiffPixels: 500, + maxDiffPixelRatio: 0.05, threshold: 0.3, animations: "disabled", }, }, - /* Use platform-agnostic snapshot names */ - snapshotPathTemplate: "{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}", + snapshotPathTemplate: getSnapshotPath(), /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { @@ -121,11 +134,11 @@ export default defineConfig({ }, webServer: { - command: `npm run build && npm run preview`, - port: previewPort, + command: "npm run dev", + url: "http://127.0.0.1:8000", reuseExistingServer: !process.env.CI, stderr: "pipe", stdout: "pipe", - timeout: 2 * 60 * 1000, // 120,000 ms = 2 minutes + timeout: visualRegression ? 5 * 60 * 1000 : 2 * 60 * 1000, // 120,000 ms = 2 minutes (build can take time in Docker) }, }); diff --git a/scripts/e2e/docker-visual.mjs b/scripts/e2e/docker-visual.mjs new file mode 100644 index 0000000000..1f5c250e5c --- /dev/null +++ b/scripts/e2e/docker-visual.mjs @@ -0,0 +1,118 @@ +#!/usr/bin/env node + +import readline from "readline"; +import { spawn } from "child_process"; + +const args = process.argv.slice(2); + +const PLAYWRIGHT_VERSION = "1.56.1"; +const PLAYWRIGHT_IMAGE = `mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-noble`; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +const prompt = (question, options) => { + return new Promise((resolve) => { + console.log(`\n${question}`); + options.forEach((opt, i) => console.log(` ${i + 1}) ${opt.label}`)); + rl.question("\nChoice [1]: ", (answer) => { + const index = parseInt(answer || "1", 10) - 1; + resolve(options[index]?.value ?? options[0].value); + }); + }); +}; + +const runDocker = (updateSnapshots) => { + const dockerConfig = { + memory: updateSnapshots ? "16g" : "12g", + memorySwap: updateSnapshots ? "16g" : "12g", + shmSize: "2g", + }; + + const volumes = [ + "$(pwd):/work", + "ak-node-modules:/work/node_modules", + "ak-npm-cache:/work/.npm-docker-cache", + ]; + + const envVars = { + HOME: "/work", + npm_config_cache: "/work/.npm-docker-cache", + NODE_OPTIONS: "--max-old-space-size=8192", + SKIP_BUILD: "true", + VITE_HOST_URL: "http://host.docker.internal:9980", + FORCE_COLOR: "1", + }; + + const playwrightCommand = [ + "npm ci --progress=true", + "npm run generate:connection-test-data", + `DOCKER_VISUAL_REGRESSION=true npx playwright test --project="Visual Regression" --workers=1${updateSnapshots ? " --update-snapshots" : ""} --reporter=list`, + ].join(" && "); + + const dockerArgs = [ + "run", + "-t", + "--rm", + `--memory=${dockerConfig.memory}`, + `--memory-swap=${dockerConfig.memorySwap}`, + `--shm-size=${dockerConfig.shmSize}`, + "--add-host=host.docker.internal:host-gateway", + ...volumes.flatMap((v) => ["-v", v]), + "-w", + "/work", + ...Object.entries(envVars).flatMap(([key, value]) => ["-e", `${key}=${value}`]), + PLAYWRIGHT_IMAGE, + "bash", + "-c", + playwrightCommand, + ]; + + console.log(`\n🐳 Running Docker visual regression tests${updateSnapshots ? " (updating snapshots)" : ""}...\n`); + + const docker = spawn("docker", dockerArgs, { + stdio: "inherit", + shell: true, + }); + + docker.on("close", (code) => { + process.exit(code); + }); + + docker.on("error", (err) => { + console.error("Failed to start Docker:", err.message); + process.exit(1); + }); +}; + +const main = async () => { + if (args.includes("--update")) { + rl.close(); + runDocker(true); + return; + } + + if (args.includes("--run")) { + rl.close(); + runDocker(false); + return; + } + + console.log("\n🐳 Docker Visual Regression Tests\n"); + + const mode = await prompt("What would you like to do?", [ + { label: "Run tests (compare against existing snapshots)", value: "run" }, + { label: "Update snapshots (regenerate baseline images)", value: "update" }, + ]); + + rl.close(); + runDocker(mode === "update"); +}; + +main().catch((err) => { + console.error(err); + rl.close(); + process.exit(1); +}); diff --git a/vite.config.ts b/vite.config.ts index 49d795571e..f2ea0d84a4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,6 +17,16 @@ const packageJsonPath = new URL("package.json", import.meta.url).pathname; const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); const version = packageJson.version; +const visualRegression = process.env.DOCKER_VISUAL_REGRESSION === "true"; + +const rollupOptions = visualRegression + ? { + output: { + manualChunks: undefined, + }, + } + : {}; + export default defineConfig({ root: __dirname, cacheDir: path.resolve(__dirname, "node_modules/.vite"), @@ -42,6 +52,7 @@ export default defineConfig({ passes: 2, }, }, + ...rollupOptions, }, define: { "import.meta.env.VITE_APP_VERSION": JSON.stringify(version), @@ -64,13 +75,9 @@ export default defineConfig({ "import.meta.env.VITE_SALESFORCE_HIDE_DEFAULT_OAUTH": process.env.VITE_SALESFORCE_HIDE_DEFAULT_OAUTH, "import.meta.env.VITE_DISPLAY_CHATBOT": process.env.VITE_DISPLAY_CHATBOT, "import.meta.env.VITE_AKBOT_URL": JSON.stringify(process.env.VITE_AKBOT_URL), - "import.meta.env.VITE_CI_CD": JSON.stringify(process.env?.CI || "false").toLowerCase() === "true", - "import.meta.env.VITE_RUN_VISUAL_REGRESSION_TESTS": - JSON.stringify(process.env?.VITE_RUN_VISUAL_REGRESSION_TESTS || "false").toLowerCase() === "true", "import.meta.env.VITE_SUPPORT_EMAIL": JSON.stringify(process.env.VITE_SUPPORT_EMAIL), "import.meta.env.VITE_AKBOT_ORIGIN": JSON.stringify(process.env.VITE_AKBOT_ORIGIN), - "import.meta.env.VITE_DISPLAY_BILLING": process.env.VITE_DISPLAY_BILLING, - "import.meta.env.VITE_DISPLAY_GLOBAL_CONNECTIONS": process.env.VITE_DISPLAY_GLOBAL_CONNECTIONS, + "import.meta.env.VITE_ENABLE_BILLING": Boolean(process.env?.VITE_ENABLE_BILLING === "true"), "import.meta.env.VITE_SALES_EMAIL": JSON.stringify(process.env.VITE_SALES_EMAIL), "import.meta.env.VITE_DATADOG_APPLICATION_ID": JSON.stringify(process.env.VITE_DATADOG_APPLICATION_ID), "import.meta.env.VITE_DATADOG_CLIENT_TOKEN": JSON.stringify(process.env.VITE_DATADOG_CLIENT_TOKEN), @@ -78,6 +85,14 @@ export default defineConfig({ "import.meta.env.VITE_DATADOG_SERVICE": JSON.stringify(process.env.VITE_DATADOG_SERVICE), "import.meta.env.VITE_DATADOG_ENV": JSON.stringify(process.env.VITE_DATADOG_ENV), "import.meta.env.VITE_FEEDBACK_WEBHOOK_URL": JSON.stringify(process.env.VITE_FEEDBACK_WEBHOOK_URL), + "import.meta.env.VITE_CI_CD": Boolean( + process.env.CI && typeof process.env.CI === "string" && process.env.CI.toString().toLowerCase() === "true" + ), + "import.meta.env.VITE_HIDE_GLOBAL_CONNECTIONS": Boolean( + process.env.VITE_HIDE_GLOBAL_CONNECTIONS && + typeof process.env.VITE_HIDE_GLOBAL_CONNECTIONS === "string" && + process.env.VITE_HIDE_GLOBAL_CONNECTIONS.toString().toLowerCase() === "true" + ), }, optimizeDeps: { include: ["tailwind-config", "apexcharts"], @@ -175,7 +190,7 @@ export default defineConfig({ }, }, server: { - host: process.env.VITE_APP_DOMAIN ? JSON.stringify(process.env.VITE_APP_DOMAIN) : true, + host: process.env.VITE_APP_DOMAIN || true, port: process.env.VITE_LOCAL_PORT ? Number(process.env.VITE_LOCAL_PORT) : 8000, strictPort: true, },