How to Write Test Cases for Pull To Refresh (With Examples)
How to Write Test Cases for Pull To Refresh (With Examples)
How to Write Test Cases for Pull To Refresh (With Examples)
Pull‑to‑refresh is a ubiquitous interaction pattern that lets users trigger a data reload by dragging down on a scrollable view and releasing. While the gesture feels simple, the underlying mechanics involve touch handling, animation coordination, network requests, and UI state updates. A missing or flaky refresh can lead to stale data, user frustration, or even crashes when the gesture interferes with ongoing animations or background work. Therefore, a well‑designed test suite that covers positive, negative, edge, and boundary scenarios is essential for delivering a reliable experience.
This guide walks you through a complete test‑case workflow: defining the anatomy of a test case, building a concrete matrix of over 20 examples, setting up test data, prioritizing by risk, and executing the cases both manually and with automation. Throughout, we show how autonomous exploration tools—such as the SUSA platform—can supplement hand‑written cases by surfacing unexpected interactions that only appear in production‑like conditions.
---
How to Write Test Cases for Pull To Refresh (With Examples)
Understanding Pull To Refresh Mechanics
At its core, pull‑to‑refresh consists of three phases:
- Drag – The user moves a finger downward, translating the view’s content offset. The UI typically shows a visual cue (e.g., an arrow) that changes as the drag distance passes a threshold.
- Release – When the finger lifts, if the drag distance exceeds the release threshold, the framework triggers a refresh callback, shows a progress indicator, and may lock further scrolling until the refresh completes.
- Reset – After the data fetch finishes (or fails), the indicator hides and the view snaps back to its resting position, allowing normal scrolling again.
Each phase can be influenced by device‑specific touch latency, frame‑rate throttling, accessibility services that inject synthetic gestures, or concurrent animations (e.g., a bottom sheet opening). Test cases must therefore probe not only the ideal path but also variations in timing, distance, and system load.
Why Test Cases Matter
A test case is more than a checklist item; it is a traceable artifact that links a requirement (e.g., “user can refresh feed”) to observable behavior. By documenting preconditions, steps, and expected results, you create a repeatable verification unit that can be executed manually, integrated into CI pipelines, or fed to an autonomous test agent. High‑signal cases catch regressions early, reduce reliance on ad‑hoc exploratory testing, and provide clear evidence when a defect surfaces in production.
---
Anatomy of a Pull To Refresh Test Case
Test Case ID and Naming Convention
Use a stable identifier that encodes the feature area, scenario type, and a sequential number. Example: PTR-POS-001 (Pull‑To‑Refresh, Positive, case 001). Prefixes help you filter test‑management tools and trace back to requirement IDs such as REQ-REFRESH-02.
Preconditions
List the exact state the app must be in before the first step. For pull‑to‑refresh this often includes:
- The screen displaying a scrollable list (e.g., a RecyclerView or FlatList).
- The list populated with at least N items (so scrolling is possible).
- Network mock configured to return a known payload within a defined latency.
- No ongoing refresh animation or modal dialog blocking the gesture.
- Device orientation set (portrait/landscape) if the test is orientation‑specific.
Being explicit prevents false positives caused by stray state from previous tests.
Test Steps
Write each action as an imperative, atomic instruction. Avoid bundling multiple actions in a single step unless they are inseparable (e.g., “drag down 80 dp and release”). Number steps sequentially and reference any helper methods or test‑data identifiers.
Expected Result
Define the observable outcome after the final step. This should be measurable: UI element visibility, text content, network call count, or animation state. Use present‑tense language (“the refresh indicator appears”, “the list shows the new items”, “no crash occurs”).
Postconditions / Cleanup
If the test leaves the app in a non‑idle state (e.g., a refresh indicator still visible), add a cleanup step to return to a known baseline. This could be a forced navigation back, a manual dismissal of a dialog, or a call to resetMockServer().
---
Positive Test Cases for Pull To Refresh
| ID | Preconditions | Steps | Expected Result |
|---|---|---|---|
| PTR-POS-001 | List with 20 items displayed; network mock returns 20 new items in 500 ms | 1. Place finger on first list item. 2. Drag down 70 dp. 3. Release. | Refresh indicator appears; network request triggered; after 500 ms indicator hides; list shows 40 items (original + new). |
| PTR-POS-002 | List empty; network mock returns empty array in 200 ms | 1. Drag down 60 dp. 2. Release. | Indicator appears; request sent; after 200 ms indicator hides; empty‑state message remains visible. |
| PTR-POS-003 | List with 10 items; network mock returns 5 items with a 2‑second delay | 1. Drag down 80 dp. 2. Release. 3. Wait 2 seconds. | Indicator shows spinner; after delay indicator hides; list shows 15 items; no UI freeze. |
| PTR-POS-004 | List scrolled halfway down; network mock returns updated timestamps | 1. Scroll up to top. 2. Drag down 55 dp. 3. Release. | Indicator appears; request sent; after response list items display updated timestamps; scroll position resets to top. |
| PTR-POS-005 | List with 30 items; accessibility service (TalkBack) enabled | 1. Enable TalkBack. 2. Drag down 65 dp. 3. Release. | Indicator appears; request succeeds; TalkBack announces “refreshing” then “refresh complete”. |
| PTR-POS-006 | Device in landscape orientation; list with 12 items | 1. Rotate device to landscape. 2. Drag down 58 dp. 3. Release. | Indicator appears; request completes; list shows new items; UI remains usable in landscape. |
| PTR-POS-007 | List with 5 items; network mock returns error after 1 second (simulated) | 1. Drag down 70 dp. 2. Release. 3. Wait for error response. | Indicator appears; after 1 s indicator hides; error toast displayed; list unchanged. |
| PTR-POS-008 | List with 15 items; pull distance just below threshold (45 dp) | 1. Drag down 45 dp. 2. Release. | No indicator; list stays stationary; no network request. |
| PTR-POS-009 | List with 20 items; rapid double pull (two gestures < 300 ms apart) | 1. Drag down 60 dp and release. 2. Immediately drag down 60 dp and release again. | First request starts; second gesture ignored while indicator visible; after first completes, list updates; no duplicate request. |
| PTR-POS-010 | List with 0 items; pull to refresh with disabled network (airplane mode) | 1. Enable airplane mode. 2. Drag down 65 dp. 3. Release. | Indicator appears; request fails after timeout; indicator hides; offline‑error banner shown; list stays empty. |
*Notes*: The distance values (dp) are illustrative; adjust according to your UI’s trigger threshold (commonly 50‑80 dp). Use a mocking library (e.g., MockWebServer, MSW) to control latency and payload.
---
Negative Test Cases for Pull To Refresh
| ID | Preconditions | Steps | Expected Result |
|---|---|---|---|
| PTR-NEG-001 | List with 10 items; network mock configured to delay response 10 s | 1. Drag down 70 dp. 2. Release. 3. Wait 2 seconds (before timeout). | Indicator visible; UI remains responsive (user can still scroll other parts if allowed); no crash. |
| PTR-NEG-002 | List with 5 items; system under high CPU load (background stress app) | 1. Start CPU‑stress app. 2. Drag down 65 dp. 3. Release. | Indicator appears; request eventually completes; UI does not freeze for >200 ms frames. |
| PTR-NEG-003 | List with 12 items; accessibility service that injects a swipe‑up gesture | 1. Enable service that sends occasional swipe‑up. 2. Perform normal pull‑to‑refresh. | Indicator appears; request completes; injected gesture does not cancel or duplicate the refresh. |
| PTR-NEG-004 | List with 0 items; pull gesture interrupted by incoming call | 1. Initiate pull‑down. 2. Simulate incoming call (adb shell am broadcast ...). 3. Accept call. | After call ends, app returns to foreground; no refresh indicator stuck; list unchanged. |
| PTR-NEG-005 | List with 15 items; network mock returns malformed JSON | 1. Drag down 60 dp. 2. Release. 3. Receive malformed payload. | Indicator appears; after response indicator hides; error toast shown; list unchanged; no crash. |
| PTR-NEG-006 | List with 8 items; pull performed while a bottom sheet is animating open | 1. Trigger bottom sheet open animation. 2. While animation runs, drag down 55 dp and release. | Indicator does not appear; request not sent; bottom sheet completes; UI stays stable. |
| PTR-NEG-007 | List with 20 items; pull distance exceeds maximum allowed (150 dp) | 1. Drag down 160 dp. 2. Release. | Indicator may appear (if UI clamps) but request sent only once; no visual glitch or overscroll bounce. |
| PTR-NEG-008 | List with 6 items; device rotated mid‑pull | 1. Start pull‑down in portrait. 2. Rotate to landscape while finger still down. 3. Release. | Indicator appears (if still past threshold); request sent; after completion UI updates correctly in new orientation. |
| PTR-NEG-009 | List with 0 items; pull performed while keyboard is visible (search bar) | 1. Focus search bar, keyboard shown. 2. Drag down 55 dp. 3. Release. | Indicator appears; request sent; keyboard remains; after response list updates; no input loss. |
| PTR-NEG-10 | List with 12 items; pull performed during a system‑wide animation scale change | 1. Open developer options → animation scale 0.5x. 2. Perform pull‑to‑refresh. | Indicator appears; request completes; animation speed respects the scale; no timing‑related flakiness. |
---
Edge and Boundary Cases for Pull To Refresh
| ID | Preconditions | Steps | Expected Result |
|---|---|---|---|
| PTR-EDG-001 | List with 100 items; pull performed with varying finger speed (slow vs fast) | 1. Perform slow drag (≈30 30 dp/s). 2. Release. | Indicator appears; request sent. |
| PTR-EDG-px/s) to threshold and release. 2. Repeat with fast drag (≈200‑px/s). | Indicator appears in both cases; request timing consistent; no missed detection due to velocity. | ||
| PTR-EDG-002 | List with 5 items; pull initiated from middle of list (not topmost visible item) | 1. Scroll list halfway down. 2. Drag down on visible item (not first). 3. Release. | No indicator; gesture treated as regular scroll; list moves accordingly; no refresh triggered. |
| PTR-EDG-003 | List with 0 items; pull performed while a system‑wide dark‑mode toggle animates | 1. Toggle dark mode via system settings (animation). 2. During animation, drag down and release. | Indicator appears; request sent; UI updates with correct color theme after animation ends. |
| PTR-EDG-004 | List with 20 items; pull performed on a device with high touch‑sampling rate (240 Hz) | 1. Connect to 240 Hz device. 2. Perform pull‑to‑refresh. | Indicator appears; request sent; UI smooth; no jitter caused by oversampling. |
| PTR-EDG-005 | List with 8 items; pull performed while a background music service changes audio focus | 1. Start music playback. 2. Receive focus loss notification (e.g., call). 3. During loss, drag down and release. | Indicator appears; request completes; audio focus returns; no interference. |
| PTR-EDG-006 | List with 15 items; pull performed immediately after app launch (cold start) | 1. Kill app process. 2. Launch app. 3. Immediately drag down and release. | Indicator appears; request uses cached config if available; after response list shows data; no crash. |
| PTR-EDG-007 | List with 12 items; pull performed with multiple fingers (two‑finger drag) | 1. Place two fingers on list. 2. Drag down together. 3. Release. | System treats as scroll; no indicator; no request; gesture recognized as multi‑touch scroll. |
| PTR-EDG-008 | List with 20 items; pull performed while a system‑wide font size change is in progress | 1. Open accessibility → font size slider. 2. Begin dragging slider. 3. While slider moves, perform pull‑to‑refresh. | Indicator appears; request sent; after both actions complete UI reflects new font size and refreshed data. |
| PTR-EDG-009 | List with 0 items; pull performed with battery saver mode restricting background network | 1. Enable battery saver. 2. Drag down and release. | Indicator appears; request may be delayed or throttled; eventual timeout shows offline message; UI stable. |
| PTR-EDG-010 | List with 30 items; pull performed while another pull‑to‑refresh is already in progress (nested) | 1. Start first pull (indicator visible). 2. Before completion, start second pull gesture. | Second gesture ignored; indicator stays visible; after first completes list updates; no duplicate request. |
---
Data Setup and Test Environment
Mock Backend Strategies
A reliable test suite depends on deterministic network responses. Use a local mock server that can:
- Encode latency (fixed or random).
- Return varied payloads (full refresh, empty list, error codes, malformed JSON).
- Simulate network loss or throttling.
For Android, MockWebServer from OkHttp integrates cleanly with Retrofit or Volley. For web, tools like MSW (Mock Service Worker) or Cypress’s cy.intercept() provide comparable control.
Test Data Seeding
When the refresh endpoint relies on server‑side state (e.g., pagination token), seed the mock with a known cursor value. After each refresh, advance the cursor in the mock so the next call returns the next page. This lets you verify that pull‑to‑refresh correctly appends rather than replaces data.
Device/Emulator Configurations
Run the matrix across a matrix of:
- API levels (21, 28, 30, 33) to catch framework‑level behavior changes.
- Screen densities (mdpi, hdpi, xhdpi, xxhdpi).
- Orientation (portrait, landscape).
- Accessibility settings (TalkBack, Switch Control, font scaling).
Automate this via Gradle’s connectedAndroidTest with testInstrumentationRunnerArguments or via a device farm.
Using SUSA for Autonomous Exploration
SUSA can be pointed at the built APK (or a web URL) to exercise the app without scripts. During an exploratory run, SUSA’s built‑in personas (e.g., “impatient user” who performs quick, short drags) will naturally generate pull‑to‑refresh gestures at varying speeds and lengths. The platform records any crashes, ANRs, or accessibility violations that occur during those gestures, surfacing edge cases that might be missed in a scripted suite. After the exploratory phase, you can export the observed flows as Appium or Playwright regression scripts, then integrate them into your CI pipeline for continuous validation.
---
Prioritization and Traceability
Mapping Test Cases to Requirements
Create a simple traceability table that links each requirement ID to the test cases that verify it. Example:
| Requirement ID | Description | Linked Test Cases |
|---|---|---|
| REQ-REFRESH-01 | User can trigger refresh by dragging down | PTR-POS-001, PTR-POS-002, PTR-POS-003 |
| REQ-REFRESH-02 | Refresh shows indicator while loading | PTR-POS-001, PTR-POS-004, PTR-NEG-001 |
| REQ-REFRESH-03 | Updated data appears after successful load | PTR-POS-001, PTR-POS-003, PTR-POS-005 |
| REQ-REFRESH-04 | Error state handled gracefully | PTR-POS-007, PTR-NEG-005, PTR-NEG-004 |
| REQ-REFRESH-05 | Gesture ignored when indicator visible | PTR-POS-009, PTR-EDG-010 |
| REQ-REFRESH-06 | Works in all supported orientations | PTR-POS-006, PTR-NEG-008 |
| REQ-REFRESH-07 | Compatible with accessibility services | PTR-POS-005, PTR-NEG-003 |
| REQ-REFRESH-08 | Stable under device performance variations | PTR-NEG-002, PTR-EDG-001, PTR-EDG-004 |
| REQ-REFRESH-09 | Resistant to interruptions (calls, rotations) | PTR-NEG-004, PTR-NEG-008 |
| REQ-REFRESH-10 | Does not conflict with other UI animations | PTR-NEG-006, PTR-EDG-003, PTR-EDG-008 |
Having this table lets you assess coverage: if a requirement lacks linked tests, you know where to add cases.
Risk‑Based Prioritization Matrix
Assign each test case a risk score based on impact (user‑visible severity) and likelihood (chance of failure given code changes). Use a 3×3 grid (Low/Medium/High) to decide execution order.
| Impact \ Likelihood | Low | Medium | High |
|---|---|---|---|
| Low | PTR-EDG-006, PTR-EDG-009 | PTR-POS-008, PTR-NEG-008 | PTR-POS-001, PTR-POS-002 |
| Medium | PTR-EDG-002, PTR-EDG-005 | PTR-POS-004, PTR-NEG-001 | PTR-POS-003, PTR-POS-005 |
| High | PTR-NEG-004, PTR-NEG-007 | PTR-NEG-002, PTR-NEG-005 | PTR-POS-006, PTR-POS-007, PTR-NEG-003 |
Execute High‑High cases first in every build; Medium‑Medium can run nightly; Low‑Low may be reserved for weekly full‑suite runs.
---
Manual vs Automated Execution
Manual Test Execution Checklist
- Setup – Launch the app on a physical device or emulator with the desired preconditions (list populated, mock server running).
- Pre‑check – Verify that no refresh indicator is visible and the list is at the expected scroll position.
- Execute Steps – Follow the numbered steps from the test case, using a finger or stylus to perform the drag.
- Observe – Watch for the indicator, listen for accessibility announcements, and check the list content after the expected delay.
- Validate – Compare against the expected result; note any deviations, crashes, or ANRs.
- Cleanup – If the test leaves the app in a non‑idle state (e.g., indicator still visible), back out or force‑stop the app to return to a clean baseline.
- Record – Log pass/fail, device details, and any observations (e.g., “indicator flickered on low‑end device”).
Repeat for each case, varying device configurations as needed.
Automated Test Script Example (Espresso)
@RunWith(AndroidJUnit4::class)
class PullToRefreshTest {
private lateinit var mockWebServer: MockWebServer
@Before
fun setUp() {
mockWebServer = MockWebServer()
mockWebServer.start()
// Inject base URL into app via Dagger or BuildConfig
App.injectMockServer(mockWebServer.url("/refresh"))
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun pullToRefresh_showsIndicatorAndUpdatesList() {
// GIVEN: list with 20 items
onView(withId(R.id.recyclerView))
.check(matches(hasMinimumChildCount(20)))
// WHEN: drag down 70dp and release
val recyclerView = onView(withId(R.id.recyclerView))
recyclerView.perform(
swipeDown() // Espresso’s built‑in swipeDown uses velocity to cross threshold
)
// THEN: indicator appears
onView(withId(R.id.refresh_progress))
.check(matches(isDisplayed()))
// AND: mock receives a GET request
mockWebServer.takeRequest(5, TimeUnit.SECONDS)?.respond {
setResponseCode(200)
setBody(mockResponseWithNewItems())
}
// AND: after delay indicator hides and list grows
onView(withId(R.id.refresh_progress))
.check(matches(not(isDisplayed())))
onView(withId(R.id.recyclerView))
.check(matches(hasMinimumChildCount(40))) // original + new
}
}
*Notes*:
swipeDown()uses Espresso’sGeneralSwipeActionwith configurable speed and press duration; adjustpressDurationorswipePercentif your trigger threshold differs.IdlingResourcecan be registered to wait for network idle before asserting UI changes, preventing flaky checks.
Automated Test Script Example (Playwright for Web)
const { test, expect } = require('@playwright/test');
test.describe('Pull-to-refresh web component', () => {
test.beforeEach(async ({ page }) => {
// Mock the API endpoint
await page.route('**/api/feed', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: Array.from({ length: 20 }, (_, i) => ({ id: i, title: `Item ${i}` })) })
});
});
await page.goto('http://localhost:3000/feed');
});
test('shows spinner and loads new items on pull', async ({ page }) => {
// Initial count
await expect(page.locator('.feed-item')).toHaveCount(20);
// Perform pull-down: move to top, drag down 120px, release
const handle = await page.locator('.feed-container');
await handle.hover({ position: { x: 10, y: 10 } });
await page.mouse.move(10, 10);
await page.mouse.down();
await page.mouse.move(10, 130, { steps: 20 }); // drag down
await page.mouse.up();
// Spinner visible
await expect(page.locator('.refresh-spinner')).toBeVisible();
// Fulfill mock with new data
await page.route('**/api/feed', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: Array.from({ length: 30 }, (_, i) => ({ id: i + 20, title: `Item ${i}` })) })
});
});
// Wait for spinner to disappear
await expect(page.locator('.refresh-spinner')).toBeHidden();
// New total count
await expect(page.locator('.feed-item')).toHaveCount(30);
});
});
*Notes*:
- Playwright’s
mouse.movewithstepscreates a smooth drag that mimics a finger. Adjust the pixel distance based on your CSS threshold. - The test leverages route mocking to control latency and payload; you can add
await page.waitForTimeout(500)to simulate delayed responses if needed.
Leveraging SUSA CLI for Regression
After an exploratory run, SUSA can generate a baseline Appium script:
susatest agent explore --app my-app.apk --output ./susatest-baseline
susatest agent generate --format appium --output ./regression-tests
You can then commit the generated *.java or *.js files to your repository and run them in CI alongside hand‑written cases. Because Susa’s personas include “adversarial” and “elderly” profiles, the produced scripts often stress the gesture with atypical speeds, multi‑finger attempts, or prolonged holds—exactly the variations that surface only after extended field usage.
---
Real‑World Production Edge Cases
Even the most thorough matrix can miss issues that appear only under specific production conditions. Below are patterns observed in live logs from several apps that implemented pull‑to‑refresh:
| Observation | Root Cause | Mitigation |
|---|---|---|
| Indicator stuck after device rotation | The fragment retaining the SwipeRefreshLayout was not retained across configuration changes, causing the UI to lose reference to the progress view. | Use viewBinding or Fragment scoped SwipeRefreshLayout and call setRefreshing(false) in onViewStateRestored. |
| Duplicate network requests on fast double‑pull | The app’s gesture listener did not disable further drags while a refresh was in progress; a second gesture triggered a second request before the first completed. | Set a flag isRefreshing and ignore additional drags until the flag is cleared. |
| ANR on low‑end devices when pulling during heavy bitmap decode | The UI thread was blocked decoding a large image from the list item while the gesture handler tried to invalidate the layout. | Move image decoding off the main thread (e.g., use Glide with appropriate downsampling) and keep the gesture handler lightweight. |
| Accessibility service TalkBack announces “refreshing” twice | Both the layout’s built‑in accessibility announcement and a custom ContentDescription update were firing. | Rely solely on the layout’s default announcements; suppress custom updates during refresh. |
| Pull‑to‑refresh ignored when a bottom sheet is expanded | The bottom sheet intercepted touch events via setFilterTouchesWhenObscured(true). | Ensure the sheet does not consume the gesture or forward it to the underlying view when the sheet’s peek height is zero. |
| Offline banner flashes then disappears instantly | The app showed an offline banner on network error but immediately dismissed it after a timeout, not waiting for the user to acknowledge. | Keep the banner visible until user action or network recovery. |
| List jumps to top after refresh on tablets in multi‑window mode | The layout’s setPaddingTop was being reset incorrectly when the window changed size. | Use WindowInsets APIs to adjust padding dynamically instead of hardcoding values. |
| Crash on devices with Android 12 when pulling while the app is in picture‑in‑picture mode | The SwipeRefreshLayout attempted to animate a view that had been detached from the window. | Guard refresh calls with if (view.isAttachedToWindow) or disable pull‑to‑refresh when PiP is active. |
| Flaky test due to varying animation duration across devices | Automated tests asserted UI state after a fixed delay, but some devices ran the refresh animation faster/slower. | Replace fixed waits with explicit IdlingResource or await page.waitForFunction(() => !document.querySelector('.spinner')). |
| Memory leak observed after many refresh cycles | Each refresh created a new anonymous AsyncTask that was never cancelled, accumulating references. | Use lifecycle‑aware coroutines or ViewModel‑scoped work (e.g., viewModelScope.launch) that auto‑cancels on view destruction. |
Incorporate these observations as additional test cases (e.g., “PTR-PROD-001: Indicator stuck after rotation”) and prioritize them based on frequency in crash reports.
---
Short Checklist for Pull To Refresh Test Cases
| ✅ Item | Description |
|---|---|
| Requirement traceability | Every test case maps to at least one requirement ID. |
| Precondition clarity | List, network mock, device state, and accessibility settings are explicitly defined. |
| Step atomicity | Each step performs a single action (drag, release, wait, verify). |
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