Refactor VPN proxy client implementation
This commit is contained in:
@@ -11,37 +11,42 @@ import {
|
||||
stabilizeDevicesByTraffic,
|
||||
trafficAxisMid,
|
||||
trafficScaleRatio,
|
||||
} from '../../src/web/utils/format.js';
|
||||
} 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.jsx'), 'utf8');
|
||||
const panel = fs.readFileSync(path.join(root, 'src/web/components/DevicesPanel.jsx'), 'utf8');
|
||||
const chart = fs.readFileSync(path.join(root, 'src/web/components/TrafficChart.jsx'), 'utf8');
|
||||
const api = fs.readFileSync(path.join(root, 'src/web/api.js'), 'utf8');
|
||||
const server = fs.readFileSync(path.join(root, 'src/server/index.js'), 'utf8');
|
||||
const styles = fs.readFileSync(path.join(root, 'src/web/styles.css'), 'utf8');
|
||||
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 && <button[\s\S]*client-devices-toggle/);
|
||||
assert.match(overview, /api\.devices\.list\(\)/);
|
||||
assert.match(overview, /api\.devices\.refresh\(\)/);
|
||||
assert.match(overview, /DEVICE_AUTO_REFRESH_MS = 15_000/);
|
||||
assert.match(overview, /setDeviceSnapshot\(\(current\) => !current \|\| next\.revision > current\.revision/);
|
||||
assert.match(overview, /snapshot=\{deviceSnapshot\}[\s\S]*onSnapshot=\{setDeviceSnapshot\}/);
|
||||
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(panel, /api\.devices\.update\(device\.id, patch, snapshot\.revision\)/);
|
||||
assert.match(panel, /requestError\.code !== 'STATE_CONFLICT'[\s\S]*api\.devices\.list\(\)[\s\S]*Object\.keys\(patch\)\.some\(\(key\) => latestDevice\[key\] !== device\[key\]\)[\s\S]*api\.devices\.update\(device\.id, patch, latest\.revision\)/);
|
||||
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, /const orderChanged = previousOrder\.current\.length > 0/);
|
||||
assert.match(panel, /previousScrollTop\.current - currentScrollTop/);
|
||||
assert.match(panel, /next\.revision > current\.revision/);
|
||||
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, /setPolicy: \(id, mode, expectedRevision\) => request\(`\/api\/devices\/\$\{id\}\/policy`/);
|
||||
assert.match(server, /requestUrl\.pathname === '\/api\/devices\/refresh'[\s\S]*deviceInventory\.refresh\(\)/);
|
||||
assert.match(server, /\/api\\\/devices\\\/\(dev_\[a-f0-9\]\{16\}\)\\\/policy\$[\s\S]*deviceInventory\.setPolicy/);
|
||||
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, /DEVICE_POLICY_PATH[\s\S]*deviceInventory\.setPolicy/);
|
||||
assert.match(panel, /<Tooltip>Изменить название<\/Tooltip>/);
|
||||
assert.match(panel, /copyText\(device\.ip\)/);
|
||||
assert.match(panel, /const hasName = Boolean\(device\.alias \|\| device\.hostname\)/);
|
||||
@@ -51,7 +56,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
||||
assert.match(panel, /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` \}\}/);
|
||||
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"|Сохранить название|Отменить изменение/);
|
||||
@@ -77,12 +82,12 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
||||
assert.match(panel, /client-device-traffic-breakdown[\s\S]*<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=\{device\.trafficHistory \|\| \[\]\}[\s\S]*scale=\{trafficScale\}[\s\S]*capacity=\{snapshot\.trafficHistoryCapacity/);
|
||||
assert.match(panel, /TrafficChart[\s\S]*samples=\{device\.trafficHistory \|\| \[\]\}[\s\S]*scale=\{trafficScale\}[\s\S]*capacity=\{snapshot\?\.trafficHistoryCapacity/);
|
||||
assert.doesNotMatch(panel, /setTrafficHistory|TRAFFIC_HISTORY_LIMIT/);
|
||||
assert.match(panel, /aria-pressed=\{trafficScale === 'linear'\}[\s\S]*aria-pressed=\{trafficScale === 'log'\}/);
|
||||
assert.match(chart, /previousScale\.current !== scale[\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\) => 100 - ratio \* \(100 - TRAFFIC_CHART_HEADROOM\)/);
|
||||
assert.match(chart, /TRAFFIC_CHART_HEADROOM = 10[\s\S]*trafficChartY = \(ratio: number\) => 100 - ratio \* \(100 - TRAFFIC_CHART_HEADROOM\)/);
|
||||
assert.match(chart, /gatewayY: trafficChartY\(trafficScaleRatio\(gateway, max, scale\)\)[\s\S]*proxyY: trafficChartY\(trafficScaleRatio\(proxy, 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"/);
|
||||
@@ -101,8 +106,8 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
||||
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(panel, /api\.devices\.setPolicy\(device\.id, mode, snapshot\.revision\)/);
|
||||
assert.match(panel, /requestError\.code !== 'STATE_CONFLICT'[\s\S]*latestDevice\.desiredPolicy !== device\.desiredPolicy[\s\S]*api\.devices\.setPolicy\(device\.id, mode, latest\.revision\)/);
|
||||
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/);
|
||||
@@ -110,7 +115,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
||||
assert.doesNotMatch(panel, /точная MAC|частная MAC|<dt>Источник<\/dt>/);
|
||||
assert.match(panel, /aria-pressed=\{device\.pinned\}/);
|
||||
assert.doesNotMatch(panel, /Закрепите устройство, чтобы изменить маршрут|Сначала верните маршрут через Gateway/);
|
||||
assert.match(panel, /maxLength="64"[\s\S]*autoFocus/);
|
||||
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/);
|
||||
@@ -166,30 +171,29 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
||||
});
|
||||
|
||||
test('Gateway Home reuses the canonical device snapshot for applied route and global traffic', () => {
|
||||
const powerStart = overview.indexOf('<section className="client-power-section"');
|
||||
const trafficStart = overview.indexOf('<section className="client-gateway-summary"');
|
||||
const powerPrefix = overview.slice(powerStart, trafficStart);
|
||||
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, /appliedServer\?\.label \|\| 'VPN-сервер не используется'/);
|
||||
assert.match(overview, /selectedServerId !== appliedServerId[\s\S]*Переключаем на \{desiredServer\.label\}/);
|
||||
assert.match(overview, /formatByteString\(globalTraffic\?\.totalBytes \|\| '0'\)/);
|
||||
assert.match(overview, /samples=\{globalTraffic\?\.history \|\| \[\]\}[\s\S]*capacity=\{deviceSnapshot\?\.trafficHistoryCapacity \|\| 120\}[\s\S]*routeLabel="Gateway"/);
|
||||
assert.match(overview, /<section className="client-power-section"[\s\S]*client-connection-title[\s\S]*client-gateway-route-summary[\s\S]*client-state-detail[\s\S]*client-proxies[\s\S]*<section className="client-gateway-summary"/);
|
||||
assert.ok(powerStart >= 0 && trafficStart > powerStart, 'traffic summary follows the power section');
|
||||
assert.equal((powerPrefix.match(/<section\b/g) || []).length, (powerPrefix.match(/<\/section>/g) || []).length, 'power section is closed before traffic summary');
|
||||
assert.equal((overview.match(/className="client-gateway-summary"/g) || []).length, 1);
|
||||
assert.doesNotMatch(overview, /<TrafficChart[\s\S]{0,240}scale=/);
|
||||
assert.match(overview, /deviceStatus === 'error' \? deviceError : null/);
|
||||
assert.match(overview, /if \(!isGateway && \(!connected \|\| !state\?\.singboxStartedAt\)\) return undefined/);
|
||||
assert.match(overview, /trafficSourceError[\s\S]*Трафик не обновляется · последние данные/);
|
||||
assert.match(overview, /client-subscription-drawer\$\{subscriptionOpen \? ' is-open' : ''\}/);
|
||||
assert.match(overview, /const confirmingDeleteRef = useRef\(confirmingDelete\)[\s\S]*if \(confirmingDeleteRef\.current\) return/);
|
||||
assert.match(overview, /onClick=\{\(\) => setConfirmingDelete\(true\)\}[\s\S]*open=\{confirmingDelete\}[\s\S]*onCancel=\{\(\) => setConfirmingDelete\(false\)\}/);
|
||||
assert.match(overview, /aria-label=\{isGateway[\s\S]*Остановить Harbor Connect[\s\S]*Запустить Harbor Connect/);
|
||||
assert.match(overview, /className="client-power-control client-tooltip-anchor"[\s\S]*aria-describedby=\{powerUnavailable \? 'gateway-power-unavailable'[\s\S]*Сначала добавьте подписку и выберите сервер/);
|
||||
assert.match(overview, /const powerButton = <button[\s\S]*className="client-power"[\s\S]*\{isGateway \? <span[\s\S]*<\/span> : powerButton\}/);
|
||||
assert.match(overview, /isGateway && <DevicesPanel[\s\S]*snapshot=\{deviceSnapshot\}/);
|
||||
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.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, /client-subscription-drawer\$\{open \? ' is-open' : ''\}/);
|
||||
assert.match(subscription, /const confirmingDeleteRef = useRef\(confirmingDelete\)[\s\S]*if \(confirmingDeleteRef\.current\) return/);
|
||||
assert.match(subscription, /requestDelete: \(\) => setConfirmingDelete\(true\)[\s\S]*open=\{feature\.confirmingDelete\}[\s\S]*onCancel=\{feature\.cancelDelete\}/);
|
||||
assert.match(connection, /aria-label=\{isGateway[\s\S]*Остановить Harbor Connect[\s\S]*Запустить Harbor Connect/);
|
||||
assert.match(connection, /className="client-power-control client-tooltip-anchor"[\s\S]*aria-describedby=\{powerUnavailable \? 'gateway-power-unavailable'[\s\S]*Сначала добавьте подписку и выберите сервер/);
|
||||
assert.match(connection, /const powerButton = <button[\s\S]*className="client-power"[\s\S]*\{isGateway \? <span[\s\S]*<\/span> : powerButton\}/);
|
||||
assert.match(overview, /isGateway && <DevicesPanel[\s\S]*feature=\{devicesFeature\}/);
|
||||
assert.doesNotMatch(panel, /const \[snapshot, setSnapshot\]|setTimeout\(\(\) => load\(true\),/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user