opensafari-mcp 0.1.1
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 +341 -0
- package/dist/115.index.js +2 -0
- package/dist/115.index.js.map +1 -0
- package/dist/67.index.js +2 -0
- package/dist/67.index.js.map +1 -0
- package/dist/679.index.js +2 -0
- package/dist/679.index.js.map +1 -0
- package/dist/681.index.js +2 -0
- package/dist/681.index.js.map +1 -0
- package/dist/838.index.js +2 -0
- package/dist/838.index.js.map +1 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/manager.d.ts +33 -0
- package/dist/auth/manager.d.ts.map +1 -0
- package/dist/cli/148.index.js +3 -0
- package/dist/cli/148.index.js.map +1 -0
- package/dist/cli/473.index.js +3 -0
- package/dist/cli/473.index.js.map +1 -0
- package/dist/cli/622.index.js +3 -0
- package/dist/cli/622.index.js.map +1 -0
- package/dist/cli/712.index.js +3 -0
- package/dist/cli/712.index.js.map +1 -0
- package/dist/cli/844.index.js +3 -0
- package/dist/cli/844.index.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/comparison/cross-viewport.d.ts +36 -0
- package/dist/comparison/cross-viewport.d.ts.map +1 -0
- package/dist/comparison/index.d.ts +4 -0
- package/dist/comparison/index.d.ts.map +1 -0
- package/dist/comparison/report.d.ts +10 -0
- package/dist/comparison/report.d.ts.map +1 -0
- package/dist/config/defaults.d.ts +33 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/global.d.ts +25 -0
- package/dist/config/global.d.ts.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/tool-tiers.d.ts +7 -0
- package/dist/config/tool-tiers.d.ts.map +1 -0
- package/dist/errors/codes.d.ts +20 -0
- package/dist/errors/codes.d.ts.map +1 -0
- package/dist/errors/index.d.ts +4 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/timeout.d.ts +20 -0
- package/dist/errors/timeout.d.ts.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +41 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/metrics/collector.d.ts +43 -0
- package/dist/metrics/collector.d.ts.map +1 -0
- package/dist/orchestration/index.d.ts +3 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/workflow-engine.d.ts +90 -0
- package/dist/orchestration/workflow-engine.d.ts.map +1 -0
- package/dist/qa/audit.d.ts +42 -0
- package/dist/qa/audit.d.ts.map +1 -0
- package/dist/qa/detectors/auto-zoom.d.ts +4 -0
- package/dist/qa/detectors/auto-zoom.d.ts.map +1 -0
- package/dist/qa/detectors/dark-mode.d.ts +5 -0
- package/dist/qa/detectors/dark-mode.d.ts.map +1 -0
- package/dist/qa/detectors/fixed-stacking.d.ts +4 -0
- package/dist/qa/detectors/fixed-stacking.d.ts.map +1 -0
- package/dist/qa/detectors/horizontal-overflow.d.ts +4 -0
- package/dist/qa/detectors/horizontal-overflow.d.ts.map +1 -0
- package/dist/qa/detectors/hover-only.d.ts +4 -0
- package/dist/qa/detectors/hover-only.d.ts.map +1 -0
- package/dist/qa/detectors/index.d.ts +14 -0
- package/dist/qa/detectors/index.d.ts.map +1 -0
- package/dist/qa/detectors/input-type.d.ts +4 -0
- package/dist/qa/detectors/input-type.d.ts.map +1 -0
- package/dist/qa/detectors/keyboard-overlap.d.ts +4 -0
- package/dist/qa/detectors/keyboard-overlap.d.ts.map +1 -0
- package/dist/qa/detectors/orientation.d.ts +5 -0
- package/dist/qa/detectors/orientation.d.ts.map +1 -0
- package/dist/qa/detectors/pwa-meta.d.ts +4 -0
- package/dist/qa/detectors/pwa-meta.d.ts.map +1 -0
- package/dist/qa/detectors/safe-area.d.ts +4 -0
- package/dist/qa/detectors/safe-area.d.ts.map +1 -0
- package/dist/qa/detectors/scroll-lock.d.ts +4 -0
- package/dist/qa/detectors/scroll-lock.d.ts.map +1 -0
- package/dist/qa/detectors/touch-targets.d.ts +4 -0
- package/dist/qa/detectors/touch-targets.d.ts.map +1 -0
- package/dist/qa/detectors/vh100.d.ts +4 -0
- package/dist/qa/detectors/vh100.d.ts.map +1 -0
- package/dist/qa/history.d.ts +44 -0
- package/dist/qa/history.d.ts.map +1 -0
- package/dist/qa/index.d.ts +8 -0
- package/dist/qa/index.d.ts.map +1 -0
- package/dist/qa/report-markdown.d.ts +3 -0
- package/dist/qa/report-markdown.d.ts.map +1 -0
- package/dist/qa/types.d.ts +29 -0
- package/dist/qa/types.d.ts.map +1 -0
- package/dist/reliability/crash-watcher.d.ts +16 -0
- package/dist/reliability/crash-watcher.d.ts.map +1 -0
- package/dist/reliability/graceful-shutdown.d.ts +3 -0
- package/dist/reliability/graceful-shutdown.d.ts.map +1 -0
- package/dist/reliability/index.d.ts +4 -0
- package/dist/reliability/index.d.ts.map +1 -0
- package/dist/reliability/zombie-cleanup.d.ts +53 -0
- package/dist/reliability/zombie-cleanup.d.ts.map +1 -0
- package/dist/security/audit-logger.d.ts +2 -0
- package/dist/security/audit-logger.d.ts.map +1 -0
- package/dist/security/content-sanitizer.d.ts +32 -0
- package/dist/security/content-sanitizer.d.ts.map +1 -0
- package/dist/security/domain-guard.d.ts +16 -0
- package/dist/security/domain-guard.d.ts.map +1 -0
- package/dist/session-manager.d.ts +54 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/simulator/batch.d.ts +22 -0
- package/dist/simulator/batch.d.ts.map +1 -0
- package/dist/simulator/index.d.ts +15 -0
- package/dist/simulator/index.d.ts.map +1 -0
- package/dist/simulator/manager.d.ts +66 -0
- package/dist/simulator/manager.d.ts.map +1 -0
- package/dist/simulator/pool.d.ts +52 -0
- package/dist/simulator/pool.d.ts.map +1 -0
- package/dist/simulator/presets.d.ts +4 -0
- package/dist/simulator/presets.d.ts.map +1 -0
- package/dist/simulator/proxy.d.ts +78 -0
- package/dist/simulator/proxy.d.ts.map +1 -0
- package/dist/simulator/simctl.d.ts +12 -0
- package/dist/simulator/simctl.d.ts.map +1 -0
- package/dist/simulator/socket-finder.d.ts +22 -0
- package/dist/simulator/socket-finder.d.ts.map +1 -0
- package/dist/simulator/types.d.ts +21 -0
- package/dist/simulator/types.d.ts.map +1 -0
- package/dist/simulator/xcode-check.d.ts +15 -0
- package/dist/simulator/xcode-check.d.ts.map +1 -0
- package/dist/tools/appearance-toggle.d.ts +3 -0
- package/dist/tools/appearance-toggle.d.ts.map +1 -0
- package/dist/tools/auth.d.ts +3 -0
- package/dist/tools/auth.d.ts.map +1 -0
- package/dist/tools/batch-execute.d.ts +5 -0
- package/dist/tools/batch-execute.d.ts.map +1 -0
- package/dist/tools/batch-navigate.d.ts +5 -0
- package/dist/tools/batch-navigate.d.ts.map +1 -0
- package/dist/tools/batch-screenshot.d.ts +5 -0
- package/dist/tools/batch-screenshot.d.ts.map +1 -0
- package/dist/tools/click.d.ts +3 -0
- package/dist/tools/click.d.ts.map +1 -0
- package/dist/tools/cookies.d.ts +3 -0
- package/dist/tools/cookies.d.ts.map +1 -0
- package/dist/tools/cross-viewport-compare.d.ts +4 -0
- package/dist/tools/cross-viewport-compare.d.ts.map +1 -0
- package/dist/tools/device-boot.d.ts +3 -0
- package/dist/tools/device-boot.d.ts.map +1 -0
- package/dist/tools/device-list.d.ts +3 -0
- package/dist/tools/device-list.d.ts.map +1 -0
- package/dist/tools/device-rotate.d.ts +3 -0
- package/dist/tools/device-rotate.d.ts.map +1 -0
- package/dist/tools/device-shutdown.d.ts +3 -0
- package/dist/tools/device-shutdown.d.ts.map +1 -0
- package/dist/tools/dismiss-keyboard.d.ts +3 -0
- package/dist/tools/dismiss-keyboard.d.ts.map +1 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/inspect.d.ts +3 -0
- package/dist/tools/inspect.d.ts.map +1 -0
- package/dist/tools/javascript.d.ts +3 -0
- package/dist/tools/javascript.d.ts.map +1 -0
- package/dist/tools/long-press.d.ts +3 -0
- package/dist/tools/long-press.d.ts.map +1 -0
- package/dist/tools/navigate.d.ts +3 -0
- package/dist/tools/navigate.d.ts.map +1 -0
- package/dist/tools/orchestration-tools.d.ts +5 -0
- package/dist/tools/orchestration-tools.d.ts.map +1 -0
- package/dist/tools/press.d.ts +3 -0
- package/dist/tools/press.d.ts.map +1 -0
- package/dist/tools/qa-audit.d.ts +3 -0
- package/dist/tools/qa-audit.d.ts.map +1 -0
- package/dist/tools/qa-detectors.d.ts +3 -0
- package/dist/tools/qa-detectors.d.ts.map +1 -0
- package/dist/tools/query-dom.d.ts +3 -0
- package/dist/tools/query-dom.d.ts.map +1 -0
- package/dist/tools/read-page.d.ts +3 -0
- package/dist/tools/read-page.d.ts.map +1 -0
- package/dist/tools/screenshot.d.ts +3 -0
- package/dist/tools/screenshot.d.ts.map +1 -0
- package/dist/tools/scroll.d.ts +3 -0
- package/dist/tools/scroll.d.ts.map +1 -0
- package/dist/tools/select-option.d.ts +3 -0
- package/dist/tools/select-option.d.ts.map +1 -0
- package/dist/tools/swipe.d.ts +3 -0
- package/dist/tools/swipe.d.ts.map +1 -0
- package/dist/tools/type.d.ts +3 -0
- package/dist/tools/type.d.ts.map +1 -0
- package/dist/tools/wait-for.d.ts +3 -0
- package/dist/tools/wait-for.d.ts.map +1 -0
- package/dist/transports/http.d.ts +57 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/index.d.ts +38 -0
- package/dist/transports/index.d.ts.map +1 -0
- package/dist/transports/stdio.d.ts +16 -0
- package/dist/transports/stdio.d.ts.map +1 -0
- package/dist/types/browser-backend.d.ts +84 -0
- package/dist/types/browser-backend.d.ts.map +1 -0
- package/dist/types/mcp.d.ts +63 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/tool-manifest.d.ts +52 -0
- package/dist/types/tool-manifest.d.ts.map +1 -0
- package/dist/utils/format-age.d.ts +5 -0
- package/dist/utils/format-age.d.ts.map +1 -0
- package/dist/utils/format-error.d.ts +5 -0
- package/dist/utils/format-error.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/rate-limiter.d.ts +72 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/request-queue.d.ts +37 -0
- package/dist/utils/request-queue.d.ts.map +1 -0
- package/dist/utils/schema-validator.d.ts +12 -0
- package/dist/utils/schema-validator.d.ts.map +1 -0
- package/dist/utils/url-utils.d.ts +5 -0
- package/dist/utils/url-utils.d.ts.map +1 -0
- package/dist/utils/with-timeout.d.ts +5 -0
- package/dist/utils/with-timeout.d.ts.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/watchdog/event-loop-monitor.d.ts +86 -0
- package/dist/watchdog/event-loop-monitor.d.ts.map +1 -0
- package/dist/watchdog/simulator-monitor.d.ts +16 -0
- package/dist/watchdog/simulator-monitor.d.ts.map +1 -0
- package/dist/webkit/client.d.ts +106 -0
- package/dist/webkit/client.d.ts.map +1 -0
- package/dist/webkit/index.d.ts +3 -0
- package/dist/webkit/index.d.ts.map +1 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenSafari Contributors
|
|
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,341 @@
|
|
|
1
|
+
<h1 align="center">OpenSafari</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<b>Smart. Fast. Parallel.</b><br>
|
|
5
|
+
iOS Safari automation MCP server via Xcode Simulator.
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="MIT"></a>
|
|
10
|
+
<a href="https://github.com/shaun0927/opensafari"><img src="https://img.shields.io/badge/status-in--development-orange" alt="Status"></a>
|
|
11
|
+
<a href="https://github.com/shaun0927/openchrome"><img src="https://img.shields.io/badge/sibling-OpenChrome-blue" alt="OpenChrome"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
### How OpenSafari compares
|
|
17
|
+
|
|
18
|
+
| | OpenSafari | Playwright WebKit | BrowserStack | Manual Testing |
|
|
19
|
+
|---|:---:|:---:|:---:|:---:|
|
|
20
|
+
| **Engine** | **Real Safari** (Xcode Sim) | Bundled WebKit (approximation) | Real devices (cloud) | Real devices |
|
|
21
|
+
| **Protocol** | **WebKit Remote Debugging** (direct) | Playwright API (wrapper) | Proprietary | N/A |
|
|
22
|
+
| **iOS Fidelity** | **exact** | close but diverges | exact | exact |
|
|
23
|
+
| **Parallel sessions** | **N simulators** | N browsers | limited by plan | 1 device |
|
|
24
|
+
| **Login persistence** | **built-in** (real Safari cookies) | manual | manual | manual |
|
|
25
|
+
| **LLM integration** | **MCP native** | none | none | none |
|
|
26
|
+
| **Cost** | **free** (Xcode) | free | $29+/mo | device cost |
|
|
27
|
+
| **iOS-specific QA** | **auto-detect** (zoom, safe area, keyboard) | none | manual | manual |
|
|
28
|
+
|
|
29
|
+
> **tl;dr** — OpenSafari controls the **real Safari** inside Xcode Simulator via WebKit Remote Debugging Protocol — the same way [OpenChrome](https://github.com/shaun0927/openchrome) controls real Chrome via CDP. No middleware, no bundled browsers. Just direct protocol access to the actual Safari.app.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## What is OpenSafari?
|
|
34
|
+
|
|
35
|
+
Imagine testing your e-commerce site on **iPhone 17e, iPhone 17, iPhone 17 Pro Max, and iPad** — all at the same time, already logged in, with an AI agent that automatically finds iOS-specific bugs. That's OpenSafari.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
You: Check our checkout flow for mobile issues across all iPhone sizes
|
|
39
|
+
|
|
40
|
+
AI: [4 parallel simulators, all devices simultaneously]
|
|
41
|
+
iPhone 17e: ⚠ Credit card input triggers iOS auto-zoom (font-size: 14px)
|
|
42
|
+
iPhone 17: ✓ Layout OK
|
|
43
|
+
iPhone 17 PM: ⚠ "Place Order" button only 38×32px (below 44px touch target)
|
|
44
|
+
iPad: ⚠ Shipping form hidden behind keyboard when focused
|
|
45
|
+
|
|
46
|
+
Time: 8s | All screenshots captured and analyzed.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| | Manual QA | OpenSafari |
|
|
50
|
+
|---|:---:|:---:|
|
|
51
|
+
| **4-device test** | ~30 min | **~10s** (parallel) |
|
|
52
|
+
| **Login** | Each device, each time | **Never** (persisted) |
|
|
53
|
+
| **iOS bug detection** | Human eye | **Automatic** (LLM vision) |
|
|
54
|
+
| **Consistency** | Varies by tester | **Deterministic** |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Core Architecture
|
|
59
|
+
|
|
60
|
+
OpenSafari follows the same direct-protocol philosophy as OpenChrome:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
OpenChrome: CDPClient → Chrome DevTools Protocol → Real Chrome
|
|
64
|
+
OpenSafari: SafariClient → WebKit Remote Debugging Protocol → Real Safari in Simulator
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
No middleware. No bundled browsers. Direct connection.
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Claude Code / AI Agent (MCP Client)
|
|
71
|
+
│
|
|
72
|
+
│ JSON-RPC (stdio / HTTP)
|
|
73
|
+
▼
|
|
74
|
+
┌─────────────────────────────────────┐
|
|
75
|
+
│ OpenSafari MCP Server │
|
|
76
|
+
│ │
|
|
77
|
+
│ ┌─────────────┐ ┌──────────────┐ │
|
|
78
|
+
│ │ Simulator │ │ Safari │ │
|
|
79
|
+
│ │ Manager │ │ Client │ │
|
|
80
|
+
│ │ (simctl) │ │ (WebKit │ │
|
|
81
|
+
│ │ │ │ Protocol) │ │
|
|
82
|
+
│ └──────┬──────┘ └──────┬───────┘ │
|
|
83
|
+
│ │ │ │
|
|
84
|
+
│ boot/shutdown navigate/click │
|
|
85
|
+
│ rotate/appear screenshot │
|
|
86
|
+
│ multi-device DOM/JS/cookies │
|
|
87
|
+
│ │ │ │
|
|
88
|
+
│ ┌──────▼────────────────▼───────┐ │
|
|
89
|
+
│ │ Xcode Simulator(s) │ │
|
|
90
|
+
│ │ ┌────────┐ ┌────────┐ │ │
|
|
91
|
+
│ │ │ iPhone │ │ iPhone │ ... │ │
|
|
92
|
+
│ │ │ SE │ │ 16 PM │ │ │
|
|
93
|
+
│ │ │ Safari │ │ Safari │ │ │
|
|
94
|
+
│ │ └────────┘ └────────┘ │ │
|
|
95
|
+
│ └───────────────────────────────┘ │
|
|
96
|
+
│ │
|
|
97
|
+
│ ┌─────────────────────────────┐ │
|
|
98
|
+
│ │ Shared Infrastructure │ │
|
|
99
|
+
│ │ (from OpenChrome) │ │
|
|
100
|
+
│ │ • Security (sanitizer, │ │
|
|
101
|
+
│ │ domain guard, audit) │ │
|
|
102
|
+
│ │ • Watchdog (event loop, │ │
|
|
103
|
+
│ │ disk, health endpoint) │ │
|
|
104
|
+
│ │ • Orchestration (workflow │ │
|
|
105
|
+
│ │ engine, parallel workers)│ │
|
|
106
|
+
│ │ • Session persistence │ │
|
|
107
|
+
│ └─────────────────────────────┘ │
|
|
108
|
+
└─────────────────────────────────────┘
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Key Features
|
|
114
|
+
|
|
115
|
+
### 1. Real Safari, Real Bugs
|
|
116
|
+
|
|
117
|
+
OpenSafari controls the **actual Safari.app** inside Xcode Simulator via WebKit Remote Debugging Protocol — not a bundled approximation. Every iOS-specific quirk is faithfully reproduced:
|
|
118
|
+
|
|
119
|
+
- **iOS auto-zoom** on inputs with `font-size < 16px`
|
|
120
|
+
- **`position: fixed`** behavior with real software keyboard
|
|
121
|
+
- **`100vh`** viewport height inconsistencies with address bar
|
|
122
|
+
- **Safe area insets** (notch, home indicator) on real device frames
|
|
123
|
+
- **`color-scheme`** dark mode forced rendering
|
|
124
|
+
- **Touch target** minimum size requirements (44×44px)
|
|
125
|
+
|
|
126
|
+
### 2. Parallel Multi-Device Testing
|
|
127
|
+
|
|
128
|
+
Test across multiple devices simultaneously with a single command:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
opensafari serve --devices "iphone-17e,iphone-17,iphone-17-pro-max,ipad-pro"
|
|
132
|
+
|
|
133
|
+
# 4 simulators boot in parallel
|
|
134
|
+
# Each gets its own Safari instance + WebKit Protocol connection
|
|
135
|
+
# All share login state via cookie injection
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 3. Persistent Login Sessions
|
|
139
|
+
|
|
140
|
+
Log in once, test forever. Cookies and localStorage are extracted directly from the real Safari session:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
# First time: login captured from real Safari
|
|
144
|
+
opensafari auth save --site myapp.com
|
|
145
|
+
→ Exports cookies via WebKit Protocol Network.getAllCookies
|
|
146
|
+
→ Saves to ~/.opensafari/auth/myapp.json
|
|
147
|
+
|
|
148
|
+
# Every subsequent run: auto-restored
|
|
149
|
+
opensafari serve
|
|
150
|
+
→ Injects cookies via Network.setCookie into each simulator's Safari
|
|
151
|
+
→ All simulators start already logged in
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 4. iOS-Specific Auto-Detection
|
|
155
|
+
|
|
156
|
+
Built-in QA checks that run on real Safari — no approximation:
|
|
157
|
+
|
|
158
|
+
| Check | What It Detects |
|
|
159
|
+
|-------|----------------|
|
|
160
|
+
| **Auto-Zoom Guard** | `<input>` elements with font-size < 16px |
|
|
161
|
+
| **Safe Area Validator** | Content hidden behind notch or home indicator |
|
|
162
|
+
| **Keyboard Overlap** | Fixed elements covered by real software keyboard |
|
|
163
|
+
| **Touch Target Audit** | Clickable elements smaller than 44×44px |
|
|
164
|
+
| **Dark Mode Diff** | Visual differences via `simctl ui appearance` toggle |
|
|
165
|
+
| **Viewport Compare** | Layout breaks across different screen sizes |
|
|
166
|
+
| **Scroll Lock Check** | Body scroll not restored after modal close |
|
|
167
|
+
|
|
168
|
+
### 5. MCP Native
|
|
169
|
+
|
|
170
|
+
Works with any MCP client — Claude Code, Cursor, VS Code, or custom agents:
|
|
171
|
+
|
|
172
|
+
```jsonc
|
|
173
|
+
// ~/.claude.json
|
|
174
|
+
{
|
|
175
|
+
"mcpServers": {
|
|
176
|
+
"opensafari": {
|
|
177
|
+
"command": "opensafari",
|
|
178
|
+
"args": ["serve"]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 6. Shared DNA with OpenChrome
|
|
185
|
+
|
|
186
|
+
OpenSafari shares battle-tested infrastructure with [OpenChrome](https://github.com/shaun0927/openchrome):
|
|
187
|
+
|
|
188
|
+
| Module | Source | Status |
|
|
189
|
+
|--------|--------|--------|
|
|
190
|
+
| MCP Server core | OpenChrome | Shared |
|
|
191
|
+
| Transport (stdio/HTTP) | OpenChrome | Shared |
|
|
192
|
+
| Security (sanitizer, guard, audit) | OpenChrome | Shared |
|
|
193
|
+
| Watchdog (event loop, disk, health) | OpenChrome | Shared |
|
|
194
|
+
| Orchestration (workflow engine) | OpenChrome | Adapted |
|
|
195
|
+
| Simulator Manager | **NEW** | OpenSafari |
|
|
196
|
+
| Safari Client (WebKit Protocol) | **NEW** | OpenSafari |
|
|
197
|
+
| iOS QA Engine | **NEW** | OpenSafari |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Tools (Planned)
|
|
202
|
+
|
|
203
|
+
### Core Tools (Phase 1)
|
|
204
|
+
|
|
205
|
+
| Tool | Description |
|
|
206
|
+
|------|-------------|
|
|
207
|
+
| `navigate` | Open URL in real Safari |
|
|
208
|
+
| `computer` | Touch, scroll, type — coordinate-based interaction |
|
|
209
|
+
| `screenshot` | Capture real Safari screen via WebKit Protocol or simctl |
|
|
210
|
+
| `read_page` | Extract visible text content |
|
|
211
|
+
| `query_dom` | CSS selector queries with element details |
|
|
212
|
+
| `javascript` | Execute JavaScript in page context via `Runtime.evaluate` |
|
|
213
|
+
| `inspect` | Element CSS, accessibility, and layout inspection |
|
|
214
|
+
| `cookies` | Get/set/clear real Safari cookies via `Network` domain |
|
|
215
|
+
|
|
216
|
+
### Device Management (Phase 1)
|
|
217
|
+
|
|
218
|
+
| Tool | Description |
|
|
219
|
+
|------|-------------|
|
|
220
|
+
| `device_list` | List available simulator device types |
|
|
221
|
+
| `device_boot` | Boot a specific device (iPhone SE, 16, iPad, etc.) |
|
|
222
|
+
| `device_shutdown` | Shutdown simulator |
|
|
223
|
+
| `device_snapshot` | Save/restore simulator state (including login) |
|
|
224
|
+
| `device_rotate` | Toggle portrait/landscape |
|
|
225
|
+
| `appearance_toggle` | Switch light/dark mode via `simctl ui` |
|
|
226
|
+
|
|
227
|
+
### Parallel & Orchestration (Phase 2)
|
|
228
|
+
|
|
229
|
+
| Tool | Description |
|
|
230
|
+
|------|-------------|
|
|
231
|
+
| `batch_screenshot` | Capture same URL across all active devices |
|
|
232
|
+
| `batch_execute` | Run JS across all simulators in parallel |
|
|
233
|
+
| `batch_navigate` | Open same URL on all devices simultaneously |
|
|
234
|
+
| `cross_viewport_compare` | Side-by-side visual comparison across devices |
|
|
235
|
+
|
|
236
|
+
### iOS QA Engine (Phase 3)
|
|
237
|
+
|
|
238
|
+
| Tool | Description |
|
|
239
|
+
|------|-------------|
|
|
240
|
+
| `qa_auto_zoom` | Detect inputs triggering iOS auto-zoom |
|
|
241
|
+
| `qa_touch_targets` | Find elements below 44×44px minimum |
|
|
242
|
+
| `qa_safe_area` | Check content behind notch/home indicator |
|
|
243
|
+
| `qa_keyboard_overlap` | Detect fixed elements hidden by real keyboard |
|
|
244
|
+
| `qa_dark_mode` | Compare light vs dark mode rendering |
|
|
245
|
+
| `qa_full_audit` | Run all QA checks and generate report |
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Quick Start (Coming Soon)
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# Prerequisites: macOS + Xcode (with iOS Simulator)
|
|
253
|
+
|
|
254
|
+
# Install
|
|
255
|
+
npm install -g opensafari-mcp
|
|
256
|
+
|
|
257
|
+
# Run
|
|
258
|
+
opensafari serve
|
|
259
|
+
|
|
260
|
+
# With specific devices
|
|
261
|
+
opensafari serve --devices "iphone-17e,iphone-17-pro-max"
|
|
262
|
+
|
|
263
|
+
# With auth state
|
|
264
|
+
opensafari serve --auth ~/.opensafari/auth/mysite.json
|
|
265
|
+
|
|
266
|
+
# Health check
|
|
267
|
+
opensafari doctor
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Requirements
|
|
273
|
+
|
|
274
|
+
- **macOS** (Xcode Simulator is macOS only)
|
|
275
|
+
- **Xcode** with iOS Simulator runtime installed
|
|
276
|
+
- **Node.js** >= 18
|
|
277
|
+
- **ios-webkit-debug-proxy** — `brew install ios-webkit-debug-proxy`
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## WebInspector Proxy Configuration
|
|
282
|
+
|
|
283
|
+
OpenSafari uses `ios_webkit_debug_proxy` to bridge WebKit Remote Debugging from Xcode Simulator. The proxy is **auto-started** by the `device_boot` tool — no manual setup is needed in most cases.
|
|
284
|
+
|
|
285
|
+
### Default Ports
|
|
286
|
+
|
|
287
|
+
| Port | Purpose |
|
|
288
|
+
|------|---------|
|
|
289
|
+
| **9321** | Device list (HTML) — serves the proxy's device listing page |
|
|
290
|
+
| **9322** | Device connection (JSON) — WebKit debugging targets for connected simulators |
|
|
291
|
+
|
|
292
|
+
Port 9322 is deliberately offset from Chrome DevTools (9222) so OpenSafari and [OpenChrome](https://github.com/shaun0927/openchrome) can run simultaneously.
|
|
293
|
+
|
|
294
|
+
### Custom Port
|
|
295
|
+
|
|
296
|
+
Set the `OPENSAFARI_PROXY_PORT` environment variable to use a different device port:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
# Use port 9500 instead of the default 9322
|
|
300
|
+
OPENSAFARI_PROXY_PORT=9500 opensafari serve
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Port resolution order:
|
|
304
|
+
1. Explicit `port` option (programmatic use)
|
|
305
|
+
2. `OPENSAFARI_PROXY_PORT` environment variable
|
|
306
|
+
3. Default: **9322**
|
|
307
|
+
|
|
308
|
+
### Multi-Session Usage
|
|
309
|
+
|
|
310
|
+
Multiple Claude Code sessions can share the same proxy. When a session detects a healthy proxy already running on its target port, it reuses it instead of starting a new one. When the owning session exits, only its own proxy is terminated — other sessions' proxies remain unaffected.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Relationship to OpenChrome
|
|
315
|
+
|
|
316
|
+
OpenSafari is the **Safari/iOS counterpart** to [OpenChrome](https://github.com/shaun0927/openchrome). Same philosophy, same architecture — different browser.
|
|
317
|
+
|
|
318
|
+
| | OpenChrome | OpenSafari |
|
|
319
|
+
|---|---|---|
|
|
320
|
+
| **Browser** | Real Chrome | Real Safari (in Simulator) |
|
|
321
|
+
| **Protocol** | Chrome DevTools Protocol (CDP) | WebKit Remote Debugging Protocol |
|
|
322
|
+
| **Client** | CDPClient (puppeteer-core) | SafariClient (WebKit Protocol) |
|
|
323
|
+
| **Execution** | `chrome --remote-debugging-port` | `xcrun simctl` + WebKit debug socket |
|
|
324
|
+
| **Use Case** | Desktop web automation | Mobile web QA & debugging |
|
|
325
|
+
| **Parallel** | N tabs in 1 Chrome | N simulators, each with Safari |
|
|
326
|
+
| **Login** | Real Chrome sessions | Real Safari sessions |
|
|
327
|
+
|
|
328
|
+
Together, they provide **complete browser coverage** — Chrome for desktop, Safari for iOS — both controlled by AI agents through MCP with direct protocol connections. No middleware, no bundled browsers.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## License
|
|
333
|
+
|
|
334
|
+
MIT
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
<p align="center">
|
|
339
|
+
<b>Built for developers who ship mobile-first products.</b><br>
|
|
340
|
+
<sub>By the creators of <a href="https://github.com/shaun0927/openchrome">OpenChrome</a></sub>
|
|
341
|
+
</p>
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";exports.id=115,exports.ids=[115],exports.modules={115(s,e,t){function r(s){const e=[`## ${s.score>=90?"V":s.score>=70?"!":"X"} iOS QA Audit Report`,"",`**Score: ${s.score}/100** | Device: ${s.device} (${s.viewport.w}x${s.viewport.h}) | ${s.timestamp}`,`**URL:** ${s.url} | Duration: ${s.duration}ms`,"","| Severity | Count |","|----------|-------|",`| Critical | ${s.summary.critical} |`,`| High | ${s.summary.high} |`,`| Medium | ${s.summary.medium} |`,`| Low | ${s.summary.low} |`,`| Passed | ${s.summary.passed}/13 detectors |`],t=s.detectors.filter(s=>!s.passed);if(t.length>0){e.push("","### Issues Found","");for(const s of t){e.push(`#### [${s.severity.toUpperCase()}] ${s.detector} (${s.issueCount} issues)`);for(const t of s.issues.slice(0,5))e.push(`- \`${t.selector}\`: ${t.problem}`),e.push(` - **Fix:** ${t.fix}`);s.issues.length>5&&e.push(`- ... and ${s.issues.length-5} more`),e.push("")}}return e.join("\n")}t.d(e,{generateAuditMarkdown:()=>r})}};
|
|
2
|
+
//# sourceMappingURL=115.index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"115.index.js","mappings":"0EAEO,SAASA,EAAsBC,GACpC,MACMC,EAAQ,CACZ,MAFYD,EAAOE,OAAS,GAAK,IAAMF,EAAOE,OAAS,GAAK,IAAM,0BAGlE,GACA,YAAYF,EAAOE,yBAAyBF,EAAOG,WAAWH,EAAOI,SAASC,KAAKL,EAAOI,SAASE,QAAQN,EAAOO,YAClH,YAAYP,EAAOQ,mBAAmBR,EAAOS,aAC7C,GACA,uBACA,uBACA,gBAAgBT,EAAOU,QAAQC,aAC/B,YAAYX,EAAOU,QAAQE,SAC3B,cAAcZ,EAAOU,QAAQG,WAC7B,WAAWb,EAAOU,QAAQI,QAC1B,cAAcd,EAAOU,QAAQK,yBAGzBC,EAAShB,EAAOiB,UAAUC,OAAOC,IAAMA,EAAEJ,QAC/C,GAAIC,EAAOI,OAAS,EAAG,CACrBnB,EAAMoB,KAAK,GAAI,mBAAoB,IACnC,IAAK,MAAMC,KAAON,EAAQ,CACxBf,EAAMoB,KAAK,SAASC,EAAIC,SAASC,kBAAkBF,EAAIG,aAAaH,EAAII,sBACxE,IAAK,MAAMC,KAASL,EAAIM,OAAOC,MAAM,EAAG,GACtC5B,EAAMoB,KAAK,OAAOM,EAAMG,eAAeH,EAAMI,WAC7C9B,EAAMoB,KAAK,gBAAgBM,EAAMK,OAE/BV,EAAIM,OAAOR,OAAS,GAAGnB,EAAMoB,KAAK,aAAaC,EAAIM,OAAOR,OAAS,UACvEnB,EAAMoB,KAAK,GACb,CACF,CAEA,OAAOpB,EAAMgC,KAAK,KACpB,C","sources":["webpack://opensafari-mcp/./src/qa/report-markdown.ts"],"sourcesContent":["import { AuditReport } from './audit';\n\nexport function generateAuditMarkdown(report: AuditReport): string {\n const emoji = report.score >= 90 ? 'V' : report.score >= 70 ? '!' : 'X';\n const lines = [\n `## ${emoji} iOS QA Audit Report`,\n '',\n `**Score: ${report.score}/100** | Device: ${report.device} (${report.viewport.w}x${report.viewport.h}) | ${report.timestamp}`,\n `**URL:** ${report.url} | Duration: ${report.duration}ms`,\n '',\n '| Severity | Count |',\n '|----------|-------|',\n `| Critical | ${report.summary.critical} |`,\n `| High | ${report.summary.high} |`,\n `| Medium | ${report.summary.medium} |`,\n `| Low | ${report.summary.low} |`,\n `| Passed | ${report.summary.passed}/13 detectors |`,\n ];\n\n const failed = report.detectors.filter(d => !d.passed);\n if (failed.length > 0) {\n lines.push('', '### Issues Found', '');\n for (const det of failed) {\n lines.push(`#### [${det.severity.toUpperCase()}] ${det.detector} (${det.issueCount} issues)`);\n for (const issue of det.issues.slice(0, 5)) {\n lines.push(`- \\`${issue.selector}\\`: ${issue.problem}`);\n lines.push(` - **Fix:** ${issue.fix}`);\n }\n if (det.issues.length > 5) lines.push(`- ... and ${det.issues.length - 5} more`);\n lines.push('');\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["generateAuditMarkdown","report","lines","score","device","viewport","w","h","timestamp","url","duration","summary","critical","high","medium","low","passed","failed","detectors","filter","d","length","push","det","severity","toUpperCase","detector","issueCount","issue","issues","slice","selector","problem","fix","join"],"sourceRoot":""}
|
package/dist/67.index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";exports.id=67,exports.ids=[67],exports.modules={448(e,t,r){r.d(t,{QAHistory:()=>o});var i=r(943),s=r(928),n=r(857);class o{reportsDir;constructor(e){this.reportsDir=e??s.join(n.homedir(),".opensafari","reports")}async save(e){const t=s.join(this.reportsDir,this.sanitizeSite(e.url));await i.mkdir(t,{recursive:!0});const r=(new Date).toISOString().replace(/[:.]/g,"-")+".json",n=s.join(t,r);return await i.writeFile(n,JSON.stringify(e,null,2)),await this.rotate(t,30),n}async getLatest(e){const t=await this.listReportFiles(e);return 0===t.length?null:JSON.parse(await i.readFile(t[t.length-1],"utf-8"))}async getPrevious(e){const t=await this.listReportFiles(e);return t.length<2?null:JSON.parse(await i.readFile(t[t.length-2],"utf-8"))}async detectRegressions(e,t){const r=this.buildFingerprints(e),i=this.buildFingerprints(t),s=[...r.values()].filter(e=>!i.has(e.fingerprint)),n=[...i.values()].filter(e=>!r.has(e.fingerprint)),o=[...r.values()].filter(e=>i.has(e.fingerprint)),a=e.score-t.score,c=a>0?"improved":a<0?"regressed":"unchanged";return{currentScore:e.score,previousScore:t.score,scoreDelta:a,newIssues:s,fixedIssues:n,recurringIssues:o,summary:`Score ${c} ${t.score} -> ${e.score} (${a>=0?"+":""}${a}). ${n.length} fixed, ${s.length} new, ${o.length} recurring.`}}getExitCode(e,t){const r={failOnCritical:!0,failOnHigh:!1,...t};return r.failOnCritical&&e.summary.critical>0||r.failOnHigh&&e.summary.high>0||r.minScore&&e.score<r.minScore?1:0}buildFingerprints(e){const t=new Map;for(const r of e.detectors)for(const e of r.issues){const i=this.fingerprint(r.detector,e.selector);t.set(i,{detector:r.detector,selector:e.selector,problem:e.problem,fingerprint:i})}return t}fingerprint(e,t){const r=`${e}::${t}`;let i=0;for(let e=0;e<r.length;e++)i=(i<<5)-i+r.charCodeAt(e),i|=0;return i.toString(36)}sanitizeSite(e){try{return new URL(e).hostname.replace(/[^a-zA-Z0-9.-]/g,"_")}catch{return e.replace(/[^a-zA-Z0-9.-]/g,"_")}}async listReportFiles(e){const t=s.join(this.reportsDir,this.sanitizeSite(e));try{return(await i.readdir(t)).filter(e=>e.endsWith(".json")).sort().map(e=>s.join(t,e))}catch{return[]}}async rotate(e,t){try{const r=(await i.readdir(e)).filter(e=>e.endsWith(".json")).sort();if(r.length>t)for(const n of r.slice(0,r.length-t))await i.unlink(s.join(e,n)).catch(()=>{})}catch{}}}}};
|
|
2
|
+
//# sourceMappingURL=67.index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"67.index.js","mappings":"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","sources":["webpack://opensafari-mcp/./src/qa/history.ts"],"sourcesContent":["import * as fs from 'fs/promises';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { AuditReport } from './audit';\n\nexport interface RegressionReport {\n currentScore: number;\n previousScore: number;\n scoreDelta: number;\n newIssues: Array<{ detector: string; selector: string; problem: string; fingerprint: string }>;\n fixedIssues: Array<{ detector: string; selector: string; problem: string; fingerprint: string }>;\n recurringIssues: Array<{ detector: string; selector: string; problem: string; fingerprint: string }>;\n summary: string;\n}\n\nexport class QAHistory {\n private reportsDir: string;\n\n constructor(reportsDir?: string) {\n this.reportsDir = reportsDir ?? path.join(os.homedir(), '.opensafari', 'reports');\n }\n\n async save(report: AuditReport): Promise<string> {\n const siteDir = path.join(this.reportsDir, this.sanitizeSite(report.url));\n await fs.mkdir(siteDir, { recursive: true });\n const filename = new Date().toISOString().replace(/[:.]/g, '-') + '.json';\n const filePath = path.join(siteDir, filename);\n await fs.writeFile(filePath, JSON.stringify(report, null, 2));\n await this.rotate(siteDir, 30);\n return filePath;\n }\n\n async getLatest(url: string): Promise<AuditReport | null> {\n const files = await this.listReportFiles(url);\n if (files.length === 0) return null;\n return JSON.parse(await fs.readFile(files[files.length - 1], 'utf-8'));\n }\n\n async getPrevious(url: string): Promise<AuditReport | null> {\n const files = await this.listReportFiles(url);\n if (files.length < 2) return null;\n return JSON.parse(await fs.readFile(files[files.length - 2], 'utf-8'));\n }\n\n async detectRegressions(current: AuditReport, previous: AuditReport): Promise<RegressionReport> {\n const currentFP = this.buildFingerprints(current);\n const previousFP = this.buildFingerprints(previous);\n\n const newIssues = [...currentFP.values()].filter(i => !previousFP.has(i.fingerprint));\n const fixedIssues = [...previousFP.values()].filter(i => !currentFP.has(i.fingerprint));\n const recurringIssues = [...currentFP.values()].filter(i => previousFP.has(i.fingerprint));\n\n const scoreDelta = current.score - previous.score;\n const trend = scoreDelta > 0 ? 'improved' : scoreDelta < 0 ? 'regressed' : 'unchanged';\n\n return {\n currentScore: current.score,\n previousScore: previous.score,\n scoreDelta,\n newIssues,\n fixedIssues,\n recurringIssues,\n summary: `Score ${trend} ${previous.score} -> ${current.score} (${scoreDelta >= 0 ? '+' : ''}${scoreDelta}). ${fixedIssues.length} fixed, ${newIssues.length} new, ${recurringIssues.length} recurring.`,\n };\n }\n\n getExitCode(report: AuditReport, options?: { failOnCritical?: boolean; failOnHigh?: boolean; minScore?: number }): number {\n const opts = { failOnCritical: true, failOnHigh: false, ...options };\n if (opts.failOnCritical && report.summary.critical > 0) return 1;\n if (opts.failOnHigh && report.summary.high > 0) return 1;\n if (opts.minScore && report.score < opts.minScore) return 1;\n return 0;\n }\n\n private buildFingerprints(report: AuditReport): Map<string, { detector: string; selector: string; problem: string; fingerprint: string }> {\n const map = new Map<string, { detector: string; selector: string; problem: string; fingerprint: string }>();\n for (const det of report.detectors) {\n for (const issue of det.issues) {\n const fp = this.fingerprint(det.detector, issue.selector);\n map.set(fp, { detector: det.detector, selector: issue.selector, problem: issue.problem, fingerprint: fp });\n }\n }\n return map;\n }\n\n private fingerprint(detector: string, selector: string): string {\n const input = `${detector}::${selector}`;\n let hash = 0;\n for (let i = 0; i < input.length; i++) {\n hash = ((hash << 5) - hash) + input.charCodeAt(i);\n hash |= 0;\n }\n return hash.toString(36);\n }\n\n private sanitizeSite(url: string): string {\n try { return new URL(url).hostname.replace(/[^a-zA-Z0-9.-]/g, '_'); }\n catch { return url.replace(/[^a-zA-Z0-9.-]/g, '_'); }\n }\n\n private async listReportFiles(url: string): Promise<string[]> {\n const siteDir = path.join(this.reportsDir, this.sanitizeSite(url));\n try {\n const files = await fs.readdir(siteDir);\n return files.filter(f => f.endsWith('.json')).sort().map(f => path.join(siteDir, f));\n } catch { return []; }\n }\n\n private async rotate(dir: string, maxReports: number): Promise<void> {\n try {\n const files = await fs.readdir(dir);\n const sorted = files.filter(f => f.endsWith('.json')).sort();\n if (sorted.length > maxReports) {\n for (const f of sorted.slice(0, sorted.length - maxReports)) {\n await fs.unlink(path.join(dir, f)).catch(() => {});\n }\n }\n } catch { /* */ }\n }\n}\n"],"names":["QAHistory","reportsDir","constructor","this","save","report","siteDir","sanitizeSite","url","recursive","filename","Date","toISOString","replace","filePath","JSON","stringify","rotate","getLatest","files","listReportFiles","length","parse","getPrevious","detectRegressions","current","previous","currentFP","buildFingerprints","previousFP","newIssues","values","filter","i","has","fingerprint","fixedIssues","recurringIssues","scoreDelta","score","trend","currentScore","previousScore","summary","getExitCode","options","opts","failOnCritical","failOnHigh","critical","high","minScore","map","Map","det","detectors","issue","issues","fp","detector","selector","set","problem","input","hash","charCodeAt","toString","URL","hostname","f","endsWith","sort","dir","maxReports","sorted","slice","catch"],"sourceRoot":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";exports.id=679,exports.ids=[679],exports.modules={679(e,s,t){t.d(s,{HTTPTransport:()=>i});var n=t(67),r=t(598),o=t(462);class i{server=null;messageHandler=null;port;sessions=new Set;sseConnections=[];sessionDeleteHandler=null;constructor(e){this.port=e}onSessionDelete(e){this.sessionDeleteHandler=e}onMessage(e){this.messageHandler=e}send(e){for(const s of this.sseConnections)try{s.res.write(`data: ${JSON.stringify(e)}\n\n`)}catch{}}start(){this.server=n.createServer((e,s)=>{this.handleHTTPRequest(e,s)}),this.server.listen(this.port,()=>{console.error(`[HTTPTransport] Listening on port ${this.port}`),console.error(`[HTTPTransport] MCP endpoint: http://localhost:${this.port}/mcp`)}),this.server.on("error",e=>{console.error("[HTTPTransport] Server error:",e)})}async close(){for(const e of this.sseConnections)try{e.res.end()}catch{}return this.sseConnections=[],new Promise(e=>{this.server?this.server.close(()=>{this.server=null,e()}):e()})}handleHTTPRequest(e,s){const t=new URL(e.url||"/",`http://localhost:${this.port}`).pathname;if(s.setHeader("Access-Control-Allow-Origin","*"),s.setHeader("Access-Control-Allow-Methods","GET, POST, DELETE, OPTIONS"),s.setHeader("Access-Control-Allow-Headers","Content-Type, Mcp-Session-Id"),s.setHeader("Access-Control-Expose-Headers","Mcp-Session-Id"),"OPTIONS"===e.method)return s.writeHead(204),void s.end();if("/health"!==t){if("/mcp"===t)switch(e.method){case"POST":return void this.handlePost(e,s);case"GET":return void this.handleSSE(e,s);case"DELETE":return void this.handleDelete(e,s);default:return s.writeHead(405,{"Content-Type":"application/json"}),void s.end(JSON.stringify({error:"Method not allowed"}))}s.writeHead(404,{"Content-Type":"application/json"}),s.end(JSON.stringify({error:"Not found"}))}else this.handleHealth(s)}handleHealth(e){e.writeHead(200,{"Content-Type":"application/json"}),e.end(JSON.stringify({status:"ok",transport:"http",activeSessions:this.sessions.size,sseConnections:this.sseConnections.length}))}handlePost(e,s){const t=[];let n=0;e.on("data",r=>{if(n+=r.length,n>10485760)return s.writeHead(413,{"Content-Type":"application/json"}),s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.INVALID_REQUEST,message:"Request body too large"}})),void e.destroy();t.push(r)}),e.on("end",async()=>{const n=Buffer.concat(t).toString("utf-8");if(!n.trim())return s.writeHead(400,{"Content-Type":"application/json"}),void s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.PARSE_ERROR,message:"Empty request body"}}));let i;try{i=JSON.parse(n)}catch(e){return s.writeHead(400,{"Content-Type":"application/json"}),void s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.PARSE_ERROR,message:e instanceof Error?e.message:"Parse error"}}))}let a=e.headers["mcp-session-id"];if(!this.messageHandler)return s.writeHead(500,{"Content-Type":"application/json"}),void s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.INTERNAL_ERROR,message:"No message handler registered"}}));if(Array.isArray(i)){const e=(await this.processBatch(i,a)).filter(e=>null!==e);return a&&s.setHeader("Mcp-Session-Id",a),void(0===e.length?(s.writeHead(202),s.end()):1===e.length?(s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify(e[0]))):(s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify(e))))}const d=i;"initialize"!==d.method||a||(a=r.randomUUID(),this.sessions.add(a));try{const e=await this.messageHandler(d);a&&s.setHeader("Mcp-Session-Id",a),null===e?(s.writeHead(202),s.end()):(s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify(e)))}catch(e){const t=d.id??0;s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify({jsonrpc:"2.0",id:t,error:{code:o.D.INTERNAL_ERROR,message:e instanceof Error?e.message:"Internal error"}}))}}),e.on("error",e=>{console.error("[HTTPTransport] Request read error:",e),s.headersSent||(s.writeHead(400,{"Content-Type":"application/json"}),s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.PARSE_ERROR,message:"Request read error"}})))})}handleSSE(e,s){const t=e.headers["mcp-session-id"]||"anonymous";s.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"}),s.write(": keepalive\n\n");const n={res:s,sessionId:t};this.sseConnections.push(n),e.on("close",()=>{const e=this.sseConnections.indexOf(n);-1!==e&&this.sseConnections.splice(e,1),console.error(`[HTTPTransport] SSE client disconnected (session: ${t})`)})}handleDelete(e,s){const t=e.headers["mcp-session-id"];t&&this.sessions.has(t)?(this.sessions.delete(t),this.sessionDeleteHandler&&this.sessionDeleteHandler(t),this.sseConnections=this.sseConnections.filter(e=>{if(e.sessionId===t){try{e.res.end()}catch{}return!1}return!0}),s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify({status:"session terminated"}))):(s.writeHead(404,{"Content-Type":"application/json"}),s.end(JSON.stringify({error:"Session not found"})))}async processBatch(e,s){const t=this.messageHandler;s||e.some(e=>"object"==typeof e&&null!==e&&"initialize"===e.method)&&(s=r.randomUUID(),this.sessions.add(s));const n=e.map(async e=>{if("object"!=typeof e||null===e)return{jsonrpc:"2.0",id:0,error:{code:o.D.INVALID_REQUEST,message:"Invalid batch element: not an object"}};const s=e;try{return await t(s)}catch(e){return{jsonrpc:"2.0",id:s.id??0,error:{code:o.D.INTERNAL_ERROR,message:e instanceof Error?e.message:"Internal error"}}}});return Promise.all(n)}}}};
|
|
2
|
+
//# sourceMappingURL=679.index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"679.index.js","mappings":"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","sources":["webpack://opensafari-mcp/./src/transports/http.ts"],"sourcesContent":["/**\n * Streamable HTTP transport for MCP server.\n *\n * Implements MCP Streamable HTTP transport (spec 2025-03-26):\n * - POST /mcp: receives JSON-RPC request/notification, returns JSON-RPC response\n * - GET /health: basic health check (separate from the self-healing health endpoint)\n * - DELETE /mcp: session termination\n *\n * Key difference from stdio: client disconnect does NOT kill the server.\n * The HTTP server continues to accept new connections.\n */\n\nimport * as http from 'node:http';\nimport * as crypto from 'node:crypto';\nimport { MCPResponse, MCPErrorCodes } from '../types/mcp';\nimport { MCPTransport } from './index';\n\n/** Maximum allowed HTTP request body size (10 MB) to prevent OOM from oversized requests */\nconst MAX_BODY_BYTES = 10 * 1024 * 1024;\n\n/** Active SSE connections for server-initiated notifications */\ninterface SSEConnection {\n res: http.ServerResponse;\n sessionId: string;\n}\n\nexport class HTTPTransport implements MCPTransport {\n private server: http.Server | null = null;\n private messageHandler: ((msg: Record<string, unknown>) => Promise<MCPResponse | null>) | null = null;\n private port: number;\n private sessions: Set<string> = new Set();\n private sseConnections: SSEConnection[] = [];\n private sessionDeleteHandler: ((sessionId: string) => void) | null = null;\n\n constructor(port: number) {\n this.port = port;\n }\n\n /**\n * Register a callback to be invoked whenever a session is deleted.\n * Used by MCPServer to clean up per-session state (e.g. rate-limiter buckets).\n */\n onSessionDelete(handler: (sessionId: string) => void): void {\n this.sessionDeleteHandler = handler;\n }\n\n onMessage(handler: (msg: Record<string, unknown>) => Promise<MCPResponse | null>): void {\n this.messageHandler = handler;\n }\n\n /**\n * Send a server-initiated notification to all connected SSE clients.\n * For HTTP, request-correlated responses are sent directly in handlePost.\n */\n send(response: MCPResponse): void {\n // Broadcast to all SSE connections\n for (const conn of this.sseConnections) {\n try {\n conn.res.write(`data: ${JSON.stringify(response)}\\n\\n`);\n } catch {\n // Connection may have been closed\n }\n }\n }\n\n start(): void {\n this.server = http.createServer((req, res) => {\n this.handleHTTPRequest(req, res);\n });\n\n this.server.listen(this.port, () => {\n console.error(`[HTTPTransport] Listening on port ${this.port}`);\n console.error(`[HTTPTransport] MCP endpoint: http://localhost:${this.port}/mcp`);\n });\n\n this.server.on('error', (err) => {\n console.error(`[HTTPTransport] Server error:`, err);\n });\n }\n\n async close(): Promise<void> {\n // Close all SSE connections\n for (const conn of this.sseConnections) {\n try {\n conn.res.end();\n } catch {\n // Already closed\n }\n }\n this.sseConnections = [];\n\n return new Promise((resolve) => {\n if (this.server) {\n this.server.close(() => {\n this.server = null;\n resolve();\n });\n } else {\n resolve();\n }\n });\n }\n\n private handleHTTPRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || '/', `http://localhost:${this.port}`);\n const pathname = url.pathname;\n\n // CORS headers for all responses\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');\n res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');\n\n // Handle CORS preflight\n if (req.method === 'OPTIONS') {\n res.writeHead(204);\n res.end();\n return;\n }\n\n if (pathname === '/health') {\n this.handleHealth(res);\n return;\n }\n\n if (pathname === '/mcp') {\n switch (req.method) {\n case 'POST':\n this.handlePost(req, res);\n return;\n case 'GET':\n this.handleSSE(req, res);\n return;\n case 'DELETE':\n this.handleDelete(req, res);\n return;\n default:\n res.writeHead(405, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Method not allowed' }));\n return;\n }\n }\n\n // Unknown path\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Not found' }));\n }\n\n /**\n * GET /health - basic health check\n */\n private handleHealth(res: http.ServerResponse): void {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n status: 'ok',\n transport: 'http',\n activeSessions: this.sessions.size,\n sseConnections: this.sseConnections.length,\n }));\n }\n\n /**\n * POST /mcp - handle JSON-RPC request or batch\n */\n private handlePost(req: http.IncomingMessage, res: http.ServerResponse): void {\n const chunks: Buffer[] = [];\n let bodyBytes = 0;\n\n req.on('data', (chunk: Buffer) => {\n bodyBytes += chunk.length;\n if (bodyBytes > MAX_BODY_BYTES) {\n res.writeHead(413, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: { code: MCPErrorCodes.INVALID_REQUEST, message: 'Request body too large' },\n }));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n\n req.on('end', async () => {\n const body = Buffer.concat(chunks).toString('utf-8');\n\n if (!body.trim()) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: { code: MCPErrorCodes.PARSE_ERROR, message: 'Empty request body' },\n }));\n return;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch (error) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: {\n code: MCPErrorCodes.PARSE_ERROR,\n message: error instanceof Error ? error.message : 'Parse error',\n },\n }));\n return;\n }\n\n // Session tracking via Mcp-Session-Id header\n let sessionId = req.headers['mcp-session-id'] as string | undefined;\n\n if (!this.messageHandler) {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: { code: MCPErrorCodes.INTERNAL_ERROR, message: 'No message handler registered' },\n }));\n return;\n }\n\n // Handle JSON-RPC batch (array of requests)\n if (Array.isArray(parsed)) {\n const results = await this.processBatch(parsed, sessionId);\n // Filter out null results (notifications don't produce responses)\n const responses = results.filter((r): r is MCPResponse => r !== null);\n\n if (sessionId) {\n res.setHeader('Mcp-Session-Id', sessionId);\n }\n\n if (responses.length === 0) {\n // All were notifications — respond with 202 Accepted\n res.writeHead(202);\n res.end();\n } else if (responses.length === 1) {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(responses[0]));\n } else {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(responses));\n }\n return;\n }\n\n // Single request/notification\n const msg = parsed as Record<string, unknown>;\n\n // Check if this is an initialize request — assign session ID\n if (msg.method === 'initialize' && !sessionId) {\n sessionId = crypto.randomUUID();\n this.sessions.add(sessionId);\n }\n\n try {\n const response = await this.messageHandler(msg);\n\n if (sessionId) {\n res.setHeader('Mcp-Session-Id', sessionId);\n }\n\n if (response === null) {\n // Notification — no response body\n res.writeHead(202);\n res.end();\n } else {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(response));\n }\n } catch (error) {\n const id = (msg.id as string | number) ?? 0;\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id,\n error: {\n code: MCPErrorCodes.INTERNAL_ERROR,\n message: error instanceof Error ? error.message : 'Internal error',\n },\n }));\n }\n });\n\n req.on('error', (err) => {\n console.error('[HTTPTransport] Request read error:', err);\n if (!res.headersSent) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: { code: MCPErrorCodes.PARSE_ERROR, message: 'Request read error' },\n }));\n }\n });\n }\n\n /**\n * GET /mcp - Server-Sent Events for server-initiated notifications\n */\n private handleSSE(req: http.IncomingMessage, res: http.ServerResponse): void {\n const sessionId = req.headers['mcp-session-id'] as string || 'anonymous';\n\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n });\n\n // Send initial keepalive\n res.write(': keepalive\\n\\n');\n\n const conn: SSEConnection = { res, sessionId };\n this.sseConnections.push(conn);\n\n // Clean up on disconnect\n req.on('close', () => {\n const idx = this.sseConnections.indexOf(conn);\n if (idx !== -1) {\n this.sseConnections.splice(idx, 1);\n }\n console.error(`[HTTPTransport] SSE client disconnected (session: ${sessionId})`);\n });\n }\n\n /**\n * DELETE /mcp - Session termination\n */\n private handleDelete(req: http.IncomingMessage, res: http.ServerResponse): void {\n const sessionId = req.headers['mcp-session-id'] as string | undefined;\n\n if (sessionId && this.sessions.has(sessionId)) {\n this.sessions.delete(sessionId);\n\n // Notify session-delete listeners (e.g. rate-limiter cleanup)\n if (this.sessionDeleteHandler) {\n this.sessionDeleteHandler(sessionId);\n }\n\n // Close any SSE connections for this session\n this.sseConnections = this.sseConnections.filter((conn) => {\n if (conn.sessionId === sessionId) {\n try {\n conn.res.end();\n } catch {\n // Already closed\n }\n return false;\n }\n return true;\n });\n\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'session terminated' }));\n } else {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Session not found' }));\n }\n }\n\n /**\n * Process a batch of JSON-RPC messages\n */\n private async processBatch(\n messages: unknown[],\n sessionId: string | undefined,\n ): Promise<(MCPResponse | null)[]> {\n const handler = this.messageHandler!;\n\n // Assign sessionId once before concurrent processing to avoid data race\n // when multiple initialize requests appear in the same batch.\n if (!sessionId) {\n const hasInitialize = messages.some(\n (msg) => typeof msg === 'object' && msg !== null && (msg as Record<string, unknown>).method === 'initialize',\n );\n if (hasInitialize) {\n sessionId = crypto.randomUUID();\n this.sessions.add(sessionId);\n }\n }\n\n const promises = messages.map(async (msg) => {\n if (typeof msg !== 'object' || msg === null) {\n return {\n jsonrpc: '2.0' as const,\n id: 0,\n error: {\n code: MCPErrorCodes.INVALID_REQUEST,\n message: 'Invalid batch element: not an object',\n },\n } as MCPResponse;\n }\n\n const record = msg as Record<string, unknown>;\n\n try {\n return await handler(record);\n } catch (error) {\n const id = (record.id as string | number) ?? 0;\n return {\n jsonrpc: '2.0' as const,\n id,\n error: {\n code: MCPErrorCodes.INTERNAL_ERROR,\n message: error instanceof Error ? error.message : 'Internal error',\n },\n } as MCPResponse;\n }\n });\n\n return Promise.all(promises);\n }\n}\n"],"names":["HTTPTransport","server","messageHandler","port","sessions","Set","sseConnections","sessionDeleteHandler","constructor","this","onSessionDelete","handler","onMessage","send","response","conn","res","write","JSON","stringify","start","req","handleHTTPRequest","listen","console","error","on","err","close","end","Promise","resolve","pathname","URL","url","setHeader","method","writeHead","handlePost","handleSSE","handleDelete","handleHealth","status","transport","activeSessions","size","length","chunks","bodyBytes","chunk","jsonrpc","id","code","INVALID_REQUEST","message","destroy","push","async","body","Buffer","concat","toString","trim","PARSE_ERROR","parsed","parse","Error","sessionId","headers","INTERNAL_ERROR","Array","isArray","responses","processBatch","filter","r","msg","add","headersSent","idx","indexOf","splice","has","delete","messages","some","promises","map","record","all"],"sourceRoot":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";exports.id=681,exports.ids=[681],exports.modules={681(e,t,n){async function s(e){return e.evaluate("\n (function() {\n var inputs = document.querySelectorAll('input, select, textarea');\n var issues = [];\n inputs.forEach(function(el) {\n var style = window.getComputedStyle(el);\n var size = parseFloat(style.fontSize);\n if (size < 16) {\n var rect = el.getBoundingClientRect();\n issues.push({\n selector: el.id ? '#' + el.id : (el.name ? el.tagName.toLowerCase() + '[name=\"' + el.name + '\"]' : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : '')),\n problem: 'font-size is ' + size + 'px (< 16px minimum)',\n fix: 'Set font-size to at least 16px to prevent iOS Safari auto-zoom on focus',\n });\n }\n });\n return { detector: 'auto_zoom', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: inputs.length, issueCount: issues.length };\n })()\n ")}async function i(e){return e.evaluate("\n (function() {\n var selectors = 'a, button, input, select, textarea, [onclick], [role=\"button\"], [role=\"link\"], [tabindex]:not([tabindex=\"-1\"])';\n var elements = document.querySelectorAll(selectors);\n var threshold = 44;\n var issues = [];\n elements.forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return;\n if (rect.width < threshold || rect.height < threshold) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Touch target is ' + Math.round(rect.width) + 'x' + Math.round(rect.height) + 'px (minimum: ' + threshold + 'x' + threshold + 'px)',\n fix: 'Increase element size to at least 44x44px or add padding',\n size: { width: Math.round(rect.width), height: Math.round(rect.height) },\n });\n }\n });\n return { detector: 'touch_targets', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: elements.length, issueCount: issues.length };\n })()\n ")}async function o(e){return e.evaluate("\n (function() {\n var issues = [];\n try {\n Array.from(document.styleSheets).forEach(function(sheet) {\n try {\n Array.from(sheet.cssRules || []).forEach(function(rule) {\n if (rule.selectorText && rule.selectorText.indexOf(':hover') !== -1) {\n var style = rule.style;\n if (style.display || style.visibility || style.opacity) {\n var baseSelector = rule.selectorText.replace(/:hover/g, '').trim();\n var el = document.querySelector(baseSelector);\n if (el) {\n issues.push({\n selector: baseSelector,\n problem: ':hover changes visibility — inaccessible on touch devices',\n fix: 'Add click/touch handler or use :focus-within as alternative',\n cssRule: rule.selectorText,\n });\n }\n }\n }\n });\n } catch(e) {}\n });\n } catch(e) {}\n return { detector: 'hover_only', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 0, issueCount: issues.length };\n })()\n ")}async function r(e){return e.evaluate("\n (function() {\n var issues = [];\n var inputs = document.querySelectorAll('input');\n inputs.forEach(function(el) {\n var type = el.getAttribute('type') || 'text';\n var inputMode = el.getAttribute('inputmode');\n var name = (el.name || el.id || '').toLowerCase();\n if (type === 'text' && !inputMode) {\n if (name.match(/phone|tel|zip|postal|code|pin|otp|cvv|cvc/)) {\n issues.push({ selector: el.id ? '#' + el.id : 'input[name=\"' + el.name + '\"]', problem: 'Likely numeric field using type=\"text\" without inputmode', fix: 'Add inputmode=\"numeric\" or inputmode=\"tel\"' });\n }\n if (name.match(/email/) && type !== 'email') {\n issues.push({ selector: el.id ? '#' + el.id : 'input[name=\"' + el.name + '\"]', problem: 'Email field using type=\"text\"', fix: 'Use type=\"email\" for email keyboard' });\n }\n }\n });\n return { detector: 'input_type', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: inputs.length, issueCount: issues.length };\n })()\n ")}async function a(e){return e.evaluate("\n (function() {\n var issues = [];\n var viewport = document.querySelector('meta[name=\"viewport\"]');\n var hasViewportFitCover = viewport && viewport.content && viewport.content.indexOf('viewport-fit=cover') !== -1;\n if (!hasViewportFitCover) {\n return { detector: 'safe_area', severity: 'pass', issues: [], passed: true, totalScanned: 0, issueCount: 0, metadata: { note: 'viewport-fit=cover not set' } };\n }\n var all = document.querySelectorAll('*');\n all.forEach(function(el) {\n var style = window.getComputedStyle(el);\n if (style.position === 'fixed' || style.position === 'sticky') {\n var top = parseFloat(style.top);\n var bottom = parseFloat(style.bottom);\n if (top === 0 || bottom === 0) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Fixed element at ' + (top === 0 ? 'top' : 'bottom') + ' edge without safe-area-inset padding',\n fix: 'Add padding: env(safe-area-inset-' + (top === 0 ? 'top' : 'bottom') + ')',\n });\n }\n }\n });\n return { detector: 'safe_area', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 1, issueCount: issues.length };\n })()\n ")}async function l(e){const t=await e.evaluate("\n (function() {\n return Array.from(document.querySelectorAll('*')).filter(function(el) {\n var s = window.getComputedStyle(el);\n return s.position === 'fixed' && parseFloat(s.bottom) < 50;\n }).map(function(el) {\n return {\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n bottom: parseFloat(window.getComputedStyle(el).bottom),\n rect: { y: el.getBoundingClientRect().y, height: el.getBoundingClientRect().height }\n };\n });\n })()\n ");if(!t||0===t.length)return{detector:"keyboard_overlap",severity:"pass",issues:[],passed:!0,totalScanned:0,issueCount:0};const n=await e.evaluate("\n Array.from(document.querySelectorAll('input:not([type=\"hidden\"]), textarea, select')).slice(0, 5).map(function(el) {\n return el.id ? '#' + el.id : (el.name ? el.tagName.toLowerCase() + '[name=\"' + el.name + '\"]' : el.tagName.toLowerCase());\n })\n "),s=[];for(const i of n||[])try{await e.click(i),await new Promise(e=>setTimeout(e,500));const n=await e.evaluate("window.visualViewport ? window.visualViewport.height : window.innerHeight");for(const e of t)e.rect.y+e.rect.height>n&&s.push({selector:e.selector,problem:`Fixed bottom element hidden behind keyboard (element bottom: ${Math.round(e.rect.y+e.rect.height)}px, viewport with keyboard: ${Math.round(n)}px)`,fix:"Use visualViewport API to adjust position when keyboard appears",triggeredBy:i});await e.dismissKeyboard(),await new Promise(e=>setTimeout(e,300))}catch{}return{detector:"keyboard_overlap",severity:s.length>0?"critical":"pass",issues:s,passed:0===s.length,totalScanned:t.length*(n?.length??0),issueCount:s.length}}async function u(e){return e.evaluate("\n (function() {\n var docWidth = document.documentElement.scrollWidth;\n var vpWidth = window.innerWidth;\n if (docWidth <= vpWidth) {\n return { detector: 'horizontal_overflow', severity: 'pass', issues: [], passed: true, totalScanned: 1, issueCount: 0 };\n }\n var issues = [];\n function findCulprits(parent) {\n Array.from(parent.children).forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (rect.right > vpWidth + 1) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Element extends to ' + Math.round(rect.right) + 'px (viewport: ' + vpWidth + 'px)',\n fix: 'Add overflow-x: hidden or max-width: 100%',\n overflow: Math.round(rect.right - vpWidth) + 'px',\n });\n }\n });\n }\n findCulprits(document.body);\n return { detector: 'horizontal_overflow', severity: 'high', issues: issues.slice(0, 20), passed: false, totalScanned: 1, issueCount: issues.length };\n })()\n ")}async function c(e){return e.evaluate("\n (function() {\n var temp = document.createElement('div');\n temp.style.cssText = 'position:fixed;top:0;height:100vh;width:1px;pointer-events:none;visibility:hidden';\n document.body.appendChild(temp);\n var vh100 = temp.offsetHeight;\n document.body.removeChild(temp);\n var innerH = window.innerHeight;\n var diff = vh100 - innerH;\n if (Math.abs(diff) < 10) {\n return { detector: '100vh', severity: 'pass', issues: [], passed: true, totalScanned: 1, issueCount: 0, metadata: { vh100: vh100, innerHeight: innerH, difference: diff } };\n }\n var issues = [];\n issues.push({\n selector: 'viewport',\n problem: '100vh = ' + vh100 + 'px but visible viewport = ' + innerH + 'px (diff: ' + diff + 'px)',\n fix: 'Use 100dvh or calc(var(--vh, 1vh) * 100) with JS viewport listener',\n });\n return { detector: '100vh', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 1, issueCount: issues.length, metadata: { vh100: vh100, innerHeight: innerH, difference: diff } };\n })()\n ")}async function d(e){return e.evaluate("\n (function() {\n var fixedEls = Array.from(document.querySelectorAll('*')).filter(function(el) {\n var s = window.getComputedStyle(el);\n return s.position === 'fixed' || s.position === 'sticky';\n }).map(function(el) {\n return {\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n rect: el.getBoundingClientRect(),\n zIndex: parseInt(window.getComputedStyle(el).zIndex) || 0,\n };\n });\n var issues = [];\n for (var i = 0; i < fixedEls.length; i++) {\n for (var j = i + 1; j < fixedEls.length; j++) {\n var a = fixedEls[i], b = fixedEls[j];\n var overlap = !(a.rect.right < b.rect.left || a.rect.left > b.rect.right || a.rect.bottom < b.rect.top || a.rect.top > b.rect.bottom);\n if (overlap && a.zIndex === b.zIndex) {\n issues.push({\n selector: a.selector + ' <-> ' + b.selector,\n problem: 'Overlapping fixed elements with same z-index (' + a.zIndex + ')',\n fix: 'Set distinct z-index values',\n });\n }\n }\n }\n return { detector: 'fixed_stacking', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: fixedEls.length, issueCount: issues.length };\n })()\n ")}async function h(e){return e.evaluate("\n (function() {\n var issues = [];\n var bodyOverflow = document.body.style.overflow;\n var htmlOverflow = document.documentElement.style.overflow;\n var hasVisibleModal = document.querySelector('[role=\"dialog\"]:not([aria-hidden=\"true\"]), .modal:not(.hidden), [data-modal]:not([hidden])');\n if ((bodyOverflow === 'hidden' || htmlOverflow === 'hidden') && !hasVisibleModal) {\n issues.push({ selector: bodyOverflow === 'hidden' ? 'document.body' : 'document.documentElement', problem: 'overflow: hidden set but no visible modal found', fix: 'Ensure modal close handlers restore overflow' });\n }\n if (bodyOverflow === 'unset') {\n issues.push({ selector: 'document.body', problem: 'overflow set to \"unset\" — not a proper reset', fix: 'Use document.body.style.overflow = \"\" (empty string)' });\n }\n return { detector: 'scroll_lock', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 2, issueCount: issues.length };\n })()\n ")}async function m(e,t,n){const s=await e.evaluate("\n (document.querySelector('meta[name=\"color-scheme\"]') || {}).content || 'not set'\n "),i=[];let o,r;if("not set"===s&&i.push({selector:"head",problem:'No <meta name="color-scheme"> tag — browser may apply forced dark mode',fix:'Add <meta name="color-scheme" content="light only"> or implement proper dark mode'}),t&&n)try{await t.setAppearance(n,"light"),await new Promise(e=>setTimeout(e,500)),o=(await e.screenshot()).toString("base64"),await t.setAppearance(n,"dark"),await new Promise(e=>setTimeout(e,500)),r=(await e.screenshot()).toString("base64"),await t.setAppearance(n,"light")}catch{}return{detector:"dark_mode",severity:i.length>0?"medium":"pass",issues:i,passed:0===i.length,totalScanned:1,issueCount:i.length,metadata:{colorScheme:s,...o?{lightScreenshot:o,darkScreenshot:r,note:"Compare screenshots visually"}:{}}}}async function p(e,t,n){const s=await e.evaluate("({\n scrollWidth: document.documentElement.scrollWidth,\n innerWidth: window.innerWidth,\n overflow: document.documentElement.scrollWidth > window.innerWidth\n })"),i=[];if(t&&n)try{await t.rotate(n),await new Promise(e=>setTimeout(e,1e3));const s=await e.evaluate("({\n scrollWidth: document.documentElement.scrollWidth,\n innerWidth: window.innerWidth,\n overflow: document.documentElement.scrollWidth > window.innerWidth\n })");s.overflow&&i.push({selector:"document.documentElement",problem:`Horizontal overflow in landscape (scrollWidth: ${s.scrollWidth}px, viewport: ${s.innerWidth}px)`,fix:"Ensure responsive layout handles landscape orientation"}),await t.rotate(n)}catch{}return{detector:"orientation",severity:i.length>0?"medium":"pass",issues:i,passed:0===i.length,totalScanned:1,issueCount:i.length,metadata:{portrait:s}}}async function f(e){return e.evaluate("\n (function() {\n var checks = [\n { name: 'viewport', selector: 'meta[name=\"viewport\"]', required: true, fix: 'Add <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' },\n { name: 'theme-color', selector: 'meta[name=\"theme-color\"]', required: false, fix: 'Add <meta name=\"theme-color\" content=\"#yourColor\">' },\n { name: 'color-scheme', selector: 'meta[name=\"color-scheme\"]', required: false, fix: 'Add <meta name=\"color-scheme\" content=\"light only\">' },\n { name: 'apple-touch-icon', selector: 'link[rel=\"apple-touch-icon\"]', required: false, fix: 'Add <link rel=\"apple-touch-icon\" href=\"/icon-180.png\">' },\n { name: 'manifest', selector: 'link[rel=\"manifest\"]', required: false, fix: 'Add <link rel=\"manifest\" href=\"/manifest.json\">' },\n ];\n var issues = [];\n checks.forEach(function(check) {\n if (!document.querySelector(check.selector)) {\n issues.push({ selector: 'head', problem: 'Missing ' + check.name + (check.required ? ' (required)' : ' (recommended)'), fix: check.fix });\n }\n });\n return { detector: 'pwa_meta', severity: issues.some(function(i) { return i.problem.indexOf('required') !== -1; }) ? 'high' : issues.length > 0 ? 'low' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: checks.length, issueCount: issues.length };\n })()\n ")}n.d(t,{QAAudit:()=>g});const v={critical:10,high:5,medium:2,low:1};class g{client;config;simulator;deviceId;deviceInfo;constructor(e,t={},n,s,i){this.client=e,this.config=t,this.simulator=n,this.deviceId=s,this.deviceInfo=i}async runFullAudit(e){e&&await this.client.navigate({url:e,waitUntil:"load"});const t=await this.client.evaluate("window.location.href"),n=Date.now(),v=await Promise.allSettled([s(this.client),i(this.client),o(this.client),r(this.client),a(this.client),u(this.client),c(this.client),d(this.client),h(this.client),f(this.client)]),g=[];try{g.push({status:"fulfilled",value:await l(this.client)})}catch(e){g.push({status:"rejected",reason:e})}try{g.push({status:"fulfilled",value:await m(this.client,this.simulator,this.deviceId)})}catch(e){g.push({status:"rejected",reason:e})}try{g.push({status:"fulfilled",value:await p(this.client,this.simulator,this.deviceId)})}catch(e){g.push({status:"rejected",reason:e})}const w=[...v,...g].map((e,t)=>"fulfilled"===e.status?function(e,t){const n=t.ignore?.filter(t=>t.detector===e.detector)??[];return n.length>0&&(e.issues=e.issues.filter(e=>!n.some(t=>e.selector.includes(t.selector))),e.issueCount=e.issues.length,e.passed=0===e.issueCount,e.passed&&(e.severity="pass")),e}(e.value,this.config):{detector:"unknown",severity:"error",issues:[],passed:!1,totalScanned:0,issueCount:0,error:e.reason instanceof Error?e.reason.message:String(e.reason)}),y=this.calculateScore(w),x=this.summarize(w);return{url:t,device:this.deviceInfo?.name??"unknown",viewport:{w:this.deviceInfo?.w??0,h:this.deviceInfo?.h??0},timestamp:(new Date).toISOString(),duration:Date.now()-n,score:y,summary:x,detectors:w}}calculateScore(e){let t=0;for(const n of e)n.severity&&"pass"!==n.severity&&"error"!==n.severity&&(t+=(v[n.severity]??0)*n.issueCount);return Math.max(0,100-t)}summarize(e){return{totalIssues:e.reduce((e,t)=>e+t.issueCount,0),critical:e.filter(e=>"critical"===e.severity).reduce((e,t)=>e+t.issueCount,0),high:e.filter(e=>"high"===e.severity).reduce((e,t)=>e+t.issueCount,0),medium:e.filter(e=>"medium"===e.severity).reduce((e,t)=>e+t.issueCount,0),low:e.filter(e=>"low"===e.severity).reduce((e,t)=>e+t.issueCount,0),passed:e.filter(e=>e.passed).length,failed:e.filter(e=>!e.passed).length,errors:e.filter(e=>"error"===e.severity).length}}}}};
|
|
2
|
+
//# sourceMappingURL=681.index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"681.index.js","mappings":"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","sources":["webpack://opensafari-mcp/./src/qa/detectors/auto-zoom.ts","webpack://opensafari-mcp/./src/qa/detectors/touch-targets.ts","webpack://opensafari-mcp/./src/qa/detectors/hover-only.ts","webpack://opensafari-mcp/./src/qa/detectors/input-type.ts","webpack://opensafari-mcp/./src/qa/detectors/safe-area.ts","webpack://opensafari-mcp/./src/qa/detectors/keyboard-overlap.ts","webpack://opensafari-mcp/./src/qa/detectors/horizontal-overflow.ts","webpack://opensafari-mcp/./src/qa/detectors/vh100.ts","webpack://opensafari-mcp/./src/qa/detectors/fixed-stacking.ts","webpack://opensafari-mcp/./src/qa/detectors/scroll-lock.ts","webpack://opensafari-mcp/./src/qa/detectors/dark-mode.ts","webpack://opensafari-mcp/./src/qa/detectors/orientation.ts","webpack://opensafari-mcp/./src/qa/detectors/pwa-meta.ts","webpack://opensafari-mcp/./src/qa/audit.ts","webpack://opensafari-mcp/./src/qa/types.ts"],"sourcesContent":["import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectAutoZoom(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var inputs = document.querySelectorAll('input, select, textarea');\n var issues = [];\n inputs.forEach(function(el) {\n var style = window.getComputedStyle(el);\n var size = parseFloat(style.fontSize);\n if (size < 16) {\n var rect = el.getBoundingClientRect();\n issues.push({\n selector: el.id ? '#' + el.id : (el.name ? el.tagName.toLowerCase() + '[name=\"' + el.name + '\"]' : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : '')),\n problem: 'font-size is ' + size + 'px (< 16px minimum)',\n fix: 'Set font-size to at least 16px to prevent iOS Safari auto-zoom on focus',\n });\n }\n });\n return { detector: 'auto_zoom', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: inputs.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectTouchTargets(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var selectors = 'a, button, input, select, textarea, [onclick], [role=\"button\"], [role=\"link\"], [tabindex]:not([tabindex=\"-1\"])';\n var elements = document.querySelectorAll(selectors);\n var threshold = 44;\n var issues = [];\n elements.forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return;\n if (rect.width < threshold || rect.height < threshold) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Touch target is ' + Math.round(rect.width) + 'x' + Math.round(rect.height) + 'px (minimum: ' + threshold + 'x' + threshold + 'px)',\n fix: 'Increase element size to at least 44x44px or add padding',\n size: { width: Math.round(rect.width), height: Math.round(rect.height) },\n });\n }\n });\n return { detector: 'touch_targets', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: elements.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectHoverOnly(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var issues = [];\n try {\n Array.from(document.styleSheets).forEach(function(sheet) {\n try {\n Array.from(sheet.cssRules || []).forEach(function(rule) {\n if (rule.selectorText && rule.selectorText.indexOf(':hover') !== -1) {\n var style = rule.style;\n if (style.display || style.visibility || style.opacity) {\n var baseSelector = rule.selectorText.replace(/:hover/g, '').trim();\n var el = document.querySelector(baseSelector);\n if (el) {\n issues.push({\n selector: baseSelector,\n problem: ':hover changes visibility — inaccessible on touch devices',\n fix: 'Add click/touch handler or use :focus-within as alternative',\n cssRule: rule.selectorText,\n });\n }\n }\n }\n });\n } catch(e) {}\n });\n } catch(e) {}\n return { detector: 'hover_only', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 0, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectInputType(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var issues = [];\n var inputs = document.querySelectorAll('input');\n inputs.forEach(function(el) {\n var type = el.getAttribute('type') || 'text';\n var inputMode = el.getAttribute('inputmode');\n var name = (el.name || el.id || '').toLowerCase();\n if (type === 'text' && !inputMode) {\n if (name.match(/phone|tel|zip|postal|code|pin|otp|cvv|cvc/)) {\n issues.push({ selector: el.id ? '#' + el.id : 'input[name=\"' + el.name + '\"]', problem: 'Likely numeric field using type=\"text\" without inputmode', fix: 'Add inputmode=\"numeric\" or inputmode=\"tel\"' });\n }\n if (name.match(/email/) && type !== 'email') {\n issues.push({ selector: el.id ? '#' + el.id : 'input[name=\"' + el.name + '\"]', problem: 'Email field using type=\"text\"', fix: 'Use type=\"email\" for email keyboard' });\n }\n }\n });\n return { detector: 'input_type', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: inputs.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectSafeArea(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var issues = [];\n var viewport = document.querySelector('meta[name=\"viewport\"]');\n var hasViewportFitCover = viewport && viewport.content && viewport.content.indexOf('viewport-fit=cover') !== -1;\n if (!hasViewportFitCover) {\n return { detector: 'safe_area', severity: 'pass', issues: [], passed: true, totalScanned: 0, issueCount: 0, metadata: { note: 'viewport-fit=cover not set' } };\n }\n var all = document.querySelectorAll('*');\n all.forEach(function(el) {\n var style = window.getComputedStyle(el);\n if (style.position === 'fixed' || style.position === 'sticky') {\n var top = parseFloat(style.top);\n var bottom = parseFloat(style.bottom);\n if (top === 0 || bottom === 0) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Fixed element at ' + (top === 0 ? 'top' : 'bottom') + ' edge without safe-area-inset padding',\n fix: 'Add padding: env(safe-area-inset-' + (top === 0 ? 'top' : 'bottom') + ')',\n });\n }\n }\n });\n return { detector: 'safe_area', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 1, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectKeyboardOverlap(client: BrowserBackend): Promise<DetectorResult> {\n // Get fixed-bottom elements\n const fixedBottom = await client.evaluate<Array<{ selector: string; bottom: number; rect: { y: number; height: number } }>>(`\n (function() {\n return Array.from(document.querySelectorAll('*')).filter(function(el) {\n var s = window.getComputedStyle(el);\n return s.position === 'fixed' && parseFloat(s.bottom) < 50;\n }).map(function(el) {\n return {\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n bottom: parseFloat(window.getComputedStyle(el).bottom),\n rect: { y: el.getBoundingClientRect().y, height: el.getBoundingClientRect().height }\n };\n });\n })()\n `);\n\n if (!fixedBottom || fixedBottom.length === 0) {\n return { detector: 'keyboard_overlap', severity: 'pass', issues: [], passed: true, totalScanned: 0, issueCount: 0 };\n }\n\n // Get inputs\n const inputs = await client.evaluate<string[]>(`\n Array.from(document.querySelectorAll('input:not([type=\"hidden\"]), textarea, select')).slice(0, 5).map(function(el) {\n return el.id ? '#' + el.id : (el.name ? el.tagName.toLowerCase() + '[name=\"' + el.name + '\"]' : el.tagName.toLowerCase());\n })\n `);\n\n const issues: Array<{ selector: string; problem: string; fix: string; triggeredBy?: string }> = [];\n\n for (const inputSelector of (inputs || [])) {\n try {\n await client.click(inputSelector);\n await new Promise(r => setTimeout(r, 500));\n\n const viewportWithKeyboard = await client.evaluate<number>('window.visualViewport ? window.visualViewport.height : window.innerHeight');\n\n for (const fixed of fixedBottom) {\n if (fixed.rect.y + fixed.rect.height > viewportWithKeyboard) {\n issues.push({\n selector: fixed.selector,\n problem: `Fixed bottom element hidden behind keyboard (element bottom: ${Math.round(fixed.rect.y + fixed.rect.height)}px, viewport with keyboard: ${Math.round(viewportWithKeyboard)}px)`,\n fix: 'Use visualViewport API to adjust position when keyboard appears',\n triggeredBy: inputSelector,\n });\n }\n }\n\n await client.dismissKeyboard();\n await new Promise(r => setTimeout(r, 300));\n } catch {\n // Input may not be focusable\n }\n }\n\n return {\n detector: 'keyboard_overlap',\n severity: issues.length > 0 ? 'critical' : 'pass',\n issues,\n passed: issues.length === 0,\n totalScanned: fixedBottom.length * (inputs?.length ?? 0),\n issueCount: issues.length,\n };\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectHorizontalOverflow(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var docWidth = document.documentElement.scrollWidth;\n var vpWidth = window.innerWidth;\n if (docWidth <= vpWidth) {\n return { detector: 'horizontal_overflow', severity: 'pass', issues: [], passed: true, totalScanned: 1, issueCount: 0 };\n }\n var issues = [];\n function findCulprits(parent) {\n Array.from(parent.children).forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (rect.right > vpWidth + 1) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Element extends to ' + Math.round(rect.right) + 'px (viewport: ' + vpWidth + 'px)',\n fix: 'Add overflow-x: hidden or max-width: 100%',\n overflow: Math.round(rect.right - vpWidth) + 'px',\n });\n }\n });\n }\n findCulprits(document.body);\n return { detector: 'horizontal_overflow', severity: 'high', issues: issues.slice(0, 20), passed: false, totalScanned: 1, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detect100vh(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var temp = document.createElement('div');\n temp.style.cssText = 'position:fixed;top:0;height:100vh;width:1px;pointer-events:none;visibility:hidden';\n document.body.appendChild(temp);\n var vh100 = temp.offsetHeight;\n document.body.removeChild(temp);\n var innerH = window.innerHeight;\n var diff = vh100 - innerH;\n if (Math.abs(diff) < 10) {\n return { detector: '100vh', severity: 'pass', issues: [], passed: true, totalScanned: 1, issueCount: 0, metadata: { vh100: vh100, innerHeight: innerH, difference: diff } };\n }\n var issues = [];\n issues.push({\n selector: 'viewport',\n problem: '100vh = ' + vh100 + 'px but visible viewport = ' + innerH + 'px (diff: ' + diff + 'px)',\n fix: 'Use 100dvh or calc(var(--vh, 1vh) * 100) with JS viewport listener',\n });\n return { detector: '100vh', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 1, issueCount: issues.length, metadata: { vh100: vh100, innerHeight: innerH, difference: diff } };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectFixedStacking(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var fixedEls = Array.from(document.querySelectorAll('*')).filter(function(el) {\n var s = window.getComputedStyle(el);\n return s.position === 'fixed' || s.position === 'sticky';\n }).map(function(el) {\n return {\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n rect: el.getBoundingClientRect(),\n zIndex: parseInt(window.getComputedStyle(el).zIndex) || 0,\n };\n });\n var issues = [];\n for (var i = 0; i < fixedEls.length; i++) {\n for (var j = i + 1; j < fixedEls.length; j++) {\n var a = fixedEls[i], b = fixedEls[j];\n var overlap = !(a.rect.right < b.rect.left || a.rect.left > b.rect.right || a.rect.bottom < b.rect.top || a.rect.top > b.rect.bottom);\n if (overlap && a.zIndex === b.zIndex) {\n issues.push({\n selector: a.selector + ' <-> ' + b.selector,\n problem: 'Overlapping fixed elements with same z-index (' + a.zIndex + ')',\n fix: 'Set distinct z-index values',\n });\n }\n }\n }\n return { detector: 'fixed_stacking', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: fixedEls.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectScrollLock(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var issues = [];\n var bodyOverflow = document.body.style.overflow;\n var htmlOverflow = document.documentElement.style.overflow;\n var hasVisibleModal = document.querySelector('[role=\"dialog\"]:not([aria-hidden=\"true\"]), .modal:not(.hidden), [data-modal]:not([hidden])');\n if ((bodyOverflow === 'hidden' || htmlOverflow === 'hidden') && !hasVisibleModal) {\n issues.push({ selector: bodyOverflow === 'hidden' ? 'document.body' : 'document.documentElement', problem: 'overflow: hidden set but no visible modal found', fix: 'Ensure modal close handlers restore overflow' });\n }\n if (bodyOverflow === 'unset') {\n issues.push({ selector: 'document.body', problem: 'overflow set to \"unset\" — not a proper reset', fix: 'Use document.body.style.overflow = \"\" (empty string)' });\n }\n return { detector: 'scroll_lock', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 2, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\nimport { SimulatorManager } from '../../simulator/manager';\n\nexport async function detectDarkMode(client: BrowserBackend, simulator?: SimulatorManager, deviceId?: string): Promise<DetectorResult> {\n const colorScheme = await client.evaluate<string>(`\n (document.querySelector('meta[name=\"color-scheme\"]') || {}).content || 'not set'\n `);\n\n const issues: Array<{ selector: string; problem: string; fix: string }> = [];\n\n if (colorScheme === 'not set') {\n issues.push({\n selector: 'head',\n problem: 'No <meta name=\"color-scheme\"> tag — browser may apply forced dark mode',\n fix: 'Add <meta name=\"color-scheme\" content=\"light only\"> or implement proper dark mode',\n });\n }\n\n // Toggle dark mode if simulator available\n let lightScreenshot: string | undefined;\n let darkScreenshot: string | undefined;\n\n if (simulator && deviceId) {\n try {\n await simulator.setAppearance(deviceId, 'light');\n await new Promise(r => setTimeout(r, 500));\n const lightBuf = await client.screenshot();\n lightScreenshot = lightBuf.toString('base64');\n\n await simulator.setAppearance(deviceId, 'dark');\n await new Promise(r => setTimeout(r, 500));\n const darkBuf = await client.screenshot();\n darkScreenshot = darkBuf.toString('base64');\n\n await simulator.setAppearance(deviceId, 'light');\n } catch {\n // Simulator not available\n }\n }\n\n return {\n detector: 'dark_mode',\n severity: issues.length > 0 ? 'medium' : 'pass',\n issues,\n passed: issues.length === 0,\n totalScanned: 1,\n issueCount: issues.length,\n metadata: {\n colorScheme,\n ...(lightScreenshot ? { lightScreenshot, darkScreenshot, note: 'Compare screenshots visually' } : {}),\n },\n };\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\nimport { SimulatorManager } from '../../simulator/manager';\n\nexport async function detectOrientation(client: BrowserBackend, simulator?: SimulatorManager, deviceId?: string): Promise<DetectorResult> {\n const portraitMeta = await client.evaluate<{ scrollWidth: number; innerWidth: number; overflow: boolean }>(`({\n scrollWidth: document.documentElement.scrollWidth,\n innerWidth: window.innerWidth,\n overflow: document.documentElement.scrollWidth > window.innerWidth\n })`);\n\n const issues: Array<{ selector: string; problem: string; fix: string }> = [];\n\n // Try rotation\n if (simulator && deviceId) {\n try {\n await simulator.rotate(deviceId);\n await new Promise(r => setTimeout(r, 1000));\n\n const landscapeMeta = await client.evaluate<{ scrollWidth: number; innerWidth: number; overflow: boolean }>(`({\n scrollWidth: document.documentElement.scrollWidth,\n innerWidth: window.innerWidth,\n overflow: document.documentElement.scrollWidth > window.innerWidth\n })`);\n\n if (landscapeMeta.overflow) {\n issues.push({\n selector: 'document.documentElement',\n problem: `Horizontal overflow in landscape (scrollWidth: ${landscapeMeta.scrollWidth}px, viewport: ${landscapeMeta.innerWidth}px)`,\n fix: 'Ensure responsive layout handles landscape orientation',\n });\n }\n\n await simulator.rotate(deviceId); // rotate back\n } catch {\n // Rotation not available\n }\n }\n\n return {\n detector: 'orientation',\n severity: issues.length > 0 ? 'medium' : 'pass',\n issues,\n passed: issues.length === 0,\n totalScanned: 1,\n issueCount: issues.length,\n metadata: { portrait: portraitMeta },\n };\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectPwaMeta(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var checks = [\n { name: 'viewport', selector: 'meta[name=\"viewport\"]', required: true, fix: 'Add <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' },\n { name: 'theme-color', selector: 'meta[name=\"theme-color\"]', required: false, fix: 'Add <meta name=\"theme-color\" content=\"#yourColor\">' },\n { name: 'color-scheme', selector: 'meta[name=\"color-scheme\"]', required: false, fix: 'Add <meta name=\"color-scheme\" content=\"light only\">' },\n { name: 'apple-touch-icon', selector: 'link[rel=\"apple-touch-icon\"]', required: false, fix: 'Add <link rel=\"apple-touch-icon\" href=\"/icon-180.png\">' },\n { name: 'manifest', selector: 'link[rel=\"manifest\"]', required: false, fix: 'Add <link rel=\"manifest\" href=\"/manifest.json\">' },\n ];\n var issues = [];\n checks.forEach(function(check) {\n if (!document.querySelector(check.selector)) {\n issues.push({ selector: 'head', problem: 'Missing ' + check.name + (check.required ? ' (required)' : ' (recommended)'), fix: check.fix });\n }\n });\n return { detector: 'pwa_meta', severity: issues.some(function(i) { return i.problem.indexOf('required') !== -1; }) ? 'high' : issues.length > 0 ? 'low' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: checks.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../types/browser-backend';\nimport { DetectorResult, QAConfig, applyIgnoreRules } from './types';\nimport { detectAutoZoom } from './detectors/auto-zoom';\nimport { detectTouchTargets } from './detectors/touch-targets';\nimport { detectHoverOnly } from './detectors/hover-only';\nimport { detectInputType } from './detectors/input-type';\nimport { detectSafeArea } from './detectors/safe-area';\nimport { detectKeyboardOverlap } from './detectors/keyboard-overlap';\nimport { detectHorizontalOverflow } from './detectors/horizontal-overflow';\nimport { detect100vh } from './detectors/vh100';\nimport { detectFixedStacking } from './detectors/fixed-stacking';\nimport { detectScrollLock } from './detectors/scroll-lock';\nimport { detectDarkMode } from './detectors/dark-mode';\nimport { detectOrientation } from './detectors/orientation';\nimport { detectPwaMeta } from './detectors/pwa-meta';\nimport { SimulatorManager } from '../simulator/manager';\n\nexport interface AuditSummary {\n totalIssues: number;\n critical: number;\n high: number;\n medium: number;\n low: number;\n passed: number;\n failed: number;\n errors: number;\n}\n\nexport interface AuditReport {\n url: string;\n device: string;\n viewport: { w: number; h: number };\n timestamp: string;\n duration: number;\n score: number;\n summary: AuditSummary;\n detectors: DetectorResult[];\n}\n\nconst SEVERITY_WEIGHTS: Record<string, number> = {\n critical: 10,\n high: 5,\n medium: 2,\n low: 1,\n};\n\nexport class QAAudit {\n constructor(\n private client: BrowserBackend,\n private config: QAConfig = {},\n private simulator?: SimulatorManager,\n private deviceId?: string,\n private deviceInfo?: { name: string; w: number; h: number },\n ) {}\n\n async runFullAudit(url?: string): Promise<AuditReport> {\n if (url) await this.client.navigate({ url, waitUntil: 'load' });\n\n const currentUrl = await this.client.evaluate<string>('window.location.href');\n const startTime = Date.now();\n\n // Parallel: stateless detectors (10)\n const parallelResults = await Promise.allSettled([\n detectAutoZoom(this.client),\n detectTouchTargets(this.client),\n detectHoverOnly(this.client),\n detectInputType(this.client),\n detectSafeArea(this.client),\n detectHorizontalOverflow(this.client),\n detect100vh(this.client),\n detectFixedStacking(this.client),\n detectScrollLock(this.client),\n detectPwaMeta(this.client),\n ]);\n\n // Sequential: stateful detectors (3)\n const sequentialResults: PromiseSettledResult<DetectorResult>[] = [];\n try {\n sequentialResults.push({ status: 'fulfilled', value: await detectKeyboardOverlap(this.client) });\n } catch (e) {\n sequentialResults.push({ status: 'rejected', reason: e });\n }\n try {\n sequentialResults.push({ status: 'fulfilled', value: await detectDarkMode(this.client, this.simulator, this.deviceId) });\n } catch (e) {\n sequentialResults.push({ status: 'rejected', reason: e });\n }\n try {\n sequentialResults.push({ status: 'fulfilled', value: await detectOrientation(this.client, this.simulator, this.deviceId) });\n } catch (e) {\n sequentialResults.push({ status: 'rejected', reason: e });\n }\n\n // Combine + apply ignore rules\n const allSettled = [...parallelResults, ...sequentialResults];\n const allResults: DetectorResult[] = allSettled.map((r, _i) => {\n if (r.status === 'fulfilled') {\n return applyIgnoreRules(r.value, this.config);\n }\n return {\n detector: 'unknown',\n severity: 'error' as const,\n issues: [],\n passed: false,\n totalScanned: 0,\n issueCount: 0,\n error: r.reason instanceof Error ? r.reason.message : String(r.reason),\n };\n });\n\n const score = this.calculateScore(allResults);\n const summary = this.summarize(allResults);\n\n return {\n url: currentUrl,\n device: this.deviceInfo?.name ?? 'unknown',\n viewport: { w: this.deviceInfo?.w ?? 0, h: this.deviceInfo?.h ?? 0 },\n timestamp: new Date().toISOString(),\n duration: Date.now() - startTime,\n score,\n summary,\n detectors: allResults,\n };\n }\n\n private calculateScore(results: DetectorResult[]): number {\n let penalty = 0;\n for (const result of results) {\n if (result.severity && result.severity !== 'pass' && result.severity !== 'error') {\n penalty += (SEVERITY_WEIGHTS[result.severity] ?? 0) * result.issueCount;\n }\n }\n return Math.max(0, 100 - penalty);\n }\n\n private summarize(results: DetectorResult[]): AuditSummary {\n return {\n totalIssues: results.reduce((s, r) => s + r.issueCount, 0),\n critical: results.filter(r => r.severity === 'critical').reduce((s, r) => s + r.issueCount, 0),\n high: results.filter(r => r.severity === 'high').reduce((s, r) => s + r.issueCount, 0),\n medium: results.filter(r => r.severity === 'medium').reduce((s, r) => s + r.issueCount, 0),\n low: results.filter(r => r.severity === 'low').reduce((s, r) => s + r.issueCount, 0),\n passed: results.filter(r => r.passed).length,\n failed: results.filter(r => !r.passed).length,\n errors: results.filter(r => r.severity === 'error').length,\n };\n }\n}\n","export interface DetectorIssue {\n selector: string;\n element?: string;\n problem: string;\n fix: string;\n [key: string]: unknown;\n}\n\nexport interface DetectorResult {\n detector: string;\n severity: 'critical' | 'high' | 'medium' | 'low' | 'pass' | 'error';\n issues: DetectorIssue[];\n passed: boolean;\n totalScanned: number;\n issueCount: number;\n metadata?: Record<string, unknown>;\n error?: string;\n}\n\nexport interface QAConfig {\n thresholds?: {\n touchTargetMinSize?: number;\n inputMinFontSize?: number;\n };\n ignore?: Array<{\n detector: string;\n selector: string;\n }>;\n}\n\nexport function applyIgnoreRules(result: DetectorResult, config: QAConfig): DetectorResult {\n const ignores = config.ignore?.filter(r => r.detector === result.detector) ?? [];\n if (ignores.length > 0) {\n result.issues = result.issues.filter(issue =>\n !ignores.some(ign => issue.selector.includes(ign.selector))\n );\n result.issueCount = result.issues.length;\n result.passed = result.issueCount === 0;\n if (result.passed) result.severity = 'pass';\n }\n return result;\n}\n"],"names":["async","detectAutoZoom","client","evaluate","detectTouchTargets","detectHoverOnly","detectInputType","detectSafeArea","detectKeyboardOverlap","fixedBottom","length","detector","severity","issues","passed","totalScanned","issueCount","inputs","inputSelector","click","Promise","r","setTimeout","viewportWithKeyboard","fixed","rect","y","height","push","selector","problem","Math","round","fix","triggeredBy","dismissKeyboard","detectHorizontalOverflow","detect100vh","detectFixedStacking","detectScrollLock","detectDarkMode","simulator","deviceId","colorScheme","lightScreenshot","darkScreenshot","setAppearance","screenshot","toString","metadata","note","detectOrientation","portraitMeta","rotate","landscapeMeta","overflow","scrollWidth","innerWidth","portrait","detectPwaMeta","SEVERITY_WEIGHTS","critical","high","medium","low","QAAudit","config","deviceInfo","constructor","runFullAudit","url","this","navigate","waitUntil","currentUrl","startTime","Date","now","parallelResults","allSettled","sequentialResults","status","value","e","reason","allResults","map","_i","result","ignores","ignore","filter","issue","some","ign","includes","applyIgnoreRules","error","Error","message","String","score","calculateScore","summary","summarize","device","name","viewport","w","h","timestamp","toISOString","duration","detectors","results","penalty","max","totalIssues","reduce","s","failed","errors"],"sourceRoot":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";exports.id=838,exports.ids=[838],exports.modules={838(s,e,r){r.d(e,{StdioTransport:()=>o});var t=r(785),n=r(462);class o{rl=null;messageHandler=null;onMessage(s){this.messageHandler=s}send(s){process.stdout.write(JSON.stringify(s)+"\n")}start(){this.rl=t.createInterface({input:process.stdin,terminal:!1}),this.rl.on("line",s=>{if(!s.trim())return;let e;try{e=JSON.parse(s)}catch(s){const e={jsonrpc:"2.0",id:0,error:{code:n.D.PARSE_ERROR,message:s instanceof Error?s.message:"Parse error"}};return void this.send(e)}this.messageHandler?this.messageHandler(e).then(s=>{s&&this.send(s)}).catch(s=>{const r={jsonrpc:"2.0",id:e.id??0,error:{code:n.D.INTERNAL_ERROR,message:s instanceof Error?s.message:"Internal error"}};this.send(r)}):console.error("[StdioTransport] No message handler registered, dropping message")}),this.rl.on("close",()=>{console.error("[StdioTransport] stdin closed, shutting down..."),process.exit(0)})}async close(){this.rl&&(this.rl.close(),this.rl=null)}}}};
|
|
2
|
+
//# sourceMappingURL=838.index.js.map
|