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" visible: "One-Time Order"
timeout: 10000 timeout: 10000
# Event Name (ORDER NAME) # Wait for form or empty state data to load from API
- tapOn: "ORDER NAME" - 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" - inputText: "Test E2E Event"
- hideKeyboard - hideKeyboard
# Wait for Vendor and Hub to auto-populate the defaults from the API. # Wait for Vendor and Hub to auto-populate the defaults from the API.
# We just need to give it a second. # We just need to give it a second.
- extendedWaitUntil: - extendedWaitUntil:
visible: "Select Role" visible: ".*(Select Role|SELECT ROLE).*"
timeout: 10000 timeout: 10000
# Select Role (Required for valid form) # Select Role (Required for valid form)
- tapOn: "Select Role" - tapOn: ".*(Select Role|SELECT ROLE).*"
- tapOn: - tapOn: ".*\\$.*" # Tap the first role from the dropdown
point: "50%,50%" # 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"
- 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) # 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 # Success screen shows "Order received." or similar success title/message
- extendedWaitUntil: - 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) { } catch (Exception e) {
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", 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 { try {
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin()); flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
} catch (Exception e) { } catch (Exception e) {

View File

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

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ import firebase_core
import geolocator_apple import geolocator_apple
import record_macos import record_macos
import shared_preferences_foundation import shared_preferences_foundation
import smart_auth
import url_launcher_macos import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
@@ -24,5 +25,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SmartAuthPlugin.register(with: registry.registrar(forPlugin: "SmartAuthPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 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 <geolocator_windows/geolocator_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.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> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -27,6 +28,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
RecordWindowsPluginCApiRegisterWithRegistrar( RecordWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
SmartAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SmartAuthPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
import 'package:flutter_bloc/flutter_bloc.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_event.dart';
import '../../../blocs/auth_bloc.dart'; import '../../../blocs/auth_bloc.dart';
@@ -29,146 +31,100 @@ class OtpInputField extends StatefulWidget {
} }
class _OtpInputFieldState extends State<OtpInputField> { class _OtpInputFieldState extends State<OtpInputField> {
final List<TextEditingController> _controllers = List<TextEditingController>.generate( late final TextEditingController _controller;
6, late final SmartAuth _smartAuth;
(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;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_hiddenFocusNode = FocusNode(); _controller = TextEditingController();
_smartAuth = SmartAuth();
_listenForSmsCode();
} }
@override @override
void dispose() { void dispose() {
_hiddenFocusNode.dispose(); _controller.dispose();
for (final TextEditingController controller in _controllers) {
controller.dispose();
}
for (final FocusNode node in _focusNodes) {
node.dispose();
}
super.dispose(); super.dispose();
} }
/// Distributes full OTP from hidden field to the 6 visible boxes and notifies Bloc. Future<void> _listenForSmsCode() async {
void _syncFromHidden(BuildContext context, String value) { final res = await _smartAuth.getSmsCode();
final String raw = value.replaceAll(RegExp(r'\D'), ''); if (res.code != null && mounted) {
final String digits = raw.length > 6 ? raw.substring(0, 6) : raw; _controller.text = res.code!;
for (int i = 0; i < 6; i++) { _onChanged(_controller.text);
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);
} }
} }
/// Helper getter to compute the current OTP code from all controllers. void _onChanged(String value) {
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();
}
// Notify the Bloc of the change // Notify the Bloc of the change
BlocProvider.of<AuthBloc>(context).add(AuthOtpUpdated(_otpCode)); BlocProvider.of<AuthBloc>(context).add(AuthOtpUpdated(value));
if (_otpCode.length == 6) { if (value.length == 6) {
widget.onCompleted(_otpCode); widget.onCompleted(value);
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const double boxWidth = 45; final defaultPinTheme = PinTheme(
const double boxHeight = 56; 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( return Column(
children: <Widget>[ children: <Widget>[
SizedBox( SizedBox(
width: 300, 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( child: Semantics(
identifier: 'staff_otp_input', identifier: 'staff_otp_input',
container: false, child: Pinput(
child: Opacity( length: 6,
opacity: 0.01, controller: _controller,
child: TextField( defaultPinTheme: defaultPinTheme,
focusNode: _hiddenFocusNode, focusedPinTheme: focusedPinTheme,
keyboardType: TextInputType.number, submittedPinTheme: submittedPinTheme,
inputFormatters: <TextInputFormatter>[ errorPinTheme: errorPinTheme,
FilteringTextInputFormatter.digitsOnly, followingPinTheme: defaultPinTheme,
LengthLimitingTextInputFormatter(6), forceErrorState: widget.error.isNotEmpty,
], onChanged: _onChanged,
maxLength: 6, onCompleted: widget.onCompleted,
onChanged: (String value) => autofillHints: const <String>[AutofillHints.oneTimeCode],
_syncFromHidden(context, value),
), ),
), ),
), ),
),
],
),
),
if (widget.error.isNotEmpty) if (widget.error.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.only(top: UiConstants.space4), padding: const EdgeInsets.only(top: UiConstants.space4),

View File

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

View File

@@ -277,6 +277,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
}) { }) {
final isActive = _activeTab == type; final isActive = _activeTab == type;
return Expanded( return Expanded(
child: Semantics(
identifier: 'shift_tab_${type.name}',
label: label,
child: GestureDetector( child: GestureDetector(
onTap: !enabled onTap: !enabled
? null ? null
@@ -359,6 +362,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
), ),
), ),
), ),
),
); );
} }
} }

View File

@@ -246,7 +246,9 @@ class _MyShiftCardState extends State<MyShiftCard> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Logo // Logo
Container( Semantics(
identifier: 'shft_card_logo_placeholder',
child: Container(
width: 44, width: 44,
height: 44, height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -283,6 +285,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
), ),
), ),
), ),
),
const SizedBox(width: UiConstants.space3), const SizedBox(width: UiConstants.space3),
// Consensed Details // Consensed Details

View File

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

View File

@@ -123,6 +123,9 @@ class StaffMainBottomBar extends StatelessWidget {
final bool isSelected = currentIndex == item.index; final bool isSelected = currentIndex == item.index;
return Expanded( return Expanded(
child: Semantics(
identifier: 'nav_${item.tabKey}',
label: item.label,
child: GestureDetector( child: GestureDetector(
onTap: () => onTap(item.index), onTap: () => onTap(item.index),
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
@@ -145,6 +148,7 @@ class StaffMainBottomBar extends StatelessWidget {
], ],
), ),
), ),
),
); );
} }
} }

View File

@@ -1133,6 +1133,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
pinput:
dependency: transitive
description:
name: pinput
sha256: c41f42ee301505ae2375ec32871c985d3717bf8aee845620465b286e0140aad2
url: "https://pub.dev"
source: hosted
version: "5.0.2"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@@ -1434,6 +1442,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.12.0" 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: source_map_stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -1544,6 +1560,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" 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: url_launcher:
dependency: transitive dependency: transitive
description: description: