chrome-extension-tester-mcp 2.0.0 → 2.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.
- package/README.md +14 -1
- package/package.json +1 -1
- package/src/state.js +19 -0
- package/src/tools/account-login.js +624 -0
- package/src/tools/index.js +2 -0
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ An **MCP (Model Context Protocol) server** that lets Claude interactively test a
|
|
|
32
32
|
- Test context menu registration and handler invocation
|
|
33
33
|
- Run assertions that return structured PASS / FAIL results
|
|
34
34
|
- Take screenshots at any point during testing
|
|
35
|
+
- Create and reuse test accounts on any website using disposable email (via Guerrilla Mail API)
|
|
35
36
|
|
|
36
37
|
---
|
|
37
38
|
|
|
@@ -139,6 +140,7 @@ Add to your project's `.mcp.json` or user-level MCP config:
|
|
|
139
140
|
| `send_message_to_background` | Send `chrome.runtime.sendMessage` from the popup context and return the response |
|
|
140
141
|
| `test_context_menu` | Check `contextMenus` API availability, simulate right-click, or invoke a menu item handler directly |
|
|
141
142
|
| `simulate_tab_events` | Open, close, switch, list, or close all browser tabs |
|
|
143
|
+
| `test_account_login` | Create or reuse a test account on any website using a disposable email; credentials are stored in `test-accounts.json` and reused across sessions |
|
|
142
144
|
|
|
143
145
|
---
|
|
144
146
|
|
|
@@ -218,6 +220,14 @@ Open a tab to https://news.ycombinator.com, then another to https://github.com,
|
|
|
218
220
|
Right-click on https://example.com and trigger the context menu item with id "my-action"
|
|
219
221
|
```
|
|
220
222
|
|
|
223
|
+
```
|
|
224
|
+
Create a test account on https://example.com/signup and save it as "my_test_account"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
Log in to https://example.com/login using the stored "my_test_account" credentials
|
|
229
|
+
```
|
|
230
|
+
|
|
221
231
|
---
|
|
222
232
|
|
|
223
233
|
## Project Structure
|
|
@@ -244,7 +254,8 @@ chrome-extension-testing-mcp/
|
|
|
244
254
|
│ ├── context-menu.js
|
|
245
255
|
│ ├── badge.js
|
|
246
256
|
│ ├── messaging.js
|
|
247
|
-
│
|
|
257
|
+
│ ├── tabs.js
|
|
258
|
+
│ └── account-login.js
|
|
248
259
|
├── package.json
|
|
249
260
|
└── README.md
|
|
250
261
|
```
|
|
@@ -259,6 +270,8 @@ chrome-extension-testing-mcp/
|
|
|
259
270
|
- Call `load_extension` again at any time to get a fresh browser instance
|
|
260
271
|
- Native Chrome context menus cannot be automated by Playwright — use `test_context_menu` with `trigger_item` to invoke handlers directly
|
|
261
272
|
- Badge and storage tools communicate via the service worker, so the extension must have a background service worker (MV3)
|
|
273
|
+
- `test_account_login` uses the [Guerrilla Mail API](https://www.guerrillamail.com/GuerrillaMailAPI.html) to generate disposable emails — no browser navigation required, no bot-blocking. Credentials are stored in `test-accounts.json` at the project root (add this to `.gitignore`)
|
|
274
|
+
- Use `action: "auto"` for `test_account_login` to automatically reuse stored credentials or create a new account if none exist
|
|
262
275
|
|
|
263
276
|
---
|
|
264
277
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-extension-tester-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "MCP server for interactive Chrome extension testing via Playwright — load, interact, assert, inspect storage, network, badges, messaging, tabs, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/state.js
CHANGED
|
@@ -42,6 +42,25 @@ export async function ensurePage() {
|
|
|
42
42
|
return state.page;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Like ensurePage but launches a plain headed Chromium if no browser is running yet.
|
|
47
|
+
* Used by tools that don't require an extension (e.g. test_account_login).
|
|
48
|
+
*/
|
|
49
|
+
export async function ensurePageStandalone() {
|
|
50
|
+
if (!state.browser) {
|
|
51
|
+
state.browser = await chromium.launchPersistentContext("", {
|
|
52
|
+
headless: false,
|
|
53
|
+
});
|
|
54
|
+
state.page = await state.browser.newPage();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!state.page || state.page.isClosed()) {
|
|
58
|
+
state.page = await state.browser.newPage();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return state.page;
|
|
62
|
+
}
|
|
63
|
+
|
|
45
64
|
export async function getServiceWorker() {
|
|
46
65
|
if (!state.browser) throw new Error("Browser not started. Call load_extension first.");
|
|
47
66
|
const workers = state.browser.serviceWorkers();
|
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { ensurePageStandalone } from "../state.js";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
// Credentials and screenshots are stored relative to the MCP server root
|
|
10
|
+
const ACCOUNTS_FILE = path.resolve(__dirname, "../../test-accounts.json");
|
|
11
|
+
const SCREENSHOTS_DIR = path.resolve(__dirname, "../../screenshots");
|
|
12
|
+
|
|
13
|
+
// Ensure the screenshots directory exists before saving any screenshot
|
|
14
|
+
function ensureScreenshotsDir() {
|
|
15
|
+
if (!fs.existsSync(SCREENSHOTS_DIR)) {
|
|
16
|
+
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildScreenshotPath(label) {
|
|
21
|
+
ensureScreenshotsDir();
|
|
22
|
+
return path.join(SCREENSHOTS_DIR, `${label}-${Date.now()}.png`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Guerrilla Mail API base — no browser required, no bot-blocking
|
|
26
|
+
const GUERRILLA_API = "https://api.guerrillamail.com/ajax.php";
|
|
27
|
+
|
|
28
|
+
// Ordered list of selectors to probe for email inputs
|
|
29
|
+
const EMAIL_SELECTORS = [
|
|
30
|
+
'input[type="email"]',
|
|
31
|
+
'input[name="email"]',
|
|
32
|
+
'input[placeholder*="email" i]',
|
|
33
|
+
'input[id*="email" i]',
|
|
34
|
+
'input[autocomplete="email"]',
|
|
35
|
+
'input[name*="email" i]',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Ordered list of selectors to probe for password inputs
|
|
39
|
+
const PASSWORD_SELECTORS = [
|
|
40
|
+
'input[type="password"]',
|
|
41
|
+
'input[name="password"]',
|
|
42
|
+
'input[id*="password" i]',
|
|
43
|
+
'input[name*="pass" i]',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Selectors for submit buttons (type-based)
|
|
47
|
+
const SUBMIT_SELECTORS = [
|
|
48
|
+
'button[type="submit"]',
|
|
49
|
+
'input[type="submit"]',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Text labels used on submit buttons across various sites
|
|
53
|
+
const SUBMIT_BUTTON_TEXTS = [
|
|
54
|
+
"Sign up",
|
|
55
|
+
"Create account",
|
|
56
|
+
"Register",
|
|
57
|
+
"Continue",
|
|
58
|
+
"Next",
|
|
59
|
+
"Get started",
|
|
60
|
+
"Join",
|
|
61
|
+
"Submit",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Phrases that indicate the site wants email verification before login
|
|
65
|
+
const VERIFICATION_PHRASES = [
|
|
66
|
+
"verify your email",
|
|
67
|
+
"check your email",
|
|
68
|
+
"verification link",
|
|
69
|
+
"confirm your email",
|
|
70
|
+
"email confirmation",
|
|
71
|
+
"sent you an email",
|
|
72
|
+
"please verify",
|
|
73
|
+
"activate your account",
|
|
74
|
+
"verification email",
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// Phrases that indicate a login attempt has failed (wrong creds or deleted account)
|
|
78
|
+
const LOGIN_FAILURE_PHRASES = [
|
|
79
|
+
"invalid",
|
|
80
|
+
"incorrect",
|
|
81
|
+
"not found",
|
|
82
|
+
"no account",
|
|
83
|
+
"doesn't exist",
|
|
84
|
+
"does not exist",
|
|
85
|
+
"wrong password",
|
|
86
|
+
"wrong email",
|
|
87
|
+
"account not found",
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// ---------- credential store helpers ----------
|
|
91
|
+
|
|
92
|
+
function loadAccounts() {
|
|
93
|
+
if (!fs.existsSync(ACCOUNTS_FILE)) return {};
|
|
94
|
+
const raw = fs.readFileSync(ACCOUNTS_FILE, "utf-8");
|
|
95
|
+
return JSON.parse(raw);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function saveAccounts(accounts) {
|
|
99
|
+
fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), "utf-8");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------- password generator ----------
|
|
103
|
+
|
|
104
|
+
function generateStrongPassword() {
|
|
105
|
+
const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
106
|
+
const lower = "abcdefghijklmnopqrstuvwxyz";
|
|
107
|
+
const digits = "0123456789";
|
|
108
|
+
const special = "!@#$%^&*()_+-=";
|
|
109
|
+
const allChars = upper + lower + digits + special;
|
|
110
|
+
|
|
111
|
+
// Guarantee at least one character from each category
|
|
112
|
+
const pickFrom = (charset) => charset[crypto.randomInt(charset.length)];
|
|
113
|
+
const mandatoryChars = [
|
|
114
|
+
pickFrom(upper),
|
|
115
|
+
pickFrom(lower),
|
|
116
|
+
pickFrom(digits),
|
|
117
|
+
pickFrom(special),
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const randomChars = Array.from({ length: 12 }, () => pickFrom(allChars));
|
|
121
|
+
|
|
122
|
+
// Shuffle mandatory + random chars together so the mandatory ones aren't always at the front
|
|
123
|
+
const allPasswordChars = [...mandatoryChars, ...randomChars];
|
|
124
|
+
for (let i = allPasswordChars.length - 1; i > 0; i--) {
|
|
125
|
+
const j = crypto.randomInt(i + 1);
|
|
126
|
+
[allPasswordChars[i], allPasswordChars[j]] = [allPasswordChars[j], allPasswordChars[i]];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return allPasswordChars.join("");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------- DOM interaction helpers ----------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Returns the first visible element matching any of the provided selectors,
|
|
136
|
+
* or null if none are found. Used to check presence without throwing.
|
|
137
|
+
*/
|
|
138
|
+
async function findVisibleElement(page, selectors) {
|
|
139
|
+
for (const selector of selectors) {
|
|
140
|
+
const el = await page.$(selector);
|
|
141
|
+
if (!el) continue;
|
|
142
|
+
const visible = await el.isVisible().catch(() => false);
|
|
143
|
+
if (visible) return el;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Fills an input field located by trying selectors in order.
|
|
150
|
+
* If no common selector matches, dumps all inputs from the DOM to help diagnose.
|
|
151
|
+
*/
|
|
152
|
+
async function fillInput(page, selectors, value, fieldLabel) {
|
|
153
|
+
const el = await findVisibleElement(page, selectors);
|
|
154
|
+
|
|
155
|
+
if (el) {
|
|
156
|
+
await el.fill(value);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fallback: dump input metadata so the caller can diagnose and pass explicit selectors
|
|
161
|
+
const allInputs = await page.$$eval("input", (elements) =>
|
|
162
|
+
elements.map((el) => ({
|
|
163
|
+
type: el.type,
|
|
164
|
+
name: el.name,
|
|
165
|
+
id: el.id,
|
|
166
|
+
placeholder: el.placeholder,
|
|
167
|
+
autocomplete: el.autocomplete,
|
|
168
|
+
}))
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Could not find a visible ${fieldLabel} input using common selectors.\n` +
|
|
173
|
+
`Available inputs on page:\n${JSON.stringify(allInputs, null, 2)}\n` +
|
|
174
|
+
`Pass an explicit ${fieldLabel}_selector to resolve this.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Clicks the submit button using type-based selectors first,
|
|
180
|
+
* then falls back to matching visible buttons by their text label.
|
|
181
|
+
*/
|
|
182
|
+
async function clickSubmitButton(page, customSelector) {
|
|
183
|
+
if (customSelector) {
|
|
184
|
+
await page.click(customSelector);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Try attribute-based selectors first
|
|
189
|
+
const attrMatch = await findVisibleElement(page, SUBMIT_SELECTORS);
|
|
190
|
+
if (attrMatch) {
|
|
191
|
+
await attrMatch.click();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Try text-based button matching
|
|
196
|
+
for (const label of SUBMIT_BUTTON_TEXTS) {
|
|
197
|
+
const btn = page.getByRole("button", { name: label, exact: false }).first();
|
|
198
|
+
const visible = await btn.isVisible().catch(() => false);
|
|
199
|
+
if (visible) {
|
|
200
|
+
await btn.click();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
throw new Error(
|
|
206
|
+
"Could not find a submit button using common selectors or text labels. " +
|
|
207
|
+
"Pass an explicit submit_selector to resolve this."
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Returns true if the current page text contains any phrase
|
|
213
|
+
* that signals the site wants the user to verify their email.
|
|
214
|
+
*/
|
|
215
|
+
async function pageRequiresVerification(page) {
|
|
216
|
+
const pageText = await page.evaluate(() => document.body.innerText.toLowerCase());
|
|
217
|
+
return VERIFICATION_PHRASES.some((phrase) => pageText.includes(phrase));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Returns true if the current page text contains any phrase
|
|
222
|
+
* that signals a login attempt failed.
|
|
223
|
+
*/
|
|
224
|
+
async function pageShowsLoginFailure(page) {
|
|
225
|
+
const pageText = await page.evaluate(() => document.body.innerText.toLowerCase());
|
|
226
|
+
return LOGIN_FAILURE_PHRASES.some((phrase) => pageText.includes(phrase));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------- Guerrilla Mail API helpers ----------
|
|
230
|
+
// These use the Guerrilla Mail REST API directly from Node — no browser required.
|
|
231
|
+
// API docs: https://www.guerrillamail.com/GuerrillaMailAPI.html
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Creates a new Guerrilla Mail session and returns { email, sidToken }.
|
|
235
|
+
* sidToken must be passed to subsequent API calls to maintain the inbox session.
|
|
236
|
+
*/
|
|
237
|
+
async function createGuerrillaSession() {
|
|
238
|
+
const response = await fetch(`${GUERRILLA_API}?f=get_email_address`);
|
|
239
|
+
const data = await response.json();
|
|
240
|
+
return {
|
|
241
|
+
email: data.email_addr,
|
|
242
|
+
sidToken: data.sid_token,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Polls the Guerrilla Mail inbox until a verification email arrives.
|
|
248
|
+
* Returns the first href that looks like a verify/confirm/activate/token URL.
|
|
249
|
+
* Polls every 3s up to 60s.
|
|
250
|
+
*/
|
|
251
|
+
async function extractVerificationLink(sidToken) {
|
|
252
|
+
const maxAttempts = 20;
|
|
253
|
+
|
|
254
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
255
|
+
const listUrl = `${GUERRILLA_API}?f=get_email_list&offset=0&sid_token=${sidToken}`;
|
|
256
|
+
const listResponse = await fetch(listUrl);
|
|
257
|
+
const listData = await listResponse.json();
|
|
258
|
+
|
|
259
|
+
const emails = listData.list || [];
|
|
260
|
+
|
|
261
|
+
if (emails.length > 0) {
|
|
262
|
+
// Fetch the full body of the first email
|
|
263
|
+
const emailId = emails[0].mail_id;
|
|
264
|
+
const fetchUrl = `${GUERRILLA_API}?f=fetch_email&email_id=${emailId}&sid_token=${sidToken}`;
|
|
265
|
+
const fetchResponse = await fetch(fetchUrl);
|
|
266
|
+
const emailData = await fetchResponse.json();
|
|
267
|
+
|
|
268
|
+
const body = emailData.mail_body || "";
|
|
269
|
+
|
|
270
|
+
// Extract all hrefs from anchor tags in the email body
|
|
271
|
+
const hrefMatches = [...body.matchAll(/href=["']([^"']+)["']/gi)];
|
|
272
|
+
const hrefs = hrefMatches.map((match) => match[1]);
|
|
273
|
+
|
|
274
|
+
// Prefer links that look like verification URLs
|
|
275
|
+
const verifyHref = hrefs.find((href) =>
|
|
276
|
+
href.includes("verify") ||
|
|
277
|
+
href.includes("confirm") ||
|
|
278
|
+
href.includes("activate") ||
|
|
279
|
+
href.includes("token")
|
|
280
|
+
);
|
|
281
|
+
if (verifyHref) return verifyHref;
|
|
282
|
+
|
|
283
|
+
// Fallback: return the first external link
|
|
284
|
+
const firstExternal = hrefs.find(
|
|
285
|
+
(href) => href.startsWith("http") && !href.includes("guerrillamail")
|
|
286
|
+
);
|
|
287
|
+
if (firstExternal) return firstExternal;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Wait 3s before polling again
|
|
291
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
throw new Error(
|
|
295
|
+
"Timed out waiting for verification email in Guerrilla Mail inbox (60s). " +
|
|
296
|
+
"The email may not have arrived yet."
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------- core account creation flow ----------
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Full flow: get temp email → navigate to signup → fill form (1 or 2 steps) →
|
|
304
|
+
* handle verification if needed → save credentials.
|
|
305
|
+
* Returns a human-readable result string.
|
|
306
|
+
*/
|
|
307
|
+
async function createNewAccount(page, args, accounts, key) {
|
|
308
|
+
// Get a disposable email address via Guerrilla Mail API (no browser navigation needed)
|
|
309
|
+
const { email: tempEmail, sidToken } = await createGuerrillaSession();
|
|
310
|
+
|
|
311
|
+
const password = generateStrongPassword();
|
|
312
|
+
|
|
313
|
+
// Navigate to the signup page
|
|
314
|
+
await page.goto(args.signup_url, { waitUntil: "domcontentloaded" });
|
|
315
|
+
await page.waitForTimeout(1500);
|
|
316
|
+
|
|
317
|
+
// Click a pre-signup trigger (e.g. a "Join Now" button that opens a modal) if specified
|
|
318
|
+
if (args.pre_click_selector) {
|
|
319
|
+
await page.click(args.pre_click_selector);
|
|
320
|
+
await page.waitForTimeout(1000);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const emailSelectors = args.email_selector ? [args.email_selector] : EMAIL_SELECTORS;
|
|
324
|
+
const passwordSelectors = args.password_selector ? [args.password_selector] : PASSWORD_SELECTORS;
|
|
325
|
+
|
|
326
|
+
// Fill the email field
|
|
327
|
+
await fillInput(page, emailSelectors, tempEmail, "email");
|
|
328
|
+
|
|
329
|
+
// Check if a password field is already visible on this page (single-step form)
|
|
330
|
+
const passwordFieldOnStep1 = await findVisibleElement(page, passwordSelectors);
|
|
331
|
+
if (passwordFieldOnStep1) {
|
|
332
|
+
await passwordFieldOnStep1.fill(password);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Submit step 1
|
|
336
|
+
await clickSubmitButton(page, args.submit_selector);
|
|
337
|
+
await page.waitForTimeout(2000);
|
|
338
|
+
|
|
339
|
+
// Handle multi-step forms where the password appears on a second page
|
|
340
|
+
const onStep2Already = args.step2_url != null;
|
|
341
|
+
const passwordMissingOnStep1 = passwordFieldOnStep1 == null;
|
|
342
|
+
|
|
343
|
+
if (onStep2Already || passwordMissingOnStep1) {
|
|
344
|
+
if (args.step2_url) {
|
|
345
|
+
await page.goto(args.step2_url, { waitUntil: "domcontentloaded" });
|
|
346
|
+
await page.waitForTimeout(1500);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const step2PasswordSelectors = args.step2_password_selector
|
|
350
|
+
? [args.step2_password_selector]
|
|
351
|
+
: passwordSelectors;
|
|
352
|
+
|
|
353
|
+
const step2PasswordField = await findVisibleElement(page, step2PasswordSelectors);
|
|
354
|
+
if (step2PasswordField) {
|
|
355
|
+
await step2PasswordField.fill(password);
|
|
356
|
+
await clickSubmitButton(page, args.step2_submit_selector);
|
|
357
|
+
await page.waitForTimeout(2000);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const screenshotPath = buildScreenshotPath(`account-created-${key}`);
|
|
362
|
+
await page.screenshot({ path: screenshotPath });
|
|
363
|
+
|
|
364
|
+
const verificationNeeded = await pageRequiresVerification(page);
|
|
365
|
+
|
|
366
|
+
let verifiedAt = null;
|
|
367
|
+
let verificationNote = "No email verification was required.";
|
|
368
|
+
|
|
369
|
+
if (verificationNeeded) {
|
|
370
|
+
verificationNote = "Site requested email verification — polling Guerrilla Mail inbox...";
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
const verifyLink = await extractVerificationLink(sidToken);
|
|
374
|
+
await page.goto(verifyLink, { waitUntil: "domcontentloaded" });
|
|
375
|
+
await page.waitForTimeout(2000);
|
|
376
|
+
verifiedAt = new Date().toISOString();
|
|
377
|
+
verificationNote = `Email verified successfully at ${verifiedAt}`;
|
|
378
|
+
} catch (verifyError) {
|
|
379
|
+
// Save credentials anyway so the user can retry verification manually
|
|
380
|
+
accounts[key] = buildCredentialEntry(
|
|
381
|
+
tempEmail,
|
|
382
|
+
password,
|
|
383
|
+
args,
|
|
384
|
+
false,
|
|
385
|
+
null,
|
|
386
|
+
verifyError.message
|
|
387
|
+
);
|
|
388
|
+
saveAccounts(accounts);
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
`Account created for "${key}" but verification failed.\n` +
|
|
392
|
+
`Error: ${verifyError.message}\n` +
|
|
393
|
+
`Credentials saved so you can retry manually.\n` +
|
|
394
|
+
`Email: ${tempEmail}\n` +
|
|
395
|
+
`Screenshot: ${screenshotPath}`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
accounts[key] = buildCredentialEntry(tempEmail, password, args, verifiedAt != null, verifiedAt, null);
|
|
401
|
+
saveAccounts(accounts);
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
`Account created for "${key}".\n` +
|
|
405
|
+
`Email: ${tempEmail}\n` +
|
|
406
|
+
`${verificationNote}\n` +
|
|
407
|
+
`Credentials saved to test-accounts.json\n` +
|
|
408
|
+
`Screenshot: ${screenshotPath}`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function buildCredentialEntry(email, password, args, verified, verifiedAt, verificationError) {
|
|
413
|
+
return {
|
|
414
|
+
email,
|
|
415
|
+
password,
|
|
416
|
+
signup_url: args.signup_url || null,
|
|
417
|
+
login_url: args.login_url || null,
|
|
418
|
+
created_at: new Date().toISOString(),
|
|
419
|
+
verified,
|
|
420
|
+
verified_at: verifiedAt,
|
|
421
|
+
verification_error: verificationError || null,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ---------- tool definition + handler ----------
|
|
426
|
+
|
|
427
|
+
export const definition = {
|
|
428
|
+
name: "test_account_login",
|
|
429
|
+
description:
|
|
430
|
+
"Create or reuse a test account for a website using a disposable email from temp-mail.org. " +
|
|
431
|
+
"Credentials are stored in test-accounts.json and reused across test sessions. " +
|
|
432
|
+
"Use action='auto' to login if credentials exist or create new ones if they don't.",
|
|
433
|
+
inputSchema: {
|
|
434
|
+
type: "object",
|
|
435
|
+
properties: {
|
|
436
|
+
action: {
|
|
437
|
+
type: "string",
|
|
438
|
+
enum: ["auto", "create", "login"],
|
|
439
|
+
description:
|
|
440
|
+
"auto: use stored credentials or create new (also re-creates if login fails). " +
|
|
441
|
+
"create: always create a new account. " +
|
|
442
|
+
"login: use stored credentials only — fails if none exist.",
|
|
443
|
+
},
|
|
444
|
+
account_key: {
|
|
445
|
+
type: "string",
|
|
446
|
+
description:
|
|
447
|
+
"Unique identifier for this account (e.g. 'jobright_main'). " +
|
|
448
|
+
"Used to store and retrieve credentials across sessions.",
|
|
449
|
+
},
|
|
450
|
+
signup_url: {
|
|
451
|
+
type: "string",
|
|
452
|
+
description: "URL of the signup/registration page. Required for 'create' and 'auto' actions.",
|
|
453
|
+
},
|
|
454
|
+
login_url: {
|
|
455
|
+
type: "string",
|
|
456
|
+
description: "URL of the login page. Required for 'login'; also used by 'auto' after account creation.",
|
|
457
|
+
},
|
|
458
|
+
pre_click_selector: {
|
|
459
|
+
type: "string",
|
|
460
|
+
description: "CSS selector for a button/link to click after navigation before the signup form appears (e.g. a 'Join Now' button that opens a modal). Optional.",
|
|
461
|
+
},
|
|
462
|
+
email_selector: {
|
|
463
|
+
type: "string",
|
|
464
|
+
description: "CSS selector for the email input field. Auto-detected if omitted.",
|
|
465
|
+
},
|
|
466
|
+
password_selector: {
|
|
467
|
+
type: "string",
|
|
468
|
+
description: "CSS selector for the password input field. Auto-detected if omitted.",
|
|
469
|
+
},
|
|
470
|
+
submit_selector: {
|
|
471
|
+
type: "string",
|
|
472
|
+
description: "CSS selector for the submit button. Auto-detected if omitted.",
|
|
473
|
+
},
|
|
474
|
+
step2_url: {
|
|
475
|
+
type: "string",
|
|
476
|
+
description:
|
|
477
|
+
"URL of a second signup page for multi-step forms where the password is on a separate page. Optional.",
|
|
478
|
+
},
|
|
479
|
+
step2_password_selector: {
|
|
480
|
+
type: "string",
|
|
481
|
+
description: "CSS selector for the password field on step 2 of a multi-step signup form. Optional.",
|
|
482
|
+
},
|
|
483
|
+
step2_submit_selector: {
|
|
484
|
+
type: "string",
|
|
485
|
+
description: "CSS selector for the submit button on step 2. Optional.",
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
required: ["action", "account_key"],
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
export async function handler(args) {
|
|
493
|
+
const page = await ensurePageStandalone();
|
|
494
|
+
const accounts = loadAccounts();
|
|
495
|
+
const key = args.account_key;
|
|
496
|
+
|
|
497
|
+
// ── LOGIN path ──────────────────────────────────────────────────────────────
|
|
498
|
+
const shouldAttemptLogin =
|
|
499
|
+
args.action === "login" || (args.action === "auto" && accounts[key]);
|
|
500
|
+
|
|
501
|
+
if (shouldAttemptLogin) {
|
|
502
|
+
const storedCreds = accounts[key];
|
|
503
|
+
|
|
504
|
+
if (!storedCreds) {
|
|
505
|
+
return {
|
|
506
|
+
content: [{
|
|
507
|
+
type: "text",
|
|
508
|
+
text:
|
|
509
|
+
`No stored credentials found for "${key}". ` +
|
|
510
|
+
`Use action: "create" or "auto" to create an account first.`,
|
|
511
|
+
}],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const loginUrl = args.login_url || storedCreds.login_url;
|
|
516
|
+
if (!loginUrl) {
|
|
517
|
+
return {
|
|
518
|
+
content: [{
|
|
519
|
+
type: "text",
|
|
520
|
+
text:
|
|
521
|
+
`No login_url provided and none stored for "${key}". ` +
|
|
522
|
+
`Pass login_url as an argument.`,
|
|
523
|
+
}],
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
await page.goto(loginUrl, { waitUntil: "domcontentloaded" });
|
|
528
|
+
await page.waitForTimeout(1500);
|
|
529
|
+
|
|
530
|
+
const emailSelectors = args.email_selector ? [args.email_selector] : EMAIL_SELECTORS;
|
|
531
|
+
const passwordSelectors = args.password_selector ? [args.password_selector] : PASSWORD_SELECTORS;
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
await fillInput(page, emailSelectors, storedCreds.email, "email");
|
|
535
|
+
await fillInput(page, passwordSelectors, storedCreds.password, "password");
|
|
536
|
+
await clickSubmitButton(page, args.submit_selector);
|
|
537
|
+
await page.waitForTimeout(2000);
|
|
538
|
+
} catch (formError) {
|
|
539
|
+
const screenshotPath = buildScreenshotPath(`login-form-error-${key}`);
|
|
540
|
+
await page.screenshot({ path: screenshotPath });
|
|
541
|
+
return {
|
|
542
|
+
content: [{
|
|
543
|
+
type: "text",
|
|
544
|
+
text:
|
|
545
|
+
`Login form interaction failed for "${key}": ${formError.message}\n` +
|
|
546
|
+
`Screenshot: ${screenshotPath}`,
|
|
547
|
+
}],
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const screenshotPath = buildScreenshotPath(`login-${key}`);
|
|
552
|
+
await page.screenshot({ path: screenshotPath });
|
|
553
|
+
|
|
554
|
+
const loginFailed = await pageShowsLoginFailure(page);
|
|
555
|
+
|
|
556
|
+
// If login failed and we're in auto mode, the account may have been deleted — recreate it
|
|
557
|
+
if (loginFailed && args.action === "auto") {
|
|
558
|
+
const recreateResult = await createNewAccount(page, args, accounts, key);
|
|
559
|
+
return {
|
|
560
|
+
content: [{
|
|
561
|
+
type: "text",
|
|
562
|
+
text:
|
|
563
|
+
`Login failed for "${key}" (account may have been deleted). Recreating...\n\n` +
|
|
564
|
+
recreateResult,
|
|
565
|
+
}],
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (loginFailed) {
|
|
570
|
+
return {
|
|
571
|
+
content: [{
|
|
572
|
+
type: "text",
|
|
573
|
+
text:
|
|
574
|
+
`Login failed for "${key}". ` +
|
|
575
|
+
`The stored account (${storedCreds.email}) may have been deleted. ` +
|
|
576
|
+
`Use action: "create" to generate a new account.\n` +
|
|
577
|
+
`Screenshot: ${screenshotPath}`,
|
|
578
|
+
}],
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const verificationNeeded = await pageRequiresVerification(page);
|
|
583
|
+
if (verificationNeeded) {
|
|
584
|
+
return {
|
|
585
|
+
content: [{
|
|
586
|
+
type: "text",
|
|
587
|
+
text:
|
|
588
|
+
`Logged in but the site is asking for email verification.\n` +
|
|
589
|
+
`Navigate to temp-mail.org to find and click the verification link.\n` +
|
|
590
|
+
`Screenshot: ${screenshotPath}`,
|
|
591
|
+
}],
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
content: [{
|
|
597
|
+
type: "text",
|
|
598
|
+
text: `Login successful for "${key}" (${storedCreds.email}).\nScreenshot: ${screenshotPath}`,
|
|
599
|
+
}],
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ── CREATE path ─────────────────────────────────────────────────────────────
|
|
604
|
+
if (args.action === "create" || args.action === "auto") {
|
|
605
|
+
if (!args.signup_url) {
|
|
606
|
+
return {
|
|
607
|
+
content: [{
|
|
608
|
+
type: "text",
|
|
609
|
+
text: `signup_url is required when action is "${args.action}".`,
|
|
610
|
+
}],
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const result = await createNewAccount(page, args, accounts, key);
|
|
615
|
+
return { content: [{ type: "text", text: result }] };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
content: [{
|
|
620
|
+
type: "text",
|
|
621
|
+
text: `Unknown action: "${args.action}". Valid values are "auto", "create", or "login".`,
|
|
622
|
+
}],
|
|
623
|
+
};
|
|
624
|
+
}
|
package/src/tools/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as contextMenu from "./context-menu.js";
|
|
|
11
11
|
import * as badge from "./badge.js";
|
|
12
12
|
import * as messaging from "./messaging.js";
|
|
13
13
|
import * as tabs from "./tabs.js";
|
|
14
|
+
import * as accountLogin from "./account-login.js";
|
|
14
15
|
|
|
15
16
|
const allTools = [
|
|
16
17
|
loadExtension,
|
|
@@ -26,6 +27,7 @@ const allTools = [
|
|
|
26
27
|
badge,
|
|
27
28
|
messaging,
|
|
28
29
|
tabs,
|
|
30
|
+
accountLogin,
|
|
29
31
|
];
|
|
30
32
|
|
|
31
33
|
export const TOOLS = allTools.map((t) => t.definition);
|