11import XCTest
22import JavaScriptKit
33
4+ @JSFunction ( from: . global) func gc( ) throws ( JSException) -> Void
5+
46@JSClass struct IdentityModeTestImports {
57 @JSFunction static func runJsIdentityModeTests( ) throws ( JSException)
68}
@@ -9,6 +11,44 @@ final class IdentityModeTests: XCTestCase {
911 func testRunJsIdentityModeTests( ) throws {
1012 try IdentityModeTestImports . runJsIdentityModeTests ( )
1113 }
14+
15+ /// Verifies that identity-cached wrappers are properly reclaimed by GC.
16+ ///
17+ /// Creates an identity-mode object, crosses it multiple times (filling the
18+ /// identity cache), drops all references, triggers GC + event loop ticks,
19+ /// and verifies the Swift object is deallocated. This proves that the
20+ /// WeakRef-based identity cache does not prevent garbage collection.
21+ func testIdentityCachedWrapperIsReclaimedByGC( ) async throws {
22+ RetainLeakSubject . deinits = 0
23+
24+ // Create object and cross it multiple times to fill identity cache
25+ _retainLeakSubject = RetainLeakSubject ( tag: 99 )
26+ weak var weakSubject = _retainLeakSubject
27+
28+ // Cross to JS 5 times (populates identity cache with WeakRef)
29+ for _ in 0 ..< 5 {
30+ _ = getRetainLeakSubject ( )
31+ }
32+
33+ // Drop Swift-side strong reference
34+ _retainLeakSubject = nil
35+
36+ // JS wrapper should still be alive via the identity cache's WeakRef,
37+ // but WeakRef doesn't prevent GC. Trigger GC + event loop ticks to
38+ // let FinalizationRegistry fire and call deinit.
39+ for _ in 0 ..< 100 {
40+ try gc ( )
41+ try await Task . sleep ( for: . milliseconds( 0 ) )
42+ if weakSubject == nil {
43+ break
44+ }
45+ }
46+
47+ // The identity-cached wrapper should have been collected,
48+ // FinalizationRegistry should have fired, deinit should have run.
49+ XCTAssertNil ( weakSubject, " Identity-cached object should be deallocated after GC " )
50+ XCTAssertEqual ( RetainLeakSubject . deinits, 1 , " Deinit should fire exactly once " )
51+ }
1252}
1353
1454@JS class IdentityTestSubject {
0 commit comments