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> |