The bug that only existed on an iPhone
I built DataLens as a side project: upload a CSV, get back summary statistics, distribution charts, a categorical breakdown and a correlation matrix. React and TypeScript on the front, FastAPI on the back, Recharts doing the drawing.
It worked. I had used it on my own laptop dozens of times. Then I wrote tests, and found out it only worked on my laptop.
Testing four browsers when you own one
I develop on Windows, which means no Safari. That is a problem, because Safari's engine is WebKit and WebKit does not always agree with Chromium about how a page should be laid out.
Playwright solves this. It ships real Chromium, Firefox and WebKit builds and drives all three from one config:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: { baseURL: 'http://localhost:4173' },
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 13'] } },
],
});
Three tests: does the page render, do the charts appear after analysis, and does anything overflow horizontally. That last one is a single line of DOM measurement:
const overflow = await page.evaluate(() =>
document.documentElement.scrollWidth > document.documentElement.clientWidth
);
expect(overflow).toBe(false);
If the document is wider than the viewport, the user gets a sideways scrollbar. On mobile that feels broken even when nothing is technically wrong.
The first failure was not a browser problem
All four browsers failed the chart test immediately. That is a useful signal: when everything fails identically, it is not a compatibility issue.
The error context Playwright captured showed the answer in the rendered page:
- text: Failed to load sample data.
My FastAPI CORS config allowed localhost:3000 and localhost:5173. The Vite dev server runs on 5173, so I had never hit this. But vite preview, which serves the actual production build, runs on 4173. Every browser was being refused by my own backend.
One line:
allow_origins=[
"http://localhost:3000",
"http://localhost:5173",
"http://localhost:4173",
]
Worth saying plainly: I would not have found this by clicking around in dev mode, because in dev mode the bug does not exist. It only appeared once I tested what I would actually ship.
The real one: mobile Safari, and nothing else
With CORS fixed, eleven of twelve tests passed. The twelfth:
[mobile-safari] › no layout overflow
Expected: false
Received: true
Chromium fine. Firefox fine. Desktop WebKit fine. Only the iPhone 13 viewport, 390 pixels wide, had horizontal overflow.
The cause was my correlation matrix. It is a table whose column count depends on how many numeric columns the uploaded CSV has, so its natural width is unbounded. On any desktop viewport there is room. At 390 pixels there is not, and the table pushed the whole document wider than the screen.
The fix was responsive CSS, not JavaScript:
@media (max-width: 480px) {
.stats-row { grid-template-columns: repeat(2, 1fr); }
.charts-grid { grid-template-columns: 1fr; }
.summary-table { font-size: 0.75rem; }
}
html, body { overflow-x: hidden; max-width: 100%; }
.table-wrapper { max-width: 100%; -webkit-overflow-scrolling: touch; }
.dashboard, .upload-card { max-width: 100%; box-sizing: border-box; }
Collapse the four-column stat row to two. Stack the charts. Shrink the table text and let its wrapper scroll independently instead of stretching the page. Twelve of twelve.
While I was there: the 601 kB problem
Vite had been warning me on every build and I had been ignoring it:
(!) Some chunks are larger than 500 kB after minification.
dist/assets/index-DS_AtZhv.js 601.34 kB │ gzip: 182.04 kB
Everything was in one file, including Recharts, which is by far the heaviest dependency. That meant a visitor who opened the page and did nothing still downloaded the entire charting library.
But the dashboard does not render until you click analyse. So it does not need to be in the initial bundle:
const AnalysisDashboard = lazy(() => import("./components/AnalysisDashboard"));
// ...
<Suspense fallback={<p>Loading dashboard...</p>}>
<AnalysisDashboard analysis={analysis} filename={filename} />
</Suspense>
Rebuild:
dist/assets/index-DYJUX-3l.js 241.42 kB │ gzip: 79.11 kB
dist/assets/AnalysisDashboard-CN02Wouk.js 360.92 kB │ gzip: 103.95 kB
Initial download went from 601 kB to 241 kB, or 182 kB to 79 kB gzipped. A 57% reduction on first load, for four lines of code. The dashboard chunk arrives only when someone actually asks for a chart.
What I would keep doing
Measure the production build, not the dev server. My first Lighthouse run scored the app at 48 with a 12 MB payload. That was Vite serving unminified source with hot reload attached — a number that describes nothing anyone will ever experience. The production build scores 100. If I had put the 48 anywhere public I would have been describing a problem that did not exist.
Write the overflow test. It is three lines and it catches an entire category of layout bug that is invisible on the machine you develop on.
Let the tooling tell you. Vite had been printing the bundle warning for weeks. Playwright found the CORS bug in its first run. Neither required insight, only paying attention.
DataLens is on GitHub: github.com/medhaholla7/datalens
Top comments (0)