From 97d00ea47d531bb876935b997a3ed749be8d5e83 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Tue, 9 Sep 2025 18:48:51 -0700 Subject: [PATCH] [test] Add component tests for Vue node slots (#5461) * add component tests for slots * use `for of` for better error report * add runtime type check to make assertions valid * add runtime type check to make assertions valid --- .../vueNodes/components/NodeSlots.spec.ts | 195 ++++++++++++++++++ vitest.config.ts | 12 +- 2 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 src/renderer/extensions/vueNodes/components/NodeSlots.spec.ts diff --git a/src/renderer/extensions/vueNodes/components/NodeSlots.spec.ts b/src/renderer/extensions/vueNodes/components/NodeSlots.spec.ts new file mode 100644 index 000000000..522a9e8a6 --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/NodeSlots.spec.ts @@ -0,0 +1,195 @@ +import { mount } from '@vue/test-utils' +import { createPinia } from 'pinia' +import { describe, expect, it } from 'vitest' +import { type PropType, defineComponent } from 'vue' +import { createI18n } from 'vue-i18n' + +import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' +import enMessages from '@/locales/en/main.json' + +import NodeSlots from './NodeSlots.vue' + +const makeNodeData = (overrides: Partial = {}): VueNodeData => ({ + id: '123', + title: 'Test Node', + type: 'TestType', + mode: 0, + selected: false, + executing: false, + inputs: [], + outputs: [], + widgets: [], + flags: { collapsed: false }, + ...overrides +}) + +// Explicit stubs to capture props for assertions +interface StubSlotData { + name?: string + type?: string + boundingRect?: [number, number, number, number] +} + +const InputSlotStub = defineComponent({ + name: 'InputSlot', + props: { + slotData: { type: Object as PropType, required: true }, + nodeId: { type: String, required: false, default: '' }, + index: { type: Number, required: true }, + readonly: { type: Boolean, required: false, default: false } + }, + template: ` +
+ ` +}) + +const OutputSlotStub = defineComponent({ + name: 'OutputSlot', + props: { + slotData: { type: Object as PropType, required: true }, + nodeId: { type: String, required: false, default: '' }, + index: { type: Number, required: true }, + readonly: { type: Boolean, required: false, default: false } + }, + template: ` +
+ ` +}) + +const mountSlots = (nodeData: VueNodeData, readonly = false) => { + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { en: enMessages } + }) + return mount(NodeSlots, { + global: { + plugins: [i18n, createPinia()], + stubs: { + InputSlot: InputSlotStub, + OutputSlot: OutputSlotStub + } + }, + props: { nodeData, readonly } + }) +} + +describe('NodeSlots.vue', () => { + it('filters out inputs with widget property and maps indexes correctly', () => { + // Two inputs without widgets (object and string) and one with widget (filtered) + const inputObjNoWidget = { + name: 'objNoWidget', + type: 'number', + boundingRect: [0, 0, 0, 0] + } + const inputObjWithWidget = { + name: 'objWithWidget', + type: 'number', + boundingRect: [0, 0, 0, 0], + widget: { name: 'objWithWidget' } + } + const inputs = [inputObjNoWidget, inputObjWithWidget, 'stringInput'] + + const wrapper = mountSlots(makeNodeData({ inputs })) + + const inputEls = wrapper + .findAll('.stub-input-slot') + .map((w) => w.element as HTMLElement) + // Should filter out the widget-backed input; expect 2 inputs rendered + expect(inputEls.length).toBe(2) + + // Verify expected tuple of {index, name, nodeId} + const info = inputEls.map((el) => ({ + index: Number(el.dataset.index), + name: el.dataset.name ?? '', + nodeId: el.dataset.nodeId ?? '', + type: el.dataset.type ?? '', + readonly: el.dataset.readonly === 'true' + })) + expect(info).toEqual([ + { + index: 0, + name: 'objNoWidget', + nodeId: '123', + type: 'number', + readonly: false + }, + // string input is converted to object with default type 'any' + { + index: 1, + name: 'stringInput', + nodeId: '123', + type: 'any', + readonly: false + } + ]) + + // Ensure widget-backed input was indeed filtered out + expect(wrapper.find('[data-name="objWithWidget"]').exists()).toBe(false) + }) + + it('maps outputs and passes correct indexes', () => { + const outputObj = { name: 'outA', type: 'any', boundingRect: [0, 0, 0, 0] } + const outputs = [outputObj, 'outB'] + + const wrapper = mountSlots(makeNodeData({ outputs })) + const outputEls = wrapper + .findAll('.stub-output-slot') + .map((w) => w.element as HTMLElement) + + expect(outputEls.length).toBe(2) + const outInfo = outputEls.map((el) => ({ + index: Number(el.dataset.index), + name: el.dataset.name ?? '', + nodeId: el.dataset.nodeId ?? '', + type: el.dataset.type ?? '', + readonly: el.dataset.readonly === 'true' + })) + expect(outInfo).toEqual([ + { index: 0, name: 'outA', nodeId: '123', type: 'any', readonly: false }, + // string output mapped to object with type 'any' + { index: 1, name: 'outB', nodeId: '123', type: 'any', readonly: false } + ]) + }) + + it('renders nothing when there are no inputs/outputs', () => { + const wrapper = mountSlots(makeNodeData({ inputs: [], outputs: [] })) + expect(wrapper.findAll('.stub-input-slot').length).toBe(0) + expect(wrapper.findAll('.stub-output-slot').length).toBe(0) + }) + + it('passes readonly to child slots', () => { + const wrapper = mountSlots( + makeNodeData({ inputs: ['a'], outputs: ['b'] }), + /* readonly */ true + ) + const all = [ + ...wrapper + .findAll('.stub-input-slot') + .filter((w) => w.element instanceof HTMLElement) + .map((w) => w.element as HTMLElement), + ...wrapper + .findAll('.stub-output-slot') + .filter((w) => w.element instanceof HTMLElement) + .map((w) => w.element as HTMLElement) + ] + expect(all.length).toBe(2) + for (const el of all) { + expect.soft(el.dataset.readonly).toBe('true') + } + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts index 765a2ec11..36fdb1a00 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -20,11 +20,19 @@ export default defineConfig({ retry: process.env.CI ? 2 : 0, include: [ 'tests-ui/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', - 'src/components/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}' + 'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}' ], coverage: { reporter: ['text', 'json', 'html'] - } + }, + exclude: [ + '**/node_modules/**', + '**/dist/**', + '**/cypress/**', + '**/.{idea,git,cache,output,temp}/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*', + 'src/lib/litegraph/test/**' + ] }, resolve: { alias: {