Skip to content

Commit 5b35abd

Browse files
authored
fix(challenge): use headless in-app webview for test runner (#1781)
* Use headless in-app webview for challenge test runner * fix challenge webview lifecycle
1 parent 00e9c91 commit 5b35abd

2 files changed

Lines changed: 97 additions & 65 deletions

File tree

mobile-app/lib/ui/views/learn/challenge/challenge_view.dart

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
import 'dart:developer';
2-
31
import 'package:flutter/material.dart';
42
import 'package:flutter_html/flutter_html.dart';
5-
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
63
import 'package:freecodecamp/enums/panel_type.dart';
74
import 'package:freecodecamp/models/learn/challenge_model.dart';
85
import 'package:freecodecamp/models/learn/curriculum_model.dart';
96
import 'package:freecodecamp/models/learn/daily_challenge_model.dart';
107
import 'package:freecodecamp/ui/theme/fcc_theme.dart';
118
import 'package:freecodecamp/ui/views/learn/challenge/challenge_viewmodel.dart';
12-
import 'package:freecodecamp/ui/views/learn/test_runner.dart';
139
import 'package:freecodecamp/ui/views/learn/widgets/challenge_widgets/project_preview.dart';
1410
import 'package:freecodecamp/ui/views/learn/widgets/challenge_widgets/symbol_bar.dart';
1511
import 'package:freecodecamp/ui/views/learn/widgets/console/console_view.dart';
@@ -270,52 +266,6 @@ class ChallengeView extends StatelessWidget {
270266
),
271267
Row(
272268
children: [
273-
SizedBox(
274-
height: 1,
275-
width: 1,
276-
child: InAppWebView(
277-
initialData: InAppWebViewInitialData(
278-
data:
279-
'<html><head><title>Test Runner</title></head><body></body></html>',
280-
mimeType: 'text/html',
281-
baseUrl: WebUri('http://localhost:8080/test-runner'),
282-
),
283-
onWebViewCreated: (controller) {
284-
model.setTestController = controller;
285-
},
286-
onConsoleMessage: (controller, console) {
287-
if (console.messageLevel == ConsoleMessageLevel.LOG) {
288-
model.setUserConsoleMessages = [
289-
...model.userConsoleMessages,
290-
'<p>${console.message}</p>',
291-
];
292-
}
293-
},
294-
onLoadStop: (controller, url) async {
295-
ScriptBuilder builder = ScriptBuilder();
296-
final res = await controller.callAsyncJavaScript(
297-
functionBody: ScriptBuilder.runnerScript,
298-
arguments: {
299-
'userCode': '',
300-
'workerType':
301-
builder.getWorkerType(challenge.challengeType),
302-
'combinedCode': '',
303-
'editableRegionContent': '',
304-
'hooks': {
305-
'beforeAll': '',
306-
'beforeEach': '',
307-
'afterEach': '',
308-
},
309-
},
310-
);
311-
log('TestRunner: $res');
312-
},
313-
initialSettings: InAppWebViewSettings(
314-
isInspectable: true,
315-
mediaPlaybackRequiresUserGesture: false,
316-
),
317-
),
318-
),
319269
..._panelIconButtons(
320270
model,
321271
challenge,

mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart

Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)