Touch swipe gesture testing has always been the part of mobile web QA that scares people. You can write a click test in your sleep, but the moment a flow depends on a carousel that advances on swipe, a bottom sheet that drags up, or a photo gallery that pages left and right, the selectors get brittle and the timing gets fussy. This guide is about a different approach: describing the gesture in plain English and letting an AI agent drive a real Chromium browser to carry it out, then return a deterministic verdict. You write the intent, the agent figures out the mechanics, and you get a pass or fail you can trust in CI.
The tool doing the driving here is BrowserBash, a free and open-source natural-language browser automation CLI. It is Apache-2.0 licensed, installs with a single npm command, and defaults to free local models through Ollama so nothing has to leave your machine. But the ideas in this article apply whether you use BrowserBash or not. The interesting shift is treating a swipe as an objective ("swipe the carousel to the third slide and confirm the caption") rather than a sequence of pointer-down, pointer-move, and pointer-up events you hand-tune for every device width.
I also want to be honest up front, because gesture testing is one of those areas where marketing tends to overpromise. Real inertial scrolling, the momentum-and-bounce physics that iOS gives you when you flick a list, is not something a desktop Chromium browser reproduces natively. We will get to exactly what an AI agent can and cannot do here, and where you genuinely need a device farm or a native driver instead. Credibility matters more than a clean demo.
Why gesture testing breaks traditional automation
Most end-to-end frameworks were built around discrete events: click this button, fill this field, assert this text. Gestures are continuous. A swipe is not one event, it is a stream of pointer positions over time, and the UI under test often reads velocity, distance, and direction to decide what to do. A slow drag of 40 pixels might do nothing; a fast flick of the same distance advances two slides. That velocity sensitivity is exactly what makes hand-coded gesture tests fragile.
Then there is the selector problem. Carousels, swipers, and image galleries are frequently built on libraries that clone slides, transform them with CSS, and shuffle DOM order as you page through. The "third slide" is not a stable element you can pin down with a CSS selector, because the library may have three copies of it for the infinite-loop illusion. Traditional automation forces you to reverse-engineer the widget's internals. You end up asserting on transform: translateX(-828px) and praying the design team never changes the slide width.
The plain-English approach sidesteps both problems. When you tell an agent to "swipe left until the pricing slide is visible, then verify the Pro plan shows $29 per month," the agent looks at the rendered page the way a human would, performs the drag, observes the result, and repeats until the goal condition holds or it gives up. There is no selector to maintain and no transform math to hardcode. That is the core of natural-language browser automation: you describe the outcome, the agent handles the interaction.
How an AI agent performs a swipe
Under the hood, a swipe on the web is a synthesized sequence of pointer events dispatched through the Chrome DevTools Protocol. The agent computes a start coordinate, an end coordinate, and a path between them, then emits pointerdown, a series of pointermove events along that path, and a pointerup. Because it controls the timing between moves, it can approximate a fast flick or a slow drag. Modern swipe libraries listen to these Pointer Events (or their touch-event fallback), so a well-synthesized gesture triggers the same handlers a real finger would.
What makes the AI layer useful is the loop around that mechanic. The agent takes a snapshot of the page, decides the swipe is needed, performs it, takes another snapshot, and checks whether the goal is closer. If the carousel did not advance, it can try a longer swipe or a different start point. This observe-act-observe loop is why a vague instruction like "get to the last slide" works: the agent keeps swiping and checking until the last slide is on screen, rather than blindly firing a fixed number of gestures.
BrowserBash ships two engines that can drive this. The default is stagehand (the MIT-licensed engine from Browserbase), and there is an in-repo builtin engine that runs an Anthropic tool-use loop over Playwright. Both can synthesize pointer gestures against a real browser. The provider layer decides where that browser runs: local uses your own Chrome, and cdp, browserbase, lambdatest, and browserstack let you point at a remote endpoint or a device grid. For gesture work you usually start on local and only reach for a grid when you need real hardware, which we will discuss later.
Your first swipe test in plain English
Here is the shortest path to driving a carousel by intent. Install the CLI, then hand it an objective. The agent opens the page, finds the carousel, and swipes until the target slide appears.
npm install -g browserbash-cli
browserbash run "Open https://example.com/gallery, swipe the main carousel \
left until the fourth image is visible, then store the image caption as 'caption'" \
--agent --headless --timeout 120
The --agent flag switches output to NDJSON, one JSON event per line, which is what you want in CI and when an AI coding agent is reading the result. Each step event tells you what the agent did; the run_end event carries the verdict, a summary, the final state, any stored values, a cost_usd estimate, and duration_ms. Exit codes are frozen and reliable: 0 passed, 1 failed, 2 error or infrastructure or budget-stop, 3 timeout. You never have to parse prose to know what happened.
If you drop --agent, you get a human-readable reporter instead, which is nicer while you are iterating locally. And because BrowserBash is Ollama-first, that command runs against a free local model by default with no API key. The honest caveat: very small local models (around 8B parameters and under) can get lost on long multi-step gesture flows. For a carousel with four or five swipes and a verification at the end, a mid-size local model in the Qwen3 or Llama 3.3 70B class, or a capable hosted model, will be far more reliable.
Making the swipe deterministic with Verify
Driving the gesture is half the job. The other half is checking the result in a way that does not depend on an LLM's judgment. BrowserBash has deterministic Verify steps that compile to real Playwright checks: URL contains, title is or contains, text visible, a named button or link or heading visible, element counts, and stored-value equality. A Verify pass means the condition actually held, and a fail comes with expected-versus-actual evidence in the assertion table.
That distinction matters for gestures. You do not want "the agent thinks the fourth slide is showing." You want "the caption element contains the exact string we expect." Put the swipe in plain English and the check in a Verify line, and you get intent-driven interaction with a machine-checked result.
A committable gesture test with testmd v2
For anything you will run more than once, move it into a *_test.md file. These are plain-Markdown tests: a # title, steps as list items, @import for composition, and {{variables}} for templating. With version: 2 frontmatter, steps run one at a time against a single browser session, and two deterministic step types never touch a model: API steps for seeding data and Verify steps for checking it through the UI. Consecutive plain-English steps run as grouped agent blocks on the same page.
Here is a carousel test that seeds a product through the API, then drives the swipe UI and verifies the result:
cat > swipe_carousel_test.md <<'EOF'
---
version: 2
---
# Product gallery swipe
1. POST https://api.example.com/products with body {"name": "Trail Runner", "images": 5}
2. Expect status 201, store $.id as 'productId'
3. Open https://example.com/products/{{productId}}
4. Swipe the product image gallery left until the fifth photo is visible
5. Verify text "Photo 5 of 5" visible
6. Verify 'Add to cart' button visible
EOF
browserbash testmd run ./swipe_carousel_test.md
Steps 1 and 2 are deterministic API steps that create the product and capture its id with no model call. Step 3 navigates. Step 4 is the plain-English swipe, handled by the agent. Steps 5 and 6 are deterministic Verify checks that compile to Playwright assertions. If step 5 fails, you get the expected string, the actual string, and a screenshot, not a vague "it looked wrong." The run writes a human-readable Result.md with an assertion table you can commit or attach to a ticket.
One thing to know: testmd v2 currently drives the builtin engine, which needs ANTHROPIC_API_KEY or a compatible ANTHROPIC_BASE_URL gateway. It does not yet run v2 on Ollama or OpenRouter directly. A v1 file (no frontmatter) behaves exactly as it always did, so you can keep gesture tests on local models in v1 and reserve v2 for suites that mix API seeding with UI checks.
The honest limits: inertial scrolling and native physics
Now the part most articles skip. A synthesized pointer gesture in desktop Chromium is not the same as a finger on a real touchscreen, and the biggest gap is inertial scrolling. When you flick a list on an actual iPhone, the content keeps moving after your finger lifts, decelerating with momentum, and it may rubber-band at the edges. That momentum physics lives in the native compositor and the platform's scroll implementation. A CCD-synthesized pointerup does not carry velocity into a native momentum animation, because there is no native touch surface underneath.
What does this mean in practice? Swipe-driven widgets that are implemented in JavaScript, which is the vast majority of web carousels, swipers, tab-strips, and drag-to-dismiss sheets, work well, because they read pointer events and run their own animation. Anything that relies on the browser's native touch-scroll momentum, like a horizontally scrolling container that depends on flick inertia to travel a long distance, will behave differently under synthesis than on a device. The agent can still scroll such a container by dragging, but it will not get the "flick and it keeps going" behavior for free.
There is also touch-versus-pointer emulation. To make a page believe it is on a touch device, you generally need a mobile viewport plus touch emulation enabled, so that ontouchstart exists and the widget takes its touch code path instead of its mouse code path. Some carousels branch on this at load time. If your carousel only wires up swipe handlers when it detects a touch device, you need the emulation on before the page initializes, not after.
When you genuinely need a real device
Be clear-eyed about this. If your acceptance criteria explicitly cover native inertial momentum, rubber-band overscroll, pinch-zoom, or platform-specific gesture recognizers, a desktop browser (driven by AI or not) is the wrong tool, and you should run on real hardware. BrowserBash can point at a device grid through the lambdatest or browserstack providers, which force the builtin engine automatically, so you can keep your plain-English tests and just change where they execute. That gets you real Safari on real iOS for the flows that truly need it, while your everyday carousel and swipe-UI regression suite stays fast and cheap on local Chromium.
The pragmatic split most teams land on: run the bulk of gesture regressions against synthesized touch on local or cloud Chromium for speed and cost, and run a small, targeted set of "does the native momentum feel right" checks on a real-device grid on a slower cadence. You do not need every swipe test on real hardware, you need the few that actually depend on native physics.
Mobile viewports and the swipe matrix
Gestures behave differently at different widths. A carousel that shows one slide at 390 by 844 might show three at 1280 by 720, which changes how many swipes it takes to reach a target and whether the swipe handler is even active. BrowserBash lets you run the same test across viewports without duplicating files.
browserbash run-all ./tests/gestures \
--matrix-viewport 390x844,768x1024,1280x720 \
--shard 2/4 \
--budget-usd 2.00 \
--junit out/junit.xml
--matrix-viewport runs every test once per viewport, labeled in the events, JUnit, and results so you can see which width failed. --shard 2/4 runs a deterministic slice of the suite, computed on sorted discovery order so parallel CI machines agree on the split without coordinating. --budget-usd 2.00 stops launching new tests once the estimated spend crosses the cap; remaining tests report as skipped, the suite exits 2, and the spend lands in the results and JUnit properties. A single --viewport WxH flag also works on individual runs if you just want to pin one width. This is how you turn one swipe test into full coverage across the phone and tablet breakpoints where gestures actually matter, without hand-maintaining a device matrix.
The run-all orchestrator is memory-aware: it derives concurrency from your real CPU and RAM, orders previously-failed and slowest tests first, and flags flaky ones. For gesture suites, which can be timing-sensitive, the flaky detection is worth having, because it tells you which swipe tests are genuinely unstable versus which just failed once.
Keeping swipe flows green with monitoring and caching
Once a gesture flow works, you want to know the day it stops working, ideally before a user swipes into a broken carousel. Monitor mode runs a test or objective on an interval and alerts only on state changes, both directions, so you get a ping when a passing swipe flow starts failing and another when it recovers, never a wall of green-run noise.
browserbash monitor ./swipe_carousel_test.md \
--every 10m \
--notify https://hooks.slack.com/services/XXX/YYY/ZZZ
Slack incoming-webhook URLs get Slack formatting automatically; any other URL gets the raw JSON payload for your own handler. The reason an always-on gesture monitor is affordable is the replay cache: a green run records its actions, and the next identical run replays them with zero model calls, stepping the agent back in only when the page actually changed. For a stable carousel, that means the monitor is nearly token-free between real changes, and it only spends model budget when the swipe behavior drifts, which is exactly when you want it paying attention.
If you already have Playwright specs that drive gestures with page.mouse or dispatchEvent, you do not have to rewrite them by hand. browserbash import converts Playwright specs to plain-English tests heuristically and deterministically, with no model involved. It translates goto, click, fill, press, check, selectOption, getBy locators, and common expects, turns process.env.X into {{X}} variables, and drops anything untranslatable into an IMPORT-REPORT.md rather than silently inventing a step. Gesture-specific pointer sequences are the kind of thing that often lands in that report, which is honest: it tells you exactly what needs a human eye instead of guessing.
Fitting gesture tests into an agent workflow
The bigger reason to describe gestures in plain English is that AI coding agents can now run these tests themselves. BrowserBash exposes its CLI over the Model Context Protocol, so any MCP host (Claude Code, Cursor, Windsurf, Codex, Zed) can call it as a validation tool.
claude mcp add browserbash -- browserbash mcp
That registers three tools: run_objective for a single plain-English objective, run_test_file for a *_test.md file, and run_suite for a folder run in parallel. Each returns the structured verdict JSON, so when an agent changes a carousel component, it can call run_objective with "swipe to the last slide and confirm the CTA is visible" and read back a real verdict. A failing gesture test is a successful validation: the tool call succeeds, and the agent reads the failure and fixes its own code. BrowserBash is listed on the official MCP Registry as io.github.PramodDutta/browserbash, so the install is a one-liner in most hosts. This is the positioning that makes plain-English gesture tests worth writing: they are the same artifact your humans read in the tutorials and your agents execute in CI.
A quick decision guide
Use synthesized touch on Chromium (local or cloud) when your swipe UI is JavaScript-driven, which covers most carousels, swipers, tab strips, drag-to-dismiss sheets, and image galleries. This is fast, cheap, and runs on every PR. Reach for a real-device grid through lambdatest or browserstack when your criteria explicitly depend on native inertial momentum, rubber-band overscroll, pinch-zoom, or OS gesture recognizers. Keep gesture tests in v1 files on local models when you want zero API cost, and move to version: 2 when you need to seed data through API steps and verify it through the swipe UI in one deterministic run. If a competitor tool claims perfect native-physics fidelity in a desktop browser, treat that claim carefully; the native momentum gap is a property of the platform, not a feature any tool can simply toggle on.
Putting it together
The honest summary is this. AI-driven, plain-English gesture testing genuinely solves the two hardest parts of the old approach: the brittle selectors inside swipe widgets and the fiddly hand-timed pointer sequences. You describe the swipe by intent, the agent drives a real browser, and deterministic Verify steps give you a machine-checked result with real evidence. You get viewport matrices, sharding, budgets, monitoring, and MCP integration around it, so a swipe test scales from your laptop to CI to your coding agent without changing the artifact.
What it does not do is fake native physics. Synthesized touch is excellent for the JavaScript swipe UIs that make up the web, and it is the wrong tool for acceptance criteria that hinge on iOS momentum scrolling or pinch-zoom, which belong on real hardware. Knowing that line is what separates a gesture suite you trust from one that quietly lies to you. Build the fast synthesized layer for everyday regression, add a thin real-device layer for the handful of flows that truly need native behavior, and you have honest, maintainable coverage of the swipe interactions your users actually feel. You can read more real-world write-ups in the case studies and browse the full command reference on GitHub.
FAQ
Can an AI agent test swipe gestures in a real browser?
Yes. An AI agent synthesizes a sequence of pointer events (pointerdown, a path of pointermove events, and pointerup) through the Chrome DevTools Protocol against a real Chromium browser, so JavaScript swipe libraries fire the same handlers a finger would. The agent observes the page after each swipe and repeats until the goal condition holds, which is how a vague instruction like "swipe to the last slide" resolves reliably. This works well for carousels, swipers, and drag UIs built in JavaScript.
Does synthesized touch reproduce native inertial scrolling?
No, and this is the honest limit worth understanding. Native inertial scrolling, the momentum and rubber-band physics you feel when you flick a list on iOS, lives in the platform compositor and does not exist on a desktop synthesized touch surface. JavaScript-driven swipe widgets work fine because they run their own animation, but flows that depend on native flick momentum should run on a real-device grid instead. Use synthesized touch for the common case and real hardware for the physics-dependent exceptions.
Do I need a device farm for mobile gesture testing?
Not for most of it. The majority of swipe interactions on the web are JavaScript-driven and behave correctly under synthesized touch on Chromium, so you can run them fast and cheap on local or cloud browsers with mobile viewport emulation. You only need a real-device grid (through providers like LambdaTest or BrowserStack) for acceptance criteria that explicitly depend on native momentum, pinch-zoom, or OS-level gesture recognizers. Most teams split it: the bulk on synthesized touch, a small targeted set on real devices.
How do I check the result of a swipe without relying on the AI's judgment?
Use deterministic Verify steps. In a BrowserBash test file, you drive the swipe in plain English but write the assertion as a Verify line that compiles to a real Playwright check, such as text visible, a named element visible, an element count, or a stored value equals. A pass means the condition actually held, and a fail comes with expected-versus-actual evidence rather than an opinion, so your CI verdict is trustworthy and reproducible.
Ready to drive your swipe and carousel UIs by intent? Install the CLI with npm install -g browserbash-cli and point it at your first gesture. You can start entirely on free local models with no account, and if you want the optional free cloud dashboard later, sign up here whenever it suits you.