roymondchen 3fe9cf9b66 fix(editor): 修复 IdleTask 任务异常或 clearTasks 时队列卡住的问题
单个任务失败不再中断整批依赖收集,并在 runTaskQueue 中正确重置 taskHandle、始终通过实例队列消费任务。
2026-07-28 15:32:58 +08:00

241 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { IdleTask } from '@editor/utils/dep/idle-task';
import * as logger from '@editor/utils/logger';
const fakeIdleDeadline = (timeRemaining: number, callsBeforeZero = 1): IdleDeadline => {
let remainingCalls = callsBeforeZero;
return {
didTimeout: false,
timeRemaining: () => {
if (remainingCalls <= 0) return 0;
remainingCalls -= 1;
return timeRemaining;
},
};
};
describe('IdleTask', () => {
let originalRic: any;
let originalCancel: any;
let scheduled: { cb: IdleRequestCallback; opts?: IdleRequestOptions }[] = [];
let idCounter = 0;
beforeEach(() => {
scheduled = [];
idCounter = 0;
originalRic = globalThis.requestIdleCallback;
originalCancel = globalThis.cancelIdleCallback;
globalThis.requestIdleCallback = ((cb: IdleRequestCallback, opts?: IdleRequestOptions) => {
scheduled.push({ cb, opts });
idCounter += 1;
return idCounter;
}) as any;
globalThis.cancelIdleCallback = vi.fn();
});
afterEach(() => {
globalThis.requestIdleCallback = originalRic;
globalThis.cancelIdleCallback = originalCancel;
});
test('入队普通任务后调度 requestIdleCallback', () => {
const task = new IdleTask<number>();
const handler = vi.fn();
task.enqueueTask(handler, 1);
expect(scheduled).toHaveLength(1);
});
test('继续入队同优先级任务不会重复调度', () => {
const task = new IdleTask<number>();
task.enqueueTask(() => undefined, 1);
task.enqueueTask(() => undefined, 2);
expect(scheduled).toHaveLength(1);
});
test('runTaskQueue - 高优先级任务优先执行', () => {
const task = new IdleTask<string>();
const order: string[] = [];
task.enqueueTask(() => order.push('low'), 'low');
task.enqueueTask(() => order.push('high'), 'high', true);
scheduled[0].cb(fakeIdleDeadline(20));
expect(order[0]).toBe('high');
expect(order[1]).toBe('low');
});
test('剩余空闲时间 <=5 时单批最多 10 个任务', () => {
const task = new IdleTask<number>();
const handler = vi.fn();
for (let i = 0; i < 1000; i++) task.enqueueTask(handler, i);
scheduled[0].cb(fakeIdleDeadline(3, 1));
expect(handler.mock.calls.length).toBeGreaterThan(0);
expect(handler.mock.calls.length).toBeLessThanOrEqual(10);
});
test('剩余时间 85-10 范围)单批最多 100', () => {
const task = new IdleTask<number>();
const handler = vi.fn();
for (let i = 0; i < 1000; i++) task.enqueueTask(handler, i);
scheduled[0].cb(fakeIdleDeadline(8, 1));
expect(handler.mock.calls.length).toBeLessThanOrEqual(100);
});
test('剩余时间 12 单批最多 300', () => {
const task = new IdleTask<number>();
const handler = vi.fn();
for (let i = 0; i < 1000; i++) task.enqueueTask(handler, i);
scheduled[0].cb(fakeIdleDeadline(12, 1));
expect(handler.mock.calls.length).toBeLessThanOrEqual(300);
});
test('剩余时间 50 单批最多 600', () => {
const task = new IdleTask<number>();
const handler = vi.fn();
for (let i = 0; i < 1000; i++) task.enqueueTask(handler, i);
scheduled[0].cb(fakeIdleDeadline(50, 1));
expect(handler.mock.calls.length).toBeLessThanOrEqual(600);
});
test('完成所有任务后触发 finish 与 hight-level-finish 事件', () => {
const task = new IdleTask<number>();
const finishHandler = vi.fn();
const hlFinishHandler = vi.fn();
const updateHandler = vi.fn();
task.on('finish', finishHandler);
task.on('hight-level-finish', hlFinishHandler);
task.on('update-task-length', updateHandler);
task.enqueueTask(() => undefined, 1);
scheduled[0].cb(fakeIdleDeadline(50));
expect(finishHandler).toHaveBeenCalled();
expect(hlFinishHandler).toHaveBeenCalled();
expect(updateHandler).toHaveBeenCalled();
});
test('剩余任务时再次调度 requestIdleCallback', () => {
const task = new IdleTask<number>();
const handler = vi.fn();
for (let i = 0; i < 200; i++) task.enqueueTask(handler, i);
scheduled[0].cb(fakeIdleDeadline(3, 1));
expect(scheduled.length).toBeGreaterThan(1);
});
test('单个任务抛错不会中断整个队列', () => {
const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => undefined);
const task = new IdleTask<number>();
const done: number[] = [];
const finishHandler = vi.fn();
task.on('finish', finishHandler);
for (let i = 0; i < 10; i++) {
task.enqueueTask((n) => {
if (n === 3) throw new Error('boom');
done.push(n);
}, i);
}
expect(() => scheduled[0].cb(fakeIdleDeadline(50))).not.toThrow();
expect(done).toEqual([0, 1, 2, 4, 5, 6, 7, 8, 9]);
expect(finishHandler).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalled();
errorSpy.mockRestore();
});
test('任务抛错且仍有剩余任务时继续调度下一轮并同步任务数', () => {
const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => undefined);
const task = new IdleTask<number>();
const updateHandler = vi.fn();
task.on('update-task-length', updateHandler);
for (let i = 0; i < 200; i++) {
task.enqueueTask((n) => {
if (n === 0) throw new Error('boom');
}, i);
}
// 单批最多执行 10 个,执行完仍有剩余任务
scheduled[0].cb(fakeIdleDeadline(3, 1));
expect(scheduled.length).toBeGreaterThan(1);
expect(updateHandler).toHaveBeenCalledWith({ length: 190, hightLevelLength: 0 });
errorSpy.mockRestore();
});
test('任务执行过程中 clearTasks 会立即停止消费已清空的队列', () => {
const task = new IdleTask<number>();
const handler = vi.fn((n: number) => {
if (n === 0) {
task.clearTasks();
}
});
for (let i = 0; i < 200; i++) task.enqueueTask(handler, i);
scheduled[0].cb(fakeIdleDeadline(50));
expect(handler).toHaveBeenCalledTimes(1);
});
test('任务执行过程中清空并重新入队,新任务仍会被执行', () => {
const task = new IdleTask<number>();
const afterHandler = vi.fn();
task.enqueueTask((n) => {
if (n === 0) {
task.clearTasks();
task.enqueueTask(afterHandler, 999);
}
}, 0);
for (let i = 1; i < 200; i++) task.enqueueTask(() => undefined, i);
scheduled[0].cb(fakeIdleDeadline(50));
expect(afterHandler).toHaveBeenCalled();
});
test('clearTasks - 取消挂起任务并重置队列', () => {
const task = new IdleTask<number>();
task.enqueueTask(() => undefined, 1);
task.clearTasks();
expect(globalThis.cancelIdleCallback).toHaveBeenCalled();
});
test('clearTasks 在没有挂起任务时也安全', () => {
const task = new IdleTask<number>();
expect(() => task.clearTasks()).not.toThrow();
});
test('once 监听器执行一次后失效', () => {
const task = new IdleTask<number>();
const fn = vi.fn();
task.once('finish', fn);
task.emit('finish');
task.emit('finish');
expect(fn).toHaveBeenCalledTimes(1);
});
test('全局 requestIdleCallback polyfill 在浏览器无原生时降级到 setTimeout', () => {
vi.useFakeTimers();
const original = globalThis.requestIdleCallback;
delete (globalThis as any).requestIdleCallback;
// 重新加载模块以触发 polyfill 注册
vi.resetModules();
return import('@editor/utils/dep/idle-task').then(() => {
expect(typeof globalThis.requestIdleCallback).toBe('function');
const cb = vi.fn();
const id = globalThis.requestIdleCallback(cb);
expect(id).toBeDefined();
vi.runAllTimers();
expect(cb).toHaveBeenCalled();
vi.useRealTimers();
globalThis.requestIdleCallback = original;
});
});
});