关于 component-fixtures
component-fixtures 是用于 VS Code 组件截图测试的开发技能,面向创建和验证隔离渲染组件的开发者。它提供主题化夹具、服务辅助工具和异步渲染稳定性指导。
主要功能
- 01创建独立组件渲染夹具
- 02自动生成深色和浅色主题变体
- 03提供编辑器服务与测试辅助工具
- 04指导异步截图就绪检查
- 05支持通过组件浏览器截图验证
适用场景
- 01为 UI 组件创建截图测试
- 02检查浅色与深色主题下的组件外观
- 03排查夹具中的布局或异步渲染问题
MCP 相关
文档提及使用 `mcp_component-exp_` 工具列出夹具并捕获截图;未说明该技能本身实现 MCP 服务。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ vscode --skill component-fixtures安装后在 Claude Code 中输入/component-fixtures确认已加载。
详细介绍
组件夹具
组件夹具通过组件浏览器渲染独立的 UI 组件,用于视觉截图测试。夹具位于 src/vs/workbench/test/browser/componentFixtures/ 中,Vite 开发服务器会通过 glob src/**/*.fixture.ts 自动发现它们。
使用 mcp_component-exp_* 工具列出夹具并截取截图。如果你看不到这些工具,请告知用户启用它们。
在本地运行夹具
- 启动组件浏览器服务器:运行 Component Explorer Server 任务
- 使用
mcp_component-exp_list_fixtures工具查看所有可用夹具及其 URL - 使用
mcp_component-exp_screenshot工具以编程方式截取截图
文件结构
每个夹具文件都会导出一个默认的 defineThemedFixtureGroup(...)。文件必须以 .fixture.ts 结尾。
src/vs/workbench/test/browser/componentFixtures/
fixtureUtils.ts # Shared helpers (DO NOT import @vscode/component-explorer elsewhere)
myComponent.fixture.ts # Your fixture file
基本模式
import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from './fixtureUtils.js';
export default defineThemedFixtureGroup({ path: 'myFeature/' }, {
Default: defineComponentFixture({ render: renderMyComponent }),
AnotherVariant: defineComponentFixture({ render: renderMyComponent }),
});
function renderMyComponent({ container, disposableStore, theme }: ComponentFixtureContext): void {
container.style.width = '400px';
const instantiationService = createEditorServices(disposableStore, {
colorTheme: theme,
additionalServices: (reg) => {
// Register additional services the component needs
reg.define(IMyService, MyServiceImpl);
reg.defineInstance(IMockService, mockInstance);
},
});
const widget = disposableStore.add(
instantiationService.createInstance(MyWidget, /* constructor args */)
);
container.appendChild(widget.domNode);
}
要点:
defineThemedFixtureGroup会自动为每个夹具创建深色和浅色变体defineComponentFixture会为渲染函数封装主题设置、异步就绪处理和可释放对象管理createEditorServices会提供一个预先注册基础编辑器服务的TestInstantiationService- 始终使用
disposableStore.add(...)注册创建的控件,以防止资源泄漏 - 将
colorTheme: theme传递给createEditorServices,以确保正确渲染主题颜色
文件图标主题
夹具默认使用 Seti 文件图标。可以在单个夹具上选择其他内置主题,或禁用文件图标:
defineComponentFixture({ fileIconTheme: 'vs-minimal', render: renderMyComponent });
defineComponentFixture({ fileIconTheme: 'none', render: renderMyComponent });
当渲染的组件读取 IThemeService 时,将选定的主题从 ComponentFixtureContext 传递给 createEditorServices:
function renderMyComponent({ disposableStore, theme, fileIconTheme }: ComponentFixtureContext): void {
const instantiationService = createEditorServices(disposableStore, {
colorTheme: theme,
fileIconTheme,
});
}
fixtureUtils.ts 中的实用工具
| 导出 | 用途 |
|---|---|
defineComponentFixture | 根据渲染函数创建深色/浅色主题的夹具变体 |
defineThemedFixtureGroup | 将多个主题化夹具归入一个命名夹具组 |
createEditorServices | 创建包含所有基础编辑器服务的 TestInstantiationService |
registerWorkbenchServices | 注册其他工作台服务(上下文菜单、标签等) |
createTextModel | 通过 ModelService 为编辑器夹具创建文本模型 |
setupTheme | 将主题 CSS 应用到容器(由 defineComponentFixture 自动调用) |
darkTheme / lightTheme | 预加载的 ColorThemeData 实例 |
重要: 只有 fixtureUtils.ts 可以从 @vscode/component-explorer 导入内容。所有夹具文件都必须通过 fixtureUtils.ts 中的辅助函数进行操作。
CSS 作用域
夹具共享文档和工作台样式表(isolation: 'none')。辅助函数会限定主题和文件图标样式的作用域,但 DOM、CSS 和进程级注册不会自动隔离。
共享主题初始化
fixtureUtilsCss.ts 会在生成并缓存主题 CSS 之前,加载共享的工作台颜色和尺寸注册,包括 vs/workbench/common/agentsColors.ts 和 agentsSizes.ts 中的 Agent 令牌。请将必需的全产品注册放在此共享引导代码中,而不要放在单个夹具内:发现另一个夹具不应改变已有夹具的字体或颜色。请复用所属共享层中的轻量级注册模块,而不要导入产品入口点或更高层的扩展。
匹配生产环境 CSS 选择器
许多 VS Code 组件的 CSS 规则限定在深层祖先选择器中(例如 .interactive-session .interactive-input-part > .widget-container .my-element)。在夹具中,必须重建这些选择器所需的祖先 DOM 结构,才能使其匹配:
function render({ container }: ComponentFixtureContext): void {
container.classList.add('interactive-session');
// Recreate ancestor structure that CSS selectors expect
const inputPart = dom.$('.interactive-input-part');
const widgetContainer = dom.$('.widget-container');
inputPart.appendChild(widgetContainer);
container.appendChild(inputPart);
widgetContainer.appendChild(myWidget.domNode);
}
新组件设计建议: 避免使用依赖特定祖先元素的深层嵌套 CSS 选择器。使用自包含的类名(例如,使用 .my-widget .my-element,而不是 .parent-view .parent-part > .wrapper .my-element)。这样组件更便于创建夹具和复用。
服务
使用 createEditorServices
createEditorServices 会预先注册以下服务:IAccessibilityService、IKeybindingService、IClipboardService、IOpenerService、INotificationService、IDialogService、IUndoRedoService、ILanguageService、IConfigurationService、IStorageService、IThemeService、IModelService、ICodeEditorService、IContextKeyService、ICommandService、ITelemetryService、IHoverService、IUserInteractionService 等。
其他服务
通过 additionalServices 注册额外服务:
createEditorServices(disposableStore, {
additionalServices: (reg) => {
// Class-based (instantiated by DI):
reg.define(IMyService, MyServiceImpl);
// Instance-based (pre-constructed):
reg.defineInstance(IMyService, myMockInstance);
},
});
模拟服务
使用 base/test/common/mock.js 中的 mock<T>() 辅助函数创建模拟服务实例:
import { mock } from '../../../../base/test/common/mock.js';
const myService = new class extends mock<IMyService>() {
override someMethod(): string { return 'test'; }
override onSomeEvent = Event.None;
};
reg.defineInstance(IMyService, myService);
对于模拟视图模型或数据对象:
const element = new class extends mock<IChatRequestViewModel>() { }();
异步渲染
组件浏览器会等待渲染 Promise 完成,然后再等待两个动画帧。这些动画帧并不能保证图像解码、原生尺寸观察器或延迟布局已就绪。
渲染 Promise 必须仅在夹具达到其命名状态时才完成:
- 等待所有会影响截图的资源加载完成。编辑器的
setInput()可能会在主图像或缩略图加载完成之前返回;请等待预期的图像数量,并等待decode()完成;若图像缺失或损坏,则应失败。 - 在定位视口之前,先完成会改变布局的操作。等待内容高度、滚动高度、滚动位置和有限动画稳定,然后滚动并再次等待稳定。断言“屏幕外”的页眉确实位于屏幕外,而“可见”的页眉没有被固定内容遮挡。
- 考虑瞬态视觉效果。高度稳定并不意味着截图稳定。无头渲染在滚动或调整大小时不会闪现自动隐藏的滚动条,因此不要等待或隐藏它们;如果滚动条是预期状态的一部分,请使用
ScrollbarVisibility.Visible。 - 通过现有服务或选项提供确定性的显示数据,例如单个自定义思考短语。仅使用带种子的随机生成器,并不能确保标签不受渲染顺序或异步回调顺序影响。
- 使用有界、基于条件的等待,并在失败时抛出错误;不要使用固定时长的休眠或额外动画帧来默默接受未完成的状态。
将夹具设计为状态 + 局部输入 + 明确的就绪条件。请提供本地账户图标,而不要让模拟对象回退到在线头像 URL,并等待图标解码;waitForFixtureCondition 会在保留虚拟时钟的同时轮询原生浏览器帧。要等待 DOM 状态(例如元素、类、属性或文本出现),请使用 waitForFixtureDomCondition,它会在每次 DOM 变更后重新检查条件;如果条件始终不成立,则会在浏览器的夹具超时后失败。优先使用共享的 DOM 加载指示器,而不是动画 SVG 背景:祖先元素的 CSS 无法停止图像内部的动画。
依赖原生浏览器图像解码或尺寸/滚动回调的夹具,可以使用 virtualTime: { enabled: false } 并显式等待这些操作完成。JavaScript 虚拟时间不会推进浏览器的图像解码器或布局流水线。
对于边缘绘制顺序敏感的异步夹具,deferPaint: true 会让无头夹具在渲染完成前保持透明,而不会改变其布局或交互式预览。它可以防止中间绘制影响最终光栅化;但不能替代等待就绪。
验证 fixture
不要添加以 fixture 或 fixture 基础设施为测试对象的自动化测试(包括仅针对 fixture 的单元测试、Playwright 测试和截图稳定性测试)。使用现有的 Component Explorer 渲染、截图、稳定性和错误工具验证 fixture 变更,并辅以直接检查或临时浏览器探测。生产行为的常规测试应放在所属的生产 API 中。
在 CI 使用的同一就绪边界进行验证:在无头模式下 renderFixture() 解析后、空闲回调执行后,以及另一个 fixture 挂载后重新挂载时,立即捕获截图。在同一平台上比较精确哈希,并单独检查预期状态;不要将临时探测提交为 fixture 测试。
陷阱:重新挂载 DOM 会导致闪烁
避免在初次渲染后将已渲染的组件移到不同的 DOM 父节点下。这会导致:
- 重新计算布局(
position: absolute坐标失效,组件随之跳动) - 焦点丢失(blur 事件可能触发 QuickInput 等组件中的隐藏逻辑)
- 截图不稳定(组件浏览器可能会捕获到中间布局状态)
错误模式——异步等待后重新挂载组件:
async function render({ container }: ComponentFixtureContext): Promise<void> {
const host = document.createElement('div');
container.appendChild(host);
// ... create widget inside host ...
await waitForWidget();
container.appendChild(widget); // BAD: reparenting causes flicker
host.remove();
}
更好的模式——从一开始就在正确的 DOM 结构中原位渲染:
function render({ container }: ComponentFixtureContext): void {
// Set up the correct DOM structure first, then create the widget inside it
const widget = createWidget(container);
container.appendChild(widget.domNode);
}
如果组件确实需要异步设置(例如在内部进行渲染的 QuickInput),则应从一开始就将宿主容器构造成最终布局的样子,以尽量减少组件出现后的 DOM 操作。
为 fixture 调整现有组件
现有组件通常只需稍作修改即可用于 fixture。当编写 fixture 遇到阻碍时,应修复组件,而不是在 fixture 中绕过问题。常见的调整包括:
将 CSS 与祖先上下文解耦
如果组件的 CSS 只有在 .workbench .sidebar .my-view .my-widget 这类深层嵌套选择器下才能生效,请重构 CSS,使其自成一体。将样式移到组件自身的根类作用域下:
/* Before: requires specific ancestors */
.workbench .sidebar .my-view .my-widget .header { font-weight: bold; }
/* After: self-contained */
.my-widget .header { font-weight: bold; }
如果组件与其父级共享样式(例如继承背景色),请使用 CSS 自定义属性,而不要依赖祖先选择器。
提取硬编码的服务依赖
如果组件直接访问单例或全局状态,而不是使用 DI,请重构组件,通过构造函数接收服务:
// Before: hard to mock in fixtures
class MyWidget {
private readonly config = getSomeGlobalConfig();
}
// After: injectable and testable
class MyWidget {
constructor(@IConfigurationService private readonly configService: IConfigurationService) { }
}
添加用于控制自动聚焦和动画的选项
创建时自动聚焦或运行动画的组件会导致截图不稳定。请添加一个选项参数:
interface IMyWidgetOptions {
shouldAutoFocus?: boolean;
}
fixture 会传入 shouldAutoFocus: false。生产调用点则保留默认行为。
公开内部状态,以支持直接渲染“已完成”状态
许多组件都具有生命周期状态(loading → active → completed)。如果组件只能通过用户交互进入“已完成”状态,请添加支持,使其能够通过构造函数数据直接初始化为该状态:
// The fixture can pass pre-filled data to render the summary/completed state
// without simulating the full user interaction flow.
const carousel: IChatQuestionCarousel = {
questions,
allowSkip: true,
kind: 'questionCarousel',
isUsed: true, // Already completed
data: { 'q1': 'answer' }, // Pre-filled answers
};
让 DOM 节点可访问
如果组件在内部构建 DOM,却未公开根元素,请添加一个公开的 readonly domNode: HTMLElement 属性,以便 fixture 将其附加到容器中。
不要将仅用于测试的操作加入生产 API
当 fixture 或测试必须驱动生产环境中只能通过用户输入或服务触发的状态(例如聚焦、选择或折叠行,或定位已渲染的行)时,不要为此添加公开方法:其他代码会开始调用这些方法,从而绕过真实流程。将最少量的方法设为 protected,并在 test/ 下的仅供测试使用的子类中实现这些操作(例如 TestSessionsList extends SessionsList),这样产品代码就无法导入它。
编写适用于 fixture 的组件
设计新的 UI 组件时,请遵循以下做法,使其易于用于 fixture:
1. 在构造函数中接收容器元素
// Good: container is passed in
class MyWidget {
constructor(container: HTMLElement, @IFoo foo: IFoo) {
this.domNode = dom.append(container, dom.$('.my-widget'));
}
}
// Also good: widget creates its own domNode for the caller to place
class MyWidget {
readonly domNode: HTMLElement;
constructor(@IFoo foo: IFoo) {
this.domNode = dom.$('.my-widget');
}
}
2. 对所有服务使用依赖注入
所有外部依赖都应通过 DI 提供,以便测试固件可以提供测试实现:
// Good: services injected
constructor(@IThemeService private readonly themeService: IThemeService) { }
// Bad: reaching into globals
constructor() { this.theme = getGlobalTheme(); }
3. 保持 CSS 选择器简洁
/* Good: self-contained, easy to fixture */
.my-widget .my-header { ... }
.my-widget .my-list-item { ... }
/* Bad: requires deep ancestor chain */
.workbench .sidebar .my-view .my-widget .my-header { ... }
4. 避免在构造期间读取布局或窗口服务
在构造期间测量窗口或读取布局尺寸的组件很难用于测试固件,因为阴影 DOM 容器的尺寸与工作台不同:
// Prefer: use CSS for sizing, or accept dimensions as parameters
container.style.width = '400px';
container.style.height = '300px';
// Avoid: reading from layoutService during construction
const width = this.layoutService.mainContainerDimension.width;
5. 支持在测试固件中禁用自动聚焦
自动聚焦可能影响截图的稳定性。提供禁用自动聚焦的选项:
interface IMyWidgetOptions {
shouldAutoFocus?: boolean; // Fixtures pass false
}
6. 暴露 DOM 节点
测试固件需要将组件的 DOM 添加到容器中。将其公开为 readonly domNode: HTMLElement。
7. 让悬停和聚焦状态可渲染
测试固件无法触发 CSS :hover,而 DOM 焦点只能通过 ctx.focus() 确定性地设置。让依赖交互的视觉效果可以通过状态触发:
- 为每个视觉效果依赖的
:hover选择器配对一个.hovered类,例如.monaco-list-row:is(:hover, .hovered);:is()可保持优先级不变。列表组件已将.monaco-list-row.hovered设置为与悬停行相同的样式。 - 通过组件设置焦点和选中状态(列表行会根据列表特征添加
.focused和.selected),如果生产环境没有相应 API,则通过仅用于测试的子类设置(参见将仅用于测试的操作排除在生产 API 之外)。当状态需要 DOM 焦点时,再调用ctx.focus()。 - 如果 JavaScript 也会响应悬停(例如
mouseover监听器),除了添加.hovered,还要在元素上分发mouseover事件。
src/vs/sessions/contrib/sessions/test/browser/sessionsListFixtureUtils.ts 以这种方式渲染 interaction: { hovered, focused, selected }。
多个测试固件变体
创建不同变体,以展示同一组件的不同状态:
export default defineThemedFixtureGroup({
// Different data states
Empty: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { items: [] }) }),
WithItems: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { items: sampleItems }) }),
// Different configurations
ReadOnly: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { readonly: true }) }),
Editable: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { readonly: false }) }),
// Lifecycle states
Loading: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { state: 'loading' }) }),
Completed: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { state: 'done' }) }),
});
经验总结
请根据开发测试固件的经验更新本节!
-
不要将组件复制到测试固件中再进行修改。始终让原始组件适配测试固件,然后在测试固件中渲染它。这样可以确保测试固件测试的是实际组件代码和生命周期,而不是可能掩盖错误的修改版本。
-
不要在测试固件中重新组合子组件。 不要手动实例化并添加应由父组件创建的子组件(例如工具栏内容组件)。应正确配置父组件(例如设置正确的编辑器选项、注册正确的提供程序),让子组件通过正常代码路径出现。手动重新组合会掩盖集成错误,也无法测试真实的组件生命周期。
-
对于依赖大量服务的组件,用数据描述状态。 如果组件会读取许多服务,为其提供一个测试框架,让它接收普通状态,并通过实际负责状态的服务和生产菜单进行渲染,而不是为每个测试固件创建模拟对象并使用 DOM 变通方法。这样,测试固件只需列出数据;添加新功能时,只需向测试框架添加状态字段。会话列表测试框架(
sessionsListFixtureUtils.ts)就采用了这种模式;其中真实的工具栏暴露出了存根菜单掩盖的标头错误。 -
切勿针对每个测试固件改变进程级注册。 资源管理器 UI 会同时挂载一个文件夹中的所有测试固件,它们共用命令、菜单和其他全局注册表。共享操作只注册一次,并在所有测试固件中保持一致;通过测试固件自身的服务(例如其菜单服务)改变每个测试固件的呈现方式。截图运行会逐个渲染测试固件,无法发现挂载顺序竞争问题,因此请预览整个文件夹(
___explorer?fixture=<folder>)进行检查。