fs_test.js

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('goog.fsTest');
goog.setTestOnly('goog.fsTest');

goog.require('goog.array');
goog.require('goog.async.Deferred');
goog.require('goog.async.DeferredList');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.fs');
goog.require('goog.fs.DirectoryEntry');
goog.require('goog.fs.Error');
goog.require('goog.fs.FileReader');
goog.require('goog.fs.FileSaver');
goog.require('goog.string');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');

var TEST_DIR = 'goog-fs-test-dir';

var fsExists = goog.isDef(goog.global.requestFileSystem) ||
    goog.isDef(goog.global.webkitRequestFileSystem);
var deferredFs = fsExists ? goog.fs.getTemporary() : null;
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall();
asyncTestCase.stepTimeout = 5000;
var stubs = new goog.testing.PropertyReplacer();

function setUpPage() {
  if (!fsExists) {
    return;
  }

  loadTestDir().addErrback(function(err) {
    var msg;
    if (err.code == goog.fs.Error.ErrorCode.QUOTA_EXCEEDED) {
      msg = err.message + '. If you\'re using Chrome, you probably need to ' +
          'pass --unlimited-quota-for-files on the command line.';
    } else if (err.code == goog.fs.Error.ErrorCode.SECURITY &&
               window.location.href.match(/^file:/)) {
      msg = err.message + '. file:// URLs can\'t access the filesystem API.';
    } else {
      msg = err.message;
    }
    var body = goog.dom.getDocument().body;
    goog.dom.insertSiblingBefore(
        goog.dom.createDom('h1', {}, msg), body.childNodes[0]);
  });
}

function tearDown() {
  if (!fsExists) {
    return;
  }

  loadTestDir().
      addCallback(function(dir) { return dir.removeRecursively(); }).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('removing filesystem');
}

function testUnavailableTemporaryFilesystem() {
  stubs.set(goog.global, 'requestFileSystem', null);
  stubs.set(goog.global, 'webkitRequestFileSystem', null);
  asyncTestCase.waitForAsync('testUnavailableTemporaryFilesystem');

  goog.fs.getTemporary(1024).addErrback(function(e) {
    assertEquals('File API unsupported', e.message);
    continueTesting();
  });
}


function testUnavailablePersistentFilesystem() {
  stubs.set(goog.global, 'requestFileSystem', null);
  stubs.set(goog.global, 'webkitRequestFileSystem', null);
  asyncTestCase.waitForAsync('testUnavailablePersistentFilesystem');

  goog.fs.getPersistent(2048).addErrback(function(e) {
    assertEquals('File API unsupported', e.message);
    continueTesting();
  });
}


function testIsFile() {
  if (!fsExists) {
    return;
  }

  loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE).
      addCallback(function(fileEntry) {
        assertFalse(fileEntry.isDirectory());
        assertTrue(fileEntry.isFile());
      }).addBoth(continueTesting);
  asyncTestCase.waitForAsync('testIsFile');
}

function testIsDirectory() {
  if (!fsExists) {
    return;
  }

  loadDirectory('test', goog.fs.DirectoryEntry.Behavior.CREATE).
      addCallback(function(fileEntry) {
        assertTrue(fileEntry.isDirectory());
        assertFalse(fileEntry.isFile());
      }).addBoth(continueTesting);
  asyncTestCase.waitForAsync('testIsDirectory');
}

function testReadFileUtf16() {
  if (!fsExists) {
    return;
  }
  var str = 'test content';
  var buf = new ArrayBuffer(str.length * 2);
  var arr = new Uint16Array(buf);
  for (var i = 0; i < str.length; i++) {
    arr[i] = str.charCodeAt(i);
  }

  loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE).
      addCallback(goog.partial(writeToFile, arr.buffer)).
      addCallback(goog.partial(checkFileContentWithEncoding, str, 'UTF-16')).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testReadFile');
}

function testReadFileUtf8() {
  if (!fsExists) {
    return;
  }
  var str = 'test content';
  var buf = new ArrayBuffer(str.length);
  var arr = new Uint8Array(buf);
  for (var i = 0; i < str.length; i++) {
    arr[i] = str.charCodeAt(i) & 0xff;
  }

  loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE).
      addCallback(goog.partial(writeToFile, arr.buffer)).
      addCallback(goog.partial(checkFileContentWithEncoding, str, 'UTF-8')).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testReadFileUtf8');
}

function testReadFileAsArrayBuffer() {
  if (!fsExists) {
    return;
  }
  var str = 'test content';
  var buf = new ArrayBuffer(str.length);
  var arr = new Uint8Array(buf);
  for (var i = 0; i < str.length; i++) {
    arr[i] = str.charCodeAt(i) & 0xff;
  }

  loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE).
      addCallback(goog.partial(writeToFile, arr.buffer)).
      addCallback(goog.partial(checkFileContentAs, arr.buffer, 'ArrayBuffer',
          undefined)).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testReadFileAsArrayBuffer');
}

function testReadFileAsBinaryString() {
  if (!fsExists) {
    return;
  }
  var str = 'test content';
  var buf = new ArrayBuffer(str.length);
  var arr = new Uint8Array(buf);
  for (var i = 0; i < str.length; i++) {
    arr[i] = str.charCodeAt(i);
  }

  loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE).
      addCallback(goog.partial(writeToFile, arr.buffer)).
      addCallback(goog.partial(checkFileContentAs, str, 'BinaryString',
          undefined)).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testReadFileAsArrayBuffer');
}

function testWriteFile() {
  if (!fsExists) {
    return;
  }

  loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE).
      addCallback(goog.partial(writeToFile, 'test content')).
      addCallback(goog.partial(checkFileContent, 'test content')).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testWriteFile');
}

function testRemoveFile() {
  if (!fsExists) {
    return;
  }

  loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE).
      addCallback(goog.partial(writeToFile, 'test content')).
      addCallback(function(file) { return file.remove(); }).
      addCallback(goog.partial(checkFileRemoved, 'test')).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testRemoveFile');
}

function testMoveFile() {
  if (!fsExists) {
    return;
  }

  var deferredSubdir = loadDirectory(
      'subdir', goog.fs.DirectoryEntry.Behavior.CREATE);
  var deferredWrittenFile =
      loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE).
      addCallback(goog.partial(writeToFile, 'test content'));
  goog.async.DeferredList.gatherResults([deferredSubdir, deferredWrittenFile]).
      addCallback(splitArgs(function(dir, file) { return file.moveTo(dir); })).
      addCallback(goog.partial(checkFileContent, 'test content')).
      addCallback(goog.partial(checkFileRemoved, 'test')).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testMoveFile');
}

function testCopyFile() {
  if (!fsExists) {
    return;
  }

  var deferredFile = loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE);
  var deferredSubdir = loadDirectory(
      'subdir', goog.fs.DirectoryEntry.Behavior.CREATE);
  var deferredWrittenFile = deferredFile.branch().
      addCallback(goog.partial(writeToFile, 'test content'));
  goog.async.DeferredList.gatherResults([deferredSubdir, deferredWrittenFile]).
      addCallback(splitArgs(function(dir, file) { return file.copyTo(dir); })).
      addCallback(goog.partial(checkFileContent, 'test content')).
      awaitDeferred(deferredFile).
      addCallback(goog.partial(checkFileContent, 'test content')).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testMoveFile');
}

function testAbortWrite() {
  // TODO(nicksantos): This test is broken in newer versions of chrome.
  // We don't know why yet.
  if (true) return;

  if (!fsExists) {
    return;
  }

  var deferredFile = loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE);
  deferredFile.branch().
      addCallback(goog.partial(startWrite, 'test content')).
      addCallback(function(writer) { writer.abort(); }).
      addCallback(
          goog.partial(waitForEvent, goog.fs.FileSaver.EventType.ABORT)).
      awaitDeferred(deferredFile).
      addCallback(goog.partial(checkFileContent, '')).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testWriteFile');
}

function testSeek() {
  if (!fsExists) {
    return;
  }

  var deferredFile = loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE);
  deferredFile.branch().
      addCallback(goog.partial(writeToFile, 'test content')).
      addCallback(function(file) { return file.createWriter(); }).
      addCallback(
          goog.partial(checkReadyState, goog.fs.FileSaver.ReadyState.INIT)).
      addCallback(function(writer) {
        writer.seek(5);
        writer.write(goog.fs.getBlob('stuff and things'));
      }).
      addCallback(
          goog.partial(checkReadyState, goog.fs.FileSaver.ReadyState.WRITING)).
      addCallback(
          goog.partial(waitForEvent, goog.fs.FileSaver.EventType.WRITE)).
      awaitDeferred(deferredFile).
      addCallback(goog.partial(checkFileContent, 'test stuff and things')).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testWriteFile');
}

function testTruncate() {
  if (!fsExists) {
    return;
  }

  var deferredFile = loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE);
  deferredFile.branch().
      addCallback(goog.partial(writeToFile, 'test content')).
      addCallback(function(file) { return file.createWriter(); }).
      addCallback(
          goog.partial(checkReadyState, goog.fs.FileSaver.ReadyState.INIT)).
      addCallback(function(writer) { writer.truncate(4); }).
      addCallback(
          goog.partial(checkReadyState, goog.fs.FileSaver.ReadyState.WRITING)).
      addCallback(
          goog.partial(waitForEvent, goog.fs.FileSaver.EventType.WRITE)).
      awaitDeferred(deferredFile).
      addCallback(goog.partial(checkFileContent, 'test')).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testWriteFile');
}

function testGetLastModified() {
  if (!fsExists) {
    return;
  }
  var now = goog.now();
  loadFile('test', goog.fs.DirectoryEntry.Behavior.CREATE).
      addCallback(function(entry) {
        return entry.getLastModified();
      }).addCallback(function(date) {
        assertRoughlyEquals('Expected the last modified date to be within ' +
                            'a few milliseconds of the test start time.',
                            now, date.getTime(), 2000);
      }).addCallback(continueTesting);

  asyncTestCase.waitForAsync('testGetLastModified');
}

function testCreatePath() {
  if (!fsExists) {
    return;
  }

  loadTestDir().
      addCallback(function(testDir) {
        return testDir.createPath('foo');
      }).
      addCallback(function(fooDir) {
        assertEquals('/goog-fs-test-dir/foo', fooDir.getFullPath());
        return fooDir.createPath('bar/baz/bat');
      }).
      addCallback(function(batDir) {
        assertEquals('/goog-fs-test-dir/foo/bar/baz/bat', batDir.getFullPath());
      }).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testCreatePath');
}

function testCreateAbsolutePath() {
  if (!fsExists) {
    return;
  }

  loadTestDir().
      addCallback(function(testDir) {
        return testDir.createPath('/' + TEST_DIR + '/fee/fi/fo/fum');
      }).
      addCallback(function(absDir) {
        assertEquals('/goog-fs-test-dir/fee/fi/fo/fum', absDir.getFullPath());
      }).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testCreateAbsolutePath');
}

function testCreateRelativePath() {
  if (!fsExists) {
    return;
  }

  loadTestDir().
      addCallback(function(dir) {
        return dir.createPath('../' + TEST_DIR + '/dir');
      }).
      addCallback(function(relDir) {
        assertEquals('/goog-fs-test-dir/dir', relDir.getFullPath());
        return relDir.createPath('.');
      }).
      addCallback(function(sameDir) {
        assertEquals('/goog-fs-test-dir/dir', sameDir.getFullPath());
        return sameDir.createPath('./././.');
      }).
      addCallback(function(reallySameDir) {
        assertEquals('/goog-fs-test-dir/dir', reallySameDir.getFullPath());
        return reallySameDir.createPath('./new/../..//dir/./new////.');
      }).
      addCallback(function(newDir) {
        assertEquals('/goog-fs-test-dir/dir/new', newDir.getFullPath());
      }).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testCreateRelativePath');

}

function testCreateBadPath() {
  if (!fsExists) {
    return;
  }

  loadTestDir().
      awaitDeferred(loadTestDir()).
      addCallback(function(dir) {
        // There is only one layer of parent directory from the test dir.
        return dir.createPath('../../../../' + TEST_DIR + '/baz/bat');
      }).
      addCallback(function(batDir) {
        assertEquals('The parent directory of the root directory should ' +
                     'point back to the root directory.',
                     '/goog-fs-test-dir/baz/bat', batDir.getFullPath());
      }).

      awaitDeferred(loadTestDir()).
      addCallback(function(dir) {
        // An empty path should return the same as the input directory.
        return dir.createPath('');
      }).
      addCallback(function(testDir) {
        assertEquals('/goog-fs-test-dir', testDir.getFullPath());
      }).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testCreateBadPath');
}

function testGetAbsolutePaths() {
  if (!fsExists) {
    return;
  }

  loadFile('foo', goog.fs.DirectoryEntry.Behavior.CREATE).
      awaitDeferred(loadTestDir()).
      addCallback(function(testDir) {
        return testDir.getDirectory('/');
      }).
      addCallback(function(root) {
        assertEquals('/', root.getFullPath());
        return root.getDirectory('/' + TEST_DIR);
      }).
      addCallback(function(testDir) {
        assertEquals('/goog-fs-test-dir', testDir.getFullPath());
        return testDir.getDirectory('//' + TEST_DIR + '////');
      }).
      addCallback(function(testDir) {
        assertEquals('/goog-fs-test-dir', testDir.getFullPath());
        return testDir.getDirectory('////');
      }).
      addCallback(function(testDir) {
        assertEquals('/', testDir.getFullPath());
      }).
      addBoth(continueTesting);
  asyncTestCase.waitForAsync('testGetAbsolutePaths');
}


function continueTesting(result) {
  asyncTestCase.continueTesting();
  if (result instanceof Error) {
    throw result;
  }
}

function testListEmptyDirectory() {
  if (!fsExists) {
    return;
  }

  loadTestDir().
      addCallback(function(dir) { return dir.listDirectory(); }).
      addCallback(function(entries) { assertArrayEquals([], entries); }).
      addCallback(continueTesting);
  asyncTestCase.waitForAsync('testListEmptyDirectory');
}


function testListDirectory() {
  if (!fsExists) {
    return;
  }

  goog.async.Deferred.succeed().
      // Create the test directory and test entries.
      awaitDeferred(
          loadDirectory('testDir', goog.fs.DirectoryEntry.Behavior.CREATE)).
      awaitDeferred(
          loadFile('testFile', goog.fs.DirectoryEntry.Behavior.CREATE)).
      awaitDeferred(loadTestDir()).

      // Verify the contents of the directory listing.
      addCallback(function(testDir) { return testDir.listDirectory(); }).
      addCallback(function(entries) {
        assertEquals(2, entries.length);

        var dir = goog.array.find(entries, function(entry) {
          return entry.getName() == 'testDir';
        });
        assertNotNull(dir);
        assertTrue(dir.isDirectory());

        var file = goog.array.find(entries, function(entry) {
          return entry.getName() == 'testFile';
        });
        assertNotNull(file);
        assertTrue(file.isFile());
      }).
      addCallback(continueTesting);

  asyncTestCase.waitForAsync('testListDirectory');
}


function testListBigDirectory() {
  // TODO(nicksantos): This test is broken in newer versions of chrome.
  // We don't know why yet.
  if (true) return;

  if (!fsExists) {
    return;
  }

  function getFileName(i) {
    return 'file' + goog.string.padNumber(i, String(count).length);
  }

  // NOTE: This was intended to verify that the results from repeated
  // DirectoryReader.readEntries() callbacks are appropriately concatenated.
  // In current versions of Chrome (March 2011), all results are returned in the
  // first callback regardless of directory size. The count can be increased in
  // the future to test batched result lists once they are implemented.
  var count = 100;

  var expectedNames = [];

  var def = goog.async.Deferred.succeed();
  for (var i = 0; i < count; i++) {
    var name = getFileName(i);
    expectedNames.push(name);

    def.awaitDeferred(
        loadFile(name, goog.fs.DirectoryEntry.Behavior.CREATE));
  }

  def.awaitDeferred(loadTestDir()).
      addCallback(function(testDir) { return testDir.listDirectory(); }).
      addCallback(function(entries) {
        assertEquals(count, entries.length);

        assertSameElements(expectedNames,
                           goog.array.map(entries, function(entry) {
                             return entry.getName();
                           }));
        assertTrue(goog.array.every(entries, function(entry) {
          return entry.isFile();
        }));
      }).
      addCallback(continueTesting);

  asyncTestCase.waitForAsync('testListBigDirectory');
}


function testSliceBlob() {
  // A mock blob object whose slice returns the parameters it was called with.
  var blob = {
    'size': 10,
    'slice': function(start, end) {
      return [start, end];
    }
  };

  // Simulate Firefox 13 that implements the new slice.
  var tmpStubs = new goog.testing.PropertyReplacer();
  tmpStubs.set(goog.userAgent, 'GECKO', true);
  tmpStubs.set(goog.userAgent, 'WEBKIT', false);
  tmpStubs.set(goog.userAgent, 'IE', false);
  tmpStubs.set(goog.userAgent, 'VERSION', '13.0');
  tmpStubs.set(goog.userAgent, 'isVersionOrHigherCache_', {});

  // Expect slice to be called with no change to parameters
  assertArrayEquals([2, 10], goog.fs.sliceBlob(blob, 2));
  assertArrayEquals([-2, 10], goog.fs.sliceBlob(blob, -2));
  assertArrayEquals([3, 6], goog.fs.sliceBlob(blob, 3, 6));
  assertArrayEquals([3, -6], goog.fs.sliceBlob(blob, 3, -6));

  // Simulate IE 10 that implements the new slice.
  var tmpStubs = new goog.testing.PropertyReplacer();
  tmpStubs.set(goog.userAgent, 'GECKO', false);
  tmpStubs.set(goog.userAgent, 'WEBKIT', false);
  tmpStubs.set(goog.userAgent, 'IE', true);
  tmpStubs.set(goog.userAgent, 'VERSION', '10.0');
  tmpStubs.set(goog.userAgent, 'isVersionOrHigherCache_', {});

  // Expect slice to be called with no change to parameters
  assertArrayEquals([2, 10], goog.fs.sliceBlob(blob, 2));
  assertArrayEquals([-2, 10], goog.fs.sliceBlob(blob, -2));
  assertArrayEquals([3, 6], goog.fs.sliceBlob(blob, 3, 6));
  assertArrayEquals([3, -6], goog.fs.sliceBlob(blob, 3, -6));

  // Simulate Firefox 4 that implements the old slice.
  tmpStubs.set(goog.userAgent, 'GECKO', true);
  tmpStubs.set(goog.userAgent, 'WEBKIT', false);
  tmpStubs.set(goog.userAgent, 'IE', false);
  tmpStubs.set(goog.userAgent, 'VERSION', '2.0');
  tmpStubs.set(goog.userAgent, 'isVersionOrHigherCache_', {});

  // Expect slice to be called with transformed parameters.
  assertArrayEquals([2, 8], goog.fs.sliceBlob(blob, 2));
  assertArrayEquals([8, 2], goog.fs.sliceBlob(blob, -2));
  assertArrayEquals([3, 3], goog.fs.sliceBlob(blob, 3, 6));
  assertArrayEquals([3, 1], goog.fs.sliceBlob(blob, 3, -6));

  // Simulate Firefox 5 that implements mozSlice (new spec).
  delete blob.slice;
  blob.mozSlice = function(start, end) {
    return ['moz', start, end];
  };
  tmpStubs.set(goog.userAgent, 'GECKO', true);
  tmpStubs.set(goog.userAgent, 'WEBKIT', false);
  tmpStubs.set(goog.userAgent, 'IE', false);
  tmpStubs.set(goog.userAgent, 'VERSION', '5.0');
  tmpStubs.set(goog.userAgent, 'isVersionOrHigherCache_', {});

  // Expect mozSlice to be called with no change to parameters.
  assertArrayEquals(['moz', 2, 10], goog.fs.sliceBlob(blob, 2));
  assertArrayEquals(['moz', -2, 10], goog.fs.sliceBlob(blob, -2));
  assertArrayEquals(['moz', 3, 6], goog.fs.sliceBlob(blob, 3, 6));
  assertArrayEquals(['moz', 3, -6], goog.fs.sliceBlob(blob, 3, -6));

  // Simulate Chrome 20 that implements webkitSlice (new spec).
  delete blob.mozSlice;
  blob.webkitSlice = function(start, end) {
    return ['webkit', start, end];
  };
  tmpStubs.set(goog.userAgent, 'GECKO', false);
  tmpStubs.set(goog.userAgent, 'WEBKIT', true);
  tmpStubs.set(goog.userAgent, 'IE', false);
  tmpStubs.set(goog.userAgent, 'VERSION', '536.10');
  tmpStubs.set(goog.userAgent, 'isVersionOrHigherCache_', {});

  // Expect webkitSlice to be called with no change to parameters.
  assertArrayEquals(['webkit', 2, 10], goog.fs.sliceBlob(blob, 2));
  assertArrayEquals(['webkit', -2, 10], goog.fs.sliceBlob(blob, -2));
  assertArrayEquals(['webkit', 3, 6], goog.fs.sliceBlob(blob, 3, 6));
  assertArrayEquals(['webkit', 3, -6], goog.fs.sliceBlob(blob, 3, -6));

  tmpStubs.reset();
}


function testBrowserSupportsObjectUrls() {
  stubs.remove(goog.global, 'URL');
  stubs.remove(goog.global, 'webkitURL');
  stubs.remove(goog.global, 'createObjectURL');

  assertFalse(goog.fs.browserSupportsObjectUrls());
  try {
    goog.fs.createObjectUrl();
    fail();
  } catch (e) {
    assertEquals('This browser doesn\'t seem to support blob URLs', e.message);
  }

  var objectUrl = {};
  function createObjectURL() { return objectUrl; }
  stubs.set(goog.global, 'createObjectURL', createObjectURL);

  assertTrue(goog.fs.browserSupportsObjectUrls());
  assertEquals(objectUrl, goog.fs.createObjectUrl());

  stubs.reset();
}


function testGetBlobThrowsError() {
  stubs.remove(goog.global, 'BlobBuilder');
  stubs.remove(goog.global, 'WebKitBlobBuilder');
  stubs.remove(goog.global, 'Blob');

  try {
    goog.fs.getBlob();
    fail();
  } catch (e) {
    assertEquals('This browser doesn\'t seem to support creating Blobs',
        e.message);
  }

  stubs.reset();
}


function testGetBlobWithProperties() {
  // Skip test if browser doesn't support Blob API.
  if (typeof(goog.global.Blob) != 'function') {
    return;
  }

  var blob = goog.fs.getBlobWithProperties(['test'], 'text/test', 'native');
  assertEquals('text/test', blob.type);
}


function testGetBlobWithPropertiesThrowsError() {
  stubs.remove(goog.global, 'BlobBuilder');
  stubs.remove(goog.global, 'WebKitBlobBuilder');
  stubs.remove(goog.global, 'Blob');

  try {
    goog.fs.getBlobWithProperties();
    fail();
  } catch (e) {
    assertEquals('This browser doesn\'t seem to support creating Blobs',
        e.message);
  }

  stubs.reset();
}


function testGetBlobWithPropertiesUsingBlobBuilder() {
  function BlobBuilder() {
    this.parts = [];
    this.append = function(value, endings) {
      this.parts.push({value: value, endings: endings});
    };
    this.getBlob = function(type) {
      return {type: type, builder: this};
    };
  }
  stubs.set(goog.global, 'BlobBuilder', BlobBuilder);

  var blob = goog.fs.getBlobWithProperties(['test'], 'text/test', 'native');
  assertEquals('text/test', blob.type);
  assertEquals('test', blob.builder.parts[0].value);
  assertEquals('native', blob.builder.parts[0].endings);

  stubs.reset();
}


function loadTestDir() {
  return deferredFs.branch().addCallback(function(fs) {
    return fs.getRoot().getDirectory(
        TEST_DIR, goog.fs.DirectoryEntry.Behavior.CREATE);
  });
}

function loadFile(filename, behavior) {
  return loadTestDir().addCallback(function(dir) {
    return dir.getFile(filename, behavior);
  });
}

function loadDirectory(filename, behavior) {
  return loadTestDir().addCallback(function(dir) {
    return dir.getDirectory(filename, behavior);
  });
}

function startWrite(content, file) {
  return file.createWriter().
      addCallback(
          goog.partial(checkReadyState, goog.fs.FileSaver.ReadyState.INIT)).
      addCallback(function(writer) {
        writer.write(goog.fs.getBlob(content));
        return writer;
      }).
      addCallback(
          goog.partial(checkReadyState, goog.fs.FileSaver.ReadyState.WRITING));
}

function waitForEvent(type, target) {
  var d = new goog.async.Deferred();
  goog.events.listenOnce(target, type, d.callback, false, d);
  return d;
}

function writeToFile(content, file) {
  return startWrite(content, file).
      addCallback(
          goog.partial(waitForEvent, goog.fs.FileSaver.EventType.WRITE)).
      addCallback(function() { return file; });
}

function checkFileContent(content, file) {
  return checkFileContentAs(content, 'Text', undefined, file);
}

function checkFileContentWithEncoding(content, encoding, file) {
  checkFileContentAs(content, 'Text', encoding, file);
}

function checkFileContentAs(content, filetype, encoding, file) {
  return file.file().
      addCallback(function(blob) {
        return goog.fs.FileReader['readAs' + filetype](blob, encoding);
      }).
      addCallback(goog.partial(checkEquals, content));
}

function checkEquals(a, b) {
  if (a instanceof ArrayBuffer && b instanceof ArrayBuffer) {
    assertEquals(a.byteLength, b.byteLength);
    var viewA = new DataView(a);
    var viewB = new DataView(b);
    for (var i = 0; i < a.byteLength; i++) {
      assertEquals(viewA.getUint8(i), viewB.getUint8(i));
    }
  } else {
    assertEquals(a, b);
  }
}

function checkFileRemoved(filename) {
  return loadFile(filename).
      addCallback(goog.partial(fail, 'expected file to be removed')).
      addErrback(function(err) {
        assertEquals(err.code, goog.fs.Error.ErrorCode.NOT_FOUND);
        return true; // Go back to callback path
      });
}

function checkReadyState(expectedState, writer) {
  assertEquals(expectedState, writer.getReadyState());
}

function splitArgs(fn) {
  return function(args) { return fn(args[0], args[1]); };
}