Files
harbor-net/test/web/device-inventory-contract.test.js
T
dokril 9f31eaf396
Build and Deploy Gateway / build-and-push (push) Successful in 13s
Build and Deploy Gateway / deploy (push) Successful in 7s
Simplify device status display and bump client versions
2026-08-07 16:33:02 +03:00

96 lines
6.2 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';
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, /online \? 'В сети' : <TextMorph from=\{seen\.label\} to=\{seen\.relative\} \/>/);
assert.match(panel, /aria-label=\{`\$\{online \? 'В сети' : 'Не в сети'\}\. Последний контакт: \$\{seen\.tooltip\}`\}/);
assert.doesNotMatch(panel, /client-device-status|STATUS_LABELS/);
assert.doesNotMatch(panel, /client-text-morph-goo/);
assert.match(panel, /client-device-addresses/);
assert.doesNotMatch(panel, /device\.interface/);
assert.doesNotMatch(panel, /device\.mac/);
assert.doesNotMatch(panel, /client-device-identity|Пояснение идентификации устройства|ⓘ/);
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: minmax\(0, 1fr\) auto 32px/);
assert.match(styles, /\.client-device-meta \{[\s\S]*grid-template-columns: minmax\(0, 1fr\) 72px auto/);
assert.doesNotMatch(styles, /\.client-device-meta \{[^}]*margin-left/);
assert.match(styles, /\.client-device-last-seen\.is-online \{[\s\S]*color: var\(--client-accent\)/);
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']);
});