193 lines
13 KiB
TypeScript
193 lines
13 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test'
|
||
|
||
async function setup(page: Page) {
|
||
const control = { hold: '', holdIface: '', fail: false, failList: false, invalidSample: undefined as unknown, mergeSample: false, metrics: {} as Record<string, unknown>, release: () => {} }
|
||
const gate = new Promise<void>(resolve => { control.release = resolve })
|
||
const card = { id: '1', name: 'Экспортёр тест', subtitle: '', site: 'TEST', country: 'RU', status: 'online', rxNow: 2, txNow: 3, sessions: 1, rxSeries: [1, 2], txSeries: [2, 3], bytes: 200 }
|
||
const data = { bpsNow: 1000000, bytes: 200, packets: 2, conversations: 1, uniqueSrc: 1, uniqueDst: 1, topProto: 'TCP', rxSeries: [1], txSeries: [2], applications: [], protocols: [], sources: [], destinations: [], interfaces: [], conversationsList: [], ifaces: [], live: false }
|
||
await page.route('**/health', r => r.fulfill({ json: { ok: true } }))
|
||
await page.route('**/api/**', async route => {
|
||
const url = new URL(route.request().url()), path = url.pathname, range = url.searchParams.get('range')
|
||
if (path === '/api/traffic/flow/monthly') return route.fulfill({ json: { month: url.searchParams.get('month'), bytes: 1500, countries: [{ id: 'RU', label: 'Россия', bytes: 1500, packets: 1, bps: 0, percent: 100 }], services: [], asns: [] } })
|
||
if (path === '/api/traffic/flow/analytics') {
|
||
if (control.hold === range || (control.holdIface && url.searchParams.get('iface') === control.holdIface)) await gate
|
||
if (control.fail) return route.fulfill({ status: 500, json: { error: 'Срез временно недоступен' } })
|
||
return route.fulfill({ json: { ...data, ...control.metrics, topCategory: url.searchParams.get('serverId') === '2' ? 'Второй сервер' : 'Категория ' + range } })
|
||
}
|
||
if (path === '/api/traffic/flow/live') return route.fulfill({ contentType: 'text/event-stream', body: 'event: sample\ndata: ' + JSON.stringify({ ...data, live: true }) + '\n\n' + (control.invalidSample === undefined ? '' : 'event: sample\ndata: ' + JSON.stringify(control.mergeSample ? { ...data, ...(control.invalidSample as object) } : control.invalidSample) + '\n\n') })
|
||
if (path === '/api/traffic/flow/exporters' && control.failList) return route.fulfill({ status: 403, json: { error: 'Нет доступа к экспортёрам' } })
|
||
const json = path === '/api/auth/config' ? { required: false }
|
||
: path === '/api/traffic/flow/exporters' ? { exporters: [card, { ...card, id: '2', name: 'Экспортёр второй' }] }
|
||
: path === '/api/traffic/flow/clients' ? { clients: [{ ...card, id: 'client1', name: 'Клиент тест' }] }
|
||
: path === '/api/traffic/flows' ? { exportersOnline: 1, bytesPerMin: 1, uniqueSrc: 1, uniqueDst: 1, topProto: 'TCP', talkers: [], listenerBound: true }
|
||
: path === '/api/traffic/servers' ? { servers: [] }
|
||
: path.endsWith('/interfaces') || path.endsWith('/bound-interfaces') ? { interfaces: [] }
|
||
: path === '/api/traffic/users' ? { users: [] }
|
||
: path === '/api/servers' ? [] : {}
|
||
return route.fulfill({ json })
|
||
})
|
||
await page.goto('/traffic')
|
||
await page.getByRole('button', { name: 'Потоки', exact: true }).click()
|
||
await expect(page.getByLabel('Скорость потоков')).toBeVisible()
|
||
return control
|
||
}
|
||
|
||
test('Flow: поздний ответ периода не заменяет текущую аналитику', async ({ page }) => {
|
||
const control = await setup(page)
|
||
control.hold = '24h'
|
||
await page.getByRole('button', { name: '24ч', exact: true }).click()
|
||
await expect(page.getByText('Загрузка аналитики…', { exact: true })).toBeVisible()
|
||
await expect(page.getByLabel('Скорость потоков')).toHaveCount(0)
|
||
await page.getByRole('button', { name: '15м', exact: true }).click()
|
||
await expect(page.getByText('Категория 15m', { exact: true }).filter({ visible: true })).toBeVisible()
|
||
const response = page.waitForResponse(r => r.url().includes('/flow/analytics?') && r.url().includes('range=24h'))
|
||
control.release()
|
||
await response
|
||
await expect(page.getByText('Категория 15m', { exact: true }).filter({ visible: true })).toBeVisible()
|
||
await expect(page.getByText('Категория 24h', { exact: true })).toHaveCount(0)
|
||
})
|
||
|
||
test('Flow: ошибка аналитики и повтор сохраняют период', async ({ page }) => {
|
||
const control = await setup(page)
|
||
control.fail = true
|
||
await page.getByRole('button', { name: '15м', exact: true }).click()
|
||
await expect(page.getByRole('alert').filter({ hasText: 'Аналитика недоступна' })).toContainText('Срез временно недоступен')
|
||
await expect(page.getByLabel('Скорость потоков')).toHaveCount(0)
|
||
control.fail = false
|
||
await page.getByRole('button', { name: 'Повторить аналитику', exact: true }).click()
|
||
await expect(page.getByText('Категория 15m', { exact: true }).filter({ visible: true })).toBeVisible()
|
||
})
|
||
|
||
test('Flow: ошибка списка не отображается как отсутствие экспортёров', async ({ page }) => {
|
||
const control = await setup(page)
|
||
control.failList = true
|
||
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('Нет экспортёров IPFIX.', { exact: false })).toHaveCount(0)
|
||
control.failList = false
|
||
await page.getByRole('button', { name: 'Повторить загрузку потоков', exact: true }).click()
|
||
await expect(page.getByLabel('Скорость потоков')).toBeVisible()
|
||
})
|
||
|
||
test('Flow: завершённый SSE не помечает последние данные как live', async ({ page }) => {
|
||
await setup(page)
|
||
await page.getByRole('button', { name: '5м', exact: true }).click()
|
||
await expect(page.getByText('Последние данные · live недоступен', { exact: true }).filter({ visible: true })).toHaveCount(2)
|
||
await expect(page.getByText('live', { exact: true })).toHaveCount(0)
|
||
})
|
||
|
||
|
||
test('Flow: месяц показывает только доступные агрегаты и не применяется к клиенту', async ({ page }) => {
|
||
await setup(page)
|
||
const monthlyRequest = page.waitForRequest(r => r.url().includes('/flow/monthly?'))
|
||
await page.getByRole('button', { name: 'Текущий месяц', exact: true }).click()
|
||
const url = new URL((await monthlyRequest).url())
|
||
expect(url.searchParams.get('serverId')).toBe('1')
|
||
expect(url.searchParams.get('month')).toMatch(/^\d{4}-\d{2}$/)
|
||
expect([...url.searchParams.keys()].sort()).toEqual(['month', 'serverId'])
|
||
const report = page.getByRole('region', { name: 'Месячный отчёт' })
|
||
await expect(report).toContainText('1.5 КБ')
|
||
await expect(report.getByRole('columnheader').filter({ hasText: 'Скорость' })).toHaveCount(0)
|
||
await expect(page.getByLabel('Скорость потоков')).toHaveCount(0)
|
||
await expect(page.getByLabel('Сводка трафика')).toHaveCount(0)
|
||
await expect(page.getByRole('switch', { name: 'Без дублей' })).toBeDisabled()
|
||
await expect(page.getByRole('switch', { name: 'Без overlay' })).toBeDisabled()
|
||
await expect(page.getByRole('switch', { name: 'Без mesh' })).toBeDisabled()
|
||
const clientRequest = page.waitForRequest(r => r.url().includes('/flow/analytics?') && r.url().includes('userId=client1'))
|
||
await page.getByRole('group', { name: 'Источник потоков' }).getByRole('button', { name: 'Клиенты', exact: true }).click()
|
||
expect(new URL((await clientRequest).url()).searchParams.get('range')).toBe('1h')
|
||
await expect(page.getByLabel('Скорость потоков')).toBeVisible()
|
||
await expect(report).toHaveCount(0)
|
||
await expect(page.getByRole('button', { name: 'Текущий месяц', exact: true })).toHaveCount(0)
|
||
await expect(page.getByRole('switch', { name: 'Без дублей' })).toBeEnabled()
|
||
})
|
||
|
||
for (const width of [320, 390, 1440]) {
|
||
test(`Flow: адаптивность ${width}px`, async ({ page }) => {
|
||
await page.setViewportSize({ width, height: 900 })
|
||
await setup(page)
|
||
for (const monthly of [false, true]) {
|
||
if (monthly) {
|
||
await page.getByRole('button', { name: 'Текущий месяц', exact: true }).click()
|
||
await expect(page.getByRole('region', { name: 'Месячный отчёт' })).toBeVisible()
|
||
}
|
||
const button = page.getByRole('button', { name: 'Текущий месяц', exact: true })
|
||
const box = await button.boundingBox()
|
||
expect(box).not.toBeNull()
|
||
expect(box!.x).toBeGreaterThanOrEqual(0)
|
||
expect(box!.x + box!.width).toBeLessThanOrEqual(width + 1)
|
||
const overflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth + 1)
|
||
expect(overflow).toBe(false)
|
||
}
|
||
await page.screenshot({ path: `test-results/flow-monthly-${width}.png`, fullPage: true })
|
||
})
|
||
}
|
||
|
||
|
||
for (const [name, invalid, merge] of [
|
||
['неполный объект', { uniqueSrc: 1, destinations: [] }, false],
|
||
['отрицательная скорость', { bpsNow: -1 }, true],
|
||
['null', null, false],
|
||
['неверная строка таблицы', { destinations: [null] }, true],
|
||
['отрицательная серия', { rxSeries: [-1] }, true],
|
||
] as const) {
|
||
test('Flow: повреждённый sample — ' + name, async ({ page }) => {
|
||
const control = await setup(page)
|
||
await page.getByRole("button", { name: "15м", exact: true }).click()
|
||
await expect(page.getByText("Категория 15m", { exact: true }).filter({ visible: true })).toBeVisible()
|
||
control.invalidSample = invalid
|
||
control.mergeSample = merge
|
||
await page.getByRole('button', { name: '5м', exact: true }).click()
|
||
await expect(page.getByText('Некорректные данные live-потока', { exact: true })).toBeVisible()
|
||
await expect(page.getByText('Последние данные · live недоступен', { exact: true }).filter({ visible: true })).toHaveCount(2)
|
||
await expect(page.getByLabel('Скорость потоков')).toContainText('1.00 Мбит/с')
|
||
await expect(page.getByText('live', { exact: true })).toHaveCount(0)
|
||
})
|
||
}
|
||
|
||
|
||
for (const [name, metrics, rate, bytes] of [
|
||
['отсутствующие', {}, 'Нет данных', 'Нет данных'],
|
||
['нулевые', { bpsOverlay: 0, bpsWire: 0, bytesMesh: 0, bytesOverlay: 0 }, '0 Кбит/с', '0 Б'],
|
||
['положительные', { bpsOverlay: 2000000, bpsWire: 2000000, bytesMesh: 1500, bytesOverlay: 1500 }, '2.00 Мбит/с', '1.5 КБ'],
|
||
['некорректные REST', { bpsOverlay: -1, bpsWire: null, bytesMesh: -1 }, 'Нет данных', 'Нет данных'],
|
||
] as const) {
|
||
test('Flow: optional-метрики — ' + name, async ({ page }) => {
|
||
const control = await setup(page)
|
||
control.metrics = metrics
|
||
await page.getByRole('button', { name: '15м', exact: true }).click()
|
||
await page.getByRole('switch', { name: 'Без mesh', exact: true }).click()
|
||
const grid = page.getByLabel('Скорость потоков')
|
||
const card = (label: string) => grid.locator('[data-slot="frame-panel"]').filter({ has: page.getByText(label, { exact: true }) })
|
||
await expect(card('JH↔EN overlay')).toContainText(rate)
|
||
await expect(card('Wire GRE')).toContainText(rate)
|
||
await expect(card('Mesh')).toContainText(bytes)
|
||
if (name === 'нулевые') await expect(card('JH↔EN overlay')).toContainText('0 Б')
|
||
})
|
||
}
|
||
|
||
|
||
test('Flow: интерфейс прежнего сервера не передаётся новому; поздний ответ игнорируется', async ({ page }) => {
|
||
const control = await setup(page)
|
||
control.metrics = { ifaces: [{ name: 'ether1', index: '1', bps: 1 }] }
|
||
await page.getByRole('button', { name: '15м', exact: true }).click()
|
||
await expect(page.getByRole('button', { name: 'ether1', exact: true })).toBeVisible()
|
||
control.holdIface = 'ether1'
|
||
const oldRequest = page.waitForRequest(r => r.url().includes('/flow/analytics?') && r.url().includes('iface=ether1'))
|
||
await page.getByRole('button', { name: 'ether1', exact: true }).click()
|
||
await oldRequest
|
||
const requests: URL[] = []
|
||
page.on('request', r => { if (r.url().includes('/flow/analytics?')) requests.push(new URL(r.url())) })
|
||
await page.getByRole('button').filter({ hasText: 'Экспортёр второй' }).click()
|
||
await expect(page.getByText('Второй сервер', { exact: true }).filter({ visible: true })).toBeVisible()
|
||
const newRequests = requests.filter(url => url.searchParams.get('serverId') === '2')
|
||
expect(newRequests.length).toBeGreaterThan(0)
|
||
expect(newRequests.every(url => !url.searchParams.has('iface'))).toBe(true)
|
||
const late = page.waitForResponse(r => r.url().includes('/flow/analytics?') && r.url().includes('iface=ether1'))
|
||
control.release()
|
||
await late
|
||
await expect(page.getByText('Второй сервер', { exact: true }).filter({ visible: true })).toBeVisible()
|
||
await expect(page.getByText('Категория 15m', { exact: true })).toHaveCount(0)
|
||
})
|