mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(desktop): scope browser webviews by host
This commit is contained in:
@@ -17,9 +17,9 @@ It validates the compositor behavior that unit tests cannot see:
|
||||
- the real-Electron host-composer sentinel proves guest Enter cannot submit a focused
|
||||
host composer;
|
||||
- the automation group loads the compiled production keyboard boundary and guest
|
||||
preload, then proves that a page window handler gets first refusal, an unhandled
|
||||
shortcut crosses once, digit wildcard shortcuts cross, and background automation
|
||||
stays in the guest.
|
||||
preload, then proves that a page window handler gets first refusal, unhandled
|
||||
shortcuts cross from both ordinary and editable page targets, digit wildcard
|
||||
shortcuts cross, and background automation stays in the guest.
|
||||
|
||||
Run it with the repo Electron:
|
||||
|
||||
|
||||
@@ -1847,8 +1847,25 @@ async function verifyBrowserKeyboardIsolation({ guest, win, browserId, usesMeta,
|
||||
pass("automation production guest preload forwards one page-unhandled browser shortcut");
|
||||
checks.push({ group: "automation", check: "browser-shortcut-page-first-forward", pass: true });
|
||||
|
||||
automationBrowserShortcut(guest, "1");
|
||||
const editableTarget = await guest.executeJavaScript(
|
||||
"document.getElementById('name').focus(); document.activeElement.id",
|
||||
true,
|
||||
);
|
||||
if (editableTarget !== "name") {
|
||||
fail(`automation editable browser shortcut target was not focused: ${editableTarget}`);
|
||||
}
|
||||
automationBrowserShortcut(guest);
|
||||
await waitForBrowserShortcutInput(shortcutInputs, 2);
|
||||
if (!isDeepStrictEqual(shortcutInputs[1], expectedBrowserShortcutInput)) {
|
||||
fail(
|
||||
`editable browser shortcut crossed boundary incorrectly: inputs=${JSON.stringify(shortcutInputs)}`,
|
||||
);
|
||||
}
|
||||
pass("automation production guest preload forwards an unhandled editable browser shortcut");
|
||||
checks.push({ group: "automation", check: "browser-shortcut-editable-forward", pass: true });
|
||||
|
||||
automationBrowserShortcut(guest, "1");
|
||||
await waitForBrowserShortcutInput(shortcutInputs, 3);
|
||||
const expectedDigitShortcutInput = {
|
||||
alt: false,
|
||||
browserId,
|
||||
@@ -1859,7 +1876,7 @@ async function verifyBrowserKeyboardIsolation({ guest, win, browserId, usesMeta,
|
||||
repeat: false,
|
||||
shift: false,
|
||||
};
|
||||
if (!isDeepStrictEqual(shortcutInputs[1], expectedDigitShortcutInput)) {
|
||||
if (!isDeepStrictEqual(shortcutInputs[2], expectedDigitShortcutInput)) {
|
||||
fail(
|
||||
`digit browser shortcut crossed boundary incorrectly: inputs=${JSON.stringify(shortcutInputs)}`,
|
||||
);
|
||||
|
||||
@@ -4,28 +4,26 @@ import { dispatchTrustedKey } from "./trusted-input.js";
|
||||
|
||||
describe("trusted browser input", () => {
|
||||
test.each([
|
||||
["a", "a"],
|
||||
["Z", "Z"],
|
||||
["Space", "Space"],
|
||||
["ArrowDown", "Down"],
|
||||
])("sends %s as Electron key code %s with unhandled redispatch disabled", (key, keyCode) => {
|
||||
const events: IsolatedKeyboardInputEvent[] = [];
|
||||
["a", "a", ["keyDown", "char", "keyUp"]],
|
||||
["Z", "Z", ["keyDown", "char", "keyUp"]],
|
||||
["Space", "Space", ["keyDown", "keyUp"]],
|
||||
["ArrowDown", "Down", ["keyDown", "keyUp"]],
|
||||
])(
|
||||
"sends %s as Electron key code %s with unhandled redispatch disabled",
|
||||
(key, keyCode, types) => {
|
||||
const events: IsolatedKeyboardInputEvent[] = [];
|
||||
|
||||
dispatchTrustedKey((event) => {
|
||||
events.push(event);
|
||||
}, key);
|
||||
dispatchTrustedKey((event) => {
|
||||
events.push(event);
|
||||
}, key);
|
||||
|
||||
expect(events).toEqual([
|
||||
{
|
||||
type: "keyDown",
|
||||
keyCode,
|
||||
skipIfUnhandled: true,
|
||||
},
|
||||
{
|
||||
type: "keyUp",
|
||||
keyCode,
|
||||
skipIfUnhandled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
expect(events).toEqual(
|
||||
types.map((type) => ({
|
||||
type,
|
||||
keyCode,
|
||||
skipIfUnhandled: true,
|
||||
})),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ const MODIFIER_MASKS: Record<InputModifier, number> = {
|
||||
};
|
||||
|
||||
export interface IsolatedKeyboardInputEvent extends KeyboardInputEvent {
|
||||
type: "keyDown" | "keyUp";
|
||||
type: "char" | "keyDown" | "keyUp";
|
||||
// Electron accepts this NativeWebKeyboardEvent flag even though its public
|
||||
// TypeScript declarations omit it. It stops an unhandled webview key from
|
||||
// being redispatched to the embedder's active DOM element or application menu.
|
||||
@@ -168,6 +168,13 @@ export function dispatchTrustedKey(send: KeyboardInputSender, key: string): void
|
||||
keyCode,
|
||||
skipIfUnhandled: true,
|
||||
});
|
||||
if (key.length === 1) {
|
||||
send({
|
||||
type: "char",
|
||||
keyCode,
|
||||
skipIfUnhandled: true,
|
||||
});
|
||||
}
|
||||
send({
|
||||
type: "keyUp",
|
||||
keyCode,
|
||||
|
||||
@@ -44,31 +44,13 @@ function matchesCode(prefixCode: string, eventCode: string): boolean {
|
||||
return /^(?:Digit|Numpad)[1-9]$/.test(eventCode);
|
||||
}
|
||||
|
||||
function isEditableTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof Element)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
target.closest("[contenteditable=true], [contenteditable=''], [contenteditable=plaintext-only]")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return target.matches("input, textarea, select, [role=textbox]");
|
||||
}
|
||||
|
||||
function installKeydownListener(): void {
|
||||
if (keydownListenerInstalled) {
|
||||
return;
|
||||
}
|
||||
keydownListenerInstalled = true;
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if (
|
||||
!event.isTrusted ||
|
||||
event.defaultPrevented ||
|
||||
!browserId ||
|
||||
isEditableTarget(event.target) ||
|
||||
!matchesPolicy(event)
|
||||
) {
|
||||
if (!event.isTrusted || event.defaultPrevented || !browserId || !matchesPolicy(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
@@ -310,39 +310,4 @@ describe("PaseoBrowserWebviewRegistry", () => {
|
||||
|
||||
expect(registry.hasBrowserInOtherHostWindow(101, "browser-a")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the same-window active selection made before the guest attaches", () => {
|
||||
const registry = new PaseoBrowserWebviewRegistry();
|
||||
|
||||
registry.setWorkspaceActiveBrowser({
|
||||
hostWebContentsId: 101,
|
||||
workspaceId: "workspace-a",
|
||||
browserId: "browser-a",
|
||||
});
|
||||
registry.registerWebContents({
|
||||
webContentsId: 11,
|
||||
browserId: "browser-a",
|
||||
hostWebContentsId: 101,
|
||||
});
|
||||
|
||||
expect(registry.getActiveBrowserIdForHostWindow(101)).toBe("browser-a");
|
||||
});
|
||||
|
||||
it("drops a pre-attach selection when the guest attaches to another host window", () => {
|
||||
const registry = new PaseoBrowserWebviewRegistry();
|
||||
|
||||
registry.setWorkspaceActiveBrowser({
|
||||
hostWebContentsId: 101,
|
||||
workspaceId: "workspace-a",
|
||||
browserId: "browser-a",
|
||||
});
|
||||
registry.registerWebContents({
|
||||
webContentsId: 11,
|
||||
browserId: "browser-a",
|
||||
hostWebContentsId: 202,
|
||||
});
|
||||
|
||||
expect(registry.getActiveBrowserIdForHostWindow(101)).toBeNull();
|
||||
expect(registry.getActiveBrowserIdForHostWindow(202)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user