Files
harbor-net/test/web/responsive-layout-contract.test.js
Dmitriy Petrov e5a69dcb73
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 18s
Build and Deploy Gateway / deploy (push) Successful in 7s
Bump Harbor clients and clarify disabled local rules
2026-07-14 21:55:37 +03:00

134 lines
7.7 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';
const root = path.resolve(import.meta.dirname, '../..');
const styles = fs.readFileSync(path.join(root, 'src/web/styles.css'), 'utf8');
const component = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.jsx'), 'utf8');
function rule(selector, source = styles) {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`^${escaped}\\s*\\{([\\s\\S]*?)\\n\\}`, 'm').exec(source)?.[1] || '';
}
test('desktop layout keeps the power control on a symmetric center axis', () => {
const panel = rule('.client-panel');
const power = rule('.client-power-section');
const form = rule('.client-form');
assert.match(panel, /grid-template-columns:\s*minmax\(0, 1fr\) minmax\(240px, 280px\) minmax\(0, 1fr\)/);
assert.match(power, /grid-column:\s*2/);
assert.match(form, /grid-column:\s*3/);
assert.match(form, /position:\s*static/);
assert.match(form, /overflow:\s*visible/);
assert.doesNotMatch(form, /\bleft\s*:|translateX|transition:[^;]*(?:left|width|transform)/);
assert.match(styles, /\.client-panel\.has-subscription \.client-form \{[\s\S]*height:\s*var\(--client-work-height\)/);
assert.match(rule('.client-form-content'), /align-content:\s*start/);
assert.match(styles, /--client-power-top:\s*calc\(\(var\(--client-work-height\) - 96px\) \/ 2\)/);
assert.match(styles, /\.client-panel\.has-subscription \{[\s\S]*transform:\s*translateY\(-9vh\)/);
assert.match(styles, /\.client-panel\.has-subscription \.client-power-section \{[\s\S]*height:\s*var\(--client-work-height\);[\s\S]*padding-top:\s*var\(--client-power-top\)/);
assert.match(styles, /\.client-panel\.has-subscription \.client-form-content \{[\s\S]*padding-top:\s*var\(--client-power-top\)/);
});
test('server rows scroll without moving the subscription column or showing a scrollbar', () => {
const scroll = rule('.client-server-scroll');
const simpleScroll = rule('.client-server-mode-panel.is-simple .client-server-scroll');
const grid = rule('.client-server-grid');
assert.match(scroll, /overflow-y:\s*auto/);
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*none/);
assert.match(simpleScroll, /overflow:\s*visible/);
assert.match(grid, /width:\s*min\(100%, 150px\)/);
assert.doesNotMatch(rule('.client-servers.is-scalable .client-server-grid'), /width:/);
});
test('tablet and mobile regions use normal flow with viewport-safe widths', () => {
const responsive = /@media \(max-width: 920px\) \{([\s\S]*?)\n\}\n\n@media \(max-width: 560px\)/.exec(styles)?.[1] || '';
assert.match(responsive, /grid-template-columns:\s*minmax\(0, 1fr\)/);
assert.match(responsive, /\.client-power-section,[\s\S]*\.client-form,[\s\S]*grid-column:\s*1/);
assert.match(responsive, /width:\s*min\(100%, 380px\)/);
assert.match(responsive, /max-height:\s*none/);
assert.match(responsive, /overflow-y:\s*visible/);
assert.match(responsive, /\.harbor-brand,[\s\S]*position:\s*absolute/);
assert.match(responsive, /\.client-inline-error\.is-connection,[\s\S]*position:\s*static/);
assert.match(rule('.client-duration-toggle'), /width:\s*min\(290px, 100%\)/);
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-confirmation-dialog\s*\{[\s\S]*width:\s*min\(430px, calc\(100vw - 48px\)\)/);
for (const viewport of [320, 390, 768]) {
const pagePadding = viewport <= 560 ? 28 : 48;
const panelPadding = 12;
const available = viewport - pagePadding - panelPadding;
assert.ok(Math.min(380, available) <= viewport, `form fits ${viewport}px viewport`);
assert.ok(Math.min(290, available) <= viewport, `duration control fits ${viewport}px viewport`);
}
const desktopOuterColumn = (1100 - 280) / 2;
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', () => {
const disabledRulesLabel = rule('.client-local-rules-toggle:disabled span');
assert.match(component, /<nav className="client-secondary-menu" aria-label="Дополнительные меню">/);
assert.match(component, /client-instructions-toggle[\s\S]*client-local-rules-toggle/);
assert.match(component, /Локальные правила недоступны: сейчас работают правила Gateway/);
assert.match(rule('.client-secondary-menu'), /right:\s*max\(14px, env\(safe-area-inset-right\)\)/);
assert.match(rule('.client-secondary-menu'), /display:\s*grid/);
assert.match(disabledRulesLabel, /opacity:\s*0/);
assert.match(disabledRulesLabel, /filter:\s*blur\(5px\)/);
assert.match(styles, /\.client-local-rules-toggle:disabled:hover span\s*\{[\s\S]*opacity:\s*1/);
assert.match(rule('.client-instructions'), /inset:\s*0 0 0 auto/);
assert.match(rule('.client-instructions'), /transform:\s*translateX\(104%\)/);
assert.match(rule('.client-local-rules'), /inset:\s*0 0 0 auto/);
assert.match(rule('.client-local-rules'), /transform:\s*translateX\(104%\)/);
});
test('responsive motion has a reduced-motion fallback', () => {
const reducedMotion = /@media \(prefers-reduced-motion: reduce\) \{([\s\S]*)\n\}/.exec(styles)?.[1] || '';
assert.match(reducedMotion, /\.client-power-section/);
assert.match(reducedMotion, /\.client-form/);
assert.match(reducedMotion, /\.harbor-brand/);
assert.match(reducedMotion, /transition:\s*none/);
assert.match(reducedMotion, /animation:\s*none/);
});
test('tooltips stay opaque, above adjacent content, and do not stick after pointer clicks', () => {
const tooltip = rule('.client-tooltip');
assert.match(tooltip, /z-index:\s*1000/);
assert.match(tooltip, /background:\s*oklch\(0\.14 0\.012 145\)/);
assert.doesNotMatch(tooltip, /transparent|backdrop-filter/);
assert.match(styles, /\.client-power-section:has\(\.client-tooltip-anchor:hover\)[\s\S]*z-index:\s*90/);
assert.match(styles, /\.client-tooltip-anchor:focus-visible > \.client-tooltip/);
assert.match(styles, /\.client-tooltip-anchor:has\(> :focus-visible\) > \.client-tooltip/);
assert.doesNotMatch(styles, /\.client-tooltip-anchor:focus-within > \.client-tooltip/);
assert.match(rule('.harbor-version-tooltip'), /background:\s*oklch\(0\.14 0\.012 145\)/);
assert.match(rule('.harbor-mode-tooltip'), /background:\s*oklch\(0\.14 0\.012 145\)/);
});
test('subscription validation waits for the provider and keeps diagnostics below errors', () => {
assert.match(component, /api\.subscription\.validate\(normalizedSubscriptionUrl/);
assert.match(component, /status: 'checking'/);
assert.match(component, /status: 'valid'/);
assert.match(component, /message: ERROR_DEFINITIONS\.SUBSCRIPTION_INVALID\.message/);
assert.match(component, /subscriptionValidationStatus === 'checking' \? '…' : '×'/);
assert.match(component, /if \(error\?\.context === 'subscription'\) onDismissError\(\)/);
assert.match(component, /error\.retry[\s\S]*error\.correlationId/);
assert.match(rule('.client-inline-error.is-subscription small'), /flex-basis:\s*100%/);
assert.match(rule('.client-inline-error.is-subscription small'), /opacity:\s*0\.45/);
assert.match(styles, /@keyframes client-subscription-error-in[\s\S]*filter:\s*blur\(7px\)/);
assert.match(styles, /@keyframes client-subscription-code-in[\s\S]*transform:\s*translateY\(-2px\)/);
assert.match(
/@media \(prefers-reduced-motion: reduce\) \{([\s\S]*)\n\}/.exec(styles)?.[1] || '',
/\.client-inline-error\.is-subscription > \*/,
);
});