@bugzy-ai/bugzy 1.2.0

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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +248 -0
  3. package/dist/cli/index.cjs +7547 -0
  4. package/dist/cli/index.cjs.map +1 -0
  5. package/dist/cli/index.d.cts +1 -0
  6. package/dist/cli/index.d.ts +1 -0
  7. package/dist/cli/index.js +7539 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/index.cjs +6439 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.d.cts +54 -0
  12. package/dist/index.d.ts +54 -0
  13. package/dist/index.js +6383 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/subagents/index.cjs +2703 -0
  16. package/dist/subagents/index.cjs.map +1 -0
  17. package/dist/subagents/index.d.cts +34 -0
  18. package/dist/subagents/index.d.ts +34 -0
  19. package/dist/subagents/index.js +2662 -0
  20. package/dist/subagents/index.js.map +1 -0
  21. package/dist/subagents/metadata.cjs +207 -0
  22. package/dist/subagents/metadata.cjs.map +1 -0
  23. package/dist/subagents/metadata.d.cts +31 -0
  24. package/dist/subagents/metadata.d.ts +31 -0
  25. package/dist/subagents/metadata.js +174 -0
  26. package/dist/subagents/metadata.js.map +1 -0
  27. package/dist/tasks/index.cjs +3464 -0
  28. package/dist/tasks/index.cjs.map +1 -0
  29. package/dist/tasks/index.d.cts +44 -0
  30. package/dist/tasks/index.d.ts +44 -0
  31. package/dist/tasks/index.js +3431 -0
  32. package/dist/tasks/index.js.map +1 -0
  33. package/dist/templates/init/.bugzy/runtime/project-context.md +35 -0
  34. package/dist/templates/init/.bugzy/runtime/templates/test-plan-template.md +25 -0
  35. package/dist/templates/init/.bugzy/runtime/testing-best-practices.md +278 -0
  36. package/dist/templates/init/.gitignore-template +4 -0
  37. package/package.json +95 -0
  38. package/templates/init/.bugzy/runtime/knowledge-base.md +61 -0
  39. package/templates/init/.bugzy/runtime/knowledge-maintenance-guide.md +97 -0
  40. package/templates/init/.bugzy/runtime/project-context.md +35 -0
  41. package/templates/init/.bugzy/runtime/subagent-memory-guide.md +87 -0
  42. package/templates/init/.bugzy/runtime/templates/test-plan-template.md +25 -0
  43. package/templates/init/.bugzy/runtime/templates/test-result-schema.md +498 -0
  44. package/templates/init/.bugzy/runtime/test-execution-strategy.md +535 -0
  45. package/templates/init/.bugzy/runtime/testing-best-practices.md +632 -0
  46. package/templates/init/.gitignore-template +25 -0
  47. package/templates/init/CLAUDE.md +157 -0
  48. package/templates/init/test-runs/README.md +45 -0
  49. package/templates/playwright/BasePage.template.ts +190 -0
  50. package/templates/playwright/auth.setup.template.ts +89 -0
  51. package/templates/playwright/dataGenerators.helper.template.ts +148 -0
  52. package/templates/playwright/dateUtils.helper.template.ts +96 -0
  53. package/templates/playwright/pages.fixture.template.ts +50 -0
  54. package/templates/playwright/playwright.config.template.ts +97 -0
  55. package/templates/playwright/reporters/bugzy-reporter.ts +454 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Date Utility Functions
3
+ * Generated by Bugzy - https://github.com/bugzy-ai/bugzy
4
+ *
5
+ * Helper functions for working with dates in tests
6
+ */
7
+
8
+ /**
9
+ * Get today's date in YYYY-MM-DD format
10
+ */
11
+ export function getTodayDate(): string {
12
+ const today = new Date();
13
+ return formatDate(today);
14
+ }
15
+
16
+ /**
17
+ * Get tomorrow's date in YYYY-MM-DD format
18
+ */
19
+ export function getTomorrowDate(): string {
20
+ const tomorrow = new Date();
21
+ tomorrow.setDate(tomorrow.getDate() + 1);
22
+ return formatDate(tomorrow);
23
+ }
24
+
25
+ /**
26
+ * Get yesterday's date in YYYY-MM-DD format
27
+ */
28
+ export function getYesterdayDate(): string {
29
+ const yesterday = new Date();
30
+ yesterday.setDate(yesterday.getDate() - 1);
31
+ return formatDate(yesterday);
32
+ }
33
+
34
+ /**
35
+ * Get date N days from now
36
+ * @param days - Number of days (positive for future, negative for past)
37
+ */
38
+ export function getDateDaysFromNow(days: number): string {
39
+ const date = new Date();
40
+ date.setDate(date.getDate() + days);
41
+ return formatDate(date);
42
+ }
43
+
44
+ /**
45
+ * Format date as YYYY-MM-DD
46
+ * @param date - Date object to format
47
+ */
48
+ export function formatDate(date: Date): string {
49
+ const year = date.getFullYear();
50
+ const month = String(date.getMonth() + 1).padStart(2, '0');
51
+ const day = String(date.getDate()).padStart(2, '0');
52
+ return `${year}-${month}-${day}`;
53
+ }
54
+
55
+ /**
56
+ * Format date as MM/DD/YYYY
57
+ * @param date - Date object to format
58
+ */
59
+ export function formatDateUS(date: Date): string {
60
+ const year = date.getFullYear();
61
+ const month = String(date.getMonth() + 1).padStart(2, '0');
62
+ const day = String(date.getDate()).padStart(2, '0');
63
+ return `${month}/${day}/${year}`;
64
+ }
65
+
66
+ /**
67
+ * Parse date string (YYYY-MM-DD) to Date object
68
+ * @param dateString - Date string in YYYY-MM-DD format
69
+ */
70
+ export function parseDate(dateString: string): Date {
71
+ const [year, month, day] = dateString.split('-').map(Number);
72
+ return new Date(year, month - 1, day);
73
+ }
74
+
75
+ /**
76
+ * Get current timestamp in milliseconds
77
+ */
78
+ export function getCurrentTimestamp(): number {
79
+ return Date.now();
80
+ }
81
+
82
+ /**
83
+ * Get current ISO timestamp string
84
+ */
85
+ export function getCurrentISOTimestamp(): string {
86
+ return new Date().toISOString();
87
+ }
88
+
89
+ /**
90
+ * Wait for a specific amount of time (use sparingly!)
91
+ * Note: Prefer Playwright's built-in waiting mechanisms over this
92
+ * @param ms - Milliseconds to wait
93
+ */
94
+ export async function wait(ms: number): Promise<void> {
95
+ return new Promise((resolve) => setTimeout(resolve, ms));
96
+ }
@@ -0,0 +1,50 @@
1
+ import { test as base } from '@playwright/test';
2
+
3
+ /**
4
+ * Custom Fixtures for Page Objects
5
+ * Generated by Bugzy - https://github.com/bugzy-ai/bugzy
6
+ *
7
+ * Fixtures provide a way to set up and tear down test dependencies
8
+ * This example shows how to create fixtures for page objects
9
+ *
10
+ * Usage in tests:
11
+ * test('my test', async ({ homePage }) => {
12
+ * await homePage.navigate();
13
+ * // ...
14
+ * });
15
+ */
16
+
17
+ // Define fixture types
18
+ type PageFixtures = {
19
+ // Add your page object fixtures here
20
+ // Example:
21
+ // homePage: HomePage;
22
+ // loginPage: LoginPage;
23
+ };
24
+
25
+ /**
26
+ * Extend the base test with custom fixtures
27
+ *
28
+ * Example implementation:
29
+ *
30
+ * import { HomePage } from '@pages/HomePage';
31
+ * import { LoginPage } from '@pages/LoginPage';
32
+ *
33
+ * export const test = base.extend<PageFixtures>({
34
+ * homePage: async ({ page }, use) => {
35
+ * const homePage = new HomePage(page);
36
+ * await use(homePage);
37
+ * },
38
+ *
39
+ * loginPage: async ({ page }, use) => {
40
+ * const loginPage = new LoginPage(page);
41
+ * await use(loginPage);
42
+ * },
43
+ * });
44
+ */
45
+
46
+ export const test = base.extend<PageFixtures>({
47
+ // Add your fixtures here
48
+ });
49
+
50
+ export { expect } from '@playwright/test';
@@ -0,0 +1,97 @@
1
+ import { defineConfig, devices } from '@playwright/test';
2
+ import * as dotenv from 'dotenv';
3
+ import * as path from 'path';
4
+
5
+ // Load non-secret environment variables from .env.testdata
6
+ dotenv.config({ path: path.resolve(__dirname, '.env.testdata') });
7
+
8
+ // Load secret environment variables from .env (for local development)
9
+ dotenv.config({ path: path.resolve(__dirname, '.env') });
10
+
11
+ /**
12
+ * Playwright Configuration
13
+ * Generated by Bugzy - https://github.com/bugzy-ai/bugzy
14
+ *
15
+ * This configuration follows Playwright best practices:
16
+ * - Parallel execution for speed
17
+ * - Retries in CI for stability
18
+ * - Trace capture for debugging
19
+ * - Optimized artifact collection
20
+ */
21
+
22
+ export default defineConfig({
23
+ // Test directory
24
+ testDir: './tests/specs',
25
+
26
+ // Fully parallel test execution
27
+ fullyParallel: true,
28
+
29
+ // Fail the build on CI if you accidentally left test.only in the source code
30
+ forbidOnly: !!process.env.CI,
31
+
32
+ // Retry on CI only (2 retries), no retries locally
33
+ retries: process.env.CI ? 2 : 0,
34
+
35
+ // Opt out of parallel tests on CI for stability (can be adjusted)
36
+ workers: process.env.CI ? 2 : undefined,
37
+
38
+ // Reporters
39
+ reporter: [
40
+ ['./reporters/bugzy-reporter.ts']
41
+ ],
42
+
43
+ // Global timeout
44
+ timeout: 30000,
45
+
46
+ // Expect timeout
47
+ expect: {
48
+ timeout: 5000
49
+ },
50
+
51
+ // Shared settings for all projects
52
+ use: {
53
+ // Base URL from environment variable
54
+ baseURL: process.env.TEST_BASE_URL || 'http://localhost:3000',
55
+
56
+ // Collect trace on failure
57
+ trace: 'retain-on-failure',
58
+
59
+ // Screenshot only on failure
60
+ screenshot: 'only-on-failure',
61
+
62
+ // Video for all tests (always record)
63
+ video: 'on',
64
+
65
+ // Maximum time for actions (click, fill, etc.)
66
+ actionTimeout: 10000,
67
+ },
68
+
69
+ // Configure projects for different browsers
70
+ projects: [
71
+ // Setup project - runs once before other tests
72
+ {
73
+ name: 'setup',
74
+ testDir: './tests/setup',
75
+ testMatch: /.*\.setup\.ts/,
76
+ },
77
+
78
+ // Chromium
79
+ {
80
+ name: 'chromium',
81
+ use: {
82
+ ...devices['Desktop Chrome'],
83
+ // Use authenticated state if available
84
+ storageState: 'tests/.auth/user.json',
85
+ },
86
+ dependencies: ['setup'],
87
+ },
88
+ ],
89
+
90
+ // Run your local dev server before starting the tests
91
+ // Uncomment and configure if needed:
92
+ // webServer: {
93
+ // command: 'npm run dev',
94
+ // url: 'http://127.0.0.1:3000',
95
+ // reuseExistingServer: !process.env.CI,
96
+ // },
97
+ });
@@ -0,0 +1,454 @@
1
+ import type {
2
+ Reporter,
3
+ FullConfig,
4
+ Suite,
5
+ TestCase,
6
+ TestResult,
7
+ FullResult,
8
+ TestStep,
9
+ } from '@playwright/test/reporter';
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+
13
+ /**
14
+ * Step data for steps.json
15
+ */
16
+ interface StepData {
17
+ index: number;
18
+ timestamp: string;
19
+ videoTimeSeconds: number;
20
+ action: string;
21
+ status: 'success' | 'failed' | 'skipped';
22
+ description: string;
23
+ technicalDetails: string;
24
+ duration?: number;
25
+ }
26
+
27
+ /**
28
+ * Bugzy Custom Playwright Reporter
29
+ *
30
+ * Records test executions in hierarchical structure:
31
+ * test-runs/YYYYMMDD-HHMMSS/TC-{id}/exec-{num}/
32
+ *
33
+ * Features:
34
+ * - Groups multiple test runs under same directory when BUGZY_EXECUTION_ID matches
35
+ * - Checks latest directory's manifest to reuse existing session directory
36
+ * - Tracks multiple execution attempts per test
37
+ * - Records videos for all tests
38
+ * - Captures traces/screenshots for failures only
39
+ * - Links to BUGZY_EXECUTION_ID for session tracking
40
+ * - Generates manifest.json with execution summary
41
+ * - Generates steps.json with video timestamps for test.step() calls
42
+ */
43
+ class BugzyReporter implements Reporter {
44
+ private testRunDir!: string;
45
+ private timestamp!: string;
46
+ private bugzyExecutionId!: string;
47
+ private startTime!: Date;
48
+ private testResults: Map<string, Array<any>> = new Map();
49
+ private testSteps: Map<string, Array<StepData>> = new Map();
50
+ private testStartTimes: Map<string, number> = new Map();
51
+
52
+ constructor() {
53
+ // No longer need to read execution number from environment
54
+ // It will be auto-detected per test case
55
+ }
56
+
57
+ /**
58
+ * Called once before running tests
59
+ */
60
+ onBegin(config: FullConfig, suite: Suite): void {
61
+ this.startTime = new Date();
62
+
63
+ // Generate timestamp in YYYYMMDD-HHMMSS format
64
+ this.timestamp = this.startTime
65
+ .toISOString()
66
+ .replace(/[-:]/g, '')
67
+ .replace(/T/, '-')
68
+ .slice(0, 15);
69
+
70
+ const testRunsRoot = path.join(process.cwd(), 'test-runs');
71
+
72
+ // Check if we should reuse an existing session
73
+ let reuseDir: string | null = null;
74
+
75
+ // If BUGZY_EXECUTION_ID is provided, use it directly
76
+ if (process.env.BUGZY_EXECUTION_ID) {
77
+ this.bugzyExecutionId = process.env.BUGZY_EXECUTION_ID;
78
+ } else {
79
+ // For local runs, check if we can reuse the latest session
80
+ // Reuse if the latest manifest is within 60 minutes
81
+ if (fs.existsSync(testRunsRoot)) {
82
+ const dirs = fs.readdirSync(testRunsRoot)
83
+ .filter(d => fs.statSync(path.join(testRunsRoot, d)).isDirectory())
84
+ .sort()
85
+ .reverse(); // Sort descending (latest first)
86
+
87
+ if (dirs.length > 0) {
88
+ const latestDir = dirs[0];
89
+ const manifestPath = path.join(testRunsRoot, latestDir, 'manifest.json');
90
+
91
+ if (fs.existsSync(manifestPath)) {
92
+ try {
93
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
94
+ const manifestTime = new Date(manifest.startTime).getTime();
95
+ const currentTime = this.startTime.getTime();
96
+ const minutesDiff = (currentTime - manifestTime) / (1000 * 60);
97
+
98
+ // Reuse if within 60 minutes and has a local execution ID
99
+ if (minutesDiff <= 60 && manifest.bugzyExecutionId?.startsWith('local-')) {
100
+ this.bugzyExecutionId = manifest.bugzyExecutionId;
101
+ reuseDir = latestDir;
102
+ }
103
+ } catch (err) {
104
+ // Ignore parsing errors
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ // If no session to reuse, generate new local ID
111
+ if (!this.bugzyExecutionId) {
112
+ this.bugzyExecutionId = 'local-' + this.timestamp;
113
+ }
114
+ }
115
+
116
+ // If we have a specific execution ID but haven't found a reuse dir yet, check for matching session
117
+ if (!reuseDir && fs.existsSync(testRunsRoot)) {
118
+ const dirs = fs.readdirSync(testRunsRoot)
119
+ .filter(d => fs.statSync(path.join(testRunsRoot, d)).isDirectory())
120
+ .sort()
121
+ .reverse();
122
+
123
+ for (const dir of dirs) {
124
+ const manifestPath = path.join(testRunsRoot, dir, 'manifest.json');
125
+ if (fs.existsSync(manifestPath)) {
126
+ try {
127
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
128
+ if (manifest.bugzyExecutionId === this.bugzyExecutionId) {
129
+ reuseDir = dir;
130
+ break;
131
+ }
132
+ } catch (err) {
133
+ // Ignore parsing errors
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ if (reuseDir) {
140
+ this.testRunDir = path.join(testRunsRoot, reuseDir);
141
+ console.log(`\n🔄 Continuing test run: ${reuseDir}`);
142
+ console.log(`📋 Execution ID: ${this.bugzyExecutionId}`);
143
+ console.log(`📁 Output directory: ${this.testRunDir}\n`);
144
+ } else {
145
+ this.testRunDir = path.join(testRunsRoot, this.timestamp);
146
+ fs.mkdirSync(this.testRunDir, { recursive: true });
147
+ console.log(`\n🆕 New test run: ${this.timestamp}`);
148
+ console.log(`📋 Execution ID: ${this.bugzyExecutionId}`);
149
+ console.log(`📁 Output directory: ${this.testRunDir}\n`);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Called after each test completes
155
+ */
156
+ onTestEnd(test: TestCase, result: TestResult): void {
157
+ // Extract test ID from test title or file path
158
+ const testId = this.extractTestId(test);
159
+
160
+ // Create test case directory
161
+ const testCaseDir = path.join(this.testRunDir, testId);
162
+ fs.mkdirSync(testCaseDir, { recursive: true });
163
+
164
+ // Auto-detect execution number from existing folders
165
+ let executionNum = 1;
166
+ if (fs.existsSync(testCaseDir)) {
167
+ const existingExecs = fs.readdirSync(testCaseDir)
168
+ .filter(d => d.startsWith('exec-') && fs.statSync(path.join(testCaseDir, d)).isDirectory())
169
+ .map(d => parseInt(d.replace('exec-', ''), 10))
170
+ .filter(n => !isNaN(n));
171
+
172
+ if (existingExecs.length > 0) {
173
+ executionNum = Math.max(...existingExecs) + 1;
174
+ }
175
+ }
176
+
177
+ // Create execution directory
178
+ const execDir = path.join(testCaseDir, `exec-${executionNum}`);
179
+ fs.mkdirSync(execDir, { recursive: true });
180
+
181
+ // Prepare result data in Playwright format
182
+ const resultData = {
183
+ status: result.status,
184
+ duration: result.duration,
185
+ errors: result.errors,
186
+ retry: result.retry,
187
+ startTime: result.startTime.toISOString(),
188
+ attachments: [] as Array<{ name: string; path: string; contentType: string }>,
189
+ };
190
+
191
+ // Handle attachments (videos, traces, screenshots)
192
+ let hasVideo = false;
193
+ let hasTrace = false;
194
+ let hasScreenshots = false;
195
+
196
+ for (const attachment of result.attachments) {
197
+ if (attachment.name === 'video' && attachment.path) {
198
+ // Copy video file to execution directory
199
+ const videoFileName = 'video.webm';
200
+ const videoDestPath = path.join(execDir, videoFileName);
201
+
202
+ try {
203
+ fs.copyFileSync(attachment.path, videoDestPath);
204
+ resultData.attachments.push({
205
+ name: 'video',
206
+ path: videoFileName,
207
+ contentType: attachment.contentType || 'video/webm',
208
+ });
209
+ hasVideo = true;
210
+ } catch (err) {
211
+ console.error(`Failed to copy video: ${err}`);
212
+ }
213
+ } else if (attachment.name === 'trace' && attachment.path) {
214
+ // Copy trace file to execution directory (only for failures)
215
+ if (result.status === 'failed' || result.status === 'timedOut') {
216
+ const traceFileName = 'trace.zip';
217
+ const traceDestPath = path.join(execDir, traceFileName);
218
+
219
+ try {
220
+ fs.copyFileSync(attachment.path, traceDestPath);
221
+ resultData.attachments.push({
222
+ name: 'trace',
223
+ path: traceFileName,
224
+ contentType: attachment.contentType || 'application/zip',
225
+ });
226
+ hasTrace = true;
227
+ } catch (err) {
228
+ console.error(`Failed to copy trace: ${err}`);
229
+ }
230
+ }
231
+ } else if (attachment.name === 'screenshot' && attachment.path) {
232
+ // Copy screenshots to execution directory (only for failures)
233
+ if (result.status === 'failed' || result.status === 'timedOut') {
234
+ const screenshotsDir = path.join(execDir, 'screenshots');
235
+ fs.mkdirSync(screenshotsDir, { recursive: true });
236
+
237
+ const screenshotFileName = path.basename(attachment.path);
238
+ const screenshotDestPath = path.join(screenshotsDir, screenshotFileName);
239
+
240
+ try {
241
+ fs.copyFileSync(attachment.path, screenshotDestPath);
242
+ resultData.attachments.push({
243
+ name: 'screenshot',
244
+ path: path.join('screenshots', screenshotFileName),
245
+ contentType: attachment.contentType || 'image/png',
246
+ });
247
+ hasScreenshots = true;
248
+ } catch (err) {
249
+ console.error(`Failed to copy screenshot: ${err}`);
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ // Write result.json
256
+ const resultPath = path.join(execDir, 'result.json');
257
+ fs.writeFileSync(resultPath, JSON.stringify(resultData, null, 2));
258
+
259
+ // Store execution info for manifest
260
+ if (!this.testResults.has(testId)) {
261
+ this.testResults.set(testId, []);
262
+ }
263
+
264
+ this.testResults.get(testId)!.push({
265
+ number: executionNum,
266
+ status: result.status,
267
+ duration: result.duration,
268
+ videoFile: hasVideo ? 'video.webm' : null,
269
+ hasTrace,
270
+ hasScreenshots,
271
+ error: result.errors.length > 0 ? result.errors[0].message : null,
272
+ });
273
+
274
+ // Generate steps.json if test has steps
275
+ const testKey = this.getTestKey(test);
276
+ const steps = this.testSteps.get(testKey);
277
+ if (steps && steps.length > 0) {
278
+ const stepsData = {
279
+ steps,
280
+ summary: {
281
+ totalSteps: steps.length,
282
+ successfulSteps: steps.filter(s => s.status === 'success').length,
283
+ failedSteps: steps.filter(s => s.status === 'failed').length,
284
+ skippedSteps: steps.filter(s => s.status === 'skipped').length,
285
+ },
286
+ };
287
+
288
+ const stepsPath = path.join(execDir, 'steps.json');
289
+ fs.writeFileSync(stepsPath, JSON.stringify(stepsData, null, 2));
290
+ }
291
+
292
+ // Log execution result
293
+ const statusIcon = result.status === 'passed' ? '✅' : result.status === 'failed' ? '❌' : '⚠️';
294
+ console.log(`${statusIcon} ${testId} [exec-${executionNum}] - ${result.status} (${result.duration}ms)`);
295
+ }
296
+
297
+ /**
298
+ * Called when a test step begins
299
+ */
300
+ onStepBegin(test: TestCase, _result: TestResult, step: TestStep): void {
301
+ // Only track test.step() calls (not hooks, fixtures, or expects)
302
+ if (step.category !== 'test.step') {
303
+ return;
304
+ }
305
+
306
+ const testKey = this.getTestKey(test);
307
+
308
+ // Record test start time on first step
309
+ if (!this.testStartTimes.has(testKey)) {
310
+ this.testStartTimes.set(testKey, step.startTime.getTime());
311
+ }
312
+
313
+ // Initialize steps array for this test
314
+ if (!this.testSteps.has(testKey)) {
315
+ this.testSteps.set(testKey, []);
316
+ }
317
+
318
+ const steps = this.testSteps.get(testKey)!;
319
+ const testStartTime = this.testStartTimes.get(testKey)!;
320
+ const videoTimeSeconds = Math.floor((step.startTime.getTime() - testStartTime) / 1000);
321
+
322
+ steps.push({
323
+ index: steps.length + 1,
324
+ timestamp: step.startTime.toISOString(),
325
+ videoTimeSeconds,
326
+ action: step.title,
327
+ status: 'success', // Will be updated in onStepEnd if it fails
328
+ description: `${step.title} - in progress`,
329
+ technicalDetails: 'test.step',
330
+ });
331
+ }
332
+
333
+ /**
334
+ * Called when a test step ends
335
+ */
336
+ onStepEnd(test: TestCase, _result: TestResult, step: TestStep): void {
337
+ // Only track test.step() calls
338
+ if (step.category !== 'test.step') {
339
+ return;
340
+ }
341
+
342
+ const testKey = this.getTestKey(test);
343
+ const steps = this.testSteps.get(testKey);
344
+
345
+ if (!steps || steps.length === 0) {
346
+ return;
347
+ }
348
+
349
+ // Update the last step with final status and duration
350
+ const lastStep = steps[steps.length - 1];
351
+ lastStep.duration = step.duration;
352
+
353
+ if (step.error) {
354
+ lastStep.status = 'failed';
355
+ lastStep.description = `${step.title} - failed: ${step.error.message}`;
356
+ } else {
357
+ lastStep.status = 'success';
358
+ lastStep.description = `${step.title} - completed successfully`;
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Called after all tests complete
364
+ */
365
+ onEnd(result: FullResult): void {
366
+ const endTime = new Date();
367
+
368
+ // Calculate statistics
369
+ let totalTests = 0;
370
+ let totalExecutions = 0;
371
+ let passedTests = 0;
372
+ let failedTests = 0;
373
+
374
+ const testCases: Array<any> = [];
375
+
376
+ for (const [testId, executions] of this.testResults.entries()) {
377
+ totalTests++;
378
+ totalExecutions += executions.length;
379
+
380
+ const finalStatus = executions[executions.length - 1].status;
381
+ if (finalStatus === 'passed') {
382
+ passedTests++;
383
+ } else {
384
+ failedTests++;
385
+ }
386
+
387
+ testCases.push({
388
+ id: testId,
389
+ name: testId.replace(/^TC-\d+-/, '').replace(/-/g, ' '),
390
+ totalExecutions: executions.length,
391
+ finalStatus,
392
+ executions,
393
+ });
394
+ }
395
+
396
+ // Generate manifest.json
397
+ const manifest = {
398
+ bugzyExecutionId: this.bugzyExecutionId,
399
+ timestamp: this.timestamp,
400
+ startTime: this.startTime.toISOString(),
401
+ endTime: endTime.toISOString(),
402
+ status: result.status,
403
+ stats: {
404
+ totalTests,
405
+ passed: passedTests,
406
+ failed: failedTests,
407
+ totalExecutions,
408
+ },
409
+ testCases,
410
+ };
411
+
412
+ const manifestPath = path.join(this.testRunDir, 'manifest.json');
413
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
414
+
415
+ console.log(`\n📊 Test Run Summary:`);
416
+ console.log(` Total tests: ${totalTests}`);
417
+ console.log(` Passed: ${passedTests}`);
418
+ console.log(` Failed: ${failedTests}`);
419
+ console.log(` Total executions: ${totalExecutions}`);
420
+ console.log(` Manifest: ${manifestPath}\n`);
421
+ }
422
+
423
+ /**
424
+ * Extract test ID from test case
425
+ * Generates TC-XXX-{test-name} format
426
+ */
427
+ private extractTestId(test: TestCase): string {
428
+ // Try to extract from test title
429
+ const title = test.title.toLowerCase().replace(/\s+/g, '-');
430
+
431
+ // Get test file name without extension
432
+ const fileName = path.basename(test.location.file, path.extname(test.location.file));
433
+
434
+ // Extract number from filename if it follows TC-XXX pattern
435
+ const tcMatch = fileName.match(/TC-(\d+)/i);
436
+ if (tcMatch) {
437
+ return `TC-${tcMatch[1]}-${title}`;
438
+ }
439
+
440
+ // Otherwise generate from index
441
+ // This is a simple fallback - you may want to improve this
442
+ const testIndex = String(test.parent.tests.indexOf(test) + 1).padStart(3, '0');
443
+ return `TC-${testIndex}-${title}`;
444
+ }
445
+
446
+ /**
447
+ * Generate unique key for test to track steps across retries
448
+ */
449
+ private getTestKey(test: TestCase): string {
450
+ return `${test.location.file}::${test.title}`;
451
+ }
452
+ }
453
+
454
+ export default BugzyReporter;