react-native-web-serial-api 0.0.3 → 0.2.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.
Files changed (177) hide show
  1. package/README.md +198 -104
  2. package/TESTING.md +542 -0
  3. package/android/build.gradle +16 -2
  4. package/android/src/main/java/dev/webserialapi/NativeUsbSerialModule.java +74 -11
  5. package/android/src/main/java/dev/webserialapi/PortPickerActivity.java +61 -59
  6. package/bin/expose-serial.js +205 -0
  7. package/lib/commonjs/UsbSerial.js +58 -26
  8. package/lib/commonjs/UsbSerial.js.map +1 -1
  9. package/lib/commonjs/WebSerial.js +273 -77
  10. package/lib/commonjs/WebSerial.js.map +1 -1
  11. package/lib/commonjs/index.js +15 -3
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/lib/dom-exception.js +176 -0
  14. package/lib/commonjs/lib/dom-exception.js.map +1 -0
  15. package/lib/commonjs/lib/event-target.js +140 -0
  16. package/lib/commonjs/lib/event-target.js.map +1 -0
  17. package/lib/commonjs/lib/promise.js +23 -0
  18. package/lib/commonjs/lib/promise.js.map +1 -0
  19. package/lib/commonjs/lib/web-streams.js +42 -0
  20. package/lib/commonjs/lib/web-streams.js.map +1 -0
  21. package/lib/commonjs/testing/device-fixture.js +70 -0
  22. package/lib/commonjs/testing/device-fixture.js.map +1 -0
  23. package/lib/commonjs/testing/expose.js +91 -0
  24. package/lib/commonjs/testing/expose.js.map +1 -0
  25. package/lib/commonjs/testing/harness.js +98 -0
  26. package/lib/commonjs/testing/harness.js.map +1 -0
  27. package/lib/commonjs/testing/in-memory-serial-transport.js +653 -0
  28. package/lib/commonjs/testing/in-memory-serial-transport.js.map +1 -0
  29. package/lib/commonjs/testing/index.js +153 -0
  30. package/lib/commonjs/testing/index.js.map +1 -0
  31. package/lib/commonjs/testing/install-in-memory-serial-transport.js +54 -0
  32. package/lib/commonjs/testing/install-in-memory-serial-transport.js.map +1 -0
  33. package/lib/commonjs/testing/serial-client.js +277 -0
  34. package/lib/commonjs/testing/serial-client.js.map +1 -0
  35. package/lib/commonjs/testing/simulated-device.js +164 -0
  36. package/lib/commonjs/testing/simulated-device.js.map +1 -0
  37. package/lib/commonjs/testing/test-suite.js +142 -0
  38. package/lib/commonjs/testing/test-suite.js.map +1 -0
  39. package/lib/commonjs/transport.js +61 -0
  40. package/lib/commonjs/transport.js.map +1 -0
  41. package/lib/commonjs/websocket/WebSocketSerialTransport.js +659 -0
  42. package/lib/commonjs/websocket/WebSocketSerialTransport.js.map +1 -0
  43. package/lib/commonjs/websocket/bridge.js +234 -0
  44. package/lib/commonjs/websocket/bridge.js.map +1 -0
  45. package/lib/commonjs/websocket/index.js +33 -0
  46. package/lib/commonjs/websocket/index.js.map +1 -0
  47. package/lib/commonjs/websocket/protocol.js +55 -0
  48. package/lib/commonjs/websocket/protocol.js.map +1 -0
  49. package/lib/commonjs/websocket/serial-device-bridge.js +130 -0
  50. package/lib/commonjs/websocket/serial-device-bridge.js.map +1 -0
  51. package/lib/typescript/src/UsbSerial.d.ts +24 -67
  52. package/lib/typescript/src/UsbSerial.d.ts.map +1 -1
  53. package/lib/typescript/src/WebSerial.d.ts +16 -7
  54. package/lib/typescript/src/WebSerial.d.ts.map +1 -1
  55. package/lib/typescript/src/index.d.ts +3 -1
  56. package/lib/typescript/src/index.d.ts.map +1 -1
  57. package/lib/typescript/src/lib/dom-exception.d.ts +100 -0
  58. package/lib/typescript/src/lib/dom-exception.d.ts.map +1 -0
  59. package/lib/typescript/src/lib/event-target.d.ts +55 -0
  60. package/lib/typescript/src/lib/event-target.d.ts.map +1 -0
  61. package/lib/typescript/src/lib/promise.d.ts +11 -0
  62. package/lib/typescript/src/lib/promise.d.ts.map +1 -0
  63. package/lib/typescript/src/lib/web-streams.d.ts +9 -0
  64. package/lib/typescript/src/lib/web-streams.d.ts.map +1 -0
  65. package/lib/typescript/src/testing/device-fixture.d.ts +70 -0
  66. package/lib/typescript/src/testing/device-fixture.d.ts.map +1 -0
  67. package/lib/typescript/src/testing/expose.d.ts +71 -0
  68. package/lib/typescript/src/testing/expose.d.ts.map +1 -0
  69. package/lib/typescript/src/testing/harness.d.ts +34 -0
  70. package/lib/typescript/src/testing/harness.d.ts.map +1 -0
  71. package/lib/typescript/src/testing/in-memory-serial-transport.d.ts +216 -0
  72. package/lib/typescript/src/testing/in-memory-serial-transport.d.ts.map +1 -0
  73. package/lib/typescript/src/testing/index.d.ts +33 -0
  74. package/lib/typescript/src/testing/index.d.ts.map +1 -0
  75. package/lib/typescript/src/testing/install-in-memory-serial-transport.d.ts +25 -0
  76. package/lib/typescript/src/testing/install-in-memory-serial-transport.d.ts.map +1 -0
  77. package/lib/typescript/src/testing/serial-client.d.ts +62 -0
  78. package/lib/typescript/src/testing/serial-client.d.ts.map +1 -0
  79. package/lib/typescript/src/testing/simulated-device.d.ts +127 -0
  80. package/lib/typescript/src/testing/simulated-device.d.ts.map +1 -0
  81. package/lib/typescript/src/testing/test-suite.d.ts +75 -0
  82. package/lib/typescript/src/testing/test-suite.d.ts.map +1 -0
  83. package/lib/typescript/src/transport.d.ts +131 -0
  84. package/lib/typescript/src/transport.d.ts.map +1 -0
  85. package/lib/typescript/src/websocket/WebSocketSerialTransport.d.ts +111 -0
  86. package/lib/typescript/src/websocket/WebSocketSerialTransport.d.ts.map +1 -0
  87. package/lib/typescript/src/websocket/bridge.d.ts +66 -0
  88. package/lib/typescript/src/websocket/bridge.d.ts.map +1 -0
  89. package/lib/typescript/src/websocket/index.d.ts +19 -0
  90. package/lib/typescript/src/websocket/index.d.ts.map +1 -0
  91. package/lib/typescript/src/websocket/protocol.d.ts +64 -0
  92. package/lib/typescript/src/websocket/protocol.d.ts.map +1 -0
  93. package/lib/typescript/src/websocket/serial-device-bridge.d.ts +32 -0
  94. package/lib/typescript/src/websocket/serial-device-bridge.d.ts.map +1 -0
  95. package/package.json +57 -3
  96. package/src/UsbSerial.ts +65 -90
  97. package/src/WebSerial.ts +351 -113
  98. package/src/index.ts +6 -8
  99. package/src/lib/dom-exception.ts +129 -60
  100. package/src/lib/event-target.ts +58 -21
  101. package/src/lib/promise.ts +7 -7
  102. package/src/lib/web-streams.ts +43 -0
  103. package/src/testing/device-fixture.ts +150 -0
  104. package/src/testing/expose.ts +147 -0
  105. package/src/testing/harness.ts +124 -0
  106. package/src/testing/in-memory-serial-transport.ts +840 -0
  107. package/src/testing/index.ts +90 -0
  108. package/src/testing/install-in-memory-serial-transport.ts +65 -0
  109. package/src/testing/serial-client.ts +313 -0
  110. package/src/testing/simulated-device.ts +193 -0
  111. package/src/testing/test-suite.ts +186 -0
  112. package/src/transport.ts +200 -0
  113. package/src/websocket/WebSocketSerialTransport.ts +796 -0
  114. package/src/websocket/bridge.ts +299 -0
  115. package/src/websocket/index.ts +38 -0
  116. package/src/websocket/protocol.ts +101 -0
  117. package/src/websocket/serial-device-bridge.ts +160 -0
  118. package/babel.config.js +0 -3
  119. package/biome.json +0 -35
  120. package/example/.watchmanconfig +0 -1
  121. package/example/App.tsx +0 -71
  122. package/example/__tests__/App.test.tsx +0 -16
  123. package/example/__tests__/connectEvents.test.tsx +0 -81
  124. package/example/__tests__/getPorts.test.tsx +0 -140
  125. package/example/android/app/build.gradle +0 -120
  126. package/example/android/app/debug.keystore +0 -0
  127. package/example/android/app/proguard-rules.pro +0 -10
  128. package/example/android/app/src/debug/AndroidManifest.xml +0 -9
  129. package/example/android/app/src/main/AndroidManifest.xml +0 -38
  130. package/example/android/app/src/main/java/dev/uzlopak/MainActivity.kt +0 -22
  131. package/example/android/app/src/main/java/dev/uzlopak/MainApplication.kt +0 -41
  132. package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +0 -37
  133. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  134. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  135. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  136. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  137. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  138. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  139. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  140. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  141. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  142. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  143. package/example/android/app/src/main/res/values/strings.xml +0 -3
  144. package/example/android/app/src/main/res/values/styles.xml +0 -9
  145. package/example/android/build.gradle +0 -22
  146. package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  147. package/example/android/gradle/wrapper/gradle-wrapper.properties +0 -7
  148. package/example/android/gradle.properties +0 -47
  149. package/example/android/gradlew +0 -252
  150. package/example/android/gradlew.bat +0 -94
  151. package/example/android/settings.gradle +0 -6
  152. package/example/app.json +0 -4
  153. package/example/babel.config.js +0 -21
  154. package/example/biome.json +0 -47
  155. package/example/deploy.sh +0 -11
  156. package/example/index.html +0 -26
  157. package/example/index.js +0 -9
  158. package/example/index.web.js +0 -8
  159. package/example/jest.config.js +0 -12
  160. package/example/metro.config.js +0 -58
  161. package/example/package-lock.json +0 -14510
  162. package/example/package.json +0 -48
  163. package/example/react-native.config.js +0 -17
  164. package/example/src/components/AppBar.tsx +0 -73
  165. package/example/src/components/Menu.tsx +0 -90
  166. package/example/src/components/SingleChoiceDialog.tsx +0 -120
  167. package/example/src/screens/ConnectScreen.tsx +0 -195
  168. package/example/src/screens/DevicesScreen.tsx +0 -252
  169. package/example/src/screens/TerminalScreen.tsx +0 -572
  170. package/example/src/settings.ts +0 -43
  171. package/example/src/theme.ts +0 -19
  172. package/example/src/util/TextUtil.ts +0 -129
  173. package/example/tsconfig.json +0 -10
  174. package/example/vite.config.mjs +0 -55
  175. package/scripts/deploy-release.sh +0 -127
  176. package/tsconfig.build.json +0 -7
  177. package/tsconfig.json +0 -20
package/src/index.ts CHANGED
@@ -1,7 +1,6 @@
1
- // Platform-resolved Web Serial API instance.
2
- // On React Native (Android) this is the USB-serial-backed polyfill; on web it
3
- // is the browser's native navigator.serial. (See serial.android.ts / serial.web.ts)
4
1
  export {default as serial} from './serial';
2
+ export type {SerialTransport} from './transport';
3
+ export {resetUsbSerial, setUsbSerial} from './UsbSerial';
5
4
  export type {
6
5
  SerialInputSignals,
7
6
  SerialOptions,
@@ -13,11 +12,10 @@ export type {
13
12
  // W3C Web Serial API classes
14
13
  export {Serial, SerialPort} from './WebSerial';
15
14
 
16
- // Lower-level access to the raw USB-serial TurboModule (Android only).
17
- // (Imported + re-exported rather than `export * as` so older Babel presets
18
- // without @babel/plugin-transform-export-namespace-from can consume the source.)
19
15
  import * as UsbSerial from './UsbSerial';
20
16
 
21
- // Web Serial API event/exception primitives used by the polyfill
22
- export {Event, EventTarget} from './lib/event-target';
17
+ export {
18
+ EventImpl as Event,
19
+ EventTargetImpl as EventTarget,
20
+ } from './lib/event-target';
23
21
  export {UsbSerial};
@@ -33,7 +33,7 @@ const DOM_EXCEPTION_CODES: Record<string, number> = {
33
33
  };
34
34
 
35
35
  const DOM_EXCEPTION_NAMES: Record<number, string> = Object.fromEntries(
36
- Object.entries(DOM_EXCEPTION_CODES).map(([name, code]) => [code, name])
36
+ Object.entries(DOM_EXCEPTION_CODES).map(([name, code]) => [code, name]),
37
37
  );
38
38
 
39
39
  export interface DOMExceptionConstructor {
@@ -78,74 +78,134 @@ class DOMExceptionPolyfill extends Error {
78
78
  readonly name: string;
79
79
 
80
80
  // --- Static legacy-code constants ---
81
- static readonly INDEX_SIZE_ERR = 1 as const;
82
- static readonly DOMSTRING_SIZE_ERR = 2 as const;
83
- static readonly HIERARCHY_REQUEST_ERR = 3 as const;
84
- static readonly WRONG_DOCUMENT_ERR = 4 as const;
85
- static readonly INVALID_CHARACTER_ERR = 5 as const;
86
- static readonly NO_DATA_ALLOWED_ERR = 6 as const;
81
+ static readonly INDEX_SIZE_ERR = 1 as const;
82
+ static readonly DOMSTRING_SIZE_ERR = 2 as const;
83
+ static readonly HIERARCHY_REQUEST_ERR = 3 as const;
84
+ static readonly WRONG_DOCUMENT_ERR = 4 as const;
85
+ static readonly INVALID_CHARACTER_ERR = 5 as const;
86
+ static readonly NO_DATA_ALLOWED_ERR = 6 as const;
87
87
  static readonly NO_MODIFICATION_ALLOWED_ERR = 7 as const;
88
- static readonly NOT_FOUND_ERR = 8 as const;
89
- static readonly NOT_SUPPORTED_ERR = 9 as const;
90
- static readonly INUSE_ATTRIBUTE_ERR = 10 as const;
91
- static readonly INVALID_STATE_ERR = 11 as const;
92
- static readonly SYNTAX_ERR = 12 as const;
93
- static readonly INVALID_MODIFICATION_ERR = 13 as const;
94
- static readonly NAMESPACE_ERR = 14 as const;
95
- static readonly INVALID_ACCESS_ERR = 15 as const;
96
- static readonly VALIDATION_ERR = 16 as const;
97
- static readonly TYPE_MISMATCH_ERR = 17 as const;
98
- static readonly SECURITY_ERR = 18 as const;
99
- static readonly NETWORK_ERR = 19 as const;
100
- static readonly ABORT_ERR = 20 as const;
101
- static readonly URL_MISMATCH_ERR = 21 as const;
102
- static readonly QUOTA_EXCEEDED_ERR = 22 as const;
103
- static readonly TIMEOUT_ERR = 23 as const;
104
- static readonly INVALID_NODE_TYPE_ERR = 24 as const;
105
- static readonly DATA_CLONE_ERR = 25 as const;
88
+ static readonly NOT_FOUND_ERR = 8 as const;
89
+ static readonly NOT_SUPPORTED_ERR = 9 as const;
90
+ static readonly INUSE_ATTRIBUTE_ERR = 10 as const;
91
+ static readonly INVALID_STATE_ERR = 11 as const;
92
+ static readonly SYNTAX_ERR = 12 as const;
93
+ static readonly INVALID_MODIFICATION_ERR = 13 as const;
94
+ static readonly NAMESPACE_ERR = 14 as const;
95
+ static readonly INVALID_ACCESS_ERR = 15 as const;
96
+ static readonly VALIDATION_ERR = 16 as const;
97
+ static readonly TYPE_MISMATCH_ERR = 17 as const;
98
+ static readonly SECURITY_ERR = 18 as const;
99
+ static readonly NETWORK_ERR = 19 as const;
100
+ static readonly ABORT_ERR = 20 as const;
101
+ static readonly URL_MISMATCH_ERR = 21 as const;
102
+ static readonly QUOTA_EXCEEDED_ERR = 22 as const;
103
+ static readonly TIMEOUT_ERR = 23 as const;
104
+ static readonly INVALID_NODE_TYPE_ERR = 24 as const;
105
+ static readonly DATA_CLONE_ERR = 25 as const;
106
106
 
107
- constructor(message = "", name = "Error") {
107
+ constructor(message = '', name = 'Error') {
108
108
  super(message);
109
109
 
110
110
  // Restore the correct prototype chain when transpiled to ES5
111
111
  Object.setPrototypeOf(this, new.target.prototype);
112
112
 
113
- this.name = name;
113
+ this.name = name;
114
114
  this.message = message;
115
- this.code = DOM_EXCEPTION_CODES[name] ?? 0;
115
+ this.code = DOM_EXCEPTION_CODES[name] ?? 0;
116
116
 
117
117
  // Provide a useful stack trace in V8 / SpiderMonkey
118
- if (typeof (Error as any).captureStackTrace === "function") {
119
- (Error as any).captureStackTrace(this, new.target);
118
+ const ErrorWithStack = Error as ErrorConstructor & {
119
+ captureStackTrace?: (
120
+ target: object,
121
+ constructorOpt?:
122
+ | ((...args: never[]) => unknown)
123
+ | (abstract new (
124
+ ...args: never[]
125
+ ) => unknown),
126
+ ) => void;
127
+ };
128
+ if (typeof ErrorWithStack.captureStackTrace === 'function') {
129
+ ErrorWithStack.captureStackTrace(this, new.target);
120
130
  }
121
131
  }
122
132
 
123
133
  /** Mirror static constants on the prototype (spec §3.1.2) */
124
- get INDEX_SIZE_ERR() { return 1; }
125
- get DOMSTRING_SIZE_ERR() { return 2; }
126
- get HIERARCHY_REQUEST_ERR() { return 3; }
127
- get WRONG_DOCUMENT_ERR() { return 4; }
128
- get INVALID_CHARACTER_ERR() { return 5; }
129
- get NO_DATA_ALLOWED_ERR() { return 6; }
130
- get NO_MODIFICATION_ALLOWED_ERR() { return 7; }
131
- get NOT_FOUND_ERR() { return 8; }
132
- get NOT_SUPPORTED_ERR() { return 9; }
133
- get INUSE_ATTRIBUTE_ERR() { return 10; }
134
- get INVALID_STATE_ERR() { return 11; }
135
- get SYNTAX_ERR() { return 12; }
136
- get INVALID_MODIFICATION_ERR() { return 13; }
137
- get NAMESPACE_ERR() { return 14; }
138
- get INVALID_ACCESS_ERR() { return 15; }
139
- get VALIDATION_ERR() { return 16; }
140
- get TYPE_MISMATCH_ERR() { return 17; }
141
- get SECURITY_ERR() { return 18; }
142
- get NETWORK_ERR() { return 19; }
143
- get ABORT_ERR() { return 20; }
144
- get URL_MISMATCH_ERR() { return 21; }
145
- get QUOTA_EXCEEDED_ERR() { return 22; }
146
- get TIMEOUT_ERR() { return 23; }
147
- get INVALID_NODE_TYPE_ERR() { return 24; }
148
- get DATA_CLONE_ERR() { return 25; }
134
+ get INDEX_SIZE_ERR() {
135
+ return 1;
136
+ }
137
+ get DOMSTRING_SIZE_ERR() {
138
+ return 2;
139
+ }
140
+ get HIERARCHY_REQUEST_ERR() {
141
+ return 3;
142
+ }
143
+ get WRONG_DOCUMENT_ERR() {
144
+ return 4;
145
+ }
146
+ get INVALID_CHARACTER_ERR() {
147
+ return 5;
148
+ }
149
+ get NO_DATA_ALLOWED_ERR() {
150
+ return 6;
151
+ }
152
+ get NO_MODIFICATION_ALLOWED_ERR() {
153
+ return 7;
154
+ }
155
+ get NOT_FOUND_ERR() {
156
+ return 8;
157
+ }
158
+ get NOT_SUPPORTED_ERR() {
159
+ return 9;
160
+ }
161
+ get INUSE_ATTRIBUTE_ERR() {
162
+ return 10;
163
+ }
164
+ get INVALID_STATE_ERR() {
165
+ return 11;
166
+ }
167
+ get SYNTAX_ERR() {
168
+ return 12;
169
+ }
170
+ get INVALID_MODIFICATION_ERR() {
171
+ return 13;
172
+ }
173
+ get NAMESPACE_ERR() {
174
+ return 14;
175
+ }
176
+ get INVALID_ACCESS_ERR() {
177
+ return 15;
178
+ }
179
+ get VALIDATION_ERR() {
180
+ return 16;
181
+ }
182
+ get TYPE_MISMATCH_ERR() {
183
+ return 17;
184
+ }
185
+ get SECURITY_ERR() {
186
+ return 18;
187
+ }
188
+ get NETWORK_ERR() {
189
+ return 19;
190
+ }
191
+ get ABORT_ERR() {
192
+ return 20;
193
+ }
194
+ get URL_MISMATCH_ERR() {
195
+ return 21;
196
+ }
197
+ get QUOTA_EXCEEDED_ERR() {
198
+ return 22;
199
+ }
200
+ get TIMEOUT_ERR() {
201
+ return 23;
202
+ }
203
+ get INVALID_NODE_TYPE_ERR() {
204
+ return 24;
205
+ }
206
+ get DATA_CLONE_ERR() {
207
+ return 25;
208
+ }
149
209
 
150
210
  /** Canonical string representation */
151
211
  toString(): string {
@@ -153,9 +213,18 @@ class DOMExceptionPolyfill extends Error {
153
213
  }
154
214
  }
155
215
 
156
- export const DOMExceptionImpl = (globalThis as any).DOMException
157
- ? (globalThis as any).DOMException as DOMExceptionConstructor
158
- : DOMExceptionPolyfill as DOMExceptionConstructor;
216
+ export const DOMExceptionImpl = (globalThis as Record<string, unknown>)
217
+ .DOMException
218
+ ? ((globalThis as Record<string, unknown>)
219
+ .DOMException as DOMExceptionConstructor)
220
+ : (DOMExceptionPolyfill as DOMExceptionConstructor);
159
221
 
160
- export { DOM_EXCEPTION_CODES, DOM_EXCEPTION_NAMES };
161
- export { DOMExceptionImpl as DOMException };
222
+ export {
223
+ DOM_EXCEPTION_CODES,
224
+ DOM_EXCEPTION_NAMES,
225
+ DOMExceptionImpl as DOMException,
226
+ // Exported for testing: in environments that already provide a global
227
+ // DOMException (Node, modern browsers) the export above is the native one, so
228
+ // the polyfill class would otherwise never run.
229
+ DOMExceptionPolyfill,
230
+ };
@@ -1,4 +1,3 @@
1
-
2
1
  export interface EventInit {
3
2
  bubbles?: boolean;
4
3
  cancelable?: boolean;
@@ -66,7 +65,7 @@ export class Event {
66
65
  }
67
66
  }
68
67
 
69
- type Mutable<T> = { -readonly [P in keyof T]: T[P] };
68
+ type Mutable<T> = {-readonly [P in keyof T]: T[P]};
70
69
 
71
70
  type EventListener = (event: Event) => void;
72
71
 
@@ -77,7 +76,7 @@ interface EventListenerObject {
77
76
  type EventListenerOrEventListenerObject = EventListener | EventListenerObject;
78
77
 
79
78
  type ListenerOptions =
80
- | { once?: boolean; capture?: boolean; passive?: boolean }
79
+ | {once?: boolean; capture?: boolean; passive?: boolean}
81
80
  | boolean
82
81
  | undefined;
83
82
 
@@ -91,7 +90,23 @@ type SecretMap = Record<string, ListenerInfo[]>;
91
90
 
92
91
  const wm = new WeakMap<object, SecretMap>();
93
92
 
94
- function define<T extends object>(target: T, name: string, value: unknown): void {
93
+ // Optional event-propagation parents. An event dispatched on a child also runs
94
+ // its parent's listeners, while event.target stays the original child. This
95
+ // models the W3C "SerialPort's parent is the Serial" relationship, so a
96
+ // connect/disconnect observed on `serial` reports event.target === the
97
+ // SerialPort (per the Web Serial spec and WPT).
98
+ const eventParents = new WeakMap<EventTarget, EventTarget>();
99
+
100
+ /** Make events dispatched on `child` also bubble to `parent`'s listeners. */
101
+ export function setEventParent(child: EventTarget, parent: EventTarget): void {
102
+ eventParents.set(child, parent);
103
+ }
104
+
105
+ function define<T extends object>(
106
+ target: T,
107
+ name: string,
108
+ value: unknown,
109
+ ): void {
95
110
  Object.defineProperty(target, name, {
96
111
  configurable: true,
97
112
  writable: true,
@@ -102,13 +117,13 @@ function define<T extends object>(target: T, name: string, value: unknown): void
102
117
  function dispatch(this: Event, info: ListenerInfo): boolean {
103
118
  const options = info.options;
104
119
  const once =
105
- typeof options === "object" && options !== null ? options.once : false;
120
+ typeof options === 'object' && options !== null ? options.once : false;
106
121
 
107
122
  if (once) {
108
123
  info.target.removeEventListener(this.type, info.listener);
109
124
  }
110
125
 
111
- if (typeof info.listener === "function") {
126
+ if (typeof info.listener === 'function') {
112
127
  info.listener.call(info.target, this);
113
128
  } else {
114
129
  info.listener.handleEvent(this);
@@ -125,40 +140,50 @@ export class EventTarget {
125
140
  addEventListener(
126
141
  type: string,
127
142
  listener: EventListenerOrEventListenerObject,
128
- options?: ListenerOptions
143
+ options?: ListenerOptions,
129
144
  ): void {
130
145
  const secret = wm.get(this)!;
131
- const listeners: ListenerInfo[] = secret[type] ?? (secret[type] = []);
146
+ let listeners = secret[type];
147
+ if (!listeners) {
148
+ listeners = [];
149
+ secret[type] = listeners;
150
+ }
132
151
 
133
152
  for (let i = 0; i < listeners.length; i++) {
134
153
  if (listeners[i].listener === listener) return;
135
154
  }
136
155
 
137
- listeners.push({ target: this, listener, options });
156
+ listeners.push({target: this, listener, options});
138
157
  }
139
158
 
140
159
  dispatchEvent(event: Event): boolean {
141
- const secret = wm.get(this)!;
142
- const listeners = secret[event.type];
143
-
144
- if (listeners) {
145
- define(event, "target", this);
146
- define(event, "currentTarget", this);
147
- listeners.slice(0).some(dispatch, event);
148
- define(event, "target", null);
149
- define(event, "currentTarget", null);
160
+ // The event's target is the node it was dispatched on; it then bubbles up
161
+ // the eventParents chain (currentTarget changes, target does not).
162
+ define(event, 'target', this);
163
+ let node: EventTarget | null = this;
164
+ while (node) {
165
+ const listeners = wm.get(node)?.[event.type];
166
+ if (listeners?.length) {
167
+ define(event, 'currentTarget', node);
168
+ listeners.slice(0).some(dispatch, event);
169
+ }
170
+ if (event.cancelBubble) break;
171
+ node = eventParents.get(node) ?? null;
150
172
  }
151
-
173
+ // Per the DOM, target persists after dispatch (a re-dispatch overwrites it
174
+ // at the top of this method); only currentTarget is cleared.
175
+ define(event, 'currentTarget', null);
152
176
  return true;
153
177
  }
154
178
 
155
179
  removeEventListener(
156
180
  type: string,
157
181
  listener: EventListenerOrEventListenerObject,
158
- _options?: ListenerOptions
182
+ _options?: ListenerOptions,
159
183
  ): void {
160
184
  const secret = wm.get(this)!;
161
- const listeners: ListenerInfo[] = secret[type] ?? (secret[type] = []);
185
+ const listeners = secret[type];
186
+ if (!listeners) return;
162
187
 
163
188
  for (let i = 0; i < listeners.length; i++) {
164
189
  if (listeners[i].listener === listener) {
@@ -168,3 +193,15 @@ export class EventTarget {
168
193
  }
169
194
  }
170
195
  }
196
+
197
+ type MaybeGlobal = Record<string, unknown>;
198
+
199
+ export const EventImpl =
200
+ typeof (globalThis as MaybeGlobal).Event === 'function'
201
+ ? ((globalThis as MaybeGlobal).Event as typeof Event)
202
+ : Event;
203
+
204
+ export const EventTargetImpl =
205
+ typeof (globalThis as MaybeGlobal).EventTarget === 'function'
206
+ ? ((globalThis as MaybeGlobal).EventTarget as typeof EventTarget)
207
+ : EventTarget;
@@ -1,19 +1,19 @@
1
1
  type DeferredPromise<T> = {
2
2
  promise: Promise<T>;
3
3
  resolve: (value?: T) => void;
4
- reject: (reason?: any) => void;
4
+ reject: (reason?: unknown) => void;
5
5
  };
6
6
 
7
7
  /**
8
8
  * @returns An object containing a promise and its resolve/reject methods.
9
9
  */
10
10
  export function createDeferredPromise<T>(): DeferredPromise<T> {
11
- let res: any;
12
- let rej: any;
11
+ let res: unknown;
12
+ let rej: unknown;
13
13
  const promise = new Promise<T>((resolve, reject) => {
14
- res = resolve
15
- rej = reject
16
- })
14
+ res = resolve;
15
+ rej = reject;
16
+ });
17
17
 
18
- return { promise, resolve: res, reject: rej } as DeferredPromise<T>;
18
+ return {promise, resolve: res, reject: rej} as DeferredPromise<T>;
19
19
  }
@@ -0,0 +1,43 @@
1
+ import {
2
+ ByteLengthQueuingStrategy as ByteLengthQueuingStrategyPolyfill,
3
+ ReadableStream as ReadableStreamPolyfill,
4
+ WritableStream as WritableStreamPolyfill,
5
+ } from 'web-streams-polyfill';
6
+
7
+ type MaybeGlobal = Record<string, unknown>;
8
+
9
+ function getCtor<T>(name: string, fallback: T): T {
10
+ const value = (globalThis as MaybeGlobal)[name];
11
+ return typeof value === 'function' ? (value as T) : fallback;
12
+ }
13
+
14
+ export {
15
+ ByteLengthQueuingStrategyPolyfill,
16
+ ReadableStreamPolyfill,
17
+ WritableStreamPolyfill,
18
+ };
19
+
20
+ export const ReadableStreamImpl = getCtor(
21
+ 'ReadableStream',
22
+ ReadableStreamPolyfill,
23
+ );
24
+
25
+ export const WritableStreamImpl = getCtor(
26
+ 'WritableStream',
27
+ WritableStreamPolyfill,
28
+ );
29
+
30
+ export const ByteLengthQueuingStrategyImpl = getCtor(
31
+ 'ByteLengthQueuingStrategy',
32
+ ByteLengthQueuingStrategyPolyfill,
33
+ );
34
+
35
+ // Companion TYPE aliases so that `Impl as X` imports carry a type too. Without
36
+ // these, an annotation like `ReadableStream<Uint8Array>` (where `ReadableStream`
37
+ // is the imported value) falls back to the ambient *global* ReadableStream and
38
+ // no longer matches the polyfill instances actually constructed — the runtime
39
+ // pick stays native-or-polyfill; only the static type is anchored to the
40
+ // (structurally faithful) polyfill instance type.
41
+ export type ReadableStreamImpl<R = unknown> = ReadableStreamPolyfill<R>;
42
+ export type WritableStreamImpl<W = unknown> = WritableStreamPolyfill<W>;
43
+ export type ByteLengthQueuingStrategyImpl = ByteLengthQueuingStrategyPolyfill;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * `createDeviceFixture` — the one-call fixture for testing serial code. It wires a
3
+ * {@link SimulatedDevice} simulator to an {@link InMemorySerialTransport} + `Serial`,
4
+ * and hands back everything a test needs to drive *both* sides:
5
+ *
6
+ * - `serial`/`port` — what the app-under-test consumes,
7
+ * - `simulatedDevice` (typed) + `device` handle — drive the device (inject data,
8
+ * move the GPS, inject faults),
9
+ * - `client` — a host-side {@link SerialClient} (for protocol tests), and
10
+ * - `whenOpened()/whenClosed()` — `await` the app connecting.
11
+ *
12
+ * @example Test how your app reacts to a device event
13
+ * const {simulatedDevice, device, whenOpened} = await createDeviceFixture(new MyGps());
14
+ * renderMyApp(); // opens the port itself
15
+ * await whenOpened();
16
+ * simulatedDevice.update({latitude: 51.48, longitude: 0});
17
+ * // …assert your app's UI updated
18
+ */
19
+
20
+ import {setUsbSerial} from '../UsbSerial';
21
+ import type {SerialPort} from '../WebSerial';
22
+ import {Serial} from '../WebSerial';
23
+ import type {
24
+ DeviceOptions,
25
+ InMemorySerialTransportOptions,
26
+ } from './in-memory-serial-transport';
27
+ import {
28
+ type DeviceHandle,
29
+ InMemorySerialTransport,
30
+ } from './in-memory-serial-transport';
31
+ import {SerialClient} from './serial-client';
32
+ import type {
33
+ SimulatedDevice,
34
+ SimulatedDeviceOpenOptions,
35
+ } from './simulated-device';
36
+
37
+ export type DeviceFixtureOptions = {
38
+ /** Per-device transport knobs (single-device form). */
39
+ device?: DeviceOptions;
40
+ /** Per-device transport knobs, by index (multi-device form). */
41
+ devices?: DeviceOptions[];
42
+ /** Transport-level options (latencyMs, chunkSize, autoGrantPermission). */
43
+ transport?: InMemorySerialTransportOptions;
44
+ /** Whether each device is already permitted. Defaults to true (so it lists). */
45
+ hasPermission?: boolean;
46
+ /** Also `setUsbSerial(transport)` so a running app's `serial` sees it. Default false. */
47
+ installGlobally?: boolean;
48
+ /** Options for the host-side {@link SerialClient}. */
49
+ client?: {defaultTimeoutMs?: number};
50
+ };
51
+
52
+ export type MountedDeviceFixture<D extends SimulatedDevice = SimulatedDevice> =
53
+ {
54
+ transport: InMemorySerialTransport;
55
+ serial: Serial;
56
+ port: SerialPort;
57
+ /** The transport-side handle: push/emitError/failNext/written/attach/detach/… */
58
+ device: DeviceHandle;
59
+ /** The concrete device simulator, typed (e.g. call `gps.update(...)`). */
60
+ simulatedDevice: D;
61
+ /** A host-side client bound to `port` — NOT opened (call `client.open()`). */
62
+ client: SerialClient;
63
+ /** Resolve when the app opens the port (now if already open). */
64
+ whenOpened(): Promise<SimulatedDeviceOpenOptions>;
65
+ /** Resolve when the app closes the port (now if not open). */
66
+ whenClosed(): Promise<void>;
67
+ };
68
+
69
+ export type MountedDeviceFixtures = {
70
+ transport: InMemorySerialTransport;
71
+ serial: Serial;
72
+ ports: SerialPort[];
73
+ devices: DeviceHandle[];
74
+ simulatedDevices: SimulatedDevice[];
75
+ clients: SerialClient[];
76
+ /** Resolve when device `index` opens, or any device when `index` is omitted. */
77
+ whenOpened(index?: number): Promise<SimulatedDeviceOpenOptions>;
78
+ /** Resolve when device `index` closes, or any device when `index` is omitted. */
79
+ whenClosed(index?: number): Promise<void>;
80
+ };
81
+
82
+ export function createDeviceFixture<D extends SimulatedDevice>(
83
+ device: D,
84
+ options?: DeviceFixtureOptions,
85
+ ): Promise<MountedDeviceFixture<D>>;
86
+ export function createDeviceFixture(
87
+ devices: SimulatedDevice[],
88
+ options?: DeviceFixtureOptions,
89
+ ): Promise<MountedDeviceFixtures>;
90
+ export async function createDeviceFixture(
91
+ deviceOrDevices: SimulatedDevice | SimulatedDevice[],
92
+ options: DeviceFixtureOptions = {},
93
+ ): Promise<MountedDeviceFixture | MountedDeviceFixtures> {
94
+ const list = Array.isArray(deviceOrDevices)
95
+ ? deviceOrDevices
96
+ : [deviceOrDevices];
97
+ const hasPermission = options.hasPermission ?? true;
98
+ const transport = new InMemorySerialTransport(options.transport);
99
+ const handles = list.map((dev, i) =>
100
+ transport.addDevice(dev, {
101
+ hasPermission,
102
+ ...(Array.isArray(deviceOrDevices)
103
+ ? options.devices?.[i]
104
+ : options.device),
105
+ }),
106
+ );
107
+ const serial = new Serial(transport);
108
+ if (options.installGlobally) setUsbSerial(transport);
109
+
110
+ const ports = await serial.getPorts();
111
+ const clients = ports.map(
112
+ p =>
113
+ new SerialClient(p, {
114
+ defaultTimeoutMs: options.client?.defaultTimeoutMs,
115
+ }),
116
+ );
117
+
118
+ if (!Array.isArray(deviceOrDevices)) {
119
+ const handle = handles[0];
120
+ const port = ports[0];
121
+ if (!port) throw new Error('createDeviceFixture: device did not enumerate');
122
+ return {
123
+ transport,
124
+ serial,
125
+ port,
126
+ device: handle,
127
+ simulatedDevice: deviceOrDevices,
128
+ client: clients[0],
129
+ whenOpened: () => handle.whenOpened(),
130
+ whenClosed: () => handle.whenClosed(),
131
+ };
132
+ }
133
+
134
+ return {
135
+ transport,
136
+ serial,
137
+ ports,
138
+ devices: handles,
139
+ simulatedDevices: list,
140
+ clients,
141
+ whenOpened: (index?: number) =>
142
+ index === undefined
143
+ ? Promise.race(handles.map(h => h.whenOpened()))
144
+ : handles[index].whenOpened(),
145
+ whenClosed: (index?: number) =>
146
+ index === undefined
147
+ ? Promise.race(handles.map(h => h.whenClosed()))
148
+ : handles[index].whenClosed(),
149
+ };
150
+ }