diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f37a078f5..ff41dbcaa 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -32,6 +32,8 @@ const config = { '*.test.*', 'public', '.eslintrc.cjs', + 'test-results', + 'playwright-report' ], rules: { '@next/next/no-img-element': 'off', diff --git a/.github/workflows/cleanup-db-branches.yml b/.github/workflows/cleanup-db-branches.yml new file mode 100644 index 000000000..a7bb35ff2 --- /dev/null +++ b/.github/workflows/cleanup-db-branches.yml @@ -0,0 +1,15 @@ +name: Clean up branched databases +on: + pull_request: + types: [closed] + +jobs: + delete-preview: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-node@v4 + - run: npm i -g neonctl@latest + - name: Delete Neon Branch + run: neonctl branches delete preview/${{ github.event.pull_request.head.ref }} --project-id ${{ secrets.NEON_PROJECT_ID }} + env: + NEON_API_KEY: ${{ secrets.NEON_API_KEY }} diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..a5fc06661 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,22 @@ +name: Playwright Tests +on: + deployment_status: +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + if: ${{ github.event_name == 'deployment_status' && startsWith(github.event.deployment_status.environment, 'preview') && github.event.deployment_status.state == 'success' }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm install -g pnpm && pnpm install + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + - name: Run tests + run: pnpm run test:e2e-ci + env: + UPLOADTHING_TOKEN: ${{ secrets.UPLOADTHING_TOKEN }} + BASE_URL: ${{ github.event.deployment_status.environment_url }} diff --git a/.gitignore b/.gitignore index 5ddfa4fed..19c903a3f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,9 +35,17 @@ yarn-error.log* # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables .env .env*.local +.env.test # vercel .vercel # typescript *.tsbuildinfo + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 000000000..a0bb0e89f --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,28 @@ +services: + postgres-test: + container_name: fresco-test-postgres + image: postgres:16-alpine + restart: always + ports: + - 5433:5432 # Avoid conflicts with dev + environment: + - POSTGRES_PASSWORD=test_postgres_password + - POSTGRES_USER=test_user + - POSTGRES_DB=test_db + volumes: + - test-postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", '-U', 'test_user'] + interval: 5s + timeout: 10s + retries: 5 + networks: + - test-network + +volumes: + test-postgres: + name: fresco-test-db-volume + +networks: + test-network: + name: fresco-test-network \ No newline at end of file diff --git a/e2e/app.spec.ts b/e2e/app.spec.ts new file mode 100644 index 000000000..d0136f6d3 --- /dev/null +++ b/e2e/app.spec.ts @@ -0,0 +1,18 @@ + +import { expect, test } from '@playwright/test'; + + +// app should be setup before this is run + +test('should sign in', async ({ page }) => { + + await page.goto("/"); // base url is set in playwright.config.ts + await expect(page).toHaveURL(/\/signin/); + + // sign in using credentials + await page.fill('input[name="username"]', 'admin'); + await page.fill('input[name="password"]', 'Administrator1!'); + await page.click('button[type="submit"]'); + + await expect(page).toHaveURL(/\/dashboard/, { timeout: 10000 }); +}); \ No newline at end of file diff --git a/e2e/files/SampleProtocol.netcanvas b/e2e/files/SampleProtocol.netcanvas new file mode 100644 index 000000000..7d330b355 Binary files /dev/null and b/e2e/files/SampleProtocol.netcanvas differ diff --git a/e2e/files/participants.csv b/e2e/files/participants.csv new file mode 100644 index 000000000..a7e6a3adb --- /dev/null +++ b/e2e/files/participants.csv @@ -0,0 +1,6 @@ +identifier,label +d8a1d0be-5f9f-4cf9-b89b-f15e6c6df19d,John Doe +4aefc585-18e6-40cb-ae04-2b511f0d007d,Jane Smith +b2d788bc-0305-4fd5-b512-8928d9b33a20,Michael Johnson +cce25123-d8c6-44cc-971b-2fc0f6cf9200,Emily Brown +657b0254-33de-4f7d-8e3a-c1057bdf4f1a,Christopher Lee \ No newline at end of file diff --git a/e2e/global.setup.ts b/e2e/global.setup.ts new file mode 100644 index 000000000..377de0a17 --- /dev/null +++ b/e2e/global.setup.ts @@ -0,0 +1,121 @@ +/* eslint-disable no-console */ +/* eslint-disable no-process-env */ + +import { expect, test } from '@playwright/test'; +import { execSync } from 'child_process'; + +test('create test database and setup app', async ({ page }) => { + console.log('Running setup test'); + test.slow(); // triple the default timeout + + // Stop any existing test db to ensure clean state + if (!process.env.CI) { + try { + execSync('docker compose -f docker-compose.test.yml down -v', { stdio: 'inherit' }); + } catch (error) { + // Ignore errors if no existing container + } + + // Start test db + execSync('docker compose -f docker-compose.test.yml up -d', { stdio: 'inherit' }); + + // Optional: Wait for database to be ready + console.log('Waiting for database to be ready'); + execSync('sleep 5', { stdio: 'inherit' }); + + // local dev, need to use .env.test.local + execSync('pnpm exec dotenv -e .env.test.local node ./setup-database.js && pnpm exec dotenv -e .env.test.local node ./initialize.js', { stdio: 'inherit' }); + } else { + console.log('CI environment detected'); + // we are in CI uiing the preview deployment + // sign in and reset database + await page.goto("/"); // base url is set in playwright.config.ts + await expect(page).toHaveURL(/\/signin/); + + // sign in using credentials + await page.fill('input[name="username"]', 'admin'); + await page.fill('input[name="password"]', 'Administrator1!'); + await page.click('button[type="submit"]'); + + await expect(page).toHaveURL(/\/dashboard/, { timeout: 10000 }); + console.log('✅ Signed in successfully'); + + // go to /settings + await page.goto("/dashboard/settings"); + // click "reset all app data" button + const resetButton = page.getByRole('button', { name: 'Reset all app data' }); + await resetButton.click({ timeout: 10000 }); + const confirmButton = page.getByRole('button', { name: 'Delete all data' }); + await confirmButton.click({ timeout: 10000 }); + + await expect(page).toHaveURL(/\/setup/, { timeout: 10000 }); + + console.log('✅ Reset app data with settings button') + + } + + // STEP 1 + await page.goto("/setup"); + // screenshot + await page.fill('input[name="username"]', 'admin', { timeout: 5000 }); + await page.fill('input[name="password"]', 'Administrator1!', { timeout: 5000 }); + + await page.fill('input[name="confirmPassword"]', 'Administrator1!', { timeout: 5000 }); + await page.click('button[type="submit"]', { timeout: 10000 }); + await expect(page).toHaveURL(/\/setup\?step=2/); + console.log('✅ Step 1 completed: admin user created'); + + // STEP 2 + // env var cannot be UPLOADTHING_TOKEN or this step will be skipped + // screenshot + await page.fill('input[name="uploadThingToken"]', process.env.UPLOADTHING_TOKEN ?? '', { timeout: 5000 }); + await page.click('button[type="submit"]', { timeout: 10000 }); + + await expect(page).toHaveURL(/\/setup\?step=3/, { timeout: 20000 }); + console.log('✅ Step 2 completed: uploadthing token set'); + + // STEP 3 + const protocolHandle = page.locator('input[type="file"]'); + await protocolHandle.setInputFiles('e2e/files/SampleProtocol.netcanvas'); + // check for uploading assets toast + await expect(page.getByText('Uploading assets...')).toBeVisible({ timeout: 5000 }); + await expect(page.getByText('Uploading assets...')).not.toBeVisible({ timeout: 120000 }); // long process if assets are large + await expect(page.getByText('Complete...')).toBeVisible({ timeout: 60000 }); + + await page.getByRole('button', { name: 'Continue' }).click({ timeout: 5000 }); + await expect(page).toHaveURL(/\/setup\?step=4/); + console.log('✅ Step 3 completed: protocol uploaded'); + + // STEP 4 + // import participants + await page.getByRole('button', { name: 'Import participants' }).click({ timeout: 5000 }); + + // dialog should be visible + await expect(page.getByRole('dialog')).toBeVisible({ timeout: 5000 }); + + const participantsHandle = page.locator('input[type="file"]'); + await participantsHandle.setInputFiles('e2e/files/participants.csv'); + await page.getByRole('button', { name: 'Import' }).click({ timeout: 5000 }); + + // participants imported toast + await expect(page.locator('div.text-sm.opacity-90', { hasText: 'Participants have been imported successfully' })).toBeVisible({ timeout: 5000 }); + + // toggle switches + const anonymousRecruitmentSwitch = page.getByRole('switch').first(); + const limitInterviewsSwitch = page.getByRole('switch').last(); + await anonymousRecruitmentSwitch.click({ timeout: 10000 }); + await limitInterviewsSwitch.click({ timeout: 10000 }); + + + await expect(anonymousRecruitmentSwitch).toBeChecked(); + await expect(limitInterviewsSwitch).toBeChecked(); + + await page.getByRole('button', { name: 'Continue' }).click({ timeout: 10000 }); + await expect(page).toHaveURL(/\/setup\?step=5/); + console.log('✅ Step 4 completed: participants imported and settings toggled'); + + // STEP 5 - documentation + await page.getByRole('button', { name: 'Go to the dashboard!' }).click({ timeout: 10000 }); + await expect(page).toHaveURL(/\/dashboard/, { timeout: 10000 }); + console.log('✅ Setup completed: dashboard reached'); +}); diff --git a/e2e/global.teardown.ts b/e2e/global.teardown.ts new file mode 100644 index 000000000..f4abf63f0 --- /dev/null +++ b/e2e/global.teardown.ts @@ -0,0 +1,25 @@ +/* eslint-disable no-process-env */ +import { test as teardown } from '@playwright/test'; +import { execSync } from 'child_process'; +import { UTApi } from 'uploadthing/server'; + +teardown('delete test database', async () => { + if (!process.env.CI) { + // remove uploaded files from uploadthing + // eslint-disable-next-line no-console + console.log('🗑️ Deleting uploaded files from uploadthing'); + + const utapi = new UTApi({ + // TODO: figure out why we cannot use getUTApi here + token: process.env.UPLOADTHING_TOKEN, + }); + + await utapi.listFiles({}).then(({ files }) => { + const keys = files.map((file) => file.key); + return utapi.deleteFiles(keys); + }); + + // Stop and remove test db + execSync('docker compose -f docker-compose.test.yml down -v', { stdio: 'inherit' }); + } +}); diff --git a/initialize.js b/initialize.js index b162ec764..3dd5e06e4 100644 --- a/initialize.js +++ b/initialize.js @@ -16,7 +16,7 @@ async function setInitializedAt() { }); if (initializedAt) { - console.log('App already initialized. Skipping.'); + console.log(`App already initialized at ${initializedAt}. Skipping.`); return; } diff --git a/knip.json b/knip.json index 1dd29557c..3c11a121b 100644 --- a/knip.json +++ b/knip.json @@ -8,7 +8,9 @@ "lib/interviewer/hooks/forceSimulation.worker.js", "lib/ui/components/Sprites/ExportSprite.js", "utils/auth.ts", - "load-test.js" + "load-test.js", + "e2e/global.setup.ts", + "e2e/global.teardown.ts" ], "ignoreDependencies": [ "server-only", @@ -18,7 +20,8 @@ "@tailwindcss/container-queries", "@tailwindcss/forms", "@tailwindcss/typography", - "tailwindcss-animate" + "tailwindcss-animate", + "dotenv-cli" ], "ignoreBinaries": ["docker-compose"] } diff --git a/package.json b/package.json index b4afbb140..c96a15b0c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "vercel-build": "node ./setup-database.js && node ./initialize.js && next build", "knip": "knip", "test": "vitest", - "load-test": "docker run -i grafana/k6 run - =14'} + '@playwright/test@1.50.1': + resolution: {integrity: sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==} + engines: {node: '>=18'} + hasBin: true + '@prisma/client@6.4.1': resolution: {integrity: sha512-A7Mwx44+GVZVexT5e2GF/WcKkEkNNKbgr059xpr5mn+oUm2ZW1svhe+0TRNBwCdzhfIZ+q23jEgsNPvKD9u+6g==} engines: {node: '>=18.18'} @@ -2884,6 +2895,14 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dotenv-cli@8.0.0: + resolution: {integrity: sha512-aLqYbK7xKOiTMIRf1lDPbI+Y+Ip/wo5k3eyp6ePysVaSqbyxjyK3dK35BTxG+rmd7djf5q2UPs4noPNH+cj0Qw==} + hasBin: true + + dotenv-expand@10.0.0: + resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + engines: {node: '>=12'} + dotenv@16.4.7: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} @@ -3253,6 +3272,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4496,6 +4520,16 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + playwright-core@1.50.1: + resolution: {integrity: sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.50.1: + resolution: {integrity: sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==} + engines: {node: '>=18'} + hasBin: true + point-in-polygon@1.1.0: resolution: {integrity: sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==} @@ -6039,9 +6073,9 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@codaco/analytics@7.0.0(next@14.2.23(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))': + '@codaco/analytics@7.0.0(next@14.2.23(@babel/core@7.25.2)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))': dependencies: - next: 14.2.23(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + next: 14.2.23(@babel/core@7.25.2)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) zod: 3.24.2 '@codaco/protocol-validation@3.0.0': {} @@ -6648,6 +6682,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.50.1': + dependencies: + playwright: 1.50.1 + '@prisma/client@6.4.1(prisma@6.4.1(typescript@5.7.3))(typescript@5.7.3)': optionalDependencies: prisma: 6.4.1(typescript@5.7.3) @@ -7570,14 +7608,14 @@ snapshots: '@uploadthing/mime-types@0.3.4': {} - '@uploadthing/react@7.3.0(next@14.2.23(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)(uploadthing@7.5.2(next@14.2.23(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(tailwindcss@4.0.9))': + '@uploadthing/react@7.3.0(next@14.2.23(@babel/core@7.25.2)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)(uploadthing@7.5.2(next@14.2.23(@babel/core@7.25.2)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(tailwindcss@4.0.9))': dependencies: '@uploadthing/shared': 7.1.7 file-selector: 0.6.0 react: 18.3.1 - uploadthing: 7.5.2(next@14.2.23(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(tailwindcss@4.0.9) + uploadthing: 7.5.2(next@14.2.23(@babel/core@7.25.2)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(tailwindcss@4.0.9) optionalDependencies: - next: 14.2.23(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + next: 14.2.23(@babel/core@7.25.2)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) '@uploadthing/shared@7.1.7': dependencies: @@ -8254,6 +8292,15 @@ snapshots: '@babel/runtime': 7.25.6 csstype: 3.1.3 + dotenv-cli@8.0.0: + dependencies: + cross-spawn: 7.0.6 + dotenv: 16.4.7 + dotenv-expand: 10.0.0 + minimist: 1.2.8 + + dotenv-expand@10.0.0: {} + dotenv@16.4.7: {} dunder-proto@1.0.1: @@ -8492,7 +8539,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.25.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.25.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: @@ -8514,7 +8561,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.25.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.25.0(eslint@8.57.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -8791,6 +8838,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -10272,7 +10322,7 @@ snapshots: natural-compare@1.4.0: {} - next@14.2.23(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1): + next@14.2.23(@babel/core@7.25.2)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1): dependencies: '@next/env': 14.2.23 '@swc/helpers': 0.5.5 @@ -10293,6 +10343,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.23 '@next/swc-win32-ia32-msvc': 14.2.23 '@next/swc-win32-x64-msvc': 14.2.23 + '@playwright/test': 1.50.1 sass: 1.85.1 transitivePeerDependencies: - '@babel/core' @@ -10313,10 +10364,10 @@ snapshots: dependencies: path-key: 3.1.1 - nuqs@1.19.1(next@14.2.23(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)): + nuqs@1.19.1(next@14.2.23(@babel/core@7.25.2)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)): dependencies: mitt: 3.0.1 - next: 14.2.23(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + next: 14.2.23(@babel/core@7.25.2)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) nwsapi@2.2.16: {} @@ -10476,6 +10527,14 @@ snapshots: dependencies: find-up: 4.1.0 + playwright-core@1.50.1: {} + + playwright@1.50.1: + dependencies: + playwright-core: 1.50.1 + optionalDependencies: + fsevents: 2.3.2 + point-in-polygon@1.1.0: {} possible-typed-array-names@1.0.0: {} @@ -11439,7 +11498,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - uploadthing@7.5.2(next@14.2.23(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(tailwindcss@4.0.9): + uploadthing@7.5.2(next@14.2.23(@babel/core@7.25.2)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(tailwindcss@4.0.9): dependencies: '@effect/platform': 0.72.0(effect@3.12.0) '@standard-schema/spec': 1.0.0-beta.4 @@ -11447,7 +11506,7 @@ snapshots: '@uploadthing/shared': 7.1.7 effect: 3.12.0 optionalDependencies: - next: 14.2.23(@babel/core@7.25.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) + next: 14.2.23(@babel/core@7.25.2)(@playwright/test@1.50.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1) tailwindcss: 4.0.9 uri-js@4.4.1: diff --git a/vitest.config.ts b/vitest.config.ts index 3803989a7..570bdc2d3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,5 +6,6 @@ export default defineConfig({ plugins: [tsconfigPaths(), react()], test: { environment: 'jsdom', + exclude: ['**/e2e/**', '**/node_modules/**'], // e2e tests are handled by playwright }, });