Simplify subscription profile management UI
This commit is contained in:
@@ -202,8 +202,8 @@ test('Gateway Home reuses the canonical device snapshot for applied route and gl
|
||||
|
||||
assert.match(overview, /const appliedServerId = state\?\.selection\?\.appliedServerId \|\| ''/);
|
||||
assert.match(overview, /const appliedServer = appliedProfile\?\.servers\.find[\s\S]*state\.selection\.appliedServerSnapshot/);
|
||||
assert.match(overview, /const mainIdentity = gatewayDirect[\s\S]*: connected[\s\S]*appliedProfile && appliedServer[\s\S]*`\$\{appliedProfile\.label\} · \$\{appliedServer\.label\}`/);
|
||||
assert.match(overview, /const switchIdentity = gatewayDirect[\s\S]*switchingServer && operationProfile && operationServer[\s\S]*`Переключаем на \$\{operationProfile\.label\} · \$\{operationServer\.label\}`/);
|
||||
assert.match(overview, /const mainIdentity = gatewayDirect[\s\S]*: connected[\s\S]*appliedProfile && appliedServer[\s\S]*`\$\{subscriptionDomain\(appliedProfile\.subscription\.host\)\} · \$\{appliedServer\.label\}`/);
|
||||
assert.match(overview, /const switchIdentity = gatewayDirect[\s\S]*switchingServer && operationProfile && operationServer[\s\S]*`Переключаем на \$\{subscriptionDomain\(operationProfile\.subscription\.host\)\} · \$\{operationServer\.label\}`/);
|
||||
assert.match(overview, /serverSlot=\{<AppliedIdentity identity=\{mainIdentity\} operation=\{switchIdentity\} \/>\}/);
|
||||
assert.match(feature, /formatByteString\(globalTraffic\?\.totalBytes \|\| '0'\)/);
|
||||
assert.match(feature, /const history = globalTraffic\?\.history \|\| \[\][\s\S]*samples=\{history\}[\s\S]*routeLabel="Gateway"[\s\S]*series="speed"/);
|
||||
|
||||
@@ -244,10 +244,11 @@ test('tooltips stay opaque, above adjacent content, and do not stick after point
|
||||
test('subscription validation is local and provider errors keep stable detail slots', () => {
|
||||
assert.match(subscription, /const validationStatus = !normalizedUrl[\s\S]*isSubscriptionUrlValid\(normalizedUrl\) \? 'valid' : 'invalid'/);
|
||||
assert.doesNotMatch(subscription, /validateSubscription\(|AbortController|status: 'checking'/);
|
||||
assert.match(subscription, /const message = feature\.duplicateLabel[\s\S]*feature\.validationStatus === 'invalid'[\s\S]*ERROR_DEFINITIONS\.SUBSCRIPTION_INVALID\.message[\s\S]*feature\.addError\?\.message/);
|
||||
assert.match(subscription, /const message = feature\.validationStatus === 'invalid'[\s\S]*ERROR_DEFINITIONS\.SUBSCRIPTION_INVALID\.message[\s\S]*feature\.addError\?\.message/);
|
||||
assert.match(subscription, /aria-invalid=\{feature\.validationStatus === 'invalid'\}/);
|
||||
assert.match(subscription, /disabled=\{feature\.addBlocked \|\| !feature\.label\.trim\(\) \|\| feature\.duplicateLabel \|\| feature\.validationStatus !== 'valid'\}/);
|
||||
assert.match(subscription, /if \(!await onAdd\(normalizedLabel, normalizedUrl\)\) return;[\s\S]*resetAdd\(\)/);
|
||||
assert.match(subscription, /disabled=\{feature\.addBlocked \|\| feature\.validationStatus !== 'valid'\}/);
|
||||
assert.match(subscription, /const host = new URL\(normalizedUrl\)\.hostname;[\s\S]*if \(!await onAdd\(label, normalizedUrl\)\) return;[\s\S]*resetAdd\(\)/);
|
||||
assert.doesNotMatch(subscription, />Название<|duplicateLabel/);
|
||||
assert.match(subscription, /setUrl: \(value: string\) => \{[\s\S]*onDismissError\(\)/);
|
||||
assert.match(component, /error\.retry[\s\S]*error\.correlationId/);
|
||||
assert.match(rule('.client-inline-error.is-subscription small'), /flex-basis:\s*100%/);
|
||||
|
||||
@@ -94,8 +94,9 @@ test('manual ping uses plain language and keeps results beside server names', ()
|
||||
assert.match(picker, /<svg className="client-server-health-checking"/);
|
||||
assert.doesNotMatch(picker, />Проверяем…<\/span>/);
|
||||
assert.match(picker, /function ServerCheckButton/);
|
||||
assert.match(picker, /client-server-check client-tooltip-anchor/);
|
||||
assert.match(picker, /<span className="client-tooltip" role="tooltip">/);
|
||||
assert.match(picker, /client-server-check\$\{checking \? ' is-checking' : ''\}/);
|
||||
assert.match(picker, /<span className="client-server-check-label">/);
|
||||
assert.doesNotMatch(picker, /client-tooltip/);
|
||||
assert.doesNotMatch(picker, /TCP|Не проверен/);
|
||||
assert.match(picker, /<strong>\{server\.label\}<\/strong>[\s\S]*?<\/button>[\s\S]*?client-server-meta/);
|
||||
assert.match(picker, /\[server\.city, server\.country\]\.filter\(Boolean\)\.join\(' · '\)/);
|
||||
@@ -132,6 +133,7 @@ test('server picker starts simple and reveals advanced controls on demand', () =
|
||||
assert.match(picker, /\.slice\(0, SIMPLE_SERVER_LIMIT\)/);
|
||||
assert.match(picker, /\{selected && <ServerRow/);
|
||||
assert.match(styles, /\.client-profile-body \.client-server-row \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\) 64px;/);
|
||||
assert.match(styles, /\.client-profile-body \.client-server-row\.is-selected \{[\s\S]*?box-shadow: inset 2px 0 var\(--client-accent\);/);
|
||||
assert.match(styles, /\.client-profile-body \.client-server-check::after \{[\s\S]*?content: 'ПРОВЕРИТЬ ПИНГ';/);
|
||||
assert.match(styles, /\.client-profile-body \.client-server-row\.is-selected \{[\s\S]*?background: color-mix\(in oklch, var\(--client-accent\) 5%, transparent\);/);
|
||||
assert.match(styles, /\.client-profile-body \.client-server-check-label \{[\s\S]*?display: inline;/);
|
||||
assert.doesNotMatch(styles, /\.client-profile-body \.client-server-check::after/);
|
||||
});
|
||||
|
||||
@@ -36,26 +36,26 @@ const expectedImports = [
|
||||
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||
const acceptedLedger = {
|
||||
counts: {
|
||||
cascadeEdges: 795,
|
||||
cascadeEdges: 792,
|
||||
customProperties: 31,
|
||||
declarations: 2996,
|
||||
declarations: 2977,
|
||||
important: 0,
|
||||
keyframes: 51,
|
||||
media: 13,
|
||||
rules: 911,
|
||||
variableReferences: 347,
|
||||
rules: 910,
|
||||
variableReferences: 338,
|
||||
},
|
||||
hashes: {
|
||||
cascadeEdges: '5cf4f4dc60eabe1e8175fa7c514a4ebc95983b588c2190adea0f75ffbbb813ef',
|
||||
cascadeEdges: 'f83d896540885ddaf09e55551a2f1ba5b587dcebfc285cc305efe80136154d93',
|
||||
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
||||
declarations: '47604ceab56972914f0c193ea10b3fc29b2532ca5b8a8057d1cfb437f5c8f86e',
|
||||
declarations: '14728723e4a95d6031b60eb39c0864da0ccc061e24798091cd093b6354a5d906',
|
||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||
duplicateSelectors: 'e063b7ac4965baed089fbcbba37544d7545eec7a7e84c3a68589465af53ed017',
|
||||
duplicateSelectors: 'c0ec5c96e48cd2ceed32aa5050383bcf29c3c0b8f1b440ed6883435e3e9b964d',
|
||||
keyframes: '9e87e997d0b776462ac3e495c7ced584871a5d9512226e5008eb84b4d311530a',
|
||||
ruleDeclarationSequences: 'd7db36cc3209e48c75e6e1b0c2748d0bff6be5a078f4e61b97c039d9da9c12cb',
|
||||
selectors: 'cd61e05ec477919f2c34d3944edc4fbb175b1572f8060a7696f0b73a687ddfad',
|
||||
variableReferences: 'b6f3567fc26a250c81f9214d1486a1cd3f7998012383aeb9d9e53fdeb32b17b1',
|
||||
witnesses: '1ba29e4efc61874a5c329d7d48d0fc399dae8b1c9dccfea0e682ceba3cb1630f',
|
||||
ruleDeclarationSequences: 'eac60ada2de39eff87713f71f5ca533cd831ee7d400d27eaa09562c29dfbfadd',
|
||||
selectors: 'e79f61996bcd2a806828406c0a626e87d60112f1b24264d60b8670f9b8f831af',
|
||||
variableReferences: '5c56197ab72968fc007f333c0cc58f90b6ec90465d6273bc757b7c77503dd26e',
|
||||
witnesses: '7066589e7b69a5b3c728ff70430a8d6d3c6c6d562cea91377244c0754e18b8da',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -110,7 +110,7 @@ test('tokens, shared primitives, and feature styles have one explicit owner', ()
|
||||
|
||||
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
||||
const witnesses = readStyleWitnesses(root);
|
||||
assert.equal(witnesses.length, 745);
|
||||
assert.equal(witnesses.length, 735);
|
||||
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
||||
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
||||
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
||||
@@ -306,8 +306,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-BJwATLJx.css']);
|
||||
assert.deepEqual(assets, ['index-pEpt1ReD.css']);
|
||||
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
||||
assert.equal(built.byteLength, 112406);
|
||||
assert.equal(sha256(built), '4b117a70cf431bbe7aacd368df3cb30ab50c5ab58498190b3822b92607aa9045');
|
||||
assert.equal(built.byteLength, 111837);
|
||||
assert.equal(sha256(built), 'f797af224ecae5e0435c7fb165cc158a237d11654c48a8cb287b32b2230e6b0c');
|
||||
});
|
||||
|
||||
@@ -33,10 +33,10 @@ test('App owns profile mutations and always uses the latest canonical revision',
|
||||
assert.equal((app.match(/revisionRef\.current = snapshot\.revision/g) || []).length, 2);
|
||||
assert.doesNotMatch(app, /if \(state\) revisionRef\.current = state\.revision/);
|
||||
assert.match(app, /api\.profiles\.add\(label, url, revisionRef\.current\)/);
|
||||
assert.match(app, /api\.profiles\.rename\(profileId, label, revisionRef\.current\)/);
|
||||
assert.match(app, /api\.profiles\.refresh\(profileId, revisionRef\.current\)/);
|
||||
assert.match(app, /api\.profiles\.forget\(profileId, mode, revisionRef\.current\)/);
|
||||
assert.match(page, /profiles,[\s\S]*onAdd: onAddProfile,[\s\S]*onRename: onRenameProfile,[\s\S]*onRefresh: onRefreshProfile,[\s\S]*onForget: onForgetProfile/);
|
||||
assert.match(page, /profiles,[\s\S]*onAdd: onAddProfile,[\s\S]*onRefresh: onRefreshProfile,[\s\S]*onForget: onForgetProfile/);
|
||||
assert.doesNotMatch(page, /onRenameProfile/);
|
||||
assert.doesNotMatch(feature, /from ['"][^'"]*\/api\/|\bapi\./);
|
||||
assert.doesNotMatch(feature, /<ServerPicker|<InlineError|<InlineProgress|<ConnectionPanel|<DevicesPanel|<DiagnosticsPanel/);
|
||||
});
|
||||
@@ -52,8 +52,8 @@ test('local validation, scoped refresh and drawer focus remain feature-owned', (
|
||||
assert.match(feature, /if \(deleteIdRef\.current\) return;[\s\S]*toggleRef\.current\?\.focus\(\)/);
|
||||
assert.match(feature, /setAdding\(false\)[\s\S]*client-profile-add-trigger'\)\?\.focus\(\)/);
|
||||
assert.doesNotMatch(feature, /addInvokerRef/);
|
||||
assert.match(feature, /function cancelRename\(\)[\s\S]*client-profile-menu-\$\{profileId\}[\s\S]*\.focus\(\)/);
|
||||
assert.match(feature, /id=\{`client-profile-menu-\$\{profile\.id\}`\}/);
|
||||
assert.match(feature, /className="client-profile-delete"[\s\S]*client-profile-delete-lid/);
|
||||
assert.doesNotMatch(feature, /cancelRename|client-profile-menu|onRename/);
|
||||
});
|
||||
|
||||
test('validation rejection parser preserves structured errors and normalizes non-objects', () => {
|
||||
@@ -85,7 +85,7 @@ test('validation rejection parser preserves structured errors and normalizes non
|
||||
});
|
||||
|
||||
test('feature keeps exact slots, truthy closes and subscription DOM order', () => {
|
||||
assert.match(feature, /if \(!await onAdd\(normalizedLabel, normalizedUrl\)\) return;[\s\S]*resetAdd\(\)/);
|
||||
assert.match(feature, /const host = new URL\(normalizedUrl\)\.hostname;[\s\S]*if \(!await onAdd\(label, normalizedUrl\)\) return;[\s\S]*resetAdd\(\)/);
|
||||
assert.match(feature, /if \(!await onForget\(deleteProfile\.id, deleteStopsVpn \? 'stop-and-delete' : 'delete'\)\) return;[\s\S]*setDeleteId\(''\)/);
|
||||
assert.match(feature, /client-profiles-operation[\s\S]*\{statusSlot\}[\s\S]*client-profile-list[\s\S]*feature\.adding[\s\S]*AddProfileForm[\s\S]*client-profile-add-trigger/);
|
||||
assert.doesNotMatch(feature, /client-profiles-current|aria-label="Добавить подписку"/);
|
||||
@@ -106,6 +106,9 @@ test('profiles render as flat accordion groups with scoped controls', () => {
|
||||
assert.match(feature, /!expanded && visibleServer && <button[\s\S]*client-profile-selected-server/);
|
||||
assert.match(feature, /const visibleServerId = applied \? feature\.selection\.appliedServerId : profile\.desiredServerId/);
|
||||
assert.match(feature, /const desired = !feature\.connected[\s\S]*&& !feature\.gatewayDirect/);
|
||||
assert.match(feature, /const profileDomain = subscriptionDomain\(profile\.subscription\.host\)/);
|
||||
assert.match(feature, /const profileDetails = `Серверов: \$\{profile\.servers\.length\}/);
|
||||
assert.doesNotMatch(feature, /<strong>\{profile\.label\}<\/strong>|Переименовать|client-profile-menu/);
|
||||
assert.match(styles, /\.client-profile-group/);
|
||||
assert.match(styles, /\.client-profile-body/);
|
||||
assert.match(styles, /\.client-profile-refresh\.is-refreshing/);
|
||||
|
||||
Reference in New Issue
Block a user