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 });