[ Tool ] Stop generating widget preview scaffold under $TMP (#186476) Previously, the `widget_preview_scaffold` project was generated under the system temporary directory (`$TMP`) to work around issues with `flutter clean`. However, generating the scaffold in `$TMP` meant that the project was recreated on every run or could be lost, leading to unnecessary regeneration overhead and tracking issues. Move the generated `widget_preview_scaffold` project from `$TMP` to a `.widget_preview` directory located directly under the project root. To support this change, `WidgetPreviewGitignoreMigration` is introduced to automatically add `.widget_preview/` to the host project's `.gitignore`. In addition, `kProjectRootPath` is passed via DTD connection info to ensure `WidgetInspectorService` correctly adds the project root to pub root directories. Finally, file watchers (`LspPreviewDetector` and `PreviewDetector`) are updated to ignore the new `.widget_preview` directory. Fixes https://github.com/flutter/flutter/issues/179036
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 def881b..1d8cacc 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 d91e39e..224fb37 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 @@ class WidgetPreviewScaffoldInspectorService with WidgetInspectorService { WidgetPreviewScaffoldInspectorService({required this.dtdServices}) { WidgetInspectorService.instance = this; + addPubRootDirectories(<String>[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 ccc79c6..1139235 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 '../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 @@ previewAnalytics.initializeLaunchStopwatch(); logger.sendInitializingEvent(); + await WidgetPreviewGitignoreMigration(rootProject, logger).migrate(); + final String? customPreviewScaffoldOutput = stringArg(kWidgetPreviewScaffoldOutputDir); widgetPreviewScaffold = customPreviewScaffoldOutput != null ? fs.directory(customPreviewScaffoldOutput) @@ -414,6 +417,7 @@ 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 0000000..ce66432 --- /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<void> 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 bc02734..ef92e6a 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart
@@ -249,10 +249,7 @@ /// 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 4b75c14..ab0d7f6 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 @@ // 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 23f7308..d37eec2 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 @@ 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 @@ ..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 eb0ff03..72d6df0 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 @@ 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 60fe339..a1374e8 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 @@ /// 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 3820a95..79f7eca 100644 --- a/packages/flutter_tools/templates/app/.gitignore.tmpl +++ b/packages/flutter_tools/templates/app/.gitignore.tmpl
@@ -43,3 +43,6 @@ /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 def881b..1d8cacc 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 d91e39e..224fb37 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 @@ class WidgetPreviewScaffoldInspectorService with WidgetInspectorService { WidgetPreviewScaffoldInspectorService({required this.dtdServices}) { WidgetInspectorService.instance = this; + addPubRootDirectories(<String>[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 d8f5fc4..b5b3f64 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 @@ dtdUri: dtdUri, widgetPreviewServiceName: 'widget-preview-service', widgetPreviewScaffoldStreamName: 'widget-preview-stream', + projectRootPath: project.directory.absolute.path, ); final expectedDtdConnectionInfo = @@ -472,6 +473,8 @@ 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 0000000..7fd065c --- /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 fd0afb9..8bb8583 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 44d2743..9935586 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 @@ /// 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 @@ 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 @@ 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 0f3f57f..cef80d3 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 @@ 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 @@ ); 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<String, Object?>, ); @@ -125,10 +125,7 @@ ); 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<String, Object?>, ); @@ -151,7 +148,7 @@ ); await deleteReloadSub.cancel(); - result = await dtdConnection.call('Lsp', 'dart/workspace/getFlutterWidgetPreviews'); + result = await getPreviews(dtdConnection); previews = FlutterWidgetPreviews.fromJson(result.result['result']! as Map<String, Object?>); if (previews.previews.isNotEmpty) { throw StateError('Preview was still detected after deletion!'); @@ -197,10 +194,7 @@ ); 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<String, Object?>, ); @@ -229,7 +223,7 @@ ); await modifyReloadSub.cancel(); - result = await dtdConnection.call('Lsp', 'dart/workspace/getFlutterWidgetPreviews'); + result = await getPreviews(dtdConnection); previews = FlutterWidgetPreviews.fromJson(result.result['result']! as Map<String, Object?>); if (previews.previews.isEmpty) { throw StateError('Preview was lost after modification!'); @@ -292,10 +286,7 @@ ); 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<String, Object?>, );
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 2717cf9..50b51ab 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 @@ 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 ea16c7f..76af3ea 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 @@ 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 @@ ); }); - 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(<String>[ - flutterBin, - 'widget-preview', - 'start', - '--no-${WidgetPreviewStartCommand.kLaunchPreviewer}', - ], workingDirectory: tempDir.path); + 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(<String>[ + 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); + // 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_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); + // 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_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. - ); + // 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); + 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. - ); + // 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 9f505a7..4b9747f 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 @@ ], workingDirectory: tempDir.path); addTearDown(() async { - process.kill(); - await process.exitCode; + if (platform.isWindows) { + try { + processManager.runSync(<String>['taskkill', '/F', '/T', '/PID', '${process.pid}']); + } on Object catch (_) { + process.kill(); + } + await process.exitCode; + } else { + process.kill(); + await process.exitCode; + } }); final controller = StreamController<String>.broadcast(); @@ -141,3 +151,12 @@ void runFlutterClean(Directory tempDir, [ProcessManager processManager = _processManager]) { processManager.runSync(<String>[flutterBin, 'clean'], workingDirectory: tempDir.path); } + +Future<DTDResponse> 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<void>.delayed(const Duration(seconds: 2)); + } + return dtdConnection.call('Lsp', 'dart/workspace/getFlutterWidgetPreviews'); +}