@fluentui-react-native/desktop-driver 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 (235) hide show
  1. package/.cache/tsconfig.tsbuildinfo +1 -0
  2. package/AGENTS.md +68 -0
  3. package/CHANGELOG.md +16 -0
  4. package/PLAN.md +1394 -0
  5. package/README.md +223 -0
  6. package/SPEC.md +105 -0
  7. package/config/cli.cjs +8 -0
  8. package/jest.config.cjs +9 -0
  9. package/lib/agent/DesktopAgent.d.ts +56 -0
  10. package/lib/agent/DesktopAgent.d.ts.map +1 -0
  11. package/lib/agent/DesktopAgent.js +118 -0
  12. package/lib/agent/DesktopAgent.js.map +1 -0
  13. package/lib/agent/DesktopAgent.test.d.ts +2 -0
  14. package/lib/agent/DesktopAgent.test.d.ts.map +1 -0
  15. package/lib/agent/DesktopAgent.test.js +97 -0
  16. package/lib/agent/DesktopAgent.test.js.map +1 -0
  17. package/lib/agent/agent.contract.cjs +66 -0
  18. package/lib/agent/agent.contract.cjs.map +1 -0
  19. package/lib/agent/agent.contract.d.cts +2 -0
  20. package/lib/agent/agent.contract.d.cts.map +1 -0
  21. package/lib/agent/index.d.ts +3 -0
  22. package/lib/agent/index.d.ts.map +1 -0
  23. package/lib/agent/index.js +2 -0
  24. package/lib/agent/index.js.map +1 -0
  25. package/lib/artifacts/ArtifactManager.d.ts +13 -0
  26. package/lib/artifacts/ArtifactManager.d.ts.map +1 -0
  27. package/lib/artifacts/ArtifactManager.js +88 -0
  28. package/lib/artifacts/ArtifactManager.js.map +1 -0
  29. package/lib/artifacts/ArtifactManager.test.d.ts +2 -0
  30. package/lib/artifacts/ArtifactManager.test.d.ts.map +1 -0
  31. package/lib/artifacts/ArtifactManager.test.js +19 -0
  32. package/lib/artifacts/ArtifactManager.test.js.map +1 -0
  33. package/lib/artifacts/index.d.ts +2 -0
  34. package/lib/artifacts/index.d.ts.map +1 -0
  35. package/lib/artifacts/index.js +2 -0
  36. package/lib/artifacts/index.js.map +1 -0
  37. package/lib/authoring/index.d.ts +4 -0
  38. package/lib/authoring/index.d.ts.map +1 -0
  39. package/lib/authoring/index.js +2 -0
  40. package/lib/authoring/index.js.map +1 -0
  41. package/lib/authoring/results.d.ts +43 -0
  42. package/lib/authoring/results.d.ts.map +1 -0
  43. package/lib/authoring/results.js +2 -0
  44. package/lib/authoring/results.js.map +1 -0
  45. package/lib/authoring/storyTests.d.ts +81 -0
  46. package/lib/authoring/storyTests.d.ts.map +1 -0
  47. package/lib/authoring/storyTests.js +272 -0
  48. package/lib/authoring/storyTests.js.map +1 -0
  49. package/lib/authoring/storyTests.test.d.ts +2 -0
  50. package/lib/authoring/storyTests.test.d.ts.map +1 -0
  51. package/lib/authoring/storyTests.test.js +46 -0
  52. package/lib/authoring/storyTests.test.js.map +1 -0
  53. package/lib/cli/DesktopDriverCli.test.d.ts +2 -0
  54. package/lib/cli/DesktopDriverCli.test.d.ts.map +1 -0
  55. package/lib/cli/DesktopDriverCli.test.js +141 -0
  56. package/lib/cli/DesktopDriverCli.test.js.map +1 -0
  57. package/lib/cli/createDesktopDriverCommand.d.ts +8 -0
  58. package/lib/cli/createDesktopDriverCommand.d.ts.map +1 -0
  59. package/lib/cli/createDesktopDriverCommand.js +242 -0
  60. package/lib/cli/createDesktopDriverCommand.js.map +1 -0
  61. package/lib/cli/index.d.ts +3 -0
  62. package/lib/cli/index.d.ts.map +1 -0
  63. package/lib/cli/index.js +2 -0
  64. package/lib/cli/index.js.map +1 -0
  65. package/lib/client/DesktopDriverClient.d.ts +60 -0
  66. package/lib/client/DesktopDriverClient.d.ts.map +1 -0
  67. package/lib/client/DesktopDriverClient.js +155 -0
  68. package/lib/client/DesktopDriverClient.js.map +1 -0
  69. package/lib/client/index.d.ts +3 -0
  70. package/lib/client/index.d.ts.map +1 -0
  71. package/lib/client/index.js +2 -0
  72. package/lib/client/index.js.map +1 -0
  73. package/lib/host/types.d.ts +153 -0
  74. package/lib/host/types.d.ts.map +1 -0
  75. package/lib/host/types.js +2 -0
  76. package/lib/host/types.js.map +1 -0
  77. package/lib/hosts/fake/FakeDesktopHost.d.ts +72 -0
  78. package/lib/hosts/fake/FakeDesktopHost.d.ts.map +1 -0
  79. package/lib/hosts/fake/FakeDesktopHost.js +452 -0
  80. package/lib/hosts/fake/FakeDesktopHost.js.map +1 -0
  81. package/lib/index.d.ts +23 -0
  82. package/lib/index.d.ts.map +1 -0
  83. package/lib/index.js +11 -0
  84. package/lib/index.js.map +1 -0
  85. package/lib/protocol/actions.d.ts +11 -0
  86. package/lib/protocol/actions.d.ts.map +1 -0
  87. package/lib/protocol/actions.js +137 -0
  88. package/lib/protocol/actions.js.map +1 -0
  89. package/lib/protocol/capabilities.d.ts +11 -0
  90. package/lib/protocol/capabilities.d.ts.map +1 -0
  91. package/lib/protocol/capabilities.js +103 -0
  92. package/lib/protocol/capabilities.js.map +1 -0
  93. package/lib/protocol/constants.d.ts +2 -0
  94. package/lib/protocol/constants.d.ts.map +1 -0
  95. package/lib/protocol/constants.js +2 -0
  96. package/lib/protocol/constants.js.map +1 -0
  97. package/lib/protocol/errors.d.ts +37 -0
  98. package/lib/protocol/errors.d.ts.map +1 -0
  99. package/lib/protocol/errors.js +66 -0
  100. package/lib/protocol/errors.js.map +1 -0
  101. package/lib/protocol/timeouts.d.ts +2 -0
  102. package/lib/protocol/timeouts.d.ts.map +1 -0
  103. package/lib/protocol/timeouts.js +23 -0
  104. package/lib/protocol/timeouts.js.map +1 -0
  105. package/lib/protocol/types.d.ts +46 -0
  106. package/lib/protocol/types.d.ts.map +1 -0
  107. package/lib/protocol/types.js +2 -0
  108. package/lib/protocol/types.js.map +1 -0
  109. package/lib/runner/StoryTestRunner.d.ts +38 -0
  110. package/lib/runner/StoryTestRunner.d.ts.map +1 -0
  111. package/lib/runner/StoryTestRunner.js +497 -0
  112. package/lib/runner/StoryTestRunner.js.map +1 -0
  113. package/lib/runner/StoryTestRunner.test.d.ts +2 -0
  114. package/lib/runner/StoryTestRunner.test.d.ts.map +1 -0
  115. package/lib/runner/StoryTestRunner.test.js +349 -0
  116. package/lib/runner/StoryTestRunner.test.js.map +1 -0
  117. package/lib/runner/index.d.ts +3 -0
  118. package/lib/runner/index.d.ts.map +1 -0
  119. package/lib/runner/index.js +2 -0
  120. package/lib/runner/index.js.map +1 -0
  121. package/lib/server/SessionManager.d.ts +52 -0
  122. package/lib/server/SessionManager.d.ts.map +1 -0
  123. package/lib/server/SessionManager.js +208 -0
  124. package/lib/server/SessionManager.js.map +1 -0
  125. package/lib/server/TargetRegistry.d.ts +9 -0
  126. package/lib/server/TargetRegistry.d.ts.map +1 -0
  127. package/lib/server/TargetRegistry.js +24 -0
  128. package/lib/server/TargetRegistry.js.map +1 -0
  129. package/lib/server/createDesktopDriverServer.d.ts +19 -0
  130. package/lib/server/createDesktopDriverServer.d.ts.map +1 -0
  131. package/lib/server/createDesktopDriverServer.js +609 -0
  132. package/lib/server/createDesktopDriverServer.js.map +1 -0
  133. package/lib/server/desktopDriver.test.d.ts +2 -0
  134. package/lib/server/desktopDriver.test.d.ts.map +1 -0
  135. package/lib/server/desktopDriver.test.js +389 -0
  136. package/lib/server/desktopDriver.test.js.map +1 -0
  137. package/lib/server/index.d.ts +6 -0
  138. package/lib/server/index.d.ts.map +1 -0
  139. package/lib/server/index.js +4 -0
  140. package/lib/server/index.js.map +1 -0
  141. package/lib/server/webdriverio.contract.cjs +35 -0
  142. package/lib/server/webdriverio.contract.cjs.map +1 -0
  143. package/lib/server/webdriverio.contract.d.cts +2 -0
  144. package/lib/server/webdriverio.contract.d.cts.map +1 -0
  145. package/lib/server/webdriverio.test.d.ts +2 -0
  146. package/lib/server/webdriverio.test.d.ts.map +1 -0
  147. package/lib/server/webdriverio.test.js +40 -0
  148. package/lib/server/webdriverio.test.js.map +1 -0
  149. package/lib/storybook.d.ts +36 -0
  150. package/lib/storybook.d.ts.map +1 -0
  151. package/lib/storybook.js +2 -0
  152. package/lib/storybook.js.map +1 -0
  153. package/lib/testing/FakeStoryOrchestrator.d.ts +21 -0
  154. package/lib/testing/FakeStoryOrchestrator.d.ts.map +1 -0
  155. package/lib/testing/FakeStoryOrchestrator.js +44 -0
  156. package/lib/testing/FakeStoryOrchestrator.js.map +1 -0
  157. package/lib/testing/fakeStoryElements.d.ts +4 -0
  158. package/lib/testing/fakeStoryElements.d.ts.map +1 -0
  159. package/lib/testing/fakeStoryElements.js +143 -0
  160. package/lib/testing/fakeStoryElements.js.map +1 -0
  161. package/lib/testing/fakeStoryElements.test.d.ts +2 -0
  162. package/lib/testing/fakeStoryElements.test.d.ts.map +1 -0
  163. package/lib/testing/fakeStoryElements.test.js +38 -0
  164. package/lib/testing/fakeStoryElements.test.js.map +1 -0
  165. package/lib/testing/index.d.ts +7 -0
  166. package/lib/testing/index.d.ts.map +1 -0
  167. package/lib/testing/index.js +5 -0
  168. package/lib/testing/index.js.map +1 -0
  169. package/lib/testing/protocolHarness.d.ts +17 -0
  170. package/lib/testing/protocolHarness.d.ts.map +1 -0
  171. package/lib/testing/protocolHarness.js +46 -0
  172. package/lib/testing/protocolHarness.js.map +1 -0
  173. package/lib/wdio/DesktopWebdriver.d.ts +47 -0
  174. package/lib/wdio/DesktopWebdriver.d.ts.map +1 -0
  175. package/lib/wdio/DesktopWebdriver.js +76 -0
  176. package/lib/wdio/DesktopWebdriver.js.map +1 -0
  177. package/lib/wdio/DesktopWebdriver.test.d.ts +2 -0
  178. package/lib/wdio/DesktopWebdriver.test.d.ts.map +1 -0
  179. package/lib/wdio/DesktopWebdriver.test.js +74 -0
  180. package/lib/wdio/DesktopWebdriver.test.js.map +1 -0
  181. package/lib/wdio/index.d.ts +3 -0
  182. package/lib/wdio/index.d.ts.map +1 -0
  183. package/lib/wdio/index.js +2 -0
  184. package/lib/wdio/index.js.map +1 -0
  185. package/lib/wdio/wdioRunner.contract.cjs +64 -0
  186. package/lib/wdio/wdioRunner.contract.cjs.map +1 -0
  187. package/lib/wdio/wdioRunner.contract.d.cts +2 -0
  188. package/lib/wdio/wdioRunner.contract.d.cts.map +1 -0
  189. package/package.json +96 -0
  190. package/src/agent/DesktopAgent.test.ts +102 -0
  191. package/src/agent/DesktopAgent.ts +170 -0
  192. package/src/agent/agent.contract.cjs +32 -0
  193. package/src/agent/index.ts +8 -0
  194. package/src/artifacts/ArtifactManager.test.ts +20 -0
  195. package/src/artifacts/ArtifactManager.ts +105 -0
  196. package/src/artifacts/index.ts +1 -0
  197. package/src/authoring/index.ts +26 -0
  198. package/src/authoring/results.ts +47 -0
  199. package/src/authoring/storyTests.test.ts +58 -0
  200. package/src/authoring/storyTests.ts +345 -0
  201. package/src/cli/DesktopDriverCli.test.ts +150 -0
  202. package/src/cli/createDesktopDriverCommand.ts +290 -0
  203. package/src/cli/index.ts +2 -0
  204. package/src/client/DesktopDriverClient.ts +218 -0
  205. package/src/client/index.ts +2 -0
  206. package/src/host/types.ts +158 -0
  207. package/src/hosts/fake/FakeDesktopHost.ts +558 -0
  208. package/src/index.ts +101 -0
  209. package/src/protocol/actions.ts +163 -0
  210. package/src/protocol/capabilities.ts +129 -0
  211. package/src/protocol/constants.ts +1 -0
  212. package/src/protocol/errors.ts +76 -0
  213. package/src/protocol/timeouts.ts +26 -0
  214. package/src/protocol/types.ts +55 -0
  215. package/src/runner/StoryTestRunner.test.ts +362 -0
  216. package/src/runner/StoryTestRunner.ts +624 -0
  217. package/src/runner/index.ts +9 -0
  218. package/src/server/SessionManager.ts +302 -0
  219. package/src/server/TargetRegistry.ts +29 -0
  220. package/src/server/createDesktopDriverServer.ts +775 -0
  221. package/src/server/desktopDriver.test.ts +421 -0
  222. package/src/server/index.ts +5 -0
  223. package/src/server/webdriverio.contract.cjs +38 -0
  224. package/src/server/webdriverio.test.ts +41 -0
  225. package/src/storybook.ts +40 -0
  226. package/src/testing/FakeStoryOrchestrator.ts +55 -0
  227. package/src/testing/fakeStoryElements.test.ts +40 -0
  228. package/src/testing/fakeStoryElements.ts +167 -0
  229. package/src/testing/index.ts +6 -0
  230. package/src/testing/protocolHarness.ts +66 -0
  231. package/src/wdio/DesktopWebdriver.test.ts +76 -0
  232. package/src/wdio/DesktopWebdriver.ts +130 -0
  233. package/src/wdio/index.ts +2 -0
  234. package/src/wdio/wdioRunner.contract.cjs +30 -0
  235. package/tsconfig.json +15 -0
@@ -0,0 +1,558 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import type {
4
+ ApplicationLease,
5
+ DesktopHost,
6
+ DesktopHostEvent,
7
+ DesktopHostFeatures,
8
+ DesktopHostInfo,
9
+ DesktopTarget,
10
+ DesktopWindow,
11
+ NativeElementSnapshot,
12
+ NativeImage,
13
+ NativeActionSequence,
14
+ NativeSearchRoot,
15
+ NativeSelector,
16
+ Rect,
17
+ } from '../../host/types.js';
18
+ import { HostStaleError, HostUnsupportedError } from '../../protocol/errors.js';
19
+ import type { DesktopClickMode, DesktopEndpoint, DesktopPlatformName } from '../../protocol/types.js';
20
+
21
+ export type FakeDesktopElement = Omit<NativeElementSnapshot, 'checked' | 'enabled' | 'expanded' | 'focused' | 'selected' | 'visible'> & {
22
+ checked?: boolean | 'mixed';
23
+ enabled?: boolean;
24
+ expanded?: boolean;
25
+ focused?: boolean;
26
+ selected?: boolean;
27
+ visible?: boolean;
28
+ };
29
+
30
+ export type FakeDesktopWindow = Omit<DesktopWindow, 'rect'> & {
31
+ elements: readonly FakeDesktopElement[];
32
+ rect?: Rect;
33
+ };
34
+
35
+ export type FakeDesktopHostOptions = {
36
+ actionDelayMs?: number;
37
+ closeDelayMs?: number;
38
+ endpoint?: DesktopEndpoint;
39
+ features?: Partial<DesktopHostFeatures>;
40
+ launchDelayMs?: number;
41
+ platformName?: DesktopPlatformName;
42
+ screenshot?: Uint8Array;
43
+ storyRootTestId?: string;
44
+ windows?: readonly FakeDesktopWindow[];
45
+ };
46
+
47
+ const onePixelPng = Uint8Array.from(
48
+ Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64'),
49
+ );
50
+
51
+ const defaultRect = { x: 0, y: 0, width: 800, height: 600 };
52
+ const defaultFeatures: DesktopHostFeatures = {
53
+ accessibilityClick: true,
54
+ elementScreenshot: true,
55
+ focus: true,
56
+ keyboard: true,
57
+ physicalClick: true,
58
+ screenshot: true,
59
+ setWindowRect: true,
60
+ wheel: true,
61
+ };
62
+
63
+ export class FakeDesktopHost implements DesktopHost {
64
+ readonly endpoint: DesktopEndpoint;
65
+ readonly actions: Record<string, unknown>[] = [];
66
+
67
+ private readonly platformName: DesktopPlatformName;
68
+ private readonly actionDelayMs: number;
69
+ private readonly closeDelayMs: number;
70
+ private readonly features: DesktopHostFeatures;
71
+ private readonly launchDelayMs: number;
72
+ private readonly screenshot: Uint8Array;
73
+ private readonly windowsById = new Map<string, DesktopWindow>();
74
+ private readonly elements = new Map<string, NativeElementSnapshot>();
75
+ private readonly initialElements = new Map<string, NativeElementSnapshot>();
76
+ private readonly listeners = new Set<(event: DesktopHostEvent) => void>();
77
+
78
+ constructor(options: FakeDesktopHostOptions = {}) {
79
+ this.endpoint = options.endpoint ?? 'windows';
80
+ this.actionDelayMs = options.actionDelayMs ?? 0;
81
+ this.closeDelayMs = options.closeDelayMs ?? 0;
82
+ this.platformName = options.platformName ?? (this.endpoint === 'macos' ? 'macos' : 'windows');
83
+ this.features = { ...defaultFeatures, ...options.features };
84
+ this.launchDelayMs = options.launchDelayMs ?? 0;
85
+ this.screenshot = options.screenshot ?? onePixelPng;
86
+
87
+ const windows = options.windows ?? [createDefaultWindow(options.storyRootTestId)];
88
+ for (const window of windows) {
89
+ this.windowsById.set(window.id, {
90
+ id: window.id,
91
+ title: window.title,
92
+ rect: { ...defaultRect, ...window.rect },
93
+ });
94
+ for (const element of window.elements) {
95
+ this.elements.set(element.id, toSnapshot(element));
96
+ }
97
+ }
98
+ for (const [id, element] of this.elements) {
99
+ this.initialElements.set(id, cloneElement(element));
100
+ }
101
+ }
102
+
103
+ setElementName(elementId: string, name: string): void {
104
+ this.requireElement(elementId).name = name;
105
+ }
106
+
107
+ setElementStateUnsupported(
108
+ elementId: string,
109
+ state: 'checked' | 'enabled' | 'expanded' | 'focused' | 'selected' | 'visible',
110
+ reason: string,
111
+ ): void {
112
+ const update = (element: NativeElementSnapshot) => {
113
+ switch (state) {
114
+ case 'checked':
115
+ element.checked = { supported: false, reason };
116
+ break;
117
+ case 'enabled':
118
+ element.enabled = { supported: false, reason };
119
+ break;
120
+ case 'expanded':
121
+ element.expanded = { supported: false, reason };
122
+ break;
123
+ case 'focused':
124
+ element.focused = { supported: false, reason };
125
+ break;
126
+ case 'selected':
127
+ element.selected = { supported: false, reason };
128
+ break;
129
+ case 'visible':
130
+ element.visible = { supported: false, reason };
131
+ break;
132
+ }
133
+ };
134
+ update(this.requireElement(elementId));
135
+ const initial = this.initialElements.get(elementId);
136
+ if (initial) {
137
+ update(initial);
138
+ }
139
+ }
140
+
141
+ async probe(signal?: AbortSignal): Promise<DesktopHostInfo> {
142
+ throwIfAborted(signal);
143
+ return {
144
+ endpoint: this.endpoint,
145
+ features: { ...this.features },
146
+ platformName: this.platformName,
147
+ protocolVersion: 1,
148
+ };
149
+ }
150
+
151
+ async launch(target: DesktopTarget, signal?: AbortSignal): Promise<ApplicationLease> {
152
+ this.actions.push({ type: 'launch', target: target.id });
153
+ await delay(this.launchDelayMs, signal);
154
+ return { id: randomUUID(), ownership: 'launched', processId: 1000, processStartedAt: new Date(0).toISOString() };
155
+ }
156
+
157
+ async attach(target: DesktopTarget, signal?: AbortSignal): Promise<ApplicationLease> {
158
+ this.actions.push({ type: 'attach', target: target.id });
159
+ await delay(this.launchDelayMs, signal);
160
+ return { id: randomUUID(), ownership: 'attached', processId: 1000, processStartedAt: new Date(0).toISOString() };
161
+ }
162
+
163
+ async closeApplication(lease: ApplicationLease, signal?: AbortSignal): Promise<void> {
164
+ throwIfAborted(signal);
165
+ this.actions.push({ type: 'close-application-start', lease: lease.id });
166
+ await delay(this.closeDelayMs, signal);
167
+ this.actions.push({ type: 'close-application', lease: lease.id, ownership: lease.ownership });
168
+ }
169
+
170
+ async windows(_lease: ApplicationLease): Promise<DesktopWindow[]> {
171
+ return [...this.windowsById.values()].map(cloneWindow);
172
+ }
173
+
174
+ async closeWindow(windowId: string): Promise<void> {
175
+ if (!this.windowsById.delete(windowId)) {
176
+ throw new HostStaleError(`Window "${windowId}" is no longer available.`);
177
+ }
178
+ for (const [id, element] of this.elements) {
179
+ if (element.windowId === windowId) {
180
+ this.elements.delete(id);
181
+ }
182
+ }
183
+ for (const listener of this.listeners) {
184
+ listener({ type: 'window-closed', windowId });
185
+ }
186
+ }
187
+
188
+ async activate(windowId: string): Promise<void> {
189
+ this.requireWindow(windowId);
190
+ this.actions.push({ type: 'activate', windowId });
191
+ }
192
+
193
+ async getWindowRect(windowId: string): Promise<Rect> {
194
+ return { ...this.requireWindow(windowId).rect };
195
+ }
196
+
197
+ async setWindowRect(windowId: string, rect: Partial<Rect>): Promise<Rect> {
198
+ if (!this.features.setWindowRect) {
199
+ throw new HostUnsupportedError('The fake target does not support setting the window rectangle.');
200
+ }
201
+ const window = this.requireWindow(windowId);
202
+ window.rect = { ...window.rect, ...rect };
203
+ return { ...window.rect };
204
+ }
205
+
206
+ async find(root: NativeSearchRoot, selector: NativeSelector): Promise<NativeElementSnapshot[]> {
207
+ this.requireWindow(root.windowId);
208
+ if (root.elementId) {
209
+ this.requireElement(root.elementId);
210
+ }
211
+
212
+ return [...this.elements.values()]
213
+ .filter((element) => element.windowId === root.windowId)
214
+ .filter((element) => !root.elementId || isDescendantOf(element, root.elementId, this.elements))
215
+ .filter((element) => matches(element, selector))
216
+ .map(cloneElement);
217
+ }
218
+
219
+ async snapshot(elementId: string): Promise<NativeElementSnapshot> {
220
+ return cloneElement(this.requireElement(elementId));
221
+ }
222
+
223
+ async activeElement(windowId: string): Promise<NativeElementSnapshot | null> {
224
+ this.requireWindow(windowId);
225
+ const focused = [...this.elements.values()].find(
226
+ (element) => element.windowId === windowId && element.focused.supported && element.focused.value,
227
+ );
228
+ return focused ? cloneElement(focused) : null;
229
+ }
230
+
231
+ async hitTest(windowId: string, x: number, y: number): Promise<NativeElementSnapshot | null> {
232
+ this.requireWindow(windowId);
233
+ const matchesPoint = [...this.elements.values()].filter(
234
+ (element) =>
235
+ element.windowId === windowId &&
236
+ isSupportedTrue(element.visible) &&
237
+ x >= element.rect.x &&
238
+ y >= element.rect.y &&
239
+ x <= element.rect.x + element.rect.width &&
240
+ y <= element.rect.y + element.rect.height,
241
+ );
242
+ const element = matchesPoint.at(-1);
243
+ return element ? cloneElement(element) : null;
244
+ }
245
+
246
+ async click(elementId: string, mode: DesktopClickMode, signal?: AbortSignal): Promise<void> {
247
+ throwIfAborted(signal);
248
+ const element = this.requireElement(elementId);
249
+ const resolvedMode =
250
+ mode === 'auto' ? (this.features.physicalClick ? 'physical' : this.features.accessibilityClick ? 'accessibility' : undefined) : mode;
251
+ if (
252
+ !resolvedMode ||
253
+ (resolvedMode === 'physical' && !this.features.physicalClick) ||
254
+ (resolvedMode === 'accessibility' && !this.features.accessibilityClick)
255
+ ) {
256
+ throw new HostUnsupportedError(`Click mode "${mode}" is unavailable.`);
257
+ }
258
+ for (const current of this.elements.values()) {
259
+ if (current.windowId === element.windowId && current.focused.supported) {
260
+ current.focused = { supported: true, value: current.id === element.id };
261
+ }
262
+ }
263
+ if (element.role === 'checkbox' && element.checked.supported) {
264
+ element.checked = { supported: true, value: element.checked.value === true ? false : true };
265
+ }
266
+ this.actions.push({ type: 'click', elementId, mode: resolvedMode });
267
+ }
268
+
269
+ async clear(elementId: string, signal?: AbortSignal): Promise<void> {
270
+ throwIfAborted(signal);
271
+ const element = this.requireElement(elementId);
272
+ element.value = '';
273
+ element.text = '';
274
+ this.actions.push({ type: 'clear', elementId });
275
+ }
276
+
277
+ async sendKeys(elementId: string, text: string, signal?: AbortSignal): Promise<void> {
278
+ throwIfAborted(signal);
279
+ const element = this.requireElement(elementId);
280
+ element.value = `${element.value ?? ''}${text}`;
281
+ element.text = element.value;
282
+ this.actions.push({ type: 'send-keys', elementId, text });
283
+ }
284
+
285
+ async performActions(actions: readonly NativeActionSequence[], signal?: AbortSignal): Promise<void> {
286
+ this.actions.push({ type: 'actions-start' });
287
+ await delay(this.actionDelayMs, signal);
288
+ this.actions.push({ type: 'actions', actions });
289
+ }
290
+
291
+ async releaseActions(signal?: AbortSignal): Promise<void> {
292
+ throwIfAborted(signal);
293
+ this.actions.push({ type: 'release-actions' });
294
+ }
295
+
296
+ async captureWindow(windowId: string): Promise<NativeImage> {
297
+ this.requireWindow(windowId);
298
+ if (!this.features.screenshot) {
299
+ throw new HostUnsupportedError('The fake target does not support screenshots.');
300
+ }
301
+ return createImage(this.screenshot);
302
+ }
303
+
304
+ async captureElement(elementId: string): Promise<NativeImage> {
305
+ this.requireElement(elementId);
306
+ if (!this.features.elementScreenshot) {
307
+ throw new HostUnsupportedError('The fake target does not support element screenshots.');
308
+ }
309
+ return createImage(this.screenshot);
310
+ }
311
+
312
+ async source(windowId: string): Promise<string> {
313
+ this.requireWindow(windowId);
314
+ const roots = [...this.elements.values()].filter((element) => element.windowId === windowId && !element.parentId);
315
+ return `<application>${roots.map((element) => serializeElement(element, this.elements)).join('')}</application>`;
316
+ }
317
+
318
+ async tree(windowId: string): Promise<NativeElementSnapshot[]> {
319
+ this.requireWindow(windowId);
320
+ return [...this.elements.values()].filter((element) => element.windowId === windowId).map(cloneElement);
321
+ }
322
+
323
+ subscribe(listener: (event: DesktopHostEvent) => void): () => void {
324
+ this.listeners.add(listener);
325
+ return () => this.listeners.delete(listener);
326
+ }
327
+
328
+ async dispose(signal?: AbortSignal): Promise<void> {
329
+ throwIfAborted(signal);
330
+ this.listeners.clear();
331
+ this.actions.push({ type: 'dispose' });
332
+ }
333
+
334
+ removeElement(elementId: string): void {
335
+ if (this.elements.delete(elementId)) {
336
+ for (const [id, element] of this.elements) {
337
+ if (element.parentId === elementId) {
338
+ this.removeElement(id);
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ replacePreview(elements: readonly FakeDesktopElement[]): void {
345
+ for (const [id, element] of this.elements) {
346
+ if (element.scope === 'preview') {
347
+ this.elements.delete(id);
348
+ }
349
+ }
350
+ for (const element of elements) {
351
+ this.elements.set(element.id, toSnapshot(element));
352
+ }
353
+ for (const windowId of new Set(elements.map((element) => element.windowId))) {
354
+ for (const listener of this.listeners) {
355
+ listener({ type: 'structure-changed', windowId });
356
+ }
357
+ }
358
+ }
359
+
360
+ resetPreview(): void {
361
+ for (const [id, element] of this.elements) {
362
+ if (element.scope === 'preview') {
363
+ this.elements.delete(id);
364
+ }
365
+ }
366
+ for (const [id, element] of this.initialElements) {
367
+ if (element.scope === 'preview') {
368
+ this.elements.set(id, cloneElement(element));
369
+ }
370
+ }
371
+ }
372
+
373
+ private requireWindow(windowId: string): DesktopWindow {
374
+ const window = this.windowsById.get(windowId);
375
+ if (!window) {
376
+ throw new HostStaleError(`Window "${windowId}" is no longer available.`);
377
+ }
378
+ return window;
379
+ }
380
+
381
+ private requireElement(elementId: string): NativeElementSnapshot {
382
+ const element = this.elements.get(elementId);
383
+ if (!element) {
384
+ throw new HostStaleError(`Element "${elementId}" is no longer available.`);
385
+ }
386
+ return element;
387
+ }
388
+ }
389
+
390
+ function createDefaultWindow(storyRootTestId = 'story-root'): FakeDesktopWindow {
391
+ return {
392
+ id: 'window-1',
393
+ title: 'Fake Desktop App',
394
+ elements: [
395
+ {
396
+ id: 'root',
397
+ automationId: 'app-root',
398
+ enabled: true,
399
+ focused: false,
400
+ rect: defaultRect,
401
+ role: 'application',
402
+ scope: 'application',
403
+ selected: false,
404
+ visible: true,
405
+ windowId: 'window-1',
406
+ },
407
+ {
408
+ id: 'story-root',
409
+ automationId: storyRootTestId,
410
+ enabled: true,
411
+ focused: false,
412
+ name: JSON.stringify({ previewGeneration: 0, storyId: 'initial--story' }),
413
+ parentId: 'root',
414
+ rect: defaultRect,
415
+ role: 'group',
416
+ scope: 'preview',
417
+ selected: false,
418
+ visible: true,
419
+ windowId: 'window-1',
420
+ },
421
+ {
422
+ id: 'button',
423
+ automationId: 'button-primary',
424
+ enabled: true,
425
+ focused: false,
426
+ name: 'Primary',
427
+ parentId: 'root',
428
+ rect: { x: 10, y: 10, width: 120, height: 40 },
429
+ role: 'button',
430
+ scope: 'preview',
431
+ selected: false,
432
+ text: 'Primary',
433
+ visible: true,
434
+ windowId: 'window-1',
435
+ },
436
+ {
437
+ id: 'input',
438
+ automationId: 'input-name',
439
+ enabled: true,
440
+ focused: false,
441
+ name: 'Name',
442
+ parentId: 'root',
443
+ rect: { x: 10, y: 60, width: 200, height: 40 },
444
+ role: 'textbox',
445
+ scope: 'preview',
446
+ selected: false,
447
+ value: '',
448
+ visible: true,
449
+ windowId: 'window-1',
450
+ },
451
+ ],
452
+ };
453
+ }
454
+
455
+ function toSnapshot(element: FakeDesktopElement): NativeElementSnapshot {
456
+ return {
457
+ ...element,
458
+ rect: { ...element.rect },
459
+ checked: { supported: true, value: element.checked ?? false },
460
+ enabled: { supported: true, value: element.enabled ?? true },
461
+ expanded: { supported: true, value: element.expanded ?? false },
462
+ focused: { supported: true, value: element.focused ?? false },
463
+ selected: { supported: true, value: element.selected ?? false },
464
+ visible: { supported: true, value: element.visible ?? true },
465
+ };
466
+ }
467
+
468
+ function cloneElement(element: NativeElementSnapshot): NativeElementSnapshot {
469
+ return {
470
+ ...element,
471
+ rect: { ...element.rect },
472
+ checked: { ...element.checked },
473
+ enabled: { ...element.enabled },
474
+ expanded: { ...element.expanded },
475
+ focused: { ...element.focused },
476
+ selected: { ...element.selected },
477
+ visible: { ...element.visible },
478
+ };
479
+ }
480
+
481
+ function cloneWindow(window: DesktopWindow): DesktopWindow {
482
+ return { ...window, rect: { ...window.rect } };
483
+ }
484
+
485
+ function isDescendantOf(element: NativeElementSnapshot, ancestorId: string, elements: ReadonlyMap<string, NativeElementSnapshot>): boolean {
486
+ let parentId = element.parentId;
487
+ while (parentId) {
488
+ if (parentId === ancestorId) {
489
+ return true;
490
+ }
491
+ parentId = elements.get(parentId)?.parentId;
492
+ }
493
+ return false;
494
+ }
495
+
496
+ function matches(element: NativeElementSnapshot, selector: NativeSelector): boolean {
497
+ switch (selector.strategy) {
498
+ case '-furn:text':
499
+ return element.text?.includes(selector.value) ?? element.value?.includes(selector.value) ?? false;
500
+ case 'accessibility id':
501
+ return element.automationId === selector.value;
502
+ case 'tag name':
503
+ return element.role === selector.value;
504
+ case 'link text':
505
+ return element.name === selector.value;
506
+ case 'partial link text':
507
+ return element.name?.includes(selector.value) ?? false;
508
+ }
509
+ }
510
+
511
+ function isSupportedTrue(value: NativeElementSnapshot['visible']): boolean {
512
+ return value.supported && value.value;
513
+ }
514
+
515
+ function createImage(data: Uint8Array): NativeImage {
516
+ return { data: Uint8Array.from(data), height: 1, mimeType: 'image/png', scaleFactor: 1, width: 1 };
517
+ }
518
+
519
+ function serializeElement(element: NativeElementSnapshot, elements: ReadonlyMap<string, NativeElementSnapshot>): string {
520
+ const attributes = [
521
+ `id="${escapeXml(element.automationId ?? '')}"`,
522
+ `name="${escapeXml(element.name ?? '')}"`,
523
+ `role="${escapeXml(element.role)}"`,
524
+ ].join(' ');
525
+ const children = [...elements.values()]
526
+ .filter((candidate) => candidate.parentId === element.id)
527
+ .map((child) => serializeElement(child, elements))
528
+ .join('');
529
+ return `<element ${attributes}>${children}</element>`;
530
+ }
531
+
532
+ function escapeXml(value: string): string {
533
+ return value.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
534
+ }
535
+
536
+ function throwIfAborted(signal: AbortSignal | undefined): void {
537
+ if (signal?.aborted) {
538
+ throw signal.reason instanceof Error ? signal.reason : new Error('Fake desktop host operation was aborted.');
539
+ }
540
+ }
541
+
542
+ function delay(milliseconds: number, signal?: AbortSignal): Promise<void> {
543
+ throwIfAborted(signal);
544
+ if (!signal) {
545
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
546
+ }
547
+ return new Promise((resolve, reject) => {
548
+ const timer = setTimeout(() => {
549
+ signal.removeEventListener('abort', onAbort);
550
+ resolve();
551
+ }, milliseconds);
552
+ const onAbort = () => {
553
+ clearTimeout(timer);
554
+ reject(signal.reason instanceof Error ? signal.reason : new Error('Fake desktop host operation was aborted.'));
555
+ };
556
+ signal.addEventListener('abort', onAbort, { once: true });
557
+ });
558
+ }
package/src/index.ts ADDED
@@ -0,0 +1,101 @@
1
+ export {
2
+ createDesktopDriverClient,
3
+ DesktopDriverClient,
4
+ DesktopElementClient,
5
+ DesktopSessionClient,
6
+ } from './client/DesktopDriverClient.js';
7
+ export type { DesktopDriverClientOptions } from './client/DesktopDriverClient.js';
8
+ export { connectDesktopAgent, DesktopAgent } from './agent/DesktopAgent.js';
9
+ export type {
10
+ DesktopAgentCheckResult,
11
+ DesktopAgentDescribeOptions,
12
+ DesktopAgentElement,
13
+ DesktopAgentOptions,
14
+ DesktopAgentStory,
15
+ } from './agent/DesktopAgent.js';
16
+ export { ArtifactManager } from './artifacts/ArtifactManager.js';
17
+ export { createDesktopDriverCommand, runDesktopDriverCli } from './cli/createDesktopDriverCommand.js';
18
+ export type { CreateDesktopDriverCommandOptions } from './cli/createDesktopDriverCommand.js';
19
+ export type {
20
+ DesktopArtifact,
21
+ DesktopRunStatus,
22
+ DesktopStepStatus,
23
+ DesktopStoryRunResult,
24
+ DesktopStoryStepResult,
25
+ DesktopStoryTestResult,
26
+ DesktopTestStatus,
27
+ } from './authoring/results.js';
28
+ export {
29
+ defineDesktopStoryTests,
30
+ desktopBy,
31
+ desktopStoryCapabilities,
32
+ desktopStoryPlatforms,
33
+ validateDesktopStoryTests,
34
+ } from './authoring/storyTests.js';
35
+ export type {
36
+ DesktopStoryCapability,
37
+ DesktopStoryExpectation,
38
+ DesktopStoryPlatform,
39
+ DesktopStorySelector,
40
+ DesktopStoryState,
41
+ DesktopStoryStep,
42
+ DesktopStoryTest,
43
+ DesktopStoryTests,
44
+ } from './authoring/storyTests.js';
45
+ export type {
46
+ ApplicationLease,
47
+ DesktopHost,
48
+ DesktopHostFeatures,
49
+ DesktopHostInfo,
50
+ DesktopTarget,
51
+ DesktopTreeNode,
52
+ DesktopWindow,
53
+ NativeElementScope,
54
+ NativeElementSnapshot,
55
+ NativeImage,
56
+ NativeAction,
57
+ NativeActionOrigin,
58
+ NativeActionSequence,
59
+ NativeSearchRoot,
60
+ NativeSelector,
61
+ Rect,
62
+ SupportedValue,
63
+ } from './host/types.js';
64
+ export { webElementIdentifier } from './protocol/constants.js';
65
+ export { HostStaleError, HostUnsupportedError, WebDriverError } from './protocol/errors.js';
66
+ export type { WebDriverErrorCode } from './protocol/errors.js';
67
+ export {
68
+ type DesktopClickMode,
69
+ type DesktopEndpoint,
70
+ type DesktopPlatformName,
71
+ type DesktopRenderer,
72
+ type DesktopTimeouts,
73
+ type NewSessionCapabilities,
74
+ type NewSessionRequest,
75
+ type WebDriverAction,
76
+ type WebDriverActionSequence,
77
+ type WebDriverElement,
78
+ type WebDriverErrorResponse,
79
+ type WebDriverResponse,
80
+ type WebDriverTimeouts,
81
+ } from './protocol/types.js';
82
+ export { createDesktopDriverServer, SessionManager, TargetRegistry } from './server/index.js';
83
+ export type { DesktopDriverServer, DesktopDriverServerOptions, DesktopSession, ElementRecord } from './server/index.js';
84
+ export type {
85
+ DesktopStoryManifest,
86
+ DesktopStoryManifestEntry,
87
+ StoryOrchestrator,
88
+ StoryReadyResult,
89
+ StorySelectionRequest,
90
+ } from './storybook.js';
91
+ export {
92
+ assertDesktopExpectation,
93
+ DesktopAssertionError,
94
+ findDesktopElement,
95
+ findDesktopElements,
96
+ runDesktopStoryTests,
97
+ selectDesktopStoryTests,
98
+ } from './runner/StoryTestRunner.js';
99
+ export type { DesktopStoryTestRunnerOptions, DesktopStoryTestSelection } from './runner/StoryTestRunner.js';
100
+ export { connectDesktopWebdriver, DesktopWebdriverSession } from './wdio/DesktopWebdriver.js';
101
+ export type { DesktopWebdriverOptions, DesktopWebdriverRunOptions } from './wdio/DesktopWebdriver.js';