diff --git a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts index cecdd6062..3db7ab211 100644 --- a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts +++ b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts @@ -1,8 +1,8 @@ // We need to set this as a global constant, so that using fake timers in Jest and Vitest won't override this on the global object. const TIMER = { - setImmediate: globalThis.setImmediate.bind(globalThis), - clearImmediate: globalThis.clearImmediate.bind(globalThis), - clearTimeout: globalThis.clearTimeout.bind(globalThis) + setTimeout: globalThis.setTimeout.bind(globalThis), + clearTimeout: globalThis.clearTimeout.bind(globalThis), + clearImmediate: globalThis.clearImmediate.bind(globalThis) }; /** @@ -114,10 +114,12 @@ export default class AsyncTaskManager { delete this.runningTasks[taskID]; this.runningTaskCount--; if (this.waitUntilCompleteTimer) { - TIMER.clearImmediate(this.waitUntilCompleteTimer); + TIMER.clearTimeout(this.waitUntilCompleteTimer); } if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) { - this.waitUntilCompleteTimer = TIMER.setImmediate(() => { + // In some cases, microtasks are used by transformed code and waitUntilComplete() is then resolved too early. + // To cater for this we use setTimeout() which has the lowest priority and will be executed last. + this.waitUntilCompleteTimer = TIMER.setTimeout(() => { this.waitUntilCompleteTimer = null; if ( !this.runningTaskCount && @@ -177,7 +179,7 @@ export default class AsyncTaskManager { this.runningTimers = []; if (this.waitUntilCompleteTimer) { - TIMER.clearImmediate(this.waitUntilCompleteTimer); + TIMER.clearTimeout(this.waitUntilCompleteTimer); this.waitUntilCompleteTimer = null; } diff --git a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts index c4737abde..ab36aaaf1 100644 --- a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts +++ b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts @@ -15,7 +15,8 @@ describe('DetachedWindowAPI', () => { }); afterEach(() => { - vi.restoreAllMocks(); + vi.clearAllMocks(); + resetMockedModules(); }); describe('get settings()', () => { @@ -68,7 +69,7 @@ describe('DetachedWindowAPI', () => { }; response.rawHeaders = ['content-length', '0']; - setTimeout(() => callback(response)); + setTimeout(() => callback(response), 20); } }, setTimeout: () => {} @@ -104,15 +105,22 @@ describe('DetachedWindowAPI', () => { window.requestAnimationFrame(() => { tasksDone++; }); + + // It is hard to replicate this bug, but in some cases, microtasks are used by transformed code and waitUntilComplete() is then resolved too early. + // This code seems to replicate the issue, at least somewhat. window.fetch('/url/1/').then((response) => { - response.json().then(() => { - window.fetch('/url/1/').then((response) => { - response.json().then(() => { + setImmediate(() => { + setImmediate(() => { + response.text().then(() => { window.fetch('/url/1/').then((response) => { - response.json().then(() => { - window.fetch('/url/1/').then((response) => { - response.json().then(() => { - tasksDone++; + setImmediate(() => { + setImmediate(() => { + response.text().then(() => { + setImmediate(() => { + setImmediate(() => { + tasksDone++; + }); + }); }); }); }); @@ -121,6 +129,7 @@ describe('DetachedWindowAPI', () => { }); }); }); + window.fetch('/url/2/').then((response) => { response.text().then(() => { tasksDone++; @@ -175,6 +184,33 @@ describe('DetachedWindowAPI', () => { describe('abort()', () => { it('Cancels all ongoing asynchrounous tasks.', async () => { await new Promise((resolve) => { + const responseText = '{ "test": "test" }'; + mockModule('https', { + request: () => { + return { + end: () => {}, + on: (event: string, callback: (response: HTTP.IncomingMessage) => void) => { + if (event === 'response') { + async function* generate(): AsyncGenerator { + yield responseText; + } + + const response = Stream.Readable.from(generate()); + + response.statusCode = 200; + response.statusMessage = ''; + response.headers = { + 'content-length': '0' + }; + response.rawHeaders = ['content-length', '0']; + + setTimeout(() => callback(response)); + } + }, + setTimeout: () => {} + }; + } + }); window.location.href = 'https://localhost:8080'; let isFirstWhenAsyncCompleteCalled = false; window.happyDOM?.waitUntilComplete().then(() => { @@ -235,7 +271,7 @@ describe('DetachedWindowAPI', () => { expect(isFirstWhenAsyncCompleteCalled).toBe(true); expect(isSecondWhenAsyncCompleteCalled).toBe(true); resolve(null); - }, 10); + }, 50); }); }); });