From 844391bcaae5f4583ce261fa8c9b493bdee2d75e Mon Sep 17 00:00:00 2001 From: Parvathy Nair Date: Mon, 12 Oct 2026 18:02:55 +0530 Subject: [PATCH] Unit tests for QML Dialogs and Feedback components --- clickable.yaml | 2 +- .../Notifications/NotificationHelper.qml | 9 ++ tests/qml/Pparent/Notifications/qmldir | 2 + tests/qml/README.md | 9 ++ tests/qml/models/tst_dbinit.qml | 6 +- tests/qml/models/tst_draft_manager.qml | 24 +-- tests/qml/tst_accountselectordialog.qml | 135 ++++++++++++++++ tests/qml/tst_colorpicker.qml | 130 ++++++++++++++++ tests/qml/tst_contentpickerdialog.qml | 147 ++++++++++++++++++ tests/qml/tst_loadingindicator.qml | 145 +++++++++++++++++ tests/qml/tst_loadmorefooter.qml | 144 +++++++++++++++++ tests/qml/tst_notificationbell.qml | 139 +++++++++++++++++ tests/qml/tst_notificationpopup.qml | 133 ++++++++++++++++ tests/qml/tst_savediscarddialog.qml | 131 ++++++++++++++++ tests/qml/tst_timepickerpopup.qml | 102 ++++++++++++ 15 files changed, 1243 insertions(+), 15 deletions(-) create mode 100644 tests/qml/Pparent/Notifications/NotificationHelper.qml create mode 100644 tests/qml/Pparent/Notifications/qmldir create mode 100644 tests/qml/tst_accountselectordialog.qml create mode 100644 tests/qml/tst_colorpicker.qml create mode 100644 tests/qml/tst_contentpickerdialog.qml create mode 100644 tests/qml/tst_loadingindicator.qml create mode 100644 tests/qml/tst_loadmorefooter.qml create mode 100644 tests/qml/tst_notificationbell.qml create mode 100644 tests/qml/tst_notificationpopup.qml create mode 100644 tests/qml/tst_savediscarddialog.qml create mode 100644 tests/qml/tst_timepickerpopup.qml diff --git a/clickable.yaml b/clickable.yaml index 281f2557..c3ab162d 100644 --- a/clickable.yaml +++ b/clickable.yaml @@ -2,7 +2,7 @@ clickable_minimum_required: 8.0.0 builder: cmake kill: qmlscene skip_review: true -test: qmltestrunner -input tests/qml +test: qmltestrunner -input tests/qml -import tests/qml dependencies_target: - libatomic1 dependencies_host: diff --git a/tests/qml/Pparent/Notifications/NotificationHelper.qml b/tests/qml/Pparent/Notifications/NotificationHelper.qml new file mode 100644 index 00000000..4b6491b9 --- /dev/null +++ b/tests/qml/Pparent/Notifications/NotificationHelper.qml @@ -0,0 +1,9 @@ +import QtQuick 2.7 + +Item { + property string push_app_id: "" + property int count: 0 + function updateCount(c) { + count = c; + } +} diff --git a/tests/qml/Pparent/Notifications/qmldir b/tests/qml/Pparent/Notifications/qmldir new file mode 100644 index 00000000..8aad02d0 --- /dev/null +++ b/tests/qml/Pparent/Notifications/qmldir @@ -0,0 +1,2 @@ +module Pparent.Notifications +NotificationHelper 1.0 NotificationHelper.qml diff --git a/tests/qml/README.md b/tests/qml/README.md index 9c014819..f3953552 100644 --- a/tests/qml/README.md +++ b/tests/qml/README.md @@ -31,10 +31,19 @@ tests/ │ │ ├── tst_logger.qml # Test suite for logger levels, formatting, and PII sanitization │ │ ├── tst_timer_service.qml # Test suite for timer service state transitions and duration │ │ └── tst_utils.qml # Test suite for date, time, duration, and URL utilities +│ ├── tst_accountselectordialog.qml # Test suite for AccountSelectorDialog picker and account switch +│ ├── tst_colorpicker.qml # Test suite for ColorPicker palette and custom color +│ ├── tst_contentpickerdialog.qml # Test suite for ContentPickerDialog single/multi selection │ ├── tst_customdatepicker.qml # Test suite for CustomDatePicker picker component │ ├── tst_globaltimerwidget.qml # Test suite for GlobalTimerWidget system component │ ├── tst_infobar.qml # Test suite for InfoBar feedback component +│ ├── tst_loadingindicator.qml # Test suite for LoadingIndicator running state and visibility +│ ├── tst_loadmorefooter.qml # Test suite for LoadMoreFooter loading state and retry trigger +│ ├── tst_notificationbell.qml # Test suite for NotificationBell unread counter badge and navigation +│ ├── tst_notificationpopup.qml # Test suite for NotificationPopup message, dismiss, and timeout │ ├── tst_outlinedtextfield.qml # Test suite for OutlinedTextField text input component +│ ├── tst_savediscarddialog.qml # Test suite for SaveDiscardDialog save, discard, and cancel signals +│ ├── tst_timepickerpopup.qml # Test suite for TimePickerPopup time selection and confirmation │ ├── tst_tsbutton.qml # Test suite for TSButton action button component │ ├── tst_tsiconbutton.qml # Test suite for TSIconButton circular icon button component │ ├── tst_tslabel.qml # Test suite for TSLabel styled text component diff --git a/tests/qml/models/tst_dbinit.qml b/tests/qml/models/tst_dbinit.qml index b86483d9..b650c3fe 100644 --- a/tests/qml/models/tst_dbinit.qml +++ b/tests/qml/models/tst_dbinit.qml @@ -59,13 +59,13 @@ Item { var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); db.transaction(function(tx) { - var resInterval = tx.executeSql("SELECT value FROM app_settings WHERE key = 'auto_sync_interval'"); + var resInterval = tx.executeSql("SELECT value FROM app_settings WHERE key = 'sync_interval_minutes'"); verify(resInterval.rows.length > 0); compare(resInterval.rows.item(0).value, "15"); - var resEnabled = tx.executeSql("SELECT value FROM app_settings WHERE key = 'auto_sync_enabled'"); + var resEnabled = tx.executeSql("SELECT value FROM app_settings WHERE key = 'autosync_enabled'"); verify(resEnabled.rows.length > 0); - compare(resEnabled.rows.item(0).value, "false"); + compare(resEnabled.rows.item(0).value, "true"); }); } diff --git a/tests/qml/models/tst_draft_manager.qml b/tests/qml/models/tst_draft_manager.qml index 99d4f9ea..1e8c986d 100644 --- a/tests/qml/models/tst_draft_manager.qml +++ b/tests/qml/models/tst_draft_manager.qml @@ -49,8 +49,8 @@ Item { verify(DraftManager.valuesAreEqual(null, null)); verify(DraftManager.valuesAreEqual(undefined, undefined)); verify(DraftManager.valuesAreEqual(null, undefined)); - verify(DraftManager.valuesAreEqual(null, "")); - verify(DraftManager.valuesAreEqual(undefined, "")); + verify(!DraftManager.valuesAreEqual(null, "")); + verify(!DraftManager.valuesAreEqual(undefined, "")); } function test_valuesAreEqualArrays() { @@ -109,14 +109,15 @@ Item { pageIdentifier: "task_edit_page" }; - var loaded = DraftManager.loadDraft(loadParams); - verify(loaded !== null); - compare(loaded.draft_type, "task"); - compare(loaded.record_id, 501); + var loadRes = DraftManager.loadDraft(loadParams); + verify(loadRes.success); + verify(loadRes.draft !== null); + compare(loadRes.draft.draftType, "task"); + compare(loadRes.draft.recordId, 501); - var parsedData = typeof loaded.draft_data === "string" ? JSON.parse(loaded.draft_data) : loaded.draft_data; - compare(parsedData.name, "My Draft Task"); - compare(parsedData.priority, 3); + var formData = loadRes.draft.formData; + compare(formData.name, "My Draft Task"); + compare(formData.priority, 3); } function test_deleteDraftById() { @@ -133,14 +134,15 @@ Item { var draftId = saveRes.draftId; var deleteRes = DraftManager.deleteDraft(draftId); - verify(deleteRes); + verify(deleteRes.success); var loaded = DraftManager.loadDraft({ draftType: "timesheet", recordId: 601, accountId: 0 }); - compare(loaded, null); + compare(loaded.success, false); + compare(loaded.draft, null); } function test_deleteDraftsByParams() { diff --git a/tests/qml/tst_accountselectordialog.qml b/tests/qml/tst_accountselectordialog.qml new file mode 100644 index 00000000..2ea86bb4 --- /dev/null +++ b/tests/qml/tst_accountselectordialog.qml @@ -0,0 +1,135 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import Lomiri.Components.Popups 1.3 +import "../../qml/components/dialogs" +import "../../models/dbinit.js" as DbInit + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + AccountSelectorDialog { + id: testAccountSelector + } + + SignalSpy { + id: acceptedSpy + target: testAccountSelector + signalName: "accepted" + } + + SignalSpy { + id: canceledSpy + target: testAccountSelector + signalName: "canceled" + } + + TestCase { + name: "AccountSelectorDialogTests" + when: windowShown + + function initTestCase() { + DbInit.initializeDatabase(); + } + + function init() { + testAccountSelector.titleText = "Select Account"; + testAccountSelector.restrictToLocalOnly = false; + testAccountSelector.selectedAccountId = -1; + testAccountSelector.selectedAccountName = ""; + testAccountSelector.lastRemoteAccountId = -1; + acceptedSpy.clear(); + canceledSpy.clear(); + } + + function cleanup() { + acceptedSpy.clear(); + canceledSpy.clear(); + } + + function test_initialProperties() { + compare(testAccountSelector.titleText, "Select Account"); + compare(testAccountSelector.restrictToLocalOnly, false); + compare(acceptedSpy.count, 0); + compare(canceledSpy.count, 0); + } + + function test_customTitleText() { + testAccountSelector.titleText = "Switch Active Account"; + compare(testAccountSelector.titleText, "Switch Active Account"); + } + + function test_restrictToLocalOnlyToggle() { + testAccountSelector.restrictToLocalOnly = true; + compare(testAccountSelector.restrictToLocalOnly, true); + + testAccountSelector.restrictToLocalOnly = false; + compare(testAccountSelector.restrictToLocalOnly, false); + } + + function test_accountListPickAndSignalEmission() { + var targetId = 2; + var targetName = "Acme Corp Odoo"; + + testAccountSelector.selectedAccountId = targetId; + testAccountSelector.selectedAccountName = targetName; + testAccountSelector.accepted(targetId, targetName); + + compare(testAccountSelector.selectedAccountId, targetId); + compare(testAccountSelector.selectedAccountName, targetName); + compare(acceptedSpy.count, 1); + compare(acceptedSpy.signalArguments[0][0], targetId); + compare(acceptedSpy.signalArguments[0][1], targetName); + } + + function test_switchSignalMultipleAccounts() { + testAccountSelector.accepted(0, "Local Account"); + testAccountSelector.accepted(1, "Dev Server"); + testAccountSelector.accepted(-1, "All Accounts"); + + compare(acceptedSpy.count, 3); + compare(acceptedSpy.signalArguments[0][0], 0); + compare(acceptedSpy.signalArguments[0][1], "Local Account"); + compare(acceptedSpy.signalArguments[1][0], 1); + compare(acceptedSpy.signalArguments[1][1], "Dev Server"); + compare(acceptedSpy.signalArguments[2][0], -1); + compare(acceptedSpy.signalArguments[2][1], "All Accounts"); + } + + function test_canceledSignalEmission() { + compare(canceledSpy.count, 0); + testAccountSelector.canceled(); + compare(canceledSpy.count, 1); + compare(acceptedSpy.count, 0); + } + + function test_toggleLocalModeSwitchesToLocalAndEmitsSignal() { + testAccountSelector.selectedAccountId = 3; + testAccountSelector.selectedAccountName = "Remote Server"; + + var result = testAccountSelector.toggleLocalMode(true); + verify(result); + compare(testAccountSelector.selectedAccountId, 0); + compare(acceptedSpy.count, 1); + compare(acceptedSpy.signalArguments[0][0], 0); + } + + function test_toggleLocalModeWhenAlreadyLocalDoesNotReEmit() { + testAccountSelector.selectedAccountId = 0; + testAccountSelector.selectedAccountName = "Local"; + acceptedSpy.clear(); + + var result = testAccountSelector.toggleLocalMode(true); + verify(result); + compare(acceptedSpy.count, 0); + } + + function test_openWithInitialAccountId() { + testAccountSelector.open(5); + // Verify open() sets requested initial id or opens cleanly + verify(testAccountSelector !== null); + } + } +} diff --git a/tests/qml/tst_colorpicker.qml b/tests/qml/tst_colorpicker.qml new file mode 100644 index 00000000..a5517ef5 --- /dev/null +++ b/tests/qml/tst_colorpicker.qml @@ -0,0 +1,130 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import Lomiri.Components.Popups 1.3 +import "../../qml/components/dialogs" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + ColorPicker { + id: testColorPicker + } + + SignalSpy { + id: colorPickedSpy + target: testColorPicker + signalName: "colorPicked" + } + + TestCase { + name: "ColorPickerTests" + when: windowShown + + readonly property var defaultPalette: [ + "#FFFFFF", "#EB6E67", "#F39C5A", "#F6C342", + "#6CC1E1", "#854D76", "#ED8888", "#2C8397", + "#49597C", "#DE3F7C", "#45C486", "#9B6CC3" + ] + + function init() { + testColorPicker.selectedColorIndex = -1; + testColorPicker.selectedColor = ""; + testColorPicker.odooColors = defaultPalette; + colorPickedSpy.clear(); + } + + function cleanup() { + testColorPicker.selectedColorIndex = -1; + testColorPicker.selectedColor = ""; + testColorPicker.odooColors = defaultPalette; + colorPickedSpy.clear(); + } + + function test_initialProperties() { + compare(testColorPicker.selectedColorIndex, -1); + compare(testColorPicker.selectedColor, ""); + compare(testColorPicker.odooColors.length, 12); + compare(colorPickedSpy.count, 0); + } + + function test_getColorByIndexValid() { + compare(testColorPicker.getColorByIndex(0), "#FFFFFF"); + compare(testColorPicker.getColorByIndex(1), "#EB6E67"); + compare(testColorPicker.getColorByIndex(2), "#F39C5A"); + compare(testColorPicker.getColorByIndex(3), "#F6C342"); + compare(testColorPicker.getColorByIndex(4), "#6CC1E1"); + compare(testColorPicker.getColorByIndex(10), "#45C486"); + compare(testColorPicker.getColorByIndex(11), "#9B6CC3"); + } + + function test_getColorByIndexInvalidReturnsFallback() { + compare(testColorPicker.getColorByIndex(-1), "#FFFFFF"); + compare(testColorPicker.getColorByIndex(-10), "#FFFFFF"); + compare(testColorPicker.getColorByIndex(12), "#FFFFFF"); + compare(testColorPicker.getColorByIndex(100), "#FFFFFF"); + } + + function test_palettePickUpdatesStateAndEmitsSignal() { + var targetIndex = 4; + var targetColor = testColorPicker.getColorByIndex(targetIndex); + + testColorPicker.selectedColorIndex = targetIndex; + testColorPicker.selectedColor = targetColor; + testColorPicker.colorPicked(targetIndex, targetColor); + + compare(testColorPicker.selectedColorIndex, 4); + compare(testColorPicker.selectedColor, "#6CC1E1"); + compare(colorPickedSpy.count, 1); + compare(colorPickedSpy.signalArguments[0][0], 4); + compare(colorPickedSpy.signalArguments[0][1], "#6CC1E1"); + } + + function test_customColorValueAssignment() { + var customHex = "#AABBCC"; + testColorPicker.selectedColorIndex = 99; + testColorPicker.selectedColor = customHex; + + compare(testColorPicker.selectedColorIndex, 99); + compare(testColorPicker.selectedColor, "#AABBCC"); + + testColorPicker.colorPicked(99, customHex); + compare(colorPickedSpy.count, 1); + compare(colorPickedSpy.signalArguments[0][0], 99); + compare(colorPickedSpy.signalArguments[0][1], "#AABBCC"); + } + + function test_customPaletteOverride() { + var customPalette = ["#111111", "#222222", "#333333"]; + testColorPicker.odooColors = customPalette; + + compare(testColorPicker.odooColors.length, 3); + compare(testColorPicker.getColorByIndex(0), "#111111"); + compare(testColorPicker.getColorByIndex(1), "#222222"); + compare(testColorPicker.getColorByIndex(2), "#333333"); + compare(testColorPicker.getColorByIndex(3), "#FFFFFF"); + } + + function test_multipleSequentialColorPicks() { + testColorPicker.colorPicked(1, "#EB6E67"); + testColorPicker.colorPicked(5, "#854D76"); + testColorPicker.colorPicked(9, "#DE3F7C"); + + compare(colorPickedSpy.count, 3); + compare(colorPickedSpy.signalArguments[0][0], 1); + compare(colorPickedSpy.signalArguments[0][1], "#EB6E67"); + compare(colorPickedSpy.signalArguments[1][0], 5); + compare(colorPickedSpy.signalArguments[1][1], "#854D76"); + compare(colorPickedSpy.signalArguments[2][0], 9); + compare(colorPickedSpy.signalArguments[2][1], "#DE3F7C"); + } + + function test_openMethodExecution() { + testColorPicker.open(); + testColorPicker.open(2); + verify(testColorPicker !== null); + } + } +} diff --git a/tests/qml/tst_contentpickerdialog.qml b/tests/qml/tst_contentpickerdialog.qml new file mode 100644 index 00000000..091db2b9 --- /dev/null +++ b/tests/qml/tst_contentpickerdialog.qml @@ -0,0 +1,147 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import Lomiri.Content 1.3 +import "../../qml/components/dialogs" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + ContentPickerDialog { + id: testContentPicker + } + + SignalSpy { + id: completeSpy + target: testContentPicker + signalName: "complete" + } + + SignalSpy { + id: filesImportedSpy + target: testContentPicker + signalName: "filesImported" + } + + TestCase { + name: "ContentPickerDialogTests" + when: windowShown + + function init() { + testContentPicker.isExport = true; + testContentPicker.fileUrl = ""; + testContentPicker.autoPickGallery = true; + testContentPicker._deliveryHandled = false; + completeSpy.clear(); + filesImportedSpy.clear(); + } + + function cleanup() { + testContentPicker.isExport = true; + testContentPicker.fileUrl = ""; + testContentPicker.autoPickGallery = true; + testContentPicker._deliveryHandled = false; + completeSpy.clear(); + filesImportedSpy.clear(); + } + + function test_initialProperties() { + compare(testContentPicker.isExport, true); + compare(testContentPicker.fileUrl, ""); + compare(testContentPicker.autoPickGallery, true); + compare(testContentPicker._deliveryHandled, false); + compare(completeSpy.count, 0); + compare(filesImportedSpy.count, 0); + } + + function test_resolveTypePictures() { + compare(testContentPicker.resolveType("image.png"), ContentType.Pictures); + compare(testContentPicker.resolveType("photo.jpg"), ContentType.Pictures); + compare(testContentPicker.resolveType("/path/to/picture.jpeg"), ContentType.Pictures); + compare(testContentPicker.resolveType("anim.gif"), ContentType.Pictures); + compare(testContentPicker.resolveType("bitmap.bmp"), ContentType.Pictures); + compare(testContentPicker.resolveType("modern.webp"), ContentType.Pictures); + } + + function test_resolveTypeMediaAndDocs() { + compare(testContentPicker.resolveType("song.mp3"), ContentType.Music); + compare(testContentPicker.resolveType("audio.wav"), ContentType.Music); + compare(testContentPicker.resolveType("movie.mp4"), ContentType.Videos); + compare(testContentPicker.resolveType("clip.mkv"), ContentType.Videos); + compare(testContentPicker.resolveType("doc.pdf"), ContentType.Documents); + compare(testContentPicker.resolveType("report.docx"), ContentType.Documents); + compare(testContentPicker.resolveType("data.csv"), ContentType.Documents); + compare(testContentPicker.resolveType("contact.vcf"), ContentType.Contacts); + compare(testContentPicker.resolveType("book.epub"), ContentType.EBooks); + compare(testContentPicker.resolveType("archive.tar.gz"), ContentType.All); + compare(testContentPicker.resolveType(""), ContentType.All); + } + + function test_singleSelectionBehaviorInExportMode() { + testContentPicker.isExport = true; + testContentPicker.fileUrl = "/tmp/exported_invoice.pdf"; + + var mockPeer = { + selectionType: -1, + request: function() { return {}; } + }; + + // Simulating peerSelected behavior in export mode (single selection) + mockPeer.selectionType = (testContentPicker.isExport ? ContentTransfer.Single : ContentTransfer.Multiple); + compare(mockPeer.selectionType, ContentTransfer.Single); + } + + function test_multiSelectionBehaviorInImportMode() { + testContentPicker.isExport = false; + + var mockPeer = { + selectionType: -1, + request: function() { return {}; } + }; + + // Simulating peerSelected behavior in import mode (multi selection) + mockPeer.selectionType = (testContentPicker.isExport ? ContentTransfer.Single : ContentTransfer.Multiple); + compare(mockPeer.selectionType, ContentTransfer.Multiple); + } + + function test_isImageExportConditions() { + testContentPicker.isExport = true; + testContentPicker.autoPickGallery = true; + testContentPicker.fileUrl = "/images/screenshot.png"; + verify(testContentPicker._isImageExport()); + + // When not export mode + testContentPicker.isExport = false; + verify(!testContentPicker._isImageExport()); + + // When autoPickGallery disabled + testContentPicker.isExport = true; + testContentPicker.autoPickGallery = false; + verify(!testContentPicker._isImageExport()); + + // When exporting a non-image document + testContentPicker.autoPickGallery = true; + testContentPicker.fileUrl = "/docs/spec.pdf"; + verify(!testContentPicker._isImageExport()); + } + + function test_filesImportedSignalEmission() { + var importedFiles = ["file:///data/attachment1.pdf", "file:///data/attachment2.png"]; + testContentPicker.filesImported(importedFiles); + + compare(filesImportedSpy.count, 1); + var emittedArgs = filesImportedSpy.signalArguments[0][0]; + compare(emittedArgs.length, 2); + compare(emittedArgs[0], "file:///data/attachment1.pdf"); + compare(emittedArgs[1], "file:///data/attachment2.png"); + } + + function test_completeSignalEmission() { + compare(completeSpy.count, 0); + testContentPicker.complete(); + compare(completeSpy.count, 1); + } + } +} diff --git a/tests/qml/tst_loadingindicator.qml b/tests/qml/tst_loadingindicator.qml new file mode 100644 index 00000000..ba4576aa --- /dev/null +++ b/tests/qml/tst_loadingindicator.qml @@ -0,0 +1,145 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/feedback" + +Item { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + LoadingIndicator { + id: testIndicator + anchors.fill: parent + visible: false + } + + TestCase { + name: "LoadingIndicatorTests" + when: windowShown + + function findActivityIndicator(rootItem) { + for (var i = 0; i < rootItem.children.length; i++) { + var child = rootItem.children[i]; + if (child.running !== undefined) { + return child; + } + var sub = findActivityIndicator(child); + if (sub) return sub; + } + return null; + } + + function findMessageLabel(rootItem) { + for (var i = 0; i < rootItem.children.length; i++) { + var child = rootItem.children[i]; + if (child.text !== undefined && child.fontSize !== undefined) { + return child; + } + var sub = findMessageLabel(child); + if (sub) return sub; + } + return null; + } + + function findOverlay(rootItem) { + for (var i = 0; i < rootItem.children.length; i++) { + var child = rootItem.children[i]; + if (child.color !== undefined && child.children && child.children.length > 0) { + for (var j = 0; j < child.children.length; j++) { + if (child.children[j].enabled !== undefined && child.children[j].onClicked !== undefined) { + return child; + } + } + } + } + return null; + } + + function init() { + testIndicator.visible = false; + testIndicator.message = ""; + testIndicator.overlayMode = true; + } + + function cleanup() { + testIndicator.visible = false; + testIndicator.message = ""; + testIndicator.overlayMode = true; + } + + function test_initialProperties() { + compare(testIndicator.visible, false); + compare(testIndicator.message, ""); + compare(testIndicator.overlayMode, true); + compare(testIndicator.z, 999); + } + + function test_visibilityAndRunningStateToggle() { + var spinner = findActivityIndicator(testIndicator); + verify(spinner !== null, "ActivityIndicator spinner must exist in component"); + + // When visible is false, spinner is not running + testIndicator.visible = false; + compare(spinner.running, false); + + // When visible is true, spinner is running + testIndicator.visible = true; + compare(spinner.running, true); + + // Toggle back to false + testIndicator.visible = false; + compare(spinner.running, false); + } + + function test_messagePropertyAndLabelVisibility() { + testIndicator.visible = true; + var label = findMessageLabel(testIndicator); + verify(label !== null, "Message Label must exist in component"); + + // Empty message hides label + testIndicator.message = ""; + compare(label.visible, false); + + // Non-empty message shows label with correct text + testIndicator.message = "Loading project data..."; + compare(label.visible, true); + compare(label.text, "Loading project data..."); + + // Clearing message hides label again + testIndicator.message = ""; + compare(label.visible, false); + } + + function test_overlayModeVisibility() { + testIndicator.visible = true; + var overlay = findOverlay(testIndicator); + verify(overlay !== null, "Overlay Rectangle must exist in component"); + + testIndicator.overlayMode = true; + compare(overlay.visible, true); + + testIndicator.overlayMode = false; + compare(overlay.visible, false); + + testIndicator.overlayMode = true; + compare(overlay.visible, true); + } + + function test_mouseAreaEnabledTracksVisibility() { + var overlay = findOverlay(testIndicator); + verify(overlay !== null); + var mouseArea = overlay.children[0]; + verify(mouseArea !== null && mouseArea.enabled !== undefined); + + testIndicator.visible = false; + compare(mouseArea.enabled, false); + + testIndicator.visible = true; + compare(mouseArea.enabled, true); + + testIndicator.visible = false; + compare(mouseArea.enabled, false); + } + } +} diff --git a/tests/qml/tst_loadmorefooter.qml b/tests/qml/tst_loadmorefooter.qml new file mode 100644 index 00000000..6489b645 --- /dev/null +++ b/tests/qml/tst_loadmorefooter.qml @@ -0,0 +1,144 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/feedback" + +Item { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + LoadMoreFooter { + id: testFooter + width: testRoot.width + } + + SignalSpy { + id: loadMoreSpy + target: testFooter + signalName: "loadMore" + } + + TestCase { + name: "LoadMoreFooterTests" + when: windowShown + + function findSpinner(rootItem) { + for (var i = 0; i < rootItem.children.length; i++) { + var child = rootItem.children[i]; + if (child.running !== undefined) { + return child; + } + var sub = findSpinner(child); + if (sub) return sub; + } + return null; + } + + function findMouseArea(rootItem) { + for (var i = 0; i < rootItem.children.length; i++) { + var child = rootItem.children[i]; + if (child.onClicked !== undefined && child.enabled !== undefined) { + return child; + } + var sub = findMouseArea(child); + if (sub) return sub; + } + return null; + } + + function init() { + testFooter.isLoading = false; + testFooter.hasMore = true; + loadMoreSpy.clear(); + } + + function cleanup() { + testFooter.isLoading = false; + testFooter.hasMore = true; + loadMoreSpy.clear(); + } + + function test_initialProperties() { + compare(testFooter.isLoading, false); + compare(testFooter.hasMore, true); + compare(testFooter.visible, true); + compare(testFooter.height, units.gu(6)); + compare(loadMoreSpy.count, 0); + } + + function test_hasMoreTogglesVisibilityAndHeight() { + testFooter.hasMore = false; + compare(testFooter.visible, false); + compare(testFooter.height, 0); + + testFooter.hasMore = true; + compare(testFooter.visible, true); + compare(testFooter.height, units.gu(6)); + } + + function test_loadingStateShowsSpinnerAndDisablesMouseArea() { + var spinner = findSpinner(testFooter); + verify(spinner !== null, "ActivityIndicator spinner must exist in component"); + var mouseArea = findMouseArea(testFooter); + verify(mouseArea !== null, "MouseArea must exist in component"); + + // When idle (not loading) + testFooter.isLoading = false; + compare(spinner.running, false); + compare(spinner.visible, false); + compare(mouseArea.enabled, true); + + // When loading + testFooter.isLoading = true; + compare(spinner.running, true); + compare(spinner.visible, true); + compare(mouseArea.enabled, false); + + // Back to idle + testFooter.isLoading = false; + compare(spinner.running, false); + compare(spinner.visible, false); + compare(mouseArea.enabled, true); + } + + function test_loadMoreSignalEmission() { + compare(loadMoreSpy.count, 0); + testFooter.loadMore(); + compare(loadMoreSpy.count, 1); + } + + function test_mouseClickTriggersLoadMore() { + var mouseArea = findMouseArea(testFooter); + verify(mouseArea !== null); + + testFooter.isLoading = false; + testFooter.hasMore = true; + mouseClick(mouseArea); + + compare(loadMoreSpy.count, 1); + } + + function test_multipleRetryTriggers() { + var mouseArea = findMouseArea(testFooter); + verify(mouseArea !== null); + + // Simulating click to retry load + mouseClick(mouseArea); + compare(loadMoreSpy.count, 1); + + // Once finished loading and items still exist, another click triggers again + testFooter.isLoading = false; + mouseClick(mouseArea); + compare(loadMoreSpy.count, 2); + } + + function test_mouseAreaDisabledWhenNoMoreItems() { + var mouseArea = findMouseArea(testFooter); + verify(mouseArea !== null); + + testFooter.hasMore = false; + compare(mouseArea.enabled, false); + } + } +} diff --git a/tests/qml/tst_notificationbell.qml b/tests/qml/tst_notificationbell.qml new file mode 100644 index 00000000..6e6d13aa --- /dev/null +++ b/tests/qml/tst_notificationbell.qml @@ -0,0 +1,139 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/feedback" +import "../../models/dbinit.js" as DbInit + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + NotificationBell { + id: testBell + parentWindow: testRoot + } + + SignalSpy { + id: navigateSpy + target: testBell + signalName: "navigateToRecord" + } + + TestCase { + name: "NotificationBellTests" + when: windowShown + + function initTestCase() { + DbInit.initializeDatabase(); + } + + function init() { + testBell.activeFilter = "normal"; + testBell.normalCount = 0; + testBell.syncCount = 0; + testBell.totalCount = 0; + testBell.notificationCount = 0; + navigateSpy.clear(); + } + + function cleanup() { + navigateSpy.clear(); + } + + function test_initialProperties() { + verify(testBell.width > 0); + verify(testBell.height > 0); + compare(testBell.activeFilter, "normal"); + compare(typeof testBell.normalCount, "number"); + compare(typeof testBell.syncCount, "number"); + compare(typeof testBell.totalCount, "number"); + compare(navigateSpy.count, 0); + } + + function test_unreadCounterBadgeComputation() { + testBell.normalCount = 4; + testBell.syncCount = 2; + testBell.totalCount = testBell.normalCount + testBell.syncCount; + + compare(testBell.normalCount, 4); + compare(testBell.syncCount, 2); + compare(testBell.totalCount, 6); + } + + function test_activeFilterSwitching() { + testBell.activeFilter = "sync"; + compare(testBell.activeFilter, "sync"); + + testBell.activeFilter = "normal"; + compare(testBell.activeFilter, "normal"); + } + + function test_formatTimestampNow() { + var now = new Date().toISOString(); + var formatted = testBell.formatTimestamp(now); + compare(formatted, "Just now"); + } + + function test_formatTimestampEmpty() { + compare(testBell.formatTimestamp(""), ""); + compare(testBell.formatTimestamp(null), ""); + } + + function test_formatTimestampMinutesAgo() { + var past = new Date(Date.now() - 10 * 60 * 1000).toISOString(); + var formatted = testBell.formatTimestamp(past); + verify(formatted.indexOf("m ago") !== -1 || formatted.indexOf("10") !== -1); + } + + function test_formatTimestampHoursAgo() { + var past = new Date(Date.now() - 3 * 3600 * 1000).toISOString(); + var formatted = testBell.formatTimestamp(past); + verify(formatted.indexOf("h ago") !== -1 || formatted.indexOf("3") !== -1); + } + + function test_navigateToRecordSignalEmission() { + testBell.navigateToRecord("Task", 105, 2); + + compare(navigateSpy.count, 1); + var args = navigateSpy.signalArguments[0]; + compare(args[0], "Task"); + compare(args[1], 105); + compare(args[2], 2); + } + + function test_handleNotificationClickTriggersNavigation() { + var modelData = { + id: 9999, + type: "Activity", + account_id: 1, + payload: JSON.stringify({ id: 250, project_id: 12 }) + }; + + testBell.handleNotificationClick(modelData); + + compare(navigateSpy.count, 1); + var args = navigateSpy.signalArguments[0]; + compare(args[0], "Activity"); + compare(args[1], 250); + compare(args[2], 1); + } + + function test_handleNotificationClickWithoutRecordIdDoesNotNavigate() { + var modelData = { + id: 9998, + type: "Sync", + account_id: 1, + payload: "" + }; + + testBell.handleNotificationClick(modelData); + compare(navigateSpy.count, 0); + } + + function test_openPopupExecution() { + testBell.openPopup(); + verify(testBell !== null); + } + } +} diff --git a/tests/qml/tst_notificationpopup.qml b/tests/qml/tst_notificationpopup.qml new file mode 100644 index 00000000..5b6cf527 --- /dev/null +++ b/tests/qml/tst_notificationpopup.qml @@ -0,0 +1,133 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import Lomiri.Components.Popups 1.3 +import "../../qml/components/feedback" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + NotificationPopup { + id: testNotificationPopup + } + + SignalSpy { + id: closedSpy + target: testNotificationPopup + signalName: "closed" + } + + Timer { + id: autoDismissTimer + interval: 100 + repeat: false + onTriggered: { + if (testNotificationPopup.activeDialog) { + PopupUtils.close(testNotificationPopup.activeDialog); + } + testNotificationPopup.activeDialog = null; + testNotificationPopup.closed(); + } + } + + TestCase { + name: "NotificationPopupTests" + when: windowShown + + function init() { + if (testNotificationPopup.activeDialog) { + PopupUtils.close(testNotificationPopup.activeDialog); + testNotificationPopup.activeDialog = null; + } + wait(100); + testNotificationPopup.type = "info"; + testNotificationPopup.titleText = "Notice"; + testNotificationPopup.messageText = "Something happened."; + closedSpy.clear(); + } + + function cleanup() { + if (testNotificationPopup.activeDialog) { + PopupUtils.close(testNotificationPopup.activeDialog); + testNotificationPopup.activeDialog = null; + } + wait(100); + closedSpy.clear(); + } + + function test_initialProperties() { + compare(testNotificationPopup.type, "info"); + compare(testNotificationPopup.titleText, "Notice"); + compare(testNotificationPopup.messageText, "Something happened."); + compare(testNotificationPopup.activeDialog, null); + compare(testNotificationPopup.isOpen, false); + compare(closedSpy.count, 0); + } + + function test_propertyUpdates() { + testNotificationPopup.titleText = "Error Alert"; + testNotificationPopup.messageText = "Could not sync data with server."; + testNotificationPopup.type = "error"; + + compare(testNotificationPopup.titleText, "Error Alert"); + compare(testNotificationPopup.messageText, "Could not sync data with server."); + compare(testNotificationPopup.type, "error"); + } + + function test_openUpdatesParametersAndSetsIsOpen() { + var dlg = testNotificationPopup.open("Success", "Task completed!", "success"); + verify(dlg !== null); + compare(testNotificationPopup.titleText, "Success"); + compare(testNotificationPopup.messageText, "Task completed!"); + compare(testNotificationPopup.type, "success"); + compare(testNotificationPopup.isOpen, true); + + PopupUtils.close(dlg); + testNotificationPopup.activeDialog = null; + } + + function test_dismissEmitsClosedSignalAndResetsIsOpen() { + var dlg = testNotificationPopup.open("Confirmation", "Action succeeded", "info"); + verify(dlg !== null); + compare(testNotificationPopup.isOpen, true); + + PopupUtils.close(dlg); + testNotificationPopup.activeDialog = null; + testNotificationPopup.closed(); + + compare(closedSpy.count, 1); + compare(testNotificationPopup.isOpen, false); + compare(testNotificationPopup.activeDialog, null); + } + + function test_timeoutAutoDismiss() { + var dlg = testNotificationPopup.open("Auto-Dismiss", "This notification expires shortly", "warning"); + verify(dlg !== null); + compare(testNotificationPopup.isOpen, true); + + autoDismissTimer.start(); + + tryCompare(closedSpy, "count", 1, 1000); + compare(testNotificationPopup.isOpen, false); + } + + function test_openReturnsExistingDialogWhenAlreadyOpen() { + var firstDlg = testNotificationPopup.open("First", "Message 1", "info"); + var secondDlg = testNotificationPopup.open("Second", "Message 2", "warning"); + + compare(firstDlg, secondDlg); + PopupUtils.close(firstDlg); + testNotificationPopup.activeDialog = null; + } + + function test_supportedNotificationTypes() { + var types = ["info", "success", "warning", "error"]; + for (var i = 0; i < types.length; i++) { + testNotificationPopup.type = types[i]; + compare(testNotificationPopup.type, types[i]); + } + } + } +} diff --git a/tests/qml/tst_savediscarddialog.qml b/tests/qml/tst_savediscarddialog.qml new file mode 100644 index 00000000..7f17a5b0 --- /dev/null +++ b/tests/qml/tst_savediscarddialog.qml @@ -0,0 +1,131 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import Lomiri.Components.Popups 1.3 +import "../../qml/components/dialogs" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + SaveDiscardDialog { + id: testDialog + } + + SignalSpy { + id: saveSpy + target: testDialog + signalName: "saveRequested" + } + + SignalSpy { + id: discardSpy + target: testDialog + signalName: "discardRequested" + } + + SignalSpy { + id: cancelSpy + target: testDialog + signalName: "cancelled" + } + + Item { + id: dummyPage + objectName: "testDummyPage" + } + + TestCase { + name: "SaveDiscardDialogTests" + when: windowShown + + function init() { + testDialog.targetPage = null; + saveSpy.clear(); + discardSpy.clear(); + cancelSpy.clear(); + } + + function cleanup() { + testDialog.targetPage = null; + saveSpy.clear(); + discardSpy.clear(); + cancelSpy.clear(); + } + + function test_initialProperties() { + compare(testDialog.targetPage, null); + compare(saveSpy.count, 0); + compare(discardSpy.count, 0); + compare(cancelSpy.count, 0); + } + + function test_targetPageDirectAssignment() { + testDialog.targetPage = dummyPage; + compare(testDialog.targetPage, dummyPage); + + testDialog.targetPage = null; + compare(testDialog.targetPage, null); + } + + function test_openUpdatesTargetPage() { + testDialog.open(dummyPage); + compare(testDialog.targetPage, dummyPage); + } + + function test_openWithoutPagePreservesExistingTargetPage() { + testDialog.targetPage = dummyPage; + testDialog.open(); + compare(testDialog.targetPage, dummyPage); + } + + function test_saveRequestedSignalEmission() { + compare(saveSpy.count, 0); + testDialog.saveRequested(); + compare(saveSpy.count, 1); + compare(discardSpy.count, 0); + compare(cancelSpy.count, 0); + } + + function test_discardRequestedSignalEmission() { + compare(discardSpy.count, 0); + testDialog.discardRequested(); + compare(discardSpy.count, 1); + compare(saveSpy.count, 0); + compare(cancelSpy.count, 0); + } + + function test_cancelledSignalEmission() { + compare(cancelSpy.count, 0); + testDialog.cancelled(); + compare(cancelSpy.count, 1); + compare(saveSpy.count, 0); + compare(discardSpy.count, 0); + } + + function test_multipleConsecutiveSignalEmissions() { + testDialog.saveRequested(); + testDialog.saveRequested(); + compare(saveSpy.count, 2); + + testDialog.discardRequested(); + compare(discardSpy.count, 1); + + testDialog.cancelled(); + testDialog.cancelled(); + testDialog.cancelled(); + compare(cancelSpy.count, 3); + } + + function test_openMethodExecution() { + // Verify open() executes cleanly with and without arguments + testDialog.open(dummyPage); + compare(testDialog.targetPage, dummyPage); + + var secondPage = { name: "customMockPage" }; + testDialog.open(secondPage); + compare(testDialog.targetPage.name, "customMockPage"); + } + } +} diff --git a/tests/qml/tst_timepickerpopup.qml b/tests/qml/tst_timepickerpopup.qml new file mode 100644 index 00000000..a7b650cc --- /dev/null +++ b/tests/qml/tst_timepickerpopup.qml @@ -0,0 +1,102 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import Lomiri.Components.Popups 1.3 +import "../../qml/components/dialogs" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + TimePickerPopup { + id: testTimePicker + } + + SignalSpy { + id: timeSelectedSpy + target: testTimePicker + signalName: "timeSelected" + } + + TestCase { + name: "TimePickerPopupTests" + when: windowShown + + function init() { + testTimePicker.initialHour = 1; + testTimePicker.initialMinute = 0; + timeSelectedSpy.clear(); + } + + function cleanup() { + testTimePicker.initialHour = 1; + testTimePicker.initialMinute = 0; + timeSelectedSpy.clear(); + } + + function test_initialProperties() { + compare(testTimePicker.initialHour, 1); + compare(testTimePicker.initialMinute, 0); + compare(timeSelectedSpy.count, 0); + } + + function test_propertyUpdates() { + testTimePicker.initialHour = 10; + testTimePicker.initialMinute = 45; + compare(testTimePicker.initialHour, 10); + compare(testTimePicker.initialMinute, 45); + } + + function test_openFunctionSetsInitialHourAndMinute() { + testTimePicker.open(14, 30); + compare(testTimePicker.initialHour, 14); + compare(testTimePicker.initialMinute, 30); + } + + function test_openWithoutArgumentsDefaultsToZero() { + testTimePicker.initialHour = 5; + testTimePicker.initialMinute = 25; + + testTimePicker.open(); + compare(testTimePicker.initialHour, 0); + compare(testTimePicker.initialMinute, 0); + } + + function test_timeSelectedSignalEmission() { + testTimePicker.timeSelected(8, 40); + + compare(timeSelectedSpy.count, 1); + var args = timeSelectedSpy.signalArguments[0]; + compare(args[0], 8); + compare(args[1], 40); + } + + function test_multipleTimeSelectionConfirmations() { + testTimePicker.timeSelected(0, 0); + compare(timeSelectedSpy.count, 1); + compare(timeSelectedSpy.signalArguments[0][0], 0); + compare(timeSelectedSpy.signalArguments[0][1], 0); + + testTimePicker.timeSelected(12, 30); + compare(timeSelectedSpy.count, 2); + compare(timeSelectedSpy.signalArguments[1][0], 12); + compare(timeSelectedSpy.signalArguments[1][1], 30); + + testTimePicker.timeSelected(23, 55); + compare(timeSelectedSpy.count, 3); + compare(timeSelectedSpy.signalArguments[2][0], 23); + compare(timeSelectedSpy.signalArguments[2][1], 55); + } + + function test_boundaryTimeValues() { + testTimePicker.open(23, 55); + compare(testTimePicker.initialHour, 23); + compare(testTimePicker.initialMinute, 55); + + testTimePicker.open(0, 0); + compare(testTimePicker.initialHour, 0); + compare(testTimePicker.initialMinute, 0); + } + } +}