component-fixtures by microsoft/vscode
npx skills add https://github.com/microsoft/vscode --skill component-fixtures组件夹具通过组件浏览器渲染独立的 UI 组件,用于可视化截图测试。夹具文件位于 src/vs/workbench/test/browser/componentFixtures/ 目录下,并由 Vite 开发服务器通过 glob 模式 src/**/*.fixture.ts 自动发现。
使用工具 mcp_component-exp_* 来列出和截图夹具。如果看不到这些工具,请告知用户启用它们。
mcp_component-exp_list_fixtures 工具查看所有可用的夹具及其 URLmcp_component-exp_screenshot 工具以编程方式捕获截图每个夹具文件导出一个默认的 defineThemedFixtureGroup(...)。文件必须以 .fixture.ts 结尾。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
src/vs/workbench/test/browser/componentFixtures/
fixtureUtils.ts # 共享辅助工具(请勿在其他地方导入 @vscode/component-explorer)
myComponent.fixture.ts # 你的夹具文件
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) => {
// 注册组件需要的额外服务
reg.define(IMyService, MyServiceImpl);
reg.defineInstance(IMockService, mockInstance);
},
});
const widget = disposableStore.add(
instantiationService.createInstance(MyWidget, /* 构造函数参数 */)
);
container.appendChild(widget.domNode);
}
要点:
defineThemedFixtureGroup 自动为每个夹具创建深色和浅色变体defineComponentFixture 用主题设置和 Shadow DOM 隔离包装你的渲染函数createEditorServices 提供一个预注册了基础编辑器服务的 TestInstantiationServicedisposableStore.add(...) 注册创建的部件,以防止内存泄漏colorTheme: theme 传递给 createEditorServices,以便正确渲染主题颜色| 导出 | 用途 |
|---|---|
defineComponentFixture | 从渲染函数创建深色/浅色主题夹具变体 |
defineThemedFixtureGroup | 将多个主题化夹具分组到一个命名的夹具组中 |
createEditorServices | 创建包含所有基础编辑器服务的 TestInstantiationService |
registerWorkbenchServices | 注册额外的工作台服务(上下文菜单、标签等) |
createTextModel | 通过 ModelService 为编辑器夹具创建文本模型 |
setupTheme | 将主题 CSS 应用到容器(由 defineComponentFixture 自动调用) |
darkTheme / lightTheme | 预加载的 ColorThemeData 实例 |
重要: 只有 fixtureUtils.ts 可以从 @vscode/component-explorer 导入。所有夹具文件都必须通过 fixtureUtils.ts 中的辅助工具。
夹具在 Shadow DOM 内部渲染。组件浏览器会自动采用全局 VS Code 样式表和主题 CSS。
许多 VS Code 组件的 CSS 规则作用域限定在深层祖先选择器(例如,.interactive-session .interactive-input-part > .widget-container .my-element)。在夹具中,你必须重新创建所需祖先 DOM 结构,以便这些选择器能够匹配:
function render({ container }: ComponentFixtureContext): void {
container.classList.add('interactive-session');
// 重新创建 CSS 选择器期望的祖先结构
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 预注册了这些服务:IAccessibilityService、IKeybindingService、IClipboardService、IOpenerService、INotificationService、IDialogService、IUndoRedoService、ILanguageService、IConfigurationService、IStorageService、IThemeService、IModelService、ICodeEditorService、IContextKeyService、ICommandService、ITelemetryService、IHoverService、IUserInteractionService 等。
通过 additionalServices 注册额外服务:
createEditorServices(disposableStore, {
additionalServices: (reg) => {
// 基于类的(由 DI 实例化):
reg.define(IMyService, MyServiceImpl);
// 基于实例的(预构建的):
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>() { }();
组件浏览器在同步渲染函数返回后等待 2 个动画帧。对于大多数组件来说,这已经足够了。
如果你的渲染函数返回一个 Promise,组件浏览器会等待该 Promise 解析。
避免在初始渲染后将渲染的部件在 DOM 父元素之间移动。这会导致:
position: absolute 坐标失效时,部件会跳动)不良模式 — 在异步等待后重新挂载部件:
async function render({ container }: ComponentFixtureContext): Promise<void> {
const host = document.createElement('div');
container.appendChild(host);
// ... 在 host 内部创建部件 ...
await waitForWidget();
container.appendChild(widget); // 错误:重新挂载会导致闪烁
host.remove();
}
更好的模式 — 从一开始就在正确的位置渲染正确的 DOM 结构:
function render({ container }: ComponentFixtureContext): void {
// 首先设置正确的 DOM 结构,然后在其中创建部件
const widget = createWidget(container);
container.appendChild(widget.domNode);
}
如果组件绝对需要异步设置(例如,QuickInput 在内部渲染),请通过从一开始就构建与最终布局匹配的宿主容器,以最小化部件出现后的 DOM 操作。
现有组件通常需要进行小的更改才能变得可夹具化。当编写夹具时发现摩擦点时,修复组件本身 — 不要在夹具中绕过它。常见的调整:
如果一个组件的 CSS 只能在像 .workbench .sidebar .my-view .my-widget 这样的深层嵌套选择器内工作,请重构 CSS 使其自包含。移动样式,使其作用域限定在组件自身的根类上:
/* 之前:需要特定的祖先 */
.workbench .sidebar .my-view .my-widget .header { font-weight: bold; }
/* 之后:自包含 */
.my-widget .header { font-weight: bold; }
如果组件与其父级共享样式(例如,继承背景颜色),请使用 CSS 自定义属性,而不是依赖祖先选择器。
如果一个组件直接访问单例或全局状态,而不是使用 DI,请重构它,使其通过构造函数接受服务:
// 之前:在夹具中难以模拟
class MyWidget {
private readonly config = getSomeGlobalConfig();
}
// 之后:可注入且可测试
class MyWidget {
constructor(@IConfigurationService private readonly configService: IConfigurationService) { }
}
在创建时自动聚焦或运行动画的组件会导致截图不稳定。添加一个选项参数:
interface IMyWidgetOptions {
shouldAutoFocus?: boolean;
}
夹具传递 shouldAutoFocus: false。生产环境的调用站点保持默认行为。
许多组件具有生命周期状态(加载 → 活动 → 完成)。如果组件只能通过用户交互达到“已完成”状态,请添加支持,允许通过构造函数数据直接初始化到该状态:
// 夹具可以传递预填充的数据来渲染摘要/已完成状态,
// 而无需模拟完整的用户交互流程。
const carousel: IChatQuestionCarousel = {
questions,
allowSkip: true,
kind: 'questionCarousel',
isUsed: true, // 已完成
data: { 'q1': 'answer' }, // 预填充的答案
};
如果一个组件在内部构建其 DOM 并且不公开根元素,请添加一个公共的 readonly domNode: HTMLElement 属性,以便夹具可以将其附加到容器。
在设计新的 UI 组件时,遵循以下实践,使其易于夹具测试:
// 好:容器被传入
class MyWidget {
constructor(container: HTMLElement, @IFoo foo: IFoo) {
this.domNode = dom.append(container, dom.$('.my-widget'));
}
}
// 也好:部件创建自己的 domNode 供调用者放置
class MyWidget {
readonly domNode: HTMLElement;
constructor(@IFoo foo: IFoo) {
this.domNode = dom.$('.my-widget');
}
}
所有外部依赖都应通过 DI 提供,以便夹具可以提供测试实现:
// 好:服务被注入
constructor(@IThemeService private readonly themeService: IThemeService) { }
// 不好:访问全局变量
constructor() { this.theme = getGlobalTheme(); }
/* 好:自包含,易于夹具测试 */
.my-widget .my-header { ... }
.my-widget .my-list-item { ... }
/* 不好:需要深层祖先链 */
.workbench .sidebar .my-view .my-widget .my-header { ... }
在构造期间测量窗口或读取布局尺寸的组件很难进行夹具测试,因为 Shadow DOM 容器的尺寸与工作台不同:
// 推荐:使用 CSS 进行尺寸调整,或将尺寸作为参数接受
container.style.width = '400px';
container.style.height = '300px';
// 避免:在构造期间从 layoutService 读取
const width = this.layoutService.mainContainerDimension.width;
自动聚焦可能会干扰截图的稳定性。提供选项来禁用它:
interface IMyWidgetOptions {
shouldAutoFocus?: boolean; // 夹具传递 false
}
夹具需要将部件的 DOM 附加到容器。将其公开为公共的 readonly domNode: HTMLElement。
创建变体以显示同一组件的不同状态:
export default defineThemedFixtureGroup({
// 不同的数据状态
Empty: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { items: [] }) }),
WithItems: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { items: sampleItems }) }),
// 不同的配置
ReadOnly: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { readonly: true }) }),
Editable: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { readonly: false }) }),
// 生命周期状态
Loading: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { state: 'loading' }) }),
Completed: defineComponentFixture({ render: (ctx) => renderWidget(ctx, { state: 'done' }) }),
});
根据你的夹具开发经验更新此部分!
不要将组件复制到夹具中并在那里修改。始终调整原始组件使其夹具友好,然后在夹具中渲染它。这确保了夹具测试的是真实的组件代码和生命周期,而不是可能隐藏错误的修改版本。
不要在夹具中重新组合子部件。 永远不要手动实例化和添加一个父组件应该创建的子部件(例如,工具栏内容部件)。相反,正确配置父组件(例如,设置正确的编辑器选项,注册正确的提供程序),以便子部件通过正常的代码路径出现。手动重新组合会隐藏集成错误,并且不会测试真实的部件生命周期。
每周安装数
74
代码仓库
GitHub 星标数
183.0K
首次出现
2026年2月27日
安全审计
安装于
kimi-cli74
gemini-cli74
amp74
cline74
github-copilot74
codex74
Component fixtures render isolated UI components for visual screenshot testing via the component explorer. Fixtures live in src/vs/workbench/test/browser/componentFixtures/ and are auto-discovered by the Vite dev server using the glob src/**/*.fixture.ts.
Use tools mcp_component-exp_* to list and screenshot fixtures. If you cannot see these tools, inform the user to them on.
mcp_component-exp_list_fixtures tool to see all available fixtures and their URLsmcp_component-exp_screenshot tool to capture screenshots programmaticallyEach fixture file exports a default defineThemedFixtureGroup(...). The file must end with .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);
}
Key points:
defineThemedFixtureGroup automatically creates Dark and Light variants for each fixturedefineComponentFixture wraps your render function with theme setup and shadow DOM isolationcreateEditorServices provides a TestInstantiationService with base editor services pre-registereddisposableStore.add(...) to prevent leakscolorTheme: theme to createEditorServices so theme colors render correctly| Export | Purpose |
|---|---|
defineComponentFixture | Creates Dark/Light themed fixture variants from a render function |
defineThemedFixtureGroup | Groups multiple themed fixtures into a named fixture group |
createEditorServices | Creates TestInstantiationService with all base editor services |
registerWorkbenchServices | Registers additional workbench services (context menu, label, etc.) |
createTextModel | Creates a text model via for editor fixtures |
Important: Only fixtureUtils.ts may import from @vscode/component-explorer. All fixture files must go through the helpers in fixtureUtils.ts.
Fixtures render inside shadow DOM. The component-explorer automatically adopts the global VS Code stylesheets and theme CSS.
Many VS Code components have CSS rules scoped to deep ancestor selectors (e.g., .interactive-session .interactive-input-part > .widget-container .my-element). In fixtures, you must recreate the required ancestor DOM structure for these selectors to match:
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);
}
Design recommendation for new components: Avoid deeply nested CSS selectors that require specific ancestor elements. Use self-contained class names (e.g., .my-widget .my-element rather than .parent-view .parent-part > .wrapper .my-element). This makes components easier to fixture and reuse.
createEditorServices pre-registers these services: IAccessibilityService, IKeybindingService, IClipboardService, IOpenerService, INotificationService, IDialogService, IUndoRedoService, ILanguageService, IConfigurationService, IStorageService, , , , , , , , , and more.
Register extra services via additionalServices:
createEditorServices(disposableStore, {
additionalServices: (reg) => {
// Class-based (instantiated by DI):
reg.define(IMyService, MyServiceImpl);
// Instance-based (pre-constructed):
reg.defineInstance(IMyService, myMockInstance);
},
});
Use the mock<T>() helper from base/test/common/mock.js to create mock service instances:
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);
For mock view models or data objects:
const element = new class extends mock<IChatRequestViewModel>() { }();
The component explorer waits 2 animation frames after the synchronous render function returns. For most components, this is sufficient.
If your render function returns a Promise, the component explorer waits for the promise to resolve.
Avoid moving rendered widgets between DOM parents after initial render. This causes:
position: absolute coordinates become invalid)Bad pattern — reparenting a widget after async wait:
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();
}
Better pattern — render in-place with the correct DOM structure from the start:
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);
}
If the component absolutely requires async setup (e.g., QuickInput which renders internally), minimize DOM manipulation after the widget appears by structuring the host container to match the final layout from the beginning.
Existing components often need small changes to become fixturable. When writing a fixture reveals friction, fix the component — don't work around it in the fixture. Common adaptations:
If a component's CSS only works inside a deeply nested selector like .workbench .sidebar .my-view .my-widget, refactor the CSS to be self-contained. Move the styles so they're scoped to the component's own root class:
/* Before: requires specific ancestors */
.workbench .sidebar .my-view .my-widget .header { font-weight: bold; }
/* After: self-contained */
.my-widget .header { font-weight: bold; }
If the component shares styles with its parent (e.g., inheriting background color), use CSS custom properties rather than relying on ancestor selectors.
If a component reaches into singletons or global state instead of using DI, refactor it to accept services through the constructor:
// Before: hard to mock in fixtures
class MyWidget {
private readonly config = getSomeGlobalConfig();
}
// After: injectable and testable
class MyWidget {
constructor(@IConfigurationService private readonly configService: IConfigurationService) { }
}
Components that auto-focus on creation or run animations cause flaky screenshots. Add an options parameter:
interface IMyWidgetOptions {
shouldAutoFocus?: boolean;
}
The fixture passes shouldAutoFocus: false. The production call site keeps the default behavior.
Many components have lifecycle states (loading → active → completed). If the component can only reach the "completed" state through user interaction, add support for initializing directly into that state via constructor data:
// 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
};
If a component builds its DOM internally and doesn't expose the root element, add a public readonly domNode: HTMLElement property so fixtures can append it to the container.
When designing new UI components, follow these practices to make them easy to fixture:
// 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');
}
}
All external dependencies should come through DI so fixtures can provide test implementations:
// Good: services injected
constructor(@IThemeService private readonly themeService: IThemeService) { }
// Bad: reaching into globals
constructor() { this.theme = getGlobalTheme(); }
/* 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 { ... }
Components that measure the window or read layout dimensions during construction are hard to fixture because the shadow DOM container has different dimensions than the workbench:
// 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;
Auto-focus can interfere with screenshot stability. Provide options to disable it:
interface IMyWidgetOptions {
shouldAutoFocus?: boolean; // Fixtures pass false
}
The fixture needs to append the widget's DOM to the container. Expose it as a public readonly domNode: HTMLElement.
Create variants to show different states of the same component:
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' }) }),
});
Update this section with insights from your fixture development experience!
Do not copy the component to the fixture and modify it there. Always adapt the original component to be fixture-friendly, then render it in the fixture. This ensures the fixture tests the real component code and lifecycle, rather than a modified version that may hide bugs.
Don't recompose child widgets in fixtures. Never manually instantiate and add a sub-widget (e.g., a toolbar content widget) that the parent component is supposed to create. Instead, configure the parent correctly (e.g., set the right editor option, register the right provider) so the child appears through the normal code path. Manually recomposing hides integration bugs and doesn't test the real widget lifecycle.
Weekly Installs
74
Repository
GitHub Stars
183.0K
First Seen
Feb 27, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
kimi-cli74
gemini-cli74
amp74
cline74
github-copilot74
codex74
后端测试指南:API端点、业务逻辑与数据库测试最佳实践
11,800 周安装
Datadog自动化监控:通过Rube MCP与Composio实现指标、日志、仪表板管理
69 周安装
Intercom自动化指南:通过Rube MCP与Composio实现客户支持对话管理
69 周安装
二进制初步分析指南:使用ReVa工具快速识别恶意软件与逆向工程
69 周安装
PrivateInvestigator 道德人员查找工具 | 公开数据调查、反向搜索与背景研究
69 周安装
TorchTitan:PyTorch原生分布式大语言模型预训练平台,支持4D并行与H100 GPU加速
69 周安装
screenshot 截图技能:跨平台桌面截图工具,支持macOS/Linux权限管理与多模式捕获
69 周安装
ModelServicesetupTheme | Applies theme CSS to a container (called automatically by defineComponentFixture) |
darkTheme / lightTheme | Pre-loaded ColorThemeData instances |
IThemeServiceIModelServiceICodeEditorServiceIContextKeyServiceICommandServiceITelemetryServiceIHoverServiceIUserInteractionService