mcpField
C
component-fixtures

component-fixtures

github.com/microsoftテスト・品質194.4k273 回インストール今日更新
リポジトリ

component-fixtures について

component-fixtures は、スクリーンショットテスト用に VS Code の UI コンポーネントを個別にレンダリングする開発スキルです。テーマ別のフィクスチャと、サービス設定や非同期描画、安定したキャプチャの手引きを提供します。

主な機能

  1. 01個別コンポーネントの描画フィクスチャ作成
  2. 02ダーク・ライトテーマのバリアント生成
  3. 03エディターサービスとテスト用ヘルパー
  4. 04非同期スクリーンショットの準備確認
  5. 05コンポーネントエクスプローラーで描画を検証

活用シーン

  1. 01UI コンポーネントのスクリーンショット用フィクスチャ作成
  2. 02両テーマでのコンポーネント表示確認
  3. 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 開発サーバーによって src/**/*.fixture.ts の glob を使用して自動検出されます。

フィクスチャの一覧表示とスクリーンショット撮影には mcp_component-exp_* ツールを使用します。これらのツールが表示されない場合は、ユーザーに有効にするよう伝えてください。

ローカルでのフィクスチャの実行

  1. コンポーネントエクスプローラーサーバーを起動します:Component Explorer Server タスクを実行します
  2. mcp_component-exp_list_fixtures ツールを使って、利用可能なすべてのフィクスチャとその URL を確認します
  3. 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(...) に登録します
  • テーマカラーを正しくレンダリングするため、createEditorServices に colorTheme: theme を渡します
ファイルアイコンのテーマ

フィクスチャではデフォルトで 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 インスタンスです

重要: @vscode/component-explorer をインポートできるのは fixtureUtils.ts のみです。すべてのフィクスチャファイルで、fixtureUtils.ts のヘルパーを使用する必要があります。

CSS のスコープ設定

フィクスチャはドキュメントとワークベンチのスタイルシートを共有します(isolation: 'none')。ヘルパーはテーマとファイルアイコンのスタイルのスコープを設定しますが、DOM、CSS、プロセス全体に適用される登録は自動的には分離されません。

共有テーマの初期化

fixtureUtilsCss.ts は、テーマ CSS の生成とキャッシュに先立ち、ワークベンチ共通のカラーとサイズの登録を読み込みます。これには vs/workbench/common/agentsColors.ts と agentsSizes.ts にあるエージェントトークンも含まれます。必要な製品全体の登録は、個別のフィクスチャではなく、この共有ブートストラップに含めてください。別のフィクスチャが検出されても、既存のフィクスチャのタイポグラフィや色が変わらないようにする必要があります。製品のエントリポイントや上位レイヤーのコントリビューションをインポートするのではなく、所有する共有レイヤーの軽量な登録モジュールを再利用してください。

製品版 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 セレクターは避けてください。自己完結したクラス名(例:.parent-view .parent-part > .wrapper .my-element ではなく .my-widget .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>() { }();

非同期レンダリング

コンポーネントエクスプローラーはレンダリングのプロミスを待機し、その後、アニメーションフレームを 2 回待機します。ただし、これらのフレームは、画像のデコード、ネイティブのリサイズオブザーバー、遅延レイアウトが完了したことを保証するものではありません。

レンダリングのプロミスは、フィクスチャが名前の示す状態を表す場合にのみ解決される必要があります。

  • スクリーンショットに影響するアセットの読み込みを待機します。エディターの setInput() は、メイン画像やサムネイルの読み込みが完了する前に戻ることがあります。想定される画像数を待ち、decode() を待機し、画像が見つからない場合や壊れている場合は失敗させます。
  • ビューポートの位置を設定する前に、レイアウトを変更する操作を完了させます。コンテンツの高さ、スクロールの高さ、スクロール位置、有限時間のアニメーションが落ち着くまで待機してからスクロールし、再度落ち着くのを待ちます。「画面外」のヘッダーが実際に画面外にあり、「表示中」のヘッダーが固定コンテンツに覆われていないことを確認します。
  • 一時的な表示にも注意します。高さが安定していても、スクリーンショットが安定しているとは限りません。ヘッドレスレンダリングでは、スクロールやリサイズ時に自動的に隠れるスクロールバーが点滅することはないため、スクロールバーを待ったり隠したりしないでください。スクロールバーが意図した状態の一部である場合は、ScrollbarVisibility.Visible を使用します。
  • 既存のサービスやオプションを使って、表示データを決定論的に指定します。たとえば、カスタムの思考フレーズを 1 つ指定します。乱数生成器にシードを設定するだけでは、レンダリング順序や非同期コールバックの順序に左右されないラベルにはなりません。
  • 固定時間のスリープや、不完全な状態を暗黙に受け入れる余分なフレームではなく、失敗時に例外を投げる、上限付きの条件ベースの待機を使用します。

フィクスチャは、状態+ローカル入力+明示的な準備完了条件として設計します。モックが実際のアバター URL にフォールバックしないよう、ローカルのアカウントアイコンを指定してデコードを待機します。waitForFixtureCondition は、仮想クロックを維持しながらネイティブのブラウザーフレームをポーリングします。画像内のアニメーションは祖先要素の CSS では停止できないため、アニメーション SVG 背景より、共有 DOM スピナーを優先してください。

ネイティブのブラウザーによる画像デコードや、リサイズ/スクロールのコールバックに依存する非同期フィクスチャでは、virtualTime: { enabled: false } を使用して、それらの処理を明示的に待機できます。仮想 JavaScript 時間は、ブラウザーの画像デコーダーやレイアウトパイプラインを進めません。

描画順序に左右されるエッジを持つ非同期フィクスチャでは、deferPaint: true を設定すると、レンダリングが完了するまでヘッドレスフィクスチャを透明に保てます。レイアウトやインタラクティブなプレビューは変わりません。これにより、中間描画が最終的なラスター化に影響するのを防げますが、準備完了を待つ処理の代わりにはなりません。

フィクスチャの検証

フィクスチャやフィクスチャのインフラストラクチャを対象とする自動テスト(フィクスチャ専用のユニットテスト、Playwright テスト、スクリーンショット安定性テストを含む)は追加しないでください。フィクスチャの変更は、既存のコンポーネントエクスプローラーのレンダリング、スクリーンショット、安定性、エラーツールに加え、直接の確認や一時的なブラウザープローブを使って検証します。通常のテストで本番の動作を検証する場合は、該当する本番 API を対象にします。

CI で使用するのと同じ準備完了の境界で検証します。ヘッドレスの renderFixture() が解決した直後、アイドルコールバック後、別のフィクスチャの後に再マウントした後にキャプチャします。同一プラットフォームで正確なハッシュを比較し、意図した状態も個別に確認します。一時的なプローブをフィクスチャのテストとしてコミットしないでください。

落とし穴:DOM の再親化によるちらつき

初回レンダリング後に、レンダリング済みのウィジェットを別の DOM 親要素に移動するのは避けてください。次の問題が発生します。

  • レイアウトの再計算(position: absolute の座標が無効になり、ウィジェットが跳ねるように移動します)
  • フォーカスの喪失(ぼかしイベントによって、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 を操作することを最小限に抑えてください。

フィクスチャ用の既存コンポーネントの調整

既存のコンポーネントをフィクスチャで使えるようにするには、少し変更が必要なことがよくあります。フィクスチャを作成する際に問題が見つかった場合は、フィクスチャ側で回避するのではなく、コンポーネントを修正してください。よくある調整は次のとおりです。

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;
}

フィクスチャでは shouldAutoFocus: false を渡します。製品コードの呼び出し元では、デフォルトの動作が維持されます。

「完了済み」の状態でレンダリングするために内部状態を公開する

多くのコンポーネントには、ライフサイクルの状態(読み込み中 → アクティブ → 完了)があります。ユーザー操作を通じてしか「完了」状態に到達できない場合は、コンストラクターデータを使ってその状態に直接初期化できるようにしてください。

// 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 というパブリックプロパティを追加してください。

テスト専用の操作を製品 API に含めない

フィクスチャやテストで、製品コードではユーザー入力やサービスを通じてのみ変更される状態(たとえば、行のフォーカス、選択、折りたたみ、またはレンダリングされた行の検索)を操作する必要がある場合、そのためのパブリックメソッドは追加しないでください。他のコードがそのメソッドを呼び出し、実際のフローを迂回するようになります。必要最小限のメンバーを protected にし、操作は test/ 以下のテスト専用サブクラス(TestSessionsList extends SessionsList など)に実装してください。製品コードからはインポートできません。

フィクスチャで使いやすいコンポーネントの作成

新しい UI コンポーネントを設計する際は、フィクスチャで使いやすくするため、次のプラクティスに従ってください。

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 ハックではなく、プレーンな状態を受け取り、実際の状態を所有するサービスと本番メニューを通じてそれをレンダリングする 1 つのハーネスを与えます。そうすれば、フィクスチャはデータを列挙するだけで済み、新機能はハーネスに状態フィールドを追加します。セッションリストのハーネス(sessionsListFixtureUtils.ts)はこのパターンに従っています。その実際のツールバーは、スタブ化されたメニューが隠していたヘッダーのバグを露呈しました。

  • プロセス全体の登録をフィクスチャごとに変えないでください。 エクスプローラーの UI はフォルダーのすべてのフィクスチャを一度にマウントし、それらはコマンド、メニュー、その他のグローバルレジストリを共有します。共有アクションは一度だけ、すべてのフィクスチャで同じように登録し、フィクスチャごとの表示はフィクスチャ自身のサービス(たとえばそのメニューサービス)を通じて変えます。スクリーンショット実行はフィクスチャを一度に 1 つずつレンダリングするため、マウント順序の競合を捕捉しません。そのため、フォルダー全体(___explorer?fixture=<folder>)をプレビューして確認してください。

コメント