@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,362 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import type { DesktopStoryTests } from '../authoring/storyTests.js';
6
+ import { ArtifactManager } from '../artifacts/ArtifactManager.js';
7
+ import { createDesktopDriverClient } from '../client/DesktopDriverClient.js';
8
+ import { createDesktopDriverStoryHarness } from '../testing/protocolHarness.js';
9
+ import type { DesktopStoryManifest } from '../storybook.js';
10
+ import { runDesktopStoryTests, selectDesktopStoryTests } from './StoryTestRunner.js';
11
+ import { findDesktopElements } from './StoryTestRunner.js';
12
+
13
+ const passingPlan: DesktopStoryTests = {
14
+ version: 1,
15
+ tests: [
16
+ {
17
+ id: 'button-contract',
18
+ requires: ['focus', 'keyboard', 'screenshot', 'wheel'],
19
+ steps: [
20
+ { action: 'wait', target: { testId: 'button-primary' }, timeoutMs: 100 },
21
+ { expect: { state: 'role', target: { testId: 'button-primary' }, value: 'button' } },
22
+ { expect: { state: 'enabled', target: { testId: 'button-primary' }, value: true } },
23
+ { action: 'click', target: { testId: 'button-primary' } },
24
+ { expect: { state: 'focused', target: { testId: 'button-primary' }, value: true } },
25
+ { action: 'type', target: { testId: 'input-name' }, text: 'Ada' },
26
+ { expect: { state: 'value', target: { testId: 'input-name' }, value: 'Ada' } },
27
+ { action: 'keys', value: ['\uE004'] },
28
+ { action: 'scroll', deltaY: 120 },
29
+ { action: 'setArgs', args: { disabled: false } },
30
+ { action: 'screenshot', name: 'button' },
31
+ { action: 'source', name: 'source' },
32
+ ],
33
+ title: 'Button contract',
34
+ },
35
+ ],
36
+ };
37
+
38
+ describe('runDesktopStoryTests', () => {
39
+ test('runs a portable plan and writes a stable evidence report', async () => {
40
+ const manifest = makeManifest(passingPlan);
41
+ const harness = await createDesktopDriverStoryHarness(manifest);
42
+ const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-driver-run-'));
43
+ try {
44
+ const client = createDesktopDriverClient({ url: harness.server.url });
45
+ const session = await client.newSession({
46
+ alwaysMatch: { platformName: 'windows', 'furn:target': harness.target.id },
47
+ });
48
+ const result = await runDesktopStoryTests({
49
+ artifacts: new ArtifactManager(temporaryDirectory),
50
+ endpoint: 'windows',
51
+ manifest,
52
+ platformName: 'windows',
53
+ runId: 'run',
54
+ session,
55
+ targetId: harness.target.id,
56
+ });
57
+
58
+ expect(result).toMatchObject({
59
+ status: 'passed',
60
+ tests: [
61
+ {
62
+ status: 'passed',
63
+ steps: expect.arrayContaining([expect.objectContaining({ status: 'passed' })]),
64
+ },
65
+ ],
66
+ });
67
+ expect(result.tests[0].artifacts.map(({ kind }) => kind)).toEqual(['screenshot', 'source']);
68
+ expect(harness.storyOrchestrator?.argUpdates).toEqual([{ args: { disabled: false }, storyId: 'components-button--default' }]);
69
+ expect(JSON.parse(fs.readFileSync(path.join(temporaryDirectory, 'run.json'), 'utf8'))).toMatchObject({
70
+ runId: 'run',
71
+ status: 'passed',
72
+ });
73
+ expect(JSON.parse(fs.readFileSync(path.join(temporaryDirectory, 'host.json'), 'utf8'))).toMatchObject({
74
+ endpoint: 'windows',
75
+ targetId: harness.target.id,
76
+ });
77
+ await session.delete();
78
+ } finally {
79
+ await harness.close();
80
+ fs.rmSync(temporaryDirectory, { force: true, recursive: true });
81
+ }
82
+ });
83
+
84
+ test('distinguishes assertion failures and captures failure evidence', async () => {
85
+ const manifest = makeManifest({
86
+ version: 1,
87
+ tests: [
88
+ {
89
+ id: 'fails',
90
+ steps: [{ expect: { state: 'role', target: { testId: 'button-primary' }, value: 'checkbox' } }],
91
+ },
92
+ ],
93
+ });
94
+ const harness = await createDesktopDriverStoryHarness(manifest);
95
+ const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-driver-failure-'));
96
+ try {
97
+ const client = createDesktopDriverClient({ url: harness.server.url });
98
+ const session = await client.newSession({
99
+ alwaysMatch: { platformName: 'windows', 'furn:target': harness.target.id },
100
+ });
101
+ const result = await runDesktopStoryTests({
102
+ artifacts: new ArtifactManager(temporaryDirectory),
103
+ endpoint: 'windows',
104
+ manifest,
105
+ platformName: 'windows',
106
+ session,
107
+ targetId: harness.target.id,
108
+ });
109
+
110
+ expect(result.status).toBe('failed');
111
+ expect(result.tests[0]).toMatchObject({
112
+ status: 'failed',
113
+ error: expect.stringContaining('checkbox'),
114
+ });
115
+ expect(result.tests[0].artifacts.map(({ name }) => name)).toEqual(['failure', 'failure-source', 'failure-tree']);
116
+ await session.delete();
117
+ } finally {
118
+ await harness.close();
119
+ fs.rmSync(temporaryDirectory, { force: true, recursive: true });
120
+ }
121
+ });
122
+
123
+ test('filters and deterministically shards plans', () => {
124
+ const manifest = makeManifest({
125
+ version: 1,
126
+ tests: [
127
+ { id: 'one', steps: [{ action: 'note', message: 'one' }] },
128
+ { id: 'two', steps: [{ action: 'note', message: 'two' }] },
129
+ { id: 'three', steps: [{ action: 'note', message: 'three' }] },
130
+ { id: 'four', steps: [{ action: 'note', message: 'four' }] },
131
+ ],
132
+ });
133
+
134
+ const first = selectDesktopStoryTests(manifest, 'windows', { shardCount: 2, shardIndex: 0 });
135
+ const second = selectDesktopStoryTests(manifest, 'windows', { shardCount: 2, shardIndex: 1 });
136
+ expect([...first, ...second].map(({ test }) => test.id).sort()).toEqual(['four', 'one', 'three', 'two']);
137
+ expect(first).toHaveLength(2);
138
+ expect(second).toHaveLength(2);
139
+ });
140
+
141
+ test('does not turn an unsupported runtime property into a passing skip', async () => {
142
+ const manifest = makeManifest({
143
+ version: 1,
144
+ tests: [
145
+ {
146
+ id: 'unsupported-checked',
147
+ steps: [{ expect: { state: 'checked', target: { testId: 'button-primary' }, value: true } }],
148
+ },
149
+ ],
150
+ });
151
+ const harness = await createDesktopDriverStoryHarness(manifest);
152
+ harness.host.setElementStateUnsupported('button', 'checked', 'not exposed by this platform');
153
+ try {
154
+ const client = createDesktopDriverClient({ url: harness.server.url });
155
+ const session = await client.newSession({
156
+ alwaysMatch: { platformName: 'windows', 'furn:target': harness.target.id },
157
+ });
158
+ const result = await runDesktopStoryTests({
159
+ endpoint: 'windows',
160
+ manifest,
161
+ platformName: 'windows',
162
+ session,
163
+ targetId: harness.target.id,
164
+ });
165
+
166
+ expect(result).toMatchObject({
167
+ status: 'failed',
168
+ tests: [{ status: 'failed', error: expect.stringContaining('not exposed') }],
169
+ });
170
+ await session.delete();
171
+ } finally {
172
+ await harness.close();
173
+ }
174
+ });
175
+
176
+ test('matches text independently from accessible name', async () => {
177
+ const harness = await createDesktopDriverStoryHarness(makeManifest(passingPlan), {
178
+ windows: [
179
+ {
180
+ id: 'window-1',
181
+ title: 'Text selector',
182
+ elements: [
183
+ {
184
+ id: 'label',
185
+ rect: { height: 20, width: 100, x: 0, y: 0 },
186
+ role: 'text',
187
+ scope: 'preview',
188
+ text: 'Visible text',
189
+ windowId: 'window-1',
190
+ },
191
+ {
192
+ id: 'story-root',
193
+ automationId: 'story-root',
194
+ name: JSON.stringify({ previewGeneration: 0, storyId: 'initial--story' }),
195
+ rect: { height: 100, width: 100, x: 0, y: 0 },
196
+ role: 'group',
197
+ scope: 'preview',
198
+ windowId: 'window-1',
199
+ },
200
+ ],
201
+ },
202
+ ],
203
+ });
204
+ try {
205
+ const client = createDesktopDriverClient({ url: harness.server.url });
206
+ const session = await client.newSession({
207
+ alwaysMatch: { platformName: 'windows', 'furn:target': harness.target.id },
208
+ });
209
+ await expect(findDesktopElements(session, { text: 'Visible' })).resolves.toHaveLength(1);
210
+ await session.delete();
211
+ } finally {
212
+ await harness.close();
213
+ }
214
+ });
215
+
216
+ test('cancels an in-flight wait and releases input before the next test', async () => {
217
+ const manifest = makeManifest({
218
+ version: 1,
219
+ tests: [
220
+ {
221
+ id: 'cancelled-wait',
222
+ steps: [{ action: 'wait', target: { testId: 'missing' }, timeoutMs: 1000 }],
223
+ },
224
+ ],
225
+ });
226
+ const harness = await createDesktopDriverStoryHarness(manifest);
227
+ const controller = new AbortController();
228
+ try {
229
+ const client = createDesktopDriverClient({ url: harness.server.url });
230
+ const session = await client.newSession({
231
+ alwaysMatch: { platformName: 'windows', 'furn:target': harness.target.id },
232
+ });
233
+ setTimeout(() => controller.abort(), 20);
234
+ const started = Date.now();
235
+ const result = await runDesktopStoryTests({
236
+ endpoint: 'windows',
237
+ manifest,
238
+ platformName: 'windows',
239
+ session,
240
+ signal: controller.signal,
241
+ targetId: harness.target.id,
242
+ });
243
+
244
+ expect(Date.now() - started).toBeLessThan(500);
245
+ expect(result).toMatchObject({ status: 'failed', tests: [{ status: 'cancelled' }] });
246
+ expect(harness.host.actions).toContainEqual({ type: 'release-actions' });
247
+ await session.delete();
248
+ } finally {
249
+ await harness.close();
250
+ }
251
+ });
252
+
253
+ test('skips a declared focus requirement when focused state is unavailable', async () => {
254
+ const manifest = makeManifest({
255
+ version: 1,
256
+ tests: [
257
+ {
258
+ id: 'focus-required',
259
+ requires: ['focus'],
260
+ steps: [{ expect: { state: 'focused', target: { testId: 'button-primary' }, value: true } }],
261
+ },
262
+ ],
263
+ });
264
+ const harness = await createDesktopDriverStoryHarness(manifest, { features: { focus: false } });
265
+ try {
266
+ const client = createDesktopDriverClient({ url: harness.server.url });
267
+ const session = await client.newSession({
268
+ alwaysMatch: { platformName: 'windows', 'furn:target': harness.target.id },
269
+ });
270
+ const result = await runDesktopStoryTests({
271
+ endpoint: 'windows',
272
+ manifest,
273
+ platformName: 'windows',
274
+ session,
275
+ targetId: harness.target.id,
276
+ });
277
+
278
+ expect(result).toMatchObject({
279
+ status: 'passed',
280
+ tests: [{ skipReason: 'Unsupported capabilities: focus', status: 'skipped', steps: [] }],
281
+ });
282
+ await session.delete();
283
+ } finally {
284
+ await harness.close();
285
+ }
286
+ });
287
+
288
+ test('drains an aborted native action before releasing input', async () => {
289
+ const manifest = makeManifest({
290
+ version: 1,
291
+ tests: [
292
+ {
293
+ id: 'cancelled-action',
294
+ steps: [
295
+ {
296
+ action: 'actions',
297
+ sequences: [{ id: 'key', type: 'key', actions: [{ type: 'keyDown', value: 'A' }] }],
298
+ },
299
+ ],
300
+ },
301
+ ],
302
+ });
303
+ const harness = await createDesktopDriverStoryHarness(manifest, { actionDelayMs: 50 });
304
+ const controller = new AbortController();
305
+ try {
306
+ const client = createDesktopDriverClient({ url: harness.server.url });
307
+ const session = await client.newSession({
308
+ alwaysMatch: { platformName: 'windows', 'furn:target': harness.target.id },
309
+ });
310
+ const running = runDesktopStoryTests({
311
+ endpoint: 'windows',
312
+ manifest,
313
+ platformName: 'windows',
314
+ session,
315
+ signal: controller.signal,
316
+ targetId: harness.target.id,
317
+ });
318
+ await waitUntil(() => harness.host.actions.some(({ type }) => type === 'actions-start'));
319
+ controller.abort();
320
+ const result = await running;
321
+
322
+ expect(result).toMatchObject({ status: 'failed', tests: [{ status: 'cancelled' }] });
323
+ const completed = harness.host.actions.findIndex(({ type }) => type === 'actions');
324
+ const released = harness.host.actions.findIndex(({ type }) => type === 'release-actions');
325
+ expect(completed).toBeGreaterThan(-1);
326
+ expect(released).toBeGreaterThan(completed);
327
+ await session.delete();
328
+ } finally {
329
+ await harness.close();
330
+ }
331
+ });
332
+ });
333
+
334
+ function makeManifest(tests: DesktopStoryTests): DesktopStoryManifest {
335
+ return {
336
+ endpoint: 'windows',
337
+ entries: [
338
+ {
339
+ id: 'components-button--default',
340
+ name: 'Default',
341
+ packageName: '@fluentui-react-native/components',
342
+ sourcePath: 'src/components/button/button.stories.tsx',
343
+ tags: ['e2e', 'story'],
344
+ tests,
345
+ title: 'Components/Button',
346
+ },
347
+ ],
348
+ platformManifestDigest: 'platform-digest',
349
+ portablePlanDigest: 'portable-digest',
350
+ schemaVersion: 1,
351
+ };
352
+ }
353
+
354
+ async function waitUntil(predicate: () => boolean, timeoutMs = 1000): Promise<void> {
355
+ const deadline = Date.now() + timeoutMs;
356
+ while (!predicate()) {
357
+ if (Date.now() >= deadline) {
358
+ throw new Error('Timed out waiting for the expected test condition.');
359
+ }
360
+ await new Promise((resolve) => setTimeout(resolve, 1));
361
+ }
362
+ }