feat: contextBridge.executeInMainWorld (#45330)

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Samuel Maddock <samuel.maddock@gmail.com>
This commit is contained in:
trop[bot] 2025-01-31 09:50:44 -05:00 committed by GitHub
parent 9d696ceffe
commit fa03b92f7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 528 additions and 130 deletions

View file

@ -108,7 +108,7 @@ describe('contextBridge', () => {
};
const callWithBindings = (fn: Function, worldId: number = 0) =>
worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]); ;
worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]);
const getGCInfo = async (): Promise<{
trackedValues: number;
@ -408,6 +408,17 @@ describe('contextBridge', () => {
expect(result).equal(true);
});
it('should proxy function arguments only once', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', (a: any, b: any) => a === b);
});
const result = await callWithBindings(async (root: any) => {
const obj = { foo: 1 };
return root.example(obj, obj);
});
expect(result).to.be.true();
});
it('should properly handle errors thrown in proxied functions', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', () => { throw new Error('oh no'); });
@ -1290,6 +1301,131 @@ describe('contextBridge', () => {
});
});
});
describe('executeInMainWorld', () => {
it('serializes function and proxies args', async () => {
await makeBindingWindow(async () => {
const values = [
undefined,
null,
123,
'string',
true,
[123, 'string', true, ['foo']],
() => 'string',
Symbol('foo')
];
function appendArg (arg: any) {
// @ts-ignore
globalThis.args = globalThis.args || [];
// @ts-ignore
globalThis.args.push(arg);
}
for (const value of values) {
try {
await contextBridge.executeInMainWorld({
func: appendArg,
args: [value]
});
} catch {
contextBridge.executeInMainWorld({
func: appendArg,
args: ['FAIL']
});
}
}
});
const result = await callWithBindings(() => {
// @ts-ignore
return globalThis.args.map(arg => {
// Map unserializable IPC types to their type string
if (['function', 'symbol'].includes(typeof arg)) {
return typeof arg;
} else {
return arg;
}
});
});
expect(result).to.deep.equal([
undefined,
null,
123,
'string',
true,
[123, 'string', true, ['foo']],
'function',
'symbol'
]);
});
it('allows function args to be invoked', async () => {
const donePromise = once(ipcMain, 'done');
makeBindingWindow(() => {
const uuid = crypto.randomUUID();
const done = (receivedUuid: string) => {
if (receivedUuid === uuid) {
require('electron').ipcRenderer.send('done');
}
};
contextBridge.executeInMainWorld({
func: (callback, innerUuid) => {
callback(innerUuid);
},
args: [done, uuid]
});
});
await donePromise;
});
it('proxies arguments only once', async () => {
await makeBindingWindow(() => {
const obj = {};
// @ts-ignore
globalThis.result = contextBridge.executeInMainWorld({
func: (a, b) => a === b,
args: [obj, obj]
});
});
const result = await callWithBindings(() => {
// @ts-ignore
return globalThis.result;
}, 999);
expect(result).to.be.true();
});
it('safely clones returned objects', async () => {
await makeBindingWindow(() => {
const obj = contextBridge.executeInMainWorld({
func: () => ({})
});
// @ts-ignore
globalThis.safe = obj.constructor === Object;
});
const result = await callWithBindings(() => {
// @ts-ignore
return globalThis.safe;
}, 999);
expect(result).to.be.true();
});
it('uses internal Function.prototype.toString', async () => {
await makeBindingWindow(() => {
const funcHack = () => {
// @ts-ignore
globalThis.hacked = 'nope';
};
funcHack.toString = () => '() => { globalThis.hacked = \'gotem\'; }';
contextBridge.executeInMainWorld({
func: funcHack
});
});
const result = await callWithBindings(() => {
// @ts-ignore
return globalThis.hacked;
});
expect(result).to.equal('nope');
});
});
});
};