NERDDISCO commited on
Commit
4500b8e
·
1 Parent(s): e5f6285

refactor: move examples into separate folders

Browse files
examples/iframe-dialog-test/README.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Iframe Dialog Test
2
+
3
+ This example tests WebSerial + WebUSB dialog behavior in iframe vs standalone environments to replicate HuggingFace Spaces behavior.
4
+
5
+ ## Purpose
6
+
7
+ Tests the user gesture consumption issue that occurs when WebSerial and WebUSB dialogs conflict in iframe contexts (like HuggingFace Spaces).
8
+
9
+ ## Files
10
+
11
+ - `iframe-dialog-test.html` - Main test page with iframe controls
12
+ - `iframe-content.html` - Content loaded in iframe to simulate HuggingFace Spaces
13
+ - `iframe-dialog-test.ts` - TypeScript logic for testing dialog behavior
14
+
15
+ ## Running
16
+
17
+ From the root directory:
18
+
19
+ ```bash
20
+ pnpm example:iframe-test
21
+ ```
22
+
23
+ Or from this directory:
24
+
25
+ ```bash
26
+ pnpm dev
27
+ ```
28
+
29
+ ## Testing
30
+
31
+ 1. Click "🔓 Permissive" to load iframe in permissive mode
32
+ 2. In the iframe, click "Test Actual findPort Function"
33
+ 3. Should demonstrate sequential dialog behavior working in iframe context
34
+
35
+ ## Browser Requirements
36
+
37
+ - Chrome/Edge 89+ with WebSerial and WebUSB APIs
38
+ - HTTPS or localhost
39
+ - Real robot hardware for full testing
examples/iframe-dialog-test/iframe-content.html ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Iframe Test Content</title>
7
+ <style>
8
+ body {
9
+ font-family: system-ui, sans-serif;
10
+ padding: 20px;
11
+ margin: 0;
12
+ background: white;
13
+ text-align: center;
14
+ }
15
+ button {
16
+ background: #2563eb;
17
+ color: white;
18
+ border: none;
19
+ padding: 10px 20px;
20
+ border-radius: 4px;
21
+ cursor: pointer;
22
+ margin: 5px;
23
+ font-size: 14px;
24
+ }
25
+ button:hover {
26
+ background: #1d4ed8;
27
+ }
28
+ .status {
29
+ margin: 10px 0;
30
+ padding: 10px;
31
+ background: #f3f4f6;
32
+ border-radius: 4px;
33
+ font-size: 14px;
34
+ }
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <h3>🖼️ Inside Iframe</h3>
39
+ <p>This simulates HuggingFace Spaces environment</p>
40
+
41
+ <button onclick="testActualFindPort()">
42
+ Test Actual findPort Function
43
+ </button>
44
+ <button onclick="testSequential()">Test Mock Sequential</button>
45
+ <button onclick="testSimultaneous()">Test Mock Simultaneous</button>
46
+
47
+ <div id="status" class="status">Ready to test...</div>
48
+
49
+ <script type="module">
50
+ import { findPort } from "../packages/web/src/index.js";
51
+
52
+ function updateStatus(message) {
53
+ const status = document.getElementById("status");
54
+ const timestamp = new Date().toLocaleTimeString();
55
+ status.textContent = timestamp + ": " + message;
56
+
57
+ // Also log to parent window
58
+ window.parent.postMessage(
59
+ {
60
+ type: "iframe-log",
61
+ message: message,
62
+ },
63
+ "*"
64
+ );
65
+ }
66
+
67
+ window.testSequential = async function () {
68
+ updateStatus("Testing sequential dialogs...");
69
+
70
+ try {
71
+ updateStatus("Requesting WebSerial port...");
72
+ const serialPort = await navigator.serial.requestPort();
73
+ updateStatus("WebSerial port selected");
74
+
75
+ updateStatus("Requesting WebUSB device...");
76
+ const usbDevice = await navigator.usb.requestDevice({ filters: [] });
77
+ updateStatus("WebUSB device selected - SUCCESS!");
78
+ } catch (error) {
79
+ updateStatus("Failed: " + error.message);
80
+ }
81
+ };
82
+
83
+ window.testSimultaneous = async function () {
84
+ updateStatus("Testing simultaneous dialogs...");
85
+
86
+ try {
87
+ updateStatus("Starting both dialogs simultaneously...");
88
+ const [serialPortPromise, usbDevicePromise] = [
89
+ navigator.serial.requestPort(),
90
+ navigator.usb.requestDevice({ filters: [] }),
91
+ ];
92
+
93
+ const [serialPort, usbDevice] = await Promise.all([
94
+ serialPortPromise,
95
+ usbDevicePromise,
96
+ ]);
97
+
98
+ updateStatus("Both dialogs completed - SUCCESS!");
99
+ } catch (error) {
100
+ updateStatus("Failed: " + error.message);
101
+ }
102
+ };
103
+
104
+ // Test actual findPort function
105
+ window.testActualFindPort = async function () {
106
+ updateStatus("Testing actual findPort function...");
107
+
108
+ try {
109
+ updateStatus("Starting findPort with fallback behavior...");
110
+ const findProcess = await findPort({
111
+ onMessage: (msg) => updateStatus("findPort: " + msg),
112
+ });
113
+
114
+ const robots = await findProcess.result;
115
+ updateStatus("SUCCESS! Found " + robots.length + " robots");
116
+
117
+ robots.forEach((robot, index) => {
118
+ updateStatus(
119
+ "Robot " +
120
+ (index + 1) +
121
+ ": " +
122
+ robot.name +
123
+ " (" +
124
+ robot.robotType +
125
+ ")"
126
+ );
127
+ });
128
+ } catch (error) {
129
+ updateStatus("findPort failed: " + error.message);
130
+
131
+ // Analyze the error for debugging
132
+ if (error.message.includes("No port selected")) {
133
+ updateStatus("🔍 This should trigger sequential fallback mode");
134
+ } else if (error.message.includes("cancelled")) {
135
+ updateStatus("🔍 User cancelled dialog - expected for testing");
136
+ }
137
+ }
138
+ };
139
+
140
+ // Check environment on load
141
+ document.addEventListener("DOMContentLoaded", () => {
142
+ updateStatus(
143
+ "Environment: " + (window === window.top ? "Standalone" : "Iframe")
144
+ );
145
+ updateStatus(
146
+ "WebSerial: " +
147
+ ("serial" in navigator ? "Supported" : "Not supported")
148
+ );
149
+ updateStatus(
150
+ "WebUSB: " + ("usb" in navigator ? "Supported" : "Not supported")
151
+ );
152
+ });
153
+ </script>
154
+ </body>
155
+ </html>
examples/iframe-dialog-test/iframe-dialog-test.html ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Iframe Dialog Test</title>
7
+ <style>
8
+ body {
9
+ font-family: system-ui, sans-serif;
10
+ max-width: 800px;
11
+ margin: 2rem auto;
12
+ padding: 2rem;
13
+ background: #f5f5f5;
14
+ }
15
+ .container {
16
+ background: white;
17
+ padding: 2rem;
18
+ border-radius: 8px;
19
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
20
+ }
21
+ button {
22
+ background: #2563eb;
23
+ color: white;
24
+ border: none;
25
+ padding: 12px 24px;
26
+ border-radius: 6px;
27
+ font-size: 16px;
28
+ cursor: pointer;
29
+ margin: 8px;
30
+ }
31
+ button:hover {
32
+ background: #1d4ed8;
33
+ }
34
+ .test-message {
35
+ background: #dcfce7;
36
+ border: 2px solid #16a34a;
37
+ color: #15803d;
38
+ padding: 1rem;
39
+ border-radius: 6px;
40
+ margin: 1rem 0;
41
+ font-size: 18px;
42
+ font-weight: bold;
43
+ text-align: center;
44
+ }
45
+ .iframe-container {
46
+ background: #e5e7eb;
47
+ padding: 20px;
48
+ border-radius: 8px;
49
+ margin-top: 20px;
50
+ border: 2px solid #d1d5db;
51
+ }
52
+ iframe {
53
+ width: 100%;
54
+ height: 400px;
55
+ border: 2px solid #9ca3af;
56
+ border-radius: 6px;
57
+ background: white;
58
+ }
59
+ .test-controls {
60
+ display: flex;
61
+ gap: 10px;
62
+ margin-bottom: 20px;
63
+ flex-wrap: wrap;
64
+ }
65
+ .log {
66
+ background: #1f2937;
67
+ color: #f9fafb;
68
+ padding: 1rem;
69
+ border-radius: 6px;
70
+ font-family: monospace;
71
+ font-size: 14px;
72
+ max-height: 400px;
73
+ overflow-y: auto;
74
+ white-space: pre-wrap;
75
+ margin-top: 1rem;
76
+ }
77
+ </style>
78
+ </head>
79
+ <body>
80
+ <div class="container">
81
+ <h1>🧪 Dialog Test</h1>
82
+
83
+ <div class="test-message">✅ TEST PAGE LOADED SUCCESSFULLY!</div>
84
+
85
+ <div class="test-controls">
86
+ <button onclick="testDialogs()">🔌 Test Dialogs (Standalone)</button>
87
+ <button onclick="clearLog()">🗑️ Clear Log</button>
88
+ </div>
89
+
90
+ <div class="iframe-container">
91
+ <h3>🖼️ Iframe Environment (Simulates HuggingFace Spaces)</h3>
92
+ <div class="test-controls">
93
+ <button onclick="loadIframe('permissive')">🔓 Permissive</button>
94
+ <button onclick="loadIframe('restricted')">🔒 Restricted</button>
95
+ <button onclick="loadIframe('crossorigin')">🌐 Cross-Origin</button>
96
+ </div>
97
+ <iframe
98
+ id="testFrame"
99
+ src="about:blank"
100
+ title="Dialog Test Iframe"
101
+ ></iframe>
102
+ <div
103
+ id="iframeInfo"
104
+ style="margin-top: 10px; font-size: 12px; color: #666"
105
+ >
106
+ Current iframe mode: None loaded
107
+ </div>
108
+ </div>
109
+
110
+ <div id="log" class="log">Ready to test...</div>
111
+ </div>
112
+
113
+ <script>
114
+ function log(message) {
115
+ const logElement = document.getElementById("log");
116
+ const timestamp = new Date().toLocaleTimeString();
117
+ logElement.textContent += "[" + timestamp + "] " + message + "\n";
118
+ logElement.scrollTop = logElement.scrollHeight;
119
+ }
120
+
121
+ function clearLog() {
122
+ document.getElementById("log").textContent = "Log cleared.\n";
123
+ }
124
+
125
+ function loadIframe(mode) {
126
+ const iframe = document.getElementById("testFrame");
127
+ const infoElement = document.getElementById("iframeInfo");
128
+
129
+ if (!iframe) return;
130
+
131
+ iframe.removeAttribute("sandbox");
132
+ iframe.removeAttribute("allow");
133
+
134
+ let description = "";
135
+
136
+ switch (mode) {
137
+ case "permissive":
138
+ iframe.setAttribute("allow", "serial; usb");
139
+ description = "Permissive: Full access to WebSerial and WebUSB";
140
+ iframe.src = "iframe-content.html";
141
+ break;
142
+ case "restricted":
143
+ iframe.setAttribute(
144
+ "sandbox",
145
+ "allow-scripts allow-same-origin allow-popups allow-forms"
146
+ );
147
+ iframe.setAttribute("allow", "serial; usb");
148
+ description = "Restricted: Sandboxed with limited permissions";
149
+ iframe.src = "iframe-content.html";
150
+ break;
151
+ case "crossorigin":
152
+ iframe.setAttribute(
153
+ "sandbox",
154
+ "allow-scripts allow-popups allow-forms"
155
+ );
156
+ iframe.setAttribute("allow", "serial; usb");
157
+ description =
158
+ "Cross-Origin: Different origin with sandbox restrictions";
159
+ iframe.src = "iframe-content.html";
160
+ break;
161
+ }
162
+
163
+ if (infoElement) {
164
+ infoElement.textContent = "Current iframe mode: " + description;
165
+ }
166
+
167
+ log("📝 Loaded iframe in " + mode + " mode: " + description);
168
+ }
169
+
170
+ async function testDialogs() {
171
+ log("🎯 Starting dialog test...");
172
+
173
+ try {
174
+ log("📡 Requesting WebSerial port...");
175
+ const serialPort = await navigator.serial.requestPort();
176
+ log("✅ WebSerial port selected");
177
+
178
+ log("🔌 Requesting WebUSB device...");
179
+ const usbDevice = await navigator.usb.requestDevice({ filters: [] });
180
+ log("✅ WebUSB device selected - SUCCESS!");
181
+ } catch (error) {
182
+ log("❌ Failed: " + error.message);
183
+ }
184
+ }
185
+
186
+ document.addEventListener("DOMContentLoaded", () => {
187
+ log("✅ Page loaded successfully");
188
+ log(
189
+ "WebSerial: " +
190
+ ("serial" in navigator ? "Supported" : "Not supported")
191
+ );
192
+ log("WebUSB: " + ("usb" in navigator ? "Supported" : "Not supported"));
193
+ });
194
+
195
+ window.addEventListener("message", (event) => {
196
+ if (event.data.type === "iframe-log") {
197
+ log("[IFRAME] " + event.data.message);
198
+ }
199
+ });
200
+ </script>
201
+ </body>
202
+ </html>
examples/iframe-dialog-test/iframe-dialog-test.ts ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Iframe Dialog Test Logic
3
+ * Tests WebSerial + WebUSB dialog behavior in iframe vs standalone environments
4
+ */
5
+
6
+ import { findPort } from "@lerobot/web";
7
+
8
+ let isRunning = false;
9
+
10
+ function log(message: string) {
11
+ const logElement = document.getElementById("log");
12
+ if (logElement) {
13
+ const timestamp = new Date().toLocaleTimeString();
14
+ logElement.textContent += `[${timestamp}] ${message}\n`;
15
+ logElement.scrollTop = logElement.scrollHeight;
16
+ }
17
+ }
18
+
19
+ function setButtonState(buttonId: string, running: boolean) {
20
+ const button = document.getElementById(buttonId) as HTMLButtonElement;
21
+ if (button) {
22
+ button.disabled = running;
23
+ if (running) {
24
+ button.textContent = "⏳ Testing...";
25
+ } else {
26
+ // Restore original text
27
+ if (buttonId === "testStandalone") {
28
+ button.textContent = "🖥️ Test Standalone (Current Window)";
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ // Mock implementation to test dialog behavior without actual findPort
35
+ async function testDialogBehavior(environment: "standalone" | "iframe") {
36
+ log(`🎯 Testing dialog behavior in ${environment} environment...`);
37
+
38
+ try {
39
+ // Method 1: Sequential (current approach)
40
+ log("📡 Method 1: Sequential WebSerial → WebUSB");
41
+
42
+ log("🔸 Requesting WebSerial port...");
43
+ const serialPort = await navigator.serial.requestPort();
44
+ log("✅ WebSerial port selected");
45
+
46
+ log("🔸 Requesting WebUSB device...");
47
+ const usbDevice = await navigator.usb.requestDevice({ filters: [] });
48
+ log("✅ WebUSB device selected");
49
+
50
+ log("🎉 Sequential method succeeded!");
51
+ return { serialPort, usbDevice, method: "sequential" };
52
+ } catch (error: any) {
53
+ log(`❌ Sequential method failed: ${error.message}`);
54
+
55
+ // Method 2: Simultaneous (new approach)
56
+ try {
57
+ log("📡 Method 2: Simultaneous WebSerial + WebUSB");
58
+
59
+ log("🚀 Starting both dialogs simultaneously...");
60
+ const [serialPortPromise, usbDevicePromise] = [
61
+ navigator.serial.requestPort(),
62
+ navigator.usb.requestDevice({ filters: [] }),
63
+ ];
64
+
65
+ log("⏳ Waiting for both dialogs...");
66
+ const [serialPort, usbDevice] = await Promise.all([
67
+ serialPortPromise,
68
+ usbDevicePromise,
69
+ ]);
70
+
71
+ log("✅ Both dialogs completed!");
72
+ log("🎉 Simultaneous method succeeded!");
73
+ return { serialPort, usbDevice, method: "simultaneous" };
74
+ } catch (simultaneousError: any) {
75
+ log(`❌ Simultaneous method also failed: ${simultaneousError.message}`);
76
+ throw simultaneousError;
77
+ }
78
+ }
79
+ }
80
+
81
+ // Test using actual findPort function
82
+ async function testActualFindPort() {
83
+ log("🔍 Testing actual findPort function...");
84
+
85
+ try {
86
+ const findProcess = await findPort();
87
+ const robots = await findProcess.result;
88
+
89
+ log(`✅ findPort succeeded! Found ${robots.length} robots`);
90
+ robots.forEach((robot, index) => {
91
+ log(` Robot ${index + 1}: ${robot.name} (${robot.robotType})`);
92
+ });
93
+
94
+ return robots;
95
+ } catch (error: any) {
96
+ log(`❌ findPort failed: ${error.message}`);
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ declare global {
102
+ interface Window {
103
+ clearLog: () => void;
104
+ testStandalone: () => Promise<void>;
105
+ loadIframe: (mode: string) => void;
106
+ }
107
+ }
108
+
109
+ window.clearLog = function () {
110
+ const logElement = document.getElementById("log");
111
+ if (logElement) {
112
+ logElement.textContent = "Log cleared.\n";
113
+ }
114
+ };
115
+
116
+ window.loadIframe = function (mode: string) {
117
+ const iframe = document.getElementById("testFrame") as HTMLIFrameElement;
118
+ const infoElement = document.getElementById("iframeInfo");
119
+
120
+ if (!iframe) return;
121
+
122
+ // Clear existing attributes
123
+ iframe.removeAttribute("sandbox");
124
+ iframe.removeAttribute("allow");
125
+
126
+ let iframeContent = "";
127
+ let description = "";
128
+
129
+ switch (mode) {
130
+ case "permissive":
131
+ // Most permissive - similar to our original test
132
+ iframe.setAttribute("allow", "serial; usb");
133
+ description = "Permissive: Full access to WebSerial and WebUSB";
134
+ iframeContent = generateIframeContent();
135
+ break;
136
+
137
+ case "restricted":
138
+ // Restricted with sandbox - might block certain permissions
139
+ iframe.setAttribute(
140
+ "sandbox",
141
+ "allow-scripts allow-same-origin allow-popups allow-forms"
142
+ );
143
+ iframe.setAttribute("allow", "serial; usb");
144
+ description = "Restricted: Sandboxed with limited permissions";
145
+ iframeContent = generateIframeContent();
146
+ break;
147
+
148
+ case "crossorigin":
149
+ // Cross-origin simulation (limited local test)
150
+ iframe.setAttribute("sandbox", "allow-scripts allow-popups allow-forms");
151
+ iframe.setAttribute("allow", "serial; usb");
152
+ description = "Cross-Origin: Different origin with sandbox restrictions";
153
+ iframeContent = generateIframeContent();
154
+ break;
155
+ }
156
+
157
+ if (infoElement) {
158
+ infoElement.textContent = `Current iframe mode: ${description}`;
159
+ }
160
+
161
+ iframe.src =
162
+ "data:text/html;charset=utf-8," + encodeURIComponent(iframeContent);
163
+ log(`📝 Loaded iframe in ${mode} mode: ${description}`);
164
+ };
165
+
166
+ window.testStandalone = async function () {
167
+ if (isRunning) return;
168
+
169
+ isRunning = true;
170
+ setButtonState("testStandalone", true);
171
+ log("🧪 Testing with actual findPort function...");
172
+
173
+ try {
174
+ // Test the actual findPort function with our new fallback logic
175
+ await testActualFindPort();
176
+
177
+ log("\n✅ findPort test completed!");
178
+ } catch (error: any) {
179
+ log(`❌ findPort test failed: ${error.message}`);
180
+
181
+ // Analyze the error
182
+ if (error.message.includes("user gesture")) {
183
+ log("🔍 User gesture consumption detected!");
184
+ log("💡 This confirms the issue - WebSerial consumes the gesture");
185
+ } else if (error.message.includes("cancelled")) {
186
+ log("🔍 User cancelled dialog - this is expected for testing");
187
+ } else if (error.message.includes("No port selected")) {
188
+ log("🔍 Dialog conflict detected - this should trigger fallback mode");
189
+ }
190
+ } finally {
191
+ isRunning = false;
192
+ setButtonState("testStandalone", false);
193
+ }
194
+ };
195
+
196
+ // Generate iframe content dynamically
197
+ function generateIframeContent(): string {
198
+ return `
199
+ <!DOCTYPE html>
200
+ <html lang="en">
201
+ <head>
202
+ <meta charset="UTF-8">
203
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
204
+ <title>Iframe Test Content</title>
205
+ <style>
206
+ body {
207
+ font-family: system-ui, sans-serif;
208
+ padding: 20px;
209
+ margin: 0;
210
+ background: white;
211
+ text-align: center;
212
+ }
213
+ button {
214
+ background: #2563eb;
215
+ color: white;
216
+ border: none;
217
+ padding: 10px 20px;
218
+ border-radius: 4px;
219
+ cursor: pointer;
220
+ margin: 5px;
221
+ font-size: 14px;
222
+ }
223
+ button:hover {
224
+ background: #1d4ed8;
225
+ }
226
+ button:disabled {
227
+ background: #9ca3af;
228
+ cursor: not-allowed;
229
+ }
230
+ .status {
231
+ margin: 10px 0;
232
+ padding: 10px;
233
+ background: #f3f4f6;
234
+ border-radius: 4px;
235
+ font-size: 14px;
236
+ }
237
+ </style>
238
+ </head>
239
+ <body>
240
+ <h3>🖼️ Inside Iframe</h3>
241
+ <p>This simulates HuggingFace Spaces environment</p>
242
+
243
+ <button onclick="testActualFindPort()">Test Actual findPort Function</button>
244
+ <button onclick="testIframeDialogs()">Test Mock Sequential</button>
245
+ <button onclick="testIframeSimultaneous()">Test Mock Simultaneous</button>
246
+
247
+ <div id="iframeStatus" class="status">Ready to test...</div>
248
+
249
+ <script type="module">
250
+ import { findPort } from '../packages/web/src/index.js';
251
+
252
+ function updateStatus(message) {
253
+ const status = document.getElementById('iframeStatus');
254
+ const timestamp = new Date().toLocaleTimeString();
255
+ status.textContent = timestamp + ': ' + message;
256
+
257
+ // Also log to parent window
258
+ window.parent.postMessage({
259
+ type: 'iframe-log',
260
+ message: message
261
+ }, '*');
262
+ }
263
+
264
+ window.testIframeDialogs = async function() {
265
+ updateStatus('Testing sequential dialogs...');
266
+
267
+ try {
268
+ updateStatus('Requesting WebSerial port...');
269
+ const serialPort = await navigator.serial.requestPort();
270
+ updateStatus('WebSerial port selected');
271
+
272
+ updateStatus('Requesting WebUSB device...');
273
+ const usbDevice = await navigator.usb.requestDevice({ filters: [] });
274
+ updateStatus('WebUSB device selected - SUCCESS!');
275
+
276
+ } catch (error) {
277
+ updateStatus('Failed: ' + error.message);
278
+ }
279
+ };
280
+
281
+ window.testIframeSimultaneous = async function() {
282
+ updateStatus('Testing simultaneous dialogs...');
283
+
284
+ try {
285
+ updateStatus('Starting both dialogs simultaneously...');
286
+ const [serialPortPromise, usbDevicePromise] = [
287
+ navigator.serial.requestPort(),
288
+ navigator.usb.requestDevice({ filters: [] })
289
+ ];
290
+
291
+ const [serialPort, usbDevice] = await Promise.all([
292
+ serialPortPromise,
293
+ usbDevicePromise
294
+ ]);
295
+
296
+ updateStatus('Both dialogs completed - SUCCESS!');
297
+
298
+ } catch (error) {
299
+ updateStatus('Failed: ' + error.message);
300
+ }
301
+ };
302
+
303
+ // Test actual findPort function (imported at top)
304
+ window.testActualFindPort = async function() {
305
+ updateStatus('Testing actual findPort function...');
306
+
307
+ try {
308
+ updateStatus('Starting findPort with fallback behavior...');
309
+ const findProcess = await findPort({
310
+ onMessage: (msg) => updateStatus('findPort: ' + msg)
311
+ });
312
+
313
+ const robots = await findProcess.result;
314
+ updateStatus('SUCCESS! Found ' + robots.length + ' robots');
315
+
316
+ robots.forEach((robot, index) => {
317
+ updateStatus('Robot ' + (index + 1) + ': ' + robot.name + ' (' + robot.robotType + ')');
318
+ });
319
+
320
+ } catch (error) {
321
+ updateStatus('findPort failed: ' + error.message);
322
+
323
+ // Analyze the error for debugging
324
+ if (error.message.includes('No port selected')) {
325
+ updateStatus('🔍 This should trigger sequential fallback mode');
326
+ } else if (error.message.includes('cancelled')) {
327
+ updateStatus('🔍 User cancelled dialog - expected for testing');
328
+ }
329
+ }
330
+ };
331
+
332
+ // Check environment
333
+ updateStatus('Environment: ' + (window === window.top ? 'Standalone' : 'Iframe'));
334
+ updateStatus('WebSerial: ' + ('serial' in navigator ? 'Supported' : 'Not supported'));
335
+ updateStatus('WebUSB: ' + ('usb' in navigator ? 'Supported' : 'Not supported'));
336
+ </script>
337
+ </body>
338
+ </html>
339
+ `;
340
+ }
341
+
342
+ // Initialize on DOM load
343
+ document.addEventListener("DOMContentLoaded", () => {
344
+ // Check browser support
345
+ if (!("serial" in navigator)) {
346
+ log("❌ WebSerial API not supported in this browser");
347
+ log("💡 Try Chrome/Edge with --enable-web-serial flag");
348
+ const button = document.getElementById(
349
+ "testStandalone"
350
+ ) as HTMLButtonElement;
351
+ if (button) button.disabled = true;
352
+ } else {
353
+ log("✅ WebSerial API supported");
354
+ }
355
+
356
+ if (!("usb" in navigator)) {
357
+ log("❌ WebUSB API not supported in this browser");
358
+ log("💡 Try Chrome/Edge with --enable-web-usb flag");
359
+ } else {
360
+ log("✅ WebUSB API supported");
361
+ }
362
+
363
+ log("🎯 Environment: " + (window === window.top ? "Standalone" : "Iframe"));
364
+ log("Ready to test dialog behavior...");
365
+
366
+ // Set up iframe content - start with permissive mode
367
+ window.loadIframe("permissive");
368
+
369
+ // Listen for messages from iframe
370
+ window.addEventListener("message", (event) => {
371
+ if (event.data.type === "iframe-log") {
372
+ log(`[IFRAME] ${event.data.message}`);
373
+ }
374
+ });
375
+ });
examples/iframe-dialog-test/package.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "iframe-dialog-test",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "Test iframe WebSerial + WebUSB dialog behavior",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "vite",
9
+ "build": "vite build",
10
+ "preview": "vite preview"
11
+ },
12
+ "devDependencies": {
13
+ "@lerobot/web": "workspace:*",
14
+ "typescript": "~5.8.3",
15
+ "vite": "^6.3.5"
16
+ }
17
+ }
examples/iframe-dialog-test/vite.config.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import { resolve } from "path";
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ alias: {
7
+ "@lerobot/web": resolve(__dirname, "../../packages/web/src"),
8
+ },
9
+ },
10
+ build: {
11
+ rollupOptions: {
12
+ input: {
13
+ main: resolve(__dirname, "iframe-dialog-test.html"),
14
+ content: resolve(__dirname, "iframe-content.html"),
15
+ },
16
+ },
17
+ },
18
+ server: {
19
+ open: "/iframe-dialog-test.html",
20
+ },
21
+ });
examples/test-sequential-operations/README.md ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Test Sequential Operations
2
+
3
+ This example tests the complete robotics workflow: findPort → releaseMotors → calibrate → teleoperate.
4
+
5
+ ## Purpose
6
+
7
+ Validates the full API chain and tests all major functions working together in sequence.
8
+
9
+ ## Files
10
+
11
+ - `test-sequential-operations.html` - HTML page with test controls
12
+ - `test-sequential-operations.ts` - TypeScript implementation of sequential testing
13
+
14
+ ## Running
15
+
16
+ From the root directory:
17
+
18
+ ```bash
19
+ pnpm example:sequential-test
20
+ ```
21
+
22
+ Or from this directory:
23
+
24
+ ```bash
25
+ pnpm dev
26
+ ```
27
+
28
+ ## Testing Workflow
29
+
30
+ 1. Click "🚀 Run Sequential Operations Test"
31
+ 2. Workflow executes:
32
+ - **findPort()** - Discovers and connects to robot
33
+ - **releaseMotors()** - Releases motor torque for free movement
34
+ - **calibrate()** - Records motor ranges (auto-stops after 8 seconds)
35
+ - **teleoperate()** - Starts keyboard control with auto key simulation
36
+
37
+ ## Expected Behavior
38
+
39
+ - Robot connection established
40
+ - Motors released for calibration setup
41
+ - Live calibration updates showing motor positions
42
+ - Automatic teleoperation with simulated key presses
43
+ - Auto-stop after test completion
44
+
45
+ ## Browser Requirements
46
+
47
+ - Chrome/Edge 89+ with WebSerial and WebUSB APIs
48
+ - HTTPS or localhost
49
+ - Connected SO-100 robot arm for full testing
examples/test-sequential-operations/package.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "test-sequential-operations",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "Test sequential operations: findPort → releaseMotors → calibrate → teleoperate",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "vite",
9
+ "build": "vite build",
10
+ "preview": "vite preview"
11
+ },
12
+ "devDependencies": {
13
+ "@lerobot/web": "workspace:*",
14
+ "typescript": "~5.8.3",
15
+ "vite": "^6.3.5"
16
+ }
17
+ }
examples/{test-sequential-operations.html → test-sequential-operations/test-sequential-operations.html} RENAMED
File without changes
examples/{test-sequential-operations.ts → test-sequential-operations/test-sequential-operations.ts} RENAMED
@@ -132,7 +132,8 @@ window.runSequentialTest = async function () {
132
  log("✅ Calibration completed (simulated)");
133
  } else {
134
  // Real calibration
135
- const calibrationProcess = await calibrate(robot, {
 
136
  onProgress: (message) => log(`📊 ${message}`),
137
  onLiveUpdate: (data) => {
138
  const motors = Object.keys(data);
@@ -210,12 +211,21 @@ window.runSequentialTest = async function () {
210
 
211
  // Step 4: Teleoperate with auto key simulation
212
  log("\n4️⃣ Starting teleoperation...");
213
- const teleoperationProcess = await teleoperate(robot, {
 
214
  calibrationData: calibrationResult,
 
 
 
 
215
  onStateUpdate: (state) => {
216
- if (state.isActive && Object.keys(state.keyStates).length > 0) {
 
 
 
 
217
  const activeKeys = Object.keys(state.keyStates).filter(
218
- (k) => state.keyStates[k].pressed
219
  );
220
  log(`🎮 Auto-simulated keys: ${activeKeys.join(", ")}`);
221
  }
 
132
  log("✅ Calibration completed (simulated)");
133
  } else {
134
  // Real calibration
135
+ const calibrationProcess = await calibrate({
136
+ robot,
137
  onProgress: (message) => log(`📊 ${message}`),
138
  onLiveUpdate: (data) => {
139
  const motors = Object.keys(data);
 
211
 
212
  // Step 4: Teleoperate with auto key simulation
213
  log("\n4️⃣ Starting teleoperation...");
214
+ const teleoperationProcess = await teleoperate({
215
+ robot,
216
  calibrationData: calibrationResult,
217
+ teleop: {
218
+ type: "keyboard",
219
+ stepSize: 25,
220
+ },
221
  onStateUpdate: (state) => {
222
+ if (
223
+ state.isActive &&
224
+ state.keyStates &&
225
+ Object.keys(state.keyStates).length > 0
226
+ ) {
227
  const activeKeys = Object.keys(state.keyStates).filter(
228
+ (k) => state.keyStates![k].pressed
229
  );
230
  log(`🎮 Auto-simulated keys: ${activeKeys.join(", ")}`);
231
  }
examples/test-sequential-operations/vite.config.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import { resolve } from "path";
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ alias: {
7
+ "@lerobot/web": resolve(__dirname, "../../packages/web/src"),
8
+ },
9
+ },
10
+ build: {
11
+ rollupOptions: {
12
+ input: {
13
+ main: resolve(__dirname, "test-sequential-operations.html"),
14
+ },
15
+ },
16
+ },
17
+ server: {
18
+ open: "/test-sequential-operations.html",
19
+ },
20
+ });
package.json CHANGED
@@ -4,7 +4,10 @@
4
  "description": "State-of-the-art AI for real-world robotics in JS",
5
  "type": "module",
6
  "workspaces": [
7
- "packages/*"
 
 
 
8
  ],
9
  "bin": {
10
  "lerobot": "./dist/cli/index.js"
@@ -24,6 +27,8 @@
24
  "scripts": {
25
  "dev": "vite --mode demo",
26
  "example:cyberpunk": "cd examples/cyberpunk-standalone && pnpm dev",
 
 
27
  "build": "pnpm run build:cli",
28
  "build:cli": "tsc --project tsconfig.cli.json",
29
  "build:demo": "tsc && vite build --mode demo",
 
4
  "description": "State-of-the-art AI for real-world robotics in JS",
5
  "type": "module",
6
  "workspaces": [
7
+ "packages/*",
8
+ "examples/cyberpunk-standalone",
9
+ "examples/iframe-dialog-test",
10
+ "examples/test-sequential-operations"
11
  ],
12
  "bin": {
13
  "lerobot": "./dist/cli/index.js"
 
27
  "scripts": {
28
  "dev": "vite --mode demo",
29
  "example:cyberpunk": "cd examples/cyberpunk-standalone && pnpm dev",
30
+ "example:iframe-test": "cd examples/iframe-dialog-test && pnpm dev",
31
+ "example:sequential-test": "cd examples/test-sequential-operations && pnpm dev",
32
  "build": "pnpm run build:cli",
33
  "build:cli": "tsc --project tsconfig.cli.json",
34
  "build:demo": "tsc && vite build --mode demo",
vite.config.ts CHANGED
@@ -54,27 +54,6 @@ export default defineConfig(({ mode }) => {
54
  };
55
  }
56
 
57
- if (mode === "test") {
58
- // Test mode - sequential operations test
59
- return {
60
- ...baseConfig,
61
- server: {
62
- open: "/examples/test-sequential-operations.html",
63
- },
64
- build: {
65
- outDir: "dist/test",
66
- rollupOptions: {
67
- input: {
68
- main: resolve(
69
- __dirname,
70
- "examples/test-sequential-operations.html"
71
- ),
72
- },
73
- },
74
- },
75
- };
76
- }
77
-
78
  if (mode === "lib") {
79
  // Library mode - core library without any demo UI
80
  return {
 
54
  };
55
  }
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  if (mode === "lib") {
58
  // Library mode - core library without any demo UI
59
  return {