Browser automation succeeds when tests model document loading, layout, networking, and rendering rather than treating a page as a static string.
Measure the Browser and DOM #
Browser dimensions answer different questions:
| API | Measurement |
|---|---|
window.innerWidth and innerHeight | Layout viewport, including a rendered scrollbar |
window.outerWidth and outerHeight | Browser window bounds |
screen.width and screen.height | Screen dimensions in CSS pixels |
screen.availWidth and availHeight | Screen space excluding reserved operating-system areas |
element.clientWidth and clientHeight | Content box plus padding, excluding border and scrollbar |
element.scrollWidth and scrollHeight | Scrollable content extent |
element.offsetWidth and offsetHeight | Border-box dimensions, rounded to integer pixels |
element.getBoundingClientRect() | Border-box geometry in viewport coordinates, retaining fractions |
clientWidth is zero for an inline element without a layout box. On the root
element, it describes the layout viewport without a classic scrollbar. Avoid
mixing viewport coordinates from getBoundingClientRect() with document
coordinates without adding the scroll offset.
Use Valid HTML Structure and Resource Hints #
<link> expresses a relationship to an external resource. Its rel attribute
is a space-separated set of standardized link types:
<link rel="stylesheet" href="/site.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">preconnect opens transport connections early; use it only for origins that
the page will contact. It does not download a stylesheet, font, or script.
<meta> supplies document metadata such as charset, viewport, description,
and HTTP-equivalent directives. Put <meta charset="utf-8"> early in the
head so the parser can decode following bytes correctly.
<noscript> provides fallback content when scripting is disabled or
unsupported. Its parsing behavior depends on whether it appears in head or
body, so test generated markup in an HTML validator and browser.
<p> elements cannot contain another paragraph. The HTML parser closes the
outer paragraph before the inner one, changing the DOM from the source text.
Template code should construct siblings or a semantic container instead:
<div class="notice">
<p>First paragraph.</p>
<p>Second paragraph.</p>
</div>Model Script Loading and Module Dependencies #
A classic script without async or defer blocks HTML parsing while the
browser fetches and executes it. Script placement therefore determines which
nodes and globals are available:
<script src="/jquery.js"></script>
<script src="/plugin-that-needs-jquery.js"></script>The order is required because the second script uses the global created by the first. Moving a dependent script before its provider raises a reference error.
| Form | Parser Blocking | Execution Order | DOM Availability |
|---|---|---|---|
| Classic script | Yes | Document order | Nodes parsed before the tag |
defer | Fetch is non-blocking | Document order after parsing | Full parsed document |
async | Fetch is non-blocking | First completed first | Not tied to parsing completion |
type="module" | Fetch is non-blocking | Module dependency order | Deferred by default |
defer suits ordered application scripts. async suits independent scripts
such as analytics that cannot rely on document position or each other.
Modules are deferred by default, resolve import dependencies, have their own
scope, and execute only once per module URL. Use DOMContentLoaded when
classic code must run after parsing, and use load only when images,
stylesheets, and subframes are also relevant.
Dynamically appended scripts are asynchronous by default. Set
script.async = false before insertion only when a loader deliberately
serializes classic scripts. Prefer native ES modules and explicit import
dependencies over a custom loader whenever deployment constraints permit it.
Design Responsive CSS from Constraints #
The CSS box model has content, padding, border, and margin. box-sizing
changes whether declared width and height include padding and border:
*, *::before, *::after {
box-sizing: border-box;
}With border-box, width: 20rem is the border-box width. With the initial
content-box, padding and border add to the rendered width. A universal
border-box rule makes component dimensions composable.
Flexbox lays out a one-dimensional sequence. Grid lays out explicit rows and columns. Use flex for a toolbar or a linear content flow; use grid when items must align across both axes:
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
gap: 1rem;
}The minmax expression preserves readable columns while allowing a single
column on a narrow viewport. gap belongs to the layout container and avoids
margin-collapsing or first/last-child exceptions.
Media queries select rules from environment features:
@media (width >= 48rem) {
.sidebar-layout {
grid-template-columns: minmax(16rem, 1fr) minmax(0, 3fr);
}
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 1ms; transition-duration: 1ms; }
}Use a viewport breakpoint when the viewport changes the design’s available space. A container query is a better fit when a reusable component can appear in different-width regions of the same page.
CSS lengths have different reference boxes. rem refers to the root font
size, em refers to the current font size, percentages depend on the property,
and vw or vh refer to viewport dimensions. Preserve non-Latin glyph
coverage in a font stack when the content requires it:
.cjk-sample::before {
content: "漢";
font-family: "Noto Sans CJK SC", sans-serif;
}The glyph is a font-coverage test, not prose content.
Respect User Color Preferences #
Declare support for both color schemes, then define semantic color variables:
:root {
color-scheme: light dark;
--surface: Canvas;
--text: CanvasText;
--accent: #2867c8;
}
@media (prefers-color-scheme: dark) {
:root { --accent: #8ab4f8; }
}prefers-color-scheme reflects an operating-system or user-agent preference.
An application-controlled theme selector can override variables by setting an
attribute on the root element, but it should retain a clear precedence rule
between saved preference, explicit user choice, and system preference.
An embedded SVG or iframe is a separate document. Parent-document CSS custom properties do not cross that boundary; the embedded resource needs its own styles or a deliberate messaging and theming mechanism.
Load Fonts with Predictable Fallbacks #
A typeface family contains related faces such as regular, italic, bold, and
bold italic. A glyph is the visible representation of a character. The browser
selects a font per character by walking font-family:
body {
font-family: "Example Sans", system-ui, sans-serif;
}When a face lacks a glyph, fallback can come from a later family. Quoted family names are necessary when a name contains spaces.
@font-face associates a family name with a downloadable resource:
@font-face {
font-family: "Example Sans";
src: url("/fonts/example-sans.woff2") format("woff2");
font-style: normal;
font-weight: 400 700;
font-display: swap;
}WOFF2 is a compressed web-font container for OpenType font data. A variable
font can expose axes such as wght, wdth, opsz, and ital; restrict the
declared ranges to the faces the site ships. font-display: swap exposes
fallback text while the web font loads, so choose a fallback with compatible
metrics or use font metric overrides to reduce layout shift.
window.queryLocalFonts() requires permission and has limited browser support.
Do not make a site’s rendering depend on enumerating local fonts.
Scale Canvas Correctly #
The width and height HTML attributes set a canvas drawing buffer in device
pixels. CSS width and height set its display box. If a 400×800 drawing
buffer is displayed at a different CSS size, the browser scales the bitmap.
For sharp high-density rendering, size the backing store from the CSS box and device pixel ratio:
const rect = canvas.getBoundingClientRect();
const scale = window.devicePixelRatio;
canvas.width = Math.round(rect.width * scale);
canvas.height = Math.round(rect.height * scale);
const context = canvas.getContext("2d");
context.setTransform(scale, 0, 0, scale, 0, 0);Reset the transform before drawing after a resize. Cap the scale on a large canvas when memory use would become excessive.
Track CSS Specifications and Icons #
CSS is a family of modules, not one monolithic release. CSS snapshots describe a coherent set of mature modules at a point in time; individual modules then advance independently. Check the module’s feature status and browser support instead of inferring support from a named CSS level.
Icons have separate accessibility, styling, and loading trade-offs:
| Form | Strength | Limitation |
|---|---|---|
| Icon font | Inherits color and text sizing | Monochrome glyph constraints and font loading |
| Unicode character | No extra asset | Semantics and appearance vary by font and platform |
| Inline SVG | Accessible structure and per-part styling | Increases HTML or component complexity |
| External SVG or raster image | Cacheable independent asset | Styling internal details requires explicit design |
Use an accessible name for an icon-only control:
<button type="button" aria-label="Search">
<svg aria-hidden="true" viewBox="0 0 24 24">...</svg>
</button>aria-hidden="true" belongs on decorative SVG only when the button supplies
an accessible name through text or aria-label.
Deliver Video over HTTP #
Browsers request byte ranges for seekable media:
GET /movie.mp4 HTTP/1.1
Range: bytes=32243712-32955076
If-Range: "entity-tag"A compliant range response has status 206 Partial Content and a
Content-Range header. Serve Accept-Ranges: bytes, stable validators, and
the correct media type. A server that ignores a range can return 200, but
seeking becomes expensive because the client may need the entire file.
MPEG-DASH splits presentation into media segments described by an MPD manifest. The client selects representations with different bit rates or resolutions and changes segment selection as bandwidth changes. Segments need keyframe boundaries compatible with switching; encoding one long GOP across a segment boundary impairs independent decoding.
HLS follows the same adaptive-segment principle with a playlist format. The
browser support surface differs, so select a player and packaging format based
on target platforms rather than assuming a <video src> can consume every
manifest.
Model Enumerated Values in TypeScript #
An API parameter that accepts one of a fixed set of values does not require a
numeric TypeScript enum. A string-literal union preserves wire values:
type Status = "queued" | "running" | "failed";
function retry(status: Status): boolean {
return status === "failed";
}When runtime iteration and one source of truth are needed, use a constant object:
const Status = {
queued: "queued",
running: "running",
failed: "failed",
} as const;
type Status = typeof Status[keyof typeof Status];Numeric enums can be suitable for a protocol that specifies numeric values, but their reverse mappings and emitted JavaScript are not free. Model richer predefined values as objects with a discriminant and explicit behavior rather than forcing business logic into enum members.
Explain Download and Inline Browser Behavior #
An HTTP response can ask a browser to download content:
Content-Type: application/pdf
Content-Disposition: attachment; filename="guide.pdf"attachment requests download behavior, while inline allows display in a
capable viewer. The browser, user preference, content type, sandbox policy,
and security headers can affect the final action. X-Content-Type-Options: nosniff prevents type sniffing and makes a correct Content-Type important.
Test downloads through browser automation rather than assuming a response
header gives identical UI behavior across browsers.
Render a Frame Through the GPU Pipeline #
An application submits buffers, textures, shader programs, and draw commands through an API such as WebGL, WebGPU, Vulkan, Metal, or Direct3D. Driver and GPU work then follow a conceptual pipeline:
- A vertex shader transforms each vertex and produces varying attributes.
- Primitive assembly groups vertices into lines, triangles, or points.
- Clipping removes or splits geometry outside clip space.
- Rasterization generates fragments that cover sample positions.
- A fragment shader computes per-fragment outputs.
- Depth, stencil, and blending operations combine outputs with the framebuffer.
Clip-space coordinates are compared against homogeneous clip bounds before the perspective divide. Triangles crossing a clip plane are split, so a fragment shader never receives coverage outside the visible clipped primitive.
The GPU interface moves data across a CPU-GPU boundary. Keep static geometry in device-resident buffers, update dynamic data with bounded allocations, and avoid synchronous readbacks because they force CPU and GPU progress to meet.
Position Cameras and Shade Geometry #
A camera transforms world coordinates to view coordinates, then projects them
to clip space. A perspective camera maps distant objects to smaller screen
regions; an orthographic camera preserves parallel lines and apparent size
across depth. A camera at (0, 0, 0) with identity orientation looks along
negative Z in the common Three.js convention, with positive Y as up.
Lambertian diffuse lighting models a matte surface:
diffuse = albedo × lightColor × max(dot(normal, lightDirection), 0)The dot product encodes the cosine between surface normal and light direction. Blinn-Phong adds an empirical specular term using the halfway vector between the light and view directions. Both models need normals transformed correctly: for non-uniform scaling, use the inverse-transpose of the model-view matrix.
Three.js cameras and materials expose these concepts at a higher level. Set a
camera position, call camera.lookAt(target), update its projection matrix
after changing aspect ratio, and render after scene updates. Do not assume
default camera orientation fits a custom coordinate system.
Compose Transparency and Color #
Standard source-over alpha compositing combines source and destination:
out.rgb = source.rgb × source.a + destination.rgb × (1 - source.a)
out.a = source.a + destination.a × (1 - source.a)The equation assumes premultiplied source RGB. Straight-alpha assets require the renderer’s expected conversion or blend factors. Blend in linear-light space, not gamma-encoded sRGB, because arithmetic on encoded values produces incorrect darkening and gradients.
Human color perception starts with cone responses to overlapping wavelength ranges. A color space maps numerical coordinates to a defined reference system; a coordinate without its space is ambiguous. sRGB defines primaries, a white point, a transfer function, and encoding conventions. Convert sRGB to linear values before lighting or interpolation, then encode for display.
HSL and HSV are convenient editing coordinates, not perceptually uniform spaces. Equal numeric steps in hue, saturation, or lightness do not produce equal perceived steps. For a gradient with physically meaningful blending, interpolate linear RGB. For a gradient designed around perceived lightness, consider a perceptual space such as OKLab and convert back to the target gamut, clipping or mapping out-of-gamut colors deliberately.
Magenta is a perceptual color created by simultaneous long- and short-wave cone response without a single spectral wavelength. Additive mixtures do not average into one wavelength; the visual system interprets their combined cone signals.
Run Playwright in a Container #
Playwright needs browser binaries and their operating-system libraries. Pin
the official image tag to the @playwright/test version in the project:
FROM mcr.microsoft.com/playwright:v1.58.2-noble
WORKDIR /work
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN chown -R pwuser:pwuser /work
USER pwuser
CMD ["npx", "playwright", "test"]The official image includes supported browsers and dependencies. Update the image and package versions together. Build and run with an init process:
docker build -t example-playwright-tests .
docker run --rm --init --ipc=host example-playwright-tests--init reaps browser child processes. Chromium benefits from a larger shared
memory area under --ipc=host; use it only in a trusted environment. A custom
Node base image must install both browser dependencies and the selected
browser during the image build:
FROM node:22-bookworm
WORKDIR /work
COPY package.json package-lock.json ./
RUN npm ci && npx playwright install --with-deps chromium
COPY . .
CMD ["npx", "playwright", "test"]Do not download browsers at every container startup. Persist reports, traces, screenshots, and videos as CI artifacts because failures often depend on the test environment.
Automate Virtualized Lists Without Infinite Loops #
A virtualized list creates nodes for visible items and uses a spacer to represent the full scroll range. An item absent from the DOM may still exist in the dataset, so a locator must drive scrolling and wait for a render turn.
Bound the search and detect lack of progress:
const scroller = page.locator('ul[role="listbox"] > div');
const option = page.getByRole("option", { name: targetAddress, exact: true });
for (let attempts = 0; attempts < 500; attempts += 1) {
if (await option.count()) {
await option.click();
break;
}
const moved = await scroller.evaluate((element) => {
const before = element.scrollTop;
element.scrollBy(0, Math.max(1, element.clientHeight * 0.8));
return element.scrollTop !== before;
});
if (!moved) throw new Error(`Option not found: ${targetAddress}`);
await page.waitForTimeout(0);
}waitForTimeout(0) yields to the browser event loop, allowing a scroll
listener or framework render to update the virtualized nodes. A production
test should prefer the application’s data API or a semantic search control
when it exists; pixel-like scrolling is a fallback for behavior that must be
tested through the rendered interface.
Comments
Read or comment on GitHub (sign-in required to comment)