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.
Files changed (43) hide show
  1. {kernhell-0.1.1 → kernhell-0.1.2}/PKG-INFO +12 -7
  2. {kernhell-0.1.1 → kernhell-0.1.2}/README.md +7 -5
  3. {kernhell-0.1.1 → kernhell-0.1.2}/pyproject.toml +5 -2
  4. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/__init__.py +1 -1
  5. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/analyzer/healer.py +6 -6
  6. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/commands/dashboard.py +11 -2
  7. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/commands/heal.py +17 -3
  8. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/scanner/parser.py +37 -20
  9. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/scanner/runner.py +70 -13
  10. {kernhell-0.1.1 → kernhell-0.1.2}/.gitignore +0 -0
  11. {kernhell-0.1.1 → kernhell-0.1.2}/.kernhellrc.toml +0 -0
  12. {kernhell-0.1.1 → kernhell-0.1.2}/LICENSE +0 -0
  13. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/__main__.py +0 -0
  14. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/analyzer/analyzer.py +0 -0
  15. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/analyzer/prompts.py +0 -0
  16. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/api/__init__.py +0 -0
  17. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/api/server.py +0 -0
  18. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cache/__init__.py +0 -0
  19. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cache/cache_manager.py +0 -0
  20. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cache/embedder.py +0 -0
  21. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cache/schemas.py +0 -0
  22. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/__init__.py +0 -0
  23. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/app.py +0 -0
  24. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/commands/__init__.py +0 -0
  25. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/commands/cache.py +0 -0
  26. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/commands/config.py +0 -0
  27. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/commands/scan.py +0 -0
  28. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/cli/ui.py +0 -0
  29. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/core/__init__.py +0 -0
  30. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/core/config.py +0 -0
  31. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/core/exceptions.py +0 -0
  32. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/core/models.py +0 -0
  33. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/core/orchestrator.py +0 -0
  34. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/patcher/patcher.py +0 -0
  35. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/patcher/verifier.py +0 -0
  36. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/pruner/__init__.py +0 -0
  37. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/pruner/dom_pruner.py +0 -0
  38. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/reporter/__init__.py +0 -0
  39. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/reporter/reporter.py +0 -0
  40. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/scanner/__init__.py +0 -0
  41. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/utils/__init__.py +0 -0
  42. {kernhell-0.1.1 → kernhell-0.1.2}/src/kernhell/utils/file_utils.py +0 -0
  43. {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.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<1.0,>=0.3
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
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/)
69
72
  [![Next.js 14](https://img.shields.io/badge/dashboard-Next.js%2014-black.svg)](https://nextjs.org/)
70
73
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
71
- [![CI/CD Status](https://img.shields.io/badge/tests-59%20passed%20(100%25)-brightgreen.svg)](https://github.com/riteshkumar072/KernHell)
74
+ [![CI/CD Status](https://img.shields.io/badge/tests-61%20passed%20(100%25)-brightgreen.svg)](https://github.com/riteshkumar072/KernHell)
75
+ [![TypeScript & Python](https://img.shields.io/badge/playwright-Python%20%7C%20TypeScript-blue.svg)](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[LibCST AST Patcher]
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 LibCST AST Patching (`Patcher`)**: Surgically transforms only the target locator expression in the AST without reformatting unrelated code or stripping inline comments.
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
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/)
8
8
  [![Next.js 14](https://img.shields.io/badge/dashboard-Next.js%2014-black.svg)](https://nextjs.org/)
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
- [![CI/CD Status](https://img.shields.io/badge/tests-59%20passed%20(100%25)-brightgreen.svg)](https://github.com/riteshkumar072/KernHell)
10
+ [![CI/CD Status](https://img.shields.io/badge/tests-61%20passed%20(100%25)-brightgreen.svg)](https://github.com/riteshkumar072/KernHell)
11
+ [![TypeScript & Python](https://img.shields.io/badge/playwright-Python%20%7C%20TypeScript-blue.svg)](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[LibCST AST Patcher]
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 LibCST AST Patching (`Patcher`)**: Surgically transforms only the target locator expression in the AST without reformatting unrelated code or stripping inline comments.
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.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,<1.0",
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
 
@@ -4,5 +4,5 @@ KernHell — AI-Powered Autonomous Self-Healing Testing Engine.
4
4
  "The Heart of Your System Where Innovation Meets Precision"
5
5
  """
6
6
 
7
- __version__ = "0.1.1"
7
+ __version__ = "0.1.2"
8
8
  __app_name__ = "kernhell"
@@ -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
- test_files = sorted(target.rglob("test_*.py"))
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 'test_*.py' files found in {target}[/yellow]")
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+):\s*Timeout\s+\d+ms\s+exceeded",
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*["'](.+?)["']\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*["'](.+?)["'])?\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 traceback lines like: "tests/test_checkout.py:15: TimeoutError"
79
- _PYTEST_LOCATION_PATTERN = re.compile(
80
- r"^(.+\.py):(\d+):\s*(TimeoutError|Error|AssertionError)",
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
- for m in _PYTEST_LOCATION_PATTERN.finditer(error_text):
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 _extract_url_from_test(test_file: Path) -> str | None:
38
- """Extract the first ``page.goto(...)`` URL from a test file.
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
- return m.group(1) if m else None
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 subprocess
95
- error_output = self._run_pytest(test_file)
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 = _extract_url_from_test(test_file)
113
- if page_url:
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
- return failures
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