Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions tests/qml/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
172 changes: 172 additions & 0 deletions tests/qml/models/tst_accounts.qml
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
198 changes: 198 additions & 0 deletions tests/qml/models/tst_activity.qml
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading
Loading