testronaut 1.0.13 → 1.0.15

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
@@ -58,8 +58,10 @@ export const loginMission = `
58
58
  Visit ${process.env.URL}.
59
59
  Fill the username field with ${process.env.USERNAME}.
60
60
  Fill the password field with ${process.env.PASSWORD}.
61
+ Take a screenshot.
61
62
  Submit the form.
62
63
  Wait for the dashboard to appear.
64
+ Take another screenshot.
63
65
  Report SUCCESS if the dashboard is loaded, otherwise report FAILURE.
64
66
  `;
65
67
 
@@ -92,6 +94,7 @@ export const fillContactFormMission =
92
94
  Input the phone number (555)555-5555 into the phone number field.
93
95
  Click the submit button.
94
96
  Upon submission success there should be a toast notification indicating the form information was saved successfully.
97
+ Take a screenshot.
95
98
  If found, report SUCCESS. Otherwise, report FAILURE.`
96
99
 
97
100
  export async function executeMission() {
@@ -154,6 +157,22 @@ Now you can use testronaut in any local project.
154
157
  ### 📄 License
155
158
  MIT
156
159
 
160
+ --
161
+
162
+ ### 📊 Track Your Missions Over Time
163
+ Want to see your test results evolve, spot trends, and keep all your reports in one place?
164
+ Sign up for **Mission Control** — your personal dashboard for Testronaut.
165
+
166
+ With Mission Control you can:
167
+ - 📈 Track your mission reports historically
168
+ - 🔍 Drill into past runs with full step-by-step details
169
+ - 🛠️ Compare results between different builds or environments
170
+ - ☁️ Access reports from anywhere, without digging through local files
171
+
172
+ Getting started is simple:
173
+ 1. [![Sign up for Mission Control](https://img.shields.io/badge/Sign%20Up%20for%20Mission%20Control-0A84FF?style=for-the-badge&logo=rocket&logoColor=white)](https://mission.testronaut.app)
174
+ 2. Link your account to Testronaut CLI
175
+ 3. Watch your mission history grow!
157
176
 
158
177
  ---
159
178
 
package/core/agent.js CHANGED
@@ -47,6 +47,8 @@ export async function runAgent(goals, missionName, maxTurns = 20, ) {
47
47
 
48
48
  missionResults.push({
49
49
  missionName,
50
+ submissionType: goal.submissionType || goal.label,
51
+ submissionName: goal.submissionName || null,
50
52
  status: result.success ? 'passed' : 'failed',
51
53
  steps: JSON.parse(JSON.stringify(steps)), // 👈 deep-clone to avoid refs
52
54
  startTime: Date.now() - 1, // set real values if you have them
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testronaut",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "main": "./runner/testronaut.js",
5
5
  "bin": {
6
6
  "testronaut": "./bin/cli.js"
@@ -26,15 +26,36 @@ export async function runMissions({ preMission, mission, postMission }, missionN
26
26
  const goals = [];
27
27
 
28
28
  if (pre.length) {
29
- goals.push(...pre.map(fn => ({ goal: fn, label: 'pre-mission' })));
29
+ goals.push(
30
+ ...pre.map((fn, i) => ({
31
+ goal: fn,
32
+ label: 'pre-mission', // legacy label
33
+ submissionType: 'premission', // normalized
34
+ submissionName: fn?.name || `pre-${i + 1}`,
35
+ }))
36
+ );
30
37
  }
31
38
 
32
39
  if (main.length) {
33
- goals.push(...main.map(fn => ({ goal: fn, label: 'mission' })));
40
+ goals.push(
41
+ ...main.map((fn, i) => ({
42
+ goal: fn,
43
+ label: 'mission',
44
+ submissionType: 'mission',
45
+ submissionName: fn?.name || `mission-${i + 1}`,
46
+ }))
47
+ );
34
48
  }
35
49
 
36
50
  if (post.length) {
37
- goals.push(...post.map(fn => ({ goal: fn, label: 'post-mission' })));
51
+ goals.push(
52
+ ...post.map((fn, i) => ({
53
+ goal: fn,
54
+ label: 'post-mission',
55
+ submissionType: 'postmission',
56
+ submissionName: fn?.name || `post-${i + 1}`,
57
+ }))
58
+ );
38
59
  }
39
60
 
40
61
  console.log(
@@ -13,7 +13,9 @@ export function generateHtmlReport(report, outputPath) {
13
13
  status === 'passed' ? '✅ Passed' :
14
14
  status === 'failed' ? '❌ Failed' : (status || '—');
15
15
 
16
- const missionBlock = (m, idx) => {
16
+ // const missionBlock = (m, idx) => {
17
+ // Render ONE submission (premission/mission/postmission)
18
+ const submissionBlock = (m, idx) => {
17
19
  const mDurationSec = m.endTime && m.startTime
18
20
  ? ((m.endTime - m.startTime) / 1000).toFixed(2)
19
21
  : '—';
@@ -37,13 +39,21 @@ export function generateHtmlReport(report, outputPath) {
37
39
  `;
38
40
  }).join('');
39
41
 
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';
48
+ const prettyTitle = `${esc(m.missionName || 'Mission')} — ${typeLabel}${m.submissionName ? `: ${esc(m.submissionName)}` : ''}`;
49
+
50
+
40
51
  return `
41
- <details class="mission">
52
+ <details class="mission-submission">
42
53
  <summary>
43
- <span class="name">${esc(m.missionName || path.basename(m.file || `mission_${idx+1}`))}</span>
54
+ <span class="name">${prettyTitle}</span>
44
55
  <span class="status ${m.status === 'passed' ? 'ok' : (m.status === 'failed' ? 'bad' : '')}">${badge(m.status)}</span>
45
- <span class="meta">steps: ${steps.length} • duration: ${mDurationSec}s • file: ${esc(m.file || '')}</span>
46
- </summary>
56
+ <span class="meta">steps: ${steps.length} • duration: ${mDurationSec}s</span>
47
57
  <div class="steps">
48
58
  ${stepItems || '<div class="empty">No steps recorded.</div>'}
49
59
  </div>
@@ -51,6 +61,42 @@ export function generateHtmlReport(report, outputPath) {
51
61
  `;
52
62
  };
53
63
 
64
+ // Group submissions by missionName
65
+ const grouped = missions.reduce((acc, m) => {
66
+ const key = m.missionName || '(unnamed mission)';
67
+ (acc[key] ||= []).push(m);
68
+ return acc;
69
+ }, {});
70
+
71
+ // Compute overall status for a group (any failure => failed)
72
+ const groupStatus = (subs) => subs.some(s => s.status === 'failed') ? 'failed' : 'passed';
73
+
74
+ // Render a mission group with its submissions collapsed inside
75
+ const missionGroupBlock = (missionName, subs) => {
76
+ const status = groupStatus(subs);
77
+ const totalSteps = subs.reduce((n, s) => n + (Array.isArray(s.steps) ? s.steps.length : 0), 0);
78
+ const firstStart = Math.min(...subs.map(s => s.startTime || 0).filter(Boolean));
79
+ const lastEnd = Math.max(...subs.map(s => s.endTime || 0).filter(Boolean));
80
+ const groupDur = (firstStart && lastEnd) ? ((lastEnd - firstStart) / 1000).toFixed(2) : '—';
81
+
82
+ // consistent order: pre → mission → post
83
+ const order = { premission: 0, mission: 1, postmission: 2 };
84
+ subs.sort((a, b) => (order[(a.submissionType||'mission')] ?? 1) - (order[(b.submissionType||'mission')] ?? 1));
85
+
86
+ return `
87
+ <details class="mission-group">
88
+ <summary>
89
+ <span class="name">${esc(missionName)}</span>
90
+ <span class="status ${status === 'passed' ? 'ok' : 'bad'}">${badge(status)}</span>
91
+ <span class="meta">submissions: ${subs.length} • steps: ${totalSteps} • duration: ${groupDur}s</span>
92
+ </summary>
93
+ <div class="group-body">
94
+ ${subs.map(submissionBlock).join('')}
95
+ </div>
96
+ </details>
97
+ `;
98
+ };
99
+
54
100
  const html = `
55
101
  <!DOCTYPE html>
56
102
  <html>
@@ -77,14 +123,24 @@ export function generateHtmlReport(report, outputPath) {
77
123
 
78
124
  details { background: var(--card); border:1px solid var(--border); border-radius:10px; margin:10px 0; }
79
125
  summary { cursor:pointer; padding:12px 14px; display:flex; align-items:center; gap:12px; }
80
- .mission > summary { font-weight:600; }
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; }
81
137
  .name { flex:1; }
82
138
  .status.ok { color: var(--ok); }
83
139
  .status.bad { color: var(--bad); }
84
140
  .meta { color: var(--muted); font-size:12px; }
85
141
 
86
142
  .steps { padding: 0 14px 12px; }
87
- .step summary { background: #fafafa; border-bottom:1px solid var(--border); border-radius: 10px 10px 0 0; }
143
+ .step summary { background: #f8fafc; border-bottom:1px solid var(--border); border-radius: 10px 10px 0 0; }
88
144
  .turn { font-weight:600; }
89
145
  .step-result.ok { color: var(--ok); }
90
146
  .step-result.bad { color: var(--bad); }
@@ -103,12 +159,24 @@ export function generateHtmlReport(report, outputPath) {
103
159
  </div>
104
160
 
105
161
  <div class="summary">
106
- <div class="pill">Missions: ${esc(summary.totalMissions ?? missions.length)}</div>
107
- <div class="pill ok">Passed: ${esc(summary.passed ?? missions.filter(m=>m.status==='passed').length)}</div>
108
- <div class="pill bad">Failed: ${esc(summary.failed ?? missions.filter(m=>m.status==='failed').length)}</div>
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
+ })()}
109
177
  </div>
110
178
 
111
- ${missions.map(missionBlock).join('')}
179
+ ${Object.entries(grouped).map(([name, subs]) => missionGroupBlock(name, subs)).join('')}
112
180
 
113
181
  </body>
114
182
  </html>