mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
fix(screenshot): hide overlays during capture so shots only contain page content (#64)
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { CAPTURE_SUPPRESS, type CaptureSuppressAck } from "@/lib/capture-suppress-bridge";
|
||||
import { CAPTURE_HIDDEN_ATTR, createCaptureSuppressController } from "../capture-suppress";
|
||||
|
||||
/**
|
||||
* rAF is stubbed with a manual queue so tests can drive the compositor
|
||||
* frame-by-frame and assert the ack only lands after the second frame.
|
||||
*/
|
||||
describe("capture-suppress controller", () => {
|
||||
let rafQueue: FrameRequestCallback[];
|
||||
let host: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
rafQueue = [];
|
||||
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
|
||||
rafQueue.push(cb);
|
||||
return rafQueue.length;
|
||||
});
|
||||
vi.stubGlobal("cancelAnimationFrame", vi.fn());
|
||||
document.body.innerHTML = "";
|
||||
host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
});
|
||||
|
||||
/** Run every queued rAF callback, draining microtasks between rounds. */
|
||||
async function flushFrames(rounds = 4): Promise<void> {
|
||||
for (let round = 0; round < rounds && rafQueue.length > 0; round += 1) {
|
||||
const callbacks = rafQueue.splice(0);
|
||||
for (const cb of callbacks) cb(0);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
it("begin hides the host immediately but only acks after two frames", async () => {
|
||||
const controller = createCaptureSuppressController(() => host);
|
||||
const sendResponse = vi.fn();
|
||||
|
||||
const needsAsync = controller.handleMessage(
|
||||
{ type: CAPTURE_SUPPRESS, phase: "begin" },
|
||||
sendResponse,
|
||||
);
|
||||
|
||||
expect(needsAsync).toBe(true);
|
||||
expect(controller.pendingCount).toBe(1);
|
||||
expect(host.hasAttribute(CAPTURE_HIDDEN_ATTR)).toBe(true);
|
||||
expect(sendResponse).not.toHaveBeenCalled();
|
||||
|
||||
// First frame commits the style change; the ack must still be pending.
|
||||
await flushFrames(1);
|
||||
expect(sendResponse).not.toHaveBeenCalled();
|
||||
|
||||
// Second frame: the compositor has produced an overlay-free frame.
|
||||
await flushFrames();
|
||||
const expected: CaptureSuppressAck = { type: CAPTURE_SUPPRESS, ok: true };
|
||||
expect(sendResponse).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
it("end removes the attribute once the count returns to zero", async () => {
|
||||
const controller = createCaptureSuppressController(() => host);
|
||||
controller.handleMessage({ type: CAPTURE_SUPPRESS, phase: "begin" }, vi.fn());
|
||||
await flushFrames();
|
||||
expect(host.hasAttribute(CAPTURE_HIDDEN_ATTR)).toBe(true);
|
||||
|
||||
const sendResponse = vi.fn();
|
||||
const needsAsync = controller.handleMessage(
|
||||
{ type: CAPTURE_SUPPRESS, phase: "end" },
|
||||
sendResponse,
|
||||
);
|
||||
|
||||
expect(needsAsync).toBe(false);
|
||||
expect(controller.pendingCount).toBe(0);
|
||||
expect(host.hasAttribute(CAPTURE_HIDDEN_ATTR)).toBe(false);
|
||||
expect(sendResponse).toHaveBeenCalledWith({ type: CAPTURE_SUPPRESS, ok: true });
|
||||
});
|
||||
|
||||
it("reference-counts concurrent begins so one end does not unhide early", async () => {
|
||||
const controller = createCaptureSuppressController(() => host);
|
||||
controller.handleMessage({ type: CAPTURE_SUPPRESS, phase: "begin" }, vi.fn());
|
||||
controller.handleMessage({ type: CAPTURE_SUPPRESS, phase: "begin" }, vi.fn());
|
||||
await flushFrames();
|
||||
expect(controller.pendingCount).toBe(2);
|
||||
|
||||
controller.handleMessage({ type: CAPTURE_SUPPRESS, phase: "end" }, vi.fn());
|
||||
expect(controller.pendingCount).toBe(1);
|
||||
expect(host.hasAttribute(CAPTURE_HIDDEN_ATTR)).toBe(true);
|
||||
|
||||
controller.handleMessage({ type: CAPTURE_SUPPRESS, phase: "end" }, vi.fn());
|
||||
expect(controller.pendingCount).toBe(0);
|
||||
expect(host.hasAttribute(CAPTURE_HIDDEN_ATTR)).toBe(false);
|
||||
});
|
||||
|
||||
it("treats a stray end as a no-op", () => {
|
||||
const controller = createCaptureSuppressController(() => host);
|
||||
const sendResponse = vi.fn();
|
||||
|
||||
controller.handleMessage({ type: CAPTURE_SUPPRESS, phase: "end" }, sendResponse);
|
||||
|
||||
expect(controller.pendingCount).toBe(0);
|
||||
expect(host.hasAttribute(CAPTURE_HIDDEN_ATTR)).toBe(false);
|
||||
expect(sendResponse).toHaveBeenCalledWith({ type: CAPTURE_SUPPRESS, ok: true });
|
||||
});
|
||||
|
||||
it("re-applies the attribute when the host is remounted mid-capture", () => {
|
||||
let currentHost: HTMLElement | null = host;
|
||||
const controller = createCaptureSuppressController(() => currentHost);
|
||||
controller.handleMessage({ type: CAPTURE_SUPPRESS, phase: "begin" }, vi.fn());
|
||||
|
||||
// Host lost and rebuilt before `end` arrives.
|
||||
currentHost = null;
|
||||
const rebuilt = document.createElement("div");
|
||||
controller.onHostMounted(rebuilt);
|
||||
|
||||
expect(rebuilt.hasAttribute(CAPTURE_HIDDEN_ATTR)).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves a remounted host visible once suppression has ended", () => {
|
||||
const controller = createCaptureSuppressController(() => host);
|
||||
const rebuilt = document.createElement("div");
|
||||
|
||||
controller.onHostMounted(rebuilt);
|
||||
|
||||
expect(rebuilt.hasAttribute(CAPTURE_HIDDEN_ATTR)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user