#include #include #include #include #include #include #include #include "panels/QuillDelta.hpp" using namespace geopro::app; namespace { QJsonArray opsFromJson(const char* json) { return QJsonDocument::fromJson(json).array(); } // 从 Delta ops 取第 idx 个文本 op 的 attributes(便于断言)。 QJsonObject attrsOf(const QJsonArray& ops, int idx) { return ops.at(idx).toObject().value(QStringLiteral("attributes")).toObject(); } } // namespace // ── 反序列化(Delta → 文档):基本行内样式落到字符格式 ────────────────────── TEST(QuillDelta, DeltaToDocumentAppliesInlineFormats) { const auto ops = opsFromJson(R"([ {"insert":"Hello","attributes":{"bold":true,"italic":true,"underline":true, "color":"#ff0000","size":"24px"}}, {"insert":"\n"} ])"); QTextDocument doc; deltaToDocument(ops, doc); EXPECT_EQ(doc.toPlainText(), QStringLiteral("Hello")); const QTextFragment frag = doc.begin().begin().fragment(); ASSERT_TRUE(frag.isValid()); const QTextCharFormat f = frag.charFormat(); EXPECT_GE(f.fontWeight(), QFont::Bold); EXPECT_TRUE(f.fontItalic()); EXPECT_TRUE(f.fontUnderline()); EXPECT_EQ(f.foreground().color().name(QColor::HexRgb), QStringLiteral("#ff0000")); EXPECT_NEAR(f.fontPointSize(), 24.0 * 3.0 / 4.0, 0.01); // 24px → 18pt } // ── 序列化(文档 → Delta):行内样式回写 attributes ───────────────────────── TEST(QuillDelta, DocumentToDeltaEmitsInlineAttrs) { QTextDocument doc; QTextCursor cur(&doc); QTextCharFormat f; f.setFontWeight(QFont::Bold); f.setForeground(QColor(QStringLiteral("#00ff00"))); cur.insertText(QStringLiteral("World"), f); const QJsonArray ops = documentToDelta(doc); ASSERT_GE(ops.size(), 1); EXPECT_EQ(ops.at(0).toObject().value(QStringLiteral("insert")).toString(), QStringLiteral("World")); const QJsonObject a = attrsOf(ops, 0); EXPECT_TRUE(a.value(QStringLiteral("bold")).toBool()); EXPECT_EQ(a.value(QStringLiteral("color")).toString(), QStringLiteral("#00ff00")); } // ── 往返:纯文本不丢、不多生成空块 ───────────────────────────────────────── TEST(QuillDelta, RoundTripPlainTextSingleLine) { const auto ops = opsFromJson(R"([{"insert":"just text"},{"insert":"\n"}])"); QTextDocument doc; deltaToDocument(ops, doc); EXPECT_EQ(doc.toPlainText(), QStringLiteral("just text")); EXPECT_EQ(doc.blockCount(), 1); // 不应多出尾部空块 const QJsonArray back = documentToDelta(doc); QString text; for (const QJsonValue& v : back) text += v.toObject().value(QStringLiteral("insert")).toString(); EXPECT_EQ(text, QStringLiteral("just text\n")); } // ── 往返:多行 + 行内样式保持 ────────────────────────────────────────────── TEST(QuillDelta, RoundTripMultilineWithBoldPreserved) { const auto ops = opsFromJson(R"([ {"insert":"line1 "}, {"insert":"bold","attributes":{"bold":true}}, {"insert":"\nline2\n"} ])"); QTextDocument doc; deltaToDocument(ops, doc); EXPECT_EQ(doc.toPlainText(), QStringLiteral("line1 bold\nline2")); EXPECT_EQ(doc.blockCount(), 2); // 第二趟往返稳定:重新序列化后再反序列化文本一致。 const QJsonArray back = documentToDelta(doc); QTextDocument doc2; deltaToDocument(back, doc2); EXPECT_EQ(doc2.toPlainText(), QStringLiteral("line1 bold\nline2")); EXPECT_EQ(doc2.blockCount(), 2); } // ── 块级:标题落到 headingLevel 并回写 header ────────────────────────────── TEST(QuillDelta, HeaderBlockRoundTrip) { const auto ops = opsFromJson(R"([ {"insert":"Title"}, {"insert":"\n","attributes":{"header":2}}, {"insert":"body"}, {"insert":"\n"} ])"); QTextDocument doc; deltaToDocument(ops, doc); EXPECT_EQ(doc.begin().blockFormat().headingLevel(), 2); const QJsonArray back = documentToDelta(doc); // 找到带 header 的换行 op。 bool found = false; for (const QJsonValue& v : back) { const QJsonObject op = v.toObject(); if (op.value(QStringLiteral("insert")).toString() == QStringLiteral("\n") && op.value(QStringLiteral("attributes")).toObject().value(QStringLiteral("header")).toInt() == 2) { found = true; break; } } EXPECT_TRUE(found); } // ── 块级:有序列表 → ListDecimal,回写 list:ordered ─────────────────────── TEST(QuillDelta, OrderedListBlockRoundTrip) { const auto ops = opsFromJson(R"([ {"insert":"item"}, {"insert":"\n","attributes":{"list":"ordered"}} ])"); QTextDocument doc; deltaToDocument(ops, doc); ASSERT_NE(doc.begin().textList(), nullptr); const QJsonArray back = documentToDelta(doc); bool found = false; for (const QJsonValue& v : back) { const QJsonObject a = v.toObject().value(QStringLiteral("attributes")).toObject(); if (a.value(QStringLiteral("list")).toString() == QStringLiteral("ordered")) { found = true; break; } } EXPECT_TRUE(found); } // ── 容错:无法识别的 attributes 降级(保留文本,不崩) ────────────────────── TEST(QuillDelta, UnknownAttributesDegradeGracefully) { const auto ops = opsFromJson(R"([ {"insert":"keepme","attributes":{"script":"super","strike":true,"link":"http://x"}}, {"insert":"\n"} ])"); QTextDocument doc; deltaToDocument(ops, doc); EXPECT_EQ(doc.toPlainText(), QStringLiteral("keepme")); } // ── 容错:非文本 insert(图片/嵌入对象)被丢弃,不崩 ─────────────────────── TEST(QuillDelta, NonStringInsertDropped) { const auto ops = opsFromJson(R"([ {"insert":"text"}, {"insert":{"image":"data:..."}}, {"insert":"\n"} ])"); QTextDocument doc; deltaToDocument(ops, doc); EXPECT_EQ(doc.toPlainText(), QStringLiteral("text")); } // ── 空 ops:空文档 ───────────────────────────────────────────────────────── TEST(QuillDelta, EmptyOpsYieldEmptyDocument) { QTextDocument doc; deltaToDocument(QJsonArray{}, doc); EXPECT_TRUE(doc.toPlainText().isEmpty()); }