import { test } from 'node:test' import assert from 'node:assert/strict' import { createPageCleanupRegistry } from '../src/shared/page-cleanup-registry.ts' interface CleanupHooks { clearTimers: () => void revokeUrl: (url: string) => void } function createHooks(): CleanupHooks & { cleared: string[] } { const cleared: string[] = [] return { cleared, clearTimers: () => cleared.push('timers'), revokeUrl: (url: string) => cleared.push(`url:${url}`), } } test('test_task_089_cleanup_normal_default_path', () => { const hooks = createHooks() const registry = createPageCleanupRegistry(hooks) const url = registry.registerObjectUrl('blob:http://x/1') assert.equal(url, 'blob:http://x/1') assert.equal(registry.trackedUrlCount, 1) assert.equal(registry.disposeCount, 0) const result = registry.dispose() assert.equal(result, 2) assert.equal(registry.disposed, true) assert.equal(registry.trackedUrlCount, 0) assert.deepEqual(hooks.cleared, ['url:blob:http://x/1', 'timers']) assert.equal(registry.disposeCount, 1) }) test('test_task_089_cleanup_normal_multiple_items', () => { const hooks = createHooks() const registry = createPageCleanupRegistry(hooks) registry.registerObjectUrl('blob:http://x/1') registry.registerObjectUrl('blob:http://x/2') registry.registerObjectUrl('blob:http://x/3') const aborted: string[] = [] const makeController = (n: number) => ({ abort: () => aborted.push(`abort:${n}`) }) as unknown as AbortController registry.registerRequestController(makeController(1)) registry.registerRequestController(makeController(2)) assert.equal(registry.trackedUrlCount, 3) assert.equal(registry.requestCount, 2) const result = registry.dispose() assert.equal(result, 6) assert.equal(registry.trackedUrlCount, 0) assert.equal(registry.requestCount, 0) assert.deepEqual(aborted, ['abort:1', 'abort:2']) assert.deepEqual( hooks.cleared, ['url:blob:http://x/1', 'url:blob:http://x/2', 'url:blob:http://x/3', 'timers'], ) }) test('test_task_089_cleanup_normal_repeated_operation_is_idempotent', () => { const hooks = createHooks() const registry = createPageCleanupRegistry(hooks) const url = registry.registerObjectUrl('blob:http://x/1') registry.dispose() registry.dispose() registry.dispose() assert.equal(registry.disposed, true) assert.equal(registry.trackedUrlCount, 0) assert.equal(hooks.cleared.length, 2, '第二次及以后的 dispose 不再重复清理') // dispose 后注册被拒绝 registry.registerObjectUrl('blob:http://x/2') assert.equal(registry.trackedUrlCount, 0) assert.equal(registry.disposeCount, 1) assert.equal(registry.rejectedCount, 1) assert.equal(url, 'blob:http://x/1') }) test('test_task_089_cleanup_boundary_empty_input', () => { const hooks = createHooks() const registry = createPageCleanupRegistry(hooks) assert.equal(registry.trackedUrlCount, 0) assert.equal(registry.requestCount, 0) const result = registry.dispose() assert.equal(result, 1, 'dispose 至少执行 clearTimers') assert.deepEqual(hooks.cleared, ['timers']) // 空字符串 URL 不注册 registry.registerObjectUrl('') assert.equal(registry.trackedUrlCount, 0) }) test('test_task_089_cleanup_boundary_single_item', () => { const hooks = createHooks() const registry = createPageCleanupRegistry(hooks) registry.registerObjectUrl('blob:http://x/single') const result = registry.dispose() assert.equal(result, 2) assert.equal(hooks.cleared.length, 2) assert.equal(hooks.cleared[0], 'url:blob:http://x/single') assert.equal(hooks.cleared[1], 'timers') }) test('test_task_089_cleanup_boundary_limit_and_overflow', () => { const hooks = createHooks() const registry = createPageCleanupRegistry(hooks, { maxTrackedUrls: 3 }) for (let i = 1; i <= 5; i++) registry.registerObjectUrl(`blob:http://x/${i}`) assert.equal(registry.trackedUrlCount, 3, '超过上限只保留最近 3 个') const result = registry.dispose() assert.equal(result, 4) assert.ok(hooks.cleared.includes('url:blob:http://x/3')) assert.ok(hooks.cleared.includes('url:blob:http://x/5')) assert.ok(!hooks.cleared.includes('url:blob:http://x/1'), '被驱逐的 URL 不再由注册表 revoke') assert.equal(hooks.cleared.length, 4) }) test('test_task_089_cleanup_invalid_input_rejected', () => { assert.throws(() => createPageCleanupRegistry({} as never), /clearTimers 必须是函数/) assert.throws( () => createPageCleanupRegistry({ clearTimers: () => {} } as never), /revokeUrl 必须是函数/, ) const hooks = createHooks() const registry = createPageCleanupRegistry(hooks, { maxTrackedUrls: 0 }) assert.equal(registry.registerObjectUrl('blob:http://x/1'), 'blob:http://x/1', 'maxTrackedUrls 为 0 时不限上限') assert.equal(registry.trackedUrlCount, 1) }) test('test_task_089_cleanup_dependency_failure_releases_resources', () => { let urlFailures = 0 const hooks: CleanupHooks & { cleared: string[] } = { cleared: [], clearTimers: () => hooks.cleared.push('timers'), revokeUrl: (url) => { if (url === 'blob:http://x/poison') { urlFailures += 1 throw new Error('revoke down') } hooks.cleared.push(`url:${url}`) }, } const registry = createPageCleanupRegistry(hooks) registry.registerObjectUrl('blob:http://x/a') registry.registerObjectUrl('blob:http://x/poison') registry.registerObjectUrl('blob:http://x/b') // revoke 抛错不中断清理,全部条目仍被尝试释放 const result = registry.dispose() assert.equal(result, 4) assert.equal(urlFailures, 1) assert.equal(registry.trackedUrlCount, 0) assert.ok(hooks.cleared.includes('url:blob:http://x/a')) assert.ok(hooks.cleared.includes('url:blob:http://x/b')) assert.ok(hooks.cleared.includes('timers')) // 错误可恢复:同一注册表 dispose 后状态一致(幂等) registry.dispose() assert.equal(registry.disposeCount, 1) assert.equal(urlFailures, 1) })