[url_launcher] Improving unit tests on mac (#4472)

Add unit tests for url_launcher canLaunch and launch methods on mac with success and failure results.

Fixes #92969
diff --git a/packages/url_launcher/url_launcher_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md
index 7b7677a..9281c91 100644
--- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md
@@ -1,6 +1,7 @@
-## NEXT
+## 2.0.3
 
 * Updates code for new analysis options.
+* Updates unit tests.
 
 ## 2.0.2
 
diff --git a/packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/RunnerTests.swift
index d08f664..adbd114 100644
--- a/packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/RunnerTests.swift
+++ b/packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/RunnerTests.swift
@@ -6,19 +6,149 @@
 import XCTest
 import url_launcher_macos
 
+/// A stub to simulate the system Url handler.
+class StubWorkspace: SystemURLHandler {
+
+  var isSuccessful = true
+
+  func open(_ url: URL) -> Bool {
+    return isSuccessful
+  }
+
+  func urlForApplication(toOpen: URL) -> URL? {
+    return toOpen
+  }
+}
+
 class RunnerTests: XCTestCase {
-  func testCanLaunch() throws {
+
+  func testCanLaunchSuccessReturnsTrue() throws {
+    let expectation = XCTestExpectation(description: "Check if the URL can be launched")
     let plugin = UrlLauncherPlugin()
+
     let call = FlutterMethodCall(
       methodName: "canLaunch",
       arguments: ["url": "https://flutter.dev"])
-    var canLaunch: Bool?
+
     plugin.handle(
       call,
       result: { (result: Any?) -> Void in
-        canLaunch = result as? Bool
+        XCTAssertEqual(result as? Bool, true)
+        expectation.fulfill()
       })
 
-    XCTAssertTrue(canLaunch == true)
+    wait(for: [expectation], timeout: 10.0)
+  }
+
+  func testCanLaunchNoAppIsAbleToOpenUrlReturnsFalse() throws {
+    let expectation = XCTestExpectation(description: "Check if the URL can be launched")
+    let plugin = UrlLauncherPlugin()
+
+    let call = FlutterMethodCall(
+      methodName: "canLaunch",
+      arguments: ["url": "example://flutter.dev"])
+
+    plugin.handle(
+      call,
+      result: { (result: Any?) -> Void in
+        XCTAssertEqual(result as? Bool, false)
+        expectation.fulfill()
+      })
+
+    wait(for: [expectation], timeout: 10.0)
+  }
+
+  func testCanLaunchInvalidUrlReturnsFalse() throws {
+    let expectation = XCTestExpectation(description: "Check if the URL can be launched")
+    let plugin = UrlLauncherPlugin()
+
+    let call = FlutterMethodCall(
+      methodName: "canLaunch",
+      arguments: ["url": "brokenUrl"])
+
+    plugin.handle(
+      call,
+      result: { (result: Any?) -> Void in
+        XCTAssertEqual(result as? Bool, false)
+        expectation.fulfill()
+      })
+
+    wait(for: [expectation], timeout: 10.0)
+  }
+
+  func testCanLaunchMissingArgumentReturnsFlutterError() throws {
+    let expectation = XCTestExpectation(description: "Check if the URL can be launched")
+    let plugin = UrlLauncherPlugin()
+
+    let call = FlutterMethodCall(
+      methodName: "canLaunch",
+      arguments: [])
+
+    plugin.handle(
+      call,
+      result: { (result: Any?) -> Void in
+        XCTAssertTrue(result is FlutterError)
+        expectation.fulfill()
+      })
+
+    wait(for: [expectation], timeout: 10.0)
+  }
+
+  func testLaunchSuccessReturnsTrue() throws {
+    let expectation = XCTestExpectation(description: "Try to open the URL")
+    let workspace = StubWorkspace()
+    let pluginWithStubWorkspace = UrlLauncherPlugin(workspace)
+
+    let call = FlutterMethodCall(
+      methodName: "launch",
+      arguments: ["url": "https://flutter.dev"])
+
+    pluginWithStubWorkspace.handle(
+      call,
+      result: { (result: Any?) -> Void in
+        XCTAssertEqual(result as? Bool, true)
+        expectation.fulfill()
+      })
+
+    wait(for: [expectation], timeout: 10.0)
+  }
+
+  func testLaunchNoAppIsAbleToOpenUrlReturnsFalse() throws {
+    let expectation = XCTestExpectation(description: "Try to open the URL")
+    let workspace = StubWorkspace()
+    workspace.isSuccessful = false
+    let pluginWithStubWorkspace = UrlLauncherPlugin(workspace)
+
+    let call = FlutterMethodCall(
+      methodName: "launch",
+      arguments: ["url": "schemethatdoesnotexist://flutter.dev"])
+
+    pluginWithStubWorkspace.handle(
+      call,
+      result: { (result: Any?) -> Void in
+        XCTAssertEqual(result as? Bool, false)
+        expectation.fulfill()
+      })
+
+    wait(for: [expectation], timeout: 10.0)
+  }
+
+  func testLaunchMissingArgumentReturnsFlutterError() throws {
+    let expectation = XCTestExpectation(description: "Try to open the URL")
+    let workspace = StubWorkspace()
+    let pluginWithStubWorkspace = UrlLauncherPlugin(workspace)
+
+    let call = FlutterMethodCall(
+      methodName: "launch",
+      arguments: [])
+
+    pluginWithStubWorkspace.handle(
+      call,
+      result: { (result: Any?) -> Void in
+        XCTAssertTrue(result is FlutterError)
+        expectation.fulfill()
+      })
+
+    wait(for: [expectation], timeout: 10.0)
   }
 }
diff --git a/packages/url_launcher/url_launcher_macos/macos/Classes/UrlLauncherPlugin.swift b/packages/url_launcher/url_launcher_macos/macos/Classes/UrlLauncherPlugin.swift
index ab89038..c311b85 100644
--- a/packages/url_launcher/url_launcher_macos/macos/Classes/UrlLauncherPlugin.swift
+++ b/packages/url_launcher/url_launcher_macos/macos/Classes/UrlLauncherPlugin.swift
@@ -5,7 +5,35 @@
 import FlutterMacOS
 import Foundation
 
+/// A handler that can launch other apps, check if any app is able to open the URL.
+public protocol SystemURLHandler {
+
+  /// Opens the location at the specified URL.
+  ///
+  /// - Parameters:
+  ///   - url: A URL specifying the location to open.
+  /// - Returns: true if the location was successfully opened; otherwise, false.
+  func open(_ url: URL) -> Bool
+
+  /// Returns the URL to the default app that would be opened.
+  ///
+  /// - Parameters:
+  ///   - toOpen: The URL of the file to open.
+  /// - Returns: The URL of the default app that would open the specified url.
+  ///   Returns nil if no app is able to open the URL, or if the file URL does not exist.
+  func urlForApplication(toOpen: URL) -> URL?
+}
+
+extension NSWorkspace: SystemURLHandler {}
+
 public class UrlLauncherPlugin: NSObject, FlutterPlugin {
+
+  private var workspace: SystemURLHandler
+
+  public init(_ workspace: SystemURLHandler = NSWorkspace.shared) {
+    self.workspace = workspace
+  }
+
   public static func register(with registrar: FlutterPluginRegistrar) {
     let channel = FlutterMethodChannel(
       name: "plugins.flutter.io/url_launcher",
@@ -24,7 +52,7 @@
         result(invalidURLError(urlString))
         return
       }
-      result(NSWorkspace.shared.urlForApplication(toOpen: url) != nil)
+      result(workspace.urlForApplication(toOpen: url) != nil)
     case "launch":
       guard let unwrappedURLString = urlString,
         let url = URL.init(string: unwrappedURLString)
@@ -32,7 +60,7 @@
         result(invalidURLError(urlString))
         return
       }
-      result(NSWorkspace.shared.open(url))
+      result(workspace.open(url))
     default:
       result(FlutterMethodNotImplemented)
     }
diff --git a/packages/url_launcher/url_launcher_macos/pubspec.yaml b/packages/url_launcher/url_launcher_macos/pubspec.yaml
index 01f1db0..55e8ec3 100644
--- a/packages/url_launcher/url_launcher_macos/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_macos/pubspec.yaml
@@ -2,7 +2,7 @@
 description: macOS implementation of the url_launcher plugin.
 repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_macos
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
-version: 2.0.2
+version: 2.0.3
 
 environment:
   sdk: ">=2.12.0 <3.0.0"