Files
MikrotikManager/tests/e2e/traffic-details.spec.ts
T

179 lines
12 KiB
TypeScript

import { observedTrafficFixturePayload } from "./traffic-observation-fixture"
import { test, expect, type Page } from '@playwright/test'
async function setup(page: Page) {
const control = { holdRange: '', failIface: false, series: undefined as Array<number | null> | undefined, txSeries: undefined as number[] | undefined, release: () => {} }
const gate = new Promise<void>(resolve => { control.release = resolve })
const server = { id: '1', name: 'Первый сервер', site: 'TEST', country: 'RU', status: 'online', rxNow: 2, txNow: 3, rxPeak: 5, txPeak: 6, rxTotal: 100, txTotal: 200, sessions: 1, rxSeries: [1, 2], txSeries: [2, 3] }
await page.route('**/health', route => route.fulfill({ json: observedTrafficFixturePayload({ ok: true }) }))
await page.route('**/api/**', async route => {
const url = new URL(route.request().url()), path = url.pathname
if (path.endsWith('/live')) return route.fulfill({ status: 503, json: observedTrafficFixturePayload({}) })
if (path === '/api/traffic/servers/1') {
const range = url.searchParams.get('range'), iface = url.searchParams.get('iface')
if (control.holdRange && range === control.holdRange) await gate
if (control.failIface && iface === 'ether2') return route.fulfill({ status: 500, json: observedTrafficFixturePayload({ error: 'Интерфейс временно недоступен' }) })
return route.fulfill({ json: observedTrafficFixturePayload({ server: { ...server, rxSeries: control.series ?? server.rxSeries, txSeries: control.txSeries ?? control.series ?? server.txSeries, name: `Детали ${range} ${iface}`, rxNow: range === '24h' ? 24 : 15 } }) })
}
const json = path === '/api/auth/config' ? { required: false }
: path === '/api/traffic/servers' ? { servers: [{ ...server, rxSeries: control.series ?? server.rxSeries, txSeries: control.txSeries ?? control.series ?? server.txSeries }] }
: path === '/api/traffic/servers/1/interfaces' ? { interfaces: [{ name: 'ether2', running: true, disabled: false, rxNow: 0, txNow: 0 }] }
: path.endsWith('/interfaces') || path === '/api/traffic/bound-interfaces' ? { interfaces: [] }
: path === '/api/traffic/users' ? { users: [] }
: path === '/api/servers' ? [] : {}
await route.fulfill({ json: observedTrafficFixturePayload(json) })
})
await page.goto('/traffic')
await expect(page.getByRole('heading', { name: 'Детали 1h __all__', exact: true })).toBeVisible()
return control
}
test('Детали трафика: поздний период не заменяет текущий', async ({ page }) => {
const control = await setup(page)
control.holdRange = '24h'
const started = page.waitForRequest(r => new URL(r.url()).pathname === '/api/traffic/servers/1' && r.url().includes('range=24h'))
await page.getByRole('button', { name: '24ч', exact: true }).click()
await started
await expect(page.getByText('Загрузка выбранного периода и интерфейса…', { exact: true })).toBeVisible()
await expect(page.getByLabel('Скорость выбранного сервера')).toHaveCount(0)
await expect(page.getByLabel('Получение истории сервера')).toHaveCount(0)
await page.getByRole('button', { name: '15м', exact: true }).click()
await expect(page.getByRole('heading', { name: 'Детали 15m __all__', exact: true })).toBeVisible()
const late = page.waitForResponse(r => new URL(r.url()).pathname === '/api/traffic/servers/1' && r.url().includes('range=24h'))
control.release()
await late
await expect(page.getByRole('heading', { name: 'Детали 15m __all__', exact: true })).toBeVisible()
await expect(page.getByRole('heading', { name: 'Детали 24h __all__', exact: true })).toHaveCount(0)
})
test('Детали трафика: ошибка интерфейса не подменяется общими данными; повтор сохраняет контекст', async ({ page }) => {
const control = await setup(page)
control.failIface = true
await page.getByRole('button', { name: 'ether2', exact: true }).click()
await expect(page.getByRole('alert').filter({ hasText: 'Детали трафика недоступны' })).toContainText('Интерфейс временно недоступен')
await expect(page.getByLabel('Скорость выбранного сервера')).toHaveCount(0)
await expect(page.getByRole('heading', { name: 'Детали 1h __all__', exact: true })).toHaveCount(0)
control.failIface = false
await page.getByRole('button', { name: 'Повторить загрузку деталей', exact: true }).click()
await expect(page.getByRole('heading', { name: 'Детали 1h ether2', exact: true })).toBeVisible()
await expect(page.getByLabel('Скорость выбранного сервера')).toBeVisible()
})
for (const width of [320, 390]) {
test(`Детали трафика: период доступен на ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 })
await setup(page)
const period = page.getByRole('button', { name: '24ч', exact: true })
const box = await period.boundingBox()
expect(box).not.toBeNull()
expect(box!.x).toBeGreaterThanOrEqual(0)
expect(box!.x + box!.width).toBeLessThanOrEqual(width + 1)
await period.click()
await expect(page.getByRole('heading', { name: 'Детали 24h __all__', exact: true })).toBeVisible()
await expect(page.getByLabel('Скорость выбранного сервера')).toBeVisible()
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true)
})
}
test('Трафик: пустая история отличается от нулевой', async ({ page }) => {
const control = await setup(page)
control.series = []
await page.getByRole('button', { name: '24ч', exact: true }).click()
await expect(page.getByText('Нет исторических измерений за выбранный период', { exact: true })).toBeVisible()
await expect(page.getByRole('region', { name: 'История RX/TX' })).toHaveCount(0)
control.series = [0, 0]
await page.getByRole('button', { name: '15м', exact: true }).click()
await expect(page.getByRole('region', { name: 'История RX/TX' })).toBeVisible()
await expect(page.getByText('Нет исторических измерений за выбранный период', { exact: true })).toHaveCount(0)
})
for (const missing of ['RX', 'TX'] as const) {
test('Трафик: отсутствие ' + missing + ' не подменяется нулевой линией', async ({ page }) => {
const control = await setup(page)
control.series = missing === 'RX' ? [] : [2, 3]
control.txSeries = missing === 'TX' ? [] : [2, 3]
await page.getByRole('button', { name: '24ч', exact: true }).click()
const chart = page.getByRole('region', { name: 'История RX/TX' })
await expect(chart.getByRole('status')).toHaveText('Нет измерений ' + missing + ' за выбранный период')
await expect(chart.locator('.recharts-line-curve')).toHaveCount(1)
const plot = chart.locator('.recharts-wrapper')
await plot.scrollIntoViewIfNeeded()
const box = await plot.boundingBox()
await page.mouse.move(box!.x + box!.width - 12, box!.y + box!.height / 2)
await expect(chart.getByText(missing + ':', { exact: true })).toHaveCount(0)
await expect(chart.getByText((missing === 'RX' ? 'TX' : 'RX') + ':', { exact: true })).toBeVisible()
})
}
test('Карточка трафика: пустая история, одна точка, нулевой TX и разрыв', async ({ page }) => {
const control = await setup(page)
const card = page.getByRole('button').filter({ hasText: 'Первый сервер' })
control.series = []
control.txSeries = []
await page.getByRole('button', { name: '24ч', exact: true }).click()
await expect(card).toContainText('Нет истории трафика')
control.series = [2]
await page.getByRole('button', { name: '15м', exact: true }).click()
await expect(card.locator('circle')).toHaveCount(1)
await expect(card.locator('circle')).toHaveAttribute('cx', '150')
control.series = [2, 3, null, 4, 5]
control.txSeries = [0, 0, 0, 0, 0]
await page.getByRole('button', { name: '4ч', exact: true }).click()
await expect(card.locator('[data-direction="rx"] polyline')).toHaveCount(2)
await expect(card.locator('[data-direction="tx"] polyline')).toHaveCount(1)
expect(await card.locator('svg').last().innerHTML()).not.toMatch(/NaN|Infinity/)
})
test('Итоги трафика: Sparkline сохраняет разрывы и одиночные измерения', async ({ page }) => {
const control = await setup(page)
control.series = [1, 2, null, 3, 4]
await page.getByRole('button', { name: '24ч', exact: true }).click()
const history = page.getByRole('region', { name: 'История RX/TX' })
await expect(history.locator('.recharts-line-curve')).toHaveCount(2)
await expect.poll(async () => history.locator('.recharts-line-curve').evaluateAll(paths => paths.map(path => (path.getAttribute('d')?.match(/M/g) ?? []).length))).toEqual([2, 2])
const spark = page.getByRole('img', { name: 'История измерений', exact: true }).first()
await expect(spark.locator('path[fill="none"]')).toHaveCount(2)
await expect(spark.locator('path[fill="none"]').nth(1)).toHaveAttribute('d', /^M135\.0,/)
expect(await spark.innerHTML()).not.toMatch(/NaN|Infinity/)
control.series = [0]
await page.getByRole('button', { name: '15м', exact: true }).click()
await expect(spark.locator('circle')).toHaveCount(1)
await expect(spark.locator('circle')).toHaveAttribute('cx', '90')
control.series = []
await page.getByRole('button', { name: '4ч', exact: true }).click()
await expect(page.getByLabel('Нет истории измерений')).toHaveCount(2)
await expect(page.getByRole('img', { name: 'История измерений', exact: true })).toHaveCount(0)
})
test('История RX/TX: одиночные значения и ноль видны между пропусками', async ({ page }) => {
const control = await setup(page)
const history = page.getByRole('region', { name: 'История RX/TX' })
control.series = [0]
control.txSeries = []
await page.getByRole('button', { name: '24ч', exact: true }).click()
const dots = history.locator('circle[data-isolated-direction]')
await expect(dots).toHaveCount(1)
await expect(dots.first()).toBeVisible()
await expect(dots.first()).toHaveAttribute('data-isolated-direction', 'rx')
expect(await dots.first().getAttribute('cx')).not.toMatch(/NaN|Infinity/)
expect(await dots.first().getAttribute('cy')).not.toMatch(/NaN|Infinity/)
control.series = [2, null, 0, null, 3]
control.txSeries = [1, 2, 3, 4, 5]
await page.getByRole('button', { name: '15м', exact: true }).click()
await expect(history.locator('circle[data-isolated-direction="rx"]')).toHaveCount(3)
await expect(history.locator('circle[data-isolated-direction="tx"]')).toHaveCount(0)
const positions = await dots.evaluateAll(nodes => nodes.map(node => Number(node.getAttribute('cx'))))
expect(positions[0]).toBeLessThan(positions[1])
expect(positions[1]).toBeLessThan(positions[2])
await history.screenshot({ path: "test-results/traffic-isolated-points.png" })
control.series = [1, 2, 3]
control.txSeries = [0]
await page.getByRole('button', { name: '4ч', exact: true }).click()
await expect(history.locator('circle[data-isolated-direction="rx"]')).toHaveCount(0)
await expect(history.locator('circle[data-isolated-direction="tx"]')).toHaveCount(1)
})