object-observe.js revision b8a8cc1952d61a2f3a2568848933943a543b5d3e
1// Copyright 2012 the V8 project authors. All rights reserved.
2// Redistribution and use in source and binary forms, with or without
3// modification, are permitted provided that the following conditions are
4// met:
5//
6//     * Redistributions of source code must retain the above copyright
7//       notice, this list of conditions and the following disclaimer.
8//     * Redistributions in binary form must reproduce the above
9//       copyright notice, this list of conditions and the following
10//       disclaimer in the documentation and/or other materials provided
11//       with the distribution.
12//     * Neither the name of Google Inc. nor the names of its
13//       contributors may be used to endorse or promote products derived
14//       from this software without specific prior written permission.
15//
16// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28// Flags: --harmony-proxies
29// Flags: --allow-natives-syntax
30
31var allObservers = [];
32function reset() {
33  allObservers.forEach(function(observer) { observer.reset(); });
34}
35
36function stringifyNoThrow(arg) {
37  try {
38    return JSON.stringify(arg);
39  } catch (e) {
40    return '{<circular reference>}';
41  }
42}
43
44function createObserver() {
45  "use strict";  // So that |this| in callback can be undefined.
46
47  var observer = {
48    records: undefined,
49    callbackCount: 0,
50    reset: function() {
51      this.records = undefined;
52      this.callbackCount = 0;
53    },
54    assertNotCalled: function() {
55      assertEquals(undefined, this.records);
56      assertEquals(0, this.callbackCount);
57    },
58    assertCalled: function() {
59      assertEquals(1, this.callbackCount);
60    },
61    assertRecordCount: function(count) {
62      this.assertCalled();
63      assertEquals(count, this.records.length);
64    },
65    assertCallbackRecords: function(recs) {
66      this.assertRecordCount(recs.length);
67      for (var i = 0; i < recs.length; i++) {
68        if ('name' in recs[i]) recs[i].name = String(recs[i].name);
69        print(i, stringifyNoThrow(this.records[i]), stringifyNoThrow(recs[i]));
70        assertSame(this.records[i].object, recs[i].object);
71        assertEquals('string', typeof recs[i].type);
72        assertPropertiesEqual(this.records[i], recs[i]);
73      }
74    }
75  };
76
77  observer.callback = function(r) {
78    assertEquals(undefined, this);
79    assertEquals('object', typeof r);
80    assertTrue(r instanceof Array)
81    observer.records = r;
82    observer.callbackCount++;
83  };
84
85  observer.reset();
86  allObservers.push(observer);
87  return observer;
88}
89
90var observer = createObserver();
91var observer2 = createObserver();
92
93assertEquals("function", typeof observer.callback);
94assertEquals("function", typeof observer2.callback);
95
96var obj = {};
97
98function frozenFunction() {}
99Object.freeze(frozenFunction);
100var nonFunction = {};
101var changeRecordWithAccessor = { type: 'foo' };
102var recordCreated = false;
103Object.defineProperty(changeRecordWithAccessor, 'name', {
104  get: function() {
105    recordCreated = true;
106    return "bar";
107  },
108  enumerable: true
109})
110
111
112// Object.observe
113assertThrows(function() { Object.observe("non-object", observer.callback); },
114             TypeError);
115assertThrows(function() { Object.observe(this, observer.callback); },
116             TypeError);
117assertThrows(function() { Object.observe(obj, nonFunction); }, TypeError);
118assertThrows(function() { Object.observe(obj, frozenFunction); }, TypeError);
119assertEquals(obj, Object.observe(obj, observer.callback, [1]));
120assertEquals(obj, Object.observe(obj, observer.callback, [true]));
121assertEquals(obj, Object.observe(obj, observer.callback, ['foo', null]));
122assertEquals(obj, Object.observe(obj, observer.callback, [undefined]));
123assertEquals(obj, Object.observe(obj, observer.callback,
124             ['foo', 'bar', 'baz']));
125assertEquals(obj, Object.observe(obj, observer.callback, []));
126assertEquals(obj, Object.observe(obj, observer.callback, undefined));
127assertEquals(obj, Object.observe(obj, observer.callback));
128
129// Object.unobserve
130assertThrows(function() { Object.unobserve(4, observer.callback); }, TypeError);
131assertThrows(function() { Object.unobserve(this, observer.callback); },
132             TypeError);
133assertThrows(function() { Object.unobserve(obj, nonFunction); }, TypeError);
134assertEquals(obj, Object.unobserve(obj, observer.callback));
135
136
137// Object.getNotifier
138var notifier = Object.getNotifier(obj);
139assertSame(notifier, Object.getNotifier(obj));
140assertEquals(null, Object.getNotifier(Object.freeze({})));
141assertThrows(function() { Object.getNotifier(this) }, TypeError);
142assertFalse(notifier.hasOwnProperty('notify'));
143assertEquals([], Object.keys(notifier));
144var notifyDesc = Object.getOwnPropertyDescriptor(notifier.__proto__, 'notify');
145assertTrue(notifyDesc.configurable);
146assertTrue(notifyDesc.writable);
147assertFalse(notifyDesc.enumerable);
148assertThrows(function() { notifier.notify({}); }, TypeError);
149assertThrows(function() { notifier.notify({ type: 4 }); }, TypeError);
150
151assertThrows(function() { notifier.performChange(1, function(){}); }, TypeError);
152assertThrows(function() { notifier.performChange(undefined, function(){}); }, TypeError);
153assertThrows(function() { notifier.performChange('foo', undefined); }, TypeError);
154assertThrows(function() { notifier.performChange('foo', 'bar'); }, TypeError);
155var global = this;
156notifier.performChange('foo', function() {
157  assertEquals(global, this);
158});
159
160var notify = notifier.notify;
161assertThrows(function() { notify.call(undefined, { type: 'a' }); }, TypeError);
162assertThrows(function() { notify.call(null, { type: 'a' }); }, TypeError);
163assertThrows(function() { notify.call(5, { type: 'a' }); }, TypeError);
164assertThrows(function() { notify.call('hello', { type: 'a' }); }, TypeError);
165assertThrows(function() { notify.call(false, { type: 'a' }); }, TypeError);
166assertThrows(function() { notify.call({}, { type: 'a' }); }, TypeError);
167assertFalse(recordCreated);
168notifier.notify(changeRecordWithAccessor);
169assertFalse(recordCreated);  // not observed yet
170
171
172// Object.deliverChangeRecords
173assertThrows(function() { Object.deliverChangeRecords(nonFunction); }, TypeError);
174
175Object.observe(obj, observer.callback);
176
177
178// notify uses to [[CreateOwnProperty]] to create changeRecord;
179reset();
180var protoExpandoAccessed = false;
181Object.defineProperty(Object.prototype, 'protoExpando',
182  {
183    configurable: true,
184    set: function() { protoExpandoAccessed = true; }
185  }
186);
187notifier.notify({ type: 'foo', protoExpando: 'val'});
188assertFalse(protoExpandoAccessed);
189delete Object.prototype.protoExpando;
190Object.deliverChangeRecords(observer.callback);
191
192
193// Multiple records are delivered.
194reset();
195notifier.notify({
196  type: 'update',
197  name: 'foo',
198  expando: 1
199});
200
201notifier.notify({
202  object: notifier,  // object property is ignored
203  type: 'delete',
204  name: 'bar',
205  expando2: 'str'
206});
207Object.deliverChangeRecords(observer.callback);
208observer.assertCallbackRecords([
209  { object: obj, name: 'foo', type: 'update', expando: 1 },
210  { object: obj, name: 'bar', type: 'delete', expando2: 'str' }
211]);
212
213// Non-string accept values are coerced to strings
214reset();
215Object.observe(obj, observer.callback, [true, 1, null, undefined]);
216notifier = Object.getNotifier(obj);
217notifier.notify({ type: 'true' });
218notifier.notify({ type: 'false' });
219notifier.notify({ type: '1' });
220notifier.notify({ type: '-1' });
221notifier.notify({ type: 'null' });
222notifier.notify({ type: 'nill' });
223notifier.notify({ type: 'undefined' });
224notifier.notify({ type: 'defined' });
225Object.deliverChangeRecords(observer.callback);
226observer.assertCallbackRecords([
227  { object: obj, type: 'true' },
228  { object: obj, type: '1' },
229  { object: obj, type: 'null' },
230  { object: obj, type: 'undefined' }
231]);
232
233// No delivery takes place if no records are pending
234reset();
235Object.deliverChangeRecords(observer.callback);
236observer.assertNotCalled();
237
238
239// Multiple observation has no effect.
240reset();
241Object.observe(obj, observer.callback);
242Object.observe(obj, observer.callback);
243Object.getNotifier(obj).notify({
244  type: 'update',
245});
246Object.deliverChangeRecords(observer.callback);
247observer.assertCalled();
248
249
250// Observation can be stopped.
251reset();
252Object.unobserve(obj, observer.callback);
253Object.getNotifier(obj).notify({
254  type: 'update',
255});
256Object.deliverChangeRecords(observer.callback);
257observer.assertNotCalled();
258
259
260// Multiple unobservation has no effect
261reset();
262Object.unobserve(obj, observer.callback);
263Object.unobserve(obj, observer.callback);
264Object.getNotifier(obj).notify({
265  type: 'update',
266});
267Object.deliverChangeRecords(observer.callback);
268observer.assertNotCalled();
269
270
271// Re-observation works and only includes changeRecords after of call.
272reset();
273Object.getNotifier(obj).notify({
274  type: 'update',
275});
276Object.observe(obj, observer.callback);
277Object.getNotifier(obj).notify({
278  type: 'update',
279});
280records = undefined;
281Object.deliverChangeRecords(observer.callback);
282observer.assertRecordCount(1);
283
284// Get notifier prior to observing
285reset();
286var obj = {};
287Object.getNotifier(obj);
288Object.observe(obj, observer.callback);
289obj.id = 1;
290Object.deliverChangeRecords(observer.callback);
291observer.assertCallbackRecords([
292  { object: obj, type: 'add', name: 'id' },
293]);
294
295// The empty-string property is observable
296reset();
297var obj = {};
298Object.observe(obj, observer.callback);
299obj[''] = '';
300obj[''] = ' ';
301delete obj[''];
302Object.deliverChangeRecords(observer.callback);
303observer.assertCallbackRecords([
304  { object: obj, type: 'add', name: '' },
305  { object: obj, type: 'update', name: '', oldValue: '' },
306  { object: obj, type: 'delete', name: '', oldValue: ' ' },
307]);
308
309// Object.preventExtensions
310reset();
311var obj = { foo: 'bar'};
312Object.observe(obj, observer.callback);
313obj.baz = 'bat';
314Object.preventExtensions(obj);
315
316Object.deliverChangeRecords(observer.callback);
317observer.assertCallbackRecords([
318  { object: obj, type: 'add', name: 'baz' },
319  { object: obj, type: 'preventExtensions' },
320]);
321
322reset();
323var obj = { foo: 'bar'};
324Object.preventExtensions(obj);
325Object.observe(obj, observer.callback);
326Object.preventExtensions(obj);
327Object.deliverChangeRecords(observer.callback);
328observer.assertNotCalled();
329
330// Object.freeze
331reset();
332var obj = { a: 'a' };
333Object.defineProperty(obj, 'b', {
334  writable: false,
335  configurable: true,
336  value: 'b'
337});
338Object.defineProperty(obj, 'c', {
339  writable: true,
340  configurable: false,
341  value: 'c'
342});
343Object.defineProperty(obj, 'd', {
344  writable: false,
345  configurable: false,
346  value: 'd'
347});
348Object.observe(obj, observer.callback);
349Object.freeze(obj);
350
351Object.deliverChangeRecords(observer.callback);
352observer.assertCallbackRecords([
353  { object: obj, type: 'reconfigure', name: 'a' },
354  { object: obj, type: 'reconfigure', name: 'b' },
355  { object: obj, type: 'reconfigure', name: 'c' },
356  { object: obj, type: 'preventExtensions' },
357]);
358
359reset();
360var obj = { foo: 'bar'};
361Object.freeze(obj);
362Object.observe(obj, observer.callback);
363Object.freeze(obj);
364Object.deliverChangeRecords(observer.callback);
365observer.assertNotCalled();
366
367// Object.seal
368reset();
369var obj = { a: 'a' };
370Object.defineProperty(obj, 'b', {
371  writable: false,
372  configurable: true,
373  value: 'b'
374});
375Object.defineProperty(obj, 'c', {
376  writable: true,
377  configurable: false,
378  value: 'c'
379});
380Object.defineProperty(obj, 'd', {
381  writable: false,
382  configurable: false,
383  value: 'd'
384});
385Object.observe(obj, observer.callback);
386Object.seal(obj);
387
388Object.deliverChangeRecords(observer.callback);
389observer.assertCallbackRecords([
390  { object: obj, type: 'reconfigure', name: 'a' },
391  { object: obj, type: 'reconfigure', name: 'b' },
392  { object: obj, type: 'preventExtensions' },
393]);
394
395reset();
396var obj = { foo: 'bar'};
397Object.seal(obj);
398Object.observe(obj, observer.callback);
399Object.seal(obj);
400Object.deliverChangeRecords(observer.callback);
401observer.assertNotCalled();
402
403// Observing a continuous stream of changes, while itermittantly unobserving.
404reset();
405var obj = {};
406Object.observe(obj, observer.callback);
407Object.getNotifier(obj).notify({
408  type: 'update',
409  val: 1
410});
411
412Object.unobserve(obj, observer.callback);
413Object.getNotifier(obj).notify({
414  type: 'update',
415  val: 2
416});
417
418Object.observe(obj, observer.callback);
419Object.getNotifier(obj).notify({
420  type: 'update',
421  val: 3
422});
423
424Object.unobserve(obj, observer.callback);
425Object.getNotifier(obj).notify({
426  type: 'update',
427  val: 4
428});
429
430Object.observe(obj, observer.callback);
431Object.getNotifier(obj).notify({
432  type: 'update',
433  val: 5
434});
435
436Object.unobserve(obj, observer.callback);
437Object.deliverChangeRecords(observer.callback);
438observer.assertCallbackRecords([
439  { object: obj, type: 'update', val: 1 },
440  { object: obj, type: 'update', val: 3 },
441  { object: obj, type: 'update', val: 5 }
442]);
443
444// Accept
445reset();
446Object.observe(obj, observer.callback, ['somethingElse']);
447Object.getNotifier(obj).notify({
448  type: 'add'
449});
450Object.getNotifier(obj).notify({
451  type: 'update'
452});
453Object.getNotifier(obj).notify({
454  type: 'delete'
455});
456Object.getNotifier(obj).notify({
457  type: 'reconfigure'
458});
459Object.getNotifier(obj).notify({
460  type: 'setPrototype'
461});
462Object.deliverChangeRecords(observer.callback);
463observer.assertNotCalled();
464
465reset();
466Object.observe(obj, observer.callback, ['add', 'delete', 'setPrototype']);
467Object.getNotifier(obj).notify({
468  type: 'add'
469});
470Object.getNotifier(obj).notify({
471  type: 'update'
472});
473Object.getNotifier(obj).notify({
474  type: 'delete'
475});
476Object.getNotifier(obj).notify({
477  type: 'delete'
478});
479Object.getNotifier(obj).notify({
480  type: 'reconfigure'
481});
482Object.getNotifier(obj).notify({
483  type: 'setPrototype'
484});
485Object.deliverChangeRecords(observer.callback);
486observer.assertCallbackRecords([
487  { object: obj, type: 'add' },
488  { object: obj, type: 'delete' },
489  { object: obj, type: 'delete' },
490  { object: obj, type: 'setPrototype' }
491]);
492
493reset();
494Object.observe(obj, observer.callback, ['update', 'foo']);
495Object.getNotifier(obj).notify({
496  type: 'add'
497});
498Object.getNotifier(obj).notify({
499  type: 'update'
500});
501Object.getNotifier(obj).notify({
502  type: 'delete'
503});
504Object.getNotifier(obj).notify({
505  type: 'foo'
506});
507Object.getNotifier(obj).notify({
508  type: 'bar'
509});
510Object.getNotifier(obj).notify({
511  type: 'foo'
512});
513Object.deliverChangeRecords(observer.callback);
514observer.assertCallbackRecords([
515  { object: obj, type: 'update' },
516  { object: obj, type: 'foo' },
517  { object: obj, type: 'foo' }
518]);
519
520reset();
521function Thingy(a, b, c) {
522  this.a = a;
523  this.b = b;
524}
525
526Thingy.MULTIPLY = 'multiply';
527Thingy.INCREMENT = 'increment';
528Thingy.INCREMENT_AND_MULTIPLY = 'incrementAndMultiply';
529
530Thingy.prototype = {
531  increment: function(amount) {
532    var notifier = Object.getNotifier(this);
533
534    var self = this;
535    notifier.performChange(Thingy.INCREMENT, function() {
536      self.a += amount;
537      self.b += amount;
538
539      return {
540        incremented: amount
541      };  // implicit notify
542    });
543  },
544
545  multiply: function(amount) {
546    var notifier = Object.getNotifier(this);
547
548    var self = this;
549    notifier.performChange(Thingy.MULTIPLY, function() {
550      self.a *= amount;
551      self.b *= amount;
552
553      return {
554        multiplied: amount
555      };  // implicit notify
556    });
557  },
558
559  incrementAndMultiply: function(incAmount, multAmount) {
560    var notifier = Object.getNotifier(this);
561
562    var self = this;
563    notifier.performChange(Thingy.INCREMENT_AND_MULTIPLY, function() {
564      self.increment(incAmount);
565      self.multiply(multAmount);
566
567      return {
568        incremented: incAmount,
569        multiplied: multAmount
570      };  // implicit notify
571    });
572  }
573}
574
575Thingy.observe = function(thingy, callback) {
576  Object.observe(thingy, callback, [Thingy.INCREMENT,
577                                    Thingy.MULTIPLY,
578                                    Thingy.INCREMENT_AND_MULTIPLY,
579                                    'update']);
580}
581
582Thingy.unobserve = function(thingy, callback) {
583  Object.unobserve(thingy);
584}
585
586var thingy = new Thingy(2, 4);
587
588Object.observe(thingy, observer.callback);
589Thingy.observe(thingy, observer2.callback);
590thingy.increment(3);               // { a: 5, b: 7 }
591thingy.b++;                        // { a: 5, b: 8 }
592thingy.multiply(2);                // { a: 10, b: 16 }
593thingy.a++;                        // { a: 11, b: 16 }
594thingy.incrementAndMultiply(2, 2); // { a: 26, b: 36 }
595
596Object.deliverChangeRecords(observer.callback);
597Object.deliverChangeRecords(observer2.callback);
598observer.assertCallbackRecords([
599  { object: thingy, type: 'update', name: 'a', oldValue: 2 },
600  { object: thingy, type: 'update', name: 'b', oldValue: 4 },
601  { object: thingy, type: 'update', name: 'b', oldValue: 7 },
602  { object: thingy, type: 'update', name: 'a', oldValue: 5 },
603  { object: thingy, type: 'update', name: 'b', oldValue: 8 },
604  { object: thingy, type: 'update', name: 'a', oldValue: 10 },
605  { object: thingy, type: 'update', name: 'a', oldValue: 11 },
606  { object: thingy, type: 'update', name: 'b', oldValue: 16 },
607  { object: thingy, type: 'update', name: 'a', oldValue: 13 },
608  { object: thingy, type: 'update', name: 'b', oldValue: 18 },
609]);
610observer2.assertCallbackRecords([
611  { object: thingy, type: Thingy.INCREMENT, incremented: 3 },
612  { object: thingy, type: 'update', name: 'b', oldValue: 7 },
613  { object: thingy, type: Thingy.MULTIPLY, multiplied: 2 },
614  { object: thingy, type: 'update', name: 'a', oldValue: 10 },
615  {
616    object: thingy,
617    type: Thingy.INCREMENT_AND_MULTIPLY,
618    incremented: 2,
619    multiplied: 2
620  }
621]);
622
623// ArrayPush cached stub
624reset();
625
626function pushMultiple(arr) {
627  arr.push('a');
628  arr.push('b');
629  arr.push('c');
630}
631
632for (var i = 0; i < 5; i++) {
633  var arr = [];
634  pushMultiple(arr);
635}
636
637for (var i = 0; i < 5; i++) {
638  reset();
639  var arr = [];
640  Object.observe(arr, observer.callback);
641  pushMultiple(arr);
642  Object.unobserve(arr, observer.callback);
643  Object.deliverChangeRecords(observer.callback);
644  observer.assertCallbackRecords([
645    { object: arr, type: 'add', name: '0' },
646    { object: arr, type: 'update', name: 'length', oldValue: 0 },
647    { object: arr, type: 'add', name: '1' },
648    { object: arr, type: 'update', name: 'length', oldValue: 1 },
649    { object: arr, type: 'add', name: '2' },
650    { object: arr, type: 'update', name: 'length', oldValue: 2 },
651  ]);
652}
653
654
655// ArrayPop cached stub
656reset();
657
658function popMultiple(arr) {
659  arr.pop();
660  arr.pop();
661  arr.pop();
662}
663
664for (var i = 0; i < 5; i++) {
665  var arr = ['a', 'b', 'c'];
666  popMultiple(arr);
667}
668
669for (var i = 0; i < 5; i++) {
670  reset();
671  var arr = ['a', 'b', 'c'];
672  Object.observe(arr, observer.callback);
673  popMultiple(arr);
674  Object.unobserve(arr, observer.callback);
675  Object.deliverChangeRecords(observer.callback);
676  observer.assertCallbackRecords([
677    { object: arr, type: 'delete', name: '2', oldValue: 'c' },
678    { object: arr, type: 'update', name: 'length', oldValue: 3 },
679    { object: arr, type: 'delete', name: '1', oldValue: 'b' },
680    { object: arr, type: 'update', name: 'length', oldValue: 2 },
681    { object: arr, type: 'delete', name: '0', oldValue: 'a' },
682    { object: arr, type: 'update', name: 'length', oldValue: 1 },
683  ]);
684}
685
686
687reset();
688function RecursiveThingy() {}
689
690RecursiveThingy.MULTIPLY_FIRST_N = 'multiplyFirstN';
691
692RecursiveThingy.prototype = {
693  __proto__: Array.prototype,
694
695  multiplyFirstN: function(amount, n) {
696    if (!n)
697      return;
698    var notifier = Object.getNotifier(this);
699    var self = this;
700    notifier.performChange(RecursiveThingy.MULTIPLY_FIRST_N, function() {
701      self[n-1] = self[n-1]*amount;
702      self.multiplyFirstN(amount, n-1);
703    });
704
705    notifier.notify({
706      type: RecursiveThingy.MULTIPLY_FIRST_N,
707      multiplied: amount,
708      n: n
709    });
710  },
711}
712
713RecursiveThingy.observe = function(thingy, callback) {
714  Object.observe(thingy, callback, [RecursiveThingy.MULTIPLY_FIRST_N]);
715}
716
717RecursiveThingy.unobserve = function(thingy, callback) {
718  Object.unobserve(thingy);
719}
720
721var thingy = new RecursiveThingy;
722thingy.push(1, 2, 3, 4);
723
724Object.observe(thingy, observer.callback);
725RecursiveThingy.observe(thingy, observer2.callback);
726thingy.multiplyFirstN(2, 3);                // [2, 4, 6, 4]
727
728Object.deliverChangeRecords(observer.callback);
729Object.deliverChangeRecords(observer2.callback);
730observer.assertCallbackRecords([
731  { object: thingy, type: 'update', name: '2', oldValue: 3 },
732  { object: thingy, type: 'update', name: '1', oldValue: 2 },
733  { object: thingy, type: 'update', name: '0', oldValue: 1 }
734]);
735observer2.assertCallbackRecords([
736  { object: thingy, type: RecursiveThingy.MULTIPLY_FIRST_N, multiplied: 2, n: 3 }
737]);
738
739reset();
740function DeckSuit() {
741  this.push('1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'A', 'Q', 'K');
742}
743
744DeckSuit.SHUFFLE = 'shuffle';
745
746DeckSuit.prototype = {
747  __proto__: Array.prototype,
748
749  shuffle: function() {
750    var notifier = Object.getNotifier(this);
751    var self = this;
752    notifier.performChange(DeckSuit.SHUFFLE, function() {
753      self.reverse();
754      self.sort(function() { return Math.random()* 2 - 1; });
755      var cut = self.splice(0, 6);
756      Array.prototype.push.apply(self, cut);
757      self.reverse();
758      self.sort(function() { return Math.random()* 2 - 1; });
759      var cut = self.splice(0, 6);
760      Array.prototype.push.apply(self, cut);
761      self.reverse();
762      self.sort(function() { return Math.random()* 2 - 1; });
763    });
764
765    notifier.notify({
766      type: DeckSuit.SHUFFLE
767    });
768  },
769}
770
771DeckSuit.observe = function(thingy, callback) {
772  Object.observe(thingy, callback, [DeckSuit.SHUFFLE]);
773}
774
775DeckSuit.unobserve = function(thingy, callback) {
776  Object.unobserve(thingy);
777}
778
779var deck = new DeckSuit;
780
781DeckSuit.observe(deck, observer2.callback);
782deck.shuffle();
783
784Object.deliverChangeRecords(observer2.callback);
785observer2.assertCallbackRecords([
786  { object: deck, type: DeckSuit.SHUFFLE }
787]);
788
789// Observing multiple objects; records appear in order.
790reset();
791var obj2 = {};
792var obj3 = {}
793Object.observe(obj, observer.callback);
794Object.observe(obj3, observer.callback);
795Object.observe(obj2, observer.callback);
796Object.getNotifier(obj).notify({
797  type: 'add',
798});
799Object.getNotifier(obj2).notify({
800  type: 'update',
801});
802Object.getNotifier(obj3).notify({
803  type: 'delete',
804});
805Object.observe(obj3, observer.callback);
806Object.deliverChangeRecords(observer.callback);
807observer.assertCallbackRecords([
808  { object: obj, type: 'add' },
809  { object: obj2, type: 'update' },
810  { object: obj3, type: 'delete' }
811]);
812
813
814// Recursive observation.
815var obj = {a: 1};
816var callbackCount = 0;
817function recursiveObserver(r) {
818  assertEquals(1, r.length);
819  ++callbackCount;
820  if (r[0].oldValue < 100) ++obj[r[0].name];
821}
822Object.observe(obj, recursiveObserver);
823++obj.a;
824Object.deliverChangeRecords(recursiveObserver);
825assertEquals(100, callbackCount);
826
827var obj1 = {a: 1};
828var obj2 = {a: 1};
829var recordCount = 0;
830function recursiveObserver2(r) {
831  recordCount += r.length;
832  if (r[0].oldValue < 100) {
833    ++obj1.a;
834    ++obj2.a;
835  }
836}
837Object.observe(obj1, recursiveObserver2);
838Object.observe(obj2, recursiveObserver2);
839++obj1.a;
840Object.deliverChangeRecords(recursiveObserver2);
841assertEquals(199, recordCount);
842
843
844// Observing named properties.
845reset();
846var obj = {a: 1}
847Object.observe(obj, observer.callback);
848obj.a = 2;
849obj["a"] = 3;
850delete obj.a;
851obj.a = 4;
852obj.a = 4;  // ignored
853obj.a = 5;
854Object.defineProperty(obj, "a", {value: 6});
855Object.defineProperty(obj, "a", {writable: false});
856obj.a = 7;  // ignored
857Object.defineProperty(obj, "a", {value: 8});
858Object.defineProperty(obj, "a", {value: 7, writable: true});
859Object.defineProperty(obj, "a", {get: function() {}});
860Object.defineProperty(obj, "a", {get: frozenFunction});
861Object.defineProperty(obj, "a", {get: frozenFunction});  // ignored
862Object.defineProperty(obj, "a", {get: frozenFunction, set: frozenFunction});
863Object.defineProperty(obj, "a", {set: frozenFunction});  // ignored
864Object.defineProperty(obj, "a", {get: undefined, set: frozenFunction});
865delete obj.a;
866delete obj.a;
867Object.defineProperty(obj, "a", {get: function() {}, configurable: true});
868Object.defineProperty(obj, "a", {value: 9, writable: true});
869obj.a = 10;
870++obj.a;
871obj.a++;
872obj.a *= 3;
873delete obj.a;
874Object.defineProperty(obj, "a", {value: 11, configurable: true});
875Object.deliverChangeRecords(observer.callback);
876observer.assertCallbackRecords([
877  { object: obj, name: "a", type: "update", oldValue: 1 },
878  { object: obj, name: "a", type: "update", oldValue: 2 },
879  { object: obj, name: "a", type: "delete", oldValue: 3 },
880  { object: obj, name: "a", type: "add" },
881  { object: obj, name: "a", type: "update", oldValue: 4 },
882  { object: obj, name: "a", type: "update", oldValue: 5 },
883  { object: obj, name: "a", type: "reconfigure" },
884  { object: obj, name: "a", type: "update", oldValue: 6 },
885  { object: obj, name: "a", type: "reconfigure", oldValue: 8 },
886  { object: obj, name: "a", type: "reconfigure", oldValue: 7 },
887  { object: obj, name: "a", type: "reconfigure" },
888  { object: obj, name: "a", type: "reconfigure" },
889  { object: obj, name: "a", type: "reconfigure" },
890  { object: obj, name: "a", type: "delete" },
891  { object: obj, name: "a", type: "add" },
892  { object: obj, name: "a", type: "reconfigure" },
893  { object: obj, name: "a", type: "update", oldValue: 9 },
894  { object: obj, name: "a", type: "update", oldValue: 10 },
895  { object: obj, name: "a", type: "update", oldValue: 11 },
896  { object: obj, name: "a", type: "update", oldValue: 12 },
897  { object: obj, name: "a", type: "delete", oldValue: 36 },
898  { object: obj, name: "a", type: "add" },
899]);
900
901
902// Observing indexed properties.
903reset();
904var obj = {'1': 1}
905Object.observe(obj, observer.callback);
906obj[1] = 2;
907obj[1] = 3;
908delete obj[1];
909obj[1] = 4;
910obj[1] = 4;  // ignored
911obj[1] = 5;
912Object.defineProperty(obj, "1", {value: 6});
913Object.defineProperty(obj, "1", {writable: false});
914obj[1] = 7;  // ignored
915Object.defineProperty(obj, "1", {value: 8});
916Object.defineProperty(obj, "1", {value: 7, writable: true});
917Object.defineProperty(obj, "1", {get: function() {}});
918Object.defineProperty(obj, "1", {get: frozenFunction});
919Object.defineProperty(obj, "1", {get: frozenFunction});  // ignored
920Object.defineProperty(obj, "1", {get: frozenFunction, set: frozenFunction});
921Object.defineProperty(obj, "1", {set: frozenFunction});  // ignored
922Object.defineProperty(obj, "1", {get: undefined, set: frozenFunction});
923delete obj[1];
924delete obj[1];
925Object.defineProperty(obj, "1", {get: function() {}, configurable: true});
926Object.defineProperty(obj, "1", {value: 9, writable: true});
927obj[1] = 10;
928++obj[1];
929obj[1]++;
930obj[1] *= 3;
931delete obj[1];
932Object.defineProperty(obj, "1", {value: 11, configurable: true});
933Object.deliverChangeRecords(observer.callback);
934observer.assertCallbackRecords([
935  { object: obj, name: "1", type: "update", oldValue: 1 },
936  { object: obj, name: "1", type: "update", oldValue: 2 },
937  { object: obj, name: "1", type: "delete", oldValue: 3 },
938  { object: obj, name: "1", type: "add" },
939  { object: obj, name: "1", type: "update", oldValue: 4 },
940  { object: obj, name: "1", type: "update", oldValue: 5 },
941  { object: obj, name: "1", type: "reconfigure" },
942  { object: obj, name: "1", type: "update", oldValue: 6 },
943  { object: obj, name: "1", type: "reconfigure", oldValue: 8 },
944  { object: obj, name: "1", type: "reconfigure", oldValue: 7 },
945  { object: obj, name: "1", type: "reconfigure" },
946  { object: obj, name: "1", type: "reconfigure" },
947  { object: obj, name: "1", type: "reconfigure" },
948  { object: obj, name: "1", type: "delete" },
949  { object: obj, name: "1", type: "add" },
950  { object: obj, name: "1", type: "reconfigure" },
951  { object: obj, name: "1", type: "update", oldValue: 9 },
952  { object: obj, name: "1", type: "update", oldValue: 10 },
953  { object: obj, name: "1", type: "update", oldValue: 11 },
954  { object: obj, name: "1", type: "update", oldValue: 12 },
955  { object: obj, name: "1", type: "delete", oldValue: 36 },
956  { object: obj, name: "1", type: "add" },
957]);
958
959
960// Observing symbol properties (not).
961print("*****")
962reset();
963var obj = {}
964var symbol = Symbol("secret");
965Object.observe(obj, observer.callback);
966obj[symbol] = 3;
967delete obj[symbol];
968Object.defineProperty(obj, symbol, {get: function() {}, configurable: true});
969Object.defineProperty(obj, symbol, {value: 6});
970Object.defineProperty(obj, symbol, {writable: false});
971delete obj[symbol];
972Object.defineProperty(obj, symbol, {value: 7});
973++obj[symbol];
974obj[symbol]++;
975obj[symbol] *= 3;
976delete obj[symbol];
977obj.__defineSetter__(symbol, function() {});
978obj.__defineGetter__(symbol, function() {});
979Object.deliverChangeRecords(observer.callback);
980observer.assertNotCalled();
981
982
983// Test all kinds of objects generically.
984function TestObserveConfigurable(obj, prop) {
985  reset();
986  Object.observe(obj, observer.callback);
987  Object.unobserve(obj, observer.callback);
988  obj[prop] = 1;
989  Object.observe(obj, observer.callback);
990  obj[prop] = 2;
991  obj[prop] = 3;
992  delete obj[prop];
993  obj[prop] = 4;
994  obj[prop] = 4;  // ignored
995  obj[prop] = 5;
996  Object.defineProperty(obj, prop, {value: 6});
997  Object.defineProperty(obj, prop, {writable: false});
998  obj[prop] = 7;  // ignored
999  Object.defineProperty(obj, prop, {value: 8});
1000  Object.defineProperty(obj, prop, {value: 7, writable: true});
1001  Object.defineProperty(obj, prop, {get: function() {}});
1002  Object.defineProperty(obj, prop, {get: frozenFunction});
1003  Object.defineProperty(obj, prop, {get: frozenFunction});  // ignored
1004  Object.defineProperty(obj, prop, {get: frozenFunction, set: frozenFunction});
1005  Object.defineProperty(obj, prop, {set: frozenFunction});  // ignored
1006  Object.defineProperty(obj, prop, {get: undefined, set: frozenFunction});
1007  obj.__defineSetter__(prop, frozenFunction);  // ignored
1008  obj.__defineSetter__(prop, function() {});
1009  obj.__defineGetter__(prop, function() {});
1010  delete obj[prop];
1011  delete obj[prop];  // ignored
1012  obj.__defineGetter__(prop, function() {});
1013  delete obj[prop];
1014  Object.defineProperty(obj, prop, {get: function() {}, configurable: true});
1015  Object.defineProperty(obj, prop, {value: 9, writable: true});
1016  obj[prop] = 10;
1017  ++obj[prop];
1018  obj[prop]++;
1019  obj[prop] *= 3;
1020  delete obj[prop];
1021  Object.defineProperty(obj, prop, {value: 11, configurable: true});
1022  Object.deliverChangeRecords(observer.callback);
1023  observer.assertCallbackRecords([
1024    { object: obj, name: prop, type: "update", oldValue: 1 },
1025    { object: obj, name: prop, type: "update", oldValue: 2 },
1026    { object: obj, name: prop, type: "delete", oldValue: 3 },
1027    { object: obj, name: prop, type: "add" },
1028    { object: obj, name: prop, type: "update", oldValue: 4 },
1029    { object: obj, name: prop, type: "update", oldValue: 5 },
1030    { object: obj, name: prop, type: "reconfigure" },
1031    { object: obj, name: prop, type: "update", oldValue: 6 },
1032    { object: obj, name: prop, type: "reconfigure", oldValue: 8 },
1033    { object: obj, name: prop, type: "reconfigure", oldValue: 7 },
1034    { object: obj, name: prop, type: "reconfigure" },
1035    { object: obj, name: prop, type: "reconfigure" },
1036    { object: obj, name: prop, type: "reconfigure" },
1037    { object: obj, name: prop, type: "reconfigure" },
1038    { object: obj, name: prop, type: "reconfigure" },
1039    { object: obj, name: prop, type: "delete" },
1040    { object: obj, name: prop, type: "add" },
1041    { object: obj, name: prop, type: "delete" },
1042    { object: obj, name: prop, type: "add" },
1043    { object: obj, name: prop, type: "reconfigure" },
1044    { object: obj, name: prop, type: "update", oldValue: 9 },
1045    { object: obj, name: prop, type: "update", oldValue: 10 },
1046    { object: obj, name: prop, type: "update", oldValue: 11 },
1047    { object: obj, name: prop, type: "update", oldValue: 12 },
1048    { object: obj, name: prop, type: "delete", oldValue: 36 },
1049    { object: obj, name: prop, type: "add" },
1050  ]);
1051  Object.unobserve(obj, observer.callback);
1052  delete obj[prop];
1053}
1054
1055function TestObserveNonConfigurable(obj, prop, desc) {
1056  reset();
1057  Object.observe(obj, observer.callback);
1058  Object.unobserve(obj, observer.callback);
1059  obj[prop] = 1;
1060  Object.observe(obj, observer.callback);
1061  obj[prop] = 4;
1062  obj[prop] = 4;  // ignored
1063  obj[prop] = 5;
1064  Object.defineProperty(obj, prop, {value: 6});
1065  Object.defineProperty(obj, prop, {value: 6});  // ignored
1066  Object.defineProperty(obj, prop, {value: 7});
1067  Object.defineProperty(obj, prop, {enumerable: desc.enumerable});  // ignored
1068  Object.defineProperty(obj, prop, {writable: false});
1069  obj[prop] = 7;  // ignored
1070  Object.deliverChangeRecords(observer.callback);
1071  observer.assertCallbackRecords([
1072    { object: obj, name: prop, type: "update", oldValue: 1 },
1073    { object: obj, name: prop, type: "update", oldValue: 4 },
1074    { object: obj, name: prop, type: "update", oldValue: 5 },
1075    { object: obj, name: prop, type: "update", oldValue: 6 },
1076    { object: obj, name: prop, type: "reconfigure" },
1077  ]);
1078  Object.unobserve(obj, observer.callback);
1079}
1080
1081// TODO(rafaelw) Enable when ES6 Proxies are implemented
1082/*
1083function createProxy(create, x) {
1084  var handler = {
1085    getPropertyDescriptor: function(k) {
1086      for (var o = this.target; o; o = Object.getPrototypeOf(o)) {
1087        var desc = Object.getOwnPropertyDescriptor(o, k);
1088        if (desc) return desc;
1089      }
1090      return undefined;
1091    },
1092    getOwnPropertyDescriptor: function(k) {
1093      return Object.getOwnPropertyDescriptor(this.target, k);
1094    },
1095    defineProperty: function(k, desc) {
1096      var x = Object.defineProperty(this.target, k, desc);
1097      Object.deliverChangeRecords(this.callback);
1098      return x;
1099    },
1100    delete: function(k) {
1101      var x = delete this.target[k];
1102      Object.deliverChangeRecords(this.callback);
1103      return x;
1104    },
1105    getPropertyNames: function() {
1106      return Object.getOwnPropertyNames(this.target);
1107    },
1108    target: {isProxy: true},
1109    callback: function(changeRecords) {
1110      print("callback", stringifyNoThrow(handler.proxy), stringifyNoThrow(got));
1111      for (var i in changeRecords) {
1112        var got = changeRecords[i];
1113        var change = {object: handler.proxy, name: got.name, type: got.type};
1114        if ("oldValue" in got) change.oldValue = got.oldValue;
1115        Object.getNotifier(handler.proxy).notify(change);
1116      }
1117    },
1118  };
1119  Object.observe(handler.target, handler.callback);
1120  return handler.proxy = create(handler, x);
1121}
1122*/
1123
1124var objects = [
1125  {},
1126  [],
1127  function(){},
1128  (function(){ return arguments })(),
1129  (function(){ "use strict"; return arguments })(),
1130  Object(1), Object(true), Object("bla"),
1131  new Date(),
1132  Object, Function, Date, RegExp,
1133  new Set, new Map, new WeakMap,
1134  new ArrayBuffer(10), new Int32Array(5)
1135// TODO(rafaelw) Enable when ES6 Proxies are implemented.
1136//  createProxy(Proxy.create, null),
1137//  createProxy(Proxy.createFunction, function(){}),
1138];
1139var properties = ["a", "1", 1, "length", "setPrototype", "name", "caller"];
1140
1141// Cases that yield non-standard results.
1142function blacklisted(obj, prop) {
1143  return (obj instanceof Int32Array && prop == 1) ||
1144         (obj instanceof Int32Array && prop === "length") ||
1145         (obj instanceof ArrayBuffer && prop == 1)
1146}
1147
1148for (var i in objects) for (var j in properties) {
1149  var obj = objects[i];
1150  var prop = properties[j];
1151  if (blacklisted(obj, prop)) continue;
1152  var desc = Object.getOwnPropertyDescriptor(obj, prop);
1153  print("***", typeof obj, stringifyNoThrow(obj), prop);
1154  if (!desc || desc.configurable)
1155    TestObserveConfigurable(obj, prop);
1156  else if (desc.writable)
1157    TestObserveNonConfigurable(obj, prop, desc);
1158}
1159
1160
1161// Observing array length (including truncation)
1162reset();
1163var arr = ['a', 'b', 'c', 'd'];
1164var arr2 = ['alpha', 'beta'];
1165var arr3 = ['hello'];
1166arr3[2] = 'goodbye';
1167arr3.length = 6;
1168Object.defineProperty(arr, '0', {configurable: false});
1169Object.defineProperty(arr, '2', {get: function(){}});
1170Object.defineProperty(arr2, '0', {get: function(){}, configurable: false});
1171Object.observe(arr, observer.callback);
1172Array.observe(arr, observer2.callback);
1173Object.observe(arr2, observer.callback);
1174Array.observe(arr2, observer2.callback);
1175Object.observe(arr3, observer.callback);
1176Array.observe(arr3, observer2.callback);
1177arr.length = 2;
1178arr.length = 0;
1179arr.length = 10;
1180Object.defineProperty(arr, 'length', {writable: false});
1181arr2.length = 0;
1182arr2.length = 1; // no change expected
1183Object.defineProperty(arr2, 'length', {value: 1, writable: false});
1184arr3.length = 0;
1185++arr3.length;
1186arr3.length++;
1187arr3.length /= 2;
1188Object.defineProperty(arr3, 'length', {value: 5});
1189arr3[4] = 5;
1190Object.defineProperty(arr3, 'length', {value: 1, writable: false});
1191Object.deliverChangeRecords(observer.callback);
1192observer.assertCallbackRecords([
1193  { object: arr, name: '3', type: 'delete', oldValue: 'd' },
1194  { object: arr, name: '2', type: 'delete' },
1195  { object: arr, name: 'length', type: 'update', oldValue: 4 },
1196  { object: arr, name: '1', type: 'delete', oldValue: 'b' },
1197  { object: arr, name: 'length', type: 'update', oldValue: 2 },
1198  { object: arr, name: 'length', type: 'update', oldValue: 1 },
1199  { object: arr, name: 'length', type: 'reconfigure' },
1200  { object: arr2, name: '1', type: 'delete', oldValue: 'beta' },
1201  { object: arr2, name: 'length', type: 'update', oldValue: 2 },
1202  { object: arr2, name: 'length', type: 'reconfigure' },
1203  { object: arr3, name: '2', type: 'delete', oldValue: 'goodbye' },
1204  { object: arr3, name: '0', type: 'delete', oldValue: 'hello' },
1205  { object: arr3, name: 'length', type: 'update', oldValue: 6 },
1206  { object: arr3, name: 'length', type: 'update', oldValue: 0 },
1207  { object: arr3, name: 'length', type: 'update', oldValue: 1 },
1208  { object: arr3, name: 'length', type: 'update', oldValue: 2 },
1209  { object: arr3, name: 'length', type: 'update', oldValue: 1 },
1210  { object: arr3, name: '4', type: 'add' },
1211  { object: arr3, name: '4', type: 'delete', oldValue: 5 },
1212  // TODO(rafaelw): It breaks spec compliance to get two records here.
1213  // When the TODO in v8natives.js::DefineArrayProperty is addressed
1214  // which prevents DefineProperty from over-writing the magic length
1215  // property, these will collapse into a single record.
1216  { object: arr3, name: 'length', type: 'update', oldValue: 5 },
1217  { object: arr3, name: 'length', type: 'reconfigure' }
1218]);
1219Object.deliverChangeRecords(observer2.callback);
1220observer2.assertCallbackRecords([
1221  { object: arr, type: 'splice', index: 2, removed: [, 'd'], addedCount: 0 },
1222  { object: arr, type: 'splice', index: 1, removed: ['b'], addedCount: 0 },
1223  { object: arr, type: 'splice', index: 1, removed: [], addedCount: 9 },
1224  { object: arr2, type: 'splice', index: 1, removed: ['beta'], addedCount: 0 },
1225  { object: arr3, type: 'splice', index: 0, removed: ['hello',, 'goodbye',,,,], addedCount: 0 },
1226  { object: arr3, type: 'splice', index: 0, removed: [], addedCount: 1 },
1227  { object: arr3, type: 'splice', index: 1, removed: [], addedCount: 1 },
1228  { object: arr3, type: 'splice', index: 1, removed: [,], addedCount: 0 },
1229  { object: arr3, type: 'splice', index: 1, removed: [], addedCount: 4 },
1230  { object: arr3, name: '4', type: 'add' },
1231  { object: arr3, type: 'splice', index: 1, removed: [,,,5], addedCount: 0 }
1232]);
1233
1234
1235// Updating length on large (slow) array
1236reset();
1237var slow_arr = %NormalizeElements([]);
1238slow_arr[500000000] = 'hello';
1239slow_arr.length = 1000000000;
1240Object.observe(slow_arr, observer.callback);
1241var spliceRecords;
1242function slowSpliceCallback(records) {
1243  spliceRecords = records;
1244}
1245Array.observe(slow_arr, slowSpliceCallback);
1246slow_arr.length = 100;
1247Object.deliverChangeRecords(observer.callback);
1248observer.assertCallbackRecords([
1249  { object: slow_arr, name: '500000000', type: 'delete', oldValue: 'hello' },
1250  { object: slow_arr, name: 'length', type: 'update', oldValue: 1000000000 },
1251]);
1252Object.deliverChangeRecords(slowSpliceCallback);
1253assertEquals(spliceRecords.length, 1);
1254// Have to custom assert this splice record because the removed array is huge.
1255var splice = spliceRecords[0];
1256assertSame(splice.object, slow_arr);
1257assertEquals(splice.type, 'splice');
1258assertEquals(splice.index, 100);
1259assertEquals(splice.addedCount, 0);
1260var array_keys = %GetArrayKeys(splice.removed, splice.removed.length);
1261assertEquals(array_keys.length, 1);
1262assertEquals(array_keys[0], 499999900);
1263assertEquals(splice.removed[499999900], 'hello');
1264assertEquals(splice.removed.length, 999999900);
1265
1266
1267// Assignments in loops (checking different IC states).
1268reset();
1269var obj = {};
1270Object.observe(obj, observer.callback);
1271for (var i = 0; i < 5; i++) {
1272  obj["a" + i] = i;
1273}
1274Object.deliverChangeRecords(observer.callback);
1275observer.assertCallbackRecords([
1276  { object: obj, name: "a0", type: "add" },
1277  { object: obj, name: "a1", type: "add" },
1278  { object: obj, name: "a2", type: "add" },
1279  { object: obj, name: "a3", type: "add" },
1280  { object: obj, name: "a4", type: "add" },
1281]);
1282
1283reset();
1284var obj = {};
1285Object.observe(obj, observer.callback);
1286for (var i = 0; i < 5; i++) {
1287  obj[i] = i;
1288}
1289Object.deliverChangeRecords(observer.callback);
1290observer.assertCallbackRecords([
1291  { object: obj, name: "0", type: "add" },
1292  { object: obj, name: "1", type: "add" },
1293  { object: obj, name: "2", type: "add" },
1294  { object: obj, name: "3", type: "add" },
1295  { object: obj, name: "4", type: "add" },
1296]);
1297
1298
1299// Adding elements past the end of an array should notify on length for
1300// Object.observe and emit "splices" for Array.observe.
1301reset();
1302var arr = [1, 2, 3];
1303Object.observe(arr, observer.callback);
1304Array.observe(arr, observer2.callback);
1305arr[3] = 10;
1306arr[100] = 20;
1307Object.defineProperty(arr, '200', {value: 7});
1308Object.defineProperty(arr, '400', {get: function(){}});
1309arr[50] = 30; // no length change expected
1310Object.deliverChangeRecords(observer.callback);
1311observer.assertCallbackRecords([
1312  { object: arr, name: '3', type: 'add' },
1313  { object: arr, name: 'length', type: 'update', oldValue: 3 },
1314  { object: arr, name: '100', type: 'add' },
1315  { object: arr, name: 'length', type: 'update', oldValue: 4 },
1316  { object: arr, name: '200', type: 'add' },
1317  { object: arr, name: 'length', type: 'update', oldValue: 101 },
1318  { object: arr, name: '400', type: 'add' },
1319  { object: arr, name: 'length', type: 'update', oldValue: 201 },
1320  { object: arr, name: '50', type: 'add' },
1321]);
1322Object.deliverChangeRecords(observer2.callback);
1323observer2.assertCallbackRecords([
1324  { object: arr, type: 'splice', index: 3, removed: [], addedCount: 1 },
1325  { object: arr, type: 'splice', index: 4, removed: [], addedCount: 97 },
1326  { object: arr, type: 'splice', index: 101, removed: [], addedCount: 100 },
1327  { object: arr, type: 'splice', index: 201, removed: [], addedCount: 200 },
1328  { object: arr, type: 'add', name: '50' },
1329]);
1330
1331
1332// Tests for array methods, first on arrays and then on plain objects
1333//
1334// === ARRAYS ===
1335//
1336// Push
1337reset();
1338var array = [1, 2];
1339Object.observe(array, observer.callback);
1340Array.observe(array, observer2.callback);
1341array.push(3, 4);
1342array.push(5);
1343Object.deliverChangeRecords(observer.callback);
1344observer.assertCallbackRecords([
1345  { object: array, name: '2', type: 'add' },
1346  { object: array, name: 'length', type: 'update', oldValue: 2 },
1347  { object: array, name: '3', type: 'add' },
1348  { object: array, name: 'length', type: 'update', oldValue: 3 },
1349  { object: array, name: '4', type: 'add' },
1350  { object: array, name: 'length', type: 'update', oldValue: 4 },
1351]);
1352Object.deliverChangeRecords(observer2.callback);
1353observer2.assertCallbackRecords([
1354  { object: array, type: 'splice', index: 2, removed: [], addedCount: 2 },
1355  { object: array, type: 'splice', index: 4, removed: [], addedCount: 1 }
1356]);
1357
1358// Pop
1359reset();
1360var array = [1, 2];
1361Object.observe(array, observer.callback);
1362array.pop();
1363array.pop();
1364Object.deliverChangeRecords(observer.callback);
1365observer.assertCallbackRecords([
1366  { object: array, name: '1', type: 'delete', oldValue: 2 },
1367  { object: array, name: 'length', type: 'update', oldValue: 2 },
1368  { object: array, name: '0', type: 'delete', oldValue: 1 },
1369  { object: array, name: 'length', type: 'update', oldValue: 1 },
1370]);
1371
1372// Shift
1373reset();
1374var array = [1, 2];
1375Object.observe(array, observer.callback);
1376array.shift();
1377array.shift();
1378Object.deliverChangeRecords(observer.callback);
1379observer.assertCallbackRecords([
1380  { object: array, name: '0', type: 'update', oldValue: 1 },
1381  { object: array, name: '1', type: 'delete', oldValue: 2 },
1382  { object: array, name: 'length', type: 'update', oldValue: 2 },
1383  { object: array, name: '0', type: 'delete', oldValue: 2 },
1384  { object: array, name: 'length', type: 'update', oldValue: 1 },
1385]);
1386
1387// Unshift
1388reset();
1389var array = [1, 2];
1390Object.observe(array, observer.callback);
1391array.unshift(3, 4);
1392Object.deliverChangeRecords(observer.callback);
1393observer.assertCallbackRecords([
1394  { object: array, name: '3', type: 'add' },
1395  { object: array, name: 'length', type: 'update', oldValue: 2 },
1396  { object: array, name: '2', type: 'add' },
1397  { object: array, name: '0', type: 'update', oldValue: 1 },
1398  { object: array, name: '1', type: 'update', oldValue: 2 },
1399]);
1400
1401// Splice
1402reset();
1403var array = [1, 2, 3];
1404Object.observe(array, observer.callback);
1405array.splice(1, 1, 4, 5);
1406Object.deliverChangeRecords(observer.callback);
1407observer.assertCallbackRecords([
1408  { object: array, name: '3', type: 'add' },
1409  { object: array, name: 'length', type: 'update', oldValue: 3 },
1410  { object: array, name: '1', type: 'update', oldValue: 2 },
1411  { object: array, name: '2', type: 'update', oldValue: 3 },
1412]);
1413
1414// Sort
1415reset();
1416var array = [3, 2, 1];
1417Object.observe(array, observer.callback);
1418array.sort();
1419assertEquals(1, array[0]);
1420assertEquals(2, array[1]);
1421assertEquals(3, array[2]);
1422Object.deliverChangeRecords(observer.callback);
1423observer.assertCallbackRecords([
1424  { object: array, name: '1', type: 'update', oldValue: 2 },
1425  { object: array, name: '0', type: 'update', oldValue: 3 },
1426  { object: array, name: '2', type: 'update', oldValue: 1 },
1427  { object: array, name: '1', type: 'update', oldValue: 3 },
1428  { object: array, name: '0', type: 'update', oldValue: 2 },
1429]);
1430
1431// Splice emitted after Array mutation methods
1432function MockArray(initial, observer) {
1433  for (var i = 0; i < initial.length; i++)
1434    this[i] = initial[i];
1435
1436  this.length_ = initial.length;
1437  this.observer = observer;
1438}
1439MockArray.prototype = {
1440  set length(length) {
1441    Object.getNotifier(this).notify({ type: 'lengthChange' });
1442    this.length_ = length;
1443    Object.observe(this, this.observer.callback, ['splice']);
1444  },
1445  get length() {
1446    return this.length_;
1447  }
1448}
1449
1450reset();
1451var array = new MockArray([], observer);
1452Object.observe(array, observer.callback, ['lengthChange']);
1453Array.prototype.push.call(array, 1);
1454Object.deliverChangeRecords(observer.callback);
1455observer.assertCallbackRecords([
1456  { object: array, type: 'lengthChange' },
1457  { object: array, type: 'splice', index: 0, removed: [], addedCount: 1 },
1458]);
1459
1460reset();
1461var array = new MockArray([1], observer);
1462Object.observe(array, observer.callback, ['lengthChange']);
1463Array.prototype.pop.call(array);
1464Object.deliverChangeRecords(observer.callback);
1465observer.assertCallbackRecords([
1466  { object: array, type: 'lengthChange' },
1467  { object: array, type: 'splice', index: 0, removed: [1], addedCount: 0 },
1468]);
1469
1470reset();
1471var array = new MockArray([1], observer);
1472Object.observe(array, observer.callback, ['lengthChange']);
1473Array.prototype.shift.call(array);
1474Object.deliverChangeRecords(observer.callback);
1475observer.assertCallbackRecords([
1476  { object: array, type: 'lengthChange' },
1477  { object: array, type: 'splice', index: 0, removed: [1], addedCount: 0 },
1478]);
1479
1480reset();
1481var array = new MockArray([], observer);
1482Object.observe(array, observer.callback, ['lengthChange']);
1483Array.prototype.unshift.call(array, 1);
1484Object.deliverChangeRecords(observer.callback);
1485observer.assertCallbackRecords([
1486  { object: array, type: 'lengthChange' },
1487  { object: array, type: 'splice', index: 0, removed: [], addedCount: 1 },
1488]);
1489
1490reset();
1491var array = new MockArray([0, 1, 2], observer);
1492Object.observe(array, observer.callback, ['lengthChange']);
1493Array.prototype.splice.call(array, 1, 1);
1494Object.deliverChangeRecords(observer.callback);
1495observer.assertCallbackRecords([
1496  { object: array, type: 'lengthChange' },
1497  { object: array, type: 'splice', index: 1, removed: [1], addedCount: 0 },
1498]);
1499
1500//
1501// === PLAIN OBJECTS ===
1502//
1503// Push
1504reset()
1505var array = {0: 1, 1: 2, length: 2}
1506Object.observe(array, observer.callback);
1507Array.prototype.push.call(array, 3, 4);
1508Object.deliverChangeRecords(observer.callback);
1509observer.assertCallbackRecords([
1510  { object: array, name: '2', type: 'add' },
1511  { object: array, name: '3', type: 'add' },
1512  { object: array, name: 'length', type: 'update', oldValue: 2 },
1513]);
1514
1515// Pop
1516reset();
1517var array = [1, 2];
1518Object.observe(array, observer.callback);
1519Array.observe(array, observer2.callback);
1520array.pop();
1521array.pop();
1522array.pop();
1523Object.deliverChangeRecords(observer.callback);
1524observer.assertCallbackRecords([
1525  { object: array, name: '1', type: 'delete', oldValue: 2 },
1526  { object: array, name: 'length', type: 'update', oldValue: 2 },
1527  { object: array, name: '0', type: 'delete', oldValue: 1 },
1528  { object: array, name: 'length', type: 'update', oldValue: 1 },
1529]);
1530Object.deliverChangeRecords(observer2.callback);
1531observer2.assertCallbackRecords([
1532  { object: array, type: 'splice', index: 1, removed: [2], addedCount: 0 },
1533  { object: array, type: 'splice', index: 0, removed: [1], addedCount: 0 }
1534]);
1535
1536// Shift
1537reset();
1538var array = [1, 2];
1539Object.observe(array, observer.callback);
1540Array.observe(array, observer2.callback);
1541array.shift();
1542array.shift();
1543array.shift();
1544Object.deliverChangeRecords(observer.callback);
1545observer.assertCallbackRecords([
1546  { object: array, name: '0', type: 'update', oldValue: 1 },
1547  { object: array, name: '1', type: 'delete', oldValue: 2 },
1548  { object: array, name: 'length', type: 'update', oldValue: 2 },
1549  { object: array, name: '0', type: 'delete', oldValue: 2 },
1550  { object: array, name: 'length', type: 'update', oldValue: 1 },
1551]);
1552Object.deliverChangeRecords(observer2.callback);
1553observer2.assertCallbackRecords([
1554  { object: array, type: 'splice', index: 0, removed: [1], addedCount: 0 },
1555  { object: array, type: 'splice', index: 0, removed: [2], addedCount: 0 }
1556]);
1557
1558// Unshift
1559reset();
1560var array = [1, 2];
1561Object.observe(array, observer.callback);
1562Array.observe(array, observer2.callback);
1563array.unshift(3, 4);
1564array.unshift(5);
1565Object.deliverChangeRecords(observer.callback);
1566observer.assertCallbackRecords([
1567  { object: array, name: '3', type: 'add' },
1568  { object: array, name: 'length', type: 'update', oldValue: 2 },
1569  { object: array, name: '2', type: 'add' },
1570  { object: array, name: '0', type: 'update', oldValue: 1 },
1571  { object: array, name: '1', type: 'update', oldValue: 2 },
1572  { object: array, name: '4', type: 'add' },
1573  { object: array, name: 'length', type: 'update', oldValue: 4 },
1574  { object: array, name: '3', type: 'update', oldValue: 2 },
1575  { object: array, name: '2', type: 'update', oldValue: 1 },
1576  { object: array, name: '1', type: 'update', oldValue: 4 },
1577  { object: array, name: '0', type: 'update', oldValue: 3 },
1578]);
1579Object.deliverChangeRecords(observer2.callback);
1580observer2.assertCallbackRecords([
1581  { object: array, type: 'splice', index: 0, removed: [], addedCount: 2 },
1582  { object: array, type: 'splice', index: 0, removed: [], addedCount: 1 }
1583]);
1584
1585// Splice
1586reset();
1587var array = [1, 2, 3];
1588Object.observe(array, observer.callback);
1589Array.observe(array, observer2.callback);
1590array.splice(1, 0, 4, 5); // 1 4 5 2 3
1591array.splice(0, 2); // 5 2 3
1592array.splice(1, 2, 6, 7); // 5 6 7
1593array.splice(2, 0);
1594Object.deliverChangeRecords(observer.callback);
1595observer.assertCallbackRecords([
1596  { object: array, name: '4', type: 'add' },
1597  { object: array, name: 'length', type: 'update', oldValue: 3 },
1598  { object: array, name: '3', type: 'add' },
1599  { object: array, name: '1', type: 'update', oldValue: 2 },
1600  { object: array, name: '2', type: 'update', oldValue: 3 },
1601
1602  { object: array, name: '0', type: 'update', oldValue: 1 },
1603  { object: array, name: '1', type: 'update', oldValue: 4 },
1604  { object: array, name: '2', type: 'update', oldValue: 5 },
1605  { object: array, name: '4', type: 'delete', oldValue: 3 },
1606  { object: array, name: '3', type: 'delete', oldValue: 2 },
1607  { object: array, name: 'length', type: 'update', oldValue: 5 },
1608
1609  { object: array, name: '1', type: 'update', oldValue: 2 },
1610  { object: array, name: '2', type: 'update', oldValue: 3 },
1611]);
1612Object.deliverChangeRecords(observer2.callback);
1613observer2.assertCallbackRecords([
1614  { object: array, type: 'splice', index: 1, removed: [], addedCount: 2 },
1615  { object: array, type: 'splice', index: 0, removed: [1, 4], addedCount: 0 },
1616  { object: array, type: 'splice', index: 1, removed: [2, 3], addedCount: 2 },
1617]);
1618
1619// Exercise StoreIC_ArrayLength
1620reset();
1621var dummy = {};
1622Object.observe(dummy, observer.callback);
1623Object.unobserve(dummy, observer.callback);
1624var array = [0];
1625Object.observe(array, observer.callback);
1626array.splice(0, 1);
1627Object.deliverChangeRecords(observer.callback);
1628observer.assertCallbackRecords([
1629  { object: array, name: '0', type: 'delete', oldValue: 0 },
1630  { object: array, name: 'length', type: 'update', oldValue: 1},
1631]);
1632
1633
1634// __proto__
1635reset();
1636var obj = {};
1637Object.observe(obj, observer.callback);
1638var p = {foo: 'yes'};
1639var q = {bar: 'no'};
1640obj.__proto__ = p;
1641obj.__proto__ = p;  // ignored
1642obj.__proto__ = null;
1643obj.__proto__ = q;  // the __proto__ accessor is gone
1644// TODO(adamk): Add tests for objects with hidden prototypes
1645// once we support observing the global object.
1646Object.deliverChangeRecords(observer.callback);
1647observer.assertCallbackRecords([
1648  { object: obj, name: '__proto__', type: 'setPrototype',
1649    oldValue: Object.prototype },
1650  { object: obj, name: '__proto__', type: 'setPrototype', oldValue: p },
1651  { object: obj, name: '__proto__', type: 'add' },
1652]);
1653
1654
1655// Function.prototype
1656reset();
1657var fun = function(){};
1658Object.observe(fun, observer.callback);
1659var myproto = {foo: 'bar'};
1660fun.prototype = myproto;
1661fun.prototype = 7;
1662fun.prototype = 7;  // ignored
1663Object.defineProperty(fun, 'prototype', {value: 8});
1664Object.deliverChangeRecords(observer.callback);
1665observer.assertRecordCount(3);
1666// Manually examine the first record in order to test
1667// lazy creation of oldValue
1668assertSame(fun, observer.records[0].object);
1669assertEquals('prototype', observer.records[0].name);
1670assertEquals('update', observer.records[0].type);
1671// The only existing reference to the oldValue object is in this
1672// record, so to test that lazy creation happened correctly
1673// we compare its constructor to our function (one of the invariants
1674// ensured when creating an object via AllocateFunctionPrototype).
1675assertSame(fun, observer.records[0].oldValue.constructor);
1676observer.records.splice(0, 1);
1677observer.assertCallbackRecords([
1678  { object: fun, name: 'prototype', type: 'update', oldValue: myproto },
1679  { object: fun, name: 'prototype', type: 'update', oldValue: 7 },
1680]);
1681
1682// Function.prototype should not be observable except on the object itself
1683reset();
1684var fun = function(){};
1685var obj = { __proto__: fun };
1686Object.observe(obj, observer.callback);
1687obj.prototype = 7;
1688Object.deliverChangeRecords(observer.callback);
1689observer.assertRecordCount(1);
1690observer.assertCallbackRecords([
1691  { object: obj, name: 'prototype', type: 'add' },
1692]);
1693
1694// Check that changes in observation status are detected in all IC states and
1695// in optimized code, especially in cases usually using fast elements.
1696var mutation = [
1697  "a[i] = v",
1698  "a[i] ? ++a[i] : a[i] = v",
1699  "a[i] ? a[i]++ : a[i] = v",
1700  "a[i] ? a[i] += 1 : a[i] = v",
1701  "a[i] ? a[i] -= -1 : a[i] = v",
1702];
1703
1704var props = [1, "1", "a"];
1705
1706function TestFastElements(prop, mutation, prepopulate, polymorphic, optimize) {
1707  var setElement = eval(
1708    "(function setElement(a, i, v) { " + mutation + "; " +
1709    "/* " + [].join.call(arguments, " ") + " */" +
1710    "})"
1711  );
1712  print("TestFastElements:", setElement);
1713
1714  var arr = prepopulate ? [1, 2, 3, 4, 5] : [0];
1715  if (prepopulate) arr[prop] = 2;  // for non-element case
1716  setElement(arr, prop, 3);
1717  setElement(arr, prop, 4);
1718  if (polymorphic) setElement(["M", "i", "l", "n", "e", "r"], 0, "m");
1719  if (optimize) %OptimizeFunctionOnNextCall(setElement);
1720  setElement(arr, prop, 5);
1721
1722  reset();
1723  Object.observe(arr, observer.callback);
1724  setElement(arr, prop, 989898);
1725  Object.deliverChangeRecords(observer.callback);
1726  observer.assertCallbackRecords([
1727    { object: arr, name: "" + prop, type: 'update', oldValue: 5 }
1728  ]);
1729}
1730
1731for (var b1 = 0; b1 < 2; ++b1)
1732  for (var b2 = 0; b2 < 2; ++b2)
1733    for (var b3 = 0; b3 < 2; ++b3)
1734      for (var i in props)
1735        for (var j in mutation)
1736          TestFastElements(props[i], mutation[j], b1 != 0, b2 != 0, b3 != 0);
1737
1738
1739var mutation = [
1740  "a.length = v",
1741  "a.length += newSize - oldSize",
1742  "a.length -= oldSize - newSize",
1743];
1744
1745var mutationByIncr = [
1746  "++a.length",
1747  "a.length++",
1748];
1749
1750function TestFastElementsLength(
1751  mutation, polymorphic, optimize, oldSize, newSize) {
1752  var setLength = eval(
1753    "(function setLength(a, v) { " + mutation + "; " +
1754    "/* " + [].join.call(arguments, " ") + " */"
1755    + "})"
1756  );
1757  print("TestFastElementsLength:", setLength);
1758
1759  function array(n) {
1760    var arr = new Array(n);
1761    for (var i = 0; i < n; ++i) arr[i] = i;
1762    return arr;
1763  }
1764
1765  setLength(array(oldSize), newSize);
1766  setLength(array(oldSize), newSize);
1767  if (polymorphic) setLength(array(oldSize).map(isNaN), newSize);
1768  if (optimize) %OptimizeFunctionOnNextCall(setLength);
1769  setLength(array(oldSize), newSize);
1770
1771  reset();
1772  var arr = array(oldSize);
1773  Object.observe(arr, observer.callback);
1774  setLength(arr, newSize);
1775  Object.deliverChangeRecords(observer.callback);
1776  if (oldSize === newSize) {
1777    observer.assertNotCalled();
1778  } else {
1779    var count = oldSize > newSize ? oldSize - newSize : 0;
1780    observer.assertRecordCount(count + 1);
1781    var lengthRecord = observer.records[count];
1782    assertSame(arr, lengthRecord.object);
1783    assertEquals('length', lengthRecord.name);
1784    assertEquals('update', lengthRecord.type);
1785    assertSame(oldSize, lengthRecord.oldValue);
1786  }
1787}
1788
1789for (var b1 = 0; b1 < 2; ++b1)
1790  for (var b2 = 0; b2 < 2; ++b2)
1791    for (var n1 = 0; n1 < 3; ++n1)
1792      for (var n2 = 0; n2 < 3; ++n2)
1793        for (var i in mutation)
1794          TestFastElementsLength(mutation[i], b1 != 0, b2 != 0, 20*n1, 20*n2);
1795
1796for (var b1 = 0; b1 < 2; ++b1)
1797  for (var b2 = 0; b2 < 2; ++b2)
1798    for (var n = 0; n < 3; ++n)
1799      for (var i in mutationByIncr)
1800        TestFastElementsLength(mutationByIncr[i], b1 != 0, b2 != 0, 7*n, 7*n+1);
1801