mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 23:43:35 +00:00
Reset capture selection on launch and fix area picker navigation
This commit is contained in:
1 parent
cbefc51d18
commit
8bae983a44
10 files changed
+156
-170
No files matched your search
@@ -22,11 +22,7 @@ vi.mock("./state", () => ({
|
||||
},
|
||||
}));
|
||||
vi.mock("./utils", () => ({ getScreen: () => ({ getAllDisplays: () => mocks.displays }) }));
|
||||
import {
|
||||
rememberCaptureSource,
|
||||
restoreLastCaptureSource,
|
||||
validateCaptureSource,
|
||||
} from "./captureSelection";
|
||||
import { getSessionCaptureSource, validateCaptureSource } from "./captureSelection";
|
||||
const platform = process.platform;
|
||||
const source = { id: "window:101:0", name: "Document", windowTitle: "Document", appName: "Notes" };
|
||||
beforeEach(() => {
|
||||
@@ -42,15 +38,16 @@ afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: platform });
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
describe("remembered capture selection", () => {
|
||||
it("restores only the same available window", async () => {
|
||||
expect(await restoreLastCaptureSource()).toMatchObject(source);
|
||||
expect(mocks.current).toMatchObject(source);
|
||||
describe("session capture selection", () => {
|
||||
it("starts without a capture source even when the previous window is still available", async () => {
|
||||
expect(await getSessionCaptureSource()).toBeNull();
|
||||
expect(mocks.current).toBeNull();
|
||||
});
|
||||
it("rejects closed windows and clears stale saved selection", async () => {
|
||||
mocks.windows.mockResolvedValue([]);
|
||||
expect(await restoreLastCaptureSource()).toBeNull();
|
||||
expect(mocks.write).toHaveBeenCalledWith("recordly.capture.lastSource.v1", null);
|
||||
it("ignores persisted selection without enumerating capture sources on startup", () => {
|
||||
expect(getSessionCaptureSource()).toBeNull();
|
||||
expect(mocks.read).not.toHaveBeenCalled();
|
||||
expect(mocks.windows).not.toHaveBeenCalled();
|
||||
expect(mocks.sources).not.toHaveBeenCalled();
|
||||
});
|
||||
it("rejects a recycled window ID with a different title", async () => {
|
||||
mocks.windows.mockResolvedValue([
|
||||
@@ -76,26 +73,15 @@ describe("remembered capture selection", () => {
|
||||
await expect(validateCaptureSource(source)).rejects.toThrow("permission denied");
|
||||
expect(mocks.sources).not.toHaveBeenCalled();
|
||||
});
|
||||
it("keeps a newer user selection made during restore", async () => {
|
||||
let resolve!: (value: unknown[]) => void;
|
||||
mocks.windows.mockReturnValue(
|
||||
new Promise((done) => {
|
||||
resolve = done;
|
||||
}),
|
||||
);
|
||||
const pending = restoreLastCaptureSource();
|
||||
const newer = { id: "screen:1:0", name: "Newer" };
|
||||
mocks.current = newer;
|
||||
resolve([]);
|
||||
expect(await pending).toEqual(newer);
|
||||
it("keeps a selection only within the current app session", () => {
|
||||
mocks.current = source;
|
||||
expect(getSessionCaptureSource()).toEqual(source);
|
||||
// A fresh main process initializes its in-memory selection to null.
|
||||
mocks.current = null;
|
||||
expect(getSessionCaptureSource()).toBeNull();
|
||||
expect(mocks.read).not.toHaveBeenCalled();
|
||||
expect(mocks.write).not.toHaveBeenCalled();
|
||||
});
|
||||
it("saves only selection metadata, excluding thumbnails", () => {
|
||||
rememberCaptureSource({ ...source, thumbnail: "private pixels" });
|
||||
const stored = mocks.write.mock.calls[0][1];
|
||||
expect(stored.source).not.toHaveProperty("thumbnail");
|
||||
expect(stored.platform).toBe("darwin");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Linux selection", () => {
|
||||
@@ -124,20 +110,13 @@ describe("Linux selection", () => {
|
||||
mocks.sources.mockResolvedValue([{ ...source, id: "window:102:0" }]);
|
||||
expect(await validateCaptureSource(source)).toBeNull();
|
||||
});
|
||||
for (const saved of [null, { platform: "linux", source }]) {
|
||||
it(`prepares Wayland recording without opening a startup portal (${saved ? "saved X11 window" : "fresh session"})`, async () => {
|
||||
vi.stubEnv("XDG_SESSION_TYPE", "wayland");
|
||||
mocks.read.mockReturnValue(saved);
|
||||
expect(await restoreLastCaptureSource()).toEqual({
|
||||
id: "screen:linux-portal",
|
||||
name: "System picker",
|
||||
sourceType: "screen",
|
||||
});
|
||||
expect(mocks.read).not.toHaveBeenCalled();
|
||||
expect(mocks.sources).not.toHaveBeenCalled();
|
||||
expect(mocks.windows).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
it("starts Wayland unselected without opening the portal", () => {
|
||||
vi.stubEnv("XDG_SESSION_TYPE", "wayland");
|
||||
expect(getSessionCaptureSource()).toBeNull();
|
||||
expect(mocks.read).not.toHaveBeenCalled();
|
||||
expect(mocks.sources).not.toHaveBeenCalled();
|
||||
expect(mocks.windows).not.toHaveBeenCalled();
|
||||
});
|
||||
it("rejects old X11 IDs on Wayland without enumerating portal sources", async () => {
|
||||
vi.stubEnv("XDG_SESSION_TYPE", "wayland");
|
||||
expect(await validateCaptureSource(source)).toBeNull();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { readAppSetting, writeAppSetting } from "../appSettingsStore";
|
||||
import { listCapturePickerWindows } from "./capturePickerSources";
|
||||
import {
|
||||
createAreaSource,
|
||||
@@ -6,7 +5,7 @@ import {
|
||||
getSourceArea,
|
||||
normalizeCapturePick,
|
||||
} from "./sourceArea";
|
||||
import { selectedSource, setSelectedSource } from "./state";
|
||||
import { selectedSource } from "./state";
|
||||
import type { SelectedSource } from "./types";
|
||||
import { getScreen } from "./utils";
|
||||
import { getDesktopSources } from "./register/sources";
|
||||
@@ -15,8 +14,6 @@ import {
|
||||
LINUX_PORTAL_SCREEN_SOURCE_ID,
|
||||
} from "./register/sourceMapping";
|
||||
|
||||
const LAST_SOURCE_KEY = "recordly.capture.lastSource.v1";
|
||||
|
||||
/** Revalidate identity and geometry; never substitute a broader capture source. */
|
||||
export async function validateCaptureSource(candidate: unknown): Promise<SelectedSource | null> {
|
||||
if (!candidate || typeof candidate !== "object") return null;
|
||||
@@ -95,56 +92,7 @@ export async function validateCaptureSource(candidate: unknown): Promise<Selecte
|
||||
: null;
|
||||
}
|
||||
|
||||
export function rememberCaptureSource(source: SelectedSource) {
|
||||
try {
|
||||
writeAppSetting(LAST_SOURCE_KEY, {
|
||||
platform: process.platform,
|
||||
source: {
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
display_id: source.display_id,
|
||||
sourceType: source.sourceType,
|
||||
appName: source.appName,
|
||||
windowTitle: source.windowTitle,
|
||||
area: source.area,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Unable to remember capture source:", error);
|
||||
}
|
||||
}
|
||||
|
||||
let restoring: Promise<SelectedSource | null> | null = null;
|
||||
export async function restoreLastCaptureSource(): Promise<SelectedSource | null> {
|
||||
if (process.platform === "linux" && isLikelyLinuxWaylandSession(process.env)) {
|
||||
const source: SelectedSource = {
|
||||
id: LINUX_PORTAL_SCREEN_SOURCE_ID,
|
||||
name: "System picker",
|
||||
sourceType: "screen",
|
||||
};
|
||||
setSelectedSource(source);
|
||||
return source;
|
||||
}
|
||||
if (selectedSource) return selectedSource;
|
||||
if (!restoring)
|
||||
restoring = (async () => {
|
||||
const saved = readAppSetting(LAST_SOURCE_KEY) as {
|
||||
platform?: unknown;
|
||||
source?: unknown;
|
||||
} | null;
|
||||
if (!saved || saved.platform !== process.platform) return null;
|
||||
try {
|
||||
const restored = await validateCaptureSource(saved.source);
|
||||
// A user selection made while validation runs takes precedence.
|
||||
if (selectedSource) return selectedSource;
|
||||
setSelectedSource(restored);
|
||||
if (!restored) writeAppSetting(LAST_SOURCE_KEY, null);
|
||||
return restored;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})().finally(() => {
|
||||
restoring = null;
|
||||
});
|
||||
return restoring;
|
||||
/** A new main process starts unselected; old saved preferences are never restored. */
|
||||
export function getSessionCaptureSource(): SelectedSource | null {
|
||||
return selectedSource;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ const mocks = vi.hoisted(() => ({
|
||||
list: vi.fn(),
|
||||
pickSourceList: vi.fn(),
|
||||
validate: vi.fn(),
|
||||
remember: vi.fn(),
|
||||
|
||||
selected: vi.fn(),
|
||||
recording: false,
|
||||
currentSource: null as Record<string, unknown> | null,
|
||||
@@ -27,7 +27,6 @@ vi.mock("../../windows", () => ({
|
||||
}));
|
||||
vi.mock("../captureSelection", () => ({
|
||||
validateCaptureSource: mocks.validate,
|
||||
rememberCaptureSource: mocks.remember,
|
||||
}));
|
||||
vi.mock("../capturePickerSources", () => ({ listCapturePickerWindows: mocks.list }));
|
||||
vi.mock("../cursor/bounds", () => ({ stopWindowBoundsCapture: vi.fn() }));
|
||||
@@ -134,7 +133,6 @@ it("rejects a window that disappeared while the picker was open", async () => {
|
||||
message: "That window or screen is no longer available.",
|
||||
});
|
||||
expect(mocks.selected).not.toHaveBeenCalled();
|
||||
expect(mocks.remember).not.toHaveBeenCalled();
|
||||
});
|
||||
it("surfaces detection failures instead of opening a screen-only overlay", async () => {
|
||||
mocks.list.mockRejectedValue(new Error("Unable to detect windows"));
|
||||
@@ -146,14 +144,14 @@ it("surfaces detection failures instead of opening a screen-only overlay", async
|
||||
expect(mocks.selected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reopens the currently selected saved area, without reusing an old area for a window", async () => {
|
||||
it("opens a fresh picker even when an area is already selected", async () => {
|
||||
mocks.currentSource = {
|
||||
id: "area:1",
|
||||
display_id: "1",
|
||||
area: { x: 100, y: 200, width: 400, height: 300 },
|
||||
};
|
||||
expect(await call("get-capture-picker-context", mocks.overlay, 1)).toMatchObject({
|
||||
lastArea: mocks.currentSource.area,
|
||||
lastArea: null,
|
||||
});
|
||||
mocks.currentSource = { id: "window:101:0", display_id: "1" };
|
||||
expect(await call("get-capture-picker-context", mocks.overlay, 1)).toMatchObject({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { rememberCaptureSource, validateCaptureSource } from "../captureSelection";
|
||||
import { validateCaptureSource } from "../captureSelection";
|
||||
import { listCapturePickerWindows } from "../capturePickerSources";
|
||||
import { ipcMain } from "electron";
|
||||
import type { SourceListPickResult } from "../../sourceListPicker";
|
||||
@@ -8,13 +8,8 @@ import {
|
||||
isCapturePickerWebContents,
|
||||
} from "../../windows";
|
||||
import { stopWindowBoundsCapture } from "../cursor/bounds";
|
||||
import { isCursorCaptureActive, selectedSource, setSelectedSource } from "../state";
|
||||
import {
|
||||
createAreaSource,
|
||||
createWindowSource,
|
||||
getSourceArea,
|
||||
normalizeCapturePick,
|
||||
} from "../sourceArea";
|
||||
import { isCursorCaptureActive, setSelectedSource } from "../state";
|
||||
import { createAreaSource, createWindowSource, normalizeCapturePick } from "../sourceArea";
|
||||
import type { CapturePick, CapturePickerWindow, SelectedSource } from "../types";
|
||||
import { getScreen } from "../utils";
|
||||
import { getScreenSourceIdForDisplay, isLikelyLinuxWaylandSession } from "./sourceMapping";
|
||||
@@ -79,7 +74,6 @@ export function registerCapturePickerHandlers({
|
||||
sourceType: "screen" as const,
|
||||
};
|
||||
setSelectedSource(source);
|
||||
rememberCaptureSource(source);
|
||||
broadcastSelectedSourceChange();
|
||||
return {
|
||||
success: true,
|
||||
@@ -121,7 +115,6 @@ export function registerCapturePickerHandlers({
|
||||
if (isCursorCaptureActive) return { success: false, canceled: true };
|
||||
// Recording brings the selected window forward; selection need not wait for it.
|
||||
setSelectedSource(source);
|
||||
rememberCaptureSource(source);
|
||||
broadcastSelectedSourceChange();
|
||||
stopWindowBoundsCapture();
|
||||
return { success: true, source };
|
||||
@@ -195,16 +188,10 @@ export function registerCapturePickerHandlers({
|
||||
window.y < y + height &&
|
||||
window.y + window.height > y,
|
||||
);
|
||||
const previous =
|
||||
selectedSource?.display_id === String(display.id)
|
||||
? getSourceArea(selectedSource)
|
||||
: null;
|
||||
return {
|
||||
displayBounds: display.bounds,
|
||||
allowArea: process.platform === "darwin",
|
||||
lastArea: previous
|
||||
? { x: previous.x, y: previous.y, width: previous.width, height: previous.height }
|
||||
: null,
|
||||
lastArea: null,
|
||||
windows,
|
||||
cursor: getScreen().getCursorScreenPoint(),
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ const mocks = vi.hoisted(() => ({
|
||||
handlers: new Map<string, (...args: unknown[]) => unknown>(),
|
||||
sources: vi.fn(),
|
||||
validate: vi.fn(),
|
||||
remember: vi.fn(),
|
||||
recording: false,
|
||||
selected: null as SelectedSource | null,
|
||||
exec: vi.fn(),
|
||||
@@ -35,8 +34,7 @@ vi.mock("../recording/ffmpeg", () => ({}));
|
||||
vi.mock("../windowsWindowControl", () => ({}));
|
||||
vi.mock("../captureSelection", () => ({
|
||||
validateCaptureSource: mocks.validate,
|
||||
rememberCaptureSource: mocks.remember,
|
||||
restoreLastCaptureSource: vi.fn(),
|
||||
getSessionCaptureSource: () => mocks.selected,
|
||||
}));
|
||||
vi.mock("../state", () => ({
|
||||
get isCursorCaptureActive() {
|
||||
@@ -146,7 +144,6 @@ it("completes X11 selection even without optional window raising tools", async (
|
||||
const source = { id: "window:101:0", name: "Document" };
|
||||
expect(await picker.select(source)).toEqual(source);
|
||||
expect(await pending).toEqual({ success: true, source });
|
||||
expect(mocks.remember).toHaveBeenCalledWith(source);
|
||||
});
|
||||
it("leaves the source list pending when a selected window no longer exists", async () => {
|
||||
const picker = pickerFixture();
|
||||
@@ -156,7 +153,6 @@ it("leaves the source list pending when a selected window no longer exists", asy
|
||||
"no longer available",
|
||||
);
|
||||
expect(mocks.selected).toBeNull();
|
||||
expect(mocks.remember).not.toHaveBeenCalled();
|
||||
picker.win.close();
|
||||
expect(await pending).toEqual({ success: false, canceled: true });
|
||||
});
|
||||
@@ -176,7 +172,6 @@ it("does not apply a selection after the source list is dismissed during validat
|
||||
await selection;
|
||||
expect(await pending).toEqual({ success: false, canceled: true });
|
||||
expect(mocks.selected).toBeNull();
|
||||
expect(mocks.remember).not.toHaveBeenCalled();
|
||||
});
|
||||
it("does not change sources if recording starts during validation", async () => {
|
||||
const picker = pickerFixture();
|
||||
@@ -187,7 +182,32 @@ it("does not change sources if recording starts during validation", async () =>
|
||||
});
|
||||
await picker.select({ id: "screen:1:0", name: "Screen" });
|
||||
expect(mocks.selected).toBeNull();
|
||||
expect(mocks.remember).not.toHaveBeenCalled();
|
||||
picker.win.close();
|
||||
expect(await pending).toEqual({ success: false, canceled: true });
|
||||
});
|
||||
|
||||
for (const platform of ["darwin", "win32", "linux"]) {
|
||||
it(`starts ${platform} with no source and keeps explicit selection within the session`, async () => {
|
||||
Object.defineProperty(process, "platform", { value: platform });
|
||||
pickerFixture();
|
||||
const read = mocks.handlers.get("get-selected-source")!;
|
||||
expect(read()).toBeNull();
|
||||
const source = { id: "screen:1:0", name: "Screen 1", display_id: "1" };
|
||||
mocks.selected = source;
|
||||
expect(read()).toEqual(source);
|
||||
});
|
||||
}
|
||||
|
||||
it("Wayland starts unselected and prepares its portal only after an explicit picker request", async () => {
|
||||
vi.stubEnv("XDG_SESSION_TYPE", "wayland");
|
||||
const picker = pickerFixture();
|
||||
expect(mocks.handlers.get("get-selected-source")!()).toBeNull();
|
||||
await mocks.handlers.get("open-source-selector")!();
|
||||
expect(mocks.handlers.get("get-selected-source")!()).toEqual({
|
||||
id: "screen:linux-portal",
|
||||
name: "System picker",
|
||||
sourceType: "screen",
|
||||
});
|
||||
expect(picker.create).not.toHaveBeenCalled();
|
||||
expect(mocks.sources).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1,10 +1,6 @@
|
||||
import { createSourceListPicker } from "../../sourceListPicker";
|
||||
import { writeAppSetting } from "../../appSettingsStore";
|
||||
import {
|
||||
rememberCaptureSource,
|
||||
restoreLastCaptureSource,
|
||||
validateCaptureSource,
|
||||
} from "../captureSelection";
|
||||
import { getSessionCaptureSource, validateCaptureSource } from "../captureSelection";
|
||||
import { createRecordingEditorNavigation } from "../../recordingEditorNavigation";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
@@ -435,7 +431,6 @@ export function registerSourceHandlers({
|
||||
if (fromSelector && getSourceSelectorWindow()?.webContents !== event.sender)
|
||||
return selectedSource;
|
||||
setSelectedSource(source);
|
||||
rememberCaptureSource(source);
|
||||
broadcastSelectedSourceChange();
|
||||
if (sourceListPicker.isSender(event.sender)) sourceListPicker.complete(source);
|
||||
stopWindowBoundsCapture();
|
||||
@@ -632,13 +627,17 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh}
|
||||
});
|
||||
|
||||
ipcMain.handle("get-selected-source", () => {
|
||||
return restoreLastCaptureSource();
|
||||
return getSessionCaptureSource();
|
||||
});
|
||||
|
||||
ipcMain.handle("open-source-selector", async () => {
|
||||
if (isCursorCaptureActive) return;
|
||||
if (process.platform === "linux" && isLikelyLinuxWaylandSession(process.env)) {
|
||||
await restoreLastCaptureSource();
|
||||
setSelectedSource({
|
||||
id: LINUX_PORTAL_SCREEN_SOURCE_ID,
|
||||
name: "System picker",
|
||||
sourceType: "screen",
|
||||
});
|
||||
broadcastSelectedSourceChange();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { PointerEvent as ReactPointerEvent } from "react";
|
||||
import {
|
||||
frontmostAt,
|
||||
@@ -108,27 +109,14 @@ export function CapturePicker() {
|
||||
),
|
||||
}))
|
||||
.filter((entry) => entry.rect.width > 0 && entry.rect.height > 0);
|
||||
const next =
|
||||
context.lastArea && context.allowArea
|
||||
? {
|
||||
kind: "area" as const,
|
||||
rect: normalizeAreaRect(
|
||||
{
|
||||
...context.lastArea,
|
||||
x: context.lastArea.x - origin.current.x,
|
||||
y: context.lastArea.y - origin.current.y,
|
||||
},
|
||||
screenRect(),
|
||||
),
|
||||
}
|
||||
: context.cursor
|
||||
? targetAt(
|
||||
context.cursor.x - origin.current.x,
|
||||
context.cursor.y - origin.current.y,
|
||||
)
|
||||
: null;
|
||||
const next = context.cursor
|
||||
? targetAt(
|
||||
context.cursor.x - origin.current.x,
|
||||
context.cursor.y - origin.current.y,
|
||||
)
|
||||
: null;
|
||||
current.current = next;
|
||||
editingArea.current = next?.kind === "area";
|
||||
editingArea.current = false;
|
||||
setTarget(next);
|
||||
requestAnimationFrame(() =>
|
||||
requestAnimationFrame(() => {
|
||||
@@ -291,6 +279,29 @@ export function CapturePicker() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{target?.kind === "area" && (
|
||||
<div
|
||||
className="absolute bottom-12 left-1/2 -translate-x-1/2"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onPointerUp={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === " ") event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
press.current = null;
|
||||
editingArea.current = false;
|
||||
current.current = null;
|
||||
setTarget(null);
|
||||
}}
|
||||
>
|
||||
Choose another source
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-24 flex justify-center">
|
||||
<div className="rounded-full bg-black/80 px-4 py-2 text-sm text-white">
|
||||
{target?.kind === "area" && editingArea.current
|
||||
|
||||
@@ -222,7 +222,7 @@ function LaunchWindowContent() {
|
||||
.catch(() => {
|
||||
if (mounted) {
|
||||
setSourceReady(true);
|
||||
toast.error("Unable to restore the previous source. Pick a source again.");
|
||||
toast.error("Unable to load the recording source. Pick a screen again.");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -284,7 +284,7 @@ function LaunchWindowContent() {
|
||||
{hasSelectedSource ? (
|
||||
<MarqueeText text={selectedSource} />
|
||||
) : (
|
||||
<span>Pick source</span>
|
||||
<span>Pick screen</span>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ProjectLibraryEntry } from "@/components/video-editor/ProjectBrows
|
||||
import type { DesktopSource } from "../popovers/launchPopoverTypes";
|
||||
|
||||
export function useLaunchWindowActions() {
|
||||
const [selectedSource, setSelectedSource] = useState("Pick source");
|
||||
const [selectedSource, setSelectedSource] = useState("Pick screen");
|
||||
const [hasSelectedSource, setHasSelectedSource] = useState(false);
|
||||
const [projectLibraryEntries, setProjectLibraryEntries] = useState<ProjectLibraryEntry[]>([]);
|
||||
|
||||
@@ -58,7 +58,7 @@ export function useLaunchWindowActions() {
|
||||
setHasSelectedSource(true);
|
||||
return;
|
||||
}
|
||||
setSelectedSource("Pick source");
|
||||
setSelectedSource("Pick screen");
|
||||
setHasSelectedSource(false);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -25,15 +25,15 @@ const windows = [
|
||||
},
|
||||
];
|
||||
|
||||
async function openPicker(page: Page, allowArea = true) {
|
||||
async function openPicker(page: Page, allowArea = true, previousArea = false) {
|
||||
await installDesktopBridge(page);
|
||||
await page.addInitScript(
|
||||
({ pickerWindows, allowArea }) => {
|
||||
({ pickerWindows, allowArea, previousArea }) => {
|
||||
const api = (window as unknown as { electronAPI: Record<string, unknown> }).electronAPI;
|
||||
Object.assign(api, {
|
||||
getCapturePickerContext: async () => ({
|
||||
displayBounds: { x: 0, y: 0, width: 1440, height: 1000 },
|
||||
lastArea: null,
|
||||
lastArea: previousArea ? { x: 100, y: 100, width: 400, height: 300 } : null,
|
||||
allowArea,
|
||||
windows: pickerWindows,
|
||||
cursor: null,
|
||||
@@ -46,7 +46,7 @@ async function openPicker(page: Page, allowArea = true) {
|
||||
},
|
||||
});
|
||||
},
|
||||
{ pickerWindows: windows, allowArea },
|
||||
{ pickerWindows: windows, allowArea, previousArea },
|
||||
);
|
||||
await page.goto("/?windowType=capture-picker&displayId=1");
|
||||
await expect(page.locator("html")).toHaveAttribute("data-picker-ready", "true");
|
||||
@@ -153,7 +153,7 @@ for (const { selected, trigger } of [
|
||||
if (selected) {
|
||||
await expect(control).toContainText("Existing screen");
|
||||
} else {
|
||||
await expect(control).toHaveText("Pick source");
|
||||
await expect(control).toHaveText("Pick screen");
|
||||
}
|
||||
await expect(page.locator("html")).not.toHaveAttribute("data-picker-calls");
|
||||
await (trigger === "source"
|
||||
@@ -296,7 +296,7 @@ test("HUD reports a failed pick and keeps cancellation silent", async ({ page })
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("unselected source reads Pick source once and Record stays disabled while picking", async ({
|
||||
test("unselected source reads Pick screen once and Record stays disabled while picking", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installDesktopBridge(page);
|
||||
@@ -314,14 +314,14 @@ test("unselected source reads Pick source once and Record stays disabled while p
|
||||
await page.goto("/?windowType=hud-overlay");
|
||||
const source = page.getByRole("button", { name: "Choose recording source" });
|
||||
await expect(source).toBeEnabled();
|
||||
await expect(source).toHaveText("Pick source");
|
||||
await expect(source).toHaveText("Pick screen");
|
||||
await expect(page.locator("html")).not.toHaveAttribute("data-picker-pending");
|
||||
await source.click();
|
||||
await expect(page.locator("html")).toHaveAttribute("data-picker-pending", "true");
|
||||
await expect(page.getByRole("button", { name: "Record", exact: true })).toBeDisabled();
|
||||
await page.evaluate(() => (window as unknown as { finishPicker: () => void }).finishPicker());
|
||||
await expect(page.getByRole("button", { name: "Record", exact: true })).toBeEnabled();
|
||||
await expect(source).toHaveText("Pick source");
|
||||
await expect(source).toHaveText("Pick screen");
|
||||
});
|
||||
|
||||
test("changing source cancels countdown and discards native warm-up before opening the picker", async ({
|
||||
@@ -441,3 +441,47 @@ test("changing source also cancels countdown on the browser capture path", async
|
||||
await expect(page.locator("html")).toHaveAttribute("data-picked-after-cancel", "true");
|
||||
await expect(page.getByRole("button", { name: "Record", exact: true })).toBeEnabled();
|
||||
});
|
||||
|
||||
test("reopening an area starts with window picking instead of the old area editor", async ({
|
||||
page,
|
||||
}) => {
|
||||
await openPicker(page, true, true);
|
||||
await expect(page.locator("[data-area-handle]")).toHaveCount(0);
|
||||
await page.mouse.move(600, 300);
|
||||
await expect(page.getByText("Notes — Launch checklist · 620 × 460")).toBeVisible();
|
||||
await page.mouse.click(600, 300);
|
||||
expect(await pick(page)).toMatchObject({ kind: "window", windowId: "window:101:0" });
|
||||
});
|
||||
|
||||
for (const destination of ["window", "screen"] as const) {
|
||||
test(`a drawn area can return to ${destination} picking without closing the picker`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await openPicker(page);
|
||||
await page.mouse.move(300, 200);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(700, 450, { steps: 5 });
|
||||
await page.mouse.up();
|
||||
await expect(page.getByText("Area · 400 × 250")).toBeVisible();
|
||||
const change = page.getByRole("button", { name: "Choose another source", exact: true });
|
||||
if (destination === "window") {
|
||||
await change.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
} else {
|
||||
await change.click();
|
||||
}
|
||||
await expect(page.locator("[data-area-handle]")).toHaveCount(0);
|
||||
expect(await pick(page)).toBeUndefined();
|
||||
const point = destination === "window" ? { x: 600, y: 300 } : { x: 1380, y: 60 };
|
||||
await page.mouse.move(point.x, point.y);
|
||||
await expect(
|
||||
page.getByText(
|
||||
destination === "window"
|
||||
? "Notes — Launch checklist · 620 × 460"
|
||||
: "Entire screen · 1440 × 1000",
|
||||
),
|
||||
).toBeVisible();
|
||||
await page.mouse.click(point.x, point.y);
|
||||
expect(await pick(page)).toMatchObject({ kind: destination, displayId: 1, record: false });
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user