@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,296 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { defineLoginFlow } from '../../../src/auth/LoginFlow.types.js';
3
- import { LoginFlow } from '../../../src/auth/LoginFlow.js';
4
- import { AuthError } from '../../../src/errors/AuthError.js';
5
- import { FakeCredentialsProvider } from '../../fixtures/FakeCredentialsProvider.js';
6
- import { makeContext } from '../../fixtures/makeContext.js';
7
-
8
- describe('LoginFlow', () => {
9
- const loginDefinition = defineLoginFlow({
10
- loginUrl: '/login',
11
- selectors: {
12
- username: '#email',
13
- password: '#password',
14
- submit: '#submit',
15
- loggedInSignal: '#account',
16
- errorMessage: '#login-error'
17
- },
18
- credentials: { id: 'example' },
19
- behavior: {
20
- submitCausesNavigation: true,
21
- errorTimeoutMs: 25
22
- }
23
- });
24
-
25
- it('navigates, fills credentials, clicks submit, and verifies login', async () => {
26
- const { context, fakePage, fakeInteractor } = makeContext();
27
- fakePage
28
- .setSelector('#email')
29
- .setSelector('#password')
30
- .setSelector('#submit')
31
- .setSelector('#account');
32
- const credentialsProvider = new FakeCredentialsProvider({ username: 'me@example.com', password: 'passw0rd' });
33
- const flow = new LoginFlow(credentialsProvider);
34
-
35
- await flow.login(context, loginDefinition);
36
-
37
- expect(fakePage.gotos[0]?.url).toBe('https://example.com/login');
38
- expect(fakeInteractor.typed).toEqual([
39
- { selector: '#email', value: 'me@example.com', options: { required: true, clear: true, clearMethod: 'select-delete' } },
40
- { selector: '#password', value: 'passw0rd', options: { required: true, clear: true, clearMethod: 'select-delete' } }
41
- ]);
42
- expect(fakeInteractor.clicks.map(click => click.selector)).toContain('#submit');
43
- expect(fakePage.waitedNavigations).toHaveLength(1);
44
- });
45
-
46
-
47
- it('passes clearFieldBeforeTyping=false through the standardized login flow', async () => {
48
- const { context, fakePage, fakeInteractor } = makeContext();
49
- fakePage
50
- .setSelector('#email')
51
- .setSelector('#password')
52
- .setSelector('#submit')
53
- .setSelector('#account');
54
- const flow = new LoginFlow(new FakeCredentialsProvider({ username: 'me@example.com', password: 'passw0rd' }));
55
-
56
- await flow.login(context, defineLoginFlow({
57
- ...loginDefinition,
58
- behavior: {
59
- ...loginDefinition.behavior,
60
- clearFieldBeforeTyping: false,
61
- typing: { targetWordsPerMinute: 70, intervalJitterMs: 6 }
62
- }
63
- }));
64
-
65
- expect(fakeInteractor.typed).toEqual([
66
- {
67
- selector: '#email',
68
- value: 'me@example.com',
69
- options: {
70
- required: true,
71
- clear: false,
72
- clearMethod: 'select-delete',
73
- typing: { targetWordsPerMinute: 70, intervalJitterMs: 6 }
74
- }
75
- },
76
- {
77
- selector: '#password',
78
- value: 'passw0rd',
79
- options: {
80
- required: true,
81
- clear: false,
82
- clearMethod: 'select-delete',
83
- typing: { targetWordsPerMinute: 70, intervalJitterMs: 6 }
84
- }
85
- }
86
- ]);
87
- });
88
-
89
-
90
- it('executes an ordered multi-step login flow with username, continue, password, and submit', async () => {
91
- const calls: string[] = [];
92
- const { context, fakePage, fakeInteractor } = makeContext();
93
- fakePage
94
- .setSelector('#username')
95
- .setSelector('#continue')
96
- .setSelector('#password')
97
- .setSelector('#password-submit')
98
- .setSelector('#account');
99
- const flow = new LoginFlow(new FakeCredentialsProvider({ username: 'me@example.com', password: 'passw0rd' }));
100
-
101
- await flow.login(context, defineLoginFlow({
102
- loginUrl: '/login',
103
- selectors: {
104
- loggedInSignal: '#account',
105
- errorMessage: '#login-error'
106
- },
107
- credentials: { id: 'example' },
108
- behavior: {
109
- errorTimeoutMs: 25,
110
- typing: { targetWordsPerMinute: 65, intervalJitterMs: 8 }
111
- },
112
- hooks: {
113
- beforeSubmit: () => { calls.push('beforeSubmit'); },
114
- afterSubmit: () => { calls.push('afterSubmit'); }
115
- },
116
- steps: [
117
- { type: 'fill', name: 'username', selector: '#username', credential: 'username' },
118
- { type: 'click', name: 'continue', selector: '#continue', waitForSelector: '#password', waitForSelectorTimeoutMs: 500 },
119
- {
120
- type: 'fill',
121
- name: 'password',
122
- selector: '#password',
123
- credential: 'password',
124
- typing: { intervalJitterMs: 4 }
125
- },
126
- {
127
- type: 'click',
128
- name: 'submit password',
129
- selector: '#password-submit',
130
- submit: true,
131
- waitForNavigation: true,
132
- checkForError: false
133
- }
134
- ]
135
- }));
136
-
137
- expect(fakePage.gotos[0]?.url).toBe('https://example.com/login');
138
- expect(fakeInteractor.typed).toEqual([
139
- {
140
- selector: '#username',
141
- value: 'me@example.com',
142
- options: {
143
- required: true,
144
- clear: true,
145
- clearMethod: 'select-delete',
146
- typing: { targetWordsPerMinute: 65, intervalJitterMs: 8 }
147
- }
148
- },
149
- {
150
- selector: '#password',
151
- value: 'passw0rd',
152
- options: {
153
- required: true,
154
- clear: true,
155
- clearMethod: 'select-delete',
156
- typing: { targetWordsPerMinute: 65, intervalJitterMs: 4 }
157
- }
158
- }
159
- ]);
160
- expect(fakeInteractor.clicks.map(click => click.selector)).toEqual(['#continue', '#password-submit']);
161
- expect(fakePage.waitedSelectors.map(wait => wait.selector)).toContain('#password');
162
- expect(fakePage.waitedNavigations).toHaveLength(1);
163
- expect(calls).toEqual(['beforeSubmit', 'afterSubmit']);
164
- });
165
-
166
- it('lets each multi-step fill override clearFieldBeforeTyping', async () => {
167
- const { context, fakePage, fakeInteractor } = makeContext();
168
- fakePage
169
- .setSelector('#username')
170
- .setSelector('#continue')
171
- .setSelector('#password')
172
- .setSelector('#submit')
173
- .setSelector('#account');
174
- const flow = new LoginFlow(new FakeCredentialsProvider({ username: 'me@example.com', password: 'passw0rd' }));
175
-
176
- await flow.login(context, defineLoginFlow({
177
- loginUrl: '/login',
178
- selectors: { loggedInSignal: '#account' },
179
- credentials: { id: 'example' },
180
- behavior: { clearFieldBeforeTyping: true },
181
- steps: [
182
- { type: 'fill', selector: '#username', credential: 'username' },
183
- { type: 'click', selector: '#continue', waitForSelector: '#password' },
184
- { type: 'fill', selector: '#password', credential: 'password', clearFieldBeforeTyping: false },
185
- { type: 'click', selector: '#submit', submit: true }
186
- ]
187
- }));
188
-
189
- expect(fakeInteractor.typed).toEqual([
190
- { selector: '#username', value: 'me@example.com', options: { required: true, clear: true, clearMethod: 'select-delete' } },
191
- { selector: '#password', value: 'passw0rd', options: { required: true, clear: false, clearMethod: 'select-delete' } }
192
- ]);
193
- });
194
-
195
- it('surfaces an intermediate multi-step login error before waiting for the next field', async () => {
196
- const { context, fakePage, fakeInteractor } = makeContext();
197
- fakePage
198
- .setSelector('#username')
199
- .setSelector('#continue')
200
- .setSelector('#login-error', { textContent: 'Unknown Apple ID' });
201
- const flow = new LoginFlow(new FakeCredentialsProvider({ username: 'me@example.com', password: 'passw0rd' }));
202
-
203
- await expect(flow.login(context, defineLoginFlow({
204
- loginUrl: '/login',
205
- selectors: {
206
- loggedInSignal: '#account',
207
- errorMessage: '#login-error'
208
- },
209
- credentials: { id: 'example' },
210
- behavior: { errorTimeoutMs: 25 },
211
- steps: [
212
- { type: 'fill', name: 'username', selector: '#username', credential: 'username' },
213
- { type: 'click', name: 'continue', selector: '#continue', waitForSelector: '#password' },
214
- { type: 'fill', name: 'password', selector: '#password', credential: 'password' }
215
- ]
216
- }))).rejects.toThrow('Login failed during step "continue": Unknown Apple ID');
217
-
218
- expect(fakeInteractor.typed).toEqual([
219
- { selector: '#username', value: 'me@example.com', options: { required: true, clear: true, clearMethod: 'select-delete' } }
220
- ]);
221
- });
222
-
223
- it('requires steps when standard username/password/submit selectors are omitted', async () => {
224
- const { context } = makeContext();
225
- const flow = new LoginFlow(new FakeCredentialsProvider());
226
-
227
- await expect(flow.login(context, defineLoginFlow({
228
- loginUrl: '/login',
229
- selectors: { loggedInSignal: '#account' },
230
- credentials: { id: 'example' }
231
- }))).rejects.toThrow('either selectors.username/password/submit');
232
- });
233
-
234
- it('skips login when the existing profile is already authenticated', async () => {
235
- const { context, fakePage } = makeContext();
236
- fakePage.setSelector('#account');
237
- const credentialsProvider = new FakeCredentialsProvider();
238
- const flow = new LoginFlow(credentialsProvider);
239
-
240
- await flow.ensureAuthenticated(context, loginDefinition);
241
-
242
- expect(credentialsProvider.requested).toHaveLength(0);
243
- expect(fakePage.gotos).toHaveLength(0);
244
- });
245
-
246
- it('throws an AuthError when an error message appears after submit', async () => {
247
- const { context, fakePage } = makeContext();
248
- fakePage
249
- .setSelector('#email')
250
- .setSelector('#password')
251
- .setSelector('#submit')
252
- .setSelector('#login-error', { textContent: 'Bad credentials' });
253
- const flow = new LoginFlow(new FakeCredentialsProvider());
254
-
255
- await expect(flow.login(context, loginDefinition)).rejects.toBeInstanceOf(AuthError);
256
- });
257
-
258
- it('calls hooks in order', async () => {
259
- const calls: string[] = [];
260
- const { context, fakePage } = makeContext();
261
- fakePage
262
- .setSelector('#email')
263
- .setSelector('#password')
264
- .setSelector('#submit')
265
- .setSelector('#account');
266
- const flow = new LoginFlow(new FakeCredentialsProvider());
267
-
268
- await flow.login(context, defineLoginFlow({
269
- ...loginDefinition,
270
- hooks: {
271
- beforeLogin: () => { calls.push('beforeLogin'); },
272
- afterUsername: () => { calls.push('afterUsername'); },
273
- afterPassword: () => { calls.push('afterPassword'); },
274
- beforeSubmit: () => { calls.push('beforeSubmit'); },
275
- afterSubmit: () => { calls.push('afterSubmit'); }
276
- }
277
- }));
278
-
279
- expect(calls).toEqual(['beforeLogin', 'afterUsername', 'afterPassword', 'beforeSubmit', 'afterSubmit']);
280
- });
281
-
282
- it('does not write raw passwords to the logger metadata', async () => {
283
- const { context, fakePage, logger } = makeContext();
284
- fakePage
285
- .setSelector('#email')
286
- .setSelector('#password')
287
- .setSelector('#submit')
288
- .setSelector('#account');
289
- const flow = new LoginFlow(new FakeCredentialsProvider({ username: 'me@example.com', password: 'super-secret' }));
290
-
291
- await flow.login(context, loginDefinition);
292
-
293
- expect(JSON.stringify(logger.records)).not.toContain('super-secret');
294
- expect(JSON.stringify(logger.records)).toContain('[REDACTED]');
295
- });
296
- });
@@ -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 { BrowserFactory } from '../../../src/browser/BrowserFactory.js';
6
- import type { PuppeteerConnectOptions, PuppeteerLauncher, PuppeteerLaunchOptions } from '../../../src/browser/PuppeteerLike.js';
7
- import type { RuntimeConfig } from '../../../src/browser/RuntimeConfig.js';
8
- import { ConfigError } from '../../../src/errors/ConfigError.js';
9
- import { FakeBrowser, FakePage } from '../../fixtures/FakePage.js';
10
-
11
- class ThrowingLauncher implements PuppeteerLauncher {
12
- async launch(): Promise<FakeBrowser> {
13
- throw new Error('DevToolsActivePort file does not exist; user data directory is already in use');
14
- }
15
-
16
- async connect(): Promise<FakeBrowser> {
17
- throw new Error('connect was not expected');
18
- }
19
- }
20
-
21
-
22
- class ThrowingLaunchConnectingLauncher implements PuppeteerLauncher {
23
- launchOptions?: PuppeteerLaunchOptions;
24
- connectOptions?: PuppeteerConnectOptions;
25
-
26
- constructor(readonly browser: FakeBrowser, private readonly connectError?: Error) {}
27
-
28
- async launch(options: PuppeteerLaunchOptions): Promise<FakeBrowser> {
29
- this.launchOptions = options;
30
- throw new Error('DevToolsActivePort file does not exist; user data directory is already in use');
31
- }
32
-
33
- async connect(options: PuppeteerConnectOptions): Promise<FakeBrowser> {
34
- this.connectOptions = options;
35
- if (this.connectError !== undefined) throw this.connectError;
36
- return this.browser;
37
- }
38
- }
39
-
40
- class ThrowingConnector implements PuppeteerLauncher {
41
- async launch(): Promise<FakeBrowser> {
42
- throw new Error('launch was not expected');
43
- }
44
-
45
- async connect(): Promise<FakeBrowser> {
46
- throw new Error('connect ECONNREFUSED 127.0.0.1:9222');
47
- }
48
- }
49
-
50
- class FakeLauncher implements PuppeteerLauncher {
51
- launchOptions?: PuppeteerLaunchOptions;
52
- connectOptions?: PuppeteerConnectOptions;
53
-
54
- constructor(readonly browser: FakeBrowser) {}
55
-
56
- async launch(options: PuppeteerLaunchOptions): Promise<FakeBrowser> {
57
- this.launchOptions = options;
58
- return this.browser;
59
- }
60
-
61
- async connect(options: PuppeteerConnectOptions): Promise<FakeBrowser> {
62
- this.connectOptions = options;
63
- return this.browser;
64
- }
65
- }
66
-
67
- function createProfileDirs(profileDirectory = 'Profile 1'): { userDataDir: string; profileDirectory: string } {
68
- const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'paf-user-data-'));
69
- fs.mkdirSync(path.join(userDataDir, profileDirectory));
70
- return { userDataDir, profileDirectory };
71
- }
72
-
73
- describe('BrowserFactory', () => {
74
- it('launches installed Chrome with an existing userDataDir and profile directory', async () => {
75
- const { userDataDir, profileDirectory } = createProfileDirs();
76
- const fakePage = new FakePage();
77
- const fakeBrowser = new FakeBrowser([fakePage]);
78
- const launcher = new FakeLauncher(fakeBrowser);
79
- const factory = new BrowserFactory(launcher);
80
-
81
- const config: RuntimeConfig = {
82
- browser: {
83
- mode: 'existing-profile',
84
- userDataDir,
85
- profileDirectory,
86
- channel: 'chrome',
87
- headless: false
88
- }
89
- };
90
-
91
- const session = await factory.launch(config);
92
-
93
- expect(launcher.launchOptions).toMatchObject({
94
- browser: 'chrome',
95
- channel: 'chrome',
96
- headless: false,
97
- userDataDir,
98
- args: [`--profile-directory=${profileDirectory}`]
99
- });
100
- expect(launcher.connectOptions).toBeUndefined();
101
- expect(session.context).toBe(fakeBrowser.context);
102
- expect(session.page).toBe(fakePage);
103
- });
104
-
105
- it('connects to an existing Chrome remote debugging endpoint', async () => {
106
- const fakeBrowser = new FakeBrowser([new FakePage()]);
107
- const launcher = new FakeLauncher(fakeBrowser);
108
- const factory = new BrowserFactory(launcher);
109
-
110
- const session = await factory.launch({
111
- browser: {
112
- mode: 'remote-debugging',
113
- browserURL: 'http://127.0.0.1:9222'
114
- }
115
- });
116
-
117
- expect(launcher.launchOptions).toBeUndefined();
118
- expect(launcher.connectOptions).toMatchObject({ browserURL: 'http://127.0.0.1:9222' });
119
- expect(session.profile).toMatchObject({
120
- mode: 'remote-debugging',
121
- browserURL: 'http://127.0.0.1:9222',
122
- closeBrowserOnFinish: false,
123
- disconnectOnFinish: true
124
- });
125
- expect(fakeBrowser.newPageCalls).toBe(1);
126
- });
127
-
128
- it('connects using browserWSEndpoint when configured', async () => {
129
- const fakeBrowser = new FakeBrowser([new FakePage()]);
130
- const launcher = new FakeLauncher(fakeBrowser);
131
- const factory = new BrowserFactory(launcher);
132
-
133
- await factory.launch({
134
- browser: {
135
- mode: 'remote-debugging',
136
- browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser/test'
137
- }
138
- });
139
-
140
- expect(launcher.connectOptions).toMatchObject({
141
- browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser/test'
142
- });
143
- });
144
-
145
- it('defaults remote-debugging mode to http://127.0.0.1:9222', async () => {
146
- const launcher = new FakeLauncher(new FakeBrowser([new FakePage()]));
147
- const factory = new BrowserFactory(launcher);
148
-
149
- await factory.launch({ browser: { mode: 'remote-debugging' } });
150
-
151
- expect(launcher.connectOptions).toMatchObject({ browserURL: 'http://127.0.0.1:9222' });
152
- });
153
-
154
- it('rejects remote-debugging configs that set both browserURL and browserWSEndpoint', async () => {
155
- const factory = new BrowserFactory(new FakeLauncher(new FakeBrowser()));
156
-
157
- await expect(factory.launch({
158
- browser: {
159
- mode: 'remote-debugging',
160
- browserURL: 'http://127.0.0.1:9222',
161
- browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser/test'
162
- }
163
- })).rejects.toBeInstanceOf(ConfigError);
164
- });
165
-
166
- it('uses browser.defaultBrowserContext and never creates an isolated context', async () => {
167
- const { userDataDir, profileDirectory } = createProfileDirs();
168
- const fakeBrowser = new FakeBrowser([new FakePage()]);
169
- const factory = new BrowserFactory(new FakeLauncher(fakeBrowser));
170
-
171
- await factory.launch({
172
- browser: {
173
- mode: 'existing-profile',
174
- userDataDir,
175
- profileDirectory,
176
- headless: false,
177
- runningInstance: { enabled: false }
178
- }
179
- });
180
-
181
- expect(fakeBrowser.defaultBrowserContextCalls).toBe(1);
182
- expect(fakeBrowser.createBrowserContextCalls).toBe(0);
183
- });
184
-
185
- it('defaults to channel chrome and headless false', () => {
186
- const { userDataDir, profileDirectory } = createProfileDirs();
187
- const factory = new BrowserFactory(new FakeLauncher(new FakeBrowser()));
188
-
189
- const options = factory.buildLaunchOptions({
190
- mode: 'existing-profile',
191
- userDataDir,
192
- profileDirectory
193
- });
194
-
195
- expect(options.channel).toBe('chrome');
196
- expect(options.headless).toBe(false);
197
- });
198
-
199
- it('throws when userDataDir does not exist by default', async () => {
200
- const missingDir = path.join(os.tmpdir(), `missing-${Date.now()}`);
201
- const factory = new BrowserFactory(new FakeLauncher(new FakeBrowser()));
202
-
203
- await expect(factory.launch({
204
- browser: {
205
- mode: 'existing-profile',
206
- userDataDir: missingDir,
207
- headless: false
208
- }
209
- })).rejects.toBeInstanceOf(ConfigError);
210
- });
211
-
212
- it('throws when a configured profileDirectory is missing by default', async () => {
213
- const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'paf-user-data-'));
214
- const factory = new BrowserFactory(new FakeLauncher(new FakeBrowser()));
215
-
216
- await expect(factory.launch({
217
- browser: {
218
- mode: 'existing-profile',
219
- userDataDir,
220
- profileDirectory: 'Missing Profile',
221
- headless: false
222
- }
223
- })).rejects.toBeInstanceOf(ConfigError);
224
- });
225
-
226
-
227
- it('automatically tries the default local debugging endpoint when an existing profile is already running', async () => {
228
- const { userDataDir, profileDirectory } = createProfileDirs();
229
- const fakeBrowser = new FakeBrowser([new FakePage()]);
230
- const launcher = new ThrowingLaunchConnectingLauncher(fakeBrowser);
231
- const factory = new BrowserFactory(launcher);
232
-
233
- const session = await factory.launch({
234
- browser: {
235
- mode: 'existing-profile',
236
- userDataDir,
237
- profileDirectory,
238
- headless: false
239
- }
240
- });
241
-
242
- expect(launcher.launchOptions).toMatchObject({ userDataDir });
243
- expect(launcher.connectOptions).toMatchObject({ browserURL: 'http://127.0.0.1:9222' });
244
- expect(session.connectionMode).toBe('connected');
245
- expect(session.profile).toMatchObject({
246
- mode: 'remote-debugging',
247
- remoteDebuggingHost: '127.0.0.1',
248
- remoteDebuggingPort: 9222,
249
- closeBrowserOnFinish: false,
250
- disconnectOnFinish: true
251
- });
252
- });
253
-
254
-
255
- it('falls back to a local debugging endpoint when an existing profile is already running and fallback is enabled', async () => {
256
- const { userDataDir, profileDirectory } = createProfileDirs();
257
- const fakeBrowser = new FakeBrowser([new FakePage()]);
258
- const launcher = new ThrowingLaunchConnectingLauncher(fakeBrowser);
259
- const factory = new BrowserFactory(launcher);
260
-
261
- const session = await factory.launch({
262
- browser: {
263
- mode: 'existing-profile',
264
- userDataDir,
265
- profileDirectory
266
- }
267
- });
268
-
269
- expect(launcher.launchOptions).toMatchObject({ userDataDir });
270
- expect(launcher.connectOptions).toMatchObject({ browserURL: 'http://127.0.0.1:9222' });
271
- expect(session.connectionMode).toBe('connected');
272
- expect(session.profile).toMatchObject({
273
- mode: 'remote-debugging',
274
- remoteDebuggingHost: '127.0.0.1',
275
- remoteDebuggingPort: 9222,
276
- closeBrowserOnFinish: false,
277
- disconnectOnFinish: true
278
- });
279
- expect(fakeBrowser.newPageCalls).toBe(1);
280
- });
281
-
282
- it('uses the configured running-profile WebSocket endpoint when fallback is enabled', async () => {
283
- const { userDataDir, profileDirectory } = createProfileDirs();
284
- const launcher = new ThrowingLaunchConnectingLauncher(new FakeBrowser([new FakePage()]));
285
- const factory = new BrowserFactory(launcher);
286
-
287
- await factory.launch({
288
- browser: {
289
- mode: 'existing-profile',
290
- userDataDir,
291
- profileDirectory,
292
- runningInstance: {
293
- enabled: true,
294
- browserWSEndpoint: 'ws://127.0.0.1:9223/devtools/browser/test',
295
- reuseExistingPage: true
296
- }
297
- }
298
- });
299
-
300
- expect(launcher.connectOptions).toMatchObject({
301
- browserWSEndpoint: 'ws://127.0.0.1:9223/devtools/browser/test'
302
- });
303
- });
304
-
305
- it('throws a fallback-specific error when the running profile debugging endpoint is unavailable', async () => {
306
- const { userDataDir, profileDirectory } = createProfileDirs();
307
- const launcher = new ThrowingLaunchConnectingLauncher(new FakeBrowser(), new Error('connect ECONNREFUSED 127.0.0.1:9222'));
308
- const factory = new BrowserFactory(launcher);
309
-
310
- await expect(factory.launch({
311
- browser: {
312
- mode: 'existing-profile',
313
- userDataDir,
314
- profileDirectory,
315
- runningInstance: { enabled: true }
316
- }
317
- })).rejects.toMatchObject({
318
- name: 'ConfigError',
319
- message: expect.stringContaining('local DevTools endpoint could not be reached'),
320
- details: expect.objectContaining({
321
- browserURL: 'http://127.0.0.1:9222'
322
- })
323
- });
324
- });
325
-
326
- it('explains likely profile lock launch failures', async () => {
327
- const { userDataDir, profileDirectory } = createProfileDirs();
328
- const factory = new BrowserFactory(new ThrowingLauncher());
329
-
330
- await expect(factory.launch({
331
- browser: {
332
- mode: 'existing-profile',
333
- userDataDir,
334
- profileDirectory,
335
- headless: false,
336
- runningInstance: { enabled: false }
337
- }
338
- })).rejects.toThrow(/local remote debugging endpoint|already be running/);
339
- });
340
-
341
- it('explains remote debugging connection failures', async () => {
342
- const factory = new BrowserFactory(new ThrowingConnector());
343
-
344
- await expect(factory.launch({
345
- browser: {
346
- mode: 'remote-debugging',
347
- browserURL: 'http://127.0.0.1:9222'
348
- }
349
- })).rejects.toThrow(/debugging endpoint/);
350
- });
351
-
352
- it('creates a new page when reuseExistingPage is false', async () => {
353
- const { userDataDir, profileDirectory } = createProfileDirs();
354
- const fakeBrowser = new FakeBrowser([new FakePage()]);
355
- const factory = new BrowserFactory(new FakeLauncher(fakeBrowser));
356
-
357
- const session = await factory.launch({
358
- browser: {
359
- mode: 'existing-profile',
360
- userDataDir,
361
- profileDirectory,
362
- reuseExistingPage: false,
363
- headless: false
364
- }
365
- });
366
-
367
- expect(fakeBrowser.newPageCalls).toBe(1);
368
- expect(session.page).not.toBe((await fakeBrowser.pages())[0]);
369
- });
370
- });