feat: add XCUITest suite with 10 critical flow tests and QA test plan

Add comprehensive UI test infrastructure with Page Object pattern,
accessibility identifiers, UI test mode (--ui-testing, --reset-state,
--disable-animations), and 10 passing tests covering app launch, tab
navigation, trip wizard, trip saving, settings, schedule, and
accessibility at XXXL Dynamic Type. Also adds a 229-case QA test plan
Excel workbook for manual QA handoff.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-16 16:23:59 -06:00
parent 787a0f795e
commit d53f222489
16 changed files with 1528 additions and 25 deletions

Binary file not shown.

542
docs/generate_qa_sheet.py Normal file
View File

@@ -0,0 +1,542 @@
#!/usr/bin/env python3
"""Generate QA test plan Excel sheet for SportsTime iOS app."""
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
wb = openpyxl.Workbook()
# Styles
header_font = Font(name="Helvetica Neue", bold=True, size=11, color="FFFFFF")
header_fill = PatternFill(start_color="2C3E50", end_color="2C3E50", fill_type="solid")
section_font = Font(name="Helvetica Neue", bold=True, size=11, color="FFFFFF")
section_fill = PatternFill(start_color="FF6B35", end_color="FF6B35", fill_type="solid")
p1_fill = PatternFill(start_color="FADBD8", end_color="FADBD8", fill_type="solid")
p2_fill = PatternFill(start_color="FEF9E7", end_color="FEF9E7", fill_type="solid")
p3_fill = PatternFill(start_color="E8F8F5", end_color="E8F8F5", fill_type="solid")
wrap = Alignment(wrap_text=True, vertical="top")
thin_border = Border(
left=Side(style="thin", color="D5D8DC"),
right=Side(style="thin", color="D5D8DC"),
top=Side(style="thin", color="D5D8DC"),
bottom=Side(style="thin", color="D5D8DC"),
)
COLUMNS = ["ID", "Feature Area", "Test Case", "Steps", "Expected Result", "Priority", "Type", "Status", "Tester", "Notes"]
COL_WIDTHS = [6, 18, 40, 55, 40, 10, 14, 10, 12, 30]
def setup_sheet(ws, title):
ws.title = title
ws.sheet_properties.tabColor = "FF6B35"
for i, (col, w) in enumerate(zip(COLUMNS, COL_WIDTHS), 1):
cell = ws.cell(row=1, column=i, value=col)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
cell.border = thin_border
ws.column_dimensions[get_column_letter(i)].width = w
ws.freeze_panes = "A2"
ws.auto_filter.ref = f"A1:J1"
def add_section(ws, row, title):
for col in range(1, len(COLUMNS) + 1):
cell = ws.cell(row=row, column=col, value=title if col == 3 else "")
cell.font = section_font
cell.fill = section_fill
cell.border = thin_border
return row + 1
def add_row(ws, row, test_id, area, case, steps, expected, priority, test_type):
data = [test_id, area, case, steps, expected, priority, test_type, "", "", ""]
for col, val in enumerate(data, 1):
cell = ws.cell(row=row, column=col, value=val)
cell.alignment = wrap
cell.border = thin_border
if col == 6:
if val == "P1":
cell.fill = p1_fill
elif val == "P2":
cell.fill = p2_fill
elif val == "P3":
cell.fill = p3_fill
return row + 1
# ============================================================
# SHEET 1: Functional Tests
# ============================================================
ws = wb.active
setup_sheet(ws, "Functional Tests")
r = 2
tid = 1
# --- App Launch & Bootstrap ---
r = add_section(ws, r, "APP LAUNCH & BOOTSTRAP")
tests = [
("App Launch", "Cold launch on first install", "1. Delete app\n2. Install fresh\n3. Launch app", "Bootstrap screen appears, then home screen loads with hero card", "P1", "Smoke"),
("App Launch", "Bootstrap loads bundled data", "1. Launch app (fresh install)\n2. Wait for home screen", "Teams, stadiums, and games load from bundled JSON. 'Start Planning' button is interactable", "P1", "Smoke"),
("App Launch", "Cold launch with existing data", "1. Launch app after prior use\n2. Observe load time", "Home screen loads quickly from cached SwiftData. No bootstrap needed", "P1", "Regression"),
("App Launch", "Launch with no network", "1. Enable airplane mode\n2. Launch app", "App launches successfully with cached data. No crash. CloudKit sync silently skipped", "P1", "Negative"),
("App Launch", "Launch after OS update", "1. Update iOS\n2. Launch app", "App launches without crash. SwiftData migration succeeds if schema changed", "P2", "Regression"),
("App Launch", "Background to foreground resume", "1. Launch app\n2. Background it\n3. Wait 30s\n4. Foreground", "App resumes without re-bootstrapping. Data intact", "P1", "Regression"),
("App Launch", "Onboarding paywall shown on first launch (free user)", "1. Fresh install\n2. Launch app\n3. Wait for home screen", "Onboarding paywall sheet appears. Can dismiss with 'Not Now'. Not shown on subsequent launches", "P2", "Functional"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws, r, f"F-{tid:03d}", area, case, steps, expected, priority, ttype)
tid += 1
# --- Tab Navigation ---
r = add_section(ws, r, "TAB NAVIGATION")
tests = [
("Navigation", "Switch to all 5 tabs", "1. Launch app\n2. Tap each tab: Home, Schedule, My Trips, Progress, Settings", "Each tab loads its content without crash. Tab bar highlight updates correctly", "P1", "Smoke"),
("Navigation", "Tab state preserved on switch", "1. Go to Schedule tab, apply filters\n2. Switch to Home tab\n3. Switch back to Schedule", "Schedule filters are still applied. Scroll position preserved", "P2", "Functional"),
("Navigation", "Double-tap tab scrolls to top", "1. Scroll down on any tab\n2. Tap the active tab again", "View scrolls to top", "P3", "UX"),
("Navigation", "Tab badge updates", "1. Save a trip\n2. Check My Trips tab", "Tab updates to reflect new content", "P3", "Functional"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws, r, f"F-{tid:03d}", area, case, steps, expected, priority, ttype)
tid += 1
# --- Home Tab ---
r = add_section(ws, r, "HOME TAB")
tests = [
("Home", "Hero card displays correctly", "1. Launch app\n2. View hero card area", "'Adventure Awaits' text visible. 'Start Planning' button visible and tappable", "P1", "Smoke"),
("Home", "Start Planning opens wizard", "1. Tap 'Start Planning' button", "Trip wizard sheet appears with 'Plan a Trip' title and planning mode options", "P1", "Smoke"),
("Home", "Featured trips carousel loads", "1. Scroll to 'Featured Trips' section\n2. Swipe horizontally", "Trip cards displayed grouped by region. Cards show city count and game count", "P2", "Functional"),
("Home", "Featured trips refresh button", "1. Tap refresh button on featured trips\n2. Wait for reload", "Loading indicator shows. New suggestions appear. Error state with retry if network fails", "P2", "Functional"),
("Home", "Tap featured trip opens detail", "1. Tap any featured trip card", "TripDetailView opens showing itinerary, map, and stats", "P2", "Functional"),
("Home", "Recent trips section (with saved trips)", "1. Save 1+ trips\n2. Go to Home tab", "Recent trips section shows up to 3 most recent saved trips with 'See All' link", "P2", "Functional"),
("Home", "Recent trips section (no saved trips)", "1. Launch fresh (no saved trips)\n2. View Home tab", "Recent trips section hidden or shows empty prompt", "P3", "Functional"),
("Home", "Planning tips section", "1. Scroll to bottom of Home tab", "3 planning tips displayed with helpful travel advice", "P3", "Functional"),
("Home", "Create trip toolbar button", "1. Tap '+' / 'Create new trip' button in toolbar", "Trip wizard opens (same as Start Planning)", "P2", "Functional"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws, r, f"F-{tid:03d}", area, case, steps, expected, priority, ttype)
tid += 1
# --- Trip Wizard ---
r = add_section(ws, r, "TRIP PLANNING WIZARD")
tests = [
("Wizard", "Planning mode selection - By Dates", "1. Open wizard\n2. Tap 'By Dates' card", "Card highlights. Date picker, sports, and regions steps appear below", "P1", "Functional"),
("Wizard", "Planning mode selection - By Games", "1. Open wizard\n2. Tap 'By Games' card", "Game picker step appears with sport selector, team multi-select, and game list", "P1", "Functional"),
("Wizard", "Planning mode selection - By Route", "1. Open wizard\n2. Tap 'By Route' card", "Start/end location fields appear with autocomplete search", "P1", "Functional"),
("Wizard", "Planning mode selection - Follow Team", "1. Open wizard\n2. Tap 'Follow Team' card", "Sport selector and single team picker appear", "P1", "Functional"),
("Wizard", "Planning mode selection - By Teams", "1. Open wizard\n2. Tap 'By Teams' card", "Sport selector and multi-team picker appear (min 2 required)", "P1", "Functional"),
("Wizard", "Switching planning modes resets fields", "1. Select 'By Dates', pick dates\n2. Switch to 'By Games'\n3. Switch back to 'By Dates'", "Previously selected dates are reset. Form starts fresh", "P2", "Functional"),
("Wizard", "Calendar navigation - forward", "1. Select 'By Dates'\n2. Tap next month arrow multiple times", "Calendar navigates forward correctly. Month label updates", "P1", "Functional"),
("Wizard", "Calendar navigation - backward", "1. Navigate to a future month\n2. Tap previous month arrow", "Calendar navigates backward. Cannot go before current month", "P2", "Functional"),
("Wizard", "Date range selection", "1. Tap a start date\n2. Tap an end date after it", "Date range highlights between start and end. Summary shows selected range", "P1", "Functional"),
("Wizard", "Date range - end before start", "1. Tap a date\n2. Tap a date before it", "Selection resets. New date becomes start date", "P2", "Edge Case"),
("Wizard", "Date range - same day", "1. Tap a date\n2. Tap the same date again", "Single-day trip selected. Duration shows '1 day'", "P2", "Edge Case"),
("Wizard", "Past dates are disabled", "1. View calendar at current month", "Days before today are grayed out and not tappable", "P2", "Validation"),
("Wizard", "Sport selection - single sport", "1. Tap MLB in sports step", "MLB highlights. Other sports remain deselected", "P1", "Functional"),
("Wizard", "Sport selection - multiple sports", "1. Tap MLB\n2. Tap NBA", "Both MLB and NBA highlight. Trip will include games from both", "P1", "Functional"),
("Wizard", "Sport availability indicator", "1. Select dates\n2. View sports step", "Sports with games in date range show availability. Sports without games show dimmed", "P2", "Functional"),
("Wizard", "Region selection - toggle regions", "1. Tap West region on map\n2. Tap Central\n3. Tap East", "Selected regions highlight on map. At least 1 must remain selected", "P1", "Functional"),
("Wizard", "Route preference - Direct/Scenic/Balanced", "1. Select a route preference option", "Selection highlights. Affects route calculation", "P2", "Functional"),
("Wizard", "Allow repeat cities toggle", "1. Toggle 'Allow Repeat Cities' off\n2. Plan trip", "Route avoids visiting same city on multiple days", "P2", "Functional"),
("Wizard", "Must-stops addition", "1. Tap 'Add Must Stop'\n2. Search for city\n3. Select it", "City added to must-stops list. Will be included in all route options", "P2", "Functional"),
("Wizard", "Review step shows summary", "1. Complete all steps\n2. Scroll to Review", "Summary shows: planning mode, dates, sports, regions, preferences", "P1", "Functional"),
("Wizard", "Plan My Trip button - enabled state", "1. Fill all required fields\n2. Check 'Plan My Trip' button", "Button is enabled and tappable", "P1", "Functional"),
("Wizard", "Plan My Trip button - disabled state", "1. Open wizard without selecting mode", "Button is disabled/grayed out until required fields are filled", "P2", "Validation"),
("Wizard", "Planning execution - success", "1. Fill valid options (By Dates, June 2026, MLB, Central)\n2. Tap 'Plan My Trip'", "Loading indicator shows. Trip Options screen appears with multiple route options", "P1", "Functional"),
("Wizard", "Planning execution - no games found", "1. Select very narrow date range with no games\n2. Tap 'Plan My Trip'", "Error message: 'No games found in your date range'. Suggestions to broaden search", "P1", "Negative"),
("Wizard", "Planning execution - no valid routes", "1. Select conflicting constraints\n2. Tap 'Plan My Trip'", "Error message: 'No valid routes found'. Helpful message to adjust parameters", "P1", "Negative"),
("Wizard", "Cancel wizard", "1. Tap Cancel button in wizard navigation bar", "Wizard sheet dismisses. Returns to previous screen", "P1", "Functional"),
("Wizard", "Wizard scroll behavior", "1. Open wizard\n2. Select planning mode\n3. Scroll through all steps", "All steps are reachable. No content clipped. Smooth scrolling", "P2", "UX"),
("Wizard", "Game picker - filter by sport then team", "1. Select 'By Games'\n2. Choose MLB\n3. Select teams\n4. View games list", "Games filtered by selected sport and teams. Dates shown correctly", "P1", "Functional"),
("Wizard", "Location search autocomplete", "1. Select 'By Route'\n2. Type city name in start field", "Autocomplete suggestions appear. Selecting one resolves coordinates", "P1", "Functional"),
("Wizard", "Follow Team mode", "1. Select 'Follow Team'\n2. Pick sport\n3. Pick team\n4. Set dates\n5. Plan", "Route follows team's away schedule within date range", "P1", "Functional"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws, r, f"F-{tid:03d}", area, case, steps, expected, priority, ttype)
tid += 1
# --- Trip Options ---
r = add_section(ws, r, "TRIP OPTIONS (RESULTS)")
tests = [
("Trip Options", "Results display after planning", "1. Complete wizard and plan", "Trip options listed with city count, game count, distance, and preview", "P1", "Functional"),
("Trip Options", "Sort by Recommended", "1. Tap sort dropdown\n2. Select 'Recommended'", "Trips re-ordered by recommendation score", "P2", "Functional"),
("Trip Options", "Sort by Most Games", "1. Tap sort dropdown\n2. Select 'Most Games'", "Trips re-ordered with most games first", "P2", "Functional"),
("Trip Options", "Sort by Least Miles", "1. Tap sort dropdown\n2. Select 'Least Miles'", "Trips re-ordered with shortest distance first", "P2", "Functional"),
("Trip Options", "Sort by Best Efficiency", "1. Tap sort dropdown\n2. Select 'Best Efficiency'", "Trips re-ordered by games-per-mile ratio", "P2", "Functional"),
("Trip Options", "Pace filter - Packed/Moderate/Relaxed", "1. Tap pace filter\n2. Select 'Packed'", "Only packed-pace trips shown. Count updates", "P2", "Functional"),
("Trip Options", "Cities filter", "1. Tap cities filter\n2. Select '5 cities max'", "Only trips with 5 or fewer cities shown", "P2", "Functional"),
("Trip Options", "Select trip opens detail", "1. Tap any trip option card", "TripDetailView opens with full itinerary for that option", "P1", "Functional"),
("Trip Options", "Back to wizard from options", "1. Tap back button on Trip Options", "Returns to wizard with selections preserved", "P2", "Functional"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws, r, f"F-{tid:03d}", area, case, steps, expected, priority, ttype)
tid += 1
# --- Trip Detail ---
r = add_section(ws, r, "TRIP DETAIL & ITINERARY")
tests = [
("Trip Detail", "Trip detail loads with map", "1. Open any trip detail", "Hero map shows all stops with route lines. Zoom level fits all stops", "P1", "Functional"),
("Trip Detail", "Stats row displays correctly", "1. View trip detail", "City count, game count, total distance, and estimated driving time shown", "P1", "Functional"),
("Trip Detail", "Itinerary day cards", "1. Scroll through itinerary", "Each day shows: date header, game cards (team logos, times, venues), travel segments", "P1", "Functional"),
("Trip Detail", "Conflict detection badge", "1. Open trip with same-day games in different cities", "Orange conflict badge appears. Route options card shows alternatives", "P2", "Functional"),
("Trip Detail", "Save trip (unsaved)", "1. Open unsaved trip\n2. Tap favorite/save button", "Button label changes to 'Remove from favorites'. Trip persisted to SwiftData", "P1", "Functional"),
("Trip Detail", "Unsave trip (saved)", "1. Open saved trip\n2. Tap favorite/save button", "Button label changes to 'Save to favorites'. Trip removed from saved list", "P1", "Functional"),
("Trip Detail", "Save trip - free tier limit", "1. As free user with 1 saved trip\n2. Try to save another", "Paywall shown. Cannot save until upgraded or existing trip deleted", "P1", "Functional"),
("Trip Detail", "Itinerary reordering (saved trips)", "1. Open saved trip\n2. Long-press a game card\n3. Drag to new position", "Card moves to new position. Itinerary recalculates travel times", "P2", "Functional"),
("Trip Detail", "Add custom item to day", "1. Open saved trip\n2. Tap '+' on a day card\n3. Fill in item details", "Custom item added to itinerary (Attraction/Hotel/Restaurant/etc)", "P2", "Functional"),
("Trip Detail", "Edit custom item", "1. Tap existing custom item\n2. Modify details\n3. Save", "Item updated with new details", "P2", "Functional"),
("Trip Detail", "Delete custom item", "1. Swipe left on custom item\n2. Tap delete", "Item removed from itinerary", "P2", "Functional"),
("Trip Detail", "Travel day override", "1. Tap travel segment\n2. Select 'Move to different day'", "Travel segment reassigned. Itinerary adjusts", "P3", "Functional"),
("Trip Detail", "Share trip card", "1. Tap share button in toolbar", "Share sheet appears with trip summary card image", "P2", "Functional"),
("Trip Detail", "Export PDF (Pro)", "1. Tap PDF export button", "Multi-page PDF generated with maps, photos, attractions. Loading overlay during generation", "P1", "Functional"),
("Trip Detail", "Export PDF (free tier - blocked)", "1. As free user, tap PDF export button", "Paywall appears. Export blocked until Pro", "P2", "Functional"),
("Trip Detail", "Export PDF with large trip (15+ stops)", "1. Plan a large trip\n2. Export PDF", "PDF generates without crash. Route split into segments (16 stop limit per map)", "P2", "Edge Case"),
("Trip Detail", "Trip detail with 1 stop", "1. Plan trip with minimal options (1 game)", "Detail view works correctly with single stop", "P3", "Edge Case"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws, r, f"F-{tid:03d}", area, case, steps, expected, priority, ttype)
tid += 1
# --- My Trips ---
r = add_section(ws, r, "MY TRIPS")
tests = [
("My Trips", "Empty state (no saved trips)", "1. Fresh install\n2. Go to My Trips tab", "Empty state shows suitcase icon and message to create trips", "P1", "Functional"),
("My Trips", "Saved trips list", "1. Save 3+ trips\n2. Go to My Trips tab", "All saved trips listed, sorted by city count descending", "P1", "Functional"),
("My Trips", "Tap saved trip opens detail", "1. Tap a saved trip card", "TripDetailView opens with editable itinerary (allowCustomItems=true)", "P1", "Functional"),
("My Trips", "Delete saved trip", "1. Swipe left on trip card\n2. Tap delete\n3. Confirm", "Trip removed from list. SwiftData updated", "P1", "Functional"),
("My Trips", "Polls section visible", "1. Go to My Trips tab", "Group Polls section visible above saved trips", "P2", "Functional"),
("My Trips", "Create poll button (2+ trips)", "1. Save 2+ trips\n2. Tap '+' in polls section", "Poll creation sheet opens", "P2", "Functional"),
("My Trips", "Create poll button hidden (<2 trips)", "1. Have 0-1 saved trips\n2. Check polls section", "Create poll button not shown. Message says 'Save at least 2 trips'", "P2", "Functional"),
("My Trips", "Pull to refresh", "1. Pull down on My Trips list", "Polls refresh from CloudKit. Loading indicator shows", "P3", "Functional"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws, r, f"F-{tid:03d}", area, case, steps, expected, priority, ttype)
tid += 1
# --- Schedule ---
r = add_section(ws, r, "SCHEDULE TAB")
tests = [
("Schedule", "Schedule loads with games", "1. Go to Schedule tab", "Games listed grouped by sport, sorted by date. Filter button visible", "P1", "Functional"),
("Schedule", "Sport filter chips", "1. Tap sport chip (e.g., MLB)\n2. Observe list", "Only MLB games shown. Chip highlights as active filter", "P1", "Functional"),
("Schedule", "Multiple sport filters", "1. Tap MLB chip\n2. Tap NBA chip", "Both MLB and NBA games shown", "P2", "Functional"),
("Schedule", "Clear all filters", "1. Apply filters\n2. Tap clear/reset button", "All games shown. Filters reset to default", "P2", "Functional"),
("Schedule", "Search by team name", "1. Tap search field\n2. Type 'Yankees'", "Only games involving Yankees shown", "P2", "Functional"),
("Schedule", "Search by venue", "1. Tap search field\n2. Type 'Wrigley'", "Only games at Wrigley Field shown", "P2", "Functional"),
("Schedule", "Date range filter", "1. Set custom date range\n2. Observe results", "Only games within date range shown", "P2", "Functional"),
("Schedule", "Empty state (no matching games)", "1. Apply very restrictive filters", "Empty state message shown with suggestion to adjust filters", "P2", "Negative"),
("Schedule", "Loading state", "1. Go to Schedule tab on slow network", "Loading indicator visible while games load", "P3", "Functional"),
("Schedule", "Schedule diagnostics", "1. Tap diagnostics button", "Schedule loading diagnostics displayed (game counts, load times)", "P3", "Functional"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws, r, f"F-{tid:03d}", area, case, steps, expected, priority, ttype)
tid += 1
# --- Progress Tab ---
r = add_section(ws, r, "PROGRESS TAB (PRO)")
tests = [
("Progress", "Progress tab loads (Pro user)", "1. As Pro user, go to Progress tab", "Stadium map, progress stats, and add visit button visible", "P1", "Functional"),
("Progress", "Progress tab blocked (free user)", "1. As free user, go to Progress tab", "Paywall or upgrade prompt shown", "P1", "Functional"),
("Progress", "League/sport selector", "1. Toggle between MLB/NBA/NHL/etc", "Map and stats update to show selected sport's stadiums", "P1", "Functional"),
("Progress", "Stadium map - visited vs unvisited", "1. Add a stadium visit\n2. View map", "Visited stadiums show different color/icon than unvisited", "P1", "Functional"),
("Progress", "Progress percentage updates", "1. Add a new stadium visit\n2. View progress stats", "Percentage completion updates for that sport", "P1", "Functional"),
("Progress", "Add visit - manual entry", "1. Tap 'Add Visit'\n2. Select 'Manual Entry'\n3. Fill all fields\n4. Save", "Visit saved. Stadium marked as visited on map. Progress updates", "P1", "Functional"),
("Progress", "Add visit - required fields", "1. Try to save visit without sport/stadium", "Save button disabled or validation error shown", "P2", "Validation"),
("Progress", "Add visit - import from photos", "1. Tap 'Add Visit'\n2. Select 'Import from Photos'\n3. Select photos", "Photos processed for EXIF. Matched stadiums shown for confirmation", "P2", "Functional"),
("Progress", "Photo import - no GPS data", "1. Import photo without GPS metadata", "Warning: 'Could not determine location'. Photo skipped or manual entry suggested", "P2", "Edge Case"),
("Progress", "Photo import - no matching stadium", "1. Import photo from non-stadium location", "No match found message. Suggest manual entry", "P2", "Edge Case"),
("Progress", "Photo import - permission denied", "1. Deny photo library permission\n2. Try import", "Permission denied message with link to Settings", "P2", "Negative"),
("Progress", "View games history", "1. Tap 'Games History'\n2. Browse list", "All visits listed grouped by year. Sport filter chips work", "P2", "Functional"),
("Progress", "Edit stadium visit", "1. Tap visit in history\n2. Edit details\n3. Save", "Visit updated with new info", "P2", "Functional"),
("Progress", "Delete stadium visit", "1. Tap visit\n2. Tap delete\n3. Confirm", "Visit removed. Progress stats update. Map updates", "P1", "Functional"),
("Progress", "Score auto-fill", "1. Add visit for recent game", "Score fields auto-populated from API if game found", "P3", "Functional"),
("Progress", "Achievements gallery", "1. Navigate to achievements section", "Badges shown in grid: earned (colored), in-progress (partial), locked (gray)", "P2", "Functional"),
("Progress", "Achievement detail", "1. Tap any achievement badge", "Detail shows: requirement, current progress, description", "P2", "Functional"),
("Progress", "Share achievement", "1. Tap earned achievement\n2. Tap share", "Share card generated with achievement image", "P3", "Functional"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws, r, f"F-{tid:03d}", area, case, steps, expected, priority, ttype)
tid += 1
# --- Polls ---
r = add_section(ws, r, "GROUP POLLS")
tests = [
("Polls", "Create poll", "1. Have 2+ saved trips\n2. Tap create poll\n3. Enter title\n4. Select 2+ trips\n5. Create", "Poll created with 6-char share code. Shows in polls list", "P1", "Functional"),
("Polls", "Create poll - validation (< 2 trips selected)", "1. Select only 1 trip\n2. Try to create", "Create button disabled. Message: 'Select at least 2 trips'", "P2", "Validation"),
("Polls", "View poll detail", "1. Tap poll in list", "Poll detail shows title, share code, trip options with vote counts", "P1", "Functional"),
("Polls", "Vote on poll", "1. Open poll\n2. Tap vote on a trip option", "Vote registers. Count updates. Current user's vote highlighted", "P1", "Functional"),
("Polls", "Change vote", "1. Vote on trip A\n2. Vote on trip B instead", "Vote moves from A to B. Counts update correctly", "P2", "Functional"),
("Polls", "Share poll link", "1. Tap share button on poll", "Share sheet with poll deep link. Code displayed for manual sharing", "P2", "Functional"),
("Polls", "Open poll via deep link", "1. Tap sportstime://poll/{code} link", "App opens. Poll detail view loads for that share code", "P1", "Functional"),
("Polls", "Deep link - invalid code", "1. Open sportstime://poll/INVALID", "Error message: 'Poll not found'", "P2", "Negative"),
("Polls", "Delete poll (creator only)", "1. As poll creator, tap delete\n2. Confirm", "Poll deleted from CloudKit. Removed from list", "P2", "Functional"),
("Polls", "Poll with network error", "1. Turn off network\n2. Try to create/vote on poll", "Error message shown. Graceful failure", "P2", "Negative"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws, r, f"F-{tid:03d}", area, case, steps, expected, priority, ttype)
tid += 1
# --- Settings ---
r = add_section(ws, r, "SETTINGS")
tests = [
("Settings", "Settings tab loads", "1. Go to Settings tab", "All sections visible: Subscription, Appearance, Theme, Animations, Sports, Travel, Sync, Privacy, About, Reset", "P1", "Smoke"),
("Settings", "App version displayed", "1. Scroll to About section", "Version number and build shown. Not empty", "P2", "Functional"),
("Settings", "Appearance - Light mode", "1. Select Light mode", "App switches to light theme immediately", "P2", "Functional"),
("Settings", "Appearance - Dark mode", "1. Select Dark mode", "App switches to dark theme immediately", "P2", "Functional"),
("Settings", "Appearance - System mode", "1. Select System\n2. Toggle device dark mode", "App follows device setting", "P2", "Functional"),
("Settings", "Toggle animations on/off", "1. Toggle 'Animations' switch", "Home background switches between animated and static", "P3", "Functional"),
("Settings", "Sports preferences toggle", "1. Toggle off a sport (e.g., NHL)\n2. Go to Schedule tab", "NHL games hidden from schedule. Sport not available in wizard", "P2", "Functional"),
("Settings", "Max driving hours slider", "1. Adjust slider to 6 hours\n2. Plan a trip", "Trip plans limited to 6 hours driving per day", "P2", "Functional"),
("Settings", "Privacy - analytics opt-out", "1. Toggle analytics off\n2. Use app normally", "No analytics events sent to PostHog", "P2", "Functional"),
("Settings", "Privacy - analytics opt-in", "1. Toggle analytics back on", "Analytics resume. Events tracked", "P2", "Functional"),
("Settings", "Manual sync trigger", "1. Tap 'Sync Now' in Data Sync section", "Sync starts. Status updates. Success/failure message shown", "P2", "Functional"),
("Settings", "View sync logs", "1. Tap 'View Sync Logs'", "Log viewer sheet opens showing recent sync activity", "P3", "Functional"),
("Settings", "Subscription section (free user)", "1. View subscription section as free user", "Shows 'Free' plan. 'Upgrade to Pro' button visible", "P1", "Functional"),
("Settings", "Subscription section (Pro user)", "1. View subscription section as Pro user", "Shows 'Pro' plan. Subscription details visible", "P1", "Functional"),
("Settings", "Restore purchases", "1. Tap 'Restore Purchases'", "StoreKit restore flow executes. Success/failure message", "P1", "Functional"),
("Settings", "Reset to defaults", "1. Tap 'Reset to Defaults'\n2. Confirm on alert", "All settings reset. App uses defaults for appearance, sports, etc", "P2", "Functional"),
("Settings", "Reset to defaults - cancel", "1. Tap 'Reset to Defaults'\n2. Cancel on alert", "Settings unchanged", "P3", "Functional"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws, r, f"F-{tid:03d}", area, case, steps, expected, priority, ttype)
tid += 1
# --- Subscription / IAP ---
r = add_section(ws, r, "SUBSCRIPTION & IN-APP PURCHASE")
tests = [
("IAP", "Paywall displays products", "1. Trigger paywall (save 2nd trip as free)", "Monthly and annual subscription options shown with pricing", "P1", "Functional"),
("IAP", "Purchase monthly subscription", "1. Tap monthly option\n2. Authenticate\n3. Confirm", "Purchase succeeds. Pro features unlocked. Analytics event tracked", "P1", "Functional"),
("IAP", "Purchase annual subscription", "1. Tap annual option\n2. Authenticate\n3. Confirm", "Purchase succeeds. Pro features unlocked", "P1", "Functional"),
("IAP", "Purchase cancellation", "1. Start purchase\n2. Cancel on App Store dialog", "Purchase cancelled. User remains on free tier. No crash", "P1", "Negative"),
("IAP", "Purchase failure", "1. Simulate purchase failure (sandbox)", "Error message shown. User remains on free tier", "P1", "Negative"),
("IAP", "Restore purchases - has subscription", "1. Previously purchased\n2. Tap Restore", "Pro status restored. Features unlocked", "P1", "Functional"),
("IAP", "Restore purchases - no subscription", "1. Never purchased\n2. Tap Restore", "Message: 'No purchases to restore'", "P2", "Negative"),
("IAP", "Pro features unlock after purchase", "1. Purchase Pro\n2. Check: unlimited trips, PDF export, progress", "All Pro features accessible. Lock badges removed", "P1", "Functional"),
("IAP", "Subscription expiry", "1. Let sandbox subscription expire\n2. Use app", "Pro features locked again. Paywall shown when accessing", "P1", "Functional"),
("IAP", "Free trial (if applicable)", "1. Start free trial\n2. Verify Pro features during trial", "Pro features available during trial. Expiry handled gracefully", "P2", "Functional"),
("IAP", "Onboarding paywall - first launch", "1. Fresh install as free user", "Onboarding paywall appears with feature carousel. Can dismiss", "P2", "Functional"),
("IAP", "Onboarding paywall not shown again", "1. Dismiss onboarding paywall\n2. Restart app", "Paywall does not appear again", "P2", "Functional"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws, r, f"F-{tid:03d}", area, case, steps, expected, priority, ttype)
tid += 1
# ============================================================
# SHEET 2: Edge Cases & Negative Tests
# ============================================================
ws2 = wb.create_sheet()
setup_sheet(ws2, "Edge Cases & Negative")
r = 2
eid = 1
r = add_section(ws2, r, "DATA & STATE EDGE CASES")
tests = [
("Data", "Duplicate game IDs in schedule", "1. Load schedule with known duplicate game IDs", "Duplicates handled gracefully. No crash. Latest version shown", "P2", "Edge Case"),
("Data", "Stadium rename handling", "1. Search for old stadium name (e.g., 'SBC Park')", "Resolves to current name via StadiumAlias lookup", "P2", "Edge Case"),
("Data", "Timezone edge case - midnight game", "1. View game at 11:30 PM local time\n2. Check which calendar day it appears on", "Game appears on correct local date, not UTC date", "P2", "Edge Case"),
("Data", "Empty games for date range", "1. Pick off-season dates (e.g., December for MLB)", "No games found. Helpful message shown", "P1", "Edge Case"),
("Data", "Very long trip (30+ days)", "1. Plan trip spanning 30+ days", "Engine handles without crash. Results may be limited", "P2", "Edge Case"),
("Data", "Very short trip (1 day)", "1. Select same start/end date\n2. Plan", "Single-day itinerary with games available that day", "P2", "Edge Case"),
("Data", "All regions deselected", "1. Try to deselect all 3 regions", "At least 1 region must remain. UI prevents full deselection", "P2", "Validation"),
("Data", "No sports selected", "1. Deselect all sports\n2. Try to plan", "Plan button disabled. Validation message shown", "P2", "Validation"),
("Data", "Large number of saved trips (50+)", "1. Save 50+ trips\n2. Browse My Trips", "List performs smoothly. No memory issues", "P3", "Performance"),
("Data", "Trip with 0 games but valid stops", "1. Create trip where games are cancelled/rescheduled", "Trip shows stops without game cards. Travel still calculated", "P3", "Edge Case"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws2, r, f"E-{eid:03d}", area, case, steps, expected, priority, ttype)
eid += 1
r = add_section(ws2, r, "NETWORK & OFFLINE")
tests = [
("Network", "Full offline usage", "1. Enable airplane mode\n2. Launch app\n3. Browse home, schedule, trips", "App works with cached data. No crash. Sync features gracefully unavailable", "P1", "Negative"),
("Network", "Lose network mid-planning", "1. Start planning a trip\n2. Toggle airplane mode\n3. Tap Plan", "Planning uses local data. May show fewer results. No crash", "P1", "Negative"),
("Network", "Lose network during PDF export", "1. Start PDF export\n2. Toggle airplane mode", "Export fails gracefully. Error message shown. No partial corrupt file", "P2", "Negative"),
("Network", "Slow network (poor connection)", "1. Use network link conditioner to simulate 3G\n2. Use app normally", "App responsive. Longer loading indicators. No timeouts causing crash", "P2", "Negative"),
("Network", "Network recovery after offline", "1. Use app offline\n2. Re-enable network\n3. Navigate around", "Background sync resumes. Data refreshes", "P2", "Functional"),
("Network", "CloudKit quota exceeded", "1. Simulate CloudKit throttling", "Sync fails gracefully. Local data unaffected. Retry on next launch", "P3", "Negative"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws2, r, f"E-{eid:03d}", area, case, steps, expected, priority, ttype)
eid += 1
r = add_section(ws2, r, "INTERRUPTIONS & LIFECYCLE")
tests = [
("Lifecycle", "Incoming phone call during planning", "1. Start planning trip\n2. Receive phone call\n3. End call\n4. Return to app", "Wizard state preserved. Can continue planning", "P1", "Interruption"),
("Lifecycle", "Memory warning during PDF export", "1. Generate PDF for large trip\n2. Open other memory-heavy apps", "Export completes or fails gracefully. No crash", "P2", "Interruption"),
("Lifecycle", "App killed during save", "1. Tap save trip\n2. Immediately force-kill app\n3. Relaunch", "Trip may or may not be saved (depends on timing). No data corruption", "P2", "Interruption"),
("Lifecycle", "Rotate device during wizard", "1. Open wizard in portrait\n2. Rotate to landscape", "Layout adapts correctly (if supported) or remains portrait-locked", "P3", "Functional"),
("Lifecycle", "Multitasking - split view (iPad)", "1. Open app in split view\n2. Use all features", "Layout adapts to smaller width. No clipping or overlaps", "P3", "Functional"),
("Lifecycle", "Background app refresh", "1. Background app for 1+ hours\n2. Foreground app", "Data may have refreshed via background sync. No stale state", "P2", "Functional"),
("Lifecycle", "Low storage on device", "1. Fill device storage\n2. Try to save trip", "Error handled gracefully. No crash. Message about storage", "P3", "Negative"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws2, r, f"E-{eid:03d}", area, case, steps, expected, priority, ttype)
eid += 1
r = add_section(ws2, r, "INPUT VALIDATION")
tests = [
("Validation", "Empty title for poll", "1. Try to create poll with empty title", "Create button disabled or validation error", "P2", "Validation"),
("Validation", "Special characters in search", "1. Type emoji/special chars in schedule search", "Search handles gracefully. No crash. May show no results", "P3", "Validation"),
("Validation", "Very long text in notes field", "1. Enter 5000+ characters in visit notes", "Text accepted or truncated. No crash. Scrollable", "P3", "Validation"),
("Validation", "Negative score in visit", "1. Enter -1 in score field", "Input rejected or clamped to 0", "P3", "Validation"),
("Validation", "Future date for stadium visit", "1. Set visit date to next year", "Date accepted (user may have tickets). Or validation if restricted", "P3", "Edge Case"),
("Validation", "Same start/end location (By Route)", "1. Enter same city for start and end", "Handled: either error message or round-trip calculated", "P2", "Validation"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws2, r, f"E-{eid:03d}", area, case, steps, expected, priority, ttype)
eid += 1
# ============================================================
# SHEET 3: Accessibility Tests
# ============================================================
ws3 = wb.create_sheet()
setup_sheet(ws3, "Accessibility")
r = 2
aid = 1
r = add_section(ws3, r, "VOICEOVER")
tests = [
("VoiceOver", "Home tab navigable", "1. Enable VoiceOver\n2. Navigate Home tab", "All elements have meaningful labels. Tab bar, hero card, buttons all announced", "P1", "A11y"),
("VoiceOver", "Wizard navigable", "1. Open wizard with VoiceOver\n2. Navigate all steps", "Planning modes, dates, sports, regions all have labels. Selection state announced", "P1", "A11y"),
("VoiceOver", "Trip detail navigable", "1. Open trip detail with VoiceOver", "Map, stats, itinerary items all labeled. Games announce teams and times", "P1", "A11y"),
("VoiceOver", "Schedule tab navigable", "1. Browse schedule with VoiceOver", "Games, filters, search all accessible. Sport chips announce selected state", "P2", "A11y"),
("VoiceOver", "Settings navigable", "1. Navigate Settings with VoiceOver", "All sections, toggles, sliders properly labeled", "P2", "A11y"),
("VoiceOver", "Decorative images hidden", "1. VoiceOver should skip decorative icons", "Icons with accessibilityHidden(true) are skipped. Only meaningful content read", "P2", "A11y"),
("VoiceOver", "Button labels descriptive", "1. Focus on all buttons via VoiceOver", "Buttons announce action (e.g., 'Save to favorites', not just 'heart')", "P1", "A11y"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws3, r, f"A-{aid:03d}", area, case, steps, expected, priority, ttype)
aid += 1
r = add_section(ws3, r, "DYNAMIC TYPE")
tests = [
("Dynamic Type", "Default text size", "1. Set text size to default\n2. Browse all screens", "All text readable. Layout correct", "P1", "A11y"),
("Dynamic Type", "Extra Large text", "1. Settings > Display > Text Size > Extra Large\n2. Browse app", "Text scales up. Layout adapts. No clipping or overlaps", "P1", "A11y"),
("Dynamic Type", "Accessibility XXL", "1. Settings > Accessibility > Larger Text > max size\n2. Browse app", "Text very large. Buttons still tappable. Can scroll to reach all content", "P1", "A11y"),
("Dynamic Type", "Extra Small text", "1. Set text size to smallest\n2. Browse app", "Text readable (not too tiny). Layout doesn't break with extra space", "P2", "A11y"),
("Dynamic Type", "Wizard at large text", "1. Set XXXL text\n2. Open trip wizard\n3. Navigate all steps", "All steps reachable via scrolling. Buttons tappable. Calendar usable", "P1", "A11y"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws3, r, f"A-{aid:03d}", area, case, steps, expected, priority, ttype)
aid += 1
r = add_section(ws3, r, "COLOR & CONTRAST")
tests = [
("Contrast", "Light mode contrast", "1. Set light mode\n2. Check all text/buttons", "Text meets WCAG AA contrast ratio (4.5:1 for normal text)", "P2", "A11y"),
("Contrast", "Dark mode contrast", "1. Set dark mode\n2. Check all text/buttons", "Text meets WCAG AA contrast ratio", "P2", "A11y"),
("Contrast", "Color-blind safe", "1. Enable color filters (Deuteranopia)\n2. Check UI", "No information conveyed by color alone. Icons/text supplement color cues", "P2", "A11y"),
("Contrast", "Reduce transparency", "1. Settings > Accessibility > Reduce Transparency\n2. Browse app", "Background effects simplified. Text remains readable", "P3", "A11y"),
("Contrast", "Bold text", "1. Settings > Accessibility > Bold Text\n2. Browse app", "Text renders bold. No layout breaks", "P3", "A11y"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws3, r, f"A-{aid:03d}", area, case, steps, expected, priority, ttype)
aid += 1
r = add_section(ws3, r, "MOTION & INTERACTION")
tests = [
("Motion", "Reduce Motion enabled", "1. Settings > Accessibility > Reduce Motion\n2. Use app", "Animations disabled/simplified. No parallax or spring effects", "P2", "A11y"),
("Motion", "Hit targets minimum 44x44pt", "1. Audit all tappable elements", "All buttons, toggles, and links meet 44x44pt minimum", "P2", "A11y"),
("Motion", "Keyboard navigation (external keyboard)", "1. Connect Bluetooth keyboard\n2. Navigate app", "Tab key moves focus. Enter activates buttons. Logical tab order", "P3", "A11y"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws3, r, f"A-{aid:03d}", area, case, steps, expected, priority, ttype)
aid += 1
# ============================================================
# SHEET 4: Performance & Stability
# ============================================================
ws4 = wb.create_sheet()
setup_sheet(ws4, "Performance & Stability")
r = 2
pid = 1
r = add_section(ws4, r, "LAUNCH & LOAD TIMES")
tests = [
("Performance", "Cold launch time", "1. Kill app\n2. Launch and time until interactive", "Home screen interactive within 3 seconds", "P1", "Performance"),
("Performance", "Warm launch time", "1. Background app\n2. Foreground and time", "App interactive within 1 second", "P1", "Performance"),
("Performance", "Bootstrap time (first launch)", "1. Fresh install\n2. Time bootstrap", "Bootstrap completes within 5 seconds (bundled JSON load)", "P1", "Performance"),
("Performance", "Planning engine response time", "1. Plan trip with typical parameters\n2. Time from tap to results", "Results appear within 10 seconds for typical trips", "P1", "Performance"),
("Performance", "PDF export time", "1. Export PDF for 5-city trip\n2. Time generation", "PDF generated within 15 seconds", "P2", "Performance"),
("Performance", "Schedule tab load time", "1. Switch to Schedule tab\n2. Time until games visible", "Games visible within 2 seconds (cached data)", "P2", "Performance"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws4, r, f"P-{pid:03d}", area, case, steps, expected, priority, ttype)
pid += 1
r = add_section(ws4, r, "MEMORY & RESOURCES")
tests = [
("Memory", "Memory usage during normal use", "1. Profile with Instruments\n2. Navigate all tabs", "Memory stays under 200MB during normal usage", "P1", "Performance"),
("Memory", "Memory during PDF export", "1. Export large trip PDF\n2. Monitor memory", "Memory spikes handled. No OOM crash. Memory returns to normal after", "P2", "Performance"),
("Memory", "No memory leaks on navigation", "1. Navigate back and forth 20x\n2. Check for leaks", "No sustained memory growth. Views deallocated properly", "P2", "Performance"),
("Memory", "Photo import memory", "1. Import 20 photos\n2. Monitor memory", "Photos processed without OOM. Memory released after processing", "P2", "Performance"),
("Memory", "Scroll performance in schedule", "1. Load full schedule\n2. Fast-scroll through list", "Smooth 60fps scrolling. No frame drops", "P2", "Performance"),
("Memory", "Background memory cleanup", "1. Background app\n2. Check memory reduction", "App releases caches and non-essential memory when backgrounded", "P3", "Performance"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws4, r, f"P-{pid:03d}", area, case, steps, expected, priority, ttype)
pid += 1
r = add_section(ws4, r, "STABILITY")
tests = [
("Stability", "No crashes in 30-min session", "1. Use app for 30 minutes covering all features", "Zero crashes. No hangs > 3 seconds", "P1", "Stability"),
("Stability", "Rapid tab switching", "1. Rapidly switch between all 5 tabs 50 times", "No crash. UI responds correctly each time", "P2", "Stability"),
("Stability", "Rapid wizard open/close", "1. Open wizard → Cancel → Open → Cancel 20 times", "No crash. No memory growth", "P2", "Stability"),
("Stability", "Plan multiple trips sequentially", "1. Plan 5 trips back-to-back", "All plans complete. No degradation. Memory stable", "P2", "Stability"),
("Stability", "Overnight background", "1. Leave app backgrounded overnight\n2. Open in morning", "App resumes normally. Data fresh from background sync", "P2", "Stability"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws4, r, f"P-{pid:03d}", area, case, steps, expected, priority, ttype)
pid += 1
# ============================================================
# SHEET 5: Device Compatibility
# ============================================================
ws5 = wb.create_sheet()
setup_sheet(ws5, "Device Compatibility")
r = 2
did = 1
r = add_section(ws5, r, "DEVICE MODELS")
devices = [
("iPhone SE (3rd gen)", "Smallest supported screen. Verify all content fits"),
("iPhone 16", "Standard size. Primary test device"),
("iPhone 16 Plus", "Larger screen. Verify layout fills properly"),
("iPhone 16 Pro Max", "Largest phone screen. Verify no excessive whitespace"),
("iPad (if supported)", "Tablet layout. Verify split view, larger canvas"),
]
for device, notes in devices:
r = add_row(ws5, r, f"D-{did:03d}", "Device", f"Full smoke test on {device}", f"1. Run full smoke test suite on {device}", f"All features work. Layout correct. {notes}", "P2", "Compatibility")
did += 1
r = add_section(ws5, r, "iOS VERSIONS")
tests = [
("iOS", "Minimum supported iOS version", "1. Run on minimum supported iOS\n2. Full smoke test", "All features work on minimum OS. No deprecated API crashes", "P1", "Compatibility"),
("iOS", "Latest iOS version", "1. Run on latest iOS\n2. Full smoke test", "All features work. No new deprecation warnings causing issues", "P1", "Compatibility"),
("iOS", "iOS beta (if available)", "1. Run on current iOS beta", "App functions. Note any beta-specific issues for future fix", "P3", "Compatibility"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws5, r, f"D-{did:03d}", area, case, steps, expected, priority, ttype)
did += 1
r = add_section(ws5, r, "DISPLAY & ORIENTATION")
tests = [
("Display", "Portrait orientation", "1. Use app in portrait throughout", "All screens render correctly in portrait", "P1", "Compatibility"),
("Display", "Landscape orientation", "1. Rotate to landscape\n2. Check all screens", "App locks to portrait or adapts layout", "P3", "Compatibility"),
("Display", "Dark mode system-wide", "1. Enable system dark mode\n2. App set to 'System'\n3. Browse all screens", "All screens use dark theme. No unreadable text or invisible elements", "P1", "Compatibility"),
("Display", "Light mode system-wide", "1. Enable system light mode\n2. Browse all screens", "All screens use light theme. Proper contrast", "P1", "Compatibility"),
]
for area, case, steps, expected, priority, ttype in tests:
r = add_row(ws5, r, f"D-{did:03d}", area, case, steps, expected, priority, ttype)
did += 1
# ============================================================
# Save
# ============================================================
output = "/Users/treyt/Desktop/code/SportsTime/docs/SportsTime_QA_Test_Plan.xlsx"
wb.save(output)
print(f"Saved to {output}")
print(f"Sheets: {[s.title for s in wb.worksheets]}")
total = sum(1 for ws in wb.worksheets for row in ws.iter_rows(min_row=2) if row[0].value and str(row[0].value).startswith(("F-", "E-", "A-", "P-", "D-")))
print(f"Total test cases: {total}")