/ src / pipeline / steps / intercept.ts
intercept.ts
 1  /**
 2   * Pipeline step: intercept — declarative XHR interception.
 3   */
 4  
 5  import type { IPage } from '../../types.js';
 6  import { render, normalizeEvaluateSource } from '../template.js';
 7  
 8  interface InterceptParams {
 9    trigger?: string;
10    capture?: string;
11    timeout?: number;
12    select?: string;
13  }
14  
15  export async function stepIntercept(
16    page: IPage | null,
17    params: unknown,
18    data: unknown,
19    args: Record<string, unknown>,
20  ): Promise<unknown> {
21    const cfg: InterceptParams = typeof params === 'object' && params !== null ? (params as InterceptParams) : {};
22    const trigger = cfg.trigger ?? '';
23    const capturePattern = cfg.capture ?? '';
24    const timeout = cfg.timeout ?? 8;
25    const selectPath = cfg.select ?? null;
26  
27    if (!capturePattern) return data;
28  
29    // Step 1: Inject fetch/XHR interceptor BEFORE trigger
30    await page!.installInterceptor(capturePattern);
31  
32    // Step 2: Execute the trigger action
33    if (trigger.startsWith('navigate:')) {
34      const url = render(trigger.slice('navigate:'.length), { args, data });
35      await page!.goto(String(url));
36    } else if (trigger.startsWith('evaluate:')) {
37      const js = trigger.slice('evaluate:'.length);
38      await page!.evaluate(normalizeEvaluateSource(render(js, { args, data }) as string));
39    } else if (trigger.startsWith('click:')) {
40      const ref = render(trigger.slice('click:'.length), { args, data });
41      await page!.click(String(ref).replace(/^@/, ''));
42    } else if (trigger === 'scroll') {
43      await page!.scroll('down');
44    }
45  
46    // Step 3: Wait for network capture (event-driven, not fixed sleep)
47    await page!.waitForCapture(timeout);
48  
49    // Step 4: Retrieve captured data
50    const matchingResponses = await page!.getInterceptedRequests();
51  
52    // Step 5: Select from response if specified
53    let result: unknown = matchingResponses.length === 1 ? matchingResponses[0] :
54                 matchingResponses.length > 1 ? matchingResponses : data;
55  
56    if (selectPath && result) {
57      let current: unknown = result;
58      for (const part of String(selectPath).split('.')) {
59        if (current && typeof current === 'object' && !Array.isArray(current)) {
60          current = (current as Record<string, unknown>)[part];
61        } else break;
62      }
63      result = current ?? result;
64    }
65  
66    return result;
67  }