mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
feat(extension): compact recording overlay with live timer
Show the recording message for 10s, then switch to a fixed-width MM:SS timer with the icon-only finish button.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1,6 +1,12 @@
|
|||||||
import { useTranslation } from "@browser-skill/i18n/react";
|
import { useTranslation } from "@browser-skill/i18n/react";
|
||||||
import { RiStopCircleLine } from "@remixicon/react";
|
import {
|
||||||
import { useEffect, useState } from "react";
|
type PointerEvent as ReactPointerEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import recordIconUrl from "../../assets/record-icon.png";
|
||||||
|
|
||||||
export interface RecordRequestData {
|
export interface RecordRequestData {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -11,9 +17,51 @@ type Props = {
|
|||||||
request: RecordRequestData | null;
|
request: RecordRequestData | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const VIEWPORT_MARGIN = 16;
|
||||||
|
const DRAG_THRESHOLD_PX = 8;
|
||||||
|
const INTRO_DURATION_MS = 10_000;
|
||||||
|
const TIMER_REFRESH_MS = 250;
|
||||||
|
|
||||||
|
function formatElapsed(seconds: number): string {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
return `${String(minutes).padStart(2, "0")}:${String(remainingSeconds).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampDragPos(
|
||||||
|
top: number,
|
||||||
|
left: number,
|
||||||
|
panelW: number,
|
||||||
|
panelH: number,
|
||||||
|
): { top: number; left: number } {
|
||||||
|
const maxLeft = window.innerWidth - panelW - VIEWPORT_MARGIN;
|
||||||
|
const maxTop = window.innerHeight - panelH - VIEWPORT_MARGIN;
|
||||||
|
return {
|
||||||
|
top: clamp(top, VIEWPORT_MARGIN, Math.max(VIEWPORT_MARGIN, maxTop)),
|
||||||
|
left: clamp(left, VIEWPORT_MARGIN, Math.max(VIEWPORT_MARGIN, maxLeft)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function RecordOverlay({ request }: Props) {
|
export function RecordOverlay({ request }: Props) {
|
||||||
const { t } = useTranslation("extension");
|
const { t } = useTranslation("extension");
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
|
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
||||||
|
const [dragPos, setDragPos] = useState<{ top: number; left: number } | null>(null);
|
||||||
|
const [dragging, setDragging] = useState(false);
|
||||||
|
const pillRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dragStartRef = useRef<{
|
||||||
|
pointerX: number;
|
||||||
|
pointerY: number;
|
||||||
|
originLeft: number;
|
||||||
|
originTop: number;
|
||||||
|
panelW: number;
|
||||||
|
panelH: number;
|
||||||
|
armed: boolean;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (request) {
|
if (request) {
|
||||||
@@ -23,8 +71,82 @@ export function RecordOverlay({ request }: Props) {
|
|||||||
setShow(false);
|
setShow(false);
|
||||||
}, [request]);
|
}, [request]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setElapsedSeconds(0);
|
||||||
|
setDragPos(null);
|
||||||
|
setDragging(false);
|
||||||
|
dragStartRef.current = null;
|
||||||
|
if (!request) return;
|
||||||
|
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const updateElapsed = () => {
|
||||||
|
setElapsedSeconds(Math.floor((Date.now() - startedAt) / 1_000));
|
||||||
|
};
|
||||||
|
const timer = window.setInterval(updateElapsed, TIMER_REFRESH_MS);
|
||||||
|
return () => window.clearInterval(timer);
|
||||||
|
}, [request?.id]);
|
||||||
|
|
||||||
|
const onPillPointerDown = useCallback((e: ReactPointerEvent<HTMLDivElement>) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
if (e.target instanceof Element && e.target.closest("button, a, input, textarea, select")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pill = pillRef.current;
|
||||||
|
if (!pill) return;
|
||||||
|
const rect = pill.getBoundingClientRect();
|
||||||
|
dragStartRef.current = {
|
||||||
|
pointerX: e.clientX,
|
||||||
|
pointerY: e.clientY,
|
||||||
|
originLeft: rect.left,
|
||||||
|
originTop: rect.top,
|
||||||
|
panelW: pill.offsetWidth || rect.width,
|
||||||
|
panelH: pill.offsetHeight || rect.height,
|
||||||
|
armed: false,
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onMove = (e: PointerEvent) => {
|
||||||
|
const start = dragStartRef.current;
|
||||||
|
if (!start) return;
|
||||||
|
const dx = e.clientX - start.pointerX;
|
||||||
|
const dy = e.clientY - start.pointerY;
|
||||||
|
if (!start.armed) {
|
||||||
|
if (Math.hypot(dx, dy) < DRAG_THRESHOLD_PX) return;
|
||||||
|
start.armed = true;
|
||||||
|
setDragging(true);
|
||||||
|
setDragPos({ top: start.originTop, left: start.originLeft });
|
||||||
|
}
|
||||||
|
setDragPos(
|
||||||
|
clampDragPos(start.originTop + dy, start.originLeft + dx, start.panelW, start.panelH),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const onUp = () => {
|
||||||
|
const start = dragStartRef.current;
|
||||||
|
if (!start) return;
|
||||||
|
if (start.armed) {
|
||||||
|
setDragging(false);
|
||||||
|
}
|
||||||
|
dragStartRef.current = null;
|
||||||
|
};
|
||||||
|
window.addEventListener("pointermove", onMove);
|
||||||
|
window.addEventListener("pointerup", onUp);
|
||||||
|
window.addEventListener("pointercancel", onUp);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pointermove", onMove);
|
||||||
|
window.removeEventListener("pointerup", onUp);
|
||||||
|
window.removeEventListener("pointercancel", onUp);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!request) return null;
|
if (!request) return null;
|
||||||
|
|
||||||
|
const introVisible = elapsedSeconds * 1_000 < INTRO_DURATION_MS;
|
||||||
|
const positioned = dragPos !== null;
|
||||||
|
const defaultTransform = show
|
||||||
|
? "translateX(-50%) translateY(0)"
|
||||||
|
: "translateX(-50%) translateY(8px)";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style>{`
|
<style>{`
|
||||||
@@ -35,78 +157,118 @@ export function RecordOverlay({ request }: Props) {
|
|||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
ref={pillRef}
|
||||||
data-slot="record-overlay-pill"
|
data-slot="record-overlay-pill"
|
||||||
|
data-phase={introVisible ? "intro" : "timer"}
|
||||||
|
data-dragging={dragging ? "true" : "false"}
|
||||||
|
onPointerDown={onPillPointerDown}
|
||||||
style={{
|
style={{
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
bottom: 32,
|
...(positioned
|
||||||
left: "50%",
|
? { top: dragPos.top, left: dragPos.left, bottom: "auto" }
|
||||||
|
: { bottom: 32, left: "50%" }),
|
||||||
zIndex: 2147483647,
|
zIndex: 2147483647,
|
||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 10,
|
gap: 5,
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
borderRadius: 9999,
|
borderRadius: 9999,
|
||||||
padding: "10px 10px 10px 20px",
|
height: 50,
|
||||||
boxShadow: "0 8px 32px rgba(15,23,42,0.16), 0 2px 8px rgba(0,0,0,0.1)",
|
boxSizing: "border-box",
|
||||||
|
padding: "6px 8px 6px 16px",
|
||||||
|
boxShadow: dragging
|
||||||
|
? "0 12px 40px rgba(15,23,42,0.22), 0 2px 8px rgba(0,0,0,0.12)"
|
||||||
|
: "0 8px 32px rgba(15,23,42,0.16), 0 2px 8px rgba(0,0,0,0.1)",
|
||||||
opacity: show ? 1 : 0,
|
opacity: show ? 1 : 0,
|
||||||
transform: show ? "translateX(-50%) translateY(0)" : "translateX(-50%) translateY(8px)",
|
transform: positioned ? "none" : defaultTransform,
|
||||||
transition: "opacity 300ms ease-out, transform 300ms ease-out",
|
transition: dragging
|
||||||
|
? "box-shadow 150ms ease-out"
|
||||||
|
: "opacity 300ms ease-out, transform 300ms ease-out, box-shadow 150ms ease-out",
|
||||||
fontFamily:
|
fontFamily:
|
||||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||||
maxWidth: "min(420px, calc(100vw - 32px))",
|
maxWidth: "min(420px, calc(100vw - 32px))",
|
||||||
|
cursor: dragging ? "grabbing" : "grab",
|
||||||
|
userSelect: "none",
|
||||||
|
touchAction: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
data-slot="record-overlay-indicator"
|
data-slot="record-overlay-status"
|
||||||
style={{
|
|
||||||
width: 9,
|
|
||||||
height: 9,
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: "#ef4444",
|
|
||||||
flexShrink: 0,
|
|
||||||
animation: "bsk-rec-pulse 1.4s ease-in-out infinite",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
style={{
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
minWidth: 0,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 16,
|
borderRadius: 9999,
|
||||||
fontWeight: 500,
|
outline: "none",
|
||||||
color: "#333",
|
minHeight: 32,
|
||||||
whiteSpace: "nowrap",
|
paddingRight: 4,
|
||||||
userSelect: "none",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("recordOverlay.recording")}
|
<span
|
||||||
</span>
|
data-slot="record-overlay-indicator"
|
||||||
|
style={{
|
||||||
|
width: 9,
|
||||||
|
height: 9,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "#ef4444",
|
||||||
|
flexShrink: 0,
|
||||||
|
animation: "bsk-rec-pulse 1.4s ease-in-out infinite",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
data-slot="record-overlay-label"
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
flex: "0 1 auto",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: "#333",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
userSelect: "none",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
width: introVisible ? "auto" : 48,
|
||||||
|
maxWidth: introVisible ? 280 : 48,
|
||||||
|
fontVariantNumeric: introVisible ? undefined : "tabular-nums",
|
||||||
|
textAlign: introVisible ? "left" : "center",
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
minWidth: 0,
|
||||||
|
transition: "max-width 180ms ease-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{introVisible ? t("recordOverlay.recording") : formatElapsed(elapsedSeconds)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-slot="record-overlay-finish"
|
data-slot="record-overlay-finish"
|
||||||
|
aria-label={t("recordOverlay.finish")}
|
||||||
onClick={request.onFinish}
|
onClick={request.onFinish}
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
display: "flex",
|
display: "block",
|
||||||
alignItems: "center",
|
width: 32,
|
||||||
gap: 6,
|
height: 32,
|
||||||
border: "none",
|
border: "none",
|
||||||
borderRadius: 9999,
|
borderRadius: 9999,
|
||||||
padding: "8px 20px 8px 16px",
|
padding: 0,
|
||||||
fontSize: 15,
|
backgroundColor: "transparent",
|
||||||
fontWeight: 600,
|
|
||||||
color: "#fff",
|
|
||||||
backgroundColor: "#f97316",
|
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
transition: "background-color 150ms ease-out, opacity 150ms ease-out",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
lineHeight: 1,
|
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RiStopCircleLine size={18} color="#fff" />
|
<img
|
||||||
{t("recordOverlay.finish")}
|
src={recordIconUrl}
|
||||||
|
alt=""
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
draggable={false}
|
||||||
|
style={{ display: "block", margin: "auto" }}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,37 +1,167 @@
|
|||||||
import { cleanup, fireEvent, render } from "@testing-library/react";
|
import { i18n } from "@browser-skill/i18n";
|
||||||
|
import { I18nextProvider } from "@browser-skill/i18n/react";
|
||||||
|
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import { createElement } from "react";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { RecordOverlay } from "../RecordOverlay";
|
import { RecordOverlay } from "../RecordOverlay";
|
||||||
|
|
||||||
|
function renderOverlay(request: { id: string; onFinish: () => void } | null) {
|
||||||
|
return render(
|
||||||
|
createElement(I18nextProvider, { i18n }, createElement(RecordOverlay, { request })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function advanceRecording(ms: number) {
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("RecordOverlay", () => {
|
describe("RecordOverlay", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the bottom pill with orange finish CTA and no logo", () => {
|
it("renders the bottom pill with the icon-only finish button", () => {
|
||||||
const onFinish = vi.fn();
|
const onFinish = vi.fn();
|
||||||
const { container } = render(<RecordOverlay request={{ id: "rec-1", onFinish }} />);
|
const { container } = renderOverlay({ id: "rec-1", onFinish });
|
||||||
|
|
||||||
const pill = container.querySelector("[data-slot='record-overlay-pill']");
|
const pill = container.querySelector("[data-slot='record-overlay-pill']");
|
||||||
expect(pill).toBeTruthy();
|
expect(pill).toBeTruthy();
|
||||||
expect(pill?.querySelector("img")).toBeNull();
|
|
||||||
expect((pill as HTMLElement).style.borderRadius).toBe("9999px");
|
expect((pill as HTMLElement).style.borderRadius).toBe("9999px");
|
||||||
expect((pill as HTMLElement).style.backgroundColor).toBe("#fff");
|
expect((pill as HTMLElement).style.backgroundColor).toBe("#fff");
|
||||||
|
expect((pill as HTMLElement).style.gap).toBe("5px");
|
||||||
|
expect((pill as HTMLElement).style.padding).toBe("6px 8px 6px 16px");
|
||||||
|
expect((pill as HTMLElement).style.height).toBe("50px");
|
||||||
|
expect((pill as HTMLElement).style.boxSizing).toBe("border-box");
|
||||||
|
expect(container.querySelector("[data-slot='record-overlay-collapse']")).toBeNull();
|
||||||
|
|
||||||
const finish = container.querySelector("[data-slot='record-overlay-finish']");
|
const status = container.querySelector("[data-slot='record-overlay-status']") as HTMLElement;
|
||||||
|
expect(status.style.minHeight).toBe("32px");
|
||||||
|
const finish = screen.getByRole("button", {
|
||||||
|
name: i18n.t("recordOverlay.finish", { ns: "extension" }),
|
||||||
|
});
|
||||||
expect(finish).toBeTruthy();
|
expect(finish).toBeTruthy();
|
||||||
expect((finish as HTMLElement).style.backgroundColor).toBe("#f97316");
|
expect(finish.textContent).toBe("");
|
||||||
|
const icon = finish.querySelector("img");
|
||||||
|
expect(icon).toBeTruthy();
|
||||||
|
expect(icon?.getAttribute("src")).toContain("record-icon.png");
|
||||||
|
expect((finish as HTMLElement).style.width).toBe("32px");
|
||||||
|
expect((finish as HTMLElement).style.height).toBe("32px");
|
||||||
|
expect(icon?.getAttribute("width")).toBe("24");
|
||||||
|
expect(icon?.getAttribute("height")).toBe("24");
|
||||||
expect((finish as HTMLElement).style.borderRadius).toBe("9999px");
|
expect((finish as HTMLElement).style.borderRadius).toBe("9999px");
|
||||||
|
|
||||||
fireEvent.click(finish!);
|
fireEvent.click(finish);
|
||||||
expect(onFinish).toHaveBeenCalledTimes(1);
|
expect(onFinish).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows a pulsing recording indicator and no full-screen glow layer", () => {
|
it("shows a pulsing recording indicator and no full-screen glow layer", () => {
|
||||||
const { container } = render(<RecordOverlay request={{ id: "rec-1", onFinish: vi.fn() }} />);
|
const { container } = renderOverlay({ id: "rec-1", onFinish: vi.fn() });
|
||||||
expect(container.querySelector("[data-slot='record-overlay']")).toBeNull();
|
expect(container.querySelector("[data-slot='record-overlay']")).toBeNull();
|
||||||
|
|
||||||
const indicator = container.querySelector("[data-slot='record-overlay-indicator']");
|
const indicator = container.querySelector("[data-slot='record-overlay-indicator']");
|
||||||
expect(indicator).toBeTruthy();
|
expect(indicator).toBeTruthy();
|
||||||
expect((indicator as HTMLElement).style.animation).toContain("bsk-rec-pulse");
|
expect((indicator as HTMLElement).style.animation).toContain("bsk-rec-pulse");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows the recording message for ten seconds, then displays elapsed time", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const { container } = renderOverlay({ id: "rec-1", onFinish: vi.fn() });
|
||||||
|
const pill = container.querySelector("[data-slot='record-overlay-pill']") as HTMLElement;
|
||||||
|
const label = container.querySelector("[data-slot='record-overlay-label']") as HTMLElement;
|
||||||
|
|
||||||
|
expect(pill.dataset.phase).toBe("intro");
|
||||||
|
expect(label.textContent).toBe(i18n.t("recordOverlay.recording", { ns: "extension" }));
|
||||||
|
|
||||||
|
advanceRecording(9999);
|
||||||
|
expect(pill.dataset.phase).toBe("intro");
|
||||||
|
|
||||||
|
advanceRecording(1);
|
||||||
|
expect(pill.dataset.phase).toBe("timer");
|
||||||
|
expect(label.textContent).toBe("00:10");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats elapsed time as zero-padded minutes and seconds", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const { container } = renderOverlay({ id: "rec-1", onFinish: vi.fn() });
|
||||||
|
const label = container.querySelector("[data-slot='record-overlay-label']") as HTMLElement;
|
||||||
|
|
||||||
|
advanceRecording(125_000);
|
||||||
|
expect(label.textContent).toBe("02:05");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a fixed timer label width with tabular numerals", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const { container } = renderOverlay({ id: "rec-1", onFinish: vi.fn() });
|
||||||
|
const label = container.querySelector("[data-slot='record-overlay-label']") as HTMLElement;
|
||||||
|
|
||||||
|
advanceRecording(10_000);
|
||||||
|
expect(label.style.width).toBe("48px");
|
||||||
|
expect(label.style.maxWidth).toBe("48px");
|
||||||
|
expect(label.style.fontVariantNumeric).toBe("tabular-nums");
|
||||||
|
expect(label.style.textAlign).toBe("center");
|
||||||
|
|
||||||
|
advanceRecording(1_000);
|
||||||
|
expect(label.textContent).toBe("00:11");
|
||||||
|
expect(label.style.width).toBe("48px");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets the intro and elapsed time for a new request", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const first = { id: "rec-1", onFinish: vi.fn() };
|
||||||
|
const second = { id: "rec-2", onFinish: vi.fn() };
|
||||||
|
const { container, rerender } = renderOverlay(first);
|
||||||
|
|
||||||
|
advanceRecording(65_000);
|
||||||
|
rerender(
|
||||||
|
createElement(I18nextProvider, { i18n }, createElement(RecordOverlay, { request: second })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pill = container.querySelector("[data-slot='record-overlay-pill']") as HTMLElement;
|
||||||
|
const label = container.querySelector("[data-slot='record-overlay-label']") as HTMLElement;
|
||||||
|
expect(pill.dataset.phase).toBe("intro");
|
||||||
|
expect(label.textContent).toBe(i18n.t("recordOverlay.recording", { ns: "extension" }));
|
||||||
|
|
||||||
|
advanceRecording(10_000);
|
||||||
|
expect(pill.dataset.phase).toBe("timer");
|
||||||
|
expect(label.textContent).toBe("00:10");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not start drag when pointer down on finish", () => {
|
||||||
|
const { container } = renderOverlay({ id: "rec-1", onFinish: vi.fn() });
|
||||||
|
const pill = container.querySelector("[data-slot='record-overlay-pill']") as HTMLElement;
|
||||||
|
const finish = container.querySelector("[data-slot='record-overlay-finish']") as HTMLElement;
|
||||||
|
|
||||||
|
fireEvent.pointerDown(finish, { button: 0, clientX: 10, clientY: 10, pointerId: 1 });
|
||||||
|
expect(pill.getAttribute("data-dragging")).toBe("false");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves the pill when dragging the non-button area without toggling collapse", () => {
|
||||||
|
const { container } = renderOverlay({ id: "rec-1", onFinish: vi.fn() });
|
||||||
|
const pill = container.querySelector("[data-slot='record-overlay-pill']") as HTMLElement;
|
||||||
|
|
||||||
|
pill.getBoundingClientRect = () =>
|
||||||
|
({
|
||||||
|
top: 600,
|
||||||
|
left: 200,
|
||||||
|
width: 320,
|
||||||
|
height: 48,
|
||||||
|
right: 520,
|
||||||
|
bottom: 648,
|
||||||
|
}) as DOMRect;
|
||||||
|
Object.defineProperty(pill, "offsetWidth", { value: 320, configurable: true });
|
||||||
|
Object.defineProperty(pill, "offsetHeight", { value: 48, configurable: true });
|
||||||
|
|
||||||
|
fireEvent.pointerDown(pill, { button: 0, clientX: 300, clientY: 620, pointerId: 1 });
|
||||||
|
fireEvent(window, new PointerEvent("pointermove", { clientX: 360, clientY: 580 }));
|
||||||
|
fireEvent(window, new PointerEvent("pointerup", { pointerId: 1 }));
|
||||||
|
|
||||||
|
expect(pill.getAttribute("data-phase")).toBe("intro");
|
||||||
|
expect(pill.style.left).toBe("260px");
|
||||||
|
expect(pill.style.top).toBe("560px");
|
||||||
|
expect(pill.style.bottom).toBe("auto");
|
||||||
|
expect(pill.style.transform).toBe("none");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
"notificationTitle": "BrowserSkill:Agent 需要你的帮助"
|
"notificationTitle": "BrowserSkill:Agent 需要你的帮助"
|
||||||
},
|
},
|
||||||
"recordOverlay": {
|
"recordOverlay": {
|
||||||
"recording": "正在录制用户操作",
|
"recording": "正在录制你的操作",
|
||||||
"finish": "结束"
|
"finish": "结束"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user