@zibby/core 0.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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/package.json +94 -0
  4. package/src/agents/base.js +361 -0
  5. package/src/constants.js +47 -0
  6. package/src/enrichment/base.js +49 -0
  7. package/src/enrichment/enrichers/accessibility-enricher.js +197 -0
  8. package/src/enrichment/enrichers/dom-enricher.js +171 -0
  9. package/src/enrichment/enrichers/page-state-enricher.js +129 -0
  10. package/src/enrichment/enrichers/position-enricher.js +67 -0
  11. package/src/enrichment/index.js +96 -0
  12. package/src/enrichment/mcp-integration.js +149 -0
  13. package/src/enrichment/mcp-ref-enricher.js +78 -0
  14. package/src/enrichment/pipeline.js +192 -0
  15. package/src/enrichment/trace-text-enricher.js +115 -0
  16. package/src/framework/AGENTS.md +98 -0
  17. package/src/framework/agents/base.js +72 -0
  18. package/src/framework/agents/claude-strategy.js +278 -0
  19. package/src/framework/agents/cursor-strategy.js +459 -0
  20. package/src/framework/agents/index.js +105 -0
  21. package/src/framework/agents/utils/cursor-output-formatter.js +67 -0
  22. package/src/framework/agents/utils/openai-proxy-formatter.js +249 -0
  23. package/src/framework/code-generator.js +301 -0
  24. package/src/framework/constants.js +33 -0
  25. package/src/framework/context-loader.js +101 -0
  26. package/src/framework/function-bridge.js +78 -0
  27. package/src/framework/function-skill-registry.js +20 -0
  28. package/src/framework/graph-compiler.js +342 -0
  29. package/src/framework/graph.js +610 -0
  30. package/src/framework/index.js +28 -0
  31. package/src/framework/node-registry.js +163 -0
  32. package/src/framework/node.js +259 -0
  33. package/src/framework/output-parser.js +71 -0
  34. package/src/framework/skill-registry.js +55 -0
  35. package/src/framework/state-utils.js +52 -0
  36. package/src/framework/state.js +67 -0
  37. package/src/framework/tool-resolver.js +65 -0
  38. package/src/index.js +342 -0
  39. package/src/runtime/generation/base.js +46 -0
  40. package/src/runtime/generation/index.js +70 -0
  41. package/src/runtime/generation/mcp-ref-strategy.js +197 -0
  42. package/src/runtime/generation/stable-id-strategy.js +170 -0
  43. package/src/runtime/stable-id-runtime.js +248 -0
  44. package/src/runtime/verification/base.js +44 -0
  45. package/src/runtime/verification/index.js +67 -0
  46. package/src/runtime/verification/playwright-json-strategy.js +119 -0
  47. package/src/runtime/zibby-runtime.js +299 -0
  48. package/src/sync/index.js +2 -0
  49. package/src/sync/uploader.js +29 -0
  50. package/src/tools/run-playwright-test.js +158 -0
  51. package/src/utils/adf-converter.js +68 -0
  52. package/src/utils/ast-utils.js +37 -0
  53. package/src/utils/ci-setup.js +124 -0
  54. package/src/utils/cursor-utils.js +71 -0
  55. package/src/utils/logger.js +144 -0
  56. package/src/utils/mcp-config-writer.js +115 -0
  57. package/src/utils/node-schema-parser.js +522 -0
  58. package/src/utils/post-process-events.js +55 -0
  59. package/src/utils/result-handler.js +102 -0
  60. package/src/utils/ripple-effect.js +84 -0
  61. package/src/utils/selector-generator.js +239 -0
  62. package/src/utils/streaming-parser.js +387 -0
  63. package/src/utils/test-post-processor.js +211 -0
  64. package/src/utils/timeline.js +217 -0
  65. package/src/utils/trace-parser.js +325 -0
  66. package/src/utils/video-organizer.js +91 -0
  67. package/templates/browser-test-automation/README.md +114 -0
  68. package/templates/browser-test-automation/graph.js +54 -0
  69. package/templates/browser-test-automation/nodes/execute-live.js +250 -0
  70. package/templates/browser-test-automation/nodes/generate-script.js +77 -0
  71. package/templates/browser-test-automation/nodes/index.js +3 -0
  72. package/templates/browser-test-automation/nodes/preflight.js +59 -0
  73. package/templates/browser-test-automation/nodes/utils.js +154 -0
  74. package/templates/browser-test-automation/result-handler.js +286 -0
  75. package/templates/code-analysis/graph.js +72 -0
  76. package/templates/code-analysis/index.js +18 -0
  77. package/templates/code-analysis/nodes/analyze-ticket-node.js +204 -0
  78. package/templates/code-analysis/nodes/create-pr-node.js +175 -0
  79. package/templates/code-analysis/nodes/finalize-node.js +118 -0
  80. package/templates/code-analysis/nodes/generate-code-node.js +425 -0
  81. package/templates/code-analysis/nodes/generate-test-cases-node.js +376 -0
  82. package/templates/code-analysis/nodes/services/prMetaService.js +86 -0
  83. package/templates/code-analysis/nodes/setup-node.js +142 -0
  84. package/templates/code-analysis/prompts/analyze-ticket.md +181 -0
  85. package/templates/code-analysis/prompts/generate-code.md +33 -0
  86. package/templates/code-analysis/prompts/generate-test-cases.md +110 -0
  87. package/templates/code-analysis/state.js +40 -0
  88. package/templates/code-implementation/graph.js +35 -0
  89. package/templates/code-implementation/index.js +7 -0
  90. package/templates/code-implementation/state.js +14 -0
  91. package/templates/global-setup.js +56 -0
  92. package/templates/index.js +94 -0
  93. package/templates/register-nodes.js +24 -0
@@ -0,0 +1,197 @@
1
+ import { TestGenerationStrategy } from './base.js';
2
+ import { readFileSync, writeFileSync } from 'fs';
3
+
4
+ /**
5
+ * MCPRefStrategy - Generates tests using MCP element descriptions
6
+ *
7
+ * This strategy generates tests that use the EXACT elements that were
8
+ * interacted with during recording, identified by their MCP ref IDs.
9
+ *
10
+ * Reliability: 99% - Works as long as the DOM structure is similar between runs
11
+ */
12
+ export class MCPRefStrategy extends TestGenerationStrategy {
13
+ constructor() {
14
+ super(
15
+ 'mcp-ref',
16
+ 'MCP Reference Replay (Exact 1:1)',
17
+ 200 // Highest priority - most accurate
18
+ );
19
+ }
20
+
21
+ canGenerate(context) {
22
+ // Check if events have MCP element descriptions
23
+ const eventsPath = context.eventsPath || `${context.sessionPath}/execute_live/events.json`;
24
+
25
+ try {
26
+ const events = JSON.parse(readFileSync(eventsPath, 'utf-8'));
27
+ const hasMCPElements = events.some(e => e.data?.params?.element);
28
+
29
+ if (!hasMCPElements) {
30
+ console.log('[MCPRefStrategy] ❌ No MCP element descriptions found in events');
31
+ return false;
32
+ }
33
+
34
+ console.log('[MCPRefStrategy] ✅ MCP element descriptions available');
35
+ return true;
36
+ } catch (e) {
37
+ console.log('[MCPRefStrategy] ❌ Failed to read events:', e.message);
38
+ return false;
39
+ }
40
+ }
41
+
42
+ getName() {
43
+ return 'MCP Reference Replay (Exact 1:1)';
44
+ }
45
+
46
+ getPriority() {
47
+ return 200;
48
+ }
49
+
50
+ async generate(context) {
51
+ const { testFilePath, sessionPath, state } = context;
52
+ const eventsPath = `${sessionPath}/execute_live/events.json`;
53
+ const testTitle = state?.title || 'Generated Test';
54
+
55
+ console.log('[MCPRefStrategy] 🎯 Generating test using MCP element descriptions (1:1 replay)');
56
+ console.log(`[MCPRefStrategy] events: ${eventsPath}`);
57
+ console.log(`[MCPRefStrategy] output: ${testFilePath}`);
58
+
59
+ const events = JSON.parse(readFileSync(eventsPath, 'utf-8'));
60
+ const actionEvents = events.filter(e =>
61
+ ['navigate', 'type', 'fill', 'click', 'select_option'].includes(e.type)
62
+ );
63
+
64
+ let testCode = `import { test, expect } from '@playwright/test';\n`;
65
+ testCode += `import { ZibbyRuntime } from '@zibby/core';\n\n`;
66
+ testCode += `test('${testTitle}', async ({ page }) => {\n`;
67
+ testCode += ` const timestamp = Date.now();\n\n`;
68
+
69
+ for (const event of actionEvents) {
70
+ const mcpElement = event.data?.params?.element || 'element';
71
+ const _ref = event.data?.params?.ref;
72
+
73
+ // Priority order for element text (handles Chinese/multi-language):
74
+ // 1. traceActualText - extracted from accessibility tree in trace.zip (MOST RELIABLE)
75
+ // 2. traceActualRole - extracted from accessibility tree (ACTUAL role)
76
+ // 3. traceActualAriaLabel - extracted from accessibility tree (ACTUAL aria-label)
77
+ // 4. actualText - captured during live recording (if available)
78
+ // 5. mcpElement - AI's description (fallback)
79
+ const traceText = event.enrichedData?.traceActualText;
80
+ const traceRole = event.enrichedData?.traceActualRole;
81
+ const _traceLabel = event.enrichedData?.traceActualAriaLabel;
82
+ const liveText = event.enrichedData?.actualText;
83
+ const actualText = traceText || liveText;
84
+ const actualRole = traceRole || event.enrichedData?.actualRole;
85
+
86
+ // Use FULL description for text matching (don't strip role suffix)
87
+ const _fullText = actualText || mcpElement;
88
+ // Use name only (without role) for getByRole
89
+ const nameOnly = actualText || this._extractName(mcpElement);
90
+ const selectorRole = actualRole || this._extractRole(mcpElement);
91
+
92
+ const sourceNote = traceText ? ' [accessibility-tree]' : (liveText ? ' [live]' : ' [AI]');
93
+
94
+ if (event.type === 'navigate') {
95
+ testCode += ` await page.goto('${event.data.params.url}');\n\n`;
96
+ } else if (event.type === 'click') {
97
+ testCode += ` // ${mcpElement}${actualText ? ` (actual: "${actualText}")${sourceNote}` : ''}\n`;
98
+ testCode += ` await ZibbyRuntime.step(page, {\n`;
99
+ testCode += ` name: '${this._escapeString(mcpElement)}',\n`;
100
+ testCode += ` action: 'click',\n`;
101
+ testCode += ` strategies: [\n`;
102
+
103
+ // 95% RELIABLE STRATEGY:
104
+ // Priority 1: Use getByRole with the EXACT description from trace
105
+ // Playwright aggregates ALL nested text automatically, so "HR Our core HR system button"
106
+ // will match even if text is split across <div>HR</div><div>Our core HR system</div>
107
+ testCode += ` { type: 'role', role: '${selectorRole}', name: '${this._escapeString(nameOnly)}' },\n`;
108
+
109
+ // Priority 2: Text search (Playwright searches ALL nested text)
110
+ testCode += ` { type: 'text', text: '${this._escapeString(nameOnly)}' }\n`;
111
+
112
+ testCode += ` ]\n`;
113
+ testCode += ` });\n\n`;
114
+ } else if (event.type === 'fill' || event.type === 'type') {
115
+ const value = event.data.params.text;
116
+ testCode += ` // ${mcpElement}${actualText ? ` (actual: "${actualText}")${sourceNote}` : ''}\n`;
117
+ testCode += ` await ZibbyRuntime.step(page, {\n`;
118
+ testCode += ` name: '${this._escapeString(mcpElement)}',\n`;
119
+ testCode += ` action: 'fill',\n`;
120
+ testCode += ` value: '${this._escapeString(value)}',\n`;
121
+ testCode += ` strategies: [\n`;
122
+ testCode += ` { type: 'role', role: '${selectorRole}', name: '${this._escapeString(nameOnly)}' },\n`;
123
+ testCode += ` { type: 'attributes', placeholder: '${this._escapeString(nameOnly)}' }\n`;
124
+ testCode += ` ]\n`;
125
+ testCode += ` });\n\n`;
126
+ } else if (event.type === 'select_option') {
127
+ const values = event.data.params.values;
128
+ const value = Array.isArray(values) ? values[0] : values;
129
+ testCode += ` // ${mcpElement}${actualText ? ` (actual: "${actualText}")${sourceNote}` : ''}\n`;
130
+ testCode += ` await ZibbyRuntime.step(page, {\n`;
131
+ testCode += ` name: '${this._escapeString(mcpElement)}',\n`;
132
+ testCode += ` action: 'select',\n`;
133
+ testCode += ` value: '${this._escapeString(value)}',\n`;
134
+ testCode += ` strategies: [\n`;
135
+ testCode += ` { type: 'role', role: 'combobox', name: '${this._escapeString(nameOnly)}' }\n`;
136
+ testCode += ` ]\n`;
137
+ testCode += ` });\n\n`;
138
+ }
139
+ }
140
+
141
+ testCode += `});\n`;
142
+
143
+ writeFileSync(testFilePath, testCode);
144
+
145
+ console.log(`[MCPRefStrategy] ✅ Generated test with ${actionEvents.length} actions using MCP descriptions`);
146
+
147
+ return {
148
+ success: true,
149
+ testPath: testFilePath,
150
+ method: 'MCP Reference Replay (1:1)',
151
+ actionsGenerated: actionEvents.length
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Extract role from MCP element description
157
+ * e.g., "Login button" -> "button", "Email textbox" -> "textbox"
158
+ */
159
+ _extractRole(mcpElement) {
160
+ const lowerElement = mcpElement.toLowerCase();
161
+
162
+ if (lowerElement.includes('button')) return 'button';
163
+ if (lowerElement.includes('textbox')) return 'textbox';
164
+ if (lowerElement.includes('link')) return 'link';
165
+ if (lowerElement.includes('checkbox')) return 'checkbox';
166
+ if (lowerElement.includes('radio')) return 'radio';
167
+ if (lowerElement.includes('combobox')) return 'combobox';
168
+ if (lowerElement.includes('heading')) return 'heading';
169
+
170
+ // Default to generic
171
+ return 'button';
172
+ }
173
+
174
+ /**
175
+ * Extract name from MCP element description
176
+ * e.g., "Login button" -> "Login", "Email textbox" -> "Email"
177
+ */
178
+ _extractName(mcpElement) {
179
+ // Remove common role suffixes
180
+ return mcpElement
181
+ .replace(/\s+(button|textbox|link|checkbox|radio|combobox)$/i, '')
182
+ .trim();
183
+ }
184
+
185
+ /**
186
+ * Escape special regex characters for safe use in regex
187
+ * Important for Chinese, Arabic, and special symbols
188
+ */
189
+ _escapeRegex(text) {
190
+ // Escape special regex characters but preserve Unicode (Chinese, etc.)
191
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
192
+ }
193
+
194
+ _escapeString(text) {
195
+ return text.replace(/'/g, "\\'").replace(/\n/g, '\\n');
196
+ }
197
+ }
@@ -0,0 +1,170 @@
1
+ import { TestGenerationStrategy } from './base.js';
2
+ import { readFileSync, writeFileSync } from 'fs';
3
+
4
+ /**
5
+ * StableIdStrategy - Generates tests using injected stable IDs
6
+ *
7
+ * This strategy uses deterministic IDs injected into the DOM during recording.
8
+ * The same IDs are re-injected during test execution for direct element lookup.
9
+ *
10
+ * Reliability: 95%+ - Works as long as DOM structure is similar between runs
11
+ * Bypasses all text-matching and CSS selector fragility issues.
12
+ */
13
+ export class StableIdStrategy extends TestGenerationStrategy {
14
+ constructor() {
15
+ super(
16
+ 'stable-id',
17
+ 'Stable ID Injection (Experimental)',
18
+ 300 // Highest priority - most reliable when available
19
+ );
20
+ }
21
+
22
+ canGenerate(context) {
23
+ // Check if events have stable IDs
24
+ const eventsPath = context.eventsPath || `${context.sessionPath}/execute_live/events.json`;
25
+
26
+ try {
27
+ const events = JSON.parse(readFileSync(eventsPath, 'utf-8'));
28
+ const hasStableIds = events.some(e => e.stableId || e.data?.stableId);
29
+
30
+ if (!hasStableIds) {
31
+ console.log('[StableIdStrategy] ❌ No stable IDs found in events');
32
+ return false;
33
+ }
34
+
35
+ console.log('[StableIdStrategy] ✅ Stable IDs available');
36
+ return true;
37
+ } catch (e) {
38
+ console.log('[StableIdStrategy] ❌ Failed to read events:', e.message);
39
+ return false;
40
+ }
41
+ }
42
+
43
+ getName() {
44
+ return 'Stable ID Injection (Experimental)';
45
+ }
46
+
47
+ getPriority() {
48
+ return 300;
49
+ }
50
+
51
+ async generate(context) {
52
+ const { testFilePath, sessionPath, state } = context;
53
+ const eventsPath = `${sessionPath}/execute_live/events.json`;
54
+ const testTitle = state?.title || 'Generated Test';
55
+
56
+ console.log('[StableIdStrategy] 🎯 Generating test using stable IDs');
57
+ console.log(`[StableIdStrategy] events: ${eventsPath}`);
58
+ console.log(`[StableIdStrategy] output: ${testFilePath}`);
59
+
60
+ const events = JSON.parse(readFileSync(eventsPath, 'utf-8'));
61
+ const actionEvents = events.filter(e =>
62
+ ['navigate', 'type', 'fill', 'click', 'select_option', 'select'].includes(e.type)
63
+ );
64
+
65
+ let testCode = `import { test, expect } from '@playwright/test';\n`;
66
+ testCode += `import { StableIdRuntime } from '@zibby/core';\n\n`;
67
+ testCode += `test('${testTitle}', async ({ page }) => {\n`;
68
+
69
+ let skipNavigates = false;
70
+ let lastClickId = null;
71
+
72
+ for (let i = 0; i < actionEvents.length; i++) {
73
+ const event = actionEvents[i];
74
+ const stableId = event.stableId || event.data?.stableId;
75
+ const mcpElement = event.data?.element || event.data?.params?.element || 'element';
76
+
77
+ // Skip duplicate consecutive clicks on same element
78
+ if (event.type === 'click' && stableId && stableId === lastClickId) {
79
+ console.log(`[StableIdStrategy] Skipping duplicate click on ${stableId}`);
80
+ continue;
81
+ }
82
+
83
+ if (event.type === 'navigate') {
84
+ const url = event.data?.url || event.data?.params?.url;
85
+ if (url && !skipNavigates) {
86
+ testCode += ` await page.goto('${url}');\n`;
87
+ testCode += ` await StableIdRuntime.injectStableIds(page);\n\n`;
88
+ }
89
+ // Keep skipNavigates as-is - multiple navigates after click should all be skipped
90
+ } else if (event.type === 'click') {
91
+ skipNavigates = true;
92
+ lastClickId = stableId;
93
+ if (stableId) {
94
+ testCode += ` await StableIdRuntime.clickWithRetry(page, '${stableId}');\n`;
95
+ } else {
96
+ const selector = this._generateSemanticSelector(mcpElement);
97
+ testCode += ` await ${selector}.click();\n`;
98
+ testCode += ` await StableIdRuntime.afterNavigation(page);\n`;
99
+ }
100
+ } else if (event.type === 'fill' || event.type === 'type') {
101
+ skipNavigates = false;
102
+ const value = event.data?.text || event.data?.params?.text || '';
103
+ if (stableId) {
104
+ testCode += ` await StableIdRuntime.fillWithRetry(page, '${stableId}', '${this._escapeString(value)}');\n`;
105
+ } else {
106
+ testCode += ` await page.getByPlaceholder('${this._escapeString(mcpElement)}').fill('${this._escapeString(value)}');\n`;
107
+ }
108
+ } else if (event.type === 'select_option' || event.type === 'select') {
109
+ skipNavigates = false;
110
+ const values = event.data?.values || event.data?.params?.values;
111
+ const value = Array.isArray(values) ? values[0] : values || '';
112
+ if (stableId) {
113
+ testCode += ` await StableIdRuntime.selectWithRetry(page, '${stableId}', '${this._escapeString(value)}');\n`;
114
+ } else {
115
+ testCode += ` await page.locator('select').selectOption('${this._escapeString(value)}');\n`;
116
+ }
117
+ }
118
+ }
119
+
120
+ testCode += `});\n`;
121
+
122
+ writeFileSync(testFilePath, testCode);
123
+
124
+ console.log(`[StableIdStrategy] ✅ Generated test with ${actionEvents.length} actions using stable IDs`);
125
+
126
+ return {
127
+ success: true,
128
+ testPath: testFilePath,
129
+ method: 'Stable ID Injection (Experimental)',
130
+ actionsGenerated: actionEvents.length
131
+ };
132
+ }
133
+
134
+ _escapeString(text) {
135
+ return text.replace(/'/g, "\\'").replace(/\n/g, '\\n');
136
+ }
137
+
138
+ _generateSemanticSelector(elementDesc) {
139
+ // Parse element description like "Login button", "Email textbox", "Our people link"
140
+ const desc = elementDesc.toLowerCase();
141
+
142
+ // Extract role and name
143
+ let role = 'locator';
144
+ let name = elementDesc;
145
+
146
+ if (desc.includes('button')) {
147
+ role = 'button';
148
+ name = elementDesc.replace(/\s*button\s*/gi, '').trim();
149
+ } else if (desc.includes('link')) {
150
+ role = 'link';
151
+ name = elementDesc.replace(/\s*link\s*/gi, '').trim();
152
+ } else if (desc.includes('textbox')) {
153
+ role = 'textbox';
154
+ name = elementDesc.replace(/\s*textbox\s*/gi, '').trim();
155
+ } else if (desc.includes('checkbox')) {
156
+ role = 'checkbox';
157
+ name = elementDesc.replace(/\s*checkbox\s*/gi, '').trim();
158
+ } else if (desc.includes('combobox') || desc.includes('dropdown') || desc.includes('select')) {
159
+ role = 'combobox';
160
+ name = elementDesc.replace(/\s*(combobox|dropdown|select)\s*/gi, '').trim();
161
+ }
162
+
163
+ if (role !== 'locator' && name) {
164
+ return `page.getByRole('${role}', { name: '${this._escapeString(name)}' })`;
165
+ }
166
+ // Generic fallback - try text match
167
+ return `page.getByText('${this._escapeString(elementDesc)}')`;
168
+
169
+ }
170
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * StableIdRuntime - Re-inject stable IDs during test execution
3
+ *
4
+ * MUST use EXACT SAME algorithm as mcps/browser/src/stable-id-inject.js
5
+ * This code runs in the browser context where browser globals are available
6
+ */
7
+ /* global document, window, URL */
8
+ export class StableIdRuntime {
9
+ static async beforeEach(page) {
10
+ await this.injectStableIds(page);
11
+ page.on('load', async () => {
12
+ await this.injectStableIds(page);
13
+ });
14
+ }
15
+
16
+ static async afterNavigation(page) {
17
+ // Wait for any pending navigation to complete
18
+ await page.waitForLoadState('domcontentloaded').catch(() => {});
19
+ await this.injectStableIds(page);
20
+ }
21
+
22
+ static async clickWithRetry(page, stableId, options = {}) {
23
+ const timeout = options.timeout || 10000;
24
+ const selector = `[data-zibby-id="${stableId}"]`;
25
+ const startTime = Date.now();
26
+
27
+ while (Date.now() - startTime < timeout) {
28
+ await this.injectStableIds(page);
29
+
30
+ const el = page.locator(selector);
31
+ if (await el.count() > 0) {
32
+ try {
33
+ await el.click({ timeout: 2000 });
34
+ return;
35
+ } catch (e) {
36
+ // If element is covered, try force click
37
+ if (e.message.includes('intercepts pointer')) {
38
+ await el.click({ force: true });
39
+ return;
40
+ }
41
+ }
42
+ }
43
+
44
+ await page.waitForTimeout(200);
45
+ }
46
+
47
+ throw new Error(`Element ${selector} not found after ${timeout}ms`);
48
+ }
49
+
50
+ static async fillWithRetry(page, stableId, value, timeout = 10000) {
51
+ const selector = `[data-zibby-id="${stableId}"]`;
52
+ const startTime = Date.now();
53
+
54
+ while (Date.now() - startTime < timeout) {
55
+ await this.injectStableIds(page);
56
+
57
+ const el = page.locator(selector);
58
+ if (await el.count() > 0) {
59
+ await el.fill(value);
60
+ return;
61
+ }
62
+
63
+ await page.waitForTimeout(200);
64
+ }
65
+
66
+ throw new Error(`Element ${selector} not found after ${timeout}ms`);
67
+ }
68
+
69
+ static async selectWithRetry(page, stableId, value, timeout = 10000) {
70
+ const selector = `[data-zibby-id="${stableId}"]`;
71
+ const startTime = Date.now();
72
+
73
+ while (Date.now() - startTime < timeout) {
74
+ await this.injectStableIds(page);
75
+
76
+ const el = page.locator(selector);
77
+ if (await el.count() > 0) {
78
+ await el.selectOption(value);
79
+ return;
80
+ }
81
+
82
+ await page.waitForTimeout(200);
83
+ }
84
+
85
+ throw new Error(`Element ${selector} not found after ${timeout}ms`);
86
+ }
87
+
88
+ static async injectStableIds(page) {
89
+ try {
90
+ await page.evaluate(() => {
91
+ // EXACT SAME algorithm as stable-id-inject.js
92
+
93
+ function getSemanticLabel(el) {
94
+ if (el.getAttribute('aria-label')) return el.getAttribute('aria-label').trim();
95
+
96
+ const labelledBy = el.getAttribute('aria-labelledby');
97
+ if (labelledBy) {
98
+ const labelEl = document.getElementById(labelledBy);
99
+ if (labelEl) return labelEl.textContent.trim();
100
+ }
101
+
102
+ if (el.id) {
103
+ const label = document.querySelector(`label[for="${el.id}"]`);
104
+ if (label) return label.textContent.trim();
105
+ }
106
+
107
+ const parentLabel = el.closest('label');
108
+ if (parentLabel) {
109
+ const clone = parentLabel.cloneNode(true);
110
+ clone.querySelectorAll('input, select, textarea').forEach(e => e.remove());
111
+ const text = clone.textContent.trim();
112
+ if (text) return text;
113
+ }
114
+
115
+ if (el.placeholder) return el.placeholder;
116
+
117
+ const tag = el.tagName.toLowerCase();
118
+ if (tag === 'button' || tag === 'a' || el.getAttribute('role') === 'button') {
119
+ return (el.textContent || '').trim().slice(0, 50);
120
+ }
121
+
122
+ if (el.title) return el.title;
123
+
124
+ if (tag === 'input' && (el.type === 'submit' || el.type === 'button')) {
125
+ return el.value || '';
126
+ }
127
+
128
+ return '';
129
+ }
130
+
131
+ function getSemanticContext(el) {
132
+ const context = [];
133
+
134
+ const form = el.closest('form');
135
+ if (form) {
136
+ if (form.id) context.push(`form#${ form.id}`);
137
+ else if (form.name) context.push(`form[name=${ form.name }]`);
138
+ else if (form.action) {
139
+ try {
140
+ const action = new URL(form.action, window.location.origin).pathname;
141
+ context.push(`form[action=${ action }]`);
142
+ } catch {
143
+ context.push(`form[action=${ form.getAttribute('action') }]`);
144
+ }
145
+ } else {
146
+ const forms = document.querySelectorAll('form');
147
+ const index = Array.from(forms).indexOf(form);
148
+ context.push(`form:nth(${ index })`);
149
+ }
150
+ }
151
+
152
+ const landmark = el.closest('header, nav, main, footer, aside, [role="banner"], [role="navigation"], [role="main"], [role="contentinfo"]');
153
+ if (landmark) {
154
+ const tag = landmark.tagName.toLowerCase();
155
+ const role = landmark.getAttribute('role');
156
+ context.push(role || tag);
157
+ }
158
+
159
+ const section = el.closest('section, article, [role="region"]');
160
+ if (section) {
161
+ const heading = section.querySelector('h1, h2, h3, h4, h5, h6');
162
+ if (heading) {
163
+ context.push(`section:${ heading.textContent.trim().slice(0, 30)}`);
164
+ }
165
+ }
166
+
167
+ const fieldset = el.closest('fieldset');
168
+ if (fieldset) {
169
+ const legend = fieldset.querySelector('legend');
170
+ if (legend) {
171
+ context.push(`fieldset:${ legend.textContent.trim()}`);
172
+ }
173
+ }
174
+
175
+ const dialog = el.closest('dialog, [role="dialog"], [role="alertdialog"]');
176
+ if (dialog) {
177
+ const title = dialog.querySelector('[role="heading"], h1, h2, h3');
178
+ if (title) context.push(`dialog:${ title.textContent.trim().slice(0, 30)}`);
179
+ else context.push('dialog');
180
+ }
181
+
182
+ return context.join('/');
183
+ }
184
+
185
+ function computeStableId(el) {
186
+ const tag = el.tagName.toLowerCase();
187
+ const id = el.id || '';
188
+ const name = el.name || '';
189
+ const type = el.type || '';
190
+ const role = el.getAttribute('role') || '';
191
+
192
+ let href = '';
193
+ if (el.href) {
194
+ try {
195
+ href = new URL(el.href, window.location.origin).pathname.slice(0, 50);
196
+ } catch {
197
+ href = el.getAttribute('href')?.slice(0, 50) || '';
198
+ }
199
+ }
200
+
201
+ const label = getSemanticLabel(el).slice(0, 50).replace(/\s+/g, ' ');
202
+ const context = getSemanticContext(el);
203
+
204
+ const sig = [tag, id, name, type, role, href, label, context].join('|');
205
+
206
+ let hash = 5381;
207
+ for (let i = 0; i < sig.length; i++) {
208
+ hash = ((hash << 5) + hash) ^ sig.charCodeAt(i);
209
+ }
210
+
211
+ return `zibby-${ (hash >>> 0).toString(36)}`;
212
+ }
213
+
214
+ const selectors = [
215
+ 'button', 'a', 'input', 'select', 'textarea',
216
+ 'label[for]',
217
+ '[role="button"]', '[role="link"]', '[role="textbox"]',
218
+ '[role="checkbox"]', '[role="radio"]', '[role="combobox"]',
219
+ '[role="menuitem"]', '[role="tab"]', '[role="option"]',
220
+ '[role="switch"]', '[role="slider"]',
221
+ '[onclick]', '[data-action]'
222
+ ].join(', ');
223
+
224
+ const idCounts = new Map();
225
+ let count = 0;
226
+
227
+ document.querySelectorAll(selectors).forEach((el) => {
228
+ const style = window.getComputedStyle(el);
229
+ if (style.display === 'none' || style.visibility === 'hidden') return;
230
+
231
+ let stableId = computeStableId(el);
232
+
233
+ const baseId = stableId;
234
+ const existing = idCounts.get(baseId) || 0;
235
+ if (existing > 0) stableId = `${baseId }-${ existing}`;
236
+ idCounts.set(baseId, existing + 1);
237
+
238
+ el.setAttribute('data-zibby-id', stableId);
239
+ count++;
240
+ });
241
+
242
+ console.log(`[Zibby] Injected ${count} stable IDs`);
243
+ });
244
+ } catch (_e) {
245
+ // Navigation destroyed context - this is OK, IDs will be injected on next retry
246
+ }
247
+ }
248
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Base class for test verification strategies
3
+ * Defines the interface that all verification strategies must implement
4
+ */
5
+ export class TestVerificationStrategy {
6
+ /**
7
+ * Verify a generated test
8
+ * @param {Object} context - Verification context
9
+ * @param {string} context.testFilePath - Path to test file
10
+ * @param {string} context.cwd - Current working directory
11
+ * @param {number} context.timeout - Test timeout in ms
12
+ * @returns {Promise<Object>} - { success: boolean, passed: number, failed: number, errorDetails?: string }
13
+ */
14
+ async verify(_context) {
15
+ throw new Error('TestVerificationStrategy.verify() must be implemented');
16
+ }
17
+
18
+ /**
19
+ * Check if this strategy can verify the test
20
+ * @param {Object} context - Same context as verify()
21
+ * @returns {boolean}
22
+ */
23
+ canVerify(_context) {
24
+ throw new Error('TestVerificationStrategy.canVerify() must be implemented');
25
+ }
26
+
27
+ /**
28
+ * Get strategy name for logging
29
+ * @returns {string}
30
+ */
31
+ getName() {
32
+ throw new Error('TestVerificationStrategy.getName() must be implemented');
33
+ }
34
+
35
+ /**
36
+ * Get strategy priority (higher = try first)
37
+ * @returns {number}
38
+ */
39
+ getPriority() {
40
+ return 0;
41
+ }
42
+ }
43
+
44
+ export default TestVerificationStrategy;