kernhell 0.1.1__tar.gz → 0.1.2__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.
- {kernhell-0.1.1 → kernhell-0.1.2}/PKG-INFO +12 -7
- {kernhell-0.1.1 → kernhell-0.1.2}/README.md +7 -5
- {kernhell-0.1.1 → kernhell-0.1.2}/pyproject.toml +5 -2
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/__init__.py +1 -1
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/analyzer/healer.py +6 -6
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/commands/dashboard.py +11 -2
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/commands/heal.py +17 -3
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/scanner/parser.py +37 -20
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/scanner/runner.py +70 -13
- {kernhell-0.1.1 → kernhell-0.1.2}/.gitignore +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/.kernhellrc.toml +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/LICENSE +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/__main__.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/analyzer/analyzer.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/analyzer/prompts.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/api/__init__.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/api/server.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cache/__init__.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cache/cache_manager.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cache/embedder.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cache/schemas.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/__init__.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/app.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/commands/__init__.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/commands/cache.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/commands/config.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/commands/scan.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/ui.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/core/__init__.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/core/config.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/core/exceptions.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/core/models.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/core/orchestrator.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/patcher/patcher.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/patcher/verifier.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/pruner/__init__.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/pruner/dom_pruner.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/reporter/__init__.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/reporter/reporter.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/scanner/__init__.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/utils/__init__.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/utils/file_utils.py +0 -0
- {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/utils/logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: kernhell
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: AI-Powered Autonomous Self-Healing Testing Engine — The Heart of Your System Where Innovation Meets Precision
|
|
5
5
|
Project-URL: Homepage, https://github.com/riteshkumar072/KernHell
|
|
6
6
|
Project-URL: Documentation, https://github.com/riteshkumar072/KernHell#readme
|
|
@@ -19,16 +19,19 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
19
19
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
20
|
Classifier: Topic :: Software Development :: Testing
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: fastapi<1.0,>=0.110
|
|
22
23
|
Requires-Dist: libcst>=1.1.0
|
|
23
24
|
Requires-Dist: litellm<2.0,>=1.30
|
|
24
25
|
Requires-Dist: playwright<2.0,>=1.40
|
|
25
26
|
Requires-Dist: pydantic-settings<3.0,>=2.1
|
|
26
27
|
Requires-Dist: pydantic<3.0,>=2.5
|
|
28
|
+
Requires-Dist: pytest<9.0,>=8.0
|
|
27
29
|
Requires-Dist: rich<16.0,>=13.0
|
|
28
|
-
Requires-Dist: selectolax<
|
|
30
|
+
Requires-Dist: selectolax<2.0,>=0.3
|
|
29
31
|
Requires-Dist: structlog>=24.0
|
|
30
32
|
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
31
33
|
Requires-Dist: typer<1.0,>=0.12
|
|
34
|
+
Requires-Dist: uvicorn<1.0,>=0.27
|
|
32
35
|
Provides-Extra: all
|
|
33
36
|
Requires-Dist: aiosqlite<1.0,>=0.19; extra == 'all'
|
|
34
37
|
Requires-Dist: build>=1.0.0; extra == 'all'
|
|
@@ -68,7 +71,8 @@ Description-Content-Type: text/markdown
|
|
|
68
71
|
[](https://www.python.org/)
|
|
69
72
|
[](https://nextjs.org/)
|
|
70
73
|
[](https://opensource.org/licenses/MIT)
|
|
71
|
-
[-brightgreen.svg)](https://github.com/riteshkumar072/KernHell)
|
|
75
|
+
[](https://playwright.dev/)
|
|
72
76
|
|
|
73
77
|
---
|
|
74
78
|
|
|
@@ -76,7 +80,7 @@ Description-Content-Type: text/markdown
|
|
|
76
80
|
|
|
77
81
|
Modern frontend engineering moves fast. When developers refactor classes, IDs, or DOM wrappers (e.g., `#buy-now` becomes `#purchase-btn`), automated Playwright and Selenium test suites fail even though the business functionality works seamlessly. QA teams waste 30–40% of their time manually maintaining broken selectors.
|
|
78
82
|
|
|
79
|
-
**KernHell autonomously resolves broken selectors in real-time, preserving 100% of your code formatting, comments, and QA sanity.**
|
|
83
|
+
**KernHell autonomously resolves broken selectors in real-time across both Python and TypeScript/JavaScript test suites, preserving 100% of your code formatting, comments, and QA sanity.**
|
|
80
84
|
|
|
81
85
|
---
|
|
82
86
|
|
|
@@ -94,7 +98,7 @@ flowchart TD
|
|
|
94
98
|
F -->|RENAME: Safe Refactor| G
|
|
95
99
|
G -->|5. Verify Unambiguous Element| I[Selector Fix: role / data-testid / text]
|
|
96
100
|
I -->|Store Embedding| D
|
|
97
|
-
I -->|6. Surgical Patch| J[
|
|
101
|
+
I -->|6. Surgical Patch| J[Surgical CST & Line Patcher]
|
|
98
102
|
J -->|Preserve Comments & Formatting| K[Automatic Verification Runner]
|
|
99
103
|
K -->|Pass| L[Commit Fix & Log Run]
|
|
100
104
|
K -->|Fail| M[Immediate Rollback from .kernhell.bak]
|
|
@@ -104,12 +108,13 @@ flowchart TD
|
|
|
104
108
|
|
|
105
109
|
## 🚀 Key Features
|
|
106
110
|
|
|
111
|
+
- **Multi-Language Playwright Support**: Seamlessly heals both **Python (`pytest-playwright`)** and **TypeScript / JavaScript (`@playwright/test`)** test suites with zero configuration.
|
|
107
112
|
- **Failure Interception & Live DOM Capture (`Scanner`)**: Intercepts Playwright selector timeouts and crashes, capturing the error stack trace, line number, broken selector, and live DOM snapshot (`page.content()`).
|
|
108
113
|
- **High-Speed Semantic DOM Pruning (`DOMPruner`)**: Strips scripts, styles, SVGs, base64 data, and non-semantic wrappers using `selectolax` to slash token usage by 80–90% while keeping semantic landmarks and accessibility roles.
|
|
109
114
|
- **Semantic Vector Memory (`Vector Cache`)**: 384-dimensional dense cosine index matching broken selectors against historical fixes. Resolves recurring failures in **<5ms** with zero LLM API cost.
|
|
110
115
|
- **Strict Anti-Hallucination Guardrails**: Distinguishes between renamed selectors (`RENAME`) and genuine missing features (`MISSING`) to prevent false-positive heals on actual product regressions.
|
|
111
|
-
- **Surgical
|
|
112
|
-
- **Automated Verification & Zero-Risk Rollback (`Verifier`)**: Re-runs the patched test in a sandbox; if it fails, immediately restores the code from `.kernhell.bak`.
|
|
116
|
+
- **Surgical Code Patching (`Patcher`)**: Surgically transforms only the target locator expression in the AST / file without reformatting unrelated code or stripping inline comments (supports Python LibCST & TypeScript line targeting).
|
|
117
|
+
- **Automated Verification & Zero-Risk Rollback (`Verifier`)**: Re-runs the patched test in a sandbox (via `pytest` or `npx playwright test`); if it fails, immediately restores the code from `.kernhell.bak`.
|
|
113
118
|
- **Mission Control Dashboard (`Next.js App Router`)**: High-tech, dark-mode glassmorphic web dashboard providing live stats, real-time healing stream, semantic memory inspector, and interactive simulation playground.
|
|
114
119
|
|
|
115
120
|
---
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
[](https://www.python.org/)
|
|
8
8
|
[](https://nextjs.org/)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
|
-
[-brightgreen.svg)](https://github.com/riteshkumar072/KernHell)
|
|
11
|
+
[](https://playwright.dev/)
|
|
11
12
|
|
|
12
13
|
---
|
|
13
14
|
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
|
|
16
17
|
Modern frontend engineering moves fast. When developers refactor classes, IDs, or DOM wrappers (e.g., `#buy-now` becomes `#purchase-btn`), automated Playwright and Selenium test suites fail even though the business functionality works seamlessly. QA teams waste 30–40% of their time manually maintaining broken selectors.
|
|
17
18
|
|
|
18
|
-
**KernHell autonomously resolves broken selectors in real-time, preserving 100% of your code formatting, comments, and QA sanity.**
|
|
19
|
+
**KernHell autonomously resolves broken selectors in real-time across both Python and TypeScript/JavaScript test suites, preserving 100% of your code formatting, comments, and QA sanity.**
|
|
19
20
|
|
|
20
21
|
---
|
|
21
22
|
|
|
@@ -33,7 +34,7 @@ flowchart TD
|
|
|
33
34
|
F -->|RENAME: Safe Refactor| G
|
|
34
35
|
G -->|5. Verify Unambiguous Element| I[Selector Fix: role / data-testid / text]
|
|
35
36
|
I -->|Store Embedding| D
|
|
36
|
-
I -->|6. Surgical Patch| J[
|
|
37
|
+
I -->|6. Surgical Patch| J[Surgical CST & Line Patcher]
|
|
37
38
|
J -->|Preserve Comments & Formatting| K[Automatic Verification Runner]
|
|
38
39
|
K -->|Pass| L[Commit Fix & Log Run]
|
|
39
40
|
K -->|Fail| M[Immediate Rollback from .kernhell.bak]
|
|
@@ -43,12 +44,13 @@ flowchart TD
|
|
|
43
44
|
|
|
44
45
|
## 🚀 Key Features
|
|
45
46
|
|
|
47
|
+
- **Multi-Language Playwright Support**: Seamlessly heals both **Python (`pytest-playwright`)** and **TypeScript / JavaScript (`@playwright/test`)** test suites with zero configuration.
|
|
46
48
|
- **Failure Interception & Live DOM Capture (`Scanner`)**: Intercepts Playwright selector timeouts and crashes, capturing the error stack trace, line number, broken selector, and live DOM snapshot (`page.content()`).
|
|
47
49
|
- **High-Speed Semantic DOM Pruning (`DOMPruner`)**: Strips scripts, styles, SVGs, base64 data, and non-semantic wrappers using `selectolax` to slash token usage by 80–90% while keeping semantic landmarks and accessibility roles.
|
|
48
50
|
- **Semantic Vector Memory (`Vector Cache`)**: 384-dimensional dense cosine index matching broken selectors against historical fixes. Resolves recurring failures in **<5ms** with zero LLM API cost.
|
|
49
51
|
- **Strict Anti-Hallucination Guardrails**: Distinguishes between renamed selectors (`RENAME`) and genuine missing features (`MISSING`) to prevent false-positive heals on actual product regressions.
|
|
50
|
-
- **Surgical
|
|
51
|
-
- **Automated Verification & Zero-Risk Rollback (`Verifier`)**: Re-runs the patched test in a sandbox; if it fails, immediately restores the code from `.kernhell.bak`.
|
|
52
|
+
- **Surgical Code Patching (`Patcher`)**: Surgically transforms only the target locator expression in the AST / file without reformatting unrelated code or stripping inline comments (supports Python LibCST & TypeScript line targeting).
|
|
53
|
+
- **Automated Verification & Zero-Risk Rollback (`Verifier`)**: Re-runs the patched test in a sandbox (via `pytest` or `npx playwright test`); if it fails, immediately restores the code from `.kernhell.bak`.
|
|
52
54
|
- **Mission Control Dashboard (`Next.js App Router`)**: High-tech, dark-mode glassmorphic web dashboard providing live stats, real-time healing stream, semantic memory inspector, and interactive simulation playground.
|
|
53
55
|
|
|
54
56
|
---
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kernhell"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "AI-Powered Autonomous Self-Healing Testing Engine — The Heart of Your System Where Innovation Meets Precision"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -29,10 +29,13 @@ dependencies = [
|
|
|
29
29
|
"pydantic>=2.5,<3.0",
|
|
30
30
|
"pydantic-settings>=2.1,<3.0",
|
|
31
31
|
"playwright>=1.40,<2.0",
|
|
32
|
-
"selectolax>=0.3,<
|
|
32
|
+
"selectolax>=0.3,<2.0",
|
|
33
33
|
"libcst>=1.1.0",
|
|
34
34
|
"structlog>=24.0",
|
|
35
35
|
"litellm>=1.30,<2.0",
|
|
36
|
+
"fastapi>=0.110,<1.0",
|
|
37
|
+
"uvicorn>=0.27,<1.0",
|
|
38
|
+
"pytest>=8.0,<9.0",
|
|
36
39
|
"tomli>=2.0;python_version<'3.11'",
|
|
37
40
|
]
|
|
38
41
|
|
|
@@ -52,14 +52,14 @@ def verify_dom_presence(new_selector: str, dom_html: str) -> bool:
|
|
|
52
52
|
|
|
53
53
|
dom_lower = dom_html.lower()
|
|
54
54
|
|
|
55
|
-
# 1. Check get_by_test_id("foo") or [data-testid="foo"]
|
|
56
|
-
testid_match = re.search(r"""(?:get_by_test_id|data-testid[=:])\s*\(?["']?([^"'\)]+)""", new_selector)
|
|
55
|
+
# 1. Check get_by_test_id("foo") or getByTestId("foo") or [data-testid="foo"]
|
|
56
|
+
testid_match = re.search(r"""(?:get_by_test_id|getByTestId|data-testid[=:])\s*\(?["']?([^"'\)]+)""", new_selector)
|
|
57
57
|
if testid_match:
|
|
58
58
|
testid = testid_match.group(1).lower()
|
|
59
59
|
return testid in dom_lower
|
|
60
60
|
|
|
61
|
-
# 2. Check get_by_role("button", name="...")
|
|
62
|
-
role_match = re.search(r"""get_by_role\(\s*["'](\w+)["'](?:\s*,\s*name\s*=\s*["']([^"']+)["'])?""", new_selector)
|
|
61
|
+
# 2. Check get_by_role("button", name="...") or getByRole("button", { name: "..." })
|
|
62
|
+
role_match = re.search(r"""(?:get_by_role|getByRole)\(\s*["'](\w+)["'](?:\s*,\s*(?:\{\s*name:\s*|name\s*=\s*)["']([^"']+)["']\s*\}?)?""", new_selector)
|
|
63
63
|
if role_match:
|
|
64
64
|
role = role_match.group(1).lower()
|
|
65
65
|
name = role_match.group(2).lower() if role_match.group(2) else ""
|
|
@@ -67,8 +67,8 @@ def verify_dom_presence(new_selector: str, dom_html: str) -> bool:
|
|
|
67
67
|
return name in dom_lower and (role in dom_lower or f"<{role}" in dom_lower)
|
|
68
68
|
return role in dom_lower or f"<{role}" in dom_lower
|
|
69
69
|
|
|
70
|
-
# 3. Check get_by_text("...")
|
|
71
|
-
text_match = re.search(r"""get_by_text\(\s*["']([^"']+)["']""", new_selector)
|
|
70
|
+
# 3. Check get_by_text("...") or getByText("...")
|
|
71
|
+
text_match = re.search(r"""(?:get_by_text|getByText)\(\s*["']([^"']+)["']""", new_selector)
|
|
72
72
|
if text_match:
|
|
73
73
|
text = text_match.group(1).lower()
|
|
74
74
|
return text in dom_lower
|
|
@@ -10,11 +10,9 @@ from pathlib import Path
|
|
|
10
10
|
from typing import Optional
|
|
11
11
|
|
|
12
12
|
import typer
|
|
13
|
-
import uvicorn
|
|
14
13
|
from rich.console import Console
|
|
15
14
|
from rich.panel import Panel
|
|
16
15
|
|
|
17
|
-
from kernhell.api.server import create_app
|
|
18
16
|
from kernhell.cli.ui import print_banner
|
|
19
17
|
from kernhell.core.config import load_config
|
|
20
18
|
from kernhell.utils.logger import setup_logging
|
|
@@ -51,5 +49,16 @@ def dashboard_command(
|
|
|
51
49
|
except Exception:
|
|
52
50
|
pass
|
|
53
51
|
|
|
52
|
+
try:
|
|
53
|
+
import uvicorn
|
|
54
|
+
from kernhell.api.server import create_app
|
|
55
|
+
except ImportError:
|
|
56
|
+
console.print(
|
|
57
|
+
"\n[bold red]Error:[/bold red] Mission Control Dashboard requires [bold cyan]fastapi[/bold cyan] and [bold cyan]uvicorn[/bold cyan].\n"
|
|
58
|
+
"Please install them via:\n"
|
|
59
|
+
" [green]pip install 'kernhell[dashboard]'[/green] or [green]pip install fastapi uvicorn[/green]\n"
|
|
60
|
+
)
|
|
61
|
+
raise typer.Exit(code=1)
|
|
62
|
+
|
|
54
63
|
app = create_app(cfg)
|
|
55
64
|
uvicorn.run(app, host=host, port=port, log_level="warning")
|
|
@@ -85,11 +85,25 @@ def heal_command(
|
|
|
85
85
|
console.print(f"[bold red]Error:[/bold red] Target path not found: {target}")
|
|
86
86
|
raise typer.Exit(code=1)
|
|
87
87
|
|
|
88
|
-
# Discover test files (single file vs batch directory mode)
|
|
88
|
+
# Discover test files (single file vs batch directory mode, Python + TypeScript/JS)
|
|
89
89
|
if target.is_dir():
|
|
90
|
-
|
|
90
|
+
candidates = set()
|
|
91
|
+
for p in target.rglob("*"):
|
|
92
|
+
if not p.is_file():
|
|
93
|
+
continue
|
|
94
|
+
name_lower = p.name.lower()
|
|
95
|
+
if (
|
|
96
|
+
(name_lower.startswith("test_") and p.suffix in (".py", ".ts", ".js"))
|
|
97
|
+
or name_lower.endswith("_test.py")
|
|
98
|
+
or name_lower.endswith(".spec.ts")
|
|
99
|
+
or name_lower.endswith(".test.ts")
|
|
100
|
+
or name_lower.endswith(".spec.js")
|
|
101
|
+
or name_lower.endswith(".test.js")
|
|
102
|
+
):
|
|
103
|
+
candidates.add(p)
|
|
104
|
+
test_files = sorted(candidates)
|
|
91
105
|
if not test_files:
|
|
92
|
-
console.print(f"[yellow]No
|
|
106
|
+
console.print(f"[yellow]No Playwright test files (.py, .ts, .js) found in {target}[/yellow]")
|
|
93
107
|
raise typer.Exit(code=0)
|
|
94
108
|
else:
|
|
95
109
|
test_files = [target]
|
|
@@ -27,59 +27,67 @@ logger = structlog.get_logger()
|
|
|
27
27
|
# Regex patterns for Playwright error parsing
|
|
28
28
|
# ---------------------------------------------------------------------------
|
|
29
29
|
|
|
30
|
-
# Matches: "locator.click: Timeout 5000ms exceeded."
|
|
30
|
+
# Matches: "locator.click: Timeout 5000ms exceeded." or "Timed out 5000ms waiting for expect(locator)..."
|
|
31
31
|
_TIMEOUT_PATTERN = re.compile(
|
|
32
|
-
r"(locator\.\w+|page\.\w
|
|
32
|
+
r"(locator\.\w+|page\.\w+|expect\(locator\)):?\s*Timeout\s+\d+ms\s+exceeded|Timed\s+out\s+\d+ms",
|
|
33
33
|
re.IGNORECASE,
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
# Matches: "waiting for locator("button#buy-now")" or 'locator("#buy-now")'
|
|
37
37
|
_LOCATOR_PATTERN = re.compile(
|
|
38
|
-
r"""locator\(\s*["'](.+?)["']
|
|
38
|
+
r"""locator\(\s*["'`]?(.+?)["'`]?\s*\)""",
|
|
39
39
|
re.IGNORECASE,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
# Matches: page.click("#buy-now") or page.fill("input.email", ...)
|
|
42
|
+
# Matches: page.click("#buy-now") or await page.fill("input.email", ...)
|
|
43
43
|
_PAGE_ACTION_PATTERN = re.compile(
|
|
44
|
-
r"""page\.(?:click|fill|type|check|uncheck|select_option|hover|dblclick|focus)\(\s*["'](.+?)["']""",
|
|
44
|
+
r"""(?:await\s+)?page\.(?:click|fill|type|check|uncheck|select_option|selectOption|hover|dblclick|focus)\(\s*["'`](.+?)["'`]""",
|
|
45
45
|
re.IGNORECASE,
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
-
# Matches: page.get_by_role("button", name="Buy Now")
|
|
48
|
+
# Matches: page.get_by_role("button", name="Buy Now") or page.getByRole("button", { name: "Buy Now" })
|
|
49
49
|
_GET_BY_ROLE_PATTERN = re.compile(
|
|
50
|
-
r"""page\.get_by_role\(\s*["'](\w+)["'](?:\s*,\s*name\s*=\s*["'](
|
|
50
|
+
r"""(?:page\.)?(?:get_by_role|getByRole)\(\s*["'](\w+)["'](?:\s*,\s*(?:\{\s*name:\s*|name\s*=\s*)["']([^"']+)["']\s*\}?)?\s*\)""",
|
|
51
51
|
re.IGNORECASE,
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
-
# Matches: page.get_by_text("Buy Now")
|
|
54
|
+
# Matches: page.get_by_text("Buy Now") or page.getByText("Buy Now")
|
|
55
55
|
_GET_BY_TEXT_PATTERN = re.compile(
|
|
56
|
-
r"""page\.get_by_text\(\s*["'](.+?)["']\s*\)""",
|
|
56
|
+
r"""(?:page\.)?(?:get_by_text|getByText)\(\s*["'](.+?)["']\s*\)""",
|
|
57
57
|
re.IGNORECASE,
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
-
# Matches: page.get_by_test_id("checkout-btn")
|
|
60
|
+
# Matches: page.get_by_test_id("checkout-btn") or page.getByTestId("checkout-btn")
|
|
61
61
|
_GET_BY_TESTID_PATTERN = re.compile(
|
|
62
|
-
r"""page\.get_by_test_id\(\s*["'](.+?)["']\s*\)""",
|
|
62
|
+
r"""(?:page\.)?(?:get_by_test_id|getByTestId)\(\s*["'](.+?)["']\s*\)""",
|
|
63
63
|
re.IGNORECASE,
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
-
# Matches: page.get_by_label("Email")
|
|
66
|
+
# Matches: page.get_by_label("Email") or page.getByLabel("Email")
|
|
67
67
|
_GET_BY_LABEL_PATTERN = re.compile(
|
|
68
|
-
r"""page\.get_by_label\(\s*["'](.+?)["']\s*\)""",
|
|
68
|
+
r"""(?:page\.)?(?:get_by_label|getByLabel)\(\s*["'](.+?)["']\s*\)""",
|
|
69
69
|
re.IGNORECASE,
|
|
70
70
|
)
|
|
71
71
|
|
|
72
|
-
# Matches: page.get_by_placeholder("Enter email")
|
|
72
|
+
# Matches: page.get_by_placeholder("Enter email") or page.getByPlaceholder("Enter email")
|
|
73
73
|
_GET_BY_PLACEHOLDER_PATTERN = re.compile(
|
|
74
|
-
r"""page\.get_by_placeholder\(\s*["'](.+?)["']\s*\)""",
|
|
74
|
+
r"""(?:page\.)?(?:get_by_placeholder|getByPlaceholder)\(\s*["'](.+?)["']\s*\)""",
|
|
75
75
|
re.IGNORECASE,
|
|
76
76
|
)
|
|
77
77
|
|
|
78
|
-
# Matches pytest
|
|
79
|
-
|
|
80
|
-
r"^(.+\.py):(\d+)
|
|
78
|
+
# Matches pytest and Playwright TS location lines
|
|
79
|
+
_LOCATION_PATTERN = re.compile(
|
|
80
|
+
r"^(.+\.(?:py|ts|js|tsx|jsx)):(\d+):(?:\d+:)?\s*(TimeoutError|Error|AssertionError)?",
|
|
81
81
|
re.MULTILINE,
|
|
82
82
|
)
|
|
83
|
+
_TS_AT_LOCATION_PATTERN = re.compile(
|
|
84
|
+
r"at\s+(?:.+\s+)?\(?(.+\.(?:ts|js|py|tsx|jsx)):(\d+):(\d+)\)?",
|
|
85
|
+
re.IGNORECASE,
|
|
86
|
+
)
|
|
87
|
+
_TS_LINE_SNIPPET_PATTERN = re.compile(
|
|
88
|
+
r">\s*(\d+)\s*\|\s*(?:await\s+)?(?:page|locator)",
|
|
89
|
+
re.IGNORECASE,
|
|
90
|
+
)
|
|
83
91
|
|
|
84
92
|
# Matches: "strict mode violation" errors
|
|
85
93
|
_STRICT_MODE_PATTERN = re.compile(
|
|
@@ -186,12 +194,21 @@ def _extract_selector(text: str) -> tuple[str, SelectorType]:
|
|
|
186
194
|
|
|
187
195
|
|
|
188
196
|
def _extract_line_number(error_text: str, test_file: Path) -> int:
|
|
189
|
-
"""Extract the failing line number from pytest traceback output."""
|
|
197
|
+
"""Extract the failing line number from pytest or Playwright TS traceback output."""
|
|
190
198
|
filename = test_file.name
|
|
191
|
-
|
|
199
|
+
|
|
200
|
+
for m in _LOCATION_PATTERN.finditer(error_text):
|
|
192
201
|
if filename in m.group(1):
|
|
193
202
|
return int(m.group(2))
|
|
194
203
|
|
|
204
|
+
for m in _TS_AT_LOCATION_PATTERN.finditer(error_text):
|
|
205
|
+
if filename in m.group(1):
|
|
206
|
+
return int(m.group(2))
|
|
207
|
+
|
|
208
|
+
snippet_match = _TS_LINE_SNIPPET_PATTERN.search(error_text)
|
|
209
|
+
if snippet_match:
|
|
210
|
+
return int(snippet_match.group(1))
|
|
211
|
+
|
|
195
212
|
# Fallback: look for any ":<number>:" pattern near the test filename
|
|
196
213
|
fallback = re.search(
|
|
197
214
|
rf"{re.escape(filename)}:(\d+)",
|
|
@@ -34,19 +34,27 @@ from kernhell.scanner.parser import parse_playwright_errors
|
|
|
34
34
|
logger = structlog.get_logger()
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
def
|
|
38
|
-
"""Extract the
|
|
39
|
-
|
|
40
|
-
This is a best-effort heuristic used to re-open the page for DOM capture.
|
|
41
|
-
"""
|
|
37
|
+
def _extract_url_or_content_from_test(test_file: Path) -> tuple[str | None, str | None]:
|
|
38
|
+
"""Extract the page URL or inline page.set_content HTML from a test file."""
|
|
42
39
|
try:
|
|
43
40
|
content = test_file.read_text(encoding="utf-8")
|
|
44
41
|
except Exception:
|
|
45
|
-
return None
|
|
42
|
+
return None, None
|
|
43
|
+
|
|
44
|
+
# Check for inline set_content first: page.set_content('<button...')
|
|
45
|
+
set_content_match = re.search(
|
|
46
|
+
r"""page\.set_content\(\s*(?:r?["']{3}(.+?)["']{3}|r?["'](.+?)["'])\s*\)""",
|
|
47
|
+
content,
|
|
48
|
+
re.DOTALL,
|
|
49
|
+
)
|
|
50
|
+
if set_content_match:
|
|
51
|
+
html = set_content_match.group(1) or set_content_match.group(2)
|
|
52
|
+
return None, html
|
|
46
53
|
|
|
47
54
|
# Match: page.goto("https://example.com") or page.goto('...')
|
|
48
55
|
m = re.search(r"""page\.goto\(\s*["'](.+?)["']""", content)
|
|
49
|
-
|
|
56
|
+
url = m.group(1) if m else None
|
|
57
|
+
return url, None
|
|
50
58
|
|
|
51
59
|
|
|
52
60
|
class ScannerRunner:
|
|
@@ -91,8 +99,8 @@ class ScannerRunner:
|
|
|
91
99
|
|
|
92
100
|
logger.info("scanner.run.start", test_file=str(test_file))
|
|
93
101
|
|
|
94
|
-
# Step 1: Run the test via pytest
|
|
95
|
-
error_output = self.
|
|
102
|
+
# Step 1: Run the test via appropriate runner (pytest for .py, npx playwright test for .ts/.js)
|
|
103
|
+
error_output = self._run_test_process(test_file)
|
|
96
104
|
|
|
97
105
|
if error_output is None:
|
|
98
106
|
raise NoFailuresDetectedError(
|
|
@@ -109,8 +117,11 @@ class ScannerRunner:
|
|
|
109
117
|
)
|
|
110
118
|
|
|
111
119
|
# Step 3: Capture live DOM for each failure
|
|
112
|
-
page_url =
|
|
113
|
-
if
|
|
120
|
+
page_url, inline_dom = _extract_url_or_content_from_test(test_file)
|
|
121
|
+
if inline_dom:
|
|
122
|
+
for f in failures:
|
|
123
|
+
f.live_dom = inline_dom
|
|
124
|
+
elif page_url:
|
|
114
125
|
live_dom = self._capture_live_dom(page_url)
|
|
115
126
|
if live_dom:
|
|
116
127
|
for f in failures:
|
|
@@ -123,7 +134,54 @@ class ScannerRunner:
|
|
|
123
134
|
failures=len(failures),
|
|
124
135
|
has_dom=bool(failures[0].live_dom) if failures else False,
|
|
125
136
|
)
|
|
126
|
-
|
|
137
|
+
def _run_test_process(self, test_file: Path) -> str | None:
|
|
138
|
+
"""Run the test subprocess (Python pytest or TypeScript Playwright test)."""
|
|
139
|
+
if test_file.suffix.lower() in (".ts", ".js", ".tsx", ".jsx", ".mjs", ".cjs"):
|
|
140
|
+
return self._run_playwright_test_ts(test_file)
|
|
141
|
+
return self._run_pytest(test_file)
|
|
142
|
+
|
|
143
|
+
def _run_playwright_test_ts(self, test_file: Path) -> str | None:
|
|
144
|
+
"""Execute a TypeScript/JavaScript Playwright test via npx playwright test."""
|
|
145
|
+
npx_cmd = "npx.cmd" if sys.platform == "win32" else "npx"
|
|
146
|
+
cmd = [
|
|
147
|
+
npx_cmd,
|
|
148
|
+
"playwright",
|
|
149
|
+
"test",
|
|
150
|
+
str(test_file),
|
|
151
|
+
"--reporter=list",
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
logger.debug("scanner.playwright_ts.exec", cmd=" ".join(cmd))
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
result = subprocess.run(
|
|
158
|
+
cmd,
|
|
159
|
+
capture_output=True,
|
|
160
|
+
text=True,
|
|
161
|
+
timeout=self._scanner_cfg.timeout_ms * 3 / 1000,
|
|
162
|
+
cwd=str(test_file.parent),
|
|
163
|
+
shell=(sys.platform == "win32"),
|
|
164
|
+
)
|
|
165
|
+
except FileNotFoundError:
|
|
166
|
+
raise PlaywrightNotInstalledError(
|
|
167
|
+
"npx / Node.js not found on PATH. Please ensure Node.js is installed to run TypeScript Playwright tests."
|
|
168
|
+
)
|
|
169
|
+
except subprocess.TimeoutExpired:
|
|
170
|
+
raise ScannerError(
|
|
171
|
+
f"TypeScript test execution timed out after {self._scanner_cfg.timeout_ms * 3}ms"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if result.returncode == 0:
|
|
175
|
+
logger.info("scanner.playwright_ts.passed", test_file=str(test_file))
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
combined = result.stdout + "\n" + result.stderr
|
|
179
|
+
logger.debug(
|
|
180
|
+
"scanner.playwright_ts.failed",
|
|
181
|
+
exit_code=result.returncode,
|
|
182
|
+
output_len=len(combined),
|
|
183
|
+
)
|
|
184
|
+
return combined
|
|
127
185
|
|
|
128
186
|
def _run_pytest(self, test_file: Path) -> str | None:
|
|
129
187
|
"""Execute pytest on the test file and return error output.
|
|
@@ -138,7 +196,6 @@ class ScannerRunner:
|
|
|
138
196
|
"-v",
|
|
139
197
|
"--tb=long",
|
|
140
198
|
"--no-header",
|
|
141
|
-
f"--timeout={self._scanner_cfg.timeout_ms // 1000}",
|
|
142
199
|
]
|
|
143
200
|
|
|
144
201
|
logger.debug("scanner.pytest.exec", cmd=" ".join(cmd))
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|