@sparkleideas/browser 3.0.0-alpha.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.
- package/README.md +730 -0
- package/agents/architect.yaml +11 -0
- package/agents/coder.yaml +11 -0
- package/agents/reviewer.yaml +10 -0
- package/agents/security-architect.yaml +10 -0
- package/agents/tester.yaml +10 -0
- package/docker/Dockerfile +22 -0
- package/docker/docker-compose.yml +52 -0
- package/docker/test-fixtures/index.html +61 -0
- package/package.json +56 -0
- package/skills/browser/SKILL.md +204 -0
- package/src/agent/index.ts +35 -0
- package/src/application/browser-service.ts +570 -0
- package/src/domain/types.ts +324 -0
- package/src/index.ts +156 -0
- package/src/infrastructure/agent-browser-adapter.ts +654 -0
- package/src/infrastructure/hooks-integration.ts +170 -0
- package/src/infrastructure/memory-integration.ts +449 -0
- package/src/infrastructure/reasoningbank-adapter.ts +282 -0
- package/src/infrastructure/security-integration.ts +528 -0
- package/src/infrastructure/workflow-templates.ts +479 -0
- package/src/mcp-tools/browser-tools.ts +1210 -0
- package/src/mcp-tools/index.ts +6 -0
- package/src/skill/index.ts +24 -0
- package/tests/agent-browser-adapter.test.ts +328 -0
- package/tests/browser-service.test.ts +137 -0
- package/tests/e2e/browser-e2e.test.ts +175 -0
- package/tests/memory-integration.test.ts +277 -0
- package/tests/reasoningbank-adapter.test.ts +219 -0
- package/tests/security-integration.test.ts +194 -0
- package/tests/workflow-templates.test.ts +231 -0
- package/tmp.json +0 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Skill Exports
|
|
3
|
+
* Re-exports skill-related functionality for Claude Code integration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { browserTools } from '../mcp-tools/browser-tools.js';
|
|
7
|
+
export { BrowserService, createBrowserService } from '../application/browser-service.js';
|
|
8
|
+
export { preBrowseHook, postBrowseHook, browserHooks } from '../infrastructure/hooks-integration.js';
|
|
9
|
+
|
|
10
|
+
// Skill metadata
|
|
11
|
+
export const SKILL_METADATA = {
|
|
12
|
+
name: 'browser',
|
|
13
|
+
description: 'Web browser automation with AI-optimized snapshots for @sparkleideas/claude-flow agents',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
triggers: ['/browser', 'browse', 'web automation', 'scrape', 'navigate', 'screenshot'],
|
|
16
|
+
tools: [
|
|
17
|
+
'browser/open',
|
|
18
|
+
'browser/snapshot',
|
|
19
|
+
'browser/click',
|
|
20
|
+
'browser/fill',
|
|
21
|
+
'browser/screenshot',
|
|
22
|
+
'browser/close',
|
|
23
|
+
],
|
|
24
|
+
};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkleideas/browser - Agent Browser Adapter Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
6
|
+
import { AgentBrowserAdapter } from '../src/infrastructure/agent-browser-adapter.js';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
|
|
9
|
+
// Mock execSync
|
|
10
|
+
vi.mock('child_process', () => ({
|
|
11
|
+
execSync: vi.fn(),
|
|
12
|
+
spawn: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const mockExecSync = vi.mocked(execSync);
|
|
16
|
+
|
|
17
|
+
describe('AgentBrowserAdapter', () => {
|
|
18
|
+
let adapter: AgentBrowserAdapter;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
adapter = new AgentBrowserAdapter({
|
|
22
|
+
session: 'test-session',
|
|
23
|
+
timeout: 5000,
|
|
24
|
+
});
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('constructor', () => {
|
|
33
|
+
it('should create adapter with default options', () => {
|
|
34
|
+
const defaultAdapter = new AgentBrowserAdapter();
|
|
35
|
+
expect(defaultAdapter).toBeInstanceOf(AgentBrowserAdapter);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should create adapter with custom options', () => {
|
|
39
|
+
const customAdapter = new AgentBrowserAdapter({
|
|
40
|
+
session: 'custom',
|
|
41
|
+
timeout: 10000,
|
|
42
|
+
headless: false,
|
|
43
|
+
debug: true,
|
|
44
|
+
});
|
|
45
|
+
expect(customAdapter).toBeInstanceOf(AgentBrowserAdapter);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('navigation', () => {
|
|
50
|
+
it('should open a URL', async () => {
|
|
51
|
+
mockExecSync.mockReturnValue(JSON.stringify({
|
|
52
|
+
success: true,
|
|
53
|
+
data: { url: 'https://example.com' },
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const result = await adapter.open({ url: 'https://example.com' });
|
|
57
|
+
|
|
58
|
+
expect(result.success).toBe(true);
|
|
59
|
+
expect(mockExecSync).toHaveBeenCalled();
|
|
60
|
+
const callArgs = mockExecSync.mock.calls[0][0] as string;
|
|
61
|
+
expect(callArgs).toContain('open');
|
|
62
|
+
expect(callArgs).toContain('https://example.com');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should go back', async () => {
|
|
66
|
+
mockExecSync.mockReturnValue(JSON.stringify({ success: true }));
|
|
67
|
+
|
|
68
|
+
const result = await adapter.back();
|
|
69
|
+
|
|
70
|
+
expect(result.success).toBe(true);
|
|
71
|
+
expect(mockExecSync).toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should go forward', async () => {
|
|
75
|
+
mockExecSync.mockReturnValue(JSON.stringify({ success: true }));
|
|
76
|
+
|
|
77
|
+
const result = await adapter.forward();
|
|
78
|
+
|
|
79
|
+
expect(result.success).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should reload', async () => {
|
|
83
|
+
mockExecSync.mockReturnValue(JSON.stringify({ success: true }));
|
|
84
|
+
|
|
85
|
+
const result = await adapter.reload();
|
|
86
|
+
|
|
87
|
+
expect(result.success).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should close', async () => {
|
|
91
|
+
mockExecSync.mockReturnValue(JSON.stringify({ success: true }));
|
|
92
|
+
|
|
93
|
+
const result = await adapter.close();
|
|
94
|
+
|
|
95
|
+
expect(result.success).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('interaction', () => {
|
|
100
|
+
it('should click an element', async () => {
|
|
101
|
+
mockExecSync.mockReturnValue(JSON.stringify({
|
|
102
|
+
success: true,
|
|
103
|
+
data: { clicked: true },
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
const result = await adapter.click({ target: '@e1' });
|
|
107
|
+
|
|
108
|
+
expect(result.success).toBe(true);
|
|
109
|
+
const callArgs = mockExecSync.mock.calls[0][0] as string;
|
|
110
|
+
expect(callArgs).toContain('click');
|
|
111
|
+
expect(callArgs).toContain('@e1');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should fill an input', async () => {
|
|
115
|
+
mockExecSync.mockReturnValue(JSON.stringify({
|
|
116
|
+
success: true,
|
|
117
|
+
data: { filled: true },
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const result = await adapter.fill({ target: '@e1', value: 'test' });
|
|
121
|
+
|
|
122
|
+
expect(result.success).toBe(true);
|
|
123
|
+
const callArgs = mockExecSync.mock.calls[0][0] as string;
|
|
124
|
+
expect(callArgs).toContain('fill');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should type text', async () => {
|
|
128
|
+
mockExecSync.mockReturnValue(JSON.stringify({ success: true }));
|
|
129
|
+
|
|
130
|
+
const result = await adapter.type({ target: '@e1', text: 'hello' });
|
|
131
|
+
|
|
132
|
+
expect(result.success).toBe(true);
|
|
133
|
+
const callArgs = mockExecSync.mock.calls[0][0] as string;
|
|
134
|
+
expect(callArgs).toContain('type');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should press a key', async () => {
|
|
138
|
+
mockExecSync.mockReturnValue(JSON.stringify({ success: true }));
|
|
139
|
+
|
|
140
|
+
const result = await adapter.press('Enter');
|
|
141
|
+
|
|
142
|
+
expect(result.success).toBe(true);
|
|
143
|
+
const callArgs = mockExecSync.mock.calls[0][0] as string;
|
|
144
|
+
expect(callArgs).toContain('press');
|
|
145
|
+
expect(callArgs).toContain('Enter');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should hover an element', async () => {
|
|
149
|
+
mockExecSync.mockReturnValue(JSON.stringify({ success: true }));
|
|
150
|
+
|
|
151
|
+
const result = await adapter.hover('@e1');
|
|
152
|
+
|
|
153
|
+
expect(result.success).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should scroll', async () => {
|
|
157
|
+
mockExecSync.mockReturnValue(JSON.stringify({ success: true }));
|
|
158
|
+
|
|
159
|
+
const result = await adapter.scroll('down', 500);
|
|
160
|
+
|
|
161
|
+
expect(result.success).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('information retrieval', () => {
|
|
166
|
+
it('should get text', async () => {
|
|
167
|
+
mockExecSync.mockReturnValue(JSON.stringify({
|
|
168
|
+
success: true,
|
|
169
|
+
data: 'Element text',
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
const result = await adapter.getText('@e1');
|
|
173
|
+
|
|
174
|
+
expect(result.success).toBe(true);
|
|
175
|
+
expect(result.data).toBe('Element text');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should get title', async () => {
|
|
179
|
+
mockExecSync.mockReturnValue(JSON.stringify({
|
|
180
|
+
success: true,
|
|
181
|
+
data: 'Page Title',
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
const result = await adapter.getTitle();
|
|
185
|
+
|
|
186
|
+
expect(result.success).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should get URL', async () => {
|
|
190
|
+
mockExecSync.mockReturnValue(JSON.stringify({
|
|
191
|
+
success: true,
|
|
192
|
+
data: 'https://example.com',
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
const result = await adapter.getUrl();
|
|
196
|
+
|
|
197
|
+
expect(result.success).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('state checks', () => {
|
|
202
|
+
it('should check visibility', async () => {
|
|
203
|
+
mockExecSync.mockReturnValue(JSON.stringify({
|
|
204
|
+
success: true,
|
|
205
|
+
data: true,
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
const result = await adapter.isVisible('@e1');
|
|
209
|
+
|
|
210
|
+
expect(result.success).toBe(true);
|
|
211
|
+
expect(result.data).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should check if enabled', async () => {
|
|
215
|
+
mockExecSync.mockReturnValue(JSON.stringify({
|
|
216
|
+
success: true,
|
|
217
|
+
data: true,
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
const result = await adapter.isEnabled('@e1');
|
|
221
|
+
|
|
222
|
+
expect(result.success).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('snapshot', () => {
|
|
227
|
+
it('should take a snapshot', async () => {
|
|
228
|
+
mockExecSync.mockReturnValue(JSON.stringify({
|
|
229
|
+
success: true,
|
|
230
|
+
data: {
|
|
231
|
+
tree: { role: 'document', children: [] },
|
|
232
|
+
refs: {},
|
|
233
|
+
url: 'https://example.com',
|
|
234
|
+
title: 'Test',
|
|
235
|
+
},
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
const result = await adapter.snapshot();
|
|
239
|
+
|
|
240
|
+
expect(result.success).toBe(true);
|
|
241
|
+
expect(result.data).toBeDefined();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should take interactive snapshot', async () => {
|
|
245
|
+
mockExecSync.mockReturnValue(JSON.stringify({
|
|
246
|
+
success: true,
|
|
247
|
+
data: { tree: { role: 'document' } },
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
const result = await adapter.snapshot({ interactive: true });
|
|
251
|
+
|
|
252
|
+
expect(result.success).toBe(true);
|
|
253
|
+
const callArgs = mockExecSync.mock.calls[0][0] as string;
|
|
254
|
+
expect(callArgs).toContain('-i');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should take compact snapshot', async () => {
|
|
258
|
+
mockExecSync.mockReturnValue(JSON.stringify({ success: true }));
|
|
259
|
+
|
|
260
|
+
const result = await adapter.snapshot({ compact: true });
|
|
261
|
+
|
|
262
|
+
const callArgs = mockExecSync.mock.calls[0][0] as string;
|
|
263
|
+
expect(callArgs).toContain('-c');
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('screenshot', () => {
|
|
268
|
+
it('should take a screenshot', async () => {
|
|
269
|
+
mockExecSync.mockReturnValue(JSON.stringify({
|
|
270
|
+
success: true,
|
|
271
|
+
data: 'base64encodedimage',
|
|
272
|
+
}));
|
|
273
|
+
|
|
274
|
+
const result = await adapter.screenshot();
|
|
275
|
+
|
|
276
|
+
expect(result.success).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should take full page screenshot', async () => {
|
|
280
|
+
mockExecSync.mockReturnValue(JSON.stringify({ success: true }));
|
|
281
|
+
|
|
282
|
+
const result = await adapter.screenshot({ fullPage: true });
|
|
283
|
+
|
|
284
|
+
const callArgs = mockExecSync.mock.calls[0][0] as string;
|
|
285
|
+
expect(callArgs).toContain('--full');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('wait', () => {
|
|
290
|
+
it('should wait for selector', async () => {
|
|
291
|
+
mockExecSync.mockReturnValue(JSON.stringify({ success: true }));
|
|
292
|
+
|
|
293
|
+
const result = await adapter.wait({ selector: '#element' });
|
|
294
|
+
|
|
295
|
+
expect(result.success).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should wait for timeout', async () => {
|
|
299
|
+
mockExecSync.mockReturnValue(JSON.stringify({ success: true }));
|
|
300
|
+
|
|
301
|
+
const result = await adapter.wait({ timeout: 1000 });
|
|
302
|
+
|
|
303
|
+
expect(result.success).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('error handling', () => {
|
|
308
|
+
it('should handle command failure', async () => {
|
|
309
|
+
mockExecSync.mockImplementation(() => {
|
|
310
|
+
throw new Error('Command failed');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const result = await adapter.open({ url: 'https://example.com' });
|
|
314
|
+
|
|
315
|
+
expect(result.success).toBe(false);
|
|
316
|
+
expect(result.error).toContain('Command failed');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should handle invalid JSON response', async () => {
|
|
320
|
+
mockExecSync.mockReturnValue('invalid json');
|
|
321
|
+
|
|
322
|
+
const result = await adapter.open({ url: 'https://example.com' });
|
|
323
|
+
|
|
324
|
+
// Should fall back to raw string
|
|
325
|
+
expect(result.success).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkleideas/browser - Browser Service Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
6
|
+
import { BrowserService, createBrowserService } from '../src/application/browser-service.js';
|
|
7
|
+
|
|
8
|
+
// Mock execSync for agent-browser CLI
|
|
9
|
+
vi.mock('child_process', () => ({
|
|
10
|
+
execSync: vi.fn(() => JSON.stringify({ success: true, data: { test: 'value' } })),
|
|
11
|
+
spawn: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe('BrowserService', () => {
|
|
15
|
+
let service: BrowserService;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
service = createBrowserService({ sessionId: 'test-session' });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('createBrowserService', () => {
|
|
26
|
+
it('should create a browser service with default options', () => {
|
|
27
|
+
const svc = createBrowserService();
|
|
28
|
+
expect(svc).toBeInstanceOf(BrowserService);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should create a browser service with custom session', () => {
|
|
32
|
+
const svc = createBrowserService({ sessionId: 'custom-session' });
|
|
33
|
+
expect(svc).toBeInstanceOf(BrowserService);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('trajectory tracking', () => {
|
|
38
|
+
it('should start a trajectory', () => {
|
|
39
|
+
const id = service.startTrajectory('test task');
|
|
40
|
+
expect(id).toBeDefined();
|
|
41
|
+
expect(typeof id).toBe('string');
|
|
42
|
+
expect(id.startsWith('traj-')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should end a trajectory', async () => {
|
|
46
|
+
service.startTrajectory('test task');
|
|
47
|
+
const trajectory = await service.endTrajectory(true, 'success');
|
|
48
|
+
|
|
49
|
+
expect(trajectory).toBeDefined();
|
|
50
|
+
expect(trajectory?.goal).toBe('test task');
|
|
51
|
+
expect(trajectory?.success).toBe(true);
|
|
52
|
+
expect(trajectory?.verdict).toBe('success');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return null if no trajectory is active', async () => {
|
|
56
|
+
const trajectory = await service.endTrajectory(true);
|
|
57
|
+
expect(trajectory).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('navigation', () => {
|
|
62
|
+
it('should navigate to a URL', async () => {
|
|
63
|
+
const result = await service.open('https://example.com');
|
|
64
|
+
expect(result).toBeDefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should close the browser', async () => {
|
|
68
|
+
const result = await service.close();
|
|
69
|
+
expect(result).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('snapshot', () => {
|
|
74
|
+
it('should take a snapshot with default options', async () => {
|
|
75
|
+
const result = await service.snapshot();
|
|
76
|
+
expect(result).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should take a snapshot with interactive option', async () => {
|
|
80
|
+
const result = await service.snapshot({ interactive: true });
|
|
81
|
+
expect(result).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('interaction', () => {
|
|
86
|
+
it('should click an element', async () => {
|
|
87
|
+
const result = await service.click('@e1');
|
|
88
|
+
expect(result).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should fill an input', async () => {
|
|
92
|
+
const result = await service.fill('@e1', 'test value');
|
|
93
|
+
expect(result).toBeDefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should type text', async () => {
|
|
97
|
+
const result = await service.type('@e1', 'typed text');
|
|
98
|
+
expect(result).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should press a key', async () => {
|
|
102
|
+
const result = await service.press('Enter');
|
|
103
|
+
expect(result).toBeDefined();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('wait', () => {
|
|
108
|
+
it('should wait for an element', async () => {
|
|
109
|
+
const result = await service.wait({ selector: '#element' });
|
|
110
|
+
expect(result).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('data extraction', () => {
|
|
115
|
+
it('should get text from element', async () => {
|
|
116
|
+
const result = await service.getText('@e1');
|
|
117
|
+
expect(result).toBeDefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should evaluate JavaScript', async () => {
|
|
121
|
+
const result = await service.eval('document.title');
|
|
122
|
+
expect(result).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('screenshot', () => {
|
|
127
|
+
it('should take a screenshot', async () => {
|
|
128
|
+
const result = await service.screenshot();
|
|
129
|
+
expect(result).toBeDefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should take a screenshot with path', async () => {
|
|
133
|
+
const result = await service.screenshot({ path: '/tmp/test.png' });
|
|
134
|
+
expect(result).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkleideas/browser - E2E Browser Tests
|
|
3
|
+
*
|
|
4
|
+
* These tests run against a real browser using agent-browser.
|
|
5
|
+
* Run with: docker compose --profile e2e up browser-e2e
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
|
|
11
|
+
const TEST_URL = process.env.TEST_URL || 'http://localhost:3000';
|
|
12
|
+
const SESSION = 'e2e-test';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Execute agent-browser command
|
|
16
|
+
*/
|
|
17
|
+
function browser(command: string): string {
|
|
18
|
+
try {
|
|
19
|
+
const result = execSync(`agent-browser --session ${SESSION} --json ${command}`, {
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
timeout: 30000,
|
|
22
|
+
});
|
|
23
|
+
return result;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`Browser command failed: ${error instanceof Error ? error.message : error}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse JSON result from agent-browser
|
|
31
|
+
*/
|
|
32
|
+
function parseResult(output: string): { success: boolean; data?: unknown; error?: string } {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(output);
|
|
35
|
+
} catch {
|
|
36
|
+
return { success: true, data: output.trim() };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('Browser E2E Tests', () => {
|
|
41
|
+
// Skip if not in E2E environment
|
|
42
|
+
const runE2E = process.env.TEST_URL !== undefined;
|
|
43
|
+
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
if (!runE2E) {
|
|
46
|
+
console.log('Skipping E2E tests - TEST_URL not set');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Open test page
|
|
51
|
+
const result = browser(`open ${TEST_URL}`);
|
|
52
|
+
const parsed = parseResult(result);
|
|
53
|
+
expect(parsed.success).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterAll(async () => {
|
|
57
|
+
if (!runE2E) return;
|
|
58
|
+
|
|
59
|
+
// Close browser
|
|
60
|
+
try {
|
|
61
|
+
browser('close');
|
|
62
|
+
} catch {
|
|
63
|
+
// Ignore close errors
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('Navigation', () => {
|
|
68
|
+
it.skipIf(!runE2E)('should navigate to test page', () => {
|
|
69
|
+
const result = browser('get url');
|
|
70
|
+
const parsed = parseResult(result);
|
|
71
|
+
|
|
72
|
+
expect(parsed.success).toBe(true);
|
|
73
|
+
expect(parsed.data).toContain(TEST_URL);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it.skipIf(!runE2E)('should get page title', () => {
|
|
77
|
+
const result = browser('get title');
|
|
78
|
+
const parsed = parseResult(result);
|
|
79
|
+
|
|
80
|
+
expect(parsed.success).toBe(true);
|
|
81
|
+
expect(parsed.data).toBe('Browser Test Page');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('Snapshot', () => {
|
|
86
|
+
it.skipIf(!runE2E)('should take accessibility snapshot', () => {
|
|
87
|
+
const result = browser('snapshot');
|
|
88
|
+
const parsed = parseResult(result);
|
|
89
|
+
|
|
90
|
+
expect(parsed.success).toBe(true);
|
|
91
|
+
expect(parsed.data).toBeDefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it.skipIf(!runE2E)('should get interactive elements only', () => {
|
|
95
|
+
const result = browser('snapshot -i');
|
|
96
|
+
const parsed = parseResult(result);
|
|
97
|
+
|
|
98
|
+
expect(parsed.success).toBe(true);
|
|
99
|
+
// Should have element refs like @e1, @e2
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('Form Interaction', () => {
|
|
104
|
+
it.skipIf(!runE2E)('should fill email input', () => {
|
|
105
|
+
// First get snapshot to find refs
|
|
106
|
+
browser('snapshot -i');
|
|
107
|
+
|
|
108
|
+
// Fill using CSS selector
|
|
109
|
+
const result = browser('fill "#email" "test@example.com"');
|
|
110
|
+
const parsed = parseResult(result);
|
|
111
|
+
|
|
112
|
+
expect(parsed.success).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it.skipIf(!runE2E)('should fill password input', () => {
|
|
116
|
+
const result = browser('fill "#password" "secretpassword"');
|
|
117
|
+
const parsed = parseResult(result);
|
|
118
|
+
|
|
119
|
+
expect(parsed.success).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it.skipIf(!runE2E)('should check checkbox', () => {
|
|
123
|
+
const result = browser('check "#agree"');
|
|
124
|
+
const parsed = parseResult(result);
|
|
125
|
+
|
|
126
|
+
expect(parsed.success).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it.skipIf(!runE2E)('should submit form', () => {
|
|
130
|
+
const result = browser('click "#submit-btn"');
|
|
131
|
+
const parsed = parseResult(result);
|
|
132
|
+
|
|
133
|
+
expect(parsed.success).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it.skipIf(!runE2E)('should see result after form submit', () => {
|
|
137
|
+
// Wait for result to appear
|
|
138
|
+
browser('wait "#result"');
|
|
139
|
+
|
|
140
|
+
const result = browser('get text "#result-data"');
|
|
141
|
+
const parsed = parseResult(result);
|
|
142
|
+
|
|
143
|
+
expect(parsed.success).toBe(true);
|
|
144
|
+
expect(parsed.data).toContain('test@example.com');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('Screenshot', () => {
|
|
149
|
+
it.skipIf(!runE2E)('should take screenshot', () => {
|
|
150
|
+
const result = browser('screenshot');
|
|
151
|
+
const parsed = parseResult(result);
|
|
152
|
+
|
|
153
|
+
expect(parsed.success).toBe(true);
|
|
154
|
+
// Should return base64 image data
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('JavaScript Execution', () => {
|
|
159
|
+
it.skipIf(!runE2E)('should execute JavaScript', () => {
|
|
160
|
+
const result = browser('eval "document.title"');
|
|
161
|
+
const parsed = parseResult(result);
|
|
162
|
+
|
|
163
|
+
expect(parsed.success).toBe(true);
|
|
164
|
+
expect(parsed.data).toBe('Browser Test Page');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it.skipIf(!runE2E)('should extract data with JavaScript', () => {
|
|
168
|
+
const result = browser('eval "document.getElementById(\'email\').value"');
|
|
169
|
+
const parsed = parseResult(result);
|
|
170
|
+
|
|
171
|
+
expect(parsed.success).toBe(true);
|
|
172
|
+
expect(parsed.data).toBe('test@example.com');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|