Reland "[ Widget Previews ] Add support for detecting previews and generating code (#161911)"" (#162337)
Reverts flutter/flutter#162327
This commit is contained in:
parent
2d11b6a362
commit
2e4b40bdfa
@ -17,8 +17,11 @@ 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());
|
||||
@ -83,6 +86,13 @@ 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();
|
||||
@ -112,9 +122,26 @@ 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',
|
||||
|
@ -0,0 +1,76 @@
|
||||
// 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());
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
// 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:
|
||||
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
|
||||
@ -121,4 +122,4 @@ dartdoc:
|
||||
# Exclude this package from the hosted API docs.
|
||||
nodoc: true
|
||||
|
||||
# PUBSPEC CHECKSUM: cf3c
|
||||
# PUBSPEC CHECKSUM: a49f
|
||||
|
@ -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/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';
|
||||
@ -33,8 +34,12 @@ void main() {
|
||||
tryToDelete(tempDir);
|
||||
});
|
||||
|
||||
Future<String> createRootProject() async {
|
||||
return createProject(tempDir, arguments: <String>['--pub']);
|
||||
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<void> runWidgetPreviewCommand(List<String> arguments) async {
|
||||
@ -43,28 +48,29 @@ void main() {
|
||||
}
|
||||
|
||||
Future<void> startWidgetPreview({
|
||||
required String? rootProjectPath,
|
||||
required Directory? rootProject,
|
||||
List<String>? arguments,
|
||||
}) async {
|
||||
await runWidgetPreviewCommand(<String>[
|
||||
'start',
|
||||
...?arguments,
|
||||
if (rootProjectPath != null) rootProjectPath,
|
||||
if (rootProject != null) rootProject.path,
|
||||
]);
|
||||
final Directory widgetPreviewScaffoldDir = widgetPreviewScaffoldFromRootProject(
|
||||
rootProject: rootProject ?? globals.fs.currentDirectory,
|
||||
);
|
||||
expect(widgetPreviewScaffoldDir, exists);
|
||||
expect(
|
||||
globals.fs
|
||||
.directory(rootProjectPath ?? globals.fs.currentDirectory.path)
|
||||
.childDirectory('.dart_tool')
|
||||
.childDirectory('widget_preview_scaffold'),
|
||||
widgetPreviewScaffoldDir.childFile(PreviewCodeGenerator.generatedPreviewFilePath),
|
||||
exists,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> cleanWidgetPreview({required String rootProjectPath}) async {
|
||||
await runWidgetPreviewCommand(<String>['clean', rootProjectPath]);
|
||||
Future<void> cleanWidgetPreview({required Directory rootProject}) async {
|
||||
await runWidgetPreviewCommand(<String>['clean', rootProject.path]);
|
||||
expect(
|
||||
globals.fs
|
||||
.directory(rootProjectPath)
|
||||
.directory(rootProject)
|
||||
.childDirectory('.dart_tool')
|
||||
.childDirectory('widget_preview_scaffold'),
|
||||
isNot(exists),
|
||||
@ -93,7 +99,7 @@ void main() {
|
||||
|
||||
testUsingContext('run outside of a Flutter project directory', () async {
|
||||
try {
|
||||
await startWidgetPreview(rootProjectPath: tempDir.path);
|
||||
await startWidgetPreview(rootProject: tempDir);
|
||||
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.'));
|
||||
@ -104,8 +110,8 @@ void main() {
|
||||
testUsingContext(
|
||||
'start creates .dart_tool/widget_preview_scaffold',
|
||||
() async {
|
||||
final String rootProjectPath = await createRootProject();
|
||||
await startWidgetPreview(rootProjectPath: rootProjectPath);
|
||||
final Directory rootProject = await createRootProject();
|
||||
await startWidgetPreview(rootProject: rootProject);
|
||||
},
|
||||
overrides: <Type, Generator>{
|
||||
Pub:
|
||||
@ -123,11 +129,87 @@ void main() {
|
||||
testUsingContext(
|
||||
'start creates .dart_tool/widget_preview_scaffold in the CWD',
|
||||
() 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(rootProjectPath: null);
|
||||
}, getCurrentDirectory: () => globals.fs.directory(rootProjectPath));
|
||||
await startWidgetPreview(rootProject: null);
|
||||
}, getCurrentDirectory: () => rootProject);
|
||||
},
|
||||
overrides: <Type, Generator>{
|
||||
Pub:
|
||||
() => Pub.test(
|
||||
fileSystem: globals.fs,
|
||||
logger: globals.logger,
|
||||
processManager: globals.processManager,
|
||||
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,
|
||||
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));
|
||||
},
|
||||
overrides: <Type, Generator>{
|
||||
Pub:
|
||||
@ -145,9 +227,9 @@ void main() {
|
||||
testUsingContext(
|
||||
'clean deletes .dart_tool/widget_preview_scaffold',
|
||||
() async {
|
||||
final String rootProjectPath = await createRootProject();
|
||||
await startWidgetPreview(rootProjectPath: rootProjectPath);
|
||||
await cleanWidgetPreview(rootProjectPath: rootProjectPath);
|
||||
final Directory rootProject = await createRootProject();
|
||||
await startWidgetPreview(rootProject: rootProject);
|
||||
await cleanWidgetPreview(rootProject: rootProject);
|
||||
},
|
||||
overrides: <Type, Generator>{
|
||||
Pub:
|
||||
@ -166,12 +248,12 @@ void main() {
|
||||
'invokes pub in online and offline modes',
|
||||
() async {
|
||||
// Run pub online first in order to populate the pub cache.
|
||||
final String rootProjectPath = await createRootProject();
|
||||
final Directory rootProject = await createRootProject();
|
||||
loggingProcessManager.clear();
|
||||
|
||||
final RegExp dartCommand = RegExp(r'dart-sdk[\\/]bin[\\/]dart');
|
||||
|
||||
await startWidgetPreview(rootProjectPath: rootProjectPath);
|
||||
await startWidgetPreview(rootProject: rootProject);
|
||||
expect(
|
||||
loggingProcessManager.commands,
|
||||
contains(
|
||||
@ -182,12 +264,12 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await cleanWidgetPreview(rootProjectPath: rootProjectPath);
|
||||
await cleanWidgetPreview(rootProject: rootProject);
|
||||
|
||||
// Run pub offline.
|
||||
loggingProcessManager.clear();
|
||||
await startWidgetPreview(
|
||||
rootProjectPath: rootProjectPath,
|
||||
rootProject: rootProject,
|
||||
arguments: <String>['--pub', '--offline'],
|
||||
);
|
||||
|
||||
|
@ -0,0 +1,74 @@
|
||||
// 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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
// 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';
|
||||
''';
|
Loading…
x
Reference in New Issue
Block a user