Merge pull request #542 from Oloodi/feature/session-persistence-new

chore: Maestro E2E restructure, remove Marionette, add Makefile e2e targets
This commit is contained in:
Achintha Isuru
2026-02-26 10:20:33 -05:00
committed by GitHub
25 changed files with 289 additions and 314 deletions

View File

@@ -56,6 +56,9 @@ help:
@echo " make mobile-test Run flutter test for client+staff" @echo " make mobile-test Run flutter test for client+staff"
@echo " make mobile-hot-reload Hot reload running Flutter app" @echo " make mobile-hot-reload Hot reload running Flutter app"
@echo " make mobile-hot-restart Hot restart running Flutter app" @echo " make mobile-hot-restart Hot restart running Flutter app"
@echo " make test-e2e Run full Maestro E2E suite (Client + Staff auth)"
@echo " make test-e2e-client Run Client Maestro E2E only"
@echo " make test-e2e-staff Run Staff Maestro E2E only"
@echo "" @echo ""
@echo " 🗄️ DATA CONNECT & BACKEND (backend/dataconnect)" @echo " 🗄️ DATA CONNECT & BACKEND (backend/dataconnect)"
@echo " ────────────────────────────────────────────────────────────────────" @echo " ────────────────────────────────────────────────────────────────────"

View File

@@ -1,5 +1,3 @@
import 'dart:io' show Platform;
import 'package:client_authentication/client_authentication.dart' import 'package:client_authentication/client_authentication.dart'
as client_authentication; as client_authentication;
import 'package:client_create_order/client_create_order.dart' 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:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marionette_flutter/marionette_flutter.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
@@ -23,23 +20,7 @@ import 'firebase_options.dart';
import 'src/widgets/session_listener.dart'; import 'src/widgets/session_listener.dart';
void main() async { 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( await Firebase.initializeApp(
options: kIsWeb ? DefaultFirebaseOptions.currentPlatform : null, options: kIsWeb ? DefaultFirebaseOptions.currentPlatform : null,
); );

View File

@@ -1,42 +1,33 @@
# Maestro Integration Tests — Client App # Maestro Integration Tests — Client App
Login and signup flows for the KROW Client app. Auth flows for the KROW Client app.
See [docs/research/flutter-testing-tools.md](/docs/research/flutter-testing-tools.md) for the evaluation report. 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).
**Full run instructions:** [docs/research/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: maestro/
```bash auth/
cd apps/mobile && flutter build apk sign_in.yaml
adb install build/app/outputs/flutter-apk/app-debug.apk sign_up.yaml
``` ```
## Credentials ## Credentials (env, never hardcoded)
| Flow | Credentials | | Flow | Env variables |
|------|-------------| |------|---------------|
| **Client login** | legendary@krowd.com / Demo2026! | | sign_in | `TEST_CLIENT_EMAIL`, `TEST_CLIENT_PASSWORD` |
| **Staff login** | 5557654321 / OTP 123456 | | sign_up | `TEST_CLIENT_EMAIL`, `TEST_CLIENT_PASSWORD`, `TEST_CLIENT_COMPANY` |
| **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) |
## Run ## Run
From the project root:
```bash ```bash
# Login # Via Makefile (export vars first)
maestro test apps/mobile/apps/client/maestro/login.yaml make test-e2e-client
# Signup # Direct
maestro test apps/mobile/apps/client/maestro/signup.yaml 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 sdk: flutter
firebase_core: ^4.4.0 firebase_core: ^4.4.0
krow_data_connect: ^0.0.1 krow_data_connect: ^0.0.1
marionette_flutter: ^0.3.0
dev_dependencies: dev_dependencies:
flutter_test: 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:core_localization/core_localization.dart' as core_localization;
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.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_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krowwithus_staff/firebase_options.dart'; import 'package:krowwithus_staff/firebase_options.dart';
import 'package:marionette_flutter/marionette_flutter.dart';
import 'package:staff_authentication/staff_authentication.dart' import 'package:staff_authentication/staff_authentication.dart'
as staff_authentication; as staff_authentication;
import 'package:staff_main/staff_main.dart' as staff_main; 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'; import 'src/widgets/session_listener.dart';
void main() async { 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); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// Register global BLoC observer for centralized error logging // Register global BLoC observer for centralized error logging

View File

@@ -1,41 +1,38 @@
# Maestro Integration Tests — Staff App # Maestro Integration Tests — Staff App
Login and signup flows for the KROW Staff app. Auth flows for the KROW Staff app.
See [docs/research/flutter-testing-tools.md](/docs/research/flutter-testing-tools.md) for the evaluation report. 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).
**Full run instructions:** [docs/research/maestro-test-run-instructions.md](/docs/research/maestro-test-run-instructions.md)
## Structure
```
maestro/
auth/
sign_in.yaml
sign_up.yaml
```
## Prerequisites ## Prerequisites
- [Maestro CLI](https://maestro.dev/docs/getting-started/installation) installed - Firebase test phone in Auth > Phone (e.g. +1 555-765-4321 / OTP 123456)
- Staff app built and installed - For sign_up: use a different test number (not yet registered)
- **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
## Credentials ## Credentials (env, never hardcoded)
| Flow | Credentials | | Flow | Env variables |
|------|-------------| |------|---------------|
| **Client login** | legendary@krowd.com / Demo2026! | | sign_in | `TEST_STAFF_PHONE`, `TEST_STAFF_OTP` |
| **Staff login** | 5557654321 / OTP 123456 | | sign_up | `TEST_STAFF_SIGNUP_PHONE`, `TEST_STAFF_OTP` |
| **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) |
## Run ## Run
From the project root:
```bash ```bash
# Login # Via Makefile (export vars first)
maestro test apps/mobile/apps/staff/maestro/login.yaml make test-e2e-staff
# Signup # Direct
maestro test apps/mobile/apps/staff/maestro/signup.yaml 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 path: ../../packages/core
krow_data_connect: krow_data_connect:
path: ../../packages/data_connect path: ../../packages/data_connect
marionette_flutter: ^0.3.0
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
flutter_modular: ^6.3.0 flutter_modular: ^6.3.0
firebase_core: ^4.4.0 firebase_core: ^4.4.0

View File

@@ -11,6 +11,7 @@ class UiTextField extends StatelessWidget {
const UiTextField({ const UiTextField({
super.key, super.key,
this.semanticsIdentifier,
this.label, this.label,
this.hintText, this.hintText,
this.onChanged, this.onChanged,
@@ -29,6 +30,8 @@ class UiTextField extends StatelessWidget {
this.onTap, this.onTap,
this.validator, this.validator,
}); });
/// Optional semantics identifier for E2E testing (e.g. Maestro).
final String? semanticsIdentifier;
/// The label text to display above the text field. /// The label text to display above the text field.
final String? label; final String? label;
@@ -90,7 +93,9 @@ class UiTextField extends StatelessWidget {
Text(label!, style: UiTypography.body4m.textSecondary), Text(label!, style: UiTypography.body4m.textSecondary),
const SizedBox(height: UiConstants.space1), const SizedBox(height: UiConstants.space1),
], ],
TextFormField( Builder(
builder: (BuildContext context) {
final Widget field = TextFormField(
controller: controller, controller: controller,
onChanged: onChanged, onChanged: onChanged,
keyboardType: keyboardType, keyboardType: keyboardType,
@@ -113,6 +118,15 @@ class UiTextField extends StatelessWidget {
? Icon(suffixIcon, size: 20, color: UiColors.iconSecondary) ? Icon(suffixIcon, size: 20, color: UiColors.iconSecondary)
: suffix, : 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>[ children: <Widget>[
// Email Field // Email Field
UiTextField( UiTextField(
semanticsIdentifier: 'sign_in_email',
label: i18n.email_label, label: i18n.email_label,
hintText: i18n.email_hint, hintText: i18n.email_hint,
controller: _emailController, controller: _emailController,
@@ -61,6 +62,7 @@ class _ClientSignInFormState extends State<ClientSignInForm> {
// Password Field // Password Field
UiTextField( UiTextField(
semanticsIdentifier: 'sign_in_password',
label: i18n.password_label, label: i18n.password_label,
hintText: i18n.password_hint, hintText: i18n.password_hint,
controller: _passwordController, controller: _passwordController,

View File

@@ -70,6 +70,7 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
children: <Widget>[ children: <Widget>[
// Company Name Field // Company Name Field
UiTextField( UiTextField(
semanticsIdentifier: 'sign_up_company',
label: i18n.company_label, label: i18n.company_label,
hintText: i18n.company_hint, hintText: i18n.company_hint,
controller: _companyController, controller: _companyController,
@@ -79,6 +80,7 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
// Email Field // Email Field
UiTextField( UiTextField(
semanticsIdentifier: 'sign_up_email',
label: i18n.email_label, label: i18n.email_label,
hintText: i18n.email_hint, hintText: i18n.email_hint,
controller: _emailController, controller: _emailController,
@@ -89,6 +91,7 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
// Password Field // Password Field
UiTextField( UiTextField(
semanticsIdentifier: 'sign_up_password',
label: i18n.password_label, label: i18n.password_label,
hintText: i18n.password_hint, hintText: i18n.password_hint,
controller: _passwordController, controller: _passwordController,
@@ -108,6 +111,7 @@ class _ClientSignUpFormState extends State<ClientSignUpForm> {
// Confirm Password Field // Confirm Password Field
UiTextField( UiTextField(
semanticsIdentifier: 'sign_up_confirm_password',
label: i18n.confirm_password_label, label: i18n.confirm_password_label,
hintText: i18n.confirm_password_hint, hintText: i18n.confirm_password_hint,
controller: _confirmPasswordController, 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:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -75,10 +75,7 @@ class _OtpInputFieldState extends State<OtpInputField> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(6, (int index) { children: List.generate(6, (int index) {
return SizedBox( final TextField field = TextField(
width: 45,
height: 56,
child: TextField(
controller: _controllers[index], controller: _controllers[index],
focusNode: _focusNodes[index], focusNode: _focusNodes[index],
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
@@ -112,7 +109,16 @@ class _OtpInputFieldState extends State<OtpInputField> {
), ),
onChanged: (String value) => onChanged: (String value) =>
_onChanged(context: context, index: index, value: 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,6 +82,8 @@ class _PhoneInputFormFieldState extends State<PhoneInputFormField> {
), ),
const SizedBox(width: UiConstants.space2), const SizedBox(width: UiConstants.space2),
Expanded( Expanded(
child: Semantics(
identifier: 'staff_phone_input',
child: TextField( child: TextField(
controller: _controller, controller: _controller,
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
@@ -95,6 +97,7 @@ class _PhoneInputFormFieldState extends State<PhoneInputFormField> {
onChanged: widget.onChanged, onChanged: widget.onChanged,
), ),
), ),
),
], ],
), ),
if (widget.error.isNotEmpty) if (widget.error.isNotEmpty)

View File

@@ -925,14 +925,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.257.0" 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: matcher:
dependency: transitive dependency: transitive
description: description:

View File

@@ -69,16 +69,14 @@ Semantics(
``` ```
### Phase 2: Repository Structure (Implemented) ### Phase 2: Repository Structure (Implemented)
Maestro flows are co-located with each app: Maestro flows are co-located with each app under `auth/`:
* `apps/mobile/apps/client/maestro/login.yaml` — Client login * `apps/mobile/apps/client/maestro/auth/sign_in.yaml` — Client sign-in
* `apps/mobile/apps/client/maestro/signup.yaml` — Client signup * `apps/mobile/apps/client/maestro/auth/sign_up.yaml` — Client sign-up
* `apps/mobile/apps/staff/maestro/login.yaml` — Staff login (phone + OTP) * `apps/mobile/apps/staff/maestro/auth/sign_in.yaml` — Staff sign-in (phone + OTP)
* `apps/mobile/apps/staff/maestro/signup.yaml` — Staff signup (phone + OTP) * `apps/mobile/apps/staff/maestro/auth/sign_up.yaml` — Staff sign-up (phone + OTP)
Each directory has a README with run instructions. Credentials are injected via env variables (never hardcoded). Use `make test-e2e` to run the suite.
**Marionette MCP:** `marionette_flutter` is added to both apps; `MarionetteBinding` is initialized in debug mode. See [marionette-spike-usage.md](marionette-spike-usage.md) for prompts and workflow.
### Phase 3: CI/CD Integration ### Phase 3: CI/CD Integration
The Maestro CLI will be added to our **GitHub Actions** workflow to automate quality gates. The Maestro CLI will be added to our **GitHub Actions** workflow to automate quality gates.

View File

@@ -1,75 +1,92 @@
# How to Run Maestro Integration Tests # How to Run Maestro Integration Tests
## Credentials Credentials are injected via env variables — **never hardcoded** in YAML.
| Flow | Credentials | ## Env variables
|------|-------------|
| **Client login** | legendary@krowd.com / Demo2026! | | Flow | Env variables |
| **Staff login** | 5557654321 / OTP 123456 | |------|---------------|
| **Client signup** | Env vars: `MAESTRO_CLIENT_EMAIL`, `MAESTRO_CLIENT_PASSWORD`, `MAESTRO_CLIENT_COMPANY` | | **Client sign-in** | `TEST_CLIENT_EMAIL`, `TEST_CLIENT_PASSWORD` |
| **Staff signup** | Env var: `MAESTRO_STAFF_SIGNUP_PHONE` (must be new Firebase test phone) | | **Client sign-up** | `TEST_CLIENT_EMAIL`, `TEST_CLIENT_PASSWORD`, `TEST_CLIENT_COMPANY` |
| **Staff sign-in** | `TEST_STAFF_PHONE`, `TEST_STAFF_OTP` |
| **Staff sign-up** | `TEST_STAFF_SIGNUP_PHONE`, `TEST_STAFF_OTP` |
**Example values (login):** legendary@krowd.com / Demo2026! (client), 5557654321 / 123456 (staff)
--- ---
## Step-by-step: Run login tests ## Step-by-step
### 1. Install Maestro CLI ### 1. Install Maestro CLI
**Windows:** Download from [Maestro releases](https://github.com/mobile-dev-inc/maestro/releases), extract, add to PATH.
**macOS/Linux:**
```bash ```bash
curl -Ls "https://get.maestro.mobile.dev" | bash curl -Ls "https://get.maestro.mobile.dev" | bash
``` ```
Or: https://maestro.dev/docs/getting-started/installation ### 2. Add Firebase test phone (Staff app)
### 2. Add Firebase test phone (Staff app only) Firebase Console → **Authentication****Sign-in method****Phone****Phone numbers for testing**:
- Add **+1 5557654321** with verification code **123456**
In [Firebase Console](https://console.firebase.google.com) → your project → **Authentication****Sign-in method****Phone****Phone numbers for testing**: ### 3. Build and install apps
- Add: **+1 5557654321** with verification code **123456**
### 3. Build and install the apps
From the **project root**:
```bash ```bash
# Client
make mobile-client-build PLATFORM=apk MODE=debug make mobile-client-build PLATFORM=apk MODE=debug
adb install apps/mobile/apps/client/build/app/outputs/flutter-apk/app-debug.apk adb install apps/mobile/apps/client/build/app/outputs/flutter-apk/app-debug.apk
# Staff
make mobile-staff-build PLATFORM=apk MODE=debug make mobile-staff-build PLATFORM=apk MODE=debug
adb install apps/mobile/apps/staff/build/app/outputs/flutter-apk/app-debug.apk adb install apps/mobile/apps/staff/build/app/outputs/flutter-apk/app-debug.apk
``` ```
Or run the app on a connected device/emulator: `make mobile-client-dev-android DEVICE=<id>` (then Maestro can launch the already-installed app by appId). ### 4. Run E2E tests via Makefile
### 4. Run Maestro tests **Export credentials, then run:**
From the **project root** (`e:\Krow-google\krow-workforce`):
```bash ```bash
# Client login (uses legendary@krowd.com / Demo2026!) # Client login credentials
maestro test apps/mobile/apps/client/maestro/login.yaml export TEST_CLIENT_EMAIL=legendary@krowd.com
export TEST_CLIENT_PASSWORD=Demo2026!
export TEST_CLIENT_COMPANY="Krow Demo"
# Staff login (uses 5557654321 / OTP 123456) # Staff login credentials
maestro test apps/mobile/apps/staff/maestro/login.yaml export TEST_STAFF_PHONE=5557654321
export TEST_STAFF_OTP=123456
export TEST_STAFF_SIGNUP_PHONE=5555550000 # use a new number for signup
# Run full suite
make test-e2e
# Or run per app
make test-e2e-client
make test-e2e-staff
``` ```
### 5. Run signup tests (optional) ### 5. Run flows directly (without Make)
**Client signup** — set env vars first:
```bash ```bash
$env:MAESTRO_CLIENT_EMAIL="newuser@example.com" maestro test apps/mobile/apps/client/maestro/auth/sign_in.yaml \
$env:MAESTRO_CLIENT_PASSWORD="YourPassword123!" -e TEST_CLIENT_EMAIL=legendary@krowd.com \
$env:MAESTRO_CLIENT_COMPANY="Test Company" -e TEST_CLIENT_PASSWORD=Demo2026!
maestro test apps/mobile/apps/client/maestro/signup.yaml
maestro test apps/mobile/apps/staff/maestro/auth/sign_in.yaml \
-e TEST_STAFF_PHONE=5557654321 \
-e TEST_STAFF_OTP=123456
``` ```
**Staff signup** — use a new Firebase test phone: ---
```bash
# Add +1 555-555-0000 / 123456 in Firebase, then: ## Folder structure
$env:MAESTRO_STAFF_SIGNUP_PHONE="5555550000"
maestro test apps/mobile/apps/staff/maestro/signup.yaml ```
apps/mobile/apps/client/maestro/auth/
sign_in.yaml
sign_up.yaml
apps/mobile/apps/staff/maestro/auth/
sign_in.yaml
sign_up.yaml
``` ```
--- ---
@@ -77,8 +94,7 @@ maestro test apps/mobile/apps/staff/maestro/signup.yaml
## Checklist ## Checklist
- [ ] Maestro CLI installed - [ ] Maestro CLI installed
- [ ] Firebase test phone +1 5557654321 / 123456 added (for staff) - [ ] Firebase test phone +1 5557654321 / 123456 added
- [ ] Client app built and installed - [ ] Client & Staff apps built and installed
- [ ] Staff app built and installed - [ ] Env vars exported
- [ ] Run from project root: `maestro test apps/mobile/apps/client/maestro/login.yaml` - [ ] `make test-e2e` run from project root
- [ ] Run from project root: `maestro test apps/mobile/apps/staff/maestro/login.yaml`

View File

@@ -1,58 +0,0 @@
# Marionette MCP Spike — Usage Guide
**Issue:** #533
**Purpose:** Document how to run the Marionette MCP spike for auth flows.
## Prerequisites
1. **Marionette MCP server** — Install globally:
```bash
dart pub global activate marionette_mcp
```
2. **Add Marionette to Cursor** — In `.cursor/mcp.json` or global config:
```json
{
"mcpServers": {
"marionette": {
"command": "marionette_mcp",
"args": []
}
}
}
```
3. **Run app in debug mode** — The app must be running with VM Service:
```bash
cd apps/mobile && flutter run -d <device_id>
```
4. **Get VM Service URI** — From the `flutter run` output, copy the `ws://127.0.0.1:XXXX/ws` URI (often shown in the DevTools link).
## Spike flows (AI agent prompts)
Use these prompts with the Marionette MCP connected to the running app.
### Client — Login
> Connect to the app using the VM Service URI. Navigate to the Get Started screen, tap "Sign In", enter legendary@krowd.com and Demo2026!, then tap "Sign In". Verify we land on the home screen.
### Client — Sign up
> Connect to the app. Tap "Create Account", fill in Company, Email, Password (and confirm) with new credentials, then tap "Create Account". Verify we land on the home screen.
### Staff — Login
> Connect to the app. Tap "Log In", enter phone number 5557654321, tap "Send Code", enter OTP 123456, tap "Continue". Verify we reach the staff home screen.
> (Firebase test phone: +1 555-765-4321 / OTP 123456)
### Staff — Sign up
> Connect to the app. Tap "Sign Up", enter a NEW phone number (Firebase test phone), tap "Send Code", enter OTP, tap "Continue". Verify we reach Profile Setup or staff home.
## Limitations observed (from spike)
- **Debug only** — Marionette needs the Dart VM Service; does not work with release builds.
- **Non-deterministic** — LLM-driven actions can vary in behavior and timing.
- **Latency** — Each step involves API roundtrips (~45s+ for full flow vs ~5s for Maestro).
- **Best use** — Exploratory testing, live debugging, smoke checks during development.

View File

@@ -1,6 +1,6 @@
# --- Mobile App Development --- # --- Mobile App Development ---
.PHONY: mobile-install mobile-info mobile-analyze mobile-client-dev-android mobile-staff-dev-android mobile-client-build mobile-staff-build mobile-hot-reload mobile-hot-restart .PHONY: mobile-install mobile-info mobile-analyze mobile-client-dev-android mobile-staff-dev-android mobile-client-build mobile-staff-build mobile-hot-reload mobile-hot-restart test-e2e test-e2e-setup test-e2e-client test-e2e-staff
MOBILE_DIR := apps/mobile MOBILE_DIR := apps/mobile
@@ -69,3 +69,32 @@ mobile-staff-build: dataconnect-generate-sdk
melos exec --scope="core_localization" -- "dart run slang" && \ melos exec --scope="core_localization" -- "dart run slang" && \
melos exec --scope="core_localization" -- "dart run build_runner build --delete-conflicting-outputs" && \ melos exec --scope="core_localization" -- "dart run build_runner build --delete-conflicting-outputs" && \
melos exec --scope="krowwithus_staff" -- "flutter build $(PLATFORM) --$(MODE) --dart-define-from-file=../../config.dev.json" melos exec --scope="krowwithus_staff" -- "flutter build $(PLATFORM) --$(MODE) --dart-define-from-file=../../config.dev.json"
# --- E2E (Maestro) ---
# Set env before running: TEST_CLIENT_EMAIL, TEST_CLIENT_PASSWORD, TEST_CLIENT_COMPANY, TEST_STAFF_PHONE, TEST_STAFF_OTP, TEST_STAFF_SIGNUP_PHONE
# Example: export TEST_CLIENT_EMAIL=legendary@krowd.com TEST_CLIENT_PASSWORD=Demo2026!
# Example: export TEST_STAFF_PHONE=5557654321 TEST_STAFF_OTP=123456
test-e2e-setup:
@echo "--> Checking Maestro CLI..."
@maestro --version
@echo "--> Maestro OK. Ensure apps are built & installed (see docs/research/maestro-test-run-instructions.md)"
test-e2e: test-e2e-setup
@echo "--> Running full E2E suite (Client + Staff auth flows)..."
@maestro test apps/mobile/apps/client/maestro/auth/sign_in.yaml apps/mobile/apps/client/maestro/auth/sign_up.yaml \
apps/mobile/apps/staff/maestro/auth/sign_in.yaml apps/mobile/apps/staff/maestro/auth/sign_up.yaml \
-e TEST_CLIENT_EMAIL="$${TEST_CLIENT_EMAIL}" -e TEST_CLIENT_PASSWORD="$${TEST_CLIENT_PASSWORD}" \
-e TEST_CLIENT_COMPANY="$${TEST_CLIENT_COMPANY}" -e TEST_STAFF_PHONE="$${TEST_STAFF_PHONE}" \
-e TEST_STAFF_OTP="$${TEST_STAFF_OTP}" -e TEST_STAFF_SIGNUP_PHONE="$${TEST_STAFF_SIGNUP_PHONE}"
test-e2e-client: test-e2e-setup
@echo "--> Running Client E2E (sign_in, sign_up)..."
@maestro test apps/mobile/apps/client/maestro/auth/sign_in.yaml apps/mobile/apps/client/maestro/auth/sign_up.yaml \
-e TEST_CLIENT_EMAIL="$${TEST_CLIENT_EMAIL}" -e TEST_CLIENT_PASSWORD="$${TEST_CLIENT_PASSWORD}" \
-e TEST_CLIENT_COMPANY="$${TEST_CLIENT_COMPANY}"
test-e2e-staff: test-e2e-setup
@echo "--> Running Staff E2E (sign_in, sign_up)..."
@maestro test apps/mobile/apps/staff/maestro/auth/sign_in.yaml apps/mobile/apps/staff/maestro/auth/sign_up.yaml \
-e TEST_STAFF_PHONE="$${TEST_STAFF_PHONE}" -e TEST_STAFF_OTP="$${TEST_STAFF_OTP}" \
-e TEST_STAFF_SIGNUP_PHONE="$${TEST_STAFF_SIGNUP_PHONE}"