Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,18 @@ public class JSObject: Equatable, ExpressibleByDictionaryLiteral {
public subscript(_ name: String) -> JSValue {
get {
assertOnOwnerThread(hint: "reading '\(name)' property")
#if Tracing
let traceEnd = JSTracingHooks.beginJSCall(.propertyGet(receiver: self, propertyName: name))
defer { traceEnd?() }
#endif
return getJSValue(this: self, name: JSString(name))
}
set {
assertOnOwnerThread(hint: "writing '\(name)' property")
#if Tracing
let traceEnd = JSTracingHooks.beginJSCall(.propertySet(receiver: self, propertyName: name, value: newValue))
defer { traceEnd?() }
#endif
setJSValue(this: self, name: JSString(name), value: newValue)
}
}
Expand All @@ -167,10 +175,20 @@ public class JSObject: Equatable, ExpressibleByDictionaryLiteral {
public subscript(_ name: JSString) -> JSValue {
get {
assertOnOwnerThread(hint: "reading '<<JSString>>' property")
#if Tracing
let traceEnd = JSTracingHooks.beginJSCall(.propertyGet(receiver: self, propertyName: String(name)))
defer { traceEnd?() }
#endif
return getJSValue(this: self, name: name)
}
set {
assertOnOwnerThread(hint: "writing '<<JSString>>' property")
#if Tracing
let traceEnd = JSTracingHooks.beginJSCall(
.propertySet(receiver: self, propertyName: String(name), value: newValue)
)
defer { traceEnd?() }
#endif
setJSValue(this: self, name: name, value: newValue)
}
}
Expand All @@ -181,10 +199,20 @@ public class JSObject: Equatable, ExpressibleByDictionaryLiteral {
public subscript(_ index: Int) -> JSValue {
get {
assertOnOwnerThread(hint: "reading '\(index)' property")
#if Tracing
let traceEnd = JSTracingHooks.beginJSCall(.propertyGet(receiver: self, propertyName: String(index)))
defer { traceEnd?() }
#endif
return getJSValue(this: self, index: Int32(index))
}
set {
assertOnOwnerThread(hint: "writing '\(index)' property")
#if Tracing
let traceEnd = JSTracingHooks.beginJSCall(
.propertySet(receiver: self, propertyName: String(index), value: newValue)
)
defer { traceEnd?() }
#endif
setJSValue(this: self, index: Int32(index), value: newValue)
}
}
Expand All @@ -195,10 +223,20 @@ public class JSObject: Equatable, ExpressibleByDictionaryLiteral {
public subscript(_ name: JSSymbol) -> JSValue {
get {
assertOnOwnerThread(hint: "reading '<<JSSymbol>>' property")
#if Tracing
let traceEnd = JSTracingHooks.beginJSCall(.propertyGet(receiver: self, propertyName: "<<JSSymbol>>"))
defer { traceEnd?() }
#endif
return getJSValue(this: self, symbol: name)
}
set {
assertOnOwnerThread(hint: "writing '<<JSSymbol>>' property")
#if Tracing
let traceEnd = JSTracingHooks.beginJSCall(
.propertySet(receiver: self, propertyName: "<<JSSymbol>>", value: newValue)
)
defer { traceEnd?() }
#endif
setJSValue(this: self, symbol: name, value: newValue)
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/JavaScriptKit/JSTracing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public struct JSTracing: Sendable {
public enum JSCallInfo {
case function(function: JSObject, arguments: [JSValue])
case method(receiver: JSObject, methodName: String?, arguments: [JSValue])
case propertyGet(receiver: JSObject, propertyName: String)
case propertySet(receiver: JSObject, propertyName: String, value: JSValue)
}

/// Register a hook for Swift to JavaScript calls.
Expand Down
60 changes: 57 additions & 3 deletions Tests/JavaScriptKitTests/JSTracingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,69 @@ final class JSTracingTests: XCTestCase {
let prop5 = try XCTUnwrap(globalObject1.prop_5.object)
_ = prop5.func6!(true, 1, 2)

XCTAssertEqual(startInfo.count, 1)
guard case let .method(receiver, methodName, arguments) = startInfo.first else {
let methodEvents = startInfo.filter {
if case .method = $0 { return true }
return false
}
XCTAssertEqual(methodEvents.count, 1)
guard case let .method(receiver, methodName, arguments) = methodEvents.first else {
XCTFail("Expected method info")
return
}
XCTAssertEqual(receiver.id, prop5.id)
XCTAssertEqual(methodName, "func6")
XCTAssertEqual(arguments, [.boolean(true), .number(1), .number(2)])
XCTAssertEqual(ended, 1)
XCTAssertEqual(ended, startInfo.count)
}

func testJSCallHookReportsPropertyAccess() throws {
var startInfo: [JSTracing.JSCallInfo] = []
var ended = 0
let remove = JSTracing.default.addJSCallHook { info in
startInfo.append(info)
return { ended += 1 }
}
defer { remove() }

let obj = JSObject()
obj.foo = .number(42)

// Reset after setup so we only capture the reads/writes below.
startInfo.removeAll()
ended = 0

// Read a property (triggers propertyGet)
let _: JSValue = obj.foo

// Write a property (triggers propertySet)
obj.foo = .number(999)

let propEvents = startInfo.filter {
switch $0 {
case .propertyGet(_, let name) where name == "foo": return true
case .propertySet(_, let name, _) where name == "foo": return true
default: return false
}
}

XCTAssertEqual(propEvents.count, 2)

guard case .propertyGet(let getReceiver, let getName) = propEvents[0] else {
XCTFail("Expected propertyGet info")
return
}
XCTAssertEqual(getReceiver.id, obj.id)
XCTAssertEqual(getName, "foo")

guard case .propertySet(let setReceiver, let setName, let setValue) = propEvents[1] else {
XCTFail("Expected propertySet info")
return
}
XCTAssertEqual(setReceiver.id, obj.id)
XCTAssertEqual(setName, "foo")
XCTAssertEqual(setValue, .number(999))

XCTAssertEqual(ended, startInfo.count)
}

func testJSClosureCallHookReportsMetadata() throws {
Expand Down