diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml new file mode 100644 index 000000000..1470d110c --- /dev/null +++ b/.github/workflows/test-ui.yaml @@ -0,0 +1,17 @@ +name: Tests CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Run Tests + run: | + npm install + npm test + working-directory: ./tests-ui diff --git a/tests-ui/globalSetup.js b/tests-ui/globalSetup.js index 4942f3fa7..b9d97f58a 100644 --- a/tests-ui/globalSetup.js +++ b/tests-ui/globalSetup.js @@ -9,4 +9,6 @@ module.exports = async function () { global.enableWebGLCanvas = nop; HTMLCanvasElement.prototype.getContext = nop; + + localStorage["Comfy.Settings.Comfy.Logging.Enabled"] = "false"; }; diff --git a/tests-ui/jest.config.js b/tests-ui/jest.config.js index a5fa0e035..b5a5d646d 100644 --- a/tests-ui/jest.config.js +++ b/tests-ui/jest.config.js @@ -1,16 +1,9 @@ -const path = require("path"); /** @type {import('jest').Config} */ const config = { testEnvironment: "jsdom", - // transform: { - // "^.+\\.[t|j]sx?$": "babel-jest", - // }, setupFiles: ["./globalSetup.js"], - // moduleDirectories: ["node_modules", path.resolve("../web/scripts")], - // moduleNameMapper: { - // "./api.js": path.resolve("../web/scripts/api.js"), - // "./api": path.resolve("../web/scripts/api.js"), - // }, + clearMocks: true, + resetModules: true, }; module.exports = config; diff --git a/tests-ui/tests/widgetInputs.test.js b/tests-ui/tests/widgetInputs.test.js index 40f0528df..022e54926 100644 --- a/tests-ui/tests/widgetInputs.test.js +++ b/tests-ui/tests/widgetInputs.test.js @@ -1,9 +1,49 @@ -/// // @ts-check +/// -const { start } = require("../utils"); +const { start, makeNodeDef, checkBeforeAndAfterReload, assertNotNullOrUndefined } = require("../utils"); const lg = require("../utils/litegraph"); +/** + * @typedef { import("../utils/ezgraph") } Ez + * @typedef { ReturnType["ez"] } EzNodeFactory + */ + +/** + * @param { EzNodeFactory } ez + * @param { InstanceType } graph + * @param { InstanceType } input + * @param { string } widgetType + * @param { boolean } hasControlWidget + * @returns + */ +async function connectPrimitiveAndReload(ez, graph, input, widgetType, hasControlWidget) { + // Connect to primitive and ensure its still connected after + let primitive = ez.PrimitiveNode(); + primitive.outputs[0].connectTo(input); + + await checkBeforeAndAfterReload(graph, async () => { + primitive = graph.find(primitive); + let { connections } = primitive.outputs[0]; + expect(connections).toHaveLength(1); + expect(connections[0].targetNode.id).toBe(input.node.node.id); + + // Ensure widget is correct type + const valueWidget = primitive.widgets.value; + expect(valueWidget.widget.type).toBe(widgetType); + + // Check if control_after_generate should be added + if (hasControlWidget) { + const controlWidget = primitive.widgets.control_after_generate; + expect(controlWidget.widget.type).toBe("combo"); + } + + // Ensure we dont have other widgets + expect(primitive.node.widgets).toHaveLength(1 + +!!hasControlWidget); + }); + + return primitive; +} describe("widget inputs", () => { beforeEach(() => { @@ -12,7 +52,6 @@ describe("widget inputs", () => { afterEach(() => { lg.teardown(global); - jest.resetModules(); }); [ @@ -28,58 +67,25 @@ describe("widget inputs", () => { { name: "combo", type: ["a", "b", "c"], control: true }, ].forEach((c) => { test(`widget conversion + primitive works on ${c.name}`, async () => { - /** - * Test node with widgets of each type - * @type { import("../../web/types/comfy").ComfyObjectInfo } ComfyObjectInfo - */ - const WidgetTestNode = { - category: "test", - name: "WidgetTestNode", - output_name: [], - input: { - required: { - [c.name]: [c.type, c.opt ?? {}], - }, - }, - }; - - const { ez } = await start({ - mockNodeDefs: { - WidgetTestNode, - }, + const { ez, graph } = await start({ + mockNodeDefs: makeNodeDef("TestNode", { [c.name]: [c.type, c.opt ?? {}] }), }); // Create test node and convert to input - const n = ez.WidgetTestNode(); + const n = ez.TestNode(); const w = n.widgets[c.name]; w.convertToInput(); expect(w.isConvertedToInput).toBeTruthy(); const input = w.getConvertedInput(); expect(input).toBeTruthy(); - // Connect to primitive - const p1 = ez.PrimitiveNode(); - // @ts-ignore : input is valid - p1.outputs[0].connectTo(input); - expect(p1.outputs[0].connectTo).toHaveLength(1); - - // Ensure widget is correct type - const valueWidget = p1.widgets.value; - expect(valueWidget.widget.type).toBe(c.widget ?? c.name); - - // Check if control_after_generate should be added - if (c.control) { - const controlWidget = p1.widgets.control_after_generate; - expect(controlWidget.widget.type).toBe("combo"); - } - - // Ensure we dont have other widgets - expect(p1.node.widgets).toHaveLength(1 + +!!c.control); + // @ts-ignore : input is valid here + await connectPrimitiveAndReload(ez, graph, input, c.widget ?? c.name, c.control); }); }); test("converted widget works after reload", async () => { - const { graph, ez } = await start(); + const { ez, graph } = await start(); let n = ez.CheckpointLoaderSimple(); const inputCount = n.inputs.length; @@ -100,31 +106,14 @@ describe("widget inputs", () => { n.widgets.ckpt_name.convertToInput(); expect(n.inputs.length).toEqual(inputCount + 1); - let primitive = ez.PrimitiveNode(); - primitive.outputs[0].connectTo(n.inputs.ckpt_name); - - await graph.reload(); - - // Find the reloaded nodes in the graph - n = graph.find(n); - primitive = graph.find(primitive); - - // Ensure widget is converted - expect(n.widgets.ckpt_name.isConvertedToInput).toBeTruthy(); - expect(n.inputs.ckpt_name).toBeTruthy(); - expect(n.inputs.length).toEqual(inputCount + 1); - - // Ensure primitive is connected - let { connections } = primitive.outputs[0]; - expect(connections).toHaveLength(1); - expect(connections[0].targetNode.id).toBe(n.node.id); + const primitive = await connectPrimitiveAndReload(ez, graph, n.inputs.ckpt_name, "combo", true); // Disconnect & reconnect - connections[0].disconnect(); - ({ connections } = primitive.outputs[0]); + primitive.outputs[0].connections[0].disconnect(); + let { connections } = primitive.outputs[0]; expect(connections).toHaveLength(0); - primitive.outputs[0].connectTo(n.inputs.ckpt_name); + primitive.outputs[0].connectTo(n.inputs.ckpt_name); ({ connections } = primitive.outputs[0]); expect(connections).toHaveLength(1); expect(connections[0].targetNode.id).toBe(n.node.id); @@ -165,4 +154,166 @@ describe("widget inputs", () => { expect(clone.widgets.ckpt_name.isConvertedToInput).toBeFalsy(); expect(clone.inputs.ckpt_name).toBeFalsy(); }); -}); \ No newline at end of file + + test("shows missing node error on custom node with converted input", async () => { + const { graph } = await start(); + + const dialogShow = jest.spyOn(graph.app.ui.dialog, "show"); + + await graph.app.loadGraphData({ + last_node_id: 3, + last_link_id: 4, + nodes: [ + { + id: 1, + type: "TestNode", + pos: [41.87329101561909, 389.7381480823742], + size: { 0: 220, 1: 374 }, + flags: {}, + order: 1, + mode: 0, + inputs: [{ name: "test", type: "FLOAT", link: 4, widget: { name: "test" }, slot_index: 0 }], + outputs: [], + properties: { "Node name for S&R": "TestNode" }, + widgets_values: [1], + }, + { + id: 3, + type: "PrimitiveNode", + pos: [-312, 433], + size: { 0: 210, 1: 82 }, + flags: {}, + order: 0, + mode: 0, + outputs: [{ links: [4], widget: { name: "test" } }], + title: "test", + properties: {}, + }, + ], + links: [[4, 3, 0, 1, 6, "FLOAT"]], + groups: [], + config: {}, + extra: {}, + version: 0.4, + }); + + expect(dialogShow).toBeCalledTimes(1); + expect(dialogShow.mock.calls[0][0]).toContain("the following node types were not found"); + expect(dialogShow.mock.calls[0][0]).toContain("TestNode"); + }); + + test("defaultInput widgets can be converted back to inputs", async () => { + const { graph, ez } = await start({ + mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { defaultInput: true }] }), + }); + + // Create test node and ensure it starts as an input + let n = ez.TestNode(); + let w = n.widgets.example; + expect(w.isConvertedToInput).toBeTruthy(); + let input = w.getConvertedInput(); + expect(input).toBeTruthy(); + + // Ensure it can be converted to + w.convertToWidget(); + expect(w.isConvertedToInput).toBeFalsy(); + expect(n.inputs.length).toEqual(0); + // and from + w.convertToInput(); + expect(w.isConvertedToInput).toBeTruthy(); + input = w.getConvertedInput(); + + // Reload and ensure it still only has 1 converted widget + if (!assertNotNullOrUndefined(input)) return; + + await connectPrimitiveAndReload(ez, graph, input, "number", true); + n = graph.find(n); + expect(n.widgets).toHaveLength(1); + w = n.widgets.example; + expect(w.isConvertedToInput).toBeTruthy(); + + // Convert back to widget and ensure it is still a widget after reload + w.convertToWidget(); + await graph.reload(); + n = graph.find(n); + expect(n.widgets).toHaveLength(1); + expect(n.widgets[0].isConvertedToInput).toBeFalsy(); + expect(n.inputs.length).toEqual(0); + }); + + test("forceInput widgets can not be converted back to inputs", async () => { + const { graph, ez } = await start({ + mockNodeDefs: makeNodeDef("TestNode", { example: ["INT", { forceInput: true }] }), + }); + + // Create test node and ensure it starts as an input + let n = ez.TestNode(); + let w = n.widgets.example; + expect(w.isConvertedToInput).toBeTruthy(); + const input = w.getConvertedInput(); + expect(input).toBeTruthy(); + + // Convert to widget should error + expect(() => w.convertToWidget()).toThrow(); + + // Reload and ensure it still only has 1 converted widget + if (assertNotNullOrUndefined(input)) { + await connectPrimitiveAndReload(ez, graph, input, "number", true); + n = graph.find(n); + expect(n.widgets).toHaveLength(1); + expect(n.widgets.example.isConvertedToInput).toBeTruthy(); + } + }); + + test("primitive can connect to matching combos on converted widgets", async () => { + const { ez } = await start({ + mockNodeDefs: { + ...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }), + ...makeNodeDef("TestNode2", { example: [["A", "B", "C"], { forceInput: true }] }), + }, + }); + + const n1 = ez.TestNode1(); + const n2 = ez.TestNode2(); + const p = ez.PrimitiveNode(); + p.outputs[0].connectTo(n1.inputs[0]); + p.outputs[0].connectTo(n2.inputs[0]); + expect(p.outputs[0].connections).toHaveLength(2); + const valueWidget = p.widgets.value; + expect(valueWidget.widget.type).toBe("combo"); + expect(valueWidget.widget.options.values).toEqual(["A", "B", "C"]); + }); + + test("primitive can not connect to non matching combos on converted widgets", async () => { + const { ez } = await start({ + mockNodeDefs: { + ...makeNodeDef("TestNode1", { example: [["A", "B", "C"], { forceInput: true }] }), + ...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true }] }), + }, + }); + + const n1 = ez.TestNode1(); + const n2 = ez.TestNode2(); + const p = ez.PrimitiveNode(); + p.outputs[0].connectTo(n1.inputs[0]); + expect(() => p.outputs[0].connectTo(n2.inputs[0])).toThrow(); + expect(p.outputs[0].connections).toHaveLength(1); + }); + + test("combo output can not connect to non matching combos list input", async () => { + const { ez } = await start({ + mockNodeDefs: { + ...makeNodeDef("TestNode1", {}, [["A", "B"]]), + ...makeNodeDef("TestNode2", { example: [["A", "B"], { forceInput: true}] }), + ...makeNodeDef("TestNode3", { example: [["A", "B", "C"], { forceInput: true}] }), + }, + }); + + const n1 = ez.TestNode1(); + const n2 = ez.TestNode2(); + const n3 = ez.TestNode3(); + + n1.outputs[0].connectTo(n2.inputs[0]); + expect(() => n1.outputs[0].connectTo(n3.inputs[0])).toThrow(); + }); +}); diff --git a/tests-ui/utils/ezgraph.js b/tests-ui/utils/ezgraph.js index fd1d2dc7f..0e81fd47b 100644 --- a/tests-ui/utils/ezgraph.js +++ b/tests-ui/utils/ezgraph.js @@ -12,7 +12,7 @@ * @typedef { (...args: EzOutput[] | [...EzOutput[], Record]) => EzNode } EzNodeFactory */ -class EzConnection { +export class EzConnection { /** @type { app } */ app; /** @type { InstanceType } */ @@ -48,7 +48,7 @@ class EzConnection { } } -class EzSlot { +export class EzSlot { /** @type { EzNode } */ node; /** @type { number } */ @@ -64,7 +64,7 @@ class EzSlot { } } -class EzInput extends EzSlot { +export class EzInput extends EzSlot { /** @type { INodeInputSlot } */ input; @@ -83,7 +83,7 @@ class EzInput extends EzSlot { } } -class EzOutput extends EzSlot { +export class EzOutput extends EzSlot { /** @type { INodeOutputSlot } */ output; @@ -98,14 +98,17 @@ class EzOutput extends EzSlot { } get connections() { - return (this.node.node.outputs?.[this.index]?.links ?? []) - .map(l => new EzConnection(this.node.app, this.node.app.graph.links[l])); + return (this.node.node.outputs?.[this.index]?.links ?? []).map( + (l) => new EzConnection(this.node.app, this.node.app.graph.links[l]) + ); } /** * @param { EzInput } input */ connectTo(input) { + if (!input) throw new Error("Invalid input"); + /** * @type { LG["LLink"] | null } */ @@ -123,18 +126,22 @@ class EzOutput extends EzSlot { } } -class EzNodeMenuItem { +export class EzNodeMenuItem { /** @type { EzNode } */ node; + /** @type { number } */ + index; /** @type { ContextMenuItem } */ item; /** * @param { EzNode } node + * @param { number } index * @param { ContextMenuItem } item */ - constructor(node, item) { + constructor(node, index, item) { this.node = node; + this.index = index; this.item = item; } @@ -147,18 +154,22 @@ class EzNodeMenuItem { } } -class EzWidget { +export class EzWidget { /** @type { EzNode } */ node; + /** @type { number } */ + index; /** @type { IWidget } */ widget; /** * @param { EzNode } node + * @param { number } index * @param { IWidget } widget */ - constructor(node, widget) { + constructor(node, index, widget) { this.node = node; + this.index = index; this.widget = widget; } @@ -176,10 +187,9 @@ class EzWidget { } getConvertedInput() { - if (!this.isConvertedToInput) - throw new Error(`Widget ${this.widget.name} is not converted to input.`); - - return this.node.inputs.find(inp => inp.input["widget"]?.name === this.widget.name); + if (!this.isConvertedToInput) throw new Error(`Widget ${this.widget.name} is not converted to input.`); + + return this.node.inputs.find((inp) => inp.input["widget"]?.name === this.widget.name); } convertToWidget() { @@ -195,7 +205,7 @@ class EzWidget { } } -class EzNode { +export class EzNode { /** @type { app } */ app; /** @type { LGNode } */ @@ -215,55 +225,68 @@ class EzNode { } get inputs() { - return this.#getSlotItems("inputs"); + return this.#makeLookupArray("inputs", "name", EzInput); } get outputs() { - return this.#getSlotItems("outputs"); + return this.#makeLookupArray("outputs", "name", EzOutput); } - /** @returns { Record } */ get widgets() { - return (this.node.widgets ?? []).reduce((p, w, i) => { - p[w.name ?? i] = new EzWidget(this, w); - return p; - }, {}); + return this.#makeLookupArray("widgets", "name", EzWidget); } get menu() { - const items = this.app.canvas.getNodeMenuOptions(this.node); - return items.reduce((p, w) => { - if(w?.content) { - p[w.content] = new EzNodeMenuItem(this, w); - } - return p; - }, {}); + return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem); } select() { this.app.canvas.selectNode(this.node); } - /** - * @template { "inputs" | "outputs" } T - * @param { T } type - * @returns { Record & (type extends "inputs" ? EzInput [] : EzOutput[]) } - */ - #getSlotItems(type) { - // @ts-ignore : these items are correct - return (this.node[type] ?? []).reduce((p, s, i) => { - if(s.name in p) { - throw new Error(`Unable to store input ${s.name} on array as name conflicts.`); - } - ; + // /** + // * @template { "inputs" | "outputs" } T + // * @param { T } type + // * @returns { Record & (type extends "inputs" ? EzInput [] : EzOutput[]) } + // */ + // #getSlotItems(type) { + // // @ts-ignore : these items are correct + // return (this.node[type] ?? []).reduce((p, s, i) => { + // if (s.name in p) { + // throw new Error(`Unable to store input ${s.name} on array as name conflicts.`); + // } + // // @ts-ignore + // p.push((p[s.name] = new (type === "inputs" ? EzInput : EzOutput)(this, i, s))); + // return p; + // }, Object.assign([], { $: this })); + // } + + /** + * @template { { new(node: EzNode, index: number, obj: any): any } } T + * @param { "inputs" | "outputs" | "widgets" | (() => Array) } nodeProperty + * @param { string } nameProperty + * @param { T } ctor + * @returns { Record> & Array> } + */ + #makeLookupArray(nodeProperty, nameProperty, ctor) { + const items = typeof nodeProperty === "function" ? nodeProperty() : this.node[nodeProperty]; + // @ts-ignore + return (items ?? []).reduce((p, s, i) => { + if (!s) return p; + + const name = s[nameProperty]; // @ts-ignore - p.push(p[s.name] = new (type === "inputs" ? EzInput : EzOutput)(this, i, s)); + if (!name || name in p) { + throw new Error(`Unable to store ${nodeProperty} ${name} on array as name conflicts.`); + } + // @ts-ignore + p.push((p[name] = new ctor(this, i, s))); return p; - }, Object.assign([], {$: this})) + }, Object.assign([], { $: this })); } } -class EzGraph { +export class EzGraph { /** @type { app } */ app; @@ -373,8 +396,7 @@ export const Ez = { const inputs = ezNode.inputs; let slot = 0; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; + for (const arg of args) { if (arg instanceof EzOutput) { arg.connectTo(inputs[slot++]); } else { diff --git a/tests-ui/utils/index.js b/tests-ui/utils/index.js index 33d0ac025..01c58b21f 100644 --- a/tests-ui/utils/index.js +++ b/tests-ui/utils/index.js @@ -12,3 +12,60 @@ export async function start(config = undefined) { await app.setup(); return Ez.graph(app, global["LiteGraph"], global["LGraphCanvas"]); } + +/** + * @param { ReturnType["graph"] } graph + * @param { (hasReloaded: boolean) => (Promise | void) } cb + */ +export async function checkBeforeAndAfterReload(graph, cb) { + await cb(false); + await graph.reload(); + await cb(true); +} + +/** + * @param { string } name + * @param { Record } input + * @param { (string | string[])[] | Record } output + * @returns { Record } + */ +export function makeNodeDef(name, input, output = {}) { + const nodeDef = { + name, + category: "test", + output: [], + output_name: [], + output_is_list: [], + input: { + required: {} + }, + }; + for(const k in input) { + nodeDef.input.required[k] = typeof input[k] === "string" ? [input[k], {}] : [...input[k]]; + } + if(output instanceof Array) { + output = output.reduce((p, c) => { + p[c] = c; + return p; + }, {}) + } + for(const k in output) { + nodeDef.output.push(output[k]); + nodeDef.output_name.push(k); + nodeDef.output_is_list.push(false); + } + + return { [name]: nodeDef }; +} + +/** +/** + * @template { any } T + * @param { T } x + * @returns { x is Exclude } + */ +export function assertNotNullOrUndefined(x) { + expect(x).not.toEqual(null); + expect(x).not.toEqual(undefined); + return true; +} \ No newline at end of file diff --git a/tests-ui/utils/litegraph.js b/tests-ui/utils/litegraph.js index 821bbe38b..777f8c3ba 100644 --- a/tests-ui/utils/litegraph.js +++ b/tests-ui/utils/litegraph.js @@ -30,4 +30,7 @@ export function setup(ctx) { export function teardown(ctx) { forEachKey((k) => delete ctx[k]); + + // Clear document after each run + document.getElementsByTagName("html")[0].innerHTML = ""; } diff --git a/web/extensions/core/widgetInputs.js b/web/extensions/core/widgetInputs.js index 3cf755a8d..ffc26ea78 100644 --- a/web/extensions/core/widgetInputs.js +++ b/web/extensions/core/widgetInputs.js @@ -100,6 +100,27 @@ function getWidgetType(config) { return { type }; } + +function isValidCombo(combo, obj) { + // New input isnt a combo + if (!(obj instanceof Array)) { + console.log(`connection rejected: tried to connect combo to ${obj}`); + return false; + } + // New imput combo has a different size + if (combo.length !== obj.length) { + console.log(`connection rejected: combo lists dont match`); + return false; + } + // New input combo has different elements + if (combo.find((v, i) => obj[i] !== v)) { + console.log(`connection rejected: combo lists dont match`); + return false; + } + + return true; +} + app.registerExtension({ name: "Comfy.WidgetInputs", async beforeRegisterNodeDef(nodeType, nodeData, app) { @@ -256,6 +277,28 @@ app.registerExtension({ return r; }; + + // Prevent connecting COMBO lists to converted inputs that dont match types + const onConnectInput = nodeType.prototype.onConnectInput; + nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) { + const v = onConnectInput?.(this, arguments); + // Not a combo, ignore + if (type !== "COMBO") return v; + // Primitive output, allow that to handle + if (originNode.outputs[originSlot].widget) return v; + + // Ensure target is also a combo + const targetCombo = this.inputs[targetSlot].widget?.[GET_CONFIG]?.()?.[0]; + if (!targetCombo || !(targetCombo instanceof Array)) return v; + + // Check they match + const originConfig = originNode.constructor?.nodeData?.output?.[originSlot]; + if (!originConfig || !isValidCombo(targetCombo, originConfig)) { + return false; + } + + return v; + }; }, registerCustomNodes() { class PrimitiveNode { @@ -315,7 +358,7 @@ app.registerExtension({ onAfterGraphConfigured() { if (this.outputs[0].links?.length && !this.widgets?.length) { - this.#onFirstConnection(); + if (!this.#onFirstConnection()) return; // Populate widget values from config data if (this.widgets) { @@ -386,13 +429,16 @@ app.registerExtension({ widget = input.widget; } - const { type } = getWidgetType(widget[GET_CONFIG]()); + const config = widget[GET_CONFIG]?.(); + if (!config) return; + + const { type } = getWidgetType(config); // Update our output to restrict to the widget type this.outputs[0].type = type; this.outputs[0].name = type; this.outputs[0].widget = widget; - this.#createWidget(widget[CONFIG] ?? widget[GET_CONFIG](), theirNode, widget.name, recreating); + this.#createWidget(widget[CONFIG] ?? config, theirNode, widget.name, recreating); } #createWidget(inputData, node, widgetName, recreating) { @@ -497,21 +543,7 @@ app.registerExtension({ const config2 = input.widget[GET_CONFIG](); if (config1[0] instanceof Array) { - // New input isnt a combo - if (!(config2[0] instanceof Array)) { - console.log(`connection rejected: tried to connect combo to ${config2[0]}`); - return false; - } - // New imput combo has a different size - if (config1[0].length !== config2[0].length) { - console.log(`connection rejected: combo lists dont match`); - return false; - } - // New input combo has different elements - if (config1[0].find((v, i) => config2[0][i] !== v)) { - console.log(`connection rejected: combo lists dont match`); - return false; - } + if (!isValidCombo(config1[0], config2[0])) return false; } else if (config1[0] !== config2[0]) { // Types dont match console.log(`connection rejected: types dont match`, config1[0], config2[0]);