Files
harbor-net/test/web/server-picker.test.js
T
dokril 79ffff194f
Build and Deploy Gateway / build-and-push (push) Successful in 19s
Build and Deploy Gateway / deploy (push) Successful in 6s
Update VPN client and gateway behavior
2026-08-11 08:50:44 +03:00

147 lines
9.0 KiB
JavaScript

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';
import {
autoServer,
filterServers,
groupServers,
parseServerPingResults,
SERVER_RESULT_WINDOW,
} from '../../.test-dist/src/web/features/servers/serverPickerModel.js';
const root = path.resolve(import.meta.dirname, '../..');
const picker = fs.readFileSync(path.join(root, 'src/web/features/servers/ServerPicker.tsx'), 'utf8');
const boundary = fs.readFileSync(path.join(root, 'src/web/features/servers/index.ts'), 'utf8');
const overview = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.tsx'), 'utf8');
const styles = readStyleSource(root);
test('server picker has one public feature owner without legacy shims', () => {
assert.equal(boundary.trim(), "export { ServerPicker } from './ServerPicker.js';");
assert.equal(fs.existsSync(path.join(root, 'src/web/components/ServerPicker.jsx')), false);
assert.equal(fs.existsSync(path.join(root, 'src/web/utils/serverPicker.js')), false);
assert.match(overview, /import \{ ServerPicker \} from '\.\.\/features\/servers\/index\.js'/);
assert.equal((overview.match(/<ServerPicker/g) || []).length, 1);
assert.doesNotMatch(picker, /from ['"][^'"]*\/api\/|\bapi\./);
assert.match(overview, /const selectedServerId = desiredProfile\?\.desiredServerId \|\| ''/);
assert.match(overview, /function selectServer\(profile: ProfileSnapshot, serverId: string\)[\s\S]*onApply\(profile\.id, serverId\)[\s\S]*onSelectProfileServer\(profile\.id, serverId\)/);
});
const fixtures = (count) => Array.from({ length: count }, (_, index) => ({
id: `srv-${String(count - index).padStart(3, '0')}`,
label: index < 2 ? 'Duplicate' : `Server ${index}`,
host: `node-${index}.example`,
country: index % 2 ? 'NL' : 'DE',
provider: index % 3 ? 'Harbor' : 'Other',
protocol: index % 2 ? 'vless' : 'trojan',
}));
test('server picker handles 1, 30 and 300 stable-ID servers with duplicate labels', () => {
for (const count of [1, 30, 300]) {
const servers = fixtures(count);
assert.equal(filterServers(servers, '').length, count);
assert.equal(new Set(servers.map(({ id }) => id)).size, count);
}
assert.equal(filterServers(fixtures(30), 'node-12.example')[0].id, 'srv-018');
assert.equal(filterServers(fixtures(30), 'trojan').length, 15);
assert.equal(groupServers(fixtures(30)).length, 2);
assert.equal(autoServer(fixtures(30)).id, 'srv-001');
assert.equal(SERVER_RESULT_WINDOW, 60);
});
test('server picker validates unknown ping payloads before publishing results', () => {
const result = { id: 'srv-1', ok: true, latency: 12, checkedAt: '2026-08-08T12:00:00.000Z', extra: 'kept' };
const failed = { id: 'srv-2', ok: false, latency: null, error: 'timeout', checkedAt: '2026-08-08T12:00:01.000Z', extra: 'kept' };
assert.deepEqual(parseServerPingResults({}), []);
assert.equal(parseServerPingResults({ results: [result] })[0], result);
assert.equal(parseServerPingResults({ results: [failed] })[0], failed);
for (const payload of [
null,
[],
{ results: null },
{ results: [{}] },
{ results: [{ id: '', ok: true }] },
{ results: [{ id: 'srv-1', ok: 'yes' }] },
{ results: [{ id: 'srv-1', latency: -1 }] },
{ results: [{ id: 'srv-1', checkedAt: 123 }] },
]) {
assert.throws(() => parseServerPingResults(payload), TypeError);
}
assert.match(picker, /parseServerPingResults\(await pingServers\(profileId, ids\)\)/);
});
test('server picker checks health only on manual refresh and bounds the result window', () => {
assert.doesNotMatch(overview, /pingAll|servers\.ping/);
assert.doesNotMatch(picker, /checkVisible\(\);/);
assert.match(picker, /onClick={checkVisible}/);
assert.match(picker, /\{ \.\.\.current\[id\], checking: true \}/);
assert.match(picker, /Math\.max\(900, Math\.ceil\(elapsed \/ 900\) \* 900\)/);
assert.match(picker, /\{ \.\.\.current\[id\], checking: false \}/);
assert.match(picker, /\.slice\(page \* SERVER_RESULT_WINDOW, \(page \+ 1\) \* SERVER_RESULT_WINDOW\)/);
assert.match(picker, /\.slice\(0, 30\)/);
assert.match(picker, /Math\.min\(index, 7\)/);
assert.match(picker, /type="search"/);
assert.match(picker, /harbor-server-favorites/);
assert.match(picker, /harbor-server-recent/);
assert.match(picker, /aria-expanded={!isCollapsed}/);
});
test('manual ping uses plain language and keeps results beside server names', () => {
assert.match(picker, /function ServerHealth/);
assert.match(picker, /<svg className="client-server-health-checking"/);
assert.doesNotMatch(picker, />Проверяем…<\/span>/);
assert.match(picker, /function ServerCheckButton/);
assert.match(picker, /client-server-check\$\{checking \? ' is-checking' : ''\}/);
assert.match(picker, /<span className="client-server-check-label">/);
assert.doesNotMatch(picker, /client-tooltip/);
assert.doesNotMatch(picker, /TCP|Не проверен/);
assert.match(picker, /<strong>\{server\.label\}<\/strong>[\s\S]*?<\/button>[\s\S]*?client-server-meta/);
assert.match(picker, /\[server\.city, server\.country\]\.filter\(Boolean\)\.join\(' · '\)/);
assert.match(picker, /is-scalable\$\{simpleServers\.length <= INLINE_SERVER_ROWS \? ' is-short' : ''\}/);
assert.match(styles, /\.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 120px minmax\(0, 1fr\);/);
assert.match(styles, /\.client-server-row \.client-server \{[\s\S]*?width: 120px;/);
assert.match(styles, /\.client-server-health \{[\s\S]*?font: var\(--type-label\);[\s\S]*?font-variant-numeric: var\(--numeric-tabular\);/);
assert.match(styles, /\.client-server-meta \.client-server-health \{[\s\S]*?place-items: end start;[\s\S]*?padding-bottom: 7px;/);
assert.match(styles, /\.client-server-health\.is-checking \.client-server-health-checking \{[\s\S]*?animation: client-spin 900ms linear infinite;/);
assert.match(styles, /\.client-server-check\.is-checking svg \{[\s\S]*?animation: client-spin 900ms linear infinite;/);
assert.match(styles, /\.client-server-check:hover:not\(:disabled\) svg[\s\S]*?transform: rotate\(90deg\);/);
assert.match(styles, /\.client-server-meta \{[\s\S]*?width: 42px;[\s\S]*?margin-left: 4px;/);
assert.match(styles, /\.client-server-favorite \{[\s\S]*?position: absolute;[\s\S]*?width: 44px;/);
assert.match(picker, /server={servers\[0\]}[\s\S]*?ping={pings\[servers\[0\]\.id\]}/);
assert.match(picker, /simpleServers\.map[\s\S]*?ping={pings\[server\.id\]}/);
});
test('server picker starts simple and reveals advanced controls on demand', () => {
assert.match(picker, /const \[advanced, setAdvanced\] = useState\(false\)/);
assert.match(picker, /aria-expanded={advanced}/);
assert.match(picker, /setAdvanced\(\(current\) => !current\)/);
assert.match(picker, /<span>Поиск и фильтры<\/span>/);
assert.match(picker, /client-server-toolbar-title">Список серверов/);
assert.doesNotMatch(picker, /client-server-toolbar-actions/);
assert.match(styles, /\.client-servers\.is-scalable \{[\s\S]*?width: min\(100%, 300px\);/);
assert.match(styles, /\.client-server-mode-toggle \{[\s\S]*?grid-row: 2;/);
assert.match(styles, /\.client-server-check \{[\s\S]*?grid-column: 3;[\s\S]*?grid-row: 1;/);
assert.match(styles, /\.client-server-auto \{[\s\S]*?width: 220px;[\s\S]*?margin-inline: auto;/);
assert.match(picker, /client-server-mode-panel is-simple/);
assert.match(picker, /client-server-mode-panel is-advanced/);
assert.match(picker, /inert={advanced \? true : undefined}/);
assert.match(picker, /inert={!advanced \? true : undefined}/);
assert.match(picker, /const INLINE_SERVER_ROWS = 8/);
assert.doesNotMatch(picker, /simpleServers[\s\S]*\.slice\(0,/);
assert.doesNotMatch(picker, /client-server-overflow-note|доступны через поиск/);
assert.match(picker, /selected && selected\.id !== anchorServerId && <ServerRow/);
assert.match(picker, /id !== selectedServerId && id !== anchorServerId/);
assert.match(overview, /anchorServerId=\{pickerState\.anchorServerId\}/);
assert.match(styles, /\.client-profile-body \.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 64px;/);
assert.match(styles, /\.client-profile-body \.client-server-row\.is-selected \{[\s\S]*?background: color-mix\(in oklch, var\(--client-accent\) 2%, transparent\);/);
assert.match(styles, /\.client-profile-body \.client-server-check-label \{[\s\S]*?display: inline;/);
assert.match(picker, /'--server-index': Math\.min\(index, 7\)[\s\S]*'--server-exit-delay'/);
assert.match(styles, /\.client-profile-body\.is-open \.client-server-row \{[\s\S]*client-profile-server-in[\s\S]*var\(--server-index\)/);
assert.match(styles, /\.client-profile-body\.is-open \.client-server-toolbar \{[\s\S]*client-profile-ping-in 300ms 620ms/);
assert.match(styles, /\.client-profile-body\.is-closing \.client-server-toolbar \{[\s\S]*client-profile-ping-out[\s\S]*\.client-profile-body\.is-closing \.client-server-row \{[\s\S]*var\(--server-exit-delay\)/);
assert.doesNotMatch(styles, /\.client-profile-body \.client-server-check::after/);
});