fix: 修复桌面 DOCX 媒体捕获超时

This commit is contained in:
SkyJourney
2026-08-02 13:50:25 +08:00
parent 14f470454c
commit 5c82b3df7f
12 changed files with 417 additions and 47 deletions
@@ -74,24 +74,55 @@ export interface PreviewEngineStyles {
echartsCss: string;
}
function waitForImages(container: ParentNode) {
const images = Array.from(container.querySelectorAll("img"));
return Promise.all(
images.map(async (image) => {
if (image.complete) {
export const DEFAULT_IMAGE_READY_TIMEOUT_MS = 10_000;
function waitForImage(
image: HTMLImageElement,
timeoutMs: number
) {
if (image.complete) {
return Promise.resolve();
}
return new Promise<void>((resolve) => {
let settled = false;
const finish = () => {
if (settled) {
return;
}
settled = true;
clearTimeout(timeout);
image.removeEventListener("load", finish);
image.removeEventListener("error", finish);
resolve();
};
const timeout = window.setTimeout(finish, timeoutMs);
try {
await image.decode();
} catch {
await new Promise<void>((resolve) => {
image.addEventListener("load", () => resolve(), { once: true });
image.addEventListener("error", () => resolve(), { once: true });
});
// 先监听事件,再检查 complete 和调用 decode,避免 Electron 中图片
// 已经失败后才注册 error 监听器而永久等待。
image.addEventListener("load", finish, { once: true });
image.addEventListener("error", finish, { once: true });
if (image.complete) {
finish();
return;
}
void image.decode().then(finish, () => {
// decode 失败时仍允许尚未到达的 load/error 事件完成等待;若事件
// 已经发生,complete 会立即结束。最终由有界超时兜底。
if (image.complete) {
finish();
}
})
);
});
});
}
export function waitForImages(
container: ParentNode,
timeoutMs = DEFAULT_IMAGE_READY_TIMEOUT_MS
) {
const images = Array.from(container.querySelectorAll("img"));
return Promise.all(images.map((image) => waitForImage(image, timeoutMs)));
}
function stylesheet(
@@ -0,0 +1,60 @@
// @vitest-environment happy-dom
import { describe, expect, it, vi } from "vitest";
import { waitForImages } from "../src/index.js";
function createPendingImage() {
const image = document.createElement("img");
Object.defineProperty(image, "complete", {
configurable: true,
get: () => false
});
return image;
}
describe("文档图片就绪等待", () => {
it("图片正常加载后立即完成", async () => {
const image = createPendingImage();
image.decode = vi.fn(() => new Promise(() => undefined));
document.body.replaceChildren(image);
const waiting = waitForImages(document, 1_000);
image.dispatchEvent(new Event("load"));
await expect(waiting).resolves.toEqual([undefined]);
});
it("decode 拒绝且图片已经失败时不会错过 error 状态", async () => {
let complete = false;
const image = document.createElement("img");
Object.defineProperty(image, "complete", {
configurable: true,
get: () => complete
});
image.decode = vi.fn(async () => {
complete = true;
throw new Error("decode failed");
});
document.body.replaceChildren(image);
await expect(waitForImages(document, 1_000)).resolves.toEqual([
undefined
]);
});
it("图片永不响应时由局部超时结束等待", async () => {
vi.useFakeTimers();
try {
const image = createPendingImage();
image.decode = vi.fn(() => new Promise(() => undefined));
document.body.replaceChildren(image);
const waiting = waitForImages(document, 25);
await vi.advanceTimersByTimeAsync(25);
await expect(waiting).resolves.toEqual([undefined]);
} finally {
vi.useRealTimers();
}
});
});