@allwright.dev/core 0.1.6 → 0.1.8
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/README.md +18 -0
- package/dist/bootstrap.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -0
- package/dist/mobile.js +195 -1
- package/dist/page.d.ts +19 -1
- package/dist/page.js +169 -3
- package/dist/types.d.ts +138 -1
- package/package.json +1 -1
- package/proto/core/v1/context.proto +49 -0
- package/proto/surfaces/mobile/v1/mobile.proto +9 -0
- package/proto/surfaces/web/v1/web.proto +42 -0
package/README.md
CHANGED
|
@@ -33,6 +33,24 @@ await page.click("a[target=_blank]");
|
|
|
33
33
|
const newPage = await hook.wait();
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
```ts
|
|
37
|
+
const hook = await page.registerHook(hooks.fileChooser);
|
|
38
|
+
await page.click("button.open-upload");
|
|
39
|
+
const chooser = await hook.wait();
|
|
40
|
+
await chooser.setFiles("fixtures/document.pdf");
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const hook = await page.registerHook(hooks.download);
|
|
45
|
+
await page.click("a.download-report");
|
|
46
|
+
const download = await hook.wait();
|
|
47
|
+
await download.saveAs(`artifacts/${download.suggestedFilename}`);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Paths passed to `setFiles` and `saveAs` belong to the test process, even when
|
|
51
|
+
the Allwright server is remote. Android apps support the same `fileChooser` and
|
|
52
|
+
`download` hook shape; Android `newPage` hooks are not applicable.
|
|
53
|
+
|
|
36
54
|
Runnable examples live in [examples/web-basic.ts](./examples/web-basic.ts) and [examples/android-basic.ts](./examples/android-basic.ts).
|
|
37
55
|
|
|
38
56
|
Shared config files are stack-agnostic and can live in `allwright.config.yaml` or `allwright.config.json`.
|
package/dist/bootstrap.js
CHANGED
|
@@ -12,7 +12,7 @@ const ALLWRIGHT_HOME_ENV_VAR = "ALLWRIGHT_HOME";
|
|
|
12
12
|
const ALLWRIGHT_REPOSITORY_ENV_VAR = "ALLWRIGHT_REPOSITORY";
|
|
13
13
|
const ALLWRIGHT_VERSION_ENV_VAR = "ALLWRIGHT_VERSION";
|
|
14
14
|
const DEFAULT_RELEASE_REPOSITORY = "allwright-dev/allwright";
|
|
15
|
-
const DEFAULT_RELEASE_VERSION = "0.1.
|
|
15
|
+
const DEFAULT_RELEASE_VERSION = "0.1.8";
|
|
16
16
|
const STARTUP_TIMEOUT_MS = 20_000;
|
|
17
17
|
const PING_TIMEOUT_MS = 1_000;
|
|
18
18
|
const PROTO_ROOT = fileURLToPath(new URL("../proto/", import.meta.url));
|
package/dist/index.d.ts
CHANGED
|
@@ -3,13 +3,15 @@ import { findConfigFile, loadConfigFile, resolveConfig } from "./config.js";
|
|
|
3
3
|
import { mobile } from "./mobile.js";
|
|
4
4
|
import { PageImpl } from "./page.js";
|
|
5
5
|
import { setServerAddr, shutdown } from "./runtime.js";
|
|
6
|
-
import type { Browser, BrowserKind, BrowserType, LaunchOptions, Page, HookType, ResolveConfigOptions, ResolvedAllwrightConfig } from "./types.js";
|
|
6
|
+
import type { Browser, BrowserKind, BrowserType, LaunchOptions, Page, FileChooser, Download, HookType, ResolveConfigOptions, ResolvedAllwrightConfig } from "./types.js";
|
|
7
7
|
export { findConfigFile, loadConfigFile, resolveConfig, setServerAddr, shutdown };
|
|
8
|
-
export type { AccessibilitySnapshotOptions, AllwrightConfig, Browser, BrowserInfo, BrowserKind, BrowserType, Hook, HookType, ClickResult, CommandOptions, CountResult, ElementResult, FillResult, HighlightOptions, HighlightResult, LaunchOptions, MobileAndroidConnectOptions, MobileAndroidDevice, MobileAndroidLaunchOptions, MobileAndroidLocator, MobileAndroidApp, Locator, LocatorInfo, MobileSurfaceNamespace, NavigateResult, Page, PageInfo, PressOptions, PressResult, ResolveConfigOptions, ResolvedAllwrightConfig, RetryConfig, ScreenshotOptions, ScreenshotResult, TextResult, WaitForSelectorOptions, WaitForSelectorResult, } from "./types.js";
|
|
8
|
+
export type { AccessibilitySnapshotOptions, AllwrightConfig, Browser, BrowserInfo, BrowserKind, BrowserType, Hook, HookType, FileChooser, Download, ClickResult, CommandOptions, CountResult, ElementResult, FillResult, HighlightOptions, HighlightResult, LaunchOptions, MobileAndroidConnectOptions, MobileAndroidDevice, MobileAndroidLaunchOptions, MobileAndroidLocator, MobileAndroidApp, Locator, LocatorInfo, MobileSurfaceNamespace, NavigateResult, Page, PageInfo, PressOptions, PressResult, ResolveConfigOptions, ResolvedAllwrightConfig, RetryConfig, ScreenshotOptions, ScreenshotResult, TextResult, WaitForSelectorOptions, WaitForSelectorResult, } from "./types.js";
|
|
9
9
|
export declare const chromium: BrowserType;
|
|
10
10
|
export declare const firefox: BrowserType;
|
|
11
11
|
export declare const hooks: {
|
|
12
12
|
newPage: HookType<Page>;
|
|
13
|
+
fileChooser: HookType<FileChooser>;
|
|
14
|
+
download: HookType<Download>;
|
|
13
15
|
};
|
|
14
16
|
export { mobile };
|
|
15
17
|
export type Tab = Page;
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ export const chromium = new BrowserTypeImpl("chromium");
|
|
|
9
9
|
export const firefox = new BrowserTypeImpl("firefox");
|
|
10
10
|
export const hooks = {
|
|
11
11
|
newPage: Object.freeze({ name: "newPage" }),
|
|
12
|
+
fileChooser: Object.freeze({ name: "fileChooser" }),
|
|
13
|
+
download: Object.freeze({ name: "download" }),
|
|
12
14
|
};
|
|
13
15
|
export { mobile };
|
|
14
16
|
export async function ping() {
|
package/dist/mobile.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createBrowserSessionHandle, createPageHandle, getRuntime } from "./runtime.js";
|
|
2
|
-
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { open, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { basename } from "node:path";
|
|
3
5
|
import { chainMobileSelectorForTransport, normalizeMobileSelectorForTransport } from "./mobileSelectors.js";
|
|
4
6
|
function retryOptions(timeoutMs) {
|
|
5
7
|
return timeoutMs ? { timeoutMs } : undefined;
|
|
@@ -17,6 +19,164 @@ class MobileAndroidAppImpl {
|
|
|
17
19
|
locator(selector) {
|
|
18
20
|
return new MobileAndroidLocatorImpl(this, normalizeMobileSelectorForTransport(selector));
|
|
19
21
|
}
|
|
22
|
+
async registerHook(type) {
|
|
23
|
+
if (type.name !== "fileChooser" && type.name !== "download") {
|
|
24
|
+
throw new Error(`hook type ${type.name} is not supported by Android apps`);
|
|
25
|
+
}
|
|
26
|
+
const handle = await this.#getHandle();
|
|
27
|
+
this.#ensureOpen(handle);
|
|
28
|
+
handle.stream.write({
|
|
29
|
+
surfaceSessionId: this.#surfaceSessionId,
|
|
30
|
+
contextSessionId: this.sessionId,
|
|
31
|
+
registerHook: type.name === "fileChooser"
|
|
32
|
+
? { mobileFileChooser: {} }
|
|
33
|
+
: { mobileDownload: {} },
|
|
34
|
+
});
|
|
35
|
+
while (true) {
|
|
36
|
+
const event = await handle.queue.next();
|
|
37
|
+
if (event.hookRegistered?.hookId) {
|
|
38
|
+
const hookId = event.hookRegistered.hookId;
|
|
39
|
+
return {
|
|
40
|
+
id: hookId,
|
|
41
|
+
type,
|
|
42
|
+
wait: (options = {}) => this.#waitForHook(hookId, type, options),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (event.error?.message)
|
|
46
|
+
throw new Error(event.error.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async #waitForHook(hookId, type, options) {
|
|
50
|
+
const handle = await this.#getHandle();
|
|
51
|
+
this.#ensureOpen(handle);
|
|
52
|
+
handle.stream.write({
|
|
53
|
+
surfaceSessionId: this.#surfaceSessionId,
|
|
54
|
+
contextSessionId: this.sessionId,
|
|
55
|
+
waitForHook: { hookId, retryOptions: retryOptions(options.timeoutMs) },
|
|
56
|
+
});
|
|
57
|
+
while (true) {
|
|
58
|
+
const event = await handle.queue.next();
|
|
59
|
+
if (event.hookCompleted?.hookId === hookId) {
|
|
60
|
+
if (type.name === "fileChooser" && event.hookCompleted.mobileFileChooser?.fileChooserId) {
|
|
61
|
+
return new MobileFileChooserImpl(this, event.hookCompleted.mobileFileChooser.fileChooserId, event.hookCompleted.mobileFileChooser.isMultiple ?? false);
|
|
62
|
+
}
|
|
63
|
+
if (type.name === "download" && event.hookCompleted.mobileDownload?.downloadId) {
|
|
64
|
+
return new MobileDownloadImpl(this, event.hookCompleted.mobileDownload.downloadId, event.hookCompleted.mobileDownload.suggestedFilename ?? "download");
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Android ${type.name} hook returned an invalid result`);
|
|
67
|
+
}
|
|
68
|
+
if (event.error?.message)
|
|
69
|
+
throw new Error(event.error.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async setChooserFiles(fileChooserId, files, options) {
|
|
73
|
+
const handle = await this.#getHandle();
|
|
74
|
+
this.#ensureOpen(handle);
|
|
75
|
+
const fileIds = [];
|
|
76
|
+
for (const file of files)
|
|
77
|
+
fileIds.push(await this.#uploadLocalFile(handle, file));
|
|
78
|
+
handle.stream.write({
|
|
79
|
+
surfaceSessionId: this.#surfaceSessionId,
|
|
80
|
+
contextSessionId: this.sessionId,
|
|
81
|
+
setMobileFileChooserFiles: {
|
|
82
|
+
fileChooserId,
|
|
83
|
+
fileIds,
|
|
84
|
+
retryOptions: retryOptions(options.timeoutMs),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
while (true) {
|
|
88
|
+
const event = await handle.queue.next();
|
|
89
|
+
if (event.mobileFileChooserFilesSet?.fileChooserId === fileChooserId)
|
|
90
|
+
return;
|
|
91
|
+
if (event.error?.message)
|
|
92
|
+
throw new Error(event.error.message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async saveMobileDownload(downloadId, path, options) {
|
|
96
|
+
const handle = await this.#getHandle();
|
|
97
|
+
this.#ensureOpen(handle);
|
|
98
|
+
handle.stream.write({
|
|
99
|
+
surfaceSessionId: this.#surfaceSessionId,
|
|
100
|
+
contextSessionId: this.sessionId,
|
|
101
|
+
saveMobileDownload: { downloadId, retryOptions: retryOptions(options.timeoutMs) },
|
|
102
|
+
});
|
|
103
|
+
while (true) {
|
|
104
|
+
const event = await handle.queue.next();
|
|
105
|
+
if (event.mobileDownloadSaved?.downloadId === downloadId && event.mobileDownloadSaved.fileId) {
|
|
106
|
+
await this.#downloadToLocalFile(handle, event.mobileDownloadSaved.fileId, path);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (event.error?.message)
|
|
110
|
+
throw new Error(event.error.message);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async #uploadLocalFile(handle, path) {
|
|
114
|
+
const source = await open(path, "r");
|
|
115
|
+
const transferId = randomUUID();
|
|
116
|
+
let offset = 0;
|
|
117
|
+
try {
|
|
118
|
+
const size = (await source.stat()).size;
|
|
119
|
+
do {
|
|
120
|
+
const buffer = Buffer.alloc(Math.min(256 * 1024, Math.max(0, size - offset)) || 1);
|
|
121
|
+
const { bytesRead } = await source.read(buffer, 0, buffer.length, offset);
|
|
122
|
+
const last = offset + bytesRead >= size;
|
|
123
|
+
handle.stream.write({
|
|
124
|
+
surfaceSessionId: this.#surfaceSessionId,
|
|
125
|
+
contextSessionId: this.sessionId,
|
|
126
|
+
uploadFileChunk: {
|
|
127
|
+
transferId,
|
|
128
|
+
name: basename(path),
|
|
129
|
+
offset: String(offset),
|
|
130
|
+
data: buffer.subarray(0, bytesRead),
|
|
131
|
+
last,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
offset += bytesRead;
|
|
135
|
+
} while (offset < size);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
await source.close();
|
|
139
|
+
}
|
|
140
|
+
while (true) {
|
|
141
|
+
const event = await handle.queue.next();
|
|
142
|
+
if (event.fileUploaded?.transferId === transferId && event.fileUploaded.fileId) {
|
|
143
|
+
return event.fileUploaded.fileId;
|
|
144
|
+
}
|
|
145
|
+
if (event.error?.message)
|
|
146
|
+
throw new Error(event.error.message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async #downloadToLocalFile(handle, fileId, path) {
|
|
150
|
+
const temporaryPath = `${path}.allwright-${randomUUID()}.tmp`;
|
|
151
|
+
const destination = await open(temporaryPath, "wx");
|
|
152
|
+
let offset = 0;
|
|
153
|
+
try {
|
|
154
|
+
while (true) {
|
|
155
|
+
handle.stream.write({
|
|
156
|
+
surfaceSessionId: this.#surfaceSessionId,
|
|
157
|
+
contextSessionId: this.sessionId,
|
|
158
|
+
readFileChunk: { fileId, offset: String(offset), maxBytes: 256 * 1024 },
|
|
159
|
+
});
|
|
160
|
+
const event = await handle.queue.next();
|
|
161
|
+
if (event.error?.message)
|
|
162
|
+
throw new Error(event.error.message);
|
|
163
|
+
if (event.fileChunk?.fileId !== fileId || Number(event.fileChunk.offset ?? -1) !== offset)
|
|
164
|
+
continue;
|
|
165
|
+
const data = event.fileChunk.data ?? new Uint8Array();
|
|
166
|
+
await destination.write(data, 0, data.length, offset);
|
|
167
|
+
offset += data.length;
|
|
168
|
+
if (event.fileChunk.last)
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
await destination.close();
|
|
172
|
+
await rename(temporaryPath, path);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
await destination.close().catch(() => undefined);
|
|
176
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
20
180
|
async click(selector, options = {}) {
|
|
21
181
|
const handle = await this.#getHandle();
|
|
22
182
|
this.#ensureOpen(handle);
|
|
@@ -322,6 +482,40 @@ class MobileAndroidAppImpl {
|
|
|
322
482
|
}
|
|
323
483
|
}
|
|
324
484
|
}
|
|
485
|
+
class MobileFileChooserImpl {
|
|
486
|
+
page;
|
|
487
|
+
id;
|
|
488
|
+
multiple;
|
|
489
|
+
constructor(page, id, multiple) {
|
|
490
|
+
this.page = page;
|
|
491
|
+
this.id = id;
|
|
492
|
+
this.multiple = multiple;
|
|
493
|
+
}
|
|
494
|
+
isMultiple() {
|
|
495
|
+
return this.multiple;
|
|
496
|
+
}
|
|
497
|
+
async setFiles(files, options = {}) {
|
|
498
|
+
const paths = typeof files === "string" ? [files] : files;
|
|
499
|
+
if (!this.multiple && paths.length > 1) {
|
|
500
|
+
throw new Error("Android file chooser does not accept multiple files");
|
|
501
|
+
}
|
|
502
|
+
await this.page.setChooserFiles(this.id, paths, options);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
class MobileDownloadImpl {
|
|
506
|
+
page;
|
|
507
|
+
id;
|
|
508
|
+
suggestedFilename;
|
|
509
|
+
url = "";
|
|
510
|
+
constructor(page, id, suggestedFilename) {
|
|
511
|
+
this.page = page;
|
|
512
|
+
this.id = id;
|
|
513
|
+
this.suggestedFilename = suggestedFilename;
|
|
514
|
+
}
|
|
515
|
+
async saveAs(path, options = {}) {
|
|
516
|
+
await this.page.saveMobileDownload(this.id, path, options);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
325
519
|
class MobileAndroidLocatorImpl {
|
|
326
520
|
page;
|
|
327
521
|
selector;
|
package/dist/page.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WebLocatorBuilders, type LocatorFilterOptions } from "./web-locators.js";
|
|
2
|
-
import type { AccessibilitySnapshotOptions, ClickResult, CommandOptions, CountResult, ElementResult, FillResult, HighlightOptions, HighlightResult, Hook, HookType, Locator, NavigateResult, Page, PageInfo, PressOptions, PressResult, RuntimeClient, ScreenshotOptions, ScreenshotResult, TextResult, WaitForSelectorOptions, WaitForSelectorResult } from "./types.js";
|
|
2
|
+
import type { AccessibilitySnapshotOptions, ClickResult, CommandOptions, CountResult, ElementResult, FillResult, FileChooser, Download, HighlightOptions, HighlightResult, Hook, HookType, Locator, NavigateResult, Page, PageInfo, PressOptions, PressResult, RuntimeClient, ScreenshotOptions, ScreenshotResult, TextResult, WaitForSelectorOptions, WaitForSelectorResult } from "./types.js";
|
|
3
3
|
export declare class PageImpl extends WebLocatorBuilders implements Page {
|
|
4
4
|
#private;
|
|
5
5
|
constructor(input: PageInfo & {
|
|
@@ -9,6 +9,8 @@ export declare class PageImpl extends WebLocatorBuilders implements Page {
|
|
|
9
9
|
readonly sessionId: string;
|
|
10
10
|
readonly browserSessionId: string;
|
|
11
11
|
registerHook<T>(type: HookType<T>): Promise<Hook<T>>;
|
|
12
|
+
setFileChooserFiles(fileChooserId: string, files: string[], options?: CommandOptions): Promise<void>;
|
|
13
|
+
saveDownload(downloadId: string, path: string, options?: CommandOptions): Promise<void>;
|
|
12
14
|
locator(selector: string, options?: LocatorFilterOptions): Locator;
|
|
13
15
|
goto(url: string, options?: CommandOptions): Promise<NavigateResult>;
|
|
14
16
|
click(selector: string, options?: CommandOptions): Promise<ClickResult>;
|
|
@@ -29,3 +31,19 @@ export declare class PageImpl extends WebLocatorBuilders implements Page {
|
|
|
29
31
|
navigate(url: string, options?: CommandOptions): Promise<NavigateResult>;
|
|
30
32
|
dispose(): void;
|
|
31
33
|
}
|
|
34
|
+
export declare class FileChooserImpl implements FileChooser {
|
|
35
|
+
readonly page: PageImpl;
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly multiple: boolean;
|
|
38
|
+
constructor(page: PageImpl, id: string, multiple: boolean);
|
|
39
|
+
isMultiple(): boolean;
|
|
40
|
+
setFiles(files: string | string[], options?: CommandOptions): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
export declare class DownloadImpl implements Download {
|
|
43
|
+
readonly page: PageImpl;
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly url: string;
|
|
46
|
+
readonly suggestedFilename: string;
|
|
47
|
+
constructor(page: PageImpl, id: string, url: string, suggestedFilename: string);
|
|
48
|
+
saveAs(path: string, options?: CommandOptions): Promise<void>;
|
|
49
|
+
}
|
package/dist/page.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { WebLocatorBuilders } from "./web-locators.js";
|
|
2
2
|
import { LocatorImpl } from "./locator.js";
|
|
3
|
-
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { open, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { basename } from "node:path";
|
|
4
6
|
import { formatActionError } from "./errors.js";
|
|
5
7
|
import { normalizeSelectorForTransport } from "./selectors.js";
|
|
6
8
|
import { createPageHandle } from "./runtime.js";
|
|
@@ -23,7 +25,7 @@ export class PageImpl extends WebLocatorBuilders {
|
|
|
23
25
|
sessionId;
|
|
24
26
|
browserSessionId;
|
|
25
27
|
async registerHook(type) {
|
|
26
|
-
if (type.name !== "newPage") {
|
|
28
|
+
if (type.name !== "newPage" && type.name !== "fileChooser" && type.name !== "download") {
|
|
27
29
|
throw formatActionError("register hook", `unsupported hook type: ${type.name}`);
|
|
28
30
|
}
|
|
29
31
|
const handle = await this.#getHandle();
|
|
@@ -31,7 +33,11 @@ export class PageImpl extends WebLocatorBuilders {
|
|
|
31
33
|
handle.stream.write({
|
|
32
34
|
surfaceSessionId: this.browserSessionId,
|
|
33
35
|
contextSessionId: this.sessionId,
|
|
34
|
-
registerHook:
|
|
36
|
+
registerHook: type.name === "newPage"
|
|
37
|
+
? { newPage: {} }
|
|
38
|
+
: type.name === "fileChooser"
|
|
39
|
+
? { fileChooser: {} }
|
|
40
|
+
: { download: {} },
|
|
35
41
|
});
|
|
36
42
|
while (true) {
|
|
37
43
|
const event = await handle.queue.next();
|
|
@@ -65,6 +71,12 @@ export class PageImpl extends WebLocatorBuilders {
|
|
|
65
71
|
if (type.name === "newPage" && event.hookCompleted.newPage?.contextSessionId) {
|
|
66
72
|
return this.#createPage(event.hookCompleted.newPage.contextSessionId);
|
|
67
73
|
}
|
|
74
|
+
if (type.name === "fileChooser" && event.hookCompleted.fileChooser?.fileChooserId) {
|
|
75
|
+
return new FileChooserImpl(this, event.hookCompleted.fileChooser.fileChooserId, event.hookCompleted.fileChooser.isMultiple ?? false);
|
|
76
|
+
}
|
|
77
|
+
if (type.name === "download" && event.hookCompleted.download?.downloadId) {
|
|
78
|
+
return new DownloadImpl(this, event.hookCompleted.download.downloadId, event.hookCompleted.download.url ?? "", event.hookCompleted.download.suggestedFilename ?? "");
|
|
79
|
+
}
|
|
68
80
|
throw formatActionError(`wait for ${type.name} hook`, "hook returned an invalid result");
|
|
69
81
|
}
|
|
70
82
|
if (event.error?.message) {
|
|
@@ -72,6 +84,125 @@ export class PageImpl extends WebLocatorBuilders {
|
|
|
72
84
|
}
|
|
73
85
|
}
|
|
74
86
|
}
|
|
87
|
+
async setFileChooserFiles(fileChooserId, files, options = {}) {
|
|
88
|
+
const handle = await this.#getHandle();
|
|
89
|
+
this.#ensureOpen(handle);
|
|
90
|
+
const fileIds = [];
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
fileIds.push(await this.#uploadLocalFile(handle, file));
|
|
93
|
+
}
|
|
94
|
+
handle.stream.write({
|
|
95
|
+
surfaceSessionId: this.browserSessionId,
|
|
96
|
+
contextSessionId: this.sessionId,
|
|
97
|
+
setFileChooserFiles: {
|
|
98
|
+
fileChooserId,
|
|
99
|
+
fileIds,
|
|
100
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
while (true) {
|
|
104
|
+
const event = await handle.queue.next();
|
|
105
|
+
if (event.fileChooserFilesSet?.fileChooserId === fileChooserId) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (event.error?.message) {
|
|
109
|
+
throw formatActionError("set file chooser files", event.error.message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async saveDownload(downloadId, path, options = {}) {
|
|
114
|
+
const handle = await this.#getHandle();
|
|
115
|
+
this.#ensureOpen(handle);
|
|
116
|
+
handle.stream.write({
|
|
117
|
+
surfaceSessionId: this.browserSessionId,
|
|
118
|
+
contextSessionId: this.sessionId,
|
|
119
|
+
saveDownload: {
|
|
120
|
+
downloadId,
|
|
121
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
while (true) {
|
|
125
|
+
const event = await handle.queue.next();
|
|
126
|
+
if (event.downloadSaved?.downloadId === downloadId && event.downloadSaved.fileId) {
|
|
127
|
+
await this.#downloadToLocalFile(handle, event.downloadSaved.fileId, path);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (event.error?.message) {
|
|
131
|
+
throw formatActionError("save download", event.error.message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async #uploadLocalFile(handle, path) {
|
|
136
|
+
const source = await open(path, "r");
|
|
137
|
+
const transferId = randomUUID();
|
|
138
|
+
const name = basename(path);
|
|
139
|
+
let offset = 0;
|
|
140
|
+
try {
|
|
141
|
+
const size = (await source.stat()).size;
|
|
142
|
+
do {
|
|
143
|
+
const buffer = Buffer.alloc(Math.min(256 * 1024, Math.max(0, size - offset)) || 1);
|
|
144
|
+
const { bytesRead } = await source.read(buffer, 0, buffer.length, offset);
|
|
145
|
+
const last = offset + bytesRead >= size;
|
|
146
|
+
handle.stream.write({
|
|
147
|
+
surfaceSessionId: this.browserSessionId,
|
|
148
|
+
contextSessionId: this.sessionId,
|
|
149
|
+
uploadFileChunk: {
|
|
150
|
+
transferId,
|
|
151
|
+
name,
|
|
152
|
+
offset: String(offset),
|
|
153
|
+
data: buffer.subarray(0, bytesRead),
|
|
154
|
+
last,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
offset += bytesRead;
|
|
158
|
+
} while (offset < size);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
await source.close();
|
|
162
|
+
}
|
|
163
|
+
while (true) {
|
|
164
|
+
const event = await handle.queue.next();
|
|
165
|
+
if (event.fileUploaded?.transferId === transferId && event.fileUploaded.fileId) {
|
|
166
|
+
return event.fileUploaded.fileId;
|
|
167
|
+
}
|
|
168
|
+
if (event.error?.message) {
|
|
169
|
+
throw formatActionError("upload file", event.error.message);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async #downloadToLocalFile(handle, fileId, path) {
|
|
174
|
+
const temporaryPath = `${path}.allwright-${randomUUID()}.tmp`;
|
|
175
|
+
const destination = await open(temporaryPath, "wx");
|
|
176
|
+
let offset = 0;
|
|
177
|
+
try {
|
|
178
|
+
while (true) {
|
|
179
|
+
handle.stream.write({
|
|
180
|
+
surfaceSessionId: this.browserSessionId,
|
|
181
|
+
contextSessionId: this.sessionId,
|
|
182
|
+
readFileChunk: { fileId, offset: String(offset), maxBytes: 256 * 1024 },
|
|
183
|
+
});
|
|
184
|
+
const event = await handle.queue.next();
|
|
185
|
+
if (event.error?.message) {
|
|
186
|
+
throw formatActionError("download file", event.error.message);
|
|
187
|
+
}
|
|
188
|
+
if (event.fileChunk?.fileId !== fileId || Number(event.fileChunk.offset ?? -1) !== offset) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const data = event.fileChunk.data ?? new Uint8Array();
|
|
192
|
+
await destination.write(data, 0, data.length, offset);
|
|
193
|
+
offset += data.length;
|
|
194
|
+
if (event.fileChunk.last)
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
await destination.close();
|
|
198
|
+
await rename(temporaryPath, path);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
await destination.close().catch(() => undefined);
|
|
202
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
75
206
|
locator(selector, options) {
|
|
76
207
|
const result = new LocatorImpl({ page: this, selector: normalizeSelectorForTransport(selector) });
|
|
77
208
|
return options ? result.filter(options) : result;
|
|
@@ -566,3 +697,38 @@ export class PageImpl extends WebLocatorBuilders {
|
|
|
566
697
|
});
|
|
567
698
|
}
|
|
568
699
|
}
|
|
700
|
+
export class FileChooserImpl {
|
|
701
|
+
page;
|
|
702
|
+
id;
|
|
703
|
+
multiple;
|
|
704
|
+
constructor(page, id, multiple) {
|
|
705
|
+
this.page = page;
|
|
706
|
+
this.id = id;
|
|
707
|
+
this.multiple = multiple;
|
|
708
|
+
}
|
|
709
|
+
isMultiple() {
|
|
710
|
+
return this.multiple;
|
|
711
|
+
}
|
|
712
|
+
async setFiles(files, options = {}) {
|
|
713
|
+
const paths = typeof files === "string" ? [files] : files;
|
|
714
|
+
if (!this.multiple && paths.length > 1) {
|
|
715
|
+
throw formatActionError("set file chooser files", "file chooser does not accept multiple files");
|
|
716
|
+
}
|
|
717
|
+
await this.page.setFileChooserFiles(this.id, paths, options);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
export class DownloadImpl {
|
|
721
|
+
page;
|
|
722
|
+
id;
|
|
723
|
+
url;
|
|
724
|
+
suggestedFilename;
|
|
725
|
+
constructor(page, id, url, suggestedFilename) {
|
|
726
|
+
this.page = page;
|
|
727
|
+
this.id = id;
|
|
728
|
+
this.url = url;
|
|
729
|
+
this.suggestedFilename = suggestedFilename;
|
|
730
|
+
}
|
|
731
|
+
async saveAs(path, options = {}) {
|
|
732
|
+
await this.page.saveDownload(this.id, path, options);
|
|
733
|
+
}
|
|
734
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -108,6 +108,19 @@ export interface Hook<T> {
|
|
|
108
108
|
readonly type: HookType<T>;
|
|
109
109
|
wait(options?: CommandOptions): Promise<T>;
|
|
110
110
|
}
|
|
111
|
+
export interface FileChooser {
|
|
112
|
+
readonly id: string;
|
|
113
|
+
readonly page: Page | MobileAndroidApp;
|
|
114
|
+
isMultiple(): boolean;
|
|
115
|
+
setFiles(files: string | string[], options?: CommandOptions): Promise<void>;
|
|
116
|
+
}
|
|
117
|
+
export interface Download {
|
|
118
|
+
readonly id: string;
|
|
119
|
+
readonly page: Page | MobileAndroidApp;
|
|
120
|
+
readonly url: string;
|
|
121
|
+
readonly suggestedFilename: string;
|
|
122
|
+
saveAs(path: string, options?: CommandOptions): Promise<void>;
|
|
123
|
+
}
|
|
111
124
|
export interface Browser extends BrowserInfo {
|
|
112
125
|
page(): Page;
|
|
113
126
|
initialPage(): Page;
|
|
@@ -179,6 +192,7 @@ export interface MobileAndroidApp {
|
|
|
179
192
|
innerText(selector: string, options?: CommandOptions): Promise<TextResult>;
|
|
180
193
|
waitForSelector(selector: string, options?: WaitForSelectorOptions): Promise<WaitForSelectorResult>;
|
|
181
194
|
screenshot(options?: ScreenshotOptions): Promise<ScreenshotResult>;
|
|
195
|
+
registerHook<T>(type: HookType<T>): Promise<Hook<T>>;
|
|
182
196
|
}
|
|
183
197
|
export interface MobileAndroidDevice {
|
|
184
198
|
readonly sessionId: string;
|
|
@@ -387,6 +401,63 @@ export interface ContextSessionEvent {
|
|
|
387
401
|
contextSessionId?: string;
|
|
388
402
|
note?: string;
|
|
389
403
|
};
|
|
404
|
+
fileChooser?: {
|
|
405
|
+
fileChooserId?: string;
|
|
406
|
+
isMultiple?: boolean;
|
|
407
|
+
note?: string;
|
|
408
|
+
};
|
|
409
|
+
download?: {
|
|
410
|
+
downloadId?: string;
|
|
411
|
+
url?: string;
|
|
412
|
+
suggestedFilename?: string;
|
|
413
|
+
note?: string;
|
|
414
|
+
};
|
|
415
|
+
mobileFileChooser?: {
|
|
416
|
+
fileChooserId?: string;
|
|
417
|
+
isMultiple?: boolean;
|
|
418
|
+
note?: string;
|
|
419
|
+
};
|
|
420
|
+
mobileDownload?: {
|
|
421
|
+
downloadId?: string;
|
|
422
|
+
suggestedFilename?: string;
|
|
423
|
+
note?: string;
|
|
424
|
+
};
|
|
425
|
+
};
|
|
426
|
+
fileChooserFilesSet?: {
|
|
427
|
+
fileChooserId?: string;
|
|
428
|
+
fileIds?: string[];
|
|
429
|
+
note?: string;
|
|
430
|
+
};
|
|
431
|
+
downloadSaved?: {
|
|
432
|
+
downloadId?: string;
|
|
433
|
+
fileId?: string;
|
|
434
|
+
suggestedFilename?: string;
|
|
435
|
+
size?: string;
|
|
436
|
+
note?: string;
|
|
437
|
+
};
|
|
438
|
+
fileUploaded?: {
|
|
439
|
+
transferId?: string;
|
|
440
|
+
fileId?: string;
|
|
441
|
+
name?: string;
|
|
442
|
+
size?: string;
|
|
443
|
+
};
|
|
444
|
+
fileChunk?: {
|
|
445
|
+
fileId?: string;
|
|
446
|
+
offset?: string;
|
|
447
|
+
data?: Uint8Array;
|
|
448
|
+
last?: boolean;
|
|
449
|
+
};
|
|
450
|
+
mobileFileChooserFilesSet?: {
|
|
451
|
+
fileChooserId?: string;
|
|
452
|
+
fileIds?: string[];
|
|
453
|
+
note?: string;
|
|
454
|
+
};
|
|
455
|
+
mobileDownloadSaved?: {
|
|
456
|
+
downloadId?: string;
|
|
457
|
+
fileId?: string;
|
|
458
|
+
suggestedFilename?: string;
|
|
459
|
+
size?: string;
|
|
460
|
+
note?: string;
|
|
390
461
|
};
|
|
391
462
|
navigated?: {
|
|
392
463
|
url?: string;
|
|
@@ -481,6 +552,72 @@ export interface RegisterHookRequest {
|
|
|
481
552
|
contextSessionId: string;
|
|
482
553
|
registerHook: {
|
|
483
554
|
newPage?: Record<string, never>;
|
|
555
|
+
fileChooser?: Record<string, never>;
|
|
556
|
+
download?: Record<string, never>;
|
|
557
|
+
mobileFileChooser?: Record<string, never>;
|
|
558
|
+
mobileDownload?: Record<string, never>;
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
export interface SaveDownloadRequest {
|
|
562
|
+
surfaceSessionId: string;
|
|
563
|
+
contextSessionId: string;
|
|
564
|
+
saveDownload: {
|
|
565
|
+
downloadId: string;
|
|
566
|
+
retryOptions?: {
|
|
567
|
+
timeoutMs?: number;
|
|
568
|
+
};
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
export interface SetFileChooserFilesRequest {
|
|
572
|
+
surfaceSessionId: string;
|
|
573
|
+
contextSessionId: string;
|
|
574
|
+
setFileChooserFiles: {
|
|
575
|
+
fileChooserId: string;
|
|
576
|
+
fileIds: string[];
|
|
577
|
+
retryOptions?: {
|
|
578
|
+
timeoutMs?: number;
|
|
579
|
+
};
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
export interface UploadFileChunkRequest {
|
|
583
|
+
surfaceSessionId: string;
|
|
584
|
+
contextSessionId: string;
|
|
585
|
+
uploadFileChunk: {
|
|
586
|
+
transferId: string;
|
|
587
|
+
name: string;
|
|
588
|
+
offset: string;
|
|
589
|
+
data: Uint8Array;
|
|
590
|
+
last: boolean;
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
export interface ReadFileChunkRequest {
|
|
594
|
+
surfaceSessionId: string;
|
|
595
|
+
contextSessionId: string;
|
|
596
|
+
readFileChunk: {
|
|
597
|
+
fileId: string;
|
|
598
|
+
offset: string;
|
|
599
|
+
maxBytes: number;
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
export interface SetMobileFileChooserFilesRequest {
|
|
603
|
+
surfaceSessionId: string;
|
|
604
|
+
contextSessionId: string;
|
|
605
|
+
setMobileFileChooserFiles: {
|
|
606
|
+
fileChooserId: string;
|
|
607
|
+
fileIds: string[];
|
|
608
|
+
retryOptions?: {
|
|
609
|
+
timeoutMs?: number;
|
|
610
|
+
};
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
export interface SaveMobileDownloadRequest {
|
|
614
|
+
surfaceSessionId: string;
|
|
615
|
+
contextSessionId: string;
|
|
616
|
+
saveMobileDownload: {
|
|
617
|
+
downloadId: string;
|
|
618
|
+
retryOptions?: {
|
|
619
|
+
timeoutMs?: number;
|
|
620
|
+
};
|
|
484
621
|
};
|
|
485
622
|
}
|
|
486
623
|
export interface WaitForHookRequest {
|
|
@@ -672,7 +809,7 @@ export interface CloseContextRequest {
|
|
|
672
809
|
close: Record<string, never>;
|
|
673
810
|
}
|
|
674
811
|
export type SurfaceSessionRequest = LaunchBrowserRequest | LaunchChromeRequest | OpenContextRequest | ConnectMobileRequest | LaunchAppRequest | SurfacePingRequest | CloseSurfaceRequest;
|
|
675
|
-
export type ContextSessionRequest = ContextPingRequest | RegisterHookRequest | WaitForHookRequest | NavigateRequest | ClickRequest | CountRequest | HighlightRequest | FocusRequest | FillRequest | HoverRequest | PressRequest | TextContentRequest | InnerTextRequest | WaitForSelectorRequest | AccessibilitySnapshotRequest | ScreenshotRequest | CloseContextRequest;
|
|
812
|
+
export type ContextSessionRequest = ContextPingRequest | RegisterHookRequest | WaitForHookRequest | SetFileChooserFilesRequest | SaveDownloadRequest | UploadFileChunkRequest | ReadFileChunkRequest | SetMobileFileChooserFilesRequest | SaveMobileDownloadRequest | NavigateRequest | ClickRequest | CountRequest | HighlightRequest | FocusRequest | FillRequest | HoverRequest | PressRequest | TextContentRequest | InnerTextRequest | WaitForSelectorRequest | AccessibilitySnapshotRequest | ScreenshotRequest | CloseContextRequest;
|
|
676
813
|
export type SurfaceSessionStream = grpc.ClientDuplexStream<SurfaceSessionRequest, SurfaceSessionEvent>;
|
|
677
814
|
export type ContextSessionStream = grpc.ClientDuplexStream<ContextSessionRequest, ContextSessionEvent>;
|
|
678
815
|
export interface EngineServiceClientShape {
|
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ option java_package = "dev.allwright.engine.v1";
|
|
|
6
6
|
option java_multiple_files = true;
|
|
7
7
|
|
|
8
8
|
import "core/v1/common.proto";
|
|
9
|
+
import "surfaces/mobile/v1/mobile.proto";
|
|
9
10
|
import "surfaces/web/v1/web.proto";
|
|
10
11
|
|
|
11
12
|
message ContextSessionCommand {
|
|
@@ -30,6 +31,12 @@ message ContextSessionCommand {
|
|
|
30
31
|
AccessibilitySnapshotCommand accessibility_snapshot = 17;
|
|
31
32
|
RegisterHookCommand register_hook = 18;
|
|
32
33
|
WaitForHookCommand wait_for_hook = 19;
|
|
34
|
+
SetFileChooserFilesCommand set_file_chooser_files = 20;
|
|
35
|
+
SaveDownloadCommand save_download = 21;
|
|
36
|
+
UploadFileChunkCommand upload_file_chunk = 22;
|
|
37
|
+
ReadFileChunkCommand read_file_chunk = 23;
|
|
38
|
+
SetMobileFileChooserFilesCommand set_mobile_file_chooser_files = 24;
|
|
39
|
+
SaveMobileDownloadCommand save_mobile_download = 25;
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
|
|
@@ -63,6 +70,12 @@ message ContextSessionEvent {
|
|
|
63
70
|
AccessibilitySnapshotCapturedEvent accessibility_snapshot_captured = 19;
|
|
64
71
|
HookRegisteredEvent hook_registered = 20;
|
|
65
72
|
HookCompletedEvent hook_completed = 21;
|
|
73
|
+
FileChooserFilesSetEvent file_chooser_files_set = 22;
|
|
74
|
+
DownloadSavedEvent download_saved = 23;
|
|
75
|
+
FileUploadedEvent file_uploaded = 24;
|
|
76
|
+
FileChunkEvent file_chunk = 25;
|
|
77
|
+
MobileFileChooserFilesSetEvent mobile_file_chooser_files_set = 26;
|
|
78
|
+
MobileDownloadSavedEvent mobile_download_saved = 27;
|
|
66
79
|
}
|
|
67
80
|
}
|
|
68
81
|
|
|
@@ -85,6 +98,10 @@ message ContextSessionErrorEvent {
|
|
|
85
98
|
message RegisterHookCommand {
|
|
86
99
|
oneof hook {
|
|
87
100
|
RegisterNewPageHook new_page = 1;
|
|
101
|
+
RegisterFileChooserHook file_chooser = 2;
|
|
102
|
+
RegisterDownloadHook download = 3;
|
|
103
|
+
RegisterMobileFileChooserHook mobile_file_chooser = 4;
|
|
104
|
+
RegisterMobileDownloadHook mobile_download = 5;
|
|
88
105
|
}
|
|
89
106
|
}
|
|
90
107
|
|
|
@@ -102,9 +119,41 @@ message HookCompletedEvent {
|
|
|
102
119
|
|
|
103
120
|
oneof result {
|
|
104
121
|
NewPageHookResult new_page = 2;
|
|
122
|
+
FileChooserHookResult file_chooser = 3;
|
|
123
|
+
DownloadHookResult download = 4;
|
|
124
|
+
MobileFileChooserHookResult mobile_file_chooser = 5;
|
|
125
|
+
MobileDownloadHookResult mobile_download = 6;
|
|
105
126
|
}
|
|
106
127
|
}
|
|
107
128
|
|
|
129
|
+
message UploadFileChunkCommand {
|
|
130
|
+
string transfer_id = 1;
|
|
131
|
+
string name = 2;
|
|
132
|
+
uint64 offset = 3;
|
|
133
|
+
bytes data = 4;
|
|
134
|
+
bool last = 5;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
message FileUploadedEvent {
|
|
138
|
+
string transfer_id = 1;
|
|
139
|
+
string file_id = 2;
|
|
140
|
+
string name = 3;
|
|
141
|
+
uint64 size = 4;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
message ReadFileChunkCommand {
|
|
145
|
+
string file_id = 1;
|
|
146
|
+
uint64 offset = 2;
|
|
147
|
+
uint32 max_bytes = 3;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
message FileChunkEvent {
|
|
151
|
+
string file_id = 1;
|
|
152
|
+
uint64 offset = 2;
|
|
153
|
+
bytes data = 3;
|
|
154
|
+
bool last = 4;
|
|
155
|
+
}
|
|
156
|
+
|
|
108
157
|
message NavigatePageCommand {
|
|
109
158
|
string url = 1;
|
|
110
159
|
optional CommandRetryOptions retry_options = 2;
|
|
@@ -56,3 +56,12 @@ message AppLaunchedEvent {
|
|
|
56
56
|
optional string activity_name = 4;
|
|
57
57
|
optional string webview_context = 5;
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
message RegisterMobileFileChooserHook {}
|
|
61
|
+
message MobileFileChooserHookResult { string file_chooser_id = 1; bool is_multiple = 2; string note = 3; }
|
|
62
|
+
message SetMobileFileChooserFilesCommand { string file_chooser_id = 1; repeated string file_ids = 2; optional CommandRetryOptions retry_options = 3; }
|
|
63
|
+
message MobileFileChooserFilesSetEvent { string file_chooser_id = 1; repeated string file_ids = 2; string note = 3; }
|
|
64
|
+
message RegisterMobileDownloadHook {}
|
|
65
|
+
message MobileDownloadHookResult { string download_id = 1; string suggested_filename = 2; string note = 3; }
|
|
66
|
+
message SaveMobileDownloadCommand { string download_id = 1; optional CommandRetryOptions retry_options = 2; }
|
|
67
|
+
message MobileDownloadSavedEvent { string download_id = 1; string file_id = 2; string suggested_filename = 3; uint64 size = 4; string note = 5; }
|
|
@@ -46,3 +46,45 @@ message NewPageHookResult {
|
|
|
46
46
|
string context_session_id = 1;
|
|
47
47
|
string note = 2;
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
message RegisterFileChooserHook {}
|
|
51
|
+
|
|
52
|
+
message FileChooserHookResult {
|
|
53
|
+
string file_chooser_id = 1;
|
|
54
|
+
bool is_multiple = 2;
|
|
55
|
+
string note = 3;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
message SetFileChooserFilesCommand {
|
|
59
|
+
string file_chooser_id = 1;
|
|
60
|
+
repeated string file_ids = 2;
|
|
61
|
+
optional CommandRetryOptions retry_options = 3;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
message FileChooserFilesSetEvent {
|
|
65
|
+
string file_chooser_id = 1;
|
|
66
|
+
repeated string file_ids = 2;
|
|
67
|
+
string note = 3;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
message RegisterDownloadHook {}
|
|
71
|
+
|
|
72
|
+
message DownloadHookResult {
|
|
73
|
+
string download_id = 1;
|
|
74
|
+
string url = 2;
|
|
75
|
+
string suggested_filename = 3;
|
|
76
|
+
string note = 4;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
message SaveDownloadCommand {
|
|
80
|
+
string download_id = 1;
|
|
81
|
+
optional CommandRetryOptions retry_options = 2;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
message DownloadSavedEvent {
|
|
85
|
+
string download_id = 1;
|
|
86
|
+
string file_id = 2;
|
|
87
|
+
string suggested_filename = 3;
|
|
88
|
+
uint64 size = 4;
|
|
89
|
+
string note = 5;
|
|
90
|
+
}
|