fix(cli): upload screenshot recordings as image/png

Screenshot recordings write screenshot.png with output: 'screenshot'
instead of recording.mp4, so the upload path failed every still with
"Missing recording.mp4". Derive the upload source and content type from
data.output: screenshots stream their PNG capture with image/png, videos
keep recording.mp4 / video/mp4.
This commit is contained in:
Olli Paloviita
2026-06-21 19:26:16 +03:00
parent ca8a1d205a
commit 5773fcf8ea
2 changed files with 102 additions and 4 deletions
+90
View File
@@ -1438,6 +1438,96 @@ describe('CLI', () => {
)
})
it('uploads a screenshot recording as image/png from screenshot.png', async () => {
mockReaddir.mockResolvedValue(['home'])
mockReadFile.mockImplementation(async (path: string | URL) => {
const pathString = String(path)
if (pathString.endsWith('package.json')) {
return JSON.stringify({ version: '0.0.32' })
}
if (pathString.endsWith('data.json')) {
return JSON.stringify({
events: [],
output: 'screenshot',
screenshot: {
path: 'screenshot.png',
width: 1920,
height: 1080,
deviceScaleFactor: 1,
},
metadata: { videoName: 'home' },
})
}
return ''
})
// The screenshot capture exists, but there is no recording.mp4.
mockExistsSync.mockImplementation(
(path: string) =>
path.endsWith('data.json') || path.endsWith('screenshot.png')
)
let recordingPut: RequestInit | undefined
mockFetch.mockImplementation(
async (input: string | URL, init?: RequestInit) => {
const url = String(input)
if (url.endsWith('/cli/upload/start')) {
return {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({
recordingId: 'recording_123',
projectId: 'project_123',
}),
text: vi.fn().mockResolvedValue(''),
}
}
if (url.endsWith('/cli/upload/recording_123/recording')) {
recordingPut = init
return {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({}),
text: vi.fn().mockResolvedValue(''),
}
}
return {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue({}),
text: vi.fn().mockResolvedValue(''),
}
}
)
const { uploadRecordings } = await import('./cli')
const result = await uploadRecordings(
'/repo/.screenci',
'Test Project',
'https://api.screenci.test',
'test-secret'
)
expect(result).toEqual({
projectId: 'project_123',
recordId: expect.any(String),
hadFailures: false,
studioNotices: [],
failedVideoNames: [],
failedVideoMessages: [],
plan: null,
})
// The capture is streamed from screenshot.png with an image content type.
expect(mockCreateReadStream).toHaveBeenCalledWith(
expect.stringContaining('screenshot.png')
)
expect(
(recordingPut?.headers as Record<string, string> | undefined)?.[
'Content-Type'
]
).toBe('image/png')
})
it('fails the upload when an asset check fails', async () => {
mockReaddir.mockResolvedValue(['demo-video'])
mockReadFile.mockImplementation(async (path: string | URL) => {
+12 -4
View File
@@ -567,7 +567,15 @@ async function uploadRecordingCandidate(
try {
uploadAbort.throwIfAborted()
const recordingPath = resolve(screenciDir, entry, 'recording.mp4')
// A screenshot recording uploads its raw page capture (always a PNG) through
// the same recording endpoint a video uses; the renderer reads those bytes as
// the capture. Videos upload recording.mp4. Output kind defaults to 'video'.
const isScreenshot = data.output === 'screenshot'
const recordingFileName = isScreenshot
? (data.screenshot?.path ?? 'screenshot.png')
: 'recording.mp4'
const recordingContentType = isScreenshot ? 'image/png' : 'video/mp4'
const recordingPath = resolve(screenciDir, entry, recordingFileName)
if (!existsSync(recordingPath)) {
progressReporter.complete(progressIndex, 'failure')
return {
@@ -575,7 +583,7 @@ async function uploadRecordingCandidate(
videoId: null,
hadFailure: true,
videoName,
failureMessage: `Missing recording.mp4 for "${videoName}"`,
failureMessage: `Missing ${recordingFileName} for "${videoName}"`,
recordId,
}
}
@@ -665,7 +673,7 @@ async function uploadRecordingCandidate(
const fileStat = await stat(recordingPath)
if (verbose) {
logger.info(
`Uploading recording.mp4 size=${(fileStat.size / 1024 / 1024).toFixed(1)}MB`
`Uploading ${recordingFileName} size=${(fileStat.size / 1024 / 1024).toFixed(1)}MB`
)
}
const recordingResponse = await withUploadRetry(async () => {
@@ -680,7 +688,7 @@ async function uploadRecordingCandidate(
return await fetch(`${apiUrl}/cli/upload/${recordingId}/recording`, {
method: 'PUT',
headers: {
'Content-Type': 'video/mp4',
'Content-Type': recordingContentType,
'Content-Length': String(fileStat.size),
'X-ScreenCI-Secret': secret,
...(elevenLabsApiKey