playwright-mimic 0.1.1 → 0.1.2

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.
Files changed (66) hide show
  1. package/README.md +134 -72
  2. package/dist/index.d.ts +1 -4
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -4
  5. package/dist/index.js.map +1 -1
  6. package/dist/mimic/annotations.d.ts +2 -1
  7. package/dist/mimic/annotations.d.ts.map +1 -1
  8. package/dist/mimic/annotations.js +10 -4
  9. package/dist/mimic/annotations.js.map +1 -1
  10. package/dist/mimic/cli.js +1 -1
  11. package/dist/mimic/cli.js.map +1 -1
  12. package/dist/mimic/click.d.ts +4 -4
  13. package/dist/mimic/click.d.ts.map +1 -1
  14. package/dist/mimic/click.js +233 -118
  15. package/dist/mimic/click.js.map +1 -1
  16. package/dist/mimic/forms.d.ts +11 -6
  17. package/dist/mimic/forms.d.ts.map +1 -1
  18. package/dist/mimic/forms.js +371 -124
  19. package/dist/mimic/forms.js.map +1 -1
  20. package/dist/mimic/markers.d.ts +133 -0
  21. package/dist/mimic/markers.d.ts.map +1 -0
  22. package/dist/mimic/markers.js +589 -0
  23. package/dist/mimic/markers.js.map +1 -0
  24. package/dist/mimic/navigation.d.ts.map +1 -1
  25. package/dist/mimic/navigation.js +29 -10
  26. package/dist/mimic/navigation.js.map +1 -1
  27. package/dist/mimic/playwrightCodeGenerator.d.ts +55 -0
  28. package/dist/mimic/playwrightCodeGenerator.d.ts.map +1 -0
  29. package/dist/mimic/playwrightCodeGenerator.js +270 -0
  30. package/dist/mimic/playwrightCodeGenerator.js.map +1 -0
  31. package/dist/mimic/replay.d.ts.map +1 -1
  32. package/dist/mimic/replay.js +45 -36
  33. package/dist/mimic/replay.js.map +1 -1
  34. package/dist/mimic/schema/action.d.ts +26 -26
  35. package/dist/mimic/schema/action.d.ts.map +1 -1
  36. package/dist/mimic/schema/action.js +13 -31
  37. package/dist/mimic/schema/action.js.map +1 -1
  38. package/dist/mimic/selector.d.ts +6 -2
  39. package/dist/mimic/selector.d.ts.map +1 -1
  40. package/dist/mimic/selector.js +681 -269
  41. package/dist/mimic/selector.js.map +1 -1
  42. package/dist/mimic/selectorDescriptor.d.ts +15 -3
  43. package/dist/mimic/selectorDescriptor.d.ts.map +1 -1
  44. package/dist/mimic/selectorDescriptor.js +25 -2
  45. package/dist/mimic/selectorDescriptor.js.map +1 -1
  46. package/dist/mimic/selectorSerialization.d.ts +5 -17
  47. package/dist/mimic/selectorSerialization.d.ts.map +1 -1
  48. package/dist/mimic/selectorSerialization.js +4 -142
  49. package/dist/mimic/selectorSerialization.js.map +1 -1
  50. package/dist/mimic/selectorTypes.d.ts +24 -102
  51. package/dist/mimic/selectorTypes.d.ts.map +1 -1
  52. package/dist/mimic/selectorUtils.d.ts +33 -7
  53. package/dist/mimic/selectorUtils.d.ts.map +1 -1
  54. package/dist/mimic/selectorUtils.js +159 -52
  55. package/dist/mimic/selectorUtils.js.map +1 -1
  56. package/dist/mimic/storage.d.ts +43 -8
  57. package/dist/mimic/storage.d.ts.map +1 -1
  58. package/dist/mimic/storage.js +258 -46
  59. package/dist/mimic/storage.js.map +1 -1
  60. package/dist/mimic/types.d.ts +38 -16
  61. package/dist/mimic/types.d.ts.map +1 -1
  62. package/dist/mimic.d.ts +1 -0
  63. package/dist/mimic.d.ts.map +1 -1
  64. package/dist/mimic.js +240 -84
  65. package/dist/mimic.js.map +1 -1
  66. package/package.json +27 -6
@@ -1,9 +1,17 @@
1
1
  import { generateText, Output } from 'ai';
2
2
  import z from 'zod';
3
3
  import { countTokens } from '../utils/token-counter';
4
+ import { generateBestSelectorForElement } from './selector';
4
5
  import { addAnnotation } from './annotations.js';
6
+ import { selectorToPlaywrightCode, generateFormCode } from './playwrightCodeGenerator.js';
7
+ import { captureScreenshot, generateAriaSnapshot } from './markers.js';
5
8
  const zFormActionResult = z.object({
6
- type: z.enum(['keypress', 'type', 'fill', 'select', 'uncheck', 'check', 'setInputFiles', 'clear']),
9
+ /**
10
+ * The mimic ID (marker number) of the target form element
11
+ * This is the number shown on the element's badge in the screenshot
12
+ */
13
+ mimicId: z.number().int().min(1).describe("The mimic ID (marker number) shown on the form element's badge in the screenshot"),
14
+ type: z.enum(['keypress', 'type', 'fill', 'select', 'uncheck', 'check', 'click', 'setInputFiles', 'clear']),
7
15
  params: z.object({
8
16
  value: z.string().describe("Value to set for the form update."),
9
17
  modifiers: z.array(z.enum(['Alt', 'Control', 'Meta', 'Shift', 'none'])).describe("Optional modifier keys to use for the form update."),
@@ -14,142 +22,218 @@ const zFormActionResult = z.object({
14
22
  */
15
23
  elementDescription: z.string().describe("Human-readable description of the target form element for test annotations"),
16
24
  });
17
- export const getFormAction = async (_page, brain, gherkinStep, targetElements, testContext) => {
18
- // Format target elements with their indices for the prompt
19
- // Include all relevant identifying information
20
- const elementsWithIndices = targetElements.map((element, index) => ({
21
- index,
22
- tag: element.tag,
23
- text: element.text,
24
- id: element.id,
25
- role: element.role,
26
- label: element.label,
27
- ariaLabel: element.ariaLabel,
28
- typeAttr: element.typeAttr,
29
- nameAttr: element.nameAttr,
30
- href: element.href,
31
- dataset: element.dataset,
32
- nthOfType: element.nthOfType,
33
- }));
34
- // Group elements by tag type
35
- const elementsByTag = new Map();
36
- for (const el of elementsWithIndices) {
37
- const tagKey = el.tag === 'a' ? 'links' : el.tag === 'button' ? 'buttons' : el.tag === 'input' ? 'inputs' : el.tag;
38
- if (!elementsByTag.has(tagKey)) {
39
- elementsByTag.set(tagKey, []);
40
- }
41
- elementsByTag.get(tagKey).push(el);
42
- }
43
- // Format element fields in selector priority order, skipping null/empty values
44
- const formatElement = (roleSection) => (el) => {
45
- const parts = [];
46
- // Priority order: testId → text → role → ariaLabel → label → name → type → href → dataAttributes → tag → id → nthOfType
47
- if (el.dataset.testid) {
48
- parts.push(` testId: "${el.dataset.testid}"`);
49
- }
50
- if (el.text && el.text.trim()) {
51
- parts.push(` text: "${el.text.trim()}"`);
52
- }
53
- if (el.role && roleSection !== el.role) {
54
- parts.push(` role: ${el.role}`);
55
- }
56
- if (el.ariaLabel) {
57
- parts.push(` ariaLabel: "${el.ariaLabel}"`);
58
- }
59
- if (el.label) {
60
- parts.push(` label: "${el.label}"`);
61
- }
62
- if (el.nameAttr) {
63
- parts.push(` name: "${el.nameAttr}"`);
64
- }
65
- if (el.typeAttr) {
66
- parts.push(` type: ${el.typeAttr}`);
67
- }
68
- if (el.href) {
69
- parts.push(` href: "${el.href}"`);
70
- }
71
- if (Object.keys(el.dataset).length > 0) {
72
- const dataKeys = Object.keys(el.dataset).filter(k => k !== 'testid');
73
- if (dataKeys.length > 0) {
74
- parts.push(` dataAttributes: ${JSON.stringify(dataKeys)}`);
75
- }
76
- }
77
- parts.push(` tag: ${el.tag}`);
78
- // if (el.id) {
79
- // parts.push(` id: "${el.id}"`);
80
- // }
81
- parts.push(` index: ${el.index}`);
82
- if (el.nthOfType > 1) {
83
- parts.push(` nthOfType: ${el.nthOfType}`);
84
- }
85
- return ` - ${parts.join('\n ')}`;
86
- };
87
- // Create formatted description grouped by tag
88
- const elementsDescription = Array.from(elementsByTag.entries())
89
- .map(([tagKey, elements]) => {
90
- const formattedElements = elements.map(formatElement(tagKey)).join('\n');
91
- return `${tagKey}:\n${formattedElements}`;
92
- })
93
- .join('\n\n');
25
+ export const getFormAction = async (page, brain, gherkinStep, testContext) => {
26
+ const startTime = Date.now();
27
+ // Capture screenshot with markers and positioning data
28
+ console.log('📸 [getFormAction] Starting screenshot capture with markers...');
29
+ const screenshotStart = Date.now();
30
+ const { image: screenshot, markers: markerData } = await captureScreenshot(page);
31
+ const screenshotTime = Date.now() - screenshotStart;
32
+ console.log(`📸 [getFormAction] Screenshot captured in ${screenshotTime}ms (${(screenshotTime / 1000).toFixed(2)}s)`);
33
+ const base64Start = Date.now();
34
+ const screenshotBase64 = screenshot.toString('base64');
35
+ const base64Time = Date.now() - base64Start;
36
+ console.log(`📸 [getFormAction] Screenshot converted to base64 in ${base64Time}ms (${(base64Time / 1000).toFixed(2)}s), size: ${(screenshotBase64.length / 1024).toFixed(2)}KB`);
37
+ // Generate accessibility snapshot to explain the screenshot structure
38
+ console.log('🔍 [getFormAction] Generating accessibility snapshot...');
39
+ const ariaSnapshotStart = Date.now();
40
+ const ariaSnapshot = await generateAriaSnapshot(page);
41
+ const ariaSnapshotTime = Date.now() - ariaSnapshotStart;
42
+ console.log(`🔍 [getFormAction] Accessibility snapshot generated in ${ariaSnapshotTime}ms (${(ariaSnapshotTime / 1000).toFixed(2)}s), length: ${ariaSnapshot.length} chars`);
43
+ // Convert marker data to format expected by prompt
44
+ const markerStart = Date.now();
45
+ // Defensive check: ensure markerData is an array before mapping
46
+ // This prevents "Cannot read properties of undefined (reading 'map')" errors
47
+ const markerInfo = (Array.isArray(markerData) ? markerData : []).map(m => {
48
+ return {
49
+ id: m.mimicId,
50
+ tag: m.tag,
51
+ text: m.text,
52
+ role: m.role,
53
+ ariaLabel: m.ariaLabel,
54
+ };
55
+ });
56
+ const markerTime = Date.now() - markerStart;
57
+ console.log(`🔍 [getFormAction] Processed ${markerInfo.length} markers in ${markerTime}ms (${(markerTime / 1000).toFixed(2)}s)`);
58
+ // Filter to form elements only (inputs, textareas, selects, buttons)
59
+ const filterStart = Date.now();
60
+ const formMarkers = markerInfo.filter(m => m.tag === 'input' || m.tag === 'textarea' || m.tag === 'select' || m.tag === 'button');
61
+ const filterTime = Date.now() - filterStart;
62
+ console.log(`🔍 [getFormAction] Filtered to ${formMarkers.length} form elements in ${filterTime}ms`);
63
+ // Build marker summary for the prompt
64
+ const summaryStart = Date.now();
65
+ const markerSummary = formMarkers
66
+ .slice(0, 50) // Limit to first 50 markers to avoid prompt size issues
67
+ .map(m => ` Marker ${m.id}: ${m.tag}${m.role ? ` (role: ${m.role})` : ''}${m.text ? ` - "${m.text.substring(0, 50)}"` : ''}${m.ariaLabel ? ` [aria-label: "${m.ariaLabel}"]` : ''}`)
68
+ .join('\n');
69
+ const summaryTime = Date.now() - summaryStart;
70
+ console.log(`📝 [getFormAction] Built marker summary in ${summaryTime}ms`);
94
71
  // Build context description for the prompt
72
+ const promptStart = Date.now();
73
+ // Build context description with defensive checks for optional testContext
74
+ // Ensure previousSteps exists and is an array before calling .map()
95
75
  const contextDescription = testContext ? `
96
76
  **Test Context:**
97
77
  - Current URL: ${testContext.currentState.url}
98
78
  - Current Page Title: ${testContext.currentState.pageTitle}
99
79
  - Step ${testContext.currentStepIndex + 1} of ${testContext.totalSteps}
100
- ${testContext.previousSteps.length > 0 ? `
80
+ ${testContext.previousSteps && Array.isArray(testContext.previousSteps) && testContext.previousSteps.length > 0 ? `
101
81
  **Previous Steps Executed:**
102
82
  ${testContext.previousSteps.map((prevStep, idx) => `${idx + 1}. Step ${prevStep.stepIndex + 1}: "${prevStep.stepText}" (${prevStep.actionKind}${prevStep.url ? ` → ${prevStep.url}` : ''})`).join('\n')}
103
83
  ` : ''}
104
84
  ` : '';
105
- const res = await generateText({
106
- model: brain,
107
- prompt: `You are an expert Playwright test engineer specializing in mapping Gherkin steps to form interactions.
85
+ const prompt = `You are an expert Playwright test engineer specializing in mapping Gherkin steps to form interactions using visual analysis.
108
86
 
109
87
  Your task is to analyze:
110
- 1. A single Gherkin step that implies a form update action (typing, filling, selecting, checking, etc.).
111
- 2. A list of candidate form elements extracted from the page.
88
+ 1. A screenshot of the page with numbered marker badges on elements
89
+ 2. An accessibility snapshot (provided below) that describes the page structure with roles, names, data-testid, and data-mimic-* attributes
90
+ 3. A single Gherkin step that implies a form update action (typing, filling, selecting, checking, etc.)
91
+
92
+ **IMPORTANT**: Look at the screenshot to identify form elements by their marker numbers. Each element has a numbered badge:
93
+ - **RED badges** = Interactive elements (buttons, links, inputs, etc.)
94
+ - **BLUE badges** = Display-only content elements
95
+ - **GREEN badges** = Structure/test anchor elements
112
96
 
113
97
  You must determine:
98
+ - The **mimicId** (marker number) of the target form element from the screenshot
114
99
  - The type of form action (fill, type, select, check, uncheck, clear, etc.)
115
100
  - The value to use (text to type, option to select, etc.)
116
101
 
102
+ ---
103
+
104
+ ### IMPORTANT RULES
105
+
106
+ - **ALWAYS use the marker ID (mimicId)** from the screenshot - this is the number shown on the form element's badge
107
+ - Do NOT invent elements or marker IDs - only use marker IDs that are visible in the screenshot
108
+ - For typing text (email addresses, names, messages, etc.), ALWAYS use "fill" or "type", NEVER "keypress"
109
+ - For checkboxes, ALWAYS use "check" or "uncheck", NEVER "keypress" or "click"
110
+ - For radio buttons, ALWAYS use "click", NEVER "check" (radio buttons should be clicked, not checked)
111
+ - "keypress" is ONLY for single keyboard keys like "Enter", "Tab", "Escape", "ArrowUp", "ArrowDown", etc.
112
+ - If the step says "type X into Y" or "fill Y with X", use "fill" (preferred) or "type", NOT "keypress"
113
+ - If the step says "check" or "select" a checkbox, use "check", NOT "keypress" or "click"
114
+ - If the step says "select" or "click" a radio button, use "click", NOT "check"
115
+ - Provide a clear, human-readable description of the target element (e.g., "Email input field", "Name field labeled 'Full Name'", "Submit button", "Country dropdown")
116
+ - Consider the test context - what steps came before may help identify the correct element
117
+
117
118
  ${contextDescription}
118
119
  **Gherkin Step:**
119
120
  ${gherkinStep}
120
121
 
121
- **Available Form Elements (${targetElements.length} total):**
122
- ${elementsDescription}
122
+ **Available Form Elements (${formMarkers.length} total):**
123
+ ${markerSummary}
124
+ ${formMarkers.length > 50 ? `\n... and ${formMarkers.length - 50} more form elements` : ''}
125
+
126
+ **Accessibility Snapshot (explains the screenshot structure):**
127
+ The following accessibility snapshot describes the page structure with roles, accessible names, data-testid attributes, and data-mimic-* attributes. Use this to understand the page structure alongside the screenshot:
128
+ \`\`\`
129
+ ${ariaSnapshot}
130
+ \`\`\`
123
131
 
124
132
  **Action Types:**
125
133
  - fill: Replace all content in a field with text (USE THIS for typing text like email addresses, names, etc.)
126
134
  - type: Type text character by character (slower, simulates real typing - use when needed for special cases)
127
135
  - select: Select an option from dropdown/select element
128
- - check: Check a checkbox (USE THIS when step says "check" or "select" a checkbox)
136
+ - check: Check a checkbox (USE THIS when step says "check" or "select" a checkbox) - DO NOT use for radio buttons
129
137
  - uncheck: Uncheck a checkbox (USE THIS when step says "uncheck" or "deselect" a checkbox)
138
+ - click: Click on a radio button to select it (USE THIS for radio buttons, NOT "check")
130
139
  - clear: Clear field content
131
- - keypress: Press a SINGLE KEY ONLY (e.g., "Enter", "Tab", "Escape", "ArrowDown") - DO NOT use for typing text strings or checkboxes
140
+ - keypress: Press a SINGLE KEY ONLY (e.g., "Enter", "Tab", "Escape", "ArrowDown") - DO NOT use for typing text strings, checkboxes, or radio buttons
132
141
  - setInputFiles: Upload a file
133
142
 
134
- **IMPORTANT:**
135
- - For typing text (email addresses, names, messages, etc.), ALWAYS use "fill" or "type", NEVER "keypress"
136
- - For checkboxes, ALWAYS use "check" or "uncheck", NEVER "keypress"
137
- - "keypress" is ONLY for single keyboard keys like "Enter", "Tab", "Escape", "ArrowUp", "ArrowDown", etc.
138
- - If the step says "type X into Y" or "fill Y with X", use "fill" (preferred) or "type", NOT "keypress"
139
- - If the step says "check" or "select" a checkbox, use "check", NOT "keypress"
140
-
141
- **Instructions:**
142
- 1. Identify what form action is being requested
143
- 2. Extract the value from the step (text to type, option to select, etc.)
144
- 3. Identify which form element is being targeted (name field, email field, submit button, etc.)
145
- 4. Return the appropriate action type, value, and a clear description of the target element
146
- - The elementDescription should clearly identify the form field (e.g., "Email input field", "Name field labeled 'Full Name'", "Submit button", "Country dropdown")
143
+ ## Analyze the screenshot and determine:
144
+ 1. Which form element (by mimicId/marker number) is being targeted
145
+ 2. What action type to perform
146
+ 3. What value to use
147
+ 4. A clear description of the target element
147
148
 
148
- Think step-by-step about what the user wants to do with the form.`,
149
- output: Output.object({ schema: zFormActionResult, name: 'formActionResult' }),
150
- maxRetries: 3,
151
- });
149
+ Use the marker ID numbers (mimicId) shown on the badges in the screenshot and referenced in the accessibility snapshot to identify the form element.`;
150
+ const promptTime = Date.now() - promptStart;
151
+ console.log(`📝 [getFormAction] Built prompt in ${promptTime}ms, prompt length: ${prompt.length} chars`);
152
+ // Build message content - try without image first, then retry with image if needed
153
+ const messageStart = Date.now();
154
+ // First attempt: text-only (no image) - faster and cheaper
155
+ const messageContentTextOnly = [
156
+ { type: 'text', text: prompt }
157
+ ];
158
+ // Second attempt: with image (if first attempt fails)
159
+ const messageContentWithImage = [
160
+ { type: 'text', text: prompt },
161
+ { type: 'image', image: screenshotBase64 }
162
+ ];
163
+ const messageTime = Date.now() - messageStart;
164
+ console.log(`📨 [getFormAction] Built message content in ${messageTime}ms`);
165
+ // Set explicit timeout for AI calls to prevent indefinite hangs
166
+ // 2 minutes should be sufficient for most AI responses, even with retries
167
+ const aiTimeout = 120_000; // 2 minutes
168
+ let res;
169
+ let aiTime;
170
+ let usedImage = false;
171
+ // First attempt: try without image (text-only with accessibility snapshot)
172
+ console.log('🤖 [getFormAction] Calling AI model (text-only, no image)...');
173
+ const aiStart = Date.now();
174
+ try {
175
+ res = await Promise.race([
176
+ generateText({
177
+ model: brain,
178
+ messages: [
179
+ {
180
+ role: 'user',
181
+ content: messageContentTextOnly
182
+ }
183
+ ],
184
+ maxRetries: 2, // Fewer retries for first attempt
185
+ output: Output.object({ schema: zFormActionResult, name: 'formActionResult' }),
186
+ }),
187
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`AI model call timed out after ${aiTimeout}ms`)), aiTimeout))
188
+ ]);
189
+ // Validate the response
190
+ if (!res.output || !res.output.mimicId || typeof res.output.mimicId !== 'number') {
191
+ throw new Error('First attempt returned invalid result, retrying with image...');
192
+ }
193
+ aiTime = Date.now() - aiStart;
194
+ console.log(`✅ [getFormAction] AI model responded successfully (text-only) in ${aiTime}ms (${(aiTime / 1000).toFixed(2)}s)`);
195
+ }
196
+ catch (error) {
197
+ // First attempt failed - retry with image
198
+ console.log(`⚠️ [getFormAction] First attempt failed, retrying with image: ${error instanceof Error ? error.message : String(error)}`);
199
+ const retryStart = Date.now();
200
+ usedImage = true;
201
+ try {
202
+ res = await Promise.race([
203
+ generateText({
204
+ model: brain,
205
+ messages: [
206
+ {
207
+ role: 'user',
208
+ content: messageContentWithImage
209
+ }
210
+ ],
211
+ maxRetries: 3,
212
+ output: Output.object({ schema: zFormActionResult, name: 'formActionResult' }),
213
+ }),
214
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`AI model call timed out after ${aiTimeout}ms`)), aiTimeout))
215
+ ]);
216
+ aiTime = Date.now() - retryStart;
217
+ console.log(`✅ [getFormAction] AI model responded successfully (with image) in ${aiTime}ms (${(aiTime / 1000).toFixed(2)}s)`);
218
+ }
219
+ catch (retryError) {
220
+ const elapsed = Date.now() - aiStart;
221
+ throw new Error(`AI model call failed after ${elapsed}ms (tried both text-only and with image): ${retryError instanceof Error ? retryError.message : String(retryError)}`);
222
+ }
223
+ }
152
224
  await countTokens(res);
225
+ const totalTime = Date.now() - startTime;
226
+ console.log(`⏱️ [getFormAction] Total time: ${totalTime}ms (${(totalTime / 1000).toFixed(2)}s)${usedImage ? ' (used image on retry)' : ' (text-only, no image needed)'}`);
227
+ console.log(` Breakdown: screenshot=${screenshotTime}ms, base64=${base64Time}ms, markers=${markerTime}ms, filter=${filterTime}ms, summary=${summaryTime}ms, prompt=${promptTime}ms, message=${messageTime}ms, AI=${aiTime}ms`);
228
+ // Validate that the AI model returned a valid structured output
229
+ // The output should always be defined when using structured outputs, but add defensive check
230
+ if (!res.output) {
231
+ throw new Error('AI model failed to generate valid form action result. The output is undefined.');
232
+ }
233
+ // Validate that mimicId exists and is valid
234
+ if (!res.output.mimicId || typeof res.output.mimicId !== 'number') {
235
+ throw new Error(`AI model returned invalid form action result: mimicId is missing or invalid. Received: ${JSON.stringify(res.output)}`);
236
+ }
153
237
  return res.output;
154
238
  };
155
239
  /**
@@ -173,19 +257,41 @@ export const executeFormAction = async (page, formActionResult, targetElement, t
173
257
  // Use LLM-generated description from the form action result
174
258
  const elementDescription = formActionResult.elementDescription || 'form field';
175
259
  let annotationDescription = '';
176
- // Get selector string for snapshot storage
177
- // Try to get a CSS selector representation if possible
260
+ // Generate Playwright code equivalent BEFORE performing the action
261
+ // This ensures the element is still available (before navigation/closure)
262
+ let playwrightCode;
178
263
  let selector = null;
179
264
  try {
180
- // Attempt to get a selector string from the locator
181
- // This is best-effort and may not always work
182
- const locatorString = targetElement.toString();
183
- if (locatorString && locatorString !== '[object Object]') {
184
- selector = locatorString;
185
- }
265
+ // Generate the best selector descriptor from the element
266
+ // This gives us a descriptive, stable selector for snapshot storage
267
+ // Use 30-second timeout for selector generation
268
+ const selectorDescriptor = await generateBestSelectorForElement(targetElement, { timeout: 30000 });
269
+ const selectorCode = selectorToPlaywrightCode(selectorDescriptor);
270
+ playwrightCode = generateFormCode(selectorCode, formActionResult.type, formActionResult.params.value);
271
+ // Store the selector descriptor for snapshot storage
272
+ selector = selectorDescriptor;
186
273
  }
187
274
  catch (error) {
188
- // If we can't get selector, that's okay - we'll rebuild from TargetInfo
275
+ // If generating from element fails, fall back to mimicId if available
276
+ // This can happen if the element is not available or page is closing
277
+ const errorMessage = error?.message || String(error);
278
+ // Check if page closed - this is a common issue with form submissions
279
+ if (errorMessage.includes('closed') || errorMessage.includes('Target page')) {
280
+ console.warn('Page closed during selector generation, using mimicId fallback');
281
+ }
282
+ if (formActionResult.mimicId) {
283
+ const selectorCode = `page.locator('[data-mimic-id="${formActionResult.mimicId}"]')`;
284
+ playwrightCode = generateFormCode(selectorCode, formActionResult.type, formActionResult.params.value);
285
+ // Create a CSS selector descriptor as fallback
286
+ selector = {
287
+ type: 'css',
288
+ selector: `[data-mimic-id="${formActionResult.mimicId}"]`
289
+ };
290
+ }
291
+ else {
292
+ // If we can't generate the code and no mimicId, log the error
293
+ console.warn('Could not generate Playwright code for form action:', errorMessage);
294
+ }
189
295
  }
190
296
  // Perform the form action with appropriate plain English annotation
191
297
  switch (formActionResult.type) {
@@ -200,7 +306,33 @@ export const executeFormAction = async (page, formActionResult, targetElement, t
200
306
  if (stepLower.includes('check') || stepLower.includes('select')) {
201
307
  console.warn(`⚠️ keypress action received empty value for checkbox operation - converting to check action`);
202
308
  annotationDescription = `→ Checking ${elementDescription} to select the option`;
203
- addAnnotation(testInfo, gherkinStep, annotationDescription);
309
+ // Update Playwright code and selector for check action
310
+ // Regenerate selector to ensure we have the best one for this specific action
311
+ try {
312
+ const selectorDescriptor = await generateBestSelectorForElement(targetElement, { timeout: 30000 });
313
+ const selectorCode = selectorToPlaywrightCode(selectorDescriptor);
314
+ playwrightCode = generateFormCode(selectorCode, 'check');
315
+ // Store the updated selector descriptor
316
+ selector = selectorDescriptor;
317
+ }
318
+ catch (error) {
319
+ // Fallback to mimicId if available
320
+ if (formActionResult.mimicId) {
321
+ const selectorCode = `page.locator('[data-mimic-id="${formActionResult.mimicId}"]')`;
322
+ playwrightCode = generateFormCode(selectorCode, 'check');
323
+ // Ensure selector is set even in fallback case
324
+ if (!selector) {
325
+ selector = {
326
+ type: 'css',
327
+ selector: `[data-mimic-id="${formActionResult.mimicId}"]`
328
+ };
329
+ }
330
+ }
331
+ else {
332
+ console.debug('Could not generate Playwright code for check action:', error);
333
+ }
334
+ }
335
+ addAnnotation(testInfo, gherkinStep, annotationDescription, playwrightCode);
204
336
  await targetElement.check();
205
337
  }
206
338
  else {
@@ -211,53 +343,168 @@ export const executeFormAction = async (page, formActionResult, targetElement, t
211
343
  // If it's not a valid single key and looks like text, use fill instead
212
344
  console.warn(`⚠️ keypress action received text "${keyValue}" - converting to fill action`);
213
345
  annotationDescription = `→ Filling ${elementDescription} with value "${keyValue}"`;
214
- addAnnotation(testInfo, gherkinStep, annotationDescription);
346
+ // Update Playwright code and selector for fill action
347
+ // Regenerate selector to ensure we have the best one for this specific action
348
+ try {
349
+ const selectorDescriptor = await generateBestSelectorForElement(targetElement, { timeout: 30000 });
350
+ const selectorCode = selectorToPlaywrightCode(selectorDescriptor);
351
+ playwrightCode = generateFormCode(selectorCode, 'fill', keyValue);
352
+ // Store the updated selector descriptor
353
+ selector = selectorDescriptor;
354
+ }
355
+ catch (error) {
356
+ // Fallback to mimicId if available
357
+ if (formActionResult.mimicId) {
358
+ const selectorCode = `page.locator('[data-mimic-id="${formActionResult.mimicId}"]')`;
359
+ playwrightCode = generateFormCode(selectorCode, 'fill', keyValue);
360
+ // Ensure selector is set even in fallback case
361
+ if (!selector) {
362
+ selector = {
363
+ type: 'css',
364
+ selector: `[data-mimic-id="${formActionResult.mimicId}"]`
365
+ };
366
+ }
367
+ }
368
+ else {
369
+ console.debug('Could not generate Playwright code for fill action:', error);
370
+ }
371
+ }
372
+ addAnnotation(testInfo, gherkinStep, annotationDescription, playwrightCode);
215
373
  await targetElement.fill(keyValue);
216
374
  }
217
375
  else {
218
376
  annotationDescription = `→ Pressing key "${keyValue}" on the keyboard`;
219
- addAnnotation(testInfo, gherkinStep, annotationDescription);
377
+ // For keypress action, ensure we have a selector stored even though we use page.keyboard
378
+ // This ensures we have a selector stored for snapshot/replay purposes
379
+ if (!selector) {
380
+ // If selector wasn't generated earlier, try to generate it now
381
+ try {
382
+ selector = await generateBestSelectorForElement(targetElement, { timeout: 30000 });
383
+ const selectorCode = selectorToPlaywrightCode(selector);
384
+ // Use the selector to focus the element before pressing key
385
+ playwrightCode = `${selectorCode}.focus();\nawait page.keyboard.press(${JSON.stringify(keyValue)});`;
386
+ }
387
+ catch (error) {
388
+ // Fallback to mimicId if available
389
+ if (formActionResult.mimicId) {
390
+ const selectorCode = `page.locator('[data-mimic-id="${formActionResult.mimicId}"]')`;
391
+ playwrightCode = `${selectorCode}.focus();\nawait page.keyboard.press(${JSON.stringify(keyValue)});`;
392
+ selector = {
393
+ type: 'css',
394
+ selector: `[data-mimic-id="${formActionResult.mimicId}"]`
395
+ };
396
+ }
397
+ else {
398
+ // Last resort: use generic keyboard press without selector
399
+ playwrightCode = `await page.keyboard.press(${JSON.stringify(keyValue)});`;
400
+ console.warn('Could not generate selector for keypress action, using generic keyboard press');
401
+ }
402
+ }
403
+ }
404
+ else {
405
+ // Use the already-generated selector
406
+ const selectorCode = selectorToPlaywrightCode(selector);
407
+ playwrightCode = `${selectorCode}.focus();\nawait page.keyboard.press(${JSON.stringify(keyValue)});`;
408
+ }
409
+ addAnnotation(testInfo, gherkinStep, annotationDescription, playwrightCode);
410
+ // Focus the element first, then press the key
411
+ await targetElement.focus();
220
412
  await page.keyboard.press(keyValue);
221
413
  }
222
414
  break;
223
415
  case 'type':
224
416
  annotationDescription = `→ Typing "${formActionResult.params.value}" using keyboard input`;
225
- addAnnotation(testInfo, gherkinStep, annotationDescription);
417
+ // For type action, we still want to use the generated selector for the target element
418
+ // even though the actual action uses page.keyboard.type
419
+ // This ensures we have a selector stored for snapshot/replay purposes
420
+ if (!selector) {
421
+ // If selector wasn't generated earlier, try to generate it now
422
+ try {
423
+ selector = await generateBestSelectorForElement(targetElement, { timeout: 30000 });
424
+ const selectorCode = selectorToPlaywrightCode(selector);
425
+ // Use the selector to focus the element before typing
426
+ playwrightCode = `${selectorCode}.focus();\nawait page.keyboard.type(${JSON.stringify(formActionResult.params.value)});`;
427
+ }
428
+ catch (error) {
429
+ // Fallback to mimicId if available
430
+ if (formActionResult.mimicId) {
431
+ const selectorCode = `page.locator('[data-mimic-id="${formActionResult.mimicId}"]')`;
432
+ playwrightCode = `${selectorCode}.focus();\nawait page.keyboard.type(${JSON.stringify(formActionResult.params.value)});`;
433
+ selector = {
434
+ type: 'css',
435
+ selector: `[data-mimic-id="${formActionResult.mimicId}"]`
436
+ };
437
+ }
438
+ else {
439
+ // Last resort: use generic keyboard type without selector
440
+ playwrightCode = `await page.keyboard.type(${JSON.stringify(formActionResult.params.value)});`;
441
+ console.warn('Could not generate selector for type action, using generic keyboard type');
442
+ }
443
+ }
444
+ }
445
+ else {
446
+ // Use the already-generated selector
447
+ const selectorCode = selectorToPlaywrightCode(selector);
448
+ playwrightCode = `${selectorCode}.focus();\nawait page.keyboard.type(${JSON.stringify(formActionResult.params.value)});`;
449
+ }
450
+ addAnnotation(testInfo, gherkinStep, annotationDescription, playwrightCode);
451
+ // Focus the element first, then type
452
+ await targetElement.focus();
226
453
  await page.keyboard.type(formActionResult.params.value);
227
454
  break;
228
455
  case 'fill':
229
456
  annotationDescription = `→ Filling ${elementDescription} with value "${formActionResult.params.value}"`;
230
- addAnnotation(testInfo, gherkinStep, annotationDescription);
457
+ addAnnotation(testInfo, gherkinStep, annotationDescription, playwrightCode);
231
458
  await targetElement.fill(formActionResult.params.value);
232
459
  break;
233
460
  case 'select':
234
461
  annotationDescription = `→ Selecting option "${formActionResult.params.value}" from ${elementDescription}`;
235
- addAnnotation(testInfo, gherkinStep, annotationDescription);
462
+ addAnnotation(testInfo, gherkinStep, annotationDescription, playwrightCode);
236
463
  await targetElement.selectOption(formActionResult.params.value);
237
464
  break;
238
465
  case 'uncheck':
239
466
  annotationDescription = `→ Unchecking ${elementDescription} to deselect the option`;
240
- addAnnotation(testInfo, gherkinStep, annotationDescription);
467
+ addAnnotation(testInfo, gherkinStep, annotationDescription, playwrightCode);
241
468
  await targetElement.uncheck();
242
469
  break;
243
470
  case 'check':
244
471
  annotationDescription = `→ Checking ${elementDescription} to select the option`;
245
- addAnnotation(testInfo, gherkinStep, annotationDescription);
472
+ addAnnotation(testInfo, gherkinStep, annotationDescription, playwrightCode);
246
473
  await targetElement.check();
247
474
  break;
475
+ case 'click':
476
+ annotationDescription = `→ Clicking on ${elementDescription} to select the radio button`;
477
+ addAnnotation(testInfo, gherkinStep, annotationDescription, playwrightCode);
478
+ await targetElement.click();
479
+ break;
248
480
  case 'setInputFiles':
249
481
  annotationDescription = `→ Uploading file "${formActionResult.params.value}" to ${elementDescription}`;
250
- addAnnotation(testInfo, gherkinStep, annotationDescription);
482
+ addAnnotation(testInfo, gherkinStep, annotationDescription, playwrightCode);
251
483
  await targetElement.setInputFiles(formActionResult.params.value);
252
484
  break;
253
485
  case 'clear':
254
486
  annotationDescription = `→ Clearing the contents of ${elementDescription}`;
255
- addAnnotation(testInfo, gherkinStep, annotationDescription);
487
+ addAnnotation(testInfo, gherkinStep, annotationDescription, playwrightCode);
256
488
  await targetElement.clear();
257
489
  break;
258
490
  default:
259
491
  throw new Error(`Unknown form action type: ${formActionResult.type}`);
260
492
  }
493
+ // Ensure selector is always set before returning
494
+ // This is critical for snapshot storage and replay functionality
495
+ if (!selector) {
496
+ // Last resort: use mimicId as fallback selector
497
+ if (formActionResult.mimicId) {
498
+ selector = {
499
+ type: 'css',
500
+ selector: `[data-mimic-id="${formActionResult.mimicId}"]`
501
+ };
502
+ console.warn(`⚠️ Using mimicId fallback selector for form action: ${formActionResult.type}`);
503
+ }
504
+ else {
505
+ throw new Error(`Could not generate or determine selector for form action: ${formActionResult.type}. This is required for snapshot storage.`);
506
+ }
507
+ }
261
508
  // Return action result and selector for snapshot storage
262
509
  return { actionResult: formActionResult, selector };
263
510
  };