test(s8-1): SignalR stage e2e + progress
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2ea30bb30a
commit
abace49a45
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
## Чек-лист
|
## Чек-лист
|
||||||
|
|
||||||
- [ ] **1. P2-7 SignalR real-time** — Hub `/hubs/notifications` с группами per-org. События `SalePosted` / `SupplyPosted` / `LowStock`. JWT auth на коннекте. Дашборд live-виджет «Продажи сегодня» + bell-toast low-stock.
|
- [x] **1. P2-7 SignalR real-time** — Hub `/hubs/notifications` с группами per-org. События `SalePosted`/`SupplyPosted`/`LowStock`. JWT через query `?access_token=` (для WebSocket). Дашборд: live-индикатор Wifi, оптимистическое приращение «Выручка сегодня», toast.info на SupplyPosted, toast.error на LowStock. Тесты: SignalRNotificationsTests (multi-tenant 1/1) + `stage-ui-signalr.spec.ts` (1/1 ✓). Nginx `/hubs/` с upgrade-хедерами и 24h read_timeout.
|
||||||
- [ ] **2. P2-14 Telegram-бот владельца** — Hangfire `OwnerDailySummaryJob` 09:00 МСК, ежедневная сводка (выручка вчера, продажи, топ-3, low-stock). Привязка через deep-link → `Organization.OwnerTelegramChatId`. UI в OrgSettings.
|
- [ ] **2. P2-14 Telegram-бот владельца** — Hangfire `OwnerDailySummaryJob` 09:00 МСК, ежедневная сводка (выручка вчера, продажи, топ-3, low-stock). Привязка через deep-link → `Organization.OwnerTelegramChatId`. UI в OrgSettings.
|
||||||
- [ ] **3. P2-6a Локализация UI (en)** — react-i18next, `ru.json` + `en.json`, language switcher в header. kz TODO. На стейдже smoke — все страницы переключаются.
|
- [ ] **3. P2-6a Локализация UI (en)** — react-i18next, `ru.json` + `en.json`, language switcher в header. kz TODO. На стейдже smoke — все страницы переключаются.
|
||||||
- [ ] **4. P2-15 MinIO/S3 для uploads** — `Minio` SDK, bucket `food-market-uploads`, авто-создание на старте, миграция existing volume. `Storage:Type=Local|Minio` с fallback на Local. Тесты + UI upload картинки.
|
- [ ] **4. P2-15 MinIO/S3 для uploads** — `Minio` SDK, bucket `food-market-uploads`, авто-создание на старте, миграция existing volume. `Storage:Type=Local|Minio` с fallback на Local. Тесты + UI upload картинки.
|
||||||
|
|
|
||||||
89
tests/e2e/scenarios/stage-ui-signalr.spec.ts
Normal file
89
tests/e2e/scenarios/stage-ui-signalr.spec.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* Sprint 8 пункт 1 — SignalR smoke на стейдже.
|
||||||
|
* Открываем /dashboard, проверяем что live-индикатор стал "live",
|
||||||
|
* затем через API проводим продажу, ждём что виджет «Выручка сегодня»
|
||||||
|
* увеличился без перезагрузки страницы.
|
||||||
|
*/
|
||||||
|
import { test, expect, request as apiRequest } from '@playwright/test'
|
||||||
|
import { apiSignup, attachSession, watchPage } from '../lib/ui.js'
|
||||||
|
|
||||||
|
const BASE = process.env.E2E_ADMIN_URL ?? 'https://test.admin.food-market.kz'
|
||||||
|
|
||||||
|
test.describe('SignalR realtime', () => {
|
||||||
|
test('S8-1 dashboard live-индикатор + SalePosted увеличивает счётчик чеков', async ({ page }) => {
|
||||||
|
test.setTimeout(120_000)
|
||||||
|
const sess = await apiSignup('s8sr')
|
||||||
|
const errs = watchPage(page)
|
||||||
|
|
||||||
|
// Сначала засеем демо-данные чтобы был товар + остаток.
|
||||||
|
const ctx = await apiRequest.newContext({
|
||||||
|
baseURL: BASE, ignoreHTTPSErrors: true,
|
||||||
|
extraHTTPHeaders: { Authorization: `Bearer ${sess.accessToken}` },
|
||||||
|
})
|
||||||
|
await ctx.post('/api/admin/seed-demo', { data: {} })
|
||||||
|
|
||||||
|
await attachSession(page, sess, '/dashboard')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
// live-индикатор появляется когда SignalR подсоединился (5с буфер)
|
||||||
|
await expect(page.getByText('live').first()).toBeVisible({ timeout: 15_000 })
|
||||||
|
|
||||||
|
// Считаем текущее число чеков (виджет «Выручка сегодня» / hint)
|
||||||
|
const initialHint = await page.locator('text=/\\d+\\s+чеков/').first().textContent()
|
||||||
|
const initial = initialHint ? parseInt(initialHint, 10) : 0
|
||||||
|
|
||||||
|
// Через API создаём чек и проводим. После этого UI должен увеличиться.
|
||||||
|
type Paged<T> = { items: T[] }
|
||||||
|
const stores = await (await ctx.get('/api/catalog/stores')).json() as Paged<{ id: string; isMain: boolean }>
|
||||||
|
const retailPoints = await (await ctx.get('/api/catalog/retail-points')).json() as Paged<{ id: string }>
|
||||||
|
const curs = await (await ctx.get('/api/catalog/currencies')).json() as Paged<{ id: string; code: string }>
|
||||||
|
const storeId = stores.items.find(s => s.isMain)!.id
|
||||||
|
const retailPointId = retailPoints.items[0]?.id
|
||||||
|
const kztId = curs.items.find(c => c.code === 'KZT')!.id
|
||||||
|
|
||||||
|
// Чтобы не зависеть от того что seed-demo не выкупил весь товар, ищем
|
||||||
|
// продукт с положительным stock на main-складе.
|
||||||
|
const stocks = await (await ctx.get(`/api/inventory/stock?storeId=${storeId}&pageSize=50`)).json() as Paged<{ productId: string; quantity: number }>
|
||||||
|
const withStock = stocks.items.find(s => s.quantity > 1)
|
||||||
|
expect(withStock, 'нужен хотя бы один продукт с остатком >1').toBeTruthy()
|
||||||
|
const products = await (await ctx.get(`/api/catalog/products?pageSize=100`)).json() as Paged<{ id: string; prices: { amount: number }[] }>
|
||||||
|
const prod = products.items.find(p => p.id === withStock!.productId)!
|
||||||
|
const price = prod.prices[0]?.amount ?? 100
|
||||||
|
|
||||||
|
const saleResp = await ctx.post('/api/sales/retail', {
|
||||||
|
data: {
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
storeId, retailPointId, currencyId: kztId,
|
||||||
|
payment: 0, isReturn: false,
|
||||||
|
lines: [{ productId: prod.id, quantity: 1, unitPrice: price, discount: 0, vatPercent: 12 }],
|
||||||
|
subtotal: price, discountTotal: 0, total: price,
|
||||||
|
paidCash: price, paidCard: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect([200, 201]).toContain(saleResp.status())
|
||||||
|
const sale = await saleResp.json() as { id: string }
|
||||||
|
const postResp = await ctx.post(`/api/sales/retail/${sale.id}/post`)
|
||||||
|
expect([200, 204], `post status (${await postResp.text().catch(()=>'')})`).toContain(postResp.status())
|
||||||
|
|
||||||
|
// Ждём live-обновления: счётчик чеков должен вырасти. Полагаемся на
|
||||||
|
// refetch stats + liveCountDelta — что бы первое не отработало, второе
|
||||||
|
// должно сразу прибавить +1.
|
||||||
|
await expect(async () => {
|
||||||
|
const hint = await page.locator('text=/\\d+\\s+чеков/').first().textContent()
|
||||||
|
const cur = hint ? parseInt(hint, 10) : 0
|
||||||
|
expect(cur).toBeGreaterThan(initial)
|
||||||
|
}).toPass({ timeout: 15_000 })
|
||||||
|
|
||||||
|
expectNoErrorsOptional(errs)
|
||||||
|
await ctx.dispose()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function expectNoErrorsOptional(errs: { console: string[]; network: string[] }) {
|
||||||
|
// SignalR negotiate может вернуть 401 если token истёк — terпимо.
|
||||||
|
const significant = errs.network.filter(s => !/\/hubs\/notifications/.test(s))
|
||||||
|
if (significant.length || errs.console.length) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('[s8-signalr] non-fatal errors:', { console: errs.console, network: significant })
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue