Animate vertical transitions between client drawers
Build and Deploy Gateway / build-and-push (push) Successful in 22s
Build and Deploy Gateway / deploy (push) Successful in 6s

This commit is contained in:
2026-08-14 15:23:00 +03:00
parent 019930924d
commit 32217f4d17
17 changed files with 260 additions and 93 deletions
+2 -1
View File
@@ -139,7 +139,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(panel, /\{!compact && <TrafficChart/);
assert.doesNotMatch(panel, /Закрепите устройство, чтобы изменить маршрут|Сначала верните маршрут через Gateway/);
assert.match(panel, /maxLength=\{64\}[\s\S]*autoFocus/);
assert.match(styles, /\.client-devices \{\s*width: min\(580px, 100vw\)/);
assert.match(styles, /\.client-drawer \{[\s\S]*width: min\(580px, 100vw\)/);
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/);
@@ -207,6 +207,7 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(styles, /\.client-devices-kicker \{[^}]*flex-wrap: nowrap/);
assert.match(styles, /\.client-devices-reset-wrap \{[^}]*margin-left: auto/);
assert.match(styles, /\.client-devices-reset-wrap > \.client-tooltip \{[^}]*white-space: nowrap/);
assert.match(styles, /\.client-devices-reset-wrap\.client-tooltip-anchor:hover > \.client-tooltip,[\s\S]*transform: translate\(0, 0\)/);
assert.match(styles, /\.client-devices-reset \{[\s\S]*oklch\(0\.68 0\.15 28\)[\s\S]*\.client-devices-reset:hover:not\(:disabled\) svg[\s\S]*rotate\(-360deg\)/);
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-device-policy svg[\s\S]*\.client-device-alias-trigger[\s\S]*\.client-device-identity-details[\s\S]*\.client-device-identity-copy[\s\S]*\.client-device-traffic-value > span[\s\S]*\.client-device-traffic-breakdown[\s\S]*\.client-device-traffic-lines[\s\S]*\.client-text-morph-value[\s\S]*transition: none;[\s\S]*animation: none/);
assert.match(styles, /\.client-drawer \{[\s\S]*z-index: 50;[\s\S]*box-shadow:/);
+1 -1
View File
@@ -37,7 +37,7 @@ test('device controller preserves Gateway-only polling, monotonic publication an
assert.match(feature, /resetOpen[\s\S]*if \(resetOpen\) return/);
assert.match(feature, /resetDeviceTraffic\(snapshot\.revision\)[\s\S]*STATE_CONFLICT[\s\S]*resetDeviceTraffic\(latest\.revision\)/);
assert.match(panel, /<ConfirmationDialog[\s\S]*title="Сбросить статистику\?"[\s\S]*confirmLabel="Сбросить"[\s\S]*busy=\{resetting\}/);
assert.match(page, /<DevicesToggle[\s\S]*devicesFeature\.toggle\(\)/);
assert.match(page, /<DevicesToggle[\s\S]*onToggle=\{\(\) => switchDrawer\('devices'\)\}/);
assert.match(page, /<GatewayTrafficSummary feature=\{devicesFeature\} now=\{now\}/);
});
+13 -9
View File
@@ -76,6 +76,7 @@ test('server rows scroll inside the subscription drawer without moving the main
const grid = rule('.client-server-grid');
assert.match(scroll, /overflow-y:\s*auto/);
assert.match(scroll, /overflow-x:\s*hidden/);
assert.match(scroll, /scrollbar-width:\s*none/);
assert.match(styles, /\.client-server-scroll::-webkit-scrollbar\s*\{[\s\S]*display:\s*none/);
assert.match(simpleScroll, /max-height:\s*352px/);
@@ -83,6 +84,7 @@ test('server rows scroll inside the subscription drawer without moving the main
assert.match(simpleScroll, /scrollbar-width:\s*thin/);
assert.match(grid, /width:\s*min\(100%, 220px\)/);
assert.match(rule('.client-drawer'), /overflow-y:\s*auto/);
assert.match(rule('.client-drawer'), /overflow-x:\s*hidden/);
assert.match(subscription, /client-profile-list[\s\S]*feature\.profiles\.map\(\(profile\) => <ProfileGroup/);
assert.match(rule('.client-server-toolbar-title'), /grid-column:\s*2/);
assert.match(rule('.client-server-mode-toggle'), /grid-row:\s*2/);
@@ -110,10 +112,7 @@ test('tablet and mobile regions use normal flow with viewport-safe widths', () =
assert.match(mobile, /\.client-secondary-menu \{[\s\S]*right:\s*8px/);
assert.match(mobile, /\.client-power-control > \.harbor-brand \{[\s\S]*scale\(1\.35\)/);
assert.match(mobile, /\.client-server-check \{[\s\S]*width:\s*44px/);
assert.match(styles, /\.client-instructions\s*\{[\s\S]*width:\s*min\(470px, 100vw\)/);
assert.match(styles, /\.client-local-rules\s*\{[\s\S]*width:\s*min\(480px, 100vw\)/);
assert.match(styles, /\.client-instructions\.client-diagnostics\s*\{[\s\S]*width:\s*min\(580px, 100vw\)/);
assert.match(styles, /\.client-subscription-drawer\s*\{[\s\S]*width:\s*min\(480px, 100vw\)/);
assert.match(rule('.client-drawer'), /width:\s*min\(580px, 100vw\)/);
assert.match(styles, /\.client-confirmation-dialog\s*\{[\s\S]*width:\s*min\(430px, calc\(100vw - 48px\)\)/);
for (const viewport of [320, 390, 768]) {
@@ -128,11 +127,11 @@ test('tablet and mobile regions use normal flow with viewport-safe widths', () =
assert.ok(Math.min(360, desktopOuterColumn) <= desktopOuterColumn, 'details fit the 1440px desktop grid');
});
test('secondary menus share one right rail and both drawers open from the right', () => {
test('secondary menus share one right rail and switch equal drawers as a vertical tape', () => {
const disabledRulesLabel = rule('.client-local-rules-toggle:disabled span');
const zIndex = (selector) => Number(/z-index:\s*(\d+)/.exec(rule(selector))?.[1]);
assert.match(component, /<nav className="client-secondary-menu" aria-label="Дополнительные меню">/);
assert.match(component, /<nav[\s\S]*className="client-secondary-menu"[\s\S]*aria-label="Дополнительные меню"[\s\S]*onPointerDown=\{\(event\) => event\.stopPropagation\(\)\}/);
assert.match(component, /<SubscriptionToggle[\s\S]*<InstructionsToggle[\s\S]*<DevicesToggle/);
assert.match(subscription, /controls="client-subscription-drawer"/);
assert.match(component, /<InstructionsToggle[\s\S]*<RoutingToggle/);
@@ -160,6 +159,7 @@ test('secondary menus share one right rail and both drawers open from the right'
assert.match(styles, /@keyframes client-rail-info-refill[\s\S]*stroke-dashoffset: 1[\s\S]*stroke-dashoffset: 0/);
assert.doesNotMatch(styles, /client-secondary-icon-motion|\.client-local-rules-toggle\.is-open svg\s*\{[^}]*rotate/);
assert.match(rule('.client-drawer'), /inset:\s*0 0 0 auto/);
assert.match(rule('.client-drawer'), /width:\s*min\(580px, 100vw\)/);
assert.match(rule('.client-drawer'), /transform:\s*translateX\(104%\)/);
assert.match(rule('.client-drawer'), /z-index:\s*50/);
assert.match(rule('.client-drawer'), /box-shadow:/);
@@ -167,12 +167,16 @@ test('secondary menus share one right rail and both drawers open from the right'
['.client-confirmation-popup', '.client-secondary-menu', '.client-drawer', '.harbor-versions'].map(zIndex),
[100, 60, 50, 40],
);
assert.match(rule('.client-instructions'), /width:\s*min\(470px, 100vw\)/);
assert.match(rule('.client-local-rules'), /width:\s*min\(480px, 100vw\)/);
assert.match(styles, /@media \(min-width: 921px\) \{[\s\S]*\.client-drawer \{[\s\S]*right:\s*64px;[\s\S]*width:\s*min\(580px, calc\(100vw - 64px\)\)/);
assert.match(instructions, /<Drawer[\s\S]*className="client-instructions"/);
assert.match(routing, /<Drawer[\s\S]*className="client-local-rules"/);
assert.match(subscription, /<Drawer[\s\S]*className="client-subscription-drawer"/);
assert.match(component, /subscriptionFeature\.close\(\)[\s\S]*devicesFeature\.close\(\)[\s\S]*diagnosticsFeature\.close\(\)[\s\S]*instructionsFeature\.toggle\(\)/);
assert.match(component, /const DRAWER_ORDER = \['subscription', 'instructions', 'devices', 'diagnostics', 'routing'\]/);
assert.match(component, /function switchDrawer\(target: DrawerKey\)[\s\S]*from\.inert = true[\s\S]*translateY\(\$\{direction \* 100\}%\)[\s\S]*translateY\(\$\{-direction \* 100\}%\)/);
assert.match(component, /const cancel = \(\) => \{[\s\S]*from\.inert = false;[\s\S]*from\.removeAttribute\('aria-hidden'\)/);
assert.match(component, /DRAWER_SWITCH_MS = 620[\s\S]*prefers-reduced-motion: reduce[\s\S]*flushSync/);
assert.match(component, /current === 'routing' && routingFeature\.dirty[\s\S]*routingFeature\.requestClose\(\)/);
assert.doesNotMatch(component, /instructionsFeature\.close\(\)[\s\S]{0,180}devicesFeature\.close\(\)[\s\S]{0,180}subscriptionFeature\.toggle\(\)/);
});
test('connectivity diagnostics render stable compact tables before the first run', () => {
+2 -2
View File
@@ -37,8 +37,8 @@ test('routing lifecycle and Page orchestration keep the existing guards and bloc
assert.match(feature, /matchMedia\('\(prefers-reduced-motion: reduce\)'\)[\s\S]*removing: true/);
assert.match(feature, /document\.startViewTransition\(update\)/);
assert.match(feature, /operationBlocked\(operations, 'routeRules'\) \|\| rules\.some/);
assert.match(page, /routingFeature\.isOpen && !routingFeature\.requestClose\(\)/);
assert.match(page, /function openRouting\(\) \{[\s\S]*subscriptionFeature\.close\(\)[\s\S]*instructionsFeature\.close\(\)[\s\S]*devicesFeature\.close\(\)[\s\S]*diagnosticsFeature\.close\(\)[\s\S]*routingFeature\.open\(\)/);
assert.match(page, /current === 'routing' && routingFeature\.dirty[\s\S]*routingFeature\.requestClose\(\)/);
assert.match(page, /routing:[\s\S]*show: routingFeature\.open,[\s\S]*close: routingFeature\.forceClose/);
assert.match(page, /routingSlot=\{<RoutingPendingStatus[\s\S]*blocked=\{connectionBlocked\}/);
assert.match(page, /<RoutingPanel[\s\S]*InlineError[\s\S]*InlineProgress/);
});
+10 -10
View File
@@ -37,24 +37,24 @@ const expectedImports = [
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const acceptedLedger = {
counts: {
cascadeEdges: 822,
cascadeEdges: 818,
customProperties: 103,
declarations: 3302,
declarations: 3300,
important: 0,
keyframes: 56,
media: 13,
rules: 952,
rules: 947,
variableReferences: 808,
},
hashes: {
cascadeEdges: '183435ac9241f0507dd2d82eda73e71eaaee8f8dcbdc0747b83cf8e17e2ce9f7',
cascadeEdges: 'e5115e193b09067562309034ecfed8dd35d7435562b581036da00190dfd95650',
customProperties: 'ef70fb1d9b61b177f1939f811babe1116bee631de26f5eac0aa0d0009ca5a1fb',
declarations: '07ab8e4c442a88368e6727f0665b29a2d27a882d64c38647250a0f4f0c069f6e',
declarations: '3e6d3c24e0df278909fd0200470193378866819c1a62a6d0404e74297cab45d7',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: '0fa66e370695261a2a5c6a189752c07d518d80d68d3e79a3ba4bb34407893c0e',
keyframes: 'd4378751f36eccc87923c12540315462055032a5d34978a0f45ecada0a5cc1ce',
ruleDeclarationSequences: '864dd1f50b04e6e07e30be6e4aa3b72d02d5425dbecf74a28afa6280ce9dfd73',
selectors: 'fa5bb2e06dcbb72802e49d67e2312504f27e87e855427498526ea24f143f84c8',
ruleDeclarationSequences: 'ef07abd92d371c865e70dc8e709a396ec46056164035594a8cddda6b0f61a629',
selectors: '085df6f266f22b99fc429c089b3142f12fd6f74bfb9b86233c55bf8baa92469c',
variableReferences: '664c093241c4076ca05a9dda06ae88d4e7024c926c4636109790201df8212df4',
witnesses: 'ac771c94e7f2c409c3d317de610c2dae5722eb1d6f0b6626a4e1aa986c83fd94',
},
@@ -405,8 +405,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
assert.deepEqual(assets, ['index-DbgBuebh.css']);
assert.deepEqual(assets, ['index-DhqvaTQ4.css']);
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
assert.equal(built.byteLength, 127802);
assert.equal(sha256(built), '19995a47de1ae634f29b26fb4e484f6d9fb048519cc42ad0a186d11904e2bfb6');
assert.equal(built.byteLength, 127639);
assert.equal(sha256(built), 'c5e9ec300671c6660fb23bfaa725a53b165dd08e387362cfac3fbcf709a8d9e9');
});
@@ -99,8 +99,8 @@ test('feature keeps exact slots, truthy closes and subscription DOM order', () =
assert.match(feature, /feature\.profiles\.length === 0 && <div className="client-form client-subscription-first-run"/);
assert.match(page, /<SubscriptionPanel[\s\S]*statusSlot=\{<>[\s\S]*InlineError[\s\S]*renderServerPicker=[\s\S]*<ServerPicker/);
assert.doesNotMatch(page, /<InlineProgress[^>]*context="subscription"/);
assert.match(page, /<SubscriptionToggle[\s\S]*subscriptionFeature\.toggle\(\)/);
assert.match(page, /subscriptionFeature\.close\(\)[\s\S]*devicesFeature\.close\(\)[\s\S]*diagnosticsFeature\.close\(\)/);
assert.match(page, /<SubscriptionToggle[\s\S]*onToggle=\{\(\) => switchDrawer\('subscription'\)\}/);
assert.match(page, /subscription: \{[\s\S]*show: subscriptionFeature\.toggle,[\s\S]*close: subscriptionFeature\.close/);
assert.doesNotMatch(feature, /setInterval|copyText|client-live-region/);
});
@@ -129,6 +129,7 @@ test('profiles render as flat accordion groups with scoped controls', () => {
assert.match(styles, /\.client-profile-body/);
assert.match(styles, /\.client-profile-body \{[\s\S]*grid-template-rows: 0fr[\s\S]*\.client-profile-body\.is-open \{[\s\S]*grid-template-rows: 1fr/);
assert.match(styles, /\.client-profile-body-inner \{[\s\S]*padding: 0 0 4px 24px/);
assert.match(styles, /\.client-profile-selected-server \{[\s\S]*padding: 0 8px 0 32px/);
assert.match(styles, /\.client-profile-group\.is-expired \.client-profile-title small \{[\s\S]*color: oklch\(0\.68 0\.14 72\)/);
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-profile-body \*/);
assert.match(styles, /\.client-profile-refresh\.is-refreshing/);