Refactor VPN proxy components and update related behavior
This commit is contained in:
@@ -75,8 +75,20 @@ test('typed endpoint facade preserves exact request contracts and raw payload id
|
||||
}],
|
||||
[() => api.subscription.refresh(), '/api/subscription/refresh', { method: 'POST' }],
|
||||
[() => api.subscription.forget(), '/api/subscription', { method: 'DELETE' }],
|
||||
[() => api.apply('server-1'), '/api/apply', {
|
||||
method: 'POST', body: JSON.stringify({ serverId: 'server-1', selectedTag: 'server-1' }),
|
||||
[() => api.profiles.add('Личный', 'https://sub', 4), '/api/profiles', {
|
||||
method: 'POST', body: JSON.stringify({ label: 'Личный', url: 'https://sub', expectedRevision: 4 }),
|
||||
}],
|
||||
[() => api.profiles.rename('profile-1', 'Работа', 5), '/api/profiles/profile-1', {
|
||||
method: 'PATCH', body: JSON.stringify({ label: 'Работа', expectedRevision: 5 }),
|
||||
}],
|
||||
[() => api.profiles.selectServer('profile-1', 'server-1', 6), '/api/profiles/profile-1/server', {
|
||||
method: 'PUT', body: JSON.stringify({ serverId: 'server-1', expectedRevision: 6 }),
|
||||
}],
|
||||
[() => api.profiles.activate('profile-1', 7), '/api/profiles/profile-1/activate', {
|
||||
method: 'POST', body: JSON.stringify({ expectedRevision: 7 }),
|
||||
}],
|
||||
[() => api.apply('profile-1', 'server-1', 8), '/api/apply', {
|
||||
method: 'POST', body: JSON.stringify({ profileId: 'profile-1', serverId: 'server-1', expectedRevision: 8 }),
|
||||
}],
|
||||
[() => api.gatewayAuto.setEnabled(true), '/api/gateway-auto', {
|
||||
method: 'POST', body: JSON.stringify({ enabled: true }),
|
||||
@@ -101,7 +113,7 @@ test('typed endpoint facade preserves exact request contracts and raw payload id
|
||||
}],
|
||||
[() => api.singbox.stop(), '/api/singbox/stop', { method: 'POST' }],
|
||||
[() => api.singbox.restart(), '/api/singbox/restart', { method: 'POST' }],
|
||||
[() => api.servers.ping(['one', 'two']), '/api/servers/ping-all', {
|
||||
[() => api.servers.ping('profile-1', ['one', 'two']), '/api/profiles/profile-1/servers/ping', {
|
||||
method: 'POST', body: JSON.stringify({ serverIds: ['one', 'two'] }),
|
||||
}],
|
||||
];
|
||||
|
||||
@@ -20,7 +20,7 @@ test('connection button chooses the only valid client action', () => {
|
||||
type: 'apply',
|
||||
serverId: 'srv_nl',
|
||||
});
|
||||
assert.deepEqual(connectionAction({ configExists: true }), { type: 'restart' });
|
||||
assert.equal(connectionAction({ configExists: true }), null);
|
||||
assert.equal(connectionAction({}), null);
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ const routing = source('features/routing/RoutingFeature.tsx');
|
||||
const diagnostics = source('features/diagnostics/ConnectivityDiagnosticsPanel.tsx');
|
||||
|
||||
test('App owns one stable mapping from typed transport to component actions', () => {
|
||||
assert.match(app, /const componentActions = \{[\s\S]*validateSubscription: api\.subscription\.validate[\s\S]*listDevices: api\.devices\.list[\s\S]*refreshDevices: api\.devices\.refresh[\s\S]*updateDevice: api\.devices\.update[\s\S]*setDevicePolicy: api\.devices\.setPolicy[\s\S]*pingServers: api\.servers\.ping[\s\S]*runConnectivityDiagnostics: api\.diagnostics\.connectivity[\s\S]*\};/);
|
||||
assert.match(app, /const componentActions = \{[\s\S]*listDevices: api\.devices\.list[\s\S]*refreshDevices: api\.devices\.refresh[\s\S]*updateDevice: api\.devices\.update[\s\S]*setDevicePolicy: api\.devices\.setPolicy[\s\S]*pingServers: api\.servers\.ping[\s\S]*runConnectivityDiagnostics: api\.diagnostics\.connectivity[\s\S]*\};/);
|
||||
assert.doesNotMatch(app, /validateSubscription: api\.subscription\.validate/);
|
||||
assert.equal((app.match(/actions=\{componentActions\}/g) || []).length, 1);
|
||||
assert.doesNotMatch(app, /componentActions\s*=\s*useMemo|componentActions\s*=\s*\([^)]*\)\s*=>/);
|
||||
});
|
||||
@@ -21,8 +22,8 @@ test('App owns one stable mapping from typed transport to component actions', ()
|
||||
test('presentational components use only injected narrow actions', () => {
|
||||
const components = [overview, subscription, devices, servers, routing, diagnostics].join('\n');
|
||||
assert.doesNotMatch(components, /from ['"][^'"]*\/api\/harborClient\.js['"]|\bapi\./);
|
||||
assert.match(overview, /validateSubscription: actions\.validateSubscription/);
|
||||
assert.match(subscription, /await validateSubscription\(normalizedUrl, \{ signal: controller\.signal \}\)/);
|
||||
assert.match(subscription, /isSubscriptionUrlValid\(normalizedUrl\)/);
|
||||
assert.doesNotMatch(subscription, /validateSubscription\(|AbortController/);
|
||||
assert.match(overview, /refreshDevices: actions\.refreshDevices/);
|
||||
assert.match(deviceFeature, /discover \? refreshDevices\(\) : listDevices\(\)/);
|
||||
assert.match(overview, /<ServerPicker[\s\S]*pingServers=\{actions\.pingServers\}/);
|
||||
@@ -31,6 +32,6 @@ test('presentational components use only injected narrow actions', () => {
|
||||
assert.match(overview, /<ConnectivityDiagnosticsPanel[\s\S]*runConnectivityDiagnostics=\{actions\.runConnectivityDiagnostics\}/);
|
||||
assert.match(deviceFeature, /requestDeviceUpdate\(device\.id, patch, snapshot\.revision\)/);
|
||||
assert.match(deviceFeature, /setDevicePolicy\(device\.id, mode, snapshot\.revision\)/);
|
||||
assert.match(servers, /await pingServers\(ids\)/);
|
||||
assert.match(servers, /await pingServers\(profileId, ids\)/);
|
||||
assert.match(diagnostics, /await runConnectivityDiagnostics\(customServices, target\)/);
|
||||
});
|
||||
|
||||
@@ -26,17 +26,28 @@ test('connection feature is the sole always-mounted power panel owner', () => {
|
||||
test('connection feature preserves actions, local preference and opaque neighbor slots', () => {
|
||||
assert.doesNotMatch(panel, /from ['"][^'"]*\/api\/|\bapi\./);
|
||||
assert.doesNotMatch(panel, /setInterval|setTimeout|useState\([^)]*state|useReducer/);
|
||||
assert.match(panel, /connectionAction\(\{ connected, selectedServerId, configExists: configured \}\)/);
|
||||
assert.match(panel, /action\?\.type === 'stop'[\s\S]*action\?\.type === 'apply'[\s\S]*action\?\.type === 'restart'/);
|
||||
assert.match(panel, /connectionAction\(\{ connected, selectedServerId \}\)/);
|
||||
assert.match(panel, /action\?\.type === 'stop'[\s\S]*action\?\.type === 'apply'/);
|
||||
assert.doesNotMatch(panel, /action\?\.type === 'restart'/);
|
||||
assert.match(panel, /if \(!await onStop\(\)\) return;[\s\S]*setConfirmingStop\(false\)/);
|
||||
assert.match(panel, /localStorage\.getItem\(DURATION_MODE_STORAGE_KEY\) === 'words'[\s\S]*localStorage\.setItem\(DURATION_MODE_STORAGE_KEY, nextMode\)/);
|
||||
assert.match(panel, /client-state-detail[\s\S]*\{routingSlot\}[\s\S]*client-state-copy[\s\S]*\{serverSlot\}[\s\S]*client-proxies[\s\S]*\{statusSlot\}/);
|
||||
assert.match(page, /routingSlot=\{<RoutingPendingStatus[\s\S]*blocked=\{connectionBlocked\}[\s\S]*onRestart=\{onRestart\}/);
|
||||
assert.match(routing, /client-route-rules-pending[\s\S]*Перезапустить VPN/);
|
||||
assert.match(page, /serverSlot=\{isGateway && <div className="client-gateway-route-summary"/);
|
||||
assert.match(page, /serverSlot=\{<AppliedIdentity identity=\{mainIdentity\} operation=\{switchIdentity\} \/>\}/);
|
||||
assert.match(page, /mainIdentity[\s\S]*appliedProfile[\s\S]*appliedServer/);
|
||||
assert.match(page, /statusSlot=\{<>[\s\S]*InlineError[\s\S]*InlineProgress/);
|
||||
});
|
||||
|
||||
test('gateway-direct is remote-owned even when the local runtime is stopped', () => {
|
||||
assert.match(page, /const mainIdentity = gatewayDirect[\s\S]*Gateway · сервер не определён[\s\S]*: connected/);
|
||||
assert.match(page, /const switchIdentity = gatewayDirect[\s\S]*Данные применённого сервера Gateway недоступны/);
|
||||
assert.match(panel, /const remoteOwned = !isGateway && gatewayDirect/);
|
||||
assert.match(panel, /const powerUnavailable = remoteOwned \|\| \(!connected && !canStart\)/);
|
||||
assert.match(panel, /aria-checked=\{connected \|\| remoteOwned\}[\s\S]*disabled=\{blocked \|\| powerUnavailable\}/);
|
||||
assert.match(panel, /remoteOwned[\s\S]*Подключением управляет Harbor Gateway[\s\S]*Gateway подключён/);
|
||||
});
|
||||
|
||||
test('shared clock, copy feedback and live announcement stay single-owned by the page', () => {
|
||||
const pageBody = page.slice(page.indexOf('export function ClientOverviewPage'));
|
||||
assert.equal((page.match(/setInterval\(\(\) => setNow\(Date\.now\(\)\), 1000\)/g) || []).length, 1);
|
||||
|
||||
@@ -201,8 +201,10 @@ test('Gateway Home reuses the canonical device snapshot for applied route and gl
|
||||
const connectionPanelStart = overview.indexOf('<ConnectionPanel');
|
||||
|
||||
assert.match(overview, /const appliedServerId = state\?\.selection\?\.appliedServerId \|\| ''/);
|
||||
assert.match(overview, /appliedServer\?\.label \|\| 'VPN-сервер не используется'/);
|
||||
assert.match(overview, /selectedServerId !== appliedServerId[\s\S]*Переключаем на \{desiredServer\.label\}/);
|
||||
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, /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"/);
|
||||
assert.match(connection, /<section className=\{`client-power-section[\s\S]*client-state-detail[\s\S]*client-connection-title[\s\S]*\{serverSlot\}[\s\S]*client-proxies/);
|
||||
@@ -212,13 +214,13 @@ test('Gateway Home reuses the canonical device snapshot for applied route and gl
|
||||
assert.match(feature, /feature\.status === 'error' \? feature\.error : null/);
|
||||
assert.match(overview, /if \(!isGateway && \(!connected \|\| !state\?\.connection\?\.startedAt\)\) return undefined/);
|
||||
assert.match(feature, /trafficSourceError[\s\S]*Трафик не обновляется · последние данные/);
|
||||
assert.match(subscription, /client-subscription-drawer\$\{open \? ' is-open' : ''\}/);
|
||||
assert.match(subscription, /const confirmingDeleteRef = useRef\(confirmingDelete\)[\s\S]*if \(confirmingDeleteRef\.current\) return/);
|
||||
assert.match(subscription, /requestDelete: \(\) => setConfirmingDelete\(true\)[\s\S]*open=\{feature\.confirmingDelete\}[\s\S]*onCancel=\{feature\.cancelDelete\}/);
|
||||
assert.match(connection, /aria-label=\{isGateway[\s\S]*Остановить Harbor Connect[\s\S]*Запустить Harbor Connect/);
|
||||
assert.match(connection, /className=\{`client-power-control\$\{isGateway \? ' client-tooltip-anchor' : ''\}`\}[\s\S]*aria-describedby=\{powerUnavailable \? 'gateway-power-unavailable'[\s\S]*Сначала добавьте подписку и выберите сервер/);
|
||||
assert.match(subscription, /client-drawer client-subscription-drawer\$\{drawerOpen \? ' is-open' : ''\}/);
|
||||
assert.match(subscription, /const deleteIdRef = useRef\(deleteId\)[\s\S]*if \(deleteIdRef\.current\) return/);
|
||||
assert.match(subscription, /requestDelete: \(profileId: string\) => setDeleteId\(profileId\)[\s\S]*open=\{Boolean\(feature\.deleteProfile\)\}[\s\S]*onCancel=\{feature\.cancelDelete\}[\s\S]*onConfirm=\{feature\.confirmDelete\}/);
|
||||
assert.match(connection, /aria-label=\{remoteOwned[\s\S]*isGateway[\s\S]*Остановить Harbor Connect[\s\S]*Запустить Harbor Connect/);
|
||||
assert.match(connection, /className=\{`client-power-control\$\{powerUnavailable \? ' client-tooltip-anchor' : ''\}`\}[\s\S]*aria-describedby=\{powerUnavailable \? 'gateway-power-unavailable'[\s\S]*Подключением управляет Harbor Gateway[\s\S]*Сначала добавьте подписку и выберите сервер/);
|
||||
assert.match(connection, /const powerButton = <button[\s\S]*className="client-power"[\s\S]*client-power-control[\s\S]*\{brandSlot\}[\s\S]*\{powerButton\}/);
|
||||
assert.match(overview, /isGateway && <DevicesPanel[\s\S]*feature=\{devicesFeature\}/);
|
||||
assert.match(overview, /isGateway && hasSubscription && <DevicesPanel[\s\S]*feature=\{devicesFeature\}/);
|
||||
assert.doesNotMatch(panel, /const \[snapshot, setSnapshot\]|setTimeout\(\(\) => load\(true\),/);
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ test('diagnostics feature is the sole owner while the conditional panel keeps re
|
||||
assert.equal((page.match(/useDiagnosticsFeature\(\)/g) || []).length, 1);
|
||||
assert.equal((page.match(/<DiagnosticsToggle/g) || []).length, 1);
|
||||
assert.equal((page.match(/<ConnectivityDiagnosticsPanel/g) || []).length, 1);
|
||||
assert.match(page, /const diagnosticsAvailable = isGateway \|\| \(hasSubscription && subscriptionContentReady\)/);
|
||||
assert.match(page, /const diagnosticsAvailable = hasSubscription/);
|
||||
assert.match(page, /\{diagnosticsAvailable && <ConnectivityDiagnosticsPanel[\s\S]*feature=\{diagnosticsFeature\}/);
|
||||
assert.match(page, /if \(!diagnosticsAvailable\) diagnosticsFeature\.close\(\)/);
|
||||
assert.doesNotMatch(page, /diagnosticsOpen|setDiagnosticsOpen|diagnosticsPanelRef|diagnosticsToggleRef|diagnosticsCloseRef|client-diagnostics-toggle/);
|
||||
|
||||
@@ -12,7 +12,12 @@ import { createStateSnapshot } from '../../.test-dist/src/shared/contracts/state
|
||||
const snapshot = (revision, desiredServerId = '', serverIds = ['one', 'two']) => ({
|
||||
apiVersion: 1,
|
||||
revision,
|
||||
selection: { desiredServerId },
|
||||
profiles: [{
|
||||
id: 'primary',
|
||||
desiredServerId,
|
||||
servers: serverIds.map((id) => ({ id })),
|
||||
}],
|
||||
selection: { desiredProfileId: 'primary', desiredServerId },
|
||||
servers: serverIds.map((id) => ({ id })),
|
||||
});
|
||||
|
||||
@@ -87,21 +92,10 @@ test('an equal revision keeps the current snapshot identity', () => {
|
||||
assert.equal(next.snapshot.selection.desiredServerId, 'one');
|
||||
});
|
||||
|
||||
test('pending selection survives polling until canonical state acknowledges it', () => {
|
||||
let state = receive(initialHarborState, snapshot(1, 'one'));
|
||||
state = harborReducer(state, { type: 'select-server', serverId: 'two' });
|
||||
state = receive(state, snapshot(2, 'one'));
|
||||
assert.equal(state.pendingServerId, 'two');
|
||||
|
||||
state = receive(state, snapshot(3, 'two'));
|
||||
assert.equal(state.pendingServerId, '');
|
||||
});
|
||||
|
||||
test('pending selection is cleared when its server disappears', () => {
|
||||
let state = receive(initialHarborState, snapshot(1, 'one'));
|
||||
state = harborReducer(state, { type: 'select-server', serverId: 'two' });
|
||||
|
||||
assert.equal(receive(state, snapshot(2, 'one', ['one'])).pendingServerId, '');
|
||||
test('selection has no client-side shadow and follows canonical profile snapshots', () => {
|
||||
const state = receive(initialHarborState, snapshot(2, 'two'));
|
||||
assert.equal(Object.hasOwn(state, 'pendingServerId'), false);
|
||||
assert.equal(state.snapshot.profiles[0].desiredServerId, 'two');
|
||||
});
|
||||
|
||||
test('data invariant: initial control outage is retryable without a fabricated snapshot', () => {
|
||||
|
||||
@@ -25,8 +25,8 @@ test('instructions feature is the sole owner behind one public boundary', () =>
|
||||
|
||||
test('unconditional controller and conditional panel preserve lifecycle and reset boundaries', () => {
|
||||
assert.match(page, /const instructionsFeature = useInstructionsFeature\([\s\S]*const diagnosticsAvailable/);
|
||||
assert.match(page, /\{\(isGateway \|\| \(hasSubscription && subscriptionContentReady\)\) && <InstructionsPanel/);
|
||||
assert.match(page, /if \(!hasSubscription\) \{[\s\S]*if \(!isGateway\) \{[\s\S]*instructionsFeature\.close\(\)/);
|
||||
assert.match(page, /\{hasSubscription && <InstructionsPanel/);
|
||||
assert.match(page, /if \(!hasSubscription\) \{[\s\S]*instructionsFeature\.close\(\)[\s\S]*devicesFeature\.close\(\)[\s\S]*diagnosticsFeature\.close\(\)/);
|
||||
assert.doesNotMatch(page, /if \(!instructionsAvailable\)|instructionsAvailable/);
|
||||
assert.match(feature, /const \[openInstructionId, setOpenInstructionId\] = useState\(''\)/);
|
||||
assert.match(feature, /function InstructionBlock[\s\S]*const \[copyFeedback, setCopyFeedback\] = useState/);
|
||||
|
||||
+10
-22
@@ -1,5 +1,4 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import http from 'node:http';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
@@ -21,38 +20,27 @@ test('operation conflicts block domain controls but leave copy and navigation al
|
||||
}
|
||||
}
|
||||
|
||||
const refreshing = { subscriptionRefresh: { status: 'running' } };
|
||||
const refreshing = { profileRefresh: { status: 'running', target: 'primary' } };
|
||||
assert.equal(operationBlocked(refreshing, 'connection'), true);
|
||||
assert.equal(operationBlocked(refreshing, 'serverApply'), true);
|
||||
assert.equal(operationBlocked(refreshing, 'subscriptionDelete'), true);
|
||||
assert.equal(operationBlocked(refreshing, 'copy'), false);
|
||||
assert.equal(operationBlocked(refreshing, 'navigation'), false);
|
||||
assert.equal(operationBlocked(refreshing, 'profileDelete'), true);
|
||||
|
||||
const applying = { serverApply: { status: 'running' } };
|
||||
const applying = { serverApply: { status: 'running', target: 'primary:server' } };
|
||||
assert.equal(operationBlocked(applying, 'connection'), true);
|
||||
assert.equal(operationBlocked(applying, 'subscriptionRefresh'), true);
|
||||
assert.equal(operationBlocked(applying, 'copy'), false);
|
||||
assert.equal(operationBlocked(applying, 'profileRefresh'), true);
|
||||
});
|
||||
|
||||
test('double click shares one in-flight request end to end', async (t) => {
|
||||
test('double click shares one in-flight request end to end', async () => {
|
||||
const request = deferred();
|
||||
let requests = 0;
|
||||
const server = http.createServer((request, response) => {
|
||||
requests += 1;
|
||||
setTimeout(() => {
|
||||
response.writeHead(200, { 'content-type': 'application/json' });
|
||||
response.end('{"success":true}');
|
||||
}, 20);
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
t.after(() => new Promise((resolve) => server.close(resolve)));
|
||||
|
||||
const registry = createOperationRegistry();
|
||||
const action = () => fetch(`http://127.0.0.1:${server.address().port}/apply`).then((response) => response.json());
|
||||
const first = registry.run('serverApply', action);
|
||||
const second = registry.run('serverApply', action);
|
||||
const action = () => { requests += 1; return request.promise; };
|
||||
const first = registry.run('serverApply', action, 'primary:server');
|
||||
const second = registry.run('serverApply', action, 'primary:server');
|
||||
|
||||
assert.equal(second, first);
|
||||
assert.equal(registry.getSnapshot().serverApply.status, 'running');
|
||||
request.resolve({ success: true });
|
||||
assert.deepEqual(await first, { success: true });
|
||||
assert.equal(requests, 1);
|
||||
assert.deepEqual(registry.getSnapshot(), {});
|
||||
|
||||
@@ -22,7 +22,7 @@ function rule(selector, source = styles) {
|
||||
return new RegExp(`^${escaped}\\s*\\{([\\s\\S]*?)\\n\\}`, 'm').exec(source)?.[1] || '';
|
||||
}
|
||||
|
||||
test('desktop layout keeps the power control on a symmetric center axis', () => {
|
||||
test('desktop layout keeps the power control on a symmetric center axis beside the subscription drawer', () => {
|
||||
const panel = rule('.client-panel');
|
||||
const power = rule('.client-power-section');
|
||||
const form = rule('.client-form');
|
||||
@@ -33,12 +33,11 @@ test('desktop layout keeps the power control on a symmetric center axis', () =>
|
||||
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.doesNotMatch(styles, /\.client-panel\.has-subscription \.client-form\b/);
|
||||
assert.doesNotMatch(styles, /\.client-form-content\b/);
|
||||
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\)/);
|
||||
assert.match(rule('.client-panel.is-gateway-home'), /min-height:\s*max\(620px, calc\(100dvh - 80px\)\)[\s\S]*grid-template-rows:\s*minmax\(260px, 1fr\) auto[\s\S]*row-gap:\s*36px/);
|
||||
assert.match(rule('.client-gateway-summary'), /grid-column:\s*1 \/ -1[\s\S]*grid-row:\s*2[\s\S]*justify-self:\s*center[\s\S]*width:\s*min\(860px, calc\(100% - 48px\)\)/);
|
||||
assert.match(styles, /\.client-power-section\.is-gateway \.client-power-control,[\s\S]*width:\s*84px;[\s\S]*height:\s*84px/);
|
||||
@@ -49,9 +48,11 @@ test('desktop layout keeps the power control on a symmetric center axis', () =>
|
||||
assert.match(rule('.client-gateway-traffic-chart'), /min-height:\s*180px/);
|
||||
assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-gateway-traffic-chart,[\s\S]*min-height:\s*130px;[\s\S]*height:\s*130px/);
|
||||
assert.doesNotMatch(rule('.client-gateway-summary'), /background|border|box-shadow/);
|
||||
assert.match(rule('.client-gateway-route-slot'), /min-height:\s*16px[\s\S]*overflow:\s*hidden[\s\S]*text-overflow:\s*ellipsis[\s\S]*white-space:\s*nowrap/);
|
||||
assert.match(rule('.client-applied-operation'), /min-height:\s*15px[\s\S]*overflow:\s*hidden[\s\S]*text-overflow:\s*ellipsis[\s\S]*white-space:\s*nowrap/);
|
||||
assert.match(component, /client-panel\$\{showPower \? '' : ' is-setup'\}[\s\S]*is-gateway-home/);
|
||||
assert.match(component, /const showPower = isGateway \|\| \(hasSubscription && Boolean\(selectedServerId\)\)/);
|
||||
assert.match(component, /const showPower = hasSubscription/);
|
||||
assert.match(component, /\$\{!hasSubscription \? ' is-first-run' : ''\}/);
|
||||
assert.match(component, /\$\{isGateway && hasSubscription \? ' is-gateway-home' : ''\}/);
|
||||
});
|
||||
|
||||
test('page reload plays one stable startup sequence', () => {
|
||||
@@ -66,7 +67,7 @@ test('page reload plays one stable startup sequence', () => {
|
||||
assert.match(styles, /@media \(prefers-reduced-motion: reduce\)[\s\S]*\.client-shell\.is-intro \.client-panel,[\s\S]*animation: none/);
|
||||
});
|
||||
|
||||
test('server rows scroll without moving the subscription column or showing a scrollbar', () => {
|
||||
test('server rows scroll inside the subscription drawer without moving the main layout', () => {
|
||||
const scroll = rule('.client-server-scroll');
|
||||
const simpleScroll = rule('.client-server-mode-panel.is-simple .client-server-scroll');
|
||||
const grid = rule('.client-server-grid');
|
||||
@@ -77,7 +78,8 @@ test('server rows scroll without moving the subscription column or showing a scr
|
||||
assert.match(simpleScroll, /max-height:\s*none/);
|
||||
assert.match(simpleScroll, /overflow:\s*visible/);
|
||||
assert.match(grid, /width:\s*min\(100%, 220px\)/);
|
||||
assert.match(rule('.client-form-content'), /gap:\s*24px/);
|
||||
assert.match(rule('.client-drawer'), /overflow-y:\s*auto/);
|
||||
assert.match(subscription, /client-profile-list[\s\S]*feature\.profiles\.map\(\(profile\) => <ProfileGroup/);
|
||||
assert.match(rule('.client-server-toolbar-title'), /grid-column:\s*2/);
|
||||
assert.match(rule('.client-server-mode-toggle'), /grid-row:\s*2/);
|
||||
assert.match(rule('.client-server-check'), /grid-column:\s*3/);
|
||||
@@ -101,7 +103,7 @@ test('tablet and mobile regions use normal flow with viewport-safe widths', () =
|
||||
|
||||
assert.match(rule('.client-duration-toggle'), /width:\s*min\(290px, 100%\)/);
|
||||
assert.match(mobile, /\.client-server-toolbar \{[\s\S]*grid-template-rows:\s*44px 44px/);
|
||||
assert.match(mobile, /\.client-form-content \{[\s\S]*gap:\s*24px/);
|
||||
assert.match(mobile, /\.client-secondary-menu \{[\s\S]*right:\s*8px/);
|
||||
assert.match(mobile, /\.client-power-control > \.harbor-brand \{[\s\S]*scale\(1\.35\)/);
|
||||
assert.match(mobile, /\.client-server-check \{[\s\S]*width:\s*44px/);
|
||||
assert.match(styles, /\.client-instructions\s*\{[\s\S]*width:\s*min\(470px, 100vw\)/);
|
||||
@@ -239,13 +241,14 @@ test('tooltips stay opaque, above adjacent content, and do not stick after point
|
||||
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(subscription, /validateSubscription\(normalizedUrl/);
|
||||
assert.match(subscription, /status: 'checking'/);
|
||||
assert.match(subscription, /status: 'valid'/);
|
||||
assert.match(subscription, /message: ERROR_DEFINITIONS\.SUBSCRIPTION_INVALID\.message/);
|
||||
assert.match(subscription, /validationStatus === 'checking' \? '…' : '×'/);
|
||||
assert.match(subscription, /if \(error\?\.context === 'subscription'\) onDismissError\(\)/);
|
||||
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, /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, /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%/);
|
||||
assert.match(rule('.client-inline-error.is-subscription small'), /opacity:\s*0\.45/);
|
||||
|
||||
@@ -26,8 +26,8 @@ test('server picker has one public feature owner without legacy shims', () => {
|
||||
assert.match(overview, /import \{ ServerPicker \} from '\.\.\/features\/servers\/index\.js'/);
|
||||
assert.equal((overview.match(/<ServerPicker/g) || []).length, 1);
|
||||
assert.doesNotMatch(picker, /from ['"][^'"]*\/api\/|\bapi\./);
|
||||
assert.match(overview, /const selectedServerId = pendingServerId \|\| state\?\.selection\?\.desiredServerId \|\| ''/);
|
||||
assert.match(overview, /setPendingServerId\(serverId\);[\s\S]*if \(connected && serverId\) onApply\(serverId\)/);
|
||||
assert.match(overview, /const selectedServerId = desiredProfile\?\.desiredServerId \|\| ''/);
|
||||
assert.match(overview, /function selectServer\(profile: ProfileSnapshot, serverId: string\)[\s\S]*onApply\(profile\.id, serverId\)[\s\S]*onSelectProfileServer\(profile\.id, serverId\)/);
|
||||
});
|
||||
|
||||
const fixtures = (count) => Array.from({ length: count }, (_, index) => ({
|
||||
@@ -70,7 +70,7 @@ test('server picker validates unknown ping payloads before publishing results',
|
||||
]) {
|
||||
assert.throws(() => parseServerPingResults(payload), TypeError);
|
||||
}
|
||||
assert.match(picker, /parseServerPingResults\(await pingServers\(ids\)\)/);
|
||||
assert.match(picker, /parseServerPingResults\(await pingServers\(profileId, ids\)\)/);
|
||||
});
|
||||
|
||||
test('server picker checks health only on manual refresh and bounds the result window', () => {
|
||||
@@ -78,7 +78,7 @@ test('server picker checks health only on manual refresh and bounds the result w
|
||||
assert.doesNotMatch(picker, /checkVisible\(\);/);
|
||||
assert.match(picker, /onClick={checkVisible}/);
|
||||
assert.match(picker, /\{ \.\.\.current\[id\], checking: true \}/);
|
||||
assert.match(picker, /900 - \(performance\.now\(\) - startedAt\)/);
|
||||
assert.match(picker, /Math\.max\(900, Math\.ceil\(elapsed \/ 900\) \* 900\)/);
|
||||
assert.match(picker, /\{ \.\.\.current\[id\], checking: false \}/);
|
||||
assert.match(picker, /\.slice\(page \* SERVER_RESULT_WINDOW, \(page \+ 1\) \* SERVER_RESULT_WINDOW\)/);
|
||||
assert.match(picker, /\.slice\(0, 30\)/);
|
||||
|
||||
@@ -36,26 +36,26 @@ const expectedImports = [
|
||||
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||
const acceptedLedger = {
|
||||
counts: {
|
||||
cascadeEdges: 821,
|
||||
cascadeEdges: 780,
|
||||
customProperties: 31,
|
||||
declarations: 2941,
|
||||
declarations: 2912,
|
||||
important: 0,
|
||||
keyframes: 51,
|
||||
media: 10,
|
||||
rules: 888,
|
||||
variableReferences: 338,
|
||||
keyframes: 49,
|
||||
media: 12,
|
||||
rules: 889,
|
||||
variableReferences: 330,
|
||||
},
|
||||
hashes: {
|
||||
cascadeEdges: '1ffc2a2f986970988643129ff3e32f19c31bb8b1e018a3a474a976e23adc9776',
|
||||
cascadeEdges: '12a578504877454d14c734b8ce608766d061d3f8f809a5abf13513a645d8b6aa',
|
||||
customProperties: 'c7dd331e4bad898c450568999d8c9c6837e275a79c365c7680e143026fde4545',
|
||||
declarations: '77314876411a21c88472dc277a59ca24192ca7d38de1ce8f367e7bd1007c5323',
|
||||
declarations: '297c4387ebbccec796e80474c4d040eba20941d316a77a29d1e72c31d19c02d6',
|
||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||
duplicateSelectors: '1565bf06e07fd7cbf601d24846bb3e1059d06720ace1544f2ac7f6ecf36cab47',
|
||||
keyframes: '0bed7ec3cd3a86ee091cf9b07c081ab2364ac5a17e55e95bc9c4a6430419b019',
|
||||
ruleDeclarationSequences: 'ce3831db5966264c17fa3af3e10fc6a41398cc619d061fa155ac43b47f0947ea',
|
||||
selectors: '828cf6e95b03c890e3a969be20eb05be1f70872d6076e8f9a6a38cf6b415d012',
|
||||
variableReferences: '9b6214da6e8e02b01deb6d5a98fe4c695a1fdaff4f65c936b805d7d5841db04d',
|
||||
witnesses: 'c34721ec2cb99104eae77841ae88ddf77cde278d15c21675a020f3cfdd13a28a',
|
||||
duplicateSelectors: 'bea7ed28b2e90fbaa3ae50aaee2346fc9063996fc9b7c585a3a47693a055b783',
|
||||
keyframes: 'bde9c628dc86cb8a4299d5397eb7a81c5fbc9481136a073130991d3182d0799d',
|
||||
ruleDeclarationSequences: '7386a7be7efa9f3527d167b885fee0f208f236f6947ba3878a0f4acabd31e7a8',
|
||||
selectors: '845bd57c761ad3767b5a908fa004623d61dd4cdb838616ae0320093ffa1718eb',
|
||||
variableReferences: '6755baede1580eefde410ea7237ede30af629fbbabd5bc13fc83012a7e4132b2',
|
||||
witnesses: 'e5869e1f591f6a559ad9aa1942edcbb67b84ebe5ca6d22b4f6108dcba584a9f5',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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, 716);
|
||||
assert.equal(witnesses.length, 740);
|
||||
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
||||
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
||||
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
||||
@@ -127,7 +127,7 @@ test('every live production selector has an expanded DOM witness', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('JSX witness expansion follows cross-file components, ReactNode slots, portals, and imperative classes', () => {
|
||||
test('JSX witness expansion follows cross-file components, render props, ReactNode slots, portals, and imperative classes', () => {
|
||||
const fixtureWitnesses = createStyleWitnesses([
|
||||
{
|
||||
file: '/fixture/Child.tsx',
|
||||
@@ -142,6 +142,20 @@ test('JSX witness expansion follows cross-file components, ReactNode slots, port
|
||||
const slot = fixtureWitnesses.find((witness) => witness.classes.includes('slot'));
|
||||
assert.deepEqual(title?.ancestorClasses, ['child', 'scope']);
|
||||
assert.deepEqual(slot?.ancestorClasses, ['child', 'scope']);
|
||||
const renderPropWitnesses = createStyleWitnesses([
|
||||
{
|
||||
file: '/fixture/List.tsx',
|
||||
source: 'export function List({ renderItem }) { return <section className="list">{renderItem()}</section>; }',
|
||||
},
|
||||
{
|
||||
file: '/fixture/App.tsx',
|
||||
source: 'export function App() { return <main className="scope"><List renderItem={() => <button className="item" />} /></main>; }',
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(
|
||||
renderPropWitnesses.find((witness) => witness.classes.includes('item'))?.ancestorClasses,
|
||||
['list', 'scope'],
|
||||
);
|
||||
const localBindingWitnesses = createStyleWitnesses([{
|
||||
file: '/fixture/App.tsx',
|
||||
source: `export function App() {
|
||||
@@ -293,8 +307,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-DQr1ElW4.css']);
|
||||
assert.deepEqual(assets, ['index-COUqegc1.css']);
|
||||
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
||||
assert.equal(built.byteLength, 110647);
|
||||
assert.equal(sha256(built), '91bf07a74af3312f9b048fdebd642e811fafc3bb2ec49b55592a35f085c25f41');
|
||||
assert.equal(built.byteLength, 108951);
|
||||
assert.equal(sha256(built), '059653e70cd17fb84953e19b8280dcb9b926760c1781fb19382820434f9d1f01');
|
||||
});
|
||||
|
||||
@@ -235,16 +235,6 @@ function dynamicClassBindings(files) {
|
||||
add(routingSuffix, 'status', [...routingStatusBody.matchAll(/return \['([a-z-]+)'/g)].map((match) => match[1]));
|
||||
}
|
||||
|
||||
const subscriptionSuffix = '/features/subscription/SubscriptionFeature.tsx';
|
||||
const subscription = bySuffix(subscriptionSuffix);
|
||||
if (subscription) {
|
||||
add(subscriptionSuffix, 'validationStatus', literalUnion(
|
||||
subscription,
|
||||
/interface SubscriptionValidation[\s\S]+?status: ([^;]+);/,
|
||||
'SubscriptionValidation.status',
|
||||
));
|
||||
}
|
||||
|
||||
const connectionSuffix = '/features/connection/ConnectionPanel.tsx';
|
||||
const connection = bySuffix(connectionSuffix);
|
||||
if (connection) {
|
||||
@@ -281,7 +271,13 @@ function bindComponentProps(definition, element, callerEnvironment, dynamicBindi
|
||||
const node = attribute.value?.type === 'JSXExpressionContainer'
|
||||
? attribute.value.expression
|
||||
: attribute.value || { type: 'BooleanLiteral', value: true };
|
||||
supplied.set(name, { node, environment: callerEnvironment });
|
||||
supplied.set(name, node.type === 'Identifier' && callerEnvironment.get(node.name)
|
||||
? callerEnvironment.get(node.name)
|
||||
: {
|
||||
callable: ['ArrowFunctionExpression', 'FunctionExpression'].includes(node.type) && producesJsx(node),
|
||||
node,
|
||||
environment: callerEnvironment,
|
||||
});
|
||||
}
|
||||
supplied.set('children', { node: element.children, environment: callerEnvironment });
|
||||
const environment = new Map(dynamicBindings.get(definition.file));
|
||||
|
||||
@@ -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