securityos/node_modules/comlink/tests/same_window.comlink.test.js

656 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* Copyright 2017 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Comlink from "/base/dist/esm/comlink.mjs";
class SampleClass {
constructor(counterInit = 1) {
this._counter = counterInit;
this._promise = Promise.resolve(4);
}
static get SOME_NUMBER() {
return 4;
}
static ADD(a, b) {
return a + b;
}
get counter() {
return this._counter;
}
set counter(value) {
this._counter = value;
}
get promise() {
return this._promise;
}
method() {
return 4;
}
increaseCounter(delta = 1) {
this._counter += delta;
}
promiseFunc() {
return new Promise((resolve) => setTimeout((_) => resolve(4), 100));
}
proxyFunc() {
return Comlink.proxy({
counter: 0,
inc() {
this.counter++;
},
});
}
throwsAnError() {
throw Error("OMG");
}
}
describe("Comlink in the same realm", function () {
beforeEach(function () {
const { port1, port2 } = new MessageChannel();
port1.start();
port2.start();
this.port1 = port1;
this.port2 = port2;
});
it("can work with objects", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose({ value: 4 }, this.port2);
expect(await thing.value).to.equal(4);
});
it("can work with functions on an object", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose({ f: (_) => 4 }, this.port2);
expect(await thing.f()).to.equal(4);
});
it("can work with functions", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose((_) => 4, this.port2);
expect(await thing()).to.equal(4);
});
it("can work with objects that have undefined properties", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose({ x: undefined }, this.port2);
expect(await thing.x).to.be.undefined;
});
it("can keep the stack and message of thrown errors", async function () {
let stack;
const thing = Comlink.wrap(this.port1);
Comlink.expose((_) => {
const error = Error("OMG");
stack = error.stack;
throw error;
}, this.port2);
try {
await thing();
throw "Should have thrown";
} catch (err) {
expect(err).to.not.eq("Should have thrown");
expect(err.message).to.equal("OMG");
expect(err.stack).to.equal(stack);
}
});
it("can forward an async function error", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(
{
async throwError() {
throw new Error("Should have thrown");
},
},
this.port2
);
try {
await thing.throwError();
} catch (err) {
expect(err.message).to.equal("Should have thrown");
}
});
it("can rethrow non-error objects", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose((_) => {
throw { test: true };
}, this.port2);
try {
await thing();
throw "Should have thrown";
} catch (err) {
expect(err).to.not.equal("Should have thrown");
expect(err.test).to.equal(true);
}
});
it("can rethrow scalars", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose((_) => {
throw "oops";
}, this.port2);
try {
await thing();
throw "Should have thrown";
} catch (err) {
expect(err).to.not.equal("Should have thrown");
expect(err).to.equal("oops");
expect(typeof err).to.equal("string");
}
});
it("can rethrow null", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose((_) => {
throw null;
}, this.port2);
try {
await thing();
throw "Should have thrown";
} catch (err) {
expect(err).to.not.equal("Should have thrown");
expect(err).to.equal(null);
expect(typeof err).to.equal("object");
}
});
it("can work with parameterized functions", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose((a, b) => a + b, this.port2);
expect(await thing(1, 3)).to.equal(4);
});
it("can work with functions that return promises", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(
(_) => new Promise((resolve) => setTimeout((_) => resolve(4), 100)),
this.port2
);
expect(await thing()).to.equal(4);
});
it("can work with classes", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
expect(await instance.method()).to.equal(4);
});
it("can pass parameters to class constructor", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing(23);
expect(await instance.counter).to.equal(23);
});
it("can access a class in an object", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose({ SampleClass }, this.port2);
const instance = await new thing.SampleClass();
expect(await instance.method()).to.equal(4);
});
it("can work with class instance properties", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
expect(await instance._counter).to.equal(1);
});
it("can set class instance properties", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
expect(await instance._counter).to.equal(1);
await (instance._counter = 4);
expect(await instance._counter).to.equal(4);
});
it("can work with class instance methods", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
expect(await instance.counter).to.equal(1);
await instance.increaseCounter();
expect(await instance.counter).to.equal(2);
});
it("can handle throwing class instance methods", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
return instance
.throwsAnError()
.then((_) => Promise.reject())
.catch((err) => {});
});
it("can work with class instance methods multiple times", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
expect(await instance.counter).to.equal(1);
await instance.increaseCounter();
await instance.increaseCounter(5);
expect(await instance.counter).to.equal(7);
});
it("can work with class instance methods that return promises", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
expect(await instance.promiseFunc()).to.equal(4);
});
it("can work with class instance properties that are promises", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
expect(await instance._promise).to.equal(4);
});
it("can work with class instance getters that are promises", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
expect(await instance.promise).to.equal(4);
});
it("can work with static class properties", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
expect(await thing.SOME_NUMBER).to.equal(4);
});
it("can work with static class methods", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
expect(await thing.ADD(1, 3)).to.equal(4);
});
it("can work with bound class instance methods", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
expect(await instance.counter).to.equal(1);
const method = instance.increaseCounter.bind(instance);
await method();
expect(await instance.counter).to.equal(2);
});
it("can work with class instance getters", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
expect(await instance.counter).to.equal(1);
await instance.increaseCounter();
expect(await instance.counter).to.equal(2);
});
it("can work with class instance setters", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
expect(await instance._counter).to.equal(1);
await (instance.counter = 4);
expect(await instance._counter).to.equal(4);
});
const hasBroadcastChannel = (_) => "BroadcastChannel" in self;
guardedIt(hasBroadcastChannel)(
"will work with BroadcastChannel",
async function () {
const b1 = new BroadcastChannel("comlink_bc_test");
const b2 = new BroadcastChannel("comlink_bc_test");
const thing = Comlink.wrap(b1);
Comlink.expose((b) => 40 + b, b2);
expect(await thing(2)).to.equal(42);
}
);
// Buffer transfers seem to have regressed in Safari 11.1, its fixed in 11.2.
const isNotSafari11_1 = (_) =>
!/11\.1(\.[0-9]+)? Safari/.test(navigator.userAgent);
guardedIt(isNotSafari11_1)("will transfer buffers", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose((b) => b.byteLength, this.port2);
const buffer = new Uint8Array([1, 2, 3]).buffer;
expect(await thing(Comlink.transfer(buffer, [buffer]))).to.equal(3);
expect(buffer.byteLength).to.equal(0);
});
guardedIt(isNotSafari11_1)("will copy TypedArrays", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose((b) => b, this.port2);
const array = new Uint8Array([1, 2, 3]);
const receive = await thing(array);
expect(array).to.not.equal(receive);
expect(array.byteLength).to.equal(receive.byteLength);
expect([...array]).to.deep.equal([...receive]);
});
guardedIt(isNotSafari11_1)("will copy nested TypedArrays", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose((b) => b, this.port2);
const array = new Uint8Array([1, 2, 3]);
const receive = await thing({
v: 1,
array,
});
expect(array).to.not.equal(receive.array);
expect(array.byteLength).to.equal(receive.array.byteLength);
expect([...array]).to.deep.equal([...receive.array]);
});
guardedIt(isNotSafari11_1)(
"will transfer deeply nested buffers",
async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose((a) => a.b.c.d.byteLength, this.port2);
const buffer = new Uint8Array([1, 2, 3]).buffer;
expect(
await thing(Comlink.transfer({ b: { c: { d: buffer } } }, [buffer]))
).to.equal(3);
expect(buffer.byteLength).to.equal(0);
}
);
it("will transfer a message port", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose((a) => a.postMessage("ohai"), this.port2);
const { port1, port2 } = new MessageChannel();
await thing(Comlink.transfer(port2, [port2]));
return new Promise((resolve) => {
port1.onmessage = (event) => {
expect(event.data).to.equal("ohai");
resolve();
};
});
});
it("will wrap marked return values", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(
(_) =>
Comlink.proxy({
counter: 0,
inc() {
this.counter += 1;
},
}),
this.port2
);
const obj = await thing();
expect(await obj.counter).to.equal(0);
await obj.inc();
expect(await obj.counter).to.equal(1);
});
it("will wrap marked return values from class instance methods", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
const obj = await instance.proxyFunc();
expect(await obj.counter).to.equal(0);
await obj.inc();
expect(await obj.counter).to.equal(1);
});
it("will wrap marked parameter values", async function () {
const thing = Comlink.wrap(this.port1);
const local = {
counter: 0,
inc() {
this.counter++;
},
};
Comlink.expose(async function (f) {
await f.inc();
}, this.port2);
expect(local.counter).to.equal(0);
await thing(Comlink.proxy(local));
expect(await local.counter).to.equal(1);
});
it("will wrap marked assignments", function (done) {
const thing = Comlink.wrap(this.port1);
const obj = {
onready: null,
call() {
this.onready();
},
};
Comlink.expose(obj, this.port2);
thing.onready = Comlink.proxy(() => done());
thing.call();
});
it("will wrap marked parameter values, simple function", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(async function (f) {
await f();
}, this.port2);
// Weird code because Mocha
await new Promise(async (resolve) => {
thing(Comlink.proxy((_) => resolve()));
});
});
it("will wrap multiple marked parameter values, simple function", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(async function (f1, f2, f3) {
return (await f1()) + (await f2()) + (await f3());
}, this.port2);
// Weird code because Mocha
expect(
await thing(
Comlink.proxy((_) => 1),
Comlink.proxy((_) => 2),
Comlink.proxy((_) => 3)
)
).to.equal(6);
});
it("will proxy deeply nested values", async function () {
const thing = Comlink.wrap(this.port1);
const obj = {
a: {
v: 4,
},
b: Comlink.proxy({
v: 5,
}),
};
Comlink.expose(obj, this.port2);
const a = await thing.a;
const b = await thing.b;
expect(await a.v).to.equal(4);
expect(await b.v).to.equal(5);
await (a.v = 8);
await (b.v = 9);
// Workaround for a weird scheduling inconsistency in Firefox.
// This test failed, but not when run in isolation, and only
// in Firefox. I think there might be problem with task ordering.
await new Promise((resolve) => setTimeout(resolve, 1));
expect(await thing.a.v).to.equal(4);
expect(await thing.b.v).to.equal(9);
});
it("will handle undefined parameters", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose({ f: (_) => 4 }, this.port2);
expect(await thing.f(undefined)).to.equal(4);
});
it("can handle destructuring", async function () {
Comlink.expose(
{
a: 4,
get b() {
return 5;
},
c() {
return 6;
},
},
this.port2
);
const { a, b, c } = Comlink.wrap(this.port1);
expect(await a).to.equal(4);
expect(await b).to.equal(5);
expect(await c()).to.equal(6);
});
it("lets users define transfer handlers", function (done) {
Comlink.transferHandlers.set("event", {
canHandle(obj) {
return obj instanceof Event;
},
serialize(obj) {
return [obj.data, []];
},
deserialize(data) {
return new MessageEvent("message", { data });
},
});
Comlink.expose((ev) => {
expect(ev).to.be.an.instanceOf(Event);
expect(ev.data).to.deep.equal({ a: 1 });
done();
}, this.port1);
const thing = Comlink.wrap(this.port2);
const { port1, port2 } = new MessageChannel();
port1.addEventListener("message", thing.bind(this));
port1.start();
port2.postMessage({ a: 1 });
});
it("can tunnels a new endpoint with createEndpoint", async function () {
Comlink.expose(
{
a: 4,
c() {
return 5;
},
},
this.port2
);
const proxy = Comlink.wrap(this.port1);
const otherEp = await proxy[Comlink.createEndpoint]();
const otherProxy = Comlink.wrap(otherEp);
expect(await otherProxy.a).to.equal(4);
expect(await proxy.a).to.equal(4);
expect(await otherProxy.c()).to.equal(5);
expect(await proxy.c()).to.equal(5);
});
it("released proxy should no longer be useable and throw an exception", async function () {
const thing = Comlink.wrap(this.port1);
Comlink.expose(SampleClass, this.port2);
const instance = await new thing();
await instance[Comlink.releaseProxy]();
expect(() => instance.method()).to.throw();
});
it("released proxy should invoke finalizer", async function () {
let finalized = false;
Comlink.expose(
{
a: "thing",
[Comlink.finalizer]: () => {
finalized = true;
},
},
this.port2
);
const instance = Comlink.wrap(this.port1);
expect(await instance.a).to.equal("thing");
await instance[Comlink.releaseProxy]();
// wait a beat to let the events process
await new Promise((resolve) => setTimeout(resolve, 1));
expect(finalized).to.be.true;
});
// commented out this test as it could be unreliable in various browsers as
// it has to wait for GC to kick in which could happen at any timing
// this does seem to work when testing locally
it.skip("released proxy via GC should invoke finalizer", async function () {
let finalized = false;
Comlink.expose(
{
a: "thing",
[Comlink.finalizer]: () => {
finalized = true;
},
},
this.port2
);
let registry;
// set a long enough timeout to wait for a garbage collection
this.timeout(10000);
// promise will resolve when the proxy is garbage collected
await new Promise(async (resolve, reject) => {
registry = new FinalizationRegistry((heldValue) => {
heldValue();
});
const instance = Comlink.wrap(this.port1);
registry.register(instance, resolve);
expect(await instance.a).to.equal("thing");
});
// wait a beat to let the events process
await new Promise((resolve) => setTimeout(resolve, 1));
expect(finalized).to.be.true;
});
it("can proxy with a given target", async function () {
const thing = Comlink.wrap(this.port1, { value: {} });
Comlink.expose({ value: 4 }, this.port2);
expect(await thing.value).to.equal(4);
});
it("can handle unserializable types", async function () {
const thing = Comlink.wrap(this.port1, { value: {} });
Comlink.expose({ value: () => "boom" }, this.port2);
try {
await thing.value;
} catch (err) {
expect(err.message).to.equal("Unserializable return value");
}
});
});
function guardedIt(f) {
return f() ? it : xit;
}