wup 0.2.14__tar.gz → 0.2.15__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.
- {wup-0.2.14/wup.egg-info → wup-0.2.15}/PKG-INFO +96 -56
- {wup-0.2.14 → wup-0.2.15}/README.md +95 -55
- {wup-0.2.14 → wup-0.2.15}/pyproject.toml +1 -1
- {wup-0.2.14 → wup-0.2.15}/tests/test_testql_watcher.py +55 -1
- {wup-0.2.14 → wup-0.2.15}/tests/test_wup.py +278 -0
- {wup-0.2.14 → wup-0.2.15}/wup/__init__.py +1 -1
- {wup-0.2.14 → wup-0.2.15}/wup/config.py +22 -3
- {wup-0.2.14 → wup-0.2.15/wup.egg-info}/PKG-INFO +96 -56
- {wup-0.2.14 → wup-0.2.15}/LICENSE +0 -0
- {wup-0.2.14 → wup-0.2.15}/setup.cfg +0 -0
- {wup-0.2.14 → wup-0.2.15}/tests/test_e2e.py +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup/cli.py +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup/core.py +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup/dependency_mapper.py +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup/models/__init__.py +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup/models/config.py +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup/testql_discovery.py +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup/testql_watcher.py +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup/visual_diff.py +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup.egg-info/SOURCES.txt +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup.egg-info/dependency_links.txt +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup.egg-info/entry_points.txt +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup.egg-info/requires.txt +0 -0
- {wup-0.2.14 → wup-0.2.15}/wup.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.15
|
|
4
4
|
Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -29,17 +29,17 @@ Dynamic: license-file
|
|
|
29
29
|
|
|
30
30
|
## AI Cost Tracking
|
|
31
31
|
|
|
32
|
-
    
|
|
33
|
+
  
|
|
34
34
|
|
|
35
|
-
- 🤖 **LLM usage:** $2.
|
|
36
|
-
- 👤 **Human dev:** ~$
|
|
35
|
+
- 🤖 **LLM usage:** $2.4000 (16 commits)
|
|
36
|
+
- 👤 **Human dev:** ~$411 (4.1h @ $100/h, 30min dedup)
|
|
37
37
|
|
|
38
38
|
Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
-
    
|
|
43
43
|
|
|
44
44
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
45
45
|
|
|
@@ -285,56 +285,86 @@ export WUP_CPU_THROTTLE=0.5
|
|
|
285
285
|
export WUP_DEBOUNCE=3
|
|
286
286
|
```
|
|
287
287
|
|
|
288
|
-
##
|
|
289
|
-
|
|
290
|
-
WUP
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
288
|
+
## Visual DOM Diff
|
|
289
|
+
|
|
290
|
+
WUP optionally scans configured pages with Playwright after each successful quick test, compares the DOM structure to the previous snapshot, and reports significant changes.
|
|
291
|
+
|
|
292
|
+
### Setup
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
pip install playwright
|
|
296
|
+
playwright install chromium
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Configuration
|
|
300
|
+
|
|
301
|
+
```yaml
|
|
302
|
+
visual_diff:
|
|
303
|
+
enabled: true
|
|
304
|
+
base_url: "http://localhost:8100" # or leave empty and set WUP_BASE_URL env var
|
|
305
|
+
base_url_env: "WUP_BASE_URL"
|
|
306
|
+
delay_seconds: 5.0 # wait after file change before scanning
|
|
307
|
+
max_depth: 10 # DOM snapshot depth
|
|
308
|
+
pages:
|
|
309
|
+
- "/health"
|
|
310
|
+
- "/dashboard"
|
|
311
|
+
pages_from_endpoints: true # also scan endpoints from testql config
|
|
312
|
+
threshold_added: 3 # min node additions to report as "changed"
|
|
313
|
+
threshold_removed: 3
|
|
314
|
+
threshold_changed: 5
|
|
315
|
+
headless: true
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Or set the base URL in `.wup.env` in the project root (not committed to git):
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# .wup.env
|
|
322
|
+
WUP_BASE_URL=http://localhost:8100
|
|
313
323
|
```
|
|
314
324
|
|
|
325
|
+
### Output
|
|
326
|
+
|
|
327
|
+
- **Snapshots** — `.wup/visual-snapshots/<service>/<page>.json`
|
|
328
|
+
- **Diff events** — `.wup/visual-diffs/<service>/<page>.jsonl` (appended on each change)
|
|
329
|
+
|
|
330
|
+
Visible in `wup status` as a "Visual DOM diffs" section.
|
|
331
|
+
|
|
332
|
+
### Graceful degradation
|
|
333
|
+
|
|
334
|
+
If Playwright is not installed, the visual diff module logs a warning and skips scanning — it does **not** break the watcher.
|
|
335
|
+
|
|
315
336
|
## Project Structure
|
|
316
337
|
|
|
317
338
|
```
|
|
318
339
|
wup/
|
|
319
340
|
├── wup/
|
|
320
|
-
│ ├── __init__.py
|
|
321
|
-
│ ├──
|
|
322
|
-
│ ├──
|
|
323
|
-
│
|
|
324
|
-
│
|
|
325
|
-
│ ├──
|
|
326
|
-
│ ├──
|
|
327
|
-
│ ├──
|
|
328
|
-
│ └──
|
|
341
|
+
│ ├── __init__.py # Package exports
|
|
342
|
+
│ ├── cli.py # CLI: watch, map-deps, status, init, testql-endpoints
|
|
343
|
+
│ ├── config.py # Config loading/saving + .wup.env support
|
|
344
|
+
│ ├── core.py # WupWatcher: detection, inference, scheduling
|
|
345
|
+
│ ├── dependency_mapper.py # DependencyMapper: codebase → deps.json
|
|
346
|
+
│ ├── testql_discovery.py # TestQLEndpointDiscovery: scenario parsing
|
|
347
|
+
│ ├── testql_watcher.py # TestQLWatcher: scenario runner + health tracking
|
|
348
|
+
│ ├── visual_diff.py # VisualDiffer: Playwright DOM snapshot + diff engine
|
|
349
|
+
│ └── models/
|
|
350
|
+
│ ├── __init__.py
|
|
351
|
+
│ └── config.py # Dataclasses: WupConfig, VisualDiffConfig, TestQLConfig...
|
|
329
352
|
├── tests/
|
|
330
|
-
│
|
|
353
|
+
│ ├── test_wup.py # unit/integration tests (incl. VisualDiffer, config)
|
|
354
|
+
│ ├── test_testql_watcher.py # TestQLWatcher + VisualDiffer integration tests
|
|
355
|
+
│ └── test_e2e.py # end-to-end CLI tests
|
|
356
|
+
├── examples/
|
|
357
|
+
│ ├── fastapi-app/ # FastAPI example project
|
|
358
|
+
│ ├── flask-app/ # Flask example project
|
|
359
|
+
│ ├── multi-service/ # Multi-service example
|
|
360
|
+
│ ├── testql_demo.py # TestQL simulation demo
|
|
361
|
+
│ ├── testql_integration.py # Custom TestQLWatcher + visual diff example
|
|
362
|
+
│ └── visual_diff_demo.py # Visual DOM diff demo (no Playwright required)
|
|
331
363
|
├── docs/
|
|
332
|
-
│
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
├── pyproject.toml # Package configuration
|
|
337
|
-
└── README.md # This file
|
|
364
|
+
│ └── TESTQL_INTEGRATION.md # TestQL integration guide
|
|
365
|
+
├── testql-scenarios/ # Auto-generated TestQL scenarios
|
|
366
|
+
├── pyproject.toml # Package config (setuptools)
|
|
367
|
+
└── README.md
|
|
338
368
|
```
|
|
339
369
|
|
|
340
370
|
## Development
|
|
@@ -343,23 +373,33 @@ wup/
|
|
|
343
373
|
|
|
344
374
|
```bash
|
|
345
375
|
# Run all tests
|
|
346
|
-
pytest
|
|
376
|
+
python3 -m pytest tests/ -v
|
|
377
|
+
|
|
378
|
+
# Run specific suite
|
|
379
|
+
python3 -m pytest tests/test_wup.py -v
|
|
380
|
+
python3 -m pytest tests/test_testql_watcher.py -v
|
|
347
381
|
|
|
348
382
|
# Run with coverage
|
|
349
|
-
pytest --cov=wup
|
|
383
|
+
python3 -m pytest tests/ --cov=wup
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Examples
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
# Visual diff demo (no Playwright required)
|
|
390
|
+
python3 examples/visual_diff_demo.py
|
|
391
|
+
|
|
392
|
+
# With live page scan (requires playwright)
|
|
393
|
+
python3 examples/visual_diff_demo.py http://localhost:8100/health
|
|
350
394
|
|
|
351
|
-
#
|
|
352
|
-
|
|
395
|
+
# TestQL + visual diff integration
|
|
396
|
+
python3 examples/testql_integration.py /path/to/project
|
|
353
397
|
```
|
|
354
398
|
|
|
355
|
-
### Building
|
|
399
|
+
### Building & Publishing
|
|
356
400
|
|
|
357
401
|
```bash
|
|
358
|
-
# Build wheel and source distribution
|
|
359
402
|
python -m build
|
|
360
|
-
|
|
361
|
-
# Install from dist
|
|
362
|
-
pip install dist/wup-0.1.6-py3-none-any.whl
|
|
363
403
|
```
|
|
364
404
|
|
|
365
405
|
## License
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $2.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $2.4000 (16 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$411 (4.1h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
12
|
Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
    
|
|
17
17
|
|
|
18
18
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
19
19
|
|
|
@@ -259,56 +259,86 @@ export WUP_CPU_THROTTLE=0.5
|
|
|
259
259
|
export WUP_DEBOUNCE=3
|
|
260
260
|
```
|
|
261
261
|
|
|
262
|
-
##
|
|
263
|
-
|
|
264
|
-
WUP
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
262
|
+
## Visual DOM Diff
|
|
263
|
+
|
|
264
|
+
WUP optionally scans configured pages with Playwright after each successful quick test, compares the DOM structure to the previous snapshot, and reports significant changes.
|
|
265
|
+
|
|
266
|
+
### Setup
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
pip install playwright
|
|
270
|
+
playwright install chromium
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Configuration
|
|
274
|
+
|
|
275
|
+
```yaml
|
|
276
|
+
visual_diff:
|
|
277
|
+
enabled: true
|
|
278
|
+
base_url: "http://localhost:8100" # or leave empty and set WUP_BASE_URL env var
|
|
279
|
+
base_url_env: "WUP_BASE_URL"
|
|
280
|
+
delay_seconds: 5.0 # wait after file change before scanning
|
|
281
|
+
max_depth: 10 # DOM snapshot depth
|
|
282
|
+
pages:
|
|
283
|
+
- "/health"
|
|
284
|
+
- "/dashboard"
|
|
285
|
+
pages_from_endpoints: true # also scan endpoints from testql config
|
|
286
|
+
threshold_added: 3 # min node additions to report as "changed"
|
|
287
|
+
threshold_removed: 3
|
|
288
|
+
threshold_changed: 5
|
|
289
|
+
headless: true
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Or set the base URL in `.wup.env` in the project root (not committed to git):
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
# .wup.env
|
|
296
|
+
WUP_BASE_URL=http://localhost:8100
|
|
287
297
|
```
|
|
288
298
|
|
|
299
|
+
### Output
|
|
300
|
+
|
|
301
|
+
- **Snapshots** — `.wup/visual-snapshots/<service>/<page>.json`
|
|
302
|
+
- **Diff events** — `.wup/visual-diffs/<service>/<page>.jsonl` (appended on each change)
|
|
303
|
+
|
|
304
|
+
Visible in `wup status` as a "Visual DOM diffs" section.
|
|
305
|
+
|
|
306
|
+
### Graceful degradation
|
|
307
|
+
|
|
308
|
+
If Playwright is not installed, the visual diff module logs a warning and skips scanning — it does **not** break the watcher.
|
|
309
|
+
|
|
289
310
|
## Project Structure
|
|
290
311
|
|
|
291
312
|
```
|
|
292
313
|
wup/
|
|
293
314
|
├── wup/
|
|
294
|
-
│ ├── __init__.py
|
|
295
|
-
│ ├──
|
|
296
|
-
│ ├──
|
|
297
|
-
│
|
|
298
|
-
│
|
|
299
|
-
│ ├──
|
|
300
|
-
│ ├──
|
|
301
|
-
│ ├──
|
|
302
|
-
│ └──
|
|
315
|
+
│ ├── __init__.py # Package exports
|
|
316
|
+
│ ├── cli.py # CLI: watch, map-deps, status, init, testql-endpoints
|
|
317
|
+
│ ├── config.py # Config loading/saving + .wup.env support
|
|
318
|
+
│ ├── core.py # WupWatcher: detection, inference, scheduling
|
|
319
|
+
│ ├── dependency_mapper.py # DependencyMapper: codebase → deps.json
|
|
320
|
+
│ ├── testql_discovery.py # TestQLEndpointDiscovery: scenario parsing
|
|
321
|
+
│ ├── testql_watcher.py # TestQLWatcher: scenario runner + health tracking
|
|
322
|
+
│ ├── visual_diff.py # VisualDiffer: Playwright DOM snapshot + diff engine
|
|
323
|
+
│ └── models/
|
|
324
|
+
│ ├── __init__.py
|
|
325
|
+
│ └── config.py # Dataclasses: WupConfig, VisualDiffConfig, TestQLConfig...
|
|
303
326
|
├── tests/
|
|
304
|
-
│
|
|
327
|
+
│ ├── test_wup.py # unit/integration tests (incl. VisualDiffer, config)
|
|
328
|
+
│ ├── test_testql_watcher.py # TestQLWatcher + VisualDiffer integration tests
|
|
329
|
+
│ └── test_e2e.py # end-to-end CLI tests
|
|
330
|
+
├── examples/
|
|
331
|
+
│ ├── fastapi-app/ # FastAPI example project
|
|
332
|
+
│ ├── flask-app/ # Flask example project
|
|
333
|
+
│ ├── multi-service/ # Multi-service example
|
|
334
|
+
│ ├── testql_demo.py # TestQL simulation demo
|
|
335
|
+
│ ├── testql_integration.py # Custom TestQLWatcher + visual diff example
|
|
336
|
+
│ └── visual_diff_demo.py # Visual DOM diff demo (no Playwright required)
|
|
305
337
|
├── docs/
|
|
306
|
-
│
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
├── pyproject.toml # Package configuration
|
|
311
|
-
└── README.md # This file
|
|
338
|
+
│ └── TESTQL_INTEGRATION.md # TestQL integration guide
|
|
339
|
+
├── testql-scenarios/ # Auto-generated TestQL scenarios
|
|
340
|
+
├── pyproject.toml # Package config (setuptools)
|
|
341
|
+
└── README.md
|
|
312
342
|
```
|
|
313
343
|
|
|
314
344
|
## Development
|
|
@@ -317,23 +347,33 @@ wup/
|
|
|
317
347
|
|
|
318
348
|
```bash
|
|
319
349
|
# Run all tests
|
|
320
|
-
pytest
|
|
350
|
+
python3 -m pytest tests/ -v
|
|
351
|
+
|
|
352
|
+
# Run specific suite
|
|
353
|
+
python3 -m pytest tests/test_wup.py -v
|
|
354
|
+
python3 -m pytest tests/test_testql_watcher.py -v
|
|
321
355
|
|
|
322
356
|
# Run with coverage
|
|
323
|
-
pytest --cov=wup
|
|
357
|
+
python3 -m pytest tests/ --cov=wup
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Examples
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
# Visual diff demo (no Playwright required)
|
|
364
|
+
python3 examples/visual_diff_demo.py
|
|
365
|
+
|
|
366
|
+
# With live page scan (requires playwright)
|
|
367
|
+
python3 examples/visual_diff_demo.py http://localhost:8100/health
|
|
324
368
|
|
|
325
|
-
#
|
|
326
|
-
|
|
369
|
+
# TestQL + visual diff integration
|
|
370
|
+
python3 examples/testql_integration.py /path/to/project
|
|
327
371
|
```
|
|
328
372
|
|
|
329
|
-
### Building
|
|
373
|
+
### Building & Publishing
|
|
330
374
|
|
|
331
375
|
```bash
|
|
332
|
-
# Build wheel and source distribution
|
|
333
376
|
python -m build
|
|
334
|
-
|
|
335
|
-
# Install from dist
|
|
336
|
-
pip install dist/wup-0.1.6-py3-none-any.whl
|
|
337
377
|
```
|
|
338
378
|
|
|
339
379
|
## License
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from subprocess import CompletedProcess
|
|
7
7
|
|
|
8
8
|
from wup.testql_watcher import TestQLWatcher
|
|
9
|
-
from wup.models.config import WupConfig, ProjectConfig, TestQLConfig
|
|
9
|
+
from wup.models.config import WupConfig, ProjectConfig, TestQLConfig, VisualDiffConfig
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def test_process_changed_file_creates_track_on_failure():
|
|
@@ -199,3 +199,57 @@ def test_service_health_transitions_are_persisted():
|
|
|
199
199
|
statuses = [event.get("status") for event in events if event.get("service") == "connect-config"]
|
|
200
200
|
assert "down" in statuses
|
|
201
201
|
assert "up" in statuses
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_visual_differ_disabled_by_default():
|
|
205
|
+
"""visual_differ exists but is disabled (no-op) when visual_diff.enabled=False."""
|
|
206
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
207
|
+
root = Path(tmpdir)
|
|
208
|
+
cfg = WupConfig(
|
|
209
|
+
project=ProjectConfig(name="demo"),
|
|
210
|
+
testql=TestQLConfig(scenario_dir="testql-scenarios"),
|
|
211
|
+
visual_diff=VisualDiffConfig(enabled=False),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
watcher = TestQLWatcher(
|
|
215
|
+
project_root=str(root),
|
|
216
|
+
deps_file=str(root / "deps.json"),
|
|
217
|
+
scenarios_dir="testql-scenarios",
|
|
218
|
+
track_dir=".wup/tracks",
|
|
219
|
+
config=cfg,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Differ is created but flagged disabled — run_for_service() must be a no-op
|
|
223
|
+
assert watcher.visual_differ is not None
|
|
224
|
+
assert watcher.visual_differ.cfg.enabled is False
|
|
225
|
+
results = asyncio.run(watcher.visual_differ.run_for_service("svc", ["/x"]))
|
|
226
|
+
assert results == []
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_visual_differ_initialized_when_enabled():
|
|
230
|
+
"""When visual_diff.enabled=True, TestQLWatcher.visual_differ is a VisualDiffer."""
|
|
231
|
+
from wup.visual_diff import VisualDiffer
|
|
232
|
+
|
|
233
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
234
|
+
root = Path(tmpdir)
|
|
235
|
+
cfg = WupConfig(
|
|
236
|
+
project=ProjectConfig(name="demo"),
|
|
237
|
+
testql=TestQLConfig(scenario_dir="testql-scenarios"),
|
|
238
|
+
visual_diff=VisualDiffConfig(
|
|
239
|
+
enabled=True,
|
|
240
|
+
base_url="http://localhost:9000",
|
|
241
|
+
pages=["/dashboard"],
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
watcher = TestQLWatcher(
|
|
246
|
+
project_root=str(root),
|
|
247
|
+
deps_file=str(root / "deps.json"),
|
|
248
|
+
scenarios_dir="testql-scenarios",
|
|
249
|
+
track_dir=".wup/tracks",
|
|
250
|
+
config=cfg,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
assert isinstance(watcher.visual_differ, VisualDiffer)
|
|
254
|
+
assert watcher.visual_differ.cfg.enabled is True
|
|
255
|
+
assert watcher.visual_differ.base_url == "http://localhost:9000"
|
|
@@ -18,8 +18,10 @@ from wup.models.config import (
|
|
|
18
18
|
NotifyConfig,
|
|
19
19
|
ServiceTestConfig,
|
|
20
20
|
ProjectConfig,
|
|
21
|
+
VisualDiffConfig,
|
|
21
22
|
)
|
|
22
23
|
from wup.testql_watcher import TestQLWatcher
|
|
24
|
+
from wup.visual_diff import VisualDiffer, _diff_snapshots, _page_slug, _resolve_base_url
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
class TestDependencyMapper:
|
|
@@ -921,6 +923,162 @@ class TestConfigModels:
|
|
|
921
923
|
assert config.project.name == "test"
|
|
922
924
|
assert len(config.services) == 1
|
|
923
925
|
assert config.services[0].name == "users"
|
|
926
|
+
# visual_diff is auto-populated with defaults
|
|
927
|
+
assert isinstance(config.visual_diff, VisualDiffConfig)
|
|
928
|
+
assert config.visual_diff.enabled is False
|
|
929
|
+
|
|
930
|
+
def test_visual_diff_config_defaults(self):
|
|
931
|
+
"""Test VisualDiffConfig has sensible defaults."""
|
|
932
|
+
cfg = VisualDiffConfig()
|
|
933
|
+
assert cfg.enabled is False
|
|
934
|
+
assert cfg.base_url == ""
|
|
935
|
+
assert cfg.base_url_env == "WUP_BASE_URL"
|
|
936
|
+
assert cfg.delay_seconds == 5.0
|
|
937
|
+
assert cfg.max_depth == 10
|
|
938
|
+
assert cfg.snapshot_dir == ".wup/visual-snapshots"
|
|
939
|
+
assert cfg.diff_dir == ".wup/visual-diffs"
|
|
940
|
+
assert cfg.pages == []
|
|
941
|
+
assert cfg.pages_from_endpoints is True
|
|
942
|
+
assert cfg.headless is True
|
|
943
|
+
|
|
944
|
+
def test_visual_diff_config_custom(self):
|
|
945
|
+
"""Test VisualDiffConfig with custom values."""
|
|
946
|
+
cfg = VisualDiffConfig(
|
|
947
|
+
enabled=True,
|
|
948
|
+
base_url="http://localhost:9000",
|
|
949
|
+
pages=["/dashboard", "/users"],
|
|
950
|
+
threshold_added=10,
|
|
951
|
+
)
|
|
952
|
+
assert cfg.enabled is True
|
|
953
|
+
assert cfg.base_url == "http://localhost:9000"
|
|
954
|
+
assert cfg.pages == ["/dashboard", "/users"]
|
|
955
|
+
assert cfg.threshold_added == 10
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
class TestVisualDiffer:
|
|
959
|
+
"""Tests for the VisualDiffer class (no Playwright required)."""
|
|
960
|
+
|
|
961
|
+
def test_resolve_base_url_from_config(self):
|
|
962
|
+
cfg = VisualDiffConfig(base_url="http://localhost:8080/")
|
|
963
|
+
assert _resolve_base_url(cfg) == "http://localhost:8080"
|
|
964
|
+
|
|
965
|
+
def test_resolve_base_url_from_env(self, monkeypatch):
|
|
966
|
+
cfg = VisualDiffConfig(base_url="", base_url_env="MY_VD_URL")
|
|
967
|
+
monkeypatch.setenv("MY_VD_URL", "http://env-host:1234/")
|
|
968
|
+
assert _resolve_base_url(cfg) == "http://env-host:1234"
|
|
969
|
+
|
|
970
|
+
def test_resolve_base_url_empty(self, monkeypatch):
|
|
971
|
+
cfg = VisualDiffConfig(base_url="", base_url_env="WUP_VD_NONE")
|
|
972
|
+
monkeypatch.delenv("WUP_VD_NONE", raising=False)
|
|
973
|
+
assert _resolve_base_url(cfg) == ""
|
|
974
|
+
|
|
975
|
+
def test_page_slug(self):
|
|
976
|
+
assert _page_slug("http://x/api/v1/users") == "api_v1_users"
|
|
977
|
+
assert _page_slug("http://x/") == "root"
|
|
978
|
+
|
|
979
|
+
def test_pages_for_service_explicit(self):
|
|
980
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
981
|
+
cfg = VisualDiffConfig(
|
|
982
|
+
base_url="http://localhost:8080",
|
|
983
|
+
pages=["/dashboard"],
|
|
984
|
+
pages_from_endpoints=False,
|
|
985
|
+
)
|
|
986
|
+
differ = VisualDiffer(tmpdir, cfg)
|
|
987
|
+
pages = differ._pages_for_service("users", ["/api/users"])
|
|
988
|
+
assert pages == ["http://localhost:8080/dashboard"]
|
|
989
|
+
|
|
990
|
+
def test_pages_for_service_from_endpoints(self):
|
|
991
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
992
|
+
cfg = VisualDiffConfig(
|
|
993
|
+
base_url="http://localhost:8080",
|
|
994
|
+
pages_from_endpoints=True,
|
|
995
|
+
)
|
|
996
|
+
differ = VisualDiffer(tmpdir, cfg)
|
|
997
|
+
pages = differ._pages_for_service("users", ["/api/users", "/api/users/1"])
|
|
998
|
+
assert "http://localhost:8080/api/users" in pages
|
|
999
|
+
assert "http://localhost:8080/api/users/1" in pages
|
|
1000
|
+
|
|
1001
|
+
def test_pages_for_service_fallback(self):
|
|
1002
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1003
|
+
cfg = VisualDiffConfig(
|
|
1004
|
+
base_url="http://localhost:8080",
|
|
1005
|
+
pages_from_endpoints=False,
|
|
1006
|
+
)
|
|
1007
|
+
differ = VisualDiffer(tmpdir, cfg)
|
|
1008
|
+
pages = differ._pages_for_service("users", [])
|
|
1009
|
+
assert pages == ["http://localhost:8080/users"]
|
|
1010
|
+
|
|
1011
|
+
def test_pages_for_service_absolute_url_passthrough(self):
|
|
1012
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1013
|
+
cfg = VisualDiffConfig(
|
|
1014
|
+
base_url="http://localhost:8080",
|
|
1015
|
+
pages=["https://example.com/health"],
|
|
1016
|
+
pages_from_endpoints=False,
|
|
1017
|
+
)
|
|
1018
|
+
differ = VisualDiffer(tmpdir, cfg)
|
|
1019
|
+
pages = differ._pages_for_service("svc", [])
|
|
1020
|
+
assert pages == ["https://example.com/health"]
|
|
1021
|
+
|
|
1022
|
+
def test_diff_snapshots_baseline(self):
|
|
1023
|
+
new = {"tag": "HTML", "children": [{"tag": "BODY"}]}
|
|
1024
|
+
diff = _diff_snapshots(None, new, max_depth=5,
|
|
1025
|
+
threshold_added=1, threshold_removed=1, threshold_changed=1)
|
|
1026
|
+
assert diff["status"] == "new"
|
|
1027
|
+
|
|
1028
|
+
def test_diff_snapshots_identical(self):
|
|
1029
|
+
snap = {"tag": "HTML", "children": [{"tag": "BODY", "id": "main"}]}
|
|
1030
|
+
diff = _diff_snapshots(snap, snap, max_depth=5,
|
|
1031
|
+
threshold_added=1, threshold_removed=1, threshold_changed=1)
|
|
1032
|
+
assert diff["status"] == "ok"
|
|
1033
|
+
assert diff["counts"]["added"] == 0
|
|
1034
|
+
assert diff["counts"]["removed"] == 0
|
|
1035
|
+
|
|
1036
|
+
def test_diff_snapshots_changed(self):
|
|
1037
|
+
old = {"tag": "HTML", "children": [{"tag": "BODY"}]}
|
|
1038
|
+
new = {"tag": "HTML", "children": [
|
|
1039
|
+
{"tag": "BODY"},
|
|
1040
|
+
{"tag": "DIV", "id": "added1"},
|
|
1041
|
+
{"tag": "DIV", "id": "added2"},
|
|
1042
|
+
{"tag": "DIV", "id": "added3"},
|
|
1043
|
+
]}
|
|
1044
|
+
diff = _diff_snapshots(old, new, max_depth=5,
|
|
1045
|
+
threshold_added=3, threshold_removed=10, threshold_changed=10)
|
|
1046
|
+
assert diff["status"] == "changed"
|
|
1047
|
+
assert diff["counts"]["added"] >= 3
|
|
1048
|
+
|
|
1049
|
+
def test_run_for_service_disabled_returns_empty(self):
|
|
1050
|
+
"""When disabled, run_for_service should be a no-op."""
|
|
1051
|
+
import asyncio
|
|
1052
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1053
|
+
cfg = VisualDiffConfig(enabled=False)
|
|
1054
|
+
differ = VisualDiffer(tmpdir, cfg)
|
|
1055
|
+
results = asyncio.run(differ.run_for_service("svc", ["/x"]))
|
|
1056
|
+
assert results == []
|
|
1057
|
+
|
|
1058
|
+
def test_get_recent_diffs_empty(self):
|
|
1059
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1060
|
+
cfg = VisualDiffConfig()
|
|
1061
|
+
differ = VisualDiffer(tmpdir, cfg)
|
|
1062
|
+
assert differ.get_recent_diffs() == []
|
|
1063
|
+
|
|
1064
|
+
def test_get_recent_diffs_filters_by_age(self):
|
|
1065
|
+
import time
|
|
1066
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1067
|
+
cfg = VisualDiffConfig(diff_dir=".wup/visual-diffs")
|
|
1068
|
+
differ = VisualDiffer(tmpdir, cfg)
|
|
1069
|
+
now = int(time.time())
|
|
1070
|
+
diff_file = differ.diff_dir / "svc" / "page.jsonl"
|
|
1071
|
+
diff_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1072
|
+
diff_file.write_text(
|
|
1073
|
+
json.dumps({"timestamp": now, "service": "svc", "url": "/x",
|
|
1074
|
+
"diff": {"status": "changed"}}) + "\n"
|
|
1075
|
+
+ json.dumps({"timestamp": now - 9999, "service": "svc", "url": "/y",
|
|
1076
|
+
"diff": {"status": "changed"}}) + "\n",
|
|
1077
|
+
encoding="utf-8",
|
|
1078
|
+
)
|
|
1079
|
+
recent = differ.get_recent_diffs(seconds=300)
|
|
1080
|
+
assert len(recent) == 1
|
|
1081
|
+
assert recent[0]["url"] == "/x"
|
|
924
1082
|
|
|
925
1083
|
|
|
926
1084
|
class TestConfigLoader:
|
|
@@ -1073,6 +1231,126 @@ project:
|
|
|
1073
1231
|
with pytest.raises(ValueError, match="project.name"):
|
|
1074
1232
|
load_config(Path(tmpdir), config_path)
|
|
1075
1233
|
|
|
1234
|
+
def test_save_and_load_visual_diff_config(self):
|
|
1235
|
+
"""Test that visual_diff section is correctly saved and reloaded."""
|
|
1236
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1237
|
+
config = WupConfig(
|
|
1238
|
+
project=ProjectConfig(name="vd-test"),
|
|
1239
|
+
visual_diff=VisualDiffConfig(
|
|
1240
|
+
enabled=True,
|
|
1241
|
+
base_url="http://localhost:9000",
|
|
1242
|
+
pages=["/health", "/users"],
|
|
1243
|
+
delay_seconds=2.5,
|
|
1244
|
+
max_depth=6,
|
|
1245
|
+
threshold_added=7,
|
|
1246
|
+
threshold_removed=4,
|
|
1247
|
+
threshold_changed=8,
|
|
1248
|
+
headless=False,
|
|
1249
|
+
),
|
|
1250
|
+
)
|
|
1251
|
+
config_path = Path(tmpdir) / "wup.yaml"
|
|
1252
|
+
save_config(config, config_path)
|
|
1253
|
+
|
|
1254
|
+
loaded = load_config(Path(tmpdir), config_path)
|
|
1255
|
+
vd = loaded.visual_diff
|
|
1256
|
+
assert vd.enabled is True
|
|
1257
|
+
assert vd.base_url == "http://localhost:9000"
|
|
1258
|
+
assert vd.pages == ["/health", "/users"]
|
|
1259
|
+
assert vd.delay_seconds == 2.5
|
|
1260
|
+
assert vd.max_depth == 6
|
|
1261
|
+
assert vd.threshold_added == 7
|
|
1262
|
+
assert vd.threshold_removed == 4
|
|
1263
|
+
assert vd.threshold_changed == 8
|
|
1264
|
+
assert vd.headless is False
|
|
1265
|
+
|
|
1266
|
+
def test_load_config_visual_diff_from_yaml(self):
|
|
1267
|
+
"""Test parsing visual_diff section from raw YAML."""
|
|
1268
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1269
|
+
yaml_content = """
|
|
1270
|
+
project:
|
|
1271
|
+
name: "my-app"
|
|
1272
|
+
visual_diff:
|
|
1273
|
+
enabled: true
|
|
1274
|
+
base_url: "http://localhost:8100"
|
|
1275
|
+
base_url_env: "MY_BASE_URL"
|
|
1276
|
+
delay_seconds: 3.0
|
|
1277
|
+
max_depth: 8
|
|
1278
|
+
snapshot_dir: ".wup/snaps"
|
|
1279
|
+
diff_dir: ".wup/diffs"
|
|
1280
|
+
pages:
|
|
1281
|
+
- "/dashboard"
|
|
1282
|
+
- "/settings"
|
|
1283
|
+
pages_from_endpoints: false
|
|
1284
|
+
threshold_added: 5
|
|
1285
|
+
threshold_removed: 2
|
|
1286
|
+
threshold_changed: 9
|
|
1287
|
+
headless: false
|
|
1288
|
+
"""
|
|
1289
|
+
config_path = Path(tmpdir) / "wup.yaml"
|
|
1290
|
+
config_path.write_text(yaml_content)
|
|
1291
|
+
config = load_config(Path(tmpdir), config_path)
|
|
1292
|
+
vd = config.visual_diff
|
|
1293
|
+
assert vd.enabled is True
|
|
1294
|
+
assert vd.base_url == "http://localhost:8100"
|
|
1295
|
+
assert vd.base_url_env == "MY_BASE_URL"
|
|
1296
|
+
assert vd.delay_seconds == 3.0
|
|
1297
|
+
assert vd.max_depth == 8
|
|
1298
|
+
assert vd.snapshot_dir == ".wup/snaps"
|
|
1299
|
+
assert vd.diff_dir == ".wup/diffs"
|
|
1300
|
+
assert vd.pages == ["/dashboard", "/settings"]
|
|
1301
|
+
assert vd.pages_from_endpoints is False
|
|
1302
|
+
assert vd.threshold_added == 5
|
|
1303
|
+
assert vd.threshold_removed == 2
|
|
1304
|
+
assert vd.threshold_changed == 9
|
|
1305
|
+
assert vd.headless is False
|
|
1306
|
+
|
|
1307
|
+
def test_load_config_visual_diff_defaults_when_section_absent(self):
|
|
1308
|
+
"""visual_diff section absent → all defaults used."""
|
|
1309
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1310
|
+
config_path = Path(tmpdir) / "wup.yaml"
|
|
1311
|
+
config_path.write_text("project:\n name: x\n")
|
|
1312
|
+
config = load_config(Path(tmpdir), config_path)
|
|
1313
|
+
vd = config.visual_diff
|
|
1314
|
+
assert vd.enabled is False
|
|
1315
|
+
assert vd.base_url == ""
|
|
1316
|
+
assert vd.max_depth == 10
|
|
1317
|
+
assert vd.pages == []
|
|
1318
|
+
assert vd.headless is True
|
|
1319
|
+
|
|
1320
|
+
def test_load_dotenv_sets_env_var(self):
|
|
1321
|
+
"""_load_dotenv should load .wup.env into os.environ."""
|
|
1322
|
+
import os
|
|
1323
|
+
from wup.config import _load_dotenv
|
|
1324
|
+
|
|
1325
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1326
|
+
root = Path(tmpdir)
|
|
1327
|
+
env_file = root / ".wup.env"
|
|
1328
|
+
env_file.write_text('WUP_TEST_DUMMY_VAR=hello_wup\n# comment\n', encoding="utf-8")
|
|
1329
|
+
existing = os.environ.pop("WUP_TEST_DUMMY_VAR", None)
|
|
1330
|
+
try:
|
|
1331
|
+
_load_dotenv(root)
|
|
1332
|
+
assert os.environ.get("WUP_TEST_DUMMY_VAR") == "hello_wup"
|
|
1333
|
+
finally:
|
|
1334
|
+
os.environ.pop("WUP_TEST_DUMMY_VAR", None)
|
|
1335
|
+
if existing is not None:
|
|
1336
|
+
os.environ["WUP_TEST_DUMMY_VAR"] = existing
|
|
1337
|
+
|
|
1338
|
+
def test_load_dotenv_does_not_overwrite_existing(self):
|
|
1339
|
+
"""_load_dotenv must NOT overwrite already-set env vars."""
|
|
1340
|
+
import os
|
|
1341
|
+
from wup.config import _load_dotenv
|
|
1342
|
+
|
|
1343
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
1344
|
+
root = Path(tmpdir)
|
|
1345
|
+
env_file = root / ".wup.env"
|
|
1346
|
+
env_file.write_text('WUP_TEST_NOOVERWRITE=from_file\n', encoding="utf-8")
|
|
1347
|
+
os.environ["WUP_TEST_NOOVERWRITE"] = "original"
|
|
1348
|
+
try:
|
|
1349
|
+
_load_dotenv(root)
|
|
1350
|
+
assert os.environ["WUP_TEST_NOOVERWRITE"] == "original"
|
|
1351
|
+
finally:
|
|
1352
|
+
os.environ.pop("WUP_TEST_NOOVERWRITE", None)
|
|
1353
|
+
|
|
1076
1354
|
|
|
1077
1355
|
class TestConfigIntegration:
|
|
1078
1356
|
"""Tests for configuration integration with WupWatcher."""
|
|
@@ -7,7 +7,7 @@ WUP monitors file changes and runs intelligent regression tests using a 3-layer
|
|
|
7
7
|
3. Detail Layer: Full tests with blame reports (only on failure)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "0.2.
|
|
10
|
+
__version__ = "0.2.15"
|
|
11
11
|
__author__ = "Tom Sapletta"
|
|
12
12
|
|
|
13
13
|
from .config import load_config, save_config, get_default_config
|
|
@@ -183,12 +183,31 @@ def validate_config(raw: dict) -> WupConfig:
|
|
|
183
183
|
|
|
184
184
|
# Parse visual_diff config
|
|
185
185
|
vd_raw = raw.get("visual_diff", {})
|
|
186
|
+
env_visual_enabled = os.environ.get("WUP_VISUAL_DIFF_ENABLED")
|
|
187
|
+
env_visual_delay = os.environ.get("WUP_VISUAL_DIFF_DELAY_SECONDS")
|
|
188
|
+
env_visual_depth = os.environ.get("WUP_VISUAL_DIFF_MAX_DEPTH")
|
|
189
|
+
|
|
190
|
+
if env_visual_enabled is None:
|
|
191
|
+
visual_enabled = vd_raw.get("enabled", False)
|
|
192
|
+
else:
|
|
193
|
+
visual_enabled = env_visual_enabled.strip().lower() in {"1", "true", "yes", "on"}
|
|
194
|
+
|
|
195
|
+
if env_visual_delay is None:
|
|
196
|
+
visual_delay = float(vd_raw.get("delay_seconds", 5.0))
|
|
197
|
+
else:
|
|
198
|
+
visual_delay = float(env_visual_delay)
|
|
199
|
+
|
|
200
|
+
if env_visual_depth is None:
|
|
201
|
+
visual_depth = int(vd_raw.get("max_depth", 10))
|
|
202
|
+
else:
|
|
203
|
+
visual_depth = int(env_visual_depth)
|
|
204
|
+
|
|
186
205
|
visual_diff = VisualDiffConfig(
|
|
187
|
-
enabled=
|
|
206
|
+
enabled=visual_enabled,
|
|
188
207
|
base_url=vd_raw.get("base_url", ""),
|
|
189
208
|
base_url_env=vd_raw.get("base_url_env", "WUP_BASE_URL"),
|
|
190
|
-
delay_seconds=
|
|
191
|
-
max_depth=
|
|
209
|
+
delay_seconds=visual_delay,
|
|
210
|
+
max_depth=visual_depth,
|
|
192
211
|
snapshot_dir=vd_raw.get("snapshot_dir", ".wup/visual-snapshots"),
|
|
193
212
|
diff_dir=vd_raw.get("diff_dir", ".wup/visual-diffs"),
|
|
194
213
|
pages=vd_raw.get("pages", []),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.15
|
|
4
4
|
Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
|
|
5
5
|
Author-email: Tom Sapletta <tom@sapletta.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -29,17 +29,17 @@ Dynamic: license-file
|
|
|
29
29
|
|
|
30
30
|
## AI Cost Tracking
|
|
31
31
|
|
|
32
|
-
    
|
|
33
|
+
  
|
|
34
34
|
|
|
35
|
-
- 🤖 **LLM usage:** $2.
|
|
36
|
-
- 👤 **Human dev:** ~$
|
|
35
|
+
- 🤖 **LLM usage:** $2.4000 (16 commits)
|
|
36
|
+
- 👤 **Human dev:** ~$411 (4.1h @ $100/h, 30min dedup)
|
|
37
37
|
|
|
38
38
|
Generated on 2026-04-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
-
    
|
|
43
43
|
|
|
44
44
|
**WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
|
|
45
45
|
|
|
@@ -285,56 +285,86 @@ export WUP_CPU_THROTTLE=0.5
|
|
|
285
285
|
export WUP_DEBOUNCE=3
|
|
286
286
|
```
|
|
287
287
|
|
|
288
|
-
##
|
|
289
|
-
|
|
290
|
-
WUP
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
288
|
+
## Visual DOM Diff
|
|
289
|
+
|
|
290
|
+
WUP optionally scans configured pages with Playwright after each successful quick test, compares the DOM structure to the previous snapshot, and reports significant changes.
|
|
291
|
+
|
|
292
|
+
### Setup
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
pip install playwright
|
|
296
|
+
playwright install chromium
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Configuration
|
|
300
|
+
|
|
301
|
+
```yaml
|
|
302
|
+
visual_diff:
|
|
303
|
+
enabled: true
|
|
304
|
+
base_url: "http://localhost:8100" # or leave empty and set WUP_BASE_URL env var
|
|
305
|
+
base_url_env: "WUP_BASE_URL"
|
|
306
|
+
delay_seconds: 5.0 # wait after file change before scanning
|
|
307
|
+
max_depth: 10 # DOM snapshot depth
|
|
308
|
+
pages:
|
|
309
|
+
- "/health"
|
|
310
|
+
- "/dashboard"
|
|
311
|
+
pages_from_endpoints: true # also scan endpoints from testql config
|
|
312
|
+
threshold_added: 3 # min node additions to report as "changed"
|
|
313
|
+
threshold_removed: 3
|
|
314
|
+
threshold_changed: 5
|
|
315
|
+
headless: true
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Or set the base URL in `.wup.env` in the project root (not committed to git):
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# .wup.env
|
|
322
|
+
WUP_BASE_URL=http://localhost:8100
|
|
313
323
|
```
|
|
314
324
|
|
|
325
|
+
### Output
|
|
326
|
+
|
|
327
|
+
- **Snapshots** — `.wup/visual-snapshots/<service>/<page>.json`
|
|
328
|
+
- **Diff events** — `.wup/visual-diffs/<service>/<page>.jsonl` (appended on each change)
|
|
329
|
+
|
|
330
|
+
Visible in `wup status` as a "Visual DOM diffs" section.
|
|
331
|
+
|
|
332
|
+
### Graceful degradation
|
|
333
|
+
|
|
334
|
+
If Playwright is not installed, the visual diff module logs a warning and skips scanning — it does **not** break the watcher.
|
|
335
|
+
|
|
315
336
|
## Project Structure
|
|
316
337
|
|
|
317
338
|
```
|
|
318
339
|
wup/
|
|
319
340
|
├── wup/
|
|
320
|
-
│ ├── __init__.py
|
|
321
|
-
│ ├──
|
|
322
|
-
│ ├──
|
|
323
|
-
│
|
|
324
|
-
│
|
|
325
|
-
│ ├──
|
|
326
|
-
│ ├──
|
|
327
|
-
│ ├──
|
|
328
|
-
│ └──
|
|
341
|
+
│ ├── __init__.py # Package exports
|
|
342
|
+
│ ├── cli.py # CLI: watch, map-deps, status, init, testql-endpoints
|
|
343
|
+
│ ├── config.py # Config loading/saving + .wup.env support
|
|
344
|
+
│ ├── core.py # WupWatcher: detection, inference, scheduling
|
|
345
|
+
│ ├── dependency_mapper.py # DependencyMapper: codebase → deps.json
|
|
346
|
+
│ ├── testql_discovery.py # TestQLEndpointDiscovery: scenario parsing
|
|
347
|
+
│ ├── testql_watcher.py # TestQLWatcher: scenario runner + health tracking
|
|
348
|
+
│ ├── visual_diff.py # VisualDiffer: Playwright DOM snapshot + diff engine
|
|
349
|
+
│ └── models/
|
|
350
|
+
│ ├── __init__.py
|
|
351
|
+
│ └── config.py # Dataclasses: WupConfig, VisualDiffConfig, TestQLConfig...
|
|
329
352
|
├── tests/
|
|
330
|
-
│
|
|
353
|
+
│ ├── test_wup.py # unit/integration tests (incl. VisualDiffer, config)
|
|
354
|
+
│ ├── test_testql_watcher.py # TestQLWatcher + VisualDiffer integration tests
|
|
355
|
+
│ └── test_e2e.py # end-to-end CLI tests
|
|
356
|
+
├── examples/
|
|
357
|
+
│ ├── fastapi-app/ # FastAPI example project
|
|
358
|
+
│ ├── flask-app/ # Flask example project
|
|
359
|
+
│ ├── multi-service/ # Multi-service example
|
|
360
|
+
│ ├── testql_demo.py # TestQL simulation demo
|
|
361
|
+
│ ├── testql_integration.py # Custom TestQLWatcher + visual diff example
|
|
362
|
+
│ └── visual_diff_demo.py # Visual DOM diff demo (no Playwright required)
|
|
331
363
|
├── docs/
|
|
332
|
-
│
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
├── pyproject.toml # Package configuration
|
|
337
|
-
└── README.md # This file
|
|
364
|
+
│ └── TESTQL_INTEGRATION.md # TestQL integration guide
|
|
365
|
+
├── testql-scenarios/ # Auto-generated TestQL scenarios
|
|
366
|
+
├── pyproject.toml # Package config (setuptools)
|
|
367
|
+
└── README.md
|
|
338
368
|
```
|
|
339
369
|
|
|
340
370
|
## Development
|
|
@@ -343,23 +373,33 @@ wup/
|
|
|
343
373
|
|
|
344
374
|
```bash
|
|
345
375
|
# Run all tests
|
|
346
|
-
pytest
|
|
376
|
+
python3 -m pytest tests/ -v
|
|
377
|
+
|
|
378
|
+
# Run specific suite
|
|
379
|
+
python3 -m pytest tests/test_wup.py -v
|
|
380
|
+
python3 -m pytest tests/test_testql_watcher.py -v
|
|
347
381
|
|
|
348
382
|
# Run with coverage
|
|
349
|
-
pytest --cov=wup
|
|
383
|
+
python3 -m pytest tests/ --cov=wup
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Examples
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
# Visual diff demo (no Playwright required)
|
|
390
|
+
python3 examples/visual_diff_demo.py
|
|
391
|
+
|
|
392
|
+
# With live page scan (requires playwright)
|
|
393
|
+
python3 examples/visual_diff_demo.py http://localhost:8100/health
|
|
350
394
|
|
|
351
|
-
#
|
|
352
|
-
|
|
395
|
+
# TestQL + visual diff integration
|
|
396
|
+
python3 examples/testql_integration.py /path/to/project
|
|
353
397
|
```
|
|
354
398
|
|
|
355
|
-
### Building
|
|
399
|
+
### Building & Publishing
|
|
356
400
|
|
|
357
401
|
```bash
|
|
358
|
-
# Build wheel and source distribution
|
|
359
402
|
python -m build
|
|
360
|
-
|
|
361
|
-
# Install from dist
|
|
362
|
-
pip install dist/wup-0.1.6-py3-none-any.whl
|
|
363
403
|
```
|
|
364
404
|
|
|
365
405
|
## License
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|