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>
84 lines
3.3 KiB
TypeScript
84 lines
3.3 KiB
TypeScript
/**
|
||
* Sprint 16 visual / authenticated-страницы:
|
||
* - /dashboard (с виджетами + chart)
|
||
* - /catalog/products (table)
|
||
* - /catalog/counterparties
|
||
* - /catalog/products/new (form)
|
||
* - /purchases/supplies
|
||
* - /purchases/supplies/new
|
||
* - /sales/retail
|
||
* - /sales/retail/new
|
||
* - /inventory/stock
|
||
* - /reports/sales
|
||
* - /reports/stock
|
||
* - /settings/organization
|
||
*
|
||
* 12 страниц × 2 темы = 24 snapshot'a per project (desktop+mobile = 48 total).
|
||
* Чтобы держать прогон под 15 мин, делаем один org per worker — фабрика
|
||
* вызывается в beforeAll.
|
||
*/
|
||
import { expect, test } from '@playwright/test'
|
||
import { OrgFactory, type BuiltOrg } from '../factories/OrgFactory.js'
|
||
import { attachSession } from '../lib/ui.js'
|
||
import { applyTheme } from './_helper.js'
|
||
|
||
const pages = [
|
||
{ path: '/dashboard', name: 'dashboard' },
|
||
{ path: '/catalog/products', name: 'products-list' },
|
||
{ path: '/catalog/counterparties', name: 'counterparties' },
|
||
{ path: '/catalog/products/new', name: 'product-new' },
|
||
{ path: '/purchases/supplies', name: 'supplies-list' },
|
||
{ path: '/purchases/supplies/new', name: 'supply-new' },
|
||
{ path: '/sales/retail', name: 'retail-list' },
|
||
{ path: '/sales/retail/new', name: 'retail-new' },
|
||
{ path: '/inventory/stock', name: 'stock' },
|
||
{ path: '/reports/sales', name: 'reports-sales' },
|
||
{ path: '/reports/stock', name: 'reports-stock' },
|
||
{ path: '/settings/organization', name: 'org-settings' },
|
||
]
|
||
|
||
// Один org на весь файл — 12 страниц × 2 темы = 24 snapshot'a одной сессией.
|
||
let built: BuiltOrg
|
||
|
||
test.beforeAll(async () => {
|
||
built = await OrgFactory.for('visual')
|
||
.withProducts(3)
|
||
.withCounterparties(2)
|
||
.withSupplies(1)
|
||
.build()
|
||
})
|
||
|
||
/** Маски для динамического контента (артикулы с Date.now, KPI'ы с
|
||
* текущей датой, текущее время). Без масок 0.2% threshold завышает
|
||
* diff'ы по «гуляющему» контенту между прогонами. */
|
||
function masks(page: import('@playwright/test').Page) {
|
||
return [
|
||
// Артикулы в таблицах товаров (содержат Date.now()).
|
||
page.locator('table td:nth-child(2)'),
|
||
// KPI-блоки на dashboard.
|
||
page.locator('[data-kpi]'),
|
||
// delta-стрелки «+12%».
|
||
page.locator('[data-delta]'),
|
||
]
|
||
}
|
||
|
||
for (const p of pages) {
|
||
test(`${p.name} light`, async ({ page }) => {
|
||
await attachSession(page, built.session, p.path)
|
||
await page.waitForLoadState('networkidle')
|
||
await applyTheme(page, 'light')
|
||
// На пути /reports/* картинки чарта мокаются ленивым chunk'ом —
|
||
// подождать дополнительно.
|
||
if (p.path.includes('/reports')) await page.waitForTimeout(500)
|
||
await expect(page).toHaveScreenshot(`${p.name}-light.png`, { mask: masks(page) })
|
||
})
|
||
|
||
test(`${p.name} dark`, async ({ page }) => {
|
||
await attachSession(page, built.session, p.path)
|
||
await page.waitForLoadState('networkidle')
|
||
await applyTheme(page, 'dark')
|
||
if (p.path.includes('/reports')) await page.waitForTimeout(500)
|
||
await expect(page).toHaveScreenshot(`${p.name}-dark.png`, { mask: masks(page) })
|
||
})
|
||
}
|