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 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 failover = fs.readFileSync(path.join(root, 'src/web/features/failover/FailoverFeature.tsx'), 'utf8'); const dialog = fs.readFileSync(path.join(root, 'src/web/ui/ConfirmationDialog.tsx'), 'utf8'); const copyButton = fs.readFileSync(path.join(root, 'src/web/ui/CopyButton.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(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 && !bypassed && 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-row-delete"[\s\S]*client-row-delete-lid/); assert.match(styles, /\.client-row-delete-lid \{[\s\S]*transition: transform/); assert.match(styles, /\.client-row-delete:hover:not\(:disabled\) \.client-row-delete-lid,[\s\S]*translateY\(-2px\) rotate\(-8deg\)/); 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/); assert.match(styles, /clip-path: polygon\(0 24%, 100% 50%, 0 76%\)/); assert.doesNotMatch(styles, /\.client-delete-strike::before/); assert.doesNotMatch(styles, /client-local-rule > button:last-child/); assert.match(styles, /@keyframes client-delete-strike[\s\S]*scaleX\(0\)[\s\S]*scaleX\(1\)/); assert.match(styles, /53%[\s\S]*transform-origin: right center[\s\S]*100%[\s\S]*scaleX\(0\)/); assert.match(styles, /@keyframes client-row-leave[\s\S]*translateX\(28px\)/); assert.match(styles, /@keyframes client-delete-content-dim[\s\S]*opacity: 0\.58[\s\S]*blur\(1\.4px\)[\s\S]*opacity: 0/); assert.match(styles, /\.client-local-rule:has\(\.client-rule-type\.is-open\)[\s\S]*z-index: 5/); }); test('critical confirmations share one accessible blocking popup', () => { assert.equal(fs.existsSync(path.join(root, 'src/web/components/ConfirmationPopup.jsx')), false); assert.equal( (component.match(/\(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(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(copyButton, /client-copy-feedback[\s\S]*feedback\.failed \? 'Ошибка' : 'Скопировано'/); assert.doesNotMatch(component, /ГОТОВО/); assert.doesNotMatch(component, />Error<|>Copied { const handle = //.exec(routing)?.[0] || ''; assert.match(routing, /className="client-rule-handle"[\s\S]*type="button"[\s\S]*aria-label=\{`Переместить правило, позиция/); assert.match(routing, /aria-describedby="client-rule-reorder-instructions"[\s\S]*aria-pressed=\{lifted\}/); assert.match(routing, /onPointerDown=\{\(event\) => feature\.startPointerReorder\(event, rule\._key\)\}/); assert.doesNotMatch(handle, /onPointerMove|onPointerUp|onPointerCancel|onLostPointerCapture/); assert.doesNotMatch(routing, /data-rule-key[^>]*onPointerDown/); assert.match(routing, /onClick=\{\(event\) => feature\.handleReorderClick\(event, rule\._key\)\}/); assert.match(routing, /