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.
6 #include "web/TextFinder.h"
8 #include "bindings/core/v8/ExceptionStatePlaceholder.h"
9 #include "core/dom/Document.h"
10 #include "core/dom/NodeList.h"
11 #include "core/dom/Range.h"
12 #include "core/dom/shadow/ShadowRoot.h"
13 #include "core/frame/FrameHost.h"
14 #include "core/html/HTMLElement.h"
15 #include "core/layout/TextAutosizer.h"
16 #include "core/page/Page.h"
17 #include "platform/testing/UnitTestHelpers.h"
18 #include "public/platform/Platform.h"
19 #include "public/web/WebDocument.h"
20 #include "web/FindInPageCoordinates.h"
21 #include "web/WebLocalFrameImpl.h"
22 #include "web/tests/FrameTestHelpers.h"
23 #include "wtf/OwnPtr.h"
24 #include <gtest/gtest.h>
26 using blink::testing::runPendingTasks
;
30 class TextFinderTest
: public ::testing::Test
{
32 void SetUp() override
;
34 Document
& document() const;
35 TextFinder
& textFinder() const;
37 static WebFloatRect
findInPageRect(Node
* startContainer
, int startOffset
, Node
* endContainer
, int endOffset
);
40 FrameTestHelpers::WebViewHelper m_webViewHelper
;
41 RefPtrWillBePersistent
<Document
> m_document
;
42 RawPtrWillBePersistent
<TextFinder
> m_textFinder
;
45 void TextFinderTest::SetUp()
47 m_webViewHelper
.initialize();
48 WebLocalFrameImpl
& frameImpl
= *m_webViewHelper
.webViewImpl()->mainFrameImpl();
49 frameImpl
.viewImpl()->resize(WebSize(640, 480));
50 frameImpl
.viewImpl()->layout();
51 m_document
= PassRefPtrWillBeRawPtr
<Document
>(frameImpl
.document());
52 m_textFinder
= &frameImpl
.ensureTextFinder();
55 Document
& TextFinderTest::document() const
60 TextFinder
& TextFinderTest::textFinder() const
65 WebFloatRect
TextFinderTest::findInPageRect(Node
* startContainer
, int startOffset
, Node
* endContainer
, int endOffset
)
67 RefPtrWillBeRawPtr
<Range
> range
= Range::create(startContainer
->document(), startContainer
, startOffset
, endContainer
, endOffset
);
68 return WebFloatRect(findInPageRectFromRange(range
.get()));
71 TEST_F(TextFinderTest
, FindTextSimple
)
73 document().body()->setInnerHTML("XXXXFindMeYYYYfindmeZZZZ", ASSERT_NO_EXCEPTION
);
74 Node
* textNode
= document().body()->firstChild();
77 WebString
searchText(String("FindMe"));
78 WebFindOptions findOptions
; // Default.
79 bool wrapWithinFrame
= true;
80 WebRect
* selectionRect
= nullptr;
82 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
83 Range
* activeMatch
= textFinder().activeMatch();
84 ASSERT_TRUE(activeMatch
);
85 EXPECT_EQ(textNode
, activeMatch
->startContainer());
86 EXPECT_EQ(4, activeMatch
->startOffset());
87 EXPECT_EQ(textNode
, activeMatch
->endContainer());
88 EXPECT_EQ(10, activeMatch
->endOffset());
90 findOptions
.findNext
= true;
91 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
92 activeMatch
= textFinder().activeMatch();
93 ASSERT_TRUE(activeMatch
);
94 EXPECT_EQ(textNode
, activeMatch
->startContainer());
95 EXPECT_EQ(14, activeMatch
->startOffset());
96 EXPECT_EQ(textNode
, activeMatch
->endContainer());
97 EXPECT_EQ(20, activeMatch
->endOffset());
99 // Should wrap to the first match.
100 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
101 activeMatch
= textFinder().activeMatch();
102 ASSERT_TRUE(activeMatch
);
103 EXPECT_EQ(textNode
, activeMatch
->startContainer());
104 EXPECT_EQ(4, activeMatch
->startOffset());
105 EXPECT_EQ(textNode
, activeMatch
->endContainer());
106 EXPECT_EQ(10, activeMatch
->endOffset());
108 // Search in the reverse order.
110 findOptions
= WebFindOptions();
111 findOptions
.forward
= false;
113 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
114 activeMatch
= textFinder().activeMatch();
115 ASSERT_TRUE(activeMatch
);
116 EXPECT_EQ(textNode
, activeMatch
->startContainer());
117 EXPECT_EQ(14, activeMatch
->startOffset());
118 EXPECT_EQ(textNode
, activeMatch
->endContainer());
119 EXPECT_EQ(20, activeMatch
->endOffset());
121 findOptions
.findNext
= true;
122 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
123 activeMatch
= textFinder().activeMatch();
124 ASSERT_TRUE(activeMatch
);
125 EXPECT_EQ(textNode
, activeMatch
->startContainer());
126 EXPECT_EQ(4, activeMatch
->startOffset());
127 EXPECT_EQ(textNode
, activeMatch
->endContainer());
128 EXPECT_EQ(10, activeMatch
->endOffset());
130 // Wrap to the first match (last occurence in the document).
131 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
132 activeMatch
= textFinder().activeMatch();
133 ASSERT_TRUE(activeMatch
);
134 EXPECT_EQ(textNode
, activeMatch
->startContainer());
135 EXPECT_EQ(14, activeMatch
->startOffset());
136 EXPECT_EQ(textNode
, activeMatch
->endContainer());
137 EXPECT_EQ(20, activeMatch
->endOffset());
140 TEST_F(TextFinderTest
, FindTextAutosizing
)
142 document().body()->setInnerHTML("XXXXFindMeYYYYfindmeZZZZ", ASSERT_NO_EXCEPTION
);
145 WebString
searchText(String("FindMe"));
146 WebFindOptions findOptions
; // Default.
147 bool wrapWithinFrame
= true;
148 WebRect
* selectionRect
= nullptr;
150 // Set viewport scale to 20 in order to simulate zoom-in
151 VisualViewport
& visualViewport
= document().page()->frameHost().visualViewport();
152 visualViewport
.setScale(20);
154 // Enforce autosizing
155 document().settings()->setTextAutosizingEnabled(true);
156 document().settings()->setTextAutosizingWindowSizeOverride(IntSize(20, 20));
157 document().textAutosizer()->updatePageInfo();
159 // In case of autosizing, scale _should_ change
160 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
161 ASSERT_TRUE(textFinder().activeMatch());
162 ASSERT_EQ(1, visualViewport
.scale()); // in this case to 1
164 // Disable autosizing and reset scale to 20
165 visualViewport
.setScale(20);
166 document().settings()->setTextAutosizingEnabled(false);
167 document().textAutosizer()->updatePageInfo();
169 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
170 ASSERT_TRUE(textFinder().activeMatch());
171 ASSERT_EQ(20, visualViewport
.scale());
174 TEST_F(TextFinderTest
, FindTextNotFound
)
176 document().body()->setInnerHTML("XXXXFindMeYYYYfindmeZZZZ", ASSERT_NO_EXCEPTION
);
179 WebString
searchText(String("Boo"));
180 WebFindOptions findOptions
; // Default.
181 bool wrapWithinFrame
= true;
182 WebRect
* selectionRect
= nullptr;
184 EXPECT_FALSE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
185 EXPECT_FALSE(textFinder().activeMatch());
188 TEST_F(TextFinderTest
, FindTextInShadowDOM
)
190 document().body()->setInnerHTML("<b>FOO</b><i>foo</i>", ASSERT_NO_EXCEPTION
);
191 RefPtrWillBeRawPtr
<ShadowRoot
> shadowRoot
= document().body()->createShadowRootInternal(ShadowRootType::OpenByDefault
, ASSERT_NO_EXCEPTION
);
192 shadowRoot
->setInnerHTML("<content select=\"i\"></content><u>Foo</u><content></content>", ASSERT_NO_EXCEPTION
);
193 Node
* textInBElement
= document().body()->firstChild()->firstChild();
194 Node
* textInIElement
= document().body()->lastChild()->firstChild();
195 Node
* textInUElement
= shadowRoot
->childNodes()->item(1)->firstChild();
198 WebString
searchText(String("foo"));
199 WebFindOptions findOptions
; // Default.
200 bool wrapWithinFrame
= true;
201 WebRect
* selectionRect
= nullptr;
203 // TextIterator currently returns the matches in the composed treeorder, so
204 // in this case the matches will be returned in the order of
205 // <i> -> <u> -> <b>.
206 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
207 Range
* 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());
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(textInUElement
, activeMatch
->startContainer());
219 EXPECT_EQ(0, activeMatch
->startOffset());
220 EXPECT_EQ(textInUElement
, activeMatch
->endContainer());
221 EXPECT_EQ(3, activeMatch
->endOffset());
223 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
224 activeMatch
= textFinder().activeMatch();
225 ASSERT_TRUE(activeMatch
);
226 EXPECT_EQ(textInBElement
, activeMatch
->startContainer());
227 EXPECT_EQ(0, activeMatch
->startOffset());
228 EXPECT_EQ(textInBElement
, activeMatch
->endContainer());
229 EXPECT_EQ(3, activeMatch
->endOffset());
231 // Should wrap to the first match.
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());
240 // Fresh search in the reverse order.
242 findOptions
= WebFindOptions();
243 findOptions
.forward
= false;
245 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
246 activeMatch
= textFinder().activeMatch();
247 ASSERT_TRUE(activeMatch
);
248 EXPECT_EQ(textInBElement
, activeMatch
->startContainer());
249 EXPECT_EQ(0, activeMatch
->startOffset());
250 EXPECT_EQ(textInBElement
, activeMatch
->endContainer());
251 EXPECT_EQ(3, activeMatch
->endOffset());
253 findOptions
.findNext
= true;
254 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
255 activeMatch
= textFinder().activeMatch();
256 ASSERT_TRUE(activeMatch
);
257 EXPECT_EQ(textInUElement
, activeMatch
->startContainer());
258 EXPECT_EQ(0, activeMatch
->startOffset());
259 EXPECT_EQ(textInUElement
, activeMatch
->endContainer());
260 EXPECT_EQ(3, activeMatch
->endOffset());
262 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
263 activeMatch
= textFinder().activeMatch();
264 ASSERT_TRUE(activeMatch
);
265 EXPECT_EQ(textInIElement
, activeMatch
->startContainer());
266 EXPECT_EQ(0, activeMatch
->startOffset());
267 EXPECT_EQ(textInIElement
, activeMatch
->endContainer());
268 EXPECT_EQ(3, activeMatch
->endOffset());
271 ASSERT_TRUE(textFinder().find(identifier
, searchText
, findOptions
, wrapWithinFrame
, selectionRect
));
272 activeMatch
= textFinder().activeMatch();
273 ASSERT_TRUE(activeMatch
);
274 EXPECT_EQ(textInBElement
, activeMatch
->startContainer());
275 EXPECT_EQ(0, activeMatch
->startOffset());
276 EXPECT_EQ(textInBElement
, activeMatch
->endContainer());
277 EXPECT_EQ(3, activeMatch
->endOffset());
280 TEST_F(TextFinderTest
, ScopeTextMatchesSimple
)
282 document().body()->setInnerHTML("XXXXFindMeYYYYfindmeZZZZ", ASSERT_NO_EXCEPTION
);
283 Node
* textNode
= document().body()->firstChild();
286 WebString
searchText(String("FindMe"));
287 WebFindOptions findOptions
; // Default.
289 textFinder().resetMatchCount();
290 textFinder().scopeStringMatches(identifier
, searchText
, findOptions
, true);
291 while (textFinder().scopingInProgress())
294 EXPECT_EQ(2, textFinder().totalMatchCount());
295 WebVector
<WebFloatRect
> matchRects
;
296 textFinder().findMatchRects(matchRects
);
297 ASSERT_EQ(2u, matchRects
.size());
298 EXPECT_EQ(findInPageRect(textNode
, 4, textNode
, 10), matchRects
[0]);
299 EXPECT_EQ(findInPageRect(textNode
, 14, textNode
, 20), matchRects
[1]);
302 TEST_F(TextFinderTest
, ScopeTextMatchesWithShadowDOM
)
304 document().body()->setInnerHTML("<b>FOO</b><i>foo</i>", ASSERT_NO_EXCEPTION
);
305 RefPtrWillBeRawPtr
<ShadowRoot
> shadowRoot
= document().body()->createShadowRootInternal(ShadowRootType::Open
, ASSERT_NO_EXCEPTION
);
306 shadowRoot
->setInnerHTML("<content select=\"i\"></content><u>Foo</u><content></content>", ASSERT_NO_EXCEPTION
);
307 Node
* textInBElement
= document().body()->firstChild()->firstChild();
308 Node
* textInIElement
= document().body()->lastChild()->firstChild();
309 Node
* textInUElement
= shadowRoot
->childNodes()->item(1)->firstChild();
312 WebString
searchText(String("fOO"));
313 WebFindOptions findOptions
; // Default.
315 textFinder().resetMatchCount();
316 textFinder().scopeStringMatches(identifier
, searchText
, findOptions
, true);
317 while (textFinder().scopingInProgress())
320 // TextIterator currently returns the matches in the composed tree order,
321 // so in this case the matches will be returned in the order of
322 // <i> -> <u> -> <b>.
323 EXPECT_EQ(3, textFinder().totalMatchCount());
324 WebVector
<WebFloatRect
> matchRects
;
325 textFinder().findMatchRects(matchRects
);
326 ASSERT_EQ(3u, matchRects
.size());
327 EXPECT_EQ(findInPageRect(textInIElement
, 0, textInIElement
, 3), matchRects
[0]);
328 EXPECT_EQ(findInPageRect(textInUElement
, 0, textInUElement
, 3), matchRects
[1]);
329 EXPECT_EQ(findInPageRect(textInBElement
, 0, textInBElement
, 3), matchRects
[2]);
332 TEST_F(TextFinderTest
, ScopeRepeatPatternTextMatches
)
334 document().body()->setInnerHTML("ab ab ab ab ab", ASSERT_NO_EXCEPTION
);
335 Node
* textNode
= document().body()->firstChild();
338 WebString
searchText(String("ab ab"));
339 WebFindOptions findOptions
; // Default.
341 textFinder().resetMatchCount();
342 textFinder().scopeStringMatches(identifier
, searchText
, findOptions
, true);
343 while (textFinder().scopingInProgress())
346 EXPECT_EQ(2, textFinder().totalMatchCount());
347 WebVector
<WebFloatRect
> matchRects
;
348 textFinder().findMatchRects(matchRects
);
349 ASSERT_EQ(2u, matchRects
.size());
350 EXPECT_EQ(findInPageRect(textNode
, 0, textNode
, 5), matchRects
[0]);
351 EXPECT_EQ(findInPageRect(textNode
, 6, textNode
, 11), matchRects
[1]);
354 TEST_F(TextFinderTest
, OverlappingMatches
)
356 document().body()->setInnerHTML("aababaa", ASSERT_NO_EXCEPTION
);
357 Node
* textNode
= document().body()->firstChild();
360 WebString
searchText(String("aba"));
361 WebFindOptions findOptions
; // Default.
363 textFinder().resetMatchCount();
364 textFinder().scopeStringMatches(identifier
, searchText
, findOptions
, true);
365 while (textFinder().scopingInProgress())
368 // We shouldn't find overlapped matches.
369 EXPECT_EQ(1, textFinder().totalMatchCount());
370 WebVector
<WebFloatRect
> matchRects
;
371 textFinder().findMatchRects(matchRects
);
372 ASSERT_EQ(1u, matchRects
.size());
373 EXPECT_EQ(findInPageRect(textNode
, 1, textNode
, 4), matchRects
[0]);
376 TEST_F(TextFinderTest
, SequentialMatches
)
378 document().body()->setInnerHTML("ababab", ASSERT_NO_EXCEPTION
);
379 Node
* textNode
= document().body()->firstChild();
382 WebString
searchText(String("ab"));
383 WebFindOptions findOptions
; // Default.
385 textFinder().resetMatchCount();
386 textFinder().scopeStringMatches(identifier
, searchText
, findOptions
, true);
387 while (textFinder().scopingInProgress())
390 EXPECT_EQ(3, textFinder().totalMatchCount());
391 WebVector
<WebFloatRect
> matchRects
;
392 textFinder().findMatchRects(matchRects
);
393 ASSERT_EQ(3u, matchRects
.size());
394 EXPECT_EQ(findInPageRect(textNode
, 0, textNode
, 2), matchRects
[0]);
395 EXPECT_EQ(findInPageRect(textNode
, 2, textNode
, 4), matchRects
[1]);
396 EXPECT_EQ(findInPageRect(textNode
, 4, textNode
, 6), matchRects
[2]);
399 class TextFinderFakeTimerTest
: public TextFinderTest
{
401 void SetUp() override
;
402 void TearDown() override
;
404 // A simple platform that mocks out the clock.
405 class TimeProxyPlatform
: public Platform
{
409 , m_fallbackPlatform(0)
414 // Check that the proxy wasn't installed yet.
415 ASSERT_NE(Platform::current(), this);
416 m_fallbackPlatform
= Platform::current();
417 m_timeCounter
= m_fallbackPlatform
->currentTime();
418 Platform::initialize(this);
419 ASSERT_EQ(Platform::current(), this);
424 // Check that the proxy was installed.
425 ASSERT_EQ(Platform::current(), this);
426 Platform::initialize(m_fallbackPlatform
);
427 ASSERT_EQ(Platform::current(), m_fallbackPlatform
);
428 m_fallbackPlatform
= 0;
432 Platform
& ensureFallback()
434 ASSERT(m_fallbackPlatform
);
435 return *m_fallbackPlatform
;
438 // From blink::Platform:
439 double currentTime() override
441 return ++m_timeCounter
;
444 // These blink::Platform methods must be overriden to make a usable object.
445 void cryptographicallyRandomValues(unsigned char* buffer
, size_t length
) override
447 ensureFallback().cryptographicallyRandomValues(buffer
, length
);
450 const unsigned char* getTraceCategoryEnabledFlag(const char* categoryName
) override
452 return ensureFallback().getTraceCategoryEnabledFlag(categoryName
);
455 // These two methods allow timers to work correctly.
456 double monotonicallyIncreasingTime() override
458 return ensureFallback().monotonicallyIncreasingTime();
461 WebThread
* currentThread() override
{ return ensureFallback().currentThread(); }
462 WebUnitTestSupport
* unitTestSupport() override
{ return ensureFallback().unitTestSupport(); }
463 WebString
defaultLocale() override
{ return ensureFallback().defaultLocale(); }
464 WebCompositorSupport
* compositorSupport() override
{ return ensureFallback().compositorSupport(); }
466 double m_timeCounter
;
467 Platform
* m_fallbackPlatform
;
470 TimeProxyPlatform m_proxyTimePlatform
;
473 void TextFinderFakeTimerTest::SetUp()
475 TextFinderTest::SetUp();
476 m_proxyTimePlatform
.install();
479 void TextFinderFakeTimerTest::TearDown()
481 m_proxyTimePlatform
.remove();
482 TextFinderTest::TearDown();
485 TEST_F(TextFinderFakeTimerTest
, ScopeWithTimeouts
)
487 // Make a long string.
488 String
text(Vector
<UChar
>(100));
490 String
searchPattern("abc");
491 // Make 4 substrings "abc" in text.
492 text
.insert(searchPattern
, 1);
493 text
.insert(searchPattern
, 10);
494 text
.insert(searchPattern
, 50);
495 text
.insert(searchPattern
, 90);
497 document().body()->setInnerHTML(text
, ASSERT_NO_EXCEPTION
);
500 WebFindOptions findOptions
; // Default.
502 textFinder().resetMatchCount();
504 // There will be only one iteration before timeout, because increment
505 // of the TimeProxyPlatform timer is greater than timeout threshold.
506 textFinder().scopeStringMatches(identifier
, searchPattern
, findOptions
, true);
507 while (textFinder().scopingInProgress())
510 EXPECT_EQ(4, textFinder().totalMatchCount());