haven-cypress-integration 1.6.2 → 2.0.1

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,75 +1,151 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const axios = require("axios");
4
- const glob = require("glob");
5
- // AWS SDK removed - EC2 instance handles S3 uploads
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const https = require('https');
6
4
 
7
- // Paths
5
+ // Shared paths provided by HAVEN runner
8
6
  const automationCasesPath = "/shared/automation-cases.json";
9
7
  const postBaseUrlPath = "/shared/result-post-url.txt";
10
8
  const triggeredByPath = "/shared/triggered-by.txt";
11
- const baseUrl = fs.readFileSync(postBaseUrlPath, "utf-8").trim();
12
9
 
13
- // Read triggered_by value
10
+ // Cache for spec file tag mappings
11
+ const specTagsCache = new Map();
12
+
13
+ /**
14
+ * Parse a Cypress spec file to extract test title -> automation ID mappings
15
+ * Looks for patterns like: it('Test title', { tags: ['@TC-AUTO-xxx'] }, () => {
16
+ */
17
+ function parseSpecFileForTags(specFilePath) {
18
+ if (specTagsCache.has(specFilePath)) {
19
+ return specTagsCache.get(specFilePath);
20
+ }
21
+
22
+ const titleToAutomationIds = new Map();
23
+
24
+ try {
25
+ const content = fs.readFileSync(specFilePath, 'utf-8');
26
+
27
+ // Match it() calls with tags - handles both single and double quotes
28
+ // Pattern: it('title' or it("title", { tags: [...] }
29
+ const itRegex = /it\s*\(\s*(['"`])([^'"`]+)\1\s*,\s*\{\s*tags\s*:\s*\[([^\]]+)\]/g;
30
+
31
+ let match;
32
+ while ((match = itRegex.exec(content)) !== null) {
33
+ const testTitle = match[2];
34
+ const tagsStr = match[3];
35
+
36
+ // Extract TC-AUTO tags from the tags array string
37
+ const automationIds = [];
38
+ const tagMatches = tagsStr.match(/@?TC-AUTO-[\w.-]+/g) || [];
39
+ tagMatches.forEach(tag => {
40
+ automationIds.push(tag.replace(/['"]/g, ''));
41
+ });
42
+
43
+ if (automationIds.length > 0) {
44
+ titleToAutomationIds.set(testTitle, automationIds);
45
+ }
46
+ }
47
+ } catch (err) {
48
+ console.warn(`Could not parse spec file ${specFilePath}:`, err.message);
49
+ }
50
+
51
+ specTagsCache.set(specFilePath, titleToAutomationIds);
52
+ return titleToAutomationIds;
53
+ }
54
+
55
+ const baseUrl = fs.readFileSync(postBaseUrlPath, "utf-8").trim();
14
56
  let triggeredBy = null;
15
57
  try {
16
58
  if (fs.existsSync(triggeredByPath)) {
17
59
  triggeredBy = fs.readFileSync(triggeredByPath, "utf-8").trim();
18
60
  }
19
61
  } catch (err) {
20
- console.warn("⚠️ Could not read triggered-by.txt:", err.message);
62
+ console.warn("Could not read triggered-by.txt:", err.message);
21
63
  }
22
64
 
23
- (async () => {
24
- let automationIds = null;
25
- const formatted = [];
26
- const foundIds = new Set();
27
-
65
+ function findJsonFiles(dir) {
66
+ const files = [];
28
67
  try {
29
- const expectedCases = JSON.parse(
30
- fs.readFileSync(automationCasesPath, "utf-8")
31
- );
32
- const ids = expectedCases
33
- .map((c) => (c.automation_id || "").trim())
34
- .filter(Boolean);
35
- if (ids.length > 0) automationIds = new Set(ids);
68
+ const entries = fs.readdirSync(dir);
69
+ for (const entry of entries) {
70
+ if (entry.endsWith('.json')) {
71
+ files.push(path.join(dir, entry));
72
+ }
73
+ }
36
74
  } catch (err) {
37
- console.warn(
38
- "⚠️ No valid automation-cases.json found. Using @regression fallback."
39
- );
75
+ console.error("Failed to read directory:", dir, err.message);
40
76
  }
77
+ return files;
78
+ }
41
79
 
42
- const mochawesomeDir = path.resolve("results/mochawesome");
43
- console.log("Looking for mochawesome JSONs in:", `${mochawesomeDir}/*.json`);
44
- const jsonFiles = glob.sync(`${mochawesomeDir}/*.json`);
80
+ function parseMochawesomeJson(jsonPath) {
81
+ const results = [];
82
+ const foundIds = new Set();
83
+ let combinedResults = { results: [] };
45
84
 
46
- if (jsonFiles.length === 0) {
47
- console.error("❌ No mochawesome report files found.");
48
- process.exit(1);
49
- }
85
+ // If specific JSON path provided, use it
86
+ if (jsonPath && fs.existsSync(jsonPath)) {
87
+ try {
88
+ const data = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
89
+ if (data?.results) {
90
+ combinedResults.results.push(...data.results);
91
+ }
92
+ } catch (err) {
93
+ console.error("Failed to parse provided JSON:", err.message);
94
+ }
95
+ } else {
96
+ // Fallback: scan mochawesome directory
97
+ const mochawesomeDir = path.resolve("results/mochawesome");
98
+ console.log("Looking for mochawesome JSONs in:", `${mochawesomeDir}/*.json`);
99
+ const jsonFiles = findJsonFiles(mochawesomeDir);
100
+
101
+ if (jsonFiles.length === 0) {
102
+ console.error("No mochawesome report files found.");
103
+ return { results: [], foundIds: new Set() };
104
+ }
50
105
 
51
- console.log(`📄 Found ${jsonFiles.length} mochawesome report(s).`);
52
- const combinedResults = { results: [] };
106
+ console.log(`Found ${jsonFiles.length} mochawesome report(s).`);
53
107
 
54
- try {
55
108
  for (const file of jsonFiles) {
56
- const data = JSON.parse(fs.readFileSync(file, "utf-8"));
57
- if (data?.results) {
58
- combinedResults.results.push(...data.results);
109
+ try {
110
+ const data = JSON.parse(fs.readFileSync(file, "utf-8"));
111
+ if (data?.results) {
112
+ combinedResults.results.push(...data.results);
113
+ }
114
+ } catch (err) {
115
+ console.error("Failed to parse mochawesome JSON:", err.message);
59
116
  }
60
117
  }
61
- } catch (err) {
62
- console.error("❌ Failed to parse mochawesome JSON:", err.message);
63
- process.exit(1);
64
118
  }
65
119
 
66
- function walkSuites(suite) {
67
- collectTests(suite.tests);
68
- (suite.suites || []).forEach(walkSuites);
120
+ function walkSuites(suite, specFilePath) {
121
+ // Get spec file path from suite or use passed-in path
122
+ const currentSpecPath = suite.fullFile || suite.file || specFilePath;
123
+ collectTests(suite.tests, currentSpecPath);
124
+ (suite.suites || []).forEach(s => walkSuites(s, currentSpecPath));
69
125
  }
70
126
 
71
- function collectTests(tests) {
127
+ function collectTests(tests, specFilePath) {
128
+ // Get tag mappings from spec file if available
129
+ let specTagMap = new Map();
130
+ if (specFilePath) {
131
+ // Try multiple possible paths for the spec file
132
+ const possiblePaths = [
133
+ specFilePath,
134
+ path.resolve(specFilePath),
135
+ path.resolve('/app', specFilePath),
136
+ path.resolve(process.cwd(), specFilePath)
137
+ ];
138
+
139
+ for (const tryPath of possiblePaths) {
140
+ if (fs.existsSync(tryPath)) {
141
+ specTagMap = parseSpecFileForTags(tryPath);
142
+ break;
143
+ }
144
+ }
145
+ }
146
+
72
147
  (tests || []).forEach((test) => {
148
+ const testTitle = test.title;
73
149
  const titleCombined =
74
150
  typeof test.fullTitle === "string"
75
151
  ? test.fullTitle
@@ -77,7 +153,38 @@ try {
77
153
  ? test.title.join(" ")
78
154
  : test.title;
79
155
 
80
- const allTags = [...(titleCombined.match(/@[\w-]+/g) || [])];
156
+ // Extract automation IDs from title first
157
+ let automationIdMatch = titleCombined.match(/@?TC-AUTO-[\w.-]+/g) || [];
158
+
159
+ // If not found in title, try looking up from spec file tags
160
+ if (automationIdMatch.length === 0 && specTagMap.has(testTitle)) {
161
+ automationIdMatch = specTagMap.get(testTitle);
162
+ }
163
+
164
+ // Also check mochawesome context for automation IDs (added via addContext)
165
+ if (test.context && automationIdMatch.length === 0) {
166
+ const contextArray = Array.isArray(test.context) ? test.context : [test.context];
167
+ for (const ctx of contextArray) {
168
+ if (ctx && ctx.title === 'automationIds' && Array.isArray(ctx.value)) {
169
+ automationIdMatch = ctx.value.filter(id => id.includes('TC-AUTO'));
170
+ break;
171
+ }
172
+ // Handle string context that might contain JSON
173
+ if (typeof ctx === 'string') {
174
+ try {
175
+ const parsed = JSON.parse(ctx);
176
+ if (parsed.title === 'automationIds' && Array.isArray(parsed.value)) {
177
+ automationIdMatch = parsed.value.filter(id => id.includes('TC-AUTO'));
178
+ break;
179
+ }
180
+ } catch (e) {
181
+ // Not JSON, check if it's a direct automation ID
182
+ const match = ctx.match(/@?TC-AUTO-[\w.-]+/g);
183
+ if (match) automationIdMatch = match;
184
+ }
185
+ }
186
+ }
187
+ }
81
188
 
82
189
  const status =
83
190
  test.state === "passed" || test.pass === true
@@ -86,140 +193,110 @@ try {
86
193
  ? "skipped"
87
194
  : "fail";
88
195
 
89
- if (!automationIds) {
90
- allTags
91
- .filter((tag) => /^TC-AUTO-\w+$/i.test(tag))
92
- .forEach((tag) => {
93
- formatted.push({ automation_id: tag, status });
94
- foundIds.add(tag);
95
- });
96
- } else {
97
- allTags
98
- .filter((tag) => automationIds.has(tag))
99
- .forEach((tag) => {
100
- formatted.push({ automation_id: tag, status });
101
- foundIds.add(tag);
102
- });
103
- }
196
+ automationIdMatch.forEach((tag) => {
197
+ const normalizedTag = tag.startsWith('@') ? tag : '@' + tag;
198
+ results.push({ automation_id: normalizedTag, status });
199
+ foundIds.add(normalizedTag.replace(/^@/, ''));
200
+ });
104
201
  });
105
202
  }
106
203
 
107
- (combinedResults.results || []).forEach(walkSuites);
204
+ (combinedResults.results || []).forEach(suite => walkSuites(suite, null));
205
+ return { results, foundIds };
206
+ }
108
207
 
109
- const notFound = automationIds
110
- ? [...automationIds].filter((id) => !foundIds.has(id))
111
- : [];
208
+ async function postJson(url, payload) {
209
+ return new Promise((resolve, reject) => {
210
+ const data = JSON.stringify(payload);
211
+ const urlObj = new URL(url);
212
+ const options = {
213
+ hostname: urlObj.hostname,
214
+ port: urlObj.port || 443,
215
+ path: urlObj.pathname + urlObj.search,
216
+ method: 'POST',
217
+ headers: {
218
+ 'Content-Type': 'application/json',
219
+ 'Content-Length': Buffer.byteLength(data)
220
+ }
221
+ };
112
222
 
113
- const planId = process.env.PLAN_ID || "unknown";
114
- const runId = process.env.RUN_ID || "unknown";
115
- const testEnvironment = process.env.TEST_ENVIRONMENT || "QA";
223
+ const req = https.request(options, res => {
224
+ let body = '';
225
+ res.on('data', c => body += c);
226
+ res.on('end', () => resolve({ statusCode: res.statusCode, body }));
227
+ });
228
+ req.on('error', reject);
229
+ req.write(data);
230
+ req.end();
231
+ });
232
+ }
116
233
 
117
- if (!planId || !runId || isNaN(Number(planId)) || isNaN(Number(runId))) {
118
- console.error("❌ Invalid or missing PLAN_ID / RUN_ID");
119
- process.exit(1);
120
- }
234
+ async function main() {
235
+ const planId = process.env.PLAN_ID || 'unknown_plan';
236
+ const runId = process.env.RUN_ID || 'unknown_run';
237
+ const testEnvironment = process.env.TEST_ENVIRONMENT || 'QA';
238
+ const jsonReportPath = process.argv[2];
121
239
 
122
- await postResults(formatted, notFound, planId, runId, testEnvironment);
123
- await postSummary(formatted, notFound, planId, runId, testEnvironment);
124
- })();
240
+ const { results: formattedResults, foundIds } = parseMochawesomeJson(jsonReportPath);
125
241
 
126
- async function postResults(formattedResults, notFoundList, planId, runId, testEnvironment) {
127
- const postUrl = baseUrl; // baseUrl already includes the complete endpoint
128
- console.log(`🔗 Posting to URL: ${postUrl}`);
242
+ console.log(`Looking for results in: ${jsonReportPath || 'results/mochawesome/*.json'}`);
243
+ console.log(`Found ${formattedResults.length} result(s).`);
129
244
 
245
+ // not_found from automation-cases.json (optional)
246
+ const normalizeId = (id) => id.replace(/^@/, '');
247
+ let notFoundList = [];
248
+ try {
249
+ const automationCases = JSON.parse(fs.readFileSync(automationCasesPath, 'utf-8'));
250
+ const expected = new Set((automationCases || []).map(a => normalizeId(a.automation_id)));
251
+ notFoundList = [...expected].filter(id => !foundIds.has(id));
252
+ } catch { }
253
+
254
+ // Post results
255
+ const resultsUrl = baseUrl;
130
256
  const payload = {
131
257
  planId,
132
258
  runId,
133
259
  environment: testEnvironment,
134
260
  results: formattedResults,
135
261
  not_found: notFoundList,
136
- triggered_by: triggeredBy,
262
+ triggered_by: triggeredBy
137
263
  };
138
-
139
- console.log(
140
- "📤 Posting result from sync report:",
141
- JSON.stringify(payload, null, 2)
142
- );
264
+ console.log('Posting result payload:', JSON.stringify(payload, null, 2));
265
+ console.log(`Posting to URL: ${resultsUrl}`);
143
266
 
144
267
  try {
145
- const response = await axios.post(postUrl, payload, {
146
- headers: { "Content-Type": "application/json" },
147
- });
148
- console.log("✅ Test case results posted:", response.status);
149
- } catch (err) {
150
- console.error(
151
- "❌ Failed to post test case results:",
152
- err.response?.status,
153
- err.response?.data || err.message
154
- );
155
- process.exit(1);
268
+ const res = await postJson(resultsUrl, payload);
269
+ console.log('Post status:', res.statusCode, res.body);
270
+ } catch (e) {
271
+ console.error('Failed to post results:', e.message);
156
272
  }
157
- }
158
273
 
159
- async function postSummary(results, notFound, planId, runId, testEnvironment) {
160
- // Replace test-results with test-run-summary in the baseUrl
161
- const url = baseUrl.replace('/api/test-results', '/api/test-run-summary');
274
+ // Post summary
275
+ const summaryUrl = baseUrl.replace('/api/test-results', '/api/test-run-summary');
276
+ const passedCount = formattedResults.filter(r => r.status === 'pass').length;
277
+ const failedCount = formattedResults.filter(r => r.status === 'fail').length;
278
+ const skippedCount = formattedResults.filter(r => r.status === 'skipped').length;
279
+ const overallStatus = failedCount > 0 ? 'failed' : (formattedResults.length > 0 ? 'completed' : 'no_tests');
162
280
 
163
- const logs =
164
- (await copyLogToShared(planId, runId)) || "Log copy failed or missing";
165
-
166
- const summaryPayload = {
167
- runId,
281
+ const summary = {
168
282
  planId,
283
+ runId,
169
284
  environment: testEnvironment,
170
- status: computeOverallStatus(results, notFound),
171
- logs,
172
- result: {
173
- total: results.length,
174
- passed: results.filter((r) => r.status === "pass").length,
175
- failed: results.filter((r) => r.status === "fail").length,
176
- skipped: results.filter((r) => r.status === "skipped").length,
177
- not_found: notFound.length,
178
- },
285
+ status: overallStatus,
286
+ total: formattedResults.length,
287
+ passed: passedCount,
288
+ failed: failedCount,
289
+ skipped: skippedCount
179
290
  };
180
-
181
- console.log("📤 Posting test run summary to:", url);
182
- console.log("📦 Summary payload:", JSON.stringify(summaryPayload, null, 2));
291
+ console.log(`Posting summary to: ${summaryUrl}`);
292
+ console.log('Summary payload:', JSON.stringify(summary, null, 2));
183
293
 
184
294
  try {
185
- const resp = await axios.post(url, summaryPayload, {
186
- headers: { "Content-Type": "application/json" },
187
- });
188
- console.log("✅ Test run summary posted:", resp.data.message);
189
- } catch (err) {
190
- console.error(
191
- "❌ Failed to post summary:",
192
- err.response?.data || err.message
193
- );
295
+ const res = await postJson(summaryUrl, summary);
296
+ console.log('Summary status:', res.statusCode, res.body);
297
+ } catch (e) {
298
+ console.error('Failed to post summary:', e.message);
194
299
  }
195
300
  }
196
301
 
197
- async function copyLogToShared(planId, runId) {
198
- const logsPath = "results/logs.txt";
199
-
200
- if (!fs.existsSync(logsPath)) {
201
- console.warn("⚠️ No logs.txt file found to copy.");
202
- return null;
203
- }
204
-
205
- const sharedDir = "/shared/test-logs";
206
- if (!fs.existsSync(sharedDir)) {
207
- fs.mkdirSync(sharedDir, { recursive: true });
208
- }
209
-
210
- try {
211
- const destPath = path.join(sharedDir, "run_log.txt");
212
- fs.copyFileSync(logsPath, destPath);
213
- console.log(`📁 Logs copied to shared volume: ${destPath}`);
214
- return destPath;
215
- } catch (err) {
216
- console.error("❌ Failed to copy log to shared volume:", err.message);
217
- return null;
218
- }
219
- }
220
-
221
- function computeOverallStatus(results, notFound) {
222
- if (!results.length && !notFound.length) return "skipped";
223
- const hasFail = results.some((r) => r.status === "fail");
224
- return hasFail ? "failed" : "passed";
225
- }
302
+ main().catch(e => { console.error(e); process.exit(1); });