@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.
- package/.cache/tsconfig.tsbuildinfo +1 -0
- package/AGENTS.md +68 -0
- package/CHANGELOG.md +16 -0
- package/PLAN.md +1394 -0
- package/README.md +223 -0
- package/SPEC.md +105 -0
- package/config/cli.cjs +8 -0
- package/jest.config.cjs +9 -0
- package/lib/agent/DesktopAgent.d.ts +56 -0
- package/lib/agent/DesktopAgent.d.ts.map +1 -0
- package/lib/agent/DesktopAgent.js +118 -0
- package/lib/agent/DesktopAgent.js.map +1 -0
- package/lib/agent/DesktopAgent.test.d.ts +2 -0
- package/lib/agent/DesktopAgent.test.d.ts.map +1 -0
- package/lib/agent/DesktopAgent.test.js +97 -0
- package/lib/agent/DesktopAgent.test.js.map +1 -0
- package/lib/agent/agent.contract.cjs +66 -0
- package/lib/agent/agent.contract.cjs.map +1 -0
- package/lib/agent/agent.contract.d.cts +2 -0
- package/lib/agent/agent.contract.d.cts.map +1 -0
- package/lib/agent/index.d.ts +3 -0
- package/lib/agent/index.d.ts.map +1 -0
- package/lib/agent/index.js +2 -0
- package/lib/agent/index.js.map +1 -0
- package/lib/artifacts/ArtifactManager.d.ts +13 -0
- package/lib/artifacts/ArtifactManager.d.ts.map +1 -0
- package/lib/artifacts/ArtifactManager.js +88 -0
- package/lib/artifacts/ArtifactManager.js.map +1 -0
- package/lib/artifacts/ArtifactManager.test.d.ts +2 -0
- package/lib/artifacts/ArtifactManager.test.d.ts.map +1 -0
- package/lib/artifacts/ArtifactManager.test.js +19 -0
- package/lib/artifacts/ArtifactManager.test.js.map +1 -0
- package/lib/artifacts/index.d.ts +2 -0
- package/lib/artifacts/index.d.ts.map +1 -0
- package/lib/artifacts/index.js +2 -0
- package/lib/artifacts/index.js.map +1 -0
- package/lib/authoring/index.d.ts +4 -0
- package/lib/authoring/index.d.ts.map +1 -0
- package/lib/authoring/index.js +2 -0
- package/lib/authoring/index.js.map +1 -0
- package/lib/authoring/results.d.ts +43 -0
- package/lib/authoring/results.d.ts.map +1 -0
- package/lib/authoring/results.js +2 -0
- package/lib/authoring/results.js.map +1 -0
- package/lib/authoring/storyTests.d.ts +81 -0
- package/lib/authoring/storyTests.d.ts.map +1 -0
- package/lib/authoring/storyTests.js +272 -0
- package/lib/authoring/storyTests.js.map +1 -0
- package/lib/authoring/storyTests.test.d.ts +2 -0
- package/lib/authoring/storyTests.test.d.ts.map +1 -0
- package/lib/authoring/storyTests.test.js +46 -0
- package/lib/authoring/storyTests.test.js.map +1 -0
- package/lib/cli/DesktopDriverCli.test.d.ts +2 -0
- package/lib/cli/DesktopDriverCli.test.d.ts.map +1 -0
- package/lib/cli/DesktopDriverCli.test.js +141 -0
- package/lib/cli/DesktopDriverCli.test.js.map +1 -0
- package/lib/cli/createDesktopDriverCommand.d.ts +8 -0
- package/lib/cli/createDesktopDriverCommand.d.ts.map +1 -0
- package/lib/cli/createDesktopDriverCommand.js +242 -0
- package/lib/cli/createDesktopDriverCommand.js.map +1 -0
- package/lib/cli/index.d.ts +3 -0
- package/lib/cli/index.d.ts.map +1 -0
- package/lib/cli/index.js +2 -0
- package/lib/cli/index.js.map +1 -0
- package/lib/client/DesktopDriverClient.d.ts +60 -0
- package/lib/client/DesktopDriverClient.d.ts.map +1 -0
- package/lib/client/DesktopDriverClient.js +155 -0
- package/lib/client/DesktopDriverClient.js.map +1 -0
- package/lib/client/index.d.ts +3 -0
- package/lib/client/index.d.ts.map +1 -0
- package/lib/client/index.js +2 -0
- package/lib/client/index.js.map +1 -0
- package/lib/host/types.d.ts +153 -0
- package/lib/host/types.d.ts.map +1 -0
- package/lib/host/types.js +2 -0
- package/lib/host/types.js.map +1 -0
- package/lib/hosts/fake/FakeDesktopHost.d.ts +72 -0
- package/lib/hosts/fake/FakeDesktopHost.d.ts.map +1 -0
- package/lib/hosts/fake/FakeDesktopHost.js +452 -0
- package/lib/hosts/fake/FakeDesktopHost.js.map +1 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -0
- package/lib/protocol/actions.d.ts +11 -0
- package/lib/protocol/actions.d.ts.map +1 -0
- package/lib/protocol/actions.js +137 -0
- package/lib/protocol/actions.js.map +1 -0
- package/lib/protocol/capabilities.d.ts +11 -0
- package/lib/protocol/capabilities.d.ts.map +1 -0
- package/lib/protocol/capabilities.js +103 -0
- package/lib/protocol/capabilities.js.map +1 -0
- package/lib/protocol/constants.d.ts +2 -0
- package/lib/protocol/constants.d.ts.map +1 -0
- package/lib/protocol/constants.js +2 -0
- package/lib/protocol/constants.js.map +1 -0
- package/lib/protocol/errors.d.ts +37 -0
- package/lib/protocol/errors.d.ts.map +1 -0
- package/lib/protocol/errors.js +66 -0
- package/lib/protocol/errors.js.map +1 -0
- package/lib/protocol/timeouts.d.ts +2 -0
- package/lib/protocol/timeouts.d.ts.map +1 -0
- package/lib/protocol/timeouts.js +23 -0
- package/lib/protocol/timeouts.js.map +1 -0
- package/lib/protocol/types.d.ts +46 -0
- package/lib/protocol/types.d.ts.map +1 -0
- package/lib/protocol/types.js +2 -0
- package/lib/protocol/types.js.map +1 -0
- package/lib/runner/StoryTestRunner.d.ts +38 -0
- package/lib/runner/StoryTestRunner.d.ts.map +1 -0
- package/lib/runner/StoryTestRunner.js +497 -0
- package/lib/runner/StoryTestRunner.js.map +1 -0
- package/lib/runner/StoryTestRunner.test.d.ts +2 -0
- package/lib/runner/StoryTestRunner.test.d.ts.map +1 -0
- package/lib/runner/StoryTestRunner.test.js +349 -0
- package/lib/runner/StoryTestRunner.test.js.map +1 -0
- package/lib/runner/index.d.ts +3 -0
- package/lib/runner/index.d.ts.map +1 -0
- package/lib/runner/index.js +2 -0
- package/lib/runner/index.js.map +1 -0
- package/lib/server/SessionManager.d.ts +52 -0
- package/lib/server/SessionManager.d.ts.map +1 -0
- package/lib/server/SessionManager.js +208 -0
- package/lib/server/SessionManager.js.map +1 -0
- package/lib/server/TargetRegistry.d.ts +9 -0
- package/lib/server/TargetRegistry.d.ts.map +1 -0
- package/lib/server/TargetRegistry.js +24 -0
- package/lib/server/TargetRegistry.js.map +1 -0
- package/lib/server/createDesktopDriverServer.d.ts +19 -0
- package/lib/server/createDesktopDriverServer.d.ts.map +1 -0
- package/lib/server/createDesktopDriverServer.js +609 -0
- package/lib/server/createDesktopDriverServer.js.map +1 -0
- package/lib/server/desktopDriver.test.d.ts +2 -0
- package/lib/server/desktopDriver.test.d.ts.map +1 -0
- package/lib/server/desktopDriver.test.js +389 -0
- package/lib/server/desktopDriver.test.js.map +1 -0
- package/lib/server/index.d.ts +6 -0
- package/lib/server/index.d.ts.map +1 -0
- package/lib/server/index.js +4 -0
- package/lib/server/index.js.map +1 -0
- package/lib/server/webdriverio.contract.cjs +35 -0
- package/lib/server/webdriverio.contract.cjs.map +1 -0
- package/lib/server/webdriverio.contract.d.cts +2 -0
- package/lib/server/webdriverio.contract.d.cts.map +1 -0
- package/lib/server/webdriverio.test.d.ts +2 -0
- package/lib/server/webdriverio.test.d.ts.map +1 -0
- package/lib/server/webdriverio.test.js +40 -0
- package/lib/server/webdriverio.test.js.map +1 -0
- package/lib/storybook.d.ts +36 -0
- package/lib/storybook.d.ts.map +1 -0
- package/lib/storybook.js +2 -0
- package/lib/storybook.js.map +1 -0
- package/lib/testing/FakeStoryOrchestrator.d.ts +21 -0
- package/lib/testing/FakeStoryOrchestrator.d.ts.map +1 -0
- package/lib/testing/FakeStoryOrchestrator.js +44 -0
- package/lib/testing/FakeStoryOrchestrator.js.map +1 -0
- package/lib/testing/fakeStoryElements.d.ts +4 -0
- package/lib/testing/fakeStoryElements.d.ts.map +1 -0
- package/lib/testing/fakeStoryElements.js +143 -0
- package/lib/testing/fakeStoryElements.js.map +1 -0
- package/lib/testing/fakeStoryElements.test.d.ts +2 -0
- package/lib/testing/fakeStoryElements.test.d.ts.map +1 -0
- package/lib/testing/fakeStoryElements.test.js +38 -0
- package/lib/testing/fakeStoryElements.test.js.map +1 -0
- package/lib/testing/index.d.ts +7 -0
- package/lib/testing/index.d.ts.map +1 -0
- package/lib/testing/index.js +5 -0
- package/lib/testing/index.js.map +1 -0
- package/lib/testing/protocolHarness.d.ts +17 -0
- package/lib/testing/protocolHarness.d.ts.map +1 -0
- package/lib/testing/protocolHarness.js +46 -0
- package/lib/testing/protocolHarness.js.map +1 -0
- package/lib/wdio/DesktopWebdriver.d.ts +47 -0
- package/lib/wdio/DesktopWebdriver.d.ts.map +1 -0
- package/lib/wdio/DesktopWebdriver.js +76 -0
- package/lib/wdio/DesktopWebdriver.js.map +1 -0
- package/lib/wdio/DesktopWebdriver.test.d.ts +2 -0
- package/lib/wdio/DesktopWebdriver.test.d.ts.map +1 -0
- package/lib/wdio/DesktopWebdriver.test.js +74 -0
- package/lib/wdio/DesktopWebdriver.test.js.map +1 -0
- package/lib/wdio/index.d.ts +3 -0
- package/lib/wdio/index.d.ts.map +1 -0
- package/lib/wdio/index.js +2 -0
- package/lib/wdio/index.js.map +1 -0
- package/lib/wdio/wdioRunner.contract.cjs +64 -0
- package/lib/wdio/wdioRunner.contract.cjs.map +1 -0
- package/lib/wdio/wdioRunner.contract.d.cts +2 -0
- package/lib/wdio/wdioRunner.contract.d.cts.map +1 -0
- package/package.json +96 -0
- package/src/agent/DesktopAgent.test.ts +102 -0
- package/src/agent/DesktopAgent.ts +170 -0
- package/src/agent/agent.contract.cjs +32 -0
- package/src/agent/index.ts +8 -0
- package/src/artifacts/ArtifactManager.test.ts +20 -0
- package/src/artifacts/ArtifactManager.ts +105 -0
- package/src/artifacts/index.ts +1 -0
- package/src/authoring/index.ts +26 -0
- package/src/authoring/results.ts +47 -0
- package/src/authoring/storyTests.test.ts +58 -0
- package/src/authoring/storyTests.ts +345 -0
- package/src/cli/DesktopDriverCli.test.ts +150 -0
- package/src/cli/createDesktopDriverCommand.ts +290 -0
- package/src/cli/index.ts +2 -0
- package/src/client/DesktopDriverClient.ts +218 -0
- package/src/client/index.ts +2 -0
- package/src/host/types.ts +158 -0
- package/src/hosts/fake/FakeDesktopHost.ts +558 -0
- package/src/index.ts +101 -0
- package/src/protocol/actions.ts +163 -0
- package/src/protocol/capabilities.ts +129 -0
- package/src/protocol/constants.ts +1 -0
- package/src/protocol/errors.ts +76 -0
- package/src/protocol/timeouts.ts +26 -0
- package/src/protocol/types.ts +55 -0
- package/src/runner/StoryTestRunner.test.ts +362 -0
- package/src/runner/StoryTestRunner.ts +624 -0
- package/src/runner/index.ts +9 -0
- package/src/server/SessionManager.ts +302 -0
- package/src/server/TargetRegistry.ts +29 -0
- package/src/server/createDesktopDriverServer.ts +775 -0
- package/src/server/desktopDriver.test.ts +421 -0
- package/src/server/index.ts +5 -0
- package/src/server/webdriverio.contract.cjs +38 -0
- package/src/server/webdriverio.test.ts +41 -0
- package/src/storybook.ts +40 -0
- package/src/testing/FakeStoryOrchestrator.ts +55 -0
- package/src/testing/fakeStoryElements.test.ts +40 -0
- package/src/testing/fakeStoryElements.ts +167 -0
- package/src/testing/index.ts +6 -0
- package/src/testing/protocolHarness.ts +66 -0
- package/src/wdio/DesktopWebdriver.test.ts +76 -0
- package/src/wdio/DesktopWebdriver.ts +130 -0
- package/src/wdio/index.ts +2 -0
- package/src/wdio/wdioRunner.contract.cjs +30 -0
- package/tsconfig.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# React Native Desktop Driver
|
|
2
|
+
|
|
3
|
+
`@fluentui-react-native/desktop-driver` is a W3C WebDriver-compatible remote end
|
|
4
|
+
for React Native desktop applications. It does not use Appium.
|
|
5
|
+
|
|
6
|
+
Stage 1 provides the complete platform-neutral protocol, sanctioned WebdriverIO
|
|
7
|
+
API, serializable story-test contract, deterministic fake host, evidence
|
|
8
|
+
reports, JSON CLI, and bounded agent API. Native Windows, Win32, and macOS host
|
|
9
|
+
providers are separate Stage 2 work described in [PLAN.md](PLAN.md).
|
|
10
|
+
|
|
11
|
+
## Package boundaries
|
|
12
|
+
|
|
13
|
+
| Surface | Responsibility |
|
|
14
|
+
| --------------------------------------- | ----------------------------------------------------------- |
|
|
15
|
+
| `@fluentui-react-native/desktop-driver` | Public types and common APIs |
|
|
16
|
+
| `/authoring` | Serializable story plans, selectors, and result types |
|
|
17
|
+
| `/wdio` | Sanctioned WebdriverIO connection, commands, and runner |
|
|
18
|
+
| `/agent` | Bounded JSON-safe inspection and action API |
|
|
19
|
+
| `/client` | Low-level typed W3C client |
|
|
20
|
+
| `/server` | Embeddable W3C remote end and target/session state |
|
|
21
|
+
| `/artifacts` | Confined atomic evidence persistence |
|
|
22
|
+
| `/testing` | Fake host, fake Storybook orchestration, and test harnesses |
|
|
23
|
+
|
|
24
|
+
The protocol server remains client-neutral. WebdriverIO is a dependency of the
|
|
25
|
+
high-level `/wdio` surface, not an implementation primitive for routing,
|
|
26
|
+
capability negotiation, or native hosts.
|
|
27
|
+
|
|
28
|
+
## Authoring story tests
|
|
29
|
+
|
|
30
|
+
Plans are inline static data under `parameters.desktopDriver`. Storybook can
|
|
31
|
+
extract and shard them without importing a React Native story module, and
|
|
32
|
+
agents can explain the same steps before execution.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import type { DesktopStoryTests } from '@fluentui-react-native/desktop-driver/authoring';
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
tags: ['desktop-e2e'],
|
|
39
|
+
parameters: {
|
|
40
|
+
desktopDriver: {
|
|
41
|
+
version: 1,
|
|
42
|
+
tests: [
|
|
43
|
+
{
|
|
44
|
+
id: 'pointer-focus',
|
|
45
|
+
title: 'Responds to activation and receives focus',
|
|
46
|
+
requires: ['focus', 'screenshot'],
|
|
47
|
+
steps: [
|
|
48
|
+
{ action: 'wait', target: { testId: 'my-button' } },
|
|
49
|
+
{ expect: { state: 'role', target: { testId: 'my-button' }, value: 'button' } },
|
|
50
|
+
{ action: 'click', target: { testId: 'my-button' } },
|
|
51
|
+
{ expect: { state: 'focused', target: { testId: 'my-button' }, value: true } },
|
|
52
|
+
{ action: 'screenshot', name: 'focused-button', target: { testId: 'my-button' } },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
} satisfies DesktopStoryTests,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Static extraction accepts JSON literals wrapped by TypeScript `satisfies` or
|
|
62
|
+
`as`. Do not hide plans behind variables, functions, spreads, computed
|
|
63
|
+
properties, or runtime platform branches. Invalid or dynamic plans fail
|
|
64
|
+
manifest generation with their source file, story, and line.
|
|
65
|
+
|
|
66
|
+
Portable selectors are:
|
|
67
|
+
|
|
68
|
+
- `{ testId }` for deterministic interaction;
|
|
69
|
+
- `{ role, name? }` for semantic lookup;
|
|
70
|
+
- `{ accessibleName }`;
|
|
71
|
+
- `{ text }`.
|
|
72
|
+
|
|
73
|
+
Supported actions include click, double-click, clear, type, keys, W3C actions,
|
|
74
|
+
scroll, Storybook arg updates, waits, screenshots, source capture, and notes.
|
|
75
|
+
Assertions cover existence/count, role, accessible name, text/value, displayed,
|
|
76
|
+
enabled, focused, selected, checked/mixed, and expanded state.
|
|
77
|
+
|
|
78
|
+
Use `requires` for capabilities such as `keyboard`, `focus`, `wheel`, or
|
|
79
|
+
`screenshot`. Missing capabilities produce an explicit skipped result rather
|
|
80
|
+
than a false pass. Platform divergence belongs in declarative `platforms` or
|
|
81
|
+
capability requirements, not branches inside a plan.
|
|
82
|
+
|
|
83
|
+
## Sanctioned WebdriverIO API
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { connectDesktopWebdriver } from '@fluentui-react-native/desktop-driver/wdio';
|
|
87
|
+
|
|
88
|
+
const desktop = await connectDesktopWebdriver({
|
|
89
|
+
platformName: 'windows',
|
|
90
|
+
targetId: 'agenticstorybook-windows',
|
|
91
|
+
url: 'http://127.0.0.1:39859',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const manifest = await desktop.browser.desktopListStories();
|
|
96
|
+
await desktop.browser.desktopOpenStory('components-button--default');
|
|
97
|
+
await desktop.browser.desktopExpect({
|
|
98
|
+
state: 'enabled',
|
|
99
|
+
target: { testId: 'agentic-storybook-button' },
|
|
100
|
+
value: true,
|
|
101
|
+
});
|
|
102
|
+
const result = await desktop.browser.desktopRunStoryTests({
|
|
103
|
+
artifactsRoot: 'artifacts/windows/desktop-driver',
|
|
104
|
+
selection: { tag: 'desktop-e2e', shardCount: 2, shardIndex: 0 },
|
|
105
|
+
});
|
|
106
|
+
} finally {
|
|
107
|
+
await desktop.delete();
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Registered browser commands are:
|
|
112
|
+
|
|
113
|
+
- `desktopListStories()`;
|
|
114
|
+
- `desktopOpenStory(storyId, runId?)`;
|
|
115
|
+
- `desktopResetStory(storyId, runId?)`;
|
|
116
|
+
- `desktopExpect(expectation)`;
|
|
117
|
+
- `desktopRunStoryTests(options?)`.
|
|
118
|
+
|
|
119
|
+
The runner filters by story, test, and tag, shards the sorted
|
|
120
|
+
`storyId/testId` list deterministically, checks required capabilities, resets
|
|
121
|
+
the preview for every run, and distinguishes assertion, timeout, cancellation,
|
|
122
|
+
skip, and infrastructure outcomes.
|
|
123
|
+
|
|
124
|
+
The server serializes commands per session and all input globally. Timeout
|
|
125
|
+
paths abort host work before releasing input, while runner cancellation drains
|
|
126
|
+
its in-flight request before cleanup, so a later test cannot inherit a late key
|
|
127
|
+
or pointer action.
|
|
128
|
+
|
|
129
|
+
## Evidence
|
|
130
|
+
|
|
131
|
+
Supplying `artifactsRoot` writes atomically beneath that root:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
artifactsRoot/
|
|
135
|
+
host.json
|
|
136
|
+
run.json
|
|
137
|
+
tests/
|
|
138
|
+
<story>-<test>/
|
|
139
|
+
<named screenshots and source>
|
|
140
|
+
failure.png
|
|
141
|
+
failure-source.xml
|
|
142
|
+
failure-tree.json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Artifact names are sanitized and confined beneath the configured root.
|
|
146
|
+
Failures attempt screenshot, source, and compact-tree capture; evidence-capture
|
|
147
|
+
errors are reported without replacing the original test failure.
|
|
148
|
+
|
|
149
|
+
## JSON CLI
|
|
150
|
+
|
|
151
|
+
The CLI prints structured JSON for automation:
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
desktop-driver serve --manifest story-manifest.windows.json --target fake-windows
|
|
155
|
+
|
|
156
|
+
desktop-driver stories list \
|
|
157
|
+
--url http://127.0.0.1:4444 \
|
|
158
|
+
--target fake-windows
|
|
159
|
+
|
|
160
|
+
desktop-driver stories explain components-button--default \
|
|
161
|
+
--url http://127.0.0.1:4444 \
|
|
162
|
+
--target fake-windows
|
|
163
|
+
|
|
164
|
+
desktop-driver stories run \
|
|
165
|
+
--url http://127.0.0.1:4444 \
|
|
166
|
+
--target fake-windows \
|
|
167
|
+
--tag desktop-e2e \
|
|
168
|
+
--artifacts artifacts/windows/desktop-driver
|
|
169
|
+
|
|
170
|
+
desktop-driver agent describe \
|
|
171
|
+
--url http://127.0.0.1:4444 \
|
|
172
|
+
--target fake-windows \
|
|
173
|
+
--scope story \
|
|
174
|
+
--artifacts artifacts/windows/desktop-driver
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`serve` is the Stage 1 fake target. It is not a native-provider substitute.
|
|
178
|
+
|
|
179
|
+
## Agent API
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { connectDesktopAgent } from '@fluentui-react-native/desktop-driver/agent';
|
|
183
|
+
|
|
184
|
+
const agent = await connectDesktopAgent({
|
|
185
|
+
artifactsRoot: 'artifacts/windows/desktop-driver',
|
|
186
|
+
platformName: 'windows',
|
|
187
|
+
targetId: 'agenticstorybook-windows',
|
|
188
|
+
url: 'http://127.0.0.1:39859',
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await agent.listStories();
|
|
193
|
+
await agent.openStory('components-button--default');
|
|
194
|
+
await agent.describe({ scope: 'story', depth: 3, maxNodes: 100 });
|
|
195
|
+
await agent.click({ testId: 'agentic-storybook-button' });
|
|
196
|
+
await agent.screenshot('button');
|
|
197
|
+
await agent.runStoryTest('components-button--default', 'pointer-focus');
|
|
198
|
+
} finally {
|
|
199
|
+
await agent.delete();
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The API intentionally exposes coarse operations and bounded tree projections.
|
|
204
|
+
It does not reveal native handles or permit arbitrary process, environment, or
|
|
205
|
+
artifact-path capabilities.
|
|
206
|
+
|
|
207
|
+
## MCP decision
|
|
208
|
+
|
|
209
|
+
Phase 3 does not add another MCP listener. The Storybook MCP remains the
|
|
210
|
+
documentation and story-metadata surface; the typed agent API and JSON CLI are
|
|
211
|
+
the executable native-validation surface. Reconsider a composed MCP adapter
|
|
212
|
+
after Stage 2 proves the native commands and their security model. A tool-schema
|
|
213
|
+
file without an executable adapter is not considered an MCP integration.
|
|
214
|
+
|
|
215
|
+
## Protocol and security
|
|
216
|
+
|
|
217
|
+
See [SPEC.md](SPEC.md) for implemented W3C routes, extension commands,
|
|
218
|
+
capabilities, device contracts, and unsupported browser behavior.
|
|
219
|
+
|
|
220
|
+
The server binds loopback, rejects browser-origin requests, accepts only
|
|
221
|
+
server-registered targets, permits one session per physical target, applies
|
|
222
|
+
host command deadlines, and preserves attached applications. Never expose it
|
|
223
|
+
remotely without a separately designed authentication and transport policy.
|
package/SPEC.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Desktop Driver Contract
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
Desktop Driver is a W3C WebDriver Classic-compatible native desktop remote end.
|
|
6
|
+
It intentionally implements native application semantics rather than
|
|
7
|
+
pretending to be a browser.
|
|
8
|
+
|
|
9
|
+
V1 platform names are `macos` and `windows`; endpoint names are `macos`,
|
|
10
|
+
`windows`, and `win32`.
|
|
11
|
+
|
|
12
|
+
## Sessions and targets
|
|
13
|
+
|
|
14
|
+
- Targets are registered by the server.
|
|
15
|
+
- Capabilities select a target with `furn:target`.
|
|
16
|
+
- Capabilities never carry executable paths, environment variables, or output
|
|
17
|
+
directories.
|
|
18
|
+
- Exactly one active session may own a physical target.
|
|
19
|
+
- Commands are serialized per session, and input operations share one global
|
|
20
|
+
mutex.
|
|
21
|
+
- A target records whether the application was launched or attached.
|
|
22
|
+
- Deleting an attached session preserves the application.
|
|
23
|
+
- Cleanup uses owned process identity, never process-name matching.
|
|
24
|
+
|
|
25
|
+
Supported extension capabilities:
|
|
26
|
+
|
|
27
|
+
- `furn:target`;
|
|
28
|
+
- `furn:endpoint`;
|
|
29
|
+
- `furn:renderer`;
|
|
30
|
+
- `furn:clickMode`: `physical`, `accessibility`, or `auto`;
|
|
31
|
+
- returned `furn:features`.
|
|
32
|
+
|
|
33
|
+
## W3C routes
|
|
34
|
+
|
|
35
|
+
Implemented standard behavior:
|
|
36
|
+
|
|
37
|
+
- status and new/delete session;
|
|
38
|
+
- get/set timeouts;
|
|
39
|
+
- current window, handles, switching, closing, and rectangles;
|
|
40
|
+
- element lookup from a window or element;
|
|
41
|
+
- active element;
|
|
42
|
+
- element name, text, attribute, property, rectangle, enabled, selected, and
|
|
43
|
+
displayed queries;
|
|
44
|
+
- click, clear, and send keys;
|
|
45
|
+
- perform/release actions;
|
|
46
|
+
- window and element screenshots;
|
|
47
|
+
- accessibility source.
|
|
48
|
+
|
|
49
|
+
Implemented `furn` extensions:
|
|
50
|
+
|
|
51
|
+
- `GET /session/{id}/furn/manifest`;
|
|
52
|
+
- `GET|POST /session/{id}/furn/story`;
|
|
53
|
+
- `POST /session/{id}/furn/story/reset`;
|
|
54
|
+
- `POST /session/{id}/furn/story/args`;
|
|
55
|
+
- `GET /session/{id}/furn/tree`.
|
|
56
|
+
|
|
57
|
+
Navigation, cookies, frames, shadow roots, prompts, printing, arbitrary
|
|
58
|
+
JavaScript execution, and browser CSS behavior return `unsupported operation`.
|
|
59
|
+
|
|
60
|
+
## Elements
|
|
61
|
+
|
|
62
|
+
Public references use only the standard
|
|
63
|
+
`element-6066-11e4-a52e-4f735466cecf` key with session UUIDs. Native handles,
|
|
64
|
+
UIA runtime IDs, AX references, React tags, and HWNDs are private.
|
|
65
|
+
|
|
66
|
+
Every command validates native liveness. Story reset increments the preview
|
|
67
|
+
generation and invalidates preview elements while preserving live application
|
|
68
|
+
and Storybook chrome references.
|
|
69
|
+
|
|
70
|
+
Pointer and wheel actions may use WebDriver element origins. The server resolves
|
|
71
|
+
the public session element UUID, validates preview generation and native
|
|
72
|
+
liveness, and passes only `{ elementId: <native-id> }` to the host contract.
|
|
73
|
+
Public WebDriver element references never cross into platform providers.
|
|
74
|
+
|
|
75
|
+
Every host operation receives an `AbortSignal`. On timeout, the server aborts
|
|
76
|
+
the host operation before input release, session teardown, or a subsequent
|
|
77
|
+
command. Providers must stop side effects and settle promptly; they may never
|
|
78
|
+
apply late input after cleanup.
|
|
79
|
+
|
|
80
|
+
Portable lookup uses accessibility ID, normalized role, accessible name, and
|
|
81
|
+
the namespaced `-furn:text` strategy. Deterministic authored interactions use
|
|
82
|
+
`testID`.
|
|
83
|
+
|
|
84
|
+
## Storybook readiness
|
|
85
|
+
|
|
86
|
+
The runtime and server validate:
|
|
87
|
+
|
|
88
|
+
- instance, target, endpoint, and catalog identity;
|
|
89
|
+
- a private bridge nonce;
|
|
90
|
+
- request and run IDs;
|
|
91
|
+
- portable-plan digest;
|
|
92
|
+
- preview generation;
|
|
93
|
+
- the native story-root marker.
|
|
94
|
+
|
|
95
|
+
Only the authenticated bridge client can complete or fail a selection. Every
|
|
96
|
+
test receives a new run ID and keyed preview remount.
|
|
97
|
+
|
|
98
|
+
## Results
|
|
99
|
+
|
|
100
|
+
Run status is `passed` or `failed`. Tests distinguish `passed`, `failed`,
|
|
101
|
+
`skipped`, `timed-out`, `cancelled`, and `infrastructure-error`. Steps record
|
|
102
|
+
status, duration, error, and artifacts.
|
|
103
|
+
|
|
104
|
+
Unsupported required capabilities are skipped explicitly. An unavailable
|
|
105
|
+
native property never becomes a false-shaped passing assertion.
|
package/config/cli.cjs
ADDED
package/jest.config.cjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DesktopArtifact, DesktopStoryRunResult } from '../authoring/results.js';
|
|
2
|
+
import type { DesktopStoryExpectation, DesktopStorySelector } from '../authoring/storyTests.js';
|
|
3
|
+
import { ArtifactManager } from '../artifacts/ArtifactManager.js';
|
|
4
|
+
import type { DesktopTreeNode } from '../host/types.js';
|
|
5
|
+
import type { DesktopStoryManifestEntry } from '../storybook.js';
|
|
6
|
+
import type { DesktopWebdriverOptions, DesktopWebdriverSession } from '../wdio/DesktopWebdriver.js';
|
|
7
|
+
export type DesktopAgentOptions = DesktopWebdriverOptions & {
|
|
8
|
+
artifactsRoot: string;
|
|
9
|
+
};
|
|
10
|
+
export type DesktopAgentStory = Pick<DesktopStoryManifestEntry, 'id' | 'name' | 'tags' | 'title'> & {
|
|
11
|
+
tests: readonly {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
16
|
+
export type DesktopAgentElement = {
|
|
17
|
+
accessibleName: unknown;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
id: string;
|
|
20
|
+
role: string;
|
|
21
|
+
selected: boolean;
|
|
22
|
+
text: string;
|
|
23
|
+
};
|
|
24
|
+
export type DesktopAgentCheckResult = {
|
|
25
|
+
message?: string;
|
|
26
|
+
passed: boolean;
|
|
27
|
+
};
|
|
28
|
+
export type DesktopAgentDescribeOptions = {
|
|
29
|
+
depth?: number;
|
|
30
|
+
maxNodes?: number;
|
|
31
|
+
scope?: 'application' | 'story';
|
|
32
|
+
};
|
|
33
|
+
export declare class DesktopAgent {
|
|
34
|
+
private readonly artifacts;
|
|
35
|
+
private readonly desktop;
|
|
36
|
+
private readonly captured;
|
|
37
|
+
constructor(desktop: DesktopWebdriverSession, artifacts: ArtifactManager);
|
|
38
|
+
listStories(): Promise<DesktopAgentStory[]>;
|
|
39
|
+
explainStory(storyId: string): Promise<DesktopAgentStory>;
|
|
40
|
+
openStory(storyId: string): Promise<{
|
|
41
|
+
previewGeneration: number;
|
|
42
|
+
runId: string;
|
|
43
|
+
storyId: string;
|
|
44
|
+
}>;
|
|
45
|
+
describe(options?: DesktopAgentDescribeOptions): Promise<DesktopTreeNode[]>;
|
|
46
|
+
find(selector: DesktopStorySelector): Promise<DesktopAgentElement>;
|
|
47
|
+
click(selector: DesktopStorySelector): Promise<void>;
|
|
48
|
+
type(selector: DesktopStorySelector, text: string): Promise<void>;
|
|
49
|
+
check(expectation: DesktopStoryExpectation): Promise<DesktopAgentCheckResult>;
|
|
50
|
+
screenshot(name: string): Promise<DesktopArtifact>;
|
|
51
|
+
runStoryTest(storyId: string, testId: string): Promise<DesktopStoryRunResult>;
|
|
52
|
+
getArtifacts(): readonly DesktopArtifact[];
|
|
53
|
+
delete(): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
export declare function connectDesktopAgent(options: DesktopAgentOptions): Promise<DesktopAgent>;
|
|
56
|
+
//# sourceMappingURL=DesktopAgent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DesktopAgent.d.ts","sourceRoot":"","sources":["../../src/agent/DesktopAgent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACtF,OAAO,KAAK,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAChG,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAEjE,OAAO,KAAK,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAEpG,MAAM,MAAM,mBAAmB,GAAG,uBAAuB,GAAG;IAC1D,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,yBAAyB,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG;IAClG,KAAK,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACjD,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,cAAc,EAAE,OAAO,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkB;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAClD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAElD,YAAY,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,eAAe,EAGvE;IAEK,WAAW,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAShD;IAEK,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAM9D;IAED,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,iBAAiB,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAEjG;IAEK,QAAQ,CAAC,OAAO,GAAE,2BAAgC,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAkBpF;IAEK,IAAI,CAAC,QAAQ,EAAE,oBAAoB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAUvE;IAEK,KAAK,CAAC,QAAQ,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEzD;IAEK,IAAI,CAAC,QAAQ,EAAE,oBAAoB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtE;IAEK,KAAK,CAAC,WAAW,EAAE,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAUlF;IAEK,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAIvD;IAED,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAK5E;IAED,YAAY,IAAI,SAAS,eAAe,EAAE,CAEzC;IAED,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAEtB;CACF;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,CAAC,CAI7F"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ArtifactManager } from '../artifacts/ArtifactManager.js';
|
|
2
|
+
import { DesktopAssertionError, assertDesktopExpectation, findDesktopElement } from '../runner/StoryTestRunner.js';
|
|
3
|
+
import { WebDriverError } from '../protocol/errors.js';
|
|
4
|
+
import { connectDesktopWebdriver } from '../wdio/DesktopWebdriver.js';
|
|
5
|
+
export class DesktopAgent {
|
|
6
|
+
artifacts;
|
|
7
|
+
desktop;
|
|
8
|
+
captured = [];
|
|
9
|
+
constructor(desktop, artifacts) {
|
|
10
|
+
this.desktop = desktop;
|
|
11
|
+
this.artifacts = artifacts;
|
|
12
|
+
}
|
|
13
|
+
async listStories() {
|
|
14
|
+
const manifest = await this.desktop.listStories();
|
|
15
|
+
return manifest.entries.map((entry) => ({
|
|
16
|
+
id: entry.id,
|
|
17
|
+
name: entry.name,
|
|
18
|
+
tags: entry.tags,
|
|
19
|
+
tests: (entry.tests?.tests ?? []).map((test) => ({ id: test.id, title: test.title ?? test.id })),
|
|
20
|
+
title: entry.title,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
async explainStory(storyId) {
|
|
24
|
+
const story = (await this.listStories()).find(({ id }) => id === storyId);
|
|
25
|
+
if (!story) {
|
|
26
|
+
throw new Error(`Story "${storyId}" is not present in the active platform manifest.`);
|
|
27
|
+
}
|
|
28
|
+
return story;
|
|
29
|
+
}
|
|
30
|
+
openStory(storyId) {
|
|
31
|
+
return this.desktop.openStory(storyId);
|
|
32
|
+
}
|
|
33
|
+
async describe(options = {}) {
|
|
34
|
+
const depth = options.depth ?? 3;
|
|
35
|
+
const maxNodes = options.maxNodes ?? 100;
|
|
36
|
+
if (!Number.isInteger(depth) || depth < 0 || !Number.isInteger(maxNodes) || maxNodes < 1) {
|
|
37
|
+
throw new TypeError('Agent tree depth must be non-negative and maxNodes must be positive.');
|
|
38
|
+
}
|
|
39
|
+
const roots = await this.desktop.session.getTree();
|
|
40
|
+
const scoped = options.scope === 'story'
|
|
41
|
+
? roots.flatMap((root) => findTreeNodes(root, (node) => node.testId === 'story-root' || (node.testId?.endsWith('-story-root') ?? false)))
|
|
42
|
+
: roots;
|
|
43
|
+
let remaining = maxNodes;
|
|
44
|
+
return scoped.flatMap((root) => {
|
|
45
|
+
const projected = projectTree(root, depth, () => remaining-- > 0);
|
|
46
|
+
return projected ? [projected] : [];
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async find(selector) {
|
|
50
|
+
const element = await findDesktopElement(this.desktop.session, selector);
|
|
51
|
+
return {
|
|
52
|
+
accessibleName: await element.getAttribute('name'),
|
|
53
|
+
enabled: await element.isEnabled(),
|
|
54
|
+
id: element.id,
|
|
55
|
+
role: await element.getTagName(),
|
|
56
|
+
selected: await element.isSelected(),
|
|
57
|
+
text: await element.getText(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async click(selector) {
|
|
61
|
+
await (await findDesktopElement(this.desktop.session, selector)).click();
|
|
62
|
+
}
|
|
63
|
+
async type(selector, text) {
|
|
64
|
+
await (await findDesktopElement(this.desktop.session, selector)).sendKeys(text);
|
|
65
|
+
}
|
|
66
|
+
async check(expectation) {
|
|
67
|
+
try {
|
|
68
|
+
await assertDesktopExpectation(this.desktop.session, expectation);
|
|
69
|
+
return { passed: true };
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof DesktopAssertionError || (error instanceof WebDriverError && error.code === 'no such element')) {
|
|
73
|
+
return { message: error.message, passed: false };
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async screenshot(name) {
|
|
79
|
+
const artifact = this.artifacts.writeScreenshot('agent', name, await this.desktop.session.takeScreenshot());
|
|
80
|
+
this.captured.push(artifact);
|
|
81
|
+
return artifact;
|
|
82
|
+
}
|
|
83
|
+
runStoryTest(storyId, testId) {
|
|
84
|
+
return this.desktop.runStoryTests({
|
|
85
|
+
artifactsRoot: this.artifacts.root,
|
|
86
|
+
selection: { story: storyId, test: testId },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
getArtifacts() {
|
|
90
|
+
return [...this.captured];
|
|
91
|
+
}
|
|
92
|
+
delete() {
|
|
93
|
+
return this.desktop.delete();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function connectDesktopAgent(options) {
|
|
97
|
+
const artifacts = new ArtifactManager(options.artifactsRoot);
|
|
98
|
+
const desktop = await connectDesktopWebdriver(options);
|
|
99
|
+
return new DesktopAgent(desktop, artifacts);
|
|
100
|
+
}
|
|
101
|
+
function findTreeNodes(node, predicate) {
|
|
102
|
+
return [...(predicate(node) ? [node] : []), ...node.children.flatMap((child) => findTreeNodes(child, predicate))];
|
|
103
|
+
}
|
|
104
|
+
function projectTree(node, depth, take) {
|
|
105
|
+
if (!take()) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...node,
|
|
110
|
+
children: depth === 0
|
|
111
|
+
? []
|
|
112
|
+
: node.children.flatMap((child) => {
|
|
113
|
+
const projected = projectTree(child, depth - 1, take);
|
|
114
|
+
return projected ? [projected] : [];
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=DesktopAgent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DesktopAgent.js","sourceRoot":"","sources":["../../src/agent/DesktopAgent.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DesktopAgent.test.d.ts","sourceRoot":"","sources":["../../src/agent/DesktopAgent.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createDesktopDriverClient } from '../client/DesktopDriverClient.js';
|
|
6
|
+
import { createDesktopDriverTestHarness } from '../testing/protocolHarness.js';
|
|
7
|
+
import { createDesktopDriverStoryHarness } from '../testing/protocolHarness.js';
|
|
8
|
+
import { connectDesktopAgent } from './DesktopAgent.js';
|
|
9
|
+
describe('DesktopAgent', () => {
|
|
10
|
+
test('lists, explains, inspects, acts, checks, captures, and runs the same authored plan', async () => {
|
|
11
|
+
const manifest = {
|
|
12
|
+
endpoint: 'windows',
|
|
13
|
+
entries: [
|
|
14
|
+
{
|
|
15
|
+
id: 'components-button--default',
|
|
16
|
+
name: 'Default',
|
|
17
|
+
packageName: '@fluentui-react-native/components',
|
|
18
|
+
sourcePath: 'src/components/button/button.stories.tsx',
|
|
19
|
+
tags: ['e2e', 'story'],
|
|
20
|
+
tests: {
|
|
21
|
+
version: 1,
|
|
22
|
+
tests: [
|
|
23
|
+
{
|
|
24
|
+
id: 'agent-plan',
|
|
25
|
+
steps: [{ expect: { state: 'enabled', target: { testId: 'button-primary' }, value: true } }],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
title: 'Components/Button',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
platformManifestDigest: 'platform-digest',
|
|
33
|
+
portablePlanDigest: 'portable-digest',
|
|
34
|
+
schemaVersion: 1,
|
|
35
|
+
};
|
|
36
|
+
const harness = await createDesktopDriverStoryHarness(manifest);
|
|
37
|
+
const artifactsRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-driver-agent-'));
|
|
38
|
+
try {
|
|
39
|
+
const response = await runContract(harness.server.url, harness.target.id, artifactsRoot);
|
|
40
|
+
expect(response).toMatchObject({
|
|
41
|
+
check: { passed: true },
|
|
42
|
+
run: { status: 'passed', tests: [{ testId: 'agent-plan' }] },
|
|
43
|
+
screenshot: { kind: 'screenshot', name: 'agent-button' },
|
|
44
|
+
stories: 1,
|
|
45
|
+
tree: 1,
|
|
46
|
+
});
|
|
47
|
+
expect(fs.existsSync(path.join(artifactsRoot, 'run.json'))).toBe(true);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await harness.close();
|
|
51
|
+
fs.rmSync(artifactsRoot, { force: true, recursive: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
test('validates the artifact root before reserving a target session', async () => {
|
|
55
|
+
const harness = await createDesktopDriverTestHarness();
|
|
56
|
+
const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-driver-agent-invalid-'));
|
|
57
|
+
const invalidRoot = path.join(temporaryDirectory, 'file');
|
|
58
|
+
fs.writeFileSync(invalidRoot, 'not a directory');
|
|
59
|
+
try {
|
|
60
|
+
await expect(connectDesktopAgent({
|
|
61
|
+
artifactsRoot: invalidRoot,
|
|
62
|
+
platformName: 'windows',
|
|
63
|
+
targetId: harness.target.id,
|
|
64
|
+
url: harness.server.url,
|
|
65
|
+
})).rejects.toThrow();
|
|
66
|
+
const client = createDesktopDriverClient({ url: harness.server.url });
|
|
67
|
+
const session = await client.newSession({
|
|
68
|
+
alwaysMatch: { platformName: 'windows', 'furn:target': harness.target.id },
|
|
69
|
+
});
|
|
70
|
+
await session.delete();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await harness.close();
|
|
74
|
+
fs.rmSync(temporaryDirectory, { force: true, recursive: true });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
function runContract(url, targetId, artifactsRoot) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const child = spawn(process.execPath, [path.join(__dirname, 'agent.contract.cjs'), url, targetId, artifactsRoot], {
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
82
|
+
});
|
|
83
|
+
const stdout = [];
|
|
84
|
+
const stderr = [];
|
|
85
|
+
child.stdout.on('data', (chunk) => stdout.push(chunk));
|
|
86
|
+
child.stderr.on('data', (chunk) => stderr.push(chunk));
|
|
87
|
+
child.once('error', reject);
|
|
88
|
+
child.once('exit', (code) => {
|
|
89
|
+
if (code !== 0) {
|
|
90
|
+
reject(new Error(Buffer.concat(stderr).toString('utf8') || `Desktop agent exited with code ${code}.`));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
resolve(JSON.parse(Buffer.concat(stdout).toString('utf8')));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=DesktopAgent.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DesktopAgent.test.js","sourceRoot":"","sources":["../../src/agent/DesktopAgent.test.ts"],"names":[],"mappings":"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"}
|