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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/cli.js +298 -0
  4. 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
+ }