[ Widget Preview ] Fix type error when retrieving flags from persistent preferences (#176546)

Also moves `dtd_services_test.dart` to actually use the
`WidgetPreviewScaffoldDtdServices` implementation, which would have
caught the typecast issue in the implementation.
This commit is contained in:
Ben Konyi 2025-10-06 05:19:29 -07:00 committed by GitHub
parent b95ab963a4
commit 0400e2cf77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 178 additions and 177 deletions

View File

@ -92,12 +92,14 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService {
/// preferences map.
///
/// Returns null if [key] is not in the map.
Future<String?> getPreference(String key) async {
Future<Object?> getPreference(String key) async {
try {
final response = StringResponse.fromDTDResponse(
(await _call(kGetPreference, params: {'key': key}))!,
);
return response.value;
final response = await _call(kGetPreference, params: {'key': key});
return switch (response?.type) {
'StringResponse' => StringResponse.fromDTDResponse(response!).value,
'BoolResponse' => BoolResponse.fromDTDResponse(response!).value,
_ => throw StateError('Unexpected response type: ${response?.type}'),
};
} on RpcException catch (e) {
if (e.code == kNoValueForKey) {
return null;
@ -110,15 +112,12 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService {
///
/// If [key] is not set, [defaultValue] is returned.
Future<bool> getFlag(String key, {bool defaultValue = false}) async {
final result = await getPreference(key);
if (result == null) {
return defaultValue;
}
return bool.tryParse(result) ?? defaultValue;
final result = await getPreference(key) as bool?;
return result ?? defaultValue;
}
/// Sets [key] to [value] in the persistent preferences map.
Future<void> setPreference(String key, Object value) async {
Future<void> setPreference(String key, Object? value) async {
await _call(kSetPreference, params: {'key': key, 'value': value});
}

View File

@ -1,146 +0,0 @@
// Copyright 2014 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.
import 'dart:async';
import 'package:dtd/dtd.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/widget_preview/dtd_services.dart';
import 'package:flutter_tools/src/widget_preview/persistent_preferences.dart';
import 'package:json_rpc_2/json_rpc_2.dart';
import 'package:test/fake.dart';
import '../../../src/common.dart';
import '../../../src/context.dart';
import '../../../src/test_flutter_command_runner.dart';
import '../utils/project_testing_utils.dart';
class FakeFlutterProject extends Fake implements FlutterProject {
FakeFlutterProject();
}
void main() {
late WidgetPreviewDtdServices dtdServer;
late LoggingProcessManager loggingProcessManager;
late Logger logger;
setUp(() async {
loggingProcessManager = LoggingProcessManager();
logger = BufferLogger.test();
});
tearDown(() async {
await dtdServer.shutdownHooks.runShutdownHooks(logger);
});
group('$WidgetPreviewDtdServices', () {
testUsingContext(
'handles ${WidgetPreviewDtdServices.kHotRestartPreviewer} invocations',
() async {
// Start DTD and register the widget preview DTD services with a custom handler for hot
// restart requests.
final hotRestartRequestCompleter = Completer<void>();
dtdServer = WidgetPreviewDtdServices(
fs: MemoryFileSystem.test(),
logger: logger,
shutdownHooks: ShutdownHooks(),
dtdLauncher: DtdLauncher(
logger: logger,
artifacts: globals.artifacts!,
processManager: globals.processManager,
),
onHotRestartPreviewerRequest: hotRestartRequestCompleter.complete,
project: FakeFlutterProject(),
);
await dtdServer.launchAndConnect();
// Connect to the DTD instance and invoke the hot restart endpoint.
final DartToolingDaemon dtd = await DartToolingDaemon.connect(dtdServer.dtdUri!);
final DTDResponse response = await dtd.call(
WidgetPreviewDtdServices.kWidgetPreviewService,
WidgetPreviewDtdServices.kHotRestartPreviewer,
);
// This will throw if the response is not an instance of Success.
expect(() => Success.fromDTDResponse(response), returnsNormally);
// Ensure the custom handler is actually invoked.
await hotRestartRequestCompleter.future;
},
overrides: <Type, Generator>{ProcessManager: () => loggingProcessManager},
);
testUsingContext('can set and retreive values from $PersistentPreferences', () async {
dtdServer = WidgetPreviewDtdServices(
fs: MemoryFileSystem.test(),
logger: logger,
shutdownHooks: ShutdownHooks(),
dtdLauncher: DtdLauncher(
logger: logger,
artifacts: globals.artifacts!,
processManager: globals.processManager,
),
onHotRestartPreviewerRequest: () {},
project: FakeFlutterProject(),
);
await dtdServer.launchAndConnect();
// The properties file should be created by the PersistentProperties constructor.
final File preferencesFile = dtdServer.preferences.file;
expect(preferencesFile.existsSync(), true);
// Connect to the DTD instance.
final DartToolingDaemon dtd = await DartToolingDaemon.connect(dtdServer.dtdUri!);
Future<String?> getPreference(String key) async {
try {
return StringResponse.fromDTDResponse(
await dtd.call(
WidgetPreviewDtdServices.kWidgetPreviewService,
WidgetPreviewDtdServices.kGetPreference,
params: {'key': key},
),
).value;
} on RpcException catch (e) {
if (e.code == WidgetPreviewDtdServices.kNoValueForKey) {
return null;
}
rethrow;
}
}
Future<void> setPreference(String key, String? value) async {
await dtd.call(
WidgetPreviewDtdServices.kWidgetPreviewService,
WidgetPreviewDtdServices.kSetPreference,
params: {'key': key, 'value': value},
);
}
const kTestKey = 'myKey';
// The preferences file should be empty.
expect(await getPreference(kTestKey), null);
expect(preferencesFile.readAsStringSync(), isEmpty);
// Set a preference and ensure it's read back.
const kFirstValue = 'foo';
await setPreference(kTestKey, kFirstValue);
expect(await getPreference(kTestKey), kFirstValue);
expect(json.decode(preferencesFile.readAsStringSync()), {kTestKey: kFirstValue});
// Overwrite kTestKey and ensure it's read back.
const kSecondValue = 'bar';
await setPreference(kTestKey, kSecondValue);
expect(await getPreference(kTestKey), kSecondValue);
expect(json.decode(preferencesFile.readAsStringSync()), {kTestKey: kSecondValue});
});
});
}

View File

@ -92,12 +92,14 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService {
/// preferences map.
///
/// Returns null if [key] is not in the map.
Future<String?> getPreference(String key) async {
Future<Object?> getPreference(String key) async {
try {
final response = StringResponse.fromDTDResponse(
(await _call(kGetPreference, params: {'key': key}))!,
);
return response.value;
final response = await _call(kGetPreference, params: {'key': key});
return switch (response?.type) {
'StringResponse' => StringResponse.fromDTDResponse(response!).value,
'BoolResponse' => BoolResponse.fromDTDResponse(response!).value,
_ => throw StateError('Unexpected response type: ${response?.type}'),
};
} on RpcException catch (e) {
if (e.code == kNoValueForKey) {
return null;
@ -110,15 +112,12 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService {
///
/// If [key] is not set, [defaultValue] is returned.
Future<bool> getFlag(String key, {bool defaultValue = false}) async {
final result = await getPreference(key);
if (result == null) {
return defaultValue;
}
return bool.tryParse(result) ?? defaultValue;
final result = await getPreference(key) as bool?;
return result ?? defaultValue;
}
/// Sets [key] to [value] in the persistent preferences map.
Future<void> setPreference(String key, Object value) async {
Future<void> setPreference(String key, Object? value) async {
await _call(kSetPreference, params: {'key': key, 'value': value});
}

View File

@ -45,8 +45,8 @@ dependencies:
term_glyph: 1.2.2
test_api: 0.7.7
typed_data: 1.4.0
unified_analytics: 8.0.1
url_launcher_android: 6.3.22
unified_analytics: 8.0.5
url_launcher_android: 6.3.23
url_launcher_ios: 6.3.4
url_launcher_linux: 3.2.1
url_launcher_macos: 3.2.3
@ -59,6 +59,11 @@ dependencies:
web_socket: 1.0.1
web_socket_channel: 3.0.3
dev_dependencies:
flutter_tools:
path: ../../../
test: 1.26.3
flutter:
uses-material-design: true
# PUBSPEC CHECKSUM: fdcqn8
# PUBSPEC CHECKSUM: 130obr

View File

@ -0,0 +1,140 @@
// Copyright 2014 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.
import 'dart:async';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/widget_preview/dtd_services.dart';
import 'package:flutter_tools/src/widget_preview/persistent_preferences.dart';
import 'package:test/fake.dart';
import 'package:widget_preview_scaffold/src/dtd/dtd_services.dart';
import '../../../src/common.dart';
import '../../../src/context.dart';
import '../../../src/test_flutter_command_runner.dart';
import '../../../commands.shard/permeable/utils/project_testing_utils.dart';
class FakeFlutterProject extends Fake implements FlutterProject {
FakeFlutterProject();
}
void main() {
late WidgetPreviewDtdServices dtdServer;
late LoggingProcessManager loggingProcessManager;
late Logger logger;
setUp(() async {
loggingProcessManager = LoggingProcessManager();
logger = BufferLogger.test();
});
tearDown(() async {
await dtdServer.shutdownHooks.runShutdownHooks(logger);
});
group('$WidgetPreviewDtdServices', () {
testUsingContext(
'handles ${WidgetPreviewDtdServices.kHotRestartPreviewer} invocations',
() async {
// Start DTD and register the widget preview DTD services with a custom handler for hot
// restart requests.
final hotRestartRequestCompleter = Completer<void>();
dtdServer = WidgetPreviewDtdServices(
fs: MemoryFileSystem.test(),
logger: logger,
shutdownHooks: ShutdownHooks(),
dtdLauncher: DtdLauncher(
logger: logger,
artifacts: globals.artifacts!,
processManager: globals.processManager,
),
onHotRestartPreviewerRequest: hotRestartRequestCompleter.complete,
project: FakeFlutterProject(),
);
await dtdServer.launchAndConnect();
// Connect to the DTD instance and invoke the hot restart endpoint.
final dtd = WidgetPreviewScaffoldDtdServices();
await dtd.connect(dtdUri: dtdServer.dtdUri);
await dtd.hotRestartPreviewer();
// Ensure the custom handler is actually invoked.
await hotRestartRequestCompleter.future;
},
overrides: <Type, Generator>{ProcessManager: () => loggingProcessManager},
);
testUsingContext(
'can set and retreive values from $PersistentPreferences',
() async {
dtdServer = WidgetPreviewDtdServices(
fs: MemoryFileSystem.test(),
logger: logger,
shutdownHooks: ShutdownHooks(),
dtdLauncher: DtdLauncher(
logger: logger,
artifacts: globals.artifacts!,
processManager: globals.processManager,
),
onHotRestartPreviewerRequest: () {},
project: FakeFlutterProject(),
);
await dtdServer.launchAndConnect();
// The properties file should be created by the PersistentProperties constructor.
final File preferencesFile = dtdServer.preferences.file;
expect(preferencesFile.existsSync(), true);
// Connect to the DTD instance.
final dtd = WidgetPreviewScaffoldDtdServices();
await dtd.connect(dtdUri: dtdServer.dtdUri);
const kTestKey = 'myKey';
// The preferences file should be empty.
expect(await dtd.getPreference(kTestKey), null);
expect(preferencesFile.readAsStringSync(), isEmpty);
// Set a preference and ensure it's read back.
const kFirstValue = 'foo';
await dtd.setPreference(kTestKey, kFirstValue);
expect(await dtd.getPreference(kTestKey), kFirstValue);
expect(json.decode(preferencesFile.readAsStringSync()), {
kTestKey: kFirstValue,
});
// Overwrite kTestKey and ensure it's read back.
const kSecondValue = 'bar';
await dtd.setPreference(kTestKey, kSecondValue);
expect(await dtd.getPreference(kTestKey), kSecondValue);
expect(json.decode(preferencesFile.readAsStringSync()), {
kTestKey: kSecondValue,
});
// Write a flag.
const kFlagKey = 'flag';
await dtd.setPreference(kFlagKey, true);
expect(await dtd.getFlag(kFlagKey), true);
expect(json.decode(preferencesFile.readAsStringSync()), {
kFlagKey: true,
kTestKey: kSecondValue,
});
// Remove an entry.
await dtd.setPreference(kTestKey, null);
expect(await dtd.getPreference(kTestKey), null);
expect(json.decode(preferencesFile.readAsStringSync()), {
kFlagKey: true,
});
},
);
});
}

View File

@ -62,7 +62,7 @@ class FakeWidgetPreviewScaffoldDtdServices extends Fake
FakeWidgetPreviewScaffoldDtdServices({this.isWindows = false});
final navigationEvents = <CodeLocation>[];
final preferences = <String, Object>{};
final preferences = <String, Object?>{};
@override
Future<void> connect({Uri? dtdUri}) async {}
@ -117,7 +117,11 @@ class FakeWidgetPreviewScaffoldDtdServices extends Fake
/// Sets [key] to [value] in the persistent preferences map.
@override
Future<void> setPreference(String key, Object value) async {
Future<void> setPreference(String key, Object? value) async {
if (value == null) {
preferences.remove(key);
return;
}
preferences[key] = value;
}
}

View File

@ -867,10 +867,10 @@ packages:
dependency: "direct main"
description:
name: url_launcher_android
sha256: "199bc33e746088546a39cc5f36bac5a278c5e53b40cb3196f99e7345fdcfae6b"
sha256: c0fb544b9ac7efa10254efaf00a951615c362d1ea1877472f8f6c0fa00fcf15b
url: "https://pub.dev"
source: hosted
version: "6.3.22"
version: "6.3.23"
url_launcher_ios:
dependency: "direct main"
description:

View File

@ -175,7 +175,7 @@ dependencies:
test_core: 0.6.12
typed_data: 1.4.0
url_launcher: 6.3.2
url_launcher_android: 6.3.22
url_launcher_android: 6.3.23
url_launcher_ios: 6.3.4
url_launcher_linux: 3.2.1
url_launcher_macos: 3.2.3
@ -212,4 +212,4 @@ dependencies:
pedantic: 1.11.1
quiver: 3.2.2
yaml_edit: 2.2.2
# PUBSPEC CHECKSUM: pb0r15
# PUBSPEC CHECKSUM: 5h4lnl