Skip to content

test(ai-image-editor): run the specs and browser tests against @uploadcare/api-emulator - #97

Open
nd0ut wants to merge 16 commits into
mainfrom
test/api-emulator
Open

nd0ut wants to merge 16 commits into
mainfrom
test/api-emulator

Conversation

@nd0ut

@nd0ut nd0ut commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

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 walks processing → uploading → success, and the result is a stored file that /info/ describes and the CDN serves.

specs (happy-dom)
  UploadcareApiClient({ fetch: emulatorFetch() })   -> handle()
  getFileInfo via upload-client's transport          -> emulator server (./listen)
browser e2e (in the page)
  MSW Service Worker: fetch, <img>                  -> handle()
  XMLHttpRequestInterceptor: the uploader's XHR     -> handle(), upload progress per body chunk
  mintAuthToken browser command                     -> Node, signs with node:crypto
  • src/entities/provider/api/emulator.testing.ts wires the specs. getFileInfo goes through upload-client's own transport, so those specs start a real emulator server, replacing the isReadyPoll module mock.
  • tests/setup.ts runs the emulator in the page with setupEmulator() from @uploadcare/api-emulator/browser, per MSW's XHR progress recipe: an MSW Service Worker answers fetch (the derivative API) and <img> loads (results on the canvas), and @mswjs/interceptors' XMLHttpRequestInterceptor answers the uploader's XHR in-process, so xhr.upload progress fires once per body chunk instead of not at all. Both resolve against the one default session, which tests/setup.ts resets before every test. Only Uploadcare's hosts and cdn.example.com (cdnHosts) are answered. With unhandled: '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/server needs node:crypto.
  • msw 2.15.0 and @mswjs/interceptors 0.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.
  • Bearer tokens are real ones the emulator verifies (mintAuthToken()).
  • Editor tests use 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.
  • Hand-written stubs stay only where the emulator can't produce the case: a bare non-JSON failure, and a poll that hangs until aborted or never finishes. Each says why next to it.
  • AGENTS.md documents 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 UploadcareApiClient and 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 test and test:next pass: 255 passed and 3 skipped in ai-image-editor, 25 passed in react-ai-image-editor, and the Next fixture builds.

Moving to the emulator also made a few tests honest:

  • The auth-token e2e tests now wait for the result to land instead of counting requests, so a refused status poll fails the test. Before, "a run that finishes proves the token was good" was never checked.
  • The getFileInfo abort test requires the cancel error (/cancel/i) instead of passing on any rejection.
  • A new browser test covers a refused generation. It sends CONTENT_MODERATED_PROMPT and checks that uc:error fires once with content_moderated, the error box shows the localized message, the canvas stays empty, the editor stays in generate mode and the prompt is kept.
  • Seven bare recordRequests(); calls whose result nobody read are gone.

Flakes: the locale-switch test in tests/plugin.e2e.test.ts cleaned up while the uploader was still loading its own German strings, which made the published @uploadcare/file-uploader@1.34.0 throw shared instance for key "*pluginManager" is not available after 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-emulator is a TEMPORARY file: devDependency on a local checkout of uploadcare/uploadcare-js-api-clients#586, marked next to its imports. CI fails at npm ci until that PR is released. The one remaining step is replacing the file: path with a version range and dropping the TEMPORARY comments.

Merge Danger

Door: two-way. Tests and AGENTS.md only; no shipped code changes.

Blast Radius: test suite.

Follow-ups

  • npm run lint fails locally if packages/react-ai-image-editor/tests/next-fixture/.next/ is left over from a test:next run, 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

nd0ut and others added 6 commits October 6, 2026 03:31
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The absolute local dependency prevents installation in CI and fresh checkouts.

Review effort: Balanced
Findings: 1 High severity

Open (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",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

nd0ut and others added 10 commits October 6, 2026 09:55
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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants