diff --git a/tests/qml/README.md b/tests/qml/README.md index 874722f1..b7a86da5 100644 --- a/tests/qml/README.md +++ b/tests/qml/README.md @@ -44,6 +44,7 @@ tests/ │ │ └── tst_taskformutils.qml # Test suite for task form validation and save data preparation │ ├── tst_accountselectordialog.qml # Test suite for AccountSelectorDialog picker and account switch │ ├── tst_assigneefiltermenu.qml # Test suite for AssigneeFilterMenu multi-selection, search, and delegate interaction +│ ├── tst_chartinfopopup.qml # Test suite for ChartInfoPopup guide dialog and chart section descriptions │ ├── 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 @@ -57,15 +58,34 @@ tests/ │ ├── tst_loadingindicator.qml # Test suite for LoadingIndicator running state and visibility │ ├── tst_loadmorefooter.qml # Test suite for LoadMoreFooter loading state and retry trigger │ ├── tst_multiassigneeselector.qml # Test suite for MultiAssigneeSelector multi-select IDs array output +│ ├── tst_mytasksclosedindicator.qml # Test suite for MyTasksClosedIndicator closed/completed tasks badge │ ├── 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_personalstageselector.qml # Test suite for PersonalStageSelector user-specific stage selection dialog +│ ├── tst_projectcard.qml # Test suite for ProjectCard dashboard summary card and progress bar │ ├── tst_projectstageselector.qml # Test suite for ProjectStageSelector stage list per project │ ├── tst_savediscarddialog.qml # Test suite for SaveDiscardDialog save, discard, and cancel signals │ ├── tst_selectionbutton.qml # Test suite for SelectionButton selection state and click +│ ├── tst_settingsheader.qml # Test suite for SettingsHeader page header and trailing actions +│ ├── tst_settingslistitem.qml # Test suite for SettingsListItem settings list item and click handling +│ ├── tst_settingspagelayout.qml # Test suite for SettingsPageLayout layout wrapper and header anchoring │ ├── tst_stagefiltermenu.qml # Test suite for StageFilterMenu stage filter menu toggling │ ├── tst_statusselector.qml # Test suite for StatusSelector status changes and badge color +│ ├── tst_taskchartcanvas.qml # Test suite for TaskChartCanvas horizontal bar chart rendering and task selection +│ ├── tst_taskdetailscard.qml # Test suite for TaskDetailsCard task details card info and action signals +│ ├── tst_tasklist.qml # Test suite for TaskList tasks list rendering, filter, and search logic +│ ├── tst_tasknamefield.qml # Test suite for TaskNameField task name input validation and signal emission +│ ├── tst_taskpriorityselector.qml # Test suite for TaskPrioritySelector star-based priority level selector +│ ├── tst_taskrecordactionsgrid.qml # Test suite for TaskRecordActionsGrid task action buttons and signal emission +│ ├── tst_taskrow.qml # Test suite for TaskRow task list row presentation and hours display +│ ├── tst_taskschedulefields.qml # Test suite for TaskScheduleFields planned hours and date range inputs +│ ├── tst_taskstageselector.qml # Test suite for TaskStageSelector project task stage selection dialog +│ ├── tst_tasktimechart.qml # Test suite for TaskTimeChart drilldown time tracking chart navigation and model updates │ ├── tst_timepickerpopup.qml # Test suite for TimePickerPopup time selection and confirmation +│ ├── tst_timerecorderwidget.qml # Test suite for TimeRecorderWidget timer start, stop, and pause actions +│ ├── tst_timesheetdescriptionpopup.qml # Test suite for TimeSheetDescriptionPopup description editing dialog +│ ├── tst_timesheetdetailscard.qml # Test suite for TimeSheetDetailsCard entry summary display and workflow signals │ ├── 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/tst_chartinfopopup.qml b/tests/qml/tst_chartinfopopup.qml new file mode 100644 index 00000000..514d4f1d --- /dev/null +++ b/tests/qml/tst_chartinfopopup.qml @@ -0,0 +1,65 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import Lomiri.Components.Popups 1.3 +import "../../qml/features/dashboard/components" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + ChartInfoPopup { + id: testPopup + } + + TestCase { + name: "ChartInfoPopupTests" + when: windowShown + + function findLabels(item) { + var labels = []; + function traverse(current) { + if (!current) return; + if (current.text !== undefined && current.text !== "") { + labels.push(current.text); + } + for (var i = 0; i < (current.children ? current.children.length : 0); i++) { + traverse(current.children[i]); + } + } + traverse(item); + return labels; + } + + function findButtonByText(root, text) { + if (!root) return null; + if (root.text && root.text.indexOf(text) !== -1) return root; + for (var i = 0; i < (root.children ? root.children.length : 0); i++) { + var found = findButtonByText(root.children[i], text); + if (found) return found; + } + return null; + } + + function test_initialProperties() { + compare(testPopup.title, "Dashboard Charts Guide"); + } + + function test_guideSectionsContent() { + var allTexts = findLabels(testPopup); + verify(allTexts.length > 0, "Popup should contain guide text labels"); + + var joined = allTexts.join(" "); + verify(joined.indexOf("Welcome to the Dashboard") !== -1, "Should have welcome message"); + verify(joined.indexOf("Priority Matrix") !== -1, "Should have Priority Matrix guide"); + verify(joined.indexOf("Projectwise Time Spent") !== -1, "Should have Projectwise guide"); + verify(joined.indexOf("Task Drilldown") !== -1, "Should have Task Drilldown guide"); + } + + function test_gotItButton() { + var btn = findButtonByText(testPopup, "Got it!"); + verify(btn !== null, "Got it! button should be present"); + } + } +} diff --git a/tests/qml/tst_mytasksclosedindicator.qml b/tests/qml/tst_mytasksclosedindicator.qml new file mode 100644 index 00000000..4c30b19e --- /dev/null +++ b/tests/qml/tst_mytasksclosedindicator.qml @@ -0,0 +1,69 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/tasks/components" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(20) + + MyTasksClosedIndicator { + id: testIndicator + width: testRoot.width + height: units.gu(6) + } + + TestCase { + name: "MyTasksClosedIndicatorTests" + when: windowShown + + function findChildByProperty(parentItem, propName, propValue) { + if (!parentItem) return null; + if (parentItem[propName] === propValue) return parentItem; + for (var i = 0; i < (parentItem.children ? parentItem.children.length : 0); i++) { + var found = findChildByProperty(parentItem.children[i], propName, propValue); + if (found) return found; + } + return null; + } + + function findTextItem(parentItem) { + if (!parentItem) return null; + if (parentItem.text !== undefined && parentItem.text.indexOf("closed/completed") !== -1) return parentItem; + for (var i = 0; i < (parentItem.children ? parentItem.children.length : 0); i++) { + var found = findTextItem(parentItem.children[i]); + if (found) return found; + } + return null; + } + + function test_initialVisualStructure() { + verify(testIndicator !== null); + compare(testIndicator.border.width, 1); + verify(testIndicator.visible, true); + } + + function test_textBadgeContent() { + var textItem = findTextItem(testIndicator); + verify(textItem !== null, "Badge label should be present"); + compare(textItem.text, "Showing closed/completed tasks"); + compare(textItem.font.bold, true); + compare(textItem.font.pixelSize, units.gu(1.5)); + } + + function test_infoIconPresence() { + var iconItem = findChildByProperty(testIndicator, "name", "info"); + verify(iconItem !== null, "Info icon should be present"); + compare(iconItem.width, units.gu(2)); + compare(iconItem.height, units.gu(2)); + } + + function test_dimensions() { + testIndicator.width = units.gu(30); + testIndicator.height = units.gu(5); + compare(testIndicator.width, units.gu(30)); + compare(testIndicator.height, units.gu(5)); + } + } +} diff --git a/tests/qml/tst_personalstageselector.qml b/tests/qml/tst_personalstageselector.qml new file mode 100644 index 00000000..79284092 --- /dev/null +++ b/tests/qml/tst_personalstageselector.qml @@ -0,0 +1,126 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import Lomiri.Components.Popups 1.3 +import QtQuick.LocalStorage 2.0 as Sql +import "../../qml/features/tasks/components" +import "../../models/dbinit.js" as DbInit +import "../../models/database.js" as DBCommon + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + PersonalStageSelector { + id: testSelector + } + + SignalSpy { + id: personalStageSelectedSpy + target: testSelector + signalName: "personalStageSelected" + } + + TestCase { + name: "PersonalStageSelectorTests" + when: windowShown + + function initTestCase() { + DbInit.initializeDatabase(); + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("DELETE FROM project_task_type_app WHERE account_id = 9991"); + tx.executeSql("INSERT INTO project_task_type_app (account_id, odoo_record_id, name, sequence, active, fold, user_id, is_global) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [9991, 501, "Inbox", 1, 1, 0, 77, "[]"]); + tx.executeSql("INSERT INTO project_task_type_app (account_id, odoo_record_id, name, sequence, active, fold, user_id, is_global) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [9991, 502, "Today", 2, 1, 0, 77, "[]"]); + tx.executeSql("INSERT INTO project_task_type_app (account_id, odoo_record_id, name, sequence, active, fold, user_id, is_global) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [9991, 503, "Done Personal", 3, 1, 1, 77, "[]"]); + }); + } + + function cleanupTestCase() { + try { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("DELETE FROM project_task_type_app WHERE account_id = 9991"); + }); + } catch (e) { + console.log("Cleanup error: " + e); + } + } + + function init() { + testSelector.taskId = -1; + testSelector.accountId = -1; + testSelector.userId = -1; + testSelector.currentPersonalStageOdooRecordId = -1; + testSelector.availablePersonalStages = []; + personalStageSelectedSpy.clear(); + } + + function cleanup() { + personalStageSelectedSpy.clear(); + } + + function test_initialProperties() { + compare(testSelector.title, "Change Personal Stage"); + compare(testSelector.taskId, -1); + compare(testSelector.accountId, -1); + compare(testSelector.userId, -1); + compare(testSelector.currentPersonalStageOdooRecordId, -1); + compare(personalStageSelectedSpy.count, 0); + } + + function test_propertyAssignments() { + testSelector.taskId = 12; + testSelector.accountId = 9991; + testSelector.userId = 77; + testSelector.currentPersonalStageOdooRecordId = 502; + + compare(testSelector.taskId, 12); + compare(testSelector.accountId, 9991); + compare(testSelector.userId, 77); + compare(testSelector.currentPersonalStageOdooRecordId, 502); + } + + function test_loadPersonalStagesForUser() { + testSelector.accountId = 9991; + testSelector.userId = 77; + testSelector.currentPersonalStageOdooRecordId = 501; + testSelector.loadPersonalStages(); + + compare(testSelector.availablePersonalStages.length, 3); + compare(testSelector.availablePersonalStages[0].odoo_record_id, 501); + compare(testSelector.availablePersonalStages[0].name, "Inbox"); + compare(testSelector.availablePersonalStages[0].fold, 0); + + compare(testSelector.availablePersonalStages[1].odoo_record_id, 502); + compare(testSelector.availablePersonalStages[1].name, "Today"); + + compare(testSelector.availablePersonalStages[2].odoo_record_id, 503); + compare(testSelector.availablePersonalStages[2].name, "Done Personal"); + compare(testSelector.availablePersonalStages[2].fold, 1); + } + + function test_loadPersonalStagesForNonExistentUser() { + testSelector.accountId = 9991; + testSelector.userId = 99999; + testSelector.loadPersonalStages(); + compare(testSelector.availablePersonalStages.length, 0); + } + + function test_personalStageSelectedSignalEmission() { + compare(personalStageSelectedSpy.count, 0); + testSelector.personalStageSelected(502, "Today"); + compare(personalStageSelectedSpy.count, 1); + compare(personalStageSelectedSpy.signalArguments[0][0], 502); + compare(personalStageSelectedSpy.signalArguments[0][1], "Today"); + } + + function test_clearPersonalStageSignalEmission() { + testSelector.personalStageSelected(null, "(Clear Personal Stage)"); + compare(personalStageSelectedSpy.count, 1); + compare(personalStageSelectedSpy.signalArguments[0][0], 0); + compare(personalStageSelectedSpy.signalArguments[0][1], "(Clear Personal Stage)"); + } + } +} diff --git a/tests/qml/tst_projectcard.qml b/tests/qml/tst_projectcard.qml new file mode 100644 index 00000000..9f835c98 --- /dev/null +++ b/tests/qml/tst_projectcard.qml @@ -0,0 +1,71 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/dashboard/components" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(30) + + ProjectCard { + id: testCard + width: testRoot.width + } + + SignalSpy { + id: clickedSpy + target: testCard + signalName: "clicked" + } + + TestCase { + name: "ProjectCardTests" + when: windowShown + + function init() { + testCard.projectData = {}; + testCard.maxHours = 1; + clickedSpy.clear(); + } + + function cleanup() { + clickedSpy.clear(); + } + + function test_initialProperties() { + compare(testCard.maxHours, 1); + compare(testCard.implicitHeight, units.gu(11)); + compare(clickedSpy.count, 0); + } + + function test_projectDataBinding() { + testCard.projectData = { + name: "Mobile App Port", + taskCount: 8, + totalHours: 24.0, + colour: "#3498DB" + }; + testCard.maxHours = 30.0; + + compare(testCard.projectData.name, "Mobile App Port"); + compare(testCard.projectData.taskCount, 8); + compare(testCard.projectData.totalHours, 24.0); + compare(testCard.projectColor, "#3498db"); + } + + function test_projectColorNearWhiteFallback() { + testCard.projectData = { + name: "White Theme Project", + colour: "#FFFFFF" + }; + compare(testCard.projectColor, "#e95420"); + } + + function test_clickedSignalEmission() { + compare(clickedSpy.count, 0); + mouseClick(testCard); + compare(clickedSpy.count, 1); + } + } +} diff --git a/tests/qml/tst_settingsheader.qml b/tests/qml/tst_settingsheader.qml new file mode 100644 index 00000000..e7c39bd7 --- /dev/null +++ b/tests/qml/tst_settingsheader.qml @@ -0,0 +1,66 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/settings/components" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + Page { + id: testPage + anchors.fill: parent + + header: SettingsHeader { + id: testHeader + title: "Settings Overview" + } + } + + Page { + id: testPageWithAction + visible: false + + header: SettingsHeader { + id: testHeaderWithAction + title: "Header With Action" + trailingActions: [ + Action { + id: saveAction + text: "Save" + iconName: "save" + } + ] + } + } + + TestCase { + name: "SettingsHeaderTests" + when: windowShown + + function test_initialProperties() { + compare(testHeader.title, "Settings Overview"); + verify(testHeader.visible, true); + } + + function test_titlePropertyUpdate() { + testHeader.title = "Connected Accounts"; + compare(testHeader.title, "Connected Accounts"); + + testHeader.title = "Voice Model Settings"; + compare(testHeader.title, "Voice Model Settings"); + } + + function test_trailingActionsBinding() { + compare(testHeader.trailingActions.length, 0); + verify(testHeader.trailingActionBar !== null); + + // Verify trailingActions binding to trailingActionBar.actions + compare(testHeaderWithAction.trailingActions.length, 1); + compare(testHeaderWithAction.trailingActionBar.actions.length, 1); + compare(testHeaderWithAction.trailingActionBar.actions[0].text, "Save"); + compare(testHeaderWithAction.trailingActionBar.actions[0].iconName, "save"); + } + } +} diff --git a/tests/qml/tst_settingslistitem.qml b/tests/qml/tst_settingslistitem.qml new file mode 100644 index 00000000..1db5790e --- /dev/null +++ b/tests/qml/tst_settingslistitem.qml @@ -0,0 +1,86 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/settings/components" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(30) + + SettingsListItem { + id: testItem + width: testRoot.width + } + + SignalSpy { + id: clickedSpy + target: testItem + signalName: "clicked" + } + + TestCase { + name: "SettingsListItemTests" + when: windowShown + + function init() { + testItem.text = ""; + testItem.iconName = ""; + testItem.iconColor = "#666"; + testItem.showProgression = true; + testItem.showDivider = true; + testItem.active = false; + testItem.collapsed = false; + clickedSpy.clear(); + } + + function cleanup() { + clickedSpy.clear(); + } + + function test_initialProperties() { + compare(testItem.text, ""); + compare(testItem.iconName, ""); + compare(testItem.showProgression, true); + compare(testItem.showDivider, true); + compare(testItem.active, false); + compare(testItem.collapsed, false); + compare(testItem.height, units.gu(7)); + compare(clickedSpy.count, 0); + } + + function test_propertyAssignments() { + testItem.text = "Connected Accounts"; + testItem.iconName = "account-multiple"; + testItem.iconColor = "#0078d4"; + testItem.active = true; + + compare(testItem.text, "Connected Accounts"); + compare(testItem.iconName, "account-multiple"); + compare(testItem.iconColor, "#0078d4"); + compare(testItem.active, true); + } + + function test_collapsedMode() { + testItem.collapsed = true; + compare(testItem.collapsed, true); + + testItem.collapsed = false; + compare(testItem.collapsed, false); + } + + function test_showProgressionAndDivider() { + testItem.showProgression = false; + testItem.showDivider = false; + + compare(testItem.showProgression, false); + compare(testItem.showDivider, false); + } + + function test_clickedSignalEmission() { + compare(clickedSpy.count, 0); + mouseClick(testItem); + compare(clickedSpy.count, 1); + } + } +} diff --git a/tests/qml/tst_settingspagelayout.qml b/tests/qml/tst_settingspagelayout.qml new file mode 100644 index 00000000..dd525566 --- /dev/null +++ b/tests/qml/tst_settingspagelayout.qml @@ -0,0 +1,53 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/settings/components" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + Rectangle { + id: dummyHeader + width: parent.width + height: units.gu(6) + color: LomiriColors.orange + } + + SettingsPageLayout { + id: testLayout + headerItem: dummyHeader + + Label { + id: innerLabel + text: "Settings Content Item" + } + } + + TestCase { + name: "SettingsPageLayoutTests" + when: windowShown + + function test_initialStructure() { + verify(testLayout !== null); + compare(testLayout.headerItem, dummyHeader); + compare(testLayout.anchors.top, dummyHeader.bottom); + } + + function test_contentChildrenHosting() { + verify(innerLabel !== null); + compare(innerLabel.text, "Settings Content Item"); + verify(innerLabel.parent !== null); + } + + function test_nullHeaderFallback() { + testLayout.headerItem = null; + compare(testLayout.headerItem, null); + compare(testLayout.anchors.top, testRoot.top); + + testLayout.headerItem = dummyHeader; + compare(testLayout.anchors.top, dummyHeader.bottom); + } + } +} diff --git a/tests/qml/tst_taskchartcanvas.qml b/tests/qml/tst_taskchartcanvas.qml new file mode 100644 index 00000000..c38c713b --- /dev/null +++ b/tests/qml/tst_taskchartcanvas.qml @@ -0,0 +1,71 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/dashboard/components" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + TaskChartCanvas { + id: testCanvas + width: testRoot.width + } + + SignalSpy { + id: taskSelectedSpy + target: testCanvas + signalName: "taskSelected" + } + + TestCase { + name: "TaskChartCanvasTests" + when: windowShown + + function init() { + testCanvas.tasksData = []; + testCanvas.accentColour = "#E95420"; + testCanvas.highlightedIndex = -1; + taskSelectedSpy.clear(); + } + + function cleanup() { + taskSelectedSpy.clear(); + } + + function test_initialProperties() { + compare(testCanvas.tasksData.length, 0); + compare(testCanvas.maxHours, 1); + compare(testCanvas.highlightedIndex, -1); + verify(testCanvas.barColors.length > 0); + compare(taskSelectedSpy.count, 0); + } + + function test_tasksDataAndMaxHoursCalculation() { + testCanvas.tasksData = [ + { id: "task_1", name: "UI Redesign", totalHours: 6.0 }, + { id: "task_2", name: "API Client", totalHours: 14.5 }, + { id: "task_3", name: "Testing", totalHours: 3.5 } + ]; + + compare(testCanvas.tasksData.length, 3); + compare(testCanvas.maxHours, 14.5); + verify(testCanvas.implicitHeight > units.gu(10)); + } + + function test_barColorsPalette() { + var colors = testCanvas.barColors; + verify(colors.indexOf("#E95420") !== -1); + verify(colors.indexOf("#3498DB") !== -1); + verify(colors.indexOf("#2ECC71") !== -1); + } + + function test_taskSelectedSignalEmission() { + compare(taskSelectedSpy.count, 0); + testCanvas.taskSelected("task_42"); + compare(taskSelectedSpy.count, 1); + compare(taskSelectedSpy.signalArguments[0][0], "task_42"); + } + } +} diff --git a/tests/qml/tst_taskdetailscard.qml b/tests/qml/tst_taskdetailscard.qml new file mode 100644 index 00000000..ed1b41f6 --- /dev/null +++ b/tests/qml/tst_taskdetailscard.qml @@ -0,0 +1,217 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/tasks/components" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + Item { + id: globalTimerWidget + signal timerStopped() + signal timerStarted() + signal timerPaused() + signal timerResumed() + } + + TaskDetailsCard { + id: testCard + width: testRoot.width + } + + SignalSpy { + id: editRequestedSpy + target: testCard + signalName: "editRequested" + } + + SignalSpy { + id: deleteRequestedSpy + target: testCard + signalName: "deleteRequested" + } + + SignalSpy { + id: viewRequestedSpy + target: testCard + signalName: "viewRequested" + } + + SignalSpy { + id: timesheetRequestedSpy + target: testCard + signalName: "timesheetRequested" + } + + SignalSpy { + id: taskUpdatedSpy + target: testCard + signalName: "taskUpdated" + } + + SignalSpy { + id: taskStageChangedSpy + target: testCard + signalName: "taskStageChanged" + } + + SignalSpy { + id: navigationRequestedSpy + target: testCard + signalName: "navigationRequested" + } + + TestCase { + name: "TaskDetailsCardTests" + when: windowShown + + function init() { + testCard.taskName = ""; + testCard.projectName = ""; + testCard.priority = 0; + testCard.allocatedHours = 0; + testCard.spentHours = 0; + testCard.deadline = ""; + testCard.localId = -1; + testCard.recordId = -1; + testCard.idVal = -1; + testCard.accountId = -1; + testCard.stage = -1; + testCard.hasChildren = false; + testCard.flatViewMode = false; + testCard.hasDraft = false; + + editRequestedSpy.clear(); + deleteRequestedSpy.clear(); + viewRequestedSpy.clear(); + timesheetRequestedSpy.clear(); + taskUpdatedSpy.clear(); + taskStageChangedSpy.clear(); + navigationRequestedSpy.clear(); + } + + function cleanup() { + editRequestedSpy.clear(); + deleteRequestedSpy.clear(); + viewRequestedSpy.clear(); + timesheetRequestedSpy.clear(); + taskUpdatedSpy.clear(); + taskStageChangedSpy.clear(); + navigationRequestedSpy.clear(); + } + + function test_initialProperties() { + compare(testCard.priority, 0); + compare(testCard.taskName, ""); + compare(testCard.projectName, ""); + compare(testCard.allocatedHours, 0); + compare(testCard.spentHours, 0); + compare(testCard.deadline, ""); + compare(testCard.localId, -1); + compare(testCard.recordId, -1); + compare(testCard.idVal, -1); + compare(testCard.hasChildren, false); + compare(testCard.flatViewMode, false); + compare(testCard.hasDraft, false); + compare(testCard.timer_on, false); + compare(testCard.timer_paused, false); + } + + function test_propertyAssignments() { + testCard.taskName = "Complete Testing"; + testCard.projectName = "Time Management"; + testCard.priority = 2; + testCard.allocatedHours = 10.5; + testCard.spentHours = 4.2; + testCard.deadline = "2026-12-31"; + testCard.localId = 42; + testCard.recordId = 1042; + testCard.accountId = 1; + testCard.hasDraft = true; + + compare(testCard.taskName, "Complete Testing"); + compare(testCard.projectName, "Time Management"); + compare(testCard.priority, 2); + compare(testCard.allocatedHours, 10.5); + compare(testCard.spentHours, 4.2); + compare(testCard.deadline, "2026-12-31"); + compare(testCard.localId, 42); + compare(testCard.recordId, 1042); + compare(testCard.accountId, 1); + compare(testCard.hasDraft, true); + } + + function test_effectiveTaskIdCalculation() { + // Case 1: idVal > 0 takes priority + testCard.idVal = 99; + testCard.localId = 10; + testCard.recordId = 20; + testCard.accountId = 1; + compare(testCard.effectiveTaskId, 99); + + // Case 2: idVal <= 0, accountId === 0 uses localId + testCard.idVal = -1; + testCard.accountId = 0; + testCard.localId = 15; + testCard.recordId = 30; + compare(testCard.effectiveTaskId, 15); + + // Case 3: idVal <= 0, recordId <= 0 uses localId + testCard.idVal = -1; + testCard.accountId = 1; + testCard.localId = 25; + testCard.recordId = 0; + compare(testCard.effectiveTaskId, 25); + + // Case 4: normal remote account uses recordId + testCard.idVal = -1; + testCard.accountId = 1; + testCard.localId = 25; + testCard.recordId = 300; + compare(testCard.effectiveTaskId, 300); + } + + function test_signalsEmission() { + testCard.localId = 88; + + compare(editRequestedSpy.count, 0); + testCard.editRequested(88); + compare(editRequestedSpy.count, 1); + compare(editRequestedSpy.signalArguments[0][0], 88); + + compare(deleteRequestedSpy.count, 0); + testCard.deleteRequested(88); + compare(deleteRequestedSpy.count, 1); + compare(deleteRequestedSpy.signalArguments[0][0], 88); + + compare(viewRequestedSpy.count, 0); + testCard.viewRequested(88); + compare(viewRequestedSpy.count, 1); + compare(viewRequestedSpy.signalArguments[0][0], 88); + + compare(timesheetRequestedSpy.count, 0); + testCard.timesheetRequested(88); + compare(timesheetRequestedSpy.count, 1); + compare(timesheetRequestedSpy.signalArguments[0][0], 88); + + compare(taskUpdatedSpy.count, 0); + testCard.taskUpdated(88); + compare(taskUpdatedSpy.count, 1); + compare(taskUpdatedSpy.signalArguments[0][0], 88); + + compare(taskStageChangedSpy.count, 0); + testCard.taskStageChanged(88); + compare(taskStageChangedSpy.count, 1); + compare(taskStageChangedSpy.signalArguments[0][0], 88); + + compare(navigationRequestedSpy.count, 0); + testCard.navigationRequested(88, 1, "Sample Task"); + compare(navigationRequestedSpy.count, 1); + compare(navigationRequestedSpy.signalArguments[0][0], 88); + compare(navigationRequestedSpy.signalArguments[0][1], 1); + compare(navigationRequestedSpy.signalArguments[0][2], "Sample Task"); + } + } +} diff --git a/tests/qml/tst_tasklist.qml b/tests/qml/tst_tasklist.qml new file mode 100644 index 00000000..aa9d1b8f --- /dev/null +++ b/tests/qml/tst_tasklist.qml @@ -0,0 +1,162 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import QtQuick.LocalStorage 2.0 as Sql +import "../../qml/features/tasks/components" +import "../../models/dbinit.js" as DbInit +import "../../models/database.js" as DBCommon + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + Item { + id: globalTimerWidget + signal timerStopped() + signal timerStarted() + signal timerPaused() + signal timerResumed() + } + + Item { + id: accountPicker + property int selectedAccountId: 0 + signal accepted(int id, string name) + } + + TaskList { + id: testTaskList + anchors.fill: parent + } + + SignalSpy { + id: taskSelectedSpy + target: testTaskList + signalName: "taskSelected" + } + + SignalSpy { + id: taskEditRequestedSpy + target: testTaskList + signalName: "taskEditRequested" + } + + SignalSpy { + id: taskDeleteRequestedSpy + target: testTaskList + signalName: "taskDeleteRequested" + } + + SignalSpy { + id: taskTimesheetRequestedSpy + target: testTaskList + signalName: "taskTimesheetRequested" + } + + TestCase { + name: "TaskListTests" + when: windowShown + + function initTestCase() { + DbInit.initializeDatabase(); + } + + function init() { + testTaskList.currentFilter = "today"; + testTaskList.currentSearchQuery = ""; + testTaskList.filterByProject = false; + testTaskList.projectOdooRecordId = -1; + testTaskList.projectAccountId = -1; + testTaskList.filterByAccount = false; + testTaskList.filterByAssignees = false; + testTaskList.flatViewMode = false; + + taskSelectedSpy.clear(); + taskEditRequestedSpy.clear(); + taskDeleteRequestedSpy.clear(); + taskTimesheetRequestedSpy.clear(); + } + + function cleanup() { + taskSelectedSpy.clear(); + taskEditRequestedSpy.clear(); + taskDeleteRequestedSpy.clear(); + taskTimesheetRequestedSpy.clear(); + } + + function test_initialProperties() { + compare(testTaskList.currentFilter, "today"); + compare(testTaskList.currentSearchQuery, ""); + compare(testTaskList.pageSize, 30); + compare(testTaskList.currentParentId, -1); + compare(testTaskList.filterByProject, false); + compare(testTaskList.flatViewMode, false); + compare(testTaskList.isMyTasksContext, false); + } + + function test_applyFilterMethod() { + testTaskList.applyFilter("all"); + compare(testTaskList.currentFilter, "all"); + compare(testTaskList.currentParentId, -1); + + testTaskList.applyFilter("starred"); + compare(testTaskList.currentFilter, "starred"); + } + + function test_applySearchMethod() { + testTaskList.applySearch("QML Unit Test"); + compare(testTaskList.currentSearchQuery, "QML Unit Test"); + compare(testTaskList.currentParentId, -1); + + testTaskList.applySearch(""); + compare(testTaskList.currentSearchQuery, ""); + } + + function test_applyProjectFilterMethod() { + testTaskList.applyProjectFilter(105, 1); + compare(testTaskList.filterByProject, true); + compare(testTaskList.projectOdooRecordId, 105); + compare(testTaskList.currentFilter, "all"); + compare(testTaskList.currentSearchQuery, ""); + } + + function test_applyProjectAndTimeFilterMethod() { + testTaskList.applyProjectAndTimeFilter(205, 2, "overdue"); + compare(testTaskList.filterByProject, true); + compare(testTaskList.projectOdooRecordId, 205); + compare(testTaskList.projectAccountId, 2); + compare(testTaskList.currentFilter, "overdue"); + } + + function test_applyProjectAndSearchFilterMethod() { + testTaskList.applyProjectAndSearchFilter(305, 3, "Database Refactor"); + compare(testTaskList.filterByProject, true); + compare(testTaskList.projectOdooRecordId, 305); + compare(testTaskList.projectAccountId, 3); + compare(testTaskList.currentSearchQuery, "Database Refactor"); + } + + function test_signalsEmission() { + compare(taskSelectedSpy.count, 0); + testTaskList.taskSelected(55); + compare(taskSelectedSpy.count, 1); + compare(taskSelectedSpy.signalArguments[0][0], 55); + + compare(taskEditRequestedSpy.count, 0); + testTaskList.taskEditRequested(66); + compare(taskEditRequestedSpy.count, 1); + compare(taskEditRequestedSpy.signalArguments[0][0], 66); + + compare(taskDeleteRequestedSpy.count, 0); + testTaskList.taskDeleteRequested(77); + compare(taskDeleteRequestedSpy.count, 1); + compare(taskDeleteRequestedSpy.signalArguments[0][0], 77); + + compare(taskTimesheetRequestedSpy.count, 0); + testTaskList.taskTimesheetRequested(88); + compare(taskTimesheetRequestedSpy.count, 1); + compare(taskTimesheetRequestedSpy.signalArguments[0][0], 88); + } + } +} diff --git a/tests/qml/tst_tasknamefield.qml b/tests/qml/tst_tasknamefield.qml new file mode 100644 index 00000000..785dd001 --- /dev/null +++ b/tests/qml/tst_tasknamefield.qml @@ -0,0 +1,100 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/tasks/components" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + TaskNameField { + id: testField + availableWidth: testRoot.width + } + + SignalSpy { + id: nameEditedSpy + target: testField + signalName: "nameEdited" + } + + TestCase { + name: "TaskNameFieldTests" + when: windowShown + + function init() { + testField.text = ""; + testField.isReadOnly = false; + testField.availableWidth = testRoot.width; + nameEditedSpy.clear(); + } + + function cleanup() { + testField.text = ""; + testField.isReadOnly = false; + nameEditedSpy.clear(); + } + + function test_initialProperties() { + compare(testField.text, ""); + compare(testField.isReadOnly, false); + compare(testField.height, units.gu(6)); + compare(testField.spacing, units.gu(1)); + compare(nameEditedSpy.count, 0); + } + + function test_textPropertyChangeEmitsSignal() { + compare(nameEditedSpy.count, 0); + testField.text = "Write Unit Tests"; + compare(testField.text, "Write Unit Tests"); + compare(nameEditedSpy.count, 1); + compare(nameEditedSpy.signalArguments[0][0], "Write Unit Tests"); + } + + function test_multipleTextEdits() { + testField.text = "First Task"; + testField.text = "Updated Task"; + compare(testField.text, "Updated Task"); + compare(nameEditedSpy.count, 2); + compare(nameEditedSpy.signalArguments[1][0], "Updated Task"); + } + + function test_emptyAndWhitespaceValidation() { + testField.text = " "; + compare(testField.text, " "); + compare(nameEditedSpy.count, 1); + compare(nameEditedSpy.signalArguments[0][0], " "); + + testField.text = ""; + compare(testField.text, ""); + compare(nameEditedSpy.count, 2); + compare(nameEditedSpy.signalArguments[1][0], ""); + } + + function test_specialCharactersInput() { + var specialText = "!@#$%^&*()_+{}[]:;\"'|<>,.?/~`"; + testField.text = specialText; + compare(testField.text, specialText); + compare(nameEditedSpy.count, 1); + compare(nameEditedSpy.signalArguments[0][0], specialText); + } + + function test_readOnlyState() { + testField.isReadOnly = true; + compare(testField.isReadOnly, true); + + // Programmatic update still works, but readOnly flag reflects state + testField.text = "Read Only Task"; + compare(testField.text, "Read Only Task"); + } + + function test_availableWidthScaling() { + testField.availableWidth = units.gu(50); + compare(testField.width, units.gu(50)); + + testField.availableWidth = units.gu(30); + compare(testField.width, units.gu(30)); + } + } +} diff --git a/tests/qml/tst_taskpriorityselector.qml b/tests/qml/tst_taskpriorityselector.qml new file mode 100644 index 00000000..0f33a216 --- /dev/null +++ b/tests/qml/tst_taskpriorityselector.qml @@ -0,0 +1,118 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/tasks/components" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(30) + + TaskPrioritySelector { + id: testSelector + width: testRoot.width + } + + TestCase { + name: "TaskPrioritySelectorTests" + when: windowShown + + function getStarMouseAreas() { + var mas = []; + function findMAs(item) { + if (!item) return; + if (item instanceof MouseArea) mas.push(item); + for (var i = 0; i < (item.children ? item.children.length : 0); i++) { + findMAs(item.children[i]); + } + } + findMAs(testSelector); + return mas; + } + + function findLevelLabel(rootItem) { + if (!rootItem) return null; + if (rootItem.text !== undefined && rootItem.text.indexOf("Level:") !== -1) return rootItem; + for (var i = 0; i < (rootItem.children ? rootItem.children.length : 0); i++) { + var found = findLevelLabel(rootItem.children[i]); + if (found) return found; + } + return null; + } + + function init() { + testSelector.priority = 0; + testSelector.isReadOnly = false; + } + + function cleanup() { + testSelector.priority = 0; + testSelector.isReadOnly = false; + } + + function test_initialProperties() { + compare(testSelector.priority, 0); + compare(testSelector.isReadOnly, false); + compare(testSelector.height, units.gu(6)); + + var lbl = findLevelLabel(testSelector); + if (lbl) { + compare(lbl.visible, false); + } + } + + function test_setPriorityDirectly() { + testSelector.priority = 1; + compare(testSelector.priority, 1); + + var lbl = findLevelLabel(testSelector); + verify(lbl !== null); + compare(lbl.visible, true); + compare(lbl.text, "(Level: 1)"); + + testSelector.priority = 3; + compare(testSelector.priority, 3); + compare(lbl.text, "(Level: 3)"); + + testSelector.priority = 0; + compare(testSelector.priority, 0); + compare(lbl.visible, false); + } + + function test_mouseClickTogglesPriority() { + var mas = getStarMouseAreas(); + verify(mas.length >= 3, "Should have 3 star mouse areas"); + + // Click star 1 (index 0) -> priority 1 + mouseClick(mas[0]); + compare(testSelector.priority, 1); + + // Click star 1 again -> toggles to 0 + mouseClick(mas[0]); + compare(testSelector.priority, 0); + + // Click star 2 (index 1) -> priority 2 + mouseClick(mas[1]); + compare(testSelector.priority, 2); + + // Click star 3 (index 2) -> priority 3 + mouseClick(mas[2]); + compare(testSelector.priority, 3); + } + + function test_readOnlyDisablesMouseClicks() { + testSelector.isReadOnly = true; + testSelector.priority = 1; + + var mas = getStarMouseAreas(); + verify(mas.length >= 3); + compare(mas[0].enabled, false); + compare(mas[1].enabled, false); + compare(mas[2].enabled, false); + + mouseClick(mas[2]); + // Priority should remain unchanged + compare(testSelector.priority, 1); + } + } +} diff --git a/tests/qml/tst_taskrecordactionsgrid.qml b/tests/qml/tst_taskrecordactionsgrid.qml new file mode 100644 index 00000000..6b43a908 --- /dev/null +++ b/tests/qml/tst_taskrecordactionsgrid.qml @@ -0,0 +1,124 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/tasks/components" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + TaskRecordActionsGrid { + id: testGrid + width: testRoot.width + } + + SignalSpy { + id: changeStageSpy + target: testGrid + signalName: "changeStageRequested" + } + + SignalSpy { + id: createActivitySpy + target: testGrid + signalName: "createActivityRequested" + } + + SignalSpy { + id: viewActivitiesSpy + target: testGrid + signalName: "viewActivitiesRequested" + } + + SignalSpy { + id: createTimesheetSpy + target: testGrid + signalName: "createTimesheetRequested" + } + + SignalSpy { + id: viewTimesheetsSpy + target: testGrid + signalName: "viewTimesheetsRequested" + } + + TestCase { + name: "TaskRecordActionsGridTests" + when: windowShown + + function init() { + testGrid.recordid = 0; + testGrid.currentTask = null; + changeStageSpy.clear(); + createActivitySpy.clear(); + viewActivitiesSpy.clear(); + createTimesheetSpy.clear(); + viewTimesheetsSpy.clear(); + } + + function cleanup() { + testGrid.recordid = 0; + testGrid.currentTask = null; + changeStageSpy.clear(); + createActivitySpy.clear(); + viewActivitiesSpy.clear(); + createTimesheetSpy.clear(); + viewTimesheetsSpy.clear(); + } + + function test_initialProperties() { + compare(testGrid.recordid, 0); + compare(testGrid.currentTask, null); + compare(testGrid.visible, false); + compare(testGrid.columns, 3); + compare(testGrid.rows, 3); + } + + function test_visibilityOnRecordId() { + testGrid.recordid = 101; + compare(testGrid.visible, true); + verify(testGrid.height > 0); + + testGrid.recordid = 0; + compare(testGrid.visible, false); + compare(testGrid.height, 0); + } + + function test_signalEmissions() { + testGrid.recordid = 101; + + compare(changeStageSpy.count, 0); + testGrid.changeStageRequested(); + compare(changeStageSpy.count, 1); + + compare(createActivitySpy.count, 0); + testGrid.createActivityRequested(); + compare(createActivitySpy.count, 1); + + compare(viewActivitiesSpy.count, 0); + testGrid.viewActivitiesRequested(); + compare(viewActivitiesSpy.count, 1); + + compare(createTimesheetSpy.count, 0); + testGrid.createTimesheetRequested(); + compare(createTimesheetSpy.count, 1); + + compare(viewTimesheetsSpy.count, 0); + testGrid.viewTimesheetsRequested(); + compare(viewTimesheetsSpy.count, 1); + } + + function test_currentTaskAssignment() { + testGrid.recordid = 42; + testGrid.currentTask = { + id: 42, + name: "Test Task", + state: 1, + account_id: 0 + }; + compare(testGrid.currentTask.name, "Test Task"); + compare(testGrid.currentTask.id, 42); + } + } +} diff --git a/tests/qml/tst_taskrow.qml b/tests/qml/tst_taskrow.qml new file mode 100644 index 00000000..5a55415d --- /dev/null +++ b/tests/qml/tst_taskrow.qml @@ -0,0 +1,70 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/dashboard/components" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(30) + + TaskRow { + id: testRow + width: testRoot.width + } + + SignalSpy { + id: clickedSpy + target: testRow + signalName: "clicked" + } + + TestCase { + name: "TaskRowTests" + when: windowShown + + function init() { + testRow.taskData = {}; + testRow.projectTotalHours = 0; + testRow.accentColour = "#E95420"; + clickedSpy.clear(); + } + + function cleanup() { + clickedSpy.clear(); + } + + function test_initialProperties() { + compare(testRow.projectTotalHours, 0); + compare(testRow.accentColour, "#e95420"); + compare(testRow.implicitHeight, units.gu(8)); + compare(clickedSpy.count, 0); + } + + function test_taskDataBinding() { + testRow.taskData = { + name: "Database Migration", + totalHours: 12.5 + }; + testRow.projectTotalHours = 50.0; + + compare(testRow.taskData.name, "Database Migration"); + compare(testRow.taskData.totalHours, 12.5); + compare(testRow.projectTotalHours, 50.0); + } + + function test_accentColourCustomization() { + testRow.accentColour = "#3498db"; + compare(testRow.accentColour, "#3498db"); + + testRow.accentColour = "#2ecc71"; + compare(testRow.accentColour, "#2ecc71"); + } + + function test_clickedSignalEmission() { + compare(clickedSpy.count, 0); + mouseClick(testRow); + compare(clickedSpy.count, 1); + } + } +} diff --git a/tests/qml/tst_taskschedulefields.qml b/tests/qml/tst_taskschedulefields.qml new file mode 100644 index 00000000..77883dfc --- /dev/null +++ b/tests/qml/tst_taskschedulefields.qml @@ -0,0 +1,109 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/tasks/components" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + TaskScheduleFields { + id: testFields + availableWidth: testRoot.width + } + + SignalSpy { + id: hoursChangedSpy + target: testFields + signalName: "hoursChanged" + } + + SignalSpy { + id: deadlineChangedSpy + target: testFields + signalName: "deadlineChanged" + } + + SignalSpy { + id: dateRangeChangedSpy + target: testFields + signalName: "dateRangeChanged" + } + + TestCase { + name: "TaskScheduleFieldsTests" + when: windowShown + + function init() { + testFields.isReadOnly = false; + testFields.hoursText = "01:00"; + testFields.deadlineText = "Not set"; + hoursChangedSpy.clear(); + deadlineChangedSpy.clear(); + dateRangeChangedSpy.clear(); + } + + function cleanup() { + hoursChangedSpy.clear(); + deadlineChangedSpy.clear(); + dateRangeChangedSpy.clear(); + } + + function test_initialProperties() { + compare(testFields.isReadOnly, false); + compare(testFields.hoursText, "01:00"); + compare(testFields.deadlineText, "Not set"); + verify(testFields.dateRangeWidget !== null); + } + + function test_hoursChangedSignalEmission() { + testFields.hoursText = "03:30"; + compare(testFields.hoursText, "03:30"); + compare(hoursChangedSpy.count, 1); + compare(hoursChangedSpy.signalArguments[0][0], "03:30"); + } + + function test_incdecHrsIncrement() { + testFields.hoursText = "01:00"; + hoursChangedSpy.clear(); + + testFields.incdecHrs(1); + compare(testFields.hoursText, "2:00"); + verify(hoursChangedSpy.count >= 1); + + testFields.incdecHrs(1); + compare(testFields.hoursText, "3:00"); + } + + function test_incdecHrsDecrement() { + testFields.hoursText = "3:00"; + testFields.incdecHrs(-1); + compare(testFields.hoursText, "2:00"); + + testFields.incdecHrs(-1); + compare(testFields.hoursText, "1:00"); + + // Decrement should not go below 0 (clamped) + testFields.incdecHrs(-1); + compare(testFields.hoursText, "0:00"); + } + + function test_deadlineTextChange() { + testFields.deadlineText = "2026-11-15"; + compare(testFields.deadlineText, "2026-11-15"); + } + + function test_readOnlyState() { + testFields.isReadOnly = true; + compare(testFields.isReadOnly, true); + compare(testFields.dateRangeWidget.readOnly, true); + } + + function test_setDateRangeFunction() { + testFields.setDateRange("2026-10-01", "2026-10-10"); + verify(testFields.formattedStartDate() !== ""); + verify(testFields.formattedEndDate() !== ""); + } + } +} diff --git a/tests/qml/tst_taskstageselector.qml b/tests/qml/tst_taskstageselector.qml new file mode 100644 index 00000000..305846af --- /dev/null +++ b/tests/qml/tst_taskstageselector.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 QtQuick.LocalStorage 2.0 as Sql +import "../../qml/features/tasks/components" +import "../../models/dbinit.js" as DbInit +import "../../models/database.js" as DBCommon + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + TaskStageSelector { + id: testStageSelector + } + + SignalSpy { + id: stageSelectedSpy + target: testStageSelector + signalName: "stageSelected" + } + + TestCase { + name: "TaskStageSelectorTests" + when: windowShown + + function initTestCase() { + DbInit.initializeDatabase(); + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("DELETE FROM project_task_type_app WHERE account_id = 9992"); + tx.executeSql("INSERT INTO project_task_type_app (account_id, odoo_record_id, name, sequence, active, fold, is_global, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [9992, 601, "New", 1, 1, 0, "1", "New tasks"]); + tx.executeSql("INSERT INTO project_task_type_app (account_id, odoo_record_id, name, sequence, active, fold, is_global, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [9992, 602, "In Progress", 2, 1, 0, "1", "Under active development"]); + tx.executeSql("INSERT INTO project_task_type_app (account_id, odoo_record_id, name, sequence, active, fold, is_global, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [9992, 603, "Done", 3, 1, 1, "1", "Completed tasks"]); + }); + } + + function cleanupTestCase() { + try { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("DELETE FROM project_task_type_app WHERE account_id = 9992"); + }); + } catch (e) { + console.log("Cleanup error: " + e); + } + } + + function init() { + testStageSelector.taskId = -1; + testStageSelector.projectOdooRecordId = -1; + testStageSelector.accountId = -1; + testStageSelector.currentStageOdooRecordId = -1; + testStageSelector.availableStages = []; + stageSelectedSpy.clear(); + } + + function cleanup() { + stageSelectedSpy.clear(); + } + + function test_initialProperties() { + compare(testStageSelector.title, "Change Task Stage"); + compare(testStageSelector.taskId, -1); + compare(testStageSelector.projectOdooRecordId, -1); + compare(testStageSelector.accountId, -1); + compare(testStageSelector.currentStageOdooRecordId, -1); + compare(stageSelectedSpy.count, 0); + } + + function test_propertyAssignments() { + testStageSelector.taskId = 100; + testStageSelector.projectOdooRecordId = 200; + testStageSelector.accountId = 9992; + testStageSelector.currentStageOdooRecordId = 601; + + compare(testStageSelector.taskId, 100); + compare(testStageSelector.projectOdooRecordId, 200); + compare(testStageSelector.accountId, 9992); + compare(testStageSelector.currentStageOdooRecordId, 601); + } + + function test_loadStagesForProject() { + testStageSelector.projectOdooRecordId = 200; + testStageSelector.accountId = 9992; + testStageSelector.currentStageOdooRecordId = 601; + testStageSelector.loadStages(); + + compare(testStageSelector.availableStages.length, 3); + compare(testStageSelector.availableStages[0].odoo_record_id, 601); + compare(testStageSelector.availableStages[0].name, "New"); + compare(testStageSelector.availableStages[0].fold, 0); + + compare(testStageSelector.availableStages[1].odoo_record_id, 602); + compare(testStageSelector.availableStages[1].name, "In Progress"); + + compare(testStageSelector.availableStages[2].odoo_record_id, 603); + compare(testStageSelector.availableStages[2].name, "Done"); + compare(testStageSelector.availableStages[2].fold, 1); + } + + function test_loadStagesEmptyAccount() { + testStageSelector.projectOdooRecordId = 200; + testStageSelector.accountId = 77777; + testStageSelector.loadStages(); + compare(testStageSelector.availableStages.length, 0); + } + + function test_stageSelectedSignalEmission() { + compare(stageSelectedSpy.count, 0); + testStageSelector.stageSelected(603, "Done"); + compare(stageSelectedSpy.count, 1); + compare(stageSelectedSpy.signalArguments[0][0], 603); + compare(stageSelectedSpy.signalArguments[0][1], "Done"); + } + + function test_stageSelectedMultipleTransitions() { + testStageSelector.stageSelected(601, "New"); + testStageSelector.stageSelected(602, "In Progress"); + testStageSelector.stageSelected(603, "Done"); + + compare(stageSelectedSpy.count, 3); + compare(stageSelectedSpy.signalArguments[0][0], 601); + compare(stageSelectedSpy.signalArguments[0][1], "New"); + compare(stageSelectedSpy.signalArguments[1][0], 602); + compare(stageSelectedSpy.signalArguments[1][1], "In Progress"); + compare(stageSelectedSpy.signalArguments[2][0], 603); + compare(stageSelectedSpy.signalArguments[2][1], "Done"); + } + } +} diff --git a/tests/qml/tst_tasktimechart.qml b/tests/qml/tst_tasktimechart.qml new file mode 100644 index 00000000..f8a54787 --- /dev/null +++ b/tests/qml/tst_tasktimechart.qml @@ -0,0 +1,162 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/dashboard/charts" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + TaskTimeChart { + id: testChart + width: testRoot.width + } + + SignalSpy { + id: projectOpenedSpy + target: testChart + signalName: "projectOpened" + } + + SignalSpy { + id: taskOpenedSpy + target: testChart + signalName: "taskOpened" + } + + SignalSpy { + id: backNavigatedSpy + target: testChart + signalName: "backNavigated" + } + + TestCase { + name: "TaskTimeChartTests" + when: windowShown + + function init() { + testChart.projectsModel = []; + testChart.currentView = "projects"; + testChart.selectedProject = null; + testChart.selectedTask = null; + testChart.selectedProjectTasks = []; + testChart.selectedTaskLogs = []; + testChart.portfolioSearchText = ""; + testChart.portfolioSortMode = "time"; + testChart.showAllTasks = false; + + projectOpenedSpy.clear(); + taskOpenedSpy.clear(); + backNavigatedSpy.clear(); + } + + function cleanup() { + projectOpenedSpy.clear(); + taskOpenedSpy.clear(); + backNavigatedSpy.clear(); + } + + function test_initialProperties() { + compare(testChart.currentView, "projects"); + compare(testChart.projectsModel.length, 0); + compare(testChart.selectedProject, null); + compare(testChart.selectedTask, null); + compare(testChart.portfolioSortMode, "time"); + compare(testChart.portfolioSearchText, ""); + compare(testChart.portfolioTotalHours, 0); + compare(testChart.portfolioMaxHours, 0); + compare(projectOpenedSpy.count, 0); + } + + function test_projectsModelCalculations() { + testChart.projectsModel = [ + { id: "proj_1", name: "Web App", totalHours: 20.0, taskCount: 4, colour: "#3498DB" }, + { id: "proj_2", name: "Mobile Client", totalHours: 45.0, taskCount: 8, colour: "#E95420" } + ]; + + compare(testChart.portfolioTotalHours, 65.0); + compare(testChart.portfolioMaxHours, 45.0); + compare(testChart.visibleProjects.length, 2); + } + + function test_openProjectNavigation() { + var sampleProject = { + id: "proj_1", + name: "Web App", + totalHours: 20.0, + colour: "#3498DB" + }; + + testChart.openProject(sampleProject); + compare(testChart.currentView, "project"); + compare(testChart.selectedProject.id, "proj_1"); + compare(projectOpenedSpy.count, 1); + compare(projectOpenedSpy.signalArguments[0][0], "proj_1"); + } + + function test_openTaskNavigation() { + var sampleProject = { + id: "proj_1", + name: "Web App", + totalHours: 20.0, + colour: "#3498DB" + }; + var sampleTask = { + id: "task_1", + name: "Implement API", + totalHours: 8.0, + stage: "In Progress", + status: "In Progress", + user: "Developer", + assignee: "Developer", + date: "2026-10-16" + }; + + testChart.openProject(sampleProject); + testChart.openTask(sampleTask); + + compare(testChart.currentView, "task"); + compare(testChart.selectedTask.id, "task_1"); + compare(taskOpenedSpy.count, 1); + compare(taskOpenedSpy.signalArguments[0][0], "proj_1"); + compare(taskOpenedSpy.signalArguments[0][1], "task_1"); + } + + function test_goBackNavigationHierarchy() { + var sampleProject = { id: "proj_1", name: "Web App", totalHours: 20.0 }; + var sampleTask = { + id: "task_1", + name: "API", + totalHours: 8.0, + stage: "In Progress", + status: "In Progress", + user: "Developer", + assignee: "Developer", + date: "2026-10-16" + }; + + testChart.openProject(sampleProject); + testChart.openTask(sampleTask); + compare(testChart.currentView, "task"); + + // First goBack returns from task to project view + testChart.goBack(); + compare(testChart.currentView, "project"); + compare(backNavigatedSpy.count, 1); + + // Second goBack returns from project to projects view + testChart.goBack(); + compare(testChart.currentView, "projects"); + compare(testChart.selectedProject, null); + compare(backNavigatedSpy.count, 2); + } + + function test_activeAccentColor() { + compare(testChart.activeAccent, "#e95420"); + + testChart.openProject({ id: "p1", name: "Custom", totalHours: 5, colour: "#2ecc71" }); + compare(testChart.activeAccent, "#2ecc71"); + } + } +} diff --git a/tests/qml/tst_timerecorderwidget.qml b/tests/qml/tst_timerecorderwidget.qml new file mode 100644 index 00000000..292a3a0a --- /dev/null +++ b/tests/qml/tst_timerecorderwidget.qml @@ -0,0 +1,116 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/timesheets/components" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + Item { + id: globalTimerWidget + signal timerStopped() + signal timerStarted() + signal timerPaused() + signal timerResumed() + } + + TimeRecorderWidget { + id: testWidget + width: testRoot.width + } + + SignalSpy { + id: invalidtimesheetSpy + target: testWidget + signalName: "invalidtimesheet" + } + + SignalSpy { + id: requestAutoSaveSpy + target: testWidget + signalName: "requestAutoSave" + } + + SignalSpy { + id: beforeStopSpy + target: testWidget + signalName: "beforeStop" + } + + SignalSpy { + id: stoppedSpy + target: testWidget + signalName: "stopped" + } + + TestCase { + name: "TimeRecorderWidgetTests" + when: windowShown + + function init() { + testWidget.isRecording = false; + testWidget.autoMode = false; + testWidget.elapsedTime = "01:00"; + testWidget.timesheetId = 0; + + invalidtimesheetSpy.clear(); + requestAutoSaveSpy.clear(); + beforeStopSpy.clear(); + stoppedSpy.clear(); + } + + function cleanup() { + invalidtimesheetSpy.clear(); + requestAutoSaveSpy.clear(); + beforeStopSpy.clear(); + stoppedSpy.clear(); + } + + function test_initialProperties() { + compare(testWidget.isRecording, false); + compare(testWidget.autoMode, false); + compare(testWidget.elapsedTime, "01:00"); + compare(testWidget.timesheetId, 0); + compare(invalidtimesheetSpy.count, 0); + } + + function test_tryStartTimerInvalidTimesheet() { + testWidget.timesheetId = 0; + testWidget.tryStartTimer(); + compare(invalidtimesheetSpy.count, 1); + + testWidget.timesheetId = -5; + testWidget.tryStartTimer(); + compare(invalidtimesheetSpy.count, 2); + } + + function test_modeToggle() { + testWidget.autoMode = true; + compare(testWidget.autoMode, true); + + testWidget.autoMode = false; + compare(testWidget.autoMode, false); + } + + function test_elapsedTimeAssignment() { + testWidget.elapsedTime = "03:45"; + compare(testWidget.elapsedTime, "03:45"); + } + + function test_signalsEmission() { + compare(beforeStopSpy.count, 0); + testWidget.beforeStop(); + compare(beforeStopSpy.count, 1); + + compare(stoppedSpy.count, 0); + testWidget.stopped(); + compare(stoppedSpy.count, 1); + + compare(requestAutoSaveSpy.count, 0); + testWidget.requestAutoSave(); + compare(requestAutoSaveSpy.count, 1); + } + } +} diff --git a/tests/qml/tst_timesheetdescriptionpopup.qml b/tests/qml/tst_timesheetdescriptionpopup.qml new file mode 100644 index 00000000..0dea6a4e --- /dev/null +++ b/tests/qml/tst_timesheetdescriptionpopup.qml @@ -0,0 +1,107 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/timesheets/components" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + TimeSheetDescriptionPopup { + id: testPopup + } + + SignalSpy { + id: savedSpy + target: testPopup + signalName: "saved" + } + + SignalSpy { + id: finalizedSpy + target: testPopup + signalName: "finalized" + } + + SignalSpy { + id: cancelledSpy + target: testPopup + signalName: "cancelled" + } + + TestCase { + name: "TimeSheetDescriptionPopupTests" + when: windowShown + + function init() { + testPopup.timesheetId = 0; + testPopup.timesheetName = ""; + testPopup.elapsedTime = ""; + testPopup.hasTask = false; + testPopup.isLocalAccount = false; + + savedSpy.clear(); + finalizedSpy.clear(); + cancelledSpy.clear(); + } + + function cleanup() { + savedSpy.clear(); + finalizedSpy.clear(); + cancelledSpy.clear(); + } + + function test_initialProperties() { + compare(testPopup.timesheetId, 0); + compare(testPopup.timesheetName, ""); + compare(testPopup.elapsedTime, ""); + compare(testPopup.hasTask, false); + compare(testPopup.isLocalAccount, false); + } + + function test_updateTimesheetDescriptionInvalidId() { + testPopup.timesheetId = 0; + var res = testPopup.updateTimesheetDescription("Work done", "draft"); + compare(res.success, false); + compare(res.error, "Invalid timesheet ID"); + + testPopup.timesheetId = -1; + var resNegative = testPopup.updateTimesheetDescription("Work done", "draft"); + compare(resNegative.success, false); + compare(resNegative.error, "Invalid timesheet ID"); + } + + function test_propertyAssignments() { + testPopup.timesheetId = 123; + testPopup.timesheetName = "Initial meeting"; + testPopup.elapsedTime = "02:15"; + testPopup.hasTask = true; + testPopup.isLocalAccount = true; + + compare(testPopup.timesheetId, 123); + compare(testPopup.timesheetName, "Initial meeting"); + compare(testPopup.elapsedTime, "02:15"); + compare(testPopup.hasTask, true); + compare(testPopup.isLocalAccount, true); + } + + function test_signalsEmission() { + compare(savedSpy.count, 0); + testPopup.saved("Draft notes", "draft"); + compare(savedSpy.count, 1); + compare(savedSpy.signalArguments[0][0], "Draft notes"); + compare(savedSpy.signalArguments[0][1], "draft"); + + compare(finalizedSpy.count, 0); + testPopup.finalized(true, "Finalized successfully"); + compare(finalizedSpy.count, 1); + compare(finalizedSpy.signalArguments[0][0], true); + compare(finalizedSpy.signalArguments[0][1], "Finalized successfully"); + + compare(cancelledSpy.count, 0); + testPopup.cancelled(); + compare(cancelledSpy.count, 1); + } + } +} diff --git a/tests/qml/tst_timesheetdetailscard.qml b/tests/qml/tst_timesheetdetailscard.qml new file mode 100644 index 00000000..b36a7197 --- /dev/null +++ b/tests/qml/tst_timesheetdetailscard.qml @@ -0,0 +1,147 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/features/timesheets/components" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + Item { + id: globalTimerWidget + signal timerStopped() + signal timerStarted() + signal timerPaused() + signal timerResumed() + } + + TimeSheetDetailsCard { + id: testCard + width: testRoot.width + } + + SignalSpy { + id: editRequestedSpy + target: testCard + signalName: "editRequested" + } + + SignalSpy { + id: viewRequestedSpy + target: testCard + signalName: "viewRequested" + } + + SignalSpy { + id: deleteRequestedSpy + target: testCard + signalName: "deleteRequested" + } + + SignalSpy { + id: refreshSpy + target: testCard + signalName: "refresh" + } + + TestCase { + name: "TimeSheetDetailsCardTests" + when: windowShown + + function init() { + testCard.name = ""; + testCard.project = ""; + testCard.task = ""; + testCard.user = ""; + testCard.date = ""; + testCard.instance = ""; + testCard.spentHours = "0"; + testCard.quadrant = "Do"; + testCard.recordId = -1; + testCard.accountId = -1; + testCard.status = ""; + testCard.timer_on = false; + testCard.timer_paused = false; + testCard.hasDraft = false; + + editRequestedSpy.clear(); + viewRequestedSpy.clear(); + deleteRequestedSpy.clear(); + refreshSpy.clear(); + } + + function cleanup() { + editRequestedSpy.clear(); + viewRequestedSpy.clear(); + deleteRequestedSpy.clear(); + refreshSpy.clear(); + } + + function test_initialProperties() { + compare(testCard.name, ""); + compare(testCard.project, ""); + compare(testCard.task, ""); + compare(testCard.user, ""); + compare(testCard.spentHours, "0"); + compare(testCard.quadrant, "Do"); + compare(testCard.recordId, -1); + compare(testCard.accountId, -1); + compare(testCard.timer_on, false); + compare(testCard.timer_paused, false); + compare(testCard.hasDraft, false); + compare(testCard.height, units.gu(12)); + } + + function test_propertyAssignments() { + testCard.name = "Refactor components"; + testCard.project = "TMM App"; + testCard.task = "Sprint Planning"; + testCard.user = "Developer"; + testCard.date = "2026-10-16"; + testCard.instance = "Production"; + testCard.spentHours = "3:30"; + testCard.quadrant = "Schedule"; + testCard.recordId = 456; + testCard.accountId = 2; + testCard.status = "draft"; + testCard.hasDraft = true; + + compare(testCard.name, "Refactor components"); + compare(testCard.project, "TMM App"); + compare(testCard.task, "Sprint Planning"); + compare(testCard.user, "Developer"); + compare(testCard.date, "2026-10-16"); + compare(testCard.instance, "Production"); + compare(testCard.spentHours, "3:30"); + compare(testCard.quadrant, "Schedule"); + compare(testCard.recordId, 456); + compare(testCard.accountId, 2); + compare(testCard.status, "draft"); + compare(testCard.hasDraft, true); + } + + function test_signalsEmission() { + testCard.recordId = 789; + + compare(editRequestedSpy.count, 0); + testCard.editRequested(789); + compare(editRequestedSpy.count, 1); + compare(editRequestedSpy.signalArguments[0][0], 789); + + compare(viewRequestedSpy.count, 0); + testCard.viewRequested(789); + compare(viewRequestedSpy.count, 1); + compare(viewRequestedSpy.signalArguments[0][0], 789); + + compare(deleteRequestedSpy.count, 0); + testCard.deleteRequested(789); + compare(deleteRequestedSpy.count, 1); + compare(deleteRequestedSpy.signalArguments[0][0], 789); + + compare(refreshSpy.count, 0); + testCard.refresh(); + compare(refreshSpy.count, 1); + } + } +}