Bug 1683281 - Part 6-2: C++ helper function for ObservableArray; r=peterv

Generate helper function for C++ to perform operations on backing objects for
each observable array attribute,
- ElementAt: Get the item at that index. Throw error if fail to get the element.
- ReplaceElementAt: Replace the item at the index, this will also trigger OnDelete
  and OnSet callback. Throw error if fail to replace the element.
- AppendElement: Append one element to the end of the array, this will also trigger
  OnSet callback. Throw error if fail to append the element.
- RemoveLastElement: Remove the element at the last index, this will also trigger
  OnDelete callback. Throw error if fail to remove the element.
- Length: Get the number of the indexd value. Throw error if fail to get the length.

Depends on D113728

Differential Revision: https://phabricator.services.mozilla.com/D113862
This commit is contained in:
Edgar Chen 2022-03-10 22:44:29 +00:00
Родитель d70b01fd77
Коммит 74c8ac1951
6 изменённых файлов: 826 добавлений и 3 удалений

Просмотреть файл

@ -16291,6 +16291,7 @@ class CGDescriptor(CGThing):
cgThings.append(
CGObservableArrayProxyHandlerGenerator(descriptor, m)
)
cgThings.append(CGObservableArrayHelperGenerator(descriptor, m))
if m.getExtendedAttribute("Unscopable"):
assert not m.isStatic()
unscopableNames.append(m.identifier.name)
@ -18172,11 +18173,19 @@ class CGBindingRoot(CGThing):
return any(hasCrossOriginProperty(m) for m in desc.interface.members)
def descriptorHasObservableArrayTypes(desc):
def hasObservableArrayTypes(m):
return m.isAttr() and m.type.isObservableArray()
return any(hasObservableArrayTypes(m) for m in desc.interface.members)
bindingDeclareHeaders["mozilla/dom/RemoteObjectProxy.h"] = any(
descriptorHasCrossOriginProperties(d) for d in descriptors
)
bindingDeclareHeaders["jsapi.h"] = any(
descriptorHasCrossOriginProperties(d) for d in descriptors
descriptorHasCrossOriginProperties(d)
or descriptorHasObservableArrayTypes(d)
for d in descriptors
)
bindingDeclareHeaders["js/TypeDecls.h"] = not bindingDeclareHeaders["jsapi.h"]
bindingDeclareHeaders["js/RootingAPI.h"] = not bindingDeclareHeaders["jsapi.h"]
@ -22000,7 +22009,7 @@ class CGIterableMethodGenerator(CGGeneric):
)
def getObservableArrayBackingObject(descriptor, attr):
def getObservableArrayBackingObject(descriptor, attr, errorReturn="return false;\n"):
"""
Generate code to get/create a JS backing list for an observableArray attribute
from the declaration slot.
@ -22014,7 +22023,7 @@ def getObservableArrayBackingObject(descriptor, attr):
bool created = false;
if (!GetObservableArrayBackingObject(cx, obj, ${slot},
&backingObj, &created, ${namespace}::ObservableArrayProxyHandler::getInstance())) {
return false;
$*{errorReturn}
}
if (created) {
PreserveWrapper(self);
@ -22025,6 +22034,7 @@ def getObservableArrayBackingObject(descriptor, attr):
""",
namespace=toBindingNamespace(MakeNativeName(attr.identifier.name)),
slot=memberReservedSlot(attr, descriptor),
errorReturn=errorReturn,
selfType=descriptor.nativeType,
)
@ -22363,6 +22373,284 @@ class CGObservableArraySetterGenerator(CGGeneric):
)
class CGObservableArrayHelperFunctionGenerator(CGHelperFunctionGenerator):
"""
Generates code to allow C++ to perform operations on backing objects. Gets
a context from the binding wrapper, turns arguments into JS::Values (via
CallbackMember/CGNativeMember argument conversion), then uses
MethodBodyGenerator to generate the body.
"""
class MethodBodyGenerator(CGThing):
"""
Creates methods body for observable array attribute. It is expected that all
methods will be have a maplike/setlike object attached. Unwrapping/wrapping
will be taken care of by the usual method generation machinery in
CGMethodCall/CGPerSignatureCall. Functionality is filled in here instead of
using CGCallGenerator.
"""
def __init__(
self,
descriptor,
attr,
methodName,
helperGenerator,
needsIndexArg,
):
assert attr.isAttr()
assert attr.type.isObservableArray()
CGThing.__init__(self)
self.helperGenerator = helperGenerator
self.cgRoot = CGList([])
self.cgRoot.append(
CGGeneric(
getObservableArrayBackingObject(
descriptor,
attr,
dedent(
"""
aRv.Throw(NS_ERROR_UNEXPECTED);
return%s;
"""
% helperGenerator.getDefaultRetval()
),
)
)
)
# Generates required code for the method. Method descriptions included
# in definitions below. Throw if we don't have a method to fill in what
# we're looking for.
try:
methodGenerator = getattr(self, methodName)
except AttributeError:
raise TypeError(
"Missing observable array method definition '%s'" % methodName
)
# Method generator returns tuple, containing:
#
# - a list of CGThings representing setup code for preparing to call
# the JS API function
# - JS API function name
# - a list of arguments needed for the JS API function we're calling
# - a list of CGThings representing code needed before return.
(setupCode, funcName, arguments, returnCode) = methodGenerator()
# Append the list of setup code CGThings
self.cgRoot.append(CGList(setupCode))
# Create the JS API call
if needsIndexArg:
arguments.insert(0, "aIndex")
self.cgRoot.append(
CGWrapper(
CGGeneric(
fill(
"""
aRv.MightThrowJSException();
if (!${funcName}(${args})) {
aRv.StealExceptionFromJSContext(cx);
return${retval};
}
""",
funcName=funcName,
args=", ".join(["cx", "backingObj"] + arguments),
retval=helperGenerator.getDefaultRetval(),
)
)
)
)
# Append code before return
self.cgRoot.append(CGList(returnCode))
def elementat(self):
setupCode = []
if not self.helperGenerator.needsScopeBody():
setupCode.append(CGGeneric("JS::Rooted<JS::Value> result(cx);\n"))
returnCode = [
CGGeneric(
fill(
"""
if (result.isUndefined()) {
aRv.Throw(NS_ERROR_NOT_AVAILABLE);
return${retval};
}
""",
retval=self.helperGenerator.getDefaultRetval(),
)
)
]
return (setupCode, "JS_GetElement", ["&result"], returnCode)
def replaceelementat(self):
setupCode = [
CGGeneric(
fill(
"""
uint32_t length;
aRv.MightThrowJSException();
if (!JS::GetArrayLength(cx, backingObj, &length)) {
aRv.StealExceptionFromJSContext(cx);
return${retval};
}
if (aIndex > length) {
aRv.ThrowRangeError("Invalid index");
return${retval};
}
""",
retval=self.helperGenerator.getDefaultRetval(),
)
)
]
return (setupCode, "JS_SetElement", ["argv[0]"], [])
def appendelement(self):
setupCode = [
CGGeneric(
fill(
"""
uint32_t length;
aRv.MightThrowJSException();
if (!JS::GetArrayLength(cx, backingObj, &length)) {
aRv.StealExceptionFromJSContext(cx);
return${retval};
}
""",
retval=self.helperGenerator.getDefaultRetval(),
)
)
]
return (setupCode, "JS_SetElement", ["length", "argv[0]"], [])
def removelastelement(self):
setupCode = [
CGGeneric(
fill(
"""
uint32_t length;
aRv.MightThrowJSException();
if (!JS::GetArrayLength(cx, backingObj, &length)) {
aRv.StealExceptionFromJSContext(cx);
return${retval};
}
if (length == 0) {
aRv.Throw(NS_ERROR_NOT_AVAILABLE);
return${retval};
}
""",
retval=self.helperGenerator.getDefaultRetval(),
)
)
]
return (setupCode, "JS::SetArrayLength", ["length - 1"], [])
def length(self):
return (
[CGGeneric("uint32_t retVal;\n")],
"JS::GetArrayLength",
["&retVal"],
[],
)
def define(self):
return self.cgRoot.define()
def __init__(
self,
descriptor,
attr,
name,
returnType=BuiltinTypes[IDLBuiltinType.Types.void],
needsResultConversion=True,
needsIndexArg=False,
needsValueArg=False,
):
assert attr.isAttr()
assert attr.type.isObservableArray()
self.attr = attr
self.needsIndexArg = needsIndexArg
args = []
if needsValueArg:
args.append(FakeArgument(attr.type.inner, "aValue"))
CGHelperFunctionGenerator.__init__(
self,
descriptor,
name,
args,
returnType,
needsResultConversion,
)
def getArgs(self, returnType, argList):
if self.needsIndexArg:
argList = [
FakeArgument(BuiltinTypes[IDLBuiltinType.Types.unsigned_long], "aIndex")
] + argList
return CGHelperFunctionGenerator.getArgs(self, returnType, argList)
def getCall(self):
return CGObservableArrayHelperFunctionGenerator.MethodBodyGenerator(
self.descriptorProvider,
self.attr,
self.name.lower(),
self,
self.needsIndexArg,
).define()
class CGObservableArrayHelperGenerator(CGNamespace):
"""
Declares and defines convenience methods for accessing backing object for
observable array type. Generates function signatures, un/packs
backing objects from slot, etc.
"""
def __init__(self, descriptor, attr):
assert attr.isAttr()
assert attr.type.isObservableArray()
namespace = "%sHelpers" % MakeNativeName(attr.identifier.name)
helpers = [
CGObservableArrayHelperFunctionGenerator(
descriptor,
attr,
"ElementAt",
returnType=attr.type.inner,
needsIndexArg=True,
),
CGObservableArrayHelperFunctionGenerator(
descriptor,
attr,
"ReplaceElementAt",
needsIndexArg=True,
needsValueArg=True,
),
CGObservableArrayHelperFunctionGenerator(
descriptor,
attr,
"AppendElement",
needsValueArg=True,
),
CGObservableArrayHelperFunctionGenerator(
descriptor,
attr,
"RemoveLastElement",
),
CGObservableArrayHelperFunctionGenerator(
descriptor,
attr,
"Length",
returnType=BuiltinTypes[IDLBuiltinType.Types.unsigned_long],
needsResultConversion=False,
),
]
CGNamespace.__init__(self, namespace, CGList(helpers, "\n"))
class GlobalGenRoots:
"""
Roots for global codegen.

Просмотреть файл

@ -131,4 +131,95 @@ void TestInterfaceObservableArray::OnDeleteObservableArrayInterface(
}
}
bool TestInterfaceObservableArray::BooleanElementAtInternal(uint32_t aIndex,
ErrorResult& aRv) {
return TestInterfaceObservableArray_Binding::ObservableArrayBooleanHelpers::
ElementAt(this, aIndex, aRv);
}
void TestInterfaceObservableArray::ObjectElementAtInternal(
JSContext* aCx, uint32_t aIndex, JS::MutableHandle<JSObject*> aValue,
ErrorResult& aRv) {
TestInterfaceObservableArray_Binding::ObservableArrayObjectHelpers::ElementAt(
this, aCx, aIndex, aValue, aRv);
}
already_AddRefed<TestInterfaceObservableArray>
TestInterfaceObservableArray::InterfaceElementAtInternal(uint32_t aIndex,
ErrorResult& aRv) {
return TestInterfaceObservableArray_Binding::ObservableArrayInterfaceHelpers::
ElementAt(this, aIndex, aRv);
}
void TestInterfaceObservableArray::BooleanReplaceElementAtInternal(
uint32_t aIndex, bool aValue, ErrorResult& aRv) {
TestInterfaceObservableArray_Binding::ObservableArrayBooleanHelpers::
ReplaceElementAt(this, aIndex, aValue, aRv);
}
void TestInterfaceObservableArray::ObjectReplaceElementAtInternal(
JSContext* aCx, uint32_t aIndex, JS::Handle<JSObject*> aValue,
ErrorResult& aRv) {
TestInterfaceObservableArray_Binding::ObservableArrayObjectHelpers::
ReplaceElementAt(this, aIndex, aValue, aRv);
}
void TestInterfaceObservableArray::InterfaceReplaceElementAtInternal(
uint32_t aIndex, TestInterfaceObservableArray& aValue, ErrorResult& aRv) {
TestInterfaceObservableArray_Binding::ObservableArrayInterfaceHelpers::
ReplaceElementAt(this, aIndex, aValue, aRv);
}
void TestInterfaceObservableArray::BooleanAppendElementInternal(
bool aValue, ErrorResult& aRv) {
TestInterfaceObservableArray_Binding::ObservableArrayBooleanHelpers::
AppendElement(this, aValue, aRv);
}
void TestInterfaceObservableArray::ObjectAppendElementInternal(
JSContext* aCx, JS::Handle<JSObject*> aValue, ErrorResult& aRv) {
TestInterfaceObservableArray_Binding::ObservableArrayObjectHelpers::
AppendElement(this, aValue, aRv);
}
void TestInterfaceObservableArray::InterfaceAppendElementInternal(
TestInterfaceObservableArray& aValue, ErrorResult& aRv) {
TestInterfaceObservableArray_Binding::ObservableArrayInterfaceHelpers::
AppendElement(this, aValue, aRv);
}
void TestInterfaceObservableArray::BooleanRemoveLastElementInternal(
ErrorResult& aRv) {
TestInterfaceObservableArray_Binding::ObservableArrayBooleanHelpers::
RemoveLastElement(this, aRv);
}
void TestInterfaceObservableArray::ObjectRemoveLastElementInternal(
ErrorResult& aRv) {
TestInterfaceObservableArray_Binding::ObservableArrayObjectHelpers::
RemoveLastElement(this, aRv);
}
void TestInterfaceObservableArray::InterfaceRemoveLastElementInternal(
ErrorResult& aRv) {
TestInterfaceObservableArray_Binding::ObservableArrayInterfaceHelpers::
RemoveLastElement(this, aRv);
}
uint32_t TestInterfaceObservableArray::BooleanLengthInternal(ErrorResult& aRv) {
return TestInterfaceObservableArray_Binding::ObservableArrayBooleanHelpers::
Length(this, aRv);
}
uint32_t TestInterfaceObservableArray::ObjectLengthInternal(ErrorResult& aRv) {
return TestInterfaceObservableArray_Binding::ObservableArrayObjectHelpers::
Length(this, aRv);
}
uint32_t TestInterfaceObservableArray::InterfaceLengthInternal(
ErrorResult& aRv) {
return TestInterfaceObservableArray_Binding::ObservableArrayInterfaceHelpers::
Length(this, aRv);
}
} // namespace mozilla::dom

Просмотреть файл

@ -65,6 +65,36 @@ class TestInterfaceObservableArray final : public nsISupports,
void OnDeleteObservableArrayInterface(TestInterfaceObservableArray* aValue,
uint32_t aIndex, ErrorResult& aRv);
bool BooleanElementAtInternal(uint32_t aIndex, ErrorResult& aRv);
void ObjectElementAtInternal(JSContext* aCx, uint32_t aIndex,
JS::MutableHandle<JSObject*> aValue,
ErrorResult& aRv);
already_AddRefed<TestInterfaceObservableArray> InterfaceElementAtInternal(
uint32_t aIndex, ErrorResult& aRv);
void BooleanReplaceElementAtInternal(uint32_t aIndex, bool aValue,
ErrorResult& aRv);
void ObjectReplaceElementAtInternal(JSContext* aCx, uint32_t aIndex,
JS::Handle<JSObject*> aValue,
ErrorResult& aRv);
void InterfaceReplaceElementAtInternal(uint32_t aIndex,
TestInterfaceObservableArray& aValue,
ErrorResult& aRv);
void BooleanAppendElementInternal(bool aValue, ErrorResult& aRv);
void ObjectAppendElementInternal(JSContext* aCx, JS::Handle<JSObject*> aValue,
ErrorResult& aRv);
void InterfaceAppendElementInternal(TestInterfaceObservableArray& aValue,
ErrorResult& aRv);
void BooleanRemoveLastElementInternal(ErrorResult& aRv);
void ObjectRemoveLastElementInternal(ErrorResult& aRv);
void InterfaceRemoveLastElementInternal(ErrorResult& aRv);
uint32_t BooleanLengthInternal(ErrorResult& aRv);
uint32_t ObjectLengthInternal(ErrorResult& aRv);
uint32_t InterfaceLengthInternal(ErrorResult& aRv);
private:
explicit TestInterfaceObservableArray(
nsPIDOMWindowInner* aParent, const ObservableArrayCallbacks& aCallbacks);

Просмотреть файл

@ -96,4 +96,6 @@ skip-if = (debug == false || bits == 32) # Large ArrayBuffers are only supported
skip-if = debug == false
[test_observablearray_proxyhandler.html]
skip-if = debug == false
[test_observablearray_helper.html]
skip-if = debug == false
[test_large_imageData.html]

Просмотреть файл

@ -0,0 +1,376 @@
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE HTML>
<html>
<head>
<title>Test Helpers of Observable Array Type</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script>
/* global TestInterfaceObservableArray */
add_task(async function init() {
await SpecialPowers.pushPrefEnv({set: [["dom.expose_test_interfaces", true]]});
});
add_task(function testObservableArray_helper_elementAt() {
let m = new TestInterfaceObservableArray();
[
// [values, property, helper]
[[true, false], "observableArrayBoolean", m.booleanElementAtInternal.bind(m)],
[[new TestInterfaceObservableArray(), new TestInterfaceObservableArray()],
"observableArrayInterface", m.interfaceElementAtInternal.bind(m)],
[[{property: "test"}, {property: 2}], "observableArrayObject",
m.objectElementAtInternal.bind(m)],
].forEach(function([values, property, helper]) {
m[property] = values;
let t = m[property];
ok(Array.isArray(t), "observable array should be an array type");
is(t.length, values.length, "length of observable array");
for (let i = 0; i < values.length; i++) {
isDeeply(values[i], helper(i), `check index ${i}`);
}
SimpleTest.doesThrow(() => {
helper(values.length);
}, `getting element outside the range should throw`);
});
});
add_task(function testObservableArray_helper_replaceElementAt() {
let setCallbackCount = 0;
let deleteCallbackCount = 0;
let setCallbackTests = null;
let deleteCallbackTests = null;
let m = new TestInterfaceObservableArray({
setBooleanCallback(value, index) {
setCallbackCount++;
if (typeof setCallbackTests === 'function') {
setCallbackTests(value, index);
}
},
deleteBooleanCallback(value, index) {
deleteCallbackCount++;
if (typeof deleteCallbackTests === 'function') {
deleteCallbackTests(value, index);
}
},
});
let b = m.observableArrayBoolean;
ok(Array.isArray(b), "observable array should be an array type");
is(b.length, 0, "length of observable array should be 0");
[
// [index, value, shouldThrow]
[b.length + 1, false, true],
[b.length, false, false],
[b.length + 1, false, false],
[b.length + 1, true, false],
].forEach(function([index, value, shouldThrow]) {
// Initialize
let oldValue = b[index];
let oldLen = b.length;
setCallbackCount = 0;
deleteCallbackCount = 0;
setCallbackTests = function(_value, _index) {
info(`set callback for ${_index}`);
is(_value, value, "setCallbackTests: test value argument");
is(_index, index, "setCallbackTests: test index argument");
};
deleteCallbackTests = function(_value, _index) {
info(`delete callback for ${_index}`);
is(_value, oldValue, "deleteCallbackTests: test value argument");
is(_index, index, "deleteCallbackTests: test index argument");
};
// Test
info(`setting value of property ${index} to ${value}`);
try {
m.booleanReplaceElementAtInternal(index, value);
ok(!shouldThrow, `setting value should not throw`);
} catch(e) {
ok(shouldThrow, `setting value throws ${e}`);
}
is(setCallbackCount, shouldThrow ? 0 : 1, "setCallback count");
is(deleteCallbackCount, (oldLen > index) ? 1 : 0, "deleteCallback count");
is(b[index], shouldThrow ? oldValue : value, `property value`);
is(b.length, shouldThrow ? oldLen : Math.max(oldLen, index + 1), `length of observable array`);
});
});
add_task(function testObservableArray_helper_replaceElementAt_callback_throw() {
let setCallbackCount = 0;
let deleteCallbackCount = 0;
let m = new TestInterfaceObservableArray({
setBooleanCallback(value, index) {
setCallbackCount++;
if (value) {
throw new Error("setBooleanCallback");
}
},
deleteBooleanCallback(value, index) {
deleteCallbackCount++;
if (index < 2) {
throw new Error("deleteBooleanCallback");
}
},
});
m.observableArrayBoolean = [false, false, false];
let b = m.observableArrayBoolean;
ok(Array.isArray(b), "observable array should be an array type");
is(b.length, 3, "length of observable array should be 3");
[
// [index, value, shouldThrow]
[b.length, true, true],
[b.length, false, false],
[b.length, true, true],
[0, false, true],
[0, true, true]
].forEach(function([index, value, shouldThrow]) {
// Initialize
let oldValue = b[index];
let oldLen = b.length;
setCallbackCount = 0;
deleteCallbackCount = 0;
// Test
info(`setting value of property ${index} to ${value}`);
try {
m.booleanReplaceElementAtInternal(index, value);
ok(!shouldThrow, `setting value should not throw`);
} catch(e) {
ok(shouldThrow, `setting value throws ${e}`);
}
is(setCallbackCount, (shouldThrow && index < 2) ? 0 : 1, "setCallback count");
is(deleteCallbackCount, (oldLen > index) ? 1 : 0, "deleteCallback count");
is(b[index], shouldThrow ? oldValue : value, "property value");
is(b.length, shouldThrow ? oldLen : Math.max(oldLen, index + 1), `length of observable array`);
});
});
add_task(function testObservableArray_helper_appendElement() {
let setCallbackCount = 0;
let deleteCallbackCount = 0;
let setCallbackTests = null;
let m = new TestInterfaceObservableArray({
setBooleanCallback(value, index) {
setCallbackCount++;
if (typeof setCallbackTests === 'function') {
setCallbackTests(value, index);
}
},
deleteBooleanCallback(value, index) {
deleteCallbackCount++;
},
});
let b = m.observableArrayBoolean;
ok(Array.isArray(b), "observable array should be an array type");
is(b.length, 0, "length of observable array should be 0");
[true, false, true, false].forEach(function(value) {
// Initialize
let oldLen = b.length;
let index = oldLen;
setCallbackCount = 0;
deleteCallbackCount = 0;
setCallbackTests = function(_value, _index) {
info(`set callback for ${_index}`);
is(_value, value, "setCallbackTests: test value argument");
is(_index, index, "setCallbackTests: test index argument");
};
// Test
info(`append ${value}`);
try {
m.booleanAppendElementInternal(value);
ok(true, `appending value should not throw`);
} catch(e) {
ok(false, `appending value throws ${e}`);
}
is(setCallbackCount, 1, "setCallback should be called");
is(deleteCallbackCount, 0, "deleteCallback should not be called");
is(b[index], value, `property value`);
is(b.length, oldLen + 1, `length of observable array`);
});
});
add_task(function testObservableArray_helper_appendElement_callback_throw() {
let setCallbackCount = 0;
let deleteCallbackCount = 0;
let m = new TestInterfaceObservableArray({
setBooleanCallback(value, index) {
setCallbackCount++;
if (value) {
throw new Error("setBooleanCallback");
}
},
deleteBooleanCallback(value, index) {
deleteCallbackCount++;
},
});
m.observableArrayBoolean = [false, false, false];
let b = m.observableArrayBoolean;
ok(Array.isArray(b), "observable array should be an array type");
is(b.length, 3, "length of observable array should be 3");
[true, false, true, false].forEach(function(value) {
// Initialize
let oldLen = b.length;
let index = oldLen;
let oldValue = b[index];
let shouldThrow = value;
setCallbackCount = 0;
deleteCallbackCount = 0;
// Test
info(`append ${value}`);
try {
m.booleanAppendElementInternal(value);
ok(!shouldThrow, `appending value should not throw`);
} catch(e) {
ok(shouldThrow, `appending value throws ${e}`);
}
is(setCallbackCount, 1, "setCallback should be called");
is(deleteCallbackCount, 0, "deleteCallback should not be called");
is(b[index], shouldThrow ? oldValue : value, "property value");
is(b.length, shouldThrow ? oldLen : oldLen + 1, `length of observable array`);
});
});
add_task(function testObservableArray_helper_removeLastElement() {
let setCallbackCount = 0;
let deleteCallbackCount = 0;
let deleteCallbackTests = null;
let m = new TestInterfaceObservableArray({
setBooleanCallback(value, index) {
setCallbackCount++;
},
deleteBooleanCallback(value, index) {
deleteCallbackCount++;
if (typeof deleteCallbackTests === 'function') {
deleteCallbackTests(value, index);
}
},
});
m.observableArrayBoolean = [true, false, true, false];
let b = m.observableArrayBoolean;
ok(Array.isArray(b), "observable array should be an array type");
is(b.length, 4, "length of observable array should be 4");
let oldValues = b.slice();
while (oldValues.length) {
// Initialize
let oldValue = oldValues.pop();
let index = oldValues.length;
setCallbackCount = 0;
deleteCallbackCount = 0;
deleteCallbackTests = function(_value, _index) {
info(`delete callback for ${_index}`);
is(_value, oldValue, "deleteCallbackTests: test value argument");
is(_index, index, "deleteCallbackTests: test index argument");
};
// Test
info(`remove last element`);
try {
m.booleanRemoveLastElementInternal();
ok(true, `removing last element should not throw`);
} catch(e) {
ok(false, `removing last element throws ${e}`);
}
is(setCallbackCount, 0, "setCallback should not be called");
is(deleteCallbackCount, 1, "deleteCallback should be called");
isDeeply(b, oldValues, `property value`);
is(b.length, oldValues.length, `length of observable array`);
}
// test when array is empty
setCallbackCount = 0;
deleteCallbackCount = 0;
SimpleTest.doesThrow(() => {
m.booleanRemoveLastElementInternal();
}, `removing last element should throw when array is empty`);
is(setCallbackCount, 0, "setCallback should not be called");
is(deleteCallbackCount, 0, "deleteCallback should not be called");
is(b.length, 0, `length of observable array`);
});
add_task(function testObservableArray_helper_removeLastElement_callback_throw() {
let setCallbackCount = 0;
let deleteCallbackCount = 0;
let m = new TestInterfaceObservableArray({
setBooleanCallback(value, index) {
setCallbackCount++;
},
deleteBooleanCallback(value, index) {
deleteCallbackCount++;
if (value) {
throw new Error("deleteBooleanCallback");
}
},
});
m.observableArrayBoolean = [false, true, false, false];
let b = m.observableArrayBoolean;
ok(Array.isArray(b), "observable array should be an array type");
is(b.length, 4, "length of observable array should be 4");
let shouldThrow = false;
while (!shouldThrow && b.length) {
// Initialize
let oldValues = b.slice();
let oldLen = b.length;
shouldThrow = oldValues[oldLen - 1];
setCallbackCount = 0;
deleteCallbackCount = 0;
// Test
info(`remove last element`);
try {
m.booleanRemoveLastElementInternal();
ok(!shouldThrow, `removing last element should not throw`);
} catch(e) {
ok(shouldThrow, `removing last element throws ${e}`);
}
is(setCallbackCount, 0, "setCallback should not be called");
is(deleteCallbackCount, 1, "deleteCallback should be called");
isDeeply(b, shouldThrow ? oldValues : oldValues.slice(0, oldLen - 1), `property value`);
is(b.length, shouldThrow ? oldLen : oldLen - 1, `length of observable array`);
}
});
add_task(function testObservableArray_helper_length() {
let m = new TestInterfaceObservableArray();
let b = m.observableArrayBoolean;
ok(Array.isArray(b), "observable array should be an array type");
is(b.length, 0, "length of observable array");
[
[false, true, false, true],
[true, false],
[false, true, false],
].forEach(function(values) {
m.observableArrayBoolean = values;
is(b.length, m.booleanLengthInternal(), "length helper function");
});
});
</script>
</body>
</html>

Просмотреть файл

@ -27,4 +27,40 @@ interface TestInterfaceObservableArray {
attribute ObservableArray<boolean> observableArrayBoolean;
attribute ObservableArray<object> observableArrayObject;
attribute ObservableArray<TestInterfaceObservableArray> observableArrayInterface;
// Tests for C++ helper function
[Throws]
boolean booleanElementAtInternal(unsigned long index);
[Throws]
TestInterfaceObservableArray interfaceElementAtInternal(unsigned long index);
[Throws]
object objectElementAtInternal(unsigned long index);
[Throws]
void booleanReplaceElementAtInternal(unsigned long index, boolean value);
[Throws]
void interfaceReplaceElementAtInternal(unsigned long index, TestInterfaceObservableArray value);
[Throws]
void objectReplaceElementAtInternal(unsigned long index, object value);
[Throws]
void booleanAppendElementInternal(boolean value);
[Throws]
void interfaceAppendElementInternal(TestInterfaceObservableArray value);
[Throws]
void objectAppendElementInternal(object value);
[Throws]
void booleanRemoveLastElementInternal();
[Throws]
void interfaceRemoveLastElementInternal();
[Throws]
void objectRemoveLastElementInternal();
[Throws]
unsigned long booleanLengthInternal();
[Throws]
unsigned long interfaceLengthInternal();
[Throws]
unsigned long objectLengthInternal();
};