import assert from 'node:assert/strict'; 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 = 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, '\\$&'); return new RegExp(`^${escaped}\\s*\\{([\\s\\S]*?)\\n\\}`, 'm').exec(source)?.[1] || ''; } test('desktop layout keeps the power control on a symmetric center axis beside the subscription drawer', () => { const panel = rule('.client-panel'); const power = rule('.client-power-section'); const form = rule('.client-form'); assert.match(panel, /grid-template-columns:\s*minmax\(0, 1fr\) minmax\(240px, 280px\) minmax\(0, 1fr\)/); assert.match(power, /grid-column:\s*2/); assert.match(form, /grid-column:\s*3/); assert.match(form, /position:\s*static/); assert.match(form, /overflow:\s*visible/); assert.doesNotMatch(form, /\bleft\s*:|translateX|transition:[^;]*(?:left|width|transform)/); assert.doesNotMatch(styles, /\.client-panel\.has-subscription \.client-form\b/); assert.doesNotMatch(styles, /\.client-form-content\b/); assert.match(styles, /--client-power-top:\s*calc\(\(var\(--client-work-height\) - 96px\) \/ 2\)/); const subscribedPanel = /\.client-panel\.has-subscription\s*\{([^}]*)\}/.exec(layoutStyles)?.[1] || ''; assert.match(subscribedPanel, /top:\s*-9vh/); assert.doesNotMatch(subscribedPanel, /\b(?:transform|filter|perspective|contain|will-change)\s*:/); assert.match(styles, /\.client-panel\.has-subscription \.client-power-section \{[\s\S]*height:\s*var\(--client-work-height\);[\s\S]*padding-top:\s*var\(--client-power-top\)/); assert.match(rule('.client-panel.is-gateway-home'), /min-height:\s*max\(620px, calc\(100dvh - 80px\)\)[\s\S]*grid-template-rows:\s*minmax\(260px, 1fr\) auto[\s\S]*row-gap:\s*36px/); assert.match(rule('.client-gateway-summary'), /grid-column:\s*1 \/ -1[\s\S]*grid-row:\s*2[\s\S]*justify-self:\s*center[\s\S]*width:\s*min\(860px, calc\(100% - 48px\)\)/); assert.match(styles, /\.client-power-section\.is-gateway \.client-power-control,[\s\S]*width:\s*84px;[\s\S]*height:\s*84px/); assert.match(rule('.client-power-control'), /position:\s*relative/); assert.match(rule('.client-power-control > .harbor-brand'), /position:\s*absolute[\s\S]*bottom:\s*calc\(100% \+ 44px\)[\s\S]*left:\s*50%[\s\S]*transform:\s*translateX\(-50%\) scale\(3\)[\s\S]*transform-origin:\s*center bottom/); assert.match(rule('.client-power-control > .client-tooltip'), /top:\s*calc\(100% \+ 12px\)[\s\S]*left:\s*50%[\s\S]*translate\(-50%, -2px\)/); assert.doesNotMatch(styles, /\.harbor-brand\.is-connected[^}]*transform|translateY\(calc\(-50% - 4px\)\)/); assert.match(rule('.client-gateway-traffic-chart'), /min-height:\s*180px/); assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-gateway-traffic-chart,[\s\S]*min-height:\s*130px;[\s\S]*height:\s*130px/); assert.doesNotMatch(rule('.client-gateway-summary'), /background|border|box-shadow/); assert.match(rule('.client-applied-operation'), /min-height:\s*15px[\s\S]*overflow:\s*hidden[\s\S]*text-overflow:\s*ellipsis[\s\S]*white-space:\s*nowrap/); assert.match(component, /client-panel\$\{showPower \? '' : ' is-setup'\}[\s\S]*is-gateway-home/); assert.match(component, /const showPower = hasSubscription/); assert.match(component, /\$\{!hasSubscription \? ' is-first-run' : ''\}/); assert.match(component, /\$\{isGateway && hasSubscription \? ' is-gateway-home' : ''\}/); }); test('page reload renders the accepted snapshot without a second startup reveal', () => { assert.doesNotMatch(component, /showIntro|is-intro/); assert.doesNotMatch(styles, /is-intro|harbor-startup-(?:brand|content)/); }); test('server rows scroll inside the subscription drawer without moving the main layout', () => { const scroll = rule('.client-server-scroll'); const simpleScroll = rule('.client-server-mode-panel.is-simple .client-server-scroll'); const grid = rule('.client-server-grid'); assert.match(scroll, /overflow-y:\s*auto/); assert.match(scroll, /overflow-x:\s*hidden/); assert.match(scroll, /scrollbar-width:\s*none/); assert.match(styles, /\.client-server-scroll::-webkit-scrollbar\s*\{[\s\S]*display:\s*none/); assert.match(simpleScroll, /max-height:\s*352px/); assert.match(simpleScroll, /overflow-y:\s*auto/); assert.match(simpleScroll, /scrollbar-width:\s*thin/); assert.match(grid, /width:\s*min\(100%, 220px\)/); assert.match(rule('.client-drawer'), /overflow-y:\s*auto/); assert.match(rule('.client-drawer'), /overflow-x:\s*hidden/); assert.match(subscription, /client-profile-list[\s\S]*feature\.profiles\.map\(\(profile\) => { 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/); assert.match(responsive, /\.client-power-section \{[\s\S]*order:\s*1/); assert.match(responsive, /\.client-gateway-summary \{[\s\S]*grid-row:\s*auto;[\s\S]*order:\s*2/); assert.match(responsive, /width:\s*min\(100%, 380px\)/); assert.match(responsive, /max-height:\s*none/); assert.match(responsive, /overflow-y:\s*visible/); assert.match(responsive, /\.harbor-brand,[\s\S]*position:\s*absolute/); assert.match(responsive, /\.client-power-control > \.harbor-brand \{[\s\S]*scale\(1\.55\)/); assert.match(responsive, /\.client-inline-error\.is-connection,[\s\S]*position:\s*static/); assert.match(rule('.client-duration-toggle'), /width:\s*min\(290px, 100%\)/); assert.match(mobile, /\.client-server-toolbar \{[\s\S]*grid-template-rows:\s*44px 44px/); assert.match(mobile, /\.client-secondary-menu \{[\s\S]*right:\s*8px/); assert.match(mobile, /\.client-power-control > \.harbor-brand \{[\s\S]*scale\(1\.35\)/); assert.match(mobile, /\.client-server-check \{[\s\S]*width:\s*44px/); assert.match(rule('.client-drawer'), /width:\s*min\(580px, 100vw\)/); assert.match(styles, /\.client-confirmation-dialog\s*\{[\s\S]*width:\s*min\(430px, calc\(100vw - 48px\)\)/); for (const viewport of [320, 390, 768]) { const pagePadding = viewport <= 560 ? 28 : 48; const panelPadding = 12; const available = viewport - pagePadding - panelPadding; assert.ok(Math.min(380, available) <= viewport, `form fits ${viewport}px viewport`); assert.ok(Math.min(290, available) <= viewport, `duration control fits ${viewport}px viewport`); } const desktopOuterColumn = (1100 - 280) / 2; assert.ok(Math.min(360, desktopOuterColumn) <= desktopOuterColumn, 'details fit the 1440px desktop grid'); }); test('secondary menus share one right rail and switch equal drawers as a vertical tape', () => { const disabledRulesLabel = rule('.client-local-rules-toggle:disabled span'); const zIndex = (selector) => Number(/z-index:\s*(\d+)/.exec(rule(selector))?.[1]); assert.match(component, / event\.stopPropagation\(\)\}/); assert.match(component, / span\s*\{[^}]*opacity:\s*0/); assert.deepEqual( ['.client-confirmation-popup', '.client-secondary-menu', '.client-drawer', '.harbor-versions'].map(zIndex), [100, 60, 50, 40], ); assert.doesNotMatch(styles, /\.client-drawer\s*\{[^}]*right:\s*64px/); assert.match(instructions, /\(null\)/); assert.match(component, /const activeRailDrawer = drawerSwitchTarget && drawerControls\[drawerSwitchTarget\]\.isOpen[\s\S]*drawerControls\[drawer\]\.isOpen/); assert.match(component, /setDrawerSwitchTarget\(target\);[\s\S]*flushSync\(\(\) => toControl\.show\(\)\)/); assert.match(component, /fromControl\.close\(\);[\s\S]*setDrawerSwitchTarget\(null\)/); assert.match(component, / \{[\s\S]*from\.inert = false;[\s\S]*from\.removeAttribute\('aria-hidden'\)/); assert.match(component, /DRAWER_SWITCH_MS = 620[\s\S]*prefers-reduced-motion: reduce[\s\S]*flushSync/); assert.match(component, /current === 'routing' && routingFeature\.dirty[\s\S]*routingFeature\.requestClose\(\)/); assert.doesNotMatch(component, /instructionsFeature\.close\(\)[\s\S]{0,180}devicesFeature\.close\(\)[\s\S]{0,180}subscriptionFeature\.toggle\(\)/); }); test('routing rules stay in one desktop row and reflow without shrinking drag targets', () => { assert.match(rule('.client-local-rule'), /grid-template-columns:\s*44px 24px 104px minmax\(84px, 1fr\) 34px 16px 28px/); assert.doesNotMatch(routing, /client-local-rule-meta/); assert.match(styles, /\.client-local-rules-list \{[\s\S]*position:\s*relative[\s\S]*padding-left:\s*14px/); assert.match(styles, /\.client-local-rules-list\.has-order-flow::after[\s\S]*clip-path:\s*polygon\(0 0, 100% 0, 50% 100%\)/); const mobile = /@media \(max-width: 560px\) \{([\s\S]*?)\n\}\s*$/.exec(layoutStyles)?.[1] || ''; assert.match(mobile, /grid-template-columns:\s*44px 44px minmax\(0, 1fr\) 44px 44px/); assert.match(mobile, /\.client-local-rule input \{[\s\S]*grid-row:\s*2/); assert.match(mobile, /\.client-rule-outbound \{[\s\S]*grid-column:\s*4;[\s\S]*grid-row:\s*1/); assert.match(mobile, /\.client-local-rule-status \{[\s\S]*grid-column:\s*5;[\s\S]*grid-row:\s*2/); assert.match(mobile, /\.client-rule-handle,[\s\S]*\.client-local-rule-enabled-wrap,[\s\S]*width:\s*44px;[\s\S]*height:\s*44px/); assert.match(mobile, /\.client-local-rule-enabled-wrap \{[\s\S]*grid-column:\s*2;[\s\S]*grid-row:\s*1/); assert.match(mobile, /\.client-rule-type-trigger,[\s\S]*\.client-rule-outbound button \{[\s\S]*height:\s*44px/); assert.match(mobile, /\.client-rule-type-list button \{[\s\S]*min-height:\s*44px/); const compact = layoutStyles.slice(layoutStyles.indexOf('@media (max-width: 360px)')); assert.match(compact, /grid-template-columns:\s*44px 44px minmax\(0, 1fr\) 44px/); assert.match(compact, /\.client-rule-outbound \{[\s\S]*grid-column:\s*2;[\s\S]*grid-row:\s*3/); assert.match(compact, /\.client-local-rule-status \{[\s\S]*grid-column:\s*4;[\s\S]*grid-row:\s*3/); }); test('connectivity diagnostics render stable compact tables before the first run', () => { assert.match(diagnostics, /CONNECTIVITY_IP_SOURCES\.map/); assert.match(diagnostics, /CONNECTIVITY_SITES\.filter\(\(\{ id \}\) => !hiddenServiceIds\.includes\(id\)\)/); assert.equal((diagnostics.match(/\.{3}<\/span>/); assert.doesNotMatch(styles, /client-diagnostics-row-pulse/); assert.doesNotMatch(rule('.client-diagnostics-status.is-running'), /animation:|transform:|opacity:/); }); 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(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(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/); }); test('responsive motion has a reduced-motion fallback', () => { const reducedMotion = /@media \(prefers-reduced-motion: reduce\) \{([\s\S]*)\n\}/.exec(styles)?.[1] || ''; assert.match(reducedMotion, /\.client-power-section/); assert.match(reducedMotion, /\.client-form/); assert.match(reducedMotion, /\.harbor-brand/); assert.match(reducedMotion, /\.client-rail-action circle/); assert.match(reducedMotion, /\.client-rail-action g/); assert.match(reducedMotion, /\.client-rail-action rect/); assert.match(reducedMotion, /\.client-drawer/); assert.match(reducedMotion, /transition:\s*none/); assert.match(reducedMotion, /animation:\s*none/); assert.doesNotMatch(styles, /\.client-rail-action:active:not\(:disabled\) svg/); }); test('tooltips stay opaque, above adjacent content, and do not stick after pointer clicks', () => { const tooltip = rule('.client-tooltip'); assert.match(tooltip, /z-index:\s*1000/); assert.match(tooltip, /background:\s*oklch\(0\.14 0\.012 145\)/); assert.doesNotMatch(tooltip, /transparent|backdrop-filter/); assert.match(styles, /\.client-power-section:has\(\.client-tooltip-anchor:hover\)[\s\S]*z-index:\s*90/); assert.match(styles, /\.client-tooltip-anchor:focus-visible > \.client-tooltip/); assert.match(styles, /\.client-tooltip-anchor:has\(> :focus-visible\) > \.client-tooltip/); assert.doesNotMatch(styles, /\.client-tooltip-anchor:focus-within > \.client-tooltip/); assert.match(rule('.harbor-version-tooltip'), /background:\s*oklch\(0\.14 0\.012 145\)/); assert.match(rule('.harbor-mode-tooltip'), /background:\s*oklch\(0\.14 0\.012 145\)/); }); test('subscription validation is local and provider errors keep stable detail slots', () => { assert.match(subscription, /const validationStatus = !normalizedUrl[\s\S]*isSubscriptionUrlValid\(normalizedUrl\) \? 'valid' : 'invalid'/); assert.doesNotMatch(subscription, /validateSubscription\(|AbortController|status: 'checking'/); assert.match(subscription, /const message = feature\.validationStatus === 'invalid'[\s\S]*ERROR_DEFINITIONS\.SUBSCRIPTION_INVALID\.message[\s\S]*feature\.addError\?\.message/); assert.match(subscription, /aria-invalid=\{feature\.validationStatus === 'invalid'\}/); assert.match(subscription, /disabled=\{feature\.addBlocked \|\| feature\.validationStatus !== 'valid'\}/); assert.match(subscription, /const host = new URL\(normalizedUrl\)\.hostname;[\s\S]*if \(!await onAdd\(label, normalizedUrl\)\) return;[\s\S]*resetAdd\(\)/); assert.doesNotMatch(subscription, />Название<|duplicateLabel/); assert.match(subscription, /setUrl: \(value: string\) => \{[\s\S]*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/); assert.match(styles, /@keyframes client-subscription-error-in[\s\S]*filter:\s*blur\(7px\)/); assert.match(styles, /@keyframes client-subscription-code-in[\s\S]*transform:\s*translateY\(-2px\)/); assert.match( /@media \(prefers-reduced-motion: reduce\) \{([\s\S]*)\n\}/.exec(styles)?.[1] || '', /\.client-inline-error\.is-subscription > \*/, ); });