diff options
Diffstat (limited to 'deps/v8/test/mjsunit/es7')
-rw-r--r-- | deps/v8/test/mjsunit/es7/object-observe.js | 1789 |
1 files changed, 1789 insertions, 0 deletions
diff --git a/deps/v8/test/mjsunit/es7/object-observe.js b/deps/v8/test/mjsunit/es7/object-observe.js new file mode 100644 index 0000000000..f5e84a6286 --- /dev/null +++ b/deps/v8/test/mjsunit/es7/object-observe.js @@ -0,0 +1,1789 @@ +// Copyright 2012 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Flags: --harmony-proxies --harmony-collections +// Flags: --harmony-symbols --allow-natives-syntax + +var allObservers = []; +function reset() { + allObservers.forEach(function(observer) { observer.reset(); }); +} + +function stringifyNoThrow(arg) { + try { + return JSON.stringify(arg); + } catch (e) { + return '{<circular reference>}'; + } +} + +function createObserver() { + "use strict"; // So that |this| in callback can be undefined. + + var observer = { + records: undefined, + callbackCount: 0, + reset: function() { + this.records = undefined; + this.callbackCount = 0; + }, + assertNotCalled: function() { + assertEquals(undefined, this.records); + assertEquals(0, this.callbackCount); + }, + assertCalled: function() { + assertEquals(1, this.callbackCount); + }, + assertRecordCount: function(count) { + this.assertCalled(); + assertEquals(count, this.records.length); + }, + assertCallbackRecords: function(recs) { + this.assertRecordCount(recs.length); + for (var i = 0; i < recs.length; i++) { + if ('name' in recs[i]) recs[i].name = String(recs[i].name); + print(i, stringifyNoThrow(this.records[i]), stringifyNoThrow(recs[i])); + assertSame(this.records[i].object, recs[i].object); + assertEquals('string', typeof recs[i].type); + assertPropertiesEqual(this.records[i], recs[i]); + } + } + }; + + observer.callback = function(r) { + assertEquals(undefined, this); + assertEquals('object', typeof r); + assertTrue(r instanceof Array) + observer.records = r; + observer.callbackCount++; + }; + + observer.reset(); + allObservers.push(observer); + return observer; +} + +var observer = createObserver(); +var observer2 = createObserver(); + +assertEquals("function", typeof observer.callback); +assertEquals("function", typeof observer2.callback); + +var obj = {}; + +function frozenFunction() {} +Object.freeze(frozenFunction); +var nonFunction = {}; +var changeRecordWithAccessor = { type: 'foo' }; +var recordCreated = false; +Object.defineProperty(changeRecordWithAccessor, 'name', { + get: function() { + recordCreated = true; + return "bar"; + }, + enumerable: true +}) + + +// Object.observe +assertThrows(function() { Object.observe("non-object", observer.callback); }, + TypeError); +assertThrows(function() { Object.observe(obj, nonFunction); }, TypeError); +assertThrows(function() { Object.observe(obj, frozenFunction); }, TypeError); +assertEquals(obj, Object.observe(obj, observer.callback, [1])); +assertEquals(obj, Object.observe(obj, observer.callback, [true])); +assertEquals(obj, Object.observe(obj, observer.callback, ['foo', null])); +assertEquals(obj, Object.observe(obj, observer.callback, [undefined])); +assertEquals(obj, Object.observe(obj, observer.callback, + ['foo', 'bar', 'baz'])); +assertEquals(obj, Object.observe(obj, observer.callback, [])); +assertEquals(obj, Object.observe(obj, observer.callback, undefined)); +assertEquals(obj, Object.observe(obj, observer.callback)); + +// Object.unobserve +assertThrows(function() { Object.unobserve(4, observer.callback); }, TypeError); +assertThrows(function() { Object.unobserve(obj, nonFunction); }, TypeError); +assertEquals(obj, Object.unobserve(obj, observer.callback)); + + +// Object.getNotifier +var notifier = Object.getNotifier(obj); +assertSame(notifier, Object.getNotifier(obj)); +assertEquals(null, Object.getNotifier(Object.freeze({}))); +assertFalse(notifier.hasOwnProperty('notify')); +assertEquals([], Object.keys(notifier)); +var notifyDesc = Object.getOwnPropertyDescriptor(notifier.__proto__, 'notify'); +assertTrue(notifyDesc.configurable); +assertTrue(notifyDesc.writable); +assertFalse(notifyDesc.enumerable); +assertThrows(function() { notifier.notify({}); }, TypeError); +assertThrows(function() { notifier.notify({ type: 4 }); }, TypeError); + +assertThrows(function() { notifier.performChange(1, function(){}); }, TypeError); +assertThrows(function() { notifier.performChange(undefined, function(){}); }, TypeError); +assertThrows(function() { notifier.performChange('foo', undefined); }, TypeError); +assertThrows(function() { notifier.performChange('foo', 'bar'); }, TypeError); +var global = this; +notifier.performChange('foo', function() { + assertEquals(global, this); +}); + +var notify = notifier.notify; +assertThrows(function() { notify.call(undefined, { type: 'a' }); }, TypeError); +assertThrows(function() { notify.call(null, { type: 'a' }); }, TypeError); +assertThrows(function() { notify.call(5, { type: 'a' }); }, TypeError); +assertThrows(function() { notify.call('hello', { type: 'a' }); }, TypeError); +assertThrows(function() { notify.call(false, { type: 'a' }); }, TypeError); +assertThrows(function() { notify.call({}, { type: 'a' }); }, TypeError); +assertFalse(recordCreated); +notifier.notify(changeRecordWithAccessor); +assertFalse(recordCreated); // not observed yet + + +// Object.deliverChangeRecords +assertThrows(function() { Object.deliverChangeRecords(nonFunction); }, TypeError); + +Object.observe(obj, observer.callback); + + +// notify uses to [[CreateOwnProperty]] to create changeRecord; +reset(); +var protoExpandoAccessed = false; +Object.defineProperty(Object.prototype, 'protoExpando', + { + configurable: true, + set: function() { protoExpandoAccessed = true; } + } +); +notifier.notify({ type: 'foo', protoExpando: 'val'}); +assertFalse(protoExpandoAccessed); +delete Object.prototype.protoExpando; +Object.deliverChangeRecords(observer.callback); + + +// Multiple records are delivered. +reset(); +notifier.notify({ + type: 'update', + name: 'foo', + expando: 1 +}); + +notifier.notify({ + object: notifier, // object property is ignored + type: 'delete', + name: 'bar', + expando2: 'str' +}); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, name: 'foo', type: 'update', expando: 1 }, + { object: obj, name: 'bar', type: 'delete', expando2: 'str' } +]); + +// Non-string accept values are coerced to strings +reset(); +Object.observe(obj, observer.callback, [true, 1, null, undefined]); +notifier = Object.getNotifier(obj); +notifier.notify({ type: 'true' }); +notifier.notify({ type: 'false' }); +notifier.notify({ type: '1' }); +notifier.notify({ type: '-1' }); +notifier.notify({ type: 'null' }); +notifier.notify({ type: 'nill' }); +notifier.notify({ type: 'undefined' }); +notifier.notify({ type: 'defined' }); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, type: 'true' }, + { object: obj, type: '1' }, + { object: obj, type: 'null' }, + { object: obj, type: 'undefined' } +]); + +// No delivery takes place if no records are pending +reset(); +Object.deliverChangeRecords(observer.callback); +observer.assertNotCalled(); + + +// Multiple observation has no effect. +reset(); +Object.observe(obj, observer.callback); +Object.observe(obj, observer.callback); +Object.getNotifier(obj).notify({ + type: 'update', +}); +Object.deliverChangeRecords(observer.callback); +observer.assertCalled(); + + +// Observation can be stopped. +reset(); +Object.unobserve(obj, observer.callback); +Object.getNotifier(obj).notify({ + type: 'update', +}); +Object.deliverChangeRecords(observer.callback); +observer.assertNotCalled(); + + +// Multiple unobservation has no effect +reset(); +Object.unobserve(obj, observer.callback); +Object.unobserve(obj, observer.callback); +Object.getNotifier(obj).notify({ + type: 'update', +}); +Object.deliverChangeRecords(observer.callback); +observer.assertNotCalled(); + + +// Re-observation works and only includes changeRecords after of call. +reset(); +Object.getNotifier(obj).notify({ + type: 'update', +}); +Object.observe(obj, observer.callback); +Object.getNotifier(obj).notify({ + type: 'update', +}); +records = undefined; +Object.deliverChangeRecords(observer.callback); +observer.assertRecordCount(1); + +// Get notifier prior to observing +reset(); +var obj = {}; +Object.getNotifier(obj); +Object.observe(obj, observer.callback); +obj.id = 1; +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, type: 'add', name: 'id' }, +]); + +// The empty-string property is observable +reset(); +var obj = {}; +Object.observe(obj, observer.callback); +obj[''] = ''; +obj[''] = ' '; +delete obj['']; +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, type: 'add', name: '' }, + { object: obj, type: 'update', name: '', oldValue: '' }, + { object: obj, type: 'delete', name: '', oldValue: ' ' }, +]); + +// Object.preventExtensions +reset(); +var obj = { foo: 'bar'}; +Object.observe(obj, observer.callback); +obj.baz = 'bat'; +Object.preventExtensions(obj); + +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, type: 'add', name: 'baz' }, + { object: obj, type: 'preventExtensions' }, +]); + +reset(); +var obj = { foo: 'bar'}; +Object.preventExtensions(obj); +Object.observe(obj, observer.callback); +Object.preventExtensions(obj); +Object.deliverChangeRecords(observer.callback); +observer.assertNotCalled(); + +// Object.freeze +reset(); +var obj = { a: 'a' }; +Object.defineProperty(obj, 'b', { + writable: false, + configurable: true, + value: 'b' +}); +Object.defineProperty(obj, 'c', { + writable: true, + configurable: false, + value: 'c' +}); +Object.defineProperty(obj, 'd', { + writable: false, + configurable: false, + value: 'd' +}); +Object.observe(obj, observer.callback); +Object.freeze(obj); + +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, type: 'reconfigure', name: 'a' }, + { object: obj, type: 'reconfigure', name: 'b' }, + { object: obj, type: 'reconfigure', name: 'c' }, + { object: obj, type: 'preventExtensions' }, +]); + +reset(); +var obj = { foo: 'bar'}; +Object.freeze(obj); +Object.observe(obj, observer.callback); +Object.freeze(obj); +Object.deliverChangeRecords(observer.callback); +observer.assertNotCalled(); + +// Object.seal +reset(); +var obj = { a: 'a' }; +Object.defineProperty(obj, 'b', { + writable: false, + configurable: true, + value: 'b' +}); +Object.defineProperty(obj, 'c', { + writable: true, + configurable: false, + value: 'c' +}); +Object.defineProperty(obj, 'd', { + writable: false, + configurable: false, + value: 'd' +}); +Object.observe(obj, observer.callback); +Object.seal(obj); + +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, type: 'reconfigure', name: 'a' }, + { object: obj, type: 'reconfigure', name: 'b' }, + { object: obj, type: 'preventExtensions' }, +]); + +reset(); +var obj = { foo: 'bar'}; +Object.seal(obj); +Object.observe(obj, observer.callback); +Object.seal(obj); +Object.deliverChangeRecords(observer.callback); +observer.assertNotCalled(); + +// Observing a continuous stream of changes, while itermittantly unobserving. +reset(); +var obj = {}; +Object.observe(obj, observer.callback); +Object.getNotifier(obj).notify({ + type: 'update', + val: 1 +}); + +Object.unobserve(obj, observer.callback); +Object.getNotifier(obj).notify({ + type: 'update', + val: 2 +}); + +Object.observe(obj, observer.callback); +Object.getNotifier(obj).notify({ + type: 'update', + val: 3 +}); + +Object.unobserve(obj, observer.callback); +Object.getNotifier(obj).notify({ + type: 'update', + val: 4 +}); + +Object.observe(obj, observer.callback); +Object.getNotifier(obj).notify({ + type: 'update', + val: 5 +}); + +Object.unobserve(obj, observer.callback); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, type: 'update', val: 1 }, + { object: obj, type: 'update', val: 3 }, + { object: obj, type: 'update', val: 5 } +]); + +// Accept +reset(); +Object.observe(obj, observer.callback, ['somethingElse']); +Object.getNotifier(obj).notify({ + type: 'add' +}); +Object.getNotifier(obj).notify({ + type: 'update' +}); +Object.getNotifier(obj).notify({ + type: 'delete' +}); +Object.getNotifier(obj).notify({ + type: 'reconfigure' +}); +Object.getNotifier(obj).notify({ + type: 'setPrototype' +}); +Object.deliverChangeRecords(observer.callback); +observer.assertNotCalled(); + +reset(); +Object.observe(obj, observer.callback, ['add', 'delete', 'setPrototype']); +Object.getNotifier(obj).notify({ + type: 'add' +}); +Object.getNotifier(obj).notify({ + type: 'update' +}); +Object.getNotifier(obj).notify({ + type: 'delete' +}); +Object.getNotifier(obj).notify({ + type: 'delete' +}); +Object.getNotifier(obj).notify({ + type: 'reconfigure' +}); +Object.getNotifier(obj).notify({ + type: 'setPrototype' +}); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, type: 'add' }, + { object: obj, type: 'delete' }, + { object: obj, type: 'delete' }, + { object: obj, type: 'setPrototype' } +]); + +reset(); +Object.observe(obj, observer.callback, ['update', 'foo']); +Object.getNotifier(obj).notify({ + type: 'add' +}); +Object.getNotifier(obj).notify({ + type: 'update' +}); +Object.getNotifier(obj).notify({ + type: 'delete' +}); +Object.getNotifier(obj).notify({ + type: 'foo' +}); +Object.getNotifier(obj).notify({ + type: 'bar' +}); +Object.getNotifier(obj).notify({ + type: 'foo' +}); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, type: 'update' }, + { object: obj, type: 'foo' }, + { object: obj, type: 'foo' } +]); + +reset(); +function Thingy(a, b, c) { + this.a = a; + this.b = b; +} + +Thingy.MULTIPLY = 'multiply'; +Thingy.INCREMENT = 'increment'; +Thingy.INCREMENT_AND_MULTIPLY = 'incrementAndMultiply'; + +Thingy.prototype = { + increment: function(amount) { + var notifier = Object.getNotifier(this); + + var self = this; + notifier.performChange(Thingy.INCREMENT, function() { + self.a += amount; + self.b += amount; + + return { + incremented: amount + }; // implicit notify + }); + }, + + multiply: function(amount) { + var notifier = Object.getNotifier(this); + + var self = this; + notifier.performChange(Thingy.MULTIPLY, function() { + self.a *= amount; + self.b *= amount; + + return { + multiplied: amount + }; // implicit notify + }); + }, + + incrementAndMultiply: function(incAmount, multAmount) { + var notifier = Object.getNotifier(this); + + var self = this; + notifier.performChange(Thingy.INCREMENT_AND_MULTIPLY, function() { + self.increment(incAmount); + self.multiply(multAmount); + + return { + incremented: incAmount, + multiplied: multAmount + }; // implicit notify + }); + } +} + +Thingy.observe = function(thingy, callback) { + Object.observe(thingy, callback, [Thingy.INCREMENT, + Thingy.MULTIPLY, + Thingy.INCREMENT_AND_MULTIPLY, + 'update']); +} + +Thingy.unobserve = function(thingy, callback) { + Object.unobserve(thingy); +} + +var thingy = new Thingy(2, 4); + +Object.observe(thingy, observer.callback); +Thingy.observe(thingy, observer2.callback); +thingy.increment(3); // { a: 5, b: 7 } +thingy.b++; // { a: 5, b: 8 } +thingy.multiply(2); // { a: 10, b: 16 } +thingy.a++; // { a: 11, b: 16 } +thingy.incrementAndMultiply(2, 2); // { a: 26, b: 36 } + +Object.deliverChangeRecords(observer.callback); +Object.deliverChangeRecords(observer2.callback); +observer.assertCallbackRecords([ + { object: thingy, type: 'update', name: 'a', oldValue: 2 }, + { object: thingy, type: 'update', name: 'b', oldValue: 4 }, + { object: thingy, type: 'update', name: 'b', oldValue: 7 }, + { object: thingy, type: 'update', name: 'a', oldValue: 5 }, + { object: thingy, type: 'update', name: 'b', oldValue: 8 }, + { object: thingy, type: 'update', name: 'a', oldValue: 10 }, + { object: thingy, type: 'update', name: 'a', oldValue: 11 }, + { object: thingy, type: 'update', name: 'b', oldValue: 16 }, + { object: thingy, type: 'update', name: 'a', oldValue: 13 }, + { object: thingy, type: 'update', name: 'b', oldValue: 18 }, +]); +observer2.assertCallbackRecords([ + { object: thingy, type: Thingy.INCREMENT, incremented: 3 }, + { object: thingy, type: 'update', name: 'b', oldValue: 7 }, + { object: thingy, type: Thingy.MULTIPLY, multiplied: 2 }, + { object: thingy, type: 'update', name: 'a', oldValue: 10 }, + { + object: thingy, + type: Thingy.INCREMENT_AND_MULTIPLY, + incremented: 2, + multiplied: 2 + } +]); + +// ArrayPush cached stub +reset(); + +function pushMultiple(arr) { + arr.push('a'); + arr.push('b'); + arr.push('c'); +} + +for (var i = 0; i < 5; i++) { + var arr = []; + pushMultiple(arr); +} + +for (var i = 0; i < 5; i++) { + reset(); + var arr = []; + Object.observe(arr, observer.callback); + pushMultiple(arr); + Object.unobserve(arr, observer.callback); + Object.deliverChangeRecords(observer.callback); + observer.assertCallbackRecords([ + { object: arr, type: 'add', name: '0' }, + { object: arr, type: 'update', name: 'length', oldValue: 0 }, + { object: arr, type: 'add', name: '1' }, + { object: arr, type: 'update', name: 'length', oldValue: 1 }, + { object: arr, type: 'add', name: '2' }, + { object: arr, type: 'update', name: 'length', oldValue: 2 }, + ]); +} + + +// ArrayPop cached stub +reset(); + +function popMultiple(arr) { + arr.pop(); + arr.pop(); + arr.pop(); +} + +for (var i = 0; i < 5; i++) { + var arr = ['a', 'b', 'c']; + popMultiple(arr); +} + +for (var i = 0; i < 5; i++) { + reset(); + var arr = ['a', 'b', 'c']; + Object.observe(arr, observer.callback); + popMultiple(arr); + Object.unobserve(arr, observer.callback); + Object.deliverChangeRecords(observer.callback); + observer.assertCallbackRecords([ + { object: arr, type: 'delete', name: '2', oldValue: 'c' }, + { object: arr, type: 'update', name: 'length', oldValue: 3 }, + { object: arr, type: 'delete', name: '1', oldValue: 'b' }, + { object: arr, type: 'update', name: 'length', oldValue: 2 }, + { object: arr, type: 'delete', name: '0', oldValue: 'a' }, + { object: arr, type: 'update', name: 'length', oldValue: 1 }, + ]); +} + + +reset(); +function RecursiveThingy() {} + +RecursiveThingy.MULTIPLY_FIRST_N = 'multiplyFirstN'; + +RecursiveThingy.prototype = { + __proto__: Array.prototype, + + multiplyFirstN: function(amount, n) { + if (!n) + return; + var notifier = Object.getNotifier(this); + var self = this; + notifier.performChange(RecursiveThingy.MULTIPLY_FIRST_N, function() { + self[n-1] = self[n-1]*amount; + self.multiplyFirstN(amount, n-1); + }); + + notifier.notify({ + type: RecursiveThingy.MULTIPLY_FIRST_N, + multiplied: amount, + n: n + }); + }, +} + +RecursiveThingy.observe = function(thingy, callback) { + Object.observe(thingy, callback, [RecursiveThingy.MULTIPLY_FIRST_N]); +} + +RecursiveThingy.unobserve = function(thingy, callback) { + Object.unobserve(thingy); +} + +var thingy = new RecursiveThingy; +thingy.push(1, 2, 3, 4); + +Object.observe(thingy, observer.callback); +RecursiveThingy.observe(thingy, observer2.callback); +thingy.multiplyFirstN(2, 3); // [2, 4, 6, 4] + +Object.deliverChangeRecords(observer.callback); +Object.deliverChangeRecords(observer2.callback); +observer.assertCallbackRecords([ + { object: thingy, type: 'update', name: '2', oldValue: 3 }, + { object: thingy, type: 'update', name: '1', oldValue: 2 }, + { object: thingy, type: 'update', name: '0', oldValue: 1 } +]); +observer2.assertCallbackRecords([ + { object: thingy, type: RecursiveThingy.MULTIPLY_FIRST_N, multiplied: 2, n: 3 } +]); + +reset(); +function DeckSuit() { + this.push('1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'A', 'Q', 'K'); +} + +DeckSuit.SHUFFLE = 'shuffle'; + +DeckSuit.prototype = { + __proto__: Array.prototype, + + shuffle: function() { + var notifier = Object.getNotifier(this); + var self = this; + notifier.performChange(DeckSuit.SHUFFLE, function() { + self.reverse(); + self.sort(function() { return Math.random()* 2 - 1; }); + var cut = self.splice(0, 6); + Array.prototype.push.apply(self, cut); + self.reverse(); + self.sort(function() { return Math.random()* 2 - 1; }); + var cut = self.splice(0, 6); + Array.prototype.push.apply(self, cut); + self.reverse(); + self.sort(function() { return Math.random()* 2 - 1; }); + }); + + notifier.notify({ + type: DeckSuit.SHUFFLE + }); + }, +} + +DeckSuit.observe = function(thingy, callback) { + Object.observe(thingy, callback, [DeckSuit.SHUFFLE]); +} + +DeckSuit.unobserve = function(thingy, callback) { + Object.unobserve(thingy); +} + +var deck = new DeckSuit; + +DeckSuit.observe(deck, observer2.callback); +deck.shuffle(); + +Object.deliverChangeRecords(observer2.callback); +observer2.assertCallbackRecords([ + { object: deck, type: DeckSuit.SHUFFLE } +]); + +// Observing multiple objects; records appear in order. +reset(); +var obj2 = {}; +var obj3 = {} +Object.observe(obj, observer.callback); +Object.observe(obj3, observer.callback); +Object.observe(obj2, observer.callback); +Object.getNotifier(obj).notify({ + type: 'add', +}); +Object.getNotifier(obj2).notify({ + type: 'update', +}); +Object.getNotifier(obj3).notify({ + type: 'delete', +}); +Object.observe(obj3, observer.callback); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, type: 'add' }, + { object: obj2, type: 'update' }, + { object: obj3, type: 'delete' } +]); + + +// Recursive observation. +var obj = {a: 1}; +var callbackCount = 0; +function recursiveObserver(r) { + assertEquals(1, r.length); + ++callbackCount; + if (r[0].oldValue < 100) ++obj[r[0].name]; +} +Object.observe(obj, recursiveObserver); +++obj.a; +Object.deliverChangeRecords(recursiveObserver); +assertEquals(100, callbackCount); + +var obj1 = {a: 1}; +var obj2 = {a: 1}; +var recordCount = 0; +function recursiveObserver2(r) { + recordCount += r.length; + if (r[0].oldValue < 100) { + ++obj1.a; + ++obj2.a; + } +} +Object.observe(obj1, recursiveObserver2); +Object.observe(obj2, recursiveObserver2); +++obj1.a; +Object.deliverChangeRecords(recursiveObserver2); +assertEquals(199, recordCount); + + +// Observing named properties. +reset(); +var obj = {a: 1} +Object.observe(obj, observer.callback); +obj.a = 2; +obj["a"] = 3; +delete obj.a; +obj.a = 4; +obj.a = 4; // ignored +obj.a = 5; +Object.defineProperty(obj, "a", {value: 6}); +Object.defineProperty(obj, "a", {writable: false}); +obj.a = 7; // ignored +Object.defineProperty(obj, "a", {value: 8}); +Object.defineProperty(obj, "a", {value: 7, writable: true}); +Object.defineProperty(obj, "a", {get: function() {}}); +Object.defineProperty(obj, "a", {get: frozenFunction}); +Object.defineProperty(obj, "a", {get: frozenFunction}); // ignored +Object.defineProperty(obj, "a", {get: frozenFunction, set: frozenFunction}); +Object.defineProperty(obj, "a", {set: frozenFunction}); // ignored +Object.defineProperty(obj, "a", {get: undefined, set: frozenFunction}); +delete obj.a; +delete obj.a; +Object.defineProperty(obj, "a", {get: function() {}, configurable: true}); +Object.defineProperty(obj, "a", {value: 9, writable: true}); +obj.a = 10; +++obj.a; +obj.a++; +obj.a *= 3; +delete obj.a; +Object.defineProperty(obj, "a", {value: 11, configurable: true}); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, name: "a", type: "update", oldValue: 1 }, + { object: obj, name: "a", type: "update", oldValue: 2 }, + { object: obj, name: "a", type: "delete", oldValue: 3 }, + { object: obj, name: "a", type: "add" }, + { object: obj, name: "a", type: "update", oldValue: 4 }, + { object: obj, name: "a", type: "update", oldValue: 5 }, + { object: obj, name: "a", type: "reconfigure" }, + { object: obj, name: "a", type: "update", oldValue: 6 }, + { object: obj, name: "a", type: "reconfigure", oldValue: 8 }, + { object: obj, name: "a", type: "reconfigure", oldValue: 7 }, + { object: obj, name: "a", type: "reconfigure" }, + { object: obj, name: "a", type: "reconfigure" }, + { object: obj, name: "a", type: "reconfigure" }, + { object: obj, name: "a", type: "delete" }, + { object: obj, name: "a", type: "add" }, + { object: obj, name: "a", type: "reconfigure" }, + { object: obj, name: "a", type: "update", oldValue: 9 }, + { object: obj, name: "a", type: "update", oldValue: 10 }, + { object: obj, name: "a", type: "update", oldValue: 11 }, + { object: obj, name: "a", type: "update", oldValue: 12 }, + { object: obj, name: "a", type: "delete", oldValue: 36 }, + { object: obj, name: "a", type: "add" }, +]); + + +// Observing indexed properties. +reset(); +var obj = {'1': 1} +Object.observe(obj, observer.callback); +obj[1] = 2; +obj[1] = 3; +delete obj[1]; +obj[1] = 4; +obj[1] = 4; // ignored +obj[1] = 5; +Object.defineProperty(obj, "1", {value: 6}); +Object.defineProperty(obj, "1", {writable: false}); +obj[1] = 7; // ignored +Object.defineProperty(obj, "1", {value: 8}); +Object.defineProperty(obj, "1", {value: 7, writable: true}); +Object.defineProperty(obj, "1", {get: function() {}}); +Object.defineProperty(obj, "1", {get: frozenFunction}); +Object.defineProperty(obj, "1", {get: frozenFunction}); // ignored +Object.defineProperty(obj, "1", {get: frozenFunction, set: frozenFunction}); +Object.defineProperty(obj, "1", {set: frozenFunction}); // ignored +Object.defineProperty(obj, "1", {get: undefined, set: frozenFunction}); +delete obj[1]; +delete obj[1]; +Object.defineProperty(obj, "1", {get: function() {}, configurable: true}); +Object.defineProperty(obj, "1", {value: 9, writable: true}); +obj[1] = 10; +++obj[1]; +obj[1]++; +obj[1] *= 3; +delete obj[1]; +Object.defineProperty(obj, "1", {value: 11, configurable: true}); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, name: "1", type: "update", oldValue: 1 }, + { object: obj, name: "1", type: "update", oldValue: 2 }, + { object: obj, name: "1", type: "delete", oldValue: 3 }, + { object: obj, name: "1", type: "add" }, + { object: obj, name: "1", type: "update", oldValue: 4 }, + { object: obj, name: "1", type: "update", oldValue: 5 }, + { object: obj, name: "1", type: "reconfigure" }, + { object: obj, name: "1", type: "update", oldValue: 6 }, + { object: obj, name: "1", type: "reconfigure", oldValue: 8 }, + { object: obj, name: "1", type: "reconfigure", oldValue: 7 }, + { object: obj, name: "1", type: "reconfigure" }, + { object: obj, name: "1", type: "reconfigure" }, + { object: obj, name: "1", type: "reconfigure" }, + { object: obj, name: "1", type: "delete" }, + { object: obj, name: "1", type: "add" }, + { object: obj, name: "1", type: "reconfigure" }, + { object: obj, name: "1", type: "update", oldValue: 9 }, + { object: obj, name: "1", type: "update", oldValue: 10 }, + { object: obj, name: "1", type: "update", oldValue: 11 }, + { object: obj, name: "1", type: "update", oldValue: 12 }, + { object: obj, name: "1", type: "delete", oldValue: 36 }, + { object: obj, name: "1", type: "add" }, +]); + + +// Observing symbol properties (not). +print("*****") +reset(); +var obj = {} +var symbol = Symbol("secret"); +Object.observe(obj, observer.callback); +obj[symbol] = 3; +delete obj[symbol]; +Object.defineProperty(obj, symbol, {get: function() {}, configurable: true}); +Object.defineProperty(obj, symbol, {value: 6}); +Object.defineProperty(obj, symbol, {writable: false}); +delete obj[symbol]; +Object.defineProperty(obj, symbol, {value: 7}); +++obj[symbol]; +obj[symbol]++; +obj[symbol] *= 3; +delete obj[symbol]; +obj.__defineSetter__(symbol, function() {}); +obj.__defineGetter__(symbol, function() {}); +Object.deliverChangeRecords(observer.callback); +observer.assertNotCalled(); + + +// Test all kinds of objects generically. +function TestObserveConfigurable(obj, prop) { + reset(); + Object.observe(obj, observer.callback); + Object.unobserve(obj, observer.callback); + obj[prop] = 1; + Object.observe(obj, observer.callback); + obj[prop] = 2; + obj[prop] = 3; + delete obj[prop]; + obj[prop] = 4; + obj[prop] = 4; // ignored + obj[prop] = 5; + Object.defineProperty(obj, prop, {value: 6}); + Object.defineProperty(obj, prop, {writable: false}); + obj[prop] = 7; // ignored + Object.defineProperty(obj, prop, {value: 8}); + Object.defineProperty(obj, prop, {value: 7, writable: true}); + Object.defineProperty(obj, prop, {get: function() {}}); + Object.defineProperty(obj, prop, {get: frozenFunction}); + Object.defineProperty(obj, prop, {get: frozenFunction}); // ignored + Object.defineProperty(obj, prop, {get: frozenFunction, set: frozenFunction}); + Object.defineProperty(obj, prop, {set: frozenFunction}); // ignored + Object.defineProperty(obj, prop, {get: undefined, set: frozenFunction}); + obj.__defineSetter__(prop, frozenFunction); // ignored + obj.__defineSetter__(prop, function() {}); + obj.__defineGetter__(prop, function() {}); + delete obj[prop]; + delete obj[prop]; // ignored + obj.__defineGetter__(prop, function() {}); + delete obj[prop]; + Object.defineProperty(obj, prop, {get: function() {}, configurable: true}); + Object.defineProperty(obj, prop, {value: 9, writable: true}); + obj[prop] = 10; + ++obj[prop]; + obj[prop]++; + obj[prop] *= 3; + delete obj[prop]; + Object.defineProperty(obj, prop, {value: 11, configurable: true}); + Object.deliverChangeRecords(observer.callback); + observer.assertCallbackRecords([ + { object: obj, name: prop, type: "update", oldValue: 1 }, + { object: obj, name: prop, type: "update", oldValue: 2 }, + { object: obj, name: prop, type: "delete", oldValue: 3 }, + { object: obj, name: prop, type: "add" }, + { object: obj, name: prop, type: "update", oldValue: 4 }, + { object: obj, name: prop, type: "update", oldValue: 5 }, + { object: obj, name: prop, type: "reconfigure" }, + { object: obj, name: prop, type: "update", oldValue: 6 }, + { object: obj, name: prop, type: "reconfigure", oldValue: 8 }, + { object: obj, name: prop, type: "reconfigure", oldValue: 7 }, + { object: obj, name: prop, type: "reconfigure" }, + { object: obj, name: prop, type: "reconfigure" }, + { object: obj, name: prop, type: "reconfigure" }, + { object: obj, name: prop, type: "reconfigure" }, + { object: obj, name: prop, type: "reconfigure" }, + { object: obj, name: prop, type: "delete" }, + { object: obj, name: prop, type: "add" }, + { object: obj, name: prop, type: "delete" }, + { object: obj, name: prop, type: "add" }, + { object: obj, name: prop, type: "reconfigure" }, + { object: obj, name: prop, type: "update", oldValue: 9 }, + { object: obj, name: prop, type: "update", oldValue: 10 }, + { object: obj, name: prop, type: "update", oldValue: 11 }, + { object: obj, name: prop, type: "update", oldValue: 12 }, + { object: obj, name: prop, type: "delete", oldValue: 36 }, + { object: obj, name: prop, type: "add" }, + ]); + Object.unobserve(obj, observer.callback); + delete obj[prop]; +} + +function TestObserveNonConfigurable(obj, prop, desc) { + reset(); + Object.observe(obj, observer.callback); + Object.unobserve(obj, observer.callback); + obj[prop] = 1; + Object.observe(obj, observer.callback); + obj[prop] = 4; + obj[prop] = 4; // ignored + obj[prop] = 5; + Object.defineProperty(obj, prop, {value: 6}); + Object.defineProperty(obj, prop, {value: 6}); // ignored + Object.defineProperty(obj, prop, {value: 7}); + Object.defineProperty(obj, prop, {enumerable: desc.enumerable}); // ignored + Object.defineProperty(obj, prop, {writable: false}); + obj[prop] = 7; // ignored + Object.deliverChangeRecords(observer.callback); + observer.assertCallbackRecords([ + { object: obj, name: prop, type: "update", oldValue: 1 }, + { object: obj, name: prop, type: "update", oldValue: 4 }, + { object: obj, name: prop, type: "update", oldValue: 5 }, + { object: obj, name: prop, type: "update", oldValue: 6 }, + { object: obj, name: prop, type: "reconfigure" }, + ]); + Object.unobserve(obj, observer.callback); +} + +function createProxy(create, x) { + var handler = { + getPropertyDescriptor: function(k) { + for (var o = this.target; o; o = Object.getPrototypeOf(o)) { + var desc = Object.getOwnPropertyDescriptor(o, k); + if (desc) return desc; + } + return undefined; + }, + getOwnPropertyDescriptor: function(k) { + return Object.getOwnPropertyDescriptor(this.target, k); + }, + defineProperty: function(k, desc) { + var x = Object.defineProperty(this.target, k, desc); + Object.deliverChangeRecords(this.callback); + return x; + }, + delete: function(k) { + var x = delete this.target[k]; + Object.deliverChangeRecords(this.callback); + return x; + }, + getPropertyNames: function() { + return Object.getOwnPropertyNames(this.target); + }, + target: {isProxy: true}, + callback: function(changeRecords) { + print("callback", stringifyNoThrow(handler.proxy), stringifyNoThrow(got)); + for (var i in changeRecords) { + var got = changeRecords[i]; + var change = {object: handler.proxy, name: got.name, type: got.type}; + if ("oldValue" in got) change.oldValue = got.oldValue; + Object.getNotifier(handler.proxy).notify(change); + } + }, + }; + Object.observe(handler.target, handler.callback); + return handler.proxy = create(handler, x); +} + +var objects = [ + {}, + [], + this, // global object + function(){}, + (function(){ return arguments })(), + (function(){ "use strict"; return arguments })(), + Object(1), Object(true), Object("bla"), + new Date(), + Object, Function, Date, RegExp, + new Set, new Map, new WeakMap, + new ArrayBuffer(10), new Int32Array(5), + createProxy(Proxy.create, null), + createProxy(Proxy.createFunction, function(){}), +]; +var properties = ["a", "1", 1, "length", "setPrototype", "name", "caller"]; + +// Cases that yield non-standard results. +function blacklisted(obj, prop) { + return (obj instanceof Int32Array && prop == 1) || + (obj instanceof Int32Array && prop === "length") || + (obj instanceof ArrayBuffer && prop == 1) +} + +for (var i in objects) for (var j in properties) { + var obj = objects[i]; + var prop = properties[j]; + if (blacklisted(obj, prop)) continue; + var desc = Object.getOwnPropertyDescriptor(obj, prop); + print("***", typeof obj, stringifyNoThrow(obj), prop); + if (!desc || desc.configurable) + TestObserveConfigurable(obj, prop); + else if (desc.writable) + TestObserveNonConfigurable(obj, prop, desc); +} + + +// Observing array length (including truncation) +reset(); +var arr = ['a', 'b', 'c', 'd']; +var arr2 = ['alpha', 'beta']; +var arr3 = ['hello']; +arr3[2] = 'goodbye'; +arr3.length = 6; +Object.defineProperty(arr, '0', {configurable: false}); +Object.defineProperty(arr, '2', {get: function(){}}); +Object.defineProperty(arr2, '0', {get: function(){}, configurable: false}); +Object.observe(arr, observer.callback); +Array.observe(arr, observer2.callback); +Object.observe(arr2, observer.callback); +Array.observe(arr2, observer2.callback); +Object.observe(arr3, observer.callback); +Array.observe(arr3, observer2.callback); +arr.length = 2; +arr.length = 0; +arr.length = 10; +Object.defineProperty(arr, 'length', {writable: false}); +arr2.length = 0; +arr2.length = 1; // no change expected +Object.defineProperty(arr2, 'length', {value: 1, writable: false}); +arr3.length = 0; +++arr3.length; +arr3.length++; +arr3.length /= 2; +Object.defineProperty(arr3, 'length', {value: 5}); +arr3[4] = 5; +Object.defineProperty(arr3, 'length', {value: 1, writable: false}); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: arr, name: '3', type: 'delete', oldValue: 'd' }, + { object: arr, name: '2', type: 'delete' }, + { object: arr, name: 'length', type: 'update', oldValue: 4 }, + { object: arr, name: '1', type: 'delete', oldValue: 'b' }, + { object: arr, name: 'length', type: 'update', oldValue: 2 }, + { object: arr, name: 'length', type: 'update', oldValue: 1 }, + { object: arr, name: 'length', type: 'reconfigure' }, + { object: arr2, name: '1', type: 'delete', oldValue: 'beta' }, + { object: arr2, name: 'length', type: 'update', oldValue: 2 }, + { object: arr2, name: 'length', type: 'reconfigure' }, + { object: arr3, name: '2', type: 'delete', oldValue: 'goodbye' }, + { object: arr3, name: '0', type: 'delete', oldValue: 'hello' }, + { object: arr3, name: 'length', type: 'update', oldValue: 6 }, + { object: arr3, name: 'length', type: 'update', oldValue: 0 }, + { object: arr3, name: 'length', type: 'update', oldValue: 1 }, + { object: arr3, name: 'length', type: 'update', oldValue: 2 }, + { object: arr3, name: 'length', type: 'update', oldValue: 1 }, + { object: arr3, name: '4', type: 'add' }, + { object: arr3, name: '4', type: 'delete', oldValue: 5 }, + // TODO(rafaelw): It breaks spec compliance to get two records here. + // When the TODO in v8natives.js::DefineArrayProperty is addressed + // which prevents DefineProperty from over-writing the magic length + // property, these will collapse into a single record. + { object: arr3, name: 'length', type: 'update', oldValue: 5 }, + { object: arr3, name: 'length', type: 'reconfigure' } +]); +Object.deliverChangeRecords(observer2.callback); +observer2.assertCallbackRecords([ + { object: arr, type: 'splice', index: 2, removed: [, 'd'], addedCount: 0 }, + { object: arr, type: 'splice', index: 1, removed: ['b'], addedCount: 0 }, + { object: arr, type: 'splice', index: 1, removed: [], addedCount: 9 }, + { object: arr2, type: 'splice', index: 1, removed: ['beta'], addedCount: 0 }, + { object: arr3, type: 'splice', index: 0, removed: ['hello',, 'goodbye',,,,], addedCount: 0 }, + { object: arr3, type: 'splice', index: 0, removed: [], addedCount: 1 }, + { object: arr3, type: 'splice', index: 1, removed: [], addedCount: 1 }, + { object: arr3, type: 'splice', index: 1, removed: [,], addedCount: 0 }, + { object: arr3, type: 'splice', index: 1, removed: [], addedCount: 4 }, + { object: arr3, name: '4', type: 'add' }, + { object: arr3, type: 'splice', index: 1, removed: [,,,5], addedCount: 0 } +]); + + +// Updating length on large (slow) array +reset(); +var slow_arr = new Array(1000000000); +slow_arr[500000000] = 'hello'; +Object.observe(slow_arr, observer.callback); +var spliceRecords; +function slowSpliceCallback(records) { + spliceRecords = records; +} +Array.observe(slow_arr, slowSpliceCallback); +slow_arr.length = 100; +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: slow_arr, name: '500000000', type: 'delete', oldValue: 'hello' }, + { object: slow_arr, name: 'length', type: 'update', oldValue: 1000000000 }, +]); +Object.deliverChangeRecords(slowSpliceCallback); +assertEquals(spliceRecords.length, 1); +// Have to custom assert this splice record because the removed array is huge. +var splice = spliceRecords[0]; +assertSame(splice.object, slow_arr); +assertEquals(splice.type, 'splice'); +assertEquals(splice.index, 100); +assertEquals(splice.addedCount, 0); +var array_keys = %GetArrayKeys(splice.removed, splice.removed.length); +assertEquals(array_keys.length, 1); +assertEquals(array_keys[0], 499999900); +assertEquals(splice.removed[499999900], 'hello'); +assertEquals(splice.removed.length, 999999900); + + +// Assignments in loops (checking different IC states). +reset(); +var obj = {}; +Object.observe(obj, observer.callback); +for (var i = 0; i < 5; i++) { + obj["a" + i] = i; +} +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, name: "a0", type: "add" }, + { object: obj, name: "a1", type: "add" }, + { object: obj, name: "a2", type: "add" }, + { object: obj, name: "a3", type: "add" }, + { object: obj, name: "a4", type: "add" }, +]); + +reset(); +var obj = {}; +Object.observe(obj, observer.callback); +for (var i = 0; i < 5; i++) { + obj[i] = i; +} +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, name: "0", type: "add" }, + { object: obj, name: "1", type: "add" }, + { object: obj, name: "2", type: "add" }, + { object: obj, name: "3", type: "add" }, + { object: obj, name: "4", type: "add" }, +]); + + +// Adding elements past the end of an array should notify on length for +// Object.observe and emit "splices" for Array.observe. +reset(); +var arr = [1, 2, 3]; +Object.observe(arr, observer.callback); +Array.observe(arr, observer2.callback); +arr[3] = 10; +arr[100] = 20; +Object.defineProperty(arr, '200', {value: 7}); +Object.defineProperty(arr, '400', {get: function(){}}); +arr[50] = 30; // no length change expected +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: arr, name: '3', type: 'add' }, + { object: arr, name: 'length', type: 'update', oldValue: 3 }, + { object: arr, name: '100', type: 'add' }, + { object: arr, name: 'length', type: 'update', oldValue: 4 }, + { object: arr, name: '200', type: 'add' }, + { object: arr, name: 'length', type: 'update', oldValue: 101 }, + { object: arr, name: '400', type: 'add' }, + { object: arr, name: 'length', type: 'update', oldValue: 201 }, + { object: arr, name: '50', type: 'add' }, +]); +Object.deliverChangeRecords(observer2.callback); +observer2.assertCallbackRecords([ + { object: arr, type: 'splice', index: 3, removed: [], addedCount: 1 }, + { object: arr, type: 'splice', index: 4, removed: [], addedCount: 97 }, + { object: arr, type: 'splice', index: 101, removed: [], addedCount: 100 }, + { object: arr, type: 'splice', index: 201, removed: [], addedCount: 200 }, + { object: arr, type: 'add', name: '50' }, +]); + + +// Tests for array methods, first on arrays and then on plain objects +// +// === ARRAYS === +// +// Push +reset(); +var array = [1, 2]; +Object.observe(array, observer.callback); +Array.observe(array, observer2.callback); +array.push(3, 4); +array.push(5); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, name: '2', type: 'add' }, + { object: array, name: 'length', type: 'update', oldValue: 2 }, + { object: array, name: '3', type: 'add' }, + { object: array, name: 'length', type: 'update', oldValue: 3 }, + { object: array, name: '4', type: 'add' }, + { object: array, name: 'length', type: 'update', oldValue: 4 }, +]); +Object.deliverChangeRecords(observer2.callback); +observer2.assertCallbackRecords([ + { object: array, type: 'splice', index: 2, removed: [], addedCount: 2 }, + { object: array, type: 'splice', index: 4, removed: [], addedCount: 1 } +]); + +// Pop +reset(); +var array = [1, 2]; +Object.observe(array, observer.callback); +array.pop(); +array.pop(); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, name: '1', type: 'delete', oldValue: 2 }, + { object: array, name: 'length', type: 'update', oldValue: 2 }, + { object: array, name: '0', type: 'delete', oldValue: 1 }, + { object: array, name: 'length', type: 'update', oldValue: 1 }, +]); + +// Shift +reset(); +var array = [1, 2]; +Object.observe(array, observer.callback); +array.shift(); +array.shift(); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, name: '0', type: 'update', oldValue: 1 }, + { object: array, name: '1', type: 'delete', oldValue: 2 }, + { object: array, name: 'length', type: 'update', oldValue: 2 }, + { object: array, name: '0', type: 'delete', oldValue: 2 }, + { object: array, name: 'length', type: 'update', oldValue: 1 }, +]); + +// Unshift +reset(); +var array = [1, 2]; +Object.observe(array, observer.callback); +array.unshift(3, 4); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, name: '3', type: 'add' }, + { object: array, name: 'length', type: 'update', oldValue: 2 }, + { object: array, name: '2', type: 'add' }, + { object: array, name: '0', type: 'update', oldValue: 1 }, + { object: array, name: '1', type: 'update', oldValue: 2 }, +]); + +// Splice +reset(); +var array = [1, 2, 3]; +Object.observe(array, observer.callback); +array.splice(1, 1, 4, 5); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, name: '3', type: 'add' }, + { object: array, name: 'length', type: 'update', oldValue: 3 }, + { object: array, name: '1', type: 'update', oldValue: 2 }, + { object: array, name: '2', type: 'update', oldValue: 3 }, +]); + +// Sort +reset(); +var array = [3, 2, 1]; +Object.observe(array, observer.callback); +array.sort(); +assertEquals(1, array[0]); +assertEquals(2, array[1]); +assertEquals(3, array[2]); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, name: '1', type: 'update', oldValue: 2 }, + { object: array, name: '0', type: 'update', oldValue: 3 }, + { object: array, name: '2', type: 'update', oldValue: 1 }, + { object: array, name: '1', type: 'update', oldValue: 3 }, + { object: array, name: '0', type: 'update', oldValue: 2 }, +]); + +// Splice emitted after Array mutation methods +function MockArray(initial, observer) { + for (var i = 0; i < initial.length; i++) + this[i] = initial[i]; + + this.length_ = initial.length; + this.observer = observer; +} +MockArray.prototype = { + set length(length) { + Object.getNotifier(this).notify({ type: 'lengthChange' }); + this.length_ = length; + Object.observe(this, this.observer.callback, ['splice']); + }, + get length() { + return this.length_; + } +} + +reset(); +var array = new MockArray([], observer); +Object.observe(array, observer.callback, ['lengthChange']); +Array.prototype.push.call(array, 1); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, type: 'lengthChange' }, + { object: array, type: 'splice', index: 0, removed: [], addedCount: 1 }, +]); + +reset(); +var array = new MockArray([1], observer); +Object.observe(array, observer.callback, ['lengthChange']); +Array.prototype.pop.call(array); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, type: 'lengthChange' }, + { object: array, type: 'splice', index: 0, removed: [1], addedCount: 0 }, +]); + +reset(); +var array = new MockArray([1], observer); +Object.observe(array, observer.callback, ['lengthChange']); +Array.prototype.shift.call(array); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, type: 'lengthChange' }, + { object: array, type: 'splice', index: 0, removed: [1], addedCount: 0 }, +]); + +reset(); +var array = new MockArray([], observer); +Object.observe(array, observer.callback, ['lengthChange']); +Array.prototype.unshift.call(array, 1); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, type: 'lengthChange' }, + { object: array, type: 'splice', index: 0, removed: [], addedCount: 1 }, +]); + +reset(); +var array = new MockArray([0, 1, 2], observer); +Object.observe(array, observer.callback, ['lengthChange']); +Array.prototype.splice.call(array, 1, 1); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, type: 'lengthChange' }, + { object: array, type: 'splice', index: 1, removed: [1], addedCount: 0 }, +]); + +// +// === PLAIN OBJECTS === +// +// Push +reset() +var array = {0: 1, 1: 2, length: 2} +Object.observe(array, observer.callback); +Array.prototype.push.call(array, 3, 4); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, name: '2', type: 'add' }, + { object: array, name: '3', type: 'add' }, + { object: array, name: 'length', type: 'update', oldValue: 2 }, +]); + +// Pop +reset(); +var array = [1, 2]; +Object.observe(array, observer.callback); +Array.observe(array, observer2.callback); +array.pop(); +array.pop(); +array.pop(); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, name: '1', type: 'delete', oldValue: 2 }, + { object: array, name: 'length', type: 'update', oldValue: 2 }, + { object: array, name: '0', type: 'delete', oldValue: 1 }, + { object: array, name: 'length', type: 'update', oldValue: 1 }, +]); +Object.deliverChangeRecords(observer2.callback); +observer2.assertCallbackRecords([ + { object: array, type: 'splice', index: 1, removed: [2], addedCount: 0 }, + { object: array, type: 'splice', index: 0, removed: [1], addedCount: 0 } +]); + +// Shift +reset(); +var array = [1, 2]; +Object.observe(array, observer.callback); +Array.observe(array, observer2.callback); +array.shift(); +array.shift(); +array.shift(); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, name: '0', type: 'update', oldValue: 1 }, + { object: array, name: '1', type: 'delete', oldValue: 2 }, + { object: array, name: 'length', type: 'update', oldValue: 2 }, + { object: array, name: '0', type: 'delete', oldValue: 2 }, + { object: array, name: 'length', type: 'update', oldValue: 1 }, +]); +Object.deliverChangeRecords(observer2.callback); +observer2.assertCallbackRecords([ + { object: array, type: 'splice', index: 0, removed: [1], addedCount: 0 }, + { object: array, type: 'splice', index: 0, removed: [2], addedCount: 0 } +]); + +// Unshift +reset(); +var array = [1, 2]; +Object.observe(array, observer.callback); +Array.observe(array, observer2.callback); +array.unshift(3, 4); +array.unshift(5); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, name: '3', type: 'add' }, + { object: array, name: 'length', type: 'update', oldValue: 2 }, + { object: array, name: '2', type: 'add' }, + { object: array, name: '0', type: 'update', oldValue: 1 }, + { object: array, name: '1', type: 'update', oldValue: 2 }, + { object: array, name: '4', type: 'add' }, + { object: array, name: 'length', type: 'update', oldValue: 4 }, + { object: array, name: '3', type: 'update', oldValue: 2 }, + { object: array, name: '2', type: 'update', oldValue: 1 }, + { object: array, name: '1', type: 'update', oldValue: 4 }, + { object: array, name: '0', type: 'update', oldValue: 3 }, +]); +Object.deliverChangeRecords(observer2.callback); +observer2.assertCallbackRecords([ + { object: array, type: 'splice', index: 0, removed: [], addedCount: 2 }, + { object: array, type: 'splice', index: 0, removed: [], addedCount: 1 } +]); + +// Splice +reset(); +var array = [1, 2, 3]; +Object.observe(array, observer.callback); +Array.observe(array, observer2.callback); +array.splice(1, 0, 4, 5); // 1 4 5 2 3 +array.splice(0, 2); // 5 2 3 +array.splice(1, 2, 6, 7); // 5 6 7 +array.splice(2, 0); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, name: '4', type: 'add' }, + { object: array, name: 'length', type: 'update', oldValue: 3 }, + { object: array, name: '3', type: 'add' }, + { object: array, name: '1', type: 'update', oldValue: 2 }, + { object: array, name: '2', type: 'update', oldValue: 3 }, + + { object: array, name: '0', type: 'update', oldValue: 1 }, + { object: array, name: '1', type: 'update', oldValue: 4 }, + { object: array, name: '2', type: 'update', oldValue: 5 }, + { object: array, name: '4', type: 'delete', oldValue: 3 }, + { object: array, name: '3', type: 'delete', oldValue: 2 }, + { object: array, name: 'length', type: 'update', oldValue: 5 }, + + { object: array, name: '1', type: 'update', oldValue: 2 }, + { object: array, name: '2', type: 'update', oldValue: 3 }, +]); +Object.deliverChangeRecords(observer2.callback); +observer2.assertCallbackRecords([ + { object: array, type: 'splice', index: 1, removed: [], addedCount: 2 }, + { object: array, type: 'splice', index: 0, removed: [1, 4], addedCount: 0 }, + { object: array, type: 'splice', index: 1, removed: [2, 3], addedCount: 2 }, +]); + +// Exercise StoreIC_ArrayLength +reset(); +var dummy = {}; +Object.observe(dummy, observer.callback); +Object.unobserve(dummy, observer.callback); +var array = [0]; +Object.observe(array, observer.callback); +array.splice(0, 1); +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: array, name: '0', type: 'delete', oldValue: 0 }, + { object: array, name: 'length', type: 'update', oldValue: 1}, +]); + + +// __proto__ +reset(); +var obj = {}; +Object.observe(obj, observer.callback); +var p = {foo: 'yes'}; +var q = {bar: 'no'}; +obj.__proto__ = p; +obj.__proto__ = p; // ignored +obj.__proto__ = null; +obj.__proto__ = q; // the __proto__ accessor is gone +// TODO(adamk): Add tests for objects with hidden prototypes +// once we support observing the global object. +Object.deliverChangeRecords(observer.callback); +observer.assertCallbackRecords([ + { object: obj, name: '__proto__', type: 'setPrototype', + oldValue: Object.prototype }, + { object: obj, name: '__proto__', type: 'setPrototype', oldValue: p }, + { object: obj, name: '__proto__', type: 'add' }, +]); + + +// Function.prototype +reset(); +var fun = function(){}; +Object.observe(fun, observer.callback); +var myproto = {foo: 'bar'}; +fun.prototype = myproto; +fun.prototype = 7; +fun.prototype = 7; // ignored +Object.defineProperty(fun, 'prototype', {value: 8}); +Object.deliverChangeRecords(observer.callback); +observer.assertRecordCount(3); +// Manually examine the first record in order to test +// lazy creation of oldValue +assertSame(fun, observer.records[0].object); +assertEquals('prototype', observer.records[0].name); +assertEquals('update', observer.records[0].type); +// The only existing reference to the oldValue object is in this +// record, so to test that lazy creation happened correctly +// we compare its constructor to our function (one of the invariants +// ensured when creating an object via AllocateFunctionPrototype). +assertSame(fun, observer.records[0].oldValue.constructor); +observer.records.splice(0, 1); +observer.assertCallbackRecords([ + { object: fun, name: 'prototype', type: 'update', oldValue: myproto }, + { object: fun, name: 'prototype', type: 'update', oldValue: 7 }, +]); + +// Function.prototype should not be observable except on the object itself +reset(); +var fun = function(){}; +var obj = { __proto__: fun }; +Object.observe(obj, observer.callback); +obj.prototype = 7; +Object.deliverChangeRecords(observer.callback); +observer.assertNotCalled(); + + +// Check that changes in observation status are detected in all IC states and +// in optimized code, especially in cases usually using fast elements. +var mutation = [ + "a[i] = v", + "a[i] ? ++a[i] : a[i] = v", + "a[i] ? a[i]++ : a[i] = v", + "a[i] ? a[i] += 1 : a[i] = v", + "a[i] ? a[i] -= -1 : a[i] = v", +]; + +var props = [1, "1", "a"]; + +function TestFastElements(prop, mutation, prepopulate, polymorphic, optimize) { + var setElement = eval( + "(function setElement(a, i, v) { " + mutation + "; " + + "/* " + [].join.call(arguments, " ") + " */" + + "})" + ); + print("TestFastElements:", setElement); + + var arr = prepopulate ? [1, 2, 3, 4, 5] : [0]; + if (prepopulate) arr[prop] = 2; // for non-element case + setElement(arr, prop, 3); + setElement(arr, prop, 4); + if (polymorphic) setElement(["M", "i", "l", "n", "e", "r"], 0, "m"); + if (optimize) %OptimizeFunctionOnNextCall(setElement); + setElement(arr, prop, 5); + + reset(); + Object.observe(arr, observer.callback); + setElement(arr, prop, 989898); + Object.deliverChangeRecords(observer.callback); + observer.assertCallbackRecords([ + { object: arr, name: "" + prop, type: 'update', oldValue: 5 } + ]); +} + +for (var b1 = 0; b1 < 2; ++b1) + for (var b2 = 0; b2 < 2; ++b2) + for (var b3 = 0; b3 < 2; ++b3) + for (var i in props) + for (var j in mutation) + TestFastElements(props[i], mutation[j], b1 != 0, b2 != 0, b3 != 0); + + +var mutation = [ + "a.length = v", + "a.length += newSize - oldSize", + "a.length -= oldSize - newSize", +]; + +var mutationByIncr = [ + "++a.length", + "a.length++", +]; + +function TestFastElementsLength( + mutation, polymorphic, optimize, oldSize, newSize) { + var setLength = eval( + "(function setLength(a, v) { " + mutation + "; " + + "/* " + [].join.call(arguments, " ") + " */" + + "})" + ); + print("TestFastElementsLength:", setLength); + + function array(n) { + var arr = new Array(n); + for (var i = 0; i < n; ++i) arr[i] = i; + return arr; + } + + setLength(array(oldSize), newSize); + setLength(array(oldSize), newSize); + if (polymorphic) setLength(array(oldSize).map(isNaN), newSize); + if (optimize) %OptimizeFunctionOnNextCall(setLength); + setLength(array(oldSize), newSize); + + reset(); + var arr = array(oldSize); + Object.observe(arr, observer.callback); + setLength(arr, newSize); + Object.deliverChangeRecords(observer.callback); + if (oldSize === newSize) { + observer.assertNotCalled(); + } else { + var count = oldSize > newSize ? oldSize - newSize : 0; + observer.assertRecordCount(count + 1); + var lengthRecord = observer.records[count]; + assertSame(arr, lengthRecord.object); + assertEquals('length', lengthRecord.name); + assertEquals('update', lengthRecord.type); + assertSame(oldSize, lengthRecord.oldValue); + } +} + +for (var b1 = 0; b1 < 2; ++b1) + for (var b2 = 0; b2 < 2; ++b2) + for (var n1 = 0; n1 < 3; ++n1) + for (var n2 = 0; n2 < 3; ++n2) + for (var i in mutation) + TestFastElementsLength(mutation[i], b1 != 0, b2 != 0, 20*n1, 20*n2); + +for (var b1 = 0; b1 < 2; ++b1) + for (var b2 = 0; b2 < 2; ++b2) + for (var n = 0; n < 3; ++n) + for (var i in mutationByIncr) + TestFastElementsLength(mutationByIncr[i], b1 != 0, b2 != 0, 7*n, 7*n+1); |