Files
Taremin-comfyui-string-tools/tests/test_string_tools.py
T
Taremin c8647cab54 fix: ComfyUIの仕様変更に伴うリスト型ノードの出力バグの修正
最新の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 の起動オプションにホワイトリストを追加しテスト実行速度を高速化
2026-07-04 09:08:02 +09:00

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)