@ash-mallick/browserstack-sync 1.1.3 → 1.1.4

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.
@@ -14,74 +14,78 @@ 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 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:
17
+ const SYSTEM_PROMPT = `You are a QA expert creating manual test steps from automated test code.
18
+
19
+ CRITICAL RULES:
20
+ 1. Generate steps ONLY for what is explicitly in the test code provided
21
+ 2. DO NOT add steps for things not in the code
22
+ 3. DO NOT hallucinate or assume content - use ONLY text/values from the code
23
+ 4. Each expect() or assertion = one verification step
24
+ 5. Output ONLY valid JSON array, nothing else
25
+
26
+ STEP FORMAT:
27
+ {
28
+ "step": "Numbered action description with exact element and location",
29
+ "result": "Expected outcome using exact text from the code"
30
+ }
31
+
32
+ MAPPING CODE TO STEPS:
33
+
34
+ 1. getByTestId('section-name') "Locate the section with test ID 'section-name'"
35
+ 2. locator('.class-name') "Find the element with class 'class-name'"
36
+ 3. getByRole('link', { name: /text/i }) "Find the link labeled 'text'"
37
+ 4. expect(element).toBeVisible() Result: "The element is visible"
38
+ 5. expect(element).toContainText('exact text') → Result: "The element displays 'exact text'"
39
+ 6. expect(element).toHaveText('exact text') → Result: "The element shows exactly 'exact text'"
40
+ 7. expect(link).toHaveAttribute('href', '/path') → Result: "The link href is '/path'"
41
+ 8. scrollIntoViewIfNeeded() → "Scroll to bring the element into view"
42
+
43
+ EXAMPLE - For this code:
44
+ const scope = page.getByTestId("success-stories");
45
+ await expect(scope.locator(".band__headline")).toHaveText("Success stories");
46
+
47
+ OUTPUT:
41
48
  [
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."}
49
+ {"step": "1. Locate the section with test ID 'success-stories' on the page.", "result": "The success-stories section is found."},
50
+ {"step": "2. In the success-stories section, find the element with class 'band__headline'. Verify its text content.", "result": "The headline displays exactly 'Success stories'."}
47
51
  ]
48
52
 
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`;
53
+ REMEMBER:
54
+ - ONLY use text/values that appear in the test code
55
+ - DO NOT make up text or values
56
+ - Number steps 1, 2, 3...
57
+ - Be precise and specific`;
55
58
 
56
59
  /**
57
60
  * Build the user prompt for AI analysis.
58
61
  */
59
62
  function buildUserPrompt(testTitle, testCode) {
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.
63
+ return `Convert this automated test into manual test steps.
61
64
 
62
- TEST NAME: "${testTitle}"
65
+ TEST: "${testTitle}"
63
66
 
64
- AUTOMATED TEST CODE:
65
- \`\`\`javascript
67
+ CODE:
66
68
  ${testCode}
67
- \`\`\`
68
-
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.`;
69
+
70
+ INSTRUCTIONS:
71
+ 1. Create one step for EACH expect() assertion in the code
72
+ 2. Use ONLY the exact text strings that appear in the code (in quotes or regex)
73
+ 3. For each getByTestId(), locator(), or getByRole() - describe THAT element
74
+ 4. For each .toContainText('X') or .toHaveText('X') - the expected result is "displays 'X'"
75
+ 5. For each .toHaveAttribute('href', 'Y') - the expected result is "href is 'Y'"
76
+ 6. DO NOT add extra text or values that are not in the code
77
+
78
+ Example mapping:
79
+ - expect(page.getByTestId("masthead")).toBeVisible()
80
+ Step: "Locate the element with test ID 'masthead'." Result: "The masthead element is visible."
81
+
82
+ - expect(el).toContainText("Hello World")
83
+ Step: "Verify the element's text content." Result: "The element contains 'Hello World'."
84
+
85
+ - expect(link).toHaveAttribute("href", "/contact")
86
+ → Step: "Check the link's href attribute." Result: "The link href is '/contact'."
87
+
88
+ OUTPUT: Return ONLY a JSON array of {step, result} objects. No other text.`;
85
89
  }
86
90
 
87
91
  /**
@@ -112,32 +112,91 @@ export async function updateTestCase(projectId, testCaseId, testCase, auth) {
112
112
 
113
113
  /** Match existing BS test case to our local case: by title or by our TC-xxx tag */
114
114
  function findExisting(existingList, localCase) {
115
- const byTitle = existingList.find((t) => (t.title || '').trim() === (localCase.title || '').trim());
115
+ const byTitle = existingList.find((t) => (t.name || t.title || '').trim() === (localCase.title || '').trim());
116
116
  if (byTitle) return byTitle;
117
117
  const byTag = existingList.find((t) => (t.tags || []).includes(localCase.id));
118
118
  return byTag;
119
119
  }
120
120
 
121
+ /**
122
+ * Compare local test case with existing BrowserStack test case.
123
+ * Returns true if content has changed and update is needed.
124
+ */
125
+ function hasContentChanged(existing, localCase) {
126
+ // Compare steps - this is the main content that changes
127
+ const existingSteps = existing.test_case_steps || [];
128
+ const localSteps = Array.isArray(localCase.steps) ? localCase.steps : [];
129
+
130
+ // Different number of steps = changed
131
+ if (existingSteps.length !== localSteps.length) {
132
+ return true;
133
+ }
134
+
135
+ // Compare each step
136
+ for (let i = 0; i < localSteps.length; i++) {
137
+ const existingStep = existingSteps[i] || {};
138
+ const localStep = localSteps[i] || {};
139
+
140
+ const existingStepText = (existingStep.step || '').trim();
141
+ const localStepText = (localStep.step || '').trim();
142
+ const existingResult = (existingStep.result || '').trim();
143
+ const localResult = (localStep.result || '').trim();
144
+
145
+ if (existingStepText !== localStepText || existingResult !== localResult) {
146
+ return true;
147
+ }
148
+ }
149
+
150
+ // Compare title
151
+ const existingTitle = (existing.name || existing.title || '').trim();
152
+ const localTitle = (localCase.title || '').trim();
153
+ if (existingTitle !== localTitle) {
154
+ return true;
155
+ }
156
+
157
+ // No changes detected
158
+ return false;
159
+ }
160
+
121
161
  export async function syncToBrowserStack(specsMap, projectId, auth) {
122
162
  console.log('\nSyncing to BrowserStack project', projectId);
163
+
164
+ let totalCreated = 0;
165
+ let totalUpdated = 0;
166
+ let totalSkipped = 0;
167
+
123
168
  for (const [baseName, { specFile, cases }] of specsMap) {
124
169
  const folderName = baseName;
125
170
  const folderId = await ensureFolder(projectId, folderName, null, auth);
126
171
  const existing = await getTestCasesInFolder(projectId, folderId, auth);
127
- console.log('Folder:', folderName, '(id:', folderId, ')');
172
+ console.log('Folder:', folderName, '(id:', folderId, ') -', cases.length, 'test(s)');
173
+
128
174
  for (const tc of cases) {
129
175
  try {
130
176
  const found = findExisting(existing, tc);
131
177
  if (found) {
132
- await updateTestCase(projectId, found.identifier, tc, auth);
133
- console.log(' Updated', tc.id, tc.title, '->', found.identifier);
178
+ // Check if content has actually changed before updating
179
+ if (hasContentChanged(found, tc)) {
180
+ await updateTestCase(projectId, found.identifier, tc, auth);
181
+ console.log(' ✓ Updated', tc.id, tc.title, '->', found.identifier);
182
+ totalUpdated++;
183
+ } else {
184
+ console.log(' ○ Skipped', tc.id, tc.title, '(no changes)');
185
+ totalSkipped++;
186
+ }
134
187
  } else {
135
188
  const id = await createTestCase(projectId, folderId, tc, auth);
136
- console.log(' Created', tc.id, tc.title, '->', id);
189
+ console.log(' + Created', tc.id, tc.title, '->', id);
190
+ totalCreated++;
137
191
  }
138
192
  } catch (e) {
139
- console.error(' Failed', tc.id, tc.title, e.message);
193
+ console.error(' Failed', tc.id, tc.title, e.message);
140
194
  }
141
195
  }
142
196
  }
197
+
198
+ console.log('\nSync summary:');
199
+ console.log(` Created: ${totalCreated} test case(s)`);
200
+ console.log(` Updated: ${totalUpdated} test case(s)`);
201
+ console.log(` Skipped: ${totalSkipped} test case(s) (no changes)`);
143
202
  }
package/lib/enrich.js CHANGED
@@ -40,6 +40,46 @@ function inferTags(specBaseName, title, isCypress) {
40
40
  return tags;
41
41
  }
42
42
 
43
+ /**
44
+ * Count the number of expect() assertions in test code.
45
+ */
46
+ function countAssertions(code) {
47
+ const expectMatches = code.match(/expect\s*\(/g) || [];
48
+ return expectMatches.length;
49
+ }
50
+
51
+ /**
52
+ * Merge AI steps with regex steps to ensure completeness.
53
+ * If AI generated fewer steps than assertions, supplement with unique regex steps.
54
+ */
55
+ function mergeStepsForCompleteness(aiSteps, regexSteps, assertionCount) {
56
+ if (!aiSteps || aiSteps.length === 0) {
57
+ return regexSteps;
58
+ }
59
+
60
+ // If AI generated enough steps, use AI steps
61
+ if (aiSteps.length >= assertionCount * 0.7) { // 70% coverage threshold
62
+ return aiSteps;
63
+ }
64
+
65
+ // If AI generated too few steps, supplement with unique regex steps
66
+ const aiStepTexts = new Set(aiSteps.map(s => s.step.toLowerCase()));
67
+ const supplementalSteps = regexSteps.filter(rs => {
68
+ // Only add regex steps that aren't similar to existing AI steps
69
+ const rsLower = rs.step.toLowerCase();
70
+ return !Array.from(aiStepTexts).some(aiText =>
71
+ aiText.includes(rsLower.slice(0, 30)) || rsLower.includes(aiText.slice(0, 30))
72
+ );
73
+ });
74
+
75
+ // Combine and renumber
76
+ const combined = [...aiSteps, ...supplementalSteps];
77
+ return combined.map((step, idx) => ({
78
+ step: step.step.replace(/^\d+\.\s*/, `${idx + 1}. `),
79
+ result: step.result,
80
+ }));
81
+ }
82
+
43
83
  /**
44
84
  * Enrich a single case with state, case_type, steps, expected_results, jira_issues, automation_status, tags, description.
45
85
  * @param {Object} case_ - Test case with id, title, code
@@ -54,13 +94,19 @@ export function enrichCase(case_, specBaseName, isCypress = false, aiSteps = nul
54
94
  const fw = isCypress ? FRAMEWORK.cypress : FRAMEWORK.playwright;
55
95
  const tags = inferTags(specBaseName, title, isCypress);
56
96
 
57
- // Determine steps: AI steps > regex-extracted steps > default
97
+ // Count assertions in code for completeness check
98
+ const assertionCount = code ? countAssertions(code) : 0;
99
+
100
+ // Get regex-based steps as fallback/supplement
101
+ const regexSteps = code ? extractStepsWithRegex(code, isCypress) : [];
102
+
103
+ // Determine steps: merge AI + regex for completeness, or use regex only
58
104
  let steps;
59
105
  if (aiSteps && aiSteps.length > 0) {
60
- steps = aiSteps;
61
- } else if (code) {
62
- // Try regex-based extraction from code
63
- steps = extractStepsWithRegex(code, isCypress);
106
+ // Merge AI steps with regex steps to ensure we don't miss assertions
107
+ steps = mergeStepsForCompleteness(aiSteps, regexSteps, assertionCount);
108
+ } else if (regexSteps.length > 0) {
109
+ steps = regexSteps;
64
110
  } else {
65
111
  // Fallback to generic step
66
112
  steps = [{ step: fw.step, result: DEFAULT_EXPECTED_RESULT }];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ash-mallick/browserstack-sync",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
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",
@@ -42,6 +42,7 @@
42
42
  "url": ""
43
43
  },
44
44
  "dependencies": {
45
+ "@ash-mallick/browserstack-sync": "^1.1.3",
45
46
  "dotenv": "^16.4.5",
46
47
  "ollama": "^0.6.3"
47
48
  },