blob: 994eb8ce7c5b612ed0740959b8609d1077cb0a2f [file] [edit]
// 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,
);
}
}