Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
bde6211
feat(test_runner): support live Firebase and HTTP under flutter test
sharjeelyunus Jul 9, 2026
8d6c6ba
refactor(test_runner): clean up HTTP overrides and improve code reada…
sharjeelyunus Jul 9, 2026
927f9bb
refactor(mock_api_provider): streamline mock API response handling
sharjeelyunus Jul 9, 2026
ffb9905
refactor(api_provider): replace mock API provider with test API provi…
sharjeelyunus Jul 9, 2026
37058ed
refactor(api_tests): remove deprecated API mock steps and streamline …
sharjeelyunus Jul 9, 2026
cb4d4ad
refactor(test_runner): update logStorage handling and schema for impr…
sharjeelyunus Jul 9, 2026
049c928
refactor(test_runner): remove state-related steps and streamline test…
sharjeelyunus Jul 9, 2026
47fd487
refactor(test_runner): enhance wait step and schema definitions for c…
sharjeelyunus Jul 10, 2026
b65837d
refactor(test_runner): enhance logging and screenshot functionality
sharjeelyunus Jul 10, 2026
9e899ee
feat(test_runner): add font loading functionality to ensure app fonts…
sharjeelyunus Jul 10, 2026
9398c6b
feat(test_runner): enhance screenshot functionality with device frame…
sharjeelyunus Jul 10, 2026
7939bde
refactor(test_runner): remove deviceFrame property from schema and re…
sharjeelyunus Jul 10, 2026
6860cb8
refactor(test_runner): improve logging structure and output formats
sharjeelyunus Jul 10, 2026
56f3a13
feat(test_runner): introduce screenshot options in ensemble tests schema
sharjeelyunus Jul 10, 2026
86c0fad
feat(test_runner): add performance logging capabilities to ensemble t…
sharjeelyunus Jul 13, 2026
9021039
feat(test_runner): introduce suite-wide configuration for ensemble tests
sharjeelyunus Jul 13, 2026
b564ceb
feat(test_runner): enhance performance logging with detailed metrics …
sharjeelyunus Jul 13, 2026
b7dc4eb
feat(view_util): add support for testId in widget model construction
sharjeelyunus Jul 13, 2026
6adf5bf
feat(test_runner): add support for CLI inputs in ensemble tests
sharjeelyunus Jul 13, 2026
f7bade8
feat(test_runner): enhance mock handling and scenario support in ense…
sharjeelyunus Jul 14, 2026
416d4ce
feat(test_runner): add delayMs support for mocked APIs in ensemble tests
sharjeelyunus Jul 14, 2026
578e562
feat(test_runner): add recording and screenshot contact sheet features
sharjeelyunus Jul 14, 2026
58d2762
feat(test_runner): enhance screenshot functionality with highlighting…
sharjeelyunus Jul 14, 2026
65318a6
refactor(test_runner): update screenshot handling and layout adjustments
sharjeelyunus Jul 14, 2026
1d44c13
refactor(test_runner): remove recording feature and update schema
sharjeelyunus Jul 14, 2026
09d8be5
feat(test_runner): enhance font handling and package info resolution
sharjeelyunus Jul 14, 2026
e292dd9
feat(test_runner): enhance live output filtering and report extraction
sharjeelyunus Jul 14, 2026
ceef94d
Merge branch 'main' into feat/ensemble-test-runner-live-firebase
sharjeelyunus Jul 15, 2026
bc3d3c1
feat(test_runner): implement toggle functionality for UI elements
sharjeelyunus Jul 15, 2026
f256e09
feat(test_runner): enhance HTTP request and command execution capabil…
sharjeelyunus Jul 17, 2026
edc46eb
refactor(test_runner): remove runCommand action and enhance mock hand…
sharjeelyunus Jul 18, 2026
907a624
feat(auth): implement retry logic for custom token sign-in and enhanc…
sharjeelyunus Jul 18, 2026
e8a93e7
feat(test_runner): enhance test status handling and screenshot manage…
sharjeelyunus Jul 18, 2026
7b4413a
refactor(tests): replace prerequisite with session in test configurat…
sharjeelyunus Jul 18, 2026
78b1a9c
feat(test_runner): improve error handling and JSON report emission
sharjeelyunus Jul 19, 2026
d1ecc8c
feat(test_runner): implement screenshot management improvements and e…
sharjeelyunus Jul 19, 2026
32bfb8f
feat(test_runner): introduce step mocks for dynamic API response hand…
sharjeelyunus Jul 19, 2026
a8b5c5d
feat(test_runner): enhance test configuration with suite-wide mocks a…
sharjeelyunus Jul 19, 2026
8b50122
feat(test_runner): introduce device matrix support for enhanced test …
sharjeelyunus Jul 19, 2026
aae4a75
feat(test_runner): update screenshot contact sheet generation and imp…
sharjeelyunus Jul 20, 2026
77b9de2
fix(test_runner): adjust header height in screenshot contact sheet test
sharjeelyunus Jul 20, 2026
f3bc9e0
feat(test_runner): add theme support to device matrix and enhance doc…
sharjeelyunus Jul 20, 2026
31bb586
fix(test_theme): prevent redundant theme application when the current…
sharjeelyunus Jul 20, 2026
4dc781f
feat(test_runner): add timeoutMs parameter for text visibility assert…
sharjeelyunus Jul 20, 2026
c25cc50
feat(test_runner): add timeoutMs parameter for tap actions in test st…
sharjeelyunus Jul 20, 2026
7c1f617
feat(mock_composition): introduce $extends and $merge for mock file c…
sharjeelyunus Jul 20, 2026
fca63bd
add post cleanup job for workers
sharjeelyunus Jul 20, 2026
5c7499a
feat(test_runner): add --device option for selective test execution o…
sharjeelyunus Jul 21, 2026
61ae040
feat(ensemble_test_execution): enhance asset handling and selection l…
sharjeelyunus Jul 21, 2026
db59fdd
feat(wifi): introduce Wi-Fi test double configuration and management
sharjeelyunus Jul 21, 2026
1e411f7
feat(reporting): enhance test reporting with per-test artifacts and H…
sharjeelyunus Jul 21, 2026
ad293e8
feat(reporting): enhance HTML report generation and test discovery
sharjeelyunus Jul 21, 2026
2212c0d
fix(reporting): improve status display in HTML test reports
sharjeelyunus Jul 21, 2026
d85cad9
feat(reporting): add step duration tracking to test reports
sharjeelyunus Jul 21, 2026
d62561b
feat(assertions): add visually actionable element detection for scree…
sharjeelyunus Jul 22, 2026
b64fdd2
feat(lottie): enhance Lottie controller and screenshot handling
sharjeelyunus Jul 22, 2026
2b26812
feat(ensemble_app): add theme and locale change event subscriptions
sharjeelyunus Jul 22, 2026
5898997
fix(reporting): improve HTML test report interaction logic
sharjeelyunus Jul 22, 2026
42565bb
refactor(reporting): update HTML test reporter styles and remove unus…
sharjeelyunus Jul 22, 2026
b499480
feat(navigation): enhance navigation handling and screenshot capturin…
sharjeelyunus Jul 22, 2026
88fdadd
feat(api): add type field to APICallRecord and enhance HTML test repo…
sharjeelyunus Jul 22, 2026
800edc9
feat(reporting): enhance screenshot handling and logging in test runner
sharjeelyunus Jul 22, 2026
ac85cc4
feat(reporting): overhaul HTML test reporting structure and artifact …
sharjeelyunus Jul 22, 2026
0d85c1e
feat(reporting): improve test report structure and flatten step data
sharjeelyunus Jul 22, 2026
9f2febe
feat(reporting): switch to gzip-compressed results for test reports
sharjeelyunus Jul 22, 2026
452daf0
feat(api): enhance APICallRecord and logging for improved request/res…
sharjeelyunus Jul 22, 2026
b51d9af
feat(reporting): enhance HTML test report styles and scrolling behavior
sharjeelyunus Jul 22, 2026
52a5203
feat(reporting): enhance test report artifacts management and sorting…
sharjeelyunus Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ starter/system.bak

# remove molos .iml files
**/melos_*.iml

generated_failed_sheet.png
generated_passed_sheet.png
generated_multidevice_sheet.png
48 changes: 46 additions & 2 deletions modules/auth/lib/signin/auth_manager.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart';

Expand Down Expand Up @@ -137,8 +138,10 @@ class AuthManager with UserAuthentication {
customFirebaseApp ??= await _initializeFirebaseSignIn();
final _auth = FirebaseAuth.instanceFor(app: customFirebaseApp!);

UserCredential userCredential =
await _auth.signInWithCustomToken(evaluatedJwtToken);
UserCredential userCredential = await _signInWithCustomTokenWithRetry(
_auth,
evaluatedJwtToken,
);
User? user = userCredential.user;
if (user == null) {
throw StateError('Firebase sign-in failed. User is null.');
Expand All @@ -153,6 +156,47 @@ class AuthManager with UserAuthentication {
}
}

Future<UserCredential> _signInWithCustomTokenWithRetry(
FirebaseAuth auth,
String token, {
int maxAttempts = 4,
}) async {
Object? lastError;
for (var attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await auth.signInWithCustomToken(token);
} catch (error) {
lastError = error;
if (!_isRetryableFirebaseAuthError(error) || attempt >= maxAttempts) {
rethrow;
}
if (kDebugMode) {
debugPrint(
'Retrying Firebase custom-token sign-in after transient error '
'($attempt/$maxAttempts): $error',
);
}
await Future<void>.delayed(Duration(milliseconds: 300 * attempt));
}
}
throw lastError ?? StateError('Firebase custom-token sign-in failed.');
}

bool _isRetryableFirebaseAuthError(Object error) {
if (error is FirebaseAuthException) {
return error.code == 'network-request-failed';
}
if (error is SocketException || error is TimeoutException) {
return true;
}
final message = error.toString().toLowerCase();
return message.contains('network-request-failed') ||
message.contains('connection reset') ||
message.contains('connection closed') ||
message.contains('timed out') ||
message.contains('socketexception');
}

Future<void> updateCurrentFirebaseUser(BuildContext context, User newUser) async {
await StorageManager()
.writeToSystemStorage(UserAuthentication._idKey, newUser.uid);
Expand Down
7 changes: 4 additions & 3 deletions modules/auth/lib/signin/sign_in_anonymous.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ class SignInAnonymousImpl implements SignInAnonymous {
if (idToken != null) {
if (action.onAuthenticated != null) {
AuthenticatedUser? currentUser = AuthManager().getCurrentUser();
ScreenController().executeAction(context, action.onAuthenticated!,
event: EnsembleEvent(null, data: {'user': currentUser, 'idToken': idToken}));
await ScreenController().executeAction(context, action.onAuthenticated!,
event: EnsembleEvent(null,
data: {'user': currentUser, 'idToken': idToken}));
}
} else {
if (action.onError != null) {
ScreenController().executeAction(context, action.onError!);
await ScreenController().executeAction(context, action.onError!);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions modules/auth/lib/signin/sign_in_with_custom_token.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ class SignInWithCustomTokenImpl implements SignInWithCustomToken {
if (idToken != null) {
if (action.onAuthenticated != null) {
AuthenticatedUser? currentUser = AuthManager().getCurrentUser();
ScreenController().executeAction(context, action.onAuthenticated!,
await ScreenController().executeAction(context, action.onAuthenticated!,
event: EnsembleEvent(null,
data: {'user': currentUser, 'idToken': idToken}));
}
}
} catch (e) {
if (action.onError != null) {
ScreenController().executeAction(context, action.onError!,
await ScreenController().executeAction(context, action.onError!,
event: EnsembleEvent(null, error: {'error': e.toString()}));
}
}
Expand Down
18 changes: 11 additions & 7 deletions modules/ensemble/lib/action/dialog_actions.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:ensemble/framework/action.dart';
import 'package:ensemble/framework/apiproviders/api_provider.dart';
import 'package:ensemble/framework/error_handling.dart';
import 'package:ensemble/framework/event.dart';
import 'package:ensemble/framework/scope.dart';
Expand Down Expand Up @@ -62,6 +63,7 @@ class ShowDialogAction extends EnsembleAction {

bool useDefaultStyle = dialogStyles['style'] != 'none';
BuildContext? dialogContext;
final apiProviders = APIProviders.of(context);

showGeneralDialog(
useRootNavigator: false,
Expand Down Expand Up @@ -113,12 +115,14 @@ class ShowDialogAction extends EnsembleAction {
useDefaultStyle ? const EdgeInsets.all(20) : null,
padding:
useDefaultStyle ? const EdgeInsets.all(20) : null,
child: DataScopeWidget(
scopeManager: scopeManager.createChildScope(),
child: SingleChildScrollView(
child: scopeManager
.buildWidgetFromDefinition(body),
))))));
child: APIProviders(
providers: apiProviders.providers,
child: DataScopeWidget(
scopeManager: scopeManager.createChildScope(),
child: SingleChildScrollView(
child: scopeManager
.buildWidgetFromDefinition(body),
)))))));
}).then((payload) {
// remove the dialog context since we are closing them
scopeManager.openedDialogs.remove(dialogContext);
Expand Down Expand Up @@ -178,4 +182,4 @@ class CloseAllDialogsAction extends EnsembleAction {
scopeManager.openedDialogs.clear();
return Future.value(null);
}
}
}
30 changes: 27 additions & 3 deletions modules/ensemble/lib/ensemble_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ class EnsembleAppState extends State<EnsembleApp> with WidgetsBindingObserver {
bool _hasInternet = true;
late final StreamSubscription<List<ConnectivityResult>>
_connectivitySubscription;
StreamSubscription<ThemeChangeEvent>? _themeChangeSubscription;
StreamSubscription<SetLocaleEvent>? _setLocaleSubscription;
StreamSubscription<ClearLocaleEvent>? _clearLocaleSubscription;
SemanticsHandle? _testSemanticsHandle;

@override
Expand All @@ -178,18 +181,30 @@ class EnsembleAppState extends State<EnsembleApp> with WidgetsBindingObserver {
Workmanager().initialize(callbackDispatcher, isInDebugMode: false);
initDeepLink(AppLifecycleState.resumed);
}
AppEventBus().eventBus.on<ThemeChangeEvent>().listen((event) {
_themeChangeSubscription =
AppEventBus().eventBus.on<ThemeChangeEvent>().listen((event) {
if (!mounted) {
return;
}
setState(() {});
});

// selecting a Locale at run time
AppEventBus().eventBus.on<SetLocaleEvent>().listen((event) async {
_setLocaleSubscription =
AppEventBus().eventBus.on<SetLocaleEvent>().listen((event) {
if (!mounted) {
return;
}
if (runtimeLocale != event.locale) {
runtimeLocale = event.locale;
rebuildApp();
}
});
AppEventBus().eventBus.on<ClearLocaleEvent>().listen((event) {
_clearLocaleSubscription =
AppEventBus().eventBus.on<ClearLocaleEvent>().listen((event) {
if (!mounted) {
return;
}
if (runtimeLocale != null) {
runtimeLocale = null;
rebuildApp();
Expand All @@ -203,6 +218,9 @@ class EnsembleAppState extends State<EnsembleApp> with WidgetsBindingObserver {
/// Check the device’s connectivity and update the state.
Future<void> _updateConnectivity() async {
final result = await Connectivity().checkConnectivity();
if (!mounted) {
return;
}
final hasInternetNow = result.any((r) => r != ConnectivityResult.none);

// If connectivity has been restored, reinitialize the app
Expand Down Expand Up @@ -246,6 +264,9 @@ class EnsembleAppState extends State<EnsembleApp> with WidgetsBindingObserver {
@override
void dispose() {
_connectivitySubscription.cancel();
_themeChangeSubscription?.cancel();
_setLocaleSubscription?.cancel();
_clearLocaleSubscription?.cancel();
_testSemanticsHandle?.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
Expand Down Expand Up @@ -289,6 +310,9 @@ class EnsembleAppState extends State<EnsembleApp> with WidgetsBindingObserver {
* at runtime where a complete rebuild is needed.
*/
void rebuildApp() {
if (!mounted) {
return;
}
setState(() {
appKey = UniqueKey();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class APIProviders extends InheritedWidget {
if (provider == null) {
return httpProvider;
} else if (provider == 'firebaseFunction') {
return FirebaseFunctionsAPIProvider();
return providers[provider] ?? FirebaseFunctionsAPIProvider();
} else if (provider == 'sse') {
return providers[provider] ?? SSEAPIProvider();
} else {
Expand Down
3 changes: 3 additions & 0 deletions modules/ensemble/lib/framework/widget/view_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ class ViewUtil {
if (callerPayload?['id'] != null) {
props["id"] = callerPayload?['id'];
}
if (callerPayload?['testId'] != null) {
props["testId"] = callerPayload?['testId'];
}

Map<String, dynamic> inputPayload = {};
if (callerPayload?['inputs'] is Map) {
Expand Down
6 changes: 5 additions & 1 deletion modules/ensemble/lib/layout/ensemble_page_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,12 @@ extension PageTransitionTypeX on PageTransitionType {
/// Page route builder that presents a screen without transition animation.
class EnsemblePageRouteNoTransitionBuilder extends PageRouteBuilder {
/// Creates a [EnsemblePageRouteNoTransitionBuilder] object.
EnsemblePageRouteNoTransitionBuilder({required Widget screenWidget})
EnsemblePageRouteNoTransitionBuilder({
required Widget screenWidget,
RouteSettings? settings,
})
: super(
settings: settings,
pageBuilder: (context, animation, secondaryAnimation) => screenWidget,
);
}
Expand Down
29 changes: 23 additions & 6 deletions modules/ensemble/lib/screen_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ class ScreenController {
(isRepeat ? repeatInterval! : 0);

// we always execute at least once, delayed by startAfter and fallback to repeatInterval (or immediate if startAfter is 0)
Timer(Duration(seconds: delay), () {
final timer = Timer(Duration(seconds: delay), () {
// execute the action
executeActionWithScope(context, scopeManager, action.onTimer);

Expand All @@ -298,7 +298,7 @@ class ScreenController {
int? repeatCount = maxTimes != null ? maxTimes - 1 : null;
if (repeatCount != 0) {
int counter = 0;
final timer =
final periodicTimer =
Timer.periodic(Duration(seconds: repeatInterval), (timer) {
// execute the action
executeActionWithScope(context, scopeManager, action.onTimer);
Expand All @@ -317,10 +317,11 @@ class ScreenController {

// save our timer to our PageData since user may want to cancel at anytime
// and also when we navigate away from the page
scopeManager.addTimer(action, timer);
scopeManager.addTimer(action, periodicTimer);
}
}
});
scopeManager.addTimer(action, timer);
} else if (action is StopTimerAction) {
try {
scopeManager.removeTimer(action.id);
Expand Down Expand Up @@ -360,9 +361,9 @@ class ScreenController {
apiMap: apiMap,
scopeManager: scopeManager);
} else if (action is SignInAnonymousAction) {
GetIt.I<SignInAnonymous>().signInAnonymously(context, action: action);
await GetIt.I<SignInAnonymous>().signInAnonymously(context, action: action);
} else if (action is SignInWithCustomTokenAction) {
GetIt.I<SignInWithCustomToken>().signInWithCustomToken(context, action: action);
await GetIt.I<SignInWithCustomToken>().signInWithCustomToken(context, action: action);
} else if (action is WalletConnectAction) {
// TODO store session: WalletConnectSession? session = await sessionStorage.getSession();

Expand Down Expand Up @@ -584,12 +585,24 @@ class ScreenController {
defaultTransitionOptions[_pageType]?['duration'],
fallback: 250);

final routeSettings = RouteSettings(
name: screenName ?? screenId,
arguments: ScreenPayload(
screenId: screenId,
screenName: screenName,
pageType: pageType,
arguments: pageArgs,
isExternal: isExternal,
),
);

PageRouteBuilder route = getScreenBuilder(
screenWidget,
pageType: pageType,
transitionType: transitionType,
alignment: alignment,
duration: duration,
settings: routeSettings,
);
// push the new route and remove all existing screens. This is suitable for logging out.
if (routeOption == RouteOption.clearAllScreens) {
Expand Down Expand Up @@ -651,12 +664,14 @@ class ScreenController {
PageTransitionType? transitionType,
Alignment? alignment,
int? duration,
RouteSettings? settings,
}) {
const enableTransition =
bool.fromEnvironment('transitions', defaultValue: true);

if (!enableTransition) {
return EnsemblePageRouteNoTransitionBuilder(screenWidget: screenWidget);
return EnsemblePageRouteNoTransitionBuilder(
screenWidget: screenWidget, settings: settings);
}

if (pageType == PageType.modal) {
Expand All @@ -669,13 +684,15 @@ class ScreenController {
duration: Duration(milliseconds: duration ?? 250),
barrierDismissible: true,
barrierColor: Colors.black54,
settings: settings,
);
} else {
return EnsemblePageRouteBuilder(
child: screenWidget,
transitionType: transitionType ?? PageTransitionType.fade,
alignment: alignment ?? Alignment.center,
duration: Duration(milliseconds: duration ?? 250),
settings: settings,
);
}
}
Expand Down
16 changes: 12 additions & 4 deletions modules/ensemble/lib/util/ensemble_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@ class EnsembleUtils {

// Fallback to RouteObserver method
final route = Ensemble().getCurrentRoute();
if (route is PopupRoute && route.isCurrent && route.navigator != null) {
return route.navigator!.maybePop(payload);
final navigator = route?.navigator;
if (route is PopupRoute &&
route.isCurrent &&
navigator != null &&
navigator.canPop()) {
navigator.pop(payload);
return Future.value(true);
}
return Future.value(false);
}
Expand Down Expand Up @@ -54,10 +59,13 @@ class EnsembleUtils {

// Fallback to RouteObserver method
final route = Ensemble().getCurrentRoute();
final navigator = route?.navigator;
if (route is ModalBottomSheetRoute &&
route.isCurrent &&
route.navigator != null) {
return route.navigator!.maybePop(payload);
navigator != null &&
navigator.canPop()) {
navigator.pop(payload);
return Future.value(true);
}
return Future.value(false);
}
Expand Down
Loading
Loading