JavaScript Csv file generator, part 1 of 3
This is part 1 of the JavaScript Csv file generator series.
I came across the need to generate CSV files locally using JavaScript, and set out to create a simple tool for that. It should be small, simple and should just get the job done.
I would like to be able to use the CSV generator something like this:
/// Requirement code vision
var propertyOrder = ["name", "age", "height"];
var csv = new Csv(propertyOrder);
csv.add({ name : "Anders",
age : 38,
height : "178cm" });
csv.add({ name : "John Doe",
age : 50,
height : "184cm" });
csv.saveAs("people.csv");
First things first, so let's start with some unit tests. I use the unit test framework that I have covered in earler posts.
Requirements
- The only parameter for the constructor is the order of the properties. This order should be saved.
- The
add
method should add one item to the list of items to export. - For this purpose, the Csv object should contain an
items
property, containing all added items. - The
saveAs
method should use thewindow.saveAs
function, requiring a FileSaver shim to be in place. - Mostly for testing purposes, the text content of the file to be generated should be accessible through the
getFileContents
method. - When calling
saveAs
orgetFileContents
, I should be able to specify which field separator to use. The default should be a comma. - When calling
saveAs
orgetFileContents
, I should be able to have the property names added automatically as a header row. The default should be not to. - For interoperability purposes, the saved file should contain the correct Byte Order Mark.
/// Tests
engine.add("Csv constructor should save properties order",
function(testContext, the) {
// Arrange
var properties = ["a", "b"];
// Act
var csv = new Csv(properties);
// Assert
the("propertyOrder").propertyOf(csv).shouldBeSameArrayAs(properties);
});
engine.add("Csv constructor should initiate the items property",
function(testContext, the) {
// Arrange
var properties = ["a", "b"];
// Act
var csv = new Csv(properties);
// Assert
the("items").propertyOf(csv).shouldBeArray();
the("length").propertyOf(csv.items).shouldBeExactly(0);
});
engine.add("Csv.add should add one item",
function(testContext, the) {
// Arrange
var properties = ["a", "b"];
var csv = new Csv(properties);
// Act
csv.add({"a" : 1, "b" : 2});
// Assert
the("length").propertyOf(csv.items).shouldBeExactly(1);
the("a").propertyOf(csv.items[0]).shouldBeExactly(1);
the("b").propertyOf(csv.items[0]).shouldBeExactly(2);
});
engine.add("Csv.getFileContents should create file correctly",
function(testContext, the) {
// Arrange
var properties = ["a", "b"];
var csv = new Csv(properties);
csv.add({"a" : "Abc", "b" : "Def"});
csv.add({"a" : "Ghi", "b" : "Jkl"});
// Act
var file = csv.getFileContents();
// Assert
the(file).shouldBeSameAs("Abc,Def\r\nGhi,Jkl");
});
engine.add("Csv.getFileContents(separator) should create file correctly",
function(testContext, the) {
// Arrange
var properties = ["a", "b"];
var csv = new Csv(properties);
csv.add({"a" : "Abc", "b" : "Def"});
csv.add({"a" : "Ghi", "b" : "Jkl"});
// Act
var file = csv.getFileContents(";");
// Assert
the(file).shouldBeSameAs("Abc;Def\r\nGhi;Jkl");
});
engine.add("Csv.getFileContents(separator, true) should create file correctly",
function(testContext, the) {
// Arrange
var properties = ["a", "b"];
var csv = new Csv(properties);
csv.add({"a" : "Abc", "b" : "Def"});
csv.add({"a" : "Ghi", "b" : "Jkl"});
// Act
var file = csv.getFileContents(";", true);
// Assert
the(file).shouldBeSameAs("a;b\r\nAbc;Def\r\nGhi;Jkl");
});
engine.add("Csv.saveAs should call window.saveAs correctly",
function(testContext, the) {
// Arrange
var properties = ["a", "b"];
var csv = new Csv(properties);
csv.add({"a" : "Abc", "b" : "Def"});
csv.add({"a" : "Ghi", "b" : "Jkl"});
var saveAsCalled = false, savedBlob, savedFilename;
var mockSaveAs = function(blob, filename) {
saveAsCalled = true;
savedBlob = blob;
savedFilename = filename;
};
var oldSaveAs = window.saveAs;
window.saveAs = mockSaveAs;
// Act
csv.saveAs("output.csv", ";", false);
// Cleanup
window.saveAs = oldSaveAs;
// Assert
the(saveAsCalled).shouldBeTrue();
the(savedBlob).shouldBeInstanceOf(window.Blob);
the(savedFilename).shouldBeSameAs("output.csv");
});
engine.add("Csv.saveAs should add UTF-8 Byte Order Mark to beginning of file",
function(testContext, the) {
// Arrange
var properties = ["a", "b"];
var csv = new Csv(properties);
csv.add({"a" : "Abc", "b" : "Def"});
csv.add({"a" : "Ghi", "b" : "Jkl"});
// Mock saveAs function to store the blob that was created
var savedBlob;
var mockSaveAs = function(blob, filename) {
savedBlob = blob;
};
var oldSaveAs = window.saveAs;
window.saveAs = mockSaveAs;
// Act
csv.saveAs("output.csv", ";", false);
// Cleanup
window.saveAs = oldSaveAs;
// Assert (reading from a Blob is done using FileReader, which is asynchronous)
var bom;
testContext.actAndWait(1000, function(testContext, the) {
var firstThreeBytes = savedBlob.slice(0, 3);
var reader = new window.FileReader();
reader.addEventListener("loadend", function() {
bom = reader.result;
testContext.actDone();
});
reader.readAsArrayBuffer(firstThreeBytes);
}).thenAssert(function(testContext, the) {
the(bom).shouldBeSameArrayAs([0xef, 0xbb, 0xbf]);
});
});
The next episode will be about implementing the Csv
class.
The latest version of the code is always available in the GitHub repository.
Articles in this series: