diff --git a/dev/integration_tests/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart b/dev/integration_tests/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart index def881bd73a03..1d8cacc43d3ee 100644 --- a/dev/integration_tests/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart +++ b/dev/integration_tests/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart @@ -5,3 +5,4 @@ const String kWidgetPreviewDtdUri = ''; const String kWidgetPreviewService = 'widget-preview'; const String kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold'; +const String kProjectRootPath = r''; diff --git a/dev/integration_tests/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart b/dev/integration_tests/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart index d91e39e752138..224fb37570e54 100644 --- a/dev/integration_tests/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart +++ b/dev/integration_tests/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/widgets.dart'; +import 'package:widget_preview_scaffold/src/dtd/dtd_connection_info.dart'; import 'package:widget_preview_scaffold/src/dtd/dtd_services.dart'; import 'package:widget_preview_scaffold/src/dtd/editor_service.dart'; import 'package:widget_preview_scaffold/src/widget_preview_rendering.dart'; @@ -16,6 +17,7 @@ import 'package:widget_preview_scaffold/src/widget_preview_rendering.dart'; class WidgetPreviewScaffoldInspectorService with WidgetInspectorService { WidgetPreviewScaffoldInspectorService({required this.dtdServices}) { WidgetInspectorService.instance = this; + addPubRootDirectories([kProjectRootPath]); } /// The DTD services instance used to communicate with the tool. diff --git a/packages/flutter_tools/lib/src/commands/widget_preview.dart b/packages/flutter_tools/lib/src/commands/widget_preview.dart index ccc79c68f0991..113923544c1cc 100644 --- a/packages/flutter_tools/lib/src/commands/widget_preview.dart +++ b/packages/flutter_tools/lib/src/commands/widget_preview.dart @@ -28,6 +28,7 @@ import '../device.dart'; import '../features.dart'; import '../globals.dart' as globals; import '../isolated/resident_web_runner.dart'; +import '../migrations/widget_preview_gitignore_migration.dart'; import '../project.dart'; import '../resident_runner.dart'; import '../runner/flutter_command.dart'; @@ -322,6 +323,8 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C previewAnalytics.initializeLaunchStopwatch(); logger.sendInitializingEvent(); + await WidgetPreviewGitignoreMigration(rootProject, logger).migrate(); + final String? customPreviewScaffoldOutput = stringArg(kWidgetPreviewScaffoldOutputDir); widgetPreviewScaffold = customPreviewScaffoldOutput != null ? fs.directory(customPreviewScaffoldOutput) @@ -414,6 +417,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C dtdUri: _dtdService.dtdUri!, widgetPreviewServiceName: _dtdService.widgetPreviewService, widgetPreviewScaffoldStreamName: _dtdService.widgetPreviewScaffoldStream, + projectRootPath: rootProject.directory.absolute.path, ); final FlutterWidgetPreviews originalPreviews = await _dtdService.getFlutterWidgetPreviews(); diff --git a/packages/flutter_tools/lib/src/migrations/widget_preview_gitignore_migration.dart b/packages/flutter_tools/lib/src/migrations/widget_preview_gitignore_migration.dart new file mode 100644 index 0000000000000..ce664328c0809 --- /dev/null +++ b/packages/flutter_tools/lib/src/migrations/widget_preview_gitignore_migration.dart @@ -0,0 +1,42 @@ +// 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 '../base/file_system.dart'; +import '../base/project_migrator.dart'; +import '../project.dart'; + +/// Adds `.widget_preview/` to the .gitignore file. +class WidgetPreviewGitignoreMigration extends ProjectMigrator { + WidgetPreviewGitignoreMigration(FlutterProject project, super.logger) + : _gitignoreFile = project.gitignoreFile; + + final File _gitignoreFile; + + @override + Future migrate() async { + if (!_gitignoreFile.existsSync()) { + logger.printTrace('.gitignore file not found, skipping widget preview .gitignore migration.'); + return; + } + + final String originalContent = _gitignoreFile.readAsStringSync(); + + // Skip if .gitignore is already migrated. + if (originalContent.contains('.widget_preview/')) { + return; + } + + logger.printTrace('.gitignore does not ignore .widget_preview/ directory, updating.'); + + final newContent = StringBuffer(originalContent); + if (!originalContent.endsWith('\n')) { + newContent.writeln(); + } + newContent + ..writeln('# Widget Preview related') + ..writeln('.widget_preview/'); + + _gitignoreFile.writeAsStringSync(newContent.toString()); + } +} diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index bc02734da00e0..ef92e6a3453e3 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -249,10 +249,7 @@ class FlutterProject { /// The location of the generated scaffolding project for hosting widget /// previews from this project. - // TODO(bkonyi): don't create this project in $TMP. - // See https://github.com/flutter/flutter/issues/179036 - late final Directory widgetPreviewScaffold = directory.fileSystem.systemTempDirectory - .createTempSync('widget_preview_scaffold'); + late final Directory widgetPreviewScaffold = directory.childDirectory('.widget_preview'); /// The directory containing the generated code for this project. Directory get generated => directory.absolute diff --git a/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart b/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart index 4b75c144d0cea..ab0d7f6e74d12 100644 --- a/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart +++ b/packages/flutter_tools/lib/src/widget_preview/lsp_preview_detector.dart @@ -168,7 +168,15 @@ class LspPreviewDetector { // Only process one FileSystemEntity at a time so we don't invalidate an AnalysisSession that's // in use when we call context.changeFile(...). await mutex.runGuarded(() async { - await _fileAddedOrUpdated(filePath: event.path); + final String eventPath = event.path; + // Ignore any files under .dart_tool, .widget_preview, or ephemeral directories created by + // the tool (e.g., build/, plugin directories, etc.). + if (eventPath.doesContainDartTool || + eventPath.doesContainWidgetPreview || + project.ephemeralDirectories.any((dir) => eventPath.contains(dir.path))) { + return; + } + await _fileAddedOrUpdated(filePath: eventPath); }); } diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart b/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart index 23f7308c2de7f..d37eec27b13d2 100644 --- a/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart +++ b/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart @@ -60,6 +60,7 @@ class PreviewCodeGenerator { required Uri dtdUri, required String widgetPreviewServiceName, required String widgetPreviewScaffoldStreamName, + required String projectRootPath, }) { final emitter = cb.DartEmitter.scoped(useNullSafetySyntax: true); final lib = cb.Library( @@ -87,6 +88,13 @@ class PreviewCodeGenerator { ..type = cb.refer('String') ..assignment = cb.literalString(widgetPreviewScaffoldStreamName).code; }), + cb.Field((b) { + b + ..name = 'kProjectRootPath' + ..modifier = cb.FieldModifier.constant + ..type = cb.refer('String') + ..assignment = cb.literalString(projectRootPath, raw: true).code; + }), ]), ); final File generatedDtdConnectionInfoFile = fs.file( diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart b/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart index eb0ff03eda499..72d6df081962e 100644 --- a/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart +++ b/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart @@ -154,9 +154,10 @@ class PreviewDetector { onPackageConfigChangeDetected?.call(event.path); return; } - // Ignore any files under .dart_tool or ephemeral directories created by + // Ignore any files under .dart_tool, .widget_preview, or ephemeral directories created by // the tool (e.g., build/, plugin directories, etc.). if (eventPath.doesContainDartTool || + eventPath.doesContainWidgetPreview || project.ephemeralDirectories.any((dir) => eventPath.contains(dir.path))) { return; } diff --git a/packages/flutter_tools/lib/src/widget_preview/utils.dart b/packages/flutter_tools/lib/src/widget_preview/utils.dart index 60fe3398b4795..a1374e84cbe71 100644 --- a/packages/flutter_tools/lib/src/widget_preview/utils.dart +++ b/packages/flutter_tools/lib/src/widget_preview/utils.dart @@ -61,9 +61,12 @@ extension AnnotationExtension on Annotation { /// Convenience getters for examining [String] paths. extension StringExtension on String { + static final RegExp _pathSeparator = RegExp(r'[/\\]'); + bool get isDartFile => endsWith('.dart'); bool get isPubspec => endsWith('pubspec.yaml'); bool get doesContainDartTool => contains('.dart_tool'); + bool get doesContainWidgetPreview => split(_pathSeparator).contains('.widget_preview'); } extension LibraryElementExtension on LibraryElement { diff --git a/packages/flutter_tools/templates/app/.gitignore.tmpl b/packages/flutter_tools/templates/app/.gitignore.tmpl index 3820a95c65c3e..79f7ecabd5c79 100644 --- a/packages/flutter_tools/templates/app/.gitignore.tmpl +++ b/packages/flutter_tools/templates/app/.gitignore.tmpl @@ -43,3 +43,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Widget Preview related +.widget_preview/ diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl index def881bd73a03..1d8cacc43d3ee 100644 --- a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl +++ b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd/dtd_connection_info.dart.tmpl @@ -5,3 +5,4 @@ const String kWidgetPreviewDtdUri = ''; const String kWidgetPreviewService = 'widget-preview'; const String kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold'; +const String kProjectRootPath = r''; diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart.tmpl index d91e39e752138..224fb37570e54 100644 --- a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart.tmpl +++ b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_inspector_service.dart.tmpl @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/widgets.dart'; +import 'package:widget_preview_scaffold/src/dtd/dtd_connection_info.dart'; import 'package:widget_preview_scaffold/src/dtd/dtd_services.dart'; import 'package:widget_preview_scaffold/src/dtd/editor_service.dart'; import 'package:widget_preview_scaffold/src/widget_preview_rendering.dart'; @@ -16,6 +17,7 @@ import 'package:widget_preview_scaffold/src/widget_preview_rendering.dart'; class WidgetPreviewScaffoldInspectorService with WidgetInspectorService { WidgetPreviewScaffoldInspectorService({required this.dtdServices}) { WidgetInspectorService.instance = this; + addPubRootDirectories([kProjectRootPath]); } /// The DTD services instance used to communicate with the tool. diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart index d8f5fc422e64f..b5b3f64dd19f9 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart @@ -463,6 +463,7 @@ List<_i1.WidgetPreview> previews() => []; dtdUri: dtdUri, widgetPreviewServiceName: 'widget-preview-service', widgetPreviewScaffoldStreamName: 'widget-preview-stream', + projectRootPath: project.directory.absolute.path, ); final expectedDtdConnectionInfo = @@ -472,6 +473,8 @@ List<_i1.WidgetPreview> previews() => []; const String kWidgetPreviewDtdUri = '$dtdUri'; const String kWidgetPreviewService = 'widget-preview-service'; const String kWidgetPreviewScaffoldStream = 'widget-preview-stream'; +const String kProjectRootPath = + r'${project.directory.absolute.path}'; '''; expect(generatedDtdConnectionInfoFile.readAsStringSync(), expectedDtdConnectionInfo); }, diff --git a/packages/flutter_tools/test/general.shard/migrations/widget_preview_gitignore_migration_test.dart b/packages/flutter_tools/test/general.shard/migrations/widget_preview_gitignore_migration_test.dart new file mode 100644 index 0000000000000..7fd065c3e99d2 --- /dev/null +++ b/packages/flutter_tools/test/general.shard/migrations/widget_preview_gitignore_migration_test.dart @@ -0,0 +1,95 @@ +// 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 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/terminal.dart'; +import 'package:flutter_tools/src/migrations/widget_preview_gitignore_migration.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:test/fake.dart'; + +import '../../src/common.dart'; + +void main() { + group('Widget Preview .gitignore migration', () { + late MemoryFileSystem memoryFileSystem; + late BufferLogger testLogger; + late FakeFlutterProject mockProject; + late File gitignoreFile; + + setUp(() { + memoryFileSystem = MemoryFileSystem.test(); + gitignoreFile = memoryFileSystem.file('.gitignore'); + + testLogger = BufferLogger( + terminal: Terminal.test(), + outputPreferences: OutputPreferences.test(), + ); + + mockProject = FakeFlutterProject(fileSystem: memoryFileSystem); + }); + + testWithoutContext('skipped if .gitignore file is missing', () async { + final migration = WidgetPreviewGitignoreMigration(mockProject, testLogger); + await migration.migrate(); + expect(gitignoreFile.existsSync(), isFalse); + + expect( + testLogger.traceText, + contains('.gitignore file not found, skipping widget preview .gitignore migration.'), + ); + expect(testLogger.warningText, isEmpty); + }); + + testWithoutContext('skipped if already migrated', () async { + const gitignoreFileContents = ''' +.DS_Store +.widget_preview/ +'''; + + gitignoreFile.writeAsStringSync(gitignoreFileContents); + + final DateTime updatedAt = gitignoreFile.lastModifiedSync(); + + final migration = WidgetPreviewGitignoreMigration(mockProject, testLogger); + await migration.migrate(); + + expect(gitignoreFile.lastModifiedSync(), updatedAt); + expect(gitignoreFile.readAsStringSync(), gitignoreFileContents); + expect(testLogger.warningText, isEmpty); + }); + + testWithoutContext('migrates project to ignore .widget_preview directory', () async { + gitignoreFile.writeAsStringSync( + '.DS_Store\n' + '.atom/\n', + ); + + final migration = WidgetPreviewGitignoreMigration(mockProject, testLogger); + await migration.migrate(); + + expect( + gitignoreFile.readAsStringSync(), + '.DS_Store\n' + '.atom/\n' + '# Widget Preview related\n' + '.widget_preview/\n', + ); + + expect( + testLogger.traceText, + contains('.gitignore does not ignore .widget_preview/ directory, updating.'), + ); + }); + }); +} + +class FakeFlutterProject extends Fake implements FlutterProject { + FakeFlutterProject({required MemoryFileSystem fileSystem}) + : gitignoreFile = fileSystem.file('.gitignore'); + + @override + File gitignoreFile; +} diff --git a/packages/flutter_tools/test/integration.shard/test_data/basic_project.dart b/packages/flutter_tools/test/integration.shard/test_data/basic_project.dart index fd0afb93792cd..8bb8583c01ff2 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/basic_project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/basic_project.dart @@ -5,9 +5,12 @@ import 'project.dart'; class BasicProject extends Project { + BasicProject({super.name}); + @override - final pubspec = ''' - name: test + String get pubspec => + ''' + name: $name environment: sdk: ^3.7.0-0 diff --git a/packages/flutter_tools/test/integration.shard/test_data/project.dart b/packages/flutter_tools/test/integration.shard/test_data/project.dart index 44d2743f332e0..993558604dbbe 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/project.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/project.dart @@ -27,7 +27,9 @@ abstract class Project { /// Creates a flutter Project for testing. /// /// If passed, `indexHtml` is used as the contents of the web/index.html file. - Project({this.indexHtml = _kDefaultHtml}); + Project({this.indexHtml = _kDefaultHtml, String? name}) : name = name ?? 'test'; + + final String name; late Directory dir; @@ -37,7 +39,7 @@ abstract class Project { String get generatedFile => ''; DeferredComponentsConfig? get deferredComponents => null; - Uri get mainDart => Uri.parse('package:test/main.dart'); + Uri get mainDart => Uri.parse('package:$name/main.dart'); /// The contents for the index.html file of this `Project`. /// @@ -67,7 +69,7 @@ abstract class Project { writeFile(fileSystem.path.join(dir.path, 'web', 'index.html'), indexHtml); writeFile(fileSystem.path.join(dir.path, 'web', 'flutter.js'), ''); writeFile(fileSystem.path.join(dir.path, 'web', 'flutter_service_worker.js'), ''); - writePackageConfigFiles(directory: dir, mainLibName: 'test'); + writePackageConfigFiles(directory: dir, mainLibName: name); await getPackages(dir.path); } diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart index 0f3f57fa90ea2..cef80d3cd0732 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_detection_test.dart @@ -21,11 +21,14 @@ void main() { late Directory tempDir; Logger? logger; DtdLauncher? dtdLauncher; - final project = BasicProject(); + late BasicProject project; + var projectCounter = 0; setUp(() async { logger = BufferLogger.test(); tempDir = createResolvedTempDirectorySync('widget_preview_detection_test.'); + projectCounter++; + project = BasicProject(name: 'test_detection_$projectCounter'); await project.setUpIn(tempDir); }); @@ -74,10 +77,7 @@ Widget myNewPreview() => Container(); ); await reloadSub.cancel(); - final DTDResponse result = await dtdConnection.call( - 'Lsp', - 'dart/workspace/getFlutterWidgetPreviews', - ); + final DTDResponse result = await getPreviews(dtdConnection); final FlutterWidgetPreviews previews = FlutterWidgetPreviews.fromJson( result.result['result']! as Map, ); @@ -125,10 +125,7 @@ Widget myRemovePreview() => Container(); ); await initReloadSub.cancel(); - DTDResponse result = await dtdConnection.call( - 'Lsp', - 'dart/workspace/getFlutterWidgetPreviews', - ); + DTDResponse result = await getPreviews(dtdConnection); FlutterWidgetPreviews previews = FlutterWidgetPreviews.fromJson( result.result['result']! as Map, ); @@ -151,7 +148,7 @@ Widget myRemovePreview() => Container(); ); await deleteReloadSub.cancel(); - result = await dtdConnection.call('Lsp', 'dart/workspace/getFlutterWidgetPreviews'); + result = await getPreviews(dtdConnection); previews = FlutterWidgetPreviews.fromJson(result.result['result']! as Map); if (previews.previews.isNotEmpty) { throw StateError('Preview was still detected after deletion!'); @@ -197,10 +194,7 @@ Widget myModifyPreview() => Container(); ); await initReloadSub.cancel(); - DTDResponse result = await dtdConnection.call( - 'Lsp', - 'dart/workspace/getFlutterWidgetPreviews', - ); + DTDResponse result = await getPreviews(dtdConnection); FlutterWidgetPreviews previews = FlutterWidgetPreviews.fromJson( result.result['result']! as Map, ); @@ -229,7 +223,7 @@ Widget myModifyPreview() => Container(); ); await modifyReloadSub.cancel(); - result = await dtdConnection.call('Lsp', 'dart/workspace/getFlutterWidgetPreviews'); + result = await getPreviews(dtdConnection); previews = FlutterWidgetPreviews.fromJson(result.result['result']! as Map); if (previews.previews.isEmpty) { throw StateError('Preview was lost after modification!'); @@ -292,10 +286,7 @@ Widget myPartPreview() => Container(); ); await reloadSub.cancel(); - final DTDResponse result = await dtdConnection.call( - 'Lsp', - 'dart/workspace/getFlutterWidgetPreviews', - ); + final DTDResponse result = await getPreviews(dtdConnection); final FlutterWidgetPreviews previews = FlutterWidgetPreviews.fromJson( result.result['result']! as Map, ); diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart index 2717cf9944d9d..50b51ab240fe2 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_pubspec_test.dart @@ -14,10 +14,13 @@ import 'widget_preview_test_helpers.dart'; void main() { late Directory tempDir; - final project = BasicProject(); + late BasicProject project; + var projectCounter = 0; setUp(() async { tempDir = createResolvedTempDirectorySync('widget_preview_pubspec_test.'); + projectCounter++; + project = BasicProject(name: 'test_pubspec_$projectCounter'); await project.setUpIn(tempDir); }); diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart index ea16c7fd33e37..76af3eacd9a9f 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_smoke_test.dart @@ -26,12 +26,15 @@ void main() { Logger? logger; DtdLauncher? dtdLauncher; DevtoolsServerLauncher? devtoolsLauncher; - final project = BasicProject(); + late BasicProject project; + var projectCounter = 0; const ProcessManager processManager = LocalProcessManager(); setUp(() async { logger = BufferLogger.test(); tempDir = createResolvedTempDirectorySync('widget_preview_smoke_test.'); + projectCounter++; + project = BasicProject(name: 'test_smoke_$projectCounter'); await project.setUpIn(tempDir); }); @@ -56,53 +59,42 @@ void main() { ); }); - testWithoutContext( - 'runs flutter pub get in widget_preview_scaffold if ' - "widget_preview_scaffold/.dart_tool doesn't exist", - () async { - // Regression test for https://github.com/flutter/flutter/issues/178660 - // Generate the widget preview scaffold, but don't bother launching it. - processManager.runSync([ - flutterBin, - 'widget-preview', - 'start', - '--no-${WidgetPreviewStartCommand.kLaunchPreviewer}', - ], workingDirectory: tempDir.path); - - // Ensure widget_preview_scaffold/.dart_tool/package_config.json exists. - final Directory widgetPreviewScaffoldDartTool = tempDir - .childDirectory('.dart_tool') - .childDirectory('widget_preview_scaffold') - .childDirectory('.dart_tool'); - expect(widgetPreviewScaffoldDartTool, exists); - expect(widgetPreviewScaffoldDartTool.childFile('package_config.json'), exists); - - // Delete widget_preview_scaffold/.dart_tool/. This simulates an interrupted - // flutter widget-preview start where 'flutter pub get' wasn't run after - // the widget_preview_scaffold project was created. - widgetPreviewScaffoldDartTool.deleteSync(recursive: true); - - // Ensure we don't crash due to the package_config.json lookup pointing to - // the parent project's package_config.json due to - // widget_preview_scaffold/.dart_tool/package_config.json not existing. - await runWidgetPreview(tempDir: tempDir, expectedMessages: subsequentLaunchMessagesWeb); - }, - // Project is always regenerated. - skip: true, // See https://github.com/flutter/flutter/issues/179036. - ); - - testWithoutContext( - 'does not recreate project on subsequent runs', - () async { - // The first run of 'flutter widget-preview start' should generate a new preview scaffold - await runWidgetPreview(tempDir: tempDir, expectedMessages: firstLaunchMessagesWeb); - - // We shouldn't regenerate the scaffold after the initial run. - await runWidgetPreview(tempDir: tempDir, expectedMessages: subsequentLaunchMessagesWeb); - }, - // Project is always regenerated. - skip: true, // See https://github.com/flutter/flutter/issues/179036. - ); + testWithoutContext('runs flutter pub get in widget_preview_scaffold if ' + "widget_preview_scaffold/.dart_tool doesn't exist", () async { + // Regression test for https://github.com/flutter/flutter/issues/178660 + // Generate the widget preview scaffold, but don't bother launching it. + processManager.runSync([ + flutterBin, + 'widget-preview', + 'start', + '--no-${WidgetPreviewStartCommand.kLaunchPreviewer}', + ], workingDirectory: tempDir.path); + + // Ensure .widget_preview/.dart_tool/package_config.json exists. + final Directory widgetPreviewScaffoldDartTool = tempDir + .childDirectory('.widget_preview') + .childDirectory('.dart_tool'); + expect(widgetPreviewScaffoldDartTool, exists); + expect(widgetPreviewScaffoldDartTool.childFile('package_config.json'), exists); + + // Delete .widget_preview/.dart_tool/. This simulates an interrupted + // flutter widget-preview start where 'flutter pub get' wasn't run after + // the widget_preview_scaffold project was created. + widgetPreviewScaffoldDartTool.deleteSync(recursive: true); + + // Ensure we don't crash due to the package_config.json lookup pointing to + // the parent project's package_config.json due to + // .widget_preview/.dart_tool/package_config.json not existing. + await runWidgetPreview(tempDir: tempDir, expectedMessages: subsequentLaunchMessagesWeb); + }); + + testWithoutContext('does not recreate project on subsequent runs', () async { + // The first run of 'flutter widget-preview start' should generate a new preview scaffold + await runWidgetPreview(tempDir: tempDir, expectedMessages: firstLaunchMessagesWeb); + + // We shouldn't regenerate the scaffold after the initial run. + await runWidgetPreview(tempDir: tempDir, expectedMessages: subsequentLaunchMessagesWeb); + }); testUsingContext('can connect to an existing DTD instance', () async { dtdLauncher = DtdLauncher( diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart index 9f505a7660c5a..4b9747f13bf33 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_test_helpers.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:dtd/dtd.dart'; import 'package:file/file.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/commands/widget_preview.dart'; @@ -60,8 +61,17 @@ Future> startWidgetPreview({ ], workingDirectory: tempDir.path); addTearDown(() async { - process.kill(); - await process.exitCode; + if (platform.isWindows) { + try { + processManager.runSync(['taskkill', '/F', '/T', '/PID', '${process.pid}']); + } on Object catch (_) { + process.kill(); + } + await process.exitCode; + } else { + process.kill(); + await process.exitCode; + } }); final controller = StreamController.broadcast(); @@ -141,3 +151,12 @@ Future> runWidgetPreview({ void runFlutterClean(Directory tempDir, [ProcessManager processManager = _processManager]) { processManager.runSync([flutterBin, 'clean'], workingDirectory: tempDir.path); } + +Future getPreviews(DartToolingDaemon dtdConnection) async { + if (platform.isWindows) { + // Give the slow Windows filesystem and analysis server plenty of time + // to finish subsequent analysis runs and rebuild the semantic model. + await Future.delayed(const Duration(seconds: 2)); + } + return dtdConnection.call('Lsp', 'dart/workspace/getFlutterWidgetPreviews'); +}