libretto 0.2.4 → 0.2.6

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.
@@ -1,23 +1,16 @@
1
- import { Page } from 'playwright';
2
-
3
- type DebugPauseContext = {
4
- page: Page;
5
- session: string;
6
- };
7
- type DebugPauseDetails = {
8
- sessionName: string;
9
- pausedAt: string;
10
- url: string;
11
- };
12
- declare class DebugPauseSignal extends Error {
13
- readonly details: DebugPauseDetails;
14
- constructor(details: DebugPauseDetails);
15
- }
16
- declare function isDebugPauseSignal(error: unknown): error is DebugPauseSignal;
17
1
  /**
18
- * Signals a workflow pause to the caller.
19
- * This always throws a typed signal that supervisors can intercept.
2
+ * Called by the CLI runtime to make the session name available to `pause()`.
3
+ */
4
+ declare function setSessionForPause(session: string): void;
5
+ /**
6
+ * Standalone pause function.
7
+ *
8
+ * - In production (`NODE_ENV === "production"`), returns immediately (no-op).
9
+ * - Otherwise, writes a `.paused` signal file and polls for a `.resume` signal,
10
+ * using the same file-based mechanism as the CLI runner.
11
+ *
12
+ * Import directly: `import { pause } from "libretto";`
20
13
  */
21
- declare function debugPause(context: DebugPauseContext): Promise<never>;
14
+ declare function pause(): Promise<void>;
22
15
 
23
- export { type DebugPauseContext, type DebugPauseDetails, DebugPauseSignal, debugPause, isDebugPauseSignal };
16
+ export { pause, setSessionForPause };
@@ -1,30 +1,55 @@
1
- class DebugPauseSignal extends Error {
2
- details;
3
- constructor(details) {
4
- super(`Workflow paused at ${details.url}`);
5
- this.name = "DebugPauseSignal";
6
- this.details = details;
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ let _sessionName;
4
+ function setSessionForPause(session) {
5
+ _sessionName = session;
6
+ }
7
+ function getSessionFromProcessArgs() {
8
+ const rawPayload = process.argv[2];
9
+ if (!rawPayload) return void 0;
10
+ try {
11
+ const parsed = JSON.parse(rawPayload);
12
+ return typeof parsed.session === "string" ? parsed.session : void 0;
13
+ } catch {
14
+ return void 0;
7
15
  }
8
16
  }
9
- function isDebugPauseSignal(error) {
10
- if (!error || typeof error !== "object") return false;
11
- const candidate = error;
12
- if (candidate.name !== "DebugPauseSignal") return false;
13
- return typeof candidate.details?.sessionName === "string" && typeof candidate.details?.pausedAt === "string" && typeof candidate.details?.url === "string";
17
+ function resolveSession() {
18
+ return _sessionName ?? getSessionFromProcessArgs();
14
19
  }
15
- async function debugPause(context) {
16
- const url = context.page.url();
20
+ async function pause() {
21
+ if (process.env.NODE_ENV === "production") {
22
+ return;
23
+ }
24
+ const session = resolveSession();
25
+ if (!session) {
26
+ return;
27
+ }
28
+ const { getPauseSignalPaths, removeSignalIfExists } = await import("../../cli/core/pause-signals.js");
29
+ const { getSessionDir } = await import("../../cli/core/context.js");
30
+ const signalPaths = getPauseSignalPaths(session);
31
+ const { pausedSignalPath, resumeSignalPath } = signalPaths;
32
+ await mkdir(getSessionDir(session), { recursive: true });
33
+ await removeSignalIfExists(resumeSignalPath);
17
34
  const details = {
18
- sessionName: context.session,
35
+ sessionName: session,
19
36
  pausedAt: (/* @__PURE__ */ new Date()).toISOString(),
20
- url
37
+ url: "unknown"
21
38
  };
22
- console.log(`[debugPause] Paused at ${url}`);
23
- console.log("[debugPause] Signaling pause to supervisor...");
24
- throw new DebugPauseSignal(details);
39
+ await writeFile(pausedSignalPath, JSON.stringify(details, null, 2), "utf8");
40
+ console.log(`[pause] Paused (session: ${session})`);
41
+ console.log("[pause] Waiting for resume signal...");
42
+ const RESUME_POLL_INTERVAL_MS = 250;
43
+ while (!existsSync(resumeSignalPath)) {
44
+ await new Promise(
45
+ (resolve) => setTimeout(resolve, RESUME_POLL_INTERVAL_MS)
46
+ );
47
+ }
48
+ await removeSignalIfExists(resumeSignalPath);
49
+ await removeSignalIfExists(pausedSignalPath);
50
+ console.log("[pause] Resume signal received. Continuing workflow...");
25
51
  }
26
52
  export {
27
- DebugPauseSignal,
28
- debugPause,
29
- isDebugPauseSignal
53
+ pause,
54
+ setSessionForPause
30
55
  };
@@ -38,8 +38,14 @@ function createLLMClientFromModel(model) {
38
38
  if (typeof msg.content === "string") {
39
39
  return { role: msg.role, content: msg.content };
40
40
  }
41
+ if (msg.role === "assistant") {
42
+ return {
43
+ role: "assistant",
44
+ content: msg.content.filter((part) => part.type === "text").map((part) => ({ type: "text", text: part.text }))
45
+ };
46
+ }
41
47
  return {
42
- role: msg.role,
48
+ role: "user",
43
49
  content: msg.content.map(
44
50
  (part) => part.type === "text" ? { type: "text", text: part.text } : { type: "image", image: part.image }
45
51
  )
@@ -15,8 +15,14 @@ function createLLMClientFromModel(model) {
15
15
  if (typeof msg.content === "string") {
16
16
  return { role: msg.role, content: msg.content };
17
17
  }
18
+ if (msg.role === "assistant") {
19
+ return {
20
+ role: "assistant",
21
+ content: msg.content.filter((part) => part.type === "text").map((part) => ({ type: "text", text: part.text }))
22
+ };
23
+ }
18
24
  return {
19
- role: msg.role,
25
+ role: "user",
20
26
  content: msg.content.map(
21
27
  (part) => part.type === "text" ? { type: "text", text: part.text } : { type: "image", image: part.image }
22
28
  )
@@ -18,18 +18,11 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var api_exports = {};
20
20
  __export(api_exports, {
21
- DebugPauseSignal: () => import_pause.DebugPauseSignal,
22
- debugPause: () => import_pause.debugPause,
23
- isDebugPauseSignal: () => import_pause.isDebugPauseSignal,
24
21
  launchBrowser: () => import_browser.launchBrowser
25
22
  });
26
23
  module.exports = __toCommonJS(api_exports);
27
24
  var import_browser = require("./browser.js");
28
- var import_pause = require("../debug/pause.js");
29
25
  // Annotate the CommonJS export names for ESM import in node:
30
26
  0 && (module.exports = {
31
- DebugPauseSignal,
32
- debugPause,
33
- isDebugPauseSignal,
34
27
  launchBrowser
35
28
  });
@@ -1,3 +1,2 @@
1
1
  export { BrowserSession, LaunchBrowserArgs, launchBrowser } from './browser.cjs';
2
- export { DebugPauseContext, DebugPauseDetails, DebugPauseSignal, debugPause, isDebugPauseSignal } from '../debug/pause.cjs';
3
2
  import 'playwright';
@@ -1,3 +1,2 @@
1
1
  export { BrowserSession, LaunchBrowserArgs, launchBrowser } from './browser.js';
2
- export { DebugPauseContext, DebugPauseDetails, DebugPauseSignal, debugPause, isDebugPauseSignal } from '../debug/pause.js';
3
2
  import 'playwright';
@@ -1,12 +1,4 @@
1
1
  import { launchBrowser } from "./browser.js";
2
- import {
3
- debugPause,
4
- DebugPauseSignal,
5
- isDebugPauseSignal
6
- } from "../debug/pause.js";
7
2
  export {
8
- DebugPauseSignal,
9
- debugPause,
10
- isDebugPauseSignal,
11
3
  launchBrowser
12
4
  };
@@ -1,34 +1,21 @@
1
- import { Page, BrowserContext, Browser } from 'playwright';
1
+ import { Page } from 'playwright';
2
2
  import { MinimalLogger } from '../logger/logger.cjs';
3
3
 
4
4
  declare const LIBRETTO_WORKFLOW_BRAND: unique symbol;
5
- type LibrettoAuthProfile = {
6
- type: "local";
7
- domain: string;
8
- };
9
- type LibrettoWorkflowMetadata = {
10
- authProfile?: LibrettoAuthProfile;
11
- };
12
- type LibrettoWorkflowContext = {
13
- logger: MinimalLogger;
5
+ type LibrettoWorkflowMetadata = {};
6
+ type LibrettoWorkflowContext<S = {}> = {
14
7
  page: Page;
15
- context: BrowserContext;
16
- browser: Browser;
17
- session: string;
18
- integrationPath: string;
19
- exportName: string;
20
- headless: boolean;
21
- debug: boolean;
22
- pause: () => Promise<void>;
8
+ logger: MinimalLogger;
9
+ services: S;
23
10
  };
24
- type LibrettoWorkflowHandler<Input = unknown, Output = unknown> = (ctx: LibrettoWorkflowContext, input: Input) => Promise<Output>;
25
- declare class LibrettoWorkflow<Input = unknown, Output = unknown> {
11
+ type LibrettoWorkflowHandler<Input = unknown, Output = unknown, S = {}> = (ctx: LibrettoWorkflowContext<S>, input: Input) => Promise<Output>;
12
+ declare class LibrettoWorkflow<Input = unknown, Output = unknown, S = {}> {
26
13
  readonly [LIBRETTO_WORKFLOW_BRAND] = true;
27
14
  readonly metadata: LibrettoWorkflowMetadata;
28
15
  private readonly handler;
29
- constructor(metadata: LibrettoWorkflowMetadata, handler: LibrettoWorkflowHandler<Input, Output>);
30
- run(ctx: LibrettoWorkflowContext, input: Input): Promise<Output>;
16
+ constructor(metadata: LibrettoWorkflowMetadata, handler: LibrettoWorkflowHandler<Input, Output, S>);
17
+ run(ctx: LibrettoWorkflowContext<S>, input: Input): Promise<Output>;
31
18
  }
32
- declare function workflow<Input = unknown, Output = unknown>(metadata: LibrettoWorkflowMetadata, handler: LibrettoWorkflowHandler<Input, Output>): LibrettoWorkflow<Input, Output>;
19
+ declare function workflow<Input = unknown, Output = unknown, S = {}>(metadata: LibrettoWorkflowMetadata, handler: LibrettoWorkflowHandler<Input, Output, S>): LibrettoWorkflow<Input, Output, S>;
33
20
 
34
- export { LIBRETTO_WORKFLOW_BRAND, type LibrettoAuthProfile, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, type LibrettoWorkflowMetadata, workflow };
21
+ export { LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, type LibrettoWorkflowMetadata, workflow };
@@ -1,34 +1,21 @@
1
- import { Page, BrowserContext, Browser } from 'playwright';
1
+ import { Page } from 'playwright';
2
2
  import { MinimalLogger } from '../logger/logger.js';
3
3
 
4
4
  declare const LIBRETTO_WORKFLOW_BRAND: unique symbol;
5
- type LibrettoAuthProfile = {
6
- type: "local";
7
- domain: string;
8
- };
9
- type LibrettoWorkflowMetadata = {
10
- authProfile?: LibrettoAuthProfile;
11
- };
12
- type LibrettoWorkflowContext = {
13
- logger: MinimalLogger;
5
+ type LibrettoWorkflowMetadata = {};
6
+ type LibrettoWorkflowContext<S = {}> = {
14
7
  page: Page;
15
- context: BrowserContext;
16
- browser: Browser;
17
- session: string;
18
- integrationPath: string;
19
- exportName: string;
20
- headless: boolean;
21
- debug: boolean;
22
- pause: () => Promise<void>;
8
+ logger: MinimalLogger;
9
+ services: S;
23
10
  };
24
- type LibrettoWorkflowHandler<Input = unknown, Output = unknown> = (ctx: LibrettoWorkflowContext, input: Input) => Promise<Output>;
25
- declare class LibrettoWorkflow<Input = unknown, Output = unknown> {
11
+ type LibrettoWorkflowHandler<Input = unknown, Output = unknown, S = {}> = (ctx: LibrettoWorkflowContext<S>, input: Input) => Promise<Output>;
12
+ declare class LibrettoWorkflow<Input = unknown, Output = unknown, S = {}> {
26
13
  readonly [LIBRETTO_WORKFLOW_BRAND] = true;
27
14
  readonly metadata: LibrettoWorkflowMetadata;
28
15
  private readonly handler;
29
- constructor(metadata: LibrettoWorkflowMetadata, handler: LibrettoWorkflowHandler<Input, Output>);
30
- run(ctx: LibrettoWorkflowContext, input: Input): Promise<Output>;
16
+ constructor(metadata: LibrettoWorkflowMetadata, handler: LibrettoWorkflowHandler<Input, Output, S>);
17
+ run(ctx: LibrettoWorkflowContext<S>, input: Input): Promise<Output>;
31
18
  }
32
- declare function workflow<Input = unknown, Output = unknown>(metadata: LibrettoWorkflowMetadata, handler: LibrettoWorkflowHandler<Input, Output>): LibrettoWorkflow<Input, Output>;
19
+ declare function workflow<Input = unknown, Output = unknown, S = {}>(metadata: LibrettoWorkflowMetadata, handler: LibrettoWorkflowHandler<Input, Output, S>): LibrettoWorkflow<Input, Output, S>;
33
20
 
34
- export { LIBRETTO_WORKFLOW_BRAND, type LibrettoAuthProfile, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, type LibrettoWorkflowMetadata, workflow };
21
+ export { LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, type LibrettoWorkflowMetadata, workflow };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -105,9 +105,15 @@
105
105
  "zod": ">=3.0.0"
106
106
  },
107
107
  "peerDependenciesMeta": {
108
- "@ai-sdk/anthropic": { "optional": true },
109
- "@ai-sdk/google-vertex": { "optional": true },
110
- "@ai-sdk/openai": { "optional": true }
108
+ "@ai-sdk/anthropic": {
109
+ "optional": true
110
+ },
111
+ "@ai-sdk/google-vertex": {
112
+ "optional": true
113
+ },
114
+ "@ai-sdk/openai": {
115
+ "optional": true
116
+ }
111
117
  },
112
118
  "devDependencies": {
113
119
  "@ai-sdk/anthropic": "^3.0.53",
package/skill/SKILL.md CHANGED
@@ -1,6 +1,10 @@
1
1
  ---
2
2
  name: libretto
3
3
  description: "Browser automation CLI for building integrations, with a network-first approach.\n\nWHEN TO USE THIS SKILL:\n- When building a new integration or data extraction workflow against a website\n- When you need to interact with a web page (click, fill, navigate) rather than just read it\n- When debugging browser agent job failures (selectors timing out, clicks not working, elements not found)\n- When you need to test or prototype Playwright interactions before codifying them\n- When you need to save or restore login sessions for authenticated pages\n- When you need to understand what's on a page (use the snapshot command)\n- When scraping dynamic content that requires JavaScript execution\n\nWHEN NOT TO USE THIS SKILL:\n- When you only need to read static web content (use read_web_page instead)\n- When you need to modify browser agent source code (edit files directly)\n- When you need to run a full browser agent job end-to-end (use npx browser-agent CLI)"
4
+ license: MIT
5
+ metadata:
6
+ author: saffron-health
7
+ version: "0.2.2"
4
8
  ---
5
9
 
6
10
  # Browser Integration with Libretto CLI
@@ -41,16 +45,24 @@ Built-in sessions: `default`, `dev-server`, `browser-agent`.
41
45
 
42
46
  Add `--visualize` to any `exec` command to show a ghost cursor and element highlight before each action executes. Use it when the user wants to see what will be clicked/filled before it happens.
43
47
 
44
- ## Workflow Pause/Resume (`ctx.pause()`)
48
+ ## Workflow Pause/Resume (`pause()`)
45
49
 
46
- Workflows pause from inside the workflow function by calling `await ctx.pause()`.
50
+ Workflows pause by calling `await pause()` (imported from `"libretto"`). In production (`NODE_ENV=production`) it is a no-op.
47
51
 
48
52
  - There are no pause options to pass at call sites. Pause is session-scoped and resolved from the active session.
49
- - `npx libretto run ...` waits until the workflow either completes or hits the next `ctx.pause()`.
53
+ - `npx libretto run ...` waits until the workflow either completes or hits the next `pause()`.
50
54
  - On pause, the workflow process stays alive and keeps browser/session state.
51
55
  - `npx libretto resume --session <name>` sends resume signal and then waits until completion or the next pause.
52
56
  - For multi-pause workflows, call `resume` repeatedly until the workflow completes.
53
57
 
58
+ ## Workflow Failures and Reruns
59
+
60
+ - `npx libretto run` always uses the same failure-inspection behavior; no separate debug flag is needed.
61
+ - On workflow failure, Libretto prints the workflow error and keeps the browser open for inspection.
62
+ - After a failed run, use `npx libretto exec --session <name> "<code>"` to inspect or prototype fixes.
63
+ - Re-running `npx libretto run ... --session <name>` re-runs the workflow for that session.
64
+ - If the same session still has a failed workflow worker, Libretto releases that failed worker process before rerunning.
65
+
54
66
  ## Globals Available in `exec`
55
67
 
56
68
  `page`, `context`, `state`, `browser`, `networkLog({ last?, filter?, method? })`, `actionLog({ last?, filter?, action?, source? })`, `console`, `fetch`, `Buffer`, `URL`, `setTimeout`
@@ -214,7 +226,8 @@ When the snapshot doesn't give you enough detail — why an element is hidden, w
214
226
 
215
227
  - **Never use `page.screenshot()` via `exec`.** Use `npx libretto snapshot` instead — it captures the viewport, sends the screenshot + HTML to a vision model, and returns actionable selectors. The `fullPage` option is especially dangerous — it scrolls the entire page to stitch a screenshot, which can crash JavaScript-heavy pages (especially EMR portals like eClinicalWorks).
216
228
  - **Never run `exec` commands in parallel.** Always wait for one `exec` to finish before starting the next. Do not use `run_in_background` for `exec` calls. Running simultaneous `exec` calls opens multiple CDP connections to the same page, which corrupts the page state and kills the browser.
217
- - `open` and `run` require an available session. If the session is already active, Libretto fails fast and asks you to close the existing session or use a different `--session`.
229
+ - `open` requires an available session. If the session is already active, Libretto fails fast and asks you to close the existing session or use a different `--session`.
230
+ - `run` also requires an available session, except for the specific case of a prior failed `run` in the same session; in that case Libretto releases the failed worker and allows rerun.
218
231
  - Use `return <value>` in `exec` to print results. Strings print raw; objects print as JSON.
219
232
  - For iframe content, access via `page.locator('iframe[name="..."]').contentFrame()`.
220
233
  - Multiple sessions allow parallel browser instances: `--session test1`, `--session test2`.
@@ -313,7 +326,7 @@ The browser stays open indefinitely until explicitly closed with `npx libretto c
313
326
 
314
327
  If the site requires login, ask the user how auth should work in the generated workflow:
315
328
 
316
- 1. Save a local profile (recommended for local runs): open in `--headed`, have the user log in manually, run `npx libretto save <domain>`, and generate workflow metadata with `authProfile: { type: "local", domain: "<hostname>" }`.
329
+ 1. Save a local profile (recommended for local runs): open in `--headed`, have the user log in manually, run `npx libretto save <domain>`, and pass `--auth-profile <domain>` when running the workflow (e.g. `npx libretto run ./file.ts main --auth-profile example.com`).
317
330
  2. Use user-managed credential logic in Playwright code (no local profile dependency).
318
331
 
319
332
  If local profile is chosen, include this warning in your generated workflow guidance: local profiles are machine-local (other users/environments will not have them), and sessions can expire so re-login/re-save may be required.
@@ -7,7 +7,7 @@ These rules apply when generating production TypeScript files from interactive b
7
7
  Generated files must export a `workflow()` instance so they can be run via `npx libretto run <file> <exportName>`. Import `workflow` and its types from `"libretto"`:
8
8
 
9
9
  ```typescript
10
- import { workflow, type LibrettoWorkflowContext } from "libretto";
10
+ import { workflow, pause, type LibrettoWorkflowContext } from "libretto";
11
11
 
12
12
  type Input = {
13
13
  // Define the expected input shape — passed via --params JSON
@@ -21,15 +21,11 @@ type Output = {
21
21
  };
22
22
 
23
23
  export const myWorkflow = workflow<Input, Output>(
24
- {
25
- // If the site requires a saved login session:
26
- authProfile: { type: "local", domain: "example.com" },
27
- // Omit authProfile if no login is needed
28
- },
29
- async (ctx: LibrettoWorkflowContext, input: Input): Promise<Output> => {
24
+ {},
25
+ async (ctx, input): Promise<Output> => {
30
26
  const { page } = ctx;
31
27
 
32
- // workflow logic here — use ctx.page, ctx.context, ctx.browser
28
+ // workflow logic here — use ctx.page, ctx.logger, ctx.services
33
29
  await page.goto("https://example.com");
34
30
  // ...
35
31
 
@@ -41,11 +37,48 @@ export const myWorkflow = workflow<Input, Output>(
41
37
  **Key points:**
42
38
 
43
39
  - The named export (e.g., `myWorkflow`) is what you pass as the second arg to `npx libretto run ./file.ts myWorkflow`
44
- - `ctx` provides `page`, `context`, `browser`, `session`, `logger`, `headless`, `integrationPath`, `exportName`
40
+ - `ctx` provides `page`, `logger`, and `services` (generic, default `{}`)
45
41
  - `input` comes from `--params '{"query":"foo"}'` or `--params-file params.json` on the CLI
46
- - If `authProfile` is set with a domain, libretto loads the saved browser profile for that domain (created via `npx libretto save <domain>`)
42
+ - If the site requires a saved login session, pass `--auth-profile <domain>` to the CLI (created via `npx libretto save <domain>`)
43
+ - Use `await pause()` (imported from `"libretto"`) to pause the workflow for debugging. It is a no-op in production.
47
44
  - The browser is launched and closed automatically by the CLI — do not launch or close it in the handler
48
45
 
46
+ ## Passing Application Dependencies via Services
47
+
48
+ Use the third generic on `workflow<Input, Output, Services>` to inject
49
+ dependencies that exist in your application but not in libretto's runtime
50
+ (DB transactions, API clients, caches, etc.):
51
+
52
+ ```typescript
53
+ import { type Transaction } from "./db";
54
+
55
+ type MyServices = { tx?: Transaction };
56
+
57
+ export const myWorkflow = workflow<Input, Output, MyServices>(
58
+ {},
59
+ async (ctx, input) => {
60
+ if (ctx.services.tx) {
61
+ await ctx.services.tx.insert(/* ... */);
62
+ } else {
63
+ ctx.logger.info("No DB transaction — skipping write");
64
+ }
65
+ // ... browser automation ...
66
+ },
67
+ );
68
+ ```
69
+
70
+ In production, the caller passes services when invoking `.run()`:
71
+
72
+ ```typescript
73
+ await myWorkflow.run(
74
+ { page, logger, services: { tx } },
75
+ input,
76
+ );
77
+ ```
78
+
79
+ When running standalone via `npx libretto run`, services defaults to `{}`,
80
+ so mark fields optional for anything unavailable in that context.
81
+
49
82
  ## Playwright Locators for DOM Interaction
50
83
 
51
84
  Generated code must use Playwright locator APIs for all DOM interactions. Do not use `page.evaluate()` with `document.querySelector`, `querySelectorAll`, `textContent`, `click()`, or other DOM APIs when a Playwright locator can do the same thing.