import assert from 'node:assert/strict'; import test from 'node:test'; import { connectionAction, copyText, formatConnectionDuration, formatConnectionDurationWords, isSubscriptionUrlValid, localProxyUrls, subscriptionDomain, subscriptionDaysLeft, subscriptionUsage, } from '../../.test-dist/src/web/utils/clientControls.js'; import { instructionBlocks } from '../../.test-dist/src/web/features/instructions/instructionBlocks.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('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('copy prefers the Clipboard API when it is available', async () => { const writes = []; let legacyCalls = 0; await copyText('socks5://127.0.0.1:8082', { clipboard: { writeText: async (text) => writes.push(text) }, documentRef: { execCommand: () => { legacyCalls += 1; } }, }); assert.deepEqual(writes, ['socks5://127.0.0.1:8082']); assert.equal(legacyCalls, 0); }); test('copy falls back after the Clipboard API rejects', async () => { const textarea = { style: {}, setAttribute() {}, select() {}, remove() { this.removed = true; }, }; const documentRef = { body: { append() {} }, createElement: () => textarea, execCommand: () => true, }; await copyText('192.168.50.111', { clipboard: { writeText: async () => { throw new Error('denied'); } }, documentRef, }); assert.equal(textarea.removed, true); }); test('copy rejects and removes the temporary textarea when the fallback fails', async () => { const textarea = { style: {}, setAttribute() {}, select() {}, remove() { this.removed = true; }, }; const documentRef = { body: { append() {} }, createElement: () => textarea, execCommand: () => false, }; await assert.rejects(copyText('192.168.50.111', { documentRef }), /Copy failed/); assert.equal(textarea.removed, true); }); test('copy removes the temporary textarea when the fallback throws', async () => { const textarea = { style: {}, setAttribute() {}, select() {}, remove() { this.removed = true; }, }; const documentRef = { body: { append() {} }, createElement: () => textarea, execCommand: () => { throw new Error('legacy failed'); }, }; await assert.rejects(copyText('192.168.50.111', { documentRef }), /legacy failed/); assert.equal(textarea.removed, true); }); 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, controlHost: '192.168.1.20:3456', }); const client = instructionBlocks({ isGateway: false, host: '127.0.0.1', port: 8082, controlHost: '127.0.0.1:3456', }); assert.equal(gateway.some((block) => block.id === 'router'), true); assert.equal(gateway.some((block) => block.id === 'prometheus'), true); assert.equal(client.some((block) => block.id === 'router'), false); assert.equal(client.some((block) => block.id === 'prometheus'), false); assert.match(client.find((block) => block.id === 'vscode').code, /127\.0\.0\.1:8082/); });