text: /*
  Distributed under both the W3C Test Suite License [1] and the W3C
  3-clause BSD License [2]. To contribute to a W3C Test Suite, see the
  policies and contribution forms [3].
  
  [1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license
  [2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license
  [3] http://www.w3.org/2004/10/27-testcases
  */
  
  /* For user documentation see docs/_writing-tests/idlharness.md */
  
  /**
   * Notes for people who want to edit this file (not just use it as a library):
   *
   * Most of the interesting stuff happens in the derived classes of IdlObject,
   * especially IdlInterface.  The entry point for all IdlObjects is .test(),
   * which is called by IdlArray.test().  An IdlObject is conceptually just
   * "thing we want to run tests on", and an IdlArray is an array of IdlObjects
   * with some additional data thrown in.
   *
   * The object model is based on what WebIDLParser.js produces, which is in turn
   * based on its pegjs grammar.  If you want to figure out what properties an
   * object will have from WebIDLParser.js, the best way is to look at the
   * grammar:
   *
   *   https://github.com/darobin/webidl.js/blob/master/lib/grammar.peg
   *
   * So for instance:
   *
   *   // interface definition
   *   interface
   *       =   extAttrs:extendedAttributeList? S? "interface" S name:identifier w herit:ifInheritance? w "{" w mem:ifMember* w "}" w ";" w
   *           { return { type: "interface", name: name, inheritance: herit, members: mem, extAttrs: extAttrs }; }
   *
   * This means that an "interface" object will have a .type property equal to
   * the string "interface", a .name property equal to the identifier that the
   * parser found, an .inheritance property equal to either null or the result of
   * the "ifInheritance" production found elsewhere in the grammar, and so on.
   * After each grammatical production is a JavaScript function in curly braces
   * that gets called with suitable arguments and returns some JavaScript value.
   *
   * (Note that the version of WebIDLParser.js we use might sometimes be
   * out-of-date or forked.)
   *
   * The members and methods of the classes defined by this file are all at least
   * briefly documented, hopefully.
   */
  (function(){
  "use strict";
  // Support subsetTestByKey from /common/subset-tests-by-key.js, but make it optional
  if (!('subsetTestByKey' in self)) {
      self.subsetTestByKey = function(key, callback, ...args) {
        return callback(...args);
      }
      self.shouldRunSubTest = () => true;
  }
  /// Helpers ///
  function constValue (cnt)
  {
      if (cnt.type === "null") return null;
      if (cnt.type === "NaN") return NaN;
      if (cnt.type === "Infinity") return cnt.negative ? -Infinity : Infinity;
      if (cnt.type === "number") return +cnt.value;
      return cnt.value;
  }
  
  function minOverloadLength(overloads)
  {
      // "The value of the Function object’s “length” property is
      // a Number determined as follows:
      // ". . .
      // "Return the length of the shortest argument list of the
      // entries in S."
      if (!overloads.length) {
          return 0;
      }
  
      return overloads.map(function(attr) {
          return attr.arguments ? attr.arguments.filter(function(arg) {
              return !arg.optional && !arg.variadic;
          }).length : 0;
      })
      .reduce(function(m, n) { return Math.min(m, n); });
  }
  
  function throwOrReject(a_test, operation, fn, obj, args, message, cb)
  {
      if (operation.idlType.generic !== "Promise") {
          assert_throws(new TypeError(), function() {
              fn.apply(obj, args);
          }, message);
          cb();
      } else {
          try {
              promise_rejects(a_test, new TypeError(), fn.apply(obj, args), message).then(cb, cb);
          } catch (e){
              a_test.step(function() {
                  assert_unreached("Throws \"" + e + "\" instead of rejecting promise");
                  cb();
              });
          }
      }
  }
  
  function awaitNCallbacks(n, cb, ctx)
  {
      var counter = 0;
      return function() {
          counter++;
          if (counter >= n) {
              cb();
          }
      };
  }
  
  var fround =
  (function(){
      if (Math.fround) return Math.fround;
  
      var arr = new Float32Array(1);
      return function fround(n) {
          arr[0] = n;
          return arr[0];
      };
  })();
  
  /// IdlHarnessError ///
  // Entry point
  self.IdlHarnessError = function(message)
  {
      /**
       * Message to be printed as the error's toString invocation.
       */
      this.message = message;
  };
  
  IdlHarnessError.prototype = Object.create(Error.prototype);
  
  IdlHarnessError.prototype.toString = function()
  {
      return this.message;
  };
  
  
  /// IdlArray ///
  // Entry point
  self.IdlArray = function()
  {
      /**
       * A map from strings to the corresponding named IdlObject, such as
       * IdlInterface or IdlException.  These are the things that test() will run
       * tests on.
       */
      this.members = {};
  
      /**
       * A map from strings to arrays of strings.  The keys are interface or
       * exception names, and are expected to also exist as keys in this.members
       * (otherwise they'll be ignored).  This is populated by add_objects() --
       * see documentation at the start of the file.  The actual tests will be
       * run by calling this.members[name].test_object(obj) for each obj in
       * this.objects[name].  obj is a string that will be eval'd to produce a
       * JavaScript value, which is supposed to be an object implementing the
       * given IdlObject (interface, exception, etc.).
       */
      this.objects = {};
  
      /**
       * When adding multiple collections of IDLs one at a time, an earlier one
       * might contain a partial interface or implements statement that depends
       * on a later one.  Save these up and handle them right before we run
       * tests.
       *
       * .partials is simply an array of objects from WebIDLParser.js'
       * "partialinterface" production.  .implements maps strings to arrays of
       * strings, such that
       *
       *   A implements B;
       *   A implements C;
       *   D implements E;
       *
       * results in this["implements"] = { A: ["B", "C"], D: ["E"] }.
       *
       * Similarly,
       *
       *   interface A : B {};
       *   interface B : C {};
       *
       * results in this["inheritance"] = { A: "B", B: "C" }
       */
      this.partials = [];
      this["implements"] = {};
      this["includes"] = {};
      this["inheritance"] = {};
  
      /**
       * Record of skipped IDL items, in case we later realize that they are a
       * dependency (to retroactively process them).
       */
      this.skipped = new Map();
  };
  
  IdlArray.prototype.add_idls = function(raw_idls, options)
  {
      /** Entry point.  See documentation at beginning of file. */
      this.internal_add_idls(WebIDL2.parse(raw_idls), options);
  };
  
  IdlArray.prototype.add_untested_idls = function(raw_idls, options)
  {
      /** Entry point.  See documentation at beginning of file. */
      var parsed_idls = WebIDL2.parse(raw_idls);
      this.mark_as_untested(parsed_idls);
      this.internal_add_idls(parsed_idls, options);
  };
  
  IdlArray.prototype.mark_as_untested = function (parsed_idls)
  {
      for (var i = 0; i < parsed_idls.length; i++) {
          parsed_idls[i].untested = true;
          if ("members" in parsed_idls[i]) {
              for (var j = 0; j < parsed_idls[i].members.length; j++) {
                  parsed_idls[i].members[j].untested = true;
              }
          }
      }
  };
  
  IdlArray.prototype.is_excluded_by_options = function (name, options)
  {
      return options &&
          (options.except && options.except.includes(name)
           || options.only && !options.only.includes(name));
  };
  
  IdlArray.prototype.add_dependency_idls = function(raw_idls, options)
  {
      const parsed_idls = WebIDL2.parse(raw_idls);
      const new_options = { only: [] }
  
      const all_deps = new Set();
      Object.values(this.inheritance).forEach(v => all_deps.add(v));
      Object.entries(this.implements).forEach(([k, v]) => {
          all_deps.add(k);
          all_deps.add(v);
      });
      // NOTE: If 'A includes B' for B that we care about, then A is also a dep.
      Object.keys(this.includes).forEach(k => {
          all_deps.add(k);
          this.includes[k].forEach(v => all_deps.add(v));
      });
      this.partials.forEach(p => all_deps.add(p.name));
      // Add 'TypeOfType' for each "typedef TypeOfType MyType;" entry.
      Object.entries(this.members).forEach(([k, v]) => {
          if (v instanceof IdlTypedef) {
              let defs = v.idlType.union
                  ? v.idlType.idlType.map(t => t.idlType)
                  : [v.idlType.idlType];
              defs.forEach(d => all_deps.add(d));
          }
      });
  
      // Add the attribute idlTypes of all the nested members of idls.
      const attrDeps = parsedIdls => {
          return parsedIdls.reduce((deps, parsed) => {
              if (parsed.members) {
                  for (const attr of Object.values(parsed.members).filter(m => m.type === 'attribute')) {
                      let attrType = attr.idlType;
                      // Check for generic members (e.g. FrozenArray<MyType>)
                      if (attrType.generic) {
                          deps.add(attrType.generic);
                          attrType = attrType.idlType;
                      }
                      deps.add(attrType.idlType);
                  }
              }
              if (parsed.base in this.members) {
                  attrDeps([this.members[parsed.base]]).forEach(dep => deps.add(dep));
              }
              return deps;
          }, new Set());
      };
  
      const testedMembers = Object.values(this.members).filter(m => !m.untested && m.members);
      attrDeps(testedMembers).forEach(dep => all_deps.add(dep));
  
      const testedPartials = this.partials.filter(m => !m.untested && m.members);
      attrDeps(testedPartials).forEach(dep => all_deps.add(dep));
  
  
      if (options && options.except && options.only) {
          throw new IdlHarnessError("The only and except options can't be used together.");
      }
  
      const defined_or_untested = name => {
          // NOTE: Deps are untested, so we're lenient, and skip re-encountered definitions.
          // e.g. for 'idl' containing A:B, B:C, C:D
          //      array.add_idls(idl, {only: ['A','B']}).
          //      array.add_dependency_idls(idl);
          // B would be encountered as tested, and encountered as a dep, so we ignore.
          return name in this.members
              || this.is_excluded_by_options(name, options);
      }
      // Maps name -> [parsed_idl, ...]
      const process = function(parsed) {
          var deps = [];
          if (parsed.name) {
              deps.push(parsed.name);
          } else if (parsed.type === "implements") {
              deps.push(parsed.target);
              deps.push(parsed.implements);
          } else if (parsed.type === "includes") {
              deps.push(parsed.target);
              deps.push(parsed.includes);
          }
  
          deps = deps.filter(function(name) {
              if (!name
                  || name === parsed.name && defined_or_untested(name)
                  || !all_deps.has(name)) {
                  // Flag as skipped, if it's not already processed, so we can
                  // come back to it later if we retrospectively call it a dep.
                  if (name && !(name in this.members)) {
                      this.skipped.has(name)
                          ? this.skipped.get(name).push(parsed)
                          : this.skipped.set(name, [parsed]);
                  }
                  return false;
              }
              return true;
          }.bind(this));
  
          deps.forEach(function(name) {
              if (!new_options.only.includes(name)) {
                  new_options.only.push(name);
              }
  
              const follow_up = new Set();
              for (const dep_type of ["inheritance", "implements", "includes"]) {
                  if (parsed[dep_type]) {
                      const inheriting = parsed[dep_type];
                      const inheritor = parsed.name || parsed.target;
                      const deps = [inheriting];
                      // For A includes B, we can ignore A, unless B (or some of its
                      // members) is being tested.
                      if (dep_type !== "includes"
                          || inheriting in this.members && !this.members[inheriting].untested
                          || this.partials.some(function(p) {
                                  return p.name === inheriting;
                              })) {
                          deps.push(inheritor);
                      }
                      for (const dep of deps) {
                          if (!new_options.only.includes(dep)) {
                              new_options.only.push(dep);
                          }
                          all_deps.add(dep);
                          follow_up.add(dep);
                      }
                  }
              }
  
              for (const deferred of follow_up) {
                  if (this.skipped.has(deferred)) {
                      const next = this.skipped.get(deferred);
                      this.skipped.delete(deferred);
                      next.forEach(process);
                  }
              }
          }.bind(this));
      }.bind(this);
  
      for (let parsed of parsed_idls) {
          process(parsed);
      }
  
      this.mark_as_untested(parsed_idls);
  
      if (new_options.only.length) {
          this.internal_add_idls(parsed_idls, new_options);
      }
  }
  
  IdlArray.prototype.internal_add_idls = function(parsed_idls, options)
  {
      /**
       * Internal helper called by add_idls() and add_untested_idls().
       *
       * parsed_idls is an array of objects that come from WebIDLParser.js's
       * "definitions" production.  The add_untested_idls() entry point
       * additionally sets an .untested property on each object (and its
       * .members) so that they'll be skipped by test() -- they'll only be
       * used for base interfaces of tested interfaces, return types, etc.
       *
       * options is a dictionary that can have an only or except member which are
       * arrays. If only is given then only members, partials and interface
       * targets listed will be added, and if except is given only those that
       * aren't listed will be added. Only one of only and except can be used.
       */
  
      if (options && options.only && options.except)
      {
          throw new IdlHarnessError("The only and except options can't be used together.");
      }
  
      var should_skip = name => {
          return this.is_excluded_by_options(name, options);
      }
  
      parsed_idls.forEach(function(parsed_idl)
      {
          var partial_types = [
              "interface",
              "interface mixin",
              "dictionary",
              "namespace",
          ];
          if (parsed_idl.partial && partial_types.includes(parsed_idl.type))
          {
              if (should_skip(parsed_idl.name))
              {
                  return;
              }
              this.partials.push(parsed_idl);
              return;
          }
  
          if (parsed_idl.type == "implements")
          {
              if (should_skip(parsed_idl.target))
              {
                  return;
              }
              if (!(parsed_idl.target in this["implements"]))
              {
                  this["implements"][parsed_idl.target] = [];
              }
              this["implements"][parsed_idl.target].push(parsed_idl["implements"]);
              return;
          }
  
          if (parsed_idl.type == "includes")
          {
              if (should_skip(parsed_idl.target))
              {
                  return;
              }
              if (!(parsed_idl.target in this["includes"]))
              {
                  this["includes"][parsed_idl.target] = [];
              }
              this["includes"][parsed_idl.target].push(parsed_idl["includes"]);
              return;
          }
  
          parsed_idl.array = this;
          if (should_skip(parsed_idl.name))
          {
              return;
          }
          if (parsed_idl.name in this.members)
          {
              throw new IdlHarnessError("Duplicate identifier " + parsed_idl.name);
          }
  
          if (parsed_idl["inheritance"]) {
              // NOTE: Clash should be impossible (would require redefinition of parsed_idl.name).
              if (parsed_idl.name in this["inheritance"]
                  && parsed_idl["inheritance"] != this["inheritance"][parsed_idl.name]) {
                  throw new IdlHarnessError(
                      `Inheritance for ${parsed_idl.name} was already defined`);
              }
              this["inheritance"][parsed_idl.name] = parsed_idl["inheritance"];
          }
  
          switch(parsed_idl.type)
          {
          case "interface":
              this.members[parsed_idl.name] =
                  new IdlInterface(parsed_idl, /* is_callback = */ false, /* is_mixin = */ false);
              break;
  
          case "interface mixin":
              this.members[parsed_idl.name] =
                  new IdlInterface(parsed_idl, /* is_callback = */ false, /* is_mixin = */ true);
              break;
  
          case "dictionary":
              // Nothing to test, but we need the dictionary info around for type
              // checks
              this.members[parsed_idl.name] = new IdlDictionary(parsed_idl);
              break;
  
          case "typedef":
              this.members[parsed_idl.name] = new IdlTypedef(parsed_idl);
              break;
  
          case "callback":
              this.members[parsed_idl.name] = new IdlCallback(parsed_idl);
              break;
  
          case "enum":
              this.members[parsed_idl.name] = new IdlEnum(parsed_idl);
              break;
  
          case "callback interface":
              this.members[parsed_idl.name] =
                  new IdlInterface(parsed_idl, /* is_callback = */ true, /* is_mixin = */ false);
              break;
  
          case "namespace":
              this.members[parsed_idl.name] = new IdlNamespace(parsed_idl);
              break;
  
          default:
              throw parsed_idl.name + ": " + parsed_idl.type + " not yet supported";
          }
      }.bind(this));
  };
  
  IdlArray.prototype.add_objects = function(dict)
  {
      /** Entry point.  See documentation at beginning of file. */
      for (var k in dict)
      {
          if (k in this.objects)
          {
              this.objects[k] = this.objects[k].concat(dict[k]);
          }
          else
          {
              this.objects[k] = dict[k];
          }
      }
  };
  
  IdlArray.prototype.prevent_multiple_testing = function(name)
  {
      /** Entry point.  See documentation at beginning of file. */
      this.members[name].prevent_multiple_testing = true;
  };
  
  IdlArray.prototype.recursively_get_implements = function(interface_name)
  {
      /**
       * Helper function for test().  Returns an array of things that implement
       * interface_name, so if the IDL contains
       *
       *   A implements B;
       *   B implements C;
       *   B implements D;
       *
       * then recursively_get_implements("A") should return ["B", "C", "D"].
       */
      var ret = this["implements"][interface_name];
      if (ret === undefined)
      {
          return [];
      }
      for (var i = 0; i < this["implements"][interface_name].length; i++)
      {
          ret = ret.concat(this.recursively_get_implements(ret[i]));
          if (ret.indexOf(ret[i]) != ret.lastIndexOf(ret[i]))
          {
              throw new IdlHarnessError("Circular implements statements involving " + ret[i]);
          }
      }
      return ret;
  };
  
  IdlArray.prototype.recursively_get_includes = function(interface_name)
  {
      /**
       * Helper function for test().  Returns an array of things that implement
       * interface_name, so if the IDL contains
       *
       *   A includes B;
       *   B includes C;
       *   B includes D;
       *
       * then recursively_get_includes("A") should return ["B", "C", "D"].
       */
      var ret = this["includes"][interface_name];
      if (ret === undefined)
      {
          return [];
      }
      for (var i = 0; i < this["includes"][interface_name].length; i++)
      {
          ret = ret.concat(this.recursively_get_includes(ret[i]));
          if (ret.indexOf(ret[i]) != ret.lastIndexOf(ret[i]))
          {
              throw new IdlHarnessError("Circular includes statements involving " + ret[i]);
          }
      }
      return ret;
  };
  
  IdlArray.prototype.is_json_type = function(type)
  {
      /**
       * Checks whether type is a JSON type as per
       * https://heycam.github.io/webidl/#dfn-json-types
       */
  
      var idlType = type.idlType;
  
      if (type.generic == "Promise") { return false; }
  
      //  nullable and annotated types don't need to be handled separately,
      //  as webidl2 doesn't represent them wrapped-up (as they're described
      //  in WebIDL).
  
      // union and record types
      if (type.union || type.generic == "record") {
          return idlType.every(this.is_json_type, this);
      }
  
      // sequence types
      if (type.generic == "sequence" || type.generic == "FrozenArray") {
          return this.is_json_type(idlType[0]);
      }
  
      if (typeof idlType != "string") { throw new Error("Unexpected type " + JSON.stringify(idlType)); }
  
      switch (idlType)
      {
         //  Numeric types
         case "byte":
         case "octet":
         case "short":
         case "unsigned short":
         case "long":
         case "unsigned long":
         case "long long":
         case "unsigned long long":
         case "float":
         case "double":
         case "unrestricted float":
         case "unrestricted double":
         // boolean
         case "boolean":
         // string types
         case "DOMString":
         case "ByteString":
         case "USVString":
         // object type
         case "object":
             return true;
         case "Error":
         case "DOMException":
         case "Int8Array":
         case "Int16Array":
         case "Int32Array":
         case "Uint8Array":
         case "Uint16Array":
         case "Uint32Array":
         case "Uint8ClampedArray":
         case "Float32Array":
         case "ArrayBuffer":
         case "DataView":
         case "any":
             return false;
         default:
             var thing = this.members[idlType];
             if (!thing) { throw new Error("Type " + idlType + " not found"); }
             if (thing instanceof IdlEnum) { return true; }
  
             if (thing instanceof IdlTypedef) {
                 return this.is_json_type(thing.idlType);
             }
  
             //  dictionaries where all of their members are JSON types
             if (thing instanceof IdlDictionary) {
                 var stack = thing.get_inheritance_stack();
                 var map = new Map();
                 while (stack.length)
                 {
                     stack.pop().members.forEach(function(m) {
                         map.set(m.name, m.idlType)
                     });
                 }
                 return Array.from(map.values()).every(this.is_json_type, this);
             }
  
             //  interface types that have a toJSON operation declared on themselves or
             //  one of their inherited or consequential interfaces.
             if (thing instanceof IdlInterface) {
                 var base;
                 while (thing)
                 {
                     if (thing.has_to_json_regular_operation()) { return true; }
                     var mixins = this.implements[thing.name] || this.includes[thing.name];
                     if (mixins) {
                         mixins = mixins.map(function(id) {
                             var mixin = this.members[id];
                             if (!mixin) {
                                 throw new Error("Interface " + id + " not found (implemented by " + thing.name + ")");
                             }
                             return mixin;
                         }, this);
                         if (mixins.some(function(m) { return m.has_to_json_regular_operation() } )) { return true; }
                     }
                     if (!thing.base) { return false; }
                     base = this.members[thing.base];
                     if (!base) {
                         throw new Error("Interface " + thing.base + " not found (inherited by " + thing.name + ")");
                     }
                     thing = base;
                 }
                 return false;
             }
             return false;
      }
  };
  
  function exposure_set(object, default_set) {
      var exposed = object.extAttrs && object.extAttrs.filter(a => a.name === "Exposed");
      if (exposed && exposed.length > 1) {
          throw new IdlHarnessError(
              `Multiple 'Exposed' extended attributes on ${object.name}`);
      }
  
      let result = default_set || ["Window"];
      if (result && !(result instanceof Set)) {
          result = new Set(result);
      }
      if (exposed && exposed.length) {
          const { rhs } = exposed[0];
          // Could be a list or a string.
          const set = rhs.type === "identifier-list" ?
              rhs.value.map(id => id.value) :
              [ rhs.value ];
          result = new Set(set);
      }
      if (result && result.has("Worker")) {
          result.delete("Worker");
          result.add("DedicatedWorker");
          result.add("ServiceWorker");
          result.add("SharedWorker");
      }
      return result;
  }
  
  function exposed_in(globals) {
      if ('document' in self) {
          return globals.has("Window");
      }
      if ('DedicatedWorkerGlobalScope' in self &&
          self instanceof DedicatedWorkerGlobalScope) {
          return globals.has("DedicatedWorker");
      }
      if ('SharedWorkerGlobalScope' in self &&
          self instanceof SharedWorkerGlobalScope) {
          return globals.has("SharedWorker");
      }
      if ('ServiceWorkerGlobalScope' in self &&
          self instanceof ServiceWorkerGlobalScope) {
          return globals.has("ServiceWorker");
      }
      throw new IdlHarnessError("Unexpected global object");
  }
  
  /**
   * Asserts that the given error message is thrown for the given function.
   * @param {string|IdlHarnessError} error Expected Error message.
   * @param {Function} idlArrayFunc Function operating on an IdlArray that should throw.
   */
  IdlArray.prototype.assert_throws = function(error, idlArrayFunc)
  {
      try {
          idlArrayFunc.call(this, this);
      } catch (e) {
          if (e instanceof AssertionError) {
              throw e;
          }
          // Assertions for behaviour of the idlharness.js engine.
          if (error instanceof IdlHarnessError) {
              error = error.message;
          }
          if (e.message !== error) {
              throw new IdlHarnessError(`${idlArrayFunc} threw "${e}", not the expected IdlHarnessError "${error}"`);
          }
          return;
      }
      throw new IdlHarnessError(`${idlArrayFunc} did not throw the expected IdlHarnessError`);
  }
  
  IdlArray.prototype.test = function()
  {
      /** Entry point.  See documentation at beginning of file. */
  
      // First merge in all the partial interfaces and implements statements we
      // encountered.
      this.collapse_partials();
  
      for (var lhs in this["implements"])
      {
          this.recursively_get_implements(lhs).forEach(function(rhs)
          {
              var errStr = lhs + " implements " + rhs + ", but ";
              if (!(lhs in this.members)) throw errStr + lhs + " is undefined.";
              if (!(this.members[lhs] instanceof IdlInterface)) throw errStr + lhs + " is not an interface.";
              if (!(rhs in this.members)) throw errStr + rhs + " is undefined.";
              if (!(this.members[rhs] instanceof IdlInterface)) throw errStr + rhs + " is not an interface.";
              this.members[rhs].members.forEach(function(member)
              {
                  this.members[lhs].members.push(new IdlInterfaceMember(member));
              }.bind(this));
          }.bind(this));
      }
      this["implements"] = {};
  
      for (var lhs in this["includes"])
      {
          this.recursively_get_includes(lhs).forEach(function(rhs)
          {
              var errStr = lhs + " includes " + rhs + ", but ";
              if (!(lhs in this.members)) throw errStr + lhs + " is undefined.";
              if (!(this.members[lhs] instanceof IdlInterface)) throw errStr + lhs + " is not an interface.";
              if (!(rhs in this.members)) throw errStr + rhs + " is undefined.";
              if (!(this.members[rhs] instanceof IdlInterface)) throw errStr + rhs + " is not an interface.";
              this.members[rhs].members.forEach(function(member)
              {
                  this.members[lhs].members.push(new IdlInterfaceMember(member));
              }.bind(this));
          }.bind(this));
      }
      this["includes"] = {};
  
      // Assert B defined for A : B
      for (const member of Object.values(this.members).filter(m => m.base)) {
          const lhs = member.name;
          const rhs = member.base;
          if (!(rhs in this.members)) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${rhs} is undefined.`);
          const lhs_is_interface = this.members[lhs] instanceof IdlInterface;
          const rhs_is_interface = this.members[rhs] instanceof IdlInterface;
          if (rhs_is_interface != lhs_is_interface) {
              if (!lhs_is_interface) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${lhs} is not an interface.`);
              if (!rhs_is_interface) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${rhs} is not an interface.`);
          }
          // Check for circular dependencies.
          member.get_inheritance_stack();
      }
  
      Object.getOwnPropertyNames(this.members).forEach(function(memberName) {
          var member = this.members[memberName];
          if (!(member instanceof IdlInterface)) {
              return;
          }
  
          var globals = exposure_set(member);
          member.exposed = exposed_in(globals);
          member.exposureSet = globals;
      }.bind(this));
  
      // Now run test() on every member, and test_object() for every object.
      for (var name in this.members)
      {
          this.members[name].test();
          if (name in this.objects)
          {
              const objects = this.objects[name];
              if (!objects || !Array.isArray(objects)) {
                  throw new IdlHarnessError(`Invalid or empty objects for member ${name}`);
              }
              objects.forEach(function(str)
              {
                  if (!this.members[name] || !(this.members[name] instanceof IdlInterface)) {
                      throw new IdlHarnessError(`Invalid object member name ${name}`);
                  }
                  this.members[name].test_object(str);
              }.bind(this));
          }
      }
  };
  
  IdlArray.prototype.collapse_partials = function()
  {
      const testedPartials = new Map();
      this.partials.forEach(function(parsed_idl)
      {
          const originalExists = parsed_idl.name in this.members
              && (this.members[parsed_idl.name] instanceof IdlInterface
                  || this.members[parsed_idl.name] instanceof IdlDictionary
                  || this.members[parsed_idl.name] instanceof IdlNamespace);
  
          let partialTestName = parsed_idl.name;
          if (!parsed_idl.untested) {
              // Ensure unique test name in case of multiple partials.
              let partialTestCount = 1;
              if (testedPartials.has(parsed_idl.name)) {
                  partialTestCount += testedPartials.get(parsed_idl.name);
                  partialTestName = `${partialTestName}[${partialTestCount}]`;
              }
              testedPartials.set(parsed_idl.name, partialTestCount);
  
              test(function () {
                  assert_true(originalExists, `Original ${parsed_idl.type} should be defined`);
  
                  var expected = IdlInterface;
                  switch (parsed_idl.type) {
                      case 'interface': expected = IdlInterface; break;
                      case 'dictionary': expected = IdlDictionary; break;
                      case 'namespace': expected = IdlNamespace; break;
                  }
                  assert_true(
                      expected.prototype.isPrototypeOf(this.members[parsed_idl.name]),
                      `Original ${parsed_idl.name} definition should have type ${parsed_idl.type}`);
              }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: original ${parsed_idl.type} defined`);
          }
          if (!originalExists) {
              // Not good.. but keep calm and carry on.
              return;
          }
  
          if (parsed_idl.extAttrs)
          {
              // Special-case "Exposed". Must be a subset of original interface's exposure.
              // Exposed on a partial is the equivalent of having the same Exposed on all nested members.
              // See https://github.com/heycam/webidl/issues/154 for discrepency between Exposed and
              // other extended attributes on partial interfaces.
              const exposureAttr = parsed_idl.extAttrs.find(a => a.name === "Exposed");
              if (exposureAttr) {
                  if (!parsed_idl.untested) {
                      test(function () {
                          const partialExposure = exposure_set(parsed_idl);
                          const memberExposure = exposure_set(this.members[parsed_idl.name]);
                          partialExposure.forEach(name => {
                              if (!memberExposure || !memberExposure.has(name)) {
                                  throw new IdlHarnessError(
                                      `Partial ${parsed_idl.name} ${parsed_idl.type} is exposed to '${name}', the original ${parsed_idl.type} is not.`);
                              }
                          });
                      }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: valid exposure set`);
                  }
                  parsed_idl.members.forEach(function (member) {
                      member.extAttrs.push(exposureAttr);
                  }.bind(this));
              }
  
              parsed_idl.extAttrs.forEach(function(extAttr)
              {
                  // "Exposed" already handled above.
                  if (extAttr.name === "Exposed") {
                      return;
                  }
                  this.members[parsed_idl.name].extAttrs.push(extAttr);
              }.bind(this));
          }
          parsed_idl.members.forEach(function(member)
          {
              this.members[parsed_idl.name].members.push(new IdlInterfaceMember(member));
          }.bind(this));
      }.bind(this));
      this.partials = [];
  }
  
  IdlArray.prototype.assert_type_is = function(value, type)
  {
      if (type.idlType in this.members
      && this.members[type.idlType] instanceof IdlTypedef) {
          this.assert_type_is(value, this.members[type.idlType].idlType);
          return;
      }
      if (type.union) {
          for (var i = 0; i < type.idlType.length; i++) {
              try {
                  this.assert_type_is(value, type.idlType[i]);
                  // No AssertionError, so we match one type in the union
                  return;
              } catch(e) {
                  if (e instanceof AssertionError) {
                      // We didn't match this type, let's try some others
                      continue;
                  }
                  throw e;
              }
          }
          // TODO: Is there a nice way to list the union's types in the message?
          assert_true(false, "Attribute has value " + format_value(value)
                      + " which doesn't match any of the types in the union");
  
      }
  
      /**
       * Helper function that tests that value is an instance of type according
       * to the rules of WebIDL.  value is any JavaScript value, and type is an
       * object produced by WebIDLParser.js' "type" production.  That production
       * is fairly elaborate due to the complexity of WebIDL's types, so it's
       * best to look at the grammar to figure out what properties it might have.
       */
      if (type.idlType == "any")
      {
          // No assertions to make
          return;
      }
  
      if (type.nullable && value === null)
      {
          // This is fine
          return;
      }
  
      if (type.array)
      {
          // TODO: not supported yet
          return;
      }
  
      if (type.generic === "sequence")
      {
          assert_true(Array.isArray(value), "should be an Array");
          if (!value.length)
          {
              // Nothing we can do.
              return;
          }
          this.assert_type_is(value[0], type.idlType[0]);
          return;
      }
  
      if (type.generic === "Promise") {
          assert_true("then" in value, "Attribute with a Promise type should have a then property");
          // TODO: Ideally, we would check on project fulfillment
          // that we get the right type
          // but that would require making the type check async
          return;
      }
  
      if (type.generic === "FrozenArray") {
          assert_true(Array.isArray(value), "Value should be array");
          assert_true(Object.isFrozen(value), "Value should be frozen");
          if (!value.length)
          {
              // Nothing we can do.
              return;
          }
          this.assert_type_is(value[0], type.idlType[0]);
          return;
      }
  
      type = Array.isArray(type.idlType) ? type.idlType[0] : type.idlType;
  
      switch(type)
      {
          case "void":
              assert_equals(value, undefined);
              return;
  
          case "boolean":
              assert_equals(typeof value, "boolean");
              return;
  
          case "byte":
              assert_equals(typeof value, "number");
              assert_equals(value, Math.floor(value), "should be an integer");
              assert_true(-128 <= value && value <= 127, "byte " + value + " should be in range [-128, 127]");
              return;
  
          case "octet":
              assert_equals(typeof value, "number");
              assert_equals(value, Math.floor(value), "should be an integer");
              assert_true(0 <= value && value <= 255, "octet " + value + " should be in range [0, 255]");
              return;
  
          case "short":
              assert_equals(typeof value, "number");
              assert_equals(value, Math.floor(value), "should be an integer");
              assert_true(-32768 <= value && value <= 32767, "short " + value + " should be in range [-32768, 32767]");
              return;
  
          case "unsigned short":
              assert_equals(typeof value, "number");
              assert_equals(value, Math.floor(value), "should be an integer");
              assert_true(0 <= value && value <= 65535, "unsigned short " + value + " should be in range [0, 65535]");
              return;
  
          case "long":
              assert_equals(typeof value, "number");
              assert_equals(value, Math.floor(value), "should be an integer");
              assert_true(-2147483648 <= value && value <= 2147483647, "long " + value + " should be in range [-2147483648, 2147483647]");
              return;
  
          case "unsigned long":
              assert_equals(typeof value, "number");
              assert_equals(value, Math.floor(value), "should be an integer");
              assert_true(0 <= value && value <= 4294967295, "unsigned long " + value + " should be in range [0, 4294967295]");
              return;
  
          case "long long":
              assert_equals(typeof value, "number");
              return;
  
          case "unsigned long long":
          case "DOMTimeStamp":
              assert_equals(typeof value, "number");
              assert_true(0 <= value, "unsigned long long should be positive");
              return;
  
          case "float":
              assert_equals(typeof value, "number");
              assert_equals(value, fround(value), "float rounded to 32-bit float should be itself");
              assert_not_equals(value, Infinity);
              assert_not_equals(value, -Infinity);
              assert_not_equals(value, NaN);
              return;
  
          case "DOMHighResTimeStamp":
          case "double":
              assert_equals(typeof value, "number");
              assert_not_equals(value, Infinity);
              assert_not_equals(value, -Infinity);
              assert_not_equals(value, NaN);
              return;
  
          case "unrestricted float":
              assert_equals(typeof value, "number");
              assert_equals(value, fround(value), "unrestricted float rounded to 32-bit float should be itself");
              return;
  
          case "unrestricted double":
              assert_equals(typeof value, "number");
              return;
  
          case "DOMString":
              assert_equals(typeof value, "string");
              return;
  
          case "ByteString":
              assert_equals(typeof value, "string");
              assert_regexp_match(value, /^[\x00-\x7F]*$/);
              return;
  
          case "USVString":
              assert_equals(typeof value, "string");
              assert_regexp_match(value, /^([\x00-\ud7ff\ue000-\uffff]|[\ud800-\udbff][\udc00-\udfff])*$/);
              return;
  
          case "object":
              assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function");
              return;
      }
  
      if (!(type in this.members))
      {
          throw new IdlHarnessError("Unrecognized type " + type);
      }
  
      if (this.members[type] instanceof IdlInterface)
      {
          // We don't want to run the full
          // IdlInterface.prototype.test_instance_of, because that could result
          // in an infinite loop.  TODO: This means we don't have tests for
          // NoInterfaceObject interfaces, and we also can't test objects that
          // come from another self.
          assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function");
          if (value instanceof Object
          && !this.members[type].has_extended_attribute("NoInterfaceObject")
          && type in self)
          {
              assert_true(value instanceof self[type], "instanceof " + type);
          }
      }
      else if (this.members[type] instanceof IdlEnum)
      {
          assert_equals(typeof value, "string");
      }
      else if (this.members[type] instanceof IdlDictionary)
      {
          // TODO: Test when we actually have something to test this on
      }
      else if (this.members[type] instanceof IdlCallback)
      {
          assert_equals(typeof value, "function");
      }
      else
      {
          throw new IdlHarnessError("Type " + type + " isn't an interface, callback or dictionary");
      }
  };
  
  /// IdlObject ///
  function IdlObject() {}
  IdlObject.prototype.test = function()
  {
      /**
       * By default, this does nothing, so no actual tests are run for IdlObjects
       * that don't define any (e.g., IdlDictionary at the time of this writing).
       */
  };
  
  IdlObject.prototype.has_extended_attribute = function(name)
  {
      /**
       * This is only meaningful for things that support extended attributes,
       * such as interfaces, exceptions, and members.
       */
      return this.extAttrs.some(function(o)
      {
          return o.name == name;
      });
  };
  
  
  /// IdlDictionary ///
  // Used for IdlArray.prototype.assert_type_is
  function IdlDictionary(obj)
  {
      /**
       * obj is an object produced by the WebIDLParser.js "dictionary"
       * production.
       */
  
      /** Self-explanatory. */
      this.name = obj.name;
  
      /** A back-reference to our IdlArray. */
      this.array = obj.array;
  
      /** An array of objects produced by the "dictionaryMember" production. */
      this.members = obj.members;
  
      /**
       * The name (as a string) of the dictionary type we inherit from, or null
       * if there is none.
       */
      this.base = obj.inheritance;
  }
  
  IdlDictionary.prototype = Object.create(IdlObject.prototype);
  
  IdlDictionary.prototype.get_inheritance_stack = function() {
      return IdlInterface.prototype.get_inheritance_stack.call(this);
  };
  
  /// IdlInterface ///
  function IdlInterface(obj, is_callback, is_mixin)
  {
      /**
       * obj is an object produced by the WebIDLParser.js "interface" production.
       */
  
      /** Self-explanatory. */
      this.name = obj.name;
  
      /** A back-reference to our IdlArray. */
      this.array = obj.array;
  
      /**
       * An indicator of whether we should run tests on the interface object and
       * interface prototype object. Tests on members are controlled by .untested
       * on each member, not this.
       */
      this.untested = obj.untested;
  
      /** An array of objects produced by the "ExtAttr" production. */
      this.extAttrs = obj.extAttrs;
  
      /** An array of IdlInterfaceMembers. */
      this.members = obj.members.map(function(m){return new IdlInterfaceMember(m); });
      if (this.has_extended_attribute("Unforgeable")) {
          this.members
              .filter(function(m) { return m.special !== "static" && (m.type == "attribute" || m.type == "operation"); })
              .forEach(function(m) { return m.isUnforgeable = true; });
      }
  
      /**
       * The name (as a string) of the type we inherit from, or null if there is
       * none.
       */
      this.base = obj.inheritance;
  
      this._is_callback = is_callback;
      this._is_mixin = is_mixin;
  }
  IdlInterface.prototype = Object.create(IdlObject.prototype);
  IdlInterface.prototype.is_callback = function()
  {
      return this._is_callback;
  };
  
  IdlInterface.prototype.is_mixin = function()
  {
      return this._is_mixin;
  };
  
  IdlInterface.prototype.has_constants = function()
  {
      return this.members.some(function(member) {
          return member.type === "const";
      });
  };
  
  IdlInterface.prototype.get_unscopables = function()
  {
      return this.members.filter(function(member) {
          return member.isUnscopable;
      });
  };
  
  IdlInterface.prototype.is_global = function()
  {
      return this.extAttrs.some(function(attribute) {
          return attribute.name === "Global";
      });
  };
  
  /**
   * Value of the LegacyNamespace extended attribute, if any.
   *
   * https://heycam.github.io/webidl/#LegacyNamespace
   */
  IdlInterface.prototype.get_legacy_namespace = function()
  {
      var legacyNamespace = this.extAttrs.find(function(attribute) {
          return attribute.name === "LegacyNamespace";
      });
      return legacyNamespace ? legacyNamespace.rhs.value : undefined;
  };
  
  IdlInterface.prototype.get_interface_object_owner = function()
  {
      var legacyNamespace = this.get_legacy_namespace();
      return legacyNamespace ? self[legacyNamespace] : self;
  };
  
  IdlInterface.prototype.assert_interface_object_exists = function()
  {
      var owner = this.get_legacy_namespace() || "self";
      assert_own_property(self[owner], this.name, owner + " does not have own property " + format_value(this.name));
  };
  
  IdlInterface.prototype.get_interface_object = function() {
      if (this.has_extended_attribute("NoInterfaceObject")) {
          throw new IdlHarnessError(this.name + " has no interface object due to NoInterfaceObject");
      }
  
      return this.get_interface_object_owner()[this.name];
  };
  
  IdlInterface.prototype.get_qualified_name = function() {
      // https://heycam.github.io/webidl/#qualified-name
      var legacyNamespace = this.get_legacy_namespace();
      if (legacyNamespace) {
          return legacyNamespace + "." + this.name;
      }
      return this.name;
  };
  
  IdlInterface.prototype.has_to_json_regular_operation = function() {
      return this.members.some(function(m) {
          return m.is_to_json_regular_operation();
      });
  };
  
  IdlInterface.prototype.has_default_to_json_regular_operation = function() {
      return this.members.some(function(m) {
          return m.is_to_json_regular_operation() && m.has_extended_attribute("Default");
      });
  };
  
  IdlInterface.prototype.get_inheritance_stack = function() {
      /**
       * See https://heycam.github.io/webidl/#create-an-inheritance-stack
       *
       * Returns an array of IdlInterface objects which contains itself
       * and all of its inherited interfaces.
       *
       * So given:
       *
       *   A : B {};
       *   B : C {};
       *   C {};
       *
       * then A.get_inheritance_stack() should return [A, B, C],
       * and B.get_inheritance_stack() should return [B, C].
       *
       * Note: as dictionary inheritance is expressed identically by the AST,
       * this works just as well for getting a stack of inherited dictionaries.
       */
  
      var stack = [this];
      var idl_interface = this;
      while (idl_interface.base) {
          var base = this.array.members[idl_interface.base];
          if (!base) {
              throw new Error(idl_interface.type + " " + idl_interface.base + " not found (inherited by " + idl_interface.name + ")");
          } else if (stack.indexOf(base) > -1) {
              stack.push(base);
              let dep_chain = stack.map(i => i.name).join(',');
              throw new IdlHarnessError(`${this.name} has a circular dependency: ${dep_chain}`);
          }
          idl_interface = base;
          stack.push(idl_interface);
      }
      return stack;
  };
  
  /**
   * Implementation of
   * https://heycam.github.io/webidl/#default-tojson-operation
   * for testing purposes.
   *
   * Collects the IDL types of the attributes that meet the criteria
   * for inclusion in the default toJSON operation for easy
   * comparison with actual value
   */
  IdlInterface.prototype.default_to_json_operation = function(callback) {
      var map = new Map(), isDefault = false;
      this.traverse_inherited_and_consequential_interfaces(function(I) {
          if (I.has_default_to_json_regular_operation()) {
              isDefault = true;
              I.members.forEach(function(m) {
                  if (m.special !== "static" && m.type == "attribute" && I.array.is_json_type(m.idlType)) {
                      map.set(m.name, m.idlType);
                  }
              });
          } else if (I.has_to_json_regular_operation()) {
              isDefault = false;
          }
      });
      return isDefault ? map : null;
  };
  
  /**
   * Traverses inherited interfaces from the top down
   * and imeplemented interfaces inside out.
   * Invokes |callback| on each interface.
   *
   * This is an abstract implementation of the traversal
   * algorithm specified in:
   * https://heycam.github.io/webidl/#collect-attribute-values
   * Given the following inheritance tree:
   *
   *           F
   *           |
   *       C   E - I
   *       |   |
   *       B - D
   *       |
   *   G - A - H - J
   *
   * Invoking traverse_inherited_and_consequential_interfaces() on A
   * would traverse the tree in the following order:
   * C -> B -> F -> E -> I -> D -> A -> G -> H -> J
   */
  
  IdlInterface.prototype.traverse_inherited_and_consequential_interfaces = function(callback) {
      if (typeof callback != "function") {
          throw new TypeError();
      }
      var stack = this.get_inheritance_stack();
      _traverse_inherited_and_consequential_interfaces(stack, callback);
  };
  
  function _traverse_inherited_and_consequential_interfaces(stack, callback) {
      var I = stack.pop();
      callback(I);
      var mixins = I.array["implements"][I.name] || I.array["includes"][I.name];
      if (mixins) {
          mixins.forEach(function(id) {
              var mixin = I.array.members[id];
              if (!mixin) {
                  throw new Error("Interface " + id + " not found (implemented by " + I.name + ")");
              }
              var interfaces = mixin.get_inheritance_stack();
              _traverse_inherited_and_consequential_interfaces(interfaces, callback);
          });
      }
      if (stack.length > 0) {
          _traverse_inherited_and_consequential_interfaces(stack, callback);
      }
  }
  
  IdlInterface.prototype.test = function()
  {
      if (this.has_extended_attribute("NoInterfaceObject") || this.is_mixin())
      {
          // No tests to do without an instance.  TODO: We should still be able
          // to run tests on the prototype object, if we obtain one through some
          // other means.
          return;
      }
  
      if (!this.exposed) {
          subsetTestByKey(this.name, test, function() {
              assert_false(this.name in self);
          }.bind(this), this.name + " interface: existence and properties of interface object");
          return;
      }
  
      if (!this.untested)
      {
          // First test things to do with the exception/interface object and
          // exception/interface prototype object.
          this.test_self();
      }
      // Then test things to do with its members (constants, fields, attributes,
      // operations, . . .).  These are run even if .untested is true, because
      // members might themselves be marked as .untested.  This might happen to
      // interfaces if the interface itself is untested but a partial interface
      // that extends it is tested -- then the interface itself and its initial
      // members will be marked as untested, but the members added by the partial
      // interface are still tested.
      this.test_members();
  };
  
  IdlInterface.prototype.test_self = function()
  {
      subsetTestByKey(this.name, test, function()
      {
          // This function tests WebIDL as of 2015-01-13.
  
          // "For every interface that is exposed in a given ECMAScript global
          // environment and:
          // * is a callback interface that has constants declared on it, or
          // * is a non-callback interface that is not declared with the
          //   [NoInterfaceObject] extended attribute,
          // a corresponding property MUST exist on the ECMAScript global object.
          // The name of the property is the identifier of the interface, and its
          // value is an object called the interface object.
          // The property has the attributes { [[Writable]]: true,
          // [[Enumerable]]: false, [[Configurable]]: true }."
          if (this.is_callback() && !this.has_constants()) {
              return;
          }
  
          // TODO: Should we test here that the property is actually writable
          // etc., or trust getOwnPropertyDescriptor?
          this.assert_interface_object_exists();
          var desc = Object.getOwnPropertyDescriptor(this.get_interface_object_owner(), this.name);
          assert_false("get" in desc, "self's property " + format_value(this.name) + " should not have a getter");
          assert_false("set" in desc, "self's property " + format_value(this.name) + " should not have a setter");
          assert_true(desc.writable, "self's property " + format_value(this.name) + " should be writable");
          assert_false(desc.enumerable, "self's property " + format_value(this.name) + " should not be enumerable");
          assert_true(desc.configurable, "self's property " + format_value(this.name) + " should be configurable");
  
          if (this.is_callback()) {
              // "The internal [[Prototype]] property of an interface object for
              // a callback interface must be the Function.prototype object."
              assert_equals(Object.getPrototypeOf(this.get_interface_object()), Function.prototype,
                            "prototype of self's property " + format_value(this.name) + " is not Object.prototype");
  
              return;
          }
  
          // "The interface object for a given non-callback interface is a
          // function object."
          // "If an object is defined to be a function object, then it has
          // characteristics as follows:"
  
          // Its [[Prototype]] internal property is otherwise specified (see
          // below).
  
          // "* Its [[Get]] internal property is set as described in ECMA-262
          //    section 9.1.8."
          // Not much to test for this.
  
          // "* Its [[Construct]] internal property is set as described in
          //    ECMA-262 section 19.2.2.3."
          // Tested below if no constructor is defined.  TODO: test constructors
          // if defined.
  
          // "* Its @@hasInstance property is set as described in ECMA-262
          //    section 19.2.3.8, unless otherwise specified."
          // TODO
  
          // ES6 (rev 30) 19.1.3.6:
          // "Else, if O has a [[Call]] internal method, then let builtinTag be
          // "Function"."
          assert_class_string(this.get_interface_object(), "Function", "class string of " + this.name);
  
          // "The [[Prototype]] internal property of an interface object for a
          // non-callback interface is determined as follows:"
          var prototype = Object.getPrototypeOf(this.get_interface_object());
          if (this.base) {
              // "* If the interface inherits from some other interface, the
              //    value of [[Prototype]] is the interface object for that other
              //    interface."
              var inherited_interface = this.array.members[this.base];
              if (!inherited_interface.has_extended_attribute("NoInterfaceObject")) {
                  inherited_interface.assert_interface_object_exists();
                  assert_equals(prototype, inherited_interface.get_interface_object(),
                                'prototype of ' + this.name + ' is not ' +
                                this.base);
              }
          } else {
              // "If the interface doesn't inherit from any other interface, the
              // value of [[Prototype]] is %FunctionPrototype% ([ECMA-262],
              // section 6.1.7.4)."
              assert_equals(prototype, Function.prototype,
                            "prototype of self's property " + format_value(this.name) + " is not Function.prototype");
          }
  
          if (!this.has_extended_attribute("Constructor")) {
              // "The internal [[Call]] method of the interface object behaves as
              // follows . . .
              //
              // "If I was not declared with a [Constructor] extended attribute,
              // then throw a TypeError."
              var interface_object = this.get_interface_object();
              assert_throws(new TypeError(), function() {
                  interface_object();
              }, "interface object didn't throw TypeError when called as a function");
              assert_throws(new TypeError(), function() {
                  new interface_object();
              }, "interface object didn't throw TypeError when called as a constructor");
          }
      }.bind(this), this.name + " interface: existence and properties of interface object");
  
      if (!this.is_callback()) {
          subsetTestByKey(this.name, test, function() {
              // This function tests WebIDL as of 2014-10-25.
              // https://heycam.github.io/webidl/#es-interface-call
  
              this.assert_interface_object_exists();
  
              // "Interface objects for non-callback interfaces MUST have a
              // property named “length” with attributes { [[Writable]]: false,
              // [[Enumerable]]: false, [[Configurable]]: true } whose value is
              // a Number."
              assert_own_property(this.get_interface_object(), "length");
              var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "length");
              assert_false("get" in desc, this.name + ".length should not have a getter");
              assert_false("set" in desc, this.name + ".length should not have a setter");
              assert_false(desc.writable, this.name + ".length should not be writable");
              assert_false(desc.enumerable, this.name + ".length should not be enumerable");
              assert_true(desc.configurable, this.name + ".length should be configurable");
  
              var constructors = this.extAttrs
                  .filter(function(attr) { return attr.name == "Constructor"; });
              var expected_length = minOverloadLength(constructors);
              assert_equals(this.get_interface_object().length, expected_length, "wrong value for " + this.name + ".length");
          }.bind(this), this.name + " interface object length");
      }
  
      if (!this.is_callback() || this.has_constants()) {
          subsetTestByKey(this.name, test, function() {
              // This function tests WebIDL as of 2015-11-17.
              // https://heycam.github.io/webidl/#interface-object
  
              this.assert_interface_object_exists();
  
              // "All interface objects must have a property named “name” with
              // attributes { [[Writable]]: false, [[Enumerable]]: false,
              // [[Configurable]]: true } whose value is the identifier of the
              // corresponding interface."
  
              assert_own_property(this.get_interface_object(), "name");
              var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "name");
              assert_false("get" in desc, this.name + ".name should not have a getter");
              assert_false("set" in desc, this.name + ".name should not have a setter");
              assert_false(desc.writable, this.name + ".name should not be writable");
              assert_false(desc.enumerable, this.name + ".name should not be enumerable");
              assert_true(desc.configurable, this.name + ".name should be configurable");
              assert_equals(this.get_interface_object().name, this.name, "wrong value for " + this.name + ".name");
          }.bind(this), this.name + " interface object name");
      }
  
  
      if (this.has_extended_attribute("LegacyWindowAlias")) {
          subsetTestByKey(this.name, test, function()
          {
              var aliasAttrs = this.extAttrs.filter(function(o) { return o.name === "LegacyWindowAlias"; });
              if (aliasAttrs.length > 1) {
                  throw new IdlHarnessError("Invalid IDL: multiple LegacyWindowAlias extended attributes on " + this.name);
              }
              if (this.is_callback()) {
                  throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on non-interface " + this.name);
              }
              if (!this.exposureSet.has("Window")) {
                  throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " which is not exposed in Window");
              }
              // TODO: when testing of [NoInterfaceObject] interfaces is supported,
              // check that it's not specified together with LegacyWindowAlias.
  
              // TODO: maybe check that [LegacyWindowAlias] is not specified on a partial interface.
  
              var rhs = aliasAttrs[0].rhs;
              if (!rhs) {
                  throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " without identifier");
              }
              var aliases;
              if (rhs.type === "identifier-list") {
                  aliases = rhs.value.map(id => id.value);
              } else { // rhs.type === identifier
                  aliases = [ rhs.value ];
              }
  
              // OK now actually check the aliases...
              var alias;
              if (exposed_in(exposure_set(this, this.exposureSet)) && 'document' in self) {
                  for (alias of aliases) {
                      assert_true(alias in self, alias + " should exist");
                      assert_equals(self[alias], this.get_interface_object(), "self." + alias + " should be the same value as self." + this.get_qualified_name());
                      var desc = Object.getOwnPropertyDescriptor(self, alias);
                      assert_equals(desc.value, this.get_interface_object(), "wrong value in " + alias + " property descriptor");
                      assert_true(desc.writable, alias + " should be writable");
                      assert_false(desc.enumerable, alias + " should not be enumerable");
                      assert_true(desc.configurable, alias + " should be configurable");
                      assert_false('get' in desc, alias + " should not have a getter");
                      assert_false('set' in desc, alias + " should not have a setter");
                  }
              } else {
                  for (alias of aliases) {
                      assert_false(alias in self, alias + " should not exist");
                  }
              }
  
          }.bind(this), this.name + " interface: legacy window alias");
      }
  
      if (this.has_extended_attribute("NamedConstructor")) {
          var constructors = this.extAttrs
              .filter(function(attr) { return attr.name == "NamedConstructor"; });
          if (constructors.length !== 1) {
              throw new IdlHarnessError("Internal error: missing support for multiple NamedConstructor extended attributes");
          }
          var constructor = constructors[0];
          var min_length = minOverloadLength([constructor]);
  
          subsetTestByKey(this.name, test, function()
          {
              // This function tests WebIDL as of 2019-01-14.
  
              // "for every [NamedConstructor] extended attribute on an exposed
              // interface, a corresponding property must exist on the ECMAScript
              // global object. The name of the property is the
              // [NamedConstructor]'s identifier, and its value is an object
              // called a named constructor, ... . The property has the attributes
              // { [[Writable]]: true, [[Enumerable]]: false,
              // [[Configurable]]: true }."
              var name = constructor.rhs.value;
              assert_own_property(self, name);
              var desc = Object.getOwnPropertyDescriptor(self, name);
              assert_equals(desc.value, self[name], "wrong value in " + name + " property descriptor");
              assert_true(desc.writable, name + " should be writable");
              assert_false(desc.enumerable, name + " should not be enumerable");
              assert_true(desc.configurable, name + " should be configurable");
              assert_false("get" in desc, name + " should not have a getter");
              assert_false("set" in desc, name + " should not have a setter");
          }.bind(this), this.name + " interface: named constructor");
  
          subsetTestByKey(this.name, test, function()
          {
              // This function tests WebIDL as of 2019-01-14.
  
              // "2. Let F be ! CreateBuiltinFunction(realm, steps,
              //     realm.[[Intrinsics]].[[%FunctionPrototype%]])."
              var name = constructor.rhs.value;
              var value = self[name];
              assert_equals(typeof value, "function", "type of value in " + name + " property descriptor");
              assert_not_equals(value, this.get_interface_object(), "wrong value in " + name + " property descriptor");
              assert_equals(Object.getPrototypeOf(value), Function.prototype, "wrong value for " + name + "'s prototype");
          }.bind(this), this.name + " interface: named constructor object");
  
          subsetTestByKey(this.name, test, function()
          {
              // This function tests WebIDL as of 2019-01-14.
  
              // "7. Let proto be the interface prototype object of interface I
              //     in realm.
              // "8. Perform ! DefinePropertyOrThrow(F, "prototype",
              //     PropertyDescriptor{
              //         [[Value]]: proto, [[Writable]]: false,
              //         [[Enumerable]]: false, [[Configurable]]: false
              //     })."
              var name = constructor.rhs.value;
              var expected = this.get_interface_object().prototype;
              var desc = Object.getOwnPropertyDescriptor(self[name], "prototype");
              assert_equals(desc.value, expected, "wrong value for " + name + ".prototype");
              assert_false(desc.writable, "prototype should not be writable");
              assert_false(desc.enumerable, "prototype should not be enumerable");
              assert_false(desc.configurable, "prototype should not be configurable");
              assert_false("get" in desc, "prototype should not have a getter");
              assert_false("set" in desc, "prototype should not have a setter");
          }.bind(this), this.name + " interface: named constructor prototype property");
  
          subsetTestByKey(this.name, test, function()
          {
              // This function tests WebIDL as of 2019-01-14.
  
              // "3. Perform ! SetFunctionName(F, id)."
              var name = constructor.rhs.value;
              var desc = Object.getOwnPropertyDescriptor(self[name], "name");
              assert_equals(desc.value, name, "wrong value for " + name + ".name");
              assert_false(desc.writable, "name should not be writable");
              assert_false(desc.enumerable, "name should not be enumerable");
              assert_true(desc.configurable, "name should be configurable");
              assert_false("get" in desc, "name should not have a getter");
              assert_false("set" in desc, "name should not have a setter");
          }.bind(this), this.name + " interface: named constructor name");
  
          subsetTestByKey(this.name, test, function()
          {
              // This function tests WebIDL as of 2019-01-14.
  
              // "4. Initialize S to the effective overload set for constructors
              //     with identifier id on interface I and with argument count 0.
              // "5. Let length be the length of the shortest argument list of
              //     the entries in S.
              // "6. Perform ! SetFunctionLength(F, length)."
              var name = constructor.rhs.value;
              var desc = Object.getOwnPropertyDescriptor(self[name], "length");
              assert_equals(desc.value, min_length, "wrong value for " + name + ".length");
              assert_false(desc.writable, "length should not be writable");
              assert_false(desc.enumerable, "length should not be enumerable");
              assert_true(desc.configurable, "length should be configurable");
              assert_false("get" in desc, "length should not have a getter");
              assert_false("set" in desc, "length should not have a setter");
          }.bind(this), this.name + " interface: named constructor length");
  
          subsetTestByKey(this.name, test, function()
          {
              // This function tests WebIDL as of 2019-01-14.
  
              // "1. Let steps be the following steps:
              // "    1. If NewTarget is undefined, then throw a TypeError."
              var name = constructor.rhs.value;
              var args = constructor.arguments.map(function(arg) {
                  return create_suitable_object(arg.idlType);
              });
              assert_throws(new TypeError(), function() {
                  self[name](...args);
              }.bind(this));
          }.bind(this), this.name + " interface: named constructor without 'new'");
      }
  
      subsetTestByKey(this.name, test, function()
      {
          // This function tests WebIDL as of 2015-01-21.
          // https://heycam.github.io/webidl/#interface-object
  
          if (this.is_callback() && !this.has_constants()) {
              return;
          }
  
          this.assert_interface_object_exists();
  
          if (this.is_callback()) {
              assert_false("prototype" in this.get_interface_object(),
                           this.name + ' should not have a "prototype" property');
              return;
          }
  
          // "An interface object for a non-callback interface must have a
          // property named “prototype” with attributes { [[Writable]]: false,
          // [[Enumerable]]: false, [[Configurable]]: false } whose value is an
          // object called the interface prototype object. This object has
          // properties that correspond to the regular attributes and regular
          // operations defined on the interface, and is described in more detail
          // in section 4.5.4 below."
          assert_own_property(this.get_interface_object(), "prototype",
                              'interface "' + this.name + '" does not have own property "prototype"');
          var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "prototype");
          assert_false("get" in desc, this.name + ".prototype should not have a getter");
          assert_false("set" in desc, this.name + ".prototype should not have a setter");
          assert_false(desc.writable, this.name + ".prototype should not be writable");
          assert_false(desc.enumerable, this.name + ".prototype should not be enumerable");
          assert_false(desc.configurable, this.name + ".prototype should not be configurable");
  
          // Next, test that the [[Prototype]] of the interface prototype object
          // is correct. (This is made somewhat difficult by the existence of
          // [NoInterfaceObject].)
          // TODO: Aryeh thinks there's at least other place in this file where
          //       we try to figure out if an interface prototype object is
          //       correct. Consolidate that code.
  
          // "The interface prototype object for a given interface A must have an
          // internal [[Prototype]] property whose value is returned from the
          // following steps:
          // "If A is declared with the [Global] extended
          // attribute, and A supports named properties, then return the named
          // properties object for A, as defined in §3.6.4 Named properties
          // object.
          // "Otherwise, if A is declared to inherit from another interface, then
          // return the interface prototype object for the inherited interface.
          // "Otherwise, return %ObjectPrototype%.
          //
          // "In the ECMAScript binding, the DOMException type has some additional
          // requirements:
          //
          //     "Unlike normal interface types, the interface prototype object
          //     for DOMException must have as its [[Prototype]] the intrinsic
          //     object %ErrorPrototype%."
          //
          if (this.name === "Window") {
              assert_class_string(Object.getPrototypeOf(this.get_interface_object().prototype),
                                  'WindowProperties',
                                  'Class name for prototype of Window' +
                                  '.prototype is not "WindowProperties"');
          } else {
              var inherit_interface, inherit_interface_interface_object;
              if (this.base) {
                  inherit_interface = this.base;
                  var parent = this.array.members[inherit_interface];
                  if (!parent.has_extended_attribute("NoInterfaceObject")) {
                      parent.assert_interface_object_exists();
                      inherit_interface_interface_object = parent.get_interface_object();
                  }
              } else if (this.name === "DOMException") {
                  inherit_interface = 'Error';
                  inherit_interface_interface_object = self.Error;
              } else {
                  inherit_interface = 'Object';
                  inherit_interface_interface_object = self.Object;
              }
              if (inherit_interface_interface_object) {
                  assert_not_equals(inherit_interface_interface_object, undefined,
                                    'should inherit from ' + inherit_interface + ', but there is no such property');
                  assert_own_property(inherit_interface_interface_object, 'prototype',
                                      'should inherit from ' + inherit_interface + ', but that object has no "prototype" property');
                  assert_equals(Object.getPrototypeOf(this.get_interface_object().prototype),
                                inherit_interface_interface_object.prototype,
                                'prototype of ' + this.name + '.prototype is not ' + inherit_interface + '.prototype');
              } else {
                  // We can't test that we get the correct object, because this is the
                  // only way to get our hands on it. We only test that its class
                  // string, at least, is correct.
                  assert_class_string(Object.getPrototypeOf(this.get_interface_object().prototype),
                                      inherit_interface + 'Prototype',
                                      'Class name for prototype of ' + this.name +
                                      '.prototype is not "' + inherit_interface + 'Prototype"');
              }
          }
  
          // "The class string of an interface prototype object is the
          // concatenation of the interface’s qualified identifier and the string
          // “Prototype”."
  
          // Skip these tests for now due to a specification issue about
          // prototype name.
          // https://www.w3.org/Bugs/Public/show_bug.cgi?id=28244
  
          // assert_class_string(this.get_interface_object().prototype, this.get_qualified_name() + "Prototype",
          //                     "class string of " + this.name + ".prototype");
  
          // String() should end up calling {}.toString if nothing defines a
          // stringifier.
          if (!this.has_stringifier()) {
              // assert_equals(String(this.get_interface_object().prototype), "[object " + this.get_qualified_name() + "Prototype]",
              //         "String(" + this.name + ".prototype)");
          }
      }.bind(this), this.name + " interface: existence and properties of interface prototype object");
  
      // "If the interface is declared with the [Global]
      // extended attribute, or the interface is in the set of inherited
      // interfaces for any other interface that is declared with one of these
      // attributes, then the interface prototype object must be an immutable
      // prototype exotic object."
      // https://heycam.github.io/webidl/#interface-prototype-object
      if (this.is_global()) {
          this.test_immutable_prototype("interface prototype object", this.get_interface_object().prototype);
      }
  
      subsetTestByKey(this.name, test, function()
      {
          if (this.is_callback() && !this.has_constants()) {
              return;
          }
  
          this.assert_interface_object_exists();
  
          if (this.is_callback()) {
              assert_false("prototype" in this.get_interface_object(),
                           this.name + ' should not have a "prototype" property');
              return;
          }
  
          assert_own_property(this.get_interface_object(), "prototype",
                              'interface "' + this.name + '" does not have own property "prototype"');
  
          // "If the [NoInterfaceObject] extended attribute was not specified on
          // the interface, then the interface prototype object must also have a
          // property named “constructor” with attributes { [[Writable]]: true,
          // [[Enumerable]]: false, [[Configurable]]: true } whose value is a
          // reference to the interface object for the interface."
          assert_own_property(this.get_interface_object().prototype, "constructor",
                              this.name + '.prototype does not have own property "constructor"');
          var desc = Object.getOwnPropertyDescriptor(this.get_interface_object().prototype, "constructor");
          assert_false("get" in desc, this.name + ".prototype.constructor should not have a getter");
          assert_false("set" in desc, this.name + ".prototype.constructor should not have a setter");
          assert_true(desc.writable, this.name + ".prototype.constructor should be writable");
          assert_false(desc.enumerable, this.name + ".prototype.constructor should not be enumerable");
          assert_true(desc.configurable, this.name + ".prototype.constructor should be configurable");
          assert_equals(this.get_interface_object().prototype.constructor, this.get_interface_object(),
                        this.name + '.prototype.constructor is not the same object as ' + this.name);
      }.bind(this), this.name + ' interface: existence and properties of interface prototype object\'s "constructor" property');
  
  
      subsetTestByKey(this.name, test, function()
      {
          if (this.is_callback() && !this.has_constants()) {
              return;
          }
  
          this.assert_interface_object_exists();
  
          if (this.is_callback()) {
              assert_false("prototype" in this.get_interface_object(),
                           this.name + ' should not have a "prototype" property');
              return;
          }
  
          assert_own_property(this.get_interface_object(), "prototype",
                              'interface "' + this.name + '" does not have own property "prototype"');
  
          // If the interface has any member declared with the [Unscopable] extended
          // attribute, then there must be a property on the interface prototype object
          // whose name is the @@unscopables symbol, which has the attributes
          // { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true },
          // and whose value is an object created as follows...
          var unscopables = this.get_unscopables().map(m => m.name);
          var proto = this.get_interface_object().prototype;
          if (unscopables.length != 0) {
              assert_own_property(
                  proto, Symbol.unscopables,
                  this.name + '.prototype should have an @@unscopables property');
              var desc = Object.getOwnPropertyDescriptor(proto, Symbol.unscopables);
              assert_false("get" in desc,
                           this.name + ".prototype[Symbol.unscopables] should not have a getter");
              assert_false("set" in desc, this.name + ".prototype[Symbol.unscopables] should not have a setter");
              assert_false(desc.writable, this.name + ".prototype[Symbol.unscopables] should not be writable");
              assert_false(desc.enumerable, this.name + ".prototype[Symbol.unscopables] should not be enumerable");
              assert_true(desc.configurable, this.name + ".prototype[Symbol.unscopables] should be configurable");
              assert_equals(desc.value, proto[Symbol.unscopables],
                            this.name + '.prototype[Symbol.unscopables] should be in the descriptor');
              assert_equals(typeof desc.value, "object",
                            this.name + '.prototype[Symbol.unscopables] should be an object');
              assert_equals(Object.getPrototypeOf(desc.value), null,
                            this.name + '.prototype[Symbol.unscopables] should have a null prototype');
              assert_equals(Object.getOwnPropertySymbols(desc.value).length,
                            0,
                            this.name + '.prototype[Symbol.unscopables] should have the right number of symbol-named properties');
  
              // Check that we do not have _extra_ unscopables.  Checking that we
              // have all the ones we should will happen in the per-member tests.
              var observed = Object.getOwnPropertyNames(desc.value);
              for (var prop of observed) {
                  assert_not_equals(unscopables.indexOf(prop),
                                    -1,
                                    this.name + '.prototype[Symbol.unscopables] has unexpected property "' + prop + '"');
              }
          } else {
              assert_equals(Object.getOwnPropertyDescriptor(this.get_interface_object().prototype, Symbol.unscopables),
                            undefined,
                            this.name + '.prototype should not have @@unscopables');
          }
      }.bind(this), this.name + ' interface: existence and properties of interface prototype object\'s @@unscopables property');
  };
  
  IdlInterface.prototype.test_immutable_prototype = function(type, obj)
  {
      if (typeof Object.setPrototypeOf !== "function") {
          return;
      }
  
      subsetTestByKey(this.name, test, function(t) {
          var originalValue = Object.getPrototypeOf(obj);
          var newValue = Object.create(null);
  
          t.add_cleanup(function() {
              try {
                  Object.setPrototypeOf(obj, originalValue);
              } catch (err) {}
          });
  
          assert_throws(new TypeError(), function() {
              Object.setPrototypeOf(obj, newValue);
          });
  
          assert_equals(
                  Object.getPrototypeOf(obj),
                  originalValue,
                  "original value not modified"
              );
      }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
          "of " + type + " - setting to a new value via Object.setPrototypeOf " +
          "should throw a TypeError");
  
      subsetTestByKey(this.name, test, function(t) {
          var originalValue = Object.getPrototypeOf(obj);
          var newValue = Object.create(null);
  
          t.add_cleanup(function() {
              var setter = Object.getOwnPropertyDescriptor(
                  Object.prototype, '__proto__'
              ).set;
  
              try {
                  setter.call(obj, originalValue);
              } catch (err) {}
          });
  
          assert_throws(new TypeError(), function() {
              obj.__proto__ = newValue;
          });
  
          assert_equals(
                  Object.getPrototypeOf(obj),
                  originalValue,
                  "original value not modified"
              );
      }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
          "of " + type + " - setting to a new value via __proto__ " +
          "should throw a TypeError");
  
      subsetTestByKey(this.name, test, function(t) {
          var originalValue = Object.getPrototypeOf(obj);
          var newValue = Object.create(null);
  
          t.add_cleanup(function() {
              try {
                  Reflect.setPrototypeOf(obj, originalValue);
              } catch (err) {}
          });
  
          assert_false(Reflect.setPrototypeOf(obj, newValue));
  
          assert_equals(
                  Object.getPrototypeOf(obj),
                  originalValue,
                  "original value not modified"
              );
      }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
          "of " + type + " - setting to a new value via Reflect.setPrototypeOf " +
          "should return false");
  
      subsetTestByKey(this.name, test, function() {
          var originalValue = Object.getPrototypeOf(obj);
  
          Object.setPrototypeOf(obj, originalValue);
      }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
          "of " + type + " - setting to its original value via Object.setPrototypeOf " +
          "should not throw");
  
      subsetTestByKey(this.name, test, function() {
          var originalValue = Object.getPrototypeOf(obj);
  
          obj.__proto__ = originalValue;
      }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
          "of " + type + " - setting to its original value via __proto__ " +
          "should not throw");
  
      subsetTestByKey(this.name, test, function() {
          var originalValue = Object.getPrototypeOf(obj);
  
          assert_true(Reflect.setPrototypeOf(obj, originalValue));
      }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
          "of " + type + " - setting to its original value via Reflect.setPrototypeOf " +
          "should return true");
  };
  
  IdlInterface.prototype.test_member_const = function(member)
  {
      if (!this.has_constants()) {
          throw new IdlHarnessError("Internal error: test_member_const called without any constants");
      }
  
      subsetTestByKey(this.name, test, function()
      {
          this.assert_interface_object_exists();
  
          // "For each constant defined on an interface A, there must be
          // a corresponding property on the interface object, if it
          // exists."
          assert_own_property(this.get_interface_object(), member.name);
          // "The value of the property is that which is obtained by
          // converting the constant’s IDL value to an ECMAScript
          // value."
          assert_equals(this.get_interface_object()[member.name], constValue(member.value),
                        "property has wrong value");
          // "The property has attributes { [[Writable]]: false,
          // [[Enumerable]]: true, [[Configurable]]: false }."
          var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), member.name);
          assert_false("get" in desc, "property should not have a getter");
          assert_false("set" in desc, "property should not have a setter");
          assert_false(desc.writable, "property should not be writable");
          assert_true(desc.enumerable, "property should be enumerable");
          assert_false(desc.configurable, "property should not be configurable");
      }.bind(this), this.name + " interface: constant " + member.name + " on interface object");
  
      // "In addition, a property with the same characteristics must
      // exist on the interface prototype object."
      subsetTestByKey(this.name, test, function()
      {
          this.assert_interface_object_exists();
  
          if (this.is_callback()) {
              assert_false("prototype" in this.get_interface_object(),
                           this.name + ' should not have a "prototype" property');
              return;
          }
  
          assert_own_property(this.get_interface_object(), "prototype",
                              'interface "' + this.name + '" does not have own property "prototype"');
  
          assert_own_property(this.get_interface_object().prototype, member.name);
          assert_equals(this.get_interface_object().prototype[member.name], constValue(member.value),
                        "property has wrong value");
          var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), member.name);
          assert_false("get" in desc, "property should not have a getter");
          assert_false("set" in desc, "property should not have a setter");
          assert_false(desc.writable, "property should not be writable");
          assert_true(desc.enumerable, "property should be enumerable");
          assert_false(desc.configurable, "property should not be configurable");
      }.bind(this), this.name + " interface: constant " + member.name + " on interface prototype object");
  };
  
  
  IdlInterface.prototype.test_member_attribute = function(member)
    {
      if (!shouldRunSubTest(this.name)) {
          return;
      }
      var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: attribute " + member.name);
      a_test.step(function()
      {
          if (this.is_callback() && !this.has_constants()) {
              a_test.done()
              return;
          }
  
          this.assert_interface_object_exists();
          assert_own_property(this.get_interface_object(), "prototype",
                              'interface "' + this.name + '" does not have own property "prototype"');
  
          if (member.special === "static") {
              assert_own_property(this.get_interface_object(), member.name,
                  "The interface object must have a property " +
                  format_value(member.name));
              a_test.done();
              return;
          }
  
          this.do_member_unscopable_asserts(member);
  
          if (this.is_global()) {
              assert_own_property(self, member.name,
                  "The global object must have a property " +
                  format_value(member.name));
              assert_false(member.name in this.get_interface_object().prototype,
                  "The prototype object should not have a property " +
                  format_value(member.name));
  
              var getter = Object.getOwnPropertyDescriptor(self, member.name).get;
              assert_equals(typeof(getter), "function",
                            format_value(member.name) + " must have a getter");
  
              // Try/catch around the get here, since it can legitimately throw.
              // If it does, we obviously can't check for equality with direct
              // invocation of the getter.
              var gotValue;
              var propVal;
              try {
                  propVal = self[member.name];
                  gotValue = true;
              } catch (e) {
                  gotValue = false;
              }
              if (gotValue) {
                  assert_equals(propVal, getter.call(undefined),
                                "Gets on a global should not require an explicit this");
              }
  
              // do_interface_attribute_asserts must be the last thing we do,
              // since it will call done() on a_test.
              this.do_interface_attribute_asserts(self, member, a_test);
          } else {
              assert_true(member.name in this.get_interface_object().prototype,
                  "The prototype object must have a property " +
                  format_value(member.name));
  
              if (!member.has_extended_attribute("LenientThis")) {
                  if (member.idlType.generic !== "Promise") {
                      assert_throws(new TypeError(), function() {
                          this.get_interface_object().prototype[member.name];
                      }.bind(this), "getting property on prototype object must throw TypeError");
                      // do_interface_attribute_asserts must be the last thing we
                      // do, since it will call done() on a_test.
                      this.do_interface_attribute_asserts(this.get_interface_object().prototype, member, a_test);
                  } else {
                      promise_rejects(a_test, new TypeError(),
                                      this.get_interface_object().prototype[member.name])
                          .then(function() {
                              // do_interface_attribute_asserts must be the last
                              // thing we do, since it will call done() on a_test.
                              this.do_interface_attribute_asserts(this.get_interface_object().prototype,
                                                                  member, a_test);
                          }.bind(this));
                  }
              } else {
                  assert_equals(this.get_interface_object().prototype[member.name], undefined,
                                "getting property on prototype object must return undefined");
                // do_interface_attribute_asserts must be the last thing we do,
                // since it will call done() on a_test.
                this.do_interface_attribute_asserts(this.get_interface_object().prototype, member, a_test);
              }
          }
      }.bind(this));
  };
  
  IdlInterface.prototype.test_member_operation = function(member)
  {
      if (!shouldRunSubTest(this.name)) {
          return;
      }
      var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: operation " + member.name +
                              "(" + member.arguments.map(
                                  function(m) {return m.idlType.idlType; } ).join(", ")
                              +")");
      a_test.step(function()
      {
          // This function tests WebIDL as of 2015-12-29.
          // https://heycam.github.io/webidl/#es-operations
  
          if (this.is_callback() && !this.has_constants()) {
              a_test.done();
              return;
          }
  
          this.assert_interface_object_exists();
  
          if (this.is_callback()) {
              assert_false("prototype" in this.get_interface_object(),
                           this.name + ' should not have a "prototype" property');
              a_test.done();
              return;
          }
  
          assert_own_property(this.get_interface_object(), "prototype",
                              'interface "' + this.name + '" does not have own property "prototype"');
  
          // "For each unique identifier of an exposed operation defined on the
          // interface, there must exist a corresponding property, unless the
          // effective overload set for that identifier and operation and with an
          // argument count of 0 has no entries."
  
          // TODO: Consider [Exposed].
  
          // "The location of the property is determined as follows:"
          var memberHolderObject;
          // "* If the operation is static, then the property exists on the
          //    interface object."
          if (member.special === "static") {
              assert_own_property(this.get_interface_object(), member.name,
                      "interface object missing static operation");
              memberHolderObject = this.get_interface_object();
          // "* Otherwise, [...] if the interface was declared with the [Global]
          //    extended attribute, then the property exists
          //    on every object that implements the interface."
          } else if (this.is_global()) {
              assert_own_property(self, member.name,
                      "global object missing non-static operation");
              memberHolderObject = self;
          // "* Otherwise, the property exists solely on the interface’s
          //    interface prototype object."
          } else {
              assert_own_property(this.get_interface_object().prototype, member.name,
                      "interface prototype object missing non-static operation");
              memberHolderObject = this.get_interface_object().prototype;
          }
          this.do_member_unscopable_asserts(member);
          this.do_member_operation_asserts(memberHolderObject, member, a_test);
      }.bind(this));
  };
  
  IdlInterface.prototype.do_member_unscopable_asserts = function(member)
  {
      // Check that if the member is unscopable then it's in the
      // @@unscopables object properly.
      if (!member.isUnscopable) {
          return;
      }
  
      var unscopables = this.get_interface_object().prototype[Symbol.unscopables];
      var prop = member.name;
      var propDesc = Object.getOwnPropertyDescriptor(unscopables, prop);
      assert_equals(typeof propDesc, "object",
                    this.name + '.prototype[Symbol.unscopables].' + prop + ' must exist')
      assert_false("get" in propDesc,
                   this.name + '.prototype[Symbol.unscopables].' + prop + ' must have no getter');
      assert_false("set" in propDesc,
                   this.name + '.prototype[Symbol.unscopables].' + prop + ' must have no setter');
      assert_true(propDesc.writable,
                  this.name + '.prototype[Symbol.unscopables].' + prop + ' must be writable');
      assert_true(propDesc.enumerable,
                  this.name + '.prototype[Symbol.unscopables].' + prop + ' must be enumerable');
      assert_true(propDesc.configurable,
                  this.name + '.prototype[Symbol.unscopables].' + prop + ' must be configurable');
      assert_equals(propDesc.value, true,
                    this.name + '.prototype[Symbol.unscopables].' + prop + ' must have the value `true`');
  };
  
  IdlInterface.prototype.do_member_operation_asserts = function(memberHolderObject, member, a_test)
  {
      var done = a_test.done.bind(a_test);
      var operationUnforgeable = member.isUnforgeable;
      var desc = Object.getOwnPropertyDescriptor(memberHolderObject, member.name);
      // "The property has attributes { [[Writable]]: B,
      // [[Enumerable]]: true, [[Configurable]]: B }, where B is false if the
      // operation is unforgeable on the interface, and true otherwise".
      assert_false("get" in desc, "property should not have a getter");
      assert_false("set" in desc, "property should not have a setter");
      assert_equals(desc.writable, !operationUnforgeable,
                    "property should be writable if and only if not unforgeable");
      assert_true(desc.enumerable, "property should be enumerable");
      assert_equals(desc.configurable, !operationUnforgeable,
                    "property should be configurable if and only if not unforgeable");
      // "The value of the property is a Function object whose
      // behavior is as follows . . ."
      assert_equals(typeof memberHolderObject[member.name], "function",
                    "property must be a function");
  
      const ctors = this.members.filter(function(m) {
          return m.type == "operation" && m.name == member.name;
      });
      assert_equals(
          memberHolderObject[member.name].length,
          minOverloadLength(ctors),
          "property has wrong .length");
  
      // Make some suitable arguments
      var args = member.arguments.map(function(arg) {
          return create_suitable_object(arg.idlType);
      });
  
      // "Let O be a value determined as follows:
      // ". . .
      // "Otherwise, throw a TypeError."
      // This should be hit if the operation is not static, there is
      // no [ImplicitThis] attribute, and the this value is null.
      //
      // TODO: We currently ignore the [ImplicitThis] case.  Except we manually
      // check for globals, since otherwise we'll invoke window.close().  And we
      // have to skip this test for anything that on the proto chain of "self",
      // since that does in fact have implicit-this behavior.
      if (member.special !== "static") {
          var cb;
          if (!this.is_global() &&
              memberHolderObject[member.name] != self[member.name])
          {
              cb = awaitNCallbacks(2, done);
              throwOrReject(a_test, member, memberHolderObject[member.name], null, args,
                            "calling operation with this = null didn't throw TypeError", cb);
          } else {
              cb = awaitNCallbacks(1, done);
          }
  
          // ". . . If O is not null and is also not a platform object
          // that implements interface I, throw a TypeError."
          //
          // TODO: Test a platform object that implements some other
          // interface.  (Have to be sure to get inheritance right.)
          throwOrReject(a_test, member, memberHolderObject[member.name], {}, args,
                        "calling operation with this = {} didn't throw TypeError", cb);
      } else {
          done();
      }
  }
  
  IdlInterface.prototype.test_to_json_operation = function(desc, memberHolderObject, member) {
      var instanceName = memberHolderObject && memberHolderObject.constructor.name
          || member.name + " object";
      if (member.has_extended_attribute("Default")) {
          subsetTestByKey(this.name, test, function() {
              var map = this.default_to_json_operation();
              var json = memberHolderObject.toJSON();
              map.forEach(function(type, k) {
                  assert_true(k in json, "property " + JSON.stringify(k) + " should be present in the output of " + this.name + ".prototype.toJSON()");
                  var descriptor = Object.getOwnPropertyDescriptor(json, k);
                  assert_true(descriptor.writable, "property " + k + " should be writable");
                  assert_true(descriptor.configurable, "property " + k + " should be configurable");
                  assert_true(descriptor.enumerable, "property " + k + " should be enumerable");
                  this.array.assert_type_is(json[k], type);
                  delete json[k];
              }, this);
          }.bind(this), this.name + " interface: default toJSON operation on " + desc);
      } else {
          subsetTestByKey(this.name, test, function() {
              assert_true(this.array.is_json_type(member.idlType), JSON.stringify(member.idlType) + " is not an appropriate return value for the toJSON operation of " + instanceName);
              this.array.assert_type_is(memberHolderObject.toJSON(), member.idlType);
          }.bind(this), this.name + " interface: toJSON operation on " + desc);
      }
  };
  
  IdlInterface.prototype.test_member_iterable = function(member)
  {
      subsetTestByKey(this.name, test, function()
      {
          var isPairIterator = member.idlType.length === 2;
          var proto = this.get_interface_object().prototype;
          var descriptor = Object.getOwnPropertyDescriptor(proto, Symbol.iterator);
  
          assert_true(descriptor.writable, "@@iterator property should be writable");
          assert_true(descriptor.configurable, "@@iterator property should be configurable");
          assert_false(descriptor.enumerable, "@@iterator property should not be enumerable");
          assert_equals(typeof descriptor.value, "function", "@@iterator property should be a function");
          assert_equals(descriptor.value.length, 0, "@@iterator function object length should be 0");
          assert_equals(descriptor.value.name, isPairIterator ? "entries" : "values", "@@iterator function object should have the right name");
  
          if (isPairIterator) {
              assert_equals(proto["entries"], proto[Symbol.iterator], "entries method should be the same as @@iterator method");
          } else {
              assert_equals(proto[Symbol.iterator], Array.prototype[Symbol.iterator], "@@iterator method should be the same as Array prototype's");
              ["entries", "keys", "values", "forEach", Symbol.iterator].forEach(function(property) {
                  var propertyName = property === Symbol.iterator ? "@@iterator" : property;
                  assert_equals(proto[property], Array.prototype[property], propertyName + " method should be the same as Array prototype's");
              }.bind(this));
          }
      }.bind(this), this.name + " interface: iterable<" + member.idlType.map(function(t) { return t.idlType; }).join(", ") + ">");
  };
  
  IdlInterface.prototype.test_member_stringifier = function(member)
  {
      subsetTestByKey(this.name, test, function()
      {
          if (this.is_callback() && !this.has_constants()) {
              return;
          }
  
          this.assert_interface_object_exists();
  
          if (this.is_callback()) {
              assert_false("prototype" in this.get_interface_object(),
                           this.name + ' should not have a "prototype" property');
              return;
          }
  
          assert_own_property(this.get_interface_object(), "prototype",
                              'interface "' + this.name + '" does not have own property "prototype"');
  
          // ". . . the property exists on the interface prototype object."
          var interfacePrototypeObject = this.get_interface_object().prototype;
          assert_own_property(interfacePrototypeObject, "toString",
                  "interface prototype object missing non-static operation");
  
          var stringifierUnforgeable = member.isUnforgeable;
          var desc = Object.getOwnPropertyDescriptor(interfacePrototypeObject, "toString");
          // "The property has attributes { [[Writable]]: B,
          // [[Enumerable]]: true, [[Configurable]]: B }, where B is false if the
          // stringifier is unforgeable on the interface, and true otherwise."
          assert_false("get" in desc, "property should not have a getter");
          assert_false("set" in desc, "property should not have a setter");
          assert_equals(desc.writable, !stringifierUnforgeable,
                        "property should be writable if and only if not unforgeable");
          assert_true(desc.enumerable, "property should be enumerable");
          assert_equals(desc.configurable, !stringifierUnforgeable,
                        "property should be configurable if and only if not unforgeable");
          // "The value of the property is a Function object, which behaves as
          // follows . . ."
          assert_equals(typeof interfacePrototypeObject.toString, "function",
                        "property must be a function");
          // "The value of the Function object’s “length” property is the Number
          // value 0."
          assert_equals(interfacePrototypeObject.toString.length, 0,
              "property has wrong .length");
  
          // "Let O be the result of calling ToObject on the this value."
          assert_throws(new TypeError(), function() {
              interfacePrototypeObject.toString.apply(null, []);
          }, "calling stringifier with this = null didn't throw TypeError");
  
          // "If O is not an object that implements the interface on which the
          // stringifier was declared, then throw a TypeError."
          //
          // TODO: Test a platform object that implements some other
          // interface.  (Have to be sure to get inheritance right.)
          assert_throws(new TypeError(), function() {
              interfacePrototypeObject.toString.apply({}, []);
          }, "calling stringifier with this = {} didn't throw TypeError");
      }.bind(this), this.name + " interface: stringifier");
  };
  
  IdlInterface.prototype.test_members = function()
  {
      for (var i = 0; i < this.members.length; i++)
      {
          var member = this.members[i];
          if (member.untested) {
              continue;
          }
  
          if (!exposed_in(exposure_set(member, this.exposureSet))) {
              subsetTestByKey(this.name, test, function() {
                  // It's not exposed, so we shouldn't find it anywhere.
                  assert_false(member.name in this.get_interface_object(),
                               "The interface object must not have a property " +
                               format_value(member.name));
                  assert_false(member.name in this.get_interface_object().prototype,
                               "The prototype object must not have a property " +
                               format_value(member.name));
              }.bind(this), this.name + " interface: member " + member.name);
              continue;
          }
  
          switch (member.type) {
          case "const":
              this.test_member_const(member);
              break;
  
          case "attribute":
              // For unforgeable attributes, we do the checks in
              // test_interface_of instead.
              if (!member.isUnforgeable)
              {
                  this.test_member_attribute(member);
              }
              if (member.special === "stringifier") {
                  this.test_member_stringifier(member);
              }
              break;
  
          case "operation":
              // TODO: Need to correctly handle multiple operations with the same
              // identifier.
              // For unforgeable operations, we do the checks in
              // test_interface_of instead.
              if (member.name) {
                  if (!member.isUnforgeable)
                  {
                      this.test_member_operation(member);
                  }
              } else if (member.special === "stringifier") {
                  this.test_member_stringifier(member);
              }
              break;
  
          case "iterable":
              this.test_member_iterable(member);
              break;
          default:
              // TODO: check more member types.
              break;
          }
      }
  };
  
  IdlInterface.prototype.test_object = function(desc)
  {
      var obj, exception = null;
      try
      {
          obj = eval(desc);
      }
      catch(e)
      {
          exception = e;
      }
  
      var expected_typeof =
          this.members.some(function(member) { return member.legacycaller; })
          ? "function"
          : "object";
  
      this.test_primary_interface_of(desc, obj, exception, expected_typeof);
  
      var current_interface = this;
      while (current_interface)
      {
          if (!(current_interface.name in this.array.members))
          {
              throw new IdlHarnessError("Interface " + current_interface.name + " not found (inherited by " + this.name + ")");
          }
          if (current_interface.prevent_multiple_testing && current_interface.already_tested)
          {
              return;
          }
          current_interface.test_interface_of(desc, obj, exception, expected_typeof);
          current_interface = this.array.members[current_interface.base];
      }
  };
  
  IdlInterface.prototype.test_primary_interface_of = function(desc, obj, exception, expected_typeof)
  {
      // Only the object itself, not its members, are tested here, so if the
      // interface is untested, there is nothing to do.
      if (this.untested)
      {
          return;
      }
  
      // "The internal [[SetPrototypeOf]] method of every platform object that
      // implements an interface with the [Global] extended
      // attribute must execute the same algorithm as is defined for the
      // [[SetPrototypeOf]] internal method of an immutable prototype exotic
      // object."
      // https://heycam.github.io/webidl/#platform-object-setprototypeof
      if (this.is_global())
      {
          this.test_immutable_prototype("global platform object", obj);
      }
  
  
      // We can't easily test that its prototype is correct if there's no
      // interface object, or the object is from a different global environment
      // (not instanceof Object).  TODO: test in this case that its prototype at
      // least looks correct, even if we can't test that it's actually correct.
      if (!this.has_extended_attribute("NoInterfaceObject")
      && (typeof obj != expected_typeof || obj instanceof Object))
      {
          subsetTestByKey(this.name, test, function()
          {
              assert_equals(exception, null, "Unexpected exception when evaluating object");
              assert_equals(typeof obj, expected_typeof, "wrong typeof object");
              this.assert_interface_object_exists();
              assert_own_property(this.get_interface_object(), "prototype",
                                  'interface "' + this.name + '" does not have own property "prototype"');
  
              // "The value of the internal [[Prototype]] property of the
              // platform object is the interface prototype object of the primary
              // interface from the platform object’s associated global
              // environment."
              assert_equals(Object.getPrototypeOf(obj),
                            this.get_interface_object().prototype,
                            desc + "'s prototype is not " + this.name + ".prototype");
          }.bind(this), this.name + " must be primary interface of " + desc);
      }
  
      // "The class string of a platform object that implements one or more
      // interfaces must be the qualified name of the primary interface of the
      // platform object."
      subsetTestByKey(this.name, test, function()
      {
          assert_equals(exception, null, "Unexpected exception when evaluating object");
          assert_equals(typeof obj, expected_typeof, "wrong typeof object");
          assert_class_string(obj, this.get_qualified_name(), "class string of " + desc);
          if (!this.has_stringifier())
          {
              assert_equals(String(obj), "[object " + this.get_qualified_name() + "]", "String(" + desc + ")");
          }
      }.bind(this), "Stringification of " + desc);
  };
  
  IdlInterface.prototype.test_interface_of = function(desc, obj, exception, expected_typeof)
  {
      // TODO: Indexed and named properties, more checks on interface members
      this.already_tested = true;
      if (!shouldRunSubTest(this.name)) {
          return;
      }
  
      for (var i = 0; i < this.members.length; i++)
      {
          var member = this.members[i];
          if (member.untested) {
              continue;
          }
          if (!exposed_in(exposure_set(member, this.exposureSet))) {
              subsetTestByKey(this.name, test, function() {
                  assert_equals(exception, null, "Unexpected exception when evaluating object");
                  assert_false(member.name in obj);
              }.bind(this), this.name + " interface: " + desc + ' must not have property "' + member.name + '"');
              continue;
          }
          if (member.type == "attribute" && member.isUnforgeable)
          {
              var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: " + desc + ' must have own property "' + member.name + '"');
              a_test.step(function() {
                  assert_equals(exception, null, "Unexpected exception when evaluating object");
                  assert_equals(typeof obj, expected_typeof, "wrong typeof object");
                  // Call do_interface_attribute_asserts last, since it will call a_test.done()
                  this.do_interface_attribute_asserts(obj, member, a_test);
              }.bind(this));
          }
          else if (member.type == "operation" &&
                   member.name &&
                   member.isUnforgeable)
          {
              var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: " + desc + ' must have own property "' + member.name + '"');
              a_test.step(function()
              {
                  assert_equals(exception, null, "Unexpected exception when evaluating object");
                  assert_equals(typeof obj, expected_typeof, "wrong typeof object");
                  assert_own_property(obj, member.name,
                                      "Doesn't have the unforgeable operation property");
                  this.do_member_operation_asserts(obj, member, a_test);
              }.bind(this));
          }
          else if ((member.type == "const"
          || member.type == "attribute"
          || member.type == "operation")
          && member.name)
          {
              var described_name = member.name;
              if (member.type == "operation")
              {
                  described_name += "(" + member.arguments.map(arg => arg.idlType.idlType).join(", ") + ")";
              }
              subsetTestByKey(this.name, test, function()
              {
                  assert_equals(exception, null, "Unexpected exception when evaluating object");
                  assert_equals(typeof obj, expected_typeof, "wrong typeof object");
                  if (member.special !== "static") {
                      if (!this.is_global()) {
                          assert_inherits(obj, member.name);
                      } else {
                          assert_own_property(obj, member.name);
                      }
  
                      if (member.type == "const")
                      {
                          assert_equals(obj[member.name], constValue(member.value));
                      }
                      if (member.type == "attribute")
                      {
                          // Attributes are accessor properties, so they might
                          // legitimately throw an exception rather than returning
                          // anything.
                          var property, thrown = false;
                          try
                          {
                              property = obj[member.name];
                          }
                          catch (e)
                          {
                              thrown = true;
                          }
                          if (!thrown)
                          {
                              this.array.assert_type_is(property, member.idlType);
                          }
                      }
                      if (member.type == "operation")
                      {
                          assert_equals(typeof obj[member.name], "function");
                      }
                  }
              }.bind(this), this.name + " interface: " + desc + ' must inherit property "' + described_name + '" with the proper type');
          }
          // TODO: This is wrong if there are multiple operations with the same
          // identifier.
          // TODO: Test passing arguments of the wrong type.
          if (member.type == "operation" && member.name && member.arguments.length)
          {
              var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: calling " + member.name +
              "(" + member.arguments.map(function(m) { return m.idlType.idlType; }).join(", ") +
              ") on " + desc + " with too few arguments must throw TypeError");
              a_test.step(function()
              {
                  assert_equals(exception, null, "Unexpected exception when evaluating object");
                  assert_equals(typeof obj, expected_typeof, "wrong typeof object");
                  var fn;
                  if (member.special !== "static") {
                      if (!this.is_global() && !member.isUnforgeable) {
                          assert_inherits(obj, member.name);
                      } else {
                          assert_own_property(obj, member.name);
                      }
                      fn = obj[member.name];
                  }
                  else
                  {
                      assert_own_property(obj.constructor, member.name, "interface object must have static operation as own property");
                      fn = obj.constructor[member.name];
                  }
  
                  var minLength = minOverloadLength(this.members.filter(function(m) {
                      return m.type == "operation" && m.name == member.name;
                  }));
                  var args = [];
                  var cb = awaitNCallbacks(minLength, a_test.done.bind(a_test));
                  for (var i = 0; i < minLength; i++) {
                      throwOrReject(a_test, member, fn, obj, args, "Called with " + i + " arguments", cb);
  
                      args.push(create_suitable_object(member.arguments[i].idlType));
                  }
                  if (minLength === 0) {
                      cb();
                  }
              }.bind(this));
          }
  
          if (member.is_to_json_regular_operation()) {
              this.test_to_json_operation(desc, obj, member);
          }
      }
  };
  
  IdlInterface.prototype.has_stringifier = function()
  {
      if (this.name === "DOMException") {
          // toString is inherited from Error, so don't assume we have the
          // default stringifer
          return true;
      }
      if (this.members.some(function(member) { return member.special === "stringifier"; })) {
          return true;
      }
      if (this.base &&
          this.array.members[this.base].has_stringifier()) {
          return true;
      }
      return false;
  };
  
  IdlInterface.prototype.do_interface_attribute_asserts = function(obj, member, a_test)
  {
      // This function tests WebIDL as of 2015-01-27.
      // TODO: Consider [Exposed].
  
      // This is called by test_member_attribute() with the prototype as obj if
      // it is not a global, and the global otherwise, and by test_interface_of()
      // with the object as obj.
  
      var pendingPromises = [];
  
      // "For each exposed attribute of the interface, whether it was declared on
      // the interface itself or one of its consequential interfaces, there MUST
      // exist a corresponding property. The characteristics of this property are
      // as follows:"
  
      // "The name of the property is the identifier of the attribute."
      assert_own_property(obj, member.name);
  
      // "The property has attributes { [[Get]]: G, [[Set]]: S, [[Enumerable]]:
      // true, [[Configurable]]: configurable }, where:
      // "configurable is false if the attribute was declared with the
      // [Unforgeable] extended attribute and true otherwise;
      // "G is the attribute getter, defined below; and
      // "S is the attribute setter, also defined below."
      var desc = Object.getOwnPropertyDescriptor(obj, member.name);
      assert_false("value" in desc, 'property descriptor should not have a "value" field');
      assert_false("writable" in desc, 'property descriptor should not have a "writable" field');
      assert_true(desc.enumerable, "property should be enumerable");
      if (member.isUnforgeable)
      {
          assert_false(desc.configurable, "[Unforgeable] property must not be configurable");
      }
      else
      {
          assert_true(desc.configurable, "property must be configurable");
      }
  
  
      // "The attribute getter is a Function object whose behavior when invoked
      // is as follows:"
      assert_equals(typeof desc.get, "function", "getter must be Function");
  
      // "If the attribute is a regular attribute, then:"
      if (member.special !== "static") {
          // "If O is not a platform object that implements I, then:
          // "If the attribute was specified with the [LenientThis] extended
          // attribute, then return undefined.
          // "Otherwise, throw a TypeError."
          if (!member.has_extended_attribute("LenientThis")) {
              if (member.idlType.generic !== "Promise") {
                  assert_throws(new TypeError(), function() {
                      desc.get.call({});
                  }.bind(this), "calling getter on wrong object type must throw TypeError");
              } else {
                  pendingPromises.push(
                      promise_rejects(a_test, new TypeError(), desc.get.call({}),
                                      "calling getter on wrong object type must reject the return promise with TypeError"));
              }
          } else {
              assert_equals(desc.get.call({}), undefined,
                            "calling getter on wrong object type must return undefined");
          }
      }
  
      // "The value of the Function object’s “length” property is the Number
      // value 0."
      assert_equals(desc.get.length, 0, "getter length must be 0");
  
      // "Let name be the string "get " prepended to attribute’s identifier."
      // "Perform ! SetFunctionName(F, name)."
      assert_equals(desc.get.name, "get " + member.name,
          "getter must have the name 'get " + member.name + "'");
  
  
      // TODO: Test calling setter on the interface prototype (should throw
      // TypeError in most cases).
      if (member.readonly
      && !member.has_extended_attribute("LenientSetter")
      && !member.has_extended_attribute("PutForwards")
      && !member.has_extended_attribute("Replaceable"))
      {
          // "The attribute setter is undefined if the attribute is declared
          // readonly and has neither a [PutForwards] nor a [Replaceable]
          // extended attribute declared on it."
          assert_equals(desc.set, undefined, "setter must be undefined for readonly attributes");
      }
      else
      {
          // "Otherwise, it is a Function object whose behavior when
          // invoked is as follows:"
          assert_equals(typeof desc.set, "function", "setter must be function for PutForwards, Replaceable, or non-readonly attributes");
  
          // "If the attribute is a regular attribute, then:"
          if (member.special !== "static") {
              // "If /validThis/ is false and the attribute was not specified
              // with the [LenientThis] extended attribute, then throw a
              // TypeError."
              // "If the attribute is declared with a [Replaceable] extended
              // attribute, then: ..."
              // "If validThis is false, then return."
              if (!member.has_extended_attribute("LenientThis")) {
                  assert_throws(new TypeError(), function() {
                      desc.set.call({});
                  }.bind(this), "calling setter on wrong object type must throw TypeError");
              } else {
                  assert_equals(desc.set.call({}), undefined,
                                "calling setter on wrong object type must return undefined");
              }
          }
  
          // "The value of the Function object’s “length” property is the Number
          // value 1."
          assert_equals(desc.set.length, 1, "setter length must be 1");
  
          // "Let name be the string "set " prepended to id."
          // "Perform ! SetFunctionName(F, name)."
          assert_equals(desc.set.name, "set " + member.name,
              "The attribute setter must have the name 'set " + member.name + "'");
      }
  
      Promise.all(pendingPromises).then(a_test.done.bind(a_test));
  }
  
  /// IdlInterfaceMember ///
  function IdlInterfaceMember(obj)
  {
      /**
       * obj is an object produced by the WebIDLParser.js "ifMember" production.
       * We just forward all properties to this object without modification,
       * except for special extAttrs handling.
       */
      for (var k in obj.toJSON())
      {
          this[k] = obj[k];
      }
      if (!("extAttrs" in this))
      {
          this.extAttrs = [];
      }
  
      this.isUnforgeable = this.has_extended_attribute("Unforgeable");
      this.isUnscopable = this.has_extended_attribute("Unscopable");
  }
  
  IdlInterfaceMember.prototype = Object.create(IdlObject.prototype);
  
  IdlInterfaceMember.prototype.toJSON = function() {
      return this;
  };
  
  IdlInterfaceMember.prototype.is_to_json_regular_operation = function() {
      return this.type == "operation" && this.special !== "static" && this.name == "toJSON";
  };
  
  /// Internal helper functions ///
  function create_suitable_object(type)
  {
      /**
       * type is an object produced by the WebIDLParser.js "type" production.  We
       * return a JavaScript value that matches the type, if we can figure out
       * how.
       */
      if (type.nullable)
      {
          return null;
      }
      switch (type.idlType)
      {
          case "any":
          case "boolean":
              return true;
  
          case "byte": case "octet": case "short": case "unsigned short":
          case "long": case "unsigned long": case "long long":
          case "unsigned long long": case "float": case "double":
          case "unrestricted float": case "unrestricted double":
              return 7;
  
          case "DOMString":
          case "ByteString":
          case "USVString":
              return "foo";
  
          case "object":
              return {a: "b"};
  
          case "Node":
              return document.createTextNode("abc");
      }
      return null;
  }
  
  /// IdlEnum ///
  // Used for IdlArray.prototype.assert_type_is
  function IdlEnum(obj)
  {
      /**
       * obj is an object produced by the WebIDLParser.js "dictionary"
       * production.
       */
  
      /** Self-explanatory. */
      this.name = obj.name;
  
      /** An array of values produced by the "enum" production. */
      this.values = obj.values;
  
  }
  
  IdlEnum.prototype = Object.create(IdlObject.prototype);
  
  /// IdlCallback ///
  // Used for IdlArray.prototype.assert_type_is
  function IdlCallback(obj)
  {
      /**
       * obj is an object produced by the WebIDLParser.js "callback"
       * production.
       */
  
      /** Self-explanatory. */
      this.name = obj.name;
  
      /** Arguments for the callback. */
      this.arguments = obj.arguments;
  }
  
  IdlCallback.prototype = Object.create(IdlObject.prototype);
  
  /// IdlTypedef ///
  // Used for IdlArray.prototype.assert_type_is
  function IdlTypedef(obj)
  {
      /**
       * obj is an object produced by the WebIDLParser.js "typedef"
       * production.
       */
  
      /** Self-explanatory. */
      this.name = obj.name;
  
      /** The idlType that we are supposed to be typedeffing to. */
      this.idlType = obj.idlType;
  
  }
  
  IdlTypedef.prototype = Object.create(IdlObject.prototype);
  
  /// IdlNamespace ///
  function IdlNamespace(obj)
  {
      this.name = obj.name;
      this.extAttrs = obj.extAttrs;
      this.untested = obj.untested;
      /** A back-reference to our IdlArray. */
      this.array = obj.array;
  
      /** An array of IdlInterfaceMembers. */
      this.members = obj.members.map(m => new IdlInterfaceMember(m));
  }
  
  IdlNamespace.prototype = Object.create(IdlObject.prototype);
  
  IdlNamespace.prototype.do_member_operation_asserts = function (memberHolderObject, member, a_test)
  {
      var desc = Object.getOwnPropertyDescriptor(memberHolderObject, member.name);
  
      assert_false("get" in desc, "property should not have a getter");
      assert_false("set" in desc, "property should not have a setter");
      assert_equals(
          desc.writable,
          !member.isUnforgeable,
          "property should be writable if and only if not unforgeable");
      assert_true(desc.enumerable, "property should be enumerable");
      assert_equals(
          desc.configurable,
          !member.isUnforgeable,
          "property should be configurable if and only if not unforgeable");
  
      assert_equals(
          typeof memberHolderObject[member.name],
          "function",
           "property must be a function");
  
      assert_equals(
          memberHolderObject[member.name].length,
          minOverloadLength(this.members.filter(function(m) {
              return m.type == "operation" && m.name == member.name;
          })),
          "operation has wrong .length");
      a_test.done();
  }
  
  IdlNamespace.prototype.test_member_operation = function(member)
  {
      if (!shouldRunSubTest(this.name)) {
          return;
      }
      var args = member.arguments.map(function(a) {
          var s = a.idlType.idlType;
          if (a.variadic) {
              s += '...';
          }
          return s;
      }).join(", ");
      var a_test = subsetTestByKey(
          this.name,
          async_test,
          this.name + ' namespace: operation ' + member.name + '(' + args + ')');
      a_test.step(function() {
          assert_own_property(
              self[this.name],
              member.name,
              'namespace object missing operation ' + format_value(member.name));
  
          this.do_member_operation_asserts(self[this.name], member, a_test);
      }.bind(this));
  };
  
  IdlNamespace.prototype.test_member_attribute = function (member)
  {
      if (!shouldRunSubTest(this.name)) {
          return;
      }
      var a_test = subsetTestByKey(
          this.name,
          async_test,
          this.name + ' namespace: attribute ' + member.name);
      a_test.step(function()
      {
          assert_own_property(
              self[this.name],
              member.name,
              this.name + ' does not have property ' + format_value(member.name));
  
          var desc = Object.getOwnPropertyDescriptor(self[this.name], member.name);
          assert_equals(desc.set, undefined, "setter must be undefined for namespace members");
          a_test.done();
      }.bind(this));
  };
  
  IdlNamespace.prototype.test = function ()
  {
      /**
       * TODO(lukebjerring): Assert:
       * - "Note that unlike interfaces or dictionaries, namespaces do not create types."
       * - "Of the extended attributes defined in this specification, only the
       *     [Exposed] and [SecureContext] extended attributes are applicable to namespaces."
       * - "Namespaces must be annotated with the [Exposed] extended attribute."
       */
  
      for (const v of Object.values(this.members)) {
          switch (v.type) {
  
          case 'operation':
              this.test_member_operation(v);
              break;
  
          case 'attribute':
              this.test_member_attribute(v);
              break;
  
          default:
              throw 'Invalid namespace member ' + v.name + ': ' + v.type + ' not supported';
          }
      };
  };
  
  }());
  
  /**
   * idl_test is a promise_test wrapper that handles the fetching of the IDL,
   * avoiding repetitive boilerplate.
   *
   * @param {String|String[]} srcs Spec name(s) for source idl files (fetched from
   *      /interfaces/{name}.idl).
   * @param {String|String[]} deps Spec name(s) for dependency idl files (fetched
   *      from /interfaces/{name}.idl). Order is important - dependencies from
   *      each source will only be included if they're already know to be a
   *      dependency (i.e. have already been seen).
   * @param {Function} setup_func Function for extra setup of the idl_array, such
   *      as adding objects. Do not call idl_array.test() in the setup; it is
   *      called by this function (idl_test).
   */
  function idl_test(srcs, deps, idl_setup_func) {
      return promise_test(function (t) {
          var idl_array = new IdlArray();
          srcs = (srcs instanceof Array) ? srcs : [srcs] || [];
          deps = (deps instanceof Array) ? deps : [deps] || [];
          var setup_error = null;
          return Promise.all(
              srcs.concat(deps).map(function(spec) {
                  return fetch_spec(spec);
              }))
              .then(function(idls) {
                  for (var i = 0; i < srcs.length; i++) {
                      idl_array.add_idls(idls[i]);
                  }
                  for (var i = srcs.length; i < srcs.length + deps.length; i++) {
                      idl_array.add_dependency_idls(idls[i]);
                  }
              })
              .then(function() {
                  if (idl_setup_func) {
                      return idl_setup_func(idl_array, t);
                  }
              })
              .catch(function(e) { setup_error = e || 'IDL setup failed.'; })
              .then(function () {
                  var error = setup_error;
                  try {
                      idl_array.test(); // Test what we can.
                  } catch (e) {
                      // If testing fails hard here, the original setup error
                      // is more likely to be the real cause.
                      error = error || e;
                  }
                  if (error) {
                      throw error;
                  }
              });
      }, 'idl_test setup');
  }
  
  /**
   * fetch_spec is a shorthand for a Promise that fetches the spec's content.
   */
  function fetch_spec(spec) {
      var url = '/interfaces/' + spec + '.idl';
      return fetch(url).then(function (r) {
          if (!r.ok) {
              throw new IdlHarnessError("Error fetching " + url + ".");
          }
          return r.text();
      });
  }
  // vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker:
  