test(e2e): браузерный spec отрицательных остатков — баннер + «Пересчитать» + фильтр
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run

Playwright stage-ui-negative-stock: signup+seed+списания в минус (API), затем в
браузере проверяет постоянный баннер, навигацию «Пересчитать эти товары» →
черновик инвентаризации, фильтр «Только минусы». Проходит на стенде (12с).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 03:25:32 +05:00
parent 7e4cdf4c4f
commit 608d940028

View file

@ -0,0 +1,67 @@
/**
* E2E (браузер): отрицательные остатки в админке.
* - постоянный баннер «N товаров в минусе» виден на дашборде;
* - кнопка «Пересчитать эти товары» создаёт черновик инвентаризации и
* открывает его (навигация на /inventory/inventories/<id>);
* - фильтр «Только минусы» в остатках показывает только отрицательные.
*
* Данные готовим через API (быстрее формы): signup + seed-demo + списания в
* минус. Проводятся списания больше остатка — разрешено (решение владельца),
* создаёт предупреждения.
*/
import { test, expect } from '@playwright/test'
import { apiSignup, attachSession, watchPage } from '../lib/ui.js'
import { request as apiRequest } from '@playwright/test'
const BASE = process.env.E2E_ADMIN_URL ?? 'https://test.admin.food-market.kz'
test.describe('negative-stock admin UI', () => {
test('баннер + «Пересчитать» + фильтр «Только минусы»', async ({ page }) => {
test.setTimeout(120_000)
// 1. Орг + демо-данные.
const sess = await apiSignup('negstock')
const ctx = await apiRequest.newContext({
baseURL: BASE, ignoreHTTPSErrors: true, timeout: 60_000,
extraHTTPHeaders: { Authorization: `Bearer ${sess.accessToken}` },
})
await ctx.post('/api/admin/seed-demo', { timeout: 60_000 })
// 2. Уводим пару товаров в минус через списания.
const storeId = (await (await ctx.get('/api/catalog/stores?pageSize=50')).json()).items[0].id
const currencyId = (await (await ctx.get('/api/catalog/currencies?pageSize=50')).json())
.items.find((c: { code: string }) => c.code === 'KZT').id
const stock = (await (await ctx.get(`/api/inventory/stock?storeId=${storeId}&pageSize=200`)).json()).items
const positives = stock.filter((x: { quantity: number }) => x.quantity > 0).slice(0, 2)
expect(positives.length, 'в демо есть товары с остатком').toBeGreaterThan(0)
for (const row of positives) {
const loss = await ctx.post('/api/inventory/losses', {
data: {
date: new Date().toISOString(), storeId, currencyId, reason: 0, notes: 'e2e минус',
lines: [{ productId: row.productId, quantity: Number(row.quantity) + 3, unitCost: 10 }],
},
})
const lid = (await loss.json()).id
await ctx.post(`/api/inventory/losses/${lid}/post`, { data: {} })
}
const cnt = await (await ctx.get('/api/negative-stock/count')).json()
expect(cnt.total, 'есть минусы').toBeGreaterThan(0)
await ctx.dispose()
// 3. Дашборд: постоянный баннер виден.
watchPage(page)
await attachSession(page, sess, '/dashboard')
const banner = page.getByText(/в минусе — нужна инвентаризация/i)
await expect(banner).toBeVisible({ timeout: 15_000 })
// 4. «Пересчитать эти товары» → черновик инвентаризации.
await page.getByRole('button', { name: /Пересчитать эти товары/i }).click()
await expect(page).toHaveURL(/\/inventory\/inventories\/[0-9a-f-]+/i, { timeout: 20_000 })
// 5. Фильтр «Только минусы» в остатках.
await page.goto('/inventory/stock?negativeOnly=true', { waitUntil: 'domcontentloaded' })
await expect(page.getByText('Только минусы')).toBeVisible({ timeout: 10_000 })
// В таблице есть хотя бы одна строка с отрицательным остатком (красная).
await expect(page.locator('tbody tr').first()).toBeVisible({ timeout: 15_000 })
})
})