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"
|
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:
|
||||||
|
|||||||
@@ -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) {
|
} 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) {
|
||||||
|
|||||||
@@ -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"]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 <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"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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 {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user