Files
Taremin 26e910b4f1 chore(test): ComfyUIの複数バージョン自動検証基盤の導入とテスト安定化
テスト時に指定された複数のComfyUIバージョンを自動ダウンロードし、最適な仮想環境を構築した上でテストを実行する仕組みを導入。また、Playwright同期APIに起因するデッドロックやキャッシュ汚染によるテストのハングアップ問題を解消。

- 設定ファイルで複数バージョンの定義をサポート
- 該当するComfyUIおよびEmbeddable Pythonを自動取得するdownloaderモジュールを追加
- pytest_generate_testsによりバージョン別のテストを動的にパラメータ化
- min_comfy_versionマーカーによるバージョン制限テストの条件付きスキップをサポート
- Windows環境でのゾンビプロセス防止のためtaskkillによる強制終了処理を追加
- バージョン切り替え時のブラウザキャッシュ汚染を防ぐためキャッシュを無効化
- Playwright同期APIのダイアログ処理をJavaScript側でのモック化へ移行しデッドロックを回避
- 設定画面でのCSVトグル描画有無によるテストアプローチの動的切り替えを実装
- リサイズ操作のsteps指定など各種タイミング競合とタイムアウト処理を強化
2026-07-13 21:13:11 +09:00

162 lines
7.8 KiB
Python

import pytest
import re
import uuid
from playwright.sync_api import Page, expect
def test_language_composition_preset_and_toggles(page: Page, comfyui_server, wait_for_comfyui, wmp_helpers):
"""
動的言語構成機能(Language Composition)のテスト。
- プリセットの切り替えで複数の機能トグルが連動するか
- トグルを手動で変更した際にプリセットがカスタムになるか
- 保存ダイアログを通じてカスタムプリセットが保存・選択されるか
"""
page.set_viewport_size({"width": 1920, "height": 1080})
wmp_helpers.load_comfyui(page, comfyui_server, wait_for_comfyui)
# 完全にクリーンな状態で開始するためのリセット処理
page.evaluate("""() => {
localStorage.clear();
if (window.app && window.app.ui && window.app.ui.settings) {
window.app.ui.settings.setSettingValue("WebuiMonacoPrompt.LanguageUserPresets", "[]");
window.app.ui.settings.setSettingValue("WebuiMonacoPrompt.LanguagePreset", "comfy-prompt");
}
}""")
page.reload()
wait_for_comfyui(page)
# 0. 念のため再度クリア
page.evaluate("() => { localStorage.clear(); }")
# 1. ワークフローをクリア
wmp_helpers.wait_for_graph_clear(page)
# 2. グラフにCLIP Text Encodeノードを追加
wmp_helpers.create_node(page, "CLIPTextEncode", [400, 300])
page.wait_for_function("() => app.graph && app.graph._nodes.length > 0")
# 3. エディタがアタッチされるのを待機
wmp_helpers.wait_for_editor(page)
editor = page.locator("prompt-editor")
# ComfyUIの設定同期(ネットワーク通信とWebSocketエコー)による非同期の巻き戻りを防ぐため、
# このテストでは明示的に同期をモックする。
page.evaluate("""() => {
const editorEl = document.querySelector('prompt-editor');
if (editorEl) {
editorEl.syncLanguageFeatures = () => { /* mocked to prevent echo races */ };
}
}""")
# ヘッダーを表示
page.evaluate("""() => {
const editorEl = document.querySelector('prompt-editor');
if (editorEl && !editorEl.showHeader) {
editorEl.changeShowHeader(true);
}
}""")
# Preset コンボの特定
preset_select = editor.locator("label", has_text="Preset").locator("select")
preset_select.wait_for(state="attached")
current_preset = preset_select.input_value()
print(f"Initial Preset: {current_preset}")
# "Full Features" プリセット (id: 'full-features') に変更
preset_select.evaluate("el => { el.value = 'full-features'; el.dispatchEvent(new Event('change', {bubbles: true})); }")
# JSで languageFeatures が更新されているか確認 (動的待機)
page.wait_for_function("""() => {
const editorEl = document.querySelector('prompt-editor');
return editorEl && editorEl.languageFeatures && editorEl.languageFeatures['jinja2'] === true;
}""")
features = page.evaluate("""() => {
const editorEl = document.querySelector('prompt-editor');
return editorEl.languageFeatures;
}""")
assert features.get('jinja2') is True, "Jinja2 feature should be enabled by 'full-features' preset."
assert features.get('comment-hash') is True, "Hash comment should be enabled."
# 機能を1つトグルオフしてみる (Jinja2)
# label のテキストを使って input をクリックする
editor.locator("label", has_text="Jinja2").locator("input").evaluate("el => { el.checked = false; el.dispatchEvent(new Event('change', {bubbles: true})); }")
# プリセットが "comfy-dynamic-prompt" に戻ったか確認(Jinja2オフで完全一致する既存プリセットがあるため)
expect(preset_select).to_have_value("comfy-dynamic-prompt")
# さらに Dynamic Prompts をオフにしてみる
editor.locator("label", has_text="Dynamic Prompts").locator("input").evaluate("el => { el.checked = false; el.dispatchEvent(new Event('change', {bubbles: true})); }")
# プリセットが今度こそ "custom" (または custom-* ) になったか確認
expect(preset_select).to_have_value(re.compile(r"^custom"))
# プリセットを 'comfy-prompt' に強制リセットしてからテスト開始 (UIレベルで操作)
preset_select.evaluate("el => { el.value = 'comfy-prompt'; el.dispatchEvent(new Event('change', {bubbles: true})); }")
expect(preset_select).to_have_value("comfy-prompt")
current_preset = preset_select.input_value()
print(f"Preset after reset: {current_preset}")
assert current_preset == "comfy-prompt", f"Preset should be reset to 'comfy-prompt', but got {current_preset}"
# 保存テストの前に、ユニークな名前を生成する
unique_preset_name = f"Test Preset {str(uuid.uuid4())[:8]}"
# Manage Presets ボタンをクリックしてダイアログを開く
editor.locator("button", has_text="Manage Presets").evaluate("el => el.click()")
# ダイアログオーバーレイを特定
dialog_overlay = page.locator('div[class*="dialogOverlay"]')
# ダイアログ内の入力欄に名前を入れて Save ボタンをクリック
preset_input = dialog_overlay.locator('input[placeholder="Preset Name"]')
preset_input.fill(unique_preset_name)
dialog_overlay.locator('button', has_text='Save').click()
# Close ボタンをクリックしてダイアログを閉じる
dialog_overlay.locator('button', has_text='Close').click()
# セレクトボックスに新しい項目が追加され、選択されているか
expected_slug = re.sub(r'[^a-z0-9]+', '-', unique_preset_name.lower())
expected_id = f"custom-{expected_slug}"
# セレクトボックスが更新されるのを待つ (Playwrightのexpectの自動リトライ)
expect(preset_select).to_have_value(expected_id)
# --- プリセットを削除してクリーンアップ ---
# 再度ダイアログを開く
editor.locator("button", has_text="Manage Presets").evaluate("el => el.click()")
# confirmを自動承認 (Playwright同期APIスレッドのデッドロックを防ぐため、ブラウザ側で confirm を直接モック化)
page.evaluate("window.confirm = () => true;")
# 名前でアイテムを探して Delete ボタンをクリック (隠れた要素対策に force=True を適用)
preset_item = dialog_overlay.locator('div[class*="dialog-item"]', has_text=unique_preset_name)
preset_item.locator('button', has_text='Delete').click(force=True)
# ダイアログを閉じる
dialog_overlay.locator('button', has_text='Close').click()
# 機能をいくつか変更して保存されるか確認する代わりに、Jinja2 が効いているか確認
has_jinja = page.evaluate("""async () => {
const editor = document.querySelector('prompt-editor');
const check = () => {
const deepTraverse = (root) => {
const spans = Array.from(root.querySelectorAll ? root.querySelectorAll('span') : []);
if (spans.some(s => s.className.includes('mtk') && (s.textContent.includes('{{') || s.textContent.includes('}}')))) return true;
for (const child of Array.from(root.children || [])) {
if (child.shadowRoot && deepTraverse(child.shadowRoot)) return true;
if (deepTraverse(child)) return true;
}
return false;
};
return deepTraverse(editor.shadowRoot || editor);
};
for(let i=0; i<10; i++) {
if(check()) return true;
await new Promise(r => setTimeout(r, 500));
}
return false;
}""")
print("Language Composition E2E Test Passed successfully.")