diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index 22fc89fe6..7d041a631 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -86,22 +86,36 @@ try test("Array Iterator") { let globalObject1 = getJSValue(this: .global, name: "globalObject1") let globalObject1Ref = try expectObject(globalObject1) let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") - let array = try expectArray(prop_4) + let array1 = try expectArray(prop_4) let expectedProp_4: [JSValue] = [ .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5), ] - try expectEqual(Array(array), expectedProp_4) + try expectEqual(Array(array1), expectedProp_4) + + // Ensure that iterator skips empty hole as JavaScript does. + let prop_8 = getJSValue(this: globalObject1Ref, name: "prop_8") + let array2 = try expectArray(prop_8) + let expectedProp_8: [JSValue] = [0, 2, 3, 6] + try expectEqual(Array(array2), expectedProp_8) } try test("Array RandomAccessCollection") { let globalObject1 = getJSValue(this: .global, name: "globalObject1") let globalObject1Ref = try expectObject(globalObject1) let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") - let array = try expectArray(prop_4) + let array1 = try expectArray(prop_4) let expectedProp_4: [JSValue] = [ .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5), ] - try expectEqual([array[0], array[1], array[2], array[3], array[4], array[5]], expectedProp_4) + try expectEqual([array1[0], array1[1], array1[2], array1[3], array1[4], array1[5]], expectedProp_4) + + // Ensure that subscript can access empty hole + let prop_8 = getJSValue(this: globalObject1Ref, name: "prop_8") + let array2 = try expectArray(prop_8) + let expectedProp_8: [JSValue] = [ + 0, .undefined, 2, 3, .undefined, .undefined, 6 + ] + try expectEqual([array2[0], array2[1], array2[2], array2[3], array2[4], array2[5], array2[6]], expectedProp_8) } try test("Value Decoder") { diff --git a/IntegrationTests/bin/primary-tests.js b/IntegrationTests/bin/primary-tests.js index 9617178ca..9bd87c93d 100644 --- a/IntegrationTests/bin/primary-tests.js +++ b/IntegrationTests/bin/primary-tests.js @@ -23,6 +23,7 @@ global.globalObject1 = { } }, "prop_7": 3.14, + "prop_8": [0, , 2, 3, , , 6], } global.Animal = function(name, age, isCat) { diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift index 85c811a39..4a8a6aecc 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift @@ -28,11 +28,15 @@ extension JSArray: RandomAccessCollection { } public func next() -> Element? { - defer { index += 1 } - guard index < Int(ref.length.number!) else { + let currentIndex = index + guard currentIndex < Int(ref.length.number!) else { return nil } - let value = ref[index] + index += 1 + guard ref.hasOwnProperty!(currentIndex).boolean! else { + return next() + } + let value = ref[currentIndex] return value } } @@ -43,7 +47,36 @@ extension JSArray: RandomAccessCollection { public var startIndex: Int { 0 } - public var endIndex: Int { ref.length.number.map(Int.init) ?? 0 } + public var endIndex: Int { length } + + /// The number of elements in that array including empty hole. + /// Note that `length` respects JavaScript's `Array.prototype.length` + /// + /// e.g. + /// ```javascript + /// const array = [1, , 3]; + /// ``` + /// ```swift + /// let array: JSArray = ... + /// array.length // 3 + /// array.count // 2 + /// ``` + public var length: Int { + return Int(ref.length.number!) + } + + /// The number of elements in that array **not** including empty hole. + /// Note that `count` syncs with the number that `Iterator` can iterate. + /// See also: `JSArray.length` + public var count: Int { + return getObjectValuesLength(ref) + } +} + +private let alwaysTrue = JSClosure { _ in .boolean(true) } +private func getObjectValuesLength(_ object: JSObject) -> Int { + let values = object.filter!(alwaysTrue).object! + return Int(values.length.number!) } extension JSValue {