diff --git a/tests/e2e/scenarios/stage-ui-negative-stock.spec.ts b/tests/e2e/scenarios/stage-ui-negative-stock.spec.ts new file mode 100644 index 0000000..566e9d7 --- /dev/null +++ b/tests/e2e/scenarios/stage-ui-negative-stock.spec.ts @@ -0,0 +1,67 @@ +/** + * E2E (браузер): отрицательные остатки в админке. + * - постоянный баннер «N товаров в минусе» виден на дашборде; + * - кнопка «Пересчитать эти товары» создаёт черновик инвентаризации и + * открывает его (навигация на /inventory/inventories/); + * - фильтр «Только минусы» в остатках показывает только отрицательные. + * + * Данные готовим через 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 }) + }) +})