1 parent
ebf8ff7869
commit
41599fa82f
67 files changed
+8775
-4
No files matched your search
@@ -0,0 +1,8 @@
|
||||
# QML: Qt Quick Test для компонентов и диалогов + дымовой тест главного окна.
|
||||
bilauncher_add_test(tst_qml LABEL qml GUI TIMEOUT 180 SOURCES tst_qml.cpp)
|
||||
target_compile_definitions(tst_qml PRIVATE QUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
# Сами тесты лежат рядом, в tst_*.qml; Qt Creator покажет их в проекте.
|
||||
target_sources(tst_qml PRIVATE tst_components.qml tst_dialogs.qml)
|
||||
set_source_files_properties(tst_components.qml tst_dialogs.qml PROPERTIES HEADER_FILE_ONLY TRUE)
|
||||
|
||||
bilauncher_add_test(tst_qml_smoke LABEL qml GUI TIMEOUT 120 SOURCES tst_qml_smoke.cpp)
|
||||
@@ -0,0 +1,155 @@
|
||||
import QtQuick
|
||||
import QtTest
|
||||
import BiLauncher
|
||||
|
||||
// Маленькие переиспользуемые компоненты: DarkCombo, LabelledField,
|
||||
// ProgressPanel, LoaderRow.
|
||||
TestCase {
|
||||
id: testCase
|
||||
name: "Components"
|
||||
width: 600
|
||||
height: 400
|
||||
visible: true
|
||||
when: windowShown
|
||||
|
||||
Component { id: comboComponent; DarkCombo { width: 200; height: 32 } }
|
||||
Component { id: fieldComponent; LabelledField { width: 220 } }
|
||||
Component { id: panelComponent; ProgressPanel {} }
|
||||
Component { id: loaderRowComponent; LoaderRow { width: 300 } }
|
||||
|
||||
// Подставной backend для LoaderRow: те же методы и сигнал, что у LauncherBackend.
|
||||
Component {
|
||||
id: fakeBackendComponent
|
||||
QtObject {
|
||||
property var versions: ({})
|
||||
property bool loading: false
|
||||
property var refreshCalls: []
|
||||
signal loaderVersionsChanged(string loaderKey, string gameVersion)
|
||||
|
||||
function loaderVersions(key, game) { return versions[key + "/" + game] || [] }
|
||||
function refreshLoaderVersions(key, game) { refreshCalls.push(key + "/" + game) }
|
||||
function loaderVersionsLoading(key, game) { return loading }
|
||||
}
|
||||
}
|
||||
|
||||
// --- DarkCombo -------------------------------------------------------------
|
||||
|
||||
function test_comboSelectsAndDisplays() {
|
||||
const combo = createTemporaryObject(comboComponent, testCase, { model: ["Alpha", "Beta", "Gamma"] })
|
||||
verify(combo)
|
||||
compare(combo.count, 3)
|
||||
compare(combo.displayText, "Alpha")
|
||||
const activated = signalSpy.createObject(testCase, { target: combo, signalName: "activated" })
|
||||
|
||||
mouseClick(combo)
|
||||
tryVerify(() => combo.popup.opened)
|
||||
combo.currentIndex = 2
|
||||
compare(combo.displayText, "Gamma")
|
||||
combo.popup.close()
|
||||
tryVerify(() => !combo.popup.visible)
|
||||
|
||||
// Клавиатура: стрелки меняют выбор и шлют activated.
|
||||
combo.forceActiveFocus()
|
||||
keyClick(Qt.Key_Up)
|
||||
compare(combo.currentIndex, 1)
|
||||
compare(activated.count, 1)
|
||||
verify(combo.indicator.source.toString().length > 0)
|
||||
}
|
||||
|
||||
// --- LabelledField -------------------------------------------------------
|
||||
|
||||
function test_labelledFieldAliasesAndSignal() {
|
||||
const field = createTemporaryObject(fieldComponent, testCase, { label: "Ник", placeholder: "Steve" })
|
||||
verify(field)
|
||||
compare(field.children[0].text, "Ник")
|
||||
const input = field.children[1]
|
||||
compare(input.placeholderText, "Steve")
|
||||
|
||||
const finished = signalSpy.createObject(testCase, { target: field, signalName: "editingFinished" })
|
||||
input.forceActiveFocus()
|
||||
keyClick(Qt.Key_A)
|
||||
keyClick(Qt.Key_B)
|
||||
compare(field.text, "ab")
|
||||
keyClick(Qt.Key_Return)
|
||||
compare(finished.count, 1)
|
||||
|
||||
field.text = "preset"
|
||||
compare(input.text, "preset")
|
||||
}
|
||||
|
||||
// --- ProgressPanel -------------------------------------------------------
|
||||
|
||||
function test_progressPanelPercentAndCancel() {
|
||||
const panel = createTemporaryObject(panelComponent, testCase,
|
||||
{ title: "1.20.1", status: "Libraries", fraction: -1 })
|
||||
verify(panel)
|
||||
const texts = panel.children.filter(c => c.text !== undefined)
|
||||
const percent = texts.find(t => t.text === "…")
|
||||
verify(percent, "без известного итога показывается многоточие")
|
||||
|
||||
panel.fraction = 0.426
|
||||
compare(percent.text, "43%")
|
||||
panel.fraction = 1
|
||||
compare(percent.text, "100%")
|
||||
|
||||
// Подпись: detail важнее status.
|
||||
const detailText = texts.find(t => t.text === "Libraries")
|
||||
verify(detailText)
|
||||
panel.detail = "brigadier-1.1.8.jar"
|
||||
compare(detailText.text, "brigadier-1.1.8.jar")
|
||||
|
||||
const cancel = signalSpy.createObject(testCase, { target: panel, signalName: "cancelRequested" })
|
||||
mouseClick(panel, panel.width - 14, panel.height - 12)
|
||||
compare(cancel.count, 1)
|
||||
|
||||
panel.cancellable = false
|
||||
mouseClick(panel, panel.width - 14, panel.height - 12)
|
||||
compare(cancel.count, 1, "крестик скрыт — отмены нет")
|
||||
}
|
||||
|
||||
// --- LoaderRow -----------------------------------------------------------
|
||||
|
||||
function test_loaderRowFollowsBackend() {
|
||||
const backend = createTemporaryObject(fakeBackendComponent, testCase)
|
||||
backend.versions = {
|
||||
"fabric/1.20.1": [ { version: "0.16.10", label: "0.16.10", recommended: false, stable: true },
|
||||
{ version: "0.16.9", label: "0.16.9", recommended: false, stable: true } ]
|
||||
}
|
||||
const row = createTemporaryObject(loaderRowComponent, testCase,
|
||||
{ backend: backend, loaderKey: "fabric", title: "Fabric Loader" })
|
||||
verify(row)
|
||||
const changed = signalSpy.createObject(testCase, { target: row, signalName: "changed" })
|
||||
|
||||
// Без версии игры галочка недоступна, список пуст.
|
||||
compare(row.entries.length, 0)
|
||||
row.gameVersion = "1.20.1"
|
||||
compare(row.entries.length, 0, "пока галочка снята — версии не запрашиваются")
|
||||
|
||||
// Сборка уже с Fabric: выбранная версия сохраняется, запрос обновления уходит.
|
||||
row.applyBuild("fabric", "0.16.9")
|
||||
verify(row.checked)
|
||||
compare(row.selectedVersion, "0.16.9")
|
||||
compare(row.entries.length, 2)
|
||||
verify(backend.refreshCalls.indexOf("fabric/1.20.1") >= 0)
|
||||
compare(changed.count, 0, "применение сохранённой сборки не считается правкой")
|
||||
|
||||
// Каталог обновился и выбранной версии в нём больше нет — берётся первая.
|
||||
backend.versions = { "fabric/1.20.1": [ { version: "0.17.0", label: "0.17.0", recommended: true, stable: true } ] }
|
||||
backend.loaderVersionsChanged("fabric", "1.20.1")
|
||||
compare(row.selectedVersion, "0.17.0")
|
||||
compare(changed.count, 1)
|
||||
|
||||
// Чужой лоадер в сборке — галочка снимается.
|
||||
row.applyBuild("forge", "47.4.0")
|
||||
verify(!row.checked)
|
||||
compare(row.selectedVersion, "")
|
||||
compare(row.entries.length, 0)
|
||||
|
||||
// Смена версии игры сбрасывает выбор.
|
||||
row.applyBuild("fabric", "0.17.0")
|
||||
row.gameVersion = "1.19.4"
|
||||
compare(row.selectedVersion, "")
|
||||
}
|
||||
|
||||
Component { id: signalSpy; SignalSpy {} }
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import QtQuick
|
||||
import QtTest
|
||||
import BiLauncher
|
||||
|
||||
// Диалоги с настоящим LauncherBackend. Данные для них подготавливает tst_qml.cpp:
|
||||
// кеш манифеста версий (1.20.1, 25w33a, b1.7.3), установленная 1.20.1,
|
||||
// кеш каталога сезонных сборок с одной сборкой. Сеть закрыта FakeInternet.
|
||||
TestCase {
|
||||
id: testCase
|
||||
name: "Dialogs"
|
||||
width: 1280
|
||||
height: 720
|
||||
visible: true
|
||||
when: windowShown
|
||||
|
||||
LauncherBackend { id: launcherBackend }
|
||||
|
||||
Component { id: versionPickerComponent; VersionPickerDialog { backend: launcherBackend } }
|
||||
Component { id: javaPickerComponent; JavaPickerDialog { backend: launcherBackend } }
|
||||
Component { id: buildsComponent; BuildsDialog { backend: launcherBackend } }
|
||||
Component { id: seasonalComponent; SeasonalBuildsDialog { backend: launcherBackend } }
|
||||
Component { id: signalSpy; SignalSpy {} }
|
||||
|
||||
function openAndWait(dialog) {
|
||||
tryVerify(() => dialog.opened, 3000, "диалог не открылся")
|
||||
}
|
||||
|
||||
function test_versionPickerFiltersCatalog() {
|
||||
const picker = createTemporaryObject(versionPickerComponent, testCase)
|
||||
verify(picker)
|
||||
tryVerify(() => launcherBackend.versionCatalog.length >= 3)
|
||||
|
||||
picker.openFor("1.20.1")
|
||||
openAndWait(picker)
|
||||
compare(picker.category, "release")
|
||||
compare(picker.visibleEntries.length, 1)
|
||||
compare(picker.visibleEntries[0].id, "1.20.1")
|
||||
verify(picker.visibleEntries[0].installed)
|
||||
compare(picker.indexOfSelected(), 0)
|
||||
|
||||
picker.category = "snapshot"
|
||||
compare(picker.visibleEntries.length, 1)
|
||||
compare(picker.visibleEntries[0].id, "25w33a")
|
||||
compare(picker.categoryOf("b1.7.3"), "old_beta")
|
||||
compare(picker.categoryOf("no-such"), "release")
|
||||
|
||||
picker.category = "release"
|
||||
picker.filterText = "1.19"
|
||||
compare(picker.visibleEntries.length, 0)
|
||||
picker.filterText = ""
|
||||
picker.installedOnly = true
|
||||
compare(picker.visibleEntries.length, 1)
|
||||
|
||||
const chosen = signalSpy.createObject(testCase, { target: picker, signalName: "versionChosen" })
|
||||
picker.selectedId = "1.20.1"
|
||||
picker.acceptSelection()
|
||||
compare(chosen.count, 1)
|
||||
compare(chosen.signalArguments[0][0], "1.20.1")
|
||||
tryVerify(() => !picker.visible)
|
||||
}
|
||||
|
||||
function test_javaPickerOpensWithRequirement() {
|
||||
const picker = createTemporaryObject(javaPickerComponent, testCase)
|
||||
verify(picker)
|
||||
picker.openFor("", 17)
|
||||
openAndWait(picker)
|
||||
compare(picker.requiredMajor, 17)
|
||||
compare(picker.category, "java")
|
||||
compare(picker.selectedEntry, null)
|
||||
const chosen = signalSpy.createObject(testCase, { target: picker, signalName: "runtimeChosen" })
|
||||
picker.acceptSelection() // ничего не выбрано — ничего не происходит
|
||||
compare(chosen.count, 0)
|
||||
picker.close()
|
||||
}
|
||||
|
||||
function test_buildsDialogEditsBackend() {
|
||||
const dialog = createTemporaryObject(buildsComponent, testCase)
|
||||
verify(dialog)
|
||||
const before = launcherBackend.customBuildNames.length
|
||||
const name = dialog.newBuildName()
|
||||
verify(name.length > 0)
|
||||
verify(launcherBackend.customBuildNames.indexOf(name) === -1)
|
||||
|
||||
launcherBackend.addCustomBuild(name, "", "1.20.1")
|
||||
tryVerify(() => !launcherBackend.busy, 20000)
|
||||
compare(launcherBackend.customBuildNames.length, before + 1)
|
||||
|
||||
dialog.open()
|
||||
openAndWait(dialog)
|
||||
dialog.selectBuild(before)
|
||||
compare(dialog.editIndex, before)
|
||||
dialog.commit({ serverUrl: "play.example.com" })
|
||||
compare(launcherBackend.customBuildAt(before).serverUrl, "play.example.com")
|
||||
verify(dialog.newBuildName() !== name)
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
function test_seasonalDialogShowsCatalog() {
|
||||
const dialog = createTemporaryObject(seasonalComponent, testCase)
|
||||
verify(dialog)
|
||||
dialog.openCatalog()
|
||||
openAndWait(dialog)
|
||||
tryVerify(() => launcherBackend.seasonalCatalog.length === 1)
|
||||
const entry = launcherBackend.seasonalCatalog[0]
|
||||
compare(entry.id, "season-qml")
|
||||
compare(dialog.cellText(entry, "loaderLabel"), entry.loaderTitle + " 0.16.10")
|
||||
compare(dialog.formatMb(3 * 1024 * 1024), "3.0")
|
||||
compare(dialog.formatMb(0), "")
|
||||
dialog.close()
|
||||
}
|
||||
|
||||
function test_languageSwitchUpdatesTexts() {
|
||||
const en = Loc.t.app.tooltip.openMods
|
||||
launcherBackend.updateSettings({ language: "ru" })
|
||||
verify(Loc.t.app.tooltip.openMods !== en)
|
||||
launcherBackend.updateSettings({ language: "en" })
|
||||
compare(Loc.t.app.tooltip.openMods, en)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Qt Quick Test для QML-компонентов лаунчера.
|
||||
//
|
||||
// Модуль BiLauncher у приложения вшит в исполняемый файл и плагина не имеет,
|
||||
// поэтому qmltestrunner его не импортирует. Здесь тот же модуль собран
|
||||
// статической библиотекой (bilauncher_testlib) и подключён через
|
||||
// Q_IMPORT_QML_PLUGIN — тесты пишут обычный «import BiLauncher».
|
||||
|
||||
#include "testenv.h"
|
||||
#include "fakeinternet.h"
|
||||
#include "fixtures.h"
|
||||
|
||||
#include "launcherpaths.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include <QQmlEngine>
|
||||
#include <QQuickStyle>
|
||||
#include <QtQml/qqmlextensionplugin.h>
|
||||
#include <QtQuickTest/quicktest.h>
|
||||
|
||||
Q_IMPORT_QML_PLUGIN(BiLauncherPlugin)
|
||||
|
||||
class Setup : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public slots:
|
||||
void applicationAvailable()
|
||||
{
|
||||
TestEnv::finishSetup();
|
||||
QQuickStyle::setStyle(QStringLiteral("Basic"));
|
||||
seedData();
|
||||
// Диалоги создают настоящий LauncherBackend — наружу он не должен ходить.
|
||||
QString error;
|
||||
if (m_net.start(&error))
|
||||
m_net.setFallback(FakeResponse::error(503));
|
||||
}
|
||||
|
||||
void qmlEngineAvailable(QQmlEngine *engine)
|
||||
{
|
||||
engine->addImportPath(QStringLiteral("qrc:/qt/qml"));
|
||||
}
|
||||
|
||||
private:
|
||||
// Данные, на которых работают диалоги (см. tst_dialogs.qml).
|
||||
static void seedData()
|
||||
{
|
||||
const QString game = TestEnv::freshDir(QStringLiteral("game"));
|
||||
const QString now = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
|
||||
Fixtures::writeJson(LauncherPaths::settingsFile(), QJsonObject{{"gameDir", game}, {"language", "en"}});
|
||||
|
||||
const auto version = [](const QString &id, const QString &type, const QString &time) {
|
||||
return QJsonObject{{"id", id}, {"type", type}, {"releaseTime", time}, {"sha1", "00"},
|
||||
{"url", "https://piston-meta.mojang.com/v1/packages/x/" + id + ".json"}};
|
||||
};
|
||||
Fixtures::writeJson(LauncherPaths::versionManifestFile(), QJsonObject{
|
||||
{"fetchedAt", now},
|
||||
{"versions", QJsonArray{version("25w33a", "snapshot", "2025-08-13T12:00:00Z"),
|
||||
version("1.20.1", "release", "2023-06-12T13:25:51Z"),
|
||||
version("b1.7.3", "old_beta", "2011-07-08T00:00:00Z")}}});
|
||||
Fixtures::write(game + "/versions/1.20.1/1.20.1.json",
|
||||
"{\"id\":\"1.20.1\",\"mainClass\":\"net.minecraft.client.main.Main\"}");
|
||||
|
||||
const QJsonObject archive{{"url", "https://launcher-builds.galeonworx.com/packs/q.zip"},
|
||||
{"size", 3 * 1024 * 1024}, {"sha256", QString(64, u'a')}};
|
||||
const QJsonObject build{{"id", "season-qml"}, {"name", "QML Season"}, {"revision", 1},
|
||||
{"minecraftVersion", "1.20.1"}, {"loader", "fabric"},
|
||||
{"loaderVersion", "0.16.10"}, {"seasonStart", "2026-01-01"},
|
||||
{"archive", archive}};
|
||||
Fixtures::writeJson(LauncherPaths::seasonalCatalogFile(), QJsonObject{
|
||||
{"formatVersion", 1}, {"fetchedAt", now},
|
||||
{"baseUrl", "https://launcher-builds.galeonworx.com"}, {"builds", QJsonArray{build}}});
|
||||
}
|
||||
|
||||
FakeInternet m_net;
|
||||
};
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
TestEnv::prepareSandbox();
|
||||
QTEST_SET_MAIN_SOURCE_PATH
|
||||
Setup setup;
|
||||
return quick_test_main_with_setup(argc, argv, "bilauncher_qml", QUICK_TEST_SOURCE_DIR, &setup);
|
||||
}
|
||||
|
||||
#include "tst_qml.moc"
|
||||
@@ -0,0 +1,109 @@
|
||||
// Дымовой тест главного окна: Main.qml поднимается так же, как в main.cpp,
|
||||
// без единого предупреждения QML, и ни одного запроса в настоящую сеть.
|
||||
// Ловит то, что не видно компилятору: опечатки в именах свойств и ключах Loc.t,
|
||||
// сломанные биндинги, несуществующие картинки в ресурсах.
|
||||
|
||||
#include "testenv.h"
|
||||
#include "fakeinternet.h"
|
||||
#include "fixtures.h"
|
||||
|
||||
#include "launcherpaths.h"
|
||||
#include "localization.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQuickStyle>
|
||||
#include <QQuickWindow>
|
||||
#include <QtQml/qqmlextensionplugin.h>
|
||||
|
||||
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<QQmlError> &list) {
|
||||
for (const QQmlError &e : list)
|
||||
warnings << e.toString();
|
||||
});
|
||||
engine.loadFromModule("BiLauncher", "Main");
|
||||
QCOMPARE(engine.rootObjects().size(), 1);
|
||||
auto *window = qobject_cast<QQuickWindow *>(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"
|
||||
Reference in new issue
Block a user