From f299cf6ba2c6d7da05be146d5c01dc0d7f03bab2 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 24 Jul 2014 11:42:06 -0600 Subject: [PATCH] Added some tests for unparse; preparation for #70 --- papaparse.js | 8 +- tests/test-cases.js | 163 +++++++++++++++++++++++-- tests/test-runner.js | 281 ++++++++++++++++++++++++++++++------------- tests/tests.css | 11 ++ tests/tests.html | 67 ++++++++--- 5 files changed, 418 insertions(+), 112 deletions(-) diff --git a/papaparse.js b/papaparse.js index 11fa1af..735ff90 100644 --- a/papaparse.js +++ b/papaparse.js @@ -251,9 +251,9 @@ { if (typeof _input.data === 'string') _input.data = JSON.parse(_input.data); - + if (_input.data instanceof Array) - { + { if (!_input.fields) _input.fields = _input.data[0] instanceof Array ? _input.fields @@ -261,9 +261,9 @@ if (!(_input.data[0] instanceof Array) && typeof _input.data[0] !== 'object') _input.data = [_input.data]; // handles input like [1,2,3] or ["asdf"] - - return serialize(_input.fields, _input.data); } + + return serialize(_input.fields || [], _input.data || []); } // Default (any valid paths should return before this) diff --git a/tests/test-cases.js b/tests/test-cases.js index 34a0fb6..c9a237e 100644 --- a/tests/test-cases.js +++ b/tests/test-cases.js @@ -1,12 +1,9 @@ -// TODO: Add tests for unparse: -// If fields is omitted, write a CSV string without a header row -// If delimiter is omitted, choose comma by default -// If data is omitted, do nothing... maybe if fields IS specified, write just the header row? - var RECORD_SEP = String.fromCharCode(30); var UNIT_SEP = String.fromCharCode(31); -var TESTS = [ + +// Tests for Papa.parse() function (CSV to JSON) +var PARSE_TESTS = [ { input: 'A,b,c', description: "One row", @@ -313,10 +310,10 @@ var TESTS = [ } }, { - input: 'a,b,c\r\n,e,f', - description: "First field of a line is empty", + input: 'a,b,\r\nd,e,f', + description: "Last field of a line is empty", expected: { - data: [['a', 'b', 'c'], ['', 'e', 'f']], + data: [['a', 'b', ''], ['d', 'e', 'f']], errors: [] } }, @@ -416,4 +413,152 @@ var TESTS = [ errors: [] } } +]; + + + + + + + + +// Tests for Papa.unparse() function (JSON to CSV) +var UNPARSE_TESTS = [ + { + description: "A simple row", + notes: "Comma should be default delimiter", + input: [['A', 'b', 'c']], + expected: 'A,b,c' + }, + { + description: "Two rows", + input: [['A', 'b', 'c'], ['d', 'E', 'f']], + expected: 'A,b,c\r\nd,E,f' + }, + { + description: "Data with quotes", + input: [['a', '"b"', 'c'], ['"d"', 'e', 'f']], + expected: 'a,"""b""",c\r\n"""d""",e,f' + }, + { + description: "Data with newlines", + input: [['a', 'b\nb', 'c'], ['d', 'e', 'f\r\nf']], + expected: 'a,"b\nb",c\r\nd,e,"f\r\nf"' + }, + { + description: "Array of objects (header row)", + input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col2": "e", "Col3": "f" }], + expected: 'Col1,Col2,Col3\r\na,b,c\r\nd,e,f' + }, + { + description: "With header row, missing a field in a row", + input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col3": "f" }], + expected: 'Col1,Col2,Col3\r\na,b,c\r\nd,,f' + }, + { + description: "With header row, with extra field in a row", + notes: "Extra field should be ignored; first object in array dictates header row", + input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col2": "e", "Extra": "g", "Col3": "f" }], + expected: 'Col1,Col2,Col3\r\na,b,c\r\nd,e,f' + }, + { + description: "Specifying column names and data separately", + input: { fields: ["Col1", "Col2", "Col3"], data: [["a", "b", "c"], ["d", "e", "f"]] }, + expected: 'Col1,Col2,Col3\r\na,b,c\r\nd,e,f' + }, + { + description: "Specifying column names only (no data)", + notes: "Papa should add a data property that is an empty array to prevent errors (no copy is made)", + input: { fields: ["Col1", "Col2", "Col3"] }, + expected: 'Col1,Col2,Col3' + }, + { + description: "Specifying data only (no field names), improperly", + notes: "A single array for a single row is wrong, but it can be compensated.
Papa should add empty fields property to prevent errors.", + input: { data: ["abc", "d", "ef"] }, + expected: 'abc,d,ef' + }, + { + description: "Specifying data only (no field names), properly", + notes: "An array of arrays, even if just a single row.
Papa should add empty fields property to prevent errors.", + input: { data: [["a", "b", "c"]] }, + expected: 'a,b,c' + }, + { + description: "Custom delimiter (semicolon)", + input: [['A', 'b', 'c'], ['d', 'e', 'f']], + config: { delimiter: ';' }, + expected: 'A;b;c\r\nd;e;f' + }, + { + description: "Custom delimiter (tab)", + input: [['Ab', 'cd', 'ef'], ['g', 'h', 'ij']], + config: { delimiter: '\t' }, + expected: 'Ab\tcd\tef\r\ng\th\tij' + }, + { + description: "Custom delimiter (ASCII 30)", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { delimiter: RECORD_SEP }, + expected: 'a'+RECORD_SEP+'b'+RECORD_SEP+'c\r\nd'+RECORD_SEP+'e'+RECORD_SEP+'f' + }, + { + description: "Bad delimiter (\\n)", + notes: "Should default to comma", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { delimiter: '\n' }, + expected: 'a,b,c\r\nd,e,f' + }, + { + description: "Custom line ending (\\r)", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { newline: '\r' }, + expected: 'a,b,c\rd,e,f' + }, + { + description: "Custom line ending (\\n)", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { newline: '\n' }, + expected: 'a,b,c\nd,e,f' + }, + { + description: "Custom, but strange, line ending ($)", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { newline: '$' }, + expected: 'a,b,c$d,e,f' + }, + { + description: "Force quotes around all fields", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { quotes: true }, + expected: '"a","b","c"\r\n"d","e","f"' + }, + { + description: "Force quotes around all fields (with header row)", + input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col2": "e", "Col3": "f" }], + config: { quotes: true }, + expected: '"Col1","Col2","Col3"\r\n"a","b","c"\r\n"d","e","f"' + }, + { + description: "Force quotes around certain fields only", + input: [['a', 'b', 'c'], ['d', 'e', 'f']], + config: { quotes: [0, 2] }, + expected: '"a",b,"c"\r\n"d",e,"f"' + }, + { + description: "Force quotes around certain fields only (with header row)", + input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col2": "e", "Col3": "f" }], + config: { quotes: [0, 2] }, + expected: '"Col1",Col2,"Col3"\r\n"a",b,"c"\r\n"d",e,"f"' + }, + { + description: "Empty input", + input: [], + expected: '' + }, + { + description: "Mismatched field counts in rows", + input: [['a', 'b', 'c'], ['d', 'e'], ['f']], + expected: 'a,b,c\r\nd,e\r\nf' + } ]; \ No newline at end of file diff --git a/tests/test-runner.js b/tests/test-runner.js index ec4bb96..a7a4944 100644 --- a/tests/test-runner.js +++ b/tests/test-runner.js @@ -1,10 +1,11 @@ var passCount = 0; var failCount = 0; +var testCount = 0; $(function() { // First, wireup! - $('#results').on('click', 'td.rvl', function() + $('.results').on('click', 'td.rvl', function() { var tr = $(this).closest('tr'); if (tr.hasClass('collapsed')) @@ -22,29 +23,22 @@ $(function() tr.toggleClass('collapsed expanded'); }); - $('#expand-all').click(function() + $('.expand-all').click(function() { - $('.collapsed .rvl').click(); + var $testGroup = $(this).closest('.test-group'); + $('.collapsed .rvl', $testGroup).click(); }); - $('#collapse-all').click(function() + $('.collapse-all').click(function() { - $('.expanded .rvl').click(); + var $testGroup = $(this).closest('.test-group'); + $('.expanded .rvl', $testGroup).click(); }); - // Next, run tests and render results! - for (var i = 0; i < TESTS.length; i++) - { - var test = TESTS[i]; - var passed = runTest(test, i); - if (passed) - passCount++; - else - failCount++; - } - + runParseTests(); + runUnparseTests(); // Finally, show the overall status. @@ -54,98 +48,212 @@ $(function() $('#status').addClass('status-fail').html(""+failCount+" test"+(failCount == 1 ? "" : "s")+" failed; "+passCount+" passed"); }); -function runTest(test, num) + + + +// Executes all tests in PARSE_TESTS from test-cases.js +// and renders results in the table. +function runParseTests() { - var actual = Papa.parse(test.input, test.config); + for (var i = 0; i < PARSE_TESTS.length; i++) + { + var test = PARSE_TESTS[i]; + var passed = runTest(test); + if (passed) + passCount++; + else + failCount++; + } - var results = compare(actual.data, actual.errors, test.expected); - var testDescription = (test.description || ""); - if (testDescription.length > 0) - testDescription += '
'; - if (test.notes) - testDescription += '' + test.notes + ''; + function runTest(test) + { + var actual; + + try + { + actual = Papa.parse(test.input, test.config); + } + catch (e) + { + actual.data = []; + actual.errors = [e]; + } + + var testId = testCount++; + var results = compare(actual.data, actual.errors, test.expected); - var tr = '' - + '+' - + '' + testDescription + '' - + passOrFailTd(results.data) - + passOrFailTd(results.errors) - + '
condensed
' - + '
condensed
' - + '
condensed
' - + '
condensed
' - + ''; + var testDescription = (test.description || ""); + if (testDescription.length > 0) + testDescription += '
'; + if (test.notes) + testDescription += '' + test.notes + ''; - $('#results').append(tr); + var tr = '' + + '+' + + '' + testDescription + '' + + passOrFailTd(results.data) + + passOrFailTd(results.errors) + + '
condensed
' + + '
condensed
' + + '
condensed
' + + '
condensed
' + + ''; - if (!results.data.passed || !results.errors.passed) - $('#test-'+num+' td.rvl').click(); + $('#tests-for-parse .results').append(tr); - return results.data.passed && results.errors.passed -} + if (!results.data.passed || !results.errors.passed) + $('#test-'+testId+' td.rvl').click(); -function compare(actualData, actualErrors, expected) -{ - var data = compareData(actualData, expected.data); - var errors = compareErrors(actualErrors, expected.errors); - return { - data: data, - errors: errors + return results.data.passed && results.errors.passed } -} -function compareData(actual, expected) -{ - var passed = true; - if (actual.length != expected.length) - passed = false; - - for (var row = 0; row < expected.length; row++) + function compare(actualData, actualErrors, expected) { - if (actual.length != expected.length) - { - passed = false; - break; + var data = compareData(actualData, expected.data); + var errors = compareErrors(actualErrors, expected.errors); + + return { + data: data, + errors: errors } - for (var col = 0; col < expected[row].length; col++) + + function compareData(actual, expected) { - if (actual[row].length != expected[row].length) - { - passed = false; - break; - } + var passed = true; - var expectedVal = expected[row][col]; - var actualVal = actual[row][col]; + if (actual.length != expected.length) + passed = false; - if (actualVal !== expectedVal) + for (var row = 0; row < expected.length; row++) { - passed = false; - break; + if (actual.length != expected.length) + { + passed = false; + break; + } + + for (var col = 0; col < expected[row].length; col++) + { + if (actual[row].length != expected[row].length) + { + passed = false; + break; + } + + var expectedVal = expected[row][col]; + var actualVal = actual[row][col]; + + if (actualVal !== expectedVal) + { + passed = false; + break; + } + } } + + // We pass back an object right now, even though it only contains + // one value, because we might add details to the test results later + // (same with compareErrors below) + return { + passed: passed + }; } - } - // We pass back an object right now, even though it only contains - // one value, because we might add details to the test results later - // (same with compareErrors below) - return { - passed: passed - }; + + function compareErrors(actual, expected) + { + var passed = JSON.stringify(actual) == JSON.stringify(expected); + + return { + passed: passed + }; + } + } } -function compareErrors(actual, expected) + + + + + +// Executes all tests in UNPARSE_TESTS from test-cases.js +// and renders results in the table. +function runUnparseTests() { - var passed = JSON.stringify(actual) == JSON.stringify(expected); + for (var i = 0; i < UNPARSE_TESTS.length; i++) + { + var test = UNPARSE_TESTS[i]; + var passed = runTest(test); + if (passed) + passCount++; + else + failCount++; + } + + function runTest(test) + { + var actual; + + try + { + actual = Papa.unparse(test.input, test.config); + } + catch (e) + { + actual = e; + } + + var testId = testCount++; + var results = compare(actual, test.expected); + + var testDescription = (test.description || ""); + if (testDescription.length > 0) + testDescription += '
'; + if (test.notes) + testDescription += '' + test.notes + ''; - return { - passed: passed - }; + var tr = '' + + '+' + + '' + testDescription + '' + + passOrFailTd(results) + + '
condensed
' + + '
condensed
' + + '
condensed
' + + '
condensed
' + + ''; + + $('#tests-for-unparse .results').append(tr); + + if (!results.passed) + $('#test-'+testId+' td.rvl').click(); + + return results.passed; + } + + + function compare(actual, expected) + { + return { + passed: actual == expected + }; + } } + + + + + + + + + + + +// Makes a TD tag with OK or FAIL depending on test result function passOrFailTd(result) { if (result.passed) @@ -154,18 +262,25 @@ function passOrFailTd(result) return 'FAIL'; } + +// Reveals some hidden, whitespace, or invisible characters function revealChars(txt) { // Make spaces and tabs more obvious when glancing txt = txt.replace(/( |\t)/ig, '$1'); - txt = txt.replace(/(\r\n|\n\r|\r|\n)/ig, '$1$1'); - // Now make the line breaks within the spans actually appear on the page + // Make UNIT_SEP and RECORD_SEP characters visible + txt = txt.replace(/(\u001e|\u001f)/ig, '$1$1'); + + // Now make the whitespace and invisible characters + // within the spans actually appear on the page txt = txt.replace(/">\r\n<\/span>/ig, '">\\r\\n'); txt = txt.replace(/">\n\r<\/span>/ig, '">\\n\\r'); txt = txt.replace(/">\r<\/span>/ig, '">\\r'); txt = txt.replace(/">\n<\/span>/ig, '">\\n'); + txt = txt.replace(/">\u001e<\/span>/ig, '">\\u001e'); + txt = txt.replace(/">\u001f<\/span>/ig, '">\\u001f'); return txt; } \ No newline at end of file diff --git a/tests/tests.css b/tests/tests.css index 9fba093..646a26e 100644 --- a/tests/tests.css +++ b/tests/tests.css @@ -31,6 +31,13 @@ h1 { margin-bottom: 30px; } +h2 { + text-align: center; + font-weight: bold; + font-size: 26px; + margin-bottom: 20px; +} + .status-pass, .status-fail { padding: 10px; @@ -51,6 +58,10 @@ h1 { background: #BB0000; } +.test-group { + margin-bottom: 50px; +} + table { width: 100%; border-collapse: collapse; diff --git a/tests/tests.html b/tests/tests.html index ed3f304..4849f2f 100644 --- a/tests/tests.html +++ b/tests/tests.html @@ -14,21 +14,56 @@
- Expand all - · - Collapse all -
- - - - - - - - - - - -
Test CaseDataErrorsConfigInputExpectedActual
+ + + +
+

Tests for Papa.parse()

+ + Expand all + · + Collapse all +
+ + + + + + + + + + + +
Test CaseDataErrorsConfigInputExpectedActual
+
+ + + + + + +
+

Tests for Papa.unparse()

+ + Expand all + · + Collapse all +
+ + + + + + + + + + +
Test CaseDataConfigInputExpectedActual
+
+ + + \ No newline at end of file