How to test CSV export: the filters, the row count, and the download that lies
Testing CSV export isn't 'click export, get a file.' It's whether the export honors your active filters, matches the row count on screen, and doesn't silently fail on an empty state. Here's how to test CSV export from the browser, in plain English — and where the honest limits are.
How to test CSV export: the filters, the row count, and the download that lies
Export to CSV is the feature that gets built in an afternoon and trusted forever. Someone wires a button to an endpoint, a file lands in Downloads, the column headers look right, and it ships. Then a customer filters a list down to 40 rows, clicks Export, opens a file with 12,000 rows in it, and files a bug titled "export is broken" — which is the least useful sentence in your tracker, because "broken" could mean the filter was ignored, the sort was dropped, the row count is wrong, the file is empty, or the download never actually started and they're looking at last week's file.
CSV export looks like a button. It's actually a small contract: the file you get should be the data you're looking at. The active filters, the current sort, the selected columns, the search you typed — all of it should carry through to the export, and the moment it doesn't, someone downstream makes a decision on the wrong numbers. This is the playbook for testing that contract from the browser, in plain-English prompts — and it's honest, up front, about the one thing a browser agent can't do, because pretending otherwise would be the exact kind of overclaim this blog keeps calling out in everyone else's tools.
The honest limit, stated first
A browser agent drives your app the way a user does: it clicks Export, and it watches what the browser does in response. That means it can verify a lot — that the export request fired, that it fired with the right parameters, that it returned a success status, that a download was triggered, and that the on-screen state around the export (a confirmation toast, a row-count label, a progress indicator, an error) behaved correctly. What it cannot reliably do is open the downloaded .csv file and read the cells inside it. Parsing the bytes of a saved file is outside what a browser agent observes — the same honest boundary the file-upload playbook draws from the other direction.
So this playbook scopes to what's genuinely observable from the browser, and it's more than you'd think, because most CSV-export bugs are decided before the file is written — in the request the app makes. If the export endpoint gets called as GET /export?status=active&sort=name when your screen shows an active-status filter sorted by name, the file is almost certainly right; if it gets called as GET /export with no parameters while your screen is filtered, the file is wrong and you know it without opening it. The request is the tell. For assertions on the actual cell contents — encoding, escaping, formula-injection safety — you'll still want a unit test on the file-generation code; this catches the far more common bug where the wrong query was exported in the first place.
What "testing CSV export" actually has to cover, from the browser
The happy path is "a file downloaded." Here's the surface that decides whether it's the right file:
- The export honors active filters and search. The single most common export bug: you filter a list to 40 rows, export, and get the unfiltered 12,000 — because the export endpoint was wired to the raw dataset, not the current view. Observable from the browser: does the export request carry the same filter/search parameters the screen is showing?
- The export honors sort and column selection. If the UI lets you sort or choose columns, the export should reflect it. The request parameters (or the absence of them) tell you whether the customization made it through.
- The row count reconciles. If the screen says "1,248 results" and the export UI shows a count — "Exporting 1,248 rows" or "Download ready (1,248)" — those numbers must match each other and match the filtered view. A mismatch between the on-screen count and the export's stated count is a browser-visible bug even without opening the file.
- The empty and no-match states. Export a filtered view that matches nothing. Does the app disable the button, show an honest "nothing to export," or cheerfully hand you a header-only (or completely empty) file and a success message? A "successful" export of zero rows presented as if it worked is a real bug.
- The large / async export. Big exports often go async — "we'll email you the file" or a background job with a progress state. The browser-observable question: does the app acknowledge the request, show a truthful pending/queued state, and not silently do nothing while pretending to work?
- The download actually starts, and only once. Click Export: did a download get triggered, or did the button spin forever with no request? Double-click it: did it fire two export requests (and potentially two charges/jobs), or is it correctly debounced?
- Authorization on the export path. Everyone tests that the page is access-controlled; the export endpoint behind it is a separate route that sometimes isn't. Can a user export data they can't see in the UI by hitting the export URL with different parameters? This is the broken-object-level-authorization risk, applied to bulk data — the worst possible thing to get wrong on an export.
Most teams test item 6's happy sibling — click export, a file appears — and ship. Items 1, 3, and 7 arrive later as "the report had the wrong numbers" or, in the bad case, as a data-leak incident. None of those is findable by clicking Export once and glancing at Downloads; they're findable by exporting from a specific filtered state and checking that the request matches — which an agent does identically on every run.
Setup
Point a Test Scenario at a staging list view with enough data to filter, sort, and paginate, and a test account whose credentials live in the Scenario config rather than the prompt body. Substitute your own URLs, filters, and column names throughout.
Prompt 1: does the export match the filtered view
Step 3 and 4 are the whole ballgame. You don't need to open the file to know the export is wrong — if the screen is filtered to 412 active rows and the export request goes out as GET /export with no status parameter, the file is going to contain everything, and the network log proves it. The agent reads the actual request the app fired, so "the export ignored my filter" becomes a captured URL instead of a hunch, and it becomes that on every run instead of the one time someone happened to open the file and count.
Prompt 2: sort, column selection, and the row count that has to reconcile
Step 3 is the reconciliation check, and it's valuable precisely because it needs no file access. Three numbers that should be identical — the list count, the export's declared count, the confirmation's count — are all rendered in the browser, and when they disagree, one of them is lying about what got exported. That's a defect you can prove from a screenshot and a network log, and it's the kind of quiet numeric mismatch a human skims right past because each number looks individually reasonable.
Prompt 3: the empty state, the double-click, and the async path
Step 2 is the honest-failure test. An export of an empty result set should tell the truth — "nothing matches, nothing to export" — not hand back a success toast and a zero-row file, which sends the customer looking for data that was never there. Step 3 catches the double-fire that, on a metered or job-queued export, turns one click into two charges. Neither requires opening the file; both are decided entirely in the browser's behavior and network activity, which is exactly what the Monito Session records.
Reading the results
Each run comes back as a Monito Session: a screenshot timeline of the list, the filters, and every export interaction; the network log with the actual export requests and their parameters and status codes; console output; and the agent's reasoning where it judged whether a request matched the view or a count reconciled. For export testing the network log is the primary evidence — every request is captured with its method, URL, and status, so "the export ignored the filter" and "the export 500'd while the button showed success" are both provable facts rather than things someone thinks they saw. The screenshot timeline is where the empty-state and reconciliation bugs live.
And to close the loop honestly: this catches the wrong-query class of export bug, which is the overwhelming majority of them, entirely from the browser. It does not open the file to verify cell-level encoding, escaping, or formula-injection safety — for those, keep a unit test on the code that writes the CSV. The two together are the full picture; a browser agent owns the half that a unit test structurally can't see, which is whether the app asked for the right data in the first place. That division of labor is the same one the subscription-billing playbook draws between the agent and the billing engine.
None of the prompts names a button ID or a table selector — only what export is for. Redesign the reports page, swap the export button, rename the columns: the contract is identical, so the prompts are too. That's why describing intent beats scripting steps for anything as quietly load-bearing as a data export.
The one to run right now
If you run a single check today, run the filter-fidelity test — it catches the single most common and most consequential export bug: the file that ignores the view you exported it from.
Save it as a Test Scenario, pin it to your staging Project, and wire it into CI against every deploy that touches list views, filtering, or the export endpoints. A full run is typically 8–13 credits — roughly $0.08–$0.13 — and your first run is free. Point it at a filtered report, click Export, and read the request the app actually made. If it asked for everything while you were looking at forty rows, you'd rather find out from an agent than from whoever downstream trusted the file.