run-page 1.0.0
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/LICENSE +21 -0
- package/README.md +127 -0
- package/cli.js +298 -0
- package/package.json +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Job Vranish
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# run-page
|
|
2
|
+
|
|
3
|
+
Run a web page in headless Chrome and capture console output.
|
|
4
|
+
|
|
5
|
+
A minimal CLI tool for running web-based tests without a testing framework. Your tests live in the page itself - just use `console.log()` to report results.
|
|
6
|
+
|
|
7
|
+
## What is this?
|
|
8
|
+
|
|
9
|
+
This is a simple command-line utility that opens a URL in a headless browser and captures the console output. It's designed for automated testing with a specific philosophy: the test infrastructure shouldn't provide anything you can't get by manually opening the page in a browser.
|
|
10
|
+
|
|
11
|
+
Your tests are built directly into your HTML pages using standard `console.log()` for output. The results can be visible both on the page itself and in the console. This means you can run your tests from the command line for automation, or simply open the HTML file in a browser to see the same results.
|
|
12
|
+
|
|
13
|
+
Made by Claude, but I personally use it frequently.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## How It Works
|
|
17
|
+
|
|
18
|
+
1. **Opens the URL** in a headless Chrome browser
|
|
19
|
+
2. **Captures `console.log()` output** and prints it to your terminal
|
|
20
|
+
3. **Waits for completion signal** - a console message matching the done pattern
|
|
21
|
+
4. **Exits with the code** from the completion message
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install run-page
|
|
27
|
+
# or
|
|
28
|
+
npx run-page <url>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
run-page <url> [options]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Arguments
|
|
38
|
+
|
|
39
|
+
- `<url>` - The URL to load (local file, localhost, or remote URL)
|
|
40
|
+
|
|
41
|
+
### Options
|
|
42
|
+
|
|
43
|
+
- `-t, --timeout <ms>` - Timeout in milliseconds (default: 30000)
|
|
44
|
+
- `-p, --done-pattern <regex>` - Regex pattern to detect completion (default: `^DONE:(\d+)$`)
|
|
45
|
+
- `--no-color` - Disable colored output
|
|
46
|
+
- `-h, --help` - Show help message
|
|
47
|
+
- `-v, --version` - Show version number
|
|
48
|
+
|
|
49
|
+
### Examples
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Basic usage
|
|
53
|
+
run-page http://localhost:8080/test.html
|
|
54
|
+
|
|
55
|
+
# Local file
|
|
56
|
+
run-page test.html
|
|
57
|
+
|
|
58
|
+
# Custom timeout (60 seconds)
|
|
59
|
+
run-page test.html --timeout 60000
|
|
60
|
+
|
|
61
|
+
# Custom completion pattern
|
|
62
|
+
run-page test.html --done-pattern "^TEST_COMPLETE:(\d+)$"
|
|
63
|
+
|
|
64
|
+
# Disable colors
|
|
65
|
+
run-page test.html --no-color
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Completion Pattern
|
|
69
|
+
|
|
70
|
+
By default, `run-page` waits for a console message matching:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
DONE:<exit-code>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Where `<exit-code>` is a number (0 = success, non-zero = failure).
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
- `console.log('DONE:0')` - Exit with code 0 (success)
|
|
80
|
+
- `console.log('DONE:1')` - Exit with code 1 (failure)
|
|
81
|
+
- `console.log('DONE:5')` - Exit with code 5
|
|
82
|
+
|
|
83
|
+
You can customize this pattern with `--done-pattern`.
|
|
84
|
+
|
|
85
|
+
### Exit Codes
|
|
86
|
+
|
|
87
|
+
- **0** - Tests passed (from `DONE:0`)
|
|
88
|
+
- **Non-zero** - Tests failed (from `DONE:<n>` where n > 0)
|
|
89
|
+
- **1** - Timeout, page error, or invalid arguments
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
## Console Message Types
|
|
93
|
+
|
|
94
|
+
Different console methods are handled differently:
|
|
95
|
+
|
|
96
|
+
- `console.log()` → stdout (with color formatting)
|
|
97
|
+
- `console.error()` → stderr
|
|
98
|
+
- `console.warn()` → stderr
|
|
99
|
+
- Uncaught errors → stderr with "Uncaught error:" prefix
|
|
100
|
+
|
|
101
|
+
## Example Test Patterns
|
|
102
|
+
|
|
103
|
+
### Simple Pass/Fail
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
let failures = 0;
|
|
107
|
+
|
|
108
|
+
function test(name, fn) {
|
|
109
|
+
try {
|
|
110
|
+
fn();
|
|
111
|
+
console.log(`✔ ${name}`);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.log(`✘ ${name}: ${err.message}`);
|
|
114
|
+
failures++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
test('addition works', () => {
|
|
119
|
+
if (1 + 1 !== 2) throw new Error('Math is broken');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('arrays work', () => {
|
|
123
|
+
if ([1, 2, 3].length !== 3) throw new Error('Arrays broken');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.log(`DONE:${failures}`);
|
|
127
|
+
```
|
package/cli.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} RunPageOptions
|
|
5
|
+
* @property {string} url - The URL to load in the headless browser
|
|
6
|
+
* @property {number} timeout - Timeout in milliseconds before failing
|
|
7
|
+
* @property {RegExp} donePattern - Regex pattern to detect test completion
|
|
8
|
+
* @property {boolean} color - Whether to enable colored output
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const puppeteer = require('puppeteer');
|
|
12
|
+
const pc = require('picocolors');
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
15
|
+
const DEFAULT_DONE_PATTERN = /^DONE:(\d+)$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse command line arguments
|
|
19
|
+
* @returns {RunPageOptions | null} Parsed options or null if help/version was shown
|
|
20
|
+
*/
|
|
21
|
+
function parseArgs() {
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
|
|
24
|
+
/** @type {Partial<RunPageOptions>} */
|
|
25
|
+
const options = {
|
|
26
|
+
timeout: DEFAULT_TIMEOUT,
|
|
27
|
+
donePattern: DEFAULT_DONE_PATTERN,
|
|
28
|
+
color: shouldUseColor(),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let url = null;
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
const arg = args[i];
|
|
35
|
+
|
|
36
|
+
if (arg === '-h' || arg === '--help') {
|
|
37
|
+
showHelp();
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (arg === '-v' || arg === '--version') {
|
|
42
|
+
showVersion();
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (arg === '--no-color') {
|
|
47
|
+
options.color = false;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (arg === '-t' || arg === '--timeout') {
|
|
52
|
+
const value = args[++i];
|
|
53
|
+
if (!value || isNaN(value)) {
|
|
54
|
+
console.error('Error: --timeout requires a numeric value in milliseconds');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
options.timeout = parseInt(value, 10);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (arg === '-p' || arg === '--done-pattern') {
|
|
62
|
+
const value = args[++i];
|
|
63
|
+
if (!value) {
|
|
64
|
+
console.error('Error: --done-pattern requires a regex pattern');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
options.donePattern = new RegExp(value);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error(`Error: Invalid regex pattern: ${err.message}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (arg.startsWith('-')) {
|
|
77
|
+
console.error(`Error: Unknown option: ${arg}`);
|
|
78
|
+
console.error('Run with --help for usage information');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!url) {
|
|
83
|
+
url = arg;
|
|
84
|
+
} else {
|
|
85
|
+
console.error('Error: Multiple URLs provided. Only one URL is allowed.');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!url) {
|
|
91
|
+
console.error('Error: URL is required');
|
|
92
|
+
console.error('Usage: run-page <url> [options]');
|
|
93
|
+
console.error('Run with --help for more information');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
options.url = url;
|
|
98
|
+
return /** @type {RunPageOptions} */ (options);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Determine if colored output should be used
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
function shouldUseColor() {
|
|
106
|
+
// Respect NO_COLOR environment variable
|
|
107
|
+
if ('NO_COLOR' in process.env) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Respect FORCE_COLOR environment variable
|
|
112
|
+
if ('FORCE_COLOR' in process.env) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Auto-detect TTY
|
|
117
|
+
return process.stdout.isTTY;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Show help message
|
|
122
|
+
*/
|
|
123
|
+
function showHelp() {
|
|
124
|
+
console.log(`
|
|
125
|
+
run-page - Run a web page in headless Chrome and capture console output
|
|
126
|
+
|
|
127
|
+
Usage:
|
|
128
|
+
run-page <url> [options]
|
|
129
|
+
|
|
130
|
+
Options:
|
|
131
|
+
-t, --timeout <ms> Timeout in milliseconds (default: 30000)
|
|
132
|
+
-p, --done-pattern <regex> Regex pattern to detect completion (default: "^DONE:(\\\\d+)$")
|
|
133
|
+
--no-color Disable colored output
|
|
134
|
+
-h, --help Show this help message
|
|
135
|
+
-v, --version Show version number
|
|
136
|
+
|
|
137
|
+
Examples:
|
|
138
|
+
run-page http://localhost:8080/test.html
|
|
139
|
+
run-page test.html --timeout 60000
|
|
140
|
+
run-page test.html --done-pattern "^TEST_COMPLETE:(\\\\d+)$"
|
|
141
|
+
run-page test.html --no-color
|
|
142
|
+
|
|
143
|
+
Environment Variables:
|
|
144
|
+
NO_COLOR Disable colored output
|
|
145
|
+
FORCE_COLOR Force colored output even if not a TTY
|
|
146
|
+
|
|
147
|
+
The page should signal completion by logging a message matching the done pattern.
|
|
148
|
+
The first capture group should contain the exit code (0 = success).
|
|
149
|
+
|
|
150
|
+
Example test page:
|
|
151
|
+
console.log('✔ Test passed');
|
|
152
|
+
console.log('✘ Test failed');
|
|
153
|
+
console.log('DONE:0'); // Exit with code 0
|
|
154
|
+
`.trim());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Show version
|
|
159
|
+
*/
|
|
160
|
+
function showVersion() {
|
|
161
|
+
const pkg = require('./package.json');
|
|
162
|
+
console.log(pkg.version);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Format console output with colors if enabled
|
|
167
|
+
* @param {string} text - The text to format
|
|
168
|
+
* @param {boolean} useColor - Whether to apply colors
|
|
169
|
+
* @returns {string}
|
|
170
|
+
*/
|
|
171
|
+
function formatOutput(text, useColor) {
|
|
172
|
+
if (!useColor) {
|
|
173
|
+
return text;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Green for check marks - only color the symbol
|
|
177
|
+
if (/^[✔✓]/.test(text)) {
|
|
178
|
+
return text.replace(/^([✔✓])/, pc.green('$1'));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Red for X marks - only color the symbol
|
|
182
|
+
if (/^[✘✗]/.test(text)) {
|
|
183
|
+
return text.replace(/^([✘✗])/, pc.red('$1'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return text;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Convert URL to a valid format for Puppeteer
|
|
191
|
+
* @param {string} url - The URL or file path
|
|
192
|
+
* @returns {string} Valid URL for Puppeteer
|
|
193
|
+
*/
|
|
194
|
+
function normalizeUrl(url) {
|
|
195
|
+
const fs = require('fs');
|
|
196
|
+
const path = require('path');
|
|
197
|
+
const { pathToFileURL } = require('url');
|
|
198
|
+
|
|
199
|
+
// If it's already a valid URL (http:// or https:// or file://), return as-is
|
|
200
|
+
if (/^(https?|file):\/\//i.test(url)) {
|
|
201
|
+
return url;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Otherwise, treat it as a file path
|
|
205
|
+
const absolutePath = path.resolve(process.cwd(), url);
|
|
206
|
+
|
|
207
|
+
// Check if file exists
|
|
208
|
+
if (!fs.existsSync(absolutePath)) {
|
|
209
|
+
throw new Error(`File not found: ${absolutePath}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Convert to file:// URL using Node's built-in function (handles cross-platform)
|
|
213
|
+
return pathToFileURL(absolutePath).href;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Run the page and capture console output
|
|
218
|
+
* @param {RunPageOptions} options
|
|
219
|
+
* @returns {Promise<number>} Exit code
|
|
220
|
+
*/
|
|
221
|
+
async function runPage(options) {
|
|
222
|
+
const browser = await puppeteer.launch({ headless: true });
|
|
223
|
+
const page = await browser.newPage();
|
|
224
|
+
|
|
225
|
+
// Normalize the URL
|
|
226
|
+
const normalizedUrl = normalizeUrl(options.url);
|
|
227
|
+
|
|
228
|
+
let exitCode = 1;
|
|
229
|
+
let done = false;
|
|
230
|
+
let resolveDone;
|
|
231
|
+
const donePromise = new Promise(r => { resolveDone = r; });
|
|
232
|
+
|
|
233
|
+
page.on('console', msg => {
|
|
234
|
+
const text = msg.text();
|
|
235
|
+
const match = text.match(options.donePattern);
|
|
236
|
+
|
|
237
|
+
if (match) {
|
|
238
|
+
// Extract exit code from first capture group
|
|
239
|
+
const capturedCode = match[1];
|
|
240
|
+
if (capturedCode !== undefined) {
|
|
241
|
+
exitCode = parseInt(capturedCode, 10);
|
|
242
|
+
if (isNaN(exitCode) || exitCode < 0 || exitCode > 255) {
|
|
243
|
+
process.stderr.write(`Warning: Invalid exit code '${capturedCode}', using 1\n`);
|
|
244
|
+
exitCode = 1;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
done = true;
|
|
248
|
+
resolveDone();
|
|
249
|
+
} else {
|
|
250
|
+
// Format and output the message
|
|
251
|
+
const formattedText = formatOutput(text, options.color);
|
|
252
|
+
|
|
253
|
+
// Route to appropriate stream based on message type
|
|
254
|
+
const type = msg.type();
|
|
255
|
+
const out = type === 'error' || type === 'warning' ? process.stderr : process.stdout;
|
|
256
|
+
out.write(formattedText + '\n');
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
page.on('pageerror', err => {
|
|
261
|
+
process.stderr.write(`Uncaught error: ${err.message}\n`);
|
|
262
|
+
done = true;
|
|
263
|
+
resolveDone();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
await page.goto(normalizedUrl, { waitUntil: 'domcontentloaded' });
|
|
268
|
+
} catch (err) {
|
|
269
|
+
process.stderr.write(`Failed to load URL: ${err.message}\n`);
|
|
270
|
+
await browser.close();
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
await Promise.race([
|
|
275
|
+
donePromise,
|
|
276
|
+
new Promise((_, reject) =>
|
|
277
|
+
setTimeout(() => reject(new Error(`Timed out after ${options.timeout}ms`)), options.timeout)
|
|
278
|
+
),
|
|
279
|
+
]).catch(err => {
|
|
280
|
+
process.stderr.write(`${err.message}\n`);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await browser.close();
|
|
284
|
+
return exitCode;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Main execution
|
|
288
|
+
(async () => {
|
|
289
|
+
const options = parseArgs();
|
|
290
|
+
|
|
291
|
+
if (!options) {
|
|
292
|
+
// Help or version was shown
|
|
293
|
+
process.exit(0);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const exitCode = await runPage(options);
|
|
297
|
+
process.exit(exitCode);
|
|
298
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "run-page",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Run a web page in headless Chrome and capture console output",
|
|
5
|
+
"main": "cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"run-page": "cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node cli.js example/test.html",
|
|
11
|
+
"typecheck": "tsc --noEmit --allowJs --checkJs cli.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"test",
|
|
15
|
+
"headless",
|
|
16
|
+
"browser",
|
|
17
|
+
"console",
|
|
18
|
+
"chrome",
|
|
19
|
+
"puppeteer",
|
|
20
|
+
"testing",
|
|
21
|
+
"cli",
|
|
22
|
+
"automation"
|
|
23
|
+
],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"type": "commonjs",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=14"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"puppeteer": "^24.40.0",
|
|
32
|
+
"picocolors": "^1.1.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.7.2"
|
|
36
|
+
}
|
|
37
|
+
}
|