From c201625b2bbe58cda638921f92ee6161114ca90c Mon Sep 17 00:00:00 2001 From: nns Date: Sat, 30 May 2026 10:46:52 +0500 Subject: [PATCH] =?UTF-8?q?docs(sprint7):=20=D0=BF=D1=83=D0=BD=D0=BA=D1=82?= =?UTF-8?q?=202=20=E2=9C=93=20+=20screenshot=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- docs/sprint7-progress.md | 10 ++- .../e2e/scripts/screenshot-confirm-dialog.ts | 82 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/scripts/screenshot-confirm-dialog.ts diff --git a/docs/sprint7-progress.md b/docs/sprint7-progress.md index 6a82563..45679cb 100644 --- a/docs/sprint7-progress.md +++ b/docs/sprint7-progress.md @@ -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** — общий `` + хук `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 ``. - [ ] **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 ✓ + +- `` + хук `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`. diff --git a/tests/e2e/scripts/screenshot-confirm-dialog.ts b/tests/e2e/scripts/screenshot-confirm-dialog.ts new file mode 100644 index 0000000..b076184 --- /dev/null +++ b/tests/e2e/scripts/screenshot-confirm-dialog.ts @@ -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) })