| // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:analysis_server/src/services/refactoring/add_import_prefix.dart'; |
| import 'package:analysis_server/src/services/refactoring/move_top_level_to_file.dart'; |
| import 'package:language_server_protocol/protocol_custom_generated.dart'; |
| import 'package:language_server_protocol/protocol_generated.dart'; |
| import 'package:matcher/matcher.dart'; |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; |
| |
| import '../../src/services/refactoring/refactoring_test_support.dart'; |
| import '../../support/interactive_forms.dart'; |
| import '../../utils/lsp_protocol_extensions.dart'; |
| import '../server_abstract.dart'; |
| |
| void main() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(CommandResolveTest); |
| defineReflectiveTests(CommandResolveFileInputTest); |
| defineReflectiveTests(CommandResolveStringInputTest); |
| }); |
| } |
| |
| /// Tests file inputs in `command/resolve` using the MoveToFile refactor. |
| @reflectiveTest |
| class CommandResolveFileInputTest extends RefactoringTest |
| with InteractiveFormsExperimentMixin { |
| /// Simple file content with a single class named 'A'. |
| final simpleClassContent = ''' |
| class ^A {} |
| '''; |
| |
| /// The title of the refactor when using [simpleClassContent]. |
| final simpleClassRefactorTitle = "Move 'A' to file"; |
| |
| @override |
| String get refactoringCommandId => MoveTopLevelToFile.commandName; |
| |
| @override |
| void setUp() { |
| super.setUp(); |
| |
| // Most of the tests here assume we support file. Tests that do not will |
| // explicitly unset this. |
| setSupportedInteractiveFormInputKinds({'file'}); |
| |
| // Move to file also requires file create support. |
| setFileCreateSupport(); |
| } |
| |
| Future<void> test_acceptsValidAnswers() async { |
| addTestSource(simpleClassContent); |
| var newFilePath = join(projectFolderPath, 'lib', 'valid_destination.dart'); |
| var newFileUri = Uri.file(newFilePath); |
| |
| await initializeServer(); |
| var action = await expectCodeActionWithTitle(simpleClassRefactorTitle); |
| var command = action.asCommand; |
| |
| // Resolve the command, which should add the outstanding form fields. |
| var resolvedCommand = await resolveCommand( |
| ExecuteCommandParams( |
| command: command.command, |
| arguments: command.arguments, |
| ), |
| ); |
| |
| // Resolve again, with a valid answer. |
| expect(resolvedCommand.formFields, hasLength(1)); |
| var field = resolvedCommand.formFields!.single; |
| resolvedCommand = await resolveCommand( |
| InteractiveExecuteCommandParams( |
| command: resolvedCommand.command, |
| arguments: resolvedCommand.arguments, |
| formFields: resolvedCommand.formFields, |
| formAnswers: [field.answer(newFileUri.toString())], // Valid answer. |
| ), |
| ); |
| |
| // Check that the form field has gone, and that the valid answer was moved |
| // into the command arguments. |
| expect(resolvedCommand.formFields, isNull); // No more fields to complete. |
| var arguments = getRefactorCommandArguments(resolvedCommand.arguments); |
| expect(arguments, hasLength(1)); |
| expect(arguments.single, newFileUri.toString()); |
| } |
| |
| Future<void> test_formFields_notSupported() async { |
| // If we don't support the 'file' kind, then we shouldn't get back any |
| // form fields, only the default value in the arguments. |
| setSupportedInteractiveFormInputKinds({'number'}); |
| |
| addTestSource(simpleClassContent); |
| var newFilePath = join(projectFolderPath, 'lib', 'a.dart'); |
| var newFileUri = Uri.file(newFilePath); |
| |
| await initializeServer(); |
| var action = await expectCodeActionWithTitle(simpleClassRefactorTitle); |
| var command = action.asCommand; |
| |
| // Resolve the command, which would normally add the form fields, but the |
| // only one used here is not supported by us. |
| var resolvedCommand = await resolveCommand( |
| ExecuteCommandParams( |
| command: command.command, |
| arguments: command.arguments, |
| ), |
| ); |
| |
| // Check basics. |
| expect(resolvedCommand.command, command.command); |
| expect(resolvedCommand.arguments, command.arguments); |
| expect(resolvedCommand.formAnswers, isNull); |
| expect(resolvedCommand.formFields, isNull); |
| |
| // Check the arguments to the command contain the default value so the |
| // command will still work without the user prompt. |
| var refactorArguments = getRefactorCommandArguments( |
| resolvedCommand.arguments, |
| ); |
| expect(refactorArguments, hasLength(1)); |
| expect(refactorArguments.single, newFileUri.toString()); |
| } |
| |
| Future<void> test_formFields_supported() async { |
| addTestSource(simpleClassContent); |
| var newFilePath = join(projectFolderPath, 'lib', 'a.dart'); |
| var newFileUri = Uri.file(newFilePath); |
| |
| await initializeServer(); |
| var action = await expectCodeActionWithTitle(simpleClassRefactorTitle); |
| var command = action.asCommand; |
| |
| // Resolve the command, which should add the outstanding form fields. |
| var resolvedCommand = await resolveCommand( |
| ExecuteCommandParams( |
| command: command.command, |
| arguments: command.arguments, |
| ), |
| ); |
| |
| // Check basics. |
| expect(resolvedCommand.command, command.command); |
| expect(resolvedCommand.arguments, command.arguments); |
| expect(resolvedCommand.formAnswers, isNull); |
| expect(resolvedCommand.formFields, hasLength(1)); |
| |
| // Check the form field is what we'd expect. |
| var field = resolvedCommand.formFields!.single; |
| expect(field.type.kind, 'file'); |
| expect(field.description, 'Move to file'); |
| expect(field.defaultValue, newFileUri.toString()); |
| expect(field.error, isNull); |
| } |
| |
| Future<void> test_validates_incorrectType() async { |
| addTestSource(simpleClassContent); |
| |
| await initializeServer(); |
| var action = await expectCodeActionWithTitle(simpleClassRefactorTitle); |
| var command = action.asCommand; |
| |
| // Resolve the command, which should add the outstanding form fields. |
| var resolvedCommand = await resolveCommand( |
| ExecuteCommandParams( |
| command: command.command, |
| arguments: command.arguments, |
| ), |
| ); |
| |
| // Resolve again, with an incorrect type for the URI. |
| expect(resolvedCommand.formFields, hasLength(1)); |
| var field = resolvedCommand.formFields!.single; |
| resolvedCommand = await resolveCommand( |
| InteractiveExecuteCommandParams( |
| command: resolvedCommand.command, |
| arguments: resolvedCommand.arguments, |
| formFields: resolvedCommand.formFields, |
| formAnswers: [field.answer(true)], // Wrong type. |
| ), |
| ); |
| |
| // Check the field has a validation error and the current value is |
| // preserved. |
| expect(resolvedCommand.formFields, hasLength(1)); |
| field = resolvedCommand.formFields!.single; |
| expect(field.error, contains('valid file:// URI')); |
| expect(resolvedCommand.formAnswers!.single, field.answer(true)); |
| } |
| |
| Future<void> test_validates_invalidUri() async { |
| addTestSource(simpleClassContent); |
| |
| await initializeServer(); |
| var action = await expectCodeActionWithTitle(simpleClassRefactorTitle); |
| var command = action.asCommand; |
| |
| // Resolve the command, which should add the outstanding form fields. |
| var resolvedCommand = await resolveCommand( |
| ExecuteCommandParams( |
| command: command.command, |
| arguments: command.arguments, |
| ), |
| ); |
| |
| // Resolve again, with an invalid value for URI. |
| expect(resolvedCommand.formFields, hasLength(1)); |
| var field = resolvedCommand.formFields!.single; |
| resolvedCommand = await resolveCommand( |
| InteractiveExecuteCommandParams( |
| command: resolvedCommand.command, |
| arguments: resolvedCommand.arguments, |
| formFields: resolvedCommand.formFields, |
| formAnswers: [field.answer('not a valid file uri')], |
| ), |
| ); |
| |
| // Check the field has a validation error and the current value is |
| // preserved. |
| expect(resolvedCommand.formFields, hasLength(1)); |
| field = resolvedCommand.formFields!.single; |
| expect(field.error, contains('valid file:// URI')); |
| expect( |
| resolvedCommand.formAnswers!.single, |
| field.answer('not a valid file uri'), |
| ); |
| } |
| |
| Future<void> test_validates_notFileUri() async { |
| addTestSource(simpleClassContent); |
| |
| await initializeServer(); |
| var action = await expectCodeActionWithTitle(simpleClassRefactorTitle); |
| var command = action.asCommand; |
| |
| // Resolve the command, which should add the outstanding form fields. |
| var resolvedCommand = await resolveCommand( |
| ExecuteCommandParams( |
| command: command.command, |
| arguments: command.arguments, |
| ), |
| ); |
| |
| // Resolve again, with the wrong scheme for the URI. |
| expect(resolvedCommand.formFields, hasLength(1)); |
| var field = resolvedCommand.formFields!.single; |
| resolvedCommand = await resolveCommand( |
| InteractiveExecuteCommandParams( |
| command: resolvedCommand.command, |
| arguments: resolvedCommand.arguments, |
| formFields: resolvedCommand.formFields, |
| formAnswers: [field.answer('https://example.org')], |
| ), |
| ); |
| |
| // Check the field has a validation error and the current value is |
| // preserved. |
| expect(resolvedCommand.formFields, hasLength(1)); |
| field = resolvedCommand.formFields!.single; |
| expect(field.error, contains('valid file:// URI')); |
| expect( |
| resolvedCommand.formAnswers!.single, |
| field.answer('https://example.org'), |
| ); |
| } |
| } |
| |
| /// Tests string inputs in `command/resolve` using the AddImportPrefix refactor. |
| @reflectiveTest |
| class CommandResolveStringInputTest extends RefactoringTest |
| with InteractiveFormsExperimentMixin { |
| final source = ''' |
| ^import 'package:test/main.dart'; |
| '''; |
| |
| @override |
| String get refactoringCommandId => AddImportPrefix.commandName; |
| |
| String get refactoringTitle => AddImportPrefix.constTitle; |
| |
| @override |
| void setUp() { |
| super.setUp(); |
| |
| // Most of the tests here assume we support string. Tests that do not will |
| // explicitly unset this. |
| setSupportedInteractiveFormInputKinds({'string'}); |
| } |
| |
| Future<void> test_acceptsValidAnswers() async { |
| addTestSource(source); |
| |
| await initializeServer(); |
| var action = await expectCodeActionWithTitle(refactoringTitle); |
| var command = action.asCommand; |
| |
| // Resolve the command, which should add the outstanding form fields. |
| var resolvedCommand = await resolveCommand( |
| ExecuteCommandParams( |
| command: command.command, |
| arguments: command.arguments, |
| ), |
| ); |
| |
| // Resolve again, with a valid answer. |
| expect(resolvedCommand.formFields, hasLength(1)); |
| var field = resolvedCommand.formFields!.single; |
| resolvedCommand = await resolveCommand( |
| InteractiveExecuteCommandParams( |
| command: resolvedCommand.command, |
| arguments: resolvedCommand.arguments, |
| formFields: resolvedCommand.formFields, |
| formAnswers: [field.answer('custom_prefix')], |
| ), |
| ); |
| |
| // Check that the form field has gone, and that the valid answer was moved |
| // into the command arguments. |
| expect(resolvedCommand.formFields, isNull); |
| var arguments = getRefactorCommandArguments(resolvedCommand.arguments); |
| expect(arguments, hasLength(1)); |
| expect(arguments.single, 'custom_prefix'); |
| } |
| |
| Future<void> test_formFields_notSupported() async { |
| // If we don't support the 'string' kind, then we shouldn't get back any |
| // form fields, only the default value in the arguments. |
| setSupportedInteractiveFormInputKinds({'file'}); |
| |
| addTestSource(source); |
| |
| await initializeServer(); |
| var action = await expectCodeActionWithTitle(refactoringTitle); |
| var command = action.asCommand; |
| |
| // Resolve the command, which would normally add the form fields, but the |
| // only one used here is not supported by us. |
| var resolvedCommand = await resolveCommand( |
| ExecuteCommandParams( |
| command: command.command, |
| arguments: command.arguments, |
| ), |
| ); |
| |
| // Check basics. |
| expect(resolvedCommand.command, command.command); |
| expect(resolvedCommand.formAnswers, isNull); |
| expect(resolvedCommand.formFields, isNull); |
| |
| // Check the arguments to the command contain the default value so the |
| // command will still work without the user prompt. |
| var refactorArguments = getRefactorCommandArguments( |
| resolvedCommand.arguments, |
| ); |
| expect(refactorArguments, hasLength(1)); |
| expect(refactorArguments.single, 'main'); |
| } |
| |
| Future<void> test_formFields_supported() async { |
| addTestSource(source); |
| |
| await initializeServer(); |
| var action = await expectCodeActionWithTitle(refactoringTitle); |
| var command = action.asCommand; |
| |
| // Resolve the command, which should add the outstanding form fields. |
| var resolvedCommand = await resolveCommand( |
| ExecuteCommandParams( |
| command: command.command, |
| arguments: command.arguments, |
| ), |
| ); |
| |
| // Check basics. |
| expect(resolvedCommand.command, command.command); |
| expect(resolvedCommand.formAnswers, isNull); |
| expect(resolvedCommand.formFields, hasLength(1)); |
| |
| // Because this argument is optional (because we only expect it when using |
| // Interactive Forms but the refactor can be used without), args are |
| // populated with the default during resolve. |
| var arguments = getRefactorCommandArguments(resolvedCommand.arguments); |
| expect(arguments, hasLength(1)); |
| expect(arguments.single, 'main'); |
| |
| // Check the form field is what we'd expect. |
| var field = resolvedCommand.formFields!.single; |
| expect(field.type.kind, 'string'); |
| expect(field.description, 'Import Prefix'); |
| expect(field.defaultValue, 'main'); |
| expect(field.error, isNull); |
| } |
| |
| Future<void> test_validates_invalidValue() async { |
| addTestSource(source); |
| |
| await initializeServer(); |
| var action = await expectCodeActionWithTitle(refactoringTitle); |
| var command = action.asCommand; |
| |
| // Resolve the command, which should add the outstanding form fields. |
| var resolvedCommand = await resolveCommand( |
| ExecuteCommandParams( |
| command: command.command, |
| arguments: command.arguments, |
| ), |
| ); |
| |
| // Resolve again, with an invalid prefix name. |
| expect(resolvedCommand.formFields, hasLength(1)); |
| var field = resolvedCommand.formFields!.single; |
| resolvedCommand = await resolveCommand( |
| InteractiveExecuteCommandParams( |
| command: resolvedCommand.command, |
| arguments: resolvedCommand.arguments, |
| formFields: resolvedCommand.formFields, |
| formAnswers: [field.answer('Invalid Prefix')], |
| ), |
| ); |
| |
| // Check the field has a validation error and the current value is |
| // preserved. |
| expect(resolvedCommand.formFields, hasLength(1)); |
| field = resolvedCommand.formFields!.single; |
| expect(field.error, "Import prefix name must not contain ' '."); |
| expect(resolvedCommand.formAnswers!.single, field.answer('Invalid Prefix')); |
| |
| // Resolve again with a valid value and ensure the field is completed. |
| resolvedCommand = await resolveCommand( |
| InteractiveExecuteCommandParams( |
| command: resolvedCommand.command, |
| arguments: resolvedCommand.arguments, |
| formFields: resolvedCommand.formFields, |
| formAnswers: [field.answer('valid_prefix')], |
| ), |
| ); |
| |
| expect(resolvedCommand.formFields, isNull); |
| var arguments = getRefactorCommandArguments(resolvedCommand.arguments); |
| expect(arguments, hasLength(1)); |
| expect(arguments.single, 'valid_prefix'); |
| } |
| } |
| |
| @reflectiveTest |
| class CommandResolveTest extends AbstractLspAnalysisServerTest { |
| Future<void> test_returnsInputForUnknownCommand() async { |
| // Basic command that only includes the normal fields from |
| // ExecuteCommandParams. This ensures calling resolve() for commands that |
| // don't need form input will return the same result (with no formFields). |
| var command = InteractiveExecuteCommandParams( |
| command: 'my_unknown_command', |
| arguments: [1, 'two'], |
| ); |
| |
| await initialize(); |
| |
| var resolvedCommand = await resolveCommand(command); |
| |
| expect(resolvedCommand, command); |
| } |
| } |
| |
| /// A temporary mixin that sets the flag to enable the Interactive Forms |
| /// experiment setting. |
| mixin InteractiveFormsExperimentMixin on RefactoringTest { |
| @override |
| Future<void> initializeServer({ |
| bool experimentalOptInFlag = true, |
| // We default this to true for these tests, though it's false in the |
| // the super implementation. |
| bool experimentalInteractiveForms = true, |
| }) { |
| return super.initializeServer( |
| experimentalOptInFlag: experimentalOptInFlag, |
| experimentalInteractiveForms: experimentalInteractiveForms, |
| ); |
| } |
| } |