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,349 @@
|
|
|
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
|
+
"""DOM snapshot data model and capture utility.
|
|
7
|
+
|
|
8
|
+
This module provides a lightweight, Playwright-agnostic data model for
|
|
9
|
+
representing page DOM state, plus an async helper that populates a
|
|
10
|
+
:class:`DOMSnapshot` from a live Playwright ``Page`` object.
|
|
11
|
+
|
|
12
|
+
Import note: Playwright is **not** imported at module level — it is only
|
|
13
|
+
referenced inside ``capture_dom_snapshot``, so this module can be imported
|
|
14
|
+
and used for pure data-modelling work without Playwright installed.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
# Interactive HTML tags / selectors that accept user input or navigation.
|
|
27
|
+
_INTERACTIVE_TAGS = frozenset({"button", "input", "select", "textarea"})
|
|
28
|
+
|
|
29
|
+
# JavaScript that extracts element data from the live DOM.
|
|
30
|
+
_DOM_EXTRACTOR_JS = """
|
|
31
|
+
() => {
|
|
32
|
+
const ATTRS = ['name', 'type', 'href', 'placeholder',
|
|
33
|
+
'aria-label', 'role', 'data-testid'];
|
|
34
|
+
const INTERACTIVE_TAGS = new Set(['button','input','select','textarea']);
|
|
35
|
+
const results = [];
|
|
36
|
+
const allElements = document.querySelectorAll('*');
|
|
37
|
+
allElements.forEach(el => {
|
|
38
|
+
const tag = el.tagName.toLowerCase();
|
|
39
|
+
const elementId = el.id || '';
|
|
40
|
+
const classes = Array.from(el.classList);
|
|
41
|
+
const rawText = (el.innerText || el.textContent || '').trim();
|
|
42
|
+
const text = rawText.length > 200 ? rawText.slice(0, 200) : rawText;
|
|
43
|
+
const attrs = {};
|
|
44
|
+
ATTRS.forEach(a => {
|
|
45
|
+
const v = el.getAttribute(a);
|
|
46
|
+
if (v !== null) attrs[a] = v;
|
|
47
|
+
});
|
|
48
|
+
const isInteractive = INTERACTIVE_TAGS.has(tag) ||
|
|
49
|
+
(tag === 'a' && el.hasAttribute('href'));
|
|
50
|
+
let bbox = null;
|
|
51
|
+
try {
|
|
52
|
+
const r = el.getBoundingClientRect();
|
|
53
|
+
if (r.width > 0 || r.height > 0) {
|
|
54
|
+
bbox = {x: r.x, y: r.y, width: r.width, height: r.height};
|
|
55
|
+
}
|
|
56
|
+
} catch(_) {}
|
|
57
|
+
results.push({tag, elementId, classes, text, attrs, isInteractive, bbox});
|
|
58
|
+
});
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# JavaScript that extracts <form> structures.
|
|
64
|
+
_FORM_EXTRACTOR_JS = """
|
|
65
|
+
() => {
|
|
66
|
+
const forms = [];
|
|
67
|
+
document.querySelectorAll('form').forEach(form => {
|
|
68
|
+
const fields = [];
|
|
69
|
+
form.querySelectorAll('input, select, textarea').forEach(el => {
|
|
70
|
+
fields.push({
|
|
71
|
+
tag: el.tagName.toLowerCase(),
|
|
72
|
+
name: el.getAttribute('name') || '',
|
|
73
|
+
type: el.getAttribute('type') || '',
|
|
74
|
+
placeholder: el.getAttribute('placeholder') || '',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
forms.push({
|
|
78
|
+
action: form.getAttribute('action') || '',
|
|
79
|
+
method: (form.getAttribute('method') || 'get').toLowerCase(),
|
|
80
|
+
fields,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
return forms;
|
|
84
|
+
}
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# JavaScript that extracts <form> structures with enhanced field metadata
|
|
89
|
+
# (required flag, associated label text, HTML5 validation pattern, and
|
|
90
|
+
# per-form submit button selector). Used by WebCrawlerAgent.
|
|
91
|
+
_ENHANCED_FORM_EXTRACTOR_JS = """
|
|
92
|
+
() => {
|
|
93
|
+
function buildSelector(el) {
|
|
94
|
+
if (el.id) return '#' + el.id;
|
|
95
|
+
const testid = el.getAttribute('data-testid');
|
|
96
|
+
if (testid) return '[data-testid="' + testid + '"]';
|
|
97
|
+
const name = el.getAttribute('name');
|
|
98
|
+
if (name) return el.tagName.toLowerCase() + '[name="' + name + '"]';
|
|
99
|
+
const classes = Array.from(el.classList);
|
|
100
|
+
if (classes.length) return el.tagName.toLowerCase() + '.' + classes.join('.');
|
|
101
|
+
return el.tagName.toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function findLabel(el) {
|
|
105
|
+
// 1. <label for="id">
|
|
106
|
+
if (el.id) {
|
|
107
|
+
const lbl = document.querySelector('label[for="' + el.id + '"]');
|
|
108
|
+
if (lbl) return (lbl.innerText || lbl.textContent || '').trim();
|
|
109
|
+
}
|
|
110
|
+
// 2. Ancestor <label>
|
|
111
|
+
let node = el.parentElement;
|
|
112
|
+
while (node) {
|
|
113
|
+
if (node.tagName && node.tagName.toLowerCase() === 'label') {
|
|
114
|
+
return (node.innerText || node.textContent || '').trim();
|
|
115
|
+
}
|
|
116
|
+
node = node.parentElement;
|
|
117
|
+
}
|
|
118
|
+
// 3. aria-label attribute
|
|
119
|
+
return el.getAttribute('aria-label') || '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const forms = [];
|
|
123
|
+
document.querySelectorAll('form').forEach(form => {
|
|
124
|
+
const fields = [];
|
|
125
|
+
form.querySelectorAll('input, select, textarea').forEach(el => {
|
|
126
|
+
fields.push({
|
|
127
|
+
tag: el.tagName.toLowerCase(),
|
|
128
|
+
name: el.getAttribute('name') || '',
|
|
129
|
+
type: (el.getAttribute('type') || el.tagName.toLowerCase() === 'select'
|
|
130
|
+
? (el.tagName.toLowerCase() === 'select' ? 'select'
|
|
131
|
+
: (el.tagName.toLowerCase() === 'textarea' ? 'textarea'
|
|
132
|
+
: (el.getAttribute('type') || 'text')))
|
|
133
|
+
: 'text'),
|
|
134
|
+
placeholder: el.getAttribute('placeholder') || '',
|
|
135
|
+
required: !!(el.required || el.hasAttribute('required')),
|
|
136
|
+
label: findLabel(el),
|
|
137
|
+
pattern: el.getAttribute('pattern') || '',
|
|
138
|
+
selector: buildSelector(el),
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Identify submit selector: prefer button[type=submit], then
|
|
143
|
+
// input[type=submit], then the last button in the form.
|
|
144
|
+
let submitSel = '';
|
|
145
|
+
const submitBtn = form.querySelector('button[type="submit"]');
|
|
146
|
+
if (submitBtn) {
|
|
147
|
+
submitSel = buildSelector(submitBtn);
|
|
148
|
+
} else {
|
|
149
|
+
const submitInput = form.querySelector('input[type="submit"]');
|
|
150
|
+
if (submitInput) {
|
|
151
|
+
submitSel = buildSelector(submitInput);
|
|
152
|
+
} else {
|
|
153
|
+
const buttons = form.querySelectorAll('button');
|
|
154
|
+
if (buttons.length) {
|
|
155
|
+
submitSel = buildSelector(buttons[buttons.length - 1]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
forms.push({
|
|
161
|
+
action: form.getAttribute('action') || '',
|
|
162
|
+
method: (form.getAttribute('method') || 'get').toUpperCase(),
|
|
163
|
+
fields,
|
|
164
|
+
submit_selector: submitSel,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
return forms;
|
|
168
|
+
}
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# Helper
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
def build_selector(
|
|
177
|
+
tag: str,
|
|
178
|
+
element_id: str,
|
|
179
|
+
classes: list[str],
|
|
180
|
+
attributes: dict[str, str],
|
|
181
|
+
) -> str:
|
|
182
|
+
"""Generate a CSS selector with priority: id > data-testid > name > class > tag.
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
tag:
|
|
187
|
+
HTML element tag name (lower-cased).
|
|
188
|
+
element_id:
|
|
189
|
+
Value of the element's ``id`` attribute (empty string if absent).
|
|
190
|
+
classes:
|
|
191
|
+
List of CSS class names on the element.
|
|
192
|
+
attributes:
|
|
193
|
+
Dict of key HTML attributes (name, type, href, placeholder, etc.).
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
str
|
|
198
|
+
A CSS selector string that can be used to locate the element.
|
|
199
|
+
"""
|
|
200
|
+
if element_id:
|
|
201
|
+
return f"#{element_id}"
|
|
202
|
+
|
|
203
|
+
data_testid = attributes.get("data-testid", "")
|
|
204
|
+
if data_testid:
|
|
205
|
+
return f'[data-testid="{data_testid}"]'
|
|
206
|
+
|
|
207
|
+
name = attributes.get("name", "")
|
|
208
|
+
if name:
|
|
209
|
+
return f'{tag}[name="{name}"]'
|
|
210
|
+
|
|
211
|
+
if classes:
|
|
212
|
+
class_part = ".".join(classes)
|
|
213
|
+
return f"{tag}.{class_part}"
|
|
214
|
+
|
|
215
|
+
return tag
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
# Data model
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class DOMElement:
|
|
224
|
+
"""Represents a single DOM element extracted from a browser page.
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
tag:
|
|
229
|
+
Lower-cased HTML tag name (e.g. ``"input"``, ``"button"``).
|
|
230
|
+
element_id:
|
|
231
|
+
Value of the ``id`` attribute; empty string if absent.
|
|
232
|
+
classes:
|
|
233
|
+
List of CSS class names present on the element.
|
|
234
|
+
text:
|
|
235
|
+
Visible text content, truncated to 200 characters.
|
|
236
|
+
attributes:
|
|
237
|
+
Dict of key HTML attributes (name, type, href, placeholder,
|
|
238
|
+
aria-label, role, data-testid).
|
|
239
|
+
selector:
|
|
240
|
+
A generated CSS selector for this element.
|
|
241
|
+
is_interactive:
|
|
242
|
+
``True`` if the element is clickable or fillable.
|
|
243
|
+
bounding_box:
|
|
244
|
+
``{x, y, width, height}`` in pixels, or ``None`` if unavailable.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
tag: str
|
|
248
|
+
element_id: str
|
|
249
|
+
classes: list[str]
|
|
250
|
+
text: str
|
|
251
|
+
attributes: dict[str, str]
|
|
252
|
+
selector: str
|
|
253
|
+
is_interactive: bool
|
|
254
|
+
bounding_box: dict[str, float] | None = None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass
|
|
258
|
+
class DOMSnapshot:
|
|
259
|
+
"""Represents a full-page DOM snapshot captured from a browser.
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
url:
|
|
264
|
+
The page URL at capture time.
|
|
265
|
+
title:
|
|
266
|
+
The page ``<title>`` text.
|
|
267
|
+
timestamp:
|
|
268
|
+
ISO 8601 UTC timestamp of when the snapshot was taken.
|
|
269
|
+
elements:
|
|
270
|
+
All DOM elements discovered on the page.
|
|
271
|
+
forms:
|
|
272
|
+
Extracted form structures (action, method, fields).
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
url: str
|
|
276
|
+
title: str
|
|
277
|
+
timestamp: str
|
|
278
|
+
elements: list[DOMElement]
|
|
279
|
+
forms: list[dict[str, Any]] = field(default_factory=list)
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def interactive_elements(self) -> list[DOMElement]:
|
|
283
|
+
"""Return only the elements marked as interactive."""
|
|
284
|
+
return [el for el in self.elements if el.is_interactive]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
# Capture
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
async def capture_dom_snapshot(page: Any) -> DOMSnapshot:
|
|
292
|
+
"""Capture a :class:`DOMSnapshot` from a live Playwright ``Page``.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
page:
|
|
297
|
+
A Playwright ``Page`` object (typed as ``Any`` to avoid a hard
|
|
298
|
+
import-time dependency on ``playwright``).
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
-------
|
|
302
|
+
DOMSnapshot
|
|
303
|
+
The full DOM state of the page at call time.
|
|
304
|
+
"""
|
|
305
|
+
url: str = page.url
|
|
306
|
+
title: str = await page.title()
|
|
307
|
+
timestamp: str = datetime.now(timezone.utc).isoformat()
|
|
308
|
+
|
|
309
|
+
raw_elements: list[dict[str, Any]] = await page.evaluate(_DOM_EXTRACTOR_JS)
|
|
310
|
+
|
|
311
|
+
elements: list[DOMElement] = []
|
|
312
|
+
for raw in raw_elements:
|
|
313
|
+
tag = raw.get("tag", "")
|
|
314
|
+
element_id = raw.get("elementId", "")
|
|
315
|
+
classes = raw.get("classes", [])
|
|
316
|
+
text = raw.get("text", "")
|
|
317
|
+
attrs = raw.get("attrs", {})
|
|
318
|
+
is_interactive = raw.get("isInteractive", False)
|
|
319
|
+
bbox = raw.get("bbox")
|
|
320
|
+
|
|
321
|
+
selector = build_selector(tag, element_id, classes, attrs)
|
|
322
|
+
|
|
323
|
+
elements.append(
|
|
324
|
+
DOMElement(
|
|
325
|
+
tag=tag,
|
|
326
|
+
element_id=element_id,
|
|
327
|
+
classes=classes,
|
|
328
|
+
text=text,
|
|
329
|
+
attributes=attrs,
|
|
330
|
+
selector=selector,
|
|
331
|
+
is_interactive=is_interactive,
|
|
332
|
+
bounding_box=bbox,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
forms: list[dict[str, Any]] = await page.evaluate(_FORM_EXTRACTOR_JS)
|
|
337
|
+
|
|
338
|
+
logger.debug(
|
|
339
|
+
"capture_dom_snapshot: url=%r elements=%d forms=%d",
|
|
340
|
+
url, len(elements), len(forms),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return DOMSnapshot(
|
|
344
|
+
url=url,
|
|
345
|
+
title=title,
|
|
346
|
+
timestamp=timestamp,
|
|
347
|
+
elements=elements,
|
|
348
|
+
forms=forms,
|
|
349
|
+
)
|