diff --git a/__tests__/flowRunner.test.js b/__tests__/flowRunner.test.js index fe016aa..5f03ce8 100644 --- a/__tests__/flowRunner.test.js +++ b/__tests__/flowRunner.test.js @@ -156,6 +156,21 @@ describe('FlowRunner', () => { global.clearTimeout = originalGlobalClearTimeout; }); + describe('graceful degradation (unknown step types)', () => { + test('unknown step type is skipped with a warning, not thrown or stopped', async () => { + const step = { id: 'u1', name: 'Future Step', type: 'future_unknown_type' }; + await runner._executeSingleStepLogic(step, {}); + const last = runner.state.results[runner.state.results.length - 1]; + expect(last.status).toBe('skipped'); + expect(last.unsupported).toBe(true); + // the flow is NOT halted + expect(runner.state.stopRequested).toBe(false); + expect(mockOnError).not.toHaveBeenCalled(); + // a warning was surfaced to the user + expect(mockOnMessage).toHaveBeenCalled(); + }); + }); + describe('state management', () => { it('should initialize with isRunning and isStepping false', () => { expect(runner.isRunning()).toBe(false); diff --git a/__tests__/flowVisualizer.test.js b/__tests__/flowVisualizer.test.js index 49a93c7..3a831d6 100644 --- a/__tests__/flowVisualizer.test.js +++ b/__tests__/flowVisualizer.test.js @@ -174,9 +174,17 @@ describe('FlowVisualizer (Drawflow)', () => { await waitForVisualizerRender(); const drawflowId = visualizer.nodeIdByStepId.get('step1'); + // A real Drawflow drag updates BOTH the data model (pos_x/pos_y) AND the + // node element's style.left/top in lock-step (see drawflow position()). + // The nodeMoved handler treats style.left/top as authoritative, so the + // test must move the DOM element too — mutating the data model alone is + // not how the interaction reaches the handler. const nodeData = visualizer.editor.drawflow.drawflow.Home.data[drawflowId]; nodeData.pos_x = 300; nodeData.pos_y = 400; + const nodeEl = container.querySelector(`#node-${drawflowId}`); + nodeEl.style.left = '300px'; + nodeEl.style.top = '400px'; visualizer.editor.dispatch('nodeMoved', drawflowId); expect(mockCallbacks.onNodeLayoutUpdate).toHaveBeenCalledWith('step1', 300, 400); @@ -231,15 +239,23 @@ describe('FlowVisualizer (Drawflow)', () => { mockCallbacks.onNodeLayoutUpdate = (id, x, y) => { handleVisualizerNodeLayoutUpdate(id, x, y); }; + // Replace the beforeEach visualizer: destroy the old one first so its + // modal (appended to document.body) does not leak into later tests. + visualizer.destroy(); visualizer = new FlowVisualizer(container, mockCallbacks); visualizer.render(mockFlow, null); await waitForVisualizerRender(); const drawflowId = visualizer.nodeIdByStepId.get('step1'); + // Mirror a real Drawflow drag: move both the data model and the DOM + // element, which the nodeMoved handler reads as authoritative. const nodeData = visualizer.editor.drawflow.drawflow.Home.data[drawflowId]; nodeData.pos_x = 500; nodeData.pos_y = 600; + const nodeEl = container.querySelector(`#node-${drawflowId}`); + nodeEl.style.left = '500px'; + nodeEl.style.top = '600px'; visualizer.editor.dispatch('nodeMoved', drawflowId); @@ -255,7 +271,10 @@ describe('FlowVisualizer (Drawflow)', () => { const dblClickEvent = new MouseEvent('dblclick', { bubbles: true }); node.dispatchEvent(dblClickEvent); - const modal = document.querySelector('.node-editor-modal'); + // Query THIS visualizer's modal. Each FlowVisualizer appends its own + // .node-editor-modal to document.body, so a bare document.querySelector + // can return a different visualizer's modal. + const modal = visualizer.nodeEditorModal; const modalTitle = modal.querySelector('.node-editor-title'); expect(modal.style.display).toBe('flex'); expect(modalTitle.textContent).toContain('Step 1'); @@ -268,7 +287,9 @@ describe('FlowVisualizer (Drawflow)', () => { const node = container.querySelector('.drawflow-node.flow-node[data-step-id="step1"]'); node.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })); - const addButton = document.querySelector('.node-editor-add'); + // Use THIS visualizer's add button; a bare document.querySelector can + // return the button of another visualizer's modal (see modal note above). + const addButton = visualizer.nodeEditorModal.querySelector('.node-editor-add'); addButton.click(); expect(mockCallbacks.onRequestAddStepAfter).toHaveBeenCalledWith( diff --git a/__tests__/transformOps.test.js b/__tests__/transformOps.test.js index d42cc45..6e62d71 100644 --- a/__tests__/transformOps.test.js +++ b/__tests__/transformOps.test.js @@ -36,4 +36,21 @@ describe('transformOps', () => { expect(context.decoded.payload.exp).toBe(123); expect(context.decoded.header.alg).toBe('none'); }); + + test('unknown transform op is skipped with a warning (not downgraded to base64_decode)', async () => { + const context = {}; + const ops = [ + // "SGVsbG8" base64url-decodes to "Hello"; the old bug set decoded = "Hello". + { op: 'totally_unknown_future_op', set: 'decoded', args: ['SGVsbG8'], options: {} }, + { op: 'base64_encode', set: 'enc', args: ['hi'], options: { base64: 'url' } }, + ]; + const output = await executeTransformOps(ops, context, { evaluatePath: simpleEvaluatePath }); + expect(context.decoded).toBeUndefined(); // unknown op skipped, NOT base64-decoded + expect(context.enc).toBeDefined(); // subsequent known op still ran + expect(output.updatedVars).toEqual(['enc']); + expect(output.warnings).toHaveLength(1); + expect(output.warnings[0].op).toBe('totally_unknown_future_op'); + expect(output.warnings[0].set).toBe('decoded'); + expect(output.warnings[0].status).toBe('skipped'); + }); }); diff --git a/assets/vendor/drawflow/package.json b/assets/vendor/drawflow/package.json new file mode 100644 index 0000000..c409e42 --- /dev/null +++ b/assets/vendor/drawflow/package.json @@ -0,0 +1,4 @@ +{ + "type": "commonjs", + "//": "Vendored UMD build of Drawflow. The app's root package.json is \"type\": \"module\", which would make Node treat this .js as ESM and break the Jest setup's require() of it. This marker keeps only this vendored dir CommonJS so it stays require-able in Node/Jest. The browser loads drawflow.min.js via a plain