{"record":{"id":"d570c4ad7a309d5e","repo":"jackwener/OpenCLI","slug":"image-upload-failed-preview-did-not-appear","errorCode":null,"errorMessage":"Image upload failed: preview did not appear.","messagePattern":"Image upload failed: preview did not appear\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"clis/twitter/utils.js","lineNumber":198,"sourceCode":"            if (nativeInputFileSetter && nativeInputFileSetter.set) {\n              nativeInputFileSetter.set.call(input, dt.files);\n              assigned = input.files && input.files.length > 0;\n            }\n          } catch(e2) { /* ignore */ }\n        }\n        if (!assigned) return { ok: false, error: 'Could not assign files to input' };\n        input.dispatchEvent(new Event('change', { bubbles: true }));\n        input.dispatchEvent(new Event('input', { bubbles: true }));\n        return { ok: true };\n      })()\n    `);\n        if (!upload?.ok) {\n            throw new Error(`Image upload failed: ${upload?.error ?? 'unknown error'}`);\n        }\n    }\n    const uploadState = await waitForComposerMediaReady(page, 1);\n    if (!uploadState?.ok) {\n        throw new Error(uploadState?.message ?? 'Image upload failed: preview did not appear.');\n    }\n}\n\nexport function isRecoverableFileInputError(error) {\n    const msg = error instanceof Error ? error.message : String(error);\n    return /unknown action|not supported|not[-\\s]?allowed|notallowederror/i.test(msg);\n}\n\nexport async function waitForComposerMediaReady(page, expectedCount = 1) {\n    const iterations = Math.ceil(MEDIA_UPLOAD_TIMEOUT_MS / MEDIA_UPLOAD_POLL_MS);\n    return page.evaluate(`\n    (async () => {\n      const expected = ${JSON.stringify(expectedCount)};\n      for (let i = 0; i < ${JSON.stringify(iterations)}; i++) {\n        await new Promise(r => setTimeout(r, ${JSON.stringify(MEDIA_UPLOAD_POLL_MS)}));\n        const previewCount = document.querySelectorAll(\n          '[data-testid=\"attachments\"] img, [data-testid=\"attachments\"] video, [data-testid=\"attachments\"] [role=\"group\"], [data-testid=\"tweetPhoto\"]'\n        ).length;","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/utils.js#L180-L216","documentation":"Thrown by attachComposerImage when the file was technically assigned to the composer input but waitForComposerMediaReady(page, 1) never observes a preview thumbnail in the [data-testid=\"attachments\"] area within MEDIA_UPLOAD_TIMEOUT_MS. This post-upload DOM check exists because a 200-style success from setFileInput can mask a silent no-attachment post.","triggerScenarios":"Files assignment succeeded but Twitter never started processing the upload: the composer's React handlers did not pick up the synthetic change/input events, the upload was rejected server-side (bad/corrupt file, unsupported format), the page navigated or the user closed the composer, or X changed its attachments DOM so the preview selectors no longer match.","commonSituations":"Attaching unsupported formats (e.g. huge GIFs or HEIC-renamed files) that X silently drops; slow networks where upload takes longer than the polling timeout; running on a modified/translated X UI where the preview selectors or remove-button labels no longer match; the image exceeding Twitter's own upload limits even though it passed the CLI's MAX_IMAGE_SIZE_BYTES check.","solutions":["Re-run with a smaller, standard-format image (JPEG/PNG under a few MB) — X silently rejects files it cannot process","Check network connectivity/latency; a slow upload can exceed MEDIA_UPLOAD_TIMEOUT_MS before the preview renders","Reload the compose page and retry, confirming the preview thumbnail appears manually","If the UI was redesigned, update the preview selectors used by waitForComposerMediaReady to the current [data-testid=\"attachments\"] markup"],"exampleFix":"// before\nawait attachComposerImage(page, '/tmp/photo.heic'); // assigned, but X drops it -> preview never appears\n// after\nexecSync('sips -s format jpeg /tmp/photo.heic --out /tmp/photo.jpg');\nawait attachComposerImage(page, '/tmp/photo.jpg');","handlingStrategy":"retry","validationCode":"// Pre-encode check: only attach formats X reliably previews\nconst ok = /\\.(png|jpe?g|webp|gif)$/i.test(absImagePath) && fs.statSync(absImagePath).size < 5 * 1024 * 1024;\nif (!ok) throw new Error('Convert to a standard, reasonably sized JPEG/PNG/WebP/GIF before attaching');","typeGuard":"function composerMediaReady(state) {\n  return Boolean(state && typeof state === 'object' && state.ok === true);\n}","tryCatchPattern":"try {\n  await attachComposerImage(page, absPath);\n} catch (err) {\n  if (err.message === 'Image upload failed: preview did not appear.') {\n    await new Promise(r => setTimeout(r, 2000));\n    await attachComposerImage(page, absPath); // single retry after slow upload\n  } else { throw err; }\n}","preventionTips":["Attach standard formats (JPEG/PNG/WebP/GIF) at moderate sizes — X silently drops unsupported files","Allow extra time on slow connections; the preview poll has a fixed timeout","Reload the compose page between attempts so attachments start from a clean state","Manually confirm the preview renders once after UI changes to detect selector drift"],"tags":["image-upload","browser-automation","timeout","twitter-cli"],"backgroundTag":"upload-verification-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}