1
0
Fork 0
firefox/testing/web-platform/tests/resources/idlharness.js
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

3573 lines
142 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 objects “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); });
}
// A helper to get the global of a Function object. This is needed to determine
// which global exceptions the function throws will come from.
function globalOf(func)
{
try {
// Use the fact that .constructor for a Function object is normally the
// Function constructor, which can be used to mint a new function in the
// right global.
return func.constructor("return this;")();
} catch (e) {
}
// If the above fails, because someone gave us a non-function, or a function
// with a weird proto chain or weird .constructor property, just fall back
// to 'self'.
return self;
}
// https://esdiscuss.org/topic/isconstructor#content-11
function isConstructor(o) {
try {
new (new Proxy(o, {construct: () => ({})}));
return true;
} catch(e) {
return false;
}
}
function throwOrReject(a_test, operation, fn, obj, args, message, cb)
{
if (operation.idlType.generic !== "Promise") {
assert_throws_js(globalOf(fn).TypeError, function() {
fn.apply(obj, args);
}, message);
cb();
} else {
try {
promise_rejects_js(a_test, 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();
}
};
}
/// 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 includes statement that depends
* on a later one. Save these up and handle them right before we run
* tests.
*
* Both this.partials and this.includes will be the objects as parsed by
* WebIDLParser.js, not wrapped in IdlInterface or similar.
*/
this.partials = [];
this.includes = [];
/**
* 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)
{
return this.internal_add_dependency_idls(WebIDL2.parse(raw_idls), options);
};
IdlArray.prototype.internal_add_dependency_idls = function(parsed_idls, options)
{
const new_options = { only: [] }
const all_deps = new Set();
Object.values(this.members).forEach(v => {
if (v.base) {
all_deps.add(v.base);
}
});
// Add both 'A' and 'B' for each 'A includes B' entry.
this.includes.forEach(i => {
all_deps.add(i.target);
all_deps.add(i.includes);
});
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 === "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", "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 == "includes")
{
if (should_skip(parsed_idl.target))
{
return;
}
this.includes.push(parsed_idl);
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);
}
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.is_json_type = function(type)
{
/**
* Checks whether type is a JSON type as per
* https://webidl.spec.whatwg.org/#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 "BigInt64Array":
case "BigUint64Array":
case "Float16Array":
case "Float32Array":
case "Float64Array":
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) {
const map = new Map();
for (const dict of thing.get_reverse_inheritance_stack()) {
for (const m of dict.members) {
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 interfaces.
if (thing instanceof IdlInterface) {
var base;
while (thing)
{
if (thing.has_to_json_regular_operation()) { return true; }
var mixins = 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 === "*" ?
[ "*" ] :
rhs.type === "identifier-list" ?
rhs.value.map(id => id.value) :
[ rhs.value ];
result = new Set(set);
}
if (result && result.has("*")) {
return "*";
}
if (result && result.has("Worker")) {
result.delete("Worker");
result.add("DedicatedWorker");
result.add("ServiceWorker");
result.add("SharedWorker");
}
return result;
}
function exposed_in(globals) {
if (globals === "*") {
return true;
}
if ('Window' 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");
}
if (Object.getPrototypeOf(self) === Object.prototype) {
// ShadowRealm - only exposed with `"*"`.
return false;
}
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 partial definitions and interface mixins.
this.merge_partials();
this.merge_mixins();
// 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_reverse_inheritance_stack();
}
Object.getOwnPropertyNames(this.members).forEach(function(memberName) {
var member = this.members[memberName];
if (!(member instanceof IdlInterface || member instanceof IdlNamespace)) {
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.merge_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);
// Ensure unique test name in case of multiple partials.
let partialTestName = parsed_idl.name;
let partialTestCount = 1;
if (testedPartials.has(parsed_idl.name)) {
partialTestCount += testedPartials.get(parsed_idl.name);
partialTestName = `${partialTestName}[${partialTestCount}]`;
}
testedPartials.set(parsed_idl.name, partialTestCount);
if (!self.shouldRunSubTest(partialTestName)) {
return;
}
if (!parsed_idl.untested) {
test(function () {
assert_true(originalExists, `Original ${parsed_idl.type} should be defined`);
var expected;
switch (parsed_idl.type) {
case 'dictionary': expected = IdlDictionary; break;
case 'namespace': expected = IdlNamespace; break;
case 'interface':
case 'interface mixin':
default:
expected = IdlInterface; 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]);
if (memberExposure === "*") {
return;
}
if (partialExposure === "*") {
throw new IdlHarnessError(
`Partial ${parsed_idl.name} ${parsed_idl.type} is exposed everywhere, the original ${parsed_idl.type} is not.`);
}
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));
}
if (parsed_idl.members.length) {
test(function () {
var clash = parsed_idl.members.find(function(member) {
return this.members[parsed_idl.name].members.find(function(m) {
return this.are_duplicate_members(m, member);
}.bind(this));
}.bind(this));
parsed_idl.members.forEach(function(member)
{
this.members[parsed_idl.name].members.push(new IdlInterfaceMember(member));
}.bind(this));
assert_true(!clash, "member " + (clash && clash.name) + " is unique");
}.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: member names are unique`);
}
}.bind(this));
this.partials = [];
}
IdlArray.prototype.merge_mixins = function()
{
for (const parsed_idl of this.includes)
{
const lhs = parsed_idl.target;
const rhs = parsed_idl.includes;
const testName = lhs + " includes " + rhs + ": member names are unique";
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.";
if (this.members[rhs].members.length && self.shouldRunSubTest(testName)) {
test(function () {
var clash = this.members[rhs].members.find(function(member) {
return this.members[lhs].members.find(function(m) {
return this.are_duplicate_members(m, member);
}.bind(this));
}.bind(this));
this.members[rhs].members.forEach(function(member) {
assert_true(
this.members[lhs].members.every(m => !this.are_duplicate_members(m, member)),
"member " + member.name + " is unique");
this.members[lhs].members.push(new IdlInterfaceMember(member));
}.bind(this));
assert_true(!clash, "member " + (clash && clash.name) + " is unique");
}.bind(this), testName);
}
}
this.includes = [];
}
IdlArray.prototype.are_duplicate_members = function(m1, m2) {
if (m1.name !== m2.name) {
return false;
}
if (m1.type === 'operation' && m2.type === 'operation'
&& m1.arguments.length !== m2.arguments.length) {
// Method overload. TODO: Deep comparison of arguments.
return false;
}
return true;
}
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.nullable && value === null)
{
// This is fine
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.array)
{
// TODO: not supported yet
return;
}
if (type.generic === "sequence" || type.generic == "ObservableArray")
{
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 "undefined":
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, Math.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, Math.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 "ArrayBufferView":
assert_true(ArrayBuffer.isView(value));
return;
case "object":
assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function");
return;
}
// This is a catch-all for any IDL type name which follows JS class
// semantics. This includes some non-interface IDL types (e.g. Int8Array,
// Function, ...), as well as any interface types that are not in the IDL
// that is fed to the harness. If an IDL type does not follow JS class
// semantics then it should go in the switch statement above. If an IDL
// type needs full checking, then the test should include it in the IDL it
// feeds to the harness.
if (!(type in this.members))
{
assert_true(value instanceof self[type], "wrong type: not a " + type);
return;
}
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
// LegacyNoInterfaceObject 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("LegacyNoInterfaceObject")
&& 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_reverse_inheritance_stack = function() {
return IdlInterface.prototype.get_reverse_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("LegacyUnforgeable")) {
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://webidl.spec.whatwg.org/#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.should_have_interface_object = function()
{
// "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
// [LegacyNoInterfaceObject] extended attribute,
// a corresponding property MUST exist on the ECMAScript global object.
return this.is_callback() ? this.has_constants() : !this.has_extended_attribute("LegacyNoInterfaceObject");
};
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.should_have_interface_object()) {
var reason = this.is_callback() ? "lack of declared constants" : "declared [LegacyNoInterfaceObject] attribute";
throw new IdlHarnessError(this.name + " has no interface object due to " + reason);
}
return this.get_interface_object_owner()[this.name];
};
IdlInterface.prototype.get_qualified_name = function() {
// https://webidl.spec.whatwg.org/#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");
});
};
/**
* Implementation of https://webidl.spec.whatwg.org/#create-an-inheritance-stack
* with the order reversed.
*
* The order is reversed so that the base class comes first in the list, because
* this is what all call sites need.
*
* So given:
*
* A : B {};
* B : C {};
* C {};
*
* then A.get_reverse_inheritance_stack() returns [C, B, A],
* and B.get_reverse_inheritance_stack() returns [C, B].
*
* Note: as dictionary inheritance is expressed identically by the AST,
* this works just as well for getting a stack of inherited dictionaries.
*/
IdlInterface.prototype.get_reverse_inheritance_stack = function() {
const stack = [this];
let idl_interface = this;
while (idl_interface.base) {
const 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.unshift(base);
const dep_chain = stack.map(i => i.name).join(',');
throw new IdlHarnessError(`${this.name} has a circular dependency: ${dep_chain}`);
}
idl_interface = base;
stack.unshift(idl_interface);
}
return stack;
};
/**
* Implementation of
* https://webidl.spec.whatwg.org/#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() {
const map = new Map()
let isDefault = false;
for (const I of this.get_reverse_inheritance_stack()) {
if (I.has_default_to_json_regular_operation()) {
isDefault = true;
for (const m of I.members) {
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;
};
IdlInterface.prototype.test = function()
{
if (this.has_extended_attribute("LegacyNoInterfaceObject") || 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 the interface object is not exposed, only test that. Members can't be
// tested either, but objects could still be tested in |test_object|.
if (!this.exposed)
{
if (!this.untested)
{
subsetTestByKey(this.name, test, function() {
assert_false(this.name in self, this.name + " interface should not exist");
}.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.constructors = function()
{
return this.members
.filter(function(m) { return m.type == "constructor"; });
}
IdlInterface.prototype.test_self = function()
{
subsetTestByKey(this.name, test, function()
{
if (!this.should_have_interface_object()) {
return;
}
// 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 }."
// 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."
// "* 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("LegacyNoInterfaceObject")) {
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");
}
// Always test for [[Construct]]:
// https://github.com/heycam/webidl/issues/698
assert_true(isConstructor(this.get_interface_object()), "interface object must pass IsConstructor check");
var interface_object = this.get_interface_object();
assert_throws_js(globalOf(interface_object).TypeError, function() {
interface_object();
}, "interface object didn't throw TypeError when called as a function");
if (!this.constructors().length) {
assert_throws_js(globalOf(interface_object).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.should_have_interface_object() && !this.is_callback()) {
subsetTestByKey(this.name, test, function() {
// This function tests WebIDL as of 2014-10-25.
// https://webidl.spec.whatwg.org/#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.constructors();
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.should_have_interface_object()) {
subsetTestByKey(this.name, test, function() {
// This function tests WebIDL as of 2015-11-17.
// https://webidl.spec.whatwg.org/#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 === "*" || 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 [LegacyNoInterfaceObject] 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("LegacyFactoryFunction")) {
var constructors = this.extAttrs
.filter(function(attr) { return attr.name == "LegacyFactoryFunction"; });
if (constructors.length !== 1) {
throw new IdlHarnessError("Internal error: missing support for multiple LegacyFactoryFunction 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 [LegacyFactoryFunction] extended attribute on an exposed
// interface, a corresponding property must exist on the ECMAScript
// global object. The name of the property is the
// [LegacyFactoryFunction]'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_js(globalOf(self[name]).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://webidl.spec.whatwg.org/#interface-object
if (!this.should_have_interface_object()) {
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
// [LegacyNoInterfaceObject].)
// 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("LegacyNoInterfaceObject")) {
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 interfaces 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://webidl.spec.whatwg.org/#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.should_have_interface_object()) {
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 [LegacyNoInterfaceObject] 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.should_have_interface_object()) {
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_js(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() {
let setter = Object.getOwnPropertyDescriptor(
Object.prototype, '__proto__'
).set;
try {
setter.call(obj, originalValue);
} catch (err) {}
});
// We need to find the actual setter for the '__proto__' property, so we
// can determine the right global for it. Walk up the prototype chain
// looking for that property until we find it.
let setter;
{
let cur = obj;
while (cur) {
const desc = Object.getOwnPropertyDescriptor(cur, "__proto__");
if (desc) {
setter = desc.set;
break;
}
cur = Object.getPrototypeOf(cur);
}
}
assert_throws_js(globalOf(setter).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 constants 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.should_have_interface_object()) {
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("LegacyLenientThis")) {
if (member.idlType.generic !== "Promise") {
// this.get_interface_object() returns a thing in our global
assert_throws_js(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_js(a_test, TypeError,
this.get_interface_object().prototype[member.name])
.then(a_test.step_func(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);
a_test.step(function()
{
// This function tests WebIDL as of 2015-12-29.
// https://webidl.spec.whatwg.org/#es-operations
if (!this.should_have_interface_object()) {
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 interfaces
// 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 operationOverloads = this.members.filter(function(m) {
return m.type == "operation" && m.name == member.name &&
(m.special === "static") === (member.special === "static");
});
assert_equals(
memberHolderObject[member.name].length,
minOverloadLength(operationOverloads),
"property has wrong .length");
assert_equals(
memberHolderObject[member.name].name,
member.name,
"property has wrong .name");
// 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_maplike = function(member) {
subsetTestByKey(this.name, test, () => {
const proto = this.get_interface_object().prototype;
const methods = [
["entries", 0],
["keys", 0],
["values", 0],
["forEach", 1],
["get", 1],
["has", 1]
];
if (!member.readonly) {
methods.push(
["set", 2],
["delete", 1],
["clear", 0]
);
}
for (const [name, length] of methods) {
const desc = Object.getOwnPropertyDescriptor(proto, name);
assert_equals(typeof desc.value, "function", `${name} should be a function`);
assert_equals(desc.enumerable, true, `${name} enumerable`);
assert_equals(desc.configurable, true, `${name} configurable`);
assert_equals(desc.writable, true, `${name} writable`);
assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
assert_equals(desc.value.name, name, `${name} function object should have the right name`);
}
const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator);
assert_equals(iteratorDesc.value, proto.entries, `@@iterator should equal entries`);
assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
const sizeDesc = Object.getOwnPropertyDescriptor(proto, "size");
assert_equals(typeof sizeDesc.get, "function", `size getter should be a function`);
assert_equals(sizeDesc.set, undefined, `size should not have a setter`);
assert_equals(sizeDesc.enumerable, true, `size enumerable`);
assert_equals(sizeDesc.configurable, true, `size configurable`);
assert_equals(sizeDesc.get.length, 0, `size getter length`);
assert_equals(sizeDesc.get.name, "get size", `size getter name`);
}, `${this.name} interface: maplike<${member.idlType.map(t => t.idlType).join(", ")}>`);
};
IdlInterface.prototype.test_member_setlike = function(member) {
subsetTestByKey(this.name, test, () => {
const proto = this.get_interface_object().prototype;
const methods = [
["entries", 0],
["keys", 0],
["values", 0],
["forEach", 1],
["has", 1]
];
if (!member.readonly) {
methods.push(
["add", 1],
["delete", 1],
["clear", 0]
);
}
for (const [name, length] of methods) {
const desc = Object.getOwnPropertyDescriptor(proto, name);
assert_equals(typeof desc.value, "function", `${name} should be a function`);
assert_equals(desc.enumerable, true, `${name} enumerable`);
assert_equals(desc.configurable, true, `${name} configurable`);
assert_equals(desc.writable, true, `${name} writable`);
assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
assert_equals(desc.value.name, name, `${name} function object should have the right name`);
}
const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator);
assert_equals(iteratorDesc.value, proto.values, `@@iterator should equal values`);
assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
const sizeDesc = Object.getOwnPropertyDescriptor(proto, "size");
assert_equals(typeof sizeDesc.get, "function", `size getter should be a function`);
assert_equals(sizeDesc.set, undefined, `size should not have a setter`);
assert_equals(sizeDesc.enumerable, true, `size enumerable`);
assert_equals(sizeDesc.configurable, true, `size configurable`);
assert_equals(sizeDesc.get.length, 0, `size getter length`);
assert_equals(sizeDesc.get.name, "get size", `size getter name`);
}, `${this.name} interface: setlike<${member.idlType.map(t => t.idlType).join(", ")}>`);
};
IdlInterface.prototype.test_member_iterable = function(member) {
subsetTestByKey(this.name, test, () => {
const isPairIterator = member.idlType.length === 2;
const proto = this.get_interface_object().prototype;
const methods = [
["entries", 0],
["keys", 0],
["values", 0],
["forEach", 1]
];
for (const [name, length] of methods) {
const desc = Object.getOwnPropertyDescriptor(proto, name);
assert_equals(typeof desc.value, "function", `${name} should be a function`);
assert_equals(desc.enumerable, true, `${name} enumerable`);
assert_equals(desc.configurable, true, `${name} configurable`);
assert_equals(desc.writable, true, `${name} writable`);
assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
assert_equals(desc.value.name, name, `${name} function object should have the right name`);
if (!isPairIterator) {
assert_equals(desc.value, Array.prototype[name], `${name} equality with Array.prototype version`);
}
}
const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator);
assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
if (isPairIterator) {
assert_equals(iteratorDesc.value, proto.entries, `@@iterator equality with entries`);
} else {
assert_equals(iteratorDesc.value, Array.prototype[Symbol.iterator], `@@iterator equality with Array.prototype version`);
}
}, `${this.name} interface: iterable<${member.idlType.map(t => t.idlType).join(", ")}>`);
};
IdlInterface.prototype.test_member_async_iterable = function(member) {
subsetTestByKey(this.name, test, () => {
const isPairIterator = member.idlType.length === 2;
const proto = this.get_interface_object().prototype;
// Note that although the spec allows arguments, which will be passed to the @@asyncIterator
// method (which is either values or entries), those arguments must always be optional. So
// length of 0 is still correct for values and entries.
const methods = [
["values", 0],
];
if (isPairIterator) {
methods.push(
["entries", 0],
["keys", 0]
);
}
for (const [name, length] of methods) {
const desc = Object.getOwnPropertyDescriptor(proto, name);
assert_equals(typeof desc.value, "function", `${name} should be a function`);
assert_equals(desc.enumerable, true, `${name} enumerable`);
assert_equals(desc.configurable, true, `${name} configurable`);
assert_equals(desc.writable, true, `${name} writable`);
assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
assert_equals(desc.value.name, name, `${name} function object should have the right name`);
}
const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.asyncIterator);
assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
if (isPairIterator) {
assert_equals(iteratorDesc.value, proto.entries, `@@iterator equality with entries`);
} else {
assert_equals(iteratorDesc.value, proto.values, `@@iterator equality with values`);
}
}, `${this.name} interface: async iterable<${member.idlType.map(t => t.idlType).join(", ")}>`);
};
IdlInterface.prototype.test_member_stringifier = function(member)
{
subsetTestByKey(this.name, test, function()
{
if (!this.should_have_interface_object()) {
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 objects “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_js(globalOf(interfacePrototypeObject.toString).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_js(globalOf(interfacePrototypeObject.toString).TypeError, function() {
interfacePrototypeObject.toString.apply({}, []);
}, "calling stringifier with this = {} didn't throw TypeError");
}.bind(this), this.name + " interface: stringifier");
};
IdlInterface.prototype.test_members = function()
{
var unexposed_members = new Set();
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))) {
if (!unexposed_members.has(member.name)) {
unexposed_members.add(member.name);
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":
if (member.async) {
this.test_member_async_iterable(member);
} else {
this.test_member_iterable(member);
}
break;
case "maplike":
this.test_member_maplike(member);
break;
case "setlike":
this.test_member_setlike(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;
if (this.name == "HTMLAllCollection")
{
// Result of [[IsHTMLDDA]] slot
expected_typeof = "undefined";
}
else
{
expected_typeof = "object";
}
if (this.is_callback()) {
assert_equals(exception, null, "Unexpected exception when evaluating object");
assert_equals(typeof obj, expected_typeof, "wrong typeof object");
} else {
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://webidl.spec.whatwg.org/#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.should_have_interface_object()
&& (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 objects 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;
}
var unexposed_properties = new Set();
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)))
{
if (!unexposed_properties.has(member.name))
{
unexposed_properties.add(member.name);
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)
{
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)
{
if (this.name == "Document" && member.name == "all")
{
// Result of [[IsHTMLDDA]] slot
assert_equals(typeof property, "undefined");
}
else
{
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 "' + member + '" 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 description =
this.name + " interface: calling " + member + " on " + desc +
" with too few arguments must throw TypeError";
var a_test = subsetTestByKey(this.name, async_test, description);
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 = [];
// "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
// [LegacyUnforgeable] 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, "[LegacyUnforgeable] 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 [LegacyLenientThis] extended
// attribute, then return undefined.
// "Otherwise, throw a TypeError."
if (!member.has_extended_attribute("LegacyLenientThis")) {
if (member.idlType.generic !== "Promise") {
assert_throws_js(globalOf(desc.get).TypeError, function() {
desc.get.call({});
}.bind(this), "calling getter on wrong object type must throw TypeError");
} else {
pendingPromises.push(
promise_rejects_js(a_test, 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 objects “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 attributes 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("LegacyLenientSetter")
&& !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 [LegacyLenientThis] 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("LegacyLenientThis")) {
assert_throws_js(globalOf(desc.set).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 objects “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("LegacyUnforgeable");
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";
};
IdlInterfaceMember.prototype.toString = function() {
function formatType(type) {
var result;
if (type.generic) {
result = type.generic + "<" + type.idlType.map(formatType).join(", ") + ">";
} else if (type.union) {
result = "(" + type.subtype.map(formatType).join(" or ") + ")";
} else {
result = type.idlType;
}
if (type.nullable) {
result += "?"
}
return result;
}
if (this.type === "operation") {
var args = this.arguments.map(function(m) {
return [
m.optional ? "optional " : "",
formatType(m.idlType),
m.variadic ? "..." : "",
].join("");
}).join(", ");
return this.name + "(" + args + ")";
}
return this.name;
}
/// 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 a_test = subsetTestByKey(
this.name,
async_test,
this.name + ' namespace: operation ' + member);
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_self = function ()
{
/**
* TODO(lukebjerring): Assert:
* - "Note that unlike interfaces or dictionaries, namespaces do not create types."
*/
subsetTestByKey(this.name, test, () => {
assert_true(this.extAttrs.every(o => o.name === "Exposed" || o.name === "SecureContext"),
"Only the [Exposed] and [SecureContext] extended attributes are applicable to namespaces");
assert_true(this.has_extended_attribute("Exposed"),
"Namespaces must be annotated with the [Exposed] extended attribute");
}, `${this.name} namespace: extended attributes`);
const namespaceObject = self[this.name];
subsetTestByKey(this.name, test, () => {
const desc = Object.getOwnPropertyDescriptor(self, this.name);
assert_equals(desc.value, namespaceObject, `wrong value for ${this.name} namespace object`);
assert_true(desc.writable, "namespace object should be writable");
assert_false(desc.enumerable, "namespace object should not be enumerable");
assert_true(desc.configurable, "namespace object should be configurable");
assert_false("get" in desc, "namespace object should not have a getter");
assert_false("set" in desc, "namespace object should not have a setter");
}, `${this.name} namespace: property descriptor`);
subsetTestByKey(this.name, test, () => {
assert_true(Object.isExtensible(namespaceObject));
}, `${this.name} namespace: [[Extensible]] is true`);
subsetTestByKey(this.name, test, () => {
assert_true(namespaceObject instanceof Object);
if (this.name === "console") {
// https://console.spec.whatwg.org/#console-namespace
const namespacePrototype = Object.getPrototypeOf(namespaceObject);
assert_equals(Reflect.ownKeys(namespacePrototype).length, 0);
assert_equals(Object.getPrototypeOf(namespacePrototype), Object.prototype);
} else {
assert_equals(Object.getPrototypeOf(namespaceObject), Object.prototype);
}
}, `${this.name} namespace: [[Prototype]] is Object.prototype`);
subsetTestByKey(this.name, test, () => {
assert_equals(typeof namespaceObject, "object");
}, `${this.name} namespace: typeof is "object"`);
subsetTestByKey(this.name, test, () => {
assert_equals(
Object.getOwnPropertyDescriptor(namespaceObject, "length"),
undefined,
"length property must be undefined"
);
}, `${this.name} namespace: has no length property`);
subsetTestByKey(this.name, test, () => {
assert_equals(
Object.getOwnPropertyDescriptor(namespaceObject, "name"),
undefined,
"name property must be undefined"
);
}, `${this.name} namespace: has no name property`);
};
IdlNamespace.prototype.test = function ()
{
// If the namespace object is not exposed, only test that. Members can't be
// tested either
if (!this.exposed) {
if (!this.untested) {
subsetTestByKey(this.name, test, function() {
assert_false(this.name in self, this.name + " namespace should not exist");
}.bind(this), this.name + " namespace: existence and properties of namespace object");
}
return;
}
if (!this.untested) {
this.test_self();
}
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[]} srcs Spec name(s) for source idl files (fetched from
* /interfaces/{name}.idl).
* @param {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();
var setup_error = null;
const validationIgnored = [
"constructor-member",
"dict-arg-default",
"require-exposed"
];
return Promise.all(
srcs.concat(deps).map(globalThis.fetch_spec))
.then(function(results) {
const astArray = results.map(result =>
WebIDL2.parse(result.idl, { sourceName: result.spec })
);
test(() => {
const validations = WebIDL2.validate(astArray)
.filter(v => !validationIgnored.includes(v.ruleName));
if (validations.length) {
const message = validations.map(v => v.message).join("\n\n");
throw new Error(message);
}
}, "idl_test validation");
for (var i = 0; i < srcs.length; i++) {
idl_array.internal_add_idls(astArray[i]);
}
for (var i = srcs.length; i < srcs.length + deps.length; i++) {
idl_array.internal_add_dependency_idls(astArray[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');
}
globalThis.idl_test = idl_test;
/**
* fetch_spec is a shorthand for a Promise that fetches the spec's content.
* Note: ShadowRealm-specific implementation in testharness-shadowrealm-inner.js
*/
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();
}).then(idl => ({ spec, idl }));
}
// vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker: