diff --git a/apps/extension/src/long-screenshot/page.test.ts b/apps/extension/src/long-screenshot/page.test.ts index 75fb33d..4f7fa7c 100644 --- a/apps/extension/src/long-screenshot/page.test.ts +++ b/apps/extension/src/long-screenshot/page.test.ts @@ -313,6 +313,24 @@ describe("page capture cleanup", () => { } expect(await send({ action: "inspect" })).toMatchObject({ bottomReady: true }); }); + it("waits for ARIA loading markers regardless of attribute case", async () => { + const loader = document.createElement("div"); + loader.setAttribute("role", "PROGRESSBAR"); + loader.setAttribute("aria-busy", "TRUE"); + document.body.append(loader); + vi.spyOn(loader, "getBoundingClientRect").mockReturnValue({ + top: 500, + bottom: 530, + width: 100, + height: 30, + } as DOMRect); + await send({ action: "begin", label: "Capture", cancelLabel: "Cancel" }); + const moving = send({ action: "move", y: 1800, capture: true, final: true }); + await vi.advanceTimersByTimeAsync(2500); + expect(await moving).toMatchObject({ bottomReady: false, loading: true }); + expect(await send({ action: "inspect" })).toMatchObject({ bottomReady: false, loading: true }); + }); + it("does not treat article text, code examples or numeric progress widgets as loading", async () => { document.body.innerHTML += '

Loading files in JavaScript

Loading...
'; diff --git a/apps/extension/src/long-screenshot/page.ts b/apps/extension/src/long-screenshot/page.ts index 481502c..196bebf 100644 --- a/apps/extension/src/long-screenshot/page.ts +++ b/apps/extension/src/long-screenshot/page.ts @@ -7,6 +7,10 @@ import { ScreenshotError, } from "./types"; +function ariaToken(element: Element, name: string): string { + return (element.getAttribute(name) ?? "").trim().toLowerCase(); +} + export function createPageCapture(onCancel: (id: string, reason: CaptureCancelReason) => void) { let task: ReturnType | undefined; @@ -70,8 +74,11 @@ export function createPageCapture(onCancel: (id: string, reason: CaptureCancelRe const loadingText = /^(?:加载中|正在加载|载入中|loading(?:\s+(?:more|content|items|results))?)[\s.。…!!]*$/i; const trackLoading = (element: Element) => { + const role = ariaToken(element, "role"); if ( - element.matches('[aria-busy="true"], [role="progressbar"], [role="status"]') || + ariaToken(element, "aria-busy") === "true" || + role === "progressbar" || + role === "status" || (element.childElementCount === 0 && !element.closest("pre, code") && loadingText.test(element.textContent?.trim() ?? "")) @@ -144,7 +151,7 @@ export function createPageCapture(onCancel: (id: string, reason: CaptureCancelRe seen.add(element); trackLoading(element); if ( - element.getAttribute("role") === "contentinfo" || + ariaToken(element, "role") === "contentinfo" || (element.tagName === "FOOTER" && !element.closest("article, aside, section")) ) footers.add(element); @@ -280,8 +287,8 @@ export function createPageCapture(onCancel: (id: string, reason: CaptureCancelRe const style = getComputedStyle(element); if (style.visibility === "hidden" || style.display === "none") continue; if ( - element.getAttribute("aria-busy") === "true" || - (element.getAttribute("role") === "progressbar" && + ariaToken(element, "aria-busy") === "true" || + (ariaToken(element, "role") === "progressbar" && !element.hasAttribute("aria-valuenow")) || (!element.closest("pre, code") && loadingText.test(element.textContent?.trim() ?? "")) ) {