Repository navigation
Conversation
…dcare/api-emulator The derivative API specs and the editor/plugin browser tests now get their Upload API and CDN from the emulator instead of hand-written responses: - specs inject a fetch backed by the emulator's handle(), and getFileInfo runs against an emulator server through upload-client's own transport (replacing the isReadyPoll module mock); - browser tests route Uploadcare's hosts (and the tests' cdn.example.com cname) to handle() through a vitest browser command, with a fresh session per test; editor tests use the instant-derivatives project so a generation doesn't wait out the 1.5s poll interval; - Bearer tokens are real ones the emulator verifies. Stubs stay only where the emulator can't produce the case: a bare non-JSON failure, a poll that hangs until aborted or never finishes. The emulator is a TEMPORARY file: dependency on an unreleased checkout, so npm ci fails in CI until it ships and this becomes a version range. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…used The emulator answers the page's fetch on its own; recordRequests() only records requests, so a bare call does nothing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ort test A bare rejects.toThrow() also passed on a 404 or an emulator that never started. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e for it The page only sees a generic network failure, so log the method and URL before aborting, as the spec-side emulatorFetch does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ests Counting requests also passed when the emulator refused the token on a status poll; a finished result proves every request was accepted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A moderated prompt from the emulator must reach the host as uc:error, show its own message, and leave the editor in generate mode with the prompt kept. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The absolute local dependency prevents installation in CI and fresh checkouts.
Review effort: Balanced
Findings: 1
What changed in this PR
Migrates Uploadcare API/CDN tests from hand-written responses to the stateful API emulator.
Changes:
- Adds shared emulator wiring for specs and browser tests.
- Updates API/editor tests to use realistic jobs, files, and auth tokens.
- Documents the testing convention and adds the temporary emulator dependency.
| File | Description |
|---|---|
packages/ai-image-editor/vitest.config.ts |
Registers emulator setup and browser commands. |
packages/ai-image-editor/tests/setup.ts |
Resets emulator state before each browser test. |
packages/ai-image-editor/tests/emulator.ts |
Routes browser API/CDN traffic through the emulator. |
packages/ai-image-editor/tests/editor/sizing.e2e.test.tsx |
Removes obsolete fetch stubbing. |
packages/ai-image-editor/tests/editor/layout.e2e.test.tsx |
Uses emulator-backed generation. |
packages/ai-image-editor/tests/editor/history.e2e.test.tsx |
Removes manual response fixtures. |
packages/ai-image-editor/tests/editor/harness.ts |
Adds emulator-aware request recording and fixtures. |
packages/ai-image-editor/tests/editor/generation.e2e.test.tsx |
Tests realistic success, refusal, and abort flows. |
packages/ai-image-editor/tests/editor/filename.e2e.test.tsx |
Records emulator generation requests. |
packages/ai-image-editor/tests/editor/auth-token.e2e.test.tsx |
Uses valid signed tokens and completed results. |
packages/ai-image-editor/tests/editor/aspect-ratio.e2e.test.tsx |
Moves request assertions to emulator traffic. |
packages/ai-image-editor/src/entities/provider/api/uploadcareDerivativeApi.test.ts |
Exercises derivative and file-info behavior against the emulator. |
packages/ai-image-editor/src/entities/provider/api/uploadcareApiClient.test.ts |
Replaces response fixtures with emulator scenarios. |
packages/ai-image-editor/src/entities/provider/api/emulator.testing.ts |
Provides shared spec-side emulator helpers. |
packages/ai-image-editor/package.json |
Adds a machine-local emulator dependency. |
package-lock.json |
Locks the temporary local dependency. |
AGENTS.md |
Documents emulator testing practices. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| "devDependencies": { | ||
| "@custom-elements-manifest/analyzer": "^0.11.0", | ||
| "@custom-elements-manifest/to-markdown": "^0.1.0", | ||
| "@uploadcare/api-emulator": "file:/Users/nd0ut/workspace/uc-586-sync/packages/api-emulator", |
There was a problem hiding this comment.
Known, and it can't be fixed yet. @uploadcare/api-emulator isn't on npm (npm view returns 404), so there's no version range to point at. The PR description lists this as the one step left before merge: once uploadcare/uploadcare-js-api-clients#586 is released, the file: path gets replaced with a version range, the lockfile is regenerated, and the TEMPORARY comments come out. Until then CI is expected to fail at npm ci.
🤖 Written by Claude Code on behalf of @nd0ut
The emulator's CDN route only answers UUID or group-shaped paths, so 'resolver-test-uuid-0001', 'first-uuid' and 'second-uuid' fell through to the harness's "does not implement" warning. UUID-shaped ids get the same quiet 404 the real CDN gives for an unknown file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…re cleanup The locale-switch test waited only for the editor's German label. The editor loads its own strings, while the uploader's LocaleManager loads de.js on its own. If that import was still pending at cleanup(), the destroyed uploader resumed, read the nulled *pluginManager and threw an unhandled rejection that vitest blamed on the next test. Delaying the uploader's de loader by 500ms reproduced it every time. file-uploader 1.34.0 still has this bug (fixed in uploadcare/file-uploader#1081 and not released yet). The test now also waits for the uploader's own 'cancel' string to switch to German. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
msw 2.15.0 (no vite peer; satisfies @vitest/mocker's ^2.4.9) and @mswjs/interceptors 0.45.7 at the root, since msw's nested 0.41.9 fires a single upload progress event where 0.45.x fires one per body chunk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The browser suite no longer fulfils Uploadcare requests from a Node page.route. tests/emulator.ts now runs handle() inside the page: an MSW Service Worker answers fetch and <img> loads, @mswjs/interceptors' XMLHttpRequestInterceptor answers the uploader's XHR so upload progress fires per chunk. Both share the default session, reset before each test. Only Uploadcare hosts and cdn.example.com are answered (handle() routes by path alone); everything else passes through. The signing command stays Node-side in tests/commands.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d of passing them through Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…re/api-emulator/browser setupEmulator() replaces the local MSW worker, XHR interceptor and response hold. Only the hosts the emulator answers are emulated now, not every *.uploadcare.com; other origins still pass through. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Under `unhandled: 'passthrough'` the emulator now refuses any Uploadcare host it has no answer for (api.uploadcare.com, social.uploadcare.com, a <sub>.ucarecdn.com), restoring the guard the old tests/emulator.ts gave; only non-Uploadcare origins such as Unsplash pass through. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

What
The derivative API specs and the editor and plugin browser tests no longer hand-write Uploadcare responses. They get the Upload API and CDN from
@uploadcare/api-emulator, a stateful stand-in: a generation is a real job, its status poll walksprocessing→uploading→success, and the result is a stored file that/info/describes and the CDN serves.src/entities/provider/api/emulator.testing.tswires the specs.getFileInfogoes through upload-client's own transport, so those specs start a real emulator server, replacing theisReadyPollmodule mock.tests/setup.tsruns the emulator in the page withsetupEmulator()from@uploadcare/api-emulator/browser, per MSW's XHR progress recipe: an MSW Service Worker answersfetch(the derivative API) and<img>loads (results on the canvas), and@mswjs/interceptors'XMLHttpRequestInterceptoranswers the uploader's XHR in-process, soxhr.uploadprogress fires once per body chunk instead of not at all. Both resolve against the one default session, whichtests/setup.tsresets before every test. Only Uploadcare's hosts andcdn.example.com(cdnHosts) are answered. Withunhandled: 'passthrough'the page's own modules and the Unsplash image pass through, while an Uploadcare host the emulator doesn't answer fails. An Uploadcare URL it has no route for logs@uploadcare/api-emulator does not implement <method> <url>and fails as a network error. Signing stays in Node (tests/commands.ts), since@uploadcare/signed-uploads/serverneedsnode:crypto.msw2.15.0 and@mswjs/interceptors0.45.7 are pinned at the root as the export's peers: the 0.41.x interceptors nested under msw fire a single upload progress event, 0.45.x fires one per chunk.mintAuthToken()).DERIVATIVE_INSTANT_PUBLIC_KEY, so a generation finishes on its first poll instead of waiting out the editor's 1.5s interval several times over.AGENTS.mddocuments all of this for the next person writing a test.Why
The derivative endpoints are not in Uploadcare's published OpenAPI document, so the hand-written responses here were the only description of them, and nothing checked that they agreed with each other or with the Upload API. The emulator's derivative routes were modelled on this repo's
UploadcareApiClientand its dev-only Zod schemas, so the same behaviour is now written down once and shared with uploadcare/file-uploader#1081.Result
Lint, typecheck, the workspace build,
npm testandtest:nextpass: 255 passed and 3 skipped inai-image-editor, 25 passed inreact-ai-image-editor, and the Next fixture builds.Moving to the emulator also made a few tests honest:
getFileInfoabort test requires the cancel error (/cancel/i) instead of passing on any rejection.CONTENT_MODERATED_PROMPTand checks thatuc:errorfires once withcontent_moderated, the error box shows the localized message, the canvas stays empty, the editor stays ingeneratemode and the prompt is kept.recordRequests();calls whose result nobody read are gone.Flakes: the locale-switch test in
tests/plugin.e2e.test.tscleaned up while the uploader was still loading its own German strings, which made the published@uploadcare/file-uploader@1.34.0throwshared instance for key "*pluginManager" is not availableafter teardown (about 1 run in 5). The test now waits for the uploader's locale switch too, and made-up CDN ids are UUID-shaped so the emulator stops logging misses. 10 consecutive full runs were clean. The race itself is fixed at its source in uploadcare/file-uploader#1081 and reaches this repo with the next file-uploader release.Not ready to land until the emulator is released
@uploadcare/api-emulatoris a TEMPORARYfile:devDependency on a local checkout of uploadcare/uploadcare-js-api-clients#586, marked next to its imports. CI fails atnpm ciuntil that PR is released. The one remaining step is replacing thefile:path with a version range and dropping the TEMPORARY comments.Merge Danger
Door: two-way. Tests and
AGENTS.mdonly; no shipped code changes.Blast Radius: test suite.
Follow-ups
npm run lintfails locally ifpackages/react-ai-image-editor/tests/next-fixture/.next/is left over from atest:nextrun, because Biome lints that gitignored folder. CI starts from a fresh checkout and is unaffected; ignoring the folder in the Biome config would fix it locally.🤖 Generated with Claude Code