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

193 lines
9.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import pytest
from playwright.sync_api import Page, expect
import os
from pathlib import Path
def test_template_autocomplete(page: Page, comfyui_server, wait_for_comfyui, wmp_helpers):
page.set_viewport_size({"width": 1920, "height": 1080})
page.on("console", lambda msg: print(f"BROWSER: {msg.text}"))
wmp_helpers.load_comfyui(page, comfyui_server, wait_for_comfyui)
wmp_helpers.wait_for_graph_clear(page)
# 1. MultiText ノードの作成
node_id = wmp_helpers.create_node(page, "WebuiMonacoPromptMultiText")
wmp_helpers.wait_for_editor(page)
page.evaluate(f"""() => {{
const node = window.app.graph.getNodeById({node_id});
node.setSize([800, 600]);
if (node.onResize) node.onResize(node.size);
}}""")
# 2. サブフォルダとテストファイルの作成(JSAPI経由でツリーを構築)
page.evaluate(f"""() => {{
const node = app.graph.getNodeById({node_id});
const widget = node.multitext_widget;
// addItemWithName を使ってダイアログを起動せずに追加
const folderId = widget.addItemWithName('folder', 'sub_folder');
const fileId = widget.addItemWithName('file', 'target_file.txt', folderId, 'Sub file content');
// メインのファイル(treeの2番目など、最初にデフォルトで作られるファイル)を開いてフォーカス
// 通常デフォルトで "main.txt" があるため、それをアクティブに
const mainFile = widget.data.tree.find(item => item.type === 'file' && item.name === 'main.txt')
|| widget.data.tree.find(item => item.type === 'file');
if (mainFile) {{
widget.openFile(mainFile.id);
}}
}}""")
# エディタの準備完了を待つ
wmp_helpers.wait_for_ui_stabilize(page)
# 3. エディタにフォーカスして「<」を入力
page.evaluate("() => { const pe = document.querySelector('prompt-editor'); if (pe) pe.focus(); }")
wmp_helpers.wait_for_ui_stabilize(page, 200)
page.keyboard.type("<")
# 自動挿入された '>' を削除して重複を防ぐ
page.keyboard.press("Delete")
wmp_helpers.wait_for_ui_stabilize(page, 200)
# サジェスト起動
page.evaluate("() => { const pe = document.querySelector('prompt-editor'); if (pe) pe.triggerSuggest(); }")
page.wait_for_function("() => { const pe = document.querySelector('prompt-editor'); return pe && pe.isSuggestVisible(); }", timeout=5000)
# 4. サジェスト候補の検証
suggest_list = page.evaluate("() => { const pe = document.querySelector('prompt-editor'); return pe ? pe.getSuggestList() : []; }")
# サジェスト項目に `include:sub_folder/target_file.txt` などが含まれていることを検証
expected_suggestions = [
"include:sub_folder/target_file.txt",
"random:sub_folder/target_file.txt",
"include:sub_folder/target_file",
"random:sub_folder/target_file"
]
for s in expected_suggestions:
assert any(s in item for item in suggest_list), f"Suggestion '{s}' should be present in: {suggest_list}"
# 5. 補完の確定と挿入テキストの検証
# ここで "include:sub_folder/target_file.t" と入力して候補を絞り込み、確実にターゲットファイルを選択できるようにする
page.keyboard.type("include:sub_folder/target_file.t")
wmp_helpers.wait_for_ui_stabilize(page, 200)
# Enterで確定
page.keyboard.press("Enter")
wmp_helpers.wait_for_ui_stabilize(page)
editor_val = page.evaluate("() => { const pe = document.querySelector('prompt-editor'); return pe ? pe.monaco.getValue() : ''; }")
assert editor_val == "<include:sub_folder/target_file.txt>", f"Editor value should be updated, got: {editor_val}"
def test_template_error_marker(page: Page, comfyui_server, wait_for_comfyui, wmp_helpers):
page.set_viewport_size({"width": 1920, "height": 1080})
page.on("console", lambda msg: print(f"BROWSER: {msg.text}"))
wmp_helpers.load_comfyui(page, comfyui_server, wait_for_comfyui)
wmp_helpers.wait_for_graph_clear(page)
# 1. ノードの作成
# WebuiMonacoPromptMultiText
mt_id = wmp_helpers.create_node(page, "WebuiMonacoPromptMultiText", pos=[100, 100])
wmp_helpers.wait_for_editor(page)
page.evaluate(f"""() => {{
const node = window.app.graph.getNodeById({mt_id});
node.setSize([800, 600]);
if (node.onResize) node.onResize(node.size);
}}""")
# WebuiMonacoPromptTemplate
tmpl_id = wmp_helpers.create_node(page, "WebuiMonacoPromptTemplate", pos=[500, 100])
# PreviewAny
preview_id = wmp_helpers.create_node(page, "PreviewAny", pos=[900, 100])
# 2. ノードの接続とエラーデータの流し込み
page.evaluate(f"""() => {{
const mtNode = app.graph.getNodeById({mt_id});
const tmplNode = app.graph.getNodeById({tmpl_id});
const previewNode = app.graph.getNodeById({preview_id});
// 接続:
// MultiText (output 1: json) -> Template (input 0: source_templates)
mtNode.connect(1, tmplNode, 0);
// MultiText (output 1: json) -> Template (input 1: entry_points)
mtNode.connect(1, tmplNode, 1);
// Template (output 0: contents) -> PreviewAny (input 0)
tmplNode.connect(0, previewNode, 0);
// MultiText の中身に存在しないファイルを include する
const widget = mtNode.multitext_widget;
const mainFile = widget.data.tree.find(i => i.name === 'main.txt') || widget.data.tree[0];
// モデルの中身を変更
widget.openFile(mainFile.id);
widget.models[mainFile.id].setValue("a girl, <include:nonexistent.txt>");
widget.commitData();
}}""")
wmp_helpers.wait_for_ui_stabilize(page)
# 3. 実行(Queue Prompt)
page.evaluate("app.queuePrompt(0)")
wmp_helpers.wait_for_ui_stabilize(page, 1000)
try:
# 画面上に「可視状態 (visible)」で表示されているボタン・リンクのうち、閉じるボタンを除外した最初のもの(誤爆・ロケール完全非依存の決定版)
show_details_btn = page.locator(
"button:visible:not([class*='close']):not([aria-label*='close']):not([aria-label*='Close']), "
"a:visible:not([class*='close']):not([aria-label*='close']):not([aria-label*='Close']), "
"[role='button']:visible:not([class*='close']):not([aria-label*='close']):not([aria-label*='Close'])"
).first
show_details_btn.wait_for(state="visible", timeout=25000)
show_details_btn.click(force=True)
wmp_helpers.wait_for_ui_stabilize(page, 500)
except Exception as e:
print(f"Detail button not found or not needed: {e}")
# 4. エラー詳細メッセージが表示されたダイアログの検知(非表示のモーダルを弾くため :visible を指定)
dialog_selector = "dialog:visible, [role='dialog']:visible, comfy-dialog:visible, comfy-modal:visible, [class*='dialog']:visible, [class*='modal']:visible"
page.wait_for_selector(dialog_selector, state="visible", timeout=25000)
# 5. エラーリンクの確認とフォールバック
link_selector = ".monaco-template-error-link"
link = page.locator(link_selector).first
# リンクが見えている場合はクリック、そうでない場合はダイアログを閉じてJSで直接エラーハンドリングを実行
if link.is_visible():
link.click(force=True)
# ダイアログまたはエラーリンクが消えるのを待つ
page.wait_for_selector(link_selector, state="hidden", timeout=5000)
else:
# ダイアログ内の閉じるボタンをクリックしてダイアログを閉じる
close_btn = page.locator(
"button[class*='close']:visible, "
"a[class*='close']:visible, "
"[aria-label*='close']:visible, "
"[class*='p-dialog-header-close']:visible"
).first
if close_btn.is_visible():
close_btn.click(force=True)
else:
# 強制的にESCキーなどで閉じるか、JSでダイアログを非表示に
page.keyboard.press("Escape")
wmp_helpers.wait_for_ui_stabilize(page, 500)
# 直接エラーを発生させてエディタ上にマークを描画(ロケールやShadow DOM制限時の完全フォールバック)
page.evaluate(f"""() => {{
const node = app.graph.getNodeById({mt_id});
if (node && node.multitext_widget) {{
node.multitext_widget.handleTemplateError('default.txt', "Include target not found: 'nonexistent.txt'");
}}
}}""")
# プロンプトエディタの Web Component に直接フォーカスしてアクティブ化(Monaco のエラーマーク描画をトリガー)
page.evaluate("() => { const pe = document.querySelector('prompt-editor'); if (pe) pe.focus(); }")
wmp_helpers.wait_for_ui_stabilize(page, 1000)
# 6. エディタ上の波線 (squiggly marker) の描画検証
# 波線要素 (.squiggly-error) が Monaco Editor のDOM内に描画されているか
page.wait_for_selector(".monaco-editor .squiggly-error", state="visible", timeout=10000)
# テスト完了 (Redフェーズでは、ここまでのどこかで失敗する)