Implement Harbor gateway device ecosystem support
This commit is contained in:
@@ -5,6 +5,8 @@ import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
formatByteString,
|
||||
deviceFilterCounts,
|
||||
filterDevices,
|
||||
formatLastSeen,
|
||||
isNewDevice,
|
||||
positiveByteDelta,
|
||||
@@ -38,7 +40,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
||||
assert.match(feature, /requestDeviceUpdate\(device\.id, patch, snapshot\.revision\)/);
|
||||
assert.match(feature, /requestError\(caught\)\.code !== 'STATE_CONFLICT'[\s\S]*listDevices\(\)[\s\S]*Object\.keys\(patch\)\.some\(\(key\) => latestDevice\[key\] !== device\[key\]\)[\s\S]*requestDeviceUpdate\(device\.id, patch, latest\.revision\)/);
|
||||
assert.match(panel, /deviceNodes\.current\.get\(id\)\?\.animate/);
|
||||
assert.match(panel, /movementAnimations\.current\.get\(id\)\?\.cancel\(\)/);
|
||||
assert.match(panel, /const activeAnimation = movementAnimations\.current\.get\(id\)[\s\S]*activeAnimation\.commitStyles\(\)[\s\S]*activeAnimation\.cancel\(\)[\s\S]*interruptedPositions\.set/);
|
||||
assert.match(panel, /previousPositions\.current\.size > 0/);
|
||||
assert.match(panel, /previousScrollTop\.current - currentScrollTop/);
|
||||
assert.match(feature, /next\.revision > current\.revision/);
|
||||
@@ -86,7 +88,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
||||
assert.match(panel, /\{device\.hostname && <button[\s\S]*copyDeviceValue\(device, 'Hostname', device\.hostname!\)/);
|
||||
assert.doesNotMatch(panel, /identityTooltipId|Hostname: \{device\.hostname/);
|
||||
assert.match(panel, /device\.confidence === 'ambiguous'/);
|
||||
assert.match(panel, /stabilizeDevicesByTraffic\(snapshot\?\.devices, sortDirection, previousIds\)/);
|
||||
assert.match(panel, /filterDevices\(allDevices, tags,[\s\S]*stabilizeDevicesByTraffic\(filteredDevices, sortDirection, previousIds\)[\s\S]*stabilizeDevicesByTraffic\(allDevices, sortDirection, previousIds\)[\s\S]*trafficOrder\.current = \{ direction: sortDirection, ids: canonical\.ids \}/);
|
||||
assert.match(panel, /Трафик временно не обновляется/);
|
||||
assert.match(panel, /Вход — накопленные Gateway\/Proxy\. Выход — приблизительно через VPN или Direct с запуска текущего учёта\./);
|
||||
assert.match(panel, /const displayedTotal = trafficView === 'outbound'[\s\S]*`≈ \$\{formatByteString\(outboundTotal\.toString\(\)\)\}`[\s\S]*const trafficLabel = trafficView === 'outbound' \? 'Выход' : 'Вход'/);
|
||||
@@ -144,6 +146,8 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
|
||||
assert.doesNotMatch(panel, /Закрепите устройство, чтобы изменить маршрут|Сначала верните маршрут через Gateway/);
|
||||
assert.match(panel, /maxLength=\{64\}[\s\S]*autoFocus/);
|
||||
assert.match(styles, /\.client-drawer \{[\s\S]*width: min\(580px, 100vw\)/);
|
||||
assert.match(styles, /\.client-drawer\.client-devices \{[\s\S]*width: min\(780px, 100vw\)/);
|
||||
assert.match(styles, /\.client-devices-layout \{[\s\S]*grid-template-columns: 168px minmax\(0, 1fr\);[\s\S]*gap: 28px/);
|
||||
assert.match(styles, /\.client-device \{[\s\S]*--client-device-chart-height: 34px;[\s\S]*grid-template-columns: 34px minmax\(0, 1fr\) 112px 34px;[\s\S]*grid-template-rows: 34px var\(--client-device-chart-height\);[\s\S]*padding: 10px 8px/);
|
||||
assert.match(styles, /\.client-device\.is-pinned \{[\s\S]*--client-device-chart-height: 72px/);
|
||||
assert.match(styles, /\.client-device\.is-deprioritized \{[\s\S]*grid-template-rows: 34px;[\s\S]*padding-block: 6px/);
|
||||
@@ -231,6 +235,41 @@ test('device copy feedback stays keyed per device', () => {
|
||||
assert.match(panel, /const feedback = copyFeedback\[device\.id\]/);
|
||||
});
|
||||
|
||||
test('device tags keep filtering, assignment and management accessible at every width', () => {
|
||||
assert.match(panel, /Найти устройство/);
|
||||
assert.match(panel, /'all', label: 'Все'[\s\S]*'new', label: 'Новые'[\s\S]*'pinned', label: 'Закреплённые'[\s\S]*'background', label: 'Фоновые'[\s\S]*'untagged', label: 'Без тегов'/);
|
||||
assert.match(panel, /if \(value === 'untagged'\) setSelectedTagIds\(\[\]\)/);
|
||||
assert.match(panel, /if \(systemFilter === 'untagged'\) setSystemFilter\('all'\)/);
|
||||
assert.match(panel, /deviceFilterCounts\(allDevices, tags\)/);
|
||||
assert.match(panel, /По этим фильтрам устройств нет\.[\s\S]*Сбросить фильтры/);
|
||||
assert.match(panel, /deviceTags = tags\.filter[\s\S]*client-device-tag-trigger[\s\S]*\+\{deviceTags\.length - 1\}[\s\S]*'\+ тег'/);
|
||||
assert.match(panel, /role="dialog"[\s\S]*aria-modal="true"[\s\S]*Теги устройства \{tagPopover\.title\}/);
|
||||
assert.match(panel, /contentRef\.current\.inert = true[\s\S]*layoutRef\.current\.inert = true/);
|
||||
assert.match(panel, /FOCUSABLE[\s\S]*event\.key === 'Escape'[\s\S]*document\.activeElement === first/);
|
||||
assert.match(panel, /catalogChanged[\s\S]*Список тегов изменился\. Откройте теги устройства снова\./);
|
||||
assert.match(panel, /Теги доступны после обновления Gateway\./);
|
||||
assert.match(panel, /Управление тегами[\s\S]*Переименовать тег[\s\S]*Удалить тег/);
|
||||
assert.match(panel, /placeholder="Найти устройство"[\s\S]*Результатов: \{devices\.length\}/);
|
||||
assert.match(panel, />Назад<\/button>[\s\S]*Не удалось сохранить теги\./);
|
||||
assert.match(panel, /restoreTagTriggerFocus[\s\S]*searchRef\.current,[\s\S]*filterButtonRef\.current,[\s\S]*candidate\?\.isConnected && !candidate\.closest\('\[inert\]'\)/);
|
||||
assert.match(panel, /updateDeviceTags\(device, tagPopover\.draft, tagPopover\.baseline\)/);
|
||||
assert.match(feature, /updateDeviceTags\(device: Device, tagIds: string\[\], baselineTagIds: string\[\]\)[\s\S]*sameStringList\(currentDevice\.tagIds, baselineTagIds\)[\s\S]*sameStringList\(latestDevice\.tagIds, baselineTagIds\)/);
|
||||
assert.match(feature, /renameTag\(tag: DeviceTag, name: string, baselineName: string\)[\s\S]*snapshot\.tags\.find[\s\S]*name !== baselineName[\s\S]*latestTag\.name !== baselineName/);
|
||||
assert.match(panel, /Тег больше не будет доступен для назначения\. Устройства и маршруты не изменятся\./);
|
||||
assert.match(panel, /Тег исчезнет у одного устройства\. Само устройство и его маршрут не изменятся\./);
|
||||
assert.match(panel, /Сами устройства и их маршруты не изменятся\./);
|
||||
assert.match(feature, /deleteDeviceTag\(tag\.id, snapshot\.revision\)[\s\S]*STATE_CONFLICT[\s\S]*listDevices\(\)[\s\S]*return 'conflict'/);
|
||||
assert.doesNotMatch(feature, /deleteDeviceTag\(tag\.id, latest\.revision\)/);
|
||||
assert.match(styles, /@media \(max-width: 640px\)[\s\S]*width: min\(300px, calc\(100vw - 48px\)\)[\s\S]*translateX\(-100%\)[\s\S]*260ms cubic-bezier\(0\.16, 1, 0\.3, 1\)/);
|
||||
assert.match(panel, /activeAnimation\.commitStyles\(\)[\s\S]*activeAnimation\.cancel\(\)[\s\S]*opacity: 0, transform: 'translateX\(-6px\)'[\s\S]*duration: 160[\s\S]*opacity: 0, transform: 'translateX\(6px\)'[\s\S]*opacity: 1, transform: 'translateX\(0\)'/);
|
||||
assert.match(styles, /client-device-tag-popover-in 160ms ease[\s\S]*translateY\(-6px\)[\s\S]*client-device-tag-popover-out 160ms ease forwards/);
|
||||
assert.match(styles, /client-devices-rail-backdrop \{[\s\S]*opacity: 0[\s\S]*opacity 260ms cubic-bezier\(0\.16, 1, 0\.3, 1\)[\s\S]*client-devices-rail-backdrop\.is-open[\s\S]*opacity: 1/);
|
||||
assert.match(styles, /client-device-tag-trigger \{[\s\S]*height: 14px[\s\S]*flex: 0 0 auto/);
|
||||
assert.match(styles, /@media \(hover: hover\)[\s\S]*client-device-tag-trigger:not\(\.has-tag\)[\s\S]*opacity: 0/);
|
||||
assert.match(styles, /@media \(hover: none\)[\s\S]*client-device-tag-trigger:not\(\.has-tag\)[\s\S]*opacity: 1/);
|
||||
assert.match(styles, /prefers-reduced-motion: reduce[\s\S]*client-devices-rail[\s\S]*client-devices-rail-view[\s\S]*client-device-tag-popover/);
|
||||
});
|
||||
|
||||
test('Gateway Home reuses the canonical device snapshot for applied route and global traffic', () => {
|
||||
const powerStart = connection.indexOf('<section className={`client-power-section');
|
||||
const trafficStart = overview.indexOf('<GatewayTrafficSummary');
|
||||
@@ -285,6 +324,54 @@ test('new-device marker uses an exact seven-day window', () => {
|
||||
assert.equal(isNewDevice('not-a-date', now), false);
|
||||
});
|
||||
|
||||
test('device filters combine system and search with tag OR while counts stay absolute', () => {
|
||||
const now = Date.parse('2026-08-31T12:00:00.000Z');
|
||||
const tags = [
|
||||
{ id: 'tag_0000000000000001', name: 'Умный дом' },
|
||||
{ id: 'tag_0000000000000002', name: 'Работа' },
|
||||
];
|
||||
const devices = Array.from({ length: 100 }, (_, index) => ({
|
||||
id: `device-${index}`,
|
||||
alias: index === 3 ? 'Телевизор' : `Клиент ${index}`,
|
||||
hostname: index === 4 ? 'office-mac.local' : null,
|
||||
ip: `192.168.50.${index + 1}`,
|
||||
mac: `00:11:22:33:44:${index.toString(16).padStart(2, '0')}`,
|
||||
firstSeenAt: new Date(now - (index < 7 ? index : 10) * 86_400_000).toISOString(),
|
||||
pinned: index === 3,
|
||||
deprioritized: index === 5,
|
||||
tagIds: index === 3 ? [tags[0].id] : index === 4 ? [tags[1].id] : [],
|
||||
}));
|
||||
|
||||
assert.deepEqual(filterDevices(devices, tags, {
|
||||
system: 'all', tagIds: tags.map(({ id }) => id), query: 'local',
|
||||
}, now).map(({ id }) => id), ['device-4']);
|
||||
assert.deepEqual(filterDevices(devices, tags, {
|
||||
system: 'pinned', tagIds: [tags[0].id, tags[1].id], query: 'умный',
|
||||
}, now).map(({ id }) => id), ['device-3']);
|
||||
assert.equal(filterDevices(devices, tags, {
|
||||
system: 'untagged', tagIds: [], query: '192.168.50.',
|
||||
}, now).length, 98);
|
||||
|
||||
assert.deepEqual(deviceFilterCounts(devices, tags, now), {
|
||||
all: 100,
|
||||
new: 7,
|
||||
pinned: 1,
|
||||
background: 1,
|
||||
untagged: 98,
|
||||
byTag: { [tags[0].id]: 1, [tags[1].id]: 1 },
|
||||
});
|
||||
|
||||
const canonical = stabilizeDevicesByTraffic(devices, 'desc');
|
||||
const visible = filterDevices(devices, tags, {
|
||||
system: 'all', tagIds: [], query: '192.168.50.1',
|
||||
}, now);
|
||||
const filteredOrder = stabilizeDevicesByTraffic(visible, 'desc', canonical.ids).devices;
|
||||
assert.deepEqual(filteredOrder.map(({ id }) => id), canonical.devices
|
||||
.filter(({ id }) => visible.some((device) => device.id === id))
|
||||
.map(({ id }) => id));
|
||||
assert.deepEqual(stabilizeDevicesByTraffic(devices, 'desc', canonical.ids).ids, canonical.ids);
|
||||
});
|
||||
|
||||
test('device traffic formatting and sorting preserve uint64 precision and canonical ties', () => {
|
||||
assert.equal(formatByteString('9007199254740993'), '8,0 ПБ');
|
||||
assert.equal(formatByteString('1536'), '1,5 КБ');
|
||||
|
||||
Reference in New Issue
Block a user