@askjo/camoufox-browser 1.0.2 → 1.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.
@@ -1,95 +0,0 @@
1
- const { spawn } = require('child_process');
2
- const path = require('path');
3
-
4
- let serverProcess = null;
5
- let serverPort = null;
6
-
7
- async function waitForServer(port, maxRetries = 30, interval = 1000) {
8
- for (let i = 0; i < maxRetries; i++) {
9
- try {
10
- const response = await fetch(`http://localhost:${port}/health`);
11
- if (response.ok) {
12
- return true;
13
- }
14
- } catch (e) {
15
- // Server not ready yet
16
- }
17
- await new Promise(r => setTimeout(r, interval));
18
- }
19
- throw new Error(`Server failed to start on port ${port} after ${maxRetries} attempts`);
20
- }
21
-
22
- async function startJoBrowser(port = 0) {
23
- // Use a random available port if not specified
24
- const usePort = port || Math.floor(3100 + Math.random() * 900);
25
-
26
- const serverPath = path.join(__dirname, '../../server-camoufox.js');
27
-
28
- serverProcess = spawn('node', [serverPath], {
29
- env: { ...process.env, PORT: usePort.toString(), DEBUG_RESPONSES: 'false' },
30
- stdio: ['ignore', 'pipe', 'pipe'],
31
- detached: false
32
- });
33
-
34
- serverProcess.stdout.on('data', (data) => {
35
- if (process.env.DEBUG_SERVER) {
36
- console.log(`[server] ${data.toString().trim()}`);
37
- }
38
- });
39
-
40
- serverProcess.stderr.on('data', (data) => {
41
- if (process.env.DEBUG_SERVER) {
42
- console.error(`[server:err] ${data.toString().trim()}`);
43
- }
44
- });
45
-
46
- serverProcess.on('error', (err) => {
47
- console.error('Failed to start server:', err);
48
- });
49
-
50
- serverPort = usePort;
51
-
52
- // Wait for server to be ready
53
- await waitForServer(usePort);
54
-
55
- console.log(`jo-browser server started on port ${usePort}`);
56
- return usePort;
57
- }
58
-
59
- async function stopJoBrowser() {
60
- if (serverProcess) {
61
- return new Promise((resolve) => {
62
- serverProcess.on('close', () => {
63
- serverProcess = null;
64
- serverPort = null;
65
- resolve();
66
- });
67
-
68
- // Send SIGTERM for graceful shutdown
69
- serverProcess.kill('SIGTERM');
70
-
71
- // Force kill after 5 seconds if still running
72
- setTimeout(() => {
73
- if (serverProcess) {
74
- serverProcess.kill('SIGKILL');
75
- }
76
- }, 5000);
77
- });
78
- }
79
- }
80
-
81
- function getServerUrl() {
82
- if (!serverPort) throw new Error('Server not started');
83
- return `http://localhost:${serverPort}`;
84
- }
85
-
86
- function getServerPort() {
87
- return serverPort;
88
- }
89
-
90
- module.exports = {
91
- startJoBrowser,
92
- stopJoBrowser,
93
- getServerUrl,
94
- getServerPort
95
- };
@@ -1,238 +0,0 @@
1
- const express = require('express');
2
-
3
- let server = null;
4
- let port = null;
5
-
6
- function createTestApp() {
7
- const app = express();
8
- app.use(express.urlencoded({ extended: true }));
9
-
10
- // Simple pages for navigation tests
11
- app.get('/pageA', (req, res) => {
12
- res.send(`
13
- <!DOCTYPE html>
14
- <html><head><title>Page A</title></head>
15
- <body>
16
- <h1>Welcome to Page A</h1>
17
- <p>This is the first test page.</p>
18
- <a href="/pageB">Go to Page B</a>
19
- </body></html>
20
- `);
21
- });
22
-
23
- app.get('/pageB', (req, res) => {
24
- res.send(`
25
- <!DOCTYPE html>
26
- <html><head><title>Page B</title></head>
27
- <body>
28
- <h1>Welcome to Page B</h1>
29
- <p>This is the second test page.</p>
30
- <a href="/pageA">Go to Page A</a>
31
- </body></html>
32
- `);
33
- });
34
-
35
- // Page with multiple links for links extraction test
36
- app.get('/links', (req, res) => {
37
- res.send(`
38
- <!DOCTYPE html>
39
- <html><head><title>Links Page</title></head>
40
- <body>
41
- <h1>Links Collection</h1>
42
- <ul>
43
- <li><a href="https://example.com/link1">Example Link 1</a></li>
44
- <li><a href="https://example.com/link2">Example Link 2</a></li>
45
- <li><a href="https://example.com/link3">Example Link 3</a></li>
46
- <li><a href="https://example.com/link4">Example Link 4</a></li>
47
- <li><a href="https://example.com/link5">Example Link 5</a></li>
48
- </ul>
49
- </body></html>
50
- `);
51
- });
52
-
53
- // Page for typing tests with live preview
54
- app.get('/typing', (req, res) => {
55
- res.send(`
56
- <!DOCTYPE html>
57
- <html><head><title>Typing Test</title></head>
58
- <body>
59
- <h1>Typing Test Page</h1>
60
- <input type="text" id="input" placeholder="Type here..." />
61
- <div id="preview"></div>
62
- <script>
63
- document.getElementById('input').addEventListener('input', (e) => {
64
- document.getElementById('preview').textContent = 'Preview: ' + e.target.value;
65
- });
66
- </script>
67
- </body></html>
68
- `);
69
- });
70
-
71
- // Page for Enter key test - redirects on Enter
72
- app.get('/enter', (req, res) => {
73
- res.send(`
74
- <!DOCTYPE html>
75
- <html><head><title>Enter Test</title></head>
76
- <body>
77
- <h1>Press Enter Test</h1>
78
- <input type="text" id="searchInput" placeholder="Type and press Enter..." />
79
- <script>
80
- document.getElementById('searchInput').addEventListener('keydown', (e) => {
81
- if (e.key === 'Enter') {
82
- const value = e.target.value;
83
- window.location.href = '/entered?value=' + encodeURIComponent(value);
84
- }
85
- });
86
- </script>
87
- </body></html>
88
- `);
89
- });
90
-
91
- app.get('/entered', (req, res) => {
92
- const value = req.query.value || '';
93
- res.send(`
94
- <!DOCTYPE html>
95
- <html><head><title>Entered</title></head>
96
- <body>
97
- <h1>Entry Received</h1>
98
- <p id="result">Entered: ${value}</p>
99
- </body></html>
100
- `);
101
- });
102
-
103
- // Form submission test
104
- app.get('/form', (req, res) => {
105
- res.send(`
106
- <!DOCTYPE html>
107
- <html><head><title>Form Test</title></head>
108
- <body>
109
- <h1>Form Submission Test</h1>
110
- <form action="/submitted" method="POST">
111
- <label for="username">Username:</label>
112
- <input type="text" id="username" name="username" />
113
- <br/>
114
- <label for="email">Email:</label>
115
- <input type="email" id="email" name="email" />
116
- <br/>
117
- <button type="submit" id="submitBtn">Submit</button>
118
- </form>
119
- </body></html>
120
- `);
121
- });
122
-
123
- app.post('/submitted', (req, res) => {
124
- const { username, email } = req.body;
125
- res.send(`
126
- <!DOCTYPE html>
127
- <html><head><title>Submitted</title></head>
128
- <body>
129
- <h1>Form Submitted Successfully</h1>
130
- <p id="username">Username: ${username || ''}</p>
131
- <p id="email">Email: ${email || ''}</p>
132
- </body></html>
133
- `);
134
- });
135
-
136
- // Page with refresh counter (to verify refresh actually works)
137
- let refreshCount = 0;
138
- app.get('/refresh-test', (req, res) => {
139
- refreshCount++;
140
- res.send(`
141
- <!DOCTYPE html>
142
- <html><head><title>Refresh Test</title></head>
143
- <body>
144
- <h1>Refresh Counter</h1>
145
- <p id="count">Count: ${refreshCount}</p>
146
- </body></html>
147
- `);
148
- });
149
-
150
- // Reset refresh counter (for test isolation)
151
- app.post('/reset-refresh-count', (req, res) => {
152
- refreshCount = 0;
153
- res.json({ ok: true });
154
- });
155
-
156
- // Page with clickable button
157
- app.get('/click', (req, res) => {
158
- res.send(`
159
- <!DOCTYPE html>
160
- <html><head><title>Click Test</title></head>
161
- <body>
162
- <h1>Click Test Page</h1>
163
- <button id="clickMe">Click Me</button>
164
- <div id="result"></div>
165
- <script>
166
- document.getElementById('clickMe').addEventListener('click', () => {
167
- document.getElementById('result').textContent = 'Button was clicked!';
168
- });
169
- </script>
170
- </body></html>
171
- `);
172
- });
173
-
174
- // Echo endpoint for macro expansion testing - echoes the full request URL
175
- app.get('/echo-url', (req, res) => {
176
- res.send(`
177
- <!DOCTYPE html>
178
- <html><head><title>Echo URL</title></head>
179
- <body>
180
- <h1>URL Echo</h1>
181
- <pre id="url">${req.originalUrl}</pre>
182
- <pre id="query">${JSON.stringify(req.query)}</pre>
183
- </body></html>
184
- `);
185
- });
186
-
187
- // Page with scrollable content
188
- app.get('/scroll', (req, res) => {
189
- const items = Array.from({ length: 100 }, (_, i) => `<p id="item${i}">Item ${i}</p>`).join('\n');
190
- res.send(`
191
- <!DOCTYPE html>
192
- <html><head><title>Scroll Test</title></head>
193
- <body style="height: 5000px;">
194
- <h1>Scroll Test Page</h1>
195
- ${items}
196
- <div id="bottom">Bottom of page</div>
197
- </body></html>
198
- `);
199
- });
200
-
201
- return app;
202
- }
203
-
204
- async function startTestSite(preferredPort = 0) {
205
- const app = createTestApp();
206
-
207
- return new Promise((resolve, reject) => {
208
- server = app.listen(preferredPort, () => {
209
- port = server.address().port;
210
- console.log(`Test site running on port ${port}`);
211
- resolve(port);
212
- });
213
- server.on('error', reject);
214
- });
215
- }
216
-
217
- async function stopTestSite() {
218
- if (server) {
219
- return new Promise((resolve) => {
220
- server.close(() => {
221
- server = null;
222
- port = null;
223
- resolve();
224
- });
225
- });
226
- }
227
- }
228
-
229
- function getTestSiteUrl() {
230
- if (!port) throw new Error('Test site not started');
231
- return `http://localhost:${port}`;
232
- }
233
-
234
- module.exports = {
235
- startTestSite,
236
- stopTestSite,
237
- getTestSiteUrl
238
- };
@@ -1,93 +0,0 @@
1
- const { startJoBrowser, stopJoBrowser, getServerUrl } = require('../helpers/startJoBrowser');
2
- const { createClient } = require('../helpers/client');
3
-
4
- // Live Google tests are opt-in due to potential captchas/rate limiting
5
- const SKIP_LIVE_TESTS = !process.env.RUN_LIVE_TESTS;
6
-
7
- describe('Live Google Search', () => {
8
- let serverUrl;
9
-
10
- beforeAll(async () => {
11
- if (SKIP_LIVE_TESTS) return;
12
-
13
- const port = await startJoBrowser();
14
- serverUrl = getServerUrl();
15
- }, 120000);
16
-
17
- afterAll(async () => {
18
- if (SKIP_LIVE_TESTS) return;
19
- await stopJoBrowser();
20
- }, 30000);
21
-
22
- (SKIP_LIVE_TESTS ? test.skip : test)('Google search via @google_search macro', async () => {
23
- const client = createClient(serverUrl);
24
-
25
- try {
26
- const { tabId } = await client.createTab();
27
-
28
- // Use the @google_search macro
29
- const result = await client.navigate(tabId, '@google_search Camoufox playwright browser');
30
-
31
- expect(result.ok).toBe(true);
32
- expect(result.url).toContain('google.com');
33
-
34
- // Get snapshot - should contain search results
35
- const snapshot = await client.getSnapshot(tabId);
36
-
37
- expect(snapshot.snapshot).toBeDefined();
38
- expect(snapshot.snapshot.length).toBeGreaterThan(100);
39
-
40
- // Should contain at least one of the search terms
41
- const containsSearchTerm =
42
- snapshot.snapshot.toLowerCase().includes('camoufox') ||
43
- snapshot.snapshot.toLowerCase().includes('playwright') ||
44
- snapshot.snapshot.toLowerCase().includes('browser');
45
-
46
- expect(containsSearchTerm).toBe(true);
47
-
48
- // Get links - should have search result links
49
- const linksResult = await client.getLinks(tabId, { limit: 20 });
50
- expect(linksResult.links.length).toBeGreaterThan(0);
51
-
52
- } finally {
53
- await client.cleanup();
54
- }
55
- }, 120000); // 2 minute timeout for live test
56
-
57
- (SKIP_LIVE_TESTS ? test.skip : test)('click on Google search result', async () => {
58
- const client = createClient(serverUrl);
59
-
60
- try {
61
- const { tabId } = await client.createTab();
62
-
63
- // Search for something specific
64
- await client.navigate(tabId, '@google_search playwright documentation');
65
-
66
- // Get snapshot to find a result link
67
- const snapshot = await client.getSnapshot(tabId);
68
-
69
- // Look for playwright.dev link in refs
70
- const playwriteMatch = snapshot.snapshot.match(/\[(e\d+)\].*playwright\.dev/i);
71
-
72
- if (playwriteMatch) {
73
- const ref = playwriteMatch[1];
74
-
75
- // Click the link
76
- await client.click(tabId, { ref });
77
-
78
- // Wait for navigation
79
- await new Promise(r => setTimeout(r, 3000));
80
-
81
- const newSnapshot = await client.getSnapshot(tabId);
82
- expect(newSnapshot.url).toContain('playwright');
83
- } else {
84
- // If no playwright.dev link found, test still passes
85
- // (Google results can vary)
86
- console.log('No playwright.dev link found in search results, skipping click test');
87
- }
88
-
89
- } finally {
90
- await client.cleanup();
91
- }
92
- }, 120000);
93
- });
@@ -1,132 +0,0 @@
1
- const { startJoBrowser, stopJoBrowser, getServerUrl } = require('../helpers/startJoBrowser');
2
- const { createClient } = require('../helpers/client');
3
-
4
- // Live macro tests are opt-in due to external site dependencies
5
- const SKIP_LIVE_TESTS = !process.env.RUN_LIVE_TESTS;
6
-
7
- describe('Live Macro URL Expansion', () => {
8
- let serverUrl;
9
-
10
- beforeAll(async () => {
11
- if (SKIP_LIVE_TESTS) return;
12
-
13
- const port = await startJoBrowser();
14
- serverUrl = getServerUrl();
15
- }, 120000);
16
-
17
- afterAll(async () => {
18
- if (SKIP_LIVE_TESTS) return;
19
- await stopJoBrowser();
20
- }, 30000);
21
-
22
- (SKIP_LIVE_TESTS ? test.skip : test)('@google_search expands to correct URL', async () => {
23
- const client = createClient(serverUrl);
24
-
25
- try {
26
- const { tabId } = await client.createTab();
27
-
28
- const result = await client.navigate(tabId, '@google_search test query');
29
-
30
- expect(result.ok).toBe(true);
31
- expect(result.url).toContain('google.com/search');
32
- expect(result.url).toContain('q=test');
33
- } finally {
34
- await client.cleanup();
35
- }
36
- }, 60000);
37
-
38
- (SKIP_LIVE_TESTS ? test.skip : test)('@youtube_search expands to correct URL', async () => {
39
- const client = createClient(serverUrl);
40
-
41
- try {
42
- const { tabId } = await client.createTab();
43
-
44
- const result = await client.navigate(tabId, '@youtube_search funny cats');
45
-
46
- expect(result.ok).toBe(true);
47
- expect(result.url).toContain('youtube.com/results');
48
- expect(result.url).toContain('search_query=funny');
49
- } finally {
50
- await client.cleanup();
51
- }
52
- }, 60000);
53
-
54
- (SKIP_LIVE_TESTS ? test.skip : test)('@amazon_search expands to correct URL', async () => {
55
- const client = createClient(serverUrl);
56
-
57
- try {
58
- const { tabId } = await client.createTab();
59
-
60
- const result = await client.navigate(tabId, '@amazon_search laptop stand');
61
-
62
- expect(result.ok).toBe(true);
63
- expect(result.url).toContain('amazon.com/s');
64
- expect(result.url).toMatch(/k=laptop[\+%20]stand/);
65
- } finally {
66
- await client.cleanup();
67
- }
68
- }, 60000);
69
-
70
- (SKIP_LIVE_TESTS ? test.skip : test)('@wikipedia_search expands to correct URL', async () => {
71
- const client = createClient(serverUrl);
72
-
73
- try {
74
- const { tabId } = await client.createTab();
75
-
76
- const result = await client.navigate(tabId, '@wikipedia_search JavaScript');
77
-
78
- expect(result.ok).toBe(true);
79
- expect(result.url).toContain('wikipedia.org');
80
- // Wikipedia may redirect to article or stay on search
81
- expect(result.url).toMatch(/JavaScript|search/i);
82
- } finally {
83
- await client.cleanup();
84
- }
85
- }, 60000);
86
-
87
- (SKIP_LIVE_TESTS ? test.skip : test)('@reddit_search expands to correct URL', async () => {
88
- const client = createClient(serverUrl);
89
-
90
- try {
91
- const { tabId } = await client.createTab();
92
-
93
- const result = await client.navigate(tabId, '@reddit_search programming');
94
-
95
- expect(result.ok).toBe(true);
96
- expect(result.url).toContain('reddit.com/search');
97
- expect(result.url).toContain('q=programming');
98
- } finally {
99
- await client.cleanup();
100
- }
101
- }, 60000);
102
-
103
- (SKIP_LIVE_TESTS ? test.skip : test)('special characters are URL encoded (live)', async () => {
104
- const client = createClient(serverUrl);
105
-
106
- try {
107
- const { tabId } = await client.createTab();
108
-
109
- const result = await client.navigate(tabId, '@google_search hello & world');
110
-
111
- expect(result.ok).toBe(true);
112
- // & should be encoded as %26
113
- expect(result.url).toContain('q=hello%20%26%20world');
114
- } finally {
115
- await client.cleanup();
116
- }
117
- }, 60000);
118
-
119
- (SKIP_LIVE_TESTS ? test.skip : test)('unknown macro returns error', async () => {
120
- const client = createClient(serverUrl);
121
-
122
- try {
123
- const { tabId } = await client.createTab();
124
-
125
- // Unknown macro with no fallback URL should fail
126
- await expect(client.navigate(tabId, '@unknown_macro test'))
127
- .rejects.toThrow(/url or macro required/);
128
- } finally {
129
- await client.cleanup();
130
- }
131
- }, 60000);
132
- });
@@ -1,123 +0,0 @@
1
- const { expandMacro, getSupportedMacros, MACROS } = require('../../lib/macros');
2
-
3
- describe('Macro URL Expansion (unit)', () => {
4
-
5
- test('all macros are defined', () => {
6
- const macros = getSupportedMacros();
7
- expect(macros).toContain('@google_search');
8
- expect(macros).toContain('@youtube_search');
9
- expect(macros).toContain('@amazon_search');
10
- expect(macros).toContain('@reddit_search');
11
- expect(macros).toContain('@wikipedia_search');
12
- expect(macros).toContain('@twitter_search');
13
- expect(macros).toContain('@yelp_search');
14
- expect(macros).toContain('@spotify_search');
15
- expect(macros).toContain('@netflix_search');
16
- expect(macros).toContain('@linkedin_search');
17
- expect(macros).toContain('@instagram_search');
18
- expect(macros).toContain('@tiktok_search');
19
- expect(macros).toContain('@twitch_search');
20
- expect(macros.length).toBe(13);
21
- });
22
-
23
- test('@google_search expands correctly', () => {
24
- expect(expandMacro('@google_search', 'test query'))
25
- .toBe('https://www.google.com/search?q=test%20query');
26
- });
27
-
28
- test('@youtube_search expands correctly', () => {
29
- expect(expandMacro('@youtube_search', 'funny cats'))
30
- .toBe('https://www.youtube.com/results?search_query=funny%20cats');
31
- });
32
-
33
- test('@amazon_search expands correctly', () => {
34
- expect(expandMacro('@amazon_search', 'laptop stand'))
35
- .toBe('https://www.amazon.com/s?k=laptop%20stand');
36
- });
37
-
38
- test('@reddit_search expands correctly', () => {
39
- expect(expandMacro('@reddit_search', 'programming'))
40
- .toBe('https://www.reddit.com/search/?q=programming');
41
- });
42
-
43
- test('@wikipedia_search expands correctly', () => {
44
- expect(expandMacro('@wikipedia_search', 'JavaScript'))
45
- .toBe('https://en.wikipedia.org/wiki/Special:Search?search=JavaScript');
46
- });
47
-
48
- test('@twitter_search expands correctly', () => {
49
- expect(expandMacro('@twitter_search', 'breaking news'))
50
- .toBe('https://twitter.com/search?q=breaking%20news');
51
- });
52
-
53
- test('@yelp_search expands correctly', () => {
54
- expect(expandMacro('@yelp_search', 'italian restaurant'))
55
- .toBe('https://www.yelp.com/search?find_desc=italian%20restaurant');
56
- });
57
-
58
- test('@spotify_search expands correctly', () => {
59
- expect(expandMacro('@spotify_search', 'jazz music'))
60
- .toBe('https://open.spotify.com/search/jazz%20music');
61
- });
62
-
63
- test('@netflix_search expands correctly', () => {
64
- expect(expandMacro('@netflix_search', 'comedy'))
65
- .toBe('https://www.netflix.com/search?q=comedy');
66
- });
67
-
68
- test('@linkedin_search expands correctly', () => {
69
- expect(expandMacro('@linkedin_search', 'software engineer'))
70
- .toBe('https://www.linkedin.com/search/results/all/?keywords=software%20engineer');
71
- });
72
-
73
- test('@instagram_search expands correctly', () => {
74
- expect(expandMacro('@instagram_search', 'travel'))
75
- .toBe('https://www.instagram.com/explore/tags/travel');
76
- });
77
-
78
- test('@tiktok_search expands correctly', () => {
79
- expect(expandMacro('@tiktok_search', 'dance'))
80
- .toBe('https://www.tiktok.com/search?q=dance');
81
- });
82
-
83
- test('@twitch_search expands correctly', () => {
84
- expect(expandMacro('@twitch_search', 'gaming'))
85
- .toBe('https://www.twitch.tv/search?term=gaming');
86
- });
87
-
88
- test('special characters are URL encoded', () => {
89
- expect(expandMacro('@google_search', 'hello & world'))
90
- .toBe('https://www.google.com/search?q=hello%20%26%20world');
91
-
92
- expect(expandMacro('@google_search', 'test?param=value'))
93
- .toBe('https://www.google.com/search?q=test%3Fparam%3Dvalue');
94
-
95
- expect(expandMacro('@google_search', 'C++ programming'))
96
- .toBe('https://www.google.com/search?q=C%2B%2B%20programming');
97
- });
98
-
99
- test('empty query is handled', () => {
100
- expect(expandMacro('@google_search', ''))
101
- .toBe('https://www.google.com/search?q=');
102
-
103
- expect(expandMacro('@google_search', null))
104
- .toBe('https://www.google.com/search?q=');
105
-
106
- expect(expandMacro('@google_search', undefined))
107
- .toBe('https://www.google.com/search?q=');
108
- });
109
-
110
- test('unknown macro returns null', () => {
111
- expect(expandMacro('@unknown_macro', 'test')).toBeNull();
112
- expect(expandMacro('@fake_search', 'query')).toBeNull();
113
- expect(expandMacro('google_search', 'no @ prefix')).toBeNull();
114
- });
115
-
116
- test('unicode characters are encoded', () => {
117
- expect(expandMacro('@google_search', '日本語'))
118
- .toBe('https://www.google.com/search?q=%E6%97%A5%E6%9C%AC%E8%AA%9E');
119
-
120
- expect(expandMacro('@google_search', 'café'))
121
- .toBe('https://www.google.com/search?q=caf%C3%A9');
122
- });
123
- });