wdio-lambdatest-service-sdk 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # WDIO LambdaTest Service
2
+
3
+ This WebdriverIO service provides seamless integration with LambdaTest. It automatically handles:
4
+
5
+ 1. **Authentication**: Pass `username` and `accessKey` in the service options.
6
+ 2. **Test Status Updates**: Automatically marks tests as Passed/Failed on the LambdaTest dashboard using Lambda Hooks.
7
+ 3. **Logs**: Provides clean, colorful status logs in the terminal.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install wdio-lambdatest-service --save-dev
13
+ ```
14
+
15
+ *(For local development, you can link the package or use the setup script described below)*
16
+
17
+ ## Automated Setup
18
+
19
+ You can automatically update your existing WebdriverIO configuration files (`*.conf.js`) to use this SDK using the included setup script.
20
+
21
+ Run the following command, providing the path to your project or test folder:
22
+
23
+ ```bash
24
+ # If installed via npm
25
+ npx wdio-lambdatest-setup ./path/to/your/project
26
+
27
+ # If running locally from source
28
+ node wdio-lambdatest-service/bin/setup.js ./android-sample
29
+ ```
30
+
31
+ This script will:
32
+ - Scan for `.conf.js` files recursively.
33
+ - Inject the service configuration.
34
+ - Set `logLevel` to `error` for cleaner output.
35
+ - Comment out hardcoded credentials (so the SDK handles them).
36
+
37
+ ## Manual Configuration
38
+
39
+ Add the service to your `wdio.conf.js`:
40
+
41
+ ```javascript
42
+ const path = require('path');
43
+
44
+ exports.config = {
45
+ // ...
46
+ // user: process.env.LT_USERNAME, // handled by SDK
47
+ // key: process.env.LT_ACCESS_KEY, // handled by SDK
48
+
49
+ logLevel: 'error', // Recommended to reduce noise
50
+
51
+ services: [
52
+ ['lambdatest', {
53
+ username: process.env.LT_USERNAME,
54
+ accessKey: process.env.LT_ACCESS_KEY
55
+ }]
56
+ ],
57
+ // ...
58
+ };
59
+ ```
60
+
61
+ If you are using the SDK locally (without npm install), use the absolute path:
62
+
63
+ ```javascript
64
+ services: [
65
+ [path.join(__dirname, '../../wdio-lambdatest-service'), {
66
+ username: process.env.LT_USERNAME,
67
+ accessKey: process.env.LT_ACCESS_KEY
68
+ }]
69
+ ],
70
+ ```
71
+
72
+ ## Features
73
+
74
+ - **Automatic Status Updates**: Checks test results in `afterTest` (Mocha/Jasmine) or `afterScenario` (Cucumber) and updates the LambdaTest job status.
75
+ - **Credential Management**: Can inject credentials if not already present in the main config.
76
+ - **Async Support**: Works seamlessly with both Sync and Async WebdriverIO execution modes.
77
+
78
+ ## Development
79
+
80
+ This package exports a **Service** (worker) and a **Launcher** (main process).
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const readline = require('readline');
5
+
6
+ const rl = readline.createInterface({
7
+ input: process.stdin,
8
+ output: process.stdout
9
+ });
10
+
11
+ const config = {
12
+ testName: '',
13
+ filename: '',
14
+ username: '',
15
+ key: '',
16
+ type: '', // 'app' or 'browser'
17
+ subType: '', // 'real', 'virtual', 'desktop', 'mobile-virtual', 'mobile-real'
18
+ parallel: 0,
19
+ specPath: ''
20
+ };
21
+
22
+ const outputDir = path.resolve(process.cwd(), 'LT_Test');
23
+
24
+ // Ensure output dir exists
25
+ if (!fs.existsSync(outputDir)) {
26
+ fs.mkdirSync(outputDir, { recursive: true });
27
+ }
28
+
29
+ function ask(question, defaultVal) {
30
+ return new Promise((resolve) => {
31
+ rl.question(`${question}${defaultVal ? ` (${defaultVal})` : ''}: `, (answer) => {
32
+ resolve(answer.trim() || defaultVal);
33
+ });
34
+ });
35
+ }
36
+
37
+ async function run() {
38
+ console.log('\n--- LambdaTest WDIO Config Generator ---\n');
39
+
40
+ config.testName = await ask('Enter Test Name (e.g. AndroidAppTest)', 'AndroidAppTest');
41
+ config.filename = `${config.testName}.conf.js`;
42
+
43
+ config.username = await ask('Enter LambdaTest Username', process.env.LT_USERNAME || 'YOUR_USERNAME');
44
+ config.key = await ask('Enter LambdaTest Access Key', process.env.LT_ACCESS_KEY || 'YOUR_ACCESS_KEY');
45
+
46
+ const typeInput = await ask('Test Type (1: App, 2: Browser)', '1');
47
+ config.type = typeInput === '1' || typeInput.toLowerCase() === 'app' ? 'app' : 'browser';
48
+
49
+ if (config.type === 'app') {
50
+ const subTypeInput = await ask('Device Type (1: Real Device, 2: Virtual Device)', '1');
51
+ config.subType = subTypeInput === '1' ? 'real' : 'virtual';
52
+ } else {
53
+ console.log('Browser Options: 1: Desktop, 2: Mobile Browser (Virtual), 3: Mobile Browser (Real)');
54
+ const subTypeInput = await ask('Select Option', '1');
55
+ if (subTypeInput === '2') config.subType = 'mobile-virtual';
56
+ else if (subTypeInput === '3') config.subType = 'mobile-real';
57
+ else config.subType = 'desktop';
58
+ }
59
+
60
+ const parallelInput = await ask('Number of Parallel Threads (0 or empty for no parallel)', '0');
61
+ config.parallel = parseInt(parallelInput) || 0;
62
+
63
+ config.specPath = await ask('Path to Spec File (e.g. ./specs/test.js)', './specs/android-test.js');
64
+
65
+ // Generate Config Content
66
+ let capabilities = [];
67
+ const commonCaps = {
68
+ build: `LT_WDIO_${config.testName}_${new Date().toISOString().split('T')[0]}`,
69
+ name: config.testName,
70
+ visual: true,
71
+ console: true
72
+ };
73
+
74
+ if (config.type === 'app') {
75
+ const cap = {
76
+ "lt:options": {
77
+ w3c: true,
78
+ platformName: "Android",
79
+ deviceName: config.subType === 'real' ? ".*" : "Pixel 4",
80
+ platformVersion: config.subType === 'real' ? undefined : "11",
81
+ isRealMobile: config.subType === 'real',
82
+ app: process.env.LT_APP_ID || "lt://proverbial-android", // Default app
83
+ }
84
+ };
85
+ capabilities.push(cap);
86
+ } else {
87
+ if (config.subType === 'desktop') {
88
+ capabilities.push({
89
+ browserName: "chrome",
90
+ browserVersion: "latest",
91
+ "lt:options": {
92
+ platformName: "Windows 10"
93
+ }
94
+ });
95
+ } else {
96
+ // Mobile Browser
97
+ capabilities.push({
98
+ "lt:options": {
99
+ w3c: true,
100
+ platformName: "Android",
101
+ deviceName: config.subType === 'mobile-real' ? ".*" : "Pixel 4",
102
+ platformVersion: config.subType === 'mobile-real' ? undefined : "11",
103
+ isRealMobile: config.subType === 'mobile-real',
104
+ }
105
+ });
106
+ }
107
+ }
108
+
109
+ // Prepare template
110
+ const template = `const path = require('path');
111
+
112
+ exports.config = {
113
+ // Authentication handled by SDK
114
+ // user: "${config.username}",
115
+ // key: "${config.key}",
116
+
117
+ updateJob: false,
118
+ specs: ["${config.specPath}"],
119
+ exclude: [],
120
+
121
+ maxInstances: ${config.parallel > 0 ? config.parallel : 1},
122
+
123
+ commonCapabilities: ${JSON.stringify(commonCaps, null, 4)},
124
+
125
+ capabilities: ${JSON.stringify(capabilities, null, 4)},
126
+
127
+ logLevel: 'error',
128
+ coloredLogs: true,
129
+ screenshotPath: "./errorShots/",
130
+ baseUrl: "https://mobile-hub.lambdatest.com",
131
+ waitforTimeout: 10000,
132
+ connectionRetryTimeout: 90000,
133
+ connectionRetryCount: 3,
134
+ path: "/wd/hub",
135
+ hostname: "mobile-hub.lambdatest.com",
136
+ port: 80,
137
+
138
+ services: [
139
+ [path.join(__dirname, '../wdio-lambdatest-service'), {
140
+ user: "${config.username}",
141
+ key: "${config.key}"
142
+ }]
143
+ ],
144
+
145
+ framework: "mocha",
146
+ mochaOpts: {
147
+ ui: "bdd",
148
+ timeout: 20000,
149
+ },
150
+ };
151
+
152
+ exports.config.capabilities.forEach(function (caps) {
153
+ for (var i in exports.config.commonCapabilities)
154
+ caps[i] = caps[i] || exports.config.commonCapabilities[i];
155
+ });
156
+ `;
157
+
158
+ const outputPath = path.join(outputDir, config.filename);
159
+ fs.writeFileSync(outputPath, template);
160
+
161
+ console.log(`\nSuccessfully created configuration file at:\n${outputPath}`);
162
+ console.log(`\nRun it with:\n./node_modules/.bin/wdio ${path.relative(process.cwd(), outputPath)}`);
163
+
164
+ rl.close();
165
+ }
166
+
167
+ run();
package/bin/setup.js ADDED
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const readline = require('readline');
5
+
6
+ const sdkPath = path.resolve(__dirname, '..'); // Absolute path to the SDK folder
7
+
8
+ function start() {
9
+ const targetDirArg = process.argv[2];
10
+
11
+ if (targetDirArg) {
12
+ traverseDirectory(path.resolve(targetDirArg));
13
+ console.log('Done scanning directories.');
14
+ } else {
15
+ const rl = readline.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout
18
+ });
19
+
20
+ rl.question('Where are your test scripts located? (e.g. ./android-sample): ', (answer) => {
21
+ if (!answer || answer.trim() === '') {
22
+ console.error('No directory provided. Exiting.');
23
+ process.exit(1);
24
+ }
25
+
26
+ const resolvedPath = path.resolve(answer.trim());
27
+ console.log(`Scanning: ${resolvedPath}`);
28
+
29
+ traverseDirectory(resolvedPath);
30
+ console.log('Done scanning directories.');
31
+ rl.close();
32
+ });
33
+ }
34
+ }
35
+
36
+ function traverseDirectory(dir) {
37
+ if (!fs.existsSync(dir)) {
38
+ console.error(`Directory not found: ${dir}`);
39
+ return;
40
+ }
41
+
42
+ const files = fs.readdirSync(dir);
43
+
44
+ files.forEach(file => {
45
+ const fullPath = path.join(dir, file);
46
+ const stat = fs.statSync(fullPath);
47
+
48
+ if (stat.isDirectory()) {
49
+ if (file !== 'node_modules' && file !== '.git') {
50
+ traverseDirectory(fullPath);
51
+ }
52
+ } else if (file.endsWith('.conf.js')) {
53
+ updateConfigFile(fullPath);
54
+ }
55
+ });
56
+ }
57
+
58
+ function updateConfigFile(filePath) {
59
+ let content = fs.readFileSync(filePath, 'utf8');
60
+
61
+ console.log(`Processing: ${filePath}`);
62
+
63
+ // Extract credentials (either active or commented out)
64
+ let userVal = "process.env.LT_USERNAME";
65
+ let keyVal = "process.env.LT_ACCESS_KEY";
66
+
67
+ const userMatch = content.match(/^\s*\/{0,2}\s*user:\s*(.*?),/m);
68
+ if (userMatch) userVal = userMatch[1].trim();
69
+
70
+ const keyMatch = content.match(/^\s*\/{0,2}\s*key:\s*(.*?),/m);
71
+ if (keyMatch) keyVal = keyMatch[1].trim();
72
+
73
+ // Calculate relative path
74
+ let relativeSdkPath = path.relative(path.dirname(filePath), sdkPath);
75
+ relativeSdkPath = relativeSdkPath.replace(/\\/g, '/');
76
+
77
+ // Construct the correct service entry with credentials
78
+ const serviceEntry = `[path.join(__dirname, '${relativeSdkPath}'), { user: ${userVal}, key: ${keyVal} }]`;
79
+
80
+ // Check if already updated (but maybe with missing credentials)
81
+ if (content.includes('wdio-lambdatest-service')) {
82
+ // Fix empty options if present: [path..., {}]
83
+ const emptyOptionsRegex = /\[path\.join\(__dirname, '.*?wdio-lambdatest-service'\), \{\}\]/;
84
+ if (emptyOptionsRegex.test(content)) {
85
+ console.log(`Fixing missing credentials in: ${filePath}`);
86
+ content = content.replace(emptyOptionsRegex, serviceEntry);
87
+
88
+ // Ensure logLevel is error
89
+ content = content.replace(/logLevel: ['"]info['"]/, "logLevel: 'error'");
90
+
91
+ fs.writeFileSync(filePath, content);
92
+ console.log(`Successfully repaired ${filePath}`);
93
+ } else {
94
+ console.log(`Skipping (already correct): ${filePath}`);
95
+ }
96
+ return;
97
+ }
98
+
99
+ console.log(`Updating: ${filePath}`);
100
+
101
+ // 1. Ensure 'path' module is required
102
+ if (!content.includes("require('path')")) {
103
+ content = "const path = require('path');\n" + content;
104
+ }
105
+
106
+ // 3. Inject Services
107
+ if (content.includes('services: [')) {
108
+ content = content.replace('services: [', `services: [\n ${serviceEntry},`);
109
+ } else {
110
+ content = content.replace('exports.config = {', `exports.config = {\n services: [\n ${serviceEntry}\n ],`);
111
+ }
112
+
113
+ // 4. Set logLevel to 'error'
114
+ if (content.includes('logLevel:')) {
115
+ content = content.replace(/logLevel:.*,/, "logLevel: 'error',");
116
+ } else {
117
+ content = content.replace('exports.config = {', `exports.config = {\n logLevel: 'error',`);
118
+ }
119
+
120
+ // 5. Comment out hardcoded user/key
121
+ // Use replace with callback to avoid re-commenting
122
+ content = content.replace(/^(\s*)user:/gm, '$1// user:');
123
+ content = content.replace(/^(\s*)key:/gm, '$1// key:');
124
+
125
+ fs.writeFileSync(filePath, content);
126
+ console.log(`Successfully updated ${filePath}`);
127
+ }
128
+
129
+ start();
package/index.js ADDED
@@ -0,0 +1,11 @@
1
+ const LambdaTestService = require('./src/service');
2
+ const LambdaTestLauncher = require('./src/launcher');
3
+
4
+ // Export the Service class as the main module export
5
+ module.exports = LambdaTestService;
6
+
7
+ // Also export as 'default' for compatibility with some WDIO loaders
8
+ module.exports.default = LambdaTestService;
9
+
10
+ // Export the Launcher
11
+ module.exports.launcher = LambdaTestLauncher;
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "wdio-lambdatest-service-sdk",
3
+ "version": "5.0.0",
4
+ "description": "WebdriverIO service for LambdaTest integration",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "wdio-lambdatest-setup": "bin/setup.js",
8
+ "wdio-lambdatest-generator": "bin/generate-config.js"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": [
14
+ "webdriverio",
15
+ "wdio",
16
+ "lambdatest",
17
+ "service",
18
+ "sdk"
19
+ ],
20
+ "author": "",
21
+ "license": "ISC",
22
+ "peerDependencies": {
23
+ "@wdio/cli": ">=5.0.0",
24
+ "webdriverio": ">=5.0.0"
25
+ },
26
+ "engines": {
27
+ "node": ">=10.0.0"
28
+ }
29
+ }
@@ -0,0 +1,45 @@
1
+
2
+ module.exports = class LambdaTestLauncher {
3
+ constructor(serviceOptions, capabilities, config) {
4
+ this.options = serviceOptions || {};
5
+ console.log('[LambdaTest Service] Launcher initialized with options:', JSON.stringify(this.options));
6
+ }
7
+
8
+ onPrepare(config, capabilities) {
9
+ console.log('[LambdaTest Service] Processing credentials...');
10
+
11
+ // 1. Update Main Config (for WDIO Basic Auth)
12
+ if (this.options.username) config.user = this.options.username;
13
+ if (this.options.accessKey) config.key = this.options.accessKey;
14
+ if (this.options.user) config.user = this.options.user;
15
+ if (this.options.key) config.key = this.options.key;
16
+
17
+ // Fallback to Env Vars
18
+ if (!config.user) config.user = process.env.LT_USERNAME;
19
+ if (!config.key) config.key = process.env.LT_ACCESS_KEY;
20
+
21
+ // 2. Update Capabilities (Explicitly pass credentials to caps)
22
+ // This ensures that even if Basic Auth fails or is missed,
23
+ // the grid sees them in the capabilities.
24
+ const user = config.user;
25
+ const key = config.key;
26
+
27
+ if (user && key) {
28
+ const capsList = Array.isArray(capabilities) ? capabilities : [capabilities];
29
+ capsList.forEach(cap => {
30
+ // Legacy JSONWP
31
+ if (!cap.user) cap.user = user;
32
+ if (!cap.accessKey) cap.accessKey = key;
33
+
34
+ // W3C Support (lt:options)
35
+ if (cap['lt:options']) {
36
+ if (!cap['lt:options'].username) cap['lt:options'].username = user;
37
+ if (!cap['lt:options'].accessKey) cap['lt:options'].accessKey = key;
38
+ }
39
+ });
40
+ console.log('[LambdaTest Service] Credentials injected into config and capabilities.');
41
+ } else {
42
+ console.error('[LambdaTest Service] Credentials not found! Please check service options or env vars.');
43
+ }
44
+ }
45
+ };
package/src/service.js ADDED
@@ -0,0 +1,97 @@
1
+
2
+ module.exports = class LambdaTestService {
3
+ constructor(serviceOptions, capabilities, config) {
4
+ this.options = serviceOptions || {};
5
+ this.isEnabled = true;
6
+ }
7
+
8
+ before(capabilities, specs) {
9
+ console.log('[LambdaTest Service] Service initialized in worker process.');
10
+ }
11
+
12
+ /**
13
+ * Triggered after each test (Mocha/Jasmine)
14
+ */
15
+ async afterTest(test, context, result) {
16
+ // Handle WDIO v5/v6/v7 differences
17
+ let passed = false;
18
+ let error = undefined;
19
+ let title = 'Test';
20
+
21
+ // Check if result is the object { error, result, duration, passed, retries }
22
+ if (result && typeof result.passed === 'boolean') {
23
+ passed = result.passed;
24
+ error = result.error;
25
+ }
26
+ // Fallback for older/different versions where result might be the passed boolean or different structure
27
+ else if (test && typeof test.passed === 'boolean') {
28
+ passed = test.passed;
29
+ error = test.error;
30
+ }
31
+
32
+ if (test && test.title) title = test.title;
33
+
34
+ await this.updateStatus(passed, error ? (error.message || error) : undefined, title);
35
+ }
36
+
37
+ /**
38
+ * Triggered after each scenario (Cucumber)
39
+ */
40
+ async afterScenario(uri, feature, scenario, result) {
41
+ const isPassed = result.status === 'passed' || result === 'passed' || result.passed === true;
42
+ const errorMessage = result.error ? result.error : (isPassed ? undefined : 'Scenario Failed');
43
+ const title = scenario.name || 'Scenario';
44
+ await this.updateStatus(isPassed, errorMessage, title);
45
+ }
46
+
47
+ /**
48
+ * Updates the test status on LambdaTest
49
+ * @param {boolean} passed
50
+ * @param {string} errorMessage
51
+ * @param {string} testTitle
52
+ */
53
+ async updateStatus(passed, errorMessage, testTitle) {
54
+ if (!this.isEnabled) return;
55
+
56
+ const status = passed ? 'passed' : 'failed';
57
+ const remark = errorMessage || (passed ? 'Test Passed' : 'Test Failed');
58
+
59
+ // ANSI Color Codes
60
+ const reset = "\x1b[0m";
61
+ const green = "\x1b[32m";
62
+ const red = "\x1b[31m";
63
+ const cyan = "\x1b[36m";
64
+ const gray = "\x1b[90m";
65
+
66
+ const color = passed ? green : red;
67
+ const icon = passed ? '✔' : '✖';
68
+
69
+ console.log(`\n${cyan}[LambdaTest SDK]${reset} ${gray}Updating Status...${reset}`);
70
+ console.log(`${cyan}[LambdaTest SDK]${reset} Test: ${testTitle}`);
71
+ console.log(`${cyan}[LambdaTest SDK]${reset} Status: ${color}${status.toUpperCase()} ${icon}${reset}`);
72
+ if (!passed) {
73
+ console.log(`${cyan}[LambdaTest SDK]${reset} Reason: ${red}${remark}${reset}`);
74
+ }
75
+ console.log(''); // Empty line
76
+
77
+ try {
78
+ // Using browser.execute to trigger the Lambda Hook
79
+ const script = `lambda-hook: ${JSON.stringify({
80
+ action: 'setTestStatus',
81
+ arguments: {
82
+ status: status,
83
+ remark: remark
84
+ }
85
+ })}`;
86
+
87
+ // Await the execution in case we are in async mode
88
+ await browser.execute(script);
89
+
90
+ // Add a small pause to ensure the hook is processed before session ends
91
+ await browser.pause(1000);
92
+
93
+ } catch (e) {
94
+ console.error(`${red}[LambdaTest SDK] Failed to update test status via hook.${reset}`, e);
95
+ }
96
+ }
97
+ };