dynamic-systems-testing-framework 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. dstf/__init__.py +41 -0
  2. dstf/__main__.py +10 -0
  3. dstf/authoring/__init__.py +13 -0
  4. dstf/authoring/builtin/__init__.py +32 -0
  5. dstf/authoring/builtin/accept.py +205 -0
  6. dstf/authoring/builtin/edit_criteria.py +479 -0
  7. dstf/authoring/builtin/edit_family.py +228 -0
  8. dstf/authoring/builtin/edit_on_plot.py +1354 -0
  9. dstf/authoring/builtin/edit_tree.py +1399 -0
  10. dstf/authoring/builtin/enqueue.py +144 -0
  11. dstf/authoring/capabilities.py +463 -0
  12. dstf/authoring/errors.py +49 -0
  13. dstf/authoring/family_rows.py +526 -0
  14. dstf/authoring/forms.py +631 -0
  15. dstf/authoring/layout.py +104 -0
  16. dstf/authoring/live_rows.py +337 -0
  17. dstf/authoring/live_runs.py +385 -0
  18. dstf/authoring/needle.py +127 -0
  19. dstf/authoring/picker_rows.py +92 -0
  20. dstf/authoring/queue_rows.py +354 -0
  21. dstf/authoring/session.py +3016 -0
  22. dstf/authoring/shell/__init__.py +9 -0
  23. dstf/authoring/shell/actions.py +702 -0
  24. dstf/authoring/shell/app.py +53 -0
  25. dstf/authoring/shell/drawer_controls.py +190 -0
  26. dstf/authoring/shell/family_fold.py +468 -0
  27. dstf/authoring/shell/figure_hook.py +249 -0
  28. dstf/authoring/shell/grips.js +1000 -0
  29. dstf/authoring/shell/header.py +783 -0
  30. dstf/authoring/shell/live_view.py +93 -0
  31. dstf/authoring/shell/picker.py +267 -0
  32. dstf/authoring/shell/placing.py +122 -0
  33. dstf/authoring/shell/plot_card.py +1171 -0
  34. dstf/authoring/shell/queue_panel.py +433 -0
  35. dstf/authoring/shell/rail.py +593 -0
  36. dstf/authoring/shell/shell.py +528 -0
  37. dstf/authoring/shell/stats_card.py +501 -0
  38. dstf/authoring/shell/strip.py +204 -0
  39. dstf/authoring/shell/style.py +721 -0
  40. dstf/authoring/shell/suite.py +277 -0
  41. dstf/authoring/shell/suite_grid.js +414 -0
  42. dstf/authoring/shell/suite_grid.py +373 -0
  43. dstf/authoring/shell/tab_theme.py +110 -0
  44. dstf/authoring/shell/tabstate.py +146 -0
  45. dstf/authoring/shell/test_list.py +179 -0
  46. dstf/authoring/shell/test_page.py +1404 -0
  47. dstf/authoring/shell/tree_card.py +1171 -0
  48. dstf/authoring/shell/variables_drawer.py +589 -0
  49. dstf/authoring/shell/widgets.py +188 -0
  50. dstf/authoring/situation.py +287 -0
  51. dstf/authoring/spec_document.py +215 -0
  52. dstf/authoring/spec_preview.py +83 -0
  53. dstf/authoring/suite_rows.py +99 -0
  54. dstf/authoring/tree_move.py +516 -0
  55. dstf/authoring/tree_pointer.py +33 -0
  56. dstf/authoring/tree_text.py +61 -0
  57. dstf/authoring/typed_number.js +64 -0
  58. dstf/authoring/typed_number.py +76 -0
  59. dstf/authoring/unscorable.py +141 -0
  60. dstf/authoring/variable_rows.py +216 -0
  61. dstf/backends/__init__.py +283 -0
  62. dstf/backends/base.py +1117 -0
  63. dstf/backends/common/__init__.py +1 -0
  64. dstf/backends/common/json_driver.py +484 -0
  65. dstf/backends/common/live_mat.py +369 -0
  66. dstf/backends/common/mat_reader.py +632 -0
  67. dstf/backends/common/proc_kill.py +238 -0
  68. dstf/backends/common/proc_output.py +20 -0
  69. dstf/backends/common/result_files.py +109 -0
  70. dstf/backends/common/result_variables.py +329 -0
  71. dstf/backends/common/statistics.py +39 -0
  72. dstf/backends/cross_backend.py +193 -0
  73. dstf/backends/dymola/__init__.py +5 -0
  74. dstf/backends/dymola/backend.py +776 -0
  75. dstf/backends/dymola/interface_loader.py +241 -0
  76. dstf/backends/dymola/log_parser.py +396 -0
  77. dstf/backends/dymola/persistent_backend.py +949 -0
  78. dstf/backends/fmpy/__init__.py +11 -0
  79. dstf/backends/fmpy/backend.py +443 -0
  80. dstf/backends/julia/__init__.py +27 -0
  81. dstf/backends/julia/backend.py +156 -0
  82. dstf/backends/julia/persistent_backend.py +541 -0
  83. dstf/backends/julia/run_persistent.jl +167 -0
  84. dstf/backends/julia/run_test.jl +146 -0
  85. dstf/backends/openmodelica/__init__.py +21 -0
  86. dstf/backends/openmodelica/backend.py +368 -0
  87. dstf/backends/openmodelica/init_xml.py +76 -0
  88. dstf/backends/openmodelica/log_parser.py +119 -0
  89. dstf/backends/openmodelica/mos_generator.py +228 -0
  90. dstf/backends/openmodelica/persistent_backend.py +1045 -0
  91. dstf/backends/openmodelica/session_loader.py +63 -0
  92. dstf/backends/persistent.py +1811 -0
  93. dstf/backends/pool.py +602 -0
  94. dstf/backends/progress.py +395 -0
  95. dstf/backends/python/__init__.py +11 -0
  96. dstf/backends/python/backend.py +141 -0
  97. dstf/backends/python/run_test.py +141 -0
  98. dstf/backends/run_metadata.py +103 -0
  99. dstf/backends/types.py +310 -0
  100. dstf/cli/__init__.py +12 -0
  101. dstf/cli/author_cmd.py +61 -0
  102. dstf/cli/baseline_cmds.py +341 -0
  103. dstf/cli/common.py +231 -0
  104. dstf/cli/diag_cmds.py +350 -0
  105. dstf/cli/main.py +561 -0
  106. dstf/cli/review.py +398 -0
  107. dstf/cli/run_cmds.py +589 -0
  108. dstf/cli/spec_cmds.py +123 -0
  109. dstf/comparison/__init__.py +13 -0
  110. dstf/comparison/algorithms.py +2127 -0
  111. dstf/comparison/builtins.py +291 -0
  112. dstf/comparison/comparator.py +1189 -0
  113. dstf/comparison/geometry.py +164 -0
  114. dstf/comparison/handles.py +1194 -0
  115. dstf/comparison/metric_tree.py +332 -0
  116. dstf/comparison/mode_schema.py +176 -0
  117. dstf/comparison/modes.py +788 -0
  118. dstf/comparison/registry.py +230 -0
  119. dstf/comparison/seeds.py +281 -0
  120. dstf/comparison/sides.py +158 -0
  121. dstf/comparison/snap.py +62 -0
  122. dstf/comparison/strict_keys.py +246 -0
  123. dstf/comparison/summaries.py +354 -0
  124. dstf/comparison/tree_eval.py +868 -0
  125. dstf/comparison/tree_spec.py +678 -0
  126. dstf/comparison/types.py +194 -0
  127. dstf/comparison/validator.py +232 -0
  128. dstf/config.py +815 -0
  129. dstf/discovery/__init__.py +0 -0
  130. dstf/discovery/json_recognizer.py +702 -0
  131. dstf/discovery/library_state.py +248 -0
  132. dstf/discovery/mo_parser.py +794 -0
  133. dstf/discovery/patch_apply.py +308 -0
  134. dstf/discovery/patch_envelope.py +63 -0
  135. dstf/discovery/plots.py +318 -0
  136. dstf/discovery/recognizer.py +103 -0
  137. dstf/discovery/scan_cache.py +154 -0
  138. dstf/discovery/spec_parser.py +538 -0
  139. dstf/discovery/spec_shape.py +122 -0
  140. dstf/discovery/spec_update.py +493 -0
  141. dstf/discovery/test_registry.py +488 -0
  142. dstf/discovery/unit_tests.py +95 -0
  143. dstf/figures/__init__.py +14 -0
  144. dstf/figures/assemble.py +48 -0
  145. dstf/figures/decimate.py +150 -0
  146. dstf/figures/families.py +289 -0
  147. dstf/figures/palette.py +170 -0
  148. dstf/figures/plotly.py +1413 -0
  149. dstf/figures/view.py +233 -0
  150. dstf/pipeline.py +435 -0
  151. dstf/plugins.py +104 -0
  152. dstf/reporting/__init__.py +0 -0
  153. dstf/reporting/console_report.py +187 -0
  154. dstf/reporting/context_builder.py +979 -0
  155. dstf/reporting/dashboard_render.py +534 -0
  156. dstf/reporting/junit_report.py +192 -0
  157. dstf/reporting/overlay_loader.py +708 -0
  158. dstf/reporting/report_suite.py +1059 -0
  159. dstf/reporting/rows.py +458 -0
  160. dstf/reporting/schema_export.py +413 -0
  161. dstf/reporting/stats_rows.py +321 -0
  162. dstf/reporting/status.py +388 -0
  163. dstf/reporting/templates/axes.js +61 -0
  164. dstf/reporting/templates/dashboard.html +284 -0
  165. dstf/reporting/templates/dashboard.js +604 -0
  166. dstf/reporting/templates/filters.js +100 -0
  167. dstf/reporting/templates/interactive.html +420 -0
  168. dstf/reporting/templates/report.js +116 -0
  169. dstf/reporting/templates/vendor/plotly.min.js +8 -0
  170. dstf/reporting/theme.py +324 -0
  171. dstf/reporting/tree_rows.py +73 -0
  172. dstf/reporting/verdicts.py +124 -0
  173. dstf/storage/__init__.py +0 -0
  174. dstf/storage/accept.py +216 -0
  175. dstf/storage/baseline.py +43 -0
  176. dstf/storage/batch_manifest.py +204 -0
  177. dstf/storage/ref_index.py +295 -0
  178. dstf/storage/reference_store.py +661 -0
  179. dstf/storage/roles.py +576 -0
  180. dstf/storage/run_stats.py +85 -0
  181. dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
  182. dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
  183. dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
  184. dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
  185. dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,414 @@
1
+ // The Suite table's client side: group rows, the typed filters (through
2
+ // filters.js — DstfFilters), the count pills, the toolbar's view toggles, a
3
+ // sort within each group, the selection's label, and the view that persists
4
+ // per viewer in localStorage. Python sends rows (DstfSuite.apply, one
5
+ // applyTransaction) and receives the selection; it never updates the grid
6
+ // element, which would rebuild the grid (suite_grid.py).
7
+ (function () {
8
+ // Per project (the page sets it before this script): two tabs on one
9
+ // project share a view, two projects do not.
10
+ const KEY = window.DstfSuiteKey || 'dstf-suite-v1';
11
+ // The count pills, [key, word, passing] (suite_grid.py's PILLS): the
12
+ // words are reporting.status's, so this script spells none of them.
13
+ const PILLS = window.DstfSuitePills;
14
+ const S = {
15
+ grouped: true, short: true, regex: false, case: false,
16
+ pills: [], filters: {}, collapsed: [], columns: null, sort: null,
17
+ };
18
+ const D = { state: S, kinds: {}, api: null, matchers: [], bad: {}, gcount: {}, counts: {} };
19
+
20
+ // A per-viewer convenience: storage may be absent or refuse (a private
21
+ // window, blocked site data); the table works without it.
22
+ function load() {
23
+ try { Object.assign(S, JSON.parse(localStorage.getItem(KEY)) || {}); } catch (e) { /* none */ }
24
+ }
25
+ function save() {
26
+ try {
27
+ if (D.api) {
28
+ const cols = D.api.getColumnState();
29
+ S.columns = cols.map(c => ({ colId: c.colId, hide: c.hide }));
30
+ S.sort = cols.filter(c => c.sort).map(c => ({ colId: c.colId, sort: c.sort }));
31
+ }
32
+ localStorage.setItem(KEY, JSON.stringify(S));
33
+ } catch (e) { /* none */ }
34
+ }
35
+ load();
36
+ // The stored view may be older than this page: what names a column or a
37
+ // pill the page does not have is dropped, and the storage rewritten (the
38
+ // filters by DstfFilters.known, the dashboard's rule). A filter on a
39
+ // column renamed since was compiled as a text filter over nothing and
40
+ // emptied the table, with no input to undo it.
41
+ function known(api) {
42
+ const col = (c) => !!(c && api.getColumn(c.colId));
43
+ const pill = new Set(PILLS.map(p => p[0]));
44
+ S.filters = DstfFilters.known(S.filters, Object.keys(D.kinds));
45
+ if (Array.isArray(S.columns)) S.columns = S.columns.filter(col);
46
+ if (Array.isArray(S.sort)) S.sort = S.sort.filter(col);
47
+ S.pills = (Array.isArray(S.pills) ? S.pills : []).filter(k => pill.has(k));
48
+ try { localStorage.setItem(KEY, JSON.stringify(S)); } catch (e) { /* none */ }
49
+ }
50
+
51
+ // -- filtering --------------------------------------------------------------
52
+ function value(r, key) {
53
+ // A number filter reads the value (the __sort), never its text; a text
54
+ // filter reads the cell's text only when the row HAS a value (S1's
55
+ // Column.filter_text: an empty cell reads "—", which `.` would match).
56
+ const sort = r[key + '__sort'];
57
+ if (D.kinds[key] === 'number') return sort;
58
+ return sort === null || sort === undefined ? '' : r[key];
59
+ }
60
+ function compile() {
61
+ D.matchers = []; D.bad = {};
62
+ for (const [key, q] of Object.entries(S.filters)) {
63
+ let pred = null;
64
+ try { pred = DstfFilters.compile(D.kinds[key] || 'text', q, { regex: S.regex, case: S.case }); }
65
+ catch (e) { if (!(e instanceof DstfFilters.BadFilter)) throw e; D.bad[key] = e.message; }
66
+ if (pred) D.matchers.push(r => pred(value(r, key)));
67
+ }
68
+ // The pills filter the status; EDITED is OR'd with them.
69
+ const pills = new Set(S.pills);
70
+ if (pills.size) {
71
+ D.matchers.push(r => pills.has(r.status_class) || (pills.has('edited') && r.edits__sort > 0));
72
+ }
73
+ }
74
+ function passes(r) { for (const m of D.matchers) if (!m(r)) return false; return true; }
75
+
76
+ D.externalPass = (node) => {
77
+ const r = node.data;
78
+ if (r._g) return S.grouped && (D.gcount[r._pkg] || 0) > 0;
79
+ return passes(r) && (!S.grouped || !S.collapsed.includes(r._pkg));
80
+ };
81
+
82
+ // -- sorting ----------------------------------------------------------------
83
+ D.comparator = (key) => (a, b, na, nb, desc) => {
84
+ const x = na.data[key + '__sort'], y = nb.data[key + '__sort'];
85
+ const xn = x === null || x === undefined, yn = y === null || y === undefined;
86
+ if (xn || yn) {
87
+ if (xn && yn) return 0;
88
+ // Empties last in both directions: AG Grid negates for descending.
89
+ return (xn ? 1 : -1) * (desc ? -1 : 1);
90
+ }
91
+ return x < y ? -1 : x > y ? 1 : 0;
92
+ };
93
+ // Grouped: each package's header, then its rows in the order the sort
94
+ // left them — AG Grid Community has no row grouping, so it is done here.
95
+ D.postSort = (p) => {
96
+ if (!S.grouped) return;
97
+ const order = [], by = new Map();
98
+ for (const n of p.nodes) {
99
+ const k = n.data._pkg;
100
+ if (!by.has(k)) { by.set(k, { g: null, rs: [] }); order.push(k); }
101
+ const b = by.get(k);
102
+ if (n.data._g) b.g = n; else b.rs.push(n);
103
+ }
104
+ order.sort();
105
+ const out = [];
106
+ for (const k of order) { const b = by.get(k); if (b.g) out.push(b.g); out.push(...b.rs); }
107
+ p.nodes.length = 0; p.nodes.push(...out);
108
+ };
109
+
110
+ // -- recount and repaint ----------------------------------------------------
111
+ function recount(api) {
112
+ compile();
113
+ const g = {}, c = {};
114
+ for (const [k] of PILLS) c[k] = 0;
115
+ api.forEachNode(n => {
116
+ const r = n.data;
117
+ if (r._g) return;
118
+ // The pills count every row, whatever the filters (the dashboard's rule).
119
+ if (r.status_class in c) c[r.status_class]++;
120
+ if (r.edits__sort > 0) c.edited++;
121
+ if (passes(r)) g[r._pkg] = (g[r._pkg] || 0) + 1;
122
+ });
123
+ D.gcount = g; D.counts = c;
124
+ }
125
+ D.refresh = (api) => {
126
+ api = api || D.api;
127
+ if (!api) return;
128
+ recount(api);
129
+ api.onFilterChanged();
130
+ const groups = [];
131
+ api.forEachNode(n => { if (n.data._g) groups.push(n); });
132
+ api.redrawRows({ rowNodes: groups });
133
+ renderToolbar();
134
+ D.renderSelection();
135
+ document.querySelectorAll('.dstf-ff').forEach(el => {
136
+ const bad = D.bad[el.dataset.col];
137
+ el.classList.toggle('bad', !!bad);
138
+ el.title = bad || el.dataset.help;
139
+ });
140
+ document.querySelectorAll('.dstf-ff-status').forEach(el => {
141
+ el.textContent = S.pills.length
142
+ ? S.pills.map(p => PILLS.find(x => x[0] === p)[1]).join(' | ') : 'all';
143
+ });
144
+ };
145
+
146
+ // -- rows from Python -------------------------------------------------------
147
+ function groupRow(pkg) { return { id: 'g:' + pkg, _g: true, _pkg: pkg }; }
148
+ // A group row for every package with rows, none for a package without.
149
+ function regroup(api) {
150
+ const rows = new Set(), groups = new Set();
151
+ api.forEachNode(n => (n.data._g ? groups : rows).add(n.data._pkg));
152
+ const add = [...rows].filter(k => !groups.has(k)).map(groupRow);
153
+ const remove = [...groups].filter(k => !rows.has(k)).map(k => ({ id: 'g:' + k }));
154
+ if (add.length || remove.length) api.applyTransaction({ add, remove });
155
+ }
156
+ // One transaction from Python: {add, update, remove, select}. `select`
157
+ // names added rows the tab has selected (rows that arrive after the grid
158
+ // was ready — drawn before the library scan finished).
159
+ D.apply = (tx) => {
160
+ const api = D.api;
161
+ if (!api) { D.pending = (D.pending || []).concat([tx]); return; }
162
+ api.applyTransaction({
163
+ add: tx.add || [], update: tx.update || [],
164
+ remove: (tx.remove || []).map(id => ({ id })),
165
+ });
166
+ regroup(api);
167
+ const want = new Set(tx.select || []);
168
+ if (want.size) {
169
+ const nodes = [];
170
+ api.forEachNode(n => { if (want.has(n.data.id)) nodes.push(n); });
171
+ api.setNodesSelected({ nodes, newValue: true, source: 'api' });
172
+ }
173
+ D.refresh(api);
174
+ };
175
+
176
+ // -- the grid's components --------------------------------------------------
177
+ D.GroupRow = class {
178
+ init(p) {
179
+ this.p = p;
180
+ this.e = document.createElement('div');
181
+ this.e.className = 'dstf-grp';
182
+ this.e.setAttribute('role', 'button');
183
+ this.e.addEventListener('click', () => D.toggleGroup(p.data._pkg));
184
+ this.render();
185
+ }
186
+ render() {
187
+ const k = this.p.data._pkg, closed = S.collapsed.includes(k), n = D.gcount[k] || 0;
188
+ this.e.setAttribute('aria-expanded', closed ? 'false' : 'true');
189
+ this.e.innerHTML = `<i class="q-icon material-icons">${closed ? 'chevron_right' : 'expand_more'}</i>`
190
+ + `<span class="dstf-grp-name"></span>`
191
+ + `<span class="dstf-grp-count">${n} ${n === 1 ? 'test' : 'tests'}</span>`;
192
+ this.e.querySelector('.dstf-grp-name').textContent = k || '(top level)';
193
+ }
194
+ getGui() { return this.e; }
195
+ refresh() { this.render(); return true; }
196
+ };
197
+ D.toggleGroup = (pkg) => {
198
+ const i = S.collapsed.indexOf(pkg);
199
+ if (i >= 0) S.collapsed.splice(i, 1); else S.collapsed.push(pkg);
200
+ save(); D.refresh();
201
+ };
202
+ // The keyboard's clicks: Enter / Space on a focused group row collapses
203
+ // it; Enter on a focused Test cell opens the test's page, as its link does.
204
+ D.keyDown = (e) => {
205
+ const k = e.event && e.event.key;
206
+ const r = e.node && e.node.data;
207
+ if (!r) return;
208
+ if (r._g && (k === 'Enter' || k === ' ')) {
209
+ e.event.preventDefault();
210
+ D.toggleGroup(r._pkg);
211
+ } else if (!r._g && k === 'Enter' && e.column && e.column.getColId() === 'model') {
212
+ e.event.preventDefault();
213
+ window.location.assign('/test/' + encodeURIComponent(r.id));
214
+ }
215
+ };
216
+ // Every column "has" a filter so AG Grid draws the floating filter row;
217
+ // the filtering itself is the external filter's.
218
+ D.NoFilter = class {
219
+ init() {} getGui() { return document.createElement('span'); }
220
+ doesFilterPass() { return true; } isFilterActive() { return false; }
221
+ getModel() { return null; } setModel() {}
222
+ };
223
+ D.FloatingInput = class {
224
+ init(p) {
225
+ const col = p.column.getColId();
226
+ if (p.kind === 'status') {
227
+ // The status is filtered by the pills: this cell mirrors them.
228
+ this.e = document.createElement('span');
229
+ this.e.className = 'dstf-ff-status';
230
+ this.e.textContent = 'all';
231
+ return;
232
+ }
233
+ this.e = document.createElement('input');
234
+ this.e.className = 'dstf-ff';
235
+ this.e.dataset.col = col;
236
+ this.e.dataset.help = p.help;
237
+ this.e.title = p.help;
238
+ this.e.placeholder = p.placeholder;
239
+ this.e.setAttribute('aria-label', 'Filter ' + p.column.getColDef().headerName);
240
+ this.e.value = S.filters[col] || '';
241
+ this.e.addEventListener('input', () => {
242
+ if (this.e.value) S.filters[col] = this.e.value; else delete S.filters[col];
243
+ save(); D.refresh(p.api);
244
+ });
245
+ }
246
+ getGui() { return this.e; }
247
+ onParentModelChanged() {}
248
+ };
249
+ function esc(s) {
250
+ return String(s).replace(/[&<>"']/g, c => (
251
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
252
+ }
253
+ D.cell = {
254
+ status: (p) => `<span class="dstf-badge-cell" `
255
+ + `style="background: var(--dstf-${p.data.status_class}-fill)">${esc(p.value)}</span>`,
256
+ // No `title`: the column's tooltip names the test over the whole cell,
257
+ // and a title on the link would show a second one over it.
258
+ model: (p) => {
259
+ const name = S.short ? p.data._name_short : p.data._name_full;
260
+ return `<a class="dstf-name-link" href="/test/${encodeURIComponent(p.data.id)}">`
261
+ + `${esc(name)}</a>`;
262
+ },
263
+ edits: (p) => (p.value ? `<span class="dstf-edits">${esc(p.value)}</span>` : ''),
264
+ // A number of another kind (Sim (s)'s CPU time) says so after it; the
265
+ // column's tooltip says what it is.
266
+ tagged: (p) => {
267
+ const tag = p.data && p.data[p.colDef.field + '__tag'];
268
+ return esc(p.value) + (tag ? `<span class="dstf-cell-tag">${esc(tag)}</span>` : '');
269
+ },
270
+ };
271
+ D.rowClass = (p) => (p.data && !p.data._g ? 'dstf-tint-' + p.data.status_class : '');
272
+
273
+ // -- the toolbar and the selection's label (their state is the browser's) --
274
+ // `key` names the control across repaints (the focus follows it).
275
+ function btn(key, icon, tip, pressed, onclick, extra) {
276
+ const b = document.createElement('button');
277
+ b.type = 'button';
278
+ b.dataset.tb = key;
279
+ b.className = 'dstf-tb' + (pressed ? ' on' : '') + (extra ? ' ' + extra : '');
280
+ b.title = tip;
281
+ b.setAttribute('aria-label', tip);
282
+ if (pressed !== null) b.setAttribute('aria-pressed', pressed ? 'true' : 'false');
283
+ b.innerHTML = icon.startsWith('txt:')
284
+ ? `<span class="dstf-tb-txt">${esc(icon.slice(4))}</span>`
285
+ : `<i class="q-icon material-icons">${icon}</i>`;
286
+ b.addEventListener('click', onclick);
287
+ return b;
288
+ }
289
+ // Drawn again on every refresh — every live transaction too — so the
290
+ // focused control is focused again: Space on a pill or Enter on a view
291
+ // icon dropped the keyboard to the page.
292
+ function renderToolbar() {
293
+ const host = document.getElementById('dstf-suite-tools');
294
+ if (!host) return;
295
+ const had = host.contains(document.activeElement) ? document.activeElement.dataset.tb : null;
296
+ host.textContent = '';
297
+ const pills = document.createElement('span');
298
+ pills.className = 'dstf-pills';
299
+ for (const [k, word, passing] of PILLS) {
300
+ const n = D.counts[k] || 0;
301
+ const on = S.pills.includes(k);
302
+ // A state a row only passes through (not scored yet, queued,
303
+ // running) is shown while a row is in it — and while it filters,
304
+ // reading 0, so a pill switched on can be switched off once the
305
+ // rows have moved on.
306
+ if (passing && !n && !on) continue;
307
+ const b = btn('pill:' + k, 'txt:' + word + ' ' + n, (on ? 'Stop showing only ' : 'Show only ') + word, on, () => {
308
+ const i = S.pills.indexOf(k);
309
+ if (i >= 0) S.pills.splice(i, 1); else S.pills.push(k);
310
+ save(); D.refresh();
311
+ }, 'dstf-pill-btn');
312
+ b.dataset.pill = k;
313
+ b.setAttribute('aria-label', word + ' ' + n); // the text: what it counts
314
+ if (!n && !on) b.disabled = true;
315
+ pills.appendChild(b);
316
+ }
317
+ host.appendChild(pills);
318
+ // A view button's label says what a click does; `.*` and `Aa` are
319
+ // toggles with a fixed name and aria-pressed.
320
+ const views = document.createElement('span');
321
+ views.className = 'dstf-views';
322
+ views.appendChild(btn('group', 'account_tree', S.grouped ? 'Show flat' : 'Group by package', null, () => {
323
+ S.grouped = !S.grouped; save(); D.api.refreshClientSideRowModel('sort'); D.refresh();
324
+ }));
325
+ views.appendChild(btn('names', 'label', S.short ? 'Full names' : 'Short names', null, () => {
326
+ S.short = !S.short; save(); D.api.refreshCells({ columns: ['model'], force: true }); D.refresh();
327
+ }));
328
+ const cols = btn('columns', 'view_column', 'Choose columns', null, (e) => { e.stopPropagation(); D.columnMenu(cols); });
329
+ cols.setAttribute('aria-haspopup', 'menu');
330
+ views.appendChild(cols);
331
+ views.appendChild(btn('regex', 'txt:.*', 'Regex in every column filter', S.regex, () => {
332
+ S.regex = !S.regex; save(); D.refresh();
333
+ }));
334
+ views.appendChild(btn('case', 'txt:Aa', 'Match case in every column filter', S.case, () => {
335
+ S.case = !S.case; save(); D.refresh();
336
+ }));
337
+ host.appendChild(views);
338
+ if (had) {
339
+ const again = host.querySelector(`[data-tb="${had}"]`);
340
+ if (again) again.focus();
341
+ }
342
+ }
343
+ // "2 selected · ✕", with the selected rows a filter or a collapsed group
344
+ // hides counted: the actions act on every selected row.
345
+ D.renderSelection = () => {
346
+ const host = document.getElementById('dstf-suite-selection');
347
+ if (!host || !D.api) return;
348
+ const ids = D.api.getSelectedRows().filter(r => !r._g).map(r => r.id);
349
+ const shown = new Set();
350
+ D.api.forEachNodeAfterFilter(n => { if (n.data && !n.data._g) shown.add(n.data.id); });
351
+ const hidden = ids.filter(id => !shown.has(id)).length;
352
+ host.textContent = '';
353
+ if (!ids.length) return;
354
+ const label = document.createElement('span');
355
+ label.textContent = `${ids.length} selected` + (hidden ? ` (${hidden} hidden)` : '') + ' ·';
356
+ host.appendChild(label);
357
+ const clear = btn('clear', 'close', 'Clear selection', null, () => D.api.deselectAll());
358
+ clear.classList.add('dstf-clear');
359
+ host.appendChild(clear);
360
+ };
361
+ D.columnMenu = (anchor) => {
362
+ let m = document.getElementById('dstf-colmenu');
363
+ if (m) { m.remove(); return; }
364
+ m = document.createElement('div');
365
+ m.id = 'dstf-colmenu';
366
+ m.setAttribute('role', 'menu');
367
+ for (const c of D.api.getColumns()) {
368
+ const def = c.getColDef();
369
+ if (!def.field) continue; // the selection column
370
+ const lab = document.createElement('label');
371
+ const box = document.createElement('input');
372
+ box.type = 'checkbox';
373
+ box.checked = c.isVisible();
374
+ box.addEventListener('change', () => { D.api.setColumnsVisible([c.getColId()], box.checked); save(); });
375
+ lab.appendChild(box);
376
+ lab.appendChild(document.createTextNode(' ' + def.headerName));
377
+ m.appendChild(lab);
378
+ }
379
+ const r = anchor.getBoundingClientRect();
380
+ m.style.top = (r.bottom + window.scrollY + 4) + 'px';
381
+ m.style.left = Math.max(8, Math.min(r.left, window.innerWidth - 220)) + 'px';
382
+ document.body.appendChild(m);
383
+ const close = () => {
384
+ m.remove();
385
+ document.removeEventListener('click', off);
386
+ document.removeEventListener('keydown', esc_);
387
+ };
388
+ const off = (e) => { if (!m.contains(e.target)) close(); };
389
+ const esc_ = (e) => { if (e.key === 'Escape') { close(); anchor.focus(); } };
390
+ setTimeout(() => {
391
+ document.addEventListener('click', off);
392
+ document.addEventListener('keydown', esc_);
393
+ });
394
+ };
395
+
396
+ // -- ready ------------------------------------------------------------------
397
+ D.ready = (p, kinds, selected) => {
398
+ D.api = p.api;
399
+ D.kinds = kinds;
400
+ known(p.api);
401
+ if (S.columns) p.api.applyColumnState({ state: S.columns });
402
+ if (S.sort) p.api.applyColumnState({ state: S.sort, defaultState: { sort: null } });
403
+ regroup(p.api);
404
+ for (const tx of D.pending || []) D.apply(tx);
405
+ D.pending = [];
406
+ const want = new Set(selected || []);
407
+ const nodes = [];
408
+ p.api.forEachNode(n => { if (want.has(n.data.id)) nodes.push(n); });
409
+ if (nodes.length) p.api.setNodesSelected({ nodes, newValue: true, source: 'api' });
410
+ D.refresh(p.api);
411
+ };
412
+ D.saveState = save;
413
+ window.DstfSuite = D;
414
+ })();