@alejandrojca/elmulo-reporter 2.0.0-beta.7 → 2.0.0-beta.9

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.
package/VERSION.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Elmulo Reporter",
3
- "version": "2.0.0-beta.7",
3
+ "version": "2.0.0-beta.9",
4
4
  "schemaVersion": 3,
5
5
  "source": "elmulo-reporter",
6
6
  "results": "elmulo-results",
package/assets/app.css CHANGED
@@ -27,9 +27,12 @@
27
27
  box-sizing: border-box;
28
28
  }
29
29
 
30
- body {
31
- margin: 0;
32
- }
30
+ html,
31
+ body {
32
+ height: 100%;
33
+ margin: 0;
34
+ overflow: hidden;
35
+ }
33
36
 
34
37
  html[data-theme="dark"],
35
38
  html[data-theme="dark"] * {
@@ -258,10 +261,54 @@ button {
258
261
  .metricCard.passed { --metric-color: var(--pw-success); }
259
262
  .metricCard.failed { --metric-color: var(--pw-danger); }
260
263
  .metricCard.skipped { --metric-color: var(--pw-gray-300); }
261
- .metricCard.flaky { --metric-color: var(--pw-warning); }
262
- .metricCard.otherErrors { --metric-color: #d18a00; }
263
-
264
- .metricCard.activeFilter {
264
+ .metricCard.flaky { --metric-color: var(--pw-warning); }
265
+ .metricCard.otherErrors { --metric-color: #d18a00; }
266
+
267
+ .metricCard.failedBreakdown {
268
+ display: grid;
269
+ gap: 0.9rem;
270
+ grid-column: span 2;
271
+ grid-template-columns: minmax(90px, 0.7fr) minmax(210px, 1.3fr);
272
+ }
273
+
274
+ .failureMetricTotal {
275
+ align-self: center;
276
+ }
277
+
278
+ .failureMetricBreakdown {
279
+ align-content: center;
280
+ display: grid;
281
+ gap: 0.35rem;
282
+ margin: 0;
283
+ }
284
+
285
+ .failureMetricBreakdown > div {
286
+ --breakdown-color: var(--pw-danger);
287
+ align-items: center;
288
+ border: 1px solid color-mix(in srgb, var(--breakdown-color) 62%, var(--pw-gray-200));
289
+ border-radius: 999px;
290
+ display: flex;
291
+ justify-content: space-between;
292
+ padding: 0.25rem 0.55rem;
293
+ }
294
+
295
+ .failureMetricBreakdown .reported { --breakdown-color: var(--pw-blue-600); }
296
+ .failureMetricBreakdown .otherErrors { --breakdown-color: var(--pw-warning); }
297
+
298
+ .failureMetricBreakdown dt,
299
+ .failureMetricBreakdown dd {
300
+ color: var(--pw-gray-700);
301
+ font-size: 0.68rem;
302
+ font-weight: 800;
303
+ margin: 0;
304
+ }
305
+
306
+ .failureMetricBreakdown dd {
307
+ color: var(--breakdown-color);
308
+ font-size: 0.82rem;
309
+ }
310
+
311
+ .metricCard.activeFilter {
265
312
  border-color: var(--metric-color, var(--pw-blue-700));
266
313
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--metric-color, var(--pw-blue-700)) 18%, transparent);
267
314
  }
@@ -363,10 +410,15 @@ button {
363
410
  padding: 0.55rem 0.65rem;
364
411
  }
365
412
 
366
- .statusLegendItem.empty {
367
- opacity: 0.56;
368
- }
369
-
413
+ .statusLegendItem.empty {
414
+ opacity: 0.56;
415
+ }
416
+
417
+ .statusLegendItem:not(.empty) {
418
+ border-color: var(--status-color);
419
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--status-color) 24%, transparent);
420
+ }
421
+
370
422
  .statusLegendDot {
371
423
  background: var(--status-color);
372
424
  border-radius: 50%;
@@ -798,19 +850,59 @@ button {
798
850
  margin: 1rem 0;
799
851
  }
800
852
 
801
- .trendHistoryStats span {
802
- background: var(--pw-blue-050);
803
- border-radius: 999px;
804
- color: var(--pw-gray-700);
805
- font-size: 0.78rem;
806
- padding: 0.45rem 0.65rem;
807
- }
808
-
809
- .trendHistoryStats .passed { color: #11694f; }
810
- .trendHistoryStats .failed { color: #b42143; }
811
- .trendHistoryStats .otherErrors { color: #875100; }
812
-
813
- .historicalTestList {
853
+ .historicalExecutionCount {
854
+ background: var(--pw-blue-050);
855
+ border-radius: 999px;
856
+ color: var(--pw-gray-700);
857
+ font-size: 0.78rem;
858
+ padding: 0.45rem 0.65rem;
859
+ }
860
+
861
+ .historicalStatusFilters {
862
+ display: flex;
863
+ flex: 1 1 420px;
864
+ flex-wrap: wrap;
865
+ gap: 0.45rem;
866
+ }
867
+
868
+ .historicalStatusFilter {
869
+ --historical-status-color: var(--pw-gray-700);
870
+ background: color-mix(in srgb, var(--historical-status-color) 7%, var(--pw-white));
871
+ border: 1px solid color-mix(in srgb, var(--historical-status-color) 55%, var(--pw-gray-200));
872
+ border-radius: 999px;
873
+ color: var(--historical-status-color);
874
+ font-size: 0.75rem;
875
+ font-weight: 700;
876
+ padding: 0.42rem 0.65rem;
877
+ transition:
878
+ background 140ms ease,
879
+ box-shadow 140ms ease,
880
+ transform 140ms ease;
881
+ }
882
+
883
+ .historicalStatusFilter.passed { --historical-status-color: var(--pw-success); }
884
+ .historicalStatusFilter.failed { --historical-status-color: var(--pw-danger); }
885
+ .historicalStatusFilter.environment_error { --historical-status-color: var(--pw-warning); }
886
+ .historicalStatusFilter.precondition_error { --historical-status-color: #e87922; }
887
+ .historicalStatusFilter.outdated_test { --historical-status-color: #7c5ce5; }
888
+ .historicalStatusFilter.reported { --historical-status-color: var(--pw-blue-600); }
889
+ .historicalStatusFilter.skipped,
890
+ .historicalStatusFilter.pending { --historical-status-color: #71859a; }
891
+
892
+ .historicalStatusFilter:hover,
893
+ .historicalStatusFilter:focus-visible {
894
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--historical-status-color) 18%, transparent);
895
+ transform: translateY(-1px);
896
+ }
897
+
898
+ .historicalStatusFilter.active {
899
+ background: var(--historical-status-color);
900
+ border-color: var(--historical-status-color);
901
+ color: #fff;
902
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--historical-status-color) 22%, transparent);
903
+ }
904
+
905
+ .historicalTestList {
814
906
  display: grid;
815
907
  gap: 0.55rem;
816
908
  max-height: 480px;
@@ -2228,18 +2320,35 @@ html[data-theme="dark"] .exportPdfButton {
2228
2320
  grid-template-columns: repeat(5, 1fr);
2229
2321
  }
2230
2322
 
2231
- .attentionCard {
2232
- background: var(--pw-blue-050);
2233
- border: 1px solid var(--pw-gray-200);
2234
- border-left: 4px solid var(--attention-color, var(--pw-blue-700));
2323
+ .attentionCard {
2324
+ background: var(--pw-blue-050);
2325
+ border: 1px solid var(--pw-gray-200);
2326
+ border-left: 4px solid var(--attention-color, var(--pw-blue-700));
2235
2327
  border-radius: 14px;
2236
2328
  color: var(--pw-gray-900);
2237
2329
  display: grid;
2238
- gap: 0.25rem;
2239
- padding: 0.8rem;
2240
- text-align: left;
2241
- }
2242
-
2330
+ gap: 0.25rem;
2331
+ padding: 0.8rem;
2332
+ text-align: left;
2333
+ transition:
2334
+ border-color 160ms ease,
2335
+ box-shadow 160ms ease,
2336
+ transform 160ms ease;
2337
+ }
2338
+
2339
+ .attentionCard:not(:disabled):hover,
2340
+ .attentionCard:not(:disabled):focus-visible {
2341
+ border-color: var(--attention-color, var(--pw-blue-700));
2342
+ box-shadow:
2343
+ 0 0 0 2px color-mix(in srgb, var(--attention-color, var(--pw-blue-700)) 24%, transparent),
2344
+ 0 12px 24px color-mix(in srgb, var(--attention-color, var(--pw-blue-700)) 14%, transparent);
2345
+ transform: translateY(-2px);
2346
+ }
2347
+
2348
+ .attentionCard:not(:disabled):focus-visible {
2349
+ outline: none;
2350
+ }
2351
+
2243
2352
  .attentionCard.failed,
2244
2353
  .attentionCard.no_comment,
2245
2354
  .attentionCard.no_ticket {
@@ -3938,15 +4047,23 @@ pre {
3938
4047
 
3939
4048
  /* Elmulo V2 application navigation */
3940
4049
 
3941
- .appFrame {
3942
- display: grid;
3943
- grid-template-columns: auto minmax(0, 1fr);
3944
- min-height: 100vh;
3945
- }
3946
-
3947
- .workspaceShell {
3948
- min-width: 0;
3949
- }
4050
+ .appFrame {
4051
+ display: grid;
4052
+ grid-template-columns: auto minmax(0, 1fr);
4053
+ height: 100vh;
4054
+ height: 100dvh;
4055
+ min-height: 0;
4056
+ overflow: hidden;
4057
+ }
4058
+
4059
+ .workspaceShell {
4060
+ display: grid;
4061
+ grid-template-rows: auto minmax(0, 1fr);
4062
+ height: 100vh;
4063
+ height: 100dvh;
4064
+ min-width: 0;
4065
+ overflow: hidden;
4066
+ }
3950
4067
 
3951
4068
  .sidebar {
3952
4069
  background:
@@ -4074,23 +4191,40 @@ pre {
4074
4191
  position: relative;
4075
4192
  }
4076
4193
 
4077
- .sidebarIcon {
4078
- align-items: center;
4079
- background: rgb(255 255 255 / 7%);
4080
- border-radius: 9px;
4081
- display: inline-flex;
4082
- font-size: 1rem;
4083
- font-weight: 800;
4084
- height: 34px;
4085
- justify-content: center;
4086
- width: 34px;
4087
- }
4088
-
4089
- .sidebarLink.active .sidebarIcon {
4090
- background: #dff8f8;
4091
- color: #075f76;
4092
- }
4093
-
4194
+ .sidebarIcon {
4195
+ align-items: center;
4196
+ background: rgb(255 255 255 / 7%);
4197
+ border-radius: 9px;
4198
+ display: inline-flex;
4199
+ height: 34px;
4200
+ justify-content: center;
4201
+ width: 34px;
4202
+ }
4203
+
4204
+ .sidebarIconImage {
4205
+ filter: brightness(0) invert(1);
4206
+ height: 21px;
4207
+ object-fit: contain;
4208
+ opacity: 0.82;
4209
+ transition: filter 140ms ease, opacity 140ms ease, transform 140ms ease;
4210
+ width: 21px;
4211
+ }
4212
+
4213
+ .sidebarLink:hover .sidebarIconImage {
4214
+ opacity: 1;
4215
+ transform: scale(1.06);
4216
+ }
4217
+
4218
+ .sidebarLink.active .sidebarIcon {
4219
+ background: #dff8f8;
4220
+ color: #075f76;
4221
+ }
4222
+
4223
+ .sidebarLink.active .sidebarIconImage {
4224
+ filter: brightness(0) saturate(100%) invert(29%) sepia(43%) saturate(1271%) hue-rotate(147deg) brightness(91%) contrast(96%);
4225
+ opacity: 1;
4226
+ }
4227
+
4094
4228
  .sidebarLabel {
4095
4229
  font-size: 0.85rem;
4096
4230
  font-weight: 700;
@@ -4196,11 +4330,16 @@ pre {
4196
4330
  width: 38px;
4197
4331
  }
4198
4332
 
4199
- .appShell {
4200
- margin: 0 auto;
4201
- max-width: 1600px;
4202
- padding: 1.5rem 2rem 4rem;
4203
- }
4333
+ .appShell {
4334
+ height: 100%;
4335
+ margin: 0 auto;
4336
+ max-width: 1600px;
4337
+ min-height: 0;
4338
+ overflow-y: auto;
4339
+ overscroll-behavior-y: contain;
4340
+ padding: 1.5rem 2rem 4rem;
4341
+ width: 100%;
4342
+ }
4204
4343
 
4205
4344
  .workspaceView[hidden] {
4206
4345
  display: none !important;
@@ -4311,11 +4450,12 @@ pre {
4311
4450
  transform: translateY(-1px);
4312
4451
  }
4313
4452
 
4314
- .overviewLaunchGrid button > span {
4315
- color: var(--pw-teal);
4316
- font-size: 1.15rem;
4317
- font-weight: 800;
4318
- }
4453
+ .overviewLaunchIcon {
4454
+ filter: brightness(0) saturate(100%) invert(61%) sepia(78%) saturate(623%) hue-rotate(132deg) brightness(87%) contrast(82%);
4455
+ height: 24px;
4456
+ object-fit: contain;
4457
+ width: 24px;
4458
+ }
4319
4459
 
4320
4460
  .overviewLaunchGrid small {
4321
4461
  color: var(--pw-gray-700);
package/assets/app.js CHANGED
@@ -42,13 +42,13 @@
42
42
  const pdfSections = [
43
43
  ["summary", "Resumen ejecutivo", "Indicadores principales y evaluación general.", true],
44
44
  ["statusDistribution", "Distribución por estado", "Resultados automáticos y clasificaciones manuales.", true],
45
- ["runContext", "Contexto de la corrida", "Ambiente, fecha, navegador, tags, rama y pipeline.", true],
45
+ ["runContext", "Contexto de la corrida", "Ambiente, fecha, navegador, tags, rama y pipeline.", false],
46
46
  ["features", "Resultados por Feature", "Totales y estados agrupados por Feature.", true],
47
47
  ["issues", "Problemas que requieren seguimiento", "Fallos, Jira, comentarios y tickets asociados.", true],
48
- ["previousComparison", "Comparación con la corrida anterior", "Variaciones de resultados y duración.", true],
49
- ["recentHistory", "Historial reciente", "Últimas ejecuciones del mismo ambiente.", true],
50
- ["flaky", "Pruebas inestables", "Casos que pasaron después de uno o más reintentos.", true],
51
- ["recurrentFailures", "Fallos recurrentes", "Pruebas con fallos repetidos en el historial.", true],
48
+ ["previousComparison", "Comparación con la corrida anterior", "Variaciones de resultados y duración.", false],
49
+ ["recentHistory", "Historial reciente", "Últimas ejecuciones del mismo ambiente.", false],
50
+ ["flaky", "Pruebas inestables", "Casos que pasaron después de uno o más reintentos.", false],
51
+ ["recurrentFailures", "Fallos recurrentes", "Pruebas con fallos repetidos en el historial.", false],
52
52
  ["slowTests", "Pruebas más lentas", "Ranking histórico de pruebas por duración.", false],
53
53
  ["recommendation", "Recomendación y pendientes", "Evaluación final, fallos sin comentario y sin ticket.", true],
54
54
  ];
@@ -201,9 +201,10 @@
201
201
  tag: initialUrlState.get("tag") || "all",
202
202
  trendStatus: "total",
203
203
  trendEnvironment: initialTrendEnvironment,
204
- selectedTrendRunId: null,
205
- historicalRun: null,
206
- historicalRerun: null,
204
+ selectedTrendRunId: null,
205
+ historicalRun: null,
206
+ historicalRerun: null,
207
+ historicalStatusFilter: "",
207
208
  historyJiraId: null,
208
209
  historyTestTitle: "",
209
210
  historyCurrentTestId: null,
@@ -705,19 +706,32 @@
705
706
  dialog.showModal();
706
707
  }
707
708
 
708
- function metric(label, value, className = "") {
709
- return `<article class="metricCard ${className}">
710
- <span>${escapeHtml(label)}</span>
711
- <strong>${value}</strong>
712
- </article>`;
713
- }
714
-
715
- function summaryMarkup() {
716
- const effectiveStatuses = run.tests.map(effectiveTestStatus);
717
- const statusCount = (status) =>
718
- effectiveStatuses.filter((value) => value === status).length;
719
- const otherErrorsCount = effectiveStatuses.filter((status) =>
720
- otherErrorStatuses.has(status)).length;
709
+ function metric(label, value, className = "") {
710
+ return `<article class="metricCard ${className}">
711
+ <span>${escapeHtml(label)}</span>
712
+ <strong>${value}</strong>
713
+ </article>`;
714
+ }
715
+
716
+ function failureMetric(total, deltaMarkup, breakdown, className = "") {
717
+ return `<article class="metricCard failed failedBreakdown ${className}">
718
+ <div class="failureMetricTotal">
719
+ <span>Fallidas</span>
720
+ <strong>${escapeHtml(total)}${deltaMarkup}</strong>
721
+ </div>
722
+ <dl class="failureMetricBreakdown" aria-label="Desglose de pruebas fallidas">
723
+ ${breakdown.map(([label, value, status]) => `<div class="${escapeHtml(status)}">
724
+ <dt>${escapeHtml(label)}</dt>
725
+ <dd>${escapeHtml(value)}</dd>
726
+ </div>`).join("")}
727
+ </dl>
728
+ </article>`;
729
+ }
730
+
731
+ function summaryMarkup() {
732
+ const effectiveStatuses = run.tests.map(effectiveTestStatus);
733
+ const statusCount = (status) =>
734
+ effectiveStatuses.filter((value) => value === status).length;
721
735
  const activeClass = (status, baseClass = status) =>
722
736
  `${baseClass} ${state.status === status ? "activeFilter" : ""}`.trim();
723
737
  const previousRun = [...(run.trends?.runs || [])]
@@ -728,26 +742,33 @@
728
742
  const difference = Number(current) - Number(previous || 0);
729
743
  if (!difference) return '<small class="metricDelta neutral">Sin cambios</small>';
730
744
  return `<small class="metricDelta ${difference > 0 ? "up" : "down"}">${difference > 0 ? "+" : ""}${difference} vs. anterior</small>`;
731
- };
732
- const passedCount = statusCount("passed");
733
- const failedCount = statusCount("failed");
734
- const previousOtherErrors = previousRun
735
- ? Number(previousRun.environment_error || 0) +
736
- Number(previousRun.precondition_error || 0) +
737
- Number(previousRun.outdated_test || 0) +
738
- Number(previousRun.reported || 0)
739
- : null;
745
+ };
746
+ const passedCount = statusCount("passed");
747
+ const automaticFailures = run.tests.filter((test) => test.status === "failed");
748
+ const failedCount = automaticFailures.length;
749
+ const pendingFailures = automaticFailures.filter((test) =>
750
+ effectiveTestStatus(test) === "failed").length;
751
+ const reportedFailures = automaticFailures.filter((test) =>
752
+ effectiveTestStatus(test) === "reported").length;
753
+ const otherFailures = automaticFailures.filter((test) => {
754
+ const status = effectiveTestStatus(test);
755
+ return otherErrorStatuses.has(status) && status !== "reported";
756
+ }).length;
740
757
 
741
758
  return `
742
759
  ${metric("Casos", groupTestsByCase(run.tests).length)}
743
760
  ${metric("Ejecuciones", run.tests.length)}
744
761
  ${metric("Exitosas", `${passedCount}${delta(passedCount, previousRun?.passed)}`, activeClass("passed"))}
745
- ${metric("Fallidas", `${failedCount}${delta(failedCount, previousRun?.failed)}`, activeClass("failed"))}
746
- ${metric(
747
- "Otros errores",
748
- `${otherErrorsCount}${delta(otherErrorsCount, previousOtherErrors)}`,
749
- activeClass("other_errors", "otherErrors"),
750
- )}
762
+ ${failureMetric(
763
+ failedCount,
764
+ delta(failedCount, previousRun?.failed),
765
+ [
766
+ ["Pendientes", pendingFailures, "pending"],
767
+ ["Reportadas", reportedFailures, "reported"],
768
+ ["Otros errores", otherFailures, "otherErrors"],
769
+ ],
770
+ activeClass("failed"),
771
+ )}
751
772
  ${metric("Inestables", run.tests.filter((test) => test.flaky).length, "flaky")}
752
773
  `;
753
774
  }
@@ -1099,7 +1120,7 @@
1099
1120
  </div>`;
1100
1121
  }
1101
1122
 
1102
- function renderHistoricalRun() {
1123
+ function renderHistoricalRun() {
1103
1124
  const historicalRun = state.historicalRun;
1104
1125
  if (!state.selectedTrendRunId) {
1105
1126
  return `<div class="trendHistoryPlaceholder">
@@ -1110,11 +1131,39 @@
1110
1131
  return `<div class="trendHistoryPlaceholder">Cargando resultados históricos…</div>`;
1111
1132
  }
1112
1133
 
1113
- const tests = historicalRun.tests || [];
1114
- const statusCount = (status) =>
1115
- tests.filter((test) => test.status === status).length;
1116
- const otherErrors = tests.filter((test) =>
1117
- otherErrorStatuses.has(test.status)).length;
1134
+ const tests = historicalRun.tests || [];
1135
+ const historicalStatusOrder = [
1136
+ "passed",
1137
+ "failed",
1138
+ "environment_error",
1139
+ "precondition_error",
1140
+ "outdated_test",
1141
+ "reported",
1142
+ "skipped",
1143
+ "pending",
1144
+ "unknown",
1145
+ ];
1146
+ const statusCounts = tests.reduce((counts, test) => {
1147
+ const status = String(test.status || "unknown");
1148
+ counts.set(status, (counts.get(status) || 0) + 1);
1149
+ return counts;
1150
+ }, new Map());
1151
+ const visibleStatuses = [...statusCounts.entries()]
1152
+ .sort(([left], [right]) => {
1153
+ const leftIndex = historicalStatusOrder.indexOf(left);
1154
+ const rightIndex = historicalStatusOrder.indexOf(right);
1155
+ return (leftIndex < 0 ? historicalStatusOrder.length : leftIndex) -
1156
+ (rightIndex < 0 ? historicalStatusOrder.length : rightIndex);
1157
+ });
1158
+ if (
1159
+ state.historicalStatusFilter &&
1160
+ !statusCounts.has(state.historicalStatusFilter)
1161
+ ) {
1162
+ state.historicalStatusFilter = "";
1163
+ }
1164
+ const filteredTests = state.historicalStatusFilter
1165
+ ? tests.filter((test) => test.status === state.historicalStatusFilter)
1166
+ : tests;
1118
1167
  const rerun = state.historicalRerun || historicalRun.rerun || {};
1119
1168
  const rerunBusy = ["starting", "running"].includes(rerun.status);
1120
1169
  const rerunLabel = rerun.status === "starting"
@@ -1152,18 +1201,23 @@
1152
1201
  >×</button>
1153
1202
  </div>
1154
1203
  </header>
1155
- ${rerunFeedback
1156
- ? `<div class="trendRerunFeedback ${rerun.status === "failed" || rerun.status === "unavailable" ? "error" : "success"}" role="status">${escapeHtml(rerunFeedback)}</div>`
1157
- : ""}
1158
- <div class="trendHistoryStats">
1159
- <span><strong>${tests.length}</strong> ejecuciones</span>
1160
- <span class="passed"><strong>${statusCount("passed")}</strong> exitosas</span>
1161
- <span class="failed"><strong>${statusCount("failed")}</strong> fallidas</span>
1162
- <span class="otherErrors"><strong>${otherErrors}</strong> otros errores</span>
1163
- </div>
1164
- <div class="historicalTestList">
1165
- ${tests.length
1166
- ? tests.map((test) => `<article class="historicalTestRow">
1204
+ ${rerunFeedback
1205
+ ? `<div class="trendRerunFeedback ${rerun.status === "failed" || rerun.status === "unavailable" ? "error" : "success"}" role="status">${escapeHtml(rerunFeedback)}</div>`
1206
+ : ""}
1207
+ <div class="trendHistoryStats">
1208
+ <span class="historicalExecutionCount"><strong>${tests.length}</strong> ejecuciones</span>
1209
+ <div class="historicalStatusFilters" aria-label="Filtrar pruebas históricas por estado">
1210
+ ${visibleStatuses.map(([status, count]) => `<button
1211
+ class="historicalStatusFilter ${escapeHtml(status)} ${state.historicalStatusFilter === status ? "active" : ""}"
1212
+ type="button"
1213
+ data-historical-status-filter="${escapeHtml(status)}"
1214
+ aria-pressed="${state.historicalStatusFilter === status}"
1215
+ ><strong>${escapeHtml(count)}</strong> ${escapeHtml(statusLabel[status] || status)}</button>`).join("")}
1216
+ </div>
1217
+ </div>
1218
+ <div class="historicalTestList">
1219
+ ${filteredTests.length
1220
+ ? filteredTests.map((test) => `<article class="historicalTestRow">
1167
1221
  <span class="statusDot ${escapeHtml(test.status)}" aria-hidden="true"></span>
1168
1222
  <span>
1169
1223
  <strong>${escapeHtml(test.title)}</strong>
@@ -1176,7 +1230,7 @@
1176
1230
  <small>${escapeHtml(formatDuration(test.duration_ms))}</small>
1177
1231
  </span>
1178
1232
  </article>`).join("")
1179
- : `<div class="emptyState"><strong>Sin resultados</strong><span>Esta corrida no tiene pruebas registradas.</span></div>`}
1233
+ : `<div class="emptyState"><strong>Sin resultados</strong><span>Esta corrida no tiene pruebas registradas.</span></div>`}
1180
1234
  </div>
1181
1235
  </section>`;
1182
1236
  }
@@ -1205,11 +1259,20 @@
1205
1259
  const closeButton = document.querySelector("[data-close-trend-history]");
1206
1260
  if (!closeButton) return;
1207
1261
  closeButton.addEventListener("click", () => {
1208
- state.selectedTrendRunId = null;
1209
- state.historicalRun = null;
1210
- state.historicalRerun = null;
1211
- renderTrendArea();
1212
- });
1262
+ state.selectedTrendRunId = null;
1263
+ state.historicalRun = null;
1264
+ state.historicalRerun = null;
1265
+ state.historicalStatusFilter = "";
1266
+ renderTrendArea();
1267
+ });
1268
+ document.querySelectorAll("[data-historical-status-filter]").forEach((button) => {
1269
+ button.addEventListener("click", () => {
1270
+ const status = button.dataset.historicalStatusFilter;
1271
+ state.historicalStatusFilter =
1272
+ state.historicalStatusFilter === status ? "" : status;
1273
+ renderTrendHistoryPanel();
1274
+ });
1275
+ });
1213
1276
  document.querySelector("[data-rerun-trend-report]")?.addEventListener(
1214
1277
  "click",
1215
1278
  startHistoricalRerun,
@@ -1282,10 +1345,11 @@
1282
1345
  function bindTrendEvents() {
1283
1346
  document.querySelectorAll("[data-trend-run-id]").forEach((button) => {
1284
1347
  button.addEventListener("click", async () => {
1285
- const requestedRunId = button.dataset.trendRunId;
1286
- state.selectedTrendRunId = requestedRunId;
1287
- state.historicalRun = null;
1288
- state.historicalRerun = null;
1348
+ const requestedRunId = button.dataset.trendRunId;
1349
+ state.selectedTrendRunId = requestedRunId;
1350
+ state.historicalRun = null;
1351
+ state.historicalRerun = null;
1352
+ state.historicalStatusFilter = "";
1289
1353
  renderTrendArea();
1290
1354
 
1291
1355
  try {
@@ -1312,9 +1376,10 @@
1312
1376
 
1313
1377
  async function loadTrendEnvironment(environment) {
1314
1378
  state.trendEnvironment = environment;
1315
- state.selectedTrendRunId = null;
1316
- state.historicalRun = null;
1317
- state.historicalRerun = null;
1379
+ state.selectedTrendRunId = null;
1380
+ state.historicalRun = null;
1381
+ state.historicalRerun = null;
1382
+ state.historicalStatusFilter = "";
1318
1383
  const trendContent = document.getElementById("trend-content");
1319
1384
  const trendHistory = document.getElementById("trend-history");
1320
1385
  const trendCounter = document.getElementById("trend-run-count");
@@ -2559,35 +2624,35 @@
2559
2624
  }
2560
2625
 
2561
2626
  const viewMetadata = {
2562
- overview: {
2563
- label: "Resumen",
2564
- description: "Estado de la corrida y señales que requieren atención.",
2565
- icon: "",
2566
- },
2627
+ overview: {
2628
+ label: "Resumen",
2629
+ description: "Estado de la corrida y señales que requieren atención.",
2630
+ icon: "summary.png",
2631
+ },
2567
2632
  executions: {
2568
2633
  label: "Ejecuciones",
2569
2634
  description: "Tendencias, historial y comparación entre corridas.",
2570
- icon: "",
2635
+ icon: "executions.png",
2571
2636
  },
2572
2637
  quality: {
2573
2638
  label: "Calidad",
2574
2639
  description: "Estabilidad, fallos recurrentes y rendimiento histórico.",
2575
- icon: "",
2640
+ icon: "quality.png",
2576
2641
  },
2577
2642
  analysis: {
2578
2643
  label: "Análisis",
2579
2644
  description: "Bandeja de trabajo para investigar y clasificar fallos.",
2580
- icon: "!",
2645
+ icon: "analysis.png",
2581
2646
  },
2582
2647
  tests: {
2583
2648
  label: "Pruebas",
2584
2649
  description: "Explorador de casos, pasos, evidencias e historial por Jira.",
2585
- icon: "",
2650
+ icon: "tests.png",
2586
2651
  },
2587
2652
  preferences: {
2588
2653
  label: "Preferencias",
2589
2654
  description: "Apariencia y configuración local del espacio de trabajo.",
2590
- icon: "",
2655
+ icon: "preferences.png",
2591
2656
  },
2592
2657
  };
2593
2658
 
@@ -2747,7 +2812,9 @@
2747
2812
  aria-current="${state.view === key ? "page" : "false"}"
2748
2813
  title="${escapeHtml(item.label)}"
2749
2814
  >
2750
- <span class="sidebarIcon" aria-hidden="true">${item.icon}</span>
2815
+ <span class="sidebarIcon" aria-hidden="true">
2816
+ <img class="sidebarIconImage" src="assets/menu-icons/${escapeHtml(item.icon)}" alt="">
2817
+ </span>
2751
2818
  <span class="sidebarLabel">${escapeHtml(item.label)}</span>
2752
2819
  ${badges[key] ? `<span class="sidebarBadge">${escapeHtml(badges[key])}</span>` : ""}
2753
2820
  </button>`).join("")}
@@ -2759,20 +2826,22 @@
2759
2826
  </aside>`;
2760
2827
  }
2761
2828
 
2762
- function overviewLaunchpadMarkup() {
2763
- const failed = run.tests.filter((test) => effectiveTestStatus(test) === "failed");
2764
- const withoutComment = failed.filter((test) => !annotationFor(test).comment).length;
2765
- return `<section class="overviewLaunchpad" aria-labelledby="overview-next-title">
2829
+ function overviewLaunchpadMarkup() {
2830
+ const failed = run.tests.filter((test) => effectiveTestStatus(test) === "failed");
2831
+ const withoutComment = failed.filter((test) => !annotationFor(test).comment).length;
2832
+ const launchIcon = (view) =>
2833
+ `<img class="overviewLaunchIcon" src="assets/menu-icons/${escapeHtml(viewMetadata[view].icon)}" alt="">`;
2834
+ return `<section class="overviewLaunchpad" aria-labelledby="overview-next-title">
2766
2835
  <header>
2767
2836
  <div><p class="eyebrow">Explorar</p><h2 id="overview-next-title">Continuá el análisis</h2></div>
2768
2837
  <span>Cada área conserva el ambiente, los filtros y la prueba seleccionada.</span>
2769
- </header>
2770
- <div class="overviewLaunchGrid">
2771
- <button type="button" data-nav-view="executions"><span>▶</span><strong>Ejecuciones</strong><small>${escapeHtml(run.trends?.totalRuns || 0)} corridas disponibles</small></button>
2772
- <button type="button" data-nav-view="quality"><span>◇</span><strong>Calidad histórica</strong><small>Estabilidad, recurrencia y tiempos</small></button>
2773
- <button type="button" data-nav-view="analysis"><span>!</span><strong>Análisis pendiente</strong><small>${withoutComment} fallos sin comentario</small></button>
2774
- <button type="button" data-nav-view="tests"><span>☑</span><strong>Explorar pruebas</strong><small>${run.tests.length} ejecuciones en la corrida</small></button>
2775
- </div>
2838
+ </header>
2839
+ <div class="overviewLaunchGrid">
2840
+ <button type="button" data-nav-view="executions">${launchIcon("executions")}<strong>Ejecuciones</strong><small>${escapeHtml(run.trends?.totalRuns || 0)} corridas disponibles</small></button>
2841
+ <button type="button" data-nav-view="quality">${launchIcon("quality")}<strong>Calidad histórica</strong><small>Estabilidad, recurrencia y tiempos</small></button>
2842
+ <button type="button" data-nav-view="analysis">${launchIcon("analysis")}<strong>Análisis pendiente</strong><small>${withoutComment} fallos sin comentario</small></button>
2843
+ <button type="button" data-nav-view="tests">${launchIcon("tests")}<strong>Explorar pruebas</strong><small>${run.tests.length} ejecuciones en la corrida</small></button>
2844
+ </div>
2776
2845
  </section>`;
2777
2846
  }
2778
2847
 
Binary file
Binary file
Binary file
Binary file
Binary file
package/finalize.cjs CHANGED
@@ -435,7 +435,7 @@ function persistRun(database, run) {
435
435
  system: run.system || {},
436
436
  source: run.source || {},
437
437
  execution: run.execution || null,
438
- reporterVersion: "2.0.0-beta.7",
438
+ reporterVersion: "2.0.0-beta.9",
439
439
  }),
440
440
  ],
441
441
  );
@@ -932,7 +932,7 @@ async function finalizeRun(options = {}) {
932
932
  if (!run) throw new Error(`No se encontró ${rawPath}`);
933
933
 
934
934
  run.schemaVersion = DATABASE_SCHEMA_VERSION;
935
- run.reporterVersion = "2.0.0-beta.7";
935
+ run.reporterVersion = "2.0.0-beta.9";
936
936
  run.lifecycle = run.lifecycle || "completed";
937
937
  run.source = run.source || {
938
938
  branch: process.env.CI_COMMIT_REF_NAME || "",
package/jira.cjs CHANGED
@@ -303,12 +303,12 @@ async function jiraRequest(config, pathname, options = {}) {
303
303
  }
304
304
 
305
305
  async function findMatchingIssues(config, fingerprint) {
306
- const jql = [
306
+ const conditions = [
307
307
  `project = "${config.projectKey}"`,
308
308
  `issuetype = "${config.issueTypeName}"`,
309
309
  `labels = "${fingerprint}"`,
310
- "ORDER BY created DESC",
311
310
  ].join(" AND ");
311
+ const jql = `${conditions} ORDER BY created DESC`;
312
312
  const query = new URLSearchParams({
313
313
  jql,
314
314
  fields: "summary,status,labels",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alejandrojca/elmulo-reporter",
3
- "version": "2.0.0-beta.7",
3
+ "version": "2.0.0-beta.9",
4
4
  "description": "Reporter independiente para ejecuciones de Cypress con dashboard, historial SQLite y evidencias.",
5
5
  "type": "commonjs",
6
6
  "main": "plugin.cjs",
package/plugin.cjs CHANGED
@@ -57,7 +57,7 @@ function registerElmuloReporter(on, config, options = {}) {
57
57
  "unknown",
58
58
  tagExpression: config.env?.TAGS || process.env.CYPRESS_TAGS || "",
59
59
  lifecycle: "running",
60
- reporterVersion: "2.0.0-beta.7",
60
+ reporterVersion: "2.0.0-beta.9",
61
61
  execution: executionScript
62
62
  ? { runner: "npm", script: executionScript, args: executionArgs }
63
63
  : null,
package/server.cjs CHANGED
@@ -182,7 +182,7 @@ async function serveReport(options = {}) {
182
182
  if (request.method === "GET" && url.pathname === "/api/health") {
183
183
  sendJson(response, 200, {
184
184
  ok: true,
185
- version: "2.0.0-beta.7",
185
+ version: "2.0.0-beta.9",
186
186
  schemaVersion: 3,
187
187
  });
188
188
  return;