From f313e7be8c38fdb4dc9f542fcbe244ff245144c8 Mon Sep 17 00:00:00 2001 From: Thorsten Bus Date: Sun, 1 Mar 2026 22:45:19 +0100 Subject: [PATCH] test(e2e): add Playwright infrastructure with auth setup - Install @playwright/test and chromium browser - Create playwright.config.ts (baseURL, workers:1, no webServer) - Create tests/e2e/auth.setup.ts (dummy login via POST /dev-login) - Add test:e2e npm script - Update .gitignore for tests/e2e/.auth/ and test-results/ - Auth setup uses page.request.post() to bypass ZiggyVue dependency - storageState pattern for reusing login across tests --- .gitignore | 2 ++ package-lock.json | 64 +++++++++++++++++++++++++++++++++++++++++ package.json | 10 ++++--- playwright.config.ts | 30 +++++++++++++++++++ tests/e2e/auth.setup.ts | 27 +++++++++++++++++ 5 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 playwright.config.ts create mode 100644 tests/e2e/auth.setup.ts diff --git a/.gitignore b/.gitignore index b71b1ea..d4c3474 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ Homestead.json Homestead.yaml Thumbs.db +tests/e2e/.auth/ +test-results/ diff --git a/package-lock.json b/package-lock.json index 9139831..904037b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "devDependencies": { "@inertiajs/vue3": "^2.0.0", "@jaxtheprime/vue3-dropzone": "^1.1.0", + "@playwright/test": "^1.58.2", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/vite": "^4.0.0", "@vitejs/plugin-vue": "^6.0.0", @@ -611,6 +612,22 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.2", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", @@ -2578,6 +2595,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", diff --git a/package.json b/package.json index 44d0e85..4f3f6bd 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,17 @@ "type": "module", "scripts": { "build": "vite build", - "dev": "vite" + "dev": "vite", + "test:e2e": "npx playwright test" }, "devDependencies": { "@inertiajs/vue3": "^2.0.0", + "@jaxtheprime/vue3-dropzone": "^1.1.0", + "@playwright/test": "^1.58.2", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/vite": "^4.0.0", "@vitejs/plugin-vue": "^6.0.0", + "@vueuse/core": "^12.0.0", "autoprefixer": "^10.4.12", "axios": "^1.11.0", "concurrently": "^9.0.1", @@ -19,8 +23,6 @@ "tailwindcss": "^4.2.1", "vite": "^7.0.0", "vue": "^3.4.0", - "@vueuse/core": "^12.0.0", - "vue-draggable-plus": "^0.6.0", - "@jaxtheprime/vue3-dropzone": "^1.1.0" + "vue-draggable-plus": "^0.6.0" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..570ae49 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,30 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: false, + workers: 1, + timeout: 30000, + expect: { + timeout: 5000, + }, + use: { + baseURL: 'http://cts-work.test', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'setup', + testMatch: /auth\.setup\.ts/, + }, + { + name: 'default', + dependencies: ['setup'], + use: { + ...devices['Desktop Chrome'], + storageState: 'tests/e2e/.auth/user.json', + }, + }, + ], + outputDir: 'test-results', +}); diff --git a/tests/e2e/auth.setup.ts b/tests/e2e/auth.setup.ts new file mode 100644 index 0000000..7b31957 --- /dev/null +++ b/tests/e2e/auth.setup.ts @@ -0,0 +1,27 @@ +import { test as setup, expect } from '@playwright/test'; + +const authFile = 'tests/e2e/.auth/user.json'; + +setup('authenticate', async ({ page }) => { + // Navigate to login page to establish session cookies (incl. XSRF-TOKEN) + await page.goto('http://cts-work.test/login'); + + // Get XSRF token from cookies for CSRF protection + const cookies = await page.context().cookies('http://cts-work.test'); + const xsrfCookie = cookies.find((c) => c.name === 'XSRF-TOKEN'); + const xsrfToken = decodeURIComponent(xsrfCookie?.value || ''); + + // POST to dev-login route directly (bypasses Vue rendering dependency) + await page.request.post('http://cts-work.test/dev-login', { + headers: { + 'X-XSRF-TOKEN': xsrfToken, + }, + }); + + // Navigate to dashboard to confirm login and load session + await page.goto('http://cts-work.test/dashboard'); + await page.waitForURL('**/dashboard'); + + // Save signed-in state + await page.context().storageState({ path: authFile }); +});