diff options
Diffstat (limited to 'tests/auto/quick/qquicktextdocument')
11 files changed, 638 insertions, 18 deletions
diff --git a/tests/auto/quick/qquicktextdocument/CMakeLists.txt b/tests/auto/quick/qquicktextdocument/CMakeLists.txt index e106efd6c2..5bcad96b0a 100644 --- a/tests/auto/quick/qquicktextdocument/CMakeLists.txt +++ b/tests/auto/quick/qquicktextdocument/CMakeLists.txt @@ -7,6 +7,12 @@ ## tst_qquicktextdocument Test: ##################################################################### +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qquicktextdocument LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + # Collect test data file(GLOB_RECURSE test_data_glob RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} diff --git a/tests/auto/quick/qquicktextdocument/data/hello-8857-7.html b/tests/auto/quick/qquicktextdocument/data/hello-8857-7.html new file mode 100644 index 0000000000..6bf900858a --- /dev/null +++ b/tests/auto/quick/qquicktextdocument/data/hello-8857-7.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="iso-8859-7"> +</head> +<body> +<p>ÃåéÜ óïõ Êüóìå!</p> +</body> +</html> diff --git a/tests/auto/quick/qquicktextdocument/data/hello-utf16be.html b/tests/auto/quick/qquicktextdocument/data/hello-utf16be.html Binary files differnew file mode 100644 index 0000000000..be84e0d63c --- /dev/null +++ b/tests/auto/quick/qquicktextdocument/data/hello-utf16be.html diff --git a/tests/auto/quick/qquicktextdocument/data/hello.html b/tests/auto/quick/qquicktextdocument/data/hello.html new file mode 100644 index 0000000000..c160cd5e63 --- /dev/null +++ b/tests/auto/quick/qquicktextdocument/data/hello.html @@ -0,0 +1 @@ +Γειά σου <i>Κόσμε</i>! diff --git a/tests/auto/quick/qquicktextdocument/data/hello.md b/tests/auto/quick/qquicktextdocument/data/hello.md new file mode 100644 index 0000000000..290f3f1397 --- /dev/null +++ b/tests/auto/quick/qquicktextdocument/data/hello.md @@ -0,0 +1 @@ +Γειά σου *Κόσμε*! diff --git a/tests/auto/quick/qquicktextdocument/data/hello.txt b/tests/auto/quick/qquicktextdocument/data/hello.txt new file mode 100644 index 0000000000..1f72c3f7a4 --- /dev/null +++ b/tests/auto/quick/qquicktextdocument/data/hello.txt @@ -0,0 +1 @@ +Γειά σου Κόσμε! diff --git a/tests/auto/quick/qquicktextdocument/data/initialText.qml b/tests/auto/quick/qquicktextdocument/data/initialText.qml new file mode 100644 index 0000000000..be9280f952 --- /dev/null +++ b/tests/auto/quick/qquicktextdocument/data/initialText.qml @@ -0,0 +1,16 @@ +import QtQuick + +TextEdit { + id: te + property int sourceChangeCount: 0 + property int modifiedChangeCount: 0 + property var statusHistory: [] + + width: 320; height: 240 + text: "Hello Qt" + focus: true + + textDocument.onSourceChanged: ++te.sourceChangeCount + textDocument.onModifiedChanged: ++te.modifiedChangeCount + textDocument.onStatusChanged: te.statusHistory.push(textDocument.status) +} diff --git a/tests/auto/quick/qquicktextdocument/data/sideBySideIndependent.qml b/tests/auto/quick/qquicktextdocument/data/sideBySideIndependent.qml new file mode 100644 index 0000000000..8275e7f5bd --- /dev/null +++ b/tests/auto/quick/qquicktextdocument/data/sideBySideIndependent.qml @@ -0,0 +1,21 @@ +import QtQuick + +Row { + width: 480; height: 200 + TextEdit { + objectName: "plain" + width: parent.width / 2 - 1 + textFormat: TextEdit.PlainText + textDocument.source: "hello.md" + } + Rectangle { + width: 2; height: parent.height + color: "lightsteelblue" + } + TextEdit { + objectName: "markdown" + width: parent.width / 2 - 1 + textFormat: TextEdit.MarkdownText + textDocument.source: "hello.md" + } +} diff --git a/tests/auto/quick/qquicktextdocument/data/sideBySideIndependentReverse.qml b/tests/auto/quick/qquicktextdocument/data/sideBySideIndependentReverse.qml new file mode 100644 index 0000000000..cbd92accec --- /dev/null +++ b/tests/auto/quick/qquicktextdocument/data/sideBySideIndependentReverse.qml @@ -0,0 +1,21 @@ +import QtQuick + +Row { + width: 480; height: 200 + TextEdit { + objectName: "plain" + width: parent.width / 2 - 1 + textDocument.source: "hello.md" + textFormat: TextEdit.PlainText + } + Rectangle { + width: 2; height: parent.height + color: "lightsteelblue" + } + TextEdit { + objectName: "markdown" + width: parent.width / 2 - 1 + textDocument.source: "hello.md" + textFormat: TextEdit.MarkdownText + } +} diff --git a/tests/auto/quick/qquicktextdocument/data/text.qml b/tests/auto/quick/qquicktextdocument/data/text.qml index 43a8c6bb82..d1b9ee7203 100644 --- a/tests/auto/quick/qquicktextdocument/data/text.qml +++ b/tests/auto/quick/qquicktextdocument/data/text.qml @@ -1,6 +1,15 @@ -import QtQuick 2.1 +import QtQuick TextEdit { - text: "" -} + id: te + property int sourceChangeCount: 0 + property int modifiedChangeCount: 0 + property var statusHistory: [] + + width: 320; height: 240 + text: "" // this is not a document modification + textDocument.onSourceChanged: ++te.sourceChangeCount + textDocument.onModifiedChanged: ++te.modifiedChangeCount + textDocument.onStatusChanged: te.statusHistory.push(textDocument.status) +} diff --git a/tests/auto/quick/qquicktextdocument/tst_qquicktextdocument.cpp b/tests/auto/quick/qquicktextdocument/tst_qquicktextdocument.cpp index 63d4f24a54..e8c9f09413 100644 --- a/tests/auto/quick/qquicktextdocument/tst_qquicktextdocument.cpp +++ b/tests/auto/quick/qquicktextdocument/tst_qquicktextdocument.cpp @@ -1,17 +1,30 @@ // Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <qtest.h> #include <QtTest/QtTest> -#include <QtQuick/QQuickTextDocument> #include <QtQuick/QQuickItem> -#include <QtQuick/private/qquicktextedit_p.h> +#include <QtQuick/QQuickTextDocument> +#include <QtQuick/QQuickView> #include <QtQuick/private/qquicktextdocument_p.h> +#include <QtQuick/private/qquicktextedit_p.h> +#include <QtQuick/private/qquicktextedit_p_p.h> +#include <QtCore/QStringConverter> +#include <QtGui/QTextBlock> #include <QtGui/QTextDocument> #include <QtGui/QTextDocumentWriter> -#include <QtQml/QQmlEngine> #include <QtQml/QQmlComponent> +#include <QtQml/QQmlEngine> +#include <QtQml/QQmlFile> #include <QtQuickTestUtils/private/qmlutils_p.h> +#include <QtQuickTestUtils/private/viewtestutils_p.h> +#ifdef Q_OS_UNIX +#include <unistd.h> +#endif + +Q_LOGGING_CATEGORY(lcTests, "qt.quick.tests") + +using namespace Qt::StringLiterals; class tst_qquicktextdocument : public QQmlDataTest { @@ -19,18 +32,92 @@ class tst_qquicktextdocument : public QQmlDataTest public: tst_qquicktextdocument(); +private: + QPair<int, int> fragmentsAndItalics(const QTextDocument *doc); + bool isMainFontFixed(); + private slots: void textDocumentWriter(); - void textDocumentWithImage(); + void customDocument(); + void replaceDocument(); + void sourceAndSave_data(); + void sourceAndSave(); + void loadErrorNoSuchFile(); + void loadErrorPermissionDenied(); + void overrideTextFormat_data(); + void overrideTextFormat(); + void independentDocumentsSameSource_data(); + void independentDocumentsSameSource(); }; QString text = QStringLiteral("foo bar"); +// similar to TestDocument in tst_qtextdocument.cpp +class FakeImageDocument : public QTextDocument +{ +public: + inline FakeImageDocument(const QUrl &testUrl, const QString &testString) + : url(testUrl), string(testString), resourceLoaded(false) {} + + bool hasResourceCached(); + +protected: + virtual QVariant loadResource(int type, const QUrl &name) override; + +private: + QUrl url; + QString string; + bool resourceLoaded; +}; + +bool FakeImageDocument::hasResourceCached() +{ + return resourceLoaded; +} + +QVariant FakeImageDocument::loadResource(int type, const QUrl &name) +{ + qCDebug(lcTests) << type << name << ": expecting" << int(QTextDocument::ImageResource) << url; + if (type == QTextDocument::ImageResource && name == url) { + resourceLoaded = true; + return string; + } + return QTextDocument::loadResource(type, name); +} + tst_qquicktextdocument::tst_qquicktextdocument() : QQmlDataTest(QT_QMLTEST_DATADIR) { } +/*! \internal + Returns {fragmentCount, italicFragmentIndex}. If no italic fragment is found, + italicFragmentIndex is -1. +*/ +QPair<int, int> tst_qquicktextdocument::fragmentsAndItalics(const QTextDocument *doc) +{ + int fragmentCount = 0; + int italicFragment = -1; + for (QTextBlock::iterator it = doc->firstBlock().begin(); !(it.atEnd()); ++it) { + QTextFragment currentFragment = it.fragment(); + if (currentFragment.charFormat().fontItalic()) + italicFragment = fragmentCount; + ++fragmentCount; + qCDebug(lcTests) << (currentFragment.charFormat().fontItalic() ? "italic" : "roman") << currentFragment.text(); + } + return {fragmentCount, italicFragment}; +} + +bool tst_qquicktextdocument::isMainFontFixed() +{ + bool ret = QFontInfo(QGuiApplication::font()).fixedPitch(); + if (ret) { + qCWarning(lcTests) << "QFontDatabase::GeneralFont is monospaced: markdown writing is likely to use too many backticks" + << QFontDatabase::systemFont(QFontDatabase::GeneralFont); + } + return ret; +} + void tst_qquicktextdocument::textDocumentWriter() { QQmlEngine e; @@ -54,18 +141,466 @@ void tst_qquicktextdocument::textDocumentWriter() delete o; } -void tst_qquicktextdocument::textDocumentWithImage() +/*! \internal + Verify that it's OK to replace the default QTextDocument that TextEdit creates + with a user-created QTextDocument. + + Also verify that the user can still override QTextDocument::loadResource(). + QTextDocument::loadResource() can call its QObject parent's loadResource(); + the default QTextDocument's parent is the QQuickTextEdit, which provides an + implementation of QQuickTextEdit::loadResource(), which uses QQuickPixmap + to load and cache images. This will be bypassed if the user overrides + loadResource() to do something different. +*/ +void tst_qquicktextdocument::customDocument() +{ + QQmlEngine e; + QQmlComponent c(&e, testFileUrl("text.qml")); + QScopedPointer<QQuickTextEdit> textEdit(qobject_cast<QQuickTextEdit*>(c.create())); + QCOMPARE(textEdit.isNull(), false); + auto *textEditPriv = QQuickTextEditPrivate::get(textEdit.get()); + QVERIFY(textEditPriv->ownsDocument); + + QQuickTextDocument *quickDocument = textEdit->property("textDocument").value<QQuickTextDocument*>(); + QVERIFY(quickDocument); + QPointer<QTextDocument> defaultDocument(quickDocument->textDocument()); + const QString imageUrl = "https://www.qt.io/hubfs/Qt-logo-neon-small.png"; + const QString fakeImageData = "foo!"; + + FakeImageDocument fdoc(QUrl(imageUrl), fakeImageData); + quickDocument->setTextDocument(&fdoc); + QVERIFY(defaultDocument.isNull()); // deleted because of being replaced (don't leak) + QCOMPARE(textEditPriv->ownsDocument, false); + + // QQuickTextEdit::setText() -> QQuickTextControl::setHtml() -> + // QQuickTextControlPrivate::setContent() -> fdoc->setHtml() + // and eventually fdoc->loadResource() which substitutes a string for the requested image + textEdit->setTextFormat(QQuickTextEdit::RichText); + textEdit->setText("an image: <img src='" + imageUrl + "'/>"); + QCOMPARE(fdoc.hasResourceCached(), true); + + auto firstBlock = fdoc.firstBlock(); + // check that image loading has been bypassed by FakeImageDocument + bool foundImage = false; + int fragmentCount = 0; + for (QTextBlock::iterator it = firstBlock.begin(); !(it.atEnd()); ++it) { + QTextFragment currentFragment = it.fragment(); + QVERIFY(currentFragment.isValid()); + ++fragmentCount; + const QString imageName = currentFragment.charFormat().stringProperty(QTextFormat::ImageName); + if (!imageName.isEmpty()) { + QCOMPARE(imageName, imageUrl); + foundImage = true; + QCOMPARE(fdoc.resource(QTextDocument::ImageResource, imageUrl).toString(), fakeImageData); + } + } + QVERIFY(foundImage); + QCOMPARE(fragmentCount, 2); +} + +/*! \internal + Verify that it's OK to replace the default QTextDocument that TextEdit creates + with a user-created QTextDocument that has different text in it, and that + interactive editing continues to function afterwards, independently of + the previous document. +*/ +void tst_qquicktextdocument::replaceDocument() // QTBUG-126267 +{ + QQuickView window; + QVERIFY(QQuickTest::showView(window, testFileUrl("initialText.qml"))); + QQuickTextEdit *textEdit = qobject_cast<QQuickTextEdit *>(window.rootObject()); + QVERIFY(textEdit); + auto *textEditPriv = QQuickTextEditPrivate::get(textEdit); + QVERIFY(textEditPriv->ownsDocument); + QQuickTextDocument *quickDocument = textEdit->property("textDocument").value<QQuickTextDocument*>(); + QVERIFY(quickDocument); + QPointer<QTextDocument> defaultDocument(quickDocument->textDocument()); + + QTextDocument replacementDoc; + const QString replacementText("Hello World"); + { + QTextCursor cursor(&replacementDoc); + cursor.insertText(replacementText); + } + QCOMPARE(textEdit->text(), "Hello Qt"); + QSignalSpy renderSpy(&window, &QQuickWindow::afterRendering); + quickDocument->setTextDocument(&replacementDoc); + QVERIFY(defaultDocument.isNull()); // deleted because of being replaced (don't leak) + QCOMPARE(textEditPriv->ownsDocument, false); + QCOMPARE(textEdit->text(), replacementText); + QCOMPARE(replacementDoc.toPlainText(), replacementText); + QTRY_COMPARE_GT(renderSpy.size(), 0); + + QCOMPARE(window.activeFocusItem(), textEdit); + QTest::keyEvent(QTest::KeyAction::Click, &window, Qt::Key_End); + QTest::keyEvent(QTest::KeyAction::Click, &window, '!'); + QCOMPARE(textEdit->text(), replacementText + '!'); +} + +void tst_qquicktextdocument::sourceAndSave_data() +{ + QTest::addColumn<QQuickTextEdit::TextFormat>("textFormat"); + QTest::addColumn<QString>("source"); + QTest::addColumn<std::optional<QStringConverter::Encoding>>("expectedEncoding"); + QTest::addColumn<Qt::TextFormat>("expectedTextFormat"); + QTest::addColumn<int>("minCharCount"); + QTest::addColumn<QString>("expectedPlainText"); + + const std::optional<QStringConverter::Encoding> nullEnc; + + QTest::newRow("plain") << QQuickTextEdit::PlainText << "hello.txt" + << nullEnc << Qt::PlainText << 15 << u"Γειά σου Κόσμε!"_s; + QTest::newRow("markdown") << QQuickTextEdit::MarkdownText << "hello.md" + << nullEnc << Qt::MarkdownText << 15 << u"Γειά σου Κόσμε!"_s; + QTest::newRow("html") << QQuickTextEdit::RichText << "hello.html" + << std::optional<QStringConverter::Encoding>(QStringConverter::Utf8) + << Qt::RichText << 15 << u"Γειά σου Κόσμε!"_s; + QTest::newRow("html-utf16be") << QQuickTextEdit::AutoText << "hello-utf16be.html" + << std::optional<QStringConverter::Encoding>(QStringConverter::Utf16BE) + << Qt::RichText << 15 << u"Γειά σου Κόσμε!"_s; +} + +void tst_qquicktextdocument::sourceAndSave() { - QQuickTextDocumentWithImageResources document(nullptr); - QImage image(1, 1, QImage::Format_Mono); - image.fill(1); + QFETCH(QQuickTextEdit::TextFormat, textFormat); + QFETCH(QString, source); + QFETCH(std::optional<QStringConverter::Encoding>, expectedEncoding); + QFETCH(Qt::TextFormat, expectedTextFormat); + QFETCH(int, minCharCount); + QFETCH(QString, expectedPlainText); + + QQmlEngine e; + QQmlComponent c(&e, testFileUrl("text.qml")); + QScopedPointer<QQuickTextEdit> textEdit(qobject_cast<QQuickTextEdit*>(c.create())); + QCOMPARE(textEdit.isNull(), false); + QQuickTextDocument *qqdoc = textEdit->property("textDocument").value<QQuickTextDocument*>(); + QVERIFY(qqdoc); + const QQmlContext *ctxt = e.rootContext(); + // text.qml has text: "" but that's not a real change; QQuickTextEdit::setText() returns early + // QQuickTextEditPrivate::init() also modifies defaults and then resets the modified state + QCOMPARE(qqdoc->isModified(), false); + // no stray signals should be visible to QML during init + QCOMPARE(textEdit->property("modifiedChangeCount").toInt(), 0); + QCOMPARE(textEdit->property("sourceChangeCount").toInt(), 0); + QTextDocument *doc = qqdoc->textDocument(); + QVERIFY(doc); + QSignalSpy sourceChangedSpy(qqdoc, &QQuickTextDocument::sourceChanged); + QSignalSpy modifiedChangedSpy(qqdoc, &QQuickTextDocument::modifiedChanged); + QSignalSpy statusChangedSpy(qqdoc, &QQuickTextDocument::statusChanged); + + QTemporaryDir tmpDir; + QVERIFY(tmpDir.isValid()); + QFile sf(QQmlFile::urlToLocalFileOrQrc(ctxt->resolvedUrl(testFileUrl(source)))); + qCDebug(lcTests) << source << "orig ->" << sf.fileName(); + QVERIFY(sf.exists()); + QString tmpPath = tmpDir.filePath(source); + QVERIFY(sf.copy(tmpPath)); + qCDebug(lcTests) << source << "copy ->" << tmpDir.path() << ":" << tmpPath; + + QCOMPARE(statusChangedSpy.size(), 0); + QCOMPARE(qqdoc->status(), QQuickTextDocument::Status::Null); + textEdit->setTextFormat(textFormat); + qqdoc->setProperty("source", QUrl::fromLocalFile(tmpPath)); + QCOMPARE(sourceChangedSpy.size(), 1); + QCOMPARE(textEdit->property("sourceChangeCount").toInt(), 1); + QCOMPARE(statusChangedSpy.size(), 2); // Loading, then Loaded + QCOMPARE(qqdoc->status(), QQuickTextDocument::Status::Loaded); + QVERIFY(qqdoc->errorString().isEmpty()); + const auto *qqdp = QQuickTextDocumentPrivate::get(qqdoc); + QCOMPARE(qqdp->detectedFormat, expectedTextFormat); + QCOMPARE_GE(doc->characterCount(), minCharCount); + QCOMPARE(doc->toPlainText().trimmed(), expectedPlainText); + QCOMPARE(qqdp->encoding, expectedEncoding); + + textEdit->setText("hello"); + QCOMPARE(qqdoc->isModified(), false); + QCOMPARE_GE(textEdit->property("modifiedChangeCount").toInt(), 1); + QCOMPARE(textEdit->property("modifiedChangeCount").toInt(), modifiedChangedSpy.size()); + modifiedChangedSpy.clear(); + textEdit->insert(5, "!"); + QCOMPARE_GE(modifiedChangedSpy.size(), 1); + QCOMPARE(qqdoc->isModified(), true); + + qqdoc->save(); + QCOMPARE(statusChangedSpy.size(), 4); // Saving, then Saved + QCOMPARE(qqdoc->status(), QQuickTextDocument::Status::Saved); + QVERIFY(qqdoc->errorString().isEmpty()); + QFile tf(tmpPath); + QVERIFY(tf.open(QIODeviceBase::ReadOnly)); + auto readBack = tf.readAll(); + if (expectedTextFormat == Qt::RichText) { + QStringDecoder dec(*expectedEncoding); + const QString decStr = dec(readBack); + QVERIFY(decStr.contains("hello!</p>")); + } else { + QVERIFY(readBack.contains("hello!")); + } + QCOMPARE(textEdit->property("sourceChangeCount").toInt(), sourceChangedSpy.size()); +} + +void tst_qquicktextdocument::loadErrorNoSuchFile() +{ + QQmlEngine e; + QQmlComponent c(&e, testFileUrl("text.qml")); + QScopedPointer<QQuickTextEdit> textEdit(qobject_cast<QQuickTextEdit*>(c.create())); + QCOMPARE(textEdit.isNull(), false); + QQuickTextDocument *qqdoc = textEdit->property("textDocument").value<QQuickTextDocument*>(); + QVERIFY(qqdoc); + QCOMPARE(textEdit->property("sourceChangeCount").toInt(), 0); + QTextDocument *doc = qqdoc->textDocument(); + QVERIFY(doc); + QSignalSpy sourceChangedSpy(qqdoc, &QQuickTextDocument::sourceChanged); + QSignalSpy statusChangedSpy(qqdoc, &QQuickTextDocument::statusChanged); + + QCOMPARE(statusChangedSpy.size(), 0); + QCOMPARE(qqdoc->status(), QQuickTextDocument::Status::Null); + const QRegularExpression err(".*does not exist"); + QTest::ignoreMessage(QtWarningMsg, err); + qqdoc->setProperty("source", testFileUrl("nosuchfile.md")); + QCOMPARE(sourceChangedSpy.size(), 1); + QCOMPARE(textEdit->property("sourceChangeCount").toInt(), 1); + qCDebug(lcTests) << "status history" << textEdit->property("statusHistory").toList(); + QCOMPARE(statusChangedSpy.size(), 1); + QCOMPARE(qqdoc->status(), QQuickTextDocument::Status::ReadError); + QVERIFY(qqdoc->errorString().contains(err)); +} + +void tst_qquicktextdocument::loadErrorPermissionDenied() +{ +#ifdef Q_OS_UNIX + if (geteuid() == 0) + QSKIP("Permission will not be denied with root privileges."); +#endif + QQmlEngine e; + QQmlComponent c(&e, testFileUrl("text.qml")); + QScopedPointer<QQuickTextEdit> textEdit(qobject_cast<QQuickTextEdit*>(c.create())); + QCOMPARE(textEdit.isNull(), false); + QQuickTextDocument *qqdoc = textEdit->property("textDocument").value<QQuickTextDocument*>(); + QVERIFY(qqdoc); + const QQmlContext *ctxt = e.rootContext(); + QCOMPARE(textEdit->property("sourceChangeCount").toInt(), 0); + QTextDocument *doc = qqdoc->textDocument(); + QVERIFY(doc); + QSignalSpy sourceChangedSpy(qqdoc, &QQuickTextDocument::sourceChanged); + QSignalSpy statusChangedSpy(qqdoc, &QQuickTextDocument::statusChanged); + + QTemporaryDir tmpDir; + QVERIFY(tmpDir.isValid()); + const QString source("hello.md"); + QFile sf(QQmlFile::urlToLocalFileOrQrc(ctxt->resolvedUrl(testFileUrl(source)))); + qCDebug(lcTests) << source << "orig ->" << sf.fileName(); + QVERIFY(sf.exists()); + QString tmpPath = tmpDir.filePath(source); + QVERIFY(sf.copy(tmpPath)); + qCDebug(lcTests) << source << "copy ->" << tmpDir.path() << ":" << tmpPath; + if (!QFile::setPermissions(tmpPath, QFileDevice::Permissions{})) // no permissions at all + QSKIP("Failed to change permissions of temporary file: cannot continue."); + + QCOMPARE(statusChangedSpy.size(), 0); + QCOMPARE(qqdoc->status(), QQuickTextDocument::Status::Null); + const QRegularExpression err(".*Failed to read: Permission denied"); + QTest::ignoreMessage(QtWarningMsg, err); + qqdoc->setProperty("source", QUrl::fromLocalFile(tmpPath)); + QCOMPARE(sourceChangedSpy.size(), 1); + QCOMPARE(textEdit->property("sourceChangeCount").toInt(), 1); + qCDebug(lcTests) << "status history" << textEdit->property("statusHistory").toList(); + QCOMPARE(statusChangedSpy.size(), 1); + QCOMPARE(qqdoc->status(), QQuickTextDocument::Status::ReadError); + QVERIFY(qqdoc->errorString().contains(err)); +} + +void tst_qquicktextdocument::overrideTextFormat_data() +{ + QTest::addColumn<QUrl>("qmlfile"); + QTest::addColumn<QQuickTextEdit::TextFormat>("initialFormat"); + QTest::addColumn<QUrl>("source"); + QTest::addColumn<int>("expectedInitialFragmentCount"); + QTest::addColumn<int>("expectedInitialItalicFragment"); + // first part of TextEdit.text after loading + QTest::addColumn<QString>("expectedTextPrefix"); + + QTest::addColumn<QQuickTextEdit::TextFormat>("replacementFormat"); + QTest::addColumn<int>("expectedFragmentCount"); + QTest::addColumn<int>("expectedItalicFragment"); + // first part of TextEdit.text after switching to replacementFormat + QTest::addColumn<QString>("expectedReplacementPrefix"); + QTest::addColumn<int>("expectedTextChangedSignalsAfterReplacement"); + + QTest::addColumn<QQuickTextEdit::TextFormat>("finalFormat"); + QTest::addColumn<int>("expectedFinalFragmentCount"); + QTest::addColumn<int>("expectedFinalItalicFragment"); + // first part of TextEdit.text after switching to finalFormat + QTest::addColumn<QString>("expectedFinalPrefix"); + + QTest::newRow("load md, switch to plain, back to md") + << testFileUrl("text.qml") << QQuickTextEdit::MarkdownText << testFileUrl("hello.md") + << 3 << 1 << u"Γειά σου *Κόσμε*!"_s + << QQuickTextEdit::PlainText << 1 << -1 << u"Γειά σου *Κόσμε*!"_s << 2 + << QQuickTextEdit::MarkdownText << 3 << 1 << u"Γειά σου *Κόσμε*!"_s; + QTest::newRow("load md, switch to plain, then auto") + << testFileUrl("text.qml") << QQuickTextEdit::MarkdownText << testFileUrl("hello.md") + << 3 << 1 << u"Γειά σου *Κόσμε*!"_s + << QQuickTextEdit::PlainText << 1 << -1 << u"Γειά σου *Κόσμε*!"_s << 2 + << QQuickTextEdit::AutoText << 3 << 1 << u"Γειά σου *Κόσμε*!"_s; + QTest::newRow("load md, switch to html, then plain") + << testFileUrl("text.qml") << QQuickTextEdit::MarkdownText << testFileUrl("hello.md") + << 3 << 1 << u"Γειά σου *Κόσμε*!"_s + << QQuickTextEdit::RichText << 3 << 1 << u"<!DOCTYPE HTML"_s << 2 + << QQuickTextEdit::PlainText << 1 << -1 << u"<!DOCTYPE HTML"_s; + QTest::newRow("load md as plain text, switch to md, back to plain") + << testFileUrl("text.qml") << QQuickTextEdit::PlainText << testFileUrl("hello.md") + << 1 << -1 << u"Γειά σου *Κόσμε*!"_s + << QQuickTextEdit::MarkdownText << 3 << 1 << u"Γειά σου *Κόσμε*!"_s << 2 + << QQuickTextEdit::PlainText << 1 << -1 << u"Γειά σου *Κόσμε*!"_s; + QTest::newRow("load md as autotext, switch to plain, back to auto") + << testFileUrl("text.qml") << QQuickTextEdit::AutoText << testFileUrl("hello.md") + << 3 << 1 << u"Γειά σου *Κόσμε*!"_s + << QQuickTextEdit::PlainText << 1 << -1 << u"Γειά σου *Κόσμε*!"_s << 2 + << QQuickTextEdit::AutoText << 3 << 1 << u"Γειά σου *Κόσμε*!"_s; + QTest::newRow("load md as autotext, switch to md, then plain") + << testFileUrl("text.qml") << QQuickTextEdit::AutoText << testFileUrl("hello.md") + << 3 << 1 << u"Γειά σου *Κόσμε*!"_s + << QQuickTextEdit::MarkdownText << 3 << 1 << u"Γειά σου *Κόσμε*!"_s + // going from AutoText to a matching explicit format does not cause extra textChanged() + << 1 + << QQuickTextEdit::PlainText << 1 << -1 << u"Γειά σου *Κόσμε*!"_s; + QTest::newRow("load md as autotext, switch to html, then plain") + << testFileUrl("text.qml") << QQuickTextEdit::AutoText << testFileUrl("hello.md") + << 3 << 1 << u"Γειά σου *Κόσμε*!"_s + << QQuickTextEdit::RichText << 3 << 1 << u"<!DOCTYPE HTML"_s << 2 + << QQuickTextEdit::PlainText << 1 << -1 << u"<!DOCTYPE HTML"_s; + + QTest::newRow("load html, switch to plain, back to rich") + << testFileUrl("text.qml") << QQuickTextEdit::RichText << testFileUrl("hello.html") + << 3 << 1 << u"<!DOCTYPE HTML"_s + << QQuickTextEdit::PlainText << 1 << -1 << u"<!DOCTYPE HTML"_s << 2 + << QQuickTextEdit::RichText << 3 << 1 << u"<!DOCTYPE HTML"_s; + QTest::newRow("load html as plain text, switch to html, back to plain") + << testFileUrl("text.qml") << QQuickTextEdit::PlainText << testFileUrl("hello.html") + << 1 << -1 << u"Γειά σου <i>Κόσμε</i>!"_s + << QQuickTextEdit::RichText << 3 << 1 << u"<!DOCTYPE HTML"_s << 2 + << QQuickTextEdit::PlainText << 1 << -1 << u"<!DOCTYPE HTML"_s; + QTest::newRow("load html as autotext, switch to plain, back to auto") + << testFileUrl("text.qml") << QQuickTextEdit::AutoText << testFileUrl("hello.html") + << 3 << 1 << u"<!DOCTYPE HTML"_s + << QQuickTextEdit::PlainText << 1 << -1 << u"<!DOCTYPE HTML"_s << 2 + << QQuickTextEdit::AutoText << 3 << 1 << u"<!DOCTYPE HTML"_s; + QTest::newRow("load html as autotext, switch to html, then plain") + << testFileUrl("text.qml") << QQuickTextEdit::AutoText << testFileUrl("hello.html") + << 3 << 1 << u"<!DOCTYPE HTML"_s + << QQuickTextEdit::RichText << 3 << 1 << u"<!DOCTYPE HTML"_s + // going from AutoText to a matching explicit format does not cause extra textChanged() + << 1 + << QQuickTextEdit::PlainText << 1 << -1 << u"<!DOCTYPE HTML"_s; + QTest::newRow("load html as autotext, switch to markdown, then plain") + << testFileUrl("text.qml") << QQuickTextEdit::AutoText << testFileUrl("hello.html") + << 3 << 1 << u"<!DOCTYPE HTML"_s + << QQuickTextEdit::MarkdownText << 3 << 1 << u"Γειά σου *Κόσμε*!"_s << 2 + << QQuickTextEdit::PlainText << 1 << -1 << u"Γειά σου *Κόσμε*!"_s; +} + +void tst_qquicktextdocument::overrideTextFormat() // QTBUG-120772 +{ + if (isMainFontFixed()) + QSKIP("fixed-pitch main font (QTBUG-103484)"); + + QFETCH(QUrl, qmlfile); + QFETCH(QQuickTextEdit::TextFormat, initialFormat); + QFETCH(QUrl, source); + QFETCH(int, expectedInitialFragmentCount); + QFETCH(int, expectedInitialItalicFragment); + QFETCH(QString, expectedTextPrefix); + + QFETCH(QQuickTextEdit::TextFormat, replacementFormat); + QFETCH(int, expectedFragmentCount); + QFETCH(int, expectedItalicFragment); + QFETCH(QString, expectedReplacementPrefix); + QFETCH(int, expectedTextChangedSignalsAfterReplacement); + + QFETCH(QQuickTextEdit::TextFormat, finalFormat); + QFETCH(int, expectedFinalFragmentCount); + QFETCH(int, expectedFinalItalicFragment); + QFETCH(QString, expectedFinalPrefix); + + QQuickView window; + QVERIFY(QQuickTest::showView(window, qmlfile)); + QQuickTextEdit *textEdit = qobject_cast<QQuickTextEdit *>(window.rootObject()); + QVERIFY(textEdit); + QQuickTextDocument *qqdoc = textEdit->property("textDocument").value<QQuickTextDocument*>(); + QVERIFY(qqdoc); + QTextDocument *doc = qqdoc->textDocument(); + QVERIFY(doc); + + textEdit->setTextFormat(initialFormat); + QCOMPARE(qqdoc->isModified(), false); + QCOMPARE(textEdit->property("sourceChangeCount").toInt(), 0); + QSignalSpy sourceChangedSpy(qqdoc, &QQuickTextDocument::sourceChanged); + QSignalSpy textChangedSpy(textEdit, &QQuickTextEdit::textChanged); + + qqdoc->setProperty("source", source); + QCOMPARE(sourceChangedSpy.size(), 1); + QCOMPARE(textEdit->property("sourceChangeCount").toInt(), 1); + QCOMPARE_GE(textChangedSpy.size(), 1); + auto fragCountAndItalic = fragmentsAndItalics(doc); + QCOMPARE(fragCountAndItalic.first, expectedInitialFragmentCount); + QCOMPARE(fragCountAndItalic.second, expectedInitialItalicFragment); + QString textPropValue = textEdit->text(); + qCDebug(lcTests) << "expect text()" << textPropValue.first(qMin(20, textPropValue.size() - 1)) + << "to start with" << expectedTextPrefix; + QVERIFY(textPropValue.startsWith(expectedTextPrefix)); + + textEdit->setTextFormat(replacementFormat); + QCOMPARE(qqdoc->isModified(), false); + QCOMPARE(sourceChangedSpy.size(), 1); + QCOMPARE_GE(textChangedSpy.size(), expectedTextChangedSignalsAfterReplacement); + fragCountAndItalic = fragmentsAndItalics(doc); + QCOMPARE(fragCountAndItalic.first, expectedFragmentCount); + QCOMPARE(fragCountAndItalic.second, expectedItalicFragment); + textPropValue = textEdit->text(); + qCDebug(lcTests) << "expect text()" << textPropValue.first(qMin(20, textPropValue.size() - 1)) + << "to start with" << expectedReplacementPrefix; + QVERIFY(textPropValue.startsWith(expectedReplacementPrefix)); + + textEdit->setTextFormat(finalFormat); + QCOMPARE(qqdoc->isModified(), false); + QCOMPARE(sourceChangedSpy.size(), 1); + QCOMPARE_GE(textChangedSpy.size(), expectedTextChangedSignalsAfterReplacement + 1); + fragCountAndItalic = fragmentsAndItalics(doc); + QCOMPARE(fragCountAndItalic.first, expectedFinalFragmentCount); + QCOMPARE(fragCountAndItalic.second, expectedFinalItalicFragment); + textPropValue = textEdit->text(); + qCDebug(lcTests) << "expect text()" << textPropValue.first(qMin(20, textPropValue.size() - 1)) + << "to start with" << expectedFinalPrefix; + QVERIFY(textPropValue.startsWith(expectedFinalPrefix)); +} + +void tst_qquicktextdocument::independentDocumentsSameSource_data() +{ + QTest::addColumn<QUrl>("qmlfile"); + + QTest::newRow("textFormat above source") << testFileUrl("sideBySideIndependent.qml"); + QTest::newRow("source above textFormat") << testFileUrl("sideBySideIndependentReverse.qml"); +} + +// ensure that two TextEdits' textFormat properties take effect, regardless of qml init order +void tst_qquicktextdocument::independentDocumentsSameSource() // QTBUG-120772 +{ + QFETCH(QUrl, qmlfile); + + QQuickView window; + QVERIFY(QQuickTest::showView(window, qmlfile)); + QQuickTextEdit *textEditPlain = window.rootObject()->findChild<QQuickTextEdit *>("plain"); + QVERIFY(textEditPlain); + QQuickTextEdit *textEditMarkdown = window.rootObject()->findChild<QQuickTextEdit *>("markdown"); + QVERIFY(textEditMarkdown); + + auto fragCountAndItalic = fragmentsAndItalics(textEditPlain->textDocument()->textDocument()); + QCOMPARE(fragCountAndItalic.first, 1); + QCOMPARE(fragCountAndItalic.second, -1); - QString name = "image"; - document.addResource(QTextDocument::ImageResource, name, image); - QTextImageFormat format; - format.setName(name); - QCOMPARE(image, document.image(format)); - QCOMPARE(image, document.resource(QTextDocument::ImageResource, name).value<QImage>()); + fragCountAndItalic = fragmentsAndItalics(textEditMarkdown->textDocument()->textDocument()); + QCOMPARE(fragCountAndItalic.first, 3); + QCOMPARE(fragCountAndItalic.second, 1); } QTEST_MAIN(tst_qquicktextdocument) |