/ src / pipeline / steps / browser.ts
browser.ts
 1  /**
 2   * Pipeline step: navigate, click, type, wait, press, snapshot.
 3   * Browser interaction primitives.
 4   */
 5  
 6  import type { IPage } from '../../types.js';
 7  import { render } from '../template.js';
 8  
 9  import { isRecord } from '../../utils.js';
10  
11  export async function stepNavigate(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
12    if (isRecord(params) && 'url' in params) {
13      const url = String(render(params.url, { args, data }));
14      await page!.goto(url, { waitUntil: params.waitUntil as 'load' | 'none' | undefined, settleMs: typeof params.settleMs === 'number' ? params.settleMs : undefined });
15    } else {
16      const url = render(params, { args, data });
17      await page!.goto(String(url));
18    }
19    return data;
20  }
21  
22  export async function stepClick(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
23    await page!.click(String(render(params, { args, data })).replace(/^@/, ''));
24    return data;
25  }
26  
27  export async function stepType(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
28    if (isRecord(params)) {
29      const ref = String(render(params.ref ?? '', { args, data })).replace(/^@/, '');
30      const text = String(render(params.text ?? '', { args, data }));
31      await page!.typeText(ref, text);
32      if (params.submit) await page!.pressKey('Enter');
33    }
34    return data;
35  }
36  
37  export async function stepWait(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
38    if (typeof params === 'number') await page!.wait(params);
39    else if (isRecord(params)) {
40      if ('text' in params) {
41        await page!.wait({
42          text: String(render(params.text, { args, data })),
43          timeout: typeof params.timeout === 'number' ? params.timeout : undefined,
44        });
45      } else if ('time' in params) await page!.wait(Number(params.time));
46    } else if (typeof params === 'string') await page!.wait(Number(render(params, { args, data })));
47    return data;
48  }
49  
50  export async function stepPress(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
51    await page!.pressKey(String(render(params, { args, data })));
52    return data;
53  }
54  
55  export async function stepSnapshot(page: IPage | null, params: unknown, _data: unknown, _args: Record<string, unknown>): Promise<unknown> {
56    const opts = isRecord(params) ? params : {};
57    return page!.snapshot({
58      interactive: typeof opts.interactive === 'boolean' ? opts.interactive : false,
59      compact: typeof opts.compact === 'boolean' ? opts.compact : false,
60      maxDepth: typeof opts.max_depth === 'number' ? opts.max_depth : undefined,
61      raw: typeof opts.raw === 'boolean' ? opts.raw : false,
62    });
63  }
64  
65  export async function stepEvaluate(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
66    const js = String(render(params, { args, data }));
67    let result: unknown = await page!.evaluate(js);
68    // Browser may return JSON as a string — auto-parse it
69    if (typeof result === 'string') {
70      const trimmed = result.trim();
71      if ((trimmed.startsWith('[') && trimmed.endsWith(']')) || (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
72        try { result = JSON.parse(trimmed); } catch {}
73      }
74    }
75    return result;
76  }