@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,150 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import { createServer } from 'node:net';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ import type { DesktopStoryManifest } from '../storybook.js';
8
+
9
+ jest.setTimeout(30_000);
10
+
11
+ describe('desktop-driver CLI', () => {
12
+ test('serves, lists, runs, and describes a fake authored plan as JSON', async () => {
13
+ const port = await getAvailablePort();
14
+ const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-driver-cli-'));
15
+ const artifactsRoot = path.join(temporaryDirectory, 'artifacts');
16
+ const manifestPath = path.join(temporaryDirectory, 'manifest.json');
17
+ const manifest: DesktopStoryManifest = {
18
+ endpoint: 'windows',
19
+ entries: [
20
+ {
21
+ id: 'components-button--default',
22
+ name: 'Default',
23
+ packageName: '@fluentui-react-native/components',
24
+ sourcePath: 'src/components/button/button.stories.tsx',
25
+ tags: ['e2e', 'story'],
26
+ tests: {
27
+ version: 1,
28
+ tests: [
29
+ {
30
+ id: 'cli-plan',
31
+ steps: [{ expect: { state: 'enabled', target: { testId: 'button-primary' }, value: true } }],
32
+ },
33
+ ],
34
+ },
35
+ title: 'Components/Button',
36
+ },
37
+ ],
38
+ platformManifestDigest: 'platform-digest',
39
+ portablePlanDigest: 'portable-digest',
40
+ schemaVersion: 1,
41
+ };
42
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest));
43
+ const server = spawnCli(['serve', '--port', String(port), '--target', 'cli-target', '--manifest', manifestPath]);
44
+ try {
45
+ await waitForResponse(loopbackUrl(port, '/status'));
46
+ const url = loopbackUrl(port, '');
47
+ const stories = await runCli(['stories', 'list', '--url', url, '--target', 'cli-target']);
48
+ expect(stories).toMatchObject({ entries: [{ id: 'components-button--default' }] });
49
+
50
+ const result = await runCli([
51
+ 'stories',
52
+ 'run',
53
+ '--url',
54
+ url,
55
+ '--target',
56
+ 'cli-target',
57
+ '--artifacts',
58
+ artifactsRoot,
59
+ '--test',
60
+ 'cli-plan',
61
+ ]);
62
+ expect(result).toMatchObject({ status: 'passed', tests: [{ testId: 'cli-plan' }] });
63
+
64
+ const tree = await runCli([
65
+ 'agent',
66
+ 'describe',
67
+ '--url',
68
+ url,
69
+ '--target',
70
+ 'cli-target',
71
+ '--artifacts',
72
+ artifactsRoot,
73
+ '--scope',
74
+ 'story',
75
+ ]);
76
+ expect(tree).toMatchObject([{ testId: 'story-root' }]);
77
+ } finally {
78
+ server.kill();
79
+ await waitForExit(server);
80
+ fs.rmSync(temporaryDirectory, { force: true, recursive: true });
81
+ }
82
+ });
83
+ });
84
+
85
+ function spawnCli(args: readonly string[]) {
86
+ return spawn(process.execPath, [path.resolve(__dirname, '../../config/cli.cjs'), ...args], {
87
+ stdio: ['ignore', 'pipe', 'pipe'],
88
+ });
89
+ }
90
+
91
+ function runCli(args: readonly string[]): Promise<Record<string, unknown>> {
92
+ return new Promise((resolve, reject) => {
93
+ const child = spawnCli(args);
94
+ const stdout: Buffer[] = [];
95
+ const stderr: Buffer[] = [];
96
+ child.stdout.on('data', (chunk: Buffer) => stdout.push(chunk));
97
+ child.stderr.on('data', (chunk: Buffer) => stderr.push(chunk));
98
+ child.once('error', reject);
99
+ child.once('exit', (code) => {
100
+ if (code !== 0) {
101
+ reject(new Error(Buffer.concat(stderr).toString('utf8') || `Desktop Driver CLI exited with code ${code}.`));
102
+ return;
103
+ }
104
+ resolve(JSON.parse(Buffer.concat(stdout).toString('utf8')) as Record<string, unknown>);
105
+ });
106
+ });
107
+ }
108
+
109
+ function getAvailablePort(): Promise<number> {
110
+ return new Promise((resolve, reject) => {
111
+ const server = createServer();
112
+ server.once('error', reject);
113
+ server.listen(0, '127.0.0.1', () => {
114
+ const address = server.address();
115
+ if (!address || typeof address === 'string') {
116
+ reject(new Error('Could not allocate a loopback port.'));
117
+ return;
118
+ }
119
+ server.close((error) => (error ? reject(error) : resolve(address.port)));
120
+ });
121
+ });
122
+ }
123
+
124
+ async function waitForResponse(url: string): Promise<void> {
125
+ const deadline = Date.now() + 20_000;
126
+ do {
127
+ try {
128
+ const response = await fetch(url);
129
+ if (response.ok) {
130
+ return;
131
+ }
132
+ } catch {
133
+ // Retry until the bounded startup deadline.
134
+ }
135
+ await new Promise((resolve) => setTimeout(resolve, 100));
136
+ } while (Date.now() < deadline);
137
+ throw new Error(`Timed out waiting for ${url}.`);
138
+ }
139
+
140
+ function waitForExit(child: ReturnType<typeof spawn>): Promise<void> {
141
+ if (child.exitCode !== null || child.signalCode !== null) {
142
+ return Promise.resolve();
143
+ }
144
+ return new Promise((resolve) => child.once('exit', () => resolve()));
145
+ }
146
+
147
+ function loopbackUrl(port: number, pathname: string): string {
148
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url -- test-only loopback service
149
+ return `http://127.0.0.1:${port}${pathname}`;
150
+ }
@@ -0,0 +1,290 @@
1
+ import fs from 'node:fs';
2
+
3
+ import { Command, Option } from 'commander';
4
+
5
+ import { connectDesktopAgent } from '../agent/DesktopAgent.js';
6
+ import { validateDesktopStoryTests } from '../authoring/storyTests.js';
7
+ import type { DesktopStoryManifest } from '../storybook.js';
8
+ import { FakeDesktopHost } from '../hosts/fake/FakeDesktopHost.js';
9
+ import type { DesktopEndpoint, DesktopPlatformName, DesktopRenderer } from '../protocol/types.js';
10
+ import { createDesktopDriverServer } from '../server/createDesktopDriverServer.js';
11
+ import { FakeStoryOrchestrator } from '../testing/FakeStoryOrchestrator.js';
12
+ import { createFakeStoryWindows } from '../testing/fakeStoryElements.js';
13
+ import { connectDesktopWebdriver } from '../wdio/DesktopWebdriver.js';
14
+
15
+ type ConnectionFlags = {
16
+ artifacts?: string;
17
+ platform: DesktopPlatformName;
18
+ target: string;
19
+ url: string;
20
+ };
21
+
22
+ type SelectionFlags = ConnectionFlags & {
23
+ shardCount?: number;
24
+ shardIndex?: number;
25
+ story?: string;
26
+ tag?: string;
27
+ test?: string;
28
+ };
29
+
30
+ export type CreateDesktopDriverCommandOptions = {
31
+ stderr?: Pick<NodeJS.WriteStream, 'write'>;
32
+ stdout?: Pick<NodeJS.WriteStream, 'write'>;
33
+ };
34
+
35
+ export function createDesktopDriverCommand(options: CreateDesktopDriverCommandOptions = {}): Command {
36
+ const stdout = options.stdout ?? process.stdout;
37
+ const stderr = options.stderr ?? process.stderr;
38
+ const program = new Command()
39
+ .name('desktop-driver')
40
+ .description('Drive React Native desktop applications through W3C WebDriver without Appium.')
41
+ .configureOutput({
42
+ writeErr: (value) => stderr.write(value),
43
+ writeOut: (value) => stdout.write(value),
44
+ });
45
+
46
+ program
47
+ .command('serve')
48
+ .description('Start a deterministic fake Desktop Driver target.')
49
+ .option('--host <host>', 'loopback host', '127.0.0.1')
50
+ .option('--port <port>', 'listener port; defaults to an available port', parsePort)
51
+ .option('--target <id>', 'registered target id', 'desktop-driver-fake')
52
+ .addOption(new Option('--platform <name>', 'WebDriver platform name').choices(['macos', 'windows']).default('windows'))
53
+ .addOption(new Option('--endpoint <name>', 'desktop endpoint').choices(['macos', 'windows', 'win32']).default('windows'))
54
+ .addOption(new Option('--renderer <name>', 'React Native renderer').choices(['fabric', 'paper']).default('fabric'))
55
+ .option('--manifest <path>', 'optional Story Manifest for fake story orchestration')
56
+ .action(async (flags) => {
57
+ const manifest = flags.manifest ? readManifest(flags.manifest) : undefined;
58
+ const host = new FakeDesktopHost({
59
+ endpoint: flags.endpoint as DesktopEndpoint,
60
+ platformName: flags.platform as DesktopPlatformName,
61
+ ...(manifest ? { storyRootTestId: 'story-root' } : {}),
62
+ ...(manifest ? { windows: createFakeStoryWindows(manifest) } : {}),
63
+ });
64
+ const storyOrchestrator = manifest ? new FakeStoryOrchestrator(manifest, host) : undefined;
65
+ const server = await createDesktopDriverServer({
66
+ host: flags.host,
67
+ port: flags.port,
68
+ targets: [
69
+ {
70
+ endpoint: flags.endpoint as DesktopEndpoint,
71
+ host,
72
+ id: flags.target,
73
+ platformName: flags.platform as DesktopPlatformName,
74
+ renderer: flags.renderer as DesktopRenderer,
75
+ ...(storyOrchestrator ? { storyOrchestrator, storyRootTestId: 'story-root' } : {}),
76
+ },
77
+ ],
78
+ });
79
+ writeJson(stdout, { server: server.url, targetId: flags.target });
80
+ try {
81
+ await waitForSignal();
82
+ } finally {
83
+ await server.close();
84
+ }
85
+ });
86
+
87
+ const stories = program.command('stories').description('Inspect or run authored desktop story tests.');
88
+ addConnectionOptions(
89
+ stories
90
+ .command('list')
91
+ .description('List stories and tests from the active platform manifest.')
92
+ .action(async (flags: ConnectionFlags) => {
93
+ const desktop = await connect(flags);
94
+ try {
95
+ writeJson(stdout, await desktop.listStories());
96
+ } finally {
97
+ await desktop.delete();
98
+ }
99
+ }),
100
+ );
101
+ addConnectionOptions(
102
+ stories
103
+ .command('explain')
104
+ .description('Print one story and its authored test plans.')
105
+ .argument('<storyId>')
106
+ .action(async (storyId: string, flags: ConnectionFlags) => {
107
+ const desktop = await connect(flags);
108
+ try {
109
+ const story = (await desktop.listStories()).entries.find(({ id }) => id === storyId);
110
+ if (!story) {
111
+ throw new Error(`Story "${storyId}" is not present in the active manifest.`);
112
+ }
113
+ writeJson(stdout, story);
114
+ } finally {
115
+ await desktop.delete();
116
+ }
117
+ }),
118
+ );
119
+ addSelectionOptions(
120
+ addConnectionOptions(
121
+ stories
122
+ .command('run')
123
+ .description('Run selected authored plans through WebdriverIO.')
124
+ .requiredOption('--artifacts <path>', 'artifact and report root')
125
+ .action(async (flags: SelectionFlags) => {
126
+ const artifactsRoot = requireOption(flags.artifacts, '--artifacts');
127
+ const desktop = await connect(flags);
128
+ try {
129
+ const result = await desktop.runStoryTests({
130
+ artifactsRoot,
131
+ selection: {
132
+ shardCount: flags.shardCount,
133
+ shardIndex: flags.shardIndex,
134
+ story: flags.story,
135
+ tag: flags.tag,
136
+ test: flags.test,
137
+ },
138
+ });
139
+ writeJson(stdout, result);
140
+ if (result.status !== 'passed') {
141
+ process.exitCode = 1;
142
+ }
143
+ } finally {
144
+ await desktop.delete();
145
+ }
146
+ }),
147
+ ),
148
+ );
149
+
150
+ const agent = program.command('agent').description('Use bounded JSON-safe agent operations.');
151
+ addConnectionOptions(
152
+ agent
153
+ .command('describe')
154
+ .description('Print a bounded accessibility-tree projection.')
155
+ .requiredOption('--artifacts <path>', 'artifact root')
156
+ .option('--depth <depth>', 'maximum tree depth', parseNonNegativeInteger, 3)
157
+ .option('--max-nodes <count>', 'maximum returned nodes', parsePositiveInteger, 100)
158
+ .addOption(new Option('--scope <scope>', 'tree scope').choices(['application', 'story']).default('story'))
159
+ .action(async (flags: ConnectionFlags & { depth: number; maxNodes: number; scope: 'application' | 'story' }) => {
160
+ const artifactsRoot = requireOption(flags.artifacts, '--artifacts');
161
+ const desktopAgent = await connectDesktopAgent({
162
+ artifactsRoot,
163
+ platformName: flags.platform,
164
+ targetId: flags.target,
165
+ url: flags.url,
166
+ });
167
+ try {
168
+ writeJson(stdout, await desktopAgent.describe({ depth: flags.depth, maxNodes: flags.maxNodes, scope: flags.scope }));
169
+ } finally {
170
+ await desktopAgent.delete();
171
+ }
172
+ }),
173
+ );
174
+ addConnectionOptions(
175
+ agent
176
+ .command('screenshot')
177
+ .description('Capture a named screenshot artifact.')
178
+ .requiredOption('--artifacts <path>', 'artifact root')
179
+ .requiredOption('--name <name>', 'artifact name')
180
+ .action(async (flags: ConnectionFlags & { name: string }) => {
181
+ const artifactsRoot = requireOption(flags.artifacts, '--artifacts');
182
+ const desktopAgent = await connectDesktopAgent({
183
+ artifactsRoot,
184
+ platformName: flags.platform,
185
+ targetId: flags.target,
186
+ url: flags.url,
187
+ });
188
+ try {
189
+ writeJson(stdout, await desktopAgent.screenshot(flags.name));
190
+ } finally {
191
+ await desktopAgent.delete();
192
+ }
193
+ }),
194
+ );
195
+
196
+ return program;
197
+ }
198
+
199
+ export async function runDesktopDriverCli(argv: readonly string[] = process.argv): Promise<void> {
200
+ await createDesktopDriverCommand().parseAsync([...argv]);
201
+ }
202
+
203
+ function addConnectionOptions<T extends Command>(command: T): T {
204
+ return command
205
+ .requiredOption('--url <url>', 'Desktop Driver server URL')
206
+ .requiredOption('--target <id>', 'registered target id')
207
+ .addOption(new Option('--platform <name>', 'WebDriver platform name').choices(['macos', 'windows']).default('windows')) as T;
208
+ }
209
+
210
+ function addSelectionOptions<T extends Command>(command: T): T {
211
+ return command
212
+ .option('--story <pattern>', 'story id glob')
213
+ .option('--test <pattern>', 'test id glob')
214
+ .option('--tag <tag>', 'required story tag')
215
+ .option('--shard-index <index>', 'zero-based shard index', parseNonNegativeInteger)
216
+ .option('--shard-count <count>', 'number of shards', parsePositiveInteger) as T;
217
+ }
218
+
219
+ function connect(flags: ConnectionFlags) {
220
+ return connectDesktopWebdriver({
221
+ platformName: flags.platform,
222
+ targetId: flags.target,
223
+ url: flags.url,
224
+ });
225
+ }
226
+
227
+ function readManifest(manifestPath: string): DesktopStoryManifest {
228
+ const value = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as DesktopStoryManifest;
229
+ if (
230
+ value.schemaVersion !== 1 ||
231
+ !Array.isArray(value.entries) ||
232
+ (value.endpoint !== 'macos' && value.endpoint !== 'windows' && value.endpoint !== 'win32') ||
233
+ typeof value.platformManifestDigest !== 'string' ||
234
+ typeof value.portablePlanDigest !== 'string'
235
+ ) {
236
+ throw new Error(`Invalid Desktop Story Manifest at ${manifestPath}.`);
237
+ }
238
+ for (const entry of value.entries) {
239
+ if (!entry || typeof entry.id !== 'string' || !Array.isArray(entry.tags)) {
240
+ throw new Error(`Invalid Desktop Story Manifest entry at ${manifestPath}.`);
241
+ }
242
+ if (entry.tests) {
243
+ validateDesktopStoryTests(entry.tests, `${manifestPath}#${entry.id}`);
244
+ }
245
+ }
246
+ return value;
247
+ }
248
+
249
+ function writeJson(output: Pick<NodeJS.WriteStream, 'write'>, value: unknown): void {
250
+ output.write(`${JSON.stringify(value, null, 2)}\n`);
251
+ }
252
+
253
+ function waitForSignal(): Promise<void> {
254
+ return new Promise((resolve) => {
255
+ process.once('SIGINT', resolve);
256
+ process.once('SIGTERM', resolve);
257
+ });
258
+ }
259
+
260
+ function parsePort(value: string): number {
261
+ const port = Number(value);
262
+ if (!Number.isInteger(port) || port < 0 || port > 65_535) {
263
+ throw new TypeError(`Port must be an integer between 0 and 65535. Received "${value}".`);
264
+ }
265
+
266
+ return port;
267
+ }
268
+
269
+ function requireOption(value: string | undefined, name: string): string {
270
+ if (!value) {
271
+ throw new TypeError(`${name} requires a non-empty value.`);
272
+ }
273
+ return value;
274
+ }
275
+
276
+ function parseNonNegativeInteger(value: string): number {
277
+ const parsed = Number(value);
278
+ if (!Number.isInteger(parsed) || parsed < 0) {
279
+ throw new TypeError(`Expected a non-negative integer. Received "${value}".`);
280
+ }
281
+ return parsed;
282
+ }
283
+
284
+ function parsePositiveInteger(value: string): number {
285
+ const parsed = Number(value);
286
+ if (!Number.isInteger(parsed) || parsed < 1) {
287
+ throw new TypeError(`Expected a positive integer. Received "${value}".`);
288
+ }
289
+ return parsed;
290
+ }
@@ -0,0 +1,2 @@
1
+ export { createDesktopDriverCommand, runDesktopDriverCli } from './createDesktopDriverCommand.js';
2
+ export type { CreateDesktopDriverCommandOptions } from './createDesktopDriverCommand.js';
@@ -0,0 +1,218 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { WebDriverError } from '../protocol/errors.js';
4
+ import { webElementIdentifier } from '../protocol/constants.js';
5
+ import type { DesktopTreeNode } from '../host/types.js';
6
+ import type {
7
+ NewSessionCapabilities,
8
+ WebDriverActionSequence,
9
+ WebDriverElement,
10
+ WebDriverErrorResponse,
11
+ WebDriverResponse,
12
+ WebDriverTimeouts,
13
+ } from '../protocol/types.js';
14
+ import type { DesktopStoryManifest, StoryReadyResult } from '../storybook.js';
15
+
16
+ export type DesktopDriverClientOptions = {
17
+ fetch?: typeof globalThis.fetch;
18
+ url: string;
19
+ };
20
+
21
+ export class DesktopDriverClient {
22
+ private readonly fetch: typeof globalThis.fetch;
23
+ private readonly url: string;
24
+
25
+ constructor({ fetch = globalThis.fetch, url }: DesktopDriverClientOptions) {
26
+ this.fetch = fetch;
27
+ this.url = url.replace(/\/$/, '');
28
+ }
29
+
30
+ status(): Promise<Record<string, unknown>> {
31
+ return this.request('GET', '/status');
32
+ }
33
+
34
+ async newSession(capabilities: NewSessionCapabilities): Promise<DesktopSessionClient> {
35
+ const value = await this.request<{ capabilities: Record<string, unknown>; sessionId: string }>('POST', '/session', {
36
+ capabilities,
37
+ });
38
+ return new DesktopSessionClient(this, value.sessionId, value.capabilities);
39
+ }
40
+
41
+ async request<T>(method: string, path: string, body?: unknown): Promise<T> {
42
+ const response = await this.fetch(`${this.url}${path}`, {
43
+ method,
44
+ ...(body === undefined
45
+ ? {}
46
+ : {
47
+ body: JSON.stringify(body),
48
+ headers: { 'Content-Type': 'application/json' },
49
+ }),
50
+ });
51
+ const payload = (await response.json()) as WebDriverResponse<T> | WebDriverErrorResponse;
52
+ if (!response.ok) {
53
+ const error = payload.value as WebDriverErrorResponse['value'];
54
+ throw new WebDriverError(error.error as ConstructorParameters<typeof WebDriverError>[0], error.message, error.data);
55
+ }
56
+ return payload.value as T;
57
+ }
58
+ }
59
+
60
+ export class DesktopSessionClient {
61
+ private readonly driver: DesktopDriverClient;
62
+ readonly id: string;
63
+ readonly capabilities: Readonly<Record<string, unknown>>;
64
+
65
+ constructor(driver: DesktopDriverClient, id: string, capabilities: Readonly<Record<string, unknown>>) {
66
+ this.driver = driver;
67
+ this.id = id;
68
+ this.capabilities = capabilities;
69
+ }
70
+
71
+ delete(): Promise<null> {
72
+ return this.command('DELETE', '');
73
+ }
74
+
75
+ getTimeouts(): Promise<WebDriverTimeouts> {
76
+ return this.command('GET', '/timeouts');
77
+ }
78
+
79
+ setTimeouts(timeouts: Partial<WebDriverTimeouts>): Promise<null> {
80
+ return this.command('POST', '/timeouts', timeouts);
81
+ }
82
+
83
+ getWindowHandle(): Promise<string> {
84
+ return this.command('GET', '/window');
85
+ }
86
+
87
+ getWindowHandles(): Promise<string[]> {
88
+ return this.command('GET', '/window/handles');
89
+ }
90
+
91
+ switchToWindow(handle: string): Promise<null> {
92
+ return this.command('POST', '/window', { handle });
93
+ }
94
+
95
+ findElement(using: string, value: string): Promise<DesktopElementClient> {
96
+ return this.command<WebDriverElement>('POST', '/element', { using, value }).then(
97
+ (element) => new DesktopElementClient(this, element[webElementIdentifier]),
98
+ );
99
+ }
100
+
101
+ findElements(using: string, value: string): Promise<DesktopElementClient[]> {
102
+ return this.command<WebDriverElement[]>('POST', '/elements', { using, value }).then((elements) =>
103
+ elements.map((element) => new DesktopElementClient(this, element[webElementIdentifier])),
104
+ );
105
+ }
106
+
107
+ getActiveElement(): Promise<DesktopElementClient | null> {
108
+ return this.command<WebDriverElement | null>('GET', '/element/active').then((element) =>
109
+ element ? new DesktopElementClient(this, element[webElementIdentifier]) : null,
110
+ );
111
+ }
112
+
113
+ performActions(actions: readonly WebDriverActionSequence[]): Promise<null> {
114
+ return this.command('POST', '/actions', { actions });
115
+ }
116
+
117
+ releaseActions(): Promise<null> {
118
+ return this.command('DELETE', '/actions');
119
+ }
120
+
121
+ takeScreenshot(): Promise<string> {
122
+ return this.command('GET', '/screenshot');
123
+ }
124
+
125
+ getPageSource(): Promise<string> {
126
+ return this.command('GET', '/source');
127
+ }
128
+
129
+ getTree(): Promise<DesktopTreeNode[]> {
130
+ return this.command('GET', '/furn/tree');
131
+ }
132
+
133
+ getStoryManifest(): Promise<DesktopStoryManifest> {
134
+ return this.command('GET', '/furn/manifest');
135
+ }
136
+
137
+ getCurrentStory(): Promise<StoryReadyResult | null> {
138
+ return this.command('GET', '/furn/story');
139
+ }
140
+
141
+ selectStory(storyId: string, runId: string = randomUUID()): Promise<StoryReadyResult> {
142
+ return this.command('POST', '/furn/story', { requestId: randomUUID(), runId, storyId });
143
+ }
144
+
145
+ resetStory(storyId: string, runId: string = randomUUID()): Promise<StoryReadyResult> {
146
+ return this.command('POST', '/furn/story/reset', { requestId: randomUUID(), runId, storyId });
147
+ }
148
+
149
+ updateStoryArgs(storyId: string, args: Readonly<Record<string, unknown>>): Promise<null> {
150
+ return this.command('POST', '/furn/story/args', { args, storyId });
151
+ }
152
+
153
+ command<T>(method: string, path: string, body?: unknown): Promise<T> {
154
+ return this.driver.request(method, `/session/${this.id}${path}`, body);
155
+ }
156
+ }
157
+
158
+ export class DesktopElementClient {
159
+ private readonly session: DesktopSessionClient;
160
+ readonly id: string;
161
+
162
+ constructor(session: DesktopSessionClient, id: string) {
163
+ this.session = session;
164
+ this.id = id;
165
+ }
166
+
167
+ click(): Promise<null> {
168
+ return this.command('POST', '/click', {});
169
+ }
170
+
171
+ clear(): Promise<null> {
172
+ return this.command('POST', '/clear', {});
173
+ }
174
+
175
+ sendKeys(text: string): Promise<null> {
176
+ return this.command('POST', '/value', { text });
177
+ }
178
+
179
+ getText(): Promise<string> {
180
+ return this.command('GET', '/text');
181
+ }
182
+
183
+ getTagName(): Promise<string> {
184
+ return this.command('GET', '/name');
185
+ }
186
+
187
+ isEnabled(): Promise<boolean> {
188
+ return this.command('GET', '/enabled');
189
+ }
190
+
191
+ isDisplayed(): Promise<boolean> {
192
+ return this.command('GET', '/displayed');
193
+ }
194
+
195
+ isSelected(): Promise<boolean> {
196
+ return this.command('GET', '/selected');
197
+ }
198
+
199
+ getAttribute(name: string): Promise<unknown> {
200
+ return this.command('GET', `/attribute/${encodeURIComponent(name)}`);
201
+ }
202
+
203
+ getProperty(name: string): Promise<unknown> {
204
+ return this.command('GET', `/property/${encodeURIComponent(name)}`);
205
+ }
206
+
207
+ takeScreenshot(): Promise<string> {
208
+ return this.command('GET', '/screenshot');
209
+ }
210
+
211
+ private command<T>(method: string, path: string, body?: unknown): Promise<T> {
212
+ return this.session.command(method, `/element/${this.id}${path}`, body);
213
+ }
214
+ }
215
+
216
+ export function createDesktopDriverClient(options: DesktopDriverClientOptions): DesktopDriverClient {
217
+ return new DesktopDriverClient(options);
218
+ }
@@ -0,0 +1,2 @@
1
+ export { createDesktopDriverClient, DesktopDriverClient, DesktopElementClient, DesktopSessionClient } from './DesktopDriverClient.js';
2
+ export type { DesktopDriverClientOptions } from './DesktopDriverClient.js';