react-native-ai-debugger 1.0.8 → 1.0.10
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 +31 -0
- package/build/core/android.d.ts +19 -0
- package/build/core/android.d.ts.map +1 -1
- package/build/core/android.js +98 -1
- package/build/core/android.js.map +1 -1
- package/build/core/executor.d.ts.map +1 -1
- package/build/core/executor.js +6 -1
- package/build/core/executor.js.map +1 -1
- package/build/core/httpServer.d.ts.map +1 -1
- package/build/core/httpServer.js +695 -5
- package/build/core/httpServer.js.map +1 -1
- package/build/core/index.d.ts +2 -1
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +3 -1
- package/build/core/index.js.map +1 -1
- package/build/core/ios.d.ts +1 -0
- package/build/core/ios.d.ts.map +1 -1
- package/build/core/ios.js +4 -2
- package/build/core/ios.js.map +1 -1
- package/build/core/telemetry.d.ts.map +1 -1
- package/build/core/telemetry.js +19 -8
- package/build/core/telemetry.js.map +1 -1
- package/build/index.js +152 -50
- package/build/index.js.map +1 -1
- package/package.json +1 -1
package/build/core/httpServer.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { createServer } from "http";
|
|
2
2
|
import { logBuffer, networkBuffer, bundleErrorBuffer, connectedApps } from "./state.js";
|
|
3
|
+
import { listAndroidDevices, androidScreenshot, androidGetScreenSize, androidTap } from "./android.js";
|
|
4
|
+
import { listIOSSimulators, iosScreenshot, iosTap } from "./ios.js";
|
|
3
5
|
const DEFAULT_HTTP_PORT = 3456;
|
|
4
6
|
const MAX_PORT_ATTEMPTS = 20;
|
|
5
7
|
// Store the active port for querying via MCP tool
|
|
6
8
|
let activeDebugServerPort = null;
|
|
9
|
+
let tapVerifierMarkers = [];
|
|
7
10
|
/**
|
|
8
11
|
* Get the port the debug HTTP server is running on (if started)
|
|
9
12
|
*/
|
|
@@ -178,6 +181,7 @@ function htmlTemplate(title, content, refreshInterval = 3000) {
|
|
|
178
181
|
<a href="/logs" ${title === 'Logs' ? 'class="active"' : ''}>Logs</a>
|
|
179
182
|
<a href="/network" ${title === 'Network' ? 'class="active"' : ''}>Network</a>
|
|
180
183
|
<a href="/apps" ${title === 'Apps' ? 'class="active"' : ''}>Apps</a>
|
|
184
|
+
<a href="/tap-verifier" ${title === 'Tap Verifier' ? 'class="active"' : ''}>Tap Verifier</a>
|
|
181
185
|
</nav>
|
|
182
186
|
<div id="content">${content}</div>
|
|
183
187
|
<script>
|
|
@@ -481,18 +485,535 @@ function renderApps() {
|
|
|
481
485
|
${appsHtml}
|
|
482
486
|
`);
|
|
483
487
|
}
|
|
488
|
+
function renderTapVerifier() {
|
|
489
|
+
return `<!DOCTYPE html>
|
|
490
|
+
<html lang="en">
|
|
491
|
+
<head>
|
|
492
|
+
<meta charset="UTF-8">
|
|
493
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
494
|
+
<title>Tap Test Page - RN Debugger</title>
|
|
495
|
+
<style>
|
|
496
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
497
|
+
body {
|
|
498
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
499
|
+
background: #1a1a2e;
|
|
500
|
+
color: #eee;
|
|
501
|
+
min-height: 100vh;
|
|
502
|
+
}
|
|
503
|
+
.page-container {
|
|
504
|
+
position: relative;
|
|
505
|
+
width: 100%;
|
|
506
|
+
min-height: 100vh;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* Sample UI Elements for Testing */
|
|
510
|
+
.header {
|
|
511
|
+
background: #16213e;
|
|
512
|
+
padding: 20px;
|
|
513
|
+
text-align: center;
|
|
514
|
+
border-bottom: 2px solid #0f3460;
|
|
515
|
+
}
|
|
516
|
+
.header h1 { font-size: 24px; margin-bottom: 8px; }
|
|
517
|
+
.header p { color: #888; font-size: 14px; }
|
|
518
|
+
|
|
519
|
+
.button-grid {
|
|
520
|
+
display: grid;
|
|
521
|
+
grid-template-columns: repeat(3, 1fr);
|
|
522
|
+
gap: 16px;
|
|
523
|
+
padding: 20px;
|
|
524
|
+
max-width: 400px;
|
|
525
|
+
margin: 0 auto;
|
|
526
|
+
}
|
|
527
|
+
.test-btn {
|
|
528
|
+
padding: 20px;
|
|
529
|
+
border: none;
|
|
530
|
+
border-radius: 12px;
|
|
531
|
+
font-size: 14px;
|
|
532
|
+
font-weight: 600;
|
|
533
|
+
cursor: pointer;
|
|
534
|
+
transition: transform 0.1s;
|
|
535
|
+
}
|
|
536
|
+
.test-btn:active { transform: scale(0.95); }
|
|
537
|
+
.btn-red { background: #e94560; color: white; }
|
|
538
|
+
.btn-blue { background: #0f3460; color: white; }
|
|
539
|
+
.btn-green { background: #1eb980; color: white; }
|
|
540
|
+
.btn-yellow { background: #f39c12; color: #333; }
|
|
541
|
+
.btn-purple { background: #9b59b6; color: white; }
|
|
542
|
+
.btn-cyan { background: #00cec9; color: #333; }
|
|
543
|
+
|
|
544
|
+
/* Grid Overlay */
|
|
545
|
+
.grid-overlay {
|
|
546
|
+
position: fixed;
|
|
547
|
+
top: 0;
|
|
548
|
+
left: 0;
|
|
549
|
+
width: 100%;
|
|
550
|
+
height: 100%;
|
|
551
|
+
pointer-events: none;
|
|
552
|
+
z-index: 9998;
|
|
553
|
+
display: none;
|
|
554
|
+
}
|
|
555
|
+
.grid-overlay.visible { display: block; }
|
|
556
|
+
.grid-line-v {
|
|
557
|
+
position: absolute;
|
|
558
|
+
top: 0;
|
|
559
|
+
bottom: 0;
|
|
560
|
+
width: 1px;
|
|
561
|
+
background: rgba(255, 255, 0, 0.6);
|
|
562
|
+
}
|
|
563
|
+
.grid-line-h {
|
|
564
|
+
position: absolute;
|
|
565
|
+
left: 0;
|
|
566
|
+
right: 0;
|
|
567
|
+
height: 1px;
|
|
568
|
+
background: rgba(255, 255, 0, 0.6);
|
|
569
|
+
}
|
|
570
|
+
.grid-label {
|
|
571
|
+
position: absolute;
|
|
572
|
+
background: rgba(0, 0, 0, 0.8);
|
|
573
|
+
color: #ffff00;
|
|
574
|
+
font-size: 10px;
|
|
575
|
+
font-family: monospace;
|
|
576
|
+
padding: 2px 4px;
|
|
577
|
+
border-radius: 2px;
|
|
578
|
+
}
|
|
579
|
+
.grid-toggle {
|
|
580
|
+
position: fixed;
|
|
581
|
+
top: 10px;
|
|
582
|
+
left: 10px;
|
|
583
|
+
background: #ffff00;
|
|
584
|
+
color: #000;
|
|
585
|
+
border: none;
|
|
586
|
+
padding: 6px 10px;
|
|
587
|
+
border-radius: 4px;
|
|
588
|
+
font-size: 11px;
|
|
589
|
+
font-weight: bold;
|
|
590
|
+
cursor: pointer;
|
|
591
|
+
z-index: 10001;
|
|
592
|
+
opacity: 0.8;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.nav-bar {
|
|
596
|
+
display: flex;
|
|
597
|
+
justify-content: space-around;
|
|
598
|
+
background: #16213e;
|
|
599
|
+
padding: 16px;
|
|
600
|
+
position: fixed;
|
|
601
|
+
bottom: 0;
|
|
602
|
+
left: 0;
|
|
603
|
+
right: 0;
|
|
604
|
+
border-top: 2px solid #0f3460;
|
|
605
|
+
}
|
|
606
|
+
.nav-item {
|
|
607
|
+
display: flex;
|
|
608
|
+
flex-direction: column;
|
|
609
|
+
align-items: center;
|
|
610
|
+
color: #888;
|
|
611
|
+
font-size: 12px;
|
|
612
|
+
padding: 8px 16px;
|
|
613
|
+
border-radius: 8px;
|
|
614
|
+
cursor: pointer;
|
|
615
|
+
}
|
|
616
|
+
.nav-item:hover { background: #0f3460; color: #fff; }
|
|
617
|
+
.nav-icon { font-size: 24px; margin-bottom: 4px; }
|
|
618
|
+
|
|
619
|
+
.card {
|
|
620
|
+
background: #16213e;
|
|
621
|
+
margin: 20px;
|
|
622
|
+
padding: 20px;
|
|
623
|
+
border-radius: 12px;
|
|
624
|
+
border: 1px solid #0f3460;
|
|
625
|
+
}
|
|
626
|
+
.card h2 { margin-bottom: 12px; font-size: 18px; }
|
|
627
|
+
.card p { color: #888; line-height: 1.6; }
|
|
628
|
+
|
|
629
|
+
.input-group {
|
|
630
|
+
margin: 20px;
|
|
631
|
+
}
|
|
632
|
+
.input-group label {
|
|
633
|
+
display: block;
|
|
634
|
+
margin-bottom: 8px;
|
|
635
|
+
color: #888;
|
|
636
|
+
}
|
|
637
|
+
.input-group input {
|
|
638
|
+
width: 100%;
|
|
639
|
+
padding: 14px;
|
|
640
|
+
border: 2px solid #0f3460;
|
|
641
|
+
border-radius: 8px;
|
|
642
|
+
background: #16213e;
|
|
643
|
+
color: #fff;
|
|
644
|
+
font-size: 16px;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/* Canvas Overlay for Markers */
|
|
648
|
+
#markerCanvas {
|
|
649
|
+
position: fixed;
|
|
650
|
+
top: 0;
|
|
651
|
+
left: 0;
|
|
652
|
+
width: 100%;
|
|
653
|
+
height: 100%;
|
|
654
|
+
pointer-events: auto;
|
|
655
|
+
z-index: 9999;
|
|
656
|
+
cursor: crosshair;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/* Marker info panel */
|
|
660
|
+
.marker-panel {
|
|
661
|
+
position: fixed;
|
|
662
|
+
top: 10px;
|
|
663
|
+
right: 10px;
|
|
664
|
+
background: rgba(0,0,0,0.8);
|
|
665
|
+
padding: 12px;
|
|
666
|
+
border-radius: 8px;
|
|
667
|
+
font-size: 12px;
|
|
668
|
+
z-index: 10000;
|
|
669
|
+
max-width: 250px;
|
|
670
|
+
}
|
|
671
|
+
.marker-panel h3 {
|
|
672
|
+
color: #ff6b6b;
|
|
673
|
+
margin-bottom: 8px;
|
|
674
|
+
font-size: 14px;
|
|
675
|
+
}
|
|
676
|
+
.marker-info {
|
|
677
|
+
color: #ccc;
|
|
678
|
+
margin-bottom: 4px;
|
|
679
|
+
}
|
|
680
|
+
.clear-btn {
|
|
681
|
+
margin-top: 8px;
|
|
682
|
+
padding: 6px 12px;
|
|
683
|
+
background: #e94560;
|
|
684
|
+
border: none;
|
|
685
|
+
border-radius: 4px;
|
|
686
|
+
color: white;
|
|
687
|
+
cursor: pointer;
|
|
688
|
+
font-size: 12px;
|
|
689
|
+
}
|
|
690
|
+
</style>
|
|
691
|
+
</head>
|
|
692
|
+
<body>
|
|
693
|
+
<div class="page-container">
|
|
694
|
+
<div class="header">
|
|
695
|
+
<h1>Tap Test Page</h1>
|
|
696
|
+
<p>Agent marks coordinates here to verify accuracy</p>
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
<div class="button-grid">
|
|
700
|
+
<button class="test-btn btn-red" onclick="btnClick('Red')">Red</button>
|
|
701
|
+
<button class="test-btn btn-blue" onclick="btnClick('Blue')">Blue</button>
|
|
702
|
+
<button class="test-btn btn-green" onclick="btnClick('Green')">Green</button>
|
|
703
|
+
<button class="test-btn btn-yellow" onclick="btnClick('Yellow')">Yellow</button>
|
|
704
|
+
<button class="test-btn btn-purple" onclick="btnClick('Purple')">Purple</button>
|
|
705
|
+
<button class="test-btn btn-cyan" onclick="btnClick('Cyan')">Cyan</button>
|
|
706
|
+
</div>
|
|
707
|
+
|
|
708
|
+
<div class="card">
|
|
709
|
+
<h2>Test Card</h2>
|
|
710
|
+
<p>This is a sample card element. The agent can try to tap on this text or the card itself to test coordinate accuracy.</p>
|
|
711
|
+
</div>
|
|
712
|
+
|
|
713
|
+
<div class="input-group">
|
|
714
|
+
<label>Test Input Field</label>
|
|
715
|
+
<input type="text" placeholder="Tap here to focus...">
|
|
716
|
+
</div>
|
|
717
|
+
|
|
718
|
+
<div class="card">
|
|
719
|
+
<h2>Instructions</h2>
|
|
720
|
+
<p>1. Agent takes screenshot of this page<br>
|
|
721
|
+
2. Agent identifies element and calculates coordinates<br>
|
|
722
|
+
3. Agent calls /api/tap-verifier/mark with coordinates<br>
|
|
723
|
+
4. Marker appears on canvas overlay<br>
|
|
724
|
+
5. Verify if marker aligns with intended element</p>
|
|
725
|
+
</div>
|
|
726
|
+
|
|
727
|
+
<div class="nav-bar">
|
|
728
|
+
<div class="nav-item" onclick="navClick('Home')">
|
|
729
|
+
<span class="nav-icon">🏠</span>
|
|
730
|
+
Home
|
|
731
|
+
</div>
|
|
732
|
+
<div class="nav-item" onclick="navClick('Search')">
|
|
733
|
+
<span class="nav-icon">🔍</span>
|
|
734
|
+
Search
|
|
735
|
+
</div>
|
|
736
|
+
<div class="nav-item" onclick="navClick('Profile')">
|
|
737
|
+
<span class="nav-icon">👤</span>
|
|
738
|
+
Profile
|
|
739
|
+
</div>
|
|
740
|
+
<div class="nav-item" onclick="navClick('Settings')">
|
|
741
|
+
<span class="nav-icon">⚙️</span>
|
|
742
|
+
Settings
|
|
743
|
+
</div>
|
|
744
|
+
</div>
|
|
745
|
+
</div>
|
|
746
|
+
|
|
747
|
+
<!-- Grid overlay for coordinate reference -->
|
|
748
|
+
<div class="grid-overlay" id="gridOverlay"></div>
|
|
749
|
+
<button class="grid-toggle" onclick="toggleGrid()">Grid</button>
|
|
750
|
+
|
|
751
|
+
<!-- Transparent canvas overlay for markers -->
|
|
752
|
+
<canvas id="markerCanvas"></canvas>
|
|
753
|
+
|
|
754
|
+
<!-- Marker info panel -->
|
|
755
|
+
<div class="marker-panel" id="markerPanel" style="display: none;">
|
|
756
|
+
<h3>Agent Markers</h3>
|
|
757
|
+
<div id="markerList"></div>
|
|
758
|
+
<button class="clear-btn" onclick="clearMarkers()">Clear All</button>
|
|
759
|
+
</div>
|
|
760
|
+
|
|
761
|
+
<script>
|
|
762
|
+
const canvas = document.getElementById('markerCanvas');
|
|
763
|
+
const ctx = canvas.getContext('2d');
|
|
764
|
+
const markerPanel = document.getElementById('markerPanel');
|
|
765
|
+
const markerList = document.getElementById('markerList');
|
|
766
|
+
|
|
767
|
+
let markers = [];
|
|
768
|
+
|
|
769
|
+
// Resize canvas to match window
|
|
770
|
+
function resizeCanvas() {
|
|
771
|
+
canvas.width = window.innerWidth;
|
|
772
|
+
canvas.height = window.innerHeight;
|
|
773
|
+
drawMarkers();
|
|
774
|
+
}
|
|
775
|
+
window.addEventListener('resize', resizeCanvas);
|
|
776
|
+
resizeCanvas();
|
|
777
|
+
|
|
778
|
+
// Click on canvas to add marker
|
|
779
|
+
canvas.addEventListener('click', async (e) => {
|
|
780
|
+
const x = e.clientX;
|
|
781
|
+
const y = e.clientY;
|
|
782
|
+
try {
|
|
783
|
+
await fetch('/api/tap-verifier/mark', {
|
|
784
|
+
method: 'POST',
|
|
785
|
+
headers: { 'Content-Type': 'application/json' },
|
|
786
|
+
body: JSON.stringify({ x, y, label: 'Click', color: '#00ff88' })
|
|
787
|
+
});
|
|
788
|
+
pollMarkers();
|
|
789
|
+
} catch (err) {
|
|
790
|
+
console.error('Failed to add marker:', err);
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// Button click feedback
|
|
795
|
+
function btnClick(name) {
|
|
796
|
+
console.log('Button clicked:', name);
|
|
797
|
+
}
|
|
798
|
+
function navClick(name) {
|
|
799
|
+
console.log('Nav clicked:', name);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Draw all markers
|
|
803
|
+
function drawMarkers() {
|
|
804
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
805
|
+
|
|
806
|
+
markers.forEach((marker, i) => {
|
|
807
|
+
const color = marker.color || '#ff6b6b';
|
|
808
|
+
const x = marker.x;
|
|
809
|
+
const y = marker.y;
|
|
810
|
+
|
|
811
|
+
// Draw outer ring (dashed)
|
|
812
|
+
ctx.beginPath();
|
|
813
|
+
ctx.arc(x, y, 30, 0, Math.PI * 2);
|
|
814
|
+
ctx.setLineDash([6, 4]);
|
|
815
|
+
ctx.strokeStyle = color;
|
|
816
|
+
ctx.lineWidth = 3;
|
|
817
|
+
ctx.stroke();
|
|
818
|
+
ctx.setLineDash([]);
|
|
819
|
+
|
|
820
|
+
// Draw inner circle
|
|
821
|
+
ctx.beginPath();
|
|
822
|
+
ctx.arc(x, y, 8, 0, Math.PI * 2);
|
|
823
|
+
ctx.fillStyle = color;
|
|
824
|
+
ctx.fill();
|
|
825
|
+
|
|
826
|
+
// Draw crosshair
|
|
827
|
+
ctx.beginPath();
|
|
828
|
+
ctx.moveTo(x - 20, y);
|
|
829
|
+
ctx.lineTo(x + 20, y);
|
|
830
|
+
ctx.moveTo(x, y - 20);
|
|
831
|
+
ctx.lineTo(x, y + 20);
|
|
832
|
+
ctx.strokeStyle = color;
|
|
833
|
+
ctx.lineWidth = 2;
|
|
834
|
+
ctx.stroke();
|
|
835
|
+
|
|
836
|
+
// Draw label
|
|
837
|
+
const label = marker.label || 'Marker ' + (i + 1);
|
|
838
|
+
const text = label + ' (' + x + ', ' + y + ')';
|
|
839
|
+
ctx.font = 'bold 12px monospace';
|
|
840
|
+
const textWidth = ctx.measureText(text).width;
|
|
841
|
+
|
|
842
|
+
ctx.fillStyle = 'rgba(0,0,0,0.8)';
|
|
843
|
+
ctx.fillRect(x + 35, y - 10, textWidth + 10, 22);
|
|
844
|
+
ctx.strokeStyle = color;
|
|
845
|
+
ctx.lineWidth = 1;
|
|
846
|
+
ctx.strokeRect(x + 35, y - 10, textWidth + 10, 22);
|
|
847
|
+
|
|
848
|
+
ctx.fillStyle = color;
|
|
849
|
+
ctx.fillText(text, x + 40, y + 5);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
updateMarkerPanel();
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Update marker panel
|
|
856
|
+
function updateMarkerPanel() {
|
|
857
|
+
if (markers.length === 0) {
|
|
858
|
+
markerPanel.style.display = 'none';
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
markerPanel.style.display = 'block';
|
|
863
|
+
markerList.innerHTML = markers.map((m, i) =>
|
|
864
|
+
'<div class="marker-info">' + (m.label || 'Marker ' + (i+1)) + ': (' + m.x + ', ' + m.y + ')</div>'
|
|
865
|
+
).join('');
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Poll for markers from server
|
|
869
|
+
async function pollMarkers() {
|
|
870
|
+
try {
|
|
871
|
+
const res = await fetch('/api/tap-verifier/markers');
|
|
872
|
+
const data = await res.json();
|
|
873
|
+
if (data.markers && JSON.stringify(data.markers) !== JSON.stringify(markers)) {
|
|
874
|
+
markers = data.markers;
|
|
875
|
+
drawMarkers();
|
|
876
|
+
}
|
|
877
|
+
} catch (err) {
|
|
878
|
+
// Ignore
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Clear markers
|
|
883
|
+
async function clearMarkers() {
|
|
884
|
+
try {
|
|
885
|
+
await fetch('/api/tap-verifier/clear-markers', { method: 'POST' });
|
|
886
|
+
markers = [];
|
|
887
|
+
drawMarkers();
|
|
888
|
+
} catch (err) {
|
|
889
|
+
console.error('Failed to clear:', err);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Grid overlay functionality
|
|
894
|
+
const gridOverlay = document.getElementById('gridOverlay');
|
|
895
|
+
let gridVisible = false;
|
|
896
|
+
|
|
897
|
+
function toggleGrid() {
|
|
898
|
+
gridVisible = !gridVisible;
|
|
899
|
+
if (gridVisible) {
|
|
900
|
+
gridOverlay.classList.add('visible');
|
|
901
|
+
createGrid();
|
|
902
|
+
} else {
|
|
903
|
+
gridOverlay.classList.remove('visible');
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function createGrid() {
|
|
908
|
+
gridOverlay.innerHTML = '';
|
|
909
|
+
const w = window.innerWidth;
|
|
910
|
+
const h = window.innerHeight;
|
|
911
|
+
|
|
912
|
+
// Calculate button grid boundaries
|
|
913
|
+
// Grid: max-width 400px, centered, padding 20px, gap 16px, 3 columns
|
|
914
|
+
const gridWidth = Math.min(400, w);
|
|
915
|
+
const gridLeft = (w - gridWidth) / 2;
|
|
916
|
+
const gridRight = gridLeft + gridWidth;
|
|
917
|
+
const padding = 20;
|
|
918
|
+
const gap = 16;
|
|
919
|
+
const colWidth = (gridWidth - 2 * padding - 2 * gap) / 3;
|
|
920
|
+
|
|
921
|
+
// Column positions (centers)
|
|
922
|
+
const col1Center = gridLeft + padding + colWidth / 2;
|
|
923
|
+
const col2Center = gridLeft + padding + colWidth + gap + colWidth / 2;
|
|
924
|
+
const col3Center = gridLeft + padding + 2 * colWidth + 2 * gap + colWidth / 2;
|
|
925
|
+
|
|
926
|
+
// Column boundaries
|
|
927
|
+
const col1Left = gridLeft + padding;
|
|
928
|
+
const col1Right = col1Left + colWidth;
|
|
929
|
+
const col2Left = col1Right + gap;
|
|
930
|
+
const col2Right = col2Left + colWidth;
|
|
931
|
+
const col3Left = col2Right + gap;
|
|
932
|
+
const col3Right = col3Left + colWidth;
|
|
933
|
+
|
|
934
|
+
// Draw vertical lines at column boundaries
|
|
935
|
+
const vLines = [
|
|
936
|
+
{ x: gridLeft, label: Math.round(gridLeft) },
|
|
937
|
+
{ x: col1Left, label: Math.round(col1Left) },
|
|
938
|
+
{ x: col1Center, label: Math.round(col1Center) + ' (C1)', isCenter: true },
|
|
939
|
+
{ x: col1Right, label: Math.round(col1Right) },
|
|
940
|
+
{ x: col2Left, label: Math.round(col2Left) },
|
|
941
|
+
{ x: col2Center, label: Math.round(col2Center) + ' (C2)', isCenter: true },
|
|
942
|
+
{ x: col2Right, label: Math.round(col2Right) },
|
|
943
|
+
{ x: col3Left, label: Math.round(col3Left) },
|
|
944
|
+
{ x: col3Center, label: Math.round(col3Center) + ' (C3)', isCenter: true },
|
|
945
|
+
{ x: col3Right, label: Math.round(col3Right) },
|
|
946
|
+
{ x: gridRight, label: Math.round(gridRight) }
|
|
947
|
+
];
|
|
948
|
+
|
|
949
|
+
vLines.forEach((line, i) => {
|
|
950
|
+
const div = document.createElement('div');
|
|
951
|
+
div.className = 'grid-line-v';
|
|
952
|
+
div.style.left = line.x + 'px';
|
|
953
|
+
if (line.isCenter) {
|
|
954
|
+
div.style.background = 'rgba(0, 255, 0, 0.8)';
|
|
955
|
+
div.style.width = '2px';
|
|
956
|
+
}
|
|
957
|
+
gridOverlay.appendChild(div);
|
|
958
|
+
|
|
959
|
+
// Label
|
|
960
|
+
const label = document.createElement('div');
|
|
961
|
+
label.className = 'grid-label';
|
|
962
|
+
label.textContent = line.label;
|
|
963
|
+
label.style.left = (line.x + 3) + 'px';
|
|
964
|
+
label.style.top = (70 + (i % 3) * 14) + 'px';
|
|
965
|
+
if (line.isCenter) label.style.color = '#00ff00';
|
|
966
|
+
gridOverlay.appendChild(label);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// Horizontal lines every 50px with labels
|
|
970
|
+
for (let y = 0; y <= h; y += 50) {
|
|
971
|
+
const div = document.createElement('div');
|
|
972
|
+
div.className = 'grid-line-h';
|
|
973
|
+
div.style.top = y + 'px';
|
|
974
|
+
if (y % 100 === 0) {
|
|
975
|
+
div.style.background = 'rgba(255, 255, 0, 0.8)';
|
|
976
|
+
}
|
|
977
|
+
gridOverlay.appendChild(div);
|
|
978
|
+
|
|
979
|
+
if (y % 100 === 0) {
|
|
980
|
+
const label = document.createElement('div');
|
|
981
|
+
label.className = 'grid-label';
|
|
982
|
+
label.textContent = 'y=' + y;
|
|
983
|
+
label.style.left = '3px';
|
|
984
|
+
label.style.top = (y + 2) + 'px';
|
|
985
|
+
gridOverlay.appendChild(label);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Recreate grid on resize
|
|
991
|
+
window.addEventListener('resize', () => {
|
|
992
|
+
if (gridVisible) createGrid();
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// Start polling
|
|
996
|
+
setInterval(pollMarkers, 500);
|
|
997
|
+
pollMarkers();
|
|
998
|
+
</script>
|
|
999
|
+
</body>
|
|
1000
|
+
</html>`;
|
|
1001
|
+
}
|
|
484
1002
|
function createRequestHandler() {
|
|
485
|
-
return (req, res) => {
|
|
1003
|
+
return async (req, res) => {
|
|
486
1004
|
// Set CORS headers for browser access
|
|
487
1005
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
488
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
1006
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
489
1007
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
490
1008
|
if (req.method === "OPTIONS") {
|
|
491
1009
|
res.statusCode = 204;
|
|
492
1010
|
res.end();
|
|
493
1011
|
return;
|
|
494
1012
|
}
|
|
495
|
-
const
|
|
1013
|
+
const fullUrl = req.url ?? "/";
|
|
1014
|
+
const [urlPath, queryString] = fullUrl.split('?');
|
|
1015
|
+
const url = urlPath; // Path without query params
|
|
1016
|
+
const params = new URLSearchParams(queryString || '');
|
|
496
1017
|
try {
|
|
497
1018
|
// HTML endpoints
|
|
498
1019
|
if (url === "/") {
|
|
@@ -515,6 +1036,11 @@ function createRequestHandler() {
|
|
|
515
1036
|
res.end(renderApps());
|
|
516
1037
|
return;
|
|
517
1038
|
}
|
|
1039
|
+
if (url === "/tap-verifier") {
|
|
1040
|
+
res.setHeader("Content-Type", "text/html");
|
|
1041
|
+
res.end(renderTapVerifier());
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
518
1044
|
// JSON API endpoints
|
|
519
1045
|
res.setHeader("Content-Type", "application/json");
|
|
520
1046
|
if (url === "/api/logs" || url === "/api/logs/") {
|
|
@@ -549,6 +1075,162 @@ function createRequestHandler() {
|
|
|
549
1075
|
};
|
|
550
1076
|
res.end(JSON.stringify(status, null, 2));
|
|
551
1077
|
}
|
|
1078
|
+
else if (url === "/api/tap-verifier/devices") {
|
|
1079
|
+
const platform = params.get('platform') || 'android';
|
|
1080
|
+
try {
|
|
1081
|
+
if (platform === 'android') {
|
|
1082
|
+
const result = await listAndroidDevices();
|
|
1083
|
+
if (result.success && result.devices) {
|
|
1084
|
+
const devices = result.devices.map(d => ({
|
|
1085
|
+
id: d.id,
|
|
1086
|
+
name: `${d.model || d.id} (${d.status})`
|
|
1087
|
+
}));
|
|
1088
|
+
res.end(JSON.stringify({ devices }));
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
res.end(JSON.stringify({ devices: [], error: result.error }));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
const result = await listIOSSimulators();
|
|
1096
|
+
if (result.success && result.simulators) {
|
|
1097
|
+
const devices = result.simulators
|
|
1098
|
+
.filter(s => s.state === 'Booted')
|
|
1099
|
+
.map(s => ({
|
|
1100
|
+
id: s.udid,
|
|
1101
|
+
name: `${s.name} (${s.runtime})`
|
|
1102
|
+
}));
|
|
1103
|
+
res.end(JSON.stringify({ devices }));
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
res.end(JSON.stringify({ devices: [], error: result.error }));
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
catch (err) {
|
|
1111
|
+
res.end(JSON.stringify({ devices: [], error: String(err) }));
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
else if (url === "/api/tap-verifier/screen-size") {
|
|
1115
|
+
const platform = params.get('platform') || 'android';
|
|
1116
|
+
const deviceId = params.get('deviceId') || undefined;
|
|
1117
|
+
try {
|
|
1118
|
+
if (platform === 'android') {
|
|
1119
|
+
const result = await androidGetScreenSize(deviceId);
|
|
1120
|
+
if (result.success && result.width && result.height) {
|
|
1121
|
+
res.end(JSON.stringify({ width: result.width, height: result.height }));
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
res.end(JSON.stringify({ error: result.error || 'Failed to get screen size' }));
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
// iOS doesn't have a direct screen size function, use common sizes
|
|
1129
|
+
// We'll get the actual size from the screenshot
|
|
1130
|
+
res.end(JSON.stringify({ width: 390, height: 844, note: 'Default iPhone size. Load screenshot for actual dimensions.' }));
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
catch (err) {
|
|
1134
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
else if (url === "/api/tap-verifier/screenshot") {
|
|
1138
|
+
const platform = params.get('platform') || 'android';
|
|
1139
|
+
const deviceId = params.get('deviceId') || undefined;
|
|
1140
|
+
try {
|
|
1141
|
+
if (platform === 'android') {
|
|
1142
|
+
const result = await androidScreenshot(deviceId);
|
|
1143
|
+
if (result.success && result.data) {
|
|
1144
|
+
const base64 = result.data.toString('base64');
|
|
1145
|
+
res.end(JSON.stringify({ success: true, image: base64 }));
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
res.end(JSON.stringify({ success: false, error: result.error || 'Failed to take screenshot' }));
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
else {
|
|
1152
|
+
const result = await iosScreenshot(undefined, deviceId);
|
|
1153
|
+
if (result.success && result.data) {
|
|
1154
|
+
const base64 = result.data.toString('base64');
|
|
1155
|
+
res.end(JSON.stringify({ success: true, image: base64 }));
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1158
|
+
res.end(JSON.stringify({ success: false, error: result.error || 'Failed to take screenshot' }));
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
catch (err) {
|
|
1163
|
+
res.end(JSON.stringify({ success: false, error: String(err) }));
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
else if (url === "/api/tap-verifier/execute" && req.method === "POST") {
|
|
1167
|
+
// Parse POST body
|
|
1168
|
+
let body = '';
|
|
1169
|
+
req.on('data', chunk => { body += chunk; });
|
|
1170
|
+
req.on('end', async () => {
|
|
1171
|
+
try {
|
|
1172
|
+
const data = JSON.parse(body);
|
|
1173
|
+
const { platform, x, y, deviceId } = data;
|
|
1174
|
+
if (typeof x !== 'number' || typeof y !== 'number') {
|
|
1175
|
+
res.end(JSON.stringify({ success: false, error: 'x and y must be numbers' }));
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (platform === 'android') {
|
|
1179
|
+
const result = await androidTap(x, y, deviceId);
|
|
1180
|
+
res.end(JSON.stringify({ success: result.success, error: result.error }));
|
|
1181
|
+
}
|
|
1182
|
+
else {
|
|
1183
|
+
const result = await iosTap(x, y, { udid: deviceId });
|
|
1184
|
+
res.end(JSON.stringify({ success: result.success, error: result.error }));
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
catch (err) {
|
|
1188
|
+
res.end(JSON.stringify({ success: false, error: String(err) }));
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
return; // Important: return here since we're handling the response asynchronously
|
|
1192
|
+
}
|
|
1193
|
+
else if (url === "/api/tap-verifier/mark" && req.method === "POST") {
|
|
1194
|
+
// Add a marker to the tap verifier (agent can mark calculated coordinates)
|
|
1195
|
+
let body = '';
|
|
1196
|
+
req.on('data', chunk => { body += chunk; });
|
|
1197
|
+
req.on('end', () => {
|
|
1198
|
+
try {
|
|
1199
|
+
const data = JSON.parse(body);
|
|
1200
|
+
const { x, y, label, color } = data;
|
|
1201
|
+
if (typeof x !== 'number' || typeof y !== 'number') {
|
|
1202
|
+
res.end(JSON.stringify({ success: false, error: 'x and y must be numbers' }));
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const marker = {
|
|
1206
|
+
x,
|
|
1207
|
+
y,
|
|
1208
|
+
label: label || undefined,
|
|
1209
|
+
color: color || undefined,
|
|
1210
|
+
timestamp: Date.now()
|
|
1211
|
+
};
|
|
1212
|
+
tapVerifierMarkers.push(marker);
|
|
1213
|
+
res.end(JSON.stringify({
|
|
1214
|
+
success: true,
|
|
1215
|
+
marker,
|
|
1216
|
+
totalMarkers: tapVerifierMarkers.length
|
|
1217
|
+
}));
|
|
1218
|
+
}
|
|
1219
|
+
catch (err) {
|
|
1220
|
+
res.end(JSON.stringify({ success: false, error: String(err) }));
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
else if (url === "/api/tap-verifier/markers") {
|
|
1226
|
+
// Get all markers added by the agent
|
|
1227
|
+
res.end(JSON.stringify({ markers: tapVerifierMarkers }));
|
|
1228
|
+
}
|
|
1229
|
+
else if (url === "/api/tap-verifier/clear-markers" && req.method === "POST") {
|
|
1230
|
+
// Clear all agent-added markers
|
|
1231
|
+
tapVerifierMarkers = [];
|
|
1232
|
+
res.end(JSON.stringify({ success: true, message: 'All markers cleared' }));
|
|
1233
|
+
}
|
|
552
1234
|
else if (url === "/api" || url === "/api/") {
|
|
553
1235
|
const endpoints = {
|
|
554
1236
|
message: "React Native AI Debugger - Debug HTTP Server",
|
|
@@ -556,14 +1238,22 @@ function createRequestHandler() {
|
|
|
556
1238
|
"/": "Dashboard",
|
|
557
1239
|
"/logs": "Console logs (colored)",
|
|
558
1240
|
"/network": "Network requests",
|
|
559
|
-
"/apps": "Connected apps"
|
|
1241
|
+
"/apps": "Connected apps",
|
|
1242
|
+
"/tap-verifier": "Tap coordinate verification tool"
|
|
560
1243
|
},
|
|
561
1244
|
api: {
|
|
562
1245
|
"/api/status": "Overall server status and buffer sizes",
|
|
563
1246
|
"/api/logs": "All captured console logs (JSON)",
|
|
564
1247
|
"/api/network": "All captured network requests (JSON)",
|
|
565
1248
|
"/api/bundle-errors": "Metro bundle/compilation errors (JSON)",
|
|
566
|
-
"/api/apps": "Connected React Native apps (JSON)"
|
|
1249
|
+
"/api/apps": "Connected React Native apps (JSON)",
|
|
1250
|
+
"/api/tap-verifier/devices": "List available devices (query: platform=android|ios)",
|
|
1251
|
+
"/api/tap-verifier/screen-size": "Get device screen size (query: platform, deviceId)",
|
|
1252
|
+
"/api/tap-verifier/screenshot": "Get device screenshot as base64 (query: platform, deviceId)",
|
|
1253
|
+
"/api/tap-verifier/execute": "Execute tap at coordinates (POST: platform, x, y, deviceId)",
|
|
1254
|
+
"/api/tap-verifier/mark": "Add a marker to visualize agent-calculated coordinates (POST: x, y, label?, color?)",
|
|
1255
|
+
"/api/tap-verifier/markers": "Get all agent-added markers (GET)",
|
|
1256
|
+
"/api/tap-verifier/clear-markers": "Clear all agent-added markers (POST)"
|
|
567
1257
|
}
|
|
568
1258
|
};
|
|
569
1259
|
res.end(JSON.stringify(endpoints, null, 2));
|