1611 lines
61 KiB
HTML
1611 lines
61 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Payfrit Works iOS — Test Walkthrough</title>
|
|
<style>
|
|
:root {
|
|
--bg: #1a1a2e;
|
|
--surface: #16213e;
|
|
--card: #0f3460;
|
|
--accent: #22b14c;
|
|
--accent-dim: #1a8a3a;
|
|
--text: #e0e0e0;
|
|
--text-dim: #8899aa;
|
|
--pass: #22b14c;
|
|
--fail: #e74c3c;
|
|
--skip: #f39c12;
|
|
--border: #2a3a5e;
|
|
--input-bg: #0d1b3e;
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
line-height: 1.6;
|
|
padding: 0 0 80px 0;
|
|
}
|
|
header {
|
|
background: linear-gradient(135deg, var(--surface), var(--card));
|
|
padding: 24px 32px;
|
|
border-bottom: 2px solid var(--accent);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
header h1 { font-size: 1.4rem; color: var(--accent); margin-bottom: 4px; }
|
|
header .meta { font-size: 0.8rem; color: var(--text-dim); }
|
|
.progress-bar-container {
|
|
margin-top: 12px;
|
|
background: var(--input-bg);
|
|
border-radius: 8px;
|
|
height: 24px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
.progress-bar-fill {
|
|
height: 100%;
|
|
border-radius: 8px;
|
|
transition: width 0.4s ease;
|
|
background: linear-gradient(90deg, var(--pass), var(--accent));
|
|
}
|
|
.progress-bar-text {
|
|
position: absolute;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 0.75rem; font-weight: 600; color: #fff;
|
|
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
|
}
|
|
.stats {
|
|
display: flex; gap: 16px; margin-top: 8px; font-size: 0.8rem;
|
|
}
|
|
.stats span { padding: 2px 8px; border-radius: 4px; font-weight: 600; }
|
|
.stat-pass { background: rgba(34,177,76,0.2); color: var(--pass); }
|
|
.stat-fail { background: rgba(231,76,60,0.2); color: var(--fail); }
|
|
.stat-skip { background: rgba(243,156,18,0.2); color: var(--skip); }
|
|
.stat-pending { background: rgba(136,153,170,0.15); color: var(--text-dim); }
|
|
|
|
.controls {
|
|
display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap;
|
|
}
|
|
.controls button {
|
|
padding: 6px 14px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
color: var(--text);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
transition: background 0.2s;
|
|
}
|
|
.controls button:hover { background: var(--card); }
|
|
.controls button.danger { border-color: var(--fail); color: var(--fail); }
|
|
.controls button.danger:hover { background: rgba(231,76,60,0.15); }
|
|
|
|
main { max-width: 960px; margin: 0 auto; padding: 24px 16px; }
|
|
|
|
.section-group {
|
|
margin-bottom: 32px;
|
|
}
|
|
.section-title {
|
|
font-size: 1.15rem;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
margin-bottom: 12px;
|
|
padding-bottom: 6px;
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex; align-items: center; gap: 8px;
|
|
}
|
|
.section-title .badge {
|
|
font-size: 0.7rem;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
background: var(--card);
|
|
color: var(--text-dim);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.step {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
margin-bottom: 10px;
|
|
overflow: hidden;
|
|
transition: border-color 0.3s;
|
|
}
|
|
.step.status-pass { border-left: 4px solid var(--pass); }
|
|
.step.status-fail { border-left: 4px solid var(--fail); }
|
|
.step.status-skip { border-left: 4px solid var(--skip); }
|
|
|
|
.step-header {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
gap: 12px;
|
|
}
|
|
.step-header:hover { background: rgba(255,255,255,0.03); }
|
|
|
|
.step-id {
|
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
color: var(--accent);
|
|
background: rgba(34,177,76,0.12);
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
min-width: 40px;
|
|
text-align: center;
|
|
}
|
|
.step-title { flex: 1; font-weight: 600; font-size: 0.9rem; }
|
|
.step-status-badge {
|
|
font-size: 0.7rem;
|
|
padding: 2px 10px;
|
|
border-radius: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
.badge-pass { background: rgba(34,177,76,0.2); color: var(--pass); }
|
|
.badge-fail { background: rgba(231,76,60,0.2); color: var(--fail); }
|
|
.badge-skip { background: rgba(243,156,18,0.2); color: var(--skip); }
|
|
.badge-pending { background: rgba(136,153,170,0.1); color: var(--text-dim); }
|
|
|
|
.step-expand { color: var(--text-dim); transition: transform 0.2s; }
|
|
.step.open .step-expand { transform: rotate(180deg); }
|
|
|
|
.step-body {
|
|
display: none;
|
|
padding: 0 16px 16px 16px;
|
|
}
|
|
.step.open .step-body { display: block; }
|
|
|
|
.step-field { margin-bottom: 10px; }
|
|
.step-field-label {
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
margin-bottom: 3px;
|
|
}
|
|
.step-field-value {
|
|
font-size: 0.85rem;
|
|
color: var(--text);
|
|
padding: 8px 12px;
|
|
background: var(--input-bg);
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.step-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.btn {
|
|
padding: 8px 20px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-weight: 600;
|
|
font-size: 0.8rem;
|
|
cursor: pointer;
|
|
transition: filter 0.2s, transform 0.1s;
|
|
}
|
|
.btn:active { transform: scale(0.97); }
|
|
.btn-pass { background: var(--pass); color: #fff; }
|
|
.btn-pass:hover { filter: brightness(1.15); }
|
|
.btn-fail { background: var(--fail); color: #fff; }
|
|
.btn-fail:hover { filter: brightness(1.15); }
|
|
.btn-skip { background: var(--skip); color: #fff; }
|
|
.btn-skip:hover { filter: brightness(1.15); }
|
|
|
|
.notes-field {
|
|
width: 100%;
|
|
min-height: 60px;
|
|
background: var(--input-bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
color: var(--text);
|
|
padding: 8px 12px;
|
|
font-family: inherit;
|
|
font-size: 0.8rem;
|
|
resize: vertical;
|
|
margin-top: 8px;
|
|
}
|
|
.notes-field:focus { outline: none; border-color: var(--accent); }
|
|
.notes-field::placeholder { color: var(--text-dim); }
|
|
|
|
.chevron { font-size: 0.7rem; }
|
|
|
|
@media (max-width: 600px) {
|
|
header { padding: 16px; }
|
|
main { padding: 12px 8px; }
|
|
.step-header { padding: 10px 12px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<h1>Payfrit Works iOS — Test Walkthrough</h1>
|
|
<div class="meta">
|
|
App: <strong>com.payfrit.works</strong> |
|
|
Version: <strong>1.0 (1)</strong> |
|
|
Environment: <strong>Development (dev.payfrit.com)</strong> |
|
|
Generated: <strong id="genDate"></strong>
|
|
</div>
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar-fill" id="progressFill" style="width:0%"></div>
|
|
<div class="progress-bar-text" id="progressText">0 / 0</div>
|
|
</div>
|
|
<div class="stats" id="stats"></div>
|
|
<div class="controls">
|
|
<button onclick="expandAll()">Expand All</button>
|
|
<button onclick="collapseAll()">Collapse All</button>
|
|
<button onclick="jumpToNext()">Jump to Next ↓</button>
|
|
<button onclick="exportResults()">Export Results (.txt)</button>
|
|
<button class="danger" onclick="resetRun()">Reset Run</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main id="main"></main>
|
|
|
|
<script>
|
|
const APP_NAME = 'Payfrit Works iOS';
|
|
const APP_VERSION = '1.0 (1)';
|
|
const APP_ENV = 'Development (dev.payfrit.com)';
|
|
const STORAGE_KEY = 'payfrit_works_ios_test_walkthrough';
|
|
|
|
document.getElementById('genDate').textContent = new Date().toISOString().split('T')[0];
|
|
|
|
const sections = [
|
|
{
|
|
title: '🚀 Install & First Launch',
|
|
steps: [
|
|
{
|
|
id: 'I01',
|
|
title: 'Install app on device/simulator',
|
|
preconditions: 'Xcode project builds successfully. Simulator or device available.',
|
|
action: 'Build and run PayfritWorks scheme on iPhone 16 Pro simulator (or real device).',
|
|
expected: 'App installs. App icon appears on home screen with Payfrit branding.',
|
|
onFail: 'Screenshot build error or crash log from Xcode console.'
|
|
},
|
|
{
|
|
id: 'I02',
|
|
title: 'First launch — splash screen',
|
|
preconditions: 'Fresh install, no saved auth credentials.',
|
|
action: 'Tap app icon to launch.',
|
|
expected: 'Splash screen shows Payfrit logo (PayfritLogoLight) centered on white background with a green progress spinner. Transitions to Login screen within 1-3 seconds.',
|
|
onFail: 'Screenshot of splash screen or crash. Check RootView.swift loadingView.'
|
|
},
|
|
{
|
|
id: 'I03',
|
|
title: 'DEV badge visible',
|
|
preconditions: 'App is configured for .development environment.',
|
|
action: 'Observe bottom-left corner of the screen after launch.',
|
|
expected: 'Orange "DEV" badge is visible, rotated 45°, in bottom-left corner. This confirms environment is development.',
|
|
onFail: 'Screenshot. Check RootView.swift isDev and APIService.swift environment property.'
|
|
},
|
|
{
|
|
id: 'I04',
|
|
title: 'Environment confirmation (API base URL)',
|
|
preconditions: 'App launched.',
|
|
action: 'Login and open Debug API from the menu. Verify the API base URL shown.',
|
|
expected: 'Debug panel shows base URL: https://dev.payfrit.com/api',
|
|
onFail: 'Screenshot of debug panel. Verify APIService.swift environment = .development.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '🔐 Login',
|
|
steps: [
|
|
{
|
|
id: 'L01',
|
|
title: 'Login screen layout',
|
|
preconditions: 'Not authenticated (fresh install or logged out).',
|
|
action: 'Observe the login screen.',
|
|
expected: 'Shows: Payfrit logo, "Payfrit Works" title, "Sign in to view and claim tasks" subtitle, red "DEV MODE — password: 123456" hint, email/phone field, password field with eye toggle, green "Sign In" button.',
|
|
onFail: 'Screenshot. Compare with LoginScreen.swift body.'
|
|
},
|
|
{
|
|
id: 'L02',
|
|
title: 'Login — empty fields validation',
|
|
preconditions: 'Login screen visible, both fields empty.',
|
|
action: 'Tap "Sign In" with both fields empty.',
|
|
expected: 'Red error banner appears: "Please enter username and password". Login does NOT call API.',
|
|
onFail: 'Screenshot of error or lack thereof.'
|
|
},
|
|
{
|
|
id: 'L03',
|
|
title: 'Login — invalid credentials',
|
|
preconditions: 'Login screen visible.',
|
|
action: 'Enter a valid email/phone but wrong password. Tap "Sign In".',
|
|
expected: 'Loading spinner shows on button. After API response, red error banner: "Invalid email/phone or password".',
|
|
onFail: 'Screenshot. Check network tab or console for API response.'
|
|
},
|
|
{
|
|
id: 'L04',
|
|
title: 'Login — valid credentials',
|
|
preconditions: 'Login screen visible. Test account: use DEV password 123456.',
|
|
action: 'Enter valid email/phone + password 123456. Tap "Sign In".',
|
|
expected: 'Loading spinner, then transition to Business Selection screen. No error shown.',
|
|
onFail: 'Screenshot. Check console for PAYFRIT logs. Verify API returns OK: true with Token.'
|
|
},
|
|
{
|
|
id: 'L05',
|
|
title: 'Login — password visibility toggle',
|
|
preconditions: 'Login screen visible, password typed.',
|
|
action: 'Type a password, tap the eye icon to toggle visibility.',
|
|
expected: 'Password toggles between SecureField (dots) and plain TextField. Eye icon changes between eye.fill and eye.slash.fill.',
|
|
onFail: 'Screenshot of both states.'
|
|
},
|
|
{
|
|
id: 'L06',
|
|
title: 'Login — keyboard submit',
|
|
preconditions: 'Both fields filled with valid credentials.',
|
|
action: 'Press Return/Go on the keyboard from the password field.',
|
|
expected: 'Login is triggered (same as tapping Sign In). Uses .onSubmit handler.',
|
|
onFail: 'Note whether keyboard submit triggers login or does nothing.'
|
|
},
|
|
{
|
|
id: 'L07',
|
|
title: 'Auth token stored in Keychain',
|
|
preconditions: 'Successful login.',
|
|
action: 'After login, force-quit the app and relaunch.',
|
|
expected: 'App does NOT show login screen — goes directly to biometric check or Business Selection (simulator skips biometrics).',
|
|
onFail: 'Screenshot. Check AuthStorage.swift saveAuth and loadAuth methods.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '🔒 Biometrics',
|
|
steps: [
|
|
{
|
|
id: 'B01',
|
|
title: 'Biometric prompt on relaunch (real device)',
|
|
preconditions: 'Logged in previously. Face ID / Touch ID enrolled. Real device only.',
|
|
action: 'Force-quit and relaunch the app.',
|
|
expected: 'Face ID / Touch ID prompt appears: "Sign in to Payfrit Works". Cancel button says "Use Password".',
|
|
onFail: 'Screenshot. Check RootView.swift checkAuthWithBiometrics.'
|
|
},
|
|
{
|
|
id: 'B02',
|
|
title: 'Biometric success',
|
|
preconditions: 'Biometric prompt shown.',
|
|
action: 'Authenticate with Face ID / Touch ID.',
|
|
expected: 'Auth loaded from Keychain, transitions to Business Selection.',
|
|
onFail: 'Note if stuck on splash or redirects to login.'
|
|
},
|
|
{
|
|
id: 'B03',
|
|
title: 'Biometric cancel — "Use Password"',
|
|
preconditions: 'Biometric prompt shown.',
|
|
action: 'Tap "Use Password" (cancel) on the biometric dialog.',
|
|
expected: 'Login screen appears. User must enter credentials again.',
|
|
onFail: 'Screenshot. Note if app crashes or shows blank screen.'
|
|
},
|
|
{
|
|
id: 'B04',
|
|
title: 'Simulator skips biometrics',
|
|
preconditions: 'Running on iOS Simulator.',
|
|
action: 'Force-quit and relaunch on simulator.',
|
|
expected: 'Biometric prompt is NOT shown (skipped via #if targetEnvironment(simulator)). Goes straight to Business Selection if previously logged in.',
|
|
onFail: 'Note if biometric prompt appears on simulator.'
|
|
},
|
|
{
|
|
id: 'B05',
|
|
title: 'No biometrics enrolled fallback',
|
|
preconditions: 'Real device with no Face ID / Touch ID enrolled.',
|
|
action: 'Relaunch app after previous login.',
|
|
expected: 'App skips biometric prompt entirely (canEvaluatePolicy returns false). Goes straight to Business Selection using saved auth.',
|
|
onFail: 'Note behavior — should NOT crash or show login.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '📋 Permissions',
|
|
steps: [
|
|
{
|
|
id: 'P01',
|
|
title: 'Location permission prompt (happy path)',
|
|
preconditions: 'Beacon scanning triggered (task detail with beacon UUID, Complete button visible).',
|
|
action: 'Accept a task with a beacon UUID. Observe for location permission dialog.',
|
|
expected: 'iOS shows "Allow While Using App" / "Don\'t Allow" prompt. Message: "Payfrit Works uses your location to detect nearby beacons for automatic task completion."',
|
|
onFail: 'Screenshot. Check Info.plist NSLocationWhenInUseUsageDescription.'
|
|
},
|
|
{
|
|
id: 'P02',
|
|
title: 'Location permission denied',
|
|
preconditions: 'Location permission set to "Don\'t Allow" in Settings.',
|
|
action: 'Open a task detail that has a beacon UUID and Complete button.',
|
|
expected: 'Error message appears: "Bluetooth permission is required for auto-complete. Please enable it in Settings." Beacon scanning does not start.',
|
|
onFail: 'Screenshot. Check BeaconScanner.swift onPermissionDenied callback.'
|
|
},
|
|
{
|
|
id: 'P03',
|
|
title: 'Bluetooth permission prompt',
|
|
preconditions: 'First time Bluetooth is accessed.',
|
|
action: 'Trigger beacon scanning.',
|
|
expected: 'iOS shows Bluetooth permission dialog: "Payfrit Works uses Bluetooth to scan for nearby beacons."',
|
|
onFail: 'Screenshot. Check Info.plist NSBluetoothAlwaysUsageDescription.'
|
|
},
|
|
{
|
|
id: 'P04',
|
|
title: 'Bluetooth off indicator',
|
|
preconditions: 'Bluetooth turned off in Control Center.',
|
|
action: 'Open a task detail with beacon + Complete button.',
|
|
expected: 'Beacon indicator shows slashed antenna icon (antenna.radiowaves.left.and.right.slash) in green.',
|
|
onFail: 'Screenshot. Check BeaconScanner onBluetoothOff callback.'
|
|
},
|
|
{
|
|
id: 'P05',
|
|
title: 'Face ID permission prompt',
|
|
preconditions: 'First launch on device with Face ID.',
|
|
action: 'Login and relaunch app.',
|
|
expected: 'iOS shows Face ID permission dialog: "Payfrit Works uses Face ID for quick sign-in."',
|
|
onFail: 'Screenshot. Check Info.plist NSFaceIDUsageDescription.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '🏢 Business Selection',
|
|
steps: [
|
|
{
|
|
id: 'BS01',
|
|
title: 'Business list loads',
|
|
preconditions: 'Logged in, user has businesses.',
|
|
action: 'After login, observe Business Selection screen.',
|
|
expected: 'Nav title "Select Business". List of businesses with: initial letter in colored square, business name, address/city, status badge (Pending/Active/Inactive), task count or green checkmark. Toolbar: refresh button and ellipsis menu.',
|
|
onFail: 'Screenshot. Check API response from /workers/myBusinesses.cfm.'
|
|
},
|
|
{
|
|
id: 'BS02',
|
|
title: 'Business list — empty state',
|
|
preconditions: 'User has no businesses assigned.',
|
|
action: 'Login with a user who has no employments.',
|
|
expected: 'Briefcase icon, "No businesses found", "You are not currently employed at any business".',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'BS03',
|
|
title: 'Business card — pending tasks badge',
|
|
preconditions: 'A business has pending tasks.',
|
|
action: 'Observe the business card.',
|
|
expected: 'Green capsule badge showing task count (e.g. "3") with "tasks" label below.',
|
|
onFail: 'Screenshot. Verify PendingTaskCount from API.'
|
|
},
|
|
{
|
|
id: 'BS04',
|
|
title: 'Business card — no pending tasks',
|
|
preconditions: 'A business has 0 pending tasks.',
|
|
action: 'Observe the business card.',
|
|
expected: 'Green checkmark icon with "clear" label. No count badge.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'BS05',
|
|
title: 'Tap business → Task List',
|
|
preconditions: 'Business list showing businesses.',
|
|
action: 'Tap any business card.',
|
|
expected: 'Navigates to Task List screen. Nav title shows business name. Business ID is set in APIService.',
|
|
onFail: 'Screenshot. Check that TaskListScreen receives businessName prop.'
|
|
},
|
|
{
|
|
id: 'BS06',
|
|
title: 'Pull to refresh',
|
|
preconditions: 'Business list visible.',
|
|
action: 'Pull down on the list.',
|
|
expected: 'Refresh indicator appears. List reloads from API. Changes in task counts are reflected.',
|
|
onFail: 'Note if refresh fails silently.'
|
|
},
|
|
{
|
|
id: 'BS07',
|
|
title: 'Auto-refresh every 2 seconds',
|
|
preconditions: 'Business list visible.',
|
|
action: 'Watch the list for 10 seconds while another worker accepts tasks in another session.',
|
|
expected: 'PendingTaskCount updates automatically without user action (silent polling every 2s).',
|
|
onFail: 'Note if counts go stale. Check refreshTimer in BusinessSelectionScreen.'
|
|
},
|
|
{
|
|
id: 'BS08',
|
|
title: 'Toolbar menu — My Tasks',
|
|
preconditions: 'Business Selection screen.',
|
|
action: 'Tap ellipsis menu → "My Tasks".',
|
|
expected: 'Navigates to My Tasks screen.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'BS09',
|
|
title: 'Toolbar menu — Account',
|
|
preconditions: 'Business Selection screen.',
|
|
action: 'Tap ellipsis menu → "Account".',
|
|
expected: 'Navigates to Account screen.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'BS10',
|
|
title: 'Toolbar menu — Debug API',
|
|
preconditions: 'Business Selection screen.',
|
|
action: 'Tap ellipsis menu → "Debug API".',
|
|
expected: 'Modal sheet appears showing raw JSON from /workers/myBusinesses.cfm and /tasks/listPending.cfm. Monospaced text, "Done" button to dismiss.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'BS11',
|
|
title: 'Toolbar menu — Logout',
|
|
preconditions: 'Business Selection screen.',
|
|
action: 'Tap ellipsis menu → Logout (red text).',
|
|
expected: 'Auth cleared (Keychain + UserDefaults). Returns to Login screen. Relaunching app also shows Login.',
|
|
onFail: 'Screenshot. Verify AuthStorage.clearAuth is called.'
|
|
},
|
|
{
|
|
id: 'BS12',
|
|
title: 'FAB — My Tasks floating button',
|
|
preconditions: 'Business Selection screen.',
|
|
action: 'Tap the green checkmark FAB button in bottom-right corner.',
|
|
expected: 'Navigates to My Tasks screen.',
|
|
onFail: 'Screenshot. Check that FAB is not covered by list items.'
|
|
},
|
|
{
|
|
id: 'BS13',
|
|
title: 'Manual refresh button',
|
|
preconditions: 'Business Selection screen.',
|
|
action: 'Tap the arrow.clockwise button in the toolbar.',
|
|
expected: 'Business list reloads. Loading spinner appears briefly.',
|
|
onFail: 'Screenshot.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '📝 Task List (Pending Tasks)',
|
|
steps: [
|
|
{
|
|
id: 'TL01',
|
|
title: 'Task list loads',
|
|
preconditions: 'Selected a business with pending tasks.',
|
|
action: 'Observe the task list screen.',
|
|
expected: 'Nav title = business name. List of task cards with: color bar, category icon (chat = bubble icon), title, location (mappin or bicycle icon), category badge, chat badge if applicable, time ago label.',
|
|
onFail: 'Screenshot. Check /tasks/listPending.cfm response.'
|
|
},
|
|
{
|
|
id: 'TL02',
|
|
title: 'Task list — empty state',
|
|
preconditions: 'Business has no pending tasks.',
|
|
action: 'Open task list for a business with 0 tasks.',
|
|
expected: 'Checkmark circle icon, "No pending tasks", "Check back soon!".',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TL03',
|
|
title: 'Task card — regular task',
|
|
preconditions: 'Task list has a non-chat task.',
|
|
action: 'Observe a regular task card.',
|
|
expected: 'Left color bar matches category color. Document icon. Title, location, category badge, time ago.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TL04',
|
|
title: 'Task card — chat task',
|
|
preconditions: 'Task list has a chat task (TaskTypeID = 2).',
|
|
action: 'Observe the chat task card.',
|
|
expected: 'Green bubble icon instead of document. Extra "Chat" badge in green. Arrow icon instead of chevron on right.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TL05',
|
|
title: 'Tap task → Task Detail',
|
|
preconditions: 'Task list with tasks.',
|
|
action: 'Tap any task card.',
|
|
expected: 'Navigates to Task Detail screen with Accept button visible (showAcceptButton: true).',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TL06',
|
|
title: 'Pull to refresh',
|
|
preconditions: 'Task list visible.',
|
|
action: 'Pull down to refresh.',
|
|
expected: 'List reloads. New tasks appear, completed/accepted tasks disappear.',
|
|
onFail: 'Note behavior.'
|
|
},
|
|
{
|
|
id: 'TL07',
|
|
title: 'Auto-refresh every 2 seconds',
|
|
preconditions: 'Task list visible.',
|
|
action: 'Wait 10 seconds. Add a task from another session.',
|
|
expected: 'New task appears automatically without user pull-to-refresh.',
|
|
onFail: 'Note if list is stale.'
|
|
},
|
|
{
|
|
id: 'TL08',
|
|
title: 'Toolbar menu — My Tasks + Logout',
|
|
preconditions: 'Task list screen.',
|
|
action: 'Tap ellipsis menu. Verify "My Tasks" and "Logout" options appear.',
|
|
expected: 'Menu shows worker name at top, My Tasks, Logout (red). Tapping each works correctly.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TL09',
|
|
title: 'FAB — My Tasks button',
|
|
preconditions: 'Task list screen.',
|
|
action: 'Tap green checkmark FAB.',
|
|
expected: 'Navigates to My Tasks.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TL10',
|
|
title: 'Back navigation to Business Selection',
|
|
preconditions: 'Task list screen.',
|
|
action: 'Tap back button or swipe back.',
|
|
expected: 'Returns to Business Selection screen.',
|
|
onFail: 'Note behavior.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '📋 Task Detail',
|
|
steps: [
|
|
{
|
|
id: 'TD01',
|
|
title: 'Task detail loads',
|
|
preconditions: 'Tapped a task from task list.',
|
|
action: 'Observe the task detail screen.',
|
|
expected: 'Loading spinner, then content: customer section (avatar/name/phone), location section (table or delivery), nav bar colored with category color. "Accept Task" button at bottom.',
|
|
onFail: 'Screenshot. Check /tasks/getDetails.cfm response.'
|
|
},
|
|
{
|
|
id: 'TD02',
|
|
title: 'Customer section — with photo',
|
|
preconditions: 'Task has a customer with a photo URL.',
|
|
action: 'Observe customer section.',
|
|
expected: 'AsyncImage loads customer photo in a 64pt circle. Name and phone displayed. Phone call button visible.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD03',
|
|
title: 'Customer section — without photo (initials)',
|
|
preconditions: 'Task has a customer without a photo URL.',
|
|
action: 'Observe customer section.',
|
|
expected: 'Circle shows initials (first letter of first + last name) in category color. "?" if name is empty.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD04',
|
|
title: 'Customer phone call button',
|
|
preconditions: 'Task has a customer with phone number.',
|
|
action: 'Tap the green phone icon.',
|
|
expected: 'Opens tel: URL. On real device, initiates phone call. On simulator, may show error (expected).',
|
|
onFail: 'Note behavior.'
|
|
},
|
|
{
|
|
id: 'TD05',
|
|
title: 'Location section — table service',
|
|
preconditions: 'Task has a ServicePointName (not delivery).',
|
|
action: 'Observe location section.',
|
|
expected: 'Table icon (table.furniture), "Table Service" label, service point name. Green background square.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD06',
|
|
title: 'Location section — delivery',
|
|
preconditions: 'Task has a DeliveryAddress.',
|
|
action: 'Observe location section.',
|
|
expected: 'Bicycle icon, "Delivery" label, delivery address. Maps button (arrow.triangle) visible if lat/lng provided.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD07',
|
|
title: 'Maps navigation button (delivery)',
|
|
preconditions: 'Delivery task with lat/lng.',
|
|
action: 'Tap the maps navigation icon.',
|
|
expected: 'Opens Google Maps with destination set to delivery coordinates.',
|
|
onFail: 'Note behavior.'
|
|
},
|
|
{
|
|
id: 'TD08',
|
|
title: 'Chat button (chat task)',
|
|
preconditions: 'Task is a chat task (TaskTypeID = 2).',
|
|
action: 'Observe the chat button section.',
|
|
expected: '"Chat with Customer" button with bubble icon, customer name subtitle, chevron. Tapping navigates to Chat screen.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD09',
|
|
title: 'Table members section',
|
|
preconditions: 'Task detail has tableMembers array.',
|
|
action: 'Observe table members section.',
|
|
expected: 'Person.3 icon, "Table Members (N)" header. Flow layout of member chips with initials circle and name. Host member highlighted in yellow.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD10',
|
|
title: 'Order items section',
|
|
preconditions: 'Task detail has lineItems.',
|
|
action: 'Observe order items section.',
|
|
expected: 'List icon, "Order Items (N)". Each item: placeholder image, quantity badge (colored), item name, modifiers as sub-items, remark in green italic.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD11',
|
|
title: 'Order remarks section',
|
|
preconditions: 'Task detail has orderRemarks.',
|
|
action: 'Observe remarks section.',
|
|
expected: 'Yellow note icon, "Order Notes" label, remark text. Yellow-tinted background.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD12',
|
|
title: 'Accept Task button',
|
|
preconditions: 'Task viewed from pending list (showAcceptButton = true).',
|
|
action: 'Tap "Accept Task" button.',
|
|
expected: 'Alert: "Accept Task? Claim this task and add it to your tasks?" with Cancel and Accept buttons.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD13',
|
|
title: 'Accept Task — confirm',
|
|
preconditions: 'Accept alert shown.',
|
|
action: 'Tap "Accept".',
|
|
expected: 'API call to /tasks/accept.cfm. On success, screen dismisses back to task list. Task removed from pending list.',
|
|
onFail: 'Screenshot. Check API response.'
|
|
},
|
|
{
|
|
id: 'TD14',
|
|
title: 'Accept Chat Task — navigates to chat',
|
|
preconditions: 'Accepting a chat task.',
|
|
action: 'Accept a chat task (TaskTypeID = 2).',
|
|
expected: 'After accept, navigates to Chat screen instead of dismissing. (showingChat = true)',
|
|
onFail: 'Screenshot. Verify acceptTask() in TaskDetailScreen sets showingChat for chat tasks.'
|
|
},
|
|
{
|
|
id: 'TD15',
|
|
title: 'Accept Task — already accepted error',
|
|
preconditions: 'Another worker already accepted the task.',
|
|
action: 'Try to accept a task that was just accepted by someone else.',
|
|
expected: 'Error: "This task has already been claimed by someone else."',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD16',
|
|
title: 'Complete Task button',
|
|
preconditions: 'Task viewed from My Tasks (showCompleteButton = true).',
|
|
action: 'Tap "Complete Task".',
|
|
expected: 'Alert: "Complete Task? Mark this task as completed?" with Cancel and Complete.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD17',
|
|
title: 'Complete Task — confirm',
|
|
preconditions: 'Complete alert shown.',
|
|
action: 'Tap "Complete".',
|
|
expected: 'API call to /tasks/complete.cfm. On success, dismisses back to My Tasks. Task moves to Completed filter.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD18',
|
|
title: 'Beacon auto-complete — detection + countdown',
|
|
preconditions: 'Task with beacon UUID, Complete button visible, near physical beacon.',
|
|
action: 'Open task detail near a matching beacon.',
|
|
expected: 'Beacon indicator turns green (detected). After 5 RSSI samples, auto-complete countdown sheet appears: timer icon, "Auto-Complete", countdown from 3.',
|
|
onFail: 'Screenshot. Check BeaconScanner RSSI threshold (-75) and sample count (5).'
|
|
},
|
|
{
|
|
id: 'TD19',
|
|
title: 'Beacon auto-complete — cancel countdown',
|
|
preconditions: 'Auto-complete countdown sheet shown.',
|
|
action: 'Tap "Cancel".',
|
|
expected: 'Countdown stops. Sheet dismisses. Beacon scanning resumes. Samples reset.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD20',
|
|
title: 'Beacon auto-complete — countdown completes',
|
|
preconditions: 'Auto-complete countdown running.',
|
|
action: 'Let countdown reach 0.',
|
|
expected: 'Checkmark icon, "Task Completed!", then auto-dismisses after 1 second. Task is completed via API.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD21',
|
|
title: 'Task detail — error and retry',
|
|
preconditions: 'Network error or server error loading details.',
|
|
action: 'Load task detail while offline or with bad TaskID.',
|
|
expected: 'Red exclamation icon, error message, "Retry" button. Tapping Retry reloads.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TD22',
|
|
title: 'FAB — My Tasks button on detail',
|
|
preconditions: 'Task detail screen.',
|
|
action: 'Tap green checkmark FAB.',
|
|
expected: 'Navigates to My Tasks.',
|
|
onFail: 'Screenshot.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '✅ My Tasks',
|
|
steps: [
|
|
{
|
|
id: 'MT01',
|
|
title: 'My Tasks screen layout',
|
|
preconditions: 'Navigated to My Tasks from any entry point.',
|
|
action: 'Observe the screen.',
|
|
expected: 'Green nav bar with "My Tasks" title. 4 filter tabs: Active (play), Today (calendar), This Week (calendar.badge.clock), Done (checkmark). White sliding indicator under active tab. Swipeable tab content.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'MT02',
|
|
title: 'Active filter — shows accepted tasks',
|
|
preconditions: 'Worker has accepted tasks.',
|
|
action: 'Select Active tab.',
|
|
expected: 'Shows tasks with status Accepted/In Progress. Each card shows: color bar, title, location, category badge, time ago, status badge.',
|
|
onFail: 'Screenshot. Verify /tasks/listMine.cfm?FilterType=active response.'
|
|
},
|
|
{
|
|
id: 'MT03',
|
|
title: 'Today filter',
|
|
preconditions: 'Worker has tasks from today.',
|
|
action: 'Tap "Today" tab.',
|
|
expected: 'Shows tasks accepted/completed today.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'MT04',
|
|
title: 'This Week filter',
|
|
preconditions: 'Worker has tasks from this week.',
|
|
action: 'Tap "This Week" tab.',
|
|
expected: 'Shows tasks accepted/completed this week.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'MT05',
|
|
title: 'Done filter — completed tasks',
|
|
preconditions: 'Worker has completed tasks.',
|
|
action: 'Tap "Done" tab.',
|
|
expected: 'Shows completed tasks. Cards have green checkmark, strikethrough title, green color bar. No status badge.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'MT06',
|
|
title: 'Empty state per filter',
|
|
preconditions: 'Filter has no tasks.',
|
|
action: 'Switch to a filter with no tasks.',
|
|
expected: 'Appropriate empty icon and messages. E.g. Active: "No active tasks" / "Claim some tasks to get started!"',
|
|
onFail: 'Screenshot. Verify empty messages match emptyMessage/emptySubMessage functions.'
|
|
},
|
|
{
|
|
id: 'MT07',
|
|
title: 'Tap task → Task Detail with Complete',
|
|
preconditions: 'Active tab with tasks.',
|
|
action: 'Tap an active task.',
|
|
expected: 'Opens Task Detail with "Complete Task" button (showCompleteButton = true). No Accept button.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'MT08',
|
|
title: 'Tap completed task → Task Detail (no buttons)',
|
|
preconditions: 'Done tab with completed tasks.',
|
|
action: 'Tap a completed task.',
|
|
expected: 'Opens Task Detail with NO action buttons (showCompleteButton = false for completed).',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'MT09',
|
|
title: 'Swipe between tabs',
|
|
preconditions: 'My Tasks screen.',
|
|
action: 'Swipe left/right between tabs.',
|
|
expected: 'Tab content swipes. White indicator moves. Selected tab updates. Content loads for new tab.',
|
|
onFail: 'Note if swipe doesn\'t work or indicator doesn\'t move.'
|
|
},
|
|
{
|
|
id: 'MT10',
|
|
title: 'Pull to refresh per tab',
|
|
preconditions: 'Any tab with data.',
|
|
action: 'Pull down to refresh.',
|
|
expected: 'Current tab reloads. Data refreshes.',
|
|
onFail: 'Note behavior.'
|
|
},
|
|
{
|
|
id: 'MT11',
|
|
title: 'Auto-refresh on active tab',
|
|
preconditions: 'My Tasks screen on any tab.',
|
|
action: 'Wait and observe for auto-refresh.',
|
|
expected: 'Current tab auto-refreshes every 2 seconds (silent).',
|
|
onFail: 'Check refreshTimer in MyTasksScreen.'
|
|
},
|
|
{
|
|
id: 'MT12',
|
|
title: 'Chat icon on chat tasks',
|
|
preconditions: 'My Tasks has a chat task.',
|
|
action: 'Observe chat task card.',
|
|
expected: 'Green speech bubble icon on right side of card.',
|
|
onFail: 'Screenshot.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '💬 Chat',
|
|
steps: [
|
|
{
|
|
id: 'CH01',
|
|
title: 'Chat screen — initial load',
|
|
preconditions: 'Navigated to Chat from task detail.',
|
|
action: 'Observe chat screen.',
|
|
expected: 'Nav title "Chat with Customer" (for worker). Loading spinner, then messages or empty state. Toolbar: WiFi indicator (green = WebSocket connected, orange slash = disconnected). Close chat X button.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'CH02',
|
|
title: 'Chat — empty state',
|
|
preconditions: 'Chat with no messages.',
|
|
action: 'Open fresh chat.',
|
|
expected: 'Bubble icon, "No messages yet", "Start the conversation!".',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'CH03',
|
|
title: 'Send a message',
|
|
preconditions: 'Chat screen open, text field visible.',
|
|
action: 'Type a message and tap send (paperplane icon).',
|
|
expected: 'Message appears in blue bubble on right. User initials (from AppState.userName) shown in small circle. Timestamp shown.',
|
|
onFail: 'Screenshot. Verify initials come from appState not hardcoded "Me".'
|
|
},
|
|
{
|
|
id: 'CH04',
|
|
title: 'Receive a message',
|
|
preconditions: 'Chat open, another party sends a message.',
|
|
action: 'Send a message from customer side (or wait for incoming).',
|
|
expected: 'New message appears in gray bubble on left. Sender avatar shows first letter of name. Sender name shown above bubble. Auto-scrolls to bottom.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'CH05',
|
|
title: 'WebSocket connection indicator',
|
|
preconditions: 'Chat screen.',
|
|
action: 'Observe WiFi icon in toolbar.',
|
|
expected: 'Green WiFi = WebSocket connected (real-time). Orange slashed WiFi = disconnected (falls back to 3s polling).',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'CH06',
|
|
title: 'Typing indicator',
|
|
preconditions: 'Chat open with WebSocket.',
|
|
action: 'Other party starts typing.',
|
|
expected: '"[Name] is typing..." appears below message list in italic.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'CH07',
|
|
title: 'Close chat (worker)',
|
|
preconditions: 'Chat screen as worker, chat active.',
|
|
action: 'Tap X button in toolbar.',
|
|
expected: 'Alert: "Close Chat — Are you sure you want to close this chat?" with Cancel and Close (red). Tapping Close: API call, WebSocket close event, "This chat has ended" banner, screen dismisses.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'CH08',
|
|
title: 'Chat ended banner',
|
|
preconditions: 'Chat that has been closed.',
|
|
action: 'Open a closed chat.',
|
|
expected: '"This chat has ended" banner at top (green tint). Input area hidden. Cannot send messages.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'CH09',
|
|
title: 'Error banner on send failure',
|
|
preconditions: 'Chat screen, network offline.',
|
|
action: 'Try to send a message while offline.',
|
|
expected: 'Red error banner appears at top of chat with error message. Tap to dismiss.',
|
|
onFail: 'Screenshot. Verify error banner exists in ChatScreen (added in recent fix).'
|
|
},
|
|
{
|
|
id: 'CH10',
|
|
title: 'HTTP fallback when WebSocket disconnected',
|
|
preconditions: 'WebSocket not connected.',
|
|
action: 'Send a message.',
|
|
expected: 'Message sent via HTTP POST to /chat/sendMessage.cfm. Messages poll via 3-second timer.',
|
|
onFail: 'Note if message fails or gets lost.'
|
|
},
|
|
{
|
|
id: 'CH11',
|
|
title: 'FAB on chat screen',
|
|
preconditions: 'Chat screen.',
|
|
action: 'Tap green checkmark FAB.',
|
|
expected: 'Navigates to My Tasks.',
|
|
onFail: 'Screenshot.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '💰 Account & Payout',
|
|
steps: [
|
|
{
|
|
id: 'AC01',
|
|
title: 'Account screen loads',
|
|
preconditions: 'Navigated to Account from menu.',
|
|
action: 'Observe Account screen.',
|
|
expected: 'Nav title "Account". Loading spinner, then: Payout Status card, Activation card, Earnings card, Logout button. FAB in bottom-right.',
|
|
onFail: 'Screenshot. Check /workers/tierStatus.cfm response.'
|
|
},
|
|
{
|
|
id: 'AC02',
|
|
title: 'Payout Status — Tier 1 unlocked',
|
|
preconditions: 'User has tier >= 1.',
|
|
action: 'Observe Payout Status card.',
|
|
expected: 'Green shield icon, "Payout Status", green checkmark + "Tier 1 unlocked — Payouts enabled".',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'AC03',
|
|
title: 'Payout Status — no Stripe account',
|
|
preconditions: 'User has tier 0, no Stripe account.',
|
|
action: 'Observe Payout Status card.',
|
|
expected: 'Gray shield icon, "Tier 1 is locked", green "Complete payout setup" button.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'AC04',
|
|
title: 'Start Stripe onboarding',
|
|
preconditions: 'No Stripe account.',
|
|
action: 'Tap "Complete payout setup".',
|
|
expected: 'API calls createStripeAccount + getOnboardingLink. Opens Stripe URL in Safari. After return, reloads data after 2 seconds.',
|
|
onFail: 'Screenshot. Note if URL opens or error appears.'
|
|
},
|
|
{
|
|
id: 'AC05',
|
|
title: 'Continue Stripe onboarding (incomplete)',
|
|
preconditions: 'Stripe account exists but setup incomplete.',
|
|
action: 'Tap "Continue payout setup".',
|
|
expected: 'Opens Stripe onboarding URL. After return, reloads data.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'AC06',
|
|
title: 'Activation card — in progress',
|
|
preconditions: 'Activation not yet complete.',
|
|
action: 'Observe Activation card.',
|
|
expected: 'Star icon, "Activation", progress bar (green tint), "$X.XX of $Y.YY completed", "Complete activation now (optional)" button, "What is activation?" link.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'AC07',
|
|
title: '"What is activation?" info button',
|
|
preconditions: 'Activation card visible, not complete.',
|
|
action: 'Tap "What is activation?".',
|
|
expected: 'Alert dialog: "What is Activation?" with explanation text about early earnings and activation cap. "Got it" dismiss button.',
|
|
onFail: 'Screenshot. Verify alert wired up (recently fixed from empty button).'
|
|
},
|
|
{
|
|
id: 'AC08',
|
|
title: 'Early unlock button',
|
|
preconditions: 'Activation not complete.',
|
|
action: 'Tap "Complete activation now (optional)".',
|
|
expected: 'API call to /workers/earlyUnlock.cfm. Opens URL in Safari. Reloads after return.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'AC09',
|
|
title: 'Activation card — complete',
|
|
preconditions: 'Activation is complete.',
|
|
action: 'Observe Activation card.',
|
|
expected: 'Green checkmark icon, "Activation complete" in green. No progress bar or buttons.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'AC10',
|
|
title: 'Earnings card — with entries',
|
|
preconditions: 'User has ledger entries.',
|
|
action: 'Observe Earnings card.',
|
|
expected: 'Dollar icon, "Earnings", total earned amount in green. List of up to 20 entries: Task #ID, date, net amount, status badge (Pending/Charged/Transferred/Reversed).',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'AC11',
|
|
title: 'Earnings card — empty',
|
|
preconditions: 'User has no ledger entries.',
|
|
action: 'Observe Earnings card.',
|
|
expected: '"No earnings yet. Complete tasks to start earning!"',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'AC12',
|
|
title: 'Logout button',
|
|
preconditions: 'Account screen.',
|
|
action: 'Tap red "Logout" button.',
|
|
expected: 'Auth cleared. Returns to Login screen.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'AC13',
|
|
title: 'Pull to refresh',
|
|
preconditions: 'Account screen.',
|
|
action: 'Pull down to refresh.',
|
|
expected: 'Tier status and ledger reload from API.',
|
|
onFail: 'Note behavior.'
|
|
},
|
|
{
|
|
id: 'AC14',
|
|
title: 'Account — error and retry',
|
|
preconditions: 'Network error loading tier status.',
|
|
action: 'Open Account while offline.',
|
|
expected: 'Error view with exclamation icon, error message, "Retry" button.',
|
|
onFail: 'Screenshot.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '🌐 Network Errors & Recovery',
|
|
steps: [
|
|
{
|
|
id: 'NE01',
|
|
title: 'Offline — Business Selection',
|
|
preconditions: 'Device offline (airplane mode).',
|
|
action: 'Open app while offline.',
|
|
expected: 'Error view with message and Retry button. After going back online, Retry loads data.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'NE02',
|
|
title: 'Offline — Task List',
|
|
preconditions: 'On task list, go offline.',
|
|
action: 'Wait for next auto-refresh or pull to refresh.',
|
|
expected: 'Silent refresh failure (no UI change for silent). Manual refresh shows error.',
|
|
onFail: 'Note behavior.'
|
|
},
|
|
{
|
|
id: 'NE03',
|
|
title: 'Offline — Task Detail',
|
|
preconditions: 'Offline.',
|
|
action: 'Try to load task detail.',
|
|
expected: 'Error view: exclamation icon + error message + "Retry" button.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'NE04',
|
|
title: 'Offline — Accept Task',
|
|
preconditions: 'Offline, on task detail with Accept button.',
|
|
action: 'Tap Accept → Accept.',
|
|
expected: 'Error message shown on task detail screen (self.error = error.localizedDescription).',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'NE05',
|
|
title: 'Offline — Complete Task',
|
|
preconditions: 'Offline, on task detail with Complete button.',
|
|
action: 'Tap Complete → Complete.',
|
|
expected: 'Error message shown.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'NE06',
|
|
title: 'Offline — Chat send',
|
|
preconditions: 'Chat screen, offline.',
|
|
action: 'Try to send a message.',
|
|
expected: 'Red error banner at top of chat. Message not sent. Tap banner to dismiss.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'NE07',
|
|
title: 'Server 500 error',
|
|
preconditions: 'Server returning 500.',
|
|
action: 'Trigger any API call.',
|
|
expected: 'Error: "HTTP 500" or similar server error message shown in appropriate error UI.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'NE08',
|
|
title: 'Auth expired (401)',
|
|
preconditions: 'Token expired or invalidated server-side.',
|
|
action: 'Trigger any API call with expired token.',
|
|
expected: 'APIError.unauthorized thrown. Error: "Unauthorized" shown. (Note: app does NOT auto-redirect to login — this is a known limitation).',
|
|
onFail: 'Screenshot. Note actual behavior.'
|
|
},
|
|
{
|
|
id: 'NE09',
|
|
title: 'Slow network — loading states',
|
|
preconditions: 'Throttled network (use Network Link Conditioner).',
|
|
action: 'Navigate between screens.',
|
|
expected: 'All screens show ProgressView() loading spinners while waiting. No blank screens.',
|
|
onFail: 'Screenshot of any blank or broken states.'
|
|
},
|
|
{
|
|
id: 'NE10',
|
|
title: 'Non-JSON response handling',
|
|
preconditions: 'Server returns HTML error page instead of JSON.',
|
|
action: 'Trigger API call that returns non-JSON.',
|
|
expected: 'App tries to extract JSON from mixed response (tryDecodeJSON fallback). If fails, shows "Decoding error: Non-JSON response".',
|
|
onFail: 'Note behavior.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '🔀 Edge Cases',
|
|
steps: [
|
|
{
|
|
id: 'EC01',
|
|
title: 'Empty business name',
|
|
preconditions: 'Business with empty Name field in DB.',
|
|
action: 'Observe business card.',
|
|
expected: 'Initial shows "?". Business name shows as empty string. No crash.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'EC02',
|
|
title: 'Task with no location',
|
|
preconditions: 'Task with no ServicePointName and no DeliveryAddress.',
|
|
action: 'Observe task detail.',
|
|
expected: 'Location section shows "No location specified".',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'EC03',
|
|
title: 'Customer with no name',
|
|
preconditions: 'Task detail where customer has no first/last name.',
|
|
action: 'Observe customer section.',
|
|
expected: 'Name shows "Guest". Initials show "?". No crash.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'EC04',
|
|
title: 'Order with no items',
|
|
preconditions: 'Task with empty LineItems array.',
|
|
action: 'Observe task detail.',
|
|
expected: 'Order Items section is hidden entirely (conditional on !details.mainItems.isEmpty).',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'EC05',
|
|
title: 'Order with modifiers and remarks',
|
|
preconditions: 'Task with line items that have modifiers (child items) and remarks.',
|
|
action: 'Observe order items.',
|
|
expected: 'Modifiers shown as "- ModName: ChildName" in gray. Remarks shown in green italic quotes.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'EC06',
|
|
title: 'Rapid screen navigation',
|
|
preconditions: 'App is running.',
|
|
action: 'Rapidly tap between businesses, tasks, back, My Tasks, Account.',
|
|
expected: 'No crashes. Navigation stack doesn\'t break. No zombie screens.',
|
|
onFail: 'Note crash or broken navigation state.'
|
|
},
|
|
{
|
|
id: 'EC07',
|
|
title: 'Double-tap Accept',
|
|
preconditions: 'Task detail with Accept button.',
|
|
action: 'Tap Accept, then quickly tap Accept again on the alert.',
|
|
expected: 'Only one API call made. No double-accept error.',
|
|
onFail: 'Note behavior.'
|
|
},
|
|
{
|
|
id: 'EC08',
|
|
title: 'Beacon UUID format variants',
|
|
preconditions: 'Task with beacon UUID in various formats (with/without dashes).',
|
|
action: 'Open task detail.',
|
|
expected: 'BeaconScanner.formatUUID normalizes the UUID. Scanning starts correctly regardless of format.',
|
|
onFail: 'Check console for UUID parsing errors.'
|
|
},
|
|
{
|
|
id: 'EC09',
|
|
title: 'CFML date format parsing',
|
|
preconditions: 'Server returns dates in various CFML formats.',
|
|
action: 'Load tasks and task details.',
|
|
expected: 'Dates parse correctly. timeAgo shows correct relative time. No "just now" for old tasks.',
|
|
onFail: 'Note incorrect dates. Check APIService.parseDate formats.'
|
|
},
|
|
{
|
|
id: 'EC10',
|
|
title: 'Mixed-case API keys',
|
|
preconditions: 'Server returns ALL_CAPS, PascalCase, or camelCase keys.',
|
|
action: 'Load any data.',
|
|
expected: 'All data populates correctly. normalizeKeys + multiple key fallbacks in models handle any casing.',
|
|
onFail: 'Note which fields show as empty or default.'
|
|
},
|
|
{
|
|
id: 'EC11',
|
|
title: 'Screen rotation (if supported)',
|
|
preconditions: 'Device with rotation enabled.',
|
|
action: 'Rotate device on each screen.',
|
|
expected: 'Layout adapts. No overlapping elements. ScrollView content remains accessible.',
|
|
onFail: 'Screenshot of broken layout.'
|
|
},
|
|
{
|
|
id: 'EC12',
|
|
title: 'Memory: background/foreground cycle',
|
|
preconditions: 'App running.',
|
|
action: 'Send app to background, wait 30 seconds, return.',
|
|
expected: 'App resumes normally. Timers restart. No stale data or crashes.',
|
|
onFail: 'Note behavior.'
|
|
},
|
|
{
|
|
id: 'EC13',
|
|
title: 'Idle timer disabled during beacon scan',
|
|
preconditions: 'Beacon scanning active (task detail with Complete + beacon UUID).',
|
|
action: 'Leave the screen on during scanning.',
|
|
expected: 'Screen does NOT dim or lock (isIdleTimerDisabled = true). Restores when scanning stops or screen dismissed.',
|
|
onFail: 'Note if screen dims.'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '🧪 Test Accounts & Environment',
|
|
steps: [
|
|
{
|
|
id: 'TA01',
|
|
title: 'Confirm DEV environment',
|
|
preconditions: 'App launched.',
|
|
action: 'Check for DEV badge and Debug API output.',
|
|
expected: 'DEV badge visible. API base URL is https://dev.payfrit.com/api. Photo URLs resolve to dev.payfrit.com.',
|
|
onFail: 'Screenshot.'
|
|
},
|
|
{
|
|
id: 'TA02',
|
|
title: 'Test login — UserID 1',
|
|
preconditions: 'Login screen.',
|
|
action: 'Login with UserID 1 credentials + password 123456.',
|
|
expected: 'Businesses shown: Vegainz LA (44), Apple Review (45), Century Bar & Grill (47). Worker name appears in menus.',
|
|
onFail: 'Note which businesses appear and any missing.'
|
|
},
|
|
{
|
|
id: 'TA03',
|
|
title: 'Test login — UserID 2',
|
|
preconditions: 'Login screen.',
|
|
action: 'Login with UserID 2 credentials.',
|
|
expected: 'Different set of businesses. Verify at least one business appears.',
|
|
onFail: 'Note behavior.'
|
|
},
|
|
{
|
|
id: 'TA04',
|
|
title: 'Seeded data verification — Vegainz LA',
|
|
preconditions: 'Logged in as UserID 1, selected Vegainz LA.',
|
|
action: 'Check pending tasks and task details.',
|
|
expected: 'Tasks appear (if seeded). Task details load with customer info, line items, etc.',
|
|
onFail: 'Note API response from Debug API.'
|
|
},
|
|
{
|
|
id: 'TA05',
|
|
title: 'Photo URL resolution',
|
|
preconditions: 'Task detail with customer photo.',
|
|
action: 'Observe customer photo loads.',
|
|
expected: 'Relative photo URLs (starting with /) are resolved to https://dev.payfrit.com/... Full URLs used as-is.',
|
|
onFail: 'Note if photos fail to load. Check APIService.resolvePhotoUrl.'
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
// ---- State ----
|
|
let state = {};
|
|
try {
|
|
state = JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
|
|
} catch(e) { state = {}; }
|
|
|
|
function saveState() {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
}
|
|
|
|
function getStepState(id) {
|
|
return state[id] || { status: 'pending', notes: '' };
|
|
}
|
|
|
|
function setStepStatus(id, status) {
|
|
if (!state[id]) state[id] = { status: 'pending', notes: '' };
|
|
state[id].status = status;
|
|
saveState();
|
|
renderStep(id);
|
|
updateProgress();
|
|
// Auto-scroll to next pending
|
|
if (status !== 'pending') {
|
|
setTimeout(() => jumpToNext(), 300);
|
|
}
|
|
}
|
|
|
|
function setStepNotes(id, notes) {
|
|
if (!state[id]) state[id] = { status: 'pending', notes: '' };
|
|
state[id].notes = notes;
|
|
saveState();
|
|
}
|
|
|
|
// ---- Render ----
|
|
const allStepIds = [];
|
|
|
|
function render() {
|
|
const main = document.getElementById('main');
|
|
main.innerHTML = '';
|
|
|
|
sections.forEach(section => {
|
|
const group = document.createElement('div');
|
|
group.className = 'section-group';
|
|
|
|
const sectionSteps = section.steps;
|
|
const done = sectionSteps.filter(s => getStepState(s.id).status !== 'pending').length;
|
|
|
|
group.innerHTML = `<div class="section-title">
|
|
${section.title}
|
|
<span class="badge">${done}/${sectionSteps.length}</span>
|
|
</div>`;
|
|
|
|
sectionSteps.forEach(step => {
|
|
allStepIds.push(step.id);
|
|
const el = createStepElement(step);
|
|
group.appendChild(el);
|
|
});
|
|
|
|
main.appendChild(group);
|
|
});
|
|
|
|
updateProgress();
|
|
}
|
|
|
|
function createStepElement(step) {
|
|
const s = getStepState(step.id);
|
|
const el = document.createElement('div');
|
|
el.className = `step status-${s.status}`;
|
|
el.id = `step-${step.id}`;
|
|
el.setAttribute('data-id', step.id);
|
|
|
|
el.innerHTML = `
|
|
<div class="step-header" onclick="toggleStep('${step.id}')">
|
|
<span class="step-id">${step.id}</span>
|
|
<span class="step-title">${step.title}</span>
|
|
<span class="step-status-badge badge-${s.status}">${s.status}</span>
|
|
<span class="step-expand chevron">▼</span>
|
|
</div>
|
|
<div class="step-body">
|
|
<div class="step-field">
|
|
<div class="step-field-label">Preconditions</div>
|
|
<div class="step-field-value">${step.preconditions}</div>
|
|
</div>
|
|
<div class="step-field">
|
|
<div class="step-field-label">Action</div>
|
|
<div class="step-field-value">${step.action}</div>
|
|
</div>
|
|
<div class="step-field">
|
|
<div class="step-field-label">Expected Result</div>
|
|
<div class="step-field-value">${step.expected}</div>
|
|
</div>
|
|
<div class="step-field">
|
|
<div class="step-field-label">What to Capture on Fail</div>
|
|
<div class="step-field-value">${step.onFail}</div>
|
|
</div>
|
|
<div class="step-actions">
|
|
<button class="btn btn-pass" onclick="setStepStatus('${step.id}','pass')">✓ Pass</button>
|
|
<button class="btn btn-fail" onclick="setStepStatus('${step.id}','fail')">✗ Fail</button>
|
|
<button class="btn btn-skip" onclick="setStepStatus('${step.id}','skip')">⊘ Skip</button>
|
|
</div>
|
|
<textarea class="notes-field" placeholder="Notes for ${step.id}..."
|
|
oninput="setStepNotes('${step.id}', this.value)">${s.notes || ''}</textarea>
|
|
</div>
|
|
`;
|
|
return el;
|
|
}
|
|
|
|
function renderStep(id) {
|
|
const el = document.getElementById(`step-${id}`);
|
|
if (!el) return;
|
|
const s = getStepState(id);
|
|
el.className = `step status-${s.status}` + (el.classList.contains('open') ? ' open' : '');
|
|
el.querySelector('.step-status-badge').className = `step-status-badge badge-${s.status}`;
|
|
el.querySelector('.step-status-badge').textContent = s.status;
|
|
|
|
// Update section badge
|
|
sections.forEach(section => {
|
|
if (section.steps.some(st => st.id === id)) {
|
|
const group = el.closest('.section-group');
|
|
if (group) {
|
|
const done = section.steps.filter(st => getStepState(st.id).status !== 'pending').length;
|
|
group.querySelector('.badge').textContent = `${done}/${section.steps.length}`;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function toggleStep(id) {
|
|
const el = document.getElementById(`step-${id}`);
|
|
if (el) el.classList.toggle('open');
|
|
}
|
|
|
|
function updateProgress() {
|
|
const total = allStepIds.length;
|
|
const passed = allStepIds.filter(id => getStepState(id).status === 'pass').length;
|
|
const failed = allStepIds.filter(id => getStepState(id).status === 'fail').length;
|
|
const skipped = allStepIds.filter(id => getStepState(id).status === 'skip').length;
|
|
const pending = total - passed - failed - skipped;
|
|
const done = passed + failed + skipped;
|
|
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
|
|
document.getElementById('progressFill').style.width = pct + '%';
|
|
document.getElementById('progressText').textContent = `${done} / ${total} (${pct}%)`;
|
|
document.getElementById('stats').innerHTML = `
|
|
<span class="stat-pass">✓ ${passed} Pass</span>
|
|
<span class="stat-fail">✗ ${failed} Fail</span>
|
|
<span class="stat-skip">⊘ ${skipped} Skip</span>
|
|
<span class="stat-pending">○ ${pending} Pending</span>
|
|
`;
|
|
}
|
|
|
|
// ---- Controls ----
|
|
function expandAll() {
|
|
document.querySelectorAll('.step').forEach(el => el.classList.add('open'));
|
|
}
|
|
|
|
function collapseAll() {
|
|
document.querySelectorAll('.step').forEach(el => el.classList.remove('open'));
|
|
}
|
|
|
|
function jumpToNext() {
|
|
// Find first step that has no allStepIds status set
|
|
for (const id of allStepIds) {
|
|
if (getStepState(id).status === 'pending') {
|
|
const el = document.getElementById(`step-${id}`);
|
|
if (el) {
|
|
el.classList.add('open');
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
// All done
|
|
alert('All steps have been evaluated!');
|
|
}
|
|
|
|
function exportResults() {
|
|
const now = new Date().toISOString();
|
|
let txt = `=== PAYFRIT WORKS iOS TEST RESULTS ===\n`;
|
|
txt += `Exported: ${now}\n`;
|
|
txt += `App: ${APP_NAME}\n`;
|
|
txt += `Version: ${APP_VERSION}\n`;
|
|
txt += `Environment: ${APP_ENV}\n`;
|
|
txt += `\n`;
|
|
|
|
const total = allStepIds.length;
|
|
const passed = allStepIds.filter(id => getStepState(id).status === 'pass').length;
|
|
const failed = allStepIds.filter(id => getStepState(id).status === 'fail').length;
|
|
const skipped = allStepIds.filter(id => getStepState(id).status === 'skip').length;
|
|
const pending = total - passed - failed - skipped;
|
|
|
|
txt += `SUMMARY: ${passed} Pass | ${failed} Fail | ${skipped} Skip | ${pending} Pending (${total} total)\n`;
|
|
txt += `${'='.repeat(60)}\n\n`;
|
|
|
|
sections.forEach(section => {
|
|
txt += `--- ${section.title} ---\n`;
|
|
section.steps.forEach(step => {
|
|
const s = getStepState(step.id);
|
|
const statusIcon = s.status === 'pass' ? '✓' : s.status === 'fail' ? '✗' : s.status === 'skip' ? '⊘' : '○';
|
|
txt += ` [${statusIcon}] ${step.id}: ${step.title} — ${s.status.toUpperCase()}\n`;
|
|
if (s.notes) {
|
|
txt += ` Notes: ${s.notes}\n`;
|
|
}
|
|
});
|
|
txt += `\n`;
|
|
});
|
|
|
|
// Download
|
|
const blob = new Blob([txt], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `payfrit-works-ios-test-results-${now.split('T')[0]}.txt`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function resetRun() {
|
|
if (!confirm('Reset all test results? This will clear all Pass/Fail/Skip statuses and notes for this walkthrough.')) return;
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
state = {};
|
|
// Re-render
|
|
allStepIds.length = 0;
|
|
render();
|
|
}
|
|
|
|
// ---- Init ----
|
|
render();
|
|
</script>
|
|
</body>
|
|
</html>
|