@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
|
@@ -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
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -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
|
+
}
|