import assert from 'node:assert/strict'; import test from 'node:test'; import { accessTabForKey, connectionAction, copyText, formatConnectionDuration, formatConnectionDurationWords, isSubscriptionUrlValid, localProxyUrls, routePresentation, subscriptionDomain, subscriptionDaysLeft, subscriptionUsage, } from '../../src/web/utils/clientControls.js'; import { instructionBlocks } from '../../src/web/instructions.js'; test('connection button chooses the only valid client action', () => { assert.deepEqual(connectionAction({ connected: true }), { type: 'stop' }); assert.deepEqual(connectionAction({ selectedServerId: 'srv_nl' }), { type: 'apply', serverId: 'srv_nl', }); assert.deepEqual(connectionAction({ configExists: true }), { type: 'restart' }); assert.equal(connectionAction({}), null); }); test('connection duration is derived from the backend start time', () => { const startedAt = '2026-07-10T10:00:00.000Z'; const now = Date.parse('2026-07-11T12:03:04.900Z'); assert.equal(formatConnectionDuration(startedAt, now), '26:03:04'); assert.equal(formatConnectionDuration(null, now), '00:00:00'); }); test('connection duration can be written with Russian units', () => { const startedAt = '2026-07-10T10:00:00.000Z'; const now = Date.parse('2026-07-11T12:03:04.900Z'); assert.equal(formatConnectionDurationWords(startedAt, now), '1 день 2 часа 3 минуты 4 секунды'); assert.equal(formatConnectionDurationWords(null, now), '0 секунд'); }); test('saved subscription is reduced to its public domain', () => { assert.equal(subscriptionDomain('sub.example.com/…'), 'sub.example.com'); assert.equal(subscriptionDomain(''), ''); }); test('subscription URL is validated locally by shape', () => { assert.equal(isSubscriptionUrlValid(' https://sub.example/token '), true); assert.equal(isSubscriptionUrlValid('http://127.0.0.1/subscription'), true); assert.equal(isSubscriptionUrlValid('ftp://sub.example/token'), false); assert.equal(isSubscriptionUrlValid('not-a-url'), false); }); test('local proxy exposes both supported URLs', () => { assert.deepEqual(localProxyUrls(18080), { socks5: 'socks5://127.0.0.1:18080', http: 'http://127.0.0.1:18080', }); }); test('local proxy defaults to the macOS client port', () => { assert.deepEqual(localProxyUrls(), { socks5: 'socks5://127.0.0.1:8082', http: 'http://127.0.0.1:8082', }); }); test('gateway proxy URLs use its network address', () => { assert.deepEqual(localProxyUrls(8080, '192.168.50.111'), { socks5: 'socks5://192.168.50.111:8080', http: 'http://192.168.50.111:8080', }); }); test('route copy distinguishes found, stale, lost, disabled and local states', () => { assert.equal(routePresentation({ mode: 'gateway-direct', reason: 'gateway-found' }).title, 'Через Harbor Gateway'); assert.equal(routePresentation({ mode: 'gateway-direct', reason: 'gateway-stale' }).tone, 'stale'); assert.match(routePresentation({ mode: 'local-vpn', reason: 'gateway-lost' }).title, /Gateway недоступен/); assert.match(routePresentation({ mode: 'local-vpn', reason: 'disabled' }).detail, /выключен/); assert.equal(routePresentation({ mode: 'local-vpn', reason: 'local' }).title, 'Локальный VPN'); }); test('copy uses the synchronous native path available on gateway HTTP', async () => { const textarea = { style: {}, setAttribute() {}, select() { this.selected = true; }, remove() { this.removed = true; }, }; const documentRef = { body: { append(node) { node.appended = true; } }, createElement: () => textarea, execCommand: (command) => command === 'copy', }; await copyText('192.168.50.111', { documentRef }); assert.equal(textarea.value, '192.168.50.111'); assert.equal(textarea.selected, true); assert.equal(textarea.removed, true); }); test('Gateway access tabs wrap with arrows and support Home and End', () => { const tabs = ['gateway', 'proxy']; assert.equal(accessTabForKey(tabs, 'gateway', 'ArrowLeft'), 'proxy'); assert.equal(accessTabForKey(tabs, 'proxy', 'ArrowRight'), 'gateway'); assert.equal(accessTabForKey(tabs, 'proxy', 'Home'), 'gateway'); assert.equal(accessTabForKey(tabs, 'gateway', 'End'), 'proxy'); assert.equal(accessTabForKey(tabs, 'gateway', 'Enter'), 'gateway'); }); test('subscription usage combines traffic and caps progress', () => { assert.deepEqual(subscriptionUsage({ upload: 30, download: 80, total: 100, expire: 2 }), { upload: 30, download: 80, total: 100, used: 110, percent: 100, expiresAt: new Date(2000), }); }); test('subscription expiry uses Russian day forms', () => { const now = Date.parse('2026-07-11T00:00:00Z'); assert.equal(subscriptionDaysLeft(new Date('2026-07-12T00:00:00Z'), now), 'остался 1 день'); assert.equal(subscriptionDaysLeft(new Date('2026-07-13T00:00:00Z'), now), 'осталось 2 дня'); assert.equal(subscriptionDaysLeft(new Date('2026-07-16T00:00:00Z'), now), 'осталось 5 дней'); }); test('gateway receives router instructions while local client does not', () => { const gateway = instructionBlocks({ isGateway: true, host: '192.168.1.20', port: 8080 }); const client = instructionBlocks({ isGateway: false, host: '127.0.0.1', port: 8082 }); assert.equal(gateway.at(-1).id, 'router'); assert.equal(client.some((block) => block.id === 'router'), false); assert.match(client.find((block) => block.id === 'vscode').code, /127\.0\.0\.1:8082/); });