Skip to content

Commit 958737f

Browse files
committed
Inject LinkOpener safari factory for testing
1 parent 8727dc7 commit 958737f

File tree

2 files changed

+45
-14
lines changed

2 files changed

+45
-14
lines changed

Shared/Sources/Shared/Services/LinkOpener.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ public enum LinkOpener {
2727
presenter.present(safariVC, animated: true)
2828
}
2929

30+
private static var safariControllerFactory: (URL, SFSafariViewController.Configuration) -> SFSafariViewController = { url, configuration in
31+
SFSafariViewController(url: url, configuration: configuration)
32+
}
33+
3034
public static func openURL(_ url: URL, with _: Post? = nil) {
3135
// Determine user preference for opening links via injected settings use case
3236
let settings = settingsProvider()
@@ -40,7 +44,7 @@ public enum LinkOpener {
4044
let config = SFSafariViewController.Configuration()
4145
config.entersReaderIfAvailable = settings.safariReaderMode
4246

43-
let safariVC = SFSafariViewController(url: url, configuration: config)
47+
let safariVC = safariControllerFactory(url, config)
4448
safariPresenter(presenter, safariVC)
4549
} else {
4650
// Fallback if we cannot present in-app browser
@@ -87,18 +91,23 @@ public enum LinkOpener {
8791
settings: (() -> any SettingsUseCase)? = nil,
8892
openURL: ((URL) -> Void)? = nil,
8993
presenter: (() -> UIViewController?)? = nil,
90-
presentSafari: ((UIViewController, SFSafariViewController) -> Void)? = nil
94+
presentSafari: ((UIViewController, SFSafariViewController) -> Void)? = nil,
95+
safariControllerFactory: ((URL, SFSafariViewController.Configuration) -> SFSafariViewController)? = nil
9196
) {
9297
if let settings { settingsProvider = settings }
9398
if let openURL { systemOpener = openURL }
9499
if let presenter { presenterProvider = presenter }
95100
if let presentSafari { safariPresenter = presentSafari }
101+
if let safariControllerFactory { self.safariControllerFactory = safariControllerFactory }
96102
}
97103

98104
static func resetEnvironment() {
99105
settingsProvider = { DependencyContainer.shared.getSettingsUseCase() }
100106
systemOpener = { url in UIApplication.shared.open(url) }
101107
presenterProvider = { findPresenter() }
102108
safariPresenter = { presenter, safariVC in presenter.present(safariVC, animated: true) }
109+
safariControllerFactory = { url, configuration in
110+
SFSafariViewController(url: url, configuration: configuration)
111+
}
103112
}
104113
}

Shared/Tests/SharedTests/LinkOpenerTests.swift

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,27 +38,46 @@ struct LinkOpenerTests {
3838
func presentsSafariViewController() {
3939
let settings = StubSettingsUseCase(safariReaderMode: true, openInDefaultBrowser: false)
4040
var capturedPresented: (UIViewController, SFSafariViewController)?
41+
var capturedFactoryInput: (URL, SFSafariViewController.Configuration)?
42+
var stubSafari: StubSafariViewController?
4143

4244
LinkOpener.setEnvironmentForTesting(
4345
settings: { settings },
4446
openURL: { _ in Issue.record("Should not open system browser when presenter exists") },
4547
presenter: { StubPresenter() },
4648
presentSafari: { presenter, safari in
4749
capturedPresented = (presenter, safari)
50+
},
51+
safariControllerFactory: { url, configuration in
52+
capturedFactoryInput = (url, configuration)
53+
let controller = StubSafariViewController(url: url, configuration: configuration)
54+
stubSafari = controller
55+
return controller
4856
}
4957
)
5058

59+
defer { LinkOpener.resetEnvironment() }
60+
5161
let url = URL(string: "https://news.ycombinator.com")!
5262
LinkOpener.openURL(url)
5363

54-
guard let (_, safariController) = capturedPresented else {
64+
guard let factoryInput = capturedFactoryInput else {
65+
Issue.record("Expected safariControllerFactory to be invoked")
66+
return
67+
}
68+
69+
#expect(factoryInput.0 == url)
70+
#expect(factoryInput.1.entersReaderIfAvailable)
71+
72+
guard let (presenter, safariController) = capturedPresented else {
5573
Issue.record("Expected Safari to be presented")
5674
return
5775
}
5876

59-
#expect(safariController.initialURL == url)
60-
#expect(safariController.resolvedConfiguration.entersReaderIfAvailable)
61-
LinkOpener.resetEnvironment()
77+
#expect(presenter is StubPresenter)
78+
#expect(safariController === stubSafari)
79+
#expect(stubSafari?.capturedURL == url)
80+
#expect(stubSafari?.capturedConfiguration.entersReaderIfAvailable == true)
6281
}
6382

6483
@MainActor
@@ -121,15 +140,18 @@ private final class StubSettingsUseCase: SettingsUseCase, @unchecked Sendable {
121140

122141
private final class StubPresenter: UIViewController {}
123142

124-
private extension SFSafariViewController {
125-
var initialURL: URL {
126-
// SFSafariViewController exposes the initial URL through private API, but we can rely on the
127-
// view controller's `value(forKey:)` for testing purposes in the test bundle.
128-
value(forKey: "URL") as? URL ?? URL(string: "about:blank")!
143+
private final class StubSafariViewController: SFSafariViewController {
144+
let capturedURL: URL
145+
let capturedConfiguration: SFSafariViewController.Configuration
146+
147+
override init(url: URL, configuration: SFSafariViewController.Configuration) {
148+
capturedURL = url
149+
capturedConfiguration = configuration
150+
super.init(url: url, configuration: configuration)
129151
}
130152

131-
var resolvedConfiguration: SFSafariViewController.Configuration {
132-
(value(forKey: "configuration") as? SFSafariViewController.Configuration)
133-
?? SFSafariViewController.Configuration()
153+
@available(*, unavailable)
154+
override required init(coder: NSCoder) {
155+
fatalError("init(coder:) has not been implemented")
134156
}
135157
}

0 commit comments

Comments
 (0)