diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000000..d242af4f18 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,66 @@ +name: Playwright Tests + +on: + push: + branches: + - master + pull_request: + types: + - opened + - synchronize + workflow_dispatch: + inputs: + debug_enabled: + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + required: false + default: 'false' + +jobs: + + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} + with: + limit-access-to-actor: true + - name: Install dependencies + run: npm ci + working-directory: frontend + - name: Install Playwright Browsers + run: npx playwright install --with-deps + working-directory: frontend + - run: docker compose build + - run: docker compose down -v --remove-orphans + - run: docker compose up -d + - name: Run Playwright tests + run: npx playwright test + working-directory: frontend + - run: docker compose down -v --remove-orphans + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: frontend/playwright-report/ + retention-days: 30 + + # https://github.com/marketplace/actions/alls-green#why + e2e-alls-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - test + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.gitignore b/.gitignore index 722d5e71d9..a6dd346572 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ .vscode +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/frontend/.gitignore b/frontend/.gitignore index 4eb6586f19..dfc4015cce 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -23,3 +23,7 @@ openapi.json *.njsproj *.sln *.sw? +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/frontend/biome.json b/frontend/biome.json index b6e397eb49..14597ce328 100644 --- a/frontend/biome.json +++ b/frontend/biome.json @@ -4,7 +4,13 @@ "enabled": true }, "files": { - "ignore": ["node_modules", "src/client/", "src/routeTree.gen.ts"] + "ignore": [ + "node_modules", + "src/client/", + "src/routeTree.gen.ts", + "playwright.config.ts", + "playwright-report" + ] }, "linter": { "enabled": true, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7c1556613f..a17e86cea5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -27,9 +27,10 @@ "devDependencies": { "@biomejs/biome": "1.6.1", "@hey-api/openapi-ts": "^0.34.1", + "@playwright/test": "^1.45.2", "@tanstack/router-devtools": "1.19.1", "@tanstack/router-vite-plugin": "1.19.0", - "@types/node": "20.10.5", + "@types/node": "^20.10.5", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", "@vitejs/plugin-react-swc": "^3.5.0", @@ -2123,6 +2124,21 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "dev": true }, + "node_modules/@playwright/test": { + "version": "1.45.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz", + "integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==", + "dev": true, + "dependencies": { + "playwright": "1.45.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -3318,6 +3334,50 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "node_modules/playwright": { + "version": "1.45.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz", + "integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==", + "dev": true, + "dependencies": { + "playwright-core": "1.45.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.45.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz", + "integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==", + "dev": true, + "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, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", @@ -5273,6 +5333,15 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "dev": true }, + "@playwright/test": { + "version": "1.45.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz", + "integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==", + "dev": true, + "requires": { + "playwright": "1.45.2" + } + }, "@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -6063,6 +6132,31 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "playwright": { + "version": "1.45.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz", + "integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.45.2" + }, + "dependencies": { + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + } + } + }, + "playwright-core": { + "version": "1.45.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz", + "integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==", + "dev": true + }, "postcss": { "version": "8.4.35", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0aff516f4c..b429c4d4c0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,9 +30,10 @@ "devDependencies": { "@biomejs/biome": "1.6.1", "@hey-api/openapi-ts": "^0.34.1", + "@playwright/test": "^1.45.2", "@tanstack/router-devtools": "1.19.1", "@tanstack/router-vite-plugin": "1.19.0", - "@types/node": "20.10.5", + "@types/node": "^20.10.5", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", "@vitejs/plugin-react-swc": "^3.5.0", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000000..4208a946bb --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,91 @@ +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: true, + /* 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://localhost:5173', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { name: 'setup', testMatch: /.*\.setup\.ts/ }, + + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/user.json', + }, + dependencies: ['setup'], + }, + + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // storageState: 'playwright/.auth/user.json', + // }, + // dependencies: ['setup'], + // }, + + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // storageState: 'playwright/.auth/user.json', + // }, + // dependencies: ['setup'], + // }, + + /* 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 dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/frontend/tests/auth.setup.ts b/frontend/tests/auth.setup.ts new file mode 100644 index 0000000000..48f1acb07e --- /dev/null +++ b/frontend/tests/auth.setup.ts @@ -0,0 +1,12 @@ +import { test as setup } from "@playwright/test" + +const authFile = "playwright/.auth/user.json" + +setup("authenticate", async ({ page }) => { + await page.goto("/login") + await page.getByPlaceholder("Email").fill("admin@example.com") + await page.getByPlaceholder("Password").fill("changethis") + await page.getByRole("button", { name: "Log In" }).click() + await page.waitForURL("/") + await page.context().storageState({ path: authFile }) +}) diff --git a/frontend/tests/example.spec.ts b/frontend/tests/example.spec.ts new file mode 100644 index 0000000000..f5dc2e73ca --- /dev/null +++ b/frontend/tests/example.spec.ts @@ -0,0 +1,8 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +});