Files
harbor-net/test/web/failover-feature-contract.test.js
T
dokril 64462d3639
Build and Deploy Gateway / build-and-push (push) Successful in 26s
Build and Deploy Gateway / deploy (push) Successful in 7s
Clarify failover messaging and bump Harbor versions
2026-08-21 13:00:54 +03:00

80 lines
7.9 KiB
JavaScript
Raw 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, /snapshot\.reason === 'checking-channels'[\s\S]*Проверяем \{checkingRole === 'primary' \? 'основной' : 'резервный'} канал/);
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, /Приостановить автоматическое переключение[\s\S]*Проверить оба канала[\s\S]*Переключить новые подключения на/);
assert.match(feature, /beforeunload/);
assert.match(styles, /\.client-failover-switch\[aria-checked='true'\][\s\S]*translateX\(22px\)/);
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*repeat\(2, minmax\(0, 1fr\)\)/);
assert.match(styles, /@media \(max-width: 560px\)[\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('runtime status keeps fixed geometry and explains active traffic without motion dependence', () => {
assert.match(feature, /Ждём паузу в передаче данных/);
assert.match(feature, /Сейчас передаётся [\s\S]*Активных подключений:[\s\S]*transmittingConnections/);
assert.match(feature, /activity\?\.blockers\.slice\(0, 2\)\.map/);
assert.match(feature, /Ещё \{activity\.blockers\.length - 2\}/);
assert.match(feature, /Проверяем основной канал ·[\s\S]*Проверим каналы снова через/);
assert.match(feature, /switchRole && snapshot\.enabled && snapshot\.activation === 'active'/);
assert.match(styles, /\.client-failover-runtime \{[^}]*min-height:\s*132px/);
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[^{]*\{[^}]*\.client-failover-number-control:hover \.client-failover-number-steps[^}]*transition:\s*none/);
});