docs(sprint7): пункт 2 ✓ + screenshot script
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
nns 2026-05-30 10:46:52 +05:00
parent 17a6da2f8b
commit c201625b2b
2 changed files with 91 additions and 1 deletions

View file

@ -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 ✓ на стейдже. - [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. - [ ] **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>`. - [ ] **4. Loading skeletons** — на DataTable и edit-pages вместо «Загрузка…» — shimmer-скелет. Reusable `<Skeleton>`.
- [ ] **5. Empty states с CTA** — list-страницы при `items.length === 0` показывают центрированный блок с иконкой, текстом и кнопкой «Создать первый …». - [ ] **5. Empty states с CTA** — list-страницы при `items.length === 0` показывают центрированный блок с иконкой, текстом и кнопкой «Создать первый …».
@ -33,3 +33,11 @@
- Идемпотентность через маркер `Article startsWith "DEMO-"`. - Идемпотентность через маркер `Article startsWith "DEMO-"`.
- E2E `stage-demo-seed` (5 шагов) — green локально + на `test.admin.food-market.kz` (после deploy-stage.sh). - 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`. - Коммит: `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`.

View 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) })