import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import test from 'node:test'; import { normalizeRequestError } from '../../.test-dist/src/web/features/subscription/requestError.js'; const root = path.resolve(import.meta.dirname, '../..'); const page = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.tsx'), 'utf8'); const app = fs.readFileSync(path.join(root, 'src/web/App.tsx'), 'utf8'); const feature = fs.readFileSync(path.join(root, 'src/web/features/subscription/SubscriptionFeature.tsx'), 'utf8'); const boundary = fs.readFileSync(path.join(root, 'src/web/features/subscription/index.ts'), 'utf8'); test('subscription feature is the sole always-mounted lifecycle and view owner', () => { assert.match(boundary, /SubscriptionDeleteDialog,[\s\S]*SubscriptionPanel,[\s\S]*SubscriptionToggle,[\s\S]*useSubscriptionFeature/); assert.match(page, /from '\.\.\/features\/subscription\/index\.js'/); assert.equal((page.match(/useSubscriptionFeature\(/g) || []).length, 1); assert.equal((page.match(/ { 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/); assert.doesNotMatch(feature, /from ['"][^'"]*\/api\/|\bapi\./); 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/); 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\(\)/); }); test('validation rejection parser preserves structured errors and normalizes non-objects', () => { const retry = () => true; const structured = Object.assign(new Error('provider unavailable'), { name: 'HarborApiError', context: 'subscription', correlationId: 'correlation-1', retryable: true, retry, }); assert.deepEqual(normalizeRequestError(structured), { name: 'HarborApiError', context: 'subscription', message: 'provider unavailable', correlationId: 'correlation-1', retryable: true, retry, }); assert.deepEqual(normalizeRequestError(null), { name: undefined, context: undefined, message: 'Ссылка подписки недействительна.', correlationId: undefined, retryable: false, retry: null, }); assert.equal(normalizeRequestError('provider rejected').message, 'Ссылка подписки недействительна.'); }); 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, /[\s\S]*InlineError[\s\S]*InlineProgress[\s\S]*serverSlot=\{hasSubscription[\s\S]*