testronaut 1.0.16 → 1.0.18

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/README.md CHANGED
@@ -24,6 +24,22 @@ Join our Discord to connect with other Testronauts, get support, and help shape
24
24
 
25
25
  ---
26
26
 
27
+ ## 📖 Documentation
28
+
29
+ Looking for deeper guides, API references, and examples?
30
+ Head over to the official Testronaut Docs:
31
+
32
+ [![Read the Docs](https://img.shields.io/badge/Read%20the%20Docs-2E8555?style=for-the-badge&logo=readthedocs&logoColor=white)](https://docs.testronaut.app)
33
+
34
+ The docs cover:
35
+ - Getting started step-by-step
36
+ - Writing advanced missions
37
+ - CLI options and configuration
38
+ - Integrating with Mission Control
39
+ - Troubleshooting & FAQs
40
+
41
+ ---
42
+
27
43
  ## 📦 Installation
28
44
 
29
45
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testronaut",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "main": "./runner/testronaut.js",
5
5
  "bin": {
6
6
  "testronaut": "./bin/cli.js"
@@ -1,11 +1,15 @@
1
+ // generateHtmlReport.js
1
2
  import fs from 'fs';
2
3
  import path from 'path';
3
4
 
4
5
  export function generateHtmlReport(report, outputPath) {
5
6
  const { runId, startTime, endTime, missions = [], summary = {} } = report;
6
- const durationSec = ((new Date(endTime) - new Date(startTime)) / 1000).toFixed(2);
7
+ const durationSec =
8
+ (startTime && endTime)
9
+ ? ((new Date(endTime) - new Date(startTime)) / 1000).toFixed(2)
10
+ : '—';
7
11
 
8
- // tiny helpers
12
+ // helpers
9
13
  const esc = (s) => String(s ?? '')
10
14
  .replace(/&/g, '&amp;').replace(/</g, '&lt;')
11
15
  .replace(/>/g, '&gt;');
@@ -13,24 +17,25 @@ export function generateHtmlReport(report, outputPath) {
13
17
  status === 'passed' ? '✅ Passed' :
14
18
  status === 'failed' ? '❌ Failed' : (status || '—');
15
19
 
16
- // const missionBlock = (m, idx) => {
17
- // Render ONE submission (premission/mission/postmission)
18
- const submissionBlock = (m, idx) => {
19
- const mDurationSec = m.endTime && m.startTime
20
- ? ((m.endTime - m.startTime) / 1000).toFixed(2)
21
- : '—';
20
+ const submissionBlock = (m) => {
21
+ const mDurationSec =
22
+ m.endTime && m.startTime
23
+ ? ((m.endTime - m.startTime) / 1000).toFixed(2)
24
+ : (typeof m.durationSeconds === 'number' ? m.durationSeconds.toFixed(2) : '—');
22
25
 
23
26
  const steps = Array.isArray(m.steps) ? m.steps : [];
24
- const stepItems = steps.map(step => {
27
+ const stepItems = steps.map((step, idx) => {
25
28
  const events = Array.isArray(step.events) ? step.events : [];
29
+ const ok = /✅|Passed|Mission Success/i.test(step.result || '');
26
30
  const imgTag = step.screenshotPath
27
- ? `<img src="${esc(step.screenshotPath)}" alt="screenshot turn ${esc(step.turn)}">`
31
+ ? `<img src="${esc(step.screenshotPath)}" alt="screenshot turn ${esc(step.turn ?? idx)}">`
28
32
  : '';
33
+
29
34
  return `
30
- <details class="step" ${/Mission Success|Passed/.test(step.result||'') ? '' : 'open'}>
35
+ <details class="step" ${ok ? '' : 'open'}>
31
36
  <summary>
32
- <span class="turn">Turn ${esc((step.turn ?? 0) + 1)}</span>
33
- <span class="step-result ${/✅/.test(step.result||'') ? 'ok' : 'bad'}">${esc(step.result || '')}</span>
37
+ <span class="turn">Turn ${esc((step.turn ?? idx) + 1)}</span>
38
+ <span class="step-result ${ok ? 'ok' : 'bad'}">${esc(step.result || '—')}</span>
34
39
  <span class="tokens">tokens: ${esc(step.tokensUsed ?? '—')} / total: ${esc(step.totalTokensUsed ?? '—')}</span>
35
40
  </summary>
36
41
  <pre class="events">${esc(events.join('\n')) || '(no events)'}</pre>
@@ -39,21 +44,18 @@ export function generateHtmlReport(report, outputPath) {
39
44
  `;
40
45
  }).join('');
41
46
 
42
- // Build a clearer title: "<mission> — premission: login"
43
- const type = (m.submissionType || '').toLowerCase();
44
- const typeLabel =
45
- type === 'premission' ? 'premission' :
46
- type === 'postmission' ? 'postmission' :
47
- 'mission';
47
+ const type = String(m.submissionType || 'mission').toLowerCase();
48
+ const typeLabel = (type === 'premission' || type === 'postmission') ? type : 'mission';
48
49
  const prettyTitle = `${esc(m.missionName || 'Mission')} — ${typeLabel}${m.submissionName ? `: ${esc(m.submissionName)}` : ''}`;
49
-
50
+ const statusClass = m.status === 'failed' ? 'bad' : (m.status === 'passed' ? 'ok' : '');
50
51
 
51
52
  return `
52
53
  <details class="mission-submission">
53
54
  <summary>
54
55
  <span class="name">${prettyTitle}</span>
55
- <span class="status ${m.status === 'passed' ? 'ok' : (m.status === 'failed' ? 'bad' : '')}">${badge(m.status)}</span>
56
+ <span class="status ${statusClass}">${badge(m.status)}</span>
56
57
  <span class="meta">steps: ${steps.length} • duration: ${mDurationSec}s</span>
58
+ </summary>
57
59
  <div class="steps">
58
60
  ${stepItems || '<div class="empty">No steps recorded.</div>'}
59
61
  </div>
@@ -61,17 +63,15 @@ export function generateHtmlReport(report, outputPath) {
61
63
  `;
62
64
  };
63
65
 
64
- // Group submissions by missionName
66
+ // group submissions by mission
65
67
  const grouped = missions.reduce((acc, m) => {
66
68
  const key = m.missionName || '(unnamed mission)';
67
69
  (acc[key] ||= []).push(m);
68
70
  return acc;
69
71
  }, {});
70
72
 
71
- // Compute overall status for a group (any failure => failed)
72
73
  const groupStatus = (subs) => subs.some(s => s.status === 'failed') ? 'failed' : 'passed';
73
74
 
74
- // Render a mission group with its submissions collapsed inside
75
75
  const missionGroupBlock = (missionName, subs) => {
76
76
  const status = groupStatus(subs);
77
77
  const totalSteps = subs.reduce((n, s) => n + (Array.isArray(s.steps) ? s.steps.length : 0), 0);
@@ -79,15 +79,18 @@ export function generateHtmlReport(report, outputPath) {
79
79
  const lastEnd = Math.max(...subs.map(s => s.endTime || 0).filter(Boolean));
80
80
  const groupDur = (firstStart && lastEnd) ? ((lastEnd - firstStart) / 1000).toFixed(2) : '—';
81
81
 
82
- // consistent order: pre → mission → post
82
+ // pre → mission → post
83
83
  const order = { premission: 0, mission: 1, postmission: 2 };
84
- subs.sort((a, b) => (order[(a.submissionType||'mission')] ?? 1) - (order[(b.submissionType||'mission')] ?? 1));
84
+ subs.sort((a, b) =>
85
+ (order[(a.submissionType || 'mission')] ?? 1) -
86
+ (order[(b.submissionType || 'mission')] ?? 1)
87
+ );
85
88
 
86
89
  return `
87
90
  <details class="mission-group">
88
91
  <summary>
89
92
  <span class="name">${esc(missionName)}</span>
90
- <span class="status ${status === 'passed' ? 'ok' : 'bad'}">${badge(status)}</span>
93
+ <span class="status ${status === 'failed' ? 'bad' : 'ok'}">${badge(status)}</span>
91
94
  <span class="meta">submissions: ${subs.length} • steps: ${totalSteps} • duration: ${groupDur}s</span>
92
95
  </summary>
93
96
  <div class="group-body">
@@ -97,92 +100,163 @@ export function generateHtmlReport(report, outputPath) {
97
100
  `;
98
101
  };
99
102
 
100
- const html = `
101
- <!DOCTYPE html>
103
+ const groupsHtml = Object.entries(grouped).map(([name, subs]) => missionGroupBlock(name, subs)).join('');
104
+
105
+ // compute totals for chips if not provided
106
+ const totals = Object.keys(grouped).reduce((acc, name) => {
107
+ const st = groupStatus(grouped[name]);
108
+ acc.total += 1;
109
+ acc.passed += st === 'passed' ? 1 : 0;
110
+ acc.failed += st === 'failed' ? 1 : 0;
111
+ return acc;
112
+ }, { total: 0, passed: 0, failed: 0 });
113
+
114
+ const html = `<!DOCTYPE html>
102
115
  <html>
103
116
  <head>
104
117
  <meta charset="UTF-8" />
105
118
  <title>Testronaut Report – ${esc(runId)}</title>
119
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
106
120
  <style>
107
- :root {
108
- --ok: #16a34a;
109
- --bad: #dc2626;
110
- --muted: #6b7280;
111
- --bg: #f8fafc;
112
- --card: #ffffff;
113
- --border: #e5e7eb;
114
- --mono: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
121
+ /* ===== Brand + surface tokens (match splash/app) ===== */
122
+ :root{
123
+ --bg-top:#0b1022;
124
+ --bg-mid:#0f172a;
125
+ --bg-btm:#0b1022;
126
+
127
+ --hairline:rgba(255,255,255,.12);
128
+ --hairline-strong:rgba(255,255,255,.18);
129
+
130
+ --text:rgba(255,255,255,.92);
131
+ --text-muted:rgba(255,255,255,.65);
132
+
133
+ --ok:#22c55e; /* green-500 */
134
+ --bad:#ef4444; /* red-500 */
135
+
136
+ --chip-ok-bg:rgba(34,197,94,.14);
137
+ --chip-bad-bg:rgba(239,68,68,.16);
138
+ --chip-border:rgba(255,255,255,.22);
139
+ --chip-ok-border:rgba(34,197,94,.40);
140
+ --chip-bad-border:rgba(239,68,68,.45);
141
+ }
142
+
143
+ /* ===== Base ===== */
144
+ html,body{height:100%;}
145
+ body{
146
+ margin:0; padding:24px;
147
+ font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Helvetica,Arial;
148
+ background: linear-gradient(180deg,var(--bg-top) 0%,var(--bg-mid) 60%,var(--bg-btm) 100%);
149
+ color: var(--text);
150
+ -webkit-font-smoothing: antialiased;
151
+ -moz-osx-font-smoothing: grayscale;
152
+ }
153
+
154
+ /* ===== Utilities ===== */
155
+ .gradient-text{
156
+ background: linear-gradient(90deg,#60a5fa,#a78bfa,#34d399);
157
+ -webkit-background-clip:text; background-clip:text; color:transparent;
158
+ }
159
+ .glass{
160
+ background: rgba(255,255,255,.05);
161
+ border: 1px solid var(--hairline);
162
+ border-radius:16px;
163
+ backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
164
+ box-shadow: 0 10px 30px rgba(59,130,246,.20);
165
+ }
166
+
167
+ /* ===== Header ===== */
168
+ .header{ max-width:960px; margin:0 auto 16px; padding:16px 18px; }
169
+ h1{ margin:0 0 6px; font-size:22px; font-weight:800; color:#fff; }
170
+ .run-meta{ color:var(--text-muted); display:grid; gap:4px; }
171
+
172
+ /* ===== Chips ===== */
173
+ .summary{ display:flex; flex-wrap:wrap; gap:12px; margin:16px auto 20px; max-width:960px; }
174
+ .pill{
175
+ border:1px solid var(--chip-border);
176
+ background: rgba(255,255,255,.06);
177
+ padding:6px 12px; border-radius:999px; font-size:12px; color:var(--text);
178
+ letter-spacing:.2px;
179
+ }
180
+ .pill.ok{ background: var(--chip-ok-bg); color: var(--ok); border-color: var(--chip-ok-border); font-weight:700; }
181
+ .pill.bad{ background: var(--chip-bad-bg); color: var(--bad); border-color: var(--chip-bad-border); font-weight:700; }
182
+
183
+ /* ===== Disclosure blocks ===== */
184
+ details{ background: rgba(255,255,255,.05); border:1px solid var(--hairline); border-radius:16px; margin:10px 0; overflow:hidden; }
185
+ summary{
186
+ cursor:pointer; padding:14px 16px; display:flex; align-items:center; gap:12px;
187
+ color:#fff; background: rgba(255,255,255,.04);
188
+ }
189
+ .mission-group > summary{ font-weight:800; font-size:15px; letter-spacing:.2px; }
190
+ .mission-group > summary:hover{ background: rgba(255,255,255,.08); }
191
+
192
+ .group-body{ padding:12px; display:grid; gap:10px; }
193
+
194
+ .mission-submission > summary{
195
+ font-weight:700; background: rgba(255,255,255,.06); border-top:1px solid var(--hairline);
115
196
  }
116
- body { margin:0; padding:24px; font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu; background: var(--bg); color:#111827; }
117
- h1 { margin: 0 0 6px 0; font-size: 20px; }
118
- .run-meta { color: var(--muted); margin-bottom: 16px; }
119
- .summary { display:flex; gap:12px; margin: 12px 0 20px; }
120
- .pill { border:1px solid var(--border); background: var(--card); padding:6px 10px; border-radius:999px; font-size:12px; }
121
- .pill.ok { border-color: #bbf7d0; background:#ecfdf5; color: var(--ok); }
122
- .pill.bad { border-color: #fecaca; background:#fef2f2; color: var(--bad); }
123
-
124
- details { background: var(--card); border:1px solid var(--border); border-radius:10px; margin:10px 0; }
125
- summary { cursor:pointer; padding:12px 14px; display:flex; align-items:center; gap:12px; }
126
- .mission-group > summary { font-weight:700; }
127
- .mission-submission > summary { font-weight:600; }
128
- /* Better contrast on summaries */
129
- .mission-submission > summary { background:#f3f4f6; }
130
- .mission-submission > summary:hover { background:#e5e7eb; }
131
- .mission-group > summary:hover { background:#f9fafb; }
132
- /* Focus ring */
133
- summary:focus-visible { outline: 2px solid #94a3b8; outline-offset: 2px; border-radius: 8px; }
134
- /* Disclosure caret */
135
- summary::marker { color: var(--muted); }
136
- .group-body { padding: 0 10px 10px; }
137
- .name { flex:1; }
138
- .status.ok { color: var(--ok); }
139
- .status.bad { color: var(--bad); }
140
- .meta { color: var(--muted); font-size:12px; }
141
-
142
- .steps { padding: 0 14px 12px; }
143
- .step summary { background: #f8fafc; border-bottom:1px solid var(--border); border-radius: 10px 10px 0 0; }
144
- .turn { font-weight:600; }
145
- .step-result.ok { color: var(--ok); }
146
- .step-result.bad { color: var(--bad); }
147
- .tokens { color: var(--muted); font-size:12px; margin-left:auto; }
148
- .events { background:#0b1020; color:#e5e7eb; padding:12px; border-radius:8px; margin:12px 0 0; font: 12px/1.4 var(--mono); max-height: 280px; overflow:auto; }
149
- img { display:block; max-width:100%; border:1px solid var(--border); border-radius:8px; margin-top:10px; }
150
- .empty { color: var(--muted); padding: 12px; }
197
+ .mission-submission > summary:hover{ background: rgba(255,255,255,.08); }
198
+
199
+ .name{ flex:1; }
200
+ .status.ok{ color: var(--ok); font-weight:700; }
201
+ .status.bad{ color: var(--bad); font-weight:700; }
202
+ .meta{ color: var(--text-muted); font-size:12px; }
203
+
204
+ .steps{ padding: 0 12px 12px; }
205
+ .step summary{
206
+ background: rgba(255,255,255,.04);
207
+ border:1px solid var(--hairline);
208
+ border-radius:12px;
209
+ }
210
+ .step summary:hover{ background: rgba(255,255,255,.08); }
211
+
212
+ .turn{ font-weight:700; }
213
+ .step-result.ok{ color: var(--ok); font-weight:700; }
214
+ .step-result.bad{ color: var(--bad); font-weight:700; }
215
+ .tokens{ color: var(--text-muted); font-size:12px; margin-left:auto; font-variant-numeric: tabular-nums; }
216
+
217
+ .events{
218
+ background: rgba(2,6,23,.9); /* near #020617 */
219
+ color: rgba(255,255,255,.92);
220
+ padding:12px; border-radius:12px; margin:12px 0 0;
221
+ font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New";
222
+ max-height:320px; overflow:auto; white-space:pre-wrap;
223
+ border: 1px solid var(--hairline-strong);
224
+ }
225
+
226
+ img{
227
+ display:block; max-width:100%;
228
+ border:1px solid var(--hairline); border-radius:12px; margin-top:10px;
229
+ background: rgba(0,0,0,.2);
230
+ }
231
+
232
+ /* container width */
233
+ .container{ max-width:960px; margin:0 auto; }
234
+ .empty{ color: var(--text-muted); padding:12px; }
151
235
  </style>
152
236
  </head>
153
237
  <body>
154
- <h1>🧑‍🚀 Testronaut Report</h1>
155
- <div class="run-meta">
156
- <div><strong>Run ID:</strong> ${esc(runId)}</div>
157
- <div><strong>Start:</strong> ${esc(startTime)}</div>
158
- <div><strong>End:</strong> ${esc(endTime)} • <strong>Duration:</strong> ${durationSec}s</div>
238
+ <div class="header glass">
239
+ <h1>🧑‍🚀 <span class="gradient-text">Testronaut Report</span></h1>
240
+ <div class="run-meta">
241
+ <div><strong>Run ID:</strong> ${esc(runId ?? '—')}</div>
242
+ <div><strong>Start:</strong> ${esc(startTime ?? '—')}</div>
243
+ <div><strong>End:</strong> ${esc(endTime ?? '—')} • <strong>Duration:</strong> ${durationSec}s</div>
244
+ </div>
159
245
  </div>
160
246
 
161
247
  <div class="summary">
162
- ${(() => {
163
- const groupNames = Object.keys(grouped);
164
- const totals = groupNames.reduce((acc, name) => {
165
- const st = groupStatus(grouped[name]);
166
- acc.total += 1;
167
- acc.passed += (st === 'passed') ? 1 : 0;
168
- acc.failed += (st === 'failed') ? 1 : 0;
169
- return acc;
170
- }, { total: 0, passed: 0, failed: 0 });
171
- return `
172
- <div class="pill">Missions: ${esc(summary.totalMissions ?? totals.total)}</div>
173
- <div class="pill ok">Passed: ${esc(summary.passed ?? totals.passed)}</div>
174
- <div class="pill bad">Failed: ${esc(summary.failed ?? totals.failed)}</div>
175
- `;
176
- })()}
248
+ <div class="pill">Missions: ${esc(summary.totalMissions ?? totals.total)}</div>
249
+ <div class="pill ok">Passed: ${esc(summary.passed ?? totals.passed)}</div>
250
+ <div class="pill bad">Failed: ${esc(summary.failed ?? totals.failed)}</div>
177
251
  </div>
178
252
 
179
- ${Object.entries(grouped).map(([name, subs]) => missionGroupBlock(name, subs)).join('')}
180
-
253
+ <div class="container">
254
+ ${groupsHtml || '<div class="glass empty">No missions recorded.</div>'}
255
+ </div>
181
256
  </body>
182
- </html>
183
- `;
257
+ </html>`;
184
258
 
185
- const out = outputPath ?? path.resolve('missions/mission_reports', `${runId}.html`);
259
+ const out = outputPath ?? path.resolve('missions/mission_reports', `${runId || 'report'}.html`);
186
260
  fs.mkdirSync(path.dirname(out), { recursive: true });
187
261
  fs.writeFileSync(out, html);
188
262
  console.log(`📝 Report generated at: ${out}`);