Refactor VPN proxy client implementation
This commit is contained in:
@@ -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/);
|
||||
|
||||
Reference in New Issue
Block a user