Files
harbor-net/test/web/failover-feature-contract.test.js
dokril e0bdafd25e
Build and Deploy Gateway / build-and-push (push) Successful in 43s
Build and Deploy Gateway / deploy (push) Successful in 6s
Refine failover channel status layout
2026-08-31 14:52:36 +03:00

108 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/failover/FailoverFeature.tsx'), 'utf8');
const styles = fs.readFileSync(path.join(root, 'src/web/styles/features/failover.css'), 'utf8');
test('reserve is a Gateway-only drawer immediately after subscriptions', () => {
assert.match(page, /<SubscriptionToggle[\s\S]*\{isGateway && <FailoverToggle[\s\S]*<InstructionsToggle/);
assert.match(page, /\{isGateway && hasSubscription && <FailoverPanel/);
assert.match(page, /DRAWER_ORDER = \['subscription', 'failover'/);
assert.match(feature, /label="Резерв"/);
assert.match(feature, /Уже открытые подключения продолжат работать/);
});
test('failover settings use Harbor switches, a two-column channel table and plain-language controls', () => {
assert.match(feature, /client-failover-heading[\s\S]*<h2>Резервный канал<\/h2>[\s\S]*<HarborSwitch/);
assert.match(feature, /role="switch"[\s\S]*aria-checked=\{checked\}/);
assert.doesNotMatch(feature, /type="checkbox"|<select/);
assert.match(feature, /\['primary', 'reserve'\][\s\S]*client-failover-\$\{channel\}-options[\s\S]*updateTarget\(channel, optionId\)/);
assert.match(feature, /channelLabel[\s\S]*profile\.trim\(\) === server\.trim\(\)[\s\S]*`\$\{profile} · \$\{server}`/);
assert.match(feature, /client-failover-channels[\s\S]*<article className="client-failover-channel"[\s\S]*<h3>\{channel === 'primary' \? 'Основной' : 'Резервный'}/);
assert.match(feature, /client-failover-channel-status[\s\S]*client-failover-channel-progress/);
assert.match(feature, /checking[\s\S]*`Проверяем \$\{channel === 'primary' \? 'основной' : 'резервный'\} канал`/);
assert.match(feature, /client-live-region[\s\S]*Проверяем основной и резервный каналы/);
assert.match(feature, /ArrowDown[\s\S]*Home[\s\S]*End[\s\S]*Escape/);
assert.match(feature, /role="listbox"[\s\S]*role="option"/);
assert.match(feature, /Что проверять[\s\S]*Harbor проверяет эти сайты через оба канала, чтобы понять, какой канал работает/);
assert.match(feature, /before="Ждать ответа не дольше"[\s\S]*min=\{2}[\s\S]*max=\{30}/);
assert.match(feature, /Проверять оба канала каждые[\s\S]*Считать основной канал недоступным, если он не отвечает[\s\S]*Вернуться на основной канал после/);
assert.match(feature, /Дождаться паузы в передаче данных[\s\S]*Переключить после[\s\S]*без передачи данных[\s\S]*Считать передачу активной при скорости выше/);
assert.match(feature, /Когда переключаться на резервный канал[\s\S]*основной канал не отвечает, а резервный канал работает[\s\S]*Повторные сбои основного канала[\s\S]*оставляет новые подключения на резервном канале/);
assert.doesNotMatch(feature, /Когда включать резервный|Harbor включит резервный,|через резервный\./);
assert.match(feature, /client-failover-number-input[\s\S]*Math\.max\(2, String\(value\)\.length \+ 0\.6\)/);
assert.match(feature, /client-failover-number-control[\s\S]*client-failover-number-input[\s\S]*client-failover-number-steps[\s\S]*уменьшить на \$\{step\}[\s\S]*><\/button>[\s\S]*увеличить на \$\{step\}[\s\S]*>\+<\/button>/);
assert.match(feature, /before="Ждать ответа не дольше"[\s\S]*step=\{1\}/);
assert.doesNotMatch(feature, /advancedOpen|advancedClosing|toggleAdvanced|aria-controls="client-failover-advanced-options"/);
assert.match(feature, /label="Добавить проверку"/);
assert.match(feature, /Новый HTTPS-сервис…/);
assert.match(feature, /Убрать проверку:/);
assert.match(feature, /client-failover-remove-lid/);
assert.match(feature, /finishRemoveCheck/);
assert.match(feature, /startViewTransition/);
assert.match(feature, /onInput=\{pulseNumber\}/);
assert.doesNotMatch(feature, /<details|<summary|Нет данных|текущий канал вне настроенной пары/);
assert.doesNotMatch(page, /Текущий канал вне резервной пары/);
assert.match(page, /Резерв применится после перезапуска VPN/);
assert.match(feature, /client-failover-header-actions[\s\S]*Автоматика[\s\S]*checked=\{!snapshot\.paused\}[\s\S]*Включить автоматическое переключение[\s\S]*Остановить автоматическое переключение[\s\S]*className="client-failover-save"[\s\S]*type="submit"[\s\S]*form="client-failover-form"[\s\S]*Сохранить/);
assert.match(feature, /onChange=\{\(automatic\) => void onPause\(!automatic\)\}/);
assert.match(feature, /checked=\{!snapshot\.paused\}[\s\S]*disabled=\{blocked \|\| !snapshot.enabled\}/);
assert.match(feature, /client-failover-check-action[\s\S]*disabled=\{blocked \|\| checking \|\| !snapshot.enabled \|\| snapshot.activation !== 'active'\}/);
assert.doesNotMatch(feature, /client-failover-actions|client-failover-global-actions|client-failover-discard/);
assert.match(feature, /channel === 'primary' && <div className="client-failover-channel-actions"[\s\S]*client-failover-check-action client-tooltip-anchor[\s\S]*aria-label="Проверить оба канала"[\s\S]*aria-busy=\{checking\}[\s\S]*client-failover-direction-action client-tooltip-anchor[\s\S]*aria-label=\{switchActionLabel\}/);
assert.match(feature, /switchActionLabel = role && switchRole[\s\S]*`Сейчас \$\{role === 'primary' \? 'основной' : 'резервный'\} канал\. Переключить на/);
assert.match(feature, /<Tooltip>Проверить оба канала<\/Tooltip>[\s\S]*<Tooltip>\{switchActionLabel\}<\/Tooltip>/);
assert.match(feature, /M20 11a8 8 0 1 0-2\.3 6\.7M20 5v6h-6/);
assert.match(feature, /M4 12h15M14 7l5 5-5 5/);
assert.match(feature, /normalizeFailoverDraft = \(policy: FailoverPolicy\)[\s\S]*minimumReserveMs: Math\.round\(normalized\.minimumReserveMs \/ 60_000\) \* 60_000/);
assert.match(feature, /before="Оставаться на резервном канале не меньше" after="минут" min=\{1\} max=\{1440\} step=\{1\}[\s\S]*value=\{draft\.minimumReserveMs \/ 60_000\}[\s\S]*minimumReserveMs: Math\.round\(value\) \* 60_000/);
assert.doesNotMatch(feature, /className="client-failover-number-input"[\s\S]*max=\{max\}[\s\S]*step=\{step\}/);
assert.match(feature, /<ConfirmationDialog[\s\S]*id="discard-failover"[\s\S]*Закрыть без сохранения\?[\s\S]*Несохранённые настройки резервного канала будут потеряны\.[\s\S]*Остаться[\s\S]*Закрыть без сохранения/);
assert.match(feature, /discardContinuationFrameRef[\s\S]*cancelAnimationFrame[\s\S]*requestAnimationFrame[\s\S]*continueNavigation[\s\S]*feature\.close\(\)/);
assert.match(page, /if \(!failoverFeature\.beforeCloseRef\.current\(\)\)[\s\S]*pendingTargetRef\.current = \(\) => switchDrawer\(target\)[\s\S]*pendingTargetRef\.current = null/);
assert.match(styles, /\.client-failover-check-action\.is-running svg \{ animation: client-spin 900ms linear infinite; \}/);
assert.match(styles, /\.client-failover-direction-action svg \{[^}]*transition: transform 260ms cubic-bezier\(0\.16, 1, 0\.3, 1\)/);
assert.match(styles, /\.client-failover-direction-action svg\.is-primary \{ transform: rotate\(180deg\); \}/);
assert.match(styles, /\.client-failover-direction-action\.is-primary \{ color: var\(--harbor-connect\); \}/);
assert.match(styles, /\.client-failover-direction-action\.is-reserve \{ color: var\(--harbor-gateway\); \}/);
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*client-failover-direction-action svg[\s\S]*transition: none[\s\S]*client-failover-check-action\.is-running svg[\s\S]*animation: none/);
assert.match(styles, /\.client-failover-switch\[aria-checked='true'\][\s\S]*translateX\(22px\)/);
assert.match(styles, /\.client-failover-header \{[^}]*position:\s*sticky[^}]*top:\s*0[^}]*background:/);
assert.match(styles, /\.client-failover-heading \{[^}]*justify-content:\s*space-between[^}]*}/);
assert.doesNotMatch(styles, /\.client-failover-heading \{[^}]*padding-right/);
assert.match(styles, /\.client-failover-channels \{[^}]*grid-template-columns:\s*minmax\(0, 1fr\) 52px minmax\(0, 1fr\)/);
assert.match(styles, /\.client-failover-channel-actions \{[^}]*grid-column:\s*2[^}]*grid-row:\s*1/);
assert.match(styles, /\.client-failover-check-action, \.client-failover-direction-action \{[^}]*width:\s*44px[^}]*height:\s*44px/);
assert.match(styles, /@media \(max-width: 680px\)[\s\S]*\.client-failover-channels \{ grid-template-columns:\s*minmax\(0, 1fr\)/);
assert.match(styles, /\.client-failover-picker-list[\s\S]*opacity:\s*0[\s\S]*\.client-failover-picker\.is-open[\s\S]*opacity:\s*1/);
assert.match(styles, /failover-service-out 300ms[\s\S]*@media \(prefers-reduced-motion: reduce\)/);
assert.match(styles, /\.client-failover-number-setting \{[^}]*display:\s*flex[^}]*flex-wrap:\s*wrap/);
assert.match(styles, /\.client-failover-number-control \{[^}]*position:\s*relative[^}]*min-width:\s*44px[^}]*padding-bottom:\s*18px/);
assert.match(styles, /\.client-failover-number-setting input \{[^}]*border:\s*0[^}]*text-align:\s*center[^}]*appearance:\s*textfield/);
assert.match(styles, /input::\-webkit-inner-spin-button[\s\S]*input::\-webkit-outer-spin-button[^}]*\-webkit-appearance:\s*none/);
assert.match(styles, /\.client-failover-number-setting \{[^}]*font:\s*var\(--type-data\)/);
assert.match(styles, /\.client-failover-section-header p,[^}]*font:\s*var\(--type-control\)/);
assert.match(styles, /\.client-failover-number-steps \{[^}]*position:\s*absolute[^}]*opacity:\s*0[^}]*pointer-events:\s*none[^}]*opacity 260ms ease 80ms/);
assert.match(styles, /\.client-failover-number-control:hover \.client-failover-number-steps,[^}]*:focus-within[^}]*opacity:\s*1[^}]*pointer-events:\s*auto[^}]*opacity 180ms ease[^}]*transform 220ms/);
assert.match(styles, /\.client-failover-timing \{[^}]*gap:\s*0[^}]*\}[\s\S]*\.client-failover-protection-options \{[^}]*gap:\s*0/);
assert.doesNotMatch(styles, /client-failover-advanced|failover-advanced-in|failover-advanced-out/);
});
test('active channel and checks stay attached to their channel columns without a duplicate runtime block', () => {
assert.match(feature, /failureProgress[\s\S]*`\$\{seconds\([\s\S]*\)} \/ \$\{seconds\(draft\.failureWindowMs\)} с`/);
assert.match(feature, /failureProgress[\s\S]*'Проверяем основной канал'/);
assert.match(feature, /client-failover-direction-action[\s\S]*disabled=\{blocked \|\| !snapshot.enabled \|\| snapshot.activation !== 'active' \|\| !switchRole\}/);
assert.match(feature, /className=\{`client-failover-direction-action client-tooltip-anchor\$\{role === 'primary'[\s\S]*role === 'reserve'/);
assert.match(feature, /<svg className=\{role === 'primary' \? 'is-primary' : role === 'reserve' \? 'is-reserve' : undefined\}/);
assert.doesNotMatch(feature, /client-failover-runtime|Сейчас используется|Новые подключения:/);
assert.doesNotMatch(feature, /client-failover-channel-checking/);
assert.doesNotMatch(styles, /\.client-failover-runtime/);
assert.match(styles, /\.client-failover-channel-status \{[^}]*min-height:\s*32px[^}]*grid-template-columns:\s*minmax\(0, 1fr\)[^}]*grid-template-rows:\s*repeat\(2, minmax\(0, 1fr\)\)/);
assert.match(styles, /\.client-failover-channel-progress \{[^}]*min-width:\s*0[^}]*text-align:\s*left/);
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[^{]*\{[^}]*\.client-failover-number-control:hover \.client-failover-number-steps[^}]*transition:\s*none/);
});