From 0400e2cf77b228ff44aafc5ea230054fdd397d7d Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Mon, 6 Oct 2025 05:19:29 -0700 Subject: [PATCH] [ 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. --- .../lib/src/dtd/dtd_services.dart.tmpl | 21 ++- .../widget_preview/dtd_services_test.dart | 146 ------------------ .../lib/src/dtd/dtd_services.dart | 21 ++- .../widget_preview_scaffold/pubspec.yaml | 11 +- .../test/dtd_services_test.dart | 140 +++++++++++++++++ .../widget_preview_scaffold_test_utils.dart | 8 +- pubspec.lock | 4 +- pubspec.yaml | 4 +- 8 files changed, 178 insertions(+), 177 deletions(-) delete mode 100644 packages/flutter_tools/test/commands.shard/permeable/widget_preview/dtd_services_test.dart create mode 100644 packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/test/dtd_services_test.dart diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_services.dart.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_services.dart.tmpl index 76469ef7b1e..ed5352dbd41 100644 --- a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_services.dart.tmpl +++ b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_services.dart.tmpl @@ -92,12 +92,14 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService { /// preferences map. /// /// Returns null if [key] is not in the map. - Future getPreference(String key) async { + Future 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 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 setPreference(String key, Object value) async { + Future setPreference(String key, Object? value) async { await _call(kSetPreference, params: {'key': key, 'value': value}); } diff --git a/packages/flutter_tools/test/commands.shard/permeable/widget_preview/dtd_services_test.dart b/packages/flutter_tools/test/commands.shard/permeable/widget_preview/dtd_services_test.dart deleted file mode 100644 index d7ce8f9b926..00000000000 --- a/packages/flutter_tools/test/commands.shard/permeable/widget_preview/dtd_services_test.dart +++ /dev/null @@ -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(); - 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: {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 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 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}); - }); - }); -} diff --git a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/dtd/dtd_services.dart b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/dtd/dtd_services.dart index 76469ef7b1e..ed5352dbd41 100644 --- a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/dtd/dtd_services.dart +++ b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/dtd/dtd_services.dart @@ -92,12 +92,14 @@ class WidgetPreviewScaffoldDtdServices with DtdEditorService { /// preferences map. /// /// Returns null if [key] is not in the map. - Future getPreference(String key) async { + Future 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 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 setPreference(String key, Object value) async { + Future setPreference(String key, Object? value) async { await _call(kSetPreference, params: {'key': key, 'value': value}); } diff --git a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/pubspec.yaml b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/pubspec.yaml index 25648b09589..cca81f2d021 100644 --- a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/pubspec.yaml +++ b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/pubspec.yaml @@ -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 diff --git a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/test/dtd_services_test.dart b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/test/dtd_services_test.dart new file mode 100644 index 00000000000..120cc064d14 --- /dev/null +++ b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/test/dtd_services_test.dart @@ -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(); + 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: {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, + }); + }, + ); + }); +} diff --git a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/test/utils/widget_preview_scaffold_test_utils.dart b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/test/utils/widget_preview_scaffold_test_utils.dart index 9eaaecd5d0f..104eca32a40 100644 --- a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/test/utils/widget_preview_scaffold_test_utils.dart +++ b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/test/utils/widget_preview_scaffold_test_utils.dart @@ -62,7 +62,7 @@ class FakeWidgetPreviewScaffoldDtdServices extends Fake FakeWidgetPreviewScaffoldDtdServices({this.isWindows = false}); final navigationEvents = []; - final preferences = {}; + final preferences = {}; @override Future connect({Uri? dtdUri}) async {} @@ -117,7 +117,11 @@ class FakeWidgetPreviewScaffoldDtdServices extends Fake /// Sets [key] to [value] in the persistent preferences map. @override - Future setPreference(String key, Object value) async { + Future setPreference(String key, Object? value) async { + if (value == null) { + preferences.remove(key); + return; + } preferences[key] = value; } } diff --git a/pubspec.lock b/pubspec.lock index abb0a9d12b6..e280aad5291 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index d3253b25ca6..adf11359f11 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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