diff options
Diffstat (limited to 'testing/web-platform/tests/js')
32 files changed, 1908 insertions, 0 deletions
diff --git a/testing/web-platform/tests/js/META.yml b/testing/web-platform/tests/js/META.yml new file mode 100644 index 0000000000..555eb7fe2f --- /dev/null +++ b/testing/web-platform/tests/js/META.yml @@ -0,0 +1,3 @@ +spec: https://tc39.es/ecma262/ +suggested_reviewers: + - Ms2ger diff --git a/testing/web-platform/tests/js/behaviours/HostEnsureCanAddPrivateElement.window.js b/testing/web-platform/tests/js/behaviours/HostEnsureCanAddPrivateElement.window.js new file mode 100644 index 0000000000..03435fa37a --- /dev/null +++ b/testing/web-platform/tests/js/behaviours/HostEnsureCanAddPrivateElement.window.js @@ -0,0 +1,129 @@ +// META: script=/common/get-host-info.sub.js + +// HTML PR https://github.com/whatwg/html/pull/8198 adds a definition for the +// HostEnsureCanAddPrivateElement host hook which disallows private fields on +// WindowProxy and Location objects. +// +// This test case ensure the hook works as designed. + +let host_info = get_host_info(); + +const path = location.pathname.substring(0, location.pathname.lastIndexOf('/')) + '/frame.html'; +const path_setdomain = path + "?setdomain"; + +class Base { + constructor(o) { + return o; + } +} + +class Stamper extends Base { + #x = 10; + static hasX(o) { return #x in o; } +}; + +function test_iframe_window(a_src, b_src) { + const iframe = document.body.appendChild(document.createElement("iframe")); + + var resolve, reject; + var promise = new Promise((res, rej) => { + resolve = res; + reject = rej + }); + + iframe.src = a_src; + iframe.onload = () => { + const windowA = iframe.contentWindow; + try { + assert_throws_js(TypeError, () => { + new Stamper(windowA); + }, "Can't Stamp (maybe cross-origin) exotic WindowProxy"); + assert_equals(Stamper.hasX(windowA), false, "Didn't stamp on WindowProxy"); + } catch (e) { + reject(e); + return; + } + + iframe.src = b_src; + iframe.onload = () => { + const windowB = iframe.contentWindow; + try { + assert_equals(windowA == windowB, true, "Window is same") + assert_throws_js(TypeError, () => { + new Stamper(windowA); + }, "Can't Stamp (maybe cross-origin) exotics on WindowProxy"); + assert_equals(Stamper.hasX(windowB), false, "Didn't stamp on WindowProxy"); + } catch (e) { + reject(e); + return; + } + resolve(); + } + }; + + return promise; +} + + +function test_iframe_location(a_src, b_src) { + const iframe = document.body.appendChild(document.createElement("iframe")); + + var resolve, reject; + var promise = new Promise((res, rej) => { + resolve = res; + reject = rej + }); + + iframe.src = a_src; + iframe.onload = () => { + const locA = iframe.contentWindow.location; + try { + assert_throws_js(TypeError, () => { + new Stamper(locA); + }, "Can't Stamp (maybe cross-origin) exotic Location"); + assert_equals(Stamper.hasX(locA), false, "Didn't stamp on Location"); + } catch (e) { + reject(e); + return; + } + + iframe.src = b_src; + iframe.onload = () => { + const locB = iframe.contentWindow.location + try { + assert_throws_js(TypeError, () => { + new Stamper(locB); + }, "Can't Stamp cross-origin exotic Location"); + assert_equals(Stamper.hasX(locB), false, "Didn't stamp on Location"); + } catch (e) { + reject(e); + return; + } + resolve(); + } + }; + + return promise; +} + +promise_test(() => test_iframe_window(host_info.HTTP_ORIGIN, host_info.HTTP_ORIGIN), "Same Origin: WindowProxy") +promise_test(() => test_iframe_window(host_info.HTTP_ORIGIN, host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT), "Cross Origin (port): WindowProxy") +promise_test(() => test_iframe_window(host_info.HTTP_ORIGIN, host_info.HTTP_REMOTE_ORIGIN), "Cross Origin (remote): WindowProxy") +promise_test(() => test_iframe_window(path, path_setdomain), "Same Origin + document.domain WindowProxy") + + +promise_test(() => test_iframe_location(host_info.HTTP_ORIGIN, host_info.HTTP_ORIGIN), "Same Origin: Location") +promise_test(() => test_iframe_location(host_info.HTTP_ORIGIN, host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT), "Cross Origin (remote): Location") +promise_test(() => test_iframe_location(host_info.HTTP_ORIGIN, host_info.HTTP_REMOTE_ORIGIN), "Cross Origin: Location") +promise_test(() => test_iframe_location(path, path_setdomain), "Same Origin + document.domain: Location") + +// We can do this because promise_test promises to queue tests +// https://web-platform-tests.org/writing-tests/testharness-api.html#promise-tests + +promise_test(async () => document.domain = document.domain, "Set document.domain"); + +promise_test(() => test_iframe_location(path, path_setdomain), "(After document.domain set) Same Origin + document.domain: Location") +promise_test(() => test_iframe_window(path, path_setdomain), "(After document.domain set) Same Origin + document.domain WindowProxy does carry private fields after navigation") + +promise_test(() => test_iframe_location(path_setdomain, path_setdomain), "(After document.domain set) Local navigation (setdomain) Location") +promise_test(() => test_iframe_window(path_setdomain, path_setdomain), "(After document.domain set) Local navigation (setdomain) WindowProxy does carry private fields after navigation") diff --git a/testing/web-platform/tests/js/behaviours/SetPrototypeOf-window.html b/testing/web-platform/tests/js/behaviours/SetPrototypeOf-window.html new file mode 100644 index 0000000000..f104ca107c --- /dev/null +++ b/testing/web-platform/tests/js/behaviours/SetPrototypeOf-window.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for [[SetPrototypeOf]] with Windows</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +test(function() { + assert_throws_js(TypeError, function() { + Object.setPrototypeOf(window, window); + }, "Setting the prototype should throw"); +}, "Setting the prototype of a window to itself via setPrototypeOf"); + +test(function() { + assert_throws_js(TypeError, function() { + window.__proto__ = window; + }, "Setting the prototype should throw"); +}, "Setting the prototype of a window to itself via __proto__"); + +test(function() { + assert_throws_js(TypeError, function() { + Object.setPrototypeOf(window, Object.create(window)); + }, "Setting the prototype should throw"); +}, "Setting the prototype of a window to something that has the window on " + + "its proto chain via setPrototypeOf"); + +test(function() { + assert_throws_js(TypeError, function() { + window.__proto__ = Object.create(window); + }, "Setting the prototype should throw"); +}, "Setting the prototype of a window to something that has the window on " + + "its proto chain via __proto__"); +</script> diff --git a/testing/web-platform/tests/js/behaviours/frame.html b/testing/web-platform/tests/js/behaviours/frame.html new file mode 100644 index 0000000000..c06d1bf2b9 --- /dev/null +++ b/testing/web-platform/tests/js/behaviours/frame.html @@ -0,0 +1,10 @@ +<!doctype html> +<html> +<head> +<script> + if (location.search == "?setdomain") { + document.domain = document.domain; + } +</script> +</head> +</html> diff --git a/testing/web-platform/tests/js/builtins/Array.DefineOwnProperty.html b/testing/web-platform/tests/js/builtins/Array.DefineOwnProperty.html new file mode 100644 index 0000000000..40ed00a4c7 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Array.DefineOwnProperty.html @@ -0,0 +1,24 @@ +<!doctype html> +<title>Array.[[DefineOwnProperty]]</title> +<link rel=author href=mailto:Ms2ger@gmail.com title=Ms2ger> +<link rel=help href=http://es5.github.com/#x15.4.5.1> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> +test(function() { + var arr = new Array; + assert_equals(arr.length, 0); + + var called = 0; + Object.defineProperty(arr, 0, { get: function() { ++called; return 7 } }); + assert_equals(arr.length, 1); + assert_equals(called, 0); + + assert_equals(arr[0], 7); + assert_equals(called, 1); + + assert_equals(String(arr), "7"); + assert_equals(called, 2); +}); +</script> diff --git a/testing/web-platform/tests/js/builtins/Array.prototype.join-order.html b/testing/web-platform/tests/js/builtins/Array.prototype.join-order.html new file mode 100644 index 0000000000..ce091a5e54 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Array.prototype.join-order.html @@ -0,0 +1,86 @@ +<!doctype html> +<title>Array.prototype.join</title> +<link rel=author href=mailto:Ms2ger@gmail.com title=Ms2ger> +<link rel=help href=http://es5.github.com/#x15.4.4.5> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> + +<div id=log></div> +<script> +var test_error = { name: "test" }; + +// Step 1. +test(function() { + assert_throws_js(TypeError, function() { + [].join.call(null, { + toString: function() { assert_unreached(); } + }); + }); + assert_throws_js(TypeError, function() { + [].join.call(undefined, { + toString: function() { assert_unreached(); } + }); + }); +}, "Array.prototype.join must call ToObject before looking at the separator argument.") + +var generateTest = function(throwing_getter, getter_name) { + var throwing_object = {}; + var interfaces = [Boolean, Number]; + + var objects = interfaces.map(function(p) { return p.prototype; }); + objects.push(throwing_object); + objects.forEach(function(object) { + Object.defineProperty(object, "length", { + get: throwing_getter, + configurable: true + }); + }); + + [throwing_object, true, false, 0, 1, Math.PI].forEach(function(that) { + test(function() { + assert_throws_exactly(test_error, function() { + [].join.call(that, ","); + }); + }, "Array.prototype.join must forward the exception from the this " + + "object's length property with this=" + format_value(that) + " and " + + "getter " + getter_name + ".") + test(function() { + assert_throws_exactly(test_error, function() { + [].join.call(that, { + toString: function() { assert_unreached(); } + }); + }); + }, "Array.prototype.join must get the this object's length property " + + "before looking at the separator argument with this=" + + format_value(that) + " and getter " + getter_name + ".") + }); + interfaces.forEach(function(iface) { + delete iface.length; + }); +} + +// Step 2. +test(function() { + generateTest(function() { throw test_error; }, "function"); +}, "Step 2."); + +// Step 3. +test(function() { + generateTest(function() { + return { + valueOf: function() { throw test_error; } + }; + }, "valueOf"); + generateTest(function() { + return { + toString: function() { throw test_error; } + }; + }, "toString"); + generateTest(function() { + return { + valueOf: function() { throw test_error; }, + toString: function() { assert_unreached("toString should not be invoked if valueOf exists"); } + }; + }, "valueOf and toString"); +}, "Step 3."); +</script> diff --git a/testing/web-platform/tests/js/builtins/Math.max.html b/testing/web-platform/tests/js/builtins/Math.max.html new file mode 100644 index 0000000000..a4a6ae27c8 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Math.max.html @@ -0,0 +1,13 @@ +<!doctype html> +<title>Math.max</title> +<link rel=author href=mailto:Ms2ger@gmail.com title=Ms2ger> +<link rel=help href=http://es5.github.com/#x15.8.2> +<link rel=help href=http://es5.github.com/#x15.8.2.11> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> + +<div id=log></div> +<script src=Math.maxmin.js></script> +<script> +testMathMaxMin("max"); +</script> diff --git a/testing/web-platform/tests/js/builtins/Math.maxmin.js b/testing/web-platform/tests/js/builtins/Math.maxmin.js new file mode 100644 index 0000000000..bf7b2cd8cd --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Math.maxmin.js @@ -0,0 +1,57 @@ +function testMathMaxMin(aFun) { + var test_error = { name: "test" }; + test(function() { + assert_throws_exactly(test_error, function() { + Math[aFun](NaN, { + valueOf: function() { + throw test_error; + } + }); + }); + }, "ToNumber should be called on all arguments: NaN."); + test(function() { + assert_throws_exactly(test_error, function() { + Math[aFun](-Infinity, { + valueOf: function() { + throw test_error; + } + }); + }); + }, "ToNumber should be called on all arguments: -Infinity."); + test(function() { + assert_throws_exactly(test_error, function() { + Math[aFun](Infinity, { + valueOf: function() { + throw test_error; + } + }); + }); + }, "ToNumber should be called on all arguments: Infinity."); + test(function() { + assert_throws_exactly(test_error, function() { + Math[aFun]({ + valueOf: function() { + throw test_error; + } + }, + { + valueOf: function() { + throw 7; + } + }); + }); + }, "ToNumber should be called left to right."); + test(function() { + assert_equals(Math[aFun]("1"), 1); + }, "Should return a number."); + test(function() { + var expected = { + "max": 0, + "min": -0 + } + assert_equals(Math[aFun](0, -0), expected[aFun]); + assert_equals(Math[aFun](-0, 0), expected[aFun]); + assert_equals(Math[aFun](0, 0), 0); + assert_equals(Math[aFun](-0, -0), -0); + }, "Should handle negative zero correctly."); +} diff --git a/testing/web-platform/tests/js/builtins/Math.min.html b/testing/web-platform/tests/js/builtins/Math.min.html new file mode 100644 index 0000000000..4ae71b9d76 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Math.min.html @@ -0,0 +1,13 @@ +<!doctype html> +<title>Math.min</title> +<link rel=author href=mailto:Ms2ger@gmail.com title=Ms2ger> +<link rel=help href=http://es5.github.com/#x15.8.2> +<link rel=help href=http://es5.github.com/#x15.8.2.12> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> + +<div id=log></div> +<script src=Math.maxmin.js></script> +<script> +testMathMaxMin("min"); +</script> diff --git a/testing/web-platform/tests/js/builtins/Object.prototype.freeze.html b/testing/web-platform/tests/js/builtins/Object.prototype.freeze.html new file mode 100644 index 0000000000..1e5ed418a9 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Object.prototype.freeze.html @@ -0,0 +1,35 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Object.freeze</title> +<link rel="author" title="Masaya Iseki" href="mailto:iseki.m.aa@gmail.com"> +<link rel="help" href="https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.freeze"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +test(function() { + [{}, []].forEach(function(that) { + assert_false(Object.isFrozen(that)); + that.prop = 'exist'; + + Object.freeze(that); + assert_false(Object.isExtensible(that)); + assert_true(Object.isSealed(that)); + assert_true(Object.isFrozen(that)); + + that.extension = 'This property should not be added'; + assert_equals(undefined, that.extension, 'Confirm to prevent adding property.'); + + that.prop = 'changed'; + assert_equals('exist', that.prop, + 'Confirm to prevent changing a property value.'); + + delete that.prop; + assert_equals('exist', that.prop, 'Confirm to prevent deleting a property.'); + }); +}); +</script> + +</body> +</html> diff --git a/testing/web-platform/tests/js/builtins/Object.prototype.getOwnPropertyNames.html b/testing/web-platform/tests/js/builtins/Object.prototype.getOwnPropertyNames.html new file mode 100644 index 0000000000..582f41ba10 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Object.prototype.getOwnPropertyNames.html @@ -0,0 +1,56 @@ +<!doctype html> +<title>Object.prototype.getOwnPropertyNames</title> +<link rel=help href=http://es5.github.io/#x15.2.3.4> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> + +<div id=log></div> +<script> +test(function () { + var obj = {0: 'a', 1: 'b', 2: 'c'}; + assert_array_equals( + Object.getOwnPropertyNames(obj).sort(), + ['0', '1', '2'] + ); +}, "object"); + +test(function () { + var arr = ['a', 'b', 'c']; + assert_array_equals( + Object.getOwnPropertyNames(arr).sort(), + ['0', '1', '2', 'length'] + ); +}, "array-like"); + +test(function () { + var obj = Object.create({}, { + getFoo: { + value: function() { return this.foo; }, + enumerable: false + } + }); + obj.foo = 1; + assert_array_equals( + Object.getOwnPropertyNames(obj).sort(), + ['foo', 'getFoo'] + ); +}, "non-enumerable property"); + +test(function() { + function ParentClass() {} + ParentClass.prototype.inheritedMethod = function() {}; + + function ChildClass() { + this.prop = 5; + this.method = function() {}; + } + ChildClass.prototype = new ParentClass; + ChildClass.prototype.prototypeMethod = function() {}; + + var obj = new ChildClass; + assert_array_equals( + Object.getOwnPropertyNames(obj).sort(), + ['method', 'prop'] + ); +}, 'items on the prototype chain are not listed'); +</script> diff --git a/testing/web-platform/tests/js/builtins/Object.prototype.hasOwnProperty-order.html b/testing/web-platform/tests/js/builtins/Object.prototype.hasOwnProperty-order.html new file mode 100644 index 0000000000..50325d9ea9 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Object.prototype.hasOwnProperty-order.html @@ -0,0 +1,21 @@ +<!doctype html> +<title>Object.prototype.hasOwnProperty</title> +<link rel=author href=mailto:Ms2ger@gmail.com title=Ms2ger> +<link rel=help href=http://es5.github.com/#x15.4.4.5> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> + +<div id=log></div> +<script> +var test_error = { name: "test" }; + +test(function() { + [null, undefined, {}].forEach(function(that) { + test(function() { + assert_throws_exactly(test_error, function() { + ({}).hasOwnProperty.call(that, { toString: function() { throw test_error; } }); + }); + }); + }); +}); +</script> diff --git a/testing/web-platform/tests/js/builtins/Object.prototype.hasOwnProperty-prototype-chain.html b/testing/web-platform/tests/js/builtins/Object.prototype.hasOwnProperty-prototype-chain.html new file mode 100644 index 0000000000..7c02257fbc --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Object.prototype.hasOwnProperty-prototype-chain.html @@ -0,0 +1,35 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Object.prototype.hasOwnProperty: Check prototype chain</title> +<link rel="author" title="Masaya Iseki" href="mailto:iseki.m.aa@gmail.com"> +<link rel="help" href="https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.prototype.hasownproperty"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +test(function() { + [{}, []].forEach(function(that) { + that.prop = 'exists'; + assert_true(that.hasOwnProperty('prop')); + assert_true('hasOwnProperty' in that); + assert_false(that.hasOwnProperty('hasOwnProperty')); + }); +}); + +test(function() { + ['foo', 42].forEach(function(that) { + assert_false(that.hasOwnProperty('hasOwnProperty')); + }); +}); + +test(function() { + [null, undefined].forEach(function(that) { + assert_throws_js(TypeError, + function() { that.hasOwnProperty('hasOwnProperty'); }); + }); +}); +</script> + +</body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/js/builtins/Object.prototype.preventExtensions.html b/testing/web-platform/tests/js/builtins/Object.prototype.preventExtensions.html new file mode 100644 index 0000000000..ceea7b3dd1 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Object.prototype.preventExtensions.html @@ -0,0 +1,35 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Object.preventExtensions</title> +<link rel="author" title="Masaya Iseki" href="mailto:iseki.m.aa@gmail.com"> +<link rel="help" href="https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.preventextensions"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +test(function() { + [{}, []].forEach(function(that){ + assert_true(Object.isExtensible(that)); + that.prop = 'exist'; + + Object.preventExtensions(that); + assert_false(Object.isExtensible(that)); + assert_false(Object.isFrozen(that)); + assert_false(Object.isSealed(that)); + + that.extension = 'This property should not be added'; + assert_equals(undefined, that.extension, 'Confirm to prevent adding property.'); + + that.prop = 'changed'; + assert_equals('changed', that.prop, + 'Confirm to be able to change a property value.'); + + delete that.prop; + assert_equals(undefined, that.prop, 'Confirm to be able to delete a property.'); + }); +}); +</script> + +</body> +</html> diff --git a/testing/web-platform/tests/js/builtins/Object.prototype.seal.html b/testing/web-platform/tests/js/builtins/Object.prototype.seal.html new file mode 100644 index 0000000000..ad84d8c218 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Object.prototype.seal.html @@ -0,0 +1,35 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Object.seal</title> +<link rel="author" title="Masaya Iseki" href="mailto:iseki.m.aa@gmail.com"> +<link rel="help" href="https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.seal"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +test(function() { + [{}, []].forEach(function(that) { + assert_false(Object.isSealed(that)); + that.prop = 'exist'; + + Object.seal(that); + assert_false(Object.isExtensible(that)); + assert_true(Object.isSealed(that)); + assert_false(Object.isFrozen(that)); + + that.extension = 'This property should not be added'; + assert_equals(undefined, that.extension, 'Confirm to prevent adding property.'); + + that.prop = 'changed'; + assert_equals('changed', that.prop, + 'Confirm to be able to change a property value.'); + + delete that.prop; + assert_equals('changed', that.prop, 'Confirm to prevent deleting a property.'); + }); +}); +</script> + +</body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/js/builtins/Promise-incumbent-global-subframe.sub.html b/testing/web-platform/tests/js/builtins/Promise-incumbent-global-subframe.sub.html new file mode 100644 index 0000000000..dde0ac953e --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Promise-incumbent-global-subframe.sub.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<iframe src="{{location[scheme]}}://{{domains[www2]}}:{{ports[http][0]}}{{location[path]}}/../Promise-incumbent-global-subsubframe.sub.html"></iframe> +<script> + document.domain = "{{host}}"; + onmessage = function(e) { + if (e.data == "start") { + frames[0].Promise.resolve().then(frames[0].postMessage.bind(frames[0], "start", "*")); + } else { + parent.postMessage(e.data, "*"); + } + } +</script> diff --git a/testing/web-platform/tests/js/builtins/Promise-incumbent-global-subsubframe.sub.html b/testing/web-platform/tests/js/builtins/Promise-incumbent-global-subsubframe.sub.html new file mode 100644 index 0000000000..9edd9d278a --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Promise-incumbent-global-subsubframe.sub.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<script> + document.domain = "{{host}}"; + onmessage = function (e) { + parent.postMessage( + { + actual: e.origin, + expected: "{{location[scheme]}}://{{domains[www1]}}:{{ports[http][0]}}", + reason: "Incumbent should have been the caller of then()" + }, + "*"); + } +</script> diff --git a/testing/web-platform/tests/js/builtins/Promise-incumbent-global.sub.html b/testing/web-platform/tests/js/builtins/Promise-incumbent-global.sub.html new file mode 100644 index 0000000000..6ae0a9fe5e --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Promise-incumbent-global.sub.html @@ -0,0 +1,20 @@ +<!doctype html> +<meta charset=utf-8> +<title></title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<iframe src="{{location[scheme]}}://{{domains[www1]}}:{{ports[http][0]}}{{location[path]}}/../Promise-incumbent-global-subframe.sub.html"></iframe> +<script> + +var t = async_test("Check the incumbent global Promise callbacks are called with"); + +onload = t.step_func(function() { + onmessage = t.step_func_done(function(e) { + var d = e.data; + assert_equals(d.actual, d.expected, d.reason); + }); + + frames[0].postMessage("start", "*"); +}); + +</script> diff --git a/testing/web-platform/tests/js/builtins/Promise-subclassing.html b/testing/web-platform/tests/js/builtins/Promise-subclassing.html new file mode 100644 index 0000000000..2349c07b05 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/Promise-subclassing.html @@ -0,0 +1,265 @@ +<!doctype html> +<meta charset=utf-8> +<title></title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> + +var theLog = []; +var speciesGets = 0; +var speciesCalls = 0; +var constructorCalls = 0; +var constructorGets = 0; +var resolveCalls = 0; +var rejectCalls = 0; +var thenCalls = 0; +var catchCalls = 0; +var allCalls = 0; +var raceCalls = 0; +var nextCalls = 0; + +function takeLog() { + var oldLog = theLog; + theLog = []; + speciesGets = speciesCalls = constructorCalls = resolveCalls = + rejectCalls = thenCalls = catchCalls = allCalls = raceCalls = + nextCalls = constructorGets = 0; + return oldLog; +} + +function clearLog() { + takeLog(); +} + +function log(str) { + theLog.push(str); +} + +class LoggingPromise extends Promise { + constructor(func) { + super(func); + Object.defineProperty(this, "constructor", + { + get: function() { + ++constructorGets; + log(`Constructor get ${constructorGets}`); + return Object.getPrototypeOf(this).constructor; + } + }); + ++constructorCalls; + log(`Constructor ${constructorCalls}`); + } + + static get [Symbol.species]() { + ++speciesGets; + log(`Species get ${speciesGets}`); + return LoggingSpecies; + } + + static resolve(val) { + ++resolveCalls; + log(`Resolve ${resolveCalls}`); + return super.resolve(val); + } + + static reject(val) { + ++rejectCalls; + log(`Reject ${rejectCalls}`); + return super.reject(val); + } + + then(resolve, reject) { + ++thenCalls; + log(`Then ${thenCalls}`); + return super.then(resolve, reject); + } + + catch(handler) { + ++catchCalls; + log(`Catch ${catchCalls}`); + return super.catch(handler); + } + + static all(val) { + ++allCalls; + log(`All ${allCalls}`); + return super.all(val); + } + + static race(val) { + ++raceCalls; + log(`Race ${raceCalls}`); + return super.race(val); + } +} + +class LoggingIterable { + constructor(array) { + this.iter = array[Symbol.iterator](); + } + + get [Symbol.iterator]() { return () => this } + + next() { + ++nextCalls; + log(`Next ${nextCalls}`); + return this.iter.next(); + } +} + +class LoggingSpecies extends LoggingPromise { + constructor(func) { + ++speciesCalls; + log(`Species call ${speciesCalls}`); + super(func) + } +} + +class SpeciesLessPromise extends LoggingPromise { + static get [Symbol.species]() { + return undefined; + } +} + +promise_test(function testBasicConstructor() { + var p = new LoggingPromise((resolve) => resolve(5)); + var log = takeLog(); + assert_array_equals(log, ["Constructor 1"]); + assert_true(p instanceof LoggingPromise); + return p.then(function(arg) { + assert_equals(arg, 5); + }); +}, "Basic constructor behavior"); + +promise_test(function testPromiseRace() { + clearLog(); + var p = LoggingPromise.race(new LoggingIterable([1, 2])); + var log = takeLog(); + assert_array_equals(log, ["Race 1", "Constructor 1", + "Next 1", "Resolve 1", "Constructor 2", + "Then 1", "Constructor get 1", "Species get 1", "Species call 1", "Constructor 3", + "Next 2", "Resolve 2", "Constructor 4", + "Then 2", "Constructor get 2", "Species get 2", "Species call 2", "Constructor 5", + "Next 3"]); + assert_true(p instanceof LoggingPromise); + return p.then(function(arg) { + assert_in_array(arg, [1, 2]); + }); +}, "Promise.race behavior"); + +promise_test(function testPromiseRaceNoSpecies() { + clearLog(); + var p = SpeciesLessPromise.race(new LoggingIterable([1, 2])); + var log = takeLog(); + assert_array_equals(log, ["Race 1", "Constructor 1", + "Next 1", "Resolve 1", "Constructor 2", + "Then 1", "Constructor get 1", + "Next 2", "Resolve 2", "Constructor 3", + "Then 2", "Constructor get 2", + "Next 3"]); + assert_true(p instanceof SpeciesLessPromise); + return p.then(function(arg) { + assert_in_array(arg, [1, 2]); + }); +}, "Promise.race without species behavior"); + +promise_test(function testPromiseAll() { + clearLog(); + var p = LoggingPromise.all(new LoggingIterable([1, 2])); + var log = takeLog(); + assert_array_equals(log, ["All 1", "Constructor 1", + "Next 1", "Resolve 1", "Constructor 2", + "Then 1", "Constructor get 1", "Species get 1", "Species call 1", "Constructor 3", + "Next 2", "Resolve 2", "Constructor 4", + "Then 2", "Constructor get 2", "Species get 2", "Species call 2", "Constructor 5", + "Next 3"]); + assert_true(p instanceof LoggingPromise); + return p.then(function(arg) { + assert_array_equals(arg, [1, 2]); + }); +}, "Promise.all behavior"); + +promise_test(function testPromiseResolve() { + clearLog(); + var p = LoggingPromise.resolve(5); + var log = takeLog(); + assert_array_equals(log, ["Resolve 1", "Constructor 1"]); + var q = LoggingPromise.resolve(p); + assert_equals(p, q, + "Promise.resolve with same constructor should preserve identity"); + log = takeLog(); + assert_array_equals(log, ["Resolve 1", "Constructor get 1"]); + + var r = Promise.resolve(p); + log = takeLog(); + assert_array_equals(log, ["Constructor get 1"]); + assert_not_equals(p, r, + "Promise.resolve with different constructor should " + + "create a new Promise instance (1)") + var s = Promise.resolve(6); + var u = LoggingPromise.resolve(s); + log = takeLog(); + assert_array_equals(log, ["Resolve 1", "Constructor 1"]); + assert_not_equals(s, u, + "Promise.resolve with different constructor should " + + "create a new Promise instance (2)") + + Object.defineProperty(s, "constructor", { value: LoggingPromise }); + var v = LoggingPromise.resolve(s); + log = takeLog(); + assert_array_equals(log, ["Resolve 1"]); + assert_equals(v, s, "Faking the .constructor should work"); + assert_false(v instanceof LoggingPromise); + + var results = Promise.all([p, q, r, s, u, v]); + return results.then(function(arg) { + assert_array_equals(arg, [5, 5, 5, 6, 6, 6]); + }); +}, "Promise.resolve behavior"); + +promise_test(function testPromiseReject() { + clearLog(); + var p = LoggingPromise.reject(5); + var log = takeLog(); + assert_array_equals(log, ["Reject 1", "Constructor 1"]); + + return p.catch(function(arg) { + assert_equals(arg, 5); + }); +}, "Promise.reject behavior"); + +promise_test(function testPromiseThen() { + clearLog(); + var p = LoggingPromise.resolve(5); + var log = takeLog(); + assert_array_equals(log, ["Resolve 1", "Constructor 1"]); + + var q = p.then((x) => x*x); + log = takeLog(); + assert_array_equals(log, ["Then 1", "Constructor get 1", "Species get 1", + "Species call 1", "Constructor 1"]); + assert_true(q instanceof LoggingPromise); + + return q.then(function(arg) { + assert_equals(arg, 25); + }); +}, "Promise.then behavior"); + +promise_test(function testPromiseCatch() { + clearLog(); + var p = LoggingPromise.reject(5); + var log = takeLog(); + assert_array_equals(log, ["Reject 1", "Constructor 1"]); + + var q = p.catch((x) => x*x); + log = takeLog(); + assert_array_equals(log, ["Catch 1", "Then 1", "Constructor get 1", + "Species get 1", "Species call 1", "Constructor 1"]); + assert_true(q instanceof LoggingPromise); + + return q.then(function(arg) { + assert_equals(arg, 25); + }); +}, "Promise.catch behavior"); + +</script> diff --git a/testing/web-platform/tests/js/builtins/WeakMap.prototype-properties.html b/testing/web-platform/tests/js/builtins/WeakMap.prototype-properties.html new file mode 100644 index 0000000000..2c2bddfeb7 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/WeakMap.prototype-properties.html @@ -0,0 +1,104 @@ +<!doctype html> +<title>WeakMap.prototype</title> +<link rel=author href=mailto:Ms2ger@gmail.com title=Ms2ger> +<link rel=help href=https://people.mozilla.org/~jorendorff/es6-draft.html#sec-properties-of-the-weakmap-prototype-object> +<link rel=help href=https://people.mozilla.org/~jorendorff/es6-draft.html#sec-functioninitialize> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> +function assert_propdesc(obj, prop, Writable, Enumerable, Configurable) { + var propdesc = Object.getOwnPropertyDescriptor(obj, prop); + assert_equals(typeof propdesc, "object"); + assert_equals(propdesc.writable, Writable, "[[Writable]]"); + assert_equals(propdesc.enumerable, Enumerable, "[[Enumerable]]"); + assert_equals(propdesc.configurable, Configurable, "[[Configurable]]"); +} + +function test_length(fun, expected) { + test(function() { + assert_propdesc(WeakMap.prototype[fun], "length", false, false, true); + assert_equals(WeakMap.prototype[fun].length, expected); + }, "WeakMap.prototype." + fun + ".length") +} + +function test_thisval(fun, args) { + // Step 1-2 + test(function() { + assert_throws_js(TypeError, function() { + WeakMap.prototype[fun].apply(null, args); + }); + assert_throws_js(TypeError, function() { + WeakMap.prototype[fun].apply(undefined, args); + }); + }, "WeakMap.prototype." + fun + ": ToObject on this") + // Step 3 + test(function() { + assert_throws_js(TypeError, function() { + WeakMap.prototype[fun].apply({}, args); + }); + }, "WeakMap.prototype." + fun + ": this has no [[WeakMapData]] internal property") +} + +// In every case, the length property of a built-in Function object described +// in this clause has the attributes { [[Writable]]: false, [[Enumerable]]: +// false, [[Configurable]]: false }. Every other property described in this +// clause has the attributes { [[Writable]]: true, [[Enumerable]]: false, +// [[Configurable]]: true } unless otherwise specified. + +test(function() { + assert_equals(Object.getPrototypeOf(WeakMap.prototype), Object.prototype); +}, "The value of the [[Prototype]] internal property of the WeakMap prototype object is the standard built-in Object prototype object (15.2.4).") + +// 23.3.3.1 WeakMap.prototype.constructor +test(function() { + assert_equals(WeakMap.prototype.constructor, WeakMap); + assert_propdesc(WeakMap.prototype, "constructor", true, false, true); +}, "The initial value of WeakMap.prototype.constructor is the built-in WeakMap constructor.") + +// 23.3.3.2 WeakMap.prototype.delete ( key ) +test(function() { + assert_propdesc(WeakMap.prototype, "delete", true, false, true); + test_length("delete", 1); + // Step 1-3 + test_thisval("delete", [{}]); +}, "WeakMap.prototype.delete") + +// 23.3.3.3 WeakMap.prototype.get ( key ) +test(function() { + assert_propdesc(WeakMap.prototype, "get", true, false, true); + test_length("get", 1); + // Step 1-3 + test_thisval("get", [{}]); + + // Step 8 + test(function() { + var wm = new WeakMap(); + var key = {}; + var res = wm.get({}, {}); + assert_equals(res, undefined); + }, "WeakMap.prototype.get: return undefined"); +}, "WeakMap.prototype.get") + +// 23.3.3.4 Map.prototype.has ( key ) +test(function() { + assert_propdesc(WeakMap.prototype, "has", true, false, true); + test_length("has", 1); + // Step 1-3 + test_thisval("has", [{}]); +}, "WeakMap.prototype.has") + +// 23.3.3.5 Map.prototype.set ( key , value ) +test(function() { + assert_propdesc(WeakMap.prototype, "set", true, false, true); + test_length("set", 2); + // Step 1-3 + test_thisval("set", [{}, {}]); +}, "WeakMap.prototype.set") + +// 23.3.3.6 Map.prototype.@@toStringTag +test(function() { + assert_class_string(new WeakMap(), "WeakMap"); + assert_class_string(WeakMap.prototype, "WeakMap"); +}, "WeakMap.prototype.@@toStringTag") +</script> diff --git a/testing/web-platform/tests/js/builtins/weakrefs/cleanup-prevented-with-reference.optional.any.js b/testing/web-platform/tests/js/builtins/weakrefs/cleanup-prevented-with-reference.optional.any.js new file mode 100644 index 0000000000..da902ac4f1 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/weakrefs/cleanup-prevented-with-reference.optional.any.js @@ -0,0 +1,51 @@ +// META: script=/common/gc.js +// META: script=resources/maybe-garbage-collect.js +// ├──> maybeGarbageCollectAndCleanupAsync +// └──> resolveGarbageCollection +/*--- +esid: sec-finalization-registry.prototype.cleanupSome +info: | + FinalizationRegistry.prototype.cleanupSome ( [ callback ] ) + + 1. Let finalizationRegistry be the this value. + 2. If Type(finalizationRegistry) is not Object, throw a TypeError exception. + 3. If finalizationRegistry does not have a [[Cells]] internal slot, throw a TypeError exception. + 4. If callback is not undefined and IsCallable(callback) is false, throw a TypeError exception. + 5. Perform ? CleanupFinalizationRegistry(finalizationRegistry, callback). + 6. Return undefined. +---*/ + +let holdingsList = []; +function cb(holding) { + holdingsList.push(holding); +}; +let finalizationRegistry = new FinalizationRegistry(function() {}); + +let referenced = {}; + +function emptyCells() { + let target = {}; + finalizationRegistry.register(target, 'target!'); + finalizationRegistry.register(referenced, 'referenced'); + + let prom = maybeGarbageCollectAndCleanupAsync(target); + target = null; + + return prom; +} + +promise_test(() => { + return (async () => { + assert_implements( + typeof FinalizationRegistry.prototype.cleanupSome === 'function', + 'FinalizationRegistry.prototype.cleanupSome is not implemented.' + ); + + await emptyCells(); + finalizationRegistry.cleanupSome(cb); + + assert_equals(holdingsList.length, 1); + assert_equals(holdingsList[0], 'target!'); + assert_equals(typeof referenced, 'object', 'referenced preserved'); + })().catch(resolveGarbageCollection); +}, 'cleanupCallback has only one optional chance to be called for a GC that cleans up a registered target.'); diff --git a/testing/web-platform/tests/js/builtins/weakrefs/cleanup-prevented-with-unregister.optional.any.js b/testing/web-platform/tests/js/builtins/weakrefs/cleanup-prevented-with-unregister.optional.any.js new file mode 100644 index 0000000000..640f21e7f3 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/weakrefs/cleanup-prevented-with-unregister.optional.any.js @@ -0,0 +1,58 @@ +// META: script=/common/gc.js +// META: script=resources/maybe-garbage-collect.js +// ├──> maybeGarbageCollectAndCleanupAsync +// └──> resolveGarbageCollection +/*--- +esid: sec-finalization-registry.prototype.cleanupSome +info: | + FinalizationRegistry.prototype.cleanupSome ( [ callback ] ) + + 1. Let finalizationRegistry be the this value. + 2. If Type(finalizationRegistry) is not Object, throw a TypeError exception. + 3. If finalizationRegistry does not have a [[Cells]] internal slot, throw a TypeError exception. + 4. If callback is not undefined and IsCallable(callback) is false, throw a TypeError exception. + 5. Perform ? CleanupFinalizationRegistry(finalizationRegistry, callback). + 6. Return undefined. + + FinalizationRegistry.prototype.unregister ( unregisterToken ) + + 1. Let removed be false. + 2. For each Record { [[Target]], [[Holdings]], [[UnregisterToken]] } cell that is an element of finalizationRegistry.[[Cells]], do + a. If SameValue(cell.[[UnregisterToken]], unregisterToken) is true, then + i. Remove cell from finalizationRegistry.[[Cells]]. + ii. Set removed to true. + 3. Return removed. +---*/ +let token = {}; +let finalizationRegistry = new FinalizationRegistry(function() {}); + +function emptyCells() { + let target = {}; + finalizationRegistry.register(target, 'target!', token); + + let prom = maybeGarbageCollectAndCleanupAsync(target); + target = null; + + return prom; +} + +promise_test(() => { + return (async () => { + assert_implements( + typeof FinalizationRegistry.prototype.cleanupSome === 'function', + 'FinalizationRegistry.prototype.cleanupSome is not implemented.' + ); + + await emptyCells(); + let called = 0; + + let res = finalizationRegistry.unregister(token); + assert_equals(res, true, 'unregister target before iterating over it in cleanup'); + + finalizationRegistry.cleanupSome((holding) => { + called += 1; + }); + + assert_equals(called, 0, 'callback was not called'); + })().catch(resolveGarbageCollection); +}, 'Cleanup might be prevented with an unregister usage'); diff --git a/testing/web-platform/tests/js/builtins/weakrefs/finalizationregistry-cleanupCallback-gets-a-microtask.optional.any.js b/testing/web-platform/tests/js/builtins/weakrefs/finalizationregistry-cleanupCallback-gets-a-microtask.optional.any.js new file mode 100644 index 0000000000..c3a84418f3 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/weakrefs/finalizationregistry-cleanupCallback-gets-a-microtask.optional.any.js @@ -0,0 +1,61 @@ +// META: script=/common/gc.js +// META: script=resources/maybe-garbage-collect.js +// ├──> maybeGarbageCollectAsync +// └──> resolveGarbageCollection +/*--- +esid: sec-finalization-registry-target +info: | + FinalizationRegistry ( cleanupCallback ) + + Execution + At any time, if a set of objects S is not live, an ECMAScript implementation may perform the + following steps atomically: + + For each obj of S, do + For each WeakRef ref such that ref.[[WeakRefTarget]] is obj, do + Set ref.[[WeakRefTarget]] to empty. + For each FinalizationRegistry fg such that fg.[[Cells]] contains cell, such that + cell.[[WeakRefTarget]] is obj, + Set cell.[[WeakRefTarget]] to empty. + Optionally, perform ! HostCleanupFinalizationRegistry(fg). + + HostCleanupFinalizationRegistry(finalizationRegistry) + + HostCleanupFinalizationRegistry is an implementation-defined abstract operation that is expected + to call CleanupFinalizationRegistry(finalizationRegistry) at some point in the future, if + possible. The host's responsibility is to make this call at a time which does not interrupt + synchronous ECMAScript code execution. +---*/ + +let count = 1_000; +let calls = 0; +let registries = []; +let callback = function() { + calls++; +}; +for (let i = 0; i < count; i++) { + registries.push( + new FinalizationRegistry(callback) + ); +} +setup({ allow_uncaught_exception: true }); + +promise_test((test) => { + assert_implements( + typeof FinalizationRegistry.prototype.register === 'function', + 'FinalizationRegistry.prototype.register is not implemented.' + ); + return (async () => { + + { + let target = {}; + for (let registry of registries) { + registry.register(target, 1); + } + target = null; + } + + await maybeGarbageCollectAsync(); + await test.step_wait(() => calls === count, `Expected ${count} registry cleanups.`); + })().catch(resolveGarbageCollection); +}, 'HostCleanupFinalizationRegistry is an implementation-defined abstract operation that is expected to call CleanupFinalizationRegistry(finalizationRegistry) at some point in the future, if possible.'); diff --git a/testing/web-platform/tests/js/builtins/weakrefs/finalizationregistry-cleanupCallback-throws-onerror-interaction.optional.window.js b/testing/web-platform/tests/js/builtins/weakrefs/finalizationregistry-cleanupCallback-throws-onerror-interaction.optional.window.js new file mode 100644 index 0000000000..71d6fbe174 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/weakrefs/finalizationregistry-cleanupCallback-throws-onerror-interaction.optional.window.js @@ -0,0 +1,70 @@ +// META: script=/common/gc.js +// META: script=resources/maybe-garbage-collect.js +// ├──> maybeGarbageCollectAsync +// └──> resolveGarbageCollection +/*--- +esid: sec-finalization-registry-target +info: | + FinalizationRegistry ( cleanupCallback ) + + CleanupFinalizationRegistry ( finalizationRegistry [ , callback ] ) + + The following steps are performed: + + Assert: finalizationRegistry has [[Cells]] and [[CleanupCallback]] internal slots. + If callback is not present or undefined, set callback to finalizationRegistry.[[CleanupCallback]]. + While finalizationRegistry.[[Cells]] contains a Record cell such that cell.[[WeakRefTarget]] is + empty, then an implementation may perform the following steps, + Choose any such cell. + Remove cell from finalizationRegistry.[[Cells]]. + Perform ? Call(callback, undefined, « cell.[[HeldValue]] »). + Return NormalCompletion(undefined). + + EDITOR'S NOTE + When called from HostCleanupFinalizationRegistry, if calling the callback throws an error, this will be caught within the RunJobs algorithm and reported to the host. HTML does not apply the RunJobs algorithm, but will also report the error, which may call window.onerror. +---*/ + +let error = new Error('FinalizationRegistryError'); + +let finalizationRegistry = new FinalizationRegistry(function() { + throw error; +}); + +setup({ allow_uncaught_exception: true }); + +promise_test((test) => { + assert_implements( + typeof FinalizationRegistry.prototype.register === 'function', + 'FinalizationRegistry.prototype.register is not implemented.' + ); + + return (async () => { + + let resolve; + let reject; + let deferred = new Promise((resolverFn, rejecterFn) => { + resolve = resolverFn; + reject = rejecterFn; + }); + + window.onerror = test.step_func((message, source, lineno, colno, exception) => { + assert_equals(exception, error, 'window.onerror received the intended error object.'); + resolve(); + }); + + { + let target = {}; + let heldValue = 1; + finalizationRegistry.register(target, heldValue); + target = null; + } + + await maybeGarbageCollectAsync(); + + // Since the process of garbage collection is non-deterministic, we cannot know when + // (if ever) it will actually occur. + test.step_timeout(() => { reject(); }, 5000); + + return deferred; + })().catch(resolveGarbageCollection); +}, 'When called from HostCleanupFinalizationRegistry, if calling the callback throws an error, this will be caught within the RunJobs algorithm and reported to the host. HTML does not apply the RunJobs algorithm, but will also report the error, which may call window.onerror.'); diff --git a/testing/web-platform/tests/js/builtins/weakrefs/gc-has-one-chance-to-call-cleanupCallback-queueMicrotask.optional.any.js b/testing/web-platform/tests/js/builtins/weakrefs/gc-has-one-chance-to-call-cleanupCallback-queueMicrotask.optional.any.js new file mode 100644 index 0000000000..456281c520 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/weakrefs/gc-has-one-chance-to-call-cleanupCallback-queueMicrotask.optional.any.js @@ -0,0 +1,109 @@ +// META: script=/common/gc.js +// META: script=resources/maybe-garbage-collect.js +// ├──> maybeGarbageCollectAndCleanupAsync +// ├──> maybeGarbageCollectAsync +// └──> resolveGarbageCollection +/*--- +esid: sec-finalization-registry-target +info: | + FinalizationRegistry ( cleanupCallback ) + + FinalizationRegistry.prototype.cleanupSome ( [ callback ] ) + + ... + 4. If callback is not undefined and IsCallable(callback) is false, throw a TypeError exception. + 5. Perform ? CleanupFinalizationRegistry(finalizationRegistry, callback). + 6. Return undefined. + + Execution + + At any time, if an object obj is not live, an ECMAScript implementation may perform the following steps atomically: + + 1. For each WeakRef ref such that ref.[[Target]] is obj, + a. Set ref.[[Target]] to empty. + 2. For each FinalizationRegistry finalizationRegistry such that finalizationRegistry.[[Cells]] contains cell, such that cell.[[Target]] is obj, + a. Set cell.[[Target]] to empty. + b. Optionally, perform ! HostCleanupFinalizationRegistry(finalizationRegistry). +---*/ + + +let cleanupCallback = 0; +let holdings = []; +function cb(holding) { + holdings.push(holding); +} + +let finalizationRegistry = new FinalizationRegistry(function() { + cleanupCallback += 1; +}); + +function emptyCells() { + let target = {}; + finalizationRegistry.register(target, 'a'); + + let prom = maybeGarbageCollectAndCleanupAsync(target); + target = null; + + return prom; +} + +promise_test(() => { + return (async () => { + assert_implements( + typeof FinalizationRegistry.prototype.cleanupSome === 'function', + 'FinalizationRegistry.prototype.cleanupSome is not implemented.' + ); + + assert_implements( + typeof queueMicrotask === 'function', + 'queueMicrotask is not implemented.' + ); + + let ticks = 0; + await emptyCells(); + await queueMicrotask(() => ticks++); + + finalizationRegistry.cleanupSome(cb); + + // cleanupSome will be invoked if there are empty cells left. If the + // cleanupCallback already ran, then cb won't be called. + let expectedCalled = cleanupCallback === 1 ? 0 : 1; + // This asserts the registered object was emptied in the previous GC. + assert_equals(holdings.length, expectedCalled, 'cleanupSome callback for the first time'); + + // At this point, we can't assert if cleanupCallback was called, because it's + // optional. Although, we can finally assert it's not gonna be called anymore + // for the other executions of the Garbage Collector. + // The chance of having it called only happens right after the + // cell.[[Target]] is set to empty. + assert_true(cleanupCallback >= 0, 'cleanupCallback might be 0'); + assert_true(cleanupCallback <= 1, 'cleanupCallback might be 1'); + + // Restoring the cleanupCallback variable to 0 will help us asserting the + // finalizationRegistry callback is not called again. + cleanupCallback = 0; + + await maybeGarbageCollectAsync(); + await queueMicrotask(() => ticks++); + + finalizationRegistry.cleanupSome(cb); + + assert_equals(holdings.length, expectedCalled, 'cleanupSome callback is not called anymore, no empty cells'); + assert_equals(cleanupCallback, 0, 'cleanupCallback is not called again #1'); + + await maybeGarbageCollectAsync(); + await queueMicrotask(() => ticks++); + + finalizationRegistry.cleanupSome(cb); + + assert_equals(holdings.length, expectedCalled, 'cleanupSome callback is not called again #2'); + assert_equals(cleanupCallback, 0, 'cleanupCallback is not called again #2'); + assert_equals(ticks, 3, 'ticks is 3'); + + if (holdings.length) { + assert_array_equals(holdings, ['a']); + } + + await maybeGarbageCollectAsync(); + })().catch(resolveGarbageCollection); +}, 'cleanupCallback has only one optional chance to be called for a GC that cleans up a registered target.'); diff --git a/testing/web-platform/tests/js/builtins/weakrefs/gc-has-one-chance-to-call-cleanupCallback-queueMicrotaskMutationObserver.optional.window.js b/testing/web-platform/tests/js/builtins/weakrefs/gc-has-one-chance-to-call-cleanupCallback-queueMicrotaskMutationObserver.optional.window.js new file mode 100644 index 0000000000..7b29f00943 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/weakrefs/gc-has-one-chance-to-call-cleanupCallback-queueMicrotaskMutationObserver.optional.window.js @@ -0,0 +1,115 @@ +// META: script=/common/gc.js +// META: script=resources/maybe-garbage-collect.js +// ├──> maybeGarbageCollectAndCleanupAsync +// ├──> maybeGarbageCollectAsync +// └──> resolveGarbageCollection +/*--- +esid: sec-finalization-registry-target +info: | + FinalizationRegistry ( cleanupCallback ) + + FinalizationRegistry.prototype.cleanupSome ( [ callback ] ) + + ... + 4. If callback is not undefined and IsCallable(callback) is false, throw a TypeError exception. + 5. Perform ? CleanupFinalizationRegistry(finalizationRegistry, callback). + 6. Return undefined. + + Execution + + At any time, if an object obj is not live, an ECMAScript implementation may perform the following steps atomically: + + 1. For each WeakRef ref such that ref.[[Target]] is obj, + a. Set ref.[[Target]] to empty. + 2. For each FinalizationRegistry finalizationRegistry such that finalizationRegistry.[[Cells]] contains cell, such that cell.[[Target]] is obj, + a. Set cell.[[Target]] to empty. + b. Optionally, perform ! HostCleanupFinalizationRegistry(finalizationRegistry). +---*/ + + +let cleanupCallback = 0; +let holdings = []; +function cb(holding) { + holdings.push(holding); +} + +let finalizationRegistry = new FinalizationRegistry(function() { + cleanupCallback += 1; +}); + +function emptyCells() { + let target = {}; + finalizationRegistry.register(target, 'a'); + + let prom = maybeGarbageCollectAndCleanupAsync(target); + target = null; + + return prom; +} + +function queueMicrotaskByMutationObserver(callback) { + const textNode = document.createTextNode(''); + new MutationObserver(callback).observe(textNode, { characterData: true }); + textNode.data = 1; +} + +promise_test(() => { + return (async () => { + assert_implements( + typeof FinalizationRegistry.prototype.cleanupSome === 'function', + 'FinalizationRegistry.prototype.cleanupSome is not implemented.' + ); + + assert_implements( + typeof MutationObserver === 'function', + 'MutationObserver is not implemented.' + ); + + let ticks = 0; + await emptyCells(); + await queueMicrotaskByMutationObserver(() => ticks++); + + finalizationRegistry.cleanupSome(cb); + + // cleanupSome will be invoked if there are empty cells left. If the + // cleanupCallback already ran, then cb won't be called. + let expectedCalled = cleanupCallback === 1 ? 0 : 1; + // This asserts the registered object was emptied in the previous GC. + assert_equals(holdings.length, expectedCalled, 'cleanupSome callback for the first time'); + + // At this point, we can't assert if cleanupCallback was called, because it's + // optional. Although, we can finally assert it's not gonna be called anymore + // for the other executions of the Garbage Collector. + // The chance of having it called only happens right after the + // cell.[[Target]] is set to empty. + assert_true(cleanupCallback >= 0, 'cleanupCallback might be 0'); + assert_true(cleanupCallback <= 1, 'cleanupCallback might be 1'); + + // Restoring the cleanupCallback variable to 0 will help us asserting the + // finalizationRegistry callback is not called again. + cleanupCallback = 0; + + await maybeGarbageCollectAsync(); + await queueMicrotaskByMutationObserver(() => ticks++); + + finalizationRegistry.cleanupSome(cb); + + assert_equals(holdings.length, expectedCalled, 'cleanupSome callback is not called anymore, no empty cells'); + assert_equals(cleanupCallback, 0, 'cleanupCallback is not called again #1'); + + await maybeGarbageCollectAsync(); + await queueMicrotaskByMutationObserver(() => ticks++); + + finalizationRegistry.cleanupSome(cb); + + assert_equals(holdings.length, expectedCalled, 'cleanupSome callback is not called again #2'); + assert_equals(cleanupCallback, 0, 'cleanupCallback is not called again #2'); + assert_equals(ticks, 3, 'ticks is 3'); + + if (holdings.length) { + assert_array_equals(holdings, ['a']); + } + + await maybeGarbageCollectAsync(); + })().catch(resolveGarbageCollection); +}, 'cleanupCallback has only one optional chance to be called for a GC that cleans up a registered target.'); diff --git a/testing/web-platform/tests/js/builtins/weakrefs/gc-has-one-chance-to-call-cleanupCallback.optional.any.js b/testing/web-platform/tests/js/builtins/weakrefs/gc-has-one-chance-to-call-cleanupCallback.optional.any.js new file mode 100644 index 0000000000..92cd322869 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/weakrefs/gc-has-one-chance-to-call-cleanupCallback.optional.any.js @@ -0,0 +1,104 @@ +// META: script=/common/gc.js +// META: script=resources/maybe-garbage-collect.js +// ├──> maybeGarbageCollectAndCleanupAsync +// ├──> maybeGarbageCollectAsync +// └──> resolveGarbageCollection +/*--- +esid: sec-finalization-registry-target +info: | + FinalizationRegistry ( cleanupCallback ) + + FinalizationRegistry.prototype.cleanupSome ( [ callback ] ) + + ... + 4. If callback is not undefined and IsCallable(callback) is false, throw a TypeError exception. + 5. Perform ? CleanupFinalizationRegistry(finalizationRegistry, callback). + 6. Return undefined. + + Execution + + At any time, if an object obj is not live, an ECMAScript implementation may perform the following steps atomically: + + 1. For each WeakRef ref such that ref.[[Target]] is obj, + a. Set ref.[[Target]] to empty. + 2. For each FinalizationRegistry finalizationRegistry such that finalizationRegistry.[[Cells]] contains cell, such that cell.[[Target]] is obj, + a. Set cell.[[Target]] to empty. + b. Optionally, perform ! HostCleanupFinalizationRegistry(finalizationRegistry). +---*/ + + +let cleanupCallback = 0; +let holdings = []; +function cb(holding) { + holdings.push(holding); +} + +let finalizationRegistry = new FinalizationRegistry(function() { + cleanupCallback += 1; +}); + +function emptyCells() { + let target = {}; + finalizationRegistry.register(target, 'a'); + + let prom = maybeGarbageCollectAndCleanupAsync(target); + target = null; + + return prom; +} + +promise_test(() => { + return (async () => { + assert_implements( + typeof FinalizationRegistry.prototype.cleanupSome === 'function', + 'FinalizationRegistry.prototype.cleanupSome is not implemented.' + ); + + let ticks = 0; + await emptyCells(); + await ticks++; + + finalizationRegistry.cleanupSome(cb); + + // cleanupSome will be invoked if there are empty cells left. If the + // cleanupCallback already ran, then cb won't be called. + let expectedCalled = cleanupCallback === 1 ? 0 : 1; + // This asserts the registered object was emptied in the previous GC. + assert_equals(holdings.length, expectedCalled, 'cleanupSome callback for the first time'); + + // At this point, we can't assert if cleanupCallback was called, because it's + // optional. Although, we can finally assert it's not gonna be called anymore + // for the other executions of the Garbage Collector. + // The chance of having it called only happens right after the + // cell.[[Target]] is set to empty. + assert_true(cleanupCallback >= 0, 'cleanupCallback might be 0'); + assert_true(cleanupCallback <= 1, 'cleanupCallback might be 1'); + + // Restoring the cleanupCallback variable to 0 will help us asserting the + // finalizationRegistry callback is not called again. + cleanupCallback = 0; + + await maybeGarbageCollectAsync(); + await ticks++; + + finalizationRegistry.cleanupSome(cb); + + assert_equals(holdings.length, expectedCalled, 'cleanupSome callback is not called anymore, no empty cells'); + assert_equals(cleanupCallback, 0, 'cleanupCallback is not called again #1'); + + await maybeGarbageCollectAsync(); + await ticks++; + + finalizationRegistry.cleanupSome(cb); + + assert_equals(holdings.length, expectedCalled, 'cleanupSome callback is not called again #2'); + assert_equals(cleanupCallback, 0, 'cleanupCallback is not called again #2'); + assert_equals(ticks, 3, 'ticks is 3'); + + if (holdings.length) { + assert_array_equals(holdings, ['a']); + } + + await maybeGarbageCollectAsync(); + })().catch(resolveGarbageCollection); +}, 'cleanupCallback has only one optional chance to be called for a GC that cleans up a registered target.'); diff --git a/testing/web-platform/tests/js/builtins/weakrefs/holdings-multiple-values.optional.any.js b/testing/web-platform/tests/js/builtins/weakrefs/holdings-multiple-values.optional.any.js new file mode 100644 index 0000000000..604174e467 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/weakrefs/holdings-multiple-values.optional.any.js @@ -0,0 +1,66 @@ +// META: script=/common/gc.js +// META: script=resources/maybe-garbage-collect.js +// ├──> maybeGarbageCollectAndCleanupAsync +// └──> resolveGarbageCollection +/*--- +esid: sec-properties-of-the-finalization-registry-constructor +info: | + FinalizationRegistry.prototype.cleanupSome ( [ callback ] ) + + 1. Let finalizationRegistry be the this value. + ... + 5. Perform ! CleanupFinalizationRegistry(finalizationRegistry, callback). + ... + + CleanupFinalizationRegistry ( finalizationRegistry [ , callback ] ) + + ... + 3. While finalizationRegistry.[[Cells]] contains a Record cell such that cell.[[WeakRefTarget]] is ~empty~, then an implementation may perform the following steps, + a. Choose any such cell. + b. Remove cell from finalizationRegistry.[[Cells]]. + c. Perform ? Call(callback, undefined, << cell.[[HeldValue]] >>). + +---*/ + +function check(value, expectedName) { + let holdings = []; + let called = 0; + let finalizationRegistry = new FinalizationRegistry(function() {}); + + function callback(holding) { + called += 1; + holdings.push(holding); + } + + // This is internal to avoid conflicts + function emptyCells(value) { + let target = {}; + finalizationRegistry.register(target, value); + + let prom = maybeGarbageCollectAndCleanupAsync(target); + target = null; + + return prom; + } + + return emptyCells(value).then(function() { + finalizationRegistry.cleanupSome(callback); + assert_equals(called, 1, expectedName); + assert_equals(holdings.length, 1, expectedName); + assert_equals(holdings[0], value, expectedName); + }); +} + +test(() => + assert_implements( + typeof FinalizationRegistry.prototype.cleanupSome === 'function', + 'FinalizationRegistry.prototype.cleanupSome is not implemented.' +), 'Requires FinalizationRegistry.prototype.cleanupSome'); +promise_test(() => check(undefined, 'undefined'), '`undefined` as registered holding value'); +promise_test(() => check(null, 'null'), '`null` as registered holding value'); +promise_test(() => check('', 'the empty string'), '`""` as registered holding value'); +promise_test(() => check({}, 'object'), '`{}` as registered holding value'); +promise_test(() => check(42, 'number'), '`42` as registered holding value'); +promise_test(() => check(true, 'true'), '`true` as registered holding value'); +promise_test(() => check(false, 'false'), '`false` as registered holding value'); +promise_test(() => check(Symbol(1), 'symbol'), '`Symbol(1)` as registered holding value'); diff --git a/testing/web-platform/tests/js/builtins/weakrefs/reentrancy.optional.any.js b/testing/web-platform/tests/js/builtins/weakrefs/reentrancy.optional.any.js new file mode 100644 index 0000000000..fa7b7d55c4 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/weakrefs/reentrancy.optional.any.js @@ -0,0 +1,51 @@ +// META: script=/common/gc.js +// META: script=resources/maybe-garbage-collect.js +// ├──> maybeGarbageCollectAndCleanupAsync +// └──> resolveGarbageCollection +/*--- +esid: sec-properties-of-the-finalization-registry-constructor +---*/ + +let called = 0; +let endOfCall = 0; +let finalizationRegistry = new FinalizationRegistry(function() {}); + +function callback(holding) { + called += 1; + + if (called === 1) { + // Atempt to re-enter the callback. + let nestedCallbackRan = false; + finalizationRegistry.cleanupSome(() => { nestedCallbackRan = true }); + assert_equals(nestedCallbackRan, true); + } + + endOfCall += 1; +} + +function emptyCells() { + let o1 = {}; + let o2 = {}; + // Register more than one objects to test reentrancy. + finalizationRegistry.register(o1, 'holdings 1'); + finalizationRegistry.register(o2, 'holdings 2'); + + let prom = maybeGarbageCollectAndCleanupAsync(o1); + o1 = null; + + return prom; +} + +promise_test(() => { + return (async () => { + assert_implements( + typeof FinalizationRegistry.prototype.cleanupSome === 'function', + 'FinalizationRegistry.prototype.cleanupSome is not implemented.' + ); + await emptyCells(); + finalizationRegistry.cleanupSome(callback); + + assert_equals(called, 1, 'callback was called'); + assert_equals(endOfCall, 1, 'callback finished'); + })().catch(resolveGarbageCollection); +}, 'cleanupCallback has only one optional chance to be called for a GC that cleans up a registered target.'); diff --git a/testing/web-platform/tests/js/builtins/weakrefs/resources/maybe-garbage-collect.js b/testing/web-platform/tests/js/builtins/weakrefs/resources/maybe-garbage-collect.js new file mode 100644 index 0000000000..8bd4cac309 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/weakrefs/resources/maybe-garbage-collect.js @@ -0,0 +1,72 @@ +/** + * maybeGarbageCollectAsync + * + * It might garbage collect, it might not. If it doesn't, that's ok. + */ +self.maybeGarbageCollectAsync = garbageCollect; + +/** + * maybeGarbageCollectKeptObjectsAsync + * + * Based on "asyncGCDeref" in https://github.com/tc39/test262/blob/master/harness/async-gc.js + * + * @return {Promise} Resolves to a trigger if ClearKeptObjects + * exists to provide one + */ +async function maybeGarbageCollectKeptObjectsAsync() { + let trigger; + + if (typeof ClearKeptObjects === 'function') { + trigger = ClearKeptObjects(); + } + + await maybeGarbageCollectAsync(); + + return trigger; +} + +/** + * maybeGarbageCollectAndCleanupAsync + * + * Based on "asyncGC" in https://github.com/tc39/test262/blob/master/harness/async-gc.js + * + * @return {undefined} + */ +async function maybeGarbageCollectAndCleanupAsync(...targets) { + let finalizationRegistry = new FinalizationRegistry(() => {}); + let length = targets.length; + + for (let target of targets) { + finalizationRegistry.register(target, 'target'); + target = null; + } + + targets = null; + + await 'tick'; + await maybeGarbageCollectKeptObjectsAsync(); + + let names = []; + + finalizationRegistry.cleanupSome(name => names.push(name)); + + if (names.length !== length) { + throw maybeGarbageCollectAndCleanupAsync.NOT_COLLECTED; + } +} + +maybeGarbageCollectAndCleanupAsync.NOT_COLLECTED = Symbol('Object was not collected'); + +/** + * resolveGarbageCollection + * + * Based on "resolveAsyncGC" in https://github.com/tc39/test262/blob/master/harness/async-gc.js + * + * @param {Error} error An error object. + * @return {undefined} + */ +function resolveGarbageCollection(error) { + if (error && error !== maybeGarbageCollectAndCleanupAsync.NOT_COLLECTED) { + throw error; + } +} diff --git a/testing/web-platform/tests/js/builtins/weakrefs/return-undefined-with-gc.optional.any.js b/testing/web-platform/tests/js/builtins/weakrefs/return-undefined-with-gc.optional.any.js new file mode 100644 index 0000000000..a5d23bf696 --- /dev/null +++ b/testing/web-platform/tests/js/builtins/weakrefs/return-undefined-with-gc.optional.any.js @@ -0,0 +1,79 @@ +// META: script=/common/gc.js +// META: script=resources/maybe-garbage-collect.js +// ├──> maybeGarbageCollectAndCleanupAsync +// └──> resolveGarbageCollection +/*--- +esid: sec-finalization-registry.prototype.cleanupSome +info: | + FinalizationRegistry.prototype.cleanupSome ( [ callback ] ) + + 1. Let finalizationRegistry be the this value. + 2. If Type(finalizationRegistry) is not Object, throw a TypeError exception. + 3. If finalizationRegistry does not have a [[Cells]] internal slot, throw a TypeError exception. + 4. If callback is not undefined and IsCallable(callback) is false, throw a TypeError exception. + 5. Perform ? CleanupFinalizationRegistry(finalizationRegistry, callback). + 6. Return undefined. +---*/ + +let called; +let fn = function() { + called += 1; + return 39; +}; +let cb = function() { + called += 1; + return 42; +}; +let finalizationRegistry = new FinalizationRegistry(fn); + +function emptyCells() { + let target = {}; + finalizationRegistry.register(target); + + let prom = maybeGarbageCollectAndCleanupAsync(target); + target = null; + + return prom; +} + +promise_test(() => { + return (async () => { + assert_implements( + typeof FinalizationRegistry.prototype.cleanupSome === 'function', + 'FinalizationRegistry.prototype.cleanupSome is not implemented.' + ); + + await emptyCells(); + called = 0; + assert_equals(finalizationRegistry.cleanupSome(cb), undefined, 'regular callback'); + assert_equals(called, 1); + + await emptyCells(); + called = 0; + assert_equals(finalizationRegistry.cleanupSome(fn), undefined, 'regular callback, same FG cleanup function'); + assert_equals(called, 1); + + await emptyCells(); + called = 0; + assert_equals(finalizationRegistry.cleanupSome(), undefined, 'undefined (implicit) callback, defer to FB callback'); + assert_equals(called, 1); + + await emptyCells(); + called = 0; + assert_equals(finalizationRegistry.cleanupSome(undefined), undefined, 'undefined (explicit) callback, defer to FB callback'); + assert_equals(called, 1); + + await emptyCells(); + assert_equals(finalizationRegistry.cleanupSome(() => 1), undefined, 'arrow function'); + + await emptyCells(); + assert_equals(finalizationRegistry.cleanupSome(async function() {}), undefined, 'async function'); + + await emptyCells(); + assert_equals(finalizationRegistry.cleanupSome(function *() {}), undefined, 'generator'); + + await emptyCells(); + assert_equals(finalizationRegistry.cleanupSome(async function *() {}), undefined, 'async generator'); + + })().catch(resolveGarbageCollection); +}, 'Return undefined regardless the result of CleanupFinalizationRegistry'); diff --git a/testing/web-platform/tests/js/builtins/weakrefs/unregister-cleaned-up-cell.optional.any.js b/testing/web-platform/tests/js/builtins/weakrefs/unregister-cleaned-up-cell.optional.any.js new file mode 100644 index 0000000000..920ea7619c --- /dev/null +++ b/testing/web-platform/tests/js/builtins/weakrefs/unregister-cleaned-up-cell.optional.any.js @@ -0,0 +1,73 @@ +// META: script=/common/gc.js +// META: script=resources/maybe-garbage-collect.js +// ├──> maybeGarbageCollectAndCleanupAsync +// └──> resolveGarbageCollection +/*--- +esid: sec-finalization-registry.prototype.unregister +info: | + FinalizationRegistry.prototype.cleanupSome ( [ callback ] ) + + 1. Let finalizationRegistry be the this value. + ... + 5. Perform ! CleanupFinalizationRegistry(finalizationRegistry, callback). + ... + + CleanupFinalizationRegistry ( finalizationRegistry [ , callback ] ) + + ... + 3. While finalizationRegistry.[[Cells]] contains a Record cell such that cell.[[WeakRefTarget]] is ~empty~, then an implementation may perform the following steps, + a. Choose any such cell. + b. Remove cell from finalizationRegistry.[[Cells]]. + c. Perform ? Call(callback, undefined, << cell.[[HeldValue]] >>). + ... + + FinalizationRegistry.prototype.unregister ( unregisterToken ) + + 1. Let removed be false. + 2. For each Record { [[Target]], [[Holdings]], [[UnregisterToken]] } cell that is an element of finalizationRegistry.[[Cells]], do + a. If SameValue(cell.[[UnregisterToken]], unregisterToken) is true, then + i. Remove cell from finalizationRegistry.[[Cells]]. + ii. Set removed to true. + 3. Return removed. + +---*/ + +let value = 'target!'; +let token = {}; +let finalizationRegistry = new FinalizationRegistry(function() {}); + +function emptyCells() { + let target = {}; + finalizationRegistry.register(target, value, token); + + let prom = maybeGarbageCollectAndCleanupAsync(target); + target = null; + + return prom; +} + +promise_test(() => { + return (async () => { + assert_implements( + typeof FinalizationRegistry.prototype.cleanupSome === 'function', + 'FinalizationRegistry.prototype.cleanupSome is not implemented.' + ); + + await emptyCells(); + let called = 0; + let holdings = []; + finalizationRegistry.cleanupSome((holding) => { + called += 1; + holdings.push(holding); + }); + + assert_equals(called, 1); + assert_equals(holdings.length, 1); + assert_equals(holdings[0], value); + + let res = finalizationRegistry.unregister(token); + assert_equals(res, false, 'unregister after iterating over it in cleanup'); + + })().catch(resolveGarbageCollection); +}, 'Cannot unregister a cell that has been cleaned up'); + |