Improve routing rule drag handling and controls
Build and Deploy Gateway / build-and-push (push) Successful in 34s
Build and Deploy Gateway / deploy (push) Successful in 6s

This commit is contained in:
2026-08-17 16:48:46 +03:00
parent f3be0b2fd0
commit 286a89051a
9 changed files with 112 additions and 92 deletions
+5 -2
View File
@@ -37,13 +37,16 @@ test('routing controller owns ordered outbound drafts, capability gating and dra
assert.match(feature, /if \(!editable \|\| blocked\) return/);
assert.match(feature, /sameRule\(rule, savedRules\[index\]\)/);
assert.match(feature, /sameRule\(rule, activeRules\[index\]\)/);
assert.match(feature, /beginRuleReorder\(dragRef\.current, ruleKey, 'pointer'\)[\s\S]*setPointerCapture/);
assert.match(feature, /beginRuleReorder\(dragRef\.current, ruleKey, 'pointer'\)[\s\S]*capture\.setPointerCapture/);
assert.match(feature, /const capture = event\.currentTarget\.closest<HTMLElement>\('\.client-local-rules-list'\)/);
assert.doesNotMatch(feature, /event\.currentTarget\.setPointerCapture/);
assert.match(feature, /beginRuleReorder\(dragRef\.current, ruleKey, 'keyboard'\)/);
assert.match(feature, /event\.detail === 0\) toggleKeyboardReorder/);
assert.match(feature, /event\.key === 'Tab'[\s\S]*finishReorder\('focus-leave'\)/);
assert.match(feature, /onBlur=\{\(event\) => feature\.handleReorderBlur/);
assert.match(feature, /endRuleReorder\(session, 'unmount'\)\.stopAutoScroll/);
assert.match(feature, /onLostPointerCapture=\{feature\.losePointerReorder\}/);
assert.match(feature, /className=\{`client-local-rules-list[\s\S]*onPointerMove=\{feature\.movePointerReorder\}[\s\S]*onLostPointerCapture=\{feature\.losePointerReorder\}/);
assert.match(feature, /session\.capture\.hasPointerCapture[\s\S]*session\.capture\.releasePointerCapture/);
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/);
+7 -4
View File
@@ -102,12 +102,14 @@ test('copy feedback, drawers and Gateway access actions expose complete semantic
});
test('ordered rules use one accessible drag handle and a compact icon route control', () => {
const handle = /<button\s+className="client-rule-handle"([\s\S]*?)<\/button>/.exec(routing)?.[0] || '';
assert.match(routing, /className="client-rule-handle"[\s\S]*type="button"[\s\S]*aria-label=\{`Переместить правило, позиция/);
assert.match(routing, /aria-describedby="client-rule-reorder-instructions"[\s\S]*aria-pressed=\{lifted\}/);
assert.match(routing, /onPointerDown=\{\(event\) => feature\.startPointerReorder\(event, rule\._key\)\}/);
assert.doesNotMatch(handle, /onPointerMove|onPointerUp|onPointerCancel|onLostPointerCapture/);
assert.doesNotMatch(routing, /data-rule-key[^>]*onPointerDown/);
assert.match(routing, /onClick=\{\(event\) => feature\.handleReorderClick\(event, rule\._key\)\}/);
assert.match(routing, /<svg viewBox="0 0 12 28" aria-hidden="true">[\s\S]*<circle[\s\S]*<circle[\s\S]*<circle/);
assert.match(routing, /<svg viewBox="0 0 16 28" aria-hidden="true">[\s\S]*cx="5" cy="5"[\s\S]*cx="11" cy="23"/);
assert.match(routing, /client-rule-reorder-instructions[\s\S]*стрелки вверх и вниз[\s\S]*Escape отменяет/);
assert.match(routing, /client-rule-reorder-live[\s\S]*aria-live="polite"/);
assert.match(routing, /className="client-rule-outbound client-tooltip-anchor"[\s\S]*className=\{direct \? 'is-direct' : 'is-vpn'\}/);
@@ -116,16 +118,17 @@ 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, /role="switch"[\s\S]*aria-checked=\{rule\.enabled\}[\s\S]*client-rule-check/);
assert.match(routing, /Отключить: правило перестанет участвовать в маршрутизации[\s\S]*Включить: правило снова будет участвовать в маршрутизации/);
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.doesNotMatch(styles, /\.client-local-rule::after/);
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-row-delete \{[\s\S]*color: oklch\(0\.62 0\.16 28\)/);
assert.match(styles, /\.client-rule-outbound \{[\s\S]*width: 34px/);
assert.match(styles, /\.client-rule-outbound svg \{[\s\S]*width: 20px/);
assert.match(styles, /\.client-local-rules-list\.has-order-flow::before[\s\S]*linear-gradient\(to bottom/);
+2
View File
@@ -26,6 +26,8 @@ test('center crossing moves one or many slots without changing stable row identi
assert.equal(crossedRuleIndex(0, 149, [100, 150, 200]), 0);
assert.equal(crossedRuleIndex(0, 151, [100, 150, 200]), 1);
assert.equal(crossedRuleIndex(0, 201, [100, 150, 200]), 2);
assert.equal(crossedRuleIndex(2, 149, [100, 150, 200]), 1);
assert.equal(crossedRuleIndex(2, 99, [100, 150, 200]), 0);
const moved = moveRule(rules, 0, 2);
assert.deepEqual(moved.map(({ _key }) => _key), ['b', 'c', 'a']);
assert.strictEqual(moved[2], rules[0]);
+14 -14
View File
@@ -37,26 +37,26 @@ const expectedImports = [
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const acceptedLedger = {
counts: {
cascadeEdges: 949,
cascadeEdges: 950,
customProperties: 108,
declarations: 3503,
declarations: 3500,
important: 0,
keyframes: 49,
media: 13,
rules: 984,
variableReferences: 844,
variableReferences: 840,
},
hashes: {
cascadeEdges: '4c0a4c67d60c95181f4267225cbcba7e24eac3355be5e6707db9ed947a4fb54f',
cascadeEdges: '5d255c2f624bf3b45037c027f127d644f02dfc5a130313e766597da9bee4875f',
customProperties: '3b97add4c3d685a532afff5046ddbe2b6eebcc0866c62781348559897f2930fb',
declarations: 'f630814428eeb4fef56795e2b8323c0fd567bc3d2334c5603973893d2a54c9f7',
declarations: '7b64892ae650b7035a2eadc2846f99c1085df66995637cdf0e0c807743275c38',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: 'e87ccd4a057619f7ae0791564b50867d47473121bf0b0468cf1aceeafb806a1a',
duplicateSelectors: '8982145dba05b33bf1c93b2d54cfbe76305b276ff3c657aa020144016ada5848',
keyframes: '9e78309512ed82b1e9f87c58aeff505dfcdb694fc30c8f54570d01dce13eb51a',
ruleDeclarationSequences: 'b6105ca3e89ca8a4b0f1c3b7e1ed7d1556da7afc856db19d3b36cc48e2de96b0',
selectors: 'd46d8c4b327f0c7ad2684ee05bf2338271c648fe0548315331c2a8719632e424',
variableReferences: '74c781fe61c4ca9734dc76415d4335612e0a0d7c26b3b3731d1bc0bd3c870b5b',
witnesses: 'c1248557ba723246417861681ca79c407b3aa47aee1b5e45fb464fb9c135c711',
ruleDeclarationSequences: '5e4f3127541777f8c404f2cc7c430bc42b3d0e15389f13cb62e7c5321d86897d',
selectors: '744f667e7e36762ab798ef84fcb41abba655fc087e6362dbc1e71b9f637ac259',
variableReferences: '924d3c0fa5aa34a4afeeec4870e3a0f3e63c4144a930e90289da232fa96075ae',
witnesses: 'aaaa1b6c3ac84e82646c8c8fd4d88f0c3c7cd291eed7d4c85ede32f2f9a2fea2',
},
};
@@ -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, 854);
assert.equal(witnesses.length, 859);
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-Ch0T5A1e.css']);
assert.deepEqual(assets, ['index-CAi7iXH5.css']);
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
assert.equal(built.byteLength, 135594);
assert.equal(sha256(built), '8425efe616036b5b37f0acc7476c1ef01c0c5168bb5e531863e498ca20a8dfa8');
assert.equal(built.byteLength, 135653);
assert.equal(sha256(built), '15cb8f4597c16fd8ba119395b1246e3cf9362fc24988aecd6a08a0666fb53d46');
});