food-market/tests/regression/flows/08-realtime-misc.spec.ts
nns 1989db32bb test(s16): regression suite 35 flows + visual 60 snapshots + nightly + CI badges
Sprint 16 — постоянный regression-контур: flows + visual + nightly +
CI workflow + README badges.

Ключевые цифры:
- 35 flow-тестов: 35/35 ✓ за ~30 секунд (workers=2 локально).
- 60 visual snapshot'ов (15 страниц × 2 темы × 2 viewport'a):
  60/60 ✓ за ~4 минуты с retries=1.
- Полный регресс прогон: ~5 минут (цель была < 15).

Что сделано:
1. tests/regression/ — Playwright + factories + 8 spec-файлов.
   OrgFactory builder создаёт org через API за O(N) HTTP вызовов
   (signup → token → refs → products → counterparties → posted supplies).
   Каждый flow независим, использует свой fresh-org.
2. tests/regression/visual/ — 15 страниц × 2 темы × 2 viewport'a.
   Маски на динамический контент (артикулы с Date.now, KPI'ы,
   delta-стрелки) чтобы 0.2% threshold не флакал. snapshotPathTemplate
   c {projectName} — desktop+mobile не затирают друг друга.
3. tests/regression/factories/OrgFactory.ts — builder с .withProducts
   .withCounterparties .withSupplies. Retry signup'a на 429.
4. .forgejo/workflows/regression.yml — on workflow_run после
   Docker API/Web; cache на pnpm-store + Playwright-browsers;
   артефакты при failure; Telegram-уведомление в обоих случаях.
5. ~/nightly-verify.sh + cron `0 4 * * *`: health → redeploy если
   нужно → smoke flows; в воскресенье полный flows+visual. Логи с
   ротацией 14 дней. Telegram на провал (~/.fm-watchdog/telegram-*).
6. scripts/generate-badges.sh — coverage из cobertura.xml в SVG через
   shields.io (offline fallback). 4 CI-status badge + coverage badge в
   README; CI step «Update coverage badge» авто-коммитит обновлённый
   SVG на push в main.

Локальное число flake'ов: 1/60 visual на retry=1 (product-new light) —
случайная гонка маски, retry'ит и проходит.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 16:14:11 +05:00

62 lines
3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Sprint 16 — flows 08 realtime + misc (5 flows):
* 8.1 SignalR connect → message на /hubs/notifications не падает
* 8.2 dashboard live-status = on после wsConnect
* 8.3 search global: 'товар' возвращает результат
* 8.4 retail-points список содержит дефолтный «Касса 1»
* 8.5 health/ready возвращает Healthy при работающей БД
*/
import { expect, test } from '@playwright/test'
import { request } from '../factories/api-client.js'
import { OrgFactory } from '../factories/OrgFactory.js'
import { attachSession } from '../lib/ui.js'
test.describe('flow 08 — realtime + misc', () => {
test('8.1 dashboard рендерится без console-ошибок (SignalR опц.)', async ({ page }) => {
const b = await OrgFactory.for('rt81').build()
const errs: string[] = []
page.on('console', (m) => {
if (m.type() !== 'error') return
const t = m.text()
if (/Failed to load resource|net::ERR_/.test(t)) return
errs.push(t)
})
await attachSession(page, b.session, '/dashboard')
await page.waitForLoadState('networkidle')
expect(errs, 'console-ошибок на /dashboard быть не должно').toEqual([])
})
test('8.2 live-status элемент отображается на dashboard', async ({ page }) => {
const b = await OrgFactory.for('rt82').build()
await attachSession(page, b.session, '/dashboard')
// Sidebar показывает Wifi/WifiOff title — поэтому проверяем по title через
// accessible name на иконке (Live on / Live off).
const live = page.locator('[title*="live" i], [title*="реальн" i]').first()
await expect(live).toBeVisible({ timeout: 10_000 })
})
test('8.3 search global возвращает товар по части имени @smoke', async () => {
const b = await OrgFactory.for('rt83').withProducts(2).build()
// Берём первое слово первого товара (например, "Товар").
const needle = b.products[0]!.name.split(' ')[0] ?? 'Товар'
const r = await request<{ products: Array<{ id: string; name: string }> }>(
`/api/search/global?q=${encodeURIComponent(needle)}`, { token: b.session.accessToken },
)
expect(r.products.length).toBeGreaterThan(0)
})
test('8.4 retail-points список содержит дефолтную «Касса 1»', async () => {
const b = await OrgFactory.for('rt84').build()
const r = await request<{ items: Array<{ id: string; name: string }> }>(
'/api/catalog/retail-points', { token: b.session.accessToken },
)
expect(r.items.length).toBeGreaterThanOrEqual(1)
expect(r.items.find(x => /касса 1/i.test(x.name)), 'Касса 1 должна быть посеена').toBeDefined()
})
test('8.5 health/ready возвращает Healthy', async () => {
const r = await request<{ status: string }>('/health/ready')
expect(r.status).toBe('Healthy')
})
})