Test for WeakRef and FinalizationGroup as specified by https://weakrefs.netlify.com
Make sure to run Chrome 74 with --js-flags="--expose-gc --harmony-weak-refs"
Test for WeakRef and FinalizationGroup as specified by https://weakrefs.netlify.com
Make sure to run Chrome 74 with --js-flags="--expose-gc --harmony-weak-refs"
| function taskTurn() { | |
| return new Promise(resolve => setTimeout(resolve, 0)); | |
| } | |
| function makeGcOf(gc, FinalizationGroup) { | |
| return async function gcOf(target) { | |
| // Avoid creating a closure which may capture target | |
| let resolve; | |
| const collected = new Promise(r => (resolve = r)); | |
| const finalizationGroup = new FinalizationGroup(resolve); | |
| finalizationGroup.register(target || {}, 0); | |
| target = undefined; | |
| await taskTurn(); | |
| gc(); | |
| await collected; | |
| return true; | |
| }; | |
| } | |
| function expectThrowIfNotObject(functionToTest) { | |
| it("undefined", async function() { | |
| expect(() => functionToTest(undefined)).to.throw(); | |
| }); | |
| it("null", async function() { | |
| expect(() => functionToTest(null)).to.throw(); | |
| }); | |
| it("boolean", async function() { | |
| expect(() => functionToTest(true)).to.throw(); | |
| }); | |
| it("string", async function() { | |
| expect(() => functionToTest("string")).to.throw(); | |
| }); | |
| it("symbol", async function() { | |
| expect(() => functionToTest(Symbol())).to.throw(); | |
| }); | |
| it("number", async function() { | |
| expect(() => functionToTest(42)).to.throw(); | |
| }); | |
| } | |
| if (typeof WeakRef == "function") { | |
| describe("WeakRef", function() { | |
| let gcOf; | |
| beforeEach(function() { | |
| if ( | |
| typeof gc == "function" && | |
| typeof FinalizationGroup == "function" | |
| ) { | |
| gcOf = makeGcOf(gc, FinalizationGroup); | |
| } else { | |
| gcOf = () => this.skip(); | |
| } | |
| }); | |
| it("should have the right species", async function() { | |
| expect(WeakRef[Symbol.species]).to.be.equal(WeakRef); | |
| }); | |
| describe("constructor", function() { | |
| it("should throw when constructor called without new", async function() { | |
| const constructorFn = WeakRef; | |
| expect(() => constructorFn({})).to.throw(); | |
| }); | |
| describe("should throw when constructed with non-object", function() { | |
| expectThrowIfNotObject(value => new WeakRef(value)); | |
| }); | |
| it("should hold target on creation", async function() { | |
| const ref = new WeakRef({}); | |
| expect(ref.deref()).to.be.ok; | |
| }); | |
| }); | |
| describe("deref", function() { | |
| describe("should throw when function invoked on non-object", function() { | |
| expectThrowIfNotObject(value => | |
| WeakRef.prototype.deref.call(value) | |
| ); | |
| }); | |
| it("should throw when method invoked with wrong this", async function() { | |
| const weakRef = new WeakRef({}); | |
| expect(() => weakRef.deref.call({})).to.throw(); | |
| }); | |
| it("should hold target on deref", async function() { | |
| let object = {}; | |
| const weakRef = new WeakRef(object); | |
| await gcOf(); | |
| expect(weakRef.deref()).to.be.ok; | |
| object = undefined; | |
| if (typeof gc == "function") gc(); // No await since we need to stay in same turn | |
| expect(weakRef.deref()).to.be.ok; | |
| }); | |
| it("should return undefined if target collected (direct observe)", async function() { | |
| let object = {}; | |
| const weakRef = new WeakRef(object); | |
| const collected = gcOf(object); | |
| object = undefined; | |
| await collected; | |
| expect(weakRef.deref()).to.be.equal(undefined); | |
| }); | |
| it("should return undefined if target collected (indirect observe)", async function() { | |
| const weakRef = new WeakRef({}); | |
| await gcOf(); | |
| expect(weakRef.deref()).to.be.equal(undefined); | |
| }); | |
| }); | |
| it("should allow being subclassed", async function() { | |
| class SemiWeakRef extends WeakRef { | |
| constructor(target) { | |
| super(target); | |
| this.target = target; | |
| } | |
| deref() { | |
| return this.target || super.deref(); | |
| } | |
| release() { | |
| this.target = undefined; | |
| } | |
| } | |
| const semiWeakRef = new SemiWeakRef({}); | |
| semiWeakRef.release(); | |
| expect(semiWeakRef.deref()).to.be.ok; | |
| }); | |
| }); | |
| } else { | |
| it("WeakRef not available"); | |
| } | |
| if (typeof FinalizationGroup == "function") { | |
| let unregisterReturnsBool = true; | |
| let workingCleanupSome = true; | |
| describe("FinalizationGroup", function() { | |
| const gcOf = | |
| typeof gc == "function" | |
| ? makeGcOf(gc, FinalizationGroup) | |
| : undefined; | |
| it("should have the right species", async function() { | |
| expect(FinalizationGroup[Symbol.species]).to.be.equal( | |
| FinalizationGroup | |
| ); | |
| }); | |
| describe("register", function() { | |
| describe("should throw when function invoked on non-object", function() { | |
| expectThrowIfNotObject(value => | |
| FinalizationGroup.prototype.register.call(value, {}, 0) | |
| ); | |
| }); | |
| it("should throw when method invoked with wrong this", async function() { | |
| expect(() => | |
| FinalizationGroup.prototype.register.call({}, {}, 0) | |
| ).to.throw(); | |
| }); | |
| describe("should throw when method invoked with non-object target", function() { | |
| let finalizationGroup; | |
| beforeEach(function() { | |
| finalizationGroup = new FinalizationGroup(() => {}); | |
| }); | |
| expectThrowIfNotObject(value => | |
| finalizationGroup.register(value, 0) | |
| ); | |
| }); | |
| it("should return undefined", async function() { | |
| const finalizationGroup = new FinalizationGroup(() => {}); | |
| expect(finalizationGroup.register({}, 0)).to.be.equal( | |
| undefined | |
| ); | |
| }); | |
| it("should allow registering the same object multiple times", async function() { | |
| const object = {}; | |
| const finalizationGroup = new FinalizationGroup(() => {}); | |
| finalizationGroup.register(object, 0); | |
| expect(() => | |
| finalizationGroup.register(object, 1) | |
| ).not.to.throw(); | |
| }); | |
| if (gcOf) { | |
| describe("collection behavior", function() { | |
| it("calls callback on collected object", async function() { | |
| // Calls FinalizationGroup with marker object | |
| // and only completes if callback called | |
| await gcOf(); | |
| }); | |
| it("calls callback on multiple FinalizationGroup for same object", async function() { | |
| let object = {}; | |
| const callback = chai.spy(); | |
| const finalizationGroup = new FinalizationGroup( | |
| callback | |
| ); | |
| finalizationGroup.register(object, 42); | |
| const collected = gcOf(object); | |
| object = undefined; | |
| await collected; | |
| expect(callback).to.have.been.called(); | |
| }); | |
| it("calls callback with multiple holdings for same object - different values", async function() { | |
| let object = {}; | |
| let holdings = new Set(); | |
| const finalizationGroup = new FinalizationGroup( | |
| items => | |
| (holdings = new Set([...holdings, ...items])) | |
| ); | |
| finalizationGroup.register(object, 42); | |
| finalizationGroup.register(object, 5); | |
| const collected = gcOf(object); | |
| object = undefined; | |
| await collected; | |
| expect(holdings).to.contain(5); | |
| expect(holdings).to.contain(42); | |
| expect(holdings).to.have.lengthOf(2); | |
| }); | |
| it("calls callback with multiple holdings for same object - same values", async function() { | |
| let object = {}; | |
| let holdings = new Array(); | |
| const finalizationGroup = new FinalizationGroup(items => | |
| holdings.push(...items) | |
| ); | |
| finalizationGroup.register(object, 42); | |
| finalizationGroup.register(object, 42); | |
| const collected = gcOf(object); | |
| object = undefined; | |
| await collected; | |
| expect(holdings[0]).to.be.equal(42); | |
| expect(holdings[1]).to.be.equal(42); | |
| expect(holdings).to.have.lengthOf(2); | |
| }); | |
| }); | |
| } else { | |
| it("collection behavior test disabled: no gc method"); | |
| } | |
| }); | |
| describe("unregister", function() { | |
| describe("should throw when function invoked on non-object", function() { | |
| expectThrowIfNotObject(value => | |
| FinalizationGroup.prototype.unregister.call(value) | |
| ); | |
| }); | |
| it("should throw when method invoked with wrong this", async function() { | |
| expect(() => | |
| FinalizationGroup.prototype.unregister.call({}) | |
| ).to.throw(); | |
| }); | |
| it("should return boolean", async function() { | |
| const finalizationGroup = new FinalizationGroup(() => {}); | |
| const ret = finalizationGroup.unregister(42); | |
| unregisterReturnsBool = ret === false; | |
| expect(ret).to.be.false; | |
| }); | |
| it("should unregister cell with specific token", async function() { | |
| if (!unregisterReturnsBool) this.skip(); | |
| const finalizationGroup = new FinalizationGroup(() => {}); | |
| finalizationGroup.register({}, 42, 42); | |
| expect(finalizationGroup.unregister(42)).to.be.true; | |
| }); | |
| it("should not unregister cell with token not strictly equal", async function() { | |
| if (!unregisterReturnsBool) this.skip(); | |
| const finalizationGroup = new FinalizationGroup(() => {}); | |
| finalizationGroup.register({}, 42, 42); | |
| expect(finalizationGroup.unregister("42")).to.be.false; | |
| }); | |
| if (gcOf) { | |
| describe("collection behavior", function() { | |
| let finalizationGroup; | |
| let holdings; | |
| before(function() { | |
| holdings = new Set(); | |
| finalizationGroup = new FinalizationGroup( | |
| items => | |
| (holdings = new Set([...holdings, ...items])) | |
| ); | |
| }); | |
| it("callback doesn't contain unregistered holdings - different objects", async function() { | |
| let objects = [{}, {}]; | |
| finalizationGroup.register(objects[0], 5, 0); | |
| finalizationGroup.register(objects[1], 42, 1); | |
| const unregisterExpectation = expect( | |
| finalizationGroup.unregister(0) | |
| ); | |
| if (unregisterReturnsBool) | |
| unregisterExpectation.to.be.true; | |
| const collected = [gcOf(objects[0]), gcOf(objects[1])]; | |
| objects = undefined; | |
| await Promise.all(collected); | |
| expect(holdings).to.contain(42); | |
| expect(holdings).to.have.lengthOf(1); | |
| }); | |
| it("callback doesn't contain unregistered holdings - same object", async function() { | |
| let object = {}; | |
| finalizationGroup.register(object, 5, 0); | |
| finalizationGroup.register(object, 42, 1); | |
| const unregisterExpectation = expect( | |
| finalizationGroup.unregister(0) | |
| ); | |
| if (unregisterReturnsBool) | |
| unregisterExpectation.to.be.true; | |
| const collected = gcOf(object); | |
| object = undefined; | |
| await collected; | |
| expect(holdings).to.contain(42); | |
| expect(holdings).to.have.lengthOf(1); | |
| }); | |
| it("callback doesn't contain unregistered holdings - undefined token value", async function() { | |
| let object = {}; | |
| finalizationGroup.register(object, 5, undefined); | |
| finalizationGroup.register(object, 42, 1); | |
| const unregisterExpectation = expect( | |
| finalizationGroup.unregister(undefined) | |
| ); | |
| if (unregisterReturnsBool) | |
| unregisterExpectation.to.be.true; | |
| const collected = gcOf(object); | |
| object = undefined; | |
| await collected; | |
| expect(holdings).to.contain(42); | |
| expect(holdings).to.have.lengthOf(1); | |
| }); | |
| it("can unregister a finalized but not spent holding", async function() { | |
| const holdings = [5, 42]; | |
| let notIterated = 0; | |
| const finalizationGroup = new FinalizationGroup( | |
| items => { | |
| for (const item of items) { | |
| if (item === notIterated) | |
| expect.fail( | |
| "Iterated on unregistered holding" | |
| ); | |
| notIterated = item; | |
| expect(holdings).to.contain(notIterated); | |
| holdings.forEach(holding => | |
| expect( | |
| finalizationGroup.unregister( | |
| holding | |
| ) | |
| ).to.be.equal(holding == notIterated) | |
| ); | |
| } | |
| } | |
| ); | |
| let object = {}; | |
| finalizationGroup.register( | |
| object, | |
| holdings[0], | |
| holdings[1] | |
| ); | |
| finalizationGroup.register( | |
| object, | |
| holdings[1], | |
| holdings[0] | |
| ); | |
| const collected = gcOf(object); | |
| object = undefined; | |
| await collected; | |
| expect(notIterated).to.be.not.equal(0); | |
| }); | |
| }); | |
| } else { | |
| it("collection behavior test disabled: no gc method"); | |
| } | |
| }); | |
| describe("cleanupSome", function() { | |
| describe("should throw when function invoked on non-object", function() { | |
| expectThrowIfNotObject(value => | |
| FinalizationGroup.prototype.cleanupSome.call(value) | |
| ); | |
| }); | |
| it("should throw when method invoked with wrong this", async function() { | |
| expect(() => | |
| FinalizationGroup.prototype.cleanupSome.call({}) | |
| ).to.throw(); | |
| }); | |
| it("should call callback even when no empty cell", async function() { | |
| const finalizationGroup = new FinalizationGroup(() => {}); | |
| workingCleanupSome = false; | |
| finalizationGroup.cleanupSome( | |
| () => (workingCleanupSome = true) | |
| ); | |
| expect(workingCleanupSome).to.be.true; | |
| }); | |
| it("should throw when called with non-callable", async function() { | |
| const finalizationGroup = new FinalizationGroup(() => {}); | |
| // No point checking this if cleanupSome does nothing | |
| if (!workingCleanupSome) this.skip(); | |
| expect(() => finalizationGroup.cleanupSome({})).to.throw(); | |
| }); | |
| it("should return undefined", async function() { | |
| const finalizationGroup = new FinalizationGroup(() => {}); | |
| expect(finalizationGroup.cleanupSome(() => {})).to.be.equal( | |
| undefined | |
| ); | |
| }); | |
| it("should not call the callback given at constructor if one is provided", async function() { | |
| const callback = chai.spy(); | |
| const constructorCallback = chai.spy(); | |
| const finalizationGroup = new FinalizationGroup( | |
| constructorCallback | |
| ); | |
| finalizationGroup.cleanupSome(callback); | |
| expect(constructorCallback).to.not.have.been.called(); | |
| if (workingCleanupSome) expect(callback).to.have.been.called(); | |
| }); | |
| if (gcOf) { | |
| describe("collection behavior", function() { | |
| it("should yield previously finalized cells", async function() { | |
| let object = {}; | |
| const callback = chai.spy(); | |
| const finalizationGroup = new FinalizationGroup( | |
| callback | |
| ); | |
| finalizationGroup.register(object, 42); | |
| const collected = gcOf(object); | |
| object = undefined; | |
| await collected; | |
| expect(callback).to.have.been.called(); | |
| let holdings; | |
| workingCleanupSome = false; | |
| expect( | |
| finalizationGroup.cleanupSome(items => { | |
| workingCleanupSome = true; | |
| holdings = [...items]; | |
| }) | |
| ); | |
| if (!workingCleanupSome) this.skip(); | |
| expect(holdings).to.contain(42); | |
| expect(holdings).to.have.lengthOf(1); | |
| }); | |
| }); | |
| } else { | |
| it("collection behavior test disabled: no gc method"); | |
| } | |
| }); | |
| describe("iterator", function() { | |
| it("should have the correct toStringTag", async function() { | |
| const finalizationGroup = new FinalizationGroup(() => {}); | |
| let called = false; | |
| finalizationGroup.cleanupSome(items => { | |
| called = true; | |
| expect(items[Symbol.toStringTag]).to.be.equal( | |
| "FinalizationGroup Cleanup Iterator" | |
| ); | |
| }); | |
| if (!called) this.skip(); | |
| }); | |
| it("should not yield any holding if nothing finalized", async function() { | |
| const finalizationGroup = new FinalizationGroup(() => {}); | |
| if (!workingCleanupSome) this.skip(); | |
| finalizationGroup.cleanupSome(items => { | |
| const result = items.next(); | |
| expect(result.done).to.be.true; | |
| expect(result.value).to.be.equal(undefined); | |
| }); | |
| }); | |
| if (gcOf) { | |
| describe("collection behavior", function() { | |
| it("doesn't remove cell if iterator not consumed", async function() { | |
| let object = {}; | |
| const callback = chai.spy(); | |
| const finalizationGroup = new FinalizationGroup( | |
| callback | |
| ); | |
| finalizationGroup.register(object, 42, 42); | |
| const collected = gcOf(object); | |
| object = undefined; | |
| await collected; | |
| expect(callback).to.have.been.called(); | |
| if (unregisterReturnsBool) { | |
| expect(finalizationGroup.unregister(42)).to.be.true; | |
| } else if (workingCleanupSome) { | |
| let notConsumed; | |
| expect( | |
| finalizationGroup.cleanupSome(items => { | |
| notConsumed = [...items]; | |
| }) | |
| ); | |
| expect(notConsumed).to.contain(42); | |
| expect(notConsumed).to.have.lengthOf(1); | |
| } else { | |
| this.skip(); | |
| } | |
| }); | |
| it("doesn't remove cell if iterator is closed before", async function() { | |
| const holdings = [5, 42]; | |
| let notIterated = 0; | |
| const finalizationGroup = new FinalizationGroup( | |
| items => { | |
| for (const item of items) { | |
| notIterated = item; | |
| expect(holdings).to.contain(notIterated); | |
| break; | |
| } | |
| } | |
| ); | |
| let object = {}; | |
| finalizationGroup.register( | |
| object, | |
| holdings[0], | |
| holdings[1] | |
| ); | |
| finalizationGroup.register( | |
| object, | |
| holdings[1], | |
| holdings[0] | |
| ); | |
| const collected = gcOf(object); | |
| object = undefined; | |
| await collected; | |
| expect(notIterated).to.be.not.equal(0); | |
| if (unregisterReturnsBool) { | |
| expect(finalizationGroup.unregister(notIterated)).to | |
| .be.true; | |
| } else if (workingCleanupSome) { | |
| finalizationGroup.cleanupSome(items => { | |
| for (const item of items) | |
| expect(item).to.equal(notIterated); | |
| notIterated = 0; | |
| }); | |
| expect(notIterated).to.be.equal(0); | |
| } else { | |
| this.skip(); | |
| } | |
| }); | |
| }); | |
| } else { | |
| it("collection behavior test disabled: no gc method"); | |
| } | |
| }); | |
| describe("constructor", function() { | |
| it("should throw when constructor called without new", async function() { | |
| const constructorFn = FinalizationGroup; | |
| expect(() => constructorFn(() => {})).to.throw(); | |
| }); | |
| describe("should throw when constructed with non-object", function() { | |
| expectThrowIfNotObject(value => new FinalizationGroup(value)); | |
| }); | |
| it("should throw when constructed with non-callable", async function() { | |
| expect(() => new FinalizationGroup({})).to.throw(); | |
| }); | |
| it("should allow being subclassed", async function() { | |
| class FinalizationGroupSubclass extends FinalizationGroup { | |
| register(target, holding) { | |
| const token = {}; | |
| super.register(target, holding, token); | |
| return token; | |
| } | |
| } | |
| const finalizationGroup = new FinalizationGroupSubclass( | |
| () => {} | |
| ); | |
| const token = finalizationGroup.register({}, 42); | |
| expect(token).to.be.ok; | |
| if (unregisterReturnsBool) | |
| expect(finalizationGroup.unregister(token)).to.be.true; | |
| }); | |
| if (gcOf) { | |
| describe("collection behavior", function() { | |
| it("doesn't call callback if has no empty cell", async function() { | |
| let object = {}; | |
| const callback = chai.spy(); | |
| const finalizationGroup = new FinalizationGroup( | |
| callback | |
| ); | |
| finalizationGroup.register(object, 42); | |
| await gcOf(); | |
| expect(callback).not.to.have.been.called(); | |
| }); | |
| }); | |
| } else { | |
| it("collection behavior test disabled: no gc method"); | |
| } | |
| }); | |
| }); | |
| } else { | |
| it("FinalizationGroup not available"); | |
| } |
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>weakrefs proposal tests</title> | |
| <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"> | |
| <link rel="stylesheet" href="https://unpkg.com/[email protected]/mocha.css"> | |
| </head> | |
| <body> | |
| <div id="mocha"></div> | |
| <script src="https://unpkg.com/[email protected]/mocha.js"></script> | |
| <script src="https://unpkg.com/[email protected]/chai.js"></script> | |
| <script src="https://unpkg.com/[email protected]/chai-spies.js"></script> | |
| <script> | |
| mocha.setup('bdd'); | |
| var expect = chai.expect; | |
| </script> | |
| <script src="./_weakrefs_tests.js"></script> | |
| <script> | |
| mocha.run(); | |
| </script> | |
| </body> | |
| </html> |