当测试建模文档加载、布局、网络和渲染,而不是把页面当作静态字符串处理时,浏览器自动化才能可靠运行。
测量浏览器和 DOM #
浏览器尺寸回答的是不同的问题:
| API | 测量对象 |
|---|---|
window.innerWidth 和 innerHeight | 布局视口,包括已渲染的滚动条 |
window.outerWidth 和 outerHeight | 浏览器窗口边界 |
screen.width 和 screen.height | 以 CSS 像素表示的屏幕尺寸 |
screen.availWidth 和 availHeight | 排除操作系统保留区域后的屏幕空间 |
element.clientWidth 和 clientHeight | 内容框加内边距,不包括边框和滚动条 |
element.scrollWidth 和 scrollHeight | 可滚动内容范围 |
element.offsetWidth 和 offsetHeight | 边框盒尺寸,四舍五入为整数像素 |
element.getBoundingClientRect() | 视口坐标中的边框盒几何信息,保留小数 |
没有布局框的内联元素的 clientWidth 为 0。对于根元素,它描述的是不包含经典滚动条的布局视口。不要在未加上滚动偏移量的情况下混用 getBoundingClientRect() 的视口坐标和文档坐标。
使用有效的 HTML 结构和资源提示 #
<link> 表示与外部资源的关系。它的 rel 属性是由空格分隔的一组标准化链接类型:
<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 会提前建立传输连接;只能对页面将要联系的源使用它。它不会下载样式表、字体或脚本。
<meta> 提供字符集、视口、描述和 HTTP 等效指令等文档元数据。将 <meta charset="utf-8"> 放在 head 的前部,使解析器能够正确解码后续字节。
<noscript> 在脚本被禁用或不受支持时提供后备内容。它的解析行为取决于它出现在 head 还是 body,因此应在 HTML 验证器和浏览器中测试生成的标记。
<p> 元素不能包含另一个段落。HTML 解析器会在内层段落之前关闭外层段落,从而使 DOM 与源文本不同。模板代码应改为构造兄弟元素或语义容器:
<div class="notice">
<p>First paragraph.</p>
<p>Second paragraph.</p>
</div>对脚本加载和模块依赖进行建模 #
没有 async 或 defer 的经典脚本会在浏览器获取并执行脚本期间阻塞 HTML 解析。因此,脚本的位置决定了哪些节点和全局变量可用:
<script src="/jquery.js"></script>
<script src="/plugin-that-needs-jquery.js"></script>第二个脚本使用第一个脚本创建的全局变量,因此必须保持该顺序。将依赖脚本移动到提供者之前会引发引用错误。
| 形式 | 是否阻塞解析器 | 执行顺序 | DOM 可用性 |
|---|---|---|---|
| 经典脚本 | 是 | 文档顺序 | 标签之前已解析的节点 |
defer | 获取不阻塞 | 解析完成后的文档顺序 | 完整的已解析文档 |
async | 获取不阻塞 | 先完成者先执行 | 不与解析完成绑定 |
type="module" | 获取不阻塞 | 模块依赖顺序 | 默认延迟执行 |
defer 适合需要保持顺序的应用脚本。async 适合分析脚本等独立脚本,因为这些脚本不能依赖文档位置或其他脚本。模块默认延迟执行,会解析 import 依赖,拥有自己的作用域,并且每个模块 URL 只执行一次。经典代码必须在解析完成后运行时使用 DOMContentLoaded;只有在图片、样式表和子框架也相关时才使用 load。
动态追加的脚本默认是异步的。只有在加载器明确需要串行化经典脚本时,才应在插入前设置 script.async = false。部署约束允许时,应优先使用原生 ES 模块和显式的 import 依赖,而不是自定义加载器。
从约束出发设计响应式 CSS #
CSS 盒模型包含内容、内边距、边框和外边距。box-sizing 决定声明的宽度和高度是否包含内边距与边框:
*, *::before, *::after {
box-sizing: border-box;
}使用 border-box 时,width: 20rem 表示边框盒宽度。使用初始值 content-box 时,内边距和边框会加到渲染宽度上。通用的边框盒规则使组件尺寸更容易组合。
Flexbox 布局一维序列。Grid 布局显式的行和列。工具栏或线性内容流适合使用 flex;当项目必须沿两个轴对齐时,应使用 grid:
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
gap: 1rem;
}minmax 表达式在窄视口中允许单列,同时保持列的可读性。gap 属于布局容器,可以避免外边距折叠或 first/last-child 例外。
媒体查询根据环境特性选择规则:
@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; }
}当视口改变设计可用空间时,应使用视口断点。当可复用组件可能出现在同一页面的不同宽度区域中时,容器查询更合适。
CSS 长度使用不同的参考框。rem 参考根字体大小,em 参考当前字体大小,百分比取决于具体属性,vw 和 vh 参考视口尺寸。如果内容需要,应在字体栈中保留非拉丁字符的字形覆盖范围:
.cjk-sample::before {
content: "漢";
font-family: "Noto Sans CJK SC", sans-serif;
}该字形用于测试字体覆盖范围,并非正文内容。
尊重用户的颜色偏好 #
声明同时支持两种配色方案,然后定义语义化颜色变量:
:root {
color-scheme: light dark;
--surface: Canvas;
--text: CanvasText;
--accent: #2867c8;
}
@media (prefers-color-scheme: dark) {
:root { --accent: #8ab4f8; }
}prefers-color-scheme 反映操作系统或用户代理的偏好。由应用控制的主题选择器可以通过在根元素上设置属性来覆盖变量,但应在已保存的偏好、用户的显式选择和系统偏好之间保留清晰的优先级规则。
嵌入式 SVG 或 iframe 是独立文档。父文档的 CSS 自定义属性不会跨越该边界;嵌入资源需要自己的样式,或需要有意设计的消息传递和主题机制。
使用可预测的后备字体加载字体 #
字体族包含常规、斜体、粗体和粗斜体等相关字面。字形是字符的可见表示。浏览器会按顺序遍历 font-family,为每个字符选择字体:
body {
font-family: "Example Sans", system-ui, sans-serif;
}当某个字面缺少某个字形时,可以从后续字体族中选择后备字体。名称包含空格的字体族名称必须加引号。
@font-face 将字体族名称与可下载资源关联起来:
@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 是用于 OpenType 字体数据的压缩 Web 字体容器。可变字体可以公开 wght、wdth、opsz 和 ital 等轴;应将声明的范围限制为网站实际发布的字面。font-display: swap 会在 Web 字体加载期间显示后备文本,因此应选择度量兼容的后备字体,或使用字体度量覆盖来减少布局偏移。
window.queryLocalFonts() 需要权限,并且浏览器支持有限。不要让网站的渲染依赖于枚举本地字体。
正确缩放 Canvas #
HTML 的 width 和 height 属性设置 Canvas 的设备像素绘图缓冲区。CSS 的 width 和 height 设置其显示框。如果以不同的 CSS 尺寸显示 400×800 的绘图缓冲区,浏览器会缩放位图。
为了进行清晰的高密度渲染,应根据 CSS 框和设备像素比设置后备存储大小:
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);调整大小后、绘制之前要重置变换。当大型 Canvas 的内存使用量可能过高时,应限制缩放比例。
跟踪 CSS 规范和图标 #
CSS 是一组模块,而不是一次单体式发布。CSS 快照描述某个时间点上一组协调一致的成熟模块;之后各模块会独立演进。不要根据命名的 CSS 级别推断支持情况,而应检查模块的功能状态和浏览器支持情况。
图标在可访问性、样式和加载方面存在不同的权衡:
| 形式 | 优点 | 限制 |
|---|---|---|
| 图标字体 | 继承 color 和文本大小 | 单色字形限制以及字体加载 |
| Unicode 字符 | 无需额外资源 | 语义和外观因字体和平台而异 |
| 内联 SVG | 可访问结构和逐部分样式 | 增加 HTML 或组件复杂度 |
| 外部 SVG 或光栅图像 | 可作为独立资源缓存 | 为内部细节设置样式需要显式设计 |
对于仅包含图标的控件,应使用可访问名称:
<button type="button" aria-label="Search">
<svg aria-hidden="true" viewBox="0 0 24 24">...</svg>
</button>只有在按钮通过文本或 aria-label 提供可访问名称时,才应将 aria-hidden="true" 放在装饰性 SVG 上。
通过 HTTP 传输视频 #
浏览器会为可跳转媒体请求字节范围:
GET /movie.mp4 HTTP/1.1
Range: bytes=32243712-32955076
If-Range: "entity-tag"符合规范的范围响应状态为 206 Partial Content,并包含 Content-Range 响应头。应提供 Accept-Ranges: bytes、稳定的验证器以及正确的媒体类型。忽略范围请求的服务器可以返回 200,但跳转会变得昂贵,因为客户端可能需要整个文件。
MPEG-DASH 将演示内容拆分为媒体片段,并由 MPD 清单描述。客户端选择具有不同比特率或分辨率的表示,并随着带宽变化调整片段选择。片段需要具有适合切换的关键帧边界;如果跨越片段边界编码一个很长的 GOP,就会损害独立解码。
HLS 通过播放列表格式遵循相同的自适应分段原则。浏览器支持范围不同,因此应根据目标平台选择播放器和打包格式,而不要假定 <video src> 能够消费所有清单。
在 TypeScript 中建模枚举值 #
接受固定值集合之一的 API 参数不需要使用数值 TypeScript enum。字符串字面量联合能够保留线路值:
type Status = "queued" | "running" | "failed";
function retry(status: Status): boolean {
return status === "failed";
}需要运行时迭代和单一事实来源时,可以使用常量对象:
const Status = {
queued: "queued",
running: "running",
failed: "failed",
} as const;
type Status = typeof Status[keyof typeof Status];对于规定数值的协议,数值枚举可能合适,但其反向映射和生成的 JavaScript 并非没有成本。应将更丰富的预定义值建模为带有判别字段和显式行为的对象,而不是把业务逻辑强行放入枚举成员。
解释下载和内联浏览器行为 #
HTTP 响应可以要求浏览器下载内容:
Content-Type: application/pdf
Content-Disposition: attachment; filename="guide.pdf"attachment 请求下载行为,而 inline 允许在支持的查看器中显示。浏览器、用户偏好、内容类型、沙箱策略和安全响应头都可能影响最终行为。X-Content-Type-Options: nosniff 会阻止类型嗅探,因此正确的 Content-Type 很重要。应通过浏览器自动化测试下载,而不要假定响应头会在不同浏览器中产生相同的 UI 行为。
通过 GPU 管线渲染帧 #
应用通过 WebGL、WebGPU、Vulkan、Metal 或 Direct3D 等 API 提交缓冲区、纹理、着色器程序和绘制命令。随后,驱动程序和 GPU 按照概念上的管线处理这些数据:
- 顶点着色器变换每个顶点,并生成 varying 属性。
- 图元装配将顶点组合为线、三角形或点。
- 裁剪移除或拆分位于裁剪空间之外的几何体。
- 光栅化生成覆盖采样位置的片段。
- 片段着色器计算每个片段的输出。
- 深度、模板和混合操作将输出与帧缓冲区合并。
在透视除法之前,裁剪空间坐标会与齐次裁剪边界进行比较。穿过裁剪平面的三角形会被拆分,因此片段着色器不会接收到可见裁剪图元之外的覆盖范围。
GPU 接口会在 CPU-GPU 边界之间移动数据。将静态几何体保存在设备驻留缓冲区中,使用有界分配更新动态数据,并避免同步读回,因为同步读回会迫使 CPU 和 GPU 的进度会合。
定位摄像机并为几何体着色 #
摄像机将世界坐标变换为视图坐标,然后投影到裁剪空间。透视摄像机会将远处物体映射到更小的屏幕区域;正交摄像机则在深度方向上保持平行线和表观尺寸。在常见的 Three.js 约定中,位于 (0, 0, 0) 且方向为单位方向的摄像机沿负 Z 轴观察,正 Y 轴向上。
Lambert 漫反射光照模型描述哑光表面:
diffuse = albedo × lightColor × max(dot(normal, lightDirection), 0)点积表示表面法线与光照方向之间夹角的余弦。Blinn-Phong 使用光照方向与视线方向之间的半程向量,增加经验性的镜面反射项。两个模型都要求正确变换法线:对于非均匀缩放,应使用模型视图矩阵的逆转置矩阵。
Three.js 的摄像机和材质在更高层次上提供了这些概念。设置摄像机位置,调用 camera.lookAt(target),更改宽高比后更新投影矩阵,并在场景更新后进行渲染。不要假定默认摄像机方向适合自定义坐标系。
合成透明度和颜色 #
标准的 source-over alpha 合成会组合源和目标:
out.rgb = source.rgb × source.a + destination.rgb × (1 - source.a)
out.a = source.a + destination.a × (1 - source.a)该公式假设源 RGB 使用预乘形式。straight-alpha 资源需要使用渲染器期望的转换或混合因子。在与伽马编码的 sRGB 中进行混合,而不是在线性光空间中进行混合,会导致错误的变暗和渐变,因为对编码值进行算术运算会产生不正确的结果。
人类颜色感知始于视锥细胞对重叠波长范围的响应。色彩空间将数值坐标映射到定义好的参考系统;没有所属空间的坐标是有歧义的。sRGB 定义原色、白点、传递函数和编码约定。在光照或插值前将 sRGB 转换为线性值,然后为显示进行编码。
HSL 和 HSV 是便于编辑的坐标,并非感知均匀的空间。色相、饱和度或亮度中相同的数值步长不会产生相同的感知步长。对于具有物理意义的混合渐变,应插值线性 RGB。对于围绕感知亮度设计的渐变,可以考虑 OKLab 等感知空间,并转换回目标色域;对超出色域的颜色进行裁剪或映射时,应明确决定处理方式。
品红是一种感知颜色,由长波和短波的视锥响应同时产生,而不存在对应的单一光谱波长。加法混合不会平均为一个波长;视觉系统会解释组合后的视锥信号。
在容器中运行 Playwright #
Playwright 需要浏览器二进制文件及其操作系统库。将官方镜像标签固定为项目中 @playwright/test 的版本:
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"]官方镜像包含受支持的浏览器及其依赖。应同时更新镜像版本和软件包版本。使用 init 进程进行构建和运行:
docker build -t example-playwright-tests .
docker run --rm --init --ipc=host example-playwright-tests--init 会回收浏览器子进程。在 --ipc=host 下,Chromium 可以使用更大的共享内存区域;但只能在受信任的环境中使用。自定义 Node 基础镜像必须在构建镜像时安装浏览器依赖和选定的浏览器:
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"]不要在每次容器启动时下载浏览器。应将报告、跟踪文件、屏幕截图和视频作为 CI 工件持久化,因为失败往往取决于测试环境。
自动化虚拟化列表而不陷入无限循环 #
虚拟化列表为可见项目创建节点,并使用间隔元素表示完整的滚动范围。某个项目可能不在 DOM 中,但仍存在于数据集中,因此定位器必须驱动滚动并等待一次渲染周期。
限制搜索次数,并检测没有进展的情况:
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) 将控制权交还给浏览器事件循环,使滚动监听器或框架渲染能够更新虚拟化节点。在生产测试中,如果应用提供了数据 API 或语义化搜索控件,应优先使用它们;类似像素滚动的方式只是必须通过渲染界面测试的行为的后备方案。
评论
在 GitHub 上阅读或评论(发表评论需要登录)