rlint 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -0
- package/dist/browser.d.ts +8 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +122 -0
- package/dist/browser.js.map +1 -0
- package/dist/checks/clickability.d.ts +3 -0
- package/dist/checks/clickability.d.ts.map +1 -0
- package/dist/checks/clickability.js +125 -0
- package/dist/checks/clickability.js.map +1 -0
- package/dist/checks/index.d.ts +9 -0
- package/dist/checks/index.d.ts.map +1 -0
- package/dist/checks/index.js +27 -0
- package/dist/checks/index.js.map +1 -0
- package/dist/checks/overflow.d.ts +3 -0
- package/dist/checks/overflow.d.ts.map +1 -0
- package/dist/checks/overflow.js +107 -0
- package/dist/checks/overflow.js.map +1 -0
- package/dist/checks/text-overflow.d.ts +3 -0
- package/dist/checks/text-overflow.d.ts.map +1 -0
- package/dist/checks/text-overflow.js +136 -0
- package/dist/checks/text-overflow.js.map +1 -0
- package/dist/checks/touch-targets.d.ts +3 -0
- package/dist/checks/touch-targets.d.ts.map +1 -0
- package/dist/checks/touch-targets.js +118 -0
- package/dist/checks/touch-targets.js.map +1 -0
- package/dist/checks/visibility.d.ts +3 -0
- package/dist/checks/visibility.d.ts.map +1 -0
- package/dist/checks/visibility.js +132 -0
- package/dist/checks/visibility.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +270 -0
- package/dist/cli.js.map +1 -0
- package/dist/frameworks/detector.d.ts +19 -0
- package/dist/frameworks/detector.d.ts.map +1 -0
- package/dist/frameworks/detector.js +132 -0
- package/dist/frameworks/detector.js.map +1 -0
- package/dist/frameworks/index.d.ts +44 -0
- package/dist/frameworks/index.d.ts.map +1 -0
- package/dist/frameworks/index.js +138 -0
- package/dist/frameworks/index.js.map +1 -0
- package/dist/frameworks/next.d.ts +34 -0
- package/dist/frameworks/next.d.ts.map +1 -0
- package/dist/frameworks/next.js +160 -0
- package/dist/frameworks/next.js.map +1 -0
- package/dist/frameworks/sveltekit.d.ts +34 -0
- package/dist/frameworks/sveltekit.d.ts.map +1 -0
- package/dist/frameworks/sveltekit.js +150 -0
- package/dist/frameworks/sveltekit.js.map +1 -0
- package/dist/frameworks/vite.d.ts +40 -0
- package/dist/frameworks/vite.d.ts.map +1 -0
- package/dist/frameworks/vite.js +211 -0
- package/dist/frameworks/vite.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +402 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/reporter.d.ts +4 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +103 -0
- package/dist/reporter.js.map +1 -0
- package/dist/runner.d.ts +8 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +63 -0
- package/dist/runner.js.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# rlint
|
|
2
|
+
|
|
3
|
+
**Catch rendered layout bugs automatically — no screenshots needed.**
|
|
4
|
+
|
|
5
|
+
Like ESLint, but for your *rendered* UI. Renderlint detects horizontal overflow, covered buttons, tiny touch targets, and other structural bugs that slip through code review.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
rlint check http://localhost:3000
|
|
9
|
+
|
|
10
|
+
❌ OVERFLOW Horizontal overflow detected (page scrolls 740px beyond viewport)
|
|
11
|
+
└─ Element: div.hero-banner
|
|
12
|
+
└─ Fix: Add overflow-x: hidden or check for elements with fixed widths
|
|
13
|
+
|
|
14
|
+
❌ CLICKABILITY Interactive element is covered by another element
|
|
15
|
+
└─ Element: button.submit-btn
|
|
16
|
+
└─ Covered by: div.modal-backdrop
|
|
17
|
+
└─ Fix: Check z-index or remove covering element
|
|
18
|
+
|
|
19
|
+
⚠️ TOUCH-TARGETS Touch target too small: 32x28px (min: 44x44px)
|
|
20
|
+
└─ Element: a.nav-link
|
|
21
|
+
└─ Fix: Add min-width: 44px and min-height: 44px
|
|
22
|
+
|
|
23
|
+
──────────────────────────────────────────────────
|
|
24
|
+
Results: 2 errors, 1 warning, 47 passed
|
|
25
|
+
──────────────────────────────────────────────────
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## The Problem
|
|
29
|
+
|
|
30
|
+
Layout bugs are invisible in code review. Your PR looks fine, tests pass, but then:
|
|
31
|
+
- The page scrolls horizontally on mobile
|
|
32
|
+
- A modal backdrop covers your buttons
|
|
33
|
+
- Touch targets are too small for actual fingers
|
|
34
|
+
|
|
35
|
+
These aren't styling issues — they're **structural bugs** that can be detected programmatically using `getBoundingClientRect()`, `getComputedStyle()`, and `elementFromPoint()`.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install rlint
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
No extra setup. Renderlint uses your system Chrome — no 150MB Chromium download. If Chrome isn't installed, Chromium is downloaded automatically on first run.
|
|
44
|
+
|
|
45
|
+
> **Note:** The first run may be slower if Chromium needs to be downloaded (~150MB). Subsequent runs use the cached browser.
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Check any URL
|
|
51
|
+
npx rlint check https://example.com
|
|
52
|
+
|
|
53
|
+
# Check your dev server
|
|
54
|
+
npx rlint check http://localhost:3000
|
|
55
|
+
|
|
56
|
+
# Check multiple viewports (mobile + desktop)
|
|
57
|
+
npx rlint check --viewport 375x667,1920x1080 http://localhost:3000
|
|
58
|
+
|
|
59
|
+
# Auto-detect framework and start dev server
|
|
60
|
+
npx rlint dev
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Checks
|
|
64
|
+
|
|
65
|
+
| Check | Severity | What it catches |
|
|
66
|
+
|-------|----------|-----------------|
|
|
67
|
+
| `overflow` | error | Horizontal scrollbars from content wider than viewport |
|
|
68
|
+
| `clickability` | error | Buttons/links covered by other elements (z-index bugs) |
|
|
69
|
+
| `touch-targets` | warning | Elements smaller than 44×44px (WCAG 2.5.5) |
|
|
70
|
+
| `visibility` | warning | Interactive elements that are invisible or off-screen |
|
|
71
|
+
| `text-overflow` | warning | Text clipped without proper ellipsis handling |
|
|
72
|
+
|
|
73
|
+
## Framework Support
|
|
74
|
+
|
|
75
|
+
Renderlint auto-detects your framework and handles hydration:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Auto-detect and start dev server
|
|
79
|
+
npx rlint dev --routes /,/about,/contact
|
|
80
|
+
|
|
81
|
+
# Specify framework manually
|
|
82
|
+
npx rlint dev --framework nextjs --routes /,/api/health
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Supported: **Next.js**, **SvelteKit**, **Vite**, **Remix**, **Astro**, **Nuxt**, **Create React App**
|
|
86
|
+
|
|
87
|
+
## Library Usage
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { checkPage } from 'rlint';
|
|
91
|
+
import { launchBrowser } from 'rlint/browser';
|
|
92
|
+
|
|
93
|
+
const browser = await launchBrowser();
|
|
94
|
+
const page = await browser.newPage();
|
|
95
|
+
await page.goto('http://localhost:3000');
|
|
96
|
+
|
|
97
|
+
const results = await checkPage(page);
|
|
98
|
+
console.log(results.summary);
|
|
99
|
+
// { passed: 47, errors: 1, warnings: 2 }
|
|
100
|
+
|
|
101
|
+
await browser.close();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## MCP Server (for AI Agents)
|
|
105
|
+
|
|
106
|
+
Renderlint includes an MCP server for integration with Claude Code and other AI tools:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"rlint": {
|
|
112
|
+
"command": "npx",
|
|
113
|
+
"args": ["rlint-mcp"]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Available tools:
|
|
120
|
+
- `check_page` — Check a URL for layout issues
|
|
121
|
+
- `check_html` — Check raw HTML content
|
|
122
|
+
- `check_file` — Check a local HTML file
|
|
123
|
+
- `screenshot` — Take a screenshot (returns image for visual debugging)
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
// rlint.config.js
|
|
129
|
+
export default {
|
|
130
|
+
checks: {
|
|
131
|
+
overflow: { horizontal: true },
|
|
132
|
+
touchTargets: { minWidth: 44, minHeight: 44 },
|
|
133
|
+
clickability: { checkCorners: true },
|
|
134
|
+
textOverflow: { allowEllipsis: true },
|
|
135
|
+
},
|
|
136
|
+
ignore: ['.tooltip', '[data-rlint-ignore]'],
|
|
137
|
+
};
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Ignoring Elements
|
|
141
|
+
|
|
142
|
+
```html
|
|
143
|
+
<!-- Skip specific elements -->
|
|
144
|
+
<input type="checkbox" data-rlint-ignore>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## CI Integration
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
# .github/workflows/rlint.yml
|
|
151
|
+
name: Renderlint Check
|
|
152
|
+
on: [push, pull_request]
|
|
153
|
+
|
|
154
|
+
jobs:
|
|
155
|
+
check:
|
|
156
|
+
runs-on: ubuntu-latest
|
|
157
|
+
steps:
|
|
158
|
+
- uses: actions/checkout@v4
|
|
159
|
+
- uses: actions/setup-node@v4
|
|
160
|
+
- run: npm ci
|
|
161
|
+
- run: npm run build
|
|
162
|
+
- run: npm start & npx wait-on http://localhost:3000
|
|
163
|
+
- run: npx rlint check --fail-on warning http://localhost:3000
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
> **Tip:** Renderlint uses system Chrome. If unavailable, Chromium is downloaded to `~/.cache/rlint` on first run. Cache this directory in CI for faster builds.
|
|
167
|
+
|
|
168
|
+
## CLI Reference
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
rlint check <urls...>
|
|
172
|
+
-v, --viewport <size> Viewport size (e.g., 375x667,1920x1080)
|
|
173
|
+
-f, --format <format> Output: text, json, junit
|
|
174
|
+
-o, --only <checks> Run specific checks only
|
|
175
|
+
-i, --ignore <selectors> Ignore matching elements
|
|
176
|
+
-c, --config <path> Config file path
|
|
177
|
+
--fail-on <severity> Exit code 1 on: error, warning
|
|
178
|
+
--headed Show browser window
|
|
179
|
+
--wait-for-hydration Wait for SPA hydration
|
|
180
|
+
|
|
181
|
+
rlint dev
|
|
182
|
+
-r, --routes <routes> Routes to check (default: /)
|
|
183
|
+
-p, --port <port> Dev server port
|
|
184
|
+
--framework <name> Framework override
|
|
185
|
+
--no-start-server Use existing dev server
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## How It Works
|
|
189
|
+
|
|
190
|
+
Renderlint doesn't compare screenshots. Instead, it queries the DOM:
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
// Overflow detection
|
|
194
|
+
document.documentElement.scrollWidth > document.documentElement.clientWidth
|
|
195
|
+
|
|
196
|
+
// Covered element detection
|
|
197
|
+
document.elementFromPoint(x, y) !== expectedElement
|
|
198
|
+
|
|
199
|
+
// Touch target detection
|
|
200
|
+
element.getBoundingClientRect().width < 44
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
These checks are deterministic, fast, and don't require human review.
|
|
204
|
+
|
|
205
|
+
## Why Not Visual Regression?
|
|
206
|
+
|
|
207
|
+
Visual regression (screenshot comparison) catches everything but requires human review for every change. Renderlint catches **structural bugs** that are objectively wrong:
|
|
208
|
+
|
|
209
|
+
| Visual Regression | Renderlint |
|
|
210
|
+
|-------------------|------------|
|
|
211
|
+
| Catches color changes | Catches layout bugs |
|
|
212
|
+
| Requires baseline images | No baselines needed |
|
|
213
|
+
| Needs human review | Fully automated |
|
|
214
|
+
| Slow (image comparison) | Fast (DOM queries) |
|
|
215
|
+
|
|
216
|
+
Use both! Visual regression for design fidelity, Renderlint for structural correctness.
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Browser } from 'puppeteer-core';
|
|
2
|
+
/**
|
|
3
|
+
* Launch browser - tries system Chrome first, falls back to download.
|
|
4
|
+
*/
|
|
5
|
+
export declare function launchBrowser(options?: {
|
|
6
|
+
headless?: boolean;
|
|
7
|
+
}): Promise<Browser>;
|
|
8
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAkB,EAAE,KAAK,OAAO,EAAE,MAAM,gBAAgB,CAAC;AA0GzD;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CA4BtF"}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import puppeteer from 'puppeteer-core';
|
|
2
|
+
import * as ChromeLauncher from 'chrome-launcher';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
/**
|
|
9
|
+
* Find Chrome/Chromium on the system using chrome-launcher.
|
|
10
|
+
* Returns the executable path or null.
|
|
11
|
+
*/
|
|
12
|
+
function findSystemChrome() {
|
|
13
|
+
try {
|
|
14
|
+
const installations = ChromeLauncher.Launcher.getInstallations();
|
|
15
|
+
if (installations.length > 0) {
|
|
16
|
+
return installations[0];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get the rlint cache directory for downloaded browsers
|
|
24
|
+
*/
|
|
25
|
+
function getCacheDir() {
|
|
26
|
+
return join(homedir(), '.cache', 'rlint');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Try to find a previously downloaded Chromium in our cache
|
|
30
|
+
*/
|
|
31
|
+
function findCachedChromium() {
|
|
32
|
+
const cacheDir = getCacheDir();
|
|
33
|
+
// Check common locations where puppeteer/browsers stores Chrome
|
|
34
|
+
const possiblePaths = [
|
|
35
|
+
join(cacheDir, 'chrome', '**', 'chrome'),
|
|
36
|
+
join(cacheDir, 'chrome', '**', 'Google Chrome for Testing'),
|
|
37
|
+
];
|
|
38
|
+
// Simple check: look for chrome-for-testing marker file
|
|
39
|
+
const markerFile = join(cacheDir, '.chromium-path');
|
|
40
|
+
if (existsSync(markerFile)) {
|
|
41
|
+
const cachedPath = require('fs').readFileSync(markerFile, 'utf-8').trim();
|
|
42
|
+
if (existsSync(cachedPath)) {
|
|
43
|
+
return cachedPath;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Download Chromium as a fallback when no system Chrome is found.
|
|
50
|
+
* Uses @puppeteer/browsers for the download.
|
|
51
|
+
*/
|
|
52
|
+
async function downloadChromium() {
|
|
53
|
+
console.log(chalk.yellow('No Chrome installation found.'));
|
|
54
|
+
console.log(chalk.yellow('Downloading Chromium (~150MB). This is a one-time setup...'));
|
|
55
|
+
try {
|
|
56
|
+
// Dynamic import to avoid requiring @puppeteer/browsers unless needed
|
|
57
|
+
const { install, Browser, resolveBuildId, detectBrowserPlatform } = await import('@puppeteer/browsers');
|
|
58
|
+
const platform = detectBrowserPlatform();
|
|
59
|
+
if (!platform) {
|
|
60
|
+
throw new Error('Could not detect browser platform');
|
|
61
|
+
}
|
|
62
|
+
const buildId = await resolveBuildId(Browser.CHROMIUM, platform, 'latest');
|
|
63
|
+
const cacheDir = getCacheDir();
|
|
64
|
+
const result = await install({
|
|
65
|
+
browser: Browser.CHROMIUM,
|
|
66
|
+
buildId,
|
|
67
|
+
cacheDir,
|
|
68
|
+
});
|
|
69
|
+
// Cache the path for future runs
|
|
70
|
+
const markerFile = join(cacheDir, '.chromium-path');
|
|
71
|
+
require('fs').writeFileSync(markerFile, result.executablePath);
|
|
72
|
+
console.log(chalk.green('Chromium downloaded successfully.'));
|
|
73
|
+
return result.executablePath;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// Fallback: try npx
|
|
77
|
+
console.log(chalk.dim('Trying npx fallback...'));
|
|
78
|
+
try {
|
|
79
|
+
execSync('npx @puppeteer/browsers install chromium@latest --path ' + getCacheDir(), {
|
|
80
|
+
stdio: 'inherit',
|
|
81
|
+
});
|
|
82
|
+
// Try to find the installed binary
|
|
83
|
+
const cachedPath = findCachedChromium();
|
|
84
|
+
if (cachedPath) {
|
|
85
|
+
return cachedPath;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
console.error(chalk.red('Failed to download Chromium.'));
|
|
90
|
+
console.error(chalk.dim('Please install Chrome or Chromium manually.'));
|
|
91
|
+
throw new Error('No browser available. Install Chrome: https://google.com/chrome');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Launch browser - tries system Chrome first, falls back to download.
|
|
96
|
+
*/
|
|
97
|
+
export async function launchBrowser(options) {
|
|
98
|
+
const headless = options?.headless ?? true;
|
|
99
|
+
// 1. Try system Chrome
|
|
100
|
+
let executablePath = findSystemChrome();
|
|
101
|
+
if (executablePath) {
|
|
102
|
+
return puppeteer.launch({
|
|
103
|
+
executablePath,
|
|
104
|
+
headless,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// 2. Try cached Chromium
|
|
108
|
+
executablePath = findCachedChromium();
|
|
109
|
+
if (executablePath) {
|
|
110
|
+
return puppeteer.launch({
|
|
111
|
+
executablePath,
|
|
112
|
+
headless,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// 3. Download Chromium
|
|
116
|
+
executablePath = await downloadChromium();
|
|
117
|
+
return puppeteer.launch({
|
|
118
|
+
executablePath,
|
|
119
|
+
headless,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clickability.d.ts","sourceRoot":"","sources":["../../src/checks/clickability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAuB,MAAM,aAAa,CAAC;AAuB9D,eAAO,MAAM,iBAAiB,EAAE,KA6I/B,CAAC"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export const clickabilityCheck = {
|
|
2
|
+
name: 'clickability',
|
|
3
|
+
description: 'Detects interactive elements that are covered by other elements',
|
|
4
|
+
async run(ctx) {
|
|
5
|
+
const { page, config } = ctx;
|
|
6
|
+
const checkConfig = typeof config.checks?.clickability === 'object'
|
|
7
|
+
? config.checks.clickability
|
|
8
|
+
: {};
|
|
9
|
+
const selectors = checkConfig.selectors || [
|
|
10
|
+
'button',
|
|
11
|
+
'a',
|
|
12
|
+
'input',
|
|
13
|
+
'select',
|
|
14
|
+
'textarea',
|
|
15
|
+
'[role="button"]',
|
|
16
|
+
'[role="link"]',
|
|
17
|
+
'[onclick]',
|
|
18
|
+
'[tabindex="0"]',
|
|
19
|
+
];
|
|
20
|
+
const ignoreSelectors = [
|
|
21
|
+
...(config.ignore || []),
|
|
22
|
+
...(checkConfig.ignore || []),
|
|
23
|
+
'[data-rlint-ignore]',
|
|
24
|
+
];
|
|
25
|
+
const coveredElements = await page.evaluate(({ selectors, ignoreSelectors }) => {
|
|
26
|
+
const results = [];
|
|
27
|
+
const selectorString = selectors.join(', ');
|
|
28
|
+
const elements = document.querySelectorAll(selectorString);
|
|
29
|
+
for (const el of elements) {
|
|
30
|
+
// Skip ignored elements
|
|
31
|
+
const shouldIgnore = ignoreSelectors.some(sel => {
|
|
32
|
+
try {
|
|
33
|
+
return el.matches(sel);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
if (shouldIgnore)
|
|
40
|
+
continue;
|
|
41
|
+
// Skip hidden elements
|
|
42
|
+
const style = getComputedStyle(el);
|
|
43
|
+
if (style.display === 'none' ||
|
|
44
|
+
style.visibility === 'hidden' ||
|
|
45
|
+
style.opacity === '0') {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const rect = el.getBoundingClientRect();
|
|
49
|
+
// Skip zero-size elements
|
|
50
|
+
if (rect.width === 0 || rect.height === 0)
|
|
51
|
+
continue;
|
|
52
|
+
// Skip off-screen elements
|
|
53
|
+
if (rect.right < 0 ||
|
|
54
|
+
rect.bottom < 0 ||
|
|
55
|
+
rect.left > window.innerWidth ||
|
|
56
|
+
rect.top > window.innerHeight) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Check center point
|
|
60
|
+
const centerX = rect.left + rect.width / 2;
|
|
61
|
+
const centerY = rect.top + rect.height / 2;
|
|
62
|
+
const topElement = document.elementFromPoint(centerX, centerY);
|
|
63
|
+
if (topElement && topElement !== el && !el.contains(topElement) && !topElement.contains(el)) {
|
|
64
|
+
// Element is covered!
|
|
65
|
+
let selector = el.tagName.toLowerCase();
|
|
66
|
+
if (el.id)
|
|
67
|
+
selector += `#${el.id}`;
|
|
68
|
+
if (el.className && typeof el.className === 'string') {
|
|
69
|
+
selector += '.' + el.className.trim().split(/\s+/).join('.');
|
|
70
|
+
}
|
|
71
|
+
let coverSelector = topElement.tagName.toLowerCase();
|
|
72
|
+
if (topElement.id)
|
|
73
|
+
coverSelector += `#${topElement.id}`;
|
|
74
|
+
if (topElement.className && typeof topElement.className === 'string') {
|
|
75
|
+
coverSelector += '.' + topElement.className.trim().split(/\s+/).join('.');
|
|
76
|
+
}
|
|
77
|
+
results.push({
|
|
78
|
+
selector,
|
|
79
|
+
tagName: el.tagName,
|
|
80
|
+
className: typeof el.className === 'string' ? el.className : '',
|
|
81
|
+
id: el.id || null,
|
|
82
|
+
textContent: el.textContent?.slice(0, 50)?.trim() || null,
|
|
83
|
+
rect: {
|
|
84
|
+
width: rect.width,
|
|
85
|
+
height: rect.height,
|
|
86
|
+
left: rect.left,
|
|
87
|
+
top: rect.top,
|
|
88
|
+
right: rect.right,
|
|
89
|
+
bottom: rect.bottom,
|
|
90
|
+
},
|
|
91
|
+
coveredBy: {
|
|
92
|
+
selector: coverSelector,
|
|
93
|
+
tagName: topElement.tagName,
|
|
94
|
+
className: typeof topElement.className === 'string' ? topElement.className : '',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}, { selectors, ignoreSelectors });
|
|
101
|
+
const issues = coveredElements.map(el => ({
|
|
102
|
+
check: 'clickability',
|
|
103
|
+
severity: 'error',
|
|
104
|
+
message: `Interactive element is covered by another element`,
|
|
105
|
+
element: {
|
|
106
|
+
selector: el.selector,
|
|
107
|
+
tagName: el.tagName,
|
|
108
|
+
className: el.className,
|
|
109
|
+
id: el.id,
|
|
110
|
+
textContent: el.textContent,
|
|
111
|
+
rect: el.rect,
|
|
112
|
+
},
|
|
113
|
+
details: {
|
|
114
|
+
coveredBy: el.coveredBy.selector,
|
|
115
|
+
},
|
|
116
|
+
fixHint: `Check z-index of ${el.selector} and ${el.coveredBy.selector}, or remove/reposition the covering element`,
|
|
117
|
+
}));
|
|
118
|
+
return {
|
|
119
|
+
check: 'clickability',
|
|
120
|
+
passed: Math.max(0, (await page.$$(selectors.join(', '))).length - issues.length),
|
|
121
|
+
issues,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=clickability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clickability.js","sourceRoot":"","sources":["../../src/checks/clickability.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { overflowCheck } from './overflow.js';
|
|
2
|
+
export { clickabilityCheck } from './clickability.js';
|
|
3
|
+
export { touchTargetsCheck } from './touch-targets.js';
|
|
4
|
+
export { visibilityCheck } from './visibility.js';
|
|
5
|
+
export { textOverflowCheck } from './text-overflow.js';
|
|
6
|
+
import type { Check } from '../types.js';
|
|
7
|
+
export declare const allChecks: Check[];
|
|
8
|
+
export declare const checksByName: Record<string, Check>;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAOvD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,eAAO,MAAM,SAAS,EAAE,KAAK,EAM5B,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAQ9C,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { overflowCheck } from './overflow.js';
|
|
2
|
+
export { clickabilityCheck } from './clickability.js';
|
|
3
|
+
export { touchTargetsCheck } from './touch-targets.js';
|
|
4
|
+
export { visibilityCheck } from './visibility.js';
|
|
5
|
+
export { textOverflowCheck } from './text-overflow.js';
|
|
6
|
+
import { overflowCheck } from './overflow.js';
|
|
7
|
+
import { clickabilityCheck } from './clickability.js';
|
|
8
|
+
import { touchTargetsCheck } from './touch-targets.js';
|
|
9
|
+
import { visibilityCheck } from './visibility.js';
|
|
10
|
+
import { textOverflowCheck } from './text-overflow.js';
|
|
11
|
+
export const allChecks = [
|
|
12
|
+
overflowCheck,
|
|
13
|
+
clickabilityCheck,
|
|
14
|
+
touchTargetsCheck,
|
|
15
|
+
visibilityCheck,
|
|
16
|
+
textOverflowCheck,
|
|
17
|
+
];
|
|
18
|
+
export const checksByName = {
|
|
19
|
+
overflow: overflowCheck,
|
|
20
|
+
clickability: clickabilityCheck,
|
|
21
|
+
'touch-targets': touchTargetsCheck,
|
|
22
|
+
touchTargets: touchTargetsCheck,
|
|
23
|
+
visibility: visibilityCheck,
|
|
24
|
+
'text-overflow': textOverflowCheck,
|
|
25
|
+
textOverflow: textOverflowCheck,
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAGvD,MAAM,CAAC,MAAM,SAAS,GAAY;IAChC,aAAa;IACb,iBAAiB;IACjB,iBAAiB;IACjB,eAAe;IACf,iBAAiB;CAClB,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAA0B;IACjD,QAAQ,EAAE,aAAa;IACvB,YAAY,EAAE,iBAAiB;IAC/B,eAAe,EAAE,iBAAiB;IAClC,YAAY,EAAE,iBAAiB;IAC/B,UAAU,EAAE,eAAe;IAC3B,eAAe,EAAE,iBAAiB;IAClC,YAAY,EAAE,iBAAiB;CAChC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overflow.d.ts","sourceRoot":"","sources":["../../src/checks/overflow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAoC,MAAM,aAAa,CAAC;AA0B3E,eAAO,MAAM,aAAa,EAAE,KAsH3B,CAAC"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export const overflowCheck = {
|
|
2
|
+
name: 'overflow',
|
|
3
|
+
description: 'Detects horizontal overflow causing unwanted scrollbars',
|
|
4
|
+
async run(ctx) {
|
|
5
|
+
const { page, config } = ctx;
|
|
6
|
+
const checkConfig = typeof config.checks?.overflow === 'object'
|
|
7
|
+
? config.checks.overflow
|
|
8
|
+
: {};
|
|
9
|
+
const ignoreSelectors = [
|
|
10
|
+
...(config.ignore || []),
|
|
11
|
+
...(checkConfig.ignore || []),
|
|
12
|
+
'[data-rlint-ignore]',
|
|
13
|
+
];
|
|
14
|
+
const data = await page.evaluate((ignoreSelectors) => {
|
|
15
|
+
const viewportWidth = window.innerWidth;
|
|
16
|
+
const documentScrollWidth = document.documentElement.scrollWidth;
|
|
17
|
+
const hasHorizontalOverflow = documentScrollWidth > viewportWidth;
|
|
18
|
+
const culprits = [];
|
|
19
|
+
if (hasHorizontalOverflow) {
|
|
20
|
+
// Find elements that extend beyond viewport
|
|
21
|
+
const allElements = document.querySelectorAll('*');
|
|
22
|
+
for (const el of allElements) {
|
|
23
|
+
// Skip ignored elements
|
|
24
|
+
const shouldIgnore = ignoreSelectors.some(sel => {
|
|
25
|
+
try {
|
|
26
|
+
return el.matches(sel);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
if (shouldIgnore)
|
|
33
|
+
continue;
|
|
34
|
+
const rect = el.getBoundingClientRect();
|
|
35
|
+
// Check if element extends beyond viewport
|
|
36
|
+
if (rect.right > viewportWidth + 1) {
|
|
37
|
+
const overflowAmount = rect.right - viewportWidth;
|
|
38
|
+
// Build a useful selector
|
|
39
|
+
let selector = el.tagName.toLowerCase();
|
|
40
|
+
if (el.id)
|
|
41
|
+
selector += `#${el.id}`;
|
|
42
|
+
if (el.className && typeof el.className === 'string') {
|
|
43
|
+
selector += '.' + el.className.trim().split(/\s+/).join('.');
|
|
44
|
+
}
|
|
45
|
+
culprits.push({
|
|
46
|
+
selector,
|
|
47
|
+
tagName: el.tagName,
|
|
48
|
+
className: typeof el.className === 'string' ? el.className : '',
|
|
49
|
+
id: el.id || null,
|
|
50
|
+
textContent: el.textContent?.slice(0, 50) || null,
|
|
51
|
+
rect: {
|
|
52
|
+
width: rect.width,
|
|
53
|
+
height: rect.height,
|
|
54
|
+
left: rect.left,
|
|
55
|
+
top: rect.top,
|
|
56
|
+
right: rect.right,
|
|
57
|
+
bottom: rect.bottom,
|
|
58
|
+
},
|
|
59
|
+
scrollWidth: el.scrollWidth,
|
|
60
|
+
clientWidth: el.clientWidth,
|
|
61
|
+
overflowAmount,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Sort by overflow amount (worst first) and dedupe by keeping worst ancestor
|
|
66
|
+
culprits.sort((a, b) => b.overflowAmount - a.overflowAmount);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
hasHorizontalOverflow,
|
|
70
|
+
documentScrollWidth,
|
|
71
|
+
viewportWidth,
|
|
72
|
+
culprits: culprits.slice(0, 10), // Limit to top 10
|
|
73
|
+
};
|
|
74
|
+
}, ignoreSelectors);
|
|
75
|
+
const issues = [];
|
|
76
|
+
if (data.hasHorizontalOverflow && data.culprits.length > 0) {
|
|
77
|
+
// Report the main culprit (usually the root cause)
|
|
78
|
+
const mainCulprit = data.culprits[0];
|
|
79
|
+
issues.push({
|
|
80
|
+
check: 'overflow',
|
|
81
|
+
severity: 'error',
|
|
82
|
+
message: `Horizontal overflow detected (page scrolls ${data.documentScrollWidth - data.viewportWidth}px beyond viewport)`,
|
|
83
|
+
element: {
|
|
84
|
+
selector: mainCulprit.selector,
|
|
85
|
+
tagName: mainCulprit.tagName,
|
|
86
|
+
className: mainCulprit.className,
|
|
87
|
+
id: mainCulprit.id,
|
|
88
|
+
textContent: mainCulprit.textContent,
|
|
89
|
+
rect: mainCulprit.rect,
|
|
90
|
+
},
|
|
91
|
+
details: {
|
|
92
|
+
documentScrollWidth: data.documentScrollWidth,
|
|
93
|
+
viewportWidth: data.viewportWidth,
|
|
94
|
+
overflowAmount: mainCulprit.overflowAmount,
|
|
95
|
+
otherCulprits: data.culprits.slice(1).map(c => c.selector),
|
|
96
|
+
},
|
|
97
|
+
fixHint: 'Add overflow-x: hidden to a container, or check for elements with fixed widths, 100vw, or negative margins',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
check: 'overflow',
|
|
102
|
+
passed: issues.length === 0 ? 1 : 0,
|
|
103
|
+
issues,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=overflow.js.map
|