diff --git a/Tests/RevenueCatUITests/BaseSnapshotTest.swift b/Tests/RevenueCatUITests/BaseSnapshotTest.swift index 45f04c7757..c2fb502b41 100644 --- a/Tests/RevenueCatUITests/BaseSnapshotTest.swift +++ b/Tests/RevenueCatUITests/BaseSnapshotTest.swift @@ -87,7 +87,9 @@ extension BaseSnapshotTest { extension View { /// Adds the receiver to a view hierarchy to be able to test lifetime logic. - func addToHierarchy() throws { + /// - Returns: dispose block that removes the view from the hierarchy. + @discardableResult + func addToHierarchy() throws -> () -> Void { UIView.setAnimationsEnabled(false) let controller = UIHostingController( @@ -100,16 +102,25 @@ extension View { window.isHidden = false window.rootViewController = controller window.frame.size = BaseSnapshotTest.fullScreenSize - window.makeKeyAndVisible() window.addSubview(controller.view) - controller.didMove(toParent: controller) window.setNeedsLayout() window.layoutIfNeeded() controller.beginAppearanceTransition(true, animated: false) controller.endAppearanceTransition() + + window.makeKeyAndVisible() + + return { + controller.beginAppearanceTransition(false, animated: false) + controller.view.removeFromSuperview() + controller.removeFromParent() + controller.endAppearanceTransition() + window.rootViewController = nil + window.resignKey() + } } } diff --git a/Tests/RevenueCatUITests/PaywallViewEventsTests.swift b/Tests/RevenueCatUITests/PaywallViewEventsTests.swift index fc732f34da..ef626478af 100644 --- a/Tests/RevenueCatUITests/PaywallViewEventsTests.swift +++ b/Tests/RevenueCatUITests/PaywallViewEventsTests.swift @@ -29,13 +29,12 @@ class PaywallViewEventsTests: TestCase { private let mode: PaywallViewMode = .random private let scheme: ColorScheme = Bool.random() ? .dark : .light - private var impressionEventExpectation: XCTestExpectation! private var closeEventExpectation: XCTestExpectation! - private var cancelEventExpectation: XCTestExpectation! - override func setUp() { super.setUp() + self.continueAfterFailure = false + self.handler = .cancelling() .map { _ in @@ -43,27 +42,21 @@ class PaywallViewEventsTests: TestCase { await self?.track(event) } } - - self.impressionEventExpectation = .init(description: "Impression event") self.closeEventExpectation = .init(description: "Close event") - self.cancelEventExpectation = .init(description: "Cancel event") } - func testPaywallImpressionEvent() throws { - try self.createView() - .addToHierarchy() + func testPaywallImpressionEvent() async throws { + try await self.runDuringViewLifetime {} - expect(self.events).toEventually(containElementSatisfying { $0.eventType == .impression }) + expect(self.events).to(containElementSatisfying { $0.eventType == .impression }) let event = try XCTUnwrap(self.events.first { $0.eventType == .impression }) self.verifyEventData(event.data) } func testPaywallCloseEvent() async throws { - try self.createView() - .addToHierarchy() - - await self.fulfillment(of: [self.closeEventExpectation], timeout: 1) + try await self.runDuringViewLifetime {} + await self.waitForCloseEvent() expect(self.events).to(haveCount(2)) expect(self.events).to(containElementSatisfying { $0.eventType == .close }) @@ -73,10 +66,8 @@ class PaywallViewEventsTests: TestCase { } func testCloseEventHasSameSessionID() async throws { - try self.createView() - .addToHierarchy() - - await self.fulfillment(of: [self.closeEventExpectation], timeout: 1) + try await self.runDuringViewLifetime {} + await self.waitForCloseEvent() expect(self.events).to(haveCount(2)) expect(self.events.map(\.eventType)) == [.impression, .close] @@ -84,13 +75,11 @@ class PaywallViewEventsTests: TestCase { } func testCancelledPurchase() async throws { - try self.createView() - .addToHierarchy() - - _ = try await self.handler.purchase(package: try XCTUnwrap(Self.offering.monthly)) + try await self.runDuringViewLifetime { + _ = try await self.handler.purchase(package: try XCTUnwrap(Self.offering.monthly)) + } - await self.fulfillment(of: [self.cancelEventExpectation, self.closeEventExpectation], - timeout: 1) + await self.waitForCloseEvent() expect(self.events).to(haveCount(3)) expect(self.events.map(\.eventType)).to(contain([.impression, .cancel, .close])) @@ -101,24 +90,15 @@ class PaywallViewEventsTests: TestCase { } func testDifferentPaywallsCreateSeparateSessionIdentifiers() async throws { - self.impressionEventExpectation.expectedFulfillmentCount = 2 self.closeEventExpectation.expectedFulfillmentCount = 2 - let firstCloseExpectation = XCTestExpectation(description: "First paywall was closed") + try await self.runDuringViewLifetime {} + try await self.runDuringViewLifetime {} - try self.createView() - .onDisappear { firstCloseExpectation.fulfill() } - .addToHierarchy() - - await self.fulfillment(of: [firstCloseExpectation], timeout: 1) - - try self.createView() - .addToHierarchy() - - await self.fulfillment(of: [self.impressionEventExpectation, self.closeEventExpectation], timeout: 1) + await self.waitForCloseEvent() expect(self.events).to(haveCount(4)) - expect(Set(self.events.map(\.eventType))) == [.impression, .close, .impression, .close] + expect(self.events.map(\.eventType)) == [.impression, .close, .impression, .close] expect(Set(self.events.map(\.data.sessionIdentifier))).to(haveCount(2)) } @@ -131,12 +111,26 @@ class PaywallViewEventsTests: TestCase { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) private extension PaywallViewEventsTests { + /// Invokes `createView` and runs the given `closure` during the lifetime of the view. + /// Returns after the view has been completly removed from the hierarchy. + func runDuringViewLifetime( + _ closure: @escaping () async throws -> Void + ) async throws { + // Create a `Task` to run inside of an `autoreleasepool`. + try await Task { + let dispose = try self.createView() + .addToHierarchy() + try await closure() + dispose() + }.value + } + func track(_ event: PaywallEvent) { self.events.append(event) switch event { - case .impression: self.impressionEventExpectation.fulfill() - case .cancel: self.cancelEventExpectation.fulfill() + case .impression: break + case .cancel: break case .close: self.closeEventExpectation.fulfill() } } @@ -160,6 +154,10 @@ private extension PaywallViewEventsTests { expect(data.darkMode) == (self.scheme == .dark) } + func waitForCloseEvent() async { + await self.fulfillment(of: [self.closeEventExpectation], timeout: 1) + } + } private extension PaywallViewMode {