mycontext-cli 4.0.0 → 4.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 (67) hide show
  1. package/README.md +56 -114
  2. package/dist/cli.js +67 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/clients/MyContextAIClient.d.ts.map +1 -1
  5. package/dist/clients/MyContextAIClient.js +12 -0
  6. package/dist/clients/MyContextAIClient.js.map +1 -1
  7. package/dist/commands/agent.d.ts +22 -0
  8. package/dist/commands/agent.d.ts.map +1 -0
  9. package/dist/commands/agent.js +245 -0
  10. package/dist/commands/agent.js.map +1 -0
  11. package/dist/commands/init.d.ts +5 -0
  12. package/dist/commands/init.d.ts.map +1 -1
  13. package/dist/commands/init.js +18 -2
  14. package/dist/commands/init.js.map +1 -1
  15. package/dist/commands/status.d.ts.map +1 -1
  16. package/dist/commands/status.js +0 -9
  17. package/dist/commands/status.js.map +1 -1
  18. package/dist/commands/sync-readme.d.ts +14 -0
  19. package/dist/commands/sync-readme.d.ts.map +1 -0
  20. package/dist/commands/sync-readme.js +131 -0
  21. package/dist/commands/sync-readme.js.map +1 -0
  22. package/dist/commands/test.d.ts +76 -0
  23. package/dist/commands/test.d.ts.map +1 -0
  24. package/dist/commands/test.js +361 -0
  25. package/dist/commands/test.js.map +1 -0
  26. package/dist/mcp/browser-test-runner.d.ts +89 -0
  27. package/dist/mcp/browser-test-runner.d.ts.map +1 -0
  28. package/dist/mcp/browser-test-runner.js +786 -0
  29. package/dist/mcp/browser-test-runner.js.map +1 -0
  30. package/dist/mcp/test-mission-manager.d.ts +82 -0
  31. package/dist/mcp/test-mission-manager.d.ts.map +1 -0
  32. package/dist/mcp/test-mission-manager.js +327 -0
  33. package/dist/mcp/test-mission-manager.js.map +1 -0
  34. package/dist/mcp/test-reporter.d.ts +54 -0
  35. package/dist/mcp/test-reporter.d.ts.map +1 -0
  36. package/dist/mcp/test-reporter.js +358 -0
  37. package/dist/mcp/test-reporter.js.map +1 -0
  38. package/dist/mcp/testing-server.d.ts +36 -0
  39. package/dist/mcp/testing-server.d.ts.map +1 -0
  40. package/dist/mcp/testing-server.js +516 -0
  41. package/dist/mcp/testing-server.js.map +1 -0
  42. package/dist/package.json +6 -2
  43. package/dist/services/ContextService.d.ts +38 -0
  44. package/dist/services/ContextService.d.ts.map +1 -0
  45. package/dist/services/ContextService.js +104 -0
  46. package/dist/services/ContextService.js.map +1 -0
  47. package/dist/services/ProbeManager.d.ts +32 -0
  48. package/dist/services/ProbeManager.d.ts.map +1 -0
  49. package/dist/services/ProbeManager.js +116 -0
  50. package/dist/services/ProbeManager.js.map +1 -0
  51. package/dist/types/design-pipeline.d.ts +6 -0
  52. package/dist/types/design-pipeline.d.ts.map +1 -1
  53. package/dist/types/flow-testing.d.ts +179 -0
  54. package/dist/types/flow-testing.d.ts.map +1 -0
  55. package/dist/types/flow-testing.js +7 -0
  56. package/dist/types/flow-testing.js.map +1 -0
  57. package/dist/types/index.d.ts +1 -0
  58. package/dist/types/index.d.ts.map +1 -1
  59. package/dist/utils/designManifestManager.d.ts +4 -0
  60. package/dist/utils/designManifestManager.d.ts.map +1 -1
  61. package/dist/utils/designManifestManager.js +161 -0
  62. package/dist/utils/designManifestManager.js.map +1 -1
  63. package/dist/utils/githubModelsClient.d.ts.map +1 -1
  64. package/dist/utils/githubModelsClient.js +6 -2
  65. package/dist/utils/githubModelsClient.js.map +1 -1
  66. package/dist/utils/mcpTools.d.ts +21 -21
  67. package/package.json +6 -2
@@ -0,0 +1,786 @@
1
+ "use strict";
2
+ /**
3
+ * Browser Test Runner
4
+ *
5
+ * Executes test missions using Playwright + AI-powered navigation.
6
+ * The AI agent autonomously navigates the UI, makes decisions, and validates outcomes.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __importDefault = (this && this.__importDefault) || function (mod) {
42
+ return (mod && mod.__esModule) ? mod : { "default": mod };
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.BrowserTestRunner = void 0;
46
+ const playwright_1 = require("playwright");
47
+ const path = __importStar(require("path"));
48
+ const fs = __importStar(require("fs-extra"));
49
+ const uuid_1 = require("uuid");
50
+ const ContextService_1 = require("../services/ContextService");
51
+ const chalk_1 = __importDefault(require("chalk"));
52
+ class BrowserTestRunner {
53
+ constructor(projectPath) {
54
+ this.projectPath = projectPath;
55
+ this.screenshotsDir = path.join(projectPath, ".mycontext", "test-screenshots");
56
+ fs.ensureDirSync(this.screenshotsDir);
57
+ // Initialize AI client (will use the project's configured provider)
58
+ this.aiClient = this.initializeAIClient();
59
+ this.contextService = new ContextService_1.ContextService(projectPath);
60
+ }
61
+ /**
62
+ * Initialize AI client from project config
63
+ */
64
+ initializeAIClient() {
65
+ // Import and initialize the provider chain
66
+ // This will check for available keys (MyContext, OpenAI, Claude, XAI)
67
+ try {
68
+ const { getProviderChain } = require("../clients/ProviderChain");
69
+ return getProviderChain();
70
+ }
71
+ catch (error) {
72
+ throw new Error("Failed to initialize AI client. Please configure an AI provider.");
73
+ }
74
+ }
75
+ /**
76
+ * Run a test mission
77
+ */
78
+ async runTest(mission, config = { headless: true }) {
79
+ const executionId = (0, uuid_1.v4)();
80
+ const startTime = Date.now();
81
+ const steps = [];
82
+ let status = "running";
83
+ let error = null;
84
+ console.log(chalk_1.default.blue(`\n🧪 Running test: ${mission.name}`));
85
+ console.log(chalk_1.default.gray(`Mission: ${mission.mission}`));
86
+ try {
87
+ // Launch browser
88
+ await this.launchBrowser(config);
89
+ // Create new page
90
+ const page = await this.context.newPage();
91
+ // Set viewport if specified
92
+ if (config.viewport) {
93
+ await page.setViewportSize(config.viewport);
94
+ }
95
+ // Start URL (use from config or mission)
96
+ const startUrl = config.baseUrl || mission.sourceFlow || "http://localhost:3000";
97
+ console.log(chalk_1.default.gray(`Starting at: ${startUrl}`));
98
+ await page.goto(startUrl);
99
+ // Take initial screenshot
100
+ const initialScreenshot = await this.takeScreenshot(page, executionId, "initial");
101
+ // Execute mission using AI
102
+ const executionSteps = await this.executeWithAI(page, mission, executionId);
103
+ steps.push(...executionSteps);
104
+ // Validate results
105
+ const validationResults = await this.validateMission(page, mission, executionId);
106
+ // Determine final status
107
+ const allValidationsPassed = validationResults.every((v) => v.passed);
108
+ status = allValidationsPassed ? "passed" : "failed";
109
+ // Take final screenshot
110
+ const finalScreenshot = await this.takeScreenshot(page, executionId, "final");
111
+ console.log(status === "passed"
112
+ ? chalk_1.default.green(`✅ Test passed!`)
113
+ : chalk_1.default.red(`❌ Test failed!`));
114
+ const endTime = Date.now();
115
+ // Milestone 4 & 5: State Attestation & Zero-Drift Synthesis
116
+ const successfulGravityChecks = steps.filter(s => s.success && !s.action.startsWith('REJECTED')).length;
117
+ const totalGravityAttempts = steps.length;
118
+ const narrativeCompliance = totalGravityAttempts > 0 ? successfulGravityChecks / totalGravityAttempts : 1.0;
119
+ const driftAlerts = [];
120
+ if (status === 'failed') {
121
+ driftAlerts.push({
122
+ type: 'objective',
123
+ severity: 'high',
124
+ message: 'Final state does not match expected outcome',
125
+ expected: mission.expectedOutcome,
126
+ actual: 'Validation failed on current page',
127
+ timestamp: new Date().toISOString()
128
+ });
129
+ }
130
+ // Check for rejected intents
131
+ steps.filter(s => s.action.startsWith('REJECTED')).forEach(s => {
132
+ driftAlerts.push({
133
+ type: 'logic',
134
+ severity: 'medium',
135
+ message: `Intent rejected by Hard Gravity Engine: ${s.intent}`,
136
+ expected: 'Aligned intent',
137
+ actual: s.intent,
138
+ timestamp: s.timestamp
139
+ });
140
+ });
141
+ const result = {
142
+ missionId: mission.id,
143
+ executionId,
144
+ status,
145
+ startedAt: new Date(startTime).toISOString(),
146
+ completedAt: new Date(endTime).toISOString(),
147
+ duration: endTime - startTime,
148
+ steps,
149
+ validationResults,
150
+ finalState: {
151
+ url: page.url(),
152
+ screenshot: finalScreenshot,
153
+ dom: await this.getSimplifiedDOM(page),
154
+ },
155
+ aiNotes: await this.generateAINotes(mission, steps, validationResults),
156
+ driftAnalysis: {
157
+ narrativeCompliance,
158
+ alerts: driftAlerts
159
+ }
160
+ };
161
+ await page.close();
162
+ return result;
163
+ }
164
+ catch (err) {
165
+ console.error(chalk_1.default.red(`❌ Test error: ${err.message}`));
166
+ error = {
167
+ message: err.message,
168
+ stack: err.stack,
169
+ };
170
+ const endTime = Date.now();
171
+ return {
172
+ missionId: mission.id,
173
+ executionId,
174
+ status: "error",
175
+ startedAt: new Date(startTime).toISOString(),
176
+ completedAt: new Date(endTime).toISOString(),
177
+ duration: endTime - startTime,
178
+ steps,
179
+ validationResults: [],
180
+ finalState: {
181
+ url: "",
182
+ screenshot: undefined,
183
+ },
184
+ error,
185
+ };
186
+ }
187
+ finally {
188
+ await this.closeBrowser();
189
+ }
190
+ }
191
+ /**
192
+ * Execute mission using AI
193
+ */
194
+ async executeWithAI(page, mission, executionId) {
195
+ const steps = [];
196
+ let stepOrder = 0;
197
+ // Load manifest for gravity (Milestone 2)
198
+ await this.contextService.initialize();
199
+ const manifest = this.contextService.getManifest();
200
+ const primeObjective = manifest?.phases.functional_summary.core_purpose || mission.mission;
201
+ // AI prompt to understand the mission
202
+ const systemPrompt = `You are an AI agent performing UI testing.
203
+
204
+ HARD GRAVITY (PRIME OBJECTIVE):
205
+ "${primeObjective}"
206
+
207
+ Your mission is:
208
+ "${mission.mission}"
209
+
210
+ Expected outcome: "${mission.expectedOutcome}"
211
+
212
+ You can navigate the UI by:
213
+ 1. Clicking elements (buttons, links, etc.)
214
+ 2. Filling forms
215
+ 3. Navigating to URLs
216
+ 4. Waiting for elements to appear
217
+
218
+ Analyze the current page and decide what action to take next to accomplish the mission.
219
+ Be specific about selectors (use text content, aria-labels, or role attributes when possible).
220
+
221
+ Current URL: ${page.url()}`;
222
+ // Get page content for AI
223
+ const pageContent = await this.getPageContentForAI(page);
224
+ console.log(chalk_1.default.yellow(`\n🤖 AI analyzing page...`));
225
+ // Main execution loop
226
+ let maxSteps = 20; // Prevent infinite loops
227
+ let missionComplete = false;
228
+ while (!missionComplete && maxSteps > 0) {
229
+ maxSteps--;
230
+ // Ask AI what to do next
231
+ const aiResponse = await this.askAI(systemPrompt, pageContent, steps, mission);
232
+ if (!aiResponse || aiResponse.action === "complete") {
233
+ missionComplete = true;
234
+ break;
235
+ }
236
+ // Milestone 2: Hard Gravity Check
237
+ const grounding = await this.contextService.validateIntent(aiResponse.intent || "");
238
+ if (!grounding.valid) {
239
+ console.log(chalk_1.default.red(`\n🛑 GRAVITY INTERVENTION: Intent deviates from Prime Objective!`));
240
+ console.log(chalk_1.default.yellow(`REASON: ${grounding.reason}`));
241
+ // Add a "failed" step to record the deviation
242
+ steps.push({
243
+ id: (0, uuid_1.v4)(),
244
+ order: stepOrder++,
245
+ action: `REJECTED: ${aiResponse.action}`,
246
+ intent: aiResponse.intent,
247
+ timestamp: new Date().toISOString(),
248
+ success: false,
249
+ error: `Gravity Intervention: ${grounding.reason}`
250
+ });
251
+ // Prompt AI to rethink with gravity awareness
252
+ const correctionPrompt = `${systemPrompt}\n\n⚠️ PREVIOUS INTENT REJECTED: "${aiResponse.intent}"\nREASON: ${grounding.reason}\n\nPlease rethink your action to stay anchored to the Prime Objective.`;
253
+ const retryResponse = await this.askAI(correctionPrompt, pageContent, steps, mission);
254
+ if (!retryResponse || retryResponse.intent === aiResponse.intent) {
255
+ console.log(chalk_1.default.red(`❌ AI failed to align with Gravity. Stopping.`));
256
+ break;
257
+ }
258
+ // Swap response for the corrected one
259
+ aiResponse.action = retryResponse.action;
260
+ aiResponse.selector = retryResponse.selector;
261
+ aiResponse.value = retryResponse.value;
262
+ aiResponse.intent = retryResponse.intent;
263
+ }
264
+ // Execute the AI's decision
265
+ const step = await this.executeAction(page, aiResponse, stepOrder++, executionId, mission);
266
+ steps.push(step);
267
+ console.log(chalk_1.default.cyan(` Step ${step.order + 1}: ${step.action}`));
268
+ if (!step.success) {
269
+ console.log(chalk_1.default.red(` Failed: ${step.error}`));
270
+ break;
271
+ }
272
+ // Wait a bit for page to settle
273
+ await page.waitForTimeout(1000);
274
+ // Update page content for next iteration
275
+ const newPageContent = await this.getPageContentForAI(page);
276
+ if (newPageContent === pageContent) {
277
+ // Page hasn't changed, might be stuck
278
+ console.log(chalk_1.default.yellow(` ⚠️ Page unchanged, stopping`));
279
+ break;
280
+ }
281
+ }
282
+ return steps;
283
+ }
284
+ /**
285
+ * Ask AI for next action
286
+ */
287
+ async askAI(systemPrompt, pageContent, previousSteps, mission) {
288
+ try {
289
+ // Prepare test data context
290
+ const testDataContext = mission.testData
291
+ ? `
292
+ Available Test Data:
293
+ - CV File: ${mission.testData.cvFileName || 'sample-cv.pdf'}
294
+ - Update Notes: ${mission.testData.updateNotes || 'N/A'}
295
+ - Job Description: ${mission.testData.jobDescription ? 'Available' : 'N/A'}
296
+ `
297
+ : '';
298
+ const prompt = `${systemPrompt}
299
+
300
+ Page Content:
301
+ ${pageContent}
302
+
303
+ Previous Steps:
304
+ ${previousSteps.map((s) => `${s.order + 1}. ${s.action} - ${s.success ? "success" : "failed"}`).join("\n")}
305
+ ${testDataContext}
306
+
307
+ What should be the next action? Respond in JSON format:
308
+ {
309
+ "action": "click|fill|goto|wait|upload|complete",
310
+ "selector": "element selector - use one of these formats:",
311
+ "value": "value for fill/upload action",
312
+ "intent": "why you're taking this action"
313
+ }
314
+
315
+ SELECTOR FORMATS (choose the most specific one that works):
316
+ 1. Text-based (preferred for buttons/links): "Button text" or "Link text"
317
+ 2. Playwright has-text: "button:has-text('Click me')" or "a:has-text('Learn more')"
318
+ 3. Standard CSS: "#id", ".class", "button[type='submit']"
319
+ 4. ARIA attributes: "[aria-label='Close']", "[role='button']"
320
+
321
+ EXAMPLES:
322
+ - Click a button: { "action": "click", "selector": "Refresh My CV", "intent": "switching to refresh mode" }
323
+ - Fill input: { "action": "fill", "selector": "input[type='email']", "value": "test@email.com", "intent": "entering email" }
324
+ - Upload file: { "action": "upload", "value": "sample-cv.pdf", "intent": "uploading CV file" }
325
+ - Navigate: { "action": "goto", "value": "/dashboard", "intent": "going to dashboard" }
326
+ - Wait: { "action": "wait", "intent": "waiting for page to load" }
327
+
328
+ IMPORTANT FOR FILE UPLOADS:
329
+ - Use "upload" action with the filename from Available Test Data
330
+ - Don't try to click file inputs - they're often hidden
331
+ - The system will automatically find the file input and upload the file
332
+
333
+ If the mission is complete, respond with { "action": "complete" }`;
334
+ const response = await this.aiClient.generateText(prompt, {
335
+ maxTokens: 500,
336
+ temperature: 0.2, // Lower temperature for more deterministic actions
337
+ });
338
+ // Parse JSON from response
339
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
340
+ if (jsonMatch) {
341
+ return JSON.parse(jsonMatch[0]);
342
+ }
343
+ return null;
344
+ }
345
+ catch (error) {
346
+ console.error("AI decision error:", error);
347
+ return null;
348
+ }
349
+ }
350
+ /**
351
+ * Infer what value to use for a field based on test data
352
+ */
353
+ inferFieldValue(fieldSelector, aiProvidedValue, mission) {
354
+ // If AI provided a value and it's not a placeholder, use it
355
+ if (aiProvidedValue && !aiProvidedValue.startsWith('[USE ')) {
356
+ return aiProvidedValue;
357
+ }
358
+ // Try to match field to test data
359
+ if (mission.testData) {
360
+ const testData = mission.testData;
361
+ const lowerSelector = fieldSelector.toLowerCase();
362
+ // Match based on field purpose
363
+ if ((lowerSelector.includes('note') || lowerSelector.includes('update')) && testData.updateNotes) {
364
+ return testData.updateNotes;
365
+ }
366
+ if ((lowerSelector.includes('job') || lowerSelector.includes('description')) && testData.jobDescription) {
367
+ return testData.jobDescription;
368
+ }
369
+ if (lowerSelector.includes('email') && testData.email) {
370
+ return testData.email;
371
+ }
372
+ if (lowerSelector.includes('password') && testData.password) {
373
+ return testData.password;
374
+ }
375
+ if (lowerSelector.includes('name') && testData.name) {
376
+ return testData.name;
377
+ }
378
+ }
379
+ // Fallback: use AI-provided value or empty string
380
+ return aiProvidedValue || '';
381
+ }
382
+ /**
383
+ * Execute an action on the page
384
+ */
385
+ async executeAction(page, aiDecision, order, executionId, mission) {
386
+ const step = {
387
+ id: (0, uuid_1.v4)(),
388
+ order,
389
+ action: `${aiDecision.action}: ${aiDecision.selector || aiDecision.value || ""}`,
390
+ intent: aiDecision.intent || "",
391
+ timestamp: new Date().toISOString(),
392
+ success: false,
393
+ };
394
+ try {
395
+ switch (aiDecision.action) {
396
+ case "click":
397
+ // Try multiple strategies to find the element
398
+ const clickSelector = await this.findElement(page, aiDecision.selector);
399
+ if (clickSelector) {
400
+ await page.click(clickSelector);
401
+ step.metadata = { clickedElement: clickSelector };
402
+ step.success = true;
403
+ }
404
+ else {
405
+ step.error = `Element not found: ${aiDecision.selector}`;
406
+ }
407
+ break;
408
+ case "fill":
409
+ const fillSelector = await this.findElement(page, aiDecision.selector);
410
+ if (fillSelector) {
411
+ // Infer the correct value to use (from test data or AI)
412
+ const valueToUse = this.inferFieldValue(aiDecision.selector, aiDecision.value, mission);
413
+ await page.fill(fillSelector, valueToUse);
414
+ step.metadata = {
415
+ elementSelector: fillSelector,
416
+ inputValue: valueToUse,
417
+ };
418
+ step.success = true;
419
+ }
420
+ else {
421
+ step.error = `Element not found: ${aiDecision.selector}`;
422
+ }
423
+ break;
424
+ case "goto":
425
+ await page.goto(aiDecision.value);
426
+ step.metadata = { navigationTarget: aiDecision.value };
427
+ step.success = true;
428
+ break;
429
+ case "wait":
430
+ await page.waitForTimeout(2000);
431
+ step.success = true;
432
+ break;
433
+ case "upload":
434
+ // Handle file upload (works with hidden file inputs)
435
+ try {
436
+ const fileInput = await page.$('input[type="file"]');
437
+ if (fileInput && aiDecision.value) {
438
+ // Construct file path from test-fixtures directory
439
+ const filePath = path.join(this.projectPath, 'test-fixtures', aiDecision.value);
440
+ // Check if file exists
441
+ if (!fs.existsSync(filePath)) {
442
+ step.error = `File not found: ${filePath}`;
443
+ break;
444
+ }
445
+ await fileInput.setInputFiles(filePath);
446
+ step.metadata = {
447
+ uploadedFile: aiDecision.value,
448
+ filePath: filePath
449
+ };
450
+ step.success = true;
451
+ }
452
+ else {
453
+ step.error = `File input not found or no file specified`;
454
+ }
455
+ }
456
+ catch (error) {
457
+ step.error = `Upload failed: ${error.message}`;
458
+ }
459
+ break;
460
+ default:
461
+ step.error = `Unknown action: ${aiDecision.action}`;
462
+ }
463
+ // Take screenshot after action
464
+ step.screenshot = await this.takeScreenshot(page, executionId, `step-${order}`);
465
+ step.metadata = {
466
+ ...step.metadata,
467
+ url: page.url(),
468
+ };
469
+ }
470
+ catch (error) {
471
+ step.error = error.message;
472
+ step.success = false;
473
+ }
474
+ return step;
475
+ }
476
+ /**
477
+ * Convert jQuery-style or ambiguous selectors to Playwright-compatible selectors
478
+ */
479
+ convertToPlaywrightSelector(selector) {
480
+ const candidates = [];
481
+ // Pattern 1: jQuery :contains() -> Playwright :has-text() or text=
482
+ // e.g., "button:contains('Refresh My CV')" -> "button:has-text('Refresh My CV')"
483
+ const containsMatch = selector.match(/^([a-z]+):contains\(['"](.+)['"]\)$/i);
484
+ if (containsMatch) {
485
+ const [, element, text] = containsMatch;
486
+ candidates.push(`${element}:has-text("${text}")`);
487
+ candidates.push(`${element} >> text="${text}"`);
488
+ candidates.push(`${element}:text("${text}")`);
489
+ return candidates;
490
+ }
491
+ // Pattern 2: Plain text -> multiple strategies
492
+ // If it looks like plain text (no CSS selector chars)
493
+ if (!/[#.\[\]:>~+]/.test(selector)) {
494
+ candidates.push(`text="${selector}"`); // Exact text
495
+ candidates.push(`text=/${selector}/i`); // Case-insensitive
496
+ candidates.push(`button:has-text("${selector}")`); // Button with text
497
+ candidates.push(`a:has-text("${selector}")`); // Link with text
498
+ candidates.push(`[aria-label="${selector}"]`); // ARIA label
499
+ return candidates;
500
+ }
501
+ // Pattern 3: Already looks like a valid CSS selector
502
+ candidates.push(selector);
503
+ return candidates;
504
+ }
505
+ /**
506
+ * Try to find form inputs using intelligent strategies
507
+ */
508
+ async tryFormInputStrategies(page, selector) {
509
+ // Extract potential field keywords from selector
510
+ const lowerSelector = selector.toLowerCase();
511
+ const keywords = lowerSelector.match(/\b(update|notes|email|password|name|description|job|message|comment|text)\b/);
512
+ if (!keywords) {
513
+ // If selector looks like it might be for a textarea or input, try generic strategies
514
+ if (lowerSelector.includes('textarea') || lowerSelector.includes('input')) {
515
+ // Try to find any visible textarea or input
516
+ const visibleTextarea = await page.$('textarea:visible');
517
+ if (visibleTextarea)
518
+ return 'textarea:visible';
519
+ const visibleInput = await page.$('input[type="text"]:visible');
520
+ if (visibleInput)
521
+ return 'input[type="text"]:visible';
522
+ }
523
+ return null;
524
+ }
525
+ const keyword = keywords[0];
526
+ // Strategy 1: Try by placeholder (case-insensitive)
527
+ try {
528
+ const byPlaceholder = `[placeholder*="${keyword}" i]`;
529
+ if (await page.$(byPlaceholder))
530
+ return byPlaceholder;
531
+ }
532
+ catch { }
533
+ // Strategy 2: Try by name attribute
534
+ try {
535
+ const byName = `[name*="${keyword}" i]`;
536
+ if (await page.$(byName))
537
+ return byName;
538
+ }
539
+ catch { }
540
+ // Strategy 3: Try by id attribute
541
+ try {
542
+ const byId = `[id*="${keyword}" i]`;
543
+ if (await page.$(byId))
544
+ return byId;
545
+ }
546
+ catch { }
547
+ // Strategy 4: Find textarea with nearby label containing keyword
548
+ try {
549
+ const textareas = await page.$$('textarea');
550
+ for (const textarea of textareas) {
551
+ if (await textarea.isVisible()) {
552
+ // Check if there's a label near this textarea
553
+ const textareaId = await textarea.getAttribute('id');
554
+ if (textareaId) {
555
+ const label = await page.$(`label[for="${textareaId}"]`);
556
+ if (label) {
557
+ const labelText = await label.textContent();
558
+ if (labelText && labelText.toLowerCase().includes(keyword)) {
559
+ return `#${textareaId}`;
560
+ }
561
+ }
562
+ }
563
+ // If no specific match, return first visible textarea for notes/updates
564
+ if (['note', 'update', 'message', 'comment'].includes(keyword)) {
565
+ return 'textarea:visible';
566
+ }
567
+ }
568
+ }
569
+ }
570
+ catch { }
571
+ // Strategy 5: Try input by type for specific keywords
572
+ try {
573
+ if (keyword === 'email') {
574
+ if (await page.$('input[type="email"]'))
575
+ return 'input[type="email"]';
576
+ }
577
+ if (keyword === 'password') {
578
+ if (await page.$('input[type="password"]'))
579
+ return 'input[type="password"]';
580
+ }
581
+ if (['name', 'text', 'description'].includes(keyword)) {
582
+ if (await page.$('input[type="text"]:visible'))
583
+ return 'input[type="text"]:visible';
584
+ }
585
+ }
586
+ catch { }
587
+ return null;
588
+ }
589
+ /**
590
+ * Find element using multiple strategies
591
+ */
592
+ async findElement(page, selector) {
593
+ // Convert selector to candidate selectors
594
+ const candidates = this.convertToPlaywrightSelector(selector);
595
+ // Try each candidate
596
+ for (const candidate of candidates) {
597
+ try {
598
+ const element = await page.$(candidate);
599
+ if (element) {
600
+ return candidate;
601
+ }
602
+ }
603
+ catch (error) {
604
+ // Selector syntax error, try next candidate
605
+ continue;
606
+ }
607
+ }
608
+ // Additional fallback: Try with partial text matching
609
+ try {
610
+ const partialText = `text=/${selector.replace(/['"]/g, '')}/i`;
611
+ if (await page.$(partialText)) {
612
+ return partialText;
613
+ }
614
+ }
615
+ catch {
616
+ // Ignore errors
617
+ }
618
+ // NEW: Try form input strategies if still no match
619
+ const formStrategy = await this.tryFormInputStrategies(page, selector);
620
+ if (formStrategy) {
621
+ return formStrategy;
622
+ }
623
+ return null;
624
+ }
625
+ /**
626
+ * Validate mission completion
627
+ */
628
+ async validateMission(page, mission, executionId) {
629
+ const results = [];
630
+ if (!mission.validationRules || mission.validationRules.length === 0) {
631
+ // No explicit validation rules, assume success if we got here
632
+ return [];
633
+ }
634
+ for (const rule of mission.validationRules) {
635
+ const result = {
636
+ rule,
637
+ passed: false,
638
+ message: "",
639
+ timestamp: new Date().toISOString(),
640
+ };
641
+ try {
642
+ switch (rule.type) {
643
+ case "url-match":
644
+ const currentUrl = page.url();
645
+ result.passed = currentUrl.includes(rule.expectedValue || "");
646
+ result.actualValue = currentUrl;
647
+ result.message = result.passed
648
+ ? `URL matches: ${currentUrl}`
649
+ : `URL doesn't match. Expected: ${rule.expectedValue}, Got: ${currentUrl}`;
650
+ break;
651
+ case "element-exists":
652
+ const element = await page.$(rule.selector || "");
653
+ result.passed = element !== null;
654
+ result.message = result.passed
655
+ ? `Element exists: ${rule.selector}`
656
+ : `Element not found: ${rule.selector}`;
657
+ break;
658
+ case "text-contains":
659
+ const content = await page.textContent("body");
660
+ result.passed = content?.includes(rule.expectedValue || "") || false;
661
+ result.message = result.passed
662
+ ? `Text found: ${rule.expectedValue}`
663
+ : `Text not found: ${rule.expectedValue}`;
664
+ break;
665
+ case "element-visible":
666
+ const visibleElement = await page.$(rule.selector || "");
667
+ result.passed =
668
+ visibleElement !== null &&
669
+ (await visibleElement.isVisible());
670
+ result.message = result.passed
671
+ ? `Element visible: ${rule.selector}`
672
+ : `Element not visible: ${rule.selector}`;
673
+ break;
674
+ default:
675
+ result.message = `Unknown validation type: ${rule.type}`;
676
+ }
677
+ }
678
+ catch (error) {
679
+ result.message = `Validation error: ${error.message}`;
680
+ }
681
+ results.push(result);
682
+ }
683
+ return results;
684
+ }
685
+ /**
686
+ * Get simplified DOM for AI analysis
687
+ */
688
+ async getSimplifiedDOM(page) {
689
+ try {
690
+ return await page.evaluate(() => {
691
+ const body = document.body;
692
+ // Get interactive elements
693
+ const buttons = Array.from(document.querySelectorAll("button")).map((b) => `Button: ${b.textContent?.trim()}`);
694
+ const links = Array.from(document.querySelectorAll("a")).map((a) => `Link: ${a.textContent?.trim()}`);
695
+ const inputs = Array.from(document.querySelectorAll("input")).map((i) => `Input: ${i.type} (${i.placeholder || i.name})`);
696
+ return [
697
+ `Interactive elements:`,
698
+ ...buttons.slice(0, 10),
699
+ ...links.slice(0, 10),
700
+ ...inputs.slice(0, 10),
701
+ ].join("\n");
702
+ });
703
+ }
704
+ catch (error) {
705
+ return "";
706
+ }
707
+ }
708
+ /**
709
+ * Get page content formatted for AI
710
+ */
711
+ async getPageContentForAI(page) {
712
+ const title = await page.title();
713
+ const url = page.url();
714
+ const dom = await this.getSimplifiedDOM(page);
715
+ return `Title: ${title}
716
+ URL: ${url}
717
+ ${dom}`;
718
+ }
719
+ /**
720
+ * Generate AI notes about the test execution
721
+ */
722
+ async generateAINotes(mission, steps, validations) {
723
+ const successfulSteps = steps.filter((s) => s.success).length;
724
+ const failedSteps = steps.filter((s) => !s.success).length;
725
+ const passedValidations = validations.filter((v) => v.passed).length;
726
+ return `Executed ${steps.length} steps (${successfulSteps} successful, ${failedSteps} failed). Validated ${passedValidations}/${validations.length} criteria.`;
727
+ }
728
+ /**
729
+ * Take screenshot
730
+ */
731
+ async takeScreenshot(page, executionId, name) {
732
+ try {
733
+ const filename = `${executionId}-${name}.png`;
734
+ const filepath = path.join(this.screenshotsDir, filename);
735
+ await page.screenshot({ path: filepath, fullPage: true });
736
+ return filepath;
737
+ }
738
+ catch (error) {
739
+ return "";
740
+ }
741
+ }
742
+ /**
743
+ * Launch browser
744
+ */
745
+ async launchBrowser(config) {
746
+ this.browser = await playwright_1.chromium.launch({
747
+ headless: config.headless !== false,
748
+ slowMo: config.slowMo || 0,
749
+ });
750
+ this.context = await this.browser.newContext({
751
+ viewport: config.viewport || { width: 1280, height: 720 },
752
+ recordVideo: config.recordVideo
753
+ ? { dir: path.join(this.screenshotsDir, "videos") }
754
+ : undefined,
755
+ });
756
+ }
757
+ /**
758
+ * Close browser
759
+ */
760
+ async closeBrowser() {
761
+ if (this.context) {
762
+ await this.context.close();
763
+ }
764
+ if (this.browser) {
765
+ await this.browser.close();
766
+ }
767
+ }
768
+ /**
769
+ * Start interactive recording mode
770
+ */
771
+ async startRecording(name, startUrl) {
772
+ // TODO: Implement interactive recording
773
+ // This would open a browser and observe user actions
774
+ throw new Error("Interactive recording not yet implemented");
775
+ }
776
+ /**
777
+ * Start watch mode
778
+ */
779
+ async startWatchMode(missions, watchPaths) {
780
+ // TODO: Implement watch mode
781
+ // This would watch files and re-run tests on changes
782
+ throw new Error("Watch mode not yet implemented");
783
+ }
784
+ }
785
+ exports.BrowserTestRunner = BrowserTestRunner;
786
+ //# sourceMappingURL=browser-test-runner.js.map