// @vitest-environment jsdom import { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, expect, it, vi } from 'vitest'; import { AssessorApiClient } from '../../src/api/assessor-api-client'; import type { GpoBackupStatusRecord } from '../../src/api/assessor-api-contract'; import { GpoFolderImport } from '../../src/features/project-import/GpoFolderImport'; import { GpoStatusRow } from '../../src/features/project-import/GpoStatusRow'; import { summarizeClientProcessingReadiness } from '../../src/features/project-import/datahub-processing-readiness'; const record: GpoBackupStatusRecord = { version: 1, artifactId: 'synthetic-source', assessmentId: 'synthetic-slot', jobId: 'synthetic-job', fileName: 'Synthetic.zip', sizeBytes: 3, submittedAt: '2026-09-17T10:00:00Z', updatedAt: '2026-09-17T10:00:00Z', state: 'upload-required', attempts: 0, maxAttempts: 3, gpoCount: 1, backupInstanceCount: 1, analysisUpdate: { identityAnalysisRunId: 'analysis', expectedLatestProcessingRunId: 'run', previousAnalyzerVersion: 'old', currentAnalyzerVersion: 'new' } }; afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it.each(['row ZIP', 'row folder', 'panel ZIP', 'panel folder'])('restores the exact missing source through the %s action', async path => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); let current = record; const list = vi.spyOn(AssessorApiClient.prototype, 'listProjectGpoBackupStatus').mockImplementation(async () => ({ version: 1, projectId: 'synthetic-project', generatedAt: record.updatedAt, records: [current] })); const submit = vi.spyOn(AssessorApiClient.prototype, 'submitGpoBackup').mockImplementation(async () => { current = { ...record, state: 'partial', analysisUpdate: undefined }; return { version: 1, artifactId: record.artifactId, jobId: record.jobId!, sha256: 'a'.repeat(64), sizeBytes: 3, status: 'queued', deduplicated: true, sourceRestored: true, repositoryRevision: 1 }; }); const retry = vi.fn(); const rootElement = document.createElement('div'); const root = createRoot(rootElement); const renderRow = () => ; try { await act(async () => root.render(path.startsWith('row') ? renderRow() : )); expect(rootElement.textContent).toContain('Upload required'); expect(rootElement.textContent).toContain('Saved results remain available'); expect(rootElement.textContent).not.toContain('no file upload is required'); expect([...rootElement.querySelectorAll('button')].some(button => button.textContent === 'Retry')).toBe(false); const isFolder = path.endsWith('folder'); const button = [...rootElement.querySelectorAll('button')].find(item => item.textContent === `Upload original ${isFolder ? 'folder' : 'ZIP'}`)!; expect(button).toBeTruthy(); const input = button.closest('.gpo-folder-actions')!.querySelector(isFolder ? 'input[multiple]' : 'input[accept]')!; const click = vi.spyOn(input, 'click').mockImplementation(() => undefined); await act(async () => button.click()); expect(click).toHaveBeenCalledOnce(); const file = new File(['abc'], isFolder ? 'Backup.xml' : 'renamed-original.zip', { type: 'application/zip' }); Object.defineProperty(file, 'arrayBuffer', { value: async () => new TextEncoder().encode('abc').buffer }); Object.defineProperty(file, 'webkitRelativePath', { value: isFolder ? 'Synthetic/instance/Backup.xml' : '' }); Object.defineProperty(input, 'files', { value: [file], configurable: true }); await act(async () => { input.dispatchEvent(new Event('change', { bubbles: true })); await vi.waitFor(() => expect(submit).toHaveBeenCalledOnce()); }); expect(submit).toHaveBeenCalledOnce(); expect(submit.mock.calls[0][0]).toBe('synthetic-project'); expect(submit.mock.calls[0][2]).toBe(record.assessmentId); expect(submit.mock.calls[0][3]).toBe(record.artifactId); expect(retry).not.toHaveBeenCalled(); expect(list).toHaveBeenCalled(); if (path.startsWith('row')) await act(async () => root.render(renderRow())); expect(rootElement.textContent).not.toContain('Upload required'); expect(summarizeClientProcessingReadiness([], [], [current]).state).toBe('ready'); } finally { await act(async () => root.unmount()); } }); it('keeps unavailable storage actionable without suggesting an upload or reprocess', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const unavailable = { ...record, state: 'storage-unavailable' as const }; const container = document.createElement('div'); const root = createRoot(container); try { await act(async () => root.render()); expect(container.textContent).toContain('Storage unavailable'); expect(container.textContent).toContain('connection and permissions'); expect(container.querySelector('input[type="file"]')).toBeNull(); expect(container.textContent).not.toContain('no file upload is required'); expect(summarizeClientProcessingReadiness([], [], [unavailable]).state).toBe('error'); expect(summarizeClientProcessingReadiness([], [], [record]).state).toBe('error'); } finally { await act(async () => root.unmount()); } }); it('still allows an evaluation-only update from retained inputs when the original file is missing', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const current: GpoBackupStatusRecord = { ...record, analysisUpdate: undefined, jobStatus: 'succeeded', evaluationUpdates: [{ evaluationRunId: 'evaluation', aggregationRunId: 'aggregation', baselineAnalysisIds: ['baseline'], artifactIds: [record.artifactId], previousVersion: '1.8.0', currentVersion: '1.9.0', reasons: ['engine-version'] }] }; const container = document.createElement('div'); const root = createRoot(container); const evaluate = vi.fn(); try { await act(async () => root.render()); const button = [...container.querySelectorAll('button')].find(item => item.textContent === 'Re-evaluate')!; expect(button.disabled).toBe(false); await act(async () => button.click()); expect(evaluate).toHaveBeenCalledOnce(); expect(container.textContent).toContain('Retained inputs can still be re-evaluated'); } finally { await act(async () => root.unmount()); } });