PRACTICAL GUIDE / Playwright boundingBox element measurement
Measure page geometry without chasing moving pixels
Measure element geometry safely, distinguish invisible or moving elements, and keep Playwright layout assertions stable across browsers and CI runs.
In this guide9 sections
- Understand what boundingBox actually reports
- Guard null after waiting for the product state
- Assert relationships instead of copying coordinates
- Keep coordinate input and measurement in the same moment
- Distinguish viewport, scroll, and frame mistakes
- Attach geometry evidence before changing tolerances
- Separate an unsettled layout from an unstyled layout
- Route geometry failures with a complete handoff
- Use geometry only when geometry is the contract
What you will learn
- Understand what boundingBox actually reports
- Guard null after waiting for the product state
- Assert relationships instead of copying coordinates
- Keep coordinate input and measurement in the same moment
A drag test crashes in CI on box!.x even though the card exists in the DOM. The headless run measures during a collapsed state, so boundingBox() returns null. The non-null assertion silences TypeScript, not the browser.
Geometry tests fail for several unrelated reasons: the element is invisible, the locator matches the wrong element, the viewport triggers another layout, an animation is still moving, or coordinates become stale before input. Treat the box as one observation from one rendered moment, not as permanent identity.
Understand what boundingBox actually reports
locator.boundingBox() returns an object with x, y, width, and height, or null when the matched element is not visible. It measures the element resolved by the locator at call time. The return type is nullable for a reason, so production test code should not scatter box! assertions or casts that erase it.
Coordinates are relative to the main frame viewport, which is usually the browser window. Scrolling affects them in the same way it affects Element.getBoundingClientRect(), and x or y may be negative. An element inside a child frame is translated into the main-frame coordinate system. Calling getBoundingClientRect() inside that child frame produces coordinates relative to the child viewport instead.
The values describe rendered geometry, not document intent. CSS transforms, responsive rules, font metrics, scroll position, zoom behavior, and animation can affect the rectangle. The method does not tell you why a box has that shape. Pair it with the viewport, relevant DOM state, and the product rule being tested.
Visibility and presence are different. A locator can match a node that is hidden and still return null for its box. Conversely, a negative coordinate does not automatically mean invisible. Part of an element may render outside the viewport while the element still has a box.
This self-contained test demonstrates the nullable contract without timing or application dependencies.
import { test, expect } from '@playwright/test';
test('hidden elements do not produce a usable box', async ({ page }) => {
await page.setContent(`
<button id="visible">Save</button>
<button id="hidden" hidden>Delete</button>
`);
const visibleBox = await page.locator('#visible').boundingBox();
const hiddenBox = await page.locator('#hidden').boundingBox();
expect(visibleBox).not.toBeNull();
expect(hiddenBox).toBeNull();
expect(visibleBox!.width).toBeGreaterThan(0);
expect(visibleBox!.height).toBeGreaterThan(0);
});The two non-null assertions at the end are safe only because the test first checks visibleBox. In reusable code, a type guard or helper gives TypeScript the same proof without repeating !.
Guard null after waiting for the product state
Visibility should come from a user-relevant state transition. If a panel expands after clicking Details, click Details and assert that the panel is visible. Do not repeatedly call boundingBox() until it returns something while ignoring whether the correct panel opened.
Even after toBeVisible() passes, keep the null guard. The page can rerender between the assertion and measurement, especially in live dashboards and animated components. A clear error naming the locator and expected state is better than Cannot read properties of null several lines later.
import { expect, type Locator } from '@playwright/test';
type Box = NonNullable<Awaited<ReturnType<Locator['boundingBox']>>>;
export async function requireVisibleBox(locator: Locator, label: string): Promise<Box> {
await expect(locator, `${label} should be visible before measurement`)
.toBeVisible();
const box = await locator.boundingBox();
if (box === null) {
throw new Error(`${label} became invisible before boundingBox completed`);
}
return box;
}The helper does not make layout stable. It proves only that the locator was visible during the assertion and returned a box during the measurement. Keep readiness outside the helper because different components signal readiness differently.
A worked panel test waits for an explicit expanded state before measuring. The expected width comes from the scenario's design contract, not from a number copied out of a local run.
import { test, expect } from '@playwright/test';
import { requireVisibleBox } from './geometry';
test('expanded inspector meets its supported minimum width', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('/workspace');
await page.getByRole('button', { name: 'Open inspector' }).click();
const inspector = page.getByRole('complementary', { name: 'Inspector' });
await expect(inspector).toHaveAttribute('data-state', 'expanded');
const box = await requireVisibleBox(inspector, 'expanded inspector');
expect(box.width).toBeGreaterThanOrEqual(320);
});If the product specification says only that the inspector must remain usable, a pixel width may still be the wrong assertion. A visible heading, reachable controls, and no overlap with the workspace can represent usability better. Geometry should encode an actual requirement, not make a test look precise.
Animation is another boundary. toBeVisible() does not mean “finished moving.” Actions such as clicks perform a stability actionability check, but a direct measurement is not an action on the element. Wait for the application's settled state: an expanded attribute, the end of a documented transition, or a class that the component sets after layout. Avoid a fixed sleep because it changes the sampling time without proving stability.
Fonts can change width after text first appears. If typography is part of the measurement, make font availability part of the page's readiness contract or await the browser's font-loading promise before reading geometry. Do this only for tests that own font behavior; a blanket font wait in every test adds latency and can hide a broken font request unless network failures remain visible.
Assert relationships instead of copying coordinates
Raw snapshots of { x, y, width, height } are brittle when the real rule is relational. A card may move down because valid content above it grew. The meaningful requirements might be that two controls do not overlap, a touch target meets a minimum size, or a menu stays inside a container.
Turn those rules into small calculations with named tolerances. The tolerance is a test-design input and should be justified by the UI contract. It is not a claim about measurements from an experiment.
The following helper calculates overlap area between two boxes. It accepts no Playwright objects, so it can be unit tested separately.
type Rect = { x: number; y: number; width: number; height: number };
export function overlapArea(a: Rect, b: Rect): number {
const overlapWidth = Math.max(
0,
Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x),
);
const overlapHeight = Math.max(
0,
Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y),
);
return overlapWidth * overlapHeight;
}Use it after the layout reaches a named state. This example defines half a square pixel as the product team's accepted rounding tolerance. That value is illustrative for the example and must be replaced by the requirement your team actually supports.
import { test, expect } from '@playwright/test';
import { overlapArea, requireVisibleBox } from './geometry';
test('account menu does not cover the save control', async ({ page }) => {
await page.setViewportSize({ width: 1024, height: 768 });
await page.goto('/settings/profile');
await page.getByRole('button', { name: 'Account menu' }).click();
const menu = page.getByRole('menu', { name: 'Account' });
const save = page.getByRole('button', { name: 'Save changes' });
await expect(menu).toBeVisible();
await expect(save).toBeVisible();
const [menuBox, saveBox] = await Promise.all([
requireVisibleBox(menu, 'account menu'),
requireVisibleBox(save, 'save changes button'),
]);
const geometryTolerance = 0.5;
expect(overlapArea(menuBox, saveBox)).toBeLessThanOrEqual(geometryTolerance);
});Parallel measurement with Promise.all narrows the interval between reads but does not make them atomic. If the layout can move, wait for settled state first. When the relationship changes on purpose across breakpoints, create explicit viewport cases and assert the contract for each one rather than using one formula for every layout.
Exact x and y assertions are appropriate for a canvas editor, diagram snap grid, ruler, or component whose supported behavior is coordinate placement. They are usually poor assertions for document flow. A small heading shift after a legitimate copy change is not a product defect unless the design owns that coordinate.
Screenshot assertions are often better for whole-component visual appearance. expect(page).toHaveScreenshot() waits until consecutive screenshots are stable before comparing with the stored baseline, and it shows a visual diff. The cost is baseline ownership, browser and platform consistency, and review work for intended changes. Geometry assertions are smaller and more explainable when the rule is one relationship.
Keep coordinate input and measurement in the same moment
The Playwright API documentation says bounding box coordinates can be used for input when the page is static. That condition is the hard part. Between boundingBox() and page.mouse.click(), a sticky header can appear, the page can scroll, an ad can resize, or the target can animate. The mouse still clicks the old viewport coordinate.
Prefer locator.click() for ordinary controls. It resolves the locator at action time and performs the relevant actionability checks. If the behavior requires a point within an element, locator.click({ position }) keeps the position relative to that element and retains locator action semantics.
import { test, expect } from '@playwright/test';
test('selects a point inside the drawing surface', async ({ page }) => {
await page.goto('/diagram');
const canvas = page.getByTestId('drawing-surface');
await expect(canvas).toBeVisible();
await canvas.click({ position: { x: 40, y: 30 } });
await expect(page.getByRole('status')).toHaveText('Point selected: 40, 30');
});Those coordinates are an input in the drawing surface's own coordinate contract, not measurements claimed from a run. If padding, transforms, or the application's canvas coordinate conversion matter, assert the product's reported point as the example does.
Use page.mouse and a measured box when raw viewport coordinates are themselves under test. Keep measurement and input adjacent, prevent known layout movement, and verify the resulting product state. Do not calculate the center once in beforeAll and reuse it across tests or viewports.
import { test, expect } from '@playwright/test';
import { requireVisibleBox } from './geometry';
test('maps the target center to raw viewport input', async ({ page }) => {
await page.goto('/coordinate-map');
const target = page.getByTestId('coordinate-target');
const box = await requireVisibleBox(target, 'coordinate target');
await page.mouse.click(
box.x + box.width / 2,
box.y + box.height / 2,
);
await expect(page.getByRole('status')).toHaveText('Target activated');
});This raw click bypasses the target locator's click actionability at the final step. That is a real trade-off. It can reveal coordinate mapping bugs, but it can also click a covering element if the page changes. Keep a trace and assert the durable outcome.
Drag-and-drop code is especially vulnerable to stale boxes because it measures two elements, then sends a sequence of mouse operations. Prefer Playwright's locator drag APIs when they represent the interaction. Use manual coordinates only for behaviors such as freeform canvas dragging where the path or exact points are the subject of the test.
Distinguish viewport, scroll, and frame mistakes
CI geometry often differs because the viewport is not what the author assumed. Set the viewport for projects that own responsive contracts and record the project name with failures. Do not infer viewport from screenshot file dimensions without accounting for the capture and device configuration.
Scroll changes viewport-relative x and y. A header measured at the top of the page and measured after a long scroll can have different coordinates even when its document position is unchanged. Sticky elements can deliberately remain near an edge. If the test cares about document placement, boundingBox() may not be the right representation. If it cares about what the user sees now, set the scroll state explicitly.
Negative x or y values deserve interpretation. A partially offscreen drawer may be mid-transition, intentionally peeking from an edge, or clipped by a responsive layout. Do not clamp coordinates to zero and continue. Clamping converts evidence about the layout into a click on a different point.
Separate three outcomes before editing the test:
nullmeans the resolved element is not visible at measurement time.- A thrown locator error means Playwright could not resolve the operation as requested, such as when a strict locator has multiple matches.
- A non-null box with coordinates outside the viewport is a real geometric observation, not a nullable failure.
That classification keeps an ambiguous selector from being “fixed” with another visibility wait. Responsive pages often render desktop and mobile versions of a control, then hide one with CSS. A broad test ID may match both nodes. Tighten the locator to the component users can identify, or assert the intended visible instance and its count before measurement. Do not call .first() because the current DOM order happens to put the desktop copy first.
When the requirement says the user can currently see the element, pair visibility with viewport evidence. toBeVisible() and toBeInViewport() answer different questions: an element can be rendered outside the current viewport. Measure only after both requirements pass.
import { test, expect } from '@playwright/test';
import { requireVisibleBox } from './geometry';
test('checkout action is visible within the supported mobile viewport', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/cart');
const checkout = page.getByRole('button', { name: 'Checkout' });
await expect(checkout).toHaveCount(1);
await expect(checkout).toBeVisible();
await expect(checkout).toBeInViewport();
const box = await requireVisibleBox(checkout, 'checkout button');
const viewport = page.viewportSize();
if (viewport === null) {
throw new Error('A fixed viewport is required for this geometry test');
}
expect(box.x).toBeGreaterThanOrEqual(0);
expect(box.y).toBeGreaterThanOrEqual(0);
expect(box.x + box.width).toBeLessThanOrEqual(viewport.width);
expect(box.y + box.height).toBeLessThanOrEqual(viewport.height);
});The arithmetic checks full containment, while toBeInViewport() checks viewport intersection according to its assertion contract. Keep the arithmetic only if full containment is a supported requirement. A sticky button partly clipped by a safe-area treatment may need a more specific product rule.
scrollIntoViewIfNeeded() is appropriate when the journey permits automatic scrolling to the target. It changes scroll state before measurement, so it cannot prove that the element was initially reachable without scrolling. Name that choice in the test.
import { test, expect } from '@playwright/test';
import { requireVisibleBox } from './geometry';
test('scrolls a named invoice into view before measuring it', async ({ page }) => {
await page.goto('/invoices');
const invoice = page.getByRole('row').filter({ hasText: 'Invoice INV-2048' });
await invoice.scrollIntoViewIfNeeded();
await expect(invoice).toBeInViewport();
const invoiceBox = await requireVisibleBox(invoice, 'invoice INV-2048');
expect(invoiceBox.y + invoiceBox.height).toBeGreaterThan(0);
});This code proves the row can be brought into view and measured afterward. It does not prove the user saw it before scrolling, that the entire row fits, or that a virtualized list contains every invoice. Write separate assertions for those claims.
Partially visible elements make a useful near-miss. A box can have positive width and height while a fixed header covers it. Geometry alone can calculate overlap with that header, but only if the header is the known obstruction. For ordinary interaction, locator.click() provides the relevant receives-events check and a call log when another element intercepts input. Do not rebuild all of Playwright's actionability model from rectangles.
Frame coordinates produce a frequent double-offset bug. Playwright already translates a child-frame element's box to the main viewport. Adding the iframe element's x and y again moves the point too far. The following test compares the two coordinate systems directly.
import { test, expect } from '@playwright/test';
test('child-frame boxes are translated to the main viewport', async ({ page }) => {
await page.setContent(`
<iframe name="tools" style="margin-top:120px" srcdoc="
<button style='margin-top:20px'>Run check</button>">
</iframe>
`);
const button = page.frameLocator('iframe[name="tools"]')
.getByRole('button', { name: 'Run check' });
const localRect = await button.evaluate(element => {
const rect = element.getBoundingClientRect();
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
});
const mainFrameBox = await button.boundingBox();
expect(mainFrameBox).not.toBeNull();
expect(mainFrameBox!.y).toBeGreaterThan(localRect.y);
});The test avoids asserting a browser-specific border calculation. It proves the relationship: the button's main-frame y includes frame placement and is greater than its frame-local y in this markup. Production code should use the Playwright box directly for page.mouse; it should not add the iframe rectangle.
A wrong locator can imitate a viewport bug. Repeated hidden and visible copies of a component may share a test ID at different breakpoints. Confirm locator count and identity before measuring. A measurement method is not a selector debugger.
Attach geometry evidence before changing tolerances
A failed number without context encourages random tolerance increases. Attach the boxes, viewport, URL, project, and the rule that failed. Take the screenshot and trace from the same attempt. Do not present those values as universal benchmarks; they are evidence from that test run.
import { test, expect } from '@playwright/test';
import { requireVisibleBox } from './geometry';
test('toolbar stays above the editor', async ({ page }, testInfo) => {
await page.goto('/editor');
const toolbar = page.getByRole('toolbar', { name: 'Formatting' });
const editor = page.getByRole('textbox', { name: 'Document' });
const toolbarBox = await requireVisibleBox(toolbar, 'formatting toolbar');
const editorBox = await requireVisibleBox(editor, 'document editor');
const evidence = {
url: page.url(),
project: testInfo.project.name,
viewport: page.viewportSize(),
toolbar: toolbarBox,
editor: editorBox,
};
await testInfo.attach('geometry', {
body: Buffer.from(JSON.stringify(evidence, null, 2)),
contentType: 'application/json',
});
expect(toolbarBox.y + toolbarBox.height).toBeLessThanOrEqual(editorBox.y);
});In Trace Viewer, inspect the action immediately before measurement, the DOM snapshot, console errors, and network failures. A missing stylesheet or font request can produce a real layout difference. A trace captured only on retry may show a stable second attempt rather than the first failure, so use --trace=on --retries=0 for focused reproduction.
Wire explicit viewport projects and failure artifacts in config. Keep browser coverage intentional; pixel-level rules across every browser multiply baseline and tolerance work.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 1 : 0,
reporter: [['line'], ['html', { open: 'never' }]],
use: {
screenshot: 'only-on-failure',
trace: 'on-first-retry',
},
projects: [
{
name: 'desktop-chromium',
use: { browserName: 'chromium', viewport: { width: 1280, height: 800 } },
},
{
name: 'mobile-chromium',
use: { browserName: 'chromium', viewport: { width: 390, height: 844 } },
},
],
});These viewport values define test cases; they are not measurements of user traffic. Choose projects from supported product breakpoints and actual coverage decisions. More projects add runtime and visual maintenance.
Separate an unsettled layout from an unstyled layout
A relation assertion can produce almost the same failure text for two different causes. In one run, the component is still moving when its box is read. In another, the component has stopped moving but a required stylesheet did not apply. Both runs can report a non-null box, a positive width, the expected expanded state, and the same failed comparison. Adding a longer wait may make the first case less frequent. It cannot repair the second case, and it delays every successful run while leaving the missing style unexplained.
The separating evidence is whether the wrong rectangle changes across same-attempt diagnostic measurements and whether the intended style rule is present. For an unsettled layout, two time-aligned box records show the same element progressing toward its final position or size. The relevant stylesheet request completed, the expected class or state is on the element, and the later record has the supported relationship. For an unstyled layout, the rectangle is stable but wrong. The network evidence shows the style resource did not complete successfully, or a resolved layout property has its fallback value because the intended rule is absent. Other elements supplied by the same resource may also lose spacing or placement. That pattern points to asset delivery or build output, not a timing threshold.
Read the geometry attachment as a tuple, not as one suspicious number. The url confirms that a redirect or an error page did not satisfy a broad locator. The project and viewport identify the responsive branch. Each element box then supplies x, y, width, and height. A healthy value is non-null and satisfies the named relation in that viewport. Positive width and height show that an area was measured, but they do not prove that the correct CSS loaded or that the element is on screen.
Consider an illustrative diagnostic record for an inspector whose supported desktop contract is a 320-pixel width. A healthy record might show x: 960, y: 0, width: 320, and height: 800 beside a 1280 by 800 viewport. An unstyled record might show x: 8, y: 8, width: 188, and height: 24. Those figures are illustrative, not measurements from a product. The important evidence is that the second box remains the same across adjacent samples while its computed layout differs from the rule the shipped component should receive.
One misleading record can look healthier than either example. A box with the expected width may still have an x value that places nearly all of it left of the viewport. Likewise, an expanded state value proves that application state changed, not that the browser applied the corresponding style. A screenshot can look broadly correct at a glance while the attached numbers came from another moment, so compare artifacts from the same attempt and the same state transition. Never convert null to an all-zero rectangle for easier serialization. That produces four numeric fields that resemble a measurement while erasing the fact that no visible box was returned.
Route geometry failures with a complete handoff
Geometry failures often cross ownership boundaries. The test owner owns locator identity, measurement timing, the assertion relation, and the evidence attachment. The component team owns the state transition and the supported layout rule. The browser infrastructure or web delivery team owns failures that prevent a stylesheet or font from reaching the page in the test environment. Product design must decide when nobody can state whether a distance, containment rule, or minimum size is actually supported.
A useful handoff contains the test name, page URL, project, viewport, locator meaning, and the user action that should establish readiness. Include both raw boxes, the exact relation that failed, and the source of any tolerance. Attach the same-run trace and screenshot, then point to the first snapshot where the state or geometry diverges. If a resource is implicated, include its request URL and observed response outcome. If timing is implicated, identify the application state that claimed completion while the rectangle was still changing. The recipient should be able to distinguish a selector problem, a component contract problem, and an environment delivery problem without rerunning the entire suite.
Ownership follows that evidence. A stable wrong box with a missing style request goes to delivery with the component owner copied for impact. A moving box after the component reports completion goes to the component team. A box from the wrong repeated control stays with the test owner. A disputed tolerance goes to the person who owns the layout contract, not to the CI maintainer who happened to see the failure first.
Use geometry only when geometry is the contract
Start a migration by finding every boundingBox() call followed by !, an unchecked destructure, or a fixed delay. Classify its purpose. Ordinary click, visibility, viewport presence, screenshot comparison, and coordinate behavior need different APIs and evidence.
For a suite that already exists, land failure evidence before tightening assertions. The first change should make the current box, viewport, project, URL, and product state available on a failed attempt while preserving current pass criteria. That gives the first wave of migration failures enough context to diagnose. Land any missing component readiness signal next. A test cannot responsibly remove its fixed delay until the application exposes a state that means the relevant layout work is complete.
Convert one representative group after those prerequisites are available. Choose a feature whose readiness state and layout owner are known, and run that group both alone and in its normal suite position. Include at least one case that returns a real rectangle and one case that exercises nullable handling. This pilot checks the helper boundary and artifact quality before a mechanical sweep creates the same weak diagnostic in many files.
Shared fixtures and helpers usually break before the layout assertions themselves. A fixture may rely on a previous test having scrolled the page, expanded a panel, or loaded a font into the browser process. A cached rectangle helper may have callers that assume navigation never occurs between measurement and use. Running the converted file alone exposes those dependencies earlier than a full suite whose execution order happens to satisfy them. Repair fixture state and helper contracts before expanding the migration to another directory.
Viewport enforcement should follow the same order. Define which existing projects own each responsive contract, then migrate containment or separation checks within those projects. If a test currently inherits an unspecified viewport, record its behavior before assigning it a breakpoint. Otherwise the rollout mixes a test-hardening change with a responsive-layout change, and reviewers cannot tell which one caused the new box.
The change is working when failures become more specific, not merely when the suite becomes green. Converted files should behave the same in isolation and in their normal suite position. A genuine relation failure should retain both usable boxes, the viewport, and the state that preceded measurement. Existing Cannot read properties of null crashes and unexplained raw mouse misses should stop appearing in the converted group. Track those failure categories during the rollout so an apparent improvement cannot be manufactured by retries or wider tolerances.
The staged approach has concrete costs. Readiness assertions and separate measurements add browser communication to every migrated test, which accumulates in large suites. Traces, screenshots, and JSON attachments consume storage and take time to upload. A readiness signal also becomes a maintained interface between component code and tests, so changing the component state model requires a coordinated test update. If raw coordinate tests are replaced wholesale with locator actions, the suite becomes easier to maintain but loses coverage of viewport coordinate mapping, so retain a focused mapping test wherever that behavior is part of the product.
Replace ordinary center clicks with locator.click(). Replace “is on screen” arithmetic with the supported viewport assertion when that matches the requirement. Keep boxes for non-overlap, minimum target size, canvas mapping, drag paths, and other actual geometry rules.
For each remaining case, set the viewport, wait for a product-ready state, guard null, state the relationship, and attach evidence on failure. Establish tolerances from the design contract and supported rendering environments, not by widening them until CI turns green.
This technique does not catch clipped content inside a correctly sized container. A localized button label can be cut off by an overflow rule while the button's outer box still meets its minimum width and height, remains inside the viewport, and does not overlap a neighbor. Every rectangle assertion can pass because the failure is in the painted content inside the rectangle. Test that risk with a focused visual check or another assertion aimed at the visible content. Do not treat a passing outer box as proof that text, icons, or canvas pixels were rendered completely.
Do not measure hidden templates, cache boxes across navigations, add iframe offsets twice, clamp negative coordinates, or compare every field to one developer's laptop. Avoid raw mouse input when a locator action expresses the same user behavior. A bounding box is valuable when the rectangle itself answers the test question; otherwise it is extra state that can go stale between the question and the click.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 04Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
Why does locator.boundingBox return null?
A null result means the matched element is not visible when Playwright measures it. Assert the state that should make it visible, then still guard the nullable return because the page can change between operations.
Are boundingBox coordinates relative to an iframe?
No. Playwright reports boxes relative to the main frame viewport, including for elements inside child frames. That differs from calling `getBoundingClientRect()` within the child frame, which uses that frame's viewport.
Can boundingBox return negative x or y values?
Yes. Scrolling and offscreen layout can place an element partly or wholly above or left of the viewport, so its returned coordinates may be negative. Negative coordinates are evidence to interpret, not values to clamp automatically.
Should I click an element using its bounding box center?
Prefer `locator.click()` because it performs actionability checks and resolves the element at action time. Center coordinates are appropriate only when coordinate-level input is the behavior under test and the page is kept static between measurement and input.
How exact should Playwright layout assertions be?
Assert the product rule with an explicit tolerance, such as minimum separation or non-overlap, rather than copying one local run's full coordinates. Exact pixel equality is justified only when exact geometry is itself the supported contract.
RELATED GUIDES
Continue the learning route
GUIDE 01
Assert CSS Pseudo-Elements with Playwright
Learn Playwright toHaveCSS pseudo element assertion with working code, failure cases, debugging steps, and CI evidence for reliable QA automation.
GUIDE 02
Measure Reliability with Playwright repeatEach Runs
Master Playwright repeatEach reliability with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.
GUIDE 03
Playwright Locators Guide: Find Elements Reliably
Playwright locators guide for stable UI tests with role selectors, filters, assertions, strict mode, debugging, and flaky selector fixes in CI.
GUIDE 04
Custom Playwright Matchers: Extend and Merge expect Safely
Create retry-aware Playwright custom matchers, merge expect modules without collisions, and preserve useful negation, timeout, and failure output.
GUIDE 05
Upgrade Playwright Test Agent Definitions Safely
Master upgrade Playwright test agents with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.