Files
harbor-net/test/web/device-inventory-contract.test.js
dokril f4882c53c2
Build and Deploy Gateway / build-and-push (push) Successful in 24s
Build and Deploy Gateway / deploy (push) Successful in 6s
Improve device tag editing and version clients
2026-08-31 02:26:24 +03:00

434 lines
40 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
deviceFilterCounts,
filterDevices,
formatLastSeen,
isNewDevice,
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, /const activeAnimation = movementAnimations\.current\.get\(id\)[\s\S]*activeAnimation\.commitStyles\(\)[\s\S]*activeAnimation\.cancel\(\)[\s\S]*interruptedPositions\.set/);
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, /const newDevice = isNewDevice\(device\.firstSeenAt\)/);
assert.match(panel, /client-device-new-badge[\s\S]*aria-hidden="true"[\s\S]*>NEW<[\s\S]*client-device-new-a11y[\s\S]*Новое устройство/);
assert.match(panel, /displayPolicy === 'direct'[\s\S]*newDevice[\s\S]*Новое устройство идёт напрямую\. Нажмите, чтобы разрешить VPN через правила Gateway/);
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, /filterDevices\(allDevices, tags,[\s\S]*stabilizeDevicesByTraffic\(filteredDevices, sortDirection, previousIds\)[\s\S]*stabilizeDevicesByTraffic\(allDevices, sortDirection, previousIds\)[\s\S]*trafficOrder\.current = \{ direction: sortDirection, ids: canonical\.ids \}/);
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\)[\s\S]*positiveByteDelta\(previous\.vpn, vpn\)[\s\S]*positiveByteDelta\(previous\.direct, direct\)[\s\S]*positiveByteDelta\(previous\.unknown, unknown\)/);
assert.match(panel, /trafficView === 'outbound' \? trafficDelta\.outboundTotal : trafficDelta\.total/);
assert.match(panel, /<b>VPN<\/b><TrafficValue[^>]*delta=\{trafficDelta\.vpn\}[\s\S]*<b>Direct<\/b><TrafficValue[^>]*delta=\{trafficDelta\.direct\}[\s\S]*<b>Другое<\/b><TrafficValue[^>]*delta=\{trafficDelta\.unknown\}/);
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-drawer \{[\s\S]*width: min\(580px, 100vw\)/);
assert.match(styles, /\.client-drawer\.client-devices \{[\s\S]*width: min\(780px, 100vw\)/);
assert.match(styles, /\.client-devices-layout \{[\s\S]*grid-template-columns: 168px minmax\(0, 1fr\);[\s\S]*gap: 28px/);
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-new-badge \{[\s\S]*height: 14px;[\s\S]*flex: 0 0 auto;[\s\S]*padding: 0 4px;[\s\S]*border: 1px solid color-mix\(in oklch, var\(--client-accent\)[\s\S]*border-radius: 999px;[\s\S]*background: var\(--client-accent-soft\);[\s\S]*font: var\(--type-micro\)/);
assert.match(styles, /\.client-device-new-a11y \{[\s\S]*position: absolute;[\s\S]*width: 1px;[\s\S]*clip-path: inset\(50%\)/);
assert.doesNotMatch(styles, /client-device-new-badge[^{]*\{[^}]*(?:transition|animation):/);
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(panel, /<div className="client-devices-kicker">[\s\S]*<header className="client-instructions-header client-devices-header">/);
assert.match(styles, /\.client-devices-kicker \{[^}]*position: sticky;[^}]*top: 0;[^}]*z-index: 30/);
assert.match(styles, /\.client-devices-kicker \.client-tooltip \{[^}]*top: calc\(100% \+ 6px\);[^}]*bottom: auto/);
assert.match(styles, /\.client-devices-reset-wrap \{[^}]*margin-left: auto/);
assert.match(styles, /\.client-devices-reset-wrap > \.client-tooltip \{[^}]*white-space: nowrap/);
assert.match(styles, /\.client-devices-reset-wrap\.client-tooltip-anchor:hover > \.client-tooltip,[\s\S]*transform: translate\(0, 0\)/);
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('device tags keep filtering, assignment and management accessible at every width', () => {
assert.match(panel, /Найти устройство/);
assert.match(panel, /'all', label: 'Все'[\s\S]*'new', label: 'Новые'[\s\S]*'pinned', label: 'Закреплённые'[\s\S]*'background', label: 'Фоновые'[\s\S]*'untagged', label: 'Без тегов'/);
assert.match(panel, /if \(value === 'untagged'\) setSelectedTagIds\(\[\]\)/);
assert.match(panel, /if \(systemFilter === 'untagged'\) setSystemFilter\('all'\)/);
assert.match(panel, /deviceFilterCounts\(allDevices, tags\)/);
assert.match(panel, /По этим фильтрам устройств нет\.[\s\S]*Сбросить фильтры/);
assert.match(panel, /deviceTags = tags\.filter[\s\S]*client-device-tag-trigger[\s\S]*\+\{deviceTags\.length - 1\}[\s\S]*'\+ тег'/);
assert.match(panel, /role="dialog"[\s\S]*aria-modal="true"[\s\S]*aria-label="Теги"/);
assert.match(panel, /client-device-tag-popover-options[\s\S]*aria-pressed=\{selected\}[\s\S]*toggleDeviceTag\(tag\.id\)/);
assert.match(panel, /tagMutationPending = useRef\(false\)[\s\S]*if \(!tagPopover \|\| tagMutationPending\.current \|\| tagSavingId === tagPopover\.deviceId\) return[\s\S]*if \(tagMutationPending\.current && !immediate\) return[\s\S]*if \(!tagPopover \|\| tagMutationPending\.current\) return/);
assert.match(panel, /current\?\.deviceId === device\.id[\s\S]*baseline: next, draft: next[\s\S]*current\?\.deviceId === device\.id[\s\S]*draft: baseline[\s\S]*tagMutationPending\.current = false[\s\S]*setTagMutationCycle/);
assert.match(panel, /aria-disabled=\{Boolean\(tagSavingId\)[\s\S]*if \(tagSavingId \|\| \(!selected && tagPopover\.draft\.length >= 8\)\) return/);
assert.doesNotMatch(panel, /client-device-tag-popover-actions|type="checkbox"|<h3>Теги устройства/);
assert.match(panel, /contentRef\.current\.inert = true[\s\S]*layoutRef\.current\.inert = true/);
assert.match(panel, /FOCUSABLE[\s\S]*event\.key === 'Escape'[\s\S]*document\.activeElement === first/);
assert.match(panel, /catalogChanged[\s\S]*Список тегов изменился\. Откройте теги устройства снова\./);
assert.match(panel, /Теги доступны после обновления Gateway\./);
assert.match(panel, /Управление тегами[\s\S]*Переименовать тег[\s\S]*Удалить тег/);
assert.match(panel, /placeholder="Найти устройство"[\s\S]*Результатов: \{devices\.length\}/);
assert.match(panel, />Назад<\/button>[\s\S]*\+ Добавить тег/);
assert.match(panel, /Не удалось изменить тег\./);
assert.match(panel, /restoreTagTriggerFocus[\s\S]*searchRef\.current,[\s\S]*filterButtonRef\.current,[\s\S]*candidate\?\.isConnected && !candidate\.closest\('\[inert\]'\)/);
assert.match(panel, /setTagPopover\(\(current\) => current && \(\{ \.\.\.current, draft: next \}\)\)[\s\S]*updateDeviceTags\(device, next, baseline\)[\s\S]*baseline: next, draft: next[\s\S]*draft: baseline/);
assert.match(feature, /updateDeviceTags\(device: Device, tagIds: string\[\], baselineTagIds: string\[\]\)[\s\S]*sameStringList\(currentDevice\.tagIds, baselineTagIds\)[\s\S]*sameStringList\(latestDevice\.tagIds, baselineTagIds\)/);
assert.match(feature, /renameTag\(tag: DeviceTag, name: string, baselineName: string\)[\s\S]*snapshot\.tags\.find[\s\S]*name !== baselineName[\s\S]*latestTag\.name !== baselineName/);
assert.match(panel, /Тег больше не будет доступен для назначения\. Устройства и маршруты не изменятся\./);
assert.match(panel, /Тег исчезнет у одного устройства\. Само устройство и его маршрут не изменятся\./);
assert.match(panel, /Сами устройства и их маршруты не изменятся\./);
assert.match(feature, /deleteDeviceTag\(tag\.id, snapshot\.revision\)[\s\S]*STATE_CONFLICT[\s\S]*listDevices\(\)[\s\S]*return 'conflict'/);
assert.doesNotMatch(feature, /deleteDeviceTag\(tag\.id, latest\.revision\)/);
assert.match(styles, /@media \(max-width: 640px\)[\s\S]*width: min\(300px, calc\(100vw - 48px\)\)[\s\S]*translateX\(-100%\)[\s\S]*260ms cubic-bezier\(0\.16, 1, 0\.3, 1\)/);
assert.match(panel, /activeAnimation\.commitStyles\(\)[\s\S]*activeAnimation\.cancel\(\)[\s\S]*opacity: 0, transform: 'translateX\(-6px\)'[\s\S]*duration: 160[\s\S]*opacity: 0, transform: 'translateX\(6px\)'[\s\S]*opacity: 1, transform: 'translateX\(0\)'/);
assert.match(styles, /client-device-tag-popover-in 160ms ease[\s\S]*translateY\(-6px\)[\s\S]*client-device-tag-popover-out 160ms ease forwards/);
assert.match(styles, /client-devices-rail-backdrop \{[\s\S]*opacity: 0[\s\S]*opacity 260ms cubic-bezier\(0\.16, 1, 0\.3, 1\)[\s\S]*client-devices-rail-backdrop\.is-open[\s\S]*opacity: 1/);
assert.match(styles, /client-device-tag-trigger \{[\s\S]*height: 14px[\s\S]*flex: 0 0 auto/);
assert.match(styles, /\.client-devices-tag-create input,[\s\S]*\.client-devices-tag-manager-row input \{[\s\S]*border: 0;[\s\S]*border-bottom: 1px solid var\(--client-border\);[\s\S]*border-radius: 0;[\s\S]*background: transparent/);
assert.match(panel, /client-devices-tag-edit[\s\S]*is-writing[\s\S]*client-row-delete client-devices-tag-delete[\s\S]*client-row-delete-lid/);
assert.match(styles, /\.client-devices-tag-manager-row form button,\n\.client-devices-tag-edit,\n\.client-device-tag-popover button,/);
assert.match(styles, /\.client-devices-manage-tags,[\s\S]*\.client-devices-filter-empty button \{[\s\S]*border: 0;[\s\S]*background: transparent/);
assert.match(styles, /\.client-devices-tag-edit\.is-writing svg \{[\s\S]*client-device-pencil-write 620ms/);
assert.match(styles, /\.client-device-tag-popover-options button \{[\s\S]*color: var\(--client-muted\)[\s\S]*button\[aria-pressed="true"\] \{[\s\S]*color: var\(--tag-tone\)[\s\S]*drop-shadow/);
assert.match(styles, /@media \(hover: hover\)[\s\S]*client-device-tag-trigger:not\(\.has-tag\)[\s\S]*opacity: 0/);
assert.match(styles, /@media \(hover: none\)[\s\S]*client-device-tag-trigger:not\(\.has-tag\)[\s\S]*opacity: 1/);
assert.match(styles, /prefers-reduced-motion: reduce[\s\S]*client-devices-rail[\s\S]*client-devices-rail-view[\s\S]*client-devices-tag-create input[\s\S]*client-devices-tag-edit[\s\S]*client-device-tag-popover/);
});
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 \|\| failoverIdentity\} \/>\}/);
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('new-device marker uses an exact seven-day window', () => {
const now = Date.parse('2026-08-31T12:00:00.000Z');
assert.equal(isNewDevice(new Date(now - (7 * 24 * 60 * 60 * 1000 - 1_000)).toISOString(), now), true);
assert.equal(isNewDevice(new Date(now - 7 * 24 * 60 * 60 * 1000).toISOString(), now), false);
assert.equal(isNewDevice(new Date(now + 1_000).toISOString(), now), false);
assert.equal(isNewDevice('not-a-date', now), false);
});
test('device filters combine system and search with tag OR while counts stay absolute', () => {
const now = Date.parse('2026-08-31T12:00:00.000Z');
const tags = [
{ id: 'tag_0000000000000001', name: 'Умный дом' },
{ id: 'tag_0000000000000002', name: 'Работа' },
];
const devices = Array.from({ length: 100 }, (_, index) => ({
id: `device-${index}`,
alias: index === 3 ? 'Телевизор' : `Клиент ${index}`,
hostname: index === 4 ? 'office-mac.local' : null,
ip: `192.168.50.${index + 1}`,
mac: `00:11:22:33:44:${index.toString(16).padStart(2, '0')}`,
firstSeenAt: new Date(now - (index < 7 ? index : 10) * 86_400_000).toISOString(),
pinned: index === 3,
deprioritized: index === 5,
tagIds: index === 3 ? [tags[0].id] : index === 4 ? [tags[1].id] : [],
}));
assert.deepEqual(filterDevices(devices, tags, {
system: 'all', tagIds: tags.map(({ id }) => id), query: 'local',
}, now).map(({ id }) => id), ['device-4']);
assert.deepEqual(filterDevices(devices, tags, {
system: 'pinned', tagIds: [tags[0].id, tags[1].id], query: 'умный',
}, now).map(({ id }) => id), ['device-3']);
assert.equal(filterDevices(devices, tags, {
system: 'untagged', tagIds: [], query: '192.168.50.',
}, now).length, 98);
assert.deepEqual(deviceFilterCounts(devices, tags, now), {
all: 100,
new: 7,
pinned: 1,
background: 1,
untagged: 98,
byTag: { [tags[0].id]: 1, [tags[1].id]: 1 },
});
const canonical = stabilizeDevicesByTraffic(devices, 'desc');
const visible = filterDevices(devices, tags, {
system: 'all', tagIds: [], query: '192.168.50.1',
}, now);
const filteredOrder = stabilizeDevicesByTraffic(visible, 'desc', canonical.ids).devices;
assert.deepEqual(filteredOrder.map(({ id }) => id), canonical.devices
.filter(({ id }) => visible.some((device) => device.id === id))
.map(({ id }) => id));
assert.deepEqual(stabilizeDevicesByTraffic(devices, 'desc', canonical.ids).ids, canonical.ids);
});
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'],
);
});