@allwright.dev/core 0.0.52 → 0.0.54

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 CHANGED
@@ -13,11 +13,14 @@ import { firefox } from "@allwright.dev/core";
13
13
 
14
14
  const browser = await firefox.launch();
15
15
  const page = browser.page();
16
- await page.goto("https://example.com");
16
+ await page.goto("https://themoderninternet.vercel.app");
17
+ await page.click(
18
+ "xpath=//div[contains(@class,'card')][.//h2[normalize-space()='Form Inputs']]//button[normalize-space()='Visit page']",
19
+ );
17
20
  await browser.close();
18
21
  ```
19
22
 
20
- A small runnable example also lives in [examples/basic.ts](./examples/basic.ts), and the fuller end-to-end playground lives in [examples/playground.ts](./examples/playground.ts).
23
+ Runnable examples live in [examples/web-basic.ts](./examples/web-basic.ts) and [examples/android-basic.ts](./examples/android-basic.ts).
21
24
 
22
25
  Shared config files are stack-agnostic and can live in `allwright.config.yaml` or `allwright.config.json`.
23
26
  The shared schema lives at the repo root in `allwright.schema.json`.
@@ -1,4 +1,2 @@
1
1
  export declare function ensureRuntimeReady(serverAddr: string): Promise<string>;
2
2
  export declare function shutdownManagedServer(): Promise<void>;
3
- export declare function ensurePluginsInstalled(pluginIds: string[]): Promise<void>;
4
- export declare function invokePlugin<TResponse>(pluginId: string, request: unknown): Promise<TResponse>;
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.0.52";
15
+ const DEFAULT_RELEASE_VERSION = "0.0.54";
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));
@@ -168,65 +168,6 @@ async function installCli() {
168
168
  fs.rmSync(assetPath, { force: true });
169
169
  return cliPath;
170
170
  }
171
- export async function ensurePluginsInstalled(pluginIds) {
172
- const expectedVersion = expectedRuntimeVersion();
173
- const cliPath = await ensureCliAvailable(expectedVersion);
174
- ensurePluginsInstalledWithCli(cliPath, expectedVersion, pluginIds);
175
- }
176
- export async function invokePlugin(pluginId, request) {
177
- const expectedVersion = expectedRuntimeVersion();
178
- const cliPath = await ensureCliAvailable(expectedVersion);
179
- const requestJson = JSON.stringify(request);
180
- const result = spawnSync(cliPath, ["plugin", "invoke", pluginId, "--request-json", requestJson], {
181
- encoding: "utf8",
182
- stdio: ["ignore", "pipe", "pipe"],
183
- });
184
- if (result.error) {
185
- throw new Error(`failed to invoke allwright ${pluginId} plugin with ${cliPath}: ${result.error.message}`);
186
- }
187
- if (result.status !== 0) {
188
- const details = [result.stdout, result.stderr]
189
- .map((value) => value?.trim())
190
- .filter((value) => !!value)
191
- .join("\n");
192
- throw new Error(details
193
- ? `allwright ${pluginId} plugin invocation failed:\n${details}`
194
- : `allwright ${pluginId} plugin invocation failed`);
195
- }
196
- try {
197
- return JSON.parse(result.stdout);
198
- }
199
- catch (error) {
200
- throw new Error(`failed to decode allwright ${pluginId} plugin response: ${error instanceof Error ? error.message : String(error)}`);
201
- }
202
- }
203
- function ensurePluginsInstalledWithCli(cliPath, expectedVersion, pluginIds) {
204
- for (const pluginId of new Set(pluginIds.map((value) => value.trim()).filter(Boolean))) {
205
- const pluginPath = path.join(allwrightHome(), "plugins", pluginId, "lib", pluginLibraryFilename(pluginId));
206
- if (isFile(pluginPath) && installedPluginVersion(pluginId) === expectedVersion) {
207
- continue;
208
- }
209
- const result = spawnSync(cliPath, ["plugin", "install", pluginId, "--version", expectedVersion], {
210
- encoding: "utf8",
211
- stdio: ["ignore", "pipe", "pipe"],
212
- });
213
- if (result.error) {
214
- throw new Error(`failed to install allwright ${pluginId} plugin with ${cliPath}: ${result.error.message}`);
215
- }
216
- if (result.status !== 0 || !isFile(pluginPath)) {
217
- const details = [result.stdout, result.stderr]
218
- .map((value) => value?.trim())
219
- .filter((value) => !!value)
220
- .join("\n");
221
- throw new Error(details
222
- ? `allwright attempted to install the \`${pluginId}\` plugin automatically, but the install did not complete successfully:\n${details}`
223
- : `allwright attempted to install the \`${pluginId}\` plugin automatically, but the install did not complete successfully`);
224
- }
225
- if (installedPluginVersion(pluginId) !== expectedVersion) {
226
- throw new Error(`allwright attempted to install the \`${pluginId}\` plugin automatically, but version ${expectedVersion} is still not active`);
227
- }
228
- }
229
- }
230
171
  async function resolveReleaseTag() {
231
172
  const version = process.env[ALLWRIGHT_VERSION_ENV_VAR]?.trim() || DEFAULT_RELEASE_VERSION;
232
173
  if (version !== "latest") {
@@ -325,23 +266,6 @@ function isLocalServerAddr(serverAddr) {
325
266
  const host = parseServerHost(serverAddr);
326
267
  return host === "127.0.0.1" || host === "localhost" || host === "::1";
327
268
  }
328
- function installedPluginVersion(pluginId) {
329
- const manifestPath = path.join(allwrightHome(), "plugins.txt");
330
- if (!isFile(manifestPath)) {
331
- return null;
332
- }
333
- for (const line of fs.readFileSync(manifestPath, "utf8").split(/\r?\n/)) {
334
- const trimmed = line.trim();
335
- if (!trimmed || trimmed.startsWith("#")) {
336
- continue;
337
- }
338
- const [id, , version] = trimmed.split("\t", 3);
339
- if (id === pluginId && version) {
340
- return normalizeReleaseVersion(version);
341
- }
342
- }
343
- return null;
344
- }
345
269
  async function allocateManagedServerAddr(serverAddr) {
346
270
  const host = localBindingHost(serverAddr);
347
271
  const port = await new Promise((resolve, reject) => {
@@ -454,26 +378,6 @@ function findExtractedCli(extractRoot) {
454
378
  }
455
379
  return null;
456
380
  }
457
- function pluginLibraryFilename(pluginId) {
458
- const stem = pluginLibraryStem(pluginId);
459
- if (process.platform === "darwin") {
460
- return `lib${stem}.dylib`;
461
- }
462
- if (process.platform === "win32") {
463
- return `${stem}.dll`;
464
- }
465
- return `lib${stem}.so`;
466
- }
467
- function pluginLibraryStem(pluginId) {
468
- switch (pluginId) {
469
- case "web":
470
- return "allwright_surface_web";
471
- case "mobile-android":
472
- return "allwright_surface_mobile_android";
473
- default:
474
- throw new Error(`automatic install is not supported for allwright plugin \`${pluginId}\``);
475
- }
476
- }
477
381
  function autoInstallEnabled() {
478
382
  const raw = process.env[ALLWRIGHT_AUTO_INSTALL_ENV_VAR]?.trim().toLowerCase();
479
383
  return raw !== "0" && raw !== "false" && raw !== "no";
package/dist/browser.js CHANGED
@@ -33,7 +33,7 @@ export class BrowserImpl {
33
33
  this.launchNote = browserInfo.launchNote;
34
34
  this.cdpWebSocketURL = browserInfo.cdpWebSocketURL;
35
35
  this.userDataDir = browserInfo.userDataDir;
36
- this.#initialPage = this.#createPage(state.launched.initialTabSessionId ?? "");
36
+ this.#initialPage = this.#createPage(state.launched.initialPageSessionId ?? "");
37
37
  }
38
38
  sessionId;
39
39
  browserName;
@@ -52,17 +52,17 @@ export class BrowserImpl {
52
52
  async newPage(options = {}) {
53
53
  this.#ensureOpen();
54
54
  this.#stream.write({
55
- openTab: {
55
+ openContext: {
56
56
  retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
57
57
  },
58
58
  });
59
59
  while (true) {
60
60
  const event = await this.#queue.next();
61
- if (event.tabOpened?.tabSessionId) {
62
- return this.#createPage(event.tabOpened.tabSessionId);
61
+ if (event.contextOpened?.contextSessionId) {
62
+ return this.#createPage(event.contextOpened.contextSessionId);
63
63
  }
64
64
  if (event.error?.message) {
65
- throw formatActionError("open tab", event.error.message);
65
+ throw formatActionError("open page", event.error.message);
66
66
  }
67
67
  }
68
68
  }
@@ -77,6 +77,9 @@ export class BrowserImpl {
77
77
  const event = await this.#queue.next();
78
78
  if (event.closed) {
79
79
  this.#closed = true;
80
+ for (const page of this.#pages.values()) {
81
+ page.dispose();
82
+ }
80
83
  this.#stream.end();
81
84
  return;
82
85
  }
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ import { PageImpl } from "./page.js";
5
5
  import { setServerAddr, shutdown } from "./runtime.js";
6
6
  import type { Browser, BrowserKind, BrowserType, LaunchOptions, Page, ResolveConfigOptions, ResolvedAllwrightConfig } from "./types.js";
7
7
  export { findConfigFile, loadConfigFile, resolveConfig, setServerAddr, shutdown };
8
- export type { AllwrightConfig, Browser, BrowserInfo, BrowserKind, BrowserType, ClickResult, CommandOptions, CountResult, ElementResult, FillResult, HighlightOptions, HighlightResult, LaunchOptions, MobileAndroidConnectOptions, MobileAndroidDevice, MobileAndroidLaunchOptions, MobileAndroidLocator, MobileAndroidPage, Locator, LocatorInfo, MobileSurfaceNamespace, NavigateResult, Page, PageInfo, PressOptions, PressResult, ResolveConfigOptions, ResolvedAllwrightConfig, RetryConfig, TextResult, WaitForSelectorOptions, WaitForSelectorResult, } from "./types.js";
8
+ export type { AllwrightConfig, Browser, BrowserInfo, BrowserKind, BrowserType, ClickResult, CommandOptions, CountResult, ElementResult, FillResult, HighlightOptions, HighlightResult, LaunchOptions, MobileAndroidConnectOptions, MobileAndroidDevice, MobileAndroidLaunchOptions, MobileAndroidLocator, MobileAndroidApp, Locator, LocatorInfo, MobileSurfaceNamespace, NavigateResult, Page, PageInfo, PressOptions, PressResult, ResolveConfigOptions, ResolvedAllwrightConfig, RetryConfig, ScreenshotResult, TextResult, WaitForSelectorOptions, WaitForSelectorResult, } from "./types.js";
9
9
  export declare const chromium: BrowserType;
10
10
  export declare const firefox: BrowserType;
11
11
  export { mobile };
package/dist/mobile.js CHANGED
@@ -1,59 +1,117 @@
1
- import { invokePlugin } from "./bootstrap.js";
1
+ import { createBrowserSessionHandle, createPageHandle, getRuntime } from "./runtime.js";
2
2
  import { chainMobileSelectorForTransport, normalizeMobileSelectorForTransport } from "./mobileSelectors.js";
3
- function timeoutMsOf(options) {
4
- return options?.timeoutMs;
3
+ function retryOptions(timeoutMs) {
4
+ return timeoutMs ? { timeoutMs } : undefined;
5
5
  }
6
- async function invokeAndroidExpected(commandName, request) {
7
- const envelope = await invokePlugin("mobile-android", request);
8
- if (!envelope.ok) {
9
- throw new Error(envelope.error ?? `mobile-android plugin ${commandName} failed`);
10
- }
11
- if (envelope.result === undefined) {
12
- throw new Error(`mobile-android plugin ${commandName} returned no result`);
13
- }
14
- return envelope.result;
15
- }
16
- class MobileAndroidPageImpl {
17
- browserSession;
18
- pageSession;
19
- constructor(browserSession, pageSession) {
20
- this.browserSession = browserSession;
21
- this.pageSession = pageSession;
22
- }
23
- get sessionId() {
24
- return this.pageSession.page_id;
6
+ class MobileAndroidAppImpl {
7
+ sessionId;
8
+ #runtime;
9
+ #surfaceSessionId;
10
+ #handlePromise = null;
11
+ constructor(runtime, surfaceSessionId, sessionId) {
12
+ this.sessionId = sessionId;
13
+ this.#runtime = runtime;
14
+ this.#surfaceSessionId = surfaceSessionId;
25
15
  }
26
16
  locator(selector) {
27
17
  return new MobileAndroidLocatorImpl(this, normalizeMobileSelectorForTransport(selector));
28
18
  }
29
19
  async click(selector, options = {}) {
30
- const result = await invokeAndroidExpected("click", {
31
- command: "click_element",
32
- browser_session: this.browserSession,
33
- page_session: this.pageSession,
34
- selector: normalizeMobileSelectorForTransport(selector),
35
- timeout_ms: timeoutMsOf(options),
20
+ const handle = await this.#getHandle();
21
+ this.#ensureOpen(handle);
22
+ handle.stream.write({
23
+ surfaceSessionId: this.#surfaceSessionId,
24
+ contextSessionId: this.sessionId,
25
+ clickElement: {
26
+ cssSelector: normalizeMobileSelectorForTransport(selector),
27
+ retryOptions: retryOptions(options.timeoutMs),
28
+ },
36
29
  });
37
- return {
38
- selector: result.selector,
39
- note: result.note,
40
- bidiSessionId: result.session_id,
41
- };
30
+ while (true) {
31
+ const event = await handle.queue.next();
32
+ if (event.elementClicked) {
33
+ return {
34
+ selector: event.elementClicked.cssSelector ?? "",
35
+ note: event.elementClicked.note ?? "",
36
+ bidiSessionId: event.elementClicked.bidiSessionId ?? "",
37
+ };
38
+ }
39
+ if (event.error?.message) {
40
+ throw new Error(event.error.message);
41
+ }
42
+ if (event.closed) {
43
+ handle.closed = true;
44
+ throw new Error(`android app session ${this.sessionId} closed while clicking`);
45
+ }
46
+ }
42
47
  }
43
48
  async fill(selector, value, options = {}) {
44
- const result = await invokeAndroidExpected("fill", {
45
- command: "fill_element",
46
- browser_session: this.browserSession,
47
- page_session: this.pageSession,
48
- selector: normalizeMobileSelectorForTransport(selector),
49
- value,
50
- timeout_ms: timeoutMsOf(options),
49
+ const handle = await this.#getHandle();
50
+ this.#ensureOpen(handle);
51
+ handle.stream.write({
52
+ surfaceSessionId: this.#surfaceSessionId,
53
+ contextSessionId: this.sessionId,
54
+ fillElement: {
55
+ cssSelector: normalizeMobileSelectorForTransport(selector),
56
+ value,
57
+ retryOptions: retryOptions(options.timeoutMs),
58
+ },
51
59
  });
52
- return {
53
- selector: result.selector,
54
- value: result.value,
55
- note: result.note,
56
- };
60
+ while (true) {
61
+ const event = await handle.queue.next();
62
+ if (event.elementFilled) {
63
+ return {
64
+ selector: event.elementFilled.cssSelector ?? "",
65
+ value: event.elementFilled.value ?? "",
66
+ note: event.elementFilled.note ?? "",
67
+ };
68
+ }
69
+ if (event.error?.message) {
70
+ throw new Error(event.error.message);
71
+ }
72
+ if (event.closed) {
73
+ handle.closed = true;
74
+ throw new Error(`android app session ${this.sessionId} closed while filling`);
75
+ }
76
+ }
77
+ }
78
+ async screenshot(options = {}) {
79
+ const handle = await this.#getHandle();
80
+ this.#ensureOpen(handle);
81
+ handle.stream.write({
82
+ surfaceSessionId: this.#surfaceSessionId,
83
+ contextSessionId: this.sessionId,
84
+ screenshot: {
85
+ retryOptions: retryOptions(options.timeoutMs),
86
+ },
87
+ });
88
+ while (true) {
89
+ const event = await handle.queue.next();
90
+ if (event.screenshotCaptured?.pngData) {
91
+ return {
92
+ pngData: event.screenshotCaptured.pngData,
93
+ note: event.screenshotCaptured.note ?? "",
94
+ };
95
+ }
96
+ if (event.error?.message) {
97
+ throw new Error(event.error.message);
98
+ }
99
+ if (event.closed) {
100
+ handle.closed = true;
101
+ throw new Error(`android app session ${this.sessionId} closed while capturing screenshot`);
102
+ }
103
+ }
104
+ }
105
+ async #getHandle() {
106
+ if (!this.#handlePromise) {
107
+ this.#handlePromise = createPageHandle(this.#runtime);
108
+ }
109
+ return this.#handlePromise;
110
+ }
111
+ #ensureOpen(handle) {
112
+ if (handle.closed) {
113
+ throw new Error(`android app session ${this.sessionId} is closed`);
114
+ }
57
115
  }
58
116
  }
59
117
  class MobileAndroidLocatorImpl {
@@ -74,48 +132,81 @@ class MobileAndroidLocatorImpl {
74
132
  }
75
133
  }
76
134
  class MobileAndroidDeviceImpl {
77
- connectInfoRaw;
78
- #initialPage;
79
- constructor(connectInfoRaw) {
80
- this.connectInfoRaw = connectInfoRaw;
81
- this.#initialPage = new MobileAndroidPageImpl(this.connectInfoRaw.browser_session, this.connectInfoRaw.initial_page.page_session);
82
- }
83
- get sessionId() {
84
- return this.connectInfoRaw.browser_session.automation.session_id;
85
- }
86
- page() {
87
- return this.#initialPage;
88
- }
89
- initialPage() {
90
- return this.#initialPage;
135
+ sessionId;
136
+ surfaceSessionId;
137
+ runtime;
138
+ #stream;
139
+ #queue;
140
+ #closed = false;
141
+ #currentApp;
142
+ constructor(sessionId, surfaceSessionId, runtime, stream, queue, initialAppSessionId) {
143
+ this.sessionId = sessionId;
144
+ this.surfaceSessionId = surfaceSessionId;
145
+ this.runtime = runtime;
146
+ this.#stream = stream;
147
+ this.#queue = queue;
148
+ this.#currentApp = new MobileAndroidAppImpl(runtime, surfaceSessionId, initialAppSessionId);
149
+ }
150
+ app() {
151
+ return this.#currentApp;
152
+ }
153
+ initialApp() {
154
+ return this.#currentApp;
91
155
  }
92
156
  async launch(options = {}) {
93
- const page = await invokeAndroidExpected("launch", {
94
- command: "launch_app",
95
- browser_session: this.connectInfoRaw.browser_session,
96
- options: {
97
- apk_path: options.apkPath,
98
- app_id: options.appId,
99
- launch_activity: options.launchActivity,
100
- stop_before_launch: options.stopBeforeLaunch ?? false,
101
- timeout_ms: timeoutMsOf(options),
157
+ this.#ensureOpen();
158
+ this.#stream.write({
159
+ launchApp: {
160
+ apkPath: options.apkPath,
161
+ appId: options.appId,
162
+ launchActivity: options.launchActivity,
163
+ stopBeforeLaunch: options.stopBeforeLaunch ?? false,
164
+ retryOptions: retryOptions(options.timeoutMs),
102
165
  },
103
166
  });
104
- this.#initialPage = new MobileAndroidPageImpl(this.connectInfoRaw.browser_session, page.page_session);
105
- return this.#initialPage;
167
+ while (true) {
168
+ const event = await this.#queue.next();
169
+ if (event.appLaunched?.appSessionId) {
170
+ this.#currentApp = new MobileAndroidAppImpl(this.runtime, this.surfaceSessionId, event.appLaunched.appSessionId);
171
+ return this.#currentApp;
172
+ }
173
+ if (event.error?.message) {
174
+ throw new Error(event.error.message);
175
+ }
176
+ if (event.closed) {
177
+ this.#closed = true;
178
+ throw new Error(`android device session ${this.sessionId} closed while launching app`);
179
+ }
180
+ }
181
+ }
182
+ #ensureOpen() {
183
+ if (this.#closed) {
184
+ throw new Error(`android device session ${this.sessionId} is closed`);
185
+ }
106
186
  }
107
187
  }
108
188
  class MobileAndroidSurfaceImpl {
109
189
  async connect(options = {}) {
110
- const connectInfo = await invokeAndroidExpected("connect", {
111
- command: "connect",
112
- platform: "android",
113
- device: options.device,
114
- adb_endpoint: options.adbEndpoint,
115
- preserve_app_state: options.preserveAppState ?? false,
116
- timeout_ms: timeoutMsOf(options),
190
+ const runtime = await getRuntime();
191
+ const { stream, queue } = await createBrowserSessionHandle(runtime);
192
+ stream.write({
193
+ connectMobile: {
194
+ platform: 1,
195
+ device: options.device,
196
+ adbEndpoint: options.adbEndpoint,
197
+ preserveAppState: options.preserveAppState ?? false,
198
+ retryOptions: retryOptions(options.timeoutMs),
199
+ },
117
200
  });
118
- return new MobileAndroidDeviceImpl(connectInfo);
201
+ while (true) {
202
+ const event = await queue.next();
203
+ if (event.mobileConnected?.initialAppSessionId) {
204
+ return new MobileAndroidDeviceImpl(event.mobileConnected.deviceSessionId ?? event.sessionId ?? "", event.sessionId ?? "", runtime, stream, queue, event.mobileConnected.initialAppSessionId);
205
+ }
206
+ if (event.error?.message) {
207
+ throw new Error(event.error.message);
208
+ }
209
+ }
119
210
  }
120
211
  }
121
212
  export const mobile = {
package/dist/page.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ClickResult, CommandOptions, CountResult, ElementResult, FillResult, HighlightOptions, HighlightResult, Locator, NavigateResult, Page, PageInfo, PressOptions, PressResult, RuntimeClient, TextResult, WaitForSelectorOptions, WaitForSelectorResult } from "./types.js";
1
+ import type { ClickResult, CommandOptions, CountResult, ElementResult, FillResult, HighlightOptions, HighlightResult, Locator, NavigateResult, Page, PageInfo, PressOptions, PressResult, RuntimeClient, ScreenshotResult, TextResult, WaitForSelectorOptions, WaitForSelectorResult } from "./types.js";
2
2
  export declare class PageImpl implements Page {
3
3
  #private;
4
4
  constructor(input: PageInfo & {
@@ -18,8 +18,10 @@ export declare class PageImpl implements Page {
18
18
  textContent(selector: string, options?: CommandOptions): Promise<TextResult>;
19
19
  innerText(selector: string, options?: CommandOptions): Promise<TextResult>;
20
20
  waitForSelector(selector: string, options?: WaitForSelectorOptions): Promise<WaitForSelectorResult>;
21
+ screenshot(options?: CommandOptions): Promise<ScreenshotResult>;
21
22
  close(): Promise<void>;
22
23
  ping(message?: string): Promise<string>;
23
24
  pageInfo(): PageInfo;
24
25
  navigate(url: string, options?: CommandOptions): Promise<NavigateResult>;
26
+ dispose(): void;
25
27
  }