@ash-mallick/browserstack-sync 1.3.1 → 1.4.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.
package/README.md CHANGED
@@ -60,29 +60,70 @@ Creates folders and test cases for all spec files in your project.
60
60
 
61
61
  ### 4. Sync Test Run Results to BrowserStack
62
62
 
63
- First, run your tests with JSON reporter:
63
+ Specify your report file location:
64
64
 
65
65
  ```bash
66
- npx playwright test --reporter=list,json --output-file=test-results.json
66
+ npx am-browserstack-sync-runs --report=test-results.json
67
67
  ```
68
68
 
69
- Then sync results:
69
+ **Supported formats (auto-detected):**
70
+ - Playwright JSON
71
+ - Cypress/Mochawesome JSON
72
+ - JUnit XML
73
+
74
+ **Configure report path** (so you don't need `--report` every time):
70
75
 
71
76
  ```bash
72
- npx am-browserstack-sync-runs
77
+ # Via environment variable
78
+ export BROWSERSTACK_REPORT_PATH=test-results.json
79
+
80
+ # Or in .am-browserstack-sync.json
81
+ { "reportPath": "test-results.json" }
73
82
  ```
74
83
 
75
- Or with custom run name:
84
+ **Generate reports** (add to your test command):
76
85
 
77
86
  ```bash
78
- npx am-browserstack-sync-runs --run-name="Nightly Run"
87
+ # Playwright
88
+ npx playwright test --reporter=json --output-file=test-results.json
89
+
90
+ # Cypress
91
+ npx cypress run --reporter mochawesome
92
+ ```
93
+
94
+ **With custom run name:**
95
+
96
+ ```bash
97
+ npx am-browserstack-sync-runs --report=test-results.json --run-name="Nightly Run"
79
98
  ```
80
99
 
81
100
  ---
82
101
 
83
- ## GitLab CI Scheduled Job
102
+ ## GitLab CI Integration
84
103
 
85
- Add to `.gitlab-ci.yml`:
104
+ ### Option A: Your pipeline already generates reports
105
+
106
+ If your existing test job generates JSON/JUnit reports, just add sync after:
107
+
108
+ ```yaml
109
+ # Your existing test job
110
+ test:
111
+ script:
112
+ - npm ci
113
+ - CENV=prod npx playwright test --reporter=allure-playwright,json --output-file=test-results.json
114
+ artifacts:
115
+ paths:
116
+ - test-results.json
117
+
118
+ # Add this job to sync results to BrowserStack
119
+ sync_browserstack:
120
+ needs: [test]
121
+ script:
122
+ - npm install @ash-mallick/browserstack-sync
123
+ - npx am-browserstack-sync-runs --report=test-results.json --run-name="Pipeline #$CI_PIPELINE_ID"
124
+ ```
125
+
126
+ ### Option B: Scheduled sync job (runs tests + syncs)
86
127
 
87
128
  ```yaml
88
129
  scheduled_browserstack_sync:
@@ -90,12 +131,14 @@ scheduled_browserstack_sync:
90
131
  - if: $CI_PIPELINE_SOURCE == "schedule"
91
132
  script:
92
133
  - npm ci
93
- - npx playwright test --reporter=list,json --output-file=test-results.json || true
134
+ - CENV=prod npx playwright test --reporter=allure-playwright,json --output-file=test-results.json || true
94
135
  - npm install @ash-mallick/browserstack-sync
95
- - npx am-browserstack-sync-runs
136
+ - npx am-browserstack-sync-runs --report=test-results.json
96
137
  ```
97
138
 
98
- Add CI/CD Variables in GitLab (**Settings → CI/CD Variables**):
139
+ ### Required CI/CD Variables
140
+
141
+ Add in **Settings → CI/CD → Variables**:
99
142
 
100
143
  | Key | Value |
101
144
  |-----|-------|
@@ -103,7 +146,7 @@ Add CI/CD Variables in GitLab (**Settings → CI/CD → Variables**):
103
146
  | `BROWSERSTACK_ACCESS_KEY` | your_access_key |
104
147
  | `BROWSERSTACK_PROJECT_ID` | PR-XXXX |
105
148
 
106
- Create schedule in **CI/CD → Schedules → New Schedule**.
149
+ For scheduled jobs: **CI/CD → Schedules → New Schedule**.
107
150
 
108
151
  ---
109
152
 
package/bin/sync-runs.js CHANGED
@@ -34,79 +34,108 @@ if (argv.includes('--help') || argv.includes('-h')) {
34
34
  📊 BrowserStack Test Run Sync
35
35
 
36
36
  Sync your Playwright/Cypress test results to BrowserStack Test Management.
37
- Add this command to your existing CI pipeline after tests complete.
37
+ Specify your report file location - supports JSON and XML formats.
38
38
 
39
39
  USAGE:
40
- npx am-browserstack-sync-runs [options]
40
+ npx am-browserstack-sync-runs --report=<path> [options]
41
41
 
42
42
  OPTIONS:
43
- --report=<path> Path to report file (auto-detects if not specified)
43
+ --report=<path> Path to your test report file (required unless configured)
44
44
  --run-name=<name> Name for the test run (default: "CI Run - <timestamp>")
45
- --format=<fmt> Report format: playwright-json, junit (default: auto)
45
+ --format=<fmt> Report format: playwright-json, mochawesome, cypress, junit (auto-detected)
46
46
  --help, -h Show this help
47
47
 
48
+ CONFIGURE REPORT PATH (optional - so you don't need --report every time):
49
+ Environment variable: BROWSERSTACK_REPORT_PATH=test-results.json
50
+ Config file: .am-browserstack-sync.json { "reportPath": "test-results.json" }
51
+ package.json: "amBrowserstackSync": { "reportPath": "test-results.json" }
52
+
48
53
  ENVIRONMENT VARIABLES (required):
49
54
  BROWSERSTACK_USERNAME Your BrowserStack username
50
55
  BROWSERSTACK_ACCESS_KEY Your BrowserStack access key
51
56
  BROWSERSTACK_PROJECT_ID Your project ID (e.g., PR-1234)
52
57
 
53
- EXAMPLES:
54
- # Auto-detect report and sync
55
- npx am-browserstack-sync-runs
58
+ SUPPORTED REPORT FORMATS:
59
+ - Playwright JSON: npx playwright test --reporter=json --output-file=test-results.json
60
+ - Cypress Mochawesome: npx cypress run --reporter mochawesome
61
+ - JUnit XML: npx playwright test --reporter=junit --output-file=junit.xml
56
62
 
63
+ EXAMPLES:
57
64
  # Specify report file
58
65
  npx am-browserstack-sync-runs --report=test-results.json
59
66
 
60
- # Custom run name (great for CI)
61
- npx am-browserstack-sync-runs --run-name="Build #\${CI_PIPELINE_ID}"
67
+ # With custom run name (great for CI)
68
+ npx am-browserstack-sync-runs --report=test-results.json --run-name="Build #123"
62
69
 
63
70
  # Sync JUnit XML results
64
- npx am-browserstack-sync-runs --report=junit.xml --format=junit
71
+ npx am-browserstack-sync-runs --report=junit.xml
65
72
 
66
- ADD TO YOUR CI PIPELINE:
73
+ CI PIPELINE EXAMPLE:
67
74
  # GitLab CI
68
75
  script:
69
- - npm test # Your existing test command
70
- - npx am-browserstack-sync-runs --run-name="Pipeline #\$CI_PIPELINE_ID"
71
-
72
- # GitHub Actions
73
- - run: npm test
74
- - run: npx am-browserstack-sync-runs --run-name="Build #\${{ github.run_number }}"
75
-
76
- PREREQUISITES:
77
- 1. First, onboard your tests to BrowserStack:
78
- npx am-browserstack-sync --all
79
-
80
- 2. Then add run sync to your pipeline:
81
- npx am-browserstack-sync-runs
76
+ - npm ci
77
+ - npx playwright test --reporter=json --output-file=test-results.json || true
78
+ - npx am-browserstack-sync-runs --report=test-results.json --run-name="Pipeline #\$CI_PIPELINE_ID"
82
79
  `);
83
80
  process.exit(0);
84
81
  }
85
82
 
86
- // Auto-detect report file
87
- function findReportFile() {
83
+ // Check if a report file exists at the given path
84
+ function checkReportExists(reportPath) {
85
+ if (!reportPath) return null;
86
+
87
+ const cwd = process.cwd();
88
+ const fullPath = path.isAbsolute(reportPath) ? reportPath : path.join(cwd, reportPath);
89
+
90
+ if (fs.existsSync(fullPath)) {
91
+ const stat = fs.statSync(fullPath);
92
+ const ageMinutes = Math.round((Date.now() - stat.mtime.getTime()) / 60000);
93
+ console.log(`📄 Using report: ${reportPath}`);
94
+ console.log(` Modified: ${ageMinutes < 60 ? ageMinutes + ' minutes ago' : Math.round(ageMinutes / 60) + ' hours ago'}`);
95
+ return fullPath;
96
+ }
97
+
98
+ return null;
99
+ }
100
+
101
+ // Get report path from config or environment
102
+ function getConfiguredReportPath() {
103
+ // Check environment variable first
104
+ if (process.env.BROWSERSTACK_REPORT_PATH) {
105
+ return process.env.BROWSERSTACK_REPORT_PATH;
106
+ }
107
+
108
+ // Check config file
88
109
  const cwd = process.cwd();
89
- const possibleFiles = [
90
- 'test-results.json',
91
- 'results.json',
92
- 'playwright-report/results.json',
93
- 'playwright-report.json',
94
- 'report.json',
95
- 'junit.xml',
96
- 'test-results.xml',
110
+ const configPaths = [
111
+ path.join(cwd, '.am-browserstack-sync.json'),
112
+ path.join(cwd, 'browserstack-sync.json'),
97
113
  ];
98
114
 
99
- for (const file of possibleFiles) {
100
- const fullPath = path.join(cwd, file);
101
- if (fs.existsSync(fullPath)) {
102
- return fullPath;
115
+ for (const configPath of configPaths) {
116
+ if (fs.existsSync(configPath)) {
117
+ try {
118
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
119
+ if (config.reportPath) {
120
+ return config.reportPath;
121
+ }
122
+ } catch {
123
+ // Ignore parse errors
124
+ }
103
125
  }
104
126
  }
105
127
 
106
- // Check for playwright-report directory
107
- const htmlReportDir = path.join(cwd, 'playwright-report');
108
- if (fs.existsSync(htmlReportDir) && fs.statSync(htmlReportDir).isDirectory()) {
109
- return htmlReportDir;
128
+ // Check package.json
129
+ const pkgPath = path.join(cwd, 'package.json');
130
+ if (fs.existsSync(pkgPath)) {
131
+ try {
132
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
133
+ if (pkg.amBrowserstackSync?.reportPath) {
134
+ return pkg.amBrowserstackSync.reportPath;
135
+ }
136
+ } catch {
137
+ // Ignore parse errors
138
+ }
110
139
  }
111
140
 
112
141
  return null;
@@ -131,24 +160,60 @@ const runName = runNameArg
131
160
  ? runNameArg.slice('--run-name='.length).trim()
132
161
  : process.env.BROWSERSTACK_RUN_NAME || `CI Run - ${new Date().toISOString().slice(0, 16).replace('T', ' ')}`;
133
162
 
134
- // Auto-detect report if not specified
163
+ // Determine report path: CLI arg > env var > config file
135
164
  if (!reportPath) {
136
- reportPath = findReportFile();
137
- if (!reportPath) {
138
- console.error(`
139
- ❌ No test report found!
140
-
141
- Make sure your test framework generates a report file:
165
+ reportPath = getConfiguredReportPath();
166
+ }
142
167
 
143
- # Playwright - add to your test command:
144
- npx playwright test --reporter=json --output-file=test-results.json
168
+ // Check if report exists
169
+ if (reportPath) {
170
+ const resolvedPath = checkReportExists(reportPath);
171
+ if (!resolvedPath) {
172
+ console.error(`
173
+ ❌ Report file not found: ${reportPath}
145
174
 
146
- # Or specify the report path:
147
- npx am-browserstack-sync-runs --report=path/to/results.json
175
+ Make sure your test run generates this report before syncing.
148
176
  `);
149
177
  process.exit(1);
150
178
  }
151
- console.log(`📄 Auto-detected report: ${reportPath}`);
179
+ reportPath = resolvedPath;
180
+ } else {
181
+ console.error(`
182
+ ❌ No report path configured!
183
+
184
+ Please specify your test report location using one of these methods:
185
+
186
+ OPTION 1: Command line argument
187
+ npx am-browserstack-sync-runs --report=test-results.json
188
+
189
+ OPTION 2: Environment variable
190
+ export BROWSERSTACK_REPORT_PATH=test-results.json
191
+ npx am-browserstack-sync-runs
192
+
193
+ OPTION 3: Config file (.am-browserstack-sync.json)
194
+ {
195
+ "reportPath": "test-results.json"
196
+ }
197
+
198
+ OPTION 4: package.json
199
+ {
200
+ "amBrowserstackSync": {
201
+ "reportPath": "test-results.json"
202
+ }
203
+ }
204
+
205
+ GENERATING REPORTS:
206
+
207
+ Playwright:
208
+ npx playwright test --reporter=json --output-file=test-results.json
209
+
210
+ Cypress (mochawesome):
211
+ npx cypress run --reporter mochawesome
212
+
213
+ JUnit XML (both):
214
+ npx playwright test --reporter=junit --output-file=junit.xml
215
+ `);
216
+ process.exit(1);
152
217
  }
153
218
 
154
219
  // Auto-detect format if not specified
@@ -108,6 +108,115 @@ function parseJunitXml(reportPath) {
108
108
  return results;
109
109
  }
110
110
 
111
+ /**
112
+ * Parse Mochawesome JSON report format (commonly used with Cypress).
113
+ * @param {string} reportPath - Path to the mochawesome JSON file
114
+ * @returns {Array<{title: string, status: string, duration: number, error?: string}>}
115
+ */
116
+ function parseMochawesome(reportPath) {
117
+ const report = JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
118
+ const results = [];
119
+
120
+ // Mochawesome structure: { results: [{ suites: [{ tests: [...] }] }] }
121
+ function extractTests(suites) {
122
+ for (const suite of suites || []) {
123
+ for (const test of suite.tests || []) {
124
+ results.push({
125
+ title: test.title,
126
+ fullTitle: test.fullTitle || `${suite.title} > ${test.title}`,
127
+ status: mapMochawesomeStatus(test.state || test.pass),
128
+ duration: test.duration || 0,
129
+ error: test.err?.message || test.err?.estack || '',
130
+ file: suite.file,
131
+ });
132
+ }
133
+
134
+ // Recursively process nested suites
135
+ if (suite.suites) {
136
+ extractTests(suite.suites);
137
+ }
138
+ }
139
+ }
140
+
141
+ // Handle both mochawesome and mochawesome-merge formats
142
+ if (report.results) {
143
+ for (const result of report.results) {
144
+ extractTests(result.suites);
145
+ }
146
+ } else if (report.suites) {
147
+ extractTests(report.suites);
148
+ }
149
+
150
+ return results;
151
+ }
152
+
153
+ /**
154
+ * Parse Cypress JSON report (spec-based results).
155
+ * @param {string} reportPath - Path to the Cypress JSON report
156
+ * @returns {Array<{title: string, status: string, duration: number, error?: string}>}
157
+ */
158
+ function parseCypressJson(reportPath) {
159
+ const report = JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
160
+ const results = [];
161
+
162
+ // Cypress native JSON structure
163
+ // { runs: [{ tests: [...], spec: {...} }] }
164
+ if (report.runs) {
165
+ for (const run of report.runs) {
166
+ for (const test of run.tests || []) {
167
+ const attempts = test.attempts || [];
168
+ const lastAttempt = attempts[attempts.length - 1] || {};
169
+
170
+ results.push({
171
+ title: test.title?.join(' > ') || test.title,
172
+ status: mapCypressStatus(test.state || lastAttempt.state),
173
+ duration: test.duration || lastAttempt.duration || 0,
174
+ error: lastAttempt.error?.message || test.displayError || '',
175
+ file: run.spec?.relative || run.spec?.name,
176
+ });
177
+ }
178
+ }
179
+ }
180
+
181
+ // Cypress Dashboard JSON structure
182
+ // { tests: [...] }
183
+ if (report.tests && !report.runs) {
184
+ for (const test of report.tests) {
185
+ results.push({
186
+ title: test.title || test.name,
187
+ status: mapCypressStatus(test.state || test.status),
188
+ duration: test.duration || 0,
189
+ error: test.error?.message || '',
190
+ });
191
+ }
192
+ }
193
+
194
+ return results;
195
+ }
196
+
197
+ /**
198
+ * Map Mochawesome status to BrowserStack status.
199
+ */
200
+ function mapMochawesomeStatus(status) {
201
+ if (status === true || status === 'passed') return 'passed';
202
+ if (status === false || status === 'failed') return 'failed';
203
+ if (status === 'pending' || status === 'skipped') return 'skipped';
204
+ return status || 'passed';
205
+ }
206
+
207
+ /**
208
+ * Map Cypress status to BrowserStack status.
209
+ */
210
+ function mapCypressStatus(status) {
211
+ const map = {
212
+ passed: 'passed',
213
+ failed: 'failed',
214
+ pending: 'skipped',
215
+ skipped: 'skipped',
216
+ };
217
+ return map[status] || status;
218
+ }
219
+
111
220
  /**
112
221
  * Parse Playwright HTML report's index.html or results.json
113
222
  */
@@ -211,17 +320,52 @@ export async function syncResultsFromReport(options = {}) {
211
320
  // Parse the report based on format
212
321
  console.log('\n Parsing test results...');
213
322
  let testResults;
323
+ let detectedFormat = format;
214
324
 
215
325
  const stat = fs.statSync(reportPath);
216
326
  if (stat.isDirectory()) {
217
327
  // Assume it's a Playwright HTML report directory
218
328
  testResults = parsePlaywrightHtmlReport(reportPath);
329
+ detectedFormat = 'playwright-html';
219
330
  } else if (format === 'junit' || reportPath.endsWith('.xml')) {
220
331
  testResults = parseJunitXml(reportPath);
332
+ detectedFormat = 'junit-xml';
221
333
  } else {
222
- testResults = parsePlaywrightJson(reportPath);
334
+ // Try to auto-detect JSON format by reading the file
335
+ const content = fs.readFileSync(reportPath, 'utf-8');
336
+ const json = JSON.parse(content);
337
+
338
+ if (format === 'mochawesome' || json.stats?.testsRegistered !== undefined || (json.results && json.results[0]?.suites)) {
339
+ // Mochawesome format (Cypress)
340
+ testResults = parseMochawesome(reportPath);
341
+ detectedFormat = 'mochawesome';
342
+ } else if (format === 'cypress' || json.runs) {
343
+ // Cypress native JSON format
344
+ testResults = parseCypressJson(reportPath);
345
+ detectedFormat = 'cypress-json';
346
+ } else if (json.suites || json.config?.projects) {
347
+ // Playwright JSON format
348
+ testResults = parsePlaywrightJson(reportPath);
349
+ detectedFormat = 'playwright-json';
350
+ } else {
351
+ // Fallback: try Playwright first, then Mochawesome
352
+ try {
353
+ testResults = parsePlaywrightJson(reportPath);
354
+ detectedFormat = 'playwright-json';
355
+ } catch {
356
+ try {
357
+ testResults = parseMochawesome(reportPath);
358
+ detectedFormat = 'mochawesome';
359
+ } catch {
360
+ testResults = parseCypressJson(reportPath);
361
+ detectedFormat = 'cypress-json';
362
+ }
363
+ }
364
+ }
223
365
  }
224
366
 
367
+ console.log(` Detected format: ${detectedFormat}`)
368
+
225
369
  console.log(` Found ${testResults.length} test result(s)`);
226
370
 
227
371
  if (testResults.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ash-mallick/browserstack-sync",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Sync Playwright & Cypress e2e specs to CSV and BrowserStack Test Management with FREE AI-powered test step extraction using Ollama (local).",
5
5
  "author": "Ashutosh Mallick",
6
6
  "type": "module",