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,487 @@
|
|
|
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
|
+
"""Cypress test-file ingestor plugin.
|
|
7
|
+
|
|
8
|
+
Parses Cypress ``.cy.js`` / ``.cy.ts`` 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 Cypress patterns
|
|
15
|
+
--------------------------
|
|
16
|
+
* ``describe('suite name', () => { ... })`` / ``context(...)`` — suite blocks
|
|
17
|
+
* ``it('test name', () => { ... })`` / ``specify(...)`` — individual test blocks
|
|
18
|
+
* ``cy.visit('url')`` / ``cy.visit("url")``
|
|
19
|
+
* ``cy.get('selector').type('value')``
|
|
20
|
+
* ``cy.get('selector').clear().type('value')``
|
|
21
|
+
* ``cy.get('selector').click()``
|
|
22
|
+
* ``cy.get('selector').check()`` / ``.uncheck()``
|
|
23
|
+
* ``cy.get('selector').select('value')``
|
|
24
|
+
* ``cy.get('selector').first().click()`` / ``.eq(n).click()``
|
|
25
|
+
* ``cy.contains('text').click()`` / ``cy.contains('selector', 'text').click()``
|
|
26
|
+
* ``cy.url().should('include', '/path')`` / ``('eq', 'url')``
|
|
27
|
+
* ``cy.get('selector').should('be.visible')`` / ``'exist'`` / ``'have.text', '...'``
|
|
28
|
+
* ``cy.intercept('method', 'url').as('alias')`` — captured as metadata
|
|
29
|
+
* ``cy.wait('@alias')`` — captured as wait step
|
|
30
|
+
* ``cy.scrollTo('position')`` / ``cy.get('selector').scrollIntoView()``
|
|
31
|
+
|
|
32
|
+
The extracted tests are tagged with ``["cypress"]`` and their source
|
|
33
|
+
file reference is set to ``file.cy.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
|
+
# it('name', ...) or it("name", ...) or it(`name`, ...)
|
|
57
|
+
# Also matches specify('name', ...)
|
|
58
|
+
# Use \b to avoid matching 'it' inside words like 'visit'
|
|
59
|
+
_RE_IT_BLOCK = re.compile(
|
|
60
|
+
r"""\b(?:it|specify)\s*\(\s*(['"`])(.*?)\1""",
|
|
61
|
+
re.DOTALL,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# describe('suite name', ...) or describe("suite name", ...)
|
|
65
|
+
# Also matches context('suite name', ...)
|
|
66
|
+
_RE_DESCRIBE_BLOCK = re.compile(
|
|
67
|
+
r"""\b(?:describe|context)\s*\(\s*(['"`])(.*?)\1""",
|
|
68
|
+
re.DOTALL,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# cy.visit('url') / cy.visit("url") / cy.visit(`url`)
|
|
72
|
+
_RE_VISIT = re.compile(
|
|
73
|
+
r"""cy\.visit\s*\(\s*(['"`])(.*?)\1""",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# cy.get('selector').type('value') — handles optional .clear() before .type()
|
|
77
|
+
_RE_GET_TYPE = re.compile(
|
|
78
|
+
r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)(?:\.clear\s*\(\s*\))?\.type\s*\(\s*(['"`])(.*?)\3\s*\)""",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# cy.get('selector').click() — supports chained .first() / .eq(n) before .click()
|
|
82
|
+
_RE_GET_CLICK = re.compile(
|
|
83
|
+
r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)(?:\.(?:first|last)\s*\(\s*\)|\.eq\s*\(\s*\d+\s*\))*\.click\s*\(\s*\)""",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# cy.get('selector').check() or .uncheck()
|
|
87
|
+
_RE_GET_CHECK = re.compile(
|
|
88
|
+
r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)\.(?:check|uncheck)\s*\(\s*\)""",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# cy.get('selector').select('value')
|
|
92
|
+
_RE_GET_SELECT = re.compile(
|
|
93
|
+
r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)\.select\s*\(\s*(['"`])(.*?)\3\s*\)""",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# cy.contains('text').click() — one-argument form
|
|
97
|
+
_RE_CONTAINS_CLICK_1 = re.compile(
|
|
98
|
+
r"""cy\.contains\s*\(\s*(['"`])(.*?)\1\s*\)\.click\s*\(\s*\)""",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# cy.contains('selector', 'text').click() — two-argument form
|
|
102
|
+
_RE_CONTAINS_CLICK_2 = re.compile(
|
|
103
|
+
r"""cy\.contains\s*\(\s*(['"`])(.*?)\1\s*,\s*(['"`])(.*?)\3\s*\)\.click\s*\(\s*\)""",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# cy.url().should('include'|'eq', 'value')
|
|
107
|
+
_RE_URL_SHOULD = re.compile(
|
|
108
|
+
r"""cy\.url\s*\(\s*\)\.should\s*\(\s*(['"`])(include|eq|contain)\1\s*,\s*(['"`])(.*?)\3\s*\)""",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# cy.get('selector').should('assertion') — single-arg form (e.g., 'be.visible', 'exist', 'not.exist')
|
|
112
|
+
_RE_GET_SHOULD_1 = re.compile(
|
|
113
|
+
r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)\.should\s*\(\s*(['"`])(.*?)\3\s*\)""",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# cy.get('selector').should('assertion', 'value') — two-arg form (e.g., 'have.text', 'have.value')
|
|
117
|
+
_RE_GET_SHOULD_2 = re.compile(
|
|
118
|
+
r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)\.should\s*\(\s*(['"`])(.*?)\3\s*,\s*(['"`])(.*?)\5\s*\)""",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# cy.intercept('METHOD', 'url').as('alias')
|
|
122
|
+
_RE_INTERCEPT = re.compile(
|
|
123
|
+
r"""cy\.intercept\s*\(\s*(['"`])(.*?)\1\s*,\s*(['"`])(.*?)\3\s*\)\.as\s*\(\s*(['"`])(.*?)\5\s*\)""",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# cy.wait('@alias')
|
|
127
|
+
_RE_WAIT = re.compile(
|
|
128
|
+
r"""cy\.wait\s*\(\s*(['"`])(@\w+)\1\s*\)""",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# cy.scrollTo('position')
|
|
132
|
+
_RE_SCROLL_TO = re.compile(
|
|
133
|
+
r"""cy\.scrollTo\s*\(\s*(['"`])(.*?)\1\s*\)""",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# cy.get('selector').scrollIntoView()
|
|
137
|
+
_RE_SCROLL_INTO_VIEW = re.compile(
|
|
138
|
+
r"""cy\.get\s*\(\s*(['"`])(.*?)\1\s*\)\.scrollIntoView\s*\(\s*\)""",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Priority heuristics
|
|
142
|
+
_HIGH_PRIORITY_KEYWORDS: tuple[str, ...] = (
|
|
143
|
+
"login", "auth", "register", "checkout", "payment", "critical",
|
|
144
|
+
)
|
|
145
|
+
_NEGATIVE_KEYWORDS: tuple[str, ...] = (
|
|
146
|
+
"invalid", "error", "fail", "negative", "reject", "wrong",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Cypress file extensions
|
|
150
|
+
_CYPRESS_EXTENSIONS: frozenset[str] = frozenset(
|
|
151
|
+
[".cy.js", ".cy.ts", ".cy.jsx", ".cy.tsx"]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Helper — extract test blocks from source
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _extract_test_blocks(content: str) -> list[tuple[str, str]]:
|
|
160
|
+
"""Return a list of (test_name, test_body) pairs from *content*.
|
|
161
|
+
|
|
162
|
+
Uses a brace-counting approach to isolate each ``it('name', ...)`` /
|
|
163
|
+
``specify('name', ...)`` block. The test name is optionally prefixed with
|
|
164
|
+
the enclosing ``describe`` / ``context`` suite name so that tests from
|
|
165
|
+
different suites are unambiguous.
|
|
166
|
+
"""
|
|
167
|
+
results: list[tuple[str, str]] = []
|
|
168
|
+
|
|
169
|
+
# Build a list of (start, end, suite_name) for every describe/context block
|
|
170
|
+
# so we can look up which suite contains each it() call.
|
|
171
|
+
suite_ranges: list[tuple[int, int, str]] = []
|
|
172
|
+
for dm in _RE_DESCRIBE_BLOCK.finditer(content):
|
|
173
|
+
suite_name = dm.group(2)
|
|
174
|
+
pos = dm.end()
|
|
175
|
+
brace_pos = content.find("{", pos)
|
|
176
|
+
if brace_pos == -1:
|
|
177
|
+
continue
|
|
178
|
+
depth = 0
|
|
179
|
+
end_pos = brace_pos
|
|
180
|
+
for i, ch in enumerate(content[brace_pos:], start=brace_pos):
|
|
181
|
+
if ch == "{":
|
|
182
|
+
depth += 1
|
|
183
|
+
elif ch == "}":
|
|
184
|
+
depth -= 1
|
|
185
|
+
if depth == 0:
|
|
186
|
+
end_pos = i
|
|
187
|
+
break
|
|
188
|
+
suite_ranges.append((dm.start(), end_pos, suite_name))
|
|
189
|
+
|
|
190
|
+
for m in _RE_IT_BLOCK.finditer(content):
|
|
191
|
+
raw_name = m.group(2)
|
|
192
|
+
|
|
193
|
+
# Determine the innermost enclosing suite (if any)
|
|
194
|
+
suite_name: str | None = None
|
|
195
|
+
for s_start, s_end, s_name in suite_ranges:
|
|
196
|
+
if s_start <= m.start() <= s_end:
|
|
197
|
+
suite_name = s_name
|
|
198
|
+
full_name = f"{suite_name} > {raw_name}" if suite_name else raw_name
|
|
199
|
+
|
|
200
|
+
# Find opening '{' of the arrow function body
|
|
201
|
+
pos = m.end()
|
|
202
|
+
arrow_match = content.find("=> {", pos)
|
|
203
|
+
if arrow_match != -1:
|
|
204
|
+
brace_pos = content.find("{", arrow_match)
|
|
205
|
+
else:
|
|
206
|
+
brace_pos = content.find("{", pos)
|
|
207
|
+
if brace_pos == -1:
|
|
208
|
+
continue
|
|
209
|
+
# Brace-count to find the matching '}'
|
|
210
|
+
depth = 0
|
|
211
|
+
end_pos = brace_pos
|
|
212
|
+
for i, ch in enumerate(content[brace_pos:], start=brace_pos):
|
|
213
|
+
if ch == "{":
|
|
214
|
+
depth += 1
|
|
215
|
+
elif ch == "}":
|
|
216
|
+
depth -= 1
|
|
217
|
+
if depth == 0:
|
|
218
|
+
end_pos = i
|
|
219
|
+
break
|
|
220
|
+
body = content[brace_pos : end_pos + 1]
|
|
221
|
+
results.append((full_name, body))
|
|
222
|
+
|
|
223
|
+
return results
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
# Step extractors
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _extract_steps(body: str) -> list[dict[str, Any]]:
|
|
232
|
+
"""Extract all recognisable Cypress actions from *body* as step dicts."""
|
|
233
|
+
steps: list[dict[str, Any]] = []
|
|
234
|
+
|
|
235
|
+
# cy.visit
|
|
236
|
+
for m in _RE_VISIT.finditer(body):
|
|
237
|
+
url = m.group(2)
|
|
238
|
+
steps.append(
|
|
239
|
+
{"action": "navigate", "selector": url, "url": url, "_pos": m.start()}
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# cy.get().type() (with optional .clear() prefix)
|
|
243
|
+
for m in _RE_GET_TYPE.finditer(body):
|
|
244
|
+
steps.append(
|
|
245
|
+
{"action": "fill", "selector": m.group(2), "value": m.group(4), "_pos": m.start()}
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# cy.get().click() (with optional .first()/.eq(n) prefix)
|
|
249
|
+
for m in _RE_GET_CLICK.finditer(body):
|
|
250
|
+
steps.append(
|
|
251
|
+
{"action": "click", "selector": m.group(2), "_pos": m.start()}
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# cy.get().check() / .uncheck() → treated as click
|
|
255
|
+
for m in _RE_GET_CHECK.finditer(body):
|
|
256
|
+
steps.append(
|
|
257
|
+
{"action": "click", "selector": m.group(2), "_pos": m.start()}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# cy.get().select() → mapped to fill
|
|
261
|
+
for m in _RE_GET_SELECT.finditer(body):
|
|
262
|
+
steps.append(
|
|
263
|
+
{"action": "fill", "selector": m.group(2), "value": m.group(4), "_pos": m.start()}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# cy.contains('selector', 'text').click() — two-arg form (more specific, match first)
|
|
267
|
+
for m in _RE_CONTAINS_CLICK_2.finditer(body):
|
|
268
|
+
steps.append(
|
|
269
|
+
{"action": "click", "selector": f"text={m.group(4)}", "_pos": m.start()}
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# cy.contains('text').click() — one-arg form
|
|
273
|
+
# Exclude positions already matched by the two-arg form.
|
|
274
|
+
contains_two_arg_positions = {
|
|
275
|
+
m.start() for m in _RE_CONTAINS_CLICK_2.finditer(body)
|
|
276
|
+
}
|
|
277
|
+
for m in _RE_CONTAINS_CLICK_1.finditer(body):
|
|
278
|
+
if m.start() not in contains_two_arg_positions:
|
|
279
|
+
steps.append(
|
|
280
|
+
{"action": "click", "selector": f"text={m.group(2)}", "_pos": m.start()}
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# cy.url().should(...)
|
|
284
|
+
for m in _RE_URL_SHOULD.finditer(body):
|
|
285
|
+
steps.append(
|
|
286
|
+
{
|
|
287
|
+
"action": "assert",
|
|
288
|
+
"assertion": "url",
|
|
289
|
+
"value": m.group(4),
|
|
290
|
+
"_pos": m.start(),
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# cy.get().should('assertion', 'value') — two-arg form (more specific, match first)
|
|
295
|
+
for m in _RE_GET_SHOULD_2.finditer(body):
|
|
296
|
+
steps.append(
|
|
297
|
+
{
|
|
298
|
+
"action": "assert",
|
|
299
|
+
"assertion": m.group(4),
|
|
300
|
+
"selector": m.group(2),
|
|
301
|
+
"value": m.group(6),
|
|
302
|
+
"_pos": m.start(),
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# cy.get().should('assertion') — single-arg form
|
|
307
|
+
# Only emit if this position wasn't already captured by the two-arg form.
|
|
308
|
+
two_arg_positions = {
|
|
309
|
+
m.start() for m in _RE_GET_SHOULD_2.finditer(body)
|
|
310
|
+
}
|
|
311
|
+
for m in _RE_GET_SHOULD_1.finditer(body):
|
|
312
|
+
if m.start() not in two_arg_positions:
|
|
313
|
+
steps.append(
|
|
314
|
+
{
|
|
315
|
+
"action": "assert",
|
|
316
|
+
"assertion": m.group(4),
|
|
317
|
+
"selector": m.group(2),
|
|
318
|
+
"_pos": m.start(),
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# cy.intercept().as() — capture as metadata step
|
|
323
|
+
for m in _RE_INTERCEPT.finditer(body):
|
|
324
|
+
steps.append(
|
|
325
|
+
{
|
|
326
|
+
"action": "intercept",
|
|
327
|
+
"method": m.group(2),
|
|
328
|
+
"url": m.group(4),
|
|
329
|
+
"alias": m.group(6),
|
|
330
|
+
"_pos": m.start(),
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# cy.wait('@alias')
|
|
335
|
+
for m in _RE_WAIT.finditer(body):
|
|
336
|
+
steps.append(
|
|
337
|
+
{"action": "wait", "alias": m.group(2), "_pos": m.start()}
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# cy.scrollTo()
|
|
341
|
+
for m in _RE_SCROLL_TO.finditer(body):
|
|
342
|
+
steps.append(
|
|
343
|
+
{"action": "scroll", "position": m.group(2), "_pos": m.start()}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# cy.get().scrollIntoView()
|
|
347
|
+
for m in _RE_SCROLL_INTO_VIEW.finditer(body):
|
|
348
|
+
steps.append(
|
|
349
|
+
{"action": "scroll", "selector": m.group(2), "_pos": m.start()}
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Sort by position so steps are in source order
|
|
353
|
+
steps.sort(key=lambda s: s.pop("_pos", 0))
|
|
354
|
+
return steps
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
# Priority / tag inference
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _infer_priority(name: str) -> str:
|
|
363
|
+
name_lower = name.lower()
|
|
364
|
+
if any(kw in name_lower for kw in _HIGH_PRIORITY_KEYWORDS):
|
|
365
|
+
return "high"
|
|
366
|
+
if any(kw in name_lower for kw in _NEGATIVE_KEYWORDS):
|
|
367
|
+
return "medium"
|
|
368
|
+
return "medium"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _infer_tags(name: str) -> list[str]:
|
|
372
|
+
tags = ["cypress"]
|
|
373
|
+
name_lower = name.lower()
|
|
374
|
+
if any(kw in name_lower for kw in _NEGATIVE_KEYWORDS):
|
|
375
|
+
tags.append("negative")
|
|
376
|
+
if any(kw in name_lower for kw in _HIGH_PRIORITY_KEYWORDS):
|
|
377
|
+
tags.append("smoke")
|
|
378
|
+
return tags
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
# CypressIngestor
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class CypressIngestor(IngestorPlugin):
|
|
387
|
+
"""Ingestor for Cypress ``.cy.js`` / ``.cy.ts`` test files.
|
|
388
|
+
|
|
389
|
+
Parses Cypress test files using regex-based extraction and converts
|
|
390
|
+
``cy.visit()``, ``cy.get().type()``, ``cy.get().click()``,
|
|
391
|
+
``cy.contains()``, ``cy.url().should()``, and similar calls into
|
|
392
|
+
:class:`~mannf.product.ingestors.models.IngestedTestCase` objects.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
name = "cypress"
|
|
396
|
+
display_name = "Cypress Test"
|
|
397
|
+
description = "Parse Cypress .cy.js/.cy.ts test files into test cases."
|
|
398
|
+
supported_extensions = [".cy.js", ".cy.ts", ".cy.jsx", ".cy.tsx"]
|
|
399
|
+
supported_formats = ["cypress"]
|
|
400
|
+
|
|
401
|
+
# ------------------------------------------------------------------
|
|
402
|
+
# IngestorPlugin interface
|
|
403
|
+
# ------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
async def ingest(
|
|
406
|
+
self, source: IngestorSource, config: dict
|
|
407
|
+
) -> IngestResult:
|
|
408
|
+
"""Parse the Cypress source and return an :class:`IngestResult`."""
|
|
409
|
+
try:
|
|
410
|
+
content = self._read_source(source)
|
|
411
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
412
|
+
return self._make_error_result(source, str(exc))
|
|
413
|
+
|
|
414
|
+
if not content.strip():
|
|
415
|
+
return IngestResult(
|
|
416
|
+
success=True,
|
|
417
|
+
source=source,
|
|
418
|
+
warnings=["Cypress source is empty"],
|
|
419
|
+
stats={"test_case_count": 0},
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
source_name = Path(source.path).name if source.path else "cypress.cy.ts"
|
|
423
|
+
test_cases = self._parse(content, source_name)
|
|
424
|
+
|
|
425
|
+
return IngestResult(
|
|
426
|
+
success=True,
|
|
427
|
+
source=source,
|
|
428
|
+
test_cases=test_cases,
|
|
429
|
+
stats={"test_case_count": len(test_cases)},
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def validate_config(self, config: dict) -> list[str]:
|
|
433
|
+
"""No required configuration keys."""
|
|
434
|
+
return []
|
|
435
|
+
|
|
436
|
+
def can_handle(self, source: IngestorSource) -> bool:
|
|
437
|
+
"""Return True for cypress format or matching file extension/content."""
|
|
438
|
+
if source.format == "cypress":
|
|
439
|
+
return True
|
|
440
|
+
if source.path is not None:
|
|
441
|
+
name = Path(source.path).name.lower()
|
|
442
|
+
if any(name.endswith(ext) for ext in _CYPRESS_EXTENSIONS):
|
|
443
|
+
return True
|
|
444
|
+
if source.raw_content is not None:
|
|
445
|
+
return (
|
|
446
|
+
"cy.visit(" in source.raw_content
|
|
447
|
+
or "cy.get(" in source.raw_content
|
|
448
|
+
or "cy.contains(" in source.raw_content
|
|
449
|
+
)
|
|
450
|
+
return False
|
|
451
|
+
|
|
452
|
+
# ------------------------------------------------------------------
|
|
453
|
+
# Internal parsing
|
|
454
|
+
# ------------------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
def _parse(self, content: str, source_name: str) -> list[IngestedTestCase]:
|
|
457
|
+
"""Extract all test cases from *content*."""
|
|
458
|
+
test_cases: list[IngestedTestCase] = []
|
|
459
|
+
seen_names: set[str] = set()
|
|
460
|
+
|
|
461
|
+
for name, body in _extract_test_blocks(content):
|
|
462
|
+
# Deduplicate test names (different describe blocks may use the
|
|
463
|
+
# same bare it() name; disambiguate with a counter)
|
|
464
|
+
unique_name = name
|
|
465
|
+
if unique_name in seen_names:
|
|
466
|
+
counter = 2
|
|
467
|
+
while f"{name} ({counter})" in seen_names:
|
|
468
|
+
counter += 1
|
|
469
|
+
unique_name = f"{name} ({counter})"
|
|
470
|
+
seen_names.add(unique_name)
|
|
471
|
+
|
|
472
|
+
steps = _extract_steps(body)
|
|
473
|
+
priority = _infer_priority(unique_name)
|
|
474
|
+
tags = _infer_tags(unique_name)
|
|
475
|
+
|
|
476
|
+
tc = IngestedTestCase(
|
|
477
|
+
name=unique_name,
|
|
478
|
+
description=f"Cypress test: {unique_name}",
|
|
479
|
+
steps=steps,
|
|
480
|
+
priority=priority,
|
|
481
|
+
tags=tags,
|
|
482
|
+
source_ref=f"{source_name}::{unique_name}",
|
|
483
|
+
metadata={"source_format": "cypress"},
|
|
484
|
+
)
|
|
485
|
+
test_cases.append(tc)
|
|
486
|
+
|
|
487
|
+
return test_cases
|