Refactor VPN proxy client implementation
Build and Deploy Gateway / build-and-push (push) Failing after 2s
Build and Deploy Gateway / deploy (push) Has been skipped

This commit is contained in:
2026-08-09 00:41:52 +03:00
parent 32be4380a3
commit 34d8b681ad
190 changed files with 20064 additions and 9761 deletions
+103 -1
View File
@@ -2,9 +2,10 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import {
api,
HarborApiError,
request,
} from '../../src/web/api.js';
} from '../../.test-dist/src/web/api/harborClient.js';
const response = (status, error) => ({
ok: status >= 200 && status < 300,
@@ -53,3 +54,104 @@ test('local unknown errors get a safe message and diagnostic reference', () => {
assert.equal(typeof error.correlationId, 'string');
assert.ok(error.correlationId.length >= 8);
});
test('typed endpoint facade preserves exact request contracts and raw payload identity', async () => {
const calls = [];
const payload = { success: true, marker: 'raw' };
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url, options) => {
calls.push([url, options]);
return { ok: true, status: 200, json: async () => payload };
};
const signal = new AbortController().signal;
try {
const cases = [
[() => api.version(), '/api/version', {}],
[() => api.subscription.validate('https://sub', { signal }), '/api/subscription/validate', {
method: 'POST', body: JSON.stringify({ url: 'https://sub' }), signal,
}],
[() => api.subscription.fetch('https://sub'), '/api/subscription/fetch', {
method: 'POST', body: JSON.stringify({ url: 'https://sub' }),
}],
[() => api.subscription.refresh(), '/api/subscription/refresh', { method: 'POST' }],
[() => api.subscription.forget(), '/api/subscription', { method: 'DELETE' }],
[() => api.apply('server-1'), '/api/apply', {
method: 'POST', body: JSON.stringify({ serverId: 'server-1', selectedTag: 'server-1' }),
}],
[() => api.gatewayAuto.setEnabled(true), '/api/gateway-auto', {
method: 'POST', body: JSON.stringify({ enabled: true }),
}],
[() => api.routeRules.update([{ type: 'domain', value: 'example.com' }], 7), '/api/route-rules', {
method: 'PUT',
body: JSON.stringify({
rules: [{ type: 'domain', value: 'example.com' }],
expectedRulesRevision: 7,
}),
}],
[() => api.devices.list(), '/api/devices', {}],
[() => api.devices.refresh(), '/api/devices/refresh', { method: 'POST' }],
[() => api.devices.update('dev_1', { alias: 'TV' }, 8), '/api/devices/dev_1', {
method: 'PUT', body: JSON.stringify({ alias: 'TV', expectedRevision: 8 }),
}],
[() => api.devices.setPolicy('dev_1', 'direct', 9), '/api/devices/dev_1/policy', {
method: 'PUT', body: JSON.stringify({ mode: 'direct', expectedRevision: 9 }),
}],
[() => api.diagnostics.connectivity(), '/api/diagnostics/connectivity', {
method: 'POST', body: JSON.stringify({ services: [], target: null }),
}],
[() => api.singbox.stop(), '/api/singbox/stop', { method: 'POST' }],
[() => api.singbox.restart(), '/api/singbox/restart', { method: 'POST' }],
[() => api.servers.ping(['one', 'two']), '/api/servers/ping-all', {
method: 'POST', body: JSON.stringify({ serverIds: ['one', 'two'] }),
}],
];
for (const [invoke, url, options] of cases) {
assert.equal(await invoke(), payload);
const [actualUrl, actualOptions] = calls.at(-1);
assert.equal(actualUrl, url);
assert.deepEqual(actualOptions, {
...options,
headers: { 'content-type': 'application/json' },
});
}
} finally {
globalThis.fetch = originalFetch;
}
});
test('request preserves caller headers, AbortError identity and JSON fallbacks', async () => {
let received;
const value = { ok: 'raw' };
assert.equal(await request('/api/test', {
headers: { 'content-type': 'application/custom', 'x-harbor': 'yes' },
}, async (url, options) => {
received = [url, options];
return { ok: true, status: 200, json: async () => value };
}), value);
assert.deepEqual(received, ['/api/test', {
headers: { 'content-type': 'application/custom', 'x-harbor': 'yes' },
}]);
const aborted = Object.assign(new Error('cancelled'), { name: 'AbortError' });
await assert.rejects(
request('/api/test', {}, async () => { throw aborted; }),
(error) => error === aborted,
);
await assert.rejects(
request('/api/test', {}, async () => ({
ok: true,
status: 200,
json: async () => { throw new Error('invalid json'); },
})),
(error) => error.code === 'UNKNOWN' && error.status === 500,
);
await assert.rejects(
request('/api/test', {}, async () => ({
ok: false,
status: 503,
json: async () => { throw new Error('invalid json'); },
})),
(error) => error.code === 'CONTROL_UNREACHABLE' && error.status === 503,
);
});
+25
View File
@@ -0,0 +1,25 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
const index = readFileSync(new URL('../../index.html', import.meta.url), 'utf8');
const main = readFileSync(new URL('../../src/web/main.tsx', import.meta.url), 'utf8');
const app = readFileSync(new URL('../../src/web/App.tsx', import.meta.url), 'utf8');
test('typed main is the sole browser bootstrap owner', () => {
assert.match(index, /src="\/src\/web\/main\.tsx"/);
assert.doesNotMatch(index, /src="\/src\/web\/App\.tsx"/);
assert.match(main, /import \{ App \} from '\.\/App\.js'/);
assert.match(main, /import '\.\/styles\/index\.css'/);
assert.match(main, /document\.getElementById\('root'\)/);
assert.match(main, /throw new Error\('Harbor root element not found'\)/);
assert.equal((main.match(/createRoot\(/g) || []).length, 1);
assert.match(main, /createRoot\(root\)\.render\(<App \/>\)/);
});
test('App remains the exported composition component without bootstrap side effects', () => {
assert.match(app, /export function App\(\)/);
assert.doesNotMatch(app, /createRoot|react-dom\/client|styles(?:\/index)?\.css|getElementById\('root'\)/);
assert.match(app, /<ClientOverviewPage/);
assert.match(app, /<StaleBanner/);
});
+2 -2
View File
@@ -11,8 +11,8 @@ import {
subscriptionDomain,
subscriptionDaysLeft,
subscriptionUsage,
} from '../../src/web/utils/clientControls.js';
import { instructionBlocks } from '../../src/web/instructions.js';
} from '../../.test-dist/src/web/utils/clientControls.js';
import { instructionBlocks } from '../../.test-dist/src/web/features/instructions/instructionBlocks.js';
test('connection button chooses the only valid client action', () => {
assert.deepEqual(connectionAction({ connected: true }), { type: 'stop' });
@@ -0,0 +1,36 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
const source = (file) => readFileSync(new URL(`../../src/web/${file}`, import.meta.url), 'utf8');
const app = source('App.tsx');
const overview = source('components/ClientOverviewPage.tsx');
const subscription = source('features/subscription/SubscriptionFeature.tsx');
const devices = source('features/devices/DevicesPanel.tsx');
const deviceFeature = source('features/devices/DevicesFeature.tsx');
const servers = source('features/servers/ServerPicker.tsx');
const routing = source('features/routing/RoutingFeature.tsx');
const diagnostics = source('features/diagnostics/ConnectivityDiagnosticsPanel.tsx');
test('App owns one stable mapping from typed transport to component actions', () => {
assert.match(app, /const componentActions = \{[\s\S]*validateSubscription: api\.subscription\.validate[\s\S]*listDevices: api\.devices\.list[\s\S]*refreshDevices: api\.devices\.refresh[\s\S]*updateDevice: api\.devices\.update[\s\S]*setDevicePolicy: api\.devices\.setPolicy[\s\S]*pingServers: api\.servers\.ping[\s\S]*runConnectivityDiagnostics: api\.diagnostics\.connectivity[\s\S]*\};/);
assert.equal((app.match(/actions=\{componentActions\}/g) || []).length, 1);
assert.doesNotMatch(app, /componentActions\s*=\s*useMemo|componentActions\s*=\s*\([^)]*\)\s*=>/);
});
test('presentational components use only injected narrow actions', () => {
const components = [overview, subscription, devices, servers, routing, diagnostics].join('\n');
assert.doesNotMatch(components, /from ['"][^'"]*\/api\/harborClient\.js['"]|\bapi\./);
assert.match(overview, /validateSubscription: actions\.validateSubscription/);
assert.match(subscription, /await validateSubscription\(normalizedUrl, \{ signal: controller\.signal \}\)/);
assert.match(overview, /refreshDevices: actions\.refreshDevices/);
assert.match(deviceFeature, /discover \? refreshDevices\(\) : listDevices\(\)/);
assert.match(overview, /<ServerPicker[\s\S]*pingServers=\{actions\.pingServers\}/);
assert.match(overview, /useDevicesFeature\(\{[\s\S]*listDevices: actions\.listDevices[\s\S]*refreshDevices: actions\.refreshDevices[\s\S]*updateDevice: actions\.updateDevice[\s\S]*setDevicePolicy: actions\.setDevicePolicy/);
assert.match(overview, /<DevicesPanel feature=\{devicesFeature\} \/>/);
assert.match(overview, /<ConnectivityDiagnosticsPanel[\s\S]*runConnectivityDiagnostics=\{actions\.runConnectivityDiagnostics\}/);
assert.match(deviceFeature, /requestDeviceUpdate\(device\.id, patch, snapshot\.revision\)/);
assert.match(deviceFeature, /setDevicePolicy\(device\.id, mode, snapshot\.revision\)/);
assert.match(servers, /await pingServers\(ids\)/);
assert.match(diagnostics, /await runConnectivityDiagnostics\(customServices, target\)/);
});
@@ -0,0 +1,46 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
const root = path.resolve(import.meta.dirname, '../..');
const page = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.tsx'), 'utf8');
const panel = fs.readFileSync(path.join(root, 'src/web/features/connection/ConnectionPanel.tsx'), 'utf8');
const routing = fs.readFileSync(path.join(root, 'src/web/features/routing/RoutingFeature.tsx'), 'utf8');
const boundary = fs.readFileSync(path.join(root, 'src/web/features/connection/index.ts'), 'utf8');
test('connection feature is the sole always-mounted power panel owner', () => {
assert.equal(boundary.trim(), "export { ConnectionPanel } from './ConnectionPanel.js';");
assert.match(page, /import \{ ConnectionPanel \} from '\.\.\/features\/connection\/index\.js'/);
assert.equal((page.match(/<ConnectionPanel/g) || []).length, 1);
assert.match(page, /<main[\s\S]*<ConnectionPanel[\s\S]*<GatewayTrafficSummary/);
assert.doesNotMatch(page, /client-power-section|const powerButton|function toggleConnection|confirmingStop|id="stop-connection"|DURATION_MODE_STORAGE_KEY/);
assert.match(panel, /\{visible && <section className="client-power-section"/);
assert.match(panel, /<ConfirmationDialog[\s\S]*open=\{confirmingStop\}/);
assert.ok(panel.indexOf('<ConfirmationDialog') > panel.indexOf('{visible && <section'), 'dialog remains mounted outside the visible section');
});
test('connection feature preserves actions, local preference and opaque neighbor slots', () => {
assert.doesNotMatch(panel, /from ['"][^'"]*\/api\/|\bapi\./);
assert.doesNotMatch(panel, /setInterval|setTimeout|useState\([^)]*state|useReducer/);
assert.match(panel, /connectionAction\(\{ connected, selectedServerId, configExists: configured \}\)/);
assert.match(panel, /action\?\.type === 'stop'[\s\S]*action\?\.type === 'apply'[\s\S]*action\?\.type === 'restart'/);
assert.match(panel, /if \(!await onStop\(\)\) return;[\s\S]*setConfirmingStop\(false\)/);
assert.match(panel, /localStorage\.getItem\(DURATION_MODE_STORAGE_KEY\) === 'words'[\s\S]*localStorage\.setItem\(DURATION_MODE_STORAGE_KEY, nextMode\)/);
assert.match(panel, /\{routingSlot\}[\s\S]*client-state-copy[\s\S]*\{serverSlot\}[\s\S]*client-state-detail[\s\S]*client-proxies[\s\S]*\{statusSlot\}/);
assert.match(page, /routingSlot=\{<RoutingPendingStatus[\s\S]*blocked=\{connectionBlocked\}[\s\S]*onRestart=\{onRestart\}/);
assert.match(routing, /client-route-rules-pending[\s\S]*Перезапустить VPN/);
assert.match(page, /serverSlot=\{isGateway && <div className="client-gateway-route-summary"/);
assert.match(page, /statusSlot=\{<>[\s\S]*InlineError[\s\S]*InlineProgress/);
});
test('shared clock, copy feedback and live announcement stay single-owned by the page', () => {
const pageBody = page.slice(page.indexOf('export function ClientOverviewPage'));
assert.equal((page.match(/setInterval\(\(\) => setNow\(Date\.now\(\)\), 1000\)/g) || []).length, 1);
assert.match(page, /export function ClientOverviewPage[\s\S]*const \[copyFeedback, setCopyFeedback\]/);
assert.doesNotMatch(panel, /const \[copyFeedback, setCopyFeedback\]/);
assert.equal((pageBody.match(/className="client-live-region"/g) || []).length, 1);
assert.match(page, /onCopyProxy=\{copyProxy\}/);
assert.match(panel, /localProxyUrls\(proxyPort, gatewayAddress\)/);
assert.match(panel, /onClick=\{\(\) => onCopyProxy\(kind\)\}/);
});
+49 -45
View File
@@ -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\),/);
});
+116
View File
@@ -0,0 +1,116 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { parseDeviceSnapshot } from '../../.test-dist/src/web/features/devices/deviceSnapshot.js';
const root = path.resolve(import.meta.dirname, '../..');
const page = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.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 model = fs.readFileSync(path.join(root, 'src/web/features/devices/deviceSnapshot.ts'), 'utf8');
const boundary = fs.readFileSync(path.join(root, 'src/web/features/devices/index.ts'), 'utf8');
test('devices feature is the sole public owner and legacy component paths are gone', () => {
assert.match(boundary, /DevicesPanel[\s\S]*DevicesToggle,[\s\S]*GatewayTrafficSummary,[\s\S]*useDevicesFeature/);
assert.equal(fs.existsSync(path.join(root, 'src/web/components/DevicesPanel.jsx')), false);
assert.equal(fs.existsSync(path.join(root, 'src/web/components/TrafficChart.jsx')), false);
assert.equal((page.match(/useDevicesFeature\(/g) || []).length, 1);
assert.equal((page.match(/<DevicesToggle/g) || []).length, 1);
assert.equal((page.match(/<GatewayTrafficSummary/g) || []).length, 1);
assert.equal((page.match(/<DevicesPanel/g) || []).length, 1);
assert.match(page, /useDevicesFeature\(\{[\s\S]*listDevices: actions\.listDevices[\s\S]*refreshDevices: actions\.refreshDevices[\s\S]*updateDevice: actions\.updateDevice[\s\S]*setDevicePolicy: actions\.setDevicePolicy[\s\S]*\}\)/);
assert.match(page, /<DevicesPanel feature=\{devicesFeature\} \/>/);
assert.doesNotMatch(page, /DEVICE_AUTO_REFRESH_MS|deviceSnapshot|deviceStatus|deviceError|devicesRefreshing|deviceRefreshCycle|devicesPanelRef|devicesToggleRef|devicesCloseRef|function loadDevices|client-devices-toggle|className="client-gateway-summary"/);
assert.doesNotMatch([feature, panel].join('\n'), /from ['"][^'"]*\/api\/|ConnectionPanel|SubscriptionFeature|RoutingFeature|DiagnosticsPanel/);
assert.doesNotMatch(panel, /listDevices|requestDeviceUpdate|setDevicePolicy|STATE_CONFLICT|DEVICE_POLICY_APPLY_FAILED|parseDeviceSnapshot/);
});
test('device controller preserves Gateway-only polling, monotonic publication and drawer lifecycle', () => {
assert.match(feature, /if \(!isGateway\) return undefined;[\s\S]*load\(\)/);
assert.match(feature, /discover \? refreshDevices\(\) : listDevices\(\)/);
assert.match(feature, /setTimeout\(\(\) => load\(true\), DEVICE_AUTO_REFRESH_MS\)/);
assert.match(feature, /setSnapshot\(\(current\) => !current \|\| next\.revision > current\.revision \? next : current\)/);
assert.match(feature, /finally \{[\s\S]*setRefreshing\(false\)[\s\S]*setRefreshCycle/);
assert.match(feature, /closeRef\.current\?\.focus\(\)[\s\S]*panelRef\.current\?\.contains[\s\S]*toggleRef\.current\?\.focus\(\)/);
assert.match(page, /<DevicesToggle[\s\S]*devicesFeature\.toggle\(\)/);
assert.match(page, /<GatewayTrafficSummary feature=\{devicesFeature\} now=\{now\}/);
});
test('all unknown inventory results pass one identity-preserving runtime parser', () => {
const observedAt = '2026-08-08T12:34:56.000Z';
const valid = {
revision: 3,
devices: [{
id: 'dev_0123456789abcdef',
alias: null,
hostname: null,
ip: null,
lastSeenAt: null,
status: 'online',
pinned: true,
downloadBytes: '12',
uploadBytes: '30',
proxyDownloadBytes: '0',
proxyUploadBytes: '0',
policyStatus: 'applied',
policyError: null,
desiredPolicy: 'vpn',
appliedPolicy: 'vpn',
confidence: 'high',
trafficHistory: [{ observedAt, gatewayBytes: '42', proxyBytes: '0' }],
}],
trafficHistoryCapacity: 120,
traffic: {
gatewayBytes: '42',
proxyBytes: '0',
totalBytes: '42',
gatewayObservedAt: observedAt,
proxyObservedAt: null,
observedAt: observedAt,
history: [{ observedAt, gatewayBytes: '42', proxyBytes: '0' }],
},
source: {
kind: 'neighbor',
lastObservedAt: observedAt,
error: null,
traffic: {
lastObservedAt: observedAt,
error: null,
proxy: { lastObservedAt: null, error: null },
},
policy: { lastAppliedAt: null, error: null },
},
extra: { retained: true },
};
assert.equal(parseDeviceSnapshot(valid), valid);
for (const invalid of [
null,
{},
{ ...valid, revision: -1 },
{ ...valid, revision: 1.5 },
{ ...valid, devices: undefined },
{ ...valid, trafficHistoryCapacity: 0 },
{ ...valid, traffic: undefined },
{ ...valid, source: undefined },
{ ...valid, devices: [{ ...valid.devices[0], id: 'not-a-device' }] },
{ ...valid, devices: [{ ...valid.devices[0], downloadBytes: 'not-bytes' }] },
{ ...valid, devices: [{ ...valid.devices[0], uploadBytes: '-1' }] },
{ ...valid, devices: [{ ...valid.devices[0], proxyUploadBytes: 1 }] },
{ ...valid, devices: [{ ...valid.devices[0], status: 'connected' }] },
{ ...valid, devices: [{ ...valid.devices[0], desiredPolicy: 'automatic' }] },
{ ...valid, devices: [{ ...valid.devices[0], policyStatus: 'queued' }] },
{ ...valid, devices: [{ ...valid.devices[0], confidence: 'unknown' }] },
{ ...valid, devices: [{ ...valid.devices[0], lastSeenAt: 'not-a-date' }] },
{ ...valid, devices: [{ ...valid.devices[0], trafficHistory: [{ observedAt, gatewayBytes: '1' }] }] },
{ ...valid, traffic: { ...valid.traffic, totalBytes: -4 } },
{ ...valid, traffic: { ...valid.traffic, observedAt: 'not-a-date' } },
{ ...valid, source: { ...valid.source, kind: 'arp' } },
{ ...valid, source: { ...valid.source, traffic: { proxy: [] } } },
]) assert.throws(() => parseDeviceSnapshot(invalid), TypeError);
assert.match(feature, /publish\(await \(discover \? refreshDevices\(\) : listDevices\(\)\)\)/);
assert.ok((feature.match(/parseDeviceSnapshot\(await/g) || []).length >= 6);
assert.match(feature, /DEVICE_POLICY_APPLY_FAILED[\s\S]*publish\(parseDeviceSnapshot\(await listDevices\(\)\)\)/);
assert.doesNotMatch(model, /\sas\s(?:DeviceSnapshot|Record<string, unknown>)/);
});
@@ -0,0 +1,87 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { parseConnectivityResult } from '../../.test-dist/src/web/features/diagnostics/connectivityResult.js';
const root = path.resolve(import.meta.dirname, '../..');
const page = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.tsx'), 'utf8');
const feature = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/DiagnosticsFeature.tsx'), 'utf8');
const panel = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/ConnectivityDiagnosticsPanel.tsx'), 'utf8');
const model = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/connectivityResult.ts'), 'utf8');
const boundary = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/index.ts'), 'utf8');
const ip = { source: 'cloudflare', address: '198.51.100.10', extra: true };
const site = { id: 'google', status: 'available', httpStatus: 204, latencyMs: 120 };
const pathResult = {
available: true,
internetAvailable: true,
ipv4: { addresses: ['198.51.100.10'], sources: [ip] },
ipv6: null,
ipv6Source: null,
sites: [site],
};
const valid = {
checkedAt: '2026-08-08T12:00:00.000Z',
direct: pathResult,
vpn: { ...pathResult, server: { id: 'server-1', label: 'Server 1' } },
extra: { retained: true },
};
test('diagnostics feature is the sole owner while the conditional panel keeps reset semantics', () => {
assert.match(boundary, /ConnectivityDiagnosticsPanel[\s\S]*DiagnosticsToggle,[\s\S]*useDiagnosticsFeature/);
assert.equal(fs.existsSync(path.join(root, 'src/web/components/ConnectivityDiagnosticsPanel.jsx')), false);
assert.equal((page.match(/useDiagnosticsFeature\(\)/g) || []).length, 1);
assert.equal((page.match(/<DiagnosticsToggle/g) || []).length, 1);
assert.equal((page.match(/<ConnectivityDiagnosticsPanel/g) || []).length, 1);
assert.match(page, /const diagnosticsAvailable = isGateway \|\| \(hasSubscription && subscriptionContentReady\)/);
assert.match(page, /\{diagnosticsAvailable && <ConnectivityDiagnosticsPanel[\s\S]*feature=\{diagnosticsFeature\}/);
assert.match(page, /if \(!diagnosticsAvailable\) diagnosticsFeature\.close\(\)/);
assert.doesNotMatch(page, /diagnosticsOpen|setDiagnosticsOpen|diagnosticsPanelRef|diagnosticsToggleRef|diagnosticsCloseRef|client-diagnostics-toggle/);
assert.doesNotMatch(feature, /setResult|customServices|localStorage|runConnectivityDiagnostics|activeTarget|removingServiceId/);
assert.match(feature, /closeRef\.current\?\.focus\(\)[\s\S]*panelRef\.current\?\.contains[\s\S]*toggleRef\.current\?\.focus\(\)/);
assert.doesNotMatch([feature, panel].join('\n'), /from ['"][^'"]*\/api\/|ConnectionPanel|SubscriptionFeature|RoutingFeature|DevicesFeature/);
});
test('unknown target and legacy-full results pass one identity-preserving parser before use', () => {
assert.equal(parseConnectivityResult(valid), valid);
const legacyFull = {
...valid,
direct: {
...valid.direct,
ipv4: { ...valid.direct.ipv4, sources: [ip, { source: 'ipify', address: null }] },
sites: [site, { id: 'youtube', status: 'responded', httpStatus: 403, latencyMs: null }],
},
};
assert.equal(parseConnectivityResult(legacyFull), legacyFull);
for (const invalid of [
null,
{},
{ ...valid, direct: undefined },
{ ...valid, direct: { ...valid.direct, available: 'yes' } },
{ ...valid, direct: { ...valid.direct, ipv4: { addresses: [], sources: [{}] } } },
{ ...valid, direct: { ...valid.direct, ipv6: 6 } },
{ ...valid, direct: { ...valid.direct, sites: [{ ...site, id: '' }] } },
{ ...valid, direct: { ...valid.direct, sites: [{ ...site, status: 'blocked' }] } },
{ ...valid, direct: { ...valid.direct, sites: [{ ...site, latencyMs: -1 }] } },
{ ...valid, vpn: { ...valid.vpn, server: undefined } },
{ ...valid, vpn: { ...valid.vpn, server: { id: 1, label: 'Server' } } },
]) assert.throws(() => parseConnectivityResult(invalid), TypeError);
assert.match(panel, /parseConnectivityResult\(await runConnectivityDiagnostics\(customServices, target\)\)/);
assert.match(panel, /const legacyFullResult = partial\.direct\.ipv4\.sources\.length > 1 \|\| partial\.direct\.sites\.length > 1/);
assert.match(panel, /next = legacyFullResult \? partial : mergeResult\(next, partial\)/);
assert.doesNotMatch(model, /\sas\s(?:ConnectivityResult|Record<string, unknown>)/);
});
test('serial probes, storage and editor behavior stay panel-owned', () => {
assert.match(panel, /const targets = \[[\s\S]*CONNECTIVITY_IP_SOURCES\.map[\s\S]*sites\.map/);
assert.match(panel, /for \(const target of targets\) \{[\s\S]*setActiveTarget\(target\)[\s\S]*await runConnectivityDiagnostics\(customServices, target\)[\s\S]*if \(legacyFullResult\) break/);
assert.match(panel, /CUSTOM_SERVICES_KEY = 'harbor-diagnostic-services'/);
assert.match(panel, /HIDDEN_SERVICES_KEY = 'harbor-hidden-diagnostic-services'/);
assert.match(panel, /slice\(0, MAX_CUSTOM_DIAGNOSTIC_SERVICES\)/);
assert.match(panel, /parsed\.protocol !== 'https:'/);
assert.match(panel, /document\.startViewTransition\(update\)/);
assert.match(panel, /retryable: Boolean\(Reflect\.get\(value, 'retryable'\)\)/);
assert.match(panel, /requestDetails\(error\)[\s\S]*requestError\.retryable/);
});
+37 -1
View File
@@ -5,7 +5,9 @@ import {
classifySyncError,
harborReducer,
initialHarborState,
} from '../../src/web/state/harborReducer.js';
} from '../../.test-dist/src/web/state/harborReducer.js';
import { parseHarborState } from '../../.test-dist/src/web/api/harborClient.js';
import { createStateSnapshot } from '../../.test-dist/src/shared/contracts/state.js';
const snapshot = (revision, desiredServerId = '', serverIds = ['one', 'two']) => ({
apiVersion: 1,
@@ -26,6 +28,40 @@ function deferred() {
return { promise, resolve };
}
test('typed Harbor client validates unknown state and isolates wire compatibility fields', () => {
const snapshot = createStateSnapshot({
storedState: {},
runtime: { running: false },
gatewayAuto: null,
appMode: 'client',
configExists: false,
subscriptionHost: '',
now: new Date('2026-07-11T12:00:00.000Z'),
});
const parsed = parseHarborState({
...snapshot,
proxyPort: 9082,
configExists: true,
gatewayAuto: { available: true },
});
assert.deepEqual(parsed.clientRuntime, {
proxyPort: 9082,
configured: true,
gatewayAvailable: true,
});
assert.equal(Object.hasOwn(parsed, 'proxyPort'), false);
let incompatible;
try {
parseHarborState({ ...snapshot, revision: -1 });
} catch (error) {
incompatible = error;
}
assert.equal(incompatible.code, 'INCOMPATIBLE_API');
const failed = harborReducer(initialHarborState, { type: 'sync-failed', error: incompatible });
assert.equal(failed.transport.bootStatus, 'incompatible-api');
});
test('data invariant: an older polling promise cannot replace a newer mutation snapshot', async () => {
let state = receive(initialHarborState, snapshot(1, 'one'));
const poll = deferred();
@@ -0,0 +1,50 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
const root = path.resolve(import.meta.dirname, '../..');
const page = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.tsx'), 'utf8');
const feature = fs.readFileSync(path.join(root, 'src/web/features/instructions/InstructionsFeature.tsx'), 'utf8');
const boundary = fs.readFileSync(path.join(root, 'src/web/features/instructions/index.ts'), 'utf8');
const blocks = fs.readFileSync(path.join(root, 'src/web/features/instructions/instructionBlocks.ts'), 'utf8');
const prometheus = fs.readFileSync(path.join(root, 'src/web/features/instructions/prometheus.ts'), 'utf8');
test('instructions feature is the sole owner behind one public boundary', () => {
assert.equal(fs.existsSync(path.join(root, 'src/web/instructions.js')), false);
assert.equal(fs.existsSync(path.join(root, 'src/web/prometheus.js')), false);
assert.match(boundary, /InstructionsPanel,[\s\S]*InstructionsToggle,[\s\S]*useInstructionsFeature/);
assert.doesNotMatch(boundary, /instructionBlocks|prometheusScrapeConfig|grafanaDashboardJson/);
assert.equal((page.match(/useInstructionsFeature\(/g) || []).length, 1);
assert.equal((page.match(/<InstructionsToggle/g) || []).length, 1);
assert.equal((page.match(/<InstructionsPanel/g) || []).length, 1);
assert.match(page, /from '..\/features\/instructions\/index\.js'/);
assert.doesNotMatch(page, /InstructionStep|InstructionBlock|instructionBlocks|openInstructionId|instructionsPanelRef|instructionsToggleRef|instructionsCloseRef|setInstructionsOpen|client-instruction-block/);
assert.doesNotMatch(feature, /from ['"][^'"]*\/api(?:\/|\.js)|SubscriptionFeature|RoutingFeature|DevicesFeature|DiagnosticsFeature/);
});
test('unconditional controller and conditional panel preserve lifecycle and reset boundaries', () => {
assert.match(page, /const instructionsFeature = useInstructionsFeature\([\s\S]*const diagnosticsAvailable/);
assert.match(page, /\{\(isGateway \|\| \(hasSubscription && subscriptionContentReady\)\) && <InstructionsPanel/);
assert.match(page, /if \(!hasSubscription\) \{[\s\S]*if \(!isGateway\) \{[\s\S]*instructionsFeature\.close\(\)/);
assert.doesNotMatch(page, /if \(!instructionsAvailable\)|instructionsAvailable/);
assert.match(feature, /const \[openInstructionId, setOpenInstructionId\] = useState\(''\)/);
assert.match(feature, /function InstructionBlock[\s\S]*const \[copyFeedback, setCopyFeedback\] = useState/);
assert.match(feature, /clearTimeout\(copyTimer\.current\)[\s\S]*setTimeout\(\(\) => setCopyFeedback\(null\), 800\)/);
assert.match(feature, /requestAnimationFrame\(\(\) => closeRef\.current\?\.focus\(\)\)[\s\S]*panelRef\.current\?\.contains[\s\S]*toggleRef\.current\?\.focus\(\)/);
assert.match(feature, /addEventListener\('pointerdown', closeOutside\)/);
assert.match(feature, /flushSync[\s\S]*prefers-reduced-motion: reduce[\s\S]*document\.startViewTransition\(update\)/);
});
test('private content helpers preserve guide, copy and monitoring contracts', () => {
assert.match(page, /const gatewayAddress = isGateway \? window\.location\.hostname : '127\.0\.0\.1'/);
assert.match(page, /const controlHost = window\.location\.host \|\| `\$\{gatewayAddress\}:3456`/);
assert.match(page, /port: state\?\.clientRuntime\?\.proxyPort \|\| \(isGateway \? 8080 : 8082\)/);
assert.match(blocks, /\.\.\.\(isGateway \? \[\{/);
assert.match(blocks, /id: 'router'[\s\S]*id: 'prometheus'/);
assert.match(blocks, /prometheusScrapeConfig\(controlHost\)[\s\S]*label: 'Grafana dashboard'/);
assert.match(prometheus, /\.\.\/\.\.\/\.\.\/\.\.\/monitoring\/grafana\/harbor-gateway\.json/);
assert.match(prometheus, /scrape_interval: 30s[\s\S]*scrape_timeout: 3s[\s\S]*metrics_path: \/metrics/);
assert.match(feature, /target="_blank" rel="noreferrer"/);
assert.match(feature, /client-copy-label">Скопировать/);
});
+1 -1
View File
@@ -6,7 +6,7 @@ import {
createOperationRegistry,
OPERATION_CONFLICTS,
operationBlocked,
} from '../../src/web/state/operations.js';
} from '../../.test-dist/src/web/state/operations.js';
const deferred = () => {
let resolve;
+9 -16
View File
@@ -3,12 +3,14 @@ import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { readStyleSource } from './style-source.js';
const root = path.resolve(import.meta.dirname, '../..');
const overview = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.jsx'), 'utf8');
const instructions = fs.readFileSync(path.join(root, 'src/web/instructions.js'), 'utf8');
const prometheus = fs.readFileSync(path.join(root, 'src/web/prometheus.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 feature = fs.readFileSync(path.join(root, 'src/web/features/instructions/InstructionsFeature.tsx'), 'utf8');
const instructions = fs.readFileSync(path.join(root, 'src/web/features/instructions/instructionBlocks.ts'), 'utf8');
const prometheus = fs.readFileSync(path.join(root, 'src/web/features/instructions/prometheus.ts'), 'utf8');
const styles = readStyleSource(root);
const dashboard = JSON.parse(fs.readFileSync(path.join(root, 'monitoring/grafana/harbor-gateway.json'), 'utf8'));
test('Gateway info drawer contains copyable Prometheus and Grafana instructions', () => {
@@ -21,21 +23,12 @@ test('Gateway info drawer contains copyable Prometheus and Grafana instructions'
assert.match(prometheus, /monitoring\/grafana\/harbor-gateway\.json/);
assert.match(prometheus, /scrape_interval: 30s[\s\S]*metrics_path: \/metrics/);
assert.match(overview, /const controlHost = window\.location\.host/);
assert.match(overview, /client-copy-label">Скопировать/);
assert.doesNotMatch(overview, /prometheus-toggle|Prometheus<\/span><\/button>/);
assert.match(feature, /client-copy-label">Скопировать/);
assert.doesNotMatch(`${overview}\n${feature}`, /prometheus-toggle|Prometheus<\/span><\/button>/);
assert.match(styles, /\.client-instruction-copy-button \{[\s\S]*width: 104px;[\s\S]*min-width: 104px/);
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-copy-feedback/);
});
test('metrics route reads the current snapshot before static fallback', () => {
const metricsRoute = server.indexOf("requestUrl.pathname === '/metrics'");
const staticFallback = server.indexOf(': serveStatic(req, res)');
assert.ok(metricsRoute >= 0 && metricsRoute < staticFallback);
assert.match(server, /requestUrl\.pathname === '\/metrics'[\s\S]*deviceInventory\.metricsSnapshot\(\)/);
assert.doesNotMatch(server.slice(metricsRoute, staticFallback), /deviceInventory\.refresh\(/);
});
test('Grafana dashboard uses one all-or-one device scope and shows active device speed', () => {
const expressions = dashboard.panels.flatMap((panel) => panel.targets || []).map(({ expr }) => expr).filter(Boolean);
const titles = dashboard.panels.map(({ title }) => title);
+44 -31
View File
@@ -3,10 +3,19 @@ import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { readStyleSource } from './style-source.js';
const root = path.resolve(import.meta.dirname, '../..');
const styles = fs.readFileSync(path.join(root, 'src/web/styles.css'), 'utf8');
const component = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.jsx'), 'utf8');
const diagnostics = fs.readFileSync(path.join(root, 'src/web/components/ConnectivityDiagnosticsPanel.jsx'), 'utf8');
const styles = readStyleSource(root);
const layoutStyles = fs.readFileSync(path.join(root, 'src/web/styles/layout.css'), 'utf8');
const component = 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 routing = fs.readFileSync(path.join(root, 'src/web/features/routing/RoutingFeature.tsx'), 'utf8');
const devices = fs.readFileSync(path.join(root, 'src/web/features/devices/DevicesFeature.tsx'), 'utf8');
const diagnosticsFeature = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/DiagnosticsFeature.tsx'), 'utf8');
const diagnostics = fs.readFileSync(path.join(root, 'src/web/features/diagnostics/ConnectivityDiagnosticsPanel.tsx'), 'utf8');
const instructions = fs.readFileSync(path.join(root, 'src/web/features/instructions/InstructionsFeature.tsx'), 'utf8');
function rule(selector, source = styles) {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -69,8 +78,8 @@ test('server rows scroll without moving the subscription column or showing a scr
});
test('tablet and mobile regions use normal flow with viewport-safe widths', () => {
const responsive = /@media \(max-width: 920px\) \{([\s\S]*?)\n\}\n\n@media \(max-width: 560px\)/.exec(styles)?.[1] || '';
const mobile = /@media \(max-width: 560px\) \{([\s\S]*?)\n\}\n\n@media \(prefers-reduced-motion/.exec(styles)?.[1] || '';
const responsive = /@media \(max-width: 920px\) \{([\s\S]*?)\n\}\n\n@media \(max-width: 560px\)/.exec(layoutStyles)?.[1] || '';
const mobile = /@media \(max-width: 560px\) \{([\s\S]*?)\n\}\s*$/.exec(layoutStyles)?.[1] || '';
assert.match(responsive, /grid-template-columns:\s*minmax\(0, 1fr\)/);
assert.match(responsive, /\.client-power-section,[\s\S]*\.client-form,[\s\S]*grid-column:\s*1/);
@@ -109,21 +118,25 @@ test('secondary menus share one right rail and both drawers open from the right'
const zIndex = (selector) => Number(/z-index:\s*(\d+)/.exec(rule(selector))?.[1]);
assert.match(component, /<nav className="client-secondary-menu" aria-label="Дополнительные меню">/);
assert.match(component, /client-subscription-toggle[\s\S]*client-instructions-toggle[\s\S]*client-devices-toggle/);
assert.match(component, /aria-controls="client-subscription-drawer"/);
assert.match(component, /client-instructions-toggle[\s\S]*client-local-rules-toggle/);
assert.match(component, /client-instructions-toggle[\s\S]*client-devices-toggle[\s\S]*client-diagnostics-toggle[\s\S]*client-local-rules-toggle/);
assert.match(component, /client-diagnostics-toggle/);
assert.match(component, /<SubscriptionToggle[\s\S]*<InstructionsToggle[\s\S]*<DevicesToggle/);
assert.match(subscription, /aria-controls="client-subscription-drawer"/);
assert.match(component, /<InstructionsToggle[\s\S]*<RoutingToggle/);
assert.match(component, /<InstructionsToggle[\s\S]*<DevicesToggle[\s\S]*<DiagnosticsToggle[\s\S]*<RoutingToggle/);
assert.match(diagnosticsFeature, /client-diagnostics-toggle/);
assert.match(component, /<ConnectivityDiagnosticsPanel/);
assert.doesNotMatch(component, /\{isGateway && <button[\s\S]{0,120}diagnosticsToggleRef/);
assert.doesNotMatch(component, /diagnosticsToggleRef|diagnosticsPanelRef|diagnosticsCloseRef/);
assert.match(component, /<ConnectivityDiagnosticsPanel[\s\S]*isGateway=\{isGateway\}/);
assert.match(component, /Локальные правила недоступны: сейчас работают правила Gateway/);
assert.match(routing, /Локальные правила недоступны: сейчас работают правила Gateway/);
assert.match(rule('.client-secondary-menu'), /right:\s*max\(14px, env\(safe-area-inset-right\)\)/);
assert.match(rule('.client-secondary-menu'), /display:\s*grid/);
assert.match(disabledRulesLabel, /opacity:\s*0/);
assert.match(disabledRulesLabel, /filter:\s*blur\(5px\)/);
assert.match(styles, /\.client-local-rules-toggle:disabled:hover span\s*\{[\s\S]*opacity:\s*1/);
assert.match(component, /client-rail-info-ring[\s\S]*client-rail-device-primary[\s\S]*client-rail-device-secondary[\s\S]*client-rail-device-link[\s\S]*client-rail-diagnostics-base[\s\S]*client-rail-diagnostics-pulse[\s\S]*client-rail-rule-knob is-top[\s\S]*client-rail-rule-knob is-bottom/);
assert.match(instructions, /client-rail-info-ring/);
assert.match(component, /<InstructionsToggle[\s\S]*<DevicesToggle[\s\S]*<DiagnosticsToggle[\s\S]*<RoutingToggle/);
assert.match(diagnosticsFeature, /client-rail-diagnostics-base[\s\S]*client-rail-diagnostics-pulse/);
assert.match(devices, /client-rail-device-primary[\s\S]*client-rail-device-secondary[\s\S]*client-rail-device-link/);
assert.match(routing, /client-rail-rule-knob is-top[\s\S]*client-rail-rule-knob is-bottom/);
assert.match(styles, /client-rail-info-refill[\s\S]*client-rail-device-left[\s\S]*client-rail-device-right[\s\S]*client-rail-diagnostics-pulse[\s\S]*client-rail-rule-top[\s\S]*client-rail-rule-bottom/);
assert.match(styles, /\.client-rail-diagnostics-pulse \{[\s\S]*stroke-dasharray: 0\.16 0\.84/);
assert.match(styles, /@keyframes client-rail-diagnostics-pulse[\s\S]*stroke-dashoffset: 1[\s\S]*stroke-dashoffset: 0/);
@@ -141,10 +154,10 @@ test('secondary menus share one right rail and both drawers open from the right'
);
assert.match(rule('.client-instructions'), /width:\s*min\(470px, 100vw\)/);
assert.match(rule('.client-local-rules'), /width:\s*min\(480px, 100vw\)/);
assert.match(component, /className={`client-drawer client-instructions/);
assert.match(component, /className={`client-drawer client-local-rules/);
assert.match(component, /client-drawer client-subscription-drawer/);
assert.match(component, /setSubscriptionOpen\(false\)[\s\S]*setDevicesOpen\(false\)[\s\S]*setDiagnosticsOpen\(false\)/);
assert.match(instructions, /className={`client-drawer client-instructions/);
assert.match(routing, /className={`client-drawer client-local-rules/);
assert.match(subscription, /client-drawer client-subscription-drawer/);
assert.match(component, /subscriptionFeature\.close\(\)[\s\S]*devicesFeature\.close\(\)[\s\S]*diagnosticsFeature\.close\(\)[\s\S]*instructionsFeature\.toggle\(\)/);
});
test('connectivity diagnostics render stable compact tables before the first run', () => {
@@ -160,13 +173,13 @@ test('connectivity diagnostics render stable compact tables before the first run
assert.match(diagnostics, /client-diagnostics-refresh/);
assert.match(diagnostics, /isGateway \? 'Gateway' : 'Connect'/);
assert.doesNotMatch(diagnostics, /Проверить ещё раз|client-diagnostics-empty|client-diagnostics-run/);
assert.match(diagnostics, /\{error && <div className="client-diagnostics-feedback"/);
assert.match(diagnostics, /\{Boolean\(error\) && <div className="client-diagnostics-feedback"/);
assert.doesNotMatch(diagnostics, /SUMMARY_COPY|client-diagnostics-summary|client-diagnostics-time|checkedAt|Прямой маршрут/);
assert.doesNotMatch(diagnostics, /PathDetails|client-diagnostics-details|Технические детали/);
assert.doesNotMatch(rule('.client-diagnostics-feedback'), /min-height:/);
assert.match(rule('.client-diagnostics-table'), /table-layout:\s*fixed/);
assert.match(diagnostics, /for \(const target of targets\)/);
assert.match(diagnostics, /api\.diagnostics\.connectivity\(customServices, target\)/);
assert.match(diagnostics, /runConnectivityDiagnostics\(customServices, target\)/);
assert.match(diagnostics, /const target = `ip:\$\{source\.id\}`;[\s\S]*activeTarget === target/);
assert.match(diagnostics, /activeTarget === `site:\$\{site\.id\}`/);
assert.match(diagnostics, /data-diagnostic-target=\{target\}/);
@@ -180,13 +193,13 @@ test('connectivity diagnostics render stable compact tables before the first run
test('duration and Gateway access keep stable geometry without tabs', () => {
assert.match(rule('.client-state-detail'), /min-height:\s*44px/);
assert.match(rule('.client-duration-toggle'), /min-height:\s*44px/);
assert.match(component, /client-duration-word-row is-calendar/);
assert.match(component, /client-duration-word-row is-clock/);
assert.match(connection, /client-duration-word-row is-calendar/);
assert.match(connection, /client-duration-word-row is-clock/);
assert.match(rule('.client-duration-toggle > .client-tooltip'), /right:\s*calc\(100% \+ 12px\)/);
assert.match(component, /\['gateway', 'GATEWAY'\]/);
assert.match(component, /\['socks5', 'SOCKS5'\]/);
assert.match(component, /\['http', 'HTTP'\]/);
assert.doesNotMatch(component, /client-access-tabs|role="tab"|role="tabpanel"/);
assert.match(connection, /\['gateway', 'GATEWAY'\]/);
assert.match(connection, /\['socks5', 'SOCKS5'\]/);
assert.match(connection, /\['http', 'HTTP'\]/);
assert.doesNotMatch(`${component}\n${connection}`, /client-access-tabs|role="tab"|role="tabpanel"/);
assert.match(rule('.client-proxies.is-gateway'), /width:\s*270px/);
});
@@ -218,12 +231,12 @@ test('tooltips stay opaque, above adjacent content, and do not stick after point
});
test('subscription validation waits for the provider and keeps diagnostics below errors', () => {
assert.match(component, /api\.subscription\.validate\(normalizedSubscriptionUrl/);
assert.match(component, /status: 'checking'/);
assert.match(component, /status: 'valid'/);
assert.match(component, /message: ERROR_DEFINITIONS\.SUBSCRIPTION_INVALID\.message/);
assert.match(component, /subscriptionValidationStatus === 'checking' \? '…' : '×'/);
assert.match(component, /if \(error\?\.context === 'subscription'\) onDismissError\(\)/);
assert.match(subscription, /validateSubscription\(normalizedUrl/);
assert.match(subscription, /status: 'checking'/);
assert.match(subscription, /status: 'valid'/);
assert.match(subscription, /message: ERROR_DEFINITIONS\.SUBSCRIPTION_INVALID\.message/);
assert.match(subscription, /validationStatus === 'checking' \? '…' : '×'/);
assert.match(subscription, /if \(error\?\.context === 'subscription'\) onDismissError\(\)/);
assert.match(component, /error\.retry[\s\S]*error\.correlationId/);
assert.match(rule('.client-inline-error.is-subscription small'), /flex-basis:\s*100%/);
assert.match(rule('.client-inline-error.is-subscription small'), /opacity:\s*0\.45/);
+44
View File
@@ -0,0 +1,44 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
const root = path.resolve(import.meta.dirname, '../..');
const page = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.tsx'), 'utf8');
const feature = fs.readFileSync(path.join(root, 'src/web/features/routing/RoutingFeature.tsx'), 'utf8');
const boundary = fs.readFileSync(path.join(root, 'src/web/features/routing/index.ts'), 'utf8');
test('routing feature is the sole owner at the four existing composition positions', () => {
assert.match(boundary, /RoutingDiscardDialog,[\s\S]*RoutingPanel,[\s\S]*RoutingPendingStatus,[\s\S]*RoutingToggle,[\s\S]*useRoutingFeature/);
assert.equal((page.match(/useRoutingFeature\(/g) || []).length, 1);
assert.equal((page.match(/<RoutingToggle/g) || []).length, 1);
assert.equal((page.match(/<RoutingPendingStatus/g) || []).length, 1);
assert.equal((page.match(/<RoutingPanel/g) || []).length, 1);
assert.equal((page.match(/<RoutingDiscardDialog/g) || []).length, 1);
assert.doesNotMatch(page, /ROUTE_RULE_OPTIONS|function RuleTypePicker|function LocalRulesPanel|localRulesBaselineRef|setLocalRulesDraft|id="discard-local-rules"|client-local-rules-toggle/);
assert.doesNotMatch(feature, /from ['"][^'"]*\/api\/|ConnectionPanel|SubscriptionFeature|ServerPicker|DevicesPanel|DiagnosticsPanel/);
});
test('routing controller preserves snapshot drafts, live status and guarded close/save semantics', () => {
assert.match(feature, /const savedRules = route\?\.localRules \|\| \[\]/);
assert.match(feature, /baselineRef\.current = JSON\.stringify\(savedRules\.map/);
assert.match(feature, /setRules\(savedRules\.map\(createLocalRuleDraft\)\)/);
assert.match(feature, /setRevision\(route\?\.localRulesRevision \|\| 0\)/);
assert.match(feature, /if \(dirty\) \{[\s\S]*setConfirmingClose\(true\);[\s\S]*return false/);
assert.match(feature, /const result = routingSaveState\(await onSave\(values, revision\)\)/);
assert.match(feature, /if \(!result\) return;[\s\S]*baselineRef\.current = JSON\.stringify\(values\);[\s\S]*setRevision\(result\.localRulesRevision\)/);
assert.match(feature, /if \(!connected \|\| !result\.localRulesPendingRestart\) setIsOpen\(false\)/);
assert.match(feature, /Number\.isSafeInteger\(localRulesRevision\)[\s\S]*localRulesRevision as number\) < 0[\s\S]*typeof localRulesPendingRestart !== 'boolean'/);
});
test('routing lifecycle and Page orchestration keep the existing guards and blocking scopes', () => {
assert.match(feature, /keyboardEvent\.key !== 'Escape' \|\| keyboardEvent\.defaultPrevented/);
assert.match(feature, /addEventListener\('beforeunload', warnBeforeUnload\)/);
assert.match(feature, /matchMedia\('\(prefers-reduced-motion: reduce\)'\)[\s\S]*removing: true/);
assert.match(feature, /document\.startViewTransition\(update\)/);
assert.match(feature, /operationBlocked\(operations, 'routeRules'\) \|\| rules\.some/);
assert.match(page, /routingFeature\.isOpen && !routingFeature\.requestClose\(\)/);
assert.match(page, /function openRouting\(\) \{[\s\S]*subscriptionFeature\.close\(\)[\s\S]*instructionsFeature\.close\(\)[\s\S]*devicesFeature\.close\(\)[\s\S]*diagnosticsFeature\.close\(\)[\s\S]*routingFeature\.open\(\)/);
assert.match(page, /routingSlot=\{<RoutingPendingStatus[\s\S]*blocked=\{connectionBlocked\}/);
assert.match(page, /<RoutingPanel[\s\S]*InlineError[\s\S]*InlineProgress/);
});
+57 -36
View File
@@ -3,29 +3,35 @@ import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { readStyleSource } from './style-source.js';
const root = path.resolve(import.meta.dirname, '../..');
const component = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.jsx'), 'utf8');
const popup = fs.readFileSync(path.join(root, 'src/web/components/ConfirmationPopup.jsx'), 'utf8');
const styles = fs.readFileSync(path.join(root, 'src/web/styles.css'), 'utf8');
const component = 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 routing = fs.readFileSync(path.join(root, 'src/web/features/routing/RoutingFeature.tsx'), 'utf8');
const instructions = fs.readFileSync(path.join(root, 'src/web/features/instructions/InstructionsFeature.tsx'), 'utf8');
const dialog = fs.readFileSync(path.join(root, 'src/web/ui/ConfirmationDialog.tsx'), 'utf8');
const styles = readStyleSource(root);
test('rule editor add latency stays constant and dirty exits are guarded', () => {
const rowRule = /\.client-local-rule \{([\s\S]*?)\n\}/.exec(styles)?.[1] || '';
assert.doesNotMatch(rowRule, /--rule-index|calc\(/);
assert.match(component, /addEventListener\('beforeunload', warnBeforeUnload\)/);
assert.match(component, /requestCloseLocalRules\(\)/);
assert.match(component, /localRulesPendingRestart/);
assert.match(component, /activeLocalRules/);
assert.match(component, /localRulesRevision/);
assert.match(component, /Не сохранено/);
assert.match(component, /if \(!runtimeActive\) return \['saved', 'Сохранено'\]/);
assert.match(component, /Ждёт перезапуска/);
assert.match(component, /Перезапустить VPN/);
assert.match(component, /const localRulesPendingRestart = connected && state\?\.route\?\.localRulesPendingRestart === true/);
assert.match(component, /if \(!connected \|\| !result\.state\.route\.localRulesPendingRestart\) setLocalRulesOpen\(false\)/);
assert.match(component, /client-deletable-row/);
assert.match(component, /className="client-delete-strike"[\s\S]*onAnimationEnd/);
assert.doesNotMatch(component, /client-rule-delete-cross/);
assert.match(component, /className="client-local-rule-delete"/);
assert.match(routing, /addEventListener\('beforeunload', warnBeforeUnload\)/);
assert.match(routing, /requestClose\(\)/);
assert.match(routing, /pendingRestart/);
assert.match(routing, /activeLocalRules/);
assert.match(routing, /localRulesRevision/);
assert.match(routing, /Не сохранено/);
assert.match(routing, /if \(!runtimeActive\) return \['saved', 'Сохранено'\]/);
assert.match(routing, /Ждёт перезапуска/);
assert.match(routing, /Перезапустить VPN/);
assert.match(routing, /const pendingRestart = connected && route\?\.localRulesPendingRestart === true/);
assert.match(routing, /if \(!connected \|\| !result\.localRulesPendingRestart\) setIsOpen\(false\)/);
assert.match(routing, /client-deletable-row/);
assert.match(routing, /className="client-delete-strike"[\s\S]*onAnimationEnd/);
assert.doesNotMatch(routing, /client-rule-delete-cross/);
assert.match(routing, /className="client-local-rule-delete"/);
assert.match(styles, /\.client-deletable-row\.is-removing > \.client-delete-strike[\s\S]*client-delete-strike/);
assert.match(styles, /\.client-delete-strike \{[\s\S]*z-index: 100[\s\S]*background: transparent/);
assert.match(styles, /\.client-deletable-row\.is-removing > :not\(\.client-delete-strike\)[\s\S]*z-index: 0/);
@@ -40,37 +46,52 @@ test('rule editor add latency stays constant and dirty exits are guarded', () =>
});
test('critical confirmations share one accessible blocking popup', () => {
assert.match(component, /id="stop-connection"/);
assert.match(component, /id="discard-local-rules"/);
assert.match(component, /id="delete-subscription"/);
assert.equal(fs.existsSync(path.join(root, 'src/web/components/ConfirmationPopup.jsx')), false);
assert.equal(
(component.match(/<ConfirmationDialog/g) || []).length
+ (connection.match(/<ConfirmationDialog/g) || []).length
+ (subscription.match(/<ConfirmationDialog/g) || []).length
+ (routing.match(/<ConfirmationDialog/g) || []).length,
3,
);
assert.match(connection, /id="stop-connection"/);
assert.match(routing, /id="discard-local-rules"/);
assert.match(subscription, /id="delete-subscription"/);
assert.doesNotMatch(component, /client-local-rules-discard|client-delete-confirmation/);
assert.match(popup, /role="alertdialog"/);
assert.match(popup, /aria-modal="true"/);
assert.match(popup, /querySelectorAll\(FOCUSABLE\)/);
assert.match(popup, /element\.inert = true/);
assert.match(popup, /requestAnimationFrame\(\(\) => cancelRef\.current\?\.focus\(\)\)/);
assert.match(dialog, /role="alertdialog"/);
assert.match(dialog, /aria-modal="true"/);
assert.match(dialog, /aria-hidden=\{!open\}[\s\S]*inert=\{!open \? true : undefined\}/);
assert.match(dialog, /querySelectorAll<HTMLElement>\(FOCUSABLE\)/);
assert.match(dialog, /element\.inert = true/);
assert.match(dialog, /requestAnimationFrame\(\(\) => cancelRef\.current\?\.focus\(\)\)/);
assert.match(dialog, /event\.key === 'Escape' && !busyRef\.current/);
assert.match(dialog, /event\.target === event\.currentTarget && !busy/);
assert.match(dialog, /background\.forEach\(\(\{ element, inert \}\) => \{ element\.inert = inert; \}\)/);
assert.match(dialog, /document\.body\.style\.overflow = previousOverflow/);
assert.match(dialog, /requestAnimationFrame\(\(\) => previousFocus\?\.focus\?\.\(\)\)/);
assert.match(dialog, /document\.querySelector\('\.app\.client-app'\) \|\| document\.body/);
assert.match(styles, /\.client-confirmation-popup\.is-open[\s\S]*backdrop-filter: blur\(18px\)/);
});
test('power click and native Enter cannot stop VPN without a separate confirmation', () => {
assert.match(component, /className="client-power"[\s\S]*type="button"[\s\S]*onClick=\{toggleConnection\}/);
assert.match(component, /if \(action\?\.type === 'stop'\) \{[\s\S]*setConfirmingStop\(true\);[\s\S]*return;[\s\S]*\}/);
assert.doesNotMatch(component, /if \(action\?\.type === 'stop'\) return onStop\(\)/);
assert.match(component, /id="stop-connection"[\s\S]*cancelLabel="Оставить включённым"[\s\S]*confirmLabel="Отключить VPN"[\s\S]*onConfirm=\{stopConnection\}/);
assert.match(connection, /className="client-power"[\s\S]*type="button"[\s\S]*onClick=\{toggleConnection\}/);
assert.match(connection, /if \(action\?\.type === 'stop'\) \{[\s\S]*setConfirmingStop\(true\);[\s\S]*return;[\s\S]*\}/);
assert.doesNotMatch(connection, /if \(action\?\.type === 'stop'\) return onStop\(\)/);
assert.match(connection, /id="stop-connection"[\s\S]*cancelLabel="Оставить включённым"[\s\S]*confirmLabel="Отключить VPN"[\s\S]*onConfirm=\{stopConnection\}/);
});
test('copy feedback, drawers and Gateway access actions expose complete semantics', () => {
assert.match(component, /className="client-live-region" role="status" aria-live="polite" aria-atomic="true"/);
assert.match(component, /Не удалось скопировать/);
assert.match(component, /Скопировано/);
assert.match(component, /client-copy-feedback[^\n]*\{copyFeedback\.failed \? 'Ошибка' : 'Скопировано'\}/);
assert.match(connection, /client-copy-feedback[^\n]*\{copyFeedback\.failed \? 'Ошибка' : 'Скопировано'\}/);
assert.doesNotMatch(component, /ГОТОВО/);
assert.doesNotMatch(component, />Error<|>Copied</);
assert.match(component, /aria-label="Закрыть инструкции"/);
assert.match(component, /aria-label="Закрыть локальные правила"/);
assert.match(component, /instructionsCloseRef\.current\?\.focus\(\)/);
assert.match(component, /localRulesCloseRef\.current\?\.focus\(\)/);
assert.match(component, /aria-label={`Скопировать \$\{label\}: \$\{kind === 'gateway' \? gatewayAddress : proxyUrls\[kind\]\}`}/);
assert.match(instructions, /aria-label="Закрыть инструкции"/);
assert.match(routing, /aria-label="Закрыть локальные правила"/);
assert.match(instructions, /closeRef\.current\?\.focus\(\)/);
assert.match(routing, /closeRef\.current\?\.focus\(\)/);
assert.match(connection, /aria-label={`Скопировать \$\{label\}: \$\{kind === 'gateway' \? gatewayAddress : proxyUrls\[kind\]\}`}/);
assert.doesNotMatch(component, /client-access-tabs|role="tab"|role="tabpanel"/);
assert.match(styles, /\.client-drawer-close \{[\s\S]*width: 44px;[\s\S]*height: 44px/);
assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-copy-button \{[\s\S]*min-height: 44px/);
+40 -4
View File
@@ -3,17 +3,32 @@ import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { readStyleSource } from './style-source.js';
import {
autoServer,
filterServers,
groupServers,
parseServerPingResults,
SERVER_RESULT_WINDOW,
} from '../../src/web/utils/serverPicker.js';
} from '../../.test-dist/src/web/features/servers/serverPickerModel.js';
const root = path.resolve(import.meta.dirname, '../..');
const picker = fs.readFileSync(path.join(root, 'src/web/components/ServerPicker.jsx'), 'utf8');
const overview = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.jsx'), 'utf8');
const styles = fs.readFileSync(path.join(root, 'src/web/styles.css'), 'utf8');
const picker = fs.readFileSync(path.join(root, 'src/web/features/servers/ServerPicker.tsx'), 'utf8');
const boundary = fs.readFileSync(path.join(root, 'src/web/features/servers/index.ts'), 'utf8');
const overview = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.tsx'), 'utf8');
const styles = readStyleSource(root);
test('server picker has one public feature owner without legacy shims', () => {
assert.equal(boundary.trim(), "export { ServerPicker } from './ServerPicker.js';");
assert.equal(fs.existsSync(path.join(root, 'src/web/components/ServerPicker.jsx')), false);
assert.equal(fs.existsSync(path.join(root, 'src/web/utils/serverPicker.js')), false);
assert.match(overview, /import \{ ServerPicker \} from '\.\.\/features\/servers\/index\.js'/);
assert.equal((overview.match(/<ServerPicker/g) || []).length, 1);
assert.doesNotMatch(picker, /from ['"][^'"]*\/api\/|\bapi\./);
assert.match(overview, /const selectedServerId = pendingServerId \|\| state\?\.selection\?\.desiredServerId \|\| ''/);
assert.match(overview, /setPendingServerId\(serverId\);[\s\S]*if \(connected && serverId\) onApply\(serverId\)/);
});
const fixtures = (count) => Array.from({ length: count }, (_, index) => ({
id: `srv-${String(count - index).padStart(3, '0')}`,
@@ -37,6 +52,27 @@ test('server picker handles 1, 30 and 300 stable-ID servers with duplicate label
assert.equal(SERVER_RESULT_WINDOW, 60);
});
test('server picker validates unknown ping payloads before publishing results', () => {
const result = { id: 'srv-1', ok: true, latency: 12, checkedAt: '2026-08-08T12:00:00.000Z', extra: 'kept' };
const failed = { id: 'srv-2', ok: false, latency: null, error: 'timeout', checkedAt: '2026-08-08T12:00:01.000Z', extra: 'kept' };
assert.deepEqual(parseServerPingResults({}), []);
assert.equal(parseServerPingResults({ results: [result] })[0], result);
assert.equal(parseServerPingResults({ results: [failed] })[0], failed);
for (const payload of [
null,
[],
{ results: null },
{ results: [{}] },
{ results: [{ id: '', ok: true }] },
{ results: [{ id: 'srv-1', ok: 'yes' }] },
{ results: [{ id: 'srv-1', latency: -1 }] },
{ results: [{ id: 'srv-1', checkedAt: 123 }] },
]) {
assert.throws(() => parseServerPingResults(payload), TypeError);
}
assert.match(picker, /parseServerPingResults\(await pingServers\(ids\)\)/);
});
test('server picker checks health only on manual refresh and bounds the result window', () => {
assert.doesNotMatch(overview, /pingAll|servers\.ping/);
assert.doesNotMatch(picker, /checkVisible\(\);/);
+300
View File
@@ -0,0 +1,300 @@
import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import {
analyzeSelectorList,
createStyleWitnesses,
createStyleLedger,
readStyleSource,
readStyleWitnesses,
selectorsWithoutWitness,
styleLeafPaths,
variableReferences,
} from './style-source.js';
const root = path.resolve(import.meta.dirname, '../..');
const stylesRoot = path.join(root, 'src/web/styles');
const indexPath = path.join(stylesRoot, 'index.css');
const index = fs.readFileSync(indexPath, 'utf8');
const expectedImports = [
'./tokens.css',
'./base.css',
'./features/devices.css',
'./features/routing.css',
'./features/instructions.css',
'./features/connection.css',
'./features/subscription.css',
'./features/servers.css',
'./primitives.css',
'./features/diagnostics.css',
'./layout.css',
'./themes.css',
];
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const acceptedLedger = {
counts: {
cascadeEdges: 742,
customProperties: 31,
declarations: 2793,
important: 0,
keyframes: 48,
media: 8,
rules: 824,
variableReferences: 323,
},
hashes: {
cascadeEdges: '9e85cea58c179358dac8767ed987433e3e1f4974324999dba1803a49cbd6f4f6',
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
declarations: '2e733c57dc0d89369e46a0eb44c1d67d09d1d40c28f7c36813c5fec607539be7',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: '1565bf06e07fd7cbf601d24846bb3e1059d06720ace1544f2ac7f6ecf36cab47',
keyframes: '853c54c05759d9db27bea891254913e1651b1f601059ad9e3e2baa2c55ef1b2b',
ruleDeclarationSequences: '4a741c5ee09f9cd694e058609b4362cf796c97009d83886c5c8b4b4fa4d73b52',
selectors: '2da3202f84a79a8cf68e0cf262d36aa4b7756268133135469c086317dcbb82ff',
variableReferences: 'e8dc6951d717dbd6a9aa51a495798a56568b00ce0bbd7fb6edc7c758a81ddb6e',
witnesses: '68723a5909eb1a0972a70cde2be75e63e90abda61babe98db713414bda6a0c23',
},
};
test('public stylesheet exposes exactly twelve flat semantic owners', () => {
const imports = Array.from(index.matchAll(/^@import ['"](\.\/[^'"]+\.css)['"];$/gm), ([, file]) => file);
assert.deepEqual(imports, expectedImports);
assert.equal(index, `${expectedImports.map((file) => `@import '${file}';`).join('\n')}\n`);
assert.equal(new Set(imports).size, imports.length);
assert.equal(fs.existsSync(path.join(root, 'src/web/styles.css')), false);
const actualCssFiles = fs.readdirSync(stylesRoot, { recursive: true, withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith('.css'))
.map((entry) => path.relative(stylesRoot, path.join(entry.parentPath, entry.name)).replaceAll('\\', '/'))
.sort();
assert.deepEqual(actualCssFiles, ['index.css', ...expectedImports.map((file) => file.slice(2))].sort());
for (const leaf of styleLeafPaths(root)) {
const source = fs.readFileSync(leaf, 'utf8');
assert.doesNotMatch(source, /@import|@layer/, path.relative(root, leaf));
}
});
test('tokens, shared primitives, and feature styles have one explicit owner', () => {
const sources = Object.fromEntries(expectedImports.map((relativePath) => [
relativePath,
fs.readFileSync(path.resolve(stylesRoot, relativePath), 'utf8'),
]));
const paletteProperties = [
'--client-bg', '--client-panel', '--client-control', '--client-border', '--client-text', '--client-muted',
'--harbor-word', '--harbor-connect', '--harbor-gateway', '--client-accent', '--client-accent-soft',
];
for (const property of paletteProperties) {
const owners = Object.entries(sources)
.filter(([, source]) => new RegExp(`^\\s*${property.replaceAll('-', '\\-')}:`, 'm').test(source))
.map(([owner]) => owner);
assert.deepEqual(owners, ['./tokens.css', './themes.css'], property);
}
for (const [owner, source] of Object.entries(sources)) {
assert.doesNotMatch(source, /\[data-theme\]/, owner);
}
for (const keyframe of [
'client-local-rule-enter', 'client-local-rule-leave', 'client-delete-strike',
'client-delete-content-dim', 'client-spin', 'client-copy-fade',
]) {
const owners = Object.entries(sources)
.filter(([, source]) => new RegExp(`@keyframes ${keyframe}\\b`).test(source))
.map(([owner]) => owner);
assert.deepEqual(owners, ['./primitives.css'], keyframe);
}
});
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
const witnesses = readStyleWitnesses(root);
assert.equal(witnesses.length, 686);
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
assert.deepEqual(ledger.counts, acceptedLedger.counts);
assert.deepEqual(ledger.hashes, acceptedLedger.hashes);
});
test('every live production selector has an expanded DOM witness', () => {
const unmatched = selectorsWithoutWitness(readStyleSource(root), readStyleWitnesses(root));
assert.deepEqual(unmatched, [
'.client-diagnostics-section-title button',
'.client-diagnostics-section-title button:disabled',
'.client-diagnostics-section-title button:focus-visible',
'.client-server small',
]);
});
test('JSX witness expansion follows cross-file components, ReactNode slots, portals, and imperative classes', () => {
const fixtureWitnesses = createStyleWitnesses([
{
file: '/fixture/Child.tsx',
source: 'export function Child({ slot }) { return <section className="child">{slot}<h2 className="title" /></section>; }',
},
{
file: '/fixture/App.tsx',
source: 'export function App() { return <main className="scope"><Child slot={<strong className="slot" />} /></main>; }',
},
]);
const title = fixtureWitnesses.find((witness) => witness.classes.includes('title'));
const slot = fixtureWitnesses.find((witness) => witness.classes.includes('slot'));
assert.deepEqual(title?.ancestorClasses, ['child', 'scope']);
assert.deepEqual(slot?.ancestorClasses, ['child', 'scope']);
const localBindingWitnesses = createStyleWitnesses([{
file: '/fixture/App.tsx',
source: `export function App() {
const content = <h2 className="local-title" />;
return <main className="local-scope"><section className="local-wrapper">{content}</section></main>;
}`,
}]);
assert.deepEqual(
localBindingWitnesses.find((witness) => witness.classes.includes('local-title'))?.ancestorClasses,
['local-scope', 'local-wrapper'],
);
assert.throws(() => createStyleWitnesses([{
file: '/fixture/App.tsx',
source: 'export function App() { return <Missing />; }',
}]), /Unresolved JSX witness component/);
assert.throws(() => createStyleWitnesses([{
file: '/fixture/App.tsx',
source: 'export function App() { const props = {}; return <Child {...props} />; } function Child() { return <div />; }',
}]), /Unsupported spread props/);
const witnesses = readStyleWitnesses(root);
const app = witnesses.find((witness) => witness.classes.includes('app') && witness.classes.includes('client-app'));
assert.ok(app);
const appThemeEdge = createStyleLedger(
'.app.client-app { --client-bg: white; } '
+ '@media (prefers-color-scheme: dark) { .app.client-app { --client-bg: black; } }',
{ witnesses },
);
assert.equal(appThemeEdge.counts.cascadeEdges, 1);
const marker = witnesses.find((witness) => witness.classes.includes('client-diagnostics-active-marker'));
assert.ok(marker?.classes.includes('is-visible'));
assert.ok(marker?.classes.includes('is-moving'));
const confirmations = witnesses.filter((witness) => witness.classes.includes('client-confirmation-popup'));
assert.ok(confirmations.some((witness) => witness.ancestorClasses.length === 0));
assert.ok(confirmations.some((witness) => witness.ancestorClasses.includes('app')));
const trafficTooltips = witnesses.filter((witness) => witness.classes.includes('client-device-traffic-point-tooltip'));
assert.ok(trafficTooltips.every((witness) => witness.ancestorClasses.length === 0));
});
test('JSX witnesses keep real multi-class collisions and exclude impossible element collisions', () => {
const witnesses = readStyleWitnesses(root);
const collision = createStyleLedger(
'.client-copy-button { color: red; } .client-instruction-copy-button { color: blue; }',
{ witnesses },
);
assert.equal(collision.counts.cascadeEdges, 1);
const impossible = createStyleLedger(
'.client-copy-button { color: red; } .client-power { color: blue; }',
{ witnesses },
);
assert.equal(impossible.counts.cascadeEdges, 0);
const conservativeSibling = createStyleLedger(
'.client-subscription-drawer .client-servers { margin-inline: auto; } '
+ '.client-server-group + .client-server-group { margin-top: 8px; }',
{ witnesses },
);
assert.equal(conservativeSibling.counts.cascadeEdges, 1);
});
test('selector proof uses the observed level-four grammar and exact specificity', () => {
const fixtures = [
['#root', [{ a: 1, b: 0, c: 0 }]],
['.client-shell:has(.harbor-brand.is-gateway-active)', [{ a: 0, b: 3, c: 0 }]],
['.client-instructions-toggle:not(.client-devices-toggle):not(.client-diagnostics-toggle)', [{ a: 0, b: 3, c: 0 }]],
[".client-power[aria-checked='true']::before", [{ a: 0, b: 2, c: 1 }]],
['.client-instruction-block:nth-child(n)', [{ a: 0, b: 2, c: 0 }]],
['.client-confirmation-actions button:hover:not(:disabled), #root', [
{ a: 0, b: 3, c: 1 },
{ a: 1, b: 0, c: 0 },
]],
];
for (const [selector, expected] of fixtures) {
assert.deepEqual(analyzeSelectorList(selector).map((entry) => entry.specificity), expected, selector);
}
assert.throws(() => analyzeSelectorList('.broken:has('), /Unsupported CSS selector grammar/);
assert.throws(() => analyzeSelectorList('& .proof'), /Unsupported CSS selector node nesting/);
assert.throws(() => analyzeSelectorList('.proof:is(.active)'), /Unsupported CSS pseudo :is/);
});
test('variable proof preserves nested and fallback references and fails closed', () => {
assert.deepEqual(variableReferences('var(--outer, color-mix(in srgb, var(--inner, red), white))'), [
{ name: '--outer', fallback: 'color-mix(in srgb, var(--inner, red), white)' },
{ name: '--inner', fallback: 'red' },
]);
assert.throws(() => variableReferences('var(color, red)'), /Invalid var\(\) name/);
assert.throws(() => variableReferences('var(--unfinished'), /Unterminated var\(\) reference/);
});
test('ledger mutations expose loss, order changes, duplicate selectors, keyframes, variables, and cascade orientation', () => {
const declaration = createStyleLedger('.proof { color: red; background: black; }');
const removed = createStyleLedger('.proof { color: red; }');
const reordered = createStyleLedger('.proof { background: black; color: red; }');
assert.notEqual(removed.hashes.declarations, declaration.hashes.declarations);
assert.equal(reordered.hashes.declarations, declaration.hashes.declarations);
assert.notEqual(reordered.hashes.ruleDeclarationSequences, declaration.hashes.ruleDeclarationSequences);
const duplicate = createStyleLedger('.proof { color: red; } .other { color: green; } .proof { color: blue; }');
const duplicateReordered = createStyleLedger('.proof { color: blue; } .other { color: green; } .proof { color: red; }');
assert.equal(duplicateReordered.hashes.declarations, duplicate.hashes.declarations);
assert.notEqual(duplicateReordered.hashes.duplicateSelectors, duplicate.hashes.duplicateSelectors);
const keyframe = createStyleLedger('@keyframes pulse { from { opacity: 0; } to { opacity: 1; } }');
const changedKeyframe = createStyleLedger('@keyframes pulse { from { opacity: 0.1; } to { opacity: 1; } }');
assert.notEqual(changedKeyframe.hashes.keyframes, keyframe.hashes.keyframes);
const variable = createStyleLedger('.proof { color: var(--outer, var(--inner, red)); }');
const changedVariable = createStyleLedger('.proof { color: var(--outer, var(--fallback, red)); }');
assert.notEqual(changedVariable.hashes.variableReferences, variable.hashes.variableReferences);
const edge = createStyleLedger('.alpha { color: red; } .beta { color: blue; }');
const reversedEdge = createStyleLedger('.beta { color: blue; } .alpha { color: red; }');
assert.equal(edge.counts.cascadeEdges, 1);
assert.equal(reversedEdge.counts.cascadeEdges, 1);
assert.notEqual(reversedEdge.hashes.cascadeEdges, edge.hashes.cascadeEdges);
const independent = createStyleLedger('.alpha { color: red; } .beta { background: blue; }');
const independentReordered = createStyleLedger('.beta { background: blue; } .alpha { color: red; }');
assert.equal(independent.counts.cascadeEdges, 0);
assert.deepEqual(independentReordered.hashes, independent.hashes);
for (const shorthand of [
'.x { place-items: center; } .y { align-items: start; }',
'.x { place-content: center; } .y { justify-content: start; }',
'.x { grid-area: 1 / 1; } .y { grid-row: 2; }',
]) {
assert.equal(createStyleLedger(shorthand).counts.cascadeEdges, 1, shorthand);
}
assert.equal(createStyleLedger('.a.c.d { color: red; } .x:not(.a.b) { color: blue; }').counts.cascadeEdges, 1);
assert.throws(() => createStyleLedger('.proof { unknown-proof-property: 1; }'), /Unsupported CSS property/);
assert.throws(() => createStyleLedger('@supports (display: grid) { .proof { display: grid; } }'), /Unsupported CSS at-rule/);
assert.throws(() => createStyleLedger('@media screen { .proof { color: red; } }'), /Unsupported media grammar/);
});
test('cascade proof dependencies are exact direct dev dependencies', () => {
const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
assert.equal(packageJson.devDependencies['@babel/parser'], '7.29.3');
assert.equal(packageJson.devDependencies.postcss, '8.5.14');
assert.equal(packageJson.devDependencies['postcss-selector-parser'], '7.1.4');
assert.equal(packageJson.devDependencies['@csstools/selector-specificity'], '6.0.0');
});
test('main owns one public stylesheet and the regrouped production CSS is deterministic', () => {
const main = fs.readFileSync(path.join(root, 'src/web/main.tsx'), 'utf8');
assert.equal((main.match(/import ['"]\.\/styles\/index\.css['"]/g) || []).length, 1);
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
assert.deepEqual(assets, ['index-jheYW1hW.css']);
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
assert.equal(built.byteLength, 103299);
assert.equal(sha256(built), '3acfedf526a1d6e867e825692b1dbdf55896d481a3ec19d97b513dd7704ae291');
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,88 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { normalizeRequestError } from '../../.test-dist/src/web/features/subscription/requestError.js';
const root = path.resolve(import.meta.dirname, '../..');
const page = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.tsx'), 'utf8');
const app = fs.readFileSync(path.join(root, 'src/web/App.tsx'), 'utf8');
const feature = fs.readFileSync(path.join(root, 'src/web/features/subscription/SubscriptionFeature.tsx'), 'utf8');
const boundary = fs.readFileSync(path.join(root, 'src/web/features/subscription/index.ts'), 'utf8');
test('subscription feature is the sole always-mounted lifecycle and view owner', () => {
assert.match(boundary, /SubscriptionDeleteDialog,[\s\S]*SubscriptionPanel,[\s\S]*SubscriptionToggle,[\s\S]*useSubscriptionFeature/);
assert.match(page, /from '\.\.\/features\/subscription\/index\.js'/);
assert.equal((page.match(/useSubscriptionFeature\(/g) || []).length, 1);
assert.equal((page.match(/<SubscriptionToggle/g) || []).length, 1);
assert.equal((page.match(/<SubscriptionPanel/g) || []).length, 1);
assert.equal((page.match(/<SubscriptionDeleteDialog/g) || []).length, 1);
assert.doesNotMatch(page, /client-subscription-summary|client-usage-bar|id="delete-subscription"/);
assert.doesNotMatch(page, /subscriptionValidationAttempt|confirmingDeleteRef|previousHasSubscriptionRef|SUBSCRIPTION_REVEAL_DELAY_MS/);
assert.match(feature, /client-subscription-summary/);
assert.match(feature, /client-usage-bar/);
assert.match(feature, /id="delete-subscription"/);
});
test('App mutation sequencing and the controlled URL draft stay unchanged', () => {
assert.match(app, /const \[subscriptionUrl, setSubscriptionUrl\] = useState\(''\)/);
assert.match(app, /async function fetchSubscription\(\)[\s\S]*api\.subscription\.fetch\(subscriptionUrl\)[\s\S]*dispatch\(\{ type: 'clear-pending-server' \}\)/);
assert.match(app, /async function forgetSubscription\(\)[\s\S]*setSubscriptionUrl\(''\)[\s\S]*dispatch\(\{ type: 'clear-pending-server' \}\)/);
assert.match(page, /subscriptionUrl,[\s\S]*setSubscriptionUrl,[\s\S]*validateSubscription: actions\.validateSubscription,[\s\S]*onImport: onFetchSubscription,[\s\S]*onRefresh: onRefreshSubscription,[\s\S]*onForget: onForgetSubscription/);
assert.doesNotMatch(feature, /from ['"][^'"]*\/api\/|\bapi\./);
assert.doesNotMatch(feature, /ServerPicker|InlineError|InlineProgress|ConnectionPanel|DevicesPanel|DiagnosticsPanel/);
});
test('validation, reveal, refresh, usage and drawer timing remain feature-owned', () => {
assert.match(feature, /setTimeout\(async \(\) => \{[\s\S]*await validateSubscription\(normalizedUrl, \{ signal: controller\.signal \}\)[\s\S]*\}, 300\)/);
assert.match(feature, /normalizeRequestError\(caught\)[\s\S]*requestError\.name === 'AbortError'[\s\S]*controller\.abort\(\)/);
assert.match(feature, /currentValidation\?\.error[\s\S]*\|\| localError[\s\S]*error\?\.context === 'subscription'/);
assert.match(feature, /retry: requestError\.retryable[\s\S]*setValidationAttempt/);
assert.match(feature, /SUBSCRIPTION_REVEAL_DELAY_MS = 1350/);
assert.match(feature, /previouslyHadSubscription[\s\S]*prefers-reduced-motion: reduce[\s\S]*setTimeout\(\(\) => setContentReady\(true\), SUBSCRIPTION_REVEAL_DELAY_MS\)/);
assert.match(feature, /if \(!hasSubscription\) return undefined;[\s\S]*onRefresh\(\);[\s\S]*\}, \[hasSubscription\]\)/);
assert.match(feature, /setTimeout\(\(\) => setEditing\(false\), 5000\)/);
assert.match(feature, /requestAnimationFrame\(tick\)[\s\S]*cancelAnimationFrame\(frame\)/);
assert.match(feature, /420 \+ Math\.min\(7, Math\.max\(0, serverCount - 1\)\) \* 90/);
assert.match(feature, /Math\.max\(900, Math\.ceil\(elapsed \/ 900\) \* 900\)/);
assert.match(feature, /if \(confirmingDeleteRef\.current\) return;[\s\S]*event\.type === 'keydown'[\s\S]*toggleRef\.current\?\.focus\(\)/);
});
test('validation rejection parser preserves structured errors and normalizes non-objects', () => {
const retry = () => true;
const structured = Object.assign(new Error('provider unavailable'), {
name: 'HarborApiError',
context: 'subscription',
correlationId: 'correlation-1',
retryable: true,
retry,
});
assert.deepEqual(normalizeRequestError(structured), {
name: 'HarborApiError',
context: 'subscription',
message: 'provider unavailable',
correlationId: 'correlation-1',
retryable: true,
retry,
});
assert.deepEqual(normalizeRequestError(null), {
name: undefined,
context: undefined,
message: 'Ссылка подписки недействительна.',
correlationId: undefined,
retryable: false,
retry: null,
});
assert.equal(normalizeRequestError('provider rejected').message, 'Ссылка подписки недействительна.');
});
test('feature keeps exact slots, truthy closes and subscription DOM order', () => {
assert.match(feature, /if \(!await onImport\(\)\) return;[\s\S]*setSubscriptionUrl\(''\)[\s\S]*setEditing\(false\)/);
assert.match(feature, /if \(!await onForget\(\)\) return;[\s\S]*setConfirmingDelete\(false\)/);
assert.match(feature, /client-subscription-summary[\s\S]*client-subscription-edit[\s\S]*\{statusSlot\}[\s\S]*client-usage[\s\S]*\{serverSlot\}/);
assert.match(page, /<SubscriptionPanel[\s\S]*statusSlot=\{<>[\s\S]*InlineError[\s\S]*InlineProgress[\s\S]*serverSlot=\{hasSubscription[\s\S]*<ServerPicker/);
assert.match(page, /<SubscriptionToggle[\s\S]*subscriptionFeature\.toggle\(\)/);
assert.match(page, /subscriptionFeature\.close\(\)[\s\S]*devicesFeature\.close\(\)[\s\S]*diagnosticsFeature\.close\(\)/);
assert.doesNotMatch(feature, /setInterval|copyText|client-live-region/);
});