From 1ff0476ff37c9934ee249b715130a0c8432cc4e3 Mon Sep 17 00:00:00 2001 From: Gunel Shukurova Date: Sun, 21 Jun 2026 18:12:11 +0400 Subject: [PATCH 1/5] test: add Playwright e2e coverage --- apps/web/e2e/access.spec.ts | 27 +++++++++++++++ apps/web/e2e/artifacts.spec.ts | 33 +++++++++++++++++++ apps/web/e2e/code-scanning.spec.ts | 27 +++++++++++++++ apps/web/e2e/dependencies.spec.ts | 33 +++++++++++++++++++ apps/web/e2e/onboarding.spec.ts | 19 +++++++++++ apps/web/e2e/overview.spec.ts | 33 +++++++++++++++++++ apps/web/e2e/repositories.spec.ts | 29 ++++++++++++++++ apps/web/e2e/repository-detail.spec.ts | 27 +++++++++++++++ apps/web/e2e/secrets.spec.ts | 29 ++++++++++++++++ apps/web/e2e/settings.spec.ts | 28 ++++++++++++++++ apps/web/e2e/supply-chain.spec.ts | 28 ++++++++++++++++ apps/web/package.json | 3 +- apps/web/playwright.config.ts | 16 +++++++++ apps/web/src/components/layout/PageHeader.tsx | 3 ++ apps/web/src/components/ui/StatTile.tsx | 4 ++- apps/web/src/features/access/AccessPage.tsx | 2 +- .../src/features/artifacts/ArtifactsPage.tsx | 14 +++++--- .../dependencies/DependenciesPage.tsx | 2 +- .../notifications/NotificationsPage.tsx | 2 +- .../src/features/overview/OverviewPage.tsx | 4 ++- .../features/repositories/RepoDetailPage.tsx | 21 ++++++++---- .../repositories/RepositoriesPage.tsx | 2 +- .../features/security/CodeScanningPage.tsx | 14 +++++--- .../web/src/features/security/SecretsPage.tsx | 14 +++++--- .../features/security/VulnerabilitiesPage.tsx | 2 +- .../src/features/settings/SettingsPage.tsx | 2 +- apps/web/src/features/shared/ComingSoon.tsx | 4 ++- .../features/supplyChain/SupplyChainPage.tsx | 14 +++++--- apps/web/test-results/.last-run.json | 4 +++ bun.lock | 10 +++--- 30 files changed, 414 insertions(+), 36 deletions(-) create mode 100644 apps/web/e2e/access.spec.ts create mode 100644 apps/web/e2e/artifacts.spec.ts create mode 100644 apps/web/e2e/code-scanning.spec.ts create mode 100644 apps/web/e2e/dependencies.spec.ts create mode 100644 apps/web/e2e/onboarding.spec.ts create mode 100644 apps/web/e2e/overview.spec.ts create mode 100644 apps/web/e2e/repositories.spec.ts create mode 100644 apps/web/e2e/repository-detail.spec.ts create mode 100644 apps/web/e2e/secrets.spec.ts create mode 100644 apps/web/e2e/settings.spec.ts create mode 100644 apps/web/e2e/supply-chain.spec.ts create mode 100644 apps/web/playwright.config.ts create mode 100644 apps/web/test-results/.last-run.json 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..9ea5f2c --- /dev/null +++ b/apps/web/e2e/secrets.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("secrets page loads", async ({ page }) => { + await page.goto("/app/security/secrets"); + + console.log(await page.locator("body").innerText()); + + 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..7a3181d --- /dev/null +++ b/apps/web/e2e/settings.spec.ts @@ -0,0 +1,28 @@ +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..8e5de4c --- /dev/null +++ b/apps/web/e2e/supply-chain.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from "@playwright/test"; + +// Инициализируем сессию в localStorage перед тестом supply-chain +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..04ab3e6 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 .", 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 (
diff --git a/apps/web/src/components/ui/StatTile.tsx b/apps/web/src/components/ui/StatTile.tsx index d54712b..57fe3fa 100644 --- a/apps/web/src/components/ui/StatTile.tsx +++ b/apps/web/src/components/ui/StatTile.tsx @@ -14,6 +14,7 @@ interface StatTileProps { hint?: ReactNode; accent?: string; className?: string; + "data-testid"?: string; } export function StatTile({ @@ -25,13 +26,14 @@ export function StatTile({ hint, accent, className, + "data-testid": dataTestId, }: StatTileProps) { const hasDelta = typeof delta === "number"; const isUp = (delta ?? 0) >= 0; const isGood = goodDirection === "up" ? isUp : !isUp; return ( - + {accent && ( m.outsideCollaborator).length; return ( -
+
+
@@ -66,7 +66,10 @@ export function ArtifactsPage() { if (error) { return ( -
+

Failed to load artifacts data.