Commit a6c88d89 authored by Gayane Petrosyan's avatar Gayane Petrosyan Committed by Commit Bot

[SH-Blink] Add prefix and suffix to text fragment selector

Add prefix and suffix to text fragment selector when only exact text
selector is not enough. Gradually add words from available prefix and
suffix range until unique match is found.

Bug: 1102382
Change-Id: I377deca1315b3f2ea781a2dce37d04bb5a57dd69
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2374369
Commit-Queue: Gayane Petrosyan <gayane@chromium.org>
Reviewed-by: default avatarDavid Bokan <bokan@chromium.org>
Reviewed-by: default avatarYoshifumi Inoue <yosin@chromium.org>
Reviewed-by: default avatarTommy Martino <tmartino@chromium.org>
Cr-Commit-Position: refs/heads/master@{#807489}
parent 6afa37b9
......@@ -56,11 +56,11 @@ bool ShouldIgnoreContents(const Node& node) {
Node* GetNonSearchableAncestor(const Node& node) {
for (Node& ancestor : FlatTreeTraversal::InclusiveAncestorsOf(node)) {
const ComputedStyle* style = ancestor.EnsureComputedStyle();
if (ancestor.IsDocumentNode())
return nullptr;
if ((style && style->Display() == EDisplay::kNone) ||
ShouldIgnoreContents(ancestor))
return &ancestor;
if (ancestor.IsDocumentNode())
return nullptr;
}
return nullptr;
}
......@@ -76,15 +76,16 @@ bool IsBlockLevel(EDisplay display) {
display == EDisplay::kFlex || display == EDisplay::kListItem;
}
// Returns the next node after |start_node| (including start node) that is a
// text node and is searchable and visible.
// Returns the next/previous node after |start_node| (including start node) that
// is a text node and is searchable and visible.
template <class Direction>
Node* GetVisibleTextNode(Node& start_node) {
Node* node = &start_node;
// Move to outside display none subtree if we're inside one.
while (Node* ancestor = GetNonSearchableAncestor(*node)) {
if (ancestor->IsDocumentNode())
if (!ancestor)
return nullptr;
node = FlatTreeTraversal::NextSkippingChildren(*ancestor);
node = Direction::NextSkippingChildren(*ancestor);
if (!node)
return nullptr;
}
......@@ -94,7 +95,7 @@ Node* GetVisibleTextNode(Node& start_node) {
if (ShouldIgnoreContents(*node) ||
(style && style->Display() == EDisplay::kNone)) {
// This element and its descendants are not visible, skip it.
node = FlatTreeTraversal::NextSkippingChildren(*node);
node = Direction::NextSkippingChildren(*node);
continue;
}
if (style && style->Visibility() == EVisibility::kVisible &&
......@@ -103,7 +104,7 @@ Node* GetVisibleTextNode(Node& start_node) {
}
// This element is hidden, but node might be visible,
// or this is not a text node, so we move on.
node = FlatTreeTraversal::Next(*node);
node = Direction::Next(*node);
}
return nullptr;
}
......@@ -195,6 +196,30 @@ Node& FindBuffer::GetFirstBlockLevelAncestorInclusive(const Node& start_node) {
return *start_node.GetDocument().documentElement();
}
Node* FindBuffer::ForwardVisibleTextNode(Node& start_node) {
struct ForwardDirection {
static Node* Next(const Node& node) {
return FlatTreeTraversal::Next(node);
}
static Node* NextSkippingChildren(const Node& node) {
return FlatTreeTraversal::NextSkippingChildren(node);
}
};
return GetVisibleTextNode<ForwardDirection>(start_node);
}
Node* FindBuffer::BackwardVisibleTextNode(Node& start_node) {
struct BackwardDirection {
static Node* Next(const Node& node) {
return FlatTreeTraversal::Previous(node);
}
static Node* NextSkippingChildren(const Node& node) {
return FlatTreeTraversal::PreviousSkippingChildren(node);
}
};
return GetVisibleTextNode<BackwardDirection>(start_node);
}
FindBuffer::Results FindBuffer::FindMatches(const WebString& search_text,
const blink::FindOptions options) {
// We should return empty result if it's impossible to get a match (buffer is
......@@ -223,7 +248,7 @@ void FindBuffer::CollectTextUntilBlockBoundary(
return;
// Get first visible text node from |start_position|.
Node* node =
GetVisibleTextNode(*range.StartPosition().NodeAsRangeFirstNode());
ForwardVisibleTextNode(*range.StartPosition().NodeAsRangeFirstNode());
if (!node || !node->isConnected())
return;
......
......@@ -35,6 +35,10 @@ class CORE_EXPORT FindBuffer {
// that is block level.
static Node& GetFirstBlockLevelAncestorInclusive(const Node& start_node);
// See |GetVisibleTextNode|.
static Node* ForwardVisibleTextNode(Node& start_node);
static Node* BackwardVisibleTextNode(Node& start_node);
// A match result, containing the starting position of the match and
// the length of the match.
struct BufferMatchResult {
......
......@@ -11,11 +11,151 @@
#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/page/scrolling/text_fragment_finder.h"
#include "third_party/blink/renderer/platform/text/text_boundaries.h"
namespace blink {
namespace {
// Returns text within given positions of the node, skipping invisible children
// and comments.
String GetText(Node* node, int start_position, int end_position) {
auto range_start = Position(node, start_position);
auto range_end = Position(node, end_position);
return PlainText(EphemeralRange(range_start, range_end));
}
// Returns text content of the node, skipping invisible children and comments.
String GetText(Node* node) {
return PlainText(EphemeralRange::RangeOfContents(*node));
}
// Returns true if text from beginning of |node| until |pos_offset| can be
// considered empty. Otherwise, return false.
bool IsFirstVisiblePosition(Node* node, unsigned pos_offset) {
return pos_offset == 0 ||
GetText(node, 0, pos_offset).StripWhiteSpace().IsEmpty();
}
// Returns true if text from |pos_offset| until end of |node| can be considered
// empty. Otherwise, return false.
bool IsLastVisiblePosition(Node* node, unsigned pos_offset) {
return pos_offset == node->textContent().length() ||
GetText(node, pos_offset, node->textContent().length())
.StripWhiteSpace()
.IsEmpty();
}
struct ForwadDirection {
static Node* Next(const Node& node) { return FlatTreeTraversal::Next(node); }
static Node* Next(const Node& node, const Node* stay_within) {
return FlatTreeTraversal::Next(node, stay_within);
}
static Node* GetVisibleTextNode(Node& start_node) {
return FindBuffer::ForwardVisibleTextNode(start_node);
}
};
struct BackwardDirection {
static Node* Next(const Node& node) {
return FlatTreeTraversal::Previous(node);
}
static Node* Next(const Node& node, const Node* stay_within) {
return FlatTreeTraversal::Previous(node, stay_within);
}
static Node* GetVisibleTextNode(Node& start_node) {
return FindBuffer::BackwardVisibleTextNode(start_node);
}
};
template <class Direction>
Node* NextNonEmptyVisibleTextNode(Node* start_node) {
if (!start_node)
return nullptr;
// Move forward/backward until non empty visible text node is found.
for (Node* node = start_node; node; node = Direction::Next(*node)) {
Node* next_node = Direction::GetVisibleTextNode(*node);
if (!next_node || !GetText(next_node).IsEmpty())
return next_node;
node = next_node;
}
return nullptr;
}
// Returns the next/previous visible node to |start_node|.
Node* FirstNonEmptyVisibleTextNode(Node* start_node) {
return NextNonEmptyVisibleTextNode<ForwadDirection>(start_node);
}
Node* BackwardNonEmptyVisibleTextNode(Node* start_node) {
return NextNonEmptyVisibleTextNode<BackwardDirection>(start_node);
}
// Returns the furthest node within same block as |start_node| without crossing
// block boundaries.
// Returns the next/previous visible node to |start_node|.
template <class Direction>
Node* FurthestVisibleTextNodeWithinBlock(Node* start_node) {
Node& block_ancestor =
FindBuffer::GetFirstBlockLevelAncestorInclusive(*start_node);
// Move forward/backward until no next/previous node is available within same
// |block_ancestor|.
Node* last_node;
for (Node* node = start_node; node;
node = Direction::Next(*node, &block_ancestor)) {
node = Direction::GetVisibleTextNode(*node);
// Stop, if crossed block boundaries.
if (!node ||
!FindBuffer::GetFirstBlockLevelAncestorInclusive(*node).isSameNode(
&block_ancestor))
break;
last_node = node;
}
return last_node;
}
Node* FirstVisibleTextNodeWithinBlock(Node* start_node) {
return FurthestVisibleTextNodeWithinBlock<ForwadDirection>(start_node);
}
Node* LastVisibleTextNodeWithinBlock(Node* start_node) {
return FurthestVisibleTextNodeWithinBlock<BackwardDirection>(start_node);
}
// Returns whitespace-trimmed substring of |text| containing the final
// |word_num| words.
String GetWordsFromEnd(String text, int word_num) {
if (text.IsEmpty())
return "";
int pos = text.length() - 1;
text.Ensure16Bit();
while (word_num-- > 0)
pos = FindNextWordBackward(text.Characters16(), text.length(), pos);
return text.Substring(pos, text.length()).StripWhiteSpace();
}
// Returns whitespace-trimmed substring of |text| containing the first
// |word_num| words.
String GetWordsFromStart(String text, int word_num) {
if (text.IsEmpty())
return "";
int pos = 0;
text.Ensure16Bit();
while (word_num-- > 0)
pos = FindNextWordForward(text.Characters16(), text.length(), pos);
return text.Substring(0, pos).StripWhiteSpace();
}
} // namespace
constexpr int kExactTextMaxChars = 300;
constexpr int kNoContextMinChars = 20;
constexpr int kMaxContextWords = 10;
void TextFragmentSelectorGenerator::UpdateSelection(
LocalFrame* selection_frame,
......@@ -47,39 +187,52 @@ void TextFragmentSelectorGenerator::GenerateSelector(
DCHECK(callback);
pending_generate_selector_callback_ = std::move(callback);
EphemeralRangeInFlatTree ephemeral_range(selection_range_);
state_ = kNeedsNewCandidate;
step_ = kExact;
max_available_prefix_ = "";
max_available_suffix_ = "";
num_prefix_words_ = 0;
num_suffix_words_ = 0;
const TextFragmentSelector kInvalidSelector(
TextFragmentSelector::SelectorType::kInvalid);
GenerateSelectorCandidate();
}
Node& start_first_block_ancestor =
FindBuffer::GetFirstBlockLevelAncestorInclusive(
*ephemeral_range.StartPosition().AnchorNode());
Node& end_first_block_ancestor =
FindBuffer::GetFirstBlockLevelAncestorInclusive(
*ephemeral_range.EndPosition().AnchorNode());
void TextFragmentSelectorGenerator::GenerateSelectorCandidate() {
DCHECK_EQ(kNeedsNewCandidate, state_);
if (!start_first_block_ancestor.isSameNode(&end_first_block_ancestor)) {
NotifySelectorReady(kInvalidSelector);
return;
}
// TODO(gayane): If same node, need to check if start and end are interrupted
// by a block. Example: <div>start of the selection <div> sub block </div>end
// of the selection</div>.
if (step_ == kExact)
GenerateExactSelector();
// TODO(gayane): Move selection start and end to contain full words.
if (step_ == kRange)
ExtendRangeSelector();
String selected_text = PlainText(ephemeral_range);
if (step_ == kContext)
ExtendContext();
ResolveSelectorState();
}
if (selected_text.length() < kNoContextMinChars ||
selected_text.length() > kExactTextMaxChars) {
NotifySelectorReady(kInvalidSelector);
return;
void TextFragmentSelectorGenerator::ResolveSelectorState() {
switch (state_) {
case kTestCandidate:
RunTextFinder();
break;
case kNeedsNewCandidate:
NOTREACHED();
ABSL_FALLTHROUGH_INTENDED;
case kFailure:
NotifySelectorReady(
TextFragmentSelector(TextFragmentSelector::SelectorType::kInvalid));
break;
case kSuccess:
NotifySelectorReady(*selector_);
break;
}
}
selector_ = std::make_unique<TextFragmentSelector>(
TextFragmentSelector::SelectorType::kExact, selected_text, "", "", "");
void TextFragmentSelectorGenerator::RunTextFinder() {
DCHECK(selector_);
// |FindMatch| will call |DidFindMatch| indicating if the match was unique.
TextFragmentFinder finder(*this, *selector_);
finder.FindMatch(*selection_frame_->GetDocument());
}
......@@ -88,12 +241,19 @@ void TextFragmentSelectorGenerator::DidFindMatch(
const EphemeralRangeInFlatTree& match,
const TextFragmentAnchorMetrics::Match match_metrics,
bool is_unique) {
if (is_unique) {
NotifySelectorReady(*selector_);
if (is_unique && PlainText(match).StripWhiteSpace().length() ==
PlainText(EphemeralRange(selection_range_))
.StripWhiteSpace()
.length()) {
state_ = kSuccess;
ResolveSelectorState();
} else {
// TODO(gayane): Should add more range and/or context.
NotifySelectorReady(
TextFragmentSelector(TextFragmentSelector::SelectorType::kInvalid));
state_ = kNeedsNewCandidate;
// If already tried exact selector then should continue by adding context.
if (step_ == kExact)
step_ = kContext;
GenerateSelectorCandidate();
}
}
......@@ -117,4 +277,152 @@ void TextFragmentSelectorGenerator::Trace(Visitor* visitor) const {
visitor->Trace(selector_producer_);
}
void TextFragmentSelectorGenerator::GenerateExactSelector() {
DCHECK_EQ(kExact, step_);
DCHECK_EQ(kNeedsNewCandidate, state_);
EphemeralRangeInFlatTree ephemeral_range(selection_range_);
Node& start_first_block_ancestor =
FindBuffer::GetFirstBlockLevelAncestorInclusive(
*ephemeral_range.StartPosition().ComputeContainerNode());
Node& end_first_block_ancestor =
FindBuffer::GetFirstBlockLevelAncestorInclusive(
*ephemeral_range.EndPosition().ComputeContainerNode());
// If not in same node, should use ranges.
if (!start_first_block_ancestor.isSameNode(&end_first_block_ancestor)) {
step_ = kRange;
return;
}
// TODO(gayane): If same node, need to check if start and end are interrupted
// by a block. Example: <div>start of the selection <div> sub block </div>end
// of the selection</div>.
// TODO(gayane): Move selection start and end to contain full words.
String selected_text = PlainText(ephemeral_range);
// If too long should use ranges.
if (selected_text.length() > kExactTextMaxChars) {
step_ = kRange;
return;
}
selector_ = std::make_unique<TextFragmentSelector>(
TextFragmentSelector::SelectorType::kExact,
selected_text.StripWhiteSpace(), "", "", "");
// If too short should use exact selector, but should add context.
if (selected_text.length() < kNoContextMinChars) {
step_ = kContext;
return;
}
state_ = kTestCandidate;
}
void TextFragmentSelectorGenerator::ExtendRangeSelector() {
// TODO(gayane): Generate range selector.
state_ = kFailure;
}
void TextFragmentSelectorGenerator::ExtendContext() {
DCHECK_EQ(kContext, step_);
DCHECK_EQ(kNeedsNewCandidate, state_);
// TODO(gayane): Change this to DCHECK after |ExtendRangeSelector| is
// implemented.
if (!selector_)
return;
// Give up if context is already too long.
if (num_prefix_words_ == kMaxContextWords ||
num_prefix_words_ == kMaxContextWords) {
state_ = kFailure;
return;
}
// Try initiating properties necessary for calculating prefix and suffix.
if (max_available_prefix_.IsEmpty() && max_available_suffix_.IsEmpty()) {
max_available_prefix_ =
GetAvailablePrefixAsText(selection_range_->StartPosition());
max_available_suffix_ =
GetAvailableSuffixAsText(selection_range_->EndPosition());
}
if (max_available_prefix_.IsEmpty() && max_available_suffix_.IsEmpty()) {
state_ = kFailure;
return;
}
String prefix = GetWordsFromEnd(max_available_prefix_, ++num_prefix_words_);
String suffix = GetWordsFromStart(max_available_suffix_, ++num_suffix_words_);
// Give up if we were unable to get new prefix and suffix.
if (prefix == selector_->Prefix() && suffix == selector_->Suffix()) {
state_ = kFailure;
return;
}
selector_ = std::make_unique<TextFragmentSelector>(
selector_->Type(), selector_->Start(), selector_->End(), prefix, suffix);
state_ = kTestCandidate;
}
String TextFragmentSelectorGenerator::GetAvailablePrefixAsText(
const Position& prefix_end_position) {
Node* prefix_end = prefix_end_position.ComputeContainerNode();
unsigned prefix_end_offset =
prefix_end_position.ComputeOffsetInContainerNode();
// If given position point to the first visible text in its containiner node,
// use the preceding visible node for the suffix.
if (IsFirstVisiblePosition(prefix_end, prefix_end_offset)) {
prefix_end = BackwardNonEmptyVisibleTextNode(
FlatTreeTraversal::PreviousSkippingChildren(*prefix_end));
if (!prefix_end)
return "";
prefix_end_offset = prefix_end->textContent().length();
}
// The furthest node within same block without crossing block boundaries would
// be the suffix end.
Node* prefix_start = LastVisibleTextNodeWithinBlock(prefix_end);
if (!prefix_start)
return "";
auto range_start = Position(prefix_start, 0);
auto range_end = Position(prefix_end, prefix_end_offset);
return PlainText(EphemeralRange(range_start, range_end)).StripWhiteSpace();
}
String TextFragmentSelectorGenerator::GetAvailableSuffixAsText(
const Position& suffix_start_position) {
Node* suffix_start = suffix_start_position.ComputeContainerNode();
unsigned suffix_start_offset =
suffix_start_position.ComputeOffsetInContainerNode();
// If given position point to the last visible text in its containiner node,
// use the following visible node for the suffix.
if (IsLastVisiblePosition(suffix_start, suffix_start_offset)) {
suffix_start = FirstNonEmptyVisibleTextNode(
FlatTreeTraversal::NextSkippingChildren(*suffix_start));
suffix_start_offset = 0;
}
if (!suffix_start)
return "";
// The furthest node within same block without crossing block boundaries would
// be the suffix end.
Node* suffix_end = FirstVisibleTextNodeWithinBlock(suffix_start);
if (!suffix_end)
return "";
auto range_start = Position(suffix_start, suffix_start_offset);
auto range_end = Position(suffix_end, suffix_end->textContent().length());
return PlainText(EphemeralRange(range_start, range_end)).StripWhiteSpace();
}
} // namespace blink
......@@ -21,6 +21,11 @@ class LocalFrame;
// Generated selectors would be later used to highlight the same
// text if successfully parsed by |TextFragmentAnchor |. Generation will be
// triggered when users request "link to text" for the selected text.
//
// TextFragmentSelectorGenerator generates candidate selectors and tries it
// against the page content to ensure the correct and unique match. Repeats the
// process adding context/range to the selector as necessary until the correct
// match is uniquely identified or no new context/range can be added.
class CORE_EXPORT TextFragmentSelectorGenerator final
: public GarbageCollected<TextFragmentSelectorGenerator>,
public TextFragmentFinder::Client,
......@@ -48,12 +53,57 @@ class CORE_EXPORT TextFragmentSelectorGenerator final
// Notifies the results of |GenerateSelector|.
void NotifySelectorReady(const TextFragmentSelector& selector);
// Wrappers for tests.
String GetAvailablePrefixAsTextForTesting(const Position& position) {
return GetAvailablePrefixAsText(position);
}
String GetAvailableSuffixAsTextForTesting(const Position& position) {
return GetAvailableSuffixAsText(position);
}
// Releases members if necessary.
void ClearSelection();
void Trace(Visitor*) const;
private:
// Used for determining the next step of selector generation.
enum GenerationStep { kExact, kRange, kContext };
// Used for determining the current state of |selector_|.
enum SelectorState {
// Candidate selector should be generated or extended.
kNeedsNewCandidate,
// Candidate selector generation was successful and selector is ready to be
// tested for uniqueness and accuracy by running against the page's content.
kTestCandidate,
// Candidate selector generation was unsuccessful. No further attempts are
// necessary.
kFailure,
// Selector is found. No further attempts are necessary.
kSuccess
};
void GenerateSelectorCandidate();
void ResolveSelectorState();
void RunTextFinder();
// Returns max text preceding given position that doesn't cross block
// boundaries.
String GetAvailablePrefixAsText(const Position& position);
// Returns max text following given position that doesn't cross block
// boundaries.
String GetAvailableSuffixAsText(const Position& position);
void GenerateExactSelector();
void ExtendRangeSelector();
void ExtendContext();
Member<LocalFrame> selection_frame_;
Member<Range> selection_range_;
std::unique_ptr<TextFragmentSelector> selector_;
......@@ -65,6 +115,20 @@ class CORE_EXPORT TextFragmentSelectorGenerator final
selector_producer_{this, nullptr};
GenerateSelectorCallback pending_generate_selector_callback_;
GenerationStep step_ = kExact;
SelectorState state_ = kNeedsNewCandidate;
// Fields used for keeping track of context.
// Strings available for gradually forming prefix and suffix.
String max_available_prefix_;
String max_available_suffix_;
// Indicates a number of words used from |max_available_prefix_| and
// |max_available_suffix_| for the current |selector_|.
int num_prefix_words_ = 0;
int num_suffix_words_ = 0;
DISALLOW_COPY_AND_ASSIGN(TextFragmentSelectorGenerator);
};
......
......@@ -12,8 +12,8 @@
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "third_party/blink/public/mojom/link_to_text/link_to_text.mojom-blink.h"
#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/html_iframe_element.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
......@@ -54,6 +54,7 @@ class TextFragmentSelectorGeneratorTest : public SimTest {
}
};
// Basic exact selector case.
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextSelector) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
......@@ -66,11 +67,14 @@ TEST_F(TextFragmentSelectorGeneratorTest, ExactTextSelector) {
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 0);
const auto& selected_end = Position(first_paragraph, 28);
ASSERT_EQ("First paragraph text that is",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"First%20paragraph%20text%20that%20is");
}
// Exact selector test where selection contains nested <i> node.
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextWithNestedTextNodes) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
......@@ -84,11 +88,14 @@ TEST_F(TextFragmentSelectorGeneratorTest, ExactTextWithNestedTextNodes) {
const auto& selected_start = Position(first_paragraph->firstChild(), 0);
const auto& selected_end =
Position(first_paragraph->firstChild()->nextSibling()->firstChild(), 6);
ASSERT_EQ("First paragraph text that is longer",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"First%20paragraph%20text%20that%20is%20longer");
}
// Exact selector test where selection contains multiple spaces.
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextWithExtraSpace) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
......@@ -96,16 +103,329 @@ TEST_F(TextFragmentSelectorGeneratorTest, ExactTextWithExtraSpace) {
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph text that is longer than 20 chars</p>
<p id='second'>Second paragraph text</p>
<p id='second'>Second paragraph
text</p>
)HTML");
Node* second_paragraph = GetDocument().getElementById("second")->firstChild();
const auto& selected_start = Position(second_paragraph, 0);
const auto& selected_end = Position(second_paragraph, 23);
const auto& selected_end = Position(second_paragraph, 27);
ASSERT_EQ("Second paragraph text",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"Second%20paragraph%20text");
}
// Exact selector where selection is too short, in which case context is
// required.
TEST_F(TextFragmentSelectorGeneratorTest,
ExactTextSelector_TooShortNeedsContext) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph prefix to unique snippet of text.</p>
<p id='second'>Second paragraph</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 26);
const auto& selected_end = Position(first_paragraph, 40);
ASSERT_EQ("unique snippet",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"to-,unique%20snippet,-of");
}
// Exact selector with context test. Case when only one word for prefix and
// suffix is enough to disambiguate the selection.
TEST_F(TextFragmentSelectorGeneratorTest,
ExactTextSelector_WithOneWordContext) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph text that is longer than 20 chars</p>
<p id='second'>Second paragraph text that is short</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 6);
const auto& selected_end = Position(first_paragraph, 28);
ASSERT_EQ("paragraph text that is",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"First-,paragraph%20text%20that%20is,-longer");
}
// Exact selector with context test. Case when multiple words for prefix and
// suffix is necessary to disambiguate the selection.
TEST_F(TextFragmentSelectorGeneratorTest,
ExactTextSelector_MultipleWordContext) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First prefix to not unique snippet of text followed by suffix</p>
<p id='second'>Second prefix to not unique snippet of text followed by suffix</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 16);
const auto& selected_end = Position(first_paragraph, 42);
ASSERT_EQ("not unique snippet of text",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"First%20prefix%20to-,not%20unique%20snippet%20of%"
"20text,-followed%20by%20suffix");
}
// Exact selector with context test. Case when multiple words for prefix and
// suffix is necessary to disambiguate the selection and prefix and suffix
// contain extra space.
TEST_F(TextFragmentSelectorGeneratorTest,
ExactTextSelector_MultipleWordContext_ExtraSpace) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First prefix to not unique snippet of text followed by suffix</p>
<p id='second'>Second prefix to not unique snippet of text followed by suffix</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 21);
const auto& selected_end = Position(first_paragraph, 47);
ASSERT_EQ("not unique snippet of text",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"First%20prefix%20to-,not%20unique%20snippet%20of%"
"20text,-followed%20by%20suffix");
}
// Exact selector with context test. Case when available prefix for all the
// occurrences of selected text is the same. In this case suffix should be
// extended until unique selector is found.
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextSelector_SamePrefix) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>Prefix to not unique snippet of text followed by different suffix</p>
<p id='second'>Prefix to not unique snippet of text followed by suffix</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 10);
const auto& selected_end = Position(first_paragraph, 36);
ASSERT_EQ("not unique snippet of text",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"Prefix%20to-,not%20unique%20snippet%20of%20text,-"
"followed%20by%20different");
}
// Exact selector with context test. Case when available suffix for all the
// occurrences of selected text is the same. In this case prefix should be
// extended until unique selector is found.
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextSelector_SameSuffix) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph prefix to not unique snippet of text followed by suffix</p>
<p id='second'>Second paragraph prefix to not unique snippet of text followed by suffix</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 26);
const auto& selected_end = Position(first_paragraph, 52);
ASSERT_EQ("not unique snippet of text",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"First%20paragraph%20prefix%20to-,not%20unique%"
"20snippet%20of%20text,-followed%20by%20suffix");
}
// Exact selector with context test. Case when available prefix and suffix for
// all the occurrences of selected text are the same. In this case generation
// should be unsuccessful.
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextSelector_SamePreffixSuffix) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>Same paragraph prefix to not unique snippet of text followed by suffix</p>
<p id='second'>Same paragraph prefix to not unique snippet of text followed by suffix</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 25);
const auto& selected_end = Position(first_paragraph, 51);
ASSERT_EQ("not unique snippet of text",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end, "");
}
// Exact selector with context test. Case when available prefix and suffix for
// all the occurrences of selected text are the same for the first 10 words. In
// this case generation should be unsuccessful.
TEST_F(TextFragmentSelectorGeneratorTest,
ExactTextSelector_SimilarLongPreffixSuffix) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph prefix one two three four five six seven
eight nine ten to not unique snippet of text followed by suffix</p>
<p id='second'>Second paragraph prefix one two three four five six seven
eight nine ten to not unique snippet of text followed by suffix</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 80);
const auto& selected_end = Position(first_paragraph, 106);
ASSERT_EQ("not unique snippet of text",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end, "");
}
// Exact selector with context test. Case when no prefix is available.
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextSelector_NoPrefix) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>Not unique snippet of text followed by first suffix</p>
<p id='second'>Not unique snippet of text followed by second suffix</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 0);
const auto& selected_end = Position(first_paragraph, 26);
ASSERT_EQ("Not unique snippet of text",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(
selected_start, selected_end,
"Not%20unique%20snippet%20of%20text,-followed%20by%20first");
}
// Exact selector with context test. Case when no suffix is available.
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextSelector_NoSuffix) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First prefix to not unique snippet of text</p>
<p id='second'>Second prefix to not unique snippet of text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("second")->firstChild();
const auto& selected_start = Position(first_paragraph, 17);
const auto& selected_end = Position(first_paragraph, 43);
ASSERT_EQ("not unique snippet of text",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"Second%20prefix%20to-,not%20unique%20snippet%20of%"
"20text");
}
// Exact selector with context test. Case when available prefix is the
// preceding block.
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextSelector_PrevNodePrefix) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph with not unique snippet</p>
<p id='second'>not unique snippet of text</p>
)HTML");
Node* second_paragraph = GetDocument().getElementById("second")->firstChild();
const auto& selected_start = Position(second_paragraph, 0);
const auto& selected_end = Position(second_paragraph, 18);
ASSERT_EQ("not unique snippet",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"snippet-,not%20unique%20snippet,-of");
}
// Exact selector with context test. Case when available prefix is the
// preceding block, which is a text node.
TEST_F(TextFragmentSelectorGeneratorTest,
ExactTextSelector_PrevTextNodePrefix) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph with not unique snippet</p>
text
<p id='second'>not unique snippet of text</p>
)HTML");
Node* second_paragraph = GetDocument().getElementById("second")->firstChild();
const auto& selected_start = Position(second_paragraph, 0);
const auto& selected_end = Position(second_paragraph, 18);
ASSERT_EQ("not unique snippet",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"text-,not%20unique%20snippet,-of");
}
// Exact selector with context test. Case when available suffix is the next
// block.
TEST_F(TextFragmentSelectorGeneratorTest, ExactTextSelector_NextNodeSuffix) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph with not unique snippet</p>
<p id='second'>not unique snippet of text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 21);
const auto& selected_end = Position(first_paragraph, 39);
ASSERT_EQ("not unique snippet",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"with-,not%20unique%20snippet,-not");
}
// Exact selector with context test. Case when available suffix is the next
// block, which is a text node.
TEST_F(TextFragmentSelectorGeneratorTest,
ExactTextSelector_NexttextNodeSuffix) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>Test page</div>
<p id='first'>First paragraph with not unique snippet</p>
text
<p id='second'>not unique snippet of text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& selected_start = Position(first_paragraph, 21);
const auto& selected_end = Position(first_paragraph, 39);
ASSERT_EQ("not unique snippet",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end,
"with-,not%20unique%20snippet,-text");
}
// Multi-block selection is currently not implemented.
TEST_F(TextFragmentSelectorGeneratorTest, MultiblockSelection) {
SimRequest request("https://example.com/test.html", "text/html");
......@@ -120,8 +440,590 @@ TEST_F(TextFragmentSelectorGeneratorTest, MultiblockSelection) {
Node* second_paragraph = GetDocument().getElementById("second")->firstChild();
const auto& selected_start = Position(first_paragraph, 0);
const auto& selected_end = Position(second_paragraph, 6);
ASSERT_EQ("First paragraph text that is longer than 20 chars\n\nSecond",
PlainText(EphemeralRange(selected_start, selected_end)));
GenerateAndVerifySelector(selected_start, selected_end, "");
}
// Basic test case for |GetAvailableSuffixAsText|.
TEST_F(TextFragmentSelectorGeneratorTest, GetAvailablePrefixAsText) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First paragraph text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 16);
const auto& end = Position(first_paragraph, 20);
ASSERT_EQ("text", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("First paragraph", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailablePrefixAsTextForTesting(start));
}
// Check the case when available prefix contains collapsible space.
TEST_F(TextFragmentSelectorGeneratorTest, GetAvailablePrefixAsText_ExtraSpace) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First
paragraph text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 26);
const auto& end = Position(first_paragraph, 30);
ASSERT_EQ("text", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("First paragraph", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailablePrefixAsTextForTesting(start));
}
// Check the case when available prefix complete text content of the previous
// block.
TEST_F(TextFragmentSelectorGeneratorTest, GetAvailablePrefixAsText_PrevNode) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First paragraph text</p>
<p id='second'>Second paragraph text</p>
)HTML");
Node* second_paragraph = GetDocument().getElementById("second")->firstChild();
const auto& start = Position(second_paragraph, 0);
const auto& end = Position(second_paragraph, 6);
ASSERT_EQ("Second", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("First paragraph text",
GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailablePrefixAsTextForTesting(start));
}
// Check the case when there is a commented block between selection and the
// available prefix.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailablePrefixAsText_PrevNode_WithComment) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First paragraph text</p>
<!--
multiline comment that should be ignored.
//-->
<p id='second'>Second paragraph text</p>
)HTML");
Node* second_paragraph = GetDocument().getElementById("second")->firstChild();
const auto& start = Position(second_paragraph, 0);
const auto& end = Position(second_paragraph, 6);
ASSERT_EQ("Second", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("First paragraph text",
GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailablePrefixAsTextForTesting(start));
}
// Check the case when available prefix is a text node outside of selection
// block.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailablePrefixAsText_PrevTextNode) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
text
<p id='first'>First paragraph text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 5);
ASSERT_EQ("First", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("text", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailablePrefixAsTextForTesting(start));
}
// Check the case when available prefix is a parent node text content outside of
// selection block.
TEST_F(TextFragmentSelectorGeneratorTest, GetAvailablePrefixAsText_ParentNode) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div>nested
<p id='first'>First paragraph text</p></div>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 5);
ASSERT_EQ("First", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("nested", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailablePrefixAsTextForTesting(start));
}
// Check the case when available prefix contains non-block tag(e.g. <b>).
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailablePrefixAsText_NestedTextNode) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First <b>bold text</b> paragraph text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->lastChild();
const auto& start = Position(first_paragraph, 11);
const auto& end = Position(first_paragraph, 15);
ASSERT_EQ("text", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("First bold text paragraph",
GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailablePrefixAsTextForTesting(start));
}
// Check the case when available prefix is collected until nested block.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailablePrefixAsText_NestedBlock) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div id='first'>First <div id='div'>div</div> paragraph text</div>
)HTML");
Node* first_paragraph = GetDocument().getElementById("div")->nextSibling();
const auto& start = Position(first_paragraph, 11);
const auto& end = Position(first_paragraph, 15);
ASSERT_EQ("text", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("paragraph", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailablePrefixAsTextForTesting(start));
}
// Check the case when available prefix includes non-block element but stops at
// nested block.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailablePrefixAsText_NestedBlockInNestedText) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div id='first'>First <b><div id='div'>div</div>bold</b> paragraph text</div>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->lastChild();
const auto& start = Position(first_paragraph, 11);
const auto& end = Position(first_paragraph, 15);
ASSERT_EQ("text", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("bold paragraph", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailablePrefixAsTextForTesting(start));
}
// Check the case when available prefix includes invisible block.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailablePrefixAsText_NestedInvisibleBlock) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div id='first'>First <div id='div' style='display:none'>invisible</div> paragraph text</div>
)HTML");
Node* first_paragraph = GetDocument().getElementById("div")->nextSibling();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 10);
ASSERT_EQ("paragraph", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("First", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailablePrefixAsTextForTesting(start));
}
// Check the case when previous node is used for available prefix when selection
// is not at index=0 but there is only space before it.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailablePrefixAsText_SpacesBeforeSelection) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First paragraph text</p>
<p id='second'>
Second paragraph text
</p>
)HTML");
Node* second_paragraph = GetDocument().getElementById("second")->firstChild();
const auto& start = Position(second_paragraph, 6);
const auto& end = Position(second_paragraph, 13);
ASSERT_EQ("Second", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("First paragraph text",
GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailablePrefixAsTextForTesting(start));
}
// Check the case when previous node is used for available prefix when selection
// is not at index=0 but there is only invisible block.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailablePrefixAsText_InvisibleBeforeSelection) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First paragraph text</p>
<div id='second'>
<p id='invisible' style='display:none'>
invisible text
</p>
Second paragraph text
</div>
)HTML");
Node* second_paragraph =
GetDocument().getElementById("invisible")->nextSibling();
const auto& start = Position(second_paragraph, 6);
const auto& end = Position(second_paragraph, 13);
ASSERT_EQ("Second", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("First paragraph text",
GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailablePrefixAsTextForTesting(start));
}
// Similar test for suffix.
// Basic test case for |GetAvailableSuffixAsText|.
TEST_F(TextFragmentSelectorGeneratorTest, GetAvailableSuffixAsText) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First paragraph text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 5);
ASSERT_EQ("First", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("paragraph text", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
// Check the case when available suffix contains collapsible space.
TEST_F(TextFragmentSelectorGeneratorTest, GetAvailableSuffixAsText_ExtraSpace) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First paragraph
text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 5);
ASSERT_EQ("First", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("paragraph text", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
// Check the case when available suffix is complete text content of the next
// block.
TEST_F(TextFragmentSelectorGeneratorTest, GetAvailableSuffixAsText_NextNode) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First paragraph text</p>
<p id='second'>Second paragraph text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 20);
ASSERT_EQ("First paragraph text", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("Second paragraph text",
GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
// Check the case when there is a commented block between selection and the
// available suffix.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailableSuffixAsText_NextNode_WithComment) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First paragraph text</p>
<!--
multiline comment that should be ignored.
//-->
<p id='second'>Second paragraph text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 20);
ASSERT_EQ("First paragraph text", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("Second paragraph text",
GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
// Check the case when available suffix is a text node outside of selection
// block.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailableSuffixAsText_NextTextNode) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First paragraph text</p>
text
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 20);
ASSERT_EQ("First paragraph text", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("text", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
// Check the case when available suffix is a parent node text content outside of
// selection block.
TEST_F(TextFragmentSelectorGeneratorTest, GetAvailableSuffixAsText_ParentNode) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div><p id='first'>First paragraph text</p> nested</div>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 20);
ASSERT_EQ("First paragraph text", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("nested", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
// Check the case when available suffix contains non-block tag(e.g. <b>).
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailableSuffixAsText_NestedTextNode) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>First <b>bold text</b> paragraph text</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 5);
ASSERT_EQ("First", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("bold text paragraph text",
GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
// Check the case when available suffix is collected until nested block.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailableSuffixAsText_NestedBlock) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div id='first'>First paragraph <div id='div'>div</div> text</div>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 5);
ASSERT_EQ("First", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("paragraph", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
// Check the case when available suffix includes non-block element but stops at
// nested block.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailableSuffixAsText_NestedBlockInNestedText) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div id='first'>First <b>bold<div id='div'>div</div></b> paragraph text</div>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 5);
ASSERT_EQ("First", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("bold", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
// Check the case when available suffix includes invisible block.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailableSuffixAsText_NestedInvisibleBlock) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div id='first'>First <div id='div' style='display:none'>invisible</div> paragraph text</div>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 0);
const auto& end = Position(first_paragraph, 5);
ASSERT_EQ("First", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("paragraph text", GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
// Check the case when next node is used for available suffix when selection is
// not at last index but there is only space after it.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailableSuffixAsText_SpacesAfterSelection) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='first'>
First paragraph text
</p>
<p id='second'>
Second paragraph text
</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 23);
const auto& end = Position(first_paragraph, 27);
ASSERT_EQ("text", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("Second paragraph text",
GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
// Check the case when next node is used for available suffix when selection is
// not at last index but there is only invisible block after it.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailableSuffixAsText_InvisibleAfterSelection) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div id='first'>
First paragraph text
<div id='invisible' style='display:none'>
invisible text
</div>
</div>
<p id='second'>
Second paragraph text
</p>
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 23);
const auto& end = Position(first_paragraph, 27);
ASSERT_EQ("text", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("Second paragraph text",
GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
// Check the case when previous node is used for available prefix when selection
// is not at last index but there is only invisible block. Invisible block
// contains another block which also should be invisible.
TEST_F(TextFragmentSelectorGeneratorTest,
GetAvailableSuffixAsText_InvisibleAfterSelection_WithNestedInvisible) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div id='first'>
First paragraph text
<div id='invisible' style='display:none'>
invisible text
<div>
nested invisible text
</div
</div>
</div>
<p id='second'>
Second paragraph text
<div id='invisible' style='display:none'>
invisible text
<div>
nested invisible text
</div
</div>
</p>
test
)HTML");
Node* first_paragraph = GetDocument().getElementById("first")->firstChild();
const auto& start = Position(first_paragraph, 23);
const auto& end = Position(first_paragraph, 27);
ASSERT_EQ("text", PlainText(EphemeralRange(start, end)));
EXPECT_EQ("Second paragraph text",
GetDocument()
.GetFrame()
->GetTextFragmentSelectorGenerator()
->GetAvailableSuffixAsTextForTesting(end));
}
} // namespace blink
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment