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 @@ -35,6 +35,13 @@ tests/
│ │ ├── 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
│ ├── scripts/ # Feature scripts and helper utility test suites
│ │ ├── tst_chartutils.qml # Test suite for chart data formatting, percentages, and sorting
│ │ ├── tst_chartworker.qml # Test suite for chart WorkerScript asynchronous processing
│ │ ├── tst_htmlsanitizer.qml # Test suite for HTML tag sanitization, color normalization, and validation
│ │ ├── tst_navigationroutes.qml # Test suite for navigation routes and page URL resolution
│ │ ├── 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_colorpicker.qml # Test suite for ColorPicker palette and custom color
│ ├── tst_contentpickerdialog.qml # Test suite for ContentPickerDialog single/multi selection
Expand Down
146 changes: 146 additions & 0 deletions tests/qml/scripts/tst_chartutils.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import QtQuick 2.7
import QtTest 1.0
import "../../../qml/features/dashboard/js/chartUtils.js" as ChartUtils

Item {
width: 200
height: 200

TestCase {
name: "ChartUtilsTests"
when: windowShown

function test_formatHours() {
compare(ChartUtils.formatHours(2.5), "2.5 h");
compare(ChartUtils.formatHours(0), "0.0 h");
compare(ChartUtils.formatHours(null), "0.0 h");
compare(ChartUtils.formatHours(undefined), "0.0 h");
compare(ChartUtils.formatHours(10.123), "10.1 h");
}

function test_calcPercent_and_percentLabel() {
compare(ChartUtils.calcPercent(25, 100), 25);
compare(ChartUtils.calcPercent(0, 100), 0);
compare(ChartUtils.calcPercent(5, 0), 0);
compare(ChartUtils.calcPercent(5, -10), 0);
compare(ChartUtils.calcPercent(null, 100), 0);

compare(ChartUtils.percentLabel(25, 100), "25.0%");
compare(ChartUtils.percentLabel(1, 3), "33.3%");
compare(ChartUtils.percentLabel(0, 0), "0.0%");
}

function test_averageLabel() {
compare(ChartUtils.averageLabel(10, 2), "5.0 h");
compare(ChartUtils.averageLabel(10, 0), "0.0 h");
compare(ChartUtils.averageLabel(10, -5), "0.0 h");
compare(ChartUtils.averageLabel(null, 2), "0.0 h");
}

function test_projectSummaries() {
var projects = [
{ id: 1, name: "Project Alpha", totalHours: 12.5, taskCount: 3 },
{ id: 2, name: "Project Beta", totalHours: 8.0, taskCount: 2 },
{ id: 3, name: "Project Gamma", totalHours: 20.0, taskCount: 5 }
];

compare(ChartUtils.sumProjectHours(projects), 40.5);
compare(ChartUtils.maxProjectHours(projects), 20.0);
compare(ChartUtils.projectSubtitle(projects[0], 3), "3 tasks | 12.5 h");
compare(ChartUtils.projectSubtitle(null, 0), "");
}

function test_taskSummaries() {
var tasks = [
{ id: 10, name: "Refactor API", totalHours: 4.5 },
{ id: 20, name: "Fix UI Glitch", totalHours: 9.0 },
{ id: 30, name: "Write Docs", totalHours: 2.0 }
];

compare(ChartUtils.maxTaskHours(tasks), 9.0);
compare(ChartUtils.topTaskName(tasks), "Fix UI Glitch");
compare(ChartUtils.topTaskName([]), "None");
compare(ChartUtils.topTaskName(null), "None");

var top2 = ChartUtils.topTasks(tasks, 2);
compare(top2.length, 2);
compare(top2[0].name, "Fix UI Glitch");
compare(top2[1].name, "Refactor API");
}

function test_prepareProjects_sorting_and_filtering() {
var projects = [
{ id: 1, name: "Backend Work", totalHours: 10.0, taskCount: 2 },
{ id: 2, name: "Frontend UI", totalHours: 25.0, taskCount: 8 },
{ id: 3, name: "DevOps Pipeline", totalHours: 15.0, taskCount: 1 }
];

// Default sort: totalHours desc
var sortedByHours = ChartUtils.prepareProjects(projects, "hours", "");
compare(sortedByHours.length, 3);
compare(sortedByHours[0].projectData.name, "Frontend UI");
compare(sortedByHours[1].projectData.name, "DevOps Pipeline");
compare(sortedByHours[2].projectData.name, "Backend Work");

// Sort by tasks desc
var sortedByTasks = ChartUtils.prepareProjects(projects, "tasks", "");
compare(sortedByTasks[0].projectData.name, "Frontend UI"); // 8 tasks
compare(sortedByTasks[1].projectData.name, "Backend Work"); // 2 tasks
compare(sortedByTasks[2].projectData.name, "DevOps Pipeline"); // 1 task

// Sort by name asc
var sortedByName = ChartUtils.prepareProjects(projects, "name", "");
compare(sortedByName[0].projectData.name, "Backend Work");
compare(sortedByName[1].projectData.name, "DevOps Pipeline");
compare(sortedByName[2].projectData.name, "Frontend UI");

// Filter with search text
var filtered = ChartUtils.prepareProjects(projects, "hours", "backend");
compare(filtered.length, 1);
compare(filtered[0].projectData.name, "Backend Work");
}

function test_prepareTasks_sorting() {
var tasks = [
{ id: 1, name: "Task B", totalHours: 5.0 },
{ id: 2, name: "Task A", totalHours: 10.0 },
{ id: 3, name: "Task C", totalHours: 5.0 }
];

var sorted = ChartUtils.prepareTasks(tasks);
compare(sorted[0].name, "Task A"); // 10.0 hours
compare(sorted[1].name, "Task B"); // 5.0 hours, alphabetical A before C
compare(sorted[2].name, "Task C"); // 5.0 hours
}

function test_findTaskById() {
var tasks = [
{ id: 101, name: "Design mockups" },
{ id: 102, name: "Implement code" }
];

var found = ChartUtils.findTaskById(tasks, 102);
verify(found !== null);
compare(found.name, "Implement code");

compare(ChartUtils.findTaskById(tasks, 999), null);
}

function test_elide() {
compare(ChartUtils.elide("Short", 10), "Short");
compare(ChartUtils.elide("Long text that should be truncated", 12), "Long text...");
compare(ChartUtils.elide("", 5), "");
compare(ChartUtils.elide(null, 5), "");
}

function test_chartSignature() {
var tasks = [
{ id: 1, totalHours: 3.5 },
{ id: 2, totalHours: 7.0 }
];

var sig = ChartUtils.chartSignature(tasks, "#ff0000", 0);
compare(sig, "#ff0000|0|1:3.5|2:7");
}
}
}
117 changes: 117 additions & 0 deletions tests/qml/scripts/tst_chartworker.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import QtQuick 2.7
import QtTest 1.0

Item {
id: root
width: 200
height: 200

property var lastMessage: null

WorkerScript {
id: chartWorker
source: "../../../qml/features/dashboard/js/chartWorker.js"
onMessage: {
root.lastMessage = messageObject;
}
}

SignalSpy {
id: messageSpy
target: chartWorker
signalName: "message"
}

TestCase {
name: "ChartWorkerTests"
when: windowShown

function init() {
root.lastMessage = null;
messageSpy.clear();
}

function test_projectsMessageProcessing() {
var projects = [
{ id: 1, name: "Backend Database", totalHours: 10.0, taskCount: 2 },
{ id: 2, name: "Frontend UI", totalHours: 30.0, taskCount: 8 },
{ id: 3, name: "DevOps Infrastructure", totalHours: 15.0, taskCount: 1 }
];

// 1. Sort by totalHours descending
chartWorker.sendMessage({
mode: "projects",
projects: projects,
sortMode: "hours",
searchText: ""
});

messageSpy.wait(2000);
verify(root.lastMessage !== null);
compare(root.lastMessage.mode, "projects");
compare(root.lastMessage.items.length, 3);
compare(root.lastMessage.items[0].projectData.name, "Frontend UI");
compare(root.lastMessage.items[1].projectData.name, "DevOps Infrastructure");
compare(root.lastMessage.items[2].projectData.name, "Backend Database");

// 2. Filter by search query
messageSpy.clear();
root.lastMessage = null;
chartWorker.sendMessage({
mode: "projects",
projects: projects,
sortMode: "hours",
searchText: "backend"
});

messageSpy.wait(2000);
verify(root.lastMessage !== null);
compare(root.lastMessage.items.length, 1);
compare(root.lastMessage.items[0].projectData.name, "Backend Database");
}

function test_tasksMessageProcessing() {
var tasks = [
{ id: 10, name: "Low priority task", totalHours: 3.0 },
{ id: 20, name: "High priority task", totalHours: 18.5 },
{ id: 30, name: "Medium priority task", totalHours: 8.0 }
];

chartWorker.sendMessage({
mode: "tasks",
projectId: 99,
tasks: tasks
});

messageSpy.wait(2000);
verify(root.lastMessage !== null);
compare(root.lastMessage.mode, "tasks");
compare(root.lastMessage.projectId, 99);
compare(root.lastMessage.items.length, 3);
compare(root.lastMessage.items[0].name, "High priority task");
compare(root.lastMessage.items[1].name, "Medium priority task");
compare(root.lastMessage.items[2].name, "Low priority task");
}

function test_tasksTieBreaking() {
var tasks = [
{ id: 1, name: "Task Beta", totalHours: 5.0 },
{ id: 2, name: "Task Alpha", totalHours: 5.0 },
{ id: 3, name: "Task Gamma", totalHours: 10.0 }
];

chartWorker.sendMessage({
mode: "tasks",
projectId: 50,
tasks: tasks
});

messageSpy.wait(2000);
verify(root.lastMessage !== null);
compare(root.lastMessage.items.length, 3);
compare(root.lastMessage.items[0].name, "Task Gamma");
compare(root.lastMessage.items[1].name, "Task Alpha");
compare(root.lastMessage.items[2].name, "Task Beta");
}
}
}
106 changes: 106 additions & 0 deletions tests/qml/scripts/tst_htmlsanitizer.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import QtQuick 2.7
import QtTest 1.0
import "../../../qml/components/richtext/js/html-sanitizer.js" as HtmlSanitizer

Item {
width: 200
height: 200

TestCase {
name: "HtmlSanitizerTests"
when: windowShown

function test_extractBodyContent() {
var fullDoc = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"><html><head><meta name="qrichtext" content="1" /></head><body><p>Clean paragraph</p></body></html>';
compare(HtmlSanitizer.extractBodyContent(fullDoc), "<p>Clean paragraph</p>");

var plainHtml = "<div>Normal content</div>";
compare(HtmlSanitizer.extractBodyContent(plainHtml), "<div>Normal content</div>");

compare(HtmlSanitizer.extractBodyContent(""), "");
compare(HtmlSanitizer.extractBodyContent(null), "");
}

function test_cleanQtStyles() {
var qtStyled = '<p style="-qt-block-indent:0; -qt-paragraph-type:empty; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; text-indent:0px; color:#ff0000;">Styled text</p>';
var cleaned = HtmlSanitizer.cleanQtStyles(qtStyled);
verify(cleaned.indexOf("-qt-") === -1);
verify(cleaned.indexOf("margin-top:12px") === -1);
verify(cleaned.indexOf("text-indent:0px") === -1);
verify(cleaned.indexOf("color:#ff0000;") !== -1);
}

function test_normalizeColors() {
var rgbHtml = '<span style="color: rgb(255, 0, 0);">Red</span>';
compare(HtmlSanitizer.normalizeColors(rgbHtml), '<span style="color: #ff0000;">Red</span>');

var rgbaHtml = '<div style="background-color: rgba(0, 128, 255, 0.5);">Blue</div>';
compare(HtmlSanitizer.normalizeColors(rgbaHtml), '<div style="background-color: #0080ff;">Blue</div>');
}

function test_normalizeOdooClasses() {
var odooHtml = '<h2 class="h1-fs text-primary">Heading</h2>';
var normalized = HtmlSanitizer.normalizeOdooClasses(odooHtml);
verify(normalized.indexOf("font-size: 28px;") !== -1);
verify(normalized.indexOf("color: #714b67;") !== -1);
}

function test_convertFontTagsToSpans() {
var legacyFont = '<font color="#00ff00" size="4">Green text</font>';
var converted = HtmlSanitizer.convertFontTagsToSpans(legacyFont);
verify(converted.startsWith('<span class="colour"'));
verify(converted.indexOf("color:#00ff00") !== -1);
verify(converted.indexOf("font-size:18px") !== -1);
verify(converted.indexOf("Green text</span>") !== -1);
}

function test_validate() {
var safeHtml = "<p>Safe text with <b>bold</b> element</p>";
var validResult = HtmlSanitizer.validate(safeHtml);
verify(validResult.isValid);
compare(validResult.issues.length, 0);

var unsafeScript = '<p>Test</p><script>alert("xss")</script>';
var scriptResult = HtmlSanitizer.validate(unsafeScript);
verify(!scriptResult.isValid);
verify(scriptResult.issues.indexOf("Contains script tags") !== -1);

var unsafeEditor = '<div>window.editor.getContent()</div>';
var editorResult = HtmlSanitizer.validate(unsafeEditor);
verify(!editorResult.isValid);
verify(editorResult.issues.indexOf("Contains editor JavaScript code") !== -1);
}

function test_needsSanitization() {
verify(HtmlSanitizer.needsSanitization('<!DOCTYPE html><html><body>Test</body></html>'));
verify(HtmlSanitizer.needsSanitization('<p style="-qt-block-indent:0;">Qt paragraph</p>'));
verify(HtmlSanitizer.needsSanitization('<span style="color: rgb(0,0,0);">Color</span>'));
verify(HtmlSanitizer.needsSanitization('<font color="red">Font tag</font>'));
verify(HtmlSanitizer.needsSanitization('<h1 class="h1-fs">Odoo Heading</h1>'));

// Clean plain text or simple semantic HTML does not need sanitization
verify(!HtmlSanitizer.needsSanitization("<p>Plain simple paragraph</p>"));
verify(!HtmlSanitizer.needsSanitization(""));
verify(!HtmlSanitizer.needsSanitization(null));
}

function test_toPlainText() {
var html = '<p>Hello <b>World</b>!</p><br><p>Second &nbsp; line &amp; &lt;escaped&gt;</p>';
var text = HtmlSanitizer.toPlainText(html);
verify(text.indexOf("Hello World!") !== -1);
verify(text.indexOf("Second line & <escaped>") !== -1);
verify(text.indexOf("<p>") === -1);
verify(text.indexOf("<br>") === -1);
}

function test_sanitize_fullPipeline() {
var rawInput = '<!DOCTYPE html><html><body><p style="-qt-block-indent:0; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px;">Text with <del>deleted</del> and <font color="rgb(255, 0, 0)">red font</font></p></body></html>';
var clean = HtmlSanitizer.sanitize(rawInput);
verify(clean.indexOf("<!DOCTYPE") === -1);
verify(clean.indexOf("-qt-") === -1);
verify(clean.indexOf("<s>deleted</s>") !== -1);
verify(clean.indexOf("#ff0000") !== -1);
verify(clean.indexOf("<font") === -1);
}
}
}
Loading
Loading