Browse Source

Added some tests for unparse; preparation for #70

pull/70/head
Matthew Holt 11 years ago
parent
commit
f299cf6ba2
  1. 4
      papaparse.js
  2. 163
      tests/test-cases.js
  3. 163
      tests/test-runner.js
  4. 11
      tests/tests.css
  5. 41
      tests/tests.html

4
papaparse.js

@ -261,9 +261,9 @@
if (!(_input.data[0] instanceof Array) && typeof _input.data[0] !== 'object') if (!(_input.data[0] instanceof Array) && typeof _input.data[0] !== 'object')
_input.data = [_input.data]; // handles input like [1,2,3] or ["asdf"] _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) // Default (any valid paths should return before this)

163
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 RECORD_SEP = String.fromCharCode(30);
var UNIT_SEP = String.fromCharCode(31); var UNIT_SEP = String.fromCharCode(31);
var TESTS = [
// Tests for Papa.parse() function (CSV to JSON)
var PARSE_TESTS = [
{ {
input: 'A,b,c', input: 'A,b,c',
description: "One row", description: "One row",
@ -313,10 +310,10 @@ var TESTS = [
} }
}, },
{ {
input: 'a,b,c\r\n,e,f', input: 'a,b,\r\nd,e,f',
description: "First field of a line is empty", description: "Last field of a line is empty",
expected: { expected: {
data: [['a', 'b', 'c'], ['', 'e', 'f']], data: [['a', 'b', ''], ['d', 'e', 'f']],
errors: [] errors: []
} }
}, },
@ -417,3 +414,151 @@ var TESTS = [
} }
} }
]; ];
// 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.<br>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.<br>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'
}
];

163
tests/test-runner.js

@ -1,10 +1,11 @@
var passCount = 0; var passCount = 0;
var failCount = 0; var failCount = 0;
var testCount = 0;
$(function() $(function()
{ {
// First, wireup! // First, wireup!
$('#results').on('click', 'td.rvl', function() $('.results').on('click', 'td.rvl', function()
{ {
var tr = $(this).closest('tr'); var tr = $(this).closest('tr');
if (tr.hasClass('collapsed')) if (tr.hasClass('collapsed'))
@ -22,29 +23,22 @@ $(function()
tr.toggleClass('collapsed expanded'); 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! // Next, run tests and render results!
for (var i = 0; i < TESTS.length; i++) runParseTests();
{ runUnparseTests();
var test = TESTS[i];
var passed = runTest(test, i);
if (passed)
passCount++;
else
failCount++;
}
// Finally, show the overall status. // Finally, show the overall status.
@ -54,10 +48,39 @@ $(function()
$('#status').addClass('status-fail').html("<b>"+failCount+"</b> test"+(failCount == 1 ? "" : "s")+" failed; <b>"+passCount+"</b> passed"); $('#status').addClass('status-fail').html("<b>"+failCount+"</b> test"+(failCount == 1 ? "" : "s")+" failed; <b>"+passCount+"</b> passed");
}); });
function runTest(test, num)
// Executes all tests in PARSE_TESTS from test-cases.js
// and renders results in the table.
function runParseTests()
{
for (var i = 0; i < PARSE_TESTS.length; i++)
{ {
var actual = Papa.parse(test.input, test.config); var test = PARSE_TESTS[i];
var passed = runTest(test);
if (passed)
passCount++;
else
failCount++;
}
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 results = compare(actual.data, actual.errors, test.expected);
var testDescription = (test.description || ""); var testDescription = (test.description || "");
@ -66,7 +89,7 @@ function runTest(test, num)
if (test.notes) if (test.notes)
testDescription += '<span class="notes">' + test.notes + '</span>'; testDescription += '<span class="notes">' + test.notes + '</span>';
var tr = '<tr class="collapsed" id="test-'+num+'">' var tr = '<tr class="collapsed" id="test-'+testId+'">'
+ '<td class="rvl">+</td>' + '<td class="rvl">+</td>'
+ '<td>' + testDescription + '</td>' + '<td>' + testDescription + '</td>'
+ passOrFailTd(results.data) + passOrFailTd(results.data)
@ -77,23 +100,25 @@ function runTest(test, num)
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">data: ' + JSON.stringify(actual.data, null, 4) + '\r\nerrors: ' + JSON.stringify(actual.errors, null, 4) + '</div></td>' + '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">data: ' + JSON.stringify(actual.data, null, 4) + '\r\nerrors: ' + JSON.stringify(actual.errors, null, 4) + '</div></td>'
+ '</tr>'; + '</tr>';
$('#results').append(tr); $('#tests-for-parse .results').append(tr);
if (!results.data.passed || !results.errors.passed) if (!results.data.passed || !results.errors.passed)
$('#test-'+num+' td.rvl').click(); $('#test-'+testId+' td.rvl').click();
return results.data.passed && results.errors.passed return results.data.passed && results.errors.passed
} }
function compare(actualData, actualErrors, expected) function compare(actualData, actualErrors, expected)
{ {
var data = compareData(actualData, expected.data); var data = compareData(actualData, expected.data);
var errors = compareErrors(actualErrors, expected.errors); var errors = compareErrors(actualErrors, expected.errors);
return { return {
data: data, data: data,
errors: errors errors: errors
} }
}
function compareData(actual, expected) function compareData(actual, expected)
{ {
@ -137,6 +162,7 @@ function compareData(actual, expected)
}; };
} }
function compareErrors(actual, expected) function compareErrors(actual, expected)
{ {
var passed = JSON.stringify(actual) == JSON.stringify(expected); var passed = JSON.stringify(actual) == JSON.stringify(expected);
@ -145,7 +171,89 @@ function compareErrors(actual, expected)
passed: passed passed: passed
}; };
} }
}
}
// Executes all tests in UNPARSE_TESTS from test-cases.js
// and renders results in the table.
function runUnparseTests()
{
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 += '<br>';
if (test.notes)
testDescription += '<span class="notes">' + test.notes + '</span>';
var tr = '<tr class="collapsed" id="test-'+testId+'">'
+ '<td class="rvl">+</td>'
+ '<td>' + testDescription + '</td>'
+ passOrFailTd(results)
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">' + JSON.stringify(test.config, null, 2) + '</div></td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">' + JSON.stringify(test.input, null, 4) + '</div></td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">' + revealChars(test.expected) + '</div></td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">' + revealChars(actual) + '</div></td>'
+ '</tr>';
$('#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) function passOrFailTd(result)
{ {
if (result.passed) if (result.passed)
@ -154,18 +262,25 @@ function passOrFailTd(result)
return '<td class="fail">FAIL</td>'; return '<td class="fail">FAIL</td>';
} }
// Reveals some hidden, whitespace, or invisible characters
function revealChars(txt) function revealChars(txt)
{ {
// Make spaces and tabs more obvious when glancing // Make spaces and tabs more obvious when glancing
txt = txt.replace(/( |\t)/ig, '<span class="whitespace-char">$1</span>'); txt = txt.replace(/( |\t)/ig, '<span class="whitespace-char">$1</span>');
txt = txt.replace(/(\r\n|\n\r|\r|\n)/ig, '<span class="whitespace-char special-char">$1</span>$1'); txt = txt.replace(/(\r\n|\n\r|\r|\n)/ig, '<span class="whitespace-char special-char">$1</span>$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, '<span class="special-char">$1</span>$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</span>'); txt = txt.replace(/">\r\n<\/span>/ig, '">\\r\\n</span>');
txt = txt.replace(/">\n\r<\/span>/ig, '">\\n\\r</span>'); txt = txt.replace(/">\n\r<\/span>/ig, '">\\n\\r</span>');
txt = txt.replace(/">\r<\/span>/ig, '">\\r</span>'); txt = txt.replace(/">\r<\/span>/ig, '">\\r</span>');
txt = txt.replace(/">\n<\/span>/ig, '">\\n</span>'); txt = txt.replace(/">\n<\/span>/ig, '">\\n</span>');
txt = txt.replace(/">\u001e<\/span>/ig, '">\\u001e</span>');
txt = txt.replace(/">\u001f<\/span>/ig, '">\\u001f</span>');
return txt; return txt;
} }

11
tests/tests.css

@ -31,6 +31,13 @@ h1 {
margin-bottom: 30px; margin-bottom: 30px;
} }
h2 {
text-align: center;
font-weight: bold;
font-size: 26px;
margin-bottom: 20px;
}
.status-pass, .status-pass,
.status-fail { .status-fail {
padding: 10px; padding: 10px;
@ -51,6 +58,10 @@ h1 {
background: #BB0000; background: #BB0000;
} }
.test-group {
margin-bottom: 50px;
}
table { table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;

41
tests/tests.html

@ -14,12 +14,18 @@
<div id="status"></div> <div id="status"></div>
<a href="javascript:" id="expand-all">Expand all</a>
<div class="test-group" id="tests-for-parse">
<h2>Tests for Papa.parse()</h2>
<a href="javascript:" class="expand-all">Expand all</a>
&middot; &middot;
<a href="javascript:" id="collapse-all">Collapse all</a> <a href="javascript:" class="collapse-all">Collapse all</a>
<br> <br>
<table id="results"> <table class="results">
<tr> <tr>
<th colspan="2">Test Case</th> <th colspan="2">Test Case</th>
<th>Data</th> <th>Data</th>
@ -30,5 +36,34 @@
<th>Actual</th> <th>Actual</th>
</tr> </tr>
</table> </table>
</div>
<div class="test-group" id="tests-for-unparse">
<h2>Tests for Papa.unparse()</h2>
<a href="javascript:" class="expand-all">Expand all</a>
&middot;
<a href="javascript:" class="collapse-all">Collapse all</a>
<br>
<table class="results">
<tr>
<th colspan="2">Test Case</th>
<th>Data</th>
<th>Config</th>
<th>Input</th>
<th>Expected</th>
<th>Actual</th>
</tr>
</table>
</div>
</body> </body>
</html> </html>
Loading…
Cancel
Save