haven-cypress-integration 1.0.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 +85 -0
- package/bin/haven-cypress.js +61 -0
- package/index.js +146 -0
- package/package.json +26 -0
- package/templates/Dockerfile +29 -0
- package/templates/run-filtered.sh +129 -0
- package/templates/syncCypressResults.js +218 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Haven-Cypress Integration
|
|
2
|
+
|
|
3
|
+
Seamless integration between Cypress test frameworks and HAVEN test case management system.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### 1. Install
|
|
8
|
+
```bash
|
|
9
|
+
npm install haven-cypress-integration
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### 2. Add Tags to Tests
|
|
13
|
+
Add `@TC-AUTO-XXXX` tags to your test descriptions:
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
it("Login test @TC-AUTO-123, @p2", () => {
|
|
17
|
+
cy.visit('/login');
|
|
18
|
+
cy.get('[data-cy=username]').type('user');
|
|
19
|
+
cy.get('[data-cy=password]').type('pass');
|
|
20
|
+
cy.get('[data-cy=submit]').click();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("User registration @TC-AUTO-124, @p1", () => {
|
|
24
|
+
cy.visit('/register');
|
|
25
|
+
// your test code
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 3. Build Docker Image
|
|
30
|
+
```bash
|
|
31
|
+
npx haven-cypress build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 4. Deploy to Haven
|
|
35
|
+
Your Docker image is now ready to be deployed and run by HAVEN!
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
### Build Image
|
|
40
|
+
```bash
|
|
41
|
+
# Basic build
|
|
42
|
+
npx haven-cypress build
|
|
43
|
+
|
|
44
|
+
# Custom tag
|
|
45
|
+
npx haven-cypress build --tag=my-tests:v1.0
|
|
46
|
+
|
|
47
|
+
# Build and push to registry
|
|
48
|
+
npx haven-cypress build --tag=my-tests:v1.0 --push
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Run Tests
|
|
52
|
+
```bash
|
|
53
|
+
# Run all tests
|
|
54
|
+
npx haven-cypress run
|
|
55
|
+
|
|
56
|
+
# Run specific test cases
|
|
57
|
+
npx haven-cypress run --automationIds=TC-AUTO-123,TC-AUTO-124
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## What's Included
|
|
61
|
+
|
|
62
|
+
- ✅ **Tag-based test filtering** (`@TC-AUTO-XXXX`)
|
|
63
|
+
- ✅ **Mochawesome reporting** with screenshots
|
|
64
|
+
- ✅ **S3 artifact upload** (reports, logs, screenshots)
|
|
65
|
+
- ✅ **HAVEN API integration** (result synchronization)
|
|
66
|
+
- ✅ **Docker containerization** ready for HAVEN deployment
|
|
67
|
+
|
|
68
|
+
## How It Works
|
|
69
|
+
|
|
70
|
+
1. **Haven runs your container** with mounted configuration and environment variables
|
|
71
|
+
2. **Your tests run** with tag filtering based on automation IDs
|
|
72
|
+
3. **Results are generated** using Mochawesome reporting
|
|
73
|
+
4. **Artifacts are uploaded** to S3 for review
|
|
74
|
+
5. **Results sync back** to HAVEN via API integration
|
|
75
|
+
|
|
76
|
+
## Requirements
|
|
77
|
+
|
|
78
|
+
- Node.js 14+
|
|
79
|
+
- Docker
|
|
80
|
+
- Existing Cypress project
|
|
81
|
+
- HAVEN access credentials (provided by Haven when container runs)
|
|
82
|
+
|
|
83
|
+
## Support
|
|
84
|
+
|
|
85
|
+
For issues or questions, contact your HAVEN administrator.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const HavenCypressIntegration = require('../index.js');
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
const command = args[0];
|
|
7
|
+
|
|
8
|
+
if (!command) {
|
|
9
|
+
console.log(`
|
|
10
|
+
Haven-Cypress Integration
|
|
11
|
+
|
|
12
|
+
Usage: npx haven-cypress <command> [options]
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
build [options] Build Docker image with Haven integration
|
|
16
|
+
run [options] Run tests with Haven integration
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--tag=name:version Docker image tag (default: haven-cypress-tests:latest)
|
|
20
|
+
--push Push image to registry after building
|
|
21
|
+
--automationIds=ID1,ID2 Run specific test cases
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
npx haven-cypress build
|
|
25
|
+
npx haven-cypress build --tag=my-tests:v1.0 --push
|
|
26
|
+
npx haven-cypress run --automationIds=TC-AUTO-123,TC-AUTO-124
|
|
27
|
+
`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const integration = new HavenCypressIntegration();
|
|
32
|
+
|
|
33
|
+
// Parse options
|
|
34
|
+
const options = {};
|
|
35
|
+
args.slice(1).forEach(arg => {
|
|
36
|
+
if (arg.startsWith('--')) {
|
|
37
|
+
const [key, value] = arg.substring(2).split('=');
|
|
38
|
+
options[key] = value || true;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
switch (command) {
|
|
44
|
+
case 'build':
|
|
45
|
+
const tag = options.tag || 'haven-cypress-tests:latest';
|
|
46
|
+
integration.buildImage(tag, options);
|
|
47
|
+
break;
|
|
48
|
+
|
|
49
|
+
case 'run':
|
|
50
|
+
const automationIds = options.automationIds || '';
|
|
51
|
+
integration.runTests(automationIds);
|
|
52
|
+
break;
|
|
53
|
+
|
|
54
|
+
default:
|
|
55
|
+
console.error(`❌ Unknown command: ${command}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(`❌ Error: ${error.message}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
class HavenCypressIntegration {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.templatesDir = path.join(__dirname, 'templates');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build Docker image with Haven integration
|
|
12
|
+
* @param {string} tag - Docker image tag
|
|
13
|
+
* @param {Object} options - Build options
|
|
14
|
+
*/
|
|
15
|
+
buildImage(tag = 'haven-cypress-tests:latest', options = {}) {
|
|
16
|
+
console.log('🐳 Building Haven-integrated Docker image...');
|
|
17
|
+
|
|
18
|
+
// Create temporary build directory
|
|
19
|
+
const buildDir = path.join(process.cwd(), '.haven-build');
|
|
20
|
+
if (fs.existsSync(buildDir)) {
|
|
21
|
+
fs.rmSync(buildDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
fs.mkdirSync(buildDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Copy user's project files
|
|
27
|
+
console.log('📁 Copying project files...');
|
|
28
|
+
this.copyProjectFiles(buildDir);
|
|
29
|
+
|
|
30
|
+
// Copy Haven integration files
|
|
31
|
+
console.log('🔧 Adding Haven integration files...');
|
|
32
|
+
this.copyHavenFiles(buildDir);
|
|
33
|
+
|
|
34
|
+
// Build Docker image
|
|
35
|
+
console.log('🏗️ Building Docker image...');
|
|
36
|
+
execSync(`docker build -t ${tag} .`, {
|
|
37
|
+
cwd: buildDir,
|
|
38
|
+
stdio: 'inherit'
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log(`✅ Docker image built successfully: ${tag}`);
|
|
42
|
+
|
|
43
|
+
// Push if requested
|
|
44
|
+
if (options.push) {
|
|
45
|
+
console.log('📤 Pushing image to registry...');
|
|
46
|
+
execSync(`docker push ${tag}`, { stdio: 'inherit' });
|
|
47
|
+
console.log('✅ Image pushed successfully');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
} finally {
|
|
51
|
+
// Cleanup build directory
|
|
52
|
+
if (fs.existsSync(buildDir)) {
|
|
53
|
+
fs.rmSync(buildDir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Copy user's project files to build directory
|
|
60
|
+
*/
|
|
61
|
+
copyProjectFiles(buildDir) {
|
|
62
|
+
const filesToCopy = [
|
|
63
|
+
'package.json',
|
|
64
|
+
'package-lock.json',
|
|
65
|
+
'cypress.config.js',
|
|
66
|
+
'cypress'
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
filesToCopy.forEach(file => {
|
|
70
|
+
const src = path.join(process.cwd(), file);
|
|
71
|
+
const dest = path.join(buildDir, file);
|
|
72
|
+
|
|
73
|
+
if (fs.existsSync(src)) {
|
|
74
|
+
if (fs.statSync(src).isDirectory()) {
|
|
75
|
+
this.copyDir(src, dest);
|
|
76
|
+
} else {
|
|
77
|
+
fs.copyFileSync(src, dest);
|
|
78
|
+
}
|
|
79
|
+
console.log(` ✅ Copied ${file}`);
|
|
80
|
+
} else {
|
|
81
|
+
console.log(` ⚠️ Skipped ${file} (not found)`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Copy Haven integration files to build directory
|
|
88
|
+
*/
|
|
89
|
+
copyHavenFiles(buildDir) {
|
|
90
|
+
const havenFiles = [
|
|
91
|
+
'Dockerfile',
|
|
92
|
+
'run-filtered.sh',
|
|
93
|
+
'syncCypressResults.js'
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
havenFiles.forEach(file => {
|
|
97
|
+
const src = path.join(this.templatesDir, file);
|
|
98
|
+
const dest = path.join(buildDir, file);
|
|
99
|
+
|
|
100
|
+
if (fs.existsSync(src)) {
|
|
101
|
+
fs.copyFileSync(src, dest);
|
|
102
|
+
console.log(` ✅ Added ${file}`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Make run-filtered.sh executable
|
|
107
|
+
execSync('chmod +x run-filtered.sh', { cwd: buildDir });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Copy directory recursively
|
|
112
|
+
*/
|
|
113
|
+
copyDir(src, dest) {
|
|
114
|
+
if (!fs.existsSync(dest)) {
|
|
115
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
119
|
+
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
const srcPath = path.join(src, entry.name);
|
|
122
|
+
const destPath = path.join(dest, entry.name);
|
|
123
|
+
|
|
124
|
+
if (entry.isDirectory()) {
|
|
125
|
+
this.copyDir(srcPath, destPath);
|
|
126
|
+
} else {
|
|
127
|
+
fs.copyFileSync(srcPath, destPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Run tests with Haven integration
|
|
134
|
+
* This is called by Haven when the container runs
|
|
135
|
+
*/
|
|
136
|
+
runTests(automationIds = '') {
|
|
137
|
+
console.log('🧪 Running Haven-integrated Cypress tests...');
|
|
138
|
+
console.log(`🔍 Automation IDs: ${automationIds || 'All tests'}`);
|
|
139
|
+
|
|
140
|
+
// This will be handled by run-filtered.sh when container runs
|
|
141
|
+
// The library just provides the interface
|
|
142
|
+
return { success: true, message: 'Tests will run via run-filtered.sh' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = HavenCypressIntegration;
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "haven-cypress-integration",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Seamless Cypress integration with HAVEN test case management",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"haven-cypress": "./bin/haven-cypress.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"cypress",
|
|
11
|
+
"haven",
|
|
12
|
+
"testing",
|
|
13
|
+
"integration",
|
|
14
|
+
"docker"
|
|
15
|
+
],
|
|
16
|
+
"author": "BillExplainer",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"bin/",
|
|
21
|
+
"templates/"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=14.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
FROM cypress/included:14.3.1
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Copy project files
|
|
6
|
+
COPY . .
|
|
7
|
+
|
|
8
|
+
# ✅ Install zip and AWS CLI v2 via official method
|
|
9
|
+
RUN apt-get update && \
|
|
10
|
+
apt-get install -y \
|
|
11
|
+
zip \
|
|
12
|
+
unzip \
|
|
13
|
+
curl && \
|
|
14
|
+
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
|
|
15
|
+
unzip awscliv2.zip && \
|
|
16
|
+
./aws/install && \
|
|
17
|
+
rm -rf aws awscliv2.zip
|
|
18
|
+
|
|
19
|
+
# ✅ Install Node deps
|
|
20
|
+
RUN npm ci
|
|
21
|
+
RUN npm install --no-save aws-sdk
|
|
22
|
+
|
|
23
|
+
# ✅ Ensure script is executable
|
|
24
|
+
COPY run-filtered.sh /app/run-filtered.sh
|
|
25
|
+
RUN chmod +x /app/run-filtered.sh
|
|
26
|
+
|
|
27
|
+
# ✅ Entrypoint
|
|
28
|
+
ENTRYPOINT ["/app/run-filtered.sh"]
|
|
29
|
+
CMD []
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
echo "🔥 RUN SCRIPT:"
|
|
4
|
+
echo "✅ ENTERED run-filtered.sh"
|
|
5
|
+
echo "⚙️ Raw args: $@"
|
|
6
|
+
|
|
7
|
+
# Extract automation IDs from CLI args
|
|
8
|
+
AUTOMATION_IDS=""
|
|
9
|
+
for arg in "$@"; do
|
|
10
|
+
case $arg in
|
|
11
|
+
--automationIds=*)
|
|
12
|
+
AUTOMATION_IDS="${arg#*=}"
|
|
13
|
+
shift
|
|
14
|
+
;;
|
|
15
|
+
esac
|
|
16
|
+
done
|
|
17
|
+
|
|
18
|
+
echo "🔍 Extracted automation IDs: ${AUTOMATION_IDS}"
|
|
19
|
+
echo "📂 Current working directory: $(pwd)"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if [ -z "$AUTOMATION_IDS" ]; then
|
|
23
|
+
echo "🔁 No automation IDs provided. Running all tests..."
|
|
24
|
+
CYPRESS_GREP=""
|
|
25
|
+
else
|
|
26
|
+
# Strip quotes from arg, replace commas with spaces
|
|
27
|
+
RAW_IDS=$(echo "$AUTOMATION_IDS" | sed "s/^['\"]//;s/['\"]$//")
|
|
28
|
+
CLEANED_IDS="${RAW_IDS//,/ }"
|
|
29
|
+
|
|
30
|
+
echo "🚀 Running Cypress with filtered tags: ${CLEANED_IDS}"
|
|
31
|
+
CYPRESS_GREP="--env grepTags='${CLEANED_IDS}'"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Set environment variables
|
|
35
|
+
TIMESTAMP=$(date +%m%d%Y_%H%M%S)
|
|
36
|
+
PRODUCT_NAME=${PRODUCT_NAME:-unknown_product}
|
|
37
|
+
PLAN_ID=${PLAN_ID:-unknown_plan}
|
|
38
|
+
RUN_ID=${RUN_ID:-unknown_run}
|
|
39
|
+
BUCKET_NAME=${BUCKET_NAME:-your-default-bucket}
|
|
40
|
+
|
|
41
|
+
# Ensure results directory exists
|
|
42
|
+
mkdir -p results/mochawesome
|
|
43
|
+
|
|
44
|
+
echo "🧹 Cleaning mochawesome reports"
|
|
45
|
+
rm -rf results/mochawesome/*
|
|
46
|
+
|
|
47
|
+
# Run Cypress
|
|
48
|
+
echo "🚀 Running Cypress with filtered tags: ${AUTOMATION_IDS}"
|
|
49
|
+
echo "💡 Final grep arg: $CYPRESS_GREP"
|
|
50
|
+
eval "npx cypress run \
|
|
51
|
+
--headless \
|
|
52
|
+
--browser chrome \
|
|
53
|
+
$CYPRESS_GREP \
|
|
54
|
+
> results/logs.txt 2>&1"
|
|
55
|
+
|
|
56
|
+
CYPRESS_EXIT_CODE=$?
|
|
57
|
+
|
|
58
|
+
# Smart HTML report generation logic
|
|
59
|
+
MOCHAWESOME_DIR="results/mochawesome"
|
|
60
|
+
MERGED_JSON="results/results.json"
|
|
61
|
+
REPORT_HTML="${MOCHAWESOME_DIR}/report.html"
|
|
62
|
+
|
|
63
|
+
echo "🛠️ Attempting to merge mochawesome JSON files..."
|
|
64
|
+
npx mochawesome-merge ${MOCHAWESOME_DIR}/*.json > "${MERGED_JSON}"
|
|
65
|
+
|
|
66
|
+
if [ -s "${MERGED_JSON}" ]; then
|
|
67
|
+
echo "✅ Merge successful. Generating report from merged JSON."
|
|
68
|
+
npx marge "${MERGED_JSON}" --reportDir "${MOCHAWESOME_DIR}" --reportFilename report.html
|
|
69
|
+
else
|
|
70
|
+
echo "⚠️ Merge failed or empty. Falling back to first mochawesome JSON file..."
|
|
71
|
+
FIRST_JSON=$(ls ${MOCHAWESOME_DIR}/*.json 2>/dev/null | head -n 1)
|
|
72
|
+
if [ -f "${FIRST_JSON}" ]; then
|
|
73
|
+
npx marge "${FIRST_JSON}" --reportDir "${MOCHAWESOME_DIR}" --reportFilename report.html
|
|
74
|
+
else
|
|
75
|
+
echo "❌ No valid mochawesome JSON file found for fallback."
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Copy screenshots (if any)
|
|
80
|
+
SCREENSHOTS_DIR="cypress/screenshots"
|
|
81
|
+
DEST_SCREENSHOTS_DIR="${MOCHAWESOME_DIR}/screenshots"
|
|
82
|
+
if [ -d "$SCREENSHOTS_DIR" ]; then
|
|
83
|
+
echo "🖼️ Copying screenshots..."
|
|
84
|
+
mkdir -p "$DEST_SCREENSHOTS_DIR"
|
|
85
|
+
cp -r "$SCREENSHOTS_DIR"/* "$DEST_SCREENSHOTS_DIR" || echo "⚠️ No screenshots to copy"
|
|
86
|
+
else
|
|
87
|
+
echo "ℹ️ No screenshots directory found."
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Zip the mochawesome report
|
|
91
|
+
ZIP_NAME="${PRODUCT_NAME}_${TIMESTAMP}.zip"
|
|
92
|
+
ZIP_PATH="/tmp/${ZIP_NAME}"
|
|
93
|
+
|
|
94
|
+
if [ -d "${MOCHAWESOME_DIR}" ]; then
|
|
95
|
+
echo "📦 Zipping mochawesome report and screenshots..."
|
|
96
|
+
zip -r "${ZIP_PATH}" "${MOCHAWESOME_DIR}" > /dev/null
|
|
97
|
+
else
|
|
98
|
+
echo "❌ Mochawesome report folder not found: ${MOCHAWESOME_DIR}"
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# Upload zipped report to S3
|
|
102
|
+
if [ -f "${ZIP_PATH}" ]; then
|
|
103
|
+
S3_BASE_KEY="artifact/${PLAN_ID}/automation/${RUN_ID}"
|
|
104
|
+
S3_ZIP_KEY="${S3_BASE_KEY}/${ZIP_NAME}"
|
|
105
|
+
echo "☁️ Uploading ZIP to s3://${BUCKET_NAME}/${S3_ZIP_KEY}"
|
|
106
|
+
aws s3 cp "${ZIP_PATH}" "s3://${BUCKET_NAME}/${S3_ZIP_KEY}" || echo "❌ S3 ZIP upload failed"
|
|
107
|
+
else
|
|
108
|
+
echo "❌ No zip file to upload"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# Upload HTML report file to S3
|
|
112
|
+
if [ -f "${REPORT_HTML}" ]; then
|
|
113
|
+
HTML_NAME=$(basename "$REPORT_HTML")
|
|
114
|
+
S3_HTML_KEY="${S3_BASE_KEY}/${HTML_NAME}"
|
|
115
|
+
echo "🌐 Uploading HTML to s3://${BUCKET_NAME}/${S3_HTML_KEY}"
|
|
116
|
+
aws s3 cp "${REPORT_HTML}" "s3://${BUCKET_NAME}/${S3_HTML_KEY}" || echo "❌ S3 HTML upload failed"
|
|
117
|
+
else
|
|
118
|
+
echo "❌ No HTML report found to upload"
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Run result sync
|
|
122
|
+
echo "🔄 Running syncCypressResults.js"
|
|
123
|
+
PLAN_ID="${PLAN_ID}" RUN_ID="${RUN_ID}" node syncCypressResults.js >> results/logs.txt 2>&1 || echo "⚠️ syncCypressResults.js failed"
|
|
124
|
+
|
|
125
|
+
# Final output
|
|
126
|
+
echo "📤 Dumping results/logs.txt for runner capture:"
|
|
127
|
+
cat results/logs.txt || echo "❌ Failed to read results/logs.txt"
|
|
128
|
+
|
|
129
|
+
exit $CYPRESS_EXIT_CODE
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const axios = require("axios");
|
|
4
|
+
const glob = require("glob");
|
|
5
|
+
const AWS = require("aws-sdk");
|
|
6
|
+
|
|
7
|
+
// AWS setup
|
|
8
|
+
const s3 = new AWS.S3({
|
|
9
|
+
region: process.env.AWS_REGION || "us-east-1",
|
|
10
|
+
});
|
|
11
|
+
const bucketName = process.env.S3_BUCKET || "your-bucket-name";
|
|
12
|
+
|
|
13
|
+
// Paths
|
|
14
|
+
const automationCasesPath = "/shared/automation-cases.json";
|
|
15
|
+
const postBaseUrlPath = "/shared/result-post-url.txt";
|
|
16
|
+
const baseUrl = fs.readFileSync(postBaseUrlPath, "utf-8").trim();
|
|
17
|
+
|
|
18
|
+
(async () => {
|
|
19
|
+
let automationIds = null;
|
|
20
|
+
const formatted = [];
|
|
21
|
+
const foundIds = new Set();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const expectedCases = JSON.parse(
|
|
25
|
+
fs.readFileSync(automationCasesPath, "utf-8")
|
|
26
|
+
);
|
|
27
|
+
const ids = expectedCases
|
|
28
|
+
.map((c) => (c.automation_id || "").trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
if (ids.length > 0) automationIds = new Set(ids);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.warn(
|
|
33
|
+
"⚠️ No valid automation-cases.json found. Using @regression fallback."
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const mochawesomeDir = path.resolve("results/mochawesome");
|
|
38
|
+
console.log("Looking for mochawesome JSONs in:", `${mochawesomeDir}/*.json`);
|
|
39
|
+
const jsonFiles = glob.sync(`${mochawesomeDir}/*.json`);
|
|
40
|
+
|
|
41
|
+
if (jsonFiles.length === 0) {
|
|
42
|
+
console.error("❌ No mochawesome report files found.");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`📄 Found ${jsonFiles.length} mochawesome report(s).`);
|
|
47
|
+
const combinedResults = { results: [] };
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
for (const file of jsonFiles) {
|
|
51
|
+
const data = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
52
|
+
if (data?.results) {
|
|
53
|
+
combinedResults.results.push(...data.results);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error("❌ Failed to parse mochawesome JSON:", err.message);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function walkSuites(suite) {
|
|
62
|
+
collectTests(suite.tests);
|
|
63
|
+
(suite.suites || []).forEach(walkSuites);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function collectTests(tests) {
|
|
67
|
+
(tests || []).forEach((test) => {
|
|
68
|
+
const titleCombined =
|
|
69
|
+
typeof test.fullTitle === "string"
|
|
70
|
+
? test.fullTitle
|
|
71
|
+
: Array.isArray(test.title)
|
|
72
|
+
? test.title.join(" ")
|
|
73
|
+
: test.title;
|
|
74
|
+
|
|
75
|
+
const allTags = [...(titleCombined.match(/@[\w-]+/g) || [])];
|
|
76
|
+
|
|
77
|
+
const status =
|
|
78
|
+
test.state === "passed" || test.pass === true
|
|
79
|
+
? "pass"
|
|
80
|
+
: test.pending === true
|
|
81
|
+
? "skipped"
|
|
82
|
+
: "fail";
|
|
83
|
+
|
|
84
|
+
if (!automationIds) {
|
|
85
|
+
allTags
|
|
86
|
+
.filter((tag) => /^TC-AUTO-\w+$/i.test(tag))
|
|
87
|
+
.forEach((tag) => {
|
|
88
|
+
formatted.push({ automation_id: tag, status });
|
|
89
|
+
foundIds.add(tag);
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
allTags
|
|
93
|
+
.filter((tag) => automationIds.has(tag))
|
|
94
|
+
.forEach((tag) => {
|
|
95
|
+
formatted.push({ automation_id: tag, status });
|
|
96
|
+
foundIds.add(tag);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
(combinedResults.results || []).forEach(walkSuites);
|
|
103
|
+
|
|
104
|
+
const notFound = automationIds
|
|
105
|
+
? [...automationIds].filter((id) => !foundIds.has(id))
|
|
106
|
+
: [];
|
|
107
|
+
|
|
108
|
+
const planId = process.env.PLAN_ID || "unknown";
|
|
109
|
+
const runId = process.env.RUN_ID || "unknown";
|
|
110
|
+
|
|
111
|
+
if (!planId || !runId || isNaN(Number(planId)) || isNaN(Number(runId))) {
|
|
112
|
+
console.error("❌ Invalid or missing PLAN_ID / RUN_ID");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await postResults(formatted, notFound, planId, runId);
|
|
117
|
+
await postSummary(formatted, notFound, planId, runId);
|
|
118
|
+
})();
|
|
119
|
+
|
|
120
|
+
async function postResults(formattedResults, notFoundList, planId, runId) {
|
|
121
|
+
const postUrl = `${baseUrl}/api/test-results`;
|
|
122
|
+
|
|
123
|
+
const payload = {
|
|
124
|
+
planId,
|
|
125
|
+
runId,
|
|
126
|
+
results: formattedResults,
|
|
127
|
+
not_found: notFoundList,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
console.log(
|
|
131
|
+
"📤 Posting result from sync report:",
|
|
132
|
+
JSON.stringify(payload, null, 2)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const response = await axios.post(postUrl, payload, {
|
|
137
|
+
headers: { "Content-Type": "application/json" },
|
|
138
|
+
});
|
|
139
|
+
console.log("✅ Test case results posted:", response.status);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error(
|
|
142
|
+
"❌ Failed to post test case results:",
|
|
143
|
+
err.response?.status,
|
|
144
|
+
err.response?.data || err.message
|
|
145
|
+
);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function postSummary(results, notFound, planId, runId) {
|
|
151
|
+
const url = `${baseUrl}/api/test-run-summary`;
|
|
152
|
+
|
|
153
|
+
const logs =
|
|
154
|
+
(await uploadLogToS3(planId, runId)) || "Log upload failed or missing";
|
|
155
|
+
|
|
156
|
+
const summaryPayload = {
|
|
157
|
+
runId,
|
|
158
|
+
planId,
|
|
159
|
+
status: computeOverallStatus(results, notFound),
|
|
160
|
+
logs,
|
|
161
|
+
result: {
|
|
162
|
+
total: results.length,
|
|
163
|
+
passed: results.filter((r) => r.status === "pass").length,
|
|
164
|
+
failed: results.filter((r) => r.status === "fail").length,
|
|
165
|
+
skipped: results.filter((r) => r.status === "skipped").length,
|
|
166
|
+
not_found: notFound.length,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
console.log("📤 Posting test run summary to:", url);
|
|
171
|
+
console.log("📦 Summary payload:", JSON.stringify(summaryPayload, null, 2));
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const resp = await axios.post(url, summaryPayload, {
|
|
175
|
+
headers: { "Content-Type": "application/json" },
|
|
176
|
+
});
|
|
177
|
+
console.log("✅ Test run summary posted:", resp.data.message);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error(
|
|
180
|
+
"❌ Failed to post summary:",
|
|
181
|
+
err.response?.data || err.message
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function uploadLogToS3(planId, runId) {
|
|
187
|
+
const logsPath = "results/logs.txt";
|
|
188
|
+
|
|
189
|
+
if (!fs.existsSync(logsPath)) {
|
|
190
|
+
console.warn("⚠️ No logs.txt file found to upload.");
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const key = `artifact/${planId}/automation/${runId}/run_log`;
|
|
195
|
+
const fileStream = fs.createReadStream(logsPath);
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
await s3
|
|
199
|
+
.upload({
|
|
200
|
+
Bucket: bucketName,
|
|
201
|
+
Key: key,
|
|
202
|
+
Body: fileStream,
|
|
203
|
+
ContentType: "text/plain",
|
|
204
|
+
})
|
|
205
|
+
.promise();
|
|
206
|
+
|
|
207
|
+
return `s3://${bucketName}/${key}`;
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.error("❌ Failed to upload log to S3:", err.message);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function computeOverallStatus(results, notFound) {
|
|
215
|
+
if (!results.length && !notFound.length) return "skipped";
|
|
216
|
+
const hasFail = results.some((r) => r.status === "fail");
|
|
217
|
+
return hasFail ? "failed" : "passed";
|
|
218
|
+
}
|