<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE HTML>
<html>
<head>
<title>Test Observable Array Type</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script>
/* global TestInterfaceObservableArray */

add_task(async function init() {
  await SpecialPowers.pushPrefEnv({set: [["dom.expose_test_interfaces", true]]});
});

add_task(function testObservableArrayExoticObjects_defineProperty() {
  let setCallbackCount = 0;
  let deleteCallbackCount = 0;
  let setCallbackTests = null;
  let deleteCallbackTests = null;

  let m = new TestInterfaceObservableArray({
    setBooleanCallback(value, index) {
      setCallbackCount++;
      if (typeof setCallbackTests === 'function') {
        setCallbackTests(value, index);
      }
    },
    deleteBooleanCallback(value, index) {
      deleteCallbackCount++;
      if (typeof deleteCallbackTests === 'function') {
        deleteCallbackTests(value, index);
      }
    },
  });
  m.observableArrayBoolean = [true, true, true];

  let b = m.observableArrayBoolean;
  ok(Array.isArray(b), "observable array should be an array type");
  is(b.length, 3, "length of observable array should be 0");

  // Test length
  [
    // [descriptor, shouldThrow, expectedResult]
    // Invalid descriptor
    [{configurable: true, value: 0}, false, false],
    [{enumerable: true, value: 0}, false, false],
    [{writable: false, value: 0}, false, false],
    [{get: ()=>{}}, false, false],
    [{set: ()=>{}}, false, false],
    [{get: ()=>{}, set: ()=>{}}, false, false],
    [{get: ()=>{}, value: 0}, true],
    // Invalid length value
    [{value: 1.9}, true],
    [{value: "invalid"}, true],
    [{value: {}}, true],
    // length value should not greater than current length
    [{value: b.length + 1}, false, false],
    // descriptor without value
    [{configurable: false, enumerable: false, writable: true}, false, true],
    // Success
    [{value: b.length}, false, true],
    [{value: b.length - 1}, false, true],
    [{value: 0}, false, true],
  ].forEach(function([descriptor, shouldThrow, expectedResult]) {
    // Initialize
    let oldLen = b.length;
    let oldValues = b.slice();
    let deleteCallbackIndex = oldLen - 1;
    let success = expectedResult && "value" in descriptor;
    setCallbackCount = 0;
    deleteCallbackCount = 0;
    setCallbackTests = null;
    deleteCallbackTests = function(_value, _index) {
      is(_value, oldValues[deleteCallbackIndex], "deleteCallbackTests: test value argument");
      is(_index, deleteCallbackIndex, "deleteCallbackTests: test index argument");
      deleteCallbackIndex--;
    };

    // Test
    info(`defining "length" property with ${JSON.stringify(descriptor)}`);
    try {
      is(Reflect.defineProperty(b, "length", descriptor), expectedResult,
         `Reflect.defineProperty should return ${expectedResult}`);
      ok(!shouldThrow, "Reflect.defineProperty should not throw");
    } catch(e) {
      ok(shouldThrow, `Reflect.defineProperty throws ${e}`);
    }
    is(setCallbackCount, 0, "setCallback count");
    is(deleteCallbackCount, success ? oldLen - descriptor.value : 0, "deleteCallback count");
    isDeeply(b, success ? oldValues.slice(0, descriptor.value) : oldValues, "property values");
    is(b.length, success ? descriptor.value : oldLen, "length of observable array");
  });

  // Test indexed value
  [
    // [index, descriptor, shouldThrow, expectedResult]
    // Invalid descriptor
    [0, {configurable: false, value: true}, false, false],
    [0, {enumerable: false, value: true}, false, false],
    [0, {writable: false, value: true}, false, false],
    [0, {get: ()=>{}}, false, false],
    [0, {set: ()=>{}}, false, false],
    [0, {get: ()=>{}, set: ()=>{}}, false, false],
    [0, {get: ()=>{}, value: true}, true],
    // Index could not greater than last index + 1.
    [b.length + 1, {configurable: true, enumerable: true, value: true}, false, false],
    // descriptor without value
    [b.length, {configurable: true, enumerable: true}, false, true],
    // Success
    [b.length, {configurable: true, enumerable: true, value: true}, false, true],
    [b.length + 1, {configurable: true, enumerable: true, value: true}, false, true],
  ].forEach(function([index, descriptor, shouldThrow, expectedResult]) {
    // Initialize
    let oldLen = b.length;
    let oldValue = b[index];
    let success = expectedResult && "value" in descriptor;
    setCallbackCount = 0;
    deleteCallbackCount = 0;
    setCallbackTests = function(_value, _index) {
      is(_value, descriptor.value, "setCallbackTests: test value argument");
      is(_index, index, "setCallbackTests: test index argument");
    };
    deleteCallbackTests = function(_value, _index) {
      is(_value, oldValue, "deleteCallbackTests: test value argument");
      is(_index, index, "deleteCallbackTests: test index argument");
    };

    // Test
    info(`defining ${index} property with ${JSON.stringify(descriptor)}`);
    try {
      is(Reflect.defineProperty(b, index, descriptor), expectedResult,
         `Reflect.defineProperty should return ${expectedResult}`);
      ok(!shouldThrow, "Reflect.defineProperty should not throw");
    } catch(e) {
      ok(shouldThrow, `Reflect.defineProperty throws ${e}`);
    }
    is(setCallbackCount, success ? 1 : 0, "setCallback count");
    is(deleteCallbackCount, (oldLen > index) ? 1 : 0, "deleteCallback count");
    is(b[index], success ? descriptor.value : oldValue, "property value");
    is(b.length, success ? Math.max(index + 1, oldLen) : oldLen, "length of observable array");
  });

  // Test other property
  [
    // [property, descriptor, shouldThrow, expectedResult]
    ["prop1", {configurable: false, value: "value1"}, false, true],
    ["prop1", {configurable: true, value: "value2"}, false, false],
    ["prop2", {enumerable: false, value: 5}, false, true],
    ["prop3", {enumerable: false, value: []}, false, true],
    ["prop4", {enumerable: false, value: {}}, false, true],
    ["prop5", {get: ()=>{}, value: true}, true, false],
    ["prop6", {get: ()=>{}, set: ()=>{}}, false, true],
  ].forEach(function([property, descriptor, shouldThrow, expectedResult]) {
    // Initialize
    let oldValue = b[property];
    let oldLen = b.length;
    setCallbackCount = 0;
    deleteCallbackCount = 0;
    setCallbackTests = null;
    deleteCallbackTests = null;

    // Test
    info(`defining ${property} property with ${JSON.stringify(descriptor)}`);
    try {
      is(Reflect.defineProperty(b, property, descriptor), expectedResult,
         `Reflect.defineProperty should return ${expectedResult}`);
      ok(!shouldThrow, "Reflect.defineProperty should not throw");
    } catch(e) {
      ok(shouldThrow, `Reflect.defineProperty throws ${e}`);
    }
    is(setCallbackCount, 0, "setCallback count");
    is(deleteCallbackCount, 0, "deleteCallback count");
    is(b[property], expectedResult ? descriptor.value : oldValue, "property value");
    is(b.length, oldLen, "length of observable array");
  });
});

add_task(function testObservableArrayExoticObjects_defineProperty_callback_throw() {
  let setCallbackCount = 0;
  let deleteCallbackCount = 0;

  const minLen = 3;
  let m = new TestInterfaceObservableArray({
    setBooleanCallback(value, index) {
      setCallbackCount++;
      if (value) {
        throw new Error("setBooleanCallback");
      }
    },
    deleteBooleanCallback(value, index) {
      deleteCallbackCount++;
      if (index < minLen) {
        throw new Error("deleteBooleanCallback");
      }
    },
  });
  m.observableArrayBoolean = [false, false, false, false, false];

  let b = m.observableArrayBoolean;
  ok(Array.isArray(b), "observable array should be an array type");
  is(b.length, 5, "length of observable array should be 3");

  // Test length
  [
    // [length, shouldThrow]
    [b.length, false],
    [b.length - 1, false],
    [0, true],
  ].forEach(function([length, shouldThrow]) {
    // Initialize
    let oldValues = b.slice();
    let oldLen = b.length;
    let descriptor = {value: length};
    setCallbackCount = 0;
    deleteCallbackCount = 0;

    // Test
    info(`defining "length" property with ${JSON.stringify(descriptor)}`);
    try {
      ok(Reflect.defineProperty(b, "length", descriptor),
         "Reflect.defineProperty should return true");
      ok(!shouldThrow, "Reflect.defineProperty should not throw");
    } catch(e) {
      ok(shouldThrow, `Reflect.defineProperty throws ${e}`);
    }
    is(setCallbackCount, 0, "setCallback count");
    is(deleteCallbackCount, oldLen - (shouldThrow ? minLen - 1 : length), "deleteCallback count");
    isDeeply(b, oldValues.slice(0, shouldThrow ? minLen : length), "property values");
    is(b.length, shouldThrow ? minLen : length, "length of observable array");
  });

  // Test indexed value
  [
    // [index, value, shouldThrow]
    [b.length, true, true],
    [b.length, false, false],
    [b.length + 1, false, false],
    [b.length + 1, true, true],
    [0, true, true],
    [0, false, true],
  ].forEach(function([index, value, shouldThrow]) {
    // Initialize
    let oldValue = b[index];
    let oldLen = b.length;
    let descriptor = {configurable: true, enumerable: true, value};
    setCallbackCount = 0;
    deleteCallbackCount = 0;

    // Test
    info(`defining ${index} property with ${JSON.stringify(descriptor)}`);
    try {
      ok(Reflect.defineProperty(b, index, descriptor), "Reflect.defineProperty should return true");
      ok(!shouldThrow, "Reflect.defineProperty should not throw");
    } catch(e) {
      ok(shouldThrow, `Reflect.defineProperty throws ${e}`);
    }
    is(setCallbackCount, (index < minLen) ? 0 : 1, "setCallback count");
    is(deleteCallbackCount, (oldLen > index) ? 1 : 0, "deleteCallback count");
    is(b[index], shouldThrow ? oldValue : value, "property value");
    is(b.length, shouldThrow ? oldLen : Math.max(oldLen, index + 1), "length of observable array");
  });

  // Test other property
  [
    // [property, descriptor, expectedResult]
    ["prop1", {configurable: false, value: "value1"}, true],
    ["prop1", {configurable: true, value: "value2"}, false],
    ["prop2", {enumerable: false, value: 5}, true],
    ["prop3", {enumerable: false, value: []}, true],
    ["prop4", {enumerable: false, value: {}}, true],
  ].forEach(function([property, descriptor, expectedResult]) {
    // Initialize
    let oldValue = b[property];
    let oldLen = b.length;
    setCallbackCount = 0;
    deleteCallbackCount = 0;

    // Test
    info(`defining ${property} property with ${JSON.stringify(descriptor)}`);
    try {
      is(Reflect.defineProperty(b, property, descriptor), expectedResult,
         `Reflect.defineProperty should return ${expectedResult}`);
      ok(true, "Reflect.defineProperty should not throw");
    } catch(e) {
      ok(false, `Reflect.defineProperty throws ${e}`);
    }
    is(setCallbackCount, 0, "setCallback count");
    is(deleteCallbackCount, 0, "deleteCallback count");
    is(b[property], expectedResult ? descriptor.value : oldValue, "property value");
    is(b.length, oldLen, "length of observable array");
  });
});

add_task(function testObservableArrayExoticObjects_deleteProperty() {
  let setCallbackCount = 0;
  let deleteCallbackCount = 0;
  let deleteCallbackTests = null;

  let m = new TestInterfaceObservableArray({
    setBooleanCallback(value, index) {
      setCallbackCount++;
    },
    deleteBooleanCallback(value, index) {
      deleteCallbackCount++;
      if (typeof deleteCallbackTests === 'function') {
        deleteCallbackTests(value, index);
      }
    },
  });
  m.observableArrayBoolean = [true, true];

  let b = m.observableArrayBoolean;
  ok(Array.isArray(b), "observable array should be an array type");
  is(b.length, 2, "length of observable array should be 2");

  // Test length
  setCallbackCount = 0;
  deleteCallbackCount = 0;
  info("deleting length property");
  ok(!Reflect.deleteProperty(b, "length"), "test result of deleting length property");
  is(setCallbackCount, 0, "setCallback should not be called");
  is(deleteCallbackCount, 0, "deleteCallback should not be called");
  is(b.length, 2, "length should still be 2");

  // Test indexed value
  [
    // [index, expectedResult]
    [2, false],
    [0, false],
    [1, true],
  ].forEach(function([index, expectedResult]) {
    // Initialize
    let oldLen = b.length;
    let oldValue = b[index];
    setCallbackCount = 0;
    deleteCallbackCount = 0;
    deleteCallbackTests = function(_value, _index) {
      is(_value, oldValue, "deleteCallbackTests: test value argument");
      is(_index, index, "deleteCallbackTests: test index argument");
    };

    // Test
    info(`deleting ${index} property`);
    is(Reflect.deleteProperty(b, index), expectedResult,
       `Reflect.deleteProperty should return ${expectedResult}`);
    is(setCallbackCount, 0, "setCallback count");
    is(deleteCallbackCount, expectedResult ? 1 : 0, "deleteCallback count");
    is(b[index], expectedResult ? undefined : oldValue, "property value");
    is(b.length, expectedResult ? oldLen - 1 : oldLen,
       "length of observable array");
  });

  // Test other property
  [
    // [property, value]
    ["prop1", "value1"],
    ["prop2", 5],
    ["prop3", []],
    ["prop4", {}],
  ].forEach(function([property, value]) {
    // Initialize
    b[property] = value;
    let oldLen = b.length;
    setCallbackCount = 0;
    deleteCallbackCount = 0;
    deleteCallbackTests = null;

    // Test
    info(`deleting ${property} property`);
    is(b[property], value, `property value should be ${value} before deleting`);
    ok(Reflect.deleteProperty(b, property), "Reflect.deleteProperty should return true");
    is(setCallbackCount, 0, "setCallback count");
    is(deleteCallbackCount, 0, "deleteCallback count");
    is(b[property], undefined, "property value should be undefined after deleting");
    is(b.length, oldLen, "length of observable array");
  });
});

add_task(function testObservableArrayExoticObjects_deleteProperty_callback_throw() {
  let setCallbackCount = 0;
  let deleteCallbackCount = 0;

  let m = new TestInterfaceObservableArray({
    setBooleanCallback(value, index) {
      setCallbackCount++;
    },
    deleteBooleanCallback(value, index) {
      deleteCallbackCount++;
      if (value) {
        throw new Error("deleteBooleanCallback");
      }
    },
  });
  m.observableArrayBoolean = [true, false];

  let b = m.observableArrayBoolean;
  ok(Array.isArray(b), "observable array should be an array type");
  is(b.length, 2, "length of observable array should be 2");

  // Test indexed value
  let index = b.length;
  while (index--) {
    // Initialize
    let oldValue = b[index];
    let oldLen = b.length;
    setCallbackCount = 0;
    deleteCallbackCount = 0;

    // Test
    info(`deleting index ${index}`);
    try {
      ok(Reflect.deleteProperty(b, index), "Reflect.deleteProperty should return true");
      ok(!oldValue, "Reflect.deleteProperty should not throw");
    } catch(e) {
      ok(oldValue, `Reflect.deleteProperty throws ${e}`);
    }
    is(setCallbackCount, 0, "setCallback count");
    is(deleteCallbackCount, 1, "deleteCallback count");
    is(b[index], oldValue ? oldValue : undefined, "property value");
    is(b.length, oldValue ? oldLen : oldLen - 1, "length of observable array");
  }

  // Test other property
  [
    // [property, value]
    ["prop1", "value1"],
    ["prop2", 5],
    ["prop3", []],
    ["prop4", {}],
    ["prop5", false],
  ].forEach(function([property, value]) {
    // Initialize
    b[property] = value;
    let oldLen = b.length;
    setCallbackCount = 0;
    deleteCallbackCount = 0;

    // Test
    info(`deleting ${property} property`);
    is(b[property], value, `property value should be ${JSON.stringify(value)} before deleting`);
    try {
      ok(Reflect.deleteProperty(b, property), `Reflect.deleteProperty should return true`);
      ok(true, "Reflect.deleteProperty should not throw");
    } catch(e) {
      ok(false, `Reflect.deleteProperty throws ${e}`);
    }
    is(setCallbackCount, 0, "setCallback count");
    is(deleteCallbackCount, 0, "deleteCallback count");
    is(b[property], undefined, `property value should be undefined after deleting`);
    is(b.length, oldLen, "length of observable array");
  });
});

add_task(function testObservableArrayExoticObjects_get() {
  let m = new TestInterfaceObservableArray();
  m.observableArrayBoolean = [true, false];

  let b = m.observableArrayBoolean;
  ok(Array.isArray(b), "observable array should be an array type");
  is(b.length, 2, "length of observable array should be 2");

  // Test length
  is(Reflect.get(b, "length"), 2, "test result of getting length property");

  // Test indexed value
  is(Reflect.get(b, 0), true, "test result of getting index 0");
  is(Reflect.get(b, 1), false, "test result of getting index 1");
  is(Reflect.get(b, 2), undefined, "test result of getting index 2");

  // Test other property
  [
    // [property, value]
    ["prop1", "value1"],
    ["prop2", 5],
    ["prop3", []],
    ["prop4", {}],
  ].forEach(function([property, value]) {
    is(Reflect.get(b, property), undefined, `test ${property} property before setting property value`);
    b[property] = value;
    is(Reflect.get(b, property), value, `test ${property} property after setting property value`);
  });
});

add_task(function testObservableArrayExoticObjects_getOwnPropertyDescriptor() {
  function TestDescriptor(object, property, exist, configurable, enumerable,
                          writable, value) {
    let descriptor = Reflect.getOwnPropertyDescriptor(object, property);
    if (!exist) {
      is(descriptor, undefined, `descriptor of ${property} property should be undefined`);
      return;
    }

    is(descriptor.configurable, configurable, `test descriptor of ${property} property (configurable)`);
    is(descriptor.enumerable, enumerable, `test descriptor of ${property} property (enumerable)`);
    is(descriptor.writable, writable, `test descriptor of ${property} property (writable)`);
    is(descriptor.value, value, `test descriptor of ${property} property (value)`);
  }

  let m = new TestInterfaceObservableArray();
  m.observableArrayBoolean = [true, false];

  let b = m.observableArrayBoolean;
  ok(Array.isArray(b), "observable array should be an array type");
  is(b.length, 2, "length of observable array should be 2");

  // Test length
  TestDescriptor(b, "length", true, false /* configurable */,
                 false /* enumerable */, true /* writable */ , 2 /* value */);

  // Test indexed value
  TestDescriptor(b, 0, true, true /* configurable */, true /* enumerable */,
                 true /* writable */ , true /* value */);
  TestDescriptor(b, 1, true, true /* configurable */, true /* enumerable */,
                 true /* writable */ , false /* value */);
  TestDescriptor(b, 2, false);

  // Test other property
  [
    // [property, value, configurable, enumerable, writable]
    ["prop1", "value1", true, true, true],
    ["prop2", 5, true, true, false],
    ["prop3", [], true, false, false],
    ["prop4", {}, false, false, false],
  ].forEach(function([property, value, configurable, enumerable, writable]) {
    Object.defineProperty(b, property, {
      value,
      configurable,
      enumerable,
      writable,
    });
    TestDescriptor(b, property, true, configurable, enumerable, writable , value);
  });
});

add_task(function testObservableArrayExoticObjects_has() {
  let m = new TestInterfaceObservableArray();
  m.observableArrayBoolean = [true, false];

  let b = m.observableArrayBoolean;
  ok(Array.isArray(b), "observable array should be an array type");
  is(b.length, 2, "length of observable array should be 2");

  // Test length
  ok(Reflect.has(b, "length"), `test length property`);

  // Test indexed value
  ok(Reflect.has(b, 0), `test 0 property`);
  ok(Reflect.has(b, 1), `test 1 property`);
  ok(!Reflect.has(b, 2), `test 2 property`);

  // Test other property
  [
    // [property, value]
    ["prop1", "value1"],
    ["prop2", 5],
    ["prop3", []],
    ["prop4", {}],
  ].forEach(function([property, value]) {
    ok(!Reflect.has(b, property), `test ${property} property before setting property value`);
    b[property] = value;
    ok(Reflect.has(b, property), `test ${property} property after setting property value`);
  });
});

add_task(function testObservableArrayExoticObjects_ownKeys() {
  let m = new TestInterfaceObservableArray();
  m.observableArrayBoolean = [true, false];

  let b = m.observableArrayBoolean;
  ok(Array.isArray(b), "observable array should be an array type");
  is(b.length, 2, "length of observable array should be 2");

  // Add other properties
  b.prop1 = "value1";
  b.prop2 = 5;
  b.prop3 = [];
  b.prop4 = {};

  let keys = Reflect.ownKeys(b);
  SimpleTest.isDeeply(keys, ["0", "1", "length", "prop1", "prop2", "prop3", "prop4"], `test property keys`);
});

add_task(function testObservableArrayExoticObjects_preventExtensions() {
  let m = new TestInterfaceObservableArray();
  let b = m.observableArrayBoolean;
  ok(Array.isArray(b), "observable array should be an array type");
  is(b.length, 0, "length of observable array should be 0");

  // Test preventExtensions
  ok(Reflect.isExtensible(b), "test isExtensible before preventExtensions");
  ok(!Reflect.preventExtensions(b), "test preventExtensions");
  ok(Reflect.isExtensible(b), "test isExtensible after preventExtensions");
});

add_task(function testObservableArrayExoticObjects_set() {
  let setCallbackCount = 0;
  let deleteCallbackCount = 0;
  let setCallbackTests = null;
  let deleteCallbackTests = null;

  let m = new TestInterfaceObservableArray({
    setBooleanCallback(value, index) {
      setCallbackCount++;
      if (typeof setCallbackTests === 'function') {
        setCallbackTests(value, index);
      }
    },
    deleteBooleanCallback(value, index) {
      deleteCallbackCount++;
      if (typeof deleteCallbackTests === 'function') {
        deleteCallbackTests(value, index);
      }
    },
  });
  m.observableArrayBoolean = [true, true, true];

  let b = m.observableArrayBoolean;
  ok(Array.isArray(b), "observable array should be an array type");
  is(b.length, 3, "length of observable array should be 3");

  // Test length
  [
    // [length, shouldThrow, expectedResult]
    // Invalid length value
    [1.9, true],
    ['invalid', true],
    [{}, true],
    // length value should not greater than current length
    [b.length + 1, false, false],
    // Success
    [b.length, false, true],
    [b.length - 1, false, true],
    [0, false, true],
  ].forEach(function([length, shouldThrow, expectedResult]) {
    // Initialize
    let oldLen = b.length;
    let oldValues = b.slice();
    setCallbackCount = 0;
    deleteCallbackCount = 0;
    setCallbackTests = null;
    let deleteCallbackIndex = oldLen - 1;
    deleteCallbackTests = function(_value, _index) {
      is(_value, oldValues[deleteCallbackIndex], "deleteCallbackTests: test value argument");
      is(_index, deleteCallbackIndex, "deleteCallbackTests: test index argument");
      deleteCallbackIndex--;
    };

    // Test
    info(`setting "length" property value to ${length}`);
    try {
      is(Reflect.set(b, "length", length), expectedResult, `Reflect.set should return ${expectedResult}`);
      ok(!shouldThrow, "Reflect.set should not throw");
    } catch(e) {
      ok(shouldThrow, `Reflect.set throws ${e}`);
    }
    is(setCallbackCount, 0, "setCallback count");
    is(deleteCallbackCount, expectedResult ? oldLen - length : 0, "deleteCallback count");
    isDeeply(b, expectedResult ? oldValues.slice(0, length) : oldValues, "property values");
    is(b.length, expectedResult ? length : oldLen, "length of observable array");
  });

  // Test indexed value
  [
    // [index, value, shouldThrow, expectedResult]
    // Index could not greater than last index.
    [b.length + 1, true, false, false],
    // Success
    [b.length, true, false, true],
    [b.length + 1, true, false, true],
  ].forEach(function([index, value, shouldThrow, expectedResult]) {
    // Initialize
    let oldLen = b.length;
    let oldValue = b[index];
    setCallbackCount = 0;
    deleteCallbackCount = 0;
    setCallbackTests = function(_value, _index) {
      is(_value, value, "setCallbackTests: test value argument");
      is(_index, index, "setCallbackTests: test index argument");
    };
    deleteCallbackTests = function(_value, _index) {
      is(_value, oldValue, "deleteCallbackTests: test value argument");
      is(_index, index, "deleteCallbackTests: test index argument");
    };

    // Test
    info(`setting ${index} property to ${value}`);
    try {
      is(Reflect.set(b, index, value), expectedResult, `Reflect.set should return ${expectedResult}`);
      ok(!shouldThrow, "Reflect.set should not throw");
    } catch(e) {
      ok(shouldThrow, `Reflect.set throws ${e}`);
    }
    is(setCallbackCount, expectedResult ? 1 : 0, "setCallback count");
    is(deleteCallbackCount, (oldLen > index) ? 1 : 0, "deleteCallback count");
    is(b[index], expectedResult ? value : oldValue, "property value");
    is(b.length, expectedResult ? Math.max(index + 1, oldLen) : oldLen, "length of observable array");
  });

  // Test other property
  [
    // [property, value]
    ["prop1", "value1"],
    ["prop1", "value2"],
    ["prop2", 5],
    ["prop3", []],
    ["prop4", {}],
  ].forEach(function([property, value]) {
    // Initialize
    let oldLen = b.length;
    setCallbackCount = 0;
    deleteCallbackCount = 0;
    setCallbackTests = null;
    deleteCallbackTests = null;

    // Test
    info(`setting ${property} property to ${value}`);
    ok(Reflect.set(b, property, value), "Reflect.defineProperty should return true");
    is(setCallbackCount, 0, "setCallback count");
    is(deleteCallbackCount, 0, "deleteCallback count");
    is(b[property], value, "property value");
    is(b.length, oldLen, "length of observable array");
  });
});

add_task(function testObservableArrayExoticObjects_set_callback_throw() {
  let setCallbackCount = 0;
  let deleteCallbackCount = 0;

  const minLen = 3;
  let m = new TestInterfaceObservableArray({
    setBooleanCallback(value, index) {
      setCallbackCount++;
      if (value) {
        throw new Error("setBooleanCallback");
      }
    },
    deleteBooleanCallback(value, index) {
      deleteCallbackCount++;
      if (index < minLen) {
        throw new Error("deleteBooleanCallback");
      }
    },
  });
  m.observableArrayBoolean = [false, false, false, false, false];

  let b = m.observableArrayBoolean;
  ok(Array.isArray(b), "observable array should be an array type");
  is(b.length, 5, "length of observable array should be 3");

  // Test length
  [
    // [value, shouldThrow]
    [b.length, false],
    [b.length - 1, false],
    [0, true],
  ].forEach(function([length, shouldThrow]) {
    // Initialize
    let oldValues = b.slice();
    let oldLen = b.length;
    setCallbackCount = 0;
    deleteCallbackCount = 0;

    // Test
    info(`setting "length" property to ${length}`);
    try {
      ok(Reflect.set(b, "length", length), "Reflect.set should return true");
      ok(!shouldThrow, `Reflect.set should not throw`);
    } catch(e) {
      ok(shouldThrow, `Reflect.set throws ${e}`);
    }
    is(setCallbackCount, 0, "setCallback should not be called");
    is(deleteCallbackCount, oldLen - (shouldThrow ? minLen - 1 : length), "deleteCallback count");
    isDeeply(b, oldValues.slice(0, shouldThrow ? minLen : length), "property values");
    is(b.length, shouldThrow ? minLen : length, "length of observable array");
  });

  // Test indexed value
  [
    // [index, value, shouldThrow]
    [b.length, true, true],
    [b.length, false, false],
    [b.length + 1, false, false],
    [b.length + 1, true, true],
    [0, false, true],
    [0, true, true],
  ].forEach(function([index, value, shouldThrow]) {
    // Initialize
    let oldValue = b[index];
    let oldLen = b.length;
    setCallbackCount = 0;
    deleteCallbackCount = 0;

    // Test
    info(`setting ${index} property to ${value}`);
    try {
      ok(Reflect.set(b, index, value), "Reflect.set should return true");
      ok(!shouldThrow, `Reflect.set should not throw`);
    } catch(e) {
      ok(shouldThrow, `Reflect.set throws ${e}`);
    }
    is(setCallbackCount, (index < minLen) ? 0 : 1, "setCallback count");
    is(deleteCallbackCount, (oldLen > index) ? 1 : 0, "deleteCallback count");
    is(b[index], shouldThrow ? oldValue : value, "property value");
    is(b.length, shouldThrow ? oldLen : Math.max(oldLen, index + 1), "length of observable array");
  });

  // Test other property
  [
    ["prop1", "value1"],
    ["prop1", "value2"],
    ["prop2", 5],
    ["prop3", []],
    ["prop4", {}],
  ].forEach(function([property, value]) {
    // Initialize
    let oldLen = b.length;
    setCallbackCount = 0;
    deleteCallbackCount = 0;

    // Test
    info(`setting ${property} property to ${JSON.stringify(value)}`);
    try {
      ok(Reflect.set(b, property, value), "Reflect.set should return true");
      ok(true, `Reflect.set should not throw`);
    } catch(e) {
      ok(false, `Reflect.set throws ${e}`);
    }
    is(setCallbackCount, 0, "setCallback should not be called");
    is(deleteCallbackCount, 0, "deleteCallback should be called");
    is(b[property], value, "property value");
    is(b.length, oldLen, "length of observable array");
  });
});

add_task(function testObservableArrayExoticObjects_invalidtype() {
  let m = new TestInterfaceObservableArray();
  let i = m.observableArrayInterface;
  ok(Array.isArray(i), "Observable array should be an array type");
  is(i.length, 0, "length should be 0");

  [true, "invalid"].forEach(function(value) {
    SimpleTest.doesThrow(() => {
      let descriptor = {configurable: true, enumerable: true, writable: true, value};
      Reflect.defineProperty(i, i.length, descriptor);
    }, `defining ${i.length} property with ${JSON.stringify(value)} should throw`);

    SimpleTest.doesThrow(() => {
      Reflect.set(i, i.length, value);
    }, `setting ${i.length} property to ${JSON.stringify(value)} should throw`);
  });

  is(i.length, 0, "length should still be 0");
});
</script>
</body>
</html>
