froth-webdriverio-framework 7.0.119-dev1.7 → 7.0.119-dev1.9

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 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,55 @@
1
- module.exports = () => ({
2
- services: ['appium'],
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
- capabilities: [{
6
- platformName: 'Android',
7
- 'appium:deviceName': 'Android Emulator',
8
- 'appium:automationName': 'UiAutomator2',
9
- 'appium:app': process.env.LOCAL_APP_PATH
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
+ const nodePath = require('path');
40
+ try {
41
+ // Add a small delay to ensure Allure finishes writing all files
42
+ await new Promise(resolve => setTimeout(resolve, 1000));
43
+
44
+ // Resolve script path relative to framework package (works from any CWD)
45
+ const scriptPath = nodePath.resolve(__dirname, '../../allure-report-utils/generate-allure-report.js');
46
+
47
+ console.log('\n🎯 Generating Allure report...');
48
+ execSync(`node "${scriptPath}"`, { stdio: 'inherit', cwd: process.cwd() });
49
+ console.log('✅ Report generation complete!');
50
+ } catch (error) {
51
+ console.error('❌ Error generating report:', error.message);
52
+ }
53
+ }
54
+ };
55
+ };
@@ -1,5 +1,12 @@
1
1
  module.exports = (bsCaps) => {
2
2
  const browserName = (bsCaps.browserName || 'chrome').toLowerCase();
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}`;
3
10
 
4
11
  // Normalize browser names for WebDriver
5
12
  const browserNameMap = {
@@ -108,11 +115,45 @@ module.exports = (bsCaps) => {
108
115
  // Auto-download drivers service - no admin permission needed
109
116
  services: service.length > 0 ? [service] : [],
110
117
 
111
- capabilities: capabilities
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
+ const nodePath = require('path');
137
+ try {
138
+ // Add a small delay to ensure Allure finishes writing all files
139
+ await new Promise(resolve => setTimeout(resolve, 1000));
140
+
141
+ // Resolve script path relative to framework package (works from any CWD)
142
+ const scriptPath = nodePath.resolve(__dirname, '../../allure-report-utils/generate-allure-report.js');
143
+
144
+ console.log('\n🎯 Generating Allure report...');
145
+ execSync(`node "${scriptPath}"`, { stdio: 'inherit', cwd: process.cwd() });
146
+ console.log('✅ Report generation complete!');
147
+ } catch (error) {
148
+ console.error('❌ Error generating report:', error.message);
149
+ }
150
+ }
112
151
  };
113
152
 
114
153
  console.log(`🌐 Local execution configured for: ${normalizedBrowserName}`);
115
154
  console.log(`📦 Browser drivers will auto-download to node_modules/ (no admin permission needed)`);
155
+ console.log(`📊 Allure results will be saved in: ${uniqueReportDir}`);
156
+ console.log(`💡 Report will be generated automatically after execution completes`);
116
157
 
117
158
  return config;
118
159
  };
@@ -63,5 +63,116 @@ if (PLATFORM === 'browserstack' || PLATFORM === 'browserstacklocal') {
63
63
  }
64
64
 
65
65
 
66
- exports.config = deepmerge(baseConfig, envConfig(bsCaps));
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.7",
3
+ "version": "7.0.119-dev1.9",
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,6 +28,8 @@
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",