From e70893148cabe089312f748772aae4fe651463cf Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 23 Jun 2026 19:15:04 +0300 Subject: [PATCH] :white_check_mark: test(e2e): role-based navigation and permission acceptance for E03 - Playwright e2e (mocked API) for internal-ui: OWNER lands on the command center with the global sidebar; OPERATOR lands on sales dialogs with the sales sidebar and no company-level navigation; owner-only route shows the 403 permission screen; shell renders at the 1024px minimum width - auto-start the internal-ui dev server via Playwright webServer Co-Authored-By: Claude Opus 4.8 --- playwright.config.ts | 6 +++ tests/e2e/internal-ui.spec.ts | 84 +++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 tests/e2e/internal-ui.spec.ts diff --git a/playwright.config.ts b/playwright.config.ts index fb09d48..7d004d1 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -8,6 +8,12 @@ export default defineConfig({ use: { trace: "on-first-retry", }, + webServer: { + command: "npm --workspace @edevs/internal-ui run dev", + url: "http://localhost:5173", + reuseExistingServer: true, + timeout: 120_000, + }, projects: [ { name: "internal-ui", diff --git a/tests/e2e/internal-ui.spec.ts b/tests/e2e/internal-ui.spec.ts new file mode 100644 index 0000000..4e8d8ea --- /dev/null +++ b/tests/e2e/internal-ui.spec.ts @@ -0,0 +1,84 @@ +import { expect, type Page, test } from "@playwright/test"; + +// Эти сценарии относятся только к internal-ui; на других проектах пропускаем. +test.beforeEach(({}, testInfo) => { + test.skip(testInfo.project.name !== "internal-ui", "internal-ui only"); +}); + +const OWNER = { + id: 1, + email: "owner@edevs.tech", + fullName: "Владелец", + role: "OWNER", + organization: "edevs", + organizationName: "Edevs", + department: null, + mustChangePassword: false, + totpRequired: false, + totpEnabled: false, +}; + +const OPERATOR = { ...OWNER, id: 2, email: "operator@edevs.tech", fullName: "Оператор", role: "OPERATOR", department: "sales" }; + +async function mockData(page: Page) { + await page.route("**/api/v1/employees/**", (route) => route.fulfill({ json: { items: [] } })); + await page.route("**/api/v1/company/departments/**", (route) => route.fulfill({ json: { items: [] } })); + await page.route("**/api/v1/company/products/**", (route) => route.fulfill({ json: { items: [] } })); +} + +async function mockSession(page: Page, user: object | null) { + await page.route("**/api/v1/auth/session/", (route) => + route.fulfill({ json: user ? { authenticated: true, user } : { authenticated: false } }), + ); +} + +async function login(page: Page, user: object) { + await mockSession(page, null); + await mockData(page); + await page.route("**/api/v1/auth/login/", (route) => route.fulfill({ json: { authenticated: true, user } })); + await page.goto("/"); + await page.getByPlaceholder("you@edevs.tech").fill("user@edevs.tech"); + await page.getByPlaceholder("Пароль").fill("Password-123"); + await page.getByRole("button", { name: "Войти" }).click(); +} + +test("OWNER logs in and lands on the command center with the global sidebar", async ({ page }) => { + await login(page, OWNER); + + await expect(page).toHaveURL(/\/$|\/command/); + await expect(page.getByRole("heading", { name: "Командный центр" })).toBeVisible(); + // Глобальный sidebar уровня компании. + await expect(page.getByText("Уровень компании")).toBeVisible(); + await expect(page.getByText("Рабочее пространство")).toHaveCount(0); +}); + +test("OPERATOR logs in and lands on sales dialogs with the sales sidebar", async ({ page }) => { + await login(page, OPERATOR); + + await expect(page).toHaveURL(/\/departments\/sales\/dialogs/); + // Операторская навигация показывает только рабочее пространство продаж. + await expect(page.getByText("Рабочее пространство")).toBeVisible(); + await expect(page.getByText("Уровень компании")).toHaveCount(0); + await expect(page.getByRole("button", { name: "Назад в Hub" })).toHaveCount(0); +}); + +test("OPERATOR opening an owner-only route sees the 403 permission screen", async ({ page }) => { + await mockSession(page, OPERATOR); + await mockData(page); + + await page.goto("/employees"); + + await expect(page.getByText("403 · Доступ запрещён")).toBeVisible(); + await expect(page.getByRole("button", { name: "Вернуться" })).toBeVisible(); +}); + +test("internal UI renders at the minimum supported width of 1024px", async ({ page }) => { + await page.setViewportSize({ width: 1024, height: 768 }); + await mockSession(page, OWNER); + await mockData(page); + + await page.goto("/"); + + await expect(page.getByRole("heading", { name: "Командный центр" })).toBeVisible(); + await page.screenshot({ path: "test-results/internal-ui-owner-1024.png" }); +});