diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index b627af8..18aebbe 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -38,3 +38,27 @@ jobs: - name: Build run: bun run build + + e2e: + name: Playwright E2E + needs: checks + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/web + + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Install Playwright browsers + run: bunx playwright install --with-deps + + - name: Run e2e tests + run: bun run e2e \ No newline at end of file diff --git a/apps/web/.gitignore b/apps/web/.gitignore index ac6aba3..b2e5a70 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -39,3 +39,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json .ufazien.json # Ufazien CLI ufazien.py + +# Playwright +test-results/ +playwright-report/ \ No newline at end of file diff --git a/apps/web/e2e/access.spec.ts b/apps/web/e2e/access.spec.ts new file mode 100644 index 0000000..8526ae7 --- /dev/null +++ b/apps/web/e2e/access.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); + + await page.evaluate(() => { + localStorage.setItem( + "cleat-org", + JSON.stringify({ + state: { + connected: true, + connectedAccountIds: ["acct_personal"], + activeAccountId: "acct_personal", + }, + version: 0, + }), + ); + }); + + await page.reload(); +}); + +test("access page loads", async ({ page }) => { + await page.goto("/app/access"); + + await expect(page.getByTestId("access-page")).toBeVisible(); +}); diff --git a/apps/web/e2e/artifacts.spec.ts b/apps/web/e2e/artifacts.spec.ts new file mode 100644 index 0000000..67fb0c1 --- /dev/null +++ b/apps/web/e2e/artifacts.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); + + await page.evaluate(() => { + localStorage.setItem( + "cleat-org", + JSON.stringify({ + state: { + connected: true, + connectedAccountIds: ["acct_personal"], + activeAccountId: "acct_personal", + }, + version: 0, + }), + ); + }); + + await page.reload(); +}); + +test("artifacts page loads", async ({ page }) => { + await page.goto("/app/artifacts"); + + await expect(page.getByTestId("artifacts-page")).toBeVisible(); + + await expect(page.getByRole("heading", { name: "Artifacts & cost" })).toBeVisible(); + + await expect(page.locator("button", { hasText: "Caches" })).toBeVisible(); + + await expect(page.locator("button", { hasText: "Packages" })).toBeVisible(); +}); diff --git a/apps/web/e2e/code-scanning.spec.ts b/apps/web/e2e/code-scanning.spec.ts new file mode 100644 index 0000000..71f29ea --- /dev/null +++ b/apps/web/e2e/code-scanning.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); + + await page.evaluate(() => { + localStorage.setItem( + "cleat-org", + JSON.stringify({ + state: { + connected: true, + connectedAccountIds: ["acct_personal"], + activeAccountId: "acct_personal", + }, + version: 0, + }), + ); + }); + + await page.reload(); +}); + +test("code scanning page loads", async ({ page }) => { + await page.goto("/app/security/code-scanning"); + + await expect(page.getByTestId("code-scanning-page")).toBeVisible(); +}); diff --git a/apps/web/e2e/dependencies.spec.ts b/apps/web/e2e/dependencies.spec.ts new file mode 100644 index 0000000..78111f1 --- /dev/null +++ b/apps/web/e2e/dependencies.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); + + await page.evaluate(() => { + localStorage.setItem( + "cleat-org", + JSON.stringify({ + state: { + connected: true, + connectedAccountIds: ["acct_personal"], + activeAccountId: "acct_personal", + }, + version: 0, + }), + ); + }); + + await page.reload(); +}); + +test("dependencies page loads", async ({ page }) => { + await page.goto("/app/dependencies"); + + await expect(page.getByTestId("dependencies-page")).toBeVisible(); + + await expect(page.getByText("Total dependencies")).toBeVisible(); + + await expect(page.getByText("Export SBOM")).toBeVisible(); + + await expect(page.getByText("Download")).toBeVisible(); +}); diff --git a/apps/web/e2e/onboarding.spec.ts b/apps/web/e2e/onboarding.spec.ts new file mode 100644 index 0000000..b7f6fb7 --- /dev/null +++ b/apps/web/e2e/onboarding.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from "@playwright/test"; + +test("user can start onboarding", async ({ page }) => { + await page.goto("/connect"); + + await expect( + page.getByRole("button", { + name: /authorize & continue/i, + }), + ).toBeVisible(); + + await page + .getByRole("button", { + name: /authorize & continue/i, + }) + .click(); + + await expect(page).toHaveURL(/onboarding/); +}); diff --git a/apps/web/e2e/overview.spec.ts b/apps/web/e2e/overview.spec.ts new file mode 100644 index 0000000..6cb636e --- /dev/null +++ b/apps/web/e2e/overview.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); + + await page.evaluate(() => { + localStorage.setItem( + "cleat-org", + JSON.stringify({ + state: { + connected: true, + connectedAccountIds: ["acct_personal"], + activeAccountId: "acct_personal", + }, + version: 0, + }), + ); + }); + + await page.reload(); +}); + +test("overview page loads", async ({ page }) => { + await page.goto("/app/overview"); + + await expect(page).toHaveURL(/overview/); + + await expect(page.getByTestId("overview-page")).toBeVisible(); + + await expect(page.getByTestId("security-posture-card")).toBeVisible(); + + await expect(page.getByTestId("open-findings-card")).toBeVisible(); +}); diff --git a/apps/web/e2e/repositories.spec.ts b/apps/web/e2e/repositories.spec.ts new file mode 100644 index 0000000..0b78ea2 --- /dev/null +++ b/apps/web/e2e/repositories.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); + + await page.evaluate(() => { + localStorage.setItem( + "cleat-org", + JSON.stringify({ + state: { + connected: true, + connectedAccountIds: ["acct_personal"], + activeAccountId: "acct_personal", + }, + version: 0, + }), + ); + }); + + await page.reload(); +}); + +test("repositories page loads", async ({ page }) => { + await page.goto("/app/repositories"); + + await expect(page).toHaveURL(/repositories/); + + await expect(page.getByTestId("repositories-page")).toBeVisible(); +}); diff --git a/apps/web/e2e/repository-detail.spec.ts b/apps/web/e2e/repository-detail.spec.ts new file mode 100644 index 0000000..750a841 --- /dev/null +++ b/apps/web/e2e/repository-detail.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); + + await page.evaluate(() => { + localStorage.setItem( + "cleat-org", + JSON.stringify({ + state: { + connected: true, + connectedAccountIds: ["acct_personal"], + activeAccountId: "acct_personal", + }, + version: 0, + }), + ); + }); + + await page.reload(); +}); + +test("repository detail page loads", async ({ page }) => { + await page.goto("/app/repositories/repo-1"); + + await expect(page.getByTestId("repo-detail-page")).toBeVisible(); +}); diff --git a/apps/web/e2e/secrets.spec.ts b/apps/web/e2e/secrets.spec.ts new file mode 100644 index 0000000..bcf6d26 --- /dev/null +++ b/apps/web/e2e/secrets.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); + + await page.evaluate(() => { + localStorage.setItem( + "cleat-org", + JSON.stringify({ + state: { + connected: true, + connectedAccountIds: ["acct_personal"], + activeAccountId: "acct_personal", + }, + version: 0, + }), + ); + }); + + await page.reload(); +}); + +test("secrets page loads", async ({ page }) => { + await page.goto("/app/security/secrets"); + + await expect(page.getByTestId("secrets-page")).toBeVisible(); +}); diff --git a/apps/web/e2e/settings.spec.ts b/apps/web/e2e/settings.spec.ts new file mode 100644 index 0000000..183ffd6 --- /dev/null +++ b/apps/web/e2e/settings.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); + + await page.evaluate(() => { + localStorage.setItem( + "cleat-org", + JSON.stringify({ + state: { + connected: true, + connectedAccountIds: ["acct_personal"], + activeAccountId: "acct_personal", + }, + version: 0, + }), + ); + }); + + await page.reload(); +}); + +test("settings page loads", async ({ page }) => { + await page.goto("/app/settings"); + + await expect(page.getByTestId("settings-page")).toBeVisible(); +}); diff --git a/apps/web/e2e/supply-chain.spec.ts b/apps/web/e2e/supply-chain.spec.ts new file mode 100644 index 0000000..df61c83 --- /dev/null +++ b/apps/web/e2e/supply-chain.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); + + await page.evaluate(() => { + localStorage.setItem( + "cleat-org", + JSON.stringify({ + state: { + connected: true, + connectedAccountIds: ["acct_personal"], + activeAccountId: "acct_personal", + }, + version: 0, + }), + ); + }); + + await page.reload(); +}); + +test("supply chain page loads", async ({ page }) => { + await page.goto("/app/supply-chain"); + + await expect(page.getByTestId("supply-chain-page")).toBeVisible(); +}); diff --git a/apps/web/package.json b/apps/web/package.json index e82e874..d028603 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -5,6 +5,7 @@ "private": true, "devDependencies": { "@eslint/js": "^10.0.1", + "@playwright/test": "^1.61.0", "@tailwindcss/vite": "^4.3.0", "@types/bun": "latest", "@types/react": "^19.2.15", @@ -30,7 +31,6 @@ "date-fns": "^4.3.0", "lucide-react": "^1.16.0", "motion": "^12.40.0", - "playwright": "^1.60.0", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.15.1", @@ -41,6 +41,7 @@ "scripts": { "dev": "vite", "build": "vite build", + "e2e": "playwright test", "preview": "vite preview", "typecheck": "tsc --noEmit", "lint": "eslint .", @@ -48,5 +49,5 @@ "lint:fix": "eslint . --fix", "format:check": "prettier --check ." }, - "version": "1.7.1" + "version": "1.7.2" } diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts new file mode 100644 index 0000000..685fe84 --- /dev/null +++ b/apps/web/playwright.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + + use: { + baseURL: "http://localhost:5173", + headless: true, + }, + + webServer: { + command: "bun run dev", + url: "http://localhost:5173", + reuseExistingServer: true, + }, +}); diff --git a/apps/web/src/components/layout/PageHeader.tsx b/apps/web/src/components/layout/PageHeader.tsx index f2b7c12..0a38f6a 100644 --- a/apps/web/src/components/layout/PageHeader.tsx +++ b/apps/web/src/components/layout/PageHeader.tsx @@ -7,15 +7,18 @@ export function PageHeader({ description, actions, className, + "data-testid": dataTestId, }: { eyebrow?: ReactNode; title: ReactNode; description?: ReactNode; actions?: ReactNode; className?: string; + "data-testid"?: string; }) { return (
Failed to load artifacts data.