218 lines
13 KiB
C++
218 lines
13 KiB
C++
#include "app/controller.h"
|
|
#include "app/pdf_canvas.h"
|
|
#include "web/web_profile.h"
|
|
#include <QInputMethodEvent>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QProcess>
|
|
#include <QQmlApplicationEngine>
|
|
#include <QQmlContext>
|
|
#include <QScopeGuard>
|
|
#include <QTemporaryDir>
|
|
#include <QtTest>
|
|
#include <QtWebEngineQuick>
|
|
using namespace docview;
|
|
|
|
class ImeTest : public QObject {
|
|
Q_OBJECT
|
|
QJsonArray events_, commands_, phases_;
|
|
QString output_ = qEnvironmentVariable("DOCVIEW_IME_OUTPUT");
|
|
bool wayland() const { return qEnvironmentVariable("DOCVIEW_IME_PLATFORM") == "wayland"; }
|
|
bool eventFilter(QObject *object, QEvent *event) override {
|
|
if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
|
|
auto *key = static_cast<QKeyEvent *>(event);
|
|
events_.append(QJsonObject{{"object", object->metaObject()->className()},
|
|
{"type", event->type() == QEvent::KeyPress ? "press" : "release"},
|
|
{"key", key->key()}, {"text", key->text()}, {"scanCode", qint64(key->nativeScanCode())},
|
|
{"virtualKey", qint64(key->nativeVirtualKey())}, {"modifiers", int(key->modifiers())}});
|
|
}
|
|
if (event->type() == QEvent::InputMethod) {
|
|
auto *input = static_cast<QInputMethodEvent *>(event);
|
|
events_.append(QJsonObject{{"object", object->metaObject()->className()},
|
|
{"preedit", input->preeditString()}, {"commit", input->commitString()}});
|
|
}
|
|
return false;
|
|
}
|
|
bool run(const QString &program, const QStringList &arguments) {
|
|
QProcess process; process.start(program, arguments);
|
|
if (!process.waitForStarted(3000)) return false;
|
|
QElapsedTimer deadline; deadline.start();
|
|
while (process.state() != QProcess::NotRunning && deadline.elapsed() < 10000) QTest::qWait(5);
|
|
if (process.state() != QProcess::NotRunning) { process.kill(); process.waitForFinished(1000); }
|
|
const auto out = QString::fromUtf8(process.readAllStandardOutput());
|
|
const auto err = QString::fromUtf8(process.readAllStandardError());
|
|
commands_.append(QJsonObject{{"program", program}, {"arguments", QJsonArray::fromStringList(arguments)},
|
|
{"exitCode", process.exitCode()}, {"stdout", out}, {"stderr", err}});
|
|
if (process.exitCode()) qWarning() << program << arguments << out << err;
|
|
QTest::qWait(60);
|
|
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
|
|
}
|
|
bool setEngine(const QString &name) {
|
|
if (!run("gdbus", {"call", "--address", qEnvironmentVariable("IBUS_ADDRESS"),
|
|
"--dest", "org.freedesktop.IBus", "--object-path", "/org/freedesktop/IBus",
|
|
"--method", "org.freedesktop.IBus.SetGlobalEngine", name})) return false;
|
|
return run("ibus", {"engine"}) && commands_.last().toObject().value("stdout").toString().trimmed() == name;
|
|
}
|
|
bool keys(const QStringList &keys) {
|
|
if (!wayland()) return run("xdotool", QStringList{"key", "--clearmodifiers", "--delay", "65"} + keys);
|
|
QStringList arguments;
|
|
for (const auto &key : keys) arguments << "-k" << key << "-s" << "65";
|
|
return run("wtype", arguments);
|
|
}
|
|
bool text(const QString &value) {
|
|
return wayland() ? run("wtype", {"-d", "100", value})
|
|
: run("xdotool", {"type", "--clearmodifiers", "--delay", "100", value});
|
|
}
|
|
bool raw(int code, bool shift = false) {
|
|
// wtype supplies its own keysym map. Wayland checks IME transport and
|
|
// logical shortcuts; only the X11 cases test physical US/JP keycodes.
|
|
if (wayland()) {
|
|
if (code == 47 && shift) return keys({"colon"});
|
|
if (code == 61 && !shift) return keys({"slash"});
|
|
if (code == 21 && shift) return keys({"plus"});
|
|
if (code == 20 && !shift) return keys({"minus"});
|
|
return false;
|
|
}
|
|
QStringList args{qEnvironmentVariable("DOCVIEW_IME_RAW_KEY"), QString::number(code)};
|
|
if (shift) args << "--shift";
|
|
return run("python3", args);
|
|
}
|
|
private slots:
|
|
void initTestCase() {
|
|
QCOMPARE(QGuiApplication::platformName(), qEnvironmentVariable("DOCVIEW_IME_PLATFORM"));
|
|
QCOMPARE(qEnvironmentVariable("QT_IM_MODULE"), "ibus");
|
|
QCOMPARE(qEnvironmentVariable("DOCVIEW_PRIVATE_IME"), "1");
|
|
QVERIFY(!output_.isEmpty());
|
|
qApp->installEventFilter(this);
|
|
}
|
|
void realImeAndLogicalKeys_data() {
|
|
QTest::addColumn<QString>("layout");
|
|
if (wayland()) QTest::newRow("virtual-keysyms") << QString("virtual-keysyms");
|
|
else {
|
|
QTest::newRow("us") << QString("us");
|
|
QTest::newRow("jp") << QString("jp");
|
|
}
|
|
}
|
|
void realImeAndLogicalKeys() {
|
|
QFETCH(QString, layout);
|
|
events_ = {}; commands_ = {}; phases_ = {}; bool success = false;
|
|
const auto save = qScopeGuard([&] {
|
|
QFile file(output_ + "/" + layout + ".json");
|
|
if (file.open(QIODevice::WriteOnly)) file.write(QJsonDocument(QJsonObject{
|
|
{"success", success}, {"layout", layout}, {"qtVersion", qVersion()},
|
|
{"qtPlatform", QGuiApplication::platformName()},
|
|
{"events", events_}, {"commands", commands_}, {"phases", phases_}}).toJson());
|
|
});
|
|
QTemporaryDir isolated; QVERIFY(isolated.isValid());
|
|
const auto config = isolated.filePath("config.toml");
|
|
QFile f(config); QVERIFY(f.open(QIODevice::WriteOnly));
|
|
f.write("schema_version=1\n[privacy]\nremember_position=false\nrecent_documents=0\n"); f.close();
|
|
Controller reader;
|
|
reader.initialize(config, false, {config, isolated.filePath("state.json"), isolated.filePath("cache"), isolated.filePath("logs")});
|
|
QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("reader", &reader);
|
|
engine.load(QUrl("qrc:/qml/Main.qml")); QVERIFY(!engine.rootObjects().isEmpty());
|
|
auto *window = qobject_cast<QQuickWindow *>(engine.rootObjects().first()); QVERIFY(window);
|
|
reader.attachWindow(window); window->resize(1100, 760);
|
|
qApp->removeEventFilter(this); qApp->installEventFilter(this);
|
|
auto *canvas = window->findChild<PdfCanvas *>(); QVERIFY(canvas);
|
|
QVERIFY(QTest::qWaitForWindowExposed(window, 10000));
|
|
if (wayland()) {
|
|
QVERIFY(run("swaymsg", {"-s", qEnvironmentVariable("SWAYSOCK"),
|
|
QString("[pid=%1] focus").arg(QCoreApplication::applicationPid())}));
|
|
} else QVERIFY(run("xdotool", {"windowfocus", "--sync", QString::number(window->winId())}));
|
|
QTRY_COMPARE_WITH_TIMEOUT(QGuiApplication::focusWindow(), window, 5000);
|
|
reader.openPath(qEnvironmentVariable("DOCVIEW_IME_PDF"));
|
|
QTRY_VERIFY_WITH_TIMEOUT(reader.state() == "Ready" && canvas->viewportReady(), 15000);
|
|
canvas->setZoom(300);
|
|
QVERIFY(canvas->totalHeight() > canvas->height() + 300);
|
|
canvas->scroll(0, 200);
|
|
QCOMPARE(canvas->offset(), 200.0);
|
|
QTRY_VERIFY_WITH_TIMEOUT(canvas->viewportReady(), 10000);
|
|
QVERIFY(setEngine("xkb:us::eng"));
|
|
if (!wayland()) {
|
|
QVERIFY(run("setxkbmap", {"-model", layout == "jp" ? "jp106" : "pc105", "-layout", layout, "-option", ""}));
|
|
QVERIFY(run("setxkbmap", {"-query"}));
|
|
QVERIFY(run("xkbcomp", {qEnvironmentVariable("DISPLAY"), output_ + "/" + layout + ".xkb"}));
|
|
}
|
|
const auto offset = canvas->offset();
|
|
// Use physical X keycodes for symbols: ':' differs between US and JP.
|
|
QVERIFY(raw(layout == "jp" ? 48 : 47, layout != "jp"));
|
|
QTRY_COMPARE(reader.mode(), "command");
|
|
QVERIFY(keys({"Escape"})); QTRY_COMPARE(reader.mode(), "normal");
|
|
QVERIFY(raw(61)); QTRY_COMPARE(reader.mode(), "search");
|
|
auto *field = window->activeFocusItem(); QVERIFY(field);
|
|
QVERIFY(field->metaObject()->indexOfProperty("inputMethodComposing") >= 0);
|
|
QVERIFY(setEngine("mozc-jp"));
|
|
QVERIFY(run("ibus", {"engine"}));
|
|
QVERIFY(keys({"Hiragana"}));
|
|
QVERIFY(text("nihongo"));
|
|
QTRY_VERIFY_WITH_TIMEOUT(field->property("inputMethodComposing").toBool(), 5000);
|
|
QCOMPARE(reader.mode(), "search"); QCOMPARE(canvas->offset(), offset);
|
|
QVERIFY(keys({"space"}));
|
|
QTRY_VERIFY(field->property("inputMethodComposing").toBool());
|
|
QVERIFY(window->grabWindow().save(output_ + "/" + layout + "-conversion.png"));
|
|
QVERIFY(keys({"Return"}));
|
|
QTRY_VERIFY(!field->property("inputMethodComposing").toBool());
|
|
QTRY_COMPARE(field->property("text").toString(), QString::fromUtf8("日本語"));
|
|
QCOMPARE(reader.mode(), "search"); QCOMPARE(canvas->offset(), offset);
|
|
phases_.append(QJsonObject{{"name", "conversion-enter-keeps-search"}, {"committed", field->property("text").toString()}});
|
|
// A second Enter performs the application's search; conversion Enter did not.
|
|
QVERIFY(keys({"Return"})); QTRY_COMPARE(reader.mode(), "normal");
|
|
QVERIFY(raw(61)); QTRY_COMPARE(reader.mode(), "search"); field = window->activeFocusItem();
|
|
QVERIFY(text("jikan123"));
|
|
QTRY_VERIFY_WITH_TIMEOUT(field->property("inputMethodComposing").toBool(), 5000);
|
|
QCOMPARE(canvas->offset(), offset); QCOMPARE(reader.mode(), "search"); QVERIFY(reader.pending().isEmpty());
|
|
QVERIFY(keys({"space"})); QTRY_VERIFY(field->property("inputMethodComposing").toBool());
|
|
QVERIFY(keys({"Escape"}));
|
|
QCOMPARE(reader.mode(), "search"); QCOMPARE(canvas->offset(), offset);
|
|
QTRY_VERIFY(field->property("inputMethodComposing").toBool());
|
|
phases_.append(QJsonObject{{"name", "conversion-escape-keeps-reading"}, {"offset", offset}});
|
|
QVERIFY(keys({"Escape"}));
|
|
QTRY_VERIFY(!field->property("inputMethodComposing").toBool());
|
|
QCOMPARE(reader.mode(), "search"); QCOMPARE(canvas->offset(), offset);
|
|
QVERIFY(keys({"Escape"})); QTRY_COMPARE(reader.mode(), "normal");
|
|
phases_.append(QJsonObject{{"name", "letters-digits-escape-protected"}, {"offsetUnchanged", true}, {"offset", offset}});
|
|
// The command field uses a different accepted action from search.
|
|
QVERIFY(raw(layout == "jp" ? 48 : 47, layout != "jp"));
|
|
QTRY_COMPARE(reader.mode(), "command"); field = window->activeFocusItem();
|
|
QVERIFY(text("kakunin"));
|
|
QTRY_VERIFY(field->property("inputMethodComposing").toBool());
|
|
QVERIFY(keys({"space"})); QVERIFY(keys({"Return"}));
|
|
QTRY_COMPARE(field->property("text").toString(), QString::fromUtf8("確認"));
|
|
QVERIFY(!field->property("inputMethodComposing").toBool());
|
|
QCOMPARE(reader.mode(), "command"); QCOMPARE(canvas->offset(), offset);
|
|
QVERIFY(window->grabWindow().save(output_ + "/" + layout + "-command.png"));
|
|
QVERIFY(keys({"Escape"})); QTRY_COMPARE(reader.mode(), "normal");
|
|
phases_.append(QJsonObject{{"name", "conversion-enter-keeps-command"}, {"committed", QString::fromUtf8("確認")}});
|
|
// Restore the engine and the actual map before checking logical zoom keys.
|
|
QVERIFY(setEngine("xkb:us::eng"));
|
|
if (!wayland()) QVERIFY(run("setxkbmap", {"-model", layout == "jp" ? "jp106" : "pc105", "-layout", layout, "-option", ""}));
|
|
const auto zoom = canvas->zoom();
|
|
QVERIFY(raw(layout == "jp" ? 47 : 21, true)); QTRY_VERIFY(canvas->zoom() > zoom);
|
|
const auto enlarged = canvas->zoom(); QVERIFY(raw(20)); QTRY_VERIFY(canvas->zoom() < enlarged);
|
|
const auto beforeScroll = canvas->offset();
|
|
QVERIFY(keys({"j"})); QTRY_VERIFY(canvas->offset() > beforeScroll);
|
|
const auto afterDown = canvas->offset();
|
|
QVERIFY(keys({"k"})); QTRY_VERIFY(canvas->offset() < afterDown);
|
|
phases_.append(QJsonObject{{"name", wayland() ? "logical-symbols-and-navigation" : "physical-symbols-and-navigation"}, {"colon", true}, {"slash", true}, {"plus", true}, {"minus", true}, {"beforeScroll", beforeScroll}, {"afterDown", afterDown}, {"afterUp", canvas->offset()}});
|
|
bool japaneseCommit = false, preedit = false;
|
|
for (const auto &value : events_) {
|
|
const auto event = value.toObject(); preedit |= !event.value("preedit").toString().isEmpty();
|
|
japaneseCommit |= event.value("commit").toString() == QString::fromUtf8("日本語");
|
|
}
|
|
QVERIFY(preedit); QVERIFY(japaneseCommit);
|
|
QVERIFY(window->grabWindow().save(output_ + "/" + layout + "-final.png"));
|
|
reader.closeDocument(); QCOMPARE(reader.state(), "Empty"); success = true;
|
|
}
|
|
};
|
|
|
|
int main(int argc, char **argv) {
|
|
registerDocumentScheme(); QtWebEngineQuick::initialize();
|
|
QGuiApplication app(argc, argv); app.setQuitOnLastWindowClosed(false);
|
|
app.setApplicationName("DocView IME validation"); app.setOrganizationName("DocView");
|
|
qmlRegisterType<PdfCanvas>("DocView", 1, 0, "PdfCanvas");
|
|
ImeTest test; return QTest::qExec(&test, argc, argv);
|
|
}
|
|
#include "test_ime.moc"
|