@epsilon-asi/actors 0.0.1 → 0.0.3

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.
Files changed (112) hide show
  1. package/package.json +12 -3
  2. package/.ai/generators/_template.ts +0 -37
  3. package/.ai/generators/abstract.ts +0 -24
  4. package/.ai/generators/actor-task-form-filler.ts +0 -140
  5. package/.ai/generators/actor-task.ts +0 -122
  6. package/.ai/generators/auth-core.ts +0 -126
  7. package/.ai/generators/browser-runtime.ts +0 -114
  8. package/.ai/generators/cli-command.ts +0 -96
  9. package/.ai/generators/core-framework.ts +0 -80
  10. package/.ai/generators/docs.ts +0 -92
  11. package/.ai/generators/error-logging.ts +0 -102
  12. package/.ai/generators/extraction-helper.ts +0 -96
  13. package/.ai/generators/interaction-behavior.ts +0 -129
  14. package/.ai/generators/site-actor.ts +0 -125
  15. package/.ai/generators/site-login-flow.ts +0 -117
  16. package/.ai/generators/unit-test.ts +0 -109
  17. package/.ai/workflows/_template.ts +0 -20
  18. package/.ai/workflows/starter.ts +0 -20
  19. package/ai-gen.config.ts +0 -67
  20. package/src/auth/AuthStateDetector.ts +0 -18
  21. package/src/auth/CredentialsProvider.ts +0 -48
  22. package/src/auth/LoginFlow.ts +0 -332
  23. package/src/auth/LoginFlow.types.ts +0 -141
  24. package/src/auth/SessionStore.ts +0 -21
  25. package/src/auth/index.ts +0 -5
  26. package/src/browser/BrowserFactory.ts +0 -253
  27. package/src/browser/BrowserSession.ts +0 -50
  28. package/src/browser/PuppeteerLike.ts +0 -65
  29. package/src/browser/RuntimeConfig.ts +0 -152
  30. package/src/browser/index.ts +0 -5
  31. package/src/browser/profileValidation.ts +0 -73
  32. package/src/cli/run.ts +0 -112
  33. package/src/core/Actor.ts +0 -167
  34. package/src/core/ActorContext.ts +0 -34
  35. package/src/core/ActorRegistry.ts +0 -26
  36. package/src/core/ActorRunner.ts +0 -240
  37. package/src/core/defineActor.ts +0 -5
  38. package/src/core/index.ts +0 -5
  39. package/src/errors/AuthError.ts +0 -7
  40. package/src/errors/AutomationError.ts +0 -26
  41. package/src/errors/ConfigError.ts +0 -7
  42. package/src/errors/ExtractionError.ts +0 -7
  43. package/src/errors/NavigationError.ts +0 -7
  44. package/src/errors/SelectorError.ts +0 -10
  45. package/src/errors/index.ts +0 -6
  46. package/src/extraction/Extractor.ts +0 -65
  47. package/src/extraction/Pagination.ts +0 -47
  48. package/src/extraction/index.ts +0 -2
  49. package/src/index.ts +0 -9
  50. package/src/interaction/FieldClearer.ts +0 -73
  51. package/src/interaction/Forms.ts +0 -27
  52. package/src/interaction/GhostCursorAdapter.ts +0 -79
  53. package/src/interaction/HumanInteractor.ts +0 -32
  54. package/src/interaction/HumanTyping.ts +0 -157
  55. package/src/interaction/NativePuppeteerInteractor.ts +0 -68
  56. package/src/interaction/Navigation.ts +0 -37
  57. package/src/interaction/PageAdapter.ts +0 -86
  58. package/src/interaction/Waits.ts +0 -5
  59. package/src/interaction/index.ts +0 -9
  60. package/src/logging/ConsoleLogger.ts +0 -44
  61. package/src/logging/Logger.ts +0 -15
  62. package/src/logging/MemoryLogger.ts +0 -34
  63. package/src/logging/NullLogger.ts +0 -8
  64. package/src/logging/index.ts +0 -4
  65. package/src/sites/example/example.actor.ts +0 -53
  66. package/src/sites/example/example.selectors.ts +0 -17
  67. package/src/sites/example/example.types.ts +0 -18
  68. package/src/sites/example/index.ts +0 -3
  69. package/src/sites/index.ts +0 -3
  70. package/src/sites/myvistage-com/index.ts +0 -3
  71. package/src/sites/myvistage-com/login-action-list.json +0 -349
  72. package/src/sites/myvistage-com/myvistage-com.actor.ts +0 -50
  73. package/src/sites/myvistage-com/myvistage-com.selectors.ts +0 -14
  74. package/src/sites/myvistage-com/myvistage-com.types.ts +0 -18
  75. package/src/sites/myvistage-com/post-comment-action.json +0 -81
  76. package/src/sites/upwork-com/index.ts +0 -6
  77. package/src/sites/upwork-com/upwork-com.actor.ts +0 -97
  78. package/src/sites/upwork-com/upwork-com.runner.ts +0 -17
  79. package/src/sites/upwork-com/upwork-com.selectors.ts +0 -10
  80. package/src/sites/upwork-com/upwork-com.types.ts +0 -102
  81. package/src/sites/upwork-com/upwork-com.util.ts +0 -41
  82. package/src/utils/delay.ts +0 -4
  83. package/src/utils/index.ts +0 -5
  84. package/src/utils/invariant.ts +0 -7
  85. package/src/utils/redact.ts +0 -53
  86. package/src/utils/retry.ts +0 -31
  87. package/src/utils/url.ts +0 -7
  88. package/tests/fixtures/FakeCredentialsProvider.ts +0 -12
  89. package/tests/fixtures/FakeCursor.ts +0 -48
  90. package/tests/fixtures/FakePage.ts +0 -266
  91. package/tests/fixtures/makeContext.ts +0 -76
  92. package/tests/unit/auth/AuthStateDetector.test.ts +0 -80
  93. package/tests/unit/auth/LoginFlow.test.ts +0 -296
  94. package/tests/unit/browser/BrowserFactory.test.ts +0 -370
  95. package/tests/unit/core/ActorRunner.test.ts +0 -370
  96. package/tests/unit/core/defineActor.test.ts +0 -112
  97. package/tests/unit/extraction/Extractor.test.ts +0 -48
  98. package/tests/unit/extraction/Pagination.test.ts +0 -54
  99. package/tests/unit/interaction/FieldClearer.test.ts +0 -29
  100. package/tests/unit/interaction/Forms.test.ts +0 -35
  101. package/tests/unit/interaction/GhostCursorAdapter.test.ts +0 -68
  102. package/tests/unit/interaction/HumanTyping.test.ts +0 -54
  103. package/tests/unit/interaction/NativePuppeteerInteractor.test.ts +0 -22
  104. package/tests/unit/interaction/PageAdapter.test.ts +0 -25
  105. package/tests/unit/logging/redact.test.ts +0 -36
  106. package/tests/unit/sites/myvistage-com.actor.test.ts +0 -19
  107. package/tests/unit/sites/myvistage-com.login.test.ts +0 -22
  108. package/tests/unit/sites/myvistage-com.postComment.test.ts +0 -70
  109. package/tests/unit/sites/upwork-com.login.test.ts +0 -52
  110. package/tsconfig.build.json +0 -9
  111. package/tsconfig.json +0 -22
  112. package/vitest.config.ts +0 -12
@@ -1,370 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { describe, expect, it } from 'vitest';
5
- import type { LoginFlowDefinition } from '../../../src/auth/LoginFlow.types.js';
6
- import { BrowserFactory } from '../../../src/browser/BrowserFactory.js';
7
- import { BrowserSession } from '../../../src/browser/BrowserSession.js';
8
- import type { RuntimeConfig } from '../../../src/browser/RuntimeConfig.js';
9
- import { defineActor } from '../../../src/core/defineActor.js';
10
- import { ActorRunner, ActorSequenceRunner } from '../../../src/core/ActorRunner.js';
11
- import { defineFormTask } from '../../../src/core/Actor.js';
12
- import { AutomationError } from '../../../src/errors/AutomationError.js';
13
- import { PuppeteerPageAdapter } from '../../../src/interaction/PageAdapter.js';
14
- import { MemoryLogger } from '../../../src/logging/MemoryLogger.js';
15
- import { FakeHumanInteractor } from '../../fixtures/FakeCursor.js';
16
- import { FakeBrowser, FakePage } from '../../fixtures/FakePage.js';
17
-
18
- class StaticBrowserFactory extends BrowserFactory {
19
- launchCalls = 0;
20
-
21
- constructor(private readonly session: BrowserSession) {
22
- super();
23
- }
24
-
25
- override async launch(): Promise<BrowserSession> {
26
- this.launchCalls += 1;
27
- return this.session;
28
- }
29
- }
30
-
31
- class FakeLoginFlow {
32
- ensureAuthenticatedCalls: LoginFlowDefinition[] = [];
33
-
34
- async ensureAuthenticated(_context: unknown, definition: LoginFlowDefinition): Promise<void> {
35
- this.ensureAuthenticatedCalls.push(definition);
36
- }
37
- }
38
-
39
- function createConfig(closeBrowserOnFinish = true): RuntimeConfig {
40
- const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'paf-runner-'));
41
- fs.mkdirSync(path.join(userDataDir, 'Default'));
42
- return {
43
- browser: {
44
- mode: 'existing-profile',
45
- userDataDir,
46
- profileDirectory: 'Default',
47
- headless: false,
48
- closeBrowserOnFinish
49
- }
50
- };
51
- }
52
-
53
- function createRemoteConfig(options: { closeBrowserOnFinish?: boolean; disconnectOnFinish?: boolean } = {}): RuntimeConfig {
54
- return {
55
- browser: {
56
- mode: 'remote-debugging',
57
- browserURL: 'http://127.0.0.1:9222',
58
- ...options
59
- }
60
- };
61
- }
62
-
63
- function createRunnerParts(closeBrowserOnFinish = true, overrideConfig?: RuntimeConfig): {
64
- runner: ActorRunner;
65
- page: FakePage;
66
- browser: FakeBrowser;
67
- browserFactory: StaticBrowserFactory;
68
- loginFlow: FakeLoginFlow;
69
- interactor: FakeHumanInteractor;
70
- } {
71
- const config = overrideConfig ?? createConfig(closeBrowserOnFinish);
72
- const page = new FakePage();
73
- const browser = new FakeBrowser([page]);
74
- const session = new BrowserSession(browser, browser.context, page, config.browser, config.browser.mode === 'remote-debugging' ? 'connected' : 'launched');
75
- const browserFactory = new StaticBrowserFactory(session);
76
- const loginFlow = new FakeLoginFlow();
77
- const interactor = new FakeHumanInteractor();
78
- const runner = new ActorRunner({
79
- config,
80
- browserFactory,
81
- loginFlow: loginFlow as never,
82
- logger: new MemoryLogger(),
83
- interactorFactory: () => interactor,
84
- pageAdapterFactory: pageLike => new PuppeteerPageAdapter(pageLike)
85
- });
86
- return { runner, page, browser, browserFactory, loginFlow, interactor };
87
- }
88
-
89
- function createSequenceRunnerParts(closeBrowserOnFinish = true, overrideConfig?: RuntimeConfig): {
90
- runner: ActorSequenceRunner;
91
- page: FakePage;
92
- browser: FakeBrowser;
93
- browserFactory: StaticBrowserFactory;
94
- loginFlow: FakeLoginFlow;
95
- } {
96
- const config = overrideConfig ?? createConfig(closeBrowserOnFinish);
97
- const page = new FakePage();
98
- const browser = new FakeBrowser([page]);
99
- const session = new BrowserSession(browser, browser.context, page, config.browser, config.browser.mode === 'remote-debugging' ? 'connected' : 'launched');
100
- const browserFactory = new StaticBrowserFactory(session);
101
- const loginFlow = new FakeLoginFlow();
102
- const runner = new ActorSequenceRunner({
103
- config,
104
- browserFactory,
105
- loginFlow: loginFlow as never,
106
- logger: new MemoryLogger(),
107
- interactorFactory: () => new FakeHumanInteractor(),
108
- pageAdapterFactory: pageLike => new PuppeteerPageAdapter(pageLike)
109
- });
110
- return { runner, page, browser, browserFactory, loginFlow };
111
- }
112
-
113
- describe('ActorRunner', () => {
114
- const auth = {
115
- loginUrl: '/login',
116
- selectors: {
117
- username: '#email',
118
- password: '#password',
119
- submit: '#submit',
120
- loggedInSignal: '#account'
121
- },
122
- credentials: { id: 'example' }
123
- } satisfies LoginFlowDefinition;
124
-
125
- it('creates actor context and runs the selected task', async () => {
126
- const { runner, browserFactory, browser } = createRunnerParts();
127
- const actor = defineActor({
128
- id: 'example',
129
- baseUrl: 'https://example.com',
130
- tasks: {
131
- hello: async context => ({ actorId: context.actor.id, url: context.page.url() })
132
- }
133
- });
134
-
135
- const result = await runner.run(actor, 'hello');
136
-
137
- expect(result).toEqual({ actorId: 'example', url: 'about:blank' });
138
- expect(browserFactory.launchCalls).toBe(1);
139
- expect(browser.closeCalls).toHaveLength(1);
140
- });
141
-
142
- it('runs auth when the actor defines a login flow', async () => {
143
- const { runner, loginFlow } = createRunnerParts();
144
- const actor = defineActor({
145
- id: 'example',
146
- baseUrl: 'https://example.com',
147
- auth,
148
- tasks: {
149
- scrape: async () => 'done'
150
- }
151
- });
152
-
153
- await expect(runner.run(actor, 'scrape')).resolves.toBe('done');
154
- expect(loginFlow.ensureAuthenticatedCalls).toEqual([auth]);
155
- });
156
-
157
- it('runs generated form tasks against actor context services', async () => {
158
- const { runner, page, interactor } = createRunnerParts();
159
- page
160
- .setSelector('#name')
161
- .setSelector('#email')
162
- .setSelector('#submit');
163
-
164
- const actor = defineActor({
165
- id: 'example',
166
- baseUrl: 'https://example.com',
167
- tasks: {
168
- submitContact: defineFormTask<{ name: string; email: string }, { submitted: string }>({
169
- url: '/contact',
170
- fields: [
171
- { selector: '#name', inputKey: 'name' },
172
- { selector: '#email', inputKey: 'email' }
173
- ],
174
- submit: {
175
- selector: '#submit',
176
- waitForNavigation: true
177
- },
178
- onComplete: (_context, input) => ({ submitted: input.email })
179
- })
180
- }
181
- });
182
-
183
- const result = await runner.run(actor, 'submitContact', {
184
- name: 'Jane Doe',
185
- email: 'jane@example.com'
186
- });
187
-
188
- expect(result).toEqual({ submitted: 'jane@example.com' });
189
- expect(page.gotos[0]?.url).toBe('https://example.com/contact');
190
- expect(interactor.typed).toEqual([
191
- { selector: '#name', value: 'Jane Doe', options: { required: true } },
192
- { selector: '#email', value: 'jane@example.com', options: { required: true } }
193
- ]);
194
- expect(interactor.clicks).toEqual([{ selector: '#submit' }]);
195
- expect(page.waitedNavigations).toHaveLength(1);
196
- });
197
-
198
- it('disconnects a launched browser without closing when closeBrowserOnFinish is false', async () => {
199
- const { runner, browser } = createRunnerParts(false);
200
- const actor = defineActor({
201
- id: 'example',
202
- baseUrl: 'https://example.com',
203
- tasks: {
204
- noop: async () => undefined
205
- }
206
- });
207
-
208
- await runner.run(actor, 'noop');
209
-
210
- expect(browser.closeCalls).toHaveLength(0);
211
- expect(browser.disconnectCalls).toHaveLength(1);
212
- });
213
-
214
- it('disconnects from a shared connected browser by default without closing Chrome', async () => {
215
- const { runner, browser } = createRunnerParts(true, createRemoteConfig());
216
- const actor = defineActor({
217
- id: 'example',
218
- baseUrl: 'https://example.com',
219
- tasks: {
220
- noop: async () => undefined
221
- }
222
- });
223
-
224
- await runner.run(actor, 'noop');
225
-
226
- expect(browser.disconnectCalls).toHaveLength(1);
227
- expect(browser.closeCalls).toHaveLength(0);
228
- });
229
-
230
- it('can close the connected browser when explicitly configured', async () => {
231
- const { runner, browser } = createRunnerParts(true, createRemoteConfig({ closeBrowserOnFinish: true, disconnectOnFinish: false }));
232
- const actor = defineActor({
233
- id: 'example',
234
- baseUrl: 'https://example.com',
235
- tasks: {
236
- noop: async () => undefined
237
- }
238
- });
239
-
240
- await runner.run(actor, 'noop');
241
-
242
- expect(browser.closeCalls).toHaveLength(1);
243
- expect(browser.disconnectCalls).toHaveLength(0);
244
- });
245
-
246
- it('wraps task failures with actor metadata and still closes the browser', async () => {
247
- const { runner, browser } = createRunnerParts();
248
- const actor = defineActor({
249
- id: 'example',
250
- baseUrl: 'https://example.com',
251
- tasks: {
252
- explode: async () => {
253
- throw new Error('boom');
254
- }
255
- }
256
- });
257
-
258
- await expect(runner.run(actor, 'explode')).rejects.toMatchObject({
259
- name: 'AutomationError',
260
- actorId: 'example',
261
- taskName: 'explode'
262
- } satisfies Partial<AutomationError>);
263
- expect(browser.closeCalls).toHaveLength(1);
264
- });
265
-
266
- it('calls actor hooks around task execution', async () => {
267
- const calls: string[] = [];
268
- const { runner } = createRunnerParts();
269
- const actor = defineActor({
270
- id: 'example',
271
- baseUrl: 'https://example.com',
272
- hooks: {
273
- beforeRun: () => { calls.push('before'); },
274
- afterRun: () => { calls.push('after'); }
275
- },
276
- tasks: {
277
- task: async () => 'ok'
278
- }
279
- });
280
-
281
- await runner.run(actor, 'task');
282
-
283
- expect(calls).toEqual(['before', 'after']);
284
- });
285
- });
286
-
287
- describe('ActorSequenceRunner', () => {
288
- const auth = {
289
- loginUrl: '/login',
290
- selectors: {
291
- username: '#email',
292
- password: '#password',
293
- submit: '#submit',
294
- loggedInSignal: '#account'
295
- },
296
- credentials: { id: 'example' }
297
- } satisfies LoginFlowDefinition;
298
-
299
- it('runs a series of tasks in a single browser session', async () => {
300
- const calls: string[] = [];
301
- const { runner, browserFactory, browser } = createSequenceRunnerParts();
302
- const actor = defineActor({
303
- id: 'example',
304
- baseUrl: 'https://example.com',
305
- tasks: {
306
- first: async () => {
307
- calls.push('first');
308
- return 'one';
309
- },
310
- second: async () => {
311
- calls.push('second');
312
- return 'two';
313
- }
314
- }
315
- });
316
-
317
- const result = await runner.runSequence(actor, ['first', 'second']);
318
-
319
- expect(result).toEqual(['one', 'two']);
320
- expect(calls).toEqual(['first', 'second']);
321
- expect(browserFactory.launchCalls).toBe(1);
322
- expect(browser.closeCalls).toHaveLength(1);
323
- });
324
-
325
- it('runs authentication once before the first sequence step', async () => {
326
- const { runner, loginFlow } = createSequenceRunnerParts();
327
- const actor = defineActor({
328
- id: 'example',
329
- baseUrl: 'https://example.com',
330
- auth,
331
- tasks: {
332
- a: async () => 'a',
333
- b: async () => 'b'
334
- }
335
- });
336
-
337
- await runner.runSequence(actor, ['a', 'b']);
338
-
339
- expect(loginFlow.ensureAuthenticatedCalls).toEqual([auth]);
340
- });
341
-
342
- it('calls hooks for each step and wraps errors with the failing task name', async () => {
343
- const calls: string[] = [];
344
- const { runner, browser } = createSequenceRunnerParts();
345
- const actor = defineActor({
346
- id: 'example',
347
- baseUrl: 'https://example.com',
348
- hooks: {
349
- beforeRun: (_context, taskName) => { calls.push(`before:${taskName}`); },
350
- afterRun: (_context, taskName) => { calls.push(`after:${taskName}`); },
351
- onError: (_context, taskName) => { calls.push(`error:${taskName}`); }
352
- },
353
- tasks: {
354
- first: async () => 'ok',
355
- second: async () => {
356
- throw new Error('boom');
357
- }
358
- }
359
- });
360
-
361
- await expect(runner.runSequence(actor, ['first', 'second'])).rejects.toMatchObject({
362
- name: 'AutomationError',
363
- actorId: 'example',
364
- taskName: 'second'
365
- } satisfies Partial<AutomationError>);
366
-
367
- expect(calls).toEqual(['before:first', 'after:first', 'before:second', 'error:second']);
368
- expect(browser.closeCalls).toHaveLength(1);
369
- });
370
- });
@@ -1,112 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { ActorRegistry } from '../../../src/core/ActorRegistry.js';
3
- import { defineActor } from '../../../src/core/defineActor.js';
4
- import { defineFormFillerTask, defineFormTask } from '../../../src/core/Actor.js';
5
- import { ConfigError } from '../../../src/errors/ConfigError.js';
6
- import { makeContext } from '../../fixtures/makeContext.js';
7
-
8
- describe('defineActor and ActorRegistry', () => {
9
- it('preserves actor definitions and registers them by id', () => {
10
- const actor = defineActor({
11
- id: 'example',
12
- baseUrl: 'https://example.com',
13
- tasks: {
14
- test: async () => 'ok'
15
- }
16
- });
17
-
18
- const registry = new ActorRegistry().register(actor);
19
-
20
- expect(registry.get('example')).toBe(actor);
21
- expect(registry.list()).toEqual([actor]);
22
- });
23
-
24
- it('rejects duplicate actor ids', () => {
25
- const actor = defineActor({
26
- id: 'example',
27
- baseUrl: 'https://example.com',
28
- tasks: {}
29
- });
30
- const registry = new ActorRegistry().register(actor);
31
-
32
- expect(() => registry.register(actor)).toThrow(ConfigError);
33
- });
34
- });
35
-
36
- describe('defineFormTask', () => {
37
- it('creates a task that fills listed fields and submits the form from input data', async () => {
38
- const { context, fakePage, fakeInteractor } = makeContext();
39
- fakePage
40
- .setSelector('#name')
41
- .setSelector('#email')
42
- .setSelector('#submit');
43
-
44
- const task = defineFormTask<{ name: string; email: string }, { submittedEmail: string }>({
45
- url: '/contact',
46
- fields: [
47
- { selector: '#name', inputKey: 'name' },
48
- { selector: '#email', inputKey: 'email' }
49
- ],
50
- submit: {
51
- selector: '#submit',
52
- waitForNavigation: true
53
- },
54
- onComplete: (_ctx, input) => ({ submittedEmail: input.email })
55
- });
56
-
57
- const result = await task(context, {
58
- name: 'Jane Doe',
59
- email: 'jane@example.com'
60
- });
61
-
62
- expect(result).toEqual({ submittedEmail: 'jane@example.com' });
63
- expect(fakePage.gotos[0]?.url).toBe('https://example.com/contact');
64
- expect(fakeInteractor.typed).toEqual([
65
- { selector: '#name', value: 'Jane Doe', options: { required: true } },
66
- { selector: '#email', value: 'jane@example.com', options: { required: true } }
67
- ]);
68
- expect(fakeInteractor.clicks).toEqual([{ selector: '#submit' }]);
69
- expect(fakePage.waitedNavigations).toHaveLength(1);
70
- });
71
-
72
- it('supports literal/resolved field values and the defineFormFillerTask alias', async () => {
73
- const { context, fakePage, fakeInteractor } = makeContext();
74
- fakePage
75
- .setSelector('#first')
76
- .setSelector('#last')
77
- .setSelector('#submit');
78
-
79
- const task = defineFormFillerTask<{ firstName: string; lastName: string }>({
80
- fields: [
81
- { selector: '#first', inputKey: 'firstName' },
82
- { selector: '#last', value: input => input.lastName.toUpperCase() }
83
- ],
84
- submit: { selector: '#submit' }
85
- });
86
-
87
- await task(context, {
88
- firstName: 'Jane',
89
- lastName: 'Doe'
90
- });
91
-
92
- expect(fakeInteractor.typed).toEqual([
93
- { selector: '#first', value: 'Jane', options: { required: true } },
94
- { selector: '#last', value: 'DOE', options: { required: true } }
95
- ]);
96
- expect(fakeInteractor.clicks).toEqual([{ selector: '#submit' }]);
97
- });
98
-
99
- it('throws when a required field has no input value', async () => {
100
- const { context, fakePage } = makeContext();
101
- fakePage
102
- .setSelector('#name')
103
- .setSelector('#submit');
104
-
105
- const task = defineFormTask<{ name?: string }>({
106
- fields: [{ selector: '#name', inputKey: 'name' }],
107
- submit: { selector: '#submit' }
108
- });
109
-
110
- await expect(task(context, {})).rejects.toThrow('missing a value');
111
- });
112
- });
@@ -1,48 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { Extractor } from '../../../src/extraction/Extractor.js';
3
- import { PuppeteerPageAdapter } from '../../../src/interaction/PageAdapter.js';
4
- import { FakePage } from '../../fixtures/FakePage.js';
5
-
6
- describe('Extractor', () => {
7
- it('extracts single text and text lists', async () => {
8
- const fakePage = new FakePage()
9
- .setSelector('#title', { textContent: ' Title ' })
10
- .setTextList('.row', [' One ', 'Two', ' ']);
11
- const extractor = new Extractor(new PuppeteerPageAdapter(fakePage));
12
-
13
- await expect(extractor.text('#title')).resolves.toBe('Title');
14
- await expect(extractor.textList('.row')).resolves.toEqual(['One', 'Two']);
15
- });
16
-
17
- it('extracts attributes and href lists', async () => {
18
- const fakePage = new FakePage()
19
- .setSelector('#link', { attributes: { href: '/single' } });
20
- fakePage.selectorLists.set('a', [
21
- { attributes: { href: '/a' } },
22
- { attributes: { href: '/b' } },
23
- { attributes: {} }
24
- ]);
25
- const extractor = new Extractor(new PuppeteerPageAdapter(fakePage));
26
-
27
- await expect(extractor.attr('#link', 'href')).resolves.toBe('/single');
28
- await expect(extractor.hrefs('a')).resolves.toEqual(['/a', '/b']);
29
- });
30
-
31
- it('extracts table rows into objects', async () => {
32
- const fakePage = new FakePage().setSelector('table', {
33
- attributes: {
34
- __headers: 'Name|Amount',
35
- __rows: 'Alice|10\nBob|20'
36
- }
37
- });
38
- const extractor = new Extractor(new PuppeteerPageAdapter(fakePage));
39
-
40
- await expect(extractor.table('table')).resolves.toEqual({
41
- headers: ['Name', 'Amount'],
42
- rows: [
43
- { Name: 'Alice', Amount: '10' },
44
- { Name: 'Bob', Amount: '20' }
45
- ]
46
- });
47
- });
48
- });
@@ -1,54 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { Pagination } from '../../../src/extraction/Pagination.js';
3
- import { PuppeteerPageAdapter } from '../../../src/interaction/PageAdapter.js';
4
- import { FakeHumanInteractor } from '../../fixtures/FakeCursor.js';
5
- import { FakePage } from '../../fixtures/FakePage.js';
6
-
7
- describe('Pagination', () => {
8
- it('collects pages until the next selector is missing', async () => {
9
- const fakePage = new FakePage().setSelector('#next');
10
- const interactor = new FakeHumanInteractor();
11
- const pagination = new Pagination(new PuppeteerPageAdapter(fakePage), interactor);
12
- let pageNumber = 0;
13
-
14
- const result = await pagination.collectPages({
15
- nextSelector: '#next',
16
- extractPage: async () => {
17
- pageNumber += 1;
18
- if (pageNumber === 2) fakePage.removeSelector('#next');
19
- return [`page-${pageNumber}`];
20
- }
21
- });
22
-
23
- expect(result).toEqual(['page-1', 'page-2']);
24
- expect(interactor.clicks.map(click => click.selector)).toEqual(['#next']);
25
- });
26
-
27
- it('respects maxPages', async () => {
28
- const fakePage = new FakePage().setSelector('#next');
29
- const interactor = new FakeHumanInteractor();
30
- const pagination = new Pagination(new PuppeteerPageAdapter(fakePage), interactor);
31
-
32
- const result = await pagination.collectPages({
33
- nextSelector: '#next',
34
- maxPages: 2,
35
- extractPage: async () => ['item']
36
- });
37
-
38
- expect(result).toEqual(['item', 'item']);
39
- expect(interactor.clicks).toHaveLength(1);
40
- });
41
-
42
- it('deduplicates collected items when configured', async () => {
43
- const fakePage = new FakePage();
44
- const pagination = new Pagination(new PuppeteerPageAdapter(fakePage), new FakeHumanInteractor());
45
-
46
- const result = await pagination.collectPages({
47
- nextSelector: '#next',
48
- extractPage: async () => [{ id: '1' }, { id: '1' }],
49
- dedupeBy: item => item.id
50
- });
51
-
52
- expect(result).toEqual([{ id: '1' }]);
53
- });
54
- });
@@ -1,29 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { clearField } from '../../../src/interaction/FieldClearer.js';
3
- import { FakePage } from '../../fixtures/FakePage.js';
4
-
5
- describe('clearField', () => {
6
- it('selects field contents and presses Backspace before typing', async () => {
7
- const page = new FakePage().setSelector('#email', { value: 'old@example.com' });
8
-
9
- await clearField(page, '#email');
10
-
11
- const state = page.selectors.get('#email');
12
- expect(state?.focused).toBe(true);
13
- expect(state?.selected).toBe(true);
14
- expect(page.keyboard.pressed).toEqual([{ key: 'Backspace' }]);
15
- expect(state?.value).toBe('');
16
- expect(state?.events).toEqual(expect.arrayContaining(['select', 'input', 'change']));
17
- });
18
-
19
- it('supports direct DOM clearing for callers that explicitly request it', async () => {
20
- const page = new FakePage().setSelector('#email', { value: 'old@example.com' });
21
-
22
- await clearField(page, '#email', { strategy: 'dom-value' });
23
-
24
- const state = page.selectors.get('#email');
25
- expect(page.keyboard.pressed).toEqual([]);
26
- expect(state?.value).toBe('');
27
- expect(state?.events).toEqual(expect.arrayContaining(['input', 'change']));
28
- });
29
- });
@@ -1,35 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { SelectorError } from '../../../src/errors/SelectorError.js';
3
- import { FormFiller } from '../../../src/interaction/Forms.js';
4
- import { PuppeteerPageAdapter } from '../../../src/interaction/PageAdapter.js';
5
- import { FakeHumanInteractor } from '../../fixtures/FakeCursor.js';
6
- import { FakePage } from '../../fixtures/FakePage.js';
7
-
8
- describe('FormFiller', () => {
9
- it('fills text through the configured human interactor', async () => {
10
- const page = new FakePage().setSelector('#email');
11
- const interactor = new FakeHumanInteractor();
12
- const form = new FormFiller(new PuppeteerPageAdapter(page), interactor);
13
-
14
- await form.fillText('#email', 'me@example.com', { delayMs: 10 });
15
-
16
- expect(interactor.typed).toEqual([{ selector: '#email', value: 'me@example.com', options: { delayMs: 10 } }]);
17
- });
18
-
19
- it('throws SelectorError for a missing required field', async () => {
20
- const page = new FakePage();
21
- const form = new FormFiller(new PuppeteerPageAdapter(page), new FakeHumanInteractor());
22
-
23
- await expect(form.fillText('#missing', 'value')).rejects.toBeInstanceOf(SelectorError);
24
- });
25
-
26
- it('skips missing optional fields', async () => {
27
- const page = new FakePage();
28
- const interactor = new FakeHumanInteractor();
29
- const form = new FormFiller(new PuppeteerPageAdapter(page), interactor);
30
-
31
- await form.fillText('#missing', 'value', { required: false });
32
-
33
- expect(interactor.typed).toHaveLength(0);
34
- });
35
- });