Polish routing rules UI and normalize pasted values
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-17 16:27:25 +03:00
parent bc86741397
commit f3be0b2fd0
11 changed files with 254 additions and 97 deletions
+2 -1
View File
@@ -203,7 +203,8 @@ test('routing rules stay in one desktop row and reflow without shrinking drag ta
assert.match(mobile, /\.client-local-rule input \{[\s\S]*grid-row:\s*2/);
assert.match(mobile, /\.client-rule-outbound \{[\s\S]*grid-column:\s*4;[\s\S]*grid-row:\s*1/);
assert.match(mobile, /\.client-local-rule-status \{[\s\S]*grid-column:\s*5;[\s\S]*grid-row:\s*2/);
assert.match(mobile, /\.client-rule-handle,[\s\S]*width:\s*44px;[\s\S]*height:\s*44px/);
assert.match(mobile, /\.client-rule-handle,[\s\S]*\.client-local-rule-enabled-wrap,[\s\S]*width:\s*44px;[\s\S]*height:\s*44px/);
assert.match(mobile, /\.client-local-rule-enabled-wrap \{[\s\S]*grid-column:\s*2;[\s\S]*grid-row:\s*1/);
assert.match(mobile, /\.client-rule-type-trigger,[\s\S]*\.client-rule-outbound button \{[\s\S]*height:\s*44px/);
assert.match(mobile, /\.client-rule-type-list button \{[\s\S]*min-height:\s*44px/);
const compact = layoutStyles.slice(layoutStyles.indexOf('@media (max-width: 360px)'));
@@ -46,6 +46,8 @@ test('routing controller owns ordered outbound drafts, capability gating and dra
assert.match(feature, /onLostPointerCapture=\{feature\.losePointerReorder\}/);
assert.match(feature, /keyboardEvent\.preventDefault\(\);[\s\S]*cancelReorder\(\)/);
assert.match(feature, /stopAutoScroll\(session\)[\s\S]*Перемещение отменено/);
assert.match(feature, /const slotCenter = \(element: HTMLElement\) => listTop \+ element\.offsetTop \+ element\.offsetHeight \/ 2/);
assert.match(feature, /const centers = rulesRef\.current\.map[\s\S]*slotCenter\(ruleRow\)/);
});
test('routing lifecycle and Page orchestration keep the existing guards and blocking scopes', () => {
+21 -2
View File
@@ -32,7 +32,9 @@ test('rule editor add latency stays constant and dirty exits are guarded', () =>
assert.match(routing, /client-deletable-row/);
assert.match(routing, /className="client-delete-strike"[\s\S]*onAnimationEnd/);
assert.doesNotMatch(routing, /client-rule-delete-cross/);
assert.match(routing, /className="client-row-delete"/);
assert.match(routing, /className="client-row-delete"[\s\S]*client-row-delete-lid/);
assert.match(styles, /\.client-row-delete-lid \{[\s\S]*transition: transform/);
assert.match(styles, /\.client-row-delete:hover:not\(:disabled\) \.client-row-delete-lid,[\s\S]*translateY\(-2px\) rotate\(-8deg\)/);
assert.match(styles, /\.client-deletable-row\.is-removing > \.client-delete-strike[\s\S]*client-delete-strike/);
assert.match(styles, /\.client-delete-strike \{[\s\S]*z-index: 100[\s\S]*background: transparent/);
assert.match(styles, /\.client-deletable-row\.is-removing > :not\(\.client-delete-strike\)[\s\S]*z-index: 0/);
@@ -96,7 +98,7 @@ test('copy feedback, drawers and Gateway access actions expose complete semantic
assert.doesNotMatch(component, /client-access-tabs|role="tab"|role="tabpanel"/);
assert.match(styles, /\.client-drawer-close \{[\s\S]*width: 44px;[\s\S]*height: 44px/);
assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-copy-button \{[\s\S]*min-height: 44px/);
assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-rule-handle,[\s\S]*\.client-local-rule-enabled,[\s\S]*\.client-row-delete \{[\s\S]*width: 44px;[\s\S]*height: 44px/);
assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-rule-handle,[\s\S]*\.client-local-rule-enabled-wrap,[\s\S]*\.client-row-delete \{[\s\S]*width: 44px;[\s\S]*height: 44px/);
});
test('ordered rules use one accessible drag handle and a compact icon route control', () => {
@@ -114,6 +116,14 @@ test('ordered rules use one accessible drag handle and a compact icon route cont
assert.doesNotMatch(routing, /client-rule-outbound" role="group"/);
assert.match(routing, /className=\{`client-local-rules-list\$\{draftRules\.length > 1 \? ' has-order-flow' : ''\}`\}[\s\S]*aria-label="Правила применяются сверху вниз"/);
assert.match(routing, /className="client-rule-status-dot"/);
assert.match(routing, /role="switch"[\s\S]*aria-checked=\{rule\.enabled\}[\s\S]*client-rule-switch-track[\s\S]*client-rule-switch-thumb/);
assert.match(routing, /Отключить правило'[\s\S]*Включить правило'[\s\S]*<Tooltip>/);
assert.match(routing, /data-route=\{rule\.outbound\}/);
assert.match(styles, /\.client-local-rule\[data-route='direct'\] \{[\s\S]*--client-rule-route-color/);
assert.match(styles, /\.client-local-rule::after \{[\s\S]*background: var\(--client-rule-route-color\)/);
assert.match(styles, /\.client-local-rule\.is-active \.client-rule-status-dot \{[\s\S]*background: currentColor/);
assert.match(styles, /\.client-local-rule\.is-unsaved \.client-rule-status-dot \{[\s\S]*border-radius: 1px[\s\S]*rotate\(45deg\)/);
assert.match(styles, /\.client-local-rule\.is-pending \.client-rule-status-dot \{[\s\S]*border: 1\.5px dashed/);
assert.doesNotMatch(routing, />\s*[↑↓]\s*</);
assert.match(styles, /\.client-rule-handle \{[\s\S]*width: 44px;[\s\S]*height: 44px;[\s\S]*touch-action: none/);
assert.match(styles, /\.client-rule-outbound \{[\s\S]*width: 34px/);
@@ -121,3 +131,12 @@ test('ordered rules use one accessible drag handle and a compact icon route cont
assert.match(styles, /\.client-local-rules-list\.has-order-flow::before[\s\S]*linear-gradient\(to bottom/);
assert.match(styles, /\.client-local-rule \+ \.client-local-rule::before[\s\S]*linear-gradient\(90deg/);
});
test('rule values normalize on paste and help stays collapsed above the list', () => {
assert.match(routing, /normalizeRouteRules\(\[\{ \.\.\.rule, value \}\], \{ strict: true \}\)/);
assert.match(routing, /onPaste=\{\(event\) => \{[\s\S]*event\.preventDefault\(\)[\s\S]*normalizeDraftRuleValue\(rule, event\.clipboardData\.getData\('text'\)\)/);
assert.match(routing, /onBlur=\{\(\) => feature\.change\(index, 'value', normalizeDraftRuleValue\(rule, rule\.value\)\)\}/);
assert.match(routing, /<details className="client-local-rules-help">[\s\S]*<summary>[\s\S]*Как работают правила[\s\S]*<form id="client-local-rules-form"/);
assert.doesNotMatch(routing, /client-local-rules-note/);
assert.match(styles, /\.client-local-rules-help > div \{[\s\S]*background: color-mix/);
});
+17 -17
View File
@@ -37,26 +37,26 @@ const expectedImports = [
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const acceptedLedger = {
counts: {
cascadeEdges: 927,
customProperties: 106,
declarations: 3436,
cascadeEdges: 949,
customProperties: 108,
declarations: 3503,
important: 0,
keyframes: 49,
media: 13,
rules: 971,
variableReferences: 834,
rules: 984,
variableReferences: 844,
},
hashes: {
cascadeEdges: 'ea513313fc4cdfa85a5de74c76bdf06a00df1fc8f71825053b05f06684c7d6df',
customProperties: '06f39794d72566c2b2f8e4a2354866a54dbdceac0a1fdf9ae2619aae9abf2f36',
declarations: '0109c86e26e22898aec17aca2cf952edb88748499735c0fd2fd2163d7dd37dda',
cascadeEdges: '4c0a4c67d60c95181f4267225cbcba7e24eac3355be5e6707db9ed947a4fb54f',
customProperties: '3b97add4c3d685a532afff5046ddbe2b6eebcc0866c62781348559897f2930fb',
declarations: 'f630814428eeb4fef56795e2b8323c0fd567bc3d2334c5603973893d2a54c9f7',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: '8982145dba05b33bf1c93b2d54cfbe76305b276ff3c657aa020144016ada5848',
duplicateSelectors: 'e87ccd4a057619f7ae0791564b50867d47473121bf0b0468cf1aceeafb806a1a',
keyframes: '9e78309512ed82b1e9f87c58aeff505dfcdb694fc30c8f54570d01dce13eb51a',
ruleDeclarationSequences: '60267f6fbf59602e9028a9be4d8b591e1cd3e999eb5b86ab4e129199fd1853ea',
selectors: '9970cb8a580a9c57a3f39f7a801bef08e76a905ba8042deae58ac637b9769781',
variableReferences: '54bf2dffb86a0c9fc42c3d309cada316f446b17d8e54ce3c89b1108adb6e3db6',
witnesses: '0db2c977cfd7cd86209a972e02bcbca9c22722155a88ba789ccf2a395eae4cfa',
ruleDeclarationSequences: 'b6105ca3e89ca8a4b0f1c3b7e1ed7d1556da7afc856db19d3b36cc48e2de96b0',
selectors: 'd46d8c4b327f0c7ad2684ee05bf2338271c648fe0548315331c2a8719632e424',
variableReferences: '74c781fe61c4ca9734dc76415d4335612e0a0d7c26b3b3731d1bc0bd3c870b5b',
witnesses: 'c1248557ba723246417861681ca79c407b3aa47aee1b5e45fb464fb9c135c711',
},
};
@@ -209,7 +209,7 @@ test('client typography uses the shared semantic scale outside the token owner',
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
const witnesses = readStyleWitnesses(root);
assert.equal(witnesses.length, 845);
assert.equal(witnesses.length, 854);
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
assert.deepEqual(ledger.counts, acceptedLedger.counts);
@@ -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-D1sDzEdb.css']);
assert.deepEqual(assets, ['index-Ch0T5A1e.css']);
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
assert.equal(built.byteLength, 133160);
assert.equal(sha256(built), '165e6706c69d737a09b9cdde9d8211e94dbea49b3df42c8e5c9d194993e6b618');
assert.equal(built.byteLength, 135594);
assert.equal(sha256(built), '8425efe616036b5b37f0acc7476c1ef01c0c5168bb5e531863e498ca20a8dfa8');
});
+1 -1
View File
@@ -32,7 +32,7 @@ test('all repeated client primitive consumers use the shared owners', () => {
.map(({ source }) => source)
.join('\n');
assert.equal((production.match(/<Tooltip\b/g) || []).length, 12);
assert.equal((production.match(/<Tooltip\b/g) || []).length, 13);
assert.equal((production.match(/<CopyButton\b/g) || []).length, 2);
assert.equal((production.match(/<RailAction\b/g) || []).length, 5);
assert.equal((production.match(/<Drawer\b/g) || []).length, 5);