UNPKG

node-red-contrib-flow-asserter

Version:

This module is a node module that supports flow testing on the Node-RED editor UI. You can test the flow with multiple test-cases and without any changes when switching between test and production.

379 lines (367 loc) 20 kB
<script type="text/x-red" data-template-name="flow-asserter in"> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="flow-asserter-in.label.name"></span></label> <input type="text" id="node-input-name" data-i18n="[placeholder]flow-asserter-in.label.name"> </div> <div class="form-row node-input-testcase-row"> <label for="node-input-testcase" style="width: 520px;"><i class="fa fa-tasks"></i> <span data-i18n="flow-asserter-in.label.testcase"></label> <ol id="node-input-testcase"></ol> </div> <div class="form-row"> <label for="node-input-onlyfail" style="width: auto;"> <input type="checkbox" id="node-input-onlyfail" style="display:inline-block; width:22px; vertical-align:baseline;"><span data-i18n="flow-asserter-in.label.onlyfail"></span> </label> </div> <div class="form-row"> <label for="node-input-flowasserterout"><i class="fa fa-check-circle"></i> <span data-i18n="flow-asserter-in.label.flowasserterout"></label> <select id="node-input-flowasserterout"> <option></option> </select> </div> </script> <script type="text/x-red" data-template-name="flow-asserter out"> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="flow-asserter-out.label.name"></span></label> <input type="text" id="node-input-name" data-i18n="[placeholder]flow-asserter-out.label.name"> </div> </script> <script type="text/x-red" data-help-name="flow-asserter in"> <p>This module helps to test your flow. Testcases are written in flow-asserter in node and flow-asserter out node is inserted into flow at the point wanted to assert <code>msg.payload</code>. By clicking the button, test will start. Each testcase stores its input in <code>msg.payload</code> and sends a message from the upper output port. When the message reaches flow-asserter out node specified by flow-asserter in node, <code>msg.payload</code> is compared with testcase's output by the rule defined in testcase and result of that comparison is sent from the lower output port.</p> <h3>Outputs</h3> <h4>Upper output port</h4> <dl class="message-properties"> <dt>payload <span class="property-type">various</span></dt> <dd>Input of testcase is stored in this property.</dd> <dt class="optional">_testcase <span class="property-type">object</span></dt> <dd>Testcase information like input, output, way to compare and others is stored. Should not change this property in flow.</dd> </dl> <h4>Lower output port(<code>test result</code>)</h4> <dl class="message-properties"> <dt>payload <span class="property-type">object</span></dt> <dd>Testcase result is stored in this property.</dd> </dl> <h3>Details</h3> <p>To write testcase, you should push add button, enter input form and output form and select operator. TestID is automatically numbered. Testcases are run in the order of TestID. Thus after one testcase finishes, next testcase is run.</p> <h4>Notes</h4> <p><code>equal</code> operator uses <a href="https://github.com/epoberezkin/fast-deep-equal">fast-deep-equal</a> for deep equality.</p> <p><code>is null</code>, <code>is not null</code>, <code>is empty</code> and <code>is not empty</code> perform in the same way to switch node. Thus <code>is null</code> performs strict comparison against that type. It do not convert between types. <code>is empty</code> passes for Strings, Arrays and Buffers that have a length of 0, or Objects that have no properties. It does not pass for <code>null</code> or <code>undefined</code> values.</p> <p>A JSONata Expression operator can be provided that will be evaluated against the whole message and will match if the expression returns a true value.</p> <h3>References</h3> <ul> <li><a href="https://github.com/epoberezkin/fast-deep-equal">fast-deep-equal</a></li> </ul> </script> <script type="text/x-red" data-help-name="flow-asserter out"> <p>This module helps to test your flow. Testcases are written in flow-asserter in node and flow-asserter out node is inserted into flow at the point wanted to assert <code>msg.payload</code>. By clicking the button, test will start. Each testcase stores its input in <code>msg.payload</code> and sends a message from the upper output port. When the message reaches flow-asserter out node specified by flow-asserter in node, <code>msg.payload</code> is compared with testcase's output by the rule defined in testcase and result of that comparison is sent from the lower output port.</p> <h3>Inputs</h3> <dl class="message-properties"> <dt class="optional">payload <span class="property-type">various</span></dt> <dd>If testcase specify this node, this property is compared with output of testcase.</dd> <dt class="optional">_testcase <span class="property-type">object</span></dt> <dd>Testcase information is stored. It is used for determining which node asserts output, how to compare and others. Should not change this property in flow.</dd> </dl> <h3>Details</h3> <p>If testcase reaching this flow-asserter out node specifies this node, this node asserts <code>msg.payload</code> and does not send message from output port. However when test is not being run or testcase specifies other node, this node simply hand over the message to next node wired to output port of this node.</p> </script> <script type="text/javascript"> (function() { function setTestcases(node) { $('#node-input-testcase').css({ 'min-height': '400px' }).editableList({ addButton: true, sortable: true, addItem: function(row, index, data) { let testcase = data; let columnTestId = $('<div>', { width: '40px', css: { display: 'inline-grid', 'font-weight': 'bolder', 'font-size': 'large', }, text: index }).appendTo($(row)); let columnInput = $('<div>', { class: 'input-column', css: { display: 'inline-grid', 'min-width': '150px', 'vertical-align': 'bottom', }, }).append($('<input>', { class: 'testcase-input', value: testcase['input'], }) ).append($('<input>', { class: 'testcase-inputType', id: 'testcase-inputType' + index, type: 'hidden', value: testcase['inputType'], }) ).appendTo($(row)); const fixedValueType = { value: 'fixed', label: 'fixed value', icon: 'fixed', options: ['null', 'undefined'] }; $('input.testcase-input').typedInput({ default: 'str', types: ['str', 'num', 'bool', 'json', 'bin', fixedValueType], typeField: '#testcase-inputType' + index }); let selectElemAssert = $('<select>', { class: 'operator-select', width: '80px', css: {'grid-row': '1', 'grid-column': '1'} }).append($('<option>', { value: 'eq', text: '==', selected: testcase['operator'] == 'eq', })).append($('<option>', { value: 'neq', text: '!=', selected: testcase['operator'] == 'neq', })).append($('<option>', { value: 'lt', text: '<', selected: testcase['operator'] == 'lt', })).append($('<option>', { value: 'lte', text: '<=', selected: testcase['operator'] == 'lte', })).append($('<option>', { value: 'gt', text: '>', selected: testcase['operator'] == 'gt', })).append($('<option>', { value: 'gte', text: '>=', selected: testcase['operator'] == 'gte', })).append($('<option>', { value: 'equal', text: 'equal', selected: testcase['operator'] == 'equal', })).append($('<option>', { value: 'cont', text: 'contains', selected: testcase['operator'] == 'cont', })).append($('<option>', { value: 'regex', text: 'matches regex', selected: testcase['operator'] == 'regex', })).append($('<option>', { value: 'null', text: 'is null', selected: testcase['operator'] == 'null', })).append($('<option>', { value: 'nnull', text: 'is not null', selected: testcase['operator'] == 'nnull', })).append($('<option>', { value: 'empty', text: 'is empty', selected: testcase['operator'] == 'empty', })).append($('<option>', { value: 'nempty', text: 'is not empty', selected: testcase['operator'] == 'nempty', })).append($('<option>', { value: 'istype', text: 'is of type', selected: testcase['operator'] == 'istype', })).append($('<option>', { value: 'jsonata', text: 'JSONata exp', selected: testcase['operator'] == 'jsonata', }) ); let columnAssert = $('<div>', { class: 'assert-column', css: { display: 'inline-grid', 'min-width': '230px', 'grid-template-rows': '1fr', 'grid-template-columns': '85px 1fr' }, }).append( selectElemAssert ).append($('<input>', { class: 'testcase-assert', id: 'testcase-assert' + index, value: testcase['assert'], css: {'grid-row': '1', 'grid-column': '2'} }) ).append($('<input>', { class: 'testcase-assertType', id: 'testcase-assertType' + index, type: 'hidden', value: testcase['assertType'], }) ).appendTo($(row)); $('input.testcase-assert').typedInput({ default: 'str', types: ['str', 'num', 'bool', 'json', 'bin'], typeField: '#testcase-assertType' + index }); selectElemAssert.change(function () { const onlySelectOperators = ['null', 'nnull', 'empty', 'nempty']; const selectedOperator = $(this).val(); let widthStr; let gridTemplateColumnsStr; let typeInputStr = 'show'; if (onlySelectOperators.includes(selectedOperator)) { widthStr = '100px'; gridTemplateColumnsStr = '120px 1fr'; typeInputStr = 'hide' } else if (selectedOperator == 'jsonata') { $('#testcase-assert' + index).typedInput('types', ['jsonata']); widthStr = '90px'; gridTemplateColumnsStr = '110px 1fr'; } else if (selectedOperator == 'istype') { $('#testcase-assert' + index).typedInput('types', [{ value: 'type', options: ['string', 'number', 'boolean', 'array', 'buffer', 'object', 'JSON string', 'undefined', 'null'] }]); widthStr = '90px'; gridTemplateColumnsStr = '110px 1fr'; } else if (selectedOperator == 'regex') { $('#testcase-assert' + index).typedInput('types', ['re']); widthStr = '90px'; gridTemplateColumnsStr = '110px 1fr'; } else { $('#testcase-assert' + index).typedInput('types', ['str', 'num', 'bool', 'json', 'bin']); widthStr = '66px'; gridTemplateColumnsStr = '85px 1fr'; } $(this).width(widthStr); $(this).parent().css({'grid-template-columns': gridTemplateColumnsStr}); $('#testcase-assert' + index).typedInput(typeInputStr); }); selectElemAssert.change(); }, header: $('<div>').append($.parseHTML('<div style="display: inline-grid; width: 60px">&ensp; TestID</div><div id="input-column-name" style="display: inline-grid; min-width: 150px">Input</div><div id="assert-column-name" style="display: inline-grid;">Assert</div>')), removable: true, }); } function onEditSave(node) { node.testcases = []; let testcases = $('#node-input-testcase').editableList('items'); testcases.each(function(index) { let tcInput = $(this).find('input.testcase-input').val(); let tcInputType = $(this).find('input.testcase-inputType').val(); let tcOperator = $(this).find('.operator-select>option:selected').val(); let tcAssert = $(this).find('input.testcase-assert').val(); let tcAssertType = $(this).find('input.testcase-assertType').val(); let tc = {'testId': index, 'input': tcInput, 'inputType': tcInputType, 'operator': tcOperator, 'assert': tcAssert, 'assertType': tcAssertType}; node.testcases.push(tc); }); } function onEditPrepare(node) { for (let i in node.testcases) { $('#node-input-testcase').editableList('addItem', node.testcases[i]); } let candidateNodes = RED.nodes.filterNodes({type:'flow-asserter out'}); let selectedNode = node.flowasserterout; for (let i in candidateNodes) { let outNode = candidateNodes[i].id; $('#node-input-flowasserterout').append($('<option>', { value: outNode, text: outNode, selected: outNode == selectedNode, }) ); } } function resizeNodeList(size) { let addWidth = (size.width - 520) / 2; addWidth = addWidth < 0 ? 0 : addWidth; let inputColumnNameWidth = (150 + addWidth) + 'px'; $('#input-column-name').css({ width: inputColumnNameWidth }); let inputFormWidth = (150 + addWidth) + 'px'; $('.input-column').css({ width: inputFormWidth }); let typedInputFormWidth = (140 + addWidth) + 'px'; $('.input-column>.red-ui-typedInput-container').css({ width: typedInputFormWidth }); let assertColumnNameWidth = (230 + addWidth) + 'px'; $('#assert-column-name').css({ width: assertColumnNameWidth }); let assertFormWidth = (230 + addWidth) + 'px'; $('.assert-column').css({ width: assertFormWidth }); let typedAssertFormWidth = (130 + addWidth) + 'px'; $('.assert-column>.red-ui-typedInput-container').css({ width: typedAssertFormWidth }); } RED.nodes.registerType('flow-asserter in',{ category: 'test', color: '#65b754', defaults: { name: {value: ''}, testcases: {value: []}, onlyfail: {value: false}, flowasserterout: {value: '', required: true}, }, inputs: 0, outputs: 2, icon: 'debug.png', outputLabels: ['', 'test result'], label: function() { return this.name || 'flow-asserter in'; }, oneditprepare: function() { setTestcases(this); onEditPrepare(this); }, oneditsave: function() { onEditSave(this); }, oneditresize: resizeNodeList, button: { enabled: function() { return this.testcases !== undefined && this.testcases.length > 0; }, onclick: function() { var label = this._def.label.call(this); if (label.length > 30) { label = label.substring(0, 50) + '...'; } label = label.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); var node = this; $.ajax({ url: 'flow-asserter-in/' + this.id, type: 'POST', success: function(resp) { RED.notify( node._('flow-asserter-in.notification.run', {label: label}), {type: 'success', id: 'flow-asserter in'} ); }, error: function(jqXHR,textStatus,errorThrown) { if (jqXHR.status == 404) { RED.notify(node._('flow-asserter-in.notification.error',{message:node._('flow-asserter-in.errors.not-deployed')}), 'error'); } else if (jqXHR.status == 500) { RED.notify(node._('flow-asserter-in.notification.error',{message:node._('flow-asserter-in.errors.failed')}), 'error'); } else if (jqXHR.status == 0) { RED.notify(node._('flow-asserter-in.notification.error',{message:node._('flow-asserter-in.errors.no-response')}), 'error'); } else { RED.notify(node._('flow-asserter-in.notification.error',{message:node._('flow-asserter-in.errors.unexpected',{status:jqXHR.status, message:textStatus})}), 'error'); } } }); } } }); RED.nodes.registerType('flow-asserter out',{ category: 'test', color: '#65b754', defaults: { name: {value: ''}, }, align: 'right', inputs: 1, outputs: 1, icon: 'debug.png', label: function() { return this.name || 'flow-asserter out'; } }); })(); </script>