| //------------------------------------------------------------------------------------------------------- |
| // Copyright (C) Microsoft. All rights reserved. |
| // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. |
| //------------------------------------------------------------------------------------------------------- |
| |
| /// <reference path="protolib.js" /> |
| if (this.WScript && this.WScript.LoadScriptFile) { |
| WScript.LoadScriptFile("protolib.js"); |
| } |
| |
| function printProtoChain(obj) { |
| var known = [ |
| Number.prototype, Boolean.prototype, String.prototype, Object.prototype, Array.prototype, Function.prototype, |
| ]; |
| var names = [ |
| "Number.prototype", "Boolean.prototype", "String.prototype", "Object.prototype", "Array.prototype", "Function.prototype", |
| ]; |
| |
| var s = ""; |
| while (obj) { |
| var i = known.indexOf(obj); |
| var name = (i >= 0 ? names[i] : JSON.stringify(obj)); |
| if (s == "") { |
| s = name; |
| } else { |
| s += " -> " + name; |
| } |
| |
| obj = Object.getPrototypeOf(obj); |
| } |
| |
| helpers.writeln(s); |
| } |
| |
| function make_point(_x, _y) { |
| return { x: _x, y: _y }; |
| } |
| |
| var tests; |
| if (helpers.isVersion10OrLater) { // TODO: Change to _11_OrLater |
| tests = [ |
| { |
| name: "Test Object.prototype.__proto__ attributes", |
| body: function () { |
| var desc = Object.getOwnPropertyDescriptor(Object.prototype, "__proto__"); |
| helpers.printObject(desc); |
| } |
| }, |
| |
| { |
| name: "Read built-in __proto__", |
| body: function () { |
| assert.isTrue(12["__proto__"] === Number.prototype, "int __proto__"); |
| assert.isTrue(12.3["__proto__"] === Number.prototype, "float __proto__"); |
| assert.isTrue((new Number(12)).__proto__ === Number.prototype, "Number Object __proto__"); |
| |
| assert.isTrue(true.__proto__ === Boolean.prototype, "Boolean Value __proto__"); |
| assert.isTrue(false.__proto__ === Boolean.prototype, "Boolean Value __proto__"); |
| assert.isTrue((new Boolean(true)).__proto__ === Boolean.prototype, "Boolean Object __proto__"); |
| assert.isTrue((new Boolean(false)).__proto__ === Boolean.prototype, "Boolean Object __proto__"); |
| |
| assert.isTrue("hello".__proto__ === String.prototype, "String Value __proto__"); |
| assert.isTrue(new String("hello").__proto__ === String.prototype, "String Object __proto__"); |
| |
| assert.isTrue({}.__proto__ === Object.prototype, "{} __proto__"); |
| assert.isTrue([].__proto__ === Array.prototype, "[] __proto__"); |
| |
| assert.isTrue(Array.prototype.__proto__ === Object.prototype, "Array.prototype.__proto__"); |
| assert.isTrue(Object.prototype.__proto__ === null, "Object.prototype.__proto__"); |
| |
| assert.isTrue(Array.__proto__ === Function.prototype, "Array.__proto__"); |
| assert.isTrue(Function.__proto__ === Function.prototype, "Function.__proto__"); |
| assert.isTrue(Function.prototype.__proto__ === Object.prototype, "Function.prototype.__proto__"); |
| |
| var o = { a: 0 }; |
| assert.isTrue(o.__proto__ === Object.prototype, "o.__proto__"); |
| assert.isTrue(o.__proto__.__proto__ === null, "o.__proto__.__proto__"); |
| } |
| }, |
| |
| { |
| name: "Change built-in's __proto__ chain", |
| body: function () { |
| function changeProtoAndTest(obj, newProto, test) { |
| var oldProto = obj.__proto__; |
| obj.__proto__ = newProto; |
| printProtoChain(obj); |
| try { |
| test(); |
| } finally { |
| obj.__proto__ = oldProto; // Restore old __proto__ |
| } |
| } |
| |
| // Number doesn't have sort method, should throw |
| assert.throws(function () { 12["sort"]() }, TypeError, "Object doesn't support property or method 'sort'"); |
| |
| // Number gets Array methods from new prototype in the chain |
| changeProtoAndTest(Number.prototype, Array.prototype, function () { |
| 12["sort"](); |
| |
| var o = new Number(34); |
| o[0] = 8; |
| o[1] = 3; |
| o[2] = 9; |
| o[3] = 5; |
| o.length = 4; |
| assert.areEqual("8 3 9 5", o.join(" ")); |
| o.sort(); |
| assert.areEqual("3 5 8 9", o.join(" ")); |
| }); |
| |
| // Boolean doesn't have String methods |
| assert.throws(function () { true.toUpperCase() }, TypeError, "Object doesn't support property or method 'toUpperCase'"); |
| |
| // Boolean gets String methods from new prototype in the chain |
| changeProtoAndTest(Boolean.prototype, new String("abc"), function () { |
| assert.areEqual("TRUE", true.toUpperCase()); |
| assert.areEqual("FALSE", false.toUpperCase()); |
| assert.areEqual("TRUE", (new Boolean(true)).toUpperCase()); |
| assert.areEqual("FALSE", (new Boolean(false)).toUpperCase()); |
| }); |
| } |
| }, |
| |
| { |
| name: "Change __proto__ to null/undefined", |
| body: function () { |
| var a = {}; |
| |
| // Set an object's prototype to undefined |
| { |
| var a = {}; |
| |
| assert.isTrue(a.__proto__ === Object.prototype); |
| assert.isTrue(!a.isPrototypeOf(a)); |
| |
| a.__proto__ = undefined; |
| |
| assert.isTrue(a.__proto__ === Object.prototype); |
| assert.isTrue(!a.isPrototypeOf(a)); |
| |
| assert.throws__proto__ArgNotObjectOrNull(function () { Object.setPrototypeOf(a, undefined); }, "Object.setPrototypeOf"); |
| } |
| |
| // Set an object's prototype to null |
| function f1(O, P) { O.__proto__ = P; } |
| function f2(O, P) { Object.setPrototypeOf(O, P); } |
| [f1, f2].forEach(function (f) { |
| var a = {}; |
| |
| assert.isTrue(a.__proto__ === Object.prototype); |
| assert.isTrue(!a.isPrototypeOf(a)); |
| |
| f(a, null); |
| assert.isTrue(Object.getPrototypeOf(a) === null); |
| assert.isTrue(a.__proto__ === undefined, "lost Object.prototype __proto__ getter"); |
| assert.isTrue(a.isPrototypeOf === undefined, "lost Object.prototype methods"); |
| }); |
| |
| // side test -- try to change null/undefined's __proto__ should throw |
| var __proto__setter = Object.getOwnPropertyDescriptor(Object.prototype, "__proto__").set; |
| [null, undefined].forEach(function (o) { |
| assert.throws__proto__ThisNotObject(function () { |
| __proto__setter.apply(o, [{}]); |
| }); |
| assert.throws__proto__ArgNotObject(function () { |
| Object.setPrototypeOf(o, {}); |
| }); |
| }); |
| } |
| }, |
| |
| { |
| name: "Change __proto__ to neither Object nor null, should throw", |
| body: function () { |
| function assertThrowAndNoChange(proto) { |
| var a = {}; |
| a.__proto__ = proto; |
| assert.isTrue(Object.getPrototypeOf(a) === Object.prototype, "Change __proto__ to: " + proto); |
| assert.throws__proto__ArgNotObjectOrNull(function () { |
| Object.setPrototypeOf(a, proto); |
| }, "Object.setPrototypeOf"); |
| assert.isTrue(Object.getPrototypeOf(a) === Object.prototype, "Change __proto__ to: " + proto); |
| } |
| |
| var __proto__setter = Object.getOwnPropertyDescriptor(Object.prototype, "__proto__").set; |
| [ |
| undefined, 0, 123, -12.3, NaN, Infinity, true, false, "str" |
| ].forEach(function (proto) { |
| assertThrowAndNoChange(proto); |
| |
| // side test -- try to change these primitive's __proto__ should pass (but has no effect) |
| if (proto !== undefined) { |
| proto.__proto__ = {}; // This is ok |
| __proto__setter.apply(proto, [{}]); // This is ok |
| Object.setPrototypeOf(proto, {}); |
| } |
| }); |
| } |
| }, |
| |
| { |
| name: "Change object.__proto__ to an object", |
| body: function () { |
| var a = { a: 100, b: 100 }; |
| var b = { b: 200, c: 200 }; |
| b.__proto__ = a; |
| helpers.printObject(b); |
| |
| helpers.writeln("\n-- delete b.b --"); |
| delete b.b; |
| helpers.printObject(b); |
| } |
| }, |
| |
| { |
| name: "Change object.__proto__ to an array", |
| body: function () { |
| var a = [8, 3, 9, 5]; |
| var b = { b: 200 }; |
| |
| // b.sort not available |
| assert.throws(function () { b.sort(); }); |
| |
| b.__proto__ = a; |
| assert.isTrue(!Array.isArray(b), "Still not Array"); |
| helpers.printObject(b); |
| |
| helpers.writeln("\n-- a.sort --"); |
| a.sort(); |
| helpers.printObject(b); |
| |
| helpers.writeln("\n-- b.sort --"); |
| b.sort(); |
| helpers.printObject(b); |
| } |
| }, |
| |
| { |
| name: "Change array.__proto__ to an object", |
| body: function () { |
| var a = { a: 100 }; |
| var b = [8, 3, 9, 5]; |
| |
| b.__proto__ = a; |
| assert.isTrue(Array.isArray(b), "Still is Array"); |
| helpers.printObject(b); |
| |
| // But now b.sort is gone |
| assert.throws(function () { b.sort(); }); |
| |
| // We can sort by apply |
| helpers.writeln("\n-- sort.apply --"); |
| Array.prototype.sort.apply(b); |
| helpers.printObject(b); |
| } |
| }, |
| |
| { |
| name: "Attempt to change DOM object __proto__", |
| body: function () { |
| function test(domObj, expected) { |
| var p0 = Object.getPrototypeOf(domObj); |
| assert.isTrue(p0 === domObj.__proto__); |
| |
| var p1 = { __proto__: p0 }; |
| domObj.__proto__ = p1; |
| |
| assert.areEqual(expected, Object.getPrototypeOf(domObj) === p1); |
| assert.isTrue(expected || domObj.document !== null); // expected succeed or otherwise window |
| } |
| |
| if (helpers.isInBrowser()) { |
| test(this, false); |
| test(window, false); |
| test(window.frames[0], false); |
| |
| test(document, true); |
| test(document.body, true); |
| test(document.createElement("div"), true); |
| |
| // test form scope |
| var f = document.getElementById("form1"); |
| var p0 = f.__proto__; |
| f.__proto__ = { form1_injected: "form1_injected_value", __proto__: p0 }; |
| |
| var button1 = document.getElementById("button1"); |
| button1.click(); |
| assert.areEqual("form1_injected_value", button1.form1_injected_copy); |
| } |
| } |
| }, |
| |
| { |
| name: "__proto__ and instanceof/isPrototypeOf", |
| body: function () { |
| function A() { } |
| function B() { } |
| |
| var a = new A(); |
| var b = new B(); |
| |
| assert.isTrue(!(b instanceof A), "Initially b is not instance of A"); |
| assert.isTrue(!(A.prototype.isPrototypeOf(b))); |
| assert.isTrue(b instanceof B, "Initially b is instance of B"); |
| assert.isTrue(B.prototype.isPrototypeOf(b)); |
| |
| b.__proto__ = A.prototype; |
| assert.isTrue(b instanceof A, "Now b is instance of A"); |
| assert.isTrue(A.prototype.isPrototypeOf(b)); |
| assert.isTrue(!(b instanceof B), "Now b is not instance of B"); |
| assert.isTrue(!(B.prototype.isPrototypeOf(b))); |
| |
| b.__proto__ = a; |
| assert.isTrue(b instanceof A, "b is still instance of A"); |
| assert.isTrue(A.prototype.isPrototypeOf(b)); |
| assert.isTrue(!(b instanceof B), "b is still not instance of B"); |
| assert.isTrue(!(B.prototype.isPrototypeOf(b))); |
| |
| b.__proto__ = B.prototype; |
| assert.isTrue(!(b instanceof A), "b is back not instance of A"); |
| assert.isTrue(!(A.prototype.isPrototypeOf(b))); |
| assert.isTrue(b instanceof B, "b is back instance of B"); |
| assert.isTrue(B.prototype.isPrototypeOf(b)); |
| } |
| }, |
| |
| { |
| name: "Verify we reject simple __proto__ cycle", |
| body: function () { |
| var a = {}; |
| var b = {}; |
| var c = {}; |
| |
| a.__proto__ = b; |
| b.__proto__ = c; |
| |
| assert.throws__proto__Cyclic(function () { c.__proto__ = a }); |
| assert.throws__proto__Cyclic(function () { a.__proto__ = a }); |
| assert.throws__proto__Cyclic(function () { Array.prototype.__proto__ = [] }); |
| } |
| }, |
| |
| { |
| name: "Verify proto cache is discarded", |
| body: function () { |
| var a = make_point(1, 2); |
| var b = make_point(3, 4); |
| |
| // |
| // Create prototype chain: |
| // x -> y -> a |
| // where x inherits properties from prototype a. |
| // |
| var y = Object.create(a); |
| var x = Object.create(y); |
| |
| function print(o) { |
| helpers.writeln(o.x + ", " + o.y); |
| } |
| |
| helpers.writeln("Before change"); |
| print(x); |
| |
| // |
| // Now change prototype chain: |
| // x -> y -> b |
| // While changing y.__proto__, we should discard inline proto cache in original chain. |
| // x should now get changed properties from prototype b. |
| // |
| helpers.writeln("After change"); |
| y.__proto__ = b; |
| print(x); |
| } |
| }, |
| |
| { |
| // Copy of above proto cache test and change a getter |
| name: "Verify proto getter cache is discarded", |
| body: function () { |
| var a = make_point(1, 2); |
| var b = make_point(3, 4); |
| Object.defineProperty(a, "y", { get: function () { return 7; }, enumerable: true, configurable: true }); |
| |
| // |
| // Create prototype chain: |
| // x -> y -> a |
| // where x inherits properties from prototype a. |
| // |
| var y = Object.create(a); |
| var x = Object.create(y); |
| |
| function print(o) { |
| helpers.writeln(o.x + ", " + o.y); |
| } |
| |
| helpers.writeln("Before change"); |
| print(x); |
| |
| // |
| // Now change prototype chain: |
| // x -> y -> b |
| // While changing y.__proto__, we should discard inline proto cache in original chain. |
| // x should now get changed properties from prototype b. |
| // |
| helpers.writeln("After change"); |
| y.__proto__ = b; |
| print(x); |
| }, |
| }, |
| |
| { |
| name: "Verify the new proto object is marked as proto, so that changing the proto object invalidates related proto cache", |
| body: function () { |
| var b = make_point(3, 4); |
| |
| function print(o) { |
| helpers.writeln(o.x + ", " + o.y); |
| } |
| |
| var x = {}; |
| |
| helpers.writeln("Before change"); |
| print(x); |
| |
| helpers.writeln("After change"); |
| x.__proto__ = b; // This should mark "b" as a prototype object |
| print(x); |
| |
| // |
| // Now we make certain change to "b". If b is marked on prototype correctly, the following should invalidate related proto cache. |
| // |
| helpers.writeln("After change proto property"); |
| Object.defineProperty(b, "x", { get: function () { return 9; }, enumerable: true, configurable: true }); |
| print(x); |
| } |
| }, |
| |
| { |
| name: "Verify changing __proto__ works safely with ObjTypeSpec", |
| body: function () { |
| function f(o, a) { |
| var m = o.x; |
| if (a > 0) |
| { |
| o.__proto__ = {}; // ObjTypeSpec won't generate 2nd type check for the next "o.x" load. But implicit call bailout should bailout right here. |
| } |
| var n = o.x; |
| |
| assert.areEqual(1, m); |
| assert.areEqual(a > 0 ? undefined : 1, n); |
| } |
| |
| var p = { x: 1 }; |
| var o = Object.create(p); |
| f(o, 0); // no __proto__ change in interpreter. |
| f(o, 0); // -maxInterpretCount:1, still no __proto__ change in jit. |
| f(o, 1); // -maxInterpretCount:1, now with __proto__ change in jit. |
| } |
| }, |
| |
| { |
| name: "Verify PathTypeHandler successor Types continue to work, case 1", |
| body: function () { |
| function f() { |
| return { x_100: 1 }; // Use x_100 to start a unique path |
| } |
| |
| var o1 = f(); |
| o1.__proto__ = { a: 2 }; |
| o1.y = 1; // This has path "x" -> "x, y" |
| |
| var o2 = f(); |
| o2.y = 1; // This also has path "x" -> "x, y" |
| |
| // But o1 and o2 can't go to the same Type. If they do, they have the same [[prototype]], which is wrong. |
| assert.areEqual(2, o1.a, "from prototype"); |
| assert.areEqual(undefined, o2.a, "should be undefined"); |
| } |
| }, |
| |
| { |
| name: "Verify PathTypeHandler successor Types continue to work, case 2", |
| body: function () { |
| function f() { |
| return { x_101: 1 }; // Use x_101 to start a unique path |
| } |
| |
| var o1 = f(); |
| o1.y = 1; // This has path "x" -> "x, y" |
| |
| var o2 = f(); |
| o2.__proto__ = { a: 2 }; |
| o2.y = 1; // This also has path "x" -> "x, y" |
| |
| // But o1 and o2 can't go to the same Type. If they do, they have the same [[prototype]], which is wrong. |
| assert.areEqual(undefined, o1.a, "should be undefined"); |
| assert.areEqual(2, o2.a, "from prototype"); |
| } |
| }, |
| |
| { |
| name: "Verify that we can shadow __proto__ property", |
| body: function () { |
| var p = {}; |
| var o = {}; |
| o.__proto__ = p; |
| |
| Object.defineProperty(p, "__proto__", { value: 10, writable: true, enumerable: true, configurable: true }); |
| assert.areEqual(10, o.__proto__); |
| |
| o.__proto__ = -100; |
| assert.areEqual(-100, o.__proto__); |
| |
| delete o.__proto__; |
| assert.areEqual(10, o.__proto__); |
| |
| delete p.__proto__; |
| assert.areEqual(p, o.__proto__); |
| assert.areEqual(Object.prototype, p.__proto__); |
| |
| o.__proto__ = { a: 123 }; |
| assert.areEqual(123, o.a); |
| } |
| }, |
| |
| { |
| name: "Test fast path o[i] with changed prototype", |
| body: function () { |
| var o = []; o.length = 10; |
| |
| var p = [123]; |
| o.__proto__ = p; // Now o's prototype != Array.prototype |
| assert.areEqual(123, o[0]); |
| |
| var p0 = { "1": 4 }; |
| p.__proto__ = p0; // Now p's prototype != Object.prototype |
| assert.areEqual(4, o[1]); |
| |
| p0.__proto__ = null; |
| assert.areEqual(undefined, o[2]); |
| } |
| }, |
| |
| { |
| name: "Test fast path o[i] when o.__proto__ == null", |
| body: function () { |
| var o = []; |
| o.__proto__ = null; |
| assert.areEqual(undefined, o[1]); |
| } |
| }, |
| |
| { |
| name: "Test fast path o[i] when o.__proto__.__proto__ == null", |
| body: function () { |
| var o = []; |
| o.__proto__ = [12, 34]; |
| o.__proto__.__proto__ = null; |
| assert.areEqual(undefined, o[5]); |
| } |
| }, |
| |
| { |
| name: "Test Array methods with changed prototype", |
| body: function () { |
| function make_array() { |
| var arr = []; |
| for (var p in arguments) { |
| var i = arguments[p]; |
| arr[i] = i; |
| } |
| return arr; |
| } |
| |
| var o = []; o.length = 10; |
| |
| o.__proto__ = make_array(0, 1, 2); |
| o.__proto__.__proto__ = make_array(3, 4); |
| o.__proto__.__proto__.__proto__ = { "5": 5, "6": 6 }; |
| o.__proto__.__proto__.__proto__.__proto__ = Array.prototype; |
| |
| var a = o.slice(); |
| assert.areEqual("0,1,2,3,4,5,6,,,", a.toString()); |
| |
| a.__proto__ = make_array(8, 9); |
| assert.areEqual("0,1,2,3,4,5,6,,8,9", a.toString()); |
| } |
| }, |
| |
| { |
| name: "Test cross-site change prototype", |
| body: function () { |
| if (this.WScript.LoadScriptFile) { |
| var eng = make_engine(); |
| var other = eng.eval('var o = { get name() {return this.v}, set name(value) {this.v = value}, v: "a name" }; o'); |
| |
| var o = { __proto__: other }; |
| assert.areEqual("a name", o.name); |
| } |
| } |
| }, |
| |
| { |
| name: "Test change prototype of global object", |
| body: function () { |
| if (this.WScript.LoadScriptFile) { |
| make_engine().run(function () { |
| function test(x) { |
| var p = { a: 1 }; |
| |
| // Should go through general change prototype code path and mark "p" as prototype. Then changes to "p" should invalidate proto cache. |
| x.__proto__ = p; |
| assert.areEqual(1, x.a); |
| |
| Object.defineProperty(p, "a", { get: function () { return 2; }, configurable: true }); |
| assert.areEqual(2, x.a); |
| } |
| |
| assert.isTrue(__proto__ === Object.prototype); // default binds to global this.__proto__ |
| test(this); |
| |
| __proto__ = { b: 3, __proto__: __proto__ }; |
| assert.areEqual(2, a); |
| assert.areEqual(3, b); |
| }); |
| } |
| } |
| }, |
| |
| { |
| name: "Blue 62526: __proto__: ArrayElementEnumerator does not expect non-TypeIds_Object on prototype", |
| body: function () { |
| var a = new String(); |
| a[1] = "a1"; |
| a[2] = "a2"; |
| a.__proto__ = []; |
| |
| var b = [0]; |
| |
| b.__proto__ = a; |
| b.length = 5; |
| b = b.concat([]); |
| |
| assert.areEqual('[0,"a1","a2",null,null]', JSON.stringify(b)); |
| } |
| }, |
| |
| { |
| name: "Blue 114364: __proto__: Object.preventExtensions should make [[prototype]] immutable", |
| body: function () { |
| var a = {}; |
| Object.preventExtensions(a); |
| |
| assert.throws( |
| function () { a.__proto__ = { x: 1 } }, |
| TypeError); |
| assert.isTrue(a.__proto__ === Object.prototype, "__proto__ should remain unchanged"); |
| } |
| }, |
| |
| { |
| name: "Blue 245453: __proto__: Invalid has-only-writable-data-property cache", |
| body: function () { |
| var a = { __proto__: {} }; |
| a.x = 0; // populate cache -- whole prototype chain has only writable data properties |
| a.__proto__.__proto__ = /a_regex/; |
| a.source = 1; |
| |
| assert.areEqual("a_regex", a.source, "prototype chain should NOT has-only-writable-data-properties"); |
| } |
| }, |
| |
| { |
| name: "Blue 245453: __proto__: Invalid has-only-writable-data-property cache -- verify cross-context", |
| body: function () { |
| if (this.WScript.LoadScriptFile) { |
| var eng = make_engine(); |
| var other = eng.eval('({b: 1})'); |
| |
| var a = { __proto__: other }; |
| assert.areEqual(1, a.b, "a.b from prototype"); |
| |
| a.x = 0; // try to populate cache -- prototype chain has only writable data properties -- our code actually would not cache cross-context |
| Object.defineProperty(other, "y", { value: 2, enumerable: true, writable: false }); |
| a.y = 1234; |
| assert.areEqual(2, a.y, "prototype chain should NOT be has-only-writable-data-properties"); |
| } |
| } |
| }, |
| |
| ]; |
| } else { |
| tests = [ |
| { |
| name: "Test Object.prototype.__proto__", |
| body: function () { |
| assert.isTrue(Object.prototype.__proto__ === undefined, "Object.prototype.__proto__ only supported on IE11 or later"); |
| assert.isTrue(Object.hasOwnProperty("getPrototypeOf") && !Object.hasOwnProperty("setPrototypeOf"), "Object.setPrototypeOf only supported on IE11 or later"); |
| |
| var o = { __proto__: { pp: 123 } }; // compat mode: not working in object literal |
| assert.areEqual(undefined, o.pp); |
| } |
| } |
| ]; |
| } |
| |
| testRunner.run(tests); |