Merge "Add simple tests for all vector types in RS API."
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 756401b..c881e28 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -951,6 +951,13 @@
         <activity android:name="android.app.cts.ActivityManagerMemoryClassTestActivity"
                 android:process=":memoryclass" />
 
+        <service android:name="android.speech.tts.cts.StubTextToSpeechService">
+            <intent-filter>
+                <action android:name="android.intent.action.TTS_SERVICE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </service>
+
     </application>
 
     <!--Test for PackageManager, please put this at the very beginning-->
diff --git a/tests/assets/webkit/form_page.html b/tests/assets/webkit/form_page.html
index b7a32a3..a1fe48a 100644
--- a/tests/assets/webkit/form_page.html
+++ b/tests/assets/webkit/form_page.html
@@ -18,24 +18,70 @@
     <title>Test Page</title>
 </head>
 <body>
-  <a name="text" />
+  <a id="tagA">Tag A</a>
+  <a name="text"></a>
   <a id=someTextId>Some text&nbsp;</a>
   <a id=nestedLinkId>Is not nested, and is not unique!</a>
-  <div id=divId>A div
-    <a id=nestedLinkId>Nested text</a>
+  <a class="divClass">Outter text</a>
+  <a id="outter" name="foo">Foo</a>
+
+  <div id=divId name="firstDiv">A div
+    <a id=nestedLinkId class="divClass" name="nestedLink">Nested text</a>
+    <a id="n1" name="linkn1">a nested link</a>
+    <a id="inner" name="foo">Foo</a>
+    <span id="n1" name="spann1"></span>
+    <a name=nested>Nested text</a>
   </div>
   <p id="spaces">     </p>
   <p id="empty"></p>
-  <a href="foo" id="linkWithEqualsSign">Link=equalssign</a>
+  <a name=foo href="foo" id="linkWithEqualsSign">Link=equalssign</a>
   <p id="self-closed" />Here is some content that should not be in the previous p tag
 
   <p class=" spaceAround ">Spaced out</p>
 
+  <a name="text" id="emptyLink"> </a>
+  <p id="spaces">     </p>
+  <p id="empty"></p>
+  <p id="self-closed"/>
+
+  <div>
+  <a id="id1" href="#">Foo</a>
+  <ul id="id2" />
+  <span id="id3"/>
+  <p id="id3">A paragraph</p>
+  </div>
+
+  <input name="inputDisabled" id="inputDisabled" disabled="disabled"> </input>
   <span id="my_span">
     <div>first_div</div>
     <div>second_div</div>
     <span>first_span</span>
     <span>second_span</span>
+    <a id="Foo">Foo</a>
   </span>
+
+  <select name="selectomatic">
+          <option id="one" selected="selected">One</option>
+          <option id="two">Two</option>
+          <option id="four">Four</option>
+          <option>Still learning how to count, apparently</option>
+  </select>
+
+  <select name="multi" id="multi" multiple="multiple">
+         <option id="eggs" selected="selected">Eggs</option>
+         <option id="ham" >Ham</option>
+         <option selected="selected">Sausages</option>
+         <option>Onion gravy</option>
+  </select>
+
+  <input type="radio" id="cheese" name="snack" value="cheese"/>Cheese<br/>
+  <input type="radio" id="peas" name="snack" value="peas"/>Peas<br/>
+  <input type="radio" id="cheese_and_peas" name="snack" value="cheese and peas" checked/>Cheese and peas<br/>
+  <input type="radio" id="nothing" name="snack" value="nowt" disabled="disabled"/>Not a sausage
+
+  <input type="hidden" name="hidden" value="fromage" />
+  Here's a checkbox: <input type="checkbox" id="checky" name="checky" value="furrfu"/><br/>
+
+
 </body>
 </html>
diff --git a/tests/src/android/speech/tts/cts/StubTextToSpeechService.java b/tests/src/android/speech/tts/cts/StubTextToSpeechService.java
new file mode 100644
index 0000000..ed675df
--- /dev/null
+++ b/tests/src/android/speech/tts/cts/StubTextToSpeechService.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.speech.tts.cts;
+
+import android.media.AudioFormat;
+import android.speech.tts.SynthesisRequest;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.TextToSpeechService;
+
+/**
+ * Stub implementation of {@link TextToSpeechService}. Used for testing the
+ * TTS engine API.
+ */
+public class StubTextToSpeechService extends TextToSpeechService {
+
+    public static final String TEXT_STREAM = "stream";
+    public static final String TEXT_COMPLETE = "complete";
+
+    @Override
+    protected String[] onGetLanguage() {
+        return new String[] { "eng", "USA", "" };
+    }
+
+    @Override
+    protected int onIsLanguageAvailable(String lang, String country, String variant) {
+        return TextToSpeech.LANG_AVAILABLE;
+    }
+
+    @Override
+    protected int onLoadLanguage(String lang, String country, String variant) {
+        return TextToSpeech.LANG_AVAILABLE;
+    }
+
+    @Override
+    protected void onStop() {
+    }
+
+    @Override
+    protected void onSynthesizeText(SynthesisRequest request) {
+        if (TEXT_STREAM.equals(request.getText())) {
+            synthesizeStreaming(request);
+        } else {
+            synthesizeComplete(request);
+        }
+    }
+
+    private void synthesizeStreaming(SynthesisRequest request) {
+        if (request.start(16000, AudioFormat.ENCODING_PCM_16BIT, 1) != TextToSpeech.SUCCESS) {
+            return;
+        }
+        byte[] data = { 0x01, 0x2 };
+        if (request.audioAvailable(data, 0, data.length) != TextToSpeech.SUCCESS) {
+            return;
+        }
+        if (request.done() != TextToSpeech.SUCCESS) {
+            return;
+        }
+    }
+
+    private void synthesizeComplete(SynthesisRequest request) {
+        byte[] data = { 0x01, 0x2 };
+        if (request.completeAudioAvailable(16000, AudioFormat.ENCODING_PCM_16BIT, 1,
+                data, 0, data.length) != TextToSpeech.SUCCESS) {
+            return;
+        }
+    }
+
+}
diff --git a/tests/tests/speech/Android.mk b/tests/tests/speech/Android.mk
index a15b2ec..932c564 100755
--- a/tests/tests/speech/Android.mk
+++ b/tests/tests/speech/Android.mk
@@ -29,5 +29,7 @@
 
 LOCAL_SDK_VERSION := current
 
+LOCAL_INSTRUMENTATION_FOR := CtsTestStubs
+
 include $(BUILD_PACKAGE)
 
diff --git a/tests/tests/speech/AndroidManifest.xml b/tests/tests/speech/AndroidManifest.xml
index 6e1e528..778f763 100755
--- a/tests/tests/speech/AndroidManifest.xml
+++ b/tests/tests/speech/AndroidManifest.xml
@@ -24,9 +24,8 @@
 
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
-    <!--  self-instrumenting test package. -->
-    <instrumentation android:name="android.test.InstrumentationTestRunner"
-                     android:targetPackage="com.android.cts.speech"
+    <instrumentation android:name="android.test.InstrumentationCtsTestRunner"
+                     android:targetPackage="com.android.cts.stub"
                      android:label="CTS tests of android.speech"/>
 
 </manifest>
diff --git a/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechServiceTest.java b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechServiceTest.java
new file mode 100644
index 0000000..7babe5a
--- /dev/null
+++ b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechServiceTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.speech.tts.cts;
+
+import android.os.Environment;
+import android.speech.tts.TextToSpeech;
+import android.test.AndroidTestCase;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.HashMap;
+
+/**
+ * Tests for {@link android.speech.tts.TextToSpeechService} using StubTextToSpeechService.
+ */
+public class TextToSpeechServiceTest extends AndroidTestCase {
+
+    private static final String UTTERANCE_ID = "utterance";
+    private static final String SAMPLE_FILE_NAME = "mytts.wav";
+
+    private TextToSpeechWrapper mTts;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mTts = TextToSpeechWrapper.createTextToSpeechMockWrapper(getContext());
+        assertNotNull(mTts);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        mTts.shutdown();
+    }
+
+    private TextToSpeech getTts() {
+        return mTts.getTts();
+    }
+
+    public void testSynthesizeToFileStreaming() throws Exception {
+        File sampleFile = new File(Environment.getExternalStorageDirectory(), SAMPLE_FILE_NAME);
+        try {
+            assertFalse(sampleFile.exists());
+
+            int result = getTts().synthesizeToFile(StubTextToSpeechService.TEXT_STREAM,
+                    createParams(), sampleFile.getPath());
+            assertEquals("synthesizeToFile() failed", TextToSpeech.SUCCESS, result);
+
+            assertTrue("synthesizeToFile() completion timeout", mTts.waitForComplete(UTTERANCE_ID));
+            assertTrue("synthesizeToFile() didn't produce a file", sampleFile.exists());
+            assertTrue("synthesizeToFile() produced a non-sound file",
+                    TextToSpeechWrapper.isSoundFile(sampleFile.getPath()));
+        } finally {
+            sampleFile.delete();
+        }
+    }
+
+    public void testSynthesizeToFileComplete() throws Exception {
+        File sampleFile = new File(Environment.getExternalStorageDirectory(), SAMPLE_FILE_NAME);
+        try {
+            assertFalse(sampleFile.exists());
+
+            int result = getTts().synthesizeToFile(StubTextToSpeechService.TEXT_COMPLETE,
+                    createParams(), sampleFile.getPath());
+            assertEquals("synthesizeToFile() failed", TextToSpeech.SUCCESS, result);
+
+            assertTrue("synthesizeToFile() completion timeout", waitForUtterance());
+            assertTrue("synthesizeToFile() didn't produce a file", sampleFile.exists());
+            assertTrue("synthesizeToFile() produced a non-sound file",
+                    TextToSpeechWrapper.isSoundFile(sampleFile.getPath()));
+        } finally {
+            sampleFile.delete();
+        }
+    }
+
+    public void testSpeakStreaming() throws Exception {
+        int result = getTts().speak(StubTextToSpeechService.TEXT_STREAM, TextToSpeech.QUEUE_FLUSH,
+                createParams());
+        assertEquals("speak() failed", TextToSpeech.SUCCESS, result);
+        assertTrue("speak() completion timeout", waitForUtterance());
+    }
+
+    public void testSpeakComplete() throws Exception {
+        int result = getTts().speak(StubTextToSpeechService.TEXT_COMPLETE, TextToSpeech.QUEUE_FLUSH,
+                createParams());
+        assertEquals("speak() failed", TextToSpeech.SUCCESS, result);
+        assertTrue("speak() completion timeout", waitForUtterance());
+    }
+
+    public void testMediaPlayerFails() throws Exception {
+        File sampleFile = new File(Environment.getExternalStorageDirectory(), "notsound.wav");
+        try {
+            assertFalse(TextToSpeechWrapper.isSoundFile(sampleFile.getPath()));
+            FileOutputStream out = new FileOutputStream(sampleFile);
+            out.write(new byte[] { 0x01, 0x02 });
+            out.close();
+            assertFalse(TextToSpeechWrapper.isSoundFile(sampleFile.getPath()));
+        } finally {
+            sampleFile.delete();
+        }
+    }
+
+    private HashMap<String, String> createParams() {
+        HashMap<String, String> params = new HashMap<String,String>();
+        params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, UTTERANCE_ID);
+        return params;
+    }
+
+    private boolean waitForUtterance() throws InterruptedException {
+        return mTts.waitForComplete(UTTERANCE_ID);
+    }
+
+}
diff --git a/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechTest.java b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechTest.java
index e012fe3..cb8aa81 100644
--- a/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechTest.java
+++ b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechTest.java
@@ -15,98 +15,31 @@
  */
 package android.speech.tts.cts;
 
-import dalvik.annotation.TestTargetClass;
-
-import android.media.MediaPlayer;
 import android.os.Environment;
 import android.speech.tts.TextToSpeech;
-import android.speech.tts.TextToSpeech.OnInitListener;
-import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
 import android.test.AndroidTestCase;
-import android.util.Log;
 
 import java.io.File;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 
 /**
  * Tests for {@link android.speech.tts.TextToSpeech}
  */
-@TestTargetClass(TextToSpeech.class)
 public class TextToSpeechTest extends AndroidTestCase {
 
-    private TextToSpeech mTts;
-
-    private static final String UTTERANCE = "utterance";
+    private static final String UTTERANCE_ID = "utterance";
     private static final String SAMPLE_TEXT = "This is a sample text to speech string";
     private static final String SAMPLE_FILE_NAME = "mytts.wav";
-    /** maximum time to wait for tts to be initialized */
-    private static final int TTS_INIT_MAX_WAIT_TIME = 30 * 1000;
-    /** maximum time to wait for speech call to be complete */
-    private static final int TTS_SPEECH_MAX_WAIT_TIME = 5 * 1000;
-    private static final String LOG_TAG = "TextToSpeechTest";
 
-    /**
-     * Listener for waiting for TTS engine initialization completion.
-     */
-    private static class InitWaitListener implements OnInitListener {
-        private int mStatus = TextToSpeech.ERROR;
-
-        public void onInit(int status) {
-            mStatus = status;
-            synchronized(this) {
-                notify();
-            }
-        }
-
-        public boolean waitForInit() throws InterruptedException {
-            if (mStatus == TextToSpeech.SUCCESS) {
-                return true;
-            }
-            synchronized (this) {
-                wait(TTS_INIT_MAX_WAIT_TIME);
-            }
-            return mStatus == TextToSpeech.SUCCESS;
-        }
-    }
-
-    /**
-     * Listener for waiting for utterance completion.
-     */
-    private static class UtteranceWaitListener implements OnUtteranceCompletedListener {
-        private boolean mIsComplete = false;
-        private final String mExpectedUtterance;
-
-        public UtteranceWaitListener(String expectedUtteranceId) {
-            mExpectedUtterance = expectedUtteranceId;
-        }
-
-        public void onUtteranceCompleted(String utteranceId) {
-            if (mExpectedUtterance.equals(utteranceId)) {
-                synchronized(this) {
-                    mIsComplete = true;
-                    notify();
-                }
-            }
-        }
-
-        public boolean waitForComplete() throws InterruptedException {
-            if (mIsComplete) {
-                return true;
-            }
-            synchronized (this) {
-                wait(TTS_SPEECH_MAX_WAIT_TIME);
-                return mIsComplete;
-            }
-        }
-    }
+    private TextToSpeechWrapper mTts;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        InitWaitListener listener = new InitWaitListener();
-        mTts = new TextToSpeech(getContext(), listener);
-        assertTrue(listener.waitForInit());
+        mTts = TextToSpeechWrapper.createTextToSpeechWrapper(getContext());
+        assertNotNull(mTts);
         assertTrue(checkAndSetLanguageAvailable());
     }
 
@@ -116,76 +49,80 @@
         mTts.shutdown();
     }
 
+    private TextToSpeech getTts() {
+        return mTts.getTts();
+    }
+
     /**
      * Ensures at least one language is available for tts
      */
-    private boolean  checkAndSetLanguageAvailable() {
+    private boolean checkAndSetLanguageAvailable() {
         // checks if at least one language is available in Tts
         for (Locale locale : Locale.getAvailableLocales()) {
-            int availability = mTts.isLanguageAvailable(locale);
+            int availability = getTts().isLanguageAvailable(locale);
             if (availability == TextToSpeech.LANG_AVAILABLE ||
                 availability == TextToSpeech.LANG_COUNTRY_AVAILABLE ||
                 availability == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) {
-                mTts.setLanguage(locale);
+                getTts().setLanguage(locale);
                 return true;
             }
         }
         return false;
     }
 
-    /**
-     * Tests that {@link TextToSpeech#synthesizeToFile(String, java.util.HashMap, String)} produces
-     * a non-zero sized file.
-     * @throws InterruptedException
-     */
     public void testSynthesizeToFile() throws Exception {
         File sampleFile = new File(Environment.getExternalStorageDirectory(), SAMPLE_FILE_NAME);
         try {
             assertFalse(sampleFile.exists());
-            // use an utterance listener to determine when synthesizing is complete
-            HashMap<String, String> param = new HashMap<String,String>();
-            param.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, UTTERANCE);
-            UtteranceWaitListener listener = new UtteranceWaitListener(UTTERANCE);
-            mTts.setOnUtteranceCompletedListener(listener);
 
-            int result = mTts.synthesizeToFile(SAMPLE_TEXT, param, sampleFile.getPath());
-            assertEquals(TextToSpeech.SUCCESS, result);
+            int result = getTts().synthesizeToFile(SAMPLE_TEXT, createParams(),
+                    sampleFile.getPath());
+            assertEquals("synthesizeToFile() failed", TextToSpeech.SUCCESS, result);
 
-            assertTrue(listener.waitForComplete());
-            assertTrue(sampleFile.exists());
-            assertTrue(isMusicFile(sampleFile.getPath()));
-
+            assertTrue("synthesizeToFile() completion timeout", waitForUtterance());
+            assertTrue("synthesizeToFile() didn't produce a file", sampleFile.exists());
+            assertTrue("synthesizeToFile() produced a non-sound file",
+                    TextToSpeechWrapper.isSoundFile(sampleFile.getPath()));
         } finally {
-            deleteFile(sampleFile);
+            sampleFile.delete();
         }
     }
 
-    /**
-     * Determine if given file path is a valid, playable music file.
-     */
-    private boolean isMusicFile(String filePath) {
-        // use media player to play the file. If it succeeds with no exceptions, assume file is
-        //valid
-        try {
-            MediaPlayer mp = new MediaPlayer();
-            mp.setDataSource(filePath);
-            mp.prepare();
-            mp.start();
-            mp.stop();
-            return true;
-        } catch (Exception e) {
-            Log.e(LOG_TAG, "Exception while attempting to play music file", e);
-            return false;
-        }
+    public void testSpeak() throws Exception {
+        int result = getTts().speak(SAMPLE_TEXT, TextToSpeech.QUEUE_FLUSH, createParams());
+        assertEquals("speak() failed", TextToSpeech.SUCCESS, result);
+        assertTrue("speak() completion timeout", waitForUtterance());
     }
 
-    /**
-     * Deletes the file at given path
-     * @param sampleFilePath
-     */
-    private void deleteFile(File file) {
-        if (file != null && file.exists()) {
-            file.delete();
-        }
+    public void testGetEnginesIncludesDefault() throws Exception {
+        List<TextToSpeech.EngineInfo> engines = getTts().getEngines();
+        assertNotNull("getEngines() returned null", engines);
+        assertContainsEngine(getTts().getDefaultEngine(), engines);
     }
+
+    public void testGetEnginesIncludesMock() throws Exception {
+        List<TextToSpeech.EngineInfo> engines = getTts().getEngines();
+        assertNotNull("getEngines() returned null", engines);
+        assertContainsEngine(TextToSpeechWrapper.MOCK_TTS_ENGINE, engines);
+    }
+
+    private void assertContainsEngine(String engine, List<TextToSpeech.EngineInfo> engines) {
+        for (TextToSpeech.EngineInfo engineInfo : engines) {
+            if (engineInfo.name.equals(engine)) {
+                return;
+            }
+        }
+        fail("Engine " + engine + " not found");
+    }
+
+    private HashMap<String, String> createParams() {
+        HashMap<String, String> params = new HashMap<String,String>();
+        params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, UTTERANCE_ID);
+        return params;
+    }
+
+    private boolean waitForUtterance() throws InterruptedException {
+        return mTts.waitForComplete(UTTERANCE_ID);
+    }
+
 }
diff --git a/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechWrapper.java b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechWrapper.java
new file mode 100644
index 0000000..cba242f
--- /dev/null
+++ b/tests/tests/speech/src/android/speech/tts/cts/TextToSpeechWrapper.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.speech.tts.cts;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.TextToSpeech.OnInitListener;
+import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
+import android.util.Log;
+
+import java.util.HashSet;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Wrapper for {@link TextToSpeech} with some handy test functionality.
+ */
+public class TextToSpeechWrapper {
+    private static final String LOG_TAG = "TextToSpeechServiceTest";
+
+    public static final String MOCK_TTS_ENGINE = "com.android.cts.stub";
+
+    private final Context mContext;
+    private TextToSpeech mTts;
+    private final InitWaitListener mInitListener;
+    private final UtteranceWaitListener mUtteranceListener;
+
+    /** maximum time to wait for tts to be initialized */
+    private static final int TTS_INIT_MAX_WAIT_TIME = 30 * 1000;
+    /** maximum time to wait for speech call to be complete */
+    private static final int TTS_SPEECH_MAX_WAIT_TIME = 5 * 1000;
+
+    private TextToSpeechWrapper(Context context) {
+        mContext = context;
+        mInitListener = new InitWaitListener();
+        mUtteranceListener = new UtteranceWaitListener();
+    }
+
+    private boolean initTts() throws InterruptedException {
+        return initTts(new TextToSpeech(mContext, mInitListener));
+    }
+
+    private boolean initTts(String engine) throws InterruptedException {
+        return initTts(new TextToSpeech(mContext, mInitListener, engine));
+    }
+
+    private boolean initTts(TextToSpeech tts) throws InterruptedException {
+        mTts = tts;
+        if (!mInitListener.waitForInit()) {
+            return false;
+        }
+        mTts.setOnUtteranceCompletedListener(mUtteranceListener);
+        return true;
+    }
+
+    public boolean waitForComplete(String utteranceId) throws InterruptedException {
+        return mUtteranceListener.waitForComplete(utteranceId);
+    }
+
+    public TextToSpeech getTts() {
+        return mTts;
+    }
+
+    public void shutdown() {
+        mTts.shutdown();
+    }
+
+    public static TextToSpeechWrapper createTextToSpeechWrapper(Context context)
+            throws InterruptedException {
+        TextToSpeechWrapper wrapper = new TextToSpeechWrapper(context);
+        if (wrapper.initTts()) {
+            return wrapper;
+        } else {
+            return null;
+        }
+    }
+
+    public static TextToSpeechWrapper createTextToSpeechMockWrapper(Context context)
+            throws InterruptedException {
+        TextToSpeechWrapper wrapper = new TextToSpeechWrapper(context);
+        if (wrapper.initTts(MOCK_TTS_ENGINE)) {
+            return wrapper;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Listener for waiting for TTS engine initialization completion.
+     */
+    private static class InitWaitListener implements OnInitListener {
+        private final Lock mLock = new ReentrantLock();
+        private final Condition mDone  = mLock.newCondition();
+        private Integer mStatus = null;
+
+        public void onInit(int status) {
+            mLock.lock();
+            try {
+                mStatus = new Integer(status);
+                mDone.signal();
+            } finally {
+                mLock.unlock();
+            }
+        }
+
+        public boolean waitForInit() throws InterruptedException {
+            long timeOutNanos = TimeUnit.MILLISECONDS.toNanos(TTS_INIT_MAX_WAIT_TIME);
+            mLock.lock();
+            try {
+                while (mStatus == null) {
+                    if (timeOutNanos <= 0) {
+                        return false;
+                    }
+                    timeOutNanos = mDone.awaitNanos(timeOutNanos);
+                }
+                return mStatus == TextToSpeech.SUCCESS;
+            } finally {
+                mLock.unlock();
+            }
+        }
+    }
+
+    /**
+     * Listener for waiting for utterance completion.
+     */
+    private static class UtteranceWaitListener implements OnUtteranceCompletedListener {
+        private final Lock mLock = new ReentrantLock();
+        private final Condition mDone  = mLock.newCondition();
+        private final HashSet<String> mCompletedUtterances = new HashSet<String>();
+
+        public void onUtteranceCompleted(String utteranceId) {
+            mLock.lock();
+            try {
+                mCompletedUtterances.add(utteranceId);
+                mDone.signal();
+            } finally {
+                mLock.unlock();
+            }
+        }
+
+        public boolean waitForComplete(String utteranceId)
+                throws InterruptedException {
+            long timeOutNanos = TimeUnit.MILLISECONDS.toNanos(TTS_INIT_MAX_WAIT_TIME);
+            mLock.lock();
+            try {
+                while (!mCompletedUtterances.remove(utteranceId)) {
+                    if (timeOutNanos <= 0) {
+                        return false;
+                    }
+                    timeOutNanos = mDone.awaitNanos(timeOutNanos);
+                }
+                return true;
+            } finally {
+                mLock.unlock();
+            }
+        }
+    }
+
+    /**
+     * Determines if given file path is a valid, playable music file.
+     */
+    public static boolean isSoundFile(String filePath) {
+        // use media player to play the file. If it succeeds with no exceptions, assume file is
+        //valid
+        MediaPlayer mp = null;
+        try {
+            mp = new MediaPlayer();
+            mp.setDataSource(filePath);
+            mp.prepare();
+            mp.start();
+            mp.stop();
+            return true;
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Exception while attempting to play music file", e);
+            return false;
+        } finally {
+            if (mp != null) {
+                mp.release();
+            }
+        }
+    }
+
+}
diff --git a/tests/tests/webkit/src/android/webkit/cts/WebDriverTest.java b/tests/tests/webkit/src/android/webkit/cts/WebDriverTest.java
index 93c7b5f..e933420 100644
--- a/tests/tests/webkit/src/android/webkit/cts/WebDriverTest.java
+++ b/tests/tests/webkit/src/android/webkit/cts/WebDriverTest.java
@@ -19,9 +19,9 @@
 import android.test.ActivityInstrumentationTestCase2;
 import android.webkit.webdriver.By;
 import android.webkit.webdriver.WebDriver;
+import android.webkit.webdriver.WebDriverException;
 import android.webkit.webdriver.WebElement;
 import android.webkit.webdriver.WebElementNotFoundException;
-import android.webkit.webdriver.WebElementStaleException;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -39,12 +39,17 @@
     private WebDriver mDriver;
     private CtsTestServer mWebServer;
     private static final String SOME_TEXT = "Some text";
-    private static final String DIV_TEXT = "A div Nested text";
+    private static final String DIV_TEXT =
+            "A div Nested text a nested link Foo Nested text";
     private static final String NESTED_TEXT = "Nested text";
     private static final String DIV_ID = "divId";
     private static final String SOME_TEXT_ID = "someTextId";
     private static final String BAD_ID = "BadId";
     private static final String NESTED_LINK_ID = "nestedLinkId";
+    private static final String FIRST_DIV = "firstDiv";
+    private static final String INEXISTENT = "inexistent";
+    private static final String ID = "id";
+    private static final String OUTTER = "outter";
 
     public WebDriverTest() {
         super(WebDriverStubActivity.class);
@@ -68,6 +73,134 @@
         assertTrue(mDriver.getPageSource().contains("hello world!"));
     }
 
+    // getText
+    public void testGetTextReturnsEmptyString() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement emptyLink = mDriver.findElement(By.id("emptyLink"));
+        assertEquals("", emptyLink.getText());
+    }
+
+    // getAttribute
+    public void testGetValidAttribute() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement link = mDriver.findElement(By.linkText("Link=equalssign"));
+        assertEquals("foo", link.getAttribute("href"));
+    }
+
+    public void testGetInvalidAttributeReturnsNull() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement link = mDriver.findElement(By.linkText("Link=equalssign"));
+        assertNull(link.getAttribute(INEXISTENT));
+    }
+
+    public void testGetAttributeNotSetReturnsNull() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement link = mDriver.findElement(By.linkText("Link=equalssign"));
+        assertNull(link.getAttribute("disabled"));
+    }
+
+    // getTagName
+    public void testTagName() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement span = mDriver.findElement(By.tagName("span"));
+        assertEquals("SPAN", span.getTagName());
+    }
+
+    // isEnabled
+    public void testIsEnabled() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement div = mDriver.findElement(By.id(DIV_ID));
+        assertTrue(div.isEnabled());
+
+        WebElement input = mDriver.findElement(By.name("inputDisabled"));
+        assertFalse(input.isEnabled());
+    }
+
+    // isSelected
+    public void testIsSelected() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement optionOne = mDriver.findElement(By.id("one"));
+        assertTrue(optionOne.isSelected());
+
+        WebElement optionTwo = mDriver.findElement(By.id("two"));
+        assertFalse(optionTwo.isSelected());
+
+        WebElement selectEggs = mDriver.findElement(By.id("eggs"));
+        assertTrue(selectEggs.isSelected());
+
+        WebElement selectHam = mDriver.findElement(By.id("ham"));
+        assertFalse(selectHam.isSelected());
+
+        WebElement inputCheese = mDriver.findElement(By.id("cheese"));
+        assertFalse(inputCheese.isSelected());
+
+        WebElement inputCheesePeas = mDriver.findElement(
+                By.id("cheese_and_peas"));
+        assertTrue(inputCheesePeas.isSelected());
+    }
+
+    public void testIsSelectedOnHiddenInputThrows() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement inputHidden = mDriver.findElement(By.name("hidden"));
+        try {
+            inputHidden.isSelected();
+            fail();
+        } catch (WebDriverException e) {
+            // This is expcted
+        }
+    }
+
+    public void testIsSelectedOnNonSelectableElementThrows() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement link= mDriver.findElement(By.linkText("Foo"));
+        try {
+            link.isSelected();
+            fail();
+        } catch (WebDriverException e) {
+            // This is expected
+        }
+    }
+
+    // toogle
+    public void testToggleCheckbox() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement check = mDriver.findElement(By.id("checky"));
+        assertFalse(check.isSelected());
+        assertTrue(check.toggle());
+        assertFalse(check.toggle());
+    }
+
+    public void testToggleOnNonTogglableElements() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement inputHidden = mDriver.findElement(By.name("hidden"));
+        try {
+            inputHidden.toggle();
+            fail();
+        } catch (WebDriverException e) {
+            // This is expected
+        }
+    }
+
+    // findElement
+    public void testFindElementThrowsIfNoPageIsLoaded() {
+        try {
+            mDriver.findElement(By.id(SOME_TEXT_ID));
+            fail();
+        } catch (NullPointerException e) {
+            // this is expected
+        }
+    }
+
+    // findElements
+    public void testFindElementsThrowsIfNoPageIsLoaded() {
+        try {
+            mDriver.findElements(By.id(SOME_TEXT_ID));
+            fail();
+        } catch (NullPointerException e) {
+            // this is expected
+        }
+    }
+
     // By id
     public void testFindElementById() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
@@ -95,20 +228,39 @@
         assertTrue(NESTED_TEXT.equals(nestedNode.getText()));
     }
 
+    public void testFindElementsById() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements = mDriver.findElements(By.id(ID + "3"));
+        assertEquals(2, elements.size());
+        assertEquals("A paragraph", elements.get(1).getText());
+    }
+
+    public void testFindElementsByIdReturnsEmptyListIfNoResultsFound() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements = mDriver.findElements(By.id(INEXISTENT));
+        assertNotNull(elements);
+        assertEquals(0, elements.size());
+    }
+
+    public void testFindNestedElementsById() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement div = mDriver.findElement(By.name(FIRST_DIV));
+        List<WebElement> elements = div.findElements(By.id("n1"));
+        assertEquals(2, elements.size());
+        assertEquals("spann1", elements.get(1).getAttribute("name"));
+    }
+
     // By linkText
     public void testFindElementByLinkText() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
-        WebElement element = mDriver.findElement(By.id(SOME_TEXT_ID));
-        assertTrue(SOME_TEXT.equals(element.getText()));
-
-        element = mDriver.findElement(By.id(DIV_ID));
-        assertTrue(DIV_TEXT.equals(element.getText()));
+        WebElement element = mDriver.findElement(By.linkText("Nested text"));
+        assertTrue(NESTED_LINK_ID.equals(element.getAttribute(ID)));
     }
 
     public void testFindElementByLinkTextThrowsIfElementDoesNotExists() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         try {
-            mDriver.findElement(By.id(BAD_ID));
+            mDriver.findElement(By.linkText(INEXISTENT));
             fail("This should have failed.");
         } catch (WebElementNotFoundException e) {
             // This is expected
@@ -118,24 +270,41 @@
     public void testFindNestedElementByLinkText() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         WebElement parent = mDriver.findElement(By.id(DIV_ID));
-        WebElement nestedNode = parent.findElement(By.id(NESTED_LINK_ID));
-        assertTrue(NESTED_TEXT.equals(nestedNode.getText()));
+        WebElement nestedNode = parent.findElement(By.linkText("Foo"));
+        assertTrue("inner".equals(nestedNode.getAttribute(ID)));
+    }
+
+    public void testFindElementsByLinkText() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements = mDriver.findElements(By.linkText("Foo"));
+        assertEquals(4, elements.size());
+    }
+
+    public void testFindElementsByLinkTextReturnsEmptyListIfNoResultsFound() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements = mDriver.findElements(By.linkText("Boo"));
+        assertEquals(0, elements.size());
+    }
+
+    public void testFindNestedElementsByLinkText() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement div = mDriver.findElement(By.name(FIRST_DIV));
+        List<WebElement> elements =
+                div.findElements(By.linkText("Nested text"));
+        assertEquals(2, elements.size());
     }
 
     // By partialLinkText
     public void testFindElementByPartialLinkText() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
-        WebElement element = mDriver.findElement(By.id(SOME_TEXT_ID));
+        WebElement element = mDriver.findElement(By.partialLinkText("text"));
         assertTrue(SOME_TEXT.equals(element.getText()));
-
-        element = mDriver.findElement(By.id(DIV_ID));
-        assertTrue(DIV_TEXT.equals(element.getText()));
     }
 
     public void testFindElementByPartialLinkTextThrowsIfElementDoesNotExists() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         try {
-            mDriver.findElement(By.id(BAD_ID));
+            mDriver.findElement(By.partialLinkText(INEXISTENT));
             fail("This should have failed.");
         } catch (WebElementNotFoundException e) {
             // This is expected
@@ -145,24 +314,44 @@
     public void testFindNestedElementByPartialLinkText() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         WebElement parent = mDriver.findElement(By.id(DIV_ID));
-        WebElement nestedNode = parent.findElement(By.id(NESTED_LINK_ID));
+        WebElement nestedNode = parent.findElement(By.partialLinkText("text"));
         assertTrue(NESTED_TEXT.equals(nestedNode.getText()));
     }
 
+    public void testFindElementsByPartialLinkText() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements =
+                mDriver.findElements(By.partialLinkText("text"));
+        assertTrue(elements.size() > 2);
+    }
+
+    public void
+    testFindElementsByPartialLinkTextReturnsEmptyListIfNoResultsFound() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements =
+                mDriver.findElements(By.partialLinkText(INEXISTENT));
+        assertEquals(0, elements.size());
+    }
+
+    public void testFindNestedElementsByPartialLinkText() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement div = mDriver.findElements(By.name(FIRST_DIV)).get(0);
+        List<WebElement> elements =
+                div.findElements(By.partialLinkText("text"));
+        assertEquals(2, elements.size());
+    }
+
     // by name
     public void testFindElementByName() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
-        WebElement element = mDriver.findElement(By.id(SOME_TEXT_ID));
-        assertTrue(SOME_TEXT.equals(element.getText()));
-
-        element = mDriver.findElement(By.id(DIV_ID));
-        assertTrue(DIV_TEXT.equals(element.getText()));
+        WebElement element = mDriver.findElement(By.name("foo"));
+        assertTrue(OUTTER.equals(element.getAttribute(ID)));
     }
 
     public void testFindElementByNameThrowsIfElementDoesNotExists() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         try {
-            mDriver.findElement(By.id(BAD_ID));
+            mDriver.findElement(By.name(INEXISTENT));
             fail("This should have failed.");
         } catch (WebElementNotFoundException e) {
             // This is expected
@@ -172,24 +361,42 @@
     public void testFindNestedElementByName() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         WebElement parent = mDriver.findElement(By.id(DIV_ID));
-        WebElement nestedNode = parent.findElement(By.id(NESTED_LINK_ID));
+        WebElement nestedNode = parent.findElement(By.name("nestedLink"));
         assertTrue(NESTED_TEXT.equals(nestedNode.getText()));
     }
 
+    public void testFindElementsByName() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements = mDriver.findElements(By.name("text"));
+        assertEquals(2, elements.size());
+    }
+
+    public void testFindElementsByNameReturnsEmptyListIfNoResultsFound() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements = mDriver.findElements(By.name(INEXISTENT));
+        assertEquals(0, elements.size());
+    }
+
+    public void testFindNestedElementsByName() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement div = mDriver.findElements(By.xpath(
+                "//div[@" + ID + "='divId']"))
+                .get(0);
+        List<WebElement> elements = div.findElements(By.name("foo"));
+        assertEquals(1, elements.size());
+    }
+
     // By tagName
     public void testFindElementByTagName() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
-        WebElement element = mDriver.findElement(By.id(SOME_TEXT_ID));
-        assertTrue(SOME_TEXT.equals(element.getText()));
-
-        element = mDriver.findElement(By.id(DIV_ID));
-        assertTrue(DIV_TEXT.equals(element.getText()));
+        WebElement element = mDriver.findElement(By.tagName("a"));
+        assertTrue("Tag A".equals(element.getText()));
     }
 
     public void testFindElementByTagNameThrowsIfElementDoesNotExists() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         try {
-            mDriver.findElement(By.id(BAD_ID));
+            mDriver.findElement(By.tagName(INEXISTENT));
             fail("This should have failed.");
         } catch (WebElementNotFoundException e) {
             // This is expected
@@ -199,24 +406,47 @@
     public void testFindNestedElementByTagName() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         WebElement parent = mDriver.findElement(By.id(DIV_ID));
-        WebElement nestedNode = parent.findElement(By.id(NESTED_LINK_ID));
+        WebElement nestedNode = parent.findElement(By.tagName("a"));
         assertTrue(NESTED_TEXT.equals(nestedNode.getText()));
     }
 
+    public void testFindElementsByTagName() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements = mDriver.findElements(By.tagName("a"));
+        assertTrue(elements.size() > 0);
+    }
+
+    public void testFindElementsByTagNameReturnsEmptyListIfNoResultsFound() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements = mDriver.findElements(
+                By.tagName(INEXISTENT));
+        assertEquals(0, elements.size());
+    }
+
+    public void testFindNestedElementsByTagName() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement div = mDriver.findElement(By.xpath(
+                "//div[@" + ID + "='divId']"));
+        List<WebElement> elements = div.findElements(By.tagName("span"));
+        assertEquals(1, elements.size());
+    }
+
     // By xpath
     public void testFindElementByXPath() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
-        WebElement element = mDriver.findElement(By.id(SOME_TEXT_ID));
+        WebElement element =
+                mDriver.findElement(By.xpath(
+                "//a[@" + ID + "=\"someTextId\"]"));
         assertTrue(SOME_TEXT.equals(element.getText()));
 
-        element = mDriver.findElement(By.id(DIV_ID));
+        element = mDriver.findElement(By.xpath("//div[@name='firstDiv']"));
         assertTrue(DIV_TEXT.equals(element.getText()));
     }
 
     public void testFindElementByXPathThrowsIfElementDoesNotExists() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         try {
-            mDriver.findElement(By.id(BAD_ID));
+            mDriver.findElement(By.xpath("//a[@" + ID + "='inexistant']"));
             fail("This should have failed.");
         } catch (WebElementNotFoundException e) {
             // This is expected
@@ -225,23 +455,140 @@
 
     public void testFindNestedElementByXPath() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
-        WebElement parent = mDriver.findElement(By.id(DIV_ID));
-        WebElement nestedNode = parent.findElement(By.id(NESTED_LINK_ID));
+        WebElement parent = mDriver.findElement(By.xpath(
+                "//div[@" + ID + "='divId']"));
+        WebElement nestedNode = parent.findElement(
+                By.xpath(".//a[@" + ID + "='nestedLinkId']"));
         assertTrue(NESTED_TEXT.equals(nestedNode.getText()));
     }
 
-    public void testGetTextThrowsIfElementIsStale() {
+    public void testFindElementsByXPath() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
-        WebElement div = mDriver.findElement(By.id(DIV_ID));
-        mDriver.get(mWebServer.getAssetUrl(HELLO_WORLD_URL));
+        List<WebElement> elements = mDriver.findElements(
+                By.xpath("//a[@name='foo']"));
+        assertTrue(elements.size() > 1);
+    }
+
+    public void testFindElementsByXPathReturnsEmptyListIfNoResultsFound() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements =
+                mDriver.findElements(By.xpath(
+                        "//a[@" + ID + "='inexistant']"));
+        assertEquals(0, elements.size());
+    }
+
+    public void testFindNestedElementsByXPath() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement div = mDriver.findElements(By.xpath(
+                "//div[@" + ID + "='divId']"))
+                .get(0);
+        List<WebElement> elements = div.findElements(
+                By.xpath(".//a[@name='foo']"));
+        assertEquals(1, elements.size());
+    }
+
+    public void testFindElementByXpathWithInvalidXPath() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         try {
-            div.getText();
+            mDriver.findElement(By.xpath("//a@" + ID + "=inexistant']"));
             fail("This should have failed.");
-        } catch (WebElementStaleException e) {
+        } catch (WebElementNotFoundException e) {
             // This is expected
         }
     }
 
+    // By className
+    public void testFindElementByClassName() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement element = mDriver.findElement(By.className(" spaceAround "));
+        assertTrue("Spaced out".equals(element.getText()));
+    }
+
+    public void testFindElementByClassNameThrowsIfElementDoesNotExists() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        try {
+            mDriver.findElement(By.className("bou"));
+            fail("This should have failed.");
+        } catch (WebElementNotFoundException e) {
+            // This is expected
+        }
+    }
+
+    public void testFindNestedElementByClassName() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement parent = mDriver.findElement(By.id(DIV_ID));
+        WebElement nestedNode = parent.findElement(By.className("divClass"));
+        assertTrue(NESTED_TEXT.equals(nestedNode.getText()));
+    }
+
+    public void testFindElementsByClassName() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements =
+                mDriver.findElements(By.className("divClass"));
+        assertTrue(elements.size() > 1);
+    }
+
+    public void testFindElementsByClassNameReturnsEmptyListIfNoResultsFound() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements =
+                mDriver.findElements(By.className(INEXISTENT));
+        assertEquals(0, elements.size());
+    }
+
+    public void testFindNestedElementsByClassName() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement parent = mDriver.findElement(By.id(DIV_ID));
+        List<WebElement> nested =
+                parent.findElements(By.className("divClass"));
+        assertTrue(nested.size() > 0);
+    }
+
+    // By css
+    public void testFindElementByCss() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement element = mDriver.findElement(By.css("#" + "outter"));
+        assertTrue("Foo".equals(element.getText()));
+    }
+
+    public void testFindElementByCssThrowsIfElementDoesNotExists() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        try {
+            mDriver.findElement(By.css("bou.foo"));
+            fail("This should have failed.");
+        } catch (WebElementNotFoundException e) {
+            // This is expected
+        }
+    }
+
+    public void testFindNestedElementByCss() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement parent = mDriver.findElement(By.id(DIV_ID));
+        WebElement nestedNode = parent.findElement(
+                By.css("#" + NESTED_LINK_ID));
+        assertTrue(NESTED_TEXT.equals(nestedNode.getText()));
+    }
+
+    public void testFindElementsByCss() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements = mDriver.findElements(
+                By.css("#" + SOME_TEXT_ID));
+        assertTrue(elements.size() > 0);
+    }
+
+    public void testFindElementsByCssReturnsEmptyListIfNoResultsFound() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        List<WebElement> elements = mDriver.findElements(By.css("bou.foo"));
+        assertEquals(0, elements.size());
+    }
+
+    public void testFindNestedElementsByCss() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        WebElement parent = mDriver.findElement(By.id(DIV_ID));
+        List<WebElement> nested = parent.findElements(
+                By.css("#" + NESTED_LINK_ID));
+        assertEquals(1, nested.size());
+    }
+
     public void testExecuteScriptShouldReturnAString() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         Object result = mDriver.executeScript("return document.title");
@@ -318,7 +665,7 @@
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         List<WebElement> result = (List<WebElement>) mDriver.executeScript(
                 "return document.getElementsByTagName('a')");
-        assertEquals(5, result.size());
+        assertTrue(result.size() > 1);
     }
 
     public void testExecuteScriptShouldReturnAMap() {
@@ -365,7 +712,8 @@
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
         WebElement div = mDriver.findElement(By.id(DIV_ID));
         Object result = mDriver.executeScript(
-                "arguments[0]['flibble'] = arguments[0].getAttribute('id');"
+                "arguments[0]['flibble'] = arguments[0].getAttribute('"
+                + ID + "');"
                 + "return arguments[0]['flibble'];", div);
         assertEquals(DIV_ID, (String) result);
     }
@@ -456,9 +804,9 @@
 
     public void testExecuteScriptShouldBeAbleToCreatePersistentValue() {
         mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
-        mDriver.executeScript("document.bidule = ['hello']");
+        mDriver.executeScript("document.b" + ID + "ule = ['hello']");
         Object result = mDriver.executeScript(
-                "return document.bidule.shift();");
+                "return document.b" + ID + "ule.shift();");
         assertEquals("hello", (String) result);
     }
 
@@ -478,4 +826,21 @@
                 "return \"f\\\"o\\\\o\\\\\\\\\\\"\" == arguments[0];",
                 "f\"o\\o\\\\\""));
     }
+
+    public void testExecuteScriptReturnsNull() {
+        mDriver.get(mWebServer.getAssetUrl(FORM_PAGE_URL));
+        Object result = mDriver.executeScript("return null;");
+        assertNull(result);
+        result = mDriver.executeScript("return undefined;");
+        assertNull(result);
+    }
+
+    public void testExecuteScriptShouldThrowIfNoPageIsLoaded() {
+        try {
+            Object result = mDriver.executeScript("return null;");
+            fail();
+        } catch (Exception e) {
+
+        }
+    }
 }