diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..00d9970 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,55 @@ +name: Playwright E2E Tests + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} + CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_PUBLISHABLE_KEY }} + VITE_CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_PUBLISHABLE_KEY }} + DATABASE_URL: file:./prisma/test.db + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Sync database + run: bunx prisma db push + + - name: Install Playwright browsers + run: bunx playwright install --with-deps + + - name: Run Playwright tests + run: bun run test:e2e + + - name: Upload Playwright report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + + - name: Upload test results + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: playwright-test-results + path: test-results/ + retention-days: 30 + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 63b1000..21f18ca 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,17 @@ .env.prod prisma/*.db + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ +/playwright/.temp/ +/playwright/screenshots/ +/playwright/videos/ +/playwright-traces/ +/playwright-debug.log +node_modules/ +.cache/ \ No newline at end of file diff --git a/bun.lock b/bun.lock index de03c3f..85072c6 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "seo-ai-experiments", @@ -49,6 +50,7 @@ }, "devDependencies": { "@eslint/js": "^9.26.0", + "@playwright/test": "^1.56.1", "@react-router/dev": "^7.6.0", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.6", @@ -411,6 +413,8 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@playwright/test": ["@playwright/test@1.56.1", "", { "dependencies": { "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" } }, "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg=="], + "@prisma/client": ["@prisma/client@6.7.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA=="], "@prisma/config": ["@prisma/config@6.7.0", "", { "dependencies": { "esbuild": ">=0.12 <1", "esbuild-register": "3.6.0" } }, "sha512-di8QDdvSz7DLUi3OOcCHSwxRNeW7jtGRUD2+Z3SdNE3A+pPiNT8WgUJoUyOwJmUr5t+JA2W15P78C/N+8RXrOA=="], @@ -1633,6 +1637,10 @@ "pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="], + "playwright": ["playwright@1.56.1", "", { "dependencies": { "playwright-core": "1.56.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw=="], + + "playwright-core": ["playwright-core@1.56.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], @@ -2123,6 +2131,8 @@ "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "react-d3-tree/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "react-router/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], diff --git a/package.json b/package.json index 40365de..bad7cb5 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,11 @@ "format": "bunx --bun prettier . --ignore-path .gitignore --write --cache --cache-location ./node_modules/.cache/prettier", "typecheck": "bunx --bun react-router typegen && tsc", "fun": "bun format && bun typecheck && bun lint", - "cron": "bun run cron/index.ts" + "cron": "bun run cron/index.ts", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:headed": "playwright test --headed", + "test:e2e:debug": "playwright test --debug" }, "dependencies": { "@ai-sdk/anthropic": "^1.2.11", @@ -58,6 +62,7 @@ }, "devDependencies": { "@eslint/js": "^9.26.0", + "@playwright/test": "^1.56.1", "@react-router/dev": "^7.6.0", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.6", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..b1db3dd --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,83 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * 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", + + // Record trace only when retrying a test for the first time. + trace: "on-first-retry", + + // Capture screenshot after each test failure. + screenshot: "only-on-failure", + + // Record video only when retrying a test for the first time. + video: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + + /* 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: `CLERK_PUBLISHABLE_KEY=${process.env.CLERK_PUBLISHABLE_KEY} \ + VITE_CLERK_PUBLISHABLE_KEY=${process.env.VITE_CLERK_PUBLISHABLE_KEY} \ + CLERK_SECRET_KEY=${process.env.CLERK_SECRET_KEY} \ + DATABASE_URL=${process.env.DATABASE_URL} \ + bun run dev`, + url: "http://localhost:5173", + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}); diff --git a/tests/e2e/example.spec.ts b/tests/e2e/example.spec.ts new file mode 100644 index 0000000..ad26e05 --- /dev/null +++ b/tests/e2e/example.spec.ts @@ -0,0 +1,20 @@ +import { expect, test } 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/); +}); + +test("get started link", async ({ page }) => { + await page.goto("https://playwright.dev/"); + + // Click the get started link. + await page.getByRole("link", { name: "Get started" }).click(); + + // Expects page to have a heading with the name of Installation. + await expect( + page.getByRole("heading", { name: "Installation" }), + ).toBeVisible(); +});