@litocodes/persona-test 1.0.0 β†’ 1.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 CHANGED
@@ -1,159 +1,139 @@
1
- # 🎭 Persona - AI-Powered User Testing Tool
1
+ # 🎭 Persona
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
+ > **Automated User Testing with Personality.**
4
4
 
5
- ## πŸ—οΈ Architecture
5
+ Most E2E tests are brittle and boring. They check if a button *works*.
6
+ **Persona** checks if a button is *frustrating*.
6
7
 
8
+ It spawns AI Agents with distinct psychological profiles to use your app β€” and records everything on video.
9
+
10
+ [![npm version](https://badge.fury.io/js/@litocodes%2Fpersona-test.svg)](https://www.npmjs.com/package/@litocodes/persona-test)
11
+
12
+ ---
13
+
14
+ ## πŸš€ The Agents
15
+
16
+ | Agent | Personality | Goal |
17
+ |-------|-------------|------|
18
+ | πŸ‘΄ **Grandpa Joe** | 70yo, low tech literacy, confused by modern UI | Find a phone number to call |
19
+ | 🏎️ **Zoomer Zoe** | 20yo, impatient, rage-clicks, ignores instructions | Sign up in under 5 clicks |
20
+ | πŸ•΅οΈ **Skeptical Sam** | 40yo, paranoid about privacy, reads fine print | Find and read the Privacy Policy |
21
+ | πŸ’€ **Hacker Harry** | Security researcher, injects SQL/XSS payloads | Test all input fields for vulnerabilities |
22
+ | 🧭 **Explorer Emma** | Curious, methodical, maps the entire site | Understand the site structure |
23
+
24
+ ---
25
+
26
+ ## πŸ“¦ Quick Start
27
+
28
+ ```bash
29
+ # Run instantly with npx
30
+ npx @litocodes/persona-test --url="https://your-site.com" --agent="zoomer"
31
+
32
+ # Or install globally
33
+ npm install -g @litocodes/persona-test
34
+ persona --url="https://example.com" --agent="hacker"
7
35
  ```
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
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
36
+
37
+ ### Requirements
38
+ - Node.js 18+
39
+ - Cerebras API key (free): https://cloud.cerebras.ai/
40
+
41
+ ```bash
42
+ # Set your API key
43
+ export CEREBRAS_API_KEY=csk-xxxxx
21
44
  ```
22
45
 
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
46
+ ---
27
47
 
28
- ## πŸ“¦ Installation
48
+ ## πŸŽ₯ Video Recording
29
49
 
30
- ```bash
31
- # Clone or create the project
32
- cd Persona
50
+ Every session is automatically recorded to `videos/` folder:
33
51
 
34
- # Install dependencies
35
- npm install
52
+ ```
53
+ videos/
54
+ β”œβ”€β”€ HACKER_2026-01-25T23-10-00_SUCCESS.webm
55
+ β”œβ”€β”€ ZOOMER_2026-01-25T23-15-00_ABANDONED.webm
56
+ └── BOOMER_2026-01-25T23-20-00_MAX_ACTIONS.webm
57
+ ```
58
+
59
+ **Pink highlight boxes** flash around elements before the AI clicks them β€” making the decision-making process visible.
60
+
61
+ ---
36
62
 
37
- # Install Playwright browsers
38
- npx playwright install chromium
63
+ ## 🧠 How It Works
64
+
65
+ ```
66
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
67
+ β”‚ Parser β”‚ ──▢ β”‚ Brain β”‚ ──▢ β”‚ Hand β”‚
68
+ β”‚ (Cheerio) β”‚ β”‚ (Cerebras) β”‚ β”‚(Playwright) β”‚
69
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
70
+ β”‚ β”‚ β”‚
71
+ β–Ό β–Ό β–Ό
72
+ DOM β†’ Text Decides: Executes:
73
+ [1] BUTTON "Click [3]" page.click()
74
+ [2] INPUT "Type XSS" page.fill()
75
+ [3] LINK "Quit"
39
76
  ```
40
77
 
41
- ## βš™οΈ Configuration
78
+ 1. **Parser** converts HTML into a numbered element list
79
+ 2. **Brain** uses Cerebras AI (Llama 3.3 70B) to decide actions based on persona psychology
80
+ 3. **Hand** uses Playwright to execute actions with human-like behavior
81
+
82
+ ### Self-Healing
83
+ - 🚫 **Blacklist**: Failed elements are marked `[BROKEN]` and avoided
84
+ - 🧠 **Memory**: Last 3 actions tracked to prevent loops
85
+ - πŸ“ **Form Logic**: AI knows to type before clicking submit
86
+
87
+ ---
42
88
 
43
- Create a `.env` file with your OpenRouter API key:
89
+ ## πŸ“‹ CLI Options
44
90
 
45
- ```env
46
- OPENAI_API_KEY=your-openrouter-api-key-here
47
- OPENROUTER_MODEL=google/gemini-2.0-flash-exp:free
91
+ ```bash
92
+ persona --url="<URL>" --agent="<AGENT>" [options]
93
+
94
+ Options:
95
+ --url, -u Target website URL (required)
96
+ --agent, -a Persona: boomer, zoomer, skeptic, hacker, explorer
97
+ --record, -r Record video (default: true)
98
+ --headless Run without visible browser
99
+ --test Test API connection
100
+ --help, -h Show help
48
101
  ```
49
102
 
50
- Get your API key from: https://openrouter.ai/keys
103
+ ---
51
104
 
52
- ## πŸš€ Usage
105
+ ## πŸ› οΈ Development
53
106
 
54
107
  ```bash
55
- # Basic usage
56
- node index.js --url="https://example.com" --agent="zoomer"
108
+ # Clone
109
+ git clone https://github.com/litocodes/persona-test
110
+ cd persona-test
57
111
 
58
- # With options
59
- node index.js --url="https://google.com" --agent="boomer" --headless
112
+ # Install dependencies
113
+ npm install
60
114
 
61
- # Test API connection
62
- node index.js --test
115
+ # Set API key
116
+ echo "CEREBRAS_API_KEY=your-key" > .env
63
117
 
64
- # Show help
65
- node index.js --help
118
+ # Run locally
119
+ node index.js --url="https://example.com" --agent="zoomer"
66
120
  ```
67
121
 
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
122
+ ---
131
123
 
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
- ```
124
+ ## 🎯 Use Cases
141
125
 
142
- ## πŸ› Troubleshooting
126
+ - **QA Testing**: Find UX issues before users do
127
+ - **Security Audits**: Hacker Harry tests for SQL injection, XSS
128
+ - **Accessibility**: See how confused users navigate your site
129
+ - **Conversion Optimization**: Watch where Zoomers rage-quit your signup flow
143
130
 
144
- **"OPENAI_API_KEY not set"**
145
- - Create a `.env` file with your OpenRouter API key
131
+ ---
146
132
 
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
133
+ ## πŸ“œ License
151
134
 
152
- **"Connection failed"**
153
- - Check your API key
154
- - Run `node index.js --test` to verify connection
155
- - Check OpenRouter status
135
+ MIT Β© 2026
156
136
 
157
- ## πŸ“„ License
137
+ ---
158
138
 
159
- MIT
139
+ **Built in one night. Ships value.** πŸš€
package/index.js CHANGED
@@ -1,130 +1,232 @@
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, // 4 Mbps
23
+ uploadThroughput: 3 * 1024 * 1024 / 8, // 3 Mbps
24
+ latency: 20
25
+ },
26
+ '3g': {
27
+ offline: false,
28
+ downloadThroughput: 1.5 * 1024 * 1024 / 8, // 1.5 Mbps
29
+ uploadThroughput: 750 * 1024 / 8, // 750 Kbps
30
+ latency: 100
31
+ },
32
+ 'lagos-3g': { // Glo Nigeria 3G simulation
33
+ offline: false,
34
+ downloadThroughput: 400 * 1024 / 8, // 400 Kbps (realistic)
35
+ uploadThroughput: 150 * 1024 / 8, // 150 Kbps
36
+ latency: 400 // High latency
37
+ },
38
+ 'edge': { // 2G/Edge
39
+ offline: false,
40
+ downloadThroughput: 50 * 1024 / 8, // 50 Kbps
41
+ uploadThroughput: 20 * 1024 / 8, // 20 Kbps
42
+ latency: 800
43
+ }
44
+ };
45
+
14
46
  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' }
47
+ string: ['url', 'agent', 'network'],
48
+ boolean: ['headless', 'test', 'help', 'record', 'all'],
49
+ default: { headless: false, record: true, network: 'wifi' },
50
+ alias: { u: 'url', a: 'agent', h: 'help', r: 'record', n: 'network' }
19
51
  });
20
52
 
21
53
  if (argv.help) {
22
54
  console.log(`
23
- 🎭 PERSONA - AI User Testing (Final Version)
24
-
25
- Usage: node index.js --url="<site>" --agent="<persona>"
26
- Personas: ${listPersonas().join(', ')}
55
+ 🎭 PERSONA - AI User Testing (Parallel + Network)
56
+
57
+ Usage:
58
+ persona --url="<site>" --agent="<persona>"
59
+ persona --url="<site>" --all # Run ALL agents in parallel!
60
+
61
+ Options:
62
+ --url, -u Target URL (required)
63
+ --agent, -a Single persona: ${listPersonas().join(', ')}
64
+ --all Run ALL personas in parallel (Mission Control mode)
65
+ --network, -n Network: wifi, 4g, 3g, lagos-3g, edge
66
+ --record, -r Record video (default: true)
67
+ --headless Run headless
68
+ --test Test API connection
69
+
70
+ Examples:
71
+ persona --url="https://example.com" --agent="zoomer" --network="lagos-3g"
72
+ persona --url="https://example.com" --all # 5 browsers at once!
27
73
  `);
28
74
  process.exit(0);
29
75
  }
30
76
 
31
77
  if (argv.test) { await testConnection(); process.exit(0); }
32
- if (!argv.url || !argv.agent) { console.error('❌ --url and --agent required'); process.exit(1); }
78
+ if (!argv.url) { console.error('❌ --url required'); process.exit(1); }
79
+ if (!argv.agent && !argv.all) { console.error('❌ --agent or --all required'); process.exit(1); }
33
80
 
34
- const persona = getPersona(argv.agent);
35
- if (!persona) { console.error(`❌ Unknown: ${argv.agent}`); process.exit(1); }
81
+ // Ensure folders exist
82
+ if (!fs.existsSync('videos')) fs.mkdirSync('videos');
36
83
 
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`);
84
+ /**
85
+ * Run a single agent session
86
+ */
87
+ async function runAgent(url, persona, options = {}) {
88
+ const { network = 'wifi', record = true, headless = false } = options;
89
+ const networkConfig = NETWORK_PRESETS[network];
45
90
 
46
- const browser = await chromium.launch({ headless: argv.headless });
47
- const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
91
+ console.log(`\nπŸš€ [${persona.name}] Starting... (Network: ${network})`);
48
92
 
49
- await page.goto(argv.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
50
- await page.waitForTimeout(2000);
51
- console.log('βœ… Page loaded\n');
93
+ const browser = await chromium.launch({ headless });
52
94
 
53
- // 🧠 MEMORY STATE
54
- const actionHistory = []; // Last 3 actions: [{action, id, text}]
55
- const failedIds = []; // Blacklisted IDs
95
+ const contextOptions = {
96
+ viewport: { width: 1280, height: 800 },
97
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0'
98
+ };
56
99
 
57
- for (let step = 1; step <= persona.maxActions; step++) {
58
- console.log(`β”Œβ”€ Step ${step}/${persona.maxActions} ${'─'.repeat(44)}`);
100
+ if (record) {
101
+ contextOptions.recordVideo = { dir: 'videos/', size: { width: 1280, height: 800 } };
102
+ }
59
103
 
104
+ const context = await browser.newContext(contextOptions);
105
+ const page = await context.newPage();
106
+
107
+ // 🌐 Apply network throttling via CDP
108
+ if (networkConfig) {
109
+ const client = await context.newCDPSession(page);
110
+ await client.send('Network.emulateNetworkConditions', networkConfig);
111
+ console.log(` πŸ“Ά [${persona.name}] Network throttled to ${network}`);
112
+ }
113
+
114
+ try {
115
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
116
+ await page.waitForTimeout(2000);
117
+ } catch (e) {
118
+ console.log(` ❌ [${persona.name}] Failed to load: ${e.message.substring(0, 50)}`);
119
+ await browser.close();
120
+ return { agent: persona.name, outcome: 'LOAD_FAILED' };
121
+ }
122
+
123
+ const actionHistory = [];
124
+ const failedIds = [];
125
+ let outcome = 'MAX_ACTIONS';
126
+
127
+ for (let step = 1; step <= persona.maxActions; step++) {
60
128
  const html = await page.content();
61
129
  const { domMap, elements, elementCount } = parsePage(html, failedIds);
62
- console.log(` πŸ“Š ${elementCount} elements (${failedIds.length} blacklisted)`);
63
130
 
64
- if (elementCount === 0) {
65
- console.log(' ⚠️ No elements');
66
- await page.mouse.wheel(0, 300);
67
- continue;
68
- }
131
+ if (elementCount === 0) { await page.mouse.wheel(0, 300); continue; }
69
132
 
70
- // Pass full history to brain
71
133
  const decision = await getAgentAction(domMap, persona, actionHistory);
72
134
 
73
- console.log(` πŸ€– ${decision.action.toUpperCase()} [${decision.elementId}]`);
74
- console.log(` πŸ’­ ${decision.reason}`);
75
- console.log(` 😀 Frustration: ${decision.frustration}/10`);
135
+ console.log(` πŸ€– [${persona.name}] Step ${step}: ${decision.action} [${decision.elementId}] - ${decision.reason?.substring(0, 40)}`);
76
136
 
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
- }
137
+ if (decision.action === 'done') { outcome = 'SUCCESS'; break; }
138
+ if (decision.action === 'quit') { outcome = 'ABANDONED'; break; }
84
139
 
85
140
  const target = decision.elementId ? findElementById(elements, decision.elementId) : null;
86
-
87
- if (!target) {
88
- console.log(' ❌ Invalid ID');
89
- continue;
90
- }
141
+ if (!target) continue;
91
142
 
92
143
  try {
93
144
  const locator = page.locator(target.selector).first();
145
+ await locator.highlight();
146
+ await page.waitForTimeout(200);
94
147
 
95
148
  if (decision.action === 'click') {
96
149
  await locator.click({ timeout: 4000 });
97
- console.log(` πŸ–±οΈ Clicked: "${target.text}"`);
98
150
  } 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');
151
+ await locator.fill(decision.inputText || persona.testStrings?.sql || 'test', { timeout: 4000 });
105
152
  }
106
153
 
107
- // βœ… Add to history (keep last 3)
108
154
  actionHistory.push({ action: decision.action, id: decision.elementId, text: target.text });
109
155
  if (actionHistory.length > 3) actionHistory.shift();
110
156
 
111
157
  } catch (e) {
112
- const msg = e.message.split('\n')[0].substring(0, 50);
113
- console.log(` ⚠️ FAILED: ${msg}`);
114
-
115
- // 🚨 Add to blacklist
116
158
  failedIds.push(decision.elementId);
117
- console.log(` 🚫 ID [${decision.elementId}] blacklisted`);
118
159
  }
119
160
 
120
- await page.waitForTimeout(2000);
121
- console.log(`β””${'─'.repeat(58)}β”˜`);
161
+ await page.waitForTimeout(1500);
122
162
  }
123
163
 
124
- console.log('\nπŸ“‹ History:', actionHistory.map(h => `${h.action}[${h.id}]`).join(' β†’ '));
125
- console.log('🚫 Blacklist:', failedIds.length ? failedIds.join(', ') : 'none');
164
+ // Save video
165
+ const videoPath = record ? await page.video()?.path() : null;
166
+ await context.close();
126
167
  await browser.close();
127
- console.log('βœ… Done\n');
168
+
169
+ if (record && videoPath && fs.existsSync(videoPath)) {
170
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
171
+ const filename = `${persona.name.replace(/\s+/g, '_')}_${network}_${timestamp}_${outcome}.webm`;
172
+ fs.renameSync(videoPath, path.join('videos', filename));
173
+ console.log(` πŸŽ₯ [${persona.name}] Saved: videos/${filename}`);
174
+ }
175
+
176
+ // πŸ“ Generate QA Report
177
+ await generateReport(persona, url, actionHistory, failedIds, outcome, network);
178
+
179
+ console.log(` βœ… [${persona.name}] Finished: ${outcome}`);
180
+ return { agent: persona.name, outcome, network };
181
+ }
182
+
183
+ /**
184
+ * Main entry point
185
+ */
186
+ async function main() {
187
+ console.log(`
188
+ ╔══════════════════════════════════════════════════════════════╗
189
+ β•‘ 🎭 PERSONA - AI User Testing (Parallel + Network) β•‘
190
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
191
+ `);
192
+ console.log(`🎯 Target: ${argv.url}`);
193
+ console.log(`πŸ“Ά Network: ${argv.network}`);
194
+
195
+ const options = {
196
+ network: argv.network,
197
+ record: argv.record,
198
+ headless: argv.headless
199
+ };
200
+
201
+ if (argv.all) {
202
+ // πŸš€ PARALLEL MODE: Run ALL agents at once!
203
+ console.log(`\nπŸ”₯ MISSION CONTROL: Launching ALL ${listPersonas().length} agents in parallel!\n`);
204
+
205
+ const agents = listPersonas().map(name => getPersona(name));
206
+
207
+ const results = await Promise.all(
208
+ agents.map(persona => runAgent(argv.url, persona, options))
209
+ );
210
+
211
+ // Summary
212
+ console.log(`\n${'═'.repeat(60)}`);
213
+ console.log('πŸ“Š MISSION REPORT');
214
+ console.log('═'.repeat(60));
215
+ results.forEach(r => {
216
+ const icon = r.outcome === 'SUCCESS' ? 'βœ…' : r.outcome === 'ABANDONED' ? '😀' : '⏱️';
217
+ console.log(` ${icon} ${r.agent}: ${r.outcome} (${r.network})`);
218
+ });
219
+ console.log('═'.repeat(60));
220
+
221
+ } else {
222
+ // Single agent mode
223
+ const persona = getPersona(argv.agent);
224
+ if (!persona) { console.error(`❌ Unknown: ${argv.agent}`); process.exit(1); }
225
+
226
+ await runAgent(argv.url, persona, options);
227
+ }
228
+
229
+ console.log('\nβœ… All done!\n');
128
230
  }
129
231
 
130
232
  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.1.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/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: 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