QA Findings — system-wide test sweep
Living defect log produced by the test-coverage expansion effort (plan: web sweep → widget · golden · e2e · edge-fn). Each row is a concrete error observed while driving the app, with a status:
- fixed — corrected in this effort (commit noted)
- test-added — regression test added so it can't recur
- flagged-backend — needs a Supabase table / edge function / server change (out of scope here)
- flagged-manual — needs a real device (Face ID, camera, push) — can't reproduce in the web harness
- open — confirmed defect, not yet fixed
Severity: S1 crash/blocker · S2 broken feature/flow · S3 visual/UX · S4 polish.
Method
Drive the dev web build (:8090, flutter run -d web-server --web-port 8090 --dart-define-from-file=env/dev.json) via the browser. Per route: load → read console errors → interact → screenshot → record. Authenticated routes require the user to sign in (the agent must not enter OTP credentials).
Local tests: this repo pins Flutter via FVM (.fvmrc → 3.47.1). Run fvm flutter test … / fvm flutter analyze — not bare dart test, which can pick up the global Dart SDK and fail compiling package:flutter with exhaustive-switch errors on Windows.
Findings
| ID | Sev | Surface | Route / Area | Symptom | Root cause / file | Status |
|---|---|---|---|---|---|---|
| F-001 | S2 | Web | /public tools grid | Tool cards rendered huge & "empty" on desktop widths | No max-width; 2-col grid ballooned. public_landing_screen.dart → wrapped in Center>ConstrainedBox(maxWidth:640) | fixed (0a11560) |
| F-002 | S2 | Web/all | /sign-in from public landing | "Login" button bounced back to /public for guests | _authRedirect treated anonymous session as signed-in. router.dart → only redirect non-anonymous away from /sign-in | fixed (29252ac) |
| F-003 | S3 | Web/all | public landing sign-in card | Implied a staff-only login that doesn't exist | Relabeled to general "تسجيل الدخول / Sign in". public_landing_screen.dart, web FAQ | fixed (0a8768b, web 5a66ba5c7) |
| F-004 | S1 | Web | app mount (long-lived tab) | Renderer froze / "slow mount"; CDP screenshot timed out | gotrue auth-refresh "Failed to fetch" retry storm pinned the isolate (transient). Mitigated by AuthRefreshGuard (circuit-breaker + visibility pause). Recover = full reload. See flutter-web-stuck-mount skill | mitigated |
| F-005 | S3 | Web | All list/content screens | Content balloons to full window width on desktop — rows/cards/inputs stretch ~1512px with huge gaps | Phone layout, no global max-width on web. Seen on /catalog, /tools/mortgage, /tools/roi, /tools/rent. Same root as F-001 but app-wide | fixed — global _GlobalWidthCap in lib/app.dart caps content to contentMaxWidth (960) on viewports wider than the cap, centered with brand-colored gutters, and narrows the reported MediaQuery width so screens don't overflow. Verified /catalog + /tools/mortgage now render as a centered column; landing's tighter local cap still applies | | F-006 | S1 | Web/all | /tools/transactions and /tools/price-map | Route froze the renderer (screen never rendered; CDP screenshot timed out) | webToolRedirects mapped each new path to itself → redirect-only self-loop → GoRouter assertion "redirect-only route must redirect elsewhere". routes.dart (removed both entries) | fixed + test-added (test/core/navigation/web_tool_redirects_test.dart) |
| F-007 | S3 | Web | /tools/price-map | Map renders but the tile/base layer is blank (no tiles, no markers) | flutter_map base layer not painting on web — no console/network error thrown. PACI vector basemap (PaciBasemapLayer in paci_flutter_map_layers.dart) reads its style directly from kuwaitportal.paci.gov.kw — CORS-blocked in the browser → _loadStyle() fails → SizedBox.shrink() = blank. | fixed — paci_flutter_map_layers.dart: on web (or vector-style load failure) render an OSM raster TileLayer instead of a blank surface; public_price_map_screen.dart: render the basemap even when the PACI polygons can't load (only show the disclosure when there's no price data at all). Verified: Map view now shows the OSM map of Kuwait + polygons + legend | | F-008 | S4 | Web | /contact | Social handle shows as aldilaijanre@ / khobaraco@ (the @ floats to the wrong side) | RTL bidi rendering of @handle; contact_screen.dart office.handle → textDirection: TextDirection.ltr | fixed |
| F-009 | S4 | Web/all (auth) | master-detail empty pane (properties, deals, valuations…) | Detail pane reads "Select an item from the list" in English on an Arabic-primary app | Hardcoded string in shared lib/shared/widgets/master_detail_scaffold.dart:178 (_DefaultEmptyDetail). One-line fix: localize via the existing key selectFromList (ar "اختيار من القائمة" / en "Select from list") | fixed — localized _DefaultEmptyDetail via paciPicker.selectFromList (one shared fix, all master-detail screens) |
Calculators verified correct (no defect)
/tools/mortgage— 100k KWD · 15yr · 5% → KWD 790.794/mo, total 142,342.853, interest 42,342.853. ✓ math correct./tools/roi— 150k price · 800 rent · 100 expenses → 6.4% gross · 5.6% net · KWD 8,400/yr. ✓ math correct.
Verified rendering OK (public sweep) — F-005 full-width ballooning applies to all
/public landing · /catalog (data loads) · /tools/rent · /tools/compare · /tools/transactions (after F-006 fix) · /tools/price-map (route ok; map blank = F-007) · /request-valuation (full form) · /about · /services · /contact (modulo F-008). Not individually screenshotted (static legal text, low risk): /terms, /privacy. Secondary public routes not yet swept: /area-guides, /submit-property, /market-indicators, /hedonic-pricing, /combined-valuation.
Authenticated sweep (signed in as admin) — all render, no crashes
Verified rendering correctly (the global F-005 cap applies on every screen): dashboard (greeting, deals pipeline, task/notification tiles, quick actions, 5-branch shell) · brokerage/properties (master-detail, data loads) · brokerage/sales/deals (6 deals + status filter chips, real data) · valuation (workflow-stage filters, list) · finance (revenue chart, invoices/quotations/AR-aging rows) · ai (Hermes chat) · comms (WhatsApp inbox) · settings (security/notifications/appearance/app-mode/sign-out) · admin/users (team list + role badges) · reports/kpi (KPI + closed-deals chart).
Only authenticated defect found: F-009 (shared master-detail empty-state string). No F-006-class crashes on the authenticated side.
Not yet manually re-swept on Flutter Web (2026-06-09): per-record tap-through detail and kanban boards. Mitigated in CI by widget render-smoke tests for all report + WhatsApp sub-screens and by web_routes_parity_test.dart + notification_link_normalization_test.dart for deep links.
Phase 5 — Edge-function deployment audit + .single() review (2026-06-08, updated 2026-06-09)
Edge functions: all healthy. All 19 functions the app invokes are deployed in the Supabase project (jrsgosnnyjonxaesqtln — 114+ functions total), so there is no silent-404 risk from a missing function: register-trusted-device, webauthn-register, webauthn-authenticate, paci-proxy, ai-valuation-analysis, ai-regression, send-email, mailbox-api, ai-marketing-caption, compute-matches, smart-whatsapp-alerts, send-whatsapp-message, get-whatsapp-media-url, generate-property-pdf, generate-valuation-pdf, generate-docx-report, generate-excel-export, whatsapp-ai-replay, whatsapp-reliability-action. Inventory test: test/core/navigation/edge_function_inventory_test.dart. (They can still error at runtime for downstream reasons, but they exist + are reachable; repos wrap calls in try/catch with empty/error fallbacks and the UI shows error states.)
.single(): all safe. Every .single() call in lib/data/repositories/ is on an insert→select→single create path (e.g. .insert(payload).select('id').single()) where exactly one row is guaranteed — none are on fetch paths that could return 0 / many rows. No crash risk found; the earlier MEDIUM flag is cleared.
Phase 2 — Widget tests for untested screens (2026-06-08)
Added widget tests for 64 previously-untested feature screens (~188 new tests), each hermetic via Riverpod provider overrides and verifying the screen's async branches don't crash on render (the class of bug the manual sweep kept surfacing — e.g. F-006). Test shape adapted per screen: golden/empty/error for lists, data/error render-smoke for dashboards, initial/data/error for interactive (forms + streaming AI chats).
Features now screen-tested: tasks, reminders, call_logs, owner_management, smart_alerts, bundles, documents (list+manager+files-dashboard), showcases, agent_actions, contracts (e+smart), matching (matching+request-matches), transactions (explorer+3 analytics), search, appointments, requests (list+detail+form), reports (all 13 doable), whatsapp (all config/list/dashboards), ai (all 8 sub-screens incl. streaming chats), email, onboarding — plus the realtime/auth screens unblocked in the follow-up (notifications, whatsapp inbox + conversation, internal chat list + thread, commissions). Full suite 1045 green, flutter analyze clean.
Reusable patterns (for the remaining/future screens):
- List:
pumpLocalizedApp(tester, const Screen(), overrides:[listProvider(query).overrideWith((ref) async => …)])→pumpAndSettle()→tester.takeException()(drains the AppBar-bottom overflow, F-010) →find.text/find.byType(EmptyState|ErrorState). - Identity-keyed family (no Equatable key): use the family-wide form
provider.overrideWith((ref, key) async => …). appPreferences-derived keys: pin a fixed-state Notifier so SharedPreferences isn't hit and the family key is deterministic.- Streaming AI chats: inject a fake repo via
repoProvider.overrideWithValue(FakeRepo()). If the repo ctor eagerly builds aSupabaseClient, passauthOptions: const AuthClientOptions(autoRefreshToken: false)(or anoSuchMethodstub) to avoid a leaked GoTrue timer that fails test teardown. - Date rows:
setUpAll(() async => initializeDateFormatting('en_GB')).
Unblocked (follow-up — the realtime/auth blocked set is now covered, +18 tests):
- Added
lib/core/auth/auth_providers.dart(currentUserProvider/currentUserIdProvider) and refactoredcommissions_screenoff the directSupabase.instance.client.auth.currentUserread → now testable viacurrentUserIdProvider.overrideWithValue(...). - Added
test/helpers/supabase_test_harness.dart:initSupabaseForTest()(insetUpAll) stands up Supabase with a stub config so realtime-in-initStatescreens mount — with no session, guarded screens (notifications) skip wiring, andresetSupabaseRealtimeForTest()(intearDown) cancels the reconnect timer for screens that wire a channel unconditionally (whatsapp/chat). - Now covered:
notifications,commissions,whatsapp_inbox,whatsapp_conversation,chat/internal_chat_list,chat/internal_chat_thread.
Still deferred: app_shell (not a content screen — exercised indirectly); clients/crm forms (left untouched — concurrent area-picker session owns that lib code).
| F-010 | S4 | mobile/web | screens with a 2-row AldilaijanAppBar bottom (tasks, reminders) | shared app-bar bottom Column overflowed its preferred height by ~4px | lib/core/widgets/app_bar.dart — bottom content (dense search + chip rows) renders a few px taller than the declared preferredSize.height | fixed — both app-bar variants reserve a 6px _bottomSafetyPad in the bottom block; the tasks golden test now renders without draining any overflow |
Phase 3 — Golden snapshot scaffolding (2026-06-08)
Golden tests for the highest-traffic presentational widgets, on the existing RUN_GOLDENS gate + golden_toolkit + goldenWrapper (brand × locale matrix — Aldilaijan/Khobara × ar/en; the app is dark-only, so brand×locale is the meaningful axis, not light/dark):
- pre-existing:
metric_tile_golden_test.dart(3 scenarios) - new:
empty_state,error_state,brand_chip,property_card,valuation_cardgolden tests — 5 files, 20 scenarios. All hermetic (pure presentational widgets, no providers/Supabase).
Without RUN_GOLDENS=1 these run as smoke tests (the widget pumps; screenMatchesGolden is a no-op) — already green in the normal suite. To generate + commit the baseline PNGs (a macOS renderer is required — Windows/Linux baselines diverge):
RUN_GOLDENS=1 flutter test --update-goldens --tags golden # generate on a macOS runner
RUN_GOLDENS=1 flutter test --tags golden # verifyBaselines are intentionally not committed from this (Windows) machine; generate them on a macOS CI lane (e.g. the existing iOS lane) or a Mac, then commit test/**/goldens/*.png. The Linux/Windows flutter test lanes stay green meanwhile (gate off → smoke only).
