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
pagesmodel.h pagesmodel.cpp
sectionsdecoratormodel.cpp sectionsdecoratormodel.h
testsrunner.h testsrunner.cpp
)
add_executable(
@ -105,6 +106,9 @@ add_executable(QmlTests
qmlTests/main.cpp
qmlTests/src/TextUtils.cpp qmlTests/src/TextUtils.h
${TEST_QML_FILES})
add_dependencies(QmlTests StatusQ)
target_compile_definitions(QmlTests PRIVATE
QML_IMPORT_ROOT="${CMAKE_CURRENT_LIST_DIR}"
STATUSQ_MODULE_IMPORT_PATH="${STATUSQ_MODULE_IMPORT_PATH}"

View File

@ -7,6 +7,7 @@
#include "figmalinks.h"
#include "pagesmodel.h"
#include "sectionsdecoratormodel.h"
#include "testsrunner.h"
struct PagesModelInitialized : public PagesModel {
explicit PagesModelInitialized(QObject *parent = nullptr)
@ -64,6 +65,16 @@ int main(int argc, char *argv[])
qmlRegisterSingletonType<CacheCleaner>(
"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
const QUrl url(QUrl::fromLocalFile(QML_IMPORT_ROOT + QStringLiteral("/main.qml")));
#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/imports"),
QML_IMPORT_ROOT + QStringLiteral("/stubs"),
QML_IMPORT_ROOT + QStringLiteral("/mocks"),
QML_IMPORT_ROOT + QStringLiteral("/src")
};
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.Layouts 1.14
import Storybook 1.0
ToolBar {
id: root
@ -54,6 +56,56 @@ ToolBar {
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 {
id: openFigmaButton
@ -72,4 +124,21 @@ ToolBar {
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
SingleItemProxyModel 1.0 SingleItemProxyModel.qml
SourceCodeBox 1.0 SourceCodeBox.qml
TestRunnerControls 1.0 TestRunnerControls.qml
singleton FigmaUtils 1.0 FigmaUtils.qml
singleton InspectionUtils 1.0 InspectionUtils.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;
};