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
5package org.chromium.android_webview.test;
6
7import android.os.Handler;
8import android.os.Message;
9import android.test.suitebuilder.annotation.LargeTest;
10import android.test.suitebuilder.annotation.SmallTest;
11import android.view.KeyEvent;
12import android.webkit.WebView.HitTestResult;
13
14import org.chromium.android_webview.AwContents;
15import org.chromium.android_webview.test.util.AwTestTouchUtils;
16import org.chromium.android_webview.test.util.CommonResources;
17import org.chromium.base.ThreadUtils;
18import org.chromium.base.test.util.Feature;
19import org.chromium.net.test.util.TestWebServer;
20
21import java.util.concurrent.Callable;
22
23/**
24 * Test for getHitTestResult, requestFocusNodeHref, and requestImageRef methods
25 */
26public class WebKitHitTestTest extends AwTestBase {
27    private TestAwContentsClient mContentsClient;
28    private AwTestContainerView mTestView;
29    private AwContents mAwContents;
30    private TestWebServer mWebServer;
31
32    private static final String HREF = "http://foo/";
33    private static final String ANCHOR_TEXT = "anchor text";
34
35    @Override
36    public void setUp() throws Exception {
37        super.setUp();
38        mContentsClient = new TestAwContentsClient();
39        mTestView = createAwTestContainerViewOnMainSync(mContentsClient);
40        mAwContents = mTestView.getAwContents();
41        mWebServer = new TestWebServer(false);
42    }
43
44    @Override
45    public void tearDown() throws Exception {
46        if (mWebServer != null) {
47            mWebServer.shutdown();
48        }
49        super.tearDown();
50    }
51
52    private void setServerResponseAndLoad(String response) throws Throwable {
53        String url = mWebServer.setResponse("/hittest.html", response, null);
54        loadUrlSync(mAwContents,
55                    mContentsClient.getOnPageFinishedHelper(),
56                    url);
57    }
58
59    private static String fullPageLink(String href, String anchorText) {
60        return CommonResources.makeHtmlPageFrom("", "<a class=\"full_view\" href=\"" +
61                href + "\" " + "onclick=\"return false;\">" + anchorText + "</a>");
62    }
63
64    private void simulateTabDownUpOnUiThread() throws Throwable {
65        runTestOnUiThread(new Runnable() {
66            @Override
67            public void run() {
68                mAwContents.getContentViewCore().dispatchKeyEvent(
69                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_TAB));
70                mAwContents.getContentViewCore().dispatchKeyEvent(
71                        new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_TAB));
72            }
73        });
74    }
75
76    private void simulateInput(boolean byTouch) throws Throwable {
77        // Send a touch click event if byTouch is true. Otherwise, send a TAB
78        // key event to change the focused element of the page.
79        if (byTouch) {
80            AwTestTouchUtils.simulateTouchCenterOfView(mTestView);
81        } else {
82            simulateTabDownUpOnUiThread();
83        }
84    }
85
86    private static boolean stringEquals(String a, String b) {
87        return a == null ? b == null : a.equals(b);
88    }
89
90    private void pollForHitTestDataOnUiThread(
91            final int expectedType, final String expectedExtra) throws Throwable {
92        pollOnUiThread(new Callable<Boolean>() {
93            @Override
94            public Boolean call() {
95                AwContents.HitTestData data = mAwContents.getLastHitTestResult();
96                return expectedType == data.hitTestResultType &&
97                       stringEquals(expectedExtra, data.hitTestResultExtraData);
98            }
99        });
100    }
101
102    private void pollForHrefAndImageSrcOnUiThread(
103            final String expectedHref,
104            final String expectedAnchorText,
105            final String expectedImageSrc) throws Throwable {
106        pollOnUiThread(new Callable<Boolean>() {
107            @Override
108            public Boolean call() {
109                AwContents.HitTestData data = mAwContents.getLastHitTestResult();
110                return stringEquals(expectedHref, data.href) &&
111                       stringEquals(expectedAnchorText, data.anchorText) &&
112                       stringEquals(expectedImageSrc, data.imgSrc);
113            }
114        });
115
116        Handler dummyHandler = new Handler();
117        final Message focusNodeHrefMsg = dummyHandler.obtainMessage();
118        final Message imageRefMsg = dummyHandler.obtainMessage();
119        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
120            @Override
121            public void run() {
122                mAwContents.requestFocusNodeHref(focusNodeHrefMsg);
123                mAwContents.requestImageRef(imageRefMsg);
124            }
125        });
126        assertEquals(expectedHref, focusNodeHrefMsg.getData().getString("url"));
127        assertEquals(expectedAnchorText, focusNodeHrefMsg.getData().getString("title"));
128        assertEquals(expectedImageSrc, focusNodeHrefMsg.getData().getString("src"));
129        assertEquals(expectedImageSrc, imageRefMsg.getData().getString("url"));
130    }
131
132    private void srcAnchorTypeTestBody(boolean byTouch) throws Throwable {
133        String page = fullPageLink(HREF, ANCHOR_TEXT);
134        setServerResponseAndLoad(page);
135        simulateInput(byTouch);
136        pollForHitTestDataOnUiThread(HitTestResult.SRC_ANCHOR_TYPE, HREF);
137        pollForHrefAndImageSrcOnUiThread(HREF, ANCHOR_TEXT, null);
138    }
139
140    @SmallTest
141    @Feature({"AndroidWebView", "WebKitHitTest"})
142    public void testSrcAnchorType() throws Throwable {
143        srcAnchorTypeTestBody(true);
144    }
145
146    @SmallTest
147    @Feature({"AndroidWebView", "WebKitHitTest"})
148    public void testSrcAnchorTypeByFocus() throws Throwable {
149        srcAnchorTypeTestBody(false);
150    }
151
152    private void blankHrefTestBody(boolean byTouch) throws Throwable {
153        String fullPath = mWebServer.getResponseUrl("/hittest.html");
154        String page = fullPageLink("", ANCHOR_TEXT);
155        setServerResponseAndLoad(page);
156        simulateInput(byTouch);
157        pollForHitTestDataOnUiThread(HitTestResult.SRC_ANCHOR_TYPE, fullPath);
158        pollForHrefAndImageSrcOnUiThread(fullPath, ANCHOR_TEXT, null);
159    }
160
161    @SmallTest
162    @Feature({"AndroidWebView", "WebKitHitTest"})
163    public void testSrcAnchorTypeBlankHref() throws Throwable {
164        blankHrefTestBody(true);
165    }
166
167    @SmallTest
168    @Feature({"AndroidWebView", "WebKitHitTest"})
169    public void testSrcAnchorTypeBlankHrefByFocus() throws Throwable {
170        blankHrefTestBody(false);
171    }
172
173    private void srcAnchorTypeRelativeUrlTestBody(boolean byTouch) throws Throwable {
174        String relPath = "/foo.html";
175        String fullPath = mWebServer.getResponseUrl(relPath);
176        String page = fullPageLink(relPath, ANCHOR_TEXT);
177        setServerResponseAndLoad(page);
178        simulateInput(byTouch);
179        pollForHitTestDataOnUiThread(HitTestResult.SRC_ANCHOR_TYPE, fullPath);
180        pollForHrefAndImageSrcOnUiThread(fullPath, ANCHOR_TEXT, null);
181    }
182
183    @SmallTest
184    @Feature({"AndroidWebView", "WebKitHitTest"})
185    public void testSrcAnchorTypeRelativeUrl() throws Throwable {
186        srcAnchorTypeRelativeUrlTestBody(true);
187    }
188
189    @SmallTest
190    @Feature({"AndroidWebView", "WebKitHitTest"})
191    public void testSrcAnchorTypeRelativeUrlByFocus() throws Throwable {
192        srcAnchorTypeRelativeUrlTestBody(false);
193    }
194
195    private void srcEmailTypeTestBody(boolean byTouch) throws Throwable {
196        String email = "foo@bar.com";
197        String prefix = "mailto:";
198        String page = fullPageLink(prefix + email, ANCHOR_TEXT);
199        setServerResponseAndLoad(page);
200        simulateInput(byTouch);
201        pollForHitTestDataOnUiThread(HitTestResult.EMAIL_TYPE, email);
202        pollForHrefAndImageSrcOnUiThread(prefix + email, ANCHOR_TEXT, null);
203    }
204
205    @SmallTest
206    @Feature({"AndroidWebView", "WebKitHitTest"})
207    public void testSrcEmailType() throws Throwable {
208        srcEmailTypeTestBody(true);
209    }
210
211    @SmallTest
212    @Feature({"AndroidWebView", "WebKitHitTest"})
213    public void testSrcEmailTypeByFocus() throws Throwable {
214        srcEmailTypeTestBody(false);
215    }
216
217    private void srcGeoTypeTestBody(boolean byTouch) throws Throwable {
218        String location = "Jilin";
219        String prefix = "geo:0,0?q=";
220        String page = fullPageLink(prefix + location, ANCHOR_TEXT);
221        setServerResponseAndLoad(page);
222        simulateInput(byTouch);
223        pollForHitTestDataOnUiThread(HitTestResult.GEO_TYPE, location);
224        pollForHrefAndImageSrcOnUiThread(prefix + location, ANCHOR_TEXT, null);
225    }
226
227    @SmallTest
228    @Feature({"AndroidWebView", "WebKitHitTest"})
229    public void testSrcGeoType() throws Throwable {
230        srcGeoTypeTestBody(true);
231    }
232
233    @SmallTest
234    @Feature({"AndroidWebView", "WebKitHitTest"})
235    public void testSrcGeoTypeByFocus() throws Throwable {
236        srcGeoTypeTestBody(false);
237    }
238
239    private void srcPhoneTypeTestBody(boolean byTouch) throws Throwable {
240        String phone_num = "%2B1234567890";
241        String expected_phone_num = "+1234567890";
242        String prefix = "tel:";
243        String page = fullPageLink("tel:" + phone_num, ANCHOR_TEXT);
244        setServerResponseAndLoad(page);
245        simulateInput(byTouch);
246        pollForHitTestDataOnUiThread(HitTestResult.PHONE_TYPE, expected_phone_num);
247        pollForHrefAndImageSrcOnUiThread(prefix + phone_num, ANCHOR_TEXT, null);
248    }
249
250    @SmallTest
251    @Feature({"AndroidWebView", "WebKitHitTest"})
252    public void testSrcPhoneType() throws Throwable {
253        srcPhoneTypeTestBody(true);
254    }
255
256    @SmallTest
257    @Feature({"AndroidWebView", "WebKitHitTest"})
258    public void testSrcPhoneTypeByFocus() throws Throwable {
259        srcPhoneTypeTestBody(false);
260    }
261
262    private void srcImgeAnchorTypeTestBody(boolean byTouch) throws Throwable {
263        String fullImageSrc = "http://foo.bar/nonexistent.jpg";
264        String page = CommonResources.makeHtmlPageFrom("", "<a class=\"full_view\" href=\"" +
265                HREF + "\"onclick=\"return false;\"><img class=\"full_view\" src=\"" +
266                fullImageSrc + "\"></a>");
267        setServerResponseAndLoad(page);
268        simulateInput(byTouch);
269        pollForHitTestDataOnUiThread(HitTestResult.SRC_IMAGE_ANCHOR_TYPE, fullImageSrc);
270        pollForHrefAndImageSrcOnUiThread(HREF, null, fullImageSrc);
271    }
272
273    @SmallTest
274    @Feature({"AndroidWebView", "WebKitHitTest"})
275    public void testSrcImgeAnchorType() throws Throwable {
276        srcImgeAnchorTypeTestBody(true);
277    }
278
279    @SmallTest
280    @Feature({"AndroidWebView", "WebKitHitTest"})
281    public void testSrcImgeAnchorTypeByFocus() throws Throwable {
282        srcImgeAnchorTypeTestBody(false);
283    }
284
285    private void srcImgeAnchorTypeRelativeUrlTestBody(boolean byTouch) throws Throwable {
286        String relImageSrc = "/nonexistent.jpg";
287        String fullImageSrc = mWebServer.getResponseUrl(relImageSrc);
288        String relPath = "/foo.html";
289        String fullPath = mWebServer.getResponseUrl(relPath);
290        String page = CommonResources.makeHtmlPageFrom("", "<a class=\"full_view\" href=\"" +
291                relPath + "\"onclick=\"return false;\"><img class=\"full_view\" src=\"" +
292                relImageSrc + "\"></a>");
293        setServerResponseAndLoad(page);
294        simulateInput(byTouch);
295        pollForHitTestDataOnUiThread(HitTestResult.SRC_IMAGE_ANCHOR_TYPE, fullImageSrc);
296        pollForHrefAndImageSrcOnUiThread(fullPath, null, fullImageSrc);
297    }
298
299    @SmallTest
300    @Feature({"AndroidWebView", "WebKitHitTest"})
301    public void testSrcImgeAnchorTypeRelativeUrl() throws Throwable {
302        srcImgeAnchorTypeRelativeUrlTestBody(true);
303    }
304
305    @SmallTest
306    @Feature({"AndroidWebView", "WebKitHitTest"})
307    public void testSrcImgeAnchorTypeRelativeUrlByFocus() throws Throwable {
308        srcImgeAnchorTypeRelativeUrlTestBody(false);
309    }
310
311    @SmallTest
312    @Feature({"AndroidWebView", "WebKitHitTest"})
313    public void testImgeType() throws Throwable {
314        String relImageSrc = "/nonexistent2.jpg";
315        String fullImageSrc = mWebServer.getResponseUrl(relImageSrc);
316        String page = CommonResources.makeHtmlPageFrom("",
317                "<img class=\"full_view\" src=\"" + relImageSrc + "\">");
318        setServerResponseAndLoad(page);
319        AwTestTouchUtils.simulateTouchCenterOfView(mTestView);
320        pollForHitTestDataOnUiThread(HitTestResult.IMAGE_TYPE, fullImageSrc);
321        pollForHrefAndImageSrcOnUiThread(null, null, fullImageSrc);
322    }
323
324    private void editTextTypeTestBody(boolean byTouch) throws Throwable {
325        String page = CommonResources.makeHtmlPageFrom("",
326                "<form><input class=\"full_view\" type=\"text\" name=\"test\"></form>");
327        setServerResponseAndLoad(page);
328        simulateInput(byTouch);
329        pollForHitTestDataOnUiThread(HitTestResult.EDIT_TEXT_TYPE, null);
330        pollForHrefAndImageSrcOnUiThread(null, null, null);
331    }
332
333    @SmallTest
334    @Feature({"AndroidWebView", "WebKitHitTest"})
335    public void testEditTextType() throws Throwable {
336        editTextTypeTestBody(true);
337    }
338
339    @SmallTest
340    @Feature({"AndroidWebView", "WebKitHitTest"})
341    public void testEditTextTypeByFocus() throws Throwable {
342        editTextTypeTestBody(false);
343    }
344
345    public void unknownTypeJavascriptSchemeTestBody(boolean byTouch) throws Throwable {
346        // Per documentation, javascript urls are special.
347        String javascript = "javascript:alert('foo');";
348        String page = fullPageLink(javascript, ANCHOR_TEXT);
349        setServerResponseAndLoad(page);
350        simulateInput(byTouch);
351        pollForHrefAndImageSrcOnUiThread(javascript, ANCHOR_TEXT, null);
352        pollForHitTestDataOnUiThread(HitTestResult.UNKNOWN_TYPE, null);
353    }
354
355    @SmallTest
356    @Feature({"AndroidWebView", "WebKitHitTest"})
357    public void testUnknownTypeJavascriptScheme() throws Throwable {
358        unknownTypeJavascriptSchemeTestBody(true);
359    }
360
361    @SmallTest
362    @Feature({"AndroidWebView", "WebKitHitTest"})
363    public void testUnknownTypeJavascriptSchemeByFocus() throws Throwable {
364        unknownTypeJavascriptSchemeTestBody(false);
365    }
366
367    @SmallTest
368    @Feature({"AndroidWebView", "WebKitHitTest"})
369    public void testUnknownTypeUnrecognizedNode() throws Throwable {
370        // Since UNKNOWN_TYPE is the default, hit test another type first for
371        // this test to be valid.
372        testSrcAnchorType();
373
374        final String title = "UNKNOWN_TYPE title";
375
376        String page = CommonResources.makeHtmlPageFrom(
377                "<title>" + title + "</title>",
378                "<div class=\"full_view\">div text</div>");
379        setServerResponseAndLoad(page);
380
381        // Wait for the new page to be loaded before trying hit test.
382        pollOnUiThread(new Callable<Boolean>() {
383            @Override
384            public Boolean call() {
385                return mAwContents.getTitle().equals(title);
386            }
387        });
388        AwTestTouchUtils.simulateTouchCenterOfView(mTestView);
389        pollForHitTestDataOnUiThread(HitTestResult.UNKNOWN_TYPE, null);
390    }
391
392    @LargeTest
393    @Feature({"AndroidWebView", "WebKitHitTest"})
394    public void testUnfocusedNodeAndTouchRace() throws Throwable {
395        // Test when the touch and focus paths racing with setting different
396        // results.
397
398        String relImageSrc = "/nonexistent3.jpg";
399        String fullImageSrc = mWebServer.getResponseUrl(relImageSrc);
400        String html = CommonResources.makeHtmlPageFrom(
401                "<meta name=\"viewport\" content=\"width=device-width,height=device-height\" />" +
402                        "<style type=\"text/css\">" +
403                        ".full_width { width:100%; position:absolute; }" +
404                        "</style>",
405                        "<form><input class=\"full_width\" style=\"height:25%;\" " +
406                        "type=\"text\" name=\"test\"></form>" +
407                        "<img class=\"full_width\" style=\"height:50%;top:25%;\" " +
408                        "src=\"" + relImageSrc + "\">");
409        setServerResponseAndLoad(html);
410
411        // Focus on input element and check the hit test results.
412        simulateTabDownUpOnUiThread();
413        pollForHitTestDataOnUiThread(HitTestResult.EDIT_TEXT_TYPE, null);
414        pollForHrefAndImageSrcOnUiThread(null, null, null);
415
416        // Touch image. Now the focus based hit test path will try to null out
417        // the results and the touch based path will update with the result of
418        // the image.
419        AwTestTouchUtils.simulateTouchCenterOfView(mTestView);
420
421        // Make sure the result of image sticks.
422        for (int i = 0; i < 2; ++i) {
423            Thread.sleep(500);
424            pollForHitTestDataOnUiThread(HitTestResult.IMAGE_TYPE, fullImageSrc);
425            pollForHrefAndImageSrcOnUiThread(null, null, fullImageSrc);
426        }
427    }
428}
429