| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/editing/editor.h" |
| |
| #include "third_party/blink/renderer/core/clipboard/system_clipboard.h" |
| #include "third_party/blink/renderer/core/dom/text.h" |
| #include "third_party/blink/renderer/core/editing/commands/editor_command.h" |
| #include "third_party/blink/renderer/core/editing/commands/undo_stack.h" |
| #include "third_party/blink/renderer/core/editing/frame_selection.h" |
| #include "third_party/blink/renderer/core/editing/selection_template.h" |
| #include "third_party/blink/renderer/core/editing/testing/editing_test_base.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/html/forms/html_input_element.h" |
| #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" |
| |
| namespace blink { |
| |
| class EditorTest : public EditingTestBase { |
| public: |
| void TearDown() override { |
| GetDocument().GetFrame()->GetSystemClipboard()->WritePlainText(String("")); |
| GetDocument().GetFrame()->GetSystemClipboard()->CommitWrite(); |
| EditingTestBase::TearDown(); |
| } |
| |
| Editor& GetEditor() const { return GetDocument().GetFrame()->GetEditor(); } |
| |
| void ExecuteCopy() { |
| Editor& editor = GetDocument().GetFrame()->GetEditor(); |
| editor.CreateCommand("Copy").Execute(); |
| test::RunPendingTasks(); |
| } |
| |
| ptrdiff_t SizeOfRedoStack() const { |
| return std::distance(GetEditor().GetUndoStack().RedoSteps().begin(), |
| GetEditor().GetUndoStack().RedoSteps().end()); |
| } |
| |
| ptrdiff_t SizeOfUndoStack() const { |
| return std::distance(GetEditor().GetUndoStack().UndoSteps().begin(), |
| GetEditor().GetUndoStack().UndoSteps().end()); |
| } |
| }; |
| |
| TEST_F(EditorTest, CanCopyCrossingShadowBoundary) { |
| const SelectionInDOMTree selection = SetSelectionTextToBody( |
| "<p><template data-mode=open>^abc</template></p><b>|</b>"); |
| Selection().SetSelection(selection, SetSelectionOptions()); |
| EXPECT_TRUE(GetDocument().GetFrame()->GetEditor().CanCopy()); |
| } |
| |
| TEST_F(EditorTest, copyGeneratedPassword) { |
| // Checks that if the password field has the value generated by Chrome |
| // (HTMLInputElement::shouldRevealPassword will be true), copying the field |
| // should be available. |
| const char* body_content = "<input type='password' id='password'></input>"; |
| SetBodyContent(body_content); |
| |
| auto& element = To<HTMLInputElement>( |
| *GetDocument().getElementById(AtomicString("password"))); |
| |
| const String kPasswordValue = "secret"; |
| element.Focus(); |
| element.SetValue(kPasswordValue); |
| element.SetSelectionRange(0, kPasswordValue.length()); |
| |
| Editor& editor = GetDocument().GetFrame()->GetEditor(); |
| EXPECT_FALSE(editor.CanCopy()); |
| |
| element.SetShouldRevealPassword(true); |
| EXPECT_TRUE(editor.CanCopy()); |
| } |
| |
| TEST_F(EditorTest, CopyVisibleSelection) { |
| const char* body_content = "<input id=hiding value=HEY>"; |
| SetBodyContent(body_content); |
| |
| auto& text_control = To<HTMLInputElement>( |
| *GetDocument().getElementById(AtomicString("hiding"))); |
| text_control.select(); |
| |
| ExecuteCopy(); |
| |
| const String copied = |
| GetDocument().GetFrame()->GetSystemClipboard()->ReadPlainText(); |
| EXPECT_EQ("HEY", copied); |
| } |
| |
| TEST_F(EditorTest, DontCopyHiddenSelections) { |
| const char* body_content = |
| "<input type=checkbox id=checkbox>" |
| "<input id=hiding value=HEY>"; |
| SetBodyContent(body_content); |
| |
| auto& text_control = To<HTMLInputElement>( |
| *GetDocument().getElementById(AtomicString("hiding"))); |
| text_control.select(); |
| |
| auto& checkbox = To<HTMLInputElement>( |
| *GetDocument().getElementById(AtomicString("checkbox"))); |
| checkbox.Focus(); |
| |
| ExecuteCopy(); |
| |
| const String copied = |
| GetDocument().GetFrame()->GetSystemClipboard()->ReadPlainText(); |
| EXPECT_TRUE(copied.empty()) << copied << " was copied."; |
| } |
| |
| TEST_F(EditorTest, ReplaceSelection) { |
| const char* body_content = "<input id=text value='HELLO'>"; |
| SetBodyContent(body_content); |
| |
| auto& text_control = |
| To<HTMLInputElement>(*GetDocument().getElementById(AtomicString("text"))); |
| text_control.select(); |
| text_control.SetSelectionRange(2, 2); |
| |
| Editor& editor = GetDocument().GetFrame()->GetEditor(); |
| editor.ReplaceSelection("NEW"); |
| |
| EXPECT_EQ("HENEWLLO", text_control.Value()); |
| } |
| |
| // http://crbug.com/263819 |
| TEST_F(EditorTest, RedoWithDisconnectedEditable) { |
| SetBodyContent("<p contenteditable id=target></p>"); |
| auto& target = *GetElementById("target"); |
| target.Focus(); |
| GetDocument().execCommand("insertHtml", false, "<b>xyz</b>", |
| ASSERT_NO_EXCEPTION); |
| ASSERT_EQ("<b>xyz</b>", target.innerHTML()); |
| ASSERT_EQ(0, SizeOfRedoStack()); |
| ASSERT_EQ(1, SizeOfUndoStack()); |
| |
| GetEditor().Undo(); |
| ASSERT_EQ(1, SizeOfRedoStack()); |
| ASSERT_EQ(0, SizeOfUndoStack()); |
| |
| target.remove(); |
| EXPECT_EQ(0, SizeOfRedoStack()) |
| << "We don't need to have redo steps for removed <input>"; |
| EXPECT_EQ(0, SizeOfUndoStack()); |
| } |
| |
| // http://crbug.com/263819 |
| TEST_F(EditorTest, RedoWithDisconnectedInput) { |
| SetBodyContent("<input id=target>"); |
| auto& input = *To<HTMLInputElement>(GetElementById("target")); |
| input.Focus(); |
| GetDocument().execCommand("insertText", false, "xyz", ASSERT_NO_EXCEPTION); |
| ASSERT_EQ("xyz", input.Value()); |
| ASSERT_EQ(0, SizeOfRedoStack()); |
| ASSERT_EQ(1, SizeOfUndoStack()); |
| |
| GetEditor().Undo(); |
| ASSERT_EQ(1, SizeOfRedoStack()); |
| ASSERT_EQ(0, SizeOfUndoStack()); |
| |
| input.remove(); |
| EXPECT_EQ(0, SizeOfRedoStack()) |
| << "We don't need to have redo steps for removed <input>"; |
| EXPECT_EQ(0, SizeOfUndoStack()); |
| } |
| |
| // http://crbug.com/263819 |
| TEST_F(EditorTest, UndoWithDisconnectedEditable) { |
| SetBodyContent("<p contenteditable id=target></p>"); |
| auto& target = *GetElementById("target"); |
| target.Focus(); |
| GetDocument().execCommand("insertHtml", false, "<b>xyz</b>", |
| ASSERT_NO_EXCEPTION); |
| ASSERT_EQ("<b>xyz</b>", target.innerHTML()); |
| ASSERT_EQ(0, SizeOfRedoStack()); |
| ASSERT_EQ(1, SizeOfUndoStack()); |
| |
| target.remove(); |
| EXPECT_EQ(0, SizeOfRedoStack()); |
| EXPECT_EQ(0, SizeOfUndoStack()) |
| << "We don't need to have undo steps for removed editable"; |
| } |
| |
| // http://crbug.com/263819 |
| TEST_F(EditorTest, UndoWithDisconnectedInput) { |
| SetBodyContent("<input id=target>"); |
| auto& input = *To<HTMLInputElement>(GetElementById("target")); |
| input.Focus(); |
| GetDocument().execCommand("insertText", false, "xyz", ASSERT_NO_EXCEPTION); |
| ASSERT_EQ("xyz", input.Value()); |
| ASSERT_EQ(0, SizeOfRedoStack()); |
| ASSERT_EQ(1, SizeOfUndoStack()); |
| |
| |
| input.remove(); |
| EXPECT_EQ(0, SizeOfRedoStack()); |
| EXPECT_EQ(0, SizeOfUndoStack()) |
| << "We don't need to have undo steps for removed <input>"; |
| } |
| |
| // http://crbug.com/873037 |
| TEST_F(EditorTest, UndoWithInvalidSelection) { |
| const SelectionInDOMTree selection = SetSelectionTextToBody( |
| "<div contenteditable><div></div><b>^abc|</b></div>"); |
| Selection().SetSelection(selection, SetSelectionOptions()); |
| auto& abc = To<Text>(*selection.Anchor().ComputeContainerNode()); |
| // Push Text node "abc" into undo stack |
| GetDocument().execCommand("italic", false, "", ASSERT_NO_EXCEPTION); |
| // Change Text node "abc" in undo stack |
| abc.setData(""); |
| GetDocument().GetFrame()->GetEditor().Undo(); |
| EXPECT_EQ("<div contenteditable><div></div><b>|</b></div>", |
| GetSelectionTextFromBody()); |
| } |
| |
| } // namespace blink |