Browser Platform, Graphics, and Playwright in Docker

Last update: 2022-08-21 22:50:08 +0800 +0800
Available translations: 日本語 / 中文(简体) / 中文(繁體)

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:

APIMeasurement
window.innerWidth and innerHeightLayout viewport, including a rendered scrollbar
window.outerWidth and outerHeightBrowser window bounds
screen.width and screen.heightScreen dimensions in CSS pixels
screen.availWidth and availHeightScreen space excluding reserved operating-system areas
element.clientWidth and clientHeightContent box plus padding, excluding border and scrollbar
element.scrollWidth and scrollHeightScrollable content extent
element.offsetWidth and offsetHeightBorder-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:

html
<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:

html
<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:

html
<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.

FormParser BlockingExecution OrderDOM Availability
Classic scriptYesDocument orderNodes parsed before the tag
deferFetch is non-blockingDocument order after parsingFull parsed document
asyncFetch is non-blockingFirst completed firstNot tied to parsing completion
type="module"Fetch is non-blockingModule dependency orderDeferred 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:

css
*, *::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:

css
.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:

css
@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:

css
.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:

css
: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:

css
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:

css
@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:

js
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:

FormStrengthLimitation
Icon fontInherits color and text sizingMonochrome glyph constraints and font loading
Unicode characterNo extra assetSemantics and appearance vary by font and platform
Inline SVGAccessible structure and per-part stylingIncreases HTML or component complexity
External SVG or raster imageCacheable independent assetStyling internal details requires explicit design

Use an accessible name for an icon-only control:

html
<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:

http
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:

ts
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:

ts
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:

http
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:

  1. A vertex shader transforms each vertex and produces varying attributes.
  2. Primitive assembly groups vertices into lines, triangles, or points.
  3. Clipping removes or splits geometry outside clip space.
  4. Rasterization generates fragments that cover sample positions.
  5. A fragment shader computes per-fragment outputs.
  6. 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:

text
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:

text
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:

dockerfile
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:

sh
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:

dockerfile
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:

ts
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)