Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ async function record(sourceKind?: string) {
}

beforeEach(() => {
// The portal helper is only looked up in a Wayland session (#1491).
vi.stubEnv("XDG_SESSION_TYPE", "wayland");
helper = new FakeHelper();
spawnMock.mockReset();
spawnMock.mockReturnValue(helper as unknown as ReturnType<typeof spawn>);
Expand All @@ -169,6 +171,7 @@ beforeEach(() => {

afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
});

describe("LinuxNativeCaptureSession", () => {
Expand Down
5 changes: 3 additions & 2 deletions electron/native-bridge/cursor/recording/factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Rectangle } from "electron";
import { isWaylandSession } from "../../../wayland";
import { MacNativeCursorRecordingSession } from "./macNativeCursorRecordingSession";
import { PipeWireCursorRecordingSession } from "./pipeWireCursorRecordingSession";
import type { CursorRecordingSession } from "./session";
Expand Down Expand Up @@ -36,7 +37,7 @@ export function createCursorRecordingSession(
});
}

if (options.platform === "linux") {
if (options.platform === "linux" && isWaylandSession(process.env)) {
// The ScreenCast portal's METADATA cursor mode is the only source of a real
// pointer position on Wayland: `screen.getCursorScreenPoint()` returns
// {0,0} there, so TelemetryRecordingSession produced well-formed recordings
Expand All @@ -50,7 +51,7 @@ export function createCursorRecordingSession(
});
}

// Anything else (a future platform, or a test harness): capture cursor
// Anything else (X11, a future platform, or a test harness): capture cursor
// positions via Electron's `screen` API on an interval. No cursor
// sprites/assets and no clicks, just position telemetry.
return new TelemetryRecordingSession({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ vi.mock("node:fs", async (importOriginal) => {
});

import { spawn } from "node:child_process";
import { PipeWireCursorRecordingSession } from "./pipeWireCursorRecordingSession";
import { createCursorRecordingSession } from "./factory";
import {
findPipeWireCursorHelperPath,
PipeWireCursorRecordingSession,
} from "./pipeWireCursorRecordingSession";
import { TelemetryRecordingSession } from "./telemetryRecordingSession";

/** Minimal stand-in for the helper process: stdio pipes plus kill bookkeeping. */
class FakeHelper extends EventEmitter {
Expand Down Expand Up @@ -93,7 +98,13 @@ function flushStdout() {
return new Promise((resolve) => setTimeout(resolve, 0));
}

const realPlatform = process.platform;

beforeEach(() => {
// The helper is only looked up in a Wayland session (#1491); pin one for the session tests.
Object.defineProperty(process, "platform", { value: "linux" });
vi.stubEnv("XDG_SESSION_TYPE", "wayland");
vi.stubEnv("WAYLAND_DISPLAY", undefined);
helper = new FakeHelper();
spawnMock.mockReset();
spawnMock.mockReturnValue(helper as unknown as ReturnType<typeof spawn>);
Expand All @@ -107,6 +118,30 @@ beforeEach(() => {

afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
Object.defineProperty(process, "platform", { value: realPlatform });
});

describe("X11 session (#1491)", () => {
const cursorOptions = {
getDisplayBounds: () => null,
maxSamples: 100,
platform: "linux" as const,
sampleIntervalMs: 33,
};

it("finds the portal helper in a Wayland session", () => {
expect(findPipeWireCursorHelperPath()).not.toBeNull();
expect(createCursorRecordingSession(cursorOptions)).toBeInstanceOf(
PipeWireCursorRecordingSession,
);
});

it("treats the helper as absent on X11, so capture falls back to Chromium", () => {
vi.stubEnv("XDG_SESSION_TYPE", "x11");
expect(findPipeWireCursorHelperPath()).toBeNull();
expect(createCursorRecordingSession(cursorOptions)).toBeInstanceOf(TelemetryRecordingSession);
});
});

describe("PipeWireCursorRecordingSession", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { accessSync, constants as fsConstants } from "node:fs";
import path from "node:path";
import type { Readable, Writable } from "node:stream";
import type { CursorRecordingData } from "../../../../src/native/contracts";
import { isWaylandSession } from "../../../wayland";
import {
NdjsonLineReader,
PipeWireCursorAccumulator,
Expand Down Expand Up @@ -70,6 +71,10 @@ function helperCandidates() {
}

export function findPipeWireCursorHelperPath() {
// The helper is a ScreenCast portal client, and KDE's portal has no ScreenCast outside
// Wayland: on X11 the take died on "CreateSession: ... Other" (#1491). Without the helper
// every caller falls back to Chromium's capture, which X11 serves without a portal.
if (!isWaylandSession(process.env)) return null;
for (const candidate of helperCandidates()) {
try {
accessSync(candidate, fsConstants.X_OK);
Expand Down
Loading