Files
Taremin-webui-monaco-prompt/tests/e2e/multitext/test_multitext_extended_features.py
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

239 lines
10 KiB
Python

import pytest
import time
from playwright.sync_api import Page, expect
# ブラウザのコンソールログを収集するヘルパー
def setup_console_log(page: Page):
page.on("console", lambda msg: print(f"BROWSER CONSOLE: {msg.type}: {msg.text}"))
page.on("pageerror", lambda err: print(f"BROWSER ERROR: {err}"))
def add_node_api_force(page: Page):
"""LiteGraphにノードが登録されるのを待ってから、APIを使用してMultiTextノードを直接追加する"""
for _ in range(10):
found = page.evaluate("""
() => {
const lg = window.LiteGraph;
if (!lg || !lg.registered_node_types) return null;
const type = Object.keys(lg.registered_node_types).find(k => k.includes("MultiText"));
if (type) {
const getApp = () => window.app || (window.comfyAPI && window.comfyAPI.app) || window.ComfyApp || (window.parent && window.parent.app);
const app = getApp();
app.graph.clear();
const node = lg.createNode(type);
node.pos = [300, 300];
node.size = [400, 400];
app.graph.add(node);
app.canvas.draw(true, true);
return { type, id: node.id };
}
return null;
}
""")
if found:
print(f"DEBUG: Added node {found}")
return found
time.sleep(2.0)
raise Exception("MultiText node type not found")
def create_items_api(page: Page, items):
"""APIを使用して一括でファイル/フォルダを作成する"""
print(f"DEBUG: Creating {len(items)} items via API...")
success = page.evaluate("""
(items) => {
const getApp = () => window.app || (window.comfyAPI && window.comfyAPI.app) || window.ComfyApp;
const app = getApp();
const node = app.graph.nodes.find(n => n.type && n.type.includes("MultiText"));
if (!node) {
console.error("MultiText node not found in graph");
return false;
}
// ウィジェットを見つける(multitext_widgetプロパティまたはwidgets内から)
let widget = node.multitext_widget;
if (!widget && node.widgets) {
widget = node.widgets.find(w => w.addItemWithName);
}
if (!widget) {
console.error("MultiText widget instance not found on node", node);
return false;
}
for (const item of items) {
widget.addItemWithName(item[0], item[1]);
}
return true;
}
""", items)
if not success:
print("ERROR: create_items_api failed in browser")
time.sleep(1.0)
def test_multitext_tab_auto_scroll(page: Page, comfyui_server, wait_for_comfyui, wmp_helpers):
"""タブの自動スクロールを確認する"""
setup_console_log(page)
page.goto(comfyui_server)
wait_for_comfyui(page)
add_node_api_force(page)
wmp_helpers.wait_for_editor(page)
# ファイルを25個作成
items = [("file", f"file_{i:02d}.txt") for i in range(1, 26)]
create_items_api(page, items)
tabs_container = page.locator("[class*='multitext-tabs-container']").first
# default.txtが画面に出るまで少し待機
page.wait_for_selector("[class*='multitext-tree-name']")
# 最初のファイルを選択
page.locator("[class*='multitext-tree-name']", has_text="default.txt").first.click(force=True)
wmp_helpers.wait_for_ui_stabilize(page, 1000)
scroll_left_start = tabs_container.evaluate("el => el.scrollLeft")
# 最後のファイルを選択
page.locator("[class*='multitext-tree-name']", has_text="file_25.txt").first.click(force=True)
wmp_helpers.wait_for_ui_stabilize(page, 3000)
scroll_left_end = tabs_container.evaluate("el => el.scrollLeft")
print(f"DEBUG: scrollLeft start={scroll_left_start}, end={scroll_left_end}")
assert scroll_left_end > scroll_left_start
def test_multitext_multiple_selection_dnd(page: Page, comfyui_server, wait_for_comfyui, wmp_helpers):
"""複数選択D&Dを確認する"""
setup_console_log(page)
page.goto(comfyui_server)
wait_for_comfyui(page)
add_node_api_force(page)
wmp_helpers.wait_for_editor(page)
# フォルダとファイル作成
items = [("folder", "target_folder"), ("file", "move_me_1.txt"), ("file", "move_me_2.txt"), ("file", "move_me_3.txt")]
create_items_api(page, items)
# 確実にツリーが描画されるのを待つ
page.wait_for_selector("[class*='multitext-tree-name']")
# Ctrl選択
page.locator("[class*='multitext-tree-name']", has_text="move_me_1.txt").first.click(force=True)
page.locator("[class*='multitext-tree-name']", has_text="move_me_2.txt").first.click(modifiers=["Control"], force=True)
page.locator("[class*='multitext-tree-name']", has_text="move_me_3.txt").first.click(modifiers=["Control"], force=True)
wmp_helpers.wait_for_ui_stabilize(page, 1000)
# 選択されたツリーアイテムの数を検証 (属性セレクタを使用)
selected_count = page.evaluate("""() => {
const items = Array.from(document.querySelectorAll("[class*='multitext-tree-item']"));
return items.filter(el => el.className.includes('selected')).length;
}""")
print(f"DEBUG: selected count via JS is {selected_count}")
assert selected_count == 3
# ドラッグ&ドロップ
source = page.locator("[class*='multitext-tree-name']", has_text="move_me_1.txt").first
target = page.locator("[class*='multitext-tree-name']", has_text="target_folder").first
source.drag_to(target, force=True)
wmp_helpers.wait_for_ui_stabilize(page, 1000)
# 選択されたツリーアイテムの数を検証 (属性セレクタを使用)
selected_count = page.evaluate("""() => {
const items = Array.from(document.querySelectorAll("[class*='multitext-tree-item']"));
return items.filter(el => el.className.includes('selected')).length;
}""")
print(f"DEBUG: selected count via JS is {selected_count}")
assert selected_count == 3
# 移動確認
folder_wrapper = page.locator("[class*='multitext-tree-item-wrapper']", has_text="target_folder").first
children = folder_wrapper.locator("[class*='multitext-tree-children']")
expect(children.get_by_text("move_me_1.txt")).to_be_visible()
expect(children.get_by_text("move_me_2.txt")).to_be_visible()
expect(children.get_by_text("move_me_3.txt")).to_be_visible()
def test_multitext_shift_selection(page: Page, comfyui_server, wait_for_comfyui, wmp_helpers):
"""Shift選択(範囲選択)を確認する"""
setup_console_log(page)
page.goto(comfyui_server)
wait_for_comfyui(page)
add_node_api_force(page)
wmp_helpers.wait_for_editor(page)
items = [("file", f"range_{i}.txt") for i in range(1, 6)]
create_items_api(page, items)
page.wait_for_selector("[class*='multitext-tree-name']")
# 最初と最後をShiftクリック
page.locator("[class*='multitext-tree-name']", has_text="range_1.txt").first.click(force=True)
page.locator("[class*='multitext-tree-name']", has_text="range_5.txt").first.click(modifiers=["Shift"], force=True)
wmp_helpers.wait_for_ui_stabilize(page, 1000)
# 選択されたツリーアイテムの数を検証
selected_count = page.evaluate("""() => {
const items = Array.from(document.querySelectorAll("[class*='multitext-tree-item']"));
return items.filter(el => el.className.includes('selected')).length;
}""")
print(f"DEBUG: Shift selection count via JS is {selected_count}")
assert selected_count == 5
def test_multitext_search_toggle_and_clear(page: Page, comfyui_server, wait_for_comfyui, wmp_helpers):
"""検索バーのトグル表示とクリアボタンの動作を確認する"""
setup_console_log(page)
page.goto(comfyui_server)
wait_for_comfyui(page)
add_node_api_force(page)
wmp_helpers.wait_for_editor(page)
# テスト用ファイル作成
items = [("file", "search_test.txt")]
create_items_api(page, items)
# 検索コンテナのロケーター(CSS Modulesを考慮し、部分一致を利用)
search_container = page.locator("div[class*='sidebar-search']").first
search_results = page.locator("div[class*='search-results']").first
# 初期状態は非表示であることを確認
expect(search_container).to_be_hidden()
expect(search_results).to_be_hidden()
# 検索ボタンをクリックして表示させる
search_btn_js = """() => {
const btns = Array.from(document.querySelectorAll("button"));
const searchBtn = btns.find(b => b.title === 'Search' || b.innerHTML.includes('M11.742'));
if (searchBtn) searchBtn.click();
return !!searchBtn;
}"""
assert page.evaluate(search_btn_js) is True
# 表示されるのを待つ
expect(search_container).to_be_visible()
expect(search_results).to_be_visible()
# 検索入力欄に文字を入力
search_input = page.locator("input[class*='search-input']").first
search_input.fill("test")
# 入力されたことを確認
expect(search_input).to_have_value("test")
# クリアボタンをクリック
clear_btn_js = """() => {
const btn = document.querySelector("button[class*='search-clear-btn']");
if (btn) btn.click();
return !!btn;
}"""
assert page.evaluate(clear_btn_js) is True
# 入力が空になったことを確認
expect(search_input).to_have_value("")
# 再度検索ボタンをクリックして非表示にする
assert page.evaluate(search_btn_js) is True
# 非表示になることを確認
expect(search_container).to_be_hidden()
expect(search_results).to_be_hidden()