wdio-lambdatest-service-sdk 5.0.0 → 5.1.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.
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Setup command logic: Inject LambdaTest service into WDIO config files.
3
+ */
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const chalk = require('chalk');
7
+ const inquirer = require('inquirer');
8
+
9
+ // SDK path relative to this file (lib/cli/setup.js -> root)
10
+ const sdkPath = path.resolve(__dirname, '../..');
11
+
12
+ /**
13
+ * Parse existing user/key from config file content (supports commented lines).
14
+ * @param {string} content - File content
15
+ * @returns {{ userVal: string, keyVal: string }}
16
+ */
17
+ function parseCredentialsFromContent(content) {
18
+ let userVal = 'process.env.LT_USERNAME';
19
+ let keyVal = 'process.env.LT_ACCESS_KEY';
20
+ const userMatch = content.match(/^\s*\/{0,2}\s*user:\s*(.*?),/m);
21
+ if (userMatch) userVal = userMatch[1].trim();
22
+ const keyMatch = content.match(/^\s*\/{0,2}\s*key:\s*(.*?),/m);
23
+ if (keyMatch) keyVal = keyMatch[1].trim();
24
+ return { userVal, keyVal };
25
+ }
26
+
27
+ /**
28
+ * Update a single WDIO config file: inject service, ensure path, set logLevel, comment user/key.
29
+ * Avoids double-injection by checking for existing wdio-lambdatest-service.
30
+ */
31
+ function updateConfigFile(filePath) {
32
+ let content;
33
+ try {
34
+ content = fs.readFileSync(filePath, 'utf8');
35
+ } catch (e) {
36
+ console.error(chalk.red(` ✖ Failed to read ${filePath}:`), e.message);
37
+ throw new Error(`Failed to read ${filePath}`);
38
+ }
39
+
40
+ const relativePath = path.relative(process.cwd(), filePath);
41
+ console.log(chalk.dim(` • Processing: `) + relativePath);
42
+
43
+ const { userVal, keyVal } = parseCredentialsFromContent(content);
44
+
45
+ let relativeSdkPath = path.relative(path.dirname(filePath), sdkPath);
46
+ relativeSdkPath = relativeSdkPath.replace(/\\/g, '/');
47
+
48
+ const serviceEntry = `[path.join(__dirname, '${relativeSdkPath}'), { user: ${userVal}, key: ${keyVal} }]`;
49
+
50
+ if (content.includes('wdio-lambdatest-service')) {
51
+ const emptyOptionsRegex = /\[path\.join\(__dirname, '.*?wdio-lambdatest-service'\), \{\}\]/;
52
+ if (emptyOptionsRegex.test(content)) {
53
+ console.log(chalk.yellow(` ⚠ Fixing missing credentials in: `) + relativePath);
54
+ content = content.replace(emptyOptionsRegex, serviceEntry);
55
+ content = content.replace(/logLevel: ['"]info['"]/, "logLevel: 'error'");
56
+ try {
57
+ fs.writeFileSync(filePath, content);
58
+ } catch (e) {
59
+ console.error(chalk.red(` ✖ Failed to write ${filePath}:`), e.message);
60
+ throw new Error(`Failed to write ${filePath}`);
61
+ }
62
+ console.log(chalk.green(` ✔ Successfully repaired `) + relativePath);
63
+ } else {
64
+ console.log(chalk.dim(` - Skipping (already correct): `) + relativePath);
65
+ }
66
+ return;
67
+ }
68
+
69
+ console.log(chalk.cyan(` → Updating: `) + relativePath);
70
+
71
+ if (!content.includes("require('path')")) {
72
+ content = "const path = require('path');\n" + content;
73
+ }
74
+
75
+ if (content.includes('services: [')) {
76
+ content = content.replace('services: [', `services: [\n ${serviceEntry},`);
77
+ } else {
78
+ content = content.replace('exports.config = {', `exports.config = {\n services: [\n ${serviceEntry}\n ],`);
79
+ }
80
+
81
+ if (content.includes('logLevel:')) {
82
+ content = content.replace(/logLevel:.*,/, "logLevel: 'error',");
83
+ } else {
84
+ content = content.replace('exports.config = {', `exports.config = {\n logLevel: 'error',`);
85
+ }
86
+
87
+ content = content.replace(/^(\s*)user:/gm, '$1// user:');
88
+ content = content.replace(/^(\s*)key:/gm, '$1// key:');
89
+
90
+ try {
91
+ fs.writeFileSync(filePath, content);
92
+ } catch (e) {
93
+ console.error(chalk.red(` ✖ Failed to write ${filePath}:`), e.message);
94
+ throw new Error(`Failed to write ${filePath}`);
95
+ }
96
+ console.log(chalk.green(` ✔ Successfully updated `) + relativePath);
97
+ }
98
+
99
+ function traverseDirectory(dir) {
100
+ if (!fs.existsSync(dir)) {
101
+ console.error(chalk.red(`✖ Directory not found: ${dir}`));
102
+ throw new Error(`Directory not found: ${dir}`);
103
+ }
104
+
105
+ const files = fs.readdirSync(dir);
106
+ for (const file of files) {
107
+ const fullPath = path.join(dir, file);
108
+ let stat;
109
+ try {
110
+ stat = fs.statSync(fullPath);
111
+ } catch (e) {
112
+ console.error(chalk.dim(` ! Cannot read ${fullPath}: ${e.message}`));
113
+ continue;
114
+ }
115
+ if (stat.isDirectory()) {
116
+ if (file !== 'node_modules' && file !== '.git') {
117
+ traverseDirectory(fullPath);
118
+ }
119
+ } else if (file.endsWith('.conf.js')) {
120
+ updateConfigFile(fullPath);
121
+ }
122
+ }
123
+ }
124
+
125
+ function printBanner() {
126
+ console.log();
127
+ console.log(chalk.cyan.bold('╭─────────────────────────────────────╮'));
128
+ console.log(chalk.cyan.bold('│') + ' 🚀 ' + chalk.bold('LambdaTest WDIO Setup') + ' ' + chalk.cyan.bold('│'));
129
+ console.log(chalk.cyan.bold('╰─────────────────────────────────────╯'));
130
+ console.log();
131
+ }
132
+
133
+ /**
134
+ * Run the setup command.
135
+ * @param {string|undefined} targetPath - Path to directory containing .conf.js files. If undefined, prompts interactively.
136
+ * @returns {Promise<void>}
137
+ */
138
+ async function runSetup(targetPath) {
139
+ printBanner();
140
+
141
+ let resolvedPath;
142
+
143
+ if (targetPath) {
144
+ resolvedPath = path.resolve(targetPath.trim());
145
+ } else {
146
+ // Interactive mode using inquirer
147
+ const answers = await inquirer.default.prompt([
148
+ {
149
+ type: 'input',
150
+ name: 'path',
151
+ message: 'Where are your test scripts located?',
152
+ default: './android-sample',
153
+ validate: (input) => {
154
+ if (!input || input.trim() === '') {
155
+ return 'Please enter a valid path';
156
+ }
157
+ return true;
158
+ }
159
+ }
160
+ ]);
161
+ resolvedPath = path.resolve(answers.path.trim());
162
+ }
163
+
164
+ console.log(chalk.cyan('→ Scanning:'), chalk.dim(resolvedPath));
165
+ console.log();
166
+
167
+ traverseDirectory(resolvedPath);
168
+
169
+ console.log();
170
+ console.log(chalk.green.bold('✔ Done scanning directories!'));
171
+ console.log();
172
+ }
173
+
174
+ module.exports = { runSetup };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared style utilities using Chalk for CLI output.
3
+ * Provides consistent, beautiful terminal styling.
4
+ */
5
+ const chalk = require('chalk');
6
+
7
+ const style = {
8
+ // Colors
9
+ reset: chalk.reset,
10
+ bold: chalk.bold,
11
+ dim: chalk.dim,
12
+ cyan: chalk.cyan,
13
+ green: chalk.green,
14
+ yellow: chalk.yellow,
15
+ red: chalk.red,
16
+ blue: chalk.blue,
17
+ magenta: chalk.magenta,
18
+ gray: chalk.gray,
19
+ white: chalk.white,
20
+
21
+ // Combinations
22
+ success: chalk.green.bold,
23
+ error: chalk.red.bold,
24
+ warning: chalk.yellow.bold,
25
+ info: chalk.cyan.bold,
26
+ highlight: chalk.cyan,
27
+ muted: chalk.dim,
28
+
29
+ // Symbols
30
+ symbols: {
31
+ success: chalk.green('✔'),
32
+ error: chalk.red('✖'),
33
+ warning: chalk.yellow('⚠'),
34
+ info: chalk.cyan('ℹ'),
35
+ arrow: chalk.cyan('→'),
36
+ bullet: chalk.dim('•')
37
+ },
38
+
39
+ // Helper functions
40
+ title: (text) => chalk.cyan.bold(text),
41
+ subtitle: (text) => chalk.dim(text),
42
+ label: (text) => chalk.bold(text),
43
+ value: (text) => chalk.dim(text),
44
+ path: (text) => chalk.dim(text),
45
+ command: (text) => chalk.green(text),
46
+
47
+ // Box drawing
48
+ box: (title) => {
49
+ const line = '─'.repeat(title.length + 4);
50
+ return `${chalk.cyan('┌' + line + '┐')}\n${chalk.cyan('│')} ${chalk.bold(title)} ${chalk.cyan('│')}\n${chalk.cyan('└' + line + '┘')}`;
51
+ },
52
+
53
+ // Banner
54
+ banner: (text) => chalk.cyan.bold(`\n--- ${text} ---\n`)
55
+ };
56
+
57
+ module.exports = style;
package/package.json CHANGED
@@ -1,9 +1,19 @@
1
1
  {
2
2
  "name": "wdio-lambdatest-service-sdk",
3
- "version": "5.0.0",
4
- "description": "WebdriverIO service for LambdaTest integration",
3
+ "version": "5.1.0",
4
+ "description": "WebdriverIO service and CLI for LambdaTest Appium & browser automation",
5
5
  "main": "index.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/LambdaTest/wdio-lambdatest-service"
9
+ },
10
+ "homepage": "https://github.com/LambdaTest/wdio-lambdatest-service#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/LambdaTest/wdio-lambdatest-service/issues"
13
+ },
6
14
  "bin": {
15
+ "wdio-lt": "bin/cli.js",
16
+ "wdio-lambdatest": "bin/cli.js",
7
17
  "wdio-lambdatest-setup": "bin/setup.js",
8
18
  "wdio-lambdatest-generator": "bin/generate-config.js"
9
19
  },
@@ -15,15 +25,25 @@
15
25
  "wdio",
16
26
  "lambdatest",
17
27
  "service",
18
- "sdk"
28
+ "sdk",
29
+ "cli",
30
+ "bun",
31
+ "commander",
32
+ "inquirer"
19
33
  ],
20
34
  "author": "",
21
35
  "license": "ISC",
36
+ "dependencies": {
37
+ "chalk": "4",
38
+ "commander": "^14.0.2",
39
+ "inquirer": "^13.2.2"
40
+ },
22
41
  "peerDependencies": {
23
42
  "@wdio/cli": ">=5.0.0",
24
43
  "webdriverio": ">=5.0.0"
25
44
  },
26
45
  "engines": {
27
- "node": ">=10.0.0"
46
+ "node": ">=18.0.0",
47
+ "bun": ">=1.0.0"
28
48
  }
29
49
  }
package/src/launcher.js CHANGED
@@ -1,3 +1,48 @@
1
+ const LT_OPTIONS_KEY = 'lt:options';
2
+
3
+ /**
4
+ * Resolve credentials from service options and env. Prefers options.user/key, then username/accessKey, then env.
5
+ * @param {object} options - Service options (user, key, username, accessKey)
6
+ * @param {object} config - WDIO config (may already have user/key from elsewhere)
7
+ * @returns {{ user: string|undefined, key: string|undefined }}
8
+ */
9
+ function resolveCredentials(options, config) {
10
+ let user = options.user || options.username || config.user;
11
+ let key = options.key || options.accessKey || config.key;
12
+ if (!user) user = process.env.LT_USERNAME;
13
+ if (!key) key = process.env.LT_ACCESS_KEY;
14
+ return { user, key };
15
+ }
16
+
17
+ /**
18
+ * Apply credentials to WDIO config (for Basic Auth).
19
+ * @param {object} config - WDIO config (mutated)
20
+ * @param {{ user: string|undefined, key: string|undefined }} credentials
21
+ */
22
+ function applyCredentialsToConfig(config, credentials) {
23
+ if (credentials.user) config.user = credentials.user;
24
+ if (credentials.key) config.key = credentials.key;
25
+ }
26
+
27
+ /**
28
+ * Apply credentials to each capability (JSONWP and W3C lt:options).
29
+ * @param {Array<object>} capabilities - List of capability objects (mutated)
30
+ * @param {{ user: string|undefined, key: string|undefined }} credentials
31
+ */
32
+ function applyCredentialsToCapabilities(capabilities, credentials) {
33
+ const { user, key } = credentials;
34
+ if (!user || !key) return;
35
+
36
+ capabilities.forEach((cap) => {
37
+ if (!cap.user) cap.user = user;
38
+ if (!cap.accessKey) cap.accessKey = key;
39
+ const ltOpts = cap[LT_OPTIONS_KEY];
40
+ if (ltOpts) {
41
+ if (!ltOpts.username) ltOpts.username = user;
42
+ if (!ltOpts.accessKey) ltOpts.accessKey = key;
43
+ }
44
+ });
45
+ }
1
46
 
2
47
  module.exports = class LambdaTestLauncher {
3
48
  constructor(serviceOptions, capabilities, config) {
@@ -8,35 +53,13 @@ module.exports = class LambdaTestLauncher {
8
53
  onPrepare(config, capabilities) {
9
54
  console.log('[LambdaTest Service] Processing credentials...');
10
55
 
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
- });
56
+ const credentials = resolveCredentials(this.options, config);
57
+ applyCredentialsToConfig(config, credentials);
58
+
59
+ const capsList = Array.isArray(capabilities) ? capabilities : [capabilities];
60
+ applyCredentialsToCapabilities(capsList, credentials);
61
+
62
+ if (credentials.user && credentials.key) {
40
63
  console.log('[LambdaTest Service] Credentials injected into config and capabilities.');
41
64
  } else {
42
65
  console.error('[LambdaTest Service] Credentials not found! Please check service options or env vars.');
package/src/service.js CHANGED
@@ -1,4 +1,16 @@
1
+ const LOG_TAG = '[LambdaTest SDK]';
2
+ const COLORS = {
3
+ reset: '\x1b[0m',
4
+ green: '\x1b[32m',
5
+ red: '\x1b[31m',
6
+ cyan: '\x1b[36m',
7
+ gray: '\x1b[90m'
8
+ };
1
9
 
10
+ /**
11
+ * WebdriverIO service for LambdaTest: updates test status on the LambdaTest platform
12
+ * after each test (Mocha/Jasmine) or scenario (Cucumber).
13
+ */
2
14
  module.exports = class LambdaTestService {
3
15
  constructor(serviceOptions, capabilities, config) {
4
16
  this.options = serviceOptions || {};
@@ -10,44 +22,40 @@ module.exports = class LambdaTestService {
10
22
  }
11
23
 
12
24
  /**
13
- * Triggered after each test (Mocha/Jasmine)
25
+ * WDIO lifecycle: after each test (Mocha/Jasmine). Handles v5/v6/v7 result shape differences.
14
26
  */
15
27
  async afterTest(test, context, result) {
16
- // Handle WDIO v5/v6/v7 differences
17
28
  let passed = false;
18
- let error = undefined;
29
+ let error;
19
30
  let title = 'Test';
20
31
 
21
- // Check if result is the object { error, result, duration, passed, retries }
22
32
  if (result && typeof result.passed === 'boolean') {
23
33
  passed = result.passed;
24
34
  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') {
35
+ } else if (test && typeof test.passed === 'boolean') {
28
36
  passed = test.passed;
29
37
  error = test.error;
30
38
  }
31
-
32
39
  if (test && test.title) title = test.title;
33
40
 
34
- await this.updateStatus(passed, error ? (error.message || error) : undefined, title);
41
+ const errorMessage = error ? (error.message || error) : undefined;
42
+ await this.updateStatus(passed, errorMessage, title);
35
43
  }
36
44
 
37
45
  /**
38
- * Triggered after each scenario (Cucumber)
46
+ * WDIO lifecycle: after each scenario (Cucumber).
39
47
  */
40
48
  async afterScenario(uri, feature, scenario, result) {
41
49
  const isPassed = result.status === 'passed' || result === 'passed' || result.passed === true;
42
- const errorMessage = result.error ? result.error : (isPassed ? undefined : 'Scenario Failed');
50
+ const errorMessage = getScenarioErrorMessage(result, isPassed);
43
51
  const title = scenario.name || 'Scenario';
44
52
  await this.updateStatus(isPassed, errorMessage, title);
45
53
  }
46
54
 
47
55
  /**
48
- * Updates the test status on LambdaTest
49
- * @param {boolean} passed
50
- * @param {string} errorMessage
56
+ * Updates the test status on LambdaTest via browser hook.
57
+ * @param {boolean} passed
58
+ * @param {string|undefined} errorMessage
51
59
  * @param {string} testTitle
52
60
  */
53
61
  async updateStatus(passed, errorMessage, testTitle) {
@@ -55,43 +63,31 @@ module.exports = class LambdaTestService {
55
63
 
56
64
  const status = passed ? 'passed' : 'failed';
57
65
  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;
66
+ const color = passed ? COLORS.green : COLORS.red;
67
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}`);
68
+
69
+ console.log(`\n${COLORS.cyan}${LOG_TAG}${COLORS.reset} ${COLORS.gray}Updating Status...${COLORS.reset}`);
70
+ console.log(`${COLORS.cyan}${LOG_TAG}${COLORS.reset} Test: ${testTitle}`);
71
+ console.log(`${COLORS.cyan}${LOG_TAG}${COLORS.reset} Status: ${color}${status.toUpperCase()} ${icon}${COLORS.reset}`);
72
72
  if (!passed) {
73
- console.log(`${cyan}[LambdaTest SDK]${reset} Reason: ${red}${remark}${reset}`);
73
+ console.log(`${COLORS.cyan}${LOG_TAG}${COLORS.reset} Reason: ${COLORS.red}${remark}${COLORS.reset}`);
74
74
  }
75
- console.log(''); // Empty line
75
+ console.log('');
76
76
 
77
77
  try {
78
- // Using browser.execute to trigger the Lambda Hook
79
78
  const script = `lambda-hook: ${JSON.stringify({
80
79
  action: 'setTestStatus',
81
- arguments: {
82
- status: status,
83
- remark: remark
84
- }
80
+ arguments: { status, remark }
85
81
  })}`;
86
-
87
- // Await the execution in case we are in async mode
88
82
  await browser.execute(script);
89
-
90
- // Add a small pause to ensure the hook is processed before session ends
91
83
  await browser.pause(1000);
92
-
93
84
  } catch (e) {
94
- console.error(`${red}[LambdaTest SDK] Failed to update test status via hook.${reset}`, e);
85
+ console.error(`${COLORS.red}${LOG_TAG} Failed to update test status via hook.${COLORS.reset}`, e);
95
86
  }
96
87
  }
97
88
  };
89
+
90
+ function getScenarioErrorMessage(result, isPassed) {
91
+ if (result.error) return result.error;
92
+ return isPassed ? undefined : 'Scenario Failed';
93
+ }