Refactor VPN proxy components and update related behavior
This commit is contained in:
@@ -21,33 +21,39 @@ test('subscription feature is the sole always-mounted lifecycle and view owner',
|
||||
assert.equal((page.match(/<SubscriptionDeleteDialog/g) || []).length, 1);
|
||||
assert.doesNotMatch(page, /client-subscription-summary|client-usage-bar|id="delete-subscription"/);
|
||||
assert.doesNotMatch(page, /subscriptionValidationAttempt|confirmingDeleteRef|previousHasSubscriptionRef|SUBSCRIPTION_REVEAL_DELAY_MS/);
|
||||
assert.match(feature, /client-subscription-summary/);
|
||||
assert.match(feature, /client-usage-bar/);
|
||||
assert.match(feature, /function ProfileGroup/);
|
||||
assert.match(feature, /client-profile-list/);
|
||||
assert.match(feature, /id="delete-subscription"/);
|
||||
});
|
||||
|
||||
test('App mutation sequencing and the controlled URL draft stay unchanged', () => {
|
||||
assert.match(app, /const \[subscriptionUrl, setSubscriptionUrl\] = useState\(''\)/);
|
||||
assert.match(app, /async function fetchSubscription\(\)[\s\S]*api\.subscription\.fetch\(subscriptionUrl\)[\s\S]*dispatch\(\{ type: 'clear-pending-server' \}\)/);
|
||||
assert.match(app, /async function forgetSubscription\(\)[\s\S]*setSubscriptionUrl\(''\)[\s\S]*dispatch\(\{ type: 'clear-pending-server' \}\)/);
|
||||
assert.match(page, /subscriptionUrl,[\s\S]*setSubscriptionUrl,[\s\S]*validateSubscription: actions\.validateSubscription,[\s\S]*onImport: onFetchSubscription,[\s\S]*onRefresh: onRefreshSubscription,[\s\S]*onForget: onForgetSubscription/);
|
||||
test('App owns profile mutations and always uses the latest canonical revision', () => {
|
||||
assert.match(app, /const revisionRef = useRef\(0\)/);
|
||||
assert.match(app, /const hasAcceptedSnapshotRef = useRef\(false\)/);
|
||||
assert.equal((app.match(/if \(!hasAcceptedSnapshotRef\.current \|\| snapshot\.revision > revisionRef\.current\)/g) || []).length, 2);
|
||||
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.doesNotMatch(feature, /from ['"][^'"]*\/api\/|\bapi\./);
|
||||
assert.doesNotMatch(feature, /ServerPicker|InlineError|InlineProgress|ConnectionPanel|DevicesPanel|DiagnosticsPanel/);
|
||||
assert.doesNotMatch(feature, /<ServerPicker|<InlineError|<InlineProgress|<ConnectionPanel|<DevicesPanel|<DiagnosticsPanel/);
|
||||
});
|
||||
|
||||
test('validation, reveal, refresh, usage and drawer timing remain feature-owned', () => {
|
||||
assert.match(feature, /setTimeout\(async \(\) => \{[\s\S]*await validateSubscription\(normalizedUrl, \{ signal: controller\.signal \}\)[\s\S]*\}, 300\)/);
|
||||
assert.match(feature, /normalizeRequestError\(caught\)[\s\S]*requestError\.name === 'AbortError'[\s\S]*controller\.abort\(\)/);
|
||||
assert.match(feature, /currentValidation\?\.error[\s\S]*\|\| localError[\s\S]*error\?\.context === 'subscription'/);
|
||||
assert.match(feature, /retry: requestError\.retryable[\s\S]*setValidationAttempt/);
|
||||
assert.match(feature, /SUBSCRIPTION_REVEAL_DELAY_MS = 1350/);
|
||||
assert.match(feature, /previouslyHadSubscription[\s\S]*prefers-reduced-motion: reduce[\s\S]*setTimeout\(\(\) => setContentReady\(true\), SUBSCRIPTION_REVEAL_DELAY_MS\)/);
|
||||
assert.match(feature, /if \(!hasSubscription\) return undefined;[\s\S]*onRefresh\(\);[\s\S]*\}, \[hasSubscription\]\)/);
|
||||
assert.match(feature, /setTimeout\(\(\) => setEditing\(false\), 5000\)/);
|
||||
assert.match(feature, /requestAnimationFrame\(tick\)[\s\S]*cancelAnimationFrame\(frame\)/);
|
||||
assert.match(feature, /420 \+ Math\.min\(7, Math\.max\(0, serverCount - 1\)\) \* 90/);
|
||||
test('local validation, scoped refresh and drawer focus remain feature-owned', () => {
|
||||
assert.match(feature, /isSubscriptionUrlValid\(normalizedUrl\)/);
|
||||
assert.doesNotMatch(feature, /validateSubscription\(|AbortController|setTimeout\(async/);
|
||||
assert.match(feature, /async function refresh\(profileId: string\)/);
|
||||
assert.match(feature, /Math\.max\(900, Math\.ceil\(elapsed \/ 900\) \* 900\)/);
|
||||
assert.match(feature, /if \(confirmingDeleteRef\.current\) return;[\s\S]*event\.type === 'keydown'[\s\S]*toggleRef\.current\?\.focus\(\)/);
|
||||
assert.match(feature, /previousProfileCountRef\.current === 0 && profiles\.length > 0[\s\S]*setOpen\(true\)[\s\S]*setExpanded/);
|
||||
assert.match(feature, /previousProfileCountRef\.current > 0\) setOpen\(false\)/);
|
||||
assert.match(feature, /if \(!adding \|\| \(profiles\.length > 0 && !open\)\) return undefined/);
|
||||
assert.match(feature, /if \(deleteIdRef\.current\) return;[\s\S]*toggleRef\.current\?\.focus\(\)/);
|
||||
assert.match(feature, /const invoker = addInvokerRef\.current[\s\S]*if \(invoker\) requestAnimationFrame\(\(\) => invoker\.focus\(\)\)/);
|
||||
assert.match(feature, /showAdd: \(\) => \{[\s\S]*addInvokerRef\.current = document\.activeElement instanceof HTMLElement/);
|
||||
assert.match(feature, /function cancelRename\(\)[\s\S]*client-profile-menu-\$\{profileId\}[\s\S]*\.focus\(\)/);
|
||||
assert.match(feature, /id=\{`client-profile-menu-\$\{profile\.id\}`\}/);
|
||||
});
|
||||
|
||||
test('validation rejection parser preserves structured errors and normalizes non-objects', () => {
|
||||
@@ -79,22 +85,27 @@ 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 onImport\(\)\) return;[\s\S]*setSubscriptionUrl\(''\)[\s\S]*setEditing\(false\)/);
|
||||
assert.match(feature, /if \(!await onForget\(\)\) return;[\s\S]*setConfirmingDelete\(false\)/);
|
||||
assert.match(feature, /client-subscription-summary[\s\S]*client-subscription-edit[\s\S]*\{statusSlot\}[\s\S]*client-usage[\s\S]*\{serverSlot\}/);
|
||||
assert.match(page, /<SubscriptionPanel[\s\S]*statusSlot=\{<>[\s\S]*InlineError[\s\S]*InlineProgress[\s\S]*serverSlot=\{hasSubscription[\s\S]*<ServerPicker/);
|
||||
assert.match(feature, /if \(!await onAdd\(normalizedLabel, 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]*client-profiles-current[\s\S]*\{statusSlot\}[\s\S]*client-profile-list/);
|
||||
assert.match(feature, /const drawerOpen = feature\.open && feature\.profiles\.length > 0/);
|
||||
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.doesNotMatch(feature, /setInterval|copyText|client-live-region/);
|
||||
});
|
||||
|
||||
test('saved subscription stays one clear shared presentation', () => {
|
||||
assert.equal((feature.match(/client-subscription-summary/g) || []).length, 1);
|
||||
assert.match(feature, /client-subscription-heading[\s\S]*client-subscription-status[\s\S]*Сохранена[\s\S]*client-subscription-actions[\s\S]*client-subscription-refresh[\s\S]*client-subscription-delete/);
|
||||
assert.match(feature, /client-subscription-domain-button[\s\S]*client-subscription-label">Подписка[\s\S]*subscriptionDomain\(subscription\?\.host\)/);
|
||||
assert.match(feature, /client-usage-summary[\s\S]*Использовано[\s\S]* из [\s\S]*client-usage-bar[\s\S]*client-usage-details[\s\S]*Действует до[\s\S]*subscriptionDaysLeft/);
|
||||
assert.doesNotMatch(feature, /role="tab"|client-subscription-card|subscriptions\.map\(/);
|
||||
assert.match(styles, /\.client-subscription-heading\s*\{[\s\S]*grid-template-columns:\s*64px minmax\(0, 1fr\) 64px/);
|
||||
assert.match(styles, /\.client-usage\s*\{[\s\S]*min-height:\s*76px/);
|
||||
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-usage-summary > strong[\s\S]*transition:\s*none/);
|
||||
test('profiles render as flat accordion groups with scoped controls', () => {
|
||||
assert.match(feature, /feature\.profiles\.map\(\(profile\) => <ProfileGroup/);
|
||||
assert.match(feature, /aria-expanded=\{expanded\}/);
|
||||
assert.match(feature, /profile\.subscription\.status === 'stale'[\s\S]*profile\.subscription\.fetchedAt/);
|
||||
assert.match(feature, /feature\.operations\.profileRefresh\?\.target === profile\.id/);
|
||||
assert.match(feature, /const currentLabel = feature\.gatewayDirect[\s\S]*Gateway · сервер не определён/);
|
||||
assert.match(feature, /const desired = !feature\.connected[\s\S]*&& !feature\.gatewayDirect/);
|
||||
assert.match(styles, /\.client-profile-group/);
|
||||
assert.match(styles, /\.client-profile-body/);
|
||||
assert.match(styles, /\.client-profile-refresh\.is-refreshing/);
|
||||
assert.doesNotMatch(feature, /client-subscription-card|role="tab"/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user