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

656 lines
19 KiB
JavaScript
Raw Permalink Normal View History

2024-09-06 15:32:35 +00:00
/**
* 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;
}