| ... | @@ -64,6 +64,7 @@ let renderer: FileRenderer; | ... | @@ -64,6 +64,7 @@ let renderer: FileRenderer; |
| 64 | let tokenizer: Tokenizer; | 64 | let tokenizer: Tokenizer; |
| 65 | let file: FileContents; | 65 | let file: FileContents; |
| 66 | let document: TextDocument<unknown>; | 66 | let document: TextDocument<unknown>; |
| | 67 | const animationFrames = new Set<ReturnType<typeof setImmediate>>(); |
| 67 | | 68 | |
| 68 | function nextResponse(): Promise<HeldResponse> { | 69 | function nextResponse(): Promise<HeldResponse> { |
| 69 | const response = responses.shift(); | 70 | const response = responses.shift(); |
| ... | @@ -147,10 +148,18 @@ beforeEach(async () => { | ... | @@ -147,10 +148,18 @@ beforeEach(async () => { |
| 147 | responses = []; | 148 | responses = []; |
| 148 | responseWaiters = []; | 149 | responseWaiters = []; |
| 149 | terminationPromises = []; | 150 | terminationPromises = []; |
| 150 | vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => | 151 | vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { |
| 151 | setImmediate(() => callback(0)), | 152 | const frame = setImmediate(() => { |
| 152 | ); | 153 | animationFrames.delete(frame); |
| 153 | vi.stubGlobal("cancelAnimationFrame", clearImmediate); | 154 | callback(0); |
| | 155 | }); |
| | 156 | animationFrames.add(frame); |
| | 157 | return frame; |
| | 158 | }); |
| | 159 | vi.stubGlobal("cancelAnimationFrame", (frame: ReturnType<typeof setImmediate>) => { |
| | 160 | animationFrames.delete(frame); |
| | 161 | clearImmediate(frame); |
| | 162 | }); |
| 154 | vi.stubGlobal("window", { matchMedia: () => ({ matches: true }) }); | 163 | vi.stubGlobal("window", { matchMedia: () => ({ matches: true }) }); |
| 155 | pool = new WorkerPoolManager( | 164 | pool = new WorkerPoolManager( |
| 156 | // Adapt browser transport only; Pierre's real worker produces each response. | 165 | // Adapt browser transport only; Pierre's real worker produces each response. |
| ... | @@ -176,15 +185,41 @@ beforeEach(async () => { | ... | @@ -176,15 +185,41 @@ beforeEach(async () => { |
| 176 | renderContents(); | 185 | renderContents(); |
| 177 | }); | 186 | }); |
| 178 | | 187 | |
| 179 | afterEach(async () => { | 188 | async function cleanUpFixture() { |
| 180 | tokenizer?.cleanUp(); | 189 | tokenizer?.cleanUp(); |
| 181 | renderer?.cleanUp(); | 190 | renderer?.cleanUp(); |
| 182 | pool?.terminate(); | 191 | pool?.terminate(); |
| 183 | await Promise.all(terminationPromises); | 192 | await Promise.all(terminationPromises); |
| | 193 | // Pool termination can queue a final broadcast after its workers have exited. |
| | 194 | for (const frame of animationFrames) clearImmediate(frame); |
| | 195 | animationFrames.clear(); |
| 184 | vi.unstubAllGlobals(); | 196 | vi.unstubAllGlobals(); |
| 185 | }); | 197 | } |
| | 198 | |
| | 199 | afterEach(cleanUpFixture); |
| 186 | | 200 | |
| 187 | describe("editable file highlighting", () => { | 201 | describe("editable file highlighting", () => { |
| | 202 | it("cleans up an already terminated worker pool", async () => { |
| | 203 | (await nextResponse()).deliver(); |
| | 204 | expect(pool.getStats().totalWorkers).toBe(1); |
| | 205 | pool.terminate(); |
| | 206 | await Promise.all(terminationPromises); |
| | 207 | expect(pool.getStats().totalWorkers).toBe(0); |
| | 208 | |
| | 209 | const animationFrame = globalThis.requestAnimationFrame; |
| | 210 | const cancelFrame = globalThis.cancelAnimationFrame; |
| | 211 | const window = globalThis.window; |
| | 212 | try { |
| | 213 | await cleanUpFixture(); |
| | 214 | // Deliver the real Immediate queue after cleanup has removed the browser globals. |
| | 215 | await new Promise<void>((resolve) => setImmediate(resolve)); |
| | 216 | } finally { |
| | 217 | vi.stubGlobal("requestAnimationFrame", animationFrame); |
| | 218 | vi.stubGlobal("cancelAnimationFrame", cancelFrame); |
| | 219 | vi.stubGlobal("window", window); |
| | 220 | } |
| | 221 | }); |
| | 222 | |
| 188 | it("still accepts an asynchronous highlight when the file has not changed", async () => { | 223 | it("still accepts an asynchronous highlight when the file has not changed", async () => { |
| 189 | expect(renderContents()).not.toContain('style="color:'); | 224 | expect(renderContents()).not.toContain('style="color:'); |
| 190 | (await nextResponse()).deliver(); | 225 | (await nextResponse()).deliver(); |