Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / content / public / android / javatests / src / org / chromium / content / browser / JavaBridgeBasicsTest.java
1 // Copyright 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package org.chromium.content.browser;
6
7 import android.test.suitebuilder.annotation.SmallTest;
8
9 import org.chromium.base.test.util.Feature;
10 import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
11 import org.chromium.content_shell_apk.ContentShellActivity;
12
13 import java.lang.annotation.Annotation;
14 import java.lang.annotation.ElementType;
15 import java.lang.annotation.Retention;
16 import java.lang.annotation.RetentionPolicy;
17 import java.lang.annotation.Target;
18 import java.lang.ref.WeakReference;
19
20 /**
21  * Part of the test suite for the Java Bridge. Tests a number of features including ...
22  * - The type of injected objects
23  * - The type of their methods
24  * - Replacing objects
25  * - Removing objects
26  * - Access control
27  * - Calling methods on returned objects
28  * - Multiply injected objects
29  * - Threading
30  * - Inheritance
31  */
32 public class JavaBridgeBasicsTest extends JavaBridgeTestBase {
33     private class TestController extends Controller {
34         private int mIntValue;
35         private long mLongValue;
36         private String mStringValue;
37         private boolean mBooleanValue;
38
39         public synchronized void setIntValue(int x) {
40             mIntValue = x;
41             notifyResultIsReady();
42         }
43         public synchronized void setLongValue(long x) {
44             mLongValue = x;
45             notifyResultIsReady();
46         }
47         public synchronized void setStringValue(String x) {
48             mStringValue = x;
49             notifyResultIsReady();
50         }
51         public synchronized void setBooleanValue(boolean x) {
52             mBooleanValue = x;
53             notifyResultIsReady();
54         }
55
56         public synchronized int waitForIntValue() {
57             waitForResult();
58             return mIntValue;
59         }
60         public synchronized long waitForLongValue() {
61             waitForResult();
62             return mLongValue;
63         }
64         public synchronized String waitForStringValue() {
65             waitForResult();
66             return mStringValue;
67         }
68         public synchronized boolean waitForBooleanValue() {
69             waitForResult();
70             return mBooleanValue;
71         }
72
73         public synchronized String getStringValue() {
74             return mStringValue;
75         }
76     }
77
78     private static class ObjectWithStaticMethod {
79         public static String staticMethod() {
80             return "foo";
81         }
82     }
83
84     TestController mTestController;
85
86     @Override
87     protected void setUp() throws Exception {
88         super.setUp();
89         mTestController = new TestController();
90         setUpContentView(mTestController, "testController");
91     }
92
93     @Override
94     protected ContentShellActivity launchContentShellWithUrl(String url) {
95         // Expose a global function "gc()" into pages.
96         return launchContentShellWithUrlAndCommandLineArgs(
97                 url, new String[]{ "--js-flags=--expose-gc" });
98     }
99
100     // Note that this requires that we can pass a JavaScript string to Java.
101     protected String executeJavaScriptAndGetStringResult(String script) throws Throwable {
102         executeJavaScript("testController.setStringValue(" + script + ");");
103         return mTestController.waitForStringValue();
104     }
105
106     protected void injectObjectAndReload(final Object object, final String name) throws Throwable {
107         injectObjectAndReload(object, name, null);
108     }
109
110     protected void injectObjectAndReload(final Object object, final String name,
111             final Class<? extends Annotation> requiredAnnotation) throws Throwable {
112         TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
113                 mTestCallbackHelperContainer.getOnPageFinishedHelper();
114         int currentCallCount = onPageFinishedHelper.getCallCount();
115         runTestOnUiThread(new Runnable() {
116             @Override
117             public void run() {
118                 getContentViewCore().addPossiblyUnsafeJavascriptInterface(object,
119                         name, requiredAnnotation);
120                 getContentViewCore().reload(true);
121             }
122         });
123         onPageFinishedHelper.waitForCallback(currentCallCount);
124     }
125
126     protected void synchronousPageReload() throws Throwable {
127         TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
128                 mTestCallbackHelperContainer.getOnPageFinishedHelper();
129         int currentCallCount = onPageFinishedHelper.getCallCount();
130         runTestOnUiThread(new Runnable() {
131             @Override
132             public void run() {
133                 getContentViewCore().reload(true);
134             }
135         });
136         onPageFinishedHelper.waitForCallback(currentCallCount);
137     }
138
139     // Note that this requires that we can pass a JavaScript boolean to Java.
140     private void assertRaisesException(String script) throws Throwable {
141         executeJavaScript("try {" +
142                           script + ";" +
143                           "  testController.setBooleanValue(false);" +
144                           "} catch (exception) {" +
145                           "  testController.setBooleanValue(true);" +
146                           "}");
147         assertTrue(mTestController.waitForBooleanValue());
148     }
149
150     @SmallTest
151     @Feature({"AndroidWebView", "Android-JavaBridge"})
152     public void testTypeOfInjectedObject() throws Throwable {
153         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
154     }
155
156     @SmallTest
157     @Feature({"AndroidWebView", "Android-JavaBridge"})
158     public void testAdditionNotReflectedUntilReload() throws Throwable {
159         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
160         runTestOnUiThread(new Runnable() {
161             @Override
162             public void run() {
163                 getContentViewCore().addPossiblyUnsafeJavascriptInterface(
164                         new Object(), "testObject", null);
165             }
166         });
167         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
168         synchronousPageReload();
169         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
170     }
171
172     @SmallTest
173     @Feature({"AndroidWebView", "Android-JavaBridge"})
174     public void testRemovalNotReflectedUntilReload() throws Throwable {
175         injectObjectAndReload(new Object(), "testObject");
176         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
177         runTestOnUiThread(new Runnable() {
178             @Override
179             public void run() {
180                 getContentViewCore().removeJavascriptInterface("testObject");
181             }
182         });
183         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
184         synchronousPageReload();
185         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
186     }
187
188     @SmallTest
189     @Feature({"AndroidWebView", "Android-JavaBridge"})
190     public void testRemoveObjectNotAdded() throws Throwable {
191         TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
192                 mTestCallbackHelperContainer.getOnPageFinishedHelper();
193         int currentCallCount = onPageFinishedHelper.getCallCount();
194         runTestOnUiThread(new Runnable() {
195             @Override
196             public void run() {
197                 getContentViewCore().removeJavascriptInterface("foo");
198                 getContentViewCore().reload(true);
199             }
200         });
201         onPageFinishedHelper.waitForCallback(currentCallCount);
202         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof foo"));
203     }
204
205     @SmallTest
206     @Feature({"AndroidWebView", "Android-JavaBridge"})
207     public void testTypeOfMethod() throws Throwable {
208         assertEquals("function",
209                 executeJavaScriptAndGetStringResult("typeof testController.setStringValue"));
210     }
211
212     @SmallTest
213     @Feature({"AndroidWebView", "Android-JavaBridge"})
214     public void testTypeOfInvalidMethod() throws Throwable {
215         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testController.foo"));
216     }
217
218     @SmallTest
219     @Feature({"AndroidWebView", "Android-JavaBridge"})
220     public void testCallingInvalidMethodRaisesException() throws Throwable {
221         assertRaisesException("testController.foo()");
222     }
223
224     @SmallTest
225     @Feature({"AndroidWebView", "Android-JavaBridge"})
226     public void testUncaughtJavaExceptionRaisesJavaScriptException() throws Throwable {
227         injectObjectAndReload(new Object() {
228             public void method() { throw new RuntimeException("foo"); }
229         }, "testObject");
230         assertRaisesException("testObject.method()");
231     }
232
233     // Note that this requires that we can pass a JavaScript string to Java.
234     @SmallTest
235     @Feature({"AndroidWebView", "Android-JavaBridge"})
236     public void testTypeOfStaticMethod() throws Throwable {
237         injectObjectAndReload(new ObjectWithStaticMethod(), "testObject");
238         executeJavaScript("testController.setStringValue(typeof testObject.staticMethod)");
239         assertEquals("function", mTestController.waitForStringValue());
240     }
241
242     // Note that this requires that we can pass a JavaScript string to Java.
243     @SmallTest
244     @Feature({"AndroidWebView", "Android-JavaBridge"})
245     public void testCallStaticMethod() throws Throwable {
246         injectObjectAndReload(new ObjectWithStaticMethod(), "testObject");
247         executeJavaScript("testController.setStringValue(testObject.staticMethod())");
248         assertEquals("foo", mTestController.waitForStringValue());
249     }
250
251     @SmallTest
252     @Feature({"AndroidWebView", "Android-JavaBridge"})
253     public void testPrivateMethodNotExposed() throws Throwable {
254         injectObjectAndReload(new Object() {
255             private void method() {}
256             protected void method2() {}
257         }, "testObject");
258         assertEquals("undefined",
259                 executeJavaScriptAndGetStringResult("typeof testObject.method"));
260         assertEquals("undefined",
261                 executeJavaScriptAndGetStringResult("typeof testObject.method2"));
262     }
263
264     @SmallTest
265     @Feature({"AndroidWebView", "Android-JavaBridge"})
266     public void testReplaceInjectedObject() throws Throwable {
267         injectObjectAndReload(new Object() {
268             public void method() { mTestController.setStringValue("object 1"); }
269         }, "testObject");
270         executeJavaScript("testObject.method()");
271         assertEquals("object 1", mTestController.waitForStringValue());
272
273         injectObjectAndReload(new Object() {
274             public void method() { mTestController.setStringValue("object 2"); }
275         }, "testObject");
276         executeJavaScript("testObject.method()");
277         assertEquals("object 2", mTestController.waitForStringValue());
278     }
279
280     @SmallTest
281     @Feature({"AndroidWebView", "Android-JavaBridge"})
282     public void testInjectNullObjectIsIgnored() throws Throwable {
283         injectObjectAndReload(null, "testObject");
284         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject"));
285     }
286
287     @SmallTest
288     @Feature({"AndroidWebView", "Android-JavaBridge"})
289     public void testReplaceInjectedObjectWithNullObjectIsIgnored() throws Throwable {
290         injectObjectAndReload(new Object(), "testObject");
291         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
292         injectObjectAndReload(null, "testObject");
293         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testObject"));
294     }
295
296     @SmallTest
297     @Feature({"AndroidWebView", "Android-JavaBridge"})
298     public void testCallOverloadedMethodWithDifferentNumberOfArguments() throws Throwable {
299         injectObjectAndReload(new Object() {
300             public void method() { mTestController.setStringValue("0 args"); }
301             public void method(int x) { mTestController.setStringValue("1 arg"); }
302             public void method(int x, int y) { mTestController.setStringValue("2 args"); }
303         }, "testObject");
304         executeJavaScript("testObject.method()");
305         assertEquals("0 args", mTestController.waitForStringValue());
306         executeJavaScript("testObject.method(42)");
307         assertEquals("1 arg", mTestController.waitForStringValue());
308         executeJavaScript("testObject.method(null)");
309         assertEquals("1 arg", mTestController.waitForStringValue());
310         executeJavaScript("testObject.method(undefined)");
311         assertEquals("1 arg", mTestController.waitForStringValue());
312         executeJavaScript("testObject.method(42, 42)");
313         assertEquals("2 args", mTestController.waitForStringValue());
314     }
315
316     @SmallTest
317     @Feature({"AndroidWebView", "Android-JavaBridge"})
318     public void testCallMethodWithWrongNumberOfArgumentsRaisesException() throws Throwable {
319         assertRaisesException("testController.setIntValue()");
320         assertRaisesException("testController.setIntValue(42, 42)");
321     }
322
323     @SmallTest
324     @Feature({"AndroidWebView", "Android-JavaBridge"})
325     public void testObjectPersistsAcrossPageLoads() throws Throwable {
326         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
327         synchronousPageReload();
328         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
329     }
330
331     @SmallTest
332     @Feature({"AndroidWebView", "Android-JavaBridge"})
333     public void testCustomPropertiesCleanedUpOnPageReloads() throws Throwable {
334         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
335         executeJavaScript("testController.myProperty = 42;");
336         assertEquals("42", executeJavaScriptAndGetStringResult("testController.myProperty"));
337         synchronousPageReload();
338         assertEquals("object", executeJavaScriptAndGetStringResult("typeof testController"));
339         assertEquals("undefined", executeJavaScriptAndGetStringResult("testController.myProperty"));
340     }
341
342     @SmallTest
343     @Feature({"AndroidWebView", "Android-JavaBridge"})
344     public void testSameObjectInjectedMultipleTimes() throws Throwable {
345         class TestObject {
346             private int mNumMethodInvocations;
347             public void method() { mTestController.setIntValue(++mNumMethodInvocations); }
348         }
349         final TestObject testObject = new TestObject();
350         TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
351                 mTestCallbackHelperContainer.getOnPageFinishedHelper();
352         int currentCallCount = onPageFinishedHelper.getCallCount();
353         runTestOnUiThread(new Runnable() {
354             @Override
355             public void run() {
356                 getContentViewCore().addPossiblyUnsafeJavascriptInterface(
357                         testObject, "testObject1", null);
358                 getContentViewCore().addPossiblyUnsafeJavascriptInterface(
359                         testObject, "testObject2", null);
360                 getContentViewCore().reload(true);
361             }
362         });
363         onPageFinishedHelper.waitForCallback(currentCallCount);
364         executeJavaScript("testObject1.method()");
365         assertEquals(1, mTestController.waitForIntValue());
366         executeJavaScript("testObject2.method()");
367         assertEquals(2, mTestController.waitForIntValue());
368     }
369
370     @SmallTest
371     @Feature({"AndroidWebView", "Android-JavaBridge"})
372     public void testCallMethodOnReturnedObject() throws Throwable {
373         injectObjectAndReload(new Object() {
374             public Object getInnerObject() {
375                 return new Object() {
376                     public void method(int x) { mTestController.setIntValue(x); }
377                 };
378             }
379         }, "testObject");
380         executeJavaScript("testObject.getInnerObject().method(42)");
381         assertEquals(42, mTestController.waitForIntValue());
382     }
383
384     @SmallTest
385     @Feature({"AndroidWebView", "Android-JavaBridge"})
386     public void testReturnedObjectInjectedElsewhere() throws Throwable {
387         class InnerObject {
388             private int mNumMethodInvocations;
389             public void method() { mTestController.setIntValue(++mNumMethodInvocations); }
390         }
391         final InnerObject innerObject = new InnerObject();
392         final Object object = new Object() {
393             public InnerObject getInnerObject() {
394                 return innerObject;
395             }
396         };
397         TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
398                 mTestCallbackHelperContainer.getOnPageFinishedHelper();
399         int currentCallCount = onPageFinishedHelper.getCallCount();
400         runTestOnUiThread(new Runnable() {
401             @Override
402             public void run() {
403                 getContentViewCore().addPossiblyUnsafeJavascriptInterface(
404                         object, "testObject", null);
405                 getContentViewCore().addPossiblyUnsafeJavascriptInterface(
406                         innerObject, "innerObject", null);
407                 getContentViewCore().reload(true);
408             }
409         });
410         onPageFinishedHelper.waitForCallback(currentCallCount);
411         executeJavaScript("testObject.getInnerObject().method()");
412         assertEquals(1, mTestController.waitForIntValue());
413         executeJavaScript("innerObject.method()");
414         assertEquals(2, mTestController.waitForIntValue());
415     }
416
417     // Verify that Java objects returned from bridge object methods are dereferenced
418     // on the Java side once they have been fully dereferenced on the JS side.
419     // Failing this test would mean that methods returning objects effectively create a memory
420     // leak.
421     @SmallTest
422     @Feature({"AndroidWebView", "Android-JavaBridge"})
423     public void testReturnedObjectIsGarbageCollected() throws Throwable {
424         // Make sure V8 exposes "gc" property on the global object (enabled with --expose-gc flag)
425         assertEquals("function", executeJavaScriptAndGetStringResult("typeof gc"));
426         class InnerObject {
427         }
428         class TestObject {
429             public InnerObject getInnerObject() {
430                 InnerObject inner = new InnerObject();
431                 mWeakRefForInner = new WeakReference<InnerObject>(inner);
432                 return inner;
433             }
434             // A weak reference is used to check InnerObject instance reachability.
435             WeakReference<InnerObject> mWeakRefForInner;
436         }
437         TestObject object = new TestObject();
438         injectObjectAndReload(object, "testObject");
439         // Initially, store a reference to the inner object in JS to make sure it's not
440         // garbage-collected prematurely.
441         assertEquals("object", executeJavaScriptAndGetStringResult(
442                         "(function() { " +
443                         "globalInner = testObject.getInnerObject(); return typeof globalInner; " +
444                         "})()"));
445         assertTrue(object.mWeakRefForInner.get() != null);
446         // Check that returned Java object is being held by the Java bridge, thus it's not
447         // collected.  Note that despite that what JavaDoc says about invoking "gc()", both Dalvik
448         // and ART actually run the collector.
449         Runtime.getRuntime().gc();
450         assertTrue(object.mWeakRefForInner.get() != null);
451         // Now dereference the inner object in JS and run GC to collect the interface object.
452         assertEquals("true", executeJavaScriptAndGetStringResult(
453                         "(function() { " +
454                         "delete globalInner; gc(); return (typeof globalInner == 'undefined'); " +
455                         "})()"));
456         // Force GC on the Java side again. The bridge had to release the inner object, so it must
457         // be collected this time.
458         Runtime.getRuntime().gc();
459         assertEquals(null, object.mWeakRefForInner.get());
460     }
461
462     @SmallTest
463     @Feature({"AndroidWebView", "Android-JavaBridge"})
464     public void testSameReturnedObjectUsesSameWrapper() throws Throwable {
465         class InnerObject {
466         }
467         final InnerObject innerObject = new InnerObject();
468         final Object injectedTestObject = new Object() {
469             public InnerObject getInnerObject() {
470                 return innerObject;
471             }
472         };
473         injectObjectAndReload(injectedTestObject, "injectedTestObject");
474         executeJavaScript("inner1 = injectedTestObject.getInnerObject()");
475         executeJavaScript("inner2 = injectedTestObject.getInnerObject()");
476         assertEquals("object", executeJavaScriptAndGetStringResult("typeof inner1"));
477         assertEquals("object", executeJavaScriptAndGetStringResult("typeof inner2"));
478         assertEquals("true", executeJavaScriptAndGetStringResult("inner1 === inner2"));
479     }
480
481     @SmallTest
482     @Feature({"AndroidWebView", "Android-JavaBridge"})
483     public void testMethodInvokedOnBackgroundThread() throws Throwable {
484         injectObjectAndReload(new Object() {
485             public void captureThreadId() {
486                 mTestController.setLongValue(Thread.currentThread().getId());
487             }
488         }, "testObject");
489         executeJavaScript("testObject.captureThreadId()");
490         final long threadId = mTestController.waitForLongValue();
491         assertFalse(threadId == Thread.currentThread().getId());
492         runTestOnUiThread(new Runnable() {
493             @Override
494             public void run() {
495                 assertFalse(threadId == Thread.currentThread().getId());
496             }
497         });
498     }
499
500     @SmallTest
501     @Feature({"AndroidWebView", "Android-JavaBridge"})
502     public void testPublicInheritedMethod() throws Throwable {
503         class Base {
504             public void method(int x) { mTestController.setIntValue(x); }
505         }
506         class Derived extends Base {
507         }
508         injectObjectAndReload(new Derived(), "testObject");
509         assertEquals("function", executeJavaScriptAndGetStringResult("typeof testObject.method"));
510         executeJavaScript("testObject.method(42)");
511         assertEquals(42, mTestController.waitForIntValue());
512     }
513
514     @SmallTest
515     @Feature({"AndroidWebView", "Android-JavaBridge"})
516     public void testPrivateInheritedMethod() throws Throwable {
517         class Base {
518             private void method() {}
519         }
520         class Derived extends Base {
521         }
522         injectObjectAndReload(new Derived(), "testObject");
523         assertEquals("undefined", executeJavaScriptAndGetStringResult("typeof testObject.method"));
524     }
525
526     @SmallTest
527     @Feature({"AndroidWebView", "Android-JavaBridge"})
528     public void testOverriddenMethod() throws Throwable {
529         class Base {
530             public void method() { mTestController.setStringValue("base"); }
531         }
532         class Derived extends Base {
533             @Override
534             public void method() { mTestController.setStringValue("derived"); }
535         }
536         injectObjectAndReload(new Derived(), "testObject");
537         executeJavaScript("testObject.method()");
538         assertEquals("derived", mTestController.waitForStringValue());
539     }
540
541     @SmallTest
542     @Feature({"AndroidWebView", "Android-JavaBridge"})
543     public void testEnumerateMembers() throws Throwable {
544         injectObjectAndReload(new Object() {
545             public void method() {}
546             private void privateMethod() {}
547             public int field;
548             private int privateField;
549         }, "testObject");
550         executeJavaScript(
551                 "var result = \"\"; " +
552                 "for (x in testObject) { result += \" \" + x } " +
553                 "testController.setStringValue(result);");
554         assertEquals(" equals getClass hashCode method notify notifyAll toString wait",
555                 mTestController.waitForStringValue());
556     }
557
558     @SmallTest
559     @Feature({"AndroidWebView", "Android-JavaBridge"})
560     public void testReflectPublicMethod() throws Throwable {
561         injectObjectAndReload(new Object() {
562             public Class<?> myGetClass() { return getClass(); }
563             public String method() { return "foo"; }
564         }, "testObject");
565         assertEquals("foo", executeJavaScriptAndGetStringResult(
566                 "testObject.myGetClass().getMethod('method', null).invoke(testObject, null)" +
567                 ".toString()"));
568     }
569
570     @SmallTest
571     @Feature({"AndroidWebView", "Android-JavaBridge"})
572     public void testReflectPublicField() throws Throwable {
573         injectObjectAndReload(new Object() {
574             public Class<?> myGetClass() { return getClass(); }
575             public String field = "foo";
576         }, "testObject");
577         assertEquals("foo", executeJavaScriptAndGetStringResult(
578                 "testObject.myGetClass().getField('field').get(testObject).toString()"));
579     }
580
581     @SmallTest
582     @Feature({"AndroidWebView", "Android-JavaBridge"})
583     public void testReflectPrivateMethodRaisesException() throws Throwable {
584         injectObjectAndReload(new Object() {
585             public Class<?> myGetClass() { return getClass(); }
586             private void method() {};
587         }, "testObject");
588         assertRaisesException("testObject.myGetClass().getMethod('method', null)");
589         // getDeclaredMethod() is able to access a private method, but invoke()
590         // throws a Java exception.
591         assertRaisesException(
592                 "testObject.myGetClass().getDeclaredMethod('method', null)." +
593                 "invoke(testObject, null)");
594     }
595
596     @SmallTest
597     @Feature({"AndroidWebView", "Android-JavaBridge"})
598     public void testReflectPrivateFieldRaisesException() throws Throwable {
599         injectObjectAndReload(new Object() {
600             public Class<?> myGetClass() { return getClass(); }
601             private int field;
602         }, "testObject");
603         assertRaisesException("testObject.myGetClass().getField('field')");
604         // getDeclaredField() is able to access a private field, but getInt()
605         // throws a Java exception.
606         assertRaisesException(
607                 "testObject.myGetClass().getDeclaredField('field').getInt(testObject)");
608     }
609
610     @SmallTest
611     @Feature({"AndroidWebView", "Android-JavaBridge"})
612     public void testAllowNonAnnotatedMethods() throws Throwable {
613         injectObjectAndReload(new Object() {
614             public String allowed() { return "foo"; }
615         }, "testObject", null);
616
617         // Test calling a method of an explicitly inherited class (Base#allowed()).
618         assertEquals("foo", executeJavaScriptAndGetStringResult("testObject.allowed()"));
619
620         // Test calling a method of an implicitly inherited class (Object#toString()).
621         assertEquals("string", executeJavaScriptAndGetStringResult("typeof testObject.toString()"));
622     }
623
624     @SmallTest
625     @Feature({"AndroidWebView", "Android-JavaBridge"})
626     public void testAllowOnlyAnnotatedMethods() throws Throwable {
627         injectObjectAndReload(new Object() {
628             @JavascriptInterface
629             public String allowed() { return "foo"; }
630
631             public String disallowed() { return "bar"; }
632         }, "testObject", JavascriptInterface.class);
633
634         // getClass() is an Object method and does not have the @JavascriptInterface annotation and
635         // should not be able to be called.
636         assertRaisesException("testObject.getClass()");
637         assertEquals("undefined", executeJavaScriptAndGetStringResult(
638                 "typeof testObject.getClass"));
639
640         // allowed() is marked with the @JavascriptInterface annotation and should be allowed to be
641         // called.
642         assertEquals("foo", executeJavaScriptAndGetStringResult("testObject.allowed()"));
643
644         // disallowed() is not marked with the @JavascriptInterface annotation and should not be
645         // able to be called.
646         assertRaisesException("testObject.disallowed()");
647         assertEquals("undefined", executeJavaScriptAndGetStringResult(
648                 "typeof testObject.disallowed"));
649     }
650
651     @SmallTest
652     @Feature({"AndroidWebView", "Android-JavaBridge"})
653     public void testAnnotationRequirementRetainsPropertyAcrossObjects() throws Throwable {
654         class Test {
655             @JavascriptInterface
656             public String safe() { return "foo"; }
657
658             public String unsafe() { return "bar"; }
659         }
660
661         class TestReturner {
662             @JavascriptInterface
663             public Test getTest() { return new Test(); }
664         }
665
666         // First test with safe mode off.
667         injectObjectAndReload(new TestReturner(), "unsafeTestObject", null);
668
669         // safe() should be able to be called regardless of whether or not we are in safe mode.
670         assertEquals("foo", executeJavaScriptAndGetStringResult(
671                 "unsafeTestObject.getTest().safe()"));
672         // unsafe() should be able to be called because we are not in safe mode.
673         assertEquals("bar", executeJavaScriptAndGetStringResult(
674                 "unsafeTestObject.getTest().unsafe()"));
675
676         // Now test with safe mode on.
677         injectObjectAndReload(new TestReturner(), "safeTestObject", JavascriptInterface.class);
678
679         // safe() should be able to be called regardless of whether or not we are in safe mode.
680         assertEquals("foo", executeJavaScriptAndGetStringResult(
681                 "safeTestObject.getTest().safe()"));
682         // unsafe() should not be able to be called because we are in safe mode.
683         assertRaisesException("safeTestObject.getTest().unsafe()");
684         assertEquals("undefined", executeJavaScriptAndGetStringResult(
685                 "typeof safeTestObject.getTest().unsafe"));
686         // getClass() is an Object method and does not have the @JavascriptInterface annotation and
687         // should not be able to be called.
688         assertRaisesException("safeTestObject.getTest().getClass()");
689         assertEquals("undefined", executeJavaScriptAndGetStringResult(
690                 "typeof safeTestObject.getTest().getClass"));
691     }
692
693     @SmallTest
694     @Feature({"AndroidWebView", "Android-JavaBridge"})
695     public void testAnnotationDoesNotGetInherited() throws Throwable {
696         class Base {
697             @JavascriptInterface
698             public void base() { }
699         }
700
701         class Child extends Base {
702             @Override
703             public void base() { }
704         }
705
706         injectObjectAndReload(new Child(), "testObject", JavascriptInterface.class);
707
708         // base() is inherited.  The inherited method does not have the @JavascriptInterface
709         // annotation and should not be able to be called.
710         assertRaisesException("testObject.base()");
711         assertEquals("undefined", executeJavaScriptAndGetStringResult(
712                 "typeof testObject.base"));
713     }
714
715     @SuppressWarnings("javadoc")
716     @Retention(RetentionPolicy.RUNTIME)
717     @Target({ElementType.METHOD})
718     @interface TestAnnotation {
719     }
720
721     @SmallTest
722     @Feature({"AndroidWebView", "Android-JavaBridge"})
723     public void testCustomAnnotationRestriction() throws Throwable {
724         class Test {
725             @TestAnnotation
726             public String checkTestAnnotationFoo() { return "bar"; }
727
728             @JavascriptInterface
729             public String checkJavascriptInterfaceFoo() { return "bar"; }
730         }
731
732         // Inject javascriptInterfaceObj and require the JavascriptInterface annotation.
733         injectObjectAndReload(new Test(), "javascriptInterfaceObj", JavascriptInterface.class);
734
735         // Test#testAnnotationFoo() should fail, as it isn't annotated with JavascriptInterface.
736         assertRaisesException("javascriptInterfaceObj.checkTestAnnotationFoo()");
737         assertEquals("undefined", executeJavaScriptAndGetStringResult(
738                 "typeof javascriptInterfaceObj.checkTestAnnotationFoo"));
739
740         // Test#javascriptInterfaceFoo() should pass, as it is annotated with JavascriptInterface.
741         assertEquals("bar", executeJavaScriptAndGetStringResult(
742                 "javascriptInterfaceObj.checkJavascriptInterfaceFoo()"));
743
744         // Inject testAnnotationObj and require the TestAnnotation annotation.
745         injectObjectAndReload(new Test(), "testAnnotationObj", TestAnnotation.class);
746
747         // Test#testAnnotationFoo() should pass, as it is annotated with TestAnnotation.
748         assertEquals("bar", executeJavaScriptAndGetStringResult(
749                 "testAnnotationObj.checkTestAnnotationFoo()"));
750
751         // Test#javascriptInterfaceFoo() should fail, as it isn't annotated with TestAnnotation.
752         assertRaisesException("testAnnotationObj.checkJavascriptInterfaceFoo()");
753         assertEquals("undefined", executeJavaScriptAndGetStringResult(
754                 "typeof testAnnotationObj.checkJavascriptInterfaceFoo"));
755     }
756
757     @SmallTest
758     @Feature({"AndroidWebView", "Android-JavaBridge"})
759     public void testAddJavascriptInterfaceIsSafeByDefault() throws Throwable {
760         class Test {
761             public String blocked() { return "bar"; }
762
763             @JavascriptInterface
764             public String allowed() { return "bar"; }
765         }
766
767         // Manually inject the Test object, making sure to use the
768         // ContentViewCore#addJavascriptInterface, not the possibly unsafe version.
769         TestCallbackHelperContainer.OnPageFinishedHelper onPageFinishedHelper =
770                 mTestCallbackHelperContainer.getOnPageFinishedHelper();
771         int currentCallCount = onPageFinishedHelper.getCallCount();
772         runTestOnUiThread(new Runnable() {
773             @Override
774             public void run() {
775                 getContentViewCore().addJavascriptInterface(new Test(),
776                         "testObject");
777                 getContentViewCore().reload(true);
778             }
779         });
780         onPageFinishedHelper.waitForCallback(currentCallCount);
781
782         // Test#allowed() should pass, as it is annotated with JavascriptInterface.
783         assertEquals("bar", executeJavaScriptAndGetStringResult(
784                 "testObject.allowed()"));
785
786         // Test#blocked() should fail, as it isn't annotated with JavascriptInterface.
787         assertRaisesException("testObject.blocked()");
788         assertEquals("undefined", executeJavaScriptAndGetStringResult(
789                 "typeof testObject.blocked"));
790     }
791
792     @SmallTest
793     @Feature({"AndroidWebView", "Android-JavaBridge"})
794     public void testObjectsInspection() throws Throwable {
795         class Test {
796             @JavascriptInterface
797             public String m1() { return "foo"; }
798
799             @JavascriptInterface
800             public String m2() { return "bar"; }
801
802             @JavascriptInterface
803             public String m2(int x) { return "bar " + x; }
804         }
805
806         final String jsObjectKeysTestTemplate = "Object.keys(%s).toString()";
807         final String jsForInTestTemplate =
808                 "(function(){" +
809                 "  var s=[]; for(var m in %s) s.push(m); return s.join(\",\")" +
810                 "})()";
811         final String inspectableObjectName = "testObj1";
812         final String nonInspectableObjectName = "testObj2";
813
814         // Inspection is enabled by default.
815         injectObjectAndReload(new Test(), inspectableObjectName, JavascriptInterface.class);
816
817         assertEquals("m1,m2", executeJavaScriptAndGetStringResult(
818                         String.format(jsObjectKeysTestTemplate, inspectableObjectName)));
819         assertEquals("m1,m2", executeJavaScriptAndGetStringResult(
820                         String.format(jsForInTestTemplate, inspectableObjectName)));
821
822         runTestOnUiThread(new Runnable() {
823             @Override
824             public void run() {
825                 getContentViewCore().setAllowJavascriptInterfacesInspection(false);
826             }
827         });
828
829         injectObjectAndReload(new Test(), nonInspectableObjectName, JavascriptInterface.class);
830
831         assertEquals("", executeJavaScriptAndGetStringResult(
832                         String.format(jsObjectKeysTestTemplate, nonInspectableObjectName)));
833         assertEquals("", executeJavaScriptAndGetStringResult(
834                         String.format(jsForInTestTemplate, nonInspectableObjectName)));
835     }
836
837     @SmallTest
838     @Feature({"AndroidWebView", "Android-JavaBridge"})
839     public void testAccessToObjectGetClassIsBlocked() throws Throwable {
840         injectObjectAndReload(new Object(), "testObject");
841         assertEquals("function", executeJavaScriptAndGetStringResult("typeof testObject.getClass"));
842         assertRaisesException("testObject.getClass()");
843     }
844 }