@ash-mallick/browserstack-sync 1.1.2 → 1.1.3
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/lib/ai-analyzer.js +241 -56
- package/lib/index.js +36 -24
- package/package.json +1 -1
package/lib/ai-analyzer.js
CHANGED
|
@@ -14,44 +14,133 @@ const OLLAMA_HOST = process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
|
14
14
|
/**
|
|
15
15
|
* System prompt for the AI to understand what we want.
|
|
16
16
|
*/
|
|
17
|
-
const SYSTEM_PROMPT = `You are a test
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
const SYSTEM_PROMPT = `You are a QA expert creating detailed manual test steps from automated test code. Your goal is to write steps so clear and detailed that ANY tester can execute the test manually by just reading them.
|
|
18
|
+
|
|
19
|
+
IMPORTANT: Output ONLY a valid JSON array. No markdown, no explanation, no extra text.
|
|
20
|
+
|
|
21
|
+
Each step must have:
|
|
22
|
+
- "step": A detailed, numbered action description. Include:
|
|
23
|
+
- Exact UI element to interact with (button name, field label, section title)
|
|
24
|
+
- Exact text/values to enter or look for
|
|
25
|
+
- Location hints (e.g., "in the header", "in the Solutions section", "at the top of the page")
|
|
26
|
+
- "result": The specific expected outcome that can be visually verified. Include:
|
|
27
|
+
- Exact text that should be displayed
|
|
28
|
+
- UI state changes (visible, enabled, selected, etc.)
|
|
29
|
+
- URL changes if applicable
|
|
30
|
+
|
|
31
|
+
RULES FOR DETAILED STEPS:
|
|
32
|
+
1. NAVIGATION: "Open the browser and navigate to [exact URL]. Wait for the page to fully load."
|
|
33
|
+
2. VISIBILITY CHECK: "Locate the [section name] section on the page. Verify it is visible and contains the text '[exact text]'."
|
|
34
|
+
3. ELEMENT VERIFICATION: "In the [section name], find the [element type] with text '[text]'. Verify it is displayed."
|
|
35
|
+
4. LINK VALIDATION: "Find the '[link text]' link in the [location]. Verify it has href pointing to '[URL pattern]'."
|
|
36
|
+
5. BUTTON CHECK: "Locate the '[button text]' button in the [section]. Verify it is visible and clickable."
|
|
37
|
+
6. CARD/TILE VALIDATION: "In the [section], locate the first card/tile. Verify it displays: title, image, and description."
|
|
38
|
+
7. SCROLL ACTION: "Scroll down to the [section name] section to bring it into view."
|
|
39
|
+
|
|
40
|
+
EXAMPLE OUTPUT:
|
|
34
41
|
[
|
|
35
|
-
{"step": "
|
|
36
|
-
{"step": "
|
|
37
|
-
{"step": "
|
|
38
|
-
{"step": "
|
|
39
|
-
|
|
42
|
+
{"step": "1. Open the browser and navigate to the application homepage (base URL + '/'). Wait for the page to fully load.", "result": "The homepage loads successfully. The masthead banner is visible at the top of the page."},
|
|
43
|
+
{"step": "2. Locate the masthead section at the top of the page. Verify the main heading is displayed.", "result": "The heading 'Accelerate AI with an open ecosystem' is visible in the masthead."},
|
|
44
|
+
{"step": "3. In the masthead section, find the 'Explore our AI ecosystem' call-to-action link.", "result": "The 'Explore our AI ecosystem' link is visible and has an href containing '/ai'."},
|
|
45
|
+
{"step": "4. Scroll down to the 'Solutions' section. Verify the section headline is displayed.", "result": "The Solutions section is visible with headline 'Find the solution you're looking for, in less time'."},
|
|
46
|
+
{"step": "5. In the Solutions section, locate the product slider/carousel. Verify the first product card is displayed.", "result": "The first product card shows: product title, product image, and product description."}
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
IMPORTANT:
|
|
50
|
+
- Number each step sequentially (1, 2, 3...)
|
|
51
|
+
- Be specific about WHAT to look for and WHERE
|
|
52
|
+
- Include exact text strings from the code in quotes
|
|
53
|
+
- Make expected results measurable and verifiable
|
|
54
|
+
- Output ONLY the JSON array, nothing else`;
|
|
40
55
|
|
|
41
56
|
/**
|
|
42
57
|
* Build the user prompt for AI analysis.
|
|
43
58
|
*/
|
|
44
59
|
function buildUserPrompt(testTitle, testCode) {
|
|
45
|
-
return `
|
|
60
|
+
return `Create detailed manual test steps from this automated test. A QA tester should be able to execute this test manually by reading your steps.
|
|
46
61
|
|
|
47
|
-
|
|
62
|
+
TEST NAME: "${testTitle}"
|
|
48
63
|
|
|
49
|
-
|
|
50
|
-
\`\`\`
|
|
64
|
+
AUTOMATED TEST CODE:
|
|
65
|
+
\`\`\`javascript
|
|
51
66
|
${testCode}
|
|
52
67
|
\`\`\`
|
|
53
68
|
|
|
54
|
-
|
|
69
|
+
ANALYZE THE CODE CAREFULLY:
|
|
70
|
+
1. Look at page.goto() calls to identify which page/URL to navigate to
|
|
71
|
+
2. Look at getByTestId(), locator(), getByRole(), getByLabel() to identify UI elements
|
|
72
|
+
3. Look at expect() assertions to understand what needs to be verified
|
|
73
|
+
4. Look at .toContainText(), .toHaveText(), .toBeVisible() for expected text/state
|
|
74
|
+
5. Look at .toHaveAttribute('href', ...) for link URL validations
|
|
75
|
+
6. Identify ALL assertions and convert them to expected results
|
|
76
|
+
|
|
77
|
+
CREATE COMPREHENSIVE STEPS:
|
|
78
|
+
- Include EVERY action from the test code
|
|
79
|
+
- Include EVERY assertion/verification from the test code
|
|
80
|
+
- Use exact text strings from .toContainText() and .toHaveText() in your expected results
|
|
81
|
+
- Number steps sequentially (1, 2, 3...)
|
|
82
|
+
- Make each step detailed enough for a manual tester
|
|
83
|
+
|
|
84
|
+
OUTPUT: Return ONLY a valid JSON array, no other text.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Attempt to repair truncated JSON array.
|
|
89
|
+
* Handles cases where the response was cut off mid-array.
|
|
90
|
+
*/
|
|
91
|
+
function repairTruncatedJSON(jsonStr) {
|
|
92
|
+
// First, try parsing as-is
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(jsonStr);
|
|
95
|
+
} catch (_) {
|
|
96
|
+
// Continue with repair attempts
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Try to find the last complete object in the array
|
|
100
|
+
// Pattern: find all complete {"step": "...", "result": "..."} objects
|
|
101
|
+
const objectPattern = /\{\s*"step"\s*:\s*"(?:[^"\\]|\\.)*"\s*,\s*"result"\s*:\s*"(?:[^"\\]|\\.)*"\s*\}/g;
|
|
102
|
+
const matches = jsonStr.match(objectPattern);
|
|
103
|
+
|
|
104
|
+
if (matches && matches.length > 0) {
|
|
105
|
+
// Reconstruct a valid JSON array from complete objects
|
|
106
|
+
const reconstructed = '[' + matches.join(',') + ']';
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(reconstructed);
|
|
109
|
+
} catch (_) {
|
|
110
|
+
// Fall through
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Try removing the last incomplete object and close the array
|
|
115
|
+
let trimmed = jsonStr.trim();
|
|
116
|
+
|
|
117
|
+
// If it starts with [, try to close it properly
|
|
118
|
+
if (trimmed.startsWith('[')) {
|
|
119
|
+
// Find the last complete }, before truncation
|
|
120
|
+
const lastCompleteObj = trimmed.lastIndexOf('},');
|
|
121
|
+
if (lastCompleteObj > 0) {
|
|
122
|
+
const fixedJson = trimmed.slice(0, lastCompleteObj + 1) + ']';
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(fixedJson);
|
|
125
|
+
} catch (_) {
|
|
126
|
+
// Fall through
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Try finding the last }
|
|
131
|
+
const lastBrace = trimmed.lastIndexOf('}');
|
|
132
|
+
if (lastBrace > 0) {
|
|
133
|
+
const fixedJson = trimmed.slice(0, lastBrace + 1) + ']';
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(fixedJson);
|
|
136
|
+
} catch (_) {
|
|
137
|
+
// Fall through
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Last resort: throw the original error
|
|
143
|
+
return JSON.parse(jsonStr);
|
|
55
144
|
}
|
|
56
145
|
|
|
57
146
|
/**
|
|
@@ -71,15 +160,22 @@ function parseAIResponse(content) {
|
|
|
71
160
|
|
|
72
161
|
// Also try to extract JSON array from anywhere in the response
|
|
73
162
|
if (!jsonStr.startsWith('[')) {
|
|
74
|
-
const arrayMatch = jsonStr.match(/\[[\s\S]
|
|
163
|
+
const arrayMatch = jsonStr.match(/\[[\s\S]*?\](?=[^}\]]*$)?/);
|
|
75
164
|
if (arrayMatch) {
|
|
76
165
|
jsonStr = arrayMatch[0];
|
|
77
166
|
}
|
|
78
167
|
}
|
|
79
168
|
|
|
169
|
+
// Try to find just the array portion if there's extra text
|
|
170
|
+
const arrayStart = jsonStr.indexOf('[');
|
|
171
|
+
if (arrayStart > 0) {
|
|
172
|
+
jsonStr = jsonStr.slice(arrayStart);
|
|
173
|
+
}
|
|
174
|
+
|
|
80
175
|
let steps;
|
|
81
176
|
try {
|
|
82
|
-
|
|
177
|
+
// Use repair function to handle truncated responses
|
|
178
|
+
steps = repairTruncatedJSON(jsonStr);
|
|
83
179
|
} catch (parseError) {
|
|
84
180
|
throw new Error(`Failed to parse AI response as JSON: ${parseError.message}. Response: ${content.slice(0, 200)}`);
|
|
85
181
|
}
|
|
@@ -88,10 +184,13 @@ function parseAIResponse(content) {
|
|
|
88
184
|
throw new Error('AI response is not an array');
|
|
89
185
|
}
|
|
90
186
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
187
|
+
// Filter out any empty or invalid steps
|
|
188
|
+
return steps
|
|
189
|
+
.filter((s) => s && (s.step || s.result))
|
|
190
|
+
.map((s) => ({
|
|
191
|
+
step: String(s.step || '').trim(),
|
|
192
|
+
result: String(s.result || '').trim(),
|
|
193
|
+
}));
|
|
95
194
|
}
|
|
96
195
|
|
|
97
196
|
/**
|
|
@@ -136,7 +235,9 @@ async function analyzeWithOllama(testTitle, testCode, model) {
|
|
|
136
235
|
{ role: 'user', content: buildUserPrompt(testTitle, testCode) },
|
|
137
236
|
],
|
|
138
237
|
options: {
|
|
139
|
-
temperature: 0.
|
|
238
|
+
temperature: 0.1, // Lower temperature for more consistent, focused output
|
|
239
|
+
num_predict: 8192, // Increased max tokens for detailed step descriptions
|
|
240
|
+
num_ctx: 8192, // Context window for longer test code input
|
|
140
241
|
},
|
|
141
242
|
});
|
|
142
243
|
|
|
@@ -176,47 +277,131 @@ export async function analyzeTestWithAI(testTitle, testCode, apiKey, options = {
|
|
|
176
277
|
*/
|
|
177
278
|
export function extractStepsWithRegex(testCode, isCypress = false) {
|
|
178
279
|
const steps = [];
|
|
280
|
+
let stepNum = 1;
|
|
179
281
|
|
|
180
|
-
//
|
|
282
|
+
// Helper to clean up regex patterns from text
|
|
283
|
+
const cleanText = (text) => text?.replace(/\\?\//gi, '').replace(/\^|\$/g, '').trim() || '';
|
|
284
|
+
|
|
285
|
+
// Common Playwright patterns - order matters for sequential extraction
|
|
181
286
|
const patterns = [
|
|
182
287
|
// page.goto or cy.visit
|
|
183
288
|
{
|
|
184
|
-
regex: /(?:page\.goto|cy\.visit)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
185
|
-
format: (match) => ({
|
|
289
|
+
regex: /(?:page\.goto|cy\.visit)\s*\(\s*(?:process\.env\.[A-Z_]+\s*\+\s*)?['"`]([^'"`]+)['"`]/gi,
|
|
290
|
+
format: (match) => ({
|
|
291
|
+
step: `${stepNum++}. Open the browser and navigate to the URL: ${match[1] || '/'}. Wait for the page to fully load.`,
|
|
292
|
+
result: 'The page loads successfully and all elements are visible.',
|
|
293
|
+
}),
|
|
186
294
|
},
|
|
187
|
-
//
|
|
295
|
+
// getByTestId - identify section
|
|
188
296
|
{
|
|
189
|
-
regex: /(?:
|
|
190
|
-
format: (match) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
},
|
|
297
|
+
regex: /(?:page\.|await\s+)getByTestId\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/gi,
|
|
298
|
+
format: (match) => ({
|
|
299
|
+
step: `${stepNum++}. Locate the section/component with test ID "${match[1]}" on the page.`,
|
|
300
|
+
result: `The "${match[1]}" section is found and visible on the page.`,
|
|
301
|
+
}),
|
|
195
302
|
},
|
|
196
|
-
//
|
|
303
|
+
// locator with class or selector
|
|
197
304
|
{
|
|
198
|
-
regex:
|
|
199
|
-
format: (match) => ({
|
|
305
|
+
regex: /\.locator\s*\(\s*['"`]\.([^'"`]+)['"`]\s*\)(?:\.first\(\))?/gi,
|
|
306
|
+
format: (match) => ({
|
|
307
|
+
step: `${stepNum++}. Within the current section, find the element with class "${match[1]}".`,
|
|
308
|
+
result: `Element with class "${match[1]}" is located.`,
|
|
309
|
+
}),
|
|
310
|
+
},
|
|
311
|
+
// getByRole with name - link
|
|
312
|
+
{
|
|
313
|
+
regex: /getByRole\s*\(\s*['"`]link['"`]\s*,\s*\{[^}]*name:\s*['"`/]([^'"`/]+)/gi,
|
|
314
|
+
format: (match) => ({
|
|
315
|
+
step: `${stepNum++}. Find the link labeled "${cleanText(match[1])}" on the page.`,
|
|
316
|
+
result: `The "${cleanText(match[1])}" link is visible and clickable.`,
|
|
317
|
+
}),
|
|
318
|
+
},
|
|
319
|
+
// getByRole with name - button
|
|
320
|
+
{
|
|
321
|
+
regex: /getByRole\s*\(\s*['"`]button['"`]\s*,\s*\{[^}]*name:\s*['"`/]([^'"`/]+)/gi,
|
|
322
|
+
format: (match) => ({
|
|
323
|
+
step: `${stepNum++}. Locate the button labeled "${cleanText(match[1])}" on the page.`,
|
|
324
|
+
result: `The "${cleanText(match[1])}" button is displayed and enabled.`,
|
|
325
|
+
}),
|
|
326
|
+
},
|
|
327
|
+
// scrollIntoViewIfNeeded
|
|
328
|
+
{
|
|
329
|
+
regex: /scrollIntoViewIfNeeded\s*\(\s*\)/gi,
|
|
330
|
+
format: () => ({
|
|
331
|
+
step: `${stepNum++}. Scroll the page until the target element is visible in the viewport.`,
|
|
332
|
+
result: 'The element scrolls into view successfully.',
|
|
333
|
+
}),
|
|
200
334
|
},
|
|
201
335
|
// expect toBeVisible
|
|
202
336
|
{
|
|
203
|
-
regex: /expect\s*\([^)]+\)\.toBeVisible\s*\(
|
|
204
|
-
format: () => ({
|
|
337
|
+
regex: /expect\s*\([^)]+\)\.toBeVisible\s*\(/gi,
|
|
338
|
+
format: () => ({
|
|
339
|
+
step: `${stepNum++}. Verify that the element is visible on the page.`,
|
|
340
|
+
result: 'The element is confirmed to be visible and displayed.',
|
|
341
|
+
}),
|
|
342
|
+
},
|
|
343
|
+
// expect toContainText with text
|
|
344
|
+
{
|
|
345
|
+
regex: /expect\s*\([^)]+\)\.toContainText\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
346
|
+
format: (match) => ({
|
|
347
|
+
step: `${stepNum++}. Verify the element contains the text "${match[1]}".`,
|
|
348
|
+
result: `The text "${match[1]}" is displayed within the element.`,
|
|
349
|
+
}),
|
|
350
|
+
},
|
|
351
|
+
// expect toHaveText with text
|
|
352
|
+
{
|
|
353
|
+
regex: /expect\s*\([^)]+\)\.toHaveText\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
354
|
+
format: (match) => ({
|
|
355
|
+
step: `${stepNum++}. Verify the element has the exact text "${match[1]}".`,
|
|
356
|
+
result: `The element displays exactly: "${match[1]}".`,
|
|
357
|
+
}),
|
|
358
|
+
},
|
|
359
|
+
// expect toHaveAttribute href - capture full URL including slashes
|
|
360
|
+
{
|
|
361
|
+
regex: /expect\s*\([^)]+\)\.toHaveAttribute\s*\(\s*['"`]href['"`]\s*,\s*(?:\/)?['"`]?([^'"`\)]+?)['"`]?\s*\)/gi,
|
|
362
|
+
format: (match) => {
|
|
363
|
+
const url = match[1]?.trim().replace(/^\/|\/$/g, '') || '';
|
|
364
|
+
return {
|
|
365
|
+
step: `${stepNum++}. Verify the link has an href attribute pointing to "${url}".`,
|
|
366
|
+
result: `The link's href contains or matches "${url}".`,
|
|
367
|
+
};
|
|
368
|
+
},
|
|
205
369
|
},
|
|
206
370
|
// expect toHaveURL
|
|
207
371
|
{
|
|
208
372
|
regex: /expect\s*\([^)]+\)\.toHaveURL\s*\(\s*['"`/]([^'"`/]+)/gi,
|
|
209
|
-
format: (match) => ({
|
|
373
|
+
format: (match) => ({
|
|
374
|
+
step: `${stepNum++}. Verify the browser URL matches "${cleanText(match[1])}".`,
|
|
375
|
+
result: `The URL is confirmed to contain/match "${cleanText(match[1])}".`,
|
|
376
|
+
}),
|
|
377
|
+
},
|
|
378
|
+
// page.fill or getByLabel().fill()
|
|
379
|
+
{
|
|
380
|
+
regex: /(?:getByLabel|getByPlaceholder)\s*\([^)]+\)\.fill\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
381
|
+
format: (match) => {
|
|
382
|
+
const labelMatch = match.input.match(/(?:getByLabel|getByPlaceholder)\s*\(\s*['"`/]([^'"`/]+)/i);
|
|
383
|
+
const label = cleanText(labelMatch?.[1]) || 'input field';
|
|
384
|
+
return {
|
|
385
|
+
step: `${stepNum++}. Locate the "${label}" input field and enter the value "${match[1]}".`,
|
|
386
|
+
result: `The value "${match[1]}" is entered in the "${label}" field.`,
|
|
387
|
+
};
|
|
388
|
+
},
|
|
210
389
|
},
|
|
211
|
-
//
|
|
390
|
+
// .click()
|
|
212
391
|
{
|
|
213
|
-
regex:
|
|
214
|
-
format: (
|
|
392
|
+
regex: /\.click\s*\(\s*\)/gi,
|
|
393
|
+
format: () => ({
|
|
394
|
+
step: `${stepNum++}. Click on the identified element.`,
|
|
395
|
+
result: 'The click action is performed successfully.',
|
|
396
|
+
}),
|
|
215
397
|
},
|
|
216
398
|
// cy.get().should()
|
|
217
399
|
{
|
|
218
400
|
regex: /cy\.get\s*\([^)]+\)\.should\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
219
|
-
format: (match) => ({
|
|
401
|
+
format: (match) => ({
|
|
402
|
+
step: `${stepNum++}. Verify the element state using Cypress assertion.`,
|
|
403
|
+
result: `Element assertion "${match[1]}" passes.`,
|
|
404
|
+
}),
|
|
220
405
|
},
|
|
221
406
|
];
|
|
222
407
|
|
|
@@ -239,8 +424,8 @@ export function extractStepsWithRegex(testCode, isCypress = false) {
|
|
|
239
424
|
if (steps.length === 0) {
|
|
240
425
|
const fw = isCypress ? 'Cypress' : 'Playwright';
|
|
241
426
|
steps.push({
|
|
242
|
-
step: `Execute ${
|
|
243
|
-
result: 'Test
|
|
427
|
+
step: `1. Execute the "${testTitle}" automated test using ${fw}.`,
|
|
428
|
+
result: 'Test completes successfully with all assertions verified.',
|
|
244
429
|
});
|
|
245
430
|
}
|
|
246
431
|
|
package/lib/index.js
CHANGED
|
@@ -38,35 +38,47 @@ export async function runSync(options = {}) {
|
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
//
|
|
41
|
+
// Filter specs BEFORE AI analysis (to avoid analyzing specs we don't need)
|
|
42
|
+
if (options.spec && options.spec.length > 0) {
|
|
43
|
+
const set = new Set(options.spec);
|
|
44
|
+
// Match by exact name, base name, or partial match
|
|
45
|
+
specsMap = new Map([...specsMap].filter(([name]) => {
|
|
46
|
+
// Check exact match
|
|
47
|
+
if (set.has(name)) return true;
|
|
48
|
+
// Check if any spec pattern matches this name
|
|
49
|
+
for (const pattern of options.spec) {
|
|
50
|
+
if (name.includes(pattern) || pattern.includes(name)) return true;
|
|
51
|
+
// Also try without extension
|
|
52
|
+
const patternBase = pattern.replace(/\.(spec|test|cy)\.(js|ts)$/i, '');
|
|
53
|
+
const nameBase = name.replace(/\.(spec|test|cy)$/i, '');
|
|
54
|
+
if (nameBase.includes(patternBase) || patternBase.includes(nameBase)) return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}));
|
|
58
|
+
if (specsMap.size === 0) {
|
|
59
|
+
console.log('No matching spec files for:', options.spec.join(', '));
|
|
60
|
+
console.log('Available specs:', [...analyzeSpecs(e2eDir).keys()].join(', '));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
} else if (!options.all && !csvOnly && options.interactive !== false && process.stdin.isTTY) {
|
|
64
|
+
// Interactive selection BEFORE AI analysis
|
|
65
|
+
const selected = await promptSpecSelection(specsMap);
|
|
66
|
+
if (selected === null) {
|
|
67
|
+
// user chose "all"
|
|
68
|
+
} else if (selected.size === 0) {
|
|
69
|
+
console.log('No spec files selected. Exiting.');
|
|
70
|
+
return;
|
|
71
|
+
} else {
|
|
72
|
+
specsMap = new Map([...specsMap].filter(([name]) => selected.has(name)));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Enrich with AI-powered step analysis (only for filtered specs)
|
|
42
77
|
await enrichSpecsMapWithAI(specsMap, {
|
|
43
78
|
useAI,
|
|
44
79
|
model: options.model,
|
|
45
80
|
});
|
|
46
81
|
|
|
47
|
-
if (!csvOnly) {
|
|
48
|
-
if (options.all) {
|
|
49
|
-
// sync all, no prompt
|
|
50
|
-
} else if (options.spec && options.spec.length > 0) {
|
|
51
|
-
const set = new Set(options.spec);
|
|
52
|
-
specsMap = new Map([...specsMap].filter(([name]) => set.has(name)));
|
|
53
|
-
if (specsMap.size === 0) {
|
|
54
|
-
console.log('No matching spec files for:', options.spec.join(', '));
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
} else if (options.interactive !== false && process.stdin.isTTY) {
|
|
58
|
-
const selected = await promptSpecSelection(specsMap);
|
|
59
|
-
if (selected === null) {
|
|
60
|
-
// user chose "all"
|
|
61
|
-
} else if (selected.size === 0) {
|
|
62
|
-
console.log('No spec files selected. Exiting.');
|
|
63
|
-
return;
|
|
64
|
-
} else {
|
|
65
|
-
specsMap = new Map([...specsMap].filter(([name]) => selected.has(name)));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
82
|
writeCsvFiles(specsMap, csvOutputDir);
|
|
71
83
|
|
|
72
84
|
if (csvOnly) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ash-mallick/browserstack-sync",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Sync Playwright & Cypress e2e specs to CSV and BrowserStack Test Management with FREE AI-powered test step extraction using Ollama (local)",
|
|
5
5
|
"author": "Ashutosh Mallick",
|
|
6
6
|
"type": "module",
|