Update Harbor client implementation
This commit is contained in:
@@ -2,6 +2,12 @@ import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
|
||||
import { TrafficGroupRow } from '../../.test-dist/src/web/features/traffic/TrafficFeature.js';
|
||||
import { HistoryRows } from '../../.test-dist/src/web/features/traffic/TrafficHistoryPanel.js';
|
||||
import { emptyTrafficHistory, parseTrafficHistoryQuery } from '../../.test-dist/src/shared/trafficHistory.js';
|
||||
|
||||
import { assertLiveTrafficSnapshot } from '../../.test-dist/src/shared/liveTraffic.js';
|
||||
import {
|
||||
@@ -23,6 +29,8 @@ const rowModel = source('src/web/features/traffic/trafficRows.ts');
|
||||
const boundary = source('src/web/features/traffic/index.ts');
|
||||
const styles = source('src/web/styles/features/traffic.css');
|
||||
const primitives = source('src/web/styles/primitives.css');
|
||||
const history = source('src/web/features/traffic/TrafficHistoryPanel.tsx');
|
||||
const controls = source('src/web/features/traffic/TrafficControls.tsx');
|
||||
|
||||
const validSnapshot = {
|
||||
apiVersion: 1,
|
||||
@@ -175,7 +183,6 @@ test('traffic drawer exposes the requested truthful states and accessible contro
|
||||
'Только трафик через Harbor Connect. Приложения macOS недоступны внутри Docker.',
|
||||
'Только соединения, прошедшие через sing-box Gateway. Трафик, обходящий sing-box напрямую, здесь не виден.',
|
||||
]) assert.match(feature, new RegExp(copy.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||
assert.match(feature, /feature\.view === 'live' \? `\$\{snapshot\?\.summary\.active \|\| 0\} АКТИВНЫХ` : 'ИСТОРИЯ'/);
|
||||
assert.match(feature, /feature\.isGateway[\s\S]*Инспектор трафика выключен в настройках Harbor Gateway\.[\s\S]*Инспектор трафика выключен в настройках Harbor Connect\./);
|
||||
assert.match(feature, /feature\.isGateway[\s\S]*Только соединения, прошедшие через sing-box Gateway\. Трафик, обходящий sing-box напрямую, здесь не виден\.[\s\S]*Только трафик через Harbor Connect\. Приложения macOS недоступны внутри Docker\./);
|
||||
assert.match(feature, /aria-label="Найти устройство"/);
|
||||
@@ -205,7 +212,6 @@ test('traffic retention and grouping use canonical server settings and the froze
|
||||
assert.match(feature, /groupTrafficConnections\(selectedConnections, grouping\)/);
|
||||
assert.match(feature, /trafficGroupMatches\(group, query, 'all', 'all'\)/);
|
||||
assert.match(feature, /group\.connections\.length === 1[\s\S]*`Завершено · \$\{group\.protocol\}`/);
|
||||
assert.match(feature, /Соединения сгруппированы по назначению, протоколу и маршруту\./);
|
||||
});
|
||||
|
||||
test('traffic groups combine compatible UUIDs with exact byte sums and whole-group search', () => {
|
||||
@@ -384,7 +390,7 @@ test('traffic groups stay mounted and inert through exit while the same group ca
|
||||
assert.match(rowModel, /const desiredIds = new Set\(desired\.map\(\(group\) => group\.id\)\)/);
|
||||
assert.match(feature, /groupTrafficConnections\(selectedConnections, grouping\)/);
|
||||
assert.match(feature, /trafficGroupMatches\(group, query, 'all', 'all'\)/);
|
||||
assert.match(feature, /reconcileTrafficGroups\(current, groups, immediate\)/);
|
||||
assert.match(feature, /reconcileTrafficGroups\(current, groups, immediate, !reorder\)/);
|
||||
assert.match(feature, /setExpandedId\(\(current\) => desiredIds\.has\(current\) \? current : ''\)/);
|
||||
assert.match(feature, /inert=\{exiting \|\| undefined\}/);
|
||||
assert.match(feature, /event\.target === event\.currentTarget[\s\S]*event\.animationName === 'client-traffic-connection-out'/);
|
||||
@@ -392,31 +398,83 @@ test('traffic groups stay mounted and inert through exit while the same group ca
|
||||
assert.match(feature, /matchMedia\('\(prefers-reduced-motion: reduce\)'\)[\s\S]*media\.addEventListener\('change', update\)[\s\S]*media\.removeEventListener\('change', update\)/);
|
||||
assert.match(feature, /const immediate = reducedMotion[\s\S]*\['disabled', 'incompatible', 'stopped'\]\.includes/);
|
||||
assert.match(feature, /aria-label="Группы активных и недавно завершённых соединений"/);
|
||||
assert.match(feature, /group\.connections\.length > 1[\s\S]*×\$\{group\.connections\.length\}/);
|
||||
assert.match(feature, /group\.activeCount > 0 && <strong>/);
|
||||
assert.match(feature, /displayedGroups\.map\(\(row\) => <TrafficGroupRow[\s\S]*key=\{row\.group\.id\}/);
|
||||
assert.match(feature, /<b>Активных распознано<\/b>/);
|
||||
assert.match(feature, /<b>Активных требует внимания<\/b>/);
|
||||
});
|
||||
|
||||
test('traffic styling preserves the shared drawer geometry and minimal motion', () => {
|
||||
test('polling updates values without reshuffling readable rows, while explicit sorting can reorder', () => {
|
||||
const groups = groupTrafficConnections([
|
||||
trafficConnection('a', { destination: { domain: 'a.test' } }),
|
||||
trafficConnection('b', { destination: { domain: 'b.test' } }),
|
||||
trafficConnection('c', { destination: { domain: 'c.test' } }),
|
||||
]);
|
||||
const [a, b, c] = groups;
|
||||
const updated = { ...b, traffic: { ...b.traffic, downloadBytes: '9007199254740993' } };
|
||||
for (const immediate of [false, true]) {
|
||||
let rows = reconcileTrafficGroups([], [a, b], immediate, true);
|
||||
rows = reconcileTrafficGroups(rows, [updated, c, a], immediate, true);
|
||||
assert.deepEqual(rows.map(({ group }) => group.label), ['a.test', 'b.test', 'c.test']);
|
||||
assert.equal(rows[1].group.traffic.downloadBytes, '9007199254740993');
|
||||
const sorted = reconcileTrafficGroups(rows, [c, updated, a], immediate, false);
|
||||
assert.deepEqual(sorted.map(({ group }) => group.label), ['c.test', 'b.test', 'a.test']);
|
||||
rows = reconcileTrafficGroups(rows, [c, a], immediate, true);
|
||||
assert.equal(rows.some((row) => row.group.id === b.id), !immediate);
|
||||
rows = reconcileTrafficGroups(rows, [a, updated, c], immediate, true);
|
||||
assert.equal(rows.find((row) => row.group.id === b.id).exiting, false);
|
||||
}
|
||||
});
|
||||
|
||||
test('minimal traffic rows distinguish current rates from historical totals and completed connections', () => {
|
||||
const [group] = groupTrafficConnections([trafficConnection('current', { traffic: {
|
||||
downloadBytes: '9007199254740993', uploadBytes: '1048576',
|
||||
downloadBytesPerSecond: '2097152', uploadBytesPerSecond: '1024',
|
||||
} })]);
|
||||
const render = (group, expanded = false) => renderToStaticMarkup(createElement(TrafficGroupRow, {
|
||||
group, expanded, exiting: false, onExited() {}, onToggle() {},
|
||||
}));
|
||||
const current = render(group);
|
||||
assert.match(current, /aria-label="Скачивание: 2,0 МБ\/с"/);
|
||||
assert.match(current, /aria-label="Отправка: 1,0 КБ\/с"/);
|
||||
assert.doesNotMatch(current, /<svg|<img|<dl|8,0 ПБ|tls/);
|
||||
assert.match(render(group, true), /<dt>Скачано<\/dt><dd>8,0 ПБ<\/dd>/);
|
||||
const completed = render({ ...group, activeCount: 0, recentCount: 1 });
|
||||
assert.match(completed, /aria-label="Скачивание: соединение завершено"/);
|
||||
assert.doesNotMatch(completed, /МБ\/с/);
|
||||
const snapshot = emptyTrafficHistory(parseTrafficHistoryQuery(new URLSearchParams()));
|
||||
snapshot.rows = [{ key: 'example.com', label: 'example.com', downloadBytes: '9007199254740993', uploadBytes: '1048576', route: 'vpn' }];
|
||||
const historical = renderToStaticMarkup(createElement(HistoryRows, {
|
||||
snapshot, active: true, load() { throw new Error('Collapsed history must not load details'); },
|
||||
}));
|
||||
assert.match(historical, /aria-label="Скачивание: 8,0 ПБ"/);
|
||||
assert.match(historical, /aria-label="Отправка: 1,0 МБ"/);
|
||||
assert.doesNotMatch(historical, /<svg|<img|\/с|VPN|Загружаем/);
|
||||
});
|
||||
|
||||
test('traffic modes retain mounted state and stop hidden polling; menu Escape stays inside the drawer', () => {
|
||||
assert.doesNotMatch(feature, /view === 'history' && <TrafficHistoryPanel/);
|
||||
assert.match(feature, /<TrafficHistoryPanel active=\{feature.isOpen && !feature.paused && feature.view === 'history'\}/);
|
||||
assert.match(feature, /aria-hidden=\{feature.view !== 'history'\} inert=\{feature.view !== 'history' \|\| undefined\}/);
|
||||
assert.match(history, /if \(!active\) return/);
|
||||
assert.match(history, /controller.abort\(\); clearTimeout\(timer\)/);
|
||||
assert.match(history, /until: snapshot.query.until/);
|
||||
assert.match(controls, /event.stopPropagation\(\);[\s\S]*trigger.current\?\.focus\(\)/);
|
||||
assert.match(controls, /aria-hidden=\{!open\} inert=\{!open \|\| undefined\}/);
|
||||
assert.match(controls, /getComputedStyle\(node\).opacity;[\s\S]*animation.cancel\(\)/);
|
||||
assert.match(controls, /media.addEventListener\('change', reduce\)/);
|
||||
assert.match(controls, /media.removeEventListener\('change', reduce\)/);
|
||||
});
|
||||
|
||||
test('traffic styling reserves readable columns and one list scroll owner with calm motion', () => {
|
||||
assert.match(styles, /\.client-traffic-toggle svg \{[\s\S]*width: 24px;[\s\S]*height: 24px/);
|
||||
assert.match(primitives, /\.client-drawer \{[\s\S]*width: min\(580px, 100vw\)/);
|
||||
assert.match(styles, /@media \(max-width: 768px\) \{[\s\S]*\.client-traffic \{[\s\S]*width: 100vw/);
|
||||
assert.doesNotMatch(styles, /overflow-y:\s*(?:auto|scroll)/);
|
||||
assert.match(styles, /\.client-traffic-meta button \{[\s\S]*width: 96px/);
|
||||
assert.match(styles, /\.client-traffic-details \{[\s\S]*animation: client-traffic-details-in 180ms/);
|
||||
assert.match(styles, /\.client-traffic-connection \{[\s\S]*animation: client-traffic-connection-in 420ms/);
|
||||
assert.match(styles, /\.client-traffic-connection\.is-exiting \{[\s\S]*animation: client-traffic-connection-out 240ms/);
|
||||
const detailMotion = /@keyframes client-traffic-details-in \{([\s\S]*?)\n\}/.exec(styles)?.[1] || '';
|
||||
assert.match(detailMotion, /opacity:/);
|
||||
assert.match(detailMotion, /translateY/);
|
||||
assert.doesNotMatch(detailMotion, /height|width|margin|padding|scale|filter/);
|
||||
assert.match(styles, /width: min\(800px, 100vw\)/);
|
||||
assert.match(styles, /@media \(max-width: 480px\)/);
|
||||
assert.match(styles, /\.client-traffic-scroll \{[\s\S]*min-height: 0;[\s\S]*overflow-y: auto/);
|
||||
assert.match(styles, /\.client-traffic-pause \{[\s\S]*width: 112px/);
|
||||
assert.match(styles, /\.client-traffic-columns,[\s\S]*grid-template-columns: minmax\(0, 1fr\) var\(--traffic-value-width\) var\(--traffic-value-width\)/);
|
||||
for (const name of ['client-traffic-connection-in', 'client-traffic-connection-out']) {
|
||||
const rowMotion = new RegExp(`@keyframes ${name} \\{([\\s\\S]*?)\\n\\}`).exec(styles)?.[1] || '';
|
||||
assert.match(rowMotion, /opacity:/);
|
||||
assert.match(rowMotion, /translateY/);
|
||||
assert.doesNotMatch(rowMotion, /height|width|margin|padding|scale|filter/);
|
||||
assert.doesNotMatch(rowMotion, /transform|height|width|margin|padding|scale|filter/);
|
||||
}
|
||||
assert.match(styles, /@media \(prefers-reduced-motion: reduce\) \{[\s\S]*\.client-traffic-connection,[\s\S]*\.client-traffic-details,[\s\S]*\.client-traffic-device-list \{[\s\S]*animation: none/);
|
||||
assert.match(styles, /@media \(prefers-reduced-motion: reduce\) \{[\s\S]*animation: none;[\s\S]*transition: none/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user