""" Discover Playwright tests (.spec.ts files). """ import re from pathlib import Path from typing import Optional from dataclasses import dataclass @dataclass class PlaywrightTestInfo: """Information about a discovered Playwright test.""" id: str name: str file_path: str test_name: str description: Optional[str] = None gherkin_feature: Optional[str] = None gherkin_scenario: Optional[str] = None tags: list[str] = None def __post_init__(self): if self.tags is None: self.tags = [] def discover_playwright_tests(tests_dir: Path) -> list[PlaywrightTestInfo]: """ Discover all Playwright tests in the frontend-tests directory. Parses .spec.ts files to extract: - test() calls - describe() blocks - Gherkin metadata from comments - Tags from comments """ if not tests_dir.exists(): return [] tests = [] # Find all .spec.ts files for spec_file in tests_dir.rglob("*.spec.ts"): relative_path = spec_file.relative_to(tests_dir) # Read file content try: content = spec_file.read_text() except Exception: continue # Extract describe blocks and tests tests_in_file = _parse_playwright_file(content, spec_file, relative_path) tests.extend(tests_in_file) return tests def _parse_playwright_file( content: str, file_path: Path, relative_path: Path ) -> list[PlaywrightTestInfo]: """Parse a Playwright test file to extract test information.""" tests = [] # Pattern to match test() calls # test('test name', async ({ page }) => { ... }) # test.only('test name', ...) test_pattern = re.compile( r"test(?:\.\w+)?\s*\(\s*['\"]([^'\"]+)['\"]", re.MULTILINE ) # Pattern to match describe() blocks describe_pattern = re.compile( r"describe\s*\(\s*['\"]([^'\"]+)['\"]", re.MULTILINE ) # Extract metadata from comments above tests # Looking for JSDoc-style comments with metadata metadata_pattern = re.compile( r"/\*\*\s*\n((?:\s*\*.*\n)+)\s*\*/\s*\n\s*test", re.MULTILINE ) # Find all describe blocks to use as context describes = describe_pattern.findall(content) describe_context = describes[0] if describes else None # Find all tests for match in test_pattern.finditer(content): test_name = match.group(1) # Look for metadata comment before this test # Search backwards from the match position before_test = content[:match.start()] metadata_match = None for m in metadata_pattern.finditer(before_test): metadata_match = m # Parse metadata if found gherkin_feature = None gherkin_scenario = None tags = [] description = None if metadata_match: metadata_block = metadata_match.group(1) # Extract Feature, Scenario, Tags from metadata feature_match = re.search(r"\*\s*Feature:\s*(.+)", metadata_block) scenario_match = re.search(r"\*\s*Scenario:\s*(.+)", metadata_block) tags_match = re.search(r"\*\s*Tags:\s*(.+)", metadata_block) desc_match = re.search(r"\*\s*@description\s+(.+)", metadata_block) if feature_match: gherkin_feature = feature_match.group(1).strip() if scenario_match: gherkin_scenario = scenario_match.group(1).strip() if tags_match: tags_str = tags_match.group(1).strip() tags = [t.strip() for t in re.findall(r"@[\w-]+", tags_str)] if desc_match: description = desc_match.group(1).strip() # Build test ID module_name = str(relative_path).replace("/", ".").replace(".spec.ts", "") test_id = f"frontend.{module_name}.{_sanitize_test_name(test_name)}" tests.append(PlaywrightTestInfo( id=test_id, name=test_name, file_path=str(relative_path), test_name=test_name, description=description or test_name, gherkin_feature=gherkin_feature, gherkin_scenario=gherkin_scenario, tags=tags, )) return tests def _sanitize_test_name(name: str) -> str: """Convert test name to a valid identifier.""" # Replace spaces and special chars with underscores sanitized = re.sub(r"[^\w]+", "_", name.lower()) # Remove leading/trailing underscores sanitized = sanitized.strip("_") return sanitized