How to Test an Indian Ecommerce Website’s Mobile Layout with Puppeteer
Use Puppeteer to test an Indian ecommerce site at mobile sizes, exercise shopping flows, check constrained networks, and capture reproducible evidence.
Direct answer: Configure Puppeteer with a phone device profile or an explicit mobile viewport before navigating, then test representative listing, product, cart, and checkout states. Check for horizontal overflow and obstructed controls, repeat important steps under a constrained network profile, and save screenshots plus request failures as evidence. Browser emulation makes runs repeatable, but it does not prove how the site behaves on every physical phone.
This guide uses JavaScript and Puppeteer against a controlled staging storefront. Replace the sample URL and selectors with your own. Test payment flows only in an authorized sandbox or test environment.
1. Set up a repeatable mobile test
Use a project-local Puppeteer install so the script and browser dependency are explicit. Puppeteer provides Page.emulate() and maintained definitions through KnownDevices. Apply the emulation before navigation: sites may not handle a desktop page being resized after load as they handle a page opened at mobile dimensions. See the Puppeteer Page.emulate() API and KnownDevices reference.
mkdir mobile-storefront-check
cd mobile-storefront-check
npm init -y
npm install puppeteer
Save the following as mobile-check.mjs. It runs a representative homepage check, records failed requests, checks horizontal overflow, and saves a full-page screenshot. Set BASE_URL to a staging site and adjust the ready selector to match that site.
import puppeteer, { KnownDevices, PredefinedNetworkConditions } from 'puppeteer';
const baseUrl = process.env.BASE_URL ?? 'https://staging.example.in/';
const readySelector = process.env.READY_SELECTOR ?? '[data-testid="product-grid"]';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
// Device metrics, mobile behavior, touch, and user agent are applied together.
await page.emulate(KnownDevices['iPhone 17 Pro']);
await page.emulateNetworkConditions(PredefinedNetworkConditions['Fast 3G']);
const requestFailures = [];
const badResponses = [];
page.on('requestfailed', request => {
requestFailures.push({
url: request.url(),
error: request.failure()?.errorText ?? 'Unknown request failure',
});
});
page.on('response', response => {
if (response.status() >= 400) {
badResponses.push({ status: response.status(), url: response.url() });
}
});
const response = await page.goto(baseUrl, {
waitUntil: 'domcontentloaded',
timeout: 45_000,
});
if (!response || !response.ok()) {
throw new Error(`Page navigation failed: HTTP ${response?.status() ?? 'no response'}`);
}
await page.waitForSelector(readySelector, { timeout: 20_000 });
const layout = await page.evaluate(() => {
const root = document.documentElement;
const viewportWidth = root.clientWidth;
const pageWidth = root.scrollWidth;
const overflowingElements = [...document.querySelectorAll('body *')]
.filter(element => {
const rect = element.getBoundingClientRect();
return rect.width > 0 && (rect.left < -1 || rect.right > viewportWidth + 1);
})
.slice(0, 20)
.map(element => ({
tag: element.tagName.toLowerCase(),
id: element.id || null,
className: typeof element.className === 'string' ? element.className : null,
left: Math.round(element.getBoundingClientRect().left),
right: Math.round(element.getBoundingClientRect().right),
}));
return {
viewportWidth,
pageWidth,
hasHorizontalOverflow: pageWidth > viewportWidth,
overflowingElements,
};
});
await page.screenshot({ path: 'mobile-home.png', fullPage: true });
console.log(JSON.stringify({
url: page.url(),
title: await page.title(),
status: response.status(),
layout,
requestFailures,
badResponses,
screenshot: 'mobile-home.png',
}, null, 2));
if (layout.hasHorizontalOverflow) process.exitCode = 1;
} finally {
await browser.close();
}
Run it with your staging URL and an element that signals the page is ready:
BASE_URL='https://staging.example.in/' \
READY_SELECTOR='[data-testid="product-grid"]' \
node mobile-check.mjs
The overflow scan is a diagnostic aid, not a complete visual test. Some elements intentionally extend beyond the viewport, and a fixed header can cover a control without causing overflow. Review the saved image and add assertions that match the storefront’s expected behavior.
2. Choose the device and viewport deliberately
A narrow viewport alone is not full mobile emulation. Puppeteer’s viewport settings include CSS-pixel width and height, device scale factor, touch support, mobile viewport behavior, and orientation. Puppeteer defaults hasTouch and isMobile to false. Setting isMobile makes the browser account for the page’s meta viewport tag; hasTouch enables touch support. See the official Page.setViewport() API.
Use KnownDevices when a maintained device preset is appropriate. To define a stable test viewport independent of a preset:
await page.setViewport({
width: 390,
height: 844,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false,
});
await page.goto('https://staging.example.in/', { waitUntil: 'domcontentloaded' });
Set the viewport before opening the page. Changing mobile or touch emulation after navigation can trigger a reload, and many sites initialize responsive behavior when they first load. Choose widths based on your supported-device data and risk areas; the research does not establish a universal device list for Indian shoppers.
| Setting | What it affects | Useful check |
|---|---|---|
width, height |
Available viewport in CSS pixels | Test relevant compact and larger phone layouts; test landscape if supported. |
deviceScaleFactor |
Emulated device pixel scale | Check image sharpness or scale-sensitive rendering where relevant. |
isMobile |
Mobile behavior, including use of the meta viewport tag | Confirm the site declares and responds to its intended viewport. |
hasTouch |
Touch support | Exercise touch-oriented controls and menus. |
isLandscape |
Orientation | Check landscape only if it matters to your supported experience. |
3. Test shopping states, not just the home page
Pick a small, repeatable journey and make each step wait for a site-specific signal. A useful baseline is a category or listing page, a product page, adding an item to the cart, and the available address and payment steps. These are recommended test states, not claims that a particular site has a defect.
- Listing: Confirm product images, names, prices, filters, sort controls, and any pagination or load-more action are visible and usable.
- Product detail: Check image gallery, variant selection, stock or delivery information, quantity, and add-to-cart controls.
- Cart: Check line items, quantity updates, totals, discount fields, and the path to checkout.
- Address and delivery: Exercise the actual address and delivery flow the site offers. Do not assume a particular postal-code or regional-language behavior is universal.
- Payment and return: Use the payment options the site actually supports and inspect the resulting confirmation, error, or return state in a safe test setup.
At each state, inspect for horizontal scrolling, clipped text or prices, buttons outside the viewport, dialogs covering the primary action, sticky bars overlapping content, and controls that are difficult to tap. A screenshot captures one rendered state; it cannot by itself establish that a flow works. Add interaction checks for the important controls.
Example: add a product-specific assertion
After the initial page check, use your own selectors to test a product and cart transition. The selectors below are placeholders. Avoid relying on incidental CSS classes when the application can provide stable test IDs.
await page.goto('https://staging.example.in/products/sample-item', {
waitUntil: 'domcontentloaded',
});
await page.waitForSelector('[data-testid="add-to-cart"]');
await page.locator('[data-testid="add-to-cart"]').click();
await page.waitForSelector('[data-testid="cart-count"]');
const cartCount = await page.locator('[data-testid="cart-count"]').innerText();
if (Number(cartCount) < 1) {
throw new Error(`Expected cart to contain an item; received ${cartCount}`);
}
await page.screenshot({ path: 'mobile-cart.png', fullPage: true });
4. Repeat under constrained network conditions
Repeat high-value states with predefined profiles such as Slow 3G, Fast 3G, Slow 4G, or Fast 4G. This can reveal loading states that appear only while assets and product data arrive. Puppeteer documents Page.emulateNetworkConditions() and PredefinedNetworkConditions; network emulation does not affect WebSockets or WebRTC peer connections. See the network conditions API.
import { PredefinedNetworkConditions } from 'puppeteer';
await page.emulateNetworkConditions(PredefinedNetworkConditions['Slow 4G']);
await page.goto('https://staging.example.in/category/example', {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await page.waitForSelector('[data-testid="product-grid"]', { timeout: 30_000 });
await page.screenshot({ path: 'category-slow-4g.png', fullPage: true });
// Restore normal network behavior for later steps.
await page.emulateNetworkConditions(null);
Prefer an explicit ready condition over assuming all requests stop. networkidle2 can be useful for pages where it behaves predictably, but analytics, polling, or other continuing requests can make generic network-idle waits brittle. A selector, application-ready marker, or bounded wait tied to the tested state is usually easier to diagnose.
5. Capture screenshots and network evidence
page.screenshot() captures a page, while ElementHandle.screenshot() captures a component. Attach request and response listeners before navigation so failures can be correlated with the URL and state. Puppeteer documents screenshots and page events.
const productCard = await page.waitForSelector('[data-testid="product-card"]');
if (!productCard) throw new Error('Product card did not appear');
await productCard.screenshot({ path: 'mobile-product-card.png' });
For reproducible evidence, record the tested URL, viewport or device preset, network profile, browser/Puppeteer version, relevant test data, and the application state. Screenshots show the specific configuration and state recorded; they are not proof of equivalent behavior on every handset.
6. Handle India-specific payment checks conditionally
Payment checks depend on the store’s actual integration. If it implements the documented Google Pay UPI web flow, include its payment-selection step and merchant return or status state. Google’s India integration guide describes UPI merchant details and transaction fields, including a unique transaction reference, transaction URL, amount, and currency; its documented web method applies to INR. See Google Pay’s India web integration guide and its payment request documentation.
That guide describes a particular integration, not a requirement for every Indian storefront. NPCI’s BHIM-UPI guidelines discuss an App and Mobile Web flow with a prominent UPI-app payment option and Android intent behavior. Apply that guidance only where the site’s flow uses it. Google Pay’s setup guide describes testing that integration with Google Pay installed and Chrome for Android 60 or later; this integration-specific note is not a recommendation to use obsolete browser versions for general layout QA. Check the current provider requirements for the integration under test.
Never use a live charge for a visual test. Use the payment provider’s approved sandbox or another authorized test environment. A browser response alone does not prove that an order settled; verifying payment status on the merchant server is a separate integration check. Google’s general web integration checklist also describes a test configuration for purchase-workflow elements: Google Pay integration checklist.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The page still looks desktop-sized | Only the width was narrowed, or mobile emulation ran after navigation. | Set a device preset or configure isMobile and hasTouch before navigating. Verify the page’s viewport metadata. |
| The ready-selector wait times out | The selector differs from staging, content is blocked, or the page did not reach that state. | Confirm the selector in the rendered DOM, inspect navigation status and request failures, and wait for a stable application-specific marker. |
networkidle never arrives |
Analytics, polling, or other requests remain active. | Use domcontentloaded followed by a state-specific selector or bounded application-ready check. |
| Screenshot misses lazy-loaded content | Images or product sections load only after scrolling or entering the viewport. | Scroll through the relevant page in a controlled way, wait for image load or a page-ready signal, then capture. Check each section that matters. |
| Overflow scan reports a false positive | Decorative or intentionally offscreen elements extend outside the viewport. | Inspect the reported element and image. Narrow assertions to the content region or explicitly allow known intentional overflow. |
| Cart or checkout differs between runs | Shared mutable test data, session state, inventory, or asynchronous updates vary. | Use isolated test accounts/data, start from a known state, and wait for the visible result of each action. |
| Payment app does not open in headless emulation | External app intents and physical-device handoffs are not equivalent to a desktop browser emulation. | Test the integration in its provider-approved test setup on a compatible physical device when the handoff itself is in scope. |
| Requests fail only on a constrained profile | The asset or API may time out, or the test timeout may be too short for that profile. | Inspect the failed URL and error, distinguish site behavior from the test’s timeout, and use a bounded timeout appropriate to the profile. |
8. Performance, reliability, and cost
Use a small device and network matrix on every change, then reserve broader combinations for release checks or high-risk flows. Reuse a browser process for several sequential checks when practical, but create a fresh page or isolated context when cookies and storage could contaminate scenarios. Keep test data deterministic and avoid unnecessary full-page captures at every step; capture the states that help diagnose failures.
Puppeteer emulation gives repeatable browser settings, but cannot establish actual handset performance, touch feel, operating-system behavior, or external payment-app handoffs. Pair it with physical-device checks for high-risk layouts and provider-specific flows. Test automation costs include browser runtime, CI resources, and maintaining selectors and test data; no universal runtime or cost benchmark is implied here.
Or skip the browser setup
If you need screenshots without installing and maintaining a browser runner, ScreenshotNeo is a website screenshot API and MCP server. Its API returns an image or PDF from a GET request. This does not replace Puppeteer interaction tests for a cart or checkout journey, but it can simplify repeatable page captures.
For mobile-size capture, pass the viewport options supported by the API; see the ScreenshotNeo API documentation for current parameter names and usage. Basic call:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does a Puppeteer screenshot prove the site works on a real phone?
No. It records one browser configuration and state. Confirm high-risk behavior on supported physical devices as well.
Should every Indian ecommerce test include UPI?
Only if the storefront offers a UPI flow you need to validate. Use the actual provider integration and its authorized test setup.
Can this script validate payment settlement?
No. It can exercise visible browser states. Settlement and server-side payment verification require separate integration checks.
Should I test every phone model?
Choose representative viewports and devices using the site’s support commitments and audience data, then expand coverage around high-risk layouts and flows.


