feat(Storybook): Add tests runner available from the app

This commit is contained in:
Michał Cieślak 2023-10-10 15:42:50 +02:00 committed by Michał
parent 0caec2ab1c
commit 8a996a9175
10 changed files with 491 additions and 1 deletions

View File

@ -52,6 +52,7 @@ add_library(${PROJECT_LIB}
modelutils.cpp modelutils.h modelutils.cpp modelutils.h
pagesmodel.h pagesmodel.cpp pagesmodel.h pagesmodel.cpp
sectionsdecoratormodel.cpp sectionsdecoratormodel.h sectionsdecoratormodel.cpp sectionsdecoratormodel.h
testsrunner.h testsrunner.cpp
) )
add_executable( add_executable(
@ -105,6 +106,9 @@ add_executable(QmlTests
qmlTests/main.cpp qmlTests/main.cpp
qmlTests/src/TextUtils.cpp qmlTests/src/TextUtils.h qmlTests/src/TextUtils.cpp qmlTests/src/TextUtils.h
${TEST_QML_FILES}) ${TEST_QML_FILES})
add_dependencies(QmlTests StatusQ)
target_compile_definitions(QmlTests PRIVATE target_compile_definitions(QmlTests PRIVATE
QML_IMPORT_ROOT="${CMAKE_CURRENT_LIST_DIR}" QML_IMPORT_ROOT="${CMAKE_CURRENT_LIST_DIR}"
STATUSQ_MODULE_IMPORT_PATH="${STATUSQ_MODULE_IMPORT_PATH}" STATUSQ_MODULE_IMPORT_PATH="${STATUSQ_MODULE_IMPORT_PATH}"

View File

@ -7,6 +7,7 @@
#include "figmalinks.h" #include "figmalinks.h"
#include "pagesmodel.h" #include "pagesmodel.h"
#include "sectionsdecoratormodel.h" #include "sectionsdecoratormodel.h"
#include "testsrunner.h"
struct PagesModelInitialized : public PagesModel { struct PagesModelInitialized : public PagesModel {
explicit PagesModelInitialized(QObject *parent = nullptr) explicit PagesModelInitialized(QObject *parent = nullptr)
@ -64,6 +65,16 @@ int main(int argc, char *argv[])
qmlRegisterSingletonType<CacheCleaner>( qmlRegisterSingletonType<CacheCleaner>(
"Storybook", 1, 0, "CacheCleaner", cleanerFactory); "Storybook", 1, 0, "CacheCleaner", cleanerFactory);
auto runnerFactory = [](QQmlEngine* engine, QJSEngine*) {
return new TestsRunner(
QCoreApplication::applicationDirPath() + QStringLiteral("/QmlTests"),
QML_IMPORT_ROOT + QStringLiteral("/qmlTests/tests"));
};
qmlRegisterSingletonType<TestsRunner>(
"Storybook", 1, 0, "TestsRunner", runnerFactory);
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
const QUrl url(QUrl::fromLocalFile(QML_IMPORT_ROOT + QStringLiteral("/main.qml"))); const QUrl url(QUrl::fromLocalFile(QML_IMPORT_ROOT + QStringLiteral("/main.qml")));
#else #else

View File

@ -0,0 +1,89 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Storybook 1.0
SplitView {
id: root
Logs { id: logs }
orientation: Qt.Vertical
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
TestRunnerControls {
mode: radioButtonsGroup.checkedButton.mode
numberOfFailedTests: 42
anchors.centerIn: parent
onRunClicked: logs.logEvent("Run clicked")
onAbortClicked: logs.logEvent("Abort clicked")
}
}
LogsAndControlsPanel {
SplitView.minimumHeight: 100
SplitView.preferredHeight: 200
SplitView.fillWidth: true
logsView.logText: logs.logText
ButtonGroup {
id: radioButtonsGroup
buttons: radioButtonsRow.children
}
RowLayout {
id: radioButtonsRow
RadioButton {
readonly property int mode: TestRunnerControls.Mode.Base
text: "Base"
}
RadioButton {
readonly property int mode: TestRunnerControls.Mode.InProgress
text: "In progress"
checked: true
}
RadioButton {
readonly property int mode: TestRunnerControls.Mode.Failed
text: "Failed"
}
RadioButton {
readonly property int mode: TestRunnerControls.Mode.Success
text: "Success"
}
RadioButton {
readonly property int mode: TestRunnerControls.Mode.Aborted
text: "Aborted"
}
RadioButton {
readonly property int mode: TestRunnerControls.Mode.Crashed
text: "Crashed"
}
}
}
}
// category: Controls

View File

@ -14,7 +14,7 @@ public slots:
QML_IMPORT_ROOT + QStringLiteral("/../ui/app"), QML_IMPORT_ROOT + QStringLiteral("/../ui/app"),
QML_IMPORT_ROOT + QStringLiteral("/../ui/imports"), QML_IMPORT_ROOT + QStringLiteral("/../ui/imports"),
QML_IMPORT_ROOT + QStringLiteral("/stubs"), QML_IMPORT_ROOT + QStringLiteral("/stubs"),
QML_IMPORT_ROOT + QStringLiteral("/mocks"), QML_IMPORT_ROOT + QStringLiteral("/src")
}; };
for (const auto& path : additionalImportPaths) for (const auto& path : additionalImportPaths)

View File

@ -0,0 +1,125 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtTest 1.15
import Storybook 1.0
TestCase {
id: root
name: "TestRunnerControlsTest"
when: windowShown
visible: true
Component {
id: testedComponent
TestRunnerControls {}
}
Label {
id: sampleLabel
}
readonly property color errorColor: "darkred"
readonly property color successColor: "darkgreen"
function test_states() {
{
const obj = createTemporaryObject(testedComponent, this, {
mode: TestRunnerControls.Mode.Base
})
const label = findChild(obj, "label")
const progressBar = findChild(obj, "progressBar")
const button = findChild(obj, "button")
compare(label.visible, false)
compare(progressBar.visible, false)
compare(button.visible, true)
compare(button.text, "Run tests")
}
{
const obj = createTemporaryObject(testedComponent, this, {
mode: TestRunnerControls.Mode.InProgress
})
const label = findChild(obj, "label")
const progressBar = findChild(obj, "progressBar")
const button = findChild(obj, "button")
compare(label.visible, true)
compare(label.text, "Running tests")
compare(label.color, sampleLabel.color)
compare(progressBar.visible, true)
compare(button.visible, true)
compare(button.text, "Abort")
}
{
const obj = createTemporaryObject(testedComponent, this, {
mode: TestRunnerControls.Mode.Failed,
numberOfFailedTests: 42
})
const label = findChild(obj, "label")
const progressBar = findChild(obj, "progressBar")
const button = findChild(obj, "button")
compare(label.visible, true)
compare(label.text, "Tests failed (42)")
compare(label.color, root.errorColor)
compare(progressBar.visible, false)
compare(button.visible, true)
compare(button.text, "Re-run tests")
}
{
const obj = createTemporaryObject(testedComponent, this, {
mode: TestRunnerControls.Mode.Success
})
const label = findChild(obj, "label")
const progressBar = findChild(obj, "progressBar")
const button = findChild(obj, "button")
compare(label.visible, true)
compare(label.text, "Tests passed")
compare(label.color, root.successColor)
compare(progressBar.visible, false)
compare(button.visible, true)
compare(button.text, "Re-run tests")
}
{
const obj = createTemporaryObject(testedComponent, this, {
mode: TestRunnerControls.Mode.Aborted
})
const label = findChild(obj, "label")
const progressBar = findChild(obj, "progressBar")
const button = findChild(obj, "button")
compare(label.visible, true)
compare(label.text, "Tests aborted")
compare(label.color, root.errorColor)
compare(progressBar.visible, false)
compare(button.visible, true)
compare(button.text, "Re-run tests")
}
{
const obj = createTemporaryObject(testedComponent, this, {
mode: TestRunnerControls.Mode.Crashed
})
const label = findChild(obj, "label")
const progressBar = findChild(obj, "progressBar")
const button = findChild(obj, "button")
compare(label.visible, true)
compare(label.text, "Tests crashed (segfault)")
compare(label.color, root.errorColor)
compare(progressBar.visible, false)
compare(button.visible, true)
compare(button.text, "Re-run tests")
}
}
}

View File

@ -2,6 +2,8 @@ import QtQuick 2.14
import QtQuick.Controls 2.14 import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14 import QtQuick.Layouts 1.14
import Storybook 1.0
ToolBar { ToolBar {
id: root id: root
@ -54,6 +56,56 @@ ToolBar {
ToolSeparator {} ToolSeparator {}
TestRunnerControls {
id: testRunnerControls
property var testProcess: null
readonly property string testFileName: `tst_${root.componentName}.qml`
onTestFileNameChanged: {
if (testRunnerControls.testProcess)
testRunnerControls.testProcess.kill()
testRunnerControls.mode = TestRunnerControls.Mode.Base
}
onRunClicked: {
const testsCount = TestsRunner.testsCount(testFileName)
if (testsCount === 0)
return noTestsDialog.open()
testRunnerControls.mode = TestRunnerControls.Mode.InProgress
const process = TestsRunner.runTests(testFileName)
testRunnerControls.testProcess = process
process.finished.connect((exitCode, exitStatus) => {
if (testRunnerControls.mode !== TestRunnerControls.Mode.InProgress)
return
if (exitStatus) {
testRunnerControls.mode = TestRunnerControls.Mode.Crashed
return
}
if (exitCode)
testRunnerControls.mode = TestRunnerControls.Mode.Failed
else
testRunnerControls.mode = TestRunnerControls.Mode.Success
testRunnerControls.numberOfFailedTests = exitCode
})
}
onAbortClicked: {
testRunnerControls.testProcess.kill()
testRunnerControls.mode = TestRunnerControls.Mode.Aborted
}
}
ToolSeparator {}
ToolButton { ToolButton {
id: openFigmaButton id: openFigmaButton
@ -72,4 +124,21 @@ ToolBar {
onClicked: root.inspectClicked() onClicked: root.inspectClicked()
} }
} }
Dialog {
id: noTestsDialog
anchors.centerIn: Overlay.overlay
title: "No tests found"
standardButtons: Dialog.Ok
Label {
// check on visible used as a workaround to avoid warning about
// binding loop on implicitWidth
text: visible
? `Please add valid tests to <b>${testRunnerControls.testFileName}</b> file`
: ""
}
}
} }

View File

@ -0,0 +1,122 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
Control {
id: root
enum Mode {
Base,
InProgress,
Failed,
Success,
Aborted,
Crashed
}
property int mode: TestRunnerControls.Mode.Base
property int numberOfFailedTests: 0
signal runClicked
signal abortClicked
contentItem: RowLayout {
id: testingRow
states: [
State {
when: root.mode === TestRunnerControls.Mode.Base
PropertyChanges {
target: button
text: "Run tests"
}
PropertyChanges {
target: label
visible: false
}
},
State {
when: root.mode === TestRunnerControls.Mode.InProgress
PropertyChanges {
target: label
text: "Running tests"
}
PropertyChanges {
target: progressBar
visible: true
}
PropertyChanges {
target: button
text: "Abort"
onClicked: root.abortClicked()
}
},
State {
when: root.mode === TestRunnerControls.Mode.Failed
PropertyChanges {
target: label
color: "darkred"
text: `Tests failed (${root.numberOfFailedTests})`
}
},
State {
when: root.mode === TestRunnerControls.Mode.Success
PropertyChanges {
target: label
color: "darkgreen"
text: "Tests passed"
}
},
State {
when: root.mode === TestRunnerControls.Mode.Aborted
PropertyChanges {
target: label
color: "darkred"
text: "Tests aborted"
}
},
State {
when: root.mode === TestRunnerControls.Mode.Crashed
PropertyChanges {
target: label
color: "darkred"
text: "Tests crashed (segfault)"
}
}
]
Label {
id: label
objectName: "label"
}
ProgressBar {
id: progressBar
objectName: "progressBar"
visible: false
indeterminate: true
width: 50
}
ToolButton {
id: button
objectName: "button"
text: "Re-run tests"
onClicked: root.runClicked()
}
}
}

View File

@ -25,6 +25,7 @@ PopupBackground 1.0 PopupBackground.qml
SettingsLayout 1.0 SettingsLayout.qml SettingsLayout 1.0 SettingsLayout.qml
SingleItemProxyModel 1.0 SingleItemProxyModel.qml SingleItemProxyModel 1.0 SingleItemProxyModel.qml
SourceCodeBox 1.0 SourceCodeBox.qml SourceCodeBox 1.0 SourceCodeBox.qml
TestRunnerControls 1.0 TestRunnerControls.qml
singleton FigmaUtils 1.0 FigmaUtils.qml singleton FigmaUtils 1.0 FigmaUtils.qml
singleton InspectionUtils 1.0 InspectionUtils.qml singleton InspectionUtils 1.0 InspectionUtils.qml
singleton StorybookUtils 1.0 StorybookUtils.qml singleton StorybookUtils 1.0 StorybookUtils.qml

51
storybook/testsrunner.cpp Normal file
View File

@ -0,0 +1,51 @@
#include "testsrunner.h"
#include <QDir>
#include <QProcess>
TestsRunner::TestsRunner(const QString& testRunnerExecutablePath,
const QString& testsBasePath, QObject* parent)
: QObject{parent}, m_testRunnerExecutablePath{testRunnerExecutablePath},
m_testsBasePath{testsBasePath}
{
}
int TestsRunner::testsCount(const QString& fileName)
{
QStringList arguments;
arguments << QStringLiteral("-functions");
arguments << QStringLiteral("-input")
<< m_testsBasePath + QDir::separator() + fileName;
QProcess testRunnerProcess;
testRunnerProcess.setProgram(m_testRunnerExecutablePath);
testRunnerProcess.setArguments(arguments);
testRunnerProcess.open(QIODevice::Text | QIODevice::ReadWrite);
testRunnerProcess.waitForFinished();
if (testRunnerProcess.exitCode())
return 0;
QByteArray functions = testRunnerProcess.readAllStandardError();
return functions.count('\n');
}
QObject* TestsRunner::runTests(const QString& fileName)
{
QStringList arguments;
arguments << QStringLiteral("-platform") << QStringLiteral("offscreen");
arguments << QStringLiteral("-input")
<< m_testsBasePath + QDir::separator() + fileName;
QProcess *testRunnerProcess = new QProcess(this);
testRunnerProcess->setProcessChannelMode(QProcess::ForwardedChannels);
testRunnerProcess->start(m_testRunnerExecutablePath, arguments);
using FinishHandlerType = void (QProcess::*)(int, QProcess::ExitStatus);
connect(testRunnerProcess,
static_cast<FinishHandlerType>(&QProcess::finished),
testRunnerProcess, &QObject::deleteLater);
return testRunnerProcess;
}

18
storybook/testsrunner.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <QObject>
class TestsRunner : public QObject
{
Q_OBJECT
public:
explicit TestsRunner(const QString& testRunnerExecutablePath,
const QString& testsPath, QObject *parent = nullptr);
Q_INVOKABLE int testsCount(const QString& path);
Q_INVOKABLE QObject* runTests(const QString& path);
private:
QString m_testRunnerExecutablePath;
QString m_testsBasePath;
};