comprehensive cases

This commit is contained in:
2026-03-17 15:21:06 +05:30
parent e3d8d30b1b
commit 68b0055cfe
30 changed files with 1285 additions and 227 deletions

View File

@@ -0,0 +1,62 @@
# Client App — E2E: Comprehensive Invoice Approval
# Purpose:
# - Navigates to Billing -> Awaiting Approval.
# - Captures the Invoice ID or date/amount (conceptually).
# - Approves the invoice.
# - Verifies success message.
# - Navigates to the "Approved" tab.
# - Verifies the invoice is now listed in the history.
#
# Run:
# maestro test \
11: # apps/mobile/apps/client/maestro/auth/happy_path/sign_in.yaml \
12: # apps/mobile/apps/client/maestro/billing/happy_path/comprehensive_billing_flow.yaml \
13: # -e TEST_CLIENT_EMAIL=... \
14: # -e TEST_CLIENT_PASSWORD=...
appId: com.krowwithus.client
---
- launchApp:
clearState: false
- tapOn: "Home"
- tapOn: "Billing"
- extendedWaitUntil:
visible: "Awaiting Approval"
timeout: 10000
# 1. Enter the approval flow
- tapOn: "Awaiting Approval"
- extendedWaitUntil:
visible: "Review & Approve"
timeout: 10000
# 2. Approve the first invoice
- tapOn: "Review & Approve"
- extendedWaitUntil:
visible: "Approve"
timeout: 10000
- tapOn: "Approve"
# 3. Verify success message
- extendedWaitUntil:
visible: "Invoice approved and payment initiated"
timeout: 15000
# 4. Deep Verification: check the 'Approved' tab
- tapOn: "Approved"
- extendedWaitUntil:
visible: "Invoice History"
timeout: 10000
# Ensure we see a 'View Receipt' or 'Approved' status instead of 'Review'
- assertVisible: "View Receipt"
- assertNotVisible: "Review & Approve"
# Back to Home
- tapOn: "Home"

View File

@@ -0,0 +1,54 @@
# Client App — E2E: Hub Management Lifecycle
# Purpose:
# - Create a new Hub.
# - Find and Edit the Hub.
# - Delete the Hub.
# - Verify it is gone from the list.
appId: com.krowwithus.client
---
- launchApp:
clearState: false
- tapOn: "Home"
- tapOn: "Settings" # Assuming Hubs are managed via Settings or a Hubs tab
# 1. Navigate to Hubs
- tapOn: "Manage Hubs"
# 2. Create Hub
- tapOn: "Add New Hub" # Or the '+' button
- tapOn: "HUB NAME"
- inputText: "Maestro Test Hub"
- tapOn: "ADDRESS"
- inputText: "123 Test Street, NYC"
- hideKeyboard
- tapOn: "Save Hub"
- extendedWaitUntil:
visible: "Hub created successfully"
timeout: 10000
# 3. Edit Hub
- tapOn: "Maestro Test Hub"
- tapOn: "HUB NAME"
- inputText: "Updated Maestro Hub"
- hideKeyboard
- tapOn: "Save Hub"
- extendedWaitUntil:
visible: "Hub updated successfully"
timeout: 10000
# 4. Delete Hub
- tapOn: "Updated Maestro Hub"
- tapOn: "Delete Hub"
- tapOn: "DELETE" # Confirmation
- extendedWaitUntil:
visible: "Hub deleted"
timeout: 10000
# 5. Verify gone
- assertNotVisible: "Updated Maestro Hub"

View File

@@ -0,0 +1,85 @@
# DEBUG: Staff Search Only
# Tests ONLY the Staff shift search flow in isolation.
# Pass a known order name to verify the Staff app can find it.
#
# Usage:
# maestro test staff_search_only.yaml -e TEST_STAFF_PHONE="5557654321" -e TEST_STAFF_OTP="123456" -e TEST_ORDER_NAME="E2E-XXXXX"
appId: com.krowwithus.staff
---
- launchApp:
clearState: false
# Wait for app to fully render
- waitForAnimationToEnd:
timeout: 15000
# Sign in if needed
- runFlow:
when:
visible: "Log In"
file: ../../../../staff/maestro/auth/happy_path/sign_in.yaml
env:
TEST_STAFF_PHONE: ${TEST_STAFF_PHONE}
TEST_STAFF_OTP: ${TEST_STAFF_OTP}
# Navigate to Shifts
- tapOn:
id: "nav_shifts"
# Pull to refresh (force reload from backend)
- swipe:
start: 50%, 30%
end: 50%, 80%
- waitForAnimationToEnd:
timeout: 8000
# 3. Aggressive Retry Loop: Search, if not found, refresh and try again.
# This handles slow backend indexing by periodically pulling fresh data.
- repeat:
times: 5
commands:
- runFlow:
when:
notVisible:
id: "shft_card_logo_placeholder"
index: 0
commands:
# 1. Clear current search
- tapOn:
id: "find_shifts_search_input"
- waitForAnimationToEnd:
timeout: 2000
- inputText: "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b" # Strategic clear if "" fails
- inputText: ""
- tapOn: "Shifts" # Dismiss keyboard
# 2. Pull-to-refresh
- swipe:
start: 50%, 60%
end: 50%, 90%
- waitForAnimationToEnd:
timeout: 15000
# 3. Search again
- tapOn:
id: "find_shifts_search_input"
- waitForAnimationToEnd:
timeout: 2000
- inputText: "${TEST_ORDER_NAME}\n"
- tapOn: "Shifts" # Dismiss keyboard
- waitForAnimationToEnd:
timeout: 5000
# Final check with a long timeout
- extendedWaitUntil:
visible:
id: "shft_card_logo_placeholder"
index: 0
timeout: 30000
- assertVisible:
id: "shft_card_logo_placeholder"
index: 0

View File

@@ -0,0 +1,118 @@
# Client App — E2E: Comprehensive Order Lifecycle (Create -> Verify in List -> View Details)
# Purpose:
# - Generates a unique order name via JS.
# - Creates a One-Time order.
# - Verifies the success confirmation.
# - Navigates to the Orders list.
# - Uses scrollUntilVisible to find the unique order.
# - Taps the order and verifies details on the summary page.
#
# Run:
# maestro test \
11: # apps/mobile/apps/client/maestro/auth/happy_path/sign_in.yaml \
12: # apps/mobile/apps/client/maestro/orders/happy_path/comprehensive_order_lifecycle.yaml \
13: # -e TEST_CLIENT_EMAIL=... \
14: # -e TEST_CLIENT_PASSWORD=...
appId: com.krowwithus.client
---
- launchApp:
clearState: false
# 1. Generate unique order name
- runScript: ../../scripts/generate_order_name.js
- tapOn: "Home"
- extendedWaitUntil:
visible: "Welcome back"
timeout: 15000
# 2. Start Create Order Flow
- tapOn: "Create Order\\nSchedule shifts"
- extendedWaitUntil:
visible: "One-Time\\nSingle Event or Shift Request"
timeout: 10000
- tapOn: "One-Time\\nSingle Event or Shift Request"
- extendedWaitUntil:
visible: "One-Time Order"
timeout: 10000
# 3. Fill Order Details
- tapOn: ".*(ORDER NAME|EVENT NAME|Order[ Nn]ame|Event[ Nn]ame).*"
- inputText: ${output.orderName}
- hideKeyboard
# Select Role
- tapOn: ".*(Select Role|SELECT ROLE).*"
- tapOn: ".*\\$.*" # Just tap the first role from the dropdown
# Set Start Time (Required for valid form)
- scrollUntilVisible:
element: "--:--"
direction: DOWN
timeout: 5000
optional: true
- tapOn: "--:--"
- extendedWaitUntil:
visible: "(?i)ok"
timeout: 5000
- tapOn: "(?i)ok"
# Set End Time (Required for valid form)
- tapOn: "--:--"
- extendedWaitUntil:
visible: "(?i)ok"
timeout: 5000
- tapOn: "(?i)ok"
# Scroll if needed to see the Create Order button
- scrollUntilVisible:
element: "(?i)Create Order"
direction: DOWN
timeout: 5000
- tapOn: "(?i)Create Order"
# 4. Verify Success Confirmation
- extendedWaitUntil:
visible: "(?i)Order Created.*"
timeout: 30000
- assertVisible: "(?i)Order Created.*"
- assertVisible: ${output.orderName}
# 5. Navigate to Orders List and Verify Persistence
- tapOn: "Back to Orders"
- extendedWaitUntil:
visible: "Orders"
timeout: 15000
# Select the "Up Next" or "Active" tab if not already selected (assuming default is correct)
- tapOn: "Up Next"
# Scroll until we find our unique order name
- scrollUntilVisible:
element: ${output.orderName}
direction: DOWN
timeout: 20000
- assertVisible: ${output.orderName}
# 6. View Details and Final Verification
- tapOn: ${output.orderName}
- extendedWaitUntil:
visible: "Order Summary"
timeout: 10000
- assertVisible: ${output.orderName}
- assertVisible: "POSTED" # or whichever status it initialises to
- assertVisible: "Role" # Check if role label is visible
# Optionally, go back to Home
- tapOn: "Home"

View File

@@ -33,24 +33,54 @@ appId: com.krowwithus.client
visible: "One-Time Order"
timeout: 10000
# Event Name (ORDER NAME)
- tapOn: "ORDER NAME"
# Wait for form or empty state data to load from API
- extendedWaitUntil:
visible: ".*(Create your order|No Vendors Available).*"
timeout: 15000
- assertNotVisible: "No Vendors Available"
- tapOn: ".*(ORDER NAME|EVENT NAME|Order[ Nn]ame|Event[ Nn]ame).*"
- inputText: "Test E2E Event"
- hideKeyboard
# Wait for Vendor and Hub to auto-populate the defaults from the API.
# We just need to give it a second.
- extendedWaitUntil:
visible: "Select Role"
visible: ".*(Select Role|SELECT ROLE).*"
timeout: 10000
# Select Role (Required for valid form)
- tapOn: "Select Role"
- tapOn:
point: "50%,50%" # Just tap the first role from the dropdown
- tapOn: ".*(Select Role|SELECT ROLE).*"
- tapOn: ".*\\$.*" # Tap the first role from the dropdown
# Set Start Time (Required for valid form)
- scrollUntilVisible:
element: "--:--"
direction: DOWN
timeout: 5000
optional: true
- tapOn: "--:--"
- extendedWaitUntil:
visible: "(?i)ok"
timeout: 5000
- tapOn: "(?i)ok"
# Set End Time (Required for valid form)
- tapOn: "--:--"
- extendedWaitUntil:
visible: "(?i)ok"
timeout: 5000
- tapOn: "(?i)ok"
- scrollUntilVisible:
element: "(?i)Create Order"
direction: DOWN
timeout: 5000
optional: true
# Wait for Create Order button to be enabled (isValid=true handles this by making it clickable)
- tapOn: "Create Order"
- tapOn: "(?i)Create Order"
# Success screen shows "Order received." or similar success title/message
- extendedWaitUntil:

View File

@@ -0,0 +1,180 @@
# Multi-App E2E: Client Creates Order -> Staff Sees Shift
# Purpose:
# - Historically verifies that an order created in the Client app
# is instantly visible as a shift in the Staff app.
#
# Run:
# maestro test cross_app_order_verification.yaml \
11: # -e TEST_CLIENT_EMAIL=... \
12: # -e TEST_CLIENT_PASSWORD=... \
13: # -e TEST_STAFF_PHONE=... \
14: # -e TEST_STAFF_OTP=...
appId: com.krowwithus.client
---
# --- PHASE 1: CLIENT CREATES ORDER ---
- launchApp:
clearState: false
- runScript: ../../scripts/generate_order_name.js
- tapOn:
text: "(?i)Home"
optional: true
- extendedWaitUntil:
visible: "(?i)Welcome back"
timeout: 30000
- tapOn: "Create Order\nSchedule shifts"
- tapOn: "One-Time\nSingle Event or Shift Request"
- extendedWaitUntil:
visible: "One-Time Order"
timeout: 10000
# 3. Fill Order Details
- tapOn: ".*(ORDER NAME|EVENT NAME|Order[ Nn]ame|Event[ Nn]ame).*"
- inputText: ${output.orderName}
- hideKeyboard
# Select Role
- tapOn: ".*(Select Role|SELECT ROLE).*"
- tapOn: ".*\\$.*"
# Set Start Time (Required for valid form)
- scrollUntilVisible:
element: "--:--"
direction: DOWN
timeout: 5000
optional: true
- tapOn: "--:--"
- extendedWaitUntil:
visible: "(?i)ok"
timeout: 5000
- tapOn: "(?i)ok"
# Set End Time (Required for valid form)
- tapOn: "--:--"
- extendedWaitUntil:
visible: "(?i)ok"
timeout: 5000
- tapOn: "(?i)ok"
- scrollUntilVisible:
element: "(?i)Create Order"
direction: DOWN
- tapOn: "(?i)Create Order"
# 4. Verify Success Confirmation
- extendedWaitUntil:
visible: "(?i)Order Created.*"
timeout: 30000
- stopApp: com.krowwithus.client
# --- PHASE 2: STAFF VERIFIES SHIFT ---
- appId: com.krowwithus.staff
- launchApp:
clearState: false
# If not logged in, we'd need a sign_in flow here, but we assume state is kept
# or we run this after a staff sign-in test.
# For a standalone test, we should include the login steps.
# (Optional login steps if app starts at landing)
- runFlow:
when:
visible: "Join the Workforce"
file: ../../../../staff/maestro/auth/happy_path/sign_in.yaml
env:
PHONE: ${TEST_STAFF_PHONE}
OTP: ${TEST_STAFF_OTP}
- extendedWaitUntil:
visible: "Shifts"
timeout: 20000
- launchApp:
appId: com.krowwithus.staff
clearState: false
# Wait for Staff app to fully render AND give backend time to index the new order
- waitForAnimationToEnd:
timeout: 30000
# 1. Navigate to Shifts tab
- tapOn:
id: "nav_shifts"
# 2. Refresh #1: Swipe to force backend reload, THEN search
- swipe:
start: 50%, 30%
end: 50%, 80%
- waitForAnimationToEnd:
timeout: 8000
# Wait for the search field to appear (visible when Find Shifts tab is active)
- extendedWaitUntil:
visible: "(?i)Search jobs.*"
timeout: 15000
# 3. Aggressive Retry Loop: Search, if not found, refresh and try again.
- repeat:
times: 5
commands:
- runFlow:
when:
notVisible:
id: "shft_card_logo_placeholder"
index: 0
commands:
# 1. Clear current search
- tapOn:
id: "find_shifts_search_input"
- waitForAnimationToEnd:
timeout: 2000
- inputText: "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
- inputText: ""
- tapOn: "Shifts" # Dismiss keyboard
# 2. Pull-to-refresh
- swipe:
start: 50%, 60%
end: 50%, 90%
- waitForAnimationToEnd:
timeout: 15000
# 3. Enter search query again
- tapOn:
id: "find_shifts_search_input"
- waitForAnimationToEnd:
timeout: 2000
- inputText: "${output.orderName}\n"
- tapOn: "Shifts" # Dismiss keyboard
- waitForAnimationToEnd:
timeout: 5000
# 4. Final wait — plenty of time for slow backends
- extendedWaitUntil:
visible:
id: "shft_card_logo_placeholder"
index: 0
timeout: 60000
- assertVisible:
id: "shft_card_logo_placeholder"
index: 0
- tapOn:
id: "shft_card_logo_placeholder"
index: 0
# Wait for Details page with a very generous timeout for slow backend syncing
- extendedWaitUntil:
visible: "LOCATION"
timeout: 45000
- assertVisible: "${output.orderName}"
- assertVisible: "(?i)(Apply|Accept|Clock|Confirm).*"

View File

@@ -0,0 +1,272 @@
# Multi-App E2E: Full Order-to-Payment Lifecycle
# Roles: Client, Staff
# Flow: Create -> Apply -> Clock In (Geofence) -> Clock Out -> Approve Payment
appId: com.krowwithus.client # Starts with Client profile
---
# === PERSONA: CLIENT ===
- launchApp:
clearState: false
# 1. Generate unique identifier for this session
- runScript: ../../scripts/generate_order_name.js
# 2. Create the Order
- waitForAnimationToEnd:
timeout: 10000
- tapOn:
text: "(?i)Home"
optional: true
- extendedWaitUntil:
visible: "(?i)Welcome back"
timeout: 30000
- tapOn: "Create Order\\nSchedule shifts"
- extendedWaitUntil:
visible: "One-Time\\nSingle Event or Shift Request"
timeout: 10000
- tapOn: "One-Time\\nSingle Event or Shift Request"
- extendedWaitUntil:
visible: "One-Time Order"
timeout: 15000
- extendedWaitUntil:
visible: ".*(Create your order|No Vendors Available).*"
timeout: 15000
# Safety check: ensure the account has vendors setup!
- assertNotVisible: "No Vendors Available"
- tapOn: ".*(ORDER NAME|EVENT NAME|Order[ Nn]ame|Event[ Nn]ame).*"
- inputText: "${output.orderName}"
- hideKeyboard
# Select a Hub (assuming first one is auto-selected or we tap)
- tapOn:
text: ".*(HUB|Hub).*"
optional: true
# Wait for Vendor and Hub to auto-populate the defaults from the API.
- extendedWaitUntil:
visible: ".*(Select Role|SELECT ROLE).*"
timeout: 10000
# Select Role (Required for valid form)
- tapOn: ".*(Select Role|SELECT ROLE).*"
- tapOn: ".*\\$.*" # Tap the first role from the dropdown (matches 'Role - $Cost')
# Set Start Time (Required for valid form)
- scrollUntilVisible:
element: "--:--"
direction: DOWN
timeout: 5000
optional: true
- tapOn: "--:--"
- extendedWaitUntil:
visible: "(?i)ok"
timeout: 5000
- tapOn: "(?i)ok"
# Set End Time (Required for valid form)
- tapOn: "--:--"
- extendedWaitUntil:
visible: "(?i)ok"
timeout: 5000
- tapOn: "(?i)ok"
- scrollUntilVisible:
element: "(?i)Create Order"
direction: DOWN
timeout: 5000
optional: true
- tapOn: "(?i)Create Order"
- extendedWaitUntil:
visible: "(?i)Order Created.*"
timeout: 30000
# Cool-down to let backend index the new order
- waitForAnimationToEnd:
timeout: 35000
- stopApp
# === PERSONA: STAFF ===
- launchApp:
appId: com.krowwithus.staff
clearState: false
# Wait for Staff app to fully render (also serves as additional backend indexing time)
- waitForAnimationToEnd:
timeout: 15000
# Sign in if needed
- runFlow:
when:
visible: "Log In"
file: ../../../../staff/maestro/auth/happy_path/sign_in.yaml
env:
TEST_STAFF_PHONE: ${TEST_STAFF_PHONE}
TEST_STAFF_OTP: ${TEST_STAFF_OTP}
# 1. Navigate to Shifts tab
- tapOn:
id: "nav_shifts"
# 2. Refresh #1: Swipe to force backend reload, THEN search
- swipe:
start: 50%, 30%
end: 50%, 80%
- waitForAnimationToEnd:
timeout: 8000
# Wait for the search field to appear (visible when Find Shifts tab is active)
- extendedWaitUntil:
visible: "(?i)Search jobs.*"
timeout: 15000
# 3. Aggressive Retry Loop: Search, if not found, refresh and try again.
- repeat:
times: 5
commands:
- runFlow:
when:
notVisible:
id: "shft_card_logo_placeholder"
index: 0 # Index 0 for Logo (ID is unique to cards)
commands:
# 1. Clear current search
- tapOn:
id: "find_shifts_search_input"
- waitForAnimationToEnd:
timeout: 2000
- inputText: "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
- inputText: ""
- tapOn: "Shifts" # Dismiss keyboard
# 2. Pull-to-refresh
- swipe:
start: 50%, 60%
end: 50%, 90%
- waitForAnimationToEnd:
timeout: 15000
# 3. Enter search query again
- tapOn:
id: "find_shifts_search_input"
- waitForAnimationToEnd:
timeout: 2000
- inputText: "${output.orderName}\n"
- tapOn: "Shifts" # Dismiss keyboard
- waitForAnimationToEnd:
timeout: 5000
# 4. Final wait — plenty of time for slow backends
- extendedWaitUntil:
visible:
id: "shft_card_logo_placeholder"
index: 0
timeout: 60000
- tapOn:
id: "shft_card_logo_placeholder"
index: 0
# Wait for Details page with a very generous timeout for slow backend syncing
- extendedWaitUntil:
visible: "LOCATION"
timeout: 45000
# 2. Book the shift (Instant Book flow)
- extendedWaitUntil:
visible: "(?i)(Apply|Accept|Clock|Confirm).*"
timeout: 20000
- tapOn: "(?i)(Apply|Accept|Clock|Confirm).*"
# Handle confirmation dialog if it appears
- runFlow:
when:
visible: "(?i)Book Shift"
file: ../../../../staff/maestro/shifts/confirm_booking_dialog.yaml
- extendedWaitUntil:
visible: "Shift successfully booked!"
timeout: 20000
- extendedWaitUntil:
visible: "(?i)Welcome back"
timeout: 20000
# Tap Home tab to reset navigation context
- tapOn:
id: "nav_home"
# Tap Clock In tab
- tapOn:
id: "nav_clock_in"
# Wait for Clock In screen to load
- extendedWaitUntil:
visible: "(?i)Clock In to your Shift"
timeout: 15000
# Set location to the Venue (NYC Grand Hotel as per ClockInCubit)
- setLocation:
latitude: 40.7128
longitude: -74.0060
- extendedWaitUntil:
visible: "Swipe to Check In"
timeout: 15000
- swipe:
direction: RIGHT
element: "Swipe to Check In"
- extendedWaitUntil:
visible: "Check In!"
timeout: 15000
# 4. Clock Out (immediately for test purposes)
- swipe:
direction: RIGHT
element: "Swipe to Check Out"
- extendedWaitUntil:
visible: "Check Out!" # Assuming similar success UI
timeout: 10000
optional: true
- stopApp
# === PERSONA: CLIENT ===
- launchApp:
appId: com.krowwithus.client
clearState: false
# 1. Verify and Approve Billing
- tapOn: "Billing"
- tapOn: "Awaiting Approval"
# Find the specific approved shift's related invoice
# For the test, we assume the top one is our most recent completion
- extendedWaitUntil:
visible: "Review & Approve"
timeout: 10000
- tapOn: "Review & Approve"
- tapOn: "Approve"
- extendedWaitUntil:
visible: "Invoice approved"
timeout: 15000
# Test complete - invoice approved successfully
- assertVisible: "Invoice approved"

View File

@@ -0,0 +1,53 @@
# Client App — E2E: Reorder Flow
# Purpose:
# - Navigates Home → Reorder Section.
# - Taps "Reorder" on a recent shift.
# - Verifies the One-Time Order form is correctly populated.
# - Submits the reordered shift.
appId: com.krowwithus.client
---
- launchApp:
clearState: false
- tapOn: "Home"
# 1. Generate a new name for the reorder to avoid confusion
- runScript: ../../scripts/generate_order_name.js
# 2. Find the "Reorder" section (translates to "Recently Completed" or similar)
# We search for the Reorder button text
- scrollUntilVisible:
element: "REORDER"
direction: DOWN
timeout: 10000
- tapOn: "REORDER"
# 3. Verification: We should be on the One-Time Order creation page
- extendedWaitUntil:
visible: "One-Time Order"
timeout: 10000
# 4. Verification: Some fields should be pre-filled (e.g. Hub)
# We can't easily check exact pre-fill values without knowing the history,
# but we can verify we are in the flow and the form is loaded.
- extendedWaitUntil:
visible: ".*(Create your order|No Vendors Available).*"
timeout: 15000
- assertNotVisible: "No Vendors Available"
- assertVisible: ".*(ORDER NAME|EVENT NAME|Order[ Nn]ame|Event[ Nn]ame).*"
# 5. Overwrite name and Submit
- tapOn: ".*(ORDER NAME|EVENT NAME|Order[ Nn]ame|Event[ Nn]ame).*"
- inputText: "${output.orderName}"
- hideKeyboard
- tapOn: "(?i)Create Order"
# 6. Success check
- extendedWaitUntil:
visible: "(?i)Order Created.*"
timeout: 15000

View File

@@ -0,0 +1,62 @@
# Client App — E2E: Reports Insights Verification
# Purpose:
# - Navigates to Reports.
# - Opens Spend Report.
# - Verifies that charts and summary cards are loaded.
# - Opens No-Show Report.
# - Verifies the empty/data states are handled.
appId: com.krowwithus.client
---
- launchApp:
clearState: false
- tapOn: "Home"
- tapOn: "Reports"
# 1. Verify Spend Report
- extendedWaitUntil:
visible: "Spend Report"
timeout: 10000
- tapOn: "Spend Report"
# Verify specialized summary cards are visible
- extendedWaitUntil:
visible: "TOTAL SPEND"
timeout: 10000
- assertVisible: "AVG DAILY COST"
# Swipe to see historical chart content if needed
- swipe:
direction: DOWN
element: "TOTAL SPEND"
- assertVisible: "SPEND BY INDUSTRY"
# Go back
- tapOn:
id: "back_button" # Or the arrow icon
optional: true
- tapOn:
point: "8% 8%" # Fallback to top-left if ID missing
optional: true
# 2. Verify No-Show Report
- extendedWaitUntil:
visible: "No-Show Report"
timeout: 10000
- tapOn: "No-Show Report"
- extendedWaitUntil:
visible: "(?i).*(No-Show|Report).*"
timeout: 10000
# Smoke check for data availability
- assertVisible:
text: "No show incidents"
optional: true
- tapOn: "Home"

View File

@@ -0,0 +1 @@
output.orderName = "E2E-" + Math.random().toString(36).substring(2, 7).toUpperCase();

View File

@@ -75,6 +75,11 @@ public final class GeneratedPluginRegistrant {
} catch (Exception e) {
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
}
try {
flutterEngine.getPlugins().add(new fman.ge.smart_auth.SmartAuthPlugin());
} catch (Exception e) {
Log.e(TAG, "Error registering plugin smart_auth, fman.ge.smart_auth.SmartAuthPlugin", e);
}
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
} catch (Exception e) {

View File

@@ -66,6 +66,12 @@
@import shared_preferences_foundation;
#endif
#if __has_include(<smart_auth/SmartAuthPlugin.h>)
#import <smart_auth/SmartAuthPlugin.h>
#else
@import smart_auth;
#endif
#if __has_include(<url_launcher_ios/URLLauncherPlugin.h>)
#import <url_launcher_ios/URLLauncherPlugin.h>
#else
@@ -85,6 +91,7 @@
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
[RecordIosPlugin registerWithRegistrar:[registry registrarForPlugin:@"RecordIosPlugin"]];
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
[SmartAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"SmartAuthPlugin"]];
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
}

View File

@@ -8,6 +8,7 @@
#include <file_selector_linux/file_selector_plugin.h>
#include <record_linux/record_linux_plugin.h>
#include <smart_auth/smart_auth_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
@@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) record_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
record_linux_plugin_register_with_registrar(record_linux_registrar);
g_autoptr(FlPluginRegistrar) smart_auth_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "SmartAuthPlugin");
smart_auth_plugin_register_with_registrar(smart_auth_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
record_linux
smart_auth
url_launcher_linux
)

View File

@@ -13,6 +13,7 @@ import firebase_core
import geolocator_apple
import record_macos
import shared_preferences_foundation
import smart_auth
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@@ -24,5 +25,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SmartAuthPlugin.register(with: registry.registrar(forPlugin: "SmartAuthPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@@ -0,0 +1,44 @@
# Staff App — E2E: Profile Update Verification
# Purpose:
# - Navigates to Profile -> Personal Info.
# - Updates the phone number.
# - Saves and verifies success snackbar.
# - Re-enters page to verify PERSISTENCE.
appId: com.krowwithus.staff
---
- launchApp:
clearState: false
- tapOn: "Profile"
- tapOn: "Personal Information"
# 1. Update Phone
- extendedWaitUntil:
visible: "PHONE NUMBER"
timeout: 10000
- tapOn: "PHONE NUMBER"
# Clear and input new dummy number
- inputText: "5550199"
- hideKeyboard
- tapOn: "Save"
# 2. Verify success
- extendedWaitUntil:
visible: "Personal details updated"
timeout: 10000
# 3. Verify PERSISTENCE (The "Really" Test)
# We stop and restart to ensure it's not just local state
- stopApp
- launchApp
- tapOn: "Profile"
- tapOn: "Personal Information"
- extendedWaitUntil:
visible: "5550199"
timeout: 10000
- assertVisible: "5550199"

View File

@@ -0,0 +1,4 @@
appId: com.krowwithus.staff
---
# Helper flow to confirm a booking dialog if it appears
- tapOn: "(?i)(Apply Now|Confirm|OK)"

View File

@@ -0,0 +1,59 @@
# Staff App — E2E: Geofence-Aware Clock-In
# Purpose:
# - Verifies that Clock-In is BLOCKED when staff is $>500m$ from venue.
# - Verifies that Clock-In is ENABLED when staff is within geofence.
# - Note: Uses Grand Hotel, NYC (40.7128, -74.0060) from ClockInCubit.
appId: com.krowwithus.staff
---
- launchApp:
clearState: false
- tapOn: "Home"
- tapOn: "Clock In"
# 1. TEST: OUT OF RANGE (London)
- setLocation:
latitude: 51.5074
longitude: -0.1278
- extendedWaitUntil:
visible: "Clock In to your Shift"
timeout: 10000
# Tap search/refresh logic if needed, but usually Cubit triggers on entry
- extendedWaitUntil:
visible: ".*away.*must be within 500m.*"
timeout: 15000
- assertVisible: ".*away.*must be within 500m.*"
- assertNotVisible: "Swipe to Check In"
# 2. TEST: IN RANGE (NYC Grand Hotel)
- setLocation:
latitude: 40.7128
longitude: -74.0060
# Give the app a moment to refresh location (or tap a refresh button if implemented)
# Assuming the Cubit polls or we re-enter the page.
- stopApp
- launchApp
- tapOn: "Clock In"
- extendedWaitUntil:
visible: "Swipe to Check In"
timeout: 15000
- assertVisible: "Swipe to Check In"
- assertNotVisible: ".*away.*must be within 500m.*"
# 3. COMPLETE CLOCK IN
- swipe:
direction: RIGHT
element: "Swipe to Check In"
- extendedWaitUntil:
visible: "Check In!"
timeout: 15000
- assertVisible: "Swipe to Check Out"

View File

@@ -12,6 +12,7 @@
#include <geolocator_windows/geolocator_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h>
#include <smart_auth/smart_auth_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -27,6 +28,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
RecordWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
SmartAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SmartAuthPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
geolocator_windows
permission_handler_windows
record_windows
smart_auth
url_launcher_windows
)

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
// 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
import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:intl/intl.dart';
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
@@ -109,11 +109,13 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
endTime: endTime,
);
final String title = sr.role.name;
mappedShifts.add(
Shift(
id: sr.shiftId,
roleId: sr.roleId,
title: sr.role.name,
title: title,
clientName: sr.shift.order.business.businessName,
logoUrl: null,
hourlyRate: sr.role.costPerHour,
@@ -213,10 +215,12 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
status = _mapApplicationStatus(s);
}
final String title = sr.role.name;
return Shift(
id: sr.shiftId,
roleId: sr.roleId,
title: sr.shift.order.business.businessName,
title: title,
clientName: sr.shift.order.business.businessName,
logoUrl: sr.shift.order.business.companyLogoUrl,
hourlyRate: sr.role.costPerHour,

View File

@@ -255,6 +255,7 @@ class _ShiftOrderFormSheetState extends State<ShiftOrderFormSheet> {
final fdc.OperationResult<dc.CreateShiftData, dc.CreateShiftVariables>
shiftResult = await _dataConnect
.createShift(title: shiftTitle, orderId: orderId)
.description(_orderNameController.text)
.date(orderTimestamp)
.location(selectedHub.hubName)
.locationAddress(selectedHub.address)

View File

@@ -46,6 +46,7 @@ class _OneTimeOrderEventNameInputState
@override
Widget build(BuildContext context) {
return UiTextField(
semanticsIdentifier: 'order_name_input',
label: widget.label,
controller: _controller,
onChanged: widget.onChanged,

View File

@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:core_localization/core_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinput/pinput.dart';
import 'package:smart_auth/smart_auth.dart';
import '../../../blocs/auth_event.dart';
import '../../../blocs/auth_bloc.dart';
@@ -29,144 +31,98 @@ class OtpInputField extends StatefulWidget {
}
class _OtpInputFieldState extends State<OtpInputField> {
final List<TextEditingController> _controllers = List<TextEditingController>.generate(
6,
(int _) => TextEditingController(),
);
final List<FocusNode> _focusNodes = List<FocusNode>.generate(6, (int _) => FocusNode());
/// Hidden field for E2E: Maestro inputText sends full OTP in one call;
/// the 6 visible boxes have maxLength:1 and would truncate.
late final FocusNode _hiddenFocusNode;
late final TextEditingController _controller;
late final SmartAuth _smartAuth;
@override
void initState() {
super.initState();
_hiddenFocusNode = FocusNode();
_controller = TextEditingController();
_smartAuth = SmartAuth();
_listenForSmsCode();
}
@override
void dispose() {
_hiddenFocusNode.dispose();
for (final TextEditingController controller in _controllers) {
controller.dispose();
}
for (final FocusNode node in _focusNodes) {
node.dispose();
}
_controller.dispose();
super.dispose();
}
/// Distributes full OTP from hidden field to the 6 visible boxes and notifies Bloc.
void _syncFromHidden(BuildContext context, String value) {
final String raw = value.replaceAll(RegExp(r'\D'), '');
final String digits = raw.length > 6 ? raw.substring(0, 6) : raw;
for (int i = 0; i < 6; i++) {
final String digit = i < digits.length ? digits[i] : '';
if (_controllers[i].text != digit) {
_controllers[i].text = digit;
}
}
BlocProvider.of<AuthBloc>(context).add(AuthOtpUpdated(digits));
if (digits.length == 6) {
widget.onCompleted(digits);
Future<void> _listenForSmsCode() async {
final res = await _smartAuth.getSmsCode();
if (res.code != null && mounted) {
_controller.text = res.code!;
_onChanged(_controller.text);
}
}
/// Helper getter to compute the current OTP code from all controllers.
String get _otpCode => _controllers.map((TextEditingController c) => c.text).join();
/// Handles changes to the OTP input fields.
void _onChanged({
required BuildContext context,
required int index,
required String value,
}) {
if (value.length == 1 && index < 5) {
_focusNodes[index + 1].requestFocus();
}
void _onChanged(String value) {
// Notify the Bloc of the change
BlocProvider.of<AuthBloc>(context).add(AuthOtpUpdated(_otpCode));
BlocProvider.of<AuthBloc>(context).add(AuthOtpUpdated(value));
if (_otpCode.length == 6) {
widget.onCompleted(_otpCode);
if (value.length == 6) {
widget.onCompleted(value);
}
}
@override
Widget build(BuildContext context) {
const double boxWidth = 45;
const double boxHeight = 56;
final defaultPinTheme = PinTheme(
width: 45,
height: 56,
textStyle: UiTypography.headline3m,
decoration: BoxDecoration(
borderRadius: UiConstants.radiusMd,
border: Border.all(
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.border,
width: 2,
),
),
);
final focusedPinTheme = defaultPinTheme.copyWith(
decoration: defaultPinTheme.decoration!.copyWith(
border: Border.all(
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary,
width: 2,
),
),
);
final submittedPinTheme = defaultPinTheme.copyWith(
decoration: defaultPinTheme.decoration!.copyWith(
border: Border.all(
color: widget.error.isNotEmpty ? UiColors.textError : UiColors.primary,
width: 2,
),
),
);
final errorPinTheme = defaultPinTheme.copyWith(
decoration: defaultPinTheme.decoration!.copyWith(
border: Border.all(color: UiColors.textError, width: 2),
),
);
return Column(
children: <Widget>[
SizedBox(
width: 300,
height: boxHeight,
child: Stack(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List<Widget>.generate(6, (int index) {
final TextField field = TextField(
controller: _controllers[index],
focusNode: _focusNodes[index],
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
maxLength: 1,
style: UiTypography.headline3m,
decoration: InputDecoration(
counterText: '',
border: OutlineInputBorder(
borderSide: BorderSide(
color: widget.error.isNotEmpty
? UiColors.textError
: (_controllers[index].text.isNotEmpty
? UiColors.primary
: UiColors.border),
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: UiConstants.radiusMd,
borderSide: BorderSide(
color: widget.error.isNotEmpty
? UiColors.textError
: (_controllers[index].text.isNotEmpty
? UiColors.primary
: UiColors.border),
width: 2,
),
),
),
onChanged: (String value) =>
_onChanged(context: context, index: index, value: value),
);
return SizedBox(width: boxWidth, height: boxHeight, child: field);
}),
),
Positioned.fill(
child: Semantics(
identifier: 'staff_otp_input',
container: false,
child: Opacity(
opacity: 0.01,
child: TextField(
focusNode: _hiddenFocusNode,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(6),
],
maxLength: 6,
onChanged: (String value) =>
_syncFromHidden(context, value),
),
),
),
),
],
child: Semantics(
identifier: 'staff_otp_input',
child: Pinput(
length: 6,
controller: _controller,
defaultPinTheme: defaultPinTheme,
focusedPinTheme: focusedPinTheme,
submittedPinTheme: submittedPinTheme,
errorPinTheme: errorPinTheme,
followingPinTheme: defaultPinTheme,
forceErrorState: widget.error.isNotEmpty,
onChanged: _onChanged,
onCompleted: widget.onCompleted,
autofillHints: const <String>[AutofillHints.oneTimeCode],
),
),
),
if (widget.error.isNotEmpty)

View File

@@ -18,6 +18,8 @@ dependencies:
firebase_auth: ^6.1.2
firebase_data_connect: ^0.2.2+1
http: ^1.2.0
pinput: ^5.0.0
smart_auth: ^1.1.0
# Architecture Packages
krow_domain:

View File

@@ -277,85 +277,89 @@ class _ShiftsPageState extends State<ShiftsPage> {
}) {
final isActive = _activeTab == type;
return Expanded(
child: GestureDetector(
onTap: !enabled
? null
: () {
setState(() => _activeTab = type);
if (type == ShiftTabType.history) {
_bloc.add(LoadHistoryShiftsEvent());
}
if (type == ShiftTabType.find) {
_bloc.add(LoadAvailableShiftsEvent());
}
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: UiConstants.space2,
horizontal: UiConstants.space2,
),
decoration: BoxDecoration(
color: isActive
? UiColors.white
: UiColors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 14,
color: !enabled
? UiColors.white.withValues(alpha: 0.5)
: isActive
? UiColors.primary
: UiColors.white,
),
const SizedBox(width: UiConstants.space1),
Flexible(
child: Text(
label,
style:
(isActive
? UiTypography.body3m.copyWith(
color: UiColors.primary,
)
: UiTypography.body3m.white)
.copyWith(
color: !enabled
? UiColors.white.withValues(alpha: 0.5)
: null,
),
overflow: TextOverflow.ellipsis,
child: Semantics(
identifier: 'shift_tab_${type.name}',
label: label,
child: GestureDetector(
onTap: !enabled
? null
: () {
setState(() => _activeTab = type);
if (type == ShiftTabType.history) {
_bloc.add(LoadHistoryShiftsEvent());
}
if (type == ShiftTabType.find) {
_bloc.add(LoadAvailableShiftsEvent());
}
},
child: Container(
padding: const EdgeInsets.symmetric(
vertical: UiConstants.space2,
horizontal: UiConstants.space2,
),
decoration: BoxDecoration(
color: isActive
? UiColors.white
: UiColors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 14,
color: !enabled
? UiColors.white.withValues(alpha: 0.5)
: isActive
? UiColors.primary
: UiColors.white,
),
),
if (showCount) ...[
const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space1,
vertical: 2,
const SizedBox(width: UiConstants.space1),
Flexible(
child: Text(
label,
style:
(isActive
? UiTypography.body3m.copyWith(
color: UiColors.primary,
)
: UiTypography.body3m.white)
.copyWith(
color: !enabled
? UiColors.white.withValues(alpha: 0.5)
: null,
),
overflow: TextOverflow.ellipsis,
),
constraints: const BoxConstraints(minWidth: 18),
decoration: BoxDecoration(
color: isActive
? UiColors.primary.withValues(alpha: 0.1)
: UiColors.white.withValues(alpha: 0.2),
borderRadius: UiConstants.radiusFull,
),
child: Center(
child: Text(
"$count",
style: UiTypography.footnote1b.copyWith(
color: isActive ? UiColors.primary : UiColors.white,
),
if (showCount) ...[
const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space1,
vertical: 2,
),
constraints: const BoxConstraints(minWidth: 18),
decoration: BoxDecoration(
color: isActive
? UiColors.primary.withValues(alpha: 0.1)
: UiColors.white.withValues(alpha: 0.2),
borderRadius: UiConstants.radiusFull,
),
child: Center(
child: Text(
"$count",
style: UiTypography.footnote1b.copyWith(
color: isActive ? UiColors.primary : UiColors.white,
),
),
),
),
),
],
],
],
),
),
),
),

View File

@@ -246,8 +246,10 @@ class _MyShiftCardState extends State<MyShiftCard> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo
Container(
width: 44,
Semantics(
identifier: 'shft_card_logo_placeholder',
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
@@ -282,6 +284,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
size: UiConstants.iconMd,
),
),
),
),
const SizedBox(width: UiConstants.space3),

View File

@@ -33,6 +33,7 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
String _jobType = 'all';
double? _maxDistance; // miles
Position? _currentPosition;
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
@@ -40,6 +41,12 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
_initLocation();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _initLocation() async {
try {
final LocationPermission permission = await Geolocator.checkPermission();
@@ -289,6 +296,7 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
final matchesSearch =
s.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
s.location.toLowerCase().contains(_searchQuery.toLowerCase()) ||
(s.description ?? '').toLowerCase().contains(_searchQuery.toLowerCase()) ||
s.clientName.toLowerCase().contains(_searchQuery.toLowerCase());
if (!matchesSearch) return false;
@@ -371,17 +379,21 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
),
const SizedBox(width: UiConstants.space2),
Expanded(
child: TextField(
onChanged: (v) =>
setState(() => _searchQuery = v),
decoration: InputDecoration(
border: InputBorder.none,
hintText: context
.t
.staff_shifts
.find_shifts
.search_hint,
hintStyle: UiTypography.body2r.textPlaceholder,
child: Semantics(
identifier: 'find_shifts_search_input',
child: TextField(
controller: _searchController,
onChanged: (v) =>
setState(() => _searchQuery = v),
decoration: InputDecoration(
border: InputBorder.none,
hintText: context
.t
.staff_shifts
.find_shifts
.search_hint,
hintStyle: UiTypography.body2r.textPlaceholder,
),
),
),
),

View File

@@ -123,26 +123,30 @@ class StaffMainBottomBar extends StatelessWidget {
final bool isSelected = currentIndex == item.index;
return Expanded(
child: GestureDetector(
onTap: () => onTap(item.index),
behavior: HitTestBehavior.opaque,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Icon(
item.icon,
color: isSelected ? activeColor : inactiveColor,
size: UiConstants.iconLg,
),
const SizedBox(height: UiConstants.space1),
Text(
item.label,
style: UiTypography.footnote2m.copyWith(
child: Semantics(
identifier: 'nav_${item.tabKey}',
label: item.label,
child: GestureDetector(
onTap: () => onTap(item.index),
behavior: HitTestBehavior.opaque,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Icon(
item.icon,
color: isSelected ? activeColor : inactiveColor,
size: UiConstants.iconLg,
),
),
],
const SizedBox(height: UiConstants.space1),
Text(
item.label,
style: UiTypography.footnote2m.copyWith(
color: isSelected ? activeColor : inactiveColor,
),
),
],
),
),
),
);

View File

@@ -1133,6 +1133,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
pinput:
dependency: transitive
description:
name: pinput
sha256: c41f42ee301505ae2375ec32871c985d3717bf8aee845620465b286e0140aad2
url: "https://pub.dev"
source: hosted
version: "5.0.2"
platform:
dependency: transitive
description:
@@ -1434,6 +1442,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.12.0"
smart_auth:
dependency: transitive
description:
name: smart_auth
sha256: a25229b38c02f733d0a4e98d941b42bed91a976cb589e934895e60ccfa674cf6
url: "https://pub.dev"
source: hosted
version: "1.1.1"
source_map_stack_trace:
dependency: transitive
description:
@@ -1544,6 +1560,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
universal_platform:
dependency: transitive
description:
name: universal_platform
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
url_launcher:
dependency: transitive
description: