diff --git a/packages/web-shell/client/test/setup.test.ts b/packages/web-shell/client/test/setup.test.ts new file mode 100644 index 00000000000..ecaddb75552 --- /dev/null +++ b/packages/web-shell/client/test/setup.test.ts @@ -0,0 +1,23 @@ +// @vitest-environment jsdom + +import { describe, expect, it } from 'vitest'; + +describe('web-shell test setup', () => { + it('provides range layout methods used by CodeMirror', () => { + const range = document.createRange(); + + expect(typeof range.getClientRects).toBe('function'); + expect(typeof range.getBoundingClientRect).toBe('function'); + expect(range.getClientRects()).toHaveLength(0); + expect(range.getBoundingClientRect()).toMatchObject({ + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + x: 0, + y: 0, + }); + }); +}); diff --git a/packages/web-shell/client/test/setup.ts b/packages/web-shell/client/test/setup.ts index 79d2dfd9d7a..ace65e79f56 100644 --- a/packages/web-shell/client/test/setup.ts +++ b/packages/web-shell/client/test/setup.ts @@ -6,9 +6,35 @@ import { vi } from 'vitest'; const globalWithDom = globalThis as typeof globalThis & { Element?: typeof Element; + Range?: typeof Range; ResizeObserver?: typeof ResizeObserver; }; +function createEmptyDOMRect(): DOMRect { + if (typeof DOMRect === 'function') { + return new DOMRect(0, 0, 0, 0); + } + + return { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + x: 0, + y: 0, + toJSON: () => ({}), + } as DOMRect; +} + +function createEmptyDOMRectList(): DOMRectList { + return { + length: 0, + item: () => null, + } as DOMRectList; +} + if (typeof globalWithDom.ResizeObserver === 'undefined') { globalWithDom.ResizeObserver = class ResizeObserverStub { observe() {} @@ -24,6 +50,16 @@ if ( globalWithDom.Element.prototype.scrollIntoView = () => {}; } +if (typeof globalWithDom.Range !== 'undefined') { + const rangePrototype = globalWithDom.Range.prototype as Range & { + getBoundingClientRect?: () => DOMRect; + getClientRects?: () => DOMRectList; + }; + + rangePrototype.getBoundingClientRect ??= createEmptyDOMRect; + rangePrototype.getClientRects ??= createEmptyDOMRectList; +} + if (typeof navigator !== 'undefined' && !navigator.clipboard) { Object.defineProperty(navigator, 'clipboard', { configurable: true, diff --git a/packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx b/packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx index 36d24597f30..76db901a4b3 100644 --- a/packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx +++ b/packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx @@ -5378,21 +5378,22 @@ describe('DaemonSessionProvider', () => { modelServices: [], workspaceCwd: '/mock-workspace', }); + const releaseHeartbeatFailure = createDeferred(); const heartbeat = vi.fn(async () => { + await releaseHeartbeatFailure.promise; throw Object.assign(new Error('session gone'), { status: 410 }); }); + const submitStarted = createDeferred(); const session = createMockSession({ heartbeat, - submitPrompt: vi.fn( - (_req: unknown, signal?: AbortSignal) => - new Promise((_resolve, reject) => { - signal?.addEventListener( - 'abort', - () => reject(createAbortError()), - { once: true }, - ); - }), - ), + submitPrompt: vi.fn((_req: unknown, signal?: AbortSignal) => { + submitStarted.resolve(); + return new Promise((_resolve, reject) => { + signal?.addEventListener('abort', () => reject(createAbortError()), { + once: true, + }); + }); + }), events: createIdleEvents(), }); sdkMocks.sessions.push(session); @@ -5415,10 +5416,12 @@ describe('DaemonSessionProvider', () => { let promptResult: Promise | undefined; await act(async () => { promptResult = providerActions.sendPrompt('still running'); + await submitStarted.promise; await flushPromises(); }); await act(async () => { + releaseHeartbeatFailure.resolve(); await wait(50); await flushPromises(); });