104 lines
6.7 KiB
TypeScript
104 lines
6.7 KiB
TypeScript
// @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 = () => <GpoStatusRow projectId="synthetic-project" record={current} imported="Synthetic date" busy={false}
|
|
onRetry={retry} onEvaluate={vi.fn()} onReprocess={vi.fn()} onDelete={vi.fn()}/>;
|
|
try {
|
|
await act(async () => root.render(path.startsWith('row') ? renderRow() : <GpoFolderImport projectId="synthetic-project"/>));
|
|
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<HTMLInputElement>(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(<GpoStatusRow projectId="synthetic-project" record={unavailable}
|
|
imported="Synthetic date" busy={false} onRetry={vi.fn()} onEvaluate={vi.fn()} onReprocess={vi.fn()} onDelete={vi.fn()}/>));
|
|
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(<GpoStatusRow projectId="synthetic-project" record={current} imported="Synthetic date"
|
|
busy={false} onRetry={vi.fn()} onEvaluate={evaluate} onReprocess={vi.fn()} onDelete={vi.fn()}/>));
|
|
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()); }
|
|
});
|