Reverts "[ Widget Previews ] Add support for detecting previews and generating code (#161911)" (#162327)

<!-- start_original_pr_link -->
Reverts: flutter/flutter#161911
<!-- end_original_pr_link -->
<!-- start_initiating_author -->
Initiated by: matanlurey
<!-- end_initiating_author -->
<!-- start_revert_reason -->
Reason for reverting: Mid-air collision with another Flutter tool
update.
<!-- end_revert_reason -->
<!-- start_original_pr_author -->
Original PR Author: bkonyi
<!-- end_original_pr_author -->

<!-- start_reviewers -->
Reviewed By: {andrewkolos}
<!-- end_reviewers -->

<!-- start_revert_body -->
This change reverts the following previous change:
`flutter widget-preview start` will now look for functions annotated
with `@Preview()` within the developer's project. These functions will
be used to generate
`.dart_tool/widget_preview_scaffold/lib/generated_preview.dart`, which
inserts the returned value from each preview function into a
`List<WidgetPreview>` returned from a `previews()` method that is
invoked by the widget preview scaffold root.

**Example generated_preview.dart:**

```dart
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:foo/foo.dart' as _i1;
import 'package:foo/src/bar.dart' as _i2;
import 'package:widget_preview/widget_preview.dart';

List<WidgetPreview> previews() => [_i1.preview(), _i2.barPreview1(), _i2.barPreview2()];
```
<!-- end_revert_body -->

Co-authored-by: auto-submit[bot] <flutter-engprod-team@google.com>
This commit is contained in:
auto-submit[bot] 2025-01-28 19:14:26 +00:00 committed by GitHub
parent 6d2792de5c
commit 1807704009
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 25 additions and 551 deletions

View File

@ -17,11 +17,8 @@ import '../flutter_manifest.dart';
import '../globals.dart' as globals;
import '../project.dart';
import '../runner/flutter_command.dart';
import '../widget_preview/preview_code_generator.dart';
import '../widget_preview/preview_detector.dart';
import 'create_base.dart';
// TODO(bkonyi): use dependency injection instead of global accessors throughout this file.
class WidgetPreviewCommand extends FlutterCommand {
WidgetPreviewCommand() {
addSubcommand(WidgetPreviewStartCommand());
@ -86,13 +83,6 @@ class WidgetPreviewStartCommand extends FlutterCommand
@override
String get name => 'start';
late final PreviewDetector _previewDetector = PreviewDetector(
logger: globals.logger,
onChangeDetected: onChangeDetected,
);
late final PreviewCodeGenerator _previewCodeGenerator;
@override
Future<FlutterCommandResult> runCommand() async {
final FlutterProject rootProject = getRootProject();
@ -122,26 +112,9 @@ class WidgetPreviewStartCommand extends FlutterCommand
);
await _populatePreviewPubspec(rootProject: rootProject);
}
// WARNING: this needs to happen after we generate the scaffold project as invoking the
// widgetPreviewScaffoldProject getter triggers lazy initialization of the preview scaffold's
// FlutterManifest before the scaffold project's pubspec has been generated.
_previewCodeGenerator = PreviewCodeGenerator(
widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject,
fs: globals.fs,
);
final PreviewMapping initialPreviews = await _previewDetector.initialize(rootProject.directory);
_previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffold(initialPreviews);
await _previewDetector.dispose();
return FlutterCommandResult.success();
}
void onChangeDetected(PreviewMapping previews) {
// TODO(bkonyi): perform hot reload
}
@visibleForTesting
static const Map<String, String> flutterGenPackageConfigEntry = <String, String>{
'name': 'flutter_gen',

View File

@ -1,76 +0,0 @@
// 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:code_builder/code_builder.dart';
import '../base/file_system.dart';
import '../project.dart';
import 'preview_detector.dart';
/// Generates the Dart source responsible for importing widget previews from the developer's project
/// into the widget preview scaffold.
class PreviewCodeGenerator {
PreviewCodeGenerator({required this.widgetPreviewScaffoldProject, required this.fs});
final FileSystem fs;
/// The project for the widget preview scaffold found under `.dart_tool/` in the developer's
/// project.
final FlutterProject widgetPreviewScaffoldProject;
static const String generatedPreviewFilePath = 'lib/generated_preview.dart';
/// Generates code used by the widget preview scaffold based on the preview instances listed in
/// [previews].
///
/// The generated file will contain a single top level function named `previews()` which returns
/// a `List<WidgetPreview>` that contains each widget preview defined in [previews].
///
/// An example of a formatted generated file containing previews from two files could be:
///
/// ```dart
/// import 'package:foo/foo.dart' as _i1;
/// import 'package:foo/src/bar.dart' as _i2;
/// import 'package:widget_preview/widget_preview.dart';
///
/// List<WidgetPreview> previews() => [
/// _i1.fooPreview(),
/// _i2.barPreview1(),
/// _i3.barPreview2(),
/// ];
/// ```
void populatePreviewsInGeneratedPreviewScaffold(PreviewMapping previews) {
final Library lib = Library(
(LibraryBuilder b) => b.body.addAll(<Spec>[
Directive.import(
// TODO(bkonyi): update with actual location in the framework
'package:widget_preview/widget_preview.dart',
),
Method(
(MethodBuilder b) =>
b
..body =
literalList(<Object?>[
for (final MapEntry<String, List<String>>(
key: String path,
value: List<String> previewMethods,
)
in previews.entries) ...<Object?>[
for (final String method in previewMethods)
refer(method, path).call(<Expression>[]),
],
]).code
..name = 'previews'
..returns = refer('List<WidgetPreview>'),
),
]),
);
final DartEmitter emitter = DartEmitter.scoped(useNullSafetySyntax: true);
final File generatedPreviewFile = fs.file(
widgetPreviewScaffoldProject.directory.uri.resolve(generatedPreviewFilePath),
);
// TODO(bkonyi): do we want to bother with formatting this?
generatedPreviewFile.writeAsStringSync(lib.accept(emitter).toString());
}
}

View File

@ -1,145 +0,0 @@
// 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 'dart:async';
// ignore: implementation_imports
import 'package:_fe_analyzer_shared/src/base/syntactic_entity.dart';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:watcher/watcher.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/utils.dart';
import '../globals.dart' as globals;
import 'preview_code_generator.dart';
typedef PreviewMapping = Map<String, List<String>>;
class PreviewDetector {
PreviewDetector({required this.logger, required this.onChangeDetected});
final Logger logger;
final void Function(PreviewMapping) onChangeDetected;
StreamSubscription<WatchEvent>? _fileWatcher;
late final PreviewMapping _pathToPreviews;
/// Starts listening for changes to Dart sources under [projectRoot] and returns
/// the initial [PreviewMapping] for the project.
Future<PreviewMapping> initialize(Directory projectRoot) async {
// Find the initial set of previews.
_pathToPreviews = findPreviewFunctions(projectRoot);
final Watcher watcher = Watcher(projectRoot.path);
// TODO(bkonyi): watch for changes to pubspec.yaml
_fileWatcher = watcher.events.listen((WatchEvent event) async {
final String eventPath = Uri.file(event.path).toString();
// Only trigger a reload when changes to Dart sources are detected. We
// ignore the generated preview file to avoid getting stuck in a loop.
if (!eventPath.endsWith('.dart') ||
eventPath.endsWith(PreviewCodeGenerator.generatedPreviewFilePath)) {
return;
}
logger.printStatus('Detected change in $eventPath.');
final PreviewMapping filePreviewsMapping = findPreviewFunctions(
globals.fs.file(Uri.file(event.path)),
);
if (filePreviewsMapping.isEmpty && !_pathToPreviews.containsKey(eventPath)) {
// No previews found or removed, nothing to do.
return;
}
if (filePreviewsMapping.length > 1) {
logger.printWarning('Previews from more than one file were detected!');
logger.printWarning('Previews: $filePreviewsMapping');
}
if (filePreviewsMapping.isNotEmpty) {
// The set of previews has changed, but there are still previews in the file.
final MapEntry<String, List<String>>(key: String uri, value: List<String> filePreviews) =
filePreviewsMapping.entries.first;
assert(uri == eventPath);
logger.printStatus('Updated previews for $eventPath: $filePreviews');
if (filePreviews.isNotEmpty) {
final List<String>? currentPreviewsForFile = _pathToPreviews[eventPath];
if (filePreviews != currentPreviewsForFile) {
_pathToPreviews[eventPath] = filePreviews;
}
}
} else {
// The file previously had previews that were removed.
logger.printStatus('Previews removed from $eventPath');
_pathToPreviews.remove(eventPath);
}
onChangeDetected(_pathToPreviews);
});
// Wait for file watcher to finish initializing, otherwise we might miss changes and cause
// tests to flake.
await watcher.ready;
return _pathToPreviews;
}
Future<void> dispose() async {
await _fileWatcher?.cancel();
}
/// Search for functions annotated with `@Preview` in the current project.
PreviewMapping findPreviewFunctions(FileSystemEntity entity) {
final AnalysisContextCollection collection = AnalysisContextCollection(
includedPaths: <String>[entity.absolute.path],
resourceProvider: PhysicalResourceProvider.INSTANCE,
);
final PreviewMapping previews = PreviewMapping();
for (final AnalysisContext context in collection.contexts) {
logger.printStatus('Finding previews in ${context.contextRoot.root.path}...');
for (final String filePath in context.contextRoot.analyzedFiles()) {
logger.printTrace('Checking file: $filePath');
if (!filePath.endsWith('.dart')) {
continue;
}
final SomeParsedLibraryResult lib = context.currentSession.getParsedLibrary(filePath);
if (lib is ParsedLibraryResult) {
for (final ParsedUnitResult unit in lib.units) {
final List<String> previewEntries = previews[unit.uri.toString()] ?? <String>[];
for (final SyntacticEntity entity in unit.unit.childEntities) {
if (entity is FunctionDeclaration && !entity.name.toString().startsWith('_')) {
bool foundPreview = false;
for (final Annotation annotation in entity.metadata) {
if (annotation.name.name == 'Preview') {
// What happens if the annotation is applied multiple times?
foundPreview = true;
break;
}
}
if (foundPreview) {
logger.printStatus('Found preview at:');
logger.printStatus('File path: ${unit.uri}');
logger.printStatus('Preview function: ${entity.name}');
logger.printStatus('');
previewEntries.add(entity.name.toString());
}
}
}
if (previewEntries.isNotEmpty) {
previews[unit.uri.toString()] = previewEntries;
}
}
} else {
logger.printWarning('Unknown library type at $filePath: $lib');
}
}
}
final int previewCount = previews.values.fold<int>(
0,
(int count, List<String> value) => count + value.length,
);
logger.printStatus('Found $previewCount ${pluralize('preview', previewCount)}.');
return previews;
}
}

View File

@ -14,7 +14,6 @@ dependencies:
args: 2.6.0
dds: 5.0.0
dwds: 24.3.3
code_builder: 4.10.1
completion: 1.0.1
coverage: 1.11.1
crypto: 3.0.6
@ -122,4 +121,4 @@ dartdoc:
# Exclude this package from the hosted API docs.
nodoc: true
# PUBSPEC CHECKSUM: a49f
# PUBSPEC CHECKSUM: cf3c

View File

@ -11,7 +11,6 @@ import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/commands/widget_preview.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/widget_preview/preview_code_generator.dart';
import '../../src/common.dart';
import '../../src/context.dart';
@ -34,12 +33,8 @@ void main() {
tryToDelete(tempDir);
});
Future<Directory> createRootProject() async {
return globals.fs.directory(await createProject(tempDir, arguments: <String>['--pub']));
}
Directory widgetPreviewScaffoldFromRootProject({required Directory rootProject}) {
return rootProject.childDirectory('.dart_tool').childDirectory('widget_preview_scaffold');
Future<String> createRootProject() async {
return createProject(tempDir, arguments: <String>['--pub']);
}
Future<void> runWidgetPreviewCommand(List<String> arguments) async {
@ -48,29 +43,28 @@ void main() {
}
Future<void> startWidgetPreview({
required Directory? rootProject,
required String? rootProjectPath,
List<String>? arguments,
}) async {
await runWidgetPreviewCommand(<String>[
'start',
...?arguments,
if (rootProject != null) rootProject.path,
if (rootProjectPath != null) rootProjectPath,
]);
final Directory widgetPreviewScaffoldDir = widgetPreviewScaffoldFromRootProject(
rootProject: rootProject ?? globals.fs.currentDirectory,
);
expect(widgetPreviewScaffoldDir, exists);
expect(
widgetPreviewScaffoldDir.childFile(PreviewCodeGenerator.generatedPreviewFilePath),
globals.fs
.directory(rootProjectPath ?? globals.fs.currentDirectory.path)
.childDirectory('.dart_tool')
.childDirectory('widget_preview_scaffold'),
exists,
);
}
Future<void> cleanWidgetPreview({required Directory rootProject}) async {
await runWidgetPreviewCommand(<String>['clean', rootProject.path]);
Future<void> cleanWidgetPreview({required String rootProjectPath}) async {
await runWidgetPreviewCommand(<String>['clean', rootProjectPath]);
expect(
globals.fs
.directory(rootProject)
.directory(rootProjectPath)
.childDirectory('.dart_tool')
.childDirectory('widget_preview_scaffold'),
isNot(exists),
@ -99,7 +93,7 @@ void main() {
testUsingContext('run outside of a Flutter project directory', () async {
try {
await startWidgetPreview(rootProject: tempDir);
await startWidgetPreview(rootProjectPath: tempDir.path);
fail('Successfully executed outside of a Flutter project directory');
} on ToolExit catch (e) {
expect(e.message, contains('${tempDir.path} is not a valid Flutter project.'));
@ -110,8 +104,8 @@ void main() {
testUsingContext(
'start creates .dart_tool/widget_preview_scaffold',
() async {
final Directory rootProject = await createRootProject();
await startWidgetPreview(rootProject: rootProject);
final String rootProjectPath = await createRootProject();
await startWidgetPreview(rootProjectPath: rootProjectPath);
},
overrides: <Type, Generator>{
Pub:
@ -129,89 +123,11 @@ void main() {
testUsingContext(
'start creates .dart_tool/widget_preview_scaffold in the CWD',
() async {
final Directory rootProject = await createRootProject();
final String rootProjectPath = await createRootProject();
await io.IOOverrides.runZoned<Future<void>>(() async {
// Try to execute using the CWD.
await startWidgetPreview(rootProject: null);
}, getCurrentDirectory: () => rootProject);
},
overrides: <Type, Generator>{
Pub:
() => Pub.test(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
stdio: mockStdio,
),
},
);
const String samplePreviewFile = '''
// This doesn't need to be valid code for testing as long as it has the @Preview() annotation
@Preview()
WidgetPreview preview() => WidgetPreview();''';
const String expectedGeneratedFileContents = '''
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:flutter_project/foo.dart' as _i1;import 'package:widget_preview/widget_preview.dart';List<WidgetPreview> previews() => [_i1.preview()];''';
testUsingContext(
'start finds existing previews and injects them into ${PreviewCodeGenerator.generatedPreviewFilePath}',
() async {
final Directory rootProject = await createRootProject();
final Directory widgetPreviewScaffoldDir = widgetPreviewScaffoldFromRootProject(
rootProject: rootProject,
);
rootProject
.childDirectory('lib')
.childFile('foo.dart')
.writeAsStringSync(samplePreviewFile);
final File generatedFile = widgetPreviewScaffoldDir.childFile(
PreviewCodeGenerator.generatedPreviewFilePath,
);
await startWidgetPreview(rootProject: rootProject);
expect(generatedFile.readAsStringSync(), expectedGeneratedFileContents);
},
overrides: <Type, Generator>{
Pub:
() => Pub.test(
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
usage: globals.flutterUsage,
botDetector: globals.botDetector,
platform: globals.platform,
stdio: mockStdio,
),
},
);
testUsingContext(
'start finds existing previews in the CWD and injects them into ${PreviewCodeGenerator.generatedPreviewFilePath}',
() async {
final Directory rootProject = await createRootProject();
final Directory widgetPreviewScaffoldDir = widgetPreviewScaffoldFromRootProject(
rootProject: rootProject,
);
rootProject
.childDirectory('lib')
.childFile('foo.dart')
.writeAsStringSync(samplePreviewFile);
final File generatedFile = widgetPreviewScaffoldDir.childFile(
PreviewCodeGenerator.generatedPreviewFilePath,
);
await io.IOOverrides.runZoned<Future<void>>(() async {
// Try to execute using the CWD.
await startWidgetPreview(rootProject: null);
expect(generatedFile.readAsStringSync(), expectedGeneratedFileContents);
}, getCurrentDirectory: () => globals.fs.directory(rootProject));
await startWidgetPreview(rootProjectPath: null);
}, getCurrentDirectory: () => globals.fs.directory(rootProjectPath));
},
overrides: <Type, Generator>{
Pub:
@ -229,9 +145,9 @@ import 'package:flutter_project/foo.dart' as _i1;import 'package:widget_preview/
testUsingContext(
'clean deletes .dart_tool/widget_preview_scaffold',
() async {
final Directory rootProject = await createRootProject();
await startWidgetPreview(rootProject: rootProject);
await cleanWidgetPreview(rootProject: rootProject);
final String rootProjectPath = await createRootProject();
await startWidgetPreview(rootProjectPath: rootProjectPath);
await cleanWidgetPreview(rootProjectPath: rootProjectPath);
},
overrides: <Type, Generator>{
Pub:
@ -250,12 +166,12 @@ import 'package:flutter_project/foo.dart' as _i1;import 'package:widget_preview/
'invokes pub in online and offline modes',
() async {
// Run pub online first in order to populate the pub cache.
final Directory rootProject = await createRootProject();
final String rootProjectPath = await createRootProject();
loggingProcessManager.clear();
final RegExp dartCommand = RegExp(r'dart-sdk[\\/]bin[\\/]dart');
await startWidgetPreview(rootProject: rootProject);
await startWidgetPreview(rootProjectPath: rootProjectPath);
expect(
loggingProcessManager.commands,
contains(
@ -266,12 +182,12 @@ import 'package:flutter_project/foo.dart' as _i1;import 'package:widget_preview/
),
);
await cleanWidgetPreview(rootProject: rootProject);
await cleanWidgetPreview(rootProjectPath: rootProjectPath);
// Run pub offline.
loggingProcessManager.clear();
await startWidgetPreview(
rootProject: rootProject,
rootProjectPath: rootProjectPath,
arguments: <String>['--pub', '--offline'],
);

View File

@ -1,74 +0,0 @@
// 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/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/widget_preview/preview_code_generator.dart';
import 'package:test/test.dart';
import '../../src/context.dart';
void main() {
group('$PreviewCodeGenerator', () {
late PreviewCodeGenerator codeGenerator;
late FlutterProject project;
setUp(() {
final FileSystem fs = MemoryFileSystem.test();
final FlutterManifest manifest = FlutterManifest.empty(logger: BufferLogger.test());
final Directory projectDir =
fs.currentDirectory.childDirectory('project')
..createSync()
..childDirectory('lib').createSync();
project = FlutterProject(projectDir, manifest, manifest);
codeGenerator = PreviewCodeGenerator(widgetPreviewScaffoldProject: project, fs: fs);
});
testUsingContext(
'correctly generates ${PreviewCodeGenerator.generatedPreviewFilePath}',
() async {
// Check that the generated preview file doesn't exist yet.
final File generatedPreviewFile = project.directory.childFile(
PreviewCodeGenerator.generatedPreviewFilePath,
);
expect(generatedPreviewFile, isNot(exists));
// Populate the generated preview file.
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(const <String, List<String>>{
'foo.dart': <String>['preview'],
'src/bar.dart': <String>['barPreview1', 'barPreview2'],
});
expect(generatedPreviewFile, exists);
// Check that the generated file contains:
// - An import of the widget preview library
// - Prefixed imports for both 'foo.dart' and 'src/bar.dart'
// - A top-level function 'List<WidgetPreview> previews()'
// - A returned list containing function calls to 'preview()' from 'foo.dart' and 'barPreview1()'
// and 'barPreview2()' from 'src/bar.dart'
//
// The generated file is unfortunately unformatted.
const String expectedGeneratedPreviewFileContents = '''
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'foo.dart' as _i1;import 'src/bar.dart' as _i2;import 'package:widget_preview/widget_preview.dart';List<WidgetPreview> previews() => [_i1.preview(), _i2.barPreview1(), _i2.barPreview2(), ];''';
expect(generatedPreviewFile.readAsStringSync(), expectedGeneratedPreviewFileContents);
// Regenerate the generated file with no previews.
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(const <String, List<String>>{});
expect(generatedPreviewFile, exists);
// The generated file should only contain:
// - An import of the widget preview library
// - A top-level function 'List<WidgetPreview> previews()' that returns an empty list.
const String emptyGeneratedPreviewFileContents = '''
import 'package:widget_preview/widget_preview.dart';List<WidgetPreview> previews() => [];''';
expect(generatedPreviewFile.readAsStringSync(), emptyGeneratedPreviewFileContents);
},
);
});
}

View File

@ -1,119 +0,0 @@
// 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 'dart:async';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/signals.dart';
import 'package:flutter_tools/src/widget_preview/preview_detector.dart';
import 'package:test/test.dart';
import '../../src/common.dart';
import '../../src/context.dart';
Directory createBasicProjectStructure(FileSystem fs) {
return fs.systemTempDirectory.createTempSync('root');
}
File addPreviewContainingFile(Directory projectRoot, String path) {
return projectRoot.childDirectory('lib').childFile(path)
..createSync(recursive: true)
..writeAsStringSync(previewContainingFileContents);
}
File addNonPreviewContainingFile(Directory projectRoot, String path) {
return projectRoot.childDirectory('lib').childFile(path)
..createSync(recursive: true)
..writeAsStringSync(nonPreviewContainingFileContents);
}
void main() {
group('$PreviewDetector', () {
// Note: we don't use a MemoryFileSystem since we don't have a way to
// provide it to package:analyzer APIs without writing a significant amount
// of wrapper logic.
late LocalFileSystem fs;
late Logger logger;
late PreviewDetector previewDetector;
late Directory projectRoot;
void Function(PreviewMapping)? onChangeDetected;
void onChangeDetectedRoot(PreviewMapping mapping) {
onChangeDetected!(mapping);
}
setUp(() {
fs = LocalFileSystem.test(signals: Signals.test());
projectRoot = createBasicProjectStructure(fs);
logger = BufferLogger.test();
previewDetector = PreviewDetector(logger: logger, onChangeDetected: onChangeDetectedRoot);
});
tearDown(() async {
await previewDetector.dispose();
projectRoot.deleteSync(recursive: true);
onChangeDetected = null;
});
testUsingContext('can detect previews in existing files', () async {
final List<File> previewFiles = <File>[
addPreviewContainingFile(projectRoot, 'foo.dart'),
addPreviewContainingFile(projectRoot, 'src/bar.dart'),
];
addNonPreviewContainingFile(projectRoot, 'baz.dart');
final PreviewMapping mapping = previewDetector.findPreviewFunctions(projectRoot);
expect(mapping.keys.toSet(), previewFiles.map((File e) => e.uri.toString()).toSet());
});
testUsingContext('can detect previews in updated files', () async {
// Create two files with existing previews and one without.
final PreviewMapping expectedInitialMapping = <String, List<String>>{
addPreviewContainingFile(projectRoot, 'foo.dart').uri.toString(): <String>['previews'],
addPreviewContainingFile(projectRoot, 'src/bar.dart').uri.toString(): <String>['previews'],
};
final File nonPreviewContainingFile = addNonPreviewContainingFile(projectRoot, 'baz.dart');
Completer<void> completer = Completer<void>();
onChangeDetected = (PreviewMapping updated) {
// The new preview in baz.dart should be included in the preview mapping.
expect(updated, <String, List<String>>{
...expectedInitialMapping,
nonPreviewContainingFile.uri.toString(): <String>['previews'],
});
completer.complete();
};
// Initialize the file watcher.
final PreviewMapping initialPreviews = await previewDetector.initialize(projectRoot);
expect(initialPreviews, expectedInitialMapping);
// Update the file without an existing preview to include a preview and ensure it triggers
// the preview detector.
addPreviewContainingFile(projectRoot, 'baz.dart');
await completer.future;
completer = Completer<void>();
onChangeDetected = (PreviewMapping updated) {
// The removed preview in baz.dart should not longer be included in the preview mapping.
expect(updated, expectedInitialMapping);
completer.complete();
};
// Update the file with an existing preview to remove the preview and ensure it triggers
// the preview detector.
addNonPreviewContainingFile(projectRoot, 'baz.dart');
await completer.future;
});
});
}
const String previewContainingFileContents = '''
@Preview()
// This isn't necessarily valid code. We're just looking for the annotation
WidgetPreview previews() => WidgetPreview();
''';
const String nonPreviewContainingFileContents = '''
String foo() => 'bar';
''';