Unit test application based on sample application from cordova-tizen repo.
authorPawel Andruszkiewicz <p.andruszkie@samsung.com>
Fri, 20 Nov 2015 10:02:49 +0000 (11:02 +0100)
committerPawel Andruszkiewicz <p.andruszkie@samsung.com>
Fri, 20 Nov 2015 10:02:49 +0000 (11:02 +0100)
Change-Id: I4788c7e5b1d2cf70c5c90368ae0667f519252b3d
Signed-off-by: Pawel Andruszkiewicz <p.andruszkie@samsung.com>
44 files changed:
.gitignore
test/.project [new file with mode: 0644]
test/.tproject [new file with mode: 0644]
test/config.xml [new file with mode: 0644]
test/icon.png [new file with mode: 0644]
test/index.html [new file with mode: 0644]
test/lib [new symlink]
test/main.js [new file with mode: 0644]
test/master.css [new file with mode: 0644]
test/unittest/all.html [new file with mode: 0644]
test/unittest/images/icon.png [new file with mode: 0644]
test/unittest/images/screen_tizen_hd_landscape.png [new file with mode: 0644]
test/unittest/images/screen_tizen_hd_portrait.png [new file with mode: 0644]
test/unittest/images/screen_tizen_wvga_landscape.png [new file with mode: 0644]
test/unittest/images/screen_tizen_wvga_portrait.png [new file with mode: 0644]
test/unittest/jasmine-2.2.0/console.js [new file with mode: 0644]
test/unittest/jasmine-2.2.0/jasmine-html.js [new file with mode: 0644]
test/unittest/jasmine-2.2.0/jasmine.css [new file with mode: 0644]
test/unittest/jasmine-2.2.0/jasmine.js [new file with mode: 0644]
test/unittest/jasmine-2.2.0/jasmine_favicon.png [new file with mode: 0644]
test/unittest/jasmine-2.2.0/jasmine_helper.js [new file with mode: 0644]
test/unittest/splashscreen.html [new file with mode: 0644]
test/unittest/tests/accelerometer.tests.js [new file with mode: 0644]
test/unittest/tests/battery.tests.js [new file with mode: 0644]
test/unittest/tests/bridge.tests.js [new file with mode: 0644]
test/unittest/tests/camera.tests.js [new file with mode: 0644]
test/unittest/tests/capture.tests.js [new file with mode: 0644]
test/unittest/tests/compass.tests.js [new file with mode: 0644]
test/unittest/tests/console.tests.js [new file with mode: 0644]
test/unittest/tests/contacts.tests.js [new file with mode: 0644]
test/unittest/tests/datauri.tests.js [new file with mode: 0644]
test/unittest/tests/device.tests.js [new file with mode: 0644]
test/unittest/tests/file.tests.js [new file with mode: 0644]
test/unittest/tests/filetransfer.tests.js [new file with mode: 0644]
test/unittest/tests/geolocation.tests.js [new file with mode: 0644]
test/unittest/tests/globalization.tests.js [new file with mode: 0644]
test/unittest/tests/media.tests.js [new file with mode: 0644]
test/unittest/tests/network.tests.js [new file with mode: 0644]
test/unittest/tests/notification.tests.js [new file with mode: 0644]
test/unittest/tests/platform.tests.js [new file with mode: 0644]
test/unittest/tests/splashscreen.tests.js [new file with mode: 0644]
test/unittest/tests/statusbar.tests.js [new file with mode: 0644]
test/unittest/tests/storage.tests.js [new file with mode: 0644]
test/unittest/tests/vibration.tests.js [new file with mode: 0644]

index 9dd14be..acc6e8a 100644 (file)
 *.swn
 *.swm
 *.log
+*.wgt
 /.project
 /.cproject
 /.pydevproject
 /.settings
+/test/.build
+/test/.rds_delta
+/test/.sdk_delta.info
+/test/.settings
+/test/.sign
diff --git a/test/.project b/test/.project
new file mode 100644 (file)
index 0000000..d0f04cb
--- /dev/null
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>TizenCordovaMobileSpec</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.wst.common.project.facet.core.builder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>json.validation.builder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.tizen.web.jslint.nature.JSLintBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.tizen.web.css.nature.CSSBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.wst.validation.validationbuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.tizen.web.project.builder.WebBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.tizen.web.privilege.nature.PrivilegeBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.tizen.web.editor.css.nature.CSSBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>json.validation.nature</nature>
+               <nature>org.tizen.web.jslint.nature.JSLintNature</nature>
+               <nature>org.tizen.web.css.nature.CSSNature</nature>
+               <nature>org.eclipse.wst.jsdt.core.jsNature</nature>
+               <nature>org.eclipse.wst.common.project.facet.core.nature</nature>
+               <nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
+               <nature>org.tizen.web.project.builder.WebNature</nature>
+               <nature>org.tizen.web.privilege.nature.PrivilegeNature</nature>
+               <nature>org.tizen.web.editor.css.nature.CSSNature</nature>
+       </natures>
+</projectDescription>
diff --git a/test/.tproject b/test/.tproject
new file mode 100644 (file)
index 0000000..801d1ed
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<tproject xmlns="http://www.tizen.org/tproject">
+    <platforms>
+        <platform>
+            <name>mobile-2.4</name>
+        </platform>
+    </platforms>
+    <package>
+        <blacklist/>
+    </package>
+</tproject>
diff --git a/test/config.xml b/test/config.xml
new file mode 100644 (file)
index 0000000..aed9be3
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets" id="http://Cordova/TizenCordovaMobileSpec" version="0.2.2" viewmodes="fullscreen">
+    <access origin="*"/>
+    <tizen:application id="G4rvZJzDIa.TizenCordovaMobileSpec" package="G4rvZJzDIa" required_version="2.2"/>
+    <content src="index.html"/>
+    <icon src="icon.png"/>
+    <name>TizenCordovaMobileSpec</name>
+    <tizen:privilege name="http://tizen.org/privilege/application.launch"/>
+    <tizen:privilege name="http://tizen.org/privilege/contact.read"/>
+    <tizen:privilege name="http://tizen.org/privilege/contact.write"/>
+    <tizen:privilege name="http://tizen.org/privilege/filesystem.read"/>
+    <tizen:privilege name="http://tizen.org/privilege/filesystem.write"/>
+    <tizen:privilege name="http://tizen.org/privilege/unlimitedstorage"/>
+    <tizen:privilege name="http://tizen.org/privilege/setting"/>
+    <tizen:privilege name="http://tizen.org/privilege/package.info"/>
+    <tizen:privilege name="http://tizen.org/privilege/notification"/>
+    <tizen:privilege name="http://tizen.org/privilege/system"/>
+    <tizen:privilege name="http://tizen.org/privilege/content"/>
+    <tizen:privilege name="http://tizen.org/privilege/content.read"/>
+    <tizen:privilege name="http://tizen.org/privilege/content.write"/>
+    <tizen:privilege name="http://tizen.org/privilege/mediacapture"/>
+    <tizen:privilege name="http://tizen.org/privilege/download"/>
+    <tizen:privilege name="http://tizen.org/privilege/internet"/>
+    <tizen:setting screen-orientation="auto-rotation" context-menu="enable" background-support="disable" encryption="disable" install-location="auto" hwkey-event="enable"/>
+</widget>
diff --git a/test/icon.png b/test/icon.png
new file mode 100644 (file)
index 0000000..c6fdaac
Binary files /dev/null and b/test/icon.png differ
diff --git a/test/index.html b/test/index.html
new file mode 100644 (file)
index 0000000..2f2b670
--- /dev/null
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you 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.
+
+-->
+
+
+<html>
+  <head>
+    <meta name="viewport" content="width=device-width,height=device-height,user-scalable=no,initial-scale=1.0, maximum-scale=1.0" />
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8" >
+    
+    <title>Cordova Mobile Spec</title>
+       
+       <link rel="stylesheet" href="master.css" type="text/css" media="screen" title="no title" charset="utf-8">
+       
+       <script type="text/javascript" charset="utf-8" src="lib/cordova.js"></script>
+       <script type="text/javascript" charset="utf-8" src="main.js"></script>
+  </head>
+  
+  <body onload="init();" id="stage" class="theme">
+    <h1>Apache Cordova Tests</h1>
+    <div id="info">
+        <h4>Platform: <span id="platform"></span></h4>
+        <h4>Version: <span id="version"></span></h4>
+        <h4>UUID: <span id="uuid"></span></h4>
+        <h4>Name: <span id="name"></span></h4>
+        <h4>Model: <span id="model"></span></h4>
+        <h4>Width: <span id="width"></span>,
+            Height: <span id="height"></span>,
+            Color Depth: <span id="colorDepth"></span></h4>
+        <h4>User-Agent: <span id="user-agent"> </span></h4>
+    </div>
+    <a href="unittest/all.html?t=accelerometer" class="btn large" style="width:100%;">Accelerometer Tests</a>
+    <a href="unittest/all.html?t=battery" class="btn large" style="width:100%;">Battery Tests</a>
+    <!-- a href="unittest/all.html?t=bridge" class="btn large" style="width:100%;">Bridge Tests</a -->
+    <a href="unittest/all.html?t=camera" class="btn large" style="width:100%;">Camera Tests</a>
+    <a href="unittest/all.html?t=capture" class="btn large" style="width:100%;">Capture Tests</a>
+    <a href="unittest/all.html?t=compass" class="btn large" style="width:100%;">Compass Tests</a>
+    <a href="unittest/all.html?t=console" class="btn large" style="width:100%;">Console Tests</a>
+    <a href="unittest/all.html?t=contacts" class="btn large" style="width:100%;">Contacts Tests</a>
+    <!-- a href="unittest/all.html?t=datauri" class="btn large" style="width:100%;">Data URI Tests</a -->
+    <a href="unittest/all.html?t=device" class="btn large" style="width:100%;">Device Tests</a>
+    <a href="unittest/all.html?t=notification" class="btn large" style="width:100%;">Dialogs Tests</a>
+    <a href="unittest/all.html?t=file" class="btn large" style="width:100%;">File Tests</a>
+    <a href="unittest/all.html?t=filetransfer" class="btn large" style="width:100%;">FileTransfer Tests</a>
+    <a href="unittest/all.html?t=geolocation" class="btn large" style="width:100%;">Geolocation Tests</a>
+    <a href="unittest/all.html?t=globalization" class="btn large" style="width:100%;">Globalization Tests</a>
+    <a href="unittest/all.html?t=media" class="btn large" style="width:100%;">Media Tests</a>
+    <a href="unittest/all.html?t=network" class="btn large" style="width:100%;">Network Tests</a>
+    <a href="unittest/all.html?t=platform" class="btn large" style="width:100%;">Platform Tests</a>
+    <a href="unittest/all.html?t=splashscreen" class="btn large" style="width:100%;">SplashScreen Tests</a>
+    <a href="unittest/all.html?t=statusbar" class="btn large" style="width:100%;">Status Bar Tests</a>
+    <!-- a href="unittest/all.html?t=storage" class="btn large" style="width:100%;">Run Storage Tests</a -->
+    <a href="unittest/all.html?t=vibration" class="btn large" style="width:100%;">Vibration Tests</a>
+  </body>
+</html>
diff --git a/test/lib b/test/lib
new file mode 120000 (symlink)
index 0000000..398ff21
--- /dev/null
+++ b/test/lib
@@ -0,0 +1 @@
+../src/lib
\ No newline at end of file
diff --git a/test/main.js b/test/main.js
new file mode 100644 (file)
index 0000000..e66c033
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+var deviceInfo = function() {
+    document.getElementById("platform").innerHTML = device.platform;
+    document.getElementById("version").innerHTML = device.version;
+    document.getElementById("uuid").innerHTML = device.uuid;
+    document.getElementById("name").innerHTML = device.name;
+    document.getElementById("model").innerHTML = device.model;
+    document.getElementById("width").innerHTML = screen.width;
+    document.getElementById("height").innerHTML = screen.height;
+    document.getElementById("colorDepth").innerHTML = screen.colorDepth;
+};
+
+var preventBehavior = function(e) {
+    e.preventDefault();
+};
+
+function init() {
+    // the next line makes it impossible to see Contacts on the HTC Evo since it
+    // doesn't have a scroll button
+    // document.addEventListener("touchmove", preventBehavior, false);
+    document.addEventListener("deviceready", deviceInfo, true);
+    document.getElementById("user-agent").textContent = navigator.userAgent;
+}
diff --git a/test/master.css b/test/master.css
new file mode 100644 (file)
index 0000000..e93c937
--- /dev/null
@@ -0,0 +1,164 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+  body {
+    background:#222 none repeat scroll 0 0;
+    color:#666;
+    font-family:Helvetica;
+    font-size:72%;
+    line-height:1.5em;
+    margin:0;
+    border-top:1px solid #393939;
+  }
+
+  #info{
+    background:#ffa;
+    border: 1px solid #ffd324;
+    -webkit-border-radius: 5px;
+    border-radius: 5px;
+    clear:both;
+    margin:15px 6px 0;
+    min-width:295px;
+    max-width:97%;
+    padding:4px 0px 2px 10px;
+    word-wrap:break-word;
+    margin-bottom:10px;
+    display:inline-block;
+    min-height: 160px;
+    max-height: 300px;
+    overflow: auto;
+    -webkit-overflow-scrolling: touch;
+  }
+  
+  #info > h4{
+    font-size:.95em;
+    margin:5px 0;
+  }
+       
+  #stage.theme{
+    padding-top:3px;
+  }
+
+  /* Definition List */
+  #stage.theme > dl{
+       padding-top:10px;
+       clear:both;
+       margin:0;
+       list-style-type:none;
+       padding-left:10px;
+       overflow:auto;
+  }
+
+  #stage.theme > dl > dt{
+       font-weight:bold;
+       float:left;
+       margin-left:5px;
+  }
+
+  #stage.theme > dl > dd{
+       width:45px;
+       float:left;
+       color:#a87;
+       font-weight:bold;
+  }
+
+  /* Content Styling */
+  #stage.theme > h1, #stage.theme > h2, #stage.theme > p{
+    margin:1em 0 .5em 13px;
+  }
+
+  #stage.theme > h1{
+    color:#eee;
+    font-size:1.6em;
+    text-align:center;
+    margin:0;
+    margin-top:15px;
+    padding:0;
+  }
+
+  #stage.theme > h2{
+       clear:both;
+    margin:0;
+    padding:3px;
+    font-size:1em;
+    text-align:center;
+  }
+
+  /* Stage Buttons */
+  #stage.theme .btn{
+       border: 1px solid #555;
+       -webkit-border-radius: 5px;
+       border-radius: 5px;
+       text-align:center;
+       display:inline-block;
+       background:#444;
+       width:150px;
+       color:#9ab;
+       font-size:1.1em;
+       text-decoration:none;
+       padding:1.2em 0;
+       margin:3px 0px 3px 5px;
+  }
+  
+  #stage.theme .large{
+       width:308px;
+       padding:1.2em 0;
+  }
+  
+  #stage.theme .wide{
+    width:100%;
+    padding:1.2em 0;
+  }
+  
+  #stage.theme .backBtn{
+   border: 1px solid #555;
+   -webkit-border-radius: 5px;
+   border-radius: 5px;
+   text-align:center;
+   display:block;
+   float:right;
+   background:#666;
+   width:75px;
+   color:#9ab;
+   font-size:1.1em;
+   text-decoration:none;
+   padding:1.2em 0;
+   margin:3px 5px 3px 5px;
+  }
+  
+  #stage.theme .input{
+   border: 1px solid #555;
+   -webkit-border-radius: 5px;
+   border-radius: 5px;
+   text-align:center;
+   display:block;
+   float:light;
+   background:#888;
+   color:#9cd;
+   font-size:1.1em;
+   text-decoration:none;
+   padding:1.2em 0;
+   margin:3px 0px 3px 5px;    
+ }
+  
+  #stage.theme .numeric{
+   width:100%;
+  }
diff --git a/test/unittest/all.html b/test/unittest/all.html
new file mode 100644 (file)
index 0000000..bb32834
--- /dev/null
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you 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.
+
+-->
+
+
+<html>
+<head>
+  <title>Cordova: API Specs</title>
+
+  <meta name="viewport" content="width=device-width, height=device-height, user-scalable=yes, initial-scale=1.0;" />
+  <link rel="stylesheet" href="../master.css" type="text/css" media="screen" title="no title" charset="utf-8">
+
+  <!-- Load jasmine -->
+
+  <link rel="stylesheet" type="text/css" href="jasmine-2.2.0/jasmine.css" media="screen">
+
+  <script type="text/javascript" src="jasmine-2.2.0/jasmine.js"></script>
+  <script type="text/javascript" src="jasmine-2.2.0/jasmine-html.js"></script>
+  <script type="text/javascript" src="jasmine-2.2.0/console.js"></script>
+  <script type="text/javascript" src="jasmine-2.2.0/jasmine_helper.js"></script>
+
+  <!-- Source -->
+  <script type="text/javascript" src="../lib/cordova.js"></script>
+
+
+  <script type="text/javascript">
+    var exports = {};
+
+    function getQueryVariable(variable) {
+      var query = window.location.search.substring(1);
+      var vars = query.split("&");
+      for (var i = 0; i < vars.length; ++i) {
+        var pair = vars[i].split("=");
+        if (pair[0] === variable) {
+          return pair[1];
+        }
+      }
+      return(false);
+    }
+
+    function createActionButton(title, callback, appendTo) {
+      appendTo = appendTo ? appendTo : 'buttons';
+      var buttons = document.getElementById(appendTo);
+      var div = document.createElement('div');
+      var button = document.createElement('a');
+      button.textContent = title;
+      button.onclick = function(e) {
+        e.preventDefault();
+        callback();
+      };
+      button.classList.add('btn');
+      button.classList.add('large');
+      div.appendChild(button);
+      buttons.appendChild(div);
+    }
+    
+    function clearContent() {
+      var content = document.getElementById('content');
+      content.innerHTML = '';
+    }
+
+    function injectJs(test, onload) {
+      var scr = document.createElement('script');
+      scr.type = 'text/javascript';
+      scr.src = 'tests/' + test + '.tests.js';
+      scr.onload = onload;
+      document.getElementsByTagName('head')[0].appendChild(scr);
+    }
+
+    function addTests() {
+      var tests = {
+        accelerometer: 'accelerometer',
+        battery: 'battery',
+        // bridge: 'bridge', // test code was not updated
+        camera: 'camera',
+        capture: 'capture',
+        compass: 'compass',
+        console: 'console',
+        contacts: 'contacts',
+        // datauri: 'datauri', // test code was not updated
+        device: 'device',
+        file: 'file',
+        filetransfer: 'filetransfer',
+        geolocation: 'geolocation',
+        globalization: 'globalization',
+        media: 'media',
+        network: 'network',
+        notification: 'notification',
+        platform: 'platform',
+        splashscreen: 'splashscreen',
+        statusbar: 'statusbar',
+        // storage: 'storage', // test code was not updated
+        vibration: 'vibration',
+      };
+
+      var t = getQueryVariable('t');
+
+      if (!t || !tests[t]) {
+        alert('Tests for "' + t + '" do not exist.');
+      } else {
+        // prepare Jasmine
+        JasmineHelper.setup();
+
+        // inject tests
+        injectJs(tests[t], function() {
+          exports.defineAutoTests();
+          createActionButton('Run auto tests', function () {
+            clearContent();
+            JasmineHelper.run();
+          });
+          createActionButton('Run manual tests', function () {
+            clearContent();
+            exports.defineManualTests(document.getElementById('content'), createActionButton);
+          });
+        });
+      }
+    }
+
+    addTests();
+  </script>
+</head>
+
+<body id="stage" class="theme">
+  <a href="javascript:" class="backBtn" onclick="backHome();">Back</a>
+    <div id='title'></div>
+    <div id='middle'>
+      <div id='buttons'></div>
+      <div id='content'></div>
+    </div>
+</body>
+</html>
diff --git a/test/unittest/images/icon.png b/test/unittest/images/icon.png
new file mode 100644 (file)
index 0000000..647c3f9
Binary files /dev/null and b/test/unittest/images/icon.png differ
diff --git a/test/unittest/images/screen_tizen_hd_landscape.png b/test/unittest/images/screen_tizen_hd_landscape.png
new file mode 100644 (file)
index 0000000..79f2f09
Binary files /dev/null and b/test/unittest/images/screen_tizen_hd_landscape.png differ
diff --git a/test/unittest/images/screen_tizen_hd_portrait.png b/test/unittest/images/screen_tizen_hd_portrait.png
new file mode 100644 (file)
index 0000000..c2e8042
Binary files /dev/null and b/test/unittest/images/screen_tizen_hd_portrait.png differ
diff --git a/test/unittest/images/screen_tizen_wvga_landscape.png b/test/unittest/images/screen_tizen_wvga_landscape.png
new file mode 100644 (file)
index 0000000..a61e2b1
Binary files /dev/null and b/test/unittest/images/screen_tizen_wvga_landscape.png differ
diff --git a/test/unittest/images/screen_tizen_wvga_portrait.png b/test/unittest/images/screen_tizen_wvga_portrait.png
new file mode 100644 (file)
index 0000000..5d6a28a
Binary files /dev/null and b/test/unittest/images/screen_tizen_wvga_portrait.png differ
diff --git a/test/unittest/jasmine-2.2.0/console.js b/test/unittest/jasmine-2.2.0/console.js
new file mode 100644 (file)
index 0000000..c5835b6
--- /dev/null
@@ -0,0 +1,184 @@
+/*
+Copyright (c) 2008-2015 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+getJasmineRequireObj().console = function(jRequire, j$) {
+  j$.ConsoleReporter = jRequire.ConsoleReporter();
+};
+
+getJasmineRequireObj().ConsoleReporter = function() {
+
+  var noopTimer = {
+    start: function(){},
+    elapsed: function(){ return 0; }
+  };
+
+  function ConsoleReporter(options) {
+    var print = options.print,
+      showColors = options.showColors || false,
+      onComplete = options.onComplete || function() {},
+      timer = options.timer || noopTimer,
+      specCount,
+      failureCount,
+      failedSpecs = [],
+      pendingCount,
+      ansi = {
+        green: '\x1B[32m',
+        red: '\x1B[31m',
+        yellow: '\x1B[33m',
+        none: '\x1B[0m'
+      },
+      failedSuites = [];
+
+    print('ConsoleReporter is deprecated and will be removed in a future version.');
+
+    this.jasmineStarted = function() {
+      specCount = 0;
+      failureCount = 0;
+      pendingCount = 0;
+      print('Started');
+      printNewline();
+      timer.start();
+    };
+
+    this.jasmineDone = function() {
+      //printNewline();
+      //for (var i = 0; i < failedSpecs.length; i++) {
+      //  specFailureDetails(failedSpecs[i]);
+      //}
+
+      if(specCount > 0) {
+        printNewline();
+
+        var specCounts = specCount + ' ' + plural('spec', specCount) + ', ' +
+          failureCount + ' ' + plural('failure', failureCount);
+
+        if (pendingCount) {
+          specCounts += ', ' + pendingCount + ' pending ' + plural('spec', pendingCount);
+        }
+
+        print(specCounts);
+      } else {
+        print('No specs found');
+      }
+
+      printNewline();
+      var seconds = timer.elapsed() / 1000;
+      print('Finished in ' + seconds + ' ' + plural('second', seconds));
+      //printNewline();
+
+      //for(i = 0; i < failedSuites.length; i++) {
+      //  suiteFailureDetails(failedSuites[i]);
+      //}
+
+      onComplete(failureCount === 0);
+    };
+
+    this.specDone = function(result) {
+      specCount++;
+
+      if (result.status == 'pending') {
+        pendingCount++;
+        print(colored('yellow', '*'));
+        return;
+      }
+
+      if (result.status == 'passed') {
+        print(colored('green', '.'));
+        return;
+      }
+
+      if (result.status == 'failed') {
+        failureCount++;
+        failedSpecs.push(result);
+        print(colored('red', 'F'));
+        specFailureDetails(result);
+      }
+    };
+
+    this.suiteDone = function(result) {
+      if (result.failedExpectations && result.failedExpectations.length > 0) {
+        failureCount++;
+        failedSuites.push(result);
+        suiteFailureDetails(result);
+      }
+    };
+
+    return this;
+
+    function printNewline() {
+      print('\n');
+    }
+
+    function colored(color, str) {
+      return showColors ? (ansi[color] + str + ansi.none) : str;
+    }
+
+    function plural(str, count) {
+      return count == 1 ? str : str + 's';
+    }
+
+    function repeat(thing, times) {
+      var arr = [];
+      for (var i = 0; i < times; i++) {
+        arr.push(thing);
+      }
+      return arr;
+    }
+
+    function indent(str, spaces) {
+      var lines = (str || '').split('\n');
+      var newArr = [];
+      for (var i = 0; i < lines.length; i++) {
+        newArr.push(repeat(' ', spaces).join('') + lines[i]);
+      }
+      return newArr.join('\n');
+    }
+
+    function specFailureDetails(result) {
+      printNewline();
+      print(result.fullName);
+
+      for (var i = 0; i < result.failedExpectations.length; i++) {
+        var failedExpectation = result.failedExpectations[i];
+        printNewline();
+        print(indent(failedExpectation.message, 2));
+        print(indent(failedExpectation.stack, 2));
+      }
+
+      printNewline();
+    }
+
+    function suiteFailureDetails(result) {
+      for (var i = 0; i < result.failedExpectations.length; i++) {
+        printNewline();
+        print(colored('red', 'An error was thrown in an afterAll'));
+        printNewline();
+        print(colored('red', 'AfterAll ' + result.failedExpectations[i].message));
+
+      }
+      printNewline();
+    }
+  }
+
+  return ConsoleReporter;
+};
diff --git a/test/unittest/jasmine-2.2.0/jasmine-html.js b/test/unittest/jasmine-2.2.0/jasmine-html.js
new file mode 100644 (file)
index 0000000..bee5a04
--- /dev/null
@@ -0,0 +1,416 @@
+/*
+Copyright (c) 2008-2015 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+jasmineRequire.html = function(j$) {
+  j$.ResultsNode = jasmineRequire.ResultsNode();
+  j$.HtmlReporter = jasmineRequire.HtmlReporter(j$);
+  j$.QueryString = jasmineRequire.QueryString();
+  j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter();
+};
+
+jasmineRequire.HtmlReporter = function(j$) {
+
+  var noopTimer = {
+    start: function() {},
+    elapsed: function() { return 0; }
+  };
+
+  function HtmlReporter(options) {
+    var env = options.env || {},
+      getContainer = options.getContainer,
+      createElement = options.createElement,
+      createTextNode = options.createTextNode,
+      onRaiseExceptionsClick = options.onRaiseExceptionsClick || function() {},
+      addToExistingQueryString = options.addToExistingQueryString || defaultQueryString,
+      timer = options.timer || noopTimer,
+      results = [],
+      specsExecuted = 0,
+      failureCount = 0,
+      pendingSpecCount = 0,
+      htmlReporterMain,
+      symbols,
+      failedSuites = [];
+
+    this.initialize = function() {
+      clearPrior();
+      htmlReporterMain = createDom('div', {className: 'jasmine_html-reporter'},
+        createDom('div', {className: 'banner'},
+          createDom('a', {className: 'title', href: 'http://jasmine.github.io/', target: '_blank'}),
+          createDom('span', {className: 'version'}, j$.version)
+        ),
+        createDom('ul', {className: 'symbol-summary'}),
+        createDom('div', {className: 'alert'}),
+        createDom('div', {className: 'results'},
+          createDom('div', {className: 'failures'})
+        )
+      );
+      getContainer().appendChild(htmlReporterMain);
+
+      symbols = find('.symbol-summary');
+    };
+
+    var totalSpecsDefined;
+    this.jasmineStarted = function(options) {
+      totalSpecsDefined = options.totalSpecsDefined || 0;
+      timer.start();
+    };
+
+    var summary = createDom('div', {className: 'summary'});
+
+    var topResults = new j$.ResultsNode({}, '', null),
+      currentParent = topResults;
+
+    this.suiteStarted = function(result) {
+      currentParent.addChild(result, 'suite');
+      currentParent = currentParent.last();
+    };
+
+    this.suiteDone = function(result) {
+      if (result.status == 'failed') {
+        failedSuites.push(result);
+      }
+
+      if (currentParent == topResults) {
+        return;
+      }
+
+      currentParent = currentParent.parent;
+    };
+
+    this.specStarted = function(result) {
+      currentParent.addChild(result, 'spec');
+    };
+
+    var failures = [];
+    this.specDone = function(result) {
+      if(noExpectations(result) && typeof console !== 'undefined' && typeof console.error !== 'undefined') {
+        console.error('Spec \'' + result.fullName + '\' has no expectations.');
+      }
+
+      if (result.status != 'disabled') {
+        specsExecuted++;
+      }
+
+      symbols.appendChild(createDom('li', {
+          className: noExpectations(result) ? 'empty' : result.status,
+          id: 'spec_' + result.id,
+          title: result.fullName
+        }
+      ));
+
+      if (result.status == 'failed') {
+        failureCount++;
+
+        var failure =
+          createDom('div', {className: 'spec-detail failed'},
+            createDom('div', {className: 'description'},
+              createDom('a', {title: result.fullName, href: specHref(result)}, result.fullName)
+            ),
+            createDom('div', {className: 'messages'})
+          );
+        var messages = failure.childNodes[1];
+
+        for (var i = 0; i < result.failedExpectations.length; i++) {
+          var expectation = result.failedExpectations[i];
+          messages.appendChild(createDom('div', {className: 'result-message'}, expectation.message));
+          messages.appendChild(createDom('div', {className: 'stack-trace'}, expectation.stack));
+        }
+
+        failures.push(failure);
+      }
+
+      if (result.status == 'pending') {
+        pendingSpecCount++;
+      }
+    };
+
+    this.jasmineDone = function() {
+      var banner = find('.banner');
+      banner.appendChild(createDom('span', {className: 'duration'}, 'finished in ' + timer.elapsed() / 1000 + 's'));
+
+      var alert = find('.alert');
+
+      alert.appendChild(createDom('span', { className: 'exceptions' },
+        createDom('label', { className: 'label', 'for': 'raise-exceptions' }, 'raise exceptions'),
+        createDom('input', {
+          className: 'raise',
+          id: 'raise-exceptions',
+          type: 'checkbox'
+        })
+      ));
+      var checkbox = find('#raise-exceptions');
+
+      checkbox.checked = !env.catchingExceptions();
+      checkbox.onclick = onRaiseExceptionsClick;
+
+      if (specsExecuted < totalSpecsDefined) {
+        var skippedMessage = 'Ran ' + specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all';
+        alert.appendChild(
+          createDom('span', {className: 'bar skipped'},
+            createDom('a', {href: '?', title: 'Run all specs'}, skippedMessage)
+          )
+        );
+      }
+      var statusBarMessage = '';
+      var statusBarClassName = 'bar ';
+
+      if (totalSpecsDefined > 0) {
+        statusBarMessage += pluralize('spec', specsExecuted) + ', ' + pluralize('failure', failureCount);
+        if (pendingSpecCount) { statusBarMessage += ', ' + pluralize('pending spec', pendingSpecCount); }
+        statusBarClassName += (failureCount > 0) ? 'failed' : 'passed';
+      } else {
+        statusBarClassName += 'skipped';
+        statusBarMessage += 'No specs found';
+      }
+
+      alert.appendChild(createDom('span', {className: statusBarClassName}, statusBarMessage));
+
+      for(i = 0; i < failedSuites.length; i++) {
+        var failedSuite = failedSuites[i];
+        for(var j = 0; j < failedSuite.failedExpectations.length; j++) {
+          var errorBarMessage = 'AfterAll ' + failedSuite.failedExpectations[j].message;
+          var errorBarClassName = 'bar errored';
+          alert.appendChild(createDom('span', {className: errorBarClassName}, errorBarMessage));
+        }
+      }
+
+      var results = find('.results');
+      results.appendChild(summary);
+
+      summaryList(topResults, summary);
+
+      function summaryList(resultsTree, domParent) {
+        var specListNode;
+        for (var i = 0; i < resultsTree.children.length; i++) {
+          var resultNode = resultsTree.children[i];
+          if (resultNode.type == 'suite') {
+            var suiteListNode = createDom('ul', {className: 'suite', id: 'suite-' + resultNode.result.id},
+              createDom('li', {className: 'suite-detail'},
+                createDom('a', {href: specHref(resultNode.result)}, resultNode.result.description)
+              )
+            );
+
+            summaryList(resultNode, suiteListNode);
+            domParent.appendChild(suiteListNode);
+          }
+          if (resultNode.type == 'spec') {
+            if (domParent.getAttribute('class') != 'specs') {
+              specListNode = createDom('ul', {className: 'specs'});
+              domParent.appendChild(specListNode);
+            }
+            var specDescription = resultNode.result.description;
+            if(noExpectations(resultNode.result)) {
+              specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription;
+            }
+            if(resultNode.result.status === 'pending' && resultNode.result.pendingReason !== '') {
+              specDescription = specDescription + ' PENDING WITH MESSAGE: ' + resultNode.result.pendingReason;
+            }
+            specListNode.appendChild(
+              createDom('li', {
+                  className: resultNode.result.status,
+                  id: 'spec-' + resultNode.result.id
+                },
+                createDom('a', {href: specHref(resultNode.result)}, specDescription)
+              )
+            );
+          }
+        }
+      }
+
+      if (failures.length) {
+        alert.appendChild(
+          createDom('span', {className: 'menu bar spec-list'},
+            createDom('span', {}, 'Spec List | '),
+            createDom('a', {className: 'failures-menu', href: '#'}, 'Failures')));
+        alert.appendChild(
+          createDom('span', {className: 'menu bar failure-list'},
+            createDom('a', {className: 'spec-list-menu', href: '#'}, 'Spec List'),
+            createDom('span', {}, ' | Failures ')));
+
+        find('.failures-menu').onclick = function() {
+          setMenuModeTo('failure-list');
+        };
+        find('.spec-list-menu').onclick = function() {
+          setMenuModeTo('spec-list');
+        };
+
+        setMenuModeTo('failure-list');
+
+        var failureNode = find('.failures');
+        for (var i = 0; i < failures.length; i++) {
+          failureNode.appendChild(failures[i]);
+        }
+      }
+    };
+
+    return this;
+
+    function find(selector) {
+      return getContainer().querySelector('.jasmine_html-reporter ' + selector);
+    }
+
+    function clearPrior() {
+      // return the reporter
+      var oldReporter = find('');
+
+      if(oldReporter) {
+        getContainer().removeChild(oldReporter);
+      }
+    }
+
+    function createDom(type, attrs, childrenVarArgs) {
+      var el = createElement(type);
+
+      for (var i = 2; i < arguments.length; i++) {
+        var child = arguments[i];
+
+        if (typeof child === 'string') {
+          el.appendChild(createTextNode(child));
+        } else {
+          if (child) {
+            el.appendChild(child);
+          }
+        }
+      }
+
+      for (var attr in attrs) {
+        if (attr == 'className') {
+          el[attr] = attrs[attr];
+        } else {
+          el.setAttribute(attr, attrs[attr]);
+        }
+      }
+
+      return el;
+    }
+
+    function pluralize(singular, count) {
+      var word = (count == 1 ? singular : singular + 's');
+
+      return '' + count + ' ' + word;
+    }
+
+    function specHref(result) {
+      return addToExistingQueryString('spec', result.fullName);
+    }
+
+    function defaultQueryString(key, value) {
+      return '?' + key + '=' + value;
+    }
+
+    function setMenuModeTo(mode) {
+      htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode);
+    }
+
+    function noExpectations(result) {
+      return (result.failedExpectations.length + result.passedExpectations.length) === 0 &&
+        result.status === 'passed';
+    }
+  }
+
+  return HtmlReporter;
+};
+
+jasmineRequire.HtmlSpecFilter = function() {
+  function HtmlSpecFilter(options) {
+    var filterString = options && options.filterString() && options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+    var filterPattern = new RegExp(filterString);
+
+    this.matches = function(specName) {
+      return filterPattern.test(specName);
+    };
+  }
+
+  return HtmlSpecFilter;
+};
+
+jasmineRequire.ResultsNode = function() {
+  function ResultsNode(result, type, parent) {
+    this.result = result;
+    this.type = type;
+    this.parent = parent;
+
+    this.children = [];
+
+    this.addChild = function(result, type) {
+      this.children.push(new ResultsNode(result, type, this));
+    };
+
+    this.last = function() {
+      return this.children[this.children.length - 1];
+    };
+  }
+
+  return ResultsNode;
+};
+
+jasmineRequire.QueryString = function() {
+  function QueryString(options) {
+
+    this.navigateWithNewParam = function(key, value) {
+      options.getWindowLocation().search = this.fullStringWithNewParam(key, value);
+    };
+
+    this.fullStringWithNewParam = function(key, value) {
+      var paramMap = queryStringToParamMap();
+      paramMap[key] = value;
+      return toQueryString(paramMap);
+    };
+
+    this.getParam = function(key) {
+      return queryStringToParamMap()[key];
+    };
+
+    return this;
+
+    function toQueryString(paramMap) {
+      var qStrPairs = [];
+      for (var prop in paramMap) {
+        qStrPairs.push(encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop]));
+      }
+      return '?' + qStrPairs.join('&');
+    }
+
+    function queryStringToParamMap() {
+      var paramStr = options.getWindowLocation().search.substring(1),
+        params = [],
+        paramMap = {};
+
+      if (paramStr.length > 0) {
+        params = paramStr.split('&');
+        for (var i = 0; i < params.length; i++) {
+          var p = params[i].split('=');
+          var value = decodeURIComponent(p[1]);
+          if (value === 'true' || value === 'false') {
+            value = JSON.parse(value);
+          }
+          paramMap[decodeURIComponent(p[0])] = value;
+        }
+      }
+
+      return paramMap;
+    }
+
+  }
+
+  return QueryString;
+};
diff --git a/test/unittest/jasmine-2.2.0/jasmine.css b/test/unittest/jasmine-2.2.0/jasmine.css
new file mode 100644 (file)
index 0000000..ecc5f5e
--- /dev/null
@@ -0,0 +1,62 @@
+body { overflow-y: scroll; }
+
+.jasmine_html-reporter { background-color: #eee; padding: 5px; margin: -8px; font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333; }
+.jasmine_html-reporter a { text-decoration: none; }
+.jasmine_html-reporter a:hover { text-decoration: underline; }
+.jasmine_html-reporter p, .jasmine_html-reporter h1, .jasmine_html-reporter h2, .jasmine_html-reporter h3, .jasmine_html-reporter h4, .jasmine_html-reporter h5, .jasmine_html-reporter h6 { margin: 0; line-height: 14px; }
+.jasmine_html-reporter .banner, .jasmine_html-reporter .symbol-summary, .jasmine_html-reporter .summary, .jasmine_html-reporter .result-message, .jasmine_html-reporter .spec .description, .jasmine_html-reporter .spec-detail .description, .jasmine_html-reporter .alert .bar, .jasmine_html-reporter .stack-trace { padding-left: 9px; padding-right: 9px; }
+.jasmine_html-reporter .banner { position: relative; }
+.jasmine_html-reporter .banner .title { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAZCAMAAACGusnyAAACdlBMVEX/////AP+AgICqVaqAQICZM5mAVYCSSZKAQICOOY6ATYCLRouAQICJO4mSSYCIRIiPQICHPIeOR4CGQ4aMQICGPYaLRoCFQ4WKQICPPYWJRYCOQoSJQICNPoSIRICMQoSHQICHRICKQoOHQICKPoOJO4OJQYOMQICMQ4CIQYKLQICIPoKLQ4CKQICNPoKJQISMQ4KJQoSLQYKJQISLQ4KIQoSKQYKIQICIQISMQoSKQYKLQIOLQoOJQYGLQIOKQIOMQoGKQYOLQYGKQIOLQoGJQYOJQIOKQYGJQIOKQoGKQIGLQIKLQ4KKQoGLQYKJQIGKQYKJQIGKQIKJQoGKQYKLQIGKQYKLQIOJQoKKQoOJQYKKQIOJQoKKQoOKQIOLQoKKQYOLQYKJQIOKQoKKQYKKQoKJQYOKQYKLQIOKQoKLQYOKQYKLQIOJQoGKQYKJQYGJQoGKQYKLQoGLQYGKQoGJQYKKQYGJQIKKQoGJQYKLQIKKQYGLQYKKQYGKQYGKQYKJQYOKQoKJQYOKQYKLQYOLQYOKQYKLQYOKQoKKQYKKQYOKQYOJQYKKQYKLQYKKQIKKQoKKQYKKQYKKQoKJQIKKQYKLQYKKQYKKQIKKQYKKQYKKQYKKQIKKQYKJQYGLQYGKQYKKQYKKQYGKQIKKQYGKQYOJQoKKQYOLQYKKQYOKQoKKQYKKQoKKQYKKQYKJQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKJQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKmIDpEAAAA0XRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAiIyQlJycoKissLS4wMTQ1Njc4OTo7PDw+P0BCQ0RISUpLTE1OUFNUVVdYWFlaW15fYGFiY2ZnaGlqa2xtb3BxcnN0dnh5ent8fX5/gIGChIWIioyNjo+QkZOUlZaYmZqbnJ2eoKGio6WmqKmsra6vsLGztre4ubq7vL2+wMHDxMjJysvNzs/Q0dLU1tfY2dvc3t/g4eLj5ebn6Onq6+zt7u/w8vP09fb3+Pn6+/z9/vkVQXAAAAMaSURBVHhe5dXxV1N1GMfxz2ABbDgIAm5VDJOyVDIJLUMaVpBWUZUaGbmqoGpZRSiGiRWp6KoZ5AB0ZY50RImZQIlahKkMYXv/R90dBvET/rJfOr3Ouc8v99zPec59zvf56j+vYKlViSf7250X4Mr3O29Tgq08BdGB4DhcekEJ5YkQKFsgWZdtj9JpV+I8xPjLFqkrsEIqO8PHSpis36jWazcqjEsfJjkvRssVU37SdIOu4XCf5vEJPsnwJpnRNU9JmxhMk8l1gehIrq7hTFjzOD+Vf88629qKMJVNltInFeRexRQyJlNeqd1iGDlSzrIUIyXbyFfm3RYprcQRe7lqtWyGYbfc6dT0R2vmdOOkX3u55C1rP37ftiH+tDby4r/RBT0w8TyEkr+epB9XgPDmSYYWbrhCuFYaIyw3fDQAXTnSkh+ANofiHmWf9l+FY1I90FdQTetstO00o23novzVsJ7uB3/C5TkbjRwZ5JerwV4iRWq9HFbFMaK/d0TYqayRiQPuIxxS3Bu8JWU90/60tKi7vkhaznez0a/TbVOKj5CaOZh6fWG6/Lyv9B/ZLR1gw/S/fpbeVD3MCW1li6SvWDOn65tr99/uvWtBS0XDm4s1t+sOHpG0kpBKx/l77wOSnxLpcx6TXmXLTPQOKYOf9Q1dfr8/SJ2mFdCvl1Yl93DiHUZvXeLJbGSzYu5gVJ2slbSakOR8dxCq5adQ2oFLqsE9Ex3L4qQO0eOPeU5x56bypXp4onSEb5OkICX6lDat55TeoztNKQcJaakrz9KCb95oD69IKq+yKW4XPjknaS52V0TZqE2cTtXjcHSCRmUO88e+85hj3EP74i9p8pylw7lxgMDyyl6OV7ZejnjNMfatu87LxRbH0IS35gt2a4ZjmGpVBdKK3Wr6INk8jWWSGqbA55CKgjBRC6E9w78ydTg3ABS3AFV1QN0Y4Aa2pgEjWnQURj9L0ayK6R2ysEqxHUKzYnLvvyU+i9KM2JHJzE4vyZOyDcOwOsySajeLPc8sNvPJkFlyJd20wpqAzZeAfZ3oWybxd+P/3j+SG3uSBdf2VQAAAABJRU5ErkJggg==') no-repeat; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmVyc2lvbj0iMS4xIgogICB3aWR0aD0iNjgxLjk2MjUyIgogICBoZWlnaHQ9IjE4Ny41IgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhOCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczYiPjxjbGlwUGF0aAogICAgICAgaWQ9ImNsaXBQYXRoMTgiPjxwYXRoCiAgICAgICAgIGQ9Ik0gMCwxNTAwIDAsMCBsIDU0NTUuNzQsMCAwLDE1MDAgTCAwLDE1MDAgeiIKICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgaWQ9InBhdGgyMCIgLz48L2NsaXBQYXRoPjwvZGVmcz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMjUsMCwwLC0xLjI1LDAsMTg3LjUpIgogICAgIGlkPSJnMTAiPjxnCiAgICAgICB0cmFuc2Zvcm09InNjYWxlKDAuMSwwLjEpIgogICAgICAgaWQ9ImcxMiI+PGcKICAgICAgICAgaWQ9ImcxNCI+PGcKICAgICAgICAgICBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgxOCkiCiAgICAgICAgICAgaWQ9ImcxNiI+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTU0NCw1OTkuNDM0IGMgMC45MiwtNDAuMzUyIDI1LjY4LC04MS42MDIgNzEuNTMsLTgxLjYwMiAyNy41MSwwIDQ3LjY4LDEyLjgzMiA2MS40NCwzNS43NTQgMTIuODMsMjIuOTMgMTIuODMsNTYuODUyIDEyLjgzLDgyLjUyNyBsIDAsMzI5LjE4NCAtNzEuNTIsMCAwLDEwNC41NDMgMjY2LjgzLDAgMCwtMTA0LjU0MyAtNzAuNiwwIDAsLTM0NC43NyBjIDAsLTU4LjY5MSAtMy42OCwtMTA0LjUzMSAtNDQuOTMsLTE1Mi4yMTggLTM2LjY4LC00Mi4xOCAtOTYuMjgsLTY2LjAyIC0xNTMuMTQsLTY2LjAyIC0xMTcuMzcsMCAtMjA3LjI0LDc3Ljk0MSAtMjAyLjY0LDE5Ny4xNDUgbCAxMzAuMiwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMjIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDIzMDEuNCw2NjIuNjk1IGMgMCw4MC43MDMgLTY2Ljk0LDE0NS44MTMgLTE0Ny42MywxNDUuODEzIC04My40NCwwIC0xNDcuNjMsLTY4Ljc4MSAtMTQ3LjYzLC0xNTEuMzAxIDAsLTc5Ljc4NSA2Ni45NCwtMTQ1LjgwMSAxNDUuOCwtMTQ1LjgwMSA4NC4zNSwwIDE0OS40Niw2Ny44NTIgMTQ5LjQ2LDE1MS4yODkgeiBtIC0xLjgzLC0xODEuNTQ3IGMgLTM1Ljc3LC01NC4wOTcgLTkzLjUzLC03OC44NTkgLTE1Ny43MiwtNzguODU5IC0xNDAuMywwIC0yNTEuMjQsMTE2LjQ0OSAtMjUxLjI0LDI1NC45MTggMCwxNDIuMTI5IDExMy43LDI2MC40MSAyNTYuNzQsMjYwLjQxIDYzLjI3LDAgMTE4LjI5LC0yOS4zMzYgMTUyLjIyLC04Mi41MjMgbCAwLDY5LjY4NyAxNzUuMTQsMCAwLC0xMDQuNTI3IC02MS40NCwwIDAsLTI4MC41OTggNjEuNDQsMCAwLC0xMDQuNTI3IC0xNzUuMTQsMCAwLDY2LjAxOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAyNjIyLjMzLDU1Ny4yNTggYyAzLjY3LC00NC4wMTYgMzMuMDEsLTczLjM0OCA3OC44NiwtNzMuMzQ4IDMzLjkzLDAgNjYuOTMsMjMuODI0IDY2LjkzLDYwLjUwNCAwLDQ4LjYwNiAtNDUuODQsNTYuODU2IC04My40NCw2Ni45NDEgLTg1LjI4LDIyLjAwNCAtMTc4LjgxLDQ4LjYwNiAtMTc4LjgxLDE1NS44NzkgMCw5My41MzYgNzguODYsMTQ3LjYzMyAxNjUuOTgsMTQ3LjYzMyA0NCwwIDgzLjQzLC05LjE3NiAxMTAuOTQsLTQ0LjAwOCBsIDAsMzMuOTIyIDgyLjUzLDAgMCwtMTMyLjk2NSAtMTA4LjIxLDAgYyAtMS44MywzNC44NTYgLTI4LjQyLDU3Ljc3NCAtNjMuMjYsNTcuNzc0IC0zMC4yNiwwIC02Mi4zNSwtMTcuNDIyIC02Mi4zNSwtNTEuMzQ4IDAsLTQ1Ljg0NyA0NC45MywtNTUuOTMgODAuNjksLTY0LjE4IDg4LjAyLC0yMC4xNzUgMTgyLjQ3LC00Ny42OTUgMTgyLjQ3LC0xNTcuNzM0IDAsLTk5LjAyNyAtODMuNDQsLTE1NC4wMzkgLTE3NS4xMywtMTU0LjAzOSAtNDkuNTMsMCAtOTQuNDYsMTUuNTgyIC0xMjYuNTUsNTMuMTggbCAwLC00MC4zNCAtODUuMjcsMCAwLDE0Mi4xMjkgMTE0LjYyLDAiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGgyNiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMjk4OC4xOCw4MDAuMjU0IC02My4yNiwwIDAsMTA0LjUyNyAxNjUuMDUsMCAwLC03My4zNTUgYyAzMS4xOCw1MS4zNDcgNzguODYsODUuMjc3IDE0MS4yMSw4NS4yNzcgNjcuODUsMCAxMjQuNzEsLTQxLjI1OCAxNTIuMjEsLTEwMi42OTkgMjYuNiw2Mi4zNTEgOTIuNjIsMTAyLjY5OSAxNjAuNDcsMTAyLjY5OSA1My4xOSwwIDEwNS40NiwtMjIgMTQxLjIxLC02Mi4zNTEgMzguNTIsLTQ0LjkzOCAzOC41MiwtOTMuNTMyIDM4LjUyLC0xNDkuNDU3IGwgMCwtMTg1LjIzOSA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40MiwwIDAsMTA0LjUyNyA2My4yOCwwIDAsMTU3LjcxNSBjIDAsMzIuMTAyIDAsNjAuNTI3IC0xNC42Nyw4OC45NTcgLTE4LjM0LDI2LjU4MiAtNDguNjEsNDAuMzQ0IC03OS43Nyw0MC4zNDQgLTMwLjI2LDAgLTYzLjI4LC0xMi44NDQgLTgyLjUzLC0zNi42NzIgLTIyLjkzLC0yOS4zNTUgLTIyLjkzLC01Ni44NjMgLTIyLjkzLC05Mi42MjkgbCAwLC0xNTcuNzE1IDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM4LjQxLDAgMCwxMDQuNTI3IDYzLjI4LDAgMCwxNTAuMzgzIGMgMCwyOS4zNDggMCw2Ni4wMjMgLTE0LjY3LDkxLjY5OSAtMTUuNTksMjkuMzM2IC00Ny42OSw0NC45MzQgLTgwLjcsNDQuOTM0IC0zMS4xOCwwIC01Ny43NywtMTEuMDA4IC03Ny45NCwtMzUuNzc0IC0yNC43NywtMzAuMjUzIC0yNi42LC02Mi4zNDMgLTI2LjYsLTk5Ljk0MSBsIDAsLTE1MS4zMDEgNjMuMjcsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNiwwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAzOTk4LjY2LDk1MS41NDcgLTExMS44NywwIDAsMTE4LjI5MyAxMTEuODcsMCAwLC0xMTguMjkzIHogbSAwLC00MzEuODkxIDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM5LjMzLDAgMCwxMDQuNTI3IDY0LjE5LDAgMCwyODAuNTk4IC02My4yNywwIDAsMTA0LjUyNyAxNzUuMTQsMCAwLC0zODUuMTI1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzAiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDQxNTkuMTIsODAwLjI1NCAtNjMuMjcsMCAwLDEwNC41MjcgMTc1LjE0LDAgMCwtNjkuNjg3IGMgMjkuMzUsNTQuMTAxIDg0LjM2LDgwLjY5OSAxNDQuODcsODAuNjk5IDUzLjE5LDAgMTA1LjQ1LC0yMi4wMTYgMTQxLjIyLC02MC41MjcgNDAuMzQsLTQ0LjkzNCA0MS4yNiwtODguMDMyIDQxLjI2LC0xNDMuOTU3IGwgMCwtMTkxLjY1MyA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40LDAgMCwxMDQuNTI3IDYzLjI2LDAgMCwxNTguNjM3IGMgMCwzMC4yNjIgMCw2MS40MzQgLTE5LjI2LDg4LjAzNSAtMjAuMTcsMjYuNTgyIC01My4xOCwzOS40MTQgLTg2LjE5LDM5LjQxNCAtMzMuOTMsMCAtNjguNzcsLTEzLjc1IC04OC45NCwtNDEuMjUgLTIxLjA5LC0yNy41IC0yMS4wOSwtNjkuNjg3IC0yMS4wOSwtMTAyLjcwNyBsIDAsLTE0Mi4xMjkgNjMuMjYsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNywwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDMyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA1MDgyLjQ4LDcwMy45NjUgYyAtMTkuMjQsNzAuNjA1IC04MS42LDExNS41NDcgLTE1NC4wNCwxMTUuNTQ3IC02Ni4wNCwwIC0xMjkuMywtNTEuMzQ4IC0xNDMuMDUsLTExNS41NDcgbCAyOTcuMDksMCB6IG0gODUuMjcsLTE0NC44ODMgYyAtMzguNTEsLTkzLjUyMyAtMTI5LjI3LC0xNTYuNzkzIC0yMzEuMDUsLTE1Ni43OTMgLTE0My4wNywwIC0yNTcuNjgsMTExLjg3MSAtMjU3LjY4LDI1NS44MzYgMCwxNDQuODgzIDEwOS4xMiwyNjEuMzI4IDI1NC45MSwyNjEuMzI4IDY3Ljg3LDAgMTM1LjcyLC0zMC4yNTggMTgzLjM5LC03OC44NjMgNDguNjIsLTUxLjM0NCA2OC43OSwtMTEzLjY5NSA2OC43OSwtMTgzLjM4MyBsIC0zLjY3LC0zOS40MzQgLTM5Ni4xMywwIGMgMTQuNjcsLTY3Ljg2MyA3Ny4wMywtMTE3LjM2MyAxNDYuNzIsLTExNy4zNjMgNDguNTksMCA5MC43NiwxOC4zMjggMTE4LjI4LDU4LjY3MiBsIDExNi40NCwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDY5MC44OTUsODUwLjcwMyA5MC43NSwwIDIyLjU0MywzMS4wMzUgMCwyNDMuMTIyIC0xMzUuODI5LDAgMCwtMjQzLjE0MSAyMi41MzYsLTMxLjAxNiIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDM2IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA2MzIuMzk1LDc0Mi4yNTggMjguMDM5LDg2LjMwNCAtMjIuNTUxLDMxLjA0IC0yMzEuMjIzLDc1LjEyOCAtNDEuOTc2LC0xMjkuMTgzIDIzMS4yNTcsLTc1LjEzNyAzNi40NTQsMTEuODQ4IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzgiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDcxNy40NDksNjUzLjEwNSAtNzMuNDEsNTMuMzYgLTM2LjQ4OCwtMTEuODc1IC0xNDIuOTAzLC0xOTYuNjkyIDEwOS44ODMsLTc5LjgyOCAxNDIuOTE4LDE5Ni43MDMgMCwzOC4zMzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0MCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gODI4LjUyLDcwNi40NjUgLTczLjQyNiwtNTMuMzQgMC4wMTEsLTM4LjM1OSBMIDg5OC4wMDQsNDE4LjA3IDEwMDcuOSw0OTcuODk4IDg2NC45NzMsNjk0LjYwOSA4MjguNTIsNzA2LjQ2NSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA4MTIuMDg2LDgyOC41ODYgMjguMDU1LC04Ni4zMiAzNi40ODQsLTExLjgzNiAyMzEuMjI1LDc1LjExNyAtNDEuOTcsMTI5LjE4MyAtMjMxLjIzOSwtNzUuMTQgLTIyLjU1NSwtMzEuMDA0IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNDQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDczNi4zMDEsMTMzNS44OCBjIC0zMjMuMDQ3LDAgLTU4NS44NzUsLTI2Mi43OCAtNTg1Ljg3NSwtNTg1Ljc4MiAwLC0zMjMuMTE4IDI2Mi44MjgsLTU4NS45NzcgNTg1Ljg3NSwtNTg1Ljk3NyAzMjMuMDE5LDAgNTg1LjgwOSwyNjIuODU5IDU4NS44MDksNTg1Ljk3NyAwLDMyMy4wMDIgLTI2Mi43OSw1ODUuNzgyIC01ODUuODA5LDU4NS43ODIgbCAwLDAgeiBtIDAsLTExOC42MSBjIDI1Ny45NzIsMCA0NjcuMTg5LC0yMDkuMTMgNDY3LjE4OSwtNDY3LjE3MiAwLC0yNTguMTI5IC0yMDkuMjE3LC00NjcuMzQ4IC00NjcuMTg5LC00NjcuMzQ4IC0yNTguMDc0LDAgLTQ2Ny4yNTQsMjA5LjIxOSAtNDY3LjI1NCw0NjcuMzQ4IDAsMjU4LjA0MiAyMDkuMTgsNDY3LjE3MiA0NjcuMjU0LDQ2Ny4xNzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0NiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTA5MS4xMyw2MTkuODgzIC0xNzUuNzcxLDU3LjEyMSAxMS42MjksMzUuODA4IDE3NS43NjIsLTU3LjEyMSAtMTEuNjIsLTM1LjgwOCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQ4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA4NjYuOTU3LDkwMi4wNzQgODM2LjUsOTI0LjE5OSA5NDUuMTIxLDEwNzMuNzMgOTc1LjU4NiwxMDUxLjYxIDg2Ni45NTcsOTAyLjA3NCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDUwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA2MDcuNDY1LDkwMy40NDUgNDk4Ljg1NSwxMDUyLjk3IDUyOS4zMiwxMDc1LjEgNjM3LjkzLDkyNS41NjYgNjA3LjQ2NSw5MDMuNDQ1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDM4MC42ODgsNjIyLjEyOSAtMTEuNjI2LDM1LjgwMSAxNzUuNzU4LDU3LjA5IDExLjYyMSwtMzUuODAxIC0xNzUuNzUzLC01Ny4wOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDU0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA3MTYuMjg5LDM3Ni41OSAzNy42NDA2LDAgMCwxODQuODE2IC0zNy42NDA2LDAgMCwtMTg0LjgxNiB6IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTYiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=') no-repeat, none; -moz-background-size: 100%; -o-background-size: 100%; -webkit-background-size: 100%; background-size: 100%; display: block; float: left; width: 90px; height: 25px; }
+.jasmine_html-reporter .banner .version { margin-left: 14px; position: relative; top: 6px; }
+.jasmine_html-reporter .banner .duration { position: absolute; right: 14px; top: 6px; }
+.jasmine_html-reporter #jasmine_content { position: fixed; right: 100%; }
+.jasmine_html-reporter .version { color: #aaa; }
+.jasmine_html-reporter .banner { margin-top: 14px; }
+.jasmine_html-reporter .duration { color: #aaa; float: right; }
+.jasmine_html-reporter .symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; }
+.jasmine_html-reporter .symbol-summary li { display: inline-block; height: 8px; width: 14px; font-size: 16px; }
+.jasmine_html-reporter .symbol-summary li.passed { font-size: 14px; }
+.jasmine_html-reporter .symbol-summary li.passed:before { color: #007069; content: "\02022"; }
+.jasmine_html-reporter .symbol-summary li.failed { line-height: 9px; }
+.jasmine_html-reporter .symbol-summary li.failed:before { color: #ca3a11; content: "\d7"; font-weight: bold; margin-left: -1px; }
+.jasmine_html-reporter .symbol-summary li.disabled { font-size: 14px; }
+.jasmine_html-reporter .symbol-summary li.disabled:before { color: #bababa; content: "\02022"; }
+.jasmine_html-reporter .symbol-summary li.pending { line-height: 17px; }
+.jasmine_html-reporter .symbol-summary li.pending:before { color: #ba9d37; content: "*"; }
+.jasmine_html-reporter .symbol-summary li.empty { font-size: 14px; }
+.jasmine_html-reporter .symbol-summary li.empty:before { color: #ba9d37; content: "\02022"; }
+.jasmine_html-reporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; }
+.jasmine_html-reporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
+.jasmine_html-reporter .bar.failed { background-color: #ca3a11; }
+.jasmine_html-reporter .bar.passed { background-color: #007069; }
+.jasmine_html-reporter .bar.skipped { background-color: #bababa; }
+.jasmine_html-reporter .bar.errored { background-color: #ca3a11; }
+.jasmine_html-reporter .bar.menu { background-color: #fff; color: #aaa; }
+.jasmine_html-reporter .bar.menu a { color: #333; }
+.jasmine_html-reporter .bar a { color: white; }
+.jasmine_html-reporter.spec-list .bar.menu.failure-list, .jasmine_html-reporter.spec-list .results .failures { display: none; }
+.jasmine_html-reporter.failure-list .bar.menu.spec-list, .jasmine_html-reporter.failure-list .summary { display: none; }
+.jasmine_html-reporter .running-alert { background-color: #666; }
+.jasmine_html-reporter .results { margin-top: 14px; }
+.jasmine_html-reporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; }
+.jasmine_html-reporter.showDetails .summaryMenuItem:hover { text-decoration: underline; }
+.jasmine_html-reporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; }
+.jasmine_html-reporter.showDetails .summary { display: none; }
+.jasmine_html-reporter.showDetails #details { display: block; }
+.jasmine_html-reporter .summaryMenuItem { font-weight: bold; text-decoration: underline; }
+.jasmine_html-reporter .summary { margin-top: 14px; }
+.jasmine_html-reporter .summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; }
+.jasmine_html-reporter .summary ul.suite { margin-top: 7px; margin-bottom: 7px; }
+.jasmine_html-reporter .summary li.passed a { color: #007069; }
+.jasmine_html-reporter .summary li.failed a { color: #ca3a11; }
+.jasmine_html-reporter .summary li.empty a { color: #ba9d37; }
+.jasmine_html-reporter .summary li.pending a { color: #ba9d37; }
+.jasmine_html-reporter .description + .suite { margin-top: 0; }
+.jasmine_html-reporter .suite { margin-top: 14px; }
+.jasmine_html-reporter .suite a { color: #333; }
+.jasmine_html-reporter .failures .spec-detail { margin-bottom: 28px; }
+.jasmine_html-reporter .failures .spec-detail .description { background-color: #ca3a11; }
+.jasmine_html-reporter .failures .spec-detail .description a { color: white; }
+.jasmine_html-reporter .result-message { padding-top: 14px; color: #333; white-space: pre; }
+.jasmine_html-reporter .result-message span.result { display: block; }
+.jasmine_html-reporter .stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666; border: 1px solid #ddd; background: white; white-space: pre; }
diff --git a/test/unittest/jasmine-2.2.0/jasmine.js b/test/unittest/jasmine-2.2.0/jasmine.js
new file mode 100644 (file)
index 0000000..6bf3f02
--- /dev/null
@@ -0,0 +1,3048 @@
+/*
+Copyright (c) 2008-2015 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+var getJasmineRequireObj = (function (jasmineGlobal) {
+  var jasmineRequire;
+
+  if (typeof module !== 'undefined' && module.exports) {
+    jasmineGlobal = global;
+    jasmineRequire = exports;
+  } else {
+    if (typeof window !== 'undefined' && typeof window.toString === 'function' && window.toString() === '[object GjsGlobal]') {
+      jasmineGlobal = window;
+    }
+    jasmineRequire = jasmineGlobal.jasmineRequire = jasmineGlobal.jasmineRequire || {};
+  }
+
+  function getJasmineRequire() {
+    return jasmineRequire;
+  }
+
+  getJasmineRequire().core = function(jRequire) {
+    var j$ = {};
+
+    jRequire.base(j$, jasmineGlobal);
+    j$.util = jRequire.util();
+    j$.Any = jRequire.Any();
+    j$.Anything = jRequire.Anything(j$);
+    j$.CallTracker = jRequire.CallTracker();
+    j$.MockDate = jRequire.MockDate();
+    j$.Clock = jRequire.Clock();
+    j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler();
+    j$.Env = jRequire.Env(j$);
+    j$.ExceptionFormatter = jRequire.ExceptionFormatter();
+    j$.Expectation = jRequire.Expectation();
+    j$.buildExpectationResult = jRequire.buildExpectationResult();
+    j$.JsApiReporter = jRequire.JsApiReporter();
+    j$.matchersUtil = jRequire.matchersUtil(j$);
+    j$.ObjectContaining = jRequire.ObjectContaining(j$);
+    j$.ArrayContaining = jRequire.ArrayContaining(j$);
+    j$.pp = jRequire.pp(j$);
+    j$.QueueRunner = jRequire.QueueRunner(j$);
+    j$.ReportDispatcher = jRequire.ReportDispatcher();
+    j$.Spec = jRequire.Spec(j$);
+    j$.SpyRegistry = jRequire.SpyRegistry(j$);
+    j$.SpyStrategy = jRequire.SpyStrategy();
+    j$.StringMatching = jRequire.StringMatching(j$);
+    j$.Suite = jRequire.Suite();
+    j$.Timer = jRequire.Timer();
+    j$.version = jRequire.version();
+
+    j$.matchers = jRequire.requireMatchers(jRequire, j$);
+
+    return j$;
+  };
+
+  return getJasmineRequire;
+})(this);
+
+getJasmineRequireObj().requireMatchers = function(jRequire, j$) {
+  var availableMatchers = [
+      'toBe',
+      'toBeCloseTo',
+      'toBeDefined',
+      'toBeFalsy',
+      'toBeGreaterThan',
+      'toBeLessThan',
+      'toBeNaN',
+      'toBeNull',
+      'toBeTruthy',
+      'toBeUndefined',
+      'toContain',
+      'toEqual',
+      'toHaveBeenCalled',
+      'toHaveBeenCalledWith',
+      'toMatch',
+      'toThrow',
+      'toThrowError'
+    ],
+    matchers = {};
+
+  for (var i = 0; i < availableMatchers.length; i++) {
+    var name = availableMatchers[i];
+    matchers[name] = jRequire[name](j$);
+  }
+
+  return matchers;
+};
+
+getJasmineRequireObj().base = function(j$, jasmineGlobal) {
+  j$.unimplementedMethod_ = function() {
+    throw new Error('unimplemented method');
+  };
+
+  j$.MAX_PRETTY_PRINT_DEPTH = 40;
+  j$.MAX_PRETTY_PRINT_ARRAY_LENGTH = 100;
+  j$.DEFAULT_TIMEOUT_INTERVAL = 5000;
+
+  j$.getGlobal = function() {
+    return jasmineGlobal;
+  };
+
+  j$.getEnv = function(options) {
+    var env = j$.currentEnv_ = j$.currentEnv_ || new j$.Env(options);
+    //jasmine. singletons in here (setTimeout blah blah).
+    return env;
+  };
+
+  j$.isArray_ = function(value) {
+    return j$.isA_('Array', value);
+  };
+
+  j$.isString_ = function(value) {
+    return j$.isA_('String', value);
+  };
+
+  j$.isNumber_ = function(value) {
+    return j$.isA_('Number', value);
+  };
+
+  j$.isA_ = function(typeName, value) {
+    return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';
+  };
+
+  j$.isDomNode = function(obj) {
+    return obj.nodeType > 0;
+  };
+
+  j$.fnNameFor = function(func) {
+    return func.name || func.toString().match(/^\s*function\s*(\w*)\s*\(/)[1];
+  };
+
+  j$.any = function(clazz) {
+    return new j$.Any(clazz);
+  };
+
+  j$.anything = function() {
+    return new j$.Anything();
+  };
+
+  j$.objectContaining = function(sample) {
+    return new j$.ObjectContaining(sample);
+  };
+
+  j$.stringMatching = function(expected) {
+    return new j$.StringMatching(expected);
+  };
+
+  j$.arrayContaining = function(sample) {
+    return new j$.ArrayContaining(sample);
+  };
+
+  j$.createSpy = function(name, originalFn) {
+
+    var spyStrategy = new j$.SpyStrategy({
+        name: name,
+        fn: originalFn,
+        getSpy: function() { return spy; }
+      }),
+      callTracker = new j$.CallTracker(),
+      spy = function() {
+        var callData = {
+          object: this,
+          args: Array.prototype.slice.apply(arguments)
+        };
+
+        callTracker.track(callData);
+        var returnValue = spyStrategy.exec.apply(this, arguments);
+        callData.returnValue = returnValue;
+
+        return returnValue;
+      };
+
+    for (var prop in originalFn) {
+      if (prop === 'and' || prop === 'calls') {
+        throw new Error('Jasmine spies would overwrite the \'and\' and \'calls\' properties on the object being spied upon');
+      }
+
+      spy[prop] = originalFn[prop];
+    }
+
+    spy.and = spyStrategy;
+    spy.calls = callTracker;
+
+    return spy;
+  };
+
+  j$.isSpy = function(putativeSpy) {
+    if (!putativeSpy) {
+      return false;
+    }
+    return putativeSpy.and instanceof j$.SpyStrategy &&
+      putativeSpy.calls instanceof j$.CallTracker;
+  };
+
+  j$.createSpyObj = function(baseName, methodNames) {
+    if (j$.isArray_(baseName) && j$.util.isUndefined(methodNames)) {
+      methodNames = baseName;
+      baseName = 'unknown';
+    }
+
+    if (!j$.isArray_(methodNames) || methodNames.length === 0) {
+      throw 'createSpyObj requires a non-empty array of method names to create spies for';
+    }
+    var obj = {};
+    for (var i = 0; i < methodNames.length; i++) {
+      obj[methodNames[i]] = j$.createSpy(baseName + '.' + methodNames[i]);
+    }
+    return obj;
+  };
+};
+
+getJasmineRequireObj().util = function() {
+
+  var util = {};
+
+  util.inherit = function(childClass, parentClass) {
+    var Subclass = function() {
+    };
+    Subclass.prototype = parentClass.prototype;
+    childClass.prototype = new Subclass();
+  };
+
+  util.htmlEscape = function(str) {
+    if (!str) {
+      return str;
+    }
+    return str.replace(/&/g, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;');
+  };
+
+  util.argsToArray = function(args) {
+    var arrayOfArgs = [];
+    for (var i = 0; i < args.length; i++) {
+      arrayOfArgs.push(args[i]);
+    }
+    return arrayOfArgs;
+  };
+
+  util.isUndefined = function(obj) {
+    return obj === void 0;
+  };
+
+  util.arrayContains = function(array, search) {
+    var i = array.length;
+    while (i--) {
+      if (array[i] === search) {
+        return true;
+      }
+    }
+    return false;
+  };
+
+  util.clone = function(obj) {
+    if (Object.prototype.toString.apply(obj) === '[object Array]') {
+      return obj.slice();
+    }
+
+    var cloned = {};
+    for (var prop in obj) {
+      if (obj.hasOwnProperty(prop)) {
+        cloned[prop] = obj[prop];
+      }
+    }
+
+    return cloned;
+  };
+
+  return util;
+};
+
+getJasmineRequireObj().Spec = function(j$) {
+  function Spec(attrs) {
+    this.expectationFactory = attrs.expectationFactory;
+    this.resultCallback = attrs.resultCallback || function() {};
+    this.id = attrs.id;
+    this.description = attrs.description || '';
+    this.queueableFn = attrs.queueableFn;
+    this.beforeAndAfterFns = attrs.beforeAndAfterFns || function() { return {befores: [], afters: []}; };
+    this.userContext = attrs.userContext || function() { return {}; };
+    this.onStart = attrs.onStart || function() {};
+    this.getSpecName = attrs.getSpecName || function() { return ''; };
+    this.expectationResultFactory = attrs.expectationResultFactory || function() { };
+    this.queueRunnerFactory = attrs.queueRunnerFactory || function() {};
+    this.catchingExceptions = attrs.catchingExceptions || function() { return true; };
+
+    if (!this.queueableFn.fn) {
+      this.pend();
+    }
+
+    this.result = {
+      id: this.id,
+      description: this.description,
+      fullName: this.getFullName(),
+      failedExpectations: [],
+      passedExpectations: [],
+      pendingReason: ''
+    };
+  }
+
+  Spec.prototype.addExpectationResult = function(passed, data) {
+    var expectationResult = this.expectationResultFactory(data);
+    if (passed) {
+      this.result.passedExpectations.push(expectationResult);
+    } else {
+      this.result.failedExpectations.push(expectationResult);
+    }
+  };
+
+  Spec.prototype.expect = function(actual) {
+    return this.expectationFactory(actual, this);
+  };
+
+  Spec.prototype.execute = function(onComplete) {
+    var self = this;
+
+    this.onStart(this);
+
+    if (this.markedPending || this.disabled) {
+      complete();
+      return;
+    }
+
+    var fns = this.beforeAndAfterFns();
+    var allFns = fns.befores.concat(this.queueableFn).concat(fns.afters);
+
+    this.queueRunnerFactory({
+      queueableFns: allFns,
+      onException: function() { self.onException.apply(self, arguments); },
+      onComplete: complete,
+      userContext: this.userContext()
+    });
+
+    function complete() {
+      self.result.status = self.status();
+      self.resultCallback(self.result);
+
+      if (onComplete) {
+        onComplete();
+      }
+    }
+  };
+
+  Spec.prototype.onException = function onException(e) {
+    if (Spec.isPendingSpecException(e)) {
+      this.pend(extractCustomPendingMessage(e));
+      return;
+    }
+
+    this.addExpectationResult(false, {
+      matcherName: '',
+      passed: false,
+      expected: '',
+      actual: '',
+      error: e
+    });
+  };
+
+  Spec.prototype.disable = function() {
+    this.disabled = true;
+  };
+
+  Spec.prototype.pend = function(message) {
+    this.markedPending = true;
+    if (message) {
+      this.result.pendingReason = message;
+    }
+  };
+
+  Spec.prototype.status = function() {
+    if (this.disabled) {
+      return 'disabled';
+    }
+
+    if (this.markedPending) {
+      return 'pending';
+    }
+
+    if (this.result.failedExpectations.length > 0) {
+      return 'failed';
+    } else {
+      return 'passed';
+    }
+  };
+
+  Spec.prototype.isExecutable = function() {
+    return !this.disabled && !this.markedPending;
+  };
+
+  Spec.prototype.getFullName = function() {
+    return this.getSpecName(this);
+  };
+
+  var extractCustomPendingMessage = function(e) {
+    var fullMessage = e.toString(),
+        boilerplateStart = fullMessage.indexOf(Spec.pendingSpecExceptionMessage),
+        boilerplateEnd = boilerplateStart + Spec.pendingSpecExceptionMessage.length;
+
+    return fullMessage.substr(boilerplateEnd);
+  };
+
+  Spec.pendingSpecExceptionMessage = '=> marked Pending';
+
+  Spec.isPendingSpecException = function(e) {
+    return !!(e && e.toString && e.toString().indexOf(Spec.pendingSpecExceptionMessage) !== -1);
+  };
+
+  return Spec;
+};
+
+if (typeof window == void 0 && typeof exports == 'object') {
+  exports.Spec = jasmineRequire.Spec;
+}
+
+getJasmineRequireObj().Env = function(j$) {
+  function Env(options) {
+    options = options || {};
+
+    var self = this;
+    var global = options.global || j$.getGlobal();
+
+    var totalSpecsDefined = 0;
+
+    var catchExceptions = true;
+
+    var realSetTimeout = j$.getGlobal().setTimeout;
+    var realClearTimeout = j$.getGlobal().clearTimeout;
+    this.clock = new j$.Clock(global, new j$.DelayedFunctionScheduler(), new j$.MockDate(global));
+
+    var runnableLookupTable = {};
+    var runnableResources = {};
+
+    var currentSpec = null;
+    var currentlyExecutingSuites = [];
+    var currentDeclarationSuite = null;
+
+    var currentSuite = function() {
+      return currentlyExecutingSuites[currentlyExecutingSuites.length - 1];
+    };
+
+    var currentRunnable = function() {
+      return currentSpec || currentSuite();
+    };
+
+    var reporter = new j$.ReportDispatcher([
+      'jasmineStarted',
+      'jasmineDone',
+      'suiteStarted',
+      'suiteDone',
+      'specStarted',
+      'specDone'
+    ]);
+
+    this.specFilter = function() {
+      return true;
+    };
+
+    this.addCustomEqualityTester = function(tester) {
+      if(!currentRunnable()) {
+        throw new Error('Custom Equalities must be added in a before function or a spec');
+      }
+      runnableResources[currentRunnable().id].customEqualityTesters.push(tester);
+    };
+
+    this.addMatchers = function(matchersToAdd) {
+      if(!currentRunnable()) {
+        throw new Error('Matchers must be added in a before function or a spec');
+      }
+      var customMatchers = runnableResources[currentRunnable().id].customMatchers;
+      for (var matcherName in matchersToAdd) {
+        customMatchers[matcherName] = matchersToAdd[matcherName];
+      }
+    };
+
+    j$.Expectation.addCoreMatchers(j$.matchers);
+
+    var nextSpecId = 0;
+    var getNextSpecId = function() {
+      return 'spec' + nextSpecId++;
+    };
+
+    var nextSuiteId = 0;
+    var getNextSuiteId = function() {
+      return 'suite' + nextSuiteId++;
+    };
+
+    var expectationFactory = function(actual, spec) {
+      return j$.Expectation.Factory({
+        util: j$.matchersUtil,
+        customEqualityTesters: runnableResources[spec.id].customEqualityTesters,
+        customMatchers: runnableResources[spec.id].customMatchers,
+        actual: actual,
+        addExpectationResult: addExpectationResult
+      });
+
+      function addExpectationResult(passed, result) {
+        return spec.addExpectationResult(passed, result);
+      }
+    };
+
+    var defaultResourcesForRunnable = function(id, parentRunnableId) {
+      var resources = {spies: [], customEqualityTesters: [], customMatchers: {}};
+
+      if(runnableResources[parentRunnableId]){
+        resources.customEqualityTesters = j$.util.clone(runnableResources[parentRunnableId].customEqualityTesters);
+        resources.customMatchers = j$.util.clone(runnableResources[parentRunnableId].customMatchers);
+      }
+
+      runnableResources[id] = resources;
+    };
+
+    var clearResourcesForRunnable = function(id) {
+        spyRegistry.clearSpies();
+        delete runnableResources[id];
+    };
+
+    var beforeAndAfterFns = function(suite, runnablesExplictlySet) {
+      return function() {
+        var befores = [],
+          afters = [],
+          beforeAlls = [],
+          afterAlls = [];
+
+        while(suite) {
+          befores = befores.concat(suite.beforeFns);
+          afters = afters.concat(suite.afterFns);
+
+          if (runnablesExplictlySet()) {
+            beforeAlls = beforeAlls.concat(suite.beforeAllFns);
+            afterAlls = afterAlls.concat(suite.afterAllFns);
+          }
+
+          suite = suite.parentSuite;
+        }
+        return {
+          befores: beforeAlls.reverse().concat(befores.reverse()),
+          afters: afters.concat(afterAlls)
+        };
+      };
+    };
+
+    var getSpecName = function(spec, suite) {
+      return suite.getFullName() + ' ' + spec.description;
+    };
+
+    // TODO: we may just be able to pass in the fn instead of wrapping here
+    var buildExpectationResult = j$.buildExpectationResult,
+        exceptionFormatter = new j$.ExceptionFormatter(),
+        expectationResultFactory = function(attrs) {
+          attrs.messageFormatter = exceptionFormatter.message;
+          attrs.stackFormatter = exceptionFormatter.stack;
+
+          return buildExpectationResult(attrs);
+        };
+
+    // TODO: fix this naming, and here's where the value comes in
+    this.catchExceptions = function(value) {
+      catchExceptions = !!value;
+      return catchExceptions;
+    };
+
+    this.catchingExceptions = function() {
+      return catchExceptions;
+    };
+
+    var maximumSpecCallbackDepth = 20;
+    var currentSpecCallbackDepth = 0;
+
+    function clearStack(fn) {
+      currentSpecCallbackDepth++;
+      if (currentSpecCallbackDepth >= maximumSpecCallbackDepth) {
+        currentSpecCallbackDepth = 0;
+        realSetTimeout(fn, 0);
+      } else {
+        fn();
+      }
+    }
+
+    var catchException = function(e) {
+      return j$.Spec.isPendingSpecException(e) || catchExceptions;
+    };
+
+    var queueRunnerFactory = function(options) {
+      options.catchException = catchException;
+      options.clearStack = options.clearStack || clearStack;
+      options.timer = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout};
+      options.fail = self.fail;
+
+      new j$.QueueRunner(options).execute();
+    };
+
+    var topSuite = new j$.Suite({
+      env: this,
+      id: getNextSuiteId(),
+      description: 'Jasmine__TopLevel__Suite',
+      queueRunner: queueRunnerFactory
+    });
+    runnableLookupTable[topSuite.id] = topSuite;
+    defaultResourcesForRunnable(topSuite.id);
+    currentDeclarationSuite = topSuite;
+
+    this.topSuite = function() {
+      return topSuite;
+    };
+
+    this.execute = function(runnablesToRun) {
+      if(runnablesToRun) {
+        runnablesExplictlySet = true;
+      } else if (focusedRunnables.length) {
+        runnablesExplictlySet = true;
+        runnablesToRun = focusedRunnables;
+      } else {
+        runnablesToRun = [topSuite.id];
+      }
+
+      var allFns = [];
+      for(var i = 0; i < runnablesToRun.length; i++) {
+        var runnable = runnableLookupTable[runnablesToRun[i]];
+        allFns.push((function(runnable) { return { fn: function(done) { runnable.execute(done); } }; })(runnable));
+      }
+
+      reporter.jasmineStarted({
+        totalSpecsDefined: totalSpecsDefined
+      });
+
+      queueRunnerFactory({queueableFns: allFns, onComplete: reporter.jasmineDone});
+    };
+
+    this.addReporter = function(reporterToAdd) {
+      reporter.addReporter(reporterToAdd);
+    };
+
+    var spyRegistry = new j$.SpyRegistry({currentSpies: function() {
+      if(!currentRunnable()) {
+        throw new Error('Spies must be created in a before function or a spec');
+      }
+      return runnableResources[currentRunnable().id].spies;
+    }});
+
+    this.spyOn = function() {
+      return spyRegistry.spyOn.apply(spyRegistry, arguments);
+    };
+
+    var suiteFactory = function(description) {
+      var suite = new j$.Suite({
+        env: self,
+        id: getNextSuiteId(),
+        description: description,
+        parentSuite: currentDeclarationSuite,
+        queueRunner: queueRunnerFactory,
+        onStart: suiteStarted,
+        expectationFactory: expectationFactory,
+        expectationResultFactory: expectationResultFactory,
+        runnablesExplictlySetGetter: runnablesExplictlySetGetter,
+        resultCallback: function(attrs) {
+          if (!suite.disabled) {
+            clearResourcesForRunnable(suite.id);
+          }
+          currentlyExecutingSuites.pop();
+          reporter.suiteDone(attrs);
+        }
+      });
+
+      runnableLookupTable[suite.id] = suite;
+      return suite;
+
+      function suiteStarted(suite) {
+        currentlyExecutingSuites.push(suite);
+        defaultResourcesForRunnable(suite.id, suite.parentSuite.id);
+        reporter.suiteStarted(suite.result);
+      }
+    };
+
+    this.describe = function(description, specDefinitions) {
+      var suite = suiteFactory(description);
+      addSpecsToSuite(suite, specDefinitions);
+      return suite;
+    };
+
+    this.xdescribe = function(description, specDefinitions) {
+      var suite = this.describe(description, specDefinitions);
+      suite.disable();
+      return suite;
+    };
+
+    var focusedRunnables = [];
+
+    this.fdescribe = function(description, specDefinitions) {
+      var suite = suiteFactory(description);
+      suite.isFocused = true;
+
+      focusedRunnables.push(suite.id);
+      unfocusAncestor();
+      addSpecsToSuite(suite, specDefinitions);
+
+      return suite;
+    };
+
+    function addSpecsToSuite(suite, specDefinitions) {
+      var parentSuite = currentDeclarationSuite;
+      parentSuite.addChild(suite);
+      currentDeclarationSuite = suite;
+
+      var declarationError = null;
+      try {
+        specDefinitions.call(suite);
+      } catch (e) {
+        declarationError = e;
+      }
+
+      if (declarationError) {
+        self.it('encountered a declaration exception', function() {
+          throw declarationError;
+        });
+      }
+
+      currentDeclarationSuite = parentSuite;
+    }
+
+    function findFocusedAncestor(suite) {
+      while (suite) {
+        if (suite.isFocused) {
+          return suite.id;
+        }
+        suite = suite.parentSuite;
+      }
+
+      return null;
+    }
+
+    function unfocusAncestor() {
+      var focusedAncestor = findFocusedAncestor(currentDeclarationSuite);
+      if (focusedAncestor) {
+        for (var i = 0; i < focusedRunnables.length; i++) {
+          if (focusedRunnables[i] === focusedAncestor) {
+            focusedRunnables.splice(i, 1);
+            break;
+          }
+        }
+      }
+    }
+
+    var runnablesExplictlySet = false;
+
+    var runnablesExplictlySetGetter = function(){
+      return runnablesExplictlySet;
+    };
+
+    var specFactory = function(description, fn, suite, timeout) {
+      totalSpecsDefined++;
+      var spec = new j$.Spec({
+        id: getNextSpecId(),
+        beforeAndAfterFns: beforeAndAfterFns(suite, runnablesExplictlySetGetter),
+        expectationFactory: expectationFactory,
+        resultCallback: specResultCallback,
+        getSpecName: function(spec) {
+          return getSpecName(spec, suite);
+        },
+        onStart: specStarted,
+        description: description,
+        expectationResultFactory: expectationResultFactory,
+        queueRunnerFactory: queueRunnerFactory,
+        userContext: function() { return suite.clonedSharedUserContext(); },
+        queueableFn: {
+          fn: fn,
+          timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; }
+        }
+      });
+
+      runnableLookupTable[spec.id] = spec;
+
+      if (!self.specFilter(spec)) {
+        spec.disable();
+      }
+
+      return spec;
+
+      function specResultCallback(result) {
+        clearResourcesForRunnable(spec.id);
+        currentSpec = null;
+        reporter.specDone(result);
+      }
+
+      function specStarted(spec) {
+        currentSpec = spec;
+        defaultResourcesForRunnable(spec.id, suite.id);
+        reporter.specStarted(spec.result);
+      }
+    };
+
+    this.it = function(description, fn, timeout) {
+      var spec = specFactory(description, fn, currentDeclarationSuite, timeout);
+      currentDeclarationSuite.addChild(spec);
+      return spec;
+    };
+
+    this.xit = function() {
+      var spec = this.it.apply(this, arguments);
+      spec.pend();
+      return spec;
+    };
+
+    this.fit = function(){
+      var spec = this.it.apply(this, arguments);
+
+      focusedRunnables.push(spec.id);
+      unfocusAncestor();
+      return spec;
+    };
+
+    this.expect = function(actual) {
+      if (!currentRunnable()) {
+        throw new Error('\'expect\' was used when there was no current spec, this could be because an asynchronous test timed out');
+      }
+
+      return currentRunnable().expect(actual);
+    };
+
+    this.beforeEach = function(beforeEachFunction, timeout) {
+      currentDeclarationSuite.beforeEach({
+        fn: beforeEachFunction,
+        timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; }
+      });
+    };
+
+    this.beforeAll = function(beforeAllFunction, timeout) {
+      currentDeclarationSuite.beforeAll({
+        fn: beforeAllFunction,
+        timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; }
+      });
+    };
+
+    this.afterEach = function(afterEachFunction, timeout) {
+      currentDeclarationSuite.afterEach({
+        fn: afterEachFunction,
+        timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; }
+      });
+    };
+
+    this.afterAll = function(afterAllFunction, timeout) {
+      currentDeclarationSuite.afterAll({
+        fn: afterAllFunction,
+        timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; }
+      });
+    };
+
+    this.pending = function(message) {
+      var fullMessage = j$.Spec.pendingSpecExceptionMessage;
+      if(message) {
+        fullMessage += message;
+      }
+      throw fullMessage;
+    };
+
+    this.fail = function(error) {
+      var message = 'Failed';
+      if (error) {
+        message += ': ';
+        message += error.message || error;
+      }
+
+      currentRunnable().addExpectationResult(false, {
+        matcherName: '',
+        passed: false,
+        expected: '',
+        actual: '',
+        message: message,
+        error: error && error.message ? error : null
+      });
+    };
+  }
+
+  return Env;
+};
+
+getJasmineRequireObj().JsApiReporter = function() {
+
+  var noopTimer = {
+    start: function(){},
+    elapsed: function(){ return 0; }
+  };
+
+  function JsApiReporter(options) {
+    var timer = options.timer || noopTimer,
+        status = 'loaded';
+
+    this.started = false;
+    this.finished = false;
+
+    this.jasmineStarted = function() {
+      this.started = true;
+      status = 'started';
+      timer.start();
+    };
+
+    var executionTime;
+
+    this.jasmineDone = function() {
+      this.finished = true;
+      executionTime = timer.elapsed();
+      status = 'done';
+    };
+
+    this.status = function() {
+      return status;
+    };
+
+    var suites = [],
+      suites_hash = {};
+
+    this.suiteStarted = function(result) {
+      suites_hash[result.id] = result;
+    };
+
+    this.suiteDone = function(result) {
+      storeSuite(result);
+    };
+
+    this.suiteResults = function(index, length) {
+      return suites.slice(index, index + length);
+    };
+
+    function storeSuite(result) {
+      suites.push(result);
+      suites_hash[result.id] = result;
+    }
+
+    this.suites = function() {
+      return suites_hash;
+    };
+
+    var specs = [];
+
+    this.specDone = function(result) {
+      specs.push(result);
+    };
+
+    this.specResults = function(index, length) {
+      return specs.slice(index, index + length);
+    };
+
+    this.specs = function() {
+      return specs;
+    };
+
+    this.executionTime = function() {
+      return executionTime;
+    };
+
+  }
+
+  return JsApiReporter;
+};
+
+getJasmineRequireObj().CallTracker = function() {
+
+  function CallTracker() {
+    var calls = [];
+
+    this.track = function(context) {
+      calls.push(context);
+    };
+
+    this.any = function() {
+      return !!calls.length;
+    };
+
+    this.count = function() {
+      return calls.length;
+    };
+
+    this.argsFor = function(index) {
+      var call = calls[index];
+      return call ? call.args : [];
+    };
+
+    this.all = function() {
+      return calls;
+    };
+
+    this.allArgs = function() {
+      var callArgs = [];
+      for(var i = 0; i < calls.length; i++){
+        callArgs.push(calls[i].args);
+      }
+
+      return callArgs;
+    };
+
+    this.first = function() {
+      return calls[0];
+    };
+
+    this.mostRecent = function() {
+      return calls[calls.length - 1];
+    };
+
+    this.reset = function() {
+      calls = [];
+    };
+  }
+
+  return CallTracker;
+};
+
+getJasmineRequireObj().Clock = function() {
+  function Clock(global, delayedFunctionScheduler, mockDate) {
+    var self = this,
+      realTimingFunctions = {
+        setTimeout: global.setTimeout,
+        clearTimeout: global.clearTimeout,
+        setInterval: global.setInterval,
+        clearInterval: global.clearInterval
+      },
+      fakeTimingFunctions = {
+        setTimeout: setTimeout,
+        clearTimeout: clearTimeout,
+        setInterval: setInterval,
+        clearInterval: clearInterval
+      },
+      installed = false,
+      timer;
+
+
+    self.install = function() {
+      replace(global, fakeTimingFunctions);
+      timer = fakeTimingFunctions;
+      installed = true;
+
+      return self;
+    };
+
+    self.uninstall = function() {
+      delayedFunctionScheduler.reset();
+      mockDate.uninstall();
+      replace(global, realTimingFunctions);
+
+      timer = realTimingFunctions;
+      installed = false;
+    };
+
+    self.mockDate = function(initialDate) {
+      mockDate.install(initialDate);
+    };
+
+    self.setTimeout = function(fn, delay, params) {
+      if (legacyIE()) {
+        if (arguments.length > 2) {
+          throw new Error('IE < 9 cannot support extra params to setTimeout without a polyfill');
+        }
+        return timer.setTimeout(fn, delay);
+      }
+      return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]);
+    };
+
+    self.setInterval = function(fn, delay, params) {
+      if (legacyIE()) {
+        if (arguments.length > 2) {
+          throw new Error('IE < 9 cannot support extra params to setInterval without a polyfill');
+        }
+        return timer.setInterval(fn, delay);
+      }
+      return Function.prototype.apply.apply(timer.setInterval, [global, arguments]);
+    };
+
+    self.clearTimeout = function(id) {
+      return Function.prototype.call.apply(timer.clearTimeout, [global, id]);
+    };
+
+    self.clearInterval = function(id) {
+      return Function.prototype.call.apply(timer.clearInterval, [global, id]);
+    };
+
+    self.tick = function(millis) {
+      if (installed) {
+        mockDate.tick(millis);
+        delayedFunctionScheduler.tick(millis);
+      } else {
+        throw new Error('Mock clock is not installed, use jasmine.clock().install()');
+      }
+    };
+
+    return self;
+
+    function legacyIE() {
+      //if these methods are polyfilled, apply will be present
+      return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply;
+    }
+
+    function replace(dest, source) {
+      for (var prop in source) {
+        dest[prop] = source[prop];
+      }
+    }
+
+    function setTimeout(fn, delay) {
+      return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2));
+    }
+
+    function clearTimeout(id) {
+      return delayedFunctionScheduler.removeFunctionWithId(id);
+    }
+
+    function setInterval(fn, interval) {
+      return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true);
+    }
+
+    function clearInterval(id) {
+      return delayedFunctionScheduler.removeFunctionWithId(id);
+    }
+
+    function argSlice(argsObj, n) {
+      return Array.prototype.slice.call(argsObj, n);
+    }
+  }
+
+  return Clock;
+};
+
+getJasmineRequireObj().DelayedFunctionScheduler = function() {
+  function DelayedFunctionScheduler() {
+    var self = this;
+    var scheduledLookup = [];
+    var scheduledFunctions = {};
+    var currentTime = 0;
+    var delayedFnCount = 0;
+
+    self.tick = function(millis) {
+      millis = millis || 0;
+      var endTime = currentTime + millis;
+
+      runScheduledFunctions(endTime);
+      currentTime = endTime;
+    };
+
+    self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) {
+      var f;
+      if (typeof(funcToCall) === 'string') {
+        /* jshint evil: true */
+        f = function() { return eval(funcToCall); };
+        /* jshint evil: false */
+      } else {
+        f = funcToCall;
+      }
+
+      millis = millis || 0;
+      timeoutKey = timeoutKey || ++delayedFnCount;
+      runAtMillis = runAtMillis || (currentTime + millis);
+
+      var funcToSchedule = {
+        runAtMillis: runAtMillis,
+        funcToCall: f,
+        recurring: recurring,
+        params: params,
+        timeoutKey: timeoutKey,
+        millis: millis
+      };
+
+      if (runAtMillis in scheduledFunctions) {
+        scheduledFunctions[runAtMillis].push(funcToSchedule);
+      } else {
+        scheduledFunctions[runAtMillis] = [funcToSchedule];
+        scheduledLookup.push(runAtMillis);
+        scheduledLookup.sort(function (a, b) {
+          return a - b;
+        });
+      }
+
+      return timeoutKey;
+    };
+
+    self.removeFunctionWithId = function(timeoutKey) {
+      for (var runAtMillis in scheduledFunctions) {
+        var funcs = scheduledFunctions[runAtMillis];
+        var i = indexOfFirstToPass(funcs, function (func) {
+          return func.timeoutKey === timeoutKey;
+        });
+
+        if (i > -1) {
+          if (funcs.length === 1) {
+            delete scheduledFunctions[runAtMillis];
+            deleteFromLookup(runAtMillis);
+          } else {
+            funcs.splice(i, 1);
+          }
+
+          // intervals get rescheduled when executed, so there's never more
+          // than a single scheduled function with a given timeoutKey
+          break;
+        }
+      }
+    };
+
+    self.reset = function() {
+      currentTime = 0;
+      scheduledLookup = [];
+      scheduledFunctions = {};
+      delayedFnCount = 0;
+    };
+
+    return self;
+
+    function indexOfFirstToPass(array, testFn) {
+      var index = -1;
+
+      for (var i = 0; i < array.length; ++i) {
+        if (testFn(array[i])) {
+          index = i;
+          break;
+        }
+      }
+
+      return index;
+    }
+
+    function deleteFromLookup(key) {
+      var value = Number(key);
+      var i = indexOfFirstToPass(scheduledLookup, function (millis) {
+        return millis === value;
+      });
+
+      if (i > -1) {
+        scheduledLookup.splice(i, 1);
+      }
+    }
+
+    function reschedule(scheduledFn) {
+      self.scheduleFunction(scheduledFn.funcToCall,
+        scheduledFn.millis,
+        scheduledFn.params,
+        true,
+        scheduledFn.timeoutKey,
+        scheduledFn.runAtMillis + scheduledFn.millis);
+    }
+
+    function forEachFunction(funcsToRun, callback) {
+      for (var i = 0; i < funcsToRun.length; ++i) {
+        callback(funcsToRun[i]);
+      }
+    }
+
+    function runScheduledFunctions(endTime) {
+      if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) {
+        return;
+      }
+
+      do {
+        currentTime = scheduledLookup.shift();
+
+        var funcsToRun = scheduledFunctions[currentTime];
+        delete scheduledFunctions[currentTime];
+
+        forEachFunction(funcsToRun, function(funcToRun) {
+          if (funcToRun.recurring) {
+            reschedule(funcToRun);
+          }
+        });
+
+        forEachFunction(funcsToRun, function(funcToRun) {
+          funcToRun.funcToCall.apply(null, funcToRun.params || []);
+        });
+      } while (scheduledLookup.length > 0 &&
+              // checking first if we're out of time prevents setTimeout(0)
+              // scheduled in a funcToRun from forcing an extra iteration
+                 currentTime !== endTime  &&
+                 scheduledLookup[0] <= endTime);
+    }
+  }
+
+  return DelayedFunctionScheduler;
+};
+
+getJasmineRequireObj().ExceptionFormatter = function() {
+  function ExceptionFormatter() {
+    this.message = function(error) {
+      var message = '';
+
+      if (error.name && error.message) {
+        message += error.name + ': ' + error.message;
+      } else {
+        message += error.toString() + ' thrown';
+      }
+
+      if (error.fileName || error.sourceURL) {
+        message += ' in ' + (error.fileName || error.sourceURL);
+      }
+
+      if (error.line || error.lineNumber) {
+        message += ' (line ' + (error.line || error.lineNumber) + ')';
+      }
+
+      return message;
+    };
+
+    this.stack = function(error) {
+      return error ? error.stack : null;
+    };
+  }
+
+  return ExceptionFormatter;
+};
+
+getJasmineRequireObj().Expectation = function() {
+
+  function Expectation(options) {
+    this.util = options.util || { buildFailureMessage: function() {} };
+    this.customEqualityTesters = options.customEqualityTesters || [];
+    this.actual = options.actual;
+    this.addExpectationResult = options.addExpectationResult || function(){};
+    this.isNot = options.isNot;
+
+    var customMatchers = options.customMatchers || {};
+    for (var matcherName in customMatchers) {
+      this[matcherName] = Expectation.prototype.wrapCompare(matcherName, customMatchers[matcherName]);
+    }
+  }
+
+  Expectation.prototype.wrapCompare = function(name, matcherFactory) {
+    return function() {
+      var args = Array.prototype.slice.call(arguments, 0),
+        expected = args.slice(0),
+        message = '';
+
+      args.unshift(this.actual);
+
+      var matcher = matcherFactory(this.util, this.customEqualityTesters),
+          matcherCompare = matcher.compare;
+
+      function defaultNegativeCompare() {
+        var result = matcher.compare.apply(null, args);
+        result.pass = !result.pass;
+        return result;
+      }
+
+      if (this.isNot) {
+        matcherCompare = matcher.negativeCompare || defaultNegativeCompare;
+      }
+
+      var result = matcherCompare.apply(null, args);
+
+      if (!result.pass) {
+        if (!result.message) {
+          args.unshift(this.isNot);
+          args.unshift(name);
+          message = this.util.buildFailureMessage.apply(null, args);
+        } else {
+          if (Object.prototype.toString.apply(result.message) === '[object Function]') {
+            message = result.message();
+          } else {
+            message = result.message;
+          }
+        }
+      }
+
+      if (expected.length == 1) {
+        expected = expected[0];
+      }
+
+      // TODO: how many of these params are needed?
+      this.addExpectationResult(
+        result.pass,
+        {
+          matcherName: name,
+          passed: result.pass,
+          message: message,
+          actual: this.actual,
+          expected: expected // TODO: this may need to be arrayified/sliced
+        }
+      );
+    };
+  };
+
+  Expectation.addCoreMatchers = function(matchers) {
+    var prototype = Expectation.prototype;
+    for (var matcherName in matchers) {
+      var matcher = matchers[matcherName];
+      prototype[matcherName] = prototype.wrapCompare(matcherName, matcher);
+    }
+  };
+
+  Expectation.Factory = function(options) {
+    options = options || {};
+
+    var expect = new Expectation(options);
+
+    // TODO: this would be nice as its own Object - NegativeExpectation
+    // TODO: copy instead of mutate options
+    options.isNot = true;
+    expect.not = new Expectation(options);
+
+    return expect;
+  };
+
+  return Expectation;
+};
+
+//TODO: expectation result may make more sense as a presentation of an expectation.
+getJasmineRequireObj().buildExpectationResult = function() {
+  function buildExpectationResult(options) {
+    var messageFormatter = options.messageFormatter || function() {},
+      stackFormatter = options.stackFormatter || function() {};
+
+    var result = {
+      matcherName: options.matcherName,
+      message: message(),
+      stack: stack(),
+      passed: options.passed
+    };
+
+    if(!result.passed) {
+      result.expected = options.expected;
+      result.actual = options.actual;
+    }
+
+    return result;
+
+    function message() {
+      if (options.passed) {
+        return 'Passed.';
+      } else if (options.message) {
+        return options.message;
+      } else if (options.error) {
+        return messageFormatter(options.error);
+      }
+      return '';
+    }
+
+    function stack() {
+      if (options.passed) {
+        return '';
+      }
+
+      var error = options.error;
+      if (!error) {
+        try {
+          throw new Error(message());
+        } catch (e) {
+          error = e;
+        }
+      }
+      return stackFormatter(error);
+    }
+  }
+
+  return buildExpectationResult;
+};
+
+getJasmineRequireObj().MockDate = function() {
+  function MockDate(global) {
+    var self = this;
+    var currentTime = 0;
+
+    if (!global || !global.Date) {
+      self.install = function() {};
+      self.tick = function() {};
+      self.uninstall = function() {};
+      return self;
+    }
+
+    var GlobalDate = global.Date;
+
+    self.install = function(mockDate) {
+      if (mockDate instanceof GlobalDate) {
+        currentTime = mockDate.getTime();
+      } else {
+        currentTime = new GlobalDate().getTime();
+      }
+
+      global.Date = FakeDate;
+    };
+
+    self.tick = function(millis) {
+      millis = millis || 0;
+      currentTime = currentTime + millis;
+    };
+
+    self.uninstall = function() {
+      currentTime = 0;
+      global.Date = GlobalDate;
+    };
+
+    createDateProperties();
+
+    return self;
+
+    function FakeDate() {
+      switch(arguments.length) {
+        case 0:
+          return new GlobalDate(currentTime);
+        case 1:
+          return new GlobalDate(arguments[0]);
+        case 2:
+          return new GlobalDate(arguments[0], arguments[1]);
+        case 3:
+          return new GlobalDate(arguments[0], arguments[1], arguments[2]);
+        case 4:
+          return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3]);
+        case 5:
+          return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
+                                arguments[4]);
+        case 6:
+          return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
+                                arguments[4], arguments[5]);
+        default:
+          return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
+                                arguments[4], arguments[5], arguments[6]);
+      }
+    }
+
+    function createDateProperties() {
+      FakeDate.prototype = GlobalDate.prototype;
+
+      FakeDate.now = function() {
+        if (GlobalDate.now) {
+          return currentTime;
+        } else {
+          throw new Error('Browser does not support Date.now()');
+        }
+      };
+
+      FakeDate.toSource = GlobalDate.toSource;
+      FakeDate.toString = GlobalDate.toString;
+      FakeDate.parse = GlobalDate.parse;
+      FakeDate.UTC = GlobalDate.UTC;
+    }
+       }
+
+  return MockDate;
+};
+
+getJasmineRequireObj().pp = function(j$) {
+
+  function PrettyPrinter() {
+    this.ppNestLevel_ = 0;
+    this.seen = [];
+  }
+
+  PrettyPrinter.prototype.format = function(value) {
+    this.ppNestLevel_++;
+    try {
+      if (j$.util.isUndefined(value)) {
+        this.emitScalar('undefined');
+      } else if (value === null) {
+        this.emitScalar('null');
+      } else if (value === 0 && 1/value === -Infinity) {
+        this.emitScalar('-0');
+      } else if (value === j$.getGlobal()) {
+        this.emitScalar('<global>');
+      } else if (value.jasmineToString) {
+        this.emitScalar(value.jasmineToString());
+      } else if (typeof value === 'string') {
+        this.emitString(value);
+      } else if (j$.isSpy(value)) {
+        this.emitScalar('spy on ' + value.and.identity());
+      } else if (value instanceof RegExp) {
+        this.emitScalar(value.toString());
+      } else if (typeof value === 'function') {
+        this.emitScalar('Function');
+      } else if (typeof value.nodeType === 'number') {
+        this.emitScalar('HTMLNode');
+      } else if (value instanceof Date) {
+        this.emitScalar('Date(' + value + ')');
+      } else if (j$.util.arrayContains(this.seen, value)) {
+        this.emitScalar('<circular reference: ' + (j$.isArray_(value) ? 'Array' : 'Object') + '>');
+      } else if (j$.isArray_(value) || j$.isA_('Object', value)) {
+        this.seen.push(value);
+        if (j$.isArray_(value)) {
+          this.emitArray(value);
+        } else {
+          this.emitObject(value);
+        }
+        this.seen.pop();
+      } else {
+        this.emitScalar(value.toString());
+      }
+    } finally {
+      this.ppNestLevel_--;
+    }
+  };
+
+  PrettyPrinter.prototype.iterateObject = function(obj, fn) {
+    for (var property in obj) {
+      if (!Object.prototype.hasOwnProperty.call(obj, property)) { continue; }
+      fn(property, obj.__lookupGetter__ ? (!j$.util.isUndefined(obj.__lookupGetter__(property)) &&
+          obj.__lookupGetter__(property) !== null) : false);
+    }
+  };
+
+  PrettyPrinter.prototype.emitArray = j$.unimplementedMethod_;
+  PrettyPrinter.prototype.emitObject = j$.unimplementedMethod_;
+  PrettyPrinter.prototype.emitScalar = j$.unimplementedMethod_;
+  PrettyPrinter.prototype.emitString = j$.unimplementedMethod_;
+
+  function StringPrettyPrinter() {
+    PrettyPrinter.call(this);
+
+    this.string = '';
+  }
+
+  j$.util.inherit(StringPrettyPrinter, PrettyPrinter);
+
+  StringPrettyPrinter.prototype.emitScalar = function(value) {
+    this.append(value);
+  };
+
+  StringPrettyPrinter.prototype.emitString = function(value) {
+    this.append('\'' + value + '\'');
+  };
+
+  StringPrettyPrinter.prototype.emitArray = function(array) {
+    if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) {
+      this.append('Array');
+      return;
+    }
+    var length = Math.min(array.length, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH);
+    this.append('[ ');
+    for (var i = 0; i < length; i++) {
+      if (i > 0) {
+        this.append(', ');
+      }
+      this.format(array[i]);
+    }
+    if(array.length > length){
+      this.append(', ...');
+    }
+    this.append(' ]');
+  };
+
+  StringPrettyPrinter.prototype.emitObject = function(obj) {
+    var constructorName = obj.constructor ? j$.fnNameFor(obj.constructor) : 'null';
+    this.append(constructorName);
+
+    if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) {
+      return;
+    }
+
+    var self = this;
+    this.append('({ ');
+    var first = true;
+
+    this.iterateObject(obj, function(property, isGetter) {
+      if (first) {
+        first = false;
+      } else {
+        self.append(', ');
+      }
+
+      self.append(property);
+      self.append(': ');
+      if (isGetter) {
+        self.append('<getter>');
+      } else {
+        self.format(obj[property]);
+      }
+    });
+
+    this.append(' })');
+  };
+
+  StringPrettyPrinter.prototype.append = function(value) {
+    this.string += value;
+  };
+
+  return function(value) {
+    var stringPrettyPrinter = new StringPrettyPrinter();
+    stringPrettyPrinter.format(value);
+    return stringPrettyPrinter.string;
+  };
+};
+
+getJasmineRequireObj().QueueRunner = function(j$) {
+
+  function once(fn) {
+    var called = false;
+    return function() {
+      if (!called) {
+        called = true;
+        fn();
+      }
+    };
+  }
+
+  function QueueRunner(attrs) {
+    this.queueableFns = attrs.queueableFns || [];
+    this.onComplete = attrs.onComplete || function() {};
+    this.clearStack = attrs.clearStack || function(fn) {fn();};
+    this.onException = attrs.onException || function() {};
+    this.catchException = attrs.catchException || function() { return true; };
+    this.userContext = attrs.userContext || {};
+    this.timer = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout};
+    this.fail = attrs.fail || function() {};
+  }
+
+  QueueRunner.prototype.execute = function() {
+    this.run(this.queueableFns, 0);
+  };
+
+  QueueRunner.prototype.run = function(queueableFns, recursiveIndex) {
+    var length = queueableFns.length,
+      self = this,
+      iterativeIndex;
+
+
+    for(iterativeIndex = recursiveIndex; iterativeIndex < length; iterativeIndex++) {
+      var queueableFn = queueableFns[iterativeIndex];
+      if (queueableFn.fn.length > 0) {
+        attemptAsync(queueableFn);
+        return;
+      } else {
+        attemptSync(queueableFn);
+      }
+    }
+
+    var runnerDone = iterativeIndex >= length;
+
+    if (runnerDone) {
+      this.clearStack(this.onComplete);
+    }
+
+    function attemptSync(queueableFn) {
+      try {
+        queueableFn.fn.call(self.userContext);
+      } catch (e) {
+        handleException(e, queueableFn);
+      }
+    }
+
+    function attemptAsync(queueableFn) {
+      var clearTimeout = function () {
+          Function.prototype.apply.apply(self.timer.clearTimeout, [j$.getGlobal(), [timeoutId]]);
+        },
+        next = once(function () {
+          clearTimeout(timeoutId);
+          self.run(queueableFns, iterativeIndex + 1);
+        }),
+        timeoutId;
+
+      next.fail = function() {
+        self.fail.apply(null, arguments);
+        next();
+      };
+
+      if (queueableFn.timeout) {
+        timeoutId = Function.prototype.apply.apply(self.timer.setTimeout, [j$.getGlobal(), [function() {
+          var error = new Error('Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.');
+          onException(error, queueableFn);
+          next();
+        }, queueableFn.timeout()]]);
+      }
+
+      try {
+        queueableFn.fn.call(self.userContext, next);
+      } catch (e) {
+        handleException(e, queueableFn);
+        next();
+      }
+    }
+
+    function onException(e, queueableFn) {
+      self.onException(e);
+    }
+
+    function handleException(e, queueableFn) {
+      onException(e, queueableFn);
+      if (!self.catchException(e)) {
+        //TODO: set a var when we catch an exception and
+        //use a finally block to close the loop in a nice way..
+        throw e;
+      }
+    }
+  };
+
+  return QueueRunner;
+};
+
+getJasmineRequireObj().ReportDispatcher = function() {
+  function ReportDispatcher(methods) {
+
+    var dispatchedMethods = methods || [];
+
+    for (var i = 0; i < dispatchedMethods.length; i++) {
+      var method = dispatchedMethods[i];
+      this[method] = (function(m) {
+        return function() {
+          dispatch(m, arguments);
+        };
+      }(method));
+    }
+
+    var reporters = [];
+
+    this.addReporter = function(reporter) {
+      reporters.push(reporter);
+    };
+
+    return this;
+
+    function dispatch(method, args) {
+      for (var i = 0; i < reporters.length; i++) {
+        var reporter = reporters[i];
+        if (reporter[method]) {
+          reporter[method].apply(reporter, args);
+        }
+      }
+    }
+  }
+
+  return ReportDispatcher;
+};
+
+
+getJasmineRequireObj().SpyRegistry = function(j$) {
+
+  function SpyRegistry(options) {
+    options = options || {};
+    var currentSpies = options.currentSpies || function() { return []; };
+
+    this.spyOn = function(obj, methodName) {
+      if (j$.util.isUndefined(obj)) {
+        throw new Error('spyOn could not find an object to spy upon for ' + methodName + '()');
+      }
+
+      if (j$.util.isUndefined(methodName)) {
+        throw new Error('No method name supplied');
+      }
+
+      if (j$.util.isUndefined(obj[methodName])) {
+        throw new Error(methodName + '() method does not exist');
+      }
+
+      if (obj[methodName] && j$.isSpy(obj[methodName])) {
+        //TODO?: should this return the current spy? Downside: may cause user confusion about spy state
+        throw new Error(methodName + ' has already been spied upon');
+      }
+
+      var spy = j$.createSpy(methodName, obj[methodName]);
+
+      currentSpies().push({
+        spy: spy,
+        baseObj: obj,
+        methodName: methodName,
+        originalValue: obj[methodName]
+      });
+
+      obj[methodName] = spy;
+
+      return spy;
+    };
+
+    this.clearSpies = function() {
+      var spies = currentSpies();
+      for (var i = 0; i < spies.length; i++) {
+        var spyEntry = spies[i];
+        spyEntry.baseObj[spyEntry.methodName] = spyEntry.originalValue;
+      }
+    };
+  }
+
+  return SpyRegistry;
+};
+
+getJasmineRequireObj().SpyStrategy = function() {
+
+  function SpyStrategy(options) {
+    options = options || {};
+
+    var identity = options.name || 'unknown',
+        originalFn = options.fn || function() {},
+        getSpy = options.getSpy || function() {},
+        plan = function() {};
+
+    this.identity = function() {
+      return identity;
+    };
+
+    this.exec = function() {
+      return plan.apply(this, arguments);
+    };
+
+    this.callThrough = function() {
+      plan = originalFn;
+      return getSpy();
+    };
+
+    this.returnValue = function(value) {
+      plan = function() {
+        return value;
+      };
+      return getSpy();
+    };
+
+    this.returnValues = function() {
+      var values = Array.prototype.slice.call(arguments);
+      plan = function () {
+        return values.shift();
+      };
+      return getSpy();
+    };
+
+    this.throwError = function(something) {
+      var error = (something instanceof Error) ? something : new Error(something);
+      plan = function() {
+        throw error;
+      };
+      return getSpy();
+    };
+
+    this.callFake = function(fn) {
+      plan = fn;
+      return getSpy();
+    };
+
+    this.stub = function(fn) {
+      plan = function() {};
+      return getSpy();
+    };
+  }
+
+  return SpyStrategy;
+};
+
+getJasmineRequireObj().Suite = function() {
+  function Suite(attrs) {
+    this.env = attrs.env;
+    this.id = attrs.id;
+    this.parentSuite = attrs.parentSuite;
+    this.description = attrs.description;
+    this.onStart = attrs.onStart || function() {};
+    this.resultCallback = attrs.resultCallback || function() {};
+    this.clearStack = attrs.clearStack || function(fn) {fn();};
+    this.expectationFactory = attrs.expectationFactory;
+    this.expectationResultFactory = attrs.expectationResultFactory;
+    this.runnablesExplictlySetGetter = attrs.runnablesExplictlySetGetter || function() {};
+
+    this.beforeFns = [];
+    this.afterFns = [];
+    this.beforeAllFns = [];
+    this.afterAllFns = [];
+    this.queueRunner = attrs.queueRunner || function() {};
+    this.disabled = false;
+
+    this.children = [];
+
+    this.result = {
+      id: this.id,
+      description: this.description,
+      fullName: this.getFullName(),
+      failedExpectations: []
+    };
+  }
+
+  Suite.prototype.expect = function(actual) {
+    return this.expectationFactory(actual, this);
+  };
+
+  Suite.prototype.getFullName = function() {
+    var fullName = this.description;
+    for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) {
+      if (parentSuite.parentSuite) {
+        fullName = parentSuite.description + ' ' + fullName;
+      }
+    }
+    return fullName;
+  };
+
+  Suite.prototype.disable = function() {
+    this.disabled = true;
+  };
+
+  Suite.prototype.beforeEach = function(fn) {
+    this.beforeFns.unshift(fn);
+  };
+
+  Suite.prototype.beforeAll = function(fn) {
+    this.beforeAllFns.push(fn);
+  };
+
+  Suite.prototype.afterEach = function(fn) {
+    this.afterFns.unshift(fn);
+  };
+
+  Suite.prototype.afterAll = function(fn) {
+    this.afterAllFns.push(fn);
+  };
+
+  Suite.prototype.addChild = function(child) {
+    this.children.push(child);
+  };
+
+  Suite.prototype.status = function() {
+    if (this.disabled) {
+      return 'disabled';
+    }
+
+    if (this.result.failedExpectations.length > 0) {
+      return 'failed';
+    } else {
+      return 'finished';
+    }
+  };
+
+  Suite.prototype.execute = function(onComplete) {
+    var self = this;
+
+    this.onStart(this);
+
+    if (this.disabled) {
+      complete();
+      return;
+    }
+
+    var allFns = [];
+
+    for (var i = 0; i < this.children.length; i++) {
+      allFns.push(wrapChildAsAsync(this.children[i]));
+    }
+
+    if (this.isExecutable()) {
+      allFns = this.beforeAllFns.concat(allFns);
+      allFns = allFns.concat(this.afterAllFns);
+    }
+
+    this.queueRunner({
+      queueableFns: allFns,
+      onComplete: complete,
+      userContext: this.sharedUserContext(),
+      onException: function() { self.onException.apply(self, arguments); }
+    });
+
+    function complete() {
+      self.result.status = self.status();
+      self.resultCallback(self.result);
+
+      if (onComplete) {
+        onComplete();
+      }
+    }
+
+    function wrapChildAsAsync(child) {
+      return { fn: function(done) { child.execute(done); } };
+    }
+  };
+
+  Suite.prototype.isExecutable = function() {
+    var runnablesExplicitlySet = this.runnablesExplictlySetGetter();
+    return !runnablesExplicitlySet && hasExecutableChild(this.children);
+  };
+
+  Suite.prototype.sharedUserContext = function() {
+    if (!this.sharedContext) {
+      this.sharedContext = this.parentSuite ? clone(this.parentSuite.sharedUserContext()) : {};
+    }
+
+    return this.sharedContext;
+  };
+
+  Suite.prototype.clonedSharedUserContext = function() {
+    return clone(this.sharedUserContext());
+  };
+
+  Suite.prototype.onException = function() {
+    if(isAfterAll(this.children)) {
+      var data = {
+        matcherName: '',
+        passed: false,
+        expected: '',
+        actual: '',
+        error: arguments[0]
+      };
+      this.result.failedExpectations.push(this.expectationResultFactory(data));
+    } else {
+      for (var i = 0; i < this.children.length; i++) {
+        var child = this.children[i];
+        child.onException.apply(child, arguments);
+      }
+    }
+  };
+
+  Suite.prototype.addExpectationResult = function () {
+    if(isAfterAll(this.children) && isFailure(arguments)){
+      var data = arguments[1];
+      this.result.failedExpectations.push(this.expectationResultFactory(data));
+    } else {
+      for (var i = 0; i < this.children.length; i++) {
+        var child = this.children[i];
+        child.addExpectationResult.apply(child, arguments);
+      }
+    }
+  };
+
+  function isAfterAll(children) {
+    return children && children[0].result.status;
+  }
+
+  function isFailure(args) {
+    return !args[0];
+  }
+
+  function hasExecutableChild(children) {
+    var foundActive = false;
+    for (var i = 0; i < children.length; i++) {
+      if (children[i].isExecutable()) {
+        foundActive = true;
+        break;
+      }
+    }
+    return foundActive;
+  }
+
+  function clone(obj) {
+    var clonedObj = {};
+    for (var prop in obj) {
+      if (obj.hasOwnProperty(prop)) {
+        clonedObj[prop] = obj[prop];
+      }
+    }
+
+    return clonedObj;
+  }
+
+  return Suite;
+};
+
+if (typeof window == void 0 && typeof exports == 'object') {
+  exports.Suite = jasmineRequire.Suite;
+}
+
+getJasmineRequireObj().Timer = function() {
+  var defaultNow = (function(Date) {
+    return function() { return new Date().getTime(); };
+  })(Date);
+
+  function Timer(options) {
+    options = options || {};
+
+    var now = options.now || defaultNow,
+      startTime;
+
+    this.start = function() {
+      startTime = now();
+    };
+
+    this.elapsed = function() {
+      return now() - startTime;
+    };
+  }
+
+  return Timer;
+};
+
+getJasmineRequireObj().Any = function() {
+
+  function Any(expectedObject) {
+    this.expectedObject = expectedObject;
+  }
+
+  Any.prototype.asymmetricMatch = function(other) {
+    if (this.expectedObject == String) {
+      return typeof other == 'string' || other instanceof String;
+    }
+
+    if (this.expectedObject == Number) {
+      return typeof other == 'number' || other instanceof Number;
+    }
+
+    if (this.expectedObject == Function) {
+      return typeof other == 'function' || other instanceof Function;
+    }
+
+    if (this.expectedObject == Object) {
+      return typeof other == 'object';
+    }
+
+    if (this.expectedObject == Boolean) {
+      return typeof other == 'boolean';
+    }
+
+    return other instanceof this.expectedObject;
+  };
+
+  Any.prototype.jasmineToString = function() {
+    return '<jasmine.any(' + this.expectedObject + ')>';
+  };
+
+  return Any;
+};
+
+getJasmineRequireObj().Anything = function(j$) {
+
+  function Anything() {}
+
+  Anything.prototype.asymmetricMatch = function(other) {
+    return !j$.util.isUndefined(other) && other !== null;
+  };
+
+  Anything.prototype.jasmineToString = function() {
+    return '<jasmine.anything>';
+  };
+
+  return Anything;
+};
+
+getJasmineRequireObj().ArrayContaining = function(j$) {
+  function ArrayContaining(sample) {
+    this.sample = sample;
+  }
+
+  ArrayContaining.prototype.asymmetricMatch = function(other) {
+    var className = Object.prototype.toString.call(this.sample);
+    if (className !== '[object Array]') { throw new Error('You must provide an array to arrayContaining, not \'' + this.sample + '\'.'); }
+
+    for (var i = 0; i < this.sample.length; i++) {
+      var item = this.sample[i];
+      if (!j$.matchersUtil.contains(other, item)) {
+        return false;
+      }
+    }
+
+    return true;
+  };
+
+  ArrayContaining.prototype.jasmineToString = function () {
+    return '<jasmine.arrayContaining(' + jasmine.pp(this.sample) +')>';
+  };
+
+  return ArrayContaining;
+};
+
+getJasmineRequireObj().ObjectContaining = function(j$) {
+
+  function ObjectContaining(sample) {
+    this.sample = sample;
+  }
+
+  ObjectContaining.prototype.asymmetricMatch = function(other) {
+    if (typeof(this.sample) !== 'object') { throw new Error('You must provide an object to objectContaining, not \''+this.sample+'\'.'); }
+
+    for (var property in this.sample) {
+      if (!Object.prototype.hasOwnProperty.call(other, property) ||
+          !j$.matchersUtil.equals(this.sample[property], other[property])) {
+        return false;
+      }
+    }
+
+    return true;
+  };
+
+  ObjectContaining.prototype.jasmineToString = function() {
+    return '<jasmine.objectContaining(' + j$.pp(this.sample) + ')>';
+  };
+
+  return ObjectContaining;
+};
+
+getJasmineRequireObj().StringMatching = function(j$) {
+
+  function StringMatching(expected) {
+    if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) {
+      throw new Error('Expected is not a String or a RegExp');
+    }
+
+    this.regexp = new RegExp(expected);
+  }
+
+  StringMatching.prototype.asymmetricMatch = function(other) {
+    return this.regexp.test(other);
+  };
+
+  StringMatching.prototype.jasmineToString = function() {
+    return '<jasmine.stringMatching(' + this.regexp + ')>';
+  };
+
+  return StringMatching;
+};
+
+getJasmineRequireObj().matchersUtil = function(j$) {
+  // TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter?
+
+  return {
+    equals: function(a, b, customTesters) {
+      customTesters = customTesters || [];
+
+      return eq(a, b, [], [], customTesters);
+    },
+
+    contains: function(haystack, needle, customTesters) {
+      customTesters = customTesters || [];
+
+      if ((Object.prototype.toString.apply(haystack) === '[object Array]') ||
+        (!!haystack && !haystack.indexOf))
+      {
+        for (var i = 0; i < haystack.length; i++) {
+          if (eq(haystack[i], needle, [], [], customTesters)) {
+            return true;
+          }
+        }
+        return false;
+      }
+
+      return !!haystack && haystack.indexOf(needle) >= 0;
+    },
+
+    buildFailureMessage: function() {
+      var args = Array.prototype.slice.call(arguments, 0),
+        matcherName = args[0],
+        isNot = args[1],
+        actual = args[2],
+        expected = args.slice(3),
+        englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
+
+      var message = 'Expected ' +
+        j$.pp(actual) +
+        (isNot ? ' not ' : ' ') +
+        englishyPredicate;
+
+      if (expected.length > 0) {
+        for (var i = 0; i < expected.length; i++) {
+          if (i > 0) {
+            message += ',';
+          }
+          message += ' ' + j$.pp(expected[i]);
+        }
+      }
+
+      return message + '.';
+    }
+  };
+
+  function isAsymmetric(obj) {
+    return obj && j$.isA_('Function', obj.asymmetricMatch);
+  }
+
+  function asymmetricMatch(a, b) {
+    var asymmetricA = isAsymmetric(a),
+        asymmetricB = isAsymmetric(b);
+
+    if (asymmetricA && asymmetricB) {
+      return undefined;
+    }
+
+    if (asymmetricA) {
+      return a.asymmetricMatch(b);
+    }
+
+    if (asymmetricB) {
+      return b.asymmetricMatch(a);
+    }
+  }
+
+  // Equality function lovingly adapted from isEqual in
+  //   [Underscore](http://underscorejs.org)
+  function eq(a, b, aStack, bStack, customTesters) {
+    var result = true;
+
+    var asymmetricResult = asymmetricMatch(a, b);
+    if (!j$.util.isUndefined(asymmetricResult)) {
+      return asymmetricResult;
+    }
+
+    for (var i = 0; i < customTesters.length; i++) {
+      var customTesterResult = customTesters[i](a, b);
+      if (!j$.util.isUndefined(customTesterResult)) {
+        return customTesterResult;
+      }
+    }
+
+    if (a instanceof Error && b instanceof Error) {
+      return a.message == b.message;
+    }
+
+    // Identical objects are equal. `0 === -0`, but they aren't identical.
+    // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
+    if (a === b) { return a !== 0 || 1 / a == 1 / b; }
+    // A strict comparison is necessary because `null == undefined`.
+    if (a === null || b === null) { return a === b; }
+    var className = Object.prototype.toString.call(a);
+    if (className != Object.prototype.toString.call(b)) { return false; }
+    switch (className) {
+      // Strings, numbers, dates, and booleans are compared by value.
+      case '[object String]':
+        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+        // equivalent to `new String("5")`.
+        return a == String(b);
+      case '[object Number]':
+        // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
+        // other numeric values.
+        return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b);
+      case '[object Date]':
+      case '[object Boolean]':
+        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+        // millisecond representations. Note that invalid dates with millisecond representations
+        // of `NaN` are not equivalent.
+        return +a == +b;
+      // RegExps are compared by their source patterns and flags.
+      case '[object RegExp]':
+        return a.source == b.source &&
+          a.global == b.global &&
+          a.multiline == b.multiline &&
+          a.ignoreCase == b.ignoreCase;
+    }
+    if (typeof a != 'object' || typeof b != 'object') { return false; }
+
+    var aIsDomNode = j$.isDomNode(a);
+    var bIsDomNode = j$.isDomNode(b);
+    if (aIsDomNode && bIsDomNode) {
+      // At first try to use DOM3 method isEqualNode
+      if (a.isEqualNode) {
+        return a.isEqualNode(b);
+      }
+      // IE8 doesn't support isEqualNode, try to use outerHTML && innerText
+      var aIsElement = a instanceof Element;
+      var bIsElement = b instanceof Element;
+      if (aIsElement && bIsElement) {
+        return a.outerHTML == b.outerHTML;
+      }
+      if (aIsElement || bIsElement) {
+        return false;
+      }
+      return a.innerText == b.innerText && a.textContent == b.textContent;
+    }
+    if (aIsDomNode || bIsDomNode) {
+      return false;
+    }
+
+    // Assume equality for cyclic structures. The algorithm for detecting cyclic
+    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+    var length = aStack.length;
+    while (length--) {
+      // Linear search. Performance is inversely proportional to the number of
+      // unique nested structures.
+      if (aStack[length] == a) { return bStack[length] == b; }
+    }
+    // Add the first object to the stack of traversed objects.
+    aStack.push(a);
+    bStack.push(b);
+    var size = 0;
+    // Recursively compare objects and arrays.
+    // Compare array lengths to determine if a deep comparison is necessary.
+    if (className == '[object Array]' && a.length !== b.length) {
+      result = false;
+    }
+
+    if (result) {
+      // Objects with different constructors are not equivalent, but `Object`s
+      // from different frames are.
+      var aCtor = a.constructor, bCtor = b.constructor;
+      if (aCtor !== bCtor && !(isFunction(aCtor) && (aCtor instanceof aCtor) &&
+        isFunction(bCtor) && (bCtor instanceof bCtor))) {
+        return false;
+      }
+      // Deep compare objects.
+      for (var key in a) {
+        if (has(a, key)) {
+          // Count the expected number of properties.
+          size++;
+          // Deep compare each member.
+          if (!(result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters))) { break; }
+        }
+      }
+      // Ensure that both objects contain the same number of properties.
+      if (result) {
+        for (key in b) {
+          if (has(b, key) && !(size--)) { break; }
+        }
+        result = !size;
+      }
+    }
+    // Remove the first object from the stack of traversed objects.
+    aStack.pop();
+    bStack.pop();
+
+    return result;
+
+    function has(obj, key) {
+      return Object.prototype.hasOwnProperty.call(obj, key);
+    }
+
+    function isFunction(obj) {
+      return typeof obj === 'function';
+    }
+  }
+};
+
+getJasmineRequireObj().toBe = function() {
+  function toBe() {
+    return {
+      compare: function(actual, expected) {
+        return {
+          pass: actual === expected
+        };
+      }
+    };
+  }
+
+  return toBe;
+};
+
+getJasmineRequireObj().toBeCloseTo = function() {
+
+  function toBeCloseTo() {
+    return {
+      compare: function(actual, expected, precision) {
+        if (precision !== 0) {
+          precision = precision || 2;
+        }
+
+        return {
+          pass: Math.abs(expected - actual) < (Math.pow(10, -precision) / 2)
+        };
+      }
+    };
+  }
+
+  return toBeCloseTo;
+};
+
+getJasmineRequireObj().toBeDefined = function() {
+  function toBeDefined() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: (void 0 !== actual)
+        };
+      }
+    };
+  }
+
+  return toBeDefined;
+};
+
+getJasmineRequireObj().toBeFalsy = function() {
+  function toBeFalsy() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: !!!actual
+        };
+      }
+    };
+  }
+
+  return toBeFalsy;
+};
+
+getJasmineRequireObj().toBeGreaterThan = function() {
+
+  function toBeGreaterThan() {
+    return {
+      compare: function(actual, expected) {
+        return {
+          pass: actual > expected
+        };
+      }
+    };
+  }
+
+  return toBeGreaterThan;
+};
+
+
+getJasmineRequireObj().toBeLessThan = function() {
+  function toBeLessThan() {
+    return {
+
+      compare: function(actual, expected) {
+        return {
+          pass: actual < expected
+        };
+      }
+    };
+  }
+
+  return toBeLessThan;
+};
+getJasmineRequireObj().toBeNaN = function(j$) {
+
+  function toBeNaN() {
+    return {
+      compare: function(actual) {
+        var result = {
+          pass: (actual !== actual)
+        };
+
+        if (result.pass) {
+          result.message = 'Expected actual not to be NaN.';
+        } else {
+          result.message = function() { return 'Expected ' + j$.pp(actual) + ' to be NaN.'; };
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toBeNaN;
+};
+
+getJasmineRequireObj().toBeNull = function() {
+
+  function toBeNull() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: actual === null
+        };
+      }
+    };
+  }
+
+  return toBeNull;
+};
+
+getJasmineRequireObj().toBeTruthy = function() {
+
+  function toBeTruthy() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: !!actual
+        };
+      }
+    };
+  }
+
+  return toBeTruthy;
+};
+
+getJasmineRequireObj().toBeUndefined = function() {
+
+  function toBeUndefined() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: void 0 === actual
+        };
+      }
+    };
+  }
+
+  return toBeUndefined;
+};
+
+getJasmineRequireObj().toContain = function() {
+  function toContain(util, customEqualityTesters) {
+    customEqualityTesters = customEqualityTesters || [];
+
+    return {
+      compare: function(actual, expected) {
+
+        return {
+          pass: util.contains(actual, expected, customEqualityTesters)
+        };
+      }
+    };
+  }
+
+  return toContain;
+};
+
+getJasmineRequireObj().toEqual = function() {
+
+  function toEqual(util, customEqualityTesters) {
+    customEqualityTesters = customEqualityTesters || [];
+
+    return {
+      compare: function(actual, expected) {
+        var result = {
+          pass: false
+        };
+
+        result.pass = util.equals(actual, expected, customEqualityTesters);
+
+        return result;
+      }
+    };
+  }
+
+  return toEqual;
+};
+
+getJasmineRequireObj().toHaveBeenCalled = function(j$) {
+
+  function toHaveBeenCalled() {
+    return {
+      compare: function(actual) {
+        var result = {};
+
+        if (!j$.isSpy(actual)) {
+          throw new Error('Expected a spy, but got ' + j$.pp(actual) + '.');
+        }
+
+        if (arguments.length > 1) {
+          throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith');
+        }
+
+        result.pass = actual.calls.any();
+
+        result.message = result.pass ?
+          'Expected spy ' + actual.and.identity() + ' not to have been called.' :
+          'Expected spy ' + actual.and.identity() + ' to have been called.';
+
+        return result;
+      }
+    };
+  }
+
+  return toHaveBeenCalled;
+};
+
+getJasmineRequireObj().toHaveBeenCalledWith = function(j$) {
+
+  function toHaveBeenCalledWith(util, customEqualityTesters) {
+    return {
+      compare: function() {
+        var args = Array.prototype.slice.call(arguments, 0),
+          actual = args[0],
+          expectedArgs = args.slice(1),
+          result = { pass: false };
+
+        if (!j$.isSpy(actual)) {
+          throw new Error('Expected a spy, but got ' + j$.pp(actual) + '.');
+        }
+
+        if (!actual.calls.any()) {
+          result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + j$.pp(expectedArgs) + ' but it was never called.'; };
+          return result;
+        }
+
+        if (util.contains(actual.calls.allArgs(), expectedArgs, customEqualityTesters)) {
+          result.pass = true;
+          result.message = function() { return 'Expected spy ' + actual.and.identity() + ' not to have been called with ' + j$.pp(expectedArgs) + ' but it was.'; };
+        } else {
+          result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + j$.pp(expectedArgs) + ' but actual calls were ' + j$.pp(actual.calls.allArgs()).replace(/^\[ | \]$/g, '') + '.'; };
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toHaveBeenCalledWith;
+};
+
+getJasmineRequireObj().toMatch = function(j$) {
+
+  function toMatch() {
+    return {
+      compare: function(actual, expected) {
+        if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) {
+          throw new Error('Expected is not a String or a RegExp');
+        }
+
+        var regexp = new RegExp(expected);
+
+        return {
+          pass: regexp.test(actual)
+        };
+      }
+    };
+  }
+
+  return toMatch;
+};
+
+getJasmineRequireObj().toThrow = function(j$) {
+
+  function toThrow(util) {
+    return {
+      compare: function(actual, expected) {
+        var result = { pass: false },
+          threw = false,
+          thrown;
+
+        if (typeof actual != 'function') {
+          throw new Error('Actual is not a Function');
+        }
+
+        try {
+          actual();
+        } catch (e) {
+          threw = true;
+          thrown = e;
+        }
+
+        if (!threw) {
+          result.message = 'Expected function to throw an exception.';
+          return result;
+        }
+
+        if (arguments.length == 1) {
+          result.pass = true;
+          result.message = function() { return 'Expected function not to throw, but it threw ' + j$.pp(thrown) + '.'; };
+
+          return result;
+        }
+
+        if (util.equals(thrown, expected)) {
+          result.pass = true;
+          result.message = function() { return 'Expected function not to throw ' + j$.pp(expected) + '.'; };
+        } else {
+          result.message = function() { return 'Expected function to throw ' + j$.pp(expected) + ', but it threw ' +  j$.pp(thrown) + '.'; };
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toThrow;
+};
+
+getJasmineRequireObj().toThrowError = function(j$) {
+  function toThrowError (util) {
+    return {
+      compare: function(actual) {
+        var threw = false,
+          pass = {pass: true},
+          fail = {pass: false},
+          thrown;
+
+        if (typeof actual != 'function') {
+          throw new Error('Actual is not a Function');
+        }
+
+        var errorMatcher = getMatcher.apply(null, arguments);
+
+        try {
+          actual();
+        } catch (e) {
+          threw = true;
+          thrown = e;
+        }
+
+        if (!threw) {
+          fail.message = 'Expected function to throw an Error.';
+          return fail;
+        }
+
+        if (!(thrown instanceof Error)) {
+          fail.message = function() { return 'Expected function to throw an Error, but it threw ' + j$.pp(thrown) + '.'; };
+          return fail;
+        }
+
+        if (errorMatcher.hasNoSpecifics()) {
+          pass.message = 'Expected function not to throw an Error, but it threw ' + j$.fnNameFor(thrown) + '.';
+          return pass;
+        }
+
+        if (errorMatcher.matches(thrown)) {
+          pass.message = function() {
+            return 'Expected function not to throw ' + errorMatcher.errorTypeDescription + errorMatcher.messageDescription() + '.';
+          };
+          return pass;
+        } else {
+          fail.message = function() {
+            return 'Expected function to throw ' + errorMatcher.errorTypeDescription + errorMatcher.messageDescription() +
+              ', but it threw ' + errorMatcher.thrownDescription(thrown) + '.';
+          };
+          return fail;
+        }
+      }
+    };
+
+    function getMatcher() {
+      var expected = null,
+          errorType = null;
+
+      if (arguments.length == 2) {
+        expected = arguments[1];
+        if (isAnErrorType(expected)) {
+          errorType = expected;
+          expected = null;
+        }
+      } else if (arguments.length > 2) {
+        errorType = arguments[1];
+        expected = arguments[2];
+        if (!isAnErrorType(errorType)) {
+          throw new Error('Expected error type is not an Error.');
+        }
+      }
+
+      if (expected && !isStringOrRegExp(expected)) {
+        if (errorType) {
+          throw new Error('Expected error message is not a string or RegExp.');
+        } else {
+          throw new Error('Expected is not an Error, string, or RegExp.');
+        }
+      }
+
+      function messageMatch(message) {
+        if (typeof expected == 'string') {
+          return expected == message;
+        } else {
+          return expected.test(message);
+        }
+      }
+
+      return {
+        errorTypeDescription: errorType ? j$.fnNameFor(errorType) : 'an exception',
+        thrownDescription: function(thrown) {
+          var thrownName = errorType ? j$.fnNameFor(thrown.constructor) : 'an exception',
+              thrownMessage = '';
+
+          if (expected) {
+            thrownMessage = ' with message ' + j$.pp(thrown.message);
+          }
+
+          return thrownName + thrownMessage;
+        },
+        messageDescription: function() {
+          if (expected === null) {
+            return '';
+          } else if (expected instanceof RegExp) {
+            return ' with a message matching ' + j$.pp(expected);
+          } else {
+            return ' with message ' + j$.pp(expected);
+          }
+        },
+        hasNoSpecifics: function() {
+          return expected === null && errorType === null;
+        },
+        matches: function(error) {
+          return (errorType === null || error.constructor === errorType) &&
+            (expected === null || messageMatch(error.message));
+        }
+      };
+    }
+
+    function isStringOrRegExp(potential) {
+      return potential instanceof RegExp || (typeof potential == 'string');
+    }
+
+    function isAnErrorType(type) {
+      if (typeof type !== 'function') {
+        return false;
+      }
+
+      var Surrogate = function() {};
+      Surrogate.prototype = type.prototype;
+      return (new Surrogate()) instanceof Error;
+    }
+  }
+
+  return toThrowError;
+};
+
+getJasmineRequireObj().interface = function(jasmine, env) {
+  var jasmineInterface = {
+    describe: function(description, specDefinitions) {
+      return env.describe(description, specDefinitions);
+    },
+
+    xdescribe: function(description, specDefinitions) {
+      return env.xdescribe(description, specDefinitions);
+    },
+
+    fdescribe: function(description, specDefinitions) {
+      return env.fdescribe(description, specDefinitions);
+    },
+
+    it: function() {
+      return env.it.apply(env, arguments);
+    },
+
+    xit: function() {
+      return env.xit.apply(env, arguments);
+    },
+
+    fit: function() {
+      return env.fit.apply(env, arguments);
+    },
+
+    beforeEach: function() {
+      return env.beforeEach.apply(env, arguments);
+    },
+
+    afterEach: function() {
+      return env.afterEach.apply(env, arguments);
+    },
+
+    beforeAll: function() {
+      return env.beforeAll.apply(env, arguments);
+    },
+
+    afterAll: function() {
+      return env.afterAll.apply(env, arguments);
+    },
+
+    expect: function(actual) {
+      return env.expect(actual);
+    },
+
+    pending: function() {
+      return env.pending.apply(env, arguments);
+    },
+
+    fail: function() {
+      return env.fail.apply(env, arguments);
+    },
+
+    spyOn: function(obj, methodName) {
+      return env.spyOn(obj, methodName);
+    },
+
+    jsApiReporter: new jasmine.JsApiReporter({
+      timer: new jasmine.Timer()
+    }),
+
+    jasmine: jasmine
+  };
+
+  jasmine.addCustomEqualityTester = function(tester) {
+    env.addCustomEqualityTester(tester);
+  };
+
+  jasmine.addMatchers = function(matchers) {
+    return env.addMatchers(matchers);
+  };
+
+  jasmine.clock = function() {
+    return env.clock;
+  };
+
+  return jasmineInterface;
+};
+
+getJasmineRequireObj().version = function() {
+  return '2.2.0';
+};
diff --git a/test/unittest/jasmine-2.2.0/jasmine_favicon.png b/test/unittest/jasmine-2.2.0/jasmine_favicon.png
new file mode 100644 (file)
index 0000000..3b84583
Binary files /dev/null and b/test/unittest/jasmine-2.2.0/jasmine_favicon.png differ
diff --git a/test/unittest/jasmine-2.2.0/jasmine_helper.js b/test/unittest/jasmine-2.2.0/jasmine_helper.js
new file mode 100644 (file)
index 0000000..29a18be
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+'use strict';
+
+var JasmineHelper = (function() {
+  var env, iface;
+
+  function extend(destination, source) {
+    for (var property in source) destination[property] = source[property];
+    return destination;
+  }
+
+  function addJasmineReporters(jasmineInterface, jasmineEnv) {
+    jasmineInterface.jsApiReporter = new jasmineInterface.jasmine.JsApiReporter({ timer: new jasmineInterface.jasmine.Timer() });
+    jasmineEnv.addReporter(jasmineInterface.jsApiReporter);
+
+    jasmineInterface.htmlReporter = new jasmineInterface.jasmine.HtmlReporter({
+      env: jasmineEnv,
+      queryString: function() { return null; },
+      onRaiseExceptionsClick: function() { },
+      getContainer: function() { return document.getElementById('content'); },
+      createElement: function() { return document.createElement.apply(document, arguments); },
+      createTextNode: function() { return document.createTextNode.apply(document, arguments); },
+      timer: new jasmineInterface.jasmine.Timer()
+    });
+    jasmineInterface.htmlReporter.initialize();
+    jasmineEnv.addReporter(jasmineInterface.htmlReporter);
+
+    var ConsoleReporter = jasmineRequire.ConsoleReporter();
+    jasmineEnv.addReporter(new ConsoleReporter({
+      print: function () {console.log.apply(console, arguments);},
+      timer: new jasmine.Timer
+    }));
+  }
+
+  function setUpJasmine() {
+    // Set up jasmine
+    var jasmine = jasmineRequire.core(jasmineRequire);
+    jasmineRequire.html(jasmine);
+    var jasmineEnv = jasmine.currentEnv_ = new jasmine.Env();
+
+    jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
+    jasmineEnv.catchExceptions(false);
+
+    // Set up jasmine interface
+    var jasmineInterface = jasmineRequire.interface(jasmine, jasmineEnv);
+
+    // Add Spec Filter
+    jasmineEnv.specFilter = function(spec) {
+      //console.log(spec.getFullName());
+      return true;
+    };
+
+    // Jasmine 2.2.0 moved this symbol, so we add a shim here.
+    jasmine.Expectation.addMatchers = jasmine.Expectation.addMatchers || function() {
+      return jasmine.addMatchers.apply(this, arguments);
+    };
+
+    window.jasmine = jasmine;
+    extend(window, jasmineInterface);
+
+    env = jasmineEnv;
+    iface = jasmineInterface;
+  }
+
+  function runJasmineTests() {
+    // Add Reporters
+    addJasmineReporters(iface, env);
+    env.execute();
+  }
+
+  return {
+    setup: setUpJasmine,
+    run: runJasmineTests
+  };
+})();
+
diff --git a/test/unittest/splashscreen.html b/test/unittest/splashscreen.html
new file mode 100644 (file)
index 0000000..76711c1
--- /dev/null
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
+    <meta name="description" content="Tizen basic template generated by Tizen Web IDE"/>
+
+    <title>Tizen Web IDE - Tizen - Tizen basic Application</title>
+
+    <link rel="stylesheet" type="text/css" href="style.css"/>
+    
+    <style media="screen and (orientation:portrait) and (max-width:360px)" type="text/css">
+        body {
+            background-image: url("images/screen_tizen_hd_portrait.png");
+            background-size: 100% auto;
+            background-position: top left;
+        }
+    </style>
+    
+    <style media="screen and (orientation:landscape) and not (max-height:360px)" type="text/css">
+        body {
+            background-image: url("images/screen_tizen_hd_landscape.png");
+            background-size: 100% auto;
+            background-position: top left;
+        }
+    </style>
+    
+    <style media="screen and (orientation:portrait) and (max-width:320px)" type="text/css">
+        body {
+            background-image: url("images/screen_tizen_wvga_portrait.png");
+            background-size: 100% auto;
+            background-position: top left;
+        }
+    </style>
+    
+    <style media="screen and (orientation:landscape) and not (max-height:320px)" type="text/css">
+        body {
+            background-image: url("images/screen_tizen_wvga_landscape.png");
+            background-size: 100% auto;
+            background-position: top left;
+        }
+    </style>
+</head>
+
+<body title="Cordova SplashScreen">
+ </body>
+</html>
diff --git a/test/unittest/tests/accelerometer.tests.js b/test/unittest/tests/accelerometer.tests.js
new file mode 100644 (file)
index 0000000..1e6f2b9
--- /dev/null
@@ -0,0 +1,353 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+exports.defineAutoTests = function () {
+    var isWindows = (cordova.platformId === "windows") || (cordova.platformId === "windows8"),
+     // Checking existence of accelerometer for windows platform 
+     // Assumed that accelerometer always exists on other platforms. Extend 
+     // condition to support accelerometer check on other platforms
+     isAccelExist = isWindows ? Windows.Devices.Sensors.Accelerometer.getDefault() !== null : true;
+
+  describe('Accelerometer (navigator.accelerometer)', function () {
+    var fail = function(done) {
+      expect(true).toBe(false);
+      done();
+    };
+
+    // This timeout is here to lessen the load on native accelerometer
+    // intensive use of which can lead to occasional test failures
+    afterEach(function(done) {
+      setTimeout(function() {
+        done();
+      }, 100);
+    });
+
+    it("accelerometer.spec.1 should exist", function () {
+      expect(navigator.accelerometer).toBeDefined();
+    });
+
+    describe("getCurrentAcceleration", function() {
+      it("accelerometer.spec.2 should exist", function() {
+        expect(typeof navigator.accelerometer.getCurrentAcceleration).toBeDefined();
+        expect(typeof navigator.accelerometer.getCurrentAcceleration == 'function').toBe(true);
+      });
+
+      it("accelerometer.spec.3 success callback should be called with an Acceleration object", function(done) {
+        // skip the test if Accelerometer doesn't exist on this device
+        if (!isAccelExist) {
+          pending();
+        }
+        var win = function(a) {
+          expect(a).toBeDefined();
+          expect(a.x).toBeDefined();
+          expect(typeof a.x == 'number').toBe(true);
+          expect(a.y).toBeDefined();
+          expect(typeof a.y == 'number').toBe(true);
+          expect(a.z).toBeDefined();
+          expect(typeof a.z == 'number').toBe(true);
+          expect(a.timestamp).toBeDefined();
+          expect(typeof a.timestamp).toBe('number');
+          done();
+        };
+
+        navigator.accelerometer.getCurrentAcceleration(win, fail.bind(null, done));
+      });
+
+      it("accelerometer.spec.4 success callback Acceleration object should have (reasonable) values for x, y and z expressed in m/s^2", function(done) {
+        // skip the test if Accelerometer doesn't exist on this device
+        if (!isAccelExist) {
+          pending();
+        }
+        var reasonableThreshold = 15;
+        var win = function(a) {
+          expect(a.x).toBeLessThan(reasonableThreshold);
+          expect(a.x).toBeGreaterThan(reasonableThreshold * -1);
+          expect(a.y).toBeLessThan(reasonableThreshold);
+          expect(a.y).toBeGreaterThan(reasonableThreshold * -1);
+          expect(a.z).toBeLessThan(reasonableThreshold);
+          expect(a.z).toBeGreaterThan(reasonableThreshold * -1);
+          done()
+        };
+
+        navigator.accelerometer.getCurrentAcceleration(win, fail.bind(null,done));
+      });
+
+      it("accelerometer.spec.5 success callback Acceleration object should return a recent timestamp", function(done) {
+        // skip the test if Accelerometer doesn't exist on this device
+        if (!isAccelExist) {
+          pending();
+        }
+        var veryRecently = (new Date()).getTime();
+        // Need to check that dates returned are not vastly greater than a recent time stamp.
+        // In case the timestamps returned are ridiculously high
+        var reasonableTimeLimit = veryRecently + 5000; // 5 seconds from now
+        var win = function(a) {
+          expect(a.timestamp).toBeGreaterThan(veryRecently);
+          expect(a.timestamp).toBeLessThan(reasonableTimeLimit);
+          done();
+        };
+
+        navigator.accelerometer.getCurrentAcceleration(win, fail.bind(null,done));
+      });
+    });
+
+    describe("watchAcceleration", function() {
+      var id;
+
+      afterEach(function() {
+          navigator.accelerometer.clearWatch(id);
+      });
+
+      it("accelerometer.spec.6 should exist", function() {
+          expect(navigator.accelerometer.watchAcceleration).toBeDefined();
+          expect(typeof navigator.accelerometer.watchAcceleration == 'function').toBe(true);
+      });
+
+      it("accelerometer.spec.7 success callback should be called with an Acceleration object", function(done) {
+        // skip the test if Accelerometer doesn't exist on this device
+        if (!isAccelExist) {
+          pending();
+        }
+        var win = function(a) {
+          expect(a).toBeDefined();
+          expect(a.x).toBeDefined();
+          expect(typeof a.x == 'number').toBe(true);
+          expect(a.y).toBeDefined();
+          expect(typeof a.y == 'number').toBe(true);
+          expect(a.z).toBeDefined();
+          expect(typeof a.z == 'number').toBe(true);
+          expect(a.timestamp).toBeDefined();
+          expect(typeof a.timestamp).toBe('number');
+          done();
+        };
+
+        id = navigator.accelerometer.watchAcceleration(win, fail.bind(null,done), {frequency:100});
+      });
+
+        it("accelerometer.spec.8 success callback Acceleration object should have (reasonable) values for x, y and z expressed in m/s^2", function(done) {
+          // skip the test if Accelerometer doesn't exist on this device
+          if (!isAccelExist) {
+            pending();
+          }
+          var reasonableThreshold = 15;
+          var win = function(a) {
+            expect(a.x).toBeLessThan(reasonableThreshold);
+            expect(a.x).toBeGreaterThan(reasonableThreshold * -1);
+            expect(a.y).toBeLessThan(reasonableThreshold);
+            expect(a.y).toBeGreaterThan(reasonableThreshold * -1);
+            expect(a.z).toBeLessThan(reasonableThreshold);
+            expect(a.z).toBeGreaterThan(reasonableThreshold * -1);
+            done();
+          };
+
+          id = navigator.accelerometer.watchAcceleration(win, fail.bind(null,done), {frequency:100});
+        });
+
+        it("accelerometer.spec.9 success callback Acceleration object should return a recent timestamp", function(done) {
+          // skip the test if Accelerometer doesn't exist on this device
+          if (!isAccelExist) {
+            pending();
+          }
+          var veryRecently = (new Date()).getTime();
+          // Need to check that dates returned are not vastly greater than a recent time stamp.
+          // In case the timestamps returned are ridiculously high
+          var reasonableTimeLimit = veryRecently + 5000; // 5 seconds from now
+          var win = function(a) {
+            expect(a.timestamp).toBeGreaterThan(veryRecently);
+            expect(a.timestamp).toBeLessThan(reasonableTimeLimit);
+            done();
+          };
+
+          id = navigator.accelerometer.watchAcceleration(win, fail.bind(null,done), {frequency:100});
+        });
+
+        it("accelerometer.spec.12 success callback should be preserved and called several times", function (done) {
+            // skip the test if Accelerometer doesn't exist on this device
+            if (!isAccelExist) {
+              pending();
+            }
+            var callbacksCallCount = 0,
+                callbacksCallTestCount = 3;
+
+            var win = function (a) {
+                if (callbacksCallCount++ < callbacksCallTestCount) return;
+                expect(typeof a).toBe('object');
+                done();
+            };
+
+            id = navigator.accelerometer.watchAcceleration(win, fail.bind(null, done), { frequency: 100 });
+        });
+    });
+
+    describe("clearWatch", function() {
+      it("accelerometer.spec.10 should exist", function() {
+          expect(navigator.accelerometer.clearWatch).toBeDefined();
+          expect(typeof navigator.accelerometer.clearWatch == 'function').toBe(true);
+      });
+
+      it("accelerometer.spec.11 should clear an existing watch", function(done) {
+          // skip the test if Accelerometer doesn't exist on this device
+          if (!isAccelExist) {
+              pending();
+          }
+          var id;
+
+          // expect win to get called exactly once
+          var win = function(a) {
+            // clear watch on first call
+            navigator.accelerometer.clearWatch(id);
+            // if win isn't called again in 201 ms we assume success
+            var tid = setTimeout(function() {
+              expect(true).toBe(true);
+              done();
+            }, 101);
+            // if win is called again, clear the timeout and fail the test
+            win = function() {
+              clearTimeout(tid);
+              fail(done);
+            }
+          };
+
+          // wrap the success call in a closure since the value of win changes between calls
+          id = navigator.accelerometer.watchAcceleration(function() { win(); }, fail.bind(null, done), {frequency:100});
+      });
+    });
+  });
+};
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    function roundNumber(num) {
+        var dec = 3;
+        var result = Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
+        return result;
+    }
+
+    var watchAccelId = null;
+
+    /**
+     * Start watching acceleration
+     */
+    var watchAccel = function () {
+        console.log("watchAccel()");
+
+        // Success callback
+        var success = function (a) {
+            document.getElementById('x').innerHTML = roundNumber(a.x);
+            document.getElementById('y').innerHTML = roundNumber(a.y);
+            document.getElementById('z').innerHTML = roundNumber(a.z);
+        };
+
+        // Fail callback
+        var fail = function (e) {
+            console.log("watchAccel fail callback with error code " + e);
+            stopAccel();
+            setAccelStatus(e);
+        };
+
+        // Update acceleration every 1 sec
+        var opt = {};
+        opt.frequency = 1000;
+        watchAccelId = navigator.accelerometer.watchAcceleration(success, fail, opt);
+
+        setAccelStatus("Running");
+    };
+
+    /**
+     * Stop watching the acceleration
+     */
+    var stopAccel = function () {
+        console.log("stopAccel()");
+        setAccelStatus("Stopped");
+        if (watchAccelId) {
+            navigator.accelerometer.clearWatch(watchAccelId);
+            watchAccelId = null;
+        }
+    };
+
+    /**
+     * Get current acceleration
+     */
+    var getAccel = function () {
+        console.log("getAccel()");
+
+        // Stop accel if running
+        stopAccel();
+
+        // Success callback
+        var success = function (a) {
+            document.getElementById('x').innerHTML = roundNumber(a.x);
+            document.getElementById('y').innerHTML = roundNumber(a.y);
+            document.getElementById('z').innerHTML = roundNumber(a.z);
+            console.log("getAccel success callback");
+        };
+
+        // Fail callback
+        var fail = function (e) {
+            console.log("getAccel fail callback with error code " + e);
+            setAccelStatus(e);
+        };
+
+        // Make call
+        var opt = {};
+        navigator.accelerometer.getCurrentAcceleration(success, fail, opt);
+    };
+
+    /**
+     * Set accelerometer status
+     */
+    var setAccelStatus = function (status) {
+        document.getElementById('accel_status').innerHTML = status;
+    };
+
+    /******************************************************************************/
+
+    var accelerometer_tests = '<div id="getAcceleration"></div>' +
+        'Expected result: Will update the status box with X, Y, and Z values when pressed. Status will read "Stopped"' +
+        '<p/> <div id="watchAcceleration"></div>' +
+        'Expected result: When pressed, will start a watch on the accelerometer and update X,Y,Z values when movement is sensed. Status will read "Running"' +
+        '<p/> <div id="clearAcceleration"></div>' +
+        'Expected result: Will clear the accelerometer watch, so X,Y,Z values will no longer be updated. Status will read "Stopped"';
+
+    contentEl.innerHTML = '<div id="info">' +
+        'Status: <span id="accel_status">Stopped</span>' +
+        '<table width="100%">' +
+        '<tr><td width="20%">X:</td><td id="x"> </td></tr>' +
+        '<tr><td width="20%">Y:</td><td id="y"> </td></tr>' +
+        '<tr><td width="20%">Z:</td><td id="z"> </td></tr>' +
+        '</table></div>' +
+        accelerometer_tests;
+
+    createActionButton('Get Acceleration', function () {
+        getAccel();
+    }, 'getAcceleration');
+
+    createActionButton('Start Watch', function () {
+        watchAccel();
+    }, 'watchAcceleration');
+
+    createActionButton('Clear Watch', function () {
+        stopAccel();
+    }, 'clearAcceleration');
+};
diff --git a/test/unittest/tests/battery.tests.js b/test/unittest/tests/battery.tests.js
new file mode 100644 (file)
index 0000000..fa06ead
--- /dev/null
@@ -0,0 +1,549 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+ */
+
+exports.defineAutoTests = function () {
+    var isWindowsStore = (cordova.platformId == "windows8") || (cordova.platformId == "windows" && !WinJS.Utilities.isPhone),
+        onEvent;
+
+    describe('Battery (navigator.battery)', function () {
+
+        it("battery.spec.1 should exist", function () {
+            if (isWindowsStore) {
+                pending('Battery status is not supported on windows store');
+            }
+
+            expect(navigator.battery).toBeDefined();
+        });
+    });
+
+    describe('Battery Events', function () {
+
+        describe("batterystatus", function () {
+
+            afterEach(function () {
+                if (!isWindowsStore) {
+                    try {
+                        window.removeEventListener("batterystatus", onEvent, false);
+                    }
+                    catch (e) {
+                        console.err('Error removing batterystatus event listener: ' + e)
+                    }
+                }
+            });
+
+            it("battery.spec.2 should fire batterystatus events", function (done) {
+                if (isWindowsStore) {
+                    pending('Battery status is not supported on windows store');
+                }
+
+                onEvent = jasmine.createSpy("BatteryStatus");
+
+                // batterystatus -> 30
+                window.addEventListener("batterystatus", onEvent, false);
+
+                navigator.battery._status({
+                    level: 30,
+                    isPlugged: false
+                });
+
+                setTimeout(function () {
+                    expect(onEvent).toHaveBeenCalled();
+                    done();
+                }, 100);
+
+            });
+        });
+
+        describe("batterylow", function () {
+
+            afterEach(function () {
+                if (!isWindowsStore) {
+                    try {
+                        window.removeEventListener("batterylow", onEvent, false);
+                    }
+                    catch (e) {
+                        console.err('Error removing batterylow event listener: ' + e)
+                    }
+                }
+            });
+
+            it("battery.spec.3 should fire batterylow event (30 -> 20)", function (done) {
+                if (isWindowsStore) {
+                    pending('Battery status is not supported on windows store');
+                }
+
+                onEvent = jasmine.createSpy("BatteryLow");
+
+                // batterylow 30 -> 20
+                window.addEventListener("batterylow", onEvent, false);
+
+                navigator.battery._status({
+                    level : 30,
+                    isPlugged : false
+                });
+
+                navigator.battery._status({
+                    level : 20,
+                    isPlugged : false
+                });
+
+                setTimeout(function () {
+                    expect(onEvent).toHaveBeenCalled();
+                    done();
+                }, 100);
+
+            });
+
+            it("battery.spec.3.1 should fire batterylow event (30 -> 19)", function (done) {
+                if (isWindowsStore) {
+                    pending('Battery status is not supported on windows store');
+                }
+
+                onEvent = jasmine.createSpy("BatteryLow");
+
+                // batterylow 30 -> 19
+                window.addEventListener("batterylow", onEvent, false);
+
+                navigator.battery._status({
+                    level : 30,
+                    isPlugged : false
+                });
+
+                navigator.battery._status({
+                    level : 19,
+                    isPlugged : false
+                });
+
+                setTimeout(function () {
+                    expect(onEvent).toHaveBeenCalled();
+                    done();
+                }, 100);
+            });
+
+            it("battery.spec.3.2 should not fire batterylow event (5 -> 20)", function (done) {
+                if (isWindowsStore) {
+                    pending('Battery status is not supported on windows store');
+                }
+
+                onEvent = jasmine.createSpy("BatteryLow");
+
+                // batterylow should not fire when level increases (5->20) ( CB-4519 )
+                window.addEventListener("batterylow", onEvent, false);
+
+                navigator.battery._status({
+                    level : 5,
+                    isPlugged : false
+                });
+
+                navigator.battery._status({
+                    level: 20,
+                    isPlugged: false
+                });
+
+                setTimeout(function () {
+                    expect(onEvent).not.toHaveBeenCalled();
+                    done();
+                }, 100);
+            });
+
+            it("battery.spec.3.3 batterylow event(21 -> 20) should not fire if charging", function (done) {
+                if (isWindowsStore) {
+                    pending('Battery status is not supported on windows store');
+                }
+
+                onEvent = jasmine.createSpy("BatteryLow");
+
+                // batterylow should NOT fire if we are charging   ( CB-4520 )
+                window.addEventListener("batterylow", onEvent, false);
+
+                navigator.battery._status({
+                    level : 21,
+                    isPlugged : true
+                });
+
+                navigator.battery._status({
+                    level : 20,
+                    isPlugged : true
+                });
+
+                setTimeout(function () {
+                    expect(onEvent).not.toHaveBeenCalled();
+                    done();
+                }, 100);
+            });
+        });
+
+        describe("batterycritical", function () {
+
+            afterEach(function () {
+                if (!isWindowsStore) {
+                    try {
+                        window.removeEventListener("batterycritical", onEvent, false);
+                    }
+                    catch (e) {
+                        console.err('Error removing batterycritical event listener: ' + e)
+                    }
+                }
+            });
+
+            it("battery.spec.4 should fire batterycritical event (19 -> 5)", function (done) {
+                if (isWindowsStore) {
+                    pending('Battery status is not supported on windows store');
+                }
+
+                onEvent = jasmine.createSpy("BatteryCritical");
+
+                // batterycritical 19->5
+                window.addEventListener("batterycritical", onEvent, false);
+
+                navigator.battery._status({
+                    level: 19,
+                    isPlugged: false
+                });
+
+                navigator.battery._status({
+                    level: 5,
+                    isPlugged: false
+                });
+
+                setTimeout(function () {
+                    expect(onEvent).toHaveBeenCalled();
+                    done();
+                }, 100);
+
+            });
+
+            it("battery.spec.4.1 should fire batterycritical event (19 -> 4)", function (done) {
+                if (isWindowsStore) {
+                    pending('Battery status is not supported on windows store');
+                }
+
+                onEvent = jasmine.createSpy("BatteryCritical");
+
+                // batterycritical 19->4
+                window.addEventListener("batterycritical", onEvent, false);
+
+                navigator.battery._status({
+                    level: 19,
+                    isPlugged: false
+                });
+
+                navigator.battery._status({
+                    level: 4,
+                    isPlugged: false
+                });
+
+                setTimeout(function () {
+                    expect(onEvent).toHaveBeenCalled();
+                    done();
+                }, 100);
+
+            });
+
+            it("battery.spec.4.2 should fire batterycritical event (100 -> 4) when decreases", function (done) {
+                if (isWindowsStore) {
+                    pending('Battery status is not supported on windows store');
+                }
+
+                onEvent = jasmine.createSpy("BatteryCritical");
+
+                // setup: batterycritical should fire when level decreases (100->4) ( CB-4519 )
+                window.addEventListener("batterycritical", onEvent, false);
+
+                navigator.battery._status({
+                    level: 100,
+                    isPlugged: false
+                });
+
+                navigator.battery._status({
+                    level: 4,
+                    isPlugged: false
+                });
+
+                setTimeout(function () {
+                    expect(onEvent).toHaveBeenCalled();
+                    done();
+                }, 100);
+            });
+
+            it("battery.spec.4.3 should not fire batterycritical event (4 -> 5) when increasing", function (done) {
+                if (isWindowsStore) {
+                    pending('Battery status is not supported on windows store');
+                }
+
+                onEvent = jasmine.createSpy("BatteryCritical");
+
+                window.addEventListener("batterycritical", onEvent, false);
+
+                // batterycritical should not fire when level increases (4->5)( CB-4519 )
+                navigator.battery._status({
+                    level: 4,
+                    isPlugged: false
+                });
+
+                navigator.battery._status({
+                    level: 5,
+                    isPlugged: false
+                });
+
+                setTimeout(function () {
+                    expect(onEvent.calls.count()).toBeLessThan(2);
+                    done();
+                }, 100);
+            });
+
+            it("battery.spec.4.4 should not fire batterycritical event (6 -> 5) if charging", function (done) {
+                if (isWindowsStore) {
+                    pending('Battery status is not supported on windows store');
+                }
+
+                onEvent = jasmine.createSpy("BatteryCritical");
+
+                window.addEventListener("batterycritical", onEvent, false);
+
+                // batterycritical should NOT fire if we are charging   ( CB-4520 )
+                navigator.battery._status({
+                    level: 6,
+                    isPlugged: true
+                });
+
+                navigator.battery._status({
+                    level: 5,
+                    isPlugged: true
+                });
+
+                setTimeout(function () {
+                    expect(onEvent).not.toHaveBeenCalled();
+                    done();
+                }, 100);
+            });
+        });
+    });
+};
+
+//******************************************************************************************
+//***************************************Manual Tests***************************************
+//******************************************************************************************
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+
+    /* Battery */
+    function updateInfo(info) {
+        document.getElementById('levelValue').innerText = info.level;
+        document.getElementById('pluggedValue').innerText = info.isPlugged;
+        if (info.level > 5) {
+            document.getElementById('criticalValue').innerText = "false";
+        }
+        if (info.level > 20) {
+            document.getElementById('lowValue').innerText = "false";
+        }
+    }
+
+    function batteryLow(info) {
+        document.getElementById('lowValue').innerText = "true";
+    }
+
+    function batteryCritical(info) {
+        document.getElementById('criticalValue').innerText = "true";
+    }
+
+    function addBattery() {
+        window.addEventListener("batterystatus", updateInfo, false);
+    }
+
+    function removeBattery() {
+        window.removeEventListener("batterystatus", updateInfo, false);
+    }
+
+    function addLow() {
+        window.addEventListener("batterylow", batteryLow, false);
+    }
+
+    function removeLow() {
+        window.removeEventListener("batterylow", batteryLow, false);
+    }
+
+    function addCritical() {
+        window.addEventListener("batterycritical", batteryCritical, false);
+    }
+
+    function removeCritical() {
+        window.removeEventListener("batterycritical", batteryCritical, false);
+    }
+
+    //Generate Dynamic Table
+    function generateTable(tableId, rows, cells, elements) {
+        var table = document.createElement('table');
+        for (var r = 0; r < rows; r++) {
+            var row = table.insertRow(r);
+            for (var c = 0; c < cells; c++) {
+                var cell = row.insertCell(c);
+                cell.setAttribute("align", "center");
+                for (var e in elements) {
+                    if (elements[e].position.row == r && elements[e].position.cell == c) {
+                        var htmlElement = document.createElement(elements[e].tag);
+                        var content;
+
+                        if (elements[e].content !== "") {
+                            content = document.createTextNode(elements[e].content);
+                            htmlElement.appendChild(content);
+                        }
+                        if (elements[e].type) {
+                            htmlElement.type = elements[e].type;
+                        }
+                        htmlElement.setAttribute("id", elements[e].id);
+                        cell.appendChild(htmlElement);
+                    }
+                }
+            }
+        }
+        table.setAttribute("align", "center");
+        table.setAttribute("id", tableId);
+        return table;
+    }
+    // Battery Elements
+    var batteryElements =
+        [{
+            id : "statusTag",
+            content : "Status:",
+            tag : "div",
+            position : {
+                row : 0,
+                cell : 0
+            }
+        }, {
+            id : "statusValue",
+            content : "",
+            tag : "div",
+            position : {
+                row : 0,
+                cell : 1
+            }
+        }, {
+            id : "levelTag",
+            content : "Level:",
+            tag : "div",
+            position : {
+                row : 1,
+                cell : 0
+            }
+        }, {
+            id : "levelValue",
+            content : "",
+            tag : "div",
+            position : {
+                row : 1,
+                cell : 1
+            }
+        }, {
+            id : "pluggedTag",
+            content : "Plugged:",
+            tag : "div",
+            position : {
+                row : 2,
+                cell : 0
+            }
+        }, {
+            id : "pluggedValue",
+            content : "",
+            tag : "div",
+            position : {
+                row : 2,
+                cell : 1
+            }
+        }, {
+            id : "lowTag",
+            content : "Low:",
+            tag : "div",
+            position : {
+                row : 3,
+                cell : 0
+            }
+        }, {
+            id : "lowValue",
+            content : "",
+            tag : "div",
+            position : {
+                row : 3,
+                cell : 1
+            }
+        }, {
+            id : "criticalTag",
+            content : "Critical:",
+            tag : "div",
+            position : {
+                row : 4,
+                cell : 0
+            }
+        }, {
+            id : "criticalValue",
+            content : "",
+            tag : "div",
+            position : {
+                row : 4,
+                cell : 1
+            }
+        }
+    ];
+
+    //Title audio results
+    var div = document.createElement('h2');
+    div.appendChild(document.createTextNode('Battery Status'));
+    div.setAttribute("align", "center");
+    contentEl.appendChild(div);
+
+    var batteryTable = generateTable('info', 5, 3, batteryElements);
+    contentEl.appendChild(batteryTable);
+
+    div = document.createElement('h2');
+    div.appendChild(document.createTextNode('Actions'));
+    div.setAttribute("align", "center");
+    contentEl.appendChild(div);
+
+    contentEl.innerHTML += '<h3>Battery Status Tests</h3>' +
+        'Will update values for level and plugged when they change. If battery low and critical values are false, they will get updated in status box, but only once' +
+        '<div id="addBS"></div><div id="remBs"></div>' +
+        '<h3>Battery Low Tests</h3>' +
+        '</p> Will update value for battery low to true when battery is below 20%' +
+        '<div id="addBl"></div><div id="remBl"></div>' +
+        '<h3>Battery Critical Tests</h3>' +
+        '</p> Will update value for battery critical to true when battery is below 5%' +
+        '<div id="addBc"></div><div id="remBc"></div>';
+
+    createActionButton('Add "batterystatus" listener', function () {
+        addBattery();
+    }, 'addBS');
+    createActionButton('Remove "batterystatus" listener', function () {
+        removeBattery();
+    }, 'remBs');
+    createActionButton('Add "batterylow" listener', function () {
+        addLow();
+    }, 'addBl');
+    createActionButton('Remove "batterylow" listener', function () {
+        removeLow();
+    }, 'remBl');
+    createActionButton('Add "batterycritical" listener', function () {
+        addCritical();
+    }, 'addBc');
+    createActionButton('Remove "batterycritical" listener', function () {
+        removeCritical();
+    }, 'remBc');
+};
diff --git a/test/unittest/tests/bridge.tests.js b/test/unittest/tests/bridge.tests.js
new file mode 100644 (file)
index 0000000..19c9105
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+/* This test requires some extra code to run, because we want benchmark results */
+
+/*
+ It's never going to be OVER 9000
+ http://youtu.be/SiMHTK15Pik 
+*/
+var FENCEPOST = 9000;
+
+var exec = cordova.require('cordova/exec'),
+    echo = cordova.require('cordova/plugin/echo'),
+    startTime,
+    endTime,
+    callCount,
+    durationMs = 1000,
+    asyncEcho,
+    useSetTimeout,
+    payloadSize,
+    payload;
+
+var vanillaWin = function(result) {
+    callCount++;
+    if (result != payload) {
+        console.log('Wrong echo data!');
+    }
+    var elapsedMs = new Date() - startTime;
+    if (elapsedMs < durationMs) {
+        if (useSetTimeout) {
+            setTimeout(echoMessage, 0);
+        } else {
+            echoMessage();
+        }
+    } else {
+        endTime = +new Date();
+    }
+}
+
+var reset = function()
+{
+    endTime = null;
+    callCount = 0;
+    useSetTimeout = false;
+    payloadSize = 5;
+    callsPerSecond = 0;
+}
+
+var echoMessage = function()
+{
+    echo(vanillaWin, fail, payload, asyncEcho);
+}
+
+var fail = function() {
+    expect(false).toBe(true);
+};
+
+function createTestCase(jsToNativeModeName, nativeToJsModeName, testAsyncEcho) {
+    it(jsToNativeModeName + '+' + nativeToJsModeName, function() {
+        expect(exec.jsToNativeModes[jsToNativeModeName]).toBeDefined();
+        expect(exec.nativeToJsModes[nativeToJsModeName]).toBeDefined();
+        reset();
+        payload = new Array(payloadSize * 10 + 1).join('012\n\n 6789');
+        asyncEcho = testAsyncEcho;
+        exec.setJsToNativeBridgeMode(exec.jsToNativeModes[jsToNativeModeName]);
+        exec.setNativeToJsBridgeMode(exec.nativeToJsModes[nativeToJsModeName]);
+
+        waits(300);
+        runs(function() {
+            startTime = +new Date();
+            echoMessage();
+        });
+        waitsFor(function() { return endTime; }, "never completed", durationMs * 2);
+        runs(function() {
+            var elapsedMs = endTime - startTime,
+                callsPerSecond = callCount * 1000 / elapsedMs;
+            expect(callsPerSecond).toBeGreaterThan(FENCEPOST);
+        });
+    });
+};
+
+// Wait so that the first benchmark doesn't have contention.
+describe('Wait for page to load.', function() {
+    it('waiting...', function() {
+        waits(1000);
+    });
+});
+
+// Before running on Android, set the following constants in NativeToJsMessagingBridge:
+// - ENABLE_LOCATION_CHANGE_EXEC_MODE = true
+// - DISABLE_EXEC_CHAINING = true
+describe('Android bridge with', function() {
+    var testAsyncEcho = false;
+    createTestCase('PROMPT', 'POLLING', testAsyncEcho);
+    createTestCase('JS_OBJECT', 'POLLING', testAsyncEcho);
+    createTestCase('LOCATION_CHANGE', 'ONLINE_EVENT', testAsyncEcho);
+
+    testAsyncEcho = true;
+    createTestCase('PROMPT', 'POLLING', testAsyncEcho);
+    createTestCase('PROMPT', 'HANGING_GET', testAsyncEcho);
+    createTestCase('PROMPT', 'LOAD_URL', testAsyncEcho);
+    createTestCase('PROMPT', 'ONLINE_EVENT', testAsyncEcho);
+    createTestCase('PROMPT', 'PRIVATE_API', testAsyncEcho);
+
+    createTestCase('JS_OBJECT', 'POLLING', testAsyncEcho);
+    createTestCase('JS_OBJECT', 'HANGING_GET', testAsyncEcho);
+    createTestCase('JS_OBJECT', 'LOAD_URL', testAsyncEcho);
+    createTestCase('JS_OBJECT', 'ONLINE_EVENT', testAsyncEcho);
+    createTestCase('JS_OBJECT', 'PRIVATE_API', testAsyncEcho);
+});
diff --git a/test/unittest/tests/camera.tests.js b/test/unittest/tests/camera.tests.js
new file mode 100644 (file)
index 0000000..fd97ec2
--- /dev/null
@@ -0,0 +1,504 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+exports.defineAutoTests = function () {
+    describe('Camera (navigator.camera)', function () {
+        it("should exist", function () {
+            expect(navigator.camera).toBeDefined();
+        });
+
+        it("should contain a getPicture function", function () {
+            expect(navigator.camera.getPicture).toBeDefined();
+            expect(typeof navigator.camera.getPicture == 'function').toBe(true);
+        });
+    });
+
+    describe('Camera Constants (window.Camera + navigator.camera)', function () {
+        it("camera.spec.1 window.Camera should exist", function () {
+            expect(window.Camera).toBeDefined();
+        });
+
+        it("camera.spec.2 should contain three DestinationType constants", function () {
+            expect(Camera.DestinationType.DATA_URL).toBe(0);
+            expect(Camera.DestinationType.FILE_URI).toBe(1);
+            expect(Camera.DestinationType.NATIVE_URI).toBe(2);
+            expect(navigator.camera.DestinationType.DATA_URL).toBe(0);
+            expect(navigator.camera.DestinationType.FILE_URI).toBe(1);
+            expect(navigator.camera.DestinationType.NATIVE_URI).toBe(2);
+        });
+
+        it("camera.spec.3 should contain two EncodingType constants", function () {
+            expect(Camera.EncodingType.JPEG).toBe(0);
+            expect(Camera.EncodingType.PNG).toBe(1);
+            expect(navigator.camera.EncodingType.JPEG).toBe(0);
+            expect(navigator.camera.EncodingType.PNG).toBe(1);
+        });
+
+        it("camera.spec.4 should contain three MediaType constants", function () {
+            expect(Camera.MediaType.PICTURE).toBe(0);
+            expect(Camera.MediaType.VIDEO).toBe(1);
+            expect(Camera.MediaType.ALLMEDIA).toBe(2);
+            expect(navigator.camera.MediaType.PICTURE).toBe(0);
+            expect(navigator.camera.MediaType.VIDEO).toBe(1);
+            expect(navigator.camera.MediaType.ALLMEDIA).toBe(2);
+        });
+
+        it("camera.spec.5 should contain three PictureSourceType constants", function () {
+            expect(Camera.PictureSourceType.PHOTOLIBRARY).toBe(0);
+            expect(Camera.PictureSourceType.CAMERA).toBe(1);
+            expect(Camera.PictureSourceType.SAVEDPHOTOALBUM).toBe(2);
+            expect(navigator.camera.PictureSourceType.PHOTOLIBRARY).toBe(0);
+            expect(navigator.camera.PictureSourceType.CAMERA).toBe(1);
+            expect(navigator.camera.PictureSourceType.SAVEDPHOTOALBUM).toBe(2);
+        });
+    });
+};
+
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    var platformId = cordova.require('cordova/platform').id;
+    var pictureUrl = null;
+    var fileObj = null;
+    var fileEntry = null;
+    var pageStartTime = +new Date();
+
+    //default camera options
+    var camQualityDefault = ['50', 50];
+    var camDestinationTypeDefault = ['FILE_URI', 1];
+    var camPictureSourceTypeDefault = ['CAMERA', 1];
+    var camAllowEditDefault = ['allowEdit', false];
+    var camEncodingTypeDefault = ['JPEG', 0];
+    var camMediaTypeDefault = ['mediaType', 0];
+    var camCorrectOrientationDefault = ['correctOrientation', false];
+    var camSaveToPhotoAlbumDefault = ['saveToPhotoAlbum', true];
+
+    var clearLog = function () {
+        var log = document.getElementById('info');
+        log.innerHTML = "";
+    }
+
+    function log(value) {
+        console.log(value);
+        document.getElementById('camera_status').textContent += (new Date() - pageStartTime) / 1000 + ': ' + value + '\n';
+    }
+
+    function clearStatus() {
+        document.getElementById('camera_status').innerHTML = '';
+        document.getElementById('camera_image').src = 'about:blank';
+        var canvas = document.getElementById('canvas');
+        canvas.width = canvas.height = 1;
+        pictureUrl = null;
+        fileObj = null;
+        fileEntry = null;
+    }
+
+    function setPicture(url, callback) {
+        try {
+            window.atob(url);
+            // if we got here it is a base64 string (DATA_URL)
+            url = "data:image/jpeg;base64," + url;
+        } catch (e) {
+            // not DATA_URL
+            log('URL: ' + url.slice(0, 100));
+        }
+
+        pictureUrl = url;
+        var img = document.getElementById('camera_image');
+        var startTime = new Date();
+        img.src = url;
+        img.onloadend = function () {
+            log('Image tag load time: ' + (new Date() - startTime));
+            callback && callback();
+        };
+    }
+
+    function onGetPictureError(e) {
+        log('Error getting picture: ' + (e.code || e));
+    }
+
+    function getPictureWin(data) {
+        setPicture(data);
+        // TODO: Fix resolveLocalFileSystemURI to work with native-uri.
+        if (pictureUrl.indexOf('file:') == 0 || pictureUrl.indexOf('content:') == 0 || pictureUrl.indexOf('ms-appdata:') === 0) {
+            resolveLocalFileSystemURI(data, function (e) {
+                fileEntry = e;
+                logCallback('resolveLocalFileSystemURI()', true)(e.toURL());
+            }, logCallback('resolveLocalFileSystemURI()', false));
+        } else if (pictureUrl.indexOf('data:image/jpeg;base64') == 0) {
+            // do nothing
+        } else {
+            var path = pictureUrl.replace(/^file:\/\/(localhost)?/, '').replace(/%20/g, ' ');
+            fileEntry = new FileEntry('image_name.png', path);
+        }
+    }
+
+    function getPicture() {
+        clearStatus();
+        var options = extractOptions();
+        log('Getting picture with options: ' + JSON.stringify(options));
+        var popoverHandle = navigator.camera.getPicture(getPictureWin, onGetPictureError, options);
+
+        // Reposition the popover if the orientation changes.
+        window.onorientationchange = function () {
+            var newPopoverOptions = new CameraPopoverOptions(0, 0, 100, 100, 0);
+            popoverHandle.setPosition(newPopoverOptions);
+        }
+    }
+
+    function uploadImage() {
+        var ft = new FileTransfer(),
+            uploadcomplete = 0,
+            progress = 0,
+            options = new FileUploadOptions();
+        options.fileKey = "photo";
+        options.fileName = 'test.jpg';
+        options.mimeType = "image/jpeg";
+        ft.onprogress = function (progressEvent) {
+            console.log('progress: ' + progressEvent.loaded + ' of ' + progressEvent.total);
+        };
+        var server = "http://cordova-filetransfer.jitsu.com";
+
+        ft.upload(pictureUrl, server + '/upload', win, fail, options);
+        function win(information_back) {
+            log('upload complete');
+        }
+        function fail(message) {
+            log('upload failed: ' + JSON.stringify(message));
+        }
+    }
+
+    function logCallback(apiName, success) {
+        return function () {
+            log('Call to ' + apiName + (success ? ' success: ' : ' failed: ') + JSON.stringify([].slice.call(arguments)));
+        };
+    }
+
+    /**
+     * Select image from library using a NATIVE_URI destination type
+     * This calls FileEntry.getMetadata, FileEntry.setMetadata, FileEntry.getParent, FileEntry.file, and FileReader.readAsDataURL.
+     */
+    function readFile() {
+        function onFileReadAsDataURL(evt) {
+            var img = document.getElementById('camera_image');
+            img.style.visibility = "visible";
+            img.style.display = "block";
+            img.src = evt.target.result;
+            log("FileReader.readAsDataURL success");
+        };
+
+        function onFileReceived(file) {
+            log('Got file: ' + JSON.stringify(file));
+            fileObj = file;
+
+            var reader = new FileReader();
+            reader.onload = function () {
+                log('FileReader.readAsDataURL() - length = ' + reader.result.length);
+            };
+            reader.onerror = logCallback('FileReader.readAsDataURL', false);
+            reader.readAsDataURL(file);
+        };
+        // Test out onFileReceived when the file object was set via a native <input> elements.
+        if (fileObj) {
+            onFileReceived(fileObj);
+        } else {
+            fileEntry.file(onFileReceived, logCallback('FileEntry.file', false));
+        }
+    }
+    function getFileInfo() {
+        // Test FileEntry API here.
+        fileEntry.getMetadata(logCallback('FileEntry.getMetadata', true), logCallback('FileEntry.getMetadata', false));
+        fileEntry.setMetadata(logCallback('FileEntry.setMetadata', true), logCallback('FileEntry.setMetadata', false), { "com.apple.MobileBackup": 1 });
+        fileEntry.getParent(logCallback('FileEntry.getParent', true), logCallback('FileEntry.getParent', false));
+        fileEntry.getParent(logCallback('FileEntry.getParent', true), logCallback('FileEntry.getParent', false));
+    };
+
+    /**
+     * Copy image from library using a NATIVE_URI destination type
+     * This calls FileEntry.copyTo and FileEntry.moveTo.
+     */
+    function copyImage() {
+        var onFileSystemReceived = function (fileSystem) {
+            var destDirEntry = fileSystem.root;
+            var origName = fileEntry.name;
+
+            // Test FileEntry API here.
+            fileEntry.copyTo(destDirEntry, 'copied_file.png', logCallback('FileEntry.copyTo', true), logCallback('FileEntry.copyTo', false));
+            fileEntry.moveTo(destDirEntry, 'moved_file.png', logCallback('FileEntry.moveTo', true), logCallback('FileEntry.moveTo', false));
+
+            //cleanup
+            //rename moved file back to original name so other tests can reference image
+            resolveLocalFileSystemURI(destDirEntry.nativeURL+'moved_file.png', function(fileEntry) {
+                fileEntry.moveTo(destDirEntry, origName, logCallback('FileEntry.moveTo', true), logCallback('FileEntry.moveTo', false));
+                console.log('Cleanup: successfully renamed file back to original name');
+            }, function () {
+                console.log('Cleanup: failed to rename file back to original name');
+            });
+
+            //remove copied file
+            resolveLocalFileSystemURI(destDirEntry.nativeURL+'copied_file.png', function(fileEntry) {
+                fileEntry.remove(logCallback('FileEntry.remove', true), logCallback('FileEntry.remove', false));
+                console.log('Cleanup: successfully removed copied file');
+            }, function () {
+                console.log('Cleanup: failed to remove copied file');
+            });
+        };
+
+        window.requestFileSystem(LocalFileSystem.TEMPORARY, 0, onFileSystemReceived, null);
+    };
+
+    /**
+     * Write image to library using a NATIVE_URI destination type
+     * This calls FileEntry.createWriter, FileWriter.write, and FileWriter.truncate.
+     */
+    function writeImage() {
+        var onFileWriterReceived = function (fileWriter) {
+            fileWriter.onwrite = logCallback('FileWriter.write', true);
+            fileWriter.onerror = logCallback('FileWriter.write', false);
+            fileWriter.write("some text!");
+        };
+
+        var onFileTruncateWriterReceived = function (fileWriter) {
+            fileWriter.onwrite = logCallback('FileWriter.truncate', true);
+            fileWriter.onerror = logCallback('FileWriter.truncate', false);
+            fileWriter.truncate(10);
+        };
+
+        fileEntry.createWriter(onFileWriterReceived, logCallback('FileEntry.createWriter', false));
+        fileEntry.createWriter(onFileTruncateWriterReceived, null);
+    };
+
+    function displayImageUsingCanvas() {
+        var canvas = document.getElementById('canvas');
+        var img = document.getElementById('camera_image');
+        var w = img.width;
+        var h = img.height;
+        h = 100 / w * h;
+        w = 100;
+        canvas.width = w;
+        canvas.height = h;
+        var context = canvas.getContext('2d');
+        context.drawImage(img, 0, 0, w, h);
+    };
+
+    /**
+     * Remove image from library using a NATIVE_URI destination type
+     * This calls FileEntry.remove.
+     */
+    function removeImage() {
+        fileEntry.remove(logCallback('FileEntry.remove', true), logCallback('FileEntry.remove', false));
+    };
+
+    function testInputTag(inputEl) {
+        clearStatus();
+        // iOS 6 likes to dead-lock in the onchange context if you
+        // do any alerts or try to remote-debug.
+        window.setTimeout(function () {
+            testNativeFile2(inputEl);
+        }, 0);
+    };
+
+    function testNativeFile2(inputEl) {
+        if (!inputEl.value) {
+            alert('No file selected.');
+            return;
+        }
+        fileObj = inputEl.files[0];
+        if (!fileObj) {
+            alert('Got value but no file.');
+            return;
+        }
+        var URLApi = window.URL || window.webkitURL;
+        if (URLApi) {
+            var blobURL = URLApi.createObjectURL(fileObj);
+            if (blobURL) {
+                setPicture(blobURL, function () {
+                    URLApi.revokeObjectURL(blobURL);
+                });
+            } else {
+                log('URL.createObjectURL returned null');
+            }
+        } else {
+            log('URL.createObjectURL() not supported.');
+        }
+    }
+
+    function extractOptions() {
+        var els = document.querySelectorAll('#image-options select');
+        var ret = {};
+        for (var i = 0, el; el = els[i]; ++i) {
+            var value = el.value;
+            if (value === '') continue;
+            if (el.isBool) {
+                ret[el.getAttribute("name")] = !!+value;
+            } else {
+                ret[el.getAttribute("name")] = +value;
+            }
+        }
+        return ret;
+    }
+
+    function createOptionsEl(name, values, selectionDefault) {
+        var openDiv = '<div style="display: inline-block">' + name + ': ';
+        var select = '<select name=' + name + '>';
+
+        var defaultOption = '';
+        if (selectionDefault == undefined) {
+            defaultOption = '<option value="">default</option>';
+        }
+
+        var options = '';
+        if (typeof values == 'boolean') {
+            values = { 'true': 1, 'false': 0 };
+        }
+        for (var k in values) {
+            var isSelected = '';
+            if (selectionDefault) {
+                if (selectionDefault[0] == k) {
+                    isSelected = 'selected';
+                }
+            }
+            options += '<option value="' + values[k] + '" ' + isSelected + '>' + k + '</option>';
+        }
+
+        var closeDiv = '</select></div>';
+
+        return openDiv + select + defaultOption + options + closeDiv;
+    }
+
+    /******************************************************************************/
+
+    var info_div = '<h1>Camera</h1>' +
+            '<div id="info">' +
+            '<b>Status:</b> <div id="camera_status"></div>' +
+            'img: <img width="100" id="camera_image">' +
+            'canvas: <canvas id="canvas" width="1" height="1"></canvas>' +
+            '</div>',
+        options_div = '<h2>Cordova Camera API Options</h2>' +
+            '<div id="image-options">' +
+            createOptionsEl('sourceType', Camera.PictureSourceType, camPictureSourceTypeDefault) +
+            createOptionsEl('destinationType', Camera.DestinationType, camDestinationTypeDefault) +
+            createOptionsEl('encodingType', Camera.EncodingType, camEncodingTypeDefault) +
+            createOptionsEl('mediaType', Camera.MediaType, camMediaTypeDefault) +
+            createOptionsEl('quality', { '0': 0, '50': 50, '80': 80, '100': 100 }, camQualityDefault) +
+            createOptionsEl('targetWidth', { '50': 50, '200': 200, '800': 800, '2048': 2048 }) +
+            createOptionsEl('targetHeight', { '50': 50, '200': 200, '800': 800, '2048': 2048 }) +
+            createOptionsEl('allowEdit', true, camAllowEditDefault) +
+            createOptionsEl('correctOrientation', true, camCorrectOrientationDefault) +
+            createOptionsEl('saveToPhotoAlbum', true, camSaveToPhotoAlbumDefault) +
+            createOptionsEl('cameraDirection', Camera.Direction) +
+            '</div>',
+        getpicture_div = '<div id="getpicture"></div>',
+        test_procedure = '<h4>Recommended Test Procedure</h4>' +
+            'Options not specified should be the default value' +
+            '<br>Status box should update with image and info whenever an image is taken or selected from library' +
+            '</p><div style="background:#B0C4DE;border:1px solid #FFA07A;margin:15px 6px 0px;min-width:295px;max-width:97%;padding:4px 0px 2px 10px;min-height:160px;max-height:200px;overflow:auto">' +
+            '<ol> <li>All default options. Should be able to edit once picture is taken and will be saved to library.</li>' +
+            '</p><li>sourceType=PHOTOLIBRARY<br>Should be able to see picture that was just taken in previous test and edit when selected</li>' +
+            '</p><li>sourceType=Camera<br>allowEdit=false<br>saveToPhotoAlbum=false<br>Should not be able to edit when taken and will not save to library</li>' +
+            '</p><li>encodingType=PNG<br>allowEdit=true<br>saveToPhotoAlbum=true<br>cameraDirection=FRONT<br>Should bring up front camera. Verify in status box info URL that image is encoded as PNG.</li>' +
+            '</p><li>sourceType=SAVEDPHOTOALBUM<br>mediaType=VIDEO<br>Should only be able to select a video</li>' +
+            '</p><li>sourceType=SAVEDPHOTOALBUM<br>mediaType=PICTURE<br>allowEdit=false<br>Should only be able to select a picture and not edit</li>' +
+            '</p><li>sourceType=PHOTOLIBRARY<br>mediaType=ALLMEDIA<br>allowEdit=true<br>Should be able to select pics and videos and edit picture if selected</li>' +
+            '</p><li>sourceType=CAMERA<br>targetWidth & targetHeight=50<br>allowEdit=false<br>Do Get File Metadata test below and take note of size<br>Repeat test but with width and height=800. Size should be significantly larger.</li>' +
+            '</p><li>quality=0<br>targetWidth & targetHeight=default<br>allowEdit=false<br>Do Get File Metadata test below and take note of size<br>Repeat test but with quality=80. Size should be significantly larger.</li>' +
+            '</ol></div>',
+        inputs_div = '<h2>Native File Inputs</h2>' +
+            'For the following tests, status box should update with file selected' +
+            '</p><div>input type=file <input type="file" class="testInputTag"></div>' +
+            '<div>capture=camera <input type="file" accept="image/*;capture=camera" class="testInputTag"></div>' +
+            '<div>capture=camcorder <input type="file" accept="video/*;capture=camcorder" class="testInputTag"></div>' +
+            '<div>capture=microphone <input type="file" accept="audio/*;capture=microphone" class="testInputTag"></div>',
+        actions_div = '<h2>Actions</h2>' +
+            'For the following tests, ensure that an image is set in status box' +
+            '</p><div id="metadata"></div>' +
+            'Expected result: Get metadata about file selected.<br>Status box will show, along with the metadata, "Call to FileEntry.getMetadata success, Call to FileEntry.setMetadata success, Call to FileEntry.getParent success"' +
+            '</p><div id="reader"></div>' +
+            'Expected result: Read contents of file.<br>Status box will show "Got file: {some metadata}, FileReader.readAsDataURL() - length = someNumber"' +
+            '</p><div id="copy"></div>' +
+            'Expected result: Copy image to new location and move file to different location.<br>Status box will show "Call to FileEntry.copyTo success:{some metadata}, Call to FileEntry.moveTo success:{some metadata}"' +
+            '</p><div id="write"></div>' +
+            'Expected result: Write image to library.<br>Status box will show "Call to FileWriter.write success:{some metadata}, Call to FileWriter.truncate success:{some metadata}"' +
+            '</p><div id="upload"></div>' +
+            'Expected result: Upload image to server.<br>Status box may print out progress. Once finished will show "upload complete"' +
+            '</p><div id="draw_canvas"></div>' +
+            'Expected result: Display image using canvas.<br>Image will be displayed in status box under "canvas:"' +
+            '</p><div id="remove"></div>' +
+            'Expected result: Remove image from library.<br>Status box will show "FileEntry.remove success:["OK"]';
+
+    // We need to wrap this code due to Windows security restrictions
+    // see http://msdn.microsoft.com/en-us/library/windows/apps/hh465380.aspx#differences for details
+    if (window.MSApp && window.MSApp.execUnsafeLocalFunction) {
+        MSApp.execUnsafeLocalFunction(function() {
+            contentEl.innerHTML = info_div + options_div + getpicture_div + test_procedure + inputs_div + actions_div;
+        });
+    } else {
+        contentEl.innerHTML = info_div + options_div + getpicture_div + test_procedure + inputs_div + actions_div;
+    }
+
+    var elements = document.getElementsByClassName("testInputTag");
+    var listener = function (e) {
+        testInputTag(e.target);
+    }
+    for (var i = 0; i < elements.length; ++i) {
+        var item = elements[i];
+        item.addEventListener("change", listener, false);
+    }
+
+    createActionButton('Get picture', function () {
+        getPicture();
+    }, 'getpicture');
+
+    createActionButton('Clear Status', function () {
+        clearStatus();
+    }, 'getpicture');
+
+    createActionButton('Get File Metadata', function () {
+        getFileInfo();
+    }, 'metadata');
+
+    createActionButton('Read with FileReader', function () {
+        readFile();
+    }, 'reader');
+
+    createActionButton('Copy Image', function () {
+        copyImage();
+    }, 'copy');
+
+    createActionButton('Write Image', function () {
+        writeImage();
+    }, 'write');
+
+    createActionButton('Upload Image', function () {
+        uploadImage();
+    }, 'upload');
+
+    createActionButton('Draw Using Canvas', function () {
+        displayImageUsingCanvas();
+    }, 'draw_canvas');
+
+    createActionButton('Remove Image', function () {
+        removeImage();
+    }, 'remove');
+};
diff --git a/test/unittest/tests/capture.tests.js b/test/unittest/tests/capture.tests.js
new file mode 100644 (file)
index 0000000..28c3dd2
--- /dev/null
@@ -0,0 +1,263 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+exports.defineAutoTests = function () {
+    describe('Capture (navigator.device.capture)', function () {
+        it("capture.spec.1 should exist", function () {
+            expect(navigator.device).toBeDefined();
+            expect(navigator.device.capture).toBeDefined();
+        });
+
+        it("capture.spec.2 should have the correct properties ", function () {
+            expect(navigator.device.capture.supportedAudioModes).toBeDefined();
+            expect(navigator.device.capture.supportedImageModes).toBeDefined();
+            expect(navigator.device.capture.supportedVideoModes).toBeDefined();
+        });
+
+        it("capture.spec.3 should contain a captureAudio function", function () {
+            expect(navigator.device.capture.captureAudio).toBeDefined();
+            expect(typeof navigator.device.capture.captureAudio == 'function').toBe(true);
+        });
+
+        it("capture.spec.4 should contain a captureImage function", function () {
+            expect(navigator.device.capture.captureImage).toBeDefined();
+            expect(typeof navigator.device.capture.captureImage == 'function').toBe(true);
+        });
+
+        it("capture.spec.5 should contain a captureVideo function", function () {
+            expect(navigator.device.capture.captureVideo).toBeDefined();
+            expect(typeof navigator.device.capture.captureVideo == 'function').toBe(true);
+        });
+
+        describe('CaptureAudioOptions', function () {
+            it("capture.spec.6 CaptureAudioOptions constructor should exist", function () {
+                var options = new CaptureAudioOptions();
+                expect(options).toBeDefined();
+                expect(options.limit).toBeDefined();
+                expect(options.duration).toBeDefined();
+            });
+        });
+
+        describe('CaptureImageOptions', function () {
+            it("capture.spec.7 CaptureImageOptions constructor should exist", function () {
+                var options = new CaptureImageOptions();
+                expect(options).toBeDefined();
+                expect(options.limit).toBeDefined();
+            });
+        });
+
+        describe('CaptureVideoOptions', function () {
+            it("capture.spec.8 CaptureVideoOptions constructor should exist", function () {
+                var options = new CaptureVideoOptions();
+                expect(options).toBeDefined();
+                expect(options.limit).toBeDefined();
+                expect(options.duration).toBeDefined();
+            });
+        });
+
+        describe('CaptureError interface', function () {
+            it("capture.spec.9 CaptureError constants should be defined", function () {
+                expect(CaptureError.CAPTURE_INTERNAL_ERR).toBe(0);
+                expect(CaptureError.CAPTURE_APPLICATION_BUSY).toBe(1);
+                expect(CaptureError.CAPTURE_INVALID_ARGUMENT).toBe(2);
+                expect(CaptureError.CAPTURE_NO_MEDIA_FILES).toBe(3);
+            });
+
+            it("capture.spec.10 CaptureError properties should exist", function () {
+                var error = new CaptureError();
+                expect(error).toBeDefined();
+                expect(error.code).toBeDefined();
+            });
+        });
+
+        describe('MediaFileData', function () {
+            it("capture.spec.11 MediaFileData constructor should exist", function () {
+                var fileData = new MediaFileData();
+                expect(fileData).toBeDefined();
+                expect(fileData.bitrate).toBeDefined();
+                expect(fileData.codecs).toBeDefined();
+                expect(fileData.duration).toBeDefined();
+                expect(fileData.height).toBeDefined();
+                expect(fileData.width).toBeDefined();
+            });
+        });
+
+        describe('MediaFile', function () {
+            it("capture.spec.12 MediaFile constructor should exist", function () {
+                var fileData = new MediaFile();
+                expect(fileData).toBeDefined();
+                expect(fileData.name).toBeDefined();
+                expect(fileData.type).toBeDefined();
+                expect(fileData.lastModifiedDate).toBeDefined();
+                expect(fileData.size).toBeDefined();
+            });
+        });
+    });
+};
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    var platformId = cordova.require('cordova/platform').id;
+    var pageStartTime = +new Date();
+
+    function log(value) {
+        document.getElementById('camera_status').textContent += (new Date() - pageStartTime) / 1000 + ': ' + value + '\n';
+        console.log(value);
+    }
+
+    function captureAudioWin(mediaFiles) {
+        var path = mediaFiles[0].fullPath;
+        log('Audio captured: ' + path);
+        var m = new Media(path);
+        m.play();
+    }
+
+    function captureAudioFail(e) {
+        log('Error getting audio: ' + e.code);
+    }
+
+    function getAudio() {
+        clearStatus();
+        var options = { limit: 1, duration: 10 };
+        navigator.device.capture.captureAudio(captureAudioWin, captureAudioFail, options);
+    }
+
+    function captureImageWin(mediaFiles) {
+        var path = mediaFiles[0].fullPath;
+        // Necessary since windows doesn't allow file URLs for <img> elements
+        if (cordova.platformId == 'windows' || cordova.platformId == 'windows8' || cordova.platformId === 'browser') {
+            path = mediaFiles[0].localURL;
+        }
+        log('Image captured: ' + path);
+        document.getElementById('camera_image').src = path;
+    }
+
+    function captureImageFail(e) {
+        log('Error getting image: ' + e.code);
+    }
+
+    function getImage() {
+        clearStatus();
+        var options = { limit: 1 };
+        navigator.device.capture.captureImage(captureImageWin, captureImageFail, options);
+    }
+
+    function captureVideoWin(mediaFiles) {
+        var path = mediaFiles[0].fullPath;
+        log('Video captured: ' + path);
+
+        // need to inject the video element into the html
+        // doesn't seem to work if you have a pre-existing video element and
+        // add in a source tag
+        var vid = document.createElement('video');
+        vid.id = "theVideo";
+        vid.width = "320";
+        vid.height = "240";
+        vid.controls = "true";
+        var source_vid = document.createElement('source');
+        source_vid.id = "theSource";
+        source_vid.src = path;
+        vid.appendChild(source_vid);
+        document.getElementById('video_container').appendChild(vid);
+    }
+
+    function captureVideoFail(e) {
+        log('Error getting video: ' + e.code);
+    }
+
+    function getVideo() {
+        clearStatus();
+        var options = { limit: 1, duration: 10 };
+        navigator.device.capture.captureVideo(captureVideoWin, captureVideoFail, options);
+    }
+
+    function resolveMediaFileURL(mediaFile, callback) {
+        resolveLocalFileSystemURL(mediaFile.localURL, function (entry) {
+            log("Resolved by URL: " + mediaFile.localURL);
+            if (callback) callback();
+        }, function (err) {
+            log("Failed to resolve by URL: " + mediaFile.localURL);
+            log("Error: " + JSON.stringify(err));
+            if (callback) callback();
+        });
+    }
+
+    function resolveMediaFile(mediaFile, callback) {
+        resolveLocalFileSystemURL(mediaFile.fullPath, function (entry) {
+            log("Resolved by path: " + mediaFile.fullPath);
+            if (callback) callback();
+        }, function (err) {
+            log("Failed to resolve by path: " + mediaFile.fullPath);
+            log("Error: " + JSON.stringify(err));
+            if (callback) callback();
+        });
+    }
+
+    function resolveVideo() {
+        clearStatus();
+        var options = { limit: 1, duration: 5 };
+        navigator.device.capture.captureVideo(function (mediaFiles) {
+            captureVideoWin(mediaFiles);
+            resolveMediaFile(mediaFiles[0], function () {
+                resolveMediaFileURL(mediaFiles[0]);
+            });
+        }, captureVideoFail, options);
+    }
+
+    function clearStatus() {
+        document.getElementById('camera_status').innerHTML = '';
+        document.getElementById('camera_image').src = 'about:blank';
+    }
+
+    /******************************************************************************/
+
+    contentEl.innerHTML = '<div id="info" style="white-space: pre-wrap">' +
+        '<b>Status:</b> <div id="camera_status"></div>' +
+        'img: <img width="100" id="camera_image">' +
+        'video: <div id="video_container"></div>' +
+        '</div><div id="audio"></div>' +
+        'Expected result: Audio recorder will come up. Press record button to record for 10 seconds. Press Done. Status box will update with audio file and automatically play recording.' +
+        '<p/> <div id="image"></div>' +
+        'Expected result: Status box will update with image just taken.' +
+        '<p/> <div id="video"></div>' +
+        'Expected result: Record 10 second video. Status box will update with video file that you can play.' +
+        '<p/> <div id="video_and_resolve"></div>' +
+        'Expected result: Record 5 second video. Status box will show that URL was resolved and video will get added at the bottom of the status box for playback.';
+
+    createActionButton('Capture 10 sec of audio and play', function () {
+        getAudio();
+    }, 'audio');
+
+    createActionButton('Capture 1 image', function () {
+        getImage();
+    }, 'image');
+
+    createActionButton('Capture 10 sec of video', function () {
+        getVideo();
+    }, 'video');
+
+    createActionButton('Capture 5 sec of video and resolve', function () {
+        resolveVideo();
+    }, 'video_and_resolve');
+};
diff --git a/test/unittest/tests/compass.tests.js b/test/unittest/tests/compass.tests.js
new file mode 100644 (file)
index 0000000..7353178
--- /dev/null
@@ -0,0 +1,252 @@
+/*
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you 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.
+*
+*/
+
+exports.defineAutoTests = function () {
+    var fail = function (done, message) {
+        message = (typeof message !== 'string') ? "Forced failure: wrong callback called" : message;
+        expect(true).toFailWithMessage(message);
+        done();
+    },
+        unexpectedSuccess = "Forced failure: success callback should not have been called",
+        unexpectedFailure = "Forced failure: error callback should not have been called";
+
+    describe('Compass (navigator.compass)', function () {
+        beforeEach(function () {
+            jasmine.Expectation.addMatchers({
+                toFailWithMessage: function () {
+                    return {
+                        compare: function (actual, customMessage) {
+                            var pass = false;
+                            if (customMessage === undefined) {
+                                customMessage = "Forced failure: wrong callback called";
+                            }
+                            return {
+                                pass: pass,
+                                message: customMessage
+                            };
+                        }
+                    };
+                }
+            });
+        });
+
+        var isCompassAvailable = true;
+
+        beforeEach(function (done) {
+            if (!isCompassAvailable) {
+                // if we're already ensured that compass is not available, no need to check it again
+                done();
+                return;
+            }
+            // Try to access compass device, and if it is not available
+            // set hardwarefailure flag to mark some tests pending
+            navigator.compass.getCurrentHeading(done, function (error) {
+                if (error.code == CompassError.COMPASS_NOT_SUPPORTED) {
+                    isCompassAvailable = false;
+                }
+                done();
+            });
+        });
+
+        it("compass.spec.1 should exist", function () {
+            expect(navigator.compass).toBeDefined();
+        });
+
+        it("compass.spec.2 should contain a getCurrentHeading function", function () {
+            expect(navigator.compass.getCurrentHeading).toBeDefined();
+            expect(typeof navigator.compass.getCurrentHeading == 'function').toBe(true);
+        });
+
+        it("compass.spec.3 getCurrentHeading success callback should be called with a Heading object", function (done) {
+            if (!isCompassAvailable) {
+                pending();
+            }
+            navigator.compass.getCurrentHeading(function (a) {
+                expect(a instanceof CompassHeading).toBe(true);
+                expect(a.magneticHeading).toBeDefined();
+                expect(typeof a.magneticHeading == 'number').toBe(true);
+                expect(a.trueHeading).not.toBe(undefined);
+                expect(typeof a.trueHeading == 'number' || a.trueHeading === null).toBe(true);
+                expect(a.headingAccuracy).not.toBe(undefined);
+                expect(typeof a.headingAccuracy == 'number' || a.headingAccuracy === null).toBe(true);
+                expect(typeof a.timestamp == 'number').toBe(true);
+                done();
+            }, fail.bind(null, done, unexpectedFailure));
+        });
+
+        it("compass.spec.4 should contain a watchHeading function", function () {
+            expect(navigator.compass.watchHeading).toBeDefined();
+            expect(typeof navigator.compass.watchHeading == 'function').toBe(true);
+        });
+
+        it("compass.spec.5 should contain a clearWatch function", function () {
+            expect(navigator.compass.clearWatch).toBeDefined();
+            expect(typeof navigator.compass.clearWatch == 'function').toBe(true);
+        });
+
+        describe('Compass Constants (window.CompassError)', function () {
+            it("compass.spec.1 should exist", function () {
+                expect(window.CompassError).toBeDefined();
+                expect(window.CompassError.COMPASS_INTERNAL_ERR).toBe(0);
+                expect(window.CompassError.COMPASS_NOT_SUPPORTED).toBe(20);
+            });
+        });
+
+        describe('Compass Heading model (CompassHeading)', function () {
+            it("compass.spec.1 should exist", function () {
+                expect(CompassHeading).toBeDefined();
+            });
+
+            it("compass.spec.8 should be able to create a new CompassHeading instance with no parameters", function () {
+                var h = new CompassHeading();
+                expect(h).toBeDefined();
+                expect(h.magneticHeading).toBeUndefined();
+                expect(h.trueHeading).toBeUndefined();
+                expect(h.headingAccuracy).toBeUndefined();
+                expect(typeof h.timestamp == 'number').toBe(true);
+            });
+
+            it("compass.spec.9 should be able to create a new CompassHeading instance with parameters", function () {
+                var h = new CompassHeading(1, 2, 3, 4);
+                expect(h.magneticHeading).toBe(1);
+                expect(h.trueHeading).toBe(2);
+                expect(h.headingAccuracy).toBe(3);
+                expect(h.timestamp.valueOf()).toBe(4);
+                expect(typeof h.timestamp == 'number').toBe(true);
+            });
+        });
+    });
+};
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    function roundNumber(num) {
+        var dec = 3;
+        var result = Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
+        return result;
+    }
+
+    var watchCompassId = null;
+
+    /**
+     * Start watching compass
+     */
+    var watchCompass = function () {
+        console.log("watchCompass()");
+
+        // Success callback
+        var success = function (a) {
+            document.getElementById('compassHeading').innerHTML = roundNumber(a.magneticHeading);
+        };
+
+        // Fail callback
+        var fail = function (e) {
+            console.log("watchCompass fail callback with error code " + e);
+            stopCompass();
+            setCompassStatus(e);
+        };
+
+        // Stop compass if running
+        stopCompass();
+
+        // Update heading every 1 sec
+        var opt = {};
+        opt.frequency = 1000;
+        watchCompassId = navigator.compass.watchHeading(success, fail, opt);
+
+        setCompassStatus("Running");
+    };
+
+    /**
+     * Stop watching the acceleration
+     */
+    var stopCompass = function () {
+        setCompassStatus("Stopped");
+        if (watchCompassId) {
+            navigator.compass.clearWatch(watchCompassId);
+            watchCompassId = null;
+        }
+    };
+
+    /**
+     * Get current compass
+     */
+    var getCompass = function () {
+        console.log("getCompass()");
+
+        // Stop compass if running
+        stopCompass();
+
+        // Success callback
+        var success = function (a) {
+            document.getElementById('compassHeading').innerHTML = roundNumber(a.magneticHeading);
+        };
+
+        // Fail callback
+        var fail = function (e) {
+            console.log("getCompass fail callback with error code " + e.toString);
+            setCompassStatus(e);
+        };
+
+        // Make call
+        var opt = {};
+        navigator.compass.getCurrentHeading(success, fail, opt);
+    };
+
+    /**
+     * Set compass status
+     */
+    var setCompassStatus = function (status) {
+        document.getElementById('compass_status').innerHTML = status;
+    };
+
+    /******************************************************************************/
+
+    var orientation_tests = '<h3>iOS devices may bring up a calibration screen when initiating these tests</h3>' +
+        '<div id="getCompass"></div>' +
+        'Expected result: Will update the status box with current heading. Status will read "Stopped"' +
+        '<p/> <div id="watchCompass"></div>' +
+        'Expected result: When pressed, will start a watch on the compass and update the heading value when heading changes. Status will read "Running"' +
+        '<p/> <div id="stopCompass"></div>' +
+        'Expected result: Will clear the compass watch, so heading value will no longer be updated. Status will read "Stopped"';
+
+    contentEl.innerHTML = '<div id="info"><b>Status: </b>' +
+        '<span id="compass_status">Stopped</span>' +
+        '<table width="100%"><tr>' +
+        '<td width="33%">Heading: <span id="compassHeading"></span>' +
+        '</td></tr></table></div>' +
+        orientation_tests;
+
+    createActionButton('Get Compass', function () {
+        getCompass();
+    }, 'getCompass');
+
+    createActionButton('Start Watching Compass', function () {
+        watchCompass();
+    }, 'watchCompass');
+
+    createActionButton('Stop Watching Compass', function () {
+        stopCompass();
+    }, 'stopCompass');
+};
diff --git a/test/unittest/tests/console.tests.js b/test/unittest/tests/console.tests.js
new file mode 100644 (file)
index 0000000..6479e41
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+exports.defineAutoTests = function () {
+  describe("Console", function () {
+    it("console.spec.1 should exist", function() {
+        expect(window.console).toBeDefined();
+    });
+    
+    it("console.spec.2 has required methods log|warn|error", function(){
+        expect(window.console.log).toBeDefined();
+        expect(typeof window.console.log).toBe('function');
+        
+        expect(window.console.warn).toBeDefined();
+        expect(typeof window.console.warn).toBe('function');
+        
+        expect(window.console.error).toBeDefined();
+        expect(typeof window.console.error).toBe('function');                   
+        
+    });
+    
+  });
+};
+
+exports.defineManualTests = function (contentEl, createActionButton) {};
diff --git a/test/unittest/tests/contacts.tests.js b/test/unittest/tests/contacts.tests.js
new file mode 100644 (file)
index 0000000..a3bb98d
--- /dev/null
@@ -0,0 +1,594 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+exports.defineAutoTests = function () {
+  // global to store a contact so it doesn't have to be created or retrieved multiple times
+  // all of the setup/teardown test methods can reference the following variables to make sure to do the right cleanup
+  var gContactObj = null,
+    gContactId = null,
+    isWindowsPhone8 = cordova.platformId == 'windowsphone',
+    isWindows = (cordova.platformId === "windows") || (cordova.platformId === "windows8"),
+    isWindowsPhone81 =  isWindows && WinJS.Utilities.isPhone;
+  var fail = function(done) {
+    expect(true).toBe(false);
+    done();
+  };
+
+  var MEDIUM_TIMEOUT = 30000;
+
+  var removeContact = function(){
+      if (gContactObj) {
+          gContactObj.remove(function(){},function(){
+              console.log("[CONTACTS ERROR]: removeContact cleanup method failed to clean up test artifacts.");
+          });
+          gContactObj = null;
+      }
+  };
+  
+  describe("Contacts (navigator.contacts)", function () {
+      it("contacts.spec.1 should exist", function() {
+          expect(navigator.contacts).toBeDefined();
+      });
+      it("contacts.spec.2 should contain a find function", function() {
+          expect(navigator.contacts.find).toBeDefined();
+          expect(typeof navigator.contacts.find).toBe('function');
+      });
+      describe("find method", function() {
+          it("contacts.spec.3 success callback should be called with an array", function(done) {
+              // Find method is not supported on Windows platform
+              if (isWindows && !isWindowsPhone81) {
+                  pending();
+                  return;
+              }
+              var win = function(result) {
+                      expect(result).toBeDefined();
+                      expect(result instanceof Array).toBe(true);
+                      done();
+                  },
+                  obj = new ContactFindOptions();
+
+              obj.filter="";
+              obj.multiple=true;
+              navigator.contacts.find(["displayName", "name", "phoneNumbers", "emails"], win, fail.bind(null, done), obj);
+          });
+          it("success callback should be called with an array, even if partial ContactFindOptions specified", function (done) {
+              // Find method is not supported on Windows platform
+              if (isWindows && !isWindowsPhone81) {
+                  pending();
+                  return;
+              }
+              var win = function (result) {
+                  expect(result).toBeDefined();
+                  expect(result instanceof Array).toBe(true);
+                  done();
+              };
+
+              navigator.contacts.find(["displayName", "name", "phoneNumbers", "emails"], win, fail.bind(null, done),
+                  { multiple: true });
+          });
+          it("contacts.spec.4 should throw an exception if success callback is empty", function() {
+              var obj = new ContactFindOptions();
+              obj.filter="";
+              obj.multiple=true;
+
+              expect(function () {
+                  navigator.contacts.find(["displayName", "name", "emails", "phoneNumbers"], null, fail.bind(null, done), obj);
+              }).toThrow();
+          });
+          it("contacts.spec.5 error callback should be called when no fields are specified", function(done) {
+              var win = fail, // we don't want this to be called
+                  error = function(result) {
+                      expect(result).toBeDefined();
+                      expect(result.code).toBe(ContactError.INVALID_ARGUMENT_ERROR);
+                      done();
+                  },
+                  obj = new ContactFindOptions();
+
+              obj.filter="";
+              obj.multiple=true;
+              navigator.contacts.find([], win, error, obj);
+          });
+          describe("with newly-created contact", function () {
+
+              afterEach(removeContact);
+
+              it("contacts.spec.6 should be able to find a contact by name", function (done) {
+                  // Find method is not supported on Windows Store apps.
+                  // also this test will be skipped for Windows Phone 8.1 because function "save" not supported on WP8.1
+                  if (isWindows || isWindowsPhone8) {
+                      pending();
+                  }
+
+                  var foundName = function(result) {
+                          var bFound = false;
+                          try {
+                              for (var i=0; i < result.length; i++) {
+                                  if (result[i].name.familyName == "Delete") {
+                                      bFound = true;
+                                      break;
+                                  }
+                              }
+                          } catch(e) {
+                              return false;
+                          }
+                          return bFound;
+                      },
+                      test = function(savedContact) {
+                          // update so contact will get removed
+                          gContactObj = savedContact;
+                          // ----
+                          // Find asserts
+                          // ---
+                          var findWin = function(object) {
+                                  expect(object instanceof Array).toBe(true);
+                                  expect(object.length >= 1).toBe(true);
+                                  expect(foundName(object)).toBe(true);
+                                  done();
+                              },
+                              findFail = fail,
+                              obj = new ContactFindOptions();
+
+                          obj.filter="Delete";
+                          obj.multiple=true;
+
+                          navigator.contacts.find(["displayName", "name", "phoneNumbers", "emails"], findWin, findFail.bind(null, done), obj);
+                      };
+
+                  gContactObj = new Contact();
+                  gContactObj.name = new ContactName();
+                  gContactObj.name.familyName = "Delete";
+                  gContactObj.save(test, fail.bind(null, done));
+              });
+          });
+
+      });
+      describe('create method', function() {
+          it("contacts.spec.1 should exist", function() {
+              expect(navigator.contacts.create).toBeDefined();
+              expect(typeof navigator.contacts.create).toBe('function');
+          });
+          it("contacts.spec.8 should return a Contact object", function() {
+              var bDay = new Date(1976, 7,4);
+              var obj = navigator.contacts.create({"displayName": "test name", "gender": "male", "note": "my note", "name": {"formatted": "Mr. Test Name"}, "emails": [{"value": "here@there.com"}, {"value": "there@here.com"}], "birthday": bDay});
+
+              expect(obj).toBeDefined();
+              expect(obj.displayName).toBe('test name');
+              expect(obj.note).toBe('my note');
+              expect(obj.name.formatted).toBe('Mr. Test Name');
+              expect(obj.emails.length).toBe(2);
+              expect(obj.emails[0].value).toBe('here@there.com');
+              expect(obj.emails[1].value).toBe('there@here.com');
+              expect(obj.nickname).toBe(null);
+              expect(obj.birthday).toBe(bDay);
+          });
+      });
+
+      describe("Contact object", function () {
+          it("contacts.spec.9 should be able to create instance", function() {
+              var contact = new Contact("a", "b", new ContactName("a", "b", "c", "d", "e", "f"), "c", [], [], [], [], [], "f", "i",
+                  [], [], []);
+              expect(contact).toBeDefined();
+              expect(contact.id).toBe("a");
+              expect(contact.displayName).toBe("b");
+              expect(contact.name.formatted).toBe("a");
+              expect(contact.nickname).toBe("c");
+              expect(contact.phoneNumbers).toBeDefined();
+              expect(contact.emails).toBeDefined();
+              expect(contact.addresses).toBeDefined();
+              expect(contact.ims).toBeDefined();
+              expect(contact.organizations).toBeDefined();
+              expect(contact.birthday).toBe("f");
+              expect(contact.note).toBe("i");
+              expect(contact.photos).toBeDefined();
+              expect(contact.categories).toBeDefined();
+              expect(contact.urls).toBeDefined();
+          });
+          it("contacts.spec.10 should be able to define a ContactName object", function() {
+              var contactName = new ContactName("Dr. First Last Jr.", "Last", "First", "Middle", "Dr.", "Jr.");
+              expect(contactName).toBeDefined();
+              expect(contactName.formatted).toBe("Dr. First Last Jr.");
+              expect(contactName.familyName).toBe("Last");
+              expect(contactName.givenName).toBe("First");
+              expect(contactName.middleName).toBe("Middle");
+              expect(contactName.honorificPrefix).toBe("Dr.");
+              expect(contactName.honorificSuffix).toBe("Jr.");
+          });
+          it("contacts.spec.11 should be able to define a ContactField object", function() {
+              var contactField = new ContactField("home", "8005551212", true);
+              expect(contactField).toBeDefined();
+              expect(contactField.type).toBe("home");
+              expect(contactField.value).toBe("8005551212");
+              expect(contactField.pref).toBe(true);
+          });
+          it("contacts.spec.12 ContactField object should coerce type and value properties to strings", function() {
+              var contactField = new ContactField(12345678, 12345678, true);
+              expect(contactField.type).toBe("12345678");
+              expect(contactField.value).toBe("12345678");
+          });
+          it("contacts.spec.13 should be able to define a ContactAddress object", function() {
+              var contactAddress = new ContactAddress(true, "home", "a","b","c","d","e","f");
+              expect(contactAddress).toBeDefined();
+              expect(contactAddress.pref).toBe(true);
+              expect(contactAddress.type).toBe("home");
+              expect(contactAddress.formatted).toBe("a");
+              expect(contactAddress.streetAddress).toBe("b");
+              expect(contactAddress.locality).toBe("c");
+              expect(contactAddress.region).toBe("d");
+              expect(contactAddress.postalCode).toBe("e");
+              expect(contactAddress.country).toBe("f");
+          });
+          it("contacts.spec.14 should be able to define a ContactOrganization object", function() {
+              var contactOrg = new ContactOrganization(true, "home", "a","b","c","d","e","f","g");
+              expect(contactOrg).toBeDefined();
+              expect(contactOrg.pref).toBe(true);
+              expect(contactOrg.type).toBe("home");
+              expect(contactOrg.name).toBe("a");
+              expect(contactOrg.department).toBe("b");
+              expect(contactOrg.title).toBe("c");
+          });
+          it("contacts.spec.15 should be able to define a ContactFindOptions object", function() {
+              var contactFindOptions = new ContactFindOptions("a", true, "b");
+              expect(contactFindOptions).toBeDefined();
+              expect(contactFindOptions.filter).toBe("a");
+              expect(contactFindOptions.multiple).toBe(true);
+          });
+          it("contacts.spec.16 should contain a clone function", function() {
+              var contact = new Contact();
+              expect(contact.clone).toBeDefined();
+              expect(typeof contact.clone).toBe('function');
+          });
+          it("contacts.spec.17 clone function should make deep copy of Contact Object", function() {
+              var contact = new Contact();
+              contact.id=1;
+              contact.displayName="Test Name";
+              contact.nickname="Testy";
+              contact.gender="male";
+              contact.note="note to be cloned";
+              contact.name = new ContactName("Mr. Test Name");
+
+              var clonedContact = contact.clone();
+
+              expect(contact.id).toBe(1);
+              expect(clonedContact.id).toBe(null);
+              expect(clonedContact.displayName).toBe(contact.displayName);
+              expect(clonedContact.nickname).toBe(contact.nickname);
+              expect(clonedContact.gender).toBe(contact.gender);
+              expect(clonedContact.note).toBe(contact.note);
+              expect(clonedContact.name.formatted).toBe(contact.name.formatted);
+              expect(clonedContact.connected).toBe(contact.connected);
+          });
+          it("contacts.spec.18 should contain a save function", function() {
+              var contact = new Contact();
+              expect(contact.save).toBeDefined();
+              expect(typeof contact.save).toBe('function');
+          });
+          it("contacts.spec.19 should contain a remove function", function() {
+              var contact = new Contact();
+              expect(contact.remove).toBeDefined();
+              expect(typeof contact.remove).toBe('function');
+          });
+      });
+      describe('save method', function () {
+          it("contacts.spec.20 should be able to save a contact", function (done) {
+              // Save method is not supported on Windows platform
+              if (isWindows || isWindowsPhone8) {
+                  pending();
+              }
+
+              var bDay = new Date(1976, 6,4);
+              gContactObj = navigator.contacts.create({"gender": "male", "note": "my note", "name": {"familyName": "Delete", "givenName": "Test"}, "emails": [{"value": "here@there.com"}, {"value": "there@here.com"}], "birthday": bDay});
+
+              var saveSuccess = function(obj) {
+                      expect(obj).toBeDefined();
+                      expect(obj.note).toBe('my note');
+                      expect(obj.name.familyName).toBe('Delete');
+                      expect(obj.name.givenName).toBe('Test');
+                      expect(obj.emails.length).toBe(2);
+                      expect(obj.emails[0].value).toBe('here@there.com');
+                      expect(obj.emails[1].value).toBe('there@here.com');
+                      expect(obj.birthday.toDateString()).toBe(bDay.toDateString());
+                      expect(obj.addresses).toBe(null);
+                      // must store returned object in order to have id for update test below
+                      gContactObj = obj;
+                      done();
+                  },
+                  saveFail = fail;
+
+              gContactObj.save(saveSuccess, saveFail);
+           });
+          // HACK: there is a reliance between the previous and next test. This is bad form.
+          it("contacts.spec.21 update a contact", function (done) {
+              // Save method is not supported on Windows platform
+              if (isWindows || isWindowsPhone8) {
+                  pending();
+              }
+
+              expect(gContactObj).toBeDefined();
+
+              var bDay = new Date(1975, 5,4);
+              var noteText = "an UPDATED note";
+
+              var win = function(obj) {
+                      expect(obj).toBeDefined();
+                      expect(obj.id).toBe(gContactObj.id);
+                      expect(obj.note).toBe(noteText);
+                      expect(obj.birthday.toDateString()).toBe(bDay.toDateString());
+                      expect(obj.emails.length).toBe(1);
+                      expect(obj.emails[0].value).toBe('here@there.com');
+                      removeContact();         // Clean up contact object
+                      done();
+                  }, fail = function() { removeContact(); fail(done); };
+
+              // remove an email
+              gContactObj.emails[1].value = "";
+              // change birthday
+              gContactObj.birthday = bDay;
+              // update note
+              gContactObj.note = noteText;
+              gContactObj.save(win, fail);
+          }, MEDIUM_TIMEOUT);
+      });
+      describe('Contact.remove method', function (done) {
+          afterEach(removeContact);
+
+          it("contacts.spec.22 calling remove on a contact has an id of null should return ContactError.UNKNOWN_ERROR", function(done) {
+              var win = function() {};
+              var fail = function(result) {
+                  expect(result.code).toBe(ContactError.UNKNOWN_ERROR);
+                  done();
+              };
+
+              var rmContact = new Contact();
+              rmContact.remove(win, fail);
+          });
+          it("contacts.spec.23 calling remove on a contact that does not exist should return ContactError.UNKNOWN_ERROR", function(done) {
+               // remove method is not supported on Windows platform
+              if (isWindows || isWindowsPhone8) {
+                  pending();
+              }
+              var rmWin = fail;
+              var rmFail = function(result) {
+                  expect(result.code).toBe(ContactError.UNKNOWN_ERROR);
+                  done();
+              };
+
+              var rmContact = new Contact();
+              // this is a bit risky as some devices may have contact ids that large
+              var contact = new Contact("this string is supposed to be a unique identifier that will never show up on a device");
+              contact.remove(rmWin, rmFail);
+          }, MEDIUM_TIMEOUT);
+      });
+      describe("Round trip Contact tests (creating + save + delete + find).", function () {
+          afterEach(removeContact);
+
+          it("contacts.spec.24 Creating, saving, finding a contact should work, removing it should work, after which we should not be able to find it, and we should not be able to delete it again.", function (done) {
+              // Save method is not supported on Windows platform
+              if (isWindows || isWindowsPhone8) {
+                  pending();
+              }
+              // First, count already existing 'DeleteMe' contacts, if any
+              var initialCount = 0;
+              var initialSearchOptions = new ContactFindOptions();
+              initialSearchOptions.filter = "DeleteMe";
+              initialSearchOptions.multiple = true;
+              navigator.contacts.find(["displayName", "name", "phoneNumbers", "emails"], function (initialContacts) {
+                  initialCount = initialContacts.length;
+                  gContactObj = new Contact();
+                  gContactObj.name = new ContactName();
+                  gContactObj.name.familyName = "DeleteMe";
+                  gContactObj.save(function(c_obj) {
+                      var findWin = function(cs) {
+                          expect(cs.length).toBe(initialCount + 1);
+                          // update to have proper saved id
+                          gContactObj = cs[0];
+                          gContactObj.remove(function() {
+                              var findWinAgain = function(seas) {
+                                  expect(seas.length).toBe(initialCount);
+                                  gContactObj.remove(function() {
+                                      throw("success callback called after non-existent Contact object called remove(). Test failed.");
+                                  }, function(e) {
+                                      expect(e.code).toBe(ContactError.UNKNOWN_ERROR);
+                                      done();
+                                  });
+                              };
+                              var findFailAgain = function(e) {
+                                  throw("find error callback invoked after delete, test failed.");
+                              };
+                              var obj = new ContactFindOptions();
+                              obj.filter="DeleteMe";
+                              obj.multiple=true;
+                              navigator.contacts.find(["displayName", "name", "phoneNumbers", "emails"], findWinAgain, findFailAgain, obj);
+                          }, function(e) {
+                              throw("Newly created contact's remove function invoked error callback. Test failed.");
+                          });
+                      };
+                      var findFail = fail;
+                      var obj = new ContactFindOptions();
+                      obj.filter="DeleteMe";
+                      obj.multiple=true;
+                      navigator.contacts.find(["displayName", "name", "phoneNumbers", "emails"], findWin, findFail, obj);
+                  }, fail);
+              }, function () {}, initialSearchOptions);
+          }, MEDIUM_TIMEOUT);
+      });
+      describe('ContactError interface', function () {
+          it("contacts.spec.25 ContactError constants should be defined", function() {
+              expect(ContactError.UNKNOWN_ERROR).toBe(0);
+              expect(ContactError.INVALID_ARGUMENT_ERROR).toBe(1);
+              expect(ContactError.TIMEOUT_ERROR).toBe(2);
+              expect(ContactError.PENDING_OPERATION_ERROR).toBe(3);
+              expect(ContactError.IO_ERROR).toBe(4);
+              expect(ContactError.NOT_SUPPORTED_ERROR).toBe(5);
+              expect(ContactError.PERMISSION_DENIED_ERROR).toBe(20);
+          });
+      });
+  });
+};
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    function getContacts() {
+        var results = document.getElementById('contact_results');
+        obj = new ContactFindOptions();
+        // show all contacts, so don't filter
+        obj.multiple = true;
+        navigator.contacts.find(
+            ["displayName", "name", "phoneNumbers", "emails", "urls", "note"],
+            function (contacts) {
+                var s = "";
+                if (contacts.length == 0) {
+                    s = "No contacts found";
+                }
+                else {
+                    s = "Number of contacts: " + contacts.length + "<br><table width='100%'><tr><th>Name</th><td>Phone</td><td>Email</td></tr>";
+                    for (var i = 0; i < contacts.length; i++) {
+                        var contact = contacts[i];
+                        s = s + "<tr><td>" + contact.name.formatted + "</td><td>";
+                        if (contact.phoneNumbers && contact.phoneNumbers.length > 0) {
+                            s = s + contact.phoneNumbers[0].value;
+                        }
+                        s = s + "</td><td>"
+                        if (contact.emails && contact.emails.length > 0) {
+                            s = s + contact.emails[0].value;
+                        }
+                        s = s + "</td></tr>";
+                    }
+                    s = s + "</table>";
+                }
+
+                results.innerHTML = s;
+            },
+            function (e) {
+                if (e.code === ContactError.NOT_SUPPORTED_ERROR) {
+                    results.innerHTML = "Searching for contacts is not supported.";
+                } else {
+                    results.innerHTML = "Search failed: error " + e.code;
+                }
+            },
+            obj);
+    }
+
+    function addContact() {
+        var results = document.getElementById('contact_results');
+
+        try {
+            var contact = navigator.contacts.create({ "displayName": "Dooney Evans" });
+            var contactName = {
+                formatted: "Dooney Evans",
+                familyName: "Evans",
+                givenName: "Dooney",
+                middleName: ""
+            };
+
+            contact.name = contactName;
+
+            var phoneNumbers = [1];
+            phoneNumbers[0] = new ContactField('work', '512-555-1234', true);
+            contact.phoneNumbers = phoneNumbers;
+
+            contact.save(
+                function () { results.innerHTML = "Contact saved."; },
+                function (e) {
+                    if (e.code === ContactError.NOT_SUPPORTED_ERROR) {
+                        results.innerHTML = "Saving contacts not supported.";
+                    } else {
+                        results.innerHTML = "Contact save failed: error " + e.code;
+                    }
+                }
+            );
+        }
+        catch (e) {
+            alert(e);
+        }
+    }
+    
+    function removeDooneyEvans() {
+        var results = document.getElementById('contact_results');
+        
+        navigator.contacts.find(["displayName", "name", "phoneNumbers", "emails", "urls", "note"], function(contacts) {
+            var removes = [];
+            contacts.forEach(function(contact) {
+                if (contact.name.formatted.indexOf('Dooney Evans') > -1) {
+                    removes.push(contact);
+                }
+            });
+            
+            var nextToRemove = undefined;
+            if (removes.length > 0) {
+              nextToRemove = removes.shift();
+            }
+            function removeNext(item) {
+                if (typeof item === 'undefined')
+                    return;
+                
+                if (removes.length > 0) {
+                    nextToRemove = removes.shift();
+                } else {
+                  nextToRemove = undefined;
+                }
+                
+                item.remove(function removeSucceeded() {
+                    results.innerHTML += '<br>Removed contact with ID ' + item.id;
+                    removeNext(nextToRemove);
+                }, function removeFailed(e) {
+                    results.innerHTML += '<br>Remove failed contact with ID ' + item.id;
+                    removeNext(nextToRemove);
+                });
+            }
+            removeNext(nextToRemove);
+        }, function (e) {
+            if (e.code === ContactError.NOT_SUPPORTED_ERROR) {
+                results.innerHTML = 'Searching for contacts is not supported.';
+            }
+            else {
+                results.innerHTML = 'Search failed: error ' + e.code;
+            }
+        })
+    }
+
+    /******************************************************************************/
+
+    contentEl.innerHTML = '<div id="info">' +
+        '<b>Results:</b><br>' +
+        '<div id="contact_results"></div>' +
+        '</div>' +
+        '<div id="get_contacts"></div>' +
+        'Expected result: Status box will show number of contacts and list them. May be empty on a fresh device until you click Add.' +
+        '</p> <div id="add_contact"></div>' +
+        'Expected result: Will add a new contact. Log will say "Contact saved." or "Saving contacts not supported." if not supported on current platform. Verify by running Get phone contacts again' +
+        '<div id="remove_dooney_evans"></div>' + 
+        '<p>Expected result: Will remove any contacts named "Dooney Evans".  Log will output success or failure, plus ID, or fail like getting contacts will fail.</p>';
+
+    createActionButton("Get phone's contacts", function () {
+        getContacts();
+    }, 'get_contacts');
+
+    createActionButton("Add a new contact 'Dooney Evans'", function () {
+        addContact();
+    }, 'add_contact');
+    
+    createActionButton("Delete all 'Dooney Evans'", function() {
+        removeDooneyEvans();
+    }, 'remove_dooney_evans');
+};
diff --git a/test/unittest/tests/datauri.tests.js b/test/unittest/tests/datauri.tests.js
new file mode 100644 (file)
index 0000000..51623ca
--- /dev/null
@@ -0,0 +1,40 @@
+exports.defineAutoTests = function () {
+describe('data uris', function () {
+    it("datauri.spec.1 should work with iframes", function() {
+        var gotFoo = false,
+            frame = document.createElement('iframe');
+        function onMessage(msg) {
+            gotFoo = gotFoo || msg.data == 'foo';
+        };
+
+        this.after(function() {
+            document.body.removeChild(frame);
+            window.removeEventListener('message', onMessage, false);
+        });
+
+        window.addEventListener('message', onMessage, false);
+        frame.src = 'data:text/html;charset=utf-8,%3Chtml%3E%3Cscript%3Eparent.postMessage%28%27foo%27%2C%27%2A%27%29%3C%2Fscript%3E%3C%2Fhtml%3E'
+        document.body.appendChild(frame);
+        waitsFor(function() {
+            return gotFoo;
+        }, 'iframe did not load.', 1000);
+        runs(function() {
+            expect(gotFoo).toBe(true);
+        });
+    });
+    it("datauri.spec.2 should work with images", function() {
+        var img = new Image();
+        img.onload = jasmine.createSpy('onLoad');
+        img.onerror = jasmine.createSpy('onError');
+        img.src = 'data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7'
+        waitsFor(function() {
+            return img.onload.wasCalled || img.onerror.wasCalled;
+        }, 'image did not load or error', 1000);
+        runs(function() {
+            expect(img.onload).toHaveBeenCalled();
+        });
+    });
+});
+};
+
+exports.defineManualTests = function (contentEl, createActionButton) {};
diff --git a/test/unittest/tests/device.tests.js b/test/unittest/tests/device.tests.js
new file mode 100644 (file)
index 0000000..21e4160
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+exports.defineAutoTests = function() {
+  describe('Device Information (window.device)', function () {
+    it("should exist", function() {
+      expect(window.device).toBeDefined();
+    });
+
+    it("should contain a platform specification that is a string", function() {
+      expect(window.device.platform).toBeDefined();
+      expect((new String(window.device.platform)).length > 0).toBe(true);
+    });
+
+    it("should contain a version specification that is a string", function() {
+      expect(window.device.version).toBeDefined();
+      expect((new String(window.device.version)).length > 0).toBe(true);
+    });
+
+    it("should contain a UUID specification that is a string or a number", function() {
+      expect(window.device.uuid).toBeDefined();
+      if (typeof window.device.uuid == 'string' || typeof window.device.uuid == 'object') {
+        expect((new String(window.device.uuid)).length > 0).toBe(true);
+      } else {
+        expect(window.device.uuid > 0).toBe(true);
+      }
+    });
+
+    it("should contain a cordova specification that is a string", function() {
+      expect(window.device.cordova).toBeDefined();
+      expect((new String(window.device.cordova)).length > 0).toBe(true);
+    });
+
+    it("should depend on the presence of cordova.version string", function() {
+      expect(window.cordova.version).toBeDefined();
+      expect((new String(window.cordova.version)).length > 0).toBe(true);
+    });
+
+    it("should contain device.cordova equal to cordova.version", function() {
+      expect(window.device.cordova).toBe(window.cordova.version);
+    });
+
+    it("should contain a model specification that is a string", function() {
+      expect(window.device.model).toBeDefined();
+      expect((new String(window.device.model)).length > 0).toBe(true);
+    });
+
+    it("should contain a manufacturer property that is a string", function() {
+      expect(window.device.manufacturer).toBeDefined();
+      expect((new String(window.device.manufacturer)).length > 0).toBe(true);
+    });
+
+    it("should contain an isVirtual property that is a boolean", function() {
+      expect(window.device.isVirtual).toBeDefined();
+      expect(typeof window.device.isVirtual).toBe("boolean");
+    });
+
+    it("should contain a serial number specification that is a string", function() {
+      expect(window.device.serial).toBeDefined();
+      expect((new String(window.device.serial)).length > 0).toBe(true);
+
+    });
+
+  });
+};
+
+exports.defineManualTests = function(contentEl, createActionButton) {
+  var logMessage = function (message, color) {
+        var log = document.getElementById('info');
+        var logLine = document.createElement('div');
+        if (color) {
+            logLine.style.color = color;
+        }
+        logLine.innerHTML = message;
+        log.appendChild(logLine);
+    }
+
+    var clearLog = function () {
+        var log = document.getElementById('info');
+        log.innerHTML = '';
+    }
+
+    var device_tests = '<h3>Press Dump Device button to get device information</h3>' +
+        '<div id="dump_device"></div>' +
+        'Expected result: Status box will get updated with device info. (i.e. platform, version, uuid, model, etc)';
+
+    contentEl.innerHTML = '<div id="info"></div>' + device_tests;
+
+    createActionButton('Dump device', function() {
+      clearLog();
+      logMessage(JSON.stringify(window.device, null, '\t'));
+    }, "dump_device");
+};
diff --git a/test/unittest/tests/file.tests.js b/test/unittest/tests/file.tests.js
new file mode 100644 (file)
index 0000000..0cf7690
--- /dev/null
@@ -0,0 +1,3689 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ *
+ */
+
+/*global cordova, exports*/
+/*jshint jasmine: true*/
+/*global FileError, LocalFileSystem, Metadata, Flags*/
+
+exports.defineAutoTests = function () {
+    var isBrowser = (cordova.platformId === "browser");
+    // Use feature detection to determine current browser instead of checking user-agent
+    var isChrome = isBrowser && window.webkitRequestFileSystem && window.webkitResolveLocalFileSystemURL;
+    var isIE = isBrowser && (window.msIndexedDB);
+    var isIndexedDBShim = isBrowser && !isChrome;   // Firefox and IE for example
+
+    var isWindows = (cordova.platformId === "windows" || cordova.platformId === "windows8");
+    
+    var MEDIUM_TIMEOUT = 15000;
+
+    describe('File API', function () {
+        // Adding a Jasmine helper matcher, to report errors when comparing to FileError better.
+        var fileErrorMap = {
+            1 : 'NOT_FOUND_ERR',
+            2 : 'SECURITY_ERR',
+            3 : 'ABORT_ERR',
+            4 : 'NOT_READABLE_ERR',
+            5 : 'ENCODING_ERR',
+            6 : 'NO_MODIFICATION_ALLOWED_ERR',
+            7 : 'INVALID_STATE_ERR',
+            8 : 'SYNTAX_ERR',
+            9 : 'INVALID_MODIFICATION_ERR',
+            10 : 'QUOTA_EXCEEDED_ERR',
+            11 : 'TYPE_MISMATCH_ERR',
+            12 : 'PATH_EXISTS_ERR'
+        },
+        root,
+        temp_root,
+        persistent_root;
+        beforeEach(function (done) {
+            // Custom Matchers
+            jasmine.Expectation.addMatchers({
+                toBeFileError : function () {
+                    return {
+                        compare : function (error, code) {
+                            var pass = error.code === code;
+                            return {
+                                pass : pass,
+                                message : 'Expected FileError with code ' + fileErrorMap[error.code] + ' (' + error.code + ') to be ' + fileErrorMap[code] + '(' + code + ')'
+                            };
+                        }
+                    };
+                },
+                toCanonicallyMatch : function () {
+                    return {
+                        compare : function (currentPath, path) {
+                            var a = path.split("/").join("").split("\\").join(""),
+                            b = currentPath.split("/").join("").split("\\").join(""),
+                            pass = a === b;
+                            return {
+                                pass : pass,
+                                message : 'Expected paths to match : ' + path + ' should be ' + currentPath
+                            };
+                        }
+                    };
+                },
+                toFailWithMessage : function () {
+                    return {
+                        compare : function (error, message) {
+                            var pass = false;
+                            return {
+                                pass : pass,
+                                message : message
+                            };
+                        }
+                    };
+                },
+                toBeDataUrl: function () {
+                    return {
+                        compare : function (url) {
+                            var pass = false;
+                            // "data:application/octet-stream;base64,"
+                            var header = url.substr(0, url.indexOf(','));
+                            var headerParts = header.split(/[:;]/);
+                            if (headerParts.length === 3 &&
+                                headerParts[0] === 'data' &&
+                                headerParts[2] === 'base64') {
+                                pass = true;
+                            }
+                            var message = 'Expected ' + url + ' to be a valid data url. ' + header + ' is not valid header for data uris';
+                            return {
+                                pass : pass,
+                                message : message
+                            };
+                        }
+                    };
+                }
+            });
+            //Define global variables
+            var onError = function (e) {
+                console.log('[ERROR] Problem setting up root filesystem for test running! Error to follow.');
+                console.log(JSON.stringify(e));
+            };
+            window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function (fileSystem) {
+                root = fileSystem.root;
+                // set in file.tests.js
+                persistent_root = root;
+                window.requestFileSystem(LocalFileSystem.TEMPORARY, 0, function (fileSystem) {
+                    temp_root = fileSystem.root;
+                    // set in file.tests.js
+                    done();
+                }, onError);
+            }, onError);
+        });
+        // HELPER FUNCTIONS
+        // deletes specified file or directory
+        var deleteEntry = function (name, success, error) {
+            // deletes entry, if it exists
+            // entry.remove success callback is required: http://www.w3.org/TR/2011/WD-file-system-api-20110419/#the-entry-interface
+            success = success || function() {};
+            error = error || failed.bind(null, success, 'deleteEntry failed.');
+
+            window.resolveLocalFileSystemURL(root.toURL() + '/' + name, function (entry) {
+                if (entry.isDirectory === true) {
+                    entry.removeRecursively(success, error);
+                } else {
+                    entry.remove(success, error);
+                }
+            }, success);
+        };
+        // deletes file, if it exists, then invokes callback
+        var deleteFile = function (fileName, callback) {
+            // entry.remove success callback is required: http://www.w3.org/TR/2011/WD-file-system-api-20110419/#the-entry-interface
+            callback = callback || function() {};
+
+            root.getFile(fileName, null, // remove file system entry
+                function (entry) {
+                entry.remove(callback, function () {
+                    console.log('[ERROR] deleteFile cleanup method invoked fail callback.');
+                });
+            }, // doesn't exist
+                callback);
+        };
+        // deletes and re-creates the specified file
+        var createFile = function (fileName, success, error) {
+            deleteEntry(fileName, function () {
+                root.getFile(fileName, {
+                    create : true
+                }, success, error);
+            }, error);
+        };
+        // deletes and re-creates the specified directory
+        var createDirectory = function (dirName, success, error) {
+            deleteEntry(dirName, function () {
+                root.getDirectory(dirName, {
+                    create : true
+                }, success, error);
+            }, error);
+        };
+        var failed = function (done, msg, error) {
+            var info = typeof msg == 'undefined' ? 'Unexpected error callback' : msg;
+            var codeMsg = (error && error.code) ? (': ' + fileErrorMap[error.code]) : '';
+            expect(true).toFailWithMessage(info + '\n' + JSON.stringify(error) + codeMsg);
+            done();
+        };
+        var succeed = function (done, msg) {
+            var info = typeof msg == 'undefined' ? 'Unexpected success callback' : msg;
+            expect(true).toFailWithMessage(info);
+            done();
+        };
+        var joinURL = function (base, extension) {
+            if (base.charAt(base.length - 1) !== '/' && extension.charAt(0) !== '/') {
+                return base + '/' + extension;
+            }
+            if (base.charAt(base.length - 1) === '/' && extension.charAt(0) === '/') {
+                return base + extension.substring(1);
+            }
+            return base + extension;
+        };
+        describe('FileError object', function () {
+            it("file.spec.1 should define FileError constants", function () {
+                expect(FileError.NOT_FOUND_ERR).toBe(1);
+                expect(FileError.SECURITY_ERR).toBe(2);
+                expect(FileError.ABORT_ERR).toBe(3);
+                expect(FileError.NOT_READABLE_ERR).toBe(4);
+                expect(FileError.ENCODING_ERR).toBe(5);
+                expect(FileError.NO_MODIFICATION_ALLOWED_ERR).toBe(6);
+                expect(FileError.INVALID_STATE_ERR).toBe(7);
+                expect(FileError.SYNTAX_ERR).toBe(8);
+                expect(FileError.INVALID_MODIFICATION_ERR).toBe(9);
+                expect(FileError.QUOTA_EXCEEDED_ERR).toBe(10);
+                expect(FileError.TYPE_MISMATCH_ERR).toBe(11);
+                expect(FileError.PATH_EXISTS_ERR).toBe(12);
+            });
+        });
+        describe('LocalFileSystem', function () {
+            it("file.spec.2 should define LocalFileSystem constants", function () {
+                expect(LocalFileSystem.TEMPORARY).toBe(0);
+                expect(LocalFileSystem.PERSISTENT).toBe(1);
+            });
+            describe('window.requestFileSystem', function () {
+                it("file.spec.3 should be defined", function () {
+                    expect(window.requestFileSystem).toBeDefined();
+                });
+                it("file.spec.4 should be able to retrieve a PERSISTENT file system", function (done) {
+                    var win = function (fileSystem) {
+                        expect(fileSystem).toBeDefined();
+                        expect(fileSystem.name).toBeDefined();
+                        isChrome ? expect(fileSystem.name).toContain("Persistent")
+                            : expect(fileSystem.name).toBe("persistent");
+                        expect(fileSystem.root).toBeDefined();
+                        expect(fileSystem.root.filesystem).toBeDefined();
+                        // Shouldn't use cdvfile by default.
+                        expect(fileSystem.root.toURL()).not.toMatch(/^cdvfile:/);
+                        // All DirectoryEntry URLs should always have a trailing slash.
+                        expect(fileSystem.root.toURL()).toMatch(/\/$/);
+                        done();
+                    };
+                    // retrieve PERSISTENT file system
+                    window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, win, failed.bind(null, done, 'window.requestFileSystem - Error retrieving PERSISTENT file system'));
+                });
+                it("file.spec.5 should be able to retrieve a TEMPORARY file system", function (done) {
+                    var win = function (fileSystem) {
+                        expect(fileSystem).toBeDefined();
+                        isChrome ? expect(fileSystem.name).toContain("Temporary")
+                            : expect(fileSystem.name).toBe("temporary");
+                        expect(fileSystem.root).toBeDefined();
+                        expect(fileSystem.root.filesystem).toBeDefined();
+                        expect(fileSystem.root.filesystem).toBe(fileSystem);
+                        done();
+                    };
+                    //retrieve TEMPORARY file system
+                    window.requestFileSystem(LocalFileSystem.TEMPORARY, 0, win, failed.bind(null, done, 'window.requestFileSystem - Error retrieving TEMPORARY file system'));
+                });
+                it("file.spec.6 should error if you request a file system that is too large", function (done) {
+                    if (isBrowser) {
+                        /*window.requestFileSystem TEMPORARY and PERSISTENT filesystem quota is not limited in Chrome.
+                        Firefox filesystem size is not limited but every 50MB request user permission.
+                        IE10 allows up to 10mb of combined AppCache and IndexedDB used in implementation
+                        of filesystem without prompting, once you hit that level you will be asked if you
+                        want to allow it to be increased up to a max of 250mb per site.
+                        So `size` parameter for `requestFileSystem` function does not affect on filesystem in Firefox and IE.*/
+                        pending();
+                    }
+
+                    var fail = function (error) {
+                        expect(error).toBeDefined();
+                        expect(error).toBeFileError(FileError.QUOTA_EXCEEDED_ERR);
+                        done();
+                    };
+                    //win = createWin('window.requestFileSystem');
+                    // Request the file system
+                    window.requestFileSystem(LocalFileSystem.TEMPORARY, 1000000000000000, failed.bind(null, done, 'window.requestFileSystem - Error retrieving TEMPORARY file system'), fail);
+                });
+                it("file.spec.7 should error out if you request a file system that does not exist", function (done) {
+
+                    var fail = function (error) {
+                        expect(error).toBeDefined();
+                        if (isChrome) {
+                            /*INVALID_MODIFICATION_ERR (code: 9) is thrown instead of SYNTAX_ERR(code: 8)
+                            on requesting of a non-existant filesystem.*/
+                            expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                        } else {
+                            expect(error).toBeFileError(FileError.SYNTAX_ERR);
+                        }
+                        done();
+                    };
+                    // Request the file system
+                    window.requestFileSystem(-1, 0, succeed.bind(null, done, 'window.requestFileSystem'), fail);
+                });
+            });
+            describe('window.resolveLocalFileSystemURL', function () {
+                it("file.spec.8 should be defined", function () {
+                    expect(window.resolveLocalFileSystemURL).toBeDefined();
+                });
+                it("file.spec.9 should resolve a valid file name", function (done) {
+                    var fileName = 'file.spec.9';
+                    var win = function (fileEntry) {
+                        expect(fileEntry).toBeDefined();
+                        expect(fileEntry.name).toCanonicallyMatch(fileName);
+                        expect(fileEntry.toURL()).not.toMatch(/^cdvfile:/, 'should not use cdvfile URL');
+                        expect(fileEntry.toURL()).not.toMatch(/\/$/, 'URL should not end with a slash');
+                        // Clean-up
+                        deleteEntry(fileName, done);
+                    };
+                    createFile(fileName, function (entry) {
+                        window.resolveLocalFileSystemURL(entry.toURL(), win, failed.bind(null, done, 'window.resolveLocalFileSystemURL - Error resolving file URL: ' + entry.toURL()));
+                    }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName), failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+                });
+                it("file.spec.9.5 should resolve a directory", function (done) {
+                    var fileName = 'file.spec.9.5';
+                    var win = function (fileEntry) {
+                        expect(fileEntry).toBeDefined();
+                        expect(fileEntry.name).toCanonicallyMatch(fileName);
+                        expect(fileEntry.toURL()).not.toMatch(/^cdvfile:/, 'should not use cdvfile URL');
+                        expect(fileEntry.toURL()).toMatch(/\/$/, 'URL end with a slash');
+                        // cleanup
+                        deleteEntry(fileName, done);
+                    };
+                    function gotDirectory(entry) {
+                        // lookup file system entry
+                        window.resolveLocalFileSystemURL(entry.toURL(), win, failed.bind(null, done, 'window.resolveLocalFileSystemURL - Error resolving directory URL: ' + entry.toURL()));
+                    }
+                    createDirectory(fileName, gotDirectory, failed.bind(null, done, 'createDirectory - Error creating directory: ' + fileName), failed.bind(null, done, 'createDirectory - Error creating directory: ' + fileName));
+                });
+                it("file.spec.10 resolve valid file name with parameters", function (done) {
+                    var fileName = "resolve.file.uri.params",
+                    win = function (fileEntry) {
+                        expect(fileEntry).toBeDefined();
+                        if (fileEntry.toURL().toLowerCase().substring(0, 10) === "cdvfile://") {
+                            expect(fileEntry.fullPath).toBe("/" + fileName + "?1234567890");
+                        }
+                        expect(fileEntry.name).toBe(fileName);
+                        // cleanup
+                        deleteEntry(fileName, done);
+                    };
+                    // create a new file entry
+                    createFile(fileName, function (entry) {
+                        window.resolveLocalFileSystemURL(entry.toURL() + "?1234567890", win, failed.bind(null, done, 'window.resolveLocalFileSystemURL - Error resolving file URI: ' + entry.toURL()));
+                    }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+                });
+                it("file.spec.11 should error (NOT_FOUND_ERR) when resolving (non-existent) invalid file name", function (done) {
+                    var fileName = cordova.platformId === 'windowsphone' ? root.toURL() + "/" + "this.is.not.a.valid.file.txt" : joinURL(root.toURL(), "this.is.not.a.valid.file.txt");
+                    fail = function (error) {
+                        expect(error).toBeDefined();
+                        expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                        done();
+                    };
+                    // lookup file system entry
+                    window.resolveLocalFileSystemURL(fileName, succeed.bind(null, done, 'window.resolveLocalFileSystemURL - Error unexpected callback resolving file URI: ' + fileName), fail);
+                });
+                it("file.spec.12 should error (ENCODING_ERR) when resolving invalid URI with leading /", function (done) {
+                    var fileName = "/this.is.not.a.valid.url",
+                    fail = function (error) {
+                        expect(error).toBeDefined();
+                        expect(error).toBeFileError(FileError.ENCODING_ERR);
+                        done();
+                    };
+                    // lookup file system entry
+                    window.resolveLocalFileSystemURL(fileName, succeed.bind(null, done, 'window.resolveLocalFileSystemURL - Error unexpected callback resolving file URI: ' + fileName), fail);
+                });
+            });
+        });
+        //LocalFileSystem
+        describe('Metadata interface', function () {
+            it("file.spec.13 should exist and have the right properties", function () {
+                var metadata = new Metadata();
+                expect(metadata).toBeDefined();
+                expect(metadata.modificationTime).toBeDefined();
+            });
+        });
+        describe('Flags interface', function () {
+            it("file.spec.14 should exist and have the right properties", function () {
+                var flags = new Flags(false, true);
+                expect(flags).toBeDefined();
+                expect(flags.create).toBeDefined();
+                expect(flags.create).toBe(false);
+                expect(flags.exclusive).toBeDefined();
+                expect(flags.exclusive).toBe(true);
+            });
+        });
+        describe('FileSystem interface', function () {
+            it("file.spec.15 should have a root that is a DirectoryEntry", function (done) {
+                var win = function (entry) {
+                    expect(entry).toBeDefined();
+                    expect(entry.isFile).toBe(false);
+                    expect(entry.isDirectory).toBe(true);
+                    expect(entry.name).toBeDefined();
+                    expect(entry.fullPath).toBeDefined();
+                    expect(entry.getMetadata).toBeDefined();
+                    expect(entry.moveTo).toBeDefined();
+                    expect(entry.copyTo).toBeDefined();
+                    expect(entry.toURL).toBeDefined();
+                    expect(entry.remove).toBeDefined();
+                    expect(entry.getParent).toBeDefined();
+                    expect(entry.createReader).toBeDefined();
+                    expect(entry.getFile).toBeDefined();
+                    expect(entry.getDirectory).toBeDefined();
+                    expect(entry.removeRecursively).toBeDefined();
+                    done();
+                };
+                window.resolveLocalFileSystemURL(root.toURL(), win, failed.bind(null, done, 'window.resolveLocalFileSystemURL - Error resolving file URI: ' + root.toURL()));
+            });
+        });
+        describe('DirectoryEntry', function () {
+            it("file.spec.16 getFile: get Entry for file that does not exist", function (done) {
+                var fileName = "de.no.file",
+                filePath = joinURL(root.fullPath, fileName),
+                fail = function (error) {
+                    expect(error).toBeDefined();
+                    expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                    done();
+                };
+                // create:false, exclusive:false, file does not exist
+                root.getFile(fileName, {
+                    create : false
+                }, succeed.bind(null, done, 'root.getFile - Error unexpected callback, file should not exists: ' + fileName), fail);
+            });
+            it("file.spec.17 getFile: create new file", function (done) {
+                var fileName = "de.create.file",
+                filePath = joinURL(root.fullPath, fileName),
+                win = function (entry) {
+                    expect(entry).toBeDefined();
+                    expect(entry.isFile).toBe(true);
+                    expect(entry.isDirectory).toBe(false);
+                    expect(entry.name).toCanonicallyMatch(fileName);
+                    expect(entry.fullPath).toCanonicallyMatch(filePath);
+                    // cleanup
+                    deleteEntry(entry.name, done);
+                };
+                // create:true, exclusive:false, file does not exist
+                root.getFile(fileName, {
+                    create : true
+                }, win, succeed.bind(null, done, 'root.getFile - Error unexpected callback, file should not exists: ' + fileName));
+            });
+            it("file.spec.18 getFile: create new file (exclusive)", function (done) {
+                var fileName = "de.create.exclusive.file",
+                filePath = joinURL(root.fullPath, fileName),
+                win = function (entry) {
+                    expect(entry).toBeDefined();
+                    expect(entry.isFile).toBe(true);
+                    expect(entry.isDirectory).toBe(false);
+                    expect(entry.name).toBe(fileName);
+                    expect(entry.fullPath).toCanonicallyMatch(filePath);
+                    // cleanup
+                    deleteEntry(entry.name, done);
+                };
+                // create:true, exclusive:true, file does not exist
+                root.getFile(fileName, {
+                    create : true,
+                    exclusive : true
+                }, win, failed.bind(null, done, 'root.getFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.19 getFile: create file that already exists", function (done) {
+                var fileName = "de.create.existing.file",
+                filePath = joinURL(root.fullPath, fileName),
+                getFile = function (file) {
+                    // create:true, exclusive:false, file exists
+                    root.getFile(fileName, {
+                        create : true
+                    }, win, failed.bind(null, done, 'root.getFile - Error creating file: ' + fileName));
+                },
+                win = function (entry) {
+                    expect(entry).toBeDefined();
+                    expect(entry.isFile).toBe(true);
+                    expect(entry.isDirectory).toBe(false);
+                    expect(entry.name).toCanonicallyMatch(fileName);
+                    expect(entry.fullPath).toCanonicallyMatch(filePath);
+                    // cleanup
+                    deleteEntry(entry.name, done);
+                };
+                // create file to kick off it
+                root.getFile(fileName, {
+                    create : true
+                }, getFile, failed.bind(null, done, 'root.getFile - Error on initial creating file: ' + fileName));
+            });
+            it("file.spec.20 getFile: create file that already exists (exclusive)", function (done) {
+
+                var fileName = "de.create.exclusive.existing.file",
+                filePath = joinURL(root.fullPath, fileName),
+                existingFile,
+                getFile = function (file) {
+                    existingFile = file;
+                    // create:true, exclusive:true, file exists
+                    root.getFile(fileName, {
+                        create : true,
+                        exclusive : true
+                    }, succeed.bind(null, done, 'root.getFile - getFile function - Error unexpected callback, file should exists: ' + fileName), fail);
+                },
+                fail = function (error) {
+                    expect(error).toBeDefined();
+                    if (isChrome) {
+                        /*INVALID_MODIFICATION_ERR (code: 9) is thrown instead of PATH_EXISTS_ERR(code: 12)
+                        on trying to exclusively create a file, which already exists in Chrome.*/
+                        expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                    } else {
+                        expect(error).toBeFileError(FileError.PATH_EXISTS_ERR);
+                    }
+                    // cleanup
+                    deleteEntry(existingFile.name, done);
+                };
+                // create file to kick off it
+                root.getFile(fileName, {
+                    create : true
+                }, getFile, failed.bind(null, done, 'root.getFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.21 DirectoryEntry.getFile: get Entry for existing file", function (done) {
+                var fileName = "de.get.file",
+                filePath = joinURL(root.fullPath, fileName),
+                win = function (entry) {
+                    expect(entry).toBeDefined();
+                    expect(entry.isFile).toBe(true);
+                    expect(entry.isDirectory).toBe(false);
+                    expect(entry.name).toCanonicallyMatch(fileName);
+                    expect(entry.fullPath).toCanonicallyMatch(filePath);
+                    expect(entry.filesystem).toBeDefined();
+                    expect(entry.filesystem).toBe(root.filesystem);
+                    //clean up
+                    deleteEntry(entry.name, done);
+                },
+                getFile = function (file) {
+                    // create:false, exclusive:false, file exists
+                    root.getFile(fileName, {
+                        create : false
+                    }, win, failed.bind(null, done, 'root.getFile - Error getting file entry: ' + fileName));
+                };
+                // create file to kick off it
+                root.getFile(fileName, {
+                    create : true
+                }, getFile, failed.bind(null, done, 'root.getFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.22 DirectoryEntry.getFile: get FileEntry for invalid path", function (done) {
+                if (isBrowser) {
+                    /*The plugin does not follow to ["8.3 Naming restrictions"]
+                    (http://www.w3.org/TR/2011/WD-file-system-api-20110419/#naming-restrictions).*/
+                    pending();
+                }
+
+                var fileName = "de:invalid:path",
+                fail = function (error) {
+                    console.error(error);
+                    expect(error).toBeDefined();
+                    expect(error).toBeFileError(FileError.ENCODING_ERR);
+                    done();
+                };
+                // create:false, exclusive:false, invalid path
+                root.getFile(fileName, {
+                    create : false
+                }, succeed.bind(null, done, 'root.getFile - Error unexpected callback, file should not exists: ' + fileName), fail);
+            });
+            it("file.spec.23 DirectoryEntry.getDirectory: get Entry for directory that does not exist", function (done) {
+                var dirName = "de.no.dir",
+                dirPath = joinURL(root.fullPath, dirName),
+                fail = function (error) {
+                    expect(error).toBeDefined();
+                    expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                    done();
+                };
+                // create:false, exclusive:false, directory does not exist
+                root.getDirectory(dirName, {
+                    create : false
+                }, succeed.bind(null, done, 'root.getDirectory - Error unexpected callback, directory should not exists: ' + dirName), fail);
+            });
+            it("file.spec.24 DirectoryEntry.getDirectory: create new dir with space then resolveLocalFileSystemURL", function (done) {
+                var dirName = "de create dir",
+                dirPath = joinURL(root.fullPath, encodeURIComponent(dirName)),
+                getDir = function (dirEntry) {
+                    expect(dirEntry.filesystem).toBeDefined();
+                    expect(dirEntry.filesystem).toBe(root.filesystem);
+                    var dirURI = dirEntry.toURL();
+                    // now encode URI and try to resolve
+                    window.resolveLocalFileSystemURL(dirURI, win, failed.bind(null, done, 'window.resolveLocalFileSystemURL - getDir function - Error resolving directory: ' + dirURI));
+                },
+                win = function (directory) {
+                    expect(directory).toBeDefined();
+                    expect(directory.isFile).toBe(false);
+                    expect(directory.isDirectory).toBe(true);
+                    expect(directory.name).toCanonicallyMatch(dirName);
+                    expect(directory.fullPath).toCanonicallyMatch(joinURL(root.fullPath, dirName));
+                    // cleanup
+                    deleteEntry(directory.name, done);
+                };
+                // create:true, exclusive:false, directory does not exist
+                root.getDirectory(dirName, {
+                    create : true
+                }, getDir, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dirName));
+            });
+            // This test is excluded, and should probably be removed. Filesystem
+            // should always be properly encoded URLs, and *not* raw paths, and it
+            // doesn't make sense to double-encode the URLs and expect that to be
+            // handled by the implementation.
+            // If a particular platform uses paths internally rather than URLs, // then that platform should careful to pass them correctly to its
+            // backend.
+            xit("file.spec.25 DirectoryEntry.getDirectory: create new dir with space resolveLocalFileSystemURL with encoded URI", function (done) {
+                var dirName = "de create dir2",
+                dirPath = joinURL(root.fullPath, dirName),
+                getDir = function (dirEntry) {
+                    var dirURI = dirEntry.toURL();
+                    // now encode URI and try to resolve
+                    window.resolveLocalFileSystemURL(encodeURI(dirURI), win, failed.bind(null, done, 'window.resolveLocalFileSystemURL - getDir function - Error resolving directory: ' + dirURI));
+                },
+                win = function (directory) {
+                    expect(directory).toBeDefined();
+                    expect(directory.isFile).toBe(false);
+                    expect(directory.isDirectory).toBe(true);
+                    expect(directory.name).toCanonicallyMatch(dirName);
+                    expect(directory.fullPath).toCanonicallyMatch(dirPath);
+                    // cleanup
+                    deleteEntry(directory.name, done);
+                };
+                // create:true, exclusive:false, directory does not exist
+                root.getDirectory(dirName, {
+                    create : true
+                }, getDir, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dirName));
+            });
+            it("file.spec.26 DirectoryEntry.getDirectory: create new directory", function (done) {
+                var dirName = "de.create.dir",
+                dirPath = joinURL(root.fullPath, dirName),
+                win = function (directory) {
+                    expect(directory).toBeDefined();
+                    expect(directory.isFile).toBe(false);
+                    expect(directory.isDirectory).toBe(true);
+                    expect(directory.name).toCanonicallyMatch(dirName);
+                    expect(directory.fullPath).toCanonicallyMatch(dirPath);
+                    expect(directory.filesystem).toBeDefined();
+                    expect(directory.filesystem).toBe(root.filesystem);
+                    // cleanup
+                    deleteEntry(directory.name, done);
+                };
+                // create:true, exclusive:false, directory does not exist
+                root.getDirectory(dirName, {
+                    create : true
+                }, win, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dirName));
+            });
+            it("file.spec.27 DirectoryEntry.getDirectory: create new directory (exclusive)", function (done) {
+                var dirName = "de.create.exclusive.dir",
+                dirPath = joinURL(root.fullPath, dirName),
+                win = function (directory) {
+                    expect(directory).toBeDefined();
+                    expect(directory.isFile).toBe(false);
+                    expect(directory.isDirectory).toBe(true);
+                    expect(directory.name).toCanonicallyMatch(dirName);
+                    expect(directory.fullPath).toCanonicallyMatch(dirPath);
+                    expect(directory.filesystem).toBeDefined();
+                    expect(directory.filesystem).toBe(root.filesystem);
+                    // cleanup
+                    deleteEntry(directory.name, done);
+                };
+                // create:true, exclusive:true, directory does not exist
+                root.getDirectory(dirName, {
+                    create : true,
+                    exclusive : true
+                }, win, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dirName));
+            });
+            it("file.spec.28 DirectoryEntry.getDirectory: create directory that already exists", function (done) {
+                var dirName = "de.create.existing.dir",
+                dirPath = joinURL(root.fullPath, dirName),
+                win = function (directory) {
+                    expect(directory).toBeDefined();
+                    expect(directory.isFile).toBe(false);
+                    expect(directory.isDirectory).toBe(true);
+                    expect(directory.name).toCanonicallyMatch(dirName);
+                    expect(directory.fullPath).toCanonicallyMatch(dirPath);
+                    // cleanup
+                    deleteEntry(directory.name, done);
+                };
+                // create directory to kick off it
+                root.getDirectory(dirName, {
+                    create : true
+                }, function () {
+                    root.getDirectory(dirName, {
+                        create : true
+                    }, win, failed.bind(null, done, 'root.getDirectory - Error creating existent second directory : ' + dirName));
+                }, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dirName));
+            });
+            it("file.spec.29 DirectoryEntry.getDirectory: create directory that already exists (exclusive)", function (done) {
+
+                var dirName = "de.create.exclusive.existing.dir",
+                dirPath = joinURL(root.fullPath, dirName),
+                existingDir,
+                fail = function (error) {
+                    expect(error).toBeDefined();
+                    if (isChrome) {
+                        /*INVALID_MODIFICATION_ERR (code: 9) is thrown instead of PATH_EXISTS_ERR(code: 12)
+                        on trying to exclusively create a file or directory, which already exists (Chrome).*/
+                        expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                    } else {
+                        expect(error).toBeFileError(FileError.PATH_EXISTS_ERR);
+                    }
+                    // cleanup
+                    deleteEntry(existingDir.name, done);
+                };
+                // create directory to kick off it
+                root.getDirectory(dirName, {
+                    create : true
+                }, function (directory) {
+                    existingDir = directory;
+                    // create:true, exclusive:true, directory exists
+                    root.getDirectory(dirName, {
+                        create : true,
+                        exclusive : true
+                    }, failed.bind(null, done, 'root.getDirectory - Unexpected success callback, second directory should not be created : ' + dirName), fail);
+                }, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dirName));
+            });
+            it("file.spec.30 DirectoryEntry.getDirectory: get Entry for existing directory", function (done) {
+                var dirName = "de.get.dir",
+                dirPath = joinURL(root.fullPath, dirName),
+                win = function (directory) {
+                    expect(directory).toBeDefined();
+                    expect(directory.isFile).toBe(false);
+                    expect(directory.isDirectory).toBe(true);
+                    expect(directory.name).toCanonicallyMatch(dirName);
+                    expect(directory.fullPath).toCanonicallyMatch(dirPath);
+                    // cleanup
+                    deleteEntry(directory.name, done);
+                };
+                // create directory to kick it off
+                root.getDirectory(dirName, {
+                    create : true
+                }, function () {
+                    root.getDirectory(dirName, {
+                        create : false
+                    }, win, failed.bind(null, done, 'root.getDirectory - Error getting directory entry : ' + dirName));
+                }, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dirName));
+            });
+            it("file.spec.31 DirectoryEntry.getDirectory: get DirectoryEntry for invalid path", function (done) {
+                if (isBrowser) {
+                    /*The plugin does not follow to ["8.3 Naming restrictions"]
+                    (http://www.w3.org/TR/2011/WD-file-system-api-20110419/#naming-restrictions).*/
+                    pending();
+                }
+
+                var dirName = "de:invalid:path",
+                fail = function (error) {
+                    expect(error).toBeDefined();
+                    expect(error).toBeFileError(FileError.ENCODING_ERR);
+                    done();
+                };
+                // create:false, exclusive:false, invalid path
+                root.getDirectory(dirName, {
+                    create : false
+                }, succeed.bind(null, done, 'root.getDirectory - Unexpected success callback, directory should not exists: ' + dirName), fail);
+            });
+            it("file.spec.32 DirectoryEntry.getDirectory: get DirectoryEntry for existing file", function (done) {
+                var fileName = "de.existing.file",
+                existingFile,
+                filePath = joinURL(root.fullPath, fileName),
+                fail = function (error) {
+                    expect(error).toBeDefined();
+                    expect(error).toBeFileError(FileError.TYPE_MISMATCH_ERR);
+                    // cleanup
+                    deleteEntry(existingFile.name, done);
+                };
+                // create file to kick off it
+                root.getFile(fileName, {
+                    create : true
+                }, function (file) {
+                    existingFile = file;
+                    root.getDirectory(fileName, {
+                        create : false
+                    }, succeed.bind(null, done, 'root.getDirectory - Unexpected success callback, directory should not exists: ' + fileName), fail);
+                }, failed.bind(null, done, 'root.getFile - Error creating file : ' + fileName));
+            });
+            it("file.spec.33 DirectoryEntry.getFile: get FileEntry for existing directory", function (done) {
+                var dirName = "de.existing.dir",
+                existingDir,
+                dirPath = joinURL(root.fullPath, dirName),
+                fail = function (error) {
+                    expect(error).toBeDefined();
+                    expect(error).toBeFileError(FileError.TYPE_MISMATCH_ERR);
+                    // cleanup
+                    deleteEntry(existingDir.name, done);
+                };
+                // create directory to kick off it
+                root.getDirectory(dirName, {
+                    create : true
+                }, function (directory) {
+                    existingDir = directory;
+                    root.getFile(dirName, {
+                        create : false
+                    }, succeed.bind(null, done, 'root.getFile - Unexpected success callback, file should not exists: ' + dirName), fail);
+                }, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dirName));
+            });
+            it("file.spec.34 DirectoryEntry.removeRecursively on directory", function (done) {
+                var dirName = "de.removeRecursively",
+                subDirName = "dir",
+                dirPath = joinURL(root.fullPath, dirName),
+                subDirPath = joinURL(dirPath, subDirName),
+                dirExists = function (error) {
+                    expect(error).toBeDefined();
+                    expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                    done();
+                };
+                // create a new directory entry to kick off it
+                root.getDirectory(dirName, {
+                    create : true
+                }, function (entry) {
+                    entry.getDirectory(subDirName, {
+                        create : true
+                    }, function (dir) {
+                        entry.removeRecursively(function () {
+                            root.getDirectory(dirName, {
+                                create : false
+                            }, succeed.bind(null, done, 'root.getDirectory - Unexpected success callback, directory should not exists: ' + dirName), dirExists);
+                        }, failed.bind(null, done, 'entry.removeRecursively - Error removing directory recursively : ' + dirName));
+                    }, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + subDirName));
+                }, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dirName));
+            });
+            it("file.spec.35 createReader: create reader on existing directory", function () {
+                // create reader for root directory
+                var reader = root.createReader();
+                expect(reader).toBeDefined();
+                expect(typeof reader.readEntries).toBe('function');
+            });
+            it("file.spec.36 removeRecursively on root file system", function (done) {
+
+                var remove = function (error) {
+                    expect(error).toBeDefined();
+                    if (isChrome) {
+                        /*INVALID_MODIFICATION_ERR (code: 9) is thrown instead of
+                        NO_MODIFICATION_ALLOWED_ERR(code: 6) on trying to call removeRecursively
+                        on the root file system (Chrome).*/
+                        expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                    } else {
+                        expect(error).toBeFileError(FileError.NO_MODIFICATION_ALLOWED_ERR);
+                    }
+                    done();
+                };
+                // remove root file system
+                root.removeRecursively(succeed.bind(null, done, 'root.removeRecursively - Unexpected success callback, root cannot be removed'), remove);
+            });
+        });
+        describe('DirectoryReader interface', function () {
+            describe("readEntries", function () {
+                it("file.spec.37 should read contents of existing directory", function (done) {
+                    var reader,
+                    win = function (entries) {
+                        expect(entries).toBeDefined();
+                        expect(entries instanceof Array).toBe(true);
+                        done();
+                    };
+                    // create reader for root directory
+                    reader = root.createReader();
+                    // read entries
+                    reader.readEntries(win, failed.bind(null, done, 'reader.readEntries - Error reading entries'));
+                });
+                it("file.spec.37.1 should read contents of existing directory", function (done) {
+                    var dirName = 'readEntries.dir',
+                    fileName = 'readeEntries.file';
+                    root.getDirectory(dirName, {
+                        create : true
+                    }, function (directory) {
+                        directory.getFile(fileName, {
+                            create : true
+                        }, function (fileEntry) {
+                            var reader = directory.createReader();
+                            reader.readEntries(function (entries) {
+                                expect(entries).toBeDefined();
+                                expect(entries instanceof Array).toBe(true);
+                                expect(entries.length).toBe(1);
+                                expect(entries[0].fullPath).toCanonicallyMatch(fileEntry.fullPath);
+                                expect(entries[0].filesystem).not.toBe(null);
+                                if (isChrome) {
+                                    // Slicing '[object {type}]' -> '{type}'
+                                    expect(entries[0].filesystem.toString().slice(8, -1)).toEqual("DOMFileSystem");
+                                }
+                                else {
+                                    expect(entries[0].filesystem instanceof FileSystem).toBe(true);
+                                }
+
+                                // cleanup
+                                deleteEntry(directory.name, done);
+                            }, failed.bind(null, done, 'reader.readEntries - Error reading entries from directory: ' + dirName));
+                        }, failed.bind(null, done, 'directory.getFile - Error creating file : ' + fileName));
+                    }, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dirName));
+                });
+                it("file.spec.109 should return an empty entry list on the second call", function (done) {
+                    var reader,
+                    fileName = 'test109.txt';
+                    // Add a file to ensure the root directory is non-empty and then read the contents of the directory.
+                    root.getFile(fileName, {
+                        create : true
+                    }, function (entry) {
+                        reader = root.createReader();
+                        //First read
+                        reader.readEntries(function (entries) {
+                            expect(entries).toBeDefined();
+                            expect(entries instanceof Array).toBe(true);
+                            expect(entries.length).not.toBe(0);
+                            //Second read
+                            reader.readEntries(function (entries_) {
+                                expect(entries_).toBeDefined();
+                                expect(entries_ instanceof Array).toBe(true);
+                                expect(entries_.length).toBe(0);
+                                //Clean up
+                                deleteEntry(entry.name, done);
+                            }, failed.bind(null, done, 'reader.readEntries - Error during SECOND reading of entries from [root] directory'));
+                        }, failed.bind(null, done, 'reader.readEntries - Error during FIRST reading of entries from [root] directory'));
+                    }, failed.bind(null, done, 'root.getFile - Error creating file : ' + fileName));
+                });
+            });
+            it("file.spec.38 should read contents of directory that has been removed", function (done) {
+                var dirName = "de.createReader.notfound",
+                dirPath = joinURL(root.fullPath, dirName);
+                // create a new directory entry to kick off it
+                root.getDirectory(dirName, {
+                    create : true
+                }, function (directory) {
+                    directory.removeRecursively(function () {
+                        var reader = directory.createReader();
+                        reader.readEntries(succeed.bind(null, done, 'reader.readEntries - Unexpected success callback, it should not read entries from deleted dir: ' + dirName), function (error) {
+                            expect(error).toBeDefined();
+                            expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                            root.getDirectory(dirName, {
+                                create : false
+                            }, succeed.bind(null, done, 'root.getDirectory - Unexpected success callback, it should not get deleted directory: ' + dirName), function (err) {
+                                expect(err).toBeDefined();
+                                expect(err).toBeFileError(FileError.NOT_FOUND_ERR);
+                                done();
+                            });
+                        });
+                    }, failed.bind(null, done, 'directory.removeRecursively - Error removing directory recursively : ' + dirName));
+                }, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dirName));
+            });
+        });
+        //DirectoryReader interface
+        describe('File', function () {
+            it("file.spec.39 constructor should be defined", function () {
+                expect(File).toBeDefined();
+                expect(typeof File).toBe('function');
+            });
+            it("file.spec.40 should be define File attributes", function () {
+                var file = new File();
+                expect(file.name).toBeDefined();
+                expect(file.type).toBeDefined();
+                expect(file.lastModifiedDate).toBeDefined();
+                expect(file.size).toBeDefined();
+            });
+        });
+        //File
+        describe('FileEntry', function () {
+
+            it("file.spec.41 should be define FileEntry methods", function (done) {
+                var fileName = "fe.methods",
+                testFileEntry = function (fileEntry) {
+                    expect(fileEntry).toBeDefined();
+                    expect(typeof fileEntry.createWriter).toBe('function');
+                    expect(typeof fileEntry.file).toBe('function');
+                    // cleanup
+                    deleteEntry(fileEntry.name, done);
+                };
+                // create a new file entry to kick off it
+                root.getFile(fileName, {
+                    create : true
+                }, testFileEntry, failed.bind(null, done, 'root.getFile - Error creating file : ' + fileName));
+            });
+            it("file.spec.42 createWriter should return a FileWriter object", function (done) {
+                var fileName = "fe.createWriter",
+                testFile,
+                testWriter = function (writer) {
+                    expect(writer).toBeDefined();
+                    if (isChrome) {
+                        // Slicing '[object {type}]' -> '{type}'
+                        expect(writer.toString().slice(8, -1)).toEqual("FileWriter");
+                    }
+                    else {
+                        expect(writer instanceof FileWriter).toBe(true);
+                    }
+
+                    // cleanup
+                    deleteEntry(testFile.name, done);
+                };
+                // create a new file entry to kick off it
+                root.getFile(fileName, {
+                    create : true
+                }, function (fileEntry) {
+                    testFile = fileEntry;
+                    fileEntry.createWriter(testWriter, failed.bind(null, done, 'fileEntry.createWriter - Error creating Writer from entry'));
+                }, failed.bind(null, done, 'root.getFile - Error creating file : ' + fileName));
+            });
+            it("file.spec.43 file should return a File object", function (done) {
+                var fileName = "fe.file",
+                newFile,
+                testFile = function (file) {
+                    expect(file).toBeDefined();
+                    if (isChrome) {
+                        // Slicing '[object {type}]' -> '{type}'
+                        expect(file.toString().slice(8, -1)).toEqual("File");
+                    }
+                    else {
+                        expect(file instanceof File).toBe(true);
+                    }
+
+                    // cleanup
+                    deleteEntry(newFile.name, done);
+                };
+                // create a new file entry to kick off it
+                root.getFile(fileName, {
+                    create : true
+                }, function (fileEntry) {
+                    newFile = fileEntry;
+                    fileEntry.file(testFile, failed.bind(null, done, 'fileEntry.file - Error reading file using fileEntry: ' + fileEntry.name));
+                }, failed.bind(null, done, 'root.getFile - Error creating file : ' + fileName));
+            });
+            it("file.spec.44 file: on File that has been removed", function (done) {
+                var fileName = "fe.no.file";
+                // create a new file entry to kick off it
+                root.getFile(fileName, {
+                    create : true
+                }, function (fileEntry) {
+                    fileEntry.remove(function () {
+                        fileEntry.file(succeed.bind(null, done, 'fileEntry.file - Unexpected success callback, file it should not be created from removed entry'), function (error) {
+                            expect(error).toBeDefined();
+                            expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                            done();
+                        });
+                    }, failed.bind(null, done, 'fileEntry.remove - Error removing entry : ' + fileName));
+                }, failed.bind(null, done, 'root.getFile - Error creating file : ' + fileName));
+            });
+        });
+        //FileEntry
+        describe('Entry', function () {
+            it("file.spec.45 Entry object", function (done) {
+                var fileName = "entry",
+                fullPath = joinURL(root.fullPath, fileName),
+                winEntry = function (entry) {
+                    expect(entry).toBeDefined();
+                    expect(entry.isFile).toBe(true);
+                    expect(entry.isDirectory).toBe(false);
+                    expect(entry.name).toCanonicallyMatch(fileName);
+                    expect(entry.fullPath).toCanonicallyMatch(fullPath);
+                    expect(typeof entry.getMetadata).toBe('function');
+                    expect(typeof entry.setMetadata).toBe('function');
+                    expect(typeof entry.moveTo).toBe('function');
+                    expect(typeof entry.copyTo).toBe('function');
+                    expect(typeof entry.toURL).toBe('function');
+                    expect(typeof entry.remove).toBe('function');
+                    expect(typeof entry.getParent).toBe('function');
+                    expect(typeof entry.createWriter).toBe('function');
+                    expect(typeof entry.file).toBe('function');
+                    // Clean up
+                    deleteEntry(fileName, done);
+                };
+                // create a new file entry
+                createFile(fileName, winEntry, failed.bind(null, done, 'createFile - Error creating file : ' + fileName));
+            });
+            it("file.spec.46 Entry.getMetadata on file", function (done) {
+                var fileName = "entry.metadata.file";
+                // create a new file entry
+                createFile(fileName, function (entry) {
+                    entry.getMetadata(function (metadata) {
+                        expect(metadata).toBeDefined();
+                        expect(metadata.modificationTime instanceof Date).toBe(true);
+                        expect(typeof metadata.size).toBe("number");
+                        // cleanup
+                        deleteEntry(fileName, done);
+                    }, failed.bind(null, done, 'entry.getMetadata - Error getting metadata from entry : ' + fileName));
+                }, failed.bind(null, done, 'createFile - Error creating file : ' + fileName));
+            });
+            it("file.spec.47 Entry.getMetadata on directory", function (done) {
+                if (isIndexedDBShim) {
+                    /* Does not support metadata for directories (Firefox, IE) */
+                    pending();
+                }
+
+                var dirName = "entry.metadata.dir";
+                // create a new directory entry
+                createDirectory(dirName, function (entry) {
+                    entry.getMetadata(function (metadata) {
+                        expect(metadata).toBeDefined();
+                        expect(metadata.modificationTime instanceof Date).toBe(true);
+                        expect(typeof metadata.size).toBe("number");
+                        expect(metadata.size).toBe(0);
+                        // cleanup
+                        deleteEntry(dirName, done);
+                    }, failed.bind(null, done, 'entry.getMetadata - Error getting metadata from entry : ' + dirName));
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + dirName));
+            });
+            it("file.spec.48 Entry.getParent on file in root file system", function (done) {
+                var fileName = "entry.parent.file",
+                rootPath = root.fullPath;
+                // create a new file entry
+                createFile(fileName, function (entry) {
+                    entry.getParent(function (parent) {
+                        expect(parent).toBeDefined();
+                        expect(parent.fullPath).toCanonicallyMatch(rootPath);
+                        // cleanup
+                        deleteEntry(fileName, done);
+                    }, failed.bind(null, done, 'entry.getParent - Error getting parent directory of file : ' + fileName));
+                }, failed.bind(null, done, 'createFile - Error creating file : ' + fileName));
+            });
+            it("file.spec.49 Entry.getParent on directory in root file system", function (done) {
+                var dirName = "entry.parent.dir",
+                rootPath = root.fullPath;
+                // create a new directory entry
+                createDirectory(dirName, function (entry) {
+                    entry.getParent(function (parent) {
+                        expect(parent).toBeDefined();
+                        expect(parent.fullPath).toCanonicallyMatch(rootPath);
+                        // cleanup
+                        deleteEntry(dirName, done);
+                    }, failed.bind(null, done, 'entry.getParent - Error getting parent directory of directory : ' + dirName));
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + dirName));
+            });
+            it("file.spec.50 Entry.getParent on root file system", function (done) {
+                var rootPath = root.fullPath,
+                winParent = function (parent) {
+                    expect(parent).toBeDefined();
+                    expect(parent.fullPath).toCanonicallyMatch(rootPath);
+                    done();
+                };
+                // create a new directory entry
+                root.getParent(winParent, failed.bind(null, done, 'root.getParent - Error getting parent directory of root'));
+            });
+            it("file.spec.51 Entry.toURL on file", function (done) {
+                var fileName = "entry.uri.file",
+                rootPath = root.fullPath,
+                winURI = function (entry) {
+                    var uri = entry.toURL();
+                    expect(uri).toBeDefined();
+                    expect(uri.indexOf(rootPath)).not.toBe(-1);
+                    // cleanup
+                    deleteEntry(fileName, done);
+                };
+                // create a new file entry
+                createFile(fileName, winURI, failed.bind(null, done, 'createFile - Error creating file : ' + fileName));
+            });
+            it("file.spec.52 Entry.toURL on directory", function (done) {
+                var dirName_1 = "num 1",
+                dirName_2 = "num 2",
+                rootPath = root.fullPath;
+                createDirectory(dirName_1, function (entry) {
+                    entry.getDirectory(dirName_2, {
+                        create : true
+                    }, function (entryFile) {
+                        var uri = entryFile.toURL();
+                        expect(uri).toBeDefined();
+                        expect(uri).toContain('/num%201/num%202/');
+                        expect(uri.indexOf(rootPath)).not.toBe(-1);
+                        // cleanup
+                        deleteEntry(dirName_1, done);
+                    }, failed.bind(null, done, 'entry.getDirectory - Error creating directory : ' + dirName_2));
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + dirName_1));
+            });
+            it("file.spec.53 Entry.remove on file", function (done) {
+                var fileName = "entr .rm.file";
+                // create a new file entry
+                createFile(fileName, function (entry) {
+                    expect(entry).toBeDefined();
+                    entry.remove(function () {
+                        root.getFile(fileName, null, succeed.bind(null, done, 'root.getFile - Unexpected success callback, it should not get deleted file : ' + fileName), function (error) {
+                            expect(error).toBeDefined();
+                            expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                            // cleanup
+                            deleteEntry(fileName, done);
+                        });
+                    }, failed.bind(null, done, 'entry.remove - Error removing entry : ' + fileName));
+                }, failed.bind(null, done, 'createFile - Error creating file : ' + fileName));
+            });
+            it("file.spec.54 remove on empty directory", function (done) {
+                var dirName = "entry.rm.dir";
+                // create a new directory entry
+                createDirectory(dirName, function (entry) {
+                    expect(entry).toBeDefined();
+                    entry.remove(function () {
+                        root.getDirectory(dirName, null, succeed.bind(null, done, 'root.getDirectory - Unexpected success callback, it should not get deleted directory : ' + dirName), function (error) {
+                            expect(error).toBeDefined();
+                            expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                            // cleanup
+                            deleteEntry(dirName, done);
+                        });
+                    }, failed.bind(null, done, 'entry.remove - Error removing entry : ' + dirName));
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + dirName));
+            });
+            it("file.spec.55 remove on non-empty directory", function (done) {
+                if (isIndexedDBShim) {
+                    /* Both Entry.remove and directoryEntry.removeRecursively don't fail when removing
+                    non-empty directories - directories being removed are cleaned
+                    along with contents instead (Firefox, IE)*/
+                    pending();
+                }
+
+                var dirName = "ent y.rm.dir.not.empty",
+                fileName = "re ove.txt",
+                fullPath = joinURL(root.fullPath, dirName);
+                // create a new directory entry
+                createDirectory(dirName, function (entry) {
+                    entry.getFile(fileName, {
+                        create : true
+                    }, function (fileEntry) {
+                        entry.remove(succeed.bind(null, done, 'entry.remove - Unexpected success callback, it should not remove a directory that contains files : ' + dirName), function (error) {
+                            expect(error).toBeDefined();
+                            expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                            root.getDirectory(dirName, null, function (entry) {
+                                expect(entry).toBeDefined();
+                                expect(entry.fullPath).toCanonicallyMatch(fullPath);
+                                // cleanup
+                                deleteEntry(dirName, done);
+                            }, failed.bind(null, done, 'root.getDirectory - Error getting directory : ' + dirName));
+                        });
+                    }, failed.bind(null, done, 'entry.getFile - Error creating file : ' + fileName + ' inside of ' + dirName));
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + dirName));
+            });
+            it("file.spec.56 remove on root file system", function (done) {
+
+                // remove entry that doesn't exist
+                root.remove(succeed.bind(null, done, 'entry.remove - Unexpected success callback, it should not remove entry that it does not exists'), function (error) {
+                    expect(error).toBeDefined();
+                    if (isChrome) {
+                        /*INVALID_MODIFICATION_ERR (code: 9) is thrown instead of
+                        NO_MODIFICATION_ALLOWED_ERR(code: 6) on trying to call removeRecursively
+                        on the root file system.*/
+                        expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                    } else {
+                        expect(error).toBeFileError(FileError.NO_MODIFICATION_ALLOWED_ERR);
+                    }
+                    done();
+                });
+            });
+            it("file.spec.57 copyTo: file", function (done) {
+                var file1 = "entry copy.file1",
+                file2 = "entry copy.file2",
+                fullPath = joinURL(root.fullPath, file2);
+                // create a new file entry to kick off it
+                deleteEntry(file2, function () {
+                    createFile(file1, function (fileEntry) {
+                        // copy file1 to file2
+                        fileEntry.copyTo(root, file2, function (entry) {
+                            expect(entry).toBeDefined();
+                            expect(entry.isFile).toBe(true);
+                            expect(entry.isDirectory).toBe(false);
+                            expect(entry.fullPath).toCanonicallyMatch(fullPath);
+                            expect(entry.name).toCanonicallyMatch(file2);
+                            root.getFile(file2, {
+                                create : false
+                            }, function (entry2) {
+                                expect(entry2).toBeDefined();
+                                expect(entry2.isFile).toBe(true);
+                                expect(entry2.isDirectory).toBe(false);
+                                expect(entry2.fullPath).toCanonicallyMatch(fullPath);
+                                expect(entry2.name).toCanonicallyMatch(file2);
+                                // cleanup
+                                deleteEntry(file1, function () {
+                                    deleteEntry(file2, done);
+                                });
+                            }, failed.bind(null, done, 'root.getFile - Error getting copied file : ' + file2));
+                        }, failed.bind(null, done, 'fileEntry.copyTo - Error copying file : ' + file2));
+                    }, failed.bind(null, done, 'createFile - Error creating file : ' + file1));
+                }, failed.bind(null, done, 'deleteEntry - Error removing file : ' + file2));
+            });
+            it("file.spec.58 copyTo: file onto itself", function (done) {
+                var file1 = "entry.copy.fos.file1";
+                // create a new file entry to kick off it
+                createFile(file1, function (entry) {
+                    // copy file1 onto itself
+                    entry.copyTo(root, null, succeed.bind(null, done, 'entry.copyTo - Unexpected success callback, it should not copy a null file'), function (error) {
+                        expect(error).toBeDefined();
+                        expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                        // cleanup
+                        deleteEntry(file1, done);
+                    });
+                }, failed.bind(null, done, 'createFile - Error creating file : ' + file1));
+            });
+            it("file.spec.59 copyTo: directory", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var file1 = "file1",
+                srcDir = "entry.copy.srcDir",
+                dstDir = "entry.copy.dstDir",
+                dstPath = joinURL(root.fullPath, dstDir),
+                filePath = joinURL(dstPath, file1);
+                // create a new directory entry to kick off it
+                deleteEntry(dstDir, function () {
+                    createDirectory(srcDir, function (directory) {
+                        // create a file within new directory
+                        directory.getFile(file1, {
+                            create : true
+                        }, function () {
+                            directory.copyTo(root, dstDir, function (directory) {
+                                expect(directory).toBeDefined();
+                                expect(directory.isFile).toBe(false);
+                                expect(directory.isDirectory).toBe(true);
+                                expect(directory.fullPath).toCanonicallyMatch(dstPath);
+                                expect(directory.name).toCanonicallyMatch(dstDir);
+                                root.getDirectory(dstDir, {
+                                    create : false
+                                }, function (dirEntry) {
+                                    expect(dirEntry).toBeDefined();
+                                    expect(dirEntry.isFile).toBe(false);
+                                    expect(dirEntry.isDirectory).toBe(true);
+                                    expect(dirEntry.fullPath).toCanonicallyMatch(dstPath);
+                                    expect(dirEntry.name).toCanonicallyMatch(dstDir);
+                                    dirEntry.getFile(file1, {
+                                        create : false
+                                    }, function (fileEntry) {
+                                        expect(fileEntry).toBeDefined();
+                                        expect(fileEntry.isFile).toBe(true);
+                                        expect(fileEntry.isDirectory).toBe(false);
+                                        expect(fileEntry.fullPath).toCanonicallyMatch(filePath);
+                                        expect(fileEntry.name).toCanonicallyMatch(file1);
+                                        // cleanup
+                                        deleteEntry(srcDir, function () {
+                                            deleteEntry(dstDir, done);
+                                        });
+                                    }, failed.bind(null, done, 'dirEntry.getFile - Error getting file : ' + file1));
+                                }, failed.bind(null, done, 'root.getDirectory - Error getting copied directory : ' + dstDir));
+                            }, failed.bind(null, done, 'directory.copyTo - Error copying directory : ' + srcDir + ' to :' + dstDir));
+                        }, failed.bind(null, done, 'directory.getFile - Error creating file : ' + file1));
+                    }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+                }, failed.bind(null, done, 'deleteEntry - Error removing directory : ' + dstDir));
+            });
+            it("file.spec.60 copyTo: directory to backup at same root directory", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var file1 = "file1",
+                srcDir = "entry.copy srcDirSame",
+                dstDir = "entry.copy srcDirSame-backup",
+                dstPath = joinURL(root.fullPath, dstDir),
+                filePath = joinURL(dstPath, file1);
+                // create a new directory entry to kick off it
+                deleteEntry(dstDir, function () {
+                    createDirectory(srcDir, function (directory) {
+                        directory.getFile(file1, {
+                            create : true
+                        }, function () {
+                            directory.copyTo(root, dstDir, function (directory) {
+                                expect(directory).toBeDefined();
+                                expect(directory.isFile).toBe(false);
+                                expect(directory.isDirectory).toBe(true);
+                                expect(directory.fullPath).toCanonicallyMatch(dstPath);
+                                expect(directory.name).toCanonicallyMatch(dstDir);
+                                root.getDirectory(dstDir, {
+                                    create : false
+                                }, function (dirEntry) {
+                                    expect(dirEntry).toBeDefined();
+                                    expect(dirEntry.isFile).toBe(false);
+                                    expect(dirEntry.isDirectory).toBe(true);
+                                    expect(dirEntry.fullPath).toCanonicallyMatch(dstPath);
+                                    expect(dirEntry.name).toCanonicallyMatch(dstDir);
+                                    dirEntry.getFile(file1, {
+                                        create : false
+                                    }, function (fileEntry) {
+                                        expect(fileEntry).toBeDefined();
+                                        expect(fileEntry.isFile).toBe(true);
+                                        expect(fileEntry.isDirectory).toBe(false);
+                                        expect(fileEntry.fullPath).toCanonicallyMatch(filePath);
+                                        expect(fileEntry.name).toCanonicallyMatch(file1);
+                                        // cleanup
+                                        deleteEntry(srcDir, function () {
+                                            deleteEntry(dstDir, done);
+                                        });
+                                    }, failed.bind(null, done, 'dirEntry.getFile - Error getting file : ' + file1));
+                                }, failed.bind(null, done, 'root.getDirectory - Error getting copied directory : ' + dstDir));
+                            }, failed.bind(null, done, 'directory.copyTo - Error copying directory : ' + srcDir + ' to :' + dstDir));
+                        }, failed.bind(null, done, 'directory.getFile - Error creating file : ' + file1));
+                    }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+                }, failed.bind(null, done, 'deleteEntry - Error removing directory : ' + dstDir));
+            });
+            it("file.spec.61 copyTo: directory onto itself", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var file1 = "file1",
+                srcDir = "entry.copy.dos.srcDir",
+                srcPath = joinURL(root.fullPath, srcDir),
+                filePath = joinURL(srcPath, file1);
+                // create a new directory entry to kick off it
+                createDirectory(srcDir, function (directory) {
+                    // create a file within new directory
+                    directory.getFile(file1, {
+                        create : true
+                    }, function (fileEntry) {
+                        // copy srcDir onto itself
+                        directory.copyTo(root, null, succeed.bind(null, done, 'directory.copyTo - Unexpected success callback, it should not copy file: ' + srcDir + ' to a null destination'), function (error) {
+                            expect(error).toBeDefined();
+                            expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                            root.getDirectory(srcDir, {
+                                create : false
+                            }, function (dirEntry) {
+                                expect(dirEntry).toBeDefined();
+                                expect(dirEntry.fullPath).toCanonicallyMatch(srcPath);
+                                dirEntry.getFile(file1, {
+                                    create : false
+                                }, function (fileEntry) {
+                                    expect(fileEntry).toBeDefined();
+                                    expect(fileEntry.fullPath).toCanonicallyMatch(filePath);
+                                    // cleanup
+                                    deleteEntry(srcDir, done);
+                                }, failed.bind(null, done, 'dirEntry.getFile - Error getting file : ' + file1));
+                            }, failed.bind(null, done, 'root.getDirectory - Error getting directory : ' + srcDir));
+                        });
+                    }, failed.bind(null, done, 'directory.getFile - Error creating file : ' + file1));
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+            });
+            it("file.spec.62 copyTo: directory into itself", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var srcDir = "entry.copy.dis.srcDir",
+                dstDir = "entry.copy.dis.dstDir",
+                srcPath = joinURL(root.fullPath, srcDir);
+                // create a new directory entry to kick off it
+                createDirectory(srcDir, function (directory) {
+                    // copy source directory into itself
+                    directory.copyTo(directory, dstDir, succeed.bind(null, done, 'directory.copyTo - Unexpected success callback, it should not copy a directory ' + srcDir + ' into itself'), function (error) {
+                        expect(error).toBeDefined();
+                        expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                        root.getDirectory(srcDir, {
+                            create : false
+                        }, function (dirEntry) {
+                            // returning confirms existence so just check fullPath entry
+                            expect(dirEntry).toBeDefined();
+                            expect(dirEntry.fullPath).toCanonicallyMatch(srcPath);
+                            // cleanup
+                            deleteEntry(srcDir, done);
+                        }, failed.bind(null, done, 'root.getDirectory - Error getting directory : ' + srcDir));
+                    });
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+            });
+            it("file.spec.63 copyTo: directory that does not exist", function (done) {
+                var file1 = "entry.copy.dnf.file1",
+                dirName = 'dir-foo';
+                createFile(file1, function (fileEntry) {
+                    createDirectory(dirName, function (dirEntry) {
+                        dirEntry.remove(function () {
+                            fileEntry.copyTo(dirEntry, null, succeed.bind(null, done, 'fileEntry.copyTo - Unexpected success callback, it should not copy a file ' + file1 + ' into a removed directory'), function (error) {
+                                expect(error).toBeDefined();
+                                expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                                done();
+                            });
+                        }, failed.bind(null, done, 'dirEntry.remove - Error removing directory : ' + dirName));
+                    }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + dirName));
+                }, failed.bind(null, done, 'createFile - Error creating file : ' + file1));
+            });
+            it("file.spec.64 copyTo: invalid target name", function (done) {
+                if (isBrowser) {
+                    /*The plugin does not follow ["8.3 Naming restrictions"]
+                    (http://www.w3.org/TR/2011/WD-file-system-api-20110419/#naming-restrictions*/
+                    pending();
+                }
+
+                var file1 = "entry.copy.itn.file1",
+                file2 = "bad:file:name";
+                // create a new file entry
+                createFile(file1, function (entry) {
+                    // copy file1 to file2
+                    entry.copyTo(root, file2, succeed.bind(null, done, 'entry.copyTo - Unexpected success callback, it should not copy a file ' + file1 + ' to an invalid file name: ' + file2), function (error) {
+                        expect(error).toBeDefined();
+                        expect(error).toBeFileError(FileError.ENCODING_ERR);
+                        // cleanup
+                        deleteEntry(file1, done);
+                    });
+                }, failed.bind(null, done, 'createFile - Error creating file : ' + file1));
+            });
+            it("file.spec.65 moveTo: file to same parent", function (done) {
+                var file1 = "entry.move.fsp.file1",
+                file2 = "entry.move.fsp.file2",
+                dstPath = joinURL(root.fullPath, file2);
+                // create a new file entry to kick off it
+                createFile(file1, function (entry) {
+                    // move file1 to file2
+                    entry.moveTo(root, file2, function (entry) {
+                        expect(entry).toBeDefined();
+                        expect(entry.isFile).toBe(true);
+                        expect(entry.isDirectory).toBe(false);
+                        expect(entry.fullPath).toCanonicallyMatch(dstPath);
+                        expect(entry.name).toCanonicallyMatch(file2);
+                        root.getFile(file2, {
+                            create : false
+                        }, function (fileEntry) {
+                            expect(fileEntry).toBeDefined();
+                            expect(fileEntry.fullPath).toCanonicallyMatch(dstPath);
+                            root.getFile(file1, {
+                                create : false
+                            }, succeed.bind(null, done, 'root.getFile - Unexpected success callback, it should not get invalid or moved file: ' + file1), function (error) {
+                                //expect(navigator.fileMgr.testFileExists(srcPath) === false, "original file should not exist.");
+                                expect(error).toBeDefined();
+                                expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                                // cleanup
+                                deleteEntry(file1, function () {
+                                    deleteEntry(file2, done);
+                                });
+                            });
+                        }, failed.bind(null, done, 'root.getFile - Error getting file : ' + file2));
+                    }, failed.bind(null, done, 'entry.moveTo - Error moving file : ' + file1 + ' to root as: ' + file2));
+                }, failed.bind(null, done, 'createFile - Error creating file : ' + file1));
+            });
+            it("file.spec.66 moveTo: file to new parent", function (done) {
+                var file1 = "entry.move.fnp.file1",
+                dir = "entry.move.fnp.dir",
+                dstPath = joinURL(joinURL(root.fullPath, dir), file1);
+                // ensure destination directory is cleaned up first
+                deleteEntry(dir, function () {
+                    // create a new file entry to kick off it
+                    createFile(file1, function (entry) {
+                        // create a parent directory to move file to
+                        root.getDirectory(dir, {
+                            create : true
+                        }, function (directory) {
+                            // move file1 to new directory
+                            // move the file
+                            entry.moveTo(directory, null, function (entry) {
+                                expect(entry).toBeDefined();
+                                expect(entry.isFile).toBe(true);
+                                expect(entry.isDirectory).toBe(false);
+                                expect(entry.fullPath).toCanonicallyMatch(dstPath);
+                                expect(entry.name).toCanonicallyMatch(file1);
+                                // test the moved file exists
+                                directory.getFile(file1, {
+                                    create : false
+                                }, function (fileEntry) {
+                                    expect(fileEntry).toBeDefined();
+                                    expect(fileEntry.fullPath).toCanonicallyMatch(dstPath);
+                                    root.getFile(file1, {
+                                        create : false
+                                    }, succeed.bind(null, done, 'root.getFile - Unexpected success callback, it should not get invalid or moved file: ' + file1), function (error) {
+                                        expect(error).toBeDefined();
+                                        expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                                        // cleanup
+                                        deleteEntry(file1, function () {
+                                            deleteEntry(dir, done);
+                                        });
+                                    });
+                                }, failed.bind(null, done, 'directory.getFile - Error getting file : ' + file1 + ' from: ' + dir));
+                            }, failed.bind(null, done, 'entry.moveTo - Error moving file : ' + file1 + ' to: ' + dir + ' with the same name'));
+                        }, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dir));
+                    }, failed.bind(null, done, 'createFile - Error creating file : ' + file1));
+                }, failed.bind(null, done, 'deleteEntry - Error removing directory : ' + dir));
+            });
+            it("file.spec.67 moveTo: directory to same parent", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var file1 = "file1",
+                srcDir = "entry.move.dsp.srcDir",
+                dstDir = "entry.move.dsp.dstDir",
+                srcPath = joinURL(root.fullPath, srcDir),
+                dstPath = joinURL(root.fullPath, dstDir),
+                filePath = joinURL(dstPath, file1);
+                // ensure destination directory is cleaned up before it
+                deleteEntry(dstDir, function () {
+                    // create a new directory entry to kick off it
+                    createDirectory(srcDir, function (directory) {
+                        // create a file within directory
+                        directory.getFile(file1, {
+                            create : true
+                        }, function () {
+                            // move srcDir to dstDir
+                            directory.moveTo(root, dstDir, function (directory) {
+                                expect(directory).toBeDefined();
+                                expect(directory.isFile).toBe(false);
+                                expect(directory.isDirectory).toBe(true);
+                                expect(directory.fullPath).toCanonicallyMatch(dstPath);
+                                expect(directory.name).toCanonicallyMatch(dstDir);
+                                // test that moved file exists in destination dir
+                                directory.getFile(file1, {
+                                    create : false
+                                }, function (fileEntry) {
+                                    expect(fileEntry).toBeDefined();
+                                    expect(fileEntry.fullPath).toCanonicallyMatch(filePath);
+                                    // check that the moved file no longer exists in original dir
+                                    root.getFile(file1, {
+                                        create : false
+                                    }, succeed.bind(null, done, 'directory.getFile - Unexpected success callback, it should not get invalid or moved file: ' + file1), function (error) {
+                                        expect(error).toBeDefined();
+                                        expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                                        // cleanup
+                                        deleteEntry(srcDir, function() {
+                                            deleteEntry(dstDir, done);
+                                        });
+                                    });
+                                }, failed.bind(null, done, 'directory.getFile - Error getting file : ' + file1 + ' from: ' + srcDir));
+                            }, failed.bind(null, done, 'entry.moveTo - Error moving directory : ' + srcDir + ' to root as: ' + dstDir));
+                        }, failed.bind(null, done, 'directory.getFile - Error creating file : ' + file1));
+                    }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+                }, failed.bind(null, done, 'deleteEntry - Error removing directory : ' + dstDir));
+            });
+            it("file.spec.68 moveTo: directory to same parent with same name", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var file1 = "file1",
+                srcDir = "entry.move.dsp.srcDir",
+                dstDir = "entry.move.dsp.srcDir-backup",
+                srcPath = joinURL(root.fullPath, srcDir),
+                dstPath = joinURL(root.fullPath, dstDir),
+                filePath = joinURL(dstPath, file1);
+                // ensure destination directory is cleaned up before it
+                deleteEntry(dstDir, function () {
+                    // create a new directory entry to kick off it
+                    createDirectory(srcDir, function (directory) {
+                        // create a file within directory
+                        directory.getFile(file1, {
+                            create : true
+                        }, function () {
+                            // move srcDir to dstDir
+                            directory.moveTo(root, dstDir, function (directory) {
+                                expect(directory).toBeDefined();
+                                expect(directory.isFile).toBe(false);
+                                expect(directory.isDirectory).toBe(true);
+                                expect(directory.fullPath).toCanonicallyMatch(dstPath);
+                                expect(directory.name).toCanonicallyMatch(dstDir);
+                                // check that moved file exists in destination dir
+                                directory.getFile(file1, {
+                                    create : false
+                                }, function (fileEntry) {
+                                    expect(fileEntry).toBeDefined();
+                                    expect(fileEntry.fullPath).toCanonicallyMatch(filePath);
+                                    // check that the moved file no longer exists in original dir
+                                    root.getFile(file1, {
+                                        create : false
+                                    }, succeed.bind(null, done, 'directory.getFile - Unexpected success callback, it should not get invalid or moved file: ' + file1), function (error) {
+                                        expect(error).toBeDefined();
+                                        expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                                        // cleanup
+                                        deleteEntry(srcDir, function() {
+                                            deleteEntry(dstDir, done);
+                                        });
+                                    });
+                                }, failed.bind(null, done, 'directory.getFile - Error getting file : ' + file1 + ' from: ' + srcDir));
+                            }, failed.bind(null, done, 'entry.moveTo - Error moving directory : ' + srcDir + ' to root as: ' + dstDir));
+                        }, failed.bind(null, done, 'directory.getFile - Error creating file : ' + file1));
+                    }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+                }, failed.bind(null, done, 'deleteEntry - Error removing directory : ' + dstDir));
+            });
+            it("file.spec.69 moveTo: directory to new parent", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var file1 = "file1",
+                srcDir = "entry.move.dnp.srcDir",
+                dstDir = "entry.move.dnp.dstDir",
+                srcPath = joinURL(root.fullPath, srcDir),
+                dstPath = joinURL(root.fullPath, dstDir),
+                filePath = joinURL(dstPath, file1);
+                // ensure destination directory is cleaned up before it
+                deleteEntry(dstDir, function () {
+                    // create a new directory entry to kick off it
+                    createDirectory(srcDir, function (directory) {
+                        // create a file within directory
+                        directory.getFile(file1, {
+                            create : true
+                        }, function () {
+                            // move srcDir to dstDir
+                            directory.moveTo(root, dstDir, function (dirEntry) {
+                                expect(dirEntry).toBeDefined();
+                                expect(dirEntry.isFile).toBe(false);
+                                expect(dirEntry.isDirectory).toBe(true);
+                                expect(dirEntry.fullPath).toCanonicallyMatch(dstPath);
+                                expect(dirEntry.name).toCanonicallyMatch(dstDir);
+                                // test that moved file exists in destination dir
+                                dirEntry.getFile(file1, {
+                                    create : false
+                                }, function (fileEntry) {
+                                    expect(fileEntry).toBeDefined();
+                                    expect(fileEntry.fullPath).toCanonicallyMatch(filePath);
+                                    // test that the moved file no longer exists in original dir
+                                    root.getFile(file1, {
+                                        create : false
+                                    }, succeed.bind(null, done, 'root.getFile - Unexpected success callback, it should not get invalid or moved file: ' + file1), function (error) {
+                                        expect(error).toBeDefined();
+                                        expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                                        // cleanup
+                                        deleteEntry(srcDir, function() {
+                                            deleteEntry(dstDir, done);
+                                        });
+                                    });
+                                }, failed.bind(null, done, 'directory.getFile - Error getting file : ' + file1 + ' from: ' + dstDir));
+                            }, failed.bind(null, done, 'directory.moveTo - Error moving directory : ' + srcDir + ' to root as: ' + dstDir));
+                        }, failed.bind(null, done, 'directory.getFile - Error creating file : ' + file1));
+                    }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+                }, failed.bind(null, done, 'deleteEntry - Error removing directory : ' + dstDir));
+            });
+            it("file.spec.70 moveTo: directory onto itself", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var file1 = "file1",
+                srcDir = "entry.move.dos.srcDir",
+                srcPath = joinURL(root.fullPath, srcDir),
+                filePath = joinURL(srcPath, file1);
+                // create a new directory entry to kick off it
+                createDirectory(srcDir, function (directory) {
+                    // create a file within new directory
+                    directory.getFile(file1, {
+                        create : true
+                    }, function () {
+                        // move srcDir onto itself
+                        directory.moveTo(root, null, succeed.bind(null, done, 'directory.moveTo - Unexpected success callback, it should not move directory to invalid path'), function (error) {
+                            expect(error).toBeDefined();
+                            expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                            // test that original dir still exists
+                            root.getDirectory(srcDir, {
+                                create : false
+                            }, function (dirEntry) {
+                                // returning confirms existence so just check fullPath entry
+                                expect(dirEntry).toBeDefined();
+                                expect(dirEntry.fullPath).toCanonicallyMatch(srcPath);
+                                dirEntry.getFile(file1, {
+                                    create : false
+                                }, function (fileEntry) {
+                                    expect(fileEntry).toBeDefined();
+                                    expect(fileEntry.fullPath).toCanonicallyMatch(filePath);
+                                    // cleanup
+                                    deleteEntry(srcDir, done);
+                                }, failed.bind(null, done, 'dirEntry.getFile - Error getting file : ' + file1 + ' from: ' + srcDir));
+                            }, failed.bind(null, done, 'root.getDirectory - Error getting directory : ' + srcDir));
+                        });
+                    }, failed.bind(null, done, 'directory.getFile - Error creating file : ' + file1));
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+            });
+            it("file.spec.71 moveTo: directory into itself", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var srcDir = "entry.move.dis.srcDir",
+                dstDir = "entry.move.dis.dstDir",
+                srcPath = joinURL(root.fullPath, srcDir);
+                // create a new directory entry to kick off it
+                createDirectory(srcDir, function (directory) {
+                    // move source directory into itself
+                    directory.moveTo(directory, dstDir, succeed.bind(null, done, 'directory.moveTo - Unexpected success callback, it should not move a directory into itself: ' + srcDir), function (error) {
+                        expect(error).toBeDefined();
+                        expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                        // make sure original directory still exists
+                        root.getDirectory(srcDir, {
+                            create : false
+                        }, function (entry) {
+                            expect(entry).toBeDefined();
+                            expect(entry.fullPath).toCanonicallyMatch(srcPath);
+                            // cleanup
+                            deleteEntry(srcDir, done);
+                        }, failed.bind(null, done, 'root.getDirectory - Error getting directory, making sure that original directory still exists: ' + srcDir));
+                    });
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+            });
+            it("file.spec.130 moveTo: directory into similar directory", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var srcDir = "entry.move.dis.srcDir",
+                dstDir = "entry.move.dis.srcDir-backup",
+                srcPath = joinURL(root.fullPath, srcDir);
+                // create a new directory entry to kick off it
+                createDirectory(srcDir, function (srcDirEntry) {
+                deleteEntry(dstDir, function () {
+                createDirectory(dstDir, function (dstDirEntry) {
+                    // move source directory into itself
+                    srcDirEntry.moveTo(dstDirEntry, 'file', function (newDirEntry) {
+                        expect(newDirEntry).toBeDefined();
+                        deleteEntry(dstDir, done);
+                    }, failed.bind(null, done, 'directory.moveTo - Error moving a directory into a similarly-named directory: ' + srcDir));
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + dstDir));
+                }, failed.bind(null, done, 'deleteEntry - Error deleting directory : ' + dstDir));
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+            });
+            it("file.spec.72 moveTo: file onto itself", function (done) {
+                var file1 = "entry.move.fos.file1",
+                filePath = joinURL(root.fullPath, file1);
+                // create a new file entry to kick off it
+                createFile(file1, function (entry) {
+                    // move file1 onto itself
+                    entry.moveTo(root, null, succeed.bind(null, done, 'entry.moveTo - Unexpected success callback, it should not move a file: ' + file1 + ' into the same parent'), function (error) {
+                        expect(error).toBeDefined();
+                        expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                        //test that original file still exists
+                        root.getFile(file1, {
+                            create : false
+                        }, function (fileEntry) {
+                            expect(fileEntry).toBeDefined();
+                            expect(fileEntry.fullPath).toCanonicallyMatch(filePath);
+                            // cleanup
+                            deleteEntry(file1, done);
+                        }, failed.bind(null, done, 'root.getFile - Error getting file, making sure that original file still exists: ' + file1));
+                    });
+                }, failed.bind(null, done, 'createFile - Error creating file : ' + file1));
+            });
+            it("file.spec.73 moveTo: file onto existing directory", function (done) {
+                var file1 = "entry.move.fod.file1",
+                dstDir = "entry.move.fod.dstDir",
+                subDir = "subDir",
+                dirPath = joinURL(joinURL(root.fullPath, dstDir), subDir),
+                filePath = joinURL(root.fullPath, file1);
+                // ensure destination directory is cleaned up before it
+                deleteEntry(dstDir, function () {
+                    // create a new file entry to kick off it
+                    createFile(file1, function (entry) {
+                        // create top level directory
+                        root.getDirectory(dstDir, {
+                            create : true
+                        }, function (directory) {
+                            // create sub-directory
+                            directory.getDirectory(subDir, {
+                                create : true
+                            }, function (subDirectory) {
+                                // move file1 onto sub-directory
+                                entry.moveTo(directory, subDir, succeed.bind(null, done, 'entry.moveTo - Unexpected success callback, it should not move a file: ' + file1 + ' into directory: ' + dstDir + '\n' + subDir + ' directory already exists'), function (error) {
+                                    expect(error).toBeDefined();
+                                    expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                                    // check that original dir still exists
+                                    directory.getDirectory(subDir, {
+                                        create : false
+                                    }, function (dirEntry) {
+                                        expect(dirEntry).toBeDefined();
+                                        expect(dirEntry.fullPath).toCanonicallyMatch(dirPath);
+                                        // check that original file still exists
+                                        root.getFile(file1, {
+                                            create : false
+                                        }, function (fileEntry) {
+                                            expect(fileEntry).toBeDefined();
+                                            expect(fileEntry.fullPath).toCanonicallyMatch(filePath);
+                                            // cleanup
+                                            deleteEntry(file1, function () {
+                                                deleteEntry(dstDir, done);
+                                            });
+                                        }, failed.bind(null, done, 'root.getFile - Error getting file, making sure that original file still exists: ' + file1));
+                                    }, failed.bind(null, done, 'directory.getDirectory - Error getting directory, making sure that original directory still exists: ' + subDir));
+                                });
+                            }, failed.bind(null, done, 'directory.getDirectory - Error creating directory : ' + subDir));
+                        }, failed.bind(null, done, 'root.getDirectory - Error creating directory : ' + dstDir));
+                    }, failed.bind(null, done, 'createFile - Error creating file : ' + file1));
+                }, failed.bind(null, done, 'deleteEntry - Error removing directory : ' + dstDir));
+            });
+            it("file.spec.74 moveTo: directory onto existing file", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var file1 = "entry.move.dof.file1",
+                srcDir = "entry.move.dof.srcDir",
+                dirPath = joinURL(root.fullPath, srcDir),
+                filePath = joinURL(root.fullPath, file1);
+                // create a new directory entry to kick off it
+                createDirectory(srcDir, function (entry) {
+                    // create file
+                    root.getFile(file1, {
+                        create : true
+                    }, function (fileEntry) {
+                        // move directory onto file
+                        entry.moveTo(root, file1, succeed.bind(null, done, 'entry.moveTo - Unexpected success callback, it should not move : \n' + srcDir + ' into root directory renamed as ' + file1 + '\n' + file1 + ' file already exists'), function (error) {
+                            expect(error).toBeDefined();
+                            expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                            // test that original directory exists
+                            root.getDirectory(srcDir, {
+                                create : false
+                            }, function (dirEntry) {
+                                // returning confirms existence so just check fullPath entry
+                                expect(dirEntry).toBeDefined();
+                                expect(dirEntry.fullPath).toCanonicallyMatch(dirPath);
+                                // test that original file exists
+                                root.getFile(file1, {
+                                    create : false
+                                }, function (fileEntry) {
+                                    expect(fileEntry).toBeDefined();
+                                    expect(fileEntry.fullPath).toCanonicallyMatch(filePath);
+                                    // cleanup
+                                    deleteEntry(file1, function () {
+                                        deleteEntry(srcDir, done);
+                                    });
+                                }, failed.bind(null, done, 'root.getFile - Error getting file, making sure that original file still exists: ' + file1));
+                            }, failed.bind(null, done, 'directory.getDirectory - Error getting directory, making sure that original directory still exists: ' + srcDir));
+                        });
+                    }, failed.bind(null, done, 'root.getFile - Error creating file : ' + file1));
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+            });
+            it("file.spec.75 copyTo: directory onto existing file", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var file1 = "entry.copy.dof.file1",
+                srcDir = "entry.copy.dof.srcDir",
+                dirPath = joinURL(root.fullPath, srcDir),
+                filePath = joinURL(root.fullPath, file1);
+                // create a new directory entry to kick off it
+                createDirectory(srcDir, function (entry) {
+                    // create file
+                    root.getFile(file1, {
+                        create : true
+                    }, function () {
+                        // copy directory onto file
+                        entry.copyTo(root, file1, succeed.bind(null, done, 'entry.copyTo - Unexpected success callback, it should not copy : \n' + srcDir + ' into root directory renamed as ' + file1 + '\n' + file1 + ' file already exists'), function (error) {
+                            expect(error).toBeDefined();
+                            expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                            //check that original dir still exists
+                            root.getDirectory(srcDir, {
+                                create : false
+                            }, function (dirEntry) {
+                                // returning confirms existence so just check fullPath entry
+                                expect(dirEntry).toBeDefined();
+                                expect(dirEntry.fullPath).toCanonicallyMatch(dirPath);
+                                // test that original file still exists
+                                root.getFile(file1, {
+                                    create : false
+                                }, function (fileEntry) {
+                                    expect(fileEntry).toBeDefined();
+                                    expect(fileEntry.fullPath).toCanonicallyMatch(filePath);
+                                    // cleanup
+                                    deleteEntry(file1, function () {
+                                        deleteEntry(srcDir, done);
+                                    });
+                                }, failed.bind(null, done, 'root.getFile - Error getting file, making sure that original file still exists: ' + file1));
+                            }, failed.bind(null, done, 'root.getDirectory - Error getting directory, making sure that original directory still exists: ' + srcDir));
+                        });
+                    }, failed.bind(null, done, 'root.getFile - Error creating file : ' + file1));
+                }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+            });
+            it("file.spec.76 moveTo: directory onto directory that is not empty", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var srcDir = "entry.move.dod.srcDir",
+                dstDir = "entry.move.dod.dstDir",
+                subDir = "subDir",
+                srcPath = joinURL(root.fullPath, srcDir),
+                dstPath = joinURL(joinURL(root.fullPath, dstDir), subDir);
+                // ensure destination directory is cleaned up before it
+                deleteEntry(dstDir, function () {
+                    // create a new file entry to kick off it
+                    createDirectory(srcDir, function (entry) {
+                        // create top level directory
+                        root.getDirectory(dstDir, {
+                            create : true
+                        }, function (directory) {
+                            // create sub-directory
+                            directory.getDirectory(subDir, {
+                                create : true
+                            }, function () {
+                                // move srcDir onto dstDir (not empty)
+                                entry.moveTo(root, dstDir, succeed.bind(null, done, 'entry.moveTo - Unexpected success callback, it should not copy : \n' + srcDir + ' into root directory renamed as ' + dstDir + '\n' + dstDir + ' directory already exists'), function (error) {
+                                    expect(error).toBeDefined();
+                                    expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                                    // making sure destination directory still exists
+                                    directory.getDirectory(subDir, {
+                                        create : false
+                                    }, function (dirEntry) {
+                                        // returning confirms existence so just check fullPath entry
+                                        expect(dirEntry).toBeDefined();
+                                        expect(dirEntry.fullPath).toCanonicallyMatch(dstPath);
+                                        // making sure source directory exists
+                                        root.getDirectory(srcDir, {
+                                            create : false
+                                        }, function (srcEntry) {
+                                            expect(srcEntry).toBeDefined();
+                                            expect(srcEntry.fullPath).toCanonicallyMatch(srcPath);
+                                            // cleanup
+                                            deleteEntry(srcDir, function () {
+                                                deleteEntry(dstDir, done);
+                                            });
+                                        }, failed.bind(null, done, 'root.getDirectory - Error getting directory, making sure that original directory still exists: ' + srcDir));
+                                    }, failed.bind(null, done, 'directory.getDirectory - Error getting directory, making sure that original directory still exists: ' + subDir));
+                                });
+                            }, failed.bind(null, done, 'directory.getDirectory - Error creating directory : ' + subDir));
+                        }, failed.bind(null, done, 'directory.getDirectory - Error creating directory : ' + subDir));
+                    }, failed.bind(null, done, 'createDirectory - Error creating directory : ' + srcDir));
+                }, failed.bind(null, done, 'deleteEntry - Error removing directory : ' + dstDir));
+            });
+            it("file.spec.77 moveTo: file replace existing file", function (done) {
+                var file1 = "entry.move.frf.file1",
+                file2 = "entry.move.frf.file2",
+                file1Path = joinURL(root.fullPath, file1),
+                file2Path = joinURL(root.fullPath, file2);
+                // create a new directory entry to kick off it
+                createFile(file1, function (entry) {
+                    // create file
+                    root.getFile(file2, {
+                        create : true
+                    }, function () {
+                        // replace file2 with file1
+                        entry.moveTo(root, file2, function (entry2) {
+                            expect(entry2).toBeDefined();
+                            expect(entry2.isFile).toBe(true);
+                            expect(entry2.isDirectory).toBe(false);
+                            expect(entry2.fullPath).toCanonicallyMatch(file2Path);
+                            expect(entry2.name).toCanonicallyMatch(file2);
+                            // old file should not exists
+                            root.getFile(file1, {
+                                create : false
+                            }, succeed.bind(null, done, 'root.getFile - Unexpected success callback, file: ' + file1 + ' should not exists'), function (error) {
+                                expect(error).toBeDefined();
+                                expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                                // test that new file exists
+                                root.getFile(file2, {
+                                    create : false
+                                }, function (fileEntry) {
+                                    expect(fileEntry).toBeDefined();
+                                    expect(fileEntry.fullPath).toCanonicallyMatch(file2Path);
+                                    // cleanup
+                                    deleteEntry(file1, function () {
+                                        deleteEntry(file2, done);
+                                    });
+                                }, failed.bind(null, done, 'root.getFile - Error getting moved file: ' + file2));
+                            });
+                        }, failed.bind(null, done, 'entry.moveTo - Error moving file : ' + file1 + ' to root as: ' + file2));
+                    }, failed.bind(null, done, 'root.getFile - Error creating file: ' + file2));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + file1));
+            });
+            it("file.spec.78 moveTo: directory replace empty directory", function (done) {
+                if (isIndexedDBShim) {
+                    /* `copyTo` and `moveTo` functions do not support directories (Firefox, IE) */
+                    pending();
+                }
+
+                var file1 = "file1",
+                srcDir = "entry.move.drd.srcDir",
+                dstDir = "entry.move.drd.dstDir",
+                srcPath = joinURL(root.fullPath, srcDir),
+                dstPath = joinURL(root.fullPath, dstDir),
+                filePath = dstPath + '/' + file1;
+                // ensure destination directory is cleaned up before it
+                deleteEntry(dstDir, function () {
+                    // create a new directory entry to kick off it
+                    createDirectory(srcDir, function (directory) {
+                        // create a file within source directory
+                        directory.getFile(file1, {
+                            create : true
+                        }, function () {
+                            // create destination directory
+                            root.getDirectory(dstDir, {
+                                create : true
+                            }, function () {
+                                // move srcDir to dstDir
+                                directory.moveTo(root, dstDir, function (dirEntry) {
+                                    expect(dirEntry).toBeDefined();
+                                    expect(dirEntry.isFile).toBe(false);
+                                    expect(dirEntry.isDirectory).toBe(true);
+                                    expect(dirEntry.fullPath).toCanonicallyMatch(dstPath);
+                                    expect(dirEntry.name).toCanonicallyMatch(dstDir);
+                                    // check that old directory contents have been moved
+                                    dirEntry.getFile(file1, {
+                                        create : false
+                                    }, function (fileEntry) {
+                                        expect(fileEntry).toBeDefined();
+                                        expect(fileEntry.fullPath).toCanonicallyMatch(filePath);
+                                        // check that old directory no longer exists
+                                        root.getDirectory(srcDir, {
+                                            create : false
+                                        }, succeed.bind(null, done, 'root.getDirectory - Unexpected success callback, directory: ' + srcDir + ' should not exists'), function (error) {
+                                            expect(error).toBeDefined();
+                                            expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                                            // cleanup
+                                            deleteEntry(srcDir, function () {
+                                                deleteEntry(dstDir, done);
+                                            });
+                                        });
+                                    }, failed.bind(null, done, 'dirEntry.getFile - Error getting moved file: ' + file1));
+                                }, failed.bind(null, done, 'entry.moveTo - Error moving directory : ' + srcDir + ' to root as: ' + dstDir));
+                            }, failed.bind(null, done, 'root.getDirectory - Error creating directory: ' + dstDir));
+                        }, failed.bind(null, done, 'root.getFile - Error creating file: ' + file1));
+                    }, failed.bind(null, done, 'createDirectory - Error creating directory: ' + srcDir));
+                }, failed.bind(null, done, 'deleteEntry - Error removing directory : ' + dstDir));
+            });
+            it("file.spec.79 moveTo: directory that does not exist", function (done) {
+
+                var file1 = "entry.move.dnf.file1",
+                dstDir = "entry.move.dnf.dstDir",
+                dstPath = joinURL(root.fullPath, dstDir);
+                // create a new file entry to kick off it
+                createFile(file1, function (entry) {
+                    // move file to directory that does not exist
+                    directory = new DirectoryEntry();
+                    directory.filesystem = root.filesystem;
+                    directory.fullPath = dstPath;
+                    entry.moveTo(directory, null, succeed.bind(null, done, 'entry.moveTo - Unexpected success callback, parent directory: ' + dstPath + ' should not exists'), function (error) {
+                        expect(error).toBeDefined();
+                        if (isChrome) {
+                            /*INVALID_MODIFICATION_ERR (code: 9) is thrown instead of NOT_FOUND_ERR(code: 1)
+                            on trying to moveTo directory that does not exist.*/
+                            expect(error).toBeFileError(FileError.INVALID_MODIFICATION_ERR);
+                        } else {
+                            expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                        }
+                        // cleanup
+                        deleteEntry(file1, done);
+                    });
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + file1));
+            });
+            it("file.spec.80 moveTo: invalid target name", function (done) {
+                if (isBrowser) {
+                    /*The plugin does not follow ["8.3 Naming restrictions"]
+                    (http://www.w3.org/TR/2011/WD-file-system-api-20110419/#naming-restrictions*/
+                    pending();
+                }
+
+                var file1 = "entry.move.itn.file1",
+                file2 = "bad:file:name";
+                // create a new file entry to kick off it
+                createFile(file1, function (entry) {
+                    // move file1 to file2
+                    entry.moveTo(root, file2, succeed.bind(null, done, 'entry.moveTo - Unexpected success callback, : ' + file1 + ' to root as: ' + file2), function (error) {
+                        expect(error).toBeDefined();
+                        expect(error).toBeFileError(FileError.ENCODING_ERR);
+                        // cleanup
+                        deleteEntry(file1, done);
+                    });
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + file1));
+            });
+        });
+        //Entry
+        describe('FileReader', function () {
+            it("file.spec.81 should have correct methods", function () {
+                var reader = new FileReader();
+                expect(reader).toBeDefined();
+                expect(typeof reader.readAsBinaryString).toBe('function');
+                expect(typeof reader.readAsDataURL).toBe('function');
+                expect(typeof reader.readAsText).toBe('function');
+                expect(typeof reader.readAsArrayBuffer).toBe('function');
+                expect(typeof reader.abort).toBe('function');
+                expect(reader.result).toBe(null);
+            });
+        });
+        //FileReader
+        describe('Read method', function () {
+            it("file.spec.82 should error out on non-existent file", function (done) {
+                var fileName = cordova.platformId === 'windowsphone' ? root.toURL() + "/" + "somefile.txt" : "somefile.txt",
+                verifier = function (evt) {
+                    expect(evt).toBeDefined();
+                    expect(evt.target.error).toBeFileError(FileError.NOT_FOUND_ERR);
+                    done();
+                };
+                root.getFile(fileName, {
+                    create : true
+                }, function (entry) {
+                    entry.file(function (file) {
+                        deleteEntry(fileName, function () {
+                            //Create FileReader
+                            var reader = new FileReader();
+                            reader.onerror = verifier;
+                            reader.onload = succeed.bind(null, done, 'reader.onload - Unexpected success callback, file: ' + fileName + ' it should not exists');
+                            reader.readAsText(file);
+                        }, failed.bind(null, done, 'deleteEntry - Error removing file: ' + fileName));
+                    }, failed.bind(null, done, 'entry.file - Error reading file: ' + fileName));
+                }, failed.bind(null, done, 'root.getFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.83 should be able to read native blob objects", function (done) {
+                // Skip test if blobs are not supported (e.g.: Android 2.3).
+                if (typeof window.Blob == 'undefined' || typeof window.Uint8Array == 'undefined') {
+                    expect(true).toFailWithMessage('Platform does not supported this feature');
+                    done();
+                }
+                var contents = 'asdf';
+                var uint8Array = new Uint8Array(contents.length);
+                for (var i = 0; i < contents.length; ++i) {
+                    uint8Array[i] = contents.charCodeAt(i);
+                }
+                var Builder = window.BlobBuilder || window.WebKitBlobBuilder;
+                var blob;
+                if (Builder) {
+                    var builder = new Builder();
+                    builder.append(uint8Array.buffer);
+                    builder.append(contents);
+                    blob = builder.getBlob("text/plain");
+                } else {
+                    try {
+                        // iOS 6 does not support Views, so pass in the buffer.
+                        blob = new Blob([uint8Array.buffer, contents]);
+                    } catch (e) {
+                        // Skip the test if we can't create a blob (e.g.: iOS 5).
+                        if (e instanceof TypeError) {
+                            expect(true).toFailWithMessage('Platform does not supported this feature');
+                            done();
+                        }
+                        throw e;
+                    }
+                }
+                var verifier = function (evt) {
+                    expect(evt).toBeDefined();
+                    expect(evt.target.result).toBe('asdfasdf');
+                    done();
+                };
+                var reader = new FileReader();
+                reader.onloadend = verifier;
+                reader.readAsText(blob);
+            });
+            function writeDummyFile(writeBinary, callback, done, fileContents) {
+                var fileName = "dummy.txt",
+                fileEntry = null,
+                // use default string if file data is not provided
+                fileData = fileContents !== undefined ? fileContents :
+                    '\u20AC\xEB - There is an exception to every rule. Except this one.',
+                fileDataAsBinaryString = fileContents !== undefined ? fileContents :
+                    '\xe2\x82\xac\xc3\xab - There is an exception to every rule. Except this one.',
+                createWriter = function (fe) {
+                    fileEntry = fe;
+                    fileEntry.createWriter(writeFile, failed.bind(null, done, 'fileEntry.createWriter - Error reading file: ' + fileName));
+                }, // writes file and reads it back in
+                writeFile = function (writer) {
+                    writer.onwriteend = function () {
+                        fileEntry.file(function (f) {
+                            callback(fileEntry, f, fileData, fileDataAsBinaryString);
+                        }, failed.bind(null, done, 'writer.onwriteend - Error writing data on file: ' + fileName));
+                    };
+                    writer.write(fileData);
+                };
+                fileData += writeBinary ? 'bin:\x01\x00' : '';
+                fileDataAsBinaryString += writeBinary ? 'bin:\x01\x00' : '';
+                // create a file, write to it, and read it in again
+                createFile(fileName, createWriter, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            }
+            function runReaderTest(funcName, writeBinary, done, verifierFunc, sliceStart, sliceEnd, fileContents) {
+                writeDummyFile(writeBinary, function (fileEntry, file, fileData, fileDataAsBinaryString) {
+                    var verifier = function (evt) {
+                        expect(evt).toBeDefined();
+                        verifierFunc(evt, fileData, fileDataAsBinaryString);
+                    };
+                    var reader = new FileReader();
+                    reader.onload = verifier;
+                    reader.onerror = failed.bind(null, done, 'reader.onerror - Error reading file: ' + file + ' using function: ' + funcName);
+                    if (sliceEnd !== undefined) {
+                        // 'type' is specified so that is will be preserved in the resulting file:
+                        // http://www.w3.org/TR/FileAPI/#slice-method-algo -> "6.4.1. The slice method" -> 4. A), 6. c)
+                        file = file.slice(sliceStart, sliceEnd, file.type);
+                    } else if (sliceStart !== undefined) {
+                        file = file.slice(sliceStart, file.size, file.type);
+                    }
+                    reader[funcName](file);
+                }, done, fileContents);
+            }
+            function arrayBufferEqualsString(ab, str) {
+                var buf = new Uint8Array(ab);
+                var match = buf.length == str.length;
+                for (var i = 0; match && i < buf.length; i++) {
+                    match = buf[i] == str.charCodeAt(i);
+                }
+                return match;
+            }
+            it("file.spec.84 should read file properly, readAsText", function (done) {
+                runReaderTest('readAsText', false, done, function (evt, fileData, fileDataAsBinaryString) {
+                    expect(evt.target.result).toBe(fileData);
+                    done();
+                });
+            });
+            it("file.spec.84.1 should read JSON file properly, readAsText", function (done) {
+                var testObject = {key1: "value1", key2: 2};
+                runReaderTest('readAsText', false, done, function (evt, fileData, fileDataAsBinaryString) {
+                    expect(evt.target.result).toEqual(JSON.stringify(testObject));
+                    done();
+                }, undefined, undefined, JSON.stringify(testObject));
+            });
+            it("file.spec.85 should read file properly, Data URI", function (done) {
+                runReaderTest('readAsDataURL', true, done, function (evt, fileData, fileDataAsBinaryString) {
+                    /* `readAsDataURL` function is supported, but the mediatype in Chrome depends on entry name extension,
+                        mediatype in IE is always empty (which is the same as `text-plain` according the specification),
+                        the mediatype in Firefox is always `application/octet-stream`.
+                        For example, if the content is `abcdefg` then Firefox returns `data:application/octet-stream;base64,YWJjZGVmZw==`,
+                        IE returns `data:;base64,YWJjZGVmZw==`, Chrome returns `data:<mediatype depending on extension of entry name>;base64,YWJjZGVmZw==`. */
+                    expect(evt.target.result).toBeDataUrl();
+
+                    //The atob function it is completely ignored during mobilespec execution, besides the returned object: evt
+                    //it is encoded and the atob function is aimed to decode a string. Even with btoa (encode) the function it gets stucked
+                    //because of the Unicode characters that contains the fileData object.
+                    //Issue reported at JIRA with all the details: CB-7095
+
+                    //expect(evt.target.result.slice(23)).toBe(atob(fileData));
+
+                    done();
+                });
+            });
+            it("file.spec.86 should read file properly, readAsBinaryString", function (done) {
+                if (isIE) {
+                    /*`readAsBinaryString` function is not supported by IE and has not the stub.*/
+                    pending();
+                }
+
+                runReaderTest('readAsBinaryString', true, done, function (evt, fileData, fileDataAsBinaryString) {
+                    expect(evt.target.result).toBe(fileDataAsBinaryString);
+                    done();
+                });
+            });
+            it("file.spec.87 should read file properly, readAsArrayBuffer", function (done) {
+                // Skip test if ArrayBuffers are not supported (e.g.: Android 2.3).
+                if (typeof window.ArrayBuffer == 'undefined') {
+                    expect(true).toFailWithMessage('Platform does not supported this feature');
+                    done();
+                }
+                runReaderTest('readAsArrayBuffer', true, done, function (evt, fileData, fileDataAsBinaryString) {
+                    expect(arrayBufferEqualsString(evt.target.result, fileDataAsBinaryString)).toBe(true);
+                    done();
+                });
+            });
+            it("file.spec.88 should read sliced file: readAsText", function (done) {
+                runReaderTest('readAsText', false, done, function (evt, fileData, fileDataAsBinaryString) {
+                    expect(evt.target.result).toBe(fileDataAsBinaryString.slice(10, 40));
+                    done();
+                }, 10, 40);
+            });
+            it("file.spec.89 should read sliced file: slice past eof", function (done) {
+                runReaderTest('readAsText', false, done, function (evt, fileData, fileDataAsBinaryString) {
+                    expect(evt.target.result).toBe(fileData.slice(-5, 9999));
+                    done();
+                }, -5, 9999);
+            });
+            it("file.spec.90 should read sliced file: slice to eof", function (done) {
+                runReaderTest('readAsText', false, done, function (evt, fileData, fileDataAsBinaryString) {
+                    expect(evt.target.result).toBe(fileData.slice(-5));
+                    done();
+                }, -5);
+            });
+            it("file.spec.91 should read empty slice", function (done) {
+                runReaderTest('readAsText', false, done, function (evt, fileData, fileDataAsBinaryString) {
+                    expect(evt.target.result).toBe('');
+                    done();
+                }, 0, 0);
+            });
+            it("file.spec.92 should read sliced file properly, readAsDataURL", function (done) {
+                runReaderTest('readAsDataURL', true, done, function (evt, fileData, fileDataAsBinaryString) {
+                    /* `readAsDataURL` function is supported, but the mediatype in Chrome depends on entry name extension,
+                        mediatype in IE is always empty (which is the same as `text-plain` according the specification),
+                        the mediatype in Firefox is always `application/octet-stream`.
+                        For example, if the content is `abcdefg` then Firefox returns `data:application/octet-stream;base64,YWJjZGVmZw==`,
+                        IE returns `data:;base64,YWJjZGVmZw==`, Chrome returns `data:<mediatype depending on extension of entry name>;base64,YWJjZGVmZw==`. */
+                    expect(evt.target.result).toBeDataUrl();
+
+                    //The atob function it is completely ignored during mobilespec execution, besides the returned object: evt
+                    //it is encoded and the atob function is aimed to decode a string. Even with btoa (encode) the function it gets stucked
+                    //because of the Unicode characters that contains the fileData object.
+                    //Issue reported at JIRA with all the details: CB-7095
+
+                    //expect(evt.target.result.slice(23)).toBe(atob(fileDataAsBinaryString.slice(10, -3)));
+
+                    done();
+                }, 10, -3);
+            });
+            it("file.spec.93 should read sliced file properly, readAsBinaryString", function (done) {
+                if (isIE) {
+                    /*`readAsBinaryString` function is not supported by IE and has not the stub.*/
+                    pending();
+                }
+
+                runReaderTest('readAsBinaryString', true, done, function (evt, fileData, fileDataAsBinaryString) {
+                    expect(evt.target.result).toBe(fileDataAsBinaryString.slice(-10, -5));
+                    done();
+                }, -10, -5);
+            });
+            it("file.spec.94 should read sliced file properly, readAsArrayBuffer", function (done) {
+                // Skip test if ArrayBuffers are not supported (e.g.: Android 2.3).
+                if (typeof window.ArrayBuffer == 'undefined') {
+                    expect(true).toFailWithMessage('Platform does not supported this feature');
+                    done();
+                }
+                runReaderTest('readAsArrayBuffer', true, done, function (evt, fileData, fileDataAsBinaryString) {
+                    expect(arrayBufferEqualsString(evt.target.result, fileDataAsBinaryString.slice(0, -1))).toBe(true);
+                    done();
+                }, 0, -1);
+            });
+        });
+        //Read method
+        describe('FileWriter', function () {
+            it("file.spec.95 should have correct methods", function (done) {
+                // retrieve a FileWriter object
+                var fileName = "writer.methods";
+                // FileWriter
+                root.getFile(fileName, {
+                    create : true
+                }, function (fileEntry) {
+                    fileEntry.createWriter(function (writer) {
+                        expect(writer).toBeDefined();
+                        expect(typeof writer.write).toBe('function');
+                        expect(typeof writer.seek).toBe('function');
+                        expect(typeof writer.truncate).toBe('function');
+                        expect(typeof writer.abort).toBe('function');
+                        // cleanup
+                        deleteFile(fileName, done);
+                    }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                }, failed.bind(null, done, 'root.getFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.96 should be able to write and append to file, createWriter", function (done) {
+                var fileName = "writer.append.createWriter", // file content
+                content = "There is an exception to every rule.", // for checkin file length
+                exception = " Except this one.",
+                length = content.length;
+                // create file, then write and append to it
+                createFile(fileName, function (fileEntry) {
+                    // writes initial file content
+                    fileEntry.createWriter(function (writer) {
+                        //Verifiers declaration
+                        var verifier = function (evt) {
+                            expect(writer.length).toBe(length);
+                            expect(writer.position).toBe(length);
+                            // Append some more data
+                            writer.onwriteend = secondVerifier;
+                            length += exception.length;
+                            writer.seek(writer.length);
+                            writer.write(exception);
+                        },
+                        secondVerifier = function (evt) {
+                            expect(writer.length).toBe(length);
+                            expect(writer.position).toBe(length);
+                            var reader = new FileReader();
+                            reader.onloadend = thirdVerifier;
+                            reader.onerror = failed.bind(null, done, 'reader.onerror - Error reading file: ' + fileName);
+                            fileEntry.file(function(f){reader.readAsText(f);});
+                        },
+                        thirdVerifier = function (evt) {
+                            expect(evt.target.result).toBe(content+exception);
+                            // cleanup
+                            deleteFile(fileName, done);
+                        };
+                        //Write process
+                        writer.onwriteend = verifier;
+                        writer.write(content);
+                    }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.97 should be able to write and append to file, File object", function (done) {
+                var fileName = "writer.append.File", // file content
+                content = "There is an exception to every rule.", // for checkin file length
+                exception = " Except this one.",
+                length = content.length;
+                root.getFile(fileName, {
+                    create : true
+                }, function (fileEntry) {
+                    fileEntry.createWriter(function (writer) {
+                        //Verifiers declaration
+                        var verifier = function () {
+                            expect(writer.length).toBe(length);
+                            expect(writer.position).toBe(length);
+                            // Append some more data
+                            writer.onwriteend = secondVerifier;
+                            length += exception.length;
+                            writer.seek(writer.length);
+                            writer.write(exception);
+                        },
+                        secondVerifier = function () {
+                            expect(writer.length).toBe(length);
+                            expect(writer.position).toBe(length);
+                            var reader = new FileReader();
+                            reader.onloadend = thirdVerifier;
+                            reader.onerror = failed.bind(null, done, 'reader.onerror - Error reading file: ' + fileName);
+                            fileEntry.file(function(f){reader.readAsText(f);});
+                        },
+                        thirdVerifier = function (evt) {
+                            expect(evt.target.result).toBe(content+exception);
+                            // cleanup
+                            deleteFile(fileName, done);
+                        };
+                        //Write process
+                        writer.onwriteend = verifier;
+                        writer.write(content);
+                    }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                }, failed.bind(null, done, 'root.getFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.98 should be able to seek to the middle of the file and write more data than file.length", function (done) {
+                var fileName = "writer.seek.write", // file content
+                content = "This is our sentence.", // for checking file length
+                exception = "newer sentence.",
+                length = content.length;
+                // create file, then write and append to it
+                createFile(fileName, function (fileEntry) {
+                    fileEntry.createWriter(function (writer) {
+                        //Verifiers declaration
+                        var verifier = function (evt) {
+                            expect(writer.length).toBe(length);
+                            expect(writer.position).toBe(length);
+                            // Append some more data
+                            writer.onwriteend = secondVerifier;
+                            length = 12 + exception.length;
+                            writer.seek(12);
+                            writer.write(exception);
+                        },
+                        secondVerifier = function (evt) {
+                            expect(writer.length).toBe(length);
+                            expect(writer.position).toBe(length);
+                            var reader = new FileReader();
+                            reader.onloadend = thirdVerifier;
+                            reader.onerror = failed.bind(null, done, 'reader.onerror - Error reading file: ' + fileName);
+                            fileEntry.file(function(f){reader.readAsText(f);});
+                        },
+                        thirdVerifier = function (evt) {
+                            expect(evt.target.result).toBe(content.substr(0,12)+exception);
+                            // cleanup
+                            deleteFile(fileName, done);
+                        };
+                        //Write process
+                        writer.onwriteend = verifier;
+                        writer.write(content);
+                    }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.99 should be able to seek to the middle of the file and write less data than file.length", function (done) {
+                if (isChrome) {
+                    /* Chrome (re)writes as follows: "This is our sentence." -> "This is new.sentence.",
+                       i.e. the length is not being changed from content.length and writer length will be equal 21 */
+                    pending();
+                }
+
+                var fileName = "writer.seek.write2", // file content
+                content = "This is our sentence.", // for checking file length
+                exception = "new.",
+                length = content.length;
+                // create file, then write and append to it
+                createFile(fileName, function (fileEntry) {
+                    fileEntry.createWriter(function (writer) {
+                        // Verifiers declaration
+                        var verifier = function (evt) {
+                            expect(writer.length).toBe(length);
+                            expect(writer.position).toBe(length);
+                            // Append some more data
+                            writer.onwriteend = secondVerifier;
+                            length = 8 + exception.length;
+                            writer.seek(8);
+                            writer.write(exception);
+                        },
+                        secondVerifier = function (evt) {
+                            expect(writer.length).toBe(length);
+                            expect(writer.position).toBe(length);
+                            var reader = new FileReader();
+                            reader.onloadend = thirdVerifier;
+                            reader.onerror = failed.bind(null, done, 'reader.onerror - Error reading file: ' + fileName);
+                            fileEntry.file(function(f){reader.readAsText(f);});
+                        },
+                        thirdVerifier = function (evt) {
+                            expect(evt.target.result).toBe(content.substr(0,8)+exception);
+                            // cleanup
+                            deleteFile(fileName, done);
+                        };
+                        //Write process
+                        writer.onwriteend = verifier;
+                        writer.write(content);
+                    }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.100 should be able to write XML data", function (done) {
+                var fileName = "writer.xml", // file content
+                content = '<?xml version="1.0" encoding="UTF-8"?>\n<test prop="ack">\nData\n</test>\n', // for testing file length
+                length = content.length;
+                // creates file, then write XML data
+                createFile(fileName, function (fileEntry) {
+                    fileEntry.createWriter(function (writer) {
+                        //Verifier content
+                        var verifier = function (evt) {
+                            expect(writer.length).toBe(length);
+                            expect(writer.position).toBe(length);
+                            // cleanup
+                            deleteFile(fileName, done);
+                        };
+                        //Write process
+                        writer.onwriteend = verifier;
+                        writer.write(content);
+                    }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.101 should be able to write JSON data", function (done) {
+                var fileName = "writer.json", // file content
+                content = '{ "name": "Guy Incognito", "email": "here@there.com" }', // for testing file length
+                length = content.length;
+                // creates file, then write JSON content
+                createFile(fileName, function (fileEntry) {
+                    fileEntry.createWriter(function (writer) {
+                        //Verifier declaration
+                        var verifier = function (evt) {
+                            expect(writer.length).toBe(length);
+                            expect(writer.position).toBe(length);
+                            // cleanup
+                            deleteFile(fileName, done);
+                        };
+                        //Write process
+                        writer.onwriteend = verifier;
+                        writer.write(content);
+                    }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.102 should be able to seek", function (done) {
+                var fileName = "writer.seek", // file content
+                content = "There is an exception to every rule. Except this one.", // for testing file length
+                length = content.length;
+                // creates file, then write JSON content
+                createFile(fileName, function (fileEntry) {
+                    // writes file content and tests writer.seek
+                    fileEntry.createWriter(function (writer) {
+                        //Verifier declaration
+                        var verifier = function () {
+                            expect(writer.position).toBe(length);
+                            writer.seek(-5);
+                            expect(writer.position).toBe(length - 5);
+                            writer.seek(length + 100);
+                            expect(writer.position).toBe(length);
+                            writer.seek(10);
+                            expect(writer.position).toBe(10);
+                            // cleanup
+                            deleteFile(fileName, done);
+                        };
+                        //Write process
+                        writer.onwriteend = verifier;
+                        writer.seek(-100);
+                        expect(writer.position).toBe(0);
+                        writer.write(content);
+                    }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.103 should be able to truncate", function (done) {
+                if (isIndexedDBShim) {
+                    /* `abort` and `truncate` functions are not supported (Firefox, IE) */
+                    pending();
+                }
+
+                var fileName = "writer.truncate",
+                content = "There is an exception to every rule. Except this one.";
+                // creates file, writes to it, then truncates it
+                createFile(fileName, function (fileEntry) {
+                    fileEntry.createWriter(function (writer) {
+                        // Verifier declaration
+                        var verifier = function () {
+                            expect(writer.length).toBe(36);
+                            expect(writer.position).toBe(36);
+                            // cleanup
+                            deleteFile(fileName, done);
+                        };
+                        //Write process
+                        writer.onwriteend = function () {
+                            //Truncate process after write
+                            writer.onwriteend = verifier;
+                            writer.truncate(36);
+                        };
+                        writer.write(content);
+                    }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.104 should be able to write binary data from an ArrayBuffer", function (done) {
+                // Skip test if ArrayBuffers are not supported (e.g.: Android 2.3).
+                if (typeof window.ArrayBuffer == 'undefined') {
+                    expect(true).toFailWithMessage('Platform does not supported this feature');
+                    done();
+                    return;
+                }
+                var fileName = "bufferwriter.bin", // file content
+                data = new ArrayBuffer(32),
+                dataView = new Int8Array(data), // for verifying file length
+                length = 32;
+                for (i = 0; i < dataView.length; i++) {
+                    dataView[i] = i;
+                }
+                // creates file, then write content
+                createFile(fileName, function (fileEntry) {
+                    // writes file content
+                    fileEntry.createWriter(function (writer) {
+                        //Verifier declaration
+                        var verifier = function () {
+                            expect(writer.length).toBe(length);
+                            expect(writer.position).toBe(length);
+                            // cleanup
+                            deleteFile(fileName, done);
+                        };
+                        //Write process
+                        writer.onwriteend = verifier;
+                        writer.write(data);
+                    }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.105 should be able to write binary data from a Blob", function (done) {
+                // Skip test if Blobs are not supported (e.g.: Android 2.3).
+                if ((typeof window.Blob == 'undefined' && typeof window.WebKitBlobBuilder == 'undefined') || typeof window.ArrayBuffer == 'undefined') {
+                    expect(true).toFailWithMessage('Platform does not supported this feature');
+                    done();
+                    return;
+                }
+                var fileName = "blobwriter.bin", // file content
+                data = new ArrayBuffer(32),
+                dataView = new Int8Array(data),
+                blob, // for verifying file length
+                length = 32;
+                for (i = 0; i < dataView.length; i++) {
+                    dataView[i] = i;
+                }
+                try {
+                    // Mobile Safari: Use Blob constructor
+                    blob = new Blob([data], {
+                            "type" : "application/octet-stream"
+                        });
+                } catch (e) {
+                    if (window.WebKitBlobBuilder) {
+                        // Android Browser: Use deprecated BlobBuilder
+                        var builder = new WebKitBlobBuilder();
+                        builder.append(data);
+                        blob = builder.getBlob('application/octet-stream');
+                    } else {
+                        // We have no way defined to create a Blob, so fail
+                        fail();
+                    }
+                }
+                if (typeof blob !== 'undefined') {
+                    // creates file, then write content
+                    createFile(fileName, function (fileEntry) {
+                        fileEntry.createWriter(function (writer) {
+                            //Verifier declaration
+                            var verifier = function () {
+                                expect(writer.length).toBe(length);
+                                expect(writer.position).toBe(length);
+                                // cleanup
+                                deleteFile(fileName, done);
+                            };
+                            //Write process
+                            writer.onwriteend = verifier;
+                            writer.write(blob);
+                        }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                    }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+                }
+            });
+            it("file.spec.106 should be able to write a File to a FileWriter", function (done) {
+                var dummyFileName = 'dummy.txt',
+                outputFileName = 'verify.txt',
+                dummyFileText = 'This text should be written to two files',
+                verifier = function (outputFileWriter) {
+                    expect(outputFileWriter.length).toBe(dummyFileText.length);
+                    expect(outputFileWriter.position).toBe(dummyFileText.length);
+                    deleteFile(outputFileName, done);
+                },
+                writeFile = function (fileName, fileData, win) {
+                    var theWriter,
+                    filePath = joinURL(root.fullPath, fileName), // writes file content to new file
+                    write_file = function (fileEntry) {
+                        writerEntry = fileEntry;
+                        fileEntry.createWriter(function (writer) {
+                            theWriter = writer;
+                            writer.onwriteend = function (ev) {
+                                if (typeof fileData.length !== "undefined") {
+                                    expect(theWriter.length).toBe(fileData.length);
+                                    expect(theWriter.position).toBe(fileData.length);
+                                }
+                                win(theWriter);
+                            };
+                            writer.onerror = failed.bind(null, done, 'writer.onerror - Error writing content on file: ' + fileName);
+                            writer.write(fileData);
+                        }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                    };
+                    createFile(fileName, write_file, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+                },
+                openFile = function (fileName, callback) {
+                    root.getFile(fileName, {
+                        create : false
+                    }, function (fileEntry) {
+                        fileEntry.file(callback, failed.bind(null, done, 'fileEntry.file - Error reading file using fileEntry: ' + fileEntry.name));
+                    }, failed.bind(null, done, 'root.getFile - Error getting file: ' + fileName));
+                };
+                writeFile(dummyFileName, dummyFileText, function (dummyFileWriter) {
+                    openFile(dummyFileName, function (file) {
+                        writeFile(outputFileName, file, verifier);
+                    });
+                });
+            });
+            it("file.spec.107 should be able to write a sliced File to a FileWriter", function (done) {
+                var dummyFileName = 'dummy2.txt',
+                outputFileName = 'verify2.txt',
+                dummyFileText = 'This text should be written to two files',
+                verifier = function (outputFileWriter) {
+                    expect(outputFileWriter.length).toBe(10);
+                    expect(outputFileWriter.position).toBe(10);
+                    deleteFile(outputFileName, done);
+                },
+                writeFile = function (fileName, fileData, win) {
+                    var theWriter,
+                    filePath = joinURL(root.fullPath, fileName), // writes file content to new file
+                    write_file = function (fileEntry) {
+                        writerEntry = fileEntry;
+                        fileEntry.createWriter(function (writer) {
+                            theWriter = writer;
+                            writer.onwriteend = function (ev) {
+                                if (typeof fileData.length !== "undefined") {
+                                    expect(theWriter.length).toBe(fileData.length);
+                                    expect(theWriter.position).toBe(fileData.length);
+                                }
+                                win(theWriter);
+                            };
+                            writer.onerror = failed.bind(null, done, 'writer.onerror - Error writing content on file: ' + fileName);
+                            writer.write(fileData);
+                        }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                    };
+                    createFile(fileName, write_file, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+                },
+                openFile = function (fileName, callback) {
+                    root.getFile(fileName, {
+                        create : false
+                    }, function (fileEntry) {
+                        fileEntry.file(callback, failed.bind(null, done, 'fileEntry.file - Error reading file using fileEntry: ' + fileEntry.name));
+                    }, failed.bind(null, done, 'root.getFile - Error getting file: ' + fileName));
+                };
+                writeFile(dummyFileName, dummyFileText, function (dummyFileWriter) {
+                    openFile(dummyFileName, function (file) {
+                        writeFile(outputFileName, file.slice(10, 20), verifier);
+                    });
+                });
+            });
+            it("file.spec.108 should be able to write binary data from a File", function (done) {
+                // Skip test if Blobs are not supported (e.g.: Android 2.3).
+                if (typeof window.Blob == 'undefined' && typeof window.WebKitBlobBuilder == 'undefined') {
+                    expect(true).toFailWithMessage('Platform does not supported this feature');
+                    done();
+                }
+                var dummyFileName = "blobwriter.bin",
+                outputFileName = 'verify.bin', // file content
+                data = new ArrayBuffer(32),
+                dataView = new Int8Array(data),
+                blob, // for verifying file length
+                length = 32,
+                verifier = function (outputFileWriter) {
+                    expect(outputFileWriter.length).toBe(length);
+                    expect(outputFileWriter.position).toBe(length);
+                    // cleanup
+                    deleteFile(outputFileName);
+                    done();
+                },
+                writeFile = function (fileName, fileData, win) {
+                    var theWriter,
+                    filePath = joinURL(root.fullPath, fileName), // writes file content to new file
+                    write_file = function (fileEntry) {
+                        writerEntry = fileEntry;
+                        fileEntry.createWriter(function (writer) {
+                            theWriter = writer;
+                            writer.onwriteend = function (ev) {
+                                if (typeof fileData.length !== "undefined") {
+                                    expect(theWriter.length).toBe(fileData.length);
+                                    expect(theWriter.position).toBe(fileData.length);
+                                }
+                                win(theWriter);
+                            };
+                            writer.onerror = failed.bind(null, done, 'writer.onerror - Error writing content on file: ' + fileName);
+                            writer.write(fileData);
+                        }, failed.bind(null, done, 'fileEntry.createWriter - Error creating writer using fileEntry: ' + fileEntry.name));
+                    };
+                    createFile(fileName, write_file, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+                },
+                openFile = function (fileName, callback) {
+                    root.getFile(fileName, {
+                        create : false
+                    }, function (fileEntry) {
+                        fileEntry.file(callback, failed.bind(null, done, 'fileEntry.file - Error reading file using fileEntry: ' + fileEntry.name));
+                    }, failed.bind(null, done, 'root.getFile - Error getting file: ' + fileName));
+                };
+                for (i = 0; i < dataView.length; i++) {
+                    dataView[i] = i;
+                }
+                try {
+                    // Mobile Safari: Use Blob constructor
+                    blob = new Blob([data], {
+                            "type" : "application/octet-stream"
+                        });
+                } catch (e) {
+                    if (window.WebKitBlobBuilder) {
+                        // Android Browser: Use deprecated BlobBuilder
+                        var builder = new WebKitBlobBuilder();
+                        builder.append(data);
+                        blob = builder.getBlob('application/octet-stream');
+                    } else {
+                        // We have no way defined to create a Blob, so fail
+                        fail();
+                    }
+                }
+                if (typeof blob !== 'undefined') {
+                    // creates file, then write content
+                    writeFile(dummyFileName, blob, function (dummyFileWriter) {
+                        openFile(dummyFileName, function (file) {
+                            writeFile(outputFileName, file, verifier);
+                        });
+                    });
+                }
+            });
+        });
+        //FileWritter
+        describe('Backwards compatibility', function () {
+            /* These specs exist to test that the File plugin can still recognize file:///
+             * URLs, and can resolve them to FileEntry and DirectoryEntry objects.
+             * They rely on an undocumented interface to File which provides absolute file
+             * paths, which are not used internally anymore.
+             * If that interface is not present, then these tests will silently succeed.
+             */
+            it("file.spec.109 should be able to resolve a file:/// URL", function (done) {
+                var localFilename = 'file.txt';
+                var originalEntry;
+                root.getFile(localFilename, {
+                    create : true
+                }, function (entry) {
+                    originalEntry = entry;
+                    /* This is an undocumented interface to File which exists only for testing
+                     * backwards compatibilty. By obtaining the raw filesystem path of the download
+                     * location, we can pass that to ft.download() to make sure that previously-stored
+                     * paths are still valid.
+                     */
+                    cordova.exec(function (localPath) {
+                        window.resolveLocalFileSystemURL("file://" + encodeURI(localPath), function (fileEntry) {
+                            expect(fileEntry.toURL()).toEqual(originalEntry.toURL());
+                            // cleanup
+                            deleteFile(localFilename);
+                            done();
+                        }, failed.bind(null, done, 'window.resolveLocalFileSystemURL - Error resolving URI: file://' + encodeURI(localPath)));
+                    }, done, 'File', '_getLocalFilesystemPath', [entry.toURL()]);
+                }, failed.bind(null, done, 'root.getFile - Error creating file: ' + localFilename));
+            });
+        });
+        //Backwards Compatibility
+        describe('Parent References', function () {
+            /* These specs verify that paths with parent references i("..") in them
+             * work correctly, and do not cause the application to crash.
+             */
+            it("file.spec.110 should not throw exception resolving parent refefences", function (done) {
+                /* This is a direct copy of file.spec.9, with the filename changed, * as reported in CB-5721.
+                 */
+                var fileName = "resolve.file.uri";
+                var dirName = "resolve.dir.uri";
+                // create a new file entry
+                createDirectory(dirName, function () {
+                    createFile(dirName+"/../" + fileName, function (entry) {
+                        // lookup file system entry
+                        window.resolveLocalFileSystemURL(entry.toURL(), function (fileEntry) {
+                            expect(fileEntry).toBeDefined();
+                            expect(fileEntry.name).toCanonicallyMatch(fileName);
+                            // cleanup
+                            deleteEntry(fileName, done);
+                        }, failed.bind(null, done, 'window.resolveLocalFileSystemURL - Error resolving URI: ' + entry.toURL()));
+                    }, failed.bind(null, done, 'createFile - Error creating file: ../' + fileName));
+                }, failed.bind(null, done, 'createDirectory - Error creating directory: ' + dirName));
+            });
+            it("file.spec.111 should not traverse above above the root directory", function (done) {
+                var fileName = "traverse.file.uri";
+                // create a new file entry
+                createFile(fileName, function (entry) {
+                    // lookup file system entry
+                    root.getFile('../' + fileName, {
+                        create : false
+                    }, function (fileEntry) {
+                        // Note: we expect this to still resolve, as the correct behaviour is to ignore the ../, not to fail out.
+                        expect(fileEntry).toBeDefined();
+                        expect(fileEntry.name).toBe(fileName);
+                        expect(fileEntry.fullPath).toCanonicallyMatch(root.fullPath +'/' + fileName);
+                        // cleanup
+                        deleteEntry(fileName, done);
+                    }, failed.bind(null, done, 'root.getFile - Error getting file: ../' + fileName));
+                }, failed.bind(null, done, 'createFile - Error creating file: ../' + fileName));
+            });
+            it("file.spec.112 should traverse above above the current directory", function (done) {
+                var fileName = "traverse2.file.uri",
+                dirName = "traverse2.subdir";
+                // create a new directory and a file entry
+                createFile(fileName, function () {
+                    createDirectory(dirName, function (entry) {
+                        // lookup file system entry
+                        entry.getFile('../' + fileName, {
+                            create : false
+                        }, function (fileEntry) {
+                            expect(fileEntry).toBeDefined();
+                            expect(fileEntry.name).toBe(fileName);
+                            expect(fileEntry.fullPath).toCanonicallyMatch('/' + fileName);
+                            // cleanup
+                            deleteEntry(fileName, function () {
+                                deleteEntry(dirName, done);
+                            });
+                        }, failed.bind(null, done, 'entry.getFile - Error getting file: ' + fileName + ' recently created above: ' + dirName));
+                    }, failed.bind(null, done, 'createDirectory - Error creating directory: ' + dirName));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.113 getFile: get Entry should error for missing file above root directory", function (done) {
+                var fileName = "../missing.file";
+                // create:false, exclusive:false, file does not exist
+                root.getFile(fileName, {
+                    create : false
+                }, succeed.bind(null, done, 'root.getFile - Unexpected success callback, it should not locate nonexistent file: ' + fileName), function (error) {
+                    expect(error).toBeDefined();
+                    expect(error).toBeFileError(FileError.NOT_FOUND_ERR);
+                    done();
+                });
+            });
+        });
+        //Parent References
+        describe('toNativeURL interface', function () {
+            /* These specs verify that FileEntries have a toNativeURL method
+             * which appears to be sane.
+             */
+            var pathExpect = cordova.platformId === 'windowsphone' ? "//nativ" : "file://";
+            if (isChrome) {
+                pathExpect = 'filesystem:file://';
+            }
+            it("file.spec.114 fileEntry should have a toNativeURL method", function (done) {
+                var fileName = "native.file.uri";
+                if (isWindows) {
+                    var rootPath = root.fullPath;
+                    pathExpect = rootPath.substr(0, rootPath.indexOf(":"));
+                }
+                // create a new file entry
+                createFile(fileName, function (entry) {
+                    expect(entry.toNativeURL).toBeDefined();
+                    expect(entry.name).toCanonicallyMatch(fileName);
+                    expect(typeof entry.toNativeURL).toBe('function');
+                    var nativeURL = entry.toNativeURL();
+                    var indexOfRoot = isWindows ? nativeURL.indexOf(":") :
+                                      isChrome ? 'filesystem:file://'.length : // Chrome uses own prefix for all filesystem urls
+                                      7; //default value - length of 'file://' string
+                    expect(typeof nativeURL).toBe("string");
+                    expect(nativeURL.substring(0, pathExpect.length)).toEqual(pathExpect);
+                    expect(nativeURL.substring(nativeURL.length - fileName.length)).toEqual(fileName);
+                    // cleanup
+                    deleteEntry(fileName, done);
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.115 DirectoryReader should return entries with toNativeURL method", function (done) {
+                var dirName = 'nativeEntries.dir',
+                fileName = 'nativeEntries.file',
+                checkEntries = function (entries) {
+                    expect(entries).toBeDefined();
+                    expect(entries instanceof Array).toBe(true);
+                    expect(entries.length).toBe(1);
+                    expect(entries[0].toNativeURL).toBeDefined();
+                    expect(typeof entries[0].toNativeURL).toBe('function');
+                    var nativeURL = entries[0].toNativeURL();
+                    var indexOfRoot = isWindows ? nativeURL.indexOf(":") :
+                                      isChrome ? 'filesystem:file://'.length : // Chrome uses own prefix for all filesystem urls
+                                      7; //default value - length of 'file://' string
+                    expect(typeof nativeURL).toBe("string");
+                    expect(nativeURL.substring(0, pathExpect.length)).toEqual(pathExpect);
+                    expect(nativeURL.substring(nativeURL.length - fileName.length)).toEqual(fileName);
+                    // cleanup
+                    directory.removeRecursively(null, null);
+                    done();
+                };
+                // create a new file entry
+                root.getDirectory(dirName, {
+                    create : true
+                }, function (directory) {
+                    directory.getFile(fileName, {
+                        create : true
+                    }, function (fileEntry) {
+                        var reader = directory.createReader();
+                        reader.readEntries(checkEntries, failed.bind(null, done, 'reader.readEntries - Error reading entries from directory: ' + dirName));
+                    }, failed.bind(null, done, 'directory.getFile - Error creating file: ' + fileName));
+                }, failed.bind(null, done, 'root.getDirectory - Error creating directory: ' + dirName));
+            });
+            it("file.spec.116 resolveLocalFileSystemURL should return entries with toNativeURL method", function (done) {
+                var fileName = "native.resolve.uri";
+                // create a new file entry
+                createFile(fileName, function (entry) {
+                    resolveLocalFileSystemURL(entry.toURL(), function (entry) {
+                        expect(entry.toNativeURL).toBeDefined();
+                        expect(entry.name).toCanonicallyMatch(fileName);
+                        expect(typeof entry.toNativeURL).toBe('function');
+                        var nativeURL = entry.toNativeURL();
+                        var indexOfRoot = isWindows ? nativeURL.indexOf(":") :
+                                      isChrome ? 'filesystem:file://'.length : // Chrome uses own prefix for all filesystem urls
+                                      7; //default value - length of 'file://' string
+                        expect(typeof nativeURL).toBe("string");
+                        expect(nativeURL.substring(0, pathExpect.length)).toEqual(pathExpect);
+                        expect(nativeURL.substring(nativeURL.length - fileName.length)).toEqual(fileName);
+                        // cleanup
+                        deleteEntry(fileName, done);
+                    }, failed.bind(null, done, 'resolveLocalFileSystemURL - Error resolving file URL: ' + entry.toURL()));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+        });
+        //toNativeURL interface
+        describe('resolveLocalFileSystemURL on file://', function () {
+            /* These specs verify that window.resolveLocalFileSystemURL works correctly on file:// URLs
+             */
+            it("file.spec.117 should not resolve native URLs outside of FS roots", function (done) {
+                // lookup file system entry
+                window.resolveLocalFileSystemURL("file:///this.is.an.invalid.url", succeed.bind(null, done, 'window.resolveLocalFileSystemURL - Unexpected success callback, it should not resolve invalid URL: file:///this.is.an.invalid.url'), function (error) {
+                    expect(error).toBeDefined();
+                    done();
+                });
+            });
+            it("file.spec.118 should not resolve native URLs outside of FS roots", function (done) {
+                // lookup file system entry
+                window.resolveLocalFileSystemURL("file://localhost/this.is.an.invalid.url", succeed.bind(null, done, 'window.resolveLocalFileSystemURL - Unexpected success callback, it should not resolve invalid URL: file://localhost/this.is.an.invalid.url'), function (error) {
+                    expect(error).toBeDefined();
+                    done();
+                });
+            });
+            it("file.spec.119 should not resolve invalid native URLs", function (done) {
+                // lookup file system entry
+                window.resolveLocalFileSystemURL("file://localhost", succeed.bind(null, done, 'window.resolveLocalFileSystemURL - Unexpected success callback, it should not resolve invalid URL: file://localhost'), function (error) {
+                    expect(error).toBeDefined();
+                    done();
+                });
+            });
+            it("file.spec.120 should not resolve invalid native URLs with query strings", function (done) {
+                // lookup file system entry
+                window.resolveLocalFileSystemURL("file://localhost?test/test", succeed.bind(null, done, 'window.resolveLocalFileSystemURL - Unexpected success callback, it should not resolve invalid URL: file://localhost?test/test'), function (error) {
+                    expect(error).toBeDefined();
+                    done();
+                });
+            });
+            it("file.spec.121 should resolve native URLs returned by API", function (done) {
+                var fileName = "native.resolve.uri1";
+                // create a new file entry
+                createFile(fileName, function (entry) {
+                    resolveLocalFileSystemURL(entry.toNativeURL(), function (fileEntry) {
+                        expect(fileEntry.fullPath).toCanonicallyMatch(root.fullPath + "/" + fileName);
+                        // cleanup
+                        deleteEntry(fileName, done);
+                    }, failed.bind(null, done, 'resolveLocalFileSystemURL - Error resolving file URL: ' + entry.toNativeURL()));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.122 should resolve native URLs returned by API with localhost", function (done) {
+                var fileName = "native.resolve.uri2";
+                // create a new file entry
+                createFile(fileName, function (entry) {
+                    var url = entry.toNativeURL();
+                    url = url.replace("///", "//localhost/");
+                    resolveLocalFileSystemURL(url, function (fileEntry) {
+                        expect(fileEntry.fullPath).toCanonicallyMatch(root.fullPath + "/" + fileName);
+                        // cleanup
+                        deleteEntry(fileName, done);
+                    }, failed.bind(null, done, 'resolveLocalFileSystemURL - Error resolving file URL: ' + url));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.123 should resolve native URLs returned by API with query string", function (done) {
+                var fileName = "native.resolve.uri3";
+                // create a new file entry
+                createFile(fileName, function (entry) {
+                    var url = entry.toNativeURL();
+                    url = url + "?test/test";
+                    resolveLocalFileSystemURL(url, function (fileEntry) {
+                        expect(fileEntry.fullPath).toCanonicallyMatch(root.fullPath + "/" + fileName);
+                        // cleanup
+                        deleteEntry(fileName, done);
+                    }, failed.bind(null, done, 'resolveLocalFileSystemURL - Error resolving file URL: ' + url));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+            it("file.spec.124 should resolve native URLs returned by API with localhost and query string", function (done) {
+                var fileName = "native.resolve.uri4";
+                // create a new file entry
+                createFile(fileName, function (entry) {
+                    var url = entry.toNativeURL();
+                    url = url.replace("///", "//localhost/") + "?test/test";
+                    resolveLocalFileSystemURL(url, function (fileEntry) {
+                        expect(fileEntry.fullPath).toCanonicallyMatch(root.fullPath + "/" + fileName);
+                        // cleanup
+                        deleteEntry(fileName, done);
+                    }, failed.bind(null, done, 'resolveLocalFileSystemURL - Error resolving file URL: ' + url));
+                }, failed.bind(null, done, 'createFile - Error creating file: ' + fileName));
+            });
+        });
+        //resolveLocalFileSystemURL on file://
+        describe('cross-file-system copy and move', function () {
+            /* These specs verify that Entry.copyTo and Entry.moveTo work correctly
+             * when crossing filesystem boundaries.
+             */
+            it("file.spec.125 copyTo: temporary -> persistent", function (done) {
+                var file1 = "entry.copy.file1a",
+                file2 = "entry.copy.file2a",
+                sourceEntry,
+                fullPath = joinURL(root.fullPath, file2),
+                validateFile = function (entry) {
+                    // a bit redundant since copy returned this entry already
+                    expect(entry).toBeDefined();
+                    expect(entry.isFile).toBe(true);
+                    expect(entry.isDirectory).toBe(false);
+                    expect(entry.name).toCanonicallyMatch(file2);
+                    expect(entry.fullPath).toCanonicallyMatch(fullPath);
+                    expect(entry.filesystem).toBeDefined();
+                    isChrome ? expect(entry.filesystem.name).toContain("Persistent")
+                        : expect(entry.filesystem.name).toEqual("persistent");
+                    // cleanup
+                    deleteEntry(entry.name);
+                    deleteEntry(sourceEntry.name, done);
+                },
+                createSourceAndTransfer = function () {
+                    temp_root.getFile(file1, {
+                        create : true
+                    }, function (entry) {
+                        expect(entry.filesystem).toBeDefined();
+                        isChrome ? expect(entry.filesystem.name).toContain("Temporary")
+                            : expect(entry.filesystem.name).toEqual("temporary");
+                        sourceEntry = entry;
+                        // Save for later cleanup
+                        entry.copyTo(persistent_root, file2, validateFile, failed.bind(null, done, 'entry.copyTo - Error copying file: ' + file1 + ' to PERSISTENT root as: ' + file2));
+                    }, failed.bind(null, done, 'temp_root.getFile - Error creating file: ' + file1 + 'at TEMPORAL root'));
+                };
+                // Delete any existing file to start things off
+                persistent_root.getFile(file2, {}, function (entry) {
+                    entry.remove(createSourceAndTransfer, failed.bind(null, done, 'entry.remove - Error removing file: ' + file2));
+                }, createSourceAndTransfer);
+            });
+            it("file.spec.126 copyTo: persistent -> temporary", function (done) {
+                var file1 = "entry.copy.file1b",
+                file2 = "entry.copy.file2b",
+                sourceEntry,
+                fullPath = joinURL(temp_root.fullPath, file2),
+                validateFile = function (entry) {
+                    expect(entry).toBeDefined();
+                    expect(entry.isFile).toBe(true);
+                    expect(entry.isDirectory).toBe(false);
+                    expect(entry.name).toCanonicallyMatch(file2);
+                    expect(entry.fullPath).toCanonicallyMatch(fullPath);
+                    isChrome ? expect(entry.filesystem.name).toContain("Temporary")
+                        : expect(entry.filesystem.name).toEqual("temporary");
+                    // cleanup
+                    deleteEntry(entry.name);
+                    deleteEntry(sourceEntry.name, done);
+                },
+                createSourceAndTransfer = function () {
+                    persistent_root.getFile(file1, {
+                        create : true
+                    }, function (entry) {
+                        expect(entry).toBeDefined();
+                        expect(entry.filesystem).toBeDefined();
+                        isChrome ? expect(entry.filesystem.name).toContain("Persistent")
+                            : expect(entry.filesystem.name).toEqual("persistent");
+                        sourceEntry = entry;
+                        // Save for later cleanup
+                        entry.copyTo(temp_root, file2, validateFile, failed.bind(null, done, 'entry.copyTo - Error copying file: ' + file1 + ' to TEMPORAL root as: ' + file2));
+                    }, failed.bind(null, done, 'persistent_root.getFile - Error creating file: ' + file1 + 'at PERSISTENT root'));
+                };
+                // Delete any existing file to start things off
+                temp_root.getFile(file2, {}, function (entry) {
+                    entry.remove(createSourceAndTransfer, failed.bind(null, done, 'entry.remove - Error removing file: ' + file2));
+                }, createSourceAndTransfer);
+            });
+            it("file.spec.127 moveTo: temporary -> persistent", function (done) {
+                var file1 = "entry.copy.file1a",
+                file2 = "entry.copy.file2a",
+                sourceEntry,
+                fullPath = joinURL(root.fullPath, file2),
+                validateFile = function (entry) {
+                    // a bit redundant since copy returned this entry already
+                    expect(entry).toBeDefined();
+                    expect(entry.isFile).toBe(true);
+                    expect(entry.isDirectory).toBe(false);
+                    expect(entry.name).toCanonicallyMatch(file2);
+                    expect(entry.fullPath).toCanonicallyMatch(fullPath);
+                    expect(entry.filesystem).toBeDefined();
+                    isChrome ? expect(entry.filesystem.name).toContain("Persistent")
+                        : expect(entry.filesystem.name).toEqual("persistent");
+                    // cleanup
+                    deleteEntry(entry.name);
+                    deleteEntry(sourceEntry.name, done);
+                },
+                createSourceAndTransfer = function () {
+                    temp_root.getFile(file1, {
+                        create : true
+                    }, function (entry) {
+                        expect(entry.filesystem).toBeDefined();
+                        isChrome ? expect(entry.filesystem.name).toContain("Temporary")
+                            : expect(entry.filesystem.name).toEqual("temporary");
+                        sourceEntry = entry;
+                        // Save for later cleanup
+                        entry.moveTo(persistent_root, file2, validateFile, failed.bind(null, done, 'entry.moveTo - Error moving file: ' + file1 + ' to PERSISTENT root as: ' + file2));
+                    }, failed.bind(null, done, 'temp_root.getFile - Error creating file: ' + file1 + 'at TEMPORAL root'));
+                };
+                // Delete any existing file to start things off
+                persistent_root.getFile(file2, {}, function (entry) {
+                    entry.remove(createSourceAndTransfer, failed.bind(null, done, 'entry.remove - Error removing file: ' + file2));
+                }, createSourceAndTransfer);
+            });
+            it("file.spec.128 moveTo: persistent -> temporary", function (done) {
+                var file1 = "entry.copy.file1b",
+                file2 = "entry.copy.file2b",
+                sourceEntry,
+                fullPath = joinURL(temp_root.fullPath, file2),
+                validateFile = function (entry) {
+                    expect(entry).toBeDefined();
+                    expect(entry.isFile).toBe(true);
+                    expect(entry.isDirectory).toBe(false);
+                    expect(entry.name).toCanonicallyMatch(file2);
+                    expect(entry.fullPath).toCanonicallyMatch(fullPath);
+                    isChrome ? expect(entry.filesystem.name).toContain("Temporary")
+                        : expect(entry.filesystem.name).toEqual("temporary");
+                    // cleanup
+                    deleteEntry(entry.name);
+                    deleteEntry(sourceEntry.name, done);
+                },
+                createSourceAndTransfer = function () {
+                    persistent_root.getFile(file1, {
+                        create : true
+                    }, function (entry) {
+                        expect(entry).toBeDefined();
+                        expect(entry.filesystem).toBeDefined();
+                        isChrome ? expect(entry.filesystem.name).toContain("Persistent")
+                            : expect(entry.filesystem.name).toEqual("persistent");
+                        sourceEntry = entry;
+                        // Save for later cleanup
+                        entry.moveTo(temp_root, file2, validateFile, failed.bind(null, done, 'entry.moveTo - Error moving file: ' + file1 + ' to TEMPORAL root as: ' + file2));
+                    }, failed.bind(null, done, 'persistent_root.getFile - Error creating file: ' + file1 + 'at PERSISTENT root'));
+                };
+                // Delete any existing file to start things off
+                temp_root.getFile(file2, {}, function (entry) {
+                    entry.remove(createSourceAndTransfer, failed.bind(null, done, 'entry.remove - Error removing file: ' + file2));
+                }, createSourceAndTransfer);
+            });
+            it("file.spec.129 cordova.file.*Directory are set", function () {
+                var expectedPaths = ['applicationDirectory', 'applicationStorageDirectory', 'dataDirectory', 'cacheDirectory'];
+                if (cordova.platformId == 'android' || cordova.platformId == 'amazon-fireos') {
+                    expectedPaths.push('externalApplicationStorageDirectory', 'externalRootDirectory', 'externalCacheDirectory', 'externalDataDirectory');
+                } else if (cordova.platformId == 'blackberry10') {
+                    expectedPaths.push('externalRootDirectory', 'sharedDirectory');
+                } else if (cordova.platformId == 'ios') {
+                    expectedPaths.push('syncedDataDirectory', 'documentsDirectory', 'tempDirectory');
+                } else if (cordova.platformId == 'osx') {
+                    expectedPaths.push('documentsDirectory', 'tempDirectory', 'rootDirectory');
+                } else {
+                    console.log('Skipping test due on unsupported platform.');
+                    return;
+                }
+                for (var i = 0; i < expectedPaths.length; ++i) {
+                    expect(typeof cordova.file[expectedPaths[i]]).toBe('string');
+                    expect(cordova.file[expectedPaths[i]]).toMatch(/\/$/, 'Path should end with a slash');
+                }
+            });
+        });
+        //cross-file-system copy and move
+        describe('IndexedDB-based impl', function () {
+            it("file.spec.131 Nested file or nested directory should be removed when removing a parent directory", function (done) {
+                var parentDirName = 'deletedDir131',
+                    nestedDirName = 'nestedDir131',
+                    nestedFileName = 'nestedFile131.txt';
+
+                createDirectory(parentDirName, function (parent) {
+                    parent.getDirectory(nestedDirName, { create: true}, function () {
+                        parent.getFile(nestedFileName, { create: true}, function () {
+                            parent.removeRecursively(function() {
+                                root.getDirectory(parentDirName,{ create: false}, failed.bind(this, done, 'root.getDirectory - unexpected success callback : ' + parentDirName), function(){
+                                    parent.getFile(nestedFileName,{ create: false}, failed.bind(this, done, 'getFile - unexpected success callback : ' + nestedFileName), function(){
+                                            parent.getDirectory(nestedDirName, { create: false}, failed.bind(this, done, 'getDirectory - unexpected success callback : ' + nestedDirName), done);
+                                        });
+                                    });
+                                }, failed.bind(this, done, 'removeRecursively - Error removing directory : ' + parentDirName));
+                        }, failed.bind(this, done, 'getFile - Error creating file : ' + nestedFileName));
+                    },failed.bind(this, done, 'getDirectory - Error creating directory : ' + nestedDirName));
+                }, failed.bind(this, done, 'root.getDirectory - Error creating directory : ' + parentDirName));
+            });
+            it("file.spec.132 Entry should be created succesfully when using relative paths if its parent directory exists", function (done) {
+                /* Directory entries have to be created successively.
+                   For example, the call `fs.root.getDirectory('dir1/dir2', {create:true}, successCallback, errorCallback)`
+                   will fail if dir1 did not exist. */
+                var parentName = 'parentName132';
+                var nestedName = 'nestedName132';
+                var path = parentName + '/' + nestedName;
+
+                var win = function(directory){
+                    expect(directory).toBeDefined();
+                    expect(directory.isFile).toBe(false);
+                    expect(directory.isDirectory).toBe(true);
+                    expect(directory.name).toCanonicallyMatch(nestedName);
+                    expect(directory.fullPath).toCanonicallyMatch('/' + path + '/');
+                    deleteEntry(directory.name);
+                    deleteEntry(parentName, done);
+                };
+
+                createDirectory(parentName, function() {
+                    root.getDirectory(parentName + '/' + nestedName, {create:true}, win,
+                        failed.bind(this, done, 'root.getDirectory - Error getting directory : ' + path));
+                }, failed.bind(this, done, 'root.getDirectory - Error getting directory : ' + parentName));
+            });
+            it("file.spec.133 A file being removed should not affect another file with name being a prefix of the removed file name.", function (done) {
+
+                // Names include special symbols so that we check the IndexedDB range used
+                var deletedFileName = 'deletedFile.0',
+                secondFileName = 'deletedFile.0.1';
+
+                var win = function(fileEntry){
+                    expect(fileEntry).toBeDefined();
+                    expect(fileEntry.isFile).toBe(true);
+                    expect(fileEntry.isDirectory).toBe(false);
+                    expect(fileEntry.name).toCanonicallyMatch(secondFileName);
+                    deleteEntry(fileEntry.name, done);
+                };
+
+                createFile(deletedFileName, function (deletedFile) {
+                    createFile(secondFileName, function () {
+                        deletedFile.remove(function() {
+                            root.getFile(deletedFileName, {create: false}, failed.bind(this, done, 'getFile - unexpected success callback getting deleted file : ' + deletedFileName), function(){
+                                root.getFile(secondFileName, {create: false}, win, failed.bind(this, done, 'getFile - Error getting file after deleting deletedFile : ' + secondFileName));
+                            });
+                        }, failed.bind(this, done, 'remove - Error removing file : ' + deletedFileName));
+                    }, failed.bind(this, done, 'getFile - Error creating file : ' + secondFileName));
+                }, failed.bind(this, done, 'getFile - Error creating file : ' + deletedFileName));
+            });
+            it("file.spec.134 A directory being removed should not affect another directory with name being a prefix of the removed directory name.", function (done) {
+
+                // Names include special symbols so that we check the IndexedDB range used
+                var deletedDirName = 'deletedDir.0',
+                secondDirName = 'deletedDir.0.1';
+
+                var win = function(directory){
+                    expect(directory).toBeDefined();
+                    expect(directory.isFile).toBe(false);
+                    expect(directory.isDirectory).toBe(true);
+                    expect(directory.name).toCanonicallyMatch(secondDirName);
+                    deleteEntry(directory.name, done);
+                };
+
+                createDirectory(deletedDirName, function (deletedDir) {
+                    createDirectory(secondDirName, function () {
+                        deletedDir.remove(function() {
+                            root.getDirectory(deletedDirName, {create: false}, failed.bind(this, done, 'getDirectory - unexpected success callback getting deleted directory : ' + deletedDirName), function() {
+                                root.getDirectory(secondDirName, {create: false}, win, failed.bind(this, done, 'getDirectory - Error getting directory after deleting deletedDirectory : ' + secondDirName));
+                            });
+                        }, failed.bind(this, done, 'remove - Error removing directory : ' + deletedDirName));
+                    }, failed.bind(this, done, 'root.getDirectory - Error creating directory : ' + secondDirName));
+                }, failed.bind(this, done, 'root.getDirectory - Error creating directory : ' + deletedDirName));
+            });
+            it("file.spec.135 Deletion of a child directory should not affect the parent directory.", function (done) {
+
+                var parentName = 'parentName135';
+                var childName = 'childName135';
+
+                var win = function(directory){
+                    expect(directory).toBeDefined();
+                    expect(directory.isFile).toBe(false);
+                    expect(directory.isDirectory).toBe(true);
+                    expect(directory.name).toCanonicallyMatch(parentName);
+                    deleteEntry(directory.name, done);
+                };
+
+                createDirectory(parentName, function(parent){
+                    parent.getDirectory(childName, {create: true}, function(child){
+                        child.removeRecursively(function(){
+                            root.getDirectory(parentName, {create: false}, win, failed.bind(this, done, 'root.getDirectory - Error getting parent directory : ' + parentName));
+                        },
+                        failed.bind(this, done, 'getDirectory - Error removing directory : ' + childName));
+                    }, failed.bind(this, done, 'getDirectory - Error creating directory : ' + childName));
+                }, failed.bind(this, done, 'root.getDirectory - Error creating directory : ' + parentName));
+            });
+            it("file.spec.136 Paths should support Unicode symbols.", function (done) {
+
+                var dirName = '文件插件';
+
+                var win = function(directory){
+                    expect(directory).toBeDefined();
+                    expect(directory.isFile).toBe(false);
+                    expect(directory.isDirectory).toBe(true);
+                    expect(directory.name).toCanonicallyMatch(dirName);
+                    deleteEntry(directory.name, done);
+                };
+
+                createDirectory(dirName, function(){
+                    root.getDirectory(dirName, {create: false}, win,
+                        failed.bind(this, done, 'root.getDirectory - Error getting directory : ' + dirName));
+                }, failed.bind(this, done, 'root.getDirectory - Error creating directory : ' + dirName));
+            });
+        });
+        // Content and Asset URLs
+        if (cordova.platformId == 'android') {
+            describe('content: URLs', function() {
+                function testContentCopy(src, done) {
+                    var file2 = "entry.copy.file2b",
+                    fullPath = joinURL(temp_root.fullPath, file2),
+                    validateFile = function (entry) {
+                        expect(entry.isFile).toBe(true);
+                        expect(entry.isDirectory).toBe(false);
+                        expect(entry.name).toCanonicallyMatch(file2);
+                        expect(entry.fullPath).toCanonicallyMatch(fullPath);
+                        expect(entry.filesystem.name).toEqual("temporary");
+                        // cleanup
+                        deleteEntry(entry.name, done);
+                    },
+                    transfer = function () {
+                        resolveLocalFileSystemURL(src, function(entry) {
+                            expect(entry).toBeDefined();
+                            expect(entry.filesystem.name).toEqual("content");
+                            entry.copyTo(temp_root, file2, validateFile, failed.bind(null, done, 'entry.copyTo - Error copying file: ' + entry.toURL() + ' to TEMPORAL root as: ' + file2));
+                        }, failed.bind(null, done, 'resolveLocalFileSystemURL failed for content provider'));
+                    };
+                    // Delete any existing file to start things off
+                    temp_root.getFile(file2, {}, function (entry) {
+                        entry.remove(transfer, failed.bind(null, done, 'entry.remove - Error removing file: ' + file2));
+                    }, transfer);
+                }
+                it("file.spec.138 copyTo: content", function(done) {
+                    testContentCopy('content://org.apache.cordova.file.testprovider/www/index.html', done);
+                });
+                it("file.spec.139 copyTo: content /w space and query", function(done) {
+                    testContentCopy('content://org.apache.cordova.file.testprovider/?name=foo%20bar&realPath=%2Fwww%2Findex.html', done);
+                });
+                it("file.spec.140 delete: content should fail", function(done) {
+                    resolveLocalFileSystemURL('content://org.apache.cordova.file.testprovider/www/index.html', function(entry) {
+                        entry.remove(failed.bind(null, done, 'expected delete to fail'), done);
+                    }, failed.bind(null, done, 'resolveLocalFileSystemURL failed for content provider'));
+                });
+            });
+
+            // these tests ensure that you can read and copy from android_asset folder
+            // for details see https://issues.apache.org/jira/browse/CB-6428
+            // and https://mail-archives.apache.org/mod_mbox/cordova-dev/201508.mbox/%3C782154441.8406572.1440182722528.JavaMail.yahoo%40mail.yahoo.com%3E
+            describe('asset: URLs', function() {
+                it("file.spec.141 filePaths.applicationStorage", function() {
+                    expect(cordova.file.applicationDirectory).toEqual('file:///android_asset/');
+                }, MEDIUM_TIMEOUT);
+                it("file.spec.142 assets should be enumerable", function(done) {
+                    resolveLocalFileSystemURL('file:///android_asset/www/fixtures/asset-test', function(entry) {
+                        var reader = entry.createReader();
+                        reader.readEntries(function (entries) {
+                            expect(entries.length).not.toBe(0);
+                            done();
+                        }, failed.bind(null, done, 'reader.readEntries - Error during reading of entries from assets directory'));
+                    }, failed.bind(null, done, 'resolveLocalFileSystemURL failed for assets'));
+                }, MEDIUM_TIMEOUT);
+                it("file.spec.143 copyTo: asset -> temporary", function(done) {
+                    var file2 = "entry.copy.file2b",
+                    fullPath = joinURL(temp_root.fullPath, file2),
+                    validateFile = function (entry) {
+                        expect(entry.isFile).toBe(true);
+                        expect(entry.isDirectory).toBe(false);
+                        expect(entry.name).toCanonicallyMatch(file2);
+                        expect(entry.fullPath).toCanonicallyMatch(fullPath);
+                        expect(entry.filesystem.name).toEqual("temporary");
+                        // cleanup
+                        deleteEntry(entry.name, done);
+                    },
+                    transfer = function () {
+                        resolveLocalFileSystemURL('file:///android_asset/www/index.html', function(entry) {
+                            expect(entry.filesystem.name).toEqual('assets');
+                            entry.copyTo(temp_root, file2, validateFile, failed.bind(null, done, 'entry.copyTo - Error copying file: ' + entry.toURL() + ' to TEMPORAL root as: ' + file2));
+                        }, failed.bind(null, done, 'resolveLocalFileSystemURL failed for assets'));
+                    };
+                    // Delete any existing file to start things off
+                    temp_root.getFile(file2, {}, function (entry) {
+                        entry.remove(transfer, failed.bind(null, done, 'entry.remove - Error removing file: ' + file2));
+                    }, transfer);
+                }, MEDIUM_TIMEOUT);
+            });
+            it("file.spec.144 copyTo: asset directory", function (done) {
+                var srcUrl = 'file:///android_asset/www/fixtures/asset-test';
+                var dstDir = "entry.copy.dstDir";
+                var dstPath = joinURL(root.fullPath, dstDir);
+                // create a new directory entry to kick off it
+                deleteEntry(dstDir, function () {
+                    resolveLocalFileSystemURL(srcUrl, function(directory) {
+                        directory.copyTo(root, dstDir, function (directory) {
+                            expect(directory).toBeDefined();
+                            expect(directory.isFile).toBe(false);
+                            expect(directory.isDirectory).toBe(true);
+                            expect(directory.fullPath).toCanonicallyMatch(dstPath);
+                            expect(directory.name).toCanonicallyMatch(dstDir);
+                            root.getDirectory(dstDir, {
+                                create : false
+                            }, function (dirEntry) {
+                                expect(dirEntry).toBeDefined();
+                                expect(dirEntry.isFile).toBe(false);
+                                expect(dirEntry.isDirectory).toBe(true);
+                                expect(dirEntry.fullPath).toCanonicallyMatch(dstPath);
+                                expect(dirEntry.name).toCanonicallyMatch(dstDir);
+                                dirEntry.getFile('asset-test.txt', {
+                                    create : false
+                                }, function (fileEntry) {
+                                    expect(fileEntry).toBeDefined();
+                                    expect(fileEntry.isFile).toBe(true);
+                                    // cleanup
+                                    deleteEntry(dstDir, done);
+                                }, failed.bind(null, done, 'dirEntry.getFile - Error getting subfile'));
+                            }, failed.bind(null, done, 'root.getDirectory - Error getting copied directory'));
+                        }, failed.bind(null, done, 'directory.copyTo - Error copying directory'));
+                    }, failed.bind(null, done, 'resolving src dir'));
+                }, failed.bind(null, done, 'deleteEntry - Error removing directory : ' + dstDir));
+            }, MEDIUM_TIMEOUT);
+        }
+    });
+
+};
+//******************************************************************************************
+//***************************************Manual Tests***************************************
+//******************************************************************************************
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+
+    function resolveFs(fsname) {
+        var fsURL = "cdvfile://localhost/" + fsname + "/";
+        logMessage("Resolving URL: " + fsURL);
+        resolveLocalFileSystemURL(fsURL, function (entry) {
+            logMessage("Success", 'green');
+            logMessage(entry.toURL(), 'blue');
+            logMessage(entry.toInternalURL(), 'blue');
+            logMessage("Resolving URL: " + entry.toURL());
+            resolveLocalFileSystemURL(entry.toURL(), function (entry2) {
+                logMessage("Success", 'green');
+                logMessage(entry2.toURL(), 'blue');
+                logMessage(entry2.toInternalURL(), 'blue');
+            }, logError("resolveLocalFileSystemURL"));
+        }, logError("resolveLocalFileSystemURL"));
+    }
+
+    function testPrivateURL() {
+        requestFileSystem(TEMPORARY, 0, function (fileSystem) {
+            logMessage("Temporary root is at " + fileSystem.root.toNativeURL());
+            fileSystem.root.getFile("testfile", {
+                create : true
+            }, function (entry) {
+                logMessage("Temporary file is at " + entry.toNativeURL());
+                if (entry.toNativeURL().substring(0, 12) == "file:///var/") {
+                    logMessage("File starts with /var/, trying /private/var");
+                    var newURL = "file://localhost/private/var/" + entry.toNativeURL().substring(12) + "?and=another_thing";
+                    //var newURL = entry.toNativeURL();
+                    logMessage(newURL, 'blue');
+                    resolveLocalFileSystemURL(newURL, function (newEntry) {
+                        logMessage("Successfully resolved.", 'green');
+                        logMessage(newEntry.toURL(), 'blue');
+                        logMessage(newEntry.toNativeURL(), 'blue');
+                    }, logError("resolveLocalFileSystemURL"));
+                }
+            }, logError("getFile"));
+        }, logError("requestFileSystem"));
+    }
+
+    function clearLog() {
+        var log = document.getElementById("info");
+        log.innerHTML = "";
+    }
+
+    function logMessage(message, color) {
+        var log = document.getElementById("info");
+        var logLine = document.createElement('div');
+        if (color) {
+            logLine.style.color = color;
+        }
+        logLine.innerHTML = message;
+        log.appendChild(logLine);
+    }
+
+    function logError(serviceName) {
+        return function (err) {
+            logMessage("ERROR: " + serviceName + " " + JSON.stringify(err), "red");
+        };
+    }
+
+    var fsRoots = {
+        "ios" : "library,library-nosync,documents,documents-nosync,cache,bundle,root,private",
+        "osx" : "library,library-nosync,documents,documents-nosync,cache,bundle,root,private",
+        "android" : "files,files-external,documents,sdcard,cache,cache-external,root",
+        "amazon-fireos" : "files,files-external,documents,sdcard,cache,cache-external,root",
+        "windows": "temporary,persistent",
+        "tizen": "temporary,persistent,root"
+    };
+
+    //Add title and align to content
+    var div = document.createElement('h2');
+    div.appendChild(document.createTextNode('File Systems'));
+    div.setAttribute("align", "center");
+    contentEl.appendChild(div);
+
+    div = document.createElement('h3');
+    div.appendChild(document.createTextNode('Results are displayed in yellow status box below with expected results noted under that'));
+    div.setAttribute("align", "center");
+    contentEl.appendChild(div);
+
+    div = document.createElement('div');
+    div.setAttribute("id", "button");
+    div.setAttribute("align", "center");
+    contentEl.appendChild(div);
+    if (fsRoots.hasOwnProperty(cordova.platformId)) {
+        (fsRoots[cordova.platformId].split(',')).forEach(function (fs) {
+            if (cordova.platformId === 'ios' && fs === 'private') {
+                createActionButton("Test private URL (iOS)", function () {
+                    clearLog();
+                    testPrivateURL();
+                }, 'button');
+            } else {
+                createActionButton(fs, function () {
+                    clearLog();
+                    resolveFs(fs);
+                }, 'button');
+            }
+        });
+    }
+
+
+    div = document.createElement('div');
+    div.setAttribute("id", "info");
+    div.setAttribute("align", "center");
+    contentEl.appendChild(div);
+
+    div = document.createElement('h3');
+    div.appendChild(document.createTextNode('For each test above, file or directory should be successfully found. ' +
+        'Status box should say Resolving URL was Success. The first URL resolved is the internal URL. ' +
+        'The second URL resolved is the absolute URL. Blue URLs must match.'));
+    contentEl.appendChild(div);
+
+    div = document.createElement('h3');
+    div.appendChild(document.createTextNode('For Test private URL (iOS), the private URL (first blue URL in status box) ' +
+        'should be successfully resolved. Status box should say Successfully resolved. Both blue URLs below ' +
+        'that should match.'));
+    contentEl.appendChild(div);
+};
diff --git a/test/unittest/tests/filetransfer.tests.js b/test/unittest/tests/filetransfer.tests.js
new file mode 100644 (file)
index 0000000..364f715
--- /dev/null
@@ -0,0 +1,991 @@
+/*
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you 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.
+*
+*/
+
+/*global exports, cordova, FileTransfer, FileTransferError,
+         FileUploadOptions, LocalFileSystem, requestFileSystem, TEMPORARY */
+
+/*global describe, it, expect, beforeEach, afterEach, spyOn,
+         jasmine, pending*/
+
+exports.defineAutoTests = function () {
+
+    // constants
+    var GRACE_TIME_DELTA = 600; // in milliseconds
+    var DEFAULT_FILESYSTEM_SIZE = 1024*50; //filesystem size in bytes
+    var UNKNOWN_HOST = "http://foobar.apache.org";
+    var HEADERS_ECHO = "http://whatheaders.com"; // NOTE: this site is very useful!
+    var DOWNLOAD_TIMEOUT = 30 * 1000; // download tests sometimes need a higher timeout to complete successfully
+    var UPLOAD_TIMEOUT = 30 * 1000; // upload tests sometimes need a higher timeout to complete successfully
+    var ABORT_DELAY = 100; // for abort() tests
+
+    // config for upload test server
+    // NOTE:
+    //      more info at https://github.com/apache/cordova-labs/tree/cordova-filetransfer
+    var SERVER                  = "http://cordova-vm.apache.org:5000";
+    var SERVER_WITH_CREDENTIALS = "http://cordova_user:cordova_password@cordova-vm.apache.org:5000";
+
+    // flags
+    var isWindows = cordova.platformId === 'windows8' || cordova.platformId === 'windows';
+    var isWP8 = cordova.platformId === 'windowsphone';
+
+    var isBrowser = cordova.platformId === 'browser';
+    var isIE = isBrowser && navigator.userAgent.indexOf('Trident') >= 0;
+
+    describe('FileTransferError', function () {
+
+        it('should exist', function () {
+            expect(FileTransferError).toBeDefined();
+        });
+
+        it('should be constructable', function () {
+            var transferError = new FileTransferError();
+            expect(transferError).toBeDefined();
+        });
+
+        it('filetransfer.spec.3 should expose proper constants', function () {
+
+            expect(FileTransferError.FILE_NOT_FOUND_ERR).toBeDefined();
+            expect(FileTransferError.INVALID_URL_ERR).toBeDefined();
+            expect(FileTransferError.CONNECTION_ERR).toBeDefined();
+            expect(FileTransferError.ABORT_ERR).toBeDefined();
+            expect(FileTransferError.NOT_MODIFIED_ERR).toBeDefined();
+
+            expect(FileTransferError.FILE_NOT_FOUND_ERR).toBe(1);
+            expect(FileTransferError.INVALID_URL_ERR).toBe(2);
+            expect(FileTransferError.CONNECTION_ERR).toBe(3);
+            expect(FileTransferError.ABORT_ERR).toBe(4);
+            expect(FileTransferError.NOT_MODIFIED_ERR).toBe(5);
+        });
+    });
+
+    describe('FileUploadOptions', function () {
+
+        it('should exist', function () {
+            expect(FileUploadOptions).toBeDefined();
+        });
+
+        it('should be constructable', function () {
+            var transferOptions = new FileUploadOptions();
+            expect(transferOptions).toBeDefined();
+        });
+    });
+
+    describe('FileTransfer', function () {
+
+        var persistentRoot, tempRoot;
+
+        // named callbacks
+        var unexpectedCallbacks = {
+            httpFail:          function () {},
+            httpWin:           function () {},
+            fileSystemFail:    function () {},
+            fileSystemWin:     function () {},
+            fileOperationFail: function () {},
+            fileOperationWin:  function () {},
+        };
+
+        var expectedCallbacks = {
+            unsupportedOperation: function (response) {
+                console.log('spec called unsupported functionality; response:', response);
+            },
+        };
+
+        // helpers
+        var deleteFile = function (fileSystem, name, done) {
+            fileSystem.getFile(name, null,
+                function (fileEntry) {
+                    fileEntry.remove(
+                        function () {
+                            done();
+                        },
+                        function () {
+                            throw new Error('failed to delete: \'' + name + '\'');
+                        }
+                    );
+                },
+                function () {
+                    done();
+                }
+            );
+        };
+
+        var writeFile = function (fileSystem, name, content, success) {
+            fileSystem.getFile(name, { create: true },
+                function (fileEntry) {
+                    fileEntry.createWriter(function (writer) {
+
+                        writer.onwrite = function () {
+                            success(fileEntry);
+                        };
+
+                        writer.onabort = function (evt) {
+                            throw new Error('aborted creating test file \'' + name + '\': ' + evt);
+                        };
+
+                        writer.error = function (evt) {
+                            throw new Error('aborted creating test file \'' + name + '\': ' + evt);
+                        };
+
+                        if (cordova.platformId === 'browser') {
+                            // var builder = new BlobBuilder();
+                            // builder.append(content + '\n');
+                            var blob = new Blob([content + '\n'], { type: 'text/plain' });
+                            writer.write(blob);
+                        } else {
+                            writer.write(content + "\n");
+                        }
+
+                    }, unexpectedCallbacks.fileOperationFail);
+                },
+                function () {
+                    throw new Error('could not create test file \'' + name + '\'');
+                }
+            );
+        };
+
+        // according to documentation, wp8 does not support onProgress:
+        // https://github.com/apache/cordova-plugin-file-transfer/blob/master/doc/index.md#supported-platforms
+        var wp8OnProgressHandler = function () {};
+
+        var defaultOnProgressHandler = function (event) {
+            if (event.lengthComputable) {
+                expect(event.loaded).toBeGreaterThan(1);
+                expect(event.total).toBeGreaterThan(0);
+                expect(event.total).not.toBeLessThan(event.loaded);
+                expect(event.lengthComputable).toBe(true, 'lengthComputable');
+            } else {
+                // In IE, when lengthComputable === false, event.total somehow is equal to 2^64
+                if (isIE) {
+                    expect(event.total).toBe(Math.pow(2, 64));
+                } else {
+                    expect(event.total).toBe(0);
+                }
+            }
+        };
+
+        var getMalformedUrl = function () {
+            if (cordova.platformId === 'android' || cordova.platformId === 'amazon-fireos') {
+                // bad protocol causes a MalformedUrlException on Android
+                return "httpssss://example.com";
+            } else {
+                // iOS doesn't care about protocol, space in hostname causes error
+                return "httpssss://exa mple.com";
+            }
+        };
+
+        // NOTE:
+        //      there are several beforeEach calls, one per async call; since calling done()
+        //      signifies a completed async call, each async call needs its own done(), and
+        //      therefore its own beforeEach
+        beforeEach(function (done) {
+            window.requestFileSystem(LocalFileSystem.PERSISTENT, DEFAULT_FILESYSTEM_SIZE,
+                function (fileSystem) {
+                    persistentRoot = fileSystem.root;
+                    done();
+                },
+                function () {
+                    throw new Error('Failed to initialize persistent file system.');
+                }
+            );
+        });
+
+        beforeEach(function (done) {
+            window.requestFileSystem(LocalFileSystem.TEMPORARY, DEFAULT_FILESYSTEM_SIZE,
+                function (fileSystem) {
+                    tempRoot = fileSystem.root;
+                    done();
+                },
+                function () {
+                    throw new Error('Failed to initialize temporary file system.');
+                }
+            );
+        });
+
+        // spy on all named callbacks
+        beforeEach(function() {
+
+            // ignore the actual implementations of the unexpected callbacks
+            for (var callback in unexpectedCallbacks) {
+                if (unexpectedCallbacks.hasOwnProperty(callback)) {
+                    spyOn(unexpectedCallbacks, callback);
+                }
+            }
+
+            // but run the implementations of the expected callbacks
+            for (callback in expectedCallbacks) { //jshint ignore: line
+                if (expectedCallbacks.hasOwnProperty(callback)) {
+                    spyOn(expectedCallbacks, callback).and.callThrough();
+                }
+            }
+        });
+
+        // at the end, check that none of the unexpected callbacks got called,
+        // and act on the expected callbacks
+        afterEach(function() {
+            for (var callback in unexpectedCallbacks) {
+                if (unexpectedCallbacks.hasOwnProperty(callback)) {
+                    expect(unexpectedCallbacks[callback]).not.toHaveBeenCalled();
+                }
+            }
+
+            if (expectedCallbacks.unsupportedOperation.calls.any()) {
+                pending();
+            }
+        });
+
+        it('should initialise correctly', function() {
+            expect(persistentRoot).toBeDefined();
+            expect(tempRoot).toBeDefined();
+        });
+
+        it('should exist', function () {
+            expect(FileTransfer).toBeDefined();
+        });
+
+        it('filetransfer.spec.1 should be constructable', function () {
+            var transfer = new FileTransfer();
+            expect(transfer).toBeDefined();
+        });
+
+        it('filetransfer.spec.2 should expose proper functions', function () {
+
+            var transfer = new FileTransfer();
+
+            expect(transfer.upload).toBeDefined();
+            expect(transfer.download).toBeDefined();
+
+            expect(transfer.upload).toEqual(jasmine.any(Function));
+            expect(transfer.download).toEqual(jasmine.any(Function));
+        });
+
+        describe('methods', function() {
+
+            var transfer;
+
+            var root;
+            var fileName;
+            var localFilePath;
+
+            beforeEach(function() {
+
+                transfer = new FileTransfer();
+
+                // assign onprogress handler
+                transfer.onprogress = isWP8 ? wp8OnProgressHandler : defaultOnProgressHandler;
+
+                // spy on the onprogress handler, but still call through to it
+                spyOn(transfer, 'onprogress').and.callThrough();
+
+                root          = persistentRoot;
+                fileName      = 'testFile.txt';
+                localFilePath = root.toURL() + fileName;
+            });
+
+            // NOTE:
+            //      if download tests are failing, check the
+            //      URL white list for the following URLs:
+            //         - 'httpssss://example.com'
+            //         - 'apache.org', with subdomains="true"
+            //         - 'cordova-filetransfer.jitsu.com'
+            describe('download', function () {
+
+                // helpers
+                var verifyDownload = function (fileEntry) {
+                    expect(fileEntry.name).toBe(fileName);
+                };
+
+                // delete the downloaded file
+                afterEach(function (done) {
+                    deleteFile(root, fileName, done);
+                });
+
+                it('ensures that test file does not exist', function (done) {
+                    deleteFile(root, fileName, done);
+                });
+
+                it('filetransfer.spec.4 should download a file', function (done) {
+
+                    var fileURL = SERVER + '/robots.txt';
+
+                    var fileWin = function (blob) {
+
+                        if (transfer.onprogress.calls.any()) {
+                            var lastProgressEvent = transfer.onprogress.calls.mostRecent().args[0];
+                            expect(lastProgressEvent.loaded).not.toBeGreaterThan(blob.size);
+                        } else {
+                            console.log('no progress events were emitted');
+                        }
+
+                        done();
+                    };
+
+                    var downloadWin = function (entry) {
+
+                        verifyDownload(entry);
+
+                        // verify the FileEntry representing this file
+                        entry.file(fileWin, unexpectedCallbacks.fileSystemFail);
+                    };
+
+                    transfer.download(fileURL, localFilePath, downloadWin, unexpectedCallbacks.httpFail);
+                }, DOWNLOAD_TIMEOUT);
+
+                it('filetransfer.spec.5 should download a file using http basic auth', function (done) {
+
+                    var fileURL = SERVER_WITH_CREDENTIALS + '/download_basic_auth';
+
+                    var downloadWin = function (entry) {
+                        verifyDownload(entry);
+                        done();
+                    };
+
+                    transfer.download(fileURL, localFilePath, downloadWin, unexpectedCallbacks.httpFail);
+                }, DOWNLOAD_TIMEOUT);
+
+                it('filetransfer.spec.6 should get 401 status on http basic auth failure', function (done) {
+
+                    // NOTE:
+                    //      using server without credentials
+                    var fileURL = SERVER + '/download_basic_auth';
+
+                    var downloadFail = function (error) {
+                        expect(error.http_status).toBe(401);
+                        expect(error.http_status).not.toBe(404, "Ensure " + fileURL + " is in the white list");
+                        done();
+                    };
+
+                    transfer.download(fileURL, localFilePath, unexpectedCallbacks.httpWin, downloadFail, null,
+                        {
+                            headers: {
+                                'If-Modified-Since': 'Thu, 19 Mar 2015 00:00:00 GMT'
+                            }
+                        });
+                }, DOWNLOAD_TIMEOUT);
+
+                it("filetransfer.spec.7 should download a file using file:// (when hosted from file://)", function (done) {
+
+                    // for Windows platform it's ms-appdata:/// by default, not file://
+                    if (isWindows) {
+                        pending();
+                        return;
+                    }
+
+                    var fileURL = window.location.protocol + '//' + window.location.pathname.replace(/ /g, '%20');
+
+                    if (!/^file:/.exec(fileURL) && cordova.platformId !== 'blackberry10') {
+                        if (cordova.platformId === 'windowsphone')
+                            expect(fileURL).toMatch(/^x-wmapp0:/);
+                        done();
+                        return;
+                    }
+
+                    var downloadWin = function (entry) {
+                        verifyDownload(entry);
+                        done();
+                    };
+
+                    transfer.download(fileURL, localFilePath, downloadWin, unexpectedCallbacks.httpFail);
+                }, DOWNLOAD_TIMEOUT);
+
+                it("filetransfer.spec.8 should download a file using https://", function (done) {
+
+                    var fileURL = "https://www.apache.org/licenses/";
+
+                    var fileWin = function (file) {
+
+                        var reader = new FileReader();
+
+                        reader.onerror = unexpectedCallbacks.fileOperationFail;
+                        reader.onload  = function () {
+                            expect(reader.result).toMatch(/The Apache Software Foundation/);
+                            done();
+                        };
+
+                        reader.readAsText(file);
+                    };
+
+                    var downloadWin = function (entry) {
+                        verifyDownload(entry);
+                        entry.file(fileWin, unexpectedCallbacks.fileSystemFail);
+                    };
+
+                    transfer.download(fileURL, localFilePath, downloadWin, unexpectedCallbacks.httpFail);
+                }, DOWNLOAD_TIMEOUT);
+
+                it("filetransfer.spec.11 should call the error callback on abort()", function (done) {
+
+                    var fileURL = 'http://cordova.apache.org/downloads/BlueZedEx.mp3';
+
+                    transfer.download(fileURL, localFilePath, unexpectedCallbacks.httpWin, done);
+                    setTimeout(function() {
+                        transfer.abort();
+                    }, ABORT_DELAY);
+                }, DOWNLOAD_TIMEOUT);
+
+                it("filetransfer.spec.9 should not leave partial file due to abort", function (done) {
+
+                    var fileURL = 'http://cordova.apache.org/downloads/logos_2.zip';
+
+                    var downloadFail = function (error) {
+
+                        expect(error.code).toBe(FileTransferError.ABORT_ERR);
+                        expect(transfer.onprogress).toHaveBeenCalled();
+
+                        // check that there is no file
+                        root.getFile(localFilePath, null, unexpectedCallbacks.fileSystemWin, done);
+                    };
+
+                    // abort at the first onprogress event
+                    transfer.onprogress = function (event) {
+                        if (event.loaded > 0) {
+                            transfer.abort();
+                        }
+                    };
+
+                    spyOn(transfer, 'onprogress').and.callThrough();
+
+                    transfer.download(fileURL, localFilePath, unexpectedCallbacks.httpWin, downloadFail);
+                }, DOWNLOAD_TIMEOUT);
+
+                it("filetransfer.spec.10 should be stopped by abort() right away", function (done) {
+
+                    var fileURL = 'http://cordova.apache.org/downloads/BlueZedEx.mp3';
+
+                    expect(transfer.abort).not.toThrow(); // should be a no-op.
+
+                    var startTime = +new Date();
+
+                    var downloadFail = function (error) {
+
+                        expect(error.code).toBe(FileTransferError.ABORT_ERR);
+                        expect(new Date() - startTime).toBeLessThan(GRACE_TIME_DELTA);
+
+                        // delay calling done() to wait for the bogus abort()
+                        setTimeout(done, GRACE_TIME_DELTA * 2);
+                    };
+
+                    transfer.download(fileURL, localFilePath, unexpectedCallbacks.httpWin, downloadFail);
+                    setTimeout(function() {
+                        transfer.abort();
+                    }, ABORT_DELAY);
+
+                    // call abort() again, after a time greater than the grace period
+                    setTimeout(function () {
+                        expect(transfer.abort).not.toThrow();
+                    }, GRACE_TIME_DELTA);
+                }, DOWNLOAD_TIMEOUT);
+
+                it("filetransfer.spec.12 should get http status on failure", function (done) {
+
+                    var fileURL = SERVER + "/404";
+
+                    var downloadFail = function (error) {
+
+                        expect(error.http_status).not.toBe(401, "Ensure " + fileURL + " is in the white list");
+                        expect(error.http_status).toBe(404);
+
+                        done();
+                    };
+
+                    transfer.download(fileURL, localFilePath, unexpectedCallbacks.httpWin, downloadFail);
+                }, DOWNLOAD_TIMEOUT);
+
+                it("filetransfer.spec.13 should get http body on failure", function (done) {
+
+                    var fileURL = SERVER + "/404";
+
+                    var downloadFail = function (error) {
+
+                        expect(error.http_status).not.toBe(401, "Ensure " + fileURL + " is in the white list");
+                        expect(error.http_status).toBe(404);
+
+                        expect(error.body).toBeDefined();
+                        expect(error.body).toMatch('You requested a 404');
+
+                        done();
+                    };
+
+                    transfer.download(fileURL, localFilePath, unexpectedCallbacks.httpWin, downloadFail);
+                }, DOWNLOAD_TIMEOUT);
+
+                it("filetransfer.spec.14 should handle malformed urls", function (done) {
+
+                    var fileURL = getMalformedUrl();
+
+                    var downloadFail = function (error) {
+
+                        // Note: Android needs the bad protocol to be added to the access list
+                        // <access origin=".*"/> won't match because ^https?:// is prepended to the regex
+                        // The bad protocol must begin with http to avoid automatic prefix
+                        expect(error.http_status).not.toBe(401, "Ensure " + fileURL + " is in the white list");
+                        expect(error.code).toBe(FileTransferError.INVALID_URL_ERR);
+
+                        done();
+                    };
+
+                    transfer.download(fileURL, localFilePath, unexpectedCallbacks.httpWin, downloadFail);
+                });
+
+                it("filetransfer.spec.15 should handle unknown host", function (done) {
+                    var fileURL = UNKNOWN_HOST;
+
+                    var downloadFail = function (error) {
+                        expect(error.code).toBe(FileTransferError.CONNECTION_ERR);
+                        done();
+                    };
+
+                    // turn off the onprogress handler
+                    transfer.onprogress = function () {};
+
+                    transfer.download(fileURL, localFilePath, unexpectedCallbacks.httpWin, downloadFail);
+                }, DOWNLOAD_TIMEOUT);
+
+                it("filetransfer.spec.16 should handle bad file path", function (done) {
+                    var fileURL = SERVER;
+                    transfer.download(fileURL, "c:\\54321", unexpectedCallbacks.httpWin, done);
+                });
+
+                it("filetransfer.spec.17 progress should work with gzip encoding", function (done) {
+
+                    // lengthComputable false on bb10 when downloading gzip
+                    if (cordova.platformId === 'blackberry10') {
+                        pending();
+                        return;
+                    }
+
+                    var fileURL = "http://www.apache.org/";
+
+                    var downloadWin = function (entry) {
+                        verifyDownload(entry);
+                        done();
+                    };
+
+                    transfer.download(fileURL, localFilePath, downloadWin, unexpectedCallbacks.httpFail);
+                }, DOWNLOAD_TIMEOUT);
+
+                it("filetransfer.spec.30 downloaded file entries should have a toNativeURL method", function (done) {
+
+                    if (cordova.platformId === 'browser') {
+                        pending();
+                        return;
+                    }
+
+                    var fileURL = SERVER + "/robots.txt";
+
+                    var downloadWin = function (entry) {
+
+                        expect(entry.toNativeURL).toBeDefined();
+                        expect(entry.toNativeURL).toEqual(jasmine.any(Function));
+
+                        var nativeURL = entry.toNativeURL();
+
+                        expect(nativeURL).toBeTruthy();
+                        expect(nativeURL).toEqual(jasmine.any(String));
+
+                        if (isWindows) {
+                            expect(nativeURL.substring(0, 14)).toBe('ms-appdata:///');
+                        } else if (isWP8) {
+                            expect(nativeURL.substring(0, 1)).toBe('/');
+                        } else {
+                            expect(nativeURL.substring(0, 7)).toBe('file://');
+                        }
+
+                        done();
+                    };
+
+                    transfer.download(fileURL, localFilePath, downloadWin, unexpectedCallbacks.httpFail);
+                }, DOWNLOAD_TIMEOUT);
+
+                it("filetransfer.spec.28 (compatibility) should be able to download a file using local paths", function (done) {
+
+                    var fileURL = SERVER + "/robots.txt";
+
+                    var unsupported = function (response) {
+                        expectedCallbacks.unsupportedOperation(response);
+                        done();
+                    };
+
+                    var downloadWin = function (entry) {
+                        verifyDownload(entry);
+                        done();
+                    };
+
+                    var internalFilePath;
+                    if (root.toInternalURL) {
+                        internalFilePath = root.toInternalURL() + fileName;
+                    } else {
+                        internalFilePath = localFilePath;
+                    }
+
+                    // This is an undocumented interface to File which exists only for testing
+                    // backwards compatibilty. By obtaining the raw filesystem path of the download
+                    // location, we can pass that to transfer.download() to make sure that previously-stored
+                    // paths are still valid.
+                    cordova.exec(function (localPath) {
+                        transfer.download(fileURL, localPath, downloadWin, unexpectedCallbacks.httpFail);
+                    }, unsupported, 'File', '_getLocalFilesystemPath', [internalFilePath]);
+                });
+            });
+
+            describe('upload', function() {
+
+                var uploadParams;
+                var uploadOptions;
+
+                var fileName;
+                var fileContents;
+                var localFilePath;
+
+                // helpers
+                var verifyUpload = function (uploadResult) {
+
+                    expect(uploadResult.bytesSent).toBeGreaterThan(0);
+                    expect(uploadResult.responseCode).toBe(200);
+
+                    var obj = null;
+                    try {
+                        obj = JSON.parse(uploadResult.response);
+                        expect(obj.fields).toBeDefined();
+                        expect(obj.fields.value1).toBe("test");
+                        expect(obj.fields.value2).toBe("param");
+                    } catch (e) {
+                        expect(obj).not.toBeNull('returned data from server should be valid json');
+                    }
+
+                    expect(transfer.onprogress).toHaveBeenCalled();
+                };
+
+                beforeEach(function(done) {
+
+                    fileName      = 'fileToUpload.txt';
+                    fileContents  = 'upload test file';
+
+                    uploadParams        = {};
+                    uploadParams.value1 = "test";
+                    uploadParams.value2 = "param";
+
+                    uploadOptions          = new FileUploadOptions();
+                    uploadOptions.fileKey  = "file";
+                    uploadOptions.fileName = fileName;
+                    uploadOptions.mimeType = "text/plain";
+                    uploadOptions.params   = uploadParams;
+
+                    var fileWin = function (entry) {
+                        localFilePath = entry.toURL();
+                        done();
+                    };
+
+                    // create a file to upload
+                    writeFile(root, fileName, fileContents, fileWin);
+                });
+
+                // delete the uploaded file
+                afterEach(function (done) {
+                    deleteFile(root, fileName, done);
+                });
+
+                it("filetransfer.spec.18 should be able to upload a file", function (done) {
+
+                    var fileURL = SERVER + '/upload';
+
+                    var uploadWin = function (uploadResult) {
+
+                        verifyUpload(uploadResult);
+
+                        if (cordova.platformId === 'ios') {
+                            expect(uploadResult.headers).toBeDefined('Expected headers to be defined.');
+                            expect(uploadResult.headers['Content-Type']).toBeDefined('Expected content-type header to be defined.');
+                        }
+
+                        done();
+                    };
+
+                    // NOTE: removing uploadOptions cause Android to timeout
+                    transfer.upload(localFilePath, fileURL, uploadWin, unexpectedCallbacks.httpFail, uploadOptions);
+                }, UPLOAD_TIMEOUT);
+
+                it("filetransfer.spec.19 should be able to upload a file with http basic auth", function (done) {
+
+                    var fileURL = SERVER_WITH_CREDENTIALS + "/upload_basic_auth";
+
+                    var uploadWin = function (uploadResult) {
+                        verifyUpload(uploadResult);
+                        done();
+                    };
+
+                    // NOTE: removing uploadOptions cause Android to timeout
+                    transfer.upload(localFilePath, fileURL, uploadWin, unexpectedCallbacks.httpFail, uploadOptions);
+                }, UPLOAD_TIMEOUT);
+
+                it("filetransfer.spec.21 should be stopped by abort() right away", function (done) {
+
+                    var fileURL = SERVER + '/upload';
+                    var startTime;
+
+                    var uploadFail = function (e) {
+                        expect(e.code).toBe(FileTransferError.ABORT_ERR);
+                        expect(new Date() - startTime).toBeLessThan(GRACE_TIME_DELTA);
+
+                        // delay calling done() to wait for the bogus abort()
+                        setTimeout(done, GRACE_TIME_DELTA * 2);
+                    };
+
+                    var fileWin = function () {
+
+                        startTime = +new Date();
+
+                        expect(transfer.abort).not.toThrow();
+
+                        // NOTE: removing uploadOptions cause Android to timeout
+                        transfer.upload(localFilePath, fileURL, unexpectedCallbacks.httpWin, uploadFail, uploadOptions);
+                        setTimeout(function() {
+                            transfer.abort();
+                        }, ABORT_DELAY);
+
+                        setTimeout(function () {
+                            expect(transfer.abort).not.toThrow();
+                        }, GRACE_TIME_DELTA);
+                    };
+
+                    writeFile(root, fileName, new Array(100000).join('aborttest!'), fileWin);
+                }, UPLOAD_TIMEOUT);
+
+                it("filetransfer.spec.22 should get http status and body on failure", function (done) {
+
+                    var fileURL = SERVER + '/403';
+
+                    var uploadFail = function (error) {
+                        expect(error.http_status).toBe(403);
+                        expect(error.http_status).not.toBe(401, "Ensure " + fileURL + " is in the white list");
+                        done();
+                    };
+
+                    transfer.upload(localFilePath, fileURL, unexpectedCallbacks.httpWin, uploadFail, uploadOptions);
+                }, UPLOAD_TIMEOUT);
+
+                it("filetransfer.spec.24 should handle malformed urls", function (done) {
+
+                    var fileURL = getMalformedUrl();
+
+                    var uploadFail = function (error) {
+                        expect(error.code).toBe(FileTransferError.INVALID_URL_ERR);
+                        expect(error.http_status).not.toBe(401, "Ensure " + fileURL + " is in the white list");
+                        done();
+                    };
+
+                    transfer.upload(localFilePath, fileURL, unexpectedCallbacks.httpWin, uploadFail, {});
+                });
+
+                it("filetransfer.spec.25 should handle unknown host", function (done) {
+
+                    var fileURL = UNKNOWN_HOST;
+
+                    var uploadFail = function (error) {
+                        expect(error.code).toBe(FileTransferError.CONNECTION_ERR);
+                        expect(error.http_status).not.toBe(401, "Ensure " + fileURL + " is in the white list");
+                        done();
+                    };
+
+                    transfer.upload(localFilePath, fileURL, unexpectedCallbacks.httpWin, uploadFail, {});
+                }, UPLOAD_TIMEOUT);
+
+                it("filetransfer.spec.25 should handle missing file", function (done) {
+
+                    var fileURL = SERVER + "/upload";
+
+                    var uploadFail = function (error) {
+                        expect(error.code).toBe(FileTransferError.FILE_NOT_FOUND_ERR);
+                        expect(error.http_status).not.toBe(401, "Ensure " + fileURL + " is in the white list");
+                        done();
+                    };
+
+                    transfer.upload('does_not_exist.txt', fileURL, unexpectedCallbacks.httpWin, uploadFail);
+                }, UPLOAD_TIMEOUT);
+
+                it("filetransfer.spec.26 should handle bad file path", function (done) {
+
+                    var fileURL = SERVER + "/upload";
+
+                    var uploadFail = function (error) {
+                        expect(error.http_status).not.toBe(401, "Ensure " + fileURL + " is in the white list");
+                        done();
+                    };
+
+                    transfer.upload("c:\\54321", fileURL, unexpectedCallbacks.httpWin, uploadFail);
+                });
+
+                it("filetransfer.spec.27 should be able to set custom headers", function (done) {
+
+                    if (cordova.platformId === 'windowsphone') {
+                        pending();
+                    }
+
+                    var fileURL = HEADERS_ECHO;
+
+                    var uploadWin = function (uploadResult) {
+
+                        expect(uploadResult.bytesSent).toBeGreaterThan(0);
+                        expect(uploadResult.responseCode).toBe(200);
+                        expect(uploadResult.response).toBeDefined();
+
+                        var responseHtml = decodeURIComponent(uploadResult.response);
+
+                        expect(responseHtml).toMatch(/CustomHeader1[\s\S]*CustomValue1/i);
+                        expect(responseHtml).toMatch(/CustomHeader2[\s\S]*CustomValue2[\s\S]*CustomValue3/i, "Should allow array values");
+
+                        done();
+                    };
+
+                    uploadOptions.headers = {
+                        "CustomHeader1": "CustomValue1",
+                        "CustomHeader2": ["CustomValue2", "CustomValue3"],
+                    };
+
+                    // NOTE: removing uploadOptions cause Android to timeout
+                    transfer.upload(localFilePath, fileURL, uploadWin, unexpectedCallbacks.httpFail, uploadOptions);
+                }, UPLOAD_TIMEOUT);
+
+                it("filetransfer.spec.29 (compatibility) should be able to upload a file using local paths", function (done) {
+
+                    var fileURL = SERVER + "/upload";
+
+                    var unsupported = function (response) {
+                        expectedCallbacks.unsupportedOperation(response);
+                        done();
+                    };
+
+                    var uploadWin = function (uploadResult) {
+                        verifyUpload(uploadResult);
+                        done();
+                    };
+
+                    var internalFilePath;
+                    if (root.toInternalURL) {
+                        internalFilePath = root.toInternalURL() + fileName;
+                    } else {
+                        internalFilePath = localFilePath;
+                    }
+
+                    // This is an undocumented interface to File which exists only for testing
+                    // backwards compatibilty. By obtaining the raw filesystem path of the download
+                    // location, we can pass that to transfer.download() to make sure that previously-stored
+                    // paths are still valid.
+                    cordova.exec(function (localPath) {
+                        transfer.upload(localPath, fileURL, uploadWin, unexpectedCallbacks.httpFail, uploadOptions);
+                    }, unsupported, 'File', '_getLocalFilesystemPath', [internalFilePath]);
+                }, UPLOAD_TIMEOUT);
+            });
+        });
+    });
+};
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    var imageURL = "http://apache.org/images/feather-small.gif";
+    var videoURL = "http://techslides.com/demos/sample-videos/small.mp4";
+
+    function clearResults() {
+        var results = document.getElementById("info");
+        results.innerHTML = '';
+    }
+
+    function downloadImg(source, urlFn, element, directory) {
+        var filename = source.substring(source.lastIndexOf("/") + 1);
+        filename = (directory || '') + filename;
+        function download(fileSystem) {
+            var ft = new FileTransfer();
+            console.log("Starting download");
+            ft.download(source, fileSystem.root.toURL() + filename, function (entry) {
+                console.log("Download complete");
+                element.src = urlFn(entry);
+                console.log("Src URL is " + element.src);
+                console.log("Inserting element");
+                document.getElementById("info").appendChild(element);
+            }, function (e) { console.log("ERROR: ft.download " + e.code); });
+        }
+        console.log("Requesting filesystem");
+        clearResults();
+        window.requestFileSystem(LocalFileSystem.TEMPORARY, 0, function (fileSystem) {
+            console.log("Checking for existing file");
+            if (typeof directory !== 'undefined') {
+                console.log("Checking for existing directory.");
+                fileSystem.root.getDirectory(directory, {}, function (dirEntry) {
+                    dirEntry.removeRecursively(function () {
+                        download(fileSystem);
+                    }, function () { console.log("ERROR: dirEntry.removeRecursively"); });
+                }, function () {
+                    download(fileSystem);
+                });
+            } else {
+                fileSystem.root.getFile(filename, { create: false }, function (entry) {
+                    console.log("Removing existing file");
+                    entry.remove(function () {
+                        download(fileSystem);
+                    }, function () { console.log("ERROR: entry.remove"); });
+                }, function () {
+                    download(fileSystem);
+                });
+            }
+        }, function () { console.log("ERROR: requestFileSystem"); });
+    }
+
+    /******************************************************************************/
+
+    var file_transfer_tests = '<h2>Image File Transfer Tests</h2>' +
+        '<h3>The following tests should display an image of the Apache feather in the status box</h3>' +
+        '<div id="cdv_image"></div>' +
+        '<div id="native_image"></div>' +
+        '<div id="non-existent_dir"></div>' +
+        '<h2>Video File Transfer Tests</h2>' +
+        '<h3>The following tests should display a video in the status box. The video should play when play is pressed</h3>' +
+        '<div id="cdv_video"></div>' +
+        '<div id="native_video"></div>';
+
+    contentEl.innerHTML = '<div id="info"></div>' +
+        file_transfer_tests;
+
+    createActionButton('Download and display img (cdvfile)', function () {
+        downloadImg(imageURL, function (entry) { return entry.toInternalURL(); }, new Image());
+    }, 'cdv_image');
+
+    createActionButton('Download and display img (native)', function () {
+        downloadImg(imageURL, function (entry) { return entry.toURL(); }, new Image());
+    }, 'native_image');
+
+    createActionButton('Download to a non-existent dir (should work)', function () {
+        downloadImg(imageURL, function (entry) { return entry.toURL(); }, new Image(), '/nonExistentDirTest/');
+    }, 'non-existent_dir');
+
+    createActionButton('Download and play video (cdvfile)', function () {
+        var videoElement = document.createElement('video');
+        videoElement.controls = "controls";
+        downloadImg(videoURL, function (entry) { return entry.toInternalURL(); }, videoElement);
+    }, 'cdv_video');
+
+    createActionButton('Download and play video (native)', function () {
+        var videoElement = document.createElement('video');
+        videoElement.controls = "controls";
+        downloadImg(videoURL, function (entry) { return entry.toURL(); }, videoElement);
+    }, 'native_video');
+};
diff --git a/test/unittest/tests/geolocation.tests.js b/test/unittest/tests/geolocation.tests.js
new file mode 100644 (file)
index 0000000..2d445c0
--- /dev/null
@@ -0,0 +1,425 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+exports.defineAutoTests = function () {
+    var fail = function (done, context, message) {
+            // prevents done() to be called several times
+            if (context) {
+                if (context.done) return;
+                context.done = true;
+            }
+
+            if (message) {
+                expect(false).toBe(true, message);
+            } else {
+                expect(false).toBe(true);
+            }
+
+            // watchPosition could call its callback sync (before returning the value)
+            // so we invoke done async to make sure we know watcher id to .clear in afterEach
+            setTimeout(function () {
+                done();
+            });
+        },
+        succeed = function (done, context) {
+            // prevents done() to be called several times
+            if (context) {
+                if (context.done) return;
+                context.done = true;
+            }
+
+            expect(true).toBe(true);
+
+            // watchPosition could call its callback sync (before returning the value)
+            // so we invoke done async to make sure we know watcher id to .clear in afterEach
+            setTimeout(function () {
+                done();
+            });
+        },
+        isWindowsStore = (cordova.platformId == "windows8") || (cordova.platformId == "windows" && !WinJS.Utilities.isPhone),
+        isAndroid = cordova.platformId == "android";
+
+    describe('Geolocation (navigator.geolocation)', function () {
+
+        it("geolocation.spec.1 should exist", function () {
+            expect(navigator.geolocation).toBeDefined();
+        });
+
+        it("geolocation.spec.2 should contain a getCurrentPosition function", function () {
+            expect(typeof navigator.geolocation.getCurrentPosition).toBeDefined();
+            expect(typeof navigator.geolocation.getCurrentPosition == 'function').toBe(true);
+        });
+
+        it("geolocation.spec.3 should contain a watchPosition function", function () {
+            expect(typeof navigator.geolocation.watchPosition).toBeDefined();
+            expect(typeof navigator.geolocation.watchPosition == 'function').toBe(true);
+        });
+
+        it("geolocation.spec.4 should contain a clearWatch function", function () {
+            expect(typeof navigator.geolocation.clearWatch).toBeDefined();
+            expect(typeof navigator.geolocation.clearWatch == 'function').toBe(true);
+        });
+
+    });
+
+    describe('getCurrentPosition method', function () {
+
+        describe('error callback', function () {
+
+            it("geolocation.spec.5 should be called if we set timeout to 0 and maximumAge to a very small number", function (done) {
+                // On Windows, this test prompts user for permission to use geolocation and interrupts autotests running.
+                // On Android geolocation Api is not available on emulator so we pended tests until we found the way to detect
+                // whether we run on emulator or real device from JavaScript. You can still run the tests on Android manually.
+                if (isWindowsStore || isAndroid) {
+                    pending();
+                }
+
+                navigator.geolocation.getCurrentPosition(
+                    fail.bind(null, done),
+                    succeed.bind(null, done),
+                    {
+                        maximumAge: 0,
+                        timeout: 0
+                    });
+            });
+
+        });
+
+        describe('success callback', function () {
+
+            it("geolocation.spec.6 should be called with a Position object", function (done) {
+                // On Windows, this test prompts user for permission to use geolocation and interrupts autotests running.
+                // On Android geolocation Api is not available on emulator so we pended tests until we found the way to detect
+                // whether we run on emulator or real device from JavaScript. You can still run the tests on Android manually.
+                if (isWindowsStore || isAndroid) {
+                    pending();
+                }
+
+                navigator.geolocation.getCurrentPosition(function (p) {
+                    expect(p.coords).toBeDefined();
+                    expect(p.timestamp).toBeDefined();
+                    done();
+                },
+                fail.bind(null, done),
+                {
+                    maximumAge: (5 * 60 * 1000) // 5 minutes maximum age of cached position
+                });
+            }, 25000); // first geolocation call can take several seconds on some devices
+        });
+
+    });
+
+    describe('watchPosition method', function () {
+
+        beforeEach(function(done) {
+            // This timeout is set to lessen the load on platform's geolocation services
+            // which were causing occasional test failures
+            setTimeout(function() {
+                done();
+            }, 100);
+        });
+
+        describe('error callback', function () {
+
+            var errorWatch = null;
+            afterEach(function () {
+                navigator.geolocation.clearWatch(errorWatch);
+            });
+
+            it("geolocation.spec.7 should be called if we set timeout to 0 and maximumAge to a very small number", function (done) {
+                // On Windows, this test prompts user for permission to use geolocation and interrupts autotests running.
+                // On Android geolocation Api is not available on emulator so we pended tests until we found the way to detect
+                // whether we run on emulator or real device from JavaScript. You can still run the tests on Android manually.
+                if (isWindowsStore || isAndroid) {
+                    pending();
+                }
+
+                var context = this;
+                errorWatch = navigator.geolocation.watchPosition(
+                    fail.bind(null, done, context, 'Unexpected win'),
+                    succeed.bind(null, done, context),
+                    {
+                        maximumAge: 0,
+                        timeout: 0
+                    });
+            });
+
+        });
+
+        describe('success callback', function () {
+
+            var successWatch = null;
+            afterEach(function () {
+                navigator.geolocation.clearWatch(successWatch);
+            });
+
+            it("geolocation.spec.8 should be called with a Position object", function (done) {
+                // On Windows, this test prompts user for permission to use geolocation and interrupts autotests running.
+                // On Android geolocation Api is not available on emulator so we pended tests until we found the way to detect
+                // whether we run on emulator or real device from JavaScript. You can still run the tests on Android manually.
+                if (isWindowsStore || isAndroid) {
+                    pending();
+                }
+
+                var context = this;
+                successWatch = navigator.geolocation.watchPosition(
+                    function (p) {
+                        // prevents done() to be called several times
+                        if (context.done) return;
+                        context.done = true;
+
+                        expect(p.coords).toBeDefined();
+                        expect(p.timestamp).toBeDefined();
+                        // callback could be called sync so we invoke done async to make sure we know watcher id to .clear in afterEach 
+                        setTimeout(function () {
+                            done();
+                        });
+                    },
+                    fail.bind(null, done, context, 'Unexpected fail callback'),
+                    {
+                        maximumAge: (5 * 60 * 1000) // 5 minutes maximum age of cached position
+                    });
+            });
+
+        });
+
+    });
+
+};
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    var newGeolocation = navigator.geolocation;
+    var origGeolocation = cordova.require('cordova/modulemapper').getOriginalSymbol(window, 'navigator.geolocation');
+    if (!origGeolocation) {
+        origGeolocation = newGeolocation;
+        newGeolocation = null;
+    }
+
+    var watchLocationId = null;
+
+    /**
+     * Start watching location
+     */
+    var watchLocation = function (usePlugin) {
+        console.log("watchLocation()");
+        var geo = usePlugin ? newGeolocation : origGeolocation;
+        if (!geo) {
+            alert('geolocation object is missing. usePlugin = ' + usePlugin);
+            return;
+        }
+
+        // Success callback
+        var success = function (p) {
+            setLocationDetails(p);
+        };
+
+        // Fail callback
+        var fail = function (e) {
+            console.log("watchLocation fail callback with error code " + e);
+            stopLocation(geo);
+        };
+
+        // Get location
+        watchLocationId = geo.watchPosition(success, fail, { enableHighAccuracy: true });
+        setLocationStatus("Running");
+    };
+
+    /**
+     * Stop watching the location
+     */
+    var stopLocation = function (usePlugin) {
+        console.log("stopLocation()");
+        var geo = usePlugin ? newGeolocation : origGeolocation;
+        if (!geo) {
+            alert('geolocation object is missing. usePlugin = ' + usePlugin);
+            return;
+        }
+        setLocationStatus("Stopped");
+        if (watchLocationId) {
+            geo.clearWatch(watchLocationId);
+            watchLocationId = null;
+        }
+    };
+
+    /**
+     * Get current location
+     */
+    var getLocation = function (usePlugin, opts) {
+        console.log("getLocation()");
+        var geo = usePlugin ? newGeolocation : origGeolocation;
+        if (!geo) {
+            alert('geolocation object is missing. usePlugin = ' + usePlugin);
+            return;
+        }
+
+        // Stop location if running
+        stopLocation(geo);
+
+        // Success callback
+        var success = function (p) {
+            setLocationDetails(p);
+            setLocationStatus("Done");
+        };
+
+        // Fail callback
+        var fail = function (e) {
+            console.log("getLocation fail callback with error code " + e.code);
+            setLocationStatus("Error: " + e.code);
+        };
+
+        setLocationStatus("Retrieving location...");
+
+        // Get location
+        geo.getCurrentPosition(success, fail, opts || { enableHighAccuracy: true }); //, {timeout: 10000});
+
+    };
+
+    /**
+     * Set location status
+     */
+    var setLocationStatus = function (status) {
+        document.getElementById('location_status').innerHTML = status;
+    };
+    var setLocationDetails = function (p) {
+        var date = (new Date(p.timestamp));
+        document.getElementById('latitude').innerHTML = p.coords.latitude;
+        document.getElementById('longitude').innerHTML = p.coords.longitude;
+        document.getElementById('altitude').innerHTML = p.coords.altitude;
+        document.getElementById('accuracy').innerHTML = p.coords.accuracy;
+        document.getElementById('heading').innerHTML = p.coords.heading;
+        document.getElementById('speed').innerHTML = p.coords.speed;
+        document.getElementById('altitude_accuracy').innerHTML = p.coords.altitudeAccuracy;
+        document.getElementById('timestamp').innerHTML = date.toDateString() + " " + date.toTimeString();
+    }
+
+    /******************************************************************************/
+
+    var location_div = '<div id="info">' +
+            '<b>Status:</b> <span id="location_status">Stopped</span>' +
+            '<table width="100%">',
+        latitude = '<tr>' +
+            '<td><b>Latitude:</b></td>' +
+            '<td id="latitude">&nbsp;</td>' +
+            '<td>(decimal degrees) geographic coordinate [<a href="http://dev.w3.org/geo/api/spec-source.html#lat">#ref]</a></td>' +
+            '</tr>',
+        longitude = '<tr>' +
+            '<td><b>Longitude:</b></td>' +
+            '<td id="longitude">&nbsp;</td>' +
+            '<td>(decimal degrees) geographic coordinate [<a href="http://dev.w3.org/geo/api/spec-source.html#lat">#ref]</a></td>' +
+            '</tr>',
+        altitude = '<tr>' +
+            '<td><b>Altitude:</b></td>' +
+            '<td id="altitude">&nbsp;</td>' +
+            '<td>null if not supported;<br>' +
+            '(meters) height above the [<a href="http://dev.w3.org/geo/api/spec-source.html#ref-wgs">WGS84</a>] ellipsoid. [<a href="http://dev.w3.org/geo/api/spec-source.html#altitude">#ref]</a></td>' +
+            '</tr>',
+        accuracy = '<tr>' +
+            '<td><b>Accuracy:</b></td>' +
+            '<td id="accuracy">&nbsp;</td>' +
+            '<td>(meters; non-negative; 95% confidence level) the accuracy level of the latitude and longitude coordinates. [<a href="http://dev.w3.org/geo/api/spec-source.html#accuracy">#ref]</a></td>' +
+            '</tr>',
+        heading = '<tr>' +
+            '<td><b>Heading:</b></td>' +
+            '<td id="heading">&nbsp;</td>' +
+            '<td>null if not supported;<br>' +
+            'NaN if speed == 0;<br>' +
+            '(degrees; 0° ? heading < 360°) direction of travel of the hosting device- counting clockwise relative to the true north. [<a href="http://dev.w3.org/geo/api/spec-source.html#heading">#ref]</a></td>' +
+            '</tr>',
+        speed = '<tr>' +
+            '<td><b>Speed:</b></td>' +
+            '<td id="speed">&nbsp;</td>' +
+            '<td>null if not supported;<br>' +
+            '(meters per second; non-negative) magnitude of the horizontal component of the hosting device current velocity. [<a href="http://dev.w3.org/geo/api/spec-source.html#speed">#ref]</a></td>' +
+            '</tr>',
+        altitude_accuracy = '<tr>' +
+            '<td><b>Altitude Accuracy:</b></td>' +
+            '<td id="altitude_accuracy">&nbsp;</td>' +
+            '<td>null if not supported;<br>(meters; non-negative; 95% confidence level) the accuracy level of the altitude. [<a href="http://dev.w3.org/geo/api/spec-source.html#altitude-accuracy">#ref]</a></td>' +
+            '</tr>',
+        time = '<tr>' +
+            '<td><b>Time:</b></td>' +
+            '<td id="timestamp">&nbsp;</td>' +
+            '<td>(DOMTimeStamp) when the position was acquired [<a href="http://dev.w3.org/geo/api/spec-source.html#timestamp">#ref]</a></td>' +
+            '</tr>' +
+            '</table>' +
+            '</div>',
+        actions =
+            '<h2>Use Built-in WebView navigator.geolocation</h2>' +
+            '<div id="built-in-getLocation"></div>' +
+            'Expected result: Will update all applicable values in status box for current location. Status will read Retrieving Location (may not see this if location is retrieved immediately) then Done.' +
+            '<p/> <div id="built-in-watchLocation"></div>' +
+            'Expected result: Will update all applicable values in status box for current location and update as location changes. Status will read Running.' +
+            '<p/> <div id="built-in-stopLocation"></div>' +
+            'Expected result: Will stop watching the location so values will not be updated. Status will read Stopped.' +
+            '<p/> <div id="built-in-getOld"></div>' +
+            'Expected result: Will update location values with a cached position that is up to 30 seconds old. Verify with time value. Status will read Done.' +
+            '<h2>Use Cordova Geolocation Plugin</h2>' +
+            '<div id="cordova-getLocation"></div>' +
+            'Expected result: Will update all applicable values in status box for current location. Status will read Retrieving Location (may not see this if location is retrieved immediately) then Done.' +
+            '<p/> <div id="cordova-watchLocation"></div>' +
+            'Expected result: Will update all applicable values in status box for current location and update as location changes. Status will read Running.' +
+            '<p/> <div id="cordova-stopLocation"></div>' +
+            'Expected result: Will stop watching the location so values will not be updated. Status will read Stopped.' +
+            '<p/> <div id="cordova-getOld"></div>' +
+            'Expected result: Will update location values with a cached position that is up to 30 seconds old. Verify with time value. Status will read Done.',
+        values_info =
+            '<h3>Details about each value are listed below in the status box</h3>',
+        note = 
+            '<h3>Allow use of current location, if prompted</h3>';
+
+    contentEl.innerHTML = values_info + location_div + latitude + longitude + altitude + accuracy + heading + speed
+        + altitude_accuracy + time + note + actions;
+
+    createActionButton('Get Location', function () {
+        getLocation(false);
+    }, 'built-in-getLocation');
+
+    createActionButton('Start Watching Location', function () {
+        watchLocation(false);
+    }, 'built-in-watchLocation');
+
+    createActionButton('Stop Watching Location', function () {
+        stopLocation(false);
+    }, 'built-in-stopLocation');
+
+    createActionButton('Get Location Up to 30 Sec Old', function () {
+        getLocation(false, { maximumAge: 30000 });
+    }, 'built-in-getOld');
+
+    createActionButton('Get Location', function () {
+        getLocation(true);
+    }, 'cordova-getLocation');
+
+    createActionButton('Start Watching Location', function () {
+        watchLocation(true);
+    }, 'cordova-watchLocation');
+
+    createActionButton('Stop Watching Location', function () {
+        stopLocation(true);
+    }, 'cordova-stopLocation');
+
+    createActionButton('Get Location Up to 30 Sec Old', function () {
+        getLocation(true, { maximumAge: 30000 });
+    }, 'cordova-getOld');
+};
diff --git a/test/unittest/tests/globalization.tests.js b/test/unittest/tests/globalization.tests.js
new file mode 100644 (file)
index 0000000..98fd31d
--- /dev/null
@@ -0,0 +1,555 @@
+/*
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you 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.
+*
+*/
+
+exports.defineAutoTests = function () {
+    var isWindowsPhone = cordova.platformId == 'windowsphone',
+        isWindows = (cordova.platformId === "windows") || (cordova.platformId === "windows8"),
+        isBrowser = cordova.platformId === "browser";
+
+    var fail = function (done) {
+        expect(true).toBe(false);
+        done();
+    };
+
+    describe('Globalization (navigator.globalization)', function () {
+
+        it("globalization.spec.1 should exist", function () {
+            expect(navigator.globalization).toBeDefined();
+        });
+
+        describe("getPreferredLanguage", function () {
+            var checkPreferredLanguage = function (a) {
+                expect(a).toBeDefined();
+                expect(typeof a).toBe('object');
+                expect(a.value).toBeDefined();
+                expect(typeof a.value).toBe('string');
+                expect(a.value.length > 0).toBe(true);
+            };
+            it("globalization.spec.1 should exist", function () {
+                expect(typeof navigator.globalization.getPreferredLanguage).toBeDefined();
+                expect(typeof navigator.globalization.getPreferredLanguage == 'function').toBe(true);
+            });
+            it("globalization.spec.3 getPreferredLanguage success callback should be called with a Properties object", function (done) {
+                navigator.globalization.getPreferredLanguage(function (a) {
+                    checkPreferredLanguage(a);
+                    done();
+                },
+                fail.bind(null, done));
+            });
+            it("globalization.spec.4 getPreferredLanguage return string should contain one or more language subtags separated by hyphen", function (done) {
+                navigator.globalization.getPreferredLanguage(function (a) {
+                    checkPreferredLanguage(a);
+                    expect(a.value.indexOf('_')).toBe(-1);
+
+                    // A language tag is composed from a sequence of one or more "subtags", separated by hyphen.
+                    // https://tools.ietf.org/html/bcp47#section-2.1
+                    expect(a.value.split('-').length).toBeGreaterThan(0); 
+
+                    done();
+                }, fail.bind(null, done));
+            });
+        });
+
+        describe("getLocaleName", function () {
+            var checkLocaleName = function (a) {
+                expect(a).toBeDefined();
+                expect(typeof a).toBe('object');
+                expect(a.value).toBeDefined();
+                expect(typeof a.value).toBe('string');
+                expect(a.value.length > 0).toBe(true);
+            };
+            it("globalization.spec.1 should exist", function () {
+                expect(typeof navigator.globalization.getLocaleName).toBeDefined();
+                expect(typeof navigator.globalization.getLocaleName == 'function').toBe(true);
+            });
+            it("globalization.spec.3 getLocaleName success callback should be called with a Properties object", function (done) {
+                navigator.globalization.getLocaleName(function (a) {
+                    checkLocaleName(a);
+                    done()
+                }, fail.bind(null, done));
+            });
+            it("globalization.spec.4 getLocaleName return string should have a hyphen", function (done) {
+                navigator.globalization.getLocaleName(function (a) {
+                    checkLocaleName(a);
+                    expect(a.value.indexOf('_')).toBe(-1);
+                    if (!isBrowser){
+                        // The browser implementation returns non-BCP 47 compatible
+                        // value in Chrome so we need to skip this expectation. See
+                        // https://github.com/MSOpenTech/cordova-plugin-globalization/blob/21f8a0ffa5aa2497ee970b6b5092b4c65fc4bf7e/README.md#browser-quirks-1
+                        expect(a.value.indexOf('-')).toBeGreaterThan(0);
+                    }
+                    done();
+                }, fail.bind(null, done));
+            });
+        });
+
+        describe('Globalization Constants (window.Globalization)', function () {
+            it("globalization.spec.1 should exist", function () {
+                expect(window.GlobalizationError).toBeDefined();
+                expect(window.GlobalizationError.UNKNOWN_ERROR).toBe(0);
+                expect(window.GlobalizationError.FORMATTING_ERROR).toBe(1);
+                expect(window.GlobalizationError.PARSING_ERROR).toBe(2);
+                expect(window.GlobalizationError.PATTERN_ERROR).toBe(3);
+            });
+        });
+
+        describe("dateToString", function () {
+            var checkDateToString = function (a) {
+                expect(a).toBeDefined();
+                expect(typeof a).toBe('object');
+                expect(a.value).toBeDefined();
+                expect(typeof a.value).toBe('string');
+                expect(a.value.length > 0).toBe(true);
+            };
+            it("globalization.spec.1 should exist", function () {
+                expect(typeof navigator.globalization.dateToString).toBeDefined();
+                expect(typeof navigator.globalization.dateToString == 'function').toBe(true);
+            });
+            it("globalization.spec.5 dateToString using default options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.dateToString(new Date(), function (a) {
+                    checkDateToString(a);
+                    done();
+                }, fail.bind(null, done));
+            });
+            it("globalization.spec.6 dateToString using formatLength=short and selector=date options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.dateToString(new Date(), function (a) {
+                    checkDateToString(a);
+                    done();
+                }, fail.bind(null, done),
+                { formatLength: 'short', selector: 'date' });
+            });
+            it("globalization.spec.7 dateToString using formatLength=full and selector=date options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.dateToString(new Date(), function (a) {
+                    checkDateToString(a);
+                    done();
+                }, fail.bind(null, done),
+                { formatLength: 'full', selector: 'date' });
+            });
+            it("globalization.spec.8 dateToString using formatLength=medium and selector=date and time(default) options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.dateToString(new Date(), function (a) {
+                    checkDateToString(a);
+                    done();
+                }, fail.bind(null, done),
+                { formatLength: 'medium' });
+            });
+            it("globalization.spec.9 dateToString using formatLength=long and selector=date and time(default) options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.dateToString(new Date(), function (a) {
+                    checkDateToString(a);
+                    done();
+                }, fail.bind(null, done),
+                { formatLength: 'long' });
+            });
+            it("globalization.spec.10 dateToString using formatLength=full and selector=date and time(default) options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.dateToString(new Date(), function (a) {
+                    checkDateToString(a);
+                    done();
+                }, fail.bind(null, done),
+                { formatLength: 'full' });
+            });
+        });
+
+        describe("stringToDate", function () {
+            var checkStringToDate = function (a) {
+                expect(a).toBeDefined();
+                expect(typeof a).toBe('object');
+                expect(a.year).toBeDefined();
+                expect(typeof a.year).toBe('number');
+                expect(a.year >= 0 && a.year <= 9999).toBe(true);
+                expect(a.month).toBeDefined();
+                expect(typeof a.month).toBe('number');
+                expect(a.month >= 0 && a.month <= 11).toBe(true);
+                expect(a.day).toBeDefined();
+                expect(typeof a.day).toBe('number');
+                expect(a.day >= 1 && a.day <= 31).toBe(true);
+                expect(a.hour).toBeDefined();
+                expect(typeof a.hour).toBe('number');
+                expect(a.hour >= 0 && a.hour <= 23).toBe(true);
+                expect(a.minute).toBeDefined();
+                expect(typeof a.minute).toBe('number');
+                expect(a.minute >= 0 && a.minute <= 59).toBe(true);
+                expect(a.second).toBeDefined();
+                expect(typeof a.second).toBe('number');
+                expect(a.second >= 0 && a.second <= 59).toBe(true);
+                expect(a.millisecond).toBeDefined();
+                expect(typeof a.millisecond).toBe('number');
+            };
+            it("globalization.spec.1 should exist", function () {
+                expect(typeof navigator.globalization.stringToDate).toBeDefined();
+                expect(typeof navigator.globalization.stringToDate == 'function').toBe(true);
+            });
+            it("globalization.spec.12 stringToDate using default options, success callback should be called with a Properties object", function (done) {
+                var win = function (a) {
+                    checkStringToDate(a);
+                    done();
+                };
+
+                navigator.globalization.dateToString(new Date(), function (a) {
+                    navigator.globalization.stringToDate(a.value, win, fail.bind(null, done));
+                }, fail.bind(null, done));
+            });
+            it("globalization.spec.13 stringToDate using formatLength=short and selector=date options, success callback should be called with a Properties object", function (done) {
+                var win = function (a) {
+                    checkStringToDate(a);
+                    done();
+                };
+
+                navigator.globalization.dateToString(new Date(), function (a) {
+                    navigator.globalization.stringToDate(a.value, win, fail.bind(null, done), { formatLength: 'short', selector: 'date' });
+                }, fail.bind(null, done), { formatLength: 'short', selector: 'date' });
+            });
+            it("globalization.spec.14 stringToDate using formatLength=full and selector=date options, success callback should be called with a Properties object", function (done) {
+                var win = function (a) {
+                    checkStringToDate(a);
+                    done();
+                };
+
+                navigator.globalization.dateToString(new Date(), function (a) {
+                    navigator.globalization.stringToDate(a.value, win, fail.bind(null, done), { formatLength: 'full', selector: 'date' });
+                }, fail.bind(null, done), { formatLength: 'full', selector: 'date' });
+            });
+            it("globalization.spec.15 stringToDate using invalid date, error callback should be called with a GlobalizationError object", function (done) {
+                navigator.globalization.stringToDate('notADate', fail.bind(null, done), function (a) {
+                    expect(a).toBeDefined();
+                    expect(typeof a).toBe('object');
+                    expect(a.code).toBeDefined();
+                    expect(typeof a.code).toBe('number');
+                    expect(a.code === GlobalizationError.PARSING_ERROR).toBe(true);
+                    expect(a.message).toBeDefined();
+                    expect(typeof a.message).toBe('string');
+                    expect(a.message !== "").toBe(true);
+                    done();
+                }, { selector: 'foobar' });
+            });
+        });
+
+        describe("getDatePattern", function () {
+            var checkDatePattern = function (a) {
+                expect(a).toBeDefined();
+                expect(typeof a).toBe('object');
+                expect(a.pattern).toBeDefined();
+                expect(typeof a.pattern).toBe('string');
+                if (!isBrowser) {
+                    // In browser the 'pattern' property is not supported and returns empty string.
+                    // https://github.com/MSOpenTech/cordova-plugin-globalization/blob/21f8a0ffa5aa2497ee970b6b5092b4c65fc4bf7e/README.md#browser-quirks-4
+                    expect(a.pattern.length > 0).toBe(true);
+                }
+                expect(a.timezone).toBeDefined();
+                expect(typeof a.timezone).toBe('string');
+                if (!isBrowser) {
+                    // The browser platform partially supports 'timezone'. Only Chrome returns 'timezone' property.
+                    // Its format is "Part of the world/{City}". Other browsers return empty string.
+                    // https://github.com/MSOpenTech/cordova-plugin-globalization/blob/21f8a0ffa5aa2497ee970b6b5092b4c65fc4bf7e/README.md#browser-quirks-4
+                    expect(a.pattern.length > 0).toBe(true);
+                }
+                expect(a.timezone.length > 0).toBe(true);
+                expect(a.utc_offset).toBeDefined();
+                expect(typeof a.utc_offset).toBe('number');
+                expect(a.dst_offset).toBeDefined();
+                expect(typeof a.dst_offset).toBe('number');
+            };
+            it("globalization.spec.1 should exist", function () {
+                expect(typeof navigator.globalization.getDatePattern).toBeDefined();
+                expect(typeof navigator.globalization.getDatePattern == 'function').toBe(true);
+            });
+            it("globalization.spec.17 getDatePattern using default options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.getDatePattern(function (a) {
+                    checkDatePattern(a);
+                    done();
+                }, fail.bind(null, done));
+            });
+            it("globalization.spec.18 getDatePattern using formatLength=medium and selector=date options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.getDatePattern(function (a) {
+                    checkDatePattern(a);
+                    done();
+                }, fail.bind(null, done),
+                { formatLength: 'medium', selector: 'date' });
+            });
+        });
+
+        describe("getDateNames", function () {
+            var checkDateNames = function (a) {
+                expect(a).toBeDefined();
+                expect(typeof a).toBe('object');
+                expect(a.value).toBeDefined();
+                expect(a.value instanceof Array).toBe(true);
+                expect(a.value.length > 0).toBe(true);
+                expect(typeof a.value[0]).toBe('string');
+            };
+            it("globalization.spec.1 should exist", function () {
+                expect(typeof navigator.globalization.getDateNames).toBeDefined();
+                expect(typeof navigator.globalization.getDateNames == 'function').toBe(true);
+            });
+            it("globalization.spec.20 getDateNames using default options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.getDateNames(function (a) {
+                    checkDateNames(a);
+                    done();
+                }, fail.bind(null, done));
+            });
+            it("globalization.spec.21 getDateNames using type=narrow and item=days options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.getDateNames(function (a) {
+                    checkDateNames(a);
+                    done();
+                }, fail.bind(null, done),
+                { type: 'narrow', item: 'days' });
+            });
+            it("globalization.spec.22 getDateNames using type=narrow and item=months options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.getDateNames(function (a) {
+                    checkDateNames(a);
+                    done();
+                }, fail.bind(null, done),
+                { type: 'narrow', item: 'months' });
+            });
+            it("globalization.spec.23 getDateNames using type=wide and item=days options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.getDateNames(function (a) {
+                    checkDateNames(a);
+                    done();
+                }, fail.bind(null, done),
+                { type: 'wide', item: 'days' });
+            });
+            it("globalization.spec.24 getDateNames using type=wide and item=months options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.getDateNames(function (a) {
+                    checkDateNames(a);
+                    done();
+                }, fail.bind(null, done),
+                { type: 'wide', item: 'months' });
+            });
+        });
+
+        describe("isDayLightSavingsTime", function () {
+            it("globalization.spec.1 should exist", function () {
+                expect(typeof navigator.globalization.isDayLightSavingsTime).toBeDefined();
+                expect(typeof navigator.globalization.isDayLightSavingsTime == 'function').toBe(true);
+            });
+            it("globalization.spec.26 isDayLightSavingsTime using default options, success callback should be called with a Properties object", function (done) {
+                navigator.globalization.isDayLightSavingsTime(new Date(), function (a) {
+                    expect(a).toBeDefined();
+                    expect(typeof a).toBe('object');
+                    expect(a.dst).toBeDefined();
+                    expect(typeof a.dst).toBe('boolean');
+                    done();
+                }, fail.bind(null, done));
+            });
+        });
+
+        describe("getFirstDayOfWeek", function () {
+            it("globalization.spec.1 should exist", function () {
+                expect(typeof navigator.globalization.getFirstDayOfWeek).toBeDefined();
+                expect(typeof navigator.globalization.getFirstDayOfWeek == 'function').toBe(true);
+            });
+            it("globalization.spec.28 getFirstDayOfWeek success callback should be called with a Properties object", function (done) {
+                navigator.globalization.getFirstDayOfWeek(function (a) {
+                    expect(a).toBeDefined();
+                    expect(typeof a).toBe('object');
+                    expect(a.value).toBeDefined();
+                    expect(typeof a.value).toBe('number');
+                    done();
+                }, fail.bind(null, done));
+            });
+        });
+
+        describe("numberToString", function () {
+            var checkNumberToString = function (a) {
+                expect(a).toBeDefined();
+                expect(typeof a).toBe('object');
+                expect(a.value).toBeDefined();
+                expect(typeof a.value).toBe('string');
+                expect(a.value.length > 0).toBe(true);
+            };
+            it("globalization.spec.1 should exist", function () {
+                expect(typeof navigator.globalization.numberToString).toBeDefined();
+                expect(typeof navigator.globalization.numberToString == 'function').toBe(true);
+            });
+            it("globalization.spec.30 numberToString using default options, should be called with a Properties object", function (done) {
+                navigator.globalization.numberToString(3.25, function (a) {
+                    checkNumberToString(a);
+                    done();
+                }, fail.bind(null, done));
+            });
+            it("globalization.spec.31 numberToString using type=percent options, should be called with a Properties object", function (done) {
+                navigator.globalization.numberToString(.25, function (a) {
+                    checkNumberToString(a);
+                    done();
+                }, fail.bind(null, done),
+                { type: 'percent' });
+            });
+            it("globalization.spec.32 numberToString using type=currency options, should be called with a Properties object", function (done) {
+                // the numberToString using type=currency is not supported on browser
+                // https://github.com/MSOpenTech/cordova-plugin-globalization/blob/21f8a0ffa5aa2497ee970b6b5092b4c65fc4bf7e/README.md#browser-quirks-7
+                if (isBrowser) {
+                    pending();
+                }
+                navigator.globalization.numberToString(5.20, function (a) {
+                    checkNumberToString(a);
+                    done();
+                }, fail.bind(null, done),
+                { type: 'currency' });
+            });
+        });
+
+        describe("stringToNumber", function () {
+            var checkStringToNumber = function (a) {
+                expect(a).toBeDefined();
+                expect(typeof a).toBe('object');
+                expect(a.value).toBeDefined();
+                expect(typeof a.value).toBe('number');
+                expect(a.value > 0).toBe(true);
+            };
+            it("globalization.spec.1 should exist", function () {
+                expect(typeof navigator.globalization.stringToNumber).toBeDefined();
+                expect(typeof navigator.globalization.stringToNumber == 'function').toBe(true);
+            });
+            it("globalization.spec.34 stringToNumber using default options, should be called with a Properties object", function (done) {
+                // the stringToNumber is not supported on browser
+                // https://github.com/MSOpenTech/cordova-plugin-globalization/blob/21f8a0ffa5aa2497ee970b6b5092b4c65fc4bf7e/README.md#supported-platforms-11
+                if (isBrowser) {
+                    pending();
+                }
+                var win = function (a) {
+                    checkStringToNumber(a);
+                    done();
+                };
+
+                navigator.globalization.numberToString(3.25, function (a) {
+                    navigator.globalization.stringToNumber(a.value, win, fail.bind(null, done));
+                }, fail.bind(null, done));
+            });
+            it("globalization.spec.35 stringToNumber using type=percent options, should be called with a Properties object", function (done) {
+                // the stringToNumber is not supported on browser
+                // https://github.com/MSOpenTech/cordova-plugin-globalization/blob/21f8a0ffa5aa2497ee970b6b5092b4c65fc4bf7e/README.md#supported-platforms-11
+                if (isBrowser) {
+                    pending();
+                }
+                var win = function (a) {
+                    checkStringToNumber(a);
+                    done();
+                };
+
+                navigator.globalization.numberToString(.25, function (a) {
+                    navigator.globalization.stringToNumber(a.value, win, fail.bind(null, done), { type: 'percent' });
+                }, fail.bind(null, done), { type: 'percent' });
+            });
+        });
+
+        describe("getNumberPattern", function () {
+            var checkNumberPattern = function (a) {
+                expect(a).toBeDefined();
+                expect(typeof a).toBe('object');
+                expect(a.pattern).toBeDefined();
+                expect(typeof a.pattern).toBe('string');
+                expect(a.pattern.length > 0).toBe(true);
+                expect(typeof a.symbol).toBe('string');
+                expect(typeof a.fraction).toBe('number');
+                expect(typeof a.rounding).toBe('number');
+                expect(a.positive).toBeDefined();
+                expect(typeof a.positive).toBe('string');
+                expect(a.positive.length >= 0).toBe(true);
+                expect(a.negative).toBeDefined();
+                expect(typeof a.negative).toBe('string');
+                expect(a.negative.length >= 0).toBe(true);
+                expect(a.decimal).toBeDefined();
+                expect(typeof a.decimal).toBe('string');
+                expect(a.decimal.length > 0).toBe(true);
+                expect(a.grouping).toBeDefined();
+                expect(typeof a.grouping).toBe('string');
+                expect(a.grouping.length > 0).toBe(true);
+            };
+
+            it("globalization.spec.1 should exist", function () {
+                expect(typeof navigator.globalization.getNumberPattern).toBeDefined();
+                expect(typeof navigator.globalization.getNumberPattern == 'function').toBe(true);
+            });
+            it("globalization.spec.37 getNumberPattern using default options, success callback should be called with a Properties object", function (done) {
+                // the pattern property is not supported on windows, windows phone and browser
+                // https://github.com/apache/cordova-plugin-globalization/blob/master/doc/index.md#windows-phone-8-quirks-5
+                // https://github.com/MSOpenTech/cordova-plugin-globalization/blob/21f8a0ffa5aa2497ee970b6b5092b4c65fc4bf7e/README.md#browser-quirks-6
+                if (isWindows || isWindowsPhone || isBrowser) {
+                    pending();
+                }
+                navigator.globalization.getNumberPattern(function (a) {
+                    checkNumberPattern(a);
+                    done();
+                }, fail.bind(null, done));
+            });
+            it("globalization.spec.38 getNumberPattern using type=percent, success callback should be called with a Properties object", function (done) {
+                // the pattern property is not supported on windows, windows phone and browser
+                // https://github.com/apache/cordova-plugin-globalization/blob/master/doc/index.md#windows-phone-8-quirks-5
+                // https://github.com/MSOpenTech/cordova-plugin-globalization/blob/21f8a0ffa5aa2497ee970b6b5092b4c65fc4bf7e/README.md#browser-quirks-6
+                if (isWindows || isWindowsPhone || isBrowser) {
+                    pending();
+                }
+                navigator.globalization.getNumberPattern(function (a) {
+                    checkNumberPattern(a);
+                    done();
+                }, fail.bind(null, done), { type: 'percent' });
+            });
+            it("globalization.spec.39 getNumberPattern using type=currency, success callback should be called with a Properties object", function (done) {
+                // the pattern property is not supported on windows, windows phone and browser
+                // https://github.com/apache/cordova-plugin-globalization/blob/master/doc/index.md#windows-phone-8-quirks-5
+                // https://github.com/MSOpenTech/cordova-plugin-globalization/blob/21f8a0ffa5aa2497ee970b6b5092b4c65fc4bf7e/README.md#browser-quirks-6
+                if (isWindows || isWindowsPhone || isBrowser) {
+                    pending();
+                }
+                navigator.globalization.getNumberPattern(function (a) {
+                    checkNumberPattern(a);
+                    done();
+                }, fail.bind(null, done), { type: 'currency' });
+            });
+        });
+
+        describe("getCurrencyPattern", function () {
+            it("globalization.spec.1 should exist", function () {
+                // wp8 is unsupported
+                if (isWindowsPhone) {
+                    pending();
+                }
+                expect(typeof navigator.globalization.getCurrencyPattern).toBeDefined();
+                expect(typeof navigator.globalization.getCurrencyPattern == 'function').toBe(true);
+            });
+            it("globalization.spec.41 getCurrencyPattern using EUR for currency, success callback should be called with a Properties object", function (done) {
+                // only `code` and `fraction` properties are supported on windows
+                // https://github.com/apache/cordova-plugin-globalization/blob/master/doc/index.md#windows-quirks-3
+                // wp8 and browser are unsupported
+                if (isWindowsPhone || isWindows || isBrowser) {
+                    pending();
+                }
+                navigator.globalization.getCurrencyPattern("EUR", function (a) {
+                    expect(a).toBeDefined();
+                    expect(typeof a).toBe('object');
+                    expect(a.pattern).toBeDefined();
+                    expect(typeof a.pattern).toBe('string');
+                    expect(a.pattern.length > 0).toBe(true);
+                    expect(a.code).toBeDefined();
+                    expect(typeof a.code).toBe('string');
+                    expect(a.code.length > 0).toBe(true);
+                    expect(typeof a.fraction).toBe('number');
+                    expect(typeof a.rounding).toBe('number');
+                    expect(a.decimal).toBeDefined();
+                    expect(typeof a.decimal).toBe('string');
+                    expect(a.decimal.length >= 0).toBe(true);
+                    expect(a.grouping).toBeDefined();
+                    expect(typeof a.grouping).toBe('string');
+                    expect(a.grouping.length >= 0).toBe(true);
+                    done();
+                }, fail.bind(null, done));
+            });
+        });
+    });
+};
diff --git a/test/unittest/tests/media.tests.js b/test/unittest/tests/media.tests.js
new file mode 100644 (file)
index 0000000..9af98de
--- /dev/null
@@ -0,0 +1,997 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+ */
+// increased timeout for actual playback to give device chance to download and play mp3 file
+// some emulators can be REALLY slow at this, so two minutes
+var ACTUAL_PLAYBACK_TEST_TIMEOUT = 2 * 60 * 1000;
+var isWindows = cordova.platformId == 'windows8' || cordova.platformId == 'windows';
+// detect whether audio hardware is available and enabled
+var isAudioSupported = isWindows ? Windows.Media.Devices.MediaDevice.getDefaultAudioRenderId(Windows.Media.Devices.AudioDeviceRole.default) : true;
+
+exports.defineAutoTests = function () {
+    var failed = function (done, msg, context) {
+        if (context && context.done) return;
+        context.done = true;
+        var info = typeof msg == 'undefined' ? 'Unexpected error callback' : msg;
+        expect(true).toFailWithMessage(info);
+        done();
+    };
+
+    var succeed = function (done, msg, context) {
+        if (context && context.done) return;
+        context.done = true;
+        var info = typeof msg == 'undefined' ? 'Unexpected success callback' : msg;
+        expect(true).toFailWithMessage(info);
+        done();
+    };
+
+    describe('Media', function () {
+
+        beforeEach(function () {
+            // Custom Matcher
+            jasmine.Expectation.addMatchers({
+                toFailWithMessage : function () {
+                    return {
+                        compare : function (error, message) {
+                            var pass = false;
+                            return {
+                                pass : pass,
+                                message : message
+                            };
+                        }
+                    };
+                }
+            });
+        });
+
+        it("media.spec.1 should exist", function () {
+            expect(Media).toBeDefined();
+            expect(typeof Media).toBe("function");
+        });
+
+        it("media.spec.2 should have the following properties", function () {
+            var media1 = new Media("dummy");
+            expect(media1.id).toBeDefined();
+            expect(media1.src).toBeDefined();
+            expect(media1._duration).toBeDefined();
+            expect(media1._position).toBeDefined();
+            media1.release();
+        });
+
+        it("media.spec.3 should define constants for Media status", function () {
+            expect(Media).toBeDefined();
+            expect(Media.MEDIA_NONE).toBe(0);
+            expect(Media.MEDIA_STARTING).toBe(1);
+            expect(Media.MEDIA_RUNNING).toBe(2);
+            expect(Media.MEDIA_PAUSED).toBe(3);
+            expect(Media.MEDIA_STOPPED).toBe(4);
+        });
+
+        it("media.spec.4 should define constants for Media errors", function () {
+            expect(MediaError).toBeDefined();
+            expect(MediaError.MEDIA_ERR_NONE_ACTIVE).toBe(0);
+            expect(MediaError.MEDIA_ERR_ABORTED).toBe(1);
+            expect(MediaError.MEDIA_ERR_NETWORK).toBe(2);
+            expect(MediaError.MEDIA_ERR_DECODE).toBe(3);
+            expect(MediaError.MEDIA_ERR_NONE_SUPPORTED).toBe(4);
+        });
+
+        it("media.spec.5 should contain a play function", function () {
+            var media1 = new Media("dummy");
+            expect(media1.play).toBeDefined();
+            expect(typeof media1.play).toBe('function');
+            media1.release();
+        });
+
+        it("media.spec.6 should contain a stop function", function () {
+            var media1 = new Media("dummy");
+            expect(media1.stop).toBeDefined();
+            expect(typeof media1.stop).toBe('function');
+            media1.release();
+        });
+
+        it("media.spec.7 should contain a seekTo function", function () {
+            var media1 = new Media("dummy");
+            expect(media1.seekTo).toBeDefined();
+            expect(typeof media1.seekTo).toBe('function');
+            media1.release();
+        });
+
+        it("media.spec.8 should contain a pause function", function () {
+            var media1 = new Media("dummy");
+            expect(media1.pause).toBeDefined();
+            expect(typeof media1.pause).toBe('function');
+            media1.release();
+        });
+
+        it("media.spec.9 should contain a getDuration function", function () {
+            var media1 = new Media("dummy");
+            expect(media1.getDuration).toBeDefined();
+            expect(typeof media1.getDuration).toBe('function');
+            media1.release();
+        });
+
+        it("media.spec.10 should contain a getCurrentPosition function", function () {
+            var media1 = new Media("dummy");
+            expect(media1.getCurrentPosition).toBeDefined();
+            expect(typeof media1.getCurrentPosition).toBe('function');
+            media1.release();
+        });
+
+        it("media.spec.11 should contain a startRecord function", function () {
+            var media1 = new Media("dummy");
+            expect(media1.startRecord).toBeDefined();
+            expect(typeof media1.startRecord).toBe('function');
+            media1.release();
+        });
+
+        it("media.spec.12 should contain a stopRecord function", function () {
+            var media1 = new Media("dummy");
+            expect(media1.stopRecord).toBeDefined();
+            expect(typeof media1.stopRecord).toBe('function');
+            media1.release();
+        });
+
+        it("media.spec.13 should contain a release function", function () {
+            var media1 = new Media("dummy");
+            expect(media1.release).toBeDefined();
+            expect(typeof media1.release).toBe('function');
+            media1.release();
+        });
+
+        it("media.spec.14 should contain a setVolume function", function () {
+            var media1 = new Media("dummy");
+            expect(media1.setVolume).toBeDefined();
+            expect(typeof media1.setVolume).toBe('function');
+            media1.release();
+        });
+
+        it("media.spec.15 should return MediaError for bad filename", function (done) {
+            //bb10 dialog pops up, preventing tests from running
+            if (cordova.platformId === 'blackberry10') {
+                pending();
+            }
+
+            var context = this,
+                fileName = 'invalid.file.name',
+                badMedia = new Media(fileName, succeed.bind(null, done, ' badMedia = new Media , Unexpected succees callback, it should not create Media object with invalid file name'), function (result) {
+                    if (context.done) return;
+                    context.done = true;
+
+                    expect(result).toBeDefined();
+                    expect(result.code).toBe(MediaError.MEDIA_ERR_ABORTED);
+                    if (badMedia) {
+                        badMedia.release();
+                    }
+                    done();
+                });
+            badMedia.play();
+        });
+
+        describe('actual playback', function() {
+            var checkInterval,
+                media;
+
+            afterEach(function() {
+                clearInterval(checkInterval);
+                if (media) {
+                    media.stop();
+                    media.release();
+                    media = null;
+                }
+            });
+
+            it("media.spec.16 position should be set properly", function (done) {
+                // no audio hardware available
+                if (!isAudioSupported) {
+                    pending();
+                }
+
+                //context variable used as an extra security statement to ensure that the callback is processed only once,
+                //in case the statusChange callback is reached more than one time with the same status code.
+                //Some information about this kind of behaviour can be found at JIRA: CB-7099.
+                var context = this,
+                    mediaFile = 'http://cordova.apache.org/downloads/BlueZedEx.mp3',
+                    successCallback = function () { },
+                    statusChange = function (statusCode) {
+                        if (!context.done && statusCode == Media.MEDIA_RUNNING) {
+                            checkInterval = setInterval(function () {
+                                if (context.done) return;
+                                media.getCurrentPosition(function successCallback(position) {
+                                    if (position > 0.0) {
+                                        context.done = true;
+                                        expect(true).toBe(true);
+                                        done();
+                                    }
+                                }, failed.bind(null, done, 'media1.getCurrentPosition - Error getting media current position', context));
+                            }, 1000);
+                        }
+                    };
+                media = new Media(mediaFile, successCallback, failed.bind(self, done, 'media1 = new Media - Error creating Media object. Media file: ' + mediaFile, context), statusChange);
+                media.play();
+            }, ACTUAL_PLAYBACK_TEST_TIMEOUT);
+
+            it("media.spec.17 duration should be set properly", function (done) {
+                if (!isAudioSupported || cordova.platformId === 'blackberry10') {
+                    pending();
+                }
+
+                //context variable used as an extra security statement to ensure that the callback is processed only once,
+                //in case the statusChange callback is reached more than one time with the same status code.
+                //Some information about this kind of behaviour can be found at JIRA: CB-7099.
+                var context = this,
+                    mediaFile = 'http://cordova.apache.org/downloads/BlueZedEx.mp3',
+                    successCallback = function () { },
+                    statusChange = function (statusCode) {
+                        if (!context.done && statusCode == Media.MEDIA_RUNNING) {
+                            checkInterval = setInterval(function () {
+                                if (context.done) return;
+                                media.getCurrentPosition(function (position) {
+                                    if (position > 0.0) {
+                                        context.done = true;
+                                        expect(media.getDuration()).toBeGreaterThan(0.0);
+                                        done();
+                                    }
+                                }, failed.bind(null, done, 'media1.getCurrentPosition - Error getting media current position', context));
+                            }, 1000);
+                        }
+                    };
+                media = new Media(mediaFile, successCallback, failed.bind(self, done, 'media1 = new Media - Error creating Media object. Media file: ' + mediaFile, context), statusChange);
+                media.play();
+            }, ACTUAL_PLAYBACK_TEST_TIMEOUT);
+
+            it("media.spec.20 should be able to resume playback after pause", function (done) {
+                if (!isAudioSupported || cordova.platformId === 'blackberry10') {
+                    pending();
+                }
+
+                //context variable used as an extra security statement to ensure that the callback is processed only once,
+                //in case the statusChange callback is reached more than one time with the same status code.
+                //Some information about this kind of behaviour can be found at JIRA: CB-7099.
+                var context = this;
+                var resumed = false;
+                var mediaFile = 'http://cordova.apache.org/downloads/BlueZedEx.mp3';
+                var successCallback = function () { };
+                var statusChange = function (statusCode) {
+                    if (context.done) return;
+
+                    if (statusCode == Media.MEDIA_RUNNING) {
+                        if (!resumed) {
+                            media.seekTo(20000);
+                            media.pause();
+                            return;
+                        }
+
+                        media.getCurrentPosition(function (position) {
+                            expect(position).toBeCloseTo(20, 0);
+                            context.done = true;
+                            done();
+                        }, failed.bind(null, done, 'media1.getCurrentPosition - Error getting media current position', context))
+                    }
+
+                    if (statusCode == Media.MEDIA_PAUSED) {
+                        resumed = true;
+                        media.play();
+                    }
+                };
+                media = new Media(mediaFile, successCallback, failed.bind(self, done, 'media1 = new Media - Error creating Media object. Media file: ' + mediaFile, context), statusChange);
+                media.play();
+            }, ACTUAL_PLAYBACK_TEST_TIMEOUT);
+
+            it("media.spec.21 should be able to seek through file", function (done) {
+                if (!isAudioSupported || cordova.platformId === 'blackberry10') {
+                    pending();
+                }
+
+                //context variable used as an extra security statement to ensure that the callback is processed only once,
+                //in case the statusChange callback is reached more than one time with the same status code.
+                //Some information about this kind of behaviour can be found at JIRA: CB-7099.
+                var context = this;
+                var mediaFile = 'http://cordova.apache.org/downloads/BlueZedEx.mp3';
+                var successCallback = function () { };
+                var statusChange = function (statusCode) {
+                    if (!context.done && statusCode == Media.MEDIA_RUNNING) {
+                        checkInterval = setInterval(function () {
+                            if (context.done) return;
+                            media.seekTo(5000);
+                            media.getCurrentPosition(function (position) {
+                                expect(position).toBeCloseTo(5, 0);
+                                context.done = true;
+                                done();
+                            }, failed.bind(null, done, 'media1.getCurrentPosition - Error getting media current position', context));
+                        }, 1000);
+                    }
+                };
+                media = new Media(mediaFile, successCallback, failed.bind(self, done, 'media1 = new Media - Error creating Media object. Media file: ' + mediaFile, context), statusChange);
+                media.play();
+            }, ACTUAL_PLAYBACK_TEST_TIMEOUT);
+        });
+
+        it("media.spec.18 should contain a setRate function", function () {
+            var media1 = new Media("dummy");
+            expect(media1.setRate).toBeDefined();
+            expect(typeof media1.setRate).toBe('function');
+            media1.release();
+        });
+
+        it("media.spec.19 playback rate should be set properly using setRate", function (done) {
+            if (cordova.platformId !== 'ios') {
+                expect(true).toFailWithMessage('Platform does not supported this feature');
+                pending();
+                return;
+            }
+            var mediaFile = 'http://cordova.apache.org/downloads/BlueZedEx.mp3',
+                mediaState = Media.MEDIA_STOPPED,
+                successCallback,
+                flag = true,
+                statusChange = function (statusCode) {
+                    if (statusCode == Media.MEDIA_RUNNING && flag) {
+                        //flag variable used to ensure an extra security statement to ensure that the callback is processed only once,
+                        //in case for some reason the statusChange callback is reached more than one time with the same status code.
+                        //Some information about this kind of behavior it can be found at JIRA: CB-7099
+                        flag = false;
+                        setTimeout(function () {
+                            media1.getCurrentPosition(function (position) {
+                                //in four seconds expect position to be two times greater with some degree (1 sec) of accuracy
+                                expect(position).toBeGreaterThan(7);
+                                media1.stop();
+                                media1.release();
+                                done();
+                            }, failed.bind(null, done, 'media1.getCurrentPosition - Error getting media current position'));
+                        }, 4000);
+                    }
+                },
+                media1 = new Media(mediaFile, successCallback, failed.bind(null, done, 'media1 = new Media - Error creating Media object. Media file: ' + mediaFile), statusChange);
+            //make audio playback two times faster
+            media1.setRate(2);
+            media1.play();
+        }, ACTUAL_PLAYBACK_TEST_TIMEOUT);
+    });
+};
+
+//******************************************************************************************
+//***************************************Manual Tests***************************************
+//******************************************************************************************
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    //-------------------------------------------------------------------------
+    // Audio player
+    //-------------------------------------------------------------------------
+    var media1 = null;
+    var media1Timer = null;
+    var audioSrc = null;
+    var defaultaudio = "http://cordova.apache.org/downloads/BlueZedEx.mp3";
+
+    //Play audio function
+    function playAudio(url) {
+        console.log("playAudio()");
+        console.log(" -- media=" + media1);
+
+        var src = defaultaudio;
+
+        if (url) {
+            src = url;
+        }
+
+        // Stop playing if src is different from currently playing source
+        if (src !== audioSrc) {
+            if (media1 !== null) {
+                stopAudio();
+                media1 = null;
+            }
+        }
+
+        if (media1 === null) {
+
+            // TEST STREAMING AUDIO PLAYBACK
+            //var src = "http://nunzioweb.com/misc/Bon_Jovi-Crush_Mystery_Train.mp3";   // works
+            //var src = "http://nunzioweb.com/misc/Bon_Jovi-Crush_Mystery_Train.m3u"; // doesn't work
+            //var src = "http://www.wav-sounds.com/cartoon/bugsbunny1.wav"; // works
+            //var src = "http://www.angelfire.com/fl5/html-tutorial/a/couldyou.mid"; // doesn't work
+            //var src = "MusicSearch/mp3/train.mp3";    // works
+            //var src = "bryce.mp3";  // works
+            //var src = "/android_asset/www/bryce.mp3"; // works
+
+            media1 = new Media(src,
+                    function () {
+                    console.log("playAudio():Audio Success");
+                },
+                    function (err) {
+                    console.log("playAudio():Audio Error: " + err.code);
+                    setAudioStatus("Error: " + err.code);
+                },
+                    function (status) {
+                    console.log("playAudio():Audio Status: " + status);
+                    setAudioStatus(Media.MEDIA_MSG[status]);
+
+                    // If stopped, then stop getting current position
+                    if (Media.MEDIA_STOPPED == status) {
+                        clearInterval(media1Timer);
+                        media1Timer = null;
+                        setAudioPosition("0 sec");
+                    }
+                });
+        }
+        audioSrc = src;
+        document.getElementById('durationValue').innerHTML = "";
+        // Play audio
+        media1.play();
+        if (media1Timer === null && media1.getCurrentPosition) {
+            media1Timer = setInterval(
+                    function () {
+                    media1.getCurrentPosition(
+                        function (position) {
+                        if (position >= 0.0) {
+                            setAudioPosition(position + " sec");
+                        }
+                    },
+                        function (e) {
+                        console.log("Error getting pos=" + e);
+                        setAudioPosition("Error: " + e);
+                    });
+                },
+                    1000);
+        }
+
+        // Get duration
+        var counter = 0;
+        var timerDur = setInterval(
+                function () {
+                counter = counter + 100;
+                if (counter > 2000) {
+                    clearInterval(timerDur);
+                }
+                var dur = media1.getDuration();
+                if (dur > 0) {
+                    clearInterval(timerDur);
+                    document.getElementById('durationValue').innerHTML = dur + " sec";
+                }
+            }, 100);
+    }
+
+    //Pause audio playback
+    function pauseAudio() {
+        console.log("pauseAudio()");
+        if (media1) {
+            media1.pause();
+        }
+    }
+
+    //Stop audio
+    function stopAudio() {
+        console.log("stopAudio()");
+        if (media1) {
+            media1.stop();
+        }
+        clearInterval(media1Timer);
+        media1Timer = null;
+    }
+
+    //Release audio
+    function releaseAudio() {
+        console.log("releaseAudio()");
+        if (media1) {
+            media1.stop(); //imlied stop of playback, resets timer
+            media1.release();
+        }
+    }
+
+    //Set audio status
+    function setAudioStatus(status) {
+        document.getElementById('statusValue').innerHTML = status;
+    }
+
+    //Set audio position
+    function setAudioPosition(position) {
+        document.getElementById('positionValue').innerHTML = position;
+    }
+
+    //Seek audio
+    function seekAudio(mode) {
+        var time = document.getElementById("seekInput").value;
+        if (time === "") {
+            time = 5000;
+        } else {
+            time = time * 1000; //we expect the input to be in seconds
+        }
+        if (media1 === null) {
+            console.log("seekTo requested while media1 is null");
+            if (audioSrc === null) {
+                audioSrc = defaultaudio;
+            }
+            media1 = new Media(audioSrc,
+                    function () {
+                    console.log("seekToAudio():Audio Success");
+                },
+                    function (err) {
+                    console.log("seekAudio():Audio Error: " + err.code);
+                    setAudioStatus("Error: " + err.code);
+                },
+                    function (status) {
+                    console.log("seekAudio():Audio Status: " + status);
+                    setAudioStatus(Media.MEDIA_MSG[status]);
+
+                    // If stopped, then stop getting current position
+                    if (Media.MEDIA_STOPPED == status) {
+                        clearInterval(media1Timer);
+                        media1Timer = null;
+                        setAudioPosition("0 sec");
+                    }
+                });
+        }
+
+        media1.getCurrentPosition(
+            function (position) {
+            var deltat = time;
+            if (mode === "by") {
+                deltat = time + position * 1000;
+            }
+            media1.seekTo(deltat,
+                function () {
+                console.log("seekAudioTo():Audio Success");
+                //force an update on the position display
+                updatePosition();
+            },
+                function (err) {
+                console.log("seekAudioTo():Audio Error: " + err.code);
+            });
+        },
+            function (e) {
+            console.log("Error getting pos=" + e);
+            setAudioPosition("Error: " + e);
+        });
+    }
+
+    //for forced updates of position after a successful seek
+
+    function updatePosition() {
+        media1.getCurrentPosition(
+            function (position) {
+            if (position >= 0.0) {
+                setAudioPosition(position + " sec");
+            }
+        },
+            function (e) {
+            console.log("Error getting pos=" + e);
+            setAudioPosition("Error: " + e);
+        });
+    }
+
+    //-------------------------------------------------------------------------
+    // Audio recorder
+    //-------------------------------------------------------------------------
+    var mediaRec = null;
+    var recTime = 0;
+    var recordSrc = "myRecording.mp3";
+
+    //Record audio
+    function recordAudio() {
+        console.log("recordAudio()");
+        console.log(" -- media=" + mediaRec);
+
+        releaseAudio();
+
+        if (!mediaRec) {
+            var src = recordSrc;
+            mediaRec = new Media(src,
+                    function () {
+                    console.log("recordAudio():Audio Success");
+                },
+                    function (err) {
+                    console.log("recordAudio():Audio Error: " + err.code);
+                    setAudioStatus("Error: " + err.code);
+                },
+                    function (status) {
+                    console.log("recordAudio():Audio Status: " + status);
+                    setAudioStatus(Media.MEDIA_MSG[status]);
+                });
+        }
+
+        // Record audio
+        mediaRec.startRecord();
+
+        // Stop recording after 10 sec
+        recTime = 0;
+        var recInterval = setInterval(function () {
+                recTime = recTime + 1;
+                setAudioPosition(recTime + " sec");
+                if (recTime >= 10) {
+                    clearInterval(recInterval);
+                    if (mediaRec.stopAudioRecord) {
+                        mediaRec.stopAudioRecord();
+                    } else {
+                        mediaRec.stopRecord();
+                    }
+                    console.log("recordAudio(): stop");
+                }
+            }, 1000);
+    }
+
+    //Play back recorded audio
+    function playRecording() {
+        playAudio(recordSrc);
+    }
+
+    //Function to create a file for iOS recording
+
+    function getRecordSrc() {
+        var fsFail = function (error) {
+            console.log("error creating file for iOS recording");
+        };
+        var gotFile = function (file) {
+            recordSrc = file.fullPath;
+            //console.log("recording Src: " + recordSrc);
+        };
+        var gotFS = function (fileSystem) {
+            fileSystem.root.getFile("iOSRecording.wav", {
+                create : true
+            }, gotFile, fsFail);
+        };
+        window.requestFileSystem(LocalFileSystem.TEMPORARY, 0, gotFS, fsFail);
+    }
+
+    //Function to create a file for BB recording
+    function getRecordSrcBB() {
+        var fsFail = function (error) {
+            console.log("error creating file for BB recording");
+        };
+        var gotFile = function (file) {
+            recordSrc = file.fullPath;
+        };
+        var gotFS = function (fileSystem) {
+            fileSystem.root.getFile("BBRecording.amr", {
+                create : true
+            }, gotFile, fsFail);
+        };
+        window.requestFileSystem(LocalFileSystem.TEMPORARY, 0, gotFS, fsFail);
+    }
+
+    //Function to create a file for Windows recording
+    function getRecordSrcWin() {
+        var fsFail = function (error) {
+            console.log("error creating file for Win recording");
+        };
+        var gotFile = function (file) {
+            recordSrc = file.name;
+        };
+        var gotFS = function (fileSystem) {
+            fileSystem.root.getFile("WinRecording.m4a", {
+                create: true
+            }, gotFile, fsFail);
+        };
+        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, gotFS, fsFail);
+    }
+
+//Generate Dynamic Table
+    function generateTable(tableId, rows, cells, elements) {
+        var table = document.createElement('table');
+        for (var r = 0; r < rows; r++) {
+            var row = table.insertRow(r);
+            for (var c = 0; c < cells; c++) {
+                var cell = row.insertCell(c);
+                cell.setAttribute("align", "center");
+                for (var e in elements) {
+                    if (elements[e].position.row == r && elements[e].position.cell == c) {
+                        var htmlElement = document.createElement(elements[e].tag);
+                        var content;
+
+                        if (elements[e].content !== "") {
+                            content = document.createTextNode(elements[e].content);
+                            htmlElement.appendChild(content);
+                        }
+                        if (elements[e].type) {
+                            htmlElement.type = elements[e].type;
+                        }
+                        htmlElement.setAttribute("id", elements[e].id);
+                        cell.appendChild(htmlElement);
+                    }
+                }
+            }
+        }
+        table.setAttribute("align", "center");
+        table.setAttribute("id", tableId);
+        return table;
+    }
+
+//Audio && Record Elements
+    var elementsResultsAudio=
+    [{
+            id : "statusTag",
+            content : "Status:",
+            tag : "div",
+            position : {
+                row : 0,
+                cell : 0
+            }
+        }, {
+            id : "statusValue",
+            content : "",
+            tag : "div",
+            position : {
+                row : 0,
+                cell : 2
+            }
+        }, {
+            id : "durationTag",
+            content : "Duration:",
+            tag : "div",
+            position : {
+                row : 1,
+                cell : 0
+            }
+        }, {
+            id : "durationValue",
+            content : "",
+            tag : "div",
+            position : {
+                row : 1,
+                cell : 2
+            }
+        }, {
+            id : "positionTag",
+            content : "Position:",
+            tag : "div",
+            position : {
+                row : 2,
+                cell : 0
+            }
+        }, {
+            id : "positionValue",
+            content : "",
+            tag : "div",
+            position : {
+                row : 2,
+                cell : 2
+            }
+        }],
+        elementsAudio =
+        [{
+            id : "playBtn",
+            content : "",
+            tag : "div",
+            position : {
+                row : 0,
+                cell : 0
+            }
+        }, {
+            id : "pauseBtn",
+            content : "",
+            tag : "div",
+            position : {
+                row : 0,
+                cell : 1
+            }
+        }, {
+            id : "stopBtn",
+            content : "",
+            tag : "div",
+            position : {
+                row : 1,
+                cell : 0
+            }
+        }, {
+            id : "releaseBtn",
+            content : "",
+            tag : "div",
+            position : {
+                row : 1,
+                cell : 1
+            }
+        }, {
+            id : "seekByBtn",
+            content : "",
+            tag : "div",
+            position : {
+                row : 2,
+                cell : 0
+            }
+        }, {
+            id : "seekToBtn",
+            content : "",
+            tag : "div",
+            position : {
+                row : 2,
+                cell : 1
+            }
+        }, {
+            id : "seekInput",
+            content : "",
+            tag : "input",
+            type : "number",
+            position : {
+                row : 2,
+                cell : 2
+            }
+        }, {
+            id: "halfSpeedBtn",
+            content:"",
+            tag:"div",
+            position:{
+                row:0,
+                cell:2
+            }
+        }
+    ],
+    elementsRecord =
+        [{
+            id : "recordbtn",
+            content : "",
+            tag : "div",
+            position : {
+                row : 1,
+                cell : 0
+            }
+        }, {
+            id : "recordplayBtn",
+            content : "",
+            tag : "div",
+            position : {
+                row : 1,
+                cell : 1
+            }
+        }, {
+            id : "recordpauseBtn",
+            content : "",
+            tag : "div",
+            position : {
+                row : 2,
+                cell : 0
+            }
+        }, {
+            id : "recordstopBtn",
+            content : "",
+            tag : "div",
+            position : {
+                row : 2,
+                cell : 1
+            }
+        }
+    ];
+
+    //Title audio results
+    var div = document.createElement('h2');
+    div.appendChild(document.createTextNode('Audio'));
+    div.setAttribute("align", "center");
+    contentEl.appendChild(div);
+    //Generate and add results table
+    contentEl.appendChild(generateTable('info', 3, 3, elementsResultsAudio));
+
+    //Title audio actions
+    div = document.createElement('h2');
+    div.appendChild(document.createTextNode('Actions'));
+    div.setAttribute("align", "center");
+    contentEl.appendChild(div);
+    //Generate and add buttons table
+    contentEl.appendChild(generateTable('audioActions', 3, 3, elementsAudio));
+    createActionButton('Play', function () {
+        playAudio();
+    }, 'playBtn');
+    createActionButton('Pause', function () {
+        pauseAudio();
+    }, 'pauseBtn');
+    createActionButton('HalfSpeed', function() {
+    
+        if(halfSpeedBtn.firstChild.firstChild.innerText == 'HalfSpeed') {
+            halfSpeedBtn.firstChild.firstChild.innerText = 'FullSpeed';
+            media1.setRate(0.5);
+        }
+        else if(halfSpeedBtn.firstChild.firstChild.innerText == 'FullSpeed') {
+            halfSpeedBtn.firstChild.firstChild.innerText = 'DoubleSpeed';
+            media1.setRate(1.0);
+        }
+        else {
+            halfSpeedBtn.firstChild.firstChild.innerText = 'HalfSpeed';
+            media1.setRate(2.0);
+        }
+    }, 'halfSpeedBtn');
+    createActionButton('Stop', function () {
+        stopAudio();
+    }, 'stopBtn');
+    createActionButton('Release', function () {
+        releaseAudio();
+    }, 'releaseBtn');
+    createActionButton('Seek By', function () {
+        seekAudio('by');
+    }, 'seekByBtn');
+    createActionButton('Seek To', function () {
+        seekAudio('to');
+    }, 'seekToBtn');
+    //get Special path to record if iOS || Blackberry
+    if (cordova.platformId === 'ios')
+        getRecordSrc();
+    else if (cordova.platformId === 'blackberry')
+        getRecordSrcBB();
+    else if (cordova.platformId === 'windows' || cordova.platformId === 'windows8')
+        getRecordSrcWin();
+
+    //testing process and details
+    function addItemToList(_list, _text)
+    {
+        var item = document.createElement('li');
+        item.appendChild(document.createTextNode(_text));
+        _list.appendChild(item);
+    }
+
+    div = document.createElement('h4');
+    div.appendChild(document.createTextNode('Recommended Test Procedure'));
+    contentEl.appendChild(div);
+
+    var list = document.createElement('ol');
+    addItemToList(list, 'Press play - Will start playing audio. Status: Running, Duration: 61.333 sec, Position: Current position of audio track');
+    addItemToList(list, 'Press pause - Will pause the audio. Status: Paused, Duration: 61.333 sec, Position: Position where track was paused');
+    addItemToList(list, 'Press play - Will begin playing where track left off from the pause');
+    addItemToList(list, 'Press stop - Will stop the audio. Status: Stopped, Duration: 61.333 sec, Position: 0 sec');
+    addItemToList(list, 'Press play - Will begin playing the audio track from the beginning');
+    addItemToList(list, 'Press release - Will stop the audio. Status: Stopped, Duration: 61.333 sec, Position: 0 sec');
+    addItemToList(list, 'Press play - Will begin playing the audio track from the beginning');
+    addItemToList(list, 'Type 10 in the text box beside Seek To button');
+    addItemToList(list, 'Press seek by - Will jump 10 seconds ahead in the audio track. Position: should jump by 10 sec');
+    addItemToList(list, 'Press stop if track is not already stopped');
+    addItemToList(list, 'Type 30 in the text box beside Seek To button');
+    addItemToList(list, 'Press play then seek to - Should jump to Position 30 sec');
+    addItemToList(list, 'Press stop');
+    addItemToList(list, 'Type 5 in the text box beside Seek To button');
+    addItemToList(list, 'Press play, let play past 10 seconds then press seek to - should jump back to position 5 sec');
+
+    div = document.createElement('div');
+    div.setAttribute("style", "background:#B0C4DE;border:1px solid #FFA07A;margin:15px 6px 0px;min-width:295px;max-width:97%;padding:4px 0px 2px 10px;min-height:160px;max-height:200px;overflow:auto");
+    div.appendChild(list);
+    contentEl.appendChild(div);
+
+    //Title Record Audio
+    div = document.createElement('h2');
+    div.appendChild(document.createTextNode('Record Audio'));
+    div.setAttribute("align", "center");
+    contentEl.appendChild(div);
+    //Generate and add Record buttons table
+    contentEl.appendChild(generateTable('recordContent', 3, 3, elementsRecord));
+    createActionButton('Record Audio \n 10 sec', function () {
+        recordAudio();
+    }, 'recordbtn');
+    createActionButton('Play', function () {
+        playRecording();
+    }, 'recordplayBtn');
+    createActionButton('Pause', function () {
+        pauseAudio();
+    }, 'recordpauseBtn');
+    createActionButton('Stop', function () {
+        stopAudio();
+    }, 'recordstopBtn');
+
+    //testing process and details
+    div = document.createElement('h4');
+    div.appendChild(document.createTextNode('Recommended Test Procedure'));
+    contentEl.appendChild(div);
+
+    list = document.createElement('ol');
+    addItemToList(list, 'Press Record Audio 10 sec - Will start recording audio for 10 seconds. Status: Running, Position: number of seconds recorded (will stop at 10)');
+    addItemToList(list, 'Status will change to Stopped when finished recording');
+    addItemToList(list, 'Press play - Will begin playing the recording. Status: Running, Duration: # of seconds of recording, Position: Current position of recording');
+    addItemToList(list, 'Press stop - Will stop playing the recording. Status: Stopped, Duration: # of seconds of recording, Position: 0 sec');
+    addItemToList(list, 'Press play - Will begin playing the recording from the beginning');
+    addItemToList(list, 'Press pause - Will pause the playback of the recording. Status: Paused, Duration: # of seconds of recording, Position: Position where recording was paused');
+    addItemToList(list, 'Press play - Will begin playing the recording from where it was paused');
+
+    div = document.createElement('div');
+    div.setAttribute("style", "background:#B0C4DE;border:1px solid #FFA07A;margin:15px 6px 0px;min-width:295px;max-width:97%;padding:4px 0px 2px 10px;min-height:160px;max-height:200px;overflow:auto");
+    div.appendChild(list);
+    contentEl.appendChild(div);
+};
diff --git a/test/unittest/tests/network.tests.js b/test/unittest/tests/network.tests.js
new file mode 100644 (file)
index 0000000..23be97a
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+exports.defineAutoTests = function () {
+    describe('Network (navigator.connection)', function () {
+        it("network.spec.1 should exist", function () {
+            expect(navigator.network && navigator.network.connection).toBeDefined();
+            expect(navigator.connection).toBeDefined();
+        });
+
+        it("network.spec.2 should be set to a valid value", function () {
+            var validValues = {
+                'unknown': 1,
+                'ethernet': 1,
+                'wifi': 1,
+                '2g': 1,
+                'cellular': 1,
+                '3g': 1,
+                '4g': 1,
+                'none': 1
+            };
+            expect(validValues[navigator.connection.type]).toBe(1);
+        });
+
+        it("network.spec.3 should have the same value in deprecated and non-deprecated apis", function () {
+            expect(navigator.network.connection.type).toBe(navigator.connection.type);
+        });
+
+        it("network.spec.4 should define constants for connection status", function () {
+            expect(Connection.UNKNOWN).toBe("unknown");
+            expect(Connection.ETHERNET).toBe("ethernet");
+            expect(Connection.WIFI).toBe("wifi");
+            expect(Connection.CELL_2G).toBe("2g");
+            expect(Connection.CELL_3G).toBe("3g");
+            expect(Connection.CELL_4G).toBe("4g");
+            expect(Connection.NONE).toBe("none");
+            expect(Connection.CELL).toBe("cellular");
+        });
+    });
+};
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    function eventOutput(s) {
+        var el = document.getElementById("results");
+        el.innerHTML = el.innerHTML + s + "<br>";
+    }
+
+    function printNetwork() {
+        eventOutput("navigator.connection.type=" + navigator.connection.type);
+        eventOutput("navigator.network.connection.type=" + navigator.network.connection.type);
+    }
+
+    function onEvent(e) {
+        eventOutput('Event of type: ' + e.type);
+        printNetwork();
+    }
+
+    /******************************************************************************/
+
+    var html = '<div id="info">' +
+        '<b>Results:</b><br>' +
+        '<span id="results"></span>' +
+        '</div><div id="connection"></div>' +
+        'Expected result: Status box will update with type of connection using two different methods. Both values must match.' +
+        '  The result will be unknown, ethernet, wifi, 2g, 3g, 4g, none, or cellular. Make sure it matches what the device is connected to.' +
+        '</p> <div id="actions"></div>';
+
+    document.addEventListener("online", onEvent, false);
+    document.addEventListener("offline", onEvent, false);
+    contentEl.innerHTML = html;
+
+    createActionButton('Show Network Connection', function () {
+        printNetwork();
+    }, 'connection');
+
+    createActionButton('Clear Log', function () {
+        document.getElementById('results').innerHTML = '';
+    }, 'actions');
+};
diff --git a/test/unittest/tests/notification.tests.js b/test/unittest/tests/notification.tests.js
new file mode 100644 (file)
index 0000000..16e9cd9
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+exports.defineAutoTests = function () {
+    describe('Notification (navigator.notification)', function () {
+        it("should exist", function () {
+            expect(navigator.notification).toBeDefined();
+        });
+
+        it("should contain a beep function", function () {
+            expect(typeof navigator.notification.beep).toBeDefined();
+            expect(typeof navigator.notification.beep).toBe("function");
+        });
+
+        it("should contain an alert function", function () {
+            expect(typeof navigator.notification.alert).toBeDefined();
+            expect(typeof navigator.notification.alert).toBe("function");
+        });
+
+        it("should contain a confirm function", function () {
+            expect(typeof navigator.notification.confirm).toBeDefined();
+            expect(typeof navigator.notification.confirm).toBe("function");
+        });
+
+        it("should contain a prompt function", function () {
+            expect(typeof navigator.notification.prompt).toBeDefined();
+            expect(typeof navigator.notification.prompt).toBe("function");
+        });
+    });
+};
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    var logMessage = function (message) {
+        var log = document.getElementById('info');
+        var logLine = document.createElement('div');
+        logLine.innerHTML = message;
+        log.appendChild(logLine);
+    }
+
+    var clearLog = function () {
+        var log = document.getElementById('info');
+        log.innerHTML = '';
+    }
+
+    var beep = function () {
+        console.log("beep()");
+        navigator.notification.beep(3);
+    };
+
+    var alertDialog = function (message, title, button) {
+        console.log("alertDialog()");
+        navigator.notification.alert(message,
+            function () {
+                console.log("Alert dismissed.");
+            },
+            title, button);
+        console.log("After alert");
+    };
+
+    var confirmDialogA = function (message, title, buttons) {
+        clearLog();
+        navigator.notification.confirm(message,
+            function (r) {
+                if (r === 0) {
+                    logMessage("Dismissed dialog without making a selection.");
+                    console.log("Dismissed dialog without making a selection.");
+                } else {
+                    console.log("You selected " + r);
+                    logMessage("You selected " + (buttons.split(","))[r - 1]);
+                }
+            },
+            title,
+            buttons);
+    };
+
+    var confirmDialogB = function (message, title, buttons) {
+        clearLog();
+        navigator.notification.confirm(message,
+            function (r) {
+                if (r === 0) {
+                    logMessage("Dismissed dialog without making a selection.");
+                    console.log("Dismissed dialog without making a selection.");
+                } else {
+                    console.log("You selected " + r);
+                    logMessage("You selected " + buttons[r - 1]);
+                }
+            },
+            title,
+            buttons);
+    };
+
+    var promptDialog = function (message, title, buttons) {
+        clearLog();
+        navigator.notification.prompt(message,
+            function (r) {
+                if (r && r.buttonIndex === 0) {
+                    var msg = "Dismissed dialog";
+                    if (r.input1) {
+                        msg += " with input: " + r.input1
+                    }
+                    logMessage(msg);
+                    console.log(msg);
+                } else {
+                    console.log("You selected " + r.buttonIndex + " and entered: " + r.input1);
+                    logMessage("You selected " + buttons[r.buttonIndex - 1] + " and entered: " + r.input1);
+                }
+            },
+            title,
+            buttons);
+    };
+
+    /******************************************************************************/
+
+    var dialogs_tests = '<div id="beep"></div>' +
+        'Expected result: Device will beep (unless device is on silent). Nothing will get updated in status box.' +
+        '<h2>Dialog Tests</h2>' +
+        '<h3>Dialog boxes will pop up for each of the following tests</h3>' +
+        '<p/> <div id="alert"></div>' +
+        'Expected result: Dialog will say "You pressed alert". Press continue to close dialog. Nothing will get updated in status box.' +
+        '<p/> <div id="confirm_deprecated"></div>' +
+        'Expected result: Dialog will say "You pressed confirm". Press Yes, No, or Maybe to close dialog. Status box will tell you what option you selected.' +
+        '<p/> <div id="confirm"></div>' +
+        'Expected result: Dialog will say "You pressed confirm". Press Yes, No, or Maybe, Not Sure to close dialog. Status box will tell you what option you selected, and should use 1-based indexing.' +
+        '<p/> <div id="prompt"></div>' +
+        'Expected result: Dialog will say "You pressed prompt". Enter any message and press Yes, No, or Maybe, Not Sure to close dialog. Status box will tell you what option you selected and message you entered, and should use 1-based indexing.' +
+        '<p/> <div id="built_in_alert"></div>' +
+        'Expected result: Dialog will have title "index.html" and say "You pressed alert" Press OK to close dialog. Nothing will get updated in status box.' +
+        '<p/> <div id="built_in_confirm"></div>' +
+        'Expected result: Dialog will have title "index.html" and say "You selected confirm". Press Cancel or OK to close dialog. Nothing will get updated in status box.' +
+        '<p/> <div id="built_in_prompt"></div>' +
+        'Expected result: Dialog will have title "index.html" and say "This is a prompt". "Default value" will be in text box. Press Cancel or OK to close dialog. Nothing will get updated in status box.';
+
+    contentEl.innerHTML = '<div id="info"></div>' +
+        dialogs_tests;
+
+    createActionButton('Beep', function () {
+        beep();
+    }, 'beep');
+
+    createActionButton('Alert Dialog', function () {
+        alertDialog('You pressed alert.', 'Alert Dialog', 'Continue');
+    }, 'alert');
+
+    // WP8.1 detection is necessary since it doesn't support confirm dialogs with more than 2 buttons
+    var isRunningOnWP81 = cordova.platformId == "windows" && navigator.userAgent.indexOf('Windows Phone') > -1;
+
+    createActionButton('Confirm Dialog - Deprecated', function () {
+        var buttons = isRunningOnWP81 ? 'Yes,No' : 'Yes,No,Maybe';
+        confirmDialogA('You pressed confirm.', 'Confirm Dialog', buttons);
+    }, 'confirm_deprecated');
+
+    createActionButton('Confirm Dialog', function () {
+        var buttons = isRunningOnWP81 ? ['Yes', 'Actually, No'] : ['Yes', 'No', 'Maybe, Not Sure'];
+        confirmDialogB('You pressed confirm.', 'Confirm Dialog', buttons);
+    }, 'confirm');
+
+    createActionButton('Prompt Dialog', function () {
+        promptDialog('You pressed prompt.', 'Prompt Dialog', ['Yes', 'No', 'Maybe, Not Sure']);
+    }, 'prompt');
+
+    createActionButton('Built-in Alert Dialog', function () {
+        typeof alert === 'function' && alert('You pressed alert');
+    }, 'built_in_alert');
+
+    createActionButton('Built-in Confirm Dialog', function () {
+        typeof confirm === 'function' && confirm('You selected confirm');
+    }, 'built_in_confirm');
+
+    createActionButton('Built-in Prompt Dialog', function () {
+        typeof prompt === 'function' && prompt('This is a prompt', 'Default value');
+    }, 'built_in_prompt');
+};
diff --git a/test/unittest/tests/platform.tests.js b/test/unittest/tests/platform.tests.js
new file mode 100644 (file)
index 0000000..6cca2b7
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+exports.defineAutoTests = function () {
+describe('Platform (cordova)', function () {
+    it("platform.spec.1 should exist", function() {
+        expect(cordova).toBeDefined();
+    });
+
+    it("platform.spec.2 exec method should exist", function() {
+        expect(cordova.exec).toBeDefined();
+        expect(typeof cordova.exec).toBe('function');
+    });
+});
+};
+
+exports.defineManualTests = function (contentEl, createActionButton) {};
\ No newline at end of file
diff --git a/test/unittest/tests/splashscreen.tests.js b/test/unittest/tests/splashscreen.tests.js
new file mode 100644 (file)
index 0000000..ff7df78
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+exports.defineAutoTests = function () {
+    describe('Splashscreen (cordova)', function () {
+        it("splashscreen.spec.1 should exist", function () {
+            expect(navigator.splashscreen).toBeDefined();
+        });
+
+        it("splashscreen.spec.2 exec method should exist", function () {
+            expect(navigator.splashscreen.show).toBeDefined();
+            expect(typeof navigator.splashscreen.show).toBe('function');
+        });
+
+        it("splashscreen.spec.3 exec method should exist", function () {
+            expect(navigator.splashscreen.hide).toBeDefined();
+            expect(typeof navigator.splashscreen.hide).toBe('function');
+        });
+    });
+};
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    function showFor(duration) {
+        navigator.splashscreen.show();
+        window.setTimeout(function () {
+            navigator.splashscreen.hide();
+        }, 1000 * duration);
+    }
+
+    contentEl.innerHTML = '<h1>Splashscreen Tests</h1>' +
+        '<h3>Note for WP: AutoHideSplashScreen must be set to false in config.xml</h3>' +
+        '<div id="show1"></div>' +
+        'Expected result: Will show the Cordova splashscreen for 1 second' +
+        '</p> <div id="show5"></div>' +
+        'Expected result: Will show the Cordova splashscreen for 5 seconds';
+
+    createActionButton('Show for 1 second', function () {
+        showFor(1);
+    }, 'show1');
+
+    createActionButton('Show for 5 seconds', function () {
+        showFor(5);
+    }, 'show5');
+};
diff --git a/test/unittest/tests/statusbar.tests.js b/test/unittest/tests/statusbar.tests.js
new file mode 100644 (file)
index 0000000..b66c5cc
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+exports.defineAutoTests = function () {
+    describe("StatusBar", function () {
+        it("statusbar.spec.1 should exist", function() {
+            expect(window.StatusBar).toBeDefined();
+        });
+
+        it("statusbar.spec.2 should have show|hide methods", function() {
+            expect(window.StatusBar.show).toBeDefined();
+            expect(typeof window.StatusBar.show).toBe("function");
+
+            expect(window.StatusBar.hide).toBeDefined();
+            expect(typeof window.StatusBar.hide).toBe("function");
+        });
+
+        it("statusbar.spec.3 should have set backgroundColor methods", function() {
+            expect(window.StatusBar.backgroundColorByName).toBeDefined();
+            expect(typeof window.StatusBar.backgroundColorByName).toBe("function");
+
+            expect(window.StatusBar.backgroundColorByHexString).toBeDefined();
+            expect(typeof window.StatusBar.backgroundColorByHexString).toBe("function");
+        });
+
+        it("statusbar.spec.4 should have set style methods", function() {
+            expect(window.StatusBar.styleBlackTranslucent).toBeDefined();
+            expect(typeof window.StatusBar.styleBlackTranslucent).toBe("function");
+
+            expect(window.StatusBar.styleDefault).toBeDefined();
+            expect(typeof window.StatusBar.styleDefault).toBe("function");
+
+            expect(window.StatusBar.styleLightContent).toBeDefined();
+            expect(typeof window.StatusBar.styleLightContent).toBe("function");
+
+            expect(window.StatusBar.styleBlackOpaque).toBeDefined();
+            expect(typeof window.StatusBar.styleBlackOpaque).toBe("function");
+
+            expect(window.StatusBar.overlaysWebView).toBeDefined();
+            expect(typeof window.StatusBar.overlaysWebView).toBe("function");
+        });
+    });
+};
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    function log(msg) {
+        var el = document.getElementById("info");
+        var logLine = document.createElement('div');
+        logLine.innerHTML = msg;
+        el.appendChild(logLine);
+    }
+
+    function doShow() {
+        StatusBar.show();
+        log('StatusBar.isVisible=' + StatusBar.isVisible);
+    }
+
+    function doHide() {
+        StatusBar.hide();
+        log('StatusBar.isVisible=' + StatusBar.isVisible);
+    }
+
+    function doColor1() {
+        log('set color=red');
+        StatusBar.backgroundColorByName('red');
+    }
+
+    function doColor2() {
+        log('set style=translucent black');
+        StatusBar.styleBlackTranslucent();
+    }
+
+    function doColor3() {
+        log('set style=default');
+        StatusBar.styleDefault();
+    }
+
+    var showOverlay = true;
+    function doOverlay() {
+        showOverlay = !showOverlay;
+        StatusBar.overlaysWebView(showOverlay);
+        log('Set overlay=' + showOverlay);
+    }
+
+    /******************************************************************************/
+
+    contentEl.innerHTML = '<div id="info"></div>' +
+        'Also: tapping bar on iOS should emit a log.' +
+        '<div id="action-show"></div>' +
+        'Expected result: Status bar will be visible' +
+        '</p> <div id="action-hide"></div>' +
+        'Expected result: Status bar will be hidden' +
+        '</p> <div id="action-color2"></div>' +
+        'Expected result: Status bar text will be a light (white) color' +
+        '</p> <div id="action-color3"></div>' +
+        'Expected result: Status bar text will be a dark (black) color' +
+        '</p> <div id="action-overlays"></div>' +
+        'Expected result:<br>Overlay true = status bar will lay on top of web view content<br>Overlay false = status bar will be separate from web view and will not cover content' +
+        '</p> <div id="action-color1"></div>' +
+        'Expected result: If overlay false, background color for status bar will be red';
+
+    log('StatusBar.isVisible=' + StatusBar.isVisible);
+    window.addEventListener('statusTap', function () {
+        log('tap!');
+    }, false);
+
+    createActionButton("Show", function () {
+        doShow();
+    }, 'action-show');
+
+    createActionButton("Hide", function () {
+        doHide();
+    }, 'action-hide');
+
+    createActionButton("Style=red (background)", function () {
+        doColor1();
+    }, 'action-color1');
+
+    createActionButton("Style=translucent black", function () {
+        doColor2();
+    }, 'action-color2');
+
+    createActionButton("Style=default", function () {
+        doColor3();
+    }, 'action-color3');
+
+    createActionButton("Toggle Overlays", function () {
+        doOverlay();
+    }, 'action-overlays');
+};
diff --git a/test/unittest/tests/storage.tests.js b/test/unittest/tests/storage.tests.js
new file mode 100644 (file)
index 0000000..5dc620e
--- /dev/null
@@ -0,0 +1,199 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+*/
+
+exports.defineAutoTests = function () {
+describe("Session Storage", function () {
+    it("storage.spec.1 should exist", function () {
+        expect(window.sessionStorage).toBeDefined();
+        expect(typeof window.sessionStorage.length).not.toBe('undefined');
+        expect(typeof(window.sessionStorage.key)).toBe('function');
+        expect(typeof(window.sessionStorage.getItem)).toBe('function');
+        expect(typeof(window.sessionStorage.setItem)).toBe('function');
+        expect(typeof(window.sessionStorage.removeItem)).toBe('function');
+        expect(typeof(window.sessionStorage.clear)).toBe('function');
+    });
+
+    it("storage.spec.2 check length", function () {
+        expect(window.sessionStorage.length).toBe(0);
+        window.sessionStorage.setItem("key","value");
+        expect(window.sessionStorage.length).toBe(1);
+        window.sessionStorage.removeItem("key");   
+        expect(window.sessionStorage.length).toBe(0);
+    });
+
+    it("storage.spec.3 check key", function () {
+        expect(window.sessionStorage.key(0)).toBe(null);
+        window.sessionStorage.setItem("test","value");
+        expect(window.sessionStorage.key(0)).toBe("test");
+        window.sessionStorage.removeItem("test");   
+        expect(window.sessionStorage.key(0)).toBe(null);
+    });
+
+    it("storage.spec.4 check getItem", function() {
+        expect(window.sessionStorage.getItem("item")).toBe(null);
+        window.sessionStorage.setItem("item","value");
+        expect(window.sessionStorage.getItem("item")).toBe("value");
+        window.sessionStorage.removeItem("item");   
+        expect(window.sessionStorage.getItem("item")).toBe(null);
+    });
+
+    it("storage.spec.5 check setItem", function() {
+        expect(window.sessionStorage.getItem("item")).toBe(null);
+        window.sessionStorage.setItem("item","value");
+        expect(window.sessionStorage.getItem("item")).toBe("value");
+        window.sessionStorage.setItem("item","newval");
+        expect(window.sessionStorage.getItem("item")).toBe("newval");
+        window.sessionStorage.removeItem("item");   
+        expect(window.sessionStorage.getItem("item")).toBe(null);
+    });
+
+    it("storage.spec.6 can remove an item", function () {
+        expect(window.sessionStorage.getItem("item")).toBe(null);
+        window.sessionStorage.setItem("item","value");
+        expect(window.sessionStorage.getItem("item")).toBe("value");
+        window.sessionStorage.removeItem("item");   
+        expect(window.sessionStorage.getItem("item")).toBe(null);
+    });
+
+    it("storage.spec.7 check clear", function() {
+        window.sessionStorage.setItem("item1","value");
+        window.sessionStorage.setItem("item2","value");
+        window.sessionStorage.setItem("item3","value");
+        expect(window.sessionStorage.length).toBe(3);
+        window.sessionStorage.clear();
+        expect(window.sessionStorage.length).toBe(0);
+    });
+
+    it("storage.spec.8 check dot notation", function() {
+        expect(window.sessionStorage.item).not.toBeDefined();
+        window.sessionStorage.item = "value";
+        expect(window.sessionStorage.item).toBe("value");
+        window.sessionStorage.removeItem("item");   
+        expect(window.sessionStorage.item).not.toBeDefined();
+    });
+
+    describe("Local Storage", function () {
+        it("storage.spec.9 should exist", function() {
+            expect(window.localStorage).toBeDefined();
+            expect(window.localStorage.length).toBeDefined();
+            expect(typeof window.localStorage.key).toBe("function");
+            expect(typeof window.localStorage.getItem).toBe("function");
+            expect(typeof window.localStorage.setItem).toBe("function");
+            expect(typeof window.localStorage.removeItem).toBe("function");
+            expect(typeof window.localStorage.clear).toBe("function");
+        });  
+
+        it("storage.spec.10 check length", function() {
+            expect(window.localStorage.length).toBe(0);
+            window.localStorage.setItem("key","value");
+            expect(window.localStorage.length).toBe(1);
+            window.localStorage.removeItem("key");   
+            expect(window.localStorage.length).toBe(0);
+        });
+
+        it("storage.spec.11 check key", function() {
+            expect(window.localStorage.key(0)).toBe(null);
+            window.localStorage.setItem("test","value");
+            expect(window.localStorage.key(0)).toBe("test");
+            window.localStorage.removeItem("test");   
+            expect(window.localStorage.key(0)).toBe(null);
+        });
+
+        it("storage.spec.4 check getItem", function() {
+            expect(window.localStorage.getItem("item")).toBe(null);
+            window.localStorage.setItem("item","value");
+            expect(window.localStorage.getItem("item")).toBe("value");
+            window.localStorage.removeItem("item");   
+            expect(window.localStorage.getItem("item")).toBe(null);
+        });
+
+        it("storage.spec.5 check setItem", function() {
+            expect(window.localStorage.getItem("item")).toBe(null);
+            window.localStorage.setItem("item","value");
+            expect(window.localStorage.getItem("item")).toBe("value");
+            window.localStorage.setItem("item","newval");
+            expect(window.localStorage.getItem("item")).toBe("newval");
+            window.localStorage.removeItem("item");   
+            expect(window.localStorage.getItem("item")).toBe(null);
+        });
+
+        it("storage.spec.14 check removeItem", function() {
+            expect(window.localStorage.getItem("item")).toBe(null);
+            window.localStorage.setItem("item","value");
+            expect(window.localStorage.getItem("item")).toBe("value");
+            window.localStorage.removeItem("item");   
+            expect(window.localStorage.getItem("item")).toBe(null);
+        });
+
+        it("storage.spec.7 check clear", function() {
+            expect(window.localStorage.getItem("item1")).toBe(null);
+            expect(window.localStorage.getItem("item2")).toBe(null);
+            expect(window.localStorage.getItem("item3")).toBe(null);
+            window.localStorage.setItem("item1","value");
+            window.localStorage.setItem("item2","value");
+            window.localStorage.setItem("item3","value");
+            expect(window.localStorage.getItem("item1")).toBe("value");
+            expect(window.localStorage.getItem("item2")).toBe("value");
+            expect(window.localStorage.getItem("item3")).toBe("value");
+            expect(window.localStorage.length).toBe(3);
+            window.localStorage.clear();
+            expect(window.localStorage.length).toBe(0);
+            expect(window.localStorage.getItem("item1")).toBe(null);
+            expect(window.localStorage.getItem("item2")).toBe(null);
+            expect(window.localStorage.getItem("item3")).toBe(null);
+        });
+
+        it("storage.spec.8 check dot notation", function() {
+            expect(window.localStorage.item).not.toBeDefined();
+            window.localStorage.item = "value";
+            expect(window.localStorage.item).toBe("value");
+            window.localStorage.removeItem("item");   
+            expect(window.localStorage.item).not.toBeDefined();
+        });
+    });
+
+    describe("HTML 5 Storage", function () {
+        it("storage.spec.9 should exist", function() {
+            expect(window.openDatabase);
+        });
+
+        it("storage.spec.18 Should be able to create and drop tables", function() {
+            var win = jasmine.createSpy('win');
+            var fail1 = createDoNotCallSpy('fail1');
+            var fail2 = createDoNotCallSpy('fail2');
+            var db = openDatabase("Database", "1.0", "HTML5 Database API example", 5*1024*1024);
+            db.transaction(function(t) {
+                t.executeSql('CREATE TABLE IF NOT EXISTS foo(id int, name varchar(255));');
+                t.executeSql('CREATE TABLE IF NOT EXISTS foo2(id int, name varchar(255));');
+            }, fail1, step2);
+            function step2() {
+              db.transaction(function(t) {
+                  t.executeSql('DROP TABLE foo;');
+                  t.executeSql('DROP TABLE foo2');
+              }, fail2, win);
+            }
+            waitsForAny(win, fail1, fail2);
+        });
+    });
+});
+};
+
+exports.defineManualTests = function (contentEl, createActionButton) {};
\ No newline at end of file
diff --git a/test/unittest/tests/vibration.tests.js b/test/unittest/tests/vibration.tests.js
new file mode 100644 (file)
index 0000000..79aff30
--- /dev/null
@@ -0,0 +1,313 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ *
+ */
+
+exports.defineAutoTests = function () {
+
+    describe('Vibration (navigator.notification.vibrate)', function () {
+        it("navigator.notification should exist with vibrate function", function () {
+            expect(navigator.notification).toBeDefined();
+            expect(typeof navigator.notification.vibrate).toBeDefined();
+            expect(typeof navigator.notification.vibrate).toBe("function");
+
+            if (cordova.platformId == "browser")
+                expect(navigator.notification.vibrate(0)).toBe(false);
+        });
+    });
+};
+
+exports.defineManualTests = function (contentEl, createActionButton) {
+    var logMessage = function (message, color) {
+        var log = document.getElementById('info');
+        var logLine = document.createElement('div');
+        if (color) {
+            logLine.style.color = color;
+        }
+        logLine.innerHTML = message;
+        log.appendChild(logLine);
+    }
+
+    var clearLog = function () {
+        var log = document.getElementById('info');
+        log.innerHTML = '';
+    }
+
+    //-------------------------------------------------------------------------
+    // Vibrations
+    //-------------------------------------------------------------------------
+
+    //old vibrate call
+    var vibrateOld = function(){
+        clearLog();
+        navigator.notification.vibrate(2500);
+        logMessage("navigator.notification.vibrate(2500)", "green");
+    };
+
+    //old vibrate with pattern call
+    var vibrateWithPatternOld = function(){
+        clearLog();
+        navigator.notification.vibrateWithPattern([1000, 3000, 2000, 5000]);
+        logMessage("navigator.notification.vibrateWithPattern([1000, 3000, 2000, 5000])", "green");
+    };
+
+    //old cancel vibrate call
+    var cancelOld = function(){
+        clearLog();
+        navigator.notification.cancelVibration();
+        logMessage("navigator.notification.cancelVibration()", "green");
+    };
+
+    //new standard vibrate call that aligns to w3c spec with param long
+    var vibrateWithInt = function() {
+        clearLog();
+        navigator.vibrate(3000);
+        logMessage("navigator.vibrate(3000)", "green");
+    };
+
+    //new standard vibrate call that aligns to w3c spec with param array
+    var vibrateWithArray = function() {
+        clearLog();
+        navigator.vibrate([3000]);
+        logMessage("navigator.vibrate([3000])", "green");
+    };
+
+    //vibrate with a pattern using w3c spec
+    var vibrateWithPattern = function() {
+        clearLog();
+        navigator.vibrate([1000, 2000, 3000, 2000, 5000]);
+        logMessage("navigator.vibrate([1000, 2000, 3000, 2000, 5000])", "green");
+    };
+
+    //cancel existing vibration using w3c spec navigator.vibrate(0)
+    var cancelWithZero = function() {
+        clearLog();
+        navigator.vibrate(0);
+        logMessage("navigator.vibrate(0)", "green");
+    };
+
+    //cancel existing vibration using w3c spec navigator.vibrate([])
+    var cancelWithEmpty = function() {
+        clearLog();
+        navigator.vibrate([]);
+        logMessage("navigator.vibrate([])", "green");
+    };
+
+    //reference to the timeout variable
+    var timeout;
+
+    //special long vibrate used to test cancel
+    var longVibrate = function() {
+        clearLog();
+        navigator.vibrate(60000);
+        vibrateOn = true;
+        logMessage("navigator.vibrate(60000)", "green");
+        timeout = setTimeout(resetVibrateOn, 60000); //if user doesn't cancel vibrate, reset vibrateOn var after 60 seconds
+    };
+
+    //special long vibrate with pattern used to test cancel
+    var longVibrateWithPattern = function() {
+        clearLog();
+        navigator.vibrate([1000, 2000, 3000, 2000, 5000, 2000, 30000]);
+        vibrateOn = true;
+        logMessage("navigator.vibrate([1000, 2000, 3000, 2000, 5000, 2000, 30000])", "green");
+        timeout = setTimeout(resetVibrateOn, 45000); //if user doesn't cancel vibrate, reset vibrateOn var after 45 seconds
+    };
+
+    //initiate two vibrations to test cancel
+    var multipleVibrations = function() {
+        clearLog();
+        navigator.vibrate(20000);
+        navigator.vibrate(45000);
+        vibrateOn = true;
+        logMessage("navigator.vibrate(15000)\nnavigator.vibrate(45000)", "green");
+        timeout = setTimeout(resetVibrateOn, 45000); //if user doesn't cancel vibrate, reset vibrateOn var after 45 seconds
+    }
+
+    function resetVibrateOn() {
+        vibrateOn = false;
+    }
+
+    //check whether there is an ongoing vibration
+    var vibrateOn = false;
+
+
+
+
+    var vibrate_tests = '<h1>Vibrate Tests</h1>' +
+        '<h3>Starred tests only work for Android and Windows. </h3>' +
+        '<h3>iOS ignores the time given for a vibrate </h3>' +
+        '<div id="vibrate_old"></div>' +
+        'Expected result: Vibrate once for 2.5 seconds.' +
+        '<p/> <div id="vibrateWithPattern_old"></div>' +
+        'Expected result: Pause for 1s, vibrate for 3s, pause for 2s, vibrate for 5s.' +
+        '<p/> <div id="cancelVibrate_old"></div>' +
+        'Expected result: Press once to initiate vibrate for 60 seconds. Press again to cancel vibrate immediately.' +
+        '<p/> <div id="cancelVibrateWithPattern_old"></div>' +
+        'Expected result: Press once to initiate vibrate with pattern for 45s. Press again to cancel vibrate immediately.' +
+        '<p/> <div id="vibrate_int"></div>' +
+        'Expected result: Vibrate once for 3 seconds.' +
+        '<p/> <div id="vibrate_array"></div>' +
+        'Expected result: Vibrate once for 3 seconds.' +
+        '<p/> <div id="vibrate_with_pattern"></div>' +
+        'Expected result: Vibrate for 1s, pause for 2s, vibrate for 3s, pause for 2s, vibrate for 5s.' +
+        '<p/> <div id="cancel_zero"></div>' +
+        'Expected result: Press once to initiate vibrate for 60 seconds. Press again to cancel vibrate immediately.' +
+        '<p/><div id="cancel_array"></div>' +
+        'Expected result: Press once to initiate vibrate for 60 seconds. Press again to cancel vibrate immediately.' +
+        '<p/> <div id="cancelWithPattern_zero"></div>' +
+        'Expected result: Press once to initiate vibrate with pattern for 45s. Press again to cancel vibrate immediately.' +
+        '<p/> <div id="cancelWithPattern_array"></div>' +
+        'Expected result: Press once to initiate vibrate with pattern for 45s. Press again to cancel vibrate immediately.' +
+        '<p/> <div id="cancelMultipleVibrations"></div>' +
+        'Expected result: Press once to initiate two vibrations simultaneously (one for 20s the other for 45s so total of 45s). Press again to cancel both vibrations immediately.';
+
+
+    contentEl.innerHTML = '<div id="info"></div>' + vibrate_tests;
+
+    //standard vibrate with old call
+    createActionButton('Vibrate (Old)', function () {
+        vibrateOld();
+    }, 'vibrate_old');
+
+    //vibrate with pattern with old call
+    createActionButton('* Vibrate with a pattern (Old)', function () {
+        vibrateWithPatternOld();
+    }, 'vibrateWithPattern_old');
+
+    //cancel vibrate with old call
+    createActionButton('* Cancel vibration (Old)', function() {
+
+        if (!vibrateOn)
+        {
+            longVibrate();
+        }
+        else
+        {
+            cancelOld();
+            resetVibrateOn();
+            clearTimeout(timeout); //clear the timeout since user has canceled the vibrate
+        }
+    }, 'cancelVibrate_old');
+
+    //cancel vibrate with pattern with old call
+    createActionButton('* Cancel vibration with pattern (Old)', function() {
+
+        if (!vibrateOn)
+        {
+            longVibrateWithPattern();
+        }
+        else
+        {
+            cancelOld();
+            resetVibrateOn();
+            clearTimeout(timeout); //clear the timeout since user has canceled the vibrate
+        }
+    }, 'cancelVibrateWithPattern_old');
+
+    //standard vibrate with new call param int
+    createActionButton('Vibrate with int', function() {
+        vibrateWithInt();
+    }, 'vibrate_int');
+
+    //standard vibrate with new call param array
+    createActionButton('Vibrate with array', function() {
+        vibrateWithArray();
+    }, 'vibrate_array');
+
+    //vibrate with a pattern
+    createActionButton('* Vibrate with a pattern', function() {
+        vibrateWithPattern();
+    }, 'vibrate_with_pattern');
+
+    //cancel any existing vibrations with param 0
+    createActionButton('* Cancel vibration with 0', function() {
+
+        if (!vibrateOn)
+        {
+            longVibrate();
+        }
+        else
+        {
+            cancelWithZero();
+            resetVibrateOn();
+            clearTimeout(timeout); //clear the timeout since user has canceled the vibrate
+        }
+    }, 'cancel_zero');
+
+    //cancel any existing vibrations with param []
+    createActionButton('* Cancel vibration with []', function() {
+
+        if (!vibrateOn)
+        {
+            longVibrate();
+        }
+        else
+        {
+            cancelWithEmpty();
+            resetVibrateOn();
+            clearTimeout(timeout); //clear the timeout since user has canceled the vibrate
+        }
+    }, 'cancel_array');
+
+    //cancel vibration with pattern with param 0
+    createActionButton('* Cancel vibration with pattern with 0', function() {
+
+        if (!vibrateOn)
+        {
+            longVibrateWithPattern();
+        }
+        else
+        {
+            cancelWithZero();
+            resetVibrateOn();
+            clearTimeout(timeout); //clear the timeout since user has canceled the vibrate
+        }
+    }, 'cancelWithPattern_zero');
+
+    //cancel vibration with pattern with param []
+    createActionButton('* Cancel vibration with pattern with []', function() {
+
+        if (!vibrateOn)
+        {
+            longVibrateWithPattern();
+        }
+        else
+        {
+            cancelWithEmpty();
+            resetVibrateOn();
+            clearTimeout(timeout); //clear the timeout since user has canceled the vibrate
+        }
+    }, 'cancelWithPattern_array');
+
+    //cancel multiple vibrations
+    createActionButton('* Cancel multiple vibrations', function() {
+
+        if (!vibrateOn)
+        {
+            multipleVibrations();
+        }
+        else
+        {
+            cancelWithZero();
+            resetVibrateOn();
+            clearTimeout(timeout); //clear the timeout since user has canceled the vibrate
+        }
+    }, 'cancelMultipleVibrations');
+};