testronaut 1.0.7 โ†’ 1.0.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/README.md CHANGED
@@ -47,7 +47,8 @@ import { runMissions } from 'testronaut';
47
47
 
48
48
  export const loginMission = `
49
49
  Visit ${process.env.URL}.
50
- Fill in the username field with ${process.env.USERNAME} and password field with ${process.env.PASSWORD}.
50
+ Fill the username field with ${process.env.USERNAME}.
51
+ Fill the password field with ${process.env.PASSWORD}.
51
52
  Submit the form.
52
53
  Wait for the dashboard to appear.
53
54
  Report SUCCESS if the dashboard is loaded, otherwise report FAILURE.
@@ -56,7 +57,7 @@ Report SUCCESS if the dashboard is loaded, otherwise report FAILURE.
56
57
  export async function executeMission() {
57
58
  await runMissions({
58
59
  mission: loginMission
59
- });
60
+ }, "Login Mission");
60
61
  }
61
62
  ```
62
63
 
@@ -89,7 +90,7 @@ export async function executeMission() {
89
90
  preMission: [loginMission, navigateToContactFormMission],
90
91
  mission: fillContactFormMission,
91
92
  postMission: logoutMission,
92
- });
93
+ }, "Fill Contact Form Mission");
93
94
  }
94
95
  ```
95
96
 
@@ -97,12 +98,12 @@ export async function executeMission() {
97
98
  Run all missions in the missions/ directory:
98
99
 
99
100
  ```
100
- testronaut
101
+ npx testronaut
101
102
  ```
102
103
  Run a specific file:
103
104
 
104
105
  ```
105
- testronaut login.mission.js
106
+ npx testronaut login.mission.js
106
107
  ```
107
108
 
108
109
  ๐Ÿงฐ Available Helpers
@@ -122,6 +123,9 @@ runSuite(objectives)
122
123
  ```
123
124
  โ€“ Executes all registered test objectives
124
125
 
126
+ ### ๐Ÿ“‹ Reports
127
+ HTML and JSON reports automatically created in the ```missions/mission_reports/``` directory for each testronaut run
128
+
125
129
  ### ๐Ÿงช Under the Hood
126
130
  Uses Playwright for browser automation
127
131
 
@@ -141,4 +145,4 @@ Now you can use testronaut in any local project.
141
145
  ### ๐Ÿ“„ License
142
146
  MIT
143
147
 
144
- ๐Ÿค– Built with โค๏ธ by Shane Fast
148
+ ๐Ÿค– Built with โค๏ธ by [Shane Fast](https://github.com/scfast)
package/bin/cli.js CHANGED
@@ -4,12 +4,17 @@ import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { initializeTestronautProject } from './init.js';
6
6
  import { createWelcomeMission } from './createWelcomeMission.js';
7
+ import { generateHtmlReport } from '../tools/generateHtmlReport.js';
7
8
 
8
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
10
 
10
11
  const args = process.argv.slice(2);
11
12
  const missionsDir = path.resolve(process.cwd(), 'missions');
12
13
 
14
+ const allResults = [];
15
+ const runId = `run_${Date.now()}`;
16
+ const startTime = new Date();
17
+
13
18
  const HELP_TEXT = `
14
19
  ๐Ÿง‘โ€๐Ÿš€ testronaut - Autonomous Agent Mission Runner
15
20
 
@@ -50,7 +55,11 @@ const runFile = async (filePath) => {
50
55
  const missionsModule = await import(`file://${modulePath}`);
51
56
 
52
57
  if (typeof missionsModule.executeMission === 'function') {
53
- await missionsModule.executeMission(); // or call a named export like `executeMissions()`
58
+ const result = await missionsModule.executeMission();
59
+ allResults.push({
60
+ file: filePath,
61
+ result
62
+ });
54
63
  }
55
64
  } catch (err) {
56
65
  console.error(`โŒ Error running mission: ${filePath}`);
@@ -70,3 +79,32 @@ if (args.length > 0) {
70
79
  await runFile(file);
71
80
  }
72
81
  }
82
+
83
+ const endTime = new Date();
84
+
85
+ const flatMissions = allResults.flatMap(entry => {
86
+ const result = entry.result;
87
+ const missions = Array.isArray(result) ? result : [result]; // normalize
88
+ return missions.map(m => ({
89
+ ...m,
90
+ file: entry.file
91
+ }));
92
+ });
93
+
94
+
95
+ const report = {
96
+ runId,
97
+ startTime: startTime.toISOString(),
98
+ endTime: endTime.toISOString(),
99
+ summary: {
100
+ totalMissions: flatMissions.length,
101
+ passed: flatMissions.filter(m => m.status === 'passed').length,
102
+ failed: flatMissions.filter(m => m.status === 'failed').length,
103
+ },
104
+ missions: flatMissions
105
+ };
106
+
107
+ const outputDir = './missions/mission_reports';
108
+ fs.mkdirSync(outputDir, { recursive: true });
109
+ fs.writeFileSync(`${outputDir}/${runId}.json`, JSON.stringify(report, null, 2));
110
+ generateHtmlReport(report, `${outputDir}/${runId}.html`);
@@ -23,9 +23,9 @@ If you cannot reach this message, report FAILURE.
23
23
  \`;
24
24
 
25
25
  export async function executeMission() {
26
- await runMissions({
26
+ return await runMissions({
27
27
  mission: welcomeGoal
28
- });
28
+ }, 'Welcome Mission!');
29
29
  }
30
30
 
31
31
  `;
package/core/agent.js CHANGED
@@ -2,41 +2,14 @@ import 'dotenv/config';
2
2
  import { ChromeBrowser } from '../tools/chromeBrowser.js';
3
3
  import { turnLoop } from './turnLoop.js';
4
4
 
5
-
6
- // export async function runAgent(goal, maxTurns = 10) {
7
- // console.log(`\n๐Ÿš€ Running Agent with goal: ${goal}`);
8
- // const browser = new ChromeBrowser();
9
- // await browser.start();
10
-
11
- // const messages = [
12
- // {
13
- // role: 'system',
14
- // content: `
15
- // You are an autonomous web agent. Use function calls to complete the user's goal.
16
- // If you are unsure of the selectors for inputs or buttons, call 'get_dom' to retrieve page HTML,
17
- // analyze it, then make your best guess based on labels, names, types, and placeholder values.
18
- // After completing the goal, respond with a final plain-text message starting with SUCCESS or FAILURE.
19
- // `.trim(),
20
- // },
21
- // { role: 'user', content: goal },
22
- // ];
23
-
24
- // try {
25
- // const result = await turnLoop(browser, messages, maxTurns);
26
- // !result && console.log('๐Ÿ›‘ Agent ran out of turns.')
27
- // } finally {
28
- // await browser.close();
29
- // }
30
- // }
31
-
32
- export async function runAgent(goals, maxTurns = 20) {
5
+ export async function runAgent(goals, missionName, maxTurns = 20, ) {
33
6
  const browser = new ChromeBrowser();
34
7
  await browser.start();
8
+ const startTime = Date.now();
35
9
 
36
10
  try {
37
11
  for (const goal of goals) {
38
- console.log(`\n๐Ÿš€ Running Agent with goal:\n${goal}\n`);
39
-
12
+ // console.log(`\n๐Ÿš€ Running Agent with goal:\n${goal}\n`);
40
13
  const messages = [
41
14
  {
42
15
  role: 'system',
@@ -59,24 +32,30 @@ export async function runAgent(goals, maxTurns = 20) {
59
32
  const success = await turnLoop(browser, messages, maxTurns);
60
33
  if (!success) {
61
34
  console.log('๐Ÿ›‘ Agent stopped due to failed goal.\n');
62
- return false;
35
+ const endTime = Date.now();
36
+ return {
37
+ missionName: missionName,
38
+ status: 'failed',
39
+ steps: [ /* tool steps or key moments */ ],
40
+ startTime: startTime,
41
+ endTime: endTime,
42
+ }
43
+ ;
63
44
  }
64
45
  }
65
46
 
66
47
  console.log('โœ… All goals completed successfully.\n');
67
- return true;
48
+ const endTime = Date.now();
49
+ return {
50
+ missionName: missionName,
51
+ status: 'passed',
52
+ steps: [ /* tool steps or key moments */ ],
53
+ startTime: startTime,
54
+ endTime: endTime,
55
+ }
56
+ ;
68
57
 
69
58
  } finally {
70
59
  await browser.close();
71
60
  }
72
61
  }
73
-
74
-
75
- // (async () => {
76
- // await runAgent(`
77
- // Visit ${process.env.URL}.
78
- // Log in using ${process.env.USERNAME} and password ${process.env.PASSWORD}.
79
- // After clicking the login button, use 'check_text' to verify that "${process.env.AFTER_LOGIN_CHECK}" appears on the page.
80
- // If found, report SUCCESS. Otherwise, report FAILURE.
81
- // `);
82
- // })();
package/core/turnLoop.js CHANGED
@@ -18,6 +18,7 @@ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
18
18
 
19
19
  let totalTokensUsed = 0;
20
20
  let turnTimestamps = [];
21
+ let shouldBackoff;
21
22
 
22
23
  const pushDOMAssistant = async (browser, messages, agentMemory, { skipIfLastTool } = {}) => {
23
24
  if (skipIfLastTool && skipIfLastTool.includes(messages.at(-1)?.name)) {
@@ -43,7 +44,7 @@ const pushDOMAssistant = async (browser, messages, agentMemory, { skipIfLastTool
43
44
 
44
45
  const domHtml = await CHROME_TOOL_MAP.get_dom(browser, { limit: 100000, exclude: true }, agentMemory);
45
46
  await tokenEstimate('gpt-4o', domHtml);
46
- fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
47
+ // fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
47
48
 
48
49
  messages.push({
49
50
  role: 'tool',
@@ -61,48 +62,40 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
61
62
 
62
63
  for (let turn = currentTurn; turn < maxTurns; turn++) {
63
64
  console.log(`\n๐Ÿ”„ Turn ${turn + 1}/${maxTurns}`);
64
- // console.log("current tools", toolsSchema);
65
-
66
- // const now = Date.now();
67
- // turnTimestamps = turnTimestamps.filter(ts => now - ts < 60000);
68
-
69
- // Recalculate the rolling window before checking cooldown
70
- const pruned = pruneOldTokenUsage(turnTimestamps);
71
- turnTimestamps = pruned.turnTimestamps;
72
- totalTokensUsed = pruned.totalTokensUsed;
73
-
74
- // Cool off check
75
- const cooldownResult = await tokenUseCoolOff(totalTokensUsed, turnTimestamps);
76
- totalTokensUsed = cooldownResult.totalTokensUsed;
77
- turnTimestamps = cooldownResult.turnTimestamps;
78
- if (cooldownResult.shouldBackoff) {
79
- return await turnLoop(browser, messages, maxTurns, turn);
80
- }
81
- // if (await tokenUseCoolOff(totalTokensUsed, turnTimestamps)) {return await turnLoop(browser, messages, maxTurns, turn)}
82
-
83
- const isValid = validateAndInsertMissingToolResponses(messages, {
84
- insertPlaceholders: true, // safe fallback behavior
85
- });
86
-
87
- if (!isValid) {
88
- console.error('โŒ Tool call structure invalid and no placeholders inserted.');
89
- return false;
90
- }
91
- // console.log(JSON.stringify(messages, null, 2));
92
-
93
65
  let response;
94
66
  try {
95
- // console.log(
96
- // '๐Ÿ” Verifying message structure before OpenAI call:\n',
97
- // messages.map(m => ({
98
- // role: m.role,
99
- // name: m.name,
100
- // tool_call_id: m.tool_call_id,
101
- // tool_calls: m.tool_calls?.map(t => t.id),
102
- // content: m.content?.slice?.(0, 100), // truncate for readability
103
- // }))
104
- // );
67
+
68
+ // Recalculate the rolling window before checking cooldown
69
+ ({ turnTimestamps, totalTokensUsed } = pruneOldTokenUsage(turnTimestamps));
70
+
71
+ // Cool off check
72
+ ({ totalTokensUsed, turnTimestamps, shouldBackoff } = await tokenUseCoolOff(totalTokensUsed, turnTimestamps));
73
+ if(shouldBackoff) return await turnLoop(browser, messages, maxTurns, turn);
74
+
75
+ //Tool Validator
76
+ if (!validateAndInsertMissingToolResponses(messages, { insertPlaceholders: true })) {
77
+ console.error('โŒ Tool call structure invalid and no placeholders inserted.');
78
+ return false;
79
+ }
80
+
81
+ const unrespondedCalls = messages.filter(
82
+ (msg, i) =>
83
+ msg.role === 'assistant' &&
84
+ msg.tool_calls?.length &&
85
+ !msg.tool_calls.every(call =>
86
+ messages.slice(i + 1).some(
87
+ m => m.role === 'tool' && m.tool_call_id === call.id
88
+ )
89
+ )
90
+ );
105
91
 
92
+ if (unrespondedCalls.length) {
93
+ console.error('๐Ÿ›‘ Detected assistant tool calls without matching tool responses:');
94
+ console.dir(unrespondedCalls, { depth: 5 });
95
+ return false;
96
+ }
97
+
98
+
106
99
  response = await openai.chat.completions.create({
107
100
  model: 'gpt-4o',
108
101
  messages,
@@ -122,7 +115,8 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
122
115
  return await turnLoop(browser, messages, maxTurns, turn, retryCount + 1);
123
116
  } else if (err.status === 400) {
124
117
  console.error('โŒ Bad request:', err.message);
125
- console.log("current tools", toolsSchema);
118
+ // console.log("current tools", toolsSchema);
119
+ // console.log("messages: ", messages)
126
120
  return false;
127
121
  } else {
128
122
  throw err;
@@ -130,19 +124,12 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
130
124
  }
131
125
  console.log('Response received from OpenAI');
132
126
  const usage = response.usage;
133
- // if (usage) {
134
- // console.log(`๐Ÿ“Š Token Usage This Turn โ†’ Prompt: ${usage.prompt_tokens}, Completion: ${usage.completion_tokens}, Total: ${usage.total_tokens}`);
135
- // totalTokensUsed += usage?.total_tokens || 0;
136
- // turnTimestamps.push(now);
137
- // console.log(`๐Ÿ“ˆ Running Total Tokens Used: ${totalTokensUsed}`);
138
- // }
139
127
  if (usage) {
140
128
  const tokensUsed = usage.total_tokens || 0;
141
129
  console.log(`๐Ÿ“Š Token Usage This Turn โ†’ Total: ${tokensUsed}`);
142
130
  recordTokenUsage(turnTimestamps, tokensUsed);
143
- const pruned = pruneOldTokenUsage(turnTimestamps);
144
- turnTimestamps = pruned.turnTimestamps;
145
- totalTokensUsed = pruned.totalTokensUsed;
131
+ ({ turnTimestamps, totalTokensUsed } = pruneOldTokenUsage(turnTimestamps));
132
+
146
133
  console.log(`๐Ÿ“ˆ Running Total Tokens Used (Rolling 60s): ${totalTokensUsed}`);
147
134
  }
148
135
 
@@ -150,29 +137,21 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
150
137
 
151
138
  if (msg.tool_calls?.length) {
152
139
  console.log('Processing tool calls...');
153
- messages.push(msg);
140
+ const toolResponses = [];
154
141
  let lastToolName = null;
142
+
155
143
  for (const call of msg.tool_calls) {
156
144
  const fnName = call.function.name;
157
145
  lastToolName = fnName;
158
146
  const args = JSON.parse(call.function.arguments || '{}');
159
147
  console.log(`[model] โ†’ ${fnName}`, args);
160
- console.log('Calling tool:', fnName);
148
+ // console.log('Calling tool:', fnName);
161
149
  let result;
162
150
  let errorMessage = null;
163
151
  try {
164
- console.log("agentMemory: ", agentMemory);
152
+ // console.log("agentMemory: ", agentMemory);
165
153
  result = await CHROME_TOOL_MAP[fnName](browser, args, agentMemory);
166
154
 
167
- // Smart DOM-ready wait after major actions like click
168
- // if (['click', 'click_text', 'fill'].includes(fnName)) {
169
- // try {
170
- // await browser.page.waitForLoadState?.('domcontentloaded', { timeout: 5000 });
171
- // await browser.page.waitForTimeout?.(2000); // allow animations/navigation
172
- // } catch (err) {
173
- // console.warn(`โš ๏ธ DOM readiness wait failed after ${fnName}: ${err.message}`);
174
- // }
175
- // }
176
155
  if (typeof result !== 'string') {
177
156
  result = JSON.stringify(result ?? '');
178
157
  }
@@ -181,26 +160,18 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
181
160
  result = errorMessage;
182
161
  }
183
162
 
184
- // ๐Ÿ” Diagnostic: log whether this tool call succeeded or failed
185
163
  console.log(`[tool ] โ† ${fnName} result:`, errorMessage ? 'โŒ Failed' : 'โœ… Success');
186
-
187
164
  const truncated = result.length > 100 ? result.slice(0, 100) + 'โ€ฆ' : result;
165
+ console.log(`[tool ] โ† ${truncated}`);
188
166
 
189
- if (result.length > 100) {
190
- const logFile = `missions/mission_reports/tool-result-${Date.now()}.log`;
191
- fs.writeFileSync(logFile, result);
192
- console.log(`[tool ] โ† ${truncated} (full output written to ${logFile})`);
193
- } else {
194
- console.log(`[tool ] โ† ${truncated}`);
195
- }
196
-
197
- messages.push({
167
+ toolResponses.push({
198
168
  role: 'tool',
199
169
  tool_call_id: call.id,
200
170
  name: fnName,
201
171
  type: 'function',
202
172
  content: result,
203
173
  });
174
+
204
175
  if (fnName === 'expand_menu') {
205
176
  agentMemory.lastMenuExpanded = true;
206
177
  }
@@ -210,17 +181,18 @@ export const turnLoop = async (browser, messages, maxTurns, currentTurn = 0, ret
210
181
  const domHtml = await CHROME_TOOL_MAP.get_dom(browser, {
211
182
  limit: 100000,
212
183
  exclude: true,
213
- focus: [], // or try focusing on ['main'] to keep it light
184
+ focus: [],
214
185
  }, agentMemory);
215
186
  await tokenEstimate('gpt-4o', domHtml);
216
- fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
187
+ // fs.writeFileSync(`missions/mission_reports/debug-expanded-${Date.now()}.html`, domHtml);
217
188
  await pushDOMAssistant(browser, messages, agentMemory, {
218
189
  skipIfLastTool: ['get_dom', 'check_text']
219
190
  });
220
191
  console.log(`[auto] โ†’ DOM size after ${fnName}: ${domHtml.length} chars`);
221
192
  }
193
+
222
194
  }
223
-
195
+ messages.push(msg, ...toolResponses);
224
196
  continue;
225
197
  }
226
198
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testronaut",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "main": "./runner/testronaut.js",
5
5
  "bin": {
6
6
  "testronaut": "./bin/cli.js"
@@ -25,5 +25,12 @@
25
25
  "openai": "^5.10.2",
26
26
  "playwright": "^1.54.1",
27
27
  "prompts": "^2.4.2"
28
- }
28
+ },
29
+ "keywords": [
30
+ "cli",
31
+ "testing",
32
+ "automation",
33
+ "node",
34
+ "qa"
35
+ ]
29
36
  }
@@ -6,4 +6,4 @@ mission('Collegium Dashboard Login Test', () => {
6
6
  objective('should land on dashboard after login', dashboardWorkflow);
7
7
  });
8
8
 
9
- await launch(loginWorkflow); // Setup: login
9
+ await launch(loginWorkflow);
@@ -16,39 +16,7 @@ export async function launch(preMissionSetup) {
16
16
  await runSuite(objectives, preMissionSetup);
17
17
  }
18
18
 
19
-
20
- // export async function runMissions({ preMission, mission, postMission }) {
21
- // const goals = [];
22
-
23
- // if (preMission) {
24
- // console.log('\n๐Ÿงญ Running pre-mission setup:', preMission);
25
- // goals.push({ goal: preMission, label: 'pre-mission' });
26
- // }
27
-
28
- // if (mission) {
29
- // console.log('\n๐Ÿงญ Running main mission:', mission);
30
- // goals.push({ goal: mission, label: 'mission' });
31
- // }
32
-
33
- // if (postMission) {
34
- // console.log('\n๐Ÿงญ Running post-mission cleanup:', postMission);
35
- // goals.push({ goal: postMission, label: 'post-mission' });
36
- // }
37
-
38
- // console.log('\n๐Ÿงญ Running mission flow:\n', goals);
39
-
40
- // // for (const { goal, label } of goals) {
41
- // // console.log(`\n๐Ÿงญ Running ${label}:\n`);
42
- // const success = await runAgent(goals);
43
- // if (!success) {
44
- // console.log(`โŒ Aborting after failed goal.`);
45
- // return;
46
- // }
47
- // // }
48
- // console.log('\nโœ… Mission flow complete.');
49
- // }
50
-
51
- export async function runMissions({ preMission, mission, postMission }) {
19
+ export async function runMissions({ preMission, mission, postMission }, missionName) {
52
20
  const normalizeToArray = (x) => (Array.isArray(x) ? x : x ? [x] : []);
53
21
 
54
22
  const pre = normalizeToArray(preMission);
@@ -58,17 +26,14 @@ export async function runMissions({ preMission, mission, postMission }) {
58
26
  const goals = [];
59
27
 
60
28
  if (pre.length) {
61
- console.log('\n๐Ÿงญ Running pre-mission setup:', pre);
62
29
  goals.push(...pre.map(fn => ({ goal: fn, label: 'pre-mission' })));
63
30
  }
64
31
 
65
32
  if (main.length) {
66
- console.log('\n๐Ÿงญ Running main mission(s):', main);
67
33
  goals.push(...main.map(fn => ({ goal: fn, label: 'mission' })));
68
34
  }
69
35
 
70
36
  if (post.length) {
71
- console.log('\n๐Ÿงญ Running post-mission cleanup:', post);
72
37
  goals.push(...post.map(fn => ({ goal: fn, label: 'post-mission' })));
73
38
  }
74
39
 
@@ -77,11 +42,11 @@ export async function runMissions({ preMission, mission, postMission }) {
77
42
  goals.map(g => `${g.label}: ${g.goal?.name ?? g.goal}`)
78
43
  );
79
44
 
80
- const success = await runAgent(goals);
45
+ const success = await runAgent(goals, missionName);
81
46
  if (!success) {
82
47
  console.log(`โŒ Aborting after failed goal.`);
83
48
  return;
84
49
  }
85
-
86
50
  console.log('\nโœ… Mission flow complete.');
51
+ return success;
87
52
  }
@@ -92,7 +92,7 @@ export class ChromeBrowser {
92
92
  await el.click();
93
93
  await this.page.screenshot({ path: `missions/mission_reports/screenshot_after_click_${Date.now()}.png` });
94
94
  await this.page.waitForTimeout(5000);
95
- // await this.page.screenshot({ path: `screenshot_after_waiting_click_${Date.now()}.png` });
95
+ await this.page.screenshot({ path: `screenshot_after_waiting_click_${Date.now()}.png` });
96
96
  return `clicked text "${text}"`;
97
97
 
98
98
  }
@@ -147,11 +147,6 @@ export class ChromeBrowser {
147
147
  await this.page.waitForSelector(selector, { timeout: 30_000 });
148
148
  await this.page.click(selector);
149
149
  await this.page.waitForTimeout(delayMs);
150
-
151
- const expandedDom = await this.page.content();
152
- const filename = `missions/mission_reports/expand_menu_dump_${Date.now()}.html`;
153
- fs.writeFileSync(filename, expandedDom);
154
- console.log(`[expand_menu] โ†’ Dumped DOM after expand click to ${filename}`);
155
150
  await this.page.screenshot({ path: `missions/mission_reports/screenshot_expand_menu_${Date.now()}.png` });
156
151
 
157
152
  return `expanded menu using ${selector}`;
@@ -0,0 +1,52 @@
1
+ import fs from 'fs';
2
+
3
+ export function generateHtmlReport(report, outputPath) {
4
+ const { runId, startTime, endTime, summary, missions } = report;
5
+
6
+ const missionHtml = missions.map(m => {
7
+ const status = m.status?.toUpperCase?.() || 'UNKNOWN';
8
+ const missionName = m.missionName || 'Unnamed';
9
+ const file = m.file || 'N/A';
10
+ const steps = Array.isArray(m.steps) ? m.steps : [];
11
+
12
+ return `
13
+ <div class="mission ${status.toLowerCase()}">
14
+ <h3>${file} - ${missionName} - <span>${status}</span></h3>
15
+ <p><em>${m.startTime || 'N/A'} โ†’ ${m.endTime || 'N/A'}</em></p>
16
+ <ul>
17
+ ${steps.map(step => `
18
+ <li>
19
+ <strong>${step.tool}</strong> โ†’ ${step.output}
20
+ </li>
21
+ `).join('')}
22
+ </ul>
23
+ </div>
24
+ `;
25
+ }).join('');
26
+
27
+ const html = `
28
+ <html>
29
+ <head>
30
+ <style>
31
+ body { font-family: sans-serif; padding: 20px; }
32
+ .passed { background-color: #e6ffed; }
33
+ .failed { background-color: #ffe6e6; }
34
+ .unknown { background-color: #f9f9f9; }
35
+ h3 span { font-weight: bold; }
36
+ ul { margin-top: 0; }
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <h1>๐Ÿง‘โ€๐Ÿš€ Testronaut Report</h1>
41
+ <p><strong>Run ID:</strong> ${runId}</p>
42
+ <p><strong>Start:</strong> ${startTime}</p>
43
+ <p><strong>End:</strong> ${endTime}</p>
44
+ <p><strong>Summary:</strong> ${summary.passed}/${summary.totalMissions} passed</p>
45
+ ${missionHtml}
46
+ </body>
47
+ </html>
48
+ `;
49
+
50
+ fs.writeFileSync(outputPath, html);
51
+ console.log(`๐Ÿ“ Report generated at: ${outputPath}`);
52
+ }