@@ -123,6 +123,48 @@ class ChallengeViewModel extends BaseViewModel {
123123 isInspectable: true ,
124124 ),
125125 );
126+ late final HeadlessInAppWebView _testRunnerWebView = HeadlessInAppWebView (
127+ initialData: InAppWebViewInitialData (
128+ data: '<html><head><title>Test Runner</title></head><body></body></html>' ,
129+ mimeType: 'text/html' ,
130+ baseUrl: WebUri ('http://localhost:8080/test-runner' ),
131+ ),
132+ onWebViewCreated: (controller) {
133+ _testController = controller;
134+ },
135+ onConsoleMessage: (controller, console) {
136+ if (console.messageLevel == ConsoleMessageLevel .LOG ) {
137+ setUserConsoleMessages = [
138+ ...userConsoleMessages,
139+ '<p>${console .message }</p>' ,
140+ ];
141+ }
142+ },
143+ onLoadStop: (controller, url) async {
144+ ScriptBuilder builder = ScriptBuilder ();
145+ final res = await controller.callAsyncJavaScript (
146+ functionBody: ScriptBuilder .runnerScript,
147+ arguments: {
148+ 'userCode' : '' ,
149+ 'workerType' : builder.getWorkerType (challenge? .challengeType ?? 0 ),
150+ 'combinedCode' : '' ,
151+ 'editableRegionContent' : '' ,
152+ 'hooks' : {
153+ 'beforeAll' : '' ,
154+ 'beforeEach' : '' ,
155+ 'afterEach' : '' ,
156+ },
157+ },
158+ );
159+ log ('TestRunner: $res ' );
160+ },
161+ initialSettings: InAppWebViewSettings (
162+ isInspectable: true ,
163+ mediaPlaybackRequiresUserGesture: false ,
164+ ),
165+ );
166+ bool _closingWebViews = false ;
167+ bool _webViewsClosed = false ;
126168
127169 bool _mounted = false ;
128170 bool get mounted => _mounted;
@@ -171,11 +213,6 @@ class ChallengeViewModel extends BaseViewModel {
171213 notifyListeners ();
172214 }
173215
174- set setTestController (InAppWebViewController controller) {
175- _testController = controller;
176- notifyListeners ();
177- }
178-
179216 set setShowPanel (bool value) {
180217 _showPanel = value;
181218 notifyListeners ();
@@ -298,9 +335,6 @@ class ChallengeViewModel extends BaseViewModel {
298335 required Challenge challenge,
299336 required DateTime ? challengeDate,
300337 }) async {
301- await _babelWebView.run ();
302- await _localhostServer.start ();
303-
304338 _challengeDate = challengeDate;
305339 _isDailyChallenge = challengeDate != null ;
306340
@@ -313,9 +347,29 @@ class ChallengeViewModel extends BaseViewModel {
313347 setChallenge = challenge;
314348 setBlock = block;
315349
350+ final webViewsStarted = await _startWebViews ();
351+ if (! webViewsStarted) return ;
352+
316353 listenToSymbolBarScrollController ();
317354 }
318355
356+ bool get _shouldStopWebViewSetup => _closingWebViews || _webViewsClosed;
357+
358+ Future <bool > _startWebViews () async {
359+ final setupSteps = [
360+ _localhostServer.start,
361+ _babelWebView.run,
362+ _testRunnerWebView.run,
363+ ];
364+
365+ for (final setupStep in setupSteps) {
366+ if (_shouldStopWebViewSetup) return false ;
367+ await setupStep ();
368+ }
369+
370+ return ! _shouldStopWebViewSetup;
371+ }
372+
319373 Future <void > setSelectedDailyChallengeLanguage (
320374 DailyChallengeLanguage lang, DateTime challengeDate) async {
321375 setShowTestsPanel = false ;
@@ -340,8 +394,27 @@ class ChallengeViewModel extends BaseViewModel {
340394 }
341395
342396 void closeWebViews () async {
343- await _babelWebView.dispose ();
344- await _localhostServer.close ();
397+ if (_closingWebViews || _webViewsClosed) return ;
398+ _closingWebViews = true ;
399+
400+ try {
401+ await _testRunnerWebView.dispose ();
402+ } catch (e) {
403+ log ('Test runner dispose error: $e ' );
404+ }
405+ try {
406+ await _babelWebView.dispose ();
407+ } catch (e) {
408+ log ('Babel dispose error: $e ' );
409+ }
410+ try {
411+ await _localhostServer.close ();
412+ } catch (e) {
413+ log ('Localhost server close error: $e ' );
414+ }
415+
416+ _webViewsClosed = true ;
417+ _closingWebViews = false ;
345418 }
346419
347420 void initFile (
@@ -731,8 +804,19 @@ class ChallengeViewModel extends BaseViewModel {
731804 return ;
732805 }
733806
807+ InAppWebViewController ? runnerController = testController;
808+ if (runnerController == null ) {
809+ setTestConsoleMessages = [
810+ ...testConsoleMessages,
811+ '<p>Test runner is still initializing. Please try again.</p>' ,
812+ '<p>// tests completed</p>' ,
813+ ];
814+ setIsRunningTests = false ;
815+ return ;
816+ }
817+
734818 if ([1 , 26 , 28 ].contains (challenge! .challengeType)) {
735- final evalResult = await testController ! .callAsyncJavaScript (
819+ final evalResult = await runnerController .callAsyncJavaScript (
736820 functionBody: userCode,
737821 );
738822 if (evalResult != null && evalResult.error != null ) {
@@ -744,9 +828,7 @@ class ChallengeViewModel extends BaseViewModel {
744828 }
745829 }
746830
747- // TODO: Handle the case when the test runner is not created
748- // ignore: unused_local_variable
749- final updateTestRunnerRes = await testController! .callAsyncJavaScript (
831+ await runnerController.callAsyncJavaScript (
750832 functionBody: ScriptBuilder .runnerScript,
751833 arguments: {
752834 'userCode' : userCode,
@@ -763,7 +845,7 @@ class ChallengeViewModel extends BaseViewModel {
763845
764846 for (int i = 0 ; i < challenge! .tests.length; i++ ) {
765847 ChallengeTest test = challenge! .tests[i];
766- final testRes = await testController ! .callAsyncJavaScript (
848+ final testRes = await runnerController .callAsyncJavaScript (
767849 functionBody: ScriptBuilder .testExecutionScript,
768850 arguments: {
769851 'testStr' : test.javaScript,
0 commit comments