How to Test GST Invoice Pages in an Indian Web App with Screenshot Comparisons
Build reliable GST invoice visual tests with deterministic fixtures, Playwright screenshots, and semantic checks for fields, tax values, and e-invoice markers.
Test a GST invoice page by combining tax-rule-aware fixture data, stable browser screenshots, and separate semantic assertions for invoice content. A screenshot comparison can catch layout regressions; it cannot establish that an invoice is legally compliant, that a tax calculation is correct, or that an IRN or QR code is valid.
This guide uses Playwright Test as a concrete example. Keep the fixtures and applicability assumptions reviewed against current rules for your transactions. For invoice particulars, start with the applicable [CBIC invoice rules](https://cbic-gst.gov.in/gst-invoice-rules.html); for e-invoice coverage and returned markers, check current [GST e-Invoice Portal guidance](https://einvoice6.gst.gov.in/content/e-invoice-printing-process-mandatory-fields-modes-of-irn-generation/).
1. Separate the three kinds of invoice testing
A useful suite keeps these checks distinct, even when they use the same fixture:
- Fixture and domain checks: provide realistic transaction inputs and verify the application’s calculated values and conditional business logic. Have a qualified owner review tax assumptions.
- Visual regression checks: compare the browser-rendered invoice against an approved baseline to find changed spacing, clipping, typography, and misplaced elements.
- E-invoice workflow checks: verify IRP integration and the IRN/QR payload through appropriate integration or validation tests. A screenshot only shows what the page rendered.
For example, assert in the UI that an IRN and QR region is displayed for a fixture marked as covered by the mandate. Separately verify the actual registration and QR payload through the applicable workflow. The GST portal describes invoice data going from the taxpayer’s accounting, billing, or ERP system to an IRP, with the returned IRN and QR code printed on the invoice. NIC describes the IRP as issuing an identification number for electronically authenticated B2B invoices. See the [GST portal printing guidance](https://einvoice6.gst.gov.in/content/e-invoice-printing-process-mandatory-fields-modes-of-irn-generation/) and [NIC’s e-invoice overview](https://www.nic.gov.in/project/gst-e-invoice/).
2. Design realistic, explicit fixtures
Do not treat every invoice as one universal shape. Build named fixtures whose transaction conditions are visible to reviewers. The following are useful dimensions to consider; include only combinations that apply to your product and have their assumptions reviewed:
- Registered and unregistered recipient, with the applicable recipient fields.
- Intra-state and inter-state supply, including the relevant supply classification and place-of-supply display.
- Goods and services, since quantity and unit are relevant to goods.
- Discounts or other supported adjustments.
- Reverse-charge cases.
- Covered and non-covered e-invoice scenarios, with IRN and QR display conditional on the fixture.
CBIC’s invoice rules list particulars such as supplier name, address and GSTIN; a consecutive serial number unique for a financial year; issue date; recipient details where applicable; HSN or accounting code; description; quantity and unit for goods; total and taxable supply values; applicable tax rates and amounts; place of supply for inter-state supply; distinct delivery address where relevant; reverse-charge status; and supplier or authorized-representative signature. Use the current applicable rule text and notification context when creating compliance-related checks. Do not hard-code a mandate threshold based on an old example.
Keep fixture inputs deterministic. Store dates, invoice identifiers, GSTIN-like test values, amounts, tax rates, supply type, recipient state, reverse-charge state, and e-invoice coverage explicitly. Use test-only data and ensure fixtures cannot be mistaken for real invoices.
3. Install and configure Playwright Test
In an existing JavaScript or TypeScript project, install Playwright Test and its browser binaries using the commands in the project’s chosen version of the [Playwright installation guide](https://playwright.dev/docs/intro). A minimal setup is:
npm init playwright@latest
Choose the TypeScript or JavaScript setup when prompted. Pin the package version in the lockfile and run CI with that same version. Add a stable test route or fixture-seeding mechanism to the app; the example below assumes an authenticated test route at /test/invoices/:id.
Set the viewport, locale, timezone, and device scale factor consistently. Make sure the CI image includes the same fonts and browser version used to approve baselines. A sample playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
use: {
baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
locale: 'en-IN',
timezoneId: 'Asia/Kolkata',
colorScheme: 'light',
viewport: { width: 1440, height: 1100 },
deviceScaleFactor: 1,
...devices['Desktop Chrome'],
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
});
For an actual suite, avoid setting contradictory values in both a device preset and project configuration: make the intended viewport and scale factor explicit in the final configuration. Treat print output and responsive sizes as separate browser projects or snapshot sets if they are supported product outputs.
4. Write visual and semantic assertions
This runnable example assumes the test server provides stable invoice fixtures through /test/fixtures/:id, and the application renders them at /test/invoices/:id. Adapt the setup route, locators, and accessible names to your app. The application should expose a meaningful ready marker after invoice data and fonts have loaded.
import { test, expect } from '@playwright/test';
test('registered inter-state goods invoice renders correctly', async ({ page }) => {
await page.request.post('/test/fixtures/registered-interstate-goods');
await page.goto('/test/invoices/registered-interstate-goods');
// Prefer an application-owned ready condition to arbitrary sleeps.
await expect(page.getByTestId('invoice-ready')).toBeVisible();
await page.evaluate(() => document.fonts.ready);
// Semantic checks catch incorrect content that may look visually plausible.
await expect(page.getByTestId('invoice-number')).toHaveText('TEST-FY26-0001');
await expect(page.getByTestId('invoice-date')).toHaveText('04 Oct 2026');
await expect(page.getByTestId('supplier-gstin')).toHaveText('TEST-SUPPLIER-GSTIN');
await expect(page.getByTestId('recipient-gstin')).toHaveText('TEST-RECIPIENT-GSTIN');
await expect(page.getByTestId('supply-type')).toHaveText('Inter-state');
await expect(page.getByTestId('taxable-value')).toHaveText('₹10,000.00');
await expect(page.getByTestId('tax-breakdown')).toContainText('₹1,800.00');
await expect(page.getByTestId('reverse-charge')).toHaveText('No');
await expect(page.getByTestId('irn-section')).toBeHidden();
// Whole-page snapshots catch clipping and unexpected displacement.
await expect(page).toHaveScreenshot('registered-interstate-goods.png', {
fullPage: true,
animations: 'disabled',
});
// Focused snapshots make high-risk areas easier to review.
await expect(page.getByTestId('invoice-totals')).toHaveScreenshot('invoice-totals.png', {
animations: 'disabled',
});
});
test('covered e-invoice fixture shows marker region', async ({ page }) => {
await page.request.post('/test/fixtures/covered-einvoice');
await page.goto('/test/invoices/covered-einvoice');
await expect(page.getByTestId('invoice-ready')).toBeVisible();
await expect(page.getByTestId('irn-section')).toBeVisible();
await expect(page.getByTestId('irn-value')).toHaveText('TEST-IRN');
await expect(page.getByTestId('qr-code')).toBeVisible();
await expect(page).toHaveScreenshot('covered-einvoice.png', {
fullPage: true,
animations: 'disabled',
});
});
The sample values and test IDs are illustrative. Replace them with known fixture outputs and app-specific selectors. If your system uses a different test runner, preserve the same method: stable fixtures, a meaningful ready condition, semantic assertions, reviewed reference images, and an inspectable diff artifact.
Playwright’s screenshot assertion waits until two consecutive screenshots are identical before comparing with the stored expectation. That reduces capture noise, but it cannot make changing application data deterministic. See [Playwright PageAssertions](https://playwright.dev/docs/api/class-pageassertions).
5. Create and review the baseline
- Start the app in the pinned local or container environment and make sure its fixture data is stable.
- Run the relevant test once to create a baseline image. Playwright stores snapshots alongside the test by default.
- Inspect the image and diff output. Confirm that the data, conditional fields, and layout are the intended state.
- Commit the baseline with the test. Treat a baseline change like a code change: require review and a clear reason.
- When updating snapshots, regenerate only the intended project and fixture set, then inspect every changed image before committing.
Playwright documents that rendering can vary with host OS, browser version, settings, hardware, power source, and headless mode. Keep baseline creation and CI in the same environment; its [visual comparison guide](https://playwright.dev/docs/test-snapshots) explains these sources of variation.
6. Decide what to capture
| Capture | Useful for | Watch for |
|---|---|---|
| Full invoice page | Unexpected content, clipping, overflow, overall layout shifts | Long pages amplify irrelevant dynamic content and make diffs harder to review |
| Totals and tax breakdown | Alignment, wrapping, and high-risk numeric presentation | Keep semantic assertions for exact values; a pixel match cannot validate arithmetic |
| Recipient and address block | Conditional field visibility and address wrapping | Use separate fixtures for recipient scenarios |
| IRN and QR region | Presence, placement, sizing, and clipping of returned markers | Visual presence does not validate registration or QR payload |
| Responsive viewport | Mobile layout, wrapping, and horizontal overflow | Maintain independent baselines per viewport and browser project |
| Print rendering | Page breaks, margins, and print-specific layout | Use print media emulation and separate snapshots from screen output |
Use both broad and focused snapshots when their jobs differ: the page-level image catches clipping and unexpected movement, while focused images keep important regions easy to review. Avoid hiding totals, tax breakdowns, or QR placement merely to quiet noisy diffs.
7. Keep captures stable without hiding real regressions
- Pin the environment: browser version, OS or container image, fonts, viewport, device scale factor, locale, timezone, color scheme, and headless mode.
- Load fonts before capture: wait on
document.fonts.readyand serve pinned or local fonts in CI. - Freeze volatile values: use fixed dates and invoice IDs in fixtures. Prefer test clocks and seeded data to masking.
- Wait for app readiness: wait for an app-owned signal after data and layout are ready. A fixed delay is a fallback for a known third-party transition, not a substitute for state.
- Disable motion: turn off CSS animations and transitions for snapshots, while keeping any motion behavior under separate functional tests.
- Keep masks narrow: if a field must vary, mask only that field and still assert its value semantically. Do not mask a whole totals or marker region.
- Inspect changes: update a baseline only after a reviewer confirms the visual change is intended and the fixture remains valid.
8. Test print and responsive output separately
An invoice that looks correct at desktop width may overflow on a phone or split badly when printed. Add independent snapshot cases for supported mobile widths and print rendering. In Playwright, emulate print media before navigation or capture, and use a separate reference name:
test('invoice print layout', async ({ page }) => {
await page.emulateMedia({ media: 'print' });
await page.goto('/test/invoices/registered-interstate-goods');
await expect(page.getByTestId('invoice-ready')).toBeVisible();
await expect(page).toHaveScreenshot('registered-interstate-goods-print.png', {
fullPage: true,
animations: 'disabled',
});
});
For mobile, set a dedicated project viewport and device scale factor, and assert that key regions remain visible and that the document does not gain unintended horizontal overflow. Do not assume one desktop baseline covers these output modes.
9. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Many text edges or spacing pixels differ | Baseline and CI differ in OS, fonts, browser build, viewport, device scale, or rendering mode | Run both in the same pinned environment and verify fonts and final config values. Playwright documents environment-dependent rendering in its visual comparisons guide. |
| Invoice values change on every run | Current time, random IDs, or mutable backend records appear in the page | Seed immutable fixtures, freeze the test clock, and assert changing values semantically. Mask only a truly irrelevant unstable field. |
| Screenshot is captured before content settles | The test navigates before invoice data, fonts, or asynchronous UI has reached its ready state | Wait for an app-owned readiness marker and fonts; remove or control asynchronous content. Screenshot assertions wait for consecutive identical captures but do not stabilize changing data. See PageAssertions. |
| Snapshot passes but the invoice is wrong | The page is visually similar while a label, amount, or conditional field is incorrect | Add DOM assertions for exact values and domain tests for calculations and rules. Visual tests cover appearance, not tax correctness. |
| QR region appears but the code is invalid | The snapshot proves presence and placement only | Validate IRN and QR payload in a separate workflow or integration check against the applicable IRP process; do not infer validity from pixels. |
| Full-page diff is noisy | Unrelated dynamic content or a long document obscures the important change | Keep a page-level check for clipping, and add focused snapshots for totals, recipient details, or marker placement. Stabilize data rather than masking high-risk areas. |
| Print baseline differs from screen baseline | Print uses different page breaks, margins, or styles | Maintain separate print snapshots and explicitly emulate print media. |
| Snapshot update unexpectedly changes many fixtures | A shared style, font, or fixture default changed, or the update command covered more projects than intended | Review the generated file list and diffs; regenerate the intended project and fixture set only. |
10. Performance, reliability, and maintenance
Image comparisons add browser startup, page rendering, image capture, and diff work to a test suite. Control suite time by reusing Playwright workers appropriately, grouping related assertions on one rendered page, and reserving full-page images for cases where whole-document layout matters. Focused snapshots can make review easier, but excessive snapshots multiply baseline maintenance.
Reliability comes mainly from deterministic inputs and a consistent rendering environment. Keep test routes independent of live IRP registration, filing, and production data. Use separate integration tests for those systems, with controlled credentials and state. A visual regression suite should remain repeatable even when an external service is unavailable.
Cost is mostly engineering and CI time: fixture maintenance, browser execution, storage and review of snapshots, and investigation of legitimate diffs. Start with a small set of high-value transaction variants and expand when the product adds a real conditional path. Avoid using visual coverage as a proxy for legal review or domain test coverage.
11. Where ScreenshotNeo fits
ScreenshotNeo is a website screenshot API and MCP server. For a test suite, it can capture a deployed invoice route without managing a browser in that particular capture step. The one-call API is useful for repeatable page captures, but keep Playwright or another browser runner for interactive setup, authenticated test state, and DOM assertions. See the ScreenshotNeo API documentation for request options.
ScreenshotNeo accepts cookie or consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. It reports page verdict and billing status in response headers. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
For this GST workflow, use a test URL that is safe to expose to the capture service, and do not put secrets or live personal invoice data in a public URL. A screenshot still cannot verify tax treatment or IRP validity.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/test/invoices/registered-interstate-goods -o shot.webp
Get an API key at the ScreenshotNeo docs. The URL above is an illustrative route; replace it with a reachable, appropriately protected test page.
12. FAQ
Does a matching screenshot prove GST compliance?
No. It shows that the rendered page matches an approved image. Compliance and tax treatment need rule-aware review and domain-level checks.
Should every invoice fixture show an IRN and QR code?
No. The e-invoice markers depend on mandate coverage and workflow state. Model those conditions explicitly and verify current applicability.
Can one baseline cover every browser and device?
No. Rendering and layout vary by browser and environment. Use separate projects or baselines for browser, viewport, and print modes that your product supports.
Should a QR code be masked to avoid flaky diffs?
Only if its visual encoding is intentionally variable and placement is still tested. Keep a separate validation for the payload and IRN.
Or skip the browser setup
ScreenshotNeo can capture a test invoice URL with one GET request. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/test/invoices/registered-interstate-goods -o shot.webp
For the same request in Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://example.com/test/invoices/registered-interstate-goods",
},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
And in Node.js:
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/test/invoices/registered-interstate-goods',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));


