qa-screens 0.1.0__tar.gz

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.
@@ -0,0 +1,22 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ matrix:
12
+ python: ["3.10", "3.12", "3.13"]
13
+ env:
14
+ QA_SCREENS_ERROR_REPORTING: local
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ with:
19
+ python-version: ${{ matrix.python }}
20
+ - run: uv pip install -e ".[dev]"
21
+ - run: python -m playwright install --with-deps chromium
22
+ - run: pytest -q
@@ -0,0 +1,33 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+
6
+ jobs:
7
+ build:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: astral-sh/setup-uv@v5
12
+ - name: Check tag matches package version
13
+ run: |
14
+ v=$(grep -m1 '__version__' src/qa_screens/__init__.py | cut -d'"' -f2)
15
+ test "v$v" = "${GITHUB_REF_NAME}" || { echo "tag ${GITHUB_REF_NAME} != version $v"; exit 1; }
16
+ - run: uv build
17
+ - uses: actions/upload-artifact@v4
18
+ with:
19
+ name: dist
20
+ path: dist/
21
+
22
+ publish:
23
+ needs: build
24
+ runs-on: ubuntu-latest
25
+ environment: pypi
26
+ permissions:
27
+ id-token: write # PyPI trusted publishing, no API token stored anywhere
28
+ steps:
29
+ - uses: actions/download-artifact@v4
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .qa-screens/
10
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Len Dierickx
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.
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.5
2
+ Name: qa-screens
3
+ Version: 0.1.0
4
+ Summary: MCP server for visual QA: capture pages, SSIM-diff them against reference screenshots, and let an AI refactor safely. Supports OAuth tokens, sessions and cookies for testing authenticated apps.
5
+ Project-URL: Homepage, https://github.com/astuanax/qa-screens
6
+ Project-URL: Issues, https://github.com/astuanax/qa-screens/issues
7
+ Author: Len Dierickx
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: mcp,playwright,qa,screenshots,ssim,visual-regression
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: httpx>=0.27
13
+ Requires-Dist: mcp<3,>=2.2
14
+ Requires-Dist: numpy>=1.26
15
+ Requires-Dist: opencv-python-headless>=4.8
16
+ Requires-Dist: pillow>=10
17
+ Requires-Dist: playwright>=1.45
18
+ Requires-Dist: scikit-image>=0.22
19
+ Provides-Extra: ai
20
+ Requires-Dist: google-genai>=1.0; extra == 'ai'
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
23
+ Requires-Dist: pytest>=8; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # qa-screens
27
+
28
+ An MCP server for **visual QA during AI refactoring**. It captures pages with
29
+ Playwright, compares them with reference screenshots using SSIM, and returns
30
+ the score, the changed regions and a labelled *REFERENCE | LIVE* preview image,
31
+ so the agent can see what broke and fix it.
32
+
33
+ - **Design conformance**: compare the running site with golden or Figma screenshots.
34
+ - **Refactor safety**: capture *before* and *after* a change and prove it is pixel-neutral.
35
+ - **A/B**: compare two running servers, such as `main` and a worktree.
36
+ - **Apps behind a login**: OAuth (client credentials, password, refresh, authorization code + PKCE),
37
+ bearer tokens, cookies, sessions, localStorage/sessionStorage and HTTP basic auth.
38
+ - **Self-reporting**: significant errors and crashes are filed as GitHub issues automatically,
39
+ with secrets scrubbed. See [Error reporting](#error-reporting).
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install qa-screens # or: uv tool install qa-screens
45
+ ```
46
+
47
+ Chromium is downloaded automatically on first use. To do it up front, run
48
+ `python -m playwright install chromium`.
49
+
50
+ ### Claude Code
51
+
52
+ ```bash
53
+ claude mcp add qa-screens -- uvx qa-screens
54
+ ```
55
+
56
+ or in a project's `.mcp.json`:
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "qa-screens": { "command": "uvx", "args": ["qa-screens"] }
62
+ }
63
+ }
64
+ ```
65
+
66
+ To run the latest `main` without PyPI, use `uvx --from git+https://github.com/astuanax/qa-screens qa-screens`.
67
+
68
+ Copy [`skills/qa-screenshots`](skills/qa-screenshots/SKILL.md) into `.claude/skills/`
69
+ so the agent runs QA on its own after every UI change.
70
+
71
+ ### Other MCP clients
72
+
73
+ The server speaks MCP over stdio. The command is `qa-screens` (or `python -m qa_screens`),
74
+ and it runs in the project directory, or wherever `QA_SCREENS_ROOT` points.
75
+
76
+ ## Configure
77
+
78
+ Put a `.qa-screens.json` in the project root. Every key is optional:
79
+
80
+ ```json
81
+ {
82
+ "base_url": "http://localhost:8080",
83
+ "references_dir": "qa/screenshots",
84
+ "route_template": "/nl-be/{name}/",
85
+ "routes": { "home": "/", "nl-be": "/nl-be/" },
86
+ "threshold": 0.9,
87
+ "mobile_device_scale_factor": 1,
88
+ "mask_selectors": [".carousel", "[data-testid=clock]"],
89
+ "default_profile": null
90
+ }
91
+ ```
92
+
93
+ | key | default | meaning |
94
+ |---|---|---|
95
+ | `base_url` | `http://localhost:8080` | where the site runs (not started by qa-screens) |
96
+ | `references_dir` | `screenshots` | reference `.png`/`.jpg` files; the file name is the page name |
97
+ | `route_template` | `/{name}/` | page name → path; `{name}` excludes the `-mobile` suffix |
98
+ | `routes` | `{}` | per-page overrides (a path, or a full URL) |
99
+ | `threshold` | `0.90` | minimum SSIM to pass |
100
+ | `mobile_suffix` | `-mobile` | names ending in it are captured in a 390px mobile context |
101
+ | `desktop_viewport` / `mobile_viewport` | 1440×900 / 390×844 | |
102
+ | `mobile_device_scale_factor` | `2` | set `1` if mobile references are 390px wide |
103
+ | `mask_selectors` | `[]` | elements hidden before capture (dynamic content) |
104
+ | `wait_until`, `navigation_timeout_ms` | `networkidle`, `30000` | |
105
+ | `runtime_dir` | `.qa-screens/runtime` | captures, diffs, previews, reports (add it to `.gitignore`) |
106
+ | `default_profile` | `null` | auth profile used when a tool doesn't pass one |
107
+ | `ai_critique`, `gemini_model` | `false` | optional Gemini second opinion (`pip install qa-screens[ai]`, `GEMINI_API_KEY`) |
108
+
109
+ The environment variables `QA_SCREENS_BASE_URL`, `QA_SCREENS_REFERENCES_DIR`,
110
+ `QA_SCREENS_ROUTE_TEMPLATE`, `QA_SCREENS_THRESHOLD` and `QA_SCREENS_PROFILE` override the file.
111
+
112
+ ## Tools
113
+
114
+ | tool | purpose |
115
+ |---|---|
116
+ | `qa_config` | effective config, references and their URLs, auth profiles; start here |
117
+ | `run_qa` | QA all or some pages; returns a JSON summary plus previews of the worst failures |
118
+ | `qa_page` | QA one page (fast fix loop); `url` overrides the mapping |
119
+ | `capture` | screenshot any URL, full page or a single element (`clip_selector`) |
120
+ | `compare_images` | SSIM two image files, for example a Figma export and a capture |
121
+ | `capture_set` / `compare_sets` | before/after parity check for refactors |
122
+ | `ab_compare` | compare two running servers page by page |
123
+ | `update_reference` | promote a live capture to reference (needs `confirm=true`, keeps a backup) |
124
+ | `sync_figma` | download Figma frames as references (`FIGMA_TOKEN`) |
125
+ | `auth_update_profile` | create or update an auth profile: token, OAuth, cookies, headers, storage |
126
+ | `auth_browser_login` | log in with a real browser (form, SSO or MFA) and save the session |
127
+ | `auth_oauth_login` | OAuth authorization code + PKCE in a browser window |
128
+ | `auth_import_storage_state` | import a Playwright `storageState` JSON |
129
+ | `auth_check` | check that a profile is logged in (status, redirect, screenshot) |
130
+ | `auth_profiles` / `auth_delete_profile` | list, inspect (never shows secrets) or delete profiles |
131
+ | `error_reports` | error-reporting status; `flush=true` posts pending reports |
132
+
133
+ Page results are `PASS`, `FAIL` or `ERROR`. `ERROR` means the comparison is meaningless:
134
+ an HTTP 4xx/5xx, a navigation failure or a missing reference. The agent should fix
135
+ the environment, not the CSS.
136
+
137
+ ## Authentication for apps
138
+
139
+ A **profile** is a named session that is applied to every browser context that uses it:
140
+
141
+ ```text
142
+ auth_update_profile(
143
+ name="staging",
144
+ origins=["https://app.staging.example.com"],
145
+ oauth={"grant_type": "client_credentials",
146
+ "token_url": "https://idp.example.com/oauth/token",
147
+ "client_id": "qa-bot", "client_secret": "env:QA_CLIENT_SECRET",
148
+ "audience": "https://api.example.com"},
149
+ apply_token_as=["header", "local_storage:access_token"])
150
+
151
+ run_qa(profile="staging", base_url="https://app.staging.example.com")
152
+ ```
153
+
154
+ - **Secrets**: any value can be `"env:VAR"`. It is resolved from the server's environment
155
+ at use time, so it never passes through the AI conversation.
156
+ - **Scoping**: headers and tokens go only to the profile's `origins` (default: the
157
+ base URL's origin), never to CDNs or third parties. Cookies follow normal browser rules.
158
+ - **Token placement** (`apply_token_as`): `header` (`Authorization: Bearer …`),
159
+ `header:X-Api-Key`, `local_storage:<key>`, `session_storage:<key>`,
160
+ `local_storage_json:<key>` (the whole token object), `cookie:<name>`.
161
+ - **Refresh**: expired tokens are refreshed through `refresh_token` or by re-running the
162
+ client-credentials/password grant, including mid-session in the long-lived server.
163
+ - **Storage**: profiles live in `~/.local/state/qa-screens/profiles/` with mode `0600`,
164
+ outside the project, so they are never committed. `QA_SCREENS_STATE_DIR` moves the location.
165
+
166
+ ## Error reporting
167
+
168
+ qa-screens reports its own bugs, so they reach the maintainers without a manual bug report.
169
+
170
+ - **Significant errors** (unexpected exceptions in a tool, not user errors like a missing
171
+ reference or bad credentials) are written to `~/.local/state/qa-screens/reports/pending/`
172
+ and posted in the background as a GitHub issue on `astuanax/qa-screens`.
173
+ - **Crashes**: uncaught exceptions are logged to a crash file, and hard crashes (segfault,
174
+ abort) are captured by `faulthandler`. On the **next start-up** the server scans for these
175
+ files and posts them.
176
+ - A report stays pending until it has been posted, so reports made offline or without a token
177
+ are sent later.
178
+ - **De-duplication**: each error has a stable fingerprint. A repeat within 24h is not
179
+ re-posted, and an open issue with the same fingerprint gets a comment instead of a new
180
+ issue. At most 10 posts per day.
181
+ - **Privacy**: tokens, JWTs, cookies, passwords, URL query strings and credentials, and the
182
+ home directory path are redacted. Tool arguments that hold secrets are dropped entirely.
183
+
184
+ | env var | default | |
185
+ |---|---|---|
186
+ | `QA_SCREENS_ERROR_REPORTING` | `on` | `on`, `local` (write files, never post) or `off` |
187
+ | `QA_SCREENS_ISSUE_REPO` | `astuanax/qa-screens` | where issues go (point it at your fork) |
188
+ | `QA_SCREENS_GITHUB_TOKEN` | falls back to `GITHUB_TOKEN`, then `gh auth token` | needs `issues:write` |
189
+
190
+ Without a token nothing is posted; reports wait in `pending/`. `qa-screens reports --flush`
191
+ posts them by hand.
192
+
193
+ ## CLI (CI-friendly)
194
+
195
+ ```bash
196
+ qa-screens # MCP server over stdio (same as `qa-screens serve`)
197
+ qa-screens run # QA every reference; exit 1 on FAIL/ERROR
198
+ qa-screens run nl-be faq --viewport desktop --base-url http://localhost:8080 --json
199
+ qa-screens reports --flush # post pending error reports
200
+ ```
201
+
202
+ A report is written to `.qa-screens/runtime/reports/latest.json` on every run.
203
+
204
+ ## How it works
205
+
206
+ 1. The reference name maps to a URL (`route_template` / `routes`). Names ending in `-mobile`
207
+ use a mobile context (390px, touch, mobile UA).
208
+ 2. The page is loaded with the HTTP cache disabled (so the CSS you just edited is what gets
209
+ measured), with animations, transitions and scrollbars disabled, and after
210
+ `document.fonts.ready`. It is captured full-page.
211
+ 3. SSIM is computed on grayscale images. Captures above 40MP are decoded at reduced
212
+ resolution, so long pages never run out of memory. Different sizes are handled by `align`:
213
+ `resize` (legacy default), `crop` or `pad` (height changes count as differences).
214
+ 4. Changed pixels are grouped into region boxes. A red heatmap and a cropped side-by-side
215
+ preview are written, and the preview is returned to the agent as an image.
216
+
217
+ ## Development
218
+
219
+ ```bash
220
+ uv venv && uv pip install -e ".[dev]"
221
+ python -m playwright install chromium
222
+ pytest
223
+ ```
224
+
225
+ Releases are published to PyPI by GitHub Actions when a `v*` tag is pushed (trusted publishing).
226
+
227
+ ## License
228
+
229
+ MIT
@@ -0,0 +1,204 @@
1
+ # qa-screens
2
+
3
+ An MCP server for **visual QA during AI refactoring**. It captures pages with
4
+ Playwright, compares them with reference screenshots using SSIM, and returns
5
+ the score, the changed regions and a labelled *REFERENCE | LIVE* preview image,
6
+ so the agent can see what broke and fix it.
7
+
8
+ - **Design conformance**: compare the running site with golden or Figma screenshots.
9
+ - **Refactor safety**: capture *before* and *after* a change and prove it is pixel-neutral.
10
+ - **A/B**: compare two running servers, such as `main` and a worktree.
11
+ - **Apps behind a login**: OAuth (client credentials, password, refresh, authorization code + PKCE),
12
+ bearer tokens, cookies, sessions, localStorage/sessionStorage and HTTP basic auth.
13
+ - **Self-reporting**: significant errors and crashes are filed as GitHub issues automatically,
14
+ with secrets scrubbed. See [Error reporting](#error-reporting).
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install qa-screens # or: uv tool install qa-screens
20
+ ```
21
+
22
+ Chromium is downloaded automatically on first use. To do it up front, run
23
+ `python -m playwright install chromium`.
24
+
25
+ ### Claude Code
26
+
27
+ ```bash
28
+ claude mcp add qa-screens -- uvx qa-screens
29
+ ```
30
+
31
+ or in a project's `.mcp.json`:
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "qa-screens": { "command": "uvx", "args": ["qa-screens"] }
37
+ }
38
+ }
39
+ ```
40
+
41
+ To run the latest `main` without PyPI, use `uvx --from git+https://github.com/astuanax/qa-screens qa-screens`.
42
+
43
+ Copy [`skills/qa-screenshots`](skills/qa-screenshots/SKILL.md) into `.claude/skills/`
44
+ so the agent runs QA on its own after every UI change.
45
+
46
+ ### Other MCP clients
47
+
48
+ The server speaks MCP over stdio. The command is `qa-screens` (or `python -m qa_screens`),
49
+ and it runs in the project directory, or wherever `QA_SCREENS_ROOT` points.
50
+
51
+ ## Configure
52
+
53
+ Put a `.qa-screens.json` in the project root. Every key is optional:
54
+
55
+ ```json
56
+ {
57
+ "base_url": "http://localhost:8080",
58
+ "references_dir": "qa/screenshots",
59
+ "route_template": "/nl-be/{name}/",
60
+ "routes": { "home": "/", "nl-be": "/nl-be/" },
61
+ "threshold": 0.9,
62
+ "mobile_device_scale_factor": 1,
63
+ "mask_selectors": [".carousel", "[data-testid=clock]"],
64
+ "default_profile": null
65
+ }
66
+ ```
67
+
68
+ | key | default | meaning |
69
+ |---|---|---|
70
+ | `base_url` | `http://localhost:8080` | where the site runs (not started by qa-screens) |
71
+ | `references_dir` | `screenshots` | reference `.png`/`.jpg` files; the file name is the page name |
72
+ | `route_template` | `/{name}/` | page name → path; `{name}` excludes the `-mobile` suffix |
73
+ | `routes` | `{}` | per-page overrides (a path, or a full URL) |
74
+ | `threshold` | `0.90` | minimum SSIM to pass |
75
+ | `mobile_suffix` | `-mobile` | names ending in it are captured in a 390px mobile context |
76
+ | `desktop_viewport` / `mobile_viewport` | 1440×900 / 390×844 | |
77
+ | `mobile_device_scale_factor` | `2` | set `1` if mobile references are 390px wide |
78
+ | `mask_selectors` | `[]` | elements hidden before capture (dynamic content) |
79
+ | `wait_until`, `navigation_timeout_ms` | `networkidle`, `30000` | |
80
+ | `runtime_dir` | `.qa-screens/runtime` | captures, diffs, previews, reports (add it to `.gitignore`) |
81
+ | `default_profile` | `null` | auth profile used when a tool doesn't pass one |
82
+ | `ai_critique`, `gemini_model` | `false` | optional Gemini second opinion (`pip install qa-screens[ai]`, `GEMINI_API_KEY`) |
83
+
84
+ The environment variables `QA_SCREENS_BASE_URL`, `QA_SCREENS_REFERENCES_DIR`,
85
+ `QA_SCREENS_ROUTE_TEMPLATE`, `QA_SCREENS_THRESHOLD` and `QA_SCREENS_PROFILE` override the file.
86
+
87
+ ## Tools
88
+
89
+ | tool | purpose |
90
+ |---|---|
91
+ | `qa_config` | effective config, references and their URLs, auth profiles; start here |
92
+ | `run_qa` | QA all or some pages; returns a JSON summary plus previews of the worst failures |
93
+ | `qa_page` | QA one page (fast fix loop); `url` overrides the mapping |
94
+ | `capture` | screenshot any URL, full page or a single element (`clip_selector`) |
95
+ | `compare_images` | SSIM two image files, for example a Figma export and a capture |
96
+ | `capture_set` / `compare_sets` | before/after parity check for refactors |
97
+ | `ab_compare` | compare two running servers page by page |
98
+ | `update_reference` | promote a live capture to reference (needs `confirm=true`, keeps a backup) |
99
+ | `sync_figma` | download Figma frames as references (`FIGMA_TOKEN`) |
100
+ | `auth_update_profile` | create or update an auth profile: token, OAuth, cookies, headers, storage |
101
+ | `auth_browser_login` | log in with a real browser (form, SSO or MFA) and save the session |
102
+ | `auth_oauth_login` | OAuth authorization code + PKCE in a browser window |
103
+ | `auth_import_storage_state` | import a Playwright `storageState` JSON |
104
+ | `auth_check` | check that a profile is logged in (status, redirect, screenshot) |
105
+ | `auth_profiles` / `auth_delete_profile` | list, inspect (never shows secrets) or delete profiles |
106
+ | `error_reports` | error-reporting status; `flush=true` posts pending reports |
107
+
108
+ Page results are `PASS`, `FAIL` or `ERROR`. `ERROR` means the comparison is meaningless:
109
+ an HTTP 4xx/5xx, a navigation failure or a missing reference. The agent should fix
110
+ the environment, not the CSS.
111
+
112
+ ## Authentication for apps
113
+
114
+ A **profile** is a named session that is applied to every browser context that uses it:
115
+
116
+ ```text
117
+ auth_update_profile(
118
+ name="staging",
119
+ origins=["https://app.staging.example.com"],
120
+ oauth={"grant_type": "client_credentials",
121
+ "token_url": "https://idp.example.com/oauth/token",
122
+ "client_id": "qa-bot", "client_secret": "env:QA_CLIENT_SECRET",
123
+ "audience": "https://api.example.com"},
124
+ apply_token_as=["header", "local_storage:access_token"])
125
+
126
+ run_qa(profile="staging", base_url="https://app.staging.example.com")
127
+ ```
128
+
129
+ - **Secrets**: any value can be `"env:VAR"`. It is resolved from the server's environment
130
+ at use time, so it never passes through the AI conversation.
131
+ - **Scoping**: headers and tokens go only to the profile's `origins` (default: the
132
+ base URL's origin), never to CDNs or third parties. Cookies follow normal browser rules.
133
+ - **Token placement** (`apply_token_as`): `header` (`Authorization: Bearer …`),
134
+ `header:X-Api-Key`, `local_storage:<key>`, `session_storage:<key>`,
135
+ `local_storage_json:<key>` (the whole token object), `cookie:<name>`.
136
+ - **Refresh**: expired tokens are refreshed through `refresh_token` or by re-running the
137
+ client-credentials/password grant, including mid-session in the long-lived server.
138
+ - **Storage**: profiles live in `~/.local/state/qa-screens/profiles/` with mode `0600`,
139
+ outside the project, so they are never committed. `QA_SCREENS_STATE_DIR` moves the location.
140
+
141
+ ## Error reporting
142
+
143
+ qa-screens reports its own bugs, so they reach the maintainers without a manual bug report.
144
+
145
+ - **Significant errors** (unexpected exceptions in a tool, not user errors like a missing
146
+ reference or bad credentials) are written to `~/.local/state/qa-screens/reports/pending/`
147
+ and posted in the background as a GitHub issue on `astuanax/qa-screens`.
148
+ - **Crashes**: uncaught exceptions are logged to a crash file, and hard crashes (segfault,
149
+ abort) are captured by `faulthandler`. On the **next start-up** the server scans for these
150
+ files and posts them.
151
+ - A report stays pending until it has been posted, so reports made offline or without a token
152
+ are sent later.
153
+ - **De-duplication**: each error has a stable fingerprint. A repeat within 24h is not
154
+ re-posted, and an open issue with the same fingerprint gets a comment instead of a new
155
+ issue. At most 10 posts per day.
156
+ - **Privacy**: tokens, JWTs, cookies, passwords, URL query strings and credentials, and the
157
+ home directory path are redacted. Tool arguments that hold secrets are dropped entirely.
158
+
159
+ | env var | default | |
160
+ |---|---|---|
161
+ | `QA_SCREENS_ERROR_REPORTING` | `on` | `on`, `local` (write files, never post) or `off` |
162
+ | `QA_SCREENS_ISSUE_REPO` | `astuanax/qa-screens` | where issues go (point it at your fork) |
163
+ | `QA_SCREENS_GITHUB_TOKEN` | falls back to `GITHUB_TOKEN`, then `gh auth token` | needs `issues:write` |
164
+
165
+ Without a token nothing is posted; reports wait in `pending/`. `qa-screens reports --flush`
166
+ posts them by hand.
167
+
168
+ ## CLI (CI-friendly)
169
+
170
+ ```bash
171
+ qa-screens # MCP server over stdio (same as `qa-screens serve`)
172
+ qa-screens run # QA every reference; exit 1 on FAIL/ERROR
173
+ qa-screens run nl-be faq --viewport desktop --base-url http://localhost:8080 --json
174
+ qa-screens reports --flush # post pending error reports
175
+ ```
176
+
177
+ A report is written to `.qa-screens/runtime/reports/latest.json` on every run.
178
+
179
+ ## How it works
180
+
181
+ 1. The reference name maps to a URL (`route_template` / `routes`). Names ending in `-mobile`
182
+ use a mobile context (390px, touch, mobile UA).
183
+ 2. The page is loaded with the HTTP cache disabled (so the CSS you just edited is what gets
184
+ measured), with animations, transitions and scrollbars disabled, and after
185
+ `document.fonts.ready`. It is captured full-page.
186
+ 3. SSIM is computed on grayscale images. Captures above 40MP are decoded at reduced
187
+ resolution, so long pages never run out of memory. Different sizes are handled by `align`:
188
+ `resize` (legacy default), `crop` or `pad` (height changes count as differences).
189
+ 4. Changed pixels are grouped into region boxes. A red heatmap and a cropped side-by-side
190
+ preview are written, and the preview is returned to the agent as an image.
191
+
192
+ ## Development
193
+
194
+ ```bash
195
+ uv venv && uv pip install -e ".[dev]"
196
+ python -m playwright install chromium
197
+ pytest
198
+ ```
199
+
200
+ Releases are published to PyPI by GitHub Actions when a `v*` tag is pushed (trusted publishing).
201
+
202
+ ## License
203
+
204
+ MIT
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "qa-screens"
3
+ dynamic = ["version"]
4
+ description = "MCP server for visual QA: capture pages, SSIM-diff them against reference screenshots, and let an AI refactor safely. Supports OAuth tokens, sessions and cookies for testing authenticated apps."
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.10"
8
+ authors = [{ name = "Len Dierickx" }]
9
+ keywords = ["mcp", "visual-regression", "qa", "playwright", "ssim", "screenshots"]
10
+ dependencies = [
11
+ "mcp>=2.2,<3",
12
+ "playwright>=1.45",
13
+ "opencv-python-headless>=4.8",
14
+ "scikit-image>=0.22",
15
+ "numpy>=1.26",
16
+ "Pillow>=10",
17
+ "httpx>=0.27",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ ai = ["google-genai>=1.0"]
22
+ dev = ["pytest>=8", "pytest-asyncio>=0.23"]
23
+
24
+ [project.scripts]
25
+ qa-screens = "qa_screens.cli:main"
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/astuanax/qa-screens"
29
+ Issues = "https://github.com/astuanax/qa-screens/issues"
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [tool.hatch.version]
36
+ path = "src/qa_screens/__init__.py"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/qa_screens"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ asyncio_mode = "auto"
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: qa-screenshots
3
+ description: Visual QA with the qa-screens MCP server — compare the running site or app against reference screenshots (SSIM) and fix visual regressions. ALWAYS use after any change that affects rendered output (HTML, CSS, Tailwind, components, layout, tokens, images, icons, fonts), without being asked. Also use for refactors that must stay pixel-identical (capture before/after), for apps behind a login (auth profiles), or when the user asks to check screenshots, visual regressions or Figma references.
4
+ ---
5
+
6
+ # Visual QA with qa-screens
7
+
8
+ The `qa-screens` MCP server captures pages with Playwright and compares them with
9
+ reference screenshots. Failing pages come back with changed regions (px boxes) and a
10
+ labelled REFERENCE | LIVE preview image you can look at directly.
11
+
12
+ ## 0. Orient
13
+
14
+ Call `qa_config` once: it shows the base URL, where references live, how reference
15
+ names map to URLs, and which auth profiles exist. The site must already be running
16
+ at the base URL — if pages come back `ERROR` with HTTP 404 / connection refused, ask
17
+ the user to start the dev server (or fix `route_template`/`routes` in `.qa-screens.json`).
18
+
19
+ ## 1. Choose the mode
20
+
21
+ - **Design conformance** (does the page match the design/golden screenshot?):
22
+ `run_qa` (all pages, or `pages=[...]`, `viewport="mobile"|"desktop"`).
23
+ - **Refactor safety** (the change must NOT alter pixels):
24
+ `capture_set(label="before")` *before editing* → make the change → rebuild →
25
+ `capture_set(label="after")` → `compare_sets()`. `identical: true` is the proof.
26
+ - **Two servers** (main branch vs worktree): `ab_compare(base_url_a, base_url_b)`.
27
+
28
+ ## 2. Diagnose each failure
29
+
30
+ For each FAIL: look at the preview image, read `regions` (x/y/width/height in px,
31
+ largest first) and `size_mismatch`/`warning`. Identify the cause (spacing, color,
32
+ font, missing/extra element, layout shift) and trace it to the source file.
33
+ `ERROR` is not a visual failure — it's a 4xx/5xx, a redirect to a login page, or
34
+ a missing reference; fix the environment, not the CSS.
35
+
36
+ ## 3. Fix, verify, repeat
37
+
38
+ Make the minimal source change, then `qa_page(page=...)` for just that page.
39
+ Up to 3 attempts per page, then stop and show the user the preview and your
40
+ hypothesis. Finish with one full `run_qa` — shared CSS can break neighbours.
41
+
42
+ If the difference looks **intentional** (the user just asked for this change),
43
+ don't "fix" it: tell the user the reference is stale. Only call
44
+ `update_reference(page, confirm=true)` after the user confirms.
45
+
46
+ ## Apps behind a login
47
+
48
+ Create an auth profile once, then pass `profile="<name>"` to any tool:
49
+ - Bearer/API token: `auth_update_profile(name, access_token="env:APP_TOKEN", apply_token_as=["header"])`
50
+ (or `local_storage:<key>` / `session_storage:<key>` / `cookie:<name>` — wherever the app reads it).
51
+ - OAuth machine login: `auth_update_profile(name, oauth={grant_type:"client_credentials", token_url, client_id, client_secret:"env:..."})` — refreshed automatically.
52
+ - OAuth user login (PKCE): configure `oauth` with `grant_type:"authorization_code"`, then `auth_oauth_login(name)`.
53
+ - Any login form / SSO / MFA: `auth_browser_login(name, login_url)` — the user signs in in a window; cookies, localStorage and sessionStorage are saved.
54
+ - Verify with `auth_check(name)`. A redirect warning means the session expired.
55
+ Prefer `env:VAR` for secrets so they never appear in the conversation.
56
+
57
+ ## Don'ts
58
+
59
+ - Don't update references or lower `threshold` to make a page pass.
60
+ - Don't skip the final full run.
61
+ - Don't treat an `ERROR` row as a design regression.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()