[analysis_server] Add imports for test/group snippets

Fixes https://github.com/Dart-Code/Dart-Code/issues/5421

Change-Id: I70b1bac0e86117c19324d482ba993f950b5be931
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/411002
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/services/snippets/dart/test_definition.dart b/pkg/analysis_server/lib/src/services/snippets/dart/test_definition.dart
index 9d702ff..c0721f8 100644
--- a/pkg/analysis_server/lib/src/services/snippets/dart/test_definition.dart
+++ b/pkg/analysis_server/lib/src/services/snippets/dart/test_definition.dart
@@ -4,10 +4,13 @@
 
 import 'package:analysis_server/src/services/snippets/snippet.dart';
 import 'package:analysis_server/src/services/snippets/snippet_producer.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:analyzer/src/dart/analysis/session_helper.dart';
 import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
+import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
 
 /// Produces a [Snippet] that creates a `test()` block.
-class TestDefinition extends DartSnippetProducer {
+class TestDefinition extends DartSnippetProducer with TestSnippetMixin {
   static const prefix = 'test';
   static const label = 'test';
 
@@ -21,7 +24,8 @@
     var builder = ChangeBuilder(session: request.analysisSession);
     var indent = utils.getLinePrefix(request.offset);
 
-    await builder.addDartFileEdit(request.filePath, (builder) {
+    await builder.addDartFileEdit(request.filePath, (builder) async {
+      await addRequiredImports(builder);
       builder.addReplacement(request.replacementRange, (builder) {
         void writeIndented(String string) => builder.write('$indent$string');
         builder.write("test('");
@@ -46,3 +50,35 @@
     return isInTestDirectory;
   }
 }
+
+mixin TestSnippetMixin {
+  final _flutterTestUri = Uri.parse('package:flutter_test/flutter_test.dart');
+
+  final _dartTestUri = Uri.parse('package:test/test.dart');
+
+  AnalysisSessionHelper get sessionHelper;
+
+  /// Adds imports for `test`/`group` if required.
+  ///
+  /// Both 'package:test' and 'package:flutter_test' are checked because Flutter
+  /// projects can use either and we don't want to add the other.
+  Future<void> addRequiredImports(DartFileEditBuilder builder) async {
+    if (builder.importsLibrary(_dartTestUri) ||
+        builder.importsLibrary(_flutterTestUri)) {
+      return;
+    }
+
+    var testUri = await getTestLibraryUri();
+    builder.importLibrary(testUri);
+  }
+
+  /// Gets the URI for the test library to import depending on whether
+  /// flutter_test is available or not.
+  Future<Uri> getTestLibraryUri() async {
+    var flutterTest = await sessionHelper.session.getLibraryByUri(
+      _flutterTestUri.toString(),
+    );
+
+    return flutterTest is LibraryElementResult ? _flutterTestUri : _dartTestUri;
+  }
+}
diff --git a/pkg/analysis_server/lib/src/services/snippets/dart/test_group_definition.dart b/pkg/analysis_server/lib/src/services/snippets/dart/test_group_definition.dart
index 16e29e2..3bf471a 100644
--- a/pkg/analysis_server/lib/src/services/snippets/dart/test_group_definition.dart
+++ b/pkg/analysis_server/lib/src/services/snippets/dart/test_group_definition.dart
@@ -2,12 +2,13 @@
 // 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/snippets/dart/test_definition.dart';
 import 'package:analysis_server/src/services/snippets/snippet.dart';
 import 'package:analysis_server/src/services/snippets/snippet_producer.dart';
 import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
 
 /// Produces a [Snippet] that creates a `group()` block.
-class TestGroupDefinition extends DartSnippetProducer {
+class TestGroupDefinition extends DartSnippetProducer with TestSnippetMixin {
   static const prefix = 'group';
   static const label = 'group';
 
@@ -21,7 +22,8 @@
     var builder = ChangeBuilder(session: request.analysisSession);
     var indent = utils.getLinePrefix(request.offset);
 
-    await builder.addDartFileEdit(request.filePath, (builder) {
+    await builder.addDartFileEdit(request.filePath, (builder) async {
+      await addRequiredImports(builder);
       builder.addReplacement(request.replacementRange, (builder) {
         void writeIndented(String string) => builder.write('$indent$string');
         builder.write("group('");
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index 2c54c89..a9ab5d7 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -4596,6 +4596,8 @@
     );
 
     expect(updated, r'''
+import 'package:test/test.dart';
+
 void f() {
   test('${1:test name}', () {
     $0
@@ -4620,6 +4622,8 @@
     );
 
     expect(updated, r'''
+import 'package:test/test.dart';
+
 void f() {
   group('${1:group name}', () {
     $0
diff --git a/pkg/analysis_server/test/services/snippets/dart/test_definition_test.dart b/pkg/analysis_server/test/services/snippets/dart/test_definition_test.dart
index 07e1668..fc24473 100644
--- a/pkg/analysis_server/test/services/snippets/dart/test_definition_test.dart
+++ b/pkg/analysis_server/test/services/snippets/dart/test_definition_test.dart
@@ -2,7 +2,6 @@
 // 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/protocol_server.dart';
 import 'package:analysis_server/src/services/snippets/dart/test_definition.dart';
 import 'package:analyzer/src/test_utilities/test_code_format.dart';
 import 'package:test/test.dart';
@@ -27,6 +26,117 @@
   @override
   String get prefix => TestDefinition.prefix;
 
+  Future<void> test_import_dart() async {
+    testFilePath = convertPath('$testPackageLibPath/test/foo_test.dart');
+    var code = TestCode.parse(r'''
+void f() {
+  test^
+}
+''');
+    var snippet = await expectValidSnippet(code);
+    var result = applySnippet(code, snippet);
+    expect(result, '''
+import 'package:test/test.dart';
+
+void f() {
+  test('test name', () {
+    
+  });
+}
+''');
+  }
+
+  Future<void> test_import_dart_existing() async {
+    testFilePath = convertPath('$testPackageLibPath/test/foo_test.dart');
+    var code = TestCode.parse(r'''
+import 'package:test/test.dart';
+
+void f() {
+  test^
+}
+''');
+    var snippet = await expectValidSnippet(code);
+    var result = applySnippet(code, snippet);
+    expect(result, '''
+import 'package:test/test.dart';
+
+void f() {
+  test('test name', () {
+    
+  });
+}
+''');
+  }
+
+  Future<void> test_import_flutter() async {
+    writeTestPackageConfig(flutter_test: true);
+    testFilePath = convertPath('$testPackageLibPath/test/foo_test.dart');
+    var code = TestCode.parse(r'''
+void f() {
+  test^
+}
+''');
+    var snippet = await expectValidSnippet(code);
+    var result = applySnippet(code, snippet);
+    expect(result, '''
+import 'package:flutter_test/flutter_test.dart';
+
+void f() {
+  test('test name', () {
+    
+  });
+}
+''');
+  }
+
+  Future<void> test_import_flutter_existing() async {
+    writeTestPackageConfig(flutter_test: true);
+    testFilePath = convertPath('$testPackageLibPath/test/foo_test.dart');
+    var code = TestCode.parse(r'''
+import 'package:flutter_test/flutter_test.dart';
+
+void f() {
+  test^
+}
+''');
+    var snippet = await expectValidSnippet(code);
+    var result = applySnippet(code, snippet);
+    expect(result, '''
+import 'package:flutter_test/flutter_test.dart';
+
+void f() {
+  test('test name', () {
+    
+  });
+}
+''');
+  }
+
+  /// Ensure we don't import package:flutter_test if package:test is already
+  /// imported.
+  Future<void> test_import_flutter_existingDart() async {
+    writeTestPackageConfig(flutter_test: true);
+    testFilePath = convertPath('$testPackageLibPath/test/foo_test.dart');
+    var code = TestCode.parse(r'''
+import 'package:test/test.dart';
+
+void f() {
+  test^
+}
+''');
+    var snippet = await expectValidSnippet(code);
+    var result = applySnippet(code, snippet);
+    expect(result, '''
+import 'package:test/test.dart';
+
+void f() {
+  test('test name', () {
+    
+  });
+}
+''');
+  }
+
   Future<void> test_inTestFile() async {
     testFilePath = convertPath('$testPackageLibPath/test/foo_test.dart');
     var code = TestCode.parse(r'''
@@ -38,11 +148,10 @@
     expect(snippet.prefix, prefix);
     expect(snippet.label, label);
     expect(snippet.change.edits, hasLength(1));
-    var result = code.code;
-    for (var edit in snippet.change.edits) {
-      result = SourceEdit.applySequence(result, edit.edits);
-    }
+    var result = applySnippet(code, snippet);
     expect(result, '''
+import 'package:test/test.dart';
+
 void f() {
   test('test name', () {
     
@@ -50,11 +159,11 @@
 }
 ''');
     expect(snippet.change.selection!.file, testFile.path);
-    expect(snippet.change.selection!.offset, 40);
+    expect(snippet.change.selection!.offset, 74);
     expect(snippet.change.linkedEditGroups.map((group) => group.toJson()), [
       {
         'positions': [
-          {'file': testFile.path, 'offset': 19},
+          {'file': testFile.path, 'offset': 53},
         ],
         'length': 9,
         'suggestions': [],
diff --git a/pkg/analysis_server/test/services/snippets/dart/test_group_definition_test.dart b/pkg/analysis_server/test/services/snippets/dart/test_group_definition_test.dart
index d9396d3..794c3bc 100644
--- a/pkg/analysis_server/test/services/snippets/dart/test_group_definition_test.dart
+++ b/pkg/analysis_server/test/services/snippets/dart/test_group_definition_test.dart
@@ -2,7 +2,6 @@
 // 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/protocol_server.dart';
 import 'package:analysis_server/src/services/snippets/dart/test_group_definition.dart';
 import 'package:analyzer/src/test_utilities/test_code_format.dart';
 import 'package:test/test.dart';
@@ -27,6 +26,117 @@
   @override
   String get prefix => TestGroupDefinition.prefix;
 
+  Future<void> test_import_dart() async {
+    testFilePath = convertPath('$testPackageLibPath/test/foo_test.dart');
+    var code = TestCode.parse(r'''
+void f() {
+  group^
+}
+''');
+    var snippet = await expectValidSnippet(code);
+    var result = applySnippet(code, snippet);
+    expect(result, '''
+import 'package:test/test.dart';
+
+void f() {
+  group('group name', () {
+    
+  });
+}
+''');
+  }
+
+  Future<void> test_import_dart_existing() async {
+    testFilePath = convertPath('$testPackageLibPath/test/foo_test.dart');
+    var code = TestCode.parse(r'''
+import 'package:test/test.dart';
+
+void f() {
+  group^
+}
+''');
+    var snippet = await expectValidSnippet(code);
+    var result = applySnippet(code, snippet);
+    expect(result, '''
+import 'package:test/test.dart';
+
+void f() {
+  group('group name', () {
+    
+  });
+}
+''');
+  }
+
+  Future<void> test_import_flutter() async {
+    writeTestPackageConfig(flutter_test: true);
+    testFilePath = convertPath('$testPackageLibPath/test/foo_test.dart');
+    var code = TestCode.parse(r'''
+void f() {
+  group^
+}
+''');
+    var snippet = await expectValidSnippet(code);
+    var result = applySnippet(code, snippet);
+    expect(result, '''
+import 'package:flutter_test/flutter_test.dart';
+
+void f() {
+  group('group name', () {
+    
+  });
+}
+''');
+  }
+
+  Future<void> test_import_flutter_existing() async {
+    writeTestPackageConfig(flutter_test: true);
+    testFilePath = convertPath('$testPackageLibPath/test/foo_test.dart');
+    var code = TestCode.parse(r'''
+import 'package:flutter_test/flutter_test.dart';
+
+void f() {
+  group^
+}
+''');
+    var snippet = await expectValidSnippet(code);
+    var result = applySnippet(code, snippet);
+    expect(result, '''
+import 'package:flutter_test/flutter_test.dart';
+
+void f() {
+  group('group name', () {
+    
+  });
+}
+''');
+  }
+
+  /// Ensure we don't import package:flutter_test if package:test is already
+  /// imported.
+  Future<void> test_import_flutter_existingDart() async {
+    writeTestPackageConfig(flutter_test: true);
+    testFilePath = convertPath('$testPackageLibPath/test/foo_test.dart');
+    var code = TestCode.parse(r'''
+import 'package:test/test.dart';
+
+void f() {
+  group^
+}
+''');
+    var snippet = await expectValidSnippet(code);
+    var result = applySnippet(code, snippet);
+    expect(result, '''
+import 'package:test/test.dart';
+
+void f() {
+  group('group name', () {
+    
+  });
+}
+''');
+  }
+
   Future<void> test_inTestFile() async {
     testFilePath = convertPath('$testPackageLibPath/test/foo_test.dart');
     var code = TestCode.parse(r'''
@@ -38,11 +148,10 @@
     expect(snippet.prefix, prefix);
     expect(snippet.label, label);
     expect(snippet.change.edits, hasLength(1));
-    var result = code.code;
-    for (var edit in snippet.change.edits) {
-      result = SourceEdit.applySequence(result, edit.edits);
-    }
+    var result = applySnippet(code, snippet);
     expect(result, '''
+import 'package:test/test.dart';
+
 void f() {
   group('group name', () {
     
@@ -50,11 +159,11 @@
 }
 ''');
     expect(snippet.change.selection!.file, testFile.path);
-    expect(snippet.change.selection!.offset, 42);
+    expect(snippet.change.selection!.offset, 76);
     expect(snippet.change.linkedEditGroups.map((group) => group.toJson()), [
       {
         'positions': [
-          {'file': testFile.path, 'offset': 20},
+          {'file': testFile.path, 'offset': 54},
         ],
         'length': 10,
         'suggestions': [],
diff --git a/pkg/analysis_server/test/services/snippets/dart/test_support.dart b/pkg/analysis_server/test/services/snippets/dart/test_support.dart
index a147507..165262a 100644
--- a/pkg/analysis_server/test/services/snippets/dart/test_support.dart
+++ b/pkg/analysis_server/test/services/snippets/dart/test_support.dart
@@ -24,6 +24,14 @@
   @override
   bool get verifyNoTestUnitErrors => false;
 
+  String applySnippet(TestCode code, Snippet snippet) {
+    var result = code.code;
+    for (var edit in snippet.change.edits) {
+      result = SourceEdit.applySequence(result, edit.edits);
+    }
+    return result;
+  }
+
   Future<void> assertSnippet(String content, String expected) async {
     var code = TestCode.parse(content);
     var expectedCode = TestCode.parse(expected);
diff --git a/pkg/analysis_server/test/support/configuration_files.dart b/pkg/analysis_server/test/support/configuration_files.dart
index d974b34..5edb780 100644
--- a/pkg/analysis_server/test/support/configuration_files.dart
+++ b/pkg/analysis_server/test/support/configuration_files.dart
@@ -39,6 +39,7 @@
     PackageConfigFileBuilder? config,
     String? languageVersion,
     bool flutter = false,
+    bool flutter_test = false,
     bool meta = false,
     bool pedantic = false,
     bool vector_math = false,
@@ -75,6 +76,11 @@
       }
     }
 
+    if (flutter_test) {
+      var libFolder = addFlutterTest();
+      config.add(name: 'flutter_test', rootPath: libFolder.parent.path);
+    }
+
     if (pedantic) {
       var libFolder = addPedantic();
       config.add(name: 'pedantic', rootPath: libFolder.parent.path);
@@ -117,6 +123,7 @@
     PackageConfigFileBuilder? config,
     String? languageVersion,
     bool flutter = false,
+    bool flutter_test = false,
     bool meta = false,
     bool pedantic = false,
     bool vector_math = false,
@@ -128,6 +135,7 @@
       languageVersion: languageVersion,
       packageName: 'test',
       flutter: flutter,
+      flutter_test: flutter_test,
       meta: meta,
       pedantic: pedantic,
       vector_math: vector_math,
diff --git a/pkg/analyzer_utilities/lib/test/mock_packages/mock_packages.dart b/pkg/analyzer_utilities/lib/test/mock_packages/mock_packages.dart
index b76403f..298d26e 100644
--- a/pkg/analyzer_utilities/lib/test/mock_packages/mock_packages.dart
+++ b/pkg/analyzer_utilities/lib/test/mock_packages/mock_packages.dart
@@ -114,6 +114,11 @@
     return packageFolder.getChildAssumingFolder('lib');
   }
 
+  Folder addFlutterTest() {
+    var packageFolder = _addFiles('flutter_test');
+    return packageFolder.getChildAssumingFolder('lib');
+  }
+
   Folder addJs() {
     var packageFolder = _addFiles('js');
     return packageFolder.getChildAssumingFolder('lib');
diff --git a/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter_test/lib/flutter_test.dart b/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter_test/lib/flutter_test.dart
new file mode 100644
index 0000000..bac6458
--- /dev/null
+++ b/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter_test/lib/flutter_test.dart
@@ -0,0 +1,14 @@
+// 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.
+
+void test(Object description, dynamic Function() body) {}
+
+void group(Object description, void Function() body) {}
+
+void main() {
+  // Because this file is called 'flutter_test.dart' and is inside the 'test'
+  // folder, it will be considered a test suite. To avoid it failing the bots
+  // with "Invoked Dart programs must have a 'main' function defined", provide
+  // an empty main function.
+}
diff --git a/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter_test/pubspec.yaml b/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter_test/pubspec.yaml
new file mode 100644
index 0000000..ee363fb
--- /dev/null
+++ b/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter_test/pubspec.yaml
@@ -0,0 +1 @@
+name: flutter_test