Capstone project. I'm ngl it's vibe-coded and it's only here so I can mess around with it
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #6 from chriskalos/feature/win-condition

Streamline win conditions

authored by

Chris and committed by
GitHub
a4ac1a35 414b1177

+66 -128
+3 -1
.gitignore
··· 2 2 .uxet-server.log 3 3 TODO.md 4 4 /test-results 5 - /tests 5 + /tests 6 + ANALYSIS.md 7 + /temp
+7 -4
README.md
··· 111 111 <option 112 112 value="testable-apps/your-app/index.html" 113 113 data-task="Describe the task for the user" 114 - data-win="selector:#success-element"> 114 + data-win="postMessage"> 115 115 Your App Name 116 116 </option> 117 117 ``` 118 118 119 - The `data-win` attribute defines when the test ends automatically. Supported formats: 119 + UXET now uses one standardized win condition: `postMessage`. When the task is complete, the app inside the iframe should send: 120 + 121 + ```js 122 + window.parent.postMessage({ type: 'UXET_TASK_COMPLETE' }, '*'); 123 + ``` 120 124 121 - - `selector:.some-class` — fires when the CSS selector matches a visible element. 122 - - `text:Some text` — fires when the specified text appears in the page. 125 + This keeps UXET app integration task-specific inside the app while giving UXET only one completion signal to listen for.
+3 -3
index.html
··· 95 95 <select id="app-select"> 96 96 <option value="">Select an app...</option> 97 97 <option value="testable-apps/shop-app/index.html" data-task="Find and purchase a blue t-shirt" 98 - data-win="selector:.checkout-success.active">THREAD Store</option> 98 + data-win="postMessage">THREAD Store</option> 99 99 <option value="testable-apps/example-app/index.html" 100 100 data-task="Fill out the feedback form with your details" 101 - data-win="text:Form submitted successfully">Feedback Terminal</option> 101 + data-win="postMessage">Feedback Terminal</option> 102 102 <option value="testable-apps/long-page-app/index.html" 103 103 data-task="Review the comparison sections and subscribe at the bottom of the page" 104 - data-win="selector:#success-banner">Signal Intelligence Report</option> 104 + data-win="postMessage">Signal Intelligence Report</option> 105 105 </select> 106 106 </label> 107 107
+15 -119
js/winConditions.js
··· 1 - class IntervalEvaluator { 2 - constructor(intervalMs, callback) { 3 - this.intervalMs = intervalMs; 4 - this.callback = callback; 5 - this.intervalId = null; 6 - } 7 - 8 - start(context) { 9 - this.stop(); 10 - this.intervalId = window.setInterval(() => { 11 - this.callback(context); 12 - }, this.intervalMs); 13 - } 14 - 15 - stop() { 16 - if (this.intervalId) { 17 - window.clearInterval(this.intervalId); 18 - this.intervalId = null; 19 - } 20 - } 21 - } 22 - 23 - function createSelectorEvaluator(selector) { 24 - return new IntervalEvaluator(250, ({ bridge, complete, session }) => { 25 - if (session.state !== 'recording') { 26 - return; 27 - } 28 - try { 29 - const doc = bridge.iframeDocument; 30 - const element = doc?.querySelector(selector); 31 - if (!element) { 32 - return; 33 - } 34 - const style = bridge.iframeWindow.getComputedStyle(element); 35 - const visible = style.display !== 'none' && 36 - style.visibility !== 'hidden' && 37 - parseFloat(style.opacity || '1') > 0 && 38 - element.offsetWidth > 0 && 39 - element.offsetHeight > 0; 40 - if (visible) { 41 - complete({ strategy: 'selector', selector }); 42 - } 43 - } catch (error) { 44 - console.warn('[WinCondition][selector]', error); 45 - } 46 - }); 47 - } 48 - 49 - function createTextEvaluator(text) { 50 - return new IntervalEvaluator(400, ({ bridge, complete, session }) => { 51 - if (session.state !== 'recording') { 52 - return; 53 - } 54 - try { 55 - const bodyText = bridge.iframeDocument?.body?.innerText || ''; 56 - if (bodyText.includes(text)) { 57 - complete({ strategy: 'text', matchedText: text }); 58 - } 59 - } catch (error) { 60 - console.warn('[WinCondition][text]', error); 61 - } 62 - }); 63 - } 64 - 65 - function createUrlEvaluator(pattern) { 66 - const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*'); 67 - const regex = new RegExp(`^${escaped}$`, 'i'); 68 - return new IntervalEvaluator(300, ({ bridge, complete, session }) => { 69 - if (session.state !== 'recording') { 70 - return; 71 - } 72 - try { 73 - const href = bridge.iframeWindow?.location?.href || ''; 74 - if (regex.test(href)) { 75 - complete({ strategy: 'url', url: href }); 76 - } 77 - } catch (error) { 78 - console.warn('[WinCondition][url]', error); 79 - } 80 - }); 81 - } 82 - 83 1 function createPostMessageEvaluator() { 84 2 return { 85 3 handler: null, 86 - start({ complete }) { 4 + start({ bridge, complete, session }) { 87 5 this.stop(); 88 6 this.handler = (event) => { 7 + if (session.state !== 'recording') { 8 + return; 9 + } 10 + if (event.source !== bridge.iframeWindow) { 11 + return; 12 + } 89 13 if (event.data?.type === 'UXET_TASK_COMPLETE') { 90 - complete({ strategy: 'postMessage' }); 14 + complete({ 15 + strategy: 'postMessage', 16 + payload: event.data?.payload || null 17 + }); 91 18 } 92 19 }; 93 20 window.addEventListener('message', this.handler); ··· 108 35 109 36 start(spec, context) { 110 37 this.stop(); 111 - if (!spec) { 112 - return; 38 + if (spec && spec !== 'postMessage') { 39 + console.warn('[WinCondition] Only postMessage win conditions are supported. Ignoring:', spec); 113 40 } 114 - 115 - const evaluator = this.createEvaluator(spec); 116 - if (!evaluator) { 117 - console.warn('[WinCondition] Unknown win condition:', spec); 118 - return; 119 - } 120 - 121 - this.activeEvaluator = evaluator; 122 - evaluator.start(context); 41 + this.activeEvaluator = createPostMessageEvaluator(); 42 + this.activeEvaluator.start(context); 123 43 } 124 44 125 45 stop() { ··· 129 49 } 130 50 } 131 51 132 - createEvaluator(spec) { 133 - if (spec === 'postMessage') { 134 - return createPostMessageEvaluator(); 135 - } 136 - 137 - const separatorIndex = spec.indexOf(':'); 138 - if (separatorIndex === -1) { 139 - return null; 140 - } 141 - 142 - const strategy = spec.slice(0, separatorIndex); 143 - const value = spec.slice(separatorIndex + 1); 144 - 145 - switch (strategy) { 146 - case 'selector': 147 - return createSelectorEvaluator(value); 148 - case 'text': 149 - return createTextEvaluator(value); 150 - case 'url': 151 - return createUrlEvaluator(value); 152 - default: 153 - return null; 154 - } 155 - } 156 52 }
+12 -1
testable-apps/example-app/index.html
··· 541 541 </div> 542 542 543 543 <script> 544 + function notifyUxetTaskComplete(payload = {}) { 545 + window.parent.postMessage({ 546 + type: 'UXET_TASK_COMPLETE', 547 + payload 548 + }, '*'); 549 + } 550 + 544 551 // Counter 545 552 let count = 0; 546 553 const counterValue = document.getElementById('counter-value'); ··· 574 581 let banner = document.getElementById('success-banner'); 575 582 banner.textContent = 'Form submitted successfully!'; 576 583 banner.style.display = 'block'; 584 + notifyUxetTaskComplete({ 585 + app: 'example-app', 586 + condition: 'feedback-form-submitted' 587 + }); 577 588 setTimeout(() => { 578 589 banner.style.display = 'none'; 579 590 }, 4000); ··· 581 592 </script> 582 593 </body> 583 594 584 - </html> 595 + </html>
+11
testable-apps/long-page-app/index.html
··· 571 571 </main> 572 572 573 573 <script> 574 + function notifyUxetTaskComplete(payload = {}) { 575 + window.parent.postMessage({ 576 + type: 'UXET_TASK_COMPLETE', 577 + payload 578 + }, '*'); 579 + } 580 + 574 581 // Scroll progress indicator 575 582 const progressBar = document.getElementById('scroll-progress'); 576 583 window.addEventListener('scroll', () => { ··· 612 619 document.getElementById('subscribe-form').addEventListener('submit', (event) => { 613 620 event.preventDefault(); 614 621 document.getElementById('success-banner').style.display = 'block'; 622 + notifyUxetTaskComplete({ 623 + app: 'long-page-app', 624 + condition: 'subscribed-to-report' 625 + }); 615 626 }); 616 627 </script> 617 628 </body>
+15
testable-apps/shop-app/index.html
··· 1054 1054 const cartTotal = document.getElementById('cart-total'); 1055 1055 const checkoutSuccess = document.getElementById('checkout-success'); 1056 1056 1057 + function notifyUxetTaskComplete(payload = {}) { 1058 + window.parent.postMessage({ 1059 + type: 'UXET_TASK_COMPLETE', 1060 + payload 1061 + }, '*'); 1062 + } 1063 + 1057 1064 function getSwatchStyle(color) { 1058 1065 const swatch = colorSwatches[color] || { bg: 'linear-gradient(135deg, #f5f5f5, #e5e5e5)', text: '#888' }; 1059 1066 return swatch; ··· 1223 1230 alert('Your bag is empty!'); 1224 1231 return; 1225 1232 } 1233 + const checkedOutCorrectShirt = cart.some(item => item.product.id === 1); 1226 1234 closeCart(); 1227 1235 checkoutSuccess.classList.add('active'); 1228 1236 cart = []; 1229 1237 updateCart(); 1238 + if (checkedOutCorrectShirt) { 1239 + notifyUxetTaskComplete({ 1240 + app: 'shop-app', 1241 + condition: 'checked-out-blue-classic-t-shirt', 1242 + productId: 1 1243 + }); 1244 + } 1230 1245 }); 1231 1246 1232 1247 document.getElementById('continue-btn').addEventListener('click', () => {