wdio-lambdatest-service-sdk 5.0.0 → 5.1.1

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/index.js CHANGED
@@ -1,11 +1,10 @@
1
+ /**
2
+ * WDIO LambdaTest service entry point.
3
+ * Exposes the service class and launcher for WebdriverIO.
4
+ */
1
5
  const LambdaTestService = require('./src/service');
2
6
  const LambdaTestLauncher = require('./src/launcher');
3
7
 
4
- // Export the Service class as the main module export
5
8
  module.exports = LambdaTestService;
6
-
7
- // Also export as 'default' for compatibility with some WDIO loaders
8
9
  module.exports.default = LambdaTestService;
9
-
10
- // Export the Launcher
11
10
  module.exports.launcher = LambdaTestLauncher;
@@ -0,0 +1,526 @@
1
+ /**
2
+ * Generate command logic: Interactively create a WDIO config for LambdaTest.
3
+ */
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const chalk = require('chalk');
7
+ const inquirer = require('inquirer');
8
+
9
+ /** Safe filename: non-empty, no path separators or reserved chars. */
10
+ function toSafeFilename(value, fallback) {
11
+ if (!value || typeof value !== 'string') return fallback;
12
+ const sanitized = value.replace(/[/\\?*:|<>"]/g, '').trim();
13
+ return sanitized || fallback;
14
+ }
15
+
16
+ /**
17
+ * Find wdio binary path relative to outputDir. Checks root node_modules then android-sample.
18
+ * @param {string} outDir - Absolute path to LT_Test (output dir)
19
+ * @returns {string} Relative path to wdio binary
20
+ */
21
+ function findWdioBinRelative(outDir) {
22
+ const rootWdio = path.resolve(outDir, '../node_modules/.bin/wdio');
23
+ const androidSampleWdio = path.resolve(outDir, '../android-sample/node_modules/.bin/wdio');
24
+ if (fs.existsSync(rootWdio)) return '../node_modules/.bin/wdio';
25
+ if (fs.existsSync(androidSampleWdio)) return '../android-sample/node_modules/.bin/wdio';
26
+ return '../node_modules/.bin/wdio';
27
+ }
28
+
29
+ /**
30
+ * Device configuration defaults for LambdaTest.
31
+ */
32
+ const DEVICE_DEFAULTS = {
33
+ // Virtual Device defaults
34
+ virtual: {
35
+ android: {
36
+ deviceName: 'Galaxy S24 Ultra',
37
+ platformVersion: '14',
38
+ appiumVersion: '2.16.2'
39
+ },
40
+ ios: {
41
+ deviceName: 'iPad (10th generation)',
42
+ platformVersion: '16',
43
+ appiumVersion: '2.1.3'
44
+ }
45
+ },
46
+ // Real Device defaults
47
+ real: {
48
+ android: {
49
+ deviceName: 'Pixel 8',
50
+ platformVersion: '14'
51
+ },
52
+ ios: {
53
+ deviceName: 'iPhone 15',
54
+ platformVersion: '17'
55
+ }
56
+ }
57
+ };
58
+
59
+ /**
60
+ * Get default App ID based on platform.
61
+ */
62
+ function getDefaultAppId(platform) {
63
+ const envAppId = process.env.LT_APP_ID;
64
+ if (envAppId) return envAppId;
65
+ return platform === 'ios' ? 'lt://proverbial-ios' : 'lt://proverbial-android';
66
+ }
67
+
68
+ /**
69
+ * Build a single capability object.
70
+ */
71
+ function buildSingleCapability(deviceType, platform, testingType, appId) {
72
+ const isReal = deviceType === 'real';
73
+ const platformName = platform === 'ios' ? 'ios' : 'android';
74
+ const defaults = DEVICE_DEFAULTS[deviceType][platform];
75
+
76
+ if (testingType === 'app') {
77
+ // Native Application Testing
78
+ if (isReal) {
79
+ return {
80
+ 'lt:options': {
81
+ platformName: platformName,
82
+ deviceName: defaults.deviceName,
83
+ platformVersion: defaults.platformVersion,
84
+ app: appId,
85
+ isRealMobile: true
86
+ }
87
+ };
88
+ } else {
89
+ return {
90
+ 'lt:options': {
91
+ platformName: platformName,
92
+ deviceName: defaults.deviceName,
93
+ appiumVersion: defaults.appiumVersion,
94
+ platformVersion: defaults.platformVersion,
95
+ app: appId,
96
+ isRealMobile: false
97
+ }
98
+ };
99
+ }
100
+ } else {
101
+ // Website (Mobile Browser) Testing
102
+ const browserName = platform === 'ios' ? 'Safari' : 'Chrome';
103
+ if (isReal) {
104
+ return {
105
+ browserName: browserName,
106
+ 'lt:options': {
107
+ platformName: platformName,
108
+ deviceName: defaults.deviceName,
109
+ platformVersion: defaults.platformVersion,
110
+ isRealMobile: true
111
+ }
112
+ };
113
+ } else {
114
+ return {
115
+ browserName: browserName,
116
+ 'lt:options': {
117
+ platformName: platformName,
118
+ deviceName: defaults.deviceName,
119
+ appiumVersion: defaults.appiumVersion,
120
+ platformVersion: defaults.platformVersion,
121
+ isRealMobile: false
122
+ }
123
+ };
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Build capabilities array from multi-select config.
130
+ * Generates all combinations of selected options.
131
+ */
132
+ function buildCapabilitiesMulti(config) {
133
+ const capabilities = [];
134
+
135
+ // App Testing combinations
136
+ if (config.testTypes.includes('app')) {
137
+ for (const deviceType of config.deviceTypes) {
138
+ for (const platform of config.platforms) {
139
+ for (const testingType of config.testingTypes) {
140
+ const appId = testingType === 'app' ? (config.appIds[platform] || getDefaultAppId(platform)) : null;
141
+ capabilities.push(buildSingleCapability(deviceType, platform, testingType, appId));
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ // Browser Testing (Desktop Website)
148
+ if (config.testTypes.includes('browser')) {
149
+ capabilities.push({
150
+ browserName: 'Chrome',
151
+ browserVersion: 'latest',
152
+ 'LT:Options': {
153
+ username: config.username,
154
+ accessKey: config.key,
155
+ platformName: 'Windows 10',
156
+ project: config.testName,
157
+ w3c: true,
158
+ plugin: 'node_js-webdriverio'
159
+ }
160
+ });
161
+ }
162
+
163
+ return capabilities;
164
+ }
165
+
166
+ /**
167
+ * Build config file template string.
168
+ */
169
+ function buildConfigTemplate(config, commonCaps, capabilities) {
170
+ return `const path = require('path');
171
+
172
+ exports.config = {
173
+ // Authentication handled by SDK
174
+ // user: "${config.username}",
175
+ // key: "${config.key}",
176
+
177
+ updateJob: false,
178
+ specs: ["${config.specPath}"],
179
+ exclude: [],
180
+
181
+ maxInstances: ${config.parallel > 0 ? config.parallel : 1},
182
+
183
+ commonCapabilities: ${JSON.stringify(commonCaps, null, 4)},
184
+
185
+ capabilities: ${JSON.stringify(capabilities, null, 4)},
186
+
187
+ logLevel: 'error',
188
+ coloredLogs: true,
189
+ screenshotPath: "./errorShots/",
190
+ baseUrl: "https://mobile-hub.lambdatest.com",
191
+ waitforTimeout: 10000,
192
+ connectionRetryTimeout: 90000,
193
+ connectionRetryCount: 3,
194
+ path: "/wd/hub",
195
+ hostname: "mobile-hub.lambdatest.com",
196
+ port: 80,
197
+
198
+ services: [
199
+ [path.join(__dirname, '../wdio-lambdatest-service'), {
200
+ user: "${config.username}",
201
+ key: "${config.key}"
202
+ }]
203
+ ],
204
+
205
+ framework: "mocha",
206
+ mochaOpts: {
207
+ ui: "bdd",
208
+ timeout: 20000,
209
+ },
210
+ };
211
+
212
+ // Merge commonCapabilities into each capability
213
+ exports.config.capabilities.forEach(function (caps) {
214
+ for (const i in exports.config.commonCapabilities) {
215
+ caps[i] = caps[i] || exports.config.commonCapabilities[i];
216
+ }
217
+ });
218
+ `;
219
+ }
220
+
221
+ function printBanner() {
222
+ console.log();
223
+ console.log(chalk.cyan.bold('╭────────────────────────────────────────────╮'));
224
+ console.log(chalk.cyan.bold('│') + ' 🛠️ ' + chalk.bold('LambdaTest WDIO Config Generator') + ' ' + chalk.cyan.bold('│'));
225
+ console.log(chalk.cyan.bold('╰────────────────────────────────────────────╯'));
226
+ console.log();
227
+ }
228
+
229
+ /**
230
+ * Run the generate command interactively.
231
+ * @returns {Promise<void>}
232
+ */
233
+ async function runGenerate() {
234
+ const outputDir = path.resolve(process.cwd(), 'LT_Test');
235
+
236
+ if (!fs.existsSync(outputDir)) {
237
+ fs.mkdirSync(outputDir, { recursive: true });
238
+ }
239
+
240
+ printBanner();
241
+
242
+ const defaultUsername = process.env.LT_USERNAME || '';
243
+ const defaultKey = process.env.LT_ACCESS_KEY || '';
244
+
245
+ // Basic Info
246
+ console.log(chalk.dim('─'.repeat(40)));
247
+ console.log(chalk.cyan.bold(' 📋 Basic Info'));
248
+ console.log(chalk.dim('─'.repeat(40)));
249
+ console.log();
250
+
251
+ const basicAnswers = await inquirer.default.prompt([
252
+ {
253
+ type: 'input',
254
+ name: 'testName',
255
+ message: 'Test name:',
256
+ default: 'LambdaTest',
257
+ validate: (input) => input.trim() !== '' || 'Test name is required'
258
+ }
259
+ ]);
260
+
261
+ const config = {
262
+ testName: toSafeFilename(basicAnswers.testName, 'LambdaTest'),
263
+ filename: '',
264
+ username: '',
265
+ key: '',
266
+ testTypes: [], // ['app', 'browser']
267
+ deviceTypes: [], // ['real', 'virtual']
268
+ platforms: [], // ['android', 'ios']
269
+ testingTypes: [], // ['app', 'website']
270
+ appIds: {}, // { android: 'lt://...', ios: 'lt://...' }
271
+ parallel: 0,
272
+ specPath: ''
273
+ };
274
+ config.filename = `${config.testName}.conf.js`;
275
+
276
+ // Credentials
277
+ console.log();
278
+ console.log(chalk.dim('─'.repeat(40)));
279
+ console.log(chalk.cyan.bold(' 🔐 LambdaTest Credentials'));
280
+ console.log(chalk.dim('─'.repeat(40)));
281
+ console.log();
282
+
283
+ const credAnswers = await inquirer.default.prompt([
284
+ {
285
+ type: 'input',
286
+ name: 'username',
287
+ message: 'LambdaTest Username:',
288
+ default: defaultUsername || undefined,
289
+ validate: (input) => input.trim() !== '' || 'Username is required'
290
+ },
291
+ {
292
+ type: 'password',
293
+ name: 'key',
294
+ message: 'LambdaTest Access Key:',
295
+ default: defaultKey || undefined,
296
+ mask: '*',
297
+ validate: (input) => input.trim() !== '' || 'Access key is required'
298
+ }
299
+ ]);
300
+
301
+ config.username = credAnswers.username;
302
+ config.key = credAnswers.key;
303
+
304
+ // Test Configuration
305
+ console.log();
306
+ console.log(chalk.dim('─'.repeat(40)));
307
+ console.log(chalk.cyan.bold(' ⚙️ Test Configuration'));
308
+ console.log(chalk.dim('─' .repeat(40)));
309
+ console.log(chalk.dim(' (Use space to select, enter to confirm)'));
310
+ console.log();
311
+
312
+ // Step 1: Test Type (Multi-select)
313
+ const testTypeAnswer = await inquirer.default.prompt([
314
+ {
315
+ type: 'checkbox',
316
+ name: 'testTypes',
317
+ message: 'Test type(s):',
318
+ choices: [
319
+ { name: '📱 Mobile Testing ', value: 'app', checked: true },
320
+ { name: '🌐 Browser Testing (Desktop)', value: 'browser' }
321
+ ],
322
+ validate: (input) => input.length > 0 || 'Please select at least one test type'
323
+ }
324
+ ]);
325
+ config.testTypes = testTypeAnswer.testTypes;
326
+
327
+ // If App Testing is selected, ask for more details
328
+ if (config.testTypes.includes('app')) {
329
+ console.log();
330
+ console.log(chalk.dim('─'.repeat(40)));
331
+ console.log(chalk.cyan.bold(' 📱 App Testing Configuration'));
332
+ console.log(chalk.dim('─'.repeat(40)));
333
+ console.log();
334
+
335
+ // Step 2: Device Type (Multi-select)
336
+ const deviceAnswer = await inquirer.default.prompt([
337
+ {
338
+ type: 'checkbox',
339
+ name: 'deviceTypes',
340
+ message: 'Device type(s):',
341
+ choices: [
342
+ { name: '📲 Mobile Device', value: 'real', checked: true },
343
+ { name: '💻 Website (Desktop)', value: 'virtual' }
344
+ ],
345
+ validate: (input) => input.length > 0 || 'Please select at least one device type'
346
+ }
347
+ ]);
348
+ config.deviceTypes = deviceAnswer.deviceTypes;
349
+
350
+ // Step 3: Platform (Multi-select)
351
+ const platformAnswer = await inquirer.default.prompt([
352
+ {
353
+ type: 'checkbox',
354
+ name: 'platforms',
355
+ message: 'Platform(s):',
356
+ choices: [
357
+ { name: '🤖 Android', value: 'android', checked: true },
358
+ { name: '🍎 iOS', value: 'ios' }
359
+ ],
360
+ validate: (input) => input.length > 0 || 'Please select at least one platform'
361
+ }
362
+ ]);
363
+ config.platforms = platformAnswer.platforms;
364
+
365
+ // Step 4: What to test (Multi-select)
366
+ const testingTypeAnswer = await inquirer.default.prompt([
367
+ {
368
+ type: 'checkbox',
369
+ name: 'testingTypes',
370
+ message: 'What do you want to test?',
371
+ choices: [
372
+ { name: '📦 Application (Native App)', value: 'app', checked: true },
373
+ { name: '🌐 Website (Mobile Browser)', value: 'website' }
374
+ ],
375
+ validate: (input) => input.length > 0 || 'Please select at least one testing type'
376
+ }
377
+ ]);
378
+ config.testingTypes = testingTypeAnswer.testingTypes;
379
+
380
+ // Step 5: App IDs (only if testing native applications)
381
+ if (config.testingTypes.includes('app')) {
382
+ console.log();
383
+ console.log(chalk.dim('─'.repeat(40)));
384
+ console.log(chalk.cyan.bold(' 📦 App Configuration'));
385
+ console.log(chalk.dim('─'.repeat(40)));
386
+ console.log();
387
+
388
+ // Ask for App ID for each selected platform
389
+ for (const platform of config.platforms) {
390
+ const platformLabel = platform === 'ios' ? 'iOS' : 'Android';
391
+ const defaultAppId = getDefaultAppId(platform);
392
+
393
+ const appIdAnswer = await inquirer.default.prompt([
394
+ {
395
+ type: 'input',
396
+ name: 'appId',
397
+ message: `${platformLabel} App ID (e.g. lt://APP123456789):`,
398
+ default: defaultAppId,
399
+ validate: (input) => input.trim() !== '' || 'App ID is required'
400
+ }
401
+ ]);
402
+ config.appIds[platform] = appIdAnswer.appId.trim();
403
+ }
404
+ }
405
+ }
406
+
407
+ // Parallel threads
408
+ console.log();
409
+ console.log(chalk.dim('─'.repeat(40)));
410
+ console.log(chalk.cyan.bold(' 🚀 Execution Settings'));
411
+ console.log(chalk.dim('─'.repeat(40)));
412
+ console.log();
413
+
414
+ const parallelAnswer = await inquirer.default.prompt([
415
+ {
416
+ type: 'number',
417
+ name: 'parallel',
418
+ message: 'Parallel threads (0 for sequential):',
419
+ default: 0,
420
+ validate: (input) => {
421
+ const num = parseInt(input, 10);
422
+ return (!isNaN(num) && num >= 0) || 'Please enter a valid number (0 or greater)';
423
+ }
424
+ }
425
+ ]);
426
+ config.parallel = parallelAnswer.parallel || 0;
427
+
428
+ // Spec file path
429
+ const specAnswer = await inquirer.default.prompt([
430
+ {
431
+ type: 'input',
432
+ name: 'specPath',
433
+ message: 'Spec file path:',
434
+ default: './specs/test.js'
435
+ }
436
+ ]);
437
+
438
+ let rawSpec = specAnswer.specPath.trim() || './specs/test.js';
439
+
440
+ // Smart resolve spec path relative to LT_Test
441
+ if (fs.existsSync(path.resolve(outputDir, rawSpec))) {
442
+ config.specPath = rawSpec;
443
+ } else {
444
+ const rootPath = path.resolve(outputDir, '..');
445
+ const relativeToRoot = rawSpec.replace(/^\.\//, '');
446
+ const pathFromRoot = path.join(rootPath, relativeToRoot);
447
+
448
+ if (fs.existsSync(pathFromRoot)) {
449
+ config.specPath = path.relative(outputDir, pathFromRoot).replace(/\\/g, '/');
450
+ } else {
451
+ if (!path.isAbsolute(rawSpec) && !rawSpec.startsWith('../')) {
452
+ config.specPath = '../' + relativeToRoot;
453
+ } else {
454
+ config.specPath = rawSpec;
455
+ }
456
+ }
457
+ }
458
+
459
+ const commonCaps = {
460
+ build: `LT_WDIO_${config.testName}_${new Date().toISOString().split('T')[0]}`,
461
+ name: config.testName,
462
+ visual: true,
463
+ console: true
464
+ };
465
+
466
+ const capabilities = buildCapabilitiesMulti(config);
467
+ const template = buildConfigTemplate(config, commonCaps, capabilities);
468
+
469
+ const outputPath = path.join(outputDir, config.filename);
470
+ fs.writeFileSync(outputPath, template);
471
+
472
+ const packageJsonPath = path.join(outputDir, 'package.json');
473
+ let pkg = { name: 'lt-generated-tests', version: '1.0.0', scripts: {} };
474
+ if (fs.existsSync(packageJsonPath)) {
475
+ try {
476
+ pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
477
+ } catch (e) {
478
+ // Ignore parse errors, use default
479
+ }
480
+ }
481
+ if (!pkg.scripts || typeof pkg.scripts !== 'object') {
482
+ pkg.scripts = {};
483
+ }
484
+
485
+ const wdioBin = findWdioBinRelative(outputDir);
486
+ pkg.scripts[`test:${config.testName}`] = `${wdioBin} ${config.filename}`;
487
+ fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2));
488
+
489
+ // Success output
490
+ console.log();
491
+ console.log(chalk.dim('─'.repeat(40)));
492
+ console.log(chalk.green.bold(' ✔ Configuration Created Successfully!'));
493
+ console.log(chalk.dim('─'.repeat(40)));
494
+ console.log();
495
+
496
+ // Show summary of selected options
497
+ console.log(chalk.cyan(' Summary:'));
498
+ console.log(chalk.dim(' ─────────────────────────'));
499
+ console.log(` ${chalk.bold('Test Types:')} ${config.testTypes.map(t => t === 'app' ? 'App Testing' : 'Browser Testing').join(', ')}`);
500
+
501
+ if (config.testTypes.includes('app')) {
502
+ console.log(` ${chalk.bold('Device Types:')} ${config.deviceTypes.map(d => d === 'real' ? 'Real Device' : 'Virtual Device').join(', ')}`);
503
+ console.log(` ${chalk.bold('Platforms:')} ${config.platforms.map(p => p === 'ios' ? 'iOS' : 'Android').join(', ')}`);
504
+ console.log(` ${chalk.bold('Testing:')} ${config.testingTypes.map(t => t === 'app' ? 'Native App' : 'Website').join(', ')}`);
505
+
506
+ if (config.testingTypes.includes('app')) {
507
+ for (const platform of config.platforms) {
508
+ const platformLabel = platform === 'ios' ? 'iOS' : 'Android';
509
+ console.log(` ${chalk.bold(`${platformLabel} App ID:`)} ${chalk.dim(config.appIds[platform])}`);
510
+ }
511
+ }
512
+ }
513
+
514
+ console.log(` ${chalk.bold('Capabilities:')} ${chalk.yellow(capabilities.length)} configuration(s) generated`);
515
+ console.log();
516
+
517
+ console.log(chalk.cyan(' Config file: ') + chalk.dim(outputPath));
518
+ console.log(chalk.cyan(' Package.json: ') + chalk.dim(packageJsonPath));
519
+ console.log();
520
+ console.log(chalk.bold(' To run your test:'));
521
+ console.log();
522
+ console.log(chalk.green(` cd LT_Test && npm run test:${config.testName}`));
523
+ console.log();
524
+ }
525
+
526
+ module.exports = { runGenerate };