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
+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/);