By default, Meticulous does not automatically store files that users upload during recording. This design decision is intentional:
- Storage efficiency: Recording file contents would significantly increase storage costs
- Privacy: Avoiding file storage prevents capturing potentially sensitive user data
- Performance: File uploads can be large and would slow down session recording
- Practicality: Most apps only need to test the upload flow, not the file contents themselves
When a user uploads a file during a recorded session (via HTML file input or drag-and-drop), Meticulous records the user interaction but not the file itself. During replay, the file won't be attached to the input element.
This guide shows you how to handle file uploads in your Meticulous tests using three different approaches.
When to use: Most cases where your app validates that a file is present before proceeding.
The recommended approach is to bypass file validation during test runs. Since Meticulous stubs out network requests, your backend will respond with mocked data as if the file was successfully uploaded, allowing you to test the complete user flow.
const handleClickUpload = () => {
if (window.Meticulous?.isRunningAsTest) {
// Skip validation during Meticulous tests
goToNextStage();
} else if (fileInput.files.length > 0) {
goToNextStage();
} else {
showIsRequiredError();
}
}
function ProfilePictureUpload() {
const [file, setFile] = useState<File | null>(null);
const [error, setError] = useState<string>('');
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = event.target.files?.[0];
if (selectedFile) {
setFile(selectedFile);
setError('');
}
};
const handleSubmit = async () => {
// Skip file validation during Meticulous tests
if (!window.Meticulous?.isRunningAsTest && !file) {
setError('Please select a file');
return;
}
// During tests, formData will be empty, but the mocked backend response
// will return as if the file was successfully uploaded
const formData = new FormData();
if (file) {
formData.append('profilePicture', file);
}
const response = await fetch('/api/upload-profile-picture', {
method: 'POST',
body: formData,
});
const data = await response.json();
// data.imageUrl will be the mocked value during tests
showSuccessMessage(`Uploaded: ${data.imageUrl}`);
};
return (
<div>
<input type="file" onChange={handleFileChange} accept="image/*" />
{error && <span className="error">{error}</span>}
<button onClick={handleSubmit}>Upload</button>
</div>
);
}
function DocumentUploadForm() {
const [resume, setResume] = useState<File | null>(null);
const [coverLetter, setCoverLetter] = useState<File | null>(null);
const handleSubmit = async () => {
// Validate files only when not running as a test
if (!window.Meticulous?.isRunningAsTest) {
if (!resume) {
alert('Resume is required');
return;
}
if (!coverLetter) {
alert('Cover letter is required');
return;
}
}
const formData = new FormData();
if (resume) formData.append('resume', resume);
if (coverLetter) formData.append('coverLetter', coverLetter);
await fetch('/api/submit-application', {
method: 'POST',
body: formData,
});
// Backend response is mocked during tests, so this will work
navigateToConfirmationPage();
};
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<div>
<label>Resume (Required)</label>
<input
type="file"
onChange={(e) => setResume(e.target.files?.[0] || null)}
accept=".pdf,.doc,.docx"
/>
</div>
<div>
<label>Cover Letter (Required)</label>
<input
type="file"
onChange={(e) => setCoverLetter(e.target.files?.[0] || null)}
accept=".pdf,.doc,.docx"
/>
</div>
<button type="submit">Submit Application</button>
</form>
);
}
function DragDropUpload() {
const [file, setFile] = useState<File | null>(null);
const [isDragging, setIsDragging] = useState(false);
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const droppedFile = e.dataTransfer.files[0];
if (droppedFile) {
setFile(droppedFile);
}
};
const handleUpload = async () => {
// Skip validation during tests
if (!window.Meticulous?.isRunningAsTest && !file) {
alert('Please drop a file first');
return;
}
const formData = new FormData();
if (file) {
formData.append('file', file);
}
await fetch('/api/upload', { method: 'POST', body: formData });
showSuccessMessage();
};
return (
<div
onDrop={handleDrop}
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
onDragLeave={() => setIsDragging(false)}
className={isDragging ? 'dragging' : ''}
>
{file ? `Selected: ${file.name}` : 'Drop file here'}
<button onClick={handleUpload}>Upload</button>
</div>
);
}
Since Meticulous stubs out network responses from your backend, requests like POST /api/upload or GET /api/file/123 will replay with the exact responses that were captured during recording. This means:
- During recording: Real file uploaded → Real backend response captured → Response includes file metadata/URL
- During replay: No file uploaded → Mocked backend response → Same response as recording, so app behaves identically
You get complete test coverage of the user flow without needing the actual file contents.
When to use: Your app reads or processes file contents on the frontend before uploading (e.g., image preview, CSV parsing, client-side validation).
For cases where you need to test frontend logic that processes file contents, use the custom values API to store and restore file data.
- Development: Up to 20MB per file (set
data-is-production-environment="false"on your Meticulous script tag) - Production: Up to 1MB per file
function ImageUploadWithPreview() {
const [preview, setPreview] = useState<string>('');
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// Read file contents for preview
const reader = new FileReader();
reader.onload = (e) => {
const dataUrl = e.target?.result as string;
setPreview(dataUrl);
// Store for replay
if (window.Meticulous?.recordCustomValues) {
window.Meticulous.recordCustomValues({
imagePreviewData: dataUrl,
});
}
};
reader.readAsDataURL(file);
};
// Restore preview during replay
useEffect(() => {
if (window.Meticulous?.isRunningAsTest) {
const storedData = window.Meticulous.getCustomValues();
if (storedData?.imagePreviewData) {
setPreview(storedData.imagePreviewData);
}
}
}, []);
return (
<div>
<input type="file" onChange={handleFileChange} accept="image/*" />
{preview && <img src={preview} alt="Preview" style={{ maxWidth: '200px' }} />}
<button onClick={() => uploadImage(preview)}>Upload</button>
</div>
);
}
function CSVUpload() {
const [data, setData] = useState<string[][]>([]);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const csvText = e.target?.result as string;
const rows = csvText.split('\n').map(row => row.split(','));
setData(rows);
// Store CSV data for replay
if (window.Meticulous?.recordCustomValues) {
window.Meticulous.recordCustomValues({
csvData: csvText,
});
}
};
reader.readAsText(file);
};
// Restore during replay
useEffect(() => {
if (window.Meticulous?.isRunningAsTest) {
const storedData = window.Meticulous.getCustomValues();
if (storedData?.csvData) {
const rows = storedData.csvData.split('\n').map((row: string) => row.split(','));
setData(rows);
}
}
}, []);
return (
<div>
<input type="file" onChange={handleFileChange} accept=".csv" />
<table>
{data.map((row, i) => (
<tr key={i}>
{row.map((cell, j) => <td key={j}>{cell}</td>)}
</tr>
))}
</table>
</div>
);
}
function ValidatedFileUpload() {
const [error, setError] = useState<string>('');
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
setError('');
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
setError('Only JPEG, PNG, and GIF images are allowed');
return;
}
// Validate file size (5MB max)
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
setError('File must be smaller than 5MB');
return;
}
// Read and store file for replay
const reader = new FileReader();
reader.onload = (e) => {
const dataUrl = e.target?.result as string;
if (window.Meticulous?.recordCustomValues) {
window.Meticulous.recordCustomValues({
uploadedFile: dataUrl,
fileName: file.name,
fileType: file.type,
});
}
processFile(dataUrl);
};
reader.readAsDataURL(file);
};
// Restore during replay
useEffect(() => {
if (window.Meticulous?.isRunningAsTest) {
const stored = window.Meticulous.getCustomValues();
if (stored?.uploadedFile) {
processFile(stored.uploadedFile);
}
}
}, []);
return (
<div>
<input type="file" onChange={handleFileChange} />
{error && <div className="error">{error}</div>}
</div>
);
}
When to use: Complex scenarios where the custom values API isn't sufficient, or you need fine-grained control over replay timing.
Use the custom event API for advanced scenarios. This is more complex but offers maximum flexibility.
function AdvancedFileUpload() {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const fileData = e.target?.result;
// Record custom event
if (window.Meticulous?.recordCustomEvent) {
window.Meticulous.recordCustomEvent({
type: 'FILE_UPLOADED',
payload: {
fileName: file.name,
fileType: file.type,
fileData: fileData,
},
});
}
};
reader.readAsDataURL(file);
};
useEffect(() => {
// Listen for replay events
if (window.Meticulous?.onReplayCustomEvent) {
window.Meticulous.onReplayCustomEvent((event) => {
if (event.type === 'FILE_UPLOADED') {
processUploadedFile(event.payload);
}
});
}
}, []);
return <input type="file" onChange={handleFileChange} />;
}
async function uploadFile(file: File | null) {
// Skip file validation during tests
if (!window.Meticulous?.isRunningAsTest && !file) {
throw new Error('No file selected');
}
const formData = new FormData();
if (file) {
formData.append('file', file);
formData.append('userId', getCurrentUserId());
formData.append('uploadType', 'document');
}
const response = await fetch('/api/v1/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
},
body: formData,
});
if (!response.ok) {
throw new Error('Upload failed');
}
// Backend response is mocked during replay
const result = await response.json();
return result.fileUrl;
}
function uploadWithProgress(file: File | null, onProgress: (percent: number) => void) {
return new Promise((resolve, reject) => {
// Skip validation during tests
if (!window.Meticulous?.isRunningAsTest && !file) {
reject(new Error('No file selected'));
return;
}
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress(percentComplete);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error('Upload failed'));
}
});
xhr.addEventListener('error', () => reject(new Error('Network error')));
xhr.open('POST', '/api/upload');
const formData = new FormData();
if (file) {
formData.append('file', file);
}
xhr.send(formData);
});
}
function MultiFileUpload() {
const [files, setFiles] = useState<File[]>([]);
const handleFilesChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = Array.from(event.target.files || []);
setFiles(selectedFiles);
};
const handleUpload = async () => {
// Skip validation during tests
if (!window.Meticulous?.isRunningAsTest && files.length === 0) {
alert('Please select at least one file');
return;
}
const formData = new FormData();
files.forEach((file, index) => {
formData.append(`file${index}`, file);
});
await fetch('/api/upload-multiple', {
method: 'POST',
body: formData,
});
};
return (
<div>
<input type="file" multiple onChange={handleFilesChange} />
<p>{files.length} files selected</p>
<button onClick={handleUpload}>Upload All</button>
</div>
);
}
function ConditionalUpload() {
const [shouldValidate, setShouldValidate] = useState(false);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
if (shouldValidate && !window.Meticulous?.isRunningAsTest) {
// Only process file if validation is enabled and not in test
const reader = new FileReader();
reader.onload = (e) => {
validateFileContents(e.target?.result);
};
reader.readAsText(file);
} else {
// Skip to upload
uploadFile(file);
}
};
return (
<div>
<label>
<input
type="checkbox"
checked={shouldValidate}
onChange={(e) => setShouldValidate(e.target.checked)}
/>
Validate file contents before upload
</label>
<input type="file" onChange={handleFileChange} />
</div>
);
}
function SmartFileUpload() {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const dataUrl = e.target?.result as string;
// Check size limit for custom values API
const maxChars = window.Meticulous?.isProductionEnvironment ? 1_000_000 : 20_000_000;
if (dataUrl.length > maxChars) {
console.warn('File too large to store in custom values, skipping preview in tests');
// Fall back to Approach 1: just skip validation
} else {
window.Meticulous?.recordCustomValues({ fileData: dataUrl });
}
processFile(dataUrl);
};
reader.readAsDataURL(file);
};
return <input type="file" onChange={handleFileChange} />;
}
function TypeSafeUpload() {
const ALLOWED_TYPES = {
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'application/pdf': ['.pdf'],
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
if (!window.Meticulous?.isRunningAsTest) {
if (!Object.keys(ALLOWED_TYPES).includes(file.type)) {
alert(`Unsupported file type: ${file.type}`);
event.target.value = ''; // Clear input
return;
}
}
uploadFile(file);
};
const acceptString = Object.values(ALLOWED_TYPES).flat().join(',');
return <input type="file" accept={acceptString} onChange={handleFileChange} />;
}
Most apps should use Approach 1 (skip validation during tests) because:
- Simple and requires minimal code changes
- Works with Meticulous' network stubbing
- Tests the complete user flow including backend responses
- No file size limitations
Use Approach 2 (store file contents) only when:
- You need to test frontend file processing logic
- Files are small enough (under 1-20MB)
- You need to display file previews or parse file contents
Use Approach 3 (custom event API) for:
- Complex scenarios requiring fine-grained control
- Advanced timing or state management needs
- Integration with existing custom event systems
For more details on the Meticulous API, see the window.Meticulous object documentation.