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.
Files changed (299) hide show
  1. mannf/__init__.py +33 -0
  2. mannf/__main__.py +10 -0
  3. mannf/_version.py +8 -0
  4. mannf/agents/__init__.py +7 -0
  5. mannf/agents/analyzer_agent.py +9 -0
  6. mannf/agents/base.py +9 -0
  7. mannf/agents/bdi_agent.py +9 -0
  8. mannf/agents/belief_state.py +9 -0
  9. mannf/agents/coordinator_agent.py +9 -0
  10. mannf/agents/executor_agent.py +9 -0
  11. mannf/agents/monitor_agent.py +9 -0
  12. mannf/agents/oracle_agent.py +9 -0
  13. mannf/agents/planner_agent.py +9 -0
  14. mannf/agents/test_agent.py +9 -0
  15. mannf/anomaly/__init__.py +7 -0
  16. mannf/anomaly/enhanced_detector.py +9 -0
  17. mannf/cli.py +9 -0
  18. mannf/core/__init__.py +26 -0
  19. mannf/core/agents/__init__.py +52 -0
  20. mannf/core/agents/accessibility_scanner_agent.py +245 -0
  21. mannf/core/agents/analyzer_agent.py +224 -0
  22. mannf/core/agents/autonomous_loop_agent.py +1086 -0
  23. mannf/core/agents/autonomous_loop_models.py +62 -0
  24. mannf/core/agents/autonomous_run_differ.py +427 -0
  25. mannf/core/agents/base.py +128 -0
  26. mannf/core/agents/bdi_agent.py +330 -0
  27. mannf/core/agents/belief_state.py +202 -0
  28. mannf/core/agents/browser_coordinator_agent.py +224 -0
  29. mannf/core/agents/browser_executor_agent.py +410 -0
  30. mannf/core/agents/coordinator_agent.py +262 -0
  31. mannf/core/agents/executor_agent.py +222 -0
  32. mannf/core/agents/monitor_agent.py +188 -0
  33. mannf/core/agents/oracle_agent.py +150 -0
  34. mannf/core/agents/performance_testing_agent.py +279 -0
  35. mannf/core/agents/planner_agent.py +128 -0
  36. mannf/core/agents/test_agent.py +249 -0
  37. mannf/core/agents/visual_regression_agent.py +311 -0
  38. mannf/core/agents/web_crawler_agent.py +510 -0
  39. mannf/core/agents/worker_pool.py +366 -0
  40. mannf/core/anomaly/__init__.py +14 -0
  41. mannf/core/anomaly/enhanced_detector.py +541 -0
  42. mannf/core/browser/__init__.py +63 -0
  43. mannf/core/browser/accessibility_scanner.py +424 -0
  44. mannf/core/browser/discovery_model.py +178 -0
  45. mannf/core/browser/dom_snapshot.py +349 -0
  46. mannf/core/browser/ingestor_bridge.py +371 -0
  47. mannf/core/browser/performance_metrics.py +217 -0
  48. mannf/core/browser/reflection_analyzer.py +442 -0
  49. mannf/core/browser/scenario_generator.py +1100 -0
  50. mannf/core/browser/security_scenario_generator.py +695 -0
  51. mannf/core/browser/visual_comparer.py +159 -0
  52. mannf/core/diagnostics/__init__.py +28 -0
  53. mannf/core/diagnostics/failure_clusterer.py +211 -0
  54. mannf/core/diagnostics/flake_detector.py +233 -0
  55. mannf/core/diagnostics/root_cause_analyzer.py +273 -0
  56. mannf/core/distributed/__init__.py +16 -0
  57. mannf/core/distributed/endpoint.py +139 -0
  58. mannf/core/distributed/system_under_test.py +207 -0
  59. mannf/core/functional_orchestrator.py +428 -0
  60. mannf/core/messaging/__init__.py +11 -0
  61. mannf/core/messaging/bus.py +113 -0
  62. mannf/core/messaging/messages.py +89 -0
  63. mannf/core/nat_orchestrator.py +342 -0
  64. mannf/core/neural/__init__.py +183 -0
  65. mannf/core/orchestrator.py +272 -0
  66. mannf/core/prioritization/__init__.py +17 -0
  67. mannf/core/prioritization/adaptive_controller.py +509 -0
  68. mannf/core/prioritization/belief_prioritizer.py +231 -0
  69. mannf/core/prioritization/risk_scorer.py +430 -0
  70. mannf/core/reporting/__init__.py +12 -0
  71. mannf/core/reporting/unified_report.py +664 -0
  72. mannf/core/testing/__init__.py +17 -0
  73. mannf/core/testing/adaptive_controller.py +149 -0
  74. mannf/core/testing/models.py +179 -0
  75. mannf/core/validation/__init__.py +10 -0
  76. mannf/core/validation/self_validation_runner.py +180 -0
  77. mannf/dashboard/__init__.py +7 -0
  78. mannf/dashboard/app.py +9 -0
  79. mannf/dashboard/models.py +9 -0
  80. mannf/dashboard/static/index.html +2538 -0
  81. mannf/dashboard/telemetry.py +9 -0
  82. mannf/distributed/__init__.py +7 -0
  83. mannf/distributed/endpoint.py +9 -0
  84. mannf/distributed/system_under_test.py +9 -0
  85. mannf/healing/__init__.py +7 -0
  86. mannf/healing/graphql_schema_diff.py +9 -0
  87. mannf/healing/healer.py +9 -0
  88. mannf/healing/models.py +9 -0
  89. mannf/healing/schema_diff.py +9 -0
  90. mannf/integrations/__init__.py +7 -0
  91. mannf/integrations/auth.py +9 -0
  92. mannf/integrations/graphql_parser.py +9 -0
  93. mannf/integrations/graphql_sut.py +9 -0
  94. mannf/integrations/http_sut.py +9 -0
  95. mannf/integrations/openapi_parser.py +9 -0
  96. mannf/integrations/postman_parser.py +9 -0
  97. mannf/llm/__init__.py +7 -0
  98. mannf/llm/anthropic_provider.py +9 -0
  99. mannf/llm/base.py +9 -0
  100. mannf/llm/config.py +9 -0
  101. mannf/llm/factory.py +9 -0
  102. mannf/llm/openai_provider.py +9 -0
  103. mannf/llm/prompts.py +9 -0
  104. mannf/messaging/__init__.py +7 -0
  105. mannf/messaging/bus.py +9 -0
  106. mannf/messaging/messages.py +9 -0
  107. mannf/nat_orchestrator.py +9 -0
  108. mannf/neural/__init__.py +7 -0
  109. mannf/orchestrator.py +9 -0
  110. mannf/prioritization/__init__.py +7 -0
  111. mannf/prioritization/adaptive_controller.py +9 -0
  112. mannf/prioritization/belief_prioritizer.py +9 -0
  113. mannf/prioritization/risk_scorer.py +9 -0
  114. mannf/product/__init__.py +29 -0
  115. mannf/product/admin/__init__.py +3 -0
  116. mannf/product/admin/routes.py +514 -0
  117. mannf/product/auth/__init__.py +5 -0
  118. mannf/product/auth/saml.py +212 -0
  119. mannf/product/billing/__init__.py +5 -0
  120. mannf/product/billing/audit.py +160 -0
  121. mannf/product/billing/feature_gates.py +180 -0
  122. mannf/product/billing/metering.py +179 -0
  123. mannf/product/billing/notifications.py +181 -0
  124. mannf/product/billing/plans.py +133 -0
  125. mannf/product/billing/rate_limits.py +35 -0
  126. mannf/product/billing/stripe_billing.py +906 -0
  127. mannf/product/billing/tenant_auth.py +233 -0
  128. mannf/product/billing/tenant_manager.py +873 -0
  129. mannf/product/cli.py +3900 -0
  130. mannf/product/cli_admin.py +408 -0
  131. mannf/product/dashboard/__init__.py +61 -0
  132. mannf/product/dashboard/app.py +3567 -0
  133. mannf/product/dashboard/models.py +460 -0
  134. mannf/product/dashboard/static/index.html +6347 -0
  135. mannf/product/dashboard/static/manifest.json +25 -0
  136. mannf/product/dashboard/static/pwa-icon-192.png +0 -0
  137. mannf/product/dashboard/static/pwa-icon-512.png +0 -0
  138. mannf/product/dashboard/static/sw.js +64 -0
  139. mannf/product/dashboard/telemetry.py +547 -0
  140. mannf/product/database.py +145 -0
  141. mannf/product/demo.py +844 -0
  142. mannf/product/doctor.py +509 -0
  143. mannf/product/exporters/__init__.py +65 -0
  144. mannf/product/exporters/azuredevops_exporter.py +257 -0
  145. mannf/product/exporters/base.py +307 -0
  146. mannf/product/exporters/bugzilla_exporter.py +200 -0
  147. mannf/product/exporters/dedup.py +275 -0
  148. mannf/product/exporters/finding_adapter.py +216 -0
  149. mannf/product/exporters/github_exporter.py +197 -0
  150. mannf/product/exporters/gitlab_exporter.py +215 -0
  151. mannf/product/exporters/jira_exporter.py +180 -0
  152. mannf/product/exporters/linear_exporter.py +195 -0
  153. mannf/product/exporters/loader.py +233 -0
  154. mannf/product/exporters/pagerduty_exporter.py +363 -0
  155. mannf/product/exporters/sentry_exporter.py +322 -0
  156. mannf/product/exporters/servicenow_exporter.py +240 -0
  157. mannf/product/exporters/shortcut_exporter.py +231 -0
  158. mannf/product/exporters/webhook_exporter.py +383 -0
  159. mannf/product/formatters/__init__.py +18 -0
  160. mannf/product/formatters/allure_formatter.py +161 -0
  161. mannf/product/formatters/ctrf_formatter.py +149 -0
  162. mannf/product/healing/__init__.py +30 -0
  163. mannf/product/healing/graphql_schema_diff.py +152 -0
  164. mannf/product/healing/healer.py +141 -0
  165. mannf/product/healing/models.py +175 -0
  166. mannf/product/healing/schema_diff.py +251 -0
  167. mannf/product/ingestors/__init__.py +77 -0
  168. mannf/product/ingestors/base.py +256 -0
  169. mannf/product/ingestors/bgstm_ingestor.py +764 -0
  170. mannf/product/ingestors/curl_ingestor.py +1019 -0
  171. mannf/product/ingestors/cypress_ingestor.py +487 -0
  172. mannf/product/ingestors/gherkin_ingestor.py +967 -0
  173. mannf/product/ingestors/graphql_ingestor.py +845 -0
  174. mannf/product/ingestors/grpc_ingestor.py +591 -0
  175. mannf/product/ingestors/har_ingestor.py +976 -0
  176. mannf/product/ingestors/loader.py +284 -0
  177. mannf/product/ingestors/models.py +146 -0
  178. mannf/product/ingestors/openapi_ingestor.py +606 -0
  179. mannf/product/ingestors/playwright_ingestor.py +449 -0
  180. mannf/product/ingestors/postman_ingestor.py +631 -0
  181. mannf/product/ingestors/traffic_ingestor.py +679 -0
  182. mannf/product/ingestors/websocket_ingestor.py +526 -0
  183. mannf/product/integrations/__init__.py +21 -0
  184. mannf/product/integrations/auth.py +190 -0
  185. mannf/product/integrations/graphql_parser.py +436 -0
  186. mannf/product/integrations/graphql_sut.py +247 -0
  187. mannf/product/integrations/grpc_sut.py +469 -0
  188. mannf/product/integrations/http_sut.py +237 -0
  189. mannf/product/integrations/kafka_adapter.py +342 -0
  190. mannf/product/integrations/openapi_parser.py +513 -0
  191. mannf/product/integrations/postman_parser.py +467 -0
  192. mannf/product/integrations/webhook_receiver.py +344 -0
  193. mannf/product/integrations/websocket_sut.py +434 -0
  194. mannf/product/llm/__init__.py +25 -0
  195. mannf/product/llm/anthropic_provider.py +94 -0
  196. mannf/product/llm/base.py +267 -0
  197. mannf/product/llm/config.py +48 -0
  198. mannf/product/llm/factory.py +42 -0
  199. mannf/product/llm/openai_provider.py +93 -0
  200. mannf/product/llm/prompts.py +403 -0
  201. mannf/product/llm/root_cause_service.py +311 -0
  202. mannf/product/llm/test_plan_models.py +78 -0
  203. mannf/product/metrics.py +149 -0
  204. mannf/product/middleware/__init__.py +3 -0
  205. mannf/product/middleware/audit_middleware.py +112 -0
  206. mannf/product/middleware/tenant_isolation.py +114 -0
  207. mannf/product/models.py +347 -0
  208. mannf/product/notifications/__init__.py +24 -0
  209. mannf/product/notifications/dispatcher.py +411 -0
  210. mannf/product/onboarding.py +190 -0
  211. mannf/product/orchestration/__init__.py +39 -0
  212. mannf/product/orchestration/ingest_scan_orchestrator.py +339 -0
  213. mannf/product/orchestration/pipeline.py +401 -0
  214. mannf/product/orchestrator.py +987 -0
  215. mannf/product/orchestrator_models.py +269 -0
  216. mannf/product/regression/__init__.py +36 -0
  217. mannf/product/regression/differ.py +172 -0
  218. mannf/product/regression/masking.py +100 -0
  219. mannf/product/regression/models.py +232 -0
  220. mannf/product/regression/recorder.py +124 -0
  221. mannf/product/regression/replayer.py +168 -0
  222. mannf/product/reports/__init__.py +10 -0
  223. mannf/product/reports/pdf.py +132 -0
  224. mannf/product/scheduling/__init__.py +57 -0
  225. mannf/product/scheduling/cron_utils.py +251 -0
  226. mannf/product/scheduling/engine.py +473 -0
  227. mannf/product/scheduling/models.py +86 -0
  228. mannf/product/scheduling/queue.py +894 -0
  229. mannf/product/scheduling/store.py +235 -0
  230. mannf/product/security/__init__.py +21 -0
  231. mannf/product/security/belief_guided.py +143 -0
  232. mannf/product/security/checks/__init__.py +55 -0
  233. mannf/product/security/checks/base.py +69 -0
  234. mannf/product/security/checks/bfla.py +77 -0
  235. mannf/product/security/checks/bola.py +77 -0
  236. mannf/product/security/checks/bopla.py +80 -0
  237. mannf/product/security/checks/broken_auth.py +86 -0
  238. mannf/product/security/checks/graphql_security.py +299 -0
  239. mannf/product/security/checks/inventory.py +70 -0
  240. mannf/product/security/checks/misconfig.py +158 -0
  241. mannf/product/security/checks/resource_consumption.py +70 -0
  242. mannf/product/security/checks/sensitive_flows.py +80 -0
  243. mannf/product/security/checks/ssrf.py +101 -0
  244. mannf/product/security/checks/unsafe_consumption.py +120 -0
  245. mannf/product/security/models.py +92 -0
  246. mannf/product/security/plugin_loader.py +182 -0
  247. mannf/product/security/reporter.py +92 -0
  248. mannf/product/security/scanner.py +183 -0
  249. mannf/product/server.py +6220 -0
  250. mannf/product/setup_wizard.py +873 -0
  251. mannf/product/status.py +404 -0
  252. mannf/product/storage/__init__.py +10 -0
  253. mannf/product/storage/artifact_store.py +343 -0
  254. mannf/product/telemetry.py +300 -0
  255. mannf/product/uninstall.py +169 -0
  256. mannf/product/upgrade.py +139 -0
  257. mannf/product/weights/__init__.py +13 -0
  258. mannf/product/weights/blob_store.py +299 -0
  259. mannf/product/weights/factory.py +42 -0
  260. mannf/product/weights/registry.py +159 -0
  261. mannf/product/weights/store.py +210 -0
  262. mannf/regression/__init__.py +7 -0
  263. mannf/regression/differ.py +9 -0
  264. mannf/regression/masking.py +9 -0
  265. mannf/regression/models.py +9 -0
  266. mannf/regression/recorder.py +9 -0
  267. mannf/regression/replayer.py +9 -0
  268. mannf/security/__init__.py +7 -0
  269. mannf/security/belief_guided.py +9 -0
  270. mannf/security/checks/__init__.py +7 -0
  271. mannf/security/checks/base.py +9 -0
  272. mannf/security/checks/bfla.py +9 -0
  273. mannf/security/checks/bola.py +9 -0
  274. mannf/security/checks/bopla.py +9 -0
  275. mannf/security/checks/broken_auth.py +9 -0
  276. mannf/security/checks/graphql_security.py +9 -0
  277. mannf/security/checks/inventory.py +9 -0
  278. mannf/security/checks/misconfig.py +9 -0
  279. mannf/security/checks/resource_consumption.py +9 -0
  280. mannf/security/checks/sensitive_flows.py +9 -0
  281. mannf/security/checks/ssrf.py +9 -0
  282. mannf/security/checks/unsafe_consumption.py +9 -0
  283. mannf/security/models.py +9 -0
  284. mannf/security/reporter.py +9 -0
  285. mannf/security/scanner.py +9 -0
  286. mannf/server.py +9 -0
  287. mannf/testing/__init__.py +7 -0
  288. mannf/testing/adaptive_controller.py +9 -0
  289. mannf/testing/models.py +9 -0
  290. mannf/weights/__init__.py +7 -0
  291. mannf/weights/registry.py +9 -0
  292. mannf/weights/store.py +9 -0
  293. nat_engine-1.dist-info/METADATA +555 -0
  294. nat_engine-1.dist-info/RECORD +299 -0
  295. nat_engine-1.dist-info/WHEEL +5 -0
  296. nat_engine-1.dist-info/entry_points.txt +4 -0
  297. nat_engine-1.dist-info/licenses/LICENSE +651 -0
  298. nat_engine-1.dist-info/licenses/NOTICE +178 -0
  299. 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