Skip to content

Commit

Permalink
Merge pull request #1448 from capricorn86/1447-waituntilcomplete-does…
Browse files Browse the repository at this point in the history
…nt-wait-long-enough-for-microtasks-to-complete

fix: [#1447] Fixes bug where waitUntilComplete() is resolved to early…
  • Loading branch information
capricorn86 committed May 29, 2024
2 parents db97904 + a6376f9 commit f021a33
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 16 deletions.
14 changes: 8 additions & 6 deletions packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts
Original file line number Diff line number Diff line change
@@ -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)
};

/**
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -177,7 +179,7 @@ export default class AsyncTaskManager {
this.runningTimers = [];

if (this.waitUntilCompleteTimer) {
TIMER.clearImmediate(this.waitUntilCompleteTimer);
TIMER.clearTimeout(this.waitUntilCompleteTimer);
this.waitUntilCompleteTimer = null;
}

Expand Down
56 changes: 46 additions & 10 deletions packages/happy-dom/test/window/DetachedWindowAPI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ describe('DetachedWindowAPI', () => {
});

afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
resetMockedModules();
});

describe('get settings()', () => {
Expand Down Expand Up @@ -68,7 +69,7 @@ describe('DetachedWindowAPI', () => {
};
response.rawHeaders = ['content-length', '0'];

setTimeout(() => callback(response));
setTimeout(() => callback(response), 20);
}
},
setTimeout: () => {}
Expand Down Expand Up @@ -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++;
});
});
});
});
});
Expand All @@ -121,6 +129,7 @@ describe('DetachedWindowAPI', () => {
});
});
});

window.fetch('/url/2/').then((response) => {
response.text().then(() => {
tasksDone++;
Expand Down Expand Up @@ -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<string> {
yield responseText;
}

const response = <HTTP.IncomingMessage>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(() => {
Expand Down Expand Up @@ -235,7 +271,7 @@ describe('DetachedWindowAPI', () => {
expect(isFirstWhenAsyncCompleteCalled).toBe(true);
expect(isSecondWhenAsyncCompleteCalled).toBe(true);
resolve(null);
}, 10);
}, 50);
});
});
});
Expand Down

0 comments on commit f021a33

Please sign in to comment.