Skip to content

test(setup): observe promises in the ClipboardItem stub - #202

Merged
johnny4young merged 1 commit into
mainfrom
codex/clipboard-stub-rejections
Sep 17, 2026
Merged

johnny4young merged 1 commit into
mainfrom
codex/clipboard-stub-rejections

Conversation

@johnny4young

Copy link
Copy Markdown
Owner

Summary

Fixes the unhandled Canceled rejection that failed the Unit job on the main push after #201, while all 8,089 tests passed. The cause is the ClipboardItem stub in tests/setup.ts: it stores the promises it is given without observing them. It now observes them the way a real ClipboardItem does, and a regression test pins the behavior.

Root cause

  1. The workaround. jsdom's user agent carries AppleWebKit without a Chrome or Safari token. When Monaco initialises its services it therefore installs its WebKit clipboard workaround: click and keydown listeners on the document body.
  2. The promise. Each click passes a pending promise into new ClipboardItem({ 'text/plain': promise }), and the next click cancels the previous one.
  3. The gap. A real ClipboardItem observes those promises. The stub only reads them in getType(), which nothing calls, so every cancellation became an unhandled rejection. Vitest fails the run on it even when every test passes.
  4. Why it was intermittent. In the unit suite, Monaco gets initialised by JsonSyntaxOutput, which imports it without awaiting and gives up if the component has already unmounted. On a fast machine the tests finish first. On a slow runner the component is still mounted, Monaco initialises, and later clicks in the same file hit the workaround.

The CI run (35143874393) reported the rejection at monaco-editor/.../clipboardService.js:74, originating in tests/components/UtilityToolbarApply.test.tsx. The failed smoke artifact upload was only a consequence: the test step stopped before writing the smoke summary. Production is unaffected, because browsers use their real ClipboardItem.

Changes

  • tests/setup.ts. The stub's constructor attaches a no-op rejection handler to each promise-like value. getType() still awaits the original promise, so a reader of a rejected type still sees the rejection; types, getType, supports and the writable global are unchanged.
  • tests/clipboardItemStub.test.tsx (new).
    • Stub level. A promise rejected after the item is built leaves no unhandled rejection. A reader of a rejected type still gets the error. Resolved promises, strings and blobs stay readable, and unknown types still throw the MIME error.
    • Monaco level. Monaco is initialised the way JsonSyntaxOutput does it, and the test clicks three times with user-event. It asserts that the workaround wrote to the clipboard, so the guard fails loudly if Monaco ever drops it, and that no rejection went unhandled.

Type of change

  • Bug fix
  • New feature
  • Refactor / internal
  • Docs
  • Build / CI / tooling

How was this tested?

  • Regression test red before the fix. Against the original stub, the delayed-rejection case failed with Error: write cancelled and the Monaco case with two Canceled rejections. With the fix, all four cases pass.
  • The failing CI path, made deterministic.
    • Temporary edit: UtilityToolbarApply.test.tsx initialises Monaco in a beforeAll, which is what a slow runner ends up doing.
    • With the original stub: its 10 tests passed alongside 6 unhandled Canceled rejections, the CI signature.
    • With the fixed stub: its 10 tests passed with none.
  • Related suites. qrCode.test.ts, QrCodePanel.test.tsx, UtilityToolbarApply.test.tsx and DeveloperUtilitiesModal.test.tsx pass 77 tests.
  • Unit. pnpm test -- --run passes 732 files and 8,095 tests with no unhandled errors: the counts on main plus the new file.
  • Other gates. tsc --noEmit, typecheck:tests, lint including the telemetry audit and changelog:check pass. The new test and tests/setup.ts also type-check clean under a scratch config. knip reports only the existing website test dependencies.
  • e2e and smokes. Not applicable: only the test environment changes.

Checklist

  • pnpm test -- --run passes
  • pnpm exec tsc --noEmit passes
  • pnpm run lint passes
  • pnpm run check:i18n and pnpm run check:i18n:copy pass (if UI/i18n touched) — no UI or i18n in this diff
  • User-facing changes were verified in a running build (web or desktop) — no user-facing change
  • Docs updated if shortcuts, execution, or workflow behavior changed

Risk and review notes

  • No silencing. No global unhandled-error filter is added; only rejections the stub itself receives are observed, as a real ClipboardItem would.
  • Test cost. The Monaco-level case loads Monaco in jsdom, which several other tests already do.
  • Alternative not taken. Giving jsdom a Chrome-like user agent would stop Monaco installing the workaround, but it would change platform detection for every test.

jsdom's user agent makes Monaco install its WebKit clipboard workaround,
which passes a pending promise into a new ClipboardItem on every click and
cancels the previous one. The stub stored those promises without observing
them, so each cancellation became an unhandled rejection that failed the
vitest run even when every test passed, as on the main push after the last
merge.

- the stub observes the promises it receives, like a real ClipboardItem,
  while getType still awaits the original so a reader sees a rejection
- a regression test covers a promise rejected after the item is built, a
  reader of a rejected type, resolved and inline values, and three clicks
  with Monaco initialised, asserting that the workaround wrote to the
  clipboard and that no rejection went unhandled
- with Monaco initialised up front in UtilityToolbarApply.test.tsx, the old
  stub produced six unhandled Canceled rejections and the new one none

Changelog: none
@johnny4young
johnny4young merged commit 4827256 into main Sep 17, 2026
10 checks passed
@johnny4young
johnny4young deleted the codex/clipboard-stub-rejections branch September 17, 2026 01:01
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.

1 participant