<!doctype html>
<meta charset=utf-8>
<meta name="timeout" content="long">
<title>Cross-origin behavior of Window and Location</title>
<link rel="author" title="Bobby Holley (:bholley)" href="bobbyholley@gmail.com">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#security-window">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#security-location">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/get-host-info.sub.js"></script>
<div id=log></div>
<iframe id="B"></iframe>
<iframe id="C"></iframe>
<iframe id="D"></iframe>
<iframe id="E"></iframe>
<iframe id="F"></iframe>
<iframe id="G"></iframe>
<iframe id="H"></iframe>
<script>

/*
 * Setup boilerplate. This gives us a same-origin window "B", cross-origin
 * windows "C" and "D", initially same-origin but then changing document.domain
 * windows "E" and "F", and not-same-site (also cross-origin, of course) windows
 * "G" and "H".
 */
var host_info = get_host_info();

setup({explicit_done: true});
path = location.pathname.substring(0, location.pathname.lastIndexOf('/')) + '/frame.html';
pathWithThen = location.pathname.substring(0, location.pathname.lastIndexOf('/')) + '/frame-with-then.html';
var B = document.getElementById('B').contentWindow;
var C = document.getElementById('C').contentWindow;
var D = document.getElementById('D').contentWindow;
var E = document.getElementById('E').contentWindow;
var F = document.getElementById('F').contentWindow;
var G = document.getElementById('G').contentWindow;
var H = document.getElementById('H').contentWindow;
B.frameElement.uriToLoad = path;
C.frameElement.uriToLoad = get_host_info().HTTP_REMOTE_ORIGIN + path;
D.frameElement.uriToLoad = get_host_info().HTTP_REMOTE_ORIGIN + pathWithThen;
E.frameElement.uriToLoad = path + "?setdomain";
F.frameElement.uriToLoad = pathWithThen + "?setdomain";
G.frameElement.uriToLoad = get_host_info().HTTP_NOTSAMESITE_ORIGIN + path;
H.frameElement.uriToLoad = get_host_info().HTTP_NOTSAMESITE_ORIGIN + pathWithThen;

function winName(win) {
  var iframes = document.getElementsByTagName('iframe');
  iframes.find = Array.prototype.find;
  var found = iframes.find(function (ifr) {
    return ifr.contentWindow == win;
  });
  if (found) {
    return found.id;
  }
  return "UNKNOWN";
}

function reloadSubframes(cb) {
  var iframes = document.getElementsByTagName('iframe');
  iframes.forEach = Array.prototype.forEach;
  var count = 0;
  function frameLoaded() {
    this.onload = null;
    if (++count == iframes.length)
      cb();
  }
  iframes.forEach(function(ifr) { ifr.onload = frameLoaded; ifr.setAttribute('src', ifr.uriToLoad); });
}
function isObject(x) { return Object(x) === x; }

/*
 * Note: we eschew assert_equals in a lot of these tests, since the harness ends
 * up throwing when it tries to format a message involving a cross-origin object.
 */

/*
 * List of tests.  Each test is actually a pair: an array of tests to run and a
 * boolean for whether these are promise tests.  We reload all the subframes in
 * between running each toplevel test.  This is done to avoid having to reload
 * all the subframes for every single test, which is overkill: some of these
 * tests are known to touch only one subframe.  And doing it makes the test
 * really slow.
 */
var testList = [];
function addTest(func, desc) {
  testList.push(
    {tests: [
      {func: func.bind(null, C),
       desc: desc + " (cross-origin)"},
      {func: func.bind(null, E),
       desc: desc + " (same-origin + document.domain)"},
      {func: func.bind(null, G),
       desc: desc + " (cross-site)"}],
     promiseTest: false});
}

function addPromiseTest(func, desc) {
  testList.push(
    {tests: [
      {func: func.bind(null, C),
       desc: desc + " (cross-origin)"},
      {func: func.bind(null, E),
       desc: desc + " (same-origin + document.domain)"},
      {func: func.bind(null, G),
       desc: desc + " (cross-site)"}],
     promiseTest: true});
}

/**
 * Similar helpers, but for the subframes that load frame-with-then.html
 */
function addThenTest(func, desc) {
  testList.push(
    {tests: [
      {func: func.bind(null, D),
       desc: desc + " (cross-origin)"},
      {func: func.bind(null, F),
       desc: desc + " (same-origin + document.domain)"},
      {func: func.bind(null, H),
       desc: desc + " (cross-site)"}],
     promiseTest: false});
}

function addPromiseThenTest(func, desc) {
  testList.push(
    {tests: [
      {func: func.bind(null, D),
       desc: desc + " (cross-origin)"},
      {func: func.bind(null, F),
       desc: desc + " (same-origin + document.domain)"},
      {func: func.bind(null, H),
       desc: desc + " (cross-site)"}],
     promiseTest: true});
}

/*
 * Basic smoke tests for same-origin and cross-origin behaviors.
 */

addTest(function(win) {
  // Note: we do not check location.host as its default port semantics are hard to reflect statically
  assert_equals(location.hostname, host_info.ORIGINAL_HOST, 'Need to run the top-level test from domain ' + host_info.ORIGINAL_HOST);
  assert_equals(get_port(location), host_info.HTTP_PORT, 'Need to run the top-level test from port ' + host_info.HTTP_PORT);
  assert_equals(B.parent, window, "window.parent works same-origin");
  assert_equals(win.parent, window, "window.parent works cross-origin");
  assert_equals(B.location.pathname, path, "location.href works same-origin");
  assert_throws_dom("SecurityError", function() { win.location.pathname; }, "location.pathname throws cross-origin");
  assert_equals(B.frames, 'override', "Overrides visible in the same-origin case");
  assert_equals(win.frames, win, "Overrides invisible in the cross-origin case");
  assert_equals(B.focus, 'override', "Overrides visible in the same-origin case");
  checkFunction(win.focus, Function.prototype);
  assert_not_equals(win.focus, focus, "Overrides invisible in the cross-origin case");
}, "Basic sanity-checking");

/*
 * Tests regarding which properties are allowed cross-origin.
 *
 * Also tests for [[GetOwnProperty]] and [[HasOwnProperty]] behavior.
 */

var allowedSymbols = [Symbol.toStringTag, Symbol.hasInstance,
                          Symbol.isConcatSpreadable];
var windowAllowlists = {
  namedFrames: ['donotleakme'],
  indices: ['0', '1'],
  getters: ['location', 'window', 'frames', 'self', 'top', 'parent',
            'opener', 'closed', 'length'],
  setters: ['location'],
  methods: ['postMessage', 'close', 'blur', 'focus'],
  // These are methods which return promises and, therefore, when called with a
  // cross-origin `this` object, do not throw immediately, but instead return a
  // Promise which rejects with the same SecurityError that they would
  // otherwise throw. They are not, however, cross-origin accessible.
  promiseMethods: ['createImageBitmap', 'fetch'],
}
windowAllowlists.propNames = Array.from(new Set([...windowAllowlists.indices,
                                                 ...windowAllowlists.getters,
                                                 ...windowAllowlists.setters,
                                                 ...windowAllowlists.methods,
                                                 'then'])).sort();
windowAllowlists.props = windowAllowlists.propNames.concat(allowedSymbols);

var locationAllowlists = {
  getters: [],
  setters: ['href'],
  methods: ['replace'],
  promiseMethods: [],
}
locationAllowlists.propNames = Array.from(new Set([...locationAllowlists.getters,
                                                   ...locationAllowlists.setters,
                                                   ...locationAllowlists.methods,
                                                   'then'])).sort();

// Define various sets of arguments to call cross-origin methods with. Arguments
// for any cross-origin-callable method must be valid, and should aim to have no
// side-effects. Any method without an entry in this list will be called with
// an empty arguments list.
var methodArgs = new Map(Object.entries({
  // As a basic smoke test, we call one cross-origin-inaccessible method with
  // both valid and invalid arguments to make sure that it rejects with the
  // same SecurityError regardless.
  assign: [
    [],
    ["javascript:undefined"],
  ],
  // Note: If we post a message to frame.html with a matching origin, its
  // "onmessage" handler will change its `document.domain`, and potentially
  // invalidate subsequent tests, so be sure to only pass non-matching origins.
  postMessage: [
    ["foo", "http://does-not.exist/"],
    ["foo", {}],
  ],
  replace: [["javascript:undefined"]],
}));

addTest(function(win) {
  for (var prop in window) {
    if (windowAllowlists.props.indexOf(prop) != -1) {
      win[prop]; // Shouldn't throw.
      Object.getOwnPropertyDescriptor(win, prop); // Shouldn't throw.
      assert_true(Object.prototype.hasOwnProperty.call(win, prop), "hasOwnProperty for " + String(prop));
    } else {
      assert_throws_dom("SecurityError", function() { win[prop]; }, "Should throw when accessing " + String(prop) + " on Window");
      assert_throws_dom("SecurityError", function() { Object.getOwnPropertyDescriptor(win, prop); },
                        "Should throw when accessing property descriptor for " + prop + " on Window");
      assert_throws_dom("SecurityError", function() { Object.prototype.hasOwnProperty.call(win, prop); },
                        "Should throw when invoking hasOwnProperty for " + prop + " on Window");
    }
    if (prop != 'location')
      assert_throws_dom("SecurityError", function() { win[prop] = undefined; }, "Should throw when writing to " + prop + " on Window");
  }
  for (var prop of windowAllowlists.namedFrames) {
    win[prop]; // Shouldn't throw.
    var desc = Object.getOwnPropertyDescriptor(win, prop);
    assert_false(desc.writable, "[[Writable]] for named frame " + String(prop));
    assert_false(desc.enumerable, "[[Enumerable]] for named frame " + String(prop));
    assert_true(desc.configurable, "[[Configurable]] for named frame " + String(prop));
    assert_true(Object.prototype.hasOwnProperty.call(win, prop), "hasOwnProperty for " + String(prop));
  }
  for (var prop in location) {
    if (prop == 'replace') {
      win.location[prop]; // Shouldn't throw.
      Object.getOwnPropertyDescriptor(win.location, prop); // Shouldn't throw.
      assert_true(Object.prototype.hasOwnProperty.call(win.location, prop), "hasOwnProperty for " + prop);
      assert_throws_dom("SecurityError", function() { win.location[prop] = undefined; }, "Should throw when writing to " + prop + " on Location");
    }
    else if (prop == 'href') {
      Object.getOwnPropertyDescriptor(win.location, prop); // Shouldn't throw.
      assert_true(Object.prototype.hasOwnProperty.call(win.location, prop), "hasOwnProperty for " + prop);
      assert_throws_dom("SecurityError", function() { win.location[prop] },
                        "Should throw reading href on Location");
    }
    else {
      assert_throws_dom("SecurityError", function() { win.location[prop]; }, "Should throw when accessing " + prop + " on Location");
      assert_throws_dom("SecurityError", function() { Object.getOwnPropertyDescriptor(win.location, prop); },
                        "Should throw when accessing property descriptor for " + prop + " on Location");
      assert_throws_dom("SecurityError", function() { Object.prototype.hasOwnProperty.call(win.location, prop); },
                        "Should throw when invoking hasOwnProperty for " + prop + " on Location");
      assert_throws_dom("SecurityError", function() { win.location[prop] = undefined; }, "Should throw when writing to " + prop + " on Location");
    }
  }
}, "Only certain properties are accessible cross-origin");

addPromiseTest(async function(win, test_obj) {
  async function checkProperties(objName, allowedlists) {
    var localObj = window[objName];
    var otherObj = win[objName];

    for (var prop in localObj) {
      let desc;
      for (let obj = localObj; !desc; obj = Object.getPrototypeOf(obj)) {
        desc = Object.getOwnPropertyDescriptor(obj, prop);

      }

      if ("value" in desc) {
        if (typeof desc.value === "function" && String(desc.value).includes("[native code]")) {
          if (allowedlists.promiseMethods.includes(prop)) {
            await promise_rejects_dom(test_obj, "SecurityError", desc.value.call(otherObj),
                                  `Should throw when calling ${objName}.${prop} with cross-origin this object`);
          } else if (!allowedlists.methods.includes(prop)) {
            for (let args of methodArgs.get(prop) || [[]]) {
              assert_throws_dom("SecurityError", desc.value.bind(otherObj, ...args),
                                `Should throw when calling ${objName}.${prop} with cross-origin this object`);
            }

          } else {
            for (let args of methodArgs.get(prop) || [[]]) {
              desc.value.apply(otherObj, args); // Shouldn't throw.
            }
          }
        }
      } else {
        if (desc.get) {
          if (allowedlists.getters.includes(prop)) {
            desc.get.call(otherObj); // Shouldn't throw.
          } else {
            assert_throws_dom("SecurityError", desc.get.bind(otherObj),
                              `Should throw when calling ${objName}.${prop} getter with cross-origin this object`);
          }
        }
        if (desc.set) {
          if (allowedlists.setters.includes(prop)) {
            desc.set.call(otherObj, "javascript:undefined"); // Shouldn't throw.
          } else {
            assert_throws_dom("SecurityError", desc.set.bind(otherObj, "foo"),
                              `Should throw when calling ${objName}.${prop} setter with cross-origin this object`);
          }
        }
      }
    }
  }

  await checkProperties("location", locationAllowlists);
  await checkProperties("window", windowAllowlists);
}, "Only certain properties are usable as cross-origin this objects");

/*
 * ES Internal Methods.
 */

/*
 * [[GetPrototypeOf]]
 */
addTest(function(win) {
  assert_equals(Object.getPrototypeOf(win), null, "cross-origin Window proto is null");
  assert_equals(Object.getPrototypeOf(win.location), null, "cross-origin Location proto is null (__proto__)");
  var protoGetter = Object.getOwnPropertyDescriptor(Object.prototype, '__proto__').get;
  assert_equals(protoGetter.call(win), null, "cross-origin Window proto is null");
  assert_equals(protoGetter.call(win.location), null, "cross-origin Location proto is null (__proto__)");
  assert_throws_dom("SecurityError", function() { win.__proto__; }, "__proto__ property not available cross-origin");
  assert_throws_dom("SecurityError", function() { win.location.__proto__; }, "__proto__ property not available cross-origin");

}, "[[GetPrototypeOf]] should return null");

/*
 * [[SetPrototypeOf]]
 */
addTest(function(win) {
  assert_throws_dom("SecurityError", function() { win.__proto__ = new Object(); }, "proto set on cross-origin Window");
  assert_throws_dom("SecurityError", function() { win.location.__proto__ = new Object(); }, "proto set on cross-origin Location");
  var setters = [Object.getOwnPropertyDescriptor(Object.prototype, '__proto__').set];
  if (Object.setPrototypeOf)
    setters.push(function(p) { Object.setPrototypeOf(this, p); });
  setters.forEach(function(protoSetter) {
    assert_throws_js(TypeError, function() { protoSetter.call(win, new Object()); }, "proto setter |call| on cross-origin Window");
    assert_throws_js(TypeError, function() { protoSetter.call(win.location, new Object()); }, "proto setter |call| on cross-origin Location");
  });
  // Hack to avoid "duplicate test name" harness issues.
  setters.forEach(function(protoSetter) {
    test(function() { protoSetter.call(win, null); },
         "proto setter |call| on cross-origin Window with null (" + protoSetter + ", " + winName(win) + ")");
    test(function() { protoSetter.call(win.location, null); },
         "proto setter |call| on cross-origin Location with null (" + protoSetter + ", " + winName(win) + ")");
  });
  if (Reflect.setPrototypeOf) {
    assert_false(Reflect.setPrototypeOf(win, new Object()),
                 "Reflect.setPrototypeOf on cross-origin Window");
    assert_true(Reflect.setPrototypeOf(win, null),
                "Reflect.setPrototypeOf on cross-origin Window with null");
    assert_false(Reflect.setPrototypeOf(win.location, new Object()),
                "Reflect.setPrototypeOf on cross-origin Location");
    assert_true(Reflect.setPrototypeOf(win.location, null),
                "Reflect.setPrototypeOf on cross-origin Location with null");
  }
}, "[[SetPrototypeOf]] should return false");

/*
 * [[IsExtensible]]
 */
addTest(function(win) {
  assert_true(Object.isExtensible(win), "cross-origin Window should be extensible");
  assert_true(Object.isExtensible(win.location), "cross-origin Location should be extensible");
}, "[[IsExtensible]] should return true for cross-origin objects");

/*
 * [[PreventExtensions]]
 */
addTest(function(win) {
  assert_throws_js(TypeError, function() { Object.preventExtensions(win) },
                   "preventExtensions on cross-origin Window should throw");
  assert_throws_js(TypeError, function() { Object.preventExtensions(win.location) },
                   "preventExtensions on cross-origin Location should throw");
  assert_false(Reflect.preventExtensions(win),
              "Reflect.preventExtensions on cross-origin Window");
  assert_false(Reflect.preventExtensions(win.location),
              "Reflect.preventExtensions on cross-origin Location");
}, "[[PreventExtensions]] should return false cross-origin objects");

/*
 * [[GetOwnProperty]]
 */

addTest(function(win) {
  assert_true(isObject(Object.getOwnPropertyDescriptor(win, 'close')), "win.close is |own|");
  assert_true(isObject(Object.getOwnPropertyDescriptor(win, 'top')), "win.top is |own|");
  assert_true(isObject(Object.getOwnPropertyDescriptor(win.location, 'href')), "win.location.href is |own|");
  assert_true(isObject(Object.getOwnPropertyDescriptor(win.location, 'replace')), "win.location.replace is |own|");
}, "[[GetOwnProperty]] - Properties on cross-origin objects should be reported |own|");

function checkPropertyDescriptor(desc, propName, expectWritable) {
  const isSymbol = typeof(propName) === "symbol";
  const isArrayIndexPropertyName = !isSymbol && !isNaN(parseInt(propName, 10));
  propName = String(propName);
  assert_true(isObject(desc), "property descriptor for " + propName + " should exist");
  assert_equals(desc.configurable, true, "property descriptor for " + propName + " should be configurable");
  if (!isArrayIndexPropertyName) {
    assert_equals(desc.enumerable, false, "property descriptor for " + propName + " should not be enumerable");
    if (isSymbol || propName == "then") {
      assert_true("value" in desc,
                  "property descriptor for " + propName + " should be a value descriptor");
      assert_equals(desc.value, undefined,
                  "symbol-named cross-origin visible prop " + propName +
                  " should come back as undefined");
    }
  } else {
    assert_equals(desc.enumerable, true, "property descriptor for " + propName + " should be enumerable");
  }
  if ('value' in desc)
    assert_equals(desc.writable, expectWritable, "property descriptor for " + propName + " should have writable: " + expectWritable);
  else
    assert_equals(typeof desc.set != 'undefined', expectWritable,
                  "property descriptor for " + propName + " should " + (expectWritable ? "" : "not ") + "have setter");
}

addTest(function(win) {
  windowAllowlists.props.forEach(function(prop) {
    var desc = Object.getOwnPropertyDescriptor(win, prop);
    checkPropertyDescriptor(desc, prop, prop == 'location');
  });
  checkPropertyDescriptor(Object.getOwnPropertyDescriptor(win.location, 'replace'), 'replace', false);
  checkPropertyDescriptor(Object.getOwnPropertyDescriptor(win.location, 'href'), 'href', true);
  assert_equals(typeof Object.getOwnPropertyDescriptor(win.location, 'href').get, 'undefined', "Cross-origin location should have no href getter");
  allowedSymbols.forEach(function(prop) {
    var desc = Object.getOwnPropertyDescriptor(win.location, prop);
    checkPropertyDescriptor(desc, prop, false);
  });
}, "[[GetOwnProperty]] - Property descriptors for cross-origin properties should be set up correctly");

addThenTest(function(win) {
  assert_equals(typeof win.then, "object");
}, "[[GetOwnProperty]] - Subframe named 'then' should shadow the default 'then' value");

addThenTest(function(win) {
  assert_equals(typeof win.close, "function");
  assert_equals(typeof win.open, "object");
}, "[[GetOwnProperty]] - Subframes should be visible cross-origin only if their names don't match the names of cross-origin-exposed IDL properties");

addTest(function(win) {
  assert_equals(typeof Object.getOwnPropertyDescriptor(win, '0').value, "object");
  assert_equals(typeof Object.getOwnPropertyDescriptor(win, '1').value, "object");
  assert_throws_dom("SecurityError", function() {
    Object.getOwnPropertyDescriptor(win, '2');
  });
}, "[[GetOwnProperty]] - Should be able to get a property descriptor for an indexed property only if it corresponds to a child window.");

/*
 * [[Delete]]
 */
addTest(function(win) {
  assert_throws_dom("SecurityError", function() { delete win[0]; }, "Can't delete cross-origin indexed property");
  assert_throws_dom("SecurityError", function() { delete win[100]; }, "Can't delete cross-origin indexed property");
  assert_throws_dom("SecurityError", function() { delete win.location; }, "Can't delete cross-origin property");
  assert_throws_dom("SecurityError", function() { delete win.parent; }, "Can't delete cross-origin property");
  assert_throws_dom("SecurityError", function() { delete win.length; }, "Can't delete cross-origin property");
  assert_throws_dom("SecurityError", function() { delete win.document; }, "Can't delete cross-origin property");
  assert_throws_dom("SecurityError", function() { delete win.foopy; }, "Can't delete cross-origin property");
  assert_throws_dom("SecurityError", function() { delete win.location.href; }, "Can't delete cross-origin property");
  assert_throws_dom("SecurityError", function() { delete win.location.replace; }, "Can't delete cross-origin property");
  assert_throws_dom("SecurityError", function() { delete win.location.port; }, "Can't delete cross-origin property");
  assert_throws_dom("SecurityError", function() { delete win.location.foopy; }, "Can't delete cross-origin property");
}, "[[Delete]] Should throw on cross-origin objects");

/*
 * [[DefineOwnProperty]]
 */
function checkDefine(obj, prop) {
  var valueDesc = { configurable: true, enumerable: false, writable: false, value: 2 };
  var accessorDesc = { configurable: true, enumerable: false, get: function() {} };
  assert_throws_dom("SecurityError", function() { Object.defineProperty(obj, prop, valueDesc); }, "Can't define cross-origin value property " + prop);
  assert_throws_dom("SecurityError", function() { Object.defineProperty(obj, prop, accessorDesc); }, "Can't define cross-origin accessor property " + prop);
}
addTest(function(win) {
  checkDefine(win, 'length');
  checkDefine(win, 'parent');
  checkDefine(win, 'location');
  checkDefine(win, 'document');
  checkDefine(win, 'foopy');
  checkDefine(win.location, 'href');
  checkDefine(win.location, 'replace');
  checkDefine(win.location, 'port');
  checkDefine(win.location, 'foopy');
}, "[[DefineOwnProperty]] Should throw for cross-origin objects");

/*
 * EnumerateObjectProperties (backed by [[OwnPropertyKeys]])
 */

addTest(function(win) {
  let i = 0;
  for (var prop in win) {
    i++;
    assert_true(windowAllowlists.indices.includes(prop), prop + " is not safelisted for a cross-origin Window");
  }
  assert_equals(i, windowAllowlists.indices.length, "Enumerate all enumerable safelisted cross-origin Window properties");
  i = 0;
  for (var prop in win.location) {
    i++;
  }
  assert_equals(i, 0, "There's nothing to enumerate for cross-origin Location properties");
}, "Can only enumerate safelisted enumerable properties");

/*
 * [[OwnPropertyKeys]]
 */

addTest(function(win) {
  assert_array_equals(Object.getOwnPropertyNames(win).sort(),
                      windowAllowlists.propNames,
                      "Object.getOwnPropertyNames() gives the right answer for cross-origin Window");
  assert_array_equals(Object.keys(win).sort(),
                      windowAllowlists.indices,
                      "Object.keys() gives the right answer for cross-origin Window");
  assert_array_equals(Object.getOwnPropertyNames(win.location).sort(),
                      locationAllowlists.propNames,
                      "Object.getOwnPropertyNames() gives the right answer for cross-origin Location");
  assert_equals(Object.keys(win.location).length, 0,
                      "Object.keys() gives the right answer for cross-origin Location");
}, "[[OwnPropertyKeys]] should return all properties from cross-origin objects");

addTest(function(win) {
  assert_array_equals(Object.getOwnPropertySymbols(win), allowedSymbols,
    "Object.getOwnPropertySymbols() should return the three symbol-named properties that are exposed on a cross-origin Window");
  assert_array_equals(Object.getOwnPropertySymbols(win.location),
                      allowedSymbols,
    "Object.getOwnPropertySymbols() should return the three symbol-named properties that are exposed on a cross-origin Location");
}, "[[OwnPropertyKeys]] should return the right symbol-named properties for cross-origin objects");

addTest(function(win) {
  var allWindowProps = Reflect.ownKeys(win);
  indexedWindowProps = allWindowProps.slice(0, windowAllowlists.indices.length);
  stringWindowProps = allWindowProps.slice(0, -1 * allowedSymbols.length);
  symbolWindowProps = allWindowProps.slice(-1 * allowedSymbols.length);
  // stringWindowProps should have "then" last in this case.  Do this
  // check before we call stringWindowProps.sort() below.
  assert_equals(stringWindowProps[stringWindowProps.length - 1], "then",
                "'then' property should be added to the end of the string list if not there");
  assert_array_equals(indexedWindowProps, windowAllowlists.indices,
                      "Reflect.ownKeys should start with the indices exposed on the cross-origin window.");
  assert_array_equals(stringWindowProps.sort(), windowAllowlists.propNames,
                      "Reflect.ownKeys should continue with the cross-origin window properties for a cross-origin Window.");
  assert_array_equals(symbolWindowProps, allowedSymbols,
                      "Reflect.ownKeys should end with the cross-origin symbols for a cross-origin Window.");

  var allLocationProps = Reflect.ownKeys(win.location);
  stringLocationProps = allLocationProps.slice(0, -1 * allowedSymbols.length);
  symbolLocationProps = allLocationProps.slice(-1 * allowedSymbols.length);
  assert_array_equals(stringLocationProps.sort(), locationAllowlists.propNames,
                      "Reflect.ownKeys should start with the cross-origin window properties for a cross-origin Location.")
  assert_array_equals(symbolLocationProps, allowedSymbols,
                      "Reflect.ownKeys should end with the cross-origin symbols for a cross-origin Location.")
}, "[[OwnPropertyKeys]] should place the symbols after the property names after the subframe indices");

addThenTest(function(win) {
  var stringProps = Object.getOwnPropertyNames(win);
  // Named frames are not exposed via [[OwnPropertyKeys]].
  assert_equals(stringProps.indexOf("a"), -1);
  assert_equals(stringProps.indexOf("b"), -1);
  assert_equals(typeof win.a, "object");
  assert_equals(typeof win.b, "object");
  assert_equals(stringProps[stringProps.length - 1], "then");
  assert_equals(stringProps.indexOf("then"), stringProps.lastIndexOf("then"));
}, "[[OwnPropertyKeys]] should not reorder where 'then' appears if it's a named subframe, nor add another copy of 'then'");

addTest(function(win) {
  assert_equals(B.eval('parent.' + winName(win)), win, "A and B observe the same identity for C's Window");
  assert_equals(B.eval('parent.' + winName(win) + '.location'), win.location, "A and B observe the same identity for C's Location");
}, "A and B jointly observe the same identity for cross-origin Window and Location");

function checkFunction(f, proto) {
  var name = f.name || '<missing name>';
  assert_equals(typeof f, 'function', name + " is a function");
  assert_equals(Object.getPrototypeOf(f), proto, f.name + " has the right prototype");
}

addTest(function(win) {
  checkFunction(win.close, Function.prototype);
  checkFunction(win.location.replace, Function.prototype);
}, "Cross-origin functions get local Function.prototype");

addTest(function(win) {
  assert_true(isObject(Object.getOwnPropertyDescriptor(win, 'parent')),
              "Need to be able to use Object.getOwnPropertyDescriptor do this test");
  checkFunction(Object.getOwnPropertyDescriptor(win, 'parent').get, Function.prototype);
  checkFunction(Object.getOwnPropertyDescriptor(win.location, 'href').set, Function.prototype);
}, "Cross-origin Window accessors get local Function.prototype");

addTest(function(win) {
  checkFunction(close, Function.prototype);
  assert_not_equals(close, B.close, 'same-origin Window functions get their own object');
  assert_not_equals(close, win.close, 'cross-origin Window functions get their own object');
  var close_B = B.eval('parent.' + winName(win) + '.close');
  assert_not_equals(close, close_B, 'close_B is unique when viewed by the parent');
  assert_not_equals(close_B, win.close, 'different Window functions per-incumbent script settings object');
  checkFunction(close_B, B.Function.prototype);

  checkFunction(location.replace, Function.prototype);
  assert_not_equals(location.replace, win.location.replace, "cross-origin Location functions get their own object");
  var replace_B = B.eval('parent.' + winName(win) + '.location.replace');
  assert_not_equals(replace_B, win.location.replace, 'different Location functions per-incumbent script settings object');
  checkFunction(replace_B, B.Function.prototype);
}, "Same-origin observers get different functions for cross-origin objects");

addTest(function(win) {
  assert_true(isObject(Object.getOwnPropertyDescriptor(win, 'parent')),
              "Need to be able to use Object.getOwnPropertyDescriptor do this test");
  var get_self_parent = Object.getOwnPropertyDescriptor(window, 'parent').get;
  var get_parent_A = Object.getOwnPropertyDescriptor(win, 'parent').get;
  var get_parent_B = B.eval('Object.getOwnPropertyDescriptor(parent.' + winName(win) + ', "parent").get');
  assert_not_equals(get_self_parent, get_parent_A, 'different Window accessors per-incumbent script settings object');
  assert_not_equals(get_parent_A, get_parent_B, 'different Window accessors per-incumbent script settings object');
  checkFunction(get_self_parent, Function.prototype);
  checkFunction(get_parent_A, Function.prototype);
  checkFunction(get_parent_B, B.Function.prototype);
}, "Same-origin observers get different accessors for cross-origin Window");

addTest(function(win) {
  var set_self_href = Object.getOwnPropertyDescriptor(window.location, 'href').set;
  var set_href_A = Object.getOwnPropertyDescriptor(win.location, 'href').set;
  var set_href_B = B.eval('Object.getOwnPropertyDescriptor(parent.' + winName(win) + '.location, "href").set');
  assert_not_equals(set_self_href, set_href_A, 'different Location accessors per-incumbent script settings object');
  assert_not_equals(set_href_A, set_href_B, 'different Location accessors per-incumbent script settings object');
  checkFunction(set_self_href, Function.prototype);
  checkFunction(set_href_A, Function.prototype);
  checkFunction(set_href_B, B.Function.prototype);
}, "Same-origin observers get different accessors for cross-origin Location");

addTest(function(win) {
  assert_equals({}.toString.call(win), "[object Object]");
  assert_equals({}.toString.call(win.location), "[object Object]");
}, "{}.toString.call() does the right thing on cross-origin objects");

addPromiseTest(function(win) {
  return Promise.resolve(win).then((arg) => {
    assert_equals(arg, win);
  });
}, "Resolving a promise with a cross-origin window without a 'then' subframe should work");

addPromiseThenTest(function(win) {
  return Promise.resolve(win).then((arg) => {
    assert_equals(arg, win);
  });
}, "Resolving a promise with a cross-origin window with a 'then' subframe should work");

addPromiseThenTest(function(win) {
  return Promise.resolve(win.location).then((arg) => {
    assert_equals(arg, win.location);
  });
}, "Resolving a promise with a cross-origin location should work");

addTest(function(win) {
  var desc = Object.getOwnPropertyDescriptor(window, "onmouseenter");
  var f = () => {};

  // Check that it has [LegacyLenientThis] behavior
  assert_equals(desc.get.call({}), undefined, "getter should return undefined");
  desc.set.call({}, f); // Should not throw.

  // Check that we can apply it to a same-origin window.
  assert_equals(desc.get.call(B), null, "Should be able to read the value");
  desc.set.call(B, f);
  assert_equals(desc.get.call(B), f, "Value should have updated");
  // And reset it for our next test
  desc.set.call(B, null);
  assert_equals(desc.get.call(B), null, "Should have been reset");

  // Check that applying it to a cross-origin window throws instead of doing
  // the [LegacyLenientThis] behavior.
  assert_throws_dom("SecurityError", () => {
    desc.get.call(win);
  }, "Should throw when getting cross-origin");
  assert_throws_dom("SecurityError", () => {
    desc.set.call(win, f);
  }, "Should throw when setting cross-origin");
}, "LegacyLenientThis behavior");

// We do a fresh load of the subframes for each test to minimize side-effects.
// It would be nice to reload ourselves as well, but we can't do that without
// disrupting the test harness.
function testDone() {
  if (testList.length != 0) {
    reloadSubframes(runNextTest);
  } else {
    done();
  }
}

async function runNextTest() {
  var entry = testList.shift();
  if (entry.promiseTest) {
    for (let t of entry.tests) {
      await new Promise(resolve => {
        promise_test(test_obj => {
          return new Promise(res => res(t.func(test_obj))).finally(resolve);
        }, t.desc);
      });
    }
  } else {
    for (let t of entry.tests) {
      test(t.func, t.desc);
    }
  }
  testDone();
}
reloadSubframes(runNextTest);

</script>
