MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 98.5% 607 / 0 / 616
Functions: 100.0% 61 / 0 / 61
Branches: 80.7% 1114 / 0 / 1380

presentation/WorkspaceState.cpp
Line Branch Exec Source
1 #include "WorkspaceState.h"
2 #include "SearchLog.h"
3 #include "ComposeLog.h"
4 #include <QUuid>
5 #include <QJsonArray>
6 #include <QSet>
7 #include <QSettings>
8 #include <algorithm>
9 #include <cmath>
10
11 1837 QString WorkspaceState::configuredReadingMode() {
12
3/3
✓ Branch 2 → 3 taken 1837 times.
✓ Branch 11 → 12 taken 1837 times.
✓ Branch 12 → 13 taken 1837 times.
5511 const QString mode = QSettings().value(QStringLiteral("view/defaultMode"), QStringLiteral("text")).toString();
13
5/6
✓ Branch 22 → 23 taken 1824 times.
✓ Branch 22 → 26 taken 13 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 1824 times.
✓ Branch 28 → 29 taken 13 times.
✓ Branch 28 → 31 taken 1824 times.
1837 if (mode == QLatin1String("light") || mode == QLatin1String("html")) return mode;
14 1824 return QStringLiteral("text");
15 1837 }
16
17 1797 WorkspaceState::WorkspaceState(QObject *parent)
18
1/1
✓ Branch 3 → 4 taken 1797 times.
5391 : QAbstractListModel(parent), m_configuredMode(configuredReadingMode()) {
19
1/3
✓ Branch 20 → 21 taken 1797 times.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
5391 Tab main{QStringLiteral("main"), tr("Inbox"), {}, m_configuredMode};
20 1797 main.kind = QStringLiteral("main");
21
1/1
✓ Branch 33 → 34 taken 1797 times.
1797 m_tabs.append(main);
22 1797 }
23
24 2 void WorkspaceState::setConfiguredReadingMode(const QString &mode) {
25
2/4
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 2 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 2 times.
2 const QString canonical = mode == QLatin1String("html-light") ? QStringLiteral("light") : mode;
26
3/6
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 20 taken 1 time.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 20 taken 1 time.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
2 if (canonical == QLatin1String("text") || canonical == QLatin1String("light") ||
27
1/2
✓ Branch 22 → 23 taken 2 times.
✗ Branch 22 → 24 not taken.
2 canonical == QLatin1String("html")) m_configuredMode = canonical;
28 2 }
29
30 24902 int WorkspaceState::rowCount(const QModelIndex &parent) const {
31
2/2
✓ Branch 3 → 4 taken 128 times.
✓ Branch 3 → 5 taken 24774 times.
24902 return parent.isValid() ? 0 : m_tabs.size();
32 }
33
34 8694 QVariant WorkspaceState::data(const QModelIndex &index, int role) const {
35
4/8
✓ Branch 3 → 4 taken 8694 times.
✗ Branch 3 → 9 not taken.
✓ Branch 5 → 6 taken 8694 times.
✗ Branch 5 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 8694 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 8694 times.
8694 if (!index.isValid() || index.row() < 0 || index.row() >= m_tabs.size()) return {};
36 8694 const auto &tab = m_tabs.at(index.row());
37
8/9
✓ Branch 15 → 16 taken 10 times.
✓ Branch 15 → 17 taken 1512 times.
✓ Branch 15 → 18 taken 833 times.
✗ Branch 15 → 22 not taken.
✓ Branch 15 → 23 taken 1343 times.
✓ Branch 15 → 25 taken 1343 times.
✓ Branch 15 → 26 taken 1367 times.
✓ Branch 15 → 29 taken 1396 times.
✓ Branch 15 → 30 taken 890 times.
8694 switch (role) {
38 10 case TabIdRole: return tab.id;
39 1512 case TitleRole: return tab.title;
40
1/1
✓ Branch 18 → 19 taken 833 times.
833 case IdentityRole: return encode(tab.mailId);
41 ✗ case ModeRole: return tab.mode;
42 1343 case ClosableRole: return index.row() != 0;
43 1343 case KindRole: return tab.kind;
44 1367 case DormantRole: return tabDormant(index.row());
45 1396 case TooltipRole: return tab.tooltip;
46 890 default: return {};
47 }
48 }
49
50 920 QHash<int, QByteArray> WorkspaceState::roleNames() const {
51 ✗ return {{TabIdRole, "tabId"}, {TitleRole, "title"}, {IdentityRole, "identity"},
52 ✗ {ModeRole, "readingMode"}, {ClosableRole, "closable"}, {KindRole, "kind"},
53
2/4
✓ Branch 12 → 13 taken 7360 times.
✓ Branch 12 → 14 taken 920 times.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
8280 {DormantRole, "dormant"}, {TooltipRole, "tooltip"}};
54 920 }
55
56 127880 QVariantMap WorkspaceState::encode(const MailId &id) {
57 ✗ return {{"accountId", id.accountId}, {"folderId", QString::number(id.folderId)},
58
2/4
✓ Branch 9 → 10 taken 383640 times.
✓ Branch 9 → 11 taken 127880 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
511520 {"uid", QString::number(id.uid)}};
59 127880 }
60
61 68906 MailId WorkspaceState::decode(const QVariantMap &map) {
62 // Accept strings only: JS numbers may already have lost precision.
63
8/16
✓ Branch 3 → 4 taken 68906 times.
✓ Branch 4 → 5 taken 68906 times.
✓ Branch 5 → 6 taken 68906 times.
✓ Branch 6 → 7 taken 68906 times.
✓ Branch 36 → 37 taken 68906 times.
✗ Branch 36 → 38 not taken.
✓ Branch 38 → 39 taken 68906 times.
✗ Branch 38 → 40 not taken.
✓ Branch 40 → 41 taken 5 times.
✓ Branch 40 → 42 taken 68901 times.
✗ Branch 104 → 105 not taken.
✗ Branch 104 → 106 not taken.
✗ Branch 108 → 109 not taken.
✗ Branch 108 → 110 not taken.
✗ Branch 112 → 113 not taken.
✗ Branch 112 → 114 not taken.
137812 if (map.value("accountId").metaType().id() != QMetaType::QString ||
64
12/20
✓ Branch 7 → 8 taken 68903 times.
✓ Branch 7 → 20 taken 3 times.
✓ Branch 9 → 10 taken 68903 times.
✓ Branch 10 → 11 taken 68903 times.
✓ Branch 11 → 12 taken 68903 times.
✓ Branch 12 → 13 taken 68903 times.
✓ Branch 13 → 14 taken 68903 times.
✗ Branch 13 → 20 not taken.
✓ Branch 30 → 31 taken 68903 times.
✓ Branch 30 → 32 taken 3 times.
✓ Branch 32 → 33 taken 68903 times.
✓ Branch 32 → 34 taken 3 times.
✓ Branch 34 → 35 taken 68906 times.
✗ Branch 34 → 36 not taken.
✗ Branch 91 → 92 not taken.
✗ Branch 91 → 93 not taken.
✗ Branch 95 → 96 not taken.
✗ Branch 95 → 97 not taken.
✗ Branch 99 → 100 not taken.
✗ Branch 99 → 101 not taken.
275621 map.value("folderId").metaType().id() != QMetaType::QString ||
65
14/20
✓ Branch 15 → 16 taken 68903 times.
✓ Branch 16 → 17 taken 68903 times.
✓ Branch 17 → 18 taken 68903 times.
✓ Branch 18 → 19 taken 68903 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 68901 times.
✓ Branch 22 → 23 taken 68903 times.
✓ Branch 22 → 24 taken 3 times.
✓ Branch 24 → 25 taken 68903 times.
✓ Branch 24 → 26 taken 3 times.
✓ Branch 26 → 27 taken 68903 times.
✓ Branch 26 → 28 taken 3 times.
✓ Branch 28 → 29 taken 68903 times.
✓ Branch 28 → 30 taken 3 times.
✗ Branch 78 → 79 not taken.
✗ Branch 78 → 80 not taken.
✗ Branch 82 → 83 not taken.
✗ Branch 82 → 84 not taken.
✗ Branch 86 → 87 not taken.
✗ Branch 86 → 88 not taken.
137814 map.value("uid").metaType().id() != QMetaType::QString) return {};
66 68901 bool folderOk = false, uidOk = false;
67
2/2
✓ Branch 43 → 44 taken 68901 times.
✓ Branch 44 → 45 taken 68901 times.
137802 MailId id{map.value("accountId").toString(),
68
4/4
✓ Branch 47 → 48 taken 68901 times.
✓ Branch 48 → 49 taken 68901 times.
✓ Branch 49 → 50 taken 68901 times.
✓ Branch 50 → 51 taken 68901 times.
137802 map.value("folderId").toString().toLongLong(&folderOk),
69
5/7
✓ Branch 45 → 46 taken 68901 times.
✓ Branch 52 → 53 taken 68901 times.
✓ Branch 53 → 54 taken 68901 times.
✓ Branch 54 → 55 taken 68901 times.
✓ Branch 55 → 56 taken 68901 times.
✗ Branch 140 → 141 not taken.
✗ Branch 140 → 142 not taken.
137802 map.value("uid").toString().toLongLong(&uidOk)};
70
5/6
✓ Branch 67 → 68 taken 68901 times.
✗ Branch 67 → 72 not taken.
✓ Branch 68 → 69 taken 68900 times.
✓ Branch 68 → 72 taken 1 time.
✓ Branch 70 → 71 taken 52323 times.
✓ Branch 70 → 72 taken 16577 times.
68901 return folderOk && uidOk && id.isValid() ? id : MailId{};
71 68901 }
72
73 15219 QVariantMap WorkspaceState::selection() const { return encode(m_selection); }
74
75 2699 void WorkspaceState::select(const MailId &id) {
76
2/2
✓ Branch 3 → 4 taken 1336 times.
✓ Branch 3 → 5 taken 1363 times.
2699 if (m_selection == id) return;
77 1363 const QString previousMode = readingMode();
78 1363 m_selection = id;
79 1363 ++m_generation;
80
1/1
✓ Branch 7 → 8 taken 1363 times.
1363 m_tabs[m_activeTab].mailId = id;
81
1/1
✓ Branch 9 → 10 taken 1363 times.
1363 m_tabs[m_activeTab].html = {};
82 // The main list follows the configured default reading mode until the
83 // user explicitly chose a mode there this session.
84
4/4
✓ Branch 12 → 13 taken 1235 times.
✓ Branch 12 → 19 taken 128 times.
✓ Branch 13 → 14 taken 1230 times.
✓ Branch 13 → 19 taken 5 times.
1363 if (m_activeTab == 0 && !m_mainModeTouched) {
85 1230 const QString &defaultMode = m_configuredMode;
86
2/4
✓ Branch 14 → 15 taken 1230 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 19 taken 1230 times.
✗ Branch 17 → 18 not taken.
1230 if (m_tabs[0].mode != defaultMode) m_tabs[0].mode = defaultMode;
87 }
88 1363 const bool modeChanged = readingMode() != previousMode;
89
3/3
✓ Branch 26 → 27 taken 1363 times.
✓ Branch 28 → 29 taken 1363 times.
✓ Branch 29 → 30 taken 1363 times.
1363 emit dataChanged(index(m_activeTab), index(m_activeTab),
90
2/4
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 1363 times.
✗ Branch 23 → 25 not taken.
✓ Branch 24 → 25 taken 1363 times.
2726 modeChanged ? QList<int>{IdentityRole, ModeRole} : QList<int>{IdentityRole});
91
1/3
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 1363 times.
✗ Branch 32 → 33 not taken.
1363 if (modeChanged) emit readingModeChanged();
92
1/1
✓ Branch 33 → 34 taken 1363 times.
1363 emit selectionChanged();
93 1363 }
94
95 1010 bool WorkspaceState::selectIdentity(const QVariantMap &identity) {
96
1/1
✓ Branch 2 → 3 taken 1010 times.
1010 const auto id = decode(identity);
97
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 1009 times.
1010 if (!id.isValid()) return false;
98
1/1
✓ Branch 6 → 7 taken 1009 times.
1009 select(id);
99 1009 return true;
100 1010 }
101
102 10 bool WorkspaceState::replaceActiveMail(const MailId &id, const QString &title) {
103
5/6
✓ Branch 2 → 3 taken 9 times.
✓ Branch 2 → 5 taken 1 time.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 9 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 9 times.
10 if (m_activeTab == 0 || !id.isValid()) return false;
104 9 auto &tab = m_tabs[m_activeTab];
105
1/1
✓ Branch 10 → 11 taken 9 times.
9 tab.title = title.left(1024);
106 // Keep the logical tab ID and reading mode; only this tab's mail changes.
107 9 const bool changed = m_selection != id;
108
1/2
✓ Branch 14 → 15 taken 9 times.
✗ Branch 14 → 18 not taken.
9 if (changed) tab.html = {};
109 9 tab.mailId = id;
110 9 m_selection = id;
111
1/2
✓ Branch 20 → 21 taken 9 times.
✗ Branch 20 → 22 not taken.
9 if (changed) ++m_generation;
112
4/4
✓ Branch 22 → 23 taken 9 times.
✓ Branch 24 → 25 taken 9 times.
✓ Branch 26 → 27 taken 9 times.
✓ Branch 27 → 28 taken 9 times.
9 emit dataChanged(index(m_activeTab), index(m_activeTab), {TitleRole, IdentityRole});
113
1/2
✓ Branch 29 → 30 taken 9 times.
✗ Branch 29 → 31 not taken.
9 if (changed) emit selectionChanged();
114 9 return true;
115 }
116
117 6 bool WorkspaceState::accepts(const MailId &id, quint64 generation) const {
118
5/6
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 8 taken 2 times.
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 8 taken 1 time.
6 return id.isValid() && id == m_selection && generation == m_generation;
119 }
120
121 1300 void WorkspaceState::setLayout(const QString &layout) {
122
8/8
✓ Branch 3 → 4 taken 291 times.
✓ Branch 3 → 6 taken 1009 times.
✓ Branch 5 → 6 taken 290 times.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 7 → 8 taken 808 times.
✓ Branch 7 → 9 taken 491 times.
✓ Branch 10 → 11 taken 809 times.
✓ Branch 10 → 12 taken 491 times.
1300 if ((layout != "classic" && layout != "wide") || layout == m_layout) return;
123 491 m_layout = layout;
124 491 emit layoutChanged();
125 }
126
127 224 void WorkspaceState::setReaderFocused(bool focused) {
128
2/2
✓ Branch 2 → 3 taken 77 times.
✓ Branch 2 → 4 taken 147 times.
224 if (focused == m_readerFocused) return;
129 147 m_readerFocused = focused;
130 147 emit readerFocusedChanged();
131 }
132
133 935 void WorkspaceState::setArea(const QString &area) {
134 // F95-02: "rules" is the server-rule manager's own area; Sprint 100
135 // (D-1): "settings" is the settings area. Neither is restored from a
136 // session (restoreSession keeps the four PIM areas).
137
12/12
✓ Branch 3 → 4 taken 680 times.
✓ Branch 3 → 16 taken 255 times.
✓ Branch 5 → 6 taken 506 times.
✓ Branch 5 → 17 taken 174 times.
✓ Branch 7 → 8 taken 365 times.
✓ Branch 7 → 17 taken 141 times.
✓ Branch 9 → 10 taken 275 times.
✓ Branch 9 → 17 taken 90 times.
✓ Branch 11 → 12 taken 187 times.
✓ Branch 11 → 17 taken 88 times.
✓ Branch 13 → 14 taken 159 times.
✓ Branch 13 → 17 taken 28 times.
1094 if (area == m_area || (area != "mail" && area != "calendar" && area != "tasks" && area != "contacts" && area != "rules"
138
3/4
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 159 times.
✓ Branch 18 → 19 taken 255 times.
✓ Branch 18 → 20 taken 680 times.
1094 && area != "settings")) return;
139 680 m_area = area;
140 680 emit areaChanged();
141 }
142
143 323 int WorkspaceState::openMail(const QVariantMap &identity, const QString &title) {
144
1/1
✓ Branch 2 → 3 taken 323 times.
323 auto id = decode(identity);
145
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 323 times.
323 if (!id.isValid()) return -1;
146
2/2
✓ Branch 21 → 7 taken 9185 times.
✓ Branch 21 → 22 taken 323 times.
9508 for (int i = 1; i < m_tabs.size(); ++i) {
147
6/9
✓ Branch 8 → 9 taken 9185 times.
✓ Branch 10 → 11 taken 9182 times.
✓ Branch 10 → 15 taken 3 times.
✓ Branch 11 → 12 taken 9182 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 9182 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 19 taken 9185 times.
✗ Branch 17 → 18 not taken.
9185 if (m_tabs[i].kind == QLatin1String("mail") && m_tabs[i].mailId == id) { activateTab(i); return i; }
148 }
149
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 25 taken 322 times.
323 if (m_tabs.size() >= MaximumTabs) return -1;
150 322 const int row = m_tabs.size();
151
1/1
✓ Branch 27 → 28 taken 322 times.
322 beginInsertRows({}, row, row);
152 322 m_tabs.append({QUuid::createUuid().toString(QUuid::WithoutBraces), title.left(1024), id,
153 322 m_configuredMode});
154
1/1
✓ Branch 39 → 40 taken 322 times.
322 endInsertRows();
155
1/1
✓ Branch 40 → 41 taken 322 times.
322 activateTab(row);
156 322 return row;
157 967 }
158
159 738 bool WorkspaceState::activateTab(int tab) {
160
5/6
✓ Branch 2 → 3 taken 738 times.
✗ Branch 2 → 5 not taken.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 737 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 737 times.
738 if (tab < 0 || tab >= m_tabs.size()) return false;
161
1/1
✓ Branch 9 → 10 taken 737 times.
737 m_tabs[tab].used = ++m_useClock;
162
2/2
✓ Branch 10 → 11 taken 133 times.
✓ Branch 10 → 12 taken 604 times.
737 if (tab == m_activeTab) return true;
163 604 const QString previousMode = readingMode();
164
1/1
✓ Branch 13 → 14 taken 604 times.
604 m_previousTabId = m_tabs[m_activeTab].id;
165 604 m_activeTab = tab;
166 // Even identical mail in a different tab invalidates callbacks for the old owner.
167
1/1
✓ Branch 15 → 16 taken 604 times.
604 m_selection = m_tabs[tab].mailId;
168 604 ++m_generation;
169
1/1
✓ Branch 17 → 18 taken 604 times.
604 emit activeTabChanged();
170
3/3
✓ Branch 21 → 22 taken 27 times.
✓ Branch 21 → 23 taken 577 times.
✓ Branch 22 → 23 taken 27 times.
604 if (readingMode() != previousMode) emit readingModeChanged();
171
1/1
✓ Branch 23 → 24 taken 604 times.
604 emit selectionChanged();
172 604 return true;
173 604 }
174
175 93 bool WorkspaceState::closeTab(int tab) {
176
5/6
✓ Branch 2 → 3 taken 72 times.
✓ Branch 2 → 5 taken 21 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 72 times.
✓ Branch 7 → 8 taken 21 times.
✓ Branch 7 → 9 taken 72 times.
93 if (tab <= 0 || tab >= m_tabs.size()) return false;
177 72 const QString previousMode = readingMode();
178 72 const bool closingActive = tab == m_activeTab;
179 72 const Tab closed = m_tabs.at(tab);
180
1/1
✓ Branch 13 → 14 taken 72 times.
72 beginRemoveRows({}, tab, tab);
181
1/1
✓ Branch 14 → 15 taken 72 times.
72 m_tabs.removeAt(tab);
182
1/1
✓ Branch 15 → 16 taken 72 times.
72 endRemoveRows();
183
4/4
✓ Branch 16 → 17 taken 14 times.
✓ Branch 16 → 18 taken 58 times.
✓ Branch 17 → 18 taken 9 times.
✓ Branch 17 → 52 taken 5 times.
72 if (closingActive || m_activeTab > tab) {
184 // D-31: closing the active search tab returns to the tab used before it;
185 // so does closing a compose tab (S108.9.1: sending returns to the mail).
186
5/6
✓ Branch 20 → 21 taken 53 times.
✓ Branch 20 → 27 taken 14 times.
✓ Branch 23 → 24 taken 16 times.
✓ Branch 23 → 27 taken 37 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 16 times.
83 const bool returns = closed.kind == QLatin1String("search") || closed.kind == QLatin1String("compose") ||
187 16 closed.kind == QLatin1String("outbox");
188
2/2
✓ Branch 29 → 30 taken 51 times.
✓ Branch 29 → 31 taken 16 times.
67 const int previous = returns ? rowForTabId(m_previousTabId) : -1;
189
4/4
✓ Branch 32 → 33 taken 58 times.
✓ Branch 32 → 39 taken 9 times.
✓ Branch 33 → 34 taken 16 times.
✓ Branch 33 → 37 taken 42 times.
67 m_activeTab = closingActive ? (previous >= 0 ? previous : qMin(tab, int(m_tabs.size()) - 1)) : m_activeTab - 1;
190
2/2
✓ Branch 40 → 41 taken 58 times.
✓ Branch 40 → 44 taken 9 times.
67 if (closingActive) {
191
1/1
✓ Branch 41 → 42 taken 58 times.
58 m_selection = m_tabs[m_activeTab].mailId;
192 58 ++m_generation;
193 }
194
1/1
✓ Branch 44 → 45 taken 67 times.
67 emit activeTabChanged();
195
3/3
✓ Branch 48 → 49 taken 2 times.
✓ Branch 48 → 50 taken 65 times.
✓ Branch 49 → 50 taken 2 times.
67 if (readingMode() != previousMode) emit readingModeChanged();
196
3/3
✓ Branch 50 → 51 taken 58 times.
✓ Branch 50 → 52 taken 9 times.
✓ Branch 51 → 52 taken 58 times.
67 if (closingActive) emit selectionChanged();
197 }
198
1/1
✓ Branch 52 → 53 taken 72 times.
72 emit tabRemoved(closed.id, closed.kind);
199 72 return true;
200 72 }
201
202 17 void WorkspaceState::invalidateFolder(const QString &accountId, qint64 folderId) {
203 44 const auto matches = [&](const Tab &tab) {
204
6/6
✓ Branch 3 → 4 taken 34 times.
✓ Branch 3 → 8 taken 10 times.
✓ Branch 5 → 6 taken 30 times.
✓ Branch 5 → 8 taken 4 times.
✓ Branch 6 → 7 taken 21 times.
✓ Branch 6 → 8 taken 9 times.
44 return tab.mailId.isValid() && tab.mailId.accountId == accountId && tab.mailId.folderId == folderId;
205 17 };
206
3/3
✓ Branch 4 → 5 taken 17 times.
✓ Branch 5 → 6 taken 9 times.
✓ Branch 5 → 7 taken 8 times.
17 if (std::none_of(m_tabs.cbegin(), m_tabs.cend(), matches)) return;
207 8 const QString previousMode = readingMode();
208
1/1
✓ Branch 8 → 9 taken 8 times.
8 const auto activeId = m_tabs[m_activeTab].id;
209 8 const int previousIndex = m_activeTab;
210 8 const auto before = m_tabs;
211
1/1
✓ Branch 11 → 12 taken 8 times.
8 beginResetModel();
212
4/4
✓ Branch 12 → 13 taken 8 times.
✓ Branch 13 → 14 taken 8 times.
✓ Branch 31 → 15 taken 20 times.
✓ Branch 31 → 32 taken 8 times.
28 for (auto &tab : m_tabs)
213
6/6
✓ Branch 18 → 19 taken 9 times.
✓ Branch 18 → 22 taken 11 times.
✓ Branch 20 → 21 taken 6 times.
✓ Branch 20 → 22 taken 3 times.
✓ Branch 23 → 24 taken 6 times.
✓ Branch 23 → 29 taken 14 times.
20 if (tab.kind != QLatin1String("mail") && matches(tab)) { tab.mailId = {}; tab.html = {}; }
214
2/2
✓ Branch 46 → 34 taken 12 times.
✓ Branch 46 → 47 taken 8 times.
20 for (int row = int(m_tabs.size()) - 1; row > 0; --row)
215
9/9
✓ Branch 35 → 36 taken 12 times.
✓ Branch 37 → 38 taken 11 times.
✓ Branch 37 → 42 taken 1 time.
✓ Branch 38 → 39 taken 11 times.
✓ Branch 40 → 41 taken 7 times.
✓ Branch 40 → 42 taken 4 times.
✓ Branch 43 → 44 taken 7 times.
✓ Branch 43 → 45 taken 5 times.
✓ Branch 44 → 45 taken 7 times.
12 if (m_tabs[row].kind == QLatin1String("mail") && matches(m_tabs[row])) m_tabs.removeAt(row);
216 8 m_activeTab = 0;
217
2/2
✓ Branch 54 → 48 taken 13 times.
✓ Branch 54 → 55 taken 8 times.
21 for (int row = 0; row < m_tabs.size(); ++row)
218
3/3
✓ Branch 48 → 49 taken 13 times.
✓ Branch 50 → 51 taken 6 times.
✓ Branch 50 → 52 taken 7 times.
13 if (m_tabs[row].id == activeId) m_activeTab = row;
219
5/6
✓ Branch 55 → 56 taken 8 times.
✓ Branch 57 → 58 taken 2 times.
✓ Branch 57 → 61 taken 6 times.
✓ Branch 58 → 59 taken 2 times.
✗ Branch 60 → 61 not taken.
✓ Branch 60 → 62 taken 2 times.
8 const bool selectionChangedValue = m_selection != m_tabs[m_activeTab].mailId || m_tabs[m_activeTab].id != activeId;
220
1/1
✓ Branch 63 → 64 taken 8 times.
8 m_selection = m_tabs[m_activeTab].mailId;
221
2/2
✓ Branch 65 → 66 taken 6 times.
✓ Branch 65 → 67 taken 2 times.
8 if (selectionChangedValue) ++m_generation;
222
1/1
✓ Branch 67 → 68 taken 8 times.
8 endResetModel();
223
7/8
✓ Branch 68 → 69 taken 5 times.
✓ Branch 68 → 72 taken 3 times.
✓ Branch 69 → 70 taken 5 times.
✗ Branch 71 → 72 not taken.
✓ Branch 71 → 73 taken 5 times.
✓ Branch 74 → 75 taken 3 times.
✓ Branch 74 → 76 taken 5 times.
✓ Branch 75 → 76 taken 3 times.
8 if (m_activeTab != previousIndex || m_tabs[m_activeTab].id != activeId) emit activeTabChanged();
224
1/3
✗ Branch 79 → 80 not taken.
✓ Branch 79 → 81 taken 8 times.
✗ Branch 80 → 81 not taken.
8 if (readingMode() != previousMode) emit readingModeChanged();
225
3/3
✓ Branch 81 → 82 taken 6 times.
✓ Branch 81 → 83 taken 2 times.
✓ Branch 82 → 83 taken 6 times.
8 if (selectionChangedValue) emit selectionChanged();
226
1/1
✓ Branch 83 → 84 taken 8 times.
8 emitRemoved(before);
227 8 }
228
229 1 void WorkspaceState::cycleTabs(int offset) {
230 1 activateTab(((m_activeTab + offset) % m_tabs.size() + m_tabs.size()) % m_tabs.size());
231 1 }
232
233 91 bool WorkspaceState::setReadingMode(const QString &mode) {
234
4/4
✓ Branch 3 → 4 taken 9 times.
✓ Branch 3 → 7 taken 82 times.
✓ Branch 8 → 9 taken 9 times.
✓ Branch 8 → 10 taken 82 times.
100 const QString canonical = mode == "html-light" ? QStringLiteral("light") : mode;
235
6/8
✓ Branch 11 → 12 taken 55 times.
✓ Branch 11 → 17 taken 36 times.
✓ Branch 13 → 14 taken 11 times.
✓ Branch 13 → 17 taken 44 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 11 times.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 91 times.
91 if (canonical != "text" && canonical != "light" && canonical != "html") return false;
236
3/3
✓ Branch 20 → 21 taken 91 times.
✓ Branch 22 → 23 taken 25 times.
✓ Branch 22 → 24 taken 66 times.
91 if (m_tabs[m_activeTab].mode == canonical) return true;
237
2/2
✓ Branch 24 → 25 taken 48 times.
✓ Branch 24 → 26 taken 18 times.
66 if (m_activeTab == 0) m_mainModeTouched = true;
238
1/1
✓ Branch 26 → 27 taken 66 times.
66 m_tabs[m_activeTab].mode = canonical;
239
4/4
✓ Branch 28 → 29 taken 66 times.
✓ Branch 30 → 31 taken 66 times.
✓ Branch 32 → 33 taken 66 times.
✓ Branch 33 → 34 taken 66 times.
66 emit dataChanged(index(m_activeTab), index(m_activeTab), {ModeRole});
240
1/1
✓ Branch 35 → 36 taken 66 times.
66 emit readingModeChanged();
241 66 return true;
242 91 }
243
244 8 static bool validReaderPosition(const QPointF &position) {
245
3/4
✓ Branch 7 → 8 taken 7 times.
✓ Branch 7 → 16 taken 1 time.
✓ Branch 10 → 11 taken 7 times.
✗ Branch 10 → 16 not taken.
23 return std::isfinite(position.x()) && std::isfinite(position.y()) &&
246
5/6
✓ Branch 4 → 5 taken 8 times.
✗ Branch 4 → 16 not taken.
✓ Branch 12 → 13 taken 6 times.
✓ Branch 12 → 16 taken 1 time.
✓ Branch 14 → 15 taken 5 times.
✓ Branch 14 → 16 taken 1 time.
23 std::abs(position.x()) <= 1e9 && position.y() >= 0 && position.y() <= 1e9;
247 }
248
249 26 WorkspaceState::Tab *WorkspaceState::findTab(const QString &tabId, const MailId &id) {
250
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 26 times.
26 if (!id.isValid()) return nullptr;
251
10/10
✓ Branch 5 → 6 taken 26 times.
✓ Branch 6 → 7 taken 26 times.
✓ Branch 10 → 11 taken 25 times.
✓ Branch 10 → 14 taken 9 times.
✓ Branch 12 → 13 taken 22 times.
✓ Branch 12 → 14 taken 3 times.
✓ Branch 15 → 16 taken 22 times.
✓ Branch 15 → 17 taken 12 times.
✓ Branch 19 → 8 taken 34 times.
✓ Branch 19 → 20 taken 4 times.
38 for (auto &tab : m_tabs) if (tab.id == tabId && tab.mailId == id) return &tab;
252 4 return nullptr;
253 }
254
255 10 WorkspaceState::HtmlReadingState WorkspaceState::bindHtmlDocument(const QString &tabId, const MailId &id,
256 quint32 epoch, const QByteArray &digest) {
257
1/1
✓ Branch 2 → 3 taken 10 times.
10 auto *tab = findTab(tabId, id);
258
3/6
✓ Branch 3 → 4 taken 10 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 10 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 10 times.
10 if (!tab || digest.size() != 32) return {};
259
6/6
✓ Branch 10 → 11 taken 6 times.
✓ Branch 10 → 14 taken 4 times.
✓ Branch 12 → 13 taken 5 times.
✓ Branch 12 → 14 taken 1 time.
✓ Branch 15 → 16 taken 5 times.
✓ Branch 15 → 17 taken 5 times.
10 if (tab->html.uidValidity == epoch && tab->html.digest == digest) return tab->html;
260 5 tab->html = {epoch, digest, {}, false};
261 5 const auto state = tab->html;
262
1/1
✓ Branch 21 → 22 taken 5 times.
5 emit readerStateChanged();
263 5 return state;
264 10 }
265
266 9 bool WorkspaceState::rememberHtmlScroll(const QString &tabId, const MailId &id, quint32 epoch,
267 const QByteArray &digest, const QPointF &position) {
268 9 auto *tab = findTab(tabId, id);
269
8/10
✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 11 taken 2 times.
✓ Branch 5 → 6 taken 7 times.
✗ Branch 5 → 11 not taken.
✓ Branch 6 → 7 taken 7 times.
✗ Branch 6 → 11 not taken.
✓ Branch 8 → 9 taken 6 times.
✓ Branch 8 → 11 taken 1 time.
✓ Branch 13 → 14 taken 6 times.
✓ Branch 13 → 15 taken 3 times.
15 if (!tab || digest.size() != 32 || tab->html.uidValidity != epoch || tab->html.digest != digest ||
270
2/2
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 12 taken 3 times.
12 !validReaderPosition(position)) return false;
271
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 2 times.
3 if (tab->html.scroll == position) return true;
272 2 tab->html.scroll = position;
273 2 emit readerStateChanged();
274 2 return true;
275 }
276
277 7 bool WorkspaceState::allowHtmlExternal(const QString &tabId, const MailId &id, quint32 epoch, const QByteArray &digest) {
278 7 auto *tab = findTab(tabId, id);
279
8/10
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 9 taken 2 times.
✓ Branch 5 → 6 taken 5 times.
✗ Branch 5 → 9 not taken.
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 9 taken 1 time.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 4 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 4 times.
7 if (!tab || digest.size() != 32 || tab->html.uidValidity != epoch || tab->html.digest != digest) return false;
280 4 tab->html.allowExternal = true;
281 4 return true;
282 }
283
284 801 void WorkspaceState::clearHtmlConsent() {
285
4/4
✓ Branch 2 → 3 taken 801 times.
✓ Branch 3 → 4 taken 801 times.
✓ Branch 8 → 5 taken 819 times.
✓ Branch 8 → 9 taken 801 times.
1620 for (auto &tab : m_tabs) tab.html.allowExternal = false;
286 801 }
287
288 835 void WorkspaceState::retainMailIdentities(const std::function<bool(const MailId &)> &acceptIdentity) {
289 835 auto retained = m_tabs;
290 835 bool changed = false;
291 // Tab 0 and search tabs stay (D-15); only their selected mail is cleared.
292
4/4
✓ Branch 3 → 4 taken 835 times.
✓ Branch 4 → 5 taken 835 times.
✓ Branch 24 → 6 taken 854 times.
✓ Branch 24 → 25 taken 835 times.
1689 for (auto &tab : retained)
293
9/9
✓ Branch 9 → 10 taken 846 times.
✓ Branch 9 → 15 taken 8 times.
✓ Branch 11 → 12 taken 17 times.
✓ Branch 11 → 15 taken 829 times.
✓ Branch 12 → 13 taken 17 times.
✓ Branch 13 → 14 taken 10 times.
✓ Branch 13 → 15 taken 7 times.
✓ Branch 16 → 17 taken 10 times.
✓ Branch 16 → 22 taken 844 times.
854 if (tab.kind != QLatin1String("mail") && tab.mailId.isValid() && !acceptIdentity(tab.mailId)) {
294 10 tab.mailId = {}; tab.html = {}; changed = true;
295 }
296
2/2
✓ Branch 40 → 27 taken 19 times.
✓ Branch 40 → 41 taken 835 times.
854 for (int row = retained.size() - 1; row > 0; --row)
297
9/9
✓ Branch 28 → 29 taken 19 times.
✓ Branch 30 → 31 taken 8 times.
✓ Branch 30 → 35 taken 11 times.
✓ Branch 31 → 32 taken 8 times.
✓ Branch 32 → 33 taken 8 times.
✓ Branch 33 → 34 taken 7 times.
✓ Branch 33 → 35 taken 1 time.
✓ Branch 36 → 37 taken 7 times.
✓ Branch 36 → 39 taken 12 times.
19 if (retained[row].kind == QLatin1String("mail") && !acceptIdentity(retained[row].mailId)) {
298
1/1
✓ Branch 37 → 38 taken 7 times.
7 retained.removeAt(row); changed = true;
299 }
300
2/2
✓ Branch 41 → 42 taken 826 times.
✓ Branch 41 → 43 taken 9 times.
835 if (!changed) return;
301 9 const QString previousMode = readingMode();
302 9 const auto oldTabId = activeTabId();
303 9 const int oldIndex = m_activeTab;
304 9 int active = 0;
305
5/5
✓ Branch 46 → 47 taken 13 times.
✓ Branch 48 → 49 taken 5 times.
✓ Branch 48 → 50 taken 8 times.
✓ Branch 52 → 46 taken 13 times.
✓ Branch 52 → 53 taken 9 times.
22 for (int row = 0; row < retained.size(); ++row) if (retained[row].id == oldTabId) active = row;
306
6/6
✓ Branch 53 → 54 taken 9 times.
✓ Branch 55 → 56 taken 5 times.
✓ Branch 55 → 59 taken 4 times.
✓ Branch 56 → 57 taken 5 times.
✓ Branch 58 → 59 taken 4 times.
✓ Branch 58 → 60 taken 1 time.
9 const bool ownerChanged = retained[active].id != oldTabId || retained[active].mailId != m_selection;
307 9 const auto before = m_tabs;
308
1/1
✓ Branch 62 → 63 taken 9 times.
9 beginResetModel();
309 9 m_tabs = std::move(retained);
310 9 m_activeTab = active;
311
1/1
✓ Branch 65 → 66 taken 9 times.
9 m_selection = m_tabs[active].mailId;
312
2/2
✓ Branch 67 → 68 taken 8 times.
✓ Branch 67 → 69 taken 1 time.
9 if (ownerChanged) ++m_generation;
313
1/1
✓ Branch 69 → 70 taken 9 times.
9 endResetModel();
314
5/5
✓ Branch 70 → 71 taken 4 times.
✓ Branch 70 → 72 taken 5 times.
✓ Branch 71 → 72 taken 3 times.
✓ Branch 71 → 73 taken 1 time.
✓ Branch 72 → 73 taken 8 times.
9 if (oldIndex != m_activeTab || ownerChanged) emit activeTabChanged();
315
1/3
✗ Branch 76 → 77 not taken.
✓ Branch 76 → 78 taken 9 times.
✗ Branch 77 → 78 not taken.
9 if (readingMode() != previousMode) emit readingModeChanged();
316
3/3
✓ Branch 78 → 79 taken 8 times.
✓ Branch 78 → 80 taken 1 time.
✓ Branch 79 → 80 taken 8 times.
9 if (ownerChanged) emit selectionChanged();
317
1/1
✓ Branch 80 → 81 taken 9 times.
9 emitRemoved(before);
318 835 }
319
320 1020 static bool validFolderExpansion(const QMap<QString, QStringList> &accounts) {
321
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 1019 times.
1020 if (accounts.size() > 64) return false;
322 1019 qsizetype characters = 0, paths = 0;
323
4/4
✓ Branch 5 → 6 taken 1019 times.
✓ Branch 47 → 48 taken 3148 times.
✓ Branch 49 → 7 taken 2130 times.
✓ Branch 49 → 50 taken 1018 times.
3148 for (auto it = accounts.cbegin(); it != accounts.cend(); ++it) {
324
4/8
✓ Branch 9 → 10 taken 2130 times.
✗ Branch 9 → 16 not taken.
✓ Branch 12 → 13 taken 2130 times.
✗ Branch 12 → 16 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 2130 times.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 2130 times.
2131 if (it.key().isEmpty() || it.key().size() > 1024 || it.value().size() > 4096) return false;
325 2130 characters += it.key().size();
326 2130 paths += it.value().size();
327
2/2
✓ Branch 41 → 27 taken 2287 times.
✓ Branch 41 → 42 taken 2129 times.
4416 for (const auto &path : it.value()) {
328
3/6
✓ Branch 29 → 30 taken 2287 times.
✗ Branch 29 → 32 not taken.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 2287 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 2287 times.
2288 if (path.isEmpty() || path.size() > 4096) return false;
329 2287 characters += path.size();
330
2/2
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 39 taken 2286 times.
2287 if (characters > 65536) return false;
331 }
332
2/4
✓ Branch 42 → 44 taken 2129 times.
✗ Branch 42 → 45 not taken.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 46 taken 2129 times.
2129 if (paths > 4096 || characters > 65536) return false;
333 }
334 1018 return true;
335 }
336
337 111 bool WorkspaceState::setFolderExpanded(const QString &account, const QString &path, bool expanded) {
338
8/10
✓ Branch 3 → 4 taken 110 times.
✓ Branch 3 → 10 taken 1 time.
✓ Branch 5 → 6 taken 110 times.
✗ Branch 5 → 10 not taken.
✓ Branch 7 → 8 taken 110 times.
✗ Branch 7 → 10 not taken.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 109 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 14 taken 109 times.
111 if (account.isEmpty() || account.size() > 1024 || path.isEmpty() || path.size() > 4096) return false;
339 109 auto accounts = m_expandedFolders;
340
1/1
✓ Branch 16 → 17 taken 109 times.
109 auto paths = accounts.value(account);
341
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 109 times.
109 if (paths.contains(path) == expanded) return true;
342
4/4
✓ Branch 21 → 22 taken 101 times.
✓ Branch 21 → 24 taken 8 times.
✓ Branch 22 → 23 taken 101 times.
✓ Branch 23 → 25 taken 101 times.
109 if (expanded) { paths.append(path); paths.sort(); }
343
1/1
✓ Branch 24 → 25 taken 8 times.
8 else paths.removeAll(path);
344
3/3
✓ Branch 26 → 27 taken 6 times.
✓ Branch 26 → 28 taken 103 times.
✓ Branch 27 → 30 taken 6 times.
109 if (paths.isEmpty()) accounts.remove(account);
345
1/1
✓ Branch 28 → 29 taken 103 times.
103 else accounts[account] = paths;
346
3/3
✓ Branch 30 → 31 taken 109 times.
✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 33 taken 107 times.
109 if (!validFolderExpansion(accounts)) return false;
347 107 m_expandedFolders = std::move(accounts);
348
1/1
✓ Branch 35 → 36 taken 107 times.
107 emit folderExpansionChanged();
349 107 return true;
350 109 }
351
352 34 void WorkspaceState::retainFolderBranches(const QString &account, const QStringList &paths) {
353
3/3
✓ Branch 2 → 3 taken 34 times.
✓ Branch 3 → 4 taken 20 times.
✓ Branch 3 → 5 taken 14 times.
40 if (!m_expandedFolders.contains(account)) return;
354
1/1
✓ Branch 7 → 8 taken 14 times.
14 const QSet<QString> valid(paths.begin(), paths.end());
355
1/1
✓ Branch 9 → 10 taken 14 times.
14 auto retained = m_expandedFolders.value(account);
356
1/1
✓ Branch 11 → 12 taken 14 times.
29 retained.removeIf([&](const QString &path) { return !valid.contains(path); });
357
4/4
✓ Branch 13 → 14 taken 14 times.
✓ Branch 14 → 15 taken 14 times.
✓ Branch 17 → 18 taken 6 times.
✓ Branch 17 → 19 taken 8 times.
14 if (retained == m_expandedFolders.value(account)) return;
358
2/3
✓ Branch 20 → 21 taken 8 times.
✗ Branch 20 → 22 not taken.
✓ Branch 21 → 24 taken 8 times.
8 if (retained.isEmpty()) m_expandedFolders.remove(account);
359 ✗ else m_expandedFolders[account] = retained;
360
1/1
✓ Branch 24 → 25 taken 8 times.
8 emit folderExpansionChanged();
361 20 }
362
363 771 QJsonObject WorkspaceState::saveSession() const {
364
1/1
✓ Branch 2 → 3 taken 771 times.
771 QJsonArray tabs;
365
2/2
✓ Branch 82 → 5 taken 1544 times.
✓ Branch 82 → 83 taken 771 times.
2315 for (const auto &tab : m_tabs) {
366 4632 QJsonObject entry{{"id", tab.id}, {"title", tab.title}, {"kind", tab.kind},
367
3/5
✓ Branch 10 → 11 taken 1544 times.
✓ Branch 15 → 16 taken 7720 times.
✓ Branch 15 → 17 taken 1544 times.
✗ Branch 112 → 113 not taken.
✗ Branch 112 → 114 not taken.
10808 {"identity", QJsonObject::fromVariantMap(encode(tab.mailId))}, {"readingMode", tab.mode}};
368
2/2
✓ Branch 21 → 22 taken 60 times.
✓ Branch 21 → 42 taken 1484 times.
1544 if (tab.kind == QLatin1String("search")) {
369 // D-16: the query and account; the zoom is the query's date part.
370
3/3
✓ Branch 22 → 23 taken 60 times.
✓ Branch 23 → 24 taken 60 times.
✓ Branch 24 → 25 taken 60 times.
60 entry.insert("query", tab.query);
371
3/3
✓ Branch 27 → 28 taken 60 times.
✓ Branch 28 → 29 taken 60 times.
✓ Branch 29 → 30 taken 60 times.
60 entry.insert("accountId", tab.accountId);
372
4/4
✓ Branch 32 → 33 taken 60 times.
✓ Branch 33 → 34 taken 60 times.
✓ Branch 35 → 36 taken 60 times.
✓ Branch 36 → 37 taken 60 times.
60 entry.insert("anchor", QJsonObject::fromVariantMap(encode(tab.anchor)));
373 }
374
5/5
✓ Branch 44 → 45 taken 58 times.
✓ Branch 44 → 51 taken 1486 times.
✓ Branch 45 → 46 taken 58 times.
✓ Branch 46 → 47 taken 58 times.
✓ Branch 47 → 48 taken 58 times.
1544 if (tab.kind == QLatin1String("compose")) entry.insert("sessionId", tab.sessionId);
375
2/2
✓ Branch 52 → 53 taken 10 times.
✓ Branch 52 → 76 taken 1534 times.
1544 if (tab.html.digest.size() == 32)
376
2/2
✓ Branch 64 → 65 taken 10 times.
✓ Branch 65 → 66 taken 10 times.
10 entry.insert("htmlReading", QJsonObject{{"epoch", QString::number(tab.html.uidValidity)},
377
1/1
✓ Branch 56 → 57 taken 10 times.
20 {"digest", QString::fromLatin1(tab.html.digest.toHex())},
378
2/4
✓ Branch 70 → 71 taken 40 times.
✓ Branch 70 → 72 taken 10 times.
✗ Branch 165 → 166 not taken.
✗ Branch 165 → 167 not taken.
60 {"x", tab.html.scroll.x()}, {"y", tab.html.scroll.y()}});
379
2/2
✓ Branch 76 → 77 taken 1544 times.
✓ Branch 77 → 78 taken 1544 times.
1544 tabs.append(entry);
380 1544 }
381 // Layout preferences belong to UiStateStore, not to the domain session.
382
1/1
✓ Branch 83 → 84 taken 771 times.
771 QJsonObject expanded;
383
4/4
✓ Branch 84 → 85 taken 771 times.
✓ Branch 94 → 95 taken 980 times.
✓ Branch 96 → 86 taken 209 times.
✓ Branch 96 → 97 taken 771 times.
980 for (auto it = m_expandedFolders.cbegin(); it != m_expandedFolders.cend(); ++it)
384
2/2
✓ Branch 87 → 88 taken 209 times.
✓ Branch 90 → 91 taken 209 times.
209 expanded.insert(it.key(), QJsonArray::fromStringList(it.value()));
385 ✗ return {{"schemaVersion", 2}, {"tabs", tabs}, {"activeTabId", m_tabs[m_activeTab].id},
386
2/4
✓ Branch 105 → 106 taken 3855 times.
✓ Branch 105 → 107 taken 771 times.
✗ Branch 203 → 204 not taken.
✗ Branch 203 → 205 not taken.
5397 {"area", m_area}, {"expandedFolders", expanded}};
387 4650 }
388
389 925 bool WorkspaceState::restoreSession(const QJsonObject &session, QString *error,
390 const std::function<bool(const MailId &)> &acceptIdentity) {
391 925 const QString previousMode = readingMode();
392
2/2
✓ Branch 2 → 3 taken 19 times.
✓ Branch 2 → 4 taken 10 times.
29 auto fail = [&](const QString &message) { if (error) *error = message; return false; };
393 // Sprint 101 S101.6.2: version 2 adds tab kinds; version 1 sessions hold
394 // the folder tab and mail tabs only.
395
3/3
✓ Branch 3 → 4 taken 925 times.
✓ Branch 4 → 5 taken 925 times.
✓ Branch 5 → 6 taken 925 times.
925 const int version = session.value("schemaVersion").toInt();
396
23/38
✓ Branch 8 → 9 taken 925 times.
✓ Branch 9 → 10 taken 925 times.
✓ Branch 10 → 11 taken 925 times.
✓ Branch 11 → 12 taken 925 times.
✓ Branch 12 → 13 taken 921 times.
✓ Branch 12 → 19 taken 4 times.
✓ Branch 13 → 14 taken 921 times.
✓ Branch 14 → 15 taken 921 times.
✓ Branch 15 → 16 taken 921 times.
✓ Branch 16 → 17 taken 921 times.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 920 times.
✓ Branch 20 → 21 taken 921 times.
✓ Branch 20 → 22 taken 4 times.
✓ Branch 22 → 23 taken 921 times.
✓ Branch 22 → 24 taken 4 times.
✓ Branch 24 → 25 taken 921 times.
✓ Branch 24 → 26 taken 4 times.
✓ Branch 26 → 27 taken 925 times.
✗ Branch 26 → 28 not taken.
✓ Branch 28 → 29 taken 925 times.
✗ Branch 28 → 30 not taken.
✓ Branch 30 → 31 taken 925 times.
✗ Branch 30 → 32 not taken.
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 40 taken 924 times.
✗ Branch 701 → 702 not taken.
✗ Branch 701 → 703 not taken.
✗ Branch 705 → 706 not taken.
✗ Branch 705 → 707 not taken.
✗ Branch 709 → 710 not taken.
✗ Branch 709 → 711 not taken.
✗ Branch 713 → 714 not taken.
✗ Branch 713 → 715 not taken.
✗ Branch 717 → 718 not taken.
✗ Branch 717 → 719 not taken.
✗ Branch 721 → 722 not taken.
✗ Branch 721 → 723 not taken.
925 if (session.value("schemaVersion") != QJsonValue(1) && session.value("schemaVersion") != QJsonValue(2))
397 1 return fail(QStringLiteral("Unsupported workspace session version"));
398 924 QMap<QString, QStringList> expanded;
399
4/4
✓ Branch 40 → 41 taken 924 times.
✓ Branch 41 → 42 taken 924 times.
✓ Branch 43 → 44 taken 917 times.
✓ Branch 43 → 184 taken 7 times.
924 if (session.contains("expandedFolders")) {
400
2/2
✓ Branch 44 → 45 taken 917 times.
✓ Branch 45 → 46 taken 917 times.
917 const auto value = session.value("expandedFolders");
401
9/13
✓ Branch 47 → 48 taken 917 times.
✓ Branch 48 → 49 taken 917 times.
✗ Branch 48 → 52 not taken.
✓ Branch 49 → 50 taken 917 times.
✓ Branch 50 → 51 taken 917 times.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 53 taken 916 times.
✓ Branch 54 → 55 taken 917 times.
✗ Branch 54 → 56 not taken.
✓ Branch 56 → 57 taken 1 time.
✓ Branch 56 → 64 taken 916 times.
✗ Branch 731 → 732 not taken.
✗ Branch 731 → 733 not taken.
917 if (!value.isObject() || value.toObject().size() > 64)
402 1 return fail(QStringLiteral("Invalid expanded-folder accounts"));
403
1/1
✓ Branch 64 → 65 taken 916 times.
916 const auto accounts = value.toObject();
404 916 qsizetype totalPaths = 0, characters = 0;
405
3/3
✓ Branch 162 → 163 taken 926 times.
✓ Branch 164 → 66 taken 15 times.
✓ Branch 164 → 165 taken 911 times.
926 for (auto it = accounts.begin(); it != accounts.end(); ++it) {
406
8/18
✓ Branch 66 → 67 taken 15 times.
✓ Branch 68 → 69 taken 15 times.
✗ Branch 68 → 75 not taken.
✓ Branch 69 → 70 taken 15 times.
✓ Branch 71 → 72 taken 15 times.
✗ Branch 71 → 75 not taken.
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 76 taken 15 times.
✓ Branch 77 → 78 taken 15 times.
✗ Branch 77 → 79 not taken.
✓ Branch 79 → 80 taken 15 times.
✗ Branch 79 → 81 not taken.
✗ Branch 81 → 82 not taken.
✓ Branch 81 → 89 taken 15 times.
✗ Branch 736 → 737 not taken.
✗ Branch 736 → 738 not taken.
✗ Branch 740 → 741 not taken.
✗ Branch 740 → 742 not taken.
15 if (it.key().isEmpty() || it.key().size() > 1024 || !it.value().isArray())
407 ✗ return fail(QStringLiteral("Invalid expanded-folder account"));
408
1/1
✓ Branch 90 → 91 taken 15 times.
15 const auto paths = it.value().toArray();
409
1/1
✓ Branch 91 → 92 taken 15 times.
15 totalPaths += paths.size();
410
1/1
✓ Branch 92 → 93 taken 15 times.
15 characters += it.key().size();
411
2/2
✓ Branch 95 → 96 taken 1 time.
✓ Branch 95 → 103 taken 14 times.
16 if (totalPaths > 4096) return fail(QStringLiteral("Too many expanded folders"));
412 14 QStringList decoded;
413 14 QSet<QString> seen;
414
3/3
✓ Branch 105 → 106 taken 14 times.
✓ Branch 140 → 107 taken 31 times.
✓ Branch 140 → 141 taken 10 times.
41 for (const auto &path : paths) {
415
1/1
✓ Branch 109 → 110 taken 31 times.
31 const auto text = path.toString();
416 31 characters += text.size();
417
5/6
✓ Branch 115 → 116 taken 30 times.
✗ Branch 115 → 121 not taken.
✓ Branch 117 → 118 taken 29 times.
✓ Branch 117 → 121 taken 1 time.
✓ Branch 118 → 119 taken 28 times.
✓ Branch 118 → 121 taken 1 time.
61 if (!path.isString() || text.isEmpty() || text.size() > 4096 ||
418
6/6
✓ Branch 113 → 114 taken 30 times.
✓ Branch 113 → 121 taken 1 time.
✓ Branch 120 → 121 taken 1 time.
✓ Branch 120 → 122 taken 27 times.
✓ Branch 123 → 124 taken 4 times.
✓ Branch 123 → 131 taken 27 times.
61 characters > 65536 || seen.contains(text))
419 4 return fail(QStringLiteral("Invalid expanded-folder path"));
420
1/1
✓ Branch 131 → 132 taken 27 times.
27 seen.insert(text);
421
1/1
✓ Branch 132 → 133 taken 27 times.
27 decoded.append(text);
422 31 }
423
1/1
✓ Branch 141 → 142 taken 10 times.
10 decoded.sort();
424
3/4
✓ Branch 143 → 144 taken 10 times.
✗ Branch 143 → 148 not taken.
✓ Branch 144 → 145 taken 10 times.
✓ Branch 145 → 146 taken 10 times.
10 if (!decoded.isEmpty()) expanded.insert(it.key(), decoded);
425 23 }
426
2/3
✓ Branch 165 → 166 taken 911 times.
✗ Branch 166 → 167 not taken.
✓ Branch 166 → 174 taken 911 times.
911 if (!validFolderExpansion(expanded)) return fail(QStringLiteral("Expanded folders exceed limits"));
427 922 }
428
5/5
✓ Branch 184 → 185 taken 918 times.
✓ Branch 185 → 186 taken 918 times.
✓ Branch 186 → 187 taken 918 times.
✓ Branch 189 → 190 taken 4 times.
✓ Branch 189 → 197 taken 914 times.
922 if (!session.value("tabs").isArray()) return fail(QStringLiteral("Missing workspace tabs"));
429
3/3
✓ Branch 197 → 198 taken 914 times.
✓ Branch 198 → 199 taken 914 times.
✓ Branch 199 → 200 taken 914 times.
914 const auto entries = session.value("tabs").toArray();
430
8/8
✓ Branch 202 → 203 taken 914 times.
✓ Branch 203 → 204 taken 911 times.
✓ Branch 203 → 206 taken 3 times.
✓ Branch 204 → 205 taken 911 times.
✓ Branch 205 → 206 taken 1 time.
✓ Branch 205 → 207 taken 910 times.
✓ Branch 208 → 209 taken 4 times.
✓ Branch 208 → 216 taken 910 times.
918 if (entries.isEmpty() || entries.size() > 128) return fail(QStringLiteral("Invalid workspace tab count"));
431 910 QList<Tab> restored;
432 910 QSet<QString> ids;
433
3/3
✓ Branch 629 → 630 taken 2580 times.
✓ Branch 630 → 218 taken 1684 times.
✓ Branch 630 → 631 taken 896 times.
2580 for (int i = 0; i < entries.size(); ++i) {
434
3/4
✓ Branch 218 → 219 taken 1684 times.
✓ Branch 219 → 220 taken 1684 times.
✗ Branch 221 → 222 not taken.
✓ Branch 221 → 229 taken 1684 times.
1684 if (!entries[i].isObject()) return fail(QStringLiteral("Malformed workspace tab"));
435
2/2
✓ Branch 229 → 230 taken 1684 times.
✓ Branch 230 → 231 taken 1684 times.
1684 const auto entry = entries[i].toObject();
436
1/1
✓ Branch 232 → 233 taken 1684 times.
1684 Tab tab;
437
3/3
✓ Branch 233 → 234 taken 1684 times.
✓ Branch 234 → 235 taken 1684 times.
✓ Branch 235 → 236 taken 1684 times.
1684 tab.id = entry.value("id").toString();
438
3/3
✓ Branch 240 → 241 taken 1684 times.
✓ Branch 241 → 242 taken 1684 times.
✓ Branch 242 → 243 taken 1684 times.
1684 tab.title = entry.value("title").toString();
439
3/3
✓ Branch 247 → 248 taken 1684 times.
✓ Branch 248 → 249 taken 1684 times.
✓ Branch 249 → 250 taken 1684 times.
1684 const auto rawIdentity = entry.value("identity").toObject();
440
2/2
✓ Branch 252 → 253 taken 1684 times.
✓ Branch 253 → 254 taken 1684 times.
1684 tab.mailId = decode(rawIdentity.toVariantMap());
441
3/3
✓ Branch 257 → 258 taken 1684 times.
✓ Branch 258 → 259 taken 1684 times.
✓ Branch 259 → 260 taken 1684 times.
1684 tab.mode = entry.value("readingMode").toString();
442
4/4
✓ Branch 264 → 265 taken 1684 times.
✓ Branch 265 → 266 taken 1684 times.
✓ Branch 267 → 268 taken 6 times.
✓ Branch 267 → 360 taken 1678 times.
1684 if (entry.contains("htmlReading")) {
443
4/5
✓ Branch 268 → 269 taken 6 times.
✓ Branch 269 → 270 taken 6 times.
✓ Branch 270 → 271 taken 6 times.
✗ Branch 273 → 274 not taken.
✓ Branch 273 → 281 taken 6 times.
6 if (!entry.value("htmlReading").isObject()) return fail(QStringLiteral("Invalid HTML reader state"));
444
3/3
✓ Branch 281 → 282 taken 6 times.
✓ Branch 282 → 283 taken 6 times.
✓ Branch 283 → 284 taken 6 times.
6 const auto html = entry.value("htmlReading").toObject();
445 6 bool epochOk = false;
446
4/4
✓ Branch 286 → 287 taken 6 times.
✓ Branch 287 → 288 taken 6 times.
✓ Branch 288 → 289 taken 6 times.
✓ Branch 289 → 290 taken 6 times.
6 tab.html.uidValidity = html.value("epoch").toString().toUInt(&epochOk);
447
4/4
✓ Branch 293 → 294 taken 6 times.
✓ Branch 294 → 295 taken 6 times.
✓ Branch 295 → 296 taken 6 times.
✓ Branch 296 → 297 taken 6 times.
6 const auto hex = html.value("digest").toString().toLatin1();
448
1/1
✓ Branch 300 → 301 taken 6 times.
6 tab.html.digest = QByteArray::fromHex(hex);
449
6/6
✓ Branch 303 → 304 taken 6 times.
✓ Branch 304 → 305 taken 6 times.
✓ Branch 305 → 306 taken 6 times.
✓ Branch 306 → 307 taken 6 times.
✓ Branch 307 → 308 taken 6 times.
✓ Branch 308 → 309 taken 6 times.
6 tab.html.scroll = {html.value("x").toDouble(), html.value("y").toDouble()};
450
6/9
✓ Branch 316 → 317 taken 4 times.
✓ Branch 316 → 330 taken 1 time.
✓ Branch 317 → 318 taken 4 times.
✓ Branch 319 → 320 taken 4 times.
✗ Branch 319 → 330 not taken.
✓ Branch 342 → 343 taken 4 times.
✓ Branch 342 → 350 taken 2 times.
✗ Branch 883 → 884 not taken.
✗ Branch 883 → 885 not taken.
11 if (!epochOk || hex.size() != 64 || tab.html.digest.toHex() != hex ||
451
23/32
✓ Branch 314 → 315 taken 5 times.
✓ Branch 314 → 330 taken 1 time.
✓ Branch 320 → 321 taken 4 times.
✓ Branch 321 → 322 taken 4 times.
✓ Branch 322 → 323 taken 4 times.
✓ Branch 323 → 324 taken 3 times.
✓ Branch 323 → 330 taken 1 time.
✓ Branch 324 → 325 taken 3 times.
✓ Branch 325 → 326 taken 3 times.
✓ Branch 326 → 327 taken 3 times.
✓ Branch 327 → 328 taken 2 times.
✓ Branch 327 → 330 taken 1 time.
✗ Branch 329 → 330 not taken.
✓ Branch 329 → 331 taken 2 times.
✓ Branch 332 → 333 taken 3 times.
✓ Branch 332 → 334 taken 3 times.
✓ Branch 334 → 335 taken 3 times.
✓ Branch 334 → 336 taken 3 times.
✓ Branch 336 → 337 taken 4 times.
✓ Branch 336 → 338 taken 2 times.
✓ Branch 338 → 339 taken 4 times.
✓ Branch 338 → 340 taken 2 times.
✓ Branch 340 → 341 taken 4 times.
✓ Branch 340 → 342 taken 2 times.
✗ Branch 867 → 868 not taken.
✗ Branch 867 → 869 not taken.
✗ Branch 871 → 872 not taken.
✗ Branch 871 → 873 not taken.
✗ Branch 875 → 876 not taken.
✗ Branch 875 → 877 not taken.
✗ Branch 879 → 880 not taken.
✗ Branch 879 → 881 not taken.
11 !html.value("x").isDouble() || !html.value("y").isDouble() || !validReaderPosition(tab.html.scroll))
452 4 return fail(QStringLiteral("Invalid HTML reader snapshot or position"));
453 10 }
454
15/23
✓ Branch 360 → 361 taken 1678 times.
✓ Branch 360 → 364 taken 2 times.
✓ Branch 361 → 362 taken 1678 times.
✓ Branch 362 → 363 taken 1678 times.
✓ Branch 363 → 371 taken 1678 times.
✓ Branch 364 → 365 taken 1 time.
✓ Branch 364 → 368 taken 1 time.
✓ Branch 373 → 374 taken 1 time.
✓ Branch 373 → 375 taken 1679 times.
✓ Branch 375 → 376 taken 1 time.
✓ Branch 375 → 377 taken 1679 times.
✓ Branch 377 → 378 taken 1678 times.
✓ Branch 377 → 379 taken 2 times.
✓ Branch 379 → 380 taken 1678 times.
✓ Branch 379 → 381 taken 2 times.
✗ Branch 892 → 893 not taken.
✗ Branch 892 → 894 not taken.
✗ Branch 896 → 897 not taken.
✗ Branch 896 → 898 not taken.
✗ Branch 900 → 901 not taken.
✗ Branch 900 → 902 not taken.
✗ Branch 904 → 905 not taken.
✗ Branch 904 → 906 not taken.
1682 tab.kind = version >= 2 ? entry.value("kind").toString() : (i == 0 ? QStringLiteral("main") : QStringLiteral("mail"));
455
4/4
✓ Branch 383 → 384 taken 1679 times.
✓ Branch 383 → 397 taken 1 time.
✓ Branch 384 → 385 taken 774 times.
✓ Branch 384 → 398 taken 905 times.
2454 if ((i == 0) != (tab.kind == QLatin1String("main")) ||
456
4/4
✓ Branch 387 → 388 taken 183 times.
✓ Branch 387 → 398 taken 591 times.
✓ Branch 390 → 391 taken 107 times.
✓ Branch 390 → 398 taken 76 times.
774 (i != 0 && tab.kind != QLatin1String("mail") && tab.kind != QLatin1String("search") &&
457
6/6
✓ Branch 393 → 394 taken 25 times.
✓ Branch 393 → 398 taken 82 times.
✓ Branch 396 → 397 taken 5 times.
✓ Branch 396 → 398 taken 20 times.
✓ Branch 399 → 400 taken 6 times.
✓ Branch 399 → 407 taken 1674 times.
1787 tab.kind != QLatin1String("compose") && tab.kind != QLatin1String("outbox")))
458 6 return fail(QStringLiteral("Invalid workspace tab kind"));
459
2/2
✓ Branch 409 → 410 taken 20 times.
✓ Branch 409 → 432 taken 1654 times.
1674 if (tab.kind == QLatin1String("outbox")) {
460 20 tab.mailId = {};
461 20 tab.mode = m_configuredMode;
462
1/1
✓ Branch 415 → 416 taken 20 times.
20 const bool duplicate = std::any_of(restored.cbegin(), restored.cend(),
463 20 [](const Tab &other) { return other.kind == QLatin1String("outbox"); });
464
6/12
✓ Branch 416 → 417 taken 20 times.
✗ Branch 416 → 425 not taken.
✓ Branch 418 → 419 taken 20 times.
✗ Branch 418 → 425 not taken.
✓ Branch 420 → 421 taken 20 times.
✗ Branch 420 → 425 not taken.
✓ Branch 422 → 423 taken 20 times.
✗ Branch 422 → 425 not taken.
✗ Branch 424 → 425 not taken.
✓ Branch 424 → 426 taken 20 times.
✗ Branch 427 → 428 not taken.
✓ Branch 427 → 429 taken 20 times.
20 if (duplicate || tab.id.isEmpty() || tab.id.size() > 64 || ids.contains(tab.id) || tab.title.size() > 1024) continue;
465
1/1
✓ Branch 429 → 430 taken 20 times.
20 ids.insert(tab.id);
466
1/1
✓ Branch 430 → 431 taken 20 times.
20 restored.append(tab);
467 20 continue;
468 20 }
469
2/2
✓ Branch 434 → 435 taken 82 times.
✓ Branch 434 → 472 taken 1572 times.
1654 if (tab.kind == QLatin1String("compose")) {
470 // The draft itself lives in the ComposeSessionStore; the tab only
471 // names it. A tab whose session id is malformed is dropped.
472
3/3
✓ Branch 435 → 436 taken 82 times.
✓ Branch 436 → 437 taken 82 times.
✓ Branch 437 → 438 taken 82 times.
82 tab.sessionId = entry.value("sessionId").toString();
473 82 tab.mailId = {};
474 82 tab.mode = m_configuredMode;
475
4/7
✓ Branch 445 → 446 taken 82 times.
✓ Branch 448 → 449 taken 81 times.
✗ Branch 448 → 455 not taken.
✓ Branch 450 → 451 taken 81 times.
✗ Branch 450 → 455 not taken.
✓ Branch 452 → 453 taken 81 times.
✗ Branch 452 → 455 not taken.
244 if (!validComposeSessionId(tab.sessionId) || tab.id.isEmpty() || tab.id.size() > 64 ||
476
5/6
✓ Branch 446 → 447 taken 81 times.
✓ Branch 446 → 455 taken 1 time.
✗ Branch 454 → 455 not taken.
✓ Branch 454 → 456 taken 81 times.
✓ Branch 457 → 458 taken 1 time.
✓ Branch 457 → 469 taken 81 times.
244 ids.contains(tab.id) || tab.title.size() > 1024) {
477
5/5
✓ Branch 458 → 459 taken 1 time.
✓ Branch 462 → 463 taken 1 time.
✓ Branch 463 → 464 taken 1 time.
✓ Branch 467 → 460 taken 1 time.
✓ Branch 467 → 468 taken 1 time.
2 qCWarning(lcCompose) << "Dropped a compose tab with an invalid session payload";
478 1 continue;
479 1 }
480
1/1
✓ Branch 469 → 470 taken 81 times.
81 ids.insert(tab.id);
481
1/1
✓ Branch 470 → 471 taken 81 times.
81 restored.append(tab);
482 81 continue;
483 81 }
484
2/2
✓ Branch 474 → 475 taken 76 times.
✓ Branch 474 → 515 taken 1496 times.
1572 if (tab.kind == QLatin1String("search")) {
485 // A corrupted search tab is dropped; the other tabs are restored.
486
3/3
✓ Branch 475 → 476 taken 76 times.
✓ Branch 476 → 477 taken 6 times.
✓ Branch 476 → 488 taken 70 times.
76 if (!restoreSearchPayload(entry, tab)) {
487
5/5
✓ Branch 477 → 478 taken 6 times.
✓ Branch 481 → 482 taken 6 times.
✓ Branch 482 → 483 taken 6 times.
✓ Branch 486 → 479 taken 6 times.
✓ Branch 486 → 487 taken 6 times.
12 qCWarning(lcSearch) << "Dropped a search tab with an invalid session payload";
488 6 continue;
489 6 }
490
9/9
✓ Branch 489 → 490 taken 52 times.
✓ Branch 489 → 495 taken 18 times.
✓ Branch 491 → 492 taken 2 times.
✓ Branch 491 → 495 taken 50 times.
✓ Branch 492 → 493 taken 2 times.
✓ Branch 493 → 494 taken 1 time.
✓ Branch 493 → 495 taken 1 time.
✓ Branch 496 → 497 taken 1 time.
✓ Branch 496 → 500 taken 69 times.
70 if (tab.mailId.isValid() && acceptIdentity && !acceptIdentity(tab.mailId)) tab.mailId = {};
491
5/10
✓ Branch 501 → 502 taken 70 times.
✗ Branch 501 → 508 not taken.
✓ Branch 503 → 504 taken 70 times.
✗ Branch 503 → 508 not taken.
✓ Branch 505 → 506 taken 70 times.
✗ Branch 505 → 508 not taken.
✗ Branch 507 → 508 not taken.
✓ Branch 507 → 509 taken 70 times.
✗ Branch 510 → 511 not taken.
✓ Branch 510 → 512 taken 70 times.
70 if (tab.id.isEmpty() || tab.id.size() > 64 || ids.contains(tab.id) || tab.title.size() > 1024) continue;
492
1/1
✓ Branch 512 → 513 taken 70 times.
70 ids.insert(tab.id);
493
1/1
✓ Branch 513 → 514 taken 70 times.
70 restored.append(tab);
494 70 continue;
495 70 }
496
1/2
✗ Branch 516 → 517 not taken.
✓ Branch 516 → 524 taken 1496 times.
1496 if (tab.mode == "html-light") tab.mode = QStringLiteral("light");
497
6/8
✓ Branch 525 → 526 taken 322 times.
✓ Branch 525 → 531 taken 1174 times.
✓ Branch 527 → 528 taken 141 times.
✓ Branch 527 → 531 taken 181 times.
✗ Branch 529 → 530 not taken.
✓ Branch 529 → 531 taken 141 times.
✗ Branch 532 → 533 not taken.
✓ Branch 532 → 534 taken 1496 times.
1496 if (tab.mode != "text" && tab.mode != "light" && tab.mode != "html") tab.mode = m_configuredMode;
498
4/6
✓ Branch 537 → 538 taken 1496 times.
✗ Branch 537 → 547 not taken.
✓ Branch 539 → 540 taken 1495 times.
✓ Branch 539 → 547 taken 1 time.
✓ Branch 541 → 542 taken 1495 times.
✗ Branch 541 → 547 not taken.
2991 if (tab.id.isEmpty() || tab.id.size() > 64 || ids.contains(tab.id) ||
499
13/21
✓ Branch 535 → 536 taken 1496 times.
✗ Branch 535 → 547 not taken.
✓ Branch 542 → 543 taken 1495 times.
✓ Branch 543 → 544 taken 1495 times.
✓ Branch 544 → 545 taken 1495 times.
✗ Branch 546 → 547 not taken.
✓ Branch 546 → 548 taken 1495 times.
✓ Branch 549 → 550 taken 1495 times.
✓ Branch 549 → 551 taken 1 time.
✓ Branch 551 → 552 taken 1495 times.
✓ Branch 551 → 553 taken 1 time.
✓ Branch 553 → 554 taken 1495 times.
✓ Branch 553 → 555 taken 1 time.
✓ Branch 555 → 556 taken 1 time.
✓ Branch 555 → 563 taken 1495 times.
✗ Branch 929 → 930 not taken.
✗ Branch 929 → 931 not taken.
✗ Branch 933 → 934 not taken.
✗ Branch 933 → 935 not taken.
✗ Branch 937 → 938 not taken.
✗ Branch 937 → 939 not taken.
4487 tab.title.size() > 1024 || rawIdentity.value("accountId").toString().size() > 1024)
500 1 return fail(QStringLiteral("Invalid or duplicate workspace tab identity"));
501
1/2
✗ Branch 564 → 565 not taken.
✓ Branch 564 → 572 taken 1495 times.
1495 if ((i == 0) != (tab.id == "main")) return fail(QStringLiteral("Missing permanent main tab"));
502
1/1
✓ Branch 572 → 573 taken 1495 times.
1495 ids.insert(tab.id);
503
2/2
✓ Branch 574 → 575 taken 350 times.
✓ Branch 574 → 596 taken 1145 times.
1495 if (!tab.mailId.isValid()) {
504 // Only an explicitly empty main tab may omit a valid identity.
505
14/21
✓ Branch 575 → 576 taken 347 times.
✓ Branch 575 → 580 taken 3 times.
✓ Branch 576 → 577 taken 347 times.
✓ Branch 577 → 578 taken 347 times.
✓ Branch 578 → 579 taken 347 times.
✗ Branch 579 → 580 not taken.
✓ Branch 579 → 581 taken 347 times.
✓ Branch 582 → 583 taken 347 times.
✓ Branch 582 → 584 taken 3 times.
✓ Branch 584 → 585 taken 347 times.
✓ Branch 584 → 586 taken 3 times.
✓ Branch 586 → 587 taken 347 times.
✓ Branch 586 → 588 taken 3 times.
✓ Branch 588 → 589 taken 3 times.
✓ Branch 588 → 610 taken 347 times.
✗ Branch 942 → 943 not taken.
✗ Branch 942 → 944 not taken.
✗ Branch 946 → 947 not taken.
✗ Branch 946 → 948 not taken.
✗ Branch 950 → 951 not taken.
✗ Branch 950 → 952 not taken.
350 if (i != 0 || rawIdentity != QJsonObject::fromVariantMap(encode(MailId{})))
506 3 return fail(QStringLiteral("Invalid workspace mail identity"));
507
7/7
✓ Branch 597 → 598 taken 12 times.
✓ Branch 597 → 601 taken 1133 times.
✓ Branch 598 → 599 taken 12 times.
✓ Branch 599 → 600 taken 6 times.
✓ Branch 599 → 601 taken 6 times.
✓ Branch 602 → 603 taken 6 times.
✓ Branch 602 → 610 taken 1139 times.
1145 } else if (acceptIdentity && !acceptIdentity(tab.mailId)) {
508 // A removed account/folder must never resolve to another account's UID.
509
2/2
✓ Branch 603 → 604 taken 3 times.
✓ Branch 603 → 605 taken 3 times.
6 if (i != 0) continue;
510 3 tab.mailId = {};
511 3 tab.html = {};
512 }
513
1/1
✓ Branch 610 → 611 taken 1489 times.
1489 restored.append(tab);
514 2074 }
515
3/3
✓ Branch 631 → 632 taken 896 times.
✓ Branch 632 → 633 taken 896 times.
✓ Branch 633 → 634 taken 896 times.
896 const QString activeId = session.value("activeTabId").toString();
516 896 int active = 0;
517
5/5
✓ Branch 637 → 638 taken 1646 times.
✓ Branch 639 → 640 taken 887 times.
✓ Branch 639 → 641 taken 759 times.
✓ Branch 643 → 637 taken 1646 times.
✓ Branch 643 → 644 taken 896 times.
2542 for (int i = 0; i < restored.size(); ++i) if (restored[i].id == activeId) active = i;
518
3/3
✓ Branch 644 → 645 taken 896 times.
✓ Branch 645 → 646 taken 896 times.
✓ Branch 646 → 647 taken 896 times.
896 QString area = session.value("area").toString();
519
10/10
✓ Branch 650 → 651 taken 413 times.
✓ Branch 650 → 658 taken 483 times.
✓ Branch 652 → 653 taken 229 times.
✓ Branch 652 → 658 taken 184 times.
✓ Branch 654 → 655 taken 174 times.
✓ Branch 654 → 658 taken 55 times.
✓ Branch 656 → 657 taken 109 times.
✓ Branch 656 → 658 taken 65 times.
✓ Branch 659 → 660 taken 109 times.
✓ Branch 659 → 667 taken 787 times.
1005 if (area != "mail" && area != "calendar" && area != "tasks" && area != "contacts") area = QStringLiteral("mail");
520
1/1
✓ Branch 667 → 668 taken 896 times.
896 beginResetModel();
521 896 m_tabs = restored;
522 896 m_activeTab = active;
523 896 m_area = area;
524 896 m_expandedFolders = std::move(expanded);
525
1/1
✓ Branch 672 → 673 taken 896 times.
896 m_selection = m_tabs[active].mailId;
526 896 ++m_generation;
527
1/1
✓ Branch 674 → 675 taken 896 times.
896 endResetModel();
528
1/1
✓ Branch 675 → 676 taken 896 times.
896 emit activeTabChanged();
529
3/3
✓ Branch 679 → 680 taken 58 times.
✓ Branch 679 → 681 taken 838 times.
✓ Branch 680 → 681 taken 58 times.
896 if (readingMode() != previousMode) emit readingModeChanged();
530
1/1
✓ Branch 681 → 682 taken 896 times.
896 emit selectionChanged();
531
1/1
✓ Branch 682 → 683 taken 896 times.
896 emit areaChanged();
532
1/1
✓ Branch 683 → 684 taken 896 times.
896 emit folderExpansionChanged();
533
2/2
✓ Branch 684 → 685 taken 867 times.
✓ Branch 684 → 686 taken 29 times.
896 if (error) error->clear();
534 896 return true;
535 925 }
536
537 // ── Sprint 101 S101.1.1: tab kinds and search tabs ──────────────────────
538
539 76 bool WorkspaceState::restoreSearchPayload(const QJsonObject &entry, Tab &tab) const {
540
17/28
✓ Branch 2 → 3 taken 76 times.
✓ Branch 3 → 4 taken 76 times.
✓ Branch 4 → 5 taken 76 times.
✓ Branch 5 → 6 taken 75 times.
✓ Branch 5 → 10 taken 1 time.
✓ Branch 6 → 7 taken 75 times.
✓ Branch 7 → 8 taken 75 times.
✓ Branch 8 → 9 taken 75 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 75 times.
✓ Branch 12 → 13 taken 75 times.
✓ Branch 12 → 14 taken 1 time.
✓ Branch 14 → 15 taken 75 times.
✓ Branch 14 → 16 taken 1 time.
✓ Branch 16 → 17 taken 76 times.
✗ Branch 16 → 18 not taken.
✓ Branch 18 → 19 taken 76 times.
✗ Branch 18 → 20 not taken.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 75 times.
✗ Branch 148 → 149 not taken.
✗ Branch 148 → 150 not taken.
✗ Branch 152 → 153 not taken.
✗ Branch 152 → 154 not taken.
✗ Branch 156 → 157 not taken.
✗ Branch 156 → 158 not taken.
✗ Branch 160 → 161 not taken.
✗ Branch 160 → 162 not taken.
76 if (!entry.value("query").isString() || !entry.value("accountId").isString()) return false;
541
3/3
✓ Branch 22 → 23 taken 75 times.
✓ Branch 23 → 24 taken 75 times.
✓ Branch 24 → 25 taken 75 times.
75 tab.query = entry.value("query").toString();
542
3/3
✓ Branch 29 → 30 taken 75 times.
✓ Branch 30 → 31 taken 75 times.
✓ Branch 31 → 32 taken 75 times.
75 tab.accountId = entry.value("accountId").toString();
543
7/8
✓ Branch 37 → 38 taken 74 times.
✓ Branch 37 → 42 taken 1 time.
✓ Branch 39 → 40 taken 73 times.
✓ Branch 39 → 42 taken 1 time.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 73 times.
✓ Branch 44 → 45 taken 2 times.
✓ Branch 44 → 46 taken 73 times.
75 if (tab.query.size() > MaximumQueryCharacters || tab.accountId.isEmpty() || tab.accountId.size() > 1024)
544 2 return false;
545
3/3
✓ Branch 46 → 47 taken 73 times.
✓ Branch 47 → 48 taken 73 times.
✓ Branch 48 → 49 taken 73 times.
73 const auto identity = entry.value("identity").toObject();
546
2/2
✓ Branch 51 → 52 taken 73 times.
✓ Branch 52 → 53 taken 73 times.
73 tab.mailId = decode(identity.toVariantMap());
547
13/21
✓ Branch 57 → 58 taken 18 times.
✓ Branch 57 → 63 taken 55 times.
✓ Branch 58 → 59 taken 18 times.
✓ Branch 59 → 60 taken 18 times.
✓ Branch 60 → 61 taken 18 times.
✗ Branch 61 → 62 not taken.
✓ Branch 61 → 63 taken 18 times.
✓ Branch 64 → 65 taken 18 times.
✓ Branch 64 → 66 taken 55 times.
✓ Branch 66 → 67 taken 18 times.
✓ Branch 66 → 68 taken 55 times.
✓ Branch 68 → 69 taken 18 times.
✓ Branch 68 → 70 taken 55 times.
✗ Branch 70 → 71 not taken.
✓ Branch 70 → 72 taken 73 times.
✗ Branch 188 → 189 not taken.
✗ Branch 188 → 190 not taken.
✗ Branch 192 → 193 not taken.
✗ Branch 192 → 194 not taken.
✗ Branch 196 → 197 not taken.
✗ Branch 196 → 198 not taken.
73 if (!tab.mailId.isValid() && identity != QJsonObject::fromVariantMap(encode(MailId{}))) return false;
548
6/6
✓ Branch 73 → 74 taken 55 times.
✓ Branch 73 → 77 taken 18 times.
✓ Branch 75 → 76 taken 1 time.
✓ Branch 75 → 77 taken 54 times.
✓ Branch 78 → 79 taken 1 time.
✓ Branch 78 → 80 taken 72 times.
73 if (tab.mailId.isValid() && tab.mailId.accountId != tab.accountId) return false;
549
3/3
✓ Branch 80 → 81 taken 72 times.
✓ Branch 81 → 82 taken 72 times.
✓ Branch 82 → 83 taken 72 times.
72 const auto anchor = entry.value("anchor").toObject();
550
2/2
✓ Branch 85 → 86 taken 72 times.
✓ Branch 86 → 87 taken 72 times.
72 tab.anchor = decode(anchor.toVariantMap());
551
17/24
✓ Branch 91 → 92 taken 5 times.
✓ Branch 91 → 99 taken 67 times.
✓ Branch 92 → 93 taken 5 times.
✓ Branch 93 → 94 taken 5 times.
✗ Branch 93 → 99 not taken.
✓ Branch 94 → 95 taken 5 times.
✓ Branch 95 → 96 taken 5 times.
✓ Branch 96 → 97 taken 5 times.
✓ Branch 97 → 98 taken 1 time.
✓ Branch 97 → 99 taken 4 times.
✓ Branch 100 → 101 taken 5 times.
✓ Branch 100 → 102 taken 67 times.
✓ Branch 102 → 103 taken 5 times.
✓ Branch 102 → 104 taken 67 times.
✓ Branch 104 → 105 taken 5 times.
✓ Branch 104 → 106 taken 67 times.
✓ Branch 106 → 107 taken 1 time.
✓ Branch 106 → 108 taken 71 times.
✗ Branch 210 → 211 not taken.
✗ Branch 210 → 212 not taken.
✗ Branch 214 → 215 not taken.
✗ Branch 214 → 216 not taken.
✗ Branch 218 → 219 not taken.
✗ Branch 218 → 220 not taken.
72 if (!tab.anchor.isValid() && !anchor.isEmpty() && anchor != QJsonObject::fromVariantMap(encode(MailId{})))
552 1 return false;
553 // S106-333: the anchor is fenced like the previewed mail.
554
6/6
✓ Branch 109 → 110 taken 67 times.
✓ Branch 109 → 113 taken 4 times.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 111 → 113 taken 66 times.
✓ Branch 114 → 115 taken 1 time.
✓ Branch 114 → 116 taken 70 times.
71 if (tab.anchor.isValid() && tab.anchor.accountId != tab.accountId) return false;
555
3/3
✓ Branch 116 → 117 taken 70 times.
✓ Branch 117 → 118 taken 70 times.
✓ Branch 118 → 119 taken 70 times.
70 tab.mode = entry.value("readingMode").toString();
556
1/2
✗ Branch 124 → 125 not taken.
✓ Branch 124 → 132 taken 70 times.
70 if (tab.mode == "html-light") tab.mode = QStringLiteral("light");
557
2/8
✗ Branch 133 → 134 not taken.
✓ Branch 133 → 139 taken 70 times.
✗ Branch 135 → 136 not taken.
✗ Branch 135 → 139 not taken.
✗ Branch 137 → 138 not taken.
✗ Branch 137 → 139 not taken.
✗ Branch 140 → 141 not taken.
✓ Branch 140 → 142 taken 70 times.
70 if (tab.mode != "text" && tab.mode != "light" && tab.mode != "html") tab.mode = m_configuredMode;
558 70 tab.tooltip = tab.query;
559 70 return true;
560 73 }
561
562 17 void WorkspaceState::emitRemoved(const QList<Tab> &before) {
563
2/2
✓ Branch 10 → 4 taken 40 times.
✓ Branch 10 → 11 taken 17 times.
57 for (const auto &tab : before)
564
3/3
✓ Branch 6 → 7 taken 14 times.
✓ Branch 6 → 8 taken 26 times.
✓ Branch 7 → 8 taken 14 times.
40 if (rowForTabId(tab.id) < 0) emit tabRemoved(tab.id, tab.kind);
565 17 }
566
567 12 QString WorkspaceState::tabKind(int index) const {
568
3/4
✓ Branch 2 → 3 taken 12 times.
✗ Branch 2 → 7 not taken.
✓ Branch 4 → 5 taken 11 times.
✓ Branch 4 → 7 taken 1 time.
12 return index >= 0 && index < m_tabs.size() ? m_tabs.at(index).kind : QString();
569 }
570
571 873 int WorkspaceState::rowForTabId(const QString &tabId) const {
572
2/2
✓ Branch 9 → 3 taken 1813 times.
✓ Branch 9 → 10 taken 113 times.
1926 for (int row = 0; row < m_tabs.size(); ++row)
573
2/2
✓ Branch 5 → 6 taken 760 times.
✓ Branch 5 → 7 taken 1053 times.
1813 if (m_tabs.at(row).id == tabId) return row;
574 113 return -1;
575 }
576
577 113 int WorkspaceState::openSearch(const QString &query, const QString &accountId, const QString &title, bool newTab) {
578
7/8
✓ Branch 3 → 4 taken 112 times.
✓ Branch 3 → 8 taken 1 time.
✓ Branch 5 → 6 taken 111 times.
✓ Branch 5 → 8 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 111 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 111 times.
113 if (query.size() > MaximumQueryCharacters || accountId.isEmpty() || accountId.size() > 1024) return -1;
579 111 int reuse = -1;
580
2/2
✓ Branch 36 → 13 taken 34 times.
✓ Branch 36 → 37 taken 110 times.
144 for (int row = 1; row < m_tabs.size(); ++row) {
581 34 const auto &tab = m_tabs.at(row);
582
6/6
✓ Branch 16 → 17 taken 25 times.
✓ Branch 16 → 19 taken 9 times.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 20 taken 23 times.
✓ Branch 21 → 22 taken 11 times.
✓ Branch 21 → 23 taken 23 times.
34 if (tab.kind != QLatin1String("search") || tab.accountId != accountId) continue;
583
3/3
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 27 taken 22 times.
✓ Branch 25 → 26 taken 1 time.
23 if (tab.query == query) { activateTab(row); return row; } // D-2: the same search exists
584
6/6
✓ Branch 27 → 28 taken 5 times.
✓ Branch 27 → 30 taken 17 times.
✓ Branch 29 → 30 taken 4 times.
✓ Branch 29 → 31 taken 1 time.
✓ Branch 32 → 33 taken 21 times.
✓ Branch 32 → 34 taken 1 time.
22 if (reuse < 0 || tab.used > m_tabs.at(reuse).used) reuse = row;
585 }
586
4/4
✓ Branch 37 → 38 taken 17 times.
✓ Branch 37 → 45 taken 93 times.
✓ Branch 38 → 39 taken 8 times.
✓ Branch 38 → 45 taken 9 times.
110 if (reuse >= 0 && !newTab) {
587 8 const QString id = m_tabs.at(reuse).id;
588
1/1
✓ Branch 41 → 42 taken 8 times.
8 activateTab(reuse);
589
1/1
✓ Branch 42 → 43 taken 8 times.
8 setSearchQuery(id, query, title, query);
590 8 return reuse;
591 8 }
592
1/2
✗ Branch 46 → 47 not taken.
✓ Branch 46 → 48 taken 102 times.
102 if (m_tabs.size() >= MaximumTabs) return -1;
593
1/1
✓ Branch 48 → 49 taken 102 times.
102 Tab tab;
594
2/2
✓ Branch 49 → 50 taken 102 times.
✓ Branch 50 → 51 taken 102 times.
102 tab.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
595 102 tab.kind = QStringLiteral("search");
596
1/1
✓ Branch 59 → 60 taken 102 times.
102 tab.title = title.left(1024);
597 102 tab.tooltip = query;
598 102 tab.query = query;
599 102 tab.accountId = accountId;
600 102 tab.mode = m_configuredMode;
601 102 const int row = m_tabs.size();
602
1/1
✓ Branch 68 → 69 taken 102 times.
102 beginInsertRows({}, row, row);
603
1/1
✓ Branch 69 → 70 taken 102 times.
102 m_tabs.append(tab);
604
1/1
✓ Branch 70 → 71 taken 102 times.
102 endInsertRows();
605
1/1
✓ Branch 71 → 72 taken 102 times.
102 activateTab(row);
606 102 return row;
607 102 }
608
609 148 bool WorkspaceState::setSearchQuery(const QString &tabId, const QString &query, const QString &title,
610 const QString &tooltip) {
611 148 const int row = rowForTabId(tabId);
612
4/8
✓ Branch 3 → 4 taken 148 times.
✗ Branch 3 → 10 not taken.
✓ Branch 7 → 8 taken 148 times.
✗ Branch 7 → 10 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 148 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 148 times.
148 if (row < 0 || m_tabs.at(row).kind != QLatin1String("search") || query.size() > MaximumQueryCharacters) return false;
613 148 auto &tab = m_tabs[row];
614
9/13
✓ Branch 16 → 17 taken 91 times.
✓ Branch 16 → 23 taken 57 times.
✓ Branch 17 → 18 taken 91 times.
✓ Branch 19 → 20 taken 91 times.
✗ Branch 19 → 23 not taken.
✓ Branch 21 → 22 taken 91 times.
✗ Branch 21 → 23 not taken.
✓ Branch 24 → 25 taken 91 times.
✓ Branch 24 → 26 taken 57 times.
✓ Branch 26 → 27 taken 91 times.
✓ Branch 26 → 28 taken 57 times.
✗ Branch 44 → 45 not taken.
✗ Branch 44 → 46 not taken.
148 if (tab.query == query && tab.title == title.left(1024) && tab.tooltip == tooltip) return true;
615 57 tab.query = query;
616
1/1
✓ Branch 29 → 30 taken 57 times.
57 tab.title = title.left(1024);
617
1/1
✓ Branch 32 → 33 taken 57 times.
57 tab.tooltip = tooltip.left(MaximumQueryCharacters);
618
4/4
✓ Branch 35 → 36 taken 57 times.
✓ Branch 37 → 38 taken 57 times.
✓ Branch 39 → 40 taken 57 times.
✓ Branch 40 → 41 taken 57 times.
57 emit dataChanged(index(row), index(row), {TitleRole, TooltipRole});
619 57 return true;
620 }
621
622 103 QString WorkspaceState::searchQuery(const QString &tabId) const {
623 103 const int row = rowForTabId(tabId);
624
1/2
✓ Branch 3 → 4 taken 103 times.
✗ Branch 3 → 6 not taken.
103 return row >= 0 ? m_tabs.at(row).query : QString();
625 }
626
627 92 QString WorkspaceState::searchAccount(const QString &tabId) const {
628 92 const int row = rowForTabId(tabId);
629
1/2
✓ Branch 3 → 4 taken 92 times.
✗ Branch 3 → 6 not taken.
92 return row >= 0 ? m_tabs.at(row).accountId : QString();
630 }
631
632 110 QStringList WorkspaceState::searchTabIds() const {
633 110 QStringList ids;
634
2/2
✓ Branch 11 → 4 taken 233 times.
✓ Branch 11 → 12 taken 110 times.
343 for (const auto &tab : m_tabs)
635
3/3
✓ Branch 7 → 8 taken 120 times.
✓ Branch 7 → 9 taken 113 times.
✓ Branch 8 → 9 taken 120 times.
233 if (tab.kind == QLatin1String("search")) ids.append(tab.id);
636 110 return ids;
637 ✗ }
638
639 120 bool WorkspaceState::setSearchAnchor(const QString &tabId, const MailId &anchor) {
640 120 const int row = rowForTabId(tabId);
641
3/6
✓ Branch 3 → 4 taken 120 times.
✗ Branch 3 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 120 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 120 times.
120 if (row < 0 || m_tabs.at(row).kind != QLatin1String("search")) return false;
642
5/6
✓ Branch 13 → 14 taken 120 times.
✗ Branch 13 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 119 times.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 21 taken 119 times.
120 if (anchor.isValid() && anchor.accountId != m_tabs.at(row).accountId) return false; // S106-333
643 119 m_tabs[row].anchor = anchor;
644 119 return true;
645 }
646
647 1 MailId WorkspaceState::searchAnchor(const QString &tabId) const {
648 1 const int row = rowForTabId(tabId);
649
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 6 not taken.
1 return row >= 0 ? m_tabs.at(row).anchor : MailId{};
650 }
651
652 1590 void WorkspaceState::setActiveAccount(const QString &accountId) {
653
2/2
✓ Branch 3 → 4 taken 810 times.
✓ Branch 3 → 5 taken 780 times.
1590 if (m_activeAccount == accountId) return;
654 780 m_activeAccount = accountId;
655
5/6
✓ Branch 7 → 8 taken 780 times.
✗ Branch 7 → 17 not taken.
✓ Branch 8 → 9 taken 780 times.
✓ Branch 11 → 12 taken 780 times.
✓ Branch 13 → 14 taken 780 times.
✓ Branch 14 → 15 taken 780 times.
780 if (!m_tabs.isEmpty()) emit dataChanged(index(0), index(int(m_tabs.size()) - 1), {DormantRole});
656 }
657
658 1589 bool WorkspaceState::tabDormant(int row) const {
659
3/6
✓ Branch 2 → 3 taken 1589 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1589 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1589 times.
1589 if (row < 0 || row >= m_tabs.size()) return false;
660 1589 const auto &tab = m_tabs.at(row);
661
5/6
✓ Branch 12 → 13 taken 318 times.
✓ Branch 12 → 18 taken 1271 times.
✓ Branch 14 → 15 taken 318 times.
✗ Branch 14 → 18 not taken.
✓ Branch 16 → 17 taken 18 times.
✓ Branch 16 → 18 taken 300 times.
1589 return tab.kind == QLatin1String("search") && !m_activeAccount.isEmpty() && tab.accountId != m_activeAccount;
662 }
663
664 2 bool WorkspaceState::activatePreviousTab() {
665 2 const int previous = rowForTabId(m_previousTabId);
666
2/4
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 6 not taken.
2 return activateTab(previous >= 0 && previous != m_activeTab ? previous : 0);
667 }
668
669 // ── Sprint 108 S108.9.1: compose tabs ───────────────────────────────────
670
671 301 bool WorkspaceState::validComposeSessionId(const QString &sessionId) {
672
5/6
✓ Branch 3 → 4 taken 300 times.
✓ Branch 3 → 6 taken 1 time.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 300 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 300 times.
301 if (sessionId.isEmpty() || sessionId.size() > 64) return false;
673 300 return std::all_of(sessionId.cbegin(), sessionId.cend(), [](QChar c) {
674
10/14
✓ Branch 5 → 6 taken 9354 times.
✓ Branch 5 → 10 taken 1173 times.
✓ Branch 9 → 10 taken 3319 times.
✓ Branch 9 → 29 taken 6035 times.
✓ Branch 13 → 14 taken 3319 times.
✓ Branch 13 → 18 taken 1173 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 29 taken 3319 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 26 taken 1173 times.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 29 not taken.
✓ Branch 28 → 29 taken 1171 times.
✓ Branch 28 → 30 taken 2 times.
10527 return (c >= u'0' && c <= u'9') || (c >= u'a' && c <= u'z') || (c >= u'A' && c <= u'Z') || c == u'-';
675 300 });
676 }
677
678 219 int WorkspaceState::openCompose(const QString &sessionId, const QString &title, bool activate) {
679
3/3
✓ Branch 2 → 3 taken 219 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 217 times.
219 if (!validComposeSessionId(sessionId)) return -1;
680 217 const int existing = rowForComposeSession(sessionId);
681
2/2
✓ Branch 6 → 7 taken 112 times.
✓ Branch 6 → 10 taken 105 times.
217 if (existing >= 0) {
682
2/3
✓ Branch 7 → 8 taken 112 times.
✗ Branch 7 → 9 not taken.
✓ Branch 8 → 9 taken 112 times.
112 if (activate) activateTab(existing);
683 112 return existing;
684 }
685
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 105 times.
105 if (m_tabs.size() >= MaximumTabs) return -1;
686
1/1
✓ Branch 13 → 14 taken 105 times.
105 Tab tab;
687
2/2
✓ Branch 14 → 15 taken 105 times.
✓ Branch 15 → 16 taken 105 times.
105 tab.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
688 105 tab.kind = QStringLiteral("compose");
689
1/1
✓ Branch 24 → 25 taken 105 times.
105 tab.title = title.left(1024);
690 105 tab.sessionId = sessionId;
691 105 tab.mode = m_configuredMode;
692 105 const int row = m_tabs.size();
693
1/1
✓ Branch 31 → 32 taken 105 times.
105 beginInsertRows({}, row, row);
694
1/1
✓ Branch 32 → 33 taken 105 times.
105 m_tabs.append(tab);
695
1/1
✓ Branch 33 → 34 taken 105 times.
105 endInsertRows();
696
3/3
✓ Branch 34 → 35 taken 3 times.
✓ Branch 34 → 36 taken 102 times.
✓ Branch 35 → 36 taken 3 times.
105 if (activate) activateTab(row);
697
2/6
✓ Branch 36 → 37 taken 105 times.
✗ Branch 40 → 41 not taken.
✗ Branch 41 → 42 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 46 → 38 not taken.
✓ Branch 46 → 47 taken 105 times.
105 qCInfo(lcCompose) << "compose tab opened for session" << sessionId;
698 105 return row;
699 105 }
700
701 106 bool WorkspaceState::setComposeTitle(const QString &sessionId, const QString &title) {
702 106 const int row = rowForComposeSession(sessionId);
703
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 105 times.
106 if (row < 0) return false;
704
3/3
✓ Branch 5 → 6 taken 105 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 104 times.
105 if (m_tabs.at(row).title == title.left(1024)) return true;
705
2/2
✓ Branch 11 → 12 taken 104 times.
✓ Branch 12 → 13 taken 104 times.
104 m_tabs[row].title = title.left(1024);
706
4/4
✓ Branch 15 → 16 taken 104 times.
✓ Branch 17 → 18 taken 104 times.
✓ Branch 19 → 20 taken 104 times.
✓ Branch 20 → 21 taken 104 times.
104 emit dataChanged(index(row), index(row), {TitleRole});
707 104 return true;
708 }
709
710 38 bool WorkspaceState::closeCompose(const QString &sessionId) {
711 38 const int row = rowForComposeSession(sessionId);
712
3/4
✓ Branch 3 → 4 taken 37 times.
✓ Branch 3 → 7 taken 1 time.
✓ Branch 5 → 6 taken 37 times.
✗ Branch 5 → 7 not taken.
38 return row >= 0 && closeTab(row);
713 }
714
715 189 QString WorkspaceState::composeSessionAt(int row) const {
716
2/4
✓ Branch 2 → 3 taken 189 times.
✗ Branch 2 → 7 not taken.
✓ Branch 4 → 5 taken 189 times.
✗ Branch 4 → 7 not taken.
189 return row >= 0 && row < m_tabs.size() ? m_tabs.at(row).sessionId : QString();
717 }
718
719 508 int WorkspaceState::rowForComposeSession(const QString &sessionId) const {
720
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 508 times.
508 if (sessionId.isEmpty()) return -1;
721
2/2
✓ Branch 19 → 6 taken 924 times.
✓ Branch 19 → 20 taken 220 times.
1144 for (int row = 0; row < m_tabs.size(); ++row)
722
6/6
✓ Branch 9 → 10 taken 381 times.
✓ Branch 9 → 14 taken 543 times.
✓ Branch 12 → 13 taken 288 times.
✓ Branch 12 → 14 taken 93 times.
✓ Branch 15 → 16 taken 288 times.
✓ Branch 15 → 17 taken 636 times.
924 if (m_tabs.at(row).kind == QLatin1String("compose") && m_tabs.at(row).sessionId == sessionId) return row;
723 220 return -1;
724 }
725
726 945 QStringList WorkspaceState::composeSessionIds() const {
727 945 QStringList ids;
728
2/2
✓ Branch 11 → 4 taken 1158 times.
✓ Branch 11 → 12 taken 945 times.
2103 for (const auto &tab : m_tabs)
729
3/3
✓ Branch 7 → 8 taken 74 times.
✓ Branch 7 → 9 taken 1084 times.
✓ Branch 8 → 9 taken 74 times.
1158 if (tab.kind == QLatin1String("compose")) ids.append(tab.sessionId);
730 945 return ids;
731 ✗ }
732
733 8 int WorkspaceState::openOutbox(const QString &title) {
734
2/2
✓ Branch 11 → 3 taken 1 time.
✓ Branch 11 → 12 taken 7 times.
8 for (int row = 1; row < m_tabs.size(); ++row)
735
1/2
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 9 not taken.
1 if (m_tabs.at(row).kind == QLatin1String("outbox")) {
736
1/1
✓ Branch 7 → 8 taken 1 time.
1 activateTab(row);
737 1 return row;
738 }
739
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 7 times.
7 if (m_tabs.size() >= MaximumTabs) return -1;
740
1/1
✓ Branch 15 → 16 taken 7 times.
7 Tab tab;
741
2/2
✓ Branch 16 → 17 taken 7 times.
✓ Branch 17 → 18 taken 7 times.
7 tab.id = QUuid::createUuid().toString(QUuid::WithoutBraces);
742 7 tab.kind = QStringLiteral("outbox");
743
1/1
✓ Branch 26 → 27 taken 7 times.
7 tab.title = title.left(1024);
744 7 tab.mode = m_configuredMode;
745 7 const int row = m_tabs.size();
746
1/1
✓ Branch 32 → 33 taken 7 times.
7 beginInsertRows({}, row, row);
747
1/1
✓ Branch 33 → 34 taken 7 times.
7 m_tabs.append(tab);
748
1/1
✓ Branch 34 → 35 taken 7 times.
7 endInsertRows();
749
1/1
✓ Branch 35 → 36 taken 7 times.
7 activateTab(row);
750 7 return row;
751 7 }
752