diff --git a/.gitea/workflows/e2e-ift.yml b/.gitea/workflows/e2e-ift.yml index 6e9b4b3..d38d764 100644 --- a/.gitea/workflows/e2e-ift.yml +++ b/.gitea/workflows/e2e-ift.yml @@ -1,6 +1,6 @@ name: E2E IFT (admin) -# После успешного Deploy IFT — полный UI-сьют (mock-тесты на живом SPA + smoke API). +# После успешного Deploy IFT — интеграционный UI-сьют против живого admin-api (без mocks). # Учётки smoke — из /opt/eventhub-ift/.env по SSH (SMOKE_ADMIN_* / ADMIN_SUPER_*). on: workflow_run: @@ -40,7 +40,7 @@ jobs: SSH_KEY: ${{ secrets.IFT_SSH_PRIVATE_KEY }} run: bash scripts/load-stand-smoke-env.sh ift - - name: E2E IFT (full UI suite) + - name: E2E IFT (integration, live API) env: E2E_TARGET: ift E2E_IFT_BASE_URL: ${{ secrets.E2E_IFT_BASE_URL }} diff --git a/.gitea/workflows/e2e-stage.yml b/.gitea/workflows/e2e-stage.yml index f279764..adcfd29 100644 --- a/.gitea/workflows/e2e-stage.yml +++ b/.gitea/workflows/e2e-stage.yml @@ -1,6 +1,6 @@ name: E2E stage (admin) -# После успешного Deploy stage — полный UI-сьют (mock-тесты на живом SPA + smoke API). +# После успешного Deploy stage — интеграционный UI-сьют против живого admin-api (без mocks). # Учётки smoke — из /opt/eventhub-stage/.env по SSH (SMOKE_ADMIN_* / ADMIN_SUPER_*). on: workflow_run: @@ -40,7 +40,7 @@ jobs: SSH_KEY: ${{ secrets.STAGE_SSH_PRIVATE_KEY }} run: bash scripts/load-stand-smoke-env.sh stage - - name: E2E stage (full UI suite) + - name: E2E stage (integration, live API) env: E2E_TARGET: stage E2E_STAGE_BASE_URL: ${{ secrets.E2E_STAGE_BASE_URL }} diff --git a/e2e/helpers/standAuth.ts b/e2e/helpers/standAuth.ts new file mode 100644 index 0000000..609996b --- /dev/null +++ b/e2e/helpers/standAuth.ts @@ -0,0 +1,32 @@ +import type { Page } from '@playwright/test'; + +/** Учётки superadmin со стенда (CI: load-stand-smoke-env.sh). */ +export function standAdminEmail(): string | undefined { + return process.env.SMOKE_ADMIN_EMAIL || process.env.ADMIN_SUPER_EMAIL || process.env.E2E_ADMIN_EMAIL; +} + +export function standAdminPassword(): string | undefined { + return ( + process.env.SMOKE_ADMIN_PASSWORD || + process.env.ADMIN_SUPER_PASSWORD || + process.env.E2E_ADMIN_PASSWORD + ); +} + +export function hasStandCredentials(): boolean { + return Boolean(standAdminEmail() && standAdminPassword()); +} + +/** Реальный login против admin-api стенда (без Playwright route mocks). */ +export async function loginAsStandAdmin(page: Page) { + const email = standAdminEmail(); + const password = standAdminPassword(); + if (!email || !password) { + throw new Error('Stand admin credentials missing (SMOKE_ADMIN_* / ADMIN_SUPER_*)'); + } + await page.goto('/login'); + await page.getByLabel(/email/i).fill(email); + await page.getByLabel(/пароль|password/i).fill(password); + await page.getByTestId('btn-login').click(); + await page.getByTestId('layout-shell').waitFor({ state: 'visible', timeout: 30_000 }); +} diff --git a/e2e/mocks/contentSeed.ts b/e2e/mocks/contentSeed.ts index fbca9e9..684b5e4 100644 --- a/e2e/mocks/contentSeed.ts +++ b/e2e/mocks/contentSeed.ts @@ -134,12 +134,14 @@ export function createContentSeed() { { id: 'audit-e2e-1', admin_id: 'admin-e2e-1', - action: 'user.update', - target_type: 'user', - target_id: 'user-e2e-1', - details: { status: 'frozen' }, + email: 'e2e@eventhub.local', + role: 'superadmin', + action: 'update_user', + entity_type: 'user', + entity_id: 'user-e2e-1', + timestamp: now, ip: '127.0.0.1', - created_at: now, + reason: 'e2e', }, ]; diff --git a/e2e/tests/stand/integration.spec.ts b/e2e/tests/stand/integration.spec.ts new file mode 100644 index 0000000..936b76f --- /dev/null +++ b/e2e/tests/stand/integration.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; +import { hasStandCredentials, loginAsStandAdmin } from '../../helpers/standAuth'; + +/** + * Интеграция UI ↔ admin-api на IFT/stage после деплоя. + * Без page.route mocks — только живой SPA и API стенда. + */ +test.describe('stand integration (live API)', () => { + test.skip(!hasStandCredentials(), 'SMOKE_ADMIN_* / ADMIN_SUPER_* from stand .env required'); + + test.beforeEach(async ({ page }) => { + await loginAsStandAdmin(page); + }); + + test('dashboard после login', async ({ page }) => { + await expect(page.getByTestId('page-dashboard')).toBeVisible(); + }); + + test('audit: страница и таблица без ошибки рендера', async ({ page }) => { + await page.goto('/audit'); + await expect(page.getByTestId('page-audit')).toBeVisible(); + await expect(page.getByTestId('filter-audit')).toBeVisible(); + await expect(page.getByText(/Ошибка отображения|Minified React error/i)).toHaveCount(0); + await expect(page.locator('[data-testid="page-audit"] table tbody tr').first()).toBeVisible({ + timeout: 20_000, + }); + }); + + test('users: список загружается', async ({ page }) => { + await page.goto('/users'); + await expect(page.getByTestId('page-users')).toBeVisible(); + await expect(page.getByText(/Ошибка отображения|Minified React error/i)).toHaveCount(0); + }); + + test('admins: superadmin видит список', async ({ page }) => { + await page.goto('/admins'); + await expect(page.getByTestId('page-admins')).toBeVisible(); + await expect(page.getByTestId('page-forbidden')).toHaveCount(0); + }); + + test('monitoring: страница загружается', async ({ page }) => { + await page.goto('/monitoring'); + await expect(page.getByTestId('page-monitoring')).toBeVisible(); + }); + + test('logout', async ({ page }) => { + await page.getByTestId('menu-user').click(); + await page.getByTestId('btn-logout').click(); + await expect(page.getByTestId('page-login')).toBeVisible(); + }); +}); diff --git a/package.json b/package.json index 8181b28..24b3591 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "lint": "eslint .", "preview": "vite preview", "test:e2e": "playwright test --project=mock", - "test:e2e:ift": "E2E_TARGET=ift playwright test --project=ift-ui --project=ift-smoke", - "test:e2e:stage": "E2E_TARGET=stage playwright test --project=stage-ui --project=stage-smoke" + "test:e2e:ift": "E2E_TARGET=ift playwright test --project=ift-ui", + "test:e2e:stage": "E2E_TARGET=stage playwright test --project=stage-ui" }, "dependencies": { "@hookform/resolvers": "^5.2.2", diff --git a/playwright.config.ts b/playwright.config.ts index 29e0902..30b2e5b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -10,6 +10,16 @@ const liveChrome = { ignoreHTTPSErrors: true, }; +/** Mock-suite (CI / локально): preview + page.route mocks. */ +const mockProject = { + name: 'mock', + use: { ...devices['Desktop Chrome'] }, + testIgnore: [/stand\//, /ift\//], +}; + +/** IFT/stage после деплоя: живой SPA + admin-api, без mocks. */ +const standTestMatch = /stand\//; + export default defineConfig({ testDir: './e2e/tests', fullyParallel: false, @@ -25,30 +35,16 @@ export default defineConfig({ locale: 'ru-RU', }, projects: [ - { - name: 'mock', - use: { ...devices['Desktop Chrome'] }, - testIgnore: /ift\//, - }, + mockProject, { name: 'ift-ui', use: { ...liveChrome, baseURL: iftBaseURL }, - testIgnore: /ift\//, - }, - { - name: 'ift-smoke', - use: { ...liveChrome, baseURL: iftBaseURL }, - testMatch: /ift\//, + testMatch: standTestMatch, }, { name: 'stage-ui', use: { ...liveChrome, baseURL: stageBaseURL }, - testIgnore: /ift\//, - }, - { - name: 'stage-smoke', - use: { ...liveChrome, baseURL: stageBaseURL }, - testMatch: /ift\//, + testMatch: standTestMatch, }, ], webServer: liveTarget