Update VPN proxy client behavior
Build and Deploy Gateway / build-and-push (push) Successful in 18s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-09 11:55:02 +03:00
parent 71ede44be0
commit 90433d7cd8
27 changed files with 770 additions and 254 deletions
+20 -8
View File
@@ -10,6 +10,7 @@ import {
sortDevicesByTraffic,
stabilizeDevicesByTraffic,
trafficAxisMid,
trafficBytesPerSecond,
trafficScaleRatio,
} from '../../.test-dist/src/web/utils/format.js';
import { readStyleSource } from './style-source.js';
@@ -63,7 +64,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(panel, /pencilAnimationId === device\.id \? ' is-writing'/);
assert.match(panel, /onAnimationEnd=\{\(\) => setPencilAnimationId/);
assert.match(panel, /COPY_FEEDBACK_MS = 800/);
assert.match(panel, /IP скопирован/);
assert.match(panel, /IP \$\{device\.ip\} скопирован/);
assert.doesNotMatch(panel, /client-device-name-feedback|client-device-name-primary/);
assert.doesNotMatch(panel, /client-device-title/);
assert.match(panel, /online \? 'В сети' : <TextMorph from="Не в сети" to=\{seen\.relative\} \/>/);
@@ -95,11 +96,11 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(chart, /createPortal\([\s\S]*client-device-traffic-point-tooltip[\s\S]*document\.body/);
assert.match(chart, /onPointerMove=\{trackPointer\}/);
assert.match(chart, /pinned && max > 0n && <span className="client-device-traffic-axis"[\s\S]*is-max[\s\S]*is-mid[\s\S]*is-zero/);
assert.match(chart, /client-device-traffic-grid[\s\S]*path className="is-gateway"[\s\S]*path className="is-proxy"/);
assert.match(chart, /client-device-traffic-cursor[\s\S]*className="is-point is-gateway"[\s\S]*className="is-point is-proxy"/);
assert.match(chart, /client-device-traffic-lines[\s\S]*series === 'speed' \? 'is-download' : 'is-gateway'[\s\S]*series === 'speed' \? 'is-upload' : 'is-proxy'/);
assert.match(chart, /client-device-traffic-cursor[\s\S]*`is-point \$\{series === 'speed' \? 'is-download' : 'is-gateway'\}`[\s\S]*`is-point \$\{series === 'speed' \? 'is-upload' : 'is-proxy'\}`/);
assert.match(panel, /routeLabel=\{device\.appliedPolicy === 'direct' \? 'Напрямую' : 'Gateway'\}/);
assert.match(chart, /\{hovered\.proxy > 0n && <span className="is-proxy">Proxy \{formatByteString\(hovered\.proxy\)\}<\/span>\}/);
assert.match(chart, /<time dateTime=\{samples\[0\]\.observedAt\}>[\s\S]*<span>15 с<\/span>/);
assert.match(chart, /<time dateTime=\{visibleSamples\[0\]\.observedAt\}>[\s\S]*series === 'speed' \? '↓ \/ ↑' : '15 с'/);
assert.match(panel, /positiveByteDelta\(previous\.gateway, gateway\)[\s\S]*positiveByteDelta\(previous\.proxy, proxy\)/);
assert.match(panel, /setTimeout\(\(\) => setTrafficDeltas\(\{\}\), TRAFFIC_DELTA_MS\)/);
assert.doesNotMatch(panel, /client-device-traffic client-tooltip-anchor|<Tooltip>\{trafficLabel\}<\/Tooltip>/);
@@ -136,6 +137,8 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(styles, /\.client-device-traffic-point-tooltip \{[\s\S]*position: fixed;[\s\S]*z-index: 1002/);
assert.match(styles, /@keyframes client-device-traffic-shift[\s\S]*translateX\(calc\(100% \/ var\(--sample-count\)\)\)[\s\S]*translateX\(0\)/);
assert.match(styles, /@keyframes client-device-traffic-line-draw[\s\S]*stroke-dashoffset: 0/);
assert.match(styles, /\.client-device-traffic-lines \.is-upload \{[\s\S]*stroke-dasharray: 5 4/);
assert.match(styles, /\.client-device-traffic-lines path\.is-new\.is-upload \{[\s\S]*stroke-dashoffset: 0;[\s\S]*animation: none/);
assert.match(styles, /@keyframes client-device-chart-expand[\s\S]*clip-path: inset\(calc\(100% - 34px\) 0 0\)[\s\S]*scaleY\(1\)/);
assert.match(styles, /\.client-device-policy \{[\s\S]*width: 34px;[\s\S]*border-radius: 50%/);
assert.match(styles, /\.client-device-alias-trigger \{[\s\S]*font: 700 14px\/1\.2/);
@@ -165,13 +168,20 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(styles, /\.client-device-traffic-plot svg \{[\s\S]*height: 100%;[\s\S]*overflow: hidden/);
assert.match(styles, /\.client-devices-sort:hover \.client-devices-sort-icon[\s\S]*rotate\(360deg\)/);
assert.match(styles, /\.client-devices-refresh-ring circle[\s\S]*client-devices-refresh-progress 15s/);
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy svg[\s\S]*\.client-device-alias-trigger[\s\S]*\.client-device-ip[\s\S]*\.client-device-traffic-value > span[\s\S]*\.client-device-traffic-breakdown[\s\S]*\.client-device-traffic-lines[\s\S]*\.client-text-morph-value/);
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy svg[\s\S]*\.client-device-alias-trigger[\s\S]*\.client-device-ip[\s\S]*\.client-device-traffic-value > span[\s\S]*\.client-device-traffic-breakdown[\s\S]*\.client-device-traffic-lines[\s\S]*\.client-text-morph-value[\s\S]*\.client-device-traffic-lines path\.is-new \{[\s\S]*stroke-dashoffset: 0;[\s\S]*animation: none/);
assert.match(styles, /\.client-drawer \{[\s\S]*z-index: 50;[\s\S]*box-shadow:/);
assert.match(styles, /\.harbor-versions \{[\s\S]*z-index: 40/);
});
test('device copy feedback stays keyed per device', () => {
assert.match(panel, /copyTimers = useRef\(new Map[\s\S]*copyTimers\.current\.get\(device\.id\)[\s\S]*copyTimers\.current\.set\(device\.id/);
assert.match(panel, /copyAttempts = useRef\(new Map[\s\S]*copyAttempts\.current\.set\(device\.id, attempt\)[\s\S]*await copyText\(device\.ip\)[\s\S]*copyAttempts\.current\.get\(device\.id\) !== attempt/);
assert.match(panel, /setCopyFeedback\(\(current\) => \(\{ \.\.\.current, \[device\.id\]: feedback \}\)\)/);
assert.match(panel, /const feedback = copyFeedback\[device\.id\]/);
});
test('Gateway Home reuses the canonical device snapshot for applied route and global traffic', () => {
const powerStart = connection.indexOf('<section className="client-power-section"');
const powerStart = connection.indexOf('<section className={`client-power-section');
const trafficStart = overview.indexOf('<GatewayTrafficSummary');
const connectionPanelStart = overview.indexOf('<ConnectionPanel');
@@ -179,8 +189,8 @@ test('Gateway Home reuses the canonical device snapshot for applied route and gl
assert.match(overview, /appliedServer\?\.label \|\| 'VPN-сервер не используется'/);
assert.match(overview, /selectedServerId !== appliedServerId[\s\S]*Переключаем на \{desiredServer\.label\}/);
assert.match(feature, /formatByteString\(globalTraffic\?\.totalBytes \|\| '0'\)/);
assert.match(feature, /samples=\{globalTraffic\?\.history \|\| \[\]\}[\s\S]*capacity=\{feature\.snapshot\?\.trafficHistoryCapacity \|\| 120\}[\s\S]*routeLabel="Gateway"/);
assert.match(connection, /<section className="client-power-section"[\s\S]*client-connection-title[\s\S]*\{serverSlot\}[\s\S]*client-state-detail[\s\S]*client-proxies/);
assert.match(feature, /const history = globalTraffic\?\.history \|\| \[\][\s\S]*samples=\{history\}[\s\S]*routeLabel="Gateway"[\s\S]*series="speed"/);
assert.match(connection, /<section className=\{`client-power-section[\s\S]*client-state-detail[\s\S]*client-connection-title[\s\S]*\{serverSlot\}[\s\S]*client-proxies/);
assert.ok(powerStart >= 0 && connectionPanelStart >= 0 && trafficStart > connectionPanelStart, 'traffic summary follows the connection panel');
assert.equal((feature.match(/className="client-gateway-summary"/g) || []).length, 1);
assert.doesNotMatch(feature, /<TrafficChart[\s\S]{0,240}scale=/);
@@ -225,6 +235,8 @@ test('device traffic formatting and sorting preserve uint64 precision and canoni
assert.equal(Math.round(trafficScaleRatio('9', '99', 'log') * 100), 50);
assert.equal(trafficAxisMid('100', 'linear'), 50n);
assert.equal(trafficAxisMid('99', 'log'), 9n);
assert.equal(trafficBytesPerSecond('15000', '2026-08-07T13:00:00.000Z', '2026-08-07T13:00:15.000Z'), 1000n);
assert.equal(trafficBytesPerSecond('15000', 'invalid', '2026-08-07T13:00:15.000Z'), 0n);
const devices = [
{ id: 'a', uploadBytes: '9007199254740993', downloadBytes: '0', proxyUploadBytes: '0' },