How to Write Test Cases for Pull To Refresh (With Examples)

How to Write Test Cases for Pull To Refresh (With Examples)

April 30, 2026 · 16 min read · How-To Guides

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:

  1. 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.
  2. 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.
  3. 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:

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

IDPreconditionsStepsExpected Result
PTR-POS-001List with 20 items displayed; network mock returns 20 new items in 500 ms1. 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-002List empty; network mock returns empty array in 200 ms1. Drag down 60 dp.
2. Release.
Indicator appears; request sent; after 200 ms indicator hides; empty‑state message remains visible.
PTR-POS-003List with 10 items; network mock returns 5 items with a 2‑second delay1. 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-004List scrolled halfway down; network mock returns updated timestamps1. 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-005List with 30 items; accessibility service (TalkBack) enabled1. Enable TalkBack.
2. Drag down 65 dp.
3. Release.
Indicator appears; request succeeds; TalkBack announces “refreshing” then “refresh complete”.
PTR-POS-006Device in landscape orientation; list with 12 items1. 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-007List 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-008List 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-009List 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-010List 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

IDPreconditionsStepsExpected Result
PTR-NEG-001List with 10 items; network mock configured to delay response 10 s1. 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-002List 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-003List with 12 items; accessibility service that injects a swipe‑up gesture1. 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-004List with 0 items; pull gesture interrupted by incoming call1. 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-005List with 15 items; network mock returns malformed JSON1. 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-006List with 8 items; pull performed while a bottom sheet is animating open1. 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-007List 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-008List with 6 items; device rotated mid‑pull1. 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-009List 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-10List with 12 items; pull performed during a system‑wide animation scale change1. 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

IDPreconditionsStepsExpected Result
PTR-EDG-001List 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-002List 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-003List with 0 items; pull performed while a system‑wide dark‑mode toggle animates1. 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-004List 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-005List with 8 items; pull performed while a background music service changes audio focus1. 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-006List 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-007List 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-008List with 20 items; pull performed while a system‑wide font size change is in progress1. 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-009List with 0 items; pull performed with battery saver mode restricting background network1. 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-010List 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:

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:

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 IDDescriptionLinked Test Cases
REQ-REFRESH-01User can trigger refresh by dragging downPTR-POS-001, PTR-POS-002, PTR-POS-003
REQ-REFRESH-02Refresh shows indicator while loadingPTR-POS-001, PTR-POS-004, PTR-NEG-001
REQ-REFRESH-03Updated data appears after successful loadPTR-POS-001, PTR-POS-003, PTR-POS-005
REQ-REFRESH-04Error state handled gracefullyPTR-POS-007, PTR-NEG-005, PTR-NEG-004
REQ-REFRESH-05Gesture ignored when indicator visiblePTR-POS-009, PTR-EDG-010
REQ-REFRESH-06Works in all supported orientationsPTR-POS-006, PTR-NEG-008
REQ-REFRESH-07Compatible with accessibility servicesPTR-POS-005, PTR-NEG-003
REQ-REFRESH-08Stable under device performance variationsPTR-NEG-002, PTR-EDG-001, PTR-EDG-004
REQ-REFRESH-09Resistant to interruptions (calls, rotations)PTR-NEG-004, PTR-NEG-008
REQ-REFRESH-10Does not conflict with other UI animationsPTR-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 \ LikelihoodLowMediumHigh
LowPTR-EDG-006, PTR-EDG-009PTR-POS-008, PTR-NEG-008PTR-POS-001, PTR-POS-002
MediumPTR-EDG-002, PTR-EDG-005PTR-POS-004, PTR-NEG-001PTR-POS-003, PTR-POS-005
HighPTR-NEG-004, PTR-NEG-007PTR-NEG-002, PTR-NEG-005PTR-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

  1. Setup – Launch the app on a physical device or emulator with the desired preconditions (list populated, mock server running).
  2. Pre‑check – Verify that no refresh indicator is visible and the list is at the expected scroll position.
  3. Execute Steps – Follow the numbered steps from the test case, using a finger or stylus to perform the drag.
  4. Observe – Watch for the indicator, listen for accessibility announcements, and check the list content after the expected delay.
  5. Validate – Compare against the expected result; note any deviations, crashes, or ANRs.
  6. 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.
  7. 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*:

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*:

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:

ObservationRoot CauseMitigation
Indicator stuck after device rotationThe 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‑pullThe 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 decodeThe 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” twiceBoth 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 expandedThe 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 instantlyThe 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 modeThe 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 modeThe 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 devicesAutomated 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 cyclesEach 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

✅ ItemDescription
Requirement traceabilityEvery test case maps to at least one requirement ID.
Precondition clarityList, network mock, device state, and accessibility settings are explicitly defined.
Step atomicityEach 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