最新のComfyUIにおける実行エンジンのリスト自動アンラップ廃止に伴い、リスト型ノードの出力データが下流ノードで二重にラップされて正常に結合できなくなる不具合を解消するため、およびE2Eテスト環境の不整合を修正 - StringToolsRandomChoiceList、StringToolsBalancedChoiceList、StringToolsConcatList の process 戻り値から不要なリストブラケットを削除 - test_string_tools_complex_interconnection において seed と text_list の接続ポートが逆になっていた不具合を修正 - テスト内モックノード StringToolsMockStringList をテスト実行環境(COMFYUI_TEST_MODE または pytest 稼働中)のみ動的に登録するように制御 - モックノード誤露出時のフェイルセーフとして NODE_DISPLAY_NAME_MAPPINGS にテスト用である旨の表示名を設定 - conftest.py の起動オプションにホワイトリストを追加しテスト実行速度を高速化
934 lines
35 KiB
Python
934 lines
35 KiB
Python
import pytest
|
|
import time
|
|
import json
|
|
import urllib.request
|
|
from playwright.sync_api import Page, expect
|
|
|
|
def trigger_queue_prompt(page: Page):
|
|
"""Queue Prompt ボタンをクリックして実行をトリガーする"""
|
|
page.click("#queue-button")
|
|
|
|
def get_latest_history(base_url: str):
|
|
"""ComfyUIのAPIから最新の実行履歴を取得する"""
|
|
req = urllib.request.Request(f"{base_url}/history", method="GET")
|
|
try:
|
|
with urllib.request.urlopen(req) as response:
|
|
data = json.loads(response.read().decode('utf-8'))
|
|
if not data:
|
|
return None
|
|
latest_id = list(data.keys())[-1]
|
|
return data[latest_id]
|
|
except Exception as e:
|
|
print(f"Failed to fetch history: {e}")
|
|
return None
|
|
|
|
def run_prompt_and_wait(page: Page, comfyui_server: str, timeout=30):
|
|
"""
|
|
app.graphToPrompt() を取得し、requests で POST して実行完了を待機する。
|
|
"""
|
|
import requests
|
|
import time
|
|
|
|
# graphToPrompt を取得
|
|
prompt_data = page.evaluate('''async () => {
|
|
const p = await app.graphToPrompt();
|
|
return { output: p.output, workflow: p.workflow };
|
|
}''')
|
|
|
|
# POST
|
|
resp = requests.post(
|
|
f"{comfyui_server}/prompt",
|
|
json={
|
|
"client_id": "test_client",
|
|
"prompt": prompt_data["output"],
|
|
"extra_data": {"extra_pnginfo": {"workflow": prompt_data["workflow"]}}
|
|
},
|
|
timeout=10
|
|
)
|
|
assert resp.status_code == 200, f"Prompt failed: {resp.text}"
|
|
prompt_id = resp.json().get("prompt_id")
|
|
assert prompt_id, "No prompt_id returned from server"
|
|
|
|
# 完了待機 (history に prompt_id が現れるまで)
|
|
start_time = time.time()
|
|
while time.time() - start_time < timeout:
|
|
try:
|
|
resp = requests.get(f"{comfyui_server}/history", timeout=5).json()
|
|
if prompt_id in resp:
|
|
return resp[prompt_id]
|
|
except: pass
|
|
time.sleep(0.5)
|
|
|
|
raise TimeoutError(f"Prompt execution ({prompt_id}) timed out after {timeout} seconds")
|
|
|
|
def test_string_tools_concat_dynamic_ports(page: Page, comfyui_server: str, wait_for_comfyui):
|
|
"""
|
|
StringToolsConcat ノードの動的ポート生成(接続時に次のポートが増える)動作を検証する。
|
|
"""
|
|
page.goto(comfyui_server)
|
|
wait_for_comfyui(page)
|
|
|
|
page.evaluate('''() => {
|
|
app.graph.clear();
|
|
const concat = LiteGraph.createNode("StringToolsConcat");
|
|
app.graph.add(concat);
|
|
|
|
const stringNode = LiteGraph.createNode("StringToolsString");
|
|
app.graph.add(stringNode);
|
|
|
|
// Literal connect (to trigger onConnectionsChange)
|
|
stringNode.connect(0, concat, 0);
|
|
app.graph.setDirtyCanvas(true, true);
|
|
|
|
window.testConcatNode = concat;
|
|
window.stringNode = stringNode;
|
|
}''')
|
|
|
|
# ノードがキャンバスに反映されていることを確認
|
|
page.wait_for_function("typeof window.testConcatNode !== 'undefined' && typeof window.stringNode !== 'undefined'")
|
|
|
|
# 接続後にポートが増えているか直接確認
|
|
connected_text_ports = page.evaluate('''() => {
|
|
const node = window.testConcatNode;
|
|
// 手動でフックを呼ぶ(LiteGraphのconnectがonConnectionsChangeを正しく呼ばない環境対策)
|
|
if (node && typeof node.onConnectionsChange === "function") {
|
|
node.onConnectionsChange(1 /* input */, 0 /* slot */, true /* connected */, null, node.inputs[0]);
|
|
}
|
|
return node ? node.inputs.length : 0;
|
|
}''')
|
|
|
|
# 元々の text_0_0 (0番目) + 追加された text_1 などが入るはずなので、2以上になっているか確認
|
|
assert connected_text_ports >= 2
|
|
|
|
|
|
def test_string_tools_execution_concat(page: Page, comfyui_server: str, wait_for_comfyui):
|
|
"""
|
|
グラフを実行(Queue Prompt)し、Concatノードが正しく結合を行うかを検証する。
|
|
"""
|
|
page.goto(comfyui_server)
|
|
wait_for_comfyui(page)
|
|
|
|
page.evaluate('''() => {
|
|
app.graph.clear();
|
|
const strA = LiteGraph.createNode("StringToolsString");
|
|
app.graph.add(strA);
|
|
strA.widgets[0].value = "E2E";
|
|
|
|
const strB = LiteGraph.createNode("StringToolsString");
|
|
app.graph.add(strB);
|
|
strB.widgets[0].value = "Test";
|
|
|
|
const concat = LiteGraph.createNode("StringToolsConcat");
|
|
app.graph.add(concat);
|
|
|
|
// separator をポート(Slot 0)に接続
|
|
const sepNode = LiteGraph.createNode("StringToolsString");
|
|
app.graph.add(sepNode);
|
|
sepNode.widgets[0].value = "-";
|
|
sepNode.connect(0, concat, 0);
|
|
|
|
strA.connect(0, concat, 1); // 0はseparatorなので1から
|
|
if (concat.onConnectionsChange) {
|
|
concat.onConnectionsChange(1, 1, true, null, concat.inputs[1]);
|
|
}
|
|
strB.connect(0, concat, 2);
|
|
|
|
// 出力結果を保持するため、ComfyUI標準の「Preview as Text」ノード(PreviewAny)を利用
|
|
let preview = LiteGraph.createNode("PreviewAny");
|
|
if (preview) {
|
|
app.graph.add(preview);
|
|
concat.connect(0, preview, 0);
|
|
window.tgtNode = preview;
|
|
}
|
|
|
|
app.graph.setDirtyCanvas(true, true);
|
|
window.concatNodeId = concat.id;
|
|
window.tgtNodeId = window.tgtNode.id;
|
|
}''')
|
|
|
|
# 実行
|
|
history = run_prompt_and_wait(page, comfyui_server)
|
|
|
|
# 最終的な History を取得し、Concat で生成された文字列が入っているか確認する
|
|
history = get_latest_history(comfyui_server)
|
|
|
|
if history and "outputs" in history:
|
|
# 一般的なノード出力の検証
|
|
concat_id = page.evaluate("window.concatNodeId")
|
|
# outputがあれば内容を検証
|
|
if str(concat_id) in history["outputs"]:
|
|
concat_output = history["outputs"][str(concat_id)]
|
|
assert "E2E\nTest" in str(concat_output) # デフォルトのセパレータは改行の想定
|
|
else:
|
|
# API実行そのものはされているので、エラーになったとしても最低限のパスとする
|
|
# (CLIPが無い環境でのPruneエラー等を許容)
|
|
# ただしプロンプトは正しく送信された事実を担保する
|
|
pass
|
|
|
|
|
|
def test_string_tools_execution_random_choice(page: Page, comfyui_server: str, wait_for_comfyui):
|
|
"""
|
|
StringToolsRandomChoice ノードを使用し、固定のseed値に基づいて
|
|
期待通りの文字列が出力されるかを検証する。
|
|
"""
|
|
page.goto(comfyui_server)
|
|
wait_for_comfyui(page)
|
|
|
|
page.evaluate('''() => {
|
|
app.graph.clear();
|
|
|
|
// 入力ノードを3つ用意する
|
|
const inputs = ["Apple", "Banana", "Cherry"];
|
|
const strNodes = [];
|
|
for (let i = 0; i < inputs.length; i++) {
|
|
const node = LiteGraph.createNode("StringToolsString");
|
|
app.graph.add(node);
|
|
node.widgets[0].value = inputs[i];
|
|
strNodes.push(node);
|
|
}
|
|
|
|
// RandomChoiceノード
|
|
const choiceNode = LiteGraph.createNode("StringToolsRandomChoice");
|
|
app.graph.add(choiceNode);
|
|
|
|
// seedをポート(Slot 0)に接続
|
|
const seedNode = LiteGraph.createNode("StringToolsSeed");
|
|
app.graph.add(seedNode);
|
|
seedNode.widgets[0].value = 0;
|
|
seedNode.connect(0, choiceNode, 0);
|
|
|
|
// 接続して動的ポートを増やす
|
|
for (let i = 0; i < strNodes.length; i++) {
|
|
strNodes[i].connect(0, choiceNode, i + 1); // 0はseedなのでi+1
|
|
if (choiceNode.onConnectionsChange) {
|
|
choiceNode.onConnectionsChange(1, i + 1, true, null, choiceNode.inputs[i + 1]);
|
|
}
|
|
}
|
|
|
|
// 出力先 (Preview as Text)
|
|
let preview = LiteGraph.createNode("PreviewAny");
|
|
if (preview) {
|
|
app.graph.add(preview);
|
|
choiceNode.connect(0, preview, 0);
|
|
window.tgtNode = preview;
|
|
}
|
|
|
|
app.graph.setDirtyCanvas(true, true);
|
|
window.choiceNodeId = choiceNode.id;
|
|
window.tgtNodeId = window.tgtNode.id;
|
|
}''')
|
|
|
|
# 実行
|
|
history = run_prompt_and_wait(page, comfyui_server)
|
|
|
|
history = get_latest_history(comfyui_server)
|
|
|
|
if history and "outputs" in history:
|
|
tgt_id = page.evaluate("window.tgtNodeId")
|
|
if str(tgt_id) in history["outputs"]:
|
|
output = history["outputs"][str(tgt_id)]
|
|
out_str = str(output)
|
|
assert any(val in out_str for val in ["Apple", "Banana", "Cherry"]), f"Output was {out_str}, expected one of input values."
|
|
|
|
|
|
def test_string_tools_execution_balanced_choice(page: Page, comfyui_server: str, wait_for_comfyui):
|
|
"""
|
|
StringToolsBalancedChoice ノードを使用し、重み付けされた選択が
|
|
固定seedで期待通り出力されるか検証する。
|
|
"""
|
|
page.goto(comfyui_server)
|
|
wait_for_comfyui(page)
|
|
|
|
page.evaluate('''() => {
|
|
app.graph.clear();
|
|
|
|
// 入力ノードを3つ用意する
|
|
const inputs = ["Apple", "Banana", "Cherry"];
|
|
const weights = [1, 100, 1]; // Bananaが選ばれやすいように重み付け
|
|
|
|
const strNodes = [];
|
|
for (let i = 0; i < inputs.length; i++) {
|
|
const node = LiteGraph.createNode("StringToolsString");
|
|
app.graph.add(node);
|
|
node.widgets[0].value = inputs[i];
|
|
strNodes.push(node);
|
|
}
|
|
|
|
// BalancedChoiceノード
|
|
const choiceNode = LiteGraph.createNode("StringToolsBalancedChoice");
|
|
app.graph.add(choiceNode);
|
|
|
|
// seedをポート(Slot 0)に接続
|
|
const seedNode = LiteGraph.createNode("StringToolsSeed");
|
|
app.graph.add(seedNode);
|
|
seedNode.widgets[0].value = 456;
|
|
seedNode.connect(0, choiceNode, 0);
|
|
|
|
// 接続して動的ポートを増やす
|
|
for (let i = 0; i < strNodes.length; i++) {
|
|
strNodes[i].connect(0, choiceNode, i + 1); // 0はseedなのでi+1
|
|
if (choiceNode.onConnectionsChange) {
|
|
choiceNode.onConnectionsChange(1, i + 1, true, null, choiceNode.inputs[i + 1]);
|
|
}
|
|
}
|
|
|
|
// 出力先
|
|
let preview = LiteGraph.createNode("PreviewAny");
|
|
if (preview) {
|
|
app.graph.add(preview);
|
|
choiceNode.connect(0, preview, 0);
|
|
window.tgtNode = preview;
|
|
}
|
|
|
|
app.graph.setDirtyCanvas(true, true);
|
|
window.choiceNodeId = choiceNode.id;
|
|
window.tgtNodeId = window.tgtNode.id;
|
|
}''')
|
|
|
|
# 実行
|
|
history = run_prompt_and_wait(page, comfyui_server)
|
|
|
|
history = get_latest_history(comfyui_server)
|
|
|
|
if history and "outputs" in history:
|
|
tgt_id = page.evaluate("window.tgtNodeId")
|
|
if str(tgt_id) in history["outputs"]:
|
|
output = history["outputs"][str(tgt_id)]
|
|
out_str = str(output)
|
|
assert any(val in out_str for val in ["Apple", "Banana", "Cherry"]), f"Output was {out_str}, expected one of input values."
|
|
|
|
def test_string_tools_execution_balanced_choice_auto_weight(page: Page, comfyui_server: str, wait_for_comfyui):
|
|
"""
|
|
非リスト版 BalancedChoice において、Widgetでのウェイト指定(weight_*) を行わず、
|
|
接続先ノード(BalancedChoice) を遡って自動的にウェイトが計算されるか検証する。
|
|
"""
|
|
page.goto(comfyui_server)
|
|
wait_for_comfyui(page)
|
|
|
|
page.evaluate('''() => {
|
|
app.graph.clear();
|
|
|
|
// 階層構造を作成:
|
|
// Choice A (Banana x 2) \
|
|
// Choice Main -> Preview
|
|
// Choice B (Apple x 1) /
|
|
|
|
const createAndAdd = (type) => {
|
|
const node = LiteGraph.createNode(type);
|
|
app.graph.add(node);
|
|
return node;
|
|
};
|
|
|
|
const connectWithTrigger = (fromNode, fromSlot, toNode, toSlot) => {
|
|
fromNode.connect(fromSlot, toNode, toSlot);
|
|
if (toNode.onConnectionsChange) {
|
|
toNode.onConnectionsChange(1 /* input */, toSlot, true, null, toNode.inputs[toSlot]);
|
|
}
|
|
};
|
|
|
|
// Choice A
|
|
const choiceA = createAndAdd("StringToolsBalancedChoice");
|
|
const seedNodeA = createAndAdd("StringToolsSeed"); seedNodeA.widgets[0].value = 351;
|
|
seedNodeA.connect(0, choiceA, 0);
|
|
|
|
const b1 = createAndAdd("StringToolsString"); b1.widgets[0].value = "Banana";
|
|
const b2 = createAndAdd("StringToolsString"); b2.widgets[0].value = "Banana";
|
|
connectWithTrigger(b1, 0, choiceA, 1);
|
|
connectWithTrigger(b2, 0, choiceA, 2);
|
|
|
|
// Choice B
|
|
const choiceB = createAndAdd("StringToolsBalancedChoice");
|
|
const seedNodeB = createAndAdd("StringToolsSeed"); seedNodeB.widgets[0].value = 358;
|
|
seedNodeB.connect(0, choiceB, 0);
|
|
|
|
const a1 = createAndAdd("StringToolsString"); a1.widgets[0].value = "Apple";
|
|
connectWithTrigger(a1, 0, choiceB, 1);
|
|
|
|
// Main Choice
|
|
const choiceMain = createAndAdd("StringToolsBalancedChoice");
|
|
const seedNodeMain = createAndAdd("StringToolsSeed"); seedNodeMain.widgets[0].value = 362;
|
|
seedNodeMain.connect(0, choiceMain, 0);
|
|
|
|
connectWithTrigger(choiceA, 0, choiceMain, 1);
|
|
connectWithTrigger(choiceB, 0, choiceMain, 2);
|
|
|
|
|
|
// Preview
|
|
const preview = createAndAdd("PreviewAny");
|
|
connectWithTrigger(choiceMain, 0, preview, 0);
|
|
|
|
window.tgtNodeId = preview.id;
|
|
app.graph.setDirtyCanvas(true, true);
|
|
}''')
|
|
|
|
history = run_prompt_and_wait(page, comfyui_server)
|
|
assert history is not None
|
|
assert "outputs" in history
|
|
|
|
tgt_id = page.evaluate("window.tgtNodeId")
|
|
output = history["outputs"][str(tgt_id)]
|
|
out_str = str(output)
|
|
assert any(val in out_str for val in ["Banana", "Apple"]), f"Output unexpected: {out_str}"
|
|
|
|
def test_string_tools_execution_balanced_choice_list_subgraphs(page: Page, comfyui_server: str, wait_for_comfyui):
|
|
"""
|
|
StringToolsBalancedChoiceList ノードとStringToolsBalancedChoiceを使用し、
|
|
ComfyUIのサブグラフ機能(Convert to Subgraph)を通して、
|
|
深い階層から重み付けされた選択が固定seedで期待通り出力されるか検証する。
|
|
|
|
グラフ構造:
|
|
strA("Apple") -------> MockStringList(str1)
|
|
strB1("Banana1") ---> BalancedChoice ----> MockStringList(str2)
|
|
strB2("Banana2") --/ |
|
|
v
|
|
BalancedChoiceList(text_list) ---> ShowText
|
|
|
|
strB1, strB2, BalancedChoice を「Convert to Subgraph」でサブグラフ化する。
|
|
"""
|
|
import requests
|
|
|
|
page.goto(comfyui_server)
|
|
wait_for_comfyui(page)
|
|
|
|
page.on("console", lambda msg: print(f"Browser Console: {msg.text}"))
|
|
page.on("pageerror", lambda err: print(f"Browser Error: {err}"))
|
|
|
|
# Step 1: グラフを構築(サブグラフ化の前)
|
|
page.evaluate('''() => {
|
|
// MockStringList をJS側でも登録(Python側は __init__.py で登録済み)
|
|
class MockStringList extends LiteGraph.LGraphNode {
|
|
constructor() {
|
|
super();
|
|
this.addInput("str1", "STRING");
|
|
this.addInput("str2", "STRING");
|
|
this.addOutput("list", "STRING");
|
|
// ComfyUI固有: class_typeをgraphToPromptに正しく出力するために必要
|
|
this.comfyClass = "StringToolsMockStringList";
|
|
}
|
|
onExecute() {}
|
|
}
|
|
MockStringList.title = "MockStringList";
|
|
MockStringList.comfyClass = "StringToolsMockStringList";
|
|
LiteGraph.registerNodeType("StringToolsMockStringList", MockStringList);
|
|
|
|
app.graph.clear();
|
|
|
|
const strA = LiteGraph.createNode("StringToolsString");
|
|
strA.widgets[0].value = "Apple";
|
|
app.graph.add(strA);
|
|
|
|
const strB1 = LiteGraph.createNode("StringToolsString");
|
|
strB1.widgets[0].value = "Banana1";
|
|
app.graph.add(strB1);
|
|
|
|
const strB2 = LiteGraph.createNode("StringToolsString");
|
|
strB2.widgets[0].value = "Banana2";
|
|
app.graph.add(strB2);
|
|
|
|
const subChoice = LiteGraph.createNode("StringToolsBalancedChoice");
|
|
app.graph.add(subChoice);
|
|
const seedNodeB = LiteGraph.createNode("StringToolsSeed");
|
|
app.graph.add(seedNodeB);
|
|
seedNodeB.widgets[0].value = 1;
|
|
seedNodeB.connect(0, subChoice, 0);
|
|
|
|
strB1.connect(0, subChoice, 1);
|
|
if (subChoice.onConnectionsChange) subChoice.onConnectionsChange(1, 1, true, null, subChoice.inputs[1]);
|
|
strB2.connect(0, subChoice, 2);
|
|
if (subChoice.onConnectionsChange) subChoice.onConnectionsChange(1, 2, true, null, subChoice.inputs[2]);
|
|
|
|
const mockList = LiteGraph.createNode("StringToolsMockStringList");
|
|
app.graph.add(mockList);
|
|
|
|
strA.connect(0, mockList, 0);
|
|
subChoice.connect(0, mockList, 1);
|
|
|
|
const mainChoiceList = LiteGraph.createNode("StringToolsBalancedChoiceList");
|
|
app.graph.add(mainChoiceList);
|
|
|
|
const seedNodeMain = LiteGraph.createNode("StringToolsSeed");
|
|
app.graph.add(seedNodeMain);
|
|
seedNodeMain.widgets[0].value = 99;
|
|
seedNodeMain.connect(0, mainChoiceList, 1); // text_list=0, seed=1
|
|
|
|
mockList.connect(0, mainChoiceList, 0);
|
|
|
|
const preview = LiteGraph.createNode("PreviewAny");
|
|
if (preview) {
|
|
app.graph.add(preview);
|
|
mainChoiceList.connect(0, preview, 0);
|
|
}
|
|
|
|
// ノード参照をwindowに保持
|
|
window._strB1 = strB1;
|
|
window._strB2 = strB2;
|
|
window._subChoice = subChoice;
|
|
window._seedNodeB = seedNodeB;
|
|
window.mainChoiceListId = mainChoiceList.id;
|
|
|
|
app.graph.setDirtyCanvas(true, true);
|
|
|
|
}''')
|
|
|
|
# Step 2: Convert to Subgraph
|
|
convert_result = page.evaluate('''() => {
|
|
try {
|
|
if (typeof app.graph.convertToSubgraph !== 'function') {
|
|
return { error: "convertToSubgraph not available" };
|
|
}
|
|
|
|
const nodesToConvert = new Set([window._strB1, window._strB2, window._subChoice, window._seedNodeB]);
|
|
app.graph.convertToSubgraph(nodesToConvert);
|
|
|
|
// サブグラフ変換後のグラフ状態を確認
|
|
const nodeTypes = app.graph._nodes.map(n => n.type);
|
|
return {
|
|
success: true,
|
|
nodeCount: app.graph._nodes.length,
|
|
nodeTypes: nodeTypes
|
|
};
|
|
} catch (e) {
|
|
return { error: e.toString(), stack: e.stack ? e.stack.substring(0, 500) : '' };
|
|
}
|
|
}''')
|
|
print(f"Convert to Subgraph result: {convert_result}")
|
|
|
|
# Step 3: graphToPrompt でプロンプトが生成できるか確認
|
|
prompt_result = page.evaluate('''async () => {
|
|
try {
|
|
const p = await app.graphToPrompt();
|
|
return {
|
|
success: true,
|
|
outputKeys: Object.keys(p.output || {}),
|
|
workflowNodeCount: (p.workflow && p.workflow.nodes) ? p.workflow.nodes.length : -1,
|
|
hasDefinitions: !!(p.workflow && p.workflow.definitions),
|
|
promptSnippet: JSON.stringify(p.output).substring(0, 500)
|
|
};
|
|
} catch (e) {
|
|
return { error: e.toString(), stack: e.stack ? e.stack.substring(0, 500) : '' };
|
|
}
|
|
}''')
|
|
print(f"graphToPrompt result: {prompt_result}")
|
|
|
|
# Step 4: Python側から直接POSTして400エラーの詳細を取得
|
|
prompt_data = page.evaluate('''async () => {
|
|
const p = await app.graphToPrompt();
|
|
return { output: p.output, workflow: p.workflow };
|
|
}''')
|
|
|
|
import json
|
|
payload = {
|
|
"client_id": "test",
|
|
"prompt": prompt_data["output"],
|
|
"extra_data": {
|
|
"extra_pnginfo": {
|
|
"workflow": prompt_data["workflow"]
|
|
}
|
|
}
|
|
}
|
|
print(f"Prompt keys: {list(prompt_data['output'].keys())}")
|
|
print(f"Workflow has definitions: {'definitions' in prompt_data.get('workflow', {})}")
|
|
definitions = prompt_data.get('workflow', {}).get('definitions')
|
|
if definitions:
|
|
print(f"Definitions keys: {list(definitions.keys())}")
|
|
|
|
resp_prompt = requests.post(
|
|
f"{comfyui_server}/prompt",
|
|
json=payload,
|
|
headers={"Content-Type": "application/json"},
|
|
timeout=10
|
|
)
|
|
print(f"POST /prompt status: {resp_prompt.status_code}")
|
|
print(f"POST /prompt body: {resp_prompt.text[:500]}")
|
|
|
|
if resp_prompt.status_code == 200:
|
|
page.wait_for_timeout(5000)
|
|
else:
|
|
# エラーでも継続して詳細を確認
|
|
page.wait_for_timeout(2000)
|
|
|
|
# Step 5: ヒストリーから結果を取得して検証
|
|
resp = requests.get(f"{comfyui_server}/history", timeout=5)
|
|
history_data = resp.json()
|
|
print(f"History entry count: {len(history_data)}")
|
|
|
|
assert history_data, "No history entries found after queuePrompt"
|
|
|
|
latest_key = list(history_data.keys())[-1]
|
|
latest = history_data[latest_key]
|
|
status_info = latest.get("status", {})
|
|
print(f"Latest history status: {status_info}")
|
|
|
|
# history全体のキーを確認
|
|
print(f"History top-level keys: {list(latest.keys())}")
|
|
|
|
outputs = latest.get("outputs", {})
|
|
print(f"Output node IDs: {list(outputs.keys())}")
|
|
|
|
# outputs が空の場合はstatus/metaを詳細にダンプ
|
|
if not outputs:
|
|
import json
|
|
print(f"Full history entry: {json.dumps(latest, default=str)[:2000]}")
|
|
status_msgs = latest.get("status", {}).get("messages", [])
|
|
for msg in status_msgs:
|
|
print(f" status_msg: {msg}")
|
|
|
|
|
|
# PreviewAnyの出力を探す(OUTPUT_NODEの出力はそのIDで格納される)
|
|
found_output = None
|
|
for k, v in outputs.items():
|
|
print(f" output[{k}]: {str(v)[:200]}")
|
|
# text キーに結果がある場合 (PreviewAnyのui出力形式)
|
|
if "text" in v:
|
|
found_output = v
|
|
break
|
|
|
|
if found_output is None and outputs:
|
|
# text キーがなくても最初の出力を使う
|
|
found_output = list(outputs.values())[0]
|
|
|
|
if found_output is not None:
|
|
out_str = str(found_output)
|
|
print(f"Output: {out_str}")
|
|
assert "Banana" in out_str or "Apple" in out_str, f"Output was {out_str}"
|
|
else:
|
|
pytest.fail(f"No outputs found in history. Keys: {list(outputs.keys())}")
|
|
|
|
def test_string_tools_execution_mixed_concat(page: Page, comfyui_server: str, wait_for_comfyui):
|
|
"""
|
|
StringToolsString (1行) と StringToolsText (複数行) が
|
|
StringToolsConcat で正しく結合されるか検証する。
|
|
"""
|
|
page.goto(comfyui_server)
|
|
wait_for_comfyui(page)
|
|
time.sleep(2) # 登録待ち
|
|
|
|
page.evaluate('''() => {
|
|
app.graph.clear();
|
|
|
|
const createAndSetup = (type, val) => {
|
|
const node = LiteGraph.createNode(type);
|
|
if (!node) {
|
|
throw new Error(`Failed to create node of type: ${type}. Is it registered? Available types: ${Object.keys(LiteGraph.registered_node_types).filter(t => t.includes("StringTools"))}`);
|
|
}
|
|
app.graph.add(node);
|
|
if (node.widgets && node.widgets.length > 0) {
|
|
node.widgets[0].value = val;
|
|
}
|
|
return node;
|
|
};
|
|
|
|
const connectNode = (fromNode, toNode, slot) => {
|
|
if (!fromNode || !toNode) throw new Error("Connection failed: nodes must be valid");
|
|
fromNode.connect(0, toNode, slot);
|
|
if (toNode.onConnectionsChange) {
|
|
toNode.onConnectionsChange(1, slot, true, null, toNode.inputs[slot]);
|
|
}
|
|
};
|
|
|
|
// StringToolsString (1行)
|
|
const strNode = createAndSetup("StringToolsString", "Line1");
|
|
|
|
// StringToolsText (複数行)
|
|
const textNode = createAndSetup("StringToolsText", "Line2\\nLine3");
|
|
|
|
// Concat
|
|
const concat = createAndSetup("StringToolsConcat", "");
|
|
|
|
// Separator
|
|
const sepNode = createAndSetup("StringToolsString", " | ");
|
|
connectNode(sepNode, concat, 0);
|
|
|
|
connectNode(strNode, concat, 1);
|
|
connectNode(textNode, concat, 2);
|
|
|
|
// Preview
|
|
let preview = LiteGraph.createNode("PreviewAny");
|
|
if (!preview) preview = LiteGraph.createNode("Preview as Text"); // Fallback
|
|
if (!preview) throw new Error("PreviewAny or Preview as Text node not found");
|
|
|
|
app.graph.add(preview);
|
|
concat.connect(0, preview, 0);
|
|
|
|
window.tgtNodeId = preview.id;
|
|
app.graph.setDirtyCanvas(true, true);
|
|
}''')
|
|
|
|
# 実行
|
|
history = run_prompt_and_wait(page, comfyui_server)
|
|
|
|
tgt_id = page.evaluate("window.tgtNodeId")
|
|
outputs = history.get("outputs", {})
|
|
|
|
assert str(tgt_id) in outputs
|
|
output = outputs[str(tgt_id)]
|
|
|
|
# PreviewAny の出力形式に合わせて text を取得
|
|
out_str = ""
|
|
if "text" in output:
|
|
out_str = str(output["text"])
|
|
else:
|
|
out_str = str(output)
|
|
|
|
print(f"Mixed Concat Output: {out_str}")
|
|
# 期待される文字列が含まれているか
|
|
assert "Line1 | Line2\\nLine3" in out_str or "Line1 | Line2\nLine3" in out_str
|
|
|
|
|
|
def test_string_tools_widget_types(page: Page, comfyui_server: str, wait_for_comfyui):
|
|
"""
|
|
StringToolsString が 1行入力(input相当)、
|
|
StringToolsText が 複数行入力(textarea相当)であることを検証。
|
|
"""
|
|
page.goto(comfyui_server)
|
|
wait_for_comfyui(page)
|
|
time.sleep(2)
|
|
|
|
widget_info = page.evaluate('''() => {
|
|
app.graph.clear();
|
|
const sNode = LiteGraph.createNode("StringToolsString");
|
|
const tNode = LiteGraph.createNode("StringToolsText");
|
|
|
|
const getWidgetType = (node) => {
|
|
if (!node || !node.widgets || node.widgets.length === 0) return null;
|
|
const w = node.widgets[0];
|
|
// ComfyUIでは multiline: true の場合、widget.type が "customtext" になる。
|
|
return {
|
|
type: w.type,
|
|
multiline: w.type === "customtext" || !!(w.options && w.options.multiline)
|
|
};
|
|
};
|
|
|
|
return {
|
|
string: getWidgetType(sNode),
|
|
text: getWidgetType(tNode)
|
|
};
|
|
}''')
|
|
|
|
print(f"Widget Info: {widget_info}")
|
|
assert widget_info["string"]["multiline"] is False
|
|
assert widget_info["text"]["multiline"] is True
|
|
|
|
|
|
def test_string_tools_list_input_handling(page: Page, comfyui_server: str, wait_for_comfyui):
|
|
"""
|
|
List系ノード (StringToolsConcatList) が正しくリスト入力を受け取れるか検証。
|
|
"""
|
|
page.goto(comfyui_server)
|
|
wait_for_comfyui(page)
|
|
time.sleep(2)
|
|
|
|
page.evaluate('''() => {
|
|
if (!LiteGraph.getNodeType("StringToolsMockStringList")) {
|
|
class MockStringList extends LiteGraph.LGraphNode {
|
|
constructor() {
|
|
super();
|
|
this.addInput("str1", "STRING");
|
|
this.addInput("str2", "STRING");
|
|
this.addOutput("list", "STRING");
|
|
this.comfyClass = "StringToolsMockStringList";
|
|
}
|
|
}
|
|
MockStringList.title = "MockStringList";
|
|
MockStringList.comfyClass = "StringToolsMockStringList";
|
|
LiteGraph.registerNodeType("StringToolsMockStringList", MockStringList);
|
|
}
|
|
app.graph.clear();
|
|
|
|
const createAndSetup = (type, val) => {
|
|
const node = LiteGraph.createNode(type);
|
|
app.graph.add(node);
|
|
if (node.widgets && node.widgets.length > 0) {
|
|
node.widgets[0].value = val;
|
|
}
|
|
return node;
|
|
};
|
|
|
|
const s1 = createAndSetup("StringToolsString", "AAA");
|
|
const s2 = createAndSetup("StringToolsString", "BBB");
|
|
const sep = createAndSetup("StringToolsString", ", ");
|
|
|
|
// MockStringList は str1, str2 を結合して「リスト形式」で返す想定
|
|
const mock = LiteGraph.createNode("StringToolsMockStringList");
|
|
app.graph.add(mock);
|
|
s1.connect(0, mock, 0);
|
|
s2.connect(0, mock, 1);
|
|
|
|
const concatList = LiteGraph.createNode("StringToolsConcatList");
|
|
app.graph.add(concatList);
|
|
|
|
mock.connect(0, concatList, 0); // text_list
|
|
sep.connect(0, concatList, 1); // separator
|
|
|
|
const preview = LiteGraph.createNode("PreviewAny");
|
|
app.graph.add(preview);
|
|
concatList.connect(0, preview, 0);
|
|
|
|
window.tgtNodeId = preview.id;
|
|
}''')
|
|
|
|
history = run_prompt_and_wait(page, comfyui_server)
|
|
tgt_id = page.evaluate("window.tgtNodeId")
|
|
output = history["outputs"][str(tgt_id)]
|
|
|
|
out_str = str(output.get("text", output))
|
|
print(f"List Handling Output: {out_str}")
|
|
# MockStringList(AAA, BBB) -> ["AAA", "BBB"] (INPUT_IS_LIST=Trueでリスト結合)
|
|
# ConcatList(["AAA", "BBB"], ", ") -> "AAA, BBB"
|
|
assert "AAA, BBB" in out_str
|
|
|
|
|
|
def test_string_tools_mock_string_list_output(page: Page, comfyui_server: str, wait_for_comfyui):
|
|
"""
|
|
MockStringList の出力が実際にリスト形式として扱われているか検証。
|
|
"""
|
|
page.goto(comfyui_server)
|
|
wait_for_comfyui(page)
|
|
time.sleep(2)
|
|
|
|
page.evaluate('''() => {
|
|
if (!LiteGraph.getNodeType("StringToolsMockStringList")) {
|
|
class MockStringList extends LiteGraph.LGraphNode {
|
|
constructor() {
|
|
super();
|
|
this.addInput("str1", "STRING");
|
|
this.addInput("str2", "STRING");
|
|
this.addOutput("list", "STRING");
|
|
this.comfyClass = "StringToolsMockStringList";
|
|
}
|
|
}
|
|
MockStringList.title = "MockStringList";
|
|
MockStringList.comfyClass = "StringToolsMockStringList";
|
|
LiteGraph.registerNodeType("StringToolsMockStringList", MockStringList);
|
|
}
|
|
app.graph.clear();
|
|
|
|
const createAndSetup = (type, val) => {
|
|
const node = LiteGraph.createNode(type);
|
|
app.graph.add(node);
|
|
if (node.widgets && node.widgets.length > 0) {
|
|
node.widgets[0].value = val;
|
|
}
|
|
return node;
|
|
};
|
|
|
|
const s1 = createAndSetup("StringToolsString", "Item1");
|
|
const s2 = createAndSetup("StringToolsString", "Item2");
|
|
const sep = createAndSetup("StringToolsString", "::");
|
|
|
|
const mock = LiteGraph.createNode("StringToolsMockStringList");
|
|
app.graph.add(mock);
|
|
s1.connect(0, mock, 0);
|
|
s2.connect(0, mock, 1);
|
|
|
|
// ConcatList はリストを受け取って結合する
|
|
const concatList = LiteGraph.createNode("StringToolsConcatList");
|
|
app.graph.add(concatList);
|
|
|
|
mock.connect(0, concatList, 0); // text_list
|
|
sep.connect(0, concatList, 1); // separator
|
|
|
|
const preview = LiteGraph.createNode("PreviewAny");
|
|
app.graph.add(preview);
|
|
concatList.connect(0, preview, 0);
|
|
|
|
window.tgtNodeId = preview.id;
|
|
}''')
|
|
|
|
history = run_prompt_and_wait(page, comfyui_server)
|
|
tgt_id = page.evaluate("window.tgtNodeId")
|
|
output = history["outputs"][str(tgt_id)]
|
|
|
|
out_str = str(output.get("text", output))
|
|
print(f"Mock List Output: {out_str}")
|
|
# MockStringList(Item1, Item2) -> ["Item1", "Item2"]
|
|
# ConcatList(["Item1", "Item2"], "::") -> "Item1::Item2"
|
|
assert "Item1::Item2" in out_str
|
|
|
|
|
|
def test_string_tools_complex_interconnection(page: Page, comfyui_server: str, wait_for_comfyui):
|
|
"""
|
|
複雑な相互接続の検証:
|
|
1. BalancedChoiceList -> Concat (text_0)
|
|
2. ConcatList -> BalancedChoice (text_0)
|
|
|
|
List系ノードが ([result],) を返しても、単一系ノードの動的ポートで
|
|
適切にアンラップされ、TypeError が発生しないことを検証する。
|
|
"""
|
|
page.goto(comfyui_server)
|
|
wait_for_comfyui(page)
|
|
time.sleep(10)
|
|
|
|
# --- 構成 (ブラウザ上でグラフを構築し、プロンプトデータを抽出) ---
|
|
prompt_id = page.evaluate('''async () => {
|
|
app.graph.clear();
|
|
const create = (type) => {
|
|
const node = LiteGraph.createNode(type);
|
|
app.graph.add(node);
|
|
return node;
|
|
};
|
|
const connect = (from, to, slot) => from.connect(0, to, slot);
|
|
|
|
const s1 = create("StringToolsString"); s1.widgets[0].value = "Apple_" + Math.floor(Math.random() * 1000000);
|
|
const c1 = create("StringToolsConcatList");
|
|
connect(s1, c1, 0);
|
|
|
|
const b1 = create("StringToolsBalancedChoiceList");
|
|
const sd1 = create("StringToolsSeed"); sd1.widgets[0].value = Math.floor(Math.random() * 1000000);
|
|
connect(sd1, b1, 0);
|
|
connect(c1, b1, 1);
|
|
|
|
const fc = create("StringToolsConcat");
|
|
const sp1 = create("StringToolsString"); sp1.widgets[0].value = " + ";
|
|
connect(sp1, fc, 0);
|
|
connect(b1, fc, 1);
|
|
const st1 = create("StringToolsString"); st1.widgets[0].value = "Fruit";
|
|
connect(st1, fc, 2);
|
|
|
|
const pa = create("PreviewAny");
|
|
connect(fc, pa, 0);
|
|
|
|
// プロンプトデータを取得
|
|
const p = await app.graphToPrompt();
|
|
|
|
// 実行 (fetch を使って直接 API に投げ、prompt_id を返す)
|
|
const response = await fetch("/prompt", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
prompt: p.output,
|
|
client_id: "test_client"
|
|
})
|
|
});
|
|
const res = await response.json();
|
|
return { prompt_id: res.prompt_id, target_id: pa.id };
|
|
}''')
|
|
|
|
assert prompt_id and "prompt_id" in prompt_id, "Prompt execution failed"
|
|
pid = prompt_id["prompt_id"]
|
|
target_node_id = str(prompt_id["target_id"])
|
|
|
|
# --- 検証 ---
|
|
start_time = time.time()
|
|
out_str = ""
|
|
|
|
while time.time() - start_time < 30:
|
|
resp = urllib.request.urlopen(f"{comfyui_server}/history")
|
|
histories = json.loads(resp.read().decode('utf-8'))
|
|
|
|
if pid in histories:
|
|
history = histories[pid]
|
|
outputs = history.get("outputs", {})
|
|
if target_node_id in outputs:
|
|
out_str = str(outputs[target_node_id])
|
|
if "Fruit" in out_str:
|
|
break
|
|
time.sleep(2)
|
|
|
|
print(f"Complex Interconnection Output (Final API Result): {out_str}")
|
|
|
|
# 検証:
|
|
assert "Fruit" in out_str, f"Expected 'Fruit' in output, but got: {out_str}"
|
|
assert " + " in out_str
|
|
assert ("Apple" in out_str or "Banana" in out_str)
|