froth-webdriverio-framework 7.0.119-dev1.6 → 7.0.119-dev1.8
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 +245 -0
- package/allure-report-utils/allure-helper.js +226 -0
- package/allure-report-utils/generate-allure-report.js +167 -0
- package/allure-report-utils/open-allure-report.js +97 -0
- package/froth_configs/base.config.js +47 -3
- package/froth_configs/commonhook.js +53 -0
- package/froth_configs/local/mobile.config.js +49 -9
- package/froth_configs/local/web.config.js +92 -54
- package/froth_configs/wdio.common.conf.js +112 -1
- package/package.json +14 -3
- package/web.conf.js +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# README.md
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
**Version:** 9.0.5-ytlc
|
|
6
|
+
|
|
7
|
+
**Repository:** https://github.com/RoboticoDigitalProjects/froth-webdriverio.git
|
|
8
|
+
|
|
9
|
+
This is a WebdriverIO test automation framework (FROTH - "froth-webdriverio-framework") that supports web, Android, and iOS testing on both BrowserStack and local environments. The framework integrates with a Froth TestOps backend API for execution tracking, reporting, and test data management.
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
### Running Tests
|
|
14
|
+
|
|
15
|
+
Tests are run using the WebdriverIO CLI. The framework uses `wdio.common.conf.js` directly without requiring platform-specific config files.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Lint code
|
|
19
|
+
npm run lint
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Viewing Test Results
|
|
23
|
+
|
|
24
|
+
#### Local Execution Reports (Automatic)
|
|
25
|
+
|
|
26
|
+
When running tests with `PLATFORM=local`, the framework **automatically generates Mochawesome HTML reports** after test execution completes:
|
|
27
|
+
|
|
28
|
+
- Reports are saved in: `./reports/mochawesome/`
|
|
29
|
+
- Each execution gets a **unique report directory** using `BROWSERSTACK_BUILD_NAME` and timestamp
|
|
30
|
+
- Format: `./reports/mochawesome/{BUILD_NAME}-{YYYY-MM-DD}/mochawesome.html`
|
|
31
|
+
- Reports **auto-open in browser** after generation
|
|
32
|
+
|
|
33
|
+
**Example:**
|
|
34
|
+
```bash
|
|
35
|
+
# Run tests with unique build name
|
|
36
|
+
BROWSERSTACK_BUILD_NAME="Web_Login_Test_20250619" \
|
|
37
|
+
PLATFORM=local \
|
|
38
|
+
YML_NAME="./ymls/browserstack/web/TOASTER_CHECK.yml" \
|
|
39
|
+
SUITE="./web_suites/Login_RD.js" \
|
|
40
|
+
npx wdio ./froth_configs/wdio.common.conf.js
|
|
41
|
+
|
|
42
|
+
# ✅ Report generates automatically after execution completes!
|
|
43
|
+
# 📍 Location: ./reports/mochawesome/Web_Login_Test_20250619-2025-06-19/mochawesome.html
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Manual report generation (if needed):**
|
|
47
|
+
```bash
|
|
48
|
+
# Generate latest report manually
|
|
49
|
+
npm run generate:report
|
|
50
|
+
|
|
51
|
+
# Generate all available reports
|
|
52
|
+
npm run generate:report:all
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### BrowserStack Execution Reports
|
|
56
|
+
|
|
57
|
+
For BrowserStack execution, test results are tracked via the Froth TestOps API. The framework automatically:
|
|
58
|
+
- Updates execution status for each test script
|
|
59
|
+
- Calculates total execution time
|
|
60
|
+
- Posts results back to TestOps via `updateScriptExecutionStatus` and `updateExecuitonDetails`
|
|
61
|
+
- Tracks BrowserStack session information
|
|
62
|
+
|
|
63
|
+
View detailed results and execution logs in your Froth TestOps dashboard using the `EXECUTION_ID`.
|
|
64
|
+
|
|
65
|
+
### Running Tests
|
|
66
|
+
|
|
67
|
+
Tests are run by specifying environment variables and the config file:
|
|
68
|
+
|
|
69
|
+
**Local execution (using BrowserStack from local machine):**
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
EXECUTION_ID=12995 \
|
|
73
|
+
ORGANISATION_DOMAIN_URL='https://api.frothtestops.com' \
|
|
74
|
+
BROWSERSTACK_BUILD_NAME="Web_Build- api-module-testing_2345_20260619" \
|
|
75
|
+
PLATFORM=local \
|
|
76
|
+
YML_NAME="./ymls/browserstack/web/TOASTER_CHECK.yml" \
|
|
77
|
+
SUITE="./web_suites/Login_RD.js" \
|
|
78
|
+
API_TOKEN="" \
|
|
79
|
+
CICD_RUN_ID=1 \
|
|
80
|
+
npx wdio ./froth_configs/wdio.common.conf.js
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**BrowserStack execution:**
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
SUITE=./android_suites/samplesuite_x.js \
|
|
87
|
+
YML_NAME=./ymls/browserstack/android/android_pixel8.yml \
|
|
88
|
+
PLATFORM=browserstack \
|
|
89
|
+
EXECUTION_ID=123 \
|
|
90
|
+
API_TOKEN=your_token \
|
|
91
|
+
ORGANISATION_DOMAIN_URL=https://api.frothtestops.com \
|
|
92
|
+
npx wdio ./froth_configs/wdio.common.conf.js
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Architecture
|
|
96
|
+
|
|
97
|
+
### Configuration Hierarchy
|
|
98
|
+
|
|
99
|
+
The framework uses a multi-layered configuration system:
|
|
100
|
+
|
|
101
|
+
1. __Base Configuration__ (`froth_configs/base.config.js`): Contains framework settings, specs path, timeouts, and Mocha options. Initializes a global `BUFFER` using `node-localstorage` for cross-module state sharing.
|
|
102
|
+
2. __Common Configuration__ (`froth_configs/wdio.common.conf.js`): Acts as the entry point. It:
|
|
103
|
+
|
|
104
|
+
- Loads capabilities from a YAML file specified by `YML_NAME`
|
|
105
|
+
- Decodes BrowserStack credentials from base64
|
|
106
|
+
- Determines platform (web vs mobile) based on `platformName` in capabilities
|
|
107
|
+
- Merges base config with platform-specific config
|
|
108
|
+
|
|
109
|
+
3. __Platform-Specific Configs__ (`froth_configs/browserstack/` and `froth_configs/local/`):
|
|
110
|
+
|
|
111
|
+
- `web.config.js`: BrowserStack web testing configuration
|
|
112
|
+
- `mobile.config.js`: BrowserStack mobile (Android/iOS) testing configuration
|
|
113
|
+
- Supports `BS_UPLOAD_MEDIA` for injecting media files into tests
|
|
114
|
+
|
|
115
|
+
### Test Lifecycle and Hooks
|
|
116
|
+
|
|
117
|
+
All test lifecycle hooks are defined in `froth_configs/commonhook.js`:
|
|
118
|
+
|
|
119
|
+
- **onPrepare**: Initializes environment variables, execution details, suite details, and test data via `setallDatailinBuffer.js`. Registers global error handlers.
|
|
120
|
+
- **beforeSession**: Validates test syntax, configures BrowserStack capabilities, sets app path for mobile testing
|
|
121
|
+
- **beforeSuite**: Fetches BrowserStack session details, updates CICD run ID
|
|
122
|
+
- **beforeTest/afterTest**: Updates individual script execution status via API
|
|
123
|
+
- **afterSession**: Calculates total execution time, updates final execution status
|
|
124
|
+
- **onComplete**: Clears the BUFFER storage
|
|
125
|
+
|
|
126
|
+
### Directory Structure
|
|
127
|
+
|
|
128
|
+
```ini
|
|
129
|
+
froth_common_actions/ # Reusable test utilities and actions
|
|
130
|
+
├── Utils.js # Central export point for all utilities
|
|
131
|
+
├── scroll.js # Scrolling helpers (scrollToEnd, scrollDownToView, etc.)
|
|
132
|
+
├── swipe.js # Gesture helpers (swipeUp, swipeDown, swipeWithCoordinates)
|
|
133
|
+
├── click.js # Click helpers (clickIfVisible, doubleClick)
|
|
134
|
+
├── assert.js # Assertion helpers (assertText, assertAttributeValue)
|
|
135
|
+
├── random.js # Random data generators (RANDOMTEXT, RNDNUMBER, etc.)
|
|
136
|
+
├── storeToBuffer.js # Buffer storage helpers for runtime data
|
|
137
|
+
├── dbValidator.js # Database field validation
|
|
138
|
+
└── ... # Other common actions
|
|
139
|
+
|
|
140
|
+
froth_api_calls/ # API integration layer
|
|
141
|
+
├── loginapi.js # Authentication token retrieval
|
|
142
|
+
├── getexecutionDetails.js # Fetch/update execution details via TestOps API
|
|
143
|
+
├── getsuiteDetails.js # Fetch test suite and script mappings
|
|
144
|
+
├── readTestdata.js # Fetch test data by ID
|
|
145
|
+
└── browsersatckSessionInfo.js # BrowserStack session management
|
|
146
|
+
|
|
147
|
+
froth_configs/ # Configuration files
|
|
148
|
+
├── base.config.js
|
|
149
|
+
├── wdio.common.conf.js
|
|
150
|
+
├── commonhook.js
|
|
151
|
+
├── setallDatailinBuffer.js
|
|
152
|
+
├── browserstack/ # BrowserStack-specific configs
|
|
153
|
+
└── local/ # Local execution configs
|
|
154
|
+
|
|
155
|
+
android/ # Android test scripts
|
|
156
|
+
android_suites/ # Android suite definitions (array of test files)
|
|
157
|
+
web/ # Web test scripts
|
|
158
|
+
web_suites/ # Web suite definitions
|
|
159
|
+
ymls/browserstack/ # Capability YAML files for different devices/browsers
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Test Data Flow
|
|
163
|
+
|
|
164
|
+
1. Environment variables are loaded in `setallDatailinBuffer.js` during `onPrepare`
|
|
165
|
+
2. Global state is stored in `BUFFER` (node-localstorage at `./buffer_storage`)
|
|
166
|
+
3. Execution details are fetched from the Froth TestOps API
|
|
167
|
+
4. Test scripts access data via `BUFFER.getItem()` / `BUFFER.setItem()`
|
|
168
|
+
5. Results are posted back to the API via `updateScriptExecutionStatus` and `updateExecuitonDetails`
|
|
169
|
+
|
|
170
|
+
### Test Suite Pattern
|
|
171
|
+
|
|
172
|
+
Suites are defined as simple modules exporting a `tests` array:
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
// Example: android_suites/samplesuite_x.js
|
|
176
|
+
module.exports = {
|
|
177
|
+
tests: [
|
|
178
|
+
'/absolute/path/to/android/test_script.js'
|
|
179
|
+
]
|
|
180
|
+
};
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Common Actions Usage
|
|
184
|
+
|
|
185
|
+
Tests import utilities from the Utils export:
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
const Util = require('../froth_common_actions/Utils');
|
|
189
|
+
|
|
190
|
+
// Scrolling
|
|
191
|
+
await Util.scrollToEnd(2, 3);
|
|
192
|
+
await Util.scrollDownToView("Some Text");
|
|
193
|
+
|
|
194
|
+
// Swiping
|
|
195
|
+
await Util.swipeWithCoordinates(x1, y1, x2, y2);
|
|
196
|
+
|
|
197
|
+
// Random data
|
|
198
|
+
const randomText = Util.randomtext(10);
|
|
199
|
+
const randomNumber = Util.randomnumber(100, 999);
|
|
200
|
+
|
|
201
|
+
// Buffer storage
|
|
202
|
+
Util.storetext('key', 'value');
|
|
203
|
+
const value = BUFFER.getItem('key');
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Required Environment Variables
|
|
207
|
+
|
|
208
|
+
| Variable | Description | Example |
|
|
209
|
+
|----------|-------------|---------|
|
|
210
|
+
| `YML_NAME` | Path to capability YAML file | `./ymls/browserstack/android/android_pixel8.yml` |
|
|
211
|
+
| `SUITE` | Path to suite file | `./android_suites/samplesuite_x.js` |
|
|
212
|
+
| `PLATFORM` | `local` for local execution, `browserstack` or `browserstacklocal` for BrowserStack | `local` or `browserstack` |
|
|
213
|
+
| `EXECUTION_ID` | Test execution ID from TestOps | `12995` |
|
|
214
|
+
| `API_TOKEN` | Authentication token for TestOps API | `eyJhbG...` |
|
|
215
|
+
| `ORGANISATION_DOMAIN_URL` | Froth TestOps API base URL | `https://api.frothtestops.com` |
|
|
216
|
+
| `CICD_RUN_ID` | CI/CD pipeline run ID (optional) | `1` |
|
|
217
|
+
| `BROWSERSTACK_BUILD_NAME` | Build name for BrowserStack session tracking | `Web_Build- api-module-testing_2345_20260619` |
|
|
218
|
+
| `BS_UPLOAD_MEDIA` | Comma-separated media URLs for BrowserStack (optional) | `url1,url2` |
|
|
219
|
+
|
|
220
|
+
## Capability YAML Format
|
|
221
|
+
|
|
222
|
+
YAML files define device/browser capabilities. Example for Android:
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
userName: "bs_username"
|
|
226
|
+
accessKey: "base64_encoded_key"
|
|
227
|
+
platformName: "Android"
|
|
228
|
+
deviceName: "Samsung Galaxy S22 Ultra"
|
|
229
|
+
platformVersion: "12.0"
|
|
230
|
+
debug: true
|
|
231
|
+
networkLogs: true
|
|
232
|
+
interactiveDebugging: false
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Important Notes
|
|
236
|
+
|
|
237
|
+
- The framework uses `node-localstorage` to create a global `BUFFER` accessible across all modules
|
|
238
|
+
- All API responses from TestOps are AES-decrypted via `aesEncryptionDecryption.js`
|
|
239
|
+
- Test scripts map to TestOps via filename (script name must match `scriptName` in suite details)
|
|
240
|
+
- Mobile tests use Appium selectors like `id:com.example:id/elementId` and `-android uiautomator:...`
|
|
241
|
+
- Web tests use standard WebdriverIO selectors (`$`, `$$`)
|
|
242
|
+
- Framework supports both BrowserStack App Automate (mobile) and Automate (web) sessions
|
|
243
|
+
- **PLATFORM variable:**
|
|
244
|
+
- Use `PLATFORM=local` when executing tests from your local machine (even when targeting BrowserStack devices)
|
|
245
|
+
- Use `PLATFORM=browserstack` or `PLATFORM=browserstacklocal` when running from BrowserStack infrastructure
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Allure Helper Utility for WebDriverIO
|
|
6
|
+
* Provides methods to add steps, screenshots, and attachments to Allure reports
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class AllureHelper {
|
|
10
|
+
/**
|
|
11
|
+
* Add a step to the Allure report
|
|
12
|
+
* @param {string} name - Step name
|
|
13
|
+
* @param {Function} stepFn - Step function to execute
|
|
14
|
+
*/
|
|
15
|
+
static async step(name, stepFn) {
|
|
16
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
17
|
+
|
|
18
|
+
console.log(`📝 Step: ${name}`);
|
|
19
|
+
|
|
20
|
+
// Create a step in Allure
|
|
21
|
+
allure.startStep(name);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Execute the step function
|
|
25
|
+
await stepFn();
|
|
26
|
+
allure.endStep('passed');
|
|
27
|
+
} catch (error) {
|
|
28
|
+
allure.endStep('failed');
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Add a description step
|
|
35
|
+
* @param {string} text - Description text
|
|
36
|
+
*/
|
|
37
|
+
static description(text) {
|
|
38
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
39
|
+
console.log(`📄 ${text}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Capture and attach screenshot to Allure report
|
|
44
|
+
* @param {string} screenshotName - Name for the screenshot
|
|
45
|
+
*/
|
|
46
|
+
static async captureScreenshot(screenshotName = 'screenshot') {
|
|
47
|
+
try {
|
|
48
|
+
// Ensure screenshot directory exists
|
|
49
|
+
const screenshotDir = './allure-screenshots';
|
|
50
|
+
if (!fs.existsSync(screenshotDir)) {
|
|
51
|
+
fs.mkdirSync(screenshotDir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Take screenshot and save to file
|
|
55
|
+
const screenshotPath = path.join(screenshotDir, `${screenshotName}.png`);
|
|
56
|
+
await browser.saveScreenshot(screenshotPath);
|
|
57
|
+
|
|
58
|
+
// Read the screenshot
|
|
59
|
+
if (fs.existsSync(screenshotPath)) {
|
|
60
|
+
const imageBuffer = fs.readFileSync(screenshotPath);
|
|
61
|
+
|
|
62
|
+
// Attach to Allure using the proper method
|
|
63
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
64
|
+
allure.addAttachment(screenshotName, imageBuffer, 'image/png');
|
|
65
|
+
|
|
66
|
+
console.log(`📸 Screenshot captured: ${screenshotName}`);
|
|
67
|
+
} else {
|
|
68
|
+
console.warn(`⚠️ Screenshot file not found: ${screenshotPath}`);
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(`❌ Error capturing screenshot: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Add text attachment to Allure report
|
|
77
|
+
* @param {string} name - Attachment name
|
|
78
|
+
* @param {string} text - Text content
|
|
79
|
+
* @param {string} mimeType - MIME type (default: text/plain)
|
|
80
|
+
*/
|
|
81
|
+
static addAttachment(name, text, mimeType = 'text/plain') {
|
|
82
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
83
|
+
allure.addAttachment(name, text, mimeType);
|
|
84
|
+
console.log(`📎 Attachment added: ${name}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Add severity level to test
|
|
89
|
+
* @param {string} level - Severity level (blocker, critical, normal, minor, trivial)
|
|
90
|
+
*/
|
|
91
|
+
static severity(level = 'normal') {
|
|
92
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
93
|
+
allure.addArgument('severity', level);
|
|
94
|
+
console.log(`🔖 Severity: ${level}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Add epic label
|
|
99
|
+
* @param {string} epic - Epic name
|
|
100
|
+
*/
|
|
101
|
+
static epic(epic) {
|
|
102
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
103
|
+
allure.addArgument('epic', epic);
|
|
104
|
+
console.log(`📚 Epic: ${epic}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Add feature label
|
|
109
|
+
* @param {string} feature - Feature name
|
|
110
|
+
*/
|
|
111
|
+
static feature(feature) {
|
|
112
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
113
|
+
allure.addArgument('feature', feature);
|
|
114
|
+
console.log(`⭐ Feature: ${feature}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Add story label
|
|
119
|
+
* @param {string} story - Story name
|
|
120
|
+
*/
|
|
121
|
+
static story(story) {
|
|
122
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
123
|
+
allure.addArgument('story', story);
|
|
124
|
+
console.log(`📖 Story: ${story}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Add test ID/TC tag
|
|
129
|
+
* @param {string} testId - Test ID
|
|
130
|
+
*/
|
|
131
|
+
static testId(testId) {
|
|
132
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
133
|
+
allure.addArgument('testId', testId);
|
|
134
|
+
console.log(`🏷️ Test ID: ${testId}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Capture screenshot on test failure
|
|
139
|
+
*/
|
|
140
|
+
static async captureOnFailure() {
|
|
141
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const testName = expect.getState().currentTestName || 'test';
|
|
145
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
146
|
+
const screenshotName = `${testName}-failure-${timestamp}`;
|
|
147
|
+
|
|
148
|
+
await this.captureScreenshot(screenshotName);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(`❌ Error capturing failure screenshot: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Add environment information to report
|
|
156
|
+
* @param {object} envInfo - Environment information object
|
|
157
|
+
*/
|
|
158
|
+
static addEnvironment(envInfo) {
|
|
159
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
160
|
+
|
|
161
|
+
Object.entries(envInfo).forEach(([key, value]) => {
|
|
162
|
+
allure.addArgument(`env:${key}`, value);
|
|
163
|
+
console.log(`🌍 Environment: ${key} = ${value}`);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Log text as a step
|
|
169
|
+
* @param {string} message - Log message
|
|
170
|
+
*/
|
|
171
|
+
static log(message) {
|
|
172
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
173
|
+
allure.addStep(message);
|
|
174
|
+
console.log(`📝 ${message}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Take a screenshot and attach it as an attachment (simpler method)
|
|
179
|
+
* @param {string} name - Attachment name
|
|
180
|
+
*/
|
|
181
|
+
static async attachScreenshot(name = 'screenshot') {
|
|
182
|
+
try {
|
|
183
|
+
// Take screenshot to buffer
|
|
184
|
+
const screenshot = await browser.takeScreenshot();
|
|
185
|
+
|
|
186
|
+
// Attach directly to Allure
|
|
187
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
188
|
+
const buffer = Buffer.from(screenshot, 'base64');
|
|
189
|
+
allure.addAttachment(name, buffer, 'image/png');
|
|
190
|
+
|
|
191
|
+
console.log(`📸 Screenshot attached: ${name}`);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(`❌ Error attaching screenshot: ${error.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Add screenshot attachment with detailed logging
|
|
199
|
+
* @param {string} stepName - Name of the current step
|
|
200
|
+
* @param {string} description - Description of what the screenshot shows
|
|
201
|
+
*/
|
|
202
|
+
static async screenshot(stepName, description = 'Screenshot') {
|
|
203
|
+
try {
|
|
204
|
+
const allure = require('@wdio/allure-reporter').default;
|
|
205
|
+
|
|
206
|
+
// Start step if not already in one
|
|
207
|
+
allure.startStep(description);
|
|
208
|
+
|
|
209
|
+
// Take screenshot
|
|
210
|
+
const screenshot = await browser.takeScreenshot();
|
|
211
|
+
const buffer = Buffer.from(screenshot, 'base64');
|
|
212
|
+
|
|
213
|
+
// Attach screenshot to the step
|
|
214
|
+
allure.addAttachment(description, buffer, 'image/png');
|
|
215
|
+
|
|
216
|
+
// End the step
|
|
217
|
+
allure.endStep('passed');
|
|
218
|
+
|
|
219
|
+
console.log(`📸 Screenshot attached for step: ${stepName}`);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error(`❌ Error with screenshot: ${error.message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = AllureHelper;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate Allure HTML report automatically after test execution
|
|
7
|
+
* Uses BROWSERSTACK_BUILD_NAME environment variable for report naming
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const REPORTS_DIR = './reports/allure';
|
|
11
|
+
const OUTPUT_DIR = './allure-report';
|
|
12
|
+
|
|
13
|
+
function findLatestReportDir() {
|
|
14
|
+
if (!fs.existsSync(REPORTS_DIR)) {
|
|
15
|
+
console.log('❌ No Allure reports directory found.');
|
|
16
|
+
console.log('💡 Reports are generated in: ./reports/allure/');
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const directories = fs.readdirSync(REPORTS_DIR)
|
|
21
|
+
.map(file => path.join(REPORTS_DIR, file))
|
|
22
|
+
.filter(file => {
|
|
23
|
+
const stat = fs.statSync(file);
|
|
24
|
+
return stat.isDirectory();
|
|
25
|
+
})
|
|
26
|
+
.sort((a, b) => {
|
|
27
|
+
// Sort by modification time, newest first
|
|
28
|
+
return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return directories.length > 0 ? directories[0] : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function generateAllureReport(reportDir) {
|
|
35
|
+
try {
|
|
36
|
+
console.log('🎯 Allure Report Generator');
|
|
37
|
+
console.log('================================\n');
|
|
38
|
+
console.log(`📁 Using results from: ${reportDir}`);
|
|
39
|
+
console.log(`🏷️ Build Name: ${process.env.BROWSERSTACK_BUILD_NAME || 'local-build'}\n`);
|
|
40
|
+
|
|
41
|
+
// Get directory name and extract build name (remove timestamp)
|
|
42
|
+
const dirName = path.basename(reportDir);
|
|
43
|
+
const timestampIndex = dirName.lastIndexOf('-');
|
|
44
|
+
const buildName = timestampIndex > 0 ? dirName.substring(0, timestampIndex) : dirName;
|
|
45
|
+
|
|
46
|
+
// Generate HTML report folder named after build name
|
|
47
|
+
const reportFolderName = buildName + '_report';
|
|
48
|
+
const outputDir = path.join(reportDir, reportFolderName);
|
|
49
|
+
|
|
50
|
+
// Clean existing report directory if it exists
|
|
51
|
+
if (fs.existsSync(outputDir)) {
|
|
52
|
+
console.log('🧹 Cleaning existing report directory...');
|
|
53
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Generate Allure HTML report
|
|
57
|
+
console.log('📊 Generating Allure HTML report...');
|
|
58
|
+
console.log(`📂 Report folder: ${reportFolderName}\n`);
|
|
59
|
+
const historyPath = path.join(reportDir, 'history');
|
|
60
|
+
|
|
61
|
+
let command = `npx allure generate ${reportDir} -o ${outputDir} --clean`;
|
|
62
|
+
|
|
63
|
+
// Add history if available
|
|
64
|
+
if (fs.existsSync(historyPath)) {
|
|
65
|
+
command = `npx allure generate ${reportDir} -o ${outputDir} --clean --history ${historyPath}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
execSync(command, { stdio: 'inherit' });
|
|
69
|
+
|
|
70
|
+
// Create a simple server start script in the report folder
|
|
71
|
+
const startScriptPath = path.join(outputDir, 'start-server.sh');
|
|
72
|
+
const startScriptContent = `#!/bin/bash
|
|
73
|
+
echo "🌐 Starting Allure Report Server..."
|
|
74
|
+
echo "📁 Report: ${outputDir}"
|
|
75
|
+
echo "🔗 Open: http://localhost:8080"
|
|
76
|
+
echo ""
|
|
77
|
+
echo "Press Ctrl+C to stop the server"
|
|
78
|
+
echo ""
|
|
79
|
+
npx allure open "${outputDir}" --port 8080
|
|
80
|
+
`;
|
|
81
|
+
fs.writeFileSync(startScriptPath, startScriptContent, { mode: 0o755 });
|
|
82
|
+
|
|
83
|
+
// Create a Windows batch file
|
|
84
|
+
const batchScriptPath = path.join(outputDir, 'start-server.bat');
|
|
85
|
+
const batchScriptContent = `@echo off
|
|
86
|
+
echo 🌐 Starting Allure Report Server...
|
|
87
|
+
echo 📁 Report: ${outputDir}
|
|
88
|
+
echo 🔗 Open: http://localhost:8080
|
|
89
|
+
echo.
|
|
90
|
+
echo Press Ctrl+C to stop the server
|
|
91
|
+
echo.
|
|
92
|
+
npx allure open "${outputDir}" --port 8080
|
|
93
|
+
`;
|
|
94
|
+
fs.writeFileSync(batchScriptPath, batchScriptContent);
|
|
95
|
+
|
|
96
|
+
// Create a README
|
|
97
|
+
const readmePath = path.join(outputDir, 'README.md');
|
|
98
|
+
const readmeContent = `# Allure Test Report
|
|
99
|
+
|
|
100
|
+
## 🌐 How to View This Report
|
|
101
|
+
|
|
102
|
+
### Option 1: Double-click the start script (Easiest)
|
|
103
|
+
- **Mac/Linux:** Double-click \`start-server.sh\`
|
|
104
|
+
- **Windows:** Double-click \`start-server.bat\`
|
|
105
|
+
- Report will open at: http://localhost:8080
|
|
106
|
+
|
|
107
|
+
### Option 2: Use npm script
|
|
108
|
+
From the project root, run:
|
|
109
|
+
\`\`\`bash
|
|
110
|
+
npm run report:open
|
|
111
|
+
\`\`\`
|
|
112
|
+
|
|
113
|
+
### Option 3: Manual command
|
|
114
|
+
\`\`\`bash
|
|
115
|
+
npx allure open "${outputDir}" --port 8080
|
|
116
|
+
\`\`\`
|
|
117
|
+
|
|
118
|
+
## ❗ Why can't I just open index.html?
|
|
119
|
+
|
|
120
|
+
Allure reports use JavaScript to dynamically load test data from JSON files.
|
|
121
|
+
When you open index.html directly (file:// protocol), browsers block this for security reasons (CORS).
|
|
122
|
+
|
|
123
|
+
You need a web server to view the report properly - that's what the start scripts do!
|
|
124
|
+
|
|
125
|
+
## 📊 Test Results
|
|
126
|
+
|
|
127
|
+
- **Build Name:** ${process.env.BROWSERSTACK_BUILD_NAME || 'local-build'}
|
|
128
|
+
- **Generated:** ${new Date().toISOString()}
|
|
129
|
+
- **Report Location:** ${outputDir}
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
fs.writeFileSync(readmePath, readmeContent);
|
|
133
|
+
|
|
134
|
+
console.log('\n✅ Allure report generated successfully!');
|
|
135
|
+
console.log(`📍 Location: ${path.resolve(outputDir)}`);
|
|
136
|
+
console.log(`📂 Results directory: ${path.resolve(reportDir)}`);
|
|
137
|
+
console.log('\n🌐 To view the report:');
|
|
138
|
+
console.log(' 1. Double-click: start-server.sh (Mac/Linux) or start-server.bat (Windows)');
|
|
139
|
+
console.log(' 2. Or run: npm run report:open');
|
|
140
|
+
console.log(' 3. Report opens at: http://localhost:8080');
|
|
141
|
+
console.log('\n💡 Press Ctrl+C to stop the server when done viewing.');
|
|
142
|
+
|
|
143
|
+
return true;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(`❌ Error generating report: ${error.message}`);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function main() {
|
|
151
|
+
const reportDir = findLatestReportDir();
|
|
152
|
+
|
|
153
|
+
if (!reportDir) {
|
|
154
|
+
console.log('❌ No Allure results found. Please run tests first.');
|
|
155
|
+
console.log('💡 Results are saved in: ./reports/allure/');
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
generateAllureReport(reportDir);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Run the script
|
|
163
|
+
if (require.main === module) {
|
|
164
|
+
main();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { generateAllureReport, findLatestReportDir };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Find and open the latest Allure HTML report
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const REPORTS_DIR = './reports/allure';
|
|
10
|
+
|
|
11
|
+
function findLatestReport() {
|
|
12
|
+
if (!fs.existsSync(REPORTS_DIR)) {
|
|
13
|
+
console.log('❌ No Allure reports directory found.');
|
|
14
|
+
console.log('💡 Reports are generated in: ./reports/allure/');
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const directories = fs.readdirSync(REPORTS_DIR)
|
|
19
|
+
.map(file => path.join(REPORTS_DIR, file))
|
|
20
|
+
.filter(file => fs.statSync(file).isDirectory())
|
|
21
|
+
.filter(file => {
|
|
22
|
+
// Check if any subdirectory ending with _report exists
|
|
23
|
+
const subdirs = fs.readdirSync(file)
|
|
24
|
+
.map(sub => path.join(file, sub))
|
|
25
|
+
.filter(sub => {
|
|
26
|
+
const stat = fs.statSync(sub);
|
|
27
|
+
return stat.isDirectory() && path.basename(sub).endsWith('_report');
|
|
28
|
+
});
|
|
29
|
+
return subdirs.length > 0;
|
|
30
|
+
})
|
|
31
|
+
.sort((a, b) => {
|
|
32
|
+
return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return directories.length > 0 ? directories[0] : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function openReport(reportDir) {
|
|
39
|
+
// Find the report folder ending with _report
|
|
40
|
+
const reportSubdirs = fs.readdirSync(reportDir)
|
|
41
|
+
.map(sub => path.join(reportDir, sub))
|
|
42
|
+
.filter(sub => {
|
|
43
|
+
const stat = fs.statSync(sub);
|
|
44
|
+
return stat.isDirectory() && path.basename(sub).endsWith('_report');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (reportSubdirs.length === 0) {
|
|
48
|
+
console.log('❌ No HTML report found in:', reportDir);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const htmlReportPath = reportSubdirs[0];
|
|
53
|
+
const indexPath = path.join(htmlReportPath, 'index.html');
|
|
54
|
+
|
|
55
|
+
if (!fs.existsSync(indexPath)) {
|
|
56
|
+
console.log('❌ No index.html found at:', indexPath);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
console.log('🎯 Opening Allure Report');
|
|
62
|
+
console.log('========================\n');
|
|
63
|
+
console.log(`📁 Report location: ${htmlReportPath}`);
|
|
64
|
+
console.log(`🏷️ Build: ${path.basename(reportDir)}\n`);
|
|
65
|
+
|
|
66
|
+
// Use Allure CLI to open the report
|
|
67
|
+
console.log('🌐 Opening report in browser on http://localhost:8080');
|
|
68
|
+
console.log('💡 Press Ctrl+C to stop the server\n');
|
|
69
|
+
|
|
70
|
+
execSync(`npx allure open ${htmlReportPath} --port 8080`, { stdio: 'inherit' });
|
|
71
|
+
|
|
72
|
+
return true;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error(`❌ Error opening report: ${error.message}`);
|
|
75
|
+
console.log(`\n💡 To view manually, open: file://${path.resolve(indexPath)}`);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function main() {
|
|
81
|
+
const reportDir = findLatestReport();
|
|
82
|
+
|
|
83
|
+
if (!reportDir) {
|
|
84
|
+
console.log('❌ No Allure HTML reports found. Please run tests first.');
|
|
85
|
+
console.log('💡 Reports are generated in: ./reports/allure/');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
openReport(reportDir);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Run the script
|
|
93
|
+
if (require.main === module) {
|
|
94
|
+
main();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { findLatestReport, openReport };
|
|
@@ -11,13 +11,11 @@ module.exports = {
|
|
|
11
11
|
specs: require(SUITE_FILE).tests,
|
|
12
12
|
exclude: [],
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
logLevel: 'info',
|
|
16
15
|
coloredLogs: true,
|
|
17
16
|
screenshotPath: './errorShots/',
|
|
18
17
|
baseUrl: '',
|
|
19
18
|
|
|
20
|
-
|
|
21
19
|
waitforTimeout: 90000,
|
|
22
20
|
connectionRetryTimeout: 90000,
|
|
23
21
|
connectionRetryCount: 3,
|
|
@@ -27,5 +25,51 @@ module.exports = {
|
|
|
27
25
|
mochaOpts: {
|
|
28
26
|
ui: 'bdd',
|
|
29
27
|
timeout: 300000
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// Enable screenshot capture for Allure (WDIO built-in)
|
|
31
|
+
screenshotOnSave: true,
|
|
32
|
+
|
|
33
|
+
// Test hooks
|
|
34
|
+
before: async function() {
|
|
35
|
+
console.log('🔧 BEFORE hook triggered in base config');
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Capture final screenshot directly in the report folder
|
|
39
|
+
after: async function(results) {
|
|
40
|
+
console.log('🔧 AFTER hook triggered in base config');
|
|
41
|
+
|
|
42
|
+
// Only process if running locally
|
|
43
|
+
if (process.env.PLATFORM === 'local') {
|
|
44
|
+
try {
|
|
45
|
+
const fs = require('fs');
|
|
46
|
+
const nodePath = require('path');
|
|
47
|
+
|
|
48
|
+
// Find the most recent Allure results directory
|
|
49
|
+
const reportsBaseDir = './reports/allure';
|
|
50
|
+
if (fs.existsSync(reportsBaseDir)) {
|
|
51
|
+
const dirs = fs.readdirSync(reportsBaseDir)
|
|
52
|
+
.map(file => nodePath.join(reportsBaseDir, file))
|
|
53
|
+
.filter(file => fs.statSync(file).isDirectory())
|
|
54
|
+
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
55
|
+
|
|
56
|
+
if (dirs.length > 0) {
|
|
57
|
+
const latestDir = dirs[0];
|
|
58
|
+
console.log(`📁 Report directory: ${latestDir}`);
|
|
59
|
+
|
|
60
|
+
// Save final screenshot directly to the report directory
|
|
61
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
62
|
+
const screenshotName = `Test_Suite_Completion_${timestamp}.png`;
|
|
63
|
+
const screenshotPath = nodePath.join(latestDir, screenshotName);
|
|
64
|
+
|
|
65
|
+
console.log(`📸 Saving final screenshot directly to report: ${screenshotName}`);
|
|
66
|
+
await browser.saveScreenshot(screenshotPath);
|
|
67
|
+
console.log(`✅ Screenshot saved in report folder: ${screenshotName}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('❌ Error saving screenshot to report folder:', error.message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
30
74
|
}
|
|
31
|
-
};
|
|
75
|
+
};
|
|
@@ -90,6 +90,52 @@ async function pushComment(msg) {
|
|
|
90
90
|
if (!global.TEST_COMMENTS) global.TEST_COMMENTS = [];
|
|
91
91
|
global.TEST_COMMENTS.push(msg);
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
/* ----------------- AUTO REPORT GENERATION ----------------- */
|
|
95
|
+
async function generateLocalReport() {
|
|
96
|
+
try {
|
|
97
|
+
const buildName = process.env.BROWSERSTACK_BUILD_NAME || 'local-build';
|
|
98
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
|
|
99
|
+
const reportDir = `./reports/mochawesome/${buildName}-${timestamp}`;
|
|
100
|
+
const jsonFile = path.join(process.cwd(), reportDir, 'mochawesome.json');
|
|
101
|
+
|
|
102
|
+
// Check if JSON report exists
|
|
103
|
+
if (!fs.existsSync(jsonFile)) {
|
|
104
|
+
console.log(`⚠️ No JSON report found at: ${jsonFile}`);
|
|
105
|
+
console.log('💡 Skipping report generation. Ensure tests ran successfully.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(`📊 Generating HTML report from: ${jsonFile}`);
|
|
110
|
+
|
|
111
|
+
// Use marge to generate HTML report
|
|
112
|
+
const { execSync } = require('child_process');
|
|
113
|
+
execSync(`npx marge "${jsonFile}" -o "${reportDir}"`, {
|
|
114
|
+
stdio: 'inherit',
|
|
115
|
+
cwd: process.cwd()
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const htmlFile = path.join(reportDir, 'mochawesome.html');
|
|
119
|
+
console.log(`✅ HTML report generated successfully!`);
|
|
120
|
+
console.log(`📍 Location: ${htmlFile}`);
|
|
121
|
+
|
|
122
|
+
// Auto-open report in browser
|
|
123
|
+
if (fs.existsSync(htmlFile)) {
|
|
124
|
+
console.log(`🌐 Opening report in browser...`);
|
|
125
|
+
try {
|
|
126
|
+
const openCommand = process.platform === 'darwin' ? 'open' :
|
|
127
|
+
process.platform === 'win32' ? 'start' :
|
|
128
|
+
'xdg-open';
|
|
129
|
+
execSync(`${openCommand} "${htmlFile}"`, { stdio: 'ignore' });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.log(`💡 To view report manually, open: ${htmlFile}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error(`❌ Error generating local report: ${error.message}`);
|
|
136
|
+
// Don't fail the entire process if report generation fails
|
|
137
|
+
}
|
|
138
|
+
}
|
|
93
139
|
/* ------------------ COMMON CONFIG ------------------ */
|
|
94
140
|
|
|
95
141
|
const commonHooks = {
|
|
@@ -348,6 +394,13 @@ const commonHooks = {
|
|
|
348
394
|
|
|
349
395
|
await safeUpdateExecution();
|
|
350
396
|
BUFFER.clear();
|
|
397
|
+
|
|
398
|
+
// Auto-generate HTML report for local execution
|
|
399
|
+
if (process.env.PLATFORM === 'local') {
|
|
400
|
+
console.log('📊 Generating local test report...');
|
|
401
|
+
await generateLocalReport();
|
|
402
|
+
}
|
|
403
|
+
|
|
351
404
|
return exitCode;
|
|
352
405
|
},
|
|
353
406
|
|
|
@@ -1,11 +1,51 @@
|
|
|
1
|
-
module.exports = () =>
|
|
2
|
-
|
|
1
|
+
module.exports = () => {
|
|
2
|
+
// Generate unique report directory using BROWSERSTACK_BUILD_NAME
|
|
3
|
+
const buildName = process.env.BROWSERSTACK_BUILD_NAME || 'local-mobile-build';
|
|
4
|
+
// Remove spaces and special characters from build name for folder name - use underscores
|
|
5
|
+
const cleanBuildName = buildName.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
6
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; // YYYY-MM-DD format
|
|
7
|
+
const uniqueReportDir = `./reports/allure/${cleanBuildName}-${timestamp}`;
|
|
3
8
|
|
|
9
|
+
console.log(`📱 Mobile local execution configured`);
|
|
10
|
+
console.log(`📊 Allure results will be saved in: ${uniqueReportDir}`);
|
|
11
|
+
console.log(`💡 Report will be generated automatically after execution completes`);
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
return {
|
|
14
|
+
services: ['appium'],
|
|
15
|
+
|
|
16
|
+
capabilities: [{
|
|
17
|
+
platformName: 'Android',
|
|
18
|
+
'appium:deviceName': 'Android Emulator',
|
|
19
|
+
'appium:automationName': 'UiAutomator2',
|
|
20
|
+
'appium:app': process.env.LOCAL_APP_PATH
|
|
21
|
+
}],
|
|
22
|
+
|
|
23
|
+
// Allure Reporter Configuration
|
|
24
|
+
reporters: [
|
|
25
|
+
['allure', {
|
|
26
|
+
outputDir: uniqueReportDir,
|
|
27
|
+
disableWebdriverStepsReporting: false, // Enable step reporting
|
|
28
|
+
disableWebdriverScreenshotsReporting: false, // Enable automatic screenshot capture
|
|
29
|
+
useCucumberStepReporter: false,
|
|
30
|
+
disableMochaHooks: false,
|
|
31
|
+
allureCode: true,
|
|
32
|
+
captureAllureScreenshots: true
|
|
33
|
+
}]
|
|
34
|
+
],
|
|
35
|
+
|
|
36
|
+
// Auto-generate report after execution completes
|
|
37
|
+
onComplete: async function(exitCode, config, capabilities, results) {
|
|
38
|
+
const { execSync } = require('child_process');
|
|
39
|
+
try {
|
|
40
|
+
// Add a small delay to ensure Allure finishes writing all files
|
|
41
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
42
|
+
|
|
43
|
+
console.log('\n🎯 Generating Allure report...');
|
|
44
|
+
execSync('node allure-report-utils/generate-allure-report.js', { stdio: 'inherit' });
|
|
45
|
+
console.log('✅ Report generation complete!');
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('❌ Error generating report:', error.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
module.exports = (bsCaps) => {
|
|
2
2
|
const browserName = (bsCaps.browserName || 'chrome').toLowerCase();
|
|
3
3
|
|
|
4
|
+
// Generate unique report directory using BROWSERSTACK_BUILD_NAME
|
|
5
|
+
const buildName = process.env.BROWSERSTACK_BUILD_NAME || 'local-build';
|
|
6
|
+
// Remove spaces and special characters from build name for folder name - use underscores
|
|
7
|
+
const cleanBuildName = buildName.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
8
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0]; // YYYY-MM-DD format
|
|
9
|
+
const uniqueReportDir = `./reports/allure/${cleanBuildName}-${timestamp}`;
|
|
10
|
+
|
|
4
11
|
// Normalize browser names for WebDriver
|
|
5
12
|
const browserNameMap = {
|
|
6
13
|
chrome: 'chrome',
|
|
@@ -17,39 +24,12 @@ module.exports = (bsCaps) => {
|
|
|
17
24
|
|
|
18
25
|
const normalizedBrowserName = browserNameMap[browserName] || 'chrome';
|
|
19
26
|
|
|
20
|
-
const config = {
|
|
21
|
-
// selenium-standalone-service automatically handles all browser drivers
|
|
22
|
-
// - Downloads Selenium Server JAR to node_modules/ (no admin needed)
|
|
23
|
-
// - Auto-downloads chromedriver, geckodriver, etc. on first run
|
|
24
|
-
// - Caches drivers locally after first download
|
|
25
|
-
// - Supports: Chrome, Firefox, Edge, Safari, IE
|
|
26
|
-
services: [['selenium-standalone', {
|
|
27
|
-
// Check for updates (set to false to use cached version)
|
|
28
|
-
checkArgs: false,
|
|
29
|
-
// Skip Selenium installation if already present (speeds up subsequent runs)
|
|
30
|
-
skipSeleniumInstall: false,
|
|
31
|
-
// Custom arguments for Selenium Server
|
|
32
|
-
seleniumArgs: [],
|
|
33
|
-
// Custom arguments for drivers
|
|
34
|
-
drivers: {
|
|
35
|
-
chrome: {
|
|
36
|
-
version: 'LATEST', // Always use latest ChromeDriver
|
|
37
|
-
baseURL: 'https://chromedriver.storage.googleapis.com'
|
|
38
|
-
},
|
|
39
|
-
firefox: {
|
|
40
|
-
version: 'LATEST', // Always use latest GeckoDriver
|
|
41
|
-
baseURL: 'https://github.com/mozilla/geckodriver/releases'
|
|
42
|
-
},
|
|
43
|
-
edge: {
|
|
44
|
-
version: 'LATEST' // Always use latest Edge Driver
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}]]
|
|
48
|
-
};
|
|
49
|
-
|
|
50
27
|
// Configure browser-specific capabilities
|
|
28
|
+
let capabilities;
|
|
29
|
+
let service;
|
|
30
|
+
|
|
51
31
|
if (normalizedBrowserName === 'chrome') {
|
|
52
|
-
|
|
32
|
+
capabilities = [{
|
|
53
33
|
browserName: 'chrome',
|
|
54
34
|
'goog:chromeOptions': {
|
|
55
35
|
args: [
|
|
@@ -61,19 +41,42 @@ module.exports = (bsCaps) => {
|
|
|
61
41
|
]
|
|
62
42
|
}
|
|
63
43
|
}];
|
|
44
|
+
|
|
45
|
+
// Use wdio-chromedriver-service - auto-downloads ChromeDriver at runtime
|
|
46
|
+
// No admin permission needed - downloads to node_modules/
|
|
47
|
+
service = ['chromedriver', {
|
|
48
|
+
// Automatically download ChromeDriver if not present
|
|
49
|
+
chromedriverOptions: {
|
|
50
|
+
// Check for ChromeDriver updates (set to false for faster startup)
|
|
51
|
+
checkForUpdates: true,
|
|
52
|
+
// Use latest ChromeDriver compatible with installed Chrome
|
|
53
|
+
// If false, uses version specified in package.json chromedriver dependency
|
|
54
|
+
autoDownload: true
|
|
55
|
+
}
|
|
56
|
+
}]
|
|
57
|
+
|
|
64
58
|
} else if (normalizedBrowserName === 'firefox') {
|
|
65
|
-
|
|
59
|
+
capabilities = [{
|
|
66
60
|
browserName: 'firefox',
|
|
67
61
|
'moz:firefoxOptions': {
|
|
68
|
-
args: ['-headless'],
|
|
62
|
+
//args: ['-headless'],
|
|
69
63
|
prefs: {
|
|
70
64
|
'dom.webdriver.enabled': false,
|
|
71
65
|
'useAutomationExtension': false
|
|
72
66
|
}
|
|
73
67
|
}
|
|
74
68
|
}];
|
|
69
|
+
|
|
70
|
+
// Use wdio-geckodriver-service for Firefox
|
|
71
|
+
service = ['geckodriver', {
|
|
72
|
+
// Auto-download geckodriver if not present
|
|
73
|
+
geckodriverOptions: {
|
|
74
|
+
// Use the geckodriver from node_modules
|
|
75
|
+
// No need to download separately
|
|
76
|
+
}
|
|
77
|
+
}];
|
|
75
78
|
} else if (normalizedBrowserName === 'MicrosoftEdge') {
|
|
76
|
-
|
|
79
|
+
capabilities = [{
|
|
77
80
|
browserName: 'MicrosoftEdge',
|
|
78
81
|
'ms:edgeOptions': {
|
|
79
82
|
args: [
|
|
@@ -83,35 +86,70 @@ module.exports = (bsCaps) => {
|
|
|
83
86
|
]
|
|
84
87
|
}
|
|
85
88
|
}];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// Safari requires manual driver enablement in Safari > Preferences > Advanced > "Show Develop menu"
|
|
91
|
-
// Ensure "Allow Remote Automation" is enabled in Develop menu
|
|
92
|
-
}
|
|
93
|
-
}];
|
|
94
|
-
} else if (normalizedBrowserName === 'internet explorer') {
|
|
95
|
-
config.capabilities = [{
|
|
96
|
-
browserName: 'internet explorer',
|
|
97
|
-
'se:ieOptions': {
|
|
98
|
-
ignoreProtectedModeSettings: true,
|
|
99
|
-
ignoreZoomSetting: true,
|
|
100
|
-
initialBrowserUrl: 'about:blank'
|
|
101
|
-
}
|
|
102
|
-
}];
|
|
89
|
+
|
|
90
|
+
// EdgeDriver is included with Edge browser - no download needed
|
|
91
|
+
service = ['edgedriver'];
|
|
92
|
+
|
|
103
93
|
} else {
|
|
104
94
|
// Default to Chrome for unsupported browsers
|
|
105
|
-
|
|
95
|
+
capabilities = [{
|
|
106
96
|
browserName: 'chrome',
|
|
107
97
|
'goog:chromeOptions': {
|
|
108
98
|
args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu']
|
|
109
99
|
}
|
|
110
100
|
}];
|
|
101
|
+
|
|
102
|
+
service = ['chromedriver', {
|
|
103
|
+
chromedriverOptions: {
|
|
104
|
+
checkForUpdates: true,
|
|
105
|
+
autoDownload: true
|
|
106
|
+
}
|
|
107
|
+
}];
|
|
111
108
|
}
|
|
112
109
|
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
const config = {
|
|
111
|
+
// Use direct WebDriver connection without Selenium Grid/Hub
|
|
112
|
+
// Opens Chrome/Firefox directly - no selenium-server
|
|
113
|
+
protocol: 'webdriver',
|
|
114
|
+
|
|
115
|
+
// Auto-download drivers service - no admin permission needed
|
|
116
|
+
services: service.length > 0 ? [service] : [],
|
|
117
|
+
|
|
118
|
+
capabilities: capabilities,
|
|
119
|
+
|
|
120
|
+
// Allure Reporter Configuration
|
|
121
|
+
reporters: [
|
|
122
|
+
['allure', {
|
|
123
|
+
outputDir: uniqueReportDir,
|
|
124
|
+
disableWebdriverStepsReporting: false, // Enable step reporting
|
|
125
|
+
disableWebdriverScreenshotsReporting: false, // Enable automatic screenshot capture
|
|
126
|
+
useCucumberStepReporter: false,
|
|
127
|
+
disableMochaHooks: false,
|
|
128
|
+
allureCode: true,
|
|
129
|
+
captureAllureScreenshots: true
|
|
130
|
+
}]
|
|
131
|
+
],
|
|
132
|
+
|
|
133
|
+
// Auto-generate report after execution completes
|
|
134
|
+
onComplete: async function(exitCode, config, capabilities, results) {
|
|
135
|
+
const { execSync } = require('child_process');
|
|
136
|
+
try {
|
|
137
|
+
// Add a small delay to ensure Allure finishes writing all files
|
|
138
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
139
|
+
|
|
140
|
+
console.log('\n🎯 Generating Allure report...');
|
|
141
|
+
execSync('node allure-report-utils/generate-allure-report.js', { stdio: 'inherit' });
|
|
142
|
+
console.log('✅ Report generation complete!');
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('❌ Error generating report:', error.message);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
console.log(`🌐 Local execution configured for: ${normalizedBrowserName}`);
|
|
150
|
+
console.log(`📦 Browser drivers will auto-download to node_modules/ (no admin permission needed)`);
|
|
151
|
+
console.log(`📊 Allure results will be saved in: ${uniqueReportDir}`);
|
|
152
|
+
console.log(`💡 Report will be generated automatically after execution completes`);
|
|
115
153
|
|
|
116
154
|
return config;
|
|
117
155
|
};
|
|
@@ -63,5 +63,116 @@ if (PLATFORM === 'browserstack' || PLATFORM === 'browserstacklocal') {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
// Debug logging
|
|
67
|
+
console.log('=== CONFIG MERGE DEBUG ===');
|
|
68
|
+
console.log('PLATFORM value:', PLATFORM);
|
|
69
|
+
console.log('baseConfig.afterEach exists:', !!baseConfig.afterEach);
|
|
70
|
+
console.log('envConfig type:', typeof envConfig);
|
|
71
|
+
|
|
72
|
+
// Get the env config but don't merge yet
|
|
73
|
+
const envConfigResult = envConfig(bsCaps);
|
|
74
|
+
console.log('envConfigResult.afterEach exists:', !!envConfigResult.afterEach);
|
|
75
|
+
|
|
76
|
+
// Merge the configs
|
|
77
|
+
const finalConfig = deepmerge(baseConfig, envConfigResult);
|
|
78
|
+
|
|
79
|
+
// Always preserve base config hooks for local platform
|
|
80
|
+
if (PLATFORM === 'local' && baseConfig.afterEach) {
|
|
81
|
+
console.log('Preserving baseConfig.afterEach for local platform');
|
|
82
|
+
finalConfig.afterEach = baseConfig.afterEach;
|
|
83
|
+
} else if (finalConfig.afterEach) {
|
|
84
|
+
console.log('Using envConfig afterEach');
|
|
85
|
+
} else {
|
|
86
|
+
console.log('No afterEach hook found in any config');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log('finalConfig.afterEach type:', typeof finalConfig.afterEach);
|
|
90
|
+
console.log('=== END CONFIG MERGE DEBUG ===');
|
|
91
|
+
|
|
92
|
+
// Add onComplete hook to attach screenshots to test results
|
|
93
|
+
const originalOnComplete = finalConfig.onComplete;
|
|
94
|
+
finalConfig.onComplete = async function(exitCode, config, capabilities, results) {
|
|
95
|
+
console.log('🎯 Attaching screenshots to test results...');
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const fs = require('fs');
|
|
99
|
+
const path = require('path');
|
|
100
|
+
|
|
101
|
+
// Find the most recent Allure results directory (where screenshots are saved)
|
|
102
|
+
const reportsBaseDir = './reports/allure';
|
|
103
|
+
if (fs.existsSync(reportsBaseDir)) {
|
|
104
|
+
const dirs = fs.readdirSync(reportsBaseDir)
|
|
105
|
+
.map(file => path.join(reportsBaseDir, file))
|
|
106
|
+
.filter(file => fs.statSync(file).isDirectory())
|
|
107
|
+
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
108
|
+
|
|
109
|
+
if (dirs.length > 0) {
|
|
110
|
+
const latestDir = dirs[0];
|
|
111
|
+
console.log(`📁 Report directory: ${latestDir}`);
|
|
112
|
+
|
|
113
|
+
// Find screenshots saved directly in the report directory
|
|
114
|
+
const screenshots = fs.readdirSync(latestDir)
|
|
115
|
+
.filter(file => file.endsWith('.png'))
|
|
116
|
+
.map(file => path.join(latestDir, file));
|
|
117
|
+
|
|
118
|
+
console.log(`📸 Found ${screenshots.length} screenshots in report folder`);
|
|
119
|
+
|
|
120
|
+
// Find test result files
|
|
121
|
+
const resultFiles = fs.readdirSync(latestDir)
|
|
122
|
+
.filter(file => file.endsWith('-result.json'));
|
|
123
|
+
|
|
124
|
+
console.log(`📋 Found ${resultFiles.length} test result files`);
|
|
125
|
+
|
|
126
|
+
// Attach a screenshot to each test result
|
|
127
|
+
resultFiles.forEach((resultFile, index) => {
|
|
128
|
+
try {
|
|
129
|
+
const resultPath = path.join(latestDir, resultFile);
|
|
130
|
+
const resultData = JSON.parse(fs.readFileSync(resultPath, 'utf8'));
|
|
131
|
+
|
|
132
|
+
// Add screenshot attachment
|
|
133
|
+
if (screenshots.length > 0) {
|
|
134
|
+
const screenshotIndex = Math.min(index, screenshots.length - 1);
|
|
135
|
+
const screenshotPath = screenshots[screenshotIndex];
|
|
136
|
+
const screenshotBuffer = fs.readFileSync(screenshotPath);
|
|
137
|
+
|
|
138
|
+
// Create attachment in the Allure directory
|
|
139
|
+
const attachmentName = `screenshot-${index}.png`;
|
|
140
|
+
const attachmentPath = path.join(latestDir, attachmentName);
|
|
141
|
+
|
|
142
|
+
fs.writeFileSync(attachmentPath, screenshotBuffer);
|
|
143
|
+
|
|
144
|
+
// Add attachment reference to test result
|
|
145
|
+
if (!resultData.attachments) {
|
|
146
|
+
resultData.attachments = [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
resultData.attachments.push({
|
|
150
|
+
name: 'Screenshot',
|
|
151
|
+
source: attachmentName,
|
|
152
|
+
type: 'image/png'
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Write updated result file
|
|
156
|
+
fs.writeFileSync(resultPath, JSON.stringify(resultData, null, 2));
|
|
157
|
+
console.log(`✅ Attached screenshot to: ${resultFile}`);
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(`❌ Error processing ${resultFile}:`, error.message);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
console.log(`✅ Attached ${resultFiles.length} screenshots to test results`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('❌ Error in onComplete screenshot attachment:', error.message);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Call original onComplete if it exists
|
|
172
|
+
if (typeof originalOnComplete === 'function') {
|
|
173
|
+
await originalOnComplete(exitCode, config, capabilities, results);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
exports.config = finalConfig;
|
|
67
178
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "froth-webdriverio-framework",
|
|
3
|
-
"version": "7.0.119-dev1.
|
|
3
|
+
"version": "7.0.119-dev1.8",
|
|
4
4
|
"readme": "WebdriverIO Integration",
|
|
5
5
|
"description": "WebdriverIO and BrowserStack App Automate",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"lint": "eslint ."
|
|
8
|
+
"lint": "eslint .",
|
|
9
|
+
"test": "npm run test:allure && npm run report:generate",
|
|
10
|
+
"test:allure": "wdio run froth_configs/local/web.config.js",
|
|
11
|
+
"report:generate": "node allure-report-utils/generate-allure-report.js",
|
|
12
|
+
"report:open": "node allure-report-utils/open-allure-report.js"
|
|
9
13
|
},
|
|
10
14
|
"repository": {
|
|
11
15
|
"type": "git",
|
|
@@ -24,12 +28,15 @@
|
|
|
24
28
|
"@wdio/mocha-framework": "9.23.0",
|
|
25
29
|
"@wdio/selenium-standalone-service": "^8.14.0",
|
|
26
30
|
"@wdio/spec-reporter": "^9.20.0",
|
|
31
|
+
"@wdio/allure-reporter": "^9.20.0",
|
|
32
|
+
"allure-commandline": "^2.30.0",
|
|
27
33
|
"appium": "^3.1.2",
|
|
28
34
|
"appium-uiautomator2-driver": "^6.7.8",
|
|
29
35
|
"assert": "^2.1.0",
|
|
30
36
|
"axios": "1.14.0",
|
|
31
37
|
"browserstack-local": "^1.5.8",
|
|
32
38
|
"chai": "^6.2.2",
|
|
39
|
+
"chromedriver": "^149.0.4",
|
|
33
40
|
"crypto-js": "^4.2.0",
|
|
34
41
|
"deepmerge": "^4.3.1",
|
|
35
42
|
"ejs": "^4.0.1",
|
|
@@ -37,6 +44,7 @@
|
|
|
37
44
|
"express": "^5.2.1",
|
|
38
45
|
"form-data": "^4.0.5",
|
|
39
46
|
"fs": "^0.0.1-security",
|
|
47
|
+
"geckodriver": "^4.5.0",
|
|
40
48
|
"js-yaml": "^4.1.1",
|
|
41
49
|
"mysql2": "^3.16.0",
|
|
42
50
|
"node-fetch": "^3.3.2",
|
|
@@ -44,6 +52,9 @@
|
|
|
44
52
|
"randexp": "^0.5.3",
|
|
45
53
|
"ts-node": "^10.9.2",
|
|
46
54
|
"typescript": "^5.9.3",
|
|
47
|
-
"vm": "^0.1.0"
|
|
55
|
+
"vm": "^0.1.0",
|
|
56
|
+
"wdio-chromedriver-service": "^8.1.1",
|
|
57
|
+
"wdio-geckodriver-service": "^5.0.2"
|
|
48
58
|
}
|
|
59
|
+
|
|
49
60
|
}
|
package/web.conf.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
exports.config = require('./froth_configs/wdio.common.conf').config;
|