diff --git a/CMakeLists.txt b/CMakeLists.txt index 57cf9459..9e53fbb2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,3 +76,7 @@ install(FILES org.sailfish.office.service DESTINATION ${CMAKE_INSTALL_PREFIX}/sh install(FILES org.sailfish.office.xml DESTINATION ${CMAKE_INSTALL_PREFIX}/share/dbus-1/interfaces) install(FILES sailfish-office.privileges DESTINATION ${CMAKE_INSTALL_PREFIX}/share/mapplauncherd/privileges.d) +install(FILES tests/tst_PDFDocumentPage.qml DESTINATION /opt/tests/sailfish-office) +install(FILES tests/data/broken.pdf DESTINATION /opt/tests/sailfish-office/data) +install(FILES tests/data/protected.pdf DESTINATION /opt/tests/sailfish-office/data) +install(FILES tests/data/sample.pdf DESTINATION /opt/tests/sailfish-office/data) \ No newline at end of file diff --git a/pdf/pdfjob.cpp b/pdf/pdfjob.cpp index e34aa5b0..bf1fc186 100644 --- a/pdf/pdfjob.cpp +++ b/pdf/pdfjob.cpp @@ -20,6 +20,7 @@ #include #include +#include #include LoadDocumentJob::LoadDocumentJob(const QString &source) @@ -115,6 +116,15 @@ void RenderPageJob::run() { Q_ASSERT(m_document); + if (m_document->isLocked()) { + qWarning() << QStringLiteral("Rendering of a locked document."); + return; + } + if (m_index >= m_document->numPages()) { + qWarning() << QStringLiteral("Rendering of page %1 in a doument with %2 pages.").arg(m_index).arg(m_document->numPages()); + return; + } + Poppler::Page *page = m_document->page(m_index); QSizeF size = page->pageSizeF(); float scale = 72.0f * (float(m_width) / size.width()); diff --git a/pdf/pdfrenderthread.cpp b/pdf/pdfrenderthread.cpp index 9ed269f3..ca3a9d92 100644 --- a/pdf/pdfrenderthread.cpp +++ b/pdf/pdfrenderthread.cpp @@ -447,7 +447,16 @@ void PDFRenderThreadQueue::processPendingJob() case PDFJob::LoadDocumentJob: d->loadFailure = false; break; + case PDFJob::UnLockDocumentJob: + job->m_document = d->document; + break; default: + // Avoid treating rendering jobs of a locked document + // because of a race condition when reusing pdfcanvas.cpp + // with the same PdfDocument instance while changing the actual + // PDF it is working on. + if (d->document->isLocked()) + return; job->m_document = d->document; break; } diff --git a/plugin/PDFDocumentPage.qml b/plugin/PDFDocumentPage.qml index c9067876..1ada0575 100644 --- a/plugin/PDFDocumentPage.qml +++ b/plugin/PDFDocumentPage.qml @@ -27,6 +27,12 @@ import "PDFStorage.js" as PDFStorage DocumentPage { id: base + // Group of accessors properties. Can be used to control the PDFDocumentPage + // from outside or for testing purposes. + property alias document: pdfDocument + property alias placeHolder: placeHolderLoader + property alias toolbar: toolbar + property var _settings // Handle save and restore the view settings using PDFStorage property ContextMenu contextMenuLinks property ContextMenu contextMenuText @@ -90,6 +96,7 @@ DocumentPage { } Loader { + id: placeHolderLoader parent: base sourceComponent: (pdfDocument.failure || pdfDocument.locked) ? placeholderComponent : null anchors.verticalCenter: parent.verticalCenter @@ -171,6 +178,11 @@ DocumentPage { property Notification notice + // Group of accessors properties. Can be used to control the toolbar + // from outside or for testing purposes. + property alias searchIconized: search.iconized + property alias searchText: search.text + width: parent.width height: base.orientation == Orientation.Portrait || base.orientation == Orientation.InvertedPortrait diff --git a/plugin/PDFView.qml b/plugin/PDFView.qml index 4636a4e9..6a254d88 100644 --- a/plugin/PDFView.qml +++ b/plugin/PDFView.qml @@ -36,6 +36,9 @@ SilicaFlickable { property alias selectionDraggable: selectionView.draggable property bool canMoveBack: (_contentYAtGotoLink >= 0) + // Accessor currently used for testing purposes. + property bool scrolling: scrollAnimation.running || quickScrollAnimation.running + property bool scaled: pdfCanvas.width != width property QtObject _feedbackEffect @@ -97,12 +100,8 @@ SilicaFlickable { _searchIndex = index var match = searchDisplay.itemAt(index) - var cX = match.x + match.width / 2. - width / 2. - cX = Math.max(0, Math.min(cX, pdfCanvas.width - width)) - var cY = match.y + match.height / 2. - height / 2. - cY = Math.max(0, Math.min(cY, pdfCanvas.height - height)) - - scrollTo(Qt.point(cX, cY), match.page, match) + scrollTo(Qt.point(match.x + match.width / 2. - width / 2., + match.y + match.height / 2. - height / 2.), match.page, match) } function nextSearchMatch() { @@ -122,24 +121,27 @@ SilicaFlickable { } function scrollTo(pt, pageId, focusItem) { + // Ensure that pt can be reached. + var ctX = Math.max(0, Math.min(contentWidth - width, pt.x)) + var ctY = Math.max(0, Math.min(contentHeight - height, pt.y)) if ((pt.y < base.contentY + base.height && pt.y > base.contentY - base.height) && (pt.x < base.contentX + base.width && pt.x > base.contentX - base.width)) { - scrollX.to = pt.x - scrollY.to = pt.y + scrollX.to = ctX + scrollY.to = ctY scrollAnimation.focusItem = (focusItem !== undefined) ? focusItem : null scrollAnimation.start() } else { - var deltaY = pt.y - base.contentY + var deltaY = ctY - base.contentY if (deltaY < 0) { deltaY = Math.max(deltaY / 2., -base.height / 2.) } else { deltaY = Math.min(deltaY / 2., base.height / 2.) } - leaveX.to = (base.contentX + pt.x) / 2 + leaveX.to = (base.contentX + ctX) / 2 leaveY.to = base.contentY + deltaY - returnX.to = pt.x - returnY.from = pt.y - deltaY - returnY.to = pt.y + returnX.to = ctX + returnY.from = ctY - deltaY + returnY.to = ctY quickScrollAnimation.pageTo = pageId quickScrollAnimation.focusItem = (focusItem !== undefined) ? focusItem : null quickScrollAnimation.start() @@ -439,20 +441,14 @@ SilicaFlickable { if (left !== undefined && left >= 0.) { scrollX = rect.x + left * rect.width - ( leftSpacing !== undefined ? leftSpacing : 0.) } - if (scrollX > contentWidth - width) { - scrollX = contentWidth - width - } // Adjust vertical position. scrollY = rect.y + (top === undefined ? 0. : top * rect.height) - ( topSpacing !== undefined ? topSpacing : 0.) - if (scrollY > contentHeight - height) { - scrollY = contentHeight - height - } - return Qt.point(Math.max(0, scrollX), Math.max(0, scrollY)) + return Qt.point(scrollX, scrollY) } function goToPage(pageNumber, top, left, topSpacing, leftSpacing) { var pt = contentAt(pageNumber, top, left, topSpacing, leftSpacing) - contentX = pt.x - contentY = pt.y + contentX = Math.max(0, Math.min(contentWidth - width, pt.x)) + contentY = Math.max(0, Math.min(contentHeight - height, pt.y)) } // This function is the inverse of goToPage(), returning (pageNumber, top, left). function getPagePosition() { diff --git a/plugin/SearchBarItem.qml b/plugin/SearchBarItem.qml index 2d4cb790..dc3e0d06 100644 --- a/plugin/SearchBarItem.qml +++ b/plugin/SearchBarItem.qml @@ -47,6 +47,7 @@ BackgroundItem { property real iconizedWidth property bool searching property alias searchProgress: progressBar.progress + property alias text: searchField._searchText property real _margin: Math.max((iconizedWidth - searchIcon.width) / 2., 0.) diff --git a/rpm/sailfish-office.spec b/rpm/sailfish-office.spec index fe9f5000..3241d018 100644 --- a/rpm/sailfish-office.spec +++ b/rpm/sailfish-office.spec @@ -32,6 +32,12 @@ Summary: Translation source for %{name} License: GPLv2 Group: System/Base +%package tests +Summary: Unitary tests for %{name} +Requires: qt5-qtdeclarative-import-qttest +Requires: qt5-qtdeclarative-devel-tools +Requires: %{name} = %{version}-%{release} + %description %{summary}. @@ -39,6 +45,9 @@ Group: System/Base %description ts-devel %{summary}. +%description tests +%{summary}. + %files %defattr(-,root,root,-) @@ -54,6 +63,9 @@ Group: System/Base %files ts-devel %{_datadir}/translations/source/*.ts +%files tests +/opt/tests/sailfish-office/tst_PDFDocumentPage.qml +/opt/tests/sailfish-office/data/*.pdf %prep %setup -q -n %{name}-%{version} diff --git a/tests/data/broken.pdf b/tests/data/broken.pdf new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/protected.pdf b/tests/data/protected.pdf new file mode 100644 index 00000000..8309bcd5 Binary files /dev/null and b/tests/data/protected.pdf differ diff --git a/tests/data/sample.pdf b/tests/data/sample.pdf new file mode 100644 index 00000000..d8778e13 Binary files /dev/null and b/tests/data/sample.pdf differ diff --git a/tests/tst_PDFDocumentPage.qml b/tests/tst_PDFDocumentPage.qml new file mode 100644 index 00000000..5d85d81a --- /dev/null +++ b/tests/tst_PDFDocumentPage.qml @@ -0,0 +1,396 @@ +/* + * Copyright (C) 2016-2019 Caliste Damien. + * Contact: Damien Caliste + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import QtQuick 2.0 +import QtTest 1.0 +import Sailfish.Silica 1.0 +import Sailfish.Office 1.0 + +ApplicationWindow { + id: window + property Item documentItem + property Page _mainPage + + initialPage: Component { + PDFDocumentPage { + id: page + mimeType: "application/pdf" + Component.onCompleted: window._mainPage = page + + function clickAt(testCase, x, y) { + clickFeedback.x = x - clickFeedback.width / 2 + clickFeedback.y = y - clickFeedback.height / 2 + clickAnimation.start() + testCase.tryCompare(clickAnimation, "running", false) + testCase.mouseClick(page, x, y) + } + + Rectangle { + id: clickFeedback + height: Theme.itemSizeMedium + width: height + radius: Theme.itemSizeMedium / 2 + color: Theme.highlightColor + opacity: 0. + SequentialAnimation { + id: clickAnimation + NumberAnimation { target: clickFeedback; property: "opacity"; duration: 500; to: 1.0; easing.type: Easing.InOutCubic } + NumberAnimation { target: clickFeedback; property: "opacity"; duration: 500; to: 0.0; easing.type: Easing.InOutCubic } + } + } + } + } + + TestCase { + name: "brokenPDF" + when: _mainPage && windowShown + + function initTestCase() { + _mainPage.path = Qt.resolvedUrl("data/broken.pdf") + } + + function test_placeholderBroken() { + tryCompare(_mainPage, "backNavigation", true) + tryCompare(_mainPage.document, "failure", true) + tryCompare(_mainPage.placeHolder, "status", Loader.Ready) + } + } + + TestCase { + name: "protectedPDF" + when: _mainPage && windowShown + + function initTestCase() { + _mainPage.path = Qt.resolvedUrl("data/protected.pdf") + } + + function test_placeholderBroken() { + tryCompare(_mainPage, "backNavigation", true) + tryCompare(_mainPage.document, "failure", false) + tryCompare(_mainPage.document, "locked", true) + tryCompare(_mainPage.placeHolder, "status", Loader.Ready) + keyClick("f") + keyClick("o") + keyClick("o") + keyClick(Qt.Key_Return) + tryCompare(_mainPage.document, "locked", false) + tryCompare(_mainPage.placeHolder, "status", Loader.Null) + tryCompare(_mainPage.documentItem, "contentAvailable", true) + } + } + + TestCase { + name: "basics" + when: _mainPage && windowShown + + function initTestCase() { + _mainPage.path = Qt.resolvedUrl("data/sample.pdf") + tryCompare(_mainPage.documentItem, "contentAvailable", true) + } + + function test_pages() { + compare(_mainPage.document.pageCount, 4) + } + + function test_currentPage() { + _mainPage.documentItem.itemWidth = window.width * 1.5 + _mainPage.documentItem.adjust() + _mainPage.documentItem.goToPage(0, 0.25, 0.25) + tryCompare(_mainPage.documentItem, "currentPage", 1) + _mainPage.documentItem.goToPage(2, 0.25, 0.25) + tryCompare(_mainPage.documentItem, "currentPage", 3) + } + } + + TestCase { + name: "urlLink" + when: _mainPage && windowShown + + property int linkPage: 0 + property point linkPagePoint: Qt.point(0.278, 0.317) + + function clickAt(page, left, top) { + var pt = _mainPage.documentItem.contentAt(page, top, left) + var at = Qt.point(pt.x - _mainPage.documentItem.contentX, + pt.y - _mainPage.documentItem.contentY) + mouseClick(_mainPage.documentItem, at.x, at.y) + } + + function checkMenuOpenAt(page, left, top) { + verify(_mainPage.contextMenuLinks) + tryCompare(_mainPage.contextMenuLinks, "active", true) + var ct = _mainPage.documentItem.mapFromItem(_mainPage.contextMenuLinks, 0, 0) + compare(ct.x, 0) + var pt = _mainPage.documentItem.contentAt(page, top, left) + fuzzyCompare(ct.y, pt.y - _mainPage.documentItem.contentY, 3 * Theme.paddingSmall) + tryCompare(_mainPage, "open", false) + } + function closeMenuAndCheck() { + verify(_mainPage.contextMenuLinks.active) + // Discard it. + mouseClick(_mainPage.documentItem, window.width / 2, 50) + verify(!_mainPage.open) + tryCompare(_mainPage.contextMenuLinks, "active", false) + } + + function initTestCase() { + _mainPage.path = Qt.resolvedUrl("data/sample.pdf") + _mainPage.allowedOrientations = Orientation.Portrait + } + + function cleanupTestCase() { + _mainPage.allowedOrientations = Orientation.All + } + + function test_00_setup() { + tryCompare(_mainPage.documentItem, "contentAvailable", true) + // Set width and position + _mainPage.documentItem.itemWidth = window.width * 1.5 + _mainPage.documentItem.adjust() + _mainPage.documentItem.goToPage(0, 0.25, 0.25) + var pt = _mainPage.documentItem.contentAt(0, 0.25, 0.25) + tryCompare(_mainPage.documentItem, "contentX", Math.round(pt.x)) + tryCompare(_mainPage.documentItem, "contentY", Math.round(pt.y)) + } + + function test_contextMenu() { + // Open the context menu. + clickAt(linkPage, linkPagePoint.x, linkPagePoint.y) + checkMenuOpenAt(linkPage, linkPagePoint.x, linkPagePoint.y) + compare(_mainPage.contextMenuLinks.url, "http://helpdesk.rpi.edu/update.do?artcenterkey=325") + } + + function test_contextMenu_finalize() { + closeMenuAndCheck() + } + + function test_deviceRotation() { + _mainPage.allowedOrientations = Orientation.Portrait + clickAt(linkPage, linkPagePoint.x, linkPagePoint.y) + checkMenuOpenAt(linkPage, linkPagePoint.x, linkPagePoint.y) + _mainPage.allowedOrientations = Orientation.Landscape + tryCompare(_mainPage, "width", Screen.height) + checkMenuOpenAt(linkPage, linkPagePoint.x, linkPagePoint.y) + } + + function test_deviceRotation_finalize() { + closeMenuAndCheck() + } + } + + TestCase { + name: "gotoLink" + when: _mainPage && windowShown + + property int gotoPage: 0 + property point gotoPagePoint: Qt.point(0.537, 0.677) + property int targetPage: 3 + property point targetPagePoint: Qt.point(0.1065, 0.3689) + + function clickAt(page, left, top) { + var pt = _mainPage.documentItem.contentAt(page, top, left) + var at = Qt.point(pt.x - _mainPage.documentItem.contentX, + pt.y - _mainPage.documentItem.contentY) + mouseClick(_mainPage.documentItem, at.x, at.y) + } + + function initTestCase() { + _mainPage.path = Qt.resolvedUrl("data/sample.pdf") + } + + function test_00_setup() { + tryCompare(_mainPage.documentItem, "contentAvailable", true) + // Set width and position + _mainPage.documentItem.itemWidth = window.width * 3 + _mainPage.documentItem.adjust() + _mainPage.documentItem.goToPage(0, 0.50, 0.50) + var pt = _mainPage.documentItem.contentAt(0, 0.50, 0.50) + tryCompare(_mainPage.documentItem, "contentX", Math.round(pt.x)) + tryCompare(_mainPage.documentItem, "contentY", Math.round(pt.y)) + } + + function test_goto() { + clickAt(gotoPage, gotoPagePoint.x, gotoPagePoint.y) + var pt = _mainPage.documentItem.contentAt(targetPage, targetPagePoint.y, targetPagePoint.x) + verify(_mainPage.toolbar._active) + tryCompare(_mainPage.documentItem, "scrolling", false) + fuzzyCompare(_mainPage.documentItem.contentX, Math.round(pt.x), Theme.paddingSmall) + fuzzyCompare(_mainPage.documentItem.contentY, Math.round(pt.y), Theme.paddingSmall) + } + + function test_goto_back() { + mouseClick(_mainPage, 3.5 * window.width / 5, window.height - Theme.itemSizeSmall) + tryCompare(_mainPage.documentItem, "scrolling", false) + tryCompare(_mainPage.toolbar, "offset", 0) + var at = _mainPage.documentItem.getPositionAt(Qt.point(_mainPage.documentItem.contentX, _mainPage.documentItem.contentY)) + compare(at[0], 0) + fuzzyCompare(at[1], 0.50, 0.01) + fuzzyCompare(at[2], 0.50, 0.01) + } + } + + TestCase { + id: searchTestCase + name: "search" + when: _mainPage && windowShown + + property int matchPage: 1 + property rect match: Qt.rect(0.546236, 0.794508, 0.0332727, 0.0126171) + property int backMatchPage: 0 + property rect backMatch: Qt.rect(0.723961, 0.312414, 0.0332727, 0.0122314) + + IconButton { + id: refIcon + width: icon.width + height: icon.height + icon.source: "image://theme/icon-m-left" + } + + function checkCentredMatch(page, match) { + tryCompare(_mainPage.documentItem, "scrolling", false) + var pt = _mainPage.documentItem.contentAt(page, match.y + match.height / 2, + match.x + match.width / 2) + fuzzyCompare(_mainPage.documentItem.contentX, + Math.round(pt.x - _mainPage.documentItem.width / 2), + Theme.paddingSmall) + fuzzyCompare(_mainPage.documentItem.contentY, + Math.round(pt.y - _mainPage.documentItem.height / 2), + Theme.paddingSmall) + } + + function initTestCase() { + _mainPage.path = Qt.resolvedUrl("data/sample.pdf") + // Set width and position + _mainPage.documentItem.itemWidth = window.width * 5 + _mainPage.documentItem.adjust() + _mainPage.allowedOrientations = Orientation.Portrait + } + + function cleanupTestCase() { + _mainPage.toolbar.searchIconized = true + } + + function test_no_match() { + _mainPage.toolbar.show() + tryCompare(_mainPage.toolbar, "offset", _mainPage.toolbar.height) + _mainPage.clickAt(searchTestCase, 0.5 * window.width / 4, window.height - _mainPage.toolbar.height / 2) + tryCompare(_mainPage.toolbar, "searchIconized", false) + verify(!_mainPage.toolbar.autoShowHide) + keyClick("p") + keyClick("l") + keyClick("o") + keyClick("u") + keyClick("m") + keyClick(Qt.Key_Return) + tryCompare(_mainPage.document, "searching", false) + verify(_mainPage.document.searchModel) + compare(_mainPage.document.searchModel.count, 0) + tryCompare(_mainPage, "height", window.height) // Ensure keyboard is folded + } + + function test_no_match_finalize() { + tryCompare(_mainPage.toolbar, "searchIconized", false) + _mainPage.clickAt(searchTestCase, window.width - refIcon.width / 2 - Theme.horizontalPageMargin, window.height - _mainPage.toolbar.height / 2) + tryCompare(_mainPage.toolbar, "searchIconized", true) + } + + function test_match() { + _mainPage.documentItem.goToPage(0, 0., 0.) + _mainPage.toolbar.show() + tryCompare(_mainPage.toolbar, "offset", _mainPage.toolbar.height) + _mainPage.clickAt(searchTestCase, 0.5 * window.width / 4, window.height - _mainPage.toolbar.height / 2) + tryCompare(_mainPage.toolbar, "searchIconized", false) + verify(!_mainPage.toolbar.autoShowHide) + keyClick("e") + keyClick("a") + keyClick("s") + keyClick("y") + keyClick(Qt.Key_Return) + tryCompare(_mainPage.document, "searching", false) + verify(_mainPage.document.searchModel) + compare(_mainPage.document.searchModel.count, 2) + tryCompare(_mainPage, "height", window.height) // Ensure keyboard is folded + tryCompare(_mainPage.documentItem, "scrolling", false) + tryCompare(_mainPage.toolbar, "searchIconized", false) + _mainPage.clickAt(searchTestCase, window.width - Theme.paddingLarge - 1.5 * refIcon.width - Theme.horizontalPageMargin, window.height - _mainPage.toolbar.height / 2) + checkCentredMatch(matchPage, match) + } + + function test_match_back_navigate() { + tryCompare(_mainPage.toolbar, "searchIconized", false) + _mainPage.clickAt(searchTestCase, window.width - 2 * Theme.paddingLarge - 2.5 * refIcon.width - Theme.horizontalPageMargin, window.height - _mainPage.toolbar.height / 2) + checkCentredMatch(backMatchPage, backMatch) + } + + function test_match_reopen() { + _mainPage.clickAt(searchTestCase, window.width / 2, window.height - _mainPage.toolbar.height / 2) + tryCompare(_mainPage.toolbar, "searchIconized", false) + _mainPage.clickAt(searchTestCase, _mainPage.width - refIcon.width / 2 - Theme.horizontalPageMargin, window.height - _mainPage.toolbar.height / 2) + tryCompare(_mainPage.toolbar, "searchIconized", false) + compare(_mainPage.toolbar.searchText, "") + } + + function test_match_reopen_finalize() { + tryCompare(_mainPage.toolbar, "searchIconized", false) + _mainPage.clickAt(searchTestCase, window.width - refIcon.width / 2 - Theme.horizontalPageMargin, window.height - _mainPage.toolbar.height / 2) + tryCompare(_mainPage.toolbar, "searchIconized", true) + _mainPage.toolbar.hide() + tryCompare(_mainPage.toolbar, "offset", 0) + } + } + +/* TestCase { + name: "scrolling" + when: _mainPage && windowShown + + function initTestCase() { + _mainPage.path = Qt.resolvedUrl("sample.pdf") + } + + function test_00_check() { + tryCompare(_mainPage.documentItem, "contentAvailable", true) + } + + function test_simpleScrollDown() { + var x = window.width / 2 + var y = window.height / 2 + var dx = window.width / 10 + var dy = -window.height / 4 + var duration = 100 + var i + var dt = 100 + mousePress(_mainPage.documentItem, x, y) + for (i = 0; i < duration; i += dt) { + mouseMove(_mainPage.documentItem, x + dx * i / duration, y + dy * i / duration) + //wait(dt) + } + mouseRelease(_mainPage.documentItem, x + dx, y + dy) + tryCompare(_mainPage.documentItem, "contentX", 0) + tryCompare(_mainPage.documentItem, "contentY", -dy) + } + + function test_simpleScrollUp() { + } + + function test_quickScrollDown() { + } + + function test_quickScrollUp() { + } + }*/ +}