diff --git a/Makefile b/Makefile index 2b2f8c55..98eef521 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,9 @@ help: @echo " make mobile-test Run flutter test for client+staff" @echo " make mobile-hot-reload Hot reload 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 " 🗄️ DATA CONNECT & BACKEND (backend/dataconnect)" @echo " ────────────────────────────────────────────────────────────────────" diff --git a/apps/mobile/apps/client/lib/main.dart b/apps/mobile/apps/client/lib/main.dart index ddfa75aa..a0e67c19 100644 --- a/apps/mobile/apps/client/lib/main.dart +++ b/apps/mobile/apps/client/lib/main.dart @@ -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, ); diff --git a/apps/mobile/apps/client/maestro/README.md b/apps/mobile/apps/client/maestro/README.md index 97407ed3..ef8712a1 100644 --- a/apps/mobile/apps/client/maestro/README.md +++ b/apps/mobile/apps/client/maestro/README.md @@ -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 | diff --git a/apps/mobile/apps/client/maestro/auth/sign_in.yaml b/apps/mobile/apps/client/maestro/auth/sign_in.yaml new file mode 100644 index 00000000..245a09f3 --- /dev/null +++ b/apps/mobile/apps/client/maestro/auth/sign_in.yaml @@ -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" diff --git a/apps/mobile/apps/client/maestro/auth/sign_up.yaml b/apps/mobile/apps/client/maestro/auth/sign_up.yaml new file mode 100644 index 00000000..010bbe8a --- /dev/null +++ b/apps/mobile/apps/client/maestro/auth/sign_up.yaml @@ -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" diff --git a/apps/mobile/apps/client/maestro/login.yaml b/apps/mobile/apps/client/maestro/login.yaml deleted file mode 100644 index 6598a03f..00000000 --- a/apps/mobile/apps/client/maestro/login.yaml +++ /dev/null @@ -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" diff --git a/apps/mobile/apps/client/maestro/signup.yaml b/apps/mobile/apps/client/maestro/signup.yaml deleted file mode 100644 index eba61eb0..00000000 --- a/apps/mobile/apps/client/maestro/signup.yaml +++ /dev/null @@ -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" diff --git a/apps/mobile/apps/client/pubspec.yaml b/apps/mobile/apps/client/pubspec.yaml index 31c14ec3..b4d6367b 100644 --- a/apps/mobile/apps/client/pubspec.yaml +++ b/apps/mobile/apps/client/pubspec.yaml @@ -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: diff --git a/apps/mobile/apps/staff/lib/main.dart b/apps/mobile/apps/staff/lib/main.dart index 440dba19..5557a971 100644 --- a/apps/mobile/apps/staff/lib/main.dart +++ b/apps/mobile/apps/staff/lib/main.dart @@ -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 diff --git a/apps/mobile/apps/staff/maestro/README.md b/apps/mobile/apps/staff/maestro/README.md index 505faaec..f790d243 100644 --- a/apps/mobile/apps/staff/maestro/README.md +++ b/apps/mobile/apps/staff/maestro/README.md @@ -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 | diff --git a/apps/mobile/apps/staff/maestro/auth/sign_in.yaml b/apps/mobile/apps/staff/maestro/auth/sign_in.yaml new file mode 100644 index 00000000..17e0dc31 --- /dev/null +++ b/apps/mobile/apps/staff/maestro/auth/sign_in.yaml @@ -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 diff --git a/apps/mobile/apps/staff/maestro/auth/sign_up.yaml b/apps/mobile/apps/staff/maestro/auth/sign_up.yaml new file mode 100644 index 00000000..e6e7c1bd --- /dev/null +++ b/apps/mobile/apps/staff/maestro/auth/sign_up.yaml @@ -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} diff --git a/apps/mobile/apps/staff/maestro/login.yaml b/apps/mobile/apps/staff/maestro/login.yaml deleted file mode 100644 index aa0b21a1..00000000 --- a/apps/mobile/apps/staff/maestro/login.yaml +++ /dev/null @@ -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. diff --git a/apps/mobile/apps/staff/maestro/signup.yaml b/apps/mobile/apps/staff/maestro/signup.yaml deleted file mode 100644 index e441e774..00000000 --- a/apps/mobile/apps/staff/maestro/signup.yaml +++ /dev/null @@ -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. diff --git a/apps/mobile/apps/staff/pubspec.yaml b/apps/mobile/apps/staff/pubspec.yaml index 4019f01b..d3b270ef 100644 --- a/apps/mobile/apps/staff/pubspec.yaml +++ b/apps/mobile/apps/staff/pubspec.yaml @@ -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 diff --git a/apps/mobile/packages/design_system/lib/src/widgets/ui_text_field.dart b/apps/mobile/packages/design_system/lib/src/widgets/ui_text_field.dart index 9ae7ff61..705e4542 100644 --- a/apps/mobile/packages/design_system/lib/src/widgets/ui_text_field.dart +++ b/apps/mobile/packages/design_system/lib/src/widgets/ui_text_field.dart @@ -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; + }, ), ], ); diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_in_page/client_sign_in_form.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_in_page/client_sign_in_form.dart index 129fc662..9a3d4c3b 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_in_page/client_sign_in_form.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_in_page/client_sign_in_form.dart @@ -52,6 +52,7 @@ class _ClientSignInFormState extends State { children: [ // 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 { // Password Field UiTextField( + semanticsIdentifier: 'sign_in_password', label: i18n.password_label, hintText: i18n.password_hint, controller: _passwordController, diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_up_page/client_sign_up_form.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_up_page/client_sign_up_form.dart index b6617bdc..2bf0f0a0 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_up_page/client_sign_up_form.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/widgets/client_sign_up_page/client_sign_up_form.dart @@ -70,6 +70,7 @@ class _ClientSignUpFormState extends State { children: [ // 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 { // 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 { // 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 { // Confirm Password Field UiTextField( + semanticsIdentifier: 'sign_up_confirm_password', label: i18n.confirm_password_label, hintText: i18n.confirm_password_hint, controller: _confirmPasswordController, diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart index 05fa1f30..1d757a3f 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/otp_verification/otp_input_field.dart @@ -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 { 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 { ), 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, ); }), ), diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/phone_input/phone_input_form_field.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/phone_input/phone_input_form_field.dart index 256e4f7b..656f1877 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/phone_input/phone_input_form_field.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/widgets/phone_verification_page/phone_input/phone_input_form_field.dart @@ -82,17 +82,20 @@ class _PhoneInputFormFieldState extends State { ), const SizedBox(width: UiConstants.space2), Expanded( - child: TextField( - controller: _controller, - keyboardType: TextInputType.phone, - inputFormatters: [ - 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: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(11), + ], + decoration: InputDecoration( + hintText: t.staff_authentication.phone_input.hint, + ), + onChanged: widget.onChanged, ), - onChanged: widget.onChanged, ), ), ], diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index 1270ef05..07839283 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -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: diff --git a/docs/research/flutter-testing-tools.md b/docs/research/flutter-testing-tools.md index d7cde701..86d9b7f8 100644 --- a/docs/research/flutter-testing-tools.md +++ b/docs/research/flutter-testing-tools.md @@ -69,16 +69,14 @@ Semantics( ``` ### 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/signup.yaml` — Client signup -* `apps/mobile/apps/staff/maestro/login.yaml` — Staff login (phone + OTP) -* `apps/mobile/apps/staff/maestro/signup.yaml` — Staff signup (phone + OTP) +* `apps/mobile/apps/client/maestro/auth/sign_in.yaml` — Client sign-in +* `apps/mobile/apps/client/maestro/auth/sign_up.yaml` — Client sign-up +* `apps/mobile/apps/staff/maestro/auth/sign_in.yaml` — Staff sign-in (phone + OTP) +* `apps/mobile/apps/staff/maestro/auth/sign_up.yaml` — Staff sign-up (phone + OTP) -Each directory has a README with run instructions. - -**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. +Credentials are injected via env variables (never hardcoded). Use `make test-e2e` to run the suite. ### Phase 3: CI/CD Integration The Maestro CLI will be added to our **GitHub Actions** workflow to automate quality gates. diff --git a/docs/research/maestro-test-run-instructions.md b/docs/research/maestro-test-run-instructions.md index a4fb80e7..190b83b0 100644 --- a/docs/research/maestro-test-run-instructions.md +++ b/docs/research/maestro-test-run-instructions.md @@ -1,75 +1,92 @@ # How to Run Maestro Integration Tests -## Credentials +Credentials are injected via env variables — **never hardcoded** in YAML. -| 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) | +## Env variables + +| Flow | Env variables | +|------|---------------| +| **Client sign-in** | `TEST_CLIENT_EMAIL`, `TEST_CLIENT_PASSWORD` | +| **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 +**Windows:** Download from [Maestro releases](https://github.com/mobile-dev-inc/maestro/releases), extract, add to PATH. + +**macOS/Linux:** ```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**: - -- Add: **+1 5557654321** with verification code **123456** - -### 3. Build and install the apps - -From the **project root**: +### 3. Build and install apps ```bash -# Client make mobile-client-build PLATFORM=apk MODE=debug adb install apps/mobile/apps/client/build/app/outputs/flutter-apk/app-debug.apk -# Staff make mobile-staff-build PLATFORM=apk MODE=debug 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=` (then Maestro can launch the already-installed app by appId). +### 4. Run E2E tests via Makefile -### 4. Run Maestro tests - -From the **project root** (`e:\Krow-google\krow-workforce`): +**Export credentials, then run:** ```bash -# Client login (uses legendary@krowd.com / Demo2026!) -maestro test apps/mobile/apps/client/maestro/login.yaml +# Client login credentials +export TEST_CLIENT_EMAIL=legendary@krowd.com +export TEST_CLIENT_PASSWORD=Demo2026! +export TEST_CLIENT_COMPANY="Krow Demo" -# Staff login (uses 5557654321 / OTP 123456) -maestro test apps/mobile/apps/staff/maestro/login.yaml +# Staff login credentials +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 -$env:MAESTRO_CLIENT_EMAIL="newuser@example.com" -$env:MAESTRO_CLIENT_PASSWORD="YourPassword123!" -$env:MAESTRO_CLIENT_COMPANY="Test Company" -maestro test apps/mobile/apps/client/maestro/signup.yaml +maestro test apps/mobile/apps/client/maestro/auth/sign_in.yaml \ + -e TEST_CLIENT_EMAIL=legendary@krowd.com \ + -e TEST_CLIENT_PASSWORD=Demo2026! + +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: -$env:MAESTRO_STAFF_SIGNUP_PHONE="5555550000" -maestro test apps/mobile/apps/staff/maestro/signup.yaml +--- + +## Folder structure + +``` +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 - [ ] Maestro CLI installed -- [ ] Firebase test phone +1 5557654321 / 123456 added (for staff) -- [ ] Client app built and installed -- [ ] Staff app built and installed -- [ ] Run from project root: `maestro test apps/mobile/apps/client/maestro/login.yaml` -- [ ] Run from project root: `maestro test apps/mobile/apps/staff/maestro/login.yaml` +- [ ] Firebase test phone +1 5557654321 / 123456 added +- [ ] Client & Staff apps built and installed +- [ ] Env vars exported +- [ ] `make test-e2e` run from project root diff --git a/docs/research/marionette-spike-usage.md b/docs/research/marionette-spike-usage.md deleted file mode 100644 index 09553e89..00000000 --- a/docs/research/marionette-spike-usage.md +++ /dev/null @@ -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 - ``` - -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. diff --git a/makefiles/mobile.mk b/makefiles/mobile.mk index 43c3d618..4338cb7b 100644 --- a/makefiles/mobile.mk +++ b/makefiles/mobile.mk @@ -1,6 +1,6 @@ # --- 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 @@ -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 build_runner build --delete-conflicting-outputs" && \ 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}"