import { test, expect, type Page } from '@playwright/test' import { statisticsDtoSchema } from '../../packages/contracts/src/statistics' async function setup(page: Page, failure = 0) { const control = { failure, empty: false, hold: false, release: () => {} } const pending = new Promise(resolve => { control.release = resolve }) await page.route('**/health', route => route.fulfill({ json: { ok: true } })) await page.route('**/api/**', async route => { const url = new URL(route.request().url()) if (url.pathname !== '/api/statistics') return route.fulfill({ json: url.pathname === '/api/auth/config' ? { required: false } : url.pathname === '/api/servers' ? [] : {} }) const from = url.searchParams.get('from')! const fail = control.failure if (control.hold && from === '2026-09-02') await pending if (fail) return route.fulfill({ status: fail, json: { error: 'Статистика временно недоступна' } }) const n = control.empty ? 0 : 1 const dto = statisticsDtoSchema.parse({ from, to: url.searchParams.get('to'), grain: 'day', kpis: { bytes: n * 1000, packets: n * 10, avgBps: n * 100, users: n, servers: n, ifaces: n, topCountry: '', topService: '' }, series: [], servers: [], interfaces: [], countries: [], services: [], asns: [], users: n ? [{ id: from, label: `Данные ${from}`, bytes: 1000, packets: 10, bps: 100, percent: 100 }] : [], }) await route.fulfill({ json: dto }) }) await page.goto('/statistics?from=2026-09-01&to=2026-09-03&dim=users') return control } for (const status of [403, 500]) { test(`Статистика: ${status} не подменяется нулём; повтор получает подтверждённый пустой ответ`, async ({ page }) => { const control = await setup(page, status) await expect(page.getByRole('alert').filter({ hasText: 'Ошибка загрузки' })).toContainText('Статистика временно недоступна') await expect(page.getByText('Нет данных куба', { exact: true })).toHaveCount(0) await expect(page.getByLabel('Сводка трафика')).toHaveCount(0) control.failure = 0 control.empty = true await page.getByRole('button', { name: 'Повторить загрузку', exact: true }).click() await expect(page.getByText('Нет данных куба', { exact: true })).toBeVisible() await expect(page.getByLabel('Сводка трафика')).toBeVisible() await expect(page.getByText('Ошибка загрузки', { exact: true })).toHaveCount(0) }) } test('Статистика: смена периода скрывает старые строки и игнорирует поздний ответ', async ({ page }) => { const control = await setup(page) await expect(page.getByText('Данные 2026-09-01', { exact: true })).toBeVisible() control.hold = true const waiting = page.waitForRequest(r => r.url().includes('/api/statistics?') && r.url().includes('from=2026-09-02')) await page.evaluate(() => window.history.pushState(null, '', '/statistics?from=2026-09-02&to=2026-09-03')) await waiting await expect(page.getByText('Загрузка статистики…', { exact: true })).toBeVisible() await expect(page.getByText('Данные 2026-09-01', { exact: true })).toHaveCount(0) await page.evaluate(() => window.history.pushState(null, '', '/statistics?from=2026-09-03&to=2026-09-03')) await expect(page.getByText('Данные 2026-09-03', { exact: true })).toBeVisible() const old = page.waitForResponse(r => r.url().includes('/api/statistics?') && r.url().includes('from=2026-09-02')) control.release() await old await expect(page.getByText('Данные 2026-09-03', { exact: true })).toBeVisible() await expect(page.getByText('Данные 2026-09-02', { exact: true })).toHaveCount(0) }) test('Статистика: ошибка pivot не показывает метрики и строки прежнего отчёта', async ({ page }) => { await setup(page) await expect(page.getByText('Данные 2026-09-01', { exact: true })).toBeVisible() await page.route('**/api/statistics/pivot?**', route => route.fulfill({ status: 500, json: { error: 'Сводка недоступна' } })) await page.getByRole('button', { name: 'Сводка', exact: true }).click() await expect(page.getByRole('alert').filter({ hasText: 'Ошибка загрузки' })).toContainText('Сводка недоступна') await expect(page.getByLabel('Сводка трафика')).toHaveCount(0) await expect(page.getByText('Нет данных куба', { exact: true })).toHaveCount(0) await expect(page.getByText('Данные 2026-09-01', { exact: true })).toHaveCount(0) await page.screenshot({ path: 'test-results/statistics-error.png', animations: 'disabled' }) }) for (const query of [ 'from=2026-02-30&to=2026-03-02', 'from=2026-09-01', 'from=2026-09-01&from=2026-09-02&to=2026-09-03', 'from=2026-09-03&to=2026-09-01', ]) { test(`Статистика: некорректный период ${query} не запрашивает API`, async ({ page }) => { const requests: string[] = [] page.on('request', r => { if (new URL(r.url()).pathname === '/api/statistics') requests.push(r.url()) }) await setup(page) await expect(page.getByText('Данные 2026-09-01', { exact: true })).toBeVisible() const before = requests.length await page.goto('/statistics?' + query + '&country=RU&keep=1') await expect(page.getByRole('alert').filter({ hasText: 'Некорректный период' })).toBeVisible() await expect(page.getByLabel('Сводка трафика')).toHaveCount(0) expect(requests).toHaveLength(before) await page.getByRole('button', { name: 'Выбрать последние 7 дней', exact: true }).click() await expect(page.getByLabel('Сводка трафика')).toBeVisible() await expect(page.getByText('Некорректный период', { exact: true })).toHaveCount(0) const params = new URL(page.url()).searchParams expect(params.getAll('from')).toHaveLength(1) expect(params.get('country')).toBe('RU') expect(params.get('keep')).toBe('1') }) } for (const query of ['serverId=bad', 'planes=unknown', 'country=RU&country=DE']) { test(`Статистика: неверный контекст ${query} блокирует запрос до сброса`, async ({ page }) => { const requests: string[] = [] page.on('request', r => { if (new URL(r.url()).pathname === '/api/statistics') requests.push(r.url()) }) await setup(page) await expect(page.getByText('Данные 2026-09-01', { exact: true })).toBeVisible() const before = requests.length await page.goto('/statistics?from=2026-09-01&to=2026-09-03&keep=1&' + query + '#report') await expect(page.getByRole('alert').filter({ hasText: 'Параметры отчёта требуют внимания' })).toBeVisible() await expect(page.getByLabel('Сводка трафика')).toHaveCount(0) expect(requests).toHaveLength(before) await page.getByRole('button', { name: 'Сбросить параметры отчёта', exact: true }).click() await expect(page.getByText('Данные 2026-09-01', { exact: true })).toBeVisible() const url = new URL(page.url()) expect(url.searchParams.get('from')).toBe('2026-09-01') expect(url.searchParams.get('keep')).toBe('1') expect(url.hash).toBe('#report') await page.goBack() await expect(page.getByRole('alert').filter({ hasText: 'Параметры отчёта требуют внимания' })).toBeVisible() }) } test('Статистика: история периода и выбранного среза восстанавливается без записи', async ({ page }) => { const writes: string[] = [] page.on('request', r => { if (!['GET', 'HEAD', 'OPTIONS'].includes(r.method())) writes.push(r.url()) }) await setup(page) await expect(page.getByText('Данные 2026-09-01', { exact: true })).toBeVisible() const original = page.url() await page.getByRole('button', { name: '30 д', exact: true }).click() await expect(page).not.toHaveURL(original) await expect(page.locator('tr[data-row-id]')).toHaveCount(1) const period = page.url() await page.locator('tr[data-row-id]').click() await expect(page).toHaveURL(/userId=/) const slice = page.url() await page.goBack() await expect(page).toHaveURL(period) await page.goBack() await expect(page).toHaveURL(original) await expect(page.getByText('Данные 2026-09-01', { exact: true })).toBeVisible() await page.goForward() await page.goForward() await expect(page).toHaveURL(slice) await page.reload() await expect(page).toHaveURL(slice) await expect(page.getByRole('button', { name: 'Применить срез', exact: true })).toBeVisible() expect(writes).toEqual([]) }) test('Статистика: клавиатурный выбор среза и доступные состояния вида', async ({ page }) => { await setup(page) const criterion = page.getByRole('combobox', { name: 'Критерий', exact: true }) await criterion.focus() await page.keyboard.press('ArrowDown') await expect(page.getByRole('listbox')).toBeVisible() await page.keyboard.press('Escape') await expect(page.getByRole('listbox')).toHaveCount(0) await expect(criterion).toBeFocused() await page.route('**/api/statistics/pivot?**', route => route.fulfill({ json: { rowDim: 'country', colDim: 'service', metric: 'bytes', columns: [], rows: [], otherBytes: 0 } })) const pivot = page.getByRole('button', { name: 'Сводка', exact: true }) await pivot.focus() await page.keyboard.press('Enter') await expect(pivot).toHaveAttribute('aria-pressed', 'true') await expect(page.getByRole('button', { name: 'Разрез', exact: true })).toHaveAttribute('aria-pressed', 'false') await expect(page.getByRole('combobox', { name: 'Строки', exact: true })).toBeVisible() await expect(page.getByRole('combobox', { name: 'Колонки', exact: true })).toBeVisible() }) for (const pivot of [false, true]) { test('Статистика: повреждённый успешный ответ не обрушает страницу ' + (pivot ? 'pivot' : 'основной'), async ({ page }) => { await setup(page) const crashes: string[] = [] page.on('pageerror', error => crashes.push(error.message)) await page.route(pivot ? '**/api/statistics/pivot?**' : '**/api/statistics?**', route => route.fulfill({ json: {} })) if (pivot) await page.getByRole('button', { name: 'Сводка', exact: true }).click() else await page.reload() await expect(page.getByRole('alert').filter({ hasText: 'Ошибка загрузки' })).toContainText(pivot ? 'Некорректный ответ сводной статистики' : 'Некорректный ответ статистики') await expect(page.getByRole('button', { name: 'Повторить загрузку', exact: true })).toBeVisible() await expect(page.getByLabel('Сводка трафика')).toHaveCount(0) expect(crashes).toHaveLength(0) }) }