Browse Source

Added some tests for unparse; preparation for #70

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

8
papaparse.js

@ -251,9 +251,9 @@ @@ -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 @@ @@ -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)

163
tests/test-cases.js

@ -1,12 +1,9 @@ @@ -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 = [ @@ -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 = [ @@ -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.<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'
}
];

281
tests/test-runner.js

@ -1,10 +1,11 @@ @@ -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() @@ -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() @@ -54,98 +48,212 @@ $(function()
$('#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()
{
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 += '<br>';
if (test.notes)
testDescription += '<span class="notes">' + test.notes + '</span>';
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 = '<tr class="collapsed" id="test-'+num+'">'
+ '<td class="rvl">+</td>'
+ '<td>' + testDescription + '</td>'
+ passOrFailTd(results.data)
+ passOrFailTd(results.errors)
+ '<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">' + revealChars(test.input) + '</div></td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">data: ' + JSON.stringify(test.expected.data, null, 4) + '\r\nerrors: ' + JSON.stringify(test.expected.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>';
var testDescription = (test.description || "");
if (testDescription.length > 0)
testDescription += '<br>';
if (test.notes)
testDescription += '<span class="notes">' + test.notes + '</span>';
$('#results').append(tr);
var tr = '<tr class="collapsed" id="test-'+testId+'">'
+ '<td class="rvl">+</td>'
+ '<td>' + testDescription + '</td>'
+ passOrFailTd(results.data)
+ passOrFailTd(results.errors)
+ '<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">' + revealChars(test.input) + '</div></td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">data: ' + JSON.stringify(test.expected.data, null, 4) + '\r\nerrors: ' + JSON.stringify(test.expected.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>';
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 += '<br>';
if (test.notes)
testDescription += '<span class="notes">' + test.notes + '</span>';
return {
passed: passed
};
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)
{
if (result.passed)
@ -154,18 +262,25 @@ function passOrFailTd(result) @@ -154,18 +262,25 @@ function passOrFailTd(result)
return '<td class="fail">FAIL</td>';
}
// Reveals some hidden, whitespace, or invisible characters
function revealChars(txt)
{
// Make spaces and tabs more obvious when glancing
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');
// 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(/">\n\r<\/span>/ig, '">\\n\\r</span>');
txt = txt.replace(/">\r<\/span>/ig, '">\\r</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;
}

11
tests/tests.css

@ -31,6 +31,13 @@ h1 { @@ -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 { @@ -51,6 +58,10 @@ h1 {
background: #BB0000;
}
.test-group {
margin-bottom: 50px;
}
table {
width: 100%;
border-collapse: collapse;

67
tests/tests.html

@ -14,21 +14,56 @@ @@ -14,21 +14,56 @@
<div id="status"></div>
<a href="javascript:" id="expand-all">Expand all</a>
&middot;
<a href="javascript:" id="collapse-all">Collapse all</a>
<br>
<table id="results">
<tr>
<th colspan="2">Test Case</th>
<th>Data</th>
<th>Errors</th>
<th>Config</th>
<th>Input</th>
<th>Expected</th>
<th>Actual</th>
</tr>
</table>
<div class="test-group" id="tests-for-parse">
<h2>Tests for Papa.parse()</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>Errors</th>
<th>Config</th>
<th>Input</th>
<th>Expected</th>
<th>Actual</th>
</tr>
</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>
</html>
Loading…
Cancel
Save