// Дымовой тест главного окна: Main.qml поднимается так же, как в main.cpp, // без единого предупреждения QML, и ни одного запроса в настоящую сеть. // Ловит то, что не видно компилятору: опечатки в именах свойств и ключах Loc.t, // сломанные биндинги, несуществующие картинки в ресурсах. #include "testenv.h" #include "fakeinternet.h" #include "fixtures.h" #include "launcherpaths.h" #include "localization.h" #include #include #include #include #include Q_IMPORT_QML_PLUGIN(BiLauncherPlugin) namespace { QStringList g_problems; QtMessageHandler g_previous = nullptr; void collect(QtMsgType type, const QMessageLogContext &context, const QString &message) { // Предупреждения QML приходят с файлом qrc:/… или с категорией qt.qml.*. const QString file = QString::fromUtf8(context.file ? context.file : ""); const QString category = QString::fromUtf8(context.category ? context.category : ""); const bool fromQml = file.startsWith(QLatin1String("qrc:")) || file.endsWith(QLatin1String(".qml")) || category.startsWith(QLatin1String("qt.qml")) || message.contains(QLatin1String(".qml:")); if (type != QtDebugMsg && type != QtInfoMsg && fromQml) g_problems << message; if (g_previous) g_previous(type, context, message); } } // namespace class TestQmlSmoke : public QObject { Q_OBJECT FakeInternet m_net; private slots: void initTestCase() { QString error; if (m_net.start(&error)) m_net.setFallback(FakeResponse::error(503)); const QString game = TestEnv::freshDir(QStringLiteral("game")); Fixtures::writeJson(LauncherPaths::settingsFile(), QJsonObject{{"gameDir", game}, {"language", "en"}}); QQuickStyle::setStyle(QStringLiteral("Basic")); g_previous = qInstallMessageHandler(collect); } void cleanupTestCase() { qInstallMessageHandler(g_previous); } void init() { g_problems.clear(); } void mainWindowLoadsCleanly() { // Один движок на весь тест, как в приложении: Localization держит указатель // на JS-движок, создавший синглтон, и второй QQmlApplicationEngine после // уничтожения первого оставил бы его висячим. QQmlApplicationEngine engine; QStringList warnings; connect(&engine, &QQmlEngine::warnings, this, [&](const QList &list) { for (const QQmlError &e : list) warnings << e.toString(); }); engine.loadFromModule("BiLauncher", "Main"); QCOMPARE(engine.rootObjects().size(), 1); auto *window = qobject_cast(engine.rootObjects().constFirst()); QVERIFY(window); QVERIFY(QTest::qWaitForWindowExposed(window)); // Даём отработать отложенным таймерам, загрузке каталогов и анимациям. QTest::qWait(2500); QVERIFY2(warnings.isEmpty(), qPrintable(warnings.join(u'\n'))); QVERIFY2(g_problems.isEmpty(), qPrintable(g_problems.join(u'\n'))); // Смена языка на лету перестраивает Loc.t — биндинги не должны ломаться. const QString titleEn = window->title(); Localization::instance().setLanguage(QStringLiteral("ru")); QTest::qWait(500); Localization::instance().setLanguage(QStringLiteral("en")); QTest::qWait(300); QCOMPARE(window->title(), titleEn); QVERIFY2(warnings.isEmpty(), qPrintable(warnings.join(u'\n'))); QVERIFY2(g_problems.isEmpty(), qPrintable(g_problems.join(u'\n'))); // Наружу — только через FakeInternet, и только на известные адреса лаунчера. static const QStringList known{"mojang.com", "galeonworx.com", "adoptium.net", "fabricmc.net", "quiltmc.org", "minecraftforge.net", "neoforged.net"}; for (const QString &url : m_net.requestedUrls()) { const QString host = QUrl(url).host(); const bool ok = std::any_of(known.cbegin(), known.cend(), [&](const QString &domain) { return host.endsWith(domain); }); QVERIFY2(ok, qPrintable("неожиданный запрос: " + url)); } } }; BILAUNCHER_GUI_TEST_MAIN(TestQmlSmoke) #include "tst_qml_smoke.moc"