@@ -26,6 +26,7 @@ import 'safari_installation.dart';
2626import 'supported_browsers.dart' ;
2727import 'test_platform.dart' ;
2828import 'utils.dart' ;
29+ import 'watcher.dart' ;
2930
3031/// The type of tests requested by the tool user.
3132enum TestTypesRequested {
@@ -48,6 +49,12 @@ class TestCommand extends Command<bool> with ArgUtils {
4849 'opportunity to add breakpoints or inspect loaded code before '
4950 'running the code.' ,
5051 )
52+ ..addFlag (
53+ 'watch' ,
54+ abbr: 'w' ,
55+ help: 'Run in watch mode so the tests re-run whenever a change is '
56+ 'made.' ,
57+ )
5158 ..addFlag (
5259 'unit-tests-only' ,
5360 defaultsTo: false ,
@@ -100,6 +107,8 @@ class TestCommand extends Command<bool> with ArgUtils {
100107 @override
101108 final String description = 'Run tests.' ;
102109
110+ bool get isWatchMode => boolArg ('watch' );
111+
103112 TestTypesRequested testTypesRequested = null ;
104113
105114 /// How many dart2js build tasks are running at the same time.
@@ -146,25 +155,80 @@ class TestCommand extends Command<bool> with ArgUtils {
146155 await macOsInfo.printInformation ();
147156 }
148157
149- switch (testTypesRequested) {
150- case TestTypesRequested .unit:
151- return runUnitTests ();
152- case TestTypesRequested .integration:
153- return runIntegrationTests ();
154- case TestTypesRequested .all:
155- if (runAllTests && isIntegrationTestsAvailable) {
156- bool unitTestResult = await runUnitTests ();
157- bool integrationTestResult = await runIntegrationTests ();
158- if (integrationTestResult != unitTestResult) {
159- print ('Tests run. Integration tests passed: $integrationTestResult '
160- 'unit tests passed: $unitTestResult ' );
158+ final Pipeline testPipeline = Pipeline (steps: < PipelineStep > [
159+ () async => clearTerminalScreen (),
160+ () => runTestsOfType (testTypesRequested),
161+ ]);
162+ await testPipeline.start ();
163+
164+ if (isWatchMode) {
165+ final FilePath dir = FilePath .fromWebUi ('' );
166+ print ('' );
167+ print ('Initial test run is done!' );
168+ print ('Watching ${dir .relativeToCwd }/lib and ${dir .relativeToCwd }/test to re-run tests' );
169+ print ('' );
170+ PipelineWatcher (
171+ dir: dir.absolute,
172+ pipeline: testPipeline,
173+ ignore: (event) {
174+ // Ignore font files that are copied whenever tests run.
175+ if (event.path.endsWith ('.ttf' )) {
176+ return true ;
177+ }
178+
179+ // Ignore auto-generated JS files.
180+ // The reason we are using `.contains()` instead of `.endsWith()` is
181+ // because the auto-generated files could end with any of the
182+ // following:
183+ //
184+ // - browser_test.dart.js
185+ // - browser_test.dart.js.map
186+ // - browser_test.dart.js.deps
187+ if (event.path.contains ('browser_test.dart.js' )) {
188+ return true ;
161189 }
162- return integrationTestResult && unitTestResult;
163- } else {
164- return await runUnitTests ();
190+
191+ // React to changes in lib/ and test/ folders.
192+ final String relativePath = path.relative (event.path, from: dir.absolute);
193+ if (relativePath.startsWith ('lib/' ) || relativePath.startsWith ('test/' )) {
194+ return false ;
195+ }
196+
197+ // Ignore anything else.
198+ return true ;
165199 }
200+ ).start ();
201+ // Return a never-ending future.
202+ return Completer <bool >().future;
203+ } else {
204+ return true ;
205+ }
206+ }
207+
208+ Future <bool > runTestsOfType (TestTypesRequested testTypesRequested) async {
209+ try {
210+ switch (testTypesRequested) {
211+ case TestTypesRequested .unit:
212+ return runUnitTests ();
213+ case TestTypesRequested .integration:
214+ return runIntegrationTests ();
215+ case TestTypesRequested .all:
216+ if (runAllTests && isIntegrationTestsAvailable) {
217+ bool unitTestResult = await runUnitTests ();
218+ bool integrationTestResult = await runIntegrationTests ();
219+ if (integrationTestResult != unitTestResult) {
220+ print ('Tests run. Integration tests passed: $integrationTestResult '
221+ 'unit tests passed: $unitTestResult ' );
222+ }
223+ return integrationTestResult && unitTestResult;
224+ } else {
225+ return await runUnitTests ();
226+ }
227+ }
228+ throw UnimplementedError ('Unknown test type requested: $testTypesRequested ' );
229+ } on TestFailureException {
230+ return true ;
166231 }
167- return false ;
168232 }
169233
170234 Future <bool > runIntegrationTests () async {
@@ -499,7 +563,12 @@ class TestCommand extends Command<bool> with ArgUtils {
499563
500564 void _checkExitCode () {
501565 if (io.exitCode != 0 ) {
502- throw ToolException ('Process exited with exit code ${io .exitCode }.' );
566+ if (isWatchMode) {
567+ io.exitCode = 0 ;
568+ throw TestFailureException ();
569+ } else {
570+ throw ToolException ('Process exited with exit code ${io .exitCode }.' );
571+ }
503572 }
504573 }
505574
@@ -729,3 +798,5 @@ class TestBuildInput {
729798
730799 TestBuildInput (this .path, {this .forCanvasKit = false });
731800}
801+
802+ class TestFailureException implements Exception {}
0 commit comments