feat: добавил тесты с использованием Playwright
Frontend CI / build-and-check (push) Failing after 19s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 8s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 8s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 20s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 3s
Frontend CI / build-and-check (push) Failing after 19s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 8s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 8s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 20s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 3s
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
name: Frontend Playwright
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: frontend
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: frontend/pnpm-lock.yaml
|
||||
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build app
|
||||
run: pnpm build-only
|
||||
|
||||
- name: Install Playwright browser
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: Run e2e
|
||||
run: pnpm test:e2e
|
||||
@@ -0,0 +1,111 @@
|
||||
# Playwright E2E тесты frontend
|
||||
|
||||
## Назначение
|
||||
|
||||
Playwright-тесты проверяют ключевые браузерные сценарии frontend-приложения UniVerse:
|
||||
|
||||
- редирект неавторизованного пользователя на страницу входа;
|
||||
- отображение каталога открытых лекций;
|
||||
- запись на доступную лекцию.
|
||||
|
||||
Тесты работают поверх production preview сборки frontend и используют mock API, поэтому для базового запуска не нужен поднятый backend.
|
||||
|
||||
## Где лежат файлы
|
||||
|
||||
- [playwright.config.ts](../frontend/playwright.config.ts) - конфигурация Playwright.
|
||||
- [auth.spec.ts](../frontend/tests/e2e/auth.spec.ts) - сценарии аутентификации.
|
||||
- [catalog.spec.ts](../frontend/tests/e2e/catalog.spec.ts) - сценарии каталога лекций.
|
||||
- [mockApi.ts](../frontend/tests/e2e/support/mockApi.ts) - перехват и mock ответов API.
|
||||
- [fixtures.ts](../frontend/tests/mocks/fixtures.ts) - тестовые данные.
|
||||
|
||||
## Как устроен запуск
|
||||
|
||||
Конфигурация находится во `frontend/playwright.config.ts`.
|
||||
|
||||
Основные параметры:
|
||||
|
||||
- `testDir: ./tests/e2e` - Playwright ищет тесты в папке `frontend/tests/e2e`.
|
||||
- `baseURL: http://127.0.0.1:4173` - базовый адрес приложения в тестах.
|
||||
- `webServer` запускает `pnpm preview --host 127.0.0.1 --port 4173`.
|
||||
- В CI включены `2` retry и GitHub reporter.
|
||||
- Локально используется list reporter.
|
||||
- По умолчанию проект запускается в Chromium.
|
||||
|
||||
## Команды
|
||||
|
||||
Запуск всех E2E-тестов из корня репозитория:
|
||||
|
||||
```bash
|
||||
pnpm -C frontend test:e2e
|
||||
```
|
||||
|
||||
Интерактивный UI Playwright:
|
||||
|
||||
```bash
|
||||
pnpm -C frontend test:e2e:ui
|
||||
```
|
||||
|
||||
Если нужно вручную поднять preview-сервер:
|
||||
|
||||
```bash
|
||||
pnpm -C frontend build-only
|
||||
pnpm -C frontend test:e2e:preview
|
||||
```
|
||||
|
||||
После этого можно запускать Playwright с переменной `PW_SKIP_WEB_SERVER=1`, чтобы он не стартовал свой `webServer`.
|
||||
|
||||
## Mock API
|
||||
|
||||
Тесты не обращаются к реальному backend. Вместо этого helper `mockApi(page, options)` перехватывает запросы к `/api/v1` через `page.route`.
|
||||
|
||||
Сейчас замоканы:
|
||||
|
||||
- `POST/GET /api/v1/auth/refresh` - refresh авторизации;
|
||||
- `/api/v1/auth/me` - текущий пользователь;
|
||||
- `/api/v1/users/me/stats` - статистика студента;
|
||||
- `/api/v1/lectures` - список лекций;
|
||||
- `/api/v1/lectures/{id}/enroll` - запись на лекцию.
|
||||
|
||||
Для авторизованного сценария используйте:
|
||||
|
||||
```ts
|
||||
await mockApi(page, { authenticated: true })
|
||||
```
|
||||
|
||||
Для проверки гостевого сценария:
|
||||
|
||||
```ts
|
||||
await mockApi(page, { authenticated: false })
|
||||
```
|
||||
|
||||
## Как добавлять новые тесты
|
||||
|
||||
1. Создавайте spec-файлы в `frontend/tests/e2e`.
|
||||
2. Для страниц, которым нужен backend, сначала добавляйте нужные ответы в `mockApi.ts` и данные в `fixtures.ts`.
|
||||
3. Проверяйте пользовательский результат через role/text/label locators: `getByRole`, `getByText`, `getByLabel`.
|
||||
4. Не завязывайтесь на CSS-классы, если сценарий можно проверить через доступные пользователю элементы.
|
||||
5. Для маршрутов под авторизацией вызывайте `mockApi(page, { authenticated: true })` до `page.goto(...)`.
|
||||
|
||||
## CI
|
||||
|
||||
Workflow находится в [.gitea/workflows/frontend-playwright.yml](../.gitea/workflows/frontend-playwright.yml).
|
||||
|
||||
Пайплайн:
|
||||
|
||||
1. Устанавливает зависимости через `pnpm install --frozen-lockfile`.
|
||||
2. Собирает frontend командой `pnpm build-only`.
|
||||
3. Устанавливает браузер Playwright Chromium.
|
||||
4. Запускает `pnpm test:e2e`.
|
||||
|
||||
## Артефакты и отладка
|
||||
|
||||
Playwright сохраняет trace на первом retry: `trace: on-first-retry`.
|
||||
|
||||
Локально полезные команды:
|
||||
|
||||
```bash
|
||||
pnpm -C frontend exec playwright show-report
|
||||
pnpm -C frontend exec playwright show-trace ./test-results/<папка>/trace.zip
|
||||
```
|
||||
|
||||
Папки `frontend/test-results` и `frontend/playwright-report` считаются временными артефактами тестовых прогонов.
|
||||
@@ -35,5 +35,10 @@ coverage
|
||||
# Vitest
|
||||
__screenshots__/
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
|
||||
# Vite
|
||||
*.timestamp-*-*.mjs
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
"lint": "run-s lint:*",
|
||||
"lint:oxlint": "oxlint . --fix",
|
||||
"lint:eslint": "eslint . --fix --cache",
|
||||
"format": "prettier --write --experimental-cli src/"
|
||||
"format": "prettier --write --experimental-cli src/",
|
||||
"test:e2e:preview": "vite preview --host 127.0.0.1 --port 4173",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:ui:edge": "PW_USE_SYSTEM_EDGE=1 PW_SKIP_WEB_SERVER=1 playwright test --ui --timeout=0 --workers=1"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^3.0.4",
|
||||
@@ -38,7 +42,8 @@
|
||||
"typescript": "~6.0.0",
|
||||
"vite": "^8.0.8",
|
||||
"vite-plugin-vue-devtools": "^8.1.1",
|
||||
"vue-tsc": "^3.2.6"
|
||||
"vue-tsc": "^3.2.6",
|
||||
"@playwright/test": "^1.54.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
const useSystemEdge = process.env.PW_USE_SYSTEM_EDGE === '1'
|
||||
const skipWebServer = process.env.PW_SKIP_WEB_SERVER === '1'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
fullyParallel: true,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:4173',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
webServer: skipWebServer
|
||||
? undefined
|
||||
: {
|
||||
command: 'pnpm preview --host 127.0.0.1 --port 4173',
|
||||
url: 'http://127.0.0.1:4173',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
cwd: '.',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: useSystemEdge ? 'msedge' : 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
...(useSystemEdge ? { channel: 'msedge' } : {}),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
Generated
+38
@@ -18,6 +18,9 @@ importers:
|
||||
specifier: ^5.0.6
|
||||
version: 5.0.6(@vue/compiler-sfc@3.5.34)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))
|
||||
devDependencies:
|
||||
'@playwright/test':
|
||||
specifier: ^1.54.2
|
||||
version: 1.54.2
|
||||
'@tsconfig/node24':
|
||||
specifier: ^24.0.4
|
||||
version: 24.0.4
|
||||
@@ -433,6 +436,11 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@playwright/test@1.54.2':
|
||||
resolution: {integrity: sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
@@ -1008,6 +1016,11 @@ packages:
|
||||
flatted@3.4.2:
|
||||
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -1347,6 +1360,16 @@ packages:
|
||||
pkg-types@2.3.1:
|
||||
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
|
||||
|
||||
playwright-core@1.54.2:
|
||||
resolution: {integrity: sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.54.2:
|
||||
resolution: {integrity: sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
postcss-safe-parser@7.0.1:
|
||||
resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==}
|
||||
engines: {node: '>=18.0'}
|
||||
@@ -2011,6 +2034,10 @@ snapshots:
|
||||
'@oxlint/binding-win32-x64-msvc@1.60.0':
|
||||
optional: true
|
||||
|
||||
'@playwright/test@1.54.2':
|
||||
dependencies:
|
||||
playwright: 1.54.2
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@rolldown/binding-android-arm64@1.0.0-rc.17':
|
||||
@@ -2606,6 +2633,9 @@ snapshots:
|
||||
|
||||
flatted@3.4.2: {}
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -2889,6 +2919,14 @@ snapshots:
|
||||
exsolve: 1.0.8
|
||||
pathe: 2.0.3
|
||||
|
||||
playwright-core@1.54.2: {}
|
||||
|
||||
playwright@1.54.2:
|
||||
dependencies:
|
||||
playwright-core: 1.54.2
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
postcss-safe-parser@7.0.1(postcss@8.5.14):
|
||||
dependencies:
|
||||
postcss: 8.5.14
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { mockApi } from './support/mockApi'
|
||||
|
||||
test('redirects unauthenticated user to login', async ({ page }) => {
|
||||
await mockApi(page, { authenticated: false })
|
||||
await page.goto('/catalog')
|
||||
await expect(page).toHaveURL(/\/login/)
|
||||
await expect(page.getByText('Войти через ЮФУ')).toBeVisible()
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { mockApi } from './support/mockApi'
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await mockApi(page, { authenticated: true })
|
||||
})
|
||||
|
||||
test('renders catalog items from mock api', async ({ page }) => {
|
||||
await page.goto('/catalog')
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Каталог открытых лекций' })).toBeVisible()
|
||||
await expect(page.getByText('Введение в ML')).toBeVisible()
|
||||
await expect(page.getByText('Квантовые вычисления')).toBeVisible()
|
||||
})
|
||||
|
||||
test('register button works for available lecture', async ({ page }) => {
|
||||
await page.goto('/catalog')
|
||||
|
||||
const firstRegisterButton = page.getByRole('button', { name: 'Записаться' }).first()
|
||||
await firstRegisterButton.click()
|
||||
|
||||
await expect(page.getByText('Вы записаны на лекцию.')).toBeVisible()
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Page } from '@playwright/test'
|
||||
import { mockAuthResponse, mockCurrentUser, mockLectures, mockUserStats } from '../../mocks/fixtures'
|
||||
|
||||
export async function mockApi(page: Page, options?: { authenticated?: boolean }) {
|
||||
const authenticated = options?.authenticated ?? false
|
||||
|
||||
await page.route('**/api/v1/auth/refresh', async (route) => {
|
||||
if (!authenticated) {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ message: 'Unauthorized' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockAuthResponse),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route('**/api/v1/auth/me', async (route) => {
|
||||
if (!authenticated) {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ message: 'Unauthorized' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockCurrentUser),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route('**/api/v1/users/me/stats', async (route) => {
|
||||
if (!authenticated) {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ message: 'Unauthorized' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockUserStats),
|
||||
})
|
||||
})
|
||||
|
||||
await page.route('**/api/v1/lectures/*/enroll', async (route) => {
|
||||
await route.fulfill({ status: 204 })
|
||||
})
|
||||
|
||||
await page.route(/\/api\/v1\/lectures(\?.*)?$/, async (route) => {
|
||||
if (route.request().method() !== 'GET') {
|
||||
await route.continue()
|
||||
return
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ items: mockLectures }),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
export const mockAuthResponse = {
|
||||
accessToken: 'fake-token',
|
||||
expiresAt: '2099-01-01T00:00:00.000Z',
|
||||
user: {
|
||||
id: 1,
|
||||
email: 'student@example.com',
|
||||
displayName: 'Test User',
|
||||
roles: ['Student'],
|
||||
},
|
||||
}
|
||||
|
||||
export const mockCurrentUser = {
|
||||
id: 1,
|
||||
email: 'student@example.com',
|
||||
displayName: 'Test User',
|
||||
roles: ['Student'],
|
||||
avatarUrl: null,
|
||||
xp: 120,
|
||||
coins: 10,
|
||||
level: 2,
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
}
|
||||
|
||||
export const mockUserStats = {
|
||||
totalLectures: 0,
|
||||
attendedLectures: 0,
|
||||
totalReviews: 0,
|
||||
xp: 120,
|
||||
coins: 10,
|
||||
level: 2,
|
||||
achievementsCount: 0,
|
||||
currentLevelXp: 20,
|
||||
nextLevelXp: 100,
|
||||
activeEnrollments: 0,
|
||||
enrollmentSlotLimit: 3,
|
||||
enrollmentSlotRules: [],
|
||||
}
|
||||
|
||||
export const mockLectures = [
|
||||
{
|
||||
id: 1,
|
||||
courseId: 101,
|
||||
courseName: 'ML',
|
||||
teacherId: 201,
|
||||
teacherName: 'Иванов И.И.',
|
||||
locationId: 301,
|
||||
locationName: 'B-1 / 101',
|
||||
title: 'Введение в ML',
|
||||
description: 'База машинного обучения',
|
||||
format: 'Offline',
|
||||
startsAt: '2026-06-12T10:00:00.000Z',
|
||||
endsAt: '2026-06-12T11:30:00.000Z',
|
||||
isOpen: true,
|
||||
maxEnrollments: 30,
|
||||
enrollmentsCount: 10,
|
||||
onlineUrl: null,
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
isEnrolled: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
courseId: 102,
|
||||
courseName: 'квантовые-вычисления',
|
||||
teacherId: 202,
|
||||
teacherName: 'Петров П.П.',
|
||||
locationId: null,
|
||||
locationName: null,
|
||||
title: 'Квантовые вычисления',
|
||||
description: 'Кубиты и алгоритмы',
|
||||
format: 'Online',
|
||||
startsAt: '2026-06-13T10:00:00.000Z',
|
||||
endsAt: '2026-06-13T11:00:00.000Z',
|
||||
isOpen: false,
|
||||
maxEnrollments: 50,
|
||||
enrollmentsCount: 50,
|
||||
onlineUrl: 'https://example.com/meet',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
isEnrolled: false,
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user