How to Test Contact List: A Complete Guide
How to Test Contact List: A Complete Guide
How to Test Contact List: A Complete Guide
Testing a contact list is more than verifying that names appear; it is about ensuring that the core social graph of an application remains reliable, private, and usable under every condition a user might encounter. A faulty contact list can break login flows, prevent messaging, expose personal data, or cause accessibility barriers that drive users away. This guide walks you through a complete, platform‑agnostic testing strategy, from why it matters to a ready‑to‑use checklist, with concrete examples, tables, and code snippets you can apply immediately.
Why Contact List Testing Matters
Direct impact on user goals
When a user opens an app to send a message, start a call, or share a file, the first action is often to pick a recipient from the contact list. If the list fails to load, shows duplicate entries, or hides a contact because of a permission glitch, the user’s goal is blocked instantly. Studies show that a single failure in a primary task flow can increase abandonment rates by up to 20 %.
Business and compliance risks
Beyond frustration, contact list defects can lead to regulatory exposure. Mishandled personal data may violate GDPR, CCPA, or HIPAA, especially if the app inadvertently shares contacts with third‑party services or stores them in plain text. Additionally, crashes or ANRs triggered by list operations generate negative reviews and affect app store rankings.
Ripple effects on other features
Many features depend on the contact list as a data source: invitation flows, referral programs, social sharing, and emergency‑contact features. A defect in the list propagates outward, multiplying the cost of a bug. Testing the list in isolation therefore protects a wide surface area of the application.
Core Concepts: What a Contact List Entails
Data model fundamentals
At its simplest, a contact record includes fields such as firstName, lastName, phoneNumbers (array), emailAddresses (array), postalAddresses, avatar, and a unique identifier (contactId). Platforms may extend this with custom fields like notes, ringtone, or groupMembership. Understanding the schema helps you design validation tests for each field type, length limits, and allowed characters.
UI interactions
Typical interactions include:
- Browse: scrolling through a list or grid, tapping section headers for quick navigation.
- Search: typing a query, applying filters (e.g., show only contacts with phone numbers).
- Select: tapping a contact to view details or to trigger an action (call, message, share).
- Edit/Create: invoking a form to add a new contact or modify existing fields.
- Delete/Archive: removing a contact or moving it to a hidden state.
Each interaction can expose different classes of bugs, from UI rendering issues to data persistence failures.
Platform variations
- Native iOS: Uses
CNContactframework; permissions are handled viaCNContactStore. - Native Android: Relies on
ContactsContract; permissions are runtime‑based (READ_CONTACTS,WRITE_CONTACTS). - Web/Browser: Often accesses contacts through the Credential Management API or relies on backend‑synced data; UI is built with HTML/CSS/JS.
- Hybrid/Cross‑platform: Frameworks like React Native or Flutter bridge to native contacts plugins, adding another layer where mismatches can occur.
Knowing these differences lets you tailor test data (e.g., phone number formats) and tooling (Appium for native, Playwright for web) to the target platform.
Test Matrix for Contact List
Below is a comprehensive matrix that covers happy paths, error paths, edge cases, accessibility, and security. Each row can be turned into a test case ID for manual or automated execution.
| Test ID | Category | Description | Expected Result | Platform Notes |
|---|---|---|---|---|
| CL‑001 | Happy path | Launch app, navigate to contact list, verify list loads within 2 s | List displays contacts, no empty placeholders | All |
| CL‑002 | Happy path | Scroll to bottom of list with 500 contacts | No jank, all contacts rendered, scroll position retained | Performance‑sensitive |
| CL‑003 | Error path | Deny contacts permission, open list | App shows permission rationale UI, no crash | iOS/Android |
| CL‑004 | Error path | Revoke permission while list is visible | List gracefully hides data, shows empty state message | iOS/Android |
| CL‑005 | Edge case | Contact with phone number exceeding 20 digits | Input rejected, validation error shown | All |
| CL‑006 | Edge case | Contact name containing Unicode emojis | Name displays correctly, searchable by emoji | iOS/Android/Web |
| CL‑007 | Edge case | Very long name (200 chars) | UI truncates with ellipsis, full name visible on detail screen | All |
| CL‑008 | Edge case | Contact with no phone or email (only name) | Contact appears, actions like call/message disabled | All |
| CL‑009 | Search | Search for “John” returns all contacts with John in any field | Results sorted alphabetically, no missing matches | All |
| CL‑010 | Search | Search with special characters (@#$%) returns no results, no crash | Empty state shown, search field cleared | All |
| CL‑011 | Create | Add new contact with mandatory fields only | Contact saved, appears in list, editable | All |
| CL‑012 | Create | Attempt to save contact with duplicate phone number | System warns of duplicate, prevents save or offers merge | Android (native) |
| CL‑013 | Edit | Change avatar image, verify persistence after app restart | New avatar displayed, original retained in backup if applicable | iOS/Android |
| CL‑014 | Delete | Swipe‑to‑delete contact, confirm deletion | Contact removed from list, undo toast appears | iOS/Android |
| CL‑015 | Delete | Delete contact while it is open in detail view | Detail view closes, list updates, no dangling reference | All |
| CL‑016 | Permission change mid‑use | Start with permission granted, revoke while typing in search | Search stops, UI shows permission prompt, no exception | iOS/Android |
| Android | ||||
| CL‑017 | Large data set | Load 10 000 contacts via sync | List loads within 5 s, memory usage < 150 MB, scroll smooth | Performance test |
| CL‑018 | Network loss | Disable Wi‑Fi/cellular after list loaded, attempt | All | |
| CL‑019 | Locale change | Switch device language to right‑to‑left (Arabic) | Layout mirrors, text aligns correctly, no clipping | iOS/Android/Web |
| CL‑020 | Accessibility | Navigate list using TalkBack/VoiceOver | Each item announces name, phone, and action hints | iOS/Android |
| CL‑021 | Security | Attempt to read contacts via background service without foreground permission | System blocks read, logs security violation | Android |
| CL‑022 | Security | Share contact via intent, verify only selected fields transmitted | Only name and phone number sent, no extra data | Android |
| CL‑023 | Data integrity | Simulate sync conflict (two edits offline) | Conflict resolution UI appears, user can choose version | iOS/Android |
| CL‑024 | Localization | Contact fields with date of birth in various formats | Dates displayed according to locale, editable correctly | All |
| CL‑025 | Stress | Rapidly add/delete 100 contacts in succession | No crashes, list stays responsive, DB integrity intact | All |
*Use this matrix as a baseline; add platform‑specific rows (e.g., such as testing CNContactFetchRequest batch size on iOS or AsyncTaskLoader behavior on Android.*
Manual Testing Approach
Exploratory testing checklist
Even the most exhaustive automated suite benefits from human intuition. When you explore the contact list manually, focus on the following areas:
- Permission flow – Grant, deny, and toggle permissions while the list is visible, during search, and while editing a contact. Observe whether the UI gracefully degrades or crashes.
- Data entry boundaries – Try maximum lengths, illegal characters, leading/trailing spaces, and varied scripts (Cyrillic, Arabic, Han). Check for truncation, overflow, or incorrect validation.
- Interaction chaining – Start from the list, open a contact detail, edit a field, return to the list, then immediately search for the edited value. Verify that the UI reflects the change without a full reload.
- Gesture variations – On touch devices, test long‑press, swipe‑left/right, double‑tap, and pinch‑to‑zoom where applicable. Ensure that unintended gestures do not trigger actions.
- Interruption simulation – Receive an incoming call, switch to another app, or lock the screen while the list.Confirm that the list returns. After resuming, the list should be in the same state, with no stale selections.
- Accessibility audit – Enable screen‑reader magnification, invert colors, and increase font size. Verify that all touch targets meet the 48 dp minimum and that labels are descriptive.
- Performance observation – Scroll quickly through a large list, flip between tabs, and note any frame drops or memory warnings in device logs.
Tools and techniques
- Device logs (
adb logcat,Console.app) help catch silent exceptions that do not surface as UI errors. - Screenshot comparison tools (e.g., Percy, Applitools) detect visual regressions after a contact‑list UI change.
- Manual test charters – Write a short mission statement like “Verify that deleting a contact while the detail view is open does not leave a ghost entry.” This keeps exploratory sessions focused.
- Session‑based test management – Log each 45‑minute session with a tester name, charter, observations, and bug IDs. Over time you can measure coverage and identify areas that need more automated checks.
Example session
*Charter*: “Validate that the contact list handles a contact with a phone number containing extensions (e.g., +1‑555‑123‑4567 ext. 89) correctly across create, edit, and search flows.”
- Launch the app, grant contacts permission.
- Tap Add Contact, fill first name “Test”, phone
+1-555-123-4567 ext. 89. - Save, return to list – verify the contact appears with the extension visible.
- Open the contact detail, change the extension to
ext. 90, save. - Return to list, search for “ext. 90” – the contact should appear.
- Revoke contacts permission, repeat step 4 – app should show permission rationale and not crash.
- Re‑grant permission, verify the edited contact still shows the updated extension.
This session catches validation, persistence, search indexing, and permission‑handling bugs in one go.
Automated Testing Approaches
Unit tests for the contact model
Validate the core data layer independent of UI.
// JUnit 5 example for Android Contact entity
@Test
void phoneNumber_withExtension_isStoredCorrectly() {
Contact c = new Contact();
c.setFirstName("Ada");
c.setPhoneNumber("+1-555-123-4567 ext. 89");
assertEquals("+1-555-123-4567 ext. 89", c.getPhoneNumber());
// Ensure getter returns trimmed value if required by business rule
assertFalse(c.getPhoneNumber().endsWith(" "));
}
Similar tests in Swift (XCTest) or JavaScript (Jest) cover edge cases like nil fields, maximum length, and Unicode normalization.
UI tests with Appium (native) and Playwright (web)
Automate the flows that users actually perform. Below is a compact Appium Java script that tests the add‑contact happy path and a permission denial scenario.
// Appium Java – Add contact
@Test
public void testAddContact() throws Exception {
// Assume driver is initialized with desiredCapabilities for Android
MobileElement addBtn = driver.findElementById("fab_add_contact");
addBtn.click();
MobileElement firstName = driver.findElementById("input_first_name");
firstName.sendKeys("Lena");
MobileElement phone = driver.findElementById("input_phone");
phone.sendKeys("+49 151 23456789");
MobileElement saveBtn = driver.findElementById("btn_save");
saveBtn.click();
// Verify contact appears in list
MobileElement listItem = driver.findElementByAndroidUIAutomator(
"new UiSelector().textContains(\"Lena\")");
assertTrue(listItem.isDisplayed());
}
// Permission denial test
@Test
public void testContactListWithoutPermission() throws Exception {
// Revoke permission via ADB before launching
DriverHelper.revokePermission("android.permission.READ_CONTACTS");
driver.launchApp();
MobileElement contactTab = driver.findElementByAccessibilityId("Contacts");
contactTab.click();
MobileElement emptyState = driver.findElementById("txt_empty_state");
assertEquals("Grant contact access to see your contacts", emptyState.getText());
}
For web contacts (e.g., a CRM that loads contacts from an API), Playwright offers a concise alternative:
// Playwright test – search and select contact
test('search returns matching contact', async ({ page }) => {
await page.goto('https://app.example.com/contacts');
await page.fill('#search-box', 'Maria Garcia');
await page.waitForSelector('tbody tr:has-text("Maria Garcia")');
const row = page.locator('tbody tr:has-text("Maria Garcia")');
await expect(row).toBeVisible();
await row.click();
await expect(page.locator('detail-panel')).toContainText('Maria Garcia');
});
Both frameworks allow you to parameterize data sets (CSV, JSON) to run the same script with many variations (long names, special characters, empty fields).
API tests for contact sync
If your app synchronizes contacts with a backend, test the contract directly.
# curl example – create contact via REST API
curl -X POST https://api.example.com/v1/contacts \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Yuki",
"lastName": "Tanaka",
"phoneNumbers": ["+81 90-1234-5678"],
"emails": ["yuki.tanaka@example.com"]
}' | jq .
Assert that the response 201 Created contains the same fields and that a subsequent GET /contacts returns the newly created record. Add negative tests for missing required fields, invalid phone formats, and oversized payloads.
Data‑driven and property‑based testing
Use tools like jqwik (Java) or hypothesis (Python) to generate random yet valid contact objects and assert invariants such as:
- The
contactIdis never null after persistence. - Searching by any non‑empty substring of a field returns the contact (if the substring matches).
- The list order is stable when no sort criteria change.
These techniques uncover edge cases that manual example‑based tests might miss, like a name consisting solely of combining characters.
Edge Cases That Only Appear in Production
Network interruptions mid‑operation
A user may start editing a contact, then lose connectivity before hitting save. The app should either queue the change for later sync or revert to the last known good state, never leaving a half‑written record in the local DB. Test by toggling airplane mode during a save flow and observing the UI and DB state after reconnection.
Permission changes while the UI is active
On Android, a user can disable READ_CONTACTS from Settings while your activity is in the foreground. The system will not automatically pause your UI; you must handle SecurityException when accessing the ContentResolver. Verify that your code catches the exception, shows a permission rationale, and does not crash.
Contact merging from multiple sources
Modern smartphones aggregate contacts from Google, Exchange, SIM, and local accounts. When two sources provide conflicting data (different phone numbers for the same name), the OS may present a unified view or keep them separate. Your app must decide whether to show the merged view, preserve source labels, or allow the user to choose. Test by adding a contact via Google sync, then editing the same contact locally, and confirming the final representation matches your merge policy.
Locale and formatting issues
Phone number formats vary wildly: +1 (555) 123-4567 in the US, 020 7946 0958 in the UK, +61 2 9876 5432 in Australia. If your app displays numbers using a hard‑coded mask, you will see misplaced parentheses or missing spaces in certain locales. Test by setting the device language to a region with a distinct format and confirming that the displayed number adheres to the local pattern (use PhoneNumberUtil from libphonenumber for verification).
Large contact list performance
Devices with limited RAM may struggle when the app loads thousands of contacts into a RecyclerView or ListView without pagination. Monitor GPU overdraw, frame time, and memory churn using Android Studio Profiler or Instruments. Implement lazy loading with Paging 3 or UITableViewDiffableDataSource and assert that 95th‑percentile scroll latency stays under 16 ms.
Background sync conflicts
Suppose the user edits a contact offline while a background sync pushes a conflicting update from the server. The app must surface a conflict resolution dialog rather than silently overwriting one version. Simulate this by disabling network, editing a contact, re‑enabling network, and injecting a mock server update with a newer timestamp. Verify the UI presents a choice and that the selected version persists.
Each of these scenarios is unlikely to surface in a clean test lab but can cause data loss, crashes, or privacy violations in the wild.
Accessibility and WCAG Considerations
Screen‑reader labels
Every list item must have an accessible name that conveys the essential info. For a contact, a good label is "{firstName} {lastName}, {phoneType}: {phoneNumber}". Avoid relying solely on visual cues; test with TalkBack (Android) and VoiceOver (iOS) to confirm that double‑tapping an item opens the detail view.
Touch target size
Minimum recommended size is 48 dp × 48 dp (≈9 mm). Use the Android ViewTreeObserver.OnGlobalLayoutListener or iOS UIView frame measurements to assert that each row’s hit‑test area meets this threshold. A common mistake is making the avatar image the only tappable part; ensure the entire row is clickable.
Color contrast
Text and icons must contrast at least 4.5:1 against the background (WCAG AA). Run automated contrast checks (e.g., axe-core for web, Accessibility Scanner for Android) on various themes, including dark mode.
Keyboard navigation
For web or desktop‑style contact lists, ensure that Tab moves focus between rows, Enter activates the selected row, and arrow keys allow scrolling without a mouse. Verify that focus rings are visible and that custom JavaScript does not trap focus.
Reduced motion
Users who prefer reduced motion may experience discomfort with animated list reorders. Respect the prefers-reduced-motion media query or Android’s isAccessibilityFeatureEnabled(AccessibilityManager.FEATURE_DYNAMIC_ANIMATIONS) and disable or simplify animations accordingly.
By integrating these checks into your CI pipeline (e.g., running axe on web builds, using AccessibilityTestFragment for Android), you catch regressions before they reach users.
Security and Privacy Testing
Permission handling
Confirm that the app requests READ_CONTACTS and WRITE_CONTACTS only when needed, and that it explains why (runtime rationale). On iOS, verify that the usage description in Info.plist is clear and that the system prompt appears exactly once per session unless the user changes Settings.
Data leakage
Ensure that contacts are never logged inadvertently. Search logcat or console output for strings containing phone numbers or email addresses after performing a contact‑list operation. Use tools like Logcat filters (grep -i "contact") to spot accidental leaks.
Sharing via intents (Android)
When the user taps “Share contact,” the app creates an Intent with ACTION_SEND and a vCard MIME type. Verify that the intent extras contain only the fields the user opted to share (e.g., name and phone numbers) and that no extra metadata like _id or rawContactId leaks. You can capture the intent with adb shell am start -S and inspect the extras.
Encryption at rest
If the app caches contacts locally (e.g., in a Room database or SharedPreferences), confirm that the data is encrypted using a strong algorithm (AES‑256‑GCM) and that the key is stored in the Android Keystore or iOS Keychain. Attempt to pull the data via adb backup or a jailbroken filesystem and verify that it is unintelligible without the key.
API security
For endpoints that return contact data, enforce OAuth2 scopes and rate limiting. Test that an unauthorized request receives 401 or 403, and that a token with insufficient scope cannot read the phoneNumbers field. Use tools like OWASP ZAP to perform active scans on the contact‑related endpoints.
Third‑party contact plugins
If you rely on a native plugin (e.g., react-native-contacts), review its source for over‑permission requests. Some plugins request both READ_CONTACTS and WRITE_CONTACTS even when the app only reads. Audit the plugin’s manifest and update to a fork that minimizes permissions.
Persona‑Driven Autonomous Exploration with SUSA
How SUSA simulates different user personas
SUSA’s autonomous agent does not rely on pre‑written scripts. Instead, it loads a behavior profile for each persona—curious, impatient, novice, adversarial, elderly, accessibility‑focused, power user, and more. Each profile defines tap frequency, scroll velocity, likelihood to use voice input, tolerance for error messages, and propensity to explore settings menus. When pointed at an app’s contact list (via APK or URL), SUSA spawns virtual users that exercise the UI in ways a manual tester might not think to try.
For example, the impatient persona rapidly taps the search bar after every character, testing debounce logic and ensuring that the UI does not crash when the query changes faster than the data source can respond. The accessibility persona enables TalkBack and navigates solely via swipe gestures, exposing missing content descriptions or focus‑order problems. The adversarial persona attempts to input SQL‑like strings (' OR 1=1 --) into name fields to probe for injection vulnerabilities in local storage or sync APIs.
Finding bugs scripts miss
Because SUSA explores state space stochastically, it can reach combinations that are rare in scripted suites. In one internal run on a messaging app, the elderlycuriously opens every context menu and then backs out) triggered a hidden bug where long‑pressing a contact’s avatar opened a modal that, upon dismissal, left the underlying RecyclerView in an inconsistent state, causing a IndexOutOfBoundsException on the next scroll. No existing automated test covered the long‑press‑avatar flow followed by a back navigation, so the bug escaped detection until SUSA reported it.
The agent also logs each action with timestamps, enabling you to replay a failing session step‑by‑step in a debugger or to add the exact sequence to your regression suite.
Example of a persona‑driven flow
Consider the novice persona, which prefers big buttons and avoids gestures like swipe‑to‑delete. SUSA will:
- Launch the app and wait for the main screen.
- Tap the “Contacts” tab (identified by accessibility label).
- Scroll slowly (200 ms per item) until it sees a contact with a phone icon.
- Tap the contact’s name to open the detail view.
- Locate the “Edit” button (large, bottom‑aligned) and tap it.
- Change the first name field to a string of 150 ‘A’ characters.
- Tap the “Save” button (wide, high‑contrast).
- Return to the list and verify that the contact appears with the truncated name (if the app imposes a limit) or that the full name is shown (if the limit is higher).
If the app crashes at step 6 due to a buffer overflow in the native contact‑store bridge, SUSA captures the stack trace and flags the issue as high severity.
By running SUSA nightly on a device farm, you gain continuous feedback on how real‑world users—beyond the idealized tester—interact with your contact list.
Cross‑Session Learning and Regression Script Generation
How SUSA builds knowledge over runs
Susa stores a graph of visited screens and transitions after each execution. Nodes represent UI states (e.g., “Contact list – showing 23 items”), edges represent actions (tap, swipe, type). When a run encounters a dead end (e.g., a button that leads to a toast with no further navigation), the agent marks that edge as low‑value and avoids repeating it in future sessions unless the app changes. Conversely, paths that consistently lead to new states or discover anomalies are reinforced. Over time, the agent focuses its exploration on under‑tested areas, increasing the likelihood of finding regressions introduced by recent code changes.
Generating Appium (Android) + Playwright (Web) scripts
After a successful run, Susa can export the traversed paths as executable test scripts. For Android, it produces an Appium Java test that uses the same element locators (by accessibility id, text, or resource‑id) that the agent observed. For web, it outputs a Playwright TypeScript script that replicates the exact sequence of clicks, fills, and navigations.
Example snippet of an generated Appium test (truncated for brevity):
@Test
public void generatedContactListFlow() throws Exception {
// Launch app
driver.launchApp();
// Tap Contacts tab (by accessibility label)
new WebDriverWait(driver, 10)
.until(ExpectedConditions.elementToBeClickable(
MobileBy.AccessibilityId("Contacts")))
.click();
// Scroll to find contact "John Doe"
UiScrollable scrollable = new UiScrollable(new UiSelector()
.scrollable(true));
scrollable.scrollIntoView(
new UiSelector().textStartsWith("John Doe"));
// Tap contact
driver.findElementByAndroidUiAutomator(
"new UiSelector().text(\"John Doe\")")
.click();
// Tap Edit button (resource id)
driver.findElementById("btn_edit_contact")
.click();
// Edit first name field
MobileElement firstName = driver.findElementById("input_first_name");
firstName.clear();
firstName.sendKeys("Jonathan");
// Tap Save
driver.findElementById("btn_save_contact")
.click();
// Verify name appears in list
driver.findElementByAndroidUiAutomator(
"new UiSelector().textContains(\"Jonathan\")");
}
For web, the generated Playwright script mirrors the same flow using CSS selectors or ARIA labels.
Using generated scripts in CI
Commit the exported scripts to your repository under a folder like tests/susa-generated/. Add a step in your CI pipeline that runs these scripts on every pull request against a staging build. Because the scripts are derived from actual exploratory behavior, they act as a living regression suite that evolves with the app. If a subsequent change breaks a path that Susa previously explored, the corresponding generated test will fail, alerting you before the defect reaches production.
You can also configure Susa to prune scripts that become obsolete (e.g., when a screen is removed) and to regenerate them on a schedule, ensuring the suite stays in sync with the UI.
Checklist for Contact List Testing
Pre‑release checklist
- [ ] Verify that the contact list loads within the defined performance budget (e.g., < 2 s for ≤ 500 contacts, < 5 s for > 5 k).
- [ ] Confirm that granting, denying, and toggling contacts permission never crashes the app.
- [ ] Validate that all required fields (first/last name, at least one phone or email) are enforced, and that optional fields accept blanks.
- [ ] Ensure that search returns correct results for every searchable field, including Unicode and emojis.
- [ ] Check that creating a contact with maximum‑length fields does not truncate unexpectedly (unless intended).
- [ ] Confirm that editing a contact persists after app restart and after a background sync.
- [ ] Verify that deleting a contact removes it from all synchronized accounts (if applicable).
- [ ] Validate that swipe‑to‑delete or long‑press‑menu actions are undoable within the prescribed time window.
- [ ] Ensure that the list remains stable (no duplicate entries, no phantom items) after rapid add/delete cycles.
- [ ] Run accessibility audits: screen‑reader navigation, touch target ≥ 48 dp, color contrast ≥ 4.5:1, keyboard focus visible.
- [ ] Perform security sanity checks: no contact data in logs, intent shares only selected fields, local DB encrypted, API endpoints require proper scopes.
- [ ] Execute at least one persona‑driven Susa session (curious + impatient) and confirm no new high‑severity bugs are reported.
Production‑only monitoring checklist
- [ ] Instrument crash reporting for
NullPointerExceptionin contact‑adapter orSQLiteExceptionin local DB. - [ ] Monitor permission‑denial events via analytics; watch for spikes after an OS update.
- [ ] Track average list scroll jank (frame‑time > 16 ms) per session; set alert if > 5 % of frames exceed threshold.
- [ ] Log sync conflict resolutions and ensure users are presented with a choice > 90 % of the time.
- [ ] Alert on any outbound network request that contains a raw phone number or email address outside of the vetted share‑intent flow.
-
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free