Skip to content

[url_launcher] Convert macOS to Pigeon #3686

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import FlutterMacOS
import XCTest
import url_launcher_macos

@testable import url_launcher_macos

/// A stub to simulate the system Url handler.
class StubWorkspace: SystemURLHandler {
Expand All @@ -23,132 +24,51 @@ class StubWorkspace: SystemURLHandler {
class RunnerTests: XCTestCase {

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"])

plugin.handle(
call,
result: { (result: Any?) -> Void in
XCTAssertEqual(result as? Bool, true)
expectation.fulfill()
})

wait(for: [expectation], timeout: 10.0)
let result = try plugin.canLaunch(url: "https://flutter.dev")
XCTAssertNil(result.error)
XCTAssertTrue(result.value)
}

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)
let result = try plugin.canLaunch(url: "example://flutter.dev")
XCTAssertNil(result.error)
XCTAssertFalse(result.value)
}

func testCanLaunchInvalidUrlReturnsFalse() throws {
let expectation = XCTestExpectation(description: "Check if the URL can be launched")
func testCanLaunchInvalidUrlReturnsError() throws {
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)
let result = try plugin.canLaunch(url: "invalid url")
XCTAssertEqual(result.error, .invalidUrl)
}

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"])
let plugin = UrlLauncherPlugin(workspace)

pluginWithStubWorkspace.handle(
call,
result: { (result: Any?) -> Void in
XCTAssertEqual(result as? Bool, true)
expectation.fulfill()
})

wait(for: [expectation], timeout: 10.0)
let result = try plugin.launch(url: "https://flutter.dev")
XCTAssertNil(result.error)
XCTAssertTrue(result.value)
}

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"])
let plugin = UrlLauncherPlugin(workspace)

pluginWithStubWorkspace.handle(
call,
result: { (result: Any?) -> Void in
XCTAssertEqual(result as? Bool, false)
expectation.fulfill()
})

wait(for: [expectation], timeout: 10.0)
let result = try plugin.launch(url: "schemethatdoesnotexist://flutter.dev")
XCTAssertNil(result.error)
XCTAssertFalse(result.value)
}

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()
})
func testLaunchInvalidUrlReturnsError() throws {
let plugin = UrlLauncherPlugin()

wait(for: [expectation], timeout: 10.0)
let result = try plugin.launch(url: "invalid url")
XCTAssertEqual(result.error, .invalidUrl)
}
}
136 changes: 136 additions & 0 deletions packages/url_launcher/url_launcher_macos/lib/src/messages.g.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v9.2.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import

import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;

import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';

/// Possible error conditions for [UrlLauncherApi] calls.
enum UrlLauncherError {
/// The URL could not be parsed as an NSURL.
invalidUrl,
}

/// Possible results for a [UrlLauncherApi] call with a boolean outcome.
class UrlLauncherBoolResult {
UrlLauncherBoolResult({
required this.value,
this.error,
});

bool value;

UrlLauncherError? error;

Object encode() {
return <Object?>[
value,
error?.index,
];
}

static UrlLauncherBoolResult decode(Object result) {
result as List<Object?>;
return UrlLauncherBoolResult(
value: result[0]! as bool,
error:
result[1] != null ? UrlLauncherError.values[result[1]! as int] : null,
);
}
}

class _UrlLauncherApiCodec extends StandardMessageCodec {
const _UrlLauncherApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is UrlLauncherBoolResult) {
buffer.putUint8(128);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
}

@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 128:
return UrlLauncherBoolResult.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
}
}

class UrlLauncherApi {
/// Constructor for [UrlLauncherApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
UrlLauncherApi({BinaryMessenger? binaryMessenger})
: _binaryMessenger = binaryMessenger;
final BinaryMessenger? _binaryMessenger;

static const MessageCodec<Object?> codec = _UrlLauncherApiCodec();

/// Returns a true result if the URL can definitely be launched.
Future<UrlLauncherBoolResult> canLaunchUrl(String arg_url) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_url]) as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as UrlLauncherBoolResult?)!;
}
}

/// Opens the URL externally, returning a true result if successful.
Future<UrlLauncherBoolResult> launchUrl(String arg_url) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_url]) as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as UrlLauncherBoolResult?)!;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:flutter/services.dart';
import 'package:url_launcher_platform_interface/link.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';

const MethodChannel _channel =
MethodChannel('plugins.flutter.io/url_launcher_macos');
import 'src/messages.g.dart';

/// An implementation of [UrlLauncherPlatform] for macOS.
class UrlLauncherMacOS extends UrlLauncherPlatform {
/// Creates a new plugin implementation instance.
UrlLauncherMacOS({
@visibleForTesting UrlLauncherApi? api,
}) : _hostApi = api ?? UrlLauncherApi();

final UrlLauncherApi _hostApi;

/// Registers this class as the default instance of [UrlLauncherPlatform].
static void registerWith() {
UrlLauncherPlatform.instance = UrlLauncherMacOS();
Expand All @@ -22,11 +27,14 @@ class UrlLauncherMacOS extends UrlLauncherPlatform {
final LinkDelegate? linkDelegate = null;

@override
Future<bool> canLaunch(String url) {
return _channel.invokeMethod<bool>(
'canLaunch',
<String, Object>{'url': url},
).then((bool? value) => value ?? false);
Future<bool> canLaunch(String url) async {
final UrlLauncherBoolResult result = await _hostApi.canLaunchUrl(url);
switch (result.error) {
case UrlLauncherError.invalidUrl:
throw _getInvalidUrlException(url);
case null:
}
return result.value;
}

@override
Expand All @@ -39,16 +47,24 @@ class UrlLauncherMacOS extends UrlLauncherPlatform {
required bool universalLinksOnly,
required Map<String, String> headers,
String? webOnlyWindowName,
}) {
return _channel.invokeMethod<bool>(
'launch',
<String, Object>{
'url': url,
'enableJavaScript': enableJavaScript,
'enableDomStorage': enableDomStorage,
'universalLinksOnly': universalLinksOnly,
'headers': headers,
},
).then((bool? value) => value ?? false);
}) async {
final UrlLauncherBoolResult result = await _hostApi.launchUrl(url);
switch (result.error) {
case UrlLauncherError.invalidUrl:
throw _getInvalidUrlException(url);
case null:
}
return result.value;
}

Exception _getInvalidUrlException(String url) {
// TODO(stuartmorgan): Make this an actual ArgumentError. This should be
// coordinated across all platforms as a breaking change to have them all
// return the same thing; currently it throws a PlatformException to
// preserve existing behavior.
return PlatformException(
code: 'argument_error',
message: 'Unable to parse URL',
details: 'Provided URL: $url');
}
}
Loading