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 +19 -0
- package/core/agent.js +2 -0
- package/package.json +1 -1
- package/runner/testronaut.js +24 -3
- package/tools/generateHtmlReport.js +79 -11
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. [](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
package/runner/testronaut.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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">${
|
|
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
|
|
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:
|
|
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: #
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
${
|
|
179
|
+
${Object.entries(grouped).map(([name, subs]) => missionGroupBlock(name, subs)).join('')}
|
|
112
180
|
|
|
113
181
|
</body>
|
|
114
182
|
</html>
|