chrometools-mcp 3.2.6 ā 3.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +212 -0
- package/README.md +14 -5
- package/angular-tools.js +9 -3
- package/bridge/bridge-client.js +62 -7
- package/bridge/bridge-service.js +80 -2
- package/browser/page-manager.js +31 -0
- package/extension/background.js +117 -0
- package/extension/content.js +3 -1
- package/extension/manifest.json +2 -1
- package/index.js +213 -45
- package/models/TextInputModel.js +56 -5
- package/models/index.js +20 -6
- package/package.json +1 -1
- package/pom/apom-tree-converter.js +19 -7
- package/server/tool-schemas.js +3 -0
- package/utils/hints-generator.js +46 -4
- package/utils/post-click-diagnostics.js +376 -0
- /package/{publish_output.txt ā nul} +0 -0
|
@@ -711,12 +711,17 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
711
711
|
// Try to find stable class name (excluding framework-specific dynamic classes)
|
|
712
712
|
const stableClass = getStableClassName(element);
|
|
713
713
|
if (stableClass) {
|
|
714
|
-
const
|
|
714
|
+
const escapedClass = CSS.escape(stableClass);
|
|
715
|
+
const classSelector = `.${escapedClass}`;
|
|
715
716
|
// Verify it's unique within parent context
|
|
716
717
|
if (element.parentElement) {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
718
|
+
try {
|
|
719
|
+
const matches = element.parentElement.querySelectorAll(classSelector);
|
|
720
|
+
if (matches.length === 1 && matches[0] === element) {
|
|
721
|
+
return classSelector;
|
|
722
|
+
}
|
|
723
|
+
} catch (e) {
|
|
724
|
+
// Invalid selector, continue to path-based approach
|
|
720
725
|
}
|
|
721
726
|
}
|
|
722
727
|
}
|
|
@@ -728,10 +733,10 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
728
733
|
while (current && current !== document.body) {
|
|
729
734
|
let selector = current.tagName.toLowerCase();
|
|
730
735
|
|
|
731
|
-
// Add stable class if available
|
|
736
|
+
// Add stable class if available (escaped for CSS selector safety)
|
|
732
737
|
const stableClass = getStableClassName(current);
|
|
733
738
|
if (stableClass) {
|
|
734
|
-
selector += `.${stableClass}`;
|
|
739
|
+
selector += `.${CSS.escape(stableClass)}`;
|
|
735
740
|
}
|
|
736
741
|
|
|
737
742
|
// Add nth-of-type if needed
|
|
@@ -754,6 +759,7 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
754
759
|
|
|
755
760
|
/**
|
|
756
761
|
* Get stable class name excluding framework-specific dynamic classes
|
|
762
|
+
* and Tailwind CSS utility classes with special characters
|
|
757
763
|
* Returns first stable class or null
|
|
758
764
|
*/
|
|
759
765
|
function getStableClassName(element) {
|
|
@@ -763,8 +769,14 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
763
769
|
|
|
764
770
|
const classes = element.className.split(/\s+/).filter(c => c);
|
|
765
771
|
|
|
766
|
-
// Filter out framework-specific classes
|
|
772
|
+
// Filter out framework-specific classes and Tailwind utilities
|
|
767
773
|
const stableClasses = classes.filter(className => {
|
|
774
|
+
// Tailwind CSS: classes with special characters that break CSS selectors
|
|
775
|
+
// Colons for variants (hover:, focus:, md:, etc.)
|
|
776
|
+
// Slashes for fractions (w-1/2)
|
|
777
|
+
// Brackets for arbitrary values (bg-[#1da1f2])
|
|
778
|
+
if (/[:\/\[\]]/.test(className)) return false;
|
|
779
|
+
|
|
768
780
|
// React: CSS Modules, Styled Components, Emotion
|
|
769
781
|
if (/^[a-zA-Z0-9_-]+-[a-zA-Z0-9_-]{5,}$/.test(className)) return false;
|
|
770
782
|
if (/^css-[a-z0-9]+(-[a-z0-9]+)?$/i.test(className)) return false;
|
package/server/tool-schemas.js
CHANGED
|
@@ -21,6 +21,8 @@ export const ClickSchema = z.object({
|
|
|
21
21
|
waitAfter: z.number().optional().describe("Milliseconds to wait after click (default: 1500)"),
|
|
22
22
|
screenshot: z.boolean().optional().describe("Capture screenshot after click (default: false for performance)"),
|
|
23
23
|
timeout: z.number().optional().describe("Maximum time to wait for operation in ms (default: 30000)"),
|
|
24
|
+
skipNetworkWait: z.boolean().optional().describe("Skip waiting for network requests (default: false). Use for forms with long-polling/WebSockets to avoid timeouts."),
|
|
25
|
+
networkWaitTimeout: z.number().optional().describe("Maximum time to wait for network requests in ms (default: 3000). Only used if skipNetworkWait is false."),
|
|
24
26
|
}).refine(data => (data.id && !data.selector) || (!data.id && data.selector), {
|
|
25
27
|
message: "Either 'id' or 'selector' must be provided, but not both"
|
|
26
28
|
});
|
|
@@ -31,6 +33,7 @@ export const TypeSchema = z.object({
|
|
|
31
33
|
text: z.string().describe("Text to type"),
|
|
32
34
|
delay: z.number().optional().describe("Delay between keystrokes in ms (default: 30)"),
|
|
33
35
|
clearFirst: z.boolean().optional().describe("Clear field before typing (default: true)"),
|
|
36
|
+
timeout: z.number().optional().describe("Maximum time to wait for operation in ms (default: 30000)"),
|
|
34
37
|
}).refine(data => (data.id && !data.selector) || (!data.id && data.selector), {
|
|
35
38
|
message: "Either 'id' or 'selector' must be provided, but not both"
|
|
36
39
|
});
|
package/utils/hints-generator.js
CHANGED
|
@@ -8,6 +8,20 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export function generateNavigationHints(page, url) {
|
|
10
10
|
return page.evaluate(() => {
|
|
11
|
+
// Helper to get safe class selector (filters Tailwind special chars)
|
|
12
|
+
function getSafeClassSelector(element) {
|
|
13
|
+
if (!element.className || typeof element.className !== 'string') return null;
|
|
14
|
+
const classes = element.className.split(' ')
|
|
15
|
+
.filter(c => c && !/[:\/\[\]]/.test(c))
|
|
16
|
+
.slice(0, 1);
|
|
17
|
+
if (classes.length === 0) return null;
|
|
18
|
+
try {
|
|
19
|
+
return `.${CSS.escape(classes[0])}`;
|
|
20
|
+
} catch (e) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
const hints = {
|
|
12
26
|
pageType: 'unknown',
|
|
13
27
|
availableActions: [],
|
|
@@ -64,7 +78,7 @@ export function generateNavigationHints(page, url) {
|
|
|
64
78
|
hints.keyElements.push({
|
|
65
79
|
type: 'primary-button',
|
|
66
80
|
text: mainButton.textContent.trim(),
|
|
67
|
-
selector: mainButton.id ? `#${mainButton.id}` :
|
|
81
|
+
selector: mainButton.id ? `#${CSS.escape(mainButton.id)}` : (getSafeClassSelector(mainButton) || 'button'),
|
|
68
82
|
});
|
|
69
83
|
}
|
|
70
84
|
|
|
@@ -74,7 +88,7 @@ export function generateNavigationHints(page, url) {
|
|
|
74
88
|
hints.keyElements.push({
|
|
75
89
|
type: 'notification',
|
|
76
90
|
text: alert.textContent.trim().substring(0, 100),
|
|
77
|
-
selector: alert
|
|
91
|
+
selector: getSafeClassSelector(alert) || '[role="alert"]',
|
|
78
92
|
});
|
|
79
93
|
}
|
|
80
94
|
});
|
|
@@ -91,6 +105,20 @@ export async function generateClickHints(page, selector) {
|
|
|
91
105
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
92
106
|
|
|
93
107
|
return page.evaluate((clickedSelector) => {
|
|
108
|
+
// Helper to get safe class selector (filters Tailwind special chars)
|
|
109
|
+
function getSafeClassSelector(element) {
|
|
110
|
+
if (!element.className || typeof element.className !== 'string') return null;
|
|
111
|
+
const classes = element.className.split(' ')
|
|
112
|
+
.filter(c => c && !/[:\/\[\]]/.test(c))
|
|
113
|
+
.slice(0, 1);
|
|
114
|
+
if (classes.length === 0) return null;
|
|
115
|
+
try {
|
|
116
|
+
return `.${CSS.escape(classes[0])}`;
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
94
122
|
const hints = {
|
|
95
123
|
pageChanged: false,
|
|
96
124
|
newElements: [],
|
|
@@ -105,7 +133,7 @@ export async function generateClickHints(page, selector) {
|
|
|
105
133
|
hints.modalOpened = true;
|
|
106
134
|
hints.newElements.push({
|
|
107
135
|
type: 'modal',
|
|
108
|
-
selector: modal
|
|
136
|
+
selector: getSafeClassSelector(modal) || '[role="dialog"]',
|
|
109
137
|
});
|
|
110
138
|
hints.suggestedNext.push('Interact with modal or close it');
|
|
111
139
|
}
|
|
@@ -145,6 +173,20 @@ export async function generateFormSubmitHints(page) {
|
|
|
145
173
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
146
174
|
|
|
147
175
|
return page.evaluate(() => {
|
|
176
|
+
// Helper to get safe class selector (filters Tailwind special chars)
|
|
177
|
+
function getSafeClassSelector(element) {
|
|
178
|
+
if (!element.className || typeof element.className !== 'string') return null;
|
|
179
|
+
const classes = element.className.split(' ')
|
|
180
|
+
.filter(c => c && !/[:\/\[\]]/.test(c))
|
|
181
|
+
.slice(0, 1);
|
|
182
|
+
if (classes.length === 0) return null;
|
|
183
|
+
try {
|
|
184
|
+
return `.${CSS.escape(classes[0])}`;
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
148
190
|
const hints = {
|
|
149
191
|
success: false,
|
|
150
192
|
errors: [],
|
|
@@ -173,7 +215,7 @@ export async function generateFormSubmitHints(page) {
|
|
|
173
215
|
if (el.offsetWidth > 0) {
|
|
174
216
|
hints.errors.push({
|
|
175
217
|
text: el.textContent.trim().substring(0, 100),
|
|
176
|
-
selector: el
|
|
218
|
+
selector: getSafeClassSelector(el) || '[aria-invalid="true"]',
|
|
177
219
|
});
|
|
178
220
|
}
|
|
179
221
|
});
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-Action Diagnostics
|
|
3
|
+
* Collects errors and waits for network requests after user actions (click, navigation, etc.)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { consoleLogs, networkRequests } from '../browser/page-manager.js';
|
|
7
|
+
import { getNetworkRequestsFromBridge, isBridgeConnected } from '../bridge/bridge-client.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Wait for network requests to complete
|
|
11
|
+
* Tracks all requests (GET, POST, PUT, PATCH, DELETE) that started within detection window
|
|
12
|
+
* @param {number} beforeActionTimestamp - Timestamp before action to track new requests
|
|
13
|
+
* @param {number} detectionWindowMs - Time window to detect requests (default: 200ms)
|
|
14
|
+
* @param {number} maxWaitMs - Maximum time to wait for requests (default: 10000ms)
|
|
15
|
+
* @returns {Promise<{pendingFound: boolean, waitedMs: number, completedRequests: number, totalRequests: number}>}
|
|
16
|
+
*/
|
|
17
|
+
export async function waitForPendingRequests(beforeActionTimestamp, detectionWindowMs = 200, maxWaitMs = 10000) {
|
|
18
|
+
const startTime = Date.now();
|
|
19
|
+
|
|
20
|
+
// Step 1: Wait for detection window to let requests start
|
|
21
|
+
await new Promise(resolve => setTimeout(resolve, detectionWindowMs));
|
|
22
|
+
|
|
23
|
+
// Step 2: Find all requests (GET, POST, PUT, PATCH, DELETE) that started within detection window
|
|
24
|
+
const cutoffStart = new Date(beforeActionTimestamp).toISOString();
|
|
25
|
+
const cutoffEnd = new Date(beforeActionTimestamp + detectionWindowMs).toISOString();
|
|
26
|
+
|
|
27
|
+
const trackedRequests = networkRequests.filter(req => {
|
|
28
|
+
// Track all HTTP methods
|
|
29
|
+
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
// Only requests in detection window [T0, T0+200ms]
|
|
33
|
+
return req.timestamp >= cutoffStart && req.timestamp <= cutoffEnd;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// If no requests found, return immediately
|
|
37
|
+
if (trackedRequests.length === 0) {
|
|
38
|
+
return {
|
|
39
|
+
pendingFound: false,
|
|
40
|
+
waitedMs: Date.now() - startTime,
|
|
41
|
+
completedRequests: 0,
|
|
42
|
+
stillPending: 0,
|
|
43
|
+
pendingRequests: [],
|
|
44
|
+
totalRequests: 0,
|
|
45
|
+
trackedRequests: []
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Step 3: Wait for these specific requests to complete
|
|
50
|
+
const trackedRequestIds = new Set(trackedRequests.map(req => req.requestId));
|
|
51
|
+
|
|
52
|
+
const checkPending = () => {
|
|
53
|
+
return networkRequests.filter(req =>
|
|
54
|
+
trackedRequestIds.has(req.requestId) && req.status === 'pending'
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
let pending = checkPending();
|
|
59
|
+
const initialPendingCount = pending.length;
|
|
60
|
+
|
|
61
|
+
// Wait for requests to complete (with configurable timeout)
|
|
62
|
+
while (pending.length > 0 && (Date.now() - startTime) < maxWaitMs) {
|
|
63
|
+
await new Promise(resolve => setTimeout(resolve, 100)); // Check every 100ms
|
|
64
|
+
pending = checkPending();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Collect final results for tracked requests
|
|
68
|
+
const finalRequests = networkRequests.filter(req => trackedRequestIds.has(req.requestId));
|
|
69
|
+
const completedRequests = finalRequests.filter(req => req.status === 'completed' || (typeof req.status === 'number'));
|
|
70
|
+
const stillPendingRequests = pending.map(req => ({
|
|
71
|
+
url: req.url,
|
|
72
|
+
method: req.method,
|
|
73
|
+
timestamp: req.timestamp,
|
|
74
|
+
status: 'pending' // Still waiting after timeout
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
pendingFound: initialPendingCount > 0,
|
|
79
|
+
waitedMs: Date.now() - startTime,
|
|
80
|
+
completedRequests: completedRequests.length,
|
|
81
|
+
stillPending: pending.length,
|
|
82
|
+
pendingRequests: stillPendingRequests,
|
|
83
|
+
totalRequests: finalRequests.length,
|
|
84
|
+
trackedRequests: finalRequests.map(req => ({
|
|
85
|
+
method: req.method,
|
|
86
|
+
url: req.url,
|
|
87
|
+
status: req.status,
|
|
88
|
+
statusText: req.statusText
|
|
89
|
+
}))
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Collect errors from console logs and network requests
|
|
95
|
+
* @param {number} sinceTimestamp - Only collect errors after this timestamp (default: collect recent errors)
|
|
96
|
+
* @param {number} maxConsoleErrors - Maximum console errors to return (default: 15)
|
|
97
|
+
* @param {number} maxNetworkErrors - Maximum network errors to return (default: 15)
|
|
98
|
+
* @returns {Object} Object with consoleErrors and networkErrors arrays
|
|
99
|
+
*/
|
|
100
|
+
export function collectErrors(sinceTimestamp = null, maxConsoleErrors = 15, maxNetworkErrors = 15) {
|
|
101
|
+
const errors = {
|
|
102
|
+
consoleErrors: [],
|
|
103
|
+
networkErrors: [],
|
|
104
|
+
jsExceptions: [],
|
|
105
|
+
consoleErrorsOmitted: 0,
|
|
106
|
+
networkErrorsOmitted: 0
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// If no timestamp provided, look back 10 seconds
|
|
110
|
+
const cutoffTime = sinceTimestamp || (Date.now() - 10000);
|
|
111
|
+
const cutoffDate = new Date(cutoffTime).toISOString();
|
|
112
|
+
|
|
113
|
+
// Collect console errors (with limit)
|
|
114
|
+
let consoleErrorCount = 0;
|
|
115
|
+
consoleLogs.forEach(log => {
|
|
116
|
+
if (log.type === 'error') {
|
|
117
|
+
// Check if error is recent
|
|
118
|
+
const logTime = new Date(log.timestamp || 0).toISOString();
|
|
119
|
+
if (!sinceTimestamp || logTime >= cutoffDate) {
|
|
120
|
+
if (consoleErrorCount < maxConsoleErrors) {
|
|
121
|
+
errors.consoleErrors.push({
|
|
122
|
+
message: log.text,
|
|
123
|
+
timestamp: log.timestamp,
|
|
124
|
+
location: log.location || 'unknown'
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
errors.consoleErrorsOmitted++;
|
|
128
|
+
}
|
|
129
|
+
consoleErrorCount++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Collect network errors (failed requests, with limit)
|
|
135
|
+
let networkErrorCount = 0;
|
|
136
|
+
networkRequests.forEach(req => {
|
|
137
|
+
if (req.status === 'failed' || (typeof req.status === 'number' && req.status >= 400)) {
|
|
138
|
+
// Check if error is recent
|
|
139
|
+
const reqTime = req.timestamp;
|
|
140
|
+
if (!sinceTimestamp || reqTime >= cutoffDate) {
|
|
141
|
+
if (networkErrorCount < maxNetworkErrors) {
|
|
142
|
+
errors.networkErrors.push({
|
|
143
|
+
url: req.url,
|
|
144
|
+
method: req.method,
|
|
145
|
+
status: req.status,
|
|
146
|
+
statusText: req.statusText,
|
|
147
|
+
errorText: req.errorText,
|
|
148
|
+
timestamp: req.timestamp
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
errors.networkErrorsOmitted++;
|
|
152
|
+
}
|
|
153
|
+
networkErrorCount++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return errors;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Full post-action diagnostics: wait for requests and collect errors
|
|
163
|
+
* @param {Page} page - Puppeteer page instance
|
|
164
|
+
* @param {number} beforeActionTimestamp - Timestamp before action (to filter errors)
|
|
165
|
+
* @param {Object} options - Options for diagnostics
|
|
166
|
+
* @param {boolean} options.skipNetworkWait - Skip waiting for network requests (default: false)
|
|
167
|
+
* @param {number} options.networkWaitTimeout - Custom timeout for network wait in ms (default: 10000)
|
|
168
|
+
* @param {string} options.urlBeforeAction - URL before action (to detect navigation/form submit)
|
|
169
|
+
* @returns {Promise<Object>} Diagnostics result with errors and network info
|
|
170
|
+
*/
|
|
171
|
+
export async function runPostClickDiagnostics(page, beforeActionTimestamp, options = {}) {
|
|
172
|
+
const { skipNetworkWait = false, networkWaitTimeout = 10000, urlBeforeAction = null } = options;
|
|
173
|
+
|
|
174
|
+
// Wait for network requests (all methods within 200ms detection window)
|
|
175
|
+
// Default maxWait = 10s for click, configurable via networkWaitTimeout parameter
|
|
176
|
+
const networkInfo = skipNetworkWait
|
|
177
|
+
? { pendingFound: false, waitedMs: 0, completedRequests: 0, stillPending: 0, pendingRequests: [], totalRequests: 0, trackedRequests: [], allRecentRequests: [] }
|
|
178
|
+
: await waitForPendingRequests(beforeActionTimestamp, 200, networkWaitTimeout);
|
|
179
|
+
|
|
180
|
+
// Small delay to let pending requests update their error status
|
|
181
|
+
// (handles case where request completes with error right after maxWait expires)
|
|
182
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
183
|
+
|
|
184
|
+
// Check for page navigation (indicates form submit in non-SPA apps)
|
|
185
|
+
const currentUrl = page.url();
|
|
186
|
+
let navigationDetected = null;
|
|
187
|
+
if (urlBeforeAction && currentUrl !== urlBeforeAction) {
|
|
188
|
+
navigationDetected = {
|
|
189
|
+
from: urlBeforeAction,
|
|
190
|
+
to: currentUrl,
|
|
191
|
+
likelyFormSubmit: true // Page URL changed after click - likely form POST with redirect
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Fetch network requests from Bridge (Extension webRequest API)
|
|
196
|
+
// These persist across page navigations, unlike CDP requests
|
|
197
|
+
let bridgeRequests = [];
|
|
198
|
+
if (isBridgeConnected()) {
|
|
199
|
+
try {
|
|
200
|
+
const allBridgeRequests = await getNetworkRequestsFromBridge({ timeout: 2000 });
|
|
201
|
+
// Filter to requests after beforeActionTimestamp
|
|
202
|
+
const cutoffTime = beforeActionTimestamp - 1000; // 1s buffer
|
|
203
|
+
bridgeRequests = allBridgeRequests.filter(req =>
|
|
204
|
+
req.timestamp >= cutoffTime
|
|
205
|
+
);
|
|
206
|
+
} catch (e) {
|
|
207
|
+
// Bridge not available, continue without
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check for chrome error page (ERR_CONNECTION_REFUSED, etc.)
|
|
212
|
+
const url = currentUrl;
|
|
213
|
+
let chromeErrorInfo = null;
|
|
214
|
+
if (url.startsWith('chrome-error://')) {
|
|
215
|
+
chromeErrorInfo = await page.evaluate(() => {
|
|
216
|
+
const errorCode = document.querySelector('#error-code');
|
|
217
|
+
const suggestionText = document.querySelector('.suggestions');
|
|
218
|
+
return {
|
|
219
|
+
errorCode: errorCode?.textContent || 'UNKNOWN_ERROR',
|
|
220
|
+
suggestion: suggestionText?.textContent?.trim() || 'Connection failed'
|
|
221
|
+
};
|
|
222
|
+
}).catch(() => ({ errorCode: 'PAGE_LOAD_ERROR', suggestion: 'Navigation failed' }));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Collect errors that occurred after the action (including errors from just-completed requests)
|
|
226
|
+
const errors = collectErrors(beforeActionTimestamp);
|
|
227
|
+
|
|
228
|
+
// Combine into diagnostics report
|
|
229
|
+
const diagnostics = {
|
|
230
|
+
networkActivity: {
|
|
231
|
+
hadPendingRequests: networkInfo.pendingFound,
|
|
232
|
+
completedRequests: networkInfo.completedRequests,
|
|
233
|
+
stillPending: networkInfo.stillPending,
|
|
234
|
+
pendingRequests: networkInfo.pendingRequests,
|
|
235
|
+
totalRequests: networkInfo.totalRequests,
|
|
236
|
+
waitedMs: networkInfo.waitedMs,
|
|
237
|
+
trackedRequests: networkInfo.trackedRequests || [],
|
|
238
|
+
allRecentRequests: networkInfo.allRecentRequests || [],
|
|
239
|
+
// Bridge requests (from Extension webRequest API) - persist across page navigations
|
|
240
|
+
bridgeRequests: bridgeRequests.map(req => ({
|
|
241
|
+
method: req.method,
|
|
242
|
+
url: req.url,
|
|
243
|
+
type: req.type,
|
|
244
|
+
status: req.status,
|
|
245
|
+
timestamp: req.timestamp
|
|
246
|
+
}))
|
|
247
|
+
},
|
|
248
|
+
navigation: navigationDetected,
|
|
249
|
+
chromeError: chromeErrorInfo,
|
|
250
|
+
errors: {
|
|
251
|
+
consoleErrors: errors.consoleErrors,
|
|
252
|
+
networkErrors: errors.networkErrors,
|
|
253
|
+
consoleErrorsOmitted: errors.consoleErrorsOmitted,
|
|
254
|
+
networkErrorsOmitted: errors.networkErrorsOmitted,
|
|
255
|
+
totalErrors: errors.consoleErrors.length + errors.networkErrors.length
|
|
256
|
+
},
|
|
257
|
+
hasErrors: (errors.consoleErrors.length + errors.networkErrors.length) > 0 || chromeErrorInfo !== null
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return diagnostics;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Format diagnostics for AI-friendly output
|
|
265
|
+
* @param {Object} diagnostics - Diagnostics object from runPostClickDiagnostics
|
|
266
|
+
* @returns {string} Formatted text for AI
|
|
267
|
+
*/
|
|
268
|
+
export function formatDiagnosticsForAI(diagnostics) {
|
|
269
|
+
let output = '\n\n** POST-ACTION DIAGNOSTICS **';
|
|
270
|
+
|
|
271
|
+
// Chrome error page (connection refused, DNS failed, etc.)
|
|
272
|
+
if (diagnostics.chromeError) {
|
|
273
|
+
output += `\n\nš“ CRITICAL: Navigation Failed`;
|
|
274
|
+
output += `\n Error: ${diagnostics.chromeError.errorCode}`;
|
|
275
|
+
output += `\n Suggestion: ${diagnostics.chromeError.suggestion}`;
|
|
276
|
+
output += `\n ā Backend likely not running or unreachable`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Page navigation detection (form submit in non-SPA apps)
|
|
280
|
+
if (diagnostics.navigation) {
|
|
281
|
+
output += `\n\nš Page navigation detected (form submit):`;
|
|
282
|
+
output += `\n From: ${diagnostics.navigation.from}`;
|
|
283
|
+
output += `\n To: ${diagnostics.navigation.to}`;
|
|
284
|
+
output += `\n ā This indicates a successful form POST with page reload`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Network activity - show all tracked requests (GET/POST/PUT/PATCH/DELETE)
|
|
288
|
+
const netActivity = diagnostics.networkActivity;
|
|
289
|
+
const trackedRequests = netActivity.trackedRequests || [];
|
|
290
|
+
|
|
291
|
+
// Show requests detected within 200ms after action
|
|
292
|
+
if (trackedRequests.length > 0) {
|
|
293
|
+
output += `\n\nš” Network requests (${trackedRequests.length}):`;
|
|
294
|
+
trackedRequests.forEach((req, idx) => {
|
|
295
|
+
const statusIcon = req.status === 'pending' ? 'ā³' :
|
|
296
|
+
(req.status === 'completed' || (typeof req.status === 'number' && req.status < 400) ? 'ā' : 'ā');
|
|
297
|
+
const statusText = req.statusText || req.status || 'pending';
|
|
298
|
+
output += `\n ${idx + 1}. ${statusIcon} ${req.method} ${req.url}`;
|
|
299
|
+
output += `\n ā Status: ${statusText}`;
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Show if some requests are still pending after timeout
|
|
303
|
+
if (netActivity.stillPending > 0) {
|
|
304
|
+
output += `\n\nā³ ${netActivity.stillPending} request(s) still pending after ${Math.round(netActivity.waitedMs)}ms timeout`;
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
output += '\n\nš” No network requests detected within 200ms';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Bridge requests (from Extension - persist across page reloads)
|
|
311
|
+
const bridgeRequests = netActivity.bridgeRequests || [];
|
|
312
|
+
if (bridgeRequests.length > 0) {
|
|
313
|
+
output += `\n\nš” Browser-level requests (via Extension):`;
|
|
314
|
+
bridgeRequests.forEach((req, idx) => {
|
|
315
|
+
const statusIcon = req.status === 'pending' ? 'ā³' :
|
|
316
|
+
(req.status === 'completed' || (typeof req.status === 'number' && req.status < 400) ? 'ā' : 'ā');
|
|
317
|
+
output += `\n ${idx + 1}. ${statusIcon} ${req.method} ${req.url}`;
|
|
318
|
+
if (req.status !== 'pending') {
|
|
319
|
+
output += ` ā ${req.status}`;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Errors
|
|
325
|
+
if (diagnostics.errors.totalErrors > 0) {
|
|
326
|
+
output += `\n\nā ļø ERRORS DETECTED (${diagnostics.errors.totalErrors} total):`;
|
|
327
|
+
|
|
328
|
+
// Console errors
|
|
329
|
+
if (diagnostics.errors.consoleErrors.length > 0) {
|
|
330
|
+
output += `\n\nJavaScript Console Errors (${diagnostics.errors.consoleErrors.length}):`;
|
|
331
|
+
diagnostics.errors.consoleErrors.forEach((err, idx) => {
|
|
332
|
+
output += `\n ${idx + 1}. ${err.message}`;
|
|
333
|
+
if (err.location && err.location !== 'unknown') {
|
|
334
|
+
output += ` [${err.location}]`;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
// Show if some errors were omitted
|
|
338
|
+
if (diagnostics.errors.consoleErrorsOmitted > 0) {
|
|
339
|
+
output += `\n ... and ${diagnostics.errors.consoleErrorsOmitted} more console error(s) (omitted to prevent spam)`;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Network errors
|
|
344
|
+
if (diagnostics.errors.networkErrors.length > 0) {
|
|
345
|
+
output += `\n\nNetwork Errors (${diagnostics.errors.networkErrors.length}):`;
|
|
346
|
+
diagnostics.errors.networkErrors.forEach((err, idx) => {
|
|
347
|
+
output += `\n ${idx + 1}. ${err.method} ${err.url}`;
|
|
348
|
+
output += `\n Status: ${err.status}${err.statusText ? ' ' + err.statusText : ''}`;
|
|
349
|
+
if (err.errorText) {
|
|
350
|
+
output += `\n Error: ${err.errorText}`;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
// Show if some errors were omitted
|
|
354
|
+
if (diagnostics.errors.networkErrorsOmitted > 0) {
|
|
355
|
+
output += `\n ... and ${diagnostics.errors.networkErrorsOmitted} more network error(s) (omitted to prevent spam)`;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
output += '\nā No errors detected';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Pending requests (if any still running after timeout)
|
|
363
|
+
if (netActivity.stillPending > 0 && netActivity.pendingRequests.length > 0) {
|
|
364
|
+
output += `\n\nā³ PENDING REQUESTS (${netActivity.stillPending} still running):`;
|
|
365
|
+
netActivity.pendingRequests.forEach((req, idx) => {
|
|
366
|
+
output += `\n ${idx + 1}. ${req.method} ${req.url}`;
|
|
367
|
+
const elapsed = Date.now() - new Date(req.timestamp).getTime();
|
|
368
|
+
output += `\n Running for: ${elapsed}ms`;
|
|
369
|
+
});
|
|
370
|
+
output += `\n\nš” Suggestion: These requests may be slow or hanging`;
|
|
371
|
+
output += `\n ā Check backend performance or network connectivity`;
|
|
372
|
+
output += `\n ā Consider using getNetworkRequest() to monitor progress`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return output;
|
|
376
|
+
}
|
|
File without changes
|