JavaScript Csv file generator, part 1

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 the window.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 or getFileContents, I should be able to specify which field separator to use. The default should be a comma.
  • When calling saveAs or getFileContents, 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 Csv. EDIT: The finished code can be found on github at /lbrtw/csv.

Posted by Anders Tornblad on Category JavaScript Labels
Tweet this