nat-engine 1__py3-none-any.whl
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.
- mannf/__init__.py +33 -0
- mannf/__main__.py +10 -0
- mannf/_version.py +8 -0
- mannf/agents/__init__.py +7 -0
- mannf/agents/analyzer_agent.py +9 -0
- mannf/agents/base.py +9 -0
- mannf/agents/bdi_agent.py +9 -0
- mannf/agents/belief_state.py +9 -0
- mannf/agents/coordinator_agent.py +9 -0
- mannf/agents/executor_agent.py +9 -0
- mannf/agents/monitor_agent.py +9 -0
- mannf/agents/oracle_agent.py +9 -0
- mannf/agents/planner_agent.py +9 -0
- mannf/agents/test_agent.py +9 -0
- mannf/anomaly/__init__.py +7 -0
- mannf/anomaly/enhanced_detector.py +9 -0
- mannf/cli.py +9 -0
- mannf/core/__init__.py +26 -0
- mannf/core/agents/__init__.py +52 -0
- mannf/core/agents/accessibility_scanner_agent.py +245 -0
- mannf/core/agents/analyzer_agent.py +224 -0
- mannf/core/agents/autonomous_loop_agent.py +1086 -0
- mannf/core/agents/autonomous_loop_models.py +62 -0
- mannf/core/agents/autonomous_run_differ.py +427 -0
- mannf/core/agents/base.py +128 -0
- mannf/core/agents/bdi_agent.py +330 -0
- mannf/core/agents/belief_state.py +202 -0
- mannf/core/agents/browser_coordinator_agent.py +224 -0
- mannf/core/agents/browser_executor_agent.py +410 -0
- mannf/core/agents/coordinator_agent.py +262 -0
- mannf/core/agents/executor_agent.py +222 -0
- mannf/core/agents/monitor_agent.py +188 -0
- mannf/core/agents/oracle_agent.py +150 -0
- mannf/core/agents/performance_testing_agent.py +279 -0
- mannf/core/agents/planner_agent.py +128 -0
- mannf/core/agents/test_agent.py +249 -0
- mannf/core/agents/visual_regression_agent.py +311 -0
- mannf/core/agents/web_crawler_agent.py +510 -0
- mannf/core/agents/worker_pool.py +366 -0
- mannf/core/anomaly/__init__.py +14 -0
- mannf/core/anomaly/enhanced_detector.py +541 -0
- mannf/core/browser/__init__.py +63 -0
- mannf/core/browser/accessibility_scanner.py +424 -0
- mannf/core/browser/discovery_model.py +178 -0
- mannf/core/browser/dom_snapshot.py +349 -0
- mannf/core/browser/ingestor_bridge.py +371 -0
- mannf/core/browser/performance_metrics.py +217 -0
- mannf/core/browser/reflection_analyzer.py +442 -0
- mannf/core/browser/scenario_generator.py +1100 -0
- mannf/core/browser/security_scenario_generator.py +695 -0
- mannf/core/browser/visual_comparer.py +159 -0
- mannf/core/diagnostics/__init__.py +28 -0
- mannf/core/diagnostics/failure_clusterer.py +211 -0
- mannf/core/diagnostics/flake_detector.py +233 -0
- mannf/core/diagnostics/root_cause_analyzer.py +273 -0
- mannf/core/distributed/__init__.py +16 -0
- mannf/core/distributed/endpoint.py +139 -0
- mannf/core/distributed/system_under_test.py +207 -0
- mannf/core/functional_orchestrator.py +428 -0
- mannf/core/messaging/__init__.py +11 -0
- mannf/core/messaging/bus.py +113 -0
- mannf/core/messaging/messages.py +89 -0
- mannf/core/nat_orchestrator.py +342 -0
- mannf/core/neural/__init__.py +183 -0
- mannf/core/orchestrator.py +272 -0
- mannf/core/prioritization/__init__.py +17 -0
- mannf/core/prioritization/adaptive_controller.py +509 -0
- mannf/core/prioritization/belief_prioritizer.py +231 -0
- mannf/core/prioritization/risk_scorer.py +430 -0
- mannf/core/reporting/__init__.py +12 -0
- mannf/core/reporting/unified_report.py +664 -0
- mannf/core/testing/__init__.py +17 -0
- mannf/core/testing/adaptive_controller.py +149 -0
- mannf/core/testing/models.py +179 -0
- mannf/core/validation/__init__.py +10 -0
- mannf/core/validation/self_validation_runner.py +180 -0
- mannf/dashboard/__init__.py +7 -0
- mannf/dashboard/app.py +9 -0
- mannf/dashboard/models.py +9 -0
- mannf/dashboard/static/index.html +2538 -0
- mannf/dashboard/telemetry.py +9 -0
- mannf/distributed/__init__.py +7 -0
- mannf/distributed/endpoint.py +9 -0
- mannf/distributed/system_under_test.py +9 -0
- mannf/healing/__init__.py +7 -0
- mannf/healing/graphql_schema_diff.py +9 -0
- mannf/healing/healer.py +9 -0
- mannf/healing/models.py +9 -0
- mannf/healing/schema_diff.py +9 -0
- mannf/integrations/__init__.py +7 -0
- mannf/integrations/auth.py +9 -0
- mannf/integrations/graphql_parser.py +9 -0
- mannf/integrations/graphql_sut.py +9 -0
- mannf/integrations/http_sut.py +9 -0
- mannf/integrations/openapi_parser.py +9 -0
- mannf/integrations/postman_parser.py +9 -0
- mannf/llm/__init__.py +7 -0
- mannf/llm/anthropic_provider.py +9 -0
- mannf/llm/base.py +9 -0
- mannf/llm/config.py +9 -0
- mannf/llm/factory.py +9 -0
- mannf/llm/openai_provider.py +9 -0
- mannf/llm/prompts.py +9 -0
- mannf/messaging/__init__.py +7 -0
- mannf/messaging/bus.py +9 -0
- mannf/messaging/messages.py +9 -0
- mannf/nat_orchestrator.py +9 -0
- mannf/neural/__init__.py +7 -0
- mannf/orchestrator.py +9 -0
- mannf/prioritization/__init__.py +7 -0
- mannf/prioritization/adaptive_controller.py +9 -0
- mannf/prioritization/belief_prioritizer.py +9 -0
- mannf/prioritization/risk_scorer.py +9 -0
- mannf/product/__init__.py +29 -0
- mannf/product/admin/__init__.py +3 -0
- mannf/product/admin/routes.py +514 -0
- mannf/product/auth/__init__.py +5 -0
- mannf/product/auth/saml.py +212 -0
- mannf/product/billing/__init__.py +5 -0
- mannf/product/billing/audit.py +160 -0
- mannf/product/billing/feature_gates.py +180 -0
- mannf/product/billing/metering.py +179 -0
- mannf/product/billing/notifications.py +181 -0
- mannf/product/billing/plans.py +133 -0
- mannf/product/billing/rate_limits.py +35 -0
- mannf/product/billing/stripe_billing.py +906 -0
- mannf/product/billing/tenant_auth.py +233 -0
- mannf/product/billing/tenant_manager.py +873 -0
- mannf/product/cli.py +3900 -0
- mannf/product/cli_admin.py +408 -0
- mannf/product/dashboard/__init__.py +61 -0
- mannf/product/dashboard/app.py +3567 -0
- mannf/product/dashboard/models.py +460 -0
- mannf/product/dashboard/static/index.html +6347 -0
- mannf/product/dashboard/static/manifest.json +25 -0
- mannf/product/dashboard/static/pwa-icon-192.png +0 -0
- mannf/product/dashboard/static/pwa-icon-512.png +0 -0
- mannf/product/dashboard/static/sw.js +64 -0
- mannf/product/dashboard/telemetry.py +547 -0
- mannf/product/database.py +145 -0
- mannf/product/demo.py +844 -0
- mannf/product/doctor.py +509 -0
- mannf/product/exporters/__init__.py +65 -0
- mannf/product/exporters/azuredevops_exporter.py +257 -0
- mannf/product/exporters/base.py +307 -0
- mannf/product/exporters/bugzilla_exporter.py +200 -0
- mannf/product/exporters/dedup.py +275 -0
- mannf/product/exporters/finding_adapter.py +216 -0
- mannf/product/exporters/github_exporter.py +197 -0
- mannf/product/exporters/gitlab_exporter.py +215 -0
- mannf/product/exporters/jira_exporter.py +180 -0
- mannf/product/exporters/linear_exporter.py +195 -0
- mannf/product/exporters/loader.py +233 -0
- mannf/product/exporters/pagerduty_exporter.py +363 -0
- mannf/product/exporters/sentry_exporter.py +322 -0
- mannf/product/exporters/servicenow_exporter.py +240 -0
- mannf/product/exporters/shortcut_exporter.py +231 -0
- mannf/product/exporters/webhook_exporter.py +383 -0
- mannf/product/formatters/__init__.py +18 -0
- mannf/product/formatters/allure_formatter.py +161 -0
- mannf/product/formatters/ctrf_formatter.py +149 -0
- mannf/product/healing/__init__.py +30 -0
- mannf/product/healing/graphql_schema_diff.py +152 -0
- mannf/product/healing/healer.py +141 -0
- mannf/product/healing/models.py +175 -0
- mannf/product/healing/schema_diff.py +251 -0
- mannf/product/ingestors/__init__.py +77 -0
- mannf/product/ingestors/base.py +256 -0
- mannf/product/ingestors/bgstm_ingestor.py +764 -0
- mannf/product/ingestors/curl_ingestor.py +1019 -0
- mannf/product/ingestors/cypress_ingestor.py +487 -0
- mannf/product/ingestors/gherkin_ingestor.py +967 -0
- mannf/product/ingestors/graphql_ingestor.py +845 -0
- mannf/product/ingestors/grpc_ingestor.py +591 -0
- mannf/product/ingestors/har_ingestor.py +976 -0
- mannf/product/ingestors/loader.py +284 -0
- mannf/product/ingestors/models.py +146 -0
- mannf/product/ingestors/openapi_ingestor.py +606 -0
- mannf/product/ingestors/playwright_ingestor.py +449 -0
- mannf/product/ingestors/postman_ingestor.py +631 -0
- mannf/product/ingestors/traffic_ingestor.py +679 -0
- mannf/product/ingestors/websocket_ingestor.py +526 -0
- mannf/product/integrations/__init__.py +21 -0
- mannf/product/integrations/auth.py +190 -0
- mannf/product/integrations/graphql_parser.py +436 -0
- mannf/product/integrations/graphql_sut.py +247 -0
- mannf/product/integrations/grpc_sut.py +469 -0
- mannf/product/integrations/http_sut.py +237 -0
- mannf/product/integrations/kafka_adapter.py +342 -0
- mannf/product/integrations/openapi_parser.py +513 -0
- mannf/product/integrations/postman_parser.py +467 -0
- mannf/product/integrations/webhook_receiver.py +344 -0
- mannf/product/integrations/websocket_sut.py +434 -0
- mannf/product/llm/__init__.py +25 -0
- mannf/product/llm/anthropic_provider.py +94 -0
- mannf/product/llm/base.py +267 -0
- mannf/product/llm/config.py +48 -0
- mannf/product/llm/factory.py +42 -0
- mannf/product/llm/openai_provider.py +93 -0
- mannf/product/llm/prompts.py +403 -0
- mannf/product/llm/root_cause_service.py +311 -0
- mannf/product/llm/test_plan_models.py +78 -0
- mannf/product/metrics.py +149 -0
- mannf/product/middleware/__init__.py +3 -0
- mannf/product/middleware/audit_middleware.py +112 -0
- mannf/product/middleware/tenant_isolation.py +114 -0
- mannf/product/models.py +347 -0
- mannf/product/notifications/__init__.py +24 -0
- mannf/product/notifications/dispatcher.py +411 -0
- mannf/product/onboarding.py +190 -0
- mannf/product/orchestration/__init__.py +39 -0
- mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
- mannf/product/orchestration/pipeline.py +401 -0
- mannf/product/orchestrator.py +987 -0
- mannf/product/orchestrator_models.py +269 -0
- mannf/product/regression/__init__.py +36 -0
- mannf/product/regression/differ.py +172 -0
- mannf/product/regression/masking.py +100 -0
- mannf/product/regression/models.py +232 -0
- mannf/product/regression/recorder.py +124 -0
- mannf/product/regression/replayer.py +168 -0
- mannf/product/reports/__init__.py +10 -0
- mannf/product/reports/pdf.py +132 -0
- mannf/product/scheduling/__init__.py +57 -0
- mannf/product/scheduling/cron_utils.py +251 -0
- mannf/product/scheduling/engine.py +473 -0
- mannf/product/scheduling/models.py +86 -0
- mannf/product/scheduling/queue.py +894 -0
- mannf/product/scheduling/store.py +235 -0
- mannf/product/security/__init__.py +21 -0
- mannf/product/security/belief_guided.py +143 -0
- mannf/product/security/checks/__init__.py +55 -0
- mannf/product/security/checks/base.py +69 -0
- mannf/product/security/checks/bfla.py +77 -0
- mannf/product/security/checks/bola.py +77 -0
- mannf/product/security/checks/bopla.py +80 -0
- mannf/product/security/checks/broken_auth.py +86 -0
- mannf/product/security/checks/graphql_security.py +299 -0
- mannf/product/security/checks/inventory.py +70 -0
- mannf/product/security/checks/misconfig.py +158 -0
- mannf/product/security/checks/resource_consumption.py +70 -0
- mannf/product/security/checks/sensitive_flows.py +80 -0
- mannf/product/security/checks/ssrf.py +101 -0
- mannf/product/security/checks/unsafe_consumption.py +120 -0
- mannf/product/security/models.py +92 -0
- mannf/product/security/plugin_loader.py +182 -0
- mannf/product/security/reporter.py +92 -0
- mannf/product/security/scanner.py +183 -0
- mannf/product/server.py +6220 -0
- mannf/product/setup_wizard.py +873 -0
- mannf/product/status.py +404 -0
- mannf/product/storage/__init__.py +10 -0
- mannf/product/storage/artifact_store.py +343 -0
- mannf/product/telemetry.py +300 -0
- mannf/product/uninstall.py +169 -0
- mannf/product/upgrade.py +139 -0
- mannf/product/weights/__init__.py +13 -0
- mannf/product/weights/blob_store.py +299 -0
- mannf/product/weights/factory.py +42 -0
- mannf/product/weights/registry.py +159 -0
- mannf/product/weights/store.py +210 -0
- mannf/regression/__init__.py +7 -0
- mannf/regression/differ.py +9 -0
- mannf/regression/masking.py +9 -0
- mannf/regression/models.py +9 -0
- mannf/regression/recorder.py +9 -0
- mannf/regression/replayer.py +9 -0
- mannf/security/__init__.py +7 -0
- mannf/security/belief_guided.py +9 -0
- mannf/security/checks/__init__.py +7 -0
- mannf/security/checks/base.py +9 -0
- mannf/security/checks/bfla.py +9 -0
- mannf/security/checks/bola.py +9 -0
- mannf/security/checks/bopla.py +9 -0
- mannf/security/checks/broken_auth.py +9 -0
- mannf/security/checks/graphql_security.py +9 -0
- mannf/security/checks/inventory.py +9 -0
- mannf/security/checks/misconfig.py +9 -0
- mannf/security/checks/resource_consumption.py +9 -0
- mannf/security/checks/sensitive_flows.py +9 -0
- mannf/security/checks/ssrf.py +9 -0
- mannf/security/checks/unsafe_consumption.py +9 -0
- mannf/security/models.py +9 -0
- mannf/security/reporter.py +9 -0
- mannf/security/scanner.py +9 -0
- mannf/server.py +9 -0
- mannf/testing/__init__.py +7 -0
- mannf/testing/adaptive_controller.py +9 -0
- mannf/testing/models.py +9 -0
- mannf/weights/__init__.py +7 -0
- mannf/weights/registry.py +9 -0
- mannf/weights/store.py +9 -0
- nat_engine-1.dist-info/METADATA +555 -0
- nat_engine-1.dist-info/RECORD +299 -0
- nat_engine-1.dist-info/WHEEL +5 -0
- nat_engine-1.dist-info/entry_points.txt +4 -0
- nat_engine-1.dist-info/licenses/LICENSE +651 -0
- nat_engine-1.dist-info/licenses/NOTICE +178 -0
- nat_engine-1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
# Copyright (C) 2026 Brad Guider
|
|
2
|
+
# This file is part of NAT (Neural Agent Testing Framework).
|
|
3
|
+
# Licensed under the AGPL-3.0. See LICENSE for details.
|
|
4
|
+
# Commercial licensing available — see COMMERCIAL_LICENSE.md.
|
|
5
|
+
|
|
6
|
+
"""Playwright test-file ingestor plugin.
|
|
7
|
+
|
|
8
|
+
Parses Playwright ``.spec.ts`` / ``.spec.js`` test files using regex-based
|
|
9
|
+
extraction (no AST or Node.js runtime required) and converts the discovered
|
|
10
|
+
test actions into :class:`~mannf.product.ingestors.models.IngestedTestCase`
|
|
11
|
+
objects that match the :class:`~mannf.core.functional_orchestrator.FunctionalTestOrchestrator`
|
|
12
|
+
task shape.
|
|
13
|
+
|
|
14
|
+
Supported Playwright patterns
|
|
15
|
+
------------------------------
|
|
16
|
+
* ``test('name', async ({ page }) => { ... })`` — top-level test blocks
|
|
17
|
+
* ``page.goto('url')`` / ``page.goto("url")``
|
|
18
|
+
* ``page.fill('selector', 'value')``
|
|
19
|
+
* ``page.type('selector', 'value')``
|
|
20
|
+
* ``page.click('selector')``
|
|
21
|
+
* ``page.locator('selector').click()``
|
|
22
|
+
* ``page.locator('selector').fill('value')``
|
|
23
|
+
* ``page.getByRole('role', { name: 'text' }).click()``
|
|
24
|
+
* ``page.getByText('text').click()``
|
|
25
|
+
* ``page.getByLabel('label').fill('value')``
|
|
26
|
+
* ``page.getByPlaceholder('ph').fill('value')``
|
|
27
|
+
* ``page.getByTestId('id').click()``
|
|
28
|
+
* ``expect(page).toHaveURL('url')`` — captured as assert steps
|
|
29
|
+
* ``expect(locator).toBeVisible()`` / ``toBeEnabled()`` etc.
|
|
30
|
+
* ``test.describe('suite', () => { ... })`` — nested describe blocks
|
|
31
|
+
|
|
32
|
+
The extracted tests are tagged with ``["playwright"]`` and their source
|
|
33
|
+
file reference is set to ``file.spec.ts::<test_name>``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import logging
|
|
39
|
+
import re
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
from mannf.product.ingestors.base import IngestorPlugin
|
|
44
|
+
from mannf.product.ingestors.models import (
|
|
45
|
+
IngestedTestCase,
|
|
46
|
+
IngestorSource,
|
|
47
|
+
IngestResult,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Regex patterns
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
# test('name', ...) or test("name", ...) or test(`name`, ...)
|
|
57
|
+
_RE_TEST_BLOCK = re.compile(
|
|
58
|
+
r"""test\s*\(\s*(['"`])(.*?)\1""",
|
|
59
|
+
re.DOTALL,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# test.describe('suite name', ...) or test.describe("suite name", ...)
|
|
63
|
+
_RE_DESCRIBE_BLOCK = re.compile(
|
|
64
|
+
r"""test\.describe\s*\(\s*(['"`])(.*?)\1""",
|
|
65
|
+
re.DOTALL,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# page.goto('url') / page.goto("url") / page.goto(`url`)
|
|
69
|
+
_RE_GOTO = re.compile(
|
|
70
|
+
r"""page\.goto\s*\(\s*(['"`])(.*?)\1""",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# page.fill('selector', 'value') or page.fill("sel", "val")
|
|
74
|
+
_RE_FILL = re.compile(
|
|
75
|
+
r"""page\.fill\s*\(\s*(['"`])(.*?)\1\s*,\s*(['"`])(.*?)\3\s*\)""",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# page.type('selector', 'value')
|
|
79
|
+
_RE_TYPE = re.compile(
|
|
80
|
+
r"""page\.type\s*\(\s*(['"`])(.*?)\1\s*,\s*(['"`])(.*?)\3\s*\)""",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# page.click('selector')
|
|
84
|
+
_RE_CLICK = re.compile(
|
|
85
|
+
r"""page\.click\s*\(\s*(['"`])(.*?)\1\s*\)""",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# page.locator('selector').click()
|
|
89
|
+
_RE_LOCATOR_CLICK = re.compile(
|
|
90
|
+
r"""page\.locator\s*\(\s*(['"`])(.*?)\1\s*\)\.click\s*\(\s*\)""",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# page.locator('selector').fill('value')
|
|
94
|
+
_RE_LOCATOR_FILL = re.compile(
|
|
95
|
+
r"""page\.locator\s*\(\s*(['"`])(.*?)\1\s*\)\.fill\s*\(\s*(['"`])(.*?)\3\s*\)""",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# page.getByRole('role', { name: 'label' }).click() or .fill('value')
|
|
99
|
+
_RE_GET_BY_ROLE = re.compile(
|
|
100
|
+
r"""page\.getByRole\s*\(\s*(['"`])(.*?)\1(?:\s*,\s*\{\s*name\s*:\s*(['"`])(.*?)\3\s*\})?\s*\)\.(click|fill)\s*\((?:\s*(['"`])(.*?)\6\s*)?\)""",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# page.getByText('text').click()
|
|
104
|
+
_RE_GET_BY_TEXT_CLICK = re.compile(
|
|
105
|
+
r"""page\.getByText\s*\(\s*(['"`])(.*?)\1\s*\)\.click\s*\(\s*\)""",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# page.getByLabel('label').fill('value')
|
|
109
|
+
_RE_GET_BY_LABEL_FILL = re.compile(
|
|
110
|
+
r"""page\.getByLabel\s*\(\s*(['"`])(.*?)\1\s*\)\.fill\s*\(\s*(['"`])(.*?)\3\s*\)""",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# page.getByLabel('label').click()
|
|
114
|
+
_RE_GET_BY_LABEL_CLICK = re.compile(
|
|
115
|
+
r"""page\.getByLabel\s*\(\s*(['"`])(.*?)\1\s*\)\.click\s*\(\s*\)""",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# page.getByPlaceholder('placeholder').fill('value')
|
|
119
|
+
_RE_GET_BY_PLACEHOLDER_FILL = re.compile(
|
|
120
|
+
r"""page\.getByPlaceholder\s*\(\s*(['"`])(.*?)\1\s*\)\.fill\s*\(\s*(['"`])(.*?)\3\s*\)""",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# page.getByTestId('testId').click()
|
|
124
|
+
_RE_GET_BY_TESTID_CLICK = re.compile(
|
|
125
|
+
r"""page\.getByTestId\s*\(\s*(['"`])(.*?)\1\s*\)\.click\s*\(\s*\)""",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# page.getByTestId('testId').fill('value')
|
|
129
|
+
_RE_GET_BY_TESTID_FILL = re.compile(
|
|
130
|
+
r"""page\.getByTestId\s*\(\s*(['"`])(.*?)\1\s*\)\.fill\s*\(\s*(['"`])(.*?)\3\s*\)""",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# expect(page).toHaveURL('url')
|
|
134
|
+
_RE_EXPECT_URL = re.compile(
|
|
135
|
+
r"""expect\s*\(\s*page\s*\)\.toHaveURL\s*\(\s*(['"`])(.*?)\1\s*\)""",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# expect(locator).toBeVisible() / toBeEnabled() / toHaveText('...') etc.
|
|
139
|
+
_RE_EXPECT_VISIBLE = re.compile(
|
|
140
|
+
r"""expect\s*\(.*?\)\.(toBeVisible|toBeEnabled|toBeDisabled|toBeChecked|toHaveText|toHaveValue|toBeHidden)\s*\(([^)]*)\)""",
|
|
141
|
+
re.DOTALL,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Priority heuristics
|
|
145
|
+
_HIGH_PRIORITY_KEYWORDS: tuple[str, ...] = (
|
|
146
|
+
"login", "auth", "register", "checkout", "payment", "critical",
|
|
147
|
+
)
|
|
148
|
+
_NEGATIVE_KEYWORDS: tuple[str, ...] = (
|
|
149
|
+
"invalid", "error", "fail", "negative", "reject", "wrong",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
# Helper — extract test blocks from source
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _extract_test_blocks(content: str) -> list[tuple[str, str]]:
|
|
158
|
+
"""Return a list of (test_name, test_body) pairs from *content*.
|
|
159
|
+
|
|
160
|
+
This uses a simple brace-counting approach to extract the body of each
|
|
161
|
+
``test('name', ...)`` call so that we can apply per-test regex extraction
|
|
162
|
+
without one test's steps leaking into another.
|
|
163
|
+
"""
|
|
164
|
+
results: list[tuple[str, str]] = []
|
|
165
|
+
|
|
166
|
+
for m in _RE_TEST_BLOCK.finditer(content):
|
|
167
|
+
name = m.group(2)
|
|
168
|
+
# Find the opening '{' of the arrow function body.
|
|
169
|
+
# We skip past arrow functions like `async ({ page }) => {` by looking
|
|
170
|
+
# for `=> {` rather than the first bare `{` (which could be the argument
|
|
171
|
+
# destructuring `({ page })`).
|
|
172
|
+
pos = m.end()
|
|
173
|
+
arrow_match = content.find("=> {", pos)
|
|
174
|
+
if arrow_match != -1:
|
|
175
|
+
brace_pos = content.find("{", arrow_match)
|
|
176
|
+
else:
|
|
177
|
+
# Fallback: find the first '{' after the match
|
|
178
|
+
brace_pos = content.find("{", pos)
|
|
179
|
+
if brace_pos == -1:
|
|
180
|
+
continue
|
|
181
|
+
# Count braces to find the matching '}'
|
|
182
|
+
depth = 0
|
|
183
|
+
end_pos = brace_pos
|
|
184
|
+
for i, ch in enumerate(content[brace_pos:], start=brace_pos):
|
|
185
|
+
if ch == "{":
|
|
186
|
+
depth += 1
|
|
187
|
+
elif ch == "}":
|
|
188
|
+
depth -= 1
|
|
189
|
+
if depth == 0:
|
|
190
|
+
end_pos = i
|
|
191
|
+
break
|
|
192
|
+
body = content[brace_pos : end_pos + 1]
|
|
193
|
+
results.append((name, body))
|
|
194
|
+
|
|
195
|
+
return results
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _extract_describe_name(content: str, pos: int) -> str | None:
|
|
199
|
+
"""Return the ``test.describe`` suite name that contains position *pos*."""
|
|
200
|
+
for m in _RE_DESCRIBE_BLOCK.finditer(content):
|
|
201
|
+
if m.start() <= pos:
|
|
202
|
+
return m.group(2)
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
# Step extractors
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _extract_steps(body: str) -> list[dict[str, Any]]:
|
|
212
|
+
"""Extract all recognisable browser actions from *body* as step dicts."""
|
|
213
|
+
steps: list[dict[str, Any]] = []
|
|
214
|
+
|
|
215
|
+
# page.goto
|
|
216
|
+
for m in _RE_GOTO.finditer(body):
|
|
217
|
+
steps.append(
|
|
218
|
+
{"action": "navigate", "selector": m.group(2), "url": m.group(2), "_pos": m.start()}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# page.fill
|
|
222
|
+
for m in _RE_FILL.finditer(body):
|
|
223
|
+
steps.append(
|
|
224
|
+
{"action": "fill", "selector": m.group(2), "value": m.group(4), "_pos": m.start()}
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# page.type
|
|
228
|
+
for m in _RE_TYPE.finditer(body):
|
|
229
|
+
steps.append(
|
|
230
|
+
{"action": "fill", "selector": m.group(2), "value": m.group(4), "_pos": m.start()}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# page.click
|
|
234
|
+
for m in _RE_CLICK.finditer(body):
|
|
235
|
+
steps.append(
|
|
236
|
+
{"action": "click", "selector": m.group(2), "_pos": m.start()}
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# page.locator().click()
|
|
240
|
+
for m in _RE_LOCATOR_CLICK.finditer(body):
|
|
241
|
+
steps.append(
|
|
242
|
+
{"action": "click", "selector": m.group(2), "_pos": m.start()}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# page.locator().fill()
|
|
246
|
+
for m in _RE_LOCATOR_FILL.finditer(body):
|
|
247
|
+
steps.append(
|
|
248
|
+
{"action": "fill", "selector": m.group(2), "value": m.group(4), "_pos": m.start()}
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# page.getByRole().click() or .fill()
|
|
252
|
+
for m in _RE_GET_BY_ROLE.finditer(body):
|
|
253
|
+
role = m.group(2)
|
|
254
|
+
label = m.group(4) or ""
|
|
255
|
+
action_name = m.group(5)
|
|
256
|
+
value = m.group(7) or ""
|
|
257
|
+
selector = f"[role={role}]" + (f"[name='{label}']" if label else "")
|
|
258
|
+
if action_name == "click":
|
|
259
|
+
steps.append({"action": "click", "selector": selector, "_pos": m.start()})
|
|
260
|
+
else:
|
|
261
|
+
steps.append({"action": "fill", "selector": selector, "value": value, "_pos": m.start()})
|
|
262
|
+
|
|
263
|
+
# page.getByText().click()
|
|
264
|
+
for m in _RE_GET_BY_TEXT_CLICK.finditer(body):
|
|
265
|
+
steps.append(
|
|
266
|
+
{"action": "click", "selector": f"text={m.group(2)}", "_pos": m.start()}
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# page.getByLabel().fill()
|
|
270
|
+
for m in _RE_GET_BY_LABEL_FILL.finditer(body):
|
|
271
|
+
steps.append(
|
|
272
|
+
{"action": "fill", "selector": f"label={m.group(2)}", "value": m.group(4), "_pos": m.start()}
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# page.getByLabel().click()
|
|
276
|
+
for m in _RE_GET_BY_LABEL_CLICK.finditer(body):
|
|
277
|
+
steps.append(
|
|
278
|
+
{"action": "click", "selector": f"label={m.group(2)}", "_pos": m.start()}
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# page.getByPlaceholder().fill()
|
|
282
|
+
for m in _RE_GET_BY_PLACEHOLDER_FILL.finditer(body):
|
|
283
|
+
steps.append(
|
|
284
|
+
{"action": "fill", "selector": f"placeholder={m.group(2)}", "value": m.group(4), "_pos": m.start()}
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# page.getByTestId().click()
|
|
288
|
+
for m in _RE_GET_BY_TESTID_CLICK.finditer(body):
|
|
289
|
+
steps.append(
|
|
290
|
+
{"action": "click", "selector": f"[data-testid='{m.group(2)}']", "_pos": m.start()}
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# page.getByTestId().fill()
|
|
294
|
+
for m in _RE_GET_BY_TESTID_FILL.finditer(body):
|
|
295
|
+
steps.append(
|
|
296
|
+
{"action": "fill", "selector": f"[data-testid='{m.group(2)}']", "value": m.group(4), "_pos": m.start()}
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# expect(page).toHaveURL()
|
|
300
|
+
for m in _RE_EXPECT_URL.finditer(body):
|
|
301
|
+
steps.append(
|
|
302
|
+
{"action": "assert", "assertion": "toHaveURL", "value": m.group(2), "_pos": m.start()}
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# expect(locator).toBeVisible / toBeEnabled / toHaveText etc.
|
|
306
|
+
for m in _RE_EXPECT_VISIBLE.finditer(body):
|
|
307
|
+
steps.append(
|
|
308
|
+
{"action": "assert", "assertion": m.group(1), "value": m.group(2).strip().strip("\"'`"), "_pos": m.start()}
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Sort by position so steps are in source order
|
|
312
|
+
steps.sort(key=lambda s: s.pop("_pos", 0))
|
|
313
|
+
return steps
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
# Priority / tag inference
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _infer_priority(name: str) -> str:
|
|
322
|
+
name_lower = name.lower()
|
|
323
|
+
if any(kw in name_lower for kw in _HIGH_PRIORITY_KEYWORDS):
|
|
324
|
+
return "high"
|
|
325
|
+
if any(kw in name_lower for kw in _NEGATIVE_KEYWORDS):
|
|
326
|
+
return "medium"
|
|
327
|
+
return "medium"
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _infer_tags(name: str) -> list[str]:
|
|
331
|
+
tags = ["playwright"]
|
|
332
|
+
name_lower = name.lower()
|
|
333
|
+
if any(kw in name_lower for kw in _NEGATIVE_KEYWORDS):
|
|
334
|
+
tags.append("negative")
|
|
335
|
+
if any(kw in name_lower for kw in _HIGH_PRIORITY_KEYWORDS):
|
|
336
|
+
tags.append("smoke")
|
|
337
|
+
return tags
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
# PlaywrightIngestor
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class PlaywrightIngestor(IngestorPlugin):
|
|
346
|
+
"""Ingestor for Playwright ``.spec.ts`` / ``.spec.js`` test files.
|
|
347
|
+
|
|
348
|
+
Parses Playwright test files using regex-based extraction and converts
|
|
349
|
+
``page.goto()``, ``page.fill()``, ``page.click()``, ``page.locator()``,
|
|
350
|
+
``page.getByRole()``, ``page.getByText()``, and similar calls into
|
|
351
|
+
:class:`~mannf.product.ingestors.models.IngestedTestCase` objects.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
name = "playwright"
|
|
355
|
+
display_name = "Playwright Test"
|
|
356
|
+
description = "Parse Playwright .spec.ts/.spec.js test files into test cases."
|
|
357
|
+
supported_extensions = [".spec.ts", ".spec.js", ".test.ts", ".test.js"]
|
|
358
|
+
supported_formats = ["playwright"]
|
|
359
|
+
|
|
360
|
+
# ------------------------------------------------------------------
|
|
361
|
+
# IngestorPlugin interface
|
|
362
|
+
# ------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
async def ingest(
|
|
365
|
+
self, source: IngestorSource, config: dict
|
|
366
|
+
) -> IngestResult:
|
|
367
|
+
"""Parse the Playwright source and return an :class:`IngestResult`."""
|
|
368
|
+
try:
|
|
369
|
+
content = self._read_source(source)
|
|
370
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
371
|
+
return self._make_error_result(source, str(exc))
|
|
372
|
+
|
|
373
|
+
if not content.strip():
|
|
374
|
+
return IngestResult(
|
|
375
|
+
success=True,
|
|
376
|
+
source=source,
|
|
377
|
+
warnings=["Playwright source is empty"],
|
|
378
|
+
stats={"test_case_count": 0},
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
source_name = Path(source.path).name if source.path else "playwright.spec.ts"
|
|
382
|
+
test_cases = self._parse(content, source_name)
|
|
383
|
+
|
|
384
|
+
return IngestResult(
|
|
385
|
+
success=True,
|
|
386
|
+
source=source,
|
|
387
|
+
test_cases=test_cases,
|
|
388
|
+
stats={"test_case_count": len(test_cases)},
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def validate_config(self, config: dict) -> list[str]:
|
|
392
|
+
"""No required configuration keys."""
|
|
393
|
+
return []
|
|
394
|
+
|
|
395
|
+
def can_handle(self, source: IngestorSource) -> bool:
|
|
396
|
+
"""Return True for playwright format or matching file extension."""
|
|
397
|
+
if source.format == "playwright":
|
|
398
|
+
return True
|
|
399
|
+
if source.path is not None:
|
|
400
|
+
path = Path(source.path)
|
|
401
|
+
return (
|
|
402
|
+
path.suffix.lower() in (".ts", ".js")
|
|
403
|
+
and any(ext in path.name.lower() for ext in (".spec.", ".test."))
|
|
404
|
+
)
|
|
405
|
+
if source.raw_content is not None:
|
|
406
|
+
return (
|
|
407
|
+
"page.goto(" in source.raw_content
|
|
408
|
+
or "page.fill(" in source.raw_content
|
|
409
|
+
or "page.click(" in source.raw_content
|
|
410
|
+
or "test(" in source.raw_content
|
|
411
|
+
)
|
|
412
|
+
return False
|
|
413
|
+
|
|
414
|
+
# ------------------------------------------------------------------
|
|
415
|
+
# Internal parsing
|
|
416
|
+
# ------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
def _parse(self, content: str, source_name: str) -> list[IngestedTestCase]:
|
|
419
|
+
"""Extract all test cases from *content*."""
|
|
420
|
+
test_cases: list[IngestedTestCase] = []
|
|
421
|
+
seen_names: set[str] = set()
|
|
422
|
+
|
|
423
|
+
for name, body in _extract_test_blocks(content):
|
|
424
|
+
# Deduplicate test names (Playwright allows duplicate names in
|
|
425
|
+
# different describe blocks, but we disambiguate with a counter)
|
|
426
|
+
unique_name = name
|
|
427
|
+
if unique_name in seen_names:
|
|
428
|
+
counter = 2
|
|
429
|
+
while f"{name} ({counter})" in seen_names:
|
|
430
|
+
counter += 1
|
|
431
|
+
unique_name = f"{name} ({counter})"
|
|
432
|
+
seen_names.add(unique_name)
|
|
433
|
+
|
|
434
|
+
steps = _extract_steps(body)
|
|
435
|
+
priority = _infer_priority(unique_name)
|
|
436
|
+
tags = _infer_tags(unique_name)
|
|
437
|
+
|
|
438
|
+
tc = IngestedTestCase(
|
|
439
|
+
name=unique_name,
|
|
440
|
+
description=f"Playwright test: {unique_name}",
|
|
441
|
+
steps=steps,
|
|
442
|
+
priority=priority,
|
|
443
|
+
tags=tags,
|
|
444
|
+
source_ref=f"{source_name}::{unique_name}",
|
|
445
|
+
metadata={"source_format": "playwright"},
|
|
446
|
+
)
|
|
447
|
+
test_cases.append(tc)
|
|
448
|
+
|
|
449
|
+
return test_cases
|