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