From 01489aec59d6804883fce190b2465316b94376a0 Mon Sep 17 00:00:00 2001 From: Miguel Date: Wed, 27 Aug 2025 10:16:08 +0200 Subject: [PATCH] feat: Add Playwright MCP testing suite with various automation scripts - Implemented test client for Playwright MCP Server in `test-server.js` - Created global automation script for Playwright MCP-like functionality in `playwright-mcp-global.js` - Configured Playwright settings in `playwright.config.js` - Added quick start script for testing setup in `quick-start.sh` - Developed initial setup script for Playwright MCP configuration in `setup.js` - Created basic connectivity and configuration tests in `tests/basic.spec.js` and `tests/configuration.spec.js` - Implemented dashboard and plotting tests in `tests/dashboard.spec.js` and `tests/plotting.spec.js` - Added data streaming feature tests in `tests/streaming.spec.js` --- application_events.json | 712 ++++++++++++++++++- core/config_manager.py | 4 +- frontend/test-results/.last-run.json | 4 + mcp-config.json | 12 + system_state.json | 9 +- testing/.gitignore | 18 + testing/README.md | 291 ++++++++ testing/browser-automation.js | 255 +++++++ testing/example.js | 163 +++++ testing/mcp-server/README.md | 186 +++++ testing/mcp-server/src/server.js | 952 ++++++++++++++++++++++++++ testing/mcp-server/src/test-server.js | 140 ++++ testing/playwright-mcp-global.js | 190 +++++ testing/playwright.config.js | 83 +++ testing/quick-start.sh | 27 + testing/setup.js | 175 +++++ testing/tests/basic.spec.js | 11 + testing/tests/configuration.spec.js | 54 ++ testing/tests/dashboard.spec.js | 48 ++ testing/tests/plotting.spec.js | 77 +++ testing/tests/streaming.spec.js | 58 ++ 21 files changed, 3458 insertions(+), 11 deletions(-) create mode 100644 frontend/test-results/.last-run.json create mode 100644 mcp-config.json create mode 100644 testing/.gitignore create mode 100644 testing/README.md create mode 100644 testing/browser-automation.js create mode 100644 testing/example.js create mode 100644 testing/mcp-server/README.md create mode 100644 testing/mcp-server/src/server.js create mode 100644 testing/mcp-server/src/test-server.js create mode 100644 testing/playwright-mcp-global.js create mode 100644 testing/playwright.config.js create mode 100644 testing/quick-start.sh create mode 100644 testing/setup.js create mode 100644 testing/tests/basic.spec.js create mode 100644 testing/tests/configuration.spec.js create mode 100644 testing/tests/dashboard.spec.js create mode 100644 testing/tests/plotting.spec.js create mode 100644 testing/tests/streaming.spec.js diff --git a/application_events.json b/application_events.json index 148ddcb..4d87cef 100644 --- a/application_events.json +++ b/application_events.json @@ -1194,8 +1194,716 @@ "read_time_avg": 0.028721141815185546, "csv_write_time_avg": 0.0 } + }, + { + "timestamp": "2025-08-27T09:24:49.597012", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.2% CPU", + "details": { + "duration": 10.002967834472656, + "points_saved": 20, + "points_rate": 1.9994066092140317, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.2, + "cpu_max": 0.2, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.027550315856933592, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:24:59.600185", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.2% CPU", + "details": { + "duration": 10.003173112869263, + "points_saved": 20, + "points_rate": 1.9993655787351754, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.2, + "cpu_max": 0.2, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.024491679668426514, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:25:09.602871", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.3% CPU", + "details": { + "duration": 10.002686023712158, + "points_saved": 20, + "points_rate": 1.9994629395132886, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.3, + "cpu_max": 0.3, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.02641488313674927, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:25:19.605776", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.0% CPU", + "details": { + "duration": 10.00290560722351, + "points_saved": 20, + "points_rate": 1.9994190473573175, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.0, + "cpu_max": 0.0, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.02399500608444214, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:25:29.608737", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.2% CPU", + "details": { + "duration": 10.002960920333862, + "points_saved": 20, + "points_rate": 1.9994079912223104, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.2, + "cpu_max": 0.2, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.03274095058441162, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:25:39.612644", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.2% CPU", + "details": { + "duration": 10.003398895263672, + "points_saved": 20, + "points_rate": 1.9993204519185412, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.2, + "cpu_max": 0.2, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.024168848991394043, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:25:49.615990", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.3% CPU", + "details": { + "duration": 10.003854274749756, + "points_saved": 20, + "points_rate": 1.999229442044256, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.3, + "cpu_max": 0.3, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.024407362937927245, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:25:59.618730", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.0% CPU", + "details": { + "duration": 10.002739667892456, + "points_saved": 20, + "points_rate": 1.9994522164959967, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.0, + "cpu_max": 0.0, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.030548858642578124, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:26:09.621902", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.6% CPU", + "details": { + "duration": 10.003172159194946, + "points_saved": 20, + "points_rate": 1.9993657693490698, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.6, + "cpu_max": 0.6, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.025050425529479982, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:26:19.624848", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.0% CPU", + "details": { + "duration": 10.002945184707642, + "points_saved": 20, + "points_rate": 1.9994111364896523, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.0, + "cpu_max": 0.0, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.02940622568130493, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:26:29.627925", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.2% CPU", + "details": { + "duration": 10.003077507019043, + "points_saved": 20, + "points_rate": 1.999384687958904, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.2, + "cpu_max": 0.2, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.02933335304260254, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:26:39.631482", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.0% CPU", + "details": { + "duration": 10.003049612045288, + "points_saved": 20, + "points_rate": 1.9993902635369085, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.0, + "cpu_max": 0.0, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.025867748260498046, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:26:49.634184", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.0% CPU", + "details": { + "duration": 10.003209590911865, + "points_saved": 20, + "points_rate": 1.9993582877809977, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.0, + "cpu_max": 0.0, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.025532746315002443, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:26:59.638474", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.0% CPU", + "details": { + "duration": 10.003774642944336, + "points_saved": 20, + "points_rate": 1.9992453562621988, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.0, + "cpu_max": 0.0, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.024468183517456055, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:27:09.642164", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.5% CPU", + "details": { + "duration": 10.004204988479614, + "points_saved": 20, + "points_rate": 1.9991593557939973, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.5, + "cpu_max": 0.5, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.02346416711807251, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:27:19.646286", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 20 points saved, 0 lost, 0.0% CPU", + "details": { + "duration": 10.00360369682312, + "points_saved": 20, + "points_rate": 1.9992795202744258, + "variables_saved": 80, + "udp_points_sent": 80, + "points_lost": 0, + "cpu_average": 0.0, + "cpu_max": 0.0, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 0, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.02898404598236084, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:27:27.072110", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.02567434310913086, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:28.103420", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.028438329696655273, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:29.136603", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.02920389175415039, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:29.649444", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 14 points saved, 2 lost, 0.0% CPU", + "details": { + "duration": 10.003676652908325, + "points_saved": 14, + "points_rate": 1.3994854577721523, + "variables_saved": 56, + "udp_points_sent": 56, + "points_lost": 2, + "cpu_average": 0.0, + "cpu_max": 0.0, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 3, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.028916682515825545, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:27:30.172484", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.03212475776672363, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:31.202046", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.025721073150634766, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:32.233564", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.028671979904174805, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:33.267227", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.030137062072753906, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:34.293528", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.022943496704101562, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:35.326557", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.029216527938842773, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:36.355204", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.02415752410888672, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:37.390098", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.03090071678161621, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:38.423909", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.030216217041015625, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:39.454574", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.027634859085083008, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:39.653724", + "level": "info", + "event_type": "performance_report", + "message": "Performance report: 0 points saved, 10 lost, 1.2% CPU", + "details": { + "duration": 10.004280090332031, + "points_saved": 0, + "points_rate": 0.0, + "variables_saved": 0, + "udp_points_sent": 0, + "points_lost": 10, + "cpu_average": 1.2, + "cpu_max": 1.2, + "delay_average": 0.0, + "delay_max": 0.0, + "read_errors": 10, + "csv_errors": 0, + "udp_errors": 0, + "read_time_avg": 0.0, + "csv_write_time_avg": 0.0 + } + }, + { + "timestamp": "2025-08-27T09:27:40.489019", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.03096294403076172, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:41.522151", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.030019760131835938, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:42.553337", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.027550458908081055, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:43.585686", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.02818918228149414, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:44.621062", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.03146553039550781, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:45.652077", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.028136253356933594, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:46.683027", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.027202129364013672, + "variables_count": 4 + } + }, + { + "timestamp": "2025-08-27T09:27:47.716461", + "level": "error", + "event_type": "dataset_loop_error", + "message": "🚨 CRITICAL ERROR: Dataset 'DAR' recording loop error: cannot schedule new futures after shutdown", + "details": { + "dataset_id": "DAR", + "error": "cannot schedule new futures after shutdown", + "consecutive_errors": 1, + "priority": "CRITICAL", + "read_time": 0.02987360954284668, + "variables_count": 4 + } } ], - "last_updated": "2025-08-27T09:24:39.594044", - "total_entries": 54 + "last_updated": "2025-08-27T09:27:47.716461", + "total_entries": 93 } \ No newline at end of file diff --git a/core/config_manager.py b/core/config_manager.py index 8528998..a2f72df 100644 --- a/core/config_manager.py +++ b/core/config_manager.py @@ -537,9 +537,7 @@ class ConfigManager: self.save_configuration() return {"old_config": old_config, "new_config": self.csv_config} - def get_csv_directory_path(self) -> str: - """Get the configured CSV directory path""" - return self.csv_config["records_directory"] + def get_csv_file_directory_path(self) -> str: """Get the directory path for current day's CSV files""" diff --git a/frontend/test-results/.last-run.json b/frontend/test-results/.last-run.json new file mode 100644 index 0000000..5fca3f8 --- /dev/null +++ b/frontend/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file diff --git a/mcp-config.json b/mcp-config.json new file mode 100644 index 0000000..f2a0fc8 --- /dev/null +++ b/mcp-config.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "playwright-automation": { + "command": "node", + "args": ["src/server.js"], + "cwd": "testing/mcp-server", + "env": { + "NODE_ENV": "production" + } + } + } +} diff --git a/system_state.json b/system_state.json index c381a17..9623168 100644 --- a/system_state.json +++ b/system_state.json @@ -1,12 +1,9 @@ { "last_state": { - "should_connect": true, + "should_connect": false, "should_stream": false, - "active_datasets": [ - "DAR" - ] + "active_datasets": [] }, "auto_recovery_enabled": true, - "last_update": "2025-08-27T09:24:15.915232", - "plotjuggler_path": "C:\\Program Files\\PlotJuggler\\plotjuggler.exe" + "last_update": "2025-08-27T09:27:26.829523" } \ No newline at end of file diff --git a/testing/.gitignore b/testing/.gitignore new file mode 100644 index 0000000..0d0dcf8 --- /dev/null +++ b/testing/.gitignore @@ -0,0 +1,18 @@ +# Playwright +test-results/ +playwright-report/ +playwright/.cache/ + +# Screenshots y videos +screenshots/*.png +videos/*.webm + +# Reportes y logs +*.json +*.log + +# Node modules +node_modules/ + +# Environment +.env.local diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 0000000..a7b29bd --- /dev/null +++ b/testing/README.md @@ -0,0 +1,291 @@ +# Playwright MCP Automation for PLC Streamer + +Este directorio contiene la configuración completa de Playwright para automatización y testing del proyecto PLC Streamer, con funcionalidad similar a MCP (Model Context Protocol) para interacción con navegadores. + +## 🚀 Instalación Global + +### 1. Instalar Playwright globalmente +```bash +npm install -g playwright +npx playwright install +``` + +### 2. Configurar para uso en otros proyectos +```bash +# Copiar el script global a un directorio en PATH +cp playwright-mcp-global.js ~/.local/bin/playwright-mcp +chmod +x ~/.local/bin/playwright-mcp + +# O en Windows, copiar a un directorio en PATH +copy playwright-mcp-global.js C:\tools\playwright-mcp.js +``` + +## 📁 Estructura del Proyecto + +``` +testing/ +├── package.json # Dependencias de testing +├── playwright.config.js # Configuración principal +├── browser-automation.js # Automatización específica del proyecto +├── playwright-mcp-global.js # Script global reutilizable +├── tests/ # Tests específicos +│ ├── dashboard.spec.js +│ ├── configuration.spec.js +│ ├── streaming.spec.js +│ └── plotting.spec.js +├── screenshots/ # Capturas de pantalla +├── videos/ # Grabaciones de video +└── README.md # Este archivo +``` + +## 🛠️ Uso Local (Proyecto PLC Streamer) + +### Ejecutar Tests +```bash +cd testing + +# Ejecutar todos los tests +npm test + +# Ejecutar con interfaz visual +npm run test:headed + +# Ejecutar con modo debug +npm run test:debug + +# Ejecutar con UI de Playwright +npm run ui +``` + +### Generar Código de Test +```bash +# Generar código interactivo +npm run codegen + +# Generar código específico para la app +npm run codegen:app +``` + +### Automatización Específica del Proyecto +```bash +# Ejecutar automatización completa +node browser-automation.js + +# O importar en otro script +import PLCStreamerBrowserAutomation from './browser-automation.js'; + +const automation = new PLCStreamerBrowserAutomation(); +await automation.initBrowser('chromium'); +await automation.testConfigurationFlow(); +``` + +## 🌐 Uso Global (Otros Proyectos) + +### Script CLI Global +```bash +# Ejecutar tests básicos +node playwright-mcp-global.js test --url http://localhost:3000 --browser chromium + +# Capturar screenshots +node playwright-mcp-global.js capture --url http://localhost:8080 --output ./captures + +# Monitorear performance +node playwright-mcp-global.js monitor --url http://localhost:3000 --duration 120 +``` + +### Ejemplo de Integración en Otros Proyectos +```javascript +import { chromium } from 'playwright'; + +// Para React apps +const testReactApp = async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + + await page.goto('http://localhost:3000'); + await page.waitForSelector('[data-testid="app"]'); + + // Tu lógica de testing aquí + + await browser.close(); +}; + +// Para Vue apps +const testVueApp = async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + + await page.goto('http://localhost:8080'); + await page.waitForSelector('#app'); + + // Tu lógica de testing aquí + + await browser.close(); +}; +``` + +## 🔧 Funcionalidades MCP-like + +### 1. Automatización de Navegador +- Control completo del navegador (Chromium, Firefox, WebKit) +- Interacción con elementos de la página +- Manejo de formularios y eventos +- Navegación automática entre páginas + +### 2. Captura de Datos +- Screenshots automáticos +- Grabación de video de sesiones +- Métricas de performance +- Logs de consola y errores + +### 3. Monitoreo en Tiempo Real +- Estado de conexión PLC +- Performance de la aplicación +- Uso de memoria +- Tiempos de carga + +### 4. Reporting Automático +- Generación de reportes JSON +- Métricas de performance +- Estados de la aplicación +- Capturas de evidencia + +## 📊 Tests Específicos del PLC Streamer + +### Dashboard Tests (`dashboard.spec.js`) +- Carga correcta de la aplicación +- Navegación entre tabs +- Estado de conexión PLC +- Elementos de interfaz + +### Configuration Tests (`configuration.spec.js`) +- Formularios de configuración +- Validación de datos +- Guardado de configuraciones +- Manejo de errores + +### Streaming Tests (`streaming.spec.js`) +- Control de streaming +- Gestión de variables +- Configuración de datasets +- Estados de streaming + +### Plotting Tests (`plotting.spec.js`) +- Creación de plots +- Actualización en tiempo real +- Exportación de datos +- Gestión de sesiones + +## 🚀 Comandos Rápidos + +```bash +# Setup inicial +npm install + +# Test completo con reporte +npm test -- --reporter=html + +# Debug test específico +npx playwright test dashboard.spec.js --debug + +# Generar código para nueva funcionalidad +npx playwright codegen http://localhost:5173/app + +# Ejecutar con todos los navegadores +npx playwright test --project=chromium --project=firefox --project=webkit + +# Solo móviles +npx playwright test --project="Mobile Chrome" --project="Mobile Safari" +``` + +## 🔍 Debugging + +### Ver Reports HTML +```bash +npx playwright show-report +``` + +### Ver Trace Viewer +```bash +npx playwright show-trace trace.zip +``` + +### Modo Debug Interactivo +```bash +npx playwright test --debug +``` + +## 🌟 Características Avanzadas + +### 1. Test Paralelo +Los tests se ejecutan en paralelo por defecto para mayor velocidad. + +### 2. Auto-waiting +Playwright espera automáticamente a que los elementos estén listos. + +### 3. Cross-browser +Soporte nativo para Chromium, Firefox y WebKit. + +### 4. Mobile Testing +Tests en viewports móviles incluidos. + +### 5. Video Recording +Grabación automática de videos en fallos. + +### 6. Network Interception +Capacidad de interceptar y modificar requests de red. + +## 🔧 Configuración Personalizada + +### Modificar `playwright.config.js` +```javascript +export default defineConfig({ + // Tu configuración personalizada + testDir: './tests', + timeout: 30000, + retries: 2, + use: { + baseURL: 'http://localhost:5173', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + } +}); +``` + +### Variables de Entorno +```bash +# Configurar URLs dinámicamente +FRONTEND_URL=http://localhost:3000 npm test +BACKEND_URL=http://localhost:8000 npm test +``` + +## 📝 Integración con CI/CD + +### GitHub Actions Example +```yaml +- name: Install Playwright + run: npx playwright install + +- name: Run tests + run: npm test + +- name: Upload test results + uses: actions/upload-artifact@v3 + with: + name: playwright-report + path: playwright-report/ +``` + +## 🤝 Contribuir + +1. Agregar nuevos tests en `/tests/` +2. Extender `browser-automation.js` con nuevas funcionalidades +3. Actualizar configuración en `playwright.config.js` +4. Documentar cambios en este README + +## 📚 Recursos Adicionales + +- [Playwright Documentation](https://playwright.dev/) +- [Playwright Test](https://playwright.dev/docs/test-intro) +- [Playwright Inspector](https://playwright.dev/docs/inspector) +- [Playwright Trace Viewer](https://playwright.dev/docs/trace-viewer) diff --git a/testing/browser-automation.js b/testing/browser-automation.js new file mode 100644 index 0000000..03719db --- /dev/null +++ b/testing/browser-automation.js @@ -0,0 +1,255 @@ +import { chromium, firefox, webkit } from 'playwright'; +import fs from 'fs/promises'; +import path from 'path'; + +/** + * PLC Streamer Browser Automation and Testing Suite + * This class provides MCP-like functionality for browser automation + */ +class PLCStreamerBrowserAutomation { + constructor() { + this.browser = null; + this.context = null; + this.page = null; + this.baseURL = 'http://localhost:5173'; + this.backendURL = 'http://localhost:5000'; + } + + /** + * Initialize browser instance + * @param {string} browserType - 'chromium', 'firefox', or 'webkit' + * @param {object} options - Browser launch options + */ + async initBrowser(browserType = 'chromium', options = {}) { + const browserLaunchers = { + chromium: chromium, + firefox: firefox, + webkit: webkit + }; + + const defaultOptions = { + headless: false, + slowMo: 100, + args: ['--disable-web-security', '--disable-features=VizDisplayCompositor'] + }; + + this.browser = await browserLaunchers[browserType].launch({ + ...defaultOptions, + ...options + }); + + this.context = await this.browser.newContext({ + viewport: { width: 1920, height: 1080 }, + recordVideo: { dir: 'videos/' } + }); + + this.page = await this.context.newPage(); + + // Add console logging + this.page.on('console', msg => { + console.log(`Browser Console [${msg.type()}]: ${msg.text()}`); + }); + + // Add error logging + this.page.on('pageerror', error => { + console.error(`Browser Error: ${error.message}`); + }); + + return this.page; + } + + /** + * Navigate to the PLC Streamer application + */ + async navigateToApp() { + await this.page.goto(`${this.baseURL}/app`); + await this.page.waitForLoadState('networkidle'); + return this.page; + } + + /** + * Check if backend server is running + */ + async checkBackendStatus() { + try { + const response = await this.page.request.get(`${this.backendURL}/api/status`); + return response.ok(); + } catch (error) { + console.error('Backend not reachable:', error.message); + return false; + } + } + + /** + * Monitor PLC connection status + */ + async monitorPLCConnection() { + await this.navigateToApp(); + + const connectionStatus = await this.page.locator('[data-testid="connection-status"]').textContent(); + console.log(`PLC Connection Status: ${connectionStatus}`); + + return connectionStatus; + } + + /** + * Automated configuration testing + */ + async testConfigurationFlow() { + await this.navigateToApp(); + + // Navigate to configuration + await this.page.click('text=Configuration'); + await this.page.waitForSelector('[data-testid="configuration-panel"]'); + + // Test PLC configuration + const ipInput = this.page.locator('input[name*="ip"]').first(); + await ipInput.clear(); + await ipInput.fill('192.168.1.100'); + + // Save configuration + await this.page.click('button:has-text("Save")'); + + // Wait for save confirmation + await this.page.waitForSelector('text*=saved', { timeout: 5000 }); + + console.log('Configuration test completed successfully'); + return true; + } + + /** + * Automated streaming test + */ + async testStreamingFlow() { + await this.navigateToApp(); + + // Navigate to streaming + await this.page.click('text=Data Streaming'); + await this.page.waitForSelector('[data-testid="streaming-panel"]'); + + // Start streaming + await this.page.click('button:has-text("Start Streaming")'); + + // Wait a bit for streaming to initialize + await this.page.waitForTimeout(3000); + + // Stop streaming + await this.page.click('button:has-text("Stop Streaming")'); + + console.log('Streaming test completed successfully'); + return true; + } + + /** + * Take screenshots for documentation + */ + async captureScreenshots() { + await this.navigateToApp(); + + const tabs = ['Dashboard', 'Configuration', 'Data Streaming', 'Plots']; + + for (const tab of tabs) { + await this.page.click(`text=${tab}`); + await this.page.waitForTimeout(1000); + + const screenshot = await this.page.screenshot({ + path: `screenshots/${tab.toLowerCase().replace(' ', '_')}.png`, + fullPage: true + }); + + console.log(`Screenshot saved: ${tab}`); + } + + return true; + } + + /** + * Performance monitoring + */ + async monitorPerformance() { + await this.navigateToApp(); + + // Start performance monitoring + await this.page.evaluate(() => performance.mark('test-start')); + + // Navigate through all tabs + const tabs = ['Configuration', 'Data Streaming', 'Plots', 'Dashboard']; + + for (const tab of tabs) { + const startTime = performance.now(); + await this.page.click(`text=${tab}`); + await this.page.waitForLoadState('networkidle'); + const endTime = performance.now(); + + console.log(`Tab ${tab} load time: ${endTime - startTime}ms`); + } + + // Get performance metrics + const metrics = await this.page.evaluate(() => { + return JSON.stringify(performance.getEntriesByType('navigation')); + }); + + await fs.writeFile('performance-metrics.json', metrics); + console.log('Performance metrics saved'); + + return JSON.parse(metrics); + } + + /** + * Generate test report + */ + async generateReport() { + const backendStatus = await this.checkBackendStatus(); + const plcStatus = await this.monitorPLCConnection(); + + const report = { + timestamp: new Date().toISOString(), + backendStatus: backendStatus ? 'Online' : 'Offline', + plcConnectionStatus: plcStatus, + browserInfo: await this.page.evaluate(() => navigator.userAgent), + url: this.page.url() + }; + + await fs.writeFile('automation-report.json', JSON.stringify(report, null, 2)); + console.log('Report generated: automation-report.json'); + + return report; + } + + /** + * Cleanup resources + */ + async cleanup() { + if (this.page) await this.page.close(); + if (this.context) await this.context.close(); + if (this.browser) await this.browser.close(); + } +} + +// Export for use in other scripts +export default PLCStreamerBrowserAutomation; + +// CLI usage example +if (import.meta.url === `file://${process.argv[1]}`) { + const automation = new PLCStreamerBrowserAutomation(); + + try { + await automation.initBrowser('chromium', { headless: false }); + + console.log('Starting automated testing...'); + + // Run all tests + await automation.testConfigurationFlow(); + await automation.testStreamingFlow(); + await automation.captureScreenshots(); + await automation.monitorPerformance(); + await automation.generateReport(); + + console.log('Automation completed successfully!'); + + } catch (error) { + console.error('Automation failed:', error); + } finally { + await automation.cleanup(); + } +} diff --git a/testing/example.js b/testing/example.js new file mode 100644 index 0000000..f87ccd8 --- /dev/null +++ b/testing/example.js @@ -0,0 +1,163 @@ +import PLCStreamerBrowserAutomation from './browser-automation.js'; + +/** + * Ejemplo de uso de la automatización de Playwright para PLC Streamer + */ +async function runExample() { + const automation = new PLCStreamerBrowserAutomation(); + + try { + console.log('🚀 Iniciando automatización de ejemplo...'); + + // Inicializar navegador + await automation.initBrowser('chromium', { + headless: false, + slowMo: 500 // Ralentizar para ver las acciones + }); + + console.log('✅ Navegador inicializado'); + + // Verificar que el backend esté funcionando + const backendStatus = await automation.checkBackendStatus(); + console.log(`🔧 Estado del backend: ${backendStatus ? 'Online' : 'Offline'}`); + + if (!backendStatus) { + console.log('⚠️ Backend no disponible. Asegúrate de que esté ejecutándose en puerto 5000'); + return; + } + + // Navegar a la aplicación + console.log('🌐 Navegando a la aplicación...'); + await automation.navigateToApp(); + + // Monitorear estado PLC + console.log('🔌 Verificando estado de conexión PLC...'); + const plcStatus = await automation.monitorPLCConnection(); + console.log(`PLC Status: ${plcStatus}`); + + // Test de configuración + console.log('⚙️ Ejecutando test de configuración...'); + await automation.testConfigurationFlow(); + + // Test de streaming + console.log('📊 Ejecutando test de streaming...'); + await automation.testStreamingFlow(); + + // Capturar screenshots + console.log('📸 Capturando screenshots...'); + await automation.captureScreenshots(); + + // Monitorear performance + console.log('⚡ Analizando performance...'); + await automation.monitorPerformance(); + + // Generar reporte + console.log('📋 Generando reporte...'); + const report = await automation.generateReport(); + + console.log('🎉 Automatización completada exitosamente!'); + console.log('📊 Reporte:', report); + + } catch (error) { + console.error('❌ Error en la automatización:', error); + } finally { + // Cleanup + await automation.cleanup(); + console.log('🧹 Recursos liberados'); + } +} + +// Función para testing específico +async function testSpecificFeature(feature) { + const automation = new PLCStreamerBrowserAutomation(); + + try { + await automation.initBrowser('chromium', { headless: false }); + await automation.navigateToApp(); + + switch (feature) { + case 'configuration': + await automation.testConfigurationFlow(); + break; + case 'streaming': + await automation.testStreamingFlow(); + break; + case 'screenshots': + await automation.captureScreenshots(); + break; + case 'performance': + await automation.monitorPerformance(); + break; + default: + console.log('Características disponibles: configuration, streaming, screenshots, performance'); + } + + } catch (error) { + console.error('Error:', error); + } finally { + await automation.cleanup(); + } +} + +// Función para monitoreo continuo +async function continuousMonitoring(durationMinutes = 5) { + const automation = new PLCStreamerBrowserAutomation(); + + try { + await automation.initBrowser('chromium', { headless: true }); + await automation.navigateToApp(); + + const endTime = Date.now() + (durationMinutes * 60 * 1000); + + console.log(`🔄 Iniciando monitoreo continuo por ${durationMinutes} minutos...`); + + while (Date.now() < endTime) { + const status = await automation.monitorPLCConnection(); + const timestamp = new Date().toISOString(); + + console.log(`[${timestamp}] PLC Status: ${status}`); + + // Esperar 30 segundos antes del siguiente check + await new Promise(resolve => setTimeout(resolve, 30000)); + } + + console.log('✅ Monitoreo continuo completado'); + + } catch (error) { + console.error('Error en monitoreo:', error); + } finally { + await automation.cleanup(); + } +} + +// Detectar argumentos de línea de comandos +const args = process.argv.slice(2); + +if (args.length === 0) { + // Ejecutar ejemplo completo + runExample(); +} else if (args[0] === 'test' && args[1]) { + // Ejecutar test específico + testSpecificFeature(args[1]); +} else if (args[0] === 'monitor') { + // Ejecutar monitoreo continuo + const duration = args[1] ? parseInt(args[1]) : 5; + continuousMonitoring(duration); +} else { + console.log(` +Uso: + node example.js # Ejecutar ejemplo completo + node example.js test # Ejecutar test específico + node example.js monitor [minutes] # Monitoreo continuo + +Características disponibles: + - configuration + - streaming + - screenshots + - performance + +Ejemplos: + node example.js test configuration + node example.js monitor 10 + `); +} diff --git a/testing/mcp-server/README.md b/testing/mcp-server/README.md new file mode 100644 index 0000000..0239f81 --- /dev/null +++ b/testing/mcp-server/README.md @@ -0,0 +1,186 @@ +# Playwright MCP Server para GitHub Copilot + +Este es un servidor MCP (Model Context Protocol) que permite a GitHub Copilot controlar navegadores web usando Playwright para automatizar tu aplicación PLC Streamer. + +## 🎯 ¿Qué hace esto? + +Permite que GitHub Copilot: +- **Lance navegadores** automáticamente +- **Navegue por tu aplicación** PLC Streamer +- **Interactúe con elementos** (clicks, llenar formularios, etc.) +- **Capture screenshots** y datos +- **Monitoree el estado PLC** en tiempo real +- **Ejecute tests automáticos** de tu aplicación + +## 🚀 Configuración para GitHub Copilot + +### 1. Configuración Global de MCP + +Crea o edita el archivo de configuración MCP en tu sistema: + +**Windows:** +``` +%APPDATA%\Code\User\globalStorage\github.copilot-chat\mcpServers.json +``` + +**macOS:** +``` +~/Library/Application Support/Code/User/globalStorage/github.copilot-chat/mcpServers.json +``` + +**Linux:** +``` +~/.config/Code/User/globalStorage/github.copilot-chat/mcpServers.json +``` + +### 2. Contenido del archivo de configuración: + +```json +{ + "mcpServers": { + "playwright-automation": { + "command": "node", + "args": ["src/server.js"], + "cwd": "D:\\Proyectos\\Scripts\\Siemens\\S7_snap7_Streamer_n_Log\\testing\\mcp-server", + "env": { + "NODE_ENV": "production" + } + } + } +} +``` + +**⚠️ Importante:** Cambia la ruta `cwd` por tu ruta absoluta real del proyecto. + +### 3. Reiniciar VS Code + +Después de agregar la configuración, reinicia VS Code para que GitHub Copilot detecte el servidor MCP. + +## 🧪 Probar el Servidor + +### Prueba manual: +```bash +cd testing/mcp-server +npm test +``` + +### Verificar conexión: +```bash +cd testing/mcp-server +npm start +# El servidor debería mostrar: "Playwright MCP Server running on stdio" +``` + +## 💬 Comandos para GitHub Copilot + +Una vez configurado, puedes usar estos comandos con GitHub Copilot: + +### Básicos: +``` +@copilot Abre un navegador y navega a mi aplicación PLC Streamer +@copilot Toma una captura de pantalla de la aplicación +@copilot Obtén el estado de conexión del PLC +@copilot Haz click en el tab "Configuration" +``` + +### Automatización PLC: +``` +@copilot Configura el PLC con IP 192.168.1.100 +@copilot Inicia el streaming de datos +@copilot Monitorea el estado del streaming por 30 segundos +@copilot Crea un plot con las variables de temperatura +``` + +### Testing automatizado: +``` +@copilot Ejecuta un test completo de la aplicación +@copilot Verifica que todos los tabs funcionen correctamente +@copilot Prueba la configuración PLC con diferentes IPs +@copilot Captura métricas de performance de la aplicación +``` + +### Debugging: +``` +@copilot Navega a la aplicación y dime si hay errores en la consola +@copilot Verifica si el backend está respondiendo +@copilot Toma screenshots de todos los tabs de la aplicación +``` + +## 🛠️ Herramientas Disponibles + +El servidor MCP proporciona estas herramientas a Copilot: + +### Navegador: +- `launch_browser` - Lanzar navegador +- `navigate_to` - Navegar a URL +- `close_browser` - Cerrar navegador +- `take_screenshot` - Capturar pantalla +- `get_page_info` - Info de la página + +### Interacción: +- `click_element` - Hacer click +- `fill_input` - Llenar campos +- `get_text` - Obtener texto +- `wait_for_element` - Esperar elemento +- `execute_script` - Ejecutar JavaScript + +### PLC Específico: +- `get_plc_status` - Estado PLC +- `test_configuration` - Probar config PLC +- `monitor_streaming` - Monitorear streaming +- `capture_performance` - Métricas de performance + +## 🔧 Troubleshooting + +### Error: "No se puede conectar al servidor MCP" +1. Verifica que la ruta en `mcpServers.json` sea correcta +2. Asegúrate de que Node.js esté instalado +3. Reinicia VS Code completamente + +### Error: "Herramientas no disponibles" +1. Verifica que el servidor inicie sin errores +2. Revisa los logs en la consola de VS Code +3. Ejecuta la prueba manual: `npm test` + +### El navegador no se abre: +1. Verifica que Playwright esté instalado: `npx playwright install` +2. Prueba manualmente: `node src/test-server.js` + +### La aplicación no responde: +1. Asegúrate de que el frontend esté corriendo: `npm run dev` en `frontend/` +2. Verifica que el backend esté activo (puerto 5000) + +## 📊 Ejemplo de Uso Completo + +```bash +# 1. Usuario pregunta a Copilot: +"Abre mi aplicación PLC Streamer, configura el PLC con IP 192.168.1.50, +inicia el streaming y toma una captura de pantalla" + +# 2. Copilot ejecutará automáticamente: +- launch_browser() +- navigate_to("http://localhost:5173/app") +- click_element("Configuration") +- test_configuration({ip: "192.168.1.50"}) +- click_element("Data Streaming") +- monitor_streaming({action: "start"}) +- take_screenshot() +``` + +## 🔒 Seguridad + +- El servidor solo acepta conexiones locales +- No expone datos sensibles del PLC +- Las sesiones de navegador se aíslan automáticamente +- Los screenshots se guardan localmente + +## 📈 Logs y Debugging + +Los logs del servidor aparecen en: +- Consola de VS Code (panel "Output" → "GitHub Copilot Chat") +- Terminal donde ejecutes `npm start` +- Archivos de error en `testing/mcp-server/logs/` + +--- + +¡Con esta configuración, GitHub Copilot puede controlar tu aplicación PLC Streamer como si fuera un asistente experto en automatización industrial! 🏭🤖 diff --git a/testing/mcp-server/src/server.js b/testing/mcp-server/src/server.js new file mode 100644 index 0000000..65400f6 --- /dev/null +++ b/testing/mcp-server/src/server.js @@ -0,0 +1,952 @@ +#!/usr/bin/env node + +/** + * Playwright MCP Server + * Provides browser automation capabilities to GitHub Copilot via MCP protocol + */ + +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { chromium, firefox, webkit } from 'playwright'; +import { v4 as uuidv4 } from 'uuid'; + +class PlaywrightMCPServer { + constructor() { + this.server = new Server( + { + name: 'playwright-automation', + version: '1.0.0' + }, + { + capabilities: { + tools: {} + } + } + ); + + this.browsers = new Map(); + this.pages = new Map(); + this.sessions = new Map(); + + this.setupTools(); + } + + setupTools() { + // Tool: Launch Browser + this.server.setRequestHandler('tools/call', async (request) => { + const { name, arguments: args } = request.params; + + switch (name) { + case 'launch_browser': + return await this.launchBrowser(args); + + case 'navigate_to': + return await this.navigateTo(args); + + case 'click_element': + return await this.clickElement(args); + + case 'fill_input': + return await this.fillInput(args); + + case 'get_text': + return await this.getText(args); + + case 'take_screenshot': + return await this.takeScreenshot(args); + + case 'wait_for_element': + return await this.waitForElement(args); + + case 'get_page_info': + return await this.getPageInfo(args); + + case 'close_browser': + return await this.closeBrowser(args); + + case 'execute_script': + return await this.executeScript(args); + + case 'get_plc_status': + return await this.getPLCStatus(args); + + case 'test_configuration': + return await this.testConfiguration(args); + + case 'monitor_streaming': + return await this.monitorStreaming(args); + + case 'capture_performance': + return await this.capturePerformance(args); + + default: + throw new Error(`Unknown tool: ${name}`); + } + }); + + // List available tools + this.server.setRequestHandler('tools/list', async () => { + return { + tools: [ + { + name: 'launch_browser', + description: 'Launch a new browser instance', + inputSchema: { + type: 'object', + properties: { + browserType: { + type: 'string', + enum: ['chromium', 'firefox', 'webkit'], + default: 'chromium', + description: 'Browser type to launch' + }, + headless: { + type: 'boolean', + default: false, + description: 'Run browser in headless mode' + }, + viewport: { + type: 'object', + properties: { + width: { type: 'number', default: 1920 }, + height: { type: 'number', default: 1080 } + } + } + } + } + }, + { + name: 'navigate_to', + description: 'Navigate to a URL', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' }, + url: { type: 'string', description: 'URL to navigate to' }, + waitUntil: { + type: 'string', + enum: ['load', 'domcontentloaded', 'networkidle'], + default: 'load' + } + }, + required: ['sessionId', 'url'] + } + }, + { + name: 'click_element', + description: 'Click on an element', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' }, + selector: { type: 'string', description: 'CSS selector for the element' }, + text: { type: 'string', description: 'Text content to match (alternative to selector)' } + }, + required: ['sessionId'] + } + }, + { + name: 'fill_input', + description: 'Fill an input field', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' }, + selector: { type: 'string', description: 'CSS selector for the input' }, + value: { type: 'string', description: 'Value to fill' }, + clear: { type: 'boolean', default: true, description: 'Clear field before filling' } + }, + required: ['sessionId', 'selector', 'value'] + } + }, + { + name: 'get_text', + description: 'Get text content from an element', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' }, + selector: { type: 'string', description: 'CSS selector for the element' } + }, + required: ['sessionId', 'selector'] + } + }, + { + name: 'take_screenshot', + description: 'Take a screenshot of the page', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' }, + path: { type: 'string', description: 'Path to save screenshot' }, + fullPage: { type: 'boolean', default: false, description: 'Capture full page' } + }, + required: ['sessionId'] + } + }, + { + name: 'wait_for_element', + description: 'Wait for an element to appear', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' }, + selector: { type: 'string', description: 'CSS selector for the element' }, + timeout: { type: 'number', default: 30000, description: 'Timeout in milliseconds' }, + state: { + type: 'string', + enum: ['attached', 'detached', 'visible', 'hidden'], + default: 'visible' + } + }, + required: ['sessionId', 'selector'] + } + }, + { + name: 'get_page_info', + description: 'Get information about the current page', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' } + }, + required: ['sessionId'] + } + }, + { + name: 'close_browser', + description: 'Close a browser session', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' } + }, + required: ['sessionId'] + } + }, + { + name: 'execute_script', + description: 'Execute JavaScript on the page', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' }, + script: { type: 'string', description: 'JavaScript code to execute' } + }, + required: ['sessionId', 'script'] + } + }, + { + name: 'get_plc_status', + description: 'Get PLC connection status from the PLC Streamer app', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' } + }, + required: ['sessionId'] + } + }, + { + name: 'test_configuration', + description: 'Test PLC configuration in the app', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' }, + ip: { type: 'string', description: 'PLC IP address' }, + rack: { type: 'number', description: 'PLC rack number' }, + slot: { type: 'number', description: 'PLC slot number' } + }, + required: ['sessionId', 'ip'] + } + }, + { + name: 'monitor_streaming', + description: 'Monitor data streaming status', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' }, + action: { + type: 'string', + enum: ['start', 'stop', 'status'], + description: 'Streaming action to perform' + } + }, + required: ['sessionId', 'action'] + } + }, + { + name: 'capture_performance', + description: 'Capture performance metrics from the application', + inputSchema: { + type: 'object', + properties: { + sessionId: { type: 'string', description: 'Browser session ID' } + }, + required: ['sessionId'] + } + } + ] + }; + }); + } + + async launchBrowser(args) { + const { browserType = 'chromium', headless = false, viewport = { width: 1920, height: 1080 } } = args; + + try { + const browsers = { chromium, firefox, webkit }; + const browser = await browsers[browserType].launch({ + headless, + slowMo: 100, + args: ['--disable-web-security', '--disable-features=VizDisplayCompositor'] + }); + + const context = await browser.newContext({ viewport }); + const page = await context.newPage(); + + const sessionId = uuidv4(); + + this.browsers.set(sessionId, browser); + this.pages.set(sessionId, page); + this.sessions.set(sessionId, { + browser, + context, + page, + browserType, + createdAt: new Date() + }); + + // Add console and error logging + page.on('console', msg => { + console.log(`[${sessionId}] Console [${msg.type()}]: ${msg.text()}`); + }); + + page.on('pageerror', error => { + console.error(`[${sessionId}] Page Error: ${error.message}`); + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + sessionId, + browserType, + message: `Browser ${browserType} launched successfully` + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async navigateTo(args) { + const { sessionId, url, waitUntil = 'load' } = args; + + try { + const page = this.pages.get(sessionId); + if (!page) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + await page.goto(url, { waitUntil }); + + const title = await page.title(); + const currentUrl = page.url(); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + title, + url: currentUrl, + message: `Navigated to ${url}` + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async clickElement(args) { + const { sessionId, selector, text } = args; + + try { + const page = this.pages.get(sessionId); + if (!page) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + let locator; + if (text) { + locator = page.getByText(text); + } else if (selector) { + locator = page.locator(selector); + } else { + throw new Error('Either selector or text must be provided'); + } + + await locator.click(); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + message: `Clicked element ${selector || text}` + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async fillInput(args) { + const { sessionId, selector, value, clear = true } = args; + + try { + const page = this.pages.get(sessionId); + if (!page) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + const input = page.locator(selector); + + if (clear) { + await input.clear(); + } + + await input.fill(value); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + message: `Filled input ${selector} with value` + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async getText(args) { + const { sessionId, selector } = args; + + try { + const page = this.pages.get(sessionId); + if (!page) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + const text = await page.locator(selector).textContent(); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + text, + selector + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async takeScreenshot(args) { + const { sessionId, path, fullPage = false } = args; + + try { + const page = this.pages.get(sessionId); + if (!page) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + const screenshotPath = path || `screenshots/screenshot-${sessionId}-${Date.now()}.png`; + + await page.screenshot({ + path: screenshotPath, + fullPage + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + path: screenshotPath, + message: 'Screenshot captured successfully' + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async waitForElement(args) { + const { sessionId, selector, timeout = 30000, state = 'visible' } = args; + + try { + const page = this.pages.get(sessionId); + if (!page) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + await page.locator(selector).waitFor({ state, timeout }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + message: `Element ${selector} is ${state}` + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async getPageInfo(args) { + const { sessionId } = args; + + try { + const page = this.pages.get(sessionId); + if (!page) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + const title = await page.title(); + const url = page.url(); + const viewport = page.viewportSize(); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + title, + url, + viewport + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async closeBrowser(args) { + const { sessionId } = args; + + try { + const session = this.sessions.get(sessionId); + if (!session) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + await session.browser.close(); + + this.browsers.delete(sessionId); + this.pages.delete(sessionId); + this.sessions.delete(sessionId); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + message: `Browser session ${sessionId} closed` + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async executeScript(args) { + const { sessionId, script } = args; + + try { + const page = this.pages.get(sessionId); + if (!page) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + const result = await page.evaluate(script); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + result + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + // PLC-specific tools + async getPLCStatus(args) { + const { sessionId } = args; + + try { + const page = this.pages.get(sessionId); + if (!page) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + // Navigate to PLC Streamer app if not already there + const currentUrl = page.url(); + if (!currentUrl.includes('localhost:5173')) { + await page.goto('http://localhost:5173/app'); + await page.waitForLoadState('networkidle'); + } + + // Get connection status + const statusElement = page.locator('[data-testid="connection-status"]'); + const status = await statusElement.textContent().catch(() => 'Unknown'); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + plcStatus: status, + timestamp: new Date().toISOString() + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async testConfiguration(args) { + const { sessionId, ip, rack = 0, slot = 1 } = args; + + try { + const page = this.pages.get(sessionId); + if (!page) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + // Navigate to configuration tab + await page.click('text=Configuration'); + await page.waitForSelector('[data-testid="configuration-panel"]'); + + // Fill PLC configuration + const ipInput = page.locator('input[name*="ip"]').first(); + await ipInput.clear(); + await ipInput.fill(ip); + + if (rack !== undefined) { + const rackInput = page.locator('input[name*="rack"]').first(); + await rackInput.clear(); + await rackInput.fill(rack.toString()); + } + + if (slot !== undefined) { + const slotInput = page.locator('input[name*="slot"]').first(); + await slotInput.clear(); + await slotInput.fill(slot.toString()); + } + + // Save configuration + await page.click('button:has-text("Save")'); + await page.waitForSelector('text*=saved', { timeout: 5000 }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + message: `PLC configuration updated: IP=${ip}, Rack=${rack}, Slot=${slot}` + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async monitorStreaming(args) { + const { sessionId, action } = args; + + try { + const page = this.pages.get(sessionId); + if (!page) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + // Navigate to streaming tab + await page.click('text=Data Streaming'); + await page.waitForSelector('[data-testid="streaming-panel"]'); + + let result; + switch (action) { + case 'start': + await page.click('button:has-text("Start Streaming")'); + result = 'Streaming started'; + break; + case 'stop': + await page.click('button:has-text("Stop Streaming")'); + result = 'Streaming stopped'; + break; + case 'status': + // Get current streaming status + const statusText = await page.locator('[data-testid="streaming-status"]').textContent().catch(() => 'Unknown'); + result = `Streaming status: ${statusText}`; + break; + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + action, + result + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async capturePerformance(args) { + const { sessionId } = args; + + try { + const page = this.pages.get(sessionId); + if (!page) { + throw new Error(`No browser session found with ID: ${sessionId}`); + } + + const metrics = await page.evaluate(() => { + const navigation = performance.getEntriesByType('navigation')[0]; + const memory = performance.memory; + + return { + loadTime: navigation ? navigation.loadEventEnd - navigation.loadEventStart : 0, + domContentLoaded: navigation ? navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart : 0, + memory: memory ? { + usedJSHeapSize: memory.usedJSHeapSize, + totalJSHeapSize: memory.totalJSHeapSize, + jsHeapSizeLimit: memory.jsHeapSizeLimit + } : null, + timestamp: Date.now() + }; + }); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: true, + metrics + }, null, 2) + } + ] + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + success: false, + error: error.message + }, null, 2) + } + ] + }; + } + } + + async start() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('Playwright MCP Server running on stdio'); + } +} + +// Start the server +const server = new PlaywrightMCPServer(); +server.start().catch(console.error); diff --git a/testing/mcp-server/src/test-server.js b/testing/mcp-server/src/test-server.js new file mode 100644 index 0000000..063c82f --- /dev/null +++ b/testing/mcp-server/src/test-server.js @@ -0,0 +1,140 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { spawn } from 'child_process'; + +/** + * Test client for Playwright MCP Server + */ +async function testMCPServer() { + console.log('🚀 Starting Playwright MCP Server test...'); + + try { + // Spawn the server process + const serverProcess = spawn('node', ['src/server.js'], { + cwd: process.cwd(), + stdio: ['pipe', 'pipe', 'inherit'] + }); + + // Create client transport + const transport = new StdioClientTransport({ + stdin: serverProcess.stdin, + stdout: serverProcess.stdout + }); + + // Create client + const client = new Client( + { + name: 'playwright-test-client', + version: '1.0.0' + }, + { + capabilities: {} + } + ); + + // Connect to server + console.log('📡 Connecting to MCP server...'); + await client.connect(transport); + console.log('✅ Connected to MCP server'); + + // List available tools + console.log('🔧 Getting available tools...'); + const tools = await client.request('tools/list', {}); + console.log(`Found ${tools.tools.length} tools:`); + tools.tools.forEach(tool => { + console.log(` - ${tool.name}: ${tool.description}`); + }); + + // Test launching browser + console.log('🌐 Testing browser launch...'); + const launchResult = await client.request('tools/call', { + name: 'launch_browser', + arguments: { + browserType: 'chromium', + headless: false + } + }); + + const launchData = JSON.parse(launchResult.content[0].text); + console.log('Browser launch result:', launchData); + + if (launchData.success) { + const sessionId = launchData.sessionId; + console.log(`✅ Browser launched with session ID: ${sessionId}`); + + // Test navigation + console.log('🧭 Testing navigation...'); + const navResult = await client.request('tools/call', { + name: 'navigate_to', + arguments: { + sessionId, + url: 'http://localhost:5173/app' + } + }); + + const navData = JSON.parse(navResult.content[0].text); + console.log('Navigation result:', navData); + + // Test taking screenshot + console.log('📸 Testing screenshot...'); + const screenshotResult = await client.request('tools/call', { + name: 'take_screenshot', + arguments: { + sessionId, + path: 'test-screenshot.png' + } + }); + + const screenshotData = JSON.parse(screenshotResult.content[0].text); + console.log('Screenshot result:', screenshotData); + + // Test getting page info + console.log('ℹ️ Testing page info...'); + const infoResult = await client.request('tools/call', { + name: 'get_page_info', + arguments: { + sessionId + } + }); + + const infoData = JSON.parse(infoResult.content[0].text); + console.log('Page info result:', infoData); + + // Test PLC status (if app is running) + console.log('🔌 Testing PLC status...'); + const plcResult = await client.request('tools/call', { + name: 'get_plc_status', + arguments: { + sessionId + } + }); + + const plcData = JSON.parse(plcResult.content[0].text); + console.log('PLC status result:', plcData); + + // Close browser + console.log('🔒 Closing browser...'); + await client.request('tools/call', { + name: 'close_browser', + arguments: { + sessionId + } + }); + } + + console.log('✅ All tests completed successfully!'); + + // Close client connection + await client.close(); + serverProcess.kill(); + + } catch (error) { + console.error('❌ Test failed:', error); + process.exit(1); + } +} + +// Run test if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + testMCPServer(); +} diff --git a/testing/playwright-mcp-global.js b/testing/playwright-mcp-global.js new file mode 100644 index 0000000..7aa74be --- /dev/null +++ b/testing/playwright-mcp-global.js @@ -0,0 +1,190 @@ +#!/usr/bin/env node + +/** + * Global Playwright MCP-like Automation Script + * This script can be used across multiple projects + */ + +import { chromium, firefox, webkit } from 'playwright'; +import { program } from 'commander'; +import fs from 'fs/promises'; +import path from 'path'; + +program + .name('playwright-mcp') + .description('Playwright MCP-like automation tool for web applications') + .version('1.0.0'); + +program + .command('test') + .description('Run automated tests') + .option('-b, --browser ', 'Browser type (chromium, firefox, webkit)', 'chromium') + .option('-h, --headless', 'Run in headless mode', false) + .option('-u, --url ', 'Base URL to test', 'http://localhost:3000') + .action(async (options) => { + const { browser, headless, url } = options; + console.log(`Running tests with ${browser} browser on ${url}`); + + // Your test logic here + await runTests({ browser, headless, url }); + }); + +program + .command('capture') + .description('Capture screenshots of application') + .option('-b, --browser ', 'Browser type', 'chromium') + .option('-u, --url ', 'URL to capture', 'http://localhost:3000') + .option('-o, --output ', 'Output directory', './screenshots') + .action(async (options) => { + await captureScreenshots(options); + }); + +program + .command('monitor') + .description('Monitor application performance') + .option('-b, --browser ', 'Browser type', 'chromium') + .option('-u, --url ', 'URL to monitor', 'http://localhost:3000') + .option('-d, --duration ', 'Monitoring duration', '60') + .action(async (options) => { + await monitorPerformance(options); + }); + +async function runTests({ browser, headless, url }) { + const browserInstance = await launchBrowser(browser, { headless }); + const context = await browserInstance.newContext(); + const page = await context.newPage(); + + try { + await page.goto(url); + await page.waitForLoadState('networkidle'); + + // Basic connectivity test + const title = await page.title(); + console.log(`Page title: ${title}`); + + // Check for common elements + const links = await page.locator('a').count(); + const buttons = await page.locator('button').count(); + + console.log(`Found ${links} links and ${buttons} buttons`); + + // Performance metrics + const metrics = await page.evaluate(() => { + const navigation = performance.getEntriesByType('navigation')[0]; + return { + loadTime: navigation.loadEventEnd - navigation.loadEventStart, + domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart, + firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime || 0 + }; + }); + + console.log('Performance metrics:', metrics); + + } catch (error) { + console.error('Test failed:', error); + } finally { + await context.close(); + await browserInstance.close(); + } +} + +async function captureScreenshots({ browser, url, output }) { + const browserInstance = await launchBrowser(browser, { headless: true }); + const context = await browserInstance.newContext(); + const page = await context.newPage(); + + try { + await page.goto(url); + await page.waitForLoadState('networkidle'); + + // Create output directory + await fs.mkdir(output, { recursive: true }); + + // Full page screenshot + await page.screenshot({ + path: path.join(output, 'full-page.png'), + fullPage: true + }); + + // Viewport screenshot + await page.screenshot({ + path: path.join(output, 'viewport.png') + }); + + console.log(`Screenshots saved to ${output}`); + + } catch (error) { + console.error('Screenshot capture failed:', error); + } finally { + await context.close(); + await browserInstance.close(); + } +} + +async function monitorPerformance({ browser, url, duration }) { + const browserInstance = await launchBrowser(browser, { headless: true }); + const context = await browserInstance.newContext(); + const page = await context.newPage(); + + const metrics = []; + const startTime = Date.now(); + const durationMs = parseInt(duration) * 1000; + + try { + await page.goto(url); + + // Monitor performance every 5 seconds + const interval = setInterval(async () => { + const currentMetrics = await page.evaluate(() => { + return { + timestamp: Date.now(), + memory: performance.memory ? { + usedJSHeapSize: performance.memory.usedJSHeapSize, + totalJSHeapSize: performance.memory.totalJSHeapSize, + jsHeapSizeLimit: performance.memory.jsHeapSizeLimit + } : null, + timing: performance.timing + }; + }); + + metrics.push(currentMetrics); + console.log(`Memory usage: ${(currentMetrics.memory?.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`); + }, 5000); + + // Stop monitoring after duration + setTimeout(() => { + clearInterval(interval); + }, durationMs); + + // Wait for monitoring to complete + await new Promise(resolve => setTimeout(resolve, durationMs)); + + // Save metrics + await fs.writeFile('performance-monitor.json', JSON.stringify(metrics, null, 2)); + console.log('Performance monitoring completed. Data saved to performance-monitor.json'); + + } catch (error) { + console.error('Performance monitoring failed:', error); + } finally { + await context.close(); + await browserInstance.close(); + } +} + +async function launchBrowser(type, options = {}) { + const browsers = { + chromium: chromium, + firefox: firefox, + webkit: webkit + }; + + const defaultOptions = { + headless: false, + slowMo: 100 + }; + + return await browsers[type].launch({ ...defaultOptions, ...options }); +} + +// Parse command line arguments +program.parse(); diff --git a/testing/playwright.config.js b/testing/playwright.config.js new file mode 100644 index 0000000..efab8e8 --- /dev/null +++ b/testing/playwright.config.js @@ -0,0 +1,83 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:5173', + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + /* Take screenshot on failure */ + screenshot: 'only-on-failure', + /* Record video on failure */ + video: 'retain-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + { + name: 'Mobile Safari', + use: { ...devices['iPhone 12'] }, + }, + + /* Test against branded browsers. */ + { + name: 'Microsoft Edge', + use: { ...devices['Desktop Edge'], channel: 'msedge' }, + }, + { + name: 'Google Chrome', + use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'npm run dev', + cwd: '../frontend', + port: 5173, + reuseExistingServer: !process.env.CI, + }, + { + command: 'python main.py', + cwd: '..', + port: 5000, + reuseExistingServer: !process.env.CI, + } + ], +}); diff --git a/testing/quick-start.sh b/testing/quick-start.sh new file mode 100644 index 0000000..1a38609 --- /dev/null +++ b/testing/quick-start.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Script de inicio rápido para testing con Playwright + +echo "🚀 Iniciando PLC Streamer Testing Suite" + +# Verificar que los servidores estén corriendo +echo "🔍 Verificando servidores..." + +# Check frontend +if curl -f http://localhost:5173 >/dev/null 2>&1; then + echo "✅ Frontend (5173) running" +else + echo "❌ Frontend no está corriendo en puerto 5173" + echo "💡 Ejecuta: cd ../frontend && npm run dev" +fi + +# Check backend +if curl -f http://localhost:5000/api/status >/dev/null 2>&1; then + echo "✅ Backend (5000) running" +else + echo "❌ Backend no está corriendo en puerto 5000" + echo "💡 Ejecuta: cd .. && python main.py" +fi + +echo "🎬 Iniciando tests..." +npm run test:headed diff --git a/testing/setup.js b/testing/setup.js new file mode 100644 index 0000000..3a3f3f9 --- /dev/null +++ b/testing/setup.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node + +/** + * Script de configuración inicial para Playwright MCP + */ + +import { execSync } from 'child_process'; +import fs from 'fs/promises'; +import path from 'path'; + +console.log('🚀 Configurando Playwright MCP para PLC Streamer...'); + +async function setup() { + try { + // 1. Verificar instalación de Playwright + console.log('📦 Verificando instalación de Playwright...'); + try { + execSync('npx playwright --version', { stdio: 'pipe' }); + console.log('✅ Playwright instalado'); + } catch (error) { + console.log('⚠️ Instalando Playwright...'); + execSync('npm install', { stdio: 'inherit' }); + execSync('npx playwright install', { stdio: 'inherit' }); + } + + // 2. Crear directorios necesarios + console.log('📁 Creando directorios...'); + const dirs = ['screenshots', 'videos', 'test-results', 'playwright-report']; + + for (const dir of dirs) { + try { + await fs.mkdir(dir, { recursive: true }); + console.log(`✅ Directorio creado: ${dir}`); + } catch (error) { + console.log(`ℹ️ Directorio ya existe: ${dir}`); + } + } + + // 3. Verificar que los servidores estén configurados + console.log('🔧 Verificando configuración de servidores...'); + + // Check frontend + const frontendPath = '../frontend/package.json'; + try { + await fs.access(frontendPath); + console.log('✅ Frontend encontrado'); + } catch (error) { + console.log('⚠️ Frontend no encontrado en ruta esperada'); + } + + // Check backend + const backendPath = '../main.py'; + try { + await fs.access(backendPath); + console.log('✅ Backend encontrado'); + } catch (error) { + console.log('⚠️ Backend no encontrado en ruta esperada'); + } + + // 4. Crear archivo de configuración de entorno + console.log('⚙️ Creando configuración de entorno...'); + const envConfig = { + FRONTEND_URL: 'http://localhost:5173', + BACKEND_URL: 'http://localhost:5000', + PLAYWRIGHT_HEADLESS: 'false', + PLAYWRIGHT_SLOWMO: '100', + SCREENSHOT_PATH: './screenshots', + VIDEO_PATH: './videos' + }; + + await fs.writeFile('.env', + Object.entries(envConfig) + .map(([key, value]) => `${key}=${value}`) + .join('\n') + ); + console.log('✅ Archivo .env creado'); + + // 5. Crear script de inicio rápido + console.log('🎯 Creando script de inicio rápido...'); + const quickStart = `#!/bin/bash + +# Script de inicio rápido para testing con Playwright + +echo "🚀 Iniciando PLC Streamer Testing Suite" + +# Verificar que los servidores estén corriendo +echo "🔍 Verificando servidores..." + +# Check frontend +if curl -f http://localhost:5173 >/dev/null 2>&1; then + echo "✅ Frontend (5173) running" +else + echo "❌ Frontend no está corriendo en puerto 5173" + echo "💡 Ejecuta: cd ../frontend && npm run dev" +fi + +# Check backend +if curl -f http://localhost:5000/api/status >/dev/null 2>&1; then + echo "✅ Backend (5000) running" +else + echo "❌ Backend no está corriendo en puerto 5000" + echo "💡 Ejecuta: cd .. && python main.py" +fi + +echo "🎬 Iniciando tests..." +npm run test:headed +`; + + await fs.writeFile('quick-start.sh', quickStart); + console.log('✅ Script quick-start.sh creado'); + + // 6. Crear archivo gitignore para testing + console.log('📝 Configurando .gitignore...'); + const gitignore = `# Playwright +test-results/ +playwright-report/ +playwright/.cache/ + +# Screenshots y videos +screenshots/*.png +videos/*.webm + +# Reportes y logs +*.json +*.log + +# Node modules +node_modules/ + +# Environment +.env.local +`; + + await fs.writeFile('.gitignore', gitignore); + console.log('✅ .gitignore configurado'); + + // 7. Mensaje final + console.log(` +🎉 ¡Configuración completada! + +📋 Próximos pasos: + +1. Asegúrate de que los servidores estén corriendo: + Frontend: cd ../frontend && npm run dev + Backend: cd .. && python main.py + +2. Ejecutar tests básicos: + npm test + +3. Ejecutar ejemplo interactivo: + npm run example + +4. Generar código de test: + npm run codegen:app + +5. Monitoreo continuo: + npm run monitor + +📚 Ver README.md para más información y ejemplos. + +🔧 Archivos creados: + - .env (configuración) + - .gitignore (exclusiones) + - quick-start.sh (inicio rápido) + +Happy testing! 🧪 + `); + + } catch (error) { + console.error('❌ Error durante la configuración:', error); + process.exit(1); + } +} + +setup(); diff --git a/testing/tests/basic.spec.js b/testing/tests/basic.spec.js new file mode 100644 index 0000000..e52176b --- /dev/null +++ b/testing/tests/basic.spec.js @@ -0,0 +1,11 @@ +import { test, expect } from '@playwright/test'; + +test('basic connectivity test', async ({ page }) => { + // Test básico de conectividad + await page.goto('http://localhost:5173'); + + // Verificar que la página cargue con el título correcto + await expect(page).toHaveTitle(/PLC S7-31x Streamer/); + + console.log('✅ Conectividad básica funcionando'); +}); diff --git a/testing/tests/configuration.spec.js b/testing/tests/configuration.spec.js new file mode 100644 index 0000000..b29a6ba --- /dev/null +++ b/testing/tests/configuration.spec.js @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; + +test.describe('PLC Configuration Management', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/app'); + // Navigate to Configuration tab + await page.click('text=Configuration'); + }); + + test('should display PLC configuration form', async ({ page }) => { + // Check if PLC configuration section is visible + await expect(page.locator('text=PLC Configuration')).toBeVisible(); + + // Check for common configuration fields + await expect(page.locator('input[name*="ip"]')).toBeVisible(); + await expect(page.locator('input[name*="rack"]')).toBeVisible(); + await expect(page.locator('input[name*="slot"]')).toBeVisible(); + }); + + test('should be able to modify PLC settings', async ({ page }) => { + // Find IP address input field + const ipInput = page.locator('input[name*="ip"]').first(); + + // Clear and set new IP + await ipInput.clear(); + await ipInput.fill('192.168.1.100'); + + // Verify the value was set + await expect(ipInput).toHaveValue('192.168.1.100'); + }); + + test('should validate configuration data', async ({ page }) => { + // Try to enter invalid IP address + const ipInput = page.locator('input[name*="ip"]').first(); + await ipInput.clear(); + await ipInput.fill('invalid-ip'); + + // Look for validation error + await expect(page.locator('text*=invalid')).toBeVisible(); + }); + + test('should save configuration changes', async ({ page }) => { + // Modify a setting + const ipInput = page.locator('input[name*="ip"]').first(); + await ipInput.clear(); + await ipInput.fill('192.168.1.101'); + + // Click save button + await page.click('button:has-text("Save")'); + + // Look for success message + await expect(page.locator('text*=saved')).toBeVisible(); + }); +}); diff --git a/testing/tests/dashboard.spec.js b/testing/tests/dashboard.spec.js new file mode 100644 index 0000000..ae5395c --- /dev/null +++ b/testing/tests/dashboard.spec.js @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; + +test.describe('PLC Streamer Application', () => { + test.beforeEach(async ({ page }) => { + // Navigate to the application + await page.goto('/app'); + }); + + test('should load the main dashboard', async ({ page }) => { + // Wait for the main content to load + await expect(page.locator('[data-testid="dashboard"]')).toBeVisible(); + + // Check if the title is correct + await expect(page).toHaveTitle(/PLC Streamer/); + }); + + test('should display navigation tabs', async ({ page }) => { + // Check if main navigation tabs are present + await expect(page.locator('text=Dashboard')).toBeVisible(); + await expect(page.locator('text=Configuration')).toBeVisible(); + await expect(page.locator('text=Data Streaming')).toBeVisible(); + await expect(page.locator('text=Plots')).toBeVisible(); + }); + + test('should be able to navigate between tabs', async ({ page }) => { + // Click on Configuration tab + await page.click('text=Configuration'); + await expect(page.locator('[data-testid="configuration-panel"]')).toBeVisible(); + + // Click on Data Streaming tab + await page.click('text=Data Streaming'); + await expect(page.locator('[data-testid="streaming-panel"]')).toBeVisible(); + + // Click on Plots tab + await page.click('text=Plots'); + await expect(page.locator('[data-testid="plots-panel"]')).toBeVisible(); + }); + + test('should show PLC connection status', async ({ page }) => { + // Look for connection status indicator + const connectionStatus = page.locator('[data-testid="connection-status"]'); + await expect(connectionStatus).toBeVisible(); + + // Status should be either "Connected" or "Disconnected" + const statusText = await connectionStatus.textContent(); + expect(['Connected', 'Disconnected', 'Connecting']).toContain(statusText?.trim()); + }); +}); diff --git a/testing/tests/plotting.spec.js b/testing/tests/plotting.spec.js new file mode 100644 index 0000000..9226f44 --- /dev/null +++ b/testing/tests/plotting.spec.js @@ -0,0 +1,77 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Real-time Plotting', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/app'); + // Navigate to Plots tab + await page.click('text=Plots'); + }); + + test('should display plot management interface', async ({ page }) => { + // Check for plot controls + await expect(page.locator('text=Plot Management')).toBeVisible(); + await expect(page.locator('button:has-text("Create Plot")')).toBeVisible(); + + // Check for existing plots list + await expect(page.locator('[data-testid="plots-list"]')).toBeVisible(); + }); + + test('should be able to create a new plot', async ({ page }) => { + // Click create plot button + await page.click('button:has-text("Create Plot")'); + + // Fill in plot details + await page.fill('input[name="plotName"]', 'Test Plot'); + await page.fill('input[name="description"]', 'Test plot for automation'); + + // Select variables for the plot + await page.check('input[name="variables"][value="Temperature"]'); + await page.check('input[name="variables"][value="Pressure"]'); + + // Save the plot + await page.click('button:has-text("Create")'); + + // Verify plot was created + await expect(page.locator('text=Test Plot')).toBeVisible(); + }); + + test('should display chart canvas', async ({ page }) => { + // Check if chart canvas is present + await expect(page.locator('canvas')).toBeVisible(); + + // Verify chart is rendered (check for Chart.js specific elements) + const canvas = page.locator('canvas').first(); + await expect(canvas).toHaveAttribute('style', /.*width.*height.*/); + }); + + test('should update plot in real-time', async ({ page }) => { + // Start a plot session + await page.click('button:has-text("Start Plot")'); + + // Wait for some time to allow data to update + await page.waitForTimeout(5000); + + // Check if chart has data points (this is basic, real implementation would check canvas content) + const canvas = page.locator('canvas').first(); + + // Verify canvas is still present and potentially has been updated + await expect(canvas).toBeVisible(); + + // Stop the plot + await page.click('button:has-text("Stop Plot")'); + }); + + test('should export plot data', async ({ page }) => { + // Set up download handler + const downloadPromise = page.waitForEvent('download'); + + // Click export button + await page.click('button:has-text("Export")'); + + // Wait for download to start + const download = await downloadPromise; + + // Verify download file name + expect(download.suggestedFilename()).toContain('.csv'); + }); +}); diff --git a/testing/tests/streaming.spec.js b/testing/tests/streaming.spec.js new file mode 100644 index 0000000..3f193a1 --- /dev/null +++ b/testing/tests/streaming.spec.js @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Data Streaming Features', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/app'); + // Navigate to Data Streaming tab + await page.click('text=Data Streaming'); + }); + + test('should display streaming controls', async ({ page }) => { + // Check for streaming control buttons + await expect(page.locator('button:has-text("Start Streaming")')).toBeVisible(); + await expect(page.locator('button:has-text("Stop Streaming")')).toBeVisible(); + + // Check for dataset configuration + await expect(page.locator('text=Dataset Configuration')).toBeVisible(); + }); + + test('should be able to start and stop streaming', async ({ page }) => { + // Start streaming + await page.click('button:has-text("Start Streaming")'); + + // Wait for streaming to start (check for status change) + await expect(page.locator('text*=streaming')).toBeVisible(); + + // Stop streaming + await page.click('button:has-text("Stop Streaming")'); + + // Verify streaming stopped + await expect(page.locator('text*=stopped')).toBeVisible(); + }); + + test('should display dataset variables', async ({ page }) => { + // Check if dataset variables table is present + await expect(page.locator('[data-testid="dataset-variables"]')).toBeVisible(); + + // Check for variable columns + await expect(page.locator('text=Variable Name')).toBeVisible(); + await expect(page.locator('text=Address')).toBeVisible(); + await expect(page.locator('text=Type')).toBeVisible(); + }); + + test('should allow adding new variables', async ({ page }) => { + // Click add variable button + await page.click('button:has-text("Add Variable")'); + + // Fill in variable details + await page.fill('input[name="name"]', 'TestVariable'); + await page.fill('input[name="address"]', 'DB1.DBD0'); + await page.selectOption('select[name="type"]', 'REAL'); + + // Save the variable + await page.click('button:has-text("Save")'); + + // Verify variable was added + await expect(page.locator('text=TestVariable')).toBeVisible(); + }); +});