diff --git a/qml/components/pickers/DateRangeHeaderFilter.qml b/qml/components/pickers/DateRangeHeaderFilter.qml index 5c874f64..51006027 100644 --- a/qml/components/pickers/DateRangeHeaderFilter.qml +++ b/qml/components/pickers/DateRangeHeaderFilter.qml @@ -46,6 +46,7 @@ Item { property bool isFiltered: presetId !== -1 property bool suppressSignal: false property bool showClearButton: true + property alias presetSelector: inlinePresetSelector signal dateRangeChanged(int presetId, string startDate, string endDate, string presetLabel) diff --git a/qml/components/pickers/DateRangeSelector.qml b/qml/components/pickers/DateRangeSelector.qml index ab210212..e2fa2ac6 100644 --- a/qml/components/pickers/DateRangeSelector.qml +++ b/qml/components/pickers/DateRangeSelector.qml @@ -11,6 +11,7 @@ Item { implicitHeight: layout.implicitHeight height: layout.implicitHeight property alias labelText: presetSelector.labelText + property alias presetSelector: presetSelector property date startDate: new Date() property date endDate: new Date() signal rangeChanged(date start, date end) diff --git a/qml/components/pickers/DaySelector.qml b/qml/components/pickers/DaySelector.qml index 7e76a63d..ac6899ff 100644 --- a/qml/components/pickers/DaySelector.qml +++ b/qml/components/pickers/DaySelector.qml @@ -15,6 +15,7 @@ Item { property date customDate: selectedDate property bool showTomorrow: false property bool readOnly: false + property alias dayCombo: dayCombo signal dateChanged(date selectedDate) function formattedDate() { diff --git a/tests/qml/README.md b/tests/qml/README.md index d9e70ea0..874722f1 100644 --- a/tests/qml/README.md +++ b/tests/qml/README.md @@ -43,25 +43,36 @@ tests/ │ │ ├── tst_squirebridge.qml # Test suite for Squire.js QtWebEngine messaging bridge │ │ └── 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_colorpicker.qml # Test suite for ColorPicker palette and custom color │ ├── tst_contentpickerdialog.qml # Test suite for ContentPickerDialog single/multi selection -│ ├── tst_assigneefiltermenu.qml # Test suite for AssigneeFilterMenu selector component │ ├── tst_customdatepicker.qml # Test suite for CustomDatePicker picker component +│ ├── tst_daterangeheaderfilter.qml # Test suite for DateRangeHeaderFilter filter toggle and active range text +│ ├── tst_daterangeselector.qml # Test suite for DateRangeSelector date bounds and range pick +│ ├── tst_dayselector.qml # Test suite for DaySelector day selection signals +│ ├── tst_filterableheadercontents.qml # Test suite for FilterableHeaderContents filter state integration │ ├── tst_globaltimerwidget.qml # Test suite for GlobalTimerWidget system component │ ├── tst_infobar.qml # Test suite for InfoBar feedback component +│ ├── tst_inlineoptionselector.qml # Test suite for InlineOptionSelector radio-style option selection │ ├── 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_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_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_stagefiltermenu.qml # Test suite for StageFilterMenu stage filter menu toggling +│ ├── tst_statusselector.qml # Test suite for StatusSelector status changes and badge color │ ├── 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 │ ├── tst_tsprogressbar.qml # Test suite for TSProgressbar progress indicator │ ├── tst_tssearchbar.qml # Test suite for TSSearchBar search input component -│ └── tst_tsswitch.qml # Test suite for TSSwitch toggle switch component +│ ├── tst_tsswitch.qml # Test suite for TSSwitch toggle switch component +│ └── tst_workitemselector.qml # Test suite for WorkItemSelector project and task selection logic ``` Clickable automatically discovers all files matching `tst_*.qml` under `tests/qml/` as configured in `clickable.yaml`. diff --git a/tests/qml/tst_assigneefiltermenu.qml b/tests/qml/tst_assigneefiltermenu.qml index 946b3484..5a415f98 100644 --- a/tests/qml/tst_assigneefiltermenu.qml +++ b/tests/qml/tst_assigneefiltermenu.qml @@ -69,6 +69,43 @@ Item { compare(testMenu.selectedAssigneeIds.length, 0); } + function test_expandedToggle() { + testMenu.expanded = true; + compare(testMenu.expanded, true); + + testMenu.expanded = false; + compare(testMenu.expanded, false); + } + + function test_createSelectionAndIsAssigneeSelected() { + var sel = testMenu.createSelection(101, 1); + compare(sel.user_id, 101); + compare(sel.account_id, 1); + + testMenu.selectedAssigneeIds = [sel]; + verify(testMenu.isAssigneeSelected(101, 1)); + verify(!testMenu.isAssigneeSelected(101, 2)); + verify(!testMenu.isAssigneeSelected(102, 1)); + } + + function test_displayAssigneeCacheAndCounts() { + testMenu.assigneeModel = [ + { id: 101, name: "Alice", account_id: 1, email: "alice@example.com" }, + { id: 102, name: "Bob", account_id: 1, email: "bob@example.com" }, + { id: 103, name: "Charlie", account_id: 1, email: "charlie@example.com" } + ]; + compare(testMenu.getDisplayAssigneeCount(), 3); + compare(testMenu.getSelectedDisplayAssigneeCount(), 0); + + testMenu.addSelectionIfMissing(testMenu.createSelection(101, 1)); + testMenu.recomputeDisplayAssigneeCache(); + compare(testMenu.getSelectedDisplayAssigneeCount(), 1); + + testMenu.addSelectionIfMissing(testMenu.createSelection(102, 1)); + testMenu.recomputeDisplayAssigneeCache(); + compare(testMenu.getSelectedDisplayAssigneeCount(), 2); + } + function test_showAllUsersOption_initialState() { testMenu.showAllUsersOption = true; testMenu.selectedAssigneeIds = []; diff --git a/tests/qml/tst_daterangeheaderfilter.qml b/tests/qml/tst_daterangeheaderfilter.qml new file mode 100644 index 00000000..7b07e6af --- /dev/null +++ b/tests/qml/tst_daterangeheaderfilter.qml @@ -0,0 +1,121 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/pickers" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + DateRangeHeaderFilter { + id: testHeaderFilter + width: testRoot.width + } + + SignalSpy { + id: dateRangeChangedSpy + target: testHeaderFilter + signalName: "dateRangeChanged" + } + + TestCase { + name: "DateRangeHeaderFilterTests" + when: windowShown + + function init() { + testHeaderFilter.clearFilter(); + testHeaderFilter.showClearButton = true; + dateRangeChangedSpy.clear(); + } + + function cleanup() { + testHeaderFilter.clearFilter(); + dateRangeChangedSpy.clear(); + } + + function test_initialProperties() { + compare(testHeaderFilter.presetId, -1); + compare(testHeaderFilter.startDate, ""); + compare(testHeaderFilter.endDate, ""); + compare(testHeaderFilter.isFiltered, false); + compare(testHeaderFilter.presetLabel, "No Filter (All Time)"); + compare(testHeaderFilter.showClearButton, true); + compare(dateRangeChangedSpy.count, 0); + } + + function test_calculateDatesToday() { + var dates = testHeaderFilter.calculateDates(0); // Today + var todayStr = Qt.formatDate(new Date(), "yyyy-MM-dd"); + compare(dates.start, todayStr); + compare(dates.end, todayStr); + } + + function test_calculateDatesThisMonth() { + var dates = testHeaderFilter.calculateDates(2); // This Month + var now = new Date(); + var expectedMonthPrefix = Qt.formatDate(now, "yyyy-MM"); + verify(dates.start.indexOf(expectedMonthPrefix + "-01") === 0); + verify(dates.end.indexOf(expectedMonthPrefix) === 0); + } + + function test_calculateDatesThisYear() { + var dates = testHeaderFilter.calculateDates(4); // This Year + var y = new Date().getFullYear(); + compare(dates.start, y + "-01-01"); + compare(dates.end, y + "-12-31"); + } + + function test_setFilterStateUpdatesPropertiesAndEmitsSignal() { + testHeaderFilter.setFilterState(1, "2026-10-12", "2026-10-18", "This Week"); + + compare(testHeaderFilter.presetId, 1); + compare(testHeaderFilter.startDate, "2026-10-12"); + compare(testHeaderFilter.endDate, "2026-10-18"); + compare(testHeaderFilter.presetLabel, "This Week"); + compare(testHeaderFilter.isFiltered, true); + + compare(dateRangeChangedSpy.count, 1); + var args = dateRangeChangedSpy.signalArguments[0]; + compare(args[0], 1); + compare(args[1], "2026-10-12"); + compare(args[2], "2026-10-18"); + compare(args[3], "This Week"); + } + + function test_applyPresetTodayTogglesFilterAndRangeText() { + testHeaderFilter.applyPreset(0, "Today"); + + compare(testHeaderFilter.isFiltered, true); + compare(testHeaderFilter.presetId, 0); + compare(testHeaderFilter.presetLabel, "Today"); + verify(testHeaderFilter.startDate !== ""); + verify(testHeaderFilter.endDate !== ""); + compare(dateRangeChangedSpy.count, 1); + } + + function test_clearFilterResetsActiveFilterAndRangeText() { + testHeaderFilter.applyPreset(0, "Today"); + compare(testHeaderFilter.isFiltered, true); + dateRangeChangedSpy.clear(); + + testHeaderFilter.clearFilter(); + + compare(testHeaderFilter.isFiltered, false); + compare(testHeaderFilter.presetId, -1); + compare(testHeaderFilter.startDate, ""); + compare(testHeaderFilter.endDate, ""); + compare(testHeaderFilter.presetLabel, "No Filter (All Time)"); + compare(dateRangeChangedSpy.count, 1); + compare(dateRangeChangedSpy.signalArguments[0][0], -1); + } + + function test_showClearButtonToggling() { + testHeaderFilter.showClearButton = false; + compare(testHeaderFilter.showClearButton, false); + + testHeaderFilter.showClearButton = true; + compare(testHeaderFilter.showClearButton, true); + } + } +} diff --git a/tests/qml/tst_daterangeselector.qml b/tests/qml/tst_daterangeselector.qml new file mode 100644 index 00000000..4d1378ec --- /dev/null +++ b/tests/qml/tst_daterangeselector.qml @@ -0,0 +1,125 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/pickers" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + DateRangeSelector { + id: testDateRangeSelector + width: testRoot.width + } + + SignalSpy { + id: rangeChangedSpy + target: testDateRangeSelector + signalName: "rangeChanged" + } + + TestCase { + name: "DateRangeSelectorTests" + when: windowShown + + function init() { + testDateRangeSelector.readOnly = false; + rangeChangedSpy.clear(); + } + + function cleanup() { + testDateRangeSelector.readOnly = false; + rangeChangedSpy.clear(); + } + + function test_initialProperties() { + verify(testDateRangeSelector.startDate instanceof Date); + verify(testDateRangeSelector.endDate instanceof Date); + compare(testDateRangeSelector.readOnly, false); + compare(testDateRangeSelector.isStartDateValid, true); + compare(testDateRangeSelector.isEndDateValid, true); + verify(testDateRangeSelector.presetSelector !== null); + } + + function test_formattedDateStrings() { + var s = new Date(2026, 8, 15); // 2026-09-15 + var e = new Date(2026, 8, 20); // 2026-09-20 + testDateRangeSelector.setDateRange(s, e); + + compare(testDateRangeSelector.formattedStartDate(), "2026-09-15"); + compare(testDateRangeSelector.formattedEndDate(), "2026-09-20"); + } + + function test_setDateRangeWithDateObjects() { + var start = new Date(2026, 5, 1); + var end = new Date(2026, 5, 30); + rangeChangedSpy.clear(); + + testDateRangeSelector.setDateRange(start, end); + + compare(rangeChangedSpy.count, 1); + compare(testDateRangeSelector.isStartDateValid, true); + compare(testDateRangeSelector.isEndDateValid, true); + compare(testDateRangeSelector.startDate.getFullYear(), 2026); + compare(testDateRangeSelector.startDate.getMonth(), 5); + compare(testDateRangeSelector.startDate.getDate(), 1); + compare(testDateRangeSelector.endDate.getDate(), 30); + } + + function test_setDateRangeWithStringInputs() { + rangeChangedSpy.clear(); + testDateRangeSelector.setDateRange("2026-11-05", "2026-11-25"); + + compare(rangeChangedSpy.count, 1); + compare(testDateRangeSelector.isStartDateValid, true); + compare(testDateRangeSelector.isEndDateValid, true); + compare(testDateRangeSelector.formattedStartDate(), "2026-11-05"); + compare(testDateRangeSelector.formattedEndDate(), "2026-11-25"); + } + + function test_setDateRangeInvalidBoundsHandling() { + testDateRangeSelector.setDateRange("not-a-date", null); + + compare(testDateRangeSelector.isStartDateValid, false); + compare(testDateRangeSelector.isEndDateValid, false); + } + + function test_updateDatesPresetToday() { + verify(testDateRangeSelector.presetSelector !== null, "presetSelector alias should be accessible"); + + testDateRangeSelector.presetSelector.selectedId = 0; // Today + rangeChangedSpy.clear(); + testDateRangeSelector.updateDates(); + + compare(rangeChangedSpy.count, 1); + compare(testDateRangeSelector.isStartDateValid, true); + compare(testDateRangeSelector.isEndDateValid, true); + var today = new Date(); + compare(testDateRangeSelector.startDate.getDate(), today.getDate()); + compare(testDateRangeSelector.endDate.getDate(), today.getDate()); + } + + function test_updateDatesPresetThisMonth() { + verify(testDateRangeSelector.presetSelector !== null, "presetSelector alias should be accessible"); + + testDateRangeSelector.presetSelector.selectedId = 3; // This Month + rangeChangedSpy.clear(); + testDateRangeSelector.updateDates(); + + compare(rangeChangedSpy.count, 1); + var today = new Date(); + compare(testDateRangeSelector.startDate.getMonth(), today.getMonth()); + compare(testDateRangeSelector.endDate.getMonth(), today.getMonth()); + verify(testDateRangeSelector.endDate.getDate() >= 26); + } + + function test_readOnlyStateToggling() { + testDateRangeSelector.readOnly = true; + compare(testDateRangeSelector.readOnly, true); + + testDateRangeSelector.readOnly = false; + compare(testDateRangeSelector.readOnly, false); + } + } +} diff --git a/tests/qml/tst_dayselector.qml b/tests/qml/tst_dayselector.qml new file mode 100644 index 00000000..54956e03 --- /dev/null +++ b/tests/qml/tst_dayselector.qml @@ -0,0 +1,132 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/pickers" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + DaySelector { + id: testDaySelector + width: testRoot.width + } + + SignalSpy { + id: dateChangedSpy + target: testDaySelector + signalName: "dateChanged" + } + + TestCase { + name: "DaySelectorTests" + when: windowShown + + function init() { + testDaySelector.readOnly = false; + testDaySelector.showTomorrow = false; + dateChangedSpy.clear(); + } + + function cleanup() { + testDaySelector.readOnly = false; + testDaySelector.showTomorrow = false; + dateChangedSpy.clear(); + } + + function test_initialProperties() { + compare(testDaySelector.labelText, "Date"); + verify(testDaySelector.selectedDate instanceof Date); + compare(testDaySelector.showTomorrow, false); + compare(testDaySelector.readOnly, false); + compare(dateChangedSpy.count, 0); + } + + function test_formattedDate() { + var target = new Date(2026, 6, 25); + testDaySelector.setSelectedDate(target); + compare(testDaySelector.formattedDate(), "2026-07-25"); + } + + function test_relativeLabelYesterdayAndTomorrow() { + testDaySelector.showTomorrow = false; + compare(testDaySelector.getRelativeLabel(), "Yesterday"); + + testDaySelector.showTomorrow = true; + compare(testDaySelector.getRelativeLabel(), "Tomorrow"); + } + + function test_relativeDateCalculation() { + var base = new Date(2026, 5, 10); // June 10 + + testDaySelector.showTomorrow = false; + var yesterday = testDaySelector.getRelativeDate(base); + compare(yesterday.getDate(), 9); + + testDaySelector.showTomorrow = true; + var tomorrow = testDaySelector.getRelativeDate(base); + compare(tomorrow.getDate(), 11); + } + + function test_isSameDateHelper() { + var d1 = new Date(2026, 3, 15, 10, 30); + var d2 = new Date(2026, 3, 15, 18, 45); + var d3 = new Date(2026, 3, 16, 10, 30); + + verify(testDaySelector.isSameDate(d1, d2)); + verify(!testDaySelector.isSameDate(d1, d3)); + verify(!testDaySelector.isSameDate(null, d1)); + verify(!testDaySelector.isSameDate(d1, "invalid")); + } + + function test_setSelectedDateWithDateObjectEmitsSignal() { + var target = new Date(2026, 9, 31); + dateChangedSpy.clear(); + + testDaySelector.setSelectedDate(target); + + compare(dateChangedSpy.count, 1); + var emittedDate = dateChangedSpy.signalArguments[0][0]; + verify(testDaySelector.isSameDate(emittedDate, target)); + } + + function test_setSelectedDateWithStringInputEmitsSignal() { + dateChangedSpy.clear(); + testDaySelector.setSelectedDate("2026-12-25"); + + compare(dateChangedSpy.count, 1); + compare(testDaySelector.formattedDate(), "2026-12-25"); + } + + function test_setSelectedDateInvalidInputIgnored() { + dateChangedSpy.clear(); + testDaySelector.setSelectedDate("not-a-valid-date"); + + compare(dateChangedSpy.count, 0); + } + + function test_updateDateRelativeSelection() { + verify(testDaySelector.dayCombo !== null, "dayCombo alias should be accessible"); + + testDaySelector.showTomorrow = false; + testDaySelector.dayCombo.selectedId = 1; // Relative (Yesterday) + dateChangedSpy.clear(); + + testDaySelector.updateDate(); + + compare(dateChangedSpy.count, 1); + var expected = new Date(); + expected.setDate(expected.getDate() - 1); + verify(testDaySelector.isSameDate(testDaySelector.selectedDate, expected)); + } + + function test_readOnlyToggle() { + testDaySelector.readOnly = true; + compare(testDaySelector.readOnly, true); + + testDaySelector.readOnly = false; + compare(testDaySelector.readOnly, false); + } + } +} diff --git a/tests/qml/tst_filterableheadercontents.qml b/tests/qml/tst_filterableheadercontents.qml new file mode 100644 index 00000000..a9a832a6 --- /dev/null +++ b/tests/qml/tst_filterableheadercontents.qml @@ -0,0 +1,141 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/pickers" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + Item { + id: headerContainer + width: testRoot.width + height: units.gu(7) + + FilterableHeaderContents { + id: testFilterableHeader + } + } + + SignalSpy { + id: dateRangeChangedSpy + target: testFilterableHeader + signalName: "dateRangeChanged" + } + + TestCase { + name: "FilterableHeaderContentsTests" + when: windowShown + + function init() { + testFilterableHeader.showDateFilter = false; + testFilterableHeader.title = ""; + testFilterableHeader.showSubtitle = true; + testFilterableHeader.userLabel = ""; + dateRangeChangedSpy.clear(); + } + + function cleanup() { + testFilterableHeader.showDateFilter = false; + dateRangeChangedSpy.clear(); + } + + function test_initialProperties() { + compare(testFilterableHeader.showDateFilter, false); + compare(testFilterableHeader.title, ""); + compare(testFilterableHeader.showSubtitle, true); + compare(testFilterableHeader.userLabel, ""); + verify(testFilterableHeader.dateFilter !== null); + compare(dateRangeChangedSpy.count, 0); + } + + function findItemWithText(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 = findItemWithText(root.children[i], text); + if (found) return found; + } + return null; + } + + function findMouseAreaWithCloseIcon(root) { + if (!root) return null; + for (var i = 0; i < (root.children ? root.children.length : 0); i++) { + var ch = root.children[i]; + if (ch.name === "close") { + for (var j = 0; j < root.children.length; j++) { + if (root.children[j].cursorShape !== undefined || root.children[j].toString().indexOf("MouseArea") !== -1) { + return root.children[j]; + } + } + } + var found = findMouseAreaWithCloseIcon(ch); + if (found) return found; + } + return null; + } + + function test_titleAndSubtitleIntegration() { + testFilterableHeader.title = "Project Tasks"; + testFilterableHeader.userLabel = "Bob Smith"; + + compare(testFilterableHeader.title, "Project Tasks"); + compare(testFilterableHeader.userLabel, "Bob Smith"); + + var titleLabel = findItemWithText(testFilterableHeader, "Project Tasks"); + verify(titleLabel !== null, "Title label should be found"); + compare(titleLabel.text, "Project Tasks"); + + var subtitleLabel = findItemWithText(testFilterableHeader, "Bob Smith"); + verify(subtitleLabel !== null, "Subtitle label should be found"); + verify(subtitleLabel.text.indexOf(" • ") !== -1); + } + + function test_subtitleWithoutUserLabel() { + testFilterableHeader.userLabel = ""; + + var subtitleLabel = findItemWithText(testFilterableHeader, testFilterableHeader.dateFilter.presetLabel); + verify(subtitleLabel !== null, "Subtitle label should match presetLabel"); + compare(subtitleLabel.text, testFilterableHeader.dateFilter.presetLabel); + } + + function test_showDateFilterToggleSwitchesViews() { + // Initially normal is visible, date filter is hidden + compare(testFilterableHeader.showDateFilter, false); + + // Toggle showDateFilter = true + testFilterableHeader.showDateFilter = true; + compare(testFilterableHeader.showDateFilter, true); + + // Toggle back + testFilterableHeader.showDateFilter = false; + compare(testFilterableHeader.showDateFilter, false); + } + + function test_closeFilterButtonClickResetsState() { + testFilterableHeader.showDateFilter = true; + compare(testFilterableHeader.showDateFilter, true); + + var closeMouse = findMouseAreaWithCloseIcon(testFilterableHeader); + verify(closeMouse !== null, "Close button MouseArea should be found"); + + mouseClick(closeMouse); + + compare(testFilterableHeader.showDateFilter, false); + } + + function test_dateFilterRangeChangedSignalIntegration() { + testFilterableHeader.showDateFilter = true; + dateRangeChangedSpy.clear(); + + // Simulate DateRangeHeaderFilter triggering range change + testFilterableHeader.dateFilter.applyPreset(0, "Today"); + + compare(dateRangeChangedSpy.count, 1); + // Once filter is applied, showDateFilter should automatically reset to false + compare(testFilterableHeader.showDateFilter, false); + } + } +} diff --git a/tests/qml/tst_inlineoptionselector.qml b/tests/qml/tst_inlineoptionselector.qml new file mode 100644 index 00000000..d7111426 --- /dev/null +++ b/tests/qml/tst_inlineoptionselector.qml @@ -0,0 +1,137 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/selectors" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + InlineOptionSelector { + id: testInlineSelector + width: testRoot.width + labelText: "Priority" + selectorType: "priority_level" + } + + SignalSpy { + id: selectionMadeSpy + target: testInlineSelector + signalName: "selectionMade" + } + + TestCase { + name: "InlineOptionSelectorTests" + when: windowShown + + readonly property var sampleOptions: [ + { id: 1, name: "Low" }, + { id: 2, name: "Medium" }, + { id: 3, name: "High" }, + { id: 4, name: "Urgent" } + ] + + function init() { + testInlineSelector.enabledState = true; + testInlineSelector.readOnly = false; + testInlineSelector.setData(sampleOptions); + testInlineSelector.clear(); + testInlineSelector.collapse(); + selectionMadeSpy.clear(); + } + + function cleanup() { + selectionMadeSpy.clear(); + } + + function test_initialProperties() { + compare(testInlineSelector.labelText, "Priority"); + compare(testInlineSelector.selectorType, "priority_level"); + compare(testInlineSelector.selectedId, -1); + compare(testInlineSelector.selectedName, ""); + compare(testInlineSelector.collapsed, true); + compare(testInlineSelector.enabledState, true); + compare(testInlineSelector.readOnly, false); + compare(selectionMadeSpy.count, 0); + } + + function test_expandAndCollapseFunctions() { + testInlineSelector.expand(); + compare(testInlineSelector.collapsed, false); + + testInlineSelector.collapse(); + compare(testInlineSelector.collapsed, true); + } + + function test_radioStyleOptionSelection() { + // Selecting Medium (exclusive radio-style selection) + var resMedium = testInlineSelector.applyDeferredSelection(2, true); + verify(resMedium); + compare(testInlineSelector.selectedId, 2); + compare(testInlineSelector.selectedName, "Medium"); + compare(selectionMadeSpy.count, 1); + compare(selectionMadeSpy.signalArguments[0][0], 2); + compare(selectionMadeSpy.signalArguments[0][1], "Medium"); + + // Switching selection to High (overwrites previous selection) + var resHigh = testInlineSelector.applyDeferredSelection(3, true); + verify(resHigh); + compare(testInlineSelector.selectedId, 3); + compare(testInlineSelector.selectedName, "High"); + compare(selectionMadeSpy.count, 2); + compare(selectionMadeSpy.signalArguments[1][0], 3); + compare(selectionMadeSpy.signalArguments[1][1], "High"); + } + + function test_applyDeferredSelectionWithoutSignal() { + var res = testInlineSelector.applyDeferredSelection(1, false); + verify(res); + compare(testInlineSelector.selectedId, 1); + compare(testInlineSelector.selectedName, "Low"); + compare(selectionMadeSpy.count, 0); + } + + function test_applyDeferredSelectionNonExistentId() { + var res = testInlineSelector.applyDeferredSelection(999, false); + verify(!res); + compare(testInlineSelector.selectedId, -1); + } + + function test_clearResetsSelection() { + testInlineSelector.applyDeferredSelection(4, false); + compare(testInlineSelector.selectedId, 4); + compare(testInlineSelector.selectedName, "Urgent"); + + testInlineSelector.clear(); + compare(testInlineSelector.selectedId, -1); + compare(testInlineSelector.selectedName, ""); + } + + function test_setDataUpdatesOptionsAndNameResolution() { + var newOptions = [ + { id: 10, name: "Tier 1" }, + { id: 20, name: "Tier 2" } + ]; + testInlineSelector.setData(newOptions); + + testInlineSelector.applyDeferredSelection(20, true); + compare(testInlineSelector.selectedId, 20); + compare(testInlineSelector.selectedName, "Tier 2"); + compare(selectionMadeSpy.count, 1); + } + + function test_setEnabledAndReadOnly() { + testInlineSelector.readOnly = false; + testInlineSelector.setEnabled(false); + compare(testInlineSelector.enabledState, false); + + testInlineSelector.setEnabled(true); + compare(testInlineSelector.enabledState, true); + + testInlineSelector.readOnly = true; + testInlineSelector.setEnabled(true); + compare(testInlineSelector.enabledState, false); + } + } +} diff --git a/tests/qml/tst_multiassigneeselector.qml b/tests/qml/tst_multiassigneeselector.qml new file mode 100644 index 00000000..725223f2 --- /dev/null +++ b/tests/qml/tst_multiassigneeselector.qml @@ -0,0 +1,140 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/selectors" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + MultiAssigneeSelector { + id: testMultiAssignee + width: testRoot.width + } + + SignalSpy { + id: assigneesChangedSpy + target: testMultiAssignee + signalName: "assigneesChanged" + } + + TestCase { + name: "MultiAssigneeSelectorTests" + when: windowShown + + function init() { + testMultiAssignee.readOnly = false; + testMultiAssignee.enabledState = true; + testMultiAssignee.setSelectedAssignees([]); + assigneesChangedSpy.clear(); + } + + function cleanup() { + testMultiAssignee.readOnly = false; + testMultiAssignee.setSelectedAssignees([]); + assigneesChangedSpy.clear(); + } + + function test_initialProperties() { + compare(testMultiAssignee.readOnly, false); + compare(testMultiAssignee.enabledState, true); + compare(testMultiAssignee.selectedAssignees.length, 0); + var ids = testMultiAssignee.getSelectedAssigneeIds(); + compare(ids.length, 0); + compare(assigneesChangedSpy.count, 0); + } + + function test_setSelectedAssigneesAndIdsArrayOutput() { + var initialList = [ + { id: 101, name: "Alice Developer" }, + { id: 102, name: "Bob Reviewer" } + ]; + + testMultiAssignee.setSelectedAssignees(initialList); + + compare(assigneesChangedSpy.count, 1); + var assignees = testMultiAssignee.getSelectedAssignees(); + compare(assignees.length, 2); + + var ids = testMultiAssignee.getSelectedAssigneeIds(); + compare(ids.length, 2); + compare(ids[0], 101); + compare(ids[1], 102); + } + + function test_addAssigneeAppendsToIdsArrayOutput() { + testMultiAssignee.setSelectedAssignees([ + { id: 201, name: "Dave Lead" } + ]); + assigneesChangedSpy.clear(); + + testMultiAssignee.addAssignee({ id: 202, name: "Eve Engineer" }); + + compare(assigneesChangedSpy.count, 1); + var ids = testMultiAssignee.getSelectedAssigneeIds(); + compare(ids.length, 2); + compare(ids[0], 201); + compare(ids[1], 202); + } + + function test_addDuplicateAssigneeDoesNotDuplicateId() { + testMultiAssignee.setSelectedAssignees([ + { id: 301, name: "Frank QA" } + ]); + assigneesChangedSpy.clear(); + + testMultiAssignee.addAssignee({ id: 301, name: "Frank QA Duplicate" }); + + compare(assigneesChangedSpy.count, 0); + var ids = testMultiAssignee.getSelectedAssigneeIds(); + compare(ids.length, 1); + compare(ids[0], 301); + } + + function test_removeAssigneeUpdatesIdsArrayOutput() { + testMultiAssignee.setSelectedAssignees([ + { id: 401, name: "Grace Support" }, + { id: 402, name: "Heidi Admin" } + ]); + assigneesChangedSpy.clear(); + + testMultiAssignee.removeAssignee(0); + + compare(assigneesChangedSpy.count, 1); + var ids = testMultiAssignee.getSelectedAssigneeIds(); + compare(ids.length, 1); + compare(ids[0], 402); + } + + function test_readOnlyBlocksAddAndRemove() { + testMultiAssignee.setSelectedAssignees([ + { id: 501, name: "Ivan DevOps" } + ]); + assigneesChangedSpy.clear(); + + testMultiAssignee.readOnly = true; + + testMultiAssignee.addAssignee({ id: 502, name: "Judy Manager" }); + compare(assigneesChangedSpy.count, 0); + compare(testMultiAssignee.getSelectedAssigneeIds().length, 1); + + testMultiAssignee.removeAssignee(0); + compare(assigneesChangedSpy.count, 0); + compare(testMultiAssignee.getSelectedAssigneeIds().length, 1); + } + + function test_setEnabledToggle() { + testMultiAssignee.readOnly = false; + testMultiAssignee.setEnabled(false); + compare(testMultiAssignee.enabledState, false); + + testMultiAssignee.setEnabled(true); + compare(testMultiAssignee.enabledState, true); + + testMultiAssignee.readOnly = true; + testMultiAssignee.setEnabled(true); + compare(testMultiAssignee.enabledState, false); + } + } +} diff --git a/tests/qml/tst_projectstageselector.qml b/tests/qml/tst_projectstageselector.qml new file mode 100644 index 00000000..2f6aa2c1 --- /dev/null +++ b/tests/qml/tst_projectstageselector.qml @@ -0,0 +1,124 @@ +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/components/selectors" +import "../../models/dbinit.js" as DbInit +import "../../models/database.js" as DBCommon + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + ProjectStageSelector { + id: testStageSelector + } + + SignalSpy { + id: stageSelectedSpy + target: testStageSelector + signalName: "stageSelected" + } + + TestCase { + name: "ProjectStageSelectorTests" + 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_project_stage_app WHERE account_id = 9999"); + tx.executeSql("INSERT INTO project_project_stage_app (account_id, odoo_record_id, name, sequence, active, fold) VALUES (?, ?, ?, ?, ?, ?)", [9999, 101, "Planning", 1, 1, 0]); + tx.executeSql("INSERT INTO project_project_stage_app (account_id, odoo_record_id, name, sequence, active, fold) VALUES (?, ?, ?, ?, ?, ?)", [9999, 102, "In Progress", 2, 1, 0]); + tx.executeSql("INSERT INTO project_project_stage_app (account_id, odoo_record_id, name, sequence, active, fold) VALUES (?, ?, ?, ?, ?, ?)", [9999, 103, "Completed", 3, 1, 1]); + }); + } + + 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_project_stage_app WHERE account_id = 9999"); + }); + } catch (e) { + console.log("Cleanup error: " + e); + } + } + + function init() { + testStageSelector.projectId = -1; + testStageSelector.accountId = -1; + testStageSelector.currentStageOdooRecordId = -1; + testStageSelector.availableStages = []; + stageSelectedSpy.clear(); + } + + function cleanup() { + stageSelectedSpy.clear(); + } + + function test_initialProperties() { + compare(testStageSelector.title, "Change Project Stage"); + compare(testStageSelector.projectId, -1); + compare(testStageSelector.accountId, -1); + compare(testStageSelector.currentStageOdooRecordId, -1); + compare(stageSelectedSpy.count, 0); + } + + function test_propertyAssignments() { + testStageSelector.projectId = 42; + testStageSelector.accountId = 9999; + testStageSelector.currentStageOdooRecordId = 101; + + compare(testStageSelector.projectId, 42); + compare(testStageSelector.accountId, 9999); + compare(testStageSelector.currentStageOdooRecordId, 101); + } + + function test_loadStagesForAccount() { + testStageSelector.accountId = 9999; + testStageSelector.currentStageOdooRecordId = 102; + testStageSelector.loadStages(); + + compare(testStageSelector.availableStages.length, 3); + compare(testStageSelector.availableStages[0].odoo_record_id, 101); + compare(testStageSelector.availableStages[0].name, "Planning"); + compare(testStageSelector.availableStages[0].fold, 0); + + compare(testStageSelector.availableStages[1].odoo_record_id, 102); + compare(testStageSelector.availableStages[1].name, "In Progress"); + + compare(testStageSelector.availableStages[2].odoo_record_id, 103); + compare(testStageSelector.availableStages[2].name, "Completed"); + compare(testStageSelector.availableStages[2].fold, 1); + } + + function test_loadStagesForEmptyAccount() { + testStageSelector.accountId = 88888; + testStageSelector.loadStages(); + compare(testStageSelector.availableStages.length, 0); + } + + function test_stageSelectedSignalEmission() { + compare(stageSelectedSpy.count, 0); + testStageSelector.stageSelected(103, "Completed"); + compare(stageSelectedSpy.count, 1); + compare(stageSelectedSpy.signalArguments[0][0], 103); + compare(stageSelectedSpy.signalArguments[0][1], "Completed"); + } + + function test_stageSelectedMultipleEmissions() { + testStageSelector.stageSelected(101, "Planning"); + testStageSelector.stageSelected(102, "In Progress"); + + compare(stageSelectedSpy.count, 2); + compare(stageSelectedSpy.signalArguments[0][0], 101); + compare(stageSelectedSpy.signalArguments[0][1], "Planning"); + compare(stageSelectedSpy.signalArguments[1][0], 102); + compare(stageSelectedSpy.signalArguments[1][1], "In Progress"); + } + } +} diff --git a/tests/qml/tst_selectionbutton.qml b/tests/qml/tst_selectionbutton.qml new file mode 100644 index 00000000..e0889c1a --- /dev/null +++ b/tests/qml/tst_selectionbutton.qml @@ -0,0 +1,136 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/selectors" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + SelectionButton { + id: testSelectionBtn + width: testRoot.width + selectorType: "Project" + labelText: "Select Project" + } + + SignalSpy { + id: selectionMadeSpy + target: testSelectionBtn + signalName: "selectionMade" + } + + TestCase { + name: "SelectionButtonTests" + when: windowShown + + readonly property var sampleData: [ + { id: 1, name: "Website Redesign" }, + { id: 2, name: "Mobile App" }, + { id: 3, name: "Database Migration" } + ] + + function init() { + testSelectionBtn.enabledState = true; + testSelectionBtn.readOnly = false; + testSelectionBtn.selectedId = -1; + testSelectionBtn.setData(sampleData); + selectionMadeSpy.clear(); + } + + function cleanup() { + selectionMadeSpy.clear(); + } + + function test_initialProperties() { + compare(testSelectionBtn.selectorType, "Project"); + compare(testSelectionBtn.labelText, "Select Project"); + compare(testSelectionBtn.enabledState, true); + compare(testSelectionBtn.readOnly, false); + compare(testSelectionBtn.selectedId, -1); + compare(testSelectionBtn.modelData.length, 3); + compare(selectionMadeSpy.count, 0); + } + + function test_setDataUpdatesModel() { + var newData = [ + { id: 10, name: "Internal Operations" }, + { id: 20, name: "Customer Support" } + ]; + testSelectionBtn.setData(newData); + + compare(testSelectionBtn.modelData.length, 2); + compare(testSelectionBtn.modelData[0].name, "Internal Operations"); + compare(testSelectionBtn.modelData[1].id, 20); + } + + function test_updateLabelSetsSelectedIdWithoutSignal() { + testSelectionBtn.update_label("Mobile App"); + + compare(testSelectionBtn.selectedId, 2); + compare(selectionMadeSpy.count, 0); + } + + function test_applyDeferredSelectionWithoutSignal() { + testSelectionBtn.applyDeferredSelection(3, false); + + compare(testSelectionBtn.selectedId, 3); + compare(selectionMadeSpy.count, 0); + } + + function test_applyDeferredSelectionWithSignal() { + testSelectionBtn.applyDeferredSelection(1, true); + + compare(testSelectionBtn.selectedId, 1); + compare(selectionMadeSpy.count, 1); + var args = selectionMadeSpy.signalArguments[0]; + compare(args[0], 1); + compare(args[1], "Website Redesign"); + compare(args[2], "Project"); + } + + function test_setEnabledWhenNotReadOnly() { + testSelectionBtn.readOnly = false; + + testSelectionBtn.setEnabled(false); + compare(testSelectionBtn.enabledState, false); + + testSelectionBtn.setEnabled(true); + compare(testSelectionBtn.enabledState, true); + } + + function test_setEnabledWhenReadOnlyStaysDisabled() { + testSelectionBtn.readOnly = true; + + testSelectionBtn.setEnabled(true); + compare(testSelectionBtn.enabledState, false); + } + + function findChildWithProperty(item, prop) { + if (!item) return null; + if (item.hasOwnProperty(prop) || item[prop] !== undefined) return item; + for (var i = 0; i < (item.children ? item.children.length : 0); i++) { + var found = findChildWithProperty(item.children[i], prop); + if (found) return found; + } + return null; + } + + function test_selectionClickEmitsSignal() { + var inlineSelector = findChildWithProperty(testSelectionBtn, "collapsed"); + verify(inlineSelector !== null, "InlineOptionSelector should be found"); + + // Expand selector + inlineSelector.expand(); + compare(inlineSelector.collapsed, false); + + // Delegate selection simulation via applyDeferredSelection with signal + testSelectionBtn.applyDeferredSelection(2, true); + + compare(testSelectionBtn.selectedId, 2); + compare(selectionMadeSpy.count, 1); + compare(selectionMadeSpy.signalArguments[0][1], "Mobile App"); + } + } +} diff --git a/tests/qml/tst_stagefiltermenu.qml b/tests/qml/tst_stagefiltermenu.qml new file mode 100644 index 00000000..d25ec4b3 --- /dev/null +++ b/tests/qml/tst_stagefiltermenu.qml @@ -0,0 +1,205 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/selectors" + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(70) + + StageFilterMenu { + id: testStageMenu + } + + SignalSpy { + id: itemSelectedSpy + target: testStageMenu + signalName: "menuItemSelected" + } + + SignalSpy { + id: filterClearedSpy + target: testStageMenu + signalName: "filterCleared" + } + + TestCase { + name: "StageFilterMenuTests" + when: windowShown + + readonly property var sampleStages: [ + { label: "All Stages", value: -1 }, + { label: "New", value: 10 }, + { label: "In Progress", value: 20 }, + { label: "Done", value: 30 } + ] + + function init() { + testStageMenu.expanded = false; + testStageMenu.selectedIndex = -1; + testStageMenu.selectedFilterName = "All Stages"; + testStageMenu.menuModel = []; + itemSelectedSpy.clear(); + filterClearedSpy.clear(); + } + + function cleanup() { + itemSelectedSpy.clear(); + filterClearedSpy.clear(); + } + + function test_initialProperties() { + compare(testStageMenu.expanded, false); + compare(testStageMenu.selectedIndex, -1); + compare(testStageMenu.selectedFilterName, "All Stages"); + compare(testStageMenu.menuModel.length, 0); + compare(itemSelectedSpy.count, 0); + compare(filterClearedSpy.count, 0); + } + + function test_expandedToggle() { + testStageMenu.expanded = true; + compare(testStageMenu.expanded, true); + + testStageMenu.expanded = false; + compare(testStageMenu.expanded, false); + } + + function test_menuModelAssignment() { + testStageMenu.menuModel = sampleStages; + compare(testStageMenu.menuModel.length, 4); + compare(testStageMenu.menuModel[0].label, "All Stages"); + compare(testStageMenu.menuModel[1].value, 10); + compare(testStageMenu.menuModel[2].label, "In Progress"); + compare(testStageMenu.menuModel[3].value, 30); + } + + function findItemWithText(root, text) { + if (!root) return null; + if (root.text && root.text === text) return root; + for (var i = 0; i < (root.children ? root.children.length : 0); i++) { + var found = findItemWithText(root.children[i], text); + if (found) return found; + } + return null; + } + + function findItemWithIcon(root, iconName) { + if (!root) return null; + if (root.iconName === iconName) return root; + for (var i = 0; i < (root.children ? root.children.length : 0); i++) { + var found = findItemWithIcon(root.children[i], iconName); + if (found) return found; + } + return null; + } + + function test_menuItemSelectedSignal() { + compare(itemSelectedSpy.count, 0); + testStageMenu.menuModel = sampleStages; + testStageMenu.expanded = true; + wait(200); + + var item = findItemWithText(testStageMenu, "In Progress"); + verify(item !== null, "Delegate with text 'In Progress' should be found"); + + mouseClick(item); + wait(100); + + compare(testStageMenu.selectedIndex, 2); + compare(testStageMenu.selectedFilterName, "In Progress"); + compare(testStageMenu.expanded, false); + compare(itemSelectedSpy.count, 1); + compare(itemSelectedSpy.signalArguments[0][0], 2); + } + + function test_filterClearedSignal() { + compare(filterClearedSpy.count, 0); + testStageMenu.menuModel = sampleStages; + testStageMenu.selectedIndex = 2; + testStageMenu.selectedFilterName = "In Progress"; + testStageMenu.expanded = true; + wait(200); + + var clearBtn = findItemWithIcon(testStageMenu, "edit-clear"); + verify(clearBtn !== null, "Clear filter button should be found"); + verify(clearBtn.visible, "Clear filter button should be visible when selectedIndex > 0"); + + mouseClick(clearBtn); + wait(100); + + compare(testStageMenu.selectedIndex, 0); + compare(testStageMenu.selectedFilterName, "All Stages"); + compare(testStageMenu.expanded, false); + compare(filterClearedSpy.count, 1); + compare(itemSelectedSpy.count, 1); + compare(itemSelectedSpy.signalArguments[0][0], 0); + } + + function test_selectionStateUpdates() { + testStageMenu.menuModel = sampleStages; + testStageMenu.selectedIndex = 2; + testStageMenu.selectedFilterName = "In Progress"; + + compare(testStageMenu.selectedIndex, 2); + compare(testStageMenu.selectedFilterName, "In Progress"); + + testStageMenu.selectedIndex = 3; + testStageMenu.selectedFilterName = "Done"; + + compare(testStageMenu.selectedIndex, 3); + compare(testStageMenu.selectedFilterName, "Done"); + } + + function test_fabTogglesExpanded() { + var fab = findItemWithIcon(testStageMenu, "filters"); + verify(fab !== null, "FAB button should be found"); + compare(testStageMenu.expanded, false); + + mouseClick(fab); + wait(100); + compare(testStageMenu.expanded, true); + + mouseClick(fab); + wait(100); + compare(testStageMenu.expanded, false); + } + + function test_clearingFilterFlow() { + testStageMenu.menuModel = sampleStages; + testStageMenu.expanded = true; + wait(200); + + // 1. Select "Done" stage (index 3) via delegate click + var doneItem = findItemWithText(testStageMenu, "Done"); + verify(doneItem !== null, "Done item should be found"); + mouseClick(doneItem); + wait(100); + + compare(testStageMenu.selectedIndex, 3); + compare(testStageMenu.selectedFilterName, "Done"); + compare(testStageMenu.expanded, false); + compare(itemSelectedSpy.count, 1); + compare(itemSelectedSpy.signalArguments[0][0], 3); + + // 2. Open menu again and click clear filter button + testStageMenu.expanded = true; + wait(200); + + var clearBtn = findItemWithIcon(testStageMenu, "edit-clear"); + verify(clearBtn !== null, "Clear button should be found"); + verify(clearBtn.visible, "Clear button should be visible"); + + mouseClick(clearBtn); + wait(100); + + compare(testStageMenu.selectedIndex, 0); + compare(testStageMenu.selectedFilterName, "All Stages"); + compare(testStageMenu.expanded, false); + compare(filterClearedSpy.count, 1); + compare(itemSelectedSpy.count, 2); + compare(itemSelectedSpy.signalArguments[1][0], 0); + } + } +} diff --git a/tests/qml/tst_statusselector.qml b/tests/qml/tst_statusselector.qml new file mode 100644 index 00000000..fd5a6985 --- /dev/null +++ b/tests/qml/tst_statusselector.qml @@ -0,0 +1,128 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/selectors" + +MainView { + id: testRoot + width: units.gu(40) + height: units.gu(70) + + StatusSelector { + id: testStatusSelector + width: testRoot.width + } + + SignalSpy { + id: statusChangedSpy + target: testStatusSelector + signalName: "statusChanged" + } + + TestCase { + name: "StatusSelectorTests" + when: windowShown + + 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 getButtons() { + var btnOnTrack = findButtonByText(testStatusSelector, "On Track"); + var btnAtRisk = findButtonByText(testStatusSelector, "At Risk"); + var btnOffTrack = findButtonByText(testStatusSelector, "Off Track"); + verify(btnOnTrack !== null, "On Track button should be found"); + verify(btnAtRisk !== null, "At Risk button should be found"); + verify(btnOffTrack !== null, "Off Track button should be found"); + return { + onTrack: btnOnTrack, + atRisk: btnAtRisk, + offTrack: btnOffTrack + }; + } + + function init() { + testStatusSelector.readOnly = false; + testStatusSelector.selectedStatus = ""; + statusChangedSpy.clear(); + } + + function cleanup() { + testStatusSelector.readOnly = false; + testStatusSelector.selectedStatus = ""; + statusChangedSpy.clear(); + } + + function test_initialProperties() { + compare(testStatusSelector.readOnly, false); + compare(testStatusSelector.selectedStatus, ""); + compare(statusChangedSpy.count, 0); + + var btns = getButtons(); + compare(btns.onTrack.enabled, true); + compare(btns.atRisk.enabled, true); + compare(btns.offTrack.enabled, true); + compare(btns.onTrack.color, LomiriColors.ash); + compare(btns.atRisk.color, LomiriColors.ash); + compare(btns.offTrack.color, LomiriColors.ash); + } + + function test_selectOnTrackStatusAndColor() { + var btns = getButtons(); + mouseClick(btns.onTrack); + + compare(testStatusSelector.selectedStatus, "on_track"); + compare(statusChangedSpy.count, 1); + compare(btns.onTrack.color, LomiriColors.green); + compare(btns.atRisk.color, LomiriColors.ash); + compare(btns.offTrack.color, LomiriColors.ash); + } + + function test_selectAtRiskStatusAndColor() { + var btns = getButtons(); + mouseClick(btns.atRisk); + + compare(testStatusSelector.selectedStatus, "at_risk"); + compare(statusChangedSpy.count, 1); + compare(btns.atRisk.color, LomiriColors.orange); + compare(btns.onTrack.color, LomiriColors.ash); + compare(btns.offTrack.color, LomiriColors.ash); + } + + function test_selectOffTrackStatusAndColor() { + var btns = getButtons(); + mouseClick(btns.offTrack); + + compare(testStatusSelector.selectedStatus, "off_track"); + compare(statusChangedSpy.count, 1); + compare(btns.offTrack.color, LomiriColors.red); + compare(btns.onTrack.color, LomiriColors.ash); + compare(btns.atRisk.color, LomiriColors.ash); + } + + function test_statusDirectAssignmentUpdatesBadgeColors() { + testStatusSelector.selectedStatus = "on_track"; + var btns = getButtons(); + compare(btns.onTrack.color, LomiriColors.green); + + testStatusSelector.selectedStatus = "off_track"; + compare(btns.offTrack.color, LomiriColors.red); + compare(btns.onTrack.color, LomiriColors.ash); + } + + function test_readOnlyDisablesButtons() { + testStatusSelector.readOnly = true; + + var btns = getButtons(); + compare(btns.onTrack.enabled, false); + compare(btns.atRisk.enabled, false); + compare(btns.offTrack.enabled, false); + } + } +} diff --git a/tests/qml/tst_workitemselector.qml b/tests/qml/tst_workitemselector.qml new file mode 100644 index 00000000..ab002392 --- /dev/null +++ b/tests/qml/tst_workitemselector.qml @@ -0,0 +1,232 @@ +import QtQuick 2.7 +import QtTest 1.0 +import Lomiri.Components 1.3 +import "../../qml/components/selectors" +import "../../models/dbinit.js" as DbInit + +MainView { + id: testRoot + width: units.gu(50) + height: units.gu(80) + + WorkItemSelector { + id: testSelector + } + + SignalSpy { + id: stateChangedSpy + target: testSelector + signalName: "stateChanged" + } + + SignalSpy { + id: multiAssigneesSpy + target: testSelector + signalName: "multiAssigneesChanged" + } + + TestCase { + name: "WorkItemSelectorTests" + when: windowShown + + function initTestCase() { + DbInit.initializeDatabase(); + } + + function init() { + testSelector.showAccountSelector = true; + testSelector.showProjectSelector = true; + testSelector.showSubProjectSelector = true; + testSelector.showTaskSelector = true; + testSelector.showSubTaskSelector = true; + testSelector.showAssigneeSelector = true; + testSelector.enableMultipleAssignees = false; + testSelector.readOnly = false; + testSelector.restrictAccountToLocalOnly = false; + testSelector.currentState = "Init"; + testSelector.selectedProjectId = -1; + testSelector.selectedSubProjectId = -1; + testSelector.selectedTaskId = -1; + testSelector.selectedSubTaskId = -1; + testSelector.selectedAssigneeId = -1; + stateChangedSpy.clear(); + multiAssigneesSpy.clear(); + } + + function cleanup() { + stateChangedSpy.clear(); + multiAssigneesSpy.clear(); + } + + function test_initialProperties() { + compare(testSelector.showAccountSelector, true); + compare(testSelector.showProjectSelector, true); + compare(testSelector.showSubProjectSelector, true); + compare(testSelector.showTaskSelector, true); + compare(testSelector.showSubTaskSelector, true); + compare(testSelector.showAssigneeSelector, true); + compare(testSelector.enableMultipleAssignees, false); + compare(testSelector.readOnly, false); + compare(testSelector.restrictAccountToLocalOnly, false); + compare(testSelector.currentState, "Init"); + compare(testSelector.selectedProjectId, -1); + compare(testSelector.selectedSubProjectId, -1); + compare(testSelector.selectedTaskId, -1); + compare(testSelector.selectedSubTaskId, -1); + compare(testSelector.selectedAssigneeId, -1); + compare(stateChangedSpy.count, 0); + } + + function test_visibilityToggles() { + testSelector.showProjectSelector = false; + compare(testSelector.showProjectSelector, false); + + testSelector.showSubProjectSelector = false; + compare(testSelector.showSubProjectSelector, false); + + testSelector.showTaskSelector = false; + compare(testSelector.showTaskSelector, false); + + testSelector.showSubTaskSelector = false; + compare(testSelector.showSubTaskSelector, false); + + testSelector.showAssigneeSelector = false; + compare(testSelector.showAssigneeSelector, false); + + testSelector.enableMultipleAssignees = true; + compare(testSelector.enableMultipleAssignees, true); + } + + function test_transitionToFunction() { + testSelector.transitionTo("CustomState", { key: "value" }); + compare(testSelector.currentState, "CustomState"); + compare(stateChangedSpy.count, 1); + compare(stateChangedSpy.signalArguments[0][0], "CustomState"); + compare(stateChangedSpy.signalArguments[0][1].key, "value"); + } + + function test_handleSelectionAccount() { + testSelector.handleSelection(1, "My Local Account", "Account"); + compare(testSelector.selectedAccountId, 1); + compare(testSelector.currentState, "AccountSelected"); + compare(stateChangedSpy.count, 1); + compare(stateChangedSpy.signalArguments[0][0], "AccountSelected"); + compare(stateChangedSpy.signalArguments[0][1].id, 1); + compare(stateChangedSpy.signalArguments[0][1].name, "My Local Account"); + } + + function test_handleSelectionProjectClearsChildSelections() { + testSelector.selectedSubProjectId = 5; + testSelector.selectedTaskId = 12; + testSelector.selectedSubTaskId = 34; + + testSelector.handleSelection(100, "Core Platform", "Project"); + + compare(testSelector.selectedProjectId, 100); + compare(testSelector.currentState, "ProjectSelected"); + compare(stateChangedSpy.count, 1); + compare(stateChangedSpy.signalArguments[0][0], "ProjectSelected"); + compare(stateChangedSpy.signalArguments[0][1].id, 100); + compare(stateChangedSpy.signalArguments[0][1].name, "Core Platform"); + } + + function test_handleSelectionSubproject() { + testSelector.selectedProjectId = 50; + + // Valid subproject + testSelector.handleSelection(55, "Frontend Sub", "Subproject"); + compare(testSelector.selectedSubProjectId, 55); + compare(testSelector.currentState, "SubprojectSelected"); + compare(stateChangedSpy.count, 1); + + stateChangedSpy.clear(); + // Reset to -1 falls back to project_component.selectedId (-1 by default) + testSelector.handleSelection(-1, "Select", "Subproject"); + compare(testSelector.selectedSubProjectId, -1); + compare(testSelector.currentState, "ProjectSelected"); + compare(stateChangedSpy.count, 1); + } + + function test_handleSelectionTaskClearsSubtask() { + testSelector.selectedSubTaskId = 88; + + testSelector.handleSelection(200, "Implement Auth", "Task"); + compare(testSelector.selectedTaskId, 200); + compare(testSelector.selectedSubTaskId, -1); + compare(testSelector.currentState, "TaskSelected"); + compare(stateChangedSpy.count, 1); + compare(stateChangedSpy.signalArguments[0][1].id, 200); + } + + function test_handleSelectionSubtask() { + testSelector.handleSelection(300, "Write Unit Tests", "Subtask"); + compare(testSelector.selectedSubTaskId, 300); + compare(testSelector.currentState, "SubtaskSelected"); + compare(stateChangedSpy.count, 1); + + stateChangedSpy.clear(); + testSelector.handleSelection(-1, "Select", "Subtask"); + compare(testSelector.selectedSubTaskId, -1); + compare(testSelector.currentState, "TaskSelected"); + compare(stateChangedSpy.count, 1); + } + + function test_handleSelectionAssignee() { + testSelector.handleSelection(42, "Jane Smith", "Assignee"); + compare(testSelector.selectedAssigneeId, 42); + compare(testSelector.currentState, "AssigneeSelected"); + compare(stateChangedSpy.count, 1); + compare(stateChangedSpy.signalArguments[0][1].id, 42); + compare(stateChangedSpy.signalArguments[0][1].name, "Jane Smith"); + } + + function test_getIdsNormalization() { + testSelector.selectedAccountId = -1; + + var ids = testSelector.getIds(); + compare(ids.account_id, null); + compare(ids.project_id, null); + compare(ids.subproject_id, null); + compare(ids.task_id, null); + compare(ids.subtask_id, null); + compare(ids.assignee_id, null); + + testSelector.selectedAccountId = 42; + var activeIds = testSelector.getIds(); + compare(activeIds.account_id, 42); + compare(activeIds.project_id, null); + } + + function test_multiAssigneesWithModeEnabled() { + testSelector.enableMultipleAssignees = true; + var sample = [ + { id: 101, name: "Alice", email: "alice@test.com" }, + { id: 102, name: "Bob", email: "bob@test.com" } + ]; + + testSelector.setMultipleAssignees(sample); + var retrieved = testSelector.getMultipleAssignees(); + compare(retrieved.length, 2); + compare(retrieved[0].id, 101); + compare(retrieved[1].id, 102); + + var ids = testSelector.getIds(); + verify(ids.multiple_assignees !== undefined); + compare(ids.multiple_assignees.length, 2); + verify(ids.assignee_ids !== undefined); + compare(ids.assignee_ids.length, 2); + compare(ids.assignee_ids[0], 101); + compare(ids.assignee_ids[1], 102); + } + + function test_readOnlyStateUpdate() { + testSelector.readOnly = true; + compare(testSelector.readOnly, true); + testSelector.updateAllSelectorStates(); + + testSelector.readOnly = false; + compare(testSelector.readOnly, false); + testSelector.updateAllSelectorStates(); + } + } +}