@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 +108 -128
- package/index.js +174 -72
- package/package.json +2 -2
- package/reporter.js +92 -0
- package/reports/REPORT_Zoomer_Zoe_2026-01-25T22-25-42.md +8 -0
- package/.env +0 -11
package/README.md
CHANGED
|
@@ -1,159 +1,139 @@
|
|
|
1
|
-
# π Persona
|
|
1
|
+
# π Persona
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Automated User Testing with Personality.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
+
[](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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
48
|
+
## π₯ Video Recording
|
|
29
49
|
|
|
30
|
-
|
|
31
|
-
# Clone or create the project
|
|
32
|
-
cd Persona
|
|
50
|
+
Every session is automatically recorded to `videos/` folder:
|
|
33
51
|
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
+
## π CLI Options
|
|
44
90
|
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
103
|
+
---
|
|
51
104
|
|
|
52
|
-
##
|
|
105
|
+
## π οΈ Development
|
|
53
106
|
|
|
54
107
|
```bash
|
|
55
|
-
#
|
|
56
|
-
|
|
108
|
+
# Clone
|
|
109
|
+
git clone https://github.com/litocodes/persona-test
|
|
110
|
+
cd persona-test
|
|
57
111
|
|
|
58
|
-
#
|
|
59
|
-
|
|
112
|
+
# Install dependencies
|
|
113
|
+
npm install
|
|
60
114
|
|
|
61
|
-
#
|
|
62
|
-
|
|
115
|
+
# Set API key
|
|
116
|
+
echo "CEREBRAS_API_KEY=your-key" > .env
|
|
63
117
|
|
|
64
|
-
#
|
|
65
|
-
node index.js --
|
|
118
|
+
# Run locally
|
|
119
|
+
node index.js --url="https://example.com" --agent="zoomer"
|
|
66
120
|
```
|
|
67
121
|
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
- Create a `.env` file with your OpenRouter API key
|
|
131
|
+
---
|
|
146
132
|
|
|
147
|
-
|
|
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
|
-
|
|
153
|
-
- Check your API key
|
|
154
|
-
- Run `node index.js --test` to verify connection
|
|
155
|
-
- Check OpenRouter status
|
|
135
|
+
MIT Β© 2026
|
|
156
136
|
|
|
157
|
-
|
|
137
|
+
---
|
|
158
138
|
|
|
159
|
-
|
|
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
|
|
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 (
|
|
24
|
-
|
|
25
|
-
Usage:
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
35
|
-
if (!
|
|
81
|
+
// Ensure folders exist
|
|
82
|
+
if (!fs.existsSync('videos')) fs.mkdirSync('videos');
|
|
36
83
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
47
|
-
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
|
|
91
|
+
console.log(`\nπ [${persona.name}] Starting... (Network: ${network})`);
|
|
48
92
|
|
|
49
|
-
await
|
|
50
|
-
await page.waitForTimeout(2000);
|
|
51
|
-
console.log('β
Page loaded\n');
|
|
93
|
+
const browser = await chromium.launch({ headless });
|
|
52
94
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
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'
|
|
78
|
-
|
|
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
|
-
|
|
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(
|
|
121
|
-
console.log(`β${'β'.repeat(58)}β`);
|
|
161
|
+
await page.waitForTimeout(1500);
|
|
122
162
|
}
|
|
123
163
|
|
|
124
|
-
|
|
125
|
-
|
|
164
|
+
// Save video
|
|
165
|
+
const videoPath = record ? await page.video()?.path() : null;
|
|
166
|
+
await context.close();
|
|
126
167
|
await browser.close();
|
|
127
|
-
|
|
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.
|
|
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
|