@ghia/native 0.0.0 → 0.11.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 (211) hide show
  1. package/LICENSE +201 -0
  2. package/browser.mjs +7 -0
  3. package/dist/aria.d.ts +72 -0
  4. package/dist/aria.d.ts.map +1 -0
  5. package/dist/aria.js +2 -0
  6. package/dist/aria.js.map +1 -0
  7. package/dist/automation/client.d.ts +243 -0
  8. package/dist/automation/client.d.ts.map +1 -0
  9. package/dist/automation/client.js +801 -0
  10. package/dist/automation/client.js.map +1 -0
  11. package/dist/automation/index.d.ts +5 -0
  12. package/dist/automation/index.d.ts.map +1 -0
  13. package/dist/automation/index.js +4 -0
  14. package/dist/automation/index.js.map +1 -0
  15. package/dist/automation/protocol.d.ts +309 -0
  16. package/dist/automation/protocol.d.ts.map +1 -0
  17. package/dist/automation/protocol.js +320 -0
  18. package/dist/automation/protocol.js.map +1 -0
  19. package/dist/blob-images.d.ts +32 -0
  20. package/dist/blob-images.d.ts.map +1 -0
  21. package/dist/blob-images.js +55 -0
  22. package/dist/blob-images.js.map +1 -0
  23. package/dist/bounds.d.ts +61 -0
  24. package/dist/bounds.d.ts.map +1 -0
  25. package/dist/bounds.js +85 -0
  26. package/dist/bounds.js.map +1 -0
  27. package/dist/clipboard-global.d.ts +11 -0
  28. package/dist/clipboard-global.d.ts.map +1 -0
  29. package/dist/clipboard-global.js +2 -0
  30. package/dist/clipboard-global.js.map +1 -0
  31. package/dist/clipboard.d.ts +19 -0
  32. package/dist/clipboard.d.ts.map +1 -0
  33. package/dist/clipboard.js +41 -0
  34. package/dist/clipboard.js.map +1 -0
  35. package/dist/cpu-throttle.d.ts +6 -0
  36. package/dist/cpu-throttle.d.ts.map +1 -0
  37. package/dist/cpu-throttle.js +42 -0
  38. package/dist/cpu-throttle.js.map +1 -0
  39. package/dist/events/data-transfer.d.ts +23 -0
  40. package/dist/events/data-transfer.d.ts.map +1 -0
  41. package/dist/events/data-transfer.js +97 -0
  42. package/dist/events/data-transfer.js.map +1 -0
  43. package/dist/events/dispatch.d.ts +144 -0
  44. package/dist/events/dispatch.d.ts.map +1 -0
  45. package/dist/events/dispatch.js +241 -0
  46. package/dist/events/dispatch.js.map +1 -0
  47. package/dist/events/drag.d.ts +21 -0
  48. package/dist/events/drag.d.ts.map +1 -0
  49. package/dist/events/drag.js +111 -0
  50. package/dist/events/drag.js.map +1 -0
  51. package/dist/events/event-props.d.ts +19 -0
  52. package/dist/events/event-props.d.ts.map +1 -0
  53. package/dist/events/event-props.js +104 -0
  54. package/dist/events/event-props.js.map +1 -0
  55. package/dist/events/keyboard-defaults.d.ts +18 -0
  56. package/dist/events/keyboard-defaults.d.ts.map +1 -0
  57. package/dist/events/keyboard-defaults.js +121 -0
  58. package/dist/events/keyboard-defaults.js.map +1 -0
  59. package/dist/events/listeners.d.ts +18 -0
  60. package/dist/events/listeners.d.ts.map +1 -0
  61. package/dist/events/listeners.js +49 -0
  62. package/dist/events/listeners.js.map +1 -0
  63. package/dist/events/route.d.ts +17 -0
  64. package/dist/events/route.d.ts.map +1 -0
  65. package/dist/events/route.js +36 -0
  66. package/dist/events/route.js.map +1 -0
  67. package/dist/events/types.d.ts +269 -0
  68. package/dist/events/types.d.ts.map +1 -0
  69. package/dist/events/types.js +2 -0
  70. package/dist/events/types.js.map +1 -0
  71. package/dist/events/window-target.d.ts +120 -0
  72. package/dist/events/window-target.d.ts.map +1 -0
  73. package/dist/events/window-target.js +311 -0
  74. package/dist/events/window-target.js.map +1 -0
  75. package/dist/eyedropper.d.ts +33 -0
  76. package/dist/eyedropper.d.ts.map +1 -0
  77. package/dist/eyedropper.js +66 -0
  78. package/dist/eyedropper.js.map +1 -0
  79. package/dist/focus.d.ts +18 -0
  80. package/dist/focus.d.ts.map +1 -0
  81. package/dist/focus.js +26 -0
  82. package/dist/focus.js.map +1 -0
  83. package/dist/fonts.d.ts +12 -0
  84. package/dist/fonts.d.ts.map +1 -0
  85. package/dist/fonts.js +17 -0
  86. package/dist/fonts.js.map +1 -0
  87. package/dist/forms.d.ts +10 -0
  88. package/dist/forms.d.ts.map +1 -0
  89. package/dist/forms.js +9 -0
  90. package/dist/forms.js.map +1 -0
  91. package/dist/host-runtime.d.ts +108 -0
  92. package/dist/host-runtime.d.ts.map +1 -0
  93. package/dist/host-runtime.js +213 -0
  94. package/dist/host-runtime.js.map +1 -0
  95. package/dist/host.d.ts +2403 -0
  96. package/dist/host.d.ts.map +1 -0
  97. package/dist/host.js +30 -0
  98. package/dist/host.js.map +1 -0
  99. package/dist/idle-gc.d.ts +36 -0
  100. package/dist/idle-gc.d.ts.map +1 -0
  101. package/dist/idle-gc.js +70 -0
  102. package/dist/idle-gc.js.map +1 -0
  103. package/dist/keyframes.d.ts +108 -0
  104. package/dist/keyframes.d.ts.map +1 -0
  105. package/dist/keyframes.js +107 -0
  106. package/dist/keyframes.js.map +1 -0
  107. package/dist/layout-animation.d.ts +38 -0
  108. package/dist/layout-animation.d.ts.map +1 -0
  109. package/dist/layout-animation.js +41 -0
  110. package/dist/layout-animation.js.map +1 -0
  111. package/dist/match-media.d.ts +62 -0
  112. package/dist/match-media.d.ts.map +1 -0
  113. package/dist/match-media.js +272 -0
  114. package/dist/match-media.js.map +1 -0
  115. package/dist/media-query.d.ts +21 -0
  116. package/dist/media-query.d.ts.map +1 -0
  117. package/dist/media-query.js +599 -0
  118. package/dist/media-query.js.map +1 -0
  119. package/dist/motion-bindings.d.ts +36 -0
  120. package/dist/motion-bindings.d.ts.map +1 -0
  121. package/dist/motion-bindings.js +107 -0
  122. package/dist/motion-bindings.js.map +1 -0
  123. package/dist/motion-config.d.ts +20 -0
  124. package/dist/motion-config.d.ts.map +1 -0
  125. package/dist/motion-config.js +51 -0
  126. package/dist/motion-config.js.map +1 -0
  127. package/dist/motion-span.d.ts +57 -0
  128. package/dist/motion-span.d.ts.map +1 -0
  129. package/dist/motion-span.js +150 -0
  130. package/dist/motion-span.js.map +1 -0
  131. package/dist/motion-value.d.ts +269 -0
  132. package/dist/motion-value.d.ts.map +1 -0
  133. package/dist/motion-value.js +941 -0
  134. package/dist/motion-value.js.map +1 -0
  135. package/dist/motion-variants.d.ts +190 -0
  136. package/dist/motion-variants.d.ts.map +1 -0
  137. package/dist/motion-variants.js +249 -0
  138. package/dist/motion-variants.js.map +1 -0
  139. package/dist/mutations.d.ts +65 -0
  140. package/dist/mutations.d.ts.map +1 -0
  141. package/dist/mutations.js +320 -0
  142. package/dist/mutations.js.map +1 -0
  143. package/dist/nativeview.d.ts +10 -0
  144. package/dist/nativeview.d.ts.map +1 -0
  145. package/dist/nativeview.js +92 -0
  146. package/dist/nativeview.js.map +1 -0
  147. package/dist/observers.d.ts +92 -0
  148. package/dist/observers.d.ts.map +1 -0
  149. package/dist/observers.js +579 -0
  150. package/dist/observers.js.map +1 -0
  151. package/dist/presence.d.ts +63 -0
  152. package/dist/presence.d.ts.map +1 -0
  153. package/dist/presence.js +54 -0
  154. package/dist/presence.js.map +1 -0
  155. package/dist/renderer-state.d.ts +155 -0
  156. package/dist/renderer-state.d.ts.map +1 -0
  157. package/dist/renderer-state.js +302 -0
  158. package/dist/renderer-state.js.map +1 -0
  159. package/dist/reorder.d.ts +63 -0
  160. package/dist/reorder.d.ts.map +1 -0
  161. package/dist/reorder.js +303 -0
  162. package/dist/reorder.js.map +1 -0
  163. package/dist/runtime.d.ts +23 -0
  164. package/dist/runtime.d.ts.map +1 -0
  165. package/dist/runtime.js +157 -0
  166. package/dist/runtime.js.map +1 -0
  167. package/dist/screen.d.ts +8 -0
  168. package/dist/screen.d.ts.map +1 -0
  169. package/dist/screen.js +16 -0
  170. package/dist/screen.js.map +1 -0
  171. package/dist/scroll.d.ts +79 -0
  172. package/dist/scroll.d.ts.map +1 -0
  173. package/dist/scroll.js +170 -0
  174. package/dist/scroll.js.map +1 -0
  175. package/dist/style-images.d.ts +20 -0
  176. package/dist/style-images.d.ts.map +1 -0
  177. package/dist/style-images.js +57 -0
  178. package/dist/style-images.js.map +1 -0
  179. package/dist/svg.d.ts +16 -0
  180. package/dist/svg.d.ts.map +1 -0
  181. package/dist/svg.js +119 -0
  182. package/dist/svg.js.map +1 -0
  183. package/dist/testing.d.ts +811 -0
  184. package/dist/testing.d.ts.map +1 -0
  185. package/dist/testing.js +1187 -0
  186. package/dist/testing.js.map +1 -0
  187. package/dist/text-match.d.ts +8 -0
  188. package/dist/text-match.d.ts.map +1 -0
  189. package/dist/text-match.js +10 -0
  190. package/dist/text-match.js.map +1 -0
  191. package/dist/text-selection.d.ts +34 -0
  192. package/dist/text-selection.d.ts.map +1 -0
  193. package/dist/text-selection.js +23 -0
  194. package/dist/text-selection.js.map +1 -0
  195. package/dist/universal-style.d.ts +22 -0
  196. package/dist/universal-style.d.ts.map +1 -0
  197. package/dist/universal-style.js +58 -0
  198. package/dist/universal-style.js.map +1 -0
  199. package/dist/web-animations.d.ts +300 -0
  200. package/dist/web-animations.d.ts.map +1 -0
  201. package/dist/web-animations.js +1032 -0
  202. package/dist/web-animations.js.map +1 -0
  203. package/dist/webview.d.ts +39 -0
  204. package/dist/webview.d.ts.map +1 -0
  205. package/dist/webview.js +26 -0
  206. package/dist/webview.js.map +1 -0
  207. package/gpuix-native.darwin-arm64.node +4 -0
  208. package/index.cjs +789 -0
  209. package/index.d.ts +2603 -0
  210. package/index.js +729 -0
  211. package/package.json +96 -4
@@ -0,0 +1,1187 @@
1
+ /// GPUIX TestRenderer — thin wrapper over the native TestGpuixRenderer.
2
+ ///
3
+ /// All state lives in Rust's RetainedTree. All mutations go directly to
4
+ /// the native renderer via napi. Inspection methods (findByType, getAllText,
5
+ /// toJSON, etc.) query the Rust tree via napi — no JS-side shadow copy.
6
+ ///
7
+ /// All event simulation goes through the native GPUI pipeline (coordinate-based
8
+ /// hit testing, GPUI dispatch, emit_event_full). The nativeSimulate* methods
9
+ /// flush the tree, dispatch through GPUI, drain events, and feed them into
10
+ /// the injected framework event dispatcher.
11
+ import { hasTestGpuixRenderer, TestGpuixRenderer, measureMotionAnimation, validateKeyframes, } from "../index.js";
12
+ import { bindClipboard } from "./clipboard.js";
13
+ import { forgetElementSource, forgetWindowSource } from "./events/window-target.js";
14
+ import { matchableText } from "./text-match.js";
15
+ import { flushPendingMutations } from "./bounds.js";
16
+ import { closeWebAnimations } from "./web-animations.js";
17
+ import { flushObservations, settleObservations } from "./observers.js";
18
+ import { readFontFiles } from "./fonts.js";
19
+ import { registerKeyframesHost, unregisterKeyframesHost, registerKeyframesValidator, } from "./keyframes.js";
20
+ import { registerAnimationMeasurer } from "./motion-value.js";
21
+ import { createRendererState, reportSyncErrors } from "./renderer-state.js";
22
+ export { applyMacCpuThrottleFromEnv, MAC_CPU_THROTTLES, readMacCpuThrottle, } from "./cpu-throttle.js";
23
+ // The Linux stub has no test methods; the native availability check guards the full API.
24
+ function hasNativeTestRendererClass(renderer) {
25
+ return hasTestGpuixRenderer();
26
+ }
27
+ const NativeTestRenderer = hasNativeTestRendererClass(TestGpuixRenderer) ? TestGpuixRenderer : null;
28
+ /** Whether the native TestGpuixRenderer is available (for conditional test registration). */
29
+ export const hasNativeTestRenderer = NativeTestRenderer != null;
30
+ registerKeyframesValidator(validateKeyframes);
31
+ registerAnimationMeasurer((from, target, transition) => {
32
+ const length = measureMotionAnimation(from, target, transition ?? null);
33
+ return { duration: length.duration, iterationDuration: length.iterationDuration };
34
+ });
35
+ // ── TestRenderer ─────────────────────────────────────────────────────
36
+ /** The test window is one per thread: making a renderer tears down the one before it. */
37
+ let latestRenderer;
38
+ /** Throws if a replayed frame of this window differed from a build of it, or if gpuix's own
39
+ * check found an element judged unchanged that built differently, and prints the counters when
40
+ * GPUIX_RETAIN_VERIFY_REPORT is set. The gpuix findings are read from the window's list, not
41
+ * from `retainMismatches`, which is one count per thread that only `resetBuildCounters` clears. */
42
+ function reportRetentionMismatches(native) {
43
+ const counters = native.getRetainedCounters();
44
+ const findings = native.getRetainMismatches();
45
+ if (process.env.GPUIX_RETAIN_VERIFY_REPORT !== undefined) {
46
+ console.log(`[retain-verify] verified=${counters.verified} mismatches=${counters.mismatches} gpuix-mismatches=${findings.length} replays=${counters.replays} ` +
47
+ `hits=${counters.hits} builds=${counters.builds} lazy=${counters.lazyBuilds} non-reusable=${counters.nonReusable}`);
48
+ }
49
+ if (counters.mismatches > 0) {
50
+ throw new Error(`${counters.mismatches} replayed frames differed from a build:\n${native.getRetentionMismatches().join('\n')}`);
51
+ }
52
+ if (findings.length > 0) {
53
+ throw new Error(`${findings.length} elements were judged unchanged and built differently:\n${findings.join('\n')}`);
54
+ }
55
+ }
56
+ export class TestRenderer {
57
+ /** Native TestGpuixRenderer — all state lives here in Rust's RetainedTree. */
58
+ native;
59
+ applyBatch;
60
+ focusNext;
61
+ focusPrevious;
62
+ promptForPaths;
63
+ /** The test window's own clipboard. It never touches the system's. */
64
+ writeClipboardText;
65
+ readClipboardText;
66
+ setWindowSelectionChange;
67
+ setWindowAppearanceChange;
68
+ setWindowMediaChange;
69
+ getMediaFeatures;
70
+ setWindowKeyEvents;
71
+ setWindowEventListeners;
72
+ dispatchEvent;
73
+ dispatchSyncEvent;
74
+ onError;
75
+ constructor(options = {}) {
76
+ if (!NativeTestRenderer) {
77
+ throw new Error("TestGpuixRenderer is macOS and Windows only. Linux builds have no test-support because wgpu cannot read a rendered image back yet. GpuixRenderer still works on Linux.");
78
+ }
79
+ const fonts = readFontFiles(options.fonts);
80
+ this.native = new NativeTestRenderer(options, options.scaleFactor);
81
+ if (latestRenderer) {
82
+ // The native side keeps one window per thread and this constructor replaced it, so the
83
+ // earlier renderer is as dead as a disposed one: it would reach the new window's state, and
84
+ // throw once that is disposed.
85
+ forgetWindowSource(latestRenderer);
86
+ unregisterKeyframesHost(latestRenderer.native);
87
+ closeWebAnimations(latestRenderer);
88
+ }
89
+ latestRenderer = this;
90
+ for (const font of fonts)
91
+ this.native.registerFont(font);
92
+ registerKeyframesHost(this.native);
93
+ this.applyBatch = this.native.applyBatch.bind(this.native);
94
+ this.focusNext = this.native.focusNext.bind(this.native);
95
+ this.focusPrevious = this.native.focusPrevious.bind(this.native);
96
+ this.promptForPaths = this.native.promptForPaths.bind(this.native);
97
+ this.writeClipboardText = this.native.writeClipboardText.bind(this.native);
98
+ this.readClipboardText = this.native.readClipboardText.bind(this.native);
99
+ this.setWindowKeyEvents = this.native.setWindowKeyEvents.bind(this.native);
100
+ this.setWindowEventListeners = this.native.setWindowEventListeners.bind(this.native);
101
+ this.setWindowSelectionChange = this.native.setWindowSelectionChange.bind(this.native);
102
+ this.setWindowAppearanceChange = this.native.setWindowAppearanceChange.bind(this.native);
103
+ this.onError = options.onError;
104
+ this.setWindowMediaChange = this.native.setWindowMediaChange.bind(this.native);
105
+ this.getMediaFeatures = this.native.getMediaFeatures.bind(this.native);
106
+ this.dispatchEvent = options.dispatchEvent ?? ((event) => {
107
+ createRendererState(this).dispatch(event);
108
+ });
109
+ const state = createRendererState(this);
110
+ state.setErrorHandler(options.onError);
111
+ this.dispatchSyncEvent = reportSyncErrors(options.dispatchSyncEvent ?? ((event) => state.dispatchSync(event)), (error) => state.reportError(error));
112
+ this.native.setSyncEventHandler((event) => this.dispatchSyncEvent(event));
113
+ bindClipboard(this);
114
+ }
115
+ /** Releases the sync event handler, which captures this renderer, and tears the native
116
+ * window down. Call it after the root is unmounted. Native calls that need the window
117
+ * throw afterwards; the retained tree stays readable. Without it the window is leaked
118
+ * when the thread exits. */
119
+ dispose() {
120
+ // A run under GPUIX_RETAIN_VERIFY fails at the window that showed a replayed frame that a
121
+ // build would have shown differently, and says which test's window it was.
122
+ if (process.env.GPUIX_RETAIN_VERIFY !== undefined)
123
+ reportRetentionMismatches(this.native);
124
+ forgetElementSource(this);
125
+ unregisterKeyframesHost(this.native);
126
+ closeWebAnimations(this);
127
+ this.native.setSyncEventHandler(null);
128
+ this.native.dispose();
129
+ }
130
+ // ── GPUI pipeline methods ───────────────────────────────────────
131
+ /** Trigger the real GPUI rendering pipeline (GpuixView::render() →
132
+ * build_element() → apply_styles() → layout). */
133
+ flush() {
134
+ this.native.flush();
135
+ }
136
+ /** Drain events collected by the native GPUI event handlers. */
137
+ drainEvents() {
138
+ return this.native.drainEvents();
139
+ }
140
+ // ── Frame loop ──────────────────────────────────────────────────
141
+ // The surface `startFrameLoop` drives, so the wake path can be checked without a live window.
142
+ // The test window draws no frames: a tick delivers the input AppKit has queued for it.
143
+ /** Wakes `callback` when the main run loop has work, as `GpuixRenderer.setFrameWakeCallback`
144
+ * does. Returns true on macOS. Pass null to clear. */
145
+ setFrameWakeCallback(callback) {
146
+ return this.native.setFrameWakeCallback(callback);
147
+ }
148
+ /** Whether `startFrameLoop` must tick this renderer: true on macOS. */
149
+ requiresTick() {
150
+ return this.native.requiresTick();
151
+ }
152
+ /** One frame-loop step: hands the input AppKit queued for the test window (see
153
+ * `postNativeEvent`) to it, then to the JS handlers, as `dispatchNativeEvents` does. Draws
154
+ * nothing: frames stay `flush` and `drawFrame`. */
155
+ tick() {
156
+ const running = this.native.tick();
157
+ this.dispatchNativeEvents();
158
+ return running;
159
+ }
160
+ /** Posts a real NSEvent for the test window from `com.apple.NSEventThread`, the way AppKit
161
+ * receives input, so an idle `startFrameLoop(testRenderer)` is woken by it. `kind` is
162
+ * `"keyDown"` (the key `a`), `"mouseMove"`, `"click"` or `"scroll"` (3 lines down); the last
163
+ * three need `x` and `y` in logical pixels from the window's top left. The window is neither
164
+ * made key nor shown, and nothing reaches the OS. `tick()` delivers the event. */
165
+ postNativeEvent(kind, x, y) {
166
+ this.native.postNativeEvent(kind, x, y);
167
+ }
168
+ // ── Native end-to-end simulation ────────────────────────────────
169
+ // These methods go through the full GPUI pipeline:
170
+ // native simulate → GPUI dispatch → hit test → event handler →
171
+ // emit_event_full → drainEvents → framework event handler
172
+ /** Drain events from the native GPUI pipeline and feed them into the
173
+ * framework event registry, triggering state updates synchronously.
174
+ * Loops until no more events are produced — handles re-entrant events
175
+ * that may be generated during framework state updates. */
176
+ dispatchNativeEvents() {
177
+ // The probe draws, and its reports are what the loop below drains.
178
+ settleObservations(this);
179
+ for (;;) {
180
+ const events = this.native.drainEvents();
181
+ if (events.length === 0)
182
+ break;
183
+ for (const event of events) {
184
+ try {
185
+ this.dispatchEvent(event);
186
+ }
187
+ catch (error) {
188
+ if (!this.onError)
189
+ throw error;
190
+ this.onError(error);
191
+ }
192
+ }
193
+ }
194
+ // Nothing else would run the observers' batch in a synchronous test.
195
+ flushObservations(this);
196
+ }
197
+ /** Press keys on whatever currently holds focus, in Playwright's
198
+ * `keyboard.press` grammar. A press is a DOM `key` value (`"Enter"`, `"a"`,
199
+ * `"A"`, `"ArrowDown"`, `"Escape"`, `"Tab"`, `"F2"`) or a `code` name
200
+ * (`"Space"`, `"KeyA"`, `"Digit1"`), with modifiers joined by `+`
201
+ * (`"Shift+Tab"`, `"Meta+a"`, `"Alt+ArrowLeft"`, `"ControlOrMeta+Shift+P"`;
202
+ * `ControlOrMeta` is Meta on macOS). `keystrokes` is a sequence of presses
203
+ * separated by spaces: `"a Shift+b Enter"`. Each press is a `keydown` then a
204
+ * `keyup`, and a typed key inserts its character into a focused input.
205
+ * Names are case-sensitive and an unknown one throws before anything is
206
+ * pressed.
207
+ *
208
+ * Unlike `nativeSimulateKeystrokes`, this focuses nothing first, which is
209
+ * the only way to test that `autoFocus` (or a click) actually moved focus. */
210
+ simulateKeystrokes(keystrokes) {
211
+ this.native.flushIfDirty();
212
+ // Events already queued reach JS before a key does, as on a live window.
213
+ // Delivered after it, they would undo what the key's own synchronous focus
214
+ // events did.
215
+ this.dispatchNativeEvents();
216
+ this.native.simulateKeystrokes(keystrokes);
217
+ this.dispatchNativeEvents();
218
+ this.native.flushIfDirty();
219
+ }
220
+ /** Focus an element, then press keys on it (the `simulateKeystrokes` grammar).
221
+ * @param elementId - element to focus
222
+ * @param keystrokes - space-separated presses, e.g. `"h i Enter Shift+Tab"` */
223
+ nativeSimulateKeystrokes(elementId, keystrokes) {
224
+ this.native.flushIfDirty();
225
+ this.native.focusElement(elementId);
226
+ // Focus events are queued but a key is delivered synchronously: let the
227
+ // focus handlers run first, as in a browser.
228
+ this.dispatchNativeEvents();
229
+ this.native.simulateKeystrokes(keystrokes);
230
+ this.dispatchNativeEvents();
231
+ }
232
+ /** Focus an element, then send one `keydown` for one press (the
233
+ * `simulateKeystrokes` grammar), with no `keyup` after it. Pairs with
234
+ * `nativeSimulateKeyUp`.
235
+ * @param elementId - element to focus
236
+ * @param keystroke - one press, e.g. `"a"`, `"Enter"`, `"Meta+s"`
237
+ * @param isHeld - whether this is a key-repeat event (default: false)
238
+ */
239
+ nativeSimulateKeyDown(elementId, keystroke, isHeld) {
240
+ this.native.flushIfDirty();
241
+ this.native.focusElement(elementId);
242
+ // Focus events are queued but a key is delivered synchronously: let the
243
+ // focus handlers run first, as in a browser.
244
+ this.dispatchNativeEvents();
245
+ this.native.simulateKeyDown(keystroke, isHeld);
246
+ this.dispatchNativeEvents();
247
+ }
248
+ /** Focus an element, then send one `keyup` for one press (the
249
+ * `simulateKeystrokes` grammar). Pairs with `nativeSimulateKeyDown`.
250
+ * @param elementId - element to focus
251
+ * @param keystroke - one press, e.g. `"a"`, `"Enter"`, `"Meta+s"`
252
+ */
253
+ nativeSimulateKeyUp(elementId, keystroke) {
254
+ this.native.flushIfDirty();
255
+ this.native.focusElement(elementId);
256
+ // Focus events are queued but a key is delivered synchronously: let the
257
+ // focus handlers run first, as in a browser.
258
+ this.dispatchNativeEvents();
259
+ this.native.simulateKeyUp(keystroke);
260
+ this.dispatchNativeEvents();
261
+ }
262
+ /** Focus a text input, then run an input method composition in it, one step
263
+ * per key. Each step is what macOS does for a key the input method takes: a
264
+ * `keydown` with `key` `"Process"`, the composition events and the `change`
265
+ * of the edit, then the `keyup` of that key. A text input is the only element
266
+ * that composes, so `elementId` must be one.
267
+ * @param elementId - the `<input>` or `<textarea>` to compose in
268
+ * @param steps - the keys the input method consumes, in order */
269
+ simulateComposition(elementId, steps) {
270
+ this.native.flushIfDirty();
271
+ this.native.focusElement(elementId);
272
+ // Focus events are queued but a key is delivered synchronously: let the
273
+ // focus handlers run first, as in a browser.
274
+ this.dispatchNativeEvents();
275
+ for (const step of steps) {
276
+ this.native.simulateImeKeyDown(step.key, step.action, step.action === "unmark" ? undefined : step.text);
277
+ // The edit's events are queued behind the keydown, and a browser fires
278
+ // them before the keyup.
279
+ this.dispatchNativeEvents();
280
+ this.native.simulateKeyUp(step.key);
281
+ this.dispatchNativeEvents();
282
+ }
283
+ this.native.flushIfDirty();
284
+ }
285
+ /** End-to-end: simulate a click through GPUI hit testing →
286
+ * dispatch resulting events to the framework. */
287
+ nativeSimulateClick(x, y, button, modifiers) {
288
+ this.native.flushIfDirty();
289
+ // Two inputs, not `native.simulateClick`: whatever the press queued must
290
+ // reach JS before the release, as it does when a person lets go later.
291
+ this.native.simulateMouseDown(x, y, button ?? 0, modifiers);
292
+ this.dispatchNativeEvents();
293
+ this.native.simulateMouseUp(x, y, button ?? 0, modifiers);
294
+ this.dispatchNativeEvents();
295
+ // Flush again after framework state updates so the Rust RetainedTree
296
+ // is fully rebuilt and GPUI has re-laid-out before any screenshot.
297
+ this.native.flushIfDirty();
298
+ }
299
+ /** End-to-end: simulate scroll wheel through GPUI →
300
+ * dispatch resulting events to the framework. Deltas have the DOM's sign, like
301
+ * `WheelEvent.deltaY`: positive `deltaY` scrolls down (the content moves up),
302
+ * positive `deltaX` scrolls right. */
303
+ nativeSimulateScrollWheel(x, y, deltaX, deltaY, modifiers, phase) {
304
+ this.native.flushIfDirty();
305
+ this.native.simulateScrollWheel(x, y, deltaX, deltaY, modifiers, phase);
306
+ this.dispatchNativeEvents();
307
+ }
308
+ /** Dispatch a wheel without the surrounding flushes, for perf sampling. Deltas
309
+ * have the DOM's sign: positive `deltaY` scrolls down.
310
+ * Call `flush()` yourself, or the sample is the framework update only and
311
+ * none of the GPUI build, layout and paint that follows. */
312
+ dispatchScrollWheel(x, y, deltaX, deltaY, modifiers, phase) {
313
+ this.native.simulateScrollWheel(x, y, deltaX, deltaY, modifiers, phase);
314
+ this.dispatchNativeEvents();
315
+ }
316
+ /** Dispatch a move without the surrounding flushes, for perf sampling.
317
+ * `nativeSimulateMouseMove` draws a pending change before and after the move, so a
318
+ * drag timed with it includes the paint and a wheel timed with `dispatchScrollWheel`
319
+ * does not. */
320
+ dispatchMouseMove(x, y, modifiers, movement) {
321
+ this.native.simulateMouseMove(x, y, modifiers, movement?.x, movement?.y);
322
+ this.dispatchNativeEvents();
323
+ }
324
+ /** End-to-end: simulate mouse move through GPUI →
325
+ * dispatch resulting events to the framework. The move reports the buttons held by
326
+ * `nativeSimulateMouseDown` and not yet released by `nativeSimulateMouseUp`, as a real
327
+ * OS does on every move, so a drag is a down, moves, an up.
328
+ * @param movement - what the platform reports as this move's motion, which becomes
329
+ * `movementX`/`movementY`. A real platform reports the delta from the last position;
330
+ * the simulation reports `{ x: 0, y: 0 }` unless told otherwise. */
331
+ nativeSimulateMouseMove(x, y, modifiers, movement) {
332
+ this.native.flushIfDirty();
333
+ this.native.simulateMouseMove(x, y, modifiers, movement?.x, movement?.y);
334
+ this.dispatchNativeEvents();
335
+ // Flush again after framework state updates so hover styles are applied
336
+ // and the Rust tree is current before any screenshot.
337
+ this.native.flushIfDirty();
338
+ }
339
+ /** End-to-end: the pointer leaves the window at (x, y), the way the platform
340
+ * reports it, with no mouse move after the last position inside. */
341
+ nativeSimulateMouseExit(x, y) {
342
+ this.native.flushIfDirty();
343
+ this.native.simulateMouseExit(x, y);
344
+ this.dispatchNativeEvents();
345
+ this.native.flushIfDirty();
346
+ }
347
+ /** The OS ate the release of `button` (0=left, the default, 1=middle, 2=right): the
348
+ * renderer forgets it as held and dispatches nothing. The next move reports no button,
349
+ * which is how a page sees a release it never got (the pointer let go outside the
350
+ * window), and the press is cancelled. */
351
+ simulateLostMouseUp(button) {
352
+ this.native.simulateLostMouseUp(button);
353
+ }
354
+ /** End-to-end: simulate mouse down through GPUI hit testing →
355
+ * dispatch resulting events to the framework.
356
+ * @param button - 0=left (default), 1=middle, 2=right
357
+ * @param clickCount - the platform's count: 2 for the second press of a double click */
358
+ nativeSimulateMouseDown(x, y, button, modifiers, clickCount) {
359
+ this.native.flushIfDirty();
360
+ this.native.simulateMouseDown(x, y, button ?? 0, modifiers, clickCount);
361
+ this.dispatchNativeEvents();
362
+ this.native.flushIfDirty();
363
+ }
364
+ /** End-to-end: simulate mouse up through GPUI hit testing →
365
+ * dispatch resulting events to the framework.
366
+ * @param button - 0=left (default), 1=middle, 2=right
367
+ * @param clickCount - the platform's count: 2 for the release of a double click */
368
+ nativeSimulateMouseUp(x, y, button, modifiers, clickCount) {
369
+ this.native.flushIfDirty();
370
+ this.native.simulateMouseUp(x, y, button ?? 0, modifiers, clickCount);
371
+ this.dispatchNativeEvents();
372
+ this.native.flushIfDirty();
373
+ }
374
+ /**
375
+ * End-to-end: files from the OS enter the window at `x`, `y`. Returns the operation the
376
+ * window answered the OS drag with, as macOS would see it: the `dropEffect` the `dragover`
377
+ * ended with (`"none"` when no handler cancelled it), or `null` when native picked none.
378
+ */
379
+ nativeSimulateFileDragEnter(x, y, paths) {
380
+ this.native.flushIfDirty();
381
+ const operation = this.native.simulateFileDragEnter(x, y, paths);
382
+ this.dispatchNativeEvents();
383
+ this.native.flushIfDirty();
384
+ return operation;
385
+ }
386
+ /** End-to-end: the dragged files move to `x`, `y`. Returns the operation, as `nativeSimulateFileDragEnter` does. */
387
+ nativeSimulateFileDragMove(x, y) {
388
+ this.native.flushIfDirty();
389
+ const operation = this.native.simulateFileDragMove(x, y);
390
+ this.dispatchNativeEvents();
391
+ this.native.flushIfDirty();
392
+ return operation;
393
+ }
394
+ /** End-to-end: the dragged files are released at `x`, `y`. */
395
+ nativeSimulateFileDragSubmit(x, y) {
396
+ this.native.flushIfDirty();
397
+ this.native.simulateFileDragSubmit(x, y);
398
+ this.dispatchNativeEvents();
399
+ this.native.flushIfDirty();
400
+ }
401
+ /** End-to-end: the dragged files leave the window. */
402
+ nativeSimulateFileDragExit() {
403
+ this.native.flushIfDirty();
404
+ this.native.simulateFileDragExit();
405
+ this.dispatchNativeEvents();
406
+ this.native.flushIfDirty();
407
+ }
408
+ /** End-to-end: the platform's drag session ends. */
409
+ nativeSimulateFileDragEnd() {
410
+ this.native.flushIfDirty();
411
+ this.native.simulateFileDragEnd();
412
+ this.dispatchNativeEvents();
413
+ this.native.flushIfDirty();
414
+ }
415
+ /** End-to-end: a whole Finder-style drop at `x`, `y`, as the platform sends it: enter, submit, exit, end. */
416
+ nativeSimulateFileDrop(x, y, paths) {
417
+ this.native.flushIfDirty();
418
+ this.native.simulateFileDrop(x, y, paths);
419
+ this.dispatchNativeEvents();
420
+ this.native.flushIfDirty();
421
+ }
422
+ // ── Tree inspection (queries Rust RetainedTree via napi) ────────
423
+ /** Build a flat map of TestElements from the native tree JSON.
424
+ * One FFI call to get the full tree, then parse into TestElement objects. */
425
+ buildElementMap() {
426
+ const json = JSON.parse(this.native.getTreeJson());
427
+ const map = new Map();
428
+ const walk = (node, parentId) => {
429
+ if (!node)
430
+ return;
431
+ map.set(node.id, {
432
+ id: node.id,
433
+ type: node.type,
434
+ style: node.style ?? {},
435
+ text: node.text ?? null,
436
+ events: new Set(node.events ?? []),
437
+ children: (node.children ?? []).map((c) => c.id),
438
+ parentId,
439
+ ...(node.testId ? { testId: node.testId } : {}),
440
+ ...(node.customProps ? { customProps: node.customProps } : {}),
441
+ });
442
+ for (const child of node.children ?? []) {
443
+ walk(child, node.id);
444
+ }
445
+ };
446
+ walk(json, null);
447
+ return map;
448
+ }
449
+ /** Get the root element. */
450
+ getRoot() {
451
+ const rootId = this.native.getRootId();
452
+ if (rootId == null)
453
+ return undefined;
454
+ return this.buildElementMap().get(rootId);
455
+ }
456
+ /** Get an element by ID. */
457
+ getElement(id) {
458
+ return this.buildElementMap().get(id);
459
+ }
460
+ /** Find elements by type (e.g. "div", "text"). */
461
+ findByType(type) {
462
+ return [...this.buildElementMap().values()].filter((el) => el.type === type);
463
+ }
464
+ /** Find the first text element containing the given string. */
465
+ findByText(text) {
466
+ const wanted = matchableText(text);
467
+ return [...this.buildElementMap().values()].find((el) => el.text != null && matchableText(el.text).includes(wanted));
468
+ }
469
+ findByTestId(testId) {
470
+ return [...this.buildElementMap().values()].find((el) => el.testId === testId);
471
+ }
472
+ /** Get all text content in the tree (depth-first). */
473
+ getAllText() {
474
+ return this.native.getAllText();
475
+ }
476
+ /** Print the tree structure for debugging. Only includes non-empty fields. */
477
+ toJSON() {
478
+ return JSON.parse(this.native.getTreeJson());
479
+ }
480
+ /** GPUI accessibility dump from the last painted frame. */
481
+ getA11yTree() {
482
+ return JSON.parse(this.native.getA11yTree());
483
+ }
484
+ getAutomationTree() {
485
+ return this.native.getAutomationTree();
486
+ }
487
+ /** Every element the native tree holds, reachable or not. `toJSON()` walks
488
+ * from the root, so only this can see a node that was detached and leaked. */
489
+ getRetainedElementCount() {
490
+ return this.native.getRetainedElementCount();
491
+ }
492
+ /** What the element build has done since the last `resetBuildCounters()`:
493
+ * elements built and `StyleDesc` / `Inherited` clones. A draw builds every
494
+ * element, so divide by the frames drawn for the per-frame figure. */
495
+ getBuildCounters() {
496
+ return this.native.getBuildCounters();
497
+ }
498
+ /** What the retained subtrees did since the window was made: subtrees not built, replayed,
499
+ * built late, and frames compared with a whole build under `setRetainVerify`. */
500
+ getRetainedCounters() {
501
+ return this.native.getRetainedCounters();
502
+ }
503
+ /** What building and drawing the elements did since the window was made: elements prepainted and
504
+ * painted (those in a culled subtree apart), and the scene primitives painting inserted against
505
+ * the ones a replay inserted again. A replayed subtree prepaints and paints none of its
506
+ * elements; its replays are `getRetainedCounters().replays`. */
507
+ getFrameWorkCounters() {
508
+ return this.native.getFrameWorkCounters();
509
+ }
510
+ /** How the frames compared under `setRetainVerify` differed from a whole build. */
511
+ getRetentionMismatches() {
512
+ return this.native.getRetentionMismatches();
513
+ }
514
+ /** Layout nodes made, found again and freed, styles and child lists set, text measured and
515
+ * shaped, and the subtrees whose extent and overflow were walked again, by the window's layout
516
+ * engine since the window was made. */
517
+ getLayoutCounters() {
518
+ return this.native.getLayoutCounters();
519
+ }
520
+ resetBuildCounters() {
521
+ this.native.resetBuildCounters();
522
+ }
523
+ /** Hashes what every element builds, and counts in `retainMismatches` an element the
524
+ * retain key judged unchanged that built differently since the last build. Costs a
525
+ * formatted style per element: leave it off to time a frame. */
526
+ setRetainVerify(verify) {
527
+ this.native.setRetainVerify(verify);
528
+ }
529
+ /** Turns the skipping of unchanged subtrees on or off (on by default): off, every frame
530
+ * builds every element, the way `GPUIX_RETAIN_OFF` makes it for the whole run. */
531
+ setRetainPlanning(planning) {
532
+ this.native.setRetainPlanning(planning);
533
+ }
534
+ /** Whether a frame records what it builds at all (on by default): off, a frame costs what it did
535
+ * before retention, as `GPUIX_RETAIN_OFF` makes it for the whole run. Turning it on starts
536
+ * from a frame that builds everything. */
537
+ setRetainEnabled(enabled) {
538
+ this.native.setRetainEnabled(enabled);
539
+ }
540
+ /** Whether a tick of animations that only change what they paint is drawn by painting the roots
541
+ * they change again in the frame before it (on by default, as `GPUIX_PATCH_OFF` turns off for the
542
+ * whole run). It is never patched while the retention is off. */
543
+ setPatchEnabled(enabled) {
544
+ this.native.setPatchEnabled(enabled);
545
+ }
546
+ /** How ticks were drawn by patching since the window was made: the frames and roots patched, the
547
+ * ticks gpui drew whole after the patch was tried (`declines`, by reason, as JSON) and the ones
548
+ * gpuix did not try to patch (`unpatchable`, by kind, as JSON). */
549
+ getPatchCounters() {
550
+ return this.native.getPatchCounters();
551
+ }
552
+ /** Counts as if these exclusions (`svg`, `overflow-hidden`, `custom-element`, ...) were lifted,
553
+ * to see which are worth lifting first. Nothing is reused. */
554
+ setRetainLift(names) {
555
+ this.native.setRetainLift(names);
556
+ }
557
+ /** The elements counted in `retainMismatches`, one line each. */
558
+ getRetainMismatches() {
559
+ return this.native.getRetainMismatches();
560
+ }
561
+ /** Frames the window has painted. A test compares it before and after to
562
+ * count the draws a read caused: a bounds or scroll read paints nothing. */
563
+ getPaintedFrameCount() {
564
+ return this.native.getPaintedFrameCount();
565
+ }
566
+ /** Draw a dirty window when an update or a simulated input flushes (the
567
+ * default), or leave that to `drawFrame`, as a real window leaves it to its
568
+ * display link. A test that counts draws per frame turns it off. */
569
+ setInlineDraws(enabled) {
570
+ this.native.setInlineDraws(enabled);
571
+ }
572
+ /** One display-link frame: deliver the queued frame callbacks, then draw a
573
+ * pending change once. Pairs with `setInlineDraws(false)`. */
574
+ drawFrame() {
575
+ this.native.drawFrame();
576
+ }
577
+ /** Layout-only passes the window has run. A read of bounds or scroll metrics
578
+ * on a dirty window is one, in place of a draw, and the next frame draws
579
+ * once regardless. */
580
+ getLayoutPassCount() {
581
+ return this.native.getLayoutPassCount();
582
+ }
583
+ /** Pretends a screen reader is connected, as the test window starts out,
584
+ * or not. Without one no accessibility tree is built. */
585
+ setA11yActive(active) {
586
+ this.native.setA11yActive(active);
587
+ }
588
+ /** Subtrees the window has skipped as lying outside their clip, over its
589
+ * life. Compare before and after a frame. */
590
+ getCulledSubtreeCount() {
591
+ return this.native.getCulledSubtreeCount();
592
+ }
593
+ getElementBounds(elementId) {
594
+ flushPendingMutations(this);
595
+ return this.native.getElementBounds(elementId);
596
+ }
597
+ /** `getElementBounds` for the first report of an observer: it lays out as that does, but not the content
598
+ * of a skipped `content-visibility` ancestor, and an element inside it has no box (null). */
599
+ probeElementBounds(elementId) {
600
+ flushPendingMutations(this);
601
+ return this.native.probeElementBounds(elementId);
602
+ }
603
+ /** The box the last layout gave an element, or null if it was not built. Unlike `getElementBounds`
604
+ * it does not lay out the content of a skipped `content-visibility` ancestor to answer, so it
605
+ * tells whether that content is built. */
606
+ getBuiltElementBounds(elementId) {
607
+ flushPendingMutations(this);
608
+ return this.native.getBuiltElementBounds(elementId);
609
+ }
610
+ /** Ids of the elements under the window point, topmost first. */
611
+ getElementsAt(x, y) {
612
+ flushPendingMutations(this);
613
+ return this.native.getElementsAt(x, y);
614
+ }
615
+ clockPause() {
616
+ return this.native.clockPause();
617
+ }
618
+ clockSet(nowMs) {
619
+ return this.native.clockSet(nowMs);
620
+ }
621
+ clockFastForward(deltaMs) {
622
+ return this.native.clockFastForward(deltaMs);
623
+ }
624
+ /** `clockFastForward` without telling the window, as time passing does in a live one: the frame
625
+ * after it is a tick, which patches the frame before it when only paint-only animations ran. */
626
+ clockAdvance(deltaMs) {
627
+ return this.native.clockAdvance(deltaMs);
628
+ }
629
+ clockResume() {
630
+ return this.native.clockResume();
631
+ }
632
+ // ── Motion values ───────────────────────────────────────────────
633
+ createMotionValue(...args) {
634
+ this.native.createMotionValue(...args);
635
+ }
636
+ createMotionTransform(...args) {
637
+ this.native.createMotionTransform(...args);
638
+ }
639
+ createMotionFollower(...args) {
640
+ this.native.createMotionFollower(...args);
641
+ }
642
+ createMotionSpring(...args) {
643
+ this.native.createMotionSpring(...args);
644
+ }
645
+ disposeMotionValue(id) {
646
+ return this.native.disposeMotionValue(id);
647
+ }
648
+ getMotionValue(id) {
649
+ return this.native.getMotionValue(id);
650
+ }
651
+ getMotionValueVelocity(id) {
652
+ return this.native.getMotionValueVelocity(id);
653
+ }
654
+ isMotionValueAnimating(id) {
655
+ return this.native.isMotionValueAnimating(id);
656
+ }
657
+ setMotionValue(id, value) {
658
+ this.native.setMotionValue(id, value);
659
+ }
660
+ jumpMotionValue(id, value) {
661
+ this.native.jumpMotionValue(id, value);
662
+ }
663
+ animateMotionValue(id, target, transition) {
664
+ this.native.animateMotionValue(id, target, transition);
665
+ }
666
+ stopMotionValue(id) {
667
+ this.native.stopMotionValue(id);
668
+ }
669
+ pauseMotionValue(id) {
670
+ this.native.pauseMotionValue(id);
671
+ }
672
+ playMotionValue(id) {
673
+ this.native.playMotionValue(id);
674
+ }
675
+ seekMotionValue(id, seconds) {
676
+ this.native.seekMotionValue(id, seconds);
677
+ }
678
+ setMotionValueSpeed(id, speed) {
679
+ this.native.setMotionValueSpeed(id, speed);
680
+ }
681
+ completeMotionValue(id) {
682
+ this.native.completeMotionValue(id);
683
+ }
684
+ detachMotionValueClock(id) {
685
+ this.native.detachMotionValueClock(id);
686
+ }
687
+ getMotionValuePlayback(id) {
688
+ return this.native.getMotionValuePlayback(id);
689
+ }
690
+ setMotionValueListener(id, change, animation) {
691
+ this.native.setMotionValueListener(id, change, animation);
692
+ }
693
+ /** Advance GPUI's test dispatcher and run due timers.
694
+ * This is not `clockFastForward`. That moves the motion clock only.
695
+ * Use this for caret blink, input drag autoscroll, and list edge scroll. */
696
+ advanceTime(milliseconds) {
697
+ this.native.advanceTime(milliseconds);
698
+ this.dispatchNativeEvents();
699
+ }
700
+ focusElement(elementId) {
701
+ this.native.flushIfDirty();
702
+ this.native.focusElement(elementId);
703
+ this.dispatchNativeEvents();
704
+ }
705
+ blurElement(elementId) {
706
+ this.native.flushIfDirty();
707
+ this.native.blurElement(elementId);
708
+ this.dispatchNativeEvents();
709
+ }
710
+ matchesFocus(elementId, selector) {
711
+ this.native.flushIfDirty();
712
+ const matches = this.native.matchesFocus(elementId, selector);
713
+ this.dispatchNativeEvents();
714
+ return matches;
715
+ }
716
+ getFocusedElementId() {
717
+ this.native.flushIfDirty();
718
+ return this.native.getFocusedElementId();
719
+ }
720
+ /** Makes the offscreen window key (`true`, the default) or not, as when the user switches apps. */
721
+ setWindowActive(active) {
722
+ this.native.setWindowActive(active);
723
+ this.native.flushIfDirty();
724
+ this.dispatchNativeEvents();
725
+ }
726
+ focusNextWithin(elementId) {
727
+ this.native.flushIfDirty();
728
+ this.native.focusNextWithin(elementId);
729
+ this.dispatchNativeEvents();
730
+ }
731
+ focusPreviousWithin(elementId) {
732
+ this.native.flushIfDirty();
733
+ this.native.focusPreviousWithin(elementId);
734
+ this.dispatchNativeEvents();
735
+ }
736
+ /** The offscreen window size, so `useWindowSize()` works under test. */
737
+ getWindowSize() {
738
+ return this.native.getWindowSize();
739
+ }
740
+ /** `"system"` | `"light"` | `"dark"`, as on the live renderer: the window's appearance
741
+ * follows (`getAppearance`, `light-dark()`, `prefers-color-scheme`) and `appearanceChange`
742
+ * is delivered. `"system"` returns to the platform's own. */
743
+ setAppearance(appearance) {
744
+ this.native.flush();
745
+ this.native.setAppearance(appearance);
746
+ this.native.flush();
747
+ this.dispatchNativeEvents();
748
+ }
749
+ /** `"overlay"` (macOS "Show scroll bars" Automatically or When scrolling, which the test
750
+ * window starts with) or `"classic"` (Always, which macOS also picks while a mouse is
751
+ * attached). Classic bars are always drawn and take 15px (11 with `scrollbarWidth: "thin"`)
752
+ * out of the container, so its `clientWidth` and content box shrink by it. The test window
753
+ * never hears from the OS, so this is how a test flips the setting. */
754
+ setScrollbarStyle(style) {
755
+ this.native.flush();
756
+ this.native.setScrollbarStyle(style);
757
+ this.native.flush();
758
+ }
759
+ /** The window's title, as the platform window has it: the `title` option, or `"GPUIX"`. */
760
+ getWindowTitle() {
761
+ this.native.flush();
762
+ return this.native.getWindowTitle();
763
+ }
764
+ /** The window's effective appearance, `"light"` or `"dark"`. */
765
+ getAppearance() {
766
+ this.native.flush();
767
+ return this.native.getAppearance();
768
+ }
769
+ /** Set the OS accessibility preferences this window reports (all start as
770
+ * "no preference") and deliver the resulting `mediaChange` event. The test
771
+ * window never hears from the OS, so this is how a test flips reduced motion. */
772
+ setMediaFeatures(features) {
773
+ this.native.setMediaFeatures(features.reducedMotion, features.increasedContrast, features.forcedColors);
774
+ this.dispatchNativeEvents();
775
+ }
776
+ /** Resize the offscreen window's content, as dragging its edge would: `getWindowSize()`
777
+ * and the size queries of `matchMedia` see the new size, and their `change` events are
778
+ * delivered. */
779
+ setWindowSize(width, height) {
780
+ this.native.setWindowSize(width, height);
781
+ this.dispatchNativeEvents();
782
+ }
783
+ /** Give the offscreen window the scale factor of a display of another density (`2` is a
784
+ * retina one), as a window dragged to it has: the next frame lays out and paints at it, and
785
+ * a screenshot is the window's size times it. The factor stays until it is set again:
786
+ * resizing the window does not reset it. The window starts at 1 (`scaleFactor` option). */
787
+ setWindowScaleFactor(scaleFactor) {
788
+ this.native.setWindowScaleFactor(scaleFactor);
789
+ }
790
+ // ── Scroll API ──────────────────────────────────────────────────
791
+ /** Scroll an element (overflow: "scroll", or a `<virtual-list>`) to a DOM scroll
792
+ * position: `left` and `top` are `scrollLeft` and `scrollTop`, zero or more, so
793
+ * scrolling down is a larger `top`. Like a browser's, the write forces a layout only if
794
+ * one is pending, and the window draws the new offset with its next frame. */
795
+ scrollTo(elementId, left, top, behavior) {
796
+ this.native.scrollTo(elementId, left, top, behavior);
797
+ }
798
+ /** Scroll a child into view by its index in the children list.
799
+ *
800
+ * `offsetInItem` is in pixels. A negative value anchors the viewport top
801
+ * above the item, resolved against measured row heights at layout time, so
802
+ * a row stays pixel-stable while unmeasured rows are spliced in above it. */
803
+ scrollToItem(elementId, index, offsetInItem) {
804
+ this.native.flush();
805
+ this.native.scrollToItem(elementId, index, offsetInItem);
806
+ this.dispatchNativeEvents();
807
+ this.native.flush();
808
+ }
809
+ /** The DOM's `scrollIntoView`: every scrollable ancestor scrolls until the element sits
810
+ * where `block` (default `"start"`) and `inline` (default `"nearest"`) ask. */
811
+ scrollIntoView(elementId, block, inline, behavior) {
812
+ this.native.flush();
813
+ this.native.scrollIntoView(elementId, block, inline, behavior);
814
+ this.dispatchNativeEvents();
815
+ this.native.flush();
816
+ }
817
+ /** Applies from the next simulated pointer input. See `PointerCaptureMethods`. */
818
+ setPointerCapture(elementId, pointerId) {
819
+ this.native.setPointerCapture(elementId, pointerId);
820
+ }
821
+ releasePointerCapture(elementId, pointerId) {
822
+ this.native.releasePointerCapture(elementId, pointerId);
823
+ }
824
+ hasPointerCapture(elementId, pointerId) {
825
+ return this.native.hasPointerCapture(elementId, pointerId);
826
+ }
827
+ clickElement(elementId) {
828
+ this.native.clickElement(elementId);
829
+ }
830
+ getAnimations(elementId) {
831
+ return this.native.getAnimations(elementId);
832
+ }
833
+ getSubtreeAnimations(elementId) {
834
+ return this.native.getSubtreeAnimations(elementId);
835
+ }
836
+ getDocumentAnimations() {
837
+ return this.native.getDocumentAnimations();
838
+ }
839
+ /**
840
+ * The XML the `<svg>` painted from its shape children in the last frame, with
841
+ * animated attributes as that frame had them, or `null` when it painted none.
842
+ */
843
+ getSvgMarkup(elementId) {
844
+ return this.native.getSvgMarkup(elementId);
845
+ }
846
+ getAnimation(animationId) {
847
+ return this.native.getAnimation(animationId);
848
+ }
849
+ releaseAnimation(animationId) {
850
+ this.native.releaseAnimation(animationId);
851
+ }
852
+ forgetAnimation(animationId) {
853
+ this.native.forgetAnimation(animationId);
854
+ }
855
+ getAnimationEffect(animationId) {
856
+ return this.native.getAnimationEffect(animationId);
857
+ }
858
+ getAnimationKeyframes(animationId) {
859
+ return this.native.getAnimationKeyframes(animationId);
860
+ }
861
+ updateAnimationTiming(animationId, timing) {
862
+ this.native.updateAnimationTiming(animationId, timing);
863
+ }
864
+ pauseAnimation(animationId) {
865
+ this.native.pauseAnimation(animationId);
866
+ }
867
+ playAnimation(animationId) {
868
+ this.native.playAnimation(animationId);
869
+ }
870
+ setAnimationCurrentTime(animationId, milliseconds) {
871
+ this.native.setAnimationCurrentTime(animationId, milliseconds);
872
+ }
873
+ setAnimationPlaybackRate(animationId, rate) {
874
+ this.native.setAnimationPlaybackRate(animationId, rate);
875
+ }
876
+ cancelAnimation(animationId) {
877
+ this.native.cancelAnimation(animationId);
878
+ }
879
+ finishAnimation(animationId) {
880
+ this.native.finishAnimation(animationId);
881
+ }
882
+ reverseAnimation(animationId) {
883
+ this.native.reverseAnimation(animationId);
884
+ }
885
+ updateAnimationPlaybackRate(animationId, rate) {
886
+ this.native.updateAnimationPlaybackRate(animationId, rate);
887
+ }
888
+ setAnimationStartTime(animationId, startTime) {
889
+ this.native.setAnimationStartTime(animationId, startTime);
890
+ }
891
+ getAnimationTimelineTime() {
892
+ return this.native.getAnimationTimelineTime();
893
+ }
894
+ /** Applies from the next simulated pointer input. */
895
+ cancelDrag() {
896
+ this.native.cancelDrag();
897
+ }
898
+ /** The frame it asks for is drawn here, so `pointerlockchange` or
899
+ * `pointerlockerror` has reached its listeners when this returns. */
900
+ requestPointerLock(elementId) {
901
+ this.native.requestPointerLock(elementId);
902
+ this.dispatchNativeEvents();
903
+ }
904
+ exitPointerLock() {
905
+ this.native.exitPointerLock();
906
+ this.dispatchNativeEvents();
907
+ }
908
+ pointerLockElement() {
909
+ return this.native.pointerLockElement();
910
+ }
911
+ /** Whether the simulated platform grants lock requests. The offscreen test
912
+ * window is never key, so a real GPUI lock would always be refused; this
913
+ * stands in for a platform that can lock, and off, for one that refuses. */
914
+ simulatePointerLockGranting(granting) {
915
+ this.native.simulatePointerLockGranting(granting);
916
+ }
917
+ /** The platform drops the lock, as when the window deactivates. */
918
+ simulatePointerLockLost() {
919
+ this.native.simulatePointerLockLost();
920
+ this.dispatchNativeEvents();
921
+ }
922
+ /** What the window was asked to do by presses in `appRegion: "drag"` boxes since the last
923
+ * call, oldest first: `"move"`, `"titlebarDoubleClick"` (macOS), `"zoom"` or `"showMenu"`
924
+ * (Linux). The offscreen window cannot start a real move, so the test window records
925
+ * instead. Every test window is frameless, so `appRegion` always applies. */
926
+ takeSimulatedWindowActions() {
927
+ return this.native.takeSimulatedWindowActions();
928
+ }
929
+ /** The default action of a scrolling key. See `scrollByKey` on the native renderer. */
930
+ scrollByKey(key, shift) {
931
+ this.native.flush();
932
+ this.native.scrollByKey(key, shift);
933
+ this.dispatchNativeEvents();
934
+ this.native.flush();
935
+ }
936
+ /** `[scrollLeft, scrollTop, scrollWidth, scrollHeight, clientWidth, clientHeight]`, or
937
+ * null for an element that has not been painted. */
938
+ getScrollMetrics(elementId) {
939
+ return this.native.getScrollMetrics(elementId);
940
+ }
941
+ setImage(elementId, bytes) {
942
+ this.native.flush();
943
+ this.native.setImage(elementId, bytes);
944
+ this.native.flush();
945
+ }
946
+ setStyleImage(url, bytes) {
947
+ this.native.flush();
948
+ this.native.setStyleImage(url, bytes);
949
+ this.native.flush();
950
+ }
951
+ failStyleImage(url, message) {
952
+ this.native.flush();
953
+ this.native.failStyleImage(url, message);
954
+ this.native.flush();
955
+ }
956
+ selectText(elementId) {
957
+ this.native.flush();
958
+ this.native.selectText(elementId);
959
+ this.native.flush();
960
+ }
961
+ setSelectionRange(elementId, start, end, direction) {
962
+ this.native.flush();
963
+ this.native.setSelectionRange(elementId, start, end, direction);
964
+ this.native.flush();
965
+ }
966
+ getTextSelection(elementId) {
967
+ this.native.flush();
968
+ return this.native.getTextSelection(elementId);
969
+ }
970
+ requestSubmit(elementId) {
971
+ this.native.flushIfDirty();
972
+ this.native.requestSubmit(elementId);
973
+ this.dispatchNativeEvents();
974
+ this.native.flushIfDirty();
975
+ }
976
+ registerFont(bytes) {
977
+ this.native.registerFont(bytes);
978
+ this.native.flush();
979
+ }
980
+ // ── <webview> ───────────────────────────────────────────────────
981
+ // The page of a webview is made when the element is first painted, so these draw first.
982
+ // The engine is a scripted page: it answers from what these set, and reports what they make it.
983
+ webviewCommand(elementId, command, argument) {
984
+ this.native.flush();
985
+ this.native.webviewCommand(elementId, command, argument);
986
+ }
987
+ webviewState(elementId) {
988
+ this.native.flush();
989
+ return this.native.webviewState(elementId);
990
+ }
991
+ webviewExecuteJavascript(elementId, code) {
992
+ this.native.flush();
993
+ return this.native.webviewExecuteJavascript(elementId, code);
994
+ }
995
+ /** What the page of the `<webview>` was asked to do, in order. */
996
+ getWebviewCommands(elementId) {
997
+ this.native.flush();
998
+ return this.native.getWebviewCommands(elementId);
999
+ }
1000
+ /** The page starts loading: `did-start-loading`. */
1001
+ simulateWebviewStartLoading(elementId) {
1002
+ this.native.flush();
1003
+ this.native.simulateWebviewStartLoading(elementId);
1004
+ this.dispatchNativeEvents();
1005
+ }
1006
+ /** The page stops loading: `did-stop-loading`. */
1007
+ simulateWebviewStopLoading(elementId) {
1008
+ this.native.flush();
1009
+ this.native.simulateWebviewStopLoading(elementId);
1010
+ this.dispatchNativeEvents();
1011
+ }
1012
+ /** The page commits `url`: `did-navigate`, and `getURL()` answers it. */
1013
+ simulateWebviewNavigate(elementId, url) {
1014
+ this.native.flush();
1015
+ this.native.simulateWebviewNavigate(elementId, url);
1016
+ this.dispatchNativeEvents();
1017
+ }
1018
+ /** The page hands the keyboard on, as it does when Tab leaves its last tabbable element
1019
+ * (`backwards`: Shift+Tab leaves its first): focus moves to the next (previous) tab stop. */
1020
+ simulateWebviewTabOut(elementId, backwards = false) {
1021
+ this.native.flush();
1022
+ this.native.simulateWebviewTabOut(elementId, backwards);
1023
+ this.dispatchNativeEvents();
1024
+ }
1025
+ /** The page gets a title: `page-title-updated`, and `getTitle()` answers it. */
1026
+ simulateWebviewTitle(elementId, title, explicitSet = true) {
1027
+ this.native.flush();
1028
+ this.native.simulateWebviewTitle(elementId, title, explicitSet);
1029
+ this.dispatchNativeEvents();
1030
+ }
1031
+ /** A load fails: `did-fail-load`, with an `NSURLError` code. */
1032
+ simulateWebviewFail(elementId, errorCode, errorDescription, validatedUrl) {
1033
+ this.native.flush();
1034
+ this.native.simulateWebviewFail(elementId, errorCode, errorDescription, validatedUrl);
1035
+ this.dispatchNativeEvents();
1036
+ }
1037
+ /** What `canGoBack()` and `canGoForward()` answer. */
1038
+ setWebviewHistory(elementId, canGoBack, canGoForward) {
1039
+ this.native.flush();
1040
+ this.native.setWebviewHistory(elementId, canGoBack, canGoForward);
1041
+ }
1042
+ /** Settles the oldest `executeJavaScript` of the `<webview>` that waits: resolves it with
1043
+ * `result` (anything JSON holds; `undefined` for `undefined`), or rejects it with `error`. */
1044
+ settleWebviewScript(elementId, outcome) {
1045
+ this.native.flush();
1046
+ this.native.settleWebviewScript(elementId, outcome.result === undefined ? undefined : JSON.stringify(outcome.result), outcome.error);
1047
+ }
1048
+ /** Where the last frame put the surface of the `<webview>`; null while it has none. */
1049
+ getWebviewSurface(elementId) {
1050
+ this.native.flush();
1051
+ return this.native.getWebviewSurface(elementId);
1052
+ }
1053
+ /** The layers of the last composed frame, bottom to top. */
1054
+ getCompositionLayers() {
1055
+ this.native.flush();
1056
+ return this.native.getCompositionLayers();
1057
+ }
1058
+ /** The views AppKit holds for the `<webview>`, with the placement the last frame applied. */
1059
+ getWebviewNativeView(elementId) {
1060
+ this.native.flush();
1061
+ return this.native.getWebviewNativeView(elementId);
1062
+ }
1063
+ // ── <nativeview> ────────────────────────────────────────────────
1064
+ // The surface of a nativeview is made when its element is first painted.
1065
+ /** Where the last frame put the native view of the `<nativeview>`; null while it has no
1066
+ * surface or the frame did not place it (`display: none`, `visibility: hidden`, culled). */
1067
+ getNativeviewSurface(elementId) {
1068
+ this.native.flush();
1069
+ return this.native.getNativeviewSurface(elementId);
1070
+ }
1071
+ /** The native surfaces the `<nativeview>` elements made and dropped, in order. `"removed"` is
1072
+ * when the element drops the surface (after `detach`); its view leaves the window with the
1073
+ * next frame. */
1074
+ getNativeSurfaceEvents() {
1075
+ this.native.flush();
1076
+ return this.native.getNativeSurfaceEvents();
1077
+ }
1078
+ /** How many warnings the `<nativeview>` has issued: one when the platform cannot make its
1079
+ * surface, however many frames are drawn. */
1080
+ getNativeviewWarnings(elementId) {
1081
+ this.native.flush();
1082
+ return this.native.getNativeviewWarnings(elementId);
1083
+ }
1084
+ /** `false` makes the window refuse native surfaces, as a platform that cannot compose does:
1085
+ * call it before the first frame that paints a `<nativeview>`. */
1086
+ setNativeSurfacesAvailable(available) {
1087
+ this.native.flush();
1088
+ this.native.setNativeSurfacesAvailable(available);
1089
+ }
1090
+ setImagePixels(elementId, width, height, pixels, format) {
1091
+ this.native.flush();
1092
+ this.native.setImagePixels(elementId, width, height, pixels, format);
1093
+ this.native.flush();
1094
+ }
1095
+ /** The DOM scroll position `[scrollLeft, scrollTop]` (zero or more, like `element.scrollLeft`
1096
+ * and the `onScroll` event), or null if the element is not scrollable. */
1097
+ getScrollOffset(elementId) {
1098
+ this.native.flush();
1099
+ const result = this.native.getScrollOffset(elementId);
1100
+ if (!result)
1101
+ return null;
1102
+ return [result[0], result[1]];
1103
+ }
1104
+ /** The CSS keyword of the cursor the window asks the platform for where the pointer last
1105
+ * was (`"default"` when nothing asks). A mouse move updates it, a draw keeps it current.
1106
+ * Keywords that name one cursor read back as the first of them: `"pointer"`, `"grabbing"`,
1107
+ * `"nwse-resize"`, `"nesw-resize"`, `"not-allowed"`. */
1108
+ getCursor() {
1109
+ this.native.flush();
1110
+ return this.native.getCursor();
1111
+ }
1112
+ /** The logical scroll anchor of a `<virtual-list>`:
1113
+ * `[itemIndex, offsetInItemPx, viewportHeightPx]`, or null for anything
1114
+ * else. `itemIndex == item count` is gpui's at-end sentinel. Exact even
1115
+ * while row heights are still estimates, because it is the anchor gpui
1116
+ * itself scrolls by. */
1117
+ getListScrollTop(elementId) {
1118
+ this.native.flush();
1119
+ const result = this.native.getListScrollTop(elementId);
1120
+ if (!result)
1121
+ return null;
1122
+ return [result[0], result[1], result[2]];
1123
+ }
1124
+ // ── Selection API ───────────────────────────────────────────────
1125
+ /** Drag-select from (x1,y1) to (x2,y2) and return the selected text.
1126
+ *
1127
+ * Selection listeners are registered during **paint**, so the native helper
1128
+ * flushes between every step. Calling simulateMouseDown/Move/Up by hand
1129
+ * without those flushes selects nothing. */
1130
+ dragSelect(x1, y1, x2, y2) {
1131
+ this.native.dragSelect(x1, y1, x2, y2);
1132
+ return this.native.getSelectedText();
1133
+ }
1134
+ /** The current selection joined in document order, or null. */
1135
+ getSelectedText() {
1136
+ return this.native.getSelectedText();
1137
+ }
1138
+ /** Every string painted in the last frame, in paint order.
1139
+ *
1140
+ * `getAllText()` only sees `<text>` nodes in the retained tree. Native
1141
+ * elements like `<code>` and `<diff>` paint their text inside GPUI, so this
1142
+ * is the only way to assert on what they rendered. */
1143
+ getPaintedText() {
1144
+ return this.native.getPaintedText();
1145
+ }
1146
+ /** Every highlight wash painted in the last frame, in paint order.
1147
+ *
1148
+ * A quad never lands in `getPaintedText()`, and a soft-wrapped match must
1149
+ * draw one box per visual row, so each entry carries its `rects`. */
1150
+ getPaintedHighlights() {
1151
+ return this.native.getPaintedHighlights();
1152
+ }
1153
+ /** Syntax-cache counters as `[hits, misses, documents]`. */
1154
+ getSyntaxCacheStats() {
1155
+ const [hits, misses, documents] = this.native.getSyntaxCacheStats();
1156
+ return [hits, misses, documents];
1157
+ }
1158
+ clearSelection() {
1159
+ this.native.clearSelection();
1160
+ this.native.flush();
1161
+ }
1162
+ setDebugFrameOverlay(mode) {
1163
+ return this.native.setDebugFrameOverlay(mode);
1164
+ }
1165
+ getDebugFrameOverlay() {
1166
+ return this.native.getDebugFrameOverlay();
1167
+ }
1168
+ cycleDebugFrameOverlay() {
1169
+ return this.native.cycleDebugFrameOverlay();
1170
+ }
1171
+ resetDebugFrameOverlayStats() {
1172
+ this.native.resetDebugFrameOverlayStats();
1173
+ }
1174
+ getDebugFrameOverlayStats() {
1175
+ return this.native.getDebugFrameOverlayStats();
1176
+ }
1177
+ /** Capture the current Metal or DirectX frame and save it as a PNG. */
1178
+ captureScreenshot(path) {
1179
+ this.native.flush();
1180
+ this.native.captureScreenshot(path);
1181
+ }
1182
+ /** Whether the native GPUI test renderer is available. Always true. */
1183
+ get hasNative() {
1184
+ return true;
1185
+ }
1186
+ }
1187
+ //# sourceMappingURL=testing.js.map