comprehensive cases
This commit is contained in:
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
@@ -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).*"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
output.orderName = "E2E-" + Math.random().toString(36).substring(2, 7).toUpperCase();
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"]];
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
record_linux
|
||||
smart_auth
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1,4 @@
|
||||
appId: com.krowwithus.staff
|
||||
---
|
||||
# Helper flow to confirm a booking dialog if it appears
|
||||
- tapOn: "(?i)(Apply Now|Confirm|OK)"
|
||||
@@ -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"
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
geolocator_windows
|
||||
permission_handler_windows
|
||||
record_windows
|
||||
smart_auth
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,146 +31,100 @@ 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: 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)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: UiConstants.space4),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -277,6 +277,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
}) {
|
||||
final isActive = _activeTab == type;
|
||||
return Expanded(
|
||||
child: Semantics(
|
||||
identifier: 'shift_tab_${type.name}',
|
||||
label: label,
|
||||
child: GestureDetector(
|
||||
onTap: !enabled
|
||||
? null
|
||||
@@ -359,6 +362,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +246,9 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Logo
|
||||
Container(
|
||||
Semantics(
|
||||
identifier: 'shft_card_logo_placeholder',
|
||||
child: Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
@@ -283,6 +285,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
|
||||
// Consensed Details
|
||||
|
||||
@@ -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,7 +379,10 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
),
|
||||
const SizedBox(width: UiConstants.space2),
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
identifier: 'find_shifts_search_input',
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: (v) =>
|
||||
setState(() => _searchQuery = v),
|
||||
decoration: InputDecoration(
|
||||
@@ -385,6 +396,7 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -123,6 +123,9 @@ class StaffMainBottomBar extends StatelessWidget {
|
||||
|
||||
final bool isSelected = currentIndex == item.index;
|
||||
return Expanded(
|
||||
child: Semantics(
|
||||
identifier: 'nav_${item.tabKey}',
|
||||
label: item.label,
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(item.index),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -145,6 +148,7 @@ class StaffMainBottomBar extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user