@allwright.dev/core 0.0.34 → 0.0.36
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/dist/bootstrap.d.ts +2 -0
- package/dist/bootstrap.js +240 -0
- package/dist/browser.js +5 -4
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +120 -0
- package/dist/index.js +2 -1
- package/dist/page.js +14 -13
- package/dist/runtime.js +10 -4
- package/dist/selectors.js +87 -2
- package/dist/types.d.ts +1 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import grpc from "@grpc/grpc-js";
|
|
7
|
+
import protoLoader from "@grpc/proto-loader";
|
|
8
|
+
const ALLWRIGHT_AUTO_INSTALL_ENV_VAR = "ALLWRIGHT_AUTO_INSTALL";
|
|
9
|
+
const ALLWRIGHT_CLI_PATH_ENV_VAR = "ALLWRIGHT_CLI_PATH";
|
|
10
|
+
const ALLWRIGHT_HOME_ENV_VAR = "ALLWRIGHT_HOME";
|
|
11
|
+
const ALLWRIGHT_REPOSITORY_ENV_VAR = "ALLWRIGHT_REPOSITORY";
|
|
12
|
+
const ALLWRIGHT_VERSION_ENV_VAR = "ALLWRIGHT_VERSION";
|
|
13
|
+
const DEFAULT_RELEASE_REPOSITORY = "allwright-dev/allwright";
|
|
14
|
+
const DEFAULT_RELEASE_VERSION = "0.0.1";
|
|
15
|
+
const STARTUP_TIMEOUT_MS = 20_000;
|
|
16
|
+
const PING_TIMEOUT_MS = 1_000;
|
|
17
|
+
const PROTO_ROOT = fileURLToPath(new URL("../proto/", import.meta.url));
|
|
18
|
+
const ENGINE_PROTO_PATH = fileURLToPath(new URL("../proto/engine/v1/engine.proto", import.meta.url));
|
|
19
|
+
let managedServer = null;
|
|
20
|
+
let managedServerAddr = null;
|
|
21
|
+
export async function ensureRuntimeReady(serverAddr) {
|
|
22
|
+
if (await pingServer(serverAddr)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!isLocalServerAddr(serverAddr)) {
|
|
26
|
+
throw new Error(`allwright could not reach engine server at ${serverAddr}. Automatic startup is only supported for local addresses.`);
|
|
27
|
+
}
|
|
28
|
+
if (managedServer && !managedServer.killed && managedServer.exitCode === null && managedServerAddr === serverAddr) {
|
|
29
|
+
await waitForServer(serverAddr);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const cliPath = await ensureCliAvailable();
|
|
33
|
+
ensureWebPlugin(cliPath);
|
|
34
|
+
managedServer = spawn(cliPath, ["serve", "--listen-addr", cliListenAddr(serverAddr)], {
|
|
35
|
+
stdio: "ignore",
|
|
36
|
+
});
|
|
37
|
+
managedServerAddr = serverAddr;
|
|
38
|
+
await waitForServer(serverAddr);
|
|
39
|
+
}
|
|
40
|
+
export async function shutdownManagedServer() {
|
|
41
|
+
if (managedServer && managedServer.exitCode === null && !managedServer.killed) {
|
|
42
|
+
managedServer.kill("SIGTERM");
|
|
43
|
+
}
|
|
44
|
+
managedServer = null;
|
|
45
|
+
managedServerAddr = null;
|
|
46
|
+
}
|
|
47
|
+
async function waitForServer(serverAddr) {
|
|
48
|
+
const deadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
49
|
+
while (Date.now() < deadline) {
|
|
50
|
+
if (await pingServer(serverAddr)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
54
|
+
}
|
|
55
|
+
await shutdownManagedServer();
|
|
56
|
+
throw new Error(`timed out waiting for allwright server at ${serverAddr} to become ready`);
|
|
57
|
+
}
|
|
58
|
+
async function pingServer(serverAddr) {
|
|
59
|
+
const loaded = protoLoader.loadSync(ENGINE_PROTO_PATH, {
|
|
60
|
+
includeDirs: [PROTO_ROOT],
|
|
61
|
+
keepCase: false,
|
|
62
|
+
longs: String,
|
|
63
|
+
enums: String,
|
|
64
|
+
defaults: true,
|
|
65
|
+
oneofs: true,
|
|
66
|
+
});
|
|
67
|
+
const proto = grpc.loadPackageDefinition(loaded);
|
|
68
|
+
const client = new proto.allwright.engine.v1.EngineService(serverAddr, grpc.credentials.createInsecure());
|
|
69
|
+
return await new Promise((resolve) => {
|
|
70
|
+
client.Ping({}, new grpc.Metadata(), { deadline: new Date(Date.now() + PING_TIMEOUT_MS) }, (error) => {
|
|
71
|
+
client.close();
|
|
72
|
+
resolve(!error);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async function ensureCliAvailable() {
|
|
77
|
+
const envPath = process.env[ALLWRIGHT_CLI_PATH_ENV_VAR]?.trim();
|
|
78
|
+
if (envPath && isFile(envPath)) {
|
|
79
|
+
return envPath;
|
|
80
|
+
}
|
|
81
|
+
const bundled = path.join(allwrightHome(), "bin", cliFilename());
|
|
82
|
+
if (isFile(bundled)) {
|
|
83
|
+
return bundled;
|
|
84
|
+
}
|
|
85
|
+
const fromPath = resolveFromPath(cliFilename());
|
|
86
|
+
if (fromPath) {
|
|
87
|
+
return fromPath;
|
|
88
|
+
}
|
|
89
|
+
if (!autoInstallEnabled()) {
|
|
90
|
+
throw new Error("allwright CLI was not found. Install it first or set ALLWRIGHT_CLI_PATH.");
|
|
91
|
+
}
|
|
92
|
+
return await installCli();
|
|
93
|
+
}
|
|
94
|
+
async function installCli() {
|
|
95
|
+
const installDir = path.join(allwrightHome(), "bin");
|
|
96
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
97
|
+
const cliPath = path.join(installDir, cliFilename());
|
|
98
|
+
const versionTag = await resolveReleaseTag();
|
|
99
|
+
const assetName = cliAssetName(versionTag);
|
|
100
|
+
const assetPath = path.join(os.tmpdir(), assetName);
|
|
101
|
+
const response = await fetch(`https://github.com/${releaseRepository()}/releases/download/${versionTag}/${assetName}`, { headers: { "user-agent": `allwright-ts/${DEFAULT_RELEASE_VERSION}` } });
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error(`failed to download allwright CLI asset ${assetName}: ${response.status} ${response.statusText}`);
|
|
104
|
+
}
|
|
105
|
+
fs.writeFileSync(assetPath, Buffer.from(await response.arrayBuffer()));
|
|
106
|
+
extractCliArchive(assetPath, cliPath);
|
|
107
|
+
fs.chmodSync(cliPath, 0o755);
|
|
108
|
+
fs.rmSync(assetPath, { force: true });
|
|
109
|
+
return cliPath;
|
|
110
|
+
}
|
|
111
|
+
function ensureWebPlugin(cliPath) {
|
|
112
|
+
const pluginPath = path.join(allwrightHome(), "plugins", "web", "lib", webPluginFilename());
|
|
113
|
+
if (isFile(pluginPath)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const version = process.env[ALLWRIGHT_VERSION_ENV_VAR]?.trim() || DEFAULT_RELEASE_VERSION;
|
|
117
|
+
const result = spawnSync(cliPath, ["plugin", "install", "web", "--version", normalizeReleaseVersion(version)], {
|
|
118
|
+
stdio: "ignore",
|
|
119
|
+
});
|
|
120
|
+
if (result.status !== 0 || !isFile(pluginPath)) {
|
|
121
|
+
throw new Error("allwright attempted to install the `web` plugin automatically, but the install did not complete successfully");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function resolveReleaseTag() {
|
|
125
|
+
const version = process.env[ALLWRIGHT_VERSION_ENV_VAR]?.trim() || DEFAULT_RELEASE_VERSION;
|
|
126
|
+
if (version !== "latest") {
|
|
127
|
+
return normalizeReleaseTag(version);
|
|
128
|
+
}
|
|
129
|
+
const response = await fetch(`https://api.github.com/repos/${releaseRepository()}/releases/latest`, {
|
|
130
|
+
headers: { "user-agent": `allwright-ts/${DEFAULT_RELEASE_VERSION}` },
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
throw new Error(`failed to resolve latest allwright release: ${response.status} ${response.statusText}`);
|
|
134
|
+
}
|
|
135
|
+
const payload = (await response.json());
|
|
136
|
+
if (!payload.tag_name?.trim()) {
|
|
137
|
+
throw new Error("latest allwright release metadata did not include tag_name");
|
|
138
|
+
}
|
|
139
|
+
return payload.tag_name;
|
|
140
|
+
}
|
|
141
|
+
function extractCliArchive(archivePath, cliPath) {
|
|
142
|
+
if (archivePath.endsWith(".zip")) {
|
|
143
|
+
const result = spawnSync("powershell", [
|
|
144
|
+
"-NoProfile",
|
|
145
|
+
"-Command",
|
|
146
|
+
`Expand-Archive -Path '${archivePath.replaceAll("'", "''")}' -DestinationPath '${path.dirname(cliPath).replaceAll("'", "''")}' -Force`,
|
|
147
|
+
], { stdio: "ignore" });
|
|
148
|
+
if (result.status !== 0) {
|
|
149
|
+
throw new Error("failed to extract allwright CLI zip archive");
|
|
150
|
+
}
|
|
151
|
+
const extracted = path.join(path.dirname(cliPath), "bin", cliFilename());
|
|
152
|
+
fs.copyFileSync(extracted, cliPath);
|
|
153
|
+
fs.rmSync(path.join(path.dirname(cliPath), "bin"), { recursive: true, force: true });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const result = spawnSync("tar", [
|
|
157
|
+
"-xzf",
|
|
158
|
+
archivePath,
|
|
159
|
+
"-C",
|
|
160
|
+
path.dirname(cliPath),
|
|
161
|
+
`bin/${cliFilename()}`,
|
|
162
|
+
], { stdio: "ignore" });
|
|
163
|
+
if (result.status !== 0) {
|
|
164
|
+
throw new Error("failed to extract allwright CLI tar archive");
|
|
165
|
+
}
|
|
166
|
+
const extracted = path.join(path.dirname(cliPath), "bin", cliFilename());
|
|
167
|
+
fs.copyFileSync(extracted, cliPath);
|
|
168
|
+
fs.rmSync(path.join(path.dirname(cliPath), "bin"), { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
function cliAssetName(versionTag) {
|
|
171
|
+
const targets = new Map([
|
|
172
|
+
["darwin/arm64", "aarch64-apple-darwin"],
|
|
173
|
+
["darwin/x64", "x86_64-apple-darwin"],
|
|
174
|
+
["linux/arm64", "aarch64-unknown-linux-gnu"],
|
|
175
|
+
["linux/x64", "x86_64-unknown-linux-gnu"],
|
|
176
|
+
["win32/arm64", "aarch64-pc-windows-msvc"],
|
|
177
|
+
["win32/x64", "x86_64-pc-windows-msvc"],
|
|
178
|
+
]);
|
|
179
|
+
const target = targets.get(`${process.platform}/${process.arch}`);
|
|
180
|
+
if (!target) {
|
|
181
|
+
throw new Error(`automatic allwright CLI install is not supported on ${process.platform}/${process.arch}`);
|
|
182
|
+
}
|
|
183
|
+
const extension = process.platform === "win32" ? "zip" : "tar.gz";
|
|
184
|
+
return `allwright-${versionTag}-${target}.${extension}`;
|
|
185
|
+
}
|
|
186
|
+
function normalizeReleaseTag(version) {
|
|
187
|
+
return version.startsWith("v") ? version : `v${version}`;
|
|
188
|
+
}
|
|
189
|
+
function normalizeReleaseVersion(version) {
|
|
190
|
+
return version.replace(/^v/, "");
|
|
191
|
+
}
|
|
192
|
+
function cliListenAddr(serverAddr) {
|
|
193
|
+
return serverAddr.replace(/^https?:\/\//, "");
|
|
194
|
+
}
|
|
195
|
+
function isLocalServerAddr(serverAddr) {
|
|
196
|
+
const host = cliListenAddr(serverAddr).split(":")[0]?.replace(/^\[|\]$/g, "") ?? "";
|
|
197
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
198
|
+
}
|
|
199
|
+
function allwrightHome() {
|
|
200
|
+
return process.env[ALLWRIGHT_HOME_ENV_VAR]?.trim() || path.join(os.homedir(), ".allwright");
|
|
201
|
+
}
|
|
202
|
+
function cliFilename() {
|
|
203
|
+
return process.platform === "win32" ? "allwright.exe" : "allwright";
|
|
204
|
+
}
|
|
205
|
+
function webPluginFilename() {
|
|
206
|
+
if (process.platform === "darwin") {
|
|
207
|
+
return "liballwright_surface_web.dylib";
|
|
208
|
+
}
|
|
209
|
+
if (process.platform === "win32") {
|
|
210
|
+
return "allwright_surface_web.dll";
|
|
211
|
+
}
|
|
212
|
+
return "liballwright_surface_web.so";
|
|
213
|
+
}
|
|
214
|
+
function autoInstallEnabled() {
|
|
215
|
+
const raw = process.env[ALLWRIGHT_AUTO_INSTALL_ENV_VAR]?.trim().toLowerCase();
|
|
216
|
+
return raw !== "0" && raw !== "false" && raw !== "no";
|
|
217
|
+
}
|
|
218
|
+
function resolveFromPath(filename) {
|
|
219
|
+
for (const entry of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
220
|
+
if (!entry) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const candidate = path.join(entry, filename);
|
|
224
|
+
if (isFile(candidate)) {
|
|
225
|
+
return candidate;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
function releaseRepository() {
|
|
231
|
+
return process.env[ALLWRIGHT_REPOSITORY_ENV_VAR]?.trim() || DEFAULT_RELEASE_REPOSITORY;
|
|
232
|
+
}
|
|
233
|
+
function isFile(candidate) {
|
|
234
|
+
try {
|
|
235
|
+
return fs.statSync(candidate).isFile();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
package/dist/browser.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PageImpl } from "./page.js";
|
|
2
|
+
import { formatActionError } from "./errors.js";
|
|
2
3
|
export class BrowserTypeImpl {
|
|
3
4
|
#browserKind;
|
|
4
5
|
constructor(browserKind = "chromium") {
|
|
@@ -61,7 +62,7 @@ export class BrowserImpl {
|
|
|
61
62
|
return this.#createPage(event.tabOpened.tabSessionId);
|
|
62
63
|
}
|
|
63
64
|
if (event.error?.message) {
|
|
64
|
-
throw
|
|
65
|
+
throw formatActionError("open tab", event.error.message);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
}
|
|
@@ -80,7 +81,7 @@ export class BrowserImpl {
|
|
|
80
81
|
return;
|
|
81
82
|
}
|
|
82
83
|
if (event.error?.message) {
|
|
83
|
-
throw
|
|
84
|
+
throw formatActionError("close browser", event.error.message);
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
}
|
|
@@ -97,7 +98,7 @@ export class BrowserImpl {
|
|
|
97
98
|
return event.pong.message;
|
|
98
99
|
}
|
|
99
100
|
if (event.error?.message) {
|
|
100
|
-
throw
|
|
101
|
+
throw formatActionError("ping browser", event.error.message);
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
}
|
|
@@ -131,7 +132,7 @@ export class BrowserImpl {
|
|
|
131
132
|
}
|
|
132
133
|
#ensureOpen() {
|
|
133
134
|
if (this.#closed) {
|
|
134
|
-
throw
|
|
135
|
+
throw formatActionError("use browser", `browser session ${this.sessionId} is closed`);
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare class AllwrightError extends Error {
|
|
2
|
+
readonly debugDetails?: string;
|
|
3
|
+
constructor(message: string, debugDetails?: string);
|
|
4
|
+
}
|
|
5
|
+
export declare function formatStreamError(raw: string): AllwrightError;
|
|
6
|
+
export declare function formatActionError(action: string, raw: string, locator?: string): AllwrightError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export class AllwrightError extends Error {
|
|
2
|
+
debugDetails;
|
|
3
|
+
constructor(message, debugDetails) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "AllwrightError";
|
|
6
|
+
this.debugDetails = debugDetails;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function formatStreamError(raw) {
|
|
10
|
+
const normalized = stripTransportPrefix(raw);
|
|
11
|
+
const evaluated = extractEvaluateMessage(normalized);
|
|
12
|
+
return createError(evaluated.userMessage, raw, evaluated.debugDetails);
|
|
13
|
+
}
|
|
14
|
+
export function formatActionError(action, raw, locator) {
|
|
15
|
+
const normalized = stripTransportPrefix(raw);
|
|
16
|
+
const evaluated = extractEvaluateMessage(normalized);
|
|
17
|
+
const locatorLabel = locator ? ` for locator ${formatLocator(locator)}` : "";
|
|
18
|
+
const message = evaluated.userMessage
|
|
19
|
+
? `${capitalize(action)} failed${locatorLabel}: ${evaluated.userMessage}`
|
|
20
|
+
: `${capitalize(action)} failed${locatorLabel}.`;
|
|
21
|
+
return createError(message, raw, evaluated.debugDetails);
|
|
22
|
+
}
|
|
23
|
+
function createError(message, raw, debugDetails) {
|
|
24
|
+
if (debugEnabled()) {
|
|
25
|
+
return new AllwrightError(`${message}\n\nDebug details: ${debugDetails ?? raw}`, debugDetails ?? raw);
|
|
26
|
+
}
|
|
27
|
+
return new AllwrightError(message, debugDetails ?? raw);
|
|
28
|
+
}
|
|
29
|
+
function extractEvaluateMessage(raw) {
|
|
30
|
+
const exceptionMarker = "Runtime.evaluate failed with exception details:";
|
|
31
|
+
const mapperMarker = "mapper Runtime.evaluate raised exception details:";
|
|
32
|
+
const marker = raw.includes(exceptionMarker)
|
|
33
|
+
? exceptionMarker
|
|
34
|
+
: raw.includes(mapperMarker)
|
|
35
|
+
? mapperMarker
|
|
36
|
+
: null;
|
|
37
|
+
if (!marker) {
|
|
38
|
+
return { userMessage: cleanupUserMessage(raw) };
|
|
39
|
+
}
|
|
40
|
+
const payload = raw.slice(raw.indexOf(marker) + marker.length).trim();
|
|
41
|
+
const parsed = tryParseJson(payload);
|
|
42
|
+
const message = pickString(parsed, ["exception", "message"]) ??
|
|
43
|
+
pickPreviewProperty(parsed, "message") ??
|
|
44
|
+
pickString(parsed, ["exception", "description"]) ??
|
|
45
|
+
raw;
|
|
46
|
+
return {
|
|
47
|
+
userMessage: cleanupUserMessage(message),
|
|
48
|
+
debugDetails: payload,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function cleanupUserMessage(message) {
|
|
52
|
+
const compact = message.replace(/\s+/g, " ").trim();
|
|
53
|
+
const invalidQuerySelector = compact.match(/Failed to execute 'querySelector(All)?' on 'Document': '(.+?)' is not a valid selector\.?/);
|
|
54
|
+
if (invalidQuerySelector) {
|
|
55
|
+
return `invalid selector ${invalidQuerySelector[2]}`;
|
|
56
|
+
}
|
|
57
|
+
return compact
|
|
58
|
+
.replace(/^SyntaxError:\s*/i, "")
|
|
59
|
+
.replace(/^DOMException:\s*/i, "")
|
|
60
|
+
.replace(/^Error:\s*/i, "");
|
|
61
|
+
}
|
|
62
|
+
function stripTransportPrefix(raw) {
|
|
63
|
+
return raw
|
|
64
|
+
.replace(/^grpc stream error:\s*/i, "")
|
|
65
|
+
.replace(/^\d+\s+INTERNAL:\s*/i, "")
|
|
66
|
+
.trim();
|
|
67
|
+
}
|
|
68
|
+
function capitalize(value) {
|
|
69
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
70
|
+
}
|
|
71
|
+
function formatLocator(locator) {
|
|
72
|
+
return JSON.stringify(locator.trim());
|
|
73
|
+
}
|
|
74
|
+
function debugEnabled() {
|
|
75
|
+
const raw = process.env.ALLWRIGHT_DEBUG?.trim().toLowerCase();
|
|
76
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
77
|
+
}
|
|
78
|
+
function tryParseJson(value) {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(value);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function pickString(root, path) {
|
|
87
|
+
let current = root;
|
|
88
|
+
for (const segment of path) {
|
|
89
|
+
if (!current || typeof current !== "object" || !(segment in current)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
current = current[segment];
|
|
93
|
+
}
|
|
94
|
+
return typeof current === "string" && current.trim() ? current.trim() : null;
|
|
95
|
+
}
|
|
96
|
+
function pickPreviewProperty(root, name) {
|
|
97
|
+
const properties = pickUnknown(root, ["exception", "preview", "properties"]);
|
|
98
|
+
if (!Array.isArray(properties)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
for (const property of properties) {
|
|
102
|
+
if (property &&
|
|
103
|
+
typeof property === "object" &&
|
|
104
|
+
property.name === name &&
|
|
105
|
+
typeof property.value === "string") {
|
|
106
|
+
return (property.value ?? "").trim() || null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function pickUnknown(root, path) {
|
|
112
|
+
let current = root;
|
|
113
|
+
for (const segment of path) {
|
|
114
|
+
if (!current || typeof current !== "object" || !(segment in current)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
current = current[segment];
|
|
118
|
+
}
|
|
119
|
+
return current;
|
|
120
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BrowserImpl, BrowserTypeImpl } from "./browser.js";
|
|
2
|
+
import { formatActionError } from "./errors.js";
|
|
2
3
|
import { findConfigFile, loadConfigFile, resolveConfig } from "./config.js";
|
|
3
4
|
import { PageImpl } from "./page.js";
|
|
4
5
|
import { createBrowserSessionHandle, getRuntime, launchConfiguredBrowser as launchConfiguredBrowserWithResolver, ping as runtimePing, resolveLaunchBrowserArgs, setServerAddr, shutdown, } from "./runtime.js";
|
|
@@ -42,7 +43,7 @@ export async function launchBrowser(browserKindOrOptions, options = {}) {
|
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
45
|
if (event.error?.message) {
|
|
45
|
-
throw
|
|
46
|
+
throw formatActionError("launch browser", event.error.message);
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
}
|
package/dist/page.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LocatorImpl } from "./locator.js";
|
|
2
|
+
import { formatActionError } from "./errors.js";
|
|
2
3
|
import { normalizeSelectorForTransport } from "./selectors.js";
|
|
3
4
|
import { createPageHandle } from "./runtime.js";
|
|
4
5
|
export class PageImpl {
|
|
@@ -36,7 +37,7 @@ export class PageImpl {
|
|
|
36
37
|
injection = event.chromiumBidiInjection;
|
|
37
38
|
}
|
|
38
39
|
if (event.error?.message) {
|
|
39
|
-
throw
|
|
40
|
+
throw formatActionError("navigate", event.error.message);
|
|
40
41
|
}
|
|
41
42
|
if (event.closed) {
|
|
42
43
|
handle.closed = true;
|
|
@@ -76,7 +77,7 @@ export class PageImpl {
|
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
if (event.error?.message) {
|
|
79
|
-
throw
|
|
80
|
+
throw formatActionError("click", event.error.message, selector);
|
|
80
81
|
}
|
|
81
82
|
if (event.closed) {
|
|
82
83
|
handle.closed = true;
|
|
@@ -106,7 +107,7 @@ export class PageImpl {
|
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
if (event.error?.message) {
|
|
109
|
-
throw
|
|
110
|
+
throw formatActionError("count elements", event.error.message, selector);
|
|
110
111
|
}
|
|
111
112
|
if (event.closed) {
|
|
112
113
|
handle.closed = true;
|
|
@@ -137,7 +138,7 @@ export class PageImpl {
|
|
|
137
138
|
};
|
|
138
139
|
}
|
|
139
140
|
if (event.error?.message) {
|
|
140
|
-
throw
|
|
141
|
+
throw formatActionError("highlight elements", event.error.message, selector);
|
|
141
142
|
}
|
|
142
143
|
if (event.closed) {
|
|
143
144
|
handle.closed = true;
|
|
@@ -166,7 +167,7 @@ export class PageImpl {
|
|
|
166
167
|
};
|
|
167
168
|
}
|
|
168
169
|
if (event.error?.message) {
|
|
169
|
-
throw
|
|
170
|
+
throw formatActionError("focus", event.error.message, selector);
|
|
170
171
|
}
|
|
171
172
|
if (event.closed) {
|
|
172
173
|
handle.closed = true;
|
|
@@ -197,7 +198,7 @@ export class PageImpl {
|
|
|
197
198
|
};
|
|
198
199
|
}
|
|
199
200
|
if (event.error?.message) {
|
|
200
|
-
throw
|
|
201
|
+
throw formatActionError("fill", event.error.message, selector);
|
|
201
202
|
}
|
|
202
203
|
if (event.closed) {
|
|
203
204
|
handle.closed = true;
|
|
@@ -226,7 +227,7 @@ export class PageImpl {
|
|
|
226
227
|
};
|
|
227
228
|
}
|
|
228
229
|
if (event.error?.message) {
|
|
229
|
-
throw
|
|
230
|
+
throw formatActionError("hover", event.error.message, selector);
|
|
230
231
|
}
|
|
231
232
|
if (event.closed) {
|
|
232
233
|
handle.closed = true;
|
|
@@ -258,7 +259,7 @@ export class PageImpl {
|
|
|
258
259
|
};
|
|
259
260
|
}
|
|
260
261
|
if (event.error?.message) {
|
|
261
|
-
throw
|
|
262
|
+
throw formatActionError("press key", event.error.message, selector);
|
|
262
263
|
}
|
|
263
264
|
if (event.closed) {
|
|
264
265
|
handle.closed = true;
|
|
@@ -295,7 +296,7 @@ export class PageImpl {
|
|
|
295
296
|
};
|
|
296
297
|
}
|
|
297
298
|
if (event.error?.message) {
|
|
298
|
-
throw
|
|
299
|
+
throw formatActionError("wait for selector", event.error.message, selector);
|
|
299
300
|
}
|
|
300
301
|
if (event.closed) {
|
|
301
302
|
handle.closed = true;
|
|
@@ -321,7 +322,7 @@ export class PageImpl {
|
|
|
321
322
|
return;
|
|
322
323
|
}
|
|
323
324
|
if (event.error?.message) {
|
|
324
|
-
throw
|
|
325
|
+
throw formatActionError("close page", event.error.message);
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
}
|
|
@@ -341,7 +342,7 @@ export class PageImpl {
|
|
|
341
342
|
return event.pong.message;
|
|
342
343
|
}
|
|
343
344
|
if (event.error?.message) {
|
|
344
|
-
throw
|
|
345
|
+
throw formatActionError("ping page", event.error.message);
|
|
345
346
|
}
|
|
346
347
|
if (event.closed) {
|
|
347
348
|
handle.closed = true;
|
|
@@ -396,7 +397,7 @@ export class PageImpl {
|
|
|
396
397
|
};
|
|
397
398
|
}
|
|
398
399
|
if (event.error?.message) {
|
|
399
|
-
throw
|
|
400
|
+
throw formatActionError("read text", event.error.message, selector);
|
|
400
401
|
}
|
|
401
402
|
if (event.closed) {
|
|
402
403
|
handle.closed = true;
|
|
@@ -406,7 +407,7 @@ export class PageImpl {
|
|
|
406
407
|
}
|
|
407
408
|
#ensureOpen(handle) {
|
|
408
409
|
if (handle.closed) {
|
|
409
|
-
throw
|
|
410
|
+
throw formatActionError("use page", `page session ${this.sessionId} is closed`);
|
|
410
411
|
}
|
|
411
412
|
}
|
|
412
413
|
async #getHandle() {
|
package/dist/runtime.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import grpc from "@grpc/grpc-js";
|
|
3
3
|
import protoLoader from "@grpc/proto-loader";
|
|
4
|
+
import { ensureRuntimeReady, shutdownManagedServer } from "./bootstrap.js";
|
|
5
|
+
import { formatStreamError } from "./errors.js";
|
|
4
6
|
import { EventQueue } from "./types.js";
|
|
5
7
|
const DEFAULT_SERVER_ADDR = "127.0.0.1:50051";
|
|
6
8
|
const SERVER_ADDR_ENV_VAR = "ALLWRIGHT_SERVER_ADDR";
|
|
@@ -11,6 +13,7 @@ let serverAddrOverride = null;
|
|
|
11
13
|
export function setServerAddr(serverAddr) {
|
|
12
14
|
serverAddrOverride = normalizeServerAddr(serverAddr);
|
|
13
15
|
runtimePromise = null;
|
|
16
|
+
void shutdownManagedServer();
|
|
14
17
|
}
|
|
15
18
|
export async function shutdown() {
|
|
16
19
|
if (!runtimePromise) {
|
|
@@ -19,6 +22,7 @@ export async function shutdown() {
|
|
|
19
22
|
const runtime = await runtimePromise;
|
|
20
23
|
runtime.client.close();
|
|
21
24
|
runtimePromise = null;
|
|
25
|
+
await shutdownManagedServer();
|
|
22
26
|
}
|
|
23
27
|
export async function ping() {
|
|
24
28
|
const runtime = await getRuntime();
|
|
@@ -34,7 +38,7 @@ export async function ping() {
|
|
|
34
38
|
}
|
|
35
39
|
export async function getRuntime() {
|
|
36
40
|
if (!runtimePromise) {
|
|
37
|
-
runtimePromise =
|
|
41
|
+
runtimePromise = createRuntime();
|
|
38
42
|
}
|
|
39
43
|
return runtimePromise;
|
|
40
44
|
}
|
|
@@ -72,7 +76,9 @@ export function normalizeServerAddr(raw) {
|
|
|
72
76
|
export function resolveLaunchBrowserArgs(browserKindOrOptions) {
|
|
73
77
|
return browserKindOrOptions === undefined || typeof browserKindOrOptions !== "string";
|
|
74
78
|
}
|
|
75
|
-
function createRuntime() {
|
|
79
|
+
async function createRuntime() {
|
|
80
|
+
const serverAddr = configuredServerAddr();
|
|
81
|
+
await ensureRuntimeReady(serverAddr);
|
|
76
82
|
const loaded = protoLoader.loadSync(ENGINE_PROTO_PATH, {
|
|
77
83
|
includeDirs: [PROTO_ROOT],
|
|
78
84
|
keepCase: false,
|
|
@@ -83,7 +89,7 @@ function createRuntime() {
|
|
|
83
89
|
});
|
|
84
90
|
const proto = grpc.loadPackageDefinition(loaded);
|
|
85
91
|
const ClientCtor = proto.allwright.engine.v1.EngineService;
|
|
86
|
-
const client = new ClientCtor(
|
|
92
|
+
const client = new ClientCtor(serverAddr, grpc.credentials.createInsecure());
|
|
87
93
|
return { client };
|
|
88
94
|
}
|
|
89
95
|
function configuredServerAddr() {
|
|
@@ -98,7 +104,7 @@ function bindStreamQueue(stream) {
|
|
|
98
104
|
queue.push(event);
|
|
99
105
|
});
|
|
100
106
|
stream.on("error", (error) => {
|
|
101
|
-
queue.fail(
|
|
107
|
+
queue.fail(formatStreamError(`grpc stream error: ${error.message}`));
|
|
102
108
|
});
|
|
103
109
|
stream.on("end", () => {
|
|
104
110
|
queue.fail(new Error("grpc stream ended"));
|
package/dist/selectors.js
CHANGED
|
@@ -1,11 +1,92 @@
|
|
|
1
|
+
const SELECTOR_PREFIXES = ["xpath=", "xpath:", "css=", "css:"];
|
|
2
|
+
function decodeSelectorBody(body) {
|
|
3
|
+
const candidate = body.trim();
|
|
4
|
+
if (candidate.startsWith("\"") && candidate.endsWith("\"")) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(candidate);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return candidate;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return candidate;
|
|
13
|
+
}
|
|
14
|
+
function parseExplicitSelectorPrefix(selector) {
|
|
15
|
+
const lowered = selector.toLowerCase();
|
|
16
|
+
if (lowered.startsWith("xpath=") || lowered.startsWith("xpath:")) {
|
|
17
|
+
return { flavor: "xpath", prefixLength: 6 };
|
|
18
|
+
}
|
|
19
|
+
if (lowered.startsWith("css=") || lowered.startsWith("css:")) {
|
|
20
|
+
return { flavor: "css", prefixLength: 4 };
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function findJsonStringEnd(value) {
|
|
25
|
+
if (!value.startsWith("\"")) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
let escaped = false;
|
|
29
|
+
for (let index = 1; index < value.length; index += 1) {
|
|
30
|
+
const char = value[index];
|
|
31
|
+
if (escaped) {
|
|
32
|
+
escaped = false;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (char === "\\") {
|
|
36
|
+
escaped = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (char === "\"") {
|
|
40
|
+
return index + 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function isNormalizedTransportSelector(selector) {
|
|
46
|
+
const trimmed = selector.trim();
|
|
47
|
+
if (!trimmed) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
let index = 0;
|
|
51
|
+
while (index < trimmed.length) {
|
|
52
|
+
const prefix = parseExplicitSelectorPrefix(trimmed.slice(index));
|
|
53
|
+
if (!prefix) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
index += prefix.prefixLength;
|
|
57
|
+
const remainder = trimmed.slice(index);
|
|
58
|
+
if (!remainder.startsWith("\"")) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const jsonEnd = findJsonStringEnd(remainder);
|
|
62
|
+
if (!jsonEnd) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
index += jsonEnd;
|
|
66
|
+
const tail = trimmed.slice(index);
|
|
67
|
+
if (!tail) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (!/^\s+/.test(tail)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const nextIndex = index + tail.match(/^\s+/)?.[0].length;
|
|
74
|
+
const nextSegment = trimmed.slice(nextIndex).toLowerCase();
|
|
75
|
+
if (!SELECTOR_PREFIXES.some((prefixValue) => nextSegment.startsWith(prefixValue))) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
index = nextIndex;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
1
82
|
export function parseSelectorForTransport(selector) {
|
|
2
83
|
const trimmed = selector.trim();
|
|
3
84
|
const lower = trimmed.toLowerCase();
|
|
4
85
|
if (lower.startsWith("xpath=") || lower.startsWith("xpath:")) {
|
|
5
|
-
return { flavor: "xpath", body: trimmed.slice(6)
|
|
86
|
+
return { flavor: "xpath", body: decodeSelectorBody(trimmed.slice(6)) };
|
|
6
87
|
}
|
|
7
88
|
if (lower.startsWith("css=") || lower.startsWith("css:")) {
|
|
8
|
-
return { flavor: "css", body: trimmed.slice(4)
|
|
89
|
+
return { flavor: "css", body: decodeSelectorBody(trimmed.slice(4)) };
|
|
9
90
|
}
|
|
10
91
|
if (trimmed.startsWith("//") ||
|
|
11
92
|
trimmed.startsWith(".//") ||
|
|
@@ -17,6 +98,10 @@ export function parseSelectorForTransport(selector) {
|
|
|
17
98
|
return { flavor: "css", body: trimmed };
|
|
18
99
|
}
|
|
19
100
|
export function normalizeSelectorForTransport(selector) {
|
|
101
|
+
const trimmed = selector.trim();
|
|
102
|
+
if (isNormalizedTransportSelector(trimmed)) {
|
|
103
|
+
return trimmed;
|
|
104
|
+
}
|
|
20
105
|
const parsed = parseSelectorForTransport(selector);
|
|
21
106
|
return `${parsed.flavor}=${JSON.stringify(parsed.body)}`;
|
|
22
107
|
}
|
package/dist/types.d.ts
CHANGED
package/dist/types.js
CHANGED