/ src / pipeline / executor.test.ts
executor.test.ts
  1  /**
  2   * Tests for pipeline/executor.ts: pipeline execution with mock page.
  3   */
  4  
  5  import { describe, it, expect, vi } from 'vitest';
  6  import { executePipeline } from './index.js';
  7  import { ConfigError } from '../errors.js';
  8  import type { IPage } from '../types.js';
  9  
 10  /** Create a minimal mock page for testing */
 11  function createMockPage(overrides: Partial<IPage> = {}): IPage {
 12    return {
 13      goto: vi.fn(),
 14      evaluate: vi.fn().mockResolvedValue(null),
 15      getCookies: vi.fn().mockResolvedValue([]),
 16      snapshot: vi.fn().mockResolvedValue(''),
 17      click: vi.fn(),
 18      typeText: vi.fn(),
 19      pressKey: vi.fn(),
 20      getFormState: vi.fn().mockResolvedValue({}),
 21      wait: vi.fn(),
 22      tabs: vi.fn().mockResolvedValue([]),
 23      selectTab: vi.fn(),
 24      networkRequests: vi.fn().mockResolvedValue([]),
 25      consoleMessages: vi.fn().mockResolvedValue(''),
 26      scroll: vi.fn(),
 27      scrollTo: vi.fn(),
 28      autoScroll: vi.fn(),
 29      installInterceptor: vi.fn(),
 30      getInterceptedRequests: vi.fn().mockResolvedValue([]),
 31      screenshot: vi.fn().mockResolvedValue(''),
 32      waitForCapture: vi.fn().mockResolvedValue(undefined),
 33      ...overrides,
 34    };
 35  }
 36  
 37  describe('executePipeline', () => {
 38    it('returns null for empty pipeline', async () => {
 39      const result = await executePipeline(null, []);
 40      expect(result).toBeNull();
 41    });
 42  
 43    it('skips null/invalid steps', async () => {
 44      const result = await executePipeline(null, [null, undefined, 42]);
 45      expect(result).toBeNull();
 46    });
 47  
 48    it('executes navigate step', async () => {
 49      const page = createMockPage();
 50      await executePipeline(page, [
 51        { navigate: 'https://example.com' },
 52      ]);
 53      expect(page.goto).toHaveBeenCalledWith('https://example.com');
 54    });
 55  
 56    it('executes evaluate + select pipeline', async () => {
 57      const page = createMockPage({
 58        evaluate: vi.fn().mockResolvedValue({ data: { list: [{ name: 'a' }, { name: 'b' }] } }),
 59      });
 60      const result = await executePipeline(page, [
 61        { evaluate: '() => ({ data: { list: [{name: "a"}, {name: "b"}] } })' },
 62        { select: 'data.list' },
 63      ]);
 64      expect(result).toEqual([{ name: 'a' }, { name: 'b' }]);
 65    });
 66  
 67    it('executes map step to transform items', async () => {
 68      const page = createMockPage({
 69        evaluate: vi.fn().mockResolvedValue([
 70          { title: 'Hello', count: 10 },
 71          { title: 'World', count: 20 },
 72        ]),
 73      });
 74      const result = await executePipeline(page, [
 75        { evaluate: 'test' },
 76        { map: { name: '${{ item.title }}', score: '${{ item.count }}' } },
 77      ]);
 78      expect(result).toEqual([
 79        { name: 'Hello', score: 10 },
 80        { name: 'World', score: 20 },
 81      ]);
 82    });
 83  
 84    it('runs inline select inside map step', async () => {
 85      const page = createMockPage({
 86        evaluate: vi.fn().mockResolvedValue({
 87          posts: [
 88            { title: 'First', rank: 1 },
 89            { title: 'Second', rank: 2 },
 90          ],
 91        }),
 92      });
 93      const result = await executePipeline(page, [
 94        { evaluate: 'test' },
 95        {
 96          map: {
 97            select: 'posts',
 98            title: '${{ item.title }}',
 99            rank: '${{ item.rank }}',
100          },
101        },
102      ]);
103  
104      expect(result).toEqual([
105        { title: 'First', rank: 1 },
106        { title: 'Second', rank: 2 },
107      ]);
108    });
109  
110    it('executes limit step', async () => {
111      const page = createMockPage({
112        evaluate: vi.fn().mockResolvedValue([1, 2, 3, 4, 5]),
113      });
114      const result = await executePipeline(page, [
115        { evaluate: 'test' },
116        { limit: '3' },
117      ]);
118      expect(result).toEqual([1, 2, 3]);
119    });
120  
121    it('executes sort step', async () => {
122      const page = createMockPage({
123        evaluate: vi.fn().mockResolvedValue([{ n: 3 }, { n: 1 }, { n: 2 }]),
124      });
125      const result = await executePipeline(page, [
126        { evaluate: 'test' },
127        { sort: { by: 'n', order: 'asc' } },
128      ]);
129      expect(result).toEqual([{ n: 1 }, { n: 2 }, { n: 3 }]);
130    });
131  
132    it('executes sort step with desc order', async () => {
133      const page = createMockPage({
134        evaluate: vi.fn().mockResolvedValue([{ n: 1 }, { n: 3 }, { n: 2 }]),
135      });
136      const result = await executePipeline(page, [
137        { evaluate: 'test' },
138        { sort: { by: 'n', order: 'desc' } },
139      ]);
140      expect(result).toEqual([{ n: 3 }, { n: 2 }, { n: 1 }]);
141    });
142  
143    it('executes wait step with number', async () => {
144      const page = createMockPage();
145      await executePipeline(page, [
146        { wait: 2 },
147      ]);
148      expect(page.wait).toHaveBeenCalledWith(2);
149    });
150  
151    it('fails fast on unknown steps', async () => {
152      await expect(executePipeline(null, [
153        { unknownStep: 'test' },
154      ], { debug: true })).rejects.toBeInstanceOf(ConfigError);
155      await expect(executePipeline(null, [
156        { unknownStep: 'test' },
157      ], { debug: true })).rejects.toThrow('Unknown pipeline step "unknownStep"');
158    });
159  
160    it('passes args through template rendering', async () => {
161      const page = createMockPage({
162        evaluate: vi.fn().mockResolvedValue([1, 2, 3, 4, 5]),
163      });
164      const result = await executePipeline(page, [
165        { evaluate: 'test' },
166        { limit: '${{ args.count }}' },
167      ], { args: { count: 2 } });
168      expect(result).toEqual([1, 2]);
169    });
170  
171    it('click step calls page.click', async () => {
172      const page = createMockPage();
173      await executePipeline(page, [
174        { click: '@5' },
175      ]);
176      expect(page.click).toHaveBeenCalledWith('5');
177    });
178  
179    it('navigate preserves existing data through pipeline', async () => {
180      const page = createMockPage({
181        evaluate: vi.fn().mockResolvedValue([{ a: 1 }]),
182      });
183      const result = await executePipeline(page, [
184        { evaluate: 'test' },
185        { navigate: 'https://example.com' },
186      ]);
187      // navigate should preserve existing data
188      expect(result).toEqual([{ a: 1 }]);
189      expect(page.goto).toHaveBeenCalledWith('https://example.com');
190    });
191  });