@litocodes/persona-test 1.0.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/.env ADDED
@@ -0,0 +1,11 @@
1
+ # Cerebras API Configuration
2
+ # Get your API key from: https://cloud.cerebras.ai/
3
+
4
+ # Your Cerebras API Key (required)
5
+ CEREBRAS_API_KEY=csk-fptmf5m4hxn84c3jx95yw9tce2v6yxkhpjkwjpkxd88cmrjp
6
+
7
+ # Model to use (Cerebras models - very fast!)
8
+ # Options:
9
+ # - llama-3.3-70b (recommended - smart and fast)
10
+ # - llama-3.1-8b (lightweight, fastest)
11
+ CEREBRAS_MODEL=llama-3.3-70b
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # 🎭 Persona - AI-Powered User Testing Tool
2
+
3
+ An automated user-testing MVP that uses AI Agents to "look" at a website (via screenshots) and behave like specific human personas.
4
+
5
+ ## πŸ—οΈ Architecture
6
+
7
+ ```
8
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
9
+ β”‚ PERSONA β”‚
10
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
11
+ β”‚ β”‚
12
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
13
+ β”‚ β”‚ πŸ‘οΈ EYE │───▢│ 🧠 BRAIN │───▢│ πŸ–οΈ HAND β”‚ β”‚
14
+ β”‚ β”‚Playwrightβ”‚ β”‚ OpenRouterβ”‚ β”‚ Locator β”‚ β”‚
15
+ β”‚ β”‚Screenshotβ”‚ β”‚ Vision β”‚ β”‚ Execute β”‚ β”‚
16
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
17
+ β”‚ β–² β”‚ β”‚
18
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
19
+ β”‚ REPEAT LOOP β”‚
20
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
21
+ ```
22
+
23
+ 1. **The Eye**: Playwright takes a JPEG screenshot of the current page
24
+ 2. **The Brain**: Screenshot + Persona System Prompt sent to Vision LLM
25
+ 3. **The Hand**: LLM returns JSON action with visual element description
26
+ 4. **The Locator**: Playwright semantic locators find and interact with elements
27
+
28
+ ## πŸ“¦ Installation
29
+
30
+ ```bash
31
+ # Clone or create the project
32
+ cd Persona
33
+
34
+ # Install dependencies
35
+ npm install
36
+
37
+ # Install Playwright browsers
38
+ npx playwright install chromium
39
+ ```
40
+
41
+ ## βš™οΈ Configuration
42
+
43
+ Create a `.env` file with your OpenRouter API key:
44
+
45
+ ```env
46
+ OPENAI_API_KEY=your-openrouter-api-key-here
47
+ OPENROUTER_MODEL=google/gemini-2.0-flash-exp:free
48
+ ```
49
+
50
+ Get your API key from: https://openrouter.ai/keys
51
+
52
+ ## πŸš€ Usage
53
+
54
+ ```bash
55
+ # Basic usage
56
+ node index.js --url="https://example.com" --agent="zoomer"
57
+
58
+ # With options
59
+ node index.js --url="https://google.com" --agent="boomer" --headless
60
+
61
+ # Test API connection
62
+ node index.js --test
63
+
64
+ # Show help
65
+ node index.js --help
66
+ ```
67
+
68
+ ### CLI Options
69
+
70
+ | Option | Alias | Description |
71
+ |--------|-------|-------------|
72
+ | `--url` | `-u` | Target website URL (required) |
73
+ | `--agent` | `-a` | Persona to simulate (required) |
74
+ | `--headless` | | Run browser without UI |
75
+ | `--test` | | Test OpenRouter connection |
76
+ | `--help` | `-h` | Show help message |
77
+
78
+ ## πŸ‘₯ Available Personas
79
+
80
+ ### πŸ§“ Boomer (70yo)
81
+ - Low tech literacy
82
+ - Needs high contrast and large text
83
+ - Quits if frustrated
84
+ - **Goal**: Find the contact phone number
85
+
86
+ ### ⚑ Zoomer (20yo)
87
+ - Extremely impatient
88
+ - Scrolls fast, ignores instructions
89
+ - Prefers social login
90
+ - **Goal**: Sign up as fast as possible
91
+
92
+ ### πŸ” Skeptic (40yo)
93
+ - Paranoid about privacy
94
+ - Refuses all cookies
95
+ - Checks Privacy Policy
96
+ - **Goal**: Evaluate site trustworthiness
97
+
98
+ ### πŸ’» Hacker (30yo)
99
+ - Security tester mindset
100
+ - Probes input fields
101
+ - Tests for SQL injection, XSS
102
+ - **Goal**: Test inputs for vulnerabilities
103
+
104
+ ## πŸ“ How It Works
105
+
106
+ 1. Launch Chromium browser and navigate to target URL
107
+ 2. Take a compressed JPEG screenshot
108
+ 3. Send screenshot + persona prompt to Vision LLM
109
+ 4. LLM analyzes the page and returns a JSON action:
110
+ ```json
111
+ {
112
+ "action": "click",
113
+ "selector": "text=Sign Up",
114
+ "reason": "Found signup button, clicking to proceed"
115
+ }
116
+ ```
117
+ 5. Execute the action using Playwright semantic locators
118
+ 6. Repeat until goal achieved or max actions reached
119
+
120
+ ## 🎯 Action Types
121
+
122
+ | Action | Description | Selector Format |
123
+ |--------|-------------|-----------------|
124
+ | `click` | Click an element | `text=Button Text` |
125
+ | `type` | Type into input | `placeholder=Email` |
126
+ | `scroll` | Scroll the page | `down`, `up`, `to=text` |
127
+ | `done` | Goal achieved | N/A |
128
+ | `quit` | Give up | N/A |
129
+
130
+ ## πŸ”§ Project Structure
131
+
132
+ ```
133
+ Persona/
134
+ β”œβ”€β”€ index.js # Main CLI entry point
135
+ β”œβ”€β”€ brain.js # OpenRouter vision client
136
+ β”œβ”€β”€ personas.js # Persona definitions
137
+ β”œβ”€β”€ package.json # Dependencies
138
+ β”œβ”€β”€ .env # API configuration
139
+ └── README.md # This file
140
+ ```
141
+
142
+ ## πŸ› Troubleshooting
143
+
144
+ **"OPENAI_API_KEY not set"**
145
+ - Create a `.env` file with your OpenRouter API key
146
+
147
+ **"Element not found"**
148
+ - The AI's selector may not match exactly
149
+ - The element might not be visible
150
+ - Try running with `--headless=false` to see what's happening
151
+
152
+ **"Connection failed"**
153
+ - Check your API key
154
+ - Run `node index.js --test` to verify connection
155
+ - Check OpenRouter status
156
+
157
+ ## πŸ“„ License
158
+
159
+ MIT
package/brain.js ADDED
@@ -0,0 +1,124 @@
1
+ // brain.js - With Action History & Strict Form Flow
2
+ // Reads last 3 actions to understand form context
3
+
4
+ import OpenAI from 'openai';
5
+ import dotenv from 'dotenv';
6
+
7
+ dotenv.config();
8
+
9
+ if (!process.env.CEREBRAS_API_KEY) {
10
+ console.error('❌ CEREBRAS_API_KEY not set');
11
+ process.exit(1);
12
+ }
13
+
14
+ const MODEL = process.env.CEREBRAS_MODEL || 'llama-3.3-70b';
15
+
16
+ const cerebras = new OpenAI({
17
+ baseURL: 'https://api.cerebras.ai/v1',
18
+ apiKey: process.env.CEREBRAS_API_KEY
19
+ });
20
+
21
+ /**
22
+ * Get agent action with full history context
23
+ * @param {string} domMap - DOM with [BROKEN] tags
24
+ * @param {object} persona - Persona profile
25
+ * @param {Array} actionHistory - Last 3 actions [{action, id, text}]
26
+ */
27
+ export async function getAgentAction(domMap, persona, actionHistory = []) {
28
+ // Format history as readable string
29
+ const historyStr = actionHistory.length > 0
30
+ ? actionHistory.map(h => `${h.action.toUpperCase()} [${h.id}] "${h.text}"`).join(' β†’ ')
31
+ : 'None yet';
32
+
33
+ // Check if we just typed (for form flow logic)
34
+ const lastTyped = actionHistory.filter(h => h.action === 'type').length;
35
+
36
+ const systemPrompt = `You are a User Tester acting as: "${persona.name}".
37
+
38
+ PERSONA: ${persona.behavior}
39
+
40
+ GOAL: "${persona.goal}"
41
+
42
+ --- RECENT HISTORY (Last 3 moves) ---
43
+ ${historyStr}
44
+ ${lastTyped > 0 ? '\n⚠️ You have already TYPED. If the form is filled, you should now CLICK a Submit/Login button!' : ''}
45
+
46
+ --- CRITICAL RULES ---
47
+ 1. πŸ›‘ NEVER click/type on elements marked [BROKEN - DO NOT USE]
48
+ 2. πŸ“ FORM FLOW:
49
+ - If you just TYPED into username/password, your NEXT action should be CLICK on a "Submit", "Login", "Create", or "Sign up" button
50
+ - Do NOT type into the same field twice
51
+ 3. πŸ”„ NO LOOPS: If your history shows the same action 2+ times, do something DIFFERENT
52
+ 4. βœ… GOAL CHECK: If PAGE CONTEXT shows your goal (phone number, success message), say "done"
53
+ 5. 😀 GIVE UP: If everything is [BROKEN] or stuck, say "quit"
54
+
55
+ ${domMap}
56
+
57
+ Return ONLY JSON (no markdown):
58
+ {
59
+ "action": "click" | "type" | "scroll" | "done" | "quit",
60
+ "elementId": 12,
61
+ "inputText": "test_user" (REQUIRED if typing),
62
+ "reason": "brief reason",
63
+ "frustration": 0-10
64
+ }`;
65
+
66
+ try {
67
+ console.log(' 🧠 Asking Cerebras...');
68
+
69
+ const response = await cerebras.chat.completions.create({
70
+ model: MODEL,
71
+ messages: [{ role: 'user', content: systemPrompt }],
72
+ max_tokens: 200,
73
+ temperature: 0.7
74
+ });
75
+
76
+ const content = response.choices[0]?.message?.content || '';
77
+ return parseAIResponse(content);
78
+
79
+ } catch (error) {
80
+ console.error(' ❌ AI Error:', error.message);
81
+ return { action: 'quit', reason: 'AI failed', frustration: 10 };
82
+ }
83
+ }
84
+
85
+ function parseAIResponse(content) {
86
+ let jsonStr = content.trim();
87
+ if (jsonStr.startsWith('```json')) jsonStr = jsonStr.slice(7);
88
+ else if (jsonStr.startsWith('```')) jsonStr = jsonStr.slice(3);
89
+ if (jsonStr.endsWith('```')) jsonStr = jsonStr.slice(0, -3);
90
+
91
+ const match = jsonStr.match(/\{[\s\S]*\}/);
92
+ if (match) jsonStr = match[0];
93
+
94
+ try {
95
+ const a = JSON.parse(jsonStr);
96
+ return {
97
+ action: a.action || 'quit',
98
+ elementId: parseInt(a.elementId) || null,
99
+ inputText: a.inputText || a.input_text || '',
100
+ reason: a.reason || '',
101
+ frustration: parseInt(a.frustration) || 0
102
+ };
103
+ } catch (e) {
104
+ return { action: 'quit', reason: 'Parse error', frustration: 10 };
105
+ }
106
+ }
107
+
108
+ export async function testConnection() {
109
+ console.log('πŸ§ͺ Testing Cerebras...');
110
+ try {
111
+ const r = await cerebras.chat.completions.create({
112
+ model: MODEL,
113
+ messages: [{ role: 'user', content: 'Say "Brain online"' }],
114
+ max_tokens: 10
115
+ });
116
+ console.log(' βœ…', r.choices[0]?.message?.content);
117
+ return true;
118
+ } catch (e) {
119
+ console.error(' ❌', e.message);
120
+ return false;
121
+ }
122
+ }
123
+
124
+ export default { getAgentAction, testConnection };
package/index.js ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+
3
+ // index.js - With Memory State (History + Blacklist)
4
+
5
+ import { chromium } from 'playwright';
6
+ import minimist from 'minimist';
7
+ import dotenv from 'dotenv';
8
+ import { parsePage, findElementById } from './parser.js';
9
+ import { getAgentAction, testConnection } from './brain.js';
10
+ import { getPersona, listPersonas } from './personas.js';
11
+
12
+ dotenv.config();
13
+
14
+ const argv = minimist(process.argv.slice(2), {
15
+ string: ['url', 'agent'],
16
+ boolean: ['headless', 'test', 'help'],
17
+ default: { headless: false },
18
+ alias: { u: 'url', a: 'agent', h: 'help' }
19
+ });
20
+
21
+ if (argv.help) {
22
+ console.log(`
23
+ 🎭 PERSONA - AI User Testing (Final Version)
24
+
25
+ Usage: node index.js --url="<site>" --agent="<persona>"
26
+ Personas: ${listPersonas().join(', ')}
27
+ `);
28
+ process.exit(0);
29
+ }
30
+
31
+ if (argv.test) { await testConnection(); process.exit(0); }
32
+ if (!argv.url || !argv.agent) { console.error('❌ --url and --agent required'); process.exit(1); }
33
+
34
+ const persona = getPersona(argv.agent);
35
+ if (!persona) { console.error(`❌ Unknown: ${argv.agent}`); process.exit(1); }
36
+
37
+ async function main() {
38
+ console.log(`
39
+ ╔══════════════════════════════════════════════════════════════╗
40
+ β•‘ 🎭 PERSONA - AI User Testing (Final) β•‘
41
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
42
+ `);
43
+ console.log(`🎯 ${argv.url}`);
44
+ console.log(`πŸ‘€ ${persona.name} | Goal: ${persona.goal}\n`);
45
+
46
+ const browser = await chromium.launch({ headless: argv.headless });
47
+ const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
48
+
49
+ await page.goto(argv.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
50
+ await page.waitForTimeout(2000);
51
+ console.log('βœ… Page loaded\n');
52
+
53
+ // 🧠 MEMORY STATE
54
+ const actionHistory = []; // Last 3 actions: [{action, id, text}]
55
+ const failedIds = []; // Blacklisted IDs
56
+
57
+ for (let step = 1; step <= persona.maxActions; step++) {
58
+ console.log(`β”Œβ”€ Step ${step}/${persona.maxActions} ${'─'.repeat(44)}`);
59
+
60
+ const html = await page.content();
61
+ const { domMap, elements, elementCount } = parsePage(html, failedIds);
62
+ console.log(` πŸ“Š ${elementCount} elements (${failedIds.length} blacklisted)`);
63
+
64
+ if (elementCount === 0) {
65
+ console.log(' ⚠️ No elements');
66
+ await page.mouse.wheel(0, 300);
67
+ continue;
68
+ }
69
+
70
+ // Pass full history to brain
71
+ const decision = await getAgentAction(domMap, persona, actionHistory);
72
+
73
+ console.log(` πŸ€– ${decision.action.toUpperCase()} [${decision.elementId}]`);
74
+ console.log(` πŸ’­ ${decision.reason}`);
75
+ console.log(` 😀 Frustration: ${decision.frustration}/10`);
76
+
77
+ if (decision.action === 'done' || decision.action === 'quit') {
78
+ console.log(`\n${'═'.repeat(60)}`);
79
+ console.log(decision.action === 'done' ? ' πŸŽ‰ GOAL ACHIEVED!' : ' 😀 GAVE UP');
80
+ console.log(` ${decision.reason}`);
81
+ console.log(`${'═'.repeat(60)}`);
82
+ break;
83
+ }
84
+
85
+ const target = decision.elementId ? findElementById(elements, decision.elementId) : null;
86
+
87
+ if (!target) {
88
+ console.log(' ❌ Invalid ID');
89
+ continue;
90
+ }
91
+
92
+ try {
93
+ const locator = page.locator(target.selector).first();
94
+
95
+ if (decision.action === 'click') {
96
+ await locator.click({ timeout: 4000 });
97
+ console.log(` πŸ–±οΈ Clicked: "${target.text}"`);
98
+ } else if (decision.action === 'type') {
99
+ const text = decision.inputText || persona.testStrings?.sql || 'test_user';
100
+ await locator.fill(text, { timeout: 4000 });
101
+ console.log(` ⌨️ Typed: "${text}"`);
102
+ } else if (decision.action === 'scroll') {
103
+ await page.mouse.wheel(0, 400);
104
+ console.log(' πŸ“œ Scrolled');
105
+ }
106
+
107
+ // βœ… Add to history (keep last 3)
108
+ actionHistory.push({ action: decision.action, id: decision.elementId, text: target.text });
109
+ if (actionHistory.length > 3) actionHistory.shift();
110
+
111
+ } catch (e) {
112
+ const msg = e.message.split('\n')[0].substring(0, 50);
113
+ console.log(` ⚠️ FAILED: ${msg}`);
114
+
115
+ // 🚨 Add to blacklist
116
+ failedIds.push(decision.elementId);
117
+ console.log(` 🚫 ID [${decision.elementId}] blacklisted`);
118
+ }
119
+
120
+ await page.waitForTimeout(2000);
121
+ console.log(`β””${'─'.repeat(58)}β”˜`);
122
+ }
123
+
124
+ console.log('\nπŸ“‹ History:', actionHistory.map(h => `${h.action}[${h.id}]`).join(' β†’ '));
125
+ console.log('🚫 Blacklist:', failedIds.length ? failedIds.join(', ') : 'none');
126
+ await browser.close();
127
+ console.log('βœ… Done\n');
128
+ }
129
+
130
+ main().catch(e => console.error('πŸ’€', e));
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@litocodes/persona-test",
3
+ "version": "1.0.0",
4
+ "description": "AI User Testing with Personality - Simulate real users breaking your website",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "persona": "./index.js",
9
+ "persona-test": "./index.js"
10
+ },
11
+ "scripts": {
12
+ "start": "node index.js",
13
+ "test": "node index.js --test",
14
+ "postinstall": "npx playwright install chromium"
15
+ },
16
+ "keywords": [
17
+ "user-testing",
18
+ "ai",
19
+ "playwright",
20
+ "automation",
21
+ "persona",
22
+ "qa",
23
+ "testing",
24
+ "ux"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/yourusername/persona-test"
34
+ },
35
+ "dependencies": {
36
+ "cheerio": "^1.0.0",
37
+ "dotenv": "^16.4.5",
38
+ "minimist": "^1.2.8",
39
+ "openai": "^4.77.0",
40
+ "playwright": "^1.49.1"
41
+ }
42
+ }
package/parser.js ADDED
@@ -0,0 +1,103 @@
1
+ // parser.js - With Blacklist Support
2
+ // Tags failed elements as [BROKEN] so AI avoids them
3
+
4
+ import * as cheerio from 'cheerio';
5
+
6
+ /**
7
+ * Parse HTML with blacklist support
8
+ * @param {string} html - Raw HTML
9
+ * @param {number[]} failedIds - IDs that previously failed
10
+ */
11
+ export function parsePage(html, failedIds = []) {
12
+ const $ = cheerio.load(html);
13
+ const elements = [];
14
+ let idCounter = 1;
15
+
16
+ $('script, style, noscript').remove();
17
+
18
+ // 1. Context
19
+ let contextText = "";
20
+ const pageTitle = $('title').text().trim();
21
+ if (pageTitle) contextText += `[TITLE] ${pageTitle}\n`;
22
+
23
+ $('h1, h2, h3, p, label').each((i, el) => {
24
+ const text = $(el).text().trim().replace(/\s+/g, ' ');
25
+ if (text.length > 5 && text.length < 150) {
26
+ contextText += `[TEXT] ${text.substring(0, 100)}\n`;
27
+ }
28
+ });
29
+
30
+ // Phone numbers
31
+ const phoneRegex = /(\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}|1-800-\w+-\w+)/g;
32
+ const phoneMatches = $('body').text().match(phoneRegex);
33
+ if (phoneMatches) {
34
+ phoneMatches.slice(0, 3).forEach(p => contextText += `[PHONE] ${p.trim()}\n`);
35
+ }
36
+
37
+ contextText = contextText.substring(0, 1500);
38
+
39
+ // 2. Interactive elements
40
+ $('button, a, input, select, textarea').each((i, el) => {
41
+ const $el = $(el);
42
+ const tagName = $el.prop('tagName')?.toLowerCase() || 'unknown';
43
+ const type = $el.attr('type') || '';
44
+
45
+ if (type === 'hidden') return;
46
+ if ($el.attr('aria-hidden') === 'true') return;
47
+
48
+ let text = $el.text().trim() ||
49
+ $el.attr('placeholder') ||
50
+ $el.attr('aria-label') ||
51
+ $el.attr('name') ||
52
+ $el.attr('value') || '';
53
+ text = text.replace(/\s+/g, ' ').trim();
54
+
55
+ let selector = tagName;
56
+ const id = $el.attr('id');
57
+ const name = $el.attr('name');
58
+ const placeholder = $el.attr('placeholder');
59
+
60
+ if (id) selector = `#${id}`;
61
+ else if (name) selector = `${tagName}[name="${name}"]`;
62
+ else if (placeholder) selector = `${tagName}[placeholder="${placeholder}"]`;
63
+ else if (type && tagName === 'input') selector = `input[type="${type}"]`;
64
+ else if (text && tagName !== 'input') selector = `${tagName}:has-text("${text.substring(0, 20)}")`;
65
+
66
+ if (!text && !id && !name && !placeholder) return;
67
+
68
+ elements.push({
69
+ id: idCounter++,
70
+ type: tagName,
71
+ text: text.substring(0, 50),
72
+ selector,
73
+ inputType: type
74
+ });
75
+ });
76
+
77
+ // 3. Build map with BROKEN tags
78
+ const interactiveMap = elements.map(el => {
79
+ const broken = failedIds.includes(el.id) ? ' ⚠️ [BROKEN - DO NOT USE]' : '';
80
+
81
+ if (el.type === 'input') return `[${el.id}] INPUT (${el.inputType || 'text'}): "${el.text}"${broken}`;
82
+ if (el.type === 'textarea') return `[${el.id}] TEXTAREA: "${el.text}"${broken}`;
83
+ if (el.type === 'select') return `[${el.id}] DROPDOWN: "${el.text}"${broken}`;
84
+ if (el.type === 'a') return `[${el.id}] LINK: "${el.text}"${broken}`;
85
+ return `[${el.id}] BUTTON: "${el.text}"${broken}`;
86
+ }).join('\n');
87
+
88
+ const domMap = `
89
+ --- PAGE CONTEXT ---
90
+ ${contextText || '(empty)'}
91
+ --------------------
92
+ --- INTERACTIVE ---
93
+ ${interactiveMap || '(none)'}
94
+ `;
95
+
96
+ return { domMap, elements, elementCount: elements.length };
97
+ }
98
+
99
+ export function findElementById(elements, id) {
100
+ return elements.find(el => el.id === id) || null;
101
+ }
102
+
103
+ export default { parsePage, findElementById };
package/personas.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Persona Profiles for AI-driven user testing
3
+ * Each persona has unique behaviors, limitations, and goals
4
+ * Optimized for text-based DOM parsing (no vision)
5
+ */
6
+
7
+ export const PERSONAS = {
8
+ boomer: {
9
+ name: "Grandpa Joe",
10
+ age: 70,
11
+ behavior: `70 years old. Low tech literacy.
12
+ Gets confused by modern UI with too many options.
13
+ Looking for obvious things like 'Contact', 'Help', or 'Phone'.
14
+ Prefers simple, clear labels over icons.
15
+ Quits if overwhelmed by too many choices.
16
+ Never clicks on anything that seems "fancy" or unclear.`,
17
+ goal: "Find a phone number or contact information to call.",
18
+ maxActions: 10,
19
+ frustrationThreshold: 5
20
+ },
21
+
22
+ zoomer: {
23
+ name: "Zoomer Zoe",
24
+ age: 20,
25
+ behavior: `20 years old. Extremely impatient.
26
+ Clicks the first interesting thing immediately.
27
+ Ignores instructions and fine print completely.
28
+ Prefers 'Sign Up', 'Get Started', 'Join' buttons.
29
+ Loves social login (Google, Apple, etc).
30
+ Gets annoyed if there are too many form fields.
31
+ KNOWS to fill out username/password fields BEFORE clicking submit.
32
+ Types fast with occasional typos. Uses simple usernames like "zoomer123".
33
+ Scrolls fast, barely reads anything.`,
34
+ goal: "Sign up or find the most interesting thing as fast as possible.",
35
+ maxActions: 10,
36
+ frustrationThreshold: 4
37
+ },
38
+
39
+ skeptic: {
40
+ name: "Skeptical Sam",
41
+ age: 40,
42
+ behavior: `40 years old. Paranoid about privacy.
43
+ Always looks for 'Privacy Policy' and 'Terms' links first.
44
+ Refuses to click 'Accept Cookies' - looks for 'Reject' or 'Manage'.
45
+ Reads the fine print before doing anything.
46
+ Distrusts sites without clear contact info.
47
+ Never uses social login - too much data sharing.
48
+ Looks for security indicators and trust badges.`,
49
+ goal: "Find and read the Privacy Policy, reject cookies, evaluate trustworthiness.",
50
+ maxActions: 12,
51
+ frustrationThreshold: 8
52
+ },
53
+
54
+ hacker: {
55
+ name: "Hacker Harry",
56
+ age: 30,
57
+ behavior: `Security researcher mindset.
58
+ Looks for INPUT fields to test with injection strings.
59
+ Types test payloads like: ' OR '1'='1' --
60
+ Also tries: <script>alert(1)</script>
61
+ Methodically tests each input field found.
62
+ Looks for hidden forms, admin links, or unusual endpoints.
63
+ Very patient and thorough.`,
64
+ goal: "Find all input fields and test them with SQL injection strings.",
65
+ maxActions: 15,
66
+ frustrationThreshold: 10,
67
+ testStrings: {
68
+ sql: "' OR '1'='1' --",
69
+ xss: "<script>alert('XSS')</script>",
70
+ traversal: "../../etc/passwd"
71
+ }
72
+ },
73
+
74
+ explorer: {
75
+ name: "Explorer Emma",
76
+ age: 35,
77
+ behavior: `Curious user who wants to understand everything.
78
+ Clicks on every main navigation link.
79
+ Reads headings and summaries.
80
+ Takes note of what each section contains.
81
+ Methodically explores the entire site structure.
82
+ Creates a mental map of the site.`,
83
+ goal: "Explore the entire site and understand its structure.",
84
+ maxActions: 20,
85
+ frustrationThreshold: 15
86
+ }
87
+ };
88
+
89
+ /**
90
+ * Get a persona by name (case-insensitive)
91
+ * @param {string} name - The persona name
92
+ * @returns {object|null} The persona object or null if not found
93
+ */
94
+ export function getPersona(name) {
95
+ const key = name.toLowerCase();
96
+ return PERSONAS[key] || null;
97
+ }
98
+
99
+ /**
100
+ * List all available persona names
101
+ * @returns {string[]} Array of persona names
102
+ */
103
+ export function listPersonas() {
104
+ return Object.keys(PERSONAS);
105
+ }
106
+
107
+ export default PERSONAS;