@askjo/camoufox-browser 1.0.1 → 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.
- package/README.md +1 -1
- package/package.json +38 -2
- package/.env.bak +0 -4
- package/.github/workflows/deploy.yml +0 -21
- package/AGENTS.md +0 -153
- package/experimental/chromium/Dockerfile +0 -35
- package/experimental/chromium/README.md +0 -47
- package/experimental/chromium/run.sh +0 -24
- package/experimental/chromium/server.js +0 -812
- package/fly.toml +0 -29
- package/jest.config.js +0 -41
- package/tests/e2e/concurrency.test.js +0 -103
- package/tests/e2e/formSubmission.test.js +0 -129
- package/tests/e2e/macroNavigation.test.js +0 -92
- package/tests/e2e/navigation.test.js +0 -128
- package/tests/e2e/scroll.test.js +0 -81
- package/tests/e2e/snapshotLinks.test.js +0 -141
- package/tests/e2e/tabLifecycle.test.js +0 -149
- package/tests/e2e/typingEnter.test.js +0 -147
- package/tests/helpers/client.js +0 -222
- package/tests/helpers/startJoBrowser.js +0 -95
- package/tests/helpers/testSite.js +0 -238
- package/tests/live/googleSearch.test.js +0 -93
- package/tests/live/macroExpansion.test.js +0 -132
- package/tests/unit/macros.test.js +0 -123
|
@@ -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
|
-
});
|