[ Widget Preview ] Add support for building and launching the widget preview scaffold (#162326)

This also wires up the preview detector to trigger hot reloads when new
previews are detected or previews are removed.

Note: while this change results in lib/generated_preview.dart being
generated and updated, it's not currently referenced by lib/main.dart
and the preview environment will render a black screen.

---------

Co-authored-by: Andrew Kolos <andrewrkolos@gmail.com>
This commit is contained in:
Ben Konyi
2025-02-05 12:44:56 -08:00
committed by GitHub
parent 3b3936a703
commit 0889cca518
17 changed files with 494 additions and 88 deletions

View File

@@ -1497,7 +1497,10 @@ targets:
dependencies: >-
[
{"dependency": "android_sdk", "version": "version:35v1"},
{"dependency": "open_jdk", "version": "version:17"}
{"dependency": "open_jdk", "version": "version:17"},
{"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"},
{"dependency": "cmake", "version": "build_id:8787856497187628321"},
{"dependency": "ninja", "version": "version:1.9.0"}
]
shard: tool_tests
subshard: commands
@@ -6322,7 +6325,8 @@ targets:
dependencies: >-
[
{"dependency": "android_sdk", "version": "version:35v1"},
{"dependency": "open_jdk", "version": "version:17"}
{"dependency": "open_jdk", "version": "version:17"},
{"dependency": "vs_build", "version": "version:vs2019"}
]
shard: tool_tests
subshard: commands

View File

@@ -85,7 +85,10 @@ Future<void> main(List<String> args) async {
final bool daemon = args.contains('daemon');
final bool runMachine =
(args.contains('--machine') && args.contains('run')) ||
(args.contains('--machine') && args.contains('attach'));
(args.contains('--machine') && args.contains('attach')) ||
// `flutter widget-preview start` starts an application that requires a logger
// to be setup for machine mode.
(args.contains('widget-preview') && args.contains('start'));
// Cache.flutterRoot must be set early because other features use it (e.g.
// enginePath's initializer uses it). This can only work with the real
@@ -247,10 +250,14 @@ List<FlutterCommand> generateCommands({required bool verboseHelp, required bool
nativeAssetsBuilder: globals.nativeAssetsBuilder,
),
WidgetPreviewCommand(
verboseHelp: verboseHelp,
logger: globals.logger,
fs: globals.fs,
projectFactory: globals.projectFactory,
cache: globals.cache,
platform: globals.platform,
shutdownHooks: globals.shutdownHooks,
os: globals.os,
),
UpgradeCommand(verboseHelp: verboseHelp),
SymbolizeCommand(stdio: globals.stdio, fileSystem: globals.fs),

View File

@@ -195,6 +195,21 @@ class Daemon {
);
}
factory Daemon.createMachineDaemon() {
final Daemon daemon = Daemon(
DaemonConnection(
daemonStreams: DaemonStreams.fromStdio(globals.stdio, logger: globals.logger),
logger: globals.logger,
),
notifyingLogger:
(globals.logger is NotifyingLogger)
? globals.logger as NotifyingLogger
: NotifyingLogger(verbose: globals.logger.isVerbose, parent: globals.logger),
logToStdout: true,
);
return daemon;
}
final DaemonConnection connection;
late DaemonDomain daemonDomain;
@@ -809,15 +824,11 @@ class AppDomain extends Domain {
);
}
final Completer<void> appStartedCompleter = Completer<void>();
// We don't want to wait for this future to complete, and callbacks won't fail,
// as it just writes to stdout.
unawaited(
appStartedCompleter.future.then<void>((void value) {
_sendAppEvent(app, 'started');
}),
);
await app._runInZone<void>(this, () async {
// This future won't complete until the application has shutdown, so we don't want to
// await it. However, we do need to listen to the future in order to handle possible
// tool exits
final Future<void> appRunFuture = app._runInZone<void>(this, () async {
try {
await runOrAttach(
connectionInfoCompleter: connectionInfoCompleter,
@@ -836,6 +847,13 @@ class AppDomain extends Domain {
_apps.remove(app);
}
});
await Future.any(<Future<void>>[
appStartedCompleter.future.then<void>((void value) {
_sendAppEvent(app, 'started');
}),
appRunFuture,
]);
return app;
}

View File

@@ -14,7 +14,6 @@ import '../base/file_system.dart';
import '../base/io.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../daemon.dart';
import '../device.dart';
import '../features.dart';
import '../globals.dart' as globals;
@@ -761,18 +760,7 @@ class RunCommand extends RunCommandBase {
@visibleForTesting
Daemon createMachineDaemon() {
final Daemon daemon = Daemon(
DaemonConnection(
daemonStreams: DaemonStreams.fromStdio(globals.stdio, logger: globals.logger),
logger: globals.logger,
),
notifyingLogger:
(globals.logger is NotifyingLogger)
? globals.logger as NotifyingLogger
: NotifyingLogger(verbose: globals.logger.isVerbose, parent: globals.logger),
logToStdout: true,
);
return daemon;
return Daemon.createMachineDaemon();
}
@override

View File

@@ -9,31 +9,47 @@ import '../base/common.dart';
import '../base/deferred_component.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../build_info.dart';
import '../bundle.dart' as bundle;
import '../cache.dart';
import '../convert.dart';
import '../dart/pub.dart';
import '../device.dart';
import '../flutter_manifest.dart';
import '../linux/build_linux.dart';
import '../macos/build_macos.dart';
import '../project.dart';
import '../runner/flutter_command.dart';
import '../widget_preview/preview_code_generator.dart';
import '../widget_preview/preview_detector.dart';
import '../windows/build_windows.dart';
import 'create_base.dart';
import 'daemon.dart';
class WidgetPreviewCommand extends FlutterCommand {
WidgetPreviewCommand({
required bool verboseHelp,
required Logger logger,
required FileSystem fs,
required FlutterProjectFactory projectFactory,
required Cache cache,
required Platform platform,
required ShutdownHooks shutdownHooks,
required OperatingSystemUtils os,
}) {
addSubcommand(
WidgetPreviewStartCommand(
verboseHelp: verboseHelp,
logger: logger,
fs: fs,
projectFactory: projectFactory,
cache: cache,
platform: platform,
shutdownHooks: shutdownHooks,
os: os,
),
);
addSubcommand(
@@ -92,20 +108,36 @@ abstract base class WidgetPreviewSubCommandBase extends FlutterCommand {
final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with CreateBase {
WidgetPreviewStartCommand({
this.verboseHelp = false,
required this.logger,
required this.fs,
required this.projectFactory,
required this.cache,
required this.platform,
required this.shutdownHooks,
required this.os,
}) {
addPubOptions();
argParser.addFlag(
kLaunchPreviewer,
defaultsTo: true,
help: 'Launches the widget preview environment.',
// Should only be used for testing.
hide: !verboseHelp,
);
}
static const String kWidgetPreviewScaffoldName = 'widget_preview_scaffold';
static const String kLaunchPreviewer = 'launch-previewer';
@override
String get description => 'Starts the widget preview environment.';
@override
String get name => 'start';
final bool verboseHelp;
@override
final FileSystem fs;
@@ -117,6 +149,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
final Cache cache;
final Platform platform;
final ShutdownHooks shutdownHooks;
final OperatingSystemUtils os;
late final PreviewDetector _previewDetector = PreviewDetector(
logger: logger,
fs: fs,
@@ -125,6 +163,9 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
late final PreviewCodeGenerator _previewCodeGenerator;
/// The currently running instance of the widget preview scaffold.
AppInstance? _widgetPreviewApp;
@override
Future<FlutterCommandResult> runCommand() async {
final FlutterProject rootProject = getRootProject();
@@ -132,44 +173,231 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C
// Check to see if a preview scaffold has already been generated. If not,
// generate one.
if (!widgetPreviewScaffold.existsSync()) {
final bool generateScaffoldProject = !widgetPreviewScaffold.existsSync();
widgetPreviewScaffold.createSync();
if (generateScaffoldProject) {
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
logger.printStatus('Creating widget preview scaffolding at: ${widgetPreviewScaffold.path}');
await generateApp(
<String>['widget_preview_scaffold'],
<String>['app', kWidgetPreviewScaffoldName],
widgetPreviewScaffold,
createTemplateContext(
organization: 'flutter',
projectName: 'widget_preview_scaffold',
projectName: kWidgetPreviewScaffoldName,
titleCaseProjectName: 'Widget Preview Scaffold',
flutterRoot: Cache.flutterRoot!,
dartSdkVersionBounds: '^${cache.dartSdkBuild}',
linux: const LocalPlatform().isLinux,
macos: const LocalPlatform().isMacOS,
windows: const LocalPlatform().isWindows,
linux: platform.isLinux,
macos: platform.isMacOS,
windows: platform.isWindows,
),
overwrite: true,
generateMetadata: false,
);
await _populatePreviewPubspec(rootProject: rootProject);
// WARNING: this access of widgetPreviewScaffoldProject needs to happen
// after we generate the scaffold project as invoking the getter triggers
// lazy initialization of the preview scaffold's FlutterManifest before
// the scaffold project's pubspec has been generated.
// TODO(bkonyi): add logic to rebuild after SDK updates
await initialBuild(widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject);
}
// 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: fs,
);
// TODO(matanlurey): Remove this comment once flutter_gen is removed.
//
// Tracking removal: https://github.com/flutter/flutter/issues/102983.
//
// Populate the pubspec after the initial build to avoid blowing away the package_config.json
// which may have manual changes for flutter_gen support.
await _populatePreviewPubspec(rootProject: rootProject);
final PreviewMapping initialPreviews = await _previewDetector.initialize(rootProject.directory);
_previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffold(initialPreviews);
if (boolArg(kLaunchPreviewer)) {
shutdownHooks.addShutdownHook(() async {
await _widgetPreviewApp?.stop();
});
_widgetPreviewApp = await runPreviewEnvironment(
widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject,
);
final int result = await _widgetPreviewApp!.runner.waitForAppToFinish();
if (result != 0) {
throwToolExit('Failed to launch the widget previewer.', exitCode: result);
}
}
await _previewDetector.dispose();
return FlutterCommandResult.success();
}
void onChangeDetected(PreviewMapping previews) {
// TODO(bkonyi): perform hot reload
logger.printStatus('Triggering reload based on change to preview set: $previews');
_widgetPreviewApp?.restart();
}
/// Builds the application binary for the widget preview scaffold the first
/// time the widget preview command is run.
///
/// The resulting binary is used to speed up subsequent widget previewer launches
/// by acting as a basic scaffold to load previews into using hot reload / restart.
Future<void> initialBuild({required FlutterProject widgetPreviewScaffoldProject}) async {
// TODO(bkonyi): handle error case where desktop device isn't enabled.
await widgetPreviewScaffoldProject.ensureReadyForPlatformSpecificTooling(
linuxPlatform: platform.isLinux,
macOSPlatform: platform.isMacOS,
windowsPlatform: platform.isWindows,
allowedPlugins: const <String>[],
);
// Generate initial package_config.json, otherwise the build will fail.
await pub.get(
context: PubContext.create,
project: widgetPreviewScaffoldProject,
offline: offline,
outputMode: PubOutputMode.summaryOnly,
);
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
logger.printStatus('Performing initial build of the Widget Preview Scaffold...');
final BuildInfo buildInfo = BuildInfo(
BuildMode.debug,
null,
treeShakeIcons: false,
packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
);
if (platform.isMacOS) {
await buildMacOS(
flutterProject: widgetPreviewScaffoldProject,
buildInfo: buildInfo,
verboseLogging: false,
);
} else if (platform.isLinux) {
await buildLinux(
widgetPreviewScaffoldProject.linux,
buildInfo,
targetPlatform:
os.hostPlatform == HostPlatform.linux_x64
? TargetPlatform.linux_x64
: TargetPlatform.linux_arm64,
logger: logger,
);
} else if (platform.isWindows) {
await buildWindows(
widgetPreviewScaffoldProject.windows,
buildInfo,
os.hostPlatform == HostPlatform.windows_x64
? TargetPlatform.windows_x64
: TargetPlatform.windows_arm64,
);
} else {
throw UnimplementedError();
}
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
logger.printStatus('Widget Preview Scaffold initial build complete.');
}
/// Returns the path to a prebuilt widget_preview_scaffold application binary.
String prebuiltApplicationBinaryPath({required FlutterProject widgetPreviewScaffoldProject}) {
assert(platform.isLinux || platform.isMacOS || platform.isWindows);
String path;
if (platform.isMacOS) {
path = fs.path.join(
getMacOSBuildDirectory(),
'Build/Products/Debug/widget_preview_scaffold.app',
);
} else if (platform.isLinux) {
path = fs.path.join(
getLinuxBuildDirectory(
os.hostPlatform == HostPlatform.linux_x64
? TargetPlatform.linux_x64
: TargetPlatform.linux_arm64,
),
'debug/bundle/widget_preview_scaffold',
);
} else if (platform.isWindows) {
path = fs.path.join(
getWindowsBuildDirectory(
os.hostPlatform == HostPlatform.windows_x64
? TargetPlatform.windows_x64
: TargetPlatform.windows_arm64,
),
'runner/Debug/widget_preview_scaffold.exe',
);
} else {
throw StateError('Unknown OS');
}
path = fs.path.join(widgetPreviewScaffoldProject.directory.path, path);
if (fs.typeSync(path) == FileSystemEntityType.notFound) {
logger.printStatus(fs.currentDirectory.toString());
throw StateError('Could not find prebuilt application binary at $path.');
}
return path;
}
Future<AppInstance> runPreviewEnvironment({
required FlutterProject widgetPreviewScaffoldProject,
}) async {
final AppInstance app;
try {
// Since the only target supported by the widget preview scaffold is the host's desktop
// device, only a single desktop device should be returned.
final List<Device> devices = await deviceManager!.getDevices(
filter: DeviceDiscoveryFilter(
supportFilter: DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProject(
flutterProject: widgetPreviewScaffoldProject,
),
deviceConnectionInterface: DeviceConnectionInterface.attached,
),
);
assert(devices.length == 1);
final Device device = devices.first;
// We launch from a prebuilt widget preview scaffold instance to reduce launch times after
// the first run.
final File prebuiltApplicationBinary = fs.file(
prebuiltApplicationBinaryPath(widgetPreviewScaffoldProject: widgetPreviewScaffoldProject),
);
const String? kEmptyRoute = null;
const bool kEnableHotReload = true;
app = await Daemon.createMachineDaemon().appDomain.startApp(
device,
widgetPreviewScaffoldProject.directory.path,
bundle.defaultMainPath,
kEmptyRoute, // route
DebuggingOptions.enabled(
BuildInfo(
BuildMode.debug,
null,
treeShakeIcons: false,
packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path,
),
),
kEnableHotReload, // hot mode
applicationBinary: prebuiltApplicationBinary,
trackWidgetCreation: false,
projectRootPath: widgetPreviewScaffoldProject.directory.path,
);
} on Exception catch (error) {
throwToolExit(error.toString());
}
// Immediately perform a hot restart to ensure new previews are loaded into the prebuilt
// application.
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
logger.printStatus('Loading previews into the Widget Preview Scaffold...');
await app.restart(fullRestart: true);
// WARNING: this log message is used by test/integration.shard/widget_preview_test.dart
logger.printStatus('Done loading previews.');
return app;
}
@visibleForTesting

View File

@@ -80,9 +80,9 @@ Future<void> buildLinux(
final Status status = logger.startProgress('Building Linux application...');
final String buildModeName = buildInfo.mode.cliName;
final Directory platformBuildDirectory = globals.fs.directory(
getLinuxBuildDirectory(targetPlatform),
);
final Directory platformBuildDirectory = globals.fs
.directory(linuxProject.parent.directory.path)
.childDirectory(getLinuxBuildDirectory(targetPlatform));
final Directory buildDirectory = platformBuildDirectory.childDirectory(buildModeName);
try {
await _runCmake(

View File

@@ -178,6 +178,7 @@ class BuildableMacOSApp extends MacOSApp {
}
return globals.fs.path.join(
project.parent.directory.path,
getMacOSBuildDirectory(),
'Build',
'Products',

View File

@@ -111,7 +111,9 @@ Future<void> buildMacOS({
final ProjectMigration migration = ProjectMigration(migrators);
await migration.run();
final Directory flutterBuildDir = globals.fs.directory(getMacOSBuildDirectory());
final Directory flutterBuildDir = flutterProject.directory.childDirectory(
getMacOSBuildDirectory(),
);
if (!flutterBuildDir.existsSync()) {
flutterBuildDir.createSync(recursive: true);
}

View File

@@ -52,13 +52,13 @@ class PreviewCodeGenerator {
b
..body =
literalList(<Object?>[
for (final MapEntry<String, List<String>>(
key: String path,
for (final MapEntry<PreviewPath, List<String>>(
key: (path: String _, :Uri uri),
value: List<String> previewMethods,
)
in previews.entries) ...<Object?>[
for (final String method in previewMethods)
refer(method, path).call(<Expression>[]),
refer(method, uri.toString()).call(<Expression>[]),
],
]).code
..name = 'previews'

View File

@@ -18,7 +18,15 @@ import '../base/logger.dart';
import '../base/utils.dart';
import 'preview_code_generator.dart';
typedef PreviewMapping = Map<String, List<String>>;
/// A path / URI pair used to map previews to a file.
///
/// We don't just use a path or a URI as the file watcher doesn't report URIs
/// (e.g., package:*) but the analyzer APIs do, and the code generator emits
/// package URIs for preview imports.
typedef PreviewPath = ({String path, Uri uri});
/// Represents a set of previews for a given file.
typedef PreviewMapping = Map<PreviewPath, List<String>>;
class PreviewDetector {
PreviewDetector({required this.fs, required this.logger, required this.onChangeDetected});
@@ -38,7 +46,7 @@ class PreviewDetector {
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();
final String eventPath = event.path;
// 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') ||
@@ -49,7 +57,9 @@ class PreviewDetector {
final PreviewMapping filePreviewsMapping = findPreviewFunctions(
fs.file(Uri.file(event.path)),
);
if (filePreviewsMapping.isEmpty && !_pathToPreviews.containsKey(eventPath)) {
final bool hasExistingPreviews =
_pathToPreviews.keys.where((PreviewPath e) => e.path == event.path).isNotEmpty;
if (filePreviewsMapping.isEmpty && !hasExistingPreviews) {
// No previews found or removed, nothing to do.
return;
}
@@ -59,20 +69,21 @@ class PreviewDetector {
}
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');
final MapEntry<PreviewPath, List<String>>(
key: PreviewPath location,
value: List<String> filePreviews,
) = filePreviewsMapping.entries.first;
logger.printStatus('Updated previews for ${location.uri}: $filePreviews');
if (filePreviews.isNotEmpty) {
final List<String>? currentPreviewsForFile = _pathToPreviews[eventPath];
final List<String>? currentPreviewsForFile = _pathToPreviews[location];
if (filePreviews != currentPreviewsForFile) {
_pathToPreviews[eventPath] = filePreviews;
_pathToPreviews[location] = filePreviews;
}
}
} else {
// The file previously had previews that were removed.
logger.printStatus('Previews removed from $eventPath');
_pathToPreviews.remove(eventPath);
_pathToPreviews.removeWhere((PreviewPath e, _) => e.path == eventPath);
}
onChangeDetected(_pathToPreviews);
});
@@ -106,7 +117,8 @@ class PreviewDetector {
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>[];
final List<String> previewEntries =
previews[(path: unit.path, uri: unit.uri)] ?? <String>[];
for (final SyntacticEntity entity in unit.unit.childEntities) {
if (entity is FunctionDeclaration && !entity.name.toString().startsWith('_')) {
bool foundPreview = false;
@@ -127,7 +139,7 @@ class PreviewDetector {
}
}
if (previewEntries.isNotEmpty) {
previews[unit.uri.toString()] = previewEntries;
previews[(path: unit.path, uri: unit.uri)] = previewEntries;
}
}
} else {

View File

@@ -2,6 +2,25 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Future<void> main() async {
// TODO(bkonyi): implement.
// TODO(bkonyi): Implement.
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello World!'),
),
),
);
}
}

View File

@@ -91,7 +91,7 @@ void main() {
'-DFLUTTER_TARGET_PLATFORM=linux-$target',
'/linux',
],
workingDirectory: 'build/linux/$target/$buildMode',
workingDirectory: '/build/linux/$target/$buildMode',
onRun: onRun,
);
}
@@ -105,7 +105,7 @@ void main() {
String stdout = '',
}) {
return FakeCommand(
command: <String>['ninja', '-C', 'build/linux/$target/$buildMode', 'install'],
command: <String>['ninja', '-C', '/build/linux/$target/$buildMode', 'install'],
environment: environment,
onRun: onRun,
stdout: stdout,
@@ -396,7 +396,7 @@ void main() {
// This contains a mix of routine build output and various types of errors
// (Dart error, compile error, link error), edited down for compactness.
const String stdout = r'''
ninja: Entering directory `build/linux/x64/release'
ninja: Entering directory `/build/linux/x64/release'
[1/6] Generating /foo/linux/flutter/ephemeral/libflutter_linux_gtk.so, /foo/linux/flutter/ephemeral/flutter_linux/flutter_linux.h, _phony
lib/main.dart:4:3: Error: Method not found: 'foo'.
[2/6] Building CXX object CMakeFiles/foo.dir/main.cc.o

View File

@@ -5,6 +5,9 @@
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/widget_preview.dart';
import 'package:flutter_tools/src/convert.dart';
@@ -24,6 +27,7 @@ void main() {
late WidgetPreviewStartCommand command;
late FlutterProject rootProject;
late Logger logger;
final Platform platform = FakePlatform.fromPlatform(const LocalPlatform());
setUp(() {
fileSystem = MemoryFileSystem.test();
@@ -33,7 +37,15 @@ void main() {
fs: fileSystem,
projectFactory: FakeFlutterProjectFactory(),
logger: logger,
cache: Cache.test(processManager: processManager),
cache: Cache.test(processManager: processManager, platform: platform),
platform: platform,
shutdownHooks: ShutdownHooks(),
os: OperatingSystemUtils(
fileSystem: fileSystem,
logger: logger,
platform: platform,
processManager: processManager,
),
);
rootProject = FakeFlutterProject(
projectRoot: 'some_project',

View File

@@ -10,7 +10,9 @@ import 'package:flutter_tools/src/base/bot_detector.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/base/signals.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/widget_preview.dart';
@@ -40,7 +42,7 @@ void main() {
botDetector = const FakeBotDetector(false);
tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_create_test.');
mockStdio = FakeStdio();
platform = const LocalPlatform();
platform = FakePlatform.fromPlatform(const LocalPlatform());
});
tearDown(() {
@@ -58,10 +60,19 @@ void main() {
Future<void> runWidgetPreviewCommand(List<String> arguments) async {
final CommandRunner<void> runner = createTestCommandRunner(
WidgetPreviewCommand(
verboseHelp: false,
logger: logger,
fs: fs,
projectFactory: FlutterProjectFactory(logger: logger, fileSystem: fs),
cache: Cache.test(processManager: loggingProcessManager, platform: platform),
platform: platform,
shutdownHooks: ShutdownHooks(),
os: OperatingSystemUtils(
fileSystem: fs,
processManager: loggingProcessManager,
logger: logger,
platform: platform,
),
),
);
await runner.run(<String>['widget-preview', ...arguments]);
@@ -74,6 +85,7 @@ void main() {
await runWidgetPreviewCommand(<String>[
'start',
...?arguments,
'--no-launch-previewer',
if (rootProject != null) rootProject.path,
]);
final Directory widgetPreviewScaffoldDir = widgetPreviewScaffoldFromRootProject(
@@ -245,7 +257,7 @@ import 'package:flutter_project/foo.dart' as _i1;import 'package:widget_preview/
);
testUsingContext(
'clean deletes .dart_tool/widget_preview_scaffold',
'start finds existing previews in the CWD and injects them into ${PreviewCodeGenerator.generatedPreviewFilePath}',
() async {
final Directory rootProject = await createRootProject();
await startWidgetPreview(rootProject: rootProject);

View File

@@ -9,6 +9,7 @@ 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:flutter_tools/src/widget_preview/preview_detector.dart';
import 'package:test/test.dart';
import '../../src/context.dart';
@@ -39,9 +40,9 @@ void main() {
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'],
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(<PreviewPath, List<String>>{
(path: '', uri: Uri(path: 'foo.dart')): <String>['preview'],
(path: '', uri: Uri(path: 'src/bar.dart')): <String>['barPreview1', 'barPreview2'],
});
expect(generatedPreviewFile, exists);
@@ -59,7 +60,9 @@ import 'foo.dart' as _i1;import 'src/bar.dart' as _i2;import 'package:widget_pre
expect(generatedPreviewFile.readAsStringSync(), expectedGeneratedPreviewFileContents);
// Regenerate the generated file with no previews.
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(const <String, List<String>>{});
codeGenerator.populatePreviewsInGeneratedPreviewScaffold(
const <PreviewPath, List<String>>{},
);
expect(generatedPreviewFile, exists);
// The generated file should only contain:

View File

@@ -6,6 +6,7 @@ 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/platform.dart';
import 'package:flutter_tools/src/base/signals.dart';
import 'package:flutter_tools/src/widget_preview/preview_detector.dart';
import 'package:test/test.dart';
@@ -17,16 +18,20 @@ 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);
PreviewPath addPreviewContainingFile(Directory projectRoot, List<String> path) {
final File file =
projectRoot.childDirectory('lib').childFile(path.join(const LocalPlatform().pathSeparator))
..createSync(recursive: true)
..writeAsStringSync(previewContainingFileContents);
return (path: file.path, uri: file.uri);
}
File addNonPreviewContainingFile(Directory projectRoot, String path) {
return projectRoot.childDirectory('lib').childFile(path)
..createSync(recursive: true)
..writeAsStringSync(nonPreviewContainingFileContents);
PreviewPath addNonPreviewContainingFile(Directory projectRoot, List<String> path) {
final File file =
projectRoot.childDirectory('lib').childFile(path.join(const LocalPlatform().pathSeparator))
..createSync(recursive: true)
..writeAsStringSync(nonPreviewContainingFileContents);
return (path: file.path, uri: file.uri);
}
void main() {
@@ -62,29 +67,32 @@ void main() {
});
testUsingContext('can detect previews in existing files', () async {
final List<File> previewFiles = <File>[
addPreviewContainingFile(projectRoot, 'foo.dart'),
addPreviewContainingFile(projectRoot, 'src/bar.dart'),
final List<PreviewPath> previewFiles = <PreviewPath>[
addPreviewContainingFile(projectRoot, <String>['foo.dart']),
addPreviewContainingFile(projectRoot, <String>['src', 'bar.dart']),
];
addNonPreviewContainingFile(projectRoot, 'baz.dart');
addNonPreviewContainingFile(projectRoot, <String>['baz.dart']);
final PreviewMapping mapping = previewDetector.findPreviewFunctions(projectRoot);
expect(mapping.keys.toSet(), previewFiles.map((File e) => e.uri.toString()).toSet());
expect(mapping.keys.toSet(), previewFiles.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 PreviewMapping expectedInitialMapping = <PreviewPath, List<String>>{
addPreviewContainingFile(projectRoot, <String>['foo.dart']): <String>['previews'],
addPreviewContainingFile(projectRoot, <String>['src', 'bar.dart']): <String>['previews'],
};
final File nonPreviewContainingFile = addNonPreviewContainingFile(projectRoot, 'baz.dart');
final PreviewPath nonPreviewContainingFile = addNonPreviewContainingFile(
projectRoot,
<String>['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>>{
expect(updated, <PreviewPath, List<String>>{
...expectedInitialMapping,
nonPreviewContainingFile.uri.toString(): <String>['previews'],
nonPreviewContainingFile: <String>['previews'],
});
completer.complete();
};
@@ -94,7 +102,7 @@ void main() {
// Update the file without an existing preview to include a preview and ensure it triggers
// the preview detector.
addPreviewContainingFile(projectRoot, 'baz.dart');
addPreviewContainingFile(projectRoot, <String>['baz.dart']);
await completer.future;
completer = Completer<void>();
@@ -106,7 +114,7 @@ void main() {
// Update the file with an existing preview to remove the preview and ensure it triggers
// the preview detector.
addNonPreviewContainingFile(projectRoot, 'baz.dart');
addNonPreviewContainingFile(projectRoot, <String>['baz.dart']);
await completer.future;
});
});

View File

@@ -0,0 +1,92 @@
// 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 'dart:convert';
import 'package:file/file.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:process/process.dart';
import '../src/common.dart';
import 'test_data/basic_project.dart';
import 'test_utils.dart';
const List<String> firstLaunchMessages = <String>[
'Creating widget preview scaffolding at:',
'Performing initial build of the Widget Preview Scaffold...',
'Widget Preview Scaffold initial build complete.',
'Loading previews into the Widget Preview Scaffold...',
'Done loading previews.',
];
const List<String> subsequentLaunchMessages = <String>[
'Loading previews into the Widget Preview Scaffold...',
'Done loading previews.',
];
void main() {
late Directory tempDir;
Process? process;
final BasicProject project = BasicProject();
const ProcessManager processManager = LocalProcessManager();
setUp(() async {
tempDir = createResolvedTempDirectorySync('widget_preview_test.');
await project.setUpIn(tempDir);
});
tearDown(() async {
process?.kill();
process = null;
tryToDelete(tempDir);
});
Future<void> runWidgetPreview({required List<String> expectedMessages}) async {
expect(expectedMessages, isNotEmpty);
int i = 0;
process = await processManager.start(<String>[
flutterBin,
'widget-preview',
'start',
], workingDirectory: tempDir.path);
final Completer<void> completer = Completer<void>();
process!.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) {
printOnFailure('STDOUT: $msg');
if (completer.isCompleted) {
return;
}
if (msg.contains(expectedMessages[i])) {
++i;
}
if (i == expectedMessages.length) {
completer.complete();
}
});
process!.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) {
printOnFailure('STDERR: $msg');
});
await completer.future;
process!.kill();
process = null;
}
group('flutter widget-preview start', () {
testWithoutContext('smoke test', () async {
await runWidgetPreview(expectedMessages: firstLaunchMessages);
});
testWithoutContext('does not rebuild project on subsequent runs', () async {
// The first run of 'flutter widget-preview start' should generate a new preview scaffold and
// pre-build the application.
await runWidgetPreview(expectedMessages: firstLaunchMessages);
// We shouldn't regenerate the scaffold after the initial run.
await runWidgetPreview(expectedMessages: subsequentLaunchMessages);
});
});
}