blob: 66452b447c617e740dbc4ff8279550965c47e8bf [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "pdf/pdfium/pdfium_engine.h"
#include <stdint.h>
#include "base/hash/md5.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "pdf/document_attachment_info.h"
#include "pdf/document_layout.h"
#include "pdf/document_metadata.h"
#include "pdf/pdf_features.h"
#include "pdf/pdfium/pdfium_page.h"
#include "pdf/pdfium/pdfium_test_base.h"
#include "pdf/test/test_client.h"
#include "pdf/test/test_document_loader.h"
#include "pdf/test/test_utils.h"
#include "ppapi/c/ppb_input_event.h"
#include "ppapi/cpp/size.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/size.h"
namespace chrome_pdf {
namespace {
using ::testing::InSequence;
using ::testing::Invoke;
using ::testing::IsEmpty;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::StrictMock;
MATCHER_P2(LayoutWithSize, width, height, "") {
return arg.size() == gfx::Size(width, height);
}
MATCHER_P(LayoutWithOptions, options, "") {
return arg.options() == options;
}
class MockTestClient : public TestClient {
public:
MockTestClient() {
ON_CALL(*this, ProposeDocumentLayout)
.WillByDefault([this](const DocumentLayout& layout) {
TestClient::ProposeDocumentLayout(layout);
});
}
MOCK_METHOD(void,
ProposeDocumentLayout,
(const DocumentLayout& layout),
(override));
MOCK_METHOD(void, ScrollToPage, (int page), (override));
};
} // namespace
class PDFiumEngineTest : public PDFiumTestBase {
protected:
void ExpectPageRect(const PDFiumEngine& engine,
size_t page_index,
const pp::Rect& expected_rect) {
const PDFiumPage& page = GetPDFiumPageForTest(engine, page_index);
CompareRect(expected_rect, page.rect());
}
// Tries to load a PDF incrementally, returning `true` if the PDF actually was
// loaded incrementally. Note that this function will return `false` if
// incremental loading fails, but also if incremental loading is disabled.
bool TryLoadIncrementally() {
NiceMock<MockTestClient> client;
InitializeEngineResult initialize_result = InitializeEngineWithoutLoading(
&client, FILE_PATH_LITERAL("linearized.pdf"));
if (!initialize_result.engine) {
ADD_FAILURE();
return false;
}
PDFiumEngine& engine = *initialize_result.engine;
// Load enough for the document to become partially available.
initialize_result.document_loader->SimulateLoadData(8192);
bool loaded_incrementally;
if (engine.GetNumberOfPages() == 0) {
// This is not necessarily a test failure; it just indicates incremental
// loading is not occurring.
loaded_incrementally = false;
} else {
// Note: Plugin size chosen so all pages of the document are visible. The
// engine only updates availability incrementally for visible pages.
EXPECT_EQ(0, CountAvailablePages(engine));
engine.PluginSizeUpdated({1024, 4096});
int available_pages = CountAvailablePages(engine);
loaded_incrementally =
0 < available_pages && available_pages < engine.GetNumberOfPages();
}
// Verify that loading can finish.
while (initialize_result.document_loader->SimulateLoadData(UINT32_MAX))
continue;
EXPECT_EQ(engine.GetNumberOfPages(), CountAvailablePages(engine));
return loaded_incrementally;
}
private:
// Counts the number of available pages. Returns `int` instead of `size_t` for
// consistency with `PDFiumEngine::GetNumberOfPages()`.
int CountAvailablePages(const PDFiumEngine& engine) {
int available_pages = 0;
for (int i = 0; i < engine.GetNumberOfPages(); ++i) {
if (GetPDFiumPageForTest(engine, i).available())
++available_pages;
}
return available_pages;
}
};
TEST_F(PDFiumEngineTest, InitializeWithRectanglesMultiPagesPdf) {
NiceMock<MockTestClient> client;
// ProposeDocumentLayout() gets called twice during loading because
// PDFiumEngine::ContinueLoadingDocument() calls LoadBody() (which eventually
// triggers a layout proposal), and then calls FinishLoadingDocument() (since
// the document is complete), which calls LoadBody() again. Coalescing these
// proposals is not correct unless we address the issue covered by
// PDFiumEngineTest.ProposeDocumentLayoutWithOverlap.
EXPECT_CALL(client, ProposeDocumentLayout(LayoutWithSize(343, 1664)))
.Times(2);
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("rectangles_multi_pages.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(5, engine->GetNumberOfPages());
ExpectPageRect(*engine, 0, {38, 3, 266, 333});
ExpectPageRect(*engine, 1, {5, 350, 333, 266});
ExpectPageRect(*engine, 2, {38, 630, 266, 333});
ExpectPageRect(*engine, 3, {38, 977, 266, 333});
ExpectPageRect(*engine, 4, {38, 1324, 266, 333});
}
TEST_F(PDFiumEngineTest, InitializeWithRectanglesMultiPagesPdfInTwoUpView) {
NiceMock<MockTestClient> client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("rectangles_multi_pages.pdf"));
ASSERT_TRUE(engine);
DocumentLayout::Options options;
options.set_two_up_view_enabled(true);
EXPECT_CALL(client, ProposeDocumentLayout(LayoutWithOptions(options)))
.WillOnce(Return());
engine->SetTwoUpView(true);
engine->ApplyDocumentLayout(options);
ASSERT_EQ(5, engine->GetNumberOfPages());
ExpectPageRect(*engine, 0, {72, 3, 266, 333});
ExpectPageRect(*engine, 1, {340, 3, 333, 266});
ExpectPageRect(*engine, 2, {72, 346, 266, 333});
ExpectPageRect(*engine, 3, {340, 346, 266, 333});
ExpectPageRect(*engine, 4, {68, 689, 266, 333});
}
TEST_F(PDFiumEngineTest, AppendBlankPagesWithFewerPages) {
NiceMock<MockTestClient> client;
{
InSequence normal_then_append;
EXPECT_CALL(client, ProposeDocumentLayout(LayoutWithSize(343, 1664)))
.Times(2);
EXPECT_CALL(client, ProposeDocumentLayout(LayoutWithSize(276, 1037)));
}
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("rectangles_multi_pages.pdf"));
ASSERT_TRUE(engine);
engine->AppendBlankPages(3);
ASSERT_EQ(3, engine->GetNumberOfPages());
ExpectPageRect(*engine, 0, {5, 3, 266, 333});
ExpectPageRect(*engine, 1, {5, 350, 266, 333});
ExpectPageRect(*engine, 2, {5, 697, 266, 333});
}
TEST_F(PDFiumEngineTest, AppendBlankPagesWithMorePages) {
NiceMock<MockTestClient> client;
{
InSequence normal_then_append;
EXPECT_CALL(client, ProposeDocumentLayout(LayoutWithSize(343, 1664)))
.Times(2);
EXPECT_CALL(client, ProposeDocumentLayout(LayoutWithSize(276, 2425)));
}
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("rectangles_multi_pages.pdf"));
ASSERT_TRUE(engine);
engine->AppendBlankPages(7);
ASSERT_EQ(7, engine->GetNumberOfPages());
ExpectPageRect(*engine, 0, {5, 3, 266, 333});
ExpectPageRect(*engine, 1, {5, 350, 266, 333});
ExpectPageRect(*engine, 2, {5, 697, 266, 333});
ExpectPageRect(*engine, 3, {5, 1044, 266, 333});
ExpectPageRect(*engine, 4, {5, 1391, 266, 333});
ExpectPageRect(*engine, 5, {5, 1738, 266, 333});
ExpectPageRect(*engine, 6, {5, 2085, 266, 333});
}
TEST_F(PDFiumEngineTest, ProposeDocumentLayoutWithOverlap) {
NiceMock<MockTestClient> client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("rectangles_multi_pages.pdf"));
ASSERT_TRUE(engine);
EXPECT_CALL(client, ProposeDocumentLayout(LayoutWithSize(343, 1463)))
.WillOnce(Return());
engine->RotateClockwise();
EXPECT_CALL(client, ProposeDocumentLayout(LayoutWithSize(343, 1664)))
.WillOnce(Return());
engine->RotateCounterclockwise();
}
TEST_F(PDFiumEngineTest, ApplyDocumentLayoutAvoidsInfiniteLoop) {
NiceMock<MockTestClient> client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("rectangles_multi_pages.pdf"));
ASSERT_TRUE(engine);
DocumentLayout::Options options;
EXPECT_CALL(client, ScrollToPage(-1)).Times(0);
EXPECT_EQ(gfx::Size(343, 1664), engine->ApplyDocumentLayout(options));
options.RotatePagesClockwise();
EXPECT_CALL(client, ScrollToPage(-1)).Times(1);
EXPECT_EQ(gfx::Size(343, 1463), engine->ApplyDocumentLayout(options));
EXPECT_EQ(gfx::Size(343, 1463), engine->ApplyDocumentLayout(options));
}
TEST_F(PDFiumEngineTest, GetDocumentAttachments) {
NiceMock<MockTestClient> client;
std::unique_ptr<PDFiumEngine> engine =
InitializeEngine(&client, FILE_PATH_LITERAL("embedded_attachments.pdf"));
ASSERT_TRUE(engine);
const std::vector<DocumentAttachmentInfo>& attachments =
engine->GetDocumentAttachmentInfoList();
ASSERT_EQ(3u, attachments.size());
{
const DocumentAttachmentInfo& attachment = attachments[0];
EXPECT_EQ("1.txt", base::UTF16ToUTF8(attachment.name));
EXPECT_TRUE(attachment.is_readable);
EXPECT_EQ(4u, attachment.size_bytes);
EXPECT_EQ("D:20170712214438-07'00'",
base::UTF16ToUTF8(attachment.creation_date));
EXPECT_EQ("D:20160115091400", base::UTF16ToUTF8(attachment.modified_date));
std::vector<uint8_t> content = engine->GetAttachmentData(0);
ASSERT_EQ(attachment.size_bytes, content.size());
std::string content_str(content.begin(), content.end());
EXPECT_EQ("test", content_str);
}
{
static constexpr char kCheckSum[] = "72afcddedf554dda63c0c88e06f1ce18";
const DocumentAttachmentInfo& attachment = attachments[1];
EXPECT_EQ("attached.pdf", base::UTF16ToUTF8(attachment.name));
EXPECT_TRUE(attachment.is_readable);
EXPECT_EQ(5869u, attachment.size_bytes);
EXPECT_EQ("D:20170712214443-07'00'",
base::UTF16ToUTF8(attachment.creation_date));
EXPECT_EQ("D:20170712214410", base::UTF16ToUTF8(attachment.modified_date));
std::vector<uint8_t> content = engine->GetAttachmentData(1);
ASSERT_EQ(attachment.size_bytes, content.size());
// The whole attachment content is too long to do string comparison.
// Instead, we only verify the checksum value here.
base::MD5Digest hash;
base::MD5Sum(content.data(), content.size(), &hash);
EXPECT_EQ(kCheckSum, base::MD5DigestToBase16(hash));
}
{
// Test attachments with no creation date or last modified date.
const DocumentAttachmentInfo& attachment = attachments[2];
EXPECT_EQ("附錄.txt", base::UTF16ToUTF8(attachment.name));
EXPECT_TRUE(attachment.is_readable);
EXPECT_EQ(5u, attachment.size_bytes);
EXPECT_THAT(attachment.creation_date, IsEmpty());
EXPECT_THAT(attachment.modified_date, IsEmpty());
std::vector<uint8_t> content = engine->GetAttachmentData(2);
ASSERT_EQ(attachment.size_bytes, content.size());
std::string content_str(content.begin(), content.end());
EXPECT_EQ("test\n", content_str);
}
}
TEST_F(PDFiumEngineTest, DocumentWithInvalidAttachment) {
NiceMock<MockTestClient> client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("embedded_attachments_invalid_data.pdf"));
ASSERT_TRUE(engine);
const std::vector<DocumentAttachmentInfo>& attachments =
engine->GetDocumentAttachmentInfoList();
ASSERT_EQ(1u, attachments.size());
// Test on an attachment which FPDFAttachment_GetFile() fails to retrieve data
// from.
const DocumentAttachmentInfo& attachment = attachments[0];
EXPECT_EQ("1.txt", base::UTF16ToUTF8(attachment.name));
EXPECT_FALSE(attachment.is_readable);
EXPECT_EQ(0u, attachment.size_bytes);
EXPECT_THAT(attachment.creation_date, IsEmpty());
EXPECT_THAT(attachment.modified_date, IsEmpty());
}
TEST_F(PDFiumEngineTest, NoDocumentAttachmentInfo) {
NiceMock<MockTestClient> client;
std::unique_ptr<PDFiumEngine> engine =
InitializeEngine(&client, FILE_PATH_LITERAL("hello_world2.pdf"));
ASSERT_TRUE(engine);
EXPECT_EQ(0u, engine->GetDocumentAttachmentInfoList().size());
}
TEST_F(PDFiumEngineTest, GetDocumentMetadata) {
NiceMock<MockTestClient> client;
std::unique_ptr<PDFiumEngine> engine =
InitializeEngine(&client, FILE_PATH_LITERAL("document_info.pdf"));
ASSERT_TRUE(engine);
const DocumentMetadata& doc_metadata = engine->GetDocumentMetadata();
EXPECT_EQ(PdfVersion::k1_7, doc_metadata.version);
EXPECT_EQ("Sample PDF Document Info", doc_metadata.title);
EXPECT_EQ("Chromium Authors", doc_metadata.author);
EXPECT_EQ("Testing", doc_metadata.subject);
EXPECT_EQ("Your Preferred Text Editor", doc_metadata.creator);
EXPECT_EQ("fixup_pdf_template.py", doc_metadata.producer);
}
TEST_F(PDFiumEngineTest, GetEmptyDocumentMetadata) {
NiceMock<MockTestClient> client;
std::unique_ptr<PDFiumEngine> engine =
InitializeEngine(&client, FILE_PATH_LITERAL("hello_world2.pdf"));
ASSERT_TRUE(engine);
const DocumentMetadata& doc_metadata = engine->GetDocumentMetadata();
EXPECT_EQ(PdfVersion::k1_7, doc_metadata.version);
EXPECT_THAT(doc_metadata.title, IsEmpty());
EXPECT_THAT(doc_metadata.author, IsEmpty());
EXPECT_THAT(doc_metadata.subject, IsEmpty());
EXPECT_THAT(doc_metadata.creator, IsEmpty());
EXPECT_THAT(doc_metadata.producer, IsEmpty());
}
TEST_F(PDFiumEngineTest, GetBadPdfVersion) {
NiceMock<MockTestClient> client;
std::unique_ptr<PDFiumEngine> engine =
InitializeEngine(&client, FILE_PATH_LITERAL("bad_version.pdf"));
ASSERT_TRUE(engine);
const DocumentMetadata& doc_metadata = engine->GetDocumentMetadata();
EXPECT_EQ(PdfVersion::kUnknown, doc_metadata.version);
}
TEST_F(PDFiumEngineTest, IncrementalLoadingFeatureDefault) {
EXPECT_TRUE(TryLoadIncrementally());
}
TEST_F(PDFiumEngineTest, IncrementalLoadingFeatureEnabled) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(features::kPdfIncrementalLoading);
EXPECT_TRUE(TryLoadIncrementally());
}
TEST_F(PDFiumEngineTest, IncrementalLoadingFeatureDisabled) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(features::kPdfIncrementalLoading);
EXPECT_FALSE(TryLoadIncrementally());
}
class TabbingTestClient : public TestClient {
public:
TabbingTestClient() = default;
~TabbingTestClient() override = default;
TabbingTestClient(const TabbingTestClient&) = delete;
TabbingTestClient& operator=(const TabbingTestClient&) = delete;
// Mock PDFEngine::Client methods.
MOCK_METHOD(void, DocumentFocusChanged, (bool), (override));
};
class PDFiumEngineTabbingTest : public PDFiumTestBase {
public:
PDFiumEngineTabbingTest() = default;
~PDFiumEngineTabbingTest() override = default;
PDFiumEngineTabbingTest(const PDFiumEngineTabbingTest&) = delete;
PDFiumEngineTabbingTest& operator=(const PDFiumEngineTabbingTest&) = delete;
bool HandleTabEvent(PDFiumEngine* engine, uint32_t modifiers) {
return engine->HandleTabEvent(modifiers);
}
PDFiumEngine::FocusElementType GetFocusedElementType(PDFiumEngine* engine) {
return engine->focus_item_type_;
}
int GetLastFocusedPage(PDFiumEngine* engine) {
return engine->last_focused_page_;
}
PDFiumEngine::FocusElementType GetLastFocusedElementType(
PDFiumEngine* engine) {
return engine->last_focused_item_type_;
}
int GetLastFocusedAnnotationIndex(PDFiumEngine* engine) {
return engine->last_focused_annot_index_;
}
bool IsInFormTextArea(PDFiumEngine* engine) {
return engine->in_form_text_area_;
}
size_t GetSelectionSize(PDFiumEngine* engine) {
return engine->selection_.size();
}
const std::string& GetLinkUnderCursor(PDFiumEngine* engine) {
return engine->link_under_cursor_;
}
void ScrollFocusedAnnotationIntoView(PDFiumEngine* engine) {
engine->ScrollFocusedAnnotationIntoView();
}
protected:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
};
TEST_F(PDFiumEngineTabbingTest, LinkUnderCursorTest) {
/*
* Document structure
* Document
* ++ Page 1
* ++++ Widget annotation
* ++++ Widget annotation
* ++++ Highlight annotation
* ++++ Link annotation
*/
// Enable feature flag.
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
chrome_pdf::features::kTabAcrossPDFAnnotations);
TestClient client;
std::unique_ptr<PDFiumEngine> engine =
InitializeEngine(&client, FILE_PATH_LITERAL("annots.pdf"));
ASSERT_TRUE(engine);
// Initial value of link under cursor.
EXPECT_EQ("", GetLinkUnderCursor(engine.get()));
// Tab through non-link annotations and validate link under cursor.
for (int i = 0; i < 4; i++) {
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ("", GetLinkUnderCursor(engine.get()));
}
// Tab to Link annotation.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ("https://www.google.com/", GetLinkUnderCursor(engine.get()));
// Tab to previous annotation.
ASSERT_TRUE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ("", GetLinkUnderCursor(engine.get()));
}
TEST_F(PDFiumEngineTabbingTest, TabbingSupportedAnnots) {
/*
* Document structure
* Document
* ++ Page 1
* ++++ Widget annotation
* ++++ Widget annotation
* ++++ Highlight annotation
* ++++ Link annotation
*/
// Enable feature flag.
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
chrome_pdf::features::kTabAcrossPDFAnnotations);
TestClient client;
std::unique_ptr<PDFiumEngine> engine =
InitializeEngine(&client, FILE_PATH_LITERAL("annots.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(1, engine->GetNumberOfPages());
ASSERT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
ASSERT_FALSE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
}
TEST_F(PDFiumEngineTabbingTest, TabbingForwardTest) {
/*
* Document structure
* Document
* ++ Page 1
* ++++ Annotation
* ++++ Annotation
* ++ Page 2
* ++++ Annotation
*/
TabbingTestClient client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("annotation_form_fields.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(2, engine->GetNumberOfPages());
static constexpr bool kExpectedFocusState[] = {true, false};
{
InSequence sequence;
for (auto focused : kExpectedFocusState)
EXPECT_CALL(client, DocumentFocusChanged(focused));
}
ASSERT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(1, GetLastFocusedPage(engine.get()));
ASSERT_FALSE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
}
TEST_F(PDFiumEngineTabbingTest, TabbingBackwardTest) {
/*
* Document structure
* Document
* ++ Page 1
* ++++ Annotation
* ++++ Annotation
* ++ Page 2
* ++++ Annotation
*/
TabbingTestClient client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("annotation_form_fields.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(2, engine->GetNumberOfPages());
static constexpr bool kExpectedFocusState[] = {true, false};
{
InSequence sequence;
for (auto focused : kExpectedFocusState)
EXPECT_CALL(client, DocumentFocusChanged(focused));
}
ASSERT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(1, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
ASSERT_TRUE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
}
TEST_F(PDFiumEngineTabbingTest, TabbingWithModifiers) {
/*
* Document structure
* Document
* ++ Page 1
* ++++ Annotation
* ++++ Annotation
* ++ Page 2
* ++++ Annotation
*/
TestClient client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("annotation_form_fields.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(2, engine->GetNumberOfPages());
ASSERT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
// Tabbing with ctrl modifier.
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_CONTROLKEY));
// Tabbing with alt modifier.
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_ALTKEY));
// Tab to bring document into focus.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
// Tabbing with ctrl modifier.
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_CONTROLKEY));
// Tabbing with alt modifier.
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_ALTKEY));
// Tab to bring first page into focus.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
// Tabbing with ctrl modifier.
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_CONTROLKEY));
// Tabbing with alt modifier.
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_ALTKEY));
}
TEST_F(PDFiumEngineTabbingTest, NoFocusableItemTabbingTest) {
/*
* Document structure
* Document
* ++ Page 1
* ++ Page 2
*/
TabbingTestClient client;
std::unique_ptr<PDFiumEngine> engine =
InitializeEngine(&client, FILE_PATH_LITERAL("hello_world2.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(2, engine->GetNumberOfPages());
static constexpr bool kExpectedFocusState[] = {true, false, true, false};
{
InSequence sequence;
for (auto focused : kExpectedFocusState)
EXPECT_CALL(client, DocumentFocusChanged(focused));
}
ASSERT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
// Tabbing forward.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
ASSERT_FALSE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
// Tabbing backward.
ASSERT_TRUE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
ASSERT_FALSE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
}
TEST_F(PDFiumEngineTabbingTest, RestoringDocumentFocusTest) {
/*
* Document structure
* Document
* ++ Page 1
* ++++ Annotation
* ++++ Annotation
* ++ Page 2
* ++++ Annotation
*/
TabbingTestClient client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("annotation_form_fields.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(2, engine->GetNumberOfPages());
static constexpr bool kExpectedFocusState[] = {true, false, true};
{
InSequence sequence;
for (auto focused : kExpectedFocusState)
EXPECT_CALL(client, DocumentFocusChanged(focused));
}
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
// Tabbing to bring the document into focus.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
engine->UpdateFocus(/*has_focus=*/false);
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetLastFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedAnnotationIndex(engine.get()));
engine->UpdateFocus(/*has_focus=*/true);
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
}
TEST_F(PDFiumEngineTabbingTest, RestoringAnnotFocusTest) {
/*
* Document structure
* Document
* ++ Page 1
* ++++ Annotation
* ++++ Annotation
* ++ Page 2
* ++++ Annotation
*/
TabbingTestClient client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("annotation_form_fields.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(2, engine->GetNumberOfPages());
static constexpr bool kExpectedFocusState[] = {true, false};
{
InSequence sequence;
for (auto focused : kExpectedFocusState)
EXPECT_CALL(client, DocumentFocusChanged(focused));
}
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
// Tabbing to bring last annotation of page 0 into focus.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
engine->UpdateFocus(/*has_focus=*/false);
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetLastFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedAnnotationIndex(engine.get()));
engine->UpdateFocus(/*has_focus=*/true);
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
// Tabbing now should bring the second page's annotation to focus.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(1, GetLastFocusedPage(engine.get()));
}
TEST_F(PDFiumEngineTabbingTest, VerifyFormFieldStatesOnTabbing) {
/*
* Document structure
* Document
* ++ Page 1
* ++++ Annotation (Text Field)
* ++++ Annotation (Radio Button)
*/
TestClient client;
std::unique_ptr<PDFiumEngine> engine =
InitializeEngine(&client, FILE_PATH_LITERAL("annots.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(1, engine->GetNumberOfPages());
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
// Bring focus to the text field.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
EXPECT_TRUE(IsInFormTextArea(engine.get()));
EXPECT_TRUE(engine->CanEditText());
// Bring focus to the button.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
EXPECT_FALSE(IsInFormTextArea(engine.get()));
EXPECT_FALSE(engine->CanEditText());
}
TEST_F(PDFiumEngineTabbingTest, ClearSelectionOnFocusInFormTextArea) {
TestClient client;
std::unique_ptr<PDFiumEngine> engine =
InitializeEngine(&client, FILE_PATH_LITERAL("form_text_fields.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(1, engine->GetNumberOfPages());
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
// Select all text.
engine->SelectAll();
EXPECT_EQ(1u, GetSelectionSize(engine.get()));
// Tab to bring focus to a form text area annotation.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
EXPECT_EQ(0u, GetSelectionSize(engine.get()));
}
TEST_F(PDFiumEngineTabbingTest, RetainSelectionOnFocusNotInFormTextArea) {
TestClient client;
std::unique_ptr<PDFiumEngine> engine =
InitializeEngine(&client, FILE_PATH_LITERAL("annots.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(1, engine->GetNumberOfPages());
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
// Select all text.
engine->SelectAll();
EXPECT_EQ(1u, GetSelectionSize(engine.get()));
// Tab to bring focus to a non form text area annotation (Button).
ASSERT_TRUE(HandleTabEvent(engine.get(), PP_INPUTEVENT_MODIFIER_SHIFTKEY));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
EXPECT_EQ(1u, GetSelectionSize(engine.get()));
}
class ScrollingTestClient : public TestClient {
public:
ScrollingTestClient() = default;
~ScrollingTestClient() override = default;
ScrollingTestClient(const ScrollingTestClient&) = delete;
ScrollingTestClient& operator=(const ScrollingTestClient&) = delete;
// Mock PDFEngine::Client methods.
MOCK_METHOD(void, ScrollToX, (int), (override));
MOCK_METHOD(void, ScrollToY, (int, bool), (override));
};
TEST_F(PDFiumEngineTabbingTest, MaintainViewportWhenFocusIsUpdated) {
StrictMock<ScrollingTestClient> client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("annotation_form_fields.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(2, engine->GetNumberOfPages());
engine->PluginSizeUpdated(gfx::Size(60, 40));
{
InSequence sequence;
static constexpr PP_Point kScrollValue = {510, 478};
EXPECT_CALL(client, ScrollToY(kScrollValue.y, false))
.WillOnce(Invoke(
[&engine]() { engine->ScrolledToYPosition(kScrollValue.y); }));
EXPECT_CALL(client, ScrollToX(kScrollValue.x)).WillOnce(Invoke([&engine]() {
engine->ScrolledToXPosition(kScrollValue.x);
}));
}
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
// Tabbing to bring the document into focus.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
// Tab to an annotation.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
// Scroll focused annotation out of viewport.
static constexpr PP_Point kScrollPosition = {242, 746};
engine->ScrolledToXPosition(kScrollPosition.x);
engine->ScrolledToYPosition(kScrollPosition.y);
engine->UpdateFocus(/*has_focus=*/false);
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetLastFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(1, GetLastFocusedAnnotationIndex(engine.get()));
// Restore focus, we shouldn't have any calls to scroll viewport.
engine->UpdateFocus(/*has_focus=*/true);
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
EXPECT_EQ(0, GetLastFocusedPage(engine.get()));
}
TEST_F(PDFiumEngineTabbingTest, ScrollFocusedAnnotationIntoView) {
StrictMock<ScrollingTestClient> client;
std::unique_ptr<PDFiumEngine> engine = InitializeEngine(
&client, FILE_PATH_LITERAL("annotation_form_fields.pdf"));
ASSERT_TRUE(engine);
ASSERT_EQ(2, engine->GetNumberOfPages());
engine->PluginSizeUpdated(gfx::Size(60, 40));
{
InSequence sequence;
static constexpr PP_Point kScrollValues[] = {{510, 478}, {510, 478}};
for (const auto& scroll_value : kScrollValues) {
EXPECT_CALL(client, ScrollToY(scroll_value.y, false))
.WillOnce(Invoke([&engine, &scroll_value]() {
engine->ScrolledToYPosition(scroll_value.y);
}));
EXPECT_CALL(client, ScrollToX(scroll_value.x))
.WillOnce(Invoke([&engine, &scroll_value]() {
engine->ScrolledToXPosition(scroll_value.x);
}));
}
}
EXPECT_EQ(PDFiumEngine::FocusElementType::kNone,
GetFocusedElementType(engine.get()));
EXPECT_EQ(-1, GetLastFocusedPage(engine.get()));
// Tabbing to bring the document into focus.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kDocument,
GetFocusedElementType(engine.get()));
// Tab to an annotation.
ASSERT_TRUE(HandleTabEvent(engine.get(), 0));
EXPECT_EQ(PDFiumEngine::FocusElementType::kPage,
GetFocusedElementType(engine.get()));
// Scroll focused annotation out of viewport.
static constexpr PP_Point kScrollPosition = {242, 746};
engine->ScrolledToXPosition(kScrollPosition.x);
engine->ScrolledToYPosition(kScrollPosition.y);
// Scroll the focused annotation into view.
ScrollFocusedAnnotationIntoView(engine.get());
}
} // namespace chrome_pdf