Compare commits

..

23 Commits

Author SHA1 Message Date
Nick O'Leary
c9e54f2ba9 Bump for 1.1.0-beta.3 2020-06-17 10:54:15 +01:00
Nick O'Leary
e2c86c4b96 Fix wiring nodes from input back to output 2020-06-17 10:52:41 +01:00
Nick O'Leary
4469a334fd Fix sometimes unable to keyboard-move group to left/up 2020-06-17 09:57:25 +01:00
Nick O'Leary
aca379db6e Fix group position in outliner 2020-06-16 20:48:28 +01:00
Nick O'Leary
9ce5210c33 Handle unknown nodes with no icon 2020-06-16 20:34:45 +01:00
Nick O'Leary
4dd68452b4 Prevent node creep when switching tabs 2020-06-16 20:23:18 +01:00
Nick O'Leary
714b3d3fe0 Bump version to 1.1.0-beta.2 2020-06-16 15:21:03 +01:00
Nick O'Leary
2378e0d961 Fix up linting in search.js 2020-06-16 15:08:30 +01:00
Nick O'Leary
f78bbdc29f Update CHANGELOG for 1.1.0-beta.2 2020-06-16 15:03:56 +01:00
Nick O'Leary
9f0490fc12 Merge pull request #2619 from kazuhitoyokoi/dev-fixuitest3
Fix page object of inject node
2020-06-16 14:38:15 +01:00
Nick O'Leary
d37eebd8ed Merge pull request #2618 from kazuhitoyokoi/dev-addjpntranslations
Fix i18n bug in outliner
2020-06-16 14:22:55 +01:00
Kazuhito Yokoi
bfeda23ce5 Fix page object of inject node 2020-06-16 21:58:08 +09:00
Kazuhito Yokoi
52eb158231 Add Japanese translations for outliner, jsonata and runtime 2020-06-16 21:32:10 +09:00
Nick O'Leary
afb782410d Merge pull request #2617 from kazuhitoyokoi/dev-fixuitest2
Fix page object of debug node
2020-06-16 10:57:16 +01:00
Nick O'Leary
aebb7da3c7 Fix deleting node in group after changing selection 2020-06-16 10:54:50 +01:00
Kazuhito Yokoi
b90710945a Fix page object of debug node 2020-06-16 11:45:27 +09:00
Nick O'Leary
56efd51c06 Fixup padding of quick-add search box 2020-06-15 22:31:47 +01:00
Nick O'Leary
76728d1783 Move config nodes under type-level hierarchy in outline
Also adds user-count label and button to open search
2020-06-15 22:31:47 +01:00
Nick O'Leary
5b1fe9aa0a Emit nodes:change event for config node users list modified 2020-06-15 22:31:47 +01:00
Nick O'Leary
e3c8466819 Merge pull request #2616 from kazuhitoyokoi/dev-fixuitest
Fix page object of inject node
2020-06-15 22:27:30 +01:00
Kazuhito Yokoi
6a70cd1975 Fix page object of inject node 2020-06-15 20:36:41 +09:00
Nick O'Leary
2c45771024 Merge pull request #2593 from kazuhitoyokoi/master-adduitest4travis
Enable automated UI testing on Travis CI
2020-06-15 11:14:14 +01:00
Kazuhito Yokoi
e44d89c2af Enable automated UI testing on Travis CI 2020-06-03 17:22:25 +09:00
22 changed files with 184 additions and 58 deletions

View File

@@ -1,4 +1,6 @@
sudo: false
addons:
chrome: stable
language: node_js
matrix:
include:
@@ -7,6 +9,7 @@ matrix:
- node_js: "10"
script:
- ./node_modules/.bin/grunt && istanbul report text && ( cat coverage/lcov.info | $(npm get prefix)/bin/coveralls || true ) && rm -rf coverage
- scripts/install-ui-test-dependencies.sh && grunt test-ui
before_script:
- npm install -g istanbul coveralls
- node_js: "8"

View File

@@ -1,3 +1,73 @@
#### 1.1.0-beta.3: Beta Release
Editor
- Fix wiring nodes from input back to output
- Fix sometimes unable to keyboard-move group to left/up
- Fix group position in outliner
- Handle unknown nodes with no icon
- Prevent node creep when switching tabs
#### 1.1.0-beta.2: Beta Release
Editor
- Add UI tests to travis build #2593 #2616 #2617 #2619 (@kazuhitoyokoi)
- Add Japanese translations for outliner, jsonata and runtime #2618 (@kazuhitoyokoi)
- Fix deleting node in group after changing selection
- Fixup padding of quick-add search box
- Move config nodes under type-level hierarchy in outline
- Emit nodes:change event for config node users list modified
- Increase group margin to avoid clash with status text
- Fix event order when quick-adding node to group
- Switch RED.events.DEBUG messages to warn to get stacktraces
- Fix empty item handling for subflows/config in outliner
- Fix search indexing of group nodes
- Avoid regenerating every node label on redraw
- Fix handling of multi-line node label
- Disable merge group menu for single item or non-group item #2611 (@HiroyasuNishiyama)
- Merge pull request #2609 from node-red-hitachi/fix-remove-from-group
- Fix position of empty group with multi-line label #2612 (@HiroyasuNishiyama)
- Make treelist of subflow/config nodes initially have empty placeholder
- Fix empty placeholder not shown on remove from group #2609 (@HiroyasuNishiyama)
- Prevent conversion of circular structure #2607 (@HiroyasuNishiyama)
- Handle null status text in the editor Fixes #2606
- Massively reduce our dependency on d3 to render the view
- EditableList/TreeList - defer adding elements to DOM
- Prevent RED.stop being called multiple times if >1 signal received
- Flag a node as removed when it is disabled
- Some performance improvements for TreeList
- Resize info/help sidebars whenever sidebar is opened
- Add search defaults to outliner searchBox
- Add search presets option to searchBox widget
- Add RED.popover.menu as a new type of menu widget
- Add support for is:XYZ search flags
- Track subflow instances on the subflow node itself
- Refresh outline filter whenever something changes Fixes #2601
- Fix Help tab search box appearance
- Rename Node Information to Information in sidebar
- Do a sync-redraw after clearing to ensure clean state
- Make catch/status/complete/link filter case-insensitive
- Add 'add' option to touch radialMenu for quick-add dialog
- Merge branch 'dev' of https://github.com/node-red/node-red into dev
- ensure trigger node detects changes to number of outputs
- Ignore whitespace when checking function setup/close code
- Preserve event handlers when moving outliner items
- Add tooltips to outliner buttons
- Only validate nodes once they have all been imported
- Ensure configNode.users is updated properly on import
Runtime
- Bump node-red-admin 0.2.6
Nodes
- WebSocket: Prevent charAt call on websocket listener #2610 ()
- Debug: fix status to migrate old nodes to correct default mode.
- Link: Fix Link node filter Fixes #2600
#### 1.1.0-beta.1: Beta Release
Runtime

View File

@@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "1.1.0-beta.1",
"version": "1.1.0-beta.3",
"description": "Low-code programming for event-driven applications",
"homepage": "http://nodered.org",
"license": "Apache-2.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/editor-api",
"version": "1.1.0-beta.1",
"version": "1.1.0-beta.3",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@@ -16,8 +16,8 @@
}
],
"dependencies": {
"@node-red/util": "1.1.0-beta.1",
"@node-red/editor-client": "1.1.0-beta.1",
"@node-red/util": "1.1.0-beta.3",
"@node-red/editor-client": "1.1.0-beta.3",
"bcryptjs": "2.4.3",
"body-parser": "1.19.0",
"clone": "2.1.2",

View File

@@ -20,7 +20,9 @@
"fill": "塗りつぶし",
"label": "ラベル",
"color": "色",
"position": "配置"
"position": "配置",
"enable": "有効",
"disable": "無効"
},
"type": {
"string": "文字列",
@@ -595,7 +597,16 @@
"showTips": "設定からヒントを表示できます",
"outline": "アウトライン",
"empty": "空",
"globalConfig": "グローバル設定ノード"
"globalConfig": "グローバル設定ノード",
"triggerAction": "アクションを実行",
"find": "ワークスペース内を検索",
"search": {
"configNodes": "設定ノード",
"unusedConfigNodes": "未使用の設定ノード",
"invalidNodes": "不正なノード",
"uknownNodes": "未知のノード",
"unusedSubflows": "未使用のサブフロー"
}
},
"help": {
"name": "ヘルプ",

View File

@@ -266,5 +266,9 @@
"$type": {
"args": "value",
"desc": "`value` の型を文字列として返します。もし `value` が未定義の場合、 `undefined` が返されます。"
},
"$moment": {
"args": "[str]",
"desc": "Momentライブラリを使用して日付オブジェクトを取得します。"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/editor-client",
"version": "1.1.0-beta.1",
"version": "1.1.0-beta.3",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@@ -83,7 +83,7 @@ RED.search = (function() {
// flagName:XYZ
var regEx = new RegExp("(?:^| )"+flagName+":([^ ]+)(?: |$)");
var m
while(m = regEx.exec(val)) {
while(!!(m = regEx.exec(val))) {
val = val.replace(regEx," ").trim();
flags[flagName] = flags[flagName] || [];
flags[flagName].push(m[1]);
@@ -93,7 +93,7 @@ RED.search = (function() {
function search(val) {
var results = [];
var keys = Object.keys(index);
var keys = [];
var typeFilter;
var m = /(?:^| )type:([^ ]+)/.exec(val);
if (m) {
@@ -118,6 +118,11 @@ RED.search = (function() {
var j;
var list = [];
var nodes = {};
if (flags.uses) {
keys = flags.uses;
} else {
keys = Object.keys(index);
}
for (i=0;i<keys.length;i++) {
var key = keys[i];
var kpos = keys[i].indexOf(val);
@@ -126,6 +131,9 @@ RED.search = (function() {
for (j=0;j<ids.length;j++) {
var node = index[key][ids[j]];
var isConfigNode = node.node._def.category === "config" && node.node.type !== 'group';
if (flags.uses && key === node.node.id) {
continue;
}
if (flags.hasOwnProperty("invalid")) {
var nodeIsValid = !node.node.hasOwnProperty("valid") || node.node.valid;
if (flags.invalid === nodeIsValid) {

View File

@@ -30,6 +30,7 @@ RED.sidebar.info.outliner = (function() {
},
{
id: "__global__",
flow: "__global__",
label: RED._("sidebar.info.globalConfig"),
types: {},
children: [
@@ -202,7 +203,7 @@ RED.sidebar.info.outliner = (function() {
}
});
RED.popover.tooltip(toggleButton,function() {
return RED._("common.label."+((n.type==='tab' && n.disabled) || (n.type!=='tab' && n.d))?"enable":"disable")
return RED._("common.label."+(((n.type==='tab' && n.disabled) || (n.type!=='tab' && n.d))?"enable":"disable"));
});
} else {
$('<div class="red-ui-info-outline-item-control-spacer">').appendTo(controls)
@@ -408,7 +409,7 @@ RED.sidebar.info.outliner = (function() {
} else {
existingObject.element.find(".red-ui-info-outline-item-label").html("&nbsp;");
}
if (parent !== existingObject.parent.id) {
if (parent !== existingObject.parent.parent.flow) {
var parentItem = existingObject.parent;
existingObject.treeList.remove(true);
if (parentItem.children.length === 0) {
@@ -432,9 +433,18 @@ RED.sidebar.info.outliner = (function() {
parentItem.treeList.addChild(getEmptyItem(parentItem.id));
}
}
// This must be a config node that has been rescoped
createFlowConfigNode(parent,n.type);
configNodeTypes[parent].types[n.type].treeList.addChild(objects[n.id]);
if (n._def.category === 'config' && n.type !== 'group') {
// This must be a config node that has been rescoped
createFlowConfigNode(parent,n.type);
configNodeTypes[parent].types[n.type].treeList.addChild(objects[n.id]);
} else {
// This is a node that has moved groups
if (empties[parent]) {
empties[parent].treeList.remove();
delete empties[parent];
}
objects[parent].treeList.addChild(existingObject)
}
// if (parent === "__global__") {
// // Global always exists here
@@ -512,6 +522,7 @@ RED.sidebar.info.outliner = (function() {
// There is no 'config nodes' item in the parent flow
configNodeTypes[parent] = {
config: true,
flow: parent,
types: {},
label: RED._("menu.label.displayConfig"),
children: []

View File

@@ -102,11 +102,13 @@ RED.view.tools = (function() {
node.n.dirty = true;
if (node.n.type === "group") {
RED.group.markDirty(node.n);
minX = Math.min(node.n.x - 5,minX);
minY = Math.min(node.n.y - 5,minY);
} else {
minX = Math.min(node.n.x-node.n.w/2-5,minX);
minY = Math.min(node.n.y-node.n.h/2-5,minY);
}
minX = Math.min(node.n.x-node.n.w/2-5,minX);
minY = Math.min(node.n.y-node.n.h/2-5,minY);
}
if (minX !== 0 || minY !== 0) {
for (var n = 0; n<moving_set.length; n++) {
node = moving_set[n];

View File

@@ -2280,7 +2280,7 @@ RED.view = (function() {
}
function portMouseDown(d,portType,portIndex, evt) {
if (RED.view.DEBUG) { console.warn("portMouseDown", mouse_mode,d); }
if (RED.view.DEBUG) { console.warn("portMouseDown", mouse_mode,d,portType,portIndex); }
evt = evt || d3.event;
if (evt === 1) {
return;
@@ -2306,7 +2306,7 @@ RED.view = (function() {
}
function portMouseUp(d,portType,portIndex,evt) {
if (RED.view.DEBUG) { console.warn("portMouseUp", mouse_mode,d); }
if (RED.view.DEBUG) { console.warn("portMouseUp", mouse_mode,d,portType,portIndex); }
evt = evt || d3.event;
if (mouse_mode === RED.state.SELECTING_NODE) {
evt.stopPropagation();
@@ -2712,6 +2712,12 @@ RED.view = (function() {
} else if (d.type === 'link out') {
direction = 0;
}
} else {
if (drag_lines[0].portType === 1) {
direction = PORT_TYPE_OUTPUT;
} else {
direction = PORT_TYPE_INPUT;
}
}
}
}
@@ -2824,7 +2830,7 @@ RED.view = (function() {
if (!d3.event.ctrlKey && !d3.event.metaKey) {
// Ctrl not pressed so clear selection
deselectGroup(nodeGroup);
selectGroup(nodeGroup,false);
selectGroup(nodeGroup,false,false);
}
// Select this node
mousedown_node.selected = true;
@@ -3025,7 +3031,7 @@ RED.view = (function() {
}
}
function portMouseDownProxy(e) { portMouseDown(this.__data__,this.__portType__,this.__portIndex__, e); }
function portMouseDownProxy(e) { portMouseDown(this.__data__,this.__portType__,this.__portIndex__, e); }
function portTouchStartProxy(e) { portMouseDown(this.__data__,this.__portType__,this.__portIndex__, e); e.preventDefault() }
function portMouseUpProxy(e) { portMouseUp(this.__data__,this.__portType__,this.__portIndex__, e); }
function portTouchEndProxy(e) { portMouseUp(this.__data__,this.__portType__,this.__portIndex__, e); e.preventDefault() }
@@ -3154,12 +3160,14 @@ RED.view = (function() {
d3.event.stopPropagation();
}
function selectGroup(g, includeNodes) {
function selectGroup(g, includeNodes, addToMovingSet) {
if (!g.selected) {
g.selected = true;
g.dirty = true;
}
moving_set.push({n:g});
if (addToMovingSet !== false) {
moving_set.push({n:g});
}
if (includeNodes) {
var currentSet = new Set(moving_set.map(function(n) { return n.n }));
var allNodes = RED.group.getNodes(g,true);
@@ -3526,7 +3534,6 @@ RED.view = (function() {
var isLink = (d.type === "link in" || d.type === "link out")
var hideLabel = d.hasOwnProperty('l')?!d.l : isLink;
node.attr("id",d.id);
d.w = node_height;
d.h = node_height;
d.resize = true;
@@ -3720,8 +3727,9 @@ RED.view = (function() {
} else {
d.w = Math.max(node_width,20*(Math.ceil((labelParts.width+50+(d._def.inputs>0?7:0))/20)) );
}
// d.w = Math.max(node_width,20*(Math.ceil((calculateTextWidth(l, "red-ui-flow-node-label", 50)+(d._def.inputs>0?7:0))/20)) );
d.x += (d.w-ow)/2;
if (ow !== undefined) {
d.x += (d.w-ow)/2;
}
d.resize = false;
}
@@ -3771,9 +3779,10 @@ RED.view = (function() {
var yp = d.h / 2 - (this.__labelLineCount__ / 2) * 24 + 13;
if ((!d._def.align && d.inputs !== 0 && d.outputs === 0) || "right" === d._def.align) {
this.__iconGroup__.classList.add("red-ui-flow-node-icon-group-right");
this.__iconGroup__.setAttribute("transform", "translate("+(d.w-30)+",0)");
if (this.__iconGroup__) {
this.__iconGroup__.classList.add("red-ui-flow-node-icon-group-right");
this.__iconGroup__.setAttribute("transform", "translate("+(d.w-30)+",0)");
}
this.__textGroup__.classList.add("red-ui-flow-node-label-right");
this.__textGroup__.setAttribute("transform", "translate("+(d.w-38)+","+yp+")");
} else {
@@ -3849,7 +3858,7 @@ RED.view = (function() {
portPort.addEventListener("mousedown", portMouseDownProxy);
portPort.addEventListener("touchstart", portTouchStartProxy);
portPort.addEventListener("mouseup", portMouseUpProxy);
portPort.addEventListener("touchstart", portTouchEndProxy);
portPort.addEventListener("touchend", portTouchEndProxy);
portPort.addEventListener("mouseover", portMouseOverProxy);
portPort.addEventListener("mouseout", portMouseOutProxy);

View File

@@ -29,6 +29,10 @@
.red-ui-searchBox-container {
display: inline-block;
margin-right: 6px;
width: 100%;
}
&:not(.red-ui-type-search) .red-ui-searchBox-container {
width: calc(100% - 30px);
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/nodes",
"version": "1.1.0-beta.1",
"version": "1.1.0-beta.3",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/registry",
"version": "1.1.0-beta.1",
"version": "1.1.0-beta.3",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@@ -16,7 +16,7 @@
}
],
"dependencies": {
"@node-red/util": "1.1.0-beta.1",
"@node-red/util": "1.1.0-beta.3",
"semver": "6.3.0",
"uglify-js": "3.9.4",
"when": "3.7.8"

View File

@@ -31,6 +31,8 @@
"install-failed": "インストールに失敗しました",
"install-failed-long": "モジュール __name__ のインストールに失敗しました:",
"install-failed-not-found": "$t(server.install.install-failed-long) モジュールが見つかりません",
"install-failed-name": "$t(server.install.install-failed-long) 不正なモジュール名: __name__",
"install-failed-url": "$t(server.install.install-failed-long) 不正なURL: __url__",
"upgrading": "モジュール __name__ をバージョン __version__ に更新します",
"upgraded": "モジュール __name__ を更新しました。新しいバージョンを使うには、Node-REDを再起動してください。",
"upgrade-failed-not-found": "$t(server.install.install-failed-long) バージョンが見つかりません",

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/runtime",
"version": "1.1.0-beta.1",
"version": "1.1.0-beta.3",
"license": "Apache-2.0",
"main": "./lib/index.js",
"repository": {
@@ -16,8 +16,8 @@
}
],
"dependencies": {
"@node-red/registry": "1.1.0-beta.1",
"@node-red/util": "1.1.0-beta.1",
"@node-red/registry": "1.1.0-beta.3",
"@node-red/util": "1.1.0-beta.3",
"clone": "2.1.2",
"express": "4.17.1",
"fs-extra": "8.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@node-red/util",
"version": "1.1.0-beta.1",
"version": "1.1.0-beta.3",
"license": "Apache-2.0",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "node-red",
"version": "1.1.0-beta.1",
"version": "1.1.0-beta.3",
"description": "Low-code programming for event-driven applications",
"homepage": "http://nodered.org",
"license": "Apache-2.0",
@@ -31,10 +31,10 @@
"flow"
],
"dependencies": {
"@node-red/editor-api": "1.1.0-beta.1",
"@node-red/runtime": "1.1.0-beta.1",
"@node-red/util": "1.1.0-beta.1",
"@node-red/nodes": "1.1.0-beta.1",
"@node-red/editor-api": "1.1.0-beta.3",
"@node-red/runtime": "1.1.0-beta.3",
"@node-red/util": "1.1.0-beta.3",
"@node-red/nodes": "1.1.0-beta.3",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"express": "4.17.1",

View File

@@ -4,6 +4,6 @@ npm install --no-save \
wdio-mocha-framework@^0.6.4 \
wdio-spec-reporter@^0.1.5 \
webdriverio@^4.14.4 \
chromedriver@^79.0.0 \
chromedriver \
wdio-browserstack-service@^0.1.19 \
browserstack-local@^1.4.4

View File

@@ -25,15 +25,17 @@ function injectNode(id) {
util.inherits(injectNode, nodePage);
var payloadTypeList = {
"flow": 1,
"global": 2,
"str": 3,
"num": 4,
"bool": 5,
"json": 6,
"bin": 7,
"date": 8,
"env": 9,
"msg": 1,
"flow": 2,
"global": 3,
"str": 4,
"num": 5,
"bool": 6,
"json": 7,
"bin": 8,
"date": 9,
"jsonata": 10,
"env": 11,
};
var repeatTypeList = {
@@ -45,18 +47,18 @@ var repeatTypeList = {
injectNode.prototype.setPayload = function(payloadType, payload) {
// Open a payload type list.
browser.clickWithWait('//*[contains(@class, "red-ui-typedInput-container")]');
browser.clickWithWait('//*[@id="node-input-property-container"]/li[1]/div/div/div[3]');
// Select a payload type.
var payloadTypeXPath = '//*[contains(@class, "red-ui-typedInput-options")]/a[' + payloadTypeList[payloadType] + ']';
browser.clickWithWait(payloadTypeXPath);
if (payload) {
// Input a value.
browser.setValue('//*[contains(@class, "red-ui-typedInput-input")]/input', payload);
browser.setValue('//*[@id="node-input-property-container"]/li[1]/div/div/div[3]/div[1]/input', payload);
}
}
injectNode.prototype.setTopic = function(topic) {
browser.setValue('#node-input-topic', topic);
browser.setValue('//*[@id="node-input-property-container"]/li[2]/div/div/div[3]/div[1]/input', topic);
}
injectNode.prototype.setOnce = function(once) {

View File

@@ -29,11 +29,11 @@ debugNode.prototype.setOutput = function (complete) {
browser.clickWithWait('//*[contains(@class, "red-ui-typedInput-container")]/button');
if (complete !== 'true') {
// Select the "msg" type.
browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options")][1]/a[1]');
browser.clickWithWait('//div[contains(@class, "red-ui-typedInput-options")][2]/a[1]');
// Input the path in msg.
browser.clickWithWait('//*[contains(@class, "red-ui-typedInput-input")]/input');
browser.keys(Array('payload'.length).fill('Backspace'));
browser.setValue('//*[contains(@class, "red-ui-typedInput-input")]/input', complete);
browser.setValue('//*[@id="dialog-form"]/div[1]/div/div[1]/input', complete);
} else {
// Select the "complete msg object" type.
browser.clickWithWait('/html/body/div[11]/a[2]');

View File

@@ -267,7 +267,7 @@ describe('cookbook', function () {
debugTab.getMessage().should.match(/^"([1-9]?[0-9],){2}[1-9]?[0-9]↵"$/);
debugTab.clearMessage();
injectNode2.clickLeftButton();
debugTab.getMessage().should.match(/^"a,b,c↵(([1-9]?[0-9],){2}[1-9]?[0-9]↵){4}"$/);
debugTab.getMessage().should.match(/^"(([1-9]?[0-9],){2}[1-9]?[0-9]↵){4}"$/);
});
it('parse CSV input', function () {