313 lines
28 KiB
JavaScript
313 lines
28 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import test from 'node:test';
|
||
import { fileURLToPath } from 'node:url';
|
||
import {
|
||
formatByteString,
|
||
formatLastSeen,
|
||
positiveByteDelta,
|
||
sortDevicesByTraffic,
|
||
stabilizeDevicesByTraffic,
|
||
trafficAxisMid,
|
||
trafficBytesPerSecond,
|
||
trafficScaleRatio,
|
||
} from '../../.test-dist/src/web/utils/format.js';
|
||
import { readStyleSource } from './style-source.js';
|
||
|
||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||
const overview = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.tsx'), 'utf8');
|
||
const connection = fs.readFileSync(path.join(root, 'src/web/features/connection/ConnectionPanel.tsx'), 'utf8');
|
||
const subscription = fs.readFileSync(path.join(root, 'src/web/features/subscription/SubscriptionFeature.tsx'), 'utf8');
|
||
const feature = fs.readFileSync(path.join(root, 'src/web/features/devices/DevicesFeature.tsx'), 'utf8');
|
||
const panel = fs.readFileSync(path.join(root, 'src/web/features/devices/DevicesPanel.tsx'), 'utf8');
|
||
const chart = fs.readFileSync(path.join(root, 'src/web/features/devices/TrafficChart.tsx'), 'utf8');
|
||
const api = fs.readFileSync(path.join(root, 'src/web/api/harborClient.ts'), 'utf8');
|
||
const server = fs.readFileSync(path.join(root, 'src/server/index.ts'), 'utf8');
|
||
const deviceRoute = fs.readFileSync(path.join(root, 'src/server/http/routes/deviceInventoryRoute.ts'), 'utf8');
|
||
const styles = readStyleSource(root);
|
||
|
||
test('Gateway device inventory uses the existing accessible responsive drawer', () => {
|
||
assert.match(overview, /isGateway && <DevicesToggle/);
|
||
assert.match(feature, /discover \? refreshDevices\(\) : listDevices\(\)/);
|
||
assert.match(feature, /DEVICE_AUTO_REFRESH_MS = 15_000/);
|
||
assert.match(feature, /setSnapshot\(\(current\) => !current \|\| next\.revision > current\.revision/);
|
||
assert.match(overview, /<DevicesPanel[\s\S]*feature=\{devicesFeature\}/);
|
||
assert.doesNotMatch(panel, /AUTO_REFRESH_MS|const \[snapshot, setSnapshot\]|setTimeout\(\(\) => load\(true\),/);
|
||
assert.match(feature, /requestDeviceUpdate\(device\.id, patch, snapshot\.revision\)/);
|
||
assert.match(feature, /requestError\(caught\)\.code !== 'STATE_CONFLICT'[\s\S]*listDevices\(\)[\s\S]*Object\.keys\(patch\)\.some\(\(key\) => latestDevice\[key\] !== device\[key\]\)[\s\S]*requestDeviceUpdate\(device\.id, patch, latest\.revision\)/);
|
||
assert.match(panel, /deviceNodes\.current\.get\(id\)\?\.animate/);
|
||
assert.match(panel, /movementAnimations\.current\.get\(id\)\?\.cancel\(\)/);
|
||
assert.match(panel, /previousPositions\.current\.size > 0/);
|
||
assert.match(panel, /previousScrollTop\.current - currentScrollTop/);
|
||
assert.match(feature, /next\.revision > current\.revision/);
|
||
assert.doesNotMatch(panel, /revision >= current\.revision/);
|
||
assert.match(panel, /prefers-reduced-motion: reduce/);
|
||
assert.match(api, /refresh: \(\) => request\('\/api\/devices\/refresh', \{ method: 'POST' \}\)/);
|
||
assert.match(api, /resetTraffic:[\s\S]*request\('\/api\/devices\/traffic'[\s\S]*method: 'DELETE'/);
|
||
assert.match(api, /setPolicy:[\s\S]*`\/api\/devices\/\$\{id\}\/policy`/);
|
||
assert.match(server, /createDeviceInventoryRoute\(\{/);
|
||
assert.match(deviceRoute, /pathname === '\/api\/devices\/refresh'[\s\S]*deviceInventory\.refresh\(\)/);
|
||
assert.match(deviceRoute, /pathname === '\/api\/devices\/traffic'[\s\S]*deviceInventory\.resetTraffic/);
|
||
assert.match(deviceRoute, /DEVICE_POLICY_PATH[\s\S]*deviceInventory\.setPolicy/);
|
||
assert.match(panel, /<Tooltip>Изменить название<\/Tooltip>/);
|
||
assert.match(panel, /copyText\(value\)/);
|
||
assert.match(panel, /const hasName = Boolean\(device\.alias \|\| device\.hostname\)/);
|
||
assert.match(panel, /const title = device\.alias \|\| device\.hostname \|\| device\.ip \|\| 'Неизвестное устройство'/);
|
||
assert.match(panel, /client-device-fallback-name[\s\S]*client-device-identity-details[\s\S]*<b>IP<\/b>[\s\S]*<b>MAC<\/b>[\s\S]*<b>Host<\/b>/);
|
||
assert.match(panel, /onClick=\{\(\) => startEditing\(device\)\}/);
|
||
assert.match(panel, /\{!hasName && !editing && <span className="client-device-edit-wrap client-tooltip-anchor">/);
|
||
assert.match(panel, /className=\{`client-device-name-heading\$\{hasName \? '' : ' is-address-only'\}\$\{editing \? ' is-editing' : ''\}`\}/);
|
||
assert.match(panel, /className="client-device-alias-input"[\s\S]*onBlur=\{\(\) => saveAlias\(device\)\}[\s\S]*event\.key === 'Enter'[\s\S]*event\.currentTarget\.blur\(\)/);
|
||
assert.match(panel, /aliasBaseline\.current = \{ id: device\.id, value \}/);
|
||
assert.match(panel, /style=\{\{ '--alias-width': `\$\{Math\.max\(1, alias\.length\)\}ch` \} as CSSProperties\}/);
|
||
assert.doesNotMatch(panel, /aliasWidth|getBoundingClientRect\(\)\.width/);
|
||
assert.match(panel, /nextAlias === aliasBaseline\.current\.value\.trim\(\)[\s\S]*setEditingId\(\(current\) => current === device\.id \? '' : current\)/);
|
||
assert.doesNotMatch(panel, /client-device-alias"|Сохранить название|Отменить изменение/);
|
||
assert.match(panel, /pencilAnimationId === device\.id \? ' is-writing'/);
|
||
assert.match(panel, /onAnimationEnd=\{\(\) => setPencilAnimationId/);
|
||
assert.match(panel, /COPY_FEEDBACK_MS = 800/);
|
||
assert.match(panel, /`\$\{field\} \$\{value\} скопирован`/);
|
||
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\} \/>/);
|
||
assert.match(panel, /aria-label=\{`\$\{online \? 'В сети' : 'Не в сети'\}\. Последний контакт: \$\{seen\.tooltip\}`\}/);
|
||
assert.doesNotMatch(panel, /client-device-status|STATUS_LABELS/);
|
||
assert.doesNotMatch(panel, /client-text-morph-goo/);
|
||
assert.doesNotMatch(panel, /client-device-addresses|client-device-manufacturer|client-device-meta/);
|
||
assert.doesNotMatch(panel, /device\.interface/);
|
||
assert.match(panel, /role="group" aria-label=\{`Технические данные устройства \$\{title\}`\}/);
|
||
assert.match(panel, /onClick=\{\(\) => copyDeviceValue\(device, 'IP', device\.ip!\)\}/);
|
||
assert.match(panel, /onClick=\{\(\) => copyDeviceValue\(device, 'MAC', device\.mac\)\}/);
|
||
assert.match(panel, /\{device\.hostname && <button[\s\S]*copyDeviceValue\(device, 'Hostname', device\.hostname!\)/);
|
||
assert.doesNotMatch(panel, /identityTooltipId|Hostname: \{device\.hostname/);
|
||
assert.match(panel, /device\.confidence === 'ambiguous'/);
|
||
assert.match(panel, /stabilizeDevicesByTraffic\(snapshot\?\.devices, sortDirection, previousIds\)/);
|
||
assert.match(panel, /Трафик временно не обновляется/);
|
||
assert.match(panel, /Вход — накопленные Gateway\/Proxy\. Выход — приблизительно через VPN или Direct с запуска текущего учёта\./);
|
||
assert.match(panel, /const displayedTotal = trafficView === 'outbound'[\s\S]*`≈ \$\{formatByteString\(outboundTotal\.toString\(\)\)\}`[\s\S]*const trafficLabel = trafficView === 'outbound' \? 'Выход' : 'Вход'/);
|
||
assert.match(panel, /client-device-traffic-total[\s\S]*<b>\{trafficLabel\}<\/b><TrafficValue value=\{displayedTotal\}/);
|
||
assert.match(panel, /client-device-traffic-breakdown[\s\S]*<b>VPN<\/b>[\s\S]*<b>Direct<\/b>[\s\S]*<b>Другое<\/b>/);
|
||
assert.match(panel, /<b>Gateway<\/b><TrafficValue value=\{gatewayTraffic\} delta=\{trafficDelta\.gateway\}/);
|
||
assert.match(panel, /const hasProxyTraffic = proxyTotal > 0n/);
|
||
assert.match(panel, /client-device-traffic-breakdown[\s\S]*\{hasProxyTraffic && <span className="is-proxy"><b>Прокси<\/b><TrafficValue value=\{proxyTraffic\} delta=\{trafficDelta\.proxy\}/);
|
||
assert.match(panel, /TrafficChart[\s\S]*samples=\{trafficView === 'outbound' \? device\.outboundTrafficHistory \|\| \[\] : device\.trafficHistory \|\| \[\]\}[\s\S]*scale=\{trafficScale\}[\s\S]*capacity=\{snapshot\?\.trafficHistoryCapacity[\s\S]*series=\{trafficView\}/);
|
||
assert.doesNotMatch(panel, /setTrafficHistory|TRAFFIC_HISTORY_LIMIT/);
|
||
assert.match(panel, /aria-pressed=\{trafficScale === 'linear'\}[\s\S]*aria-pressed=\{trafficScale === 'log'\}/);
|
||
assert.match(panel, /useState<'outbound' \| 'inbound'>\('outbound'\)[\s\S]*aria-pressed=\{trafficView === 'outbound'\}[\s\S]*>Выход<\/button>[\s\S]*aria-pressed=\{trafficView === 'inbound'\}[\s\S]*>Вход<\/button>/);
|
||
assert.match(chart, /function trafficPathAnimationSource[\s\S]*previousByTime[\s\S]*gatewayY: 100[\s\S]*attributeName="d"[\s\S]*dur="520ms"/);
|
||
assert.match(chart, /function smoothTrafficPath[\s\S]*const midX = \(previous\.x \+ point\.x\) \/ 2[\s\S]* C /);
|
||
assert.match(chart, /TRAFFIC_CHART_HEADROOM = 10[\s\S]*trafficChartY = \(ratio: number\) => 100 - ratio \* \(100 - TRAFFIC_CHART_HEADROOM\)/);
|
||
assert.match(chart, /proxy: byteString\(outbound\.directTrackedBytes\) \+ byteString\(outbound\.directIpv4Bytes\)/);
|
||
assert.match(chart, /gatewayY: trafficChartY\(trafficScaleRatio\(gateway, max, scale\)\)[\s\S]*proxyY: trafficChartY\(trafficScaleRatio\(proxy, max, scale\)\)[\s\S]*unknownY: trafficChartY\(trafficScaleRatio\(unknown, max, scale\)\)/);
|
||
assert.match(chart, /client-device-traffic-grid[\s\S]*y1=\{TRAFFIC_CHART_HEADROOM\}[\s\S]*y1=\{\(100 \+ TRAFFIC_CHART_HEADROOM\) \/ 2\}/);
|
||
assert.match(chart, /client-device-traffic-cursor[\s\S]*y1=\{TRAFFIC_CHART_HEADROOM\} y2="100"/);
|
||
assert.doesNotMatch(panel, /key=\{latest\}/);
|
||
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, /series === 'outbound'[\s\S]*'is-vpn'[\s\S]*'is-direct-total'[\s\S]*'is-unknown'/);
|
||
assert.match(chart, /client-device-traffic-lines[\s\S]*visibleLines\.map[\s\S]*smoothTrafficPath\(previous, line\.yKey\)[\s\S]*client-device-traffic-cursor[\s\S]*visibleLines\.map/);
|
||
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, /Примерно[\s\S]*VPN[\s\S]*Direct[\s\S]*через sing-box[\s\S]*мимо sing-box · IPv4[\s\S]*Маршрут не определён · sing-box[\s\S]*Оценка за 15-секундный интервал/);
|
||
assert.match(chart, /hovered\.proxy - hovered\.directIpv4 > 0n && hovered\.directIpv4 > 0n && <>[\s\S]*через sing-box[\s\S]*мимо sing-box · IPv4/);
|
||
assert.match(chart, /client-device-traffic-legend[\s\S]*valueKey === 'gateway'[\s\S]*>VPN<\/span>[\s\S]*valueKey === 'proxy'[\s\S]*>Direct<\/span>[\s\S]*valueKey === 'unknown'[\s\S]*>Другое<\/span>/);
|
||
assert.doesNotMatch(chart, /Direct ≈|>\?<\/span>/);
|
||
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>/);
|
||
assert.match(panel, /source\?\.traffic\?\.proxy\?\.error/);
|
||
assert.match(panel, /client-device-pin-wrap[\s\S]*client-device-main[\s\S]*client-device-traffic[\s\S]*client-device-policy-wrap/);
|
||
assert.doesNotMatch(panel, /client-device-details/);
|
||
assert.match(feature, /setDevicePolicy\(device\.id, mode, snapshot\.revision\)/);
|
||
assert.match(feature, /requestError\(caught\)\.code !== 'STATE_CONFLICT'[\s\S]*latestDevice\.desiredPolicy !== device\.desiredPolicy[\s\S]*setDevicePolicy\(device\.id, mode, latest\.revision\)/);
|
||
assert.match(panel, /className=\{`client-device-policy is-\$\{displayPolicy\}/);
|
||
assert.match(panel, /displayPolicy === 'direct' \? <svg[\s\S]*M4 12h15M14 7l5 5-5 5[\s\S]*M12 3 19 6v5/);
|
||
assert.match(panel, /Полностью обходит sing-box/);
|
||
assert.match(panel, /<Drawer[\s\S]*className="client-instructions client-devices"/);
|
||
assert.doesNotMatch(panel, /точная MAC|частная MAC|<dt>Источник<\/dt>/);
|
||
assert.match(panel, /aria-pressed=\{device\.pinned\}/);
|
||
assert.match(panel, /const \[pinCollapses, setPinCollapses\][\s\S]*animationDone[\s\S]*result: saved \? 'saved' : 'failed'/);
|
||
assert.match(panel, /const compact = deprioritized && !collapsing[\s\S]*const expanded = device\.pinned && !collapsing[\s\S]*const showDetails = !compact && \(device\.pinned \|\| collapsing\)[\s\S]*pinned=\{showDetails\}[\s\S]*onCollapseEnd=/);
|
||
assert.match(panel, /Закреплённые[\s\S]*Фоновые[\s\S]*Остальные/);
|
||
assert.match(panel, /client-device-group-heading[\s\S]*role=\{groupStart \? 'heading' : undefined\}[\s\S]*aria-level=\{groupStart \? 3 : undefined\}/);
|
||
assert.match(panel, /toggleDeprioritized[\s\S]*deprioritized: !deprioritized[\s\S]*deprioritized: true/);
|
||
assert.match(panel, /client-device-deprioritize[\s\S]*aria-pressed=\{deprioritized\}[\s\S]*Вернуть \$\{title\} в основной список[\s\S]*Убрать \$\{title\} в фон/);
|
||
assert.match(panel, /\{!compact && <TrafficChart/);
|
||
assert.doesNotMatch(panel, /Закрепите устройство, чтобы изменить маршрут|Сначала верните маршрут через Gateway/);
|
||
assert.match(panel, /maxLength=\{64\}[\s\S]*autoFocus/);
|
||
assert.match(styles, /\.client-devices \{\s*width: min\(580px, 100vw\)/);
|
||
assert.match(styles, /\.client-device \{[\s\S]*--client-device-chart-height: 34px;[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\) 112px 34px;[\s\S]*grid-template-rows: 34px var\(--client-device-chart-height\);[\s\S]*padding: 10px 8px/);
|
||
assert.match(styles, /\.client-device\.is-pinned \{[\s\S]*--client-device-chart-height: 72px/);
|
||
assert.match(styles, /\.client-device\.is-deprioritized \{[\s\S]*grid-template-rows: 34px;[\s\S]*padding-block: 6px/);
|
||
assert.match(styles, /\.client-device\.is-deprioritized \.client-device-main \{[\s\S]*grid-column: 1 \/ 4/);
|
||
assert.match(styles, /\.client-device-deprioritize-wrap \{[\s\S]*grid-column: 4;[\s\S]*grid-row: 2/);
|
||
assert.match(styles, /\.client-device\.is-deprioritized \.client-device-deprioritize-wrap \{[\s\S]*grid-row: 1/);
|
||
assert.match(styles, /\.client-device-group-heading \{[\s\S]*text-transform: var\(--type-label-transform\)/);
|
||
assert.match(styles, /\.client-device\.is-pinned \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-expand 520ms/);
|
||
assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-plot \{[\s\S]*client-device-traffic-plot-collapse 520ms/);
|
||
assert.match(styles, /\.client-device\.is-collapsing \.client-device-traffic-axis \{[\s\S]*top: 0;[\s\S]*height: 58px/);
|
||
assert.match(styles, /\.client-device-main \{[\s\S]*display: flex;[\s\S]*align-items: center/);
|
||
assert.match(styles, /\.client-device-traffic-total,[\s\S]*\.client-device-traffic-breakdown > span \{[\s\S]*grid-template-columns: 46px minmax\(0, 1fr\)/);
|
||
assert.match(styles, /\.client-device-traffic-breakdown > span \{[\s\S]*translateY\(-12px\)[\s\S]*\.client-device-traffic:hover \.client-device-traffic-breakdown > span,[\s\S]*opacity: 1[\s\S]*translateY\(0\)/);
|
||
assert.match(styles, /\.client-device-traffic-breakdown \{[^}]*background: color-mix\(in oklch, var\(--client-bg\) 84%, transparent\);[^}]*backdrop-filter: blur\(8px\)/);
|
||
assert.doesNotMatch(styles, /\.client-device-traffic-breakdown \{[^}]*box-shadow:/);
|
||
assert.match(styles, /\.client-device-traffic-chart \{[\s\S]*grid-column: 1 \/ 4;[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\);[\s\S]*column-gap: 8px/);
|
||
assert.match(styles, /\.client-device-traffic-plot \{[\s\S]*grid-column: 2;/);
|
||
assert.match(styles, /\.client-device-traffic-time \{[\s\S]*grid-column: 2;/);
|
||
assert.match(styles, /\.client-device-traffic-grid line \{[\s\S]*vector-effect: non-scaling-stroke/);
|
||
assert.match(styles, /\.client-device-traffic-lines path,[\s\S]*stroke-width: 1\.8/);
|
||
assert.match(styles, /\.client-device-traffic-cursor \.is-point \{[\s\S]*stroke-width: 4;[\s\S]*stroke-linecap: round;[\s\S]*vector-effect: non-scaling-stroke/);
|
||
assert.match(styles, /\.client-device-traffic-axis > \.is-max \{[\s\S]*top: var\(--traffic-chart-top\)/);
|
||
assert.match(styles, /\.client-device-traffic-axis > \.is-mid \{[\s\S]*top: var\(--traffic-chart-mid\)/);
|
||
assert.match(styles, /\.client-device-traffic-point-tooltip \{[\s\S]*position: fixed;[\s\S]*z-index: 1002/);
|
||
assert.doesNotMatch(styles, /client-device-traffic-shift|client-device-traffic-line-draw/);
|
||
assert.match(styles, /\.client-device-traffic-lines \.is-upload \{[\s\S]*stroke-dasharray: 5 4/);
|
||
assert.match(styles, /\.client-device-traffic-lines \.is-vpn[\s\S]*\.is-direct-total[\s\S]*\.is-unknown/);
|
||
assert.match(styles, /\.client-device-traffic-breakdown \.is-vpn[\s\S]*\.is-direct-total[\s\S]*\.is-unknown/);
|
||
assert.match(styles, /@keyframes client-device-traffic-plot-expand[\s\S]*scaleY\(0\.3448275862\)[\s\S]*scaleY\(1\)/);
|
||
assert.match(styles, /@keyframes client-device-traffic-plot-collapse[\s\S]*scaleY\(2\.9\)[\s\S]*scaleY\(1\)/);
|
||
assert.match(styles, /@keyframes client-device-traffic-detail-in[\s\S]*opacity: 0[\s\S]*opacity: 1/);
|
||
assert.match(styles, /@keyframes client-device-traffic-detail-out[\s\S]*opacity: 1[\s\S]*opacity: 0/);
|
||
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: var\(--type-item-title\)/);
|
||
assert.match(styles, /\.client-device-alias-trigger\.is-custom-name \{[\s\S]*font: var\(--type-section-title\)/);
|
||
assert.match(styles, /\.client-device-alias-input \{[\s\S]*width: var\(--alias-width\)[\s\S]*caret-color: var\(--client-accent\)[\s\S]*font: var\(--type-section-title\)[\s\S]*client-device-alias-edit-in 360ms/);
|
||
assert.match(styles, /@keyframes client-device-alias-edit-in[\s\S]*color: var\(--client-accent\)[\s\S]*filter: blur\(2px\)/);
|
||
assert.doesNotMatch(styles, /\.client-device-alias \{/);
|
||
assert.match(styles, /\.client-device-identity-copy span \{[\s\S]*font: var\(--type-data\)/);
|
||
assert.match(styles, /\.client-device-last-seen \{[\s\S]*position: absolute;[\s\S]*top: 0;[\s\S]*left: 0;[\s\S]*font: var\(--type-micro\)/);
|
||
assert.match(styles, /\.client-device-traffic strong \{[\s\S]*font: var\(--type-control\)/);
|
||
assert.match(styles, /\.client-device-identity-details \{[\s\S]*top: 100%;[\s\S]*pointer-events: none/);
|
||
assert.match(styles, /\.client-device-name-heading:hover > \.client-device-identity-details,[\s\S]*pointer-events: auto/);
|
||
assert.match(styles, /\.client-device-identity-copy\.is-copied \{[\s\S]*client-device-copy 800ms/);
|
||
assert.match(styles, /\.client-device-main \{[^}]*height: 34px[\s\S]*align-items: flex-end[\s\S]*padding: 11px 0 0/);
|
||
assert.match(styles, /\.client-device-main > h4 \{[\s\S]*flex: 0 1 auto/);
|
||
assert.match(styles, /\.client-device-last-seen \{[^}]*height: 10px[\s\S]*align-items: center/);
|
||
assert.match(styles, /\.client-device-traffic-value\.has-delta > \.is-total[\s\S]*translateY\(-0\.18em\)/);
|
||
assert.match(styles, /\.client-device-last-seen\.is-online \{[\s\S]*color: var\(--client-accent\)/);
|
||
assert.match(styles, /\.client-text-morph-value \{[\s\S]*transition: opacity 360ms[\s\S]*filter 480ms/);
|
||
assert.match(styles, /\.client-device-last-seen:hover \.client-text-morph-value\.is-relative[\s\S]*opacity: 1/);
|
||
assert.match(styles, /\.client-device-pin-wrap\.client-tooltip-anchor:hover > \.client-tooltip[\s\S]*translate\(0, 0\)/);
|
||
assert.match(styles, /\.client-device-pin:hover:not\(:disabled\) svg[\s\S]*translateY\(-2px\) rotate\(-12deg\)/);
|
||
assert.match(styles, /\.client-device-pin\[aria-pressed="true"\] svg/);
|
||
assert.match(styles, /\.client-device-edit-wrap \{[\s\S]*flex: 0 0 23px;[\s\S]*width: 23px/);
|
||
assert.doesNotMatch(styles, /\.client-device-edit-wrap \{[^}]*position: absolute/);
|
||
assert.match(styles, /\.client-device-edit\.is-writing svg \{[\s\S]*client-device-pencil-write 620ms/);
|
||
assert.match(styles, /@keyframes client-device-pencil-write[\s\S]*0%, 100%[\s\S]*translate\(1px, -1px\) rotate\(-5deg\)/);
|
||
assert.match(styles, /\.client-device-traffic-plot \{[\s\S]*height: 100%;[\s\S]*min-height: 0;[\s\S]*overflow: hidden/);
|
||
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(panel, /client-devices-reset-wrap client-tooltip-anchor[\s\S]*aria-label="Сбросить вход и выход всех устройств"[\s\S]*<Tooltip>Сбросить вход и выход всех устройств<\/Tooltip>/);
|
||
assert.doesNotMatch(panel, /className="client-devices-reset"[\s\S]{0,320}<span>/);
|
||
assert.match(styles, /\.client-devices-kicker \{[^}]*flex-wrap: nowrap/);
|
||
assert.match(styles, /\.client-devices-reset-wrap \{[^}]*margin-left: auto/);
|
||
assert.match(styles, /\.client-devices-reset \{[\s\S]*oklch\(0\.68 0\.15 28\)[\s\S]*\.client-devices-reset:hover:not\(:disabled\) svg[\s\S]*rotate\(-360deg\)/);
|
||
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy svg[\s\S]*\.client-device-alias-trigger[\s\S]*\.client-device-identity-details[\s\S]*\.client-device-identity-copy[\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]*transition: none;[\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\(value\)[\s\S]*copyAttempts\.current\.get\(device\.id\) !== attempt/);
|
||
assert.match(panel, /setCopyFeedback\(\(current\) => \(\{ \.\.\.current, \[device\.id\]: \{ \.\.\.feedback, field \} \}\)\)/);
|
||
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 trafficStart = overview.indexOf('<GatewayTrafficSummary');
|
||
const connectionPanelStart = overview.indexOf('<ConnectionPanel');
|
||
|
||
assert.match(overview, /const appliedServerId = state\?\.selection\?\.appliedServerId \|\| ''/);
|
||
assert.match(overview, /const appliedServer = appliedProfile\?\.servers\.find[\s\S]*state\.selection\.appliedServerSnapshot/);
|
||
assert.match(overview, /const mainIdentity = gatewayDirect[\s\S]*: connected[\s\S]*appliedProfile && appliedServer[\s\S]*`\$\{subscriptionDomain\(appliedProfile\.subscription\.host\)\} · \$\{appliedServer\.label\}`/);
|
||
assert.match(overview, /const switchIdentity = gatewayDirect[\s\S]*switchingServer && operationProfile && operationServer[\s\S]*`Переключаем на \$\{subscriptionDomain\(operationProfile\.subscription\.host\)\} · \$\{operationServer\.label\}`/);
|
||
assert.match(overview, /serverSlot=\{<AppliedIdentity identity=\{mainIdentity\} operation=\{switchIdentity\} \/>\}/);
|
||
assert.match(feature, /formatByteString\(globalTraffic\?\.totalBytes \|\| '0'\)/);
|
||
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=/);
|
||
assert.match(feature, /feature\.status === 'error' \? feature\.error : null/);
|
||
assert.match(overview, /if \(!isGateway && \(!connected \|\| !state\?\.connection\?\.startedAt\)\) return undefined/);
|
||
assert.match(feature, /trafficSourceError[\s\S]*Трафик не обновляется · последние данные/);
|
||
assert.match(subscription, /<Drawer[\s\S]*className="client-subscription-drawer"[\s\S]*open=\{drawerOpen\}/);
|
||
assert.match(subscription, /const deleteIdRef = useRef\(deleteId\)[\s\S]*if \(deleteIdRef\.current\) return/);
|
||
assert.match(subscription, /requestDelete: \(profileId: string\) => setDeleteId\(profileId\)[\s\S]*open=\{Boolean\(feature\.deleteProfile\)\}[\s\S]*onCancel=\{feature\.cancelDelete\}[\s\S]*onConfirm=\{feature\.confirmDelete\}/);
|
||
assert.match(connection, /aria-label=\{remoteOwned[\s\S]*isGateway[\s\S]*Остановить Harbor Connect[\s\S]*Запустить Harbor Connect/);
|
||
assert.match(connection, /className=\{`client-power-control\$\{powerUnavailable \? ' client-tooltip-anchor' : ''\}`\}[\s\S]*aria-describedby=\{powerUnavailable \? 'gateway-power-unavailable'[\s\S]*Подключением управляет Harbor Gateway[\s\S]*Сначала добавьте подписку и выберите сервер/);
|
||
assert.match(connection, /const powerButton = <button[\s\S]*className="client-power"[\s\S]*client-power-control[\s\S]*\{brandSlot\}[\s\S]*\{powerButton\}/);
|
||
assert.match(overview, /isGateway && hasSubscription && <DevicesPanel[\s\S]*feature=\{devicesFeature\}/);
|
||
assert.doesNotMatch(panel, /const \[snapshot, setSnapshot\]|setTimeout\(\(\) => load\(true\),/);
|
||
});
|
||
|
||
test('traffic chart scale compares every visible route before choosing the maximum', () => {
|
||
assert.match(chart, /function trafficSeriesMax[\s\S]*Object\.values\(value\)\.reduce[\s\S]*item > current \? item : current/);
|
||
});
|
||
|
||
test('device last-seen copy is compact with precise accessible and relative forms', () => {
|
||
const lastSeen = new Date(2026, 7, 7, 13, 15);
|
||
const now = new Date(2026, 7, 7, 13, 27);
|
||
assert.deepEqual(
|
||
formatLastSeen(lastSeen.toISOString(), now),
|
||
{
|
||
label: 'Сегодня, 13:15',
|
||
relative: '12 минут назад',
|
||
tooltip: '7 августа 2026 г. в 13:15 (12 минут назад)',
|
||
},
|
||
);
|
||
});
|
||
|
||
test('device traffic formatting and sorting preserve uint64 precision and canonical ties', () => {
|
||
assert.equal(formatByteString('9007199254740993'), '8,0 ПБ');
|
||
assert.equal(formatByteString('1536'), '1,5 КБ');
|
||
assert.equal(formatByteString('invalid'), '0 Б');
|
||
assert.equal(positiveByteDelta('1048576', '3145728'), '2,0 МБ');
|
||
assert.equal(positiveByteDelta('3145728', '3145728'), '');
|
||
assert.equal(positiveByteDelta('3145728', '1048576'), '');
|
||
assert.equal(trafficScaleRatio('50', '100', 'linear'), 0.5);
|
||
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' },
|
||
{ id: 'b', uploadBytes: '9007199254740992', downloadBytes: '2' },
|
||
{ id: 'd', uploadBytes: '15', downloadBytes: '5', proxyUploadBytes: '100' },
|
||
{ id: 'c', uploadBytes: '10', downloadBytes: '10', proxyDownloadBytes: '100' },
|
||
{ id: 'e', pinned: true, uploadBytes: '0', downloadBytes: '0' },
|
||
{ id: 'f', deprioritized: true, uploadBytes: '999999', downloadBytes: '0' },
|
||
];
|
||
assert.deepEqual(sortDevicesByTraffic(devices, 'desc').map(({ id }) => id), ['e', 'b', 'a', 'c', 'd', 'f']);
|
||
assert.deepEqual(sortDevicesByTraffic(devices, 'asc').map(({ id }) => id), ['e', 'c', 'd', 'a', 'b', 'f']);
|
||
assert.deepEqual(
|
||
stabilizeDevicesByTraffic([
|
||
{ id: 'a', uploadBytes: '999' },
|
||
{ id: 'b', uploadBytes: '1' },
|
||
], 'desc', ['b', 'a']).ids,
|
||
['b', 'a'],
|
||
);
|
||
assert.deepEqual(
|
||
stabilizeDevicesByTraffic([
|
||
{ id: 'a', uploadBytes: '999' },
|
||
{ id: 'b', pinned: true, uploadBytes: '1' },
|
||
], 'desc', ['a', 'b']).ids,
|
||
['b', 'a'],
|
||
);
|
||
assert.deepEqual(
|
||
stabilizeDevicesByTraffic([
|
||
{ id: 'a', deprioritized: true, uploadBytes: '999' },
|
||
{ id: 'b', uploadBytes: '1' },
|
||
{ id: 'c', pinned: true, uploadBytes: '0' },
|
||
], 'desc', ['a', 'b', 'c']).ids,
|
||
['c', 'b', 'a'],
|
||
);
|
||
});
|