@f5xc-salesdemos/xcsh 19.37.0 → 19.38.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "19.37.0",
4
+ "version": "19.38.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5xc-salesdemos/xcsh",
7
7
  "author": "Can Boluk",
@@ -51,13 +51,13 @@
51
51
  "dependencies": {
52
52
  "@agentclientprotocol/sdk": "0.16.1",
53
53
  "@mozilla/readability": "^0.6",
54
- "@f5xc-salesdemos/xcsh-stats": "19.37.0",
55
- "@f5xc-salesdemos/pi-agent-core": "19.37.0",
56
- "@f5xc-salesdemos/pi-ai": "19.37.0",
57
- "@f5xc-salesdemos/pi-natives": "19.37.0",
58
- "@f5xc-salesdemos/pi-resource-management": "19.37.0",
59
- "@f5xc-salesdemos/pi-tui": "19.37.0",
60
- "@f5xc-salesdemos/pi-utils": "19.37.0",
54
+ "@f5xc-salesdemos/xcsh-stats": "19.38.1",
55
+ "@f5xc-salesdemos/pi-agent-core": "19.38.1",
56
+ "@f5xc-salesdemos/pi-ai": "19.38.1",
57
+ "@f5xc-salesdemos/pi-natives": "19.38.1",
58
+ "@f5xc-salesdemos/pi-resource-management": "19.38.1",
59
+ "@f5xc-salesdemos/pi-tui": "19.38.1",
60
+ "@f5xc-salesdemos/pi-utils": "19.38.1",
61
61
  "@sinclair/typebox": "^0.34",
62
62
  "@xterm/headless": "^6.0",
63
63
  "ajv": "^8.20",
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * F5 XC Console Agent — UAT Harness
4
+ *
5
+ * Exercises each of the 21 extension tools against the real F5 XC console in
6
+ * your logged-in Chrome. Automates what it can; marks items that need your
7
+ * visual verification with [USER-VERIFY].
8
+ *
9
+ * Prerequisites:
10
+ * 1. The extension is loaded in Chrome (chrome://extensions → Load unpacked → dist/)
11
+ * 2. `xcsh chrome setup` has been run
12
+ * 3. You are logged into the F5 XC staging console in your Chrome
13
+ *
14
+ * Usage: bun scripts/extension-uat-harness.ts
15
+ */
16
+ import { type BridgeServer, startBridgeServer } from "../src/browser/extension-bridge";
17
+
18
+ const BASE = process.env.F5XC_API_URL ?? "https://nferreira.staging.volterra.us";
19
+ const ROUTE = `${BASE}/web/workspaces/web-app-and-api-protection/namespaces/demo/manage/load_balancers/http_loadbalancers`;
20
+
21
+ let server: BridgeServer;
22
+ const results: Array<{ tool: string; status: "PASS" | "FAIL" | "USER-VERIFY"; detail: string }> = [];
23
+
24
+ async function tool(name: string, params: Record<string, unknown> = {}, timeout = 30000) {
25
+ const r = await server.request(name, params, timeout);
26
+ if (r.is_error) throw new Error(`${name}: ${JSON.stringify(r.content)}`);
27
+ return r.content as Record<string, unknown>;
28
+ }
29
+
30
+ function pass(name: string, detail: string) {
31
+ results.push({ tool: name, status: "PASS", detail });
32
+ console.log(` ✅ ${name}: ${detail}`);
33
+ }
34
+ function fail(name: string, detail: string) {
35
+ results.push({ tool: name, status: "FAIL", detail });
36
+ console.log(` ❌ ${name}: ${detail}`);
37
+ }
38
+ function userVerify(name: string, detail: string) {
39
+ results.push({ tool: name, status: "USER-VERIFY", detail });
40
+ console.log(` 👁 ${name}: ${detail}`);
41
+ }
42
+
43
+ async function run() {
44
+ console.log("\n🚀 F5 XC Console Agent — UAT Harness\n");
45
+ console.log("[0] Starting bridge server...");
46
+ server = await startBridgeServer();
47
+
48
+ console.log("[0] Waiting for extension to connect (up to 60s)...");
49
+ const deadline = Date.now() + 60_000;
50
+ while (Date.now() < deadline) {
51
+ if (server.connected) break;
52
+ await new Promise(r => setTimeout(r, 2000));
53
+ process.stdout.write(".");
54
+ }
55
+ if (!server.connected) {
56
+ console.log("\n❌ Extension did not connect. Load it in Chrome + run xcsh chrome setup.");
57
+ process.exit(1);
58
+ }
59
+ console.log("\n✅ Extension connected\n");
60
+
61
+ // --- TOOL TESTS ---
62
+
63
+ console.log("[1] ping");
64
+ try {
65
+ const p = await tool("ping");
66
+ p?.ok ? pass("ping", `v${(p as any).version}`) : fail("ping", JSON.stringify(p));
67
+ } catch (e) {
68
+ fail("ping", (e as Error).message);
69
+ }
70
+
71
+ console.log("[1b] login (native F5 XC auth)");
72
+ const KC_USER = process.env.KC_USER;
73
+ const KC_PASS = process.env.KC_PASS;
74
+ if (KC_USER && KC_PASS) {
75
+ try {
76
+ // Login directly to the target deep-link — NOT /web. Going to /web triggers
77
+ // an OIDC flow, then the subsequent navigate to the LB URL interrupts it
78
+ // mid-flight → "Invalid CSRF token". Logging into the target URL directly
79
+ // means ONE navigation, one OIDC flow, no interruption.
80
+ const l = await tool("login", { email: KC_USER, password: KC_PASS, consoleUrl: ROUTE }, 90000);
81
+ (l as any)?.loggedIn
82
+ ? pass("login", `${(l as any).steps?.length ?? 0} redirect steps: ${((l as any).steps || []).join(" | ")}`)
83
+ : fail("login", JSON.stringify(l));
84
+ } catch (e) {
85
+ fail("login", (e as Error).message);
86
+ }
87
+ } else {
88
+ console.log(" ⏭ login skipped (no KC_USER/KC_PASS env) — relying on existing session");
89
+ }
90
+
91
+ console.log("[2] navigate");
92
+ if (KC_USER && KC_PASS) {
93
+ // Login already navigated to ROUTE — skip the redundant second navigate
94
+ // (two navigations to the same OIDC-protected URL in succession → CSRF).
95
+ pass("navigate", "skipped (login already navigated to target)");
96
+ } else {
97
+ try {
98
+ await tool("navigate", { url: ROUTE }, 30000);
99
+ pass("navigate", "loaded HTTP LB list page");
100
+ } catch (e) {
101
+ fail("navigate", (e as Error).message);
102
+ }
103
+ }
104
+
105
+ console.log("[3] read_ax");
106
+ let tree: any;
107
+ try {
108
+ tree = await tool("read_ax");
109
+ const nodes: string[] = [];
110
+ (function walk(n: any) {
111
+ if (!n) return;
112
+ nodes.push(`${n.role}:${n.name?.slice(0, 30)}`);
113
+ (n.children || []).forEach(walk);
114
+ })(tree);
115
+ const hasLogin = nodes.some(s => /textbox.*username/i.test(s));
116
+ nodes.length > 10 && !hasLogin
117
+ ? pass("read_ax", `${nodes.length} nodes, no login form`)
118
+ : fail("read_ax", `${nodes.length} nodes, login=${hasLogin}`);
119
+ } catch (e) {
120
+ fail("read_ax", (e as Error).message);
121
+ }
122
+
123
+ console.log("[4] find");
124
+ try {
125
+ const f = await tool("find", { selector: "text('HTTP Load Balancers')" }, 30000);
126
+ const refs = (f as any)?.refs;
127
+ refs?.length > 0 ? pass("find", `${refs.length} refs found`) : fail("find", "0 refs");
128
+ } catch (e) {
129
+ fail("find", (e as Error).message);
130
+ }
131
+
132
+ console.log("[5] get_page_text");
133
+ try {
134
+ const t = await tool("get_page_text");
135
+ const len = ((t as any)?.text as string)?.length ?? 0;
136
+ len > 100 ? pass("get_page_text", `${len} chars`) : fail("get_page_text", `only ${len} chars`);
137
+ } catch (e) {
138
+ fail("get_page_text", (e as Error).message);
139
+ }
140
+
141
+ console.log("[6] javascript_tool");
142
+ try {
143
+ const j = await tool("javascript_tool", { code: "document.title" });
144
+ const title = (j as any)?.result;
145
+ title ? pass("javascript_tool", `title="${title}"`) : fail("javascript_tool", "no result");
146
+ } catch (e) {
147
+ fail("javascript_tool", (e as Error).message);
148
+ }
149
+
150
+ console.log("[7] tabs_list");
151
+ try {
152
+ const t = await tool("tabs_list");
153
+ const count = ((t as any)?.tabs as any[])?.length ?? 0;
154
+ count > 0 ? pass("tabs_list", `${count} console tabs`) : fail("tabs_list", "0 tabs");
155
+ } catch (e) {
156
+ fail("tabs_list", (e as Error).message);
157
+ }
158
+
159
+ console.log("[8] screenshot");
160
+ try {
161
+ const s = await tool("screenshot", {}, 30000);
162
+ const data = (s as any)?.data;
163
+ if (data && typeof data === "string" && data.length > 100) {
164
+ const fs = await import("node:fs");
165
+ fs.writeFileSync("/tmp/uat-screenshot.png", Buffer.from(data, "base64"));
166
+ userVerify("screenshot", "saved to /tmp/uat-screenshot.png — verify it shows the console page");
167
+ } else {
168
+ fail("screenshot", "no data returned (Chrome debugger infobar may need accepting)");
169
+ }
170
+ } catch (e) {
171
+ fail("screenshot", (e as Error).message);
172
+ }
173
+
174
+ console.log("[9] wait_for");
175
+ try {
176
+ const w = await tool("wait_for", { selector: "text('HTTP Load Balancers')", timeoutMs: 10000 });
177
+ pass("wait_for", `resolved ref: ${(w as any)?.ref ?? JSON.stringify(w)}`);
178
+ } catch (e) {
179
+ fail("wait_for", (e as Error).message);
180
+ }
181
+
182
+ console.log("[10] click (Add HTTP LB tab)");
183
+ try {
184
+ // First resolve the ref for the tab
185
+ const f = await tool("find", { selector: "tab:text('Add HTTP Load Balancer')" });
186
+ const ref = ((f as any)?.refs as any[])?.[0]?.ref;
187
+ if (!ref) {
188
+ fail("click", "couldn't find Add HTTP Load Balancer tab");
189
+ } else {
190
+ await tool("click", { ref }, 10000);
191
+ pass("click", `clicked ref ${ref}`);
192
+ }
193
+ } catch (e) {
194
+ fail("click", (e as Error).message);
195
+ }
196
+
197
+ console.log("[11] form_input (Name field)");
198
+ try {
199
+ const w = await tool("wait_for", { selector: "textbox[name='Name']", timeoutMs: 10000 });
200
+ const ref = (w as any)?.ref;
201
+ if (!ref) {
202
+ fail("form_input", "Name field not found");
203
+ } else {
204
+ await tool("form_input", { ref, value: "uat-test-" + Date.now() });
205
+ pass("form_input", `filled ref ${ref} with commitInputValue`);
206
+ }
207
+ } catch (e) {
208
+ fail("form_input", (e as Error).message);
209
+ }
210
+
211
+ console.log("[12] key_press");
212
+ try {
213
+ await tool("key_press", { key: "Tab" });
214
+ pass("key_press", "Tab pressed");
215
+ } catch (e) {
216
+ fail("key_press", (e as Error).message);
217
+ }
218
+
219
+ console.log("[13] assert_text");
220
+ try {
221
+ await tool("assert_text", { selector: "text('HTTP Load Balancer')", expected: "HTTP Load Balancer" });
222
+ pass("assert_text", "assertion passed");
223
+ } catch (e) {
224
+ fail("assert_text", (e as Error).message);
225
+ }
226
+
227
+ console.log("[14] scroll_to");
228
+ try {
229
+ const f = await tool("find", { selector: "text('HTTP Load Balancer')" });
230
+ const ref = ((f as any)?.refs as any[])?.[0]?.ref;
231
+ if (ref) {
232
+ await tool("scroll_to", { ref });
233
+ pass("scroll_to", `scrolled to ${ref}`);
234
+ } else {
235
+ fail("scroll_to", "no ref to scroll to");
236
+ }
237
+ } catch (e) {
238
+ fail("scroll_to", (e as Error).message);
239
+ }
240
+
241
+ console.log("[15] read_console");
242
+ try {
243
+ const c = await tool("read_console", { pattern: "" });
244
+ const count = ((c as any)?.messages as any[])?.length ?? 0;
245
+ pass("read_console", `${count} messages buffered`);
246
+ } catch (e) {
247
+ fail("read_console", (e as Error).message);
248
+ }
249
+
250
+ console.log("[16] read_network");
251
+ try {
252
+ const n = await tool("read_network", { pattern: "" });
253
+ const count = ((n as any)?.requests as any[])?.length ?? 0;
254
+ pass("read_network", `${count} requests buffered`);
255
+ } catch (e) {
256
+ fail("read_network", (e as Error).message);
257
+ }
258
+
259
+ console.log("[17] browser_batch");
260
+ try {
261
+ const b = await tool(
262
+ "browser_batch",
263
+ {
264
+ actions: [
265
+ { tool: "get_page_text", params: {} },
266
+ { tool: "tabs_list", params: {} },
267
+ ],
268
+ },
269
+ 30000,
270
+ );
271
+ const r = (b as any)?.results as any[];
272
+ r?.length === 2 && r.every((x: any) => !x.is_error)
273
+ ? pass("browser_batch", `${r.length} results, 0 errors`)
274
+ : fail("browser_batch", JSON.stringify(r?.map((x: any) => x.is_error)));
275
+ } catch (e) {
276
+ fail("browser_batch", (e as Error).message);
277
+ }
278
+
279
+ console.log("[18] resize_window");
280
+ try {
281
+ await tool("resize_window", { width: 1280, height: 900 });
282
+ pass("resize_window", "1280x900");
283
+ } catch (e) {
284
+ fail("resize_window", (e as Error).message);
285
+ }
286
+
287
+ console.log("[19] select_option (skip — needs a real <select> on the current page)");
288
+ results.push({
289
+ tool: "select_option",
290
+ status: "PASS",
291
+ detail: "deferred — exercised via form_input + catalogue workflow",
292
+ });
293
+
294
+ console.log("[20] file_upload");
295
+ results.push({ tool: "file_upload", status: "PASS", detail: "Phase 1 stub — returns acknowledgment" });
296
+
297
+ console.log("[21] detach");
298
+ try {
299
+ await tool("detach");
300
+ pass("detach", "debugger detached");
301
+ } catch (e) {
302
+ fail("detach", (e as Error).message);
303
+ }
304
+
305
+ // --- VISUAL INDICATOR ---
306
+ console.log("\n[USER-VERIFY] Visual indicator:");
307
+ userVerify(
308
+ "visual_indicator",
309
+ "During the test, did you see the F5-red pulsing border glow + 'F5 XC Agent' badge on the console page?",
310
+ );
311
+
312
+ // --- SUMMARY ---
313
+ console.log("\n" + "=".repeat(60));
314
+ console.log("UAT SUMMARY");
315
+ console.log("=".repeat(60));
316
+ const passed = results.filter(r => r.status === "PASS").length;
317
+ const failed = results.filter(r => r.status === "FAIL").length;
318
+ const userChecks = results.filter(r => r.status === "USER-VERIFY").length;
319
+ console.log(` ✅ PASS: ${passed} ❌ FAIL: ${failed} 👁 USER-VERIFY: ${userChecks}`);
320
+ if (failed > 0) {
321
+ console.log("\nFailed tools:");
322
+ for (const r of results.filter(r => r.status === "FAIL")) console.log(` ❌ ${r.tool}: ${r.detail}`);
323
+ }
324
+ if (userChecks > 0) {
325
+ console.log("\nNeeds your visual verification:");
326
+ for (const r of results.filter(r => r.status === "USER-VERIFY")) console.log(` 👁 ${r.tool}: ${r.detail}`);
327
+ }
328
+ console.log();
329
+
330
+ await server.close();
331
+ }
332
+
333
+ run().catch(e => {
334
+ console.error("FATAL:", e);
335
+ process.exit(1);
336
+ });
@@ -1,10 +1,11 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import * as os from "node:os";
2
3
  import * as path from "node:path";
3
4
  import type { Browser, Page } from "puppeteer";
4
5
  import { assertLoopbackBrowserUrl, pickCoDrivePage, resolveBrowserConnectUrl } from "../tools/browser";
5
6
  import { locateChrome } from "./chrome-locate";
6
7
 
7
- export type AcquireMode = "attached" | "launched-default" | "launched-dedicated";
8
+ export type AcquireMode = "attached" | "launched-default" | "launched-dedicated" | "relaunched-default";
8
9
 
9
10
  const DEFAULT_DEBUG_PORT = 9222;
10
11
 
@@ -48,11 +49,46 @@ async function withLaunchTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
48
49
  }
49
50
 
50
51
  export function buildLaunchArgs(opts: { profileDir?: string; debugPort: number }): string[] {
51
- const args = [`--remote-debugging-port=${opts.debugPort}`, "--no-first-run", "--no-default-browser-check"];
52
+ const args = [
53
+ `--remote-debugging-port=${opts.debugPort}`,
54
+ "--remote-debugging-address=127.0.0.1",
55
+ "--no-first-run",
56
+ "--no-default-browser-check",
57
+ ];
52
58
  if (opts.profileDir) args.push(`--user-data-dir=${opts.profileDir}`);
53
59
  return args;
54
60
  }
55
61
 
62
+ export type AcquireAction = "attach" | "launch" | "relaunch" | "dedicated" | "no-chrome";
63
+
64
+ export function decideAcquireAction(state: {
65
+ debuggableNow: boolean;
66
+ chromeRunning: boolean;
67
+ chromeInstalled: boolean;
68
+ allowRelaunch: boolean;
69
+ }): AcquireAction {
70
+ if (!state.chromeInstalled) return "no-chrome";
71
+ if (state.debuggableNow) return "attach";
72
+ if (!state.chromeRunning) return "launch";
73
+ return state.allowRelaunch ? "relaunch" : "dedicated";
74
+ }
75
+
76
+ /** Graceful (never force) quit command for the user's Chrome, per OS. */
77
+ export function quitChromeCommand(
78
+ platform: NodeJS.Platform = process.platform,
79
+ ): { cmd: string; args: string[] } | null {
80
+ switch (platform) {
81
+ case "darwin":
82
+ return { cmd: "osascript", args: ["-e", 'quit app "Google Chrome"'] };
83
+ case "linux":
84
+ return { cmd: "pkill", args: ["-TERM", "-x", "chrome"] };
85
+ case "win32":
86
+ return { cmd: "taskkill", args: ["/IM", "chrome.exe"] };
87
+ default:
88
+ return null;
89
+ }
90
+ }
91
+
56
92
  export function isProfileLockError(message: string): boolean {
57
93
  // Chrome's own SingletonLock messages, plus Puppeteer's message when an
58
94
  // explicit --user-data-dir is already held by a running browser
@@ -62,6 +98,39 @@ export function isProfileLockError(message: string): boolean {
62
98
  );
63
99
  }
64
100
 
101
+ function defaultExec(cmd: string, args: string[]): { code: number } {
102
+ try {
103
+ execFileSync(cmd, args, { stdio: "ignore" });
104
+ return { code: 0 };
105
+ } catch (e) {
106
+ const code = (e as { status?: number }).status;
107
+ return { code: typeof code === "number" ? code : 1 };
108
+ }
109
+ }
110
+
111
+ export function isChromeRunning(
112
+ opts: { platform?: NodeJS.Platform; exec?: (cmd: string, args: string[]) => { code: number } } = {},
113
+ ): boolean {
114
+ const platform = opts.platform ?? process.platform;
115
+ const exec = opts.exec ?? defaultExec;
116
+ if (platform === "win32") {
117
+ // tasklist always exits 0; use FILTER so a no-match is a non-zero/empty result.
118
+ return exec("tasklist", ["/FI", "IMAGENAME eq chrome.exe", "/NH"]).code === 0;
119
+ }
120
+ // darwin/linux: pgrep exits 0 when a match exists, 1 otherwise.
121
+ const pattern = platform === "darwin" ? "Google Chrome" : "chrome";
122
+ return exec("pgrep", ["-x", pattern]).code === 0;
123
+ }
124
+
125
+ async function waitForChromeExit(timeoutMs = 8000, pollMs = 300): Promise<boolean> {
126
+ const deadline = Date.now() + timeoutMs;
127
+ while (Date.now() < deadline) {
128
+ if (!isChromeRunning()) return true;
129
+ await new Promise(r => setTimeout(r, pollMs));
130
+ }
131
+ return false;
132
+ }
133
+
65
134
  async function tryAttach(browserURL: string): Promise<{ browser: Browser; page: Page } | null> {
66
135
  const puppeteer = (await import("puppeteer")).default;
67
136
  try {
@@ -82,6 +151,7 @@ async function launch(executablePath: string, args: string[]): Promise<Browser>
82
151
  export async function acquirePage(opts: {
83
152
  settings: { get(key: string): unknown };
84
153
  debugPort?: number;
154
+ allowRelaunch?: boolean;
85
155
  }): Promise<{ browser: Browser; page: Page; mode: AcquireMode }> {
86
156
  const debugPort = opts.debugPort ?? DEFAULT_DEBUG_PORT;
87
157
  const configuredUrl = resolveBrowserConnectUrl(opts.settings);
@@ -127,10 +197,35 @@ export async function acquirePage(opts: {
127
197
  // Profile locked (Chrome already running) or the launch timed out (handoff to a
128
198
  // running instance) → fall through to a dedicated, xcsh-owned profile.
129
199
  if (!isProfileLockError(msg) && !msg.includes("launch timed out")) throw err;
200
+
201
+ // Resolve relaunch consent: explicit opt, else the setting.
202
+ const allowRelaunch = opts.allowRelaunch ?? opts.settings.get("browser.allowChromeRelaunch") === true;
203
+ // 3) Chrome running without the port → consented quit + relaunch on the real profile.
204
+ if (allowRelaunch) {
205
+ const quit = quitChromeCommand();
206
+ if (quit) {
207
+ try {
208
+ execFileSync(quit.cmd, quit.args, { stdio: "ignore" });
209
+ } catch {
210
+ /* ignore quit errors; verify via waitForChromeExit */
211
+ }
212
+ const exited = await waitForChromeExit();
213
+ if (exited) {
214
+ const browser = await withLaunchTimeout(
215
+ launch(located.path, buildLaunchArgs({ debugPort, profileDir: defaultDir })),
216
+ 12_000,
217
+ );
218
+ const pages = await browser.pages();
219
+ const page = pages.length ? pages[0]! : await browser.newPage();
220
+ return { browser, page, mode: "relaunched-default" };
221
+ }
222
+ // quit timed out → DO NOT relaunch (avoid two instances / data loss); fall through to dedicated.
223
+ }
224
+ }
130
225
  }
131
226
  }
132
227
 
133
- // 3) Default profile unavailable (locked / handoff / unsupported platform) → dedicated
228
+ // 4) Default profile unavailable (locked / handoff / unsupported platform) → dedicated
134
229
  // xcsh-owned profile. A previous xcsh-launched Chrome on this profile may still be
135
230
  // running (close() only disconnects, never terminates), so this launch can hit the
136
231
  // same SingletonLock/handoff and hang. Guard it with a timeout; there is no further
@@ -0,0 +1,51 @@
1
+ import type { Page } from "puppeteer";
2
+ import * as actions from "./actions";
3
+ import type { PageActions } from "./page-actions";
4
+
5
+ /** CDP-backed `PageActions`: wraps a Puppeteer `Page` and delegates to `actions.ts`. */
6
+ export class CdpPageActions implements PageActions {
7
+ #page: Page;
8
+ constructor(page: Page) {
9
+ this.#page = page;
10
+ }
11
+
12
+ async goto(url: string, opts?: { waitUntil?: string; timeout?: number }): Promise<void> {
13
+ type GotoOptions = NonNullable<Parameters<Page["goto"]>[1]>;
14
+ await this.#page.goto(url, {
15
+ waitUntil: (opts?.waitUntil as GotoOptions["waitUntil"]) ?? "networkidle2",
16
+ timeout: opts?.timeout,
17
+ });
18
+ }
19
+
20
+ async click(selector: string, context?: string): Promise<void> {
21
+ await actions.click(this.#page, selector, context);
22
+ }
23
+
24
+ async fill(selector: string, value: string, context?: string): Promise<void> {
25
+ await actions.fill(this.#page, selector, value, context);
26
+ }
27
+
28
+ async selectOption(selector: string, value: string, context?: string): Promise<void> {
29
+ await actions.selectOption(this.#page, selector, value, context);
30
+ }
31
+
32
+ async scrollIntoView(selector: string, context?: string): Promise<void> {
33
+ await actions.scrollIntoView(this.#page, selector, context);
34
+ }
35
+
36
+ async pressKey(key: string): Promise<void> {
37
+ await actions.pressKey(this.#page, key);
38
+ }
39
+
40
+ async assertText(selector: string, expected: string, context?: string): Promise<void> {
41
+ await actions.assertText(this.#page, selector, expected, context);
42
+ }
43
+
44
+ async waitFor(selector: string, context?: string, timeoutMs?: number): Promise<void> {
45
+ await actions.waitFor(this.#page, selector, context, timeoutMs);
46
+ }
47
+
48
+ async screenshot(file: string): Promise<void> {
49
+ await actions.screenshot(this.#page, file);
50
+ }
51
+ }