···11+# UXET — UX Eye-Tracking Testing Framework
22+33+UXET is a browser-based UX testing framework with integrated **WebGazer.js** eye tracking. Load any web app into a sandboxed iframe, define a task and a win condition, and UXET records mouse, keyboard, scroll, and gaze data — generating a heatmap when the test completes.
44+55+---
66+77+## Quick Start
88+99+```bash
1010+# Serve the project root (any static server works)
1111+npx http-server . -p 8080
1212+1313+# Open http://localhost:8080
1414+```
1515+1616+1. Select an app from the dropdown
1717+2. Click **Load**
1818+3. Complete the 9-point eye-tracking calibration (or skip it with the invisible debug button at the top-right corner)
1919+4. Read the task briefing, then click **Begin Testing**
2020+5. Interact with the app — the test auto-stops when the **win condition** is met
2121+6. View the debrief screen with stats and a gaze heatmap
2222+2323+---
2424+2525+## Adding a Test App
2626+2727+### 1. Create the app
2828+2929+Place your app inside `testable-apps/`:
3030+3131+```
3232+testable-apps/
3333+ my-app/
3434+ index.html
3535+```
3636+3737+Your app is a normal HTML page. **No UXET-specific code is required.**
3838+3939+### 2. Register it in the dropdown
4040+4141+Open `index.html` and add a `<div class="dropdown-item">` inside `#dropdown-menu`:
4242+4343+```html
4444+<div class="dropdown-item"
4545+ data-value="testable-apps/my-app/index.html"
4646+ data-task="Complete the signup flow"
4747+ data-win="selector:.signup-success">
4848+ <span class="item-name">My App</span>
4949+ <span class="item-task">Sign up</span>
5050+</div>
5151+```
5252+5353+| Attribute | Description |
5454+|---|---|
5555+| `data-value` | Path to the app's entry HTML file |
5656+| `data-task` | Task description shown to the test participant |
5757+| `data-win` | Win condition that ends the test (see below) |
5858+5959+---
6060+6161+## Win Conditions
6262+6363+Win conditions define **when the test automatically stops**. They are evaluated externally by UXET — the test app does not need any UXET-specific code.
6464+6565+### Syntax
6666+6767+```
6868+data-win="strategy:value"
6969+```
7070+7171+### Available Strategies
7272+7373+| Strategy | Syntax | What it detects |
7474+|---|---|---|
7575+| `selector` | `selector:<CSS selector>` | Element exists **and is visible** in the iframe |
7676+| `text` | `text:<substring>` | Substring appears in the iframe's visible text |
7777+| `url` | `url:<glob pattern>` | Iframe URL matches a glob pattern (`*` = wildcard) |
7878+| `postMessage` | `postMessage` | Iframe sends `{ type: 'UXET_TASK_COMPLETE' }` via `postMessage` |
7979+8080+### Examples
8181+8282+```html
8383+<!-- Fires when .checkout-success becomes visible -->
8484+data-win="selector:.checkout-success.active"
8585+8686+<!-- Fires when "Order confirmed" appears on the page -->
8787+data-win="text:Order confirmed"
8888+8989+<!-- Fires when the iframe navigates to a /success URL -->
9090+data-win="url:*/success*"
9191+9292+<!-- Legacy: app explicitly signals completion -->
9393+data-win="postMessage"
9494+```
9595+9696+### Strategy Details
9797+9898+**`selector:`** — Polls every 300ms for the CSS selector inside the iframe DOM. The element must be visible (not `display: none`, `visibility: hidden`, or `opacity: 0`). Best for single-page apps where a success state is reflected by a DOM change.
9999+100100+**`text:`** — Polls every 500ms for a substring match in `document.body.innerText`. Best for detecting success messages, confirmation text, or any visible string.
101101+102102+**`url:`** — Polls every 500ms. The glob pattern uses `*` as a wildcard. Best for multi-page flows where success means navigating to a specific URL.
103103+104104+**`postMessage`** — Listens for `window.postMessage({ type: 'UXET_TASK_COMPLETE' })` from the iframe. The only strategy that works with **cross-origin** iframes.
105105+106106+> **Cross-origin note:** `selector:` and `text:` require same-origin iframe access. If the iframe is cross-origin, use `url:` or `postMessage` instead. UXET will log a warning to the console if a DOM-based strategy fails due to CORS restrictions.
107107+108108+---
109109+110110+## Test Flow
111111+112112+```
113113+Load App → Calibration → Task Briefing → Testing → Debrief + Heatmap
114114+```
115115+116116+1. **Load**: App is loaded into the iframe; WebGazer initializes the webcam (hidden)
117117+2. **Calibration**: 9-point gaze calibration grid. Click each point 5 times. An advisory card explains the process first.
118118+3. **Task Briefing**: Shows the task description (`data-task`). Participant clicks "Begin Testing" when ready.
119119+4. **Testing**: Participant interacts with the app. Mouse, keyboard, scroll, and gaze events are recorded. The win condition watcher runs in the background.
120120+5. **Debrief**: Shows session stats (time, clicks, keystrokes, scroll events, gaze points, fixations) and a rendered gaze heatmap.
121121+122122+---
123123+124124+## File Structure
125125+126126+```
127127+UXET/
128128+├── index.html # Main UXET shell (dropdown, calibration, briefing, debrief)
129129+├── index.css # All styles
130130+├── js/
131131+│ ├── main.js # App controller — flow, win conditions, heatmap
132132+│ ├── tracker.js # Mouse, keyboard, scroll event tracking
133133+│ ├── session.js # Session timer and state management
134134+│ └── gazeTracker.js # WebGazer.js wrapper — gaze data collection
135135+├── testable-apps/
136136+│ ├── shop-app/
137137+│ │ └── index.html # ShopEasy store demo (win: checkout success)
138138+│ └── example-app/
139139+│ └── index.html # Form demo (win: form submitted)
140140+└── README.md
141141+```
142142+143143+### Key Modules
144144+145145+| Module | Responsibility |
146146+|---|---|
147147+| `main.js` (`UXETApp`) | Orchestrates the entire flow: app loading, calibration, testing, win conditions, heatmap rendering, data export |
148148+| `tracker.js` (`Tracker`) | Attaches to the iframe and records mouse position/clicks/distance, keyboard events, and scroll events |
149149+| `session.js` (`Session`) | Manages session lifecycle (start/stop/reset) and elapsed time |
150150+| `gazeTracker.js` (`GazeTracker`) | Initializes WebGazer.js, runs calibration, collects gaze coordinates mapped to iframe-relative positions, detects fixations |
151151+152152+---
153153+154154+## Heatmap
155155+156156+When a test completes, UXET renders a gaze density heatmap on a `<canvas>` in the debrief screen:
157157+158158+- Each gaze point is drawn as a **Gaussian splat** (radial gradient)
159159+- Intensity is normalized and colorized: **blue → cyan → green → yellow → red**
160160+- Only gaze points that fell within the iframe are included
161161+- A reference grid is drawn for spatial context
162162+163163+---
164164+165165+## Debug Skip
166166+167167+For automated testing, an invisible 20×20px button at the top-right corner of the calibration overlay bypasses calibration entirely. It has `id="debug-skip-calibration"` with `opacity: 0`.
168168+169169+---
170170+171171+## Data Export
172172+173173+Click **Export Data** on the debrief screen to download a JSON file containing:
174174+175175+- Session metadata (app, task, duration)
176176+- All tracked events (mouse, keyboard, scroll)
177177+- Raw gaze data points with iframe-relative coordinates
178178+- Per-screen heatmap data
179179+- Aggregate statistics
180180+181181+---
182182+183183+## Dependencies
184184+185185+- [WebGazer.js](https://webgazer.cs.brown.edu/) — loaded from jsDelivr CDN
186186+- [MediaPipe Face Mesh](https://google.github.io/mediapipe/) — loaded internally by WebGazer from jsDelivr CDN
187187+188188+No build step, no npm install. Just serve and open.