From cd83269d3a44a064bcca0c8b8afde6cfb9d23c38 Mon Sep 17 00:00:00 2001 From: nns Date: Sat, 30 May 2026 11:07:54 +0500 Subject: [PATCH] =?UTF-8?q?docs(sprint7):=20=D0=BF=D1=83=D0=BD=D0=BA=D1=82?= =?UTF-8?q?=204=20=E2=9C=93=20+=20skeleton=20screenshot?= 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 | 11 +++++- tests/e2e/scripts/screenshot-skeleton.ts | 49 ++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/scripts/screenshot-skeleton.ts diff --git a/docs/sprint7-progress.md b/docs/sprint7-progress.md index e07b694..4082c78 100644 --- a/docs/sprint7-progress.md +++ b/docs/sprint7-progress.md @@ -15,7 +15,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] **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`. - [x] **3. Toast-система ошибок** — собственная `lib/toast.ts` + ``. Axios interceptor: 4xx/5xx → error toast (humanizeError() читает ProblemDetails: errors.X[0] / detail / message / title). 401 — refresh-flow, без toast. Success — глобальный mutation onSuccess (через `meta.successMessage`): useCatalogMutations + 36 мутаций на doc-edit pages. Top-right, autoclose 5s, дедуп, ручное закрытие X. Скриншот: `tests/e2e/reports/toast-error-*.png`. -- [ ] **4. Loading skeletons** — на DataTable и edit-pages вместо «Загрузка…» — shimmer-скелет. Reusable ``. +- [x] **4. Loading skeletons** — `Skeleton.tsx` экспортирует ``, ``, ``. DataTable рендерит TableSkeleton при isLoading. 9 doc-edit pages + OrganizationSettingsPage показывают FormSkeleton пока тащат документ. DashboardPage график → Skeleton block. Скриншот: `tests/e2e/reports/skeleton-table-*.png`. - [ ] **5. Empty states с CTA** — list-страницы при `items.length === 0` показывают центрированный блок с иконкой, текстом и кнопкой «Создать первый …». - [ ] **6. Breadcrumbs** — на edit-страницах Reusable ``. - [ ] **7. Keyboard shortcuts** — edit: Ctrl+S = save, Esc = cancel/back; list: `/` = focus search, `n` = create. Hint в footer / `?` overlay. @@ -51,3 +51,12 @@ - Doc-edit pages: 36 мутаций (save/post/unpost/remove) получили meta. - Скриншот: `tests/e2e/reports/toast-error-1780120703501.png` — toast «Не найдено / Not Found» рендерится top-right. - Коммит: `27ce8dd feat(web): toast-система`. + +### 2026-05-30 — пункт 4 ✓ + +- `Skeleton.tsx`: базовый `` + готовые шаблоны ``, ``. +- DataTable: при isLoading рендерит 8 строк скелета по числу колонок (псевдослучайная ширина для естественности). +- Edit-pages: 9 doc-форм + OrganizationSettingsPage показывают `` при `existing.isLoading`. +- DashboardPage: график выручки — `Skeleton block h-72`. +- Скриншот: `tests/e2e/reports/skeleton-table-1780121234164.png` — shimmer на /catalog/products при искусственной 3-сек задержке через page.route(). +- Коммит: `faa1352 feat(web): loading skeletons`. diff --git a/tests/e2e/scripts/screenshot-skeleton.ts b/tests/e2e/scripts/screenshot-skeleton.ts new file mode 100644 index 0000000..8e5f3b4 --- /dev/null +++ b/tests/e2e/scripts/screenshot-skeleton.ts @@ -0,0 +1,49 @@ +/** + * Sprint 7 item 4 — визуально подтверждаем shimmer skeleton на DataTable. + * Чтобы скелет видно было дольше, искусственно тормозим ответ через + * page.route() — 4s задержка на /api/catalog/products. + */ +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 ?? `skel-shot-${TS}@food-market.local` +const PASS = process.env.E2E_PASSWORD ?? 'SkelShot12345!' + +async function ensureSession() { + const api = makeClient() + const r = await api.post('/api/auth/signup', { + email: EMAIL, password: PASS, + organizationName: `SkelShot ${TS}`, phone: '+77011190001', plan: 'start', + }) + if (r.status !== 200 && r.status !== 409) throw new Error(`signup ${r.status}: ${JSON.stringify(r.data)}`) + return login(EMAIL, PASS) +} + +async function main() { + const sess = await ensureSession() + console.log(`[shot] session ${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() + await page.goto(`${BASE}/`) + await page.evaluate(({ token }) => localStorage.setItem('fm.access_token', token), { token: sess.accessToken }) + + // Замедляем именно products list endpoint — 3 сек задержка, чтобы скелет + // успел отрисоваться, а Playwright успел сделать снимок. + await page.route('**/api/catalog/products**', async route => { + await new Promise(r => setTimeout(r, 3000)) + await route.continue() + }) + + await page.goto(`${BASE}/catalog/products`, { waitUntil: 'domcontentloaded' }) + // Ждём появления первого со скелетом (animate-pulse class) + await page.waitForSelector('tbody .animate-pulse', { timeout: 8000 }) + await page.waitForTimeout(300) // дать pulse-анимации стабилизироваться + await page.screenshot({ path: `reports/skeleton-table-${TS}.png` }) + console.log(`[shot] saved → reports/skeleton-table-${TS}.png`) + await browser.close() +} + +main().catch(err => { console.error(err); process.exit(1) })