@logicsync/pixelprobe 0.1.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +377 -0
  3. package/dist/bin/pixelprobe.d.ts +3 -0
  4. package/dist/bin/pixelprobe.d.ts.map +1 -0
  5. package/dist/bin/pixelprobe.js +218 -0
  6. package/dist/bin/pixelprobe.js.map +1 -0
  7. package/dist/browser/BrowserManager.d.ts +20 -0
  8. package/dist/browser/BrowserManager.d.ts.map +1 -0
  9. package/dist/browser/BrowserManager.js +64 -0
  10. package/dist/browser/BrowserManager.js.map +1 -0
  11. package/dist/browser/ViewportManager.d.ts +15 -0
  12. package/dist/browser/ViewportManager.d.ts.map +1 -0
  13. package/dist/browser/ViewportManager.js +65 -0
  14. package/dist/browser/ViewportManager.js.map +1 -0
  15. package/dist/cli/formatter.d.ts +4 -0
  16. package/dist/cli/formatter.d.ts.map +1 -0
  17. package/dist/cli/formatter.js +107 -0
  18. package/dist/cli/formatter.js.map +1 -0
  19. package/dist/comparator/DiffAggregator.d.ts +24 -0
  20. package/dist/comparator/DiffAggregator.d.ts.map +1 -0
  21. package/dist/comparator/DiffAggregator.js +94 -0
  22. package/dist/comparator/DiffAggregator.js.map +1 -0
  23. package/dist/comparator/LayoutComparator.d.ts +8 -0
  24. package/dist/comparator/LayoutComparator.d.ts.map +1 -0
  25. package/dist/comparator/LayoutComparator.js +68 -0
  26. package/dist/comparator/LayoutComparator.js.map +1 -0
  27. package/dist/comparator/PixelComparator.d.ts +9 -0
  28. package/dist/comparator/PixelComparator.d.ts.map +1 -0
  29. package/dist/comparator/PixelComparator.js +72 -0
  30. package/dist/comparator/PixelComparator.js.map +1 -0
  31. package/dist/comparator/StyleComparator.d.ts +8 -0
  32. package/dist/comparator/StyleComparator.d.ts.map +1 -0
  33. package/dist/comparator/StyleComparator.js +119 -0
  34. package/dist/comparator/StyleComparator.js.map +1 -0
  35. package/dist/core.d.ts +23 -0
  36. package/dist/core.d.ts.map +1 -0
  37. package/dist/core.js +211 -0
  38. package/dist/core.js.map +1 -0
  39. package/dist/extractor/ScreenshotCapturer.d.ts +20 -0
  40. package/dist/extractor/ScreenshotCapturer.d.ts.map +1 -0
  41. package/dist/extractor/ScreenshotCapturer.js +56 -0
  42. package/dist/extractor/ScreenshotCapturer.js.map +1 -0
  43. package/dist/extractor/SectionExtractor.d.ts +26 -0
  44. package/dist/extractor/SectionExtractor.d.ts.map +1 -0
  45. package/dist/extractor/SectionExtractor.js +92 -0
  46. package/dist/extractor/SectionExtractor.js.map +1 -0
  47. package/dist/extractor/StyleExtractor.d.ts +23 -0
  48. package/dist/extractor/StyleExtractor.d.ts.map +1 -0
  49. package/dist/extractor/StyleExtractor.js +88 -0
  50. package/dist/extractor/StyleExtractor.js.map +1 -0
  51. package/dist/index.d.ts +8 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +9 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/mcp/server.d.ts +3 -0
  56. package/dist/mcp/server.d.ts.map +1 -0
  57. package/dist/mcp/server.js +203 -0
  58. package/dist/mcp/server.js.map +1 -0
  59. package/dist/reporter/HTMLReporter.d.ts +8 -0
  60. package/dist/reporter/HTMLReporter.d.ts.map +1 -0
  61. package/dist/reporter/HTMLReporter.js +219 -0
  62. package/dist/reporter/HTMLReporter.js.map +1 -0
  63. package/dist/reporter/JSONReporter.d.ts +17 -0
  64. package/dist/reporter/JSONReporter.d.ts.map +1 -0
  65. package/dist/reporter/JSONReporter.js +77 -0
  66. package/dist/reporter/JSONReporter.js.map +1 -0
  67. package/dist/types/comparison.d.ts +133 -0
  68. package/dist/types/comparison.d.ts.map +1 -0
  69. package/dist/types/comparison.js +3 -0
  70. package/dist/types/comparison.js.map +1 -0
  71. package/dist/types/config.d.ts +138 -0
  72. package/dist/types/config.d.ts.map +1 -0
  73. package/dist/types/config.js +151 -0
  74. package/dist/types/config.js.map +1 -0
  75. package/package.json +72 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LogicSync
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,377 @@
1
+ # pixelprobe
2
+
3
+ Responsive visual & style diff for web page sections — CLI + MCP.
4
+
5
+ ## What it does
6
+
7
+ You're rebuilding a section from a live website. You want to know: does my local build match the original? Where are the styling differences? At which breakpoints do things break?
8
+
9
+ `pixelprobe` answers this by comparing **computed styles**, **layout metrics**, and **pixel-level screenshots** between a source URL and a target URL, across multiple responsive breakpoints.
10
+
11
+ It works on any element — full page sections, individual cards, buttons, navbars, footers, or any element you can target with a CSS selector.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install -g pixelprobe
17
+
18
+ # Or use directly with npx
19
+ npx pixelprobe compare --help
20
+ ```
21
+
22
+ Requires Node.js 18+. On first run, Playwright will download a Chromium binary (~110MB) if one isn't already installed. You can also install it manually:
23
+
24
+ ```bash
25
+ npx playwright install chromium
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ # Compare a hero section between production and your local build
32
+ pixelprobe compare \
33
+ --source https://example.com \
34
+ --target http://localhost:3000 \
35
+ --selector ".hero-section"
36
+ ```
37
+
38
+ This will compare the `.hero-section` element at 4 default breakpoints (mobile, tablet, desktop, desktop-lg) and output a color-coded terminal report plus HTML and JSON reports.
39
+
40
+ ## CLI Usage
41
+
42
+ ### `pixelprobe compare`
43
+
44
+ The main command. Compares a section between two URLs across breakpoints.
45
+
46
+ ```bash
47
+ pixelprobe compare \
48
+ --source <url> \
49
+ --target <url> \
50
+ --selector <css-selector> \
51
+ [options]
52
+ ```
53
+
54
+ #### Options
55
+
56
+ | Flag | Short | Description | Default |
57
+ |------|-------|-------------|---------|
58
+ | `--source` | `-s` | Source URL (the reference/original page) | required |
59
+ | `--target` | `-t` | Target URL (your local build or new version) | required |
60
+ | `--selector` | `-S` | CSS selector for the section on the source page | required |
61
+ | `--target-selector` | `-T` | CSS selector on the target page (if different from source) | same as `--selector` |
62
+ | `--index` | `-i` | Element index if selector matches multiple elements | `0` |
63
+ | `--breakpoints` | `-b` | Comma-separated breakpoint names or `WIDTHxHEIGHT` | `mobile,tablet,desktop,desktop-lg` |
64
+ | `--output` | `-o` | Output directory for reports | `./pixelprobe-output` |
65
+ | `--format` | `-f` | Output format: `json`, `html`, or `both` | `both` |
66
+ | `--threshold` | | Pixel comparison sensitivity (0-1, lower = stricter) | `0.1` |
67
+ | `--wait` | `-w` | Wait N milliseconds after page load before capturing | `0` |
68
+ | `--json` | | Output compact JSON to stdout (for piping to other tools) | `false` |
69
+ | `--headed` | | Run browser in visible (non-headless) mode | `false` |
70
+ | `--no-visual` | | Skip pixel-level visual comparison | `false` |
71
+ | `--no-styles` | | Skip computed style comparison | `false` |
72
+ | `--no-layout` | | Skip layout metrics comparison | `false` |
73
+
74
+ ### `pixelprobe enumerate`
75
+
76
+ List all elements matching a selector on a page. Useful when your selector matches multiple elements and you need to find the right `--index`.
77
+
78
+ ```bash
79
+ pixelprobe enumerate --url <url> --selector <css-selector>
80
+ ```
81
+
82
+ Example output:
83
+
84
+ ```
85
+ Found 3 element(s) matching ".card":
86
+
87
+ [0] <div> (320Ɨ200 at 32,400) — "Pricing Plan A..."
88
+ [1] <div> (320Ɨ200 at 384,400) — "Pricing Plan B..."
89
+ [2] <div> (320Ɨ200 at 736,400) — "Pricing Plan C..."
90
+
91
+ Use --index N with the compare command to select a specific element.
92
+ ```
93
+
94
+ ### `pixelprobe breakpoints`
95
+
96
+ List all available breakpoint presets.
97
+
98
+ ```
99
+ Available breakpoint presets:
100
+
101
+ mobile-sm 320Ɨ568 Mobile Small
102
+ mobile 375Ɨ812 Mobile
103
+ mobile-lg 428Ɨ926 Mobile Large
104
+ tablet 768Ɨ1024 Tablet
105
+ tablet-lg 1024Ɨ1366 Tablet Large
106
+ desktop 1280Ɨ800 Desktop
107
+ desktop-lg 1920Ɨ1080 Desktop Large
108
+ desktop-xl 2560Ɨ1440 Desktop XL
109
+ ```
110
+
111
+ Custom breakpoints use `WIDTHxHEIGHT` format and can be mixed with presets:
112
+
113
+ ```bash
114
+ --breakpoints "mobile,tablet,1440x900,1920x1080"
115
+ ```
116
+
117
+ ## Examples
118
+
119
+ ### Compare a header with different selectors on each site
120
+
121
+ When source and target use different markup (e.g. WordPress vs a custom build):
122
+
123
+ ```bash
124
+ pixelprobe compare \
125
+ --source "https://mysite.com" \
126
+ --target "http://localhost:3000" \
127
+ --selector "#masthead" \
128
+ --target-selector "header" \
129
+ --breakpoints "384x812,768x1024,1024x1366,1280x800,1440x900,1920x1080" \
130
+ --wait 3000
131
+ ```
132
+
133
+ ### Compare a specific card in a grid
134
+
135
+ ```bash
136
+ # First, find which index your card is at
137
+ pixelprobe enumerate --url https://mysite.com --selector ".pricing-card"
138
+
139
+ # Then compare the 2nd card (index 1)
140
+ pixelprobe compare \
141
+ --source "https://mysite.com" \
142
+ --target "http://localhost:3000" \
143
+ --selector ".pricing-card" \
144
+ --index 1
145
+ ```
146
+
147
+ ### Pipe JSON output to another tool
148
+
149
+ ```bash
150
+ # Get machine-readable output for CI/CD
151
+ pixelprobe compare \
152
+ --source "https://mysite.com" \
153
+ --target "http://localhost:3000" \
154
+ --selector ".hero" \
155
+ --json | jq '.summary'
156
+ ```
157
+
158
+ Exit codes: `0` = no differences (pass), `1` = differences found, `2` = error.
159
+
160
+ ### Compare with strict pixel matching
161
+
162
+ ```bash
163
+ pixelprobe compare \
164
+ --source "https://mysite.com" \
165
+ --target "http://localhost:3000" \
166
+ --selector ".hero" \
167
+ --threshold 0.05
168
+ ```
169
+
170
+ ### Style-only comparison (skip screenshots)
171
+
172
+ ```bash
173
+ pixelprobe compare \
174
+ --source "https://mysite.com" \
175
+ --target "http://localhost:3000" \
176
+ --selector ".footer" \
177
+ --no-visual
178
+ ```
179
+
180
+ ## Output
181
+
182
+ Each run creates a timestamped directory inside your output folder:
183
+
184
+ ```
185
+ pixelprobe-output/
186
+ ā”œā”€ā”€ run_2026-03-25_14-30-22_a1b2c3d4/
187
+ │ ā”œā”€ā”€ comparison-a1b2c3d4.json # Full JSON report
188
+ │ ā”œā”€ā”€ report-a1b2c3d4.html # Interactive HTML report
189
+ │ ā”œā”€ā”€ diff-mobile.png # Visual diff per breakpoint
190
+ │ ā”œā”€ā”€ diff-tablet.png
191
+ │ ā”œā”€ā”€ diff-desktop.png
192
+ │ └── diff-desktop-lg.png
193
+ ā”œā”€ā”€ run_2026-03-25_15-10-45_e5f6g7h8/
194
+ │ └── ...
195
+ ```
196
+
197
+ ### Console output
198
+
199
+ Color-coded terminal output with severity indicators (critical, warning, info) for each breakpoint. Shows style property diffs, layout dimension changes, and pixel diff percentages.
200
+
201
+ ### HTML report
202
+
203
+ Self-contained interactive report with breakpoint tabs, visual diff images, collapsible style diff tables, and layout comparison tables. Open directly in a browser.
204
+
205
+ ### JSON report
206
+
207
+ Machine-readable diff data. Includes per-breakpoint style differences, layout deviations, pixel diff metrics, and summary statistics. Useful for CI/CD integration and automated pipelines.
208
+
209
+ ## What it compares
210
+
211
+ ### Style Comparison
212
+
213
+ Extracts `getComputedStyle()` for layout-critical CSS properties organized into categories: layout, flexbox, grid, spacing, sizing, typography, colors, borders, effects, and positioning.
214
+
215
+ Each difference is classified by severity based on visual impact:
216
+
217
+ - **Critical** — display, position, width, height, flex-direction, grid-template, font-size, visibility
218
+ - **Warning** — margin, padding, font-family, font-weight, color, background-color, border, gap, alignment
219
+ - **Info** — everything else (transitions, animations, minor effects)
220
+
221
+ ### Pixel Comparison
222
+
223
+ Uses [pixelmatch](https://github.com/mapbox/pixelmatch) for pixel-level screenshot comparison. Generates diff images highlighting changed pixels in red. Handles size mismatches by padding the smaller image with magenta so dimensional differences are immediately visible.
224
+
225
+ ### Layout Comparison
226
+
227
+ Compares the box model: bounding box dimensions, margin, padding, border widths, and scroll dimensions. Reports both absolute (px) and percentage deviations. Thresholds: 10px+ = critical, 3px+ = warning.
228
+
229
+ ## MCP Server (for AI Agents)
230
+
231
+ `pixelprobe` includes an MCP (Model Context Protocol) server so AI agents can use it as a tool.
232
+
233
+ ### Setup
234
+
235
+ Add to your Claude Desktop or Claude Code MCP config:
236
+
237
+ ```json
238
+ {
239
+ "mcpServers": {
240
+ "pixelprobe": {
241
+ "command": "node",
242
+ "args": ["/path/to/pixelprobe/dist/mcp/server.js"]
243
+ }
244
+ }
245
+ }
246
+ ```
247
+
248
+ ### MCP Tools
249
+
250
+ #### `compare_sections`
251
+
252
+ Full comparison across breakpoints. Accepts `sourceUrl`, `targetUrl`, `selector`, `targetSelector`, `breakpoints`, `pixelThreshold`, and other options. Returns structured JSON diff and saves HTML/JSON reports.
253
+
254
+ #### `capture_section`
255
+
256
+ Inspect and enumerate elements matching a selector on a page. Useful for discovering element structure before running a comparison.
257
+
258
+ #### `list_breakpoints`
259
+
260
+ Returns all available breakpoint presets with their dimensions.
261
+
262
+ ## Programmatic API
263
+
264
+ ```typescript
265
+ import { WebSectionComparator } from 'pixelprobe';
266
+
267
+ const comparator = new WebSectionComparator({ headless: true });
268
+
269
+ const result = await comparator.compare({
270
+ sourceUrl: 'https://example.com',
271
+ targetUrl: 'http://localhost:3000',
272
+ selector: '.hero-section',
273
+ targetSelector: '.hero', // optional, if target uses different markup
274
+ breakpoints: ['mobile', 'tablet', 'desktop'],
275
+ pixelThreshold: 0.1,
276
+ });
277
+
278
+ console.log(result.summary);
279
+ // {
280
+ // overallSeverity: 'warning',
281
+ // totalBreakpoints: 3,
282
+ // breakpointsWithDifferences: 2,
283
+ // totalStyleDifferences: 12,
284
+ // totalLayoutDifferences: 4,
285
+ // averagePixelDiff: 3.5,
286
+ // worstBreakpoint: 'mobile'
287
+ // }
288
+
289
+ // Enumerate elements before comparing
290
+ const elements = await comparator.enumerate('https://example.com', '.card');
291
+ // [{ index: 0, tagName: 'div', textPreview: '...', boundingBox: {...} }, ...]
292
+ ```
293
+
294
+ ## Tips
295
+
296
+ - **Use `--wait`** for sites with animations, lazy-loading, or web fonts. 2000-3000ms is usually enough.
297
+ - **Use `--headed`** to watch the browser and debug selector issues.
298
+ - **Use `--target-selector`** when source and target have different markup (e.g. WordPress vs a custom React build).
299
+ - **Use `enumerate`** first when you're unsure which element your selector is matching.
300
+ - **Use `--no-visual`** to speed things up if you only care about computed style differences.
301
+ - **Custom breakpoints** let you test at the exact widths your design uses: `--breakpoints "375x812,768x1024,1440x900"`.
302
+
303
+ ## Contributing
304
+
305
+ Contributions are welcome! Here's how to get started:
306
+
307
+ ### Setup
308
+
309
+ ```bash
310
+ git clone https://github.com/user/pixelprobe.git
311
+ cd pixelprobe
312
+ npm install
313
+ npx playwright install chromium
314
+ ```
315
+
316
+ ### Development
317
+
318
+ ```bash
319
+ # Type-check
320
+ npx tsc --noEmit
321
+
322
+ # Build
323
+ npx tsc
324
+
325
+ # Run CLI during development
326
+ node dist/bin/pixelprobe.js compare --help
327
+
328
+ # Run tests
329
+ npm test
330
+ ```
331
+
332
+ ### Project Structure
333
+
334
+ ```
335
+ src/
336
+ ā”œā”€ā”€ types/ # TypeScript interfaces (config, comparison results)
337
+ ā”œā”€ā”€ browser/ # Playwright browser lifecycle and viewport management
338
+ ā”œā”€ā”€ extractor/ # Section selection, style extraction, screenshots
339
+ ā”œā”€ā”€ comparator/ # Style diff, pixel diff, layout diff, aggregation
340
+ ā”œā”€ā”€ reporter/ # JSON and HTML report generation
341
+ ā”œā”€ā”€ cli/ # Terminal output formatting
342
+ ā”œā”€ā”€ mcp/ # MCP server (tools and handlers)
343
+ ā”œā”€ā”€ bin/ # CLI entry point
344
+ ā”œā”€ā”€ core.ts # Main orchestrator
345
+ └── index.ts # Public API exports
346
+ ```
347
+
348
+ ### Guidelines
349
+
350
+ - Keep it dependency-light. The core should only need Playwright and pixelmatch.
351
+ - All comparison logic lives in `src/comparator/`. Reporters just format what comparators produce.
352
+ - Add types to `src/types/` before implementing. The types are the contract.
353
+ - Test with real URLs when possible — CSS rendering has endless edge cases.
354
+ - CLI and MCP are thin wrappers over `core.ts`. Don't put business logic in them.
355
+
356
+ ### Reporting Issues
357
+
358
+ When filing a bug, please include:
359
+
360
+ - The full command you ran
361
+ - The error message or unexpected output
362
+ - Your Node.js version (`node --version`)
363
+ - Your OS
364
+
365
+ ## Roadmap
366
+
367
+ - [ ] Pseudo-state comparison (hover, focus, active)
368
+ - [ ] Watch mode for live development
369
+ - [ ] CI/CD integration examples (GitHub Actions, GitLab CI)
370
+ - [ ] Side-by-side source/target screenshots in HTML report
371
+ - [ ] Config file support (`.pixelproberc.json`)
372
+ - [ ] Accessibility attribute comparison
373
+ - [ ] Performance metrics comparison (LCP, CLS)
374
+
375
+ ## License
376
+
377
+ MIT License — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=pixelprobe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pixelprobe.d.ts","sourceRoot":"","sources":["../../src/bin/pixelprobe.ts"],"names":[],"mappings":""}
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ import { join } from "node:path";
3
+ import yargs from "yargs";
4
+ import { hideBin } from "yargs/helpers";
5
+ import { WebSectionComparator } from "../core.js";
6
+ import { JSONReporter } from "../reporter/JSONReporter.js";
7
+ import { HTMLReporter } from "../reporter/HTMLReporter.js";
8
+ import { formatResult } from "../cli/formatter.js";
9
+ import { PRESET_BREAKPOINTS } from "../types/config.js";
10
+ const cli = yargs(hideBin(process.argv))
11
+ .scriptName("pixelprobe")
12
+ .usage("$0 <command> [options]")
13
+ .command("compare", "Compare a section between source and target URLs across breakpoints", (y) => y
14
+ .option("source", {
15
+ alias: "s",
16
+ type: "string",
17
+ describe: "Source URL (the reference page)",
18
+ demandOption: true,
19
+ })
20
+ .option("target", {
21
+ alias: "t",
22
+ type: "string",
23
+ describe: "Target URL (your local build)",
24
+ demandOption: true,
25
+ })
26
+ .option("selector", {
27
+ alias: "S",
28
+ type: "string",
29
+ describe: "CSS selector for the section on the source page",
30
+ demandOption: true,
31
+ })
32
+ .option("target-selector", {
33
+ alias: "T",
34
+ type: "string",
35
+ describe: "CSS selector for the section on the target page (if different from source). Falls back to --selector.",
36
+ })
37
+ .option("index", {
38
+ alias: "i",
39
+ type: "number",
40
+ describe: "Element index if selector matches multiple elements",
41
+ default: 0,
42
+ })
43
+ .option("breakpoints", {
44
+ alias: "b",
45
+ type: "string",
46
+ describe: 'Comma-separated breakpoint names or WIDTHxHEIGHT (e.g. "mobile,tablet,1440x900")',
47
+ default: "mobile,tablet,desktop,desktop-lg",
48
+ })
49
+ .option("output", {
50
+ alias: "o",
51
+ type: "string",
52
+ describe: "Output directory for reports and artifacts",
53
+ default: "./pixelprobe-output",
54
+ })
55
+ .option("format", {
56
+ alias: "f",
57
+ type: "string",
58
+ describe: 'Output formats: "json", "html", "both"',
59
+ default: "both",
60
+ })
61
+ .option("threshold", {
62
+ type: "number",
63
+ describe: "Pixel comparison threshold (0-1, lower = stricter)",
64
+ default: 0.1,
65
+ })
66
+ .option("no-visual", {
67
+ type: "boolean",
68
+ describe: "Skip visual (pixel) comparison",
69
+ default: false,
70
+ })
71
+ .option("no-styles", {
72
+ type: "boolean",
73
+ describe: "Skip computed style comparison",
74
+ default: false,
75
+ })
76
+ .option("no-layout", {
77
+ type: "boolean",
78
+ describe: "Skip layout metrics comparison",
79
+ default: false,
80
+ })
81
+ .option("wait", {
82
+ alias: "w",
83
+ type: "number",
84
+ describe: "Wait N ms after page load before capturing",
85
+ default: 0,
86
+ })
87
+ .option("headed", {
88
+ type: "boolean",
89
+ describe: "Run browser in headed mode (visible)",
90
+ default: false,
91
+ })
92
+ .option("json", {
93
+ type: "boolean",
94
+ describe: "Output compact JSON to stdout (for piping)",
95
+ default: false,
96
+ }), async (argv) => {
97
+ const breakpoints = argv.breakpoints.split(",").map((s) => s.trim());
98
+ const comparator = new WebSectionComparator({
99
+ headless: !argv.headed,
100
+ timeout: 30000,
101
+ });
102
+ // Progress display
103
+ const spinnerFrames = ["ā ‹", "ā ™", "ā ¹", "ā ø", "ā ¼", "ā “", "ā ¦", "ā §", "ā ‡", "ā "];
104
+ let spinnerIdx = 0;
105
+ let spinnerInterval;
106
+ if (!argv.json) {
107
+ spinnerInterval = setInterval(() => {
108
+ spinnerIdx = (spinnerIdx + 1) % spinnerFrames.length;
109
+ }, 80);
110
+ }
111
+ try {
112
+ const result = await comparator.compare({
113
+ sourceUrl: argv.source,
114
+ targetUrl: argv.target,
115
+ selector: argv.selector,
116
+ targetSelector: argv["target-selector"],
117
+ elementIndex: argv.index,
118
+ breakpoints,
119
+ pixelThreshold: argv.threshold,
120
+ includeVisual: !argv["no-visual"],
121
+ includeStyles: !argv["no-styles"],
122
+ includeLayout: !argv["no-layout"],
123
+ waitForTimeout: argv.wait,
124
+ }, !argv.json
125
+ ? (event) => {
126
+ process.stderr.write(`\r${spinnerFrames[spinnerIdx]} ${event.message} (${Math.round(event.progress)}%) `);
127
+ }
128
+ : undefined);
129
+ if (spinnerInterval)
130
+ clearInterval(spinnerInterval);
131
+ // JSON-only mode (for piping)
132
+ if (argv.json) {
133
+ console.log(JSON.stringify(JSONReporter.toCompactSummary(result), null, 2));
134
+ process.exit(result.summary.overallSeverity === "pass" ? 0 : 1);
135
+ return;
136
+ }
137
+ // Clear spinner line
138
+ process.stderr.write("\r" + " ".repeat(80) + "\r");
139
+ // Console output
140
+ console.log(formatResult(result));
141
+ // Create a timestamped run directory inside the output dir
142
+ const timestamp = new Date()
143
+ .toISOString()
144
+ .replace(/[:.]/g, "-")
145
+ .replace("T", "_")
146
+ .slice(0, 19);
147
+ const runDir = join(argv.output, `run_${timestamp}_${result.id}`);
148
+ // Generate reports into the run directory
149
+ const format = argv.format;
150
+ if (format === "json" || format === "both") {
151
+ const jsonPath = await JSONReporter.generate(result, runDir);
152
+ console.log(` šŸ“„ JSON report: ${jsonPath}`);
153
+ }
154
+ if (format === "html" || format === "both") {
155
+ const htmlPath = await HTMLReporter.generate(result, runDir);
156
+ console.log(` 🌐 HTML report: ${htmlPath}`);
157
+ }
158
+ console.log("");
159
+ // Exit with non-zero if differences found
160
+ process.exit(result.summary.overallSeverity === "pass" ? 0 : 1);
161
+ }
162
+ catch (err) {
163
+ if (spinnerInterval)
164
+ clearInterval(spinnerInterval);
165
+ process.stderr.write("\r" + " ".repeat(80) + "\r");
166
+ console.error(`\nāŒ Error: ${err.message}`);
167
+ process.exit(2);
168
+ }
169
+ })
170
+ .command("enumerate", "List all elements matching a selector on a page", (y) => y
171
+ .option("url", {
172
+ alias: "u",
173
+ type: "string",
174
+ describe: "URL to inspect",
175
+ demandOption: true,
176
+ })
177
+ .option("selector", {
178
+ alias: "S",
179
+ type: "string",
180
+ describe: "CSS selector",
181
+ demandOption: true,
182
+ }), async (argv) => {
183
+ const comparator = new WebSectionComparator({ headless: true });
184
+ try {
185
+ const elements = await comparator.enumerate(argv.url, argv.selector);
186
+ if (elements.length === 0) {
187
+ console.log(`No elements found matching "${argv.selector}"`);
188
+ process.exit(1);
189
+ return;
190
+ }
191
+ console.log(`\nFound ${elements.length} element(s) matching "${argv.selector}":\n`);
192
+ for (const el of elements) {
193
+ const box = el.boundingBox;
194
+ console.log(` [${el.index}] <${el.tagName}> (${box.width}Ɨ${box.height} at ${box.x},${box.y})`);
195
+ if (el.textPreview) {
196
+ console.log(` "${el.textPreview}"`);
197
+ }
198
+ }
199
+ console.log(`\nUse --index N with the compare command to select a specific element.`);
200
+ }
201
+ catch (err) {
202
+ console.error(`āŒ Error: ${err.message}`);
203
+ process.exit(2);
204
+ }
205
+ })
206
+ .command("breakpoints", "List available breakpoint presets", () => { }, () => {
207
+ console.log("\nAvailable breakpoint presets:\n");
208
+ for (const [name, bp] of Object.entries(PRESET_BREAKPOINTS)) {
209
+ console.log(` ${name.padEnd(14)} ${bp.width}Ɨ${bp.height} ${bp.label}`);
210
+ }
211
+ console.log(`\nYou can also use custom breakpoints in WIDTHxHEIGHT format (e.g. 1440x900).`);
212
+ })
213
+ .demandCommand(1, "Please specify a command")
214
+ .help()
215
+ .version("0.1.0")
216
+ .strict();
217
+ cli.parse();
218
+ //# sourceMappingURL=pixelprobe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pixelprobe.js","sourceRoot":"","sources":["../../src/bin/pixelprobe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { type Page } from "playwright";
2
+ import type { Breakpoint } from "../types/config.js";
3
+ export interface BrowserManagerOptions {
4
+ headless?: boolean;
5
+ timeout?: number;
6
+ }
7
+ export declare class BrowserManager {
8
+ private browser;
9
+ private options;
10
+ constructor(options?: BrowserManagerOptions);
11
+ launch(): Promise<void>;
12
+ createPage(breakpoint: Breakpoint): Promise<Page>;
13
+ navigateAndWait(page: Page, url: string, options?: {
14
+ waitForSelector?: string;
15
+ waitForTimeout?: number;
16
+ }): Promise<void>;
17
+ close(): Promise<void>;
18
+ get isLaunched(): boolean;
19
+ }
20
+ //# sourceMappingURL=BrowserManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BrowserManager.d.ts","sourceRoot":"","sources":["../../src/browser/BrowserManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+C,KAAK,IAAI,EAAE,MAAM,YAAY,CAAC;AACpF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,OAAO,CAAkC;gBAErC,OAAO,GAAE,qBAA0B;IAOzC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAOvB,UAAU,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBjD,eAAe,CACnB,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;QACP,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,cAAc,CAAC,EAAE,MAAM,CAAC;KACpB,GACL,OAAO,CAAC,IAAI,CAAC;IAwBV,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B,IAAI,UAAU,IAAI,OAAO,CAExB;CACF"}