feat(Storybook): Add tests runner available from the app
This commit is contained in:
parent
0caec2ab1c
commit
8a996a9175
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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`
|
||||
: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
};
|
Loading…
Reference in New Issue