fix(player): Change vimeo player with chewie for bette performance
This commit is contained in:
parent
539d8bf6c2
commit
beee3717ba
|
|
@ -52,6 +52,7 @@ import 'package:yimaru_app/services/in_app_update_service.dart';
|
|||
import 'package:yimaru_app/ui/views/learn_program/learn_program_view.dart';
|
||||
import 'package:yimaru_app/ui/views/learn_course/learn_course_view.dart';
|
||||
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
|
||||
import 'package:yimaru_app/services/vimeo_service.dart';
|
||||
// @stacked-import
|
||||
|
||||
@StackedApp(
|
||||
|
|
@ -112,6 +113,7 @@ import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
|
|||
LazySingleton(classType: AudioPlayerService),
|
||||
LazySingleton(classType: VoiceRecorderService),
|
||||
LazySingleton(classType: InAppUpdateService),
|
||||
LazySingleton(classType: VimeoService),
|
||||
// @stacked-service
|
||||
],
|
||||
bottomsheets: [
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import '../services/permission_handler_service.dart';
|
|||
import '../services/secure_storage_service.dart';
|
||||
import '../services/smart_auth_service.dart';
|
||||
import '../services/status_checker_service.dart';
|
||||
import '../services/vimeo_service.dart';
|
||||
import '../services/voice_recorder_service.dart';
|
||||
|
||||
final locator = StackedLocator.instance;
|
||||
|
|
@ -55,4 +56,5 @@ Future<void> setupLocator(
|
|||
locator.registerLazySingleton(() => AudioPlayerService());
|
||||
locator.registerLazySingleton(() => VoiceRecorderService());
|
||||
locator.registerLazySingleton(() => InAppUpdateService());
|
||||
locator.registerLazySingleton(() => VimeoService());
|
||||
}
|
||||
|
|
|
|||
85
lib/services/vimeo_service.dart
Normal file
85
lib/services/vimeo_service.dart
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class VimeoService {
|
||||
Future<String?> getVideoUrl(String vimeoUrl) async {
|
||||
final videoId = _extractVideoId(vimeoUrl);
|
||||
|
||||
if (videoId == null) {
|
||||
throw Exception('Invalid Vimeo URL');
|
||||
}
|
||||
|
||||
final html = await _fetchHtml(videoId);
|
||||
|
||||
final config = _extractConfig(html);
|
||||
|
||||
if (config == null) {
|
||||
throw Exception('Failed to extract Vimeo config');
|
||||
}
|
||||
|
||||
return _extractHlsUrl(config);
|
||||
}
|
||||
|
||||
Future<String> _fetchHtml(String videoId) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('https://player.vimeo.com/video/$videoId'),
|
||||
headers: {
|
||||
'Origin': 'https://vimeo.com',
|
||||
'Referer': 'https://vimeo.com/',
|
||||
'Accept': 'text/html,application/xhtml+xml',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
},
|
||||
);
|
||||
|
||||
return response.body;
|
||||
}
|
||||
|
||||
static Map<String, dynamic>? _extractConfig(String html) {
|
||||
final regex = RegExp(
|
||||
r'window\.playerConfig\s*=\s*({.*?})\s*</script>',
|
||||
dotAll: true,
|
||||
);
|
||||
|
||||
final match = regex.firstMatch(html);
|
||||
if (match == null) return null;
|
||||
|
||||
final jsonString = match.group(1);
|
||||
if (jsonString == null) return null;
|
||||
|
||||
return jsonDecode(jsonString);
|
||||
}
|
||||
|
||||
String? _extractHlsUrl(Map<String, dynamic> config) {
|
||||
try {
|
||||
final files = config['request']?['files'];
|
||||
final hls = files?['hls'];
|
||||
|
||||
if (hls == null) return null;
|
||||
|
||||
final cdns = hls['cdns'] as Map<String, dynamic>;
|
||||
|
||||
// Prefer fastly_skyfire or fallback to first CDN
|
||||
final cdn = cdns['fastly_skyfire'] ?? cdns.values.first;
|
||||
|
||||
return cdn['url'];
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? _extractVideoId(String url) {
|
||||
try {
|
||||
final uri = Uri.parse(url);
|
||||
|
||||
if (uri.pathSegments.isNotEmpty) {
|
||||
return uri.pathSegments.last;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
|
|
|
|||
|
|
@ -312,9 +312,8 @@ TextStyle style14MG400 = const TextStyle(
|
|||
TextStyle style14DG500 =
|
||||
const TextStyle(color: kcDarkGrey, fontWeight: FontWeight.w500);
|
||||
|
||||
TextStyle style18MG500 =
|
||||
const TextStyle(fontSize: 18,color: kcMediumGrey, fontWeight: FontWeight.w500);
|
||||
|
||||
TextStyle style18MG500 = const TextStyle(
|
||||
fontSize: 18, color: kcMediumGrey, fontWeight: FontWeight.w500);
|
||||
|
||||
TextStyle style14DG400 = const TextStyle(
|
||||
color: kcDarkGrey,
|
||||
|
|
|
|||
|
|
@ -75,11 +75,11 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
_currentQuestionIndex = 0;
|
||||
_pageController.jumpToPage(_currentQuestionIndex);
|
||||
_currentAssessment = assessments[currentAssessmentIndex];
|
||||
await getAssessmentQuestions(
|
||||
_currentAssessment?.id ?? 0);
|
||||
await getAssessmentQuestions(_currentAssessment?.id ?? 0);
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
Map<String, dynamic> evaluateAssessment() {
|
||||
bool levelPassed = canPassLevel();
|
||||
if (levelPassed) {
|
||||
|
|
@ -166,8 +166,7 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
_currentQuestionIndex = 0;
|
||||
_proficiencyLevel = response['level'];
|
||||
_currentAssessment = assessments[currentAssessmentIndex];
|
||||
await getAssessmentQuestions(
|
||||
_currentAssessment?.id ?? 0);
|
||||
await getAssessmentQuestions(_currentAssessment?.id ?? 0);
|
||||
_pageController.jumpToPage(_currentQuestionIndex);
|
||||
} else {
|
||||
_proficiencyLevel = response['level'];
|
||||
|
|
@ -274,8 +273,7 @@ class AssessmentViewModel extends BaseViewModel {
|
|||
}
|
||||
|
||||
// Assessments
|
||||
Future<void> getAssessments() async =>
|
||||
await runBusyFuture(_getAssessments(),
|
||||
Future<void> getAssessments() async => await runBusyFuture(_getAssessments(),
|
||||
busyObject: StateObjects.assessments);
|
||||
|
||||
Future<void> _getAssessments() async {
|
||||
|
|
|
|||
|
|
@ -139,7 +139,8 @@ class AssessmentQuestionsScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
onTap: viewModel.selectedAnswers.containsKey(question.toString())
|
||||
? () => viewModel.nextQuestion()
|
||||
: null,
|
||||
text: viewModel.currentQuestionIndex == viewModel.assessmentQuestions.length - 1
|
||||
text: viewModel.currentQuestionIndex ==
|
||||
viewModel.assessmentQuestions.length - 1
|
||||
? 'Finish Level'
|
||||
: 'Continue',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -86,9 +86,7 @@ class AssessmentResultScreen extends ViewModelWidget<AssessmentViewModel> {
|
|||
);
|
||||
|
||||
Widget _buildIconWrapper(AssessmentViewModel viewModel) =>
|
||||
viewModel.proficiencyLevel != null
|
||||
? _buildIcon(viewModel)
|
||||
: Container();
|
||||
viewModel.proficiencyLevel != null ? _buildIcon(viewModel) : Container();
|
||||
|
||||
Widget _buildIcon(AssessmentViewModel viewModel) => SvgPicture.asset(
|
||||
'assets/icons/${viewModel.proficiencyLevel?.substring(0, 1).toLowerCase()}_${viewModel.proficiencyLevel?.substring(1).toLowerCase()}.svg');
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:vimeo_video_player/vimeo_video_player.dart';
|
||||
import 'package:yimaru_app/models/learn_lesson.dart';
|
||||
|
||||
import '../../common/app_colors.dart';
|
||||
import '../../common/enmus.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
import '../../widgets/custom_elevated_button.dart';
|
||||
import '../../widgets/empty_video_player.dart';
|
||||
import '../../widgets/small_app_bar.dart';
|
||||
import 'learn_lesson_detail_viewmodel.dart';
|
||||
|
||||
|
|
@ -20,6 +23,12 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
|||
await viewModel.navigateToLearnPractice(lesson.id ?? 0);
|
||||
}
|
||||
|
||||
@override
|
||||
void onViewModelReady(LearnLessonDetailViewModel viewModel) async {
|
||||
await viewModel.initializePlayer(lesson.videoUrl ?? '');
|
||||
super.onViewModelReady(viewModel);
|
||||
}
|
||||
|
||||
@override
|
||||
void onDispose(LearnLessonDetailViewModel viewModel) {
|
||||
viewModel.close();
|
||||
|
|
@ -120,19 +129,23 @@ class LearnLessonDetailView extends StackedView<LearnLessonDetailViewModel> {
|
|||
height: 200,
|
||||
color: kcBlack,
|
||||
width: double.maxFinite,
|
||||
child: _buildVideoPlayer(viewModel),
|
||||
child: _buildVideoPlayerState(viewModel),
|
||||
);
|
||||
|
||||
Widget _buildVideoPlayerState(LearnLessonDetailViewModel viewModel) =>
|
||||
viewModel.chewieController != null &&
|
||||
viewModel.videoPlayerController != null &&
|
||||
!viewModel.busy(StateObjects.loadLessonVideo)
|
||||
? _buildVideoPlayer(viewModel)
|
||||
: _buildEmptyVideoPlayer();
|
||||
|
||||
Widget _buildVideoPlayer(LearnLessonDetailViewModel viewModel) =>
|
||||
_buildVimeoPlayer(viewModel);
|
||||
_buildChewiePlayer(viewModel);
|
||||
|
||||
Widget _buildVimeoPlayer(LearnLessonDetailViewModel viewModel) =>
|
||||
VimeoVideoPlayer(
|
||||
isAutoPlay: true,
|
||||
onInAppWebViewCreated: (controller) =>
|
||||
viewModel.initializePlayer(controller),
|
||||
videoId: lesson.videoUrl?.split('/').last ?? '',
|
||||
);
|
||||
Widget _buildChewiePlayer(LearnLessonDetailViewModel viewModel) =>
|
||||
Chewie(controller: viewModel.chewieController!);
|
||||
|
||||
Widget _buildEmptyVideoPlayer() => const EmptyVideoPlayer();
|
||||
|
||||
Widget _buildDescriptionWrapper() => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
|
|
|
|||
|
|
@ -1,45 +1,67 @@
|
|||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
import 'package:stacked_services/stacked_services.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:yimaru_app/ui/common/enmus.dart';
|
||||
|
||||
import '../../../app/app.locator.dart';
|
||||
import '../../../app/app.router.dart';
|
||||
import '../../../services/status_checker_service.dart';
|
||||
import '../../../services/vimeo_service.dart';
|
||||
import '../../common/app_constants.dart';
|
||||
import '../../common/helper_functions.dart';
|
||||
import '../../common/ui_helpers.dart';
|
||||
|
||||
class LearnLessonDetailViewModel extends BaseViewModel {
|
||||
// Dependency injection
|
||||
final _statusChecker = locator<StatusCheckerService>();
|
||||
final _vimeoService = locator<VimeoService>();
|
||||
|
||||
final _navigationService = locator<NavigationService>();
|
||||
|
||||
// Video player config
|
||||
InAppWebViewController? _webViewController;
|
||||
ChewieController? _chewieController;
|
||||
|
||||
InAppWebViewController? get webViewController => _webViewController;
|
||||
ChewieController? get chewieController => _chewieController;
|
||||
|
||||
VideoPlayerController? _videoPlayerController;
|
||||
|
||||
VideoPlayerController? get videoPlayerController => _videoPlayerController;
|
||||
|
||||
// Video player
|
||||
void close() {
|
||||
webViewController?.dispose();
|
||||
_videoPlayerController?.dispose();
|
||||
_chewieController?.dispose();
|
||||
}
|
||||
|
||||
Future<void> pause() async {
|
||||
await webViewController?.pause();
|
||||
await _chewieController?.pause();
|
||||
}
|
||||
|
||||
void initializePlayer(InAppWebViewController controller) {
|
||||
_webViewController = controller;
|
||||
rebuildUi();
|
||||
Future<void> initializePlayer(String url) async =>
|
||||
await runBusyFuture(_initializePlayer(url),
|
||||
busyObject: StateObjects.loadLessonVideo);
|
||||
|
||||
Future<void> _initializePlayer(String url) async {
|
||||
final playableUrl = await _vimeoService.getVideoUrl(url);
|
||||
|
||||
if (playableUrl == null) {
|
||||
throw Exception("Unable to load video");
|
||||
}
|
||||
|
||||
void onLoadVideoStart() {
|
||||
setBusyForObject(StateObjects.loadLessonVideo, true);
|
||||
rebuildUi();
|
||||
}
|
||||
_videoPlayerController =
|
||||
VideoPlayerController.networkUrl(Uri.parse(playableUrl));
|
||||
|
||||
void onLoadVideoComplete() {
|
||||
setBusyForObject(StateObjects.loadLessonVideo, false);
|
||||
rebuildUi();
|
||||
await _videoPlayerController?.initialize();
|
||||
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoPlayerController!,
|
||||
autoPlay: true,
|
||||
looping: true,
|
||||
aspectRatio: 16 / 9,
|
||||
);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Navigation
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@ class LearnPracticeView extends StackedView<LearnPracticeViewModel> {
|
|||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, data) {
|
||||
if (!didPop) {
|
||||
Future.microtask(()async =>await _showSheet(context: context, viewModel: viewModel));
|
||||
Future.microtask(() async =>
|
||||
await _showSheet(context: context, viewModel: viewModel));
|
||||
}
|
||||
},
|
||||
|
||||
child: _buildScaffoldWrapper(viewModel));
|
||||
|
||||
Widget _buildSheet(LearnPracticeViewModel viewModel) =>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ class LearnPracticeViewModel extends ReactiveViewModel {
|
|||
return _position.inMilliseconds / _duration.inMilliseconds;
|
||||
}
|
||||
|
||||
|
||||
// Voice recorder
|
||||
WaveformRecorderController get _waveController =>
|
||||
_voiceRecorderService.waveController;
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ class InteractLearnPracticeScreen
|
|||
await viewModel.stopRecording();
|
||||
viewModel.pop();
|
||||
viewModel.pop();
|
||||
|
||||
}
|
||||
|
||||
void _reply(LearnPracticeViewModel viewModel) =>
|
||||
viewModel.replayVoicePrompt(question);
|
||||
|
||||
|
|
@ -360,7 +360,6 @@ class InteractLearnPracticeScreen
|
|||
onContinue: viewModel.pop,
|
||||
user: viewModel.user?.firstName ?? '',
|
||||
onCancel: () async => await _cancel(viewModel),
|
||||
|
||||
);
|
||||
|
||||
Widget _buildProgressIndicatorState(LearnPracticeViewModel viewModel) =>
|
||||
|
|
|
|||
|
|
@ -56,9 +56,11 @@ class LearnLoadingScreen extends StatelessWidget {
|
|||
|
||||
Widget _buildPageIndicator() => const PageLoadingIndicator();
|
||||
|
||||
Widget _buildRefreshButtonWrapper() => Align(
|
||||
alignment: Alignment.center,
|
||||
child:_buildRefreshButton());
|
||||
Widget _buildRefreshButtonWrapper() =>
|
||||
Align(alignment: Alignment.center, child: _buildRefreshButton());
|
||||
|
||||
Widget _buildRefreshButton()=> NoDataIndicator(title:'No practice available!' ,onTap: onTap,);
|
||||
Widget _buildRefreshButton() => NoDataIndicator(
|
||||
title: 'No practice available!',
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
|||
await viewModel.stopRecording();
|
||||
viewModel.pop();
|
||||
viewModel.pop();
|
||||
|
||||
}
|
||||
|
||||
Future<void> _showSheet(
|
||||
|
|
@ -83,7 +82,6 @@ class LearnPracticeIntroScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
|||
onContinue: viewModel.pop,
|
||||
user: viewModel.user?.firstName ?? '',
|
||||
onCancel: () async => await _cancel(viewModel),
|
||||
|
||||
);
|
||||
|
||||
Widget _buildBodyColumnWrapper(LearnPracticeViewModel viewModel) => Expanded(
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ class LearnPracticeQuestionsScreen
|
|||
required LearnQuestion question,
|
||||
}) =>
|
||||
[
|
||||
if(index ==1) _buildStartLearnPracticeScreen(index: index, question: question),
|
||||
if (index == 1)
|
||||
_buildStartLearnPracticeScreen(index: index, question: question),
|
||||
_buildInteractLearnPracticeScreen(index: index, question: question)
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ class LearnPracticeResultScreen
|
|||
await viewModel.stopRecording();
|
||||
viewModel.pop();
|
||||
viewModel.pop();
|
||||
|
||||
}
|
||||
|
||||
Future<void> _showSheet(
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ class StartLearnPracticeScreen extends ViewModelWidget<LearnPracticeViewModel> {
|
|||
await viewModel.stopRecording();
|
||||
viewModel.pop();
|
||||
viewModel.pop();
|
||||
|
||||
}
|
||||
|
||||
void _start(LearnPracticeViewModel viewModel) {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ class StartupViewModel extends ReactiveViewModel {
|
|||
}
|
||||
|
||||
// Navigation
|
||||
Future<void> replaceWithFailure() async => await _navigationService.replaceWithFailureView(
|
||||
Future<void> replaceWithFailure() async =>
|
||||
await _navigationService.replaceWithFailureView(
|
||||
label: 'Check you internet connection',
|
||||
onTap: () async => await _getProfileStatus());
|
||||
|
||||
|
|
|
|||
|
|
@ -46,5 +46,6 @@ class LearnPracticeResultsWrapper
|
|||
itemBuilder: (context, index) => _buildResult(viewModel.answers[index]),
|
||||
);
|
||||
|
||||
Widget _buildResult(Map<String, dynamic> answer) => LearnPracticeResultCard(answer: answer);
|
||||
Widget _buildResult(Map<String, dynamic> answer) =>
|
||||
LearnPracticeResultCard(answer: answer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ class NoDataIndicator extends StatelessWidget {
|
|||
_buildIconWrapper(),
|
||||
verticalSpaceMedium,
|
||||
_buildTitle(),
|
||||
|
||||
];
|
||||
|
||||
Widget _buildTitle() => Text(
|
||||
|
|
@ -40,6 +39,4 @@ class NoDataIndicator extends StatelessWidget {
|
|||
size: 75,
|
||||
color: kcPrimaryColor,
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import 'package:yimaru_app/services/course_service.dart';
|
|||
import 'package:yimaru_app/services/audio_player_service.dart';
|
||||
import 'package:yimaru_app/services/voice_recorder_service.dart';
|
||||
import 'package:yimaru_app/services/in_app_update_service.dart';
|
||||
import 'package:yimaru_app/services/vimeo_service.dart';
|
||||
// @stacked-import
|
||||
|
||||
import 'test_helpers.mocks.dart';
|
||||
|
|
@ -44,6 +45,8 @@ import 'test_helpers.mocks.dart';
|
|||
MockSpec<AudioPlayerService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<VoiceRecorderService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<InAppUpdateService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<VimeoService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
MockSpec<VimeoService>(onMissingStub: OnMissingStub.returnDefault),
|
||||
// @stacked-mock-spec
|
||||
],
|
||||
)
|
||||
|
|
@ -66,6 +69,8 @@ void registerServices() {
|
|||
getAndRegisterAudioPlayerService();
|
||||
getAndRegisterVoiceRecorderService();
|
||||
getAndRegisterInAppUpdateService();
|
||||
getAndRegisterVimeoService();
|
||||
getAndRegisterVimeoService();
|
||||
// @stacked-mock-register
|
||||
}
|
||||
|
||||
|
|
@ -227,6 +232,13 @@ MockInAppUpdateService getAndRegisterInAppUpdateService() {
|
|||
locator.registerSingleton<InAppUpdateService>(service);
|
||||
return service;
|
||||
}
|
||||
|
||||
MockVimeoService getAndRegisterVimeoService() {
|
||||
_removeRegistrationIfExists<VimeoService>();
|
||||
final service = MockVimeoService();
|
||||
locator.registerSingleton<VimeoService>(service);
|
||||
return service;
|
||||
}
|
||||
// @stacked-mock-create
|
||||
|
||||
void _removeRegistrationIfExists<T extends Object>() {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
11
test/services/vimeo_service_test.dart
Normal file
11
test/services/vimeo_service_test.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yimaru_app/app/app.locator.dart';
|
||||
|
||||
import '../helpers/test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('VimeoServiceTest -', () {
|
||||
setUp(() => registerServices());
|
||||
tearDown(() => locator.reset());
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user