1// Copyright 2014 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#include "config.h"
6
7#include "web/TextFinder.h"
8
9#include "bindings/core/v8/ExceptionStatePlaceholder.h"
10#include "core/dom/Document.h"
11#include "core/dom/NodeList.h"
12#include "core/dom/Range.h"
13#include "core/dom/shadow/ShadowRoot.h"
14#include "core/html/HTMLElement.h"
15#include "public/platform/Platform.h"
16#include "public/web/WebDocument.h"
17#include "web/FindInPageCoordinates.h"
18#include "web/WebLocalFrameImpl.h"
19#include "web/tests/FrameTestHelpers.h"
20#include "wtf/OwnPtr.h"
21#include <gtest/gtest.h>
22
23using namespace blink;
24
25namespace {
26
27class TextFinderTest : public ::testing::Test {
28protected:
29    virtual void SetUp() OVERRIDE;
30
31    Document& document() const;
32    TextFinder& textFinder() const;
33
34    static WebFloatRect findInPageRect(Node* startContainer, int startOffset, Node* endContainer, int endOffset);
35
36private:
37    FrameTestHelpers::WebViewHelper m_webViewHelper;
38    RefPtrWillBePersistent<Document> m_document;
39    TextFinder* m_textFinder;
40};
41
42void TextFinderTest::SetUp()
43{
44    m_webViewHelper.initialize();
45    WebLocalFrameImpl& frameImpl = *m_webViewHelper.webViewImpl()->mainFrameImpl();
46    frameImpl.viewImpl()->resize(WebSize(640, 480));
47    m_document = PassRefPtrWillBeRawPtr<Document>(frameImpl.document());
48    m_textFinder = &frameImpl.ensureTextFinder();
49}
50
51Document& TextFinderTest::document() const
52{
53    return *m_document;
54}
55
56TextFinder& TextFinderTest::textFinder() const
57{
58    return *m_textFinder;
59}
60
61WebFloatRect TextFinderTest::findInPageRect(Node* startContainer, int startOffset, Node* endContainer, int endOffset)
62{
63    RefPtrWillBeRawPtr<Range> range = Range::create(startContainer->document(), startContainer, startOffset, endContainer, endOffset);
64    return WebFloatRect(findInPageRectFromRange(range.get()));
65}
66
67TEST_F(TextFinderTest, FindTextSimple)
68{
69    document().body()->setInnerHTML("XXXXFindMeYYYYfindmeZZZZ", ASSERT_NO_EXCEPTION);
70    Node* textNode = document().body()->firstChild();
71
72    int identifier = 0;
73    WebString searchText(String("FindMe"));
74    WebFindOptions findOptions; // Default.
75    bool wrapWithinFrame = true;
76    WebRect* selectionRect = 0;
77
78    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
79    Range* activeMatch = textFinder().activeMatch();
80    ASSERT_TRUE(activeMatch);
81    EXPECT_EQ(textNode, activeMatch->startContainer());
82    EXPECT_EQ(4, activeMatch->startOffset());
83    EXPECT_EQ(textNode, activeMatch->endContainer());
84    EXPECT_EQ(10, activeMatch->endOffset());
85
86    findOptions.findNext = true;
87    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
88    activeMatch = textFinder().activeMatch();
89    ASSERT_TRUE(activeMatch);
90    EXPECT_EQ(textNode, activeMatch->startContainer());
91    EXPECT_EQ(14, activeMatch->startOffset());
92    EXPECT_EQ(textNode, activeMatch->endContainer());
93    EXPECT_EQ(20, activeMatch->endOffset());
94
95    // Should wrap to the first match.
96    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
97    activeMatch = textFinder().activeMatch();
98    ASSERT_TRUE(activeMatch);
99    EXPECT_EQ(textNode, activeMatch->startContainer());
100    EXPECT_EQ(4, activeMatch->startOffset());
101    EXPECT_EQ(textNode, activeMatch->endContainer());
102    EXPECT_EQ(10, activeMatch->endOffset());
103
104    // Search in the reverse order.
105    identifier = 1;
106    findOptions = WebFindOptions();
107    findOptions.forward = false;
108
109    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
110    activeMatch = textFinder().activeMatch();
111    ASSERT_TRUE(activeMatch);
112    EXPECT_EQ(textNode, activeMatch->startContainer());
113    EXPECT_EQ(14, activeMatch->startOffset());
114    EXPECT_EQ(textNode, activeMatch->endContainer());
115    EXPECT_EQ(20, activeMatch->endOffset());
116
117    findOptions.findNext = true;
118    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
119    activeMatch = textFinder().activeMatch();
120    ASSERT_TRUE(activeMatch);
121    EXPECT_EQ(textNode, activeMatch->startContainer());
122    EXPECT_EQ(4, activeMatch->startOffset());
123    EXPECT_EQ(textNode, activeMatch->endContainer());
124    EXPECT_EQ(10, activeMatch->endOffset());
125
126    // Wrap to the first match (last occurence in the document).
127    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
128    activeMatch = textFinder().activeMatch();
129    ASSERT_TRUE(activeMatch);
130    EXPECT_EQ(textNode, activeMatch->startContainer());
131    EXPECT_EQ(14, activeMatch->startOffset());
132    EXPECT_EQ(textNode, activeMatch->endContainer());
133    EXPECT_EQ(20, activeMatch->endOffset());
134}
135
136TEST_F(TextFinderTest, FindTextNotFound)
137{
138    document().body()->setInnerHTML("XXXXFindMeYYYYfindmeZZZZ", ASSERT_NO_EXCEPTION);
139
140    int identifier = 0;
141    WebString searchText(String("Boo"));
142    WebFindOptions findOptions; // Default.
143    bool wrapWithinFrame = true;
144    WebRect* selectionRect = 0;
145
146    EXPECT_FALSE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
147    EXPECT_FALSE(textFinder().activeMatch());
148}
149
150TEST_F(TextFinderTest, FindTextInShadowDOM)
151{
152    document().body()->setInnerHTML("<b>FOO</b><i>foo</i>", ASSERT_NO_EXCEPTION);
153    RefPtrWillBeRawPtr<ShadowRoot> shadowRoot = document().body()->createShadowRoot(ASSERT_NO_EXCEPTION);
154    shadowRoot->setInnerHTML("<content select=\"i\"></content><u>Foo</u><content></content>", ASSERT_NO_EXCEPTION);
155    Node* textInBElement = document().body()->firstChild()->firstChild();
156    Node* textInIElement = document().body()->lastChild()->firstChild();
157    Node* textInUElement = shadowRoot->childNodes()->item(1)->firstChild();
158
159    int identifier = 0;
160    WebString searchText(String("foo"));
161    WebFindOptions findOptions; // Default.
162    bool wrapWithinFrame = true;
163    WebRect* selectionRect = 0;
164
165    // TextIterator currently returns the matches in the document order, instead of the visual order. It visits
166    // the shadow roots first, so in this case the matches will be returned in the order of <u> -> <b> -> <i>.
167    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
168    Range* activeMatch = textFinder().activeMatch();
169    ASSERT_TRUE(activeMatch);
170    EXPECT_EQ(textInUElement, activeMatch->startContainer());
171    EXPECT_EQ(0, activeMatch->startOffset());
172    EXPECT_EQ(textInUElement, activeMatch->endContainer());
173    EXPECT_EQ(3, activeMatch->endOffset());
174
175    findOptions.findNext = true;
176    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
177    activeMatch = textFinder().activeMatch();
178    ASSERT_TRUE(activeMatch);
179    EXPECT_EQ(textInBElement, activeMatch->startContainer());
180    EXPECT_EQ(0, activeMatch->startOffset());
181    EXPECT_EQ(textInBElement, activeMatch->endContainer());
182    EXPECT_EQ(3, activeMatch->endOffset());
183
184    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
185    activeMatch = textFinder().activeMatch();
186    ASSERT_TRUE(activeMatch);
187    EXPECT_EQ(textInIElement, activeMatch->startContainer());
188    EXPECT_EQ(0, activeMatch->startOffset());
189    EXPECT_EQ(textInIElement, activeMatch->endContainer());
190    EXPECT_EQ(3, activeMatch->endOffset());
191
192    // Should wrap to the first match.
193    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
194    activeMatch = textFinder().activeMatch();
195    ASSERT_TRUE(activeMatch);
196    EXPECT_EQ(textInUElement, activeMatch->startContainer());
197    EXPECT_EQ(0, activeMatch->startOffset());
198    EXPECT_EQ(textInUElement, activeMatch->endContainer());
199    EXPECT_EQ(3, activeMatch->endOffset());
200
201    // Fresh search in the reverse order.
202    identifier = 1;
203    findOptions = WebFindOptions();
204    findOptions.forward = false;
205
206    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
207    activeMatch = textFinder().activeMatch();
208    ASSERT_TRUE(activeMatch);
209    EXPECT_EQ(textInIElement, activeMatch->startContainer());
210    EXPECT_EQ(0, activeMatch->startOffset());
211    EXPECT_EQ(textInIElement, activeMatch->endContainer());
212    EXPECT_EQ(3, activeMatch->endOffset());
213
214    findOptions.findNext = true;
215    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
216    activeMatch = textFinder().activeMatch();
217    ASSERT_TRUE(activeMatch);
218    EXPECT_EQ(textInBElement, activeMatch->startContainer());
219    EXPECT_EQ(0, activeMatch->startOffset());
220    EXPECT_EQ(textInBElement, activeMatch->endContainer());
221    EXPECT_EQ(3, activeMatch->endOffset());
222
223    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
224    activeMatch = textFinder().activeMatch();
225    ASSERT_TRUE(activeMatch);
226    EXPECT_EQ(textInUElement, activeMatch->startContainer());
227    EXPECT_EQ(0, activeMatch->startOffset());
228    EXPECT_EQ(textInUElement, activeMatch->endContainer());
229    EXPECT_EQ(3, activeMatch->endOffset());
230
231    // And wrap.
232    ASSERT_TRUE(textFinder().find(identifier, searchText, findOptions, wrapWithinFrame, selectionRect));
233    activeMatch = textFinder().activeMatch();
234    ASSERT_TRUE(activeMatch);
235    EXPECT_EQ(textInIElement, activeMatch->startContainer());
236    EXPECT_EQ(0, activeMatch->startOffset());
237    EXPECT_EQ(textInIElement, activeMatch->endContainer());
238    EXPECT_EQ(3, activeMatch->endOffset());
239}
240
241TEST_F(TextFinderTest, ScopeTextMatchesSimple)
242{
243    document().body()->setInnerHTML("XXXXFindMeYYYYfindmeZZZZ", ASSERT_NO_EXCEPTION);
244    Node* textNode = document().body()->firstChild();
245
246    int identifier = 0;
247    WebString searchText(String("FindMe"));
248    WebFindOptions findOptions; // Default.
249
250    textFinder().resetMatchCount();
251    textFinder().scopeStringMatches(identifier, searchText, findOptions, true);
252    while (textFinder().scopingInProgress())
253        FrameTestHelpers::runPendingTasks();
254
255    EXPECT_EQ(2, textFinder().totalMatchCount());
256    WebVector<WebFloatRect> matchRects;
257    textFinder().findMatchRects(matchRects);
258    ASSERT_EQ(2u, matchRects.size());
259    EXPECT_EQ(findInPageRect(textNode, 4, textNode, 10), matchRects[0]);
260    EXPECT_EQ(findInPageRect(textNode, 14, textNode, 20), matchRects[1]);
261}
262
263TEST_F(TextFinderTest, ScopeTextMatchesWithShadowDOM)
264{
265    document().body()->setInnerHTML("<b>FOO</b><i>foo</i>", ASSERT_NO_EXCEPTION);
266    RefPtrWillBeRawPtr<ShadowRoot> shadowRoot = document().body()->createShadowRoot(ASSERT_NO_EXCEPTION);
267    shadowRoot->setInnerHTML("<content select=\"i\"></content><u>Foo</u><content></content>", ASSERT_NO_EXCEPTION);
268    Node* textInBElement = document().body()->firstChild()->firstChild();
269    Node* textInIElement = document().body()->lastChild()->firstChild();
270    Node* textInUElement = shadowRoot->childNodes()->item(1)->firstChild();
271
272    int identifier = 0;
273    WebString searchText(String("fOO"));
274    WebFindOptions findOptions; // Default.
275
276    textFinder().resetMatchCount();
277    textFinder().scopeStringMatches(identifier, searchText, findOptions, true);
278    while (textFinder().scopingInProgress())
279        FrameTestHelpers::runPendingTasks();
280
281    // TextIterator currently returns the matches in the document order, instead of the visual order. It visits
282    // the shadow roots first, so in this case the matches will be returned in the order of <u> -> <b> -> <i>.
283    EXPECT_EQ(3, textFinder().totalMatchCount());
284    WebVector<WebFloatRect> matchRects;
285    textFinder().findMatchRects(matchRects);
286    ASSERT_EQ(3u, matchRects.size());
287    EXPECT_EQ(findInPageRect(textInUElement, 0, textInUElement, 3), matchRects[0]);
288    EXPECT_EQ(findInPageRect(textInBElement, 0, textInBElement, 3), matchRects[1]);
289    EXPECT_EQ(findInPageRect(textInIElement, 0, textInIElement, 3), matchRects[2]);
290}
291
292TEST_F(TextFinderTest, ScopeRepeatPatternTextMatches)
293{
294    document().body()->setInnerHTML("ab ab ab ab ab", ASSERT_NO_EXCEPTION);
295    Node* textNode = document().body()->firstChild();
296
297    int identifier = 0;
298    WebString searchText(String("ab ab"));
299    WebFindOptions findOptions; // Default.
300
301    textFinder().resetMatchCount();
302    textFinder().scopeStringMatches(identifier, searchText, findOptions, true);
303    while (textFinder().scopingInProgress())
304        FrameTestHelpers::runPendingTasks();
305
306    EXPECT_EQ(2, textFinder().totalMatchCount());
307    WebVector<WebFloatRect> matchRects;
308    textFinder().findMatchRects(matchRects);
309    ASSERT_EQ(2u, matchRects.size());
310    EXPECT_EQ(findInPageRect(textNode, 0, textNode, 5), matchRects[0]);
311    EXPECT_EQ(findInPageRect(textNode, 6, textNode, 11), matchRects[1]);
312}
313
314TEST_F(TextFinderTest, OverlappingMatches)
315{
316    document().body()->setInnerHTML("aababaa", ASSERT_NO_EXCEPTION);
317    Node* textNode = document().body()->firstChild();
318
319    int identifier = 0;
320    WebString searchText(String("aba"));
321    WebFindOptions findOptions; // Default.
322
323    textFinder().resetMatchCount();
324    textFinder().scopeStringMatches(identifier, searchText, findOptions, true);
325    while (textFinder().scopingInProgress())
326        FrameTestHelpers::runPendingTasks();
327
328    // We shouldn't find overlapped matches.
329    EXPECT_EQ(1, textFinder().totalMatchCount());
330    WebVector<WebFloatRect> matchRects;
331    textFinder().findMatchRects(matchRects);
332    ASSERT_EQ(1u, matchRects.size());
333    EXPECT_EQ(findInPageRect(textNode, 1, textNode, 4), matchRects[0]);
334}
335
336TEST_F(TextFinderTest, SequentialMatches)
337{
338    document().body()->setInnerHTML("ababab", ASSERT_NO_EXCEPTION);
339    Node* textNode = document().body()->firstChild();
340
341    int identifier = 0;
342    WebString searchText(String("ab"));
343    WebFindOptions findOptions; // Default.
344
345    textFinder().resetMatchCount();
346    textFinder().scopeStringMatches(identifier, searchText, findOptions, true);
347    while (textFinder().scopingInProgress())
348        FrameTestHelpers::runPendingTasks();
349
350    EXPECT_EQ(3, textFinder().totalMatchCount());
351    WebVector<WebFloatRect> matchRects;
352    textFinder().findMatchRects(matchRects);
353    ASSERT_EQ(3u, matchRects.size());
354    EXPECT_EQ(findInPageRect(textNode, 0, textNode, 2), matchRects[0]);
355    EXPECT_EQ(findInPageRect(textNode, 2, textNode, 4), matchRects[1]);
356    EXPECT_EQ(findInPageRect(textNode, 4, textNode, 6), matchRects[2]);
357}
358
359class TextFinderFakeTimerTest : public TextFinderTest {
360protected:
361    virtual void SetUp() OVERRIDE;
362    virtual void TearDown() OVERRIDE;
363
364    // A simple platform that mocks out the clock.
365    class TimeProxyPlatform : public Platform {
366    public:
367        TimeProxyPlatform()
368            : m_timeCounter(0.)
369            , m_fallbackPlatform(0)
370        { }
371
372        void install()
373        {
374            // Check that the proxy wasn't installed yet.
375            ASSERT_NE(Platform::current(), this);
376            m_fallbackPlatform = Platform::current();
377            m_timeCounter = m_fallbackPlatform->currentTime();
378            Platform::initialize(this);
379            ASSERT_EQ(Platform::current(), this);
380        }
381
382        void remove()
383        {
384            // Check that the proxy was installed.
385            ASSERT_EQ(Platform::current(), this);
386            Platform::initialize(m_fallbackPlatform);
387            ASSERT_EQ(Platform::current(), m_fallbackPlatform);
388            m_fallbackPlatform = 0;
389        }
390
391    private:
392        Platform& ensureFallback()
393        {
394            ASSERT(m_fallbackPlatform);
395            return *m_fallbackPlatform;
396        }
397
398        // From blink::Platform:
399        virtual double currentTime() OVERRIDE
400        {
401            return ++m_timeCounter;
402        }
403
404        // These blink::Platform methods must be overriden to make a usable object.
405        virtual void cryptographicallyRandomValues(unsigned char* buffer, size_t length) OVERRIDE
406        {
407            ensureFallback().cryptographicallyRandomValues(buffer, length);
408        }
409
410        virtual const unsigned char* getTraceCategoryEnabledFlag(const char* categoryName) OVERRIDE
411        {
412            return ensureFallback().getTraceCategoryEnabledFlag(categoryName);
413        }
414
415        // These two methods allow timers to work correctly.
416        virtual double monotonicallyIncreasingTime() OVERRIDE
417        {
418            return ensureFallback().monotonicallyIncreasingTime();
419        }
420
421        virtual void setSharedTimerFireInterval(double interval) OVERRIDE
422        {
423            ensureFallback().setSharedTimerFireInterval(interval);
424        }
425
426        virtual WebThread* currentThread() OVERRIDE { return ensureFallback().currentThread(); }
427        virtual WebUnitTestSupport* unitTestSupport() OVERRIDE { return ensureFallback().unitTestSupport(); }
428        virtual WebString defaultLocale() OVERRIDE { return ensureFallback().defaultLocale(); }
429        virtual WebCompositorSupport* compositorSupport() OVERRIDE { return ensureFallback().compositorSupport(); }
430
431        double m_timeCounter;
432        Platform* m_fallbackPlatform;
433    };
434
435    TimeProxyPlatform m_proxyTimePlatform;
436};
437
438void TextFinderFakeTimerTest::SetUp()
439{
440    TextFinderTest::SetUp();
441    m_proxyTimePlatform.install();
442}
443
444void TextFinderFakeTimerTest::TearDown()
445{
446    m_proxyTimePlatform.remove();
447    TextFinderTest::TearDown();
448}
449
450TEST_F(TextFinderFakeTimerTest, ScopeWithTimeouts)
451{
452    // Make a long string.
453    String text(Vector<UChar>(100));
454    text.fill('a');
455    String searchPattern("abc");
456    // Make 4 substrings "abc" in text.
457    text.insert(searchPattern, 1);
458    text.insert(searchPattern, 10);
459    text.insert(searchPattern, 50);
460    text.insert(searchPattern, 90);
461
462    document().body()->setInnerHTML(text, ASSERT_NO_EXCEPTION);
463
464    int identifier = 0;
465    WebFindOptions findOptions; // Default.
466
467    textFinder().resetMatchCount();
468
469    // There will be only one iteration before timeout, because increment
470    // of the TimeProxyPlatform timer is greater than timeout threshold.
471    textFinder().scopeStringMatches(identifier, searchPattern, findOptions, true);
472    while (textFinder().scopingInProgress())
473        FrameTestHelpers::runPendingTasks();
474
475    EXPECT_EQ(4, textFinder().totalMatchCount());
476}
477
478} // namespace
479