Skip to content
Snippets Groups Projects
Unverified Commit f25b4b77 authored by Ben Konyi's avatar Ben Konyi Committed by GitHub
Browse files

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

`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()];
```
parent a9c50335
Tags
No related merge requests found
...@@ -17,8 +17,11 @@ import '../flutter_manifest.dart'; ...@@ -17,8 +17,11 @@ import '../flutter_manifest.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../project.dart'; import '../project.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../widget_preview/preview_code_generator.dart';
import '../widget_preview/preview_detector.dart';
import 'create_base.dart'; import 'create_base.dart';
// TODO(bkonyi): use dependency injection instead of global accessors throughout this file.
class WidgetPreviewCommand extends FlutterCommand { class WidgetPreviewCommand extends FlutterCommand {
WidgetPreviewCommand() { WidgetPreviewCommand() {
addSubcommand(WidgetPreviewStartCommand()); addSubcommand(WidgetPreviewStartCommand());
...@@ -83,6 +86,13 @@ class WidgetPreviewStartCommand extends FlutterCommand ...@@ -83,6 +86,13 @@ class WidgetPreviewStartCommand extends FlutterCommand
@override @override
String get name => 'start'; String get name => 'start';
late final PreviewDetector _previewDetector = PreviewDetector(
logger: globals.logger,
onChangeDetected: onChangeDetected,
);
late final PreviewCodeGenerator _previewCodeGenerator;
@override @override
Future<FlutterCommandResult> runCommand() async { Future<FlutterCommandResult> runCommand() async {
final FlutterProject rootProject = getRootProject(); final FlutterProject rootProject = getRootProject();
...@@ -112,9 +122,26 @@ class WidgetPreviewStartCommand extends FlutterCommand ...@@ -112,9 +122,26 @@ class WidgetPreviewStartCommand extends FlutterCommand
); );
await _populatePreviewPubspec(rootProject: rootProject); 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(); return FlutterCommandResult.success();
} }
void onChangeDetected(PreviewMapping previews) {
// TODO(bkonyi): perform hot reload
}
@visibleForTesting @visibleForTesting
static const Map<String, String> flutterGenPackageConfigEntry = <String, String>{ static const Map<String, String> flutterGenPackageConfigEntry = <String, String>{
'name': 'flutter_gen', 'name': 'flutter_gen',
......
// 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());
}
}
// 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;
}
}
...@@ -14,6 +14,7 @@ dependencies: ...@@ -14,6 +14,7 @@ dependencies:
args: 2.6.0 args: 2.6.0
dds: 5.0.0 dds: 5.0.0
dwds: 24.3.3 dwds: 24.3.3
code_builder: 4.10.1
completion: 1.0.1 completion: 1.0.1
coverage: 1.11.1 coverage: 1.11.1
crypto: 3.0.6 crypto: 3.0.6
...@@ -121,4 +122,4 @@ dartdoc: ...@@ -121,4 +122,4 @@ dartdoc:
# Exclude this package from the hosted API docs. # Exclude this package from the hosted API docs.
nodoc: true nodoc: true
# PUBSPEC CHECKSUM: cf3c # PUBSPEC CHECKSUM: a49f
...@@ -11,6 +11,7 @@ import 'package:flutter_tools/src/base/file_system.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/commands/widget_preview.dart'; import 'package:flutter_tools/src/commands/widget_preview.dart';
import 'package:flutter_tools/src/dart/pub.dart'; import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/globals.dart' as globals; 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/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
...@@ -33,8 +34,12 @@ void main() { ...@@ -33,8 +34,12 @@ void main() {
tryToDelete(tempDir); tryToDelete(tempDir);
}); });
Future<String> createRootProject() async { Future<Directory> createRootProject() async {
return createProject(tempDir, arguments: <String>['--pub']); 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<void> runWidgetPreviewCommand(List<String> arguments) async { Future<void> runWidgetPreviewCommand(List<String> arguments) async {
...@@ -43,28 +48,29 @@ void main() { ...@@ -43,28 +48,29 @@ void main() {
} }
Future<void> startWidgetPreview({ Future<void> startWidgetPreview({
required String? rootProjectPath, required Directory? rootProject,
List<String>? arguments, List<String>? arguments,
}) async { }) async {
await runWidgetPreviewCommand(<String>[ await runWidgetPreviewCommand(<String>[
'start', 'start',
...?arguments, ...?arguments,
if (rootProjectPath != null) rootProjectPath, if (rootProject != null) rootProject.path,
]); ]);
final Directory widgetPreviewScaffoldDir = widgetPreviewScaffoldFromRootProject(
rootProject: rootProject ?? globals.fs.currentDirectory,
);
expect(widgetPreviewScaffoldDir, exists);
expect( expect(
globals.fs widgetPreviewScaffoldDir.childFile(PreviewCodeGenerator.generatedPreviewFilePath),
.directory(rootProjectPath ?? globals.fs.currentDirectory.path)
.childDirectory('.dart_tool')
.childDirectory('widget_preview_scaffold'),
exists, exists,
); );
} }
Future<void> cleanWidgetPreview({required String rootProjectPath}) async { Future<void> cleanWidgetPreview({required Directory rootProject}) async {
await runWidgetPreviewCommand(<String>['clean', rootProjectPath]); await runWidgetPreviewCommand(<String>['clean', rootProject.path]);
expect( expect(
globals.fs globals.fs
.directory(rootProjectPath) .directory(rootProject)
.childDirectory('.dart_tool') .childDirectory('.dart_tool')
.childDirectory('widget_preview_scaffold'), .childDirectory('widget_preview_scaffold'),
isNot(exists), isNot(exists),
...@@ -93,7 +99,7 @@ void main() { ...@@ -93,7 +99,7 @@ void main() {
testUsingContext('run outside of a Flutter project directory', () async { testUsingContext('run outside of a Flutter project directory', () async {
try { try {
await startWidgetPreview(rootProjectPath: tempDir.path); await startWidgetPreview(rootProject: tempDir);
fail('Successfully executed outside of a Flutter project directory'); fail('Successfully executed outside of a Flutter project directory');
} on ToolExit catch (e) { } on ToolExit catch (e) {
expect(e.message, contains('${tempDir.path} is not a valid Flutter project.')); expect(e.message, contains('${tempDir.path} is not a valid Flutter project.'));
...@@ -104,8 +110,8 @@ void main() { ...@@ -104,8 +110,8 @@ void main() {
testUsingContext( testUsingContext(
'start creates .dart_tool/widget_preview_scaffold', 'start creates .dart_tool/widget_preview_scaffold',
() async { () async {
final String rootProjectPath = await createRootProject(); final Directory rootProject = await createRootProject();
await startWidgetPreview(rootProjectPath: rootProjectPath); await startWidgetPreview(rootProject: rootProject);
}, },
overrides: <Type, Generator>{ overrides: <Type, Generator>{
Pub: Pub:
...@@ -123,11 +129,89 @@ void main() { ...@@ -123,11 +129,89 @@ void main() {
testUsingContext( testUsingContext(
'start creates .dart_tool/widget_preview_scaffold in the CWD', 'start creates .dart_tool/widget_preview_scaffold in the CWD',
() async { () async {
final String rootProjectPath = await createRootProject(); final Directory rootProject = 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 { await io.IOOverrides.runZoned<Future<void>>(() async {
// Try to execute using the CWD. // Try to execute using the CWD.
await startWidgetPreview(rootProjectPath: null); await startWidgetPreview(rootProject: null);
}, getCurrentDirectory: () => globals.fs.directory(rootProjectPath)); expect(generatedFile.readAsStringSync(), expectedGeneratedFileContents);
}, getCurrentDirectory: () => globals.fs.directory(rootProject));
}, },
overrides: <Type, Generator>{ overrides: <Type, Generator>{
Pub: Pub:
...@@ -145,9 +229,9 @@ void main() { ...@@ -145,9 +229,9 @@ void main() {
testUsingContext( testUsingContext(
'clean deletes .dart_tool/widget_preview_scaffold', 'clean deletes .dart_tool/widget_preview_scaffold',
() async { () async {
final String rootProjectPath = await createRootProject(); final Directory rootProject = await createRootProject();
await startWidgetPreview(rootProjectPath: rootProjectPath); await startWidgetPreview(rootProject: rootProject);
await cleanWidgetPreview(rootProjectPath: rootProjectPath); await cleanWidgetPreview(rootProject: rootProject);
}, },
overrides: <Type, Generator>{ overrides: <Type, Generator>{
Pub: Pub:
...@@ -166,12 +250,12 @@ void main() { ...@@ -166,12 +250,12 @@ void main() {
'invokes pub in online and offline modes', 'invokes pub in online and offline modes',
() async { () async {
// Run pub online first in order to populate the pub cache. // Run pub online first in order to populate the pub cache.
final String rootProjectPath = await createRootProject(); final Directory rootProject = await createRootProject();
loggingProcessManager.clear(); loggingProcessManager.clear();
final RegExp dartCommand = RegExp(r'dart-sdk[\\/]bin[\\/]dart'); final RegExp dartCommand = RegExp(r'dart-sdk[\\/]bin[\\/]dart');
await startWidgetPreview(rootProjectPath: rootProjectPath); await startWidgetPreview(rootProject: rootProject);
expect( expect(
loggingProcessManager.commands, loggingProcessManager.commands,
contains( contains(
...@@ -182,12 +266,12 @@ void main() { ...@@ -182,12 +266,12 @@ void main() {
), ),
); );
await cleanWidgetPreview(rootProjectPath: rootProjectPath); await cleanWidgetPreview(rootProject: rootProject);
// Run pub offline. // Run pub offline.
loggingProcessManager.clear(); loggingProcessManager.clear();
await startWidgetPreview( await startWidgetPreview(
rootProjectPath: rootProjectPath, rootProject: rootProject,
arguments: <String>['--pub', '--offline'], arguments: <String>['--pub', '--offline'],
); );
......
// 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);
},
);
});
}
// 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';
''';
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment