91 lines
5.7 KiB
JavaScript
91 lines
5.7 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import test from 'node:test';
|
||
import { fileURLToPath } from 'node:url';
|
||
import {
|
||
formatByteString,
|
||
formatLastSeen,
|
||
sortDevicesByTraffic,
|
||
} from '../../src/web/utils/format.js';
|
||
|
||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||
const overview = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.jsx'), 'utf8');
|
||
const panel = fs.readFileSync(path.join(root, 'src/web/components/DevicesPanel.jsx'), 'utf8');
|
||
const api = fs.readFileSync(path.join(root, 'src/web/api.js'), 'utf8');
|
||
const server = fs.readFileSync(path.join(root, 'src/server/index.js'), 'utf8');
|
||
const styles = fs.readFileSync(path.join(root, 'src/web/styles.css'), 'utf8');
|
||
|
||
test('Gateway device inventory uses the existing accessible responsive drawer', () => {
|
||
assert.match(overview, /isGateway && <button[\s\S]*client-devices-toggle/);
|
||
assert.match(panel, /api\.devices\.list\(\)/);
|
||
assert.match(panel, /api\.devices\.refresh\(\)/);
|
||
assert.match(panel, /api\.devices\.update\(device\.id, patch, snapshot\.revision\)/);
|
||
assert.match(panel, /requestError\.code !== 'STATE_CONFLICT'[\s\S]*api\.devices\.list\(\)[\s\S]*Object\.keys\(patch\)\.some\(\(key\) => latestDevice\[key\] !== device\[key\]\)[\s\S]*api\.devices\.update\(device\.id, patch, latest\.revision\)/);
|
||
assert.match(panel, /deviceNodes\.current\.get\(id\)\?\.animate/);
|
||
assert.match(panel, /prefers-reduced-motion: reduce/);
|
||
assert.match(api, /refresh: \(\) => request\('\/api\/devices\/refresh', \{ method: 'POST' \}\)/);
|
||
assert.match(api, /setPolicy: \(id, mode, expectedRevision\) => request\(`\/api\/devices\/\$\{id\}\/policy`/);
|
||
assert.match(server, /requestUrl\.pathname === '\/api\/devices\/refresh'[\s\S]*deviceInventory\.refresh\(\)/);
|
||
assert.match(server, /\/api\\\/devices\\\/\(dev_\[a-f0-9\]\{16\}\)\\\/policy\$[\s\S]*deviceInventory\.setPolicy/);
|
||
assert.match(panel, /<Tooltip>Изменить название<\/Tooltip>/);
|
||
assert.match(panel, /<TextMorph from=\{seen\.label\} to=\{seen\.relative\} \/>/);
|
||
assert.doesNotMatch(panel, /client-text-morph-goo/);
|
||
assert.match(panel, /client-device-addresses/);
|
||
assert.doesNotMatch(panel, /device\.interface/);
|
||
assert.doesNotMatch(panel, /device\.mac/);
|
||
assert.match(panel, /device\.confidence === 'ambiguous'/);
|
||
assert.match(panel, /sortDevicesByTraffic\(snapshot\?\.devices, sortDirection\)/);
|
||
assert.match(panel, /Трафик временно не обновляется/);
|
||
assert.match(panel, /Получено \$\{download\}, отдано \$\{upload\}/);
|
||
assert.match(panel, /client-device-traffic-slot[\s\S]*client-device-pin-wrap/);
|
||
assert.doesNotMatch(panel, /client-device-details/);
|
||
assert.match(panel, /api\.devices\.setPolicy\(device\.id, mode, snapshot\.revision\)/);
|
||
assert.match(panel, /requestError\.code !== 'STATE_CONFLICT'[\s\S]*latestDevice\.desiredPolicy !== device\.desiredPolicy[\s\S]*api\.devices\.setPolicy\(device\.id, mode, latest\.revision\)/);
|
||
assert.match(panel, /className=\{`client-device-policy is-\$\{displayPolicy\}/);
|
||
assert.match(panel, /Полностью обходит sing-box/);
|
||
assert.match(panel, /client-drawer client-instructions client-devices/);
|
||
assert.doesNotMatch(panel, /точная MAC|частная MAC|<dt>Источник<\/dt>/);
|
||
assert.match(panel, /aria-pressed=\{device\.pinned\}/);
|
||
assert.match(panel, /maxLength="64"[\s\S]*autoFocus/);
|
||
assert.match(styles, /\.client-devices \{\s*width: min\(560px, 100vw\)/);
|
||
assert.match(styles, /\.client-device \{[\s\S]*gap: 3px;[\s\S]*padding: 10px 8px/);
|
||
assert.match(styles, /\.client-device-heading \{[\s\S]*grid-template-columns: 60px minmax\(0, 1fr\) auto 32px/);
|
||
assert.match(styles, /\.client-device-meta \{[\s\S]*grid-template-columns: minmax\(0, 1fr\) 72px auto/);
|
||
assert.match(styles, /\.client-text-morph-value \{[\s\S]*transition: opacity 360ms[\s\S]*filter 480ms/);
|
||
assert.match(styles, /\.client-device-last-seen:hover \.client-text-morph-value\.is-relative[\s\S]*opacity: 1/);
|
||
assert.match(styles, /\.client-device-pin-wrap\.client-tooltip-anchor:hover > \.client-tooltip[\s\S]*translate\(0, 0\)/);
|
||
assert.match(styles, /\.client-devices-refresh-ring circle[\s\S]*client-devices-refresh-progress 15s/);
|
||
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy[\s\S]*\.client-text-morph-value/);
|
||
assert.match(styles, /\.client-drawer \{[\s\S]*z-index: 50;[\s\S]*box-shadow:/);
|
||
assert.match(styles, /\.harbor-versions \{[\s\S]*z-index: 40/);
|
||
});
|
||
|
||
test('device last-seen copy is compact with precise accessible and relative forms', () => {
|
||
const lastSeen = new Date(2026, 7, 7, 13, 15);
|
||
const now = new Date(2026, 7, 7, 13, 27);
|
||
assert.deepEqual(
|
||
formatLastSeen(lastSeen.toISOString(), now),
|
||
{
|
||
label: 'Сегодня, 13:15',
|
||
relative: '12 минут назад',
|
||
tooltip: '7 августа 2026 г. в 13:15 (12 минут назад)',
|
||
},
|
||
);
|
||
});
|
||
|
||
test('device traffic formatting and sorting preserve uint64 precision and canonical ties', () => {
|
||
assert.equal(formatByteString('9007199254740993'), '8,0 ПБ');
|
||
assert.equal(formatByteString('1536'), '1,5 КБ');
|
||
assert.equal(formatByteString('invalid'), '0 Б');
|
||
|
||
const devices = [
|
||
{ id: 'a', uploadBytes: '9007199254740993', downloadBytes: '0' },
|
||
{ id: 'b', uploadBytes: '9007199254740992', downloadBytes: '2' },
|
||
{ id: 'c', uploadBytes: '10', downloadBytes: '10' },
|
||
{ id: 'd', uploadBytes: '15', downloadBytes: '5' },
|
||
];
|
||
assert.deepEqual(sortDevicesByTraffic(devices, 'desc').map(({ id }) => id), ['b', 'a', 'c', 'd']);
|
||
assert.deepEqual(sortDevicesByTraffic(devices, 'asc').map(({ id }) => id), ['c', 'd', 'a', 'b']);
|
||
});
|