geopro/tests/app/test_quill_delta.cpp

227 lines
9.1 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <gtest/gtest.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTextBlock>
#include <QTextDocument>
#include <QTextFragment>
#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);
}
// ── 行内:背景色往返(原版 ql-background─────────────────────────────────
TEST(QuillDelta, BackgroundColorRoundTrip) {
const auto ops = opsFromJson(R"([
{"insert":"hl","attributes":{"background":"#ffff00"}},
{"insert":"\n"}
])");
QTextDocument doc;
deltaToDocument(ops, doc);
const QTextCharFormat f = doc.begin().begin().fragment().charFormat();
EXPECT_EQ(f.background().color().name(QColor::HexRgb), QStringLiteral("#ffff00"));
const QJsonObject a = attrsOf(documentToDelta(doc), 0);
EXPECT_EQ(a.value(QStringLiteral("background")).toString(), QStringLiteral("#ffff00"));
}
// ── 行内:字体族往返(原版 ql-font token ↔ Qt family──────────────────────
TEST(QuillDelta, FontFamilyRoundTrip) {
const auto ops = opsFromJson(R"([
{"insert":"font","attributes":{"font":"Microsoft-YaHei"}},
{"insert":"\n"}
])");
QTextDocument doc;
deltaToDocument(ops, doc);
const QTextCharFormat f = doc.begin().begin().fragment().charFormat();
EXPECT_EQ(f.fontFamilies().toStringList().value(0), QStringLiteral("Microsoft YaHei"));
// 回写应还原为原版 token连字符形式
const QJsonObject a = attrsOf(documentToDelta(doc), 0);
EXPECT_EQ(a.value(QStringLiteral("font")).toString(), QStringLiteral("Microsoft-YaHei"));
}
// ── 块级:对齐往返(原版 ql-align────────────────────────────────────────
TEST(QuillDelta, AlignBlockRoundTrip) {
const auto ops = opsFromJson(R"([
{"insert":"centered"},
{"insert":"\n","attributes":{"align":"center"}}
])");
QTextDocument doc;
deltaToDocument(ops, doc);
EXPECT_EQ(doc.begin().blockFormat().alignment() & Qt::AlignHorizontal_Mask, Qt::AlignHCenter);
bool found = false;
for (const QJsonValue& v : documentToDelta(doc)) {
const QJsonObject a = v.toObject().value(QStringLiteral("attributes")).toObject();
if (a.value(QStringLiteral("align")).toString() == QStringLiteral("center")) found = true;
}
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());
}