From f0cc44b3bdf14e5089aef15fe5755587f862ee41 Mon Sep 17 00:00:00 2001 From: Suraj Yadav Date: Tue, 13 Oct 2026 13:27:12 +0530 Subject: [PATCH] Add unit tests for JavaScript data models Add QML test suites covering data models including task, timesheet, project, activity, accounts, notifications, and main helpers. --- tests/qml/README.md | 7 + tests/qml/models/tst_accounts.qml | 172 ++++++++++++++++ tests/qml/models/tst_activity.qml | 198 ++++++++++++++++++ tests/qml/models/tst_main.qml | 143 +++++++++++++ tests/qml/models/tst_notifications.qml | 142 +++++++++++++ tests/qml/models/tst_project.qml | 199 ++++++++++++++++++ tests/qml/models/tst_task.qml | 268 +++++++++++++++++++++++++ tests/qml/models/tst_timesheet.qml | 268 +++++++++++++++++++++++++ 8 files changed, 1397 insertions(+) create mode 100644 tests/qml/models/tst_accounts.qml create mode 100644 tests/qml/models/tst_activity.qml create mode 100644 tests/qml/models/tst_main.qml create mode 100644 tests/qml/models/tst_notifications.qml create mode 100644 tests/qml/models/tst_project.qml create mode 100644 tests/qml/models/tst_task.qml create mode 100644 tests/qml/models/tst_timesheet.qml diff --git a/tests/qml/README.md b/tests/qml/README.md index 60c4f4c0..81830d1f 100644 --- a/tests/qml/README.md +++ b/tests/qml/README.md @@ -23,10 +23,17 @@ tests/ ├── qml/ │ ├── README.md # This guide │ ├── models/ # JavaScript core and model test suites +│ │ ├── tst_accounts.qml # Test suite for account management, defaults, sync settings, and deletion +│ │ ├── tst_activity.qml # Test suite for activity data, sanitizeId, type icons, date filters, and CRUD │ │ ├── tst_constants.qml # Test suite for constants and quadrant color configuration │ │ ├── tst_dashboard_user_filter.qml # Test suite for dashboard user filter state, models, and queries │ │ ├── tst_logger.qml # Test suite for logger levels, formatting, and PII sanitization +│ │ ├── tst_main.qml # Test suite for Main.js date helpers, name resolvers, and spent hours queries +│ │ ├── tst_notifications.qml # Test suite for notifications creation, read status, and sync notifications +│ │ ├── tst_project.qml # Test suite for project details, offline cache, favorites, and project updates +│ │ ├── tst_task.qml # Test suite for task model filters, assignee parsing, date status, and CRUD │ │ ├── tst_timer_service.qml # Test suite for timer service state transitions and duration +│ │ ├── tst_timesheet.qml # Test suite for timesheet creation, duration, readiness, and status transitions │ │ └── tst_utils.qml # Test suite for date, time, duration, and URL utilities │ ├── tst_accountselectordialog.qml # Test suite for AccountSelectorDialog picker and account switch │ ├── tst_colorpicker.qml # Test suite for ColorPicker palette and custom color diff --git a/tests/qml/models/tst_accounts.qml b/tests/qml/models/tst_accounts.qml new file mode 100644 index 00000000..581f43b4 --- /dev/null +++ b/tests/qml/models/tst_accounts.qml @@ -0,0 +1,172 @@ +import QtQuick 2.7 +import QtTest 1.0 +import QtQuick.LocalStorage 2.7 as Sql +import "../../../models/logger.js" as Logger +import "../../../models/database.js" as DBCommon +import "../../../models/dbinit.js" as DbInit +import "../../../models/utils.js" as Utils +import "../../../models/accounts.js" as AccountsModel + +Item { + width: 200 + height: 200 + + TestCase { + name: "AccountsModelTests" + when: windowShown + + function initTestCase() { + Logger.setLogLevel(Logger.LogLevel.NONE); + DbInit.initializeDatabase(); + } + + function cleanupTestCase() { + Logger.setLogLevel(Logger.LogLevel.WARN); + } + + function clearTestData() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("DELETE FROM users WHERE id >= 90000 OR name LIKE 'TestAccount%' OR name LIKE 'CreatedAccount%'"); + tx.executeSql("DELETE FROM attachment_download_app WHERE account_id = 999 OR record_id >= 90000"); + tx.executeSql("UPDATE users SET is_default = 0"); + // Ensure local account is default + tx.executeSql("UPDATE users SET is_default = 1 WHERE id = 0"); + }); + } + + function init() { + clearTestData(); + } + + function cleanup() { + clearTestData(); + } + + // --- 1. Default Account Management --- + + function test_defaultAccountHandling() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO users (id, name, link, database, username, is_default) VALUES (?, ?, ?, ?, ?, ?)", + [90001, "TestAccount Alpha", "https://alpha.example.com", "db_alpha", "user_alpha", 0] + ); + tx.executeSql( + "INSERT INTO users (id, name, link, database, username, is_default) VALUES (?, ?, ?, ?, ?, ?)", + [90002, "TestAccount Beta", "https://beta.example.com", "db_beta", "user_beta", 0] + ); + }); + + AccountsModel.setDefaultAccount(90001); + compare(AccountsModel.getDefaultAccountId(), 90001); + + AccountsModel.setDefaultAccount(90002); + compare(AccountsModel.getDefaultAccountId(), 90002); + + AccountsModel.clearDefaultAccount(); + // When none marked default, getDefaultAccountId falls back to first account (usually id 0) + compare(AccountsModel.getDefaultAccountId(), 0); + } + + // --- 2. Account Creation and Duplicate Detection --- + + function test_createAccount_success_and_duplicates() { + var res1 = AccountsModel.createAccount("CreatedAccount One", "https://odoo1.com", "db1", "admin1", 1, "apikey1"); + verify(!res1.duplicateFound); + + // Duplicate name (case-insensitive) + var resDupName = AccountsModel.createAccount("createdaccount one", "https://other.com", "otherdb", "otheruser", 1, "key"); + verify(resDupName.duplicateFound); + compare(resDupName.duplicateType, "name"); + + // Duplicate server connection + var resDupConn = AccountsModel.createAccount("CreatedAccount Two", "https://odoo1.com", "db1", "admin1", 1, "apikey2"); + verify(resDupConn.duplicateFound); + compare(resDupConn.duplicateType, "connection"); + } + + // --- 3. Account Name and Update --- + + function test_getAccountName_and_updateAccount() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO users (id, name, link, database, username) VALUES (?, ?, ?, ?, ?)", + [90010, "TestAccount Gamma", "https://gamma.com", "db_gamma", "user_gamma"] + ); + }); + + // Local account ID 0 name check + compare(AccountsModel.getAccountName(0), "Local"); + + // User account name check + compare(AccountsModel.getAccountName(90010), "TestAccount Gamma"); + + // Update account + var updateRes = AccountsModel.updateAccount(90010, "TestAccount Gamma Renamed", "https://gamma2.com", "db_gamma2", "user_gamma2", 1, "newkey"); + verify(updateRes.success); + + compare(AccountsModel.getAccountName(90010), "TestAccount Gamma Renamed"); + } + + // --- 4. Delete Account and Related Data --- + + function test_deleteAccountAndRelatedData() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO users (id, name, link, database, username) VALUES (?, ?, ?, ?, ?)", + [90020, "TestAccount Delta", "https://delta.com", "db_delta", "user_delta"] + ); + tx.executeSql( + "INSERT INTO project_project_app (id, odoo_record_id, name, account_id) VALUES (?, ?, ?, ?)", + [90021, 90021, "Delta Project", 90020] + ); + }); + + AccountsModel.deleteAccountAndRelatedData(90020); + + // User record should be deleted + var afterUser = AccountsModel.getAccountName(90020); + compare(afterUser, ""); + + // Related project should also be deleted + db.transaction(function (tx) { + var rs = tx.executeSql("SELECT COUNT(*) AS count FROM project_project_app WHERE account_id = ?", [90020]); + compare(rs.rows.item(0).count, 0); + }); + + // Verify local account 0 cannot be deleted + AccountsModel.deleteAccountAndRelatedData(0); + compare(AccountsModel.getAccountName(0), "Local"); + } + + // --- 5. Sync Settings --- + + function test_accountSyncSettings() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO users (id, name, link, database, username) VALUES (?, ?, ?, ?, ?)", + [90030, "TestAccount Sync", "https://sync.com", "db_sync", "user_sync"] + ); + }); + + AccountsModel.updateAccountSyncSettings(90030, 30, "download_only", 1); + var settings = AccountsModel.getAccountSyncSettings(90030); + compare(settings.sync_interval_minutes, 30); + compare(settings.sync_direction, "download_only"); + compare(settings.autosync_enabled, 1); + } + + // --- 6. Attachment Download Tracking --- + + function test_attachmentDownloadTracking() { + compare(AccountsModel.isAttachmentDownloaded(999, 90040), false); + + AccountsModel.markAttachmentDownloaded(999, 90040, "specs.pdf"); + compare(AccountsModel.isAttachmentDownloaded(999, 90040), true); + } + } +} diff --git a/tests/qml/models/tst_activity.qml b/tests/qml/models/tst_activity.qml new file mode 100644 index 00000000..ca1d8800 --- /dev/null +++ b/tests/qml/models/tst_activity.qml @@ -0,0 +1,198 @@ +import QtQuick 2.7 +import QtTest 1.0 +import QtQuick.LocalStorage 2.7 as Sql +import "../../../models/logger.js" as Logger +import "../../../models/database.js" as DBCommon +import "../../../models/dbinit.js" as DbInit +import "../../../models/utils.js" as Utils +import "../../../models/activity.js" as ActivityModel + +Item { + width: 200 + height: 200 + + TestCase { + name: "ActivityModelTests" + when: windowShown + + function initTestCase() { + Logger.setLogLevel(Logger.LogLevel.NONE); + DbInit.initializeDatabase(); + } + + function cleanupTestCase() { + Logger.setLogLevel(Logger.LogLevel.WARN); + } + + function clearTestData() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("DELETE FROM mail_activity_app WHERE account_id = 999 OR id >= 90000"); + tx.executeSql("DELETE FROM mail_activity_type_app WHERE account_id = 999 OR id >= 90000"); + }); + } + + function init() { + clearTestData(); + } + + function cleanup() { + clearTestData(); + } + + // --- 1. Pure Helper Functions --- + + function test_sanitizeId() { + compare(ActivityModel.sanitizeId(null), -1); + compare(ActivityModel.sanitizeId(undefined), -1); + compare(ActivityModel.sanitizeId(""), -1); + compare(ActivityModel.sanitizeId(0), 0); + compare(ActivityModel.sanitizeId(42), 42); + compare(ActivityModel.sanitizeId("42"), 42); + compare(ActivityModel.sanitizeId(-5), -5); + compare(ActivityModel.sanitizeId("invalid"), -1); + } + + function test_getActivityIconForType() { + compare(ActivityModel.getActivityIconForType("Send Mail"), "activity_mail.png"); + compare(ActivityModel.getActivityIconForType("Client Call"), "activity_call.png"); + compare(ActivityModel.getActivityIconForType("Project Meeting"), "activity_meeting.png"); + compare(ActivityModel.getActivityIconForType("Todo List"), "activity_others.png"); + compare(ActivityModel.getActivityIconForType(""), "activity_others.png"); + compare(ActivityModel.getActivityIconForType(null), "activity_others.png"); + } + + function test_passesActivityDateFilter() { + var refDate = new Date(2026, 9, 13); // 2026-10-13 + + var actToday = { due_date: "2026-10-13" }; + var actYesterday = { due_date: "2026-10-12" }; + var actNoDate = { due_date: null }; + + // "all" filter + verify(ActivityModel.passesActivityDateFilter(actToday, "all", refDate)); + verify(ActivityModel.passesActivityDateFilter(actNoDate, "all", refDate)); + + // Without date, non-"all" filters return false + verify(!ActivityModel.passesActivityDateFilter(actNoDate, "today", refDate)); + + // "today" includes today and overdue items + verify(ActivityModel.passesActivityDateFilter(actToday, "today", refDate)); + verify(ActivityModel.passesActivityDateFilter(actYesterday, "today", refDate)); + + // "overdue" + verify(ActivityModel.passesActivityDateFilter(actYesterday, "overdue", refDate)); + verify(!ActivityModel.passesActivityDateFilter(actToday, "overdue", refDate)); + } + + function test_passesActivitySearchFilter() { + var activity = { + summary: "Quarterly Planning", + notes: "Discuss Q4 objectives and budget", + activity_type_id: 1, + account_id: 999 + }; + + verify(ActivityModel.passesActivitySearchFilter(activity, "")); + verify(ActivityModel.passesActivitySearchFilter(activity, null)); + verify(ActivityModel.passesActivitySearchFilter(activity, "Quarterly")); + verify(ActivityModel.passesActivitySearchFilter(activity, "planning")); + verify(ActivityModel.passesActivitySearchFilter(activity, "budget")); + verify(!ActivityModel.passesActivitySearchFilter(activity, "UnrelatedTerm")); + } + + // --- 2. Database CRUD and Mutations --- + + function test_saveActivityData_and_getActivityById() { + var newActivity = { + updatedAccount: 999, + updatedActivity: 1, + updatedSummary: "Review Architecture Doc", + updatedUserId: 10, + updatedDate: "2026-10-14", + updatedNote: "Detailed review comments", + resModel: "project.project", + resId: 101, + task_id: 0, + project_id: 101, + link_id: 101, + state: "planned", + status: "normal" + }; + + var saveRes = ActivityModel.saveActivityData(newActivity, 0); + verify(saveRes.success); + var actId = saveRes.recordId; + verify(actId > 0); + + var details = ActivityModel.getActivityById(actId, 999); + verify(details !== null); + compare(details.id, actId); + compare(details.summary, "Review Architecture Doc"); + compare(details.account_id, 999); + compare(details.state, "planned"); + + // Update the activity + newActivity.updatedSummary = "Review Architecture Doc Updated"; + var updateRes = ActivityModel.saveActivityData(newActivity, actId); + verify(updateRes.success); + + var updatedDetails = ActivityModel.getActivityById(actId, 999); + compare(updatedDetails.summary, "Review Architecture Doc Updated"); + } + + function test_markAsDone_and_updateActivityDate() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO mail_activity_app (id, account_id, summary, due_date, state, status) VALUES (?, ?, ?, ?, ?, ?)", + [90010, 999, "Call Vendor", "2026-10-13", "planned", "normal"] + ); + }); + + ActivityModel.updateActivityDate(999, 90010, "2026-10-15"); + var d1 = ActivityModel.getActivityById(90010, 999); + compare(d1.due_date, "2026-10-15"); + compare(d1.status, "updated"); + + ActivityModel.markAsDone(999, 90010); + var d2 = ActivityModel.getActivityById(90010, 999); + compare(d2.state, "done"); + compare(d2.status, "updated"); + } + + function test_deleteActivity() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO mail_activity_app (id, account_id, summary, status) VALUES (?, ?, ?, ?)", + [90020, 999, "Temporary Activity", "normal"] + ); + }); + + var delRes = ActivityModel.deleteActivity(999, 90020); + verify(delRes.success); + + var details = ActivityModel.getActivityById(90020, 999); + // Non-existent activity returns empty object or null + verify(!details || !details.id); + } + + function test_getActivityTypesForAccount() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO mail_activity_type_app (id, odoo_record_id, name, account_id, status) VALUES (?, ?, ?, ?, ?)", + [90031, 90031, "Email", 999, "normal"] + ); + tx.executeSql( + "INSERT INTO mail_activity_type_app (id, odoo_record_id, name, account_id, status) VALUES (?, ?, ?, ?, ?)", + [90032, 90032, "Meeting", 999, "normal"] + ); + }); + + var types = ActivityModel.getActivityTypesForAccount(999); + compare(types.length, 2); + } + } +} diff --git a/tests/qml/models/tst_main.qml b/tests/qml/models/tst_main.qml new file mode 100644 index 00000000..fa5f5165 --- /dev/null +++ b/tests/qml/models/tst_main.qml @@ -0,0 +1,143 @@ +import QtQuick 2.7 +import QtTest 1.0 +import QtQuick.LocalStorage 2.7 as Sql +import "../../../models/logger.js" as Logger +import "../../../models/database.js" as DBCommon +import "../../../models/dbinit.js" as DbInit +import "../../../models/utils.js" as Utils +import "../../../models/Main.js" as MainModel + +Item { + width: 200 + height: 200 + + TestCase { + name: "MainModelTests" + when: windowShown + + function initTestCase() { + Logger.setLogLevel(Logger.LogLevel.NONE); + DbInit.initializeDatabase(); + } + + function cleanupTestCase() { + Logger.setLogLevel(Logger.LogLevel.WARN); + } + + function clearTestData() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("DELETE FROM account_analytic_line_app WHERE account_id = 999 OR id >= 90000"); + tx.executeSql("DELETE FROM project_task_app WHERE account_id = 999 OR id >= 90000"); + tx.executeSql("DELETE FROM project_project_app WHERE account_id = 999 OR id >= 90000"); + }); + } + + function init() { + clearTestData(); + } + + function cleanup() { + clearTestData(); + } + + // --- 1. Pure Helper Date Functions --- + + function test_dateHelpers() { + var monday = MainModel.getMondayOfCurrentWeek(); + verify(monday !== ""); + // Matches YYYY-MM-DD + verify(/^\d{4}-\d{2}-\d{2}$/.test(monday)); + + var firstDay = MainModel.getFirstDayOfCurrentMonth(); + verify(firstDay !== ""); + verify(/^\d{4}-\d{2}-01$/.test(firstDay)); + } + + // --- 2. Entity Name Resolvers --- + + function test_getProjectAndTaskName() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("INSERT INTO project_project_app (id, odoo_record_id, name, account_id) VALUES (?, ?, ?, ?)", [90001, 90001, "Main Project Test", 999]); + tx.executeSql("INSERT INTO project_task_app (id, odoo_record_id, name, account_id) VALUES (?, ?, ?, ?)", [90002, 90002, "Main Task Test", 999]); + }); + + compare(MainModel.get_project_name(90001), "Main Project Test"); + compare(MainModel.get_project_name(99999), ""); + + compare(MainModel.get_task_name(90002), "Main Task Test"); + compare(MainModel.get_task_name(99999), ""); + } + + // --- 3. Quadrant and Spent Hours Aggregations --- + + function test_get_quadrant_difference() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, quadrant_id, unit_amount, status) VALUES (?, ?, ?, ?, ?)", + [90011, 999, 0, 3.5, "saved"] + ); + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, quadrant_id, unit_amount, status) VALUES (?, ?, ?, ?, ?)", + [90012, 999, 1, 4.0, "saved"] + ); + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, quadrant_id, unit_amount, status) VALUES (?, ?, ?, ?, ?)", + [90013, 999, 2, 2.0, "saved"] + ); + }); + + var quadDiff = MainModel.get_quadrant_difference(); + verify(quadDiff !== null); + compare(quadDiff[0], 3.5); + compare(quadDiff[1], 4.0); + compare(quadDiff[2], 2.0); + } + + function test_get_spent_hours_query() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, project_id, unit_amount, record_date, status) VALUES (?, ?, ?, ?, ?, ?)", + [90021, 999, 101, 1.5, "2026-10-10", "saved"] + ); + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, project_id, unit_amount, record_date, status) VALUES (?, ?, ?, ?, ?, ?)", + [90022, 999, 101, 2.5, "2026-10-12", "saved"] + ); + }); + + // Group by project_id + var spentGrouped = MainModel.get_spent_hours({ group_by: "project_id" }, 999); + verify(spentGrouped.length >= 1); + compare(spentGrouped[0].total, 4.0); + + // Filter with dateFilter + var spentFiltered = MainModel.get_spent_hours({ dateFilter: "2026-10-11" }, 999); + compare(spentFiltered.length, 1); + compare(spentFiltered[0].id, 90022); + } + + function test_get_projects_and_tasks_spent_hours() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("INSERT INTO project_project_app (id, odoo_record_id, name, account_id) VALUES (?, ?, ?, ?)", [90100, 90100, "Project Gamma", 999]); + tx.executeSql("INSERT INTO project_task_app (id, odoo_record_id, name, account_id, project_id) VALUES (?, ?, ?, ?, ?)", [90200, 90200, "Task Delta", 999, 90100]); + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, project_id, task_id, unit_amount, record_date, status, user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + [90031, 999, 90100, 90200, 5.0, "2026-10-13", "saved", 10] + ); + }); + + var projectsSummary = MainModel.get_projects_spent_hours(999, "2026-10-01", "2026-10-31", 10); + verify(projectsSummary.length >= 1); + compare(projectsSummary[0].name, "Project Gamma"); + compare(projectsSummary[0].total, 5.0); + + var tasksSummary = MainModel.get_tasks_spent_hours(999, "2026-10-01", "2026-10-31", 10); + compare(tasksSummary["Task Delta"], 5.0); + } + } +} diff --git a/tests/qml/models/tst_notifications.qml b/tests/qml/models/tst_notifications.qml new file mode 100644 index 00000000..7dc4b0fc --- /dev/null +++ b/tests/qml/models/tst_notifications.qml @@ -0,0 +1,142 @@ +import QtQuick 2.7 +import QtTest 1.0 +import QtQuick.LocalStorage 2.7 as Sql +import "../../../models/logger.js" as Logger +import "../../../models/database.js" as DBCommon +import "../../../models/dbinit.js" as DbInit +import "../../../models/notifications.js" as NotificationsModel + +Item { + width: 200 + height: 200 + + TestCase { + name: "NotificationsModelTests" + when: windowShown + + function initTestCase() { + Logger.setLogLevel(Logger.LogLevel.NONE); + DbInit.initializeDatabase(); + } + + function cleanupTestCase() { + Logger.setLogLevel(Logger.LogLevel.WARN); + } + + function clearTestData() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("DELETE FROM notification WHERE account_id = 999 OR id >= 90000"); + }); + } + + function init() { + clearTestData(); + } + + function cleanup() { + clearTestData(); + } + + // --- 1. Notification Creation and Details --- + + function test_addNotification_and_getDetails() { + var payload = { taskId: 123, priority: "high" }; + NotificationsModel.addNotification(999, "Task", "New task assigned", payload); + + var unread = NotificationsModel.getUnreadNotifications(); + verify(unread.length > 0); + + var found = null; + for (var i = 0; i < unread.length; i++) { + if (unread[i].account_id === 999 && unread[i].message === "New task assigned") { + found = unread[i]; + break; + } + } + verify(found !== null); + compare(found.type, "Task"); + compare(found.read_status, 0); + + var details = NotificationsModel.getDetailsOfNotification(found.id); + verify(details !== null); + compare(details.id, found.id); + compare(details.message, "New task assigned"); + + var parsedPayload = JSON.parse(details.payload); + compare(parsedPayload.taskId, 123); + } + + // --- 2. Read Status and Counts --- + + function test_readStatusAndCounts() { + NotificationsModel.addNotification(999, "Project", "Project milestone met", { projectId: 10 }); + NotificationsModel.addNotification(999, "Activity", "Meeting scheduled", { activityId: 20 }); + + var unreadCountBefore = NotificationsModel.getUnreadCount(); + verify(unreadCountBefore >= 2); + + var unreadList = NotificationsModel.getUnreadNotifications(); + var targetId = unreadList[0].id; + + NotificationsModel.markAsRead(targetId); + var details = NotificationsModel.getDetailsOfNotification(targetId); + compare(details.read_status, 1); + + NotificationsModel.markAllAsRead(); + compare(NotificationsModel.getUnreadCount(), 0); + + // Delete all read notifications + NotificationsModel.deleteAllRead(); + var detailsAfterDelete = NotificationsModel.getDetailsOfNotification(targetId); + compare(detailsAfterDelete, null); + } + + // --- 3. Sync vs Normal Notifications --- + + function test_syncVsNormalNotifications() { + NotificationsModel.addNotification(999, "Task", "Normal task notification", { id: 1 }); + NotificationsModel.addNotification(999, "Sync", "Sync failed on network timeout", { error: "timeout" }); + + verify(NotificationsModel.hasSyncNotifications()); + + var normalList = NotificationsModel.getUnreadNormalNotifications(); + verify(normalList.length >= 1); + compare(normalList[0].type !== "Sync", true); + + var syncList = NotificationsModel.getUnreadSyncNotifications(); + verify(syncList.length >= 1); + compare(syncList[0].type, "Sync"); + + var normalCount = NotificationsModel.getUnreadNormalCount(); + var syncCount = NotificationsModel.getUnreadSyncCount(); + verify(normalCount >= 1); + verify(syncCount >= 1); + + // Clear sync notifications for account 999 + var cleared = NotificationsModel.clearSyncNotifications(999); + verify(cleared >= 1); + compare(NotificationsModel.getUnreadSyncCount(), 0); + + // Normal notifications should remain + compare(NotificationsModel.getUnreadNormalCount(), normalCount); + } + + // --- 4. Deletion --- + + function test_deleteNotification_and_deleteAll() { + NotificationsModel.addNotification(999, "Task", "Task to delete", {}); + var unread = NotificationsModel.getUnreadNotifications(); + verify(unread.length > 0); + + var noteId = unread[0].id; + NotificationsModel.deleteNotification(noteId); + compare(NotificationsModel.getDetailsOfNotification(noteId), null); + + NotificationsModel.addNotification(999, "Task", "Task A", {}); + NotificationsModel.addNotification(999, "Task", "Task B", {}); + NotificationsModel.deleteAllNotifications(); + compare(NotificationsModel.getUnreadCount(), 0); + } + } +} diff --git a/tests/qml/models/tst_project.qml b/tests/qml/models/tst_project.qml new file mode 100644 index 00000000..a6298695 --- /dev/null +++ b/tests/qml/models/tst_project.qml @@ -0,0 +1,199 @@ +import QtQuick 2.7 +import QtTest 1.0 +import QtQuick.LocalStorage 2.7 as Sql +import "../../../models/logger.js" as Logger +import "../../../models/database.js" as DBCommon +import "../../../models/dbinit.js" as DbInit +import "../../../models/utils.js" as Utils +import "../../../models/project.js" as ProjectModel + +Item { + width: 200 + height: 200 + + TestCase { + name: "ProjectModelTests" + when: windowShown + + function initTestCase() { + Logger.setLogLevel(Logger.LogLevel.NONE); + DbInit.initializeDatabase(); + } + + function cleanupTestCase() { + Logger.setLogLevel(Logger.LogLevel.WARN); + } + + function clearTestData() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("DELETE FROM project_project_app WHERE account_id = 999 OR id >= 90000"); + tx.executeSql("DELETE FROM project_update_app WHERE account_id = 999 OR id >= 90000"); + tx.executeSql("DELETE FROM dl_cache_app WHERE record_id >= 90000"); + }); + } + + function init() { + clearTestData(); + } + + function cleanup() { + clearTestData(); + } + + // --- 1. Cache Functions --- + + function test_cacheOperations() { + compare(ProjectModel.isPresentInCache(90001), false); + compare(ProjectModel.getFromCache(90001), null); + + ProjectModel.putInCache(90001, "SGVsbG8gV29ybGQ="); // base64 + compare(ProjectModel.isPresentInCache(90001), true); + compare(ProjectModel.getFromCache(90001), "SGVsbG8gV29ybGQ="); + + ProjectModel.putInCache(90002, "VXBkYXRlZA=="); + compare(ProjectModel.isPresentInCache(90002), true); + compare(ProjectModel.getFromCache(90002), "VXBkYXRlZA=="); + } + + // --- 2. ID Mapping and Details --- + + function test_getLocalIdFromOdooId() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO project_project_app (id, odoo_record_id, name, account_id) VALUES (?, ?, ?, ?)", + [90010, 44444, "Mapped Project", 999] + ); + }); + + compare(ProjectModel.getLocalIdFromOdooId(44444, 999), 90010); + compare(ProjectModel.getLocalIdFromOdooId(99999, 999), -1); + } + + function test_getProjectName_and_getProjectDetails() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO project_project_app (id, odoo_record_id, name, account_id, description, favorites, color_pallet) " + + "VALUES (?, ?, ?, ?, ?, ?, ?)", + [90020, 55555, "Alpha Project", 999, "A detailed project", 0, "#FF0000"] + ); + }); + + var name = ProjectModel.getProjectName(90020, 999); + compare(name, "Alpha Project"); + + var details = ProjectModel.getProjectDetails(90020); + verify(details !== null); + compare(details.id, 90020); + compare(details.name, "Alpha Project"); + compare(details.account_id, 999); + compare(details.description, "A detailed project"); + compare(details.color_pallet, "#FF0000"); + } + + // --- 3. Project Creation, Updating, and Favorites --- + + function test_createUpdateProject() { + var newProject = { + account_id: 999, + name: "Created Project", + parent_id: 0, + planned_start_date: "2026-10-13", + planned_end_date: "2026-10-20", + allocated_hours: "10:00", + favorites: 0, + description: "Test description", + color: "#00FF00", + stage: 1, + status: "normal" + }; + + var createRes = ProjectModel.createUpdateProject(newProject, 0); + verify(createRes.is_success); + var projectId = createRes.record_id; + verify(projectId > 0); + + var details = ProjectModel.getProjectDetails(projectId); + compare(details.name, "Created Project"); + compare(details.account_id, 999); + compare(details.color_pallet, "#00FF00"); + + // Update project + newProject.name = "Updated Project Name"; + newProject.favorites = 1; + var updateRes = ProjectModel.createUpdateProject(newProject, projectId); + verify(updateRes.is_success); + + var updatedDetails = ProjectModel.getProjectDetails(projectId); + compare(updatedDetails.name, "Updated Project Name"); + compare(updatedDetails.favorites, 1); + } + + function test_toggleProjectFavorite() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO project_project_app (id, odoo_record_id, name, account_id, favorites, status) VALUES (?, ?, ?, ?, ?, ?)", + [90030, 90030, "Favorite Test Project", 999, 0, "normal"] + ); + }); + + var resFav = ProjectModel.toggleProjectFavorite(90030, true, "updated"); + verify(resFav.success); + var d1 = ProjectModel.getProjectDetails(90030); + compare(d1.favorites, 1); + compare(d1.status, "updated"); + + var resUnfav = ProjectModel.toggleProjectFavorite(90030, false, "updated"); + verify(resUnfav.success); + var d2 = ProjectModel.getProjectDetails(90030); + compare(d2.favorites, 0); + } + + // --- 4. Project Updates / Snapshots --- + + function test_projectUpdatesCRUD() { + var updateData = { + project_id: 90020, + name: "Weekly Status Update", + project_status: "on_track", + progress: 50, + description: "Halfway completed milestones", + account_id: 999, + user_id: 10 + }; + + var res = ProjectModel.createUpdateSnapShot(updateData, 0); + verify(res.is_success); + var updateId = res.record_id; + verify(updateId > 0); + + var updates = ProjectModel.getAllProjectUpdates(999); + verify(updates.length > 0); + + var delRes = ProjectModel.markProjectUpdateAsDeleted(updateId); + verify(delRes.success); + } + + // --- 5. Project Listing --- + + function test_getProjectsForAccount() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO project_project_app (id, odoo_record_id, name, account_id, status) VALUES (?, ?, ?, ?, ?)", + [90041, 90041, "Account Project 1", 999, "normal"] + ); + tx.executeSql( + "INSERT INTO project_project_app (id, odoo_record_id, name, account_id, status) VALUES (?, ?, ?, ?, ?)", + [90042, 90042, "Account Project 2", 999, "normal"] + ); + }); + + var projects = ProjectModel.getProjectsForAccount(999); + compare(projects.length, 2); + } + } +} diff --git a/tests/qml/models/tst_task.qml b/tests/qml/models/tst_task.qml new file mode 100644 index 00000000..7fc11fc3 --- /dev/null +++ b/tests/qml/models/tst_task.qml @@ -0,0 +1,268 @@ +import QtQuick 2.7 +import QtTest 1.0 +import QtQuick.LocalStorage 2.7 as Sql +import "../../../models/logger.js" as Logger +import "../../../models/database.js" as DBCommon +import "../../../models/dbinit.js" as DbInit +import "../../../models/utils.js" as Utils +import "../../../models/task.js" as TaskModel + +Item { + width: 200 + height: 200 + + TestCase { + name: "TaskModelTests" + when: windowShown + + function initTestCase() { + Logger.setLogLevel(Logger.LogLevel.NONE); + DbInit.initializeDatabase(); + } + + function cleanupTestCase() { + Logger.setLogLevel(Logger.LogLevel.WARN); + } + + function init() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("DELETE FROM project_task_app WHERE account_id = 999 OR id >= 90000"); + tx.executeSql("DELETE FROM project_project_app WHERE account_id = 999 OR id >= 90000"); + tx.executeSql("DELETE FROM project_task_type_app WHERE account_id = 999 OR odoo_record_id >= 90000"); + }); + } + + function cleanup() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("DELETE FROM project_task_app WHERE account_id = 999 OR id >= 90000"); + tx.executeSql("DELETE FROM project_project_app WHERE account_id = 999 OR id >= 90000"); + tx.executeSql("DELETE FROM project_task_type_app WHERE account_id = 999 OR odoo_record_id >= 90000"); + }); + } + + // --- 1. Pure Helper Functions --- + + function test_validId() { + compare(TaskModel.validId(10), 10); + compare(TaskModel.validId(1), 1); + compare(TaskModel.validId(0), null); + compare(TaskModel.validId(-5), null); + compare(TaskModel.validId(undefined), null); + compare(TaskModel.validId(null), null); + } + + function test_parseAssigneeIds() { + compare(TaskModel.parseAssigneeIds("").length, 0); + compare(TaskModel.parseAssigneeIds(null).length, 0); + compare(TaskModel.parseAssigneeIds(undefined).length, 0); + + var parsedSingle = TaskModel.parseAssigneeIds("42"); + compare(parsedSingle.length, 1); + compare(parsedSingle[0], 42); + + var parsedMulti = TaskModel.parseAssigneeIds("10, 25, 30"); + compare(parsedMulti.length, 3); + compare(parsedMulti[0], 10); + compare(parsedMulti[1], 25); + compare(parsedMulti[2], 30); + + var parsedDirty = TaskModel.parseAssigneeIds("10, invalid, -2, 0, 50"); + compare(parsedDirty.length, 2); + compare(parsedDirty[0], 10); + compare(parsedDirty[1], 50); + } + + function test_formatAssigneeIds() { + compare(TaskModel.formatAssigneeIds([]), ""); + compare(TaskModel.formatAssigneeIds(null), ""); + compare(TaskModel.formatAssigneeIds(undefined), ""); + + var assignees = [{ id: 5 }, { id: 12 }, { id: 99 }]; + compare(TaskModel.formatAssigneeIds(assignees), "5,12,99"); + + var single = [{ id: 7 }]; + compare(TaskModel.formatAssigneeIds(single), "7"); + } + + function test_passesSearchFilter() { + var task = { + name: "Implement Dashboard Filter", + description: "Allow filtering tasks by employee and project" + }; + + verify(TaskModel.passesSearchFilter(task, "")); + verify(TaskModel.passesSearchFilter(task, null)); + verify(TaskModel.passesSearchFilter(task, "Dashboard")); + verify(TaskModel.passesSearchFilter(task, "dashboard")); // Case-insensitive + verify(TaskModel.passesSearchFilter(task, "filtering")); + verify(TaskModel.passesSearchFilter(task, "EMPLOYEE")); + verify(!TaskModel.passesSearchFilter(task, "NonexistentKeyword")); + } + + function test_stageClassification() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("INSERT INTO project_task_type_app (odoo_record_id, name, account_id) VALUES (?, ?, ?)", [90001, "Done", 999]); + tx.executeSql("INSERT INTO project_task_type_app (odoo_record_id, name, account_id) VALUES (?, ?, ?)", [90002, "In Progress", 999]); + tx.executeSql("INSERT INTO project_task_type_app (odoo_record_id, name, account_id) VALUES (?, ?, ?)", [90003, "Cancelled", 999]); + }); + + var doneTask = { state: 90001, account_id: 999 }; + var activeTask = { state: 90002, account_id: 999 }; + var cancelledTask = { state: 90003, account_id: 999 }; + + verify(TaskModel.isTaskInDoneStage(doneTask)); + verify(!TaskModel.isTaskInDoneStage(activeTask)); + + verify(TaskModel.isTaskInCancelledStage(cancelledTask)); + verify(!TaskModel.isTaskInCancelledStage(doneTask)); + } + + function test_passesDateFilter_allFilter() { + var taskWithDates = { start_date: "2026-10-13", end_date: "2026-10-14" }; + var taskNoDates = { start_date: null, end_date: null }; + + verify(TaskModel.passesDateFilter(taskWithDates, "all", new Date())); + verify(TaskModel.passesDateFilter(taskNoDates, "all", new Date())); + verify(!TaskModel.passesDateFilter(taskNoDates, "today", new Date())); + } + + function test_dueCalculationsAndOverdue() { + var refDate = new Date(2026, 9, 13); // 2026-10-13 + + var taskToday = { start_date: "2026-10-13", end_date: "2026-10-13" }; + var taskYesterday = { start_date: "2026-10-12", end_date: "2026-10-12" }; + var taskTomorrow = { start_date: "2026-10-14", end_date: "2026-10-14" }; + + // Today task is due today + verify(TaskModel.isTaskDueToday(taskToday, refDate)); + + // Tomorrow task is not due today + verify(!TaskModel.isTaskDueToday(taskTomorrow, refDate)); + + // Yesterday task is overdue (and shown in today view) + verify(TaskModel.isTaskDueToday(taskYesterday, refDate)); + verify(TaskModel.isTaskOverdue(taskYesterday, refDate)); + verify(!TaskModel.isTaskOverdue(taskToday, refDate)); + verify(!TaskModel.isTaskOverdue(taskTomorrow, refDate)); + + var statusOverdue = TaskModel.getTaskDateStatus(taskYesterday, refDate); + verify(statusOverdue.isOverdue); + + var statusToday = TaskModel.getTaskDateStatus(taskToday, refDate); + verify(statusToday.isInRange); + } + + // --- 2. Database CRUD and Query Functions --- + + function test_saveOrUpdateTask_and_getTaskDetails() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO project_project_app (id, odoo_record_id, name, account_id, parent_id) VALUES (?, ?, ?, ?, ?)", + [101, 101, "Test Project", 999, 0] + ); + }); + + var taskData = { + name: "Unit Test Task Alpha", + description: "Test task description", + projectId: 101, + priority: 1, + accountId: 999, + assigneeUserId: "10,20", + status: "normal" + }; + + var res = TaskModel.saveOrUpdateTask(taskData); + verify(res.success); + var taskId = res.taskId; + verify(taskId > 0); + + var details = TaskModel.getTaskDetails(taskId); + verify(details !== null); + compare(details.name, "Unit Test Task Alpha"); + compare(details.project_id, 101); + compare(details.account_id, 999); + compare(details.priority, "1"); + + // Now test updating the existing task + taskData.record_id = taskId; + taskData.name = "Unit Test Task Alpha Updated"; + var updateRes = TaskModel.saveOrUpdateTask(taskData); + verify(updateRes.success); + + var updatedDetails = TaskModel.getTaskDetails(taskId); + compare(updatedDetails.name, "Unit Test Task Alpha Updated"); + } + + function test_isUserAssignedToTask() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO project_task_app (id, odoo_record_id, name, user_id, account_id) VALUES (?, ?, ?, ?, ?)", + [90002, 90002, "Multi Assignee Task", "10, 20, 30", 999] + ); + }); + + verify(TaskModel.isUserAssignedToTask(90002, 10)); + verify(TaskModel.isUserAssignedToTask(90002, 20)); + verify(TaskModel.isUserAssignedToTask(90002, 30)); + verify(!TaskModel.isUserAssignedToTask(90002, 40)); + verify(!TaskModel.isUserAssignedToTask(90002, 99)); + } + + function test_setTaskPriority() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO project_task_app (id, odoo_record_id, name, priority, account_id) VALUES (?, ?, ?, ?, ?)", + [90003, 90003, "Priority Test Task", 0, 999] + ); + }); + + var res = TaskModel.setTaskPriority(90003, 3, "updated"); + verify(res.success); + + var details = TaskModel.getTaskDetails(90003); + verify(details !== null); + compare(details.priority, "3"); + compare(details.status, "updated"); + } + + function test_markTaskAsDeleted() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO project_task_app (id, odoo_record_id, name, status, account_id) VALUES (?, ?, ?, ?, ?)", + [90004, 90004, "Task to Delete", "normal", 999] + ); + }); + + var deleted = TaskModel.markTaskAsDeleted(90004); + verify(deleted); + + var details = TaskModel.getTaskDetails(90004); + verify(details !== null); + compare(details.status, "deleted"); + } + + function test_getLocalIdFromOdooId() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO project_task_app (id, odoo_record_id, name, account_id) VALUES (?, ?, ?, ?)", + [90005, 54321, "Odoo Id Mapping Task", 999] + ); + }); + + var localId = TaskModel.getLocalIdFromOdooId(54321, 999); + compare(localId, 90005); + + var nonExistent = TaskModel.getLocalIdFromOdooId(99999, 999); + compare(nonExistent, -1); + } + } +} diff --git a/tests/qml/models/tst_timesheet.qml b/tests/qml/models/tst_timesheet.qml new file mode 100644 index 00000000..e536ed5f --- /dev/null +++ b/tests/qml/models/tst_timesheet.qml @@ -0,0 +1,268 @@ +import QtQuick 2.7 +import QtTest 1.0 +import QtQuick.LocalStorage 2.7 as Sql +import "../../../models/logger.js" as Logger +import "../../../models/database.js" as DBCommon +import "../../../models/dbinit.js" as DbInit +import "../../../models/utils.js" as Utils +import "../../../models/timesheet.js" as TimesheetModel + +Item { + width: 200 + height: 200 + + TestCase { + name: "TimesheetModelTests" + when: windowShown + + function initTestCase() { + Logger.setLogLevel(Logger.LogLevel.NONE); + DbInit.initializeDatabase(); + } + + function cleanupTestCase() { + Logger.setLogLevel(Logger.LogLevel.WARN); + } + + function clearTestData() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("DELETE FROM account_analytic_line_app WHERE account_id = 999 OR id >= 90000"); + tx.executeSql("DELETE FROM project_task_app WHERE account_id = 999 OR id >= 90000"); + tx.executeSql("DELETE FROM project_project_app WHERE account_id = 999 OR id >= 90000"); + }); + } + + function init() { + clearTestData(); + } + + function cleanup() { + clearTestData(); + } + + // --- 1. Creation and Details --- + + function test_createTimesheet_and_getTimeSheetDetails() { + var createRes = TimesheetModel.createTimesheet(999, 10); + verify(createRes.success); + var sheetId = createRes.id; + verify(sheetId > 0); + + var details = TimesheetModel.getTimeSheetDetails(sheetId, 999); + verify(details !== null); + compare(details.id, sheetId); + compare(details.instance_id, 999); + compare(details.user_id, 10); + compare(details.status, "draft"); + } + + function test_createTimesheet_invalidAccount() { + var res = TimesheetModel.createTimesheet(-1, 10); + verify(!res.success); + } + + // --- 2. Readiness and Finalization --- + + function test_readinessChecks() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + // Neither project nor task + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, project_id, task_id, status) VALUES (?, ?, ?, ?, ?)", + [90010, 999, null, null, "draft"] + ); + // Project only + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, project_id, task_id, status) VALUES (?, ?, ?, ?, ?)", + [90011, 999, 101, null, "draft"] + ); + // Project and task + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, project_id, task_id, status) VALUES (?, ?, ?, ?, ?)", + [90012, 999, 101, 201, "draft"] + ); + }); + + // Timer ready requires project only + verify(!TimesheetModel.isTimesheetReadyToStartTimer(90010)); + verify(TimesheetModel.isTimesheetReadyToStartTimer(90011)); + verify(TimesheetModel.isTimesheetReadyToStartTimer(90012)); + + // Record/sync ready requires both project and task + verify(!TimesheetModel.isTimesheetReadyToRecord(90010)); + verify(!TimesheetModel.isTimesheetReadyToRecord(90011)); + verify(TimesheetModel.isTimesheetReadyToRecord(90012)); + } + + function test_isTimesheetFinalized() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("INSERT INTO account_analytic_line_app (id, account_id, status) VALUES (?, ?, ?)", [90020, 999, "draft"]); + tx.executeSql("INSERT INTO account_analytic_line_app (id, account_id, status) VALUES (?, ?, ?)", [90021, 999, "active"]); + tx.executeSql("INSERT INTO account_analytic_line_app (id, account_id, status) VALUES (?, ?, ?)", [90022, 999, "updated"]); + tx.executeSql("INSERT INTO account_analytic_line_app (id, account_id, status) VALUES (?, ?, ?)", [90023, 999, "saved"]); + }); + + verify(!TimesheetModel.isTimesheetFinalized(90020)); + verify(!TimesheetModel.isTimesheetFinalized(90021)); + verify(TimesheetModel.isTimesheetFinalized(90022)); + verify(TimesheetModel.isTimesheetFinalized(90023)); + } + + // --- 3. Save, Duration, and Deletion --- + + function test_saveTimesheet() { + var createRes = TimesheetModel.createTimesheet(999, 10); + verify(createRes.success); + var sheetId = createRes.id; + + var updateData = { + id: sheetId, + instance_id: 999, + description: "Work on QML Unit Tests", + project: 101, + task: 201, + unit_amount: 2.5, + status: "draft" + }; + + var saveRes = TimesheetModel.saveTimesheet(updateData); + verify(saveRes.success); + + var details = TimesheetModel.getTimeSheetDetails(sheetId, 999); + compare(details.name, "Work on QML Unit Tests"); + compare(details.project_id, 101); + compare(details.task_id, 201); + compare(details.spentHours, "2:30"); + } + + function test_updateDuration_and_getUnitAmount() { + var createRes = TimesheetModel.createTimesheet(999, 10); + var sheetId = createRes.id; + + TimesheetModel.updateTimesheetWithDuration(sheetId, "01:45"); + var amount = TimesheetModel.getTimesheetUnitAmount(sheetId); + compare(amount, 1.75); + } + + function test_markTimesheetAsDeleted() { + var createRes = TimesheetModel.createTimesheet(999, 10); + var sheetId = createRes.id; + + var res = TimesheetModel.markTimesheetAsDeleted(sheetId); + verify(res.success); + + var details = TimesheetModel.getTimeSheetDetails(sheetId, 999); + compare(details.status, "deleted"); + } + + // --- 4. Status Transitions --- + + function test_statusTransitions() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, project_id, task_id, status) VALUES (?, ?, ?, ?, ?)", + [90030, 999, 101, 201, "draft"] + ); + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, project_id, task_id, status) VALUES (?, ?, ?, ?, ?)", + [90031, 999, 101, 201, "draft"] + ); + }); + + // Mark 90030 as active + TimesheetModel.markTimesheetAsActiveById(90030); + var d1 = TimesheetModel.getTimeSheetDetails(90030, 999); + compare(d1.status, "active"); + + // Mark 90031 as active -> 90030 should revert to draft + TimesheetModel.markTimesheetAsActiveById(90031); + var d1Reverted = TimesheetModel.getTimeSheetDetails(90030, 999); + var d2Active = TimesheetModel.getTimeSheetDetails(90031, 999); + compare(d1Reverted.status, "draft"); + compare(d2Active.status, "active"); + + // Mark 90030 as saved + TimesheetModel.markTimesheetAsSavedById(90030); + var d1Saved = TimesheetModel.getTimeSheetDetails(90030, 999); + compare(d1Saved.status, "saved"); + + // Mark 90030 as draft + TimesheetModel.markTimesheetAsDraftById(90030); + var d1Draft = TimesheetModel.getTimeSheetDetails(90030, 999); + compare(d1Draft.status, "draft"); + + // Mark 90030 as ready (with project and task present) + var readyRes = TimesheetModel.markTimesheetAsReadyById(90030); + verify(readyRes.success); + var d1Ready = TimesheetModel.getTimeSheetDetails(90030, 999); + compare(d1Ready.status, "updated"); + } + + // --- 5. Display Names and Matching --- + + function test_getTimesheetDisplayName_and_Matching() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql("INSERT INTO project_project_app (id, odoo_record_id, name, account_id) VALUES (?, ?, ?, ?)", [90101, 90101, "Test Project", 999]); + tx.executeSql("INSERT INTO project_task_app (id, odoo_record_id, name, account_id) VALUES (?, ?, ?, ?)", [90201, 90201, "Test Task", 999]); + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, project_id, task_id, name, status) VALUES (?, ?, ?, ?, ?, ?)", + [90040, 999, 90101, 90201, "Custom Work Description", "active"] + ); + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, project_id, task_id, name, status) VALUES (?, ?, ?, ?, ?, ?)", + [90041, 999, 90101, 90201, "", "draft"] + ); + }); + + // When name is present, display name is the description + var name1 = TimesheetModel.getTimesheetDisplayName(90040); + compare(name1, "Custom Work Description"); + + // When name is empty, display name falls back to "Project - Task" + var name2 = TimesheetModel.getTimesheetDisplayName(90041); + compare(name2, "Test Project - Test Task"); + + // Matching active sheet + verify(TimesheetModel.doesProjectIdMatchSheetInActive(90101, 90040)); + verify(!TimesheetModel.doesProjectIdMatchSheetInActive(99999, 90040)); + + verify(TimesheetModel.doesTaskIdMatchSheetInActive(90201, 90040)); + verify(!TimesheetModel.doesTaskIdMatchSheetInActive(99999, 90040)); + + // getTimesheetAccountId + compare(TimesheetModel.getTimesheetAccountId(90040), 999); + } + + // --- 6. Fetching by Status --- + + function test_fetchTimesheetsByStatus() { + var db = Sql.LocalStorage.openDatabaseSync(DBCommon.NAME, DBCommon.VERSION, DBCommon.DISPLAY_NAME, DBCommon.SIZE); + db.transaction(function (tx) { + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, status, record_date) VALUES (?, ?, ?, ?)", + [90051, 999, "draft", "2026-10-13"] + ); + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, status, record_date) VALUES (?, ?, ?, ?)", + [90052, 999, "saved", "2026-10-13"] + ); + tx.executeSql( + "INSERT INTO account_analytic_line_app (id, account_id, status, record_date) VALUES (?, ?, ?, ?)", + [90053, 999, "deleted", "2026-10-13"] + ); + }); + + var drafts = TimesheetModel.fetchTimesheetsByStatus("draft", 999); + // In fetchTimesheetsByStatus, status === "draft" matches both "draft" and "saved" + compare(drafts.length, 2); + + var all = TimesheetModel.fetchTimesheetsByStatus("all", 999); + // "all" returns non-deleted records + compare(all.length, 2); + } + } +}