food-market/tests/regression/flows/09-onboarding-wizard.spec.ts
nns f56c6efab1 feat(s17): onboarding wizard + help kb + feedback + diagnostic + whats-new
Sprint 17 — onboarding-контур: 4-шаг wizard, контекстный help, in-app
feedback, admin self-diagnostic, /whats-new из CHANGELOG.md.

Ключевые цифры:
- Wizard: 4 шага + skip каждого, 7 e2e тестов ✓ за 20 секунд.
- Diagnostic: 7 параллельных проверок, ~80ms на stage.
- Bundle impact: initial +4 KB gzip (только FeedbackWidget +
  HelpTooltip + EmptyStateWithDemo в основном bundle; страницы lazy).
- Regression-suite: 35 → 42 flows + 60 → 66 visual snapshots.

Backend (новые endpoint'ы):
- /api/admin/diagnostic/run — 7 параллельных проверок (DB, SMTP,
  MinIO, Hangfire, диск, сертификаты, бэкап). Task.WhenAll, ~80ms.
- /api/feedback — POST {category, message}, email на FromEmail +
  Telegram (если SupportTelegram:* настроены). Rate-limit 5/час.
- /api/whats-new — парсер CHANGELOG.md, возвращает {buildVersion,
  items}. Dockerfile.api копирует CHANGELOG.md в content-root +
  пишет VERSION из GIT_SHA build-arg.

Frontend:
- /onboarding-wizard — 4-step builder, состояние в useState,
  localStorage.fm.wizardCompleted после завершения.
- <HelpTooltip topic="key"/> — popover на каждой странице, mapping
  src/lib/help-topics.ts (13 keys).
- /help — knowledge base, 7 markdown topics через import.meta.glob,
  mini-renderer без heavy deps, fuzzy search.
- /whats-new — список из /api/whats-new, иконки по типу (feat/fix).
- /admin/diagnostic — Admin/SuperAdmin only, 🟢/🟡/🔴 индикаторы.
- <FeedbackWidget> в sidebar footer + ссылки на /help и /whats-new.
- <EmptyStateWithDemo> placeholder для будущих видео-демо.

scripts/generate-changelog.sh — git log feat:/fix: за 90 дней
→ CHANGELOG.md (307 строк сгенерировано).

Wizard UX-screenshots в docs/sprint17-screenshots/ (6 PNG: 4 шага +
help + diagnostic).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 17:04:26 +05:00

106 lines
5.6 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 17 — flow 09 onboarding wizard (4 шага):
* 9.1 wizard рендерится на /onboarding-wizard и показывает 4 шага через progress-bar
* 9.2 skip всех шагов → /dashboard + localStorage.fm.wizardCompleted=1
* 9.3 шаг 1: сохранение названия магазина обновляет org.name
* 9.4 /help рендерит markdown topics + поиск
* 9.5 /admin/diagnostic возвращает 7 проверок (sendTestEmail=false)
* 9.6 POST /api/feedback с минимальным payload → ok
* 9.7 /api/whats-new возвращает массив items
*/
import { expect, test } from '@playwright/test'
import { OrgFactory } from '../factories/OrgFactory.js'
import { request } from '../factories/api-client.js'
import { attachSession } from '../lib/ui.js'
test.describe('flow 09 — onboarding wizard + help + diagnostic + feedback + whats-new', () => {
test('9.1 wizard рендерится с 4-шаговым progress-bar @smoke', async ({ page }) => {
const b = await OrgFactory.for('wiz91').build()
await attachSession(page, b.session, '/onboarding-wizard')
await expect(page.getByText(/Шаг 1 из 4/)).toBeVisible()
await expect(page.getByRole('heading', { name: /Магазин/i })).toBeVisible()
})
test('9.2 skip всех 4 шагов → /dashboard + wizardCompleted', async ({ page }) => {
const b = await OrgFactory.for('wiz92').build()
await attachSession(page, b.session, '/onboarding-wizard')
// Шаг 1: «Пропустить» (две кнопки — основная и нижняя; используем основную в footer'е модала).
await page.getByRole('button', { name: 'Пропустить', exact: true }).first().click()
// Шаг 2.
await expect(page.getByText(/Шаг 2 из 4/)).toBeVisible()
await page.getByRole('button', { name: 'Пропустить', exact: true }).first().click()
// Шаг 3 — кнопка «Сделать позже».
await expect(page.getByText(/Шаг 3 из 4/)).toBeVisible()
await page.getByRole('button', { name: /Сделать позже/i }).click()
// Шаг 4 — «Не нужно» завершает.
await expect(page.getByText(/Шаг 4 из 4/)).toBeVisible()
await page.getByRole('button', { name: /Не нужно/i }).click()
await page.waitForURL(/\/dashboard/, { timeout: 10_000 })
const flag = await page.evaluate(() => localStorage.getItem('fm.wizardCompleted'))
expect(flag).toBe('1')
})
test('9.3 шаг 1: сохранение названия магазина обновляет org', async ({ page }) => {
const b = await OrgFactory.for('wiz93').build()
await attachSession(page, b.session, '/onboarding-wizard')
const newName = `WizardOrg-${Date.now()}`
await page.getByLabel(/Название магазина/).fill(newName)
await page.getByRole('button', { name: /Дальше/i }).click()
await expect(page.getByText(/Шаг 2 из 4/)).toBeVisible()
// Проверяем что сохранилось.
const settings = await request<{ name: string }>(
'/api/organization/settings', { token: b.session.accessToken },
)
expect(settings.name).toBe(newName)
})
test('9.4 /help рендерит markdown topics с поиском @smoke', async ({ page }) => {
const b = await OrgFactory.for('wiz94').build()
await attachSession(page, b.session, '/help')
await expect(page.getByRole('heading', { name: /База знаний/i })).toBeVisible()
// Должны быть основные топики.
await expect(page.getByText(/Начало работы/i).first()).toBeVisible()
await expect(page.getByText(/Каталог/i).first()).toBeVisible()
// Поиск.
await page.getByPlaceholder(/Поиск по темам/).fill('лояльность')
await expect(page.getByText(/Программы скидок|Лояльность/i).first()).toBeVisible({ timeout: 3_000 })
})
test('9.5 /api/admin/diagnostic/run возвращает 7 проверок @smoke', async () => {
const b = await OrgFactory.for('wiz95').build()
const r = await request<{ overall: string; checks: Array<{ name: string; status: string }>; ranAt: string }>(
'/api/admin/diagnostic/run?sendTestEmail=false', { token: b.session.accessToken },
)
expect(r.checks.length).toBeGreaterThanOrEqual(7)
const names = r.checks.map(c => c.name)
expect(names).toContain('Database')
expect(names).toContain('SMTP')
expect(names).toContain('Hangfire')
expect(names).toContain('Disk')
expect(names).toContain('Backup')
// overall должен быть Ok / Warning / Fail / Skipped.
expect(['Ok', 'Warning', 'Fail', 'Skipped']).toContain(r.overall)
})
test('9.6 POST /api/feedback принимает минимальный payload', async () => {
const b = await OrgFactory.for('wiz96').build()
const r = await request<{ ok: boolean }>(
'/api/feedback',
{
token: b.session.accessToken,
body: { category: 1, message: 'Тестовое сообщение из regression test' },
},
)
expect(r.ok).toBe(true)
})
test('9.7 /api/whats-new возвращает buildVersion + items', async () => {
const b = await OrgFactory.for('wiz97').build()
const r = await request<{ buildVersion: string; items: Array<{ date: string; title: string; type: string }> }>(
'/api/whats-new', { token: b.session.accessToken },
)
expect(typeof r.buildVersion).toBe('string')
expect(Array.isArray(r.items)).toBe(true)
})
})