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>
106 lines
5.6 KiB
TypeScript
106 lines
5.6 KiB
TypeScript
/**
|
||
* 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)
|
||
})
|
||
})
|