docs(sprint7): пункт 2 ✓ + screenshot script
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
17a6da2f8b
commit
c201625b2b
|
|
@ -13,7 +13,7 @@
|
|||
## Чек-лист
|
||||
|
||||
- [x] **1. Demo-data seeder для stage** — POST /api/admin/seed-demo + admin-кнопка «Заполнить демо-данными». 50 товаров / 5 групп / 10 контрагентов / 5 приёмок / 30 продаж / 1 опт / 1 списание / 1 перемещение / 1 инвентаризация. Идемпотентно (маркер DEMO-). E2E 5/5 ✓ на стейдже.
|
||||
- [ ] **2. ConfirmDialog на destructive actions** — общий компонент. Удаление товара / контрагента / документа, archive org, unpost, disable 2FA, удаление сотрудника. Esc=cancel.
|
||||
- [x] **2. ConfirmDialog на destructive actions** — общий `<ConfirmDialog>` + хук `useConfirm()`. Применён к 17 страницам + ProductImageGallery. Esc=cancel, focus-on-Cancel, tone='danger'|'warning'. Org-archive уже использует Modal с confirmation-name (не трогали). 2FA UI ещё не существует в web — пропущено. Скриншот стейджа: `tests/e2e/reports/confirm-dialog-1780119970286.png`.
|
||||
- [ ] **3. Toast-система ошибок** — замена console.error в `src/lib/api.ts`. 4xx/5xx → error toast c message из API; мутации 2xx → success. Top-right, 5s autoclose.
|
||||
- [ ] **4. Loading skeletons** — на DataTable и edit-pages вместо «Загрузка…» — shimmer-скелет. Reusable `<Skeleton>`.
|
||||
- [ ] **5. Empty states с CTA** — list-страницы при `items.length === 0` показывают центрированный блок с иконкой, текстом и кнопкой «Создать первый …».
|
||||
|
|
@ -33,3 +33,11 @@
|
|||
- Идемпотентность через маркер `Article startsWith "DEMO-"`.
|
||||
- E2E `stage-demo-seed` (5 шагов) — green локально + на `test.admin.food-market.kz` (после deploy-stage.sh).
|
||||
- Коммит: `ad09b56 feat(stage): demo-data seeder для test.admin.food-market.kz`.
|
||||
|
||||
### 2026-05-30 — пункт 2 ✓
|
||||
|
||||
- `<ConfirmDialog>` + хук `useConfirm()`. Button переведён на forwardRef.
|
||||
- Применил к ProductEdit + 6 doc-edit + RetailSaleEdit + 8 list-pages + EmployeesPage + ProductImageGallery (всего 18 файлов).
|
||||
- Esc=cancel, фокус на Cancel (Enter не подтверждает). Tone='danger'/'warning'.
|
||||
- Скриншот стейджа: `tests/e2e/reports/confirm-dialog-*.png` — диалог рендерится, Esc-закрытие работает.
|
||||
- Коммит: `17a6da2 feat(web): ConfirmDialog компонент + useConfirm hook`.
|
||||
|
|
|
|||
82
tests/e2e/scripts/screenshot-confirm-dialog.ts
Normal file
82
tests/e2e/scripts/screenshot-confirm-dialog.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* Sprint 7 item 2 — визуальная проверка ConfirmDialog на стейдже.
|
||||
* Логинимся под demo-orgом, открываем карточку любого товара, жмём
|
||||
* «Удалить», скриншот, отмена → проверяем что товар не удалился.
|
||||
*
|
||||
* Запуск: cd tests/e2e && pnpm exec tsx scripts/screenshot-confirm-dialog.ts
|
||||
* Env: E2E_ADMIN_URL (default https://test.admin.food-market.kz),
|
||||
* E2E_EMAIL / E2E_PASSWORD (если нет — создаст временный signup +
|
||||
* запустит seed демо-данных).
|
||||
*/
|
||||
import { chromium } from 'playwright'
|
||||
import { makeClient, login } from '../lib/api.js'
|
||||
|
||||
const BASE = process.env.E2E_ADMIN_URL ?? 'https://test.admin.food-market.kz'
|
||||
const TS = Date.now()
|
||||
const EMAIL = process.env.E2E_EMAIL ?? `confirm-shot-${TS}@food-market.local`
|
||||
const PASS = process.env.E2E_PASSWORD ?? 'ConfirmShot12345!'
|
||||
|
||||
async function ensureSession() {
|
||||
const api = makeClient()
|
||||
// если signup провалится 409 — значит юзер уже есть, просто залогинимся
|
||||
const r = await api.post('/api/auth/signup', {
|
||||
email: EMAIL, password: PASS,
|
||||
organizationName: `ConfirmShot ${TS}`, phone: '+77011190001', plan: 'start',
|
||||
})
|
||||
if (r.status !== 200 && r.status !== 409) {
|
||||
throw new Error(`signup ${r.status}: ${JSON.stringify(r.data)}`)
|
||||
}
|
||||
const sess = await login(EMAIL, PASS)
|
||||
// Seed демо-данных — нам нужен хотя бы один товар, чтобы зайти в edit.
|
||||
const seed = makeClient(sess.accessToken)
|
||||
await seed.post('/api/admin/seed-demo', {})
|
||||
return sess
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const sess = await ensureSession()
|
||||
console.log(`[shot] session ok email=${sess.email}`)
|
||||
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
const ctx = await browser.newContext({
|
||||
ignoreHTTPSErrors: true,
|
||||
viewport: { width: 1280, height: 800 },
|
||||
})
|
||||
const page = await ctx.newPage()
|
||||
|
||||
// Авто-логин через токен: положим в localStorage то, что ожидает web
|
||||
// (web использует ключ fm.access_token, см. src/lib/auth.ts).
|
||||
await page.goto(`${BASE}/`)
|
||||
await page.evaluate(({ token }) => {
|
||||
localStorage.setItem('fm.access_token', token)
|
||||
}, { token: sess.accessToken })
|
||||
await page.goto(`${BASE}/catalog/products`, { waitUntil: 'domcontentloaded' })
|
||||
await page.waitForLoadState('networkidle')
|
||||
await page.screenshot({ path: `reports/confirm-list-${TS}.png`, fullPage: false })
|
||||
|
||||
// Ждём первую строку таблицы товаров
|
||||
await page.locator('tbody tr').first().waitFor({ timeout: 15000 })
|
||||
await page.locator('tbody tr').first().click()
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Кнопка «Удалить» — danger variant в правом верхнем углу
|
||||
const deleteBtn = page.getByRole('button', { name: /удалить/i }).first()
|
||||
await deleteBtn.click()
|
||||
|
||||
// Ждём появления нашего диалога
|
||||
await page.waitForSelector('[aria-labelledby="confirm-dialog-title"]', { timeout: 5000 })
|
||||
|
||||
const outDir = 'reports'
|
||||
await page.screenshot({ path: `${outDir}/confirm-dialog-${TS}.png`, fullPage: false })
|
||||
console.log(`[shot] saved → ${outDir}/confirm-dialog-${TS}.png`)
|
||||
|
||||
// Проверка: Esc закрывает (NOT удаляет)
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(300)
|
||||
const stillOpen = await page.locator('[aria-labelledby="confirm-dialog-title"]').count()
|
||||
console.log(`[shot] dialog closed after Esc: ${stillOpen === 0 ? '✓' : '✗ STILL OPEN'}`)
|
||||
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
main().catch(err => { console.error(err); process.exit(1) })
|
||||
Loading…
Reference in a new issue