feat: exposeInMainWorld allow to expose non-object APIs (#26594)

This commit is contained in:
Nikita Kot 2020-12-04 18:43:20 +01:00 committed by GitHub
parent b111bba387
commit 7672aa9525
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 313 additions and 13 deletions

View file

@ -104,6 +104,27 @@ describe('contextBridge', () => {
};
it('should proxy numbers', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', 123);
});
const result = await callWithBindings((root: any) => {
return root.example;
});
expect(result).to.equal(123);
});
it('should make global properties read-only', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', 123);
});
const result = await callWithBindings((root: any) => {
root.example = 456;
return root.example;
});
expect(result).to.equal(123);
});
it('should proxy nested numbers', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
myNumber: 123
@ -129,6 +150,16 @@ describe('contextBridge', () => {
});
it('should proxy strings', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', 'my-words');
});
const result = await callWithBindings((root: any) => {
return root.example;
});
expect(result).to.equal('my-words');
});
it('should proxy nested strings', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
myString: 'my-words'
@ -141,6 +172,16 @@ describe('contextBridge', () => {
});
it('should proxy arrays', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', [123, 'my-words']);
});
const result = await callWithBindings((root: any) => {
return [root.example, Array.isArray(root.example)];
});
expect(result).to.deep.equal([[123, 'my-words'], true]);
});
it('should proxy nested arrays', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
myArr: [123, 'my-words']
@ -153,6 +194,21 @@ describe('contextBridge', () => {
});
it('should make arrays immutable', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', [123, 'my-words']);
});
const immutable = await callWithBindings((root: any) => {
try {
root.example.push(456);
return false;
} catch {
return true;
}
});
expect(immutable).to.equal(true);
});
it('should make nested arrays immutable', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
myArr: [123, 'my-words']
@ -170,6 +226,16 @@ describe('contextBridge', () => {
});
it('should proxy booleans', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', true);
});
const result = await callWithBindings((root: any) => {
return root.example;
});
expect(result).to.equal(true);
});
it('should proxy nested booleans', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
myBool: true
@ -182,6 +248,18 @@ describe('contextBridge', () => {
});
it('should proxy promises and resolve with the correct value', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example',
Promise.resolve('i-resolved')
);
});
const result = await callWithBindings((root: any) => {
return root.example;
});
expect(result).to.equal('i-resolved');
});
it('should proxy nested promises and resolve with the correct value', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
myPromise: Promise.resolve('i-resolved')
@ -194,6 +272,21 @@ describe('contextBridge', () => {
});
it('should proxy promises and reject with the correct value', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', Promise.reject(new Error('i-rejected')));
});
const result = await callWithBindings(async (root: any) => {
try {
await root.example;
return null;
} catch (err) {
return err;
}
});
expect(result).to.be.an.instanceOf(Error).with.property('message', 'Uncaught Error: i-rejected');
});
it('should proxy nested promises and reject with the correct value', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
myPromise: Promise.reject(new Error('i-rejected'))
@ -249,6 +342,16 @@ describe('contextBridge', () => {
expect(result).to.deep.equal([123, 'help', false, 'promise']);
});
it('should proxy functions', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', () => 'return-value');
});
const result = await callWithBindings(async (root: any) => {
return root.example();
});
expect(result).equal('return-value');
});
it('should proxy methods that are callable multiple times', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
@ -299,7 +402,31 @@ describe('contextBridge', () => {
expect(result).to.deep.equal([123, 456, 789, false]);
});
it('it should proxy null and undefined correctly', async () => {
it('it should proxy null', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', null);
});
const result = await callWithBindings((root: any) => {
// Convert to strings as although the context bridge keeps the right value
// IPC does not
return `${root.example}`;
});
expect(result).to.deep.equal('null');
});
it('it should proxy undefined', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', undefined);
});
const result = await callWithBindings((root: any) => {
// Convert to strings as although the context bridge keeps the right value
// IPC does not
return `${root.example}`;
});
expect(result).to.deep.equal('undefined');
});
it('it should proxy nested null and undefined correctly', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
values: [null, undefined]
@ -313,6 +440,19 @@ describe('contextBridge', () => {
expect(result).to.deep.equal(['null', 'undefined']);
});
it('should proxy symbols', async () => {
await makeBindingWindow(() => {
const mySymbol = Symbol('unique');
const isSymbol = (s: Symbol) => s === mySymbol;
contextBridge.exposeInMainWorld('symbol', mySymbol);
contextBridge.exposeInMainWorld('isSymbol', isSymbol);
});
const result = await callWithBindings((root: any) => {
return root.isSymbol(root.symbol);
});
expect(result).to.equal(true, 'symbols should be equal across contexts');
});
it('should proxy symbols such that symbol equality works', async () => {
await makeBindingWindow(() => {
const mySymbol = Symbol('unique');
@ -341,6 +481,26 @@ describe('contextBridge', () => {
expect(result).to.equal(123, 'symbols key lookup should work across contexts');
});
it('should proxy typed arrays', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', new Uint8Array(100));
});
const result = await callWithBindings((root: any) => {
return Object.getPrototypeOf(root.example) === Uint8Array.prototype;
});
expect(result).equal(true);
});
it('should proxy regexps', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', /a/g);
});
const result = await callWithBindings((root: any) => {
return Object.getPrototypeOf(root.example) === RegExp.prototype;
});
expect(result).equal(true);
});
it('should proxy typed arrays and regexps through the serializer', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
@ -466,6 +626,22 @@ describe('contextBridge', () => {
expect(result).to.equal('final value');
});
it('should work with complex nested methods and promises attached directly to the global', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example',
(second: Function) => second((fourth: Function) => {
return fourth();
})
);
});
const result = await callWithBindings((root: any) => {
return root.example((third: Function) => {
return third(() => Promise.resolve('final value'));
});
});
expect(result).to.equal('final value');
});
it('should throw an error when recursion depth is exceeded', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', {
@ -641,6 +817,127 @@ describe('contextBridge', () => {
expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true));
});
it('should not leak prototypes when attaching directly to the global', async () => {
await makeBindingWindow(() => {
const toExpose = {
number: 123,
string: 'string',
boolean: true,
arr: [123, 'string', true, ['foo']],
symbol: Symbol('foo'),
bigInt: 10n,
getObject: () => ({ thing: 123 }),
getNumber: () => 123,
getString: () => 'string',
getBoolean: () => true,
getArr: () => [123, 'string', true, ['foo']],
getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] }),
getFunctionFromFunction: async () => () => null,
object: {
number: 123,
string: 'string',
boolean: true,
arr: [123, 'string', true, ['foo']],
getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] })
},
receiveArguments: (fn: any) => fn({ key: 'value' }),
symbolKeyed: {
[Symbol('foo')]: 123
}
};
const keys: string[] = [];
Object.entries(toExpose).forEach(([key, value]) => {
keys.push(key);
contextBridge.exposeInMainWorld(key, value);
});
contextBridge.exposeInMainWorld('keys', keys);
});
const result = await callWithBindings(async (root: any) => {
const { keys } = root;
const cleanedRoot: any = {};
for (const [key, value] of Object.entries(root)) {
if (keys.includes(key)) {
cleanedRoot[key] = value;
}
}
let arg: any;
cleanedRoot.receiveArguments((o: any) => { arg = o; });
const protoChecks = [
...Object.keys(cleanedRoot).map(key => [key, String]),
...Object.getOwnPropertySymbols(cleanedRoot.symbolKeyed).map(key => [key, Symbol]),
[cleanedRoot, Object],
[cleanedRoot.number, Number],
[cleanedRoot.string, String],
[cleanedRoot.boolean, Boolean],
[cleanedRoot.arr, Array],
[cleanedRoot.arr[0], Number],
[cleanedRoot.arr[1], String],
[cleanedRoot.arr[2], Boolean],
[cleanedRoot.arr[3], Array],
[cleanedRoot.arr[3][0], String],
[cleanedRoot.symbol, Symbol],
[cleanedRoot.bigInt, BigInt],
[cleanedRoot.getNumber, Function],
[cleanedRoot.getNumber(), Number],
[cleanedRoot.getObject(), Object],
[cleanedRoot.getString(), String],
[cleanedRoot.getBoolean(), Boolean],
[cleanedRoot.getArr(), Array],
[cleanedRoot.getArr()[0], Number],
[cleanedRoot.getArr()[1], String],
[cleanedRoot.getArr()[2], Boolean],
[cleanedRoot.getArr()[3], Array],
[cleanedRoot.getArr()[3][0], String],
[cleanedRoot.getFunctionFromFunction, Function],
[cleanedRoot.getFunctionFromFunction(), Promise],
[await cleanedRoot.getFunctionFromFunction(), Function],
[cleanedRoot.getPromise(), Promise],
[await cleanedRoot.getPromise(), Object],
[(await cleanedRoot.getPromise()).number, Number],
[(await cleanedRoot.getPromise()).string, String],
[(await cleanedRoot.getPromise()).boolean, Boolean],
[(await cleanedRoot.getPromise()).fn, Function],
[(await cleanedRoot.getPromise()).fn(), String],
[(await cleanedRoot.getPromise()).arr, Array],
[(await cleanedRoot.getPromise()).arr[0], Number],
[(await cleanedRoot.getPromise()).arr[1], String],
[(await cleanedRoot.getPromise()).arr[2], Boolean],
[(await cleanedRoot.getPromise()).arr[3], Array],
[(await cleanedRoot.getPromise()).arr[3][0], String],
[cleanedRoot.object, Object],
[cleanedRoot.object.number, Number],
[cleanedRoot.object.string, String],
[cleanedRoot.object.boolean, Boolean],
[cleanedRoot.object.arr, Array],
[cleanedRoot.object.arr[0], Number],
[cleanedRoot.object.arr[1], String],
[cleanedRoot.object.arr[2], Boolean],
[cleanedRoot.object.arr[3], Array],
[cleanedRoot.object.arr[3][0], String],
[await cleanedRoot.object.getPromise(), Object],
[(await cleanedRoot.object.getPromise()).number, Number],
[(await cleanedRoot.object.getPromise()).string, String],
[(await cleanedRoot.object.getPromise()).boolean, Boolean],
[(await cleanedRoot.object.getPromise()).fn, Function],
[(await cleanedRoot.object.getPromise()).fn(), String],
[(await cleanedRoot.object.getPromise()).arr, Array],
[(await cleanedRoot.object.getPromise()).arr[0], Number],
[(await cleanedRoot.object.getPromise()).arr[1], String],
[(await cleanedRoot.object.getPromise()).arr[2], Boolean],
[(await cleanedRoot.object.getPromise()).arr[3], Array],
[(await cleanedRoot.object.getPromise()).arr[3][0], String],
[arg, Object],
[arg.key, String]
];
return {
protoMatches: protoChecks.map(([a, Constructor]) => Object.getPrototypeOf(a) === Constructor.prototype)
};
});
// Every protomatch should be true
expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true));
});
describe('internalContextBridge', () => {
describe('overrideGlobalValueFromIsolatedWorld', () => {
it('should override top level properties', async () => {