donobu 2.23.2 → 2.23.4

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.
@@ -1 +1 @@
1
- 1256
1
+ 1259
@@ -29,12 +29,6 @@ function formatDuration(ms) {
29
29
  return `${minutes}m ${remainingSeconds}s`;
30
30
  }
31
31
 
32
- // Function to format date in a readable way
33
- function formatDate(dateString) {
34
- const date = new Date(dateString);
35
- return date.toLocaleString();
36
- }
37
-
38
32
  // Read the JSON data from file or stdin
39
33
  function readInput() {
40
34
  const args = process.argv.slice(2);
@@ -47,7 +41,7 @@ function readInput() {
47
41
 
48
42
  // Process JSON and create markdown
49
43
  function generateMarkdown(jsonData) {
50
- const { suites, stats } = jsonData;
44
+ const { suites } = jsonData;
51
45
 
52
46
  // Count self-healed tests
53
47
  let selfHealedCount = 0;
@@ -66,117 +60,106 @@ function generateMarkdown(jsonData) {
66
60
 
67
61
  // Create report header
68
62
  let markdown = `# Playwright Test Report\n\n`;
69
-
70
- // Summary section
63
+ // Tests by file
71
64
  markdown += `## Summary\n\n`;
72
- markdown += `- **Start Time**: ${formatDate(stats.startTime)}\n`;
73
- markdown += `- **Duration**: ${formatDuration(stats.duration)}\n`;
74
- markdown += `- **Tests**: ${stats.expected + stats.unexpected}\n`;
75
- markdown += `- **Passed**: ${stats.expected}\n`;
76
- markdown += `- **Self-Healed**: ${selfHealedCount}\n`;
77
- markdown += `- **Failed**: ${stats.unexpected - selfHealedCount}\n`;
78
- markdown += `- **Skipped**: ${stats.skipped}\n`;
79
- markdown += `- **Flaky**: ${stats.flaky}\n\n`;
80
-
81
- // Create pass rate and visual indicator
82
- const totalTests = stats.expected + stats.unexpected;
83
- const passRate =
84
- totalTests > 0 ? Math.round((stats.expected / totalTests) * 100) : 0;
85
- const selfHealedRate =
86
- totalTests > 0 ? Math.round((selfHealedCount / totalTests) * 100) : 0;
87
- const failRate =
88
- totalTests > 0
89
- ? Math.round(((stats.unexpected - selfHealedCount) / totalTests) * 100)
90
- : 0;
91
-
92
- markdown += `### Test Results\n\n`;
93
- markdown += `- **Passed**: ${passRate}%\n`;
94
- markdown += `- **Self-Healed**: ${selfHealedRate}%\n`;
95
- markdown += `- **Failed**: ${failRate}%\n\n`;
96
-
97
- // Create visual pass/self-healed/fail bar
98
- const passChar = '🟢';
99
- const selfHealedChar = '❤️‍🩹';
100
- const failChar = '🔴';
101
-
102
- const passCount = stats.expected;
103
- const failCount = stats.unexpected - selfHealedCount;
104
-
105
- const statusBar =
106
- passChar.repeat(passCount) +
107
- selfHealedChar.repeat(selfHealedCount) +
108
- failChar.repeat(failCount);
109
-
110
- markdown += `${statusBar}\n\n`;
111
65
 
112
- // Tests by file
113
- markdown += `## Test Files\n\n`;
66
+ // Create file summary table with status counts
67
+ markdown += `| File | Passed | Self-Healed | Failed | Timed Out | Skipped | Interrupted | Duration |\n`;
68
+ markdown += `| - | - | - | - | - | - | - | - |\n`;
114
69
 
115
- // Create file summary table
116
- markdown += `| File | Status | Tests | Duration |\n`;
117
- markdown += `| ---- | ------ | ----- | -------- |\n`;
70
+ // Track totals for summary row
71
+ let totalPassed = 0;
72
+ let totalFailed = 0;
73
+ let totalTimedOut = 0;
74
+ let totalSkipped = 0;
75
+ let totalInterrupted = 0;
76
+ let totalSelfHealed = 0;
77
+ let totalDuration = 0;
118
78
 
119
79
  suites.forEach((suite) => {
120
- const fileTests = suite.specs.reduce(
121
- (count, spec) => count + spec.tests.length,
122
- 0,
123
- );
80
+ // Count tests by status for this file
81
+ let passed = 0;
82
+ let failed = 0;
83
+ let timedOut = 0;
84
+ let skipped = 0;
85
+ let interrupted = 0;
86
+ let selfHealed = 0;
87
+
124
88
  const fileDuration = suite.specs.reduce(
125
89
  (total, spec) =>
126
90
  total +
127
91
  spec.tests.reduce((testTotal, test) => {
128
- const result = test.results && test.results[0];
92
+ const result = test.results && test.results.at(-1);
93
+ const isSelfHealed =
94
+ test.annotations &&
95
+ test.annotations.some((a) => a.type === 'self-healed');
96
+
97
+ if (result) {
98
+ if (isSelfHealed) {
99
+ selfHealed++;
100
+ } else {
101
+ switch (result.status) {
102
+ case 'passed':
103
+ passed++;
104
+ break;
105
+ case 'failed':
106
+ failed++;
107
+ break;
108
+ case 'timedOut':
109
+ timedOut++;
110
+ break;
111
+ case 'skipped':
112
+ skipped++;
113
+ break;
114
+ case 'interrupted':
115
+ interrupted++;
116
+ break;
117
+ }
118
+ }
119
+ }
120
+
129
121
  return testTotal + (result?.duration || 0);
130
122
  }, 0),
131
123
  0,
132
124
  );
133
125
 
134
- // Check if all tests passed (including self-healed)
135
- const allPassed = suite.specs.every((spec) => spec.ok);
136
- // Check if any test self-healed
137
- let hasSelfHealed = false;
138
- suite.specs.forEach((spec) => {
139
- spec.tests.forEach((test) => {
140
- if (
141
- test.annotations &&
142
- test.annotations.some((a) => a.type === 'self-healed')
143
- ) {
144
- hasSelfHealed = true;
145
- }
146
- });
147
- });
148
-
149
- // Determine status icon
150
- let status;
151
- if (allPassed) {
152
- status = '✅';
153
- } else if (hasSelfHealed) {
154
- status = '❤️‍🩹';
155
- } else {
156
- status = '❌';
157
- }
158
-
159
- markdown += `| ${suite.file} | ${status} | ${fileTests} | ${formatDuration(fileDuration)} |\n`;
126
+ // Add to totals
127
+ totalPassed += passed;
128
+ totalFailed += failed;
129
+ totalTimedOut += timedOut;
130
+ totalSkipped += skipped;
131
+ totalInterrupted += interrupted;
132
+ totalSelfHealed += selfHealed;
133
+ totalDuration += fileDuration;
134
+ const anchor = suite.file
135
+ .replace(/[^\w\s-]/g, '') // remove non-alphanumeric
136
+ .trim()
137
+ .replace(/\s+/g, '-') // spaces to dashes
138
+ .toLowerCase();
139
+
140
+ markdown += `| [${suite.file}](#${anchor}) | ${passed ? passed + ' ✅' : ''} | ${selfHealed ? selfHealed + ' ❤️‍🩹' : ''} | ${failed ? failed + ' ❌' : ''} | ${timedOut ? timedOut + ' ⏰' : ''} | ${skipped ? skipped + ' ⏭️' : ''} | ${interrupted ? interrupted + ' ⚡' : ''} | ${formatDuration(fileDuration)} |\n`;
160
141
  });
161
142
 
143
+ // Add totals row
144
+ markdown += `| **TOTAL** | **${totalPassed + ' ✅'}** | **${totalSelfHealed + ' ❤️‍🩹'}** | **${totalFailed + ' ❌'}** | **${totalTimedOut + ' ⏰'}** | **${totalSkipped + ' ⏭️'}** | **${totalInterrupted + ' ⚡'}** | **${formatDuration(totalDuration)}** |\n`;
145
+
162
146
  markdown += `\n`;
163
147
 
164
148
  // Generate test details sections
165
149
  suites.forEach((suite) => {
166
- const fileName = suite.file.split('/').pop();
150
+ const fileName = suite.file;
167
151
  markdown += `## ${fileName}\n\n`;
168
152
 
169
153
  suite.specs.forEach((spec) => {
170
154
  markdown += `### ${spec.title}\n\n`;
171
155
 
172
156
  spec.tests.forEach((test) => {
173
- // Add null checking for results
174
- const result = test.results && test.results[0];
157
+ const result = test.results && test.results.at(-1);
175
158
 
176
159
  // Skip tests without results
177
160
  if (!result) {
178
- markdown += `**Status**: ⚠️ No Results \n`;
179
- markdown += `**Duration**: N/A \n`;
161
+ markdown += `**Status**: ⚠️ No Results\n`;
162
+ markdown += `**Duration**: N/A\n`;
180
163
  markdown += `**Objective**: Test skipped or no results available\n`;
181
164
  markdown += `---\n\n`;
182
165
  return;
@@ -194,8 +177,12 @@ function generateMarkdown(jsonData) {
194
177
  status = '❌ Failed (❤️‍🩹 Self-Healed)';
195
178
  } else if (result.status === 'failed') {
196
179
  status = '❌ Failed';
180
+ } else if (result.status === 'timedOut') {
181
+ status = '⏰ Timed Out';
197
182
  } else if (result.status === 'skipped') {
198
183
  status = '⏭️ Skipped';
184
+ } else if (result.status === 'interrupted') {
185
+ status = '⚡ Interrupted';
199
186
  } else {
200
187
  status = `⚠️ ${result.status || 'Unknown'}`;
201
188
  }
@@ -1 +1 @@
1
- 1256
1
+ 1259
@@ -29,12 +29,6 @@ function formatDuration(ms) {
29
29
  return `${minutes}m ${remainingSeconds}s`;
30
30
  }
31
31
 
32
- // Function to format date in a readable way
33
- function formatDate(dateString) {
34
- const date = new Date(dateString);
35
- return date.toLocaleString();
36
- }
37
-
38
32
  // Read the JSON data from file or stdin
39
33
  function readInput() {
40
34
  const args = process.argv.slice(2);
@@ -47,7 +41,7 @@ function readInput() {
47
41
 
48
42
  // Process JSON and create markdown
49
43
  function generateMarkdown(jsonData) {
50
- const { suites, stats } = jsonData;
44
+ const { suites } = jsonData;
51
45
 
52
46
  // Count self-healed tests
53
47
  let selfHealedCount = 0;
@@ -66,117 +60,106 @@ function generateMarkdown(jsonData) {
66
60
 
67
61
  // Create report header
68
62
  let markdown = `# Playwright Test Report\n\n`;
69
-
70
- // Summary section
63
+ // Tests by file
71
64
  markdown += `## Summary\n\n`;
72
- markdown += `- **Start Time**: ${formatDate(stats.startTime)}\n`;
73
- markdown += `- **Duration**: ${formatDuration(stats.duration)}\n`;
74
- markdown += `- **Tests**: ${stats.expected + stats.unexpected}\n`;
75
- markdown += `- **Passed**: ${stats.expected}\n`;
76
- markdown += `- **Self-Healed**: ${selfHealedCount}\n`;
77
- markdown += `- **Failed**: ${stats.unexpected - selfHealedCount}\n`;
78
- markdown += `- **Skipped**: ${stats.skipped}\n`;
79
- markdown += `- **Flaky**: ${stats.flaky}\n\n`;
80
-
81
- // Create pass rate and visual indicator
82
- const totalTests = stats.expected + stats.unexpected;
83
- const passRate =
84
- totalTests > 0 ? Math.round((stats.expected / totalTests) * 100) : 0;
85
- const selfHealedRate =
86
- totalTests > 0 ? Math.round((selfHealedCount / totalTests) * 100) : 0;
87
- const failRate =
88
- totalTests > 0
89
- ? Math.round(((stats.unexpected - selfHealedCount) / totalTests) * 100)
90
- : 0;
91
-
92
- markdown += `### Test Results\n\n`;
93
- markdown += `- **Passed**: ${passRate}%\n`;
94
- markdown += `- **Self-Healed**: ${selfHealedRate}%\n`;
95
- markdown += `- **Failed**: ${failRate}%\n\n`;
96
-
97
- // Create visual pass/self-healed/fail bar
98
- const passChar = '🟢';
99
- const selfHealedChar = '❤️‍🩹';
100
- const failChar = '🔴';
101
-
102
- const passCount = stats.expected;
103
- const failCount = stats.unexpected - selfHealedCount;
104
-
105
- const statusBar =
106
- passChar.repeat(passCount) +
107
- selfHealedChar.repeat(selfHealedCount) +
108
- failChar.repeat(failCount);
109
-
110
- markdown += `${statusBar}\n\n`;
111
65
 
112
- // Tests by file
113
- markdown += `## Test Files\n\n`;
66
+ // Create file summary table with status counts
67
+ markdown += `| File | Passed | Self-Healed | Failed | Timed Out | Skipped | Interrupted | Duration |\n`;
68
+ markdown += `| - | - | - | - | - | - | - | - |\n`;
114
69
 
115
- // Create file summary table
116
- markdown += `| File | Status | Tests | Duration |\n`;
117
- markdown += `| ---- | ------ | ----- | -------- |\n`;
70
+ // Track totals for summary row
71
+ let totalPassed = 0;
72
+ let totalFailed = 0;
73
+ let totalTimedOut = 0;
74
+ let totalSkipped = 0;
75
+ let totalInterrupted = 0;
76
+ let totalSelfHealed = 0;
77
+ let totalDuration = 0;
118
78
 
119
79
  suites.forEach((suite) => {
120
- const fileTests = suite.specs.reduce(
121
- (count, spec) => count + spec.tests.length,
122
- 0,
123
- );
80
+ // Count tests by status for this file
81
+ let passed = 0;
82
+ let failed = 0;
83
+ let timedOut = 0;
84
+ let skipped = 0;
85
+ let interrupted = 0;
86
+ let selfHealed = 0;
87
+
124
88
  const fileDuration = suite.specs.reduce(
125
89
  (total, spec) =>
126
90
  total +
127
91
  spec.tests.reduce((testTotal, test) => {
128
- const result = test.results && test.results[0];
92
+ const result = test.results && test.results.at(-1);
93
+ const isSelfHealed =
94
+ test.annotations &&
95
+ test.annotations.some((a) => a.type === 'self-healed');
96
+
97
+ if (result) {
98
+ if (isSelfHealed) {
99
+ selfHealed++;
100
+ } else {
101
+ switch (result.status) {
102
+ case 'passed':
103
+ passed++;
104
+ break;
105
+ case 'failed':
106
+ failed++;
107
+ break;
108
+ case 'timedOut':
109
+ timedOut++;
110
+ break;
111
+ case 'skipped':
112
+ skipped++;
113
+ break;
114
+ case 'interrupted':
115
+ interrupted++;
116
+ break;
117
+ }
118
+ }
119
+ }
120
+
129
121
  return testTotal + (result?.duration || 0);
130
122
  }, 0),
131
123
  0,
132
124
  );
133
125
 
134
- // Check if all tests passed (including self-healed)
135
- const allPassed = suite.specs.every((spec) => spec.ok);
136
- // Check if any test self-healed
137
- let hasSelfHealed = false;
138
- suite.specs.forEach((spec) => {
139
- spec.tests.forEach((test) => {
140
- if (
141
- test.annotations &&
142
- test.annotations.some((a) => a.type === 'self-healed')
143
- ) {
144
- hasSelfHealed = true;
145
- }
146
- });
147
- });
148
-
149
- // Determine status icon
150
- let status;
151
- if (allPassed) {
152
- status = '✅';
153
- } else if (hasSelfHealed) {
154
- status = '❤️‍🩹';
155
- } else {
156
- status = '❌';
157
- }
158
-
159
- markdown += `| ${suite.file} | ${status} | ${fileTests} | ${formatDuration(fileDuration)} |\n`;
126
+ // Add to totals
127
+ totalPassed += passed;
128
+ totalFailed += failed;
129
+ totalTimedOut += timedOut;
130
+ totalSkipped += skipped;
131
+ totalInterrupted += interrupted;
132
+ totalSelfHealed += selfHealed;
133
+ totalDuration += fileDuration;
134
+ const anchor = suite.file
135
+ .replace(/[^\w\s-]/g, '') // remove non-alphanumeric
136
+ .trim()
137
+ .replace(/\s+/g, '-') // spaces to dashes
138
+ .toLowerCase();
139
+
140
+ markdown += `| [${suite.file}](#${anchor}) | ${passed ? passed + ' ✅' : ''} | ${selfHealed ? selfHealed + ' ❤️‍🩹' : ''} | ${failed ? failed + ' ❌' : ''} | ${timedOut ? timedOut + ' ⏰' : ''} | ${skipped ? skipped + ' ⏭️' : ''} | ${interrupted ? interrupted + ' ⚡' : ''} | ${formatDuration(fileDuration)} |\n`;
160
141
  });
161
142
 
143
+ // Add totals row
144
+ markdown += `| **TOTAL** | **${totalPassed + ' ✅'}** | **${totalSelfHealed + ' ❤️‍🩹'}** | **${totalFailed + ' ❌'}** | **${totalTimedOut + ' ⏰'}** | **${totalSkipped + ' ⏭️'}** | **${totalInterrupted + ' ⚡'}** | **${formatDuration(totalDuration)}** |\n`;
145
+
162
146
  markdown += `\n`;
163
147
 
164
148
  // Generate test details sections
165
149
  suites.forEach((suite) => {
166
- const fileName = suite.file.split('/').pop();
150
+ const fileName = suite.file;
167
151
  markdown += `## ${fileName}\n\n`;
168
152
 
169
153
  suite.specs.forEach((spec) => {
170
154
  markdown += `### ${spec.title}\n\n`;
171
155
 
172
156
  spec.tests.forEach((test) => {
173
- // Add null checking for results
174
- const result = test.results && test.results[0];
157
+ const result = test.results && test.results.at(-1);
175
158
 
176
159
  // Skip tests without results
177
160
  if (!result) {
178
- markdown += `**Status**: ⚠️ No Results \n`;
179
- markdown += `**Duration**: N/A \n`;
161
+ markdown += `**Status**: ⚠️ No Results\n`;
162
+ markdown += `**Duration**: N/A\n`;
180
163
  markdown += `**Objective**: Test skipped or no results available\n`;
181
164
  markdown += `---\n\n`;
182
165
  return;
@@ -194,8 +177,12 @@ function generateMarkdown(jsonData) {
194
177
  status = '❌ Failed (❤️‍🩹 Self-Healed)';
195
178
  } else if (result.status === 'failed') {
196
179
  status = '❌ Failed';
180
+ } else if (result.status === 'timedOut') {
181
+ status = '⏰ Timed Out';
197
182
  } else if (result.status === 'skipped') {
198
183
  status = '⏭️ Skipped';
184
+ } else if (result.status === 'interrupted') {
185
+ status = '⚡ Interrupted';
199
186
  } else {
200
187
  status = `⚠️ ${result.status || 'Unknown'}`;
201
188
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "donobu",
3
- "version": "2.23.2",
3
+ "version": "2.23.4",
4
4
  "description": "Create browser automations with an LLM agent and replay them as Playwright scripts.",
5
5
  "main": "dist/main.js",
6
6
  "module": "dist/esm/main.js",