chore: Maestro restructure, remove Marionette, add Makefile e2e commands

This commit is contained in:
2026-02-26 16:07:43 +05:30
parent c69949abf4
commit fd43494bd4
25 changed files with 289 additions and 314 deletions

View File

@@ -1,5 +1,3 @@
import 'dart:io' show Platform;
import 'package:client_authentication/client_authentication.dart'
as client_authentication;
import 'package:client_create_order/client_create_order.dart'
@@ -12,7 +10,6 @@ import 'package:design_system/design_system.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:marionette_flutter/marionette_flutter.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_modular/flutter_modular.dart';
@@ -23,23 +20,7 @@ import 'firebase_options.dart';
import 'src/widgets/session_listener.dart';
void main() async {
final bool isFlutterTest =
!kIsWeb ? Platform.environment.containsKey('FLUTTER_TEST') : false;
if (kDebugMode && !isFlutterTest) {
MarionetteBinding.ensureInitialized(
MarionetteConfiguration(
isInteractiveWidget: (Type type) =>
type == UiButton || type == UiTextField,
extractText: (Widget widget) {
if (widget is UiTextField) return widget.label;
if (widget is UiButton) return widget.text;
return null;
},
),
);
} else {
WidgetsFlutterBinding.ensureInitialized();
}
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: kIsWeb ? DefaultFirebaseOptions.currentPlatform : null,
);

View File

@@ -1,42 +1,33 @@
# Maestro Integration Tests — Client App
Login and signup flows for the KROW Client app.
See [docs/research/flutter-testing-tools.md](/docs/research/flutter-testing-tools.md) for the evaluation report.
**Full run instructions:** [docs/research/maestro-test-run-instructions.md](/docs/research/maestro-test-run-instructions.md)
Auth flows for the KROW Client app.
See [docs/research/flutter-testing-tools.md](/docs/research/flutter-testing-tools.md) and [maestro-test-run-instructions.md](/docs/research/maestro-test-run-instructions.md).
## Prerequisites
## Structure
- [Maestro CLI](https://maestro.dev/docs/getting-started/installation) installed
- Client app built and installed on device/emulator:
```bash
cd apps/mobile && flutter build apk
adb install build/app/outputs/flutter-apk/app-debug.apk
```
```
maestro/
auth/
sign_in.yaml
sign_up.yaml
```
## Credentials
## Credentials (env, never hardcoded)
| Flow | Credentials |
|------|-------------|
| **Client login** | legendary@krowd.com / Demo2026! |
| **Staff login** | 5557654321 / OTP 123456 |
| **Client signup** | Env vars: `MAESTRO_CLIENT_EMAIL`, `MAESTRO_CLIENT_PASSWORD`, `MAESTRO_CLIENT_COMPANY` |
| **Staff signup** | Env var: `MAESTRO_STAFF_SIGNUP_PHONE` (must be new Firebase test phone) |
| Flow | Env variables |
|------|---------------|
| sign_in | `TEST_CLIENT_EMAIL`, `TEST_CLIENT_PASSWORD` |
| sign_up | `TEST_CLIENT_EMAIL`, `TEST_CLIENT_PASSWORD`, `TEST_CLIENT_COMPANY` |
## Run
From the project root:
```bash
# Login
maestro test apps/mobile/apps/client/maestro/login.yaml
# Via Makefile (export vars first)
make test-e2e-client
# Signup
maestro test apps/mobile/apps/client/maestro/signup.yaml
# Direct
maestro test apps/mobile/apps/client/maestro/auth/sign_in.yaml \
-e TEST_CLIENT_EMAIL=... -e TEST_CLIENT_PASSWORD=...
maestro test apps/mobile/apps/client/maestro/auth/sign_up.yaml \
-e TEST_CLIENT_EMAIL=... -e TEST_CLIENT_PASSWORD=... -e TEST_CLIENT_COMPANY=...
```
## Flows
| File | Flow | Description |
|------------|-------------|--------------------------------------------|
| login.yaml | Client Login| Get Started → Sign In → Home |
| signup.yaml| Client Signup| Get Started → Create Account → Home |

View File

@@ -0,0 +1,22 @@
# Client App — Sign In flow
# Credentials via env: TEST_CLIENT_EMAIL, TEST_CLIENT_PASSWORD
# Run: maestro test apps/mobile/apps/client/maestro/auth/sign_in.yaml -e TEST_CLIENT_EMAIL=... -e TEST_CLIENT_PASSWORD=...
# Or: export MAESTRO_TEST_CLIENT_EMAIL / MAESTRO_TEST_CLIENT_PASSWORD (Maestro auto-reads MAESTRO_*)
appId: com.krowwithus.client
env:
EMAIL: ${TEST_CLIENT_EMAIL}
PASSWORD: ${TEST_CLIENT_PASSWORD}
---
- launchApp
- assertVisible: "Sign In"
- tapOn: "Sign In"
- assertVisible: "Email"
- tapOn:
id: sign_in_email
- inputText: ${EMAIL}
- tapOn:
id: sign_in_password
- inputText: ${PASSWORD}
- tapOn: "Sign In"
- assertVisible: "Home"

View File

@@ -0,0 +1,28 @@
# Client App — Sign Up flow
# Credentials via env: TEST_CLIENT_EMAIL, TEST_CLIENT_PASSWORD, TEST_CLIENT_COMPANY
# Run: maestro test apps/mobile/apps/client/maestro/auth/sign_up.yaml -e TEST_CLIENT_EMAIL=... -e TEST_CLIENT_PASSWORD=... -e TEST_CLIENT_COMPANY=...
appId: com.krowwithus.client
env:
EMAIL: ${TEST_CLIENT_EMAIL}
PASSWORD: ${TEST_CLIENT_PASSWORD}
COMPANY: ${TEST_CLIENT_COMPANY}
---
- launchApp
- assertVisible: "Create Account"
- tapOn: "Create Account"
- assertVisible: "Company"
- tapOn:
id: sign_up_company
- inputText: ${COMPANY}
- tapOn:
id: sign_up_email
- inputText: ${EMAIL}
- tapOn:
id: sign_up_password
- inputText: ${PASSWORD}
- tapOn:
id: sign_up_confirm_password
- inputText: ${PASSWORD}
- tapOn: "Create Account"
- assertVisible: "Home"

View File

@@ -1,18 +0,0 @@
# Client App - Login Flow
# Prerequisites: App built and installed (debug or release)
# Run: maestro test apps/mobile/apps/client/maestro/login.yaml
# Test credentials: legendary@krowd.com / Demo2026!
# Note: Auth uses Firebase/Data Connect
appId: com.krowwithus.client
---
- launchApp
- assertVisible: "Sign In"
- tapOn: "Sign In"
- assertVisible: "Email"
- tapOn: "Email"
- inputText: "legendary@krowd.com"
- tapOn: "Password"
- inputText: "Demo2026!"
- tapOn: "Sign In"
- assertVisible: "Home"

View File

@@ -1,23 +0,0 @@
# Client App - Sign Up Flow
# Prerequisites: App built and installed
# Run: maestro test apps/mobile/apps/client/maestro/signup.yaml
# Use NEW credentials for signup (creates new account)
# Env: MAESTRO_CLIENT_EMAIL, MAESTRO_CLIENT_PASSWORD, MAESTRO_CLIENT_COMPANY
appId: com.krowwithus.client
---
- launchApp
- assertVisible: "Create Account"
- tapOn: "Create Account"
- assertVisible: "Company"
- tapOn: "Company"
- inputText: "${MAESTRO_CLIENT_COMPANY}"
- tapOn: "Email"
- inputText: "${MAESTRO_CLIENT_EMAIL}"
- tapOn: "Password"
- inputText: "${MAESTRO_CLIENT_PASSWORD}"
- tapOn:
text: "Confirm Password"
- inputText: "${MAESTRO_CLIENT_PASSWORD}"
- tapOn: "Create Account"
- assertVisible: "Home"

View File

@@ -42,7 +42,6 @@ dependencies:
sdk: flutter
firebase_core: ^4.4.0
krow_data_connect: ^0.0.1
marionette_flutter: ^0.3.0
dev_dependencies:
flutter_test:

View File

@@ -1,9 +1,6 @@
import 'dart:io' show Platform;
import 'package:core_localization/core_localization.dart' as core_localization;
import 'package:design_system/design_system.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
@@ -11,7 +8,6 @@ import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krowwithus_staff/firebase_options.dart';
import 'package:marionette_flutter/marionette_flutter.dart';
import 'package:staff_authentication/staff_authentication.dart'
as staff_authentication;
import 'package:staff_main/staff_main.dart' as staff_main;
@@ -19,24 +15,7 @@ import 'package:staff_main/staff_main.dart' as staff_main;
import 'src/widgets/session_listener.dart';
void main() async {
final bool isFlutterTest = !kIsWeb
? Platform.environment.containsKey('FLUTTER_TEST')
: false;
if (kDebugMode && !isFlutterTest) {
MarionetteBinding.ensureInitialized(
MarionetteConfiguration(
isInteractiveWidget: (Type type) =>
type == UiButton || type == UiTextField,
extractText: (Widget widget) {
if (widget is UiTextField) return widget.label;
if (widget is UiButton) return widget.text;
return null;
},
),
);
} else {
WidgetsFlutterBinding.ensureInitialized();
}
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// Register global BLoC observer for centralized error logging

View File

@@ -1,41 +1,38 @@
# Maestro Integration Tests — Staff App
Login and signup flows for the KROW Staff app.
See [docs/research/flutter-testing-tools.md](/docs/research/flutter-testing-tools.md) for the evaluation report.
**Full run instructions:** [docs/research/maestro-test-run-instructions.md](/docs/research/maestro-test-run-instructions.md)
Auth flows for the KROW Staff app.
See [docs/research/flutter-testing-tools.md](/docs/research/flutter-testing-tools.md) and [maestro-test-run-instructions.md](/docs/research/maestro-test-run-instructions.md).
## Structure
```
maestro/
auth/
sign_in.yaml
sign_up.yaml
```
## Prerequisites
- [Maestro CLI](https://maestro.dev/docs/getting-started/installation) installed
- Staff app built and installed
- **Firebase test phone** in Firebase Console (Auth > Sign-in method > Phone):
- Login: +1 555-765-4321 / OTP 123456
- Signup: add a different test number for new accounts
- Firebase test phone in Auth > Phone (e.g. +1 555-765-4321 / OTP 123456)
- For sign_up: use a different test number (not yet registered)
## Credentials
## Credentials (env, never hardcoded)
| Flow | Credentials |
|------|-------------|
| **Client login** | legendary@krowd.com / Demo2026! |
| **Staff login** | 5557654321 / OTP 123456 |
| **Client signup** | Env vars: `MAESTRO_CLIENT_EMAIL`, `MAESTRO_CLIENT_PASSWORD`, `MAESTRO_CLIENT_COMPANY` |
| **Staff signup** | Env var: `MAESTRO_STAFF_SIGNUP_PHONE` (must be new Firebase test phone) |
| Flow | Env variables |
|------|---------------|
| sign_in | `TEST_STAFF_PHONE`, `TEST_STAFF_OTP` |
| sign_up | `TEST_STAFF_SIGNUP_PHONE`, `TEST_STAFF_OTP` |
## Run
From the project root:
```bash
# Login
maestro test apps/mobile/apps/staff/maestro/login.yaml
# Via Makefile (export vars first)
make test-e2e-staff
# Signup
maestro test apps/mobile/apps/staff/maestro/signup.yaml
# Direct
maestro test apps/mobile/apps/staff/maestro/auth/sign_in.yaml \
-e TEST_STAFF_PHONE=5557654321 -e TEST_STAFF_OTP=123456
maestro test apps/mobile/apps/staff/maestro/auth/sign_up.yaml \
-e TEST_STAFF_SIGNUP_PHONE=... -e TEST_STAFF_OTP=123456
```
## Flows
| File | Flow | Description |
|------------|------------|-------------------------------------|
| login.yaml | Staff Login| Get Started → Log In → Phone → OTP → Home |
| signup.yaml| Staff Signup| Get Started → Sign Up → Phone → OTP → Profile Setup |

View File

@@ -0,0 +1,24 @@
# Staff App — Sign In flow (Phone + OTP)
# Credentials via env: TEST_STAFF_PHONE, TEST_STAFF_OTP
# Firebase: add test phone in Auth > Phone (e.g. +1 555-765-4321 / OTP 123456)
# Run: maestro test apps/mobile/apps/staff/maestro/auth/sign_in.yaml -e TEST_STAFF_PHONE=5557654321 -e TEST_STAFF_OTP=123456
appId: com.krowwithus.staff
env:
PHONE: ${TEST_STAFF_PHONE}
OTP: ${TEST_STAFF_OTP}
---
- launchApp
- assertVisible: "Log In"
- tapOn: "Log In"
- assertVisible: "Send Code"
- tapOn:
id: staff_phone_input
- inputText: ${PHONE}
- tapOn: "Send Code"
# OTP screen: Continue button visible until we finish typing
- assertVisible: "Continue"
- tapOn:
id: staff_otp_input
- inputText: ${OTP}
# OTP auto-submits when 6th digit is entered; app navigates to staff main

View File

@@ -0,0 +1,23 @@
# Staff App — Sign Up flow (Phone + OTP)
# Credentials via env: TEST_STAFF_SIGNUP_PHONE, TEST_STAFF_OTP
# Use a NEW Firebase test phone (not yet registered)
# Run: maestro test apps/mobile/apps/staff/maestro/auth/sign_up.yaml -e TEST_STAFF_SIGNUP_PHONE=... -e TEST_STAFF_OTP=123456
appId: com.krowwithus.staff
env:
PHONE: ${TEST_STAFF_SIGNUP_PHONE}
OTP: ${TEST_STAFF_OTP}
---
- launchApp
- assertVisible: "Sign Up"
- tapOn: "Sign Up"
- assertVisible: "Send Code"
- tapOn:
id: staff_phone_input
- inputText: ${PHONE}
- tapOn: "Send Code"
# OTP auto-submits when 6th digit entered
- assertVisible: "Continue"
- tapOn:
id: staff_otp_input
- inputText: ${OTP}

View File

@@ -1,18 +0,0 @@
# Staff App - Login Flow (Phone + OTP)
# Prerequisites: App built and installed; Firebase test phone configured
# Firebase test phone: +1 555-765-4321 / OTP 123456
# Run: maestro test apps/mobile/apps/staff/maestro/login.yaml
appId: com.krowwithus.staff
---
- launchApp
- assertVisible: "Log In"
- tapOn: "Log In"
- assertVisible: "Send Code"
- inputText: "5557654321"
- tapOn: "Send Code"
# Wait for OTP screen
- assertVisible: "Continue"
- inputText: "123456"
- tapOn: "Continue"
# On success: staff main. Adjust final assertion to match staff home screen.

View File

@@ -1,18 +0,0 @@
# Staff App - Sign Up Flow (Phone + OTP)
# Prerequisites: App built and installed; Firebase test phone for NEW number
# Use a NEW phone number for signup (creates new account)
# Firebase: add test phone in Auth > Phone; e.g. +1 555-555-0000 / 123456
# Run: maestro test apps/mobile/apps/staff/maestro/signup.yaml
appId: com.krowwithus.staff
---
- launchApp
- assertVisible: "Sign Up"
- tapOn: "Sign Up"
- assertVisible: "Send Code"
- inputText: "${MAESTRO_STAFF_SIGNUP_PHONE}"
- tapOn: "Send Code"
- assertVisible: "Continue"
- inputText: "123456"
- tapOn: "Continue"
# On success: Profile Setup. Adjust assertion to match destination.

View File

@@ -30,7 +30,6 @@ dependencies:
path: ../../packages/core
krow_data_connect:
path: ../../packages/data_connect
marionette_flutter: ^0.3.0
cupertino_icons: ^1.0.8
flutter_modular: ^6.3.0
firebase_core: ^4.4.0

View File

@@ -11,6 +11,7 @@ class UiTextField extends StatelessWidget {
const UiTextField({
super.key,
this.semanticsIdentifier,
this.label,
this.hintText,
this.onChanged,
@@ -29,6 +30,8 @@ class UiTextField extends StatelessWidget {
this.onTap,
this.validator,
});
/// Optional semantics identifier for E2E testing (e.g. Maestro).
final String? semanticsIdentifier;
/// The label text to display above the text field.
final String? label;
@@ -90,7 +93,9 @@ class UiTextField extends StatelessWidget {
Text(label!, style: UiTypography.body4m.textSecondary),
const SizedBox(height: UiConstants.space1),
],
TextFormField(
Builder(
builder: (BuildContext context) {
final Widget field = TextFormField(
controller: controller,
onChanged: onChanged,
keyboardType: keyboardType,
@@ -113,6 +118,15 @@ class UiTextField extends StatelessWidget {
? Icon(suffixIcon, size: 20, color: UiColors.iconSecondary)
: suffix,
),
);
if (semanticsIdentifier != null) {
return Semantics(
identifier: semanticsIdentifier!,
child: field,
);
}
return field;
},
),
],
);

View File

@@ -52,6 +52,7 @@ class _ClientSignInFormState extends State<ClientSignInForm> {
children: <Widget>[
// Email Field
UiTextField(
semanticsIdentifier: 'sign_in_email',
label: i18n.email_label,
hintText: i18n.email_hint,
controller: _emailController,
@@ -61,6 +62,7 @@ class _ClientSignInFormState extends State<ClientSignInForm> {
// Password Field
UiTextField(
semanticsIdentifier: 'sign_in_password',
label: i18n.password_label,
hintText: i18n.password_hint,
controller: _passwordController,

View File

@@ -70,6 +70,7 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
children: <Widget>[
// Company Name Field
UiTextField(
semanticsIdentifier: 'sign_up_company',
label: i18n.company_label,
hintText: i18n.company_hint,
controller: _companyController,
@@ -79,6 +80,7 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
// Email Field
UiTextField(
semanticsIdentifier: 'sign_up_email',
label: i18n.email_label,
hintText: i18n.email_hint,
controller: _emailController,
@@ -89,6 +91,7 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
// Password Field
UiTextField(
semanticsIdentifier: 'sign_up_password',
label: i18n.password_label,
hintText: i18n.password_hint,
controller: _passwordController,
@@ -108,6 +111,7 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
// Confirm Password Field
UiTextField(
semanticsIdentifier: 'sign_up_confirm_password',
label: i18n.confirm_password_label,
hintText: i18n.confirm_password_hint,
controller: _confirmPasswordController,

View File

@@ -1,4 +1,4 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs, implementation_imports
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -75,10 +75,7 @@ class _OtpInputFieldState extends State<OtpInputField> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(6, (int index) {
return SizedBox(
width: 45,
height: 56,
child: TextField(
final TextField field = TextField(
controller: _controllers[index],
focusNode: _focusNodes[index],
keyboardType: TextInputType.number,
@@ -112,7 +109,16 @@ class _OtpInputFieldState extends State<OtpInputField> {
),
onChanged: (String value) =>
_onChanged(context: context, index: index, value: value),
),
);
return SizedBox(
width: 45,
height: 56,
child: index == 0
? Semantics(
identifier: 'staff_otp_input',
child: field,
)
: field,
);
}),
),

View File

@@ -82,17 +82,20 @@ class _PhoneInputFormFieldState extends State<PhoneInputFormField> {
),
const SizedBox(width: UiConstants.space2),
Expanded(
child: TextField(
controller: _controller,
keyboardType: TextInputType.phone,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(11),
],
decoration: InputDecoration(
hintText: t.staff_authentication.phone_input.hint,
child: Semantics(
identifier: 'staff_phone_input',
child: TextField(
controller: _controller,
keyboardType: TextInputType.phone,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(11),
],
decoration: InputDecoration(
hintText: t.staff_authentication.phone_input.hint,
),
onChanged: widget.onChanged,
),
onChanged: widget.onChanged,
),
),
],

View File

@@ -925,14 +925,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.257.0"
marionette_flutter:
dependency: transitive
description:
name: marionette_flutter
sha256: "0077073f62a8031879a91be41aa91629f741a7f1348b18feacd53443dae3819f"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
matcher:
dependency: transitive
description: