Browse Source

Merge pull request #1 from mholt/master

pulling the latest changes
pull/223/head^2
ommar 10 years ago
parent
commit
7e5044a7a1
  1. 3
      .travis.yml
  2. 11
      package.json
  3. 84
      papaparse.js
  4. 4
      papaparse.min.js
  5. 11
      tests/node-tests.js
  6. 149
      tests/test-cases.js
  7. 448
      tests/test-runner.js
  8. 14
      tests/test.js
  9. 173
      tests/tests.css
  10. 120
      tests/tests.html

3
.travis.yml

@ -0,0 +1,3 @@
language: node_js
node_js:
- "0.12"

11
package.json

@ -1,6 +1,6 @@
{ {
"name": "papaparse", "name": "papaparse",
"version": "4.1.1", "version": "4.1.2",
"description": "Fast and powerful CSV parser for the browser that supports web workers and streaming large files. Converts CSV to JSON and JSON to CSV.", "description": "Fast and powerful CSV parser for the browser that supports web workers and streaming large files. Converts CSV to JSON and JSON to CSV.",
"keywords": [ "keywords": [
"csv", "csv",
@ -41,13 +41,20 @@
], ],
"main": "papaparse.js", "main": "papaparse.js",
"devDependencies": { "devDependencies": {
"chai": "^3.0.0",
"connect": "^3.3.3", "connect": "^3.3.3",
"grunt": "^0.4.5", "grunt": "^0.4.5",
"grunt-contrib-uglify": "^0.6.0", "grunt-contrib-uglify": "^0.6.0",
"mocha": "^2.2.5",
"mocha-phantomjs": "^3.5.4",
"open": "0.0.5", "open": "0.0.5",
"phantomjs": "1.9.1 - 1.9.7-15",
"serve-static": "^1.7.1" "serve-static": "^1.7.1"
}, },
"scripts": { "scripts": {
"test": "node tests/test.js" "test-browser": "node tests/test.js",
"test-phantomjs": "node tests/test.js --phantomjs",
"test-node": "mocha tests/node-tests.js tests/test-cases.js",
"test": "npm run test-node && npm run test-phantomjs"
} }
} }

84
papaparse.js

@ -1,13 +1,15 @@
/*! /*!
Papa Parse Papa Parse
v4.1.1 v4.1.2
https://github.com/mholt/PapaParse https://github.com/mholt/PapaParse
*/ */
(function(global) (function(global)
{ {
"use strict"; "use strict";
var IS_WORKER = (!global.document && !!global.postMessage), LOADED_SYNC = false, AUTO_SCRIPT_PATH; var IS_WORKER = !global.document && !!global.postMessage,
IS_PAPA_WORKER = IS_WORKER && /(\?|&)papaworker(=|&|$)/.test(global.location.search),
LOADED_SYNC = false, AUTO_SCRIPT_PATH;
var workers = {}, workerIdCounter = 0; var workers = {}, workerIdCounter = 0;
var Papa = {}; var Papa = {};
@ -19,7 +21,7 @@
Papa.UNIT_SEP = String.fromCharCode(31); Papa.UNIT_SEP = String.fromCharCode(31);
Papa.BYTE_ORDER_MARK = "\ufeff"; Papa.BYTE_ORDER_MARK = "\ufeff";
Papa.BAD_DELIMITERS = ["\r", "\n", "\"", Papa.BYTE_ORDER_MARK]; Papa.BAD_DELIMITERS = ["\r", "\n", "\"", Papa.BYTE_ORDER_MARK];
Papa.WORKERS_SUPPORTED = !!global.Worker; Papa.WORKERS_SUPPORTED = !IS_WORKER && !!global.Worker;
Papa.SCRIPT_PATH = null; // Must be set by your code if you use workers and this lib is loaded asynchronously Papa.SCRIPT_PATH = null; // Must be set by your code if you use workers and this lib is loaded asynchronously
// Configurable chunk sizes for local and remote files, respectively // Configurable chunk sizes for local and remote files, respectively
@ -42,7 +44,7 @@
else if (isFunction(global.define) && global.define.amd) else if (isFunction(global.define) && global.define.amd)
{ {
// Wireup with RequireJS // Wireup with RequireJS
global.define(function() { return Papa; }); define(function() { return Papa; });
} }
else else
{ {
@ -145,7 +147,7 @@
} }
if (IS_WORKER) if (IS_PAPA_WORKER)
{ {
global.onmessage = workerThreadReceivedMessage; global.onmessage = workerThreadReceivedMessage;
} }
@ -223,9 +225,15 @@
var _fields = []; var _fields = [];
// Default configuration // Default configuration
var _quotes = false; // whether to surround every datum with quotes
var _delimiter = ","; // delimiting character /** whether to surround every datum with quotes */
var _newline = "\r\n"; // newline character(s) var _quotes = false;
/** delimiting character */
var _delimiter = ",";
/** newline character(s) */
var _newline = "\r\n";
unpackConfig(); unpackConfig();
@ -283,7 +291,7 @@
} }
// Turns an object's keys into an array /** Turns an object's keys into an array */
function objectKeys(obj) function objectKeys(obj)
{ {
if (typeof obj !== 'object') if (typeof obj !== 'object')
@ -294,7 +302,7 @@
return keys; return keys;
} }
// The double for loop that iterates the data and writes out a CSV string including header row /** The double for loop that iterates the data and writes out a CSV string including header row */
function serialize(fields, data) function serialize(fields, data)
{ {
var csv = ""; var csv = "";
@ -340,7 +348,7 @@
return csv; return csv;
} }
// Encloses a value around quotes if needed (makes a value safe for CSV insertion) /** Encloses a value around quotes if needed (makes a value safe for CSV insertion) */
function safe(str, col) function safe(str, col)
{ {
if (typeof str === "undefined" || str === null) if (typeof str === "undefined" || str === null)
@ -367,7 +375,7 @@
} }
} }
// ChunkStreamer is the base prototype for various streamer implementations. /** ChunkStreamer is the base prototype for various streamer implementations. */
function ChunkStreamer(config) function ChunkStreamer(config)
{ {
this._handle = null; this._handle = null;
@ -420,7 +428,7 @@
var finishedIncludingPreview = this._finished || (this._config.preview && this._rowCount >= this._config.preview); var finishedIncludingPreview = this._finished || (this._config.preview && this._rowCount >= this._config.preview);
if (IS_WORKER) if (IS_PAPA_WORKER)
{ {
global.postMessage({ global.postMessage({
results: results, results: results,
@ -456,7 +464,7 @@
{ {
if (isFunction(this._config.error)) if (isFunction(this._config.error))
this._config.error(error); this._config.error(error);
else if (IS_WORKER && this._config.error) else if (IS_PAPA_WORKER && this._config.error)
{ {
global.postMessage({ global.postMessage({
workerId: Papa.WORKER_ID, workerId: Papa.WORKER_ID,
@ -721,9 +729,11 @@
}; };
} }
// Parses input. Most users won't need, and shouldn't mess with, the baseIndex /**
// and ignoreLastRow parameters. They are used by streamers (wrapper functions) * Parses input. Most users won't need, and shouldn't mess with, the baseIndex
// when an input comes in multiple chunks, like from a file. * and ignoreLastRow parameters. They are used by streamers (wrapper functions)
* when an input comes in multiple chunks, like from a file.
*/
this.parse = function(input, baseIndex, ignoreLastRow) this.parse = function(input, baseIndex, ignoreLastRow)
{ {
if (!_config.newline) if (!_config.newline)
@ -905,6 +915,7 @@
} }
} }
if (preview.data.length > 0)
avgFieldCount /= preview.data.length; avgFieldCount /= preview.data.length;
if ((typeof bestDelta === 'undefined' || delta < bestDelta) if ((typeof bestDelta === 'undefined' || delta < bestDelta)
@ -963,7 +974,7 @@
// The core parser implements speedy and correct CSV parsing /** The core parser implements speedy and correct CSV parsing */
function Parser(config) function Parser(config)
{ {
// Unpack the config object // Unpack the config object
@ -1185,13 +1196,15 @@
lastCursor = cursor; lastCursor = cursor;
} }
// Appends the remaining input from cursor to the end into /**
// row, saves the row, calls step, and returns the results. * Appends the remaining input from cursor to the end into
* row, saves the row, calls step, and returns the results.
*/
function finish(value) function finish(value)
{ {
if (ignoreLastRow) if (ignoreLastRow)
return returnable(); return returnable();
if (!value) if (typeof value === 'undefined')
value = input.substr(cursor); value = input.substr(cursor);
row.push(value); row.push(value);
cursor = inputLen; // important in case parsing is paused cursor = inputLen; // important in case parsing is paused
@ -1201,10 +1214,12 @@
return returnable(); return returnable();
} }
// Appends the current row to the results. It sets the cursor /**
// to newCursor and finds the nextNewline. The caller should * Appends the current row to the results. It sets the cursor
// take care to execute user's step function and check for * to newCursor and finds the nextNewline. The caller should
// preview and end parsing if necessary. * take care to execute user's step function and check for
* preview and end parsing if necessary.
*/
function saveRow(newCursor) function saveRow(newCursor)
{ {
cursor = newCursor; cursor = newCursor;
@ -1213,7 +1228,7 @@
nextNewline = input.indexOf(newline, cursor); nextNewline = input.indexOf(newline, cursor);
} }
// Returns an object with the results, errors, and meta. /** Returns an object with the results, errors, and meta. */
function returnable(stopped) function returnable(stopped)
{ {
return { return {
@ -1229,7 +1244,7 @@
}; };
} }
// Executes the user's step function and resets data & errors. /** Executes the user's step function and resets data & errors. */
function doStep() function doStep()
{ {
step(returnable()); step(returnable());
@ -1237,13 +1252,13 @@
} }
}; };
// Sets the abort flag /** Sets the abort flag */
this.abort = function() this.abort = function()
{ {
aborted = true; aborted = true;
}; };
// Gets the cursor position /** Gets the cursor position */
this.getCharIndex = function() this.getCharIndex = function()
{ {
return cursor; return cursor;
@ -1268,14 +1283,17 @@
'Script path cannot be determined automatically when Papa Parse is loaded asynchronously. ' + 'Script path cannot be determined automatically when Papa Parse is loaded asynchronously. ' +
'You need to set Papa.SCRIPT_PATH manually.' 'You need to set Papa.SCRIPT_PATH manually.'
); );
var w = new global.Worker(Papa.SCRIPT_PATH || AUTO_SCRIPT_PATH); var workerUrl = Papa.SCRIPT_PATH || AUTO_SCRIPT_PATH;
// Append "papaworker" to the search string to tell papaparse that this is our worker.
workerUrl += (workerUrl.indexOf('?') !== -1 ? '&' : '?') + 'papaworker';
var w = new global.Worker(workerUrl);
w.onmessage = mainThreadReceivedMessage; w.onmessage = mainThreadReceivedMessage;
w.id = workerIdCounter++; w.id = workerIdCounter++;
workers[w.id] = w; workers[w.id] = w;
return w; return w;
} }
// Callback when main thread receives a message /** Callback when main thread receives a message */
function mainThreadReceivedMessage(e) function mainThreadReceivedMessage(e)
{ {
var msg = e.data; var msg = e.data;
@ -1334,7 +1352,7 @@
throw "Not implemented."; throw "Not implemented.";
} }
// Callback when worker thread receives a message /** Callback when worker thread receives a message */
function workerThreadReceivedMessage(e) function workerThreadReceivedMessage(e)
{ {
var msg = e.data; var msg = e.data;
@ -1362,7 +1380,7 @@
} }
} }
// Makes a deep copy of an array or object (mostly) /** Makes a deep copy of an array or object (mostly) */
function copy(obj) function copy(obj)
{ {
if (typeof obj !== 'object') if (typeof obj !== 'object')

4
papaparse.min.js vendored

File diff suppressed because one or more lines are too long

11
tests/node-tests.js

@ -37,16 +37,19 @@
assert.equal(parsedCsv.errors.length, 0) assert.equal(parsedCsv.errors.length, 0)
} }
var synchronouslyParsedCsvShouldBeCorrectlyParsed = function() { describe('PapaParse', function() {
it('synchronously parsed CSV should be correctly parsed', function() {
assertLongSampleParsedCorrectly(Papa.parse(longSampleRawCsv)); assertLongSampleParsedCorrectly(Papa.parse(longSampleRawCsv));
}(); });
var asynchronouslyParsedCsvShouldBeCorrectlyParsed = function() { it('asynchronously parsed CSV should be correctly parsed', function(done) {
Papa.parse(longSampleRawCsv, { Papa.parse(longSampleRawCsv, {
complete: function(parsedCsv) { complete: function(parsedCsv) {
assertLongSampleParsedCorrectly(parsedCsv); assertLongSampleParsedCorrectly(parsedCsv);
done();
}, },
}); });
}(); });
});
})(); })();

149
tests/test-cases.js

@ -1,3 +1,10 @@
if (typeof module !== 'undefined' && module.exports) {
var chai = require('chai');
var Papa = require('../papaparse.js');
}
var assert = chai.assert;
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 FILES_ENABLED = false; var FILES_ENABLED = false;
@ -6,6 +13,12 @@ try {
FILES_ENABLED = true; FILES_ENABLED = true;
} catch (e) {} // safari, ie } catch (e) {} // safari, ie
var XHR_ENABLED = false;
try {
new XMLHttpRequest();
XHR_ENABLED = true;
} catch (e) {} // safari, ie
// Tests for the core parser using new Papa.Parser().parse() (CSV to JSON) // Tests for the core parser using new Papa.Parser().parse() (CSV to JSON)
var CORE_PARSER_TESTS = [ var CORE_PARSER_TESTS = [
{ {
@ -206,6 +219,14 @@ var CORE_PARSER_TESTS = [
errors: [] errors: []
} }
}, },
{
description: "Empty quoted field at EOF is empty",
input: 'a,b,""\na,b,""',
expected: {
data: [['a', 'b', ''], ['a', 'b', '']],
errors: []
}
},
{ {
description: "Multiple consecutive empty fields", description: "Multiple consecutive empty fields",
input: 'a,b,,,c,d\n,,e,,,f', input: 'a,b,,,c,d\n,,e,,,f',
@ -428,6 +449,21 @@ var CORE_PARSER_TESTS = [
} }
]; ];
describe('Core Parser Tests', function() {
function generateTest(test) {
(test.disabled ? it.skip : it)(test.description, function() {
var actual = new Papa.Parser(test.config).parse(test.input);
assert.deepEqual(JSON.stringify(actual.errors), JSON.stringify(test.expected.errors));
assert.deepEqual(actual.data, test.expected.data);
});
}
for (var i = 0; i < CORE_PARSER_TESTS.length; i++) {
generateTest(CORE_PARSER_TESTS[i]);
}
});
// Tests for Papa.parse() function -- high-level wrapped parser (CSV to JSON) // Tests for Papa.parse() function -- high-level wrapped parser (CSV to JSON)
var PARSE_TESTS = [ var PARSE_TESTS = [
@ -818,9 +854,19 @@ var PARSE_TESTS = [
} }
]; ];
describe('Parse Tests', function() {
function generateTest(test) {
(test.disabled ? it.skip : it)(test.description, function() {
var actual = Papa.parse(test.input, test.config);
assert.deepEqual(JSON.stringify(actual.errors), JSON.stringify(test.expected.errors));
assert.deepEqual(actual.data, test.expected.data);
});
}
for (var i = 0; i < PARSE_TESTS.length; i++) {
generateTest(PARSE_TESTS[i]);
}
});
@ -843,6 +889,7 @@ var PARSE_ASYNC_TESTS = [
config: { config: {
download: true download: true
}, },
disabled: !XHR_ENABLED,
expected: { expected: {
data: [['A','B','C'],['X','Y','Z']], data: [['A','B','C'],['X','Y','Z']],
errors: [] errors: []
@ -855,6 +902,7 @@ var PARSE_ASYNC_TESTS = [
worker: true, worker: true,
download: true download: true
}, },
disabled: !XHR_ENABLED,
expected: { expected: {
data: [['A','B','C'],['X','Y','Z']], data: [['A','B','C'],['X','Y','Z']],
errors: [] errors: []
@ -885,10 +933,29 @@ var PARSE_ASYNC_TESTS = [
} }
]; ];
describe('Parse Async Tests', function() {
function generateTest(test) {
(test.disabled ? it.skip : it)(test.description, function(done) {
var config = test.config;
config.complete = function(actual) {
assert.deepEqual(JSON.stringify(actual.errors), JSON.stringify(test.expected.errors));
assert.deepEqual(actual.data, test.expected.data);
done();
};
config.error = function(err) {
throw err;
};
Papa.parse(test.input, config);
});
}
for (var i = 0; i < PARSE_ASYNC_TESTS.length; i++) {
generateTest(PARSE_ASYNC_TESTS[i]);
}
});
@ -1038,12 +1105,36 @@ var UNPARSE_TESTS = [
} }
]; ];
describe('Unparse Tests', function() {
function generateTest(test) {
(test.disabled ? it.skip : it)(test.description, function() {
var actual;
try {
actual = Papa.unparse(test.input, test.config);
} catch (e) {
if (e instanceof Error) {
throw e;
}
actual = e;
}
assert.strictEqual(actual, test.expected);
});
}
for (var i = 0; i < UNPARSE_TESTS.length; i++) {
generateTest(UNPARSE_TESTS[i]);
}
});
var CUSTOM_TESTS = [ var CUSTOM_TESTS = [
{ {
description: "Complete is called with all results if neither step nor chunk is defined", description: "Complete is called with all results if neither step nor chunk is defined",
expected: [['A', 'b', 'c'], ['d', 'E', 'f'], ['G', 'h', 'i']], expected: [['A', 'b', 'c'], ['d', 'E', 'f'], ['G', 'h', 'i']],
disabled: !FILES_ENABLED,
run: function(callback) { run: function(callback) {
Papa.parse(new File(['A,b,c\nd,E,f\nG,h,i'], 'sample.csv'), { Papa.parse(new File(['A,b,c\nd,E,f\nG,h,i'], 'sample.csv'), {
chunkSize: 3, chunkSize: 3,
@ -1097,6 +1188,7 @@ var CUSTOM_TESTS = [
{ {
description: "Step exposes cursor for downloads", description: "Step exposes cursor for downloads",
expected: [129, 287, 452, 595, 727, 865, 1031, 1209], expected: [129, 287, 452, 595, 727, 865, 1031, 1209],
disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = []; var updates = [];
Papa.parse("/tests/long-sample.csv", { Papa.parse("/tests/long-sample.csv", {
@ -1113,6 +1205,7 @@ var CUSTOM_TESTS = [
{ {
description: "Step exposes cursor for chunked downloads", description: "Step exposes cursor for chunked downloads",
expected: [129, 287, 452, 595, 727, 865, 1031, 1209], expected: [129, 287, 452, 595, 727, 865, 1031, 1209],
disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = []; var updates = [];
Papa.parse("/tests/long-sample.csv", { Papa.parse("/tests/long-sample.csv", {
@ -1130,6 +1223,7 @@ var CUSTOM_TESTS = [
{ {
description: "Step exposes cursor for workers", description: "Step exposes cursor for workers",
expected: [452, 452, 452, 865, 865, 865, 1209, 1209], expected: [452, 452, 452, 865, 865, 865, 1209, 1209],
disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = []; var updates = [];
Papa.parse("/tests/long-sample.csv", { Papa.parse("/tests/long-sample.csv", {
@ -1148,6 +1242,7 @@ var CUSTOM_TESTS = [
{ {
description: "Chunk is called for each chunk", description: "Chunk is called for each chunk",
expected: [3, 3, 2], expected: [3, 3, 2],
disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = []; var updates = [];
Papa.parse("/tests/long-sample.csv", { Papa.parse("/tests/long-sample.csv", {
@ -1165,6 +1260,7 @@ var CUSTOM_TESTS = [
{ {
description: "Chunk is called with cursor position", description: "Chunk is called with cursor position",
expected: [452, 865, 1209], expected: [452, 865, 1209],
disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = []; var updates = [];
Papa.parse("/tests/long-sample.csv", { Papa.parse("/tests/long-sample.csv", {
@ -1305,6 +1401,7 @@ var CUSTOM_TESTS = [
{ {
description: "Step functions can abort workers", description: "Step functions can abort workers",
expected: 1, expected: 1,
disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = 0; var updates = 0;
Papa.parse("/tests/long-sample.csv", { Papa.parse("/tests/long-sample.csv", {
@ -1324,6 +1421,7 @@ var CUSTOM_TESTS = [
{ {
description: "beforeFirstChunk manipulates only first chunk", description: "beforeFirstChunk manipulates only first chunk",
expected: 7, expected: 7,
disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = 0; var updates = 0;
Papa.parse("/tests/long-sample.csv", { Papa.parse("/tests/long-sample.csv", {
@ -1344,6 +1442,7 @@ var CUSTOM_TESTS = [
{ {
description: "First chunk not modified if beforeFirstChunk returns nothing", description: "First chunk not modified if beforeFirstChunk returns nothing",
expected: 8, expected: 8,
disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = 0; var updates = 0;
Papa.parse("/tests/long-sample.csv", { Papa.parse("/tests/long-sample.csv", {
@ -1359,6 +1458,52 @@ var CUSTOM_TESTS = [
} }
}); });
} }
},
{
description: "Should not assume we own the worker unless papaworker is in the search string",
disabled: typeof Worker === 'undefined',
expected: [false, true, true, true, true],
run: function(callback) {
var searchStrings = [
'',
'?papaworker',
'?x=1&papaworker',
'?x=1&papaworker&y=1',
'?x=1&papaworker=1'
];
var results = searchStrings.map(function () { return false; });
var workers = [];
// Give it .5s to do something
setTimeout(function () {
workers.forEach(function (w) { w.terminate(); });
callback(results);
}, 500);
searchStrings.forEach(function (searchString, idx) {
var w = new Worker('../papaparse.js' + searchString);
workers.push(w);
w.addEventListener('message', function () {
results[idx] = true;
});
w.postMessage({input: 'a,b,c\n1,2,3'});
});
}
} }
]; ];
describe('Custom Tests', function() {
function generateTest(test) {
(test.disabled ? it.skip : it)(test.description, function(done) {
test.run(function (actual) {
assert.deepEqual(JSON.stringify(actual), JSON.stringify(test.expected));
done();
});
});
}
for (var i = 0; i < CUSTOM_TESTS.length; i++) {
generateTest(CUSTOM_TESTS[i]);
}
});

448
tests/test-runner.js

@ -1,448 +0,0 @@
var passCount = 0;
var failCount = 0;
var testCount = 0;
$(function()
{
// First, wireup!
$('.results').on('click', 'td.rvl', function()
{
var tr = $(this).closest('tr');
if (tr.hasClass('collapsed'))
{
$('.revealer', tr).hide();
$('.hidden', tr).show();
$(this).html("-");
}
else
{
$('.revealer', tr).show();
$('.hidden', tr).hide();
$(this).html("+");
}
tr.toggleClass('collapsed expanded');
});
$('.expand-all').click(function()
{
var $testGroup = $(this).closest('.test-group');
$('.collapsed .rvl', $testGroup).click();
});
$('.collapse-all').click(function()
{
var $testGroup = $(this).closest('.test-group');
$('.expanded .rvl', $testGroup).click();
});
function asyncDone()
{
// Finally, show the overall status.
if (failCount == 0)
$('#status').addClass('status-pass').html("All <b>" + passCount + "</b> test" + (passCount == 1 ? "" : "s") + " passed");
else
$('#status').addClass('status-fail').html("<b>" + failCount + "</b> test" + (failCount == 1 ? "" : "s") + " failed; <b>" + passCount + "</b> passed");
}
// Next, run tests and render results!
runCoreParserTests();
runParseTests(asyncDone);
runUnparseTests();
runCustomTests(asyncDone);
});
// Executes all tests in CORE_PARSER_TESTS from test-cases.js
// and renders results in the table.
function runCoreParserTests()
{
for (var i = 0; i < CORE_PARSER_TESTS.length; i++)
{
var test = CORE_PARSER_TESTS[i];
var passed = runTest(test);
if (passed)
passCount++;
else
failCount++;
}
function runTest(test)
{
var actual = new Papa.Parser(test.config).parse(test.input);
var results = compare(actual.data, actual.errors, test.expected);
displayResults('#tests-for-core-parser', test, actual, results);
return results.data.passed && results.errors.passed
}
}
// Executes all tests in PARSE_TESTS from test-cases.js
// and renders results in the table.
function runParseTests(asyncDone)
{
for (var i = 0; i < PARSE_TESTS.length; i++)
{
var test = PARSE_TESTS[i];
var passed = runTest(test);
if (passed)
passCount++;
else
failCount++;
}
var asyncRemaining = 0;
PARSE_ASYNC_TESTS.forEach(function(test)
{
if (test.disabled)
return;
asyncRemaining++;
var config = test.config;
config.complete = function(actual)
{
var results = compare(actual.data, actual.errors, test.expected);
displayResults("#tests-for-parse", test, actual, results);
if (results.data.passed && results.errors.passed) {
passCount++;
} else {
failCount++;
}
if (--asyncRemaining === 0) {
asyncDone();
}
};
config.error = function(err)
{
failCount++;
displayResults("#tests-for-parse", test, {data:[],errors:err}, test.expected);
if (--asyncRemaining === 0) {
asyncDone();
}
};
Papa.parse(test.input, config);
});
function runTest(test)
{
var actual = Papa.parse(test.input, test.config);
var results = compare(actual.data, actual.errors, test.expected);
displayResults('#tests-for-parse', test, actual, results);
return results.data.passed && results.errors.passed
}
}
function displayResults(tableId, test, actual, results)
{
var testId = testCount++;
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.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>';
$(tableId+' .results').append(tr);
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
};
function compareData(actual, expected)
{
var passed = true;
if (actual.length != expected.length)
passed = false;
else
{
// The order is important, so we go through manually before using stringify to check everything else
for (var row = 0; row < expected.length; row++)
{
if (actual[row].length != expected[row].length)
{
passed = false;
break;
}
for (var col = 0; col < expected[row].length; col++)
{
var expectedVal = expected[row][col];
var actualVal = actual[row][col];
if (actualVal !== expectedVal)
{
passed = false;
break;
}
}
}
}
if (passed) // final check will catch any other differences
passed = JSON.stringify(actual) == JSON.stringify(expected);
// 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
};
}
}
// 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)
{
if (e instanceof Error) {
throw 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
};
}
}
// Executes all tests in CUSTOM_TESTS from test-cases.js
// and renders results in the table.
function runCustomTests(asyncDone)
{
var asyncRemaining = 0;
for (var i = 0; i < CUSTOM_TESTS.length; i++)
{
runTest(CUSTOM_TESTS[i]);
}
function runTest(test)
{
if (test.disabled)
return;
asyncRemaining++;
try
{
displayAsyncTest(test);
}
catch (e)
{
displayResults(test, e);
}
}
function displayAsyncTest(test)
{
var testId = testCount++;
test.testId = testId;
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>'
+ '<td class="status pending">pending</td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden">' + test.expected + '</div></td>'
+ '<td class="revealable pre"><div class="revealer">condensed</div><div class="hidden actual"></div></td>'
+ '</tr>';
$('#custom-tests .results').append(tr);
test.run(function(actual)
{
displayAsyncResults(test, actual);
});
setTimeout(function()
{
if (test.complete) return;
displayAsyncResults(test, '(incomplete)');
}, 2000);
}
function displayAsyncResults(test, actual)
{
var testId = test.testId;
if (test.complete)
{
asyncRemaining++;
actual = '(multiple results from test)';
}
test.complete = true;
var results = compare(actual, test.expected);
var tr = $('#test-'+testId);
tr.find('.actual').text(actual);
var status = $(passOrFailTd(results));
var oldStatus = tr.find('.status');
oldStatus.attr('class', status.attr('class'));
oldStatus.text(status.text());
if (!results.passed)
$('#test-' + testId + ' td.rvl').click();
if (results.passed)
passCount++;
else
failCount++;
if (--asyncRemaining === 0)
asyncDone();
}
function compare(actual, expected)
{
return {
passed: JSON.stringify(actual) === JSON.stringify(expected)
};
}
}
// Makes a TD tag with OK or FAIL depending on test result
function passOrFailTd(result)
{
if (result.passed)
return '<td class="status ok">OK</td>';
else
return '<td class="status fail">FAIL</td>';
}
// Reveals some hidden, whitespace, or invisible characters
function revealChars(txt)
{
if (typeof txt != 'string')
return '(file)';
// 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');
// 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;
}

14
tests/test.js

@ -1,11 +1,19 @@
require('./node-tests.js');
var connect = require('connect'); var connect = require('connect');
var serveStatic = require('serve-static'); var serveStatic = require('serve-static');
var open = require('open'); var open = require('open');
var path = require('path'); var path = require('path');
var child_process = require('child_process');
var server = connect().use(serveStatic(path.join(__dirname, '/..'))).listen(8071, function() {
if (process.argv.indexOf('--phantomjs') !== -1) {
child_process.spawn('node_modules/.bin/mocha-phantomjs', ['http://localhost:8071/tests/tests.html'], {
stdio: 'inherit'
}).on('exit', function () {
server.close();
});
connect().use(serveStatic(path.join(__dirname, '/..'))).listen(8071, function() { } else {
open('http://localhost:8071/tests/tests.html'); open('http://localhost:8071/tests/tests.html');
console.log('Serving tests...'); console.log('Serving tests...');
}
}); });

173
tests/tests.css

@ -1,173 +0,0 @@
/* Eric Meyer's Reset CSS v2.0 */
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0}
body {
background: #F0F0F0;
font: 14px 'Helvetica Neue', sans-serif;
color: #333;
padding: 30px 15px;
}
a {
color: rgb(0, 142, 236);
}
a:hover {
color: rgb(82, 186, 255);
}
b {
font-weight: bold;
}
i {
font-style: italic;
}
h1 {
text-align: center;
font-weight: bold;
font-size: 62px;
margin-bottom: 30px;
}
h2 {
text-align: center;
font-weight: bold;
font-size: 26px;
margin-bottom: 20px;
}
.status-pass,
.status-fail {
padding: 10px;
margin-bottom: 30px;
color: #FFF;
text-align: center;
text-transform: uppercase;
font-size: 18px;
letter-spacing: 1px;
font-weight: 100;
}
.status-pass {
background: rgb(3, 168, 3);
}
.status-fail {
background: #BB0000;
}
.test-group {
margin-bottom: 50px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 5px;
}
table th,
table td {
padding: 5px;
border: 1px solid #BBB;
}
table th {
color: #000;
background: #DDD;
font-weight: bold;
padding: 10px 5px;
text-transform: uppercase;
}
table td {
background: #FFF;
color: #555;
font-size: 14px;
}
td.status {
text-transform: uppercase;
font-weight: 300;
vertical-align: middle;
text-align: center;
width: 80px;
}
td.ok {
background: rgb(204, 250, 144);
}
td.fail {
background: rgb(255, 192, 192);
}
td.pending {
background: rgb(255, 255, 150);
}
td.rvl {
background: #444;
color: #999;
vertical-align: middle;
text-align: center;
cursor: pointer;
width: 20px;
}
td.rvl:hover {
color: #FFF;
}
tr.collapsed td.revealable {
background: #ECECEC;
vertical-align: middle;
text-align: center;
font-family: 'Helvetica Neue', sans-serif;
text-transform: lowercase;
color: #AAA;
}
tr.expanded .revealer {
font-family: 'Helvetica Neue', sans-serif;
text-transform: lowercase;
font-size: 10px;
background: #FFF;
position: absolute;
display: block;
padding: 3px;
top: -5px;
right: -5px;
}
td .container {
position: relative;
}
.notes {
color: #888;
font-size: 12px;
}
.pre {
font-family: Menlo, Monaco, monospace;
white-space: pre-wrap;
}
td.pre {
font-size: 12px;
}
.hidden {
display: none;
}
.special-char {
color: #78B7E7;
}
.whitespace-char {
background: #D5FCFA;
}

120
tests/tests.html

@ -1,113 +1,29 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<title>Papa Parse Tests</title> <title>Papa Parse Tests</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="stylesheet" href="tests.css"> <link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script> <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="../papaparse.js"></script> <script src="../papaparse.js"></script>
<script src="http://chaijs.com/chai.js"></script>
<script src="../node_modules/mocha/mocha.js"></script>
<script>mocha.setup('bdd')</script>
<script src="test-cases.js"></script> <script src="test-cases.js"></script>
<script src="test-runner.js"></script>
</head> </head>
<body> <body>
<h1>Papa Parse Tests</h1> <div id="mocha"></div>
<div id="status"></div> <script>
if (window.mochaPhantomJS) {
mochaPhantomJS.run();
<div class="test-group" id="tests-for-core-parser">
<h2>Core Parser Tests</h2> } else {
mocha.checkLeaks();
<a href="javascript:" class="expand-all">Expand all</a> mocha.globals(['jQuery']);
&middot; mocha.run();
<a href="javascript:" class="collapse-all">Collapse all</a> }
<br> </script>
<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-parse">
<h2>Papa.parse() Wrapper Tests</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>Papa.unparse() Tests</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>
<div class="test-group" id="custom-tests">
<h2>Miscellaneous Tests</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>Expected</th>
<th>Actual</th>
</tr>
</table>
</div>
</body> </body>
</html> </html>

Loading…
Cancel
Save