@litocodes/persona-test 1.0.0 β†’ 1.2.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 CHANGED
@@ -1,159 +1,123 @@
1
- # 🎭 Persona - AI-Powered User Testing Tool
1
+ # 🌍 Persona: The Lagos Test
2
2
 
3
- An automated user-testing MVP that uses AI Agents to "look" at a website (via screenshots) and behave like specific human personas.
3
+ > **Your app works on a MacBook Pro on Fiber. It breaks for the other 5 billion people.**
4
4
 
5
- ## πŸ—οΈ Architecture
5
+ Persona is **Chaos Engineering for UX**. It spawns adversarial AI agents with distinct psychological profiles to attack your app under real-world conditions.
6
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
- ```
7
+ [![npm](https://img.shields.io/npm/v/@litocodes/persona-test)](https://www.npmjs.com/package/@litocodes/persona-test)
8
+
9
+ ---
22
10
 
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
11
+ ## πŸ”₯ The Lagos Test
27
12
 
28
- ## πŸ“¦ Installation
13
+ Silicon Valley builds software for perfect internet. Persona tests for the **real world**:
29
14
 
30
15
  ```bash
31
- # Clone or create the project
32
- cd Persona
16
+ # Test your site as an emerging market user on 3G
17
+ npx @litocodes/persona-test --url="https://your-site.com" --agent="lagos" --network="lagos-3g"
18
+ ```
33
19
 
34
- # Install dependencies
35
- npm install
20
+ **What happens:**
21
+ - 🌐 400ms latency, 400 Kbps bandwidth (real Lagos conditions)
22
+ - πŸ“± Simulated $50 Android phone behavior
23
+ - πŸ–±οΈ Double-clicks (because first click "didn't work")
24
+ - πŸ”„ Aggressive refresh when spinners hang
25
+ - 😀 Immediate distrust of popups and data collection
36
26
 
37
- # Install Playwright browsers
38
- npx playwright install chromium
39
- ```
27
+ ---
28
+
29
+ ## 🎭 The Agents
40
30
 
41
- ## βš™οΈ Configuration
31
+ | Agent | Personality | The Test |
32
+ |-------|-------------|----------|
33
+ | 🌍 **Agent Lagos** | Emerging market user on $50 phone, 3G | Survives your bloated JS bundle |
34
+ | πŸ‘΄ **Grandpa Joe** | 70yo, low tech literacy | Finds the phone number |
35
+ | 🏎️ **Zoomer Zoe** | 20yo, rage-clicks, impatient | Signs up in <5 clicks |
36
+ | πŸ•΅οΈ **Skeptical Sam** | Privacy paranoid, reads fine print | Finds and rejects cookies |
37
+ | πŸ’€ **Hacker Harry** | Security researcher | SQL injection testing |
38
+ | 🧭 **Explorer Emma** | Methodical, maps everything | Full site exploration |
42
39
 
43
- Create a `.env` file with your OpenRouter API key:
40
+ ---
44
41
 
45
- ```env
46
- OPENAI_API_KEY=your-openrouter-api-key-here
47
- OPENROUTER_MODEL=google/gemini-2.0-flash-exp:free
42
+ ## 🌐 Network Chaos Modes
43
+
44
+ ```bash
45
+ --network="wifi" # No throttling
46
+ --network="4g" # 4 Mbps, 20ms
47
+ --network="3g" # 1.5 Mbps, 100ms
48
+ --network="lagos-3g" # 400 Kbps, 400ms (Nigerian 3G)
49
+ --network="lagos-tunnel" # 200 Kbps, 800ms (tunnel effect)
50
+ --network="chaos" # 100 Kbps, 1200ms (worst case)
51
+ --network="edge" # 50 Kbps, 800ms (2G)
48
52
  ```
49
53
 
50
- Get your API key from: https://openrouter.ai/keys
54
+ ---
51
55
 
52
- ## πŸš€ Usage
56
+ ## πŸš€ Quick Start
53
57
 
54
58
  ```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
59
+ # Run instantly
60
+ npx @litocodes/persona-test --url="https://your-site.com" --agent="lagos" --network="lagos-3g"
60
61
 
61
- # Test API connection
62
- node index.js --test
62
+ # Run ALL agents in parallel (Mission Control)
63
+ npx @litocodes/persona-test --url="https://your-site.com" --all
63
64
 
64
- # Show help
65
- node index.js --help
65
+ # Set your Cerebras API key (free)
66
+ export CEREBRAS_API_KEY=csk-xxxxx
66
67
  ```
67
68
 
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
69
+ ---
70
+
71
+ ## πŸ“¦ Output
72
+
73
+ Every session produces:
74
+
75
+ 1. **πŸŽ₯ Video Recording** - Watch the AI break your site
76
+ 2. **πŸ“ QA Report** - AI-generated friction analysis
77
+ 3. **πŸ“‹ Action Log** - Step-by-step decision trail
131
78
 
132
79
  ```
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
80
+ videos/Agent_Lagos_lagos-3g_2026-01-25_ABANDONED.webm
81
+ reports/REPORT_Agent_Lagos_2026-01-25.md
140
82
  ```
141
83
 
142
- ## πŸ› Troubleshooting
84
+ ---
85
+
86
+ ## 🎯 Why This Exists
87
+
88
+ > "If you pass the Lagos Test, you're ready for the world."
89
+ > "If you fail, you're just a US-only toy."
90
+
91
+ Most testing tools check if buttons **work**.
92
+ Persona checks if buttons are **frustrating**.
93
+
94
+ We test for:
95
+ - **Confusion**, not correctness
96
+ - **Trust**, not just functionality
97
+ - **Global readiness**, not just US/EU
98
+
99
+ ---
100
+
101
+ ## πŸ› οΈ CLI Options
102
+
103
+ ```bash
104
+ persona --url="<URL>" --agent="<AGENT>" [options]
105
+
106
+ Options:
107
+ --url, -u Target URL (required)
108
+ --agent, -a lagos, boomer, zoomer, skeptic, hacker, explorer
109
+ --all Run ALL agents in parallel
110
+ --network, -n wifi, 4g, 3g, lagos-3g, lagos-tunnel, chaos, edge
111
+ --record Record video (default: true)
112
+ --headless Run without visible browser
113
+ ```
143
114
 
144
- **"OPENAI_API_KEY not set"**
145
- - Create a `.env` file with your OpenRouter API key
115
+ ---
146
116
 
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
117
+ ## πŸ“œ License
151
118
 
152
- **"Connection failed"**
153
- - Check your API key
154
- - Run `node index.js --test` to verify connection
155
- - Check OpenRouter status
119
+ MIT Β© 2026
156
120
 
157
- ## πŸ“„ License
121
+ ---
158
122
 
159
- MIT
123
+ **Built for the next billion users.** 🌍
package/index.js CHANGED
@@ -1,130 +1,253 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
- // index.js - With Memory State (History + Blacklist)
3
+ // index.js - With Parallel Execution & Network Throttling πŸš€
4
4
 
5
5
  import { chromium } from 'playwright';
6
6
  import minimist from 'minimist';
7
7
  import dotenv from 'dotenv';
8
+ import fs from 'fs';
9
+ import path from 'path';
8
10
  import { parsePage, findElementById } from './parser.js';
9
11
  import { getAgentAction, testConnection } from './brain.js';
10
- import { getPersona, listPersonas } from './personas.js';
12
+ import { getPersona, listPersonas, PERSONAS } from './personas.js';
13
+ import { generateReport } from './reporter.js';
11
14
 
12
15
  dotenv.config();
13
16
 
17
+ // Network throttling presets (simulating real-world conditions)
18
+ const NETWORK_PRESETS = {
19
+ 'wifi': null, // No throttling
20
+ '4g': {
21
+ offline: false,
22
+ downloadThroughput: 4 * 1024 * 1024 / 8,
23
+ uploadThroughput: 3 * 1024 * 1024 / 8,
24
+ latency: 20
25
+ },
26
+ '3g': {
27
+ offline: false,
28
+ downloadThroughput: 1.5 * 1024 * 1024 / 8,
29
+ uploadThroughput: 750 * 1024 / 8,
30
+ latency: 100
31
+ },
32
+ 'lagos-3g': { // 🌍 The Lagos Test - Real emerging market conditions
33
+ offline: false,
34
+ downloadThroughput: 400 * 1024 / 8, // 400 Kbps
35
+ uploadThroughput: 150 * 1024 / 8, // 150 Kbps
36
+ latency: 400 // High latency (the killer)
37
+ },
38
+ 'lagos-tunnel': { // πŸš‡ The Tunnel Effect - Connection drops, reconnects
39
+ offline: false,
40
+ downloadThroughput: 200 * 1024 / 8, // 200 Kbps (barely usable)
41
+ uploadThroughput: 50 * 1024 / 8, // 50 Kbps
42
+ latency: 800 // Extreme latency
43
+ },
44
+ 'chaos': { // πŸ’€ Chaos Mode - The worst case scenario
45
+ offline: false,
46
+ downloadThroughput: 100 * 1024 / 8, // 100 Kbps
47
+ uploadThroughput: 30 * 1024 / 8, // 30 Kbps
48
+ latency: 1200 // Pain
49
+ },
50
+ 'edge': { // 2G/Edge
51
+ offline: false,
52
+ downloadThroughput: 50 * 1024 / 8,
53
+ uploadThroughput: 20 * 1024 / 8,
54
+ latency: 800
55
+ }
56
+ };
57
+
58
+ // Device simulation presets
59
+ const DEVICE_PRESETS = {
60
+ 'macbook': { slowMo: 0 },
61
+ 'iphone': { slowMo: 50 },
62
+ 'android-mid': { slowMo: 100 },
63
+ 'android-budget': { slowMo: 200 }, // $50 phone - noticeable input lag
64
+ 'android-chaos': { slowMo: 400 } // $30 phone - painful
65
+ };
66
+
14
67
  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' }
68
+ string: ['url', 'agent', 'network'],
69
+ boolean: ['headless', 'test', 'help', 'record', 'all'],
70
+ default: { headless: false, record: true, network: 'wifi' },
71
+ alias: { u: 'url', a: 'agent', h: 'help', r: 'record', n: 'network' }
19
72
  });
20
73
 
21
74
  if (argv.help) {
22
75
  console.log(`
23
- 🎭 PERSONA - AI User Testing (Final Version)
24
-
25
- Usage: node index.js --url="<site>" --agent="<persona>"
26
- Personas: ${listPersonas().join(', ')}
76
+ 🎭 PERSONA - AI User Testing (Parallel + Network)
77
+
78
+ Usage:
79
+ persona --url="<site>" --agent="<persona>"
80
+ persona --url="<site>" --all # Run ALL agents in parallel!
81
+
82
+ Options:
83
+ --url, -u Target URL (required)
84
+ --agent, -a Single persona: ${listPersonas().join(', ')}
85
+ --all Run ALL personas in parallel (Mission Control mode)
86
+ --network, -n Network: wifi, 4g, 3g, lagos-3g, edge
87
+ --record, -r Record video (default: true)
88
+ --headless Run headless
89
+ --test Test API connection
90
+
91
+ Examples:
92
+ persona --url="https://example.com" --agent="zoomer" --network="lagos-3g"
93
+ persona --url="https://example.com" --all # 5 browsers at once!
27
94
  `);
28
95
  process.exit(0);
29
96
  }
30
97
 
31
98
  if (argv.test) { await testConnection(); process.exit(0); }
32
- if (!argv.url || !argv.agent) { console.error('❌ --url and --agent required'); process.exit(1); }
99
+ if (!argv.url) { console.error('❌ --url required'); process.exit(1); }
100
+ if (!argv.agent && !argv.all) { console.error('❌ --agent or --all required'); process.exit(1); }
33
101
 
34
- const persona = getPersona(argv.agent);
35
- if (!persona) { console.error(`❌ Unknown: ${argv.agent}`); process.exit(1); }
102
+ // Ensure folders exist
103
+ if (!fs.existsSync('videos')) fs.mkdirSync('videos');
36
104
 
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`);
105
+ /**
106
+ * Run a single agent session
107
+ */
108
+ async function runAgent(url, persona, options = {}) {
109
+ const { network = 'wifi', record = true, headless = false } = options;
110
+ const networkConfig = NETWORK_PRESETS[network];
45
111
 
46
- const browser = await chromium.launch({ headless: argv.headless });
47
- const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
112
+ console.log(`\nπŸš€ [${persona.name}] Starting... (Network: ${network})`);
48
113
 
49
- await page.goto(argv.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
50
- await page.waitForTimeout(2000);
51
- console.log('βœ… Page loaded\n');
114
+ const browser = await chromium.launch({ headless });
52
115
 
53
- // 🧠 MEMORY STATE
54
- const actionHistory = []; // Last 3 actions: [{action, id, text}]
55
- const failedIds = []; // Blacklisted IDs
116
+ const contextOptions = {
117
+ viewport: { width: 1280, height: 800 },
118
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0'
119
+ };
56
120
 
57
- for (let step = 1; step <= persona.maxActions; step++) {
58
- console.log(`β”Œβ”€ Step ${step}/${persona.maxActions} ${'─'.repeat(44)}`);
121
+ if (record) {
122
+ contextOptions.recordVideo = { dir: 'videos/', size: { width: 1280, height: 800 } };
123
+ }
124
+
125
+ const context = await browser.newContext(contextOptions);
126
+ const page = await context.newPage();
59
127
 
128
+ // 🌐 Apply network throttling via CDP
129
+ if (networkConfig) {
130
+ const client = await context.newCDPSession(page);
131
+ await client.send('Network.emulateNetworkConditions', networkConfig);
132
+ console.log(` πŸ“Ά [${persona.name}] Network throttled to ${network}`);
133
+ }
134
+
135
+ try {
136
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
137
+ await page.waitForTimeout(2000);
138
+ } catch (e) {
139
+ console.log(` ❌ [${persona.name}] Failed to load: ${e.message.substring(0, 50)}`);
140
+ await browser.close();
141
+ return { agent: persona.name, outcome: 'LOAD_FAILED' };
142
+ }
143
+
144
+ const actionHistory = [];
145
+ const failedIds = [];
146
+ let outcome = 'MAX_ACTIONS';
147
+
148
+ for (let step = 1; step <= persona.maxActions; step++) {
60
149
  const html = await page.content();
61
150
  const { domMap, elements, elementCount } = parsePage(html, failedIds);
62
- console.log(` πŸ“Š ${elementCount} elements (${failedIds.length} blacklisted)`);
63
151
 
64
- if (elementCount === 0) {
65
- console.log(' ⚠️ No elements');
66
- await page.mouse.wheel(0, 300);
67
- continue;
68
- }
152
+ if (elementCount === 0) { await page.mouse.wheel(0, 300); continue; }
69
153
 
70
- // Pass full history to brain
71
154
  const decision = await getAgentAction(domMap, persona, actionHistory);
72
155
 
73
- console.log(` πŸ€– ${decision.action.toUpperCase()} [${decision.elementId}]`);
74
- console.log(` πŸ’­ ${decision.reason}`);
75
- console.log(` 😀 Frustration: ${decision.frustration}/10`);
156
+ console.log(` πŸ€– [${persona.name}] Step ${step}: ${decision.action} [${decision.elementId}] - ${decision.reason?.substring(0, 40)}`);
76
157
 
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
- }
158
+ if (decision.action === 'done') { outcome = 'SUCCESS'; break; }
159
+ if (decision.action === 'quit') { outcome = 'ABANDONED'; break; }
84
160
 
85
161
  const target = decision.elementId ? findElementById(elements, decision.elementId) : null;
86
-
87
- if (!target) {
88
- console.log(' ❌ Invalid ID');
89
- continue;
90
- }
162
+ if (!target) continue;
91
163
 
92
164
  try {
93
165
  const locator = page.locator(target.selector).first();
166
+ await locator.highlight();
167
+ await page.waitForTimeout(200);
94
168
 
95
169
  if (decision.action === 'click') {
96
170
  await locator.click({ timeout: 4000 });
97
- console.log(` πŸ–±οΈ Clicked: "${target.text}"`);
98
171
  } 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');
172
+ await locator.fill(decision.inputText || persona.testStrings?.sql || 'test', { timeout: 4000 });
105
173
  }
106
174
 
107
- // βœ… Add to history (keep last 3)
108
175
  actionHistory.push({ action: decision.action, id: decision.elementId, text: target.text });
109
176
  if (actionHistory.length > 3) actionHistory.shift();
110
177
 
111
178
  } catch (e) {
112
- const msg = e.message.split('\n')[0].substring(0, 50);
113
- console.log(` ⚠️ FAILED: ${msg}`);
114
-
115
- // 🚨 Add to blacklist
116
179
  failedIds.push(decision.elementId);
117
- console.log(` 🚫 ID [${decision.elementId}] blacklisted`);
118
180
  }
119
181
 
120
- await page.waitForTimeout(2000);
121
- console.log(`β””${'─'.repeat(58)}β”˜`);
182
+ await page.waitForTimeout(1500);
122
183
  }
123
184
 
124
- console.log('\nπŸ“‹ History:', actionHistory.map(h => `${h.action}[${h.id}]`).join(' β†’ '));
125
- console.log('🚫 Blacklist:', failedIds.length ? failedIds.join(', ') : 'none');
185
+ // Save video
186
+ const videoPath = record ? await page.video()?.path() : null;
187
+ await context.close();
126
188
  await browser.close();
127
- console.log('βœ… Done\n');
189
+
190
+ if (record && videoPath && fs.existsSync(videoPath)) {
191
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
192
+ const filename = `${persona.name.replace(/\s+/g, '_')}_${network}_${timestamp}_${outcome}.webm`;
193
+ fs.renameSync(videoPath, path.join('videos', filename));
194
+ console.log(` πŸŽ₯ [${persona.name}] Saved: videos/${filename}`);
195
+ }
196
+
197
+ // πŸ“ Generate QA Report
198
+ await generateReport(persona, url, actionHistory, failedIds, outcome, network);
199
+
200
+ console.log(` βœ… [${persona.name}] Finished: ${outcome}`);
201
+ return { agent: persona.name, outcome, network };
202
+ }
203
+
204
+ /**
205
+ * Main entry point
206
+ */
207
+ async function main() {
208
+ console.log(`
209
+ ╔══════════════════════════════════════════════════════════════╗
210
+ β•‘ 🎭 PERSONA - AI User Testing (Parallel + Network) β•‘
211
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
212
+ `);
213
+ console.log(`🎯 Target: ${argv.url}`);
214
+ console.log(`πŸ“Ά Network: ${argv.network}`);
215
+
216
+ const options = {
217
+ network: argv.network,
218
+ record: argv.record,
219
+ headless: argv.headless
220
+ };
221
+
222
+ if (argv.all) {
223
+ // πŸš€ PARALLEL MODE: Run ALL agents at once!
224
+ console.log(`\nπŸ”₯ MISSION CONTROL: Launching ALL ${listPersonas().length} agents in parallel!\n`);
225
+
226
+ const agents = listPersonas().map(name => getPersona(name));
227
+
228
+ const results = await Promise.all(
229
+ agents.map(persona => runAgent(argv.url, persona, options))
230
+ );
231
+
232
+ // Summary
233
+ console.log(`\n${'═'.repeat(60)}`);
234
+ console.log('πŸ“Š MISSION REPORT');
235
+ console.log('═'.repeat(60));
236
+ results.forEach(r => {
237
+ const icon = r.outcome === 'SUCCESS' ? 'βœ…' : r.outcome === 'ABANDONED' ? '😀' : '⏱️';
238
+ console.log(` ${icon} ${r.agent}: ${r.outcome} (${r.network})`);
239
+ });
240
+ console.log('═'.repeat(60));
241
+
242
+ } else {
243
+ // Single agent mode
244
+ const persona = getPersona(argv.agent);
245
+ if (!persona) { console.error(`❌ Unknown: ${argv.agent}`); process.exit(1); }
246
+
247
+ await runAgent(argv.url, persona, options);
248
+ }
249
+
250
+ console.log('\nβœ… All done!\n');
128
251
  }
129
252
 
130
253
  main().catch(e => console.error('πŸ’€', e));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@litocodes/persona-test",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "AI User Testing with Personality - Simulate real users breaking your website",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -39,4 +39,4 @@
39
39
  "openai": "^4.77.0",
40
40
  "playwright": "^1.49.1"
41
41
  }
42
- }
42
+ }
package/personas.js CHANGED
@@ -83,6 +83,29 @@ Creates a mental map of the site.`,
83
83
  goal: "Explore the entire site and understand its structure.",
84
84
  maxActions: 20,
85
85
  frustrationThreshold: 15
86
+ },
87
+
88
+ lagos: {
89
+ name: "Agent Lagos",
90
+ age: 28,
91
+ behavior: `Emerging market user on a $50 Android phone with unstable 3G.
92
+ DOUBLE-CLICKS everything because the first click "didn't work."
93
+ Refreshes aggressively if any spinner shows for >2 seconds.
94
+ Deeply suspicious of sites asking for personal info - "Is this a scam?"
95
+ Expects things to be SLOW but gets frustrated when they freeze completely.
96
+ Closes modals immediately - "I don't trust popups."
97
+ Looks for WhatsApp contact links instead of email forms.
98
+ Types slowly, one finger, makes typos.
99
+ If the page goes white/blank, assumes internet is down and quits.
100
+ Prefers simple text over fancy animations (they don't load).`,
101
+ goal: "Complete a basic task (signup/purchase) despite terrible network and device.",
102
+ maxActions: 12,
103
+ frustrationThreshold: 4,
104
+ testStrings: {
105
+ email: "user1234@gmail.com",
106
+ phone: "+234801234567",
107
+ name: "Chidi Okonkwo"
108
+ }
86
109
  }
87
110
  };
88
111
 
package/reporter.js ADDED
@@ -0,0 +1,92 @@
1
+ // reporter.js - AI-Generated QA Reports
2
+ // Uses Cerebras to write professional test reports from session data
3
+
4
+ import OpenAI from 'openai';
5
+ import dotenv from 'dotenv';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+
9
+ dotenv.config();
10
+
11
+ const openai = new OpenAI({
12
+ baseURL: 'https://api.cerebras.ai/v1',
13
+ apiKey: process.env.CEREBRAS_API_KEY
14
+ });
15
+
16
+ const MODEL = process.env.CEREBRAS_MODEL || 'llama-3.3-70b';
17
+
18
+ /**
19
+ * Generate a markdown report from test session data
20
+ */
21
+ export async function generateReport(agent, url, actionHistory, failedIds, outcome, network) {
22
+ console.log('\nπŸ“ Generating QA Report via Cerebras...');
23
+
24
+ const historyStr = actionHistory.length > 0
25
+ ? actionHistory.map((h, i) => `${i + 1}. ${h.action.toUpperCase()} [${h.id}] "${h.text}"`).join('\n')
26
+ : 'No actions recorded';
27
+
28
+ const prompt = `You are a Senior QA Engineer & UX Specialist.
29
+ You just watched an AI Agent test a website. Write a professional but personality-driven report.
30
+
31
+ --- AGENT PROFILE ---
32
+ Name: ${agent.name}
33
+ Age: ${agent.age}
34
+ Traits: ${agent.behavior}
35
+ Goal: ${agent.goal}
36
+
37
+ --- TEST SESSION ---
38
+ URL: ${url}
39
+ Network: ${network}
40
+ Outcome: ${outcome}
41
+ Failed Elements: ${failedIds.length > 0 ? failedIds.join(', ') : 'None'}
42
+
43
+ Action Log:
44
+ ${historyStr}
45
+
46
+ --- WRITE A MARKDOWN REPORT ---
47
+ Include:
48
+ 1. **Result**: Pass/Fail with emoji
49
+ 2. **Summary**: 2-3 sentences describing what happened
50
+ 3. **Friction Points**: Where did the agent struggle? (loops, failed clicks, confusion)
51
+ 4. **Recommendations**: 1-2 specific technical fixes (e.g., "Add loading state", "Fix aria-labels")
52
+ 5. **Persona Quote**: A funny one-liner in character (e.g., Zoomer: "ugh why is this so slow")
53
+
54
+ Keep it concise. Sound like the persona where appropriate.`;
55
+
56
+ try {
57
+ const response = await openai.chat.completions.create({
58
+ model: MODEL,
59
+ messages: [
60
+ { role: 'system', content: 'You are an expert QA reporter who writes concise, actionable test reports.' },
61
+ { role: 'user', content: prompt }
62
+ ],
63
+ max_tokens: 500,
64
+ temperature: 0.7
65
+ });
66
+
67
+ const reportText = response.choices[0]?.message?.content || 'Report generation failed';
68
+
69
+ // Save to file
70
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
71
+ const agentName = agent.name.replace(/\s+/g, '_');
72
+ const filename = `REPORT_${agentName}_${timestamp}.md`;
73
+
74
+ if (!fs.existsSync('reports')) fs.mkdirSync('reports');
75
+
76
+ const filepath = path.join('reports', filename);
77
+ fs.writeFileSync(filepath, `# QA Report: ${agent.name}\n\n${reportText}`);
78
+
79
+ console.log(`βœ… Report saved: ${filepath}`);
80
+ console.log(`\n${'─'.repeat(50)}`);
81
+ console.log(reportText.substring(0, 400) + (reportText.length > 400 ? '...' : ''));
82
+ console.log('─'.repeat(50));
83
+
84
+ return filepath;
85
+
86
+ } catch (error) {
87
+ console.error('❌ Report generation failed:', error.message);
88
+ return null;
89
+ }
90
+ }
91
+
92
+ export default { generateReport };
@@ -0,0 +1,8 @@
1
+ # QA Report: Agent Lagos
2
+
3
+ ### Test Report: Agent Lagos on Stripe.com
4
+ #### **Result**: 🚫 Fail
5
+ #### **Summary**: Agent Lagos attempted to complete a basic task on Stripe.com but encountered significant friction due to the site's behavior on a slow network and low-end device. The agent's interactions were marred by failed clicks, aggressive refreshing, and a general distrust of the site's requests for personal information. Despite persistence, the agent ultimately failed to complete the task.
6
+ #### **Friction Points**: The agent struggled with double-clicking on elements that didn't respond immediately, refreshing the page when spinners appeared for more than 2 seconds, and being suspicious of popups and personal info requests. Specifically, elements 2, 1, 182, and 15 caused issues.
7
+ #### **Recommendations**: To improve the user experience for agents like Lagos, I recommend adding a clear loading state to indicate when the site is processing requests, and fixing aria-labels to improve accessibility and reduce confusion.
8
+ #### **Persona Quote**: "Is this a scam? Why it no work?!"
@@ -0,0 +1,8 @@
1
+ # QA Report: Zoomer Zoe
2
+
3
+ ### Test Report: Zoomer Zoe on Hacker News
4
+ #### **Result**: 🚫 Fail
5
+ #### **Summary**: Zoomer Zoe attempted to login to Hacker News, but ended up in a password reset loop. The agent clicked "login", then "Forgot your password?", and finally "Send reset email" without completing the intended sign-up process. This behavior indicates frustration with the login process.
6
+ #### **Friction Points**: The agent struggled with the login form, ignoring instructions and getting stuck in a loop. The presence of a "Forgot your password?" link before even attempting to login suggests the agent was already looking for an escape route.
7
+ #### **Recommendations**: Add a clear "Sign up" or "Create account" button on the initial page to streamline the onboarding process. Consider simplifying the login form to reduce friction.
8
+ #### **Persona Quote**: Zoomer Zoe: "Ugh, can't I just use Google to login already?!"
package/.env DELETED
@@ -1,11 +0,0 @@
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