synapse-hub 2.2.4 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/.agents/agents/failure-analyst.md +11 -0
  2. package/.agents/agents/live-explorer.md +28 -0
  3. package/.agents/agents/orchestrator.md +20 -4
  4. package/.agents/agents/pom-crafter.md +12 -0
  5. package/.agents/agents/quality-gatekeeper.md +19 -4
  6. package/.agents/agents/test-runner.md +11 -0
  7. package/.agents/rules/communication-style.md +40 -0
  8. package/.agents/rules/context-engineering.md +23 -0
  9. package/.agents/rules/git-push-protocol.md +10 -16
  10. package/.agents/rules/graphify.md +14 -14
  11. package/.agents/rules/mandatory-playwright-live.md +29 -29
  12. package/.agents/rules/mobile_appium_rules.md +132 -0
  13. package/.agents/rules/performance_testing_rules.md +74 -0
  14. package/.agents/rules/systematic-debugging.md +6 -6
  15. package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +64 -215
  16. package/.agents/skills/agent-orchestration-multi-agent-optimize/examples/swarm_workload_config.json +12 -0
  17. package/.agents/skills/antigravity-test-sentinel/SKILL.md +3 -3
  18. package/.agents/skills/antigravity-test-sentinel/examples/dod_gate_policy.json +11 -0
  19. package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
  20. package/.agents/skills/api-network-mocking-interception/examples/network_route_stub.json +9 -0
  21. package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +9 -9
  22. package/.agents/skills/api-spec-to-e2e-matrix/examples/sample_openapi.json +19 -0
  23. package/.agents/skills/architecture-decision-records/SKILL.md +35 -412
  24. package/.agents/skills/architecture-decision-records/examples/0001-record-architecture-decisions.md +12 -0
  25. package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +1 -1
  26. package/.agents/skills/auth-session-and-cookie-manager/examples/storage_state_sample.json +11 -0
  27. package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +80 -186
  28. package/.agents/skills/backend-load-and-stress-patterns/examples/k6_load_test_sample.js +13 -0
  29. package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
  30. package/.agents/skills/bats-testing-patterns/examples/cli_runner_test.bats +6 -0
  31. package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -614
  32. package/.agents/skills/clean-pom-crafter/SKILL.md +21 -3
  33. package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -28
  34. package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -138
  35. package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +2 -2
  36. package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_page_sample.groovy +9 -0
  37. package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +5 -5
  38. package/.agents/skills/db-state-verifier-and-seeder/examples/db_fixture_seed.json +12 -0
  39. package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
  40. package/.agents/skills/e2e-testing-patterns/examples/playwright_sharded_config.ts +12 -0
  41. package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -522
  42. package/.agents/skills/failure-autopsy-and-healing/SKILL.md +17 -3
  43. package/.agents/skills/failure-autopsy-and-healing/examples/sample_junit_failure.xml +8 -0
  44. package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -64
  45. package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -56
  46. package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -98
  47. package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
  48. package/.agents/skills/file-and-report-validator/examples/sample_export_schema.json +14 -0
  49. package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +3 -3
  50. package/.agents/skills/flaky-test-sentinel-and-quarantine/examples/quarantine_registry.json +11 -0
  51. package/.agents/skills/gherkin-to-suite/SKILL.md +103 -0
  52. package/.agents/skills/gherkin-to-suite/examples/checkout.feature +39 -0
  53. package/.agents/skills/gherkin-to-suite/examples/suite_report_sample.json +334 -0
  54. package/.agents/skills/github-actions-templates/SKILL.md +47 -304
  55. package/.agents/skills/github-actions-templates/examples/matrix_test_pipeline.yml +8 -0
  56. package/.agents/skills/graphify-impact-analyzer/SKILL.md +6 -6
  57. package/.agents/skills/graphify-impact-analyzer/examples/dependency_blast_radius.json +8 -0
  58. package/.agents/skills/katalon-cli-runner/SKILL.md +2 -2
  59. package/.agents/skills/katalon-cli-runner/examples/headless_kre_exec.sh +3 -0
  60. package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -209
  61. package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -155
  62. package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -67
  63. package/.agents/skills/lam-script-generator/SKILL.md +42 -1
  64. package/.agents/skills/lam-script-generator/examples/user_story_sample.md +8 -0
  65. package/.agents/skills/lean-code-ladder/SKILL.md +83 -0
  66. package/.agents/skills/lean-code-ladder/examples/ladder_walkthrough.md +37 -0
  67. package/.agents/skills/meta-prompt-crafter/SKILL.md +107 -0
  68. package/.agents/skills/meta-prompt-crafter/examples/qa_prompt_template.md +35 -0
  69. package/.agents/skills/persistent-planning-files/SKILL.md +108 -0
  70. package/.agents/skills/persistent-planning-files/examples/task_plan_example.md +56 -0
  71. package/.agents/skills/playwright-live-interaction/SKILL.md +49 -1
  72. package/.agents/skills/playwright-live-interaction/examples/live_probe_script.js +5 -0
  73. package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -160
  74. package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -75
  75. package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -388
  76. package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -134
  77. package/.agents/skills/playwright-webapp-testing/SKILL.md +93 -0
  78. package/.agents/skills/playwright-webapp-testing/examples/server_lifecycle_playwright.py +63 -0
  79. package/.agents/skills/rbt-manual-testing/SKILL.md +150 -0
  80. package/.agents/skills/rbt-manual-testing/examples/checkout_rbt_manual_suite.md +25 -0
  81. package/.agents/skills/security-dast-sentinel/SKILL.md +54 -135
  82. package/.agents/skills/security-dast-sentinel/examples/dast_scan_config.json +10 -0
  83. package/.agents/skills/self-healing-locator/SKILL.md +24 -10
  84. package/.agents/skills/self-healing-locator/examples/aom_diff_sample.json +16 -0
  85. package/.agents/skills/skillspector-security-guard/SKILL.md +32 -90
  86. package/.agents/skills/skillspector-security-guard/examples/safe_mcp_config.json +12 -0
  87. package/.agents/skills/surgical-coding-principles/SKILL.md +89 -0
  88. package/.agents/skills/surgical-coding-principles/examples/surgical_vs_scope_creep.md +46 -0
  89. package/.agents/skills/synthetic-test-data-generator/SKILL.md +3 -3
  90. package/.agents/skills/synthetic-test-data-generator/examples/idempotent_data_recipe.json +9 -0
  91. package/.agents/skills/systematic-debugging/SKILL.md +100 -0
  92. package/.agents/skills/systematic-debugging/examples/four_phase_case.md +44 -0
  93. package/.agents/skills/test-driven-development/SKILL.md +114 -0
  94. package/.agents/skills/test-driven-development/examples/red_green_refactor_qa.md +78 -0
  95. package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
  96. package/.agents/skills/token-and-context-optimizer/examples/distilled_log_sample.txt +3 -0
  97. package/.agents/skills/verification-before-completion/SKILL.md +100 -0
  98. package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +46 -0
  99. package/.agents/skills/visual-regression-sentinel/SKILL.md +21 -1
  100. package/.agents/skills/visual-regression-sentinel/examples/visual_mask_rules.json +9 -0
  101. package/.agents/skills/wcag-audit-patterns/SKILL.md +3 -3
  102. package/.agents/skills/wcag-audit-patterns/examples/wcag_compliance_checklist.json +10 -0
  103. package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -541
  104. package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
  105. package/.agents/skills/web-vitals-and-performance-profiler/examples/cwv_budget_policy.json +6 -0
  106. package/.agents/templates/completion_report.md +57 -57
  107. package/.agents/workflows/analyze_requirement_document.md +18 -0
  108. package/.agents/workflows/export_testcases_excel.md +32 -0
  109. package/.agents/workflows/generate_combinatorial_test_data.md +28 -0
  110. package/.agents/workflows/generate_cross_module_test_plan.md +37 -0
  111. package/.agents/workflows/generate_manual_testcases_rbt.md +56 -0
  112. package/.agents/workflows/generate_requirements_from_website.md +25 -0
  113. package/.agents/workflows/generate_testcases_from_requirements.md +21 -0
  114. package/.agents/workflows/graphify.md +10 -10
  115. package/.agents/workflows/verify.md +2 -2
  116. package/.cursor/rules/language.mdc +7 -7
  117. package/.cursor/rules/synapse-qa.mdc +23 -0
  118. package/.cursorrules +7 -0
  119. package/.pre-commit-hooks.yaml +14 -0
  120. package/.windsurfrules +7 -0
  121. package/AGENTS.md +54 -15
  122. package/CLAUDE.md +35 -12
  123. package/LICENSE +30 -0
  124. package/NOTICE +180 -0
  125. package/README.en.md +325 -0
  126. package/README.md +247 -516
  127. package/action.yml +51 -0
  128. package/benchmarks/qa_eval/README.md +71 -0
  129. package/benchmarks/qa_eval/build_dataset.py +677 -0
  130. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +22 -0
  131. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +22 -0
  132. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +22 -0
  133. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +22 -0
  134. package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +22 -0
  135. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +21 -0
  136. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +21 -0
  137. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +21 -0
  138. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +21 -0
  139. package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +21 -0
  140. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +23 -0
  141. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +23 -0
  142. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +23 -0
  143. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +23 -0
  144. package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +23 -0
  145. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +22 -0
  146. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +22 -0
  147. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +22 -0
  148. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +22 -0
  149. package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +23 -0
  150. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +22 -0
  151. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +22 -0
  152. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +22 -0
  153. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +22 -0
  154. package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +22 -0
  155. package/benchmarks/qa_eval/eval_runner.py +418 -0
  156. package/benchmarks/qa_eval/reports/AB_METHODOLOGY_REPORT.md +83 -0
  157. package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +69 -0
  158. package/benchmarks/qa_eval/reports/eval_results.json +105 -0
  159. package/bin/agent-qa.js +10 -3
  160. package/docs/ARCHITECTURE.md +107 -0
  161. package/docs/CI_CD_INTEGRATION.md +94 -0
  162. package/docs/CLI_REFERENCE.md +157 -0
  163. package/docs/CONTRIBUTING.md +91 -0
  164. package/docs/PLUGIN_DEVELOPMENT_GUIDE.md +126 -0
  165. package/docs/TOKEN_QUOTA_PLAYBOOK.md +41 -0
  166. package/docs/adopted-ecosystem-tools.md +87 -0
  167. package/docs/schemas/synapse.config.schema.json +64 -0
  168. package/examples/cypress-ecommerce-suite/README.md +45 -0
  169. package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +17 -0
  170. package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +29 -0
  171. package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +51 -0
  172. package/examples/cypress-ecommerce-suite/cypress.config.js +13 -0
  173. package/examples/cypress-ecommerce-suite/package.json +13 -0
  174. package/examples/katalon-suite/Keywords/pages/BasePage.groovy +19 -0
  175. package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +29 -0
  176. package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +45 -0
  177. package/examples/katalon-suite/Profiles/default.glbl +12 -0
  178. package/examples/katalon-suite/README.md +18 -0
  179. package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +12 -0
  180. package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +15 -0
  181. package/examples/katalon-suite/katalon_suite.prj +21 -0
  182. package/examples/playwright-crm-suite/README.md +40 -0
  183. package/examples/playwright-crm-suite/package.json +14 -0
  184. package/examples/playwright-crm-suite/pages/BasePage.ts +37 -0
  185. package/examples/playwright-crm-suite/pages/CustomerPage.ts +57 -0
  186. package/examples/playwright-crm-suite/pages/LoginPage.ts +32 -0
  187. package/examples/playwright-crm-suite/playwright.config.ts +27 -0
  188. package/examples/playwright-crm-suite/tests/auth.spec.ts +22 -0
  189. package/examples/playwright-crm-suite/tests/customers.spec.ts +33 -0
  190. package/examples/plugins/sample-custom-plugin/README.md +14 -0
  191. package/examples/plugins/sample-custom-plugin/plugin.json +16 -0
  192. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +29 -0
  193. package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +8 -0
  194. package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +125 -0
  195. package/examples/practices/README.md +22 -0
  196. package/examples/practices/requirements/crm/requirements_contracts.md +292 -0
  197. package/examples/practices/requirements/crm/requirements_customers.md +462 -0
  198. package/examples/practices/requirements/crm/requirements_login.md +202 -0
  199. package/examples/practices/requirements/crm/requirements_projects.md +396 -0
  200. package/examples/practices/requirements/crm/requirements_tasks.md +343 -0
  201. package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +402 -0
  202. package/examples/practices/requirements/jira/KAN-5/KAN-1/2026-04-03_04-28-27.png +0 -0
  203. package/examples/practices/requirements/jira/KAN-5/KAN-1/KAN-1_requirement.md +52 -0
  204. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-19.png +0 -0
  205. package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-57.png +0 -0
  206. package/examples/practices/requirements/jira/KAN-5/KAN-4/KAN-4_requirement.md +119 -0
  207. package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +9 -0
  208. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-08.png +0 -0
  209. package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-23.png +0 -0
  210. package/examples/practices/requirements/jira/KAN-5/KAN-6/KAN-6_requirement.md +32 -0
  211. package/examples/practices/testcases/api/book_management_api_test_cases.md +144 -0
  212. package/examples/practices/testcases/api/user_management_api_test_cases.md +139 -0
  213. package/examples/practices/testcases/crm/test_cases_crm_login.md +148 -0
  214. package/examples/practices/testcases/crm/test_cases_customers.md +156 -0
  215. package/examples/pytest-api-suite/README.md +45 -0
  216. package/examples/pytest-api-suite/conftest.py +31 -0
  217. package/examples/pytest-api-suite/pages/user_api_client.py +36 -0
  218. package/examples/pytest-api-suite/requirements.txt +2 -0
  219. package/examples/pytest-api-suite/tests/test_user_api.py +44 -0
  220. package/examples/selenium-suite/README.md +18 -0
  221. package/examples/selenium-suite/pages/base_page.py +27 -0
  222. package/examples/selenium-suite/pages/login_page.py +29 -0
  223. package/examples/selenium-suite/pages/order_page.py +25 -0
  224. package/examples/selenium-suite/pytest.ini +6 -0
  225. package/examples/selenium-suite/requirements.txt +2 -0
  226. package/examples/selenium-suite/tests/test_login.py +10 -0
  227. package/examples/selenium-suite/tests/test_order.py +10 -0
  228. package/package.json +22 -7
  229. package/scripts/generate_skill_examples.py +244 -0
  230. package/scripts/install.ps1 +112 -0
  231. package/scripts/install_global_plugin.ps1 +94 -0
  232. package/scripts/install_global_plugin.sh +49 -0
  233. package/scripts/lint_skills.ps1 +57 -0
  234. package/scripts/package_dist.py +323 -0
  235. package/scripts/setup.sh +59 -0
  236. package/scripts/update_hub.ps1 +19 -0
  237. package/scripts/update_hub.sh +15 -0
  238. package/skills_manifest.json +209 -6
  239. package/synapse.config.json +30 -0
  240. package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +7 -7
  241. package/templates/PROJECT_STATE_TEMPLATE.md +1 -1
  242. package/templates/ai_bridges/CLAUDE.md +33 -16
  243. package/templates/ai_bridges/copilot-instructions.md +14 -21
  244. package/templates/ai_bridges/cursor_synapse_qa.mdc +17 -20
  245. package/templates/ai_bridges/windsurfrules +21 -21
  246. package/templates/ci/github-actions-synapse.yml +71 -4
  247. package/templates/welcome.html.tmpl +1674 -0
  248. package/tools/.infer_baseline.json +1102 -0
  249. package/tools/TOOLS_RUNBOOK.md +92 -130
  250. package/tools/__init__.py +26 -0
  251. package/tools/agent_canary.py +501 -501
  252. package/tools/analyze_project.py +31 -17
  253. package/tools/api_fuzzer.py +278 -6
  254. package/tools/author_integrity.py +6 -6
  255. package/tools/auto_heal.py +1 -1
  256. package/tools/browser_helpers.js +105 -7
  257. package/tools/bug_reporter.py +31 -1
  258. package/tools/bump_version.py +23 -3
  259. package/tools/cache_resolver.py +248 -14
  260. package/tools/check_env.py +55 -14
  261. package/tools/check_version_sync.py +153 -0
  262. package/tools/ci_healing_hook.py +1 -1
  263. package/tools/cli.py +263 -13
  264. package/tools/count_tokens.py +1257 -0
  265. package/tools/dashboard_generator.py +2638 -0
  266. package/tools/db_sandbox.py +198 -20
  267. package/tools/e2e_flow_prober.ps1 +12 -64
  268. package/tools/e2e_flow_prober.py +99 -0
  269. package/tools/e2e_flow_prober.sh +4 -0
  270. package/tools/flaky_detector.py +4 -3
  271. package/tools/generate_allure_report.ps1 +12 -29
  272. package/tools/generate_allure_report.py +77 -0
  273. package/tools/generate_allure_report.sh +4 -0
  274. package/tools/generate_jmx_plan.py +257 -58
  275. package/tools/generate_test_reports.ps1 +12 -134
  276. package/tools/generate_test_reports.py +133 -0
  277. package/tools/generate_test_reports.sh +4 -0
  278. package/tools/gherkin_suite.py +758 -0
  279. package/tools/git_pre_commit_guard.py +12 -6
  280. package/tools/goldfish_review.py +366 -0
  281. package/tools/heal_pr_automator.py +1 -1
  282. package/tools/heuristic_locator_matcher.py +170 -21
  283. package/tools/infer_linter.py +612 -70
  284. package/tools/lint_ai_docs.py +64 -0
  285. package/tools/matrix_sharder.py +1 -1
  286. package/tools/mcp_excel_server.ps1 +12 -250
  287. package/tools/mcp_excel_server.py +249 -0
  288. package/tools/mcp_excel_server.sh +4 -0
  289. package/tools/mcp_server.py +368 -144
  290. package/tools/md_to_xlsx.py +307 -0
  291. package/tools/micro_mutator.py +300 -69
  292. package/tools/omni_transpiler.py +2 -2
  293. package/tools/output_formatter.py +128 -0
  294. package/tools/pinpoint_failure.py +88 -88
  295. package/tools/project_state.py +2 -2
  296. package/tools/repo_map.py +1 -1
  297. package/tools/safety_firewall.py +196 -45
  298. package/tools/setup_project.py +294 -30
  299. package/tools/spec_drift_sentinel.py +439 -0
  300. package/tools/sync_agent_rules.py +248 -0
  301. package/tools/telemetry_correlator.py +45 -0
  302. package/tools/test_impact.py +51 -12
  303. package/tools/tms_sync_bridge.py +21 -5
  304. package/tools/universal_ir.py +3 -0
  305. package/tools/update_sentinel.py +87 -15
  306. package/tools/verify_all.ps1 +13 -0
  307. package/tools/verify_all.py +463 -79
  308. package/tools/visual_diff_canvas.py +1 -0
  309. package/tools/visual_som_helper.py +1 -1
  310. package/tools/watch_mode.py +305 -0
  311. package/tools/welcome_page.py +291 -0
@@ -1,388 +1,388 @@
1
- # Enterprise Playwright Code Recipes (20 Recipes)
2
-
3
- All code recipes below execute directly via `browser_run_code_unsafe` with parameter `code: "async (page) => { ... }"`.
4
-
5
- ---
6
-
7
- ## Recipe 1: Authentication & Session Verification
8
- ```javascript
9
- async (page) => {
10
- if (page.url().includes('/auth') || page.url().includes('/login')) {
11
- const user = process.env.APP_USERNAME || 'your_username';
12
- const pass = process.env.APP_PASSWORD || 'your_password';
13
- await page.locator('input[formcontrolname="username"]').fill(user);
14
- await page.locator('input[formcontrolname="password"]').fill(pass);
15
- await page.locator('button[type="submit"]').click();
16
- await page.waitForNavigation({ waitUntil: 'networkidle' });
17
- }
18
- return { currentUrl: page.url(), title: await page.title() };
19
- }
20
- ```
21
-
22
- ## Recipe 2: Search, Filter & PrimeNG Table Verification (`p-table`)
23
- ```javascript
24
- async (page) => {
25
- const searchInput = page.locator('input.p-inputtext[placeholder*="Tìm kiếm"], input[placeholder*="tên"], input[placeholder*="mã"]').first();
26
- await searchInput.fill('Hà Nội');
27
- await searchInput.press('Enter');
28
- await page.waitForTimeout(1000);
29
-
30
- const rowCount = await page.locator('p-table table tbody tr').count();
31
- const firstRowText = rowCount > 0 ? await page.locator('p-table table tbody tr').first().innerText() : '';
32
- return { rowCount, firstRowText };
33
- }
34
- ```
35
-
36
- ## Recipe 3: PrimeNG Dropdown & MultiSelect (`p-dropdown`, `p-multiselect`)
37
- ```javascript
38
- async (page) => {
39
- const dropdown = page.locator('p-dropdown[formcontrolname="status"], p-dropdown[name="status"]').first();
40
- await dropdown.click();
41
- await page.waitForTimeout(400);
42
-
43
- const option = page.locator('.p-dropdown-panel .p-dropdown-item').filter({ hasText: 'Đang hoạt động' }).first();
44
- await option.click();
45
- await page.waitForTimeout(300);
46
-
47
- return { selectedLabel: await dropdown.locator('.p-dropdown-label').innerText() };
48
- }
49
- ```
50
-
51
- ## Recipe 4: Modal Dialogs & Popup Forms (`p-dialog`, `.card.popup-data`)
52
- ```javascript
53
- async (page) => {
54
- await page.evaluate(() => {
55
- function setAngularInput(selector, val) {
56
- const el = document.querySelector(selector);
57
- if (el) {
58
- el.value = val;
59
- el.dispatchEvent(new Event('input', { bubbles: true }));
60
- el.dispatchEvent(new Event('change', { bubbles: true }));
61
- }
62
- }
63
- const testCode = 'CCN_' + Math.floor(Date.now() / 1000);
64
- setAngularInput('input[formcontrolname="code"]', testCode);
65
- setAngularInput('input[formcontrolname="name"]', 'Cụm Công Nghiệp ' + testCode);
66
- setAngularInput('textarea[formcontrolname="address"]', 'Địa chỉ kiểm thử tự động');
67
- });
68
-
69
- const saveBtn = page.locator('button:has-text("Lưu"), button:has-text("Xác nhận"), button[title="Xác nhận"]').first();
70
- const isEnabled = !(await saveBtn.isDisabled());
71
- if (isEnabled) {
72
- await saveBtn.click();
73
- await page.waitForTimeout(1500);
74
- }
75
- return { isSaveButtonEnabled: isEnabled };
76
- }
77
- ```
78
-
79
- ## Recipe 5: Confirm Dialog & Deletion (`p-confirmdialog`)
80
- ```javascript
81
- async (page) => {
82
- await page.locator('p-table tbody tr button.p-button-danger, p-table tbody tr button[icon*="trash"]').first().click();
83
- await page.waitForTimeout(500);
84
-
85
- const confirmBtn = page.locator('.p-confirm-dialog-accept, button:has-text("Đồng ý"), button:has-text("Xóa")').first();
86
- await confirmBtn.click();
87
- await page.waitForTimeout(1000);
88
-
89
- const toastText = await page.locator('.p-toast-message-content').innerText().catch(() => 'No toast');
90
- return { toastText };
91
- }
92
- ```
93
-
94
- ## Recipe 6: File Upload & Excel Import (`input[type="file"]`)
95
- ```javascript
96
- async (page, customPath = 'Data Files/sample_import.xlsx') => {
97
- const fs = require('fs');
98
- const filePath = fs.existsSync(customPath) ? customPath : 'Data Files/sample_import.xlsx';
99
- await page.locator('input[type="file"]').setInputFiles(filePath);
100
- await page.waitForTimeout(1000);
101
-
102
- await page.locator('button:has-text("Tải lên"), button:has-text("Xác nhận")').first().click();
103
- await page.waitForTimeout(2000);
104
- return { status: "Uploaded", fileUsed: filePath };
105
- }
106
- ```
107
-
108
- ## Recipe 7: OpenLayers GIS Canvas Interaction (`div.ol-viewport`)
109
- ```javascript
110
- async (page) => {
111
- const canvas = page.locator('div.ol-viewport canvas').first();
112
- const box = await canvas.boundingBox();
113
- const cx = box.x + box.width / 2;
114
- const cy = box.y + box.height / 2;
115
-
116
- // Click polygon at non-overlapping offset (-180, -80)
117
- await page.mouse.click(cx - 180, cy - 80);
118
- await page.waitForTimeout(400);
119
- await page.mouse.click(cx - 150, cy - 80);
120
- await page.waitForTimeout(400);
121
- await page.mouse.click(cx - 150, cy - 50);
122
- await page.waitForTimeout(400);
123
- await page.mouse.dblclick(cx - 180, cy - 80);
124
- await page.waitForTimeout(1000);
125
-
126
- const result = await page.evaluate(() => {
127
- const error = document.querySelector('.text-danger, .invalid-feedback, editor-widget');
128
- return {
129
- hasError: !!error,
130
- errorText: error ? error.innerText : ''
131
- };
132
- });
133
- return result;
134
- }
135
- ```
136
-
137
- ## Recipe 8: Advanced OpenLayers Map Inspection & VN2000 Coordinates
138
- ```javascript
139
- async (page) => {
140
- return await page.evaluate(() => {
141
- const viewport = document.querySelector('div.ol-viewport');
142
- if (!viewport) return { error: "div.ol-viewport not found" };
143
-
144
- const map = viewport['__ol_map__'] || (window.map instanceof Object ? window.map : null);
145
- let mapInfo = {};
146
- if (map && map.getView) {
147
- const view = map.getView();
148
- mapInfo = {
149
- zoom: view.getZoom(),
150
- center: view.getCenter(),
151
- projection: view.getProjection() ? view.getProjection().getCode() : 'Unknown'
152
- };
153
- }
154
- const toolbarButtons = Array.from(document.querySelectorAll('.ol-control button, .map-toolbar button, .gis-tools button'))
155
- .map(b => ({ title: b.getAttribute('title') || b.innerText.trim(), className: b.className }));
156
- return { mapInfo, toolbarButtons };
157
- });
158
- }
159
- ```
160
-
161
- ## Recipe 9: Two-Phase Search Workflow
162
- ```javascript
163
- async (page) => {
164
- const searchToggleBtn = page.locator('button.p-button-icon-only[icon*="search"], button[title*="Tìm kiếm"], button:has-text("Tìm kiếm")').first();
165
- const searchInput = page.locator('input.p-inputtext[placeholder*="Tìm kiếm"], input[placeholder*="tên"], input[placeholder*="mã"]').first();
166
-
167
- if (!(await searchInput.isVisible())) {
168
- await searchToggleBtn.click();
169
- await page.waitForTimeout(300);
170
- }
171
-
172
- const keyword = 'CCN_TEST_2026';
173
- await searchInput.fill(keyword);
174
- await searchInput.press('Enter');
175
- await page.waitForTimeout(1000);
176
-
177
- const rowCount = await page.locator('p-table table tbody tr').count();
178
- const matched = rowCount > 0 && (await page.locator('p-table table tbody tr').first().innerText()).includes(keyword);
179
- return { rowCount, matched };
180
- }
181
- ```
182
-
183
- ## Recipe 10: Intercepting PrimeNG Toasts (`p-toast`)
184
- ```javascript
185
- async (page) => {
186
- const toastSelector = '.p-toast-message, .p-toast-message-content';
187
- try {
188
- const toast = await page.waitForSelector(toastSelector, { timeout: 4000 });
189
- const text = await toast.innerText();
190
- const isSuccess = (await toast.getAttribute('class') || '').includes('p-toast-message-success');
191
- return { success: true, text, isSuccess };
192
- } catch (e) {
193
- return { success: false, message: 'Toast did not appear or expired too quickly' };
194
- }
195
- }
196
- ```
197
-
198
- ## Recipe 11: Record Deletion & Confirmation Dialog (`p-confirmdialog`)
199
- ```javascript
200
- async (page, targetKeyword) => {
201
- const searchInput = page.locator('input.p-inputtext[placeholder*="Tìm kiếm"], input[placeholder*="tên"], input[placeholder*="mã"]').first();
202
- await searchInput.fill(targetKeyword);
203
- await searchInput.press('Enter');
204
- await page.waitForTimeout(1000);
205
-
206
- const deleteBtn = page.locator('p-table tbody tr:first-child button.p-button-danger, p-table tbody tr:first-child button[icon*="trash"]').first();
207
- await deleteBtn.click();
208
- await page.waitForTimeout(500);
209
-
210
- const confirmBtn = page.locator('.p-confirm-dialog-accept, .p-dialog-footer button:has-text("Xóa"), .p-dialog-footer button:has-text("Đồng ý"), button.p-button-danger:has-text("Xóa")').first();
211
- await confirmBtn.waitFor({ state: 'visible', timeout: 3000 });
212
- await confirmBtn.click();
213
- await page.waitForTimeout(1500);
214
-
215
- await searchInput.fill(targetKeyword);
216
- await searchInput.press('Enter');
217
- await page.waitForTimeout(1000);
218
- const remainingRows = await page.locator('p-table table tbody tr').count();
219
- const firstRowText = remainingRows > 0 ? await page.locator('p-table table tbody tr').first().innerText() : '';
220
- const isDeleted = remainingRows === 0 || !firstRowText.includes(targetKeyword) || firstRowText.includes('Không có dữ liệu');
221
-
222
- return { isDeleted, remainingRows, firstRowText };
223
- }
224
- ```
225
-
226
- ## Recipe 12: PrimeNG Calendar Date Input (`p-calendar`)
227
- ```javascript
228
- async (page) => {
229
- const dateValue = '25/10/2026';
230
- await page.evaluate(({ selector, val }) => {
231
- const calInput = document.querySelector(selector);
232
- if (calInput) {
233
- calInput.value = val;
234
- calInput.dispatchEvent(new Event('input', { bubbles: true }));
235
- calInput.dispatchEvent(new Event('change', { bubbles: true }));
236
- calInput.dispatchEvent(new Event('blur', { bubbles: true }));
237
- }
238
- }, { selector: 'p-calendar input, input[formcontrolname*="date"], input[formcontrolname*="Date"]', val: dateValue });
239
-
240
- await page.waitForTimeout(300);
241
- return { status: 'Date filled', value: dateValue };
242
- }
243
- ```
244
-
245
- ## Recipe 13: PrimeNG MultiSelect Dropdown (`p-multiselect`)
246
- ```javascript
247
- async (page, optionsToSelect = ['Công nghiệp chế biến', 'Điện lực']) => {
248
- const multiSelect = page.locator('p-multiselect').first();
249
- await multiSelect.click();
250
- await page.waitForSelector('.p-multiselect-panel', { state: 'visible', timeout: 3000 });
251
- await page.waitForTimeout(300);
252
-
253
- for (const optText of optionsToSelect) {
254
- const item = page.locator('.p-multiselect-item').filter({ hasText: optText }).first();
255
- if (await item.isVisible()) {
256
- await item.click();
257
- await page.waitForTimeout(200);
258
- }
259
- }
260
-
261
- const closeBtn = page.locator('.p-multiselect-close').first();
262
- if (await closeBtn.isVisible()) {
263
- await closeBtn.click();
264
- } else {
265
- await page.keyboard.press('Escape');
266
- }
267
- await page.waitForTimeout(300);
268
- return { selected: optionsToSelect };
269
- }
270
- ```
271
-
272
- ## Recipe 14: Handling Stale Elements & Angular DOM Re-render Retries
273
- ```javascript
274
- async (page, actionFn) => {
275
- const maxRetries = 3;
276
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
277
- try {
278
- return await actionFn(page);
279
- } catch (err) {
280
- if (err.message.includes('Element is not attached to the DOM') || err.message.includes('stale') || attempt === maxRetries) {
281
- if (attempt === maxRetries) throw err;
282
- await page.waitForTimeout(500 * attempt);
283
- } else {
284
- throw err;
285
- }
286
- }
287
- }
288
- }
289
- ```
290
-
291
-
292
- ## Recipe 15: Instant Angular Form Audit (Diagnose Disabled Save Button)
293
- ```javascript
294
- async (page) => {
295
- return await page.evaluate(() => {
296
- const invalidInputs = Array.from(document.querySelectorAll('form .ng-invalid, input.ng-invalid, p-dropdown.ng-invalid, textarea.ng-invalid'));
297
- return invalidInputs.map(el => ({
298
- tag: el.tagName.toLowerCase(),
299
- control: el.getAttribute('formcontrolname') || el.getAttribute('name') || el.id || 'unnamed',
300
- placeholder: el.getAttribute('placeholder') || '',
301
- currentValue: el.value || el.innerText.trim()
302
- }));
303
- });
304
- }
305
- ```
306
-
307
- ## Recipe 16: Smart Action Batching (Atomic Execution with Pause-on-Failure)
308
- ```javascript
309
- async (page) => {
310
- return await page.evaluate(() => {
311
- const steps = [
312
- { name: 'fill_code', fn: () => { const el = document.querySelector("input[formcontrolname='code']"); el.value = 'CCN_TEST'; el.dispatchEvent(new Event('input', {bubbles: true})); } },
313
- { name: 'fill_name', fn: () => { const el = document.querySelector("input[formcontrolname='name']"); el.value = 'Cụm CN Test'; el.dispatchEvent(new Event('input', {bubbles: true})); } },
314
- { name: 'submit', fn: () => { document.querySelector("button[type='submit']").click(); } }
315
- ];
316
-
317
- for (let i = 0; i < steps.length; i++) {
318
- try {
319
- steps[i].fn();
320
- } catch (err) {
321
- return { status: 'FAILED', failedStep: i + 1, name: steps[i].name, error: err.message, url: location.href };
322
- }
323
- }
324
- return { status: 'SUCCESS', totalSteps: steps.length };
325
- });
326
- }
327
- ```
328
-
329
- ## Recipe 17: Runtime Helper Injection (`window.__qa`)
330
- ```javascript
331
- async (page) => {
332
- // Injects the centralized browser helper tools/browser_helpers.js
333
- const fs = require('fs');
334
- const helperCode = fs.readFileSync('tools/browser_helpers.js', 'utf8');
335
- await page.evaluate(helperCode);
336
- return await page.evaluate(() => ({
337
- version: window.__qa.__version,
338
- auditResults: window.__qa.audit()
339
- }));
340
- }
341
- ```
342
-
343
- ## Recipe 18: Self-Healing Locator Fallback (`window.__qa.heal`)
344
- ```javascript
345
- async (page, brokenXPath, expectedText) => {
346
- return await page.evaluate(({ xpath, text }) => {
347
- if (window.__qa) {
348
- return window.__qa.heal(xpath, text);
349
- }
350
- // Fallback search
351
- const buttons = Array.from(document.querySelectorAll('button')).filter(b => b.innerText.includes(text));
352
- return {
353
- candidates: buttons.map(b => `//button[contains(normalize-space(.), '${b.innerText.trim()}')]`)
354
- };
355
- }, { xpath: brokenXPath, text: expectedText });
356
- }
357
- ```
358
-
359
- ## Recipe 19: Overlay-Proof Safe Click (`window.__qa.clickSafe`)
360
- ```javascript
361
- async (page, selector) => {
362
- return await page.evaluate((sel) => {
363
- if (window.__qa) {
364
- return window.__qa.clickSafe(sel);
365
- }
366
- const el = document.querySelector(sel);
367
- if (!el) return false;
368
- document.querySelectorAll('.p-component-overlay').forEach(o => o.style.pointerEvents = 'none');
369
- el.click();
370
- return true;
371
- }, selector);
372
- }
373
- ```
374
-
375
- ## Recipe 20: OpenLayers GIS Canvas Coordinate Inspector (`window.__qa.gisHUD`)
376
- ```javascript
377
- async (page) => {
378
- return await page.evaluate(() => {
379
- if (window.__qa) {
380
- return window.__qa.gisHUD();
381
- }
382
- const vp = document.querySelector('.ol-viewport');
383
- if (!vp) return { status: 'NO_MAP' };
384
- const r = vp.getBoundingClientRect();
385
- return { width: r.width, height: r.height, safeCenter: { x: r.width / 2, y: r.height / 2 } };
386
- });
387
- }
388
- ```
1
+ # Enterprise Playwright Code Recipes (20 Recipes)
2
+
3
+ All code recipes below execute directly via `browser_run_code_unsafe` with parameter `code: "async (page) => { ... }"`.
4
+
5
+ ---
6
+
7
+ ## Recipe 1: Authentication & Session Verification
8
+ ```javascript
9
+ async (page) => {
10
+ if (page.url().includes('/auth') || page.url().includes('/login')) {
11
+ const user = process.env.APP_USERNAME || 'your_username';
12
+ const pass = process.env.APP_PASSWORD || 'your_password';
13
+ await page.locator('input[formcontrolname="username"]').fill(user);
14
+ await page.locator('input[formcontrolname="password"]').fill(pass);
15
+ await page.locator('button[type="submit"]').click();
16
+ await page.waitForNavigation({ waitUntil: 'networkidle' });
17
+ }
18
+ return { currentUrl: page.url(), title: await page.title() };
19
+ }
20
+ ```
21
+
22
+ ## Recipe 2: Search, Filter & PrimeNG Table Verification (`p-table`)
23
+ ```javascript
24
+ async (page) => {
25
+ const searchInput = page.locator('input.p-inputtext[placeholder*="Tìm kiếm"], input[placeholder*="tên"], input[placeholder*="mã"]').first();
26
+ await searchInput.fill('Hà Nội');
27
+ await searchInput.press('Enter');
28
+ await page.waitForTimeout(1000);
29
+
30
+ const rowCount = await page.locator('p-table table tbody tr').count();
31
+ const firstRowText = rowCount > 0 ? await page.locator('p-table table tbody tr').first().innerText() : '';
32
+ return { rowCount, firstRowText };
33
+ }
34
+ ```
35
+
36
+ ## Recipe 3: PrimeNG Dropdown & MultiSelect (`p-dropdown`, `p-multiselect`)
37
+ ```javascript
38
+ async (page) => {
39
+ const dropdown = page.locator('p-dropdown[formcontrolname="status"], p-dropdown[name="status"]').first();
40
+ await dropdown.click();
41
+ await page.waitForTimeout(400);
42
+
43
+ const option = page.locator('.p-dropdown-panel .p-dropdown-item').filter({ hasText: 'Đang hoạt động' }).first();
44
+ await option.click();
45
+ await page.waitForTimeout(300);
46
+
47
+ return { selectedLabel: await dropdown.locator('.p-dropdown-label').innerText() };
48
+ }
49
+ ```
50
+
51
+ ## Recipe 4: Modal Dialogs & Popup Forms (`p-dialog`, `.card.popup-data`)
52
+ ```javascript
53
+ async (page) => {
54
+ await page.evaluate(() => {
55
+ function setAngularInput(selector, val) {
56
+ const el = document.querySelector(selector);
57
+ if (el) {
58
+ el.value = val;
59
+ el.dispatchEvent(new Event('input', { bubbles: true }));
60
+ el.dispatchEvent(new Event('change', { bubbles: true }));
61
+ }
62
+ }
63
+ const testCode = 'CCN_' + Math.floor(Date.now() / 1000);
64
+ setAngularInput('input[formcontrolname="code"]', testCode);
65
+ setAngularInput('input[formcontrolname="name"]', 'Cụm Công Nghiệp ' + testCode);
66
+ setAngularInput('textarea[formcontrolname="address"]', 'Địa chỉ kiểm thử tự động');
67
+ });
68
+
69
+ const saveBtn = page.locator('button:has-text("Lưu"), button:has-text("Xác nhận"), button[title="Xác nhận"]').first();
70
+ const isEnabled = !(await saveBtn.isDisabled());
71
+ if (isEnabled) {
72
+ await saveBtn.click();
73
+ await page.waitForTimeout(1500);
74
+ }
75
+ return { isSaveButtonEnabled: isEnabled };
76
+ }
77
+ ```
78
+
79
+ ## Recipe 5: Confirm Dialog & Deletion (`p-confirmdialog`)
80
+ ```javascript
81
+ async (page) => {
82
+ await page.locator('p-table tbody tr button.p-button-danger, p-table tbody tr button[icon*="trash"]').first().click();
83
+ await page.waitForTimeout(500);
84
+
85
+ const confirmBtn = page.locator('.p-confirm-dialog-accept, button:has-text("Đồng ý"), button:has-text("Xóa")').first();
86
+ await confirmBtn.click();
87
+ await page.waitForTimeout(1000);
88
+
89
+ const toastText = await page.locator('.p-toast-message-content').innerText().catch(() => 'No toast');
90
+ return { toastText };
91
+ }
92
+ ```
93
+
94
+ ## Recipe 6: File Upload & Excel Import (`input[type="file"]`)
95
+ ```javascript
96
+ async (page, customPath = 'Data Files/sample_import.xlsx') => {
97
+ const fs = require('fs');
98
+ const filePath = fs.existsSync(customPath) ? customPath : 'Data Files/sample_import.xlsx';
99
+ await page.locator('input[type="file"]').setInputFiles(filePath);
100
+ await page.waitForTimeout(1000);
101
+
102
+ await page.locator('button:has-text("Tải lên"), button:has-text("Xác nhận")').first().click();
103
+ await page.waitForTimeout(2000);
104
+ return { status: "Uploaded", fileUsed: filePath };
105
+ }
106
+ ```
107
+
108
+ ## Recipe 7: OpenLayers GIS Canvas Interaction (`div.ol-viewport`)
109
+ ```javascript
110
+ async (page) => {
111
+ const canvas = page.locator('div.ol-viewport canvas').first();
112
+ const box = await canvas.boundingBox();
113
+ const cx = box.x + box.width / 2;
114
+ const cy = box.y + box.height / 2;
115
+
116
+ // Click polygon at non-overlapping offset (-180, -80)
117
+ await page.mouse.click(cx - 180, cy - 80);
118
+ await page.waitForTimeout(400);
119
+ await page.mouse.click(cx - 150, cy - 80);
120
+ await page.waitForTimeout(400);
121
+ await page.mouse.click(cx - 150, cy - 50);
122
+ await page.waitForTimeout(400);
123
+ await page.mouse.dblclick(cx - 180, cy - 80);
124
+ await page.waitForTimeout(1000);
125
+
126
+ const result = await page.evaluate(() => {
127
+ const error = document.querySelector('.text-danger, .invalid-feedback, editor-widget');
128
+ return {
129
+ hasError: !!error,
130
+ errorText: error ? error.innerText : ''
131
+ };
132
+ });
133
+ return result;
134
+ }
135
+ ```
136
+
137
+ ## Recipe 8: Advanced OpenLayers Map Inspection & VN2000 Coordinates
138
+ ```javascript
139
+ async (page) => {
140
+ return await page.evaluate(() => {
141
+ const viewport = document.querySelector('div.ol-viewport');
142
+ if (!viewport) return { error: "div.ol-viewport not found" };
143
+
144
+ const map = viewport['__ol_map__'] || (window.map instanceof Object ? window.map : null);
145
+ let mapInfo = {};
146
+ if (map && map.getView) {
147
+ const view = map.getView();
148
+ mapInfo = {
149
+ zoom: view.getZoom(),
150
+ center: view.getCenter(),
151
+ projection: view.getProjection() ? view.getProjection().getCode() : 'Unknown'
152
+ };
153
+ }
154
+ const toolbarButtons = Array.from(document.querySelectorAll('.ol-control button, .map-toolbar button, .gis-tools button'))
155
+ .map(b => ({ title: b.getAttribute('title') || b.innerText.trim(), className: b.className }));
156
+ return { mapInfo, toolbarButtons };
157
+ });
158
+ }
159
+ ```
160
+
161
+ ## Recipe 9: Two-Phase Search Workflow
162
+ ```javascript
163
+ async (page) => {
164
+ const searchToggleBtn = page.locator('button.p-button-icon-only[icon*="search"], button[title*="Tìm kiếm"], button:has-text("Tìm kiếm")').first();
165
+ const searchInput = page.locator('input.p-inputtext[placeholder*="Tìm kiếm"], input[placeholder*="tên"], input[placeholder*="mã"]').first();
166
+
167
+ if (!(await searchInput.isVisible())) {
168
+ await searchToggleBtn.click();
169
+ await page.waitForTimeout(300);
170
+ }
171
+
172
+ const keyword = 'CCN_TEST_2026';
173
+ await searchInput.fill(keyword);
174
+ await searchInput.press('Enter');
175
+ await page.waitForTimeout(1000);
176
+
177
+ const rowCount = await page.locator('p-table table tbody tr').count();
178
+ const matched = rowCount > 0 && (await page.locator('p-table table tbody tr').first().innerText()).includes(keyword);
179
+ return { rowCount, matched };
180
+ }
181
+ ```
182
+
183
+ ## Recipe 10: Intercepting PrimeNG Toasts (`p-toast`)
184
+ ```javascript
185
+ async (page) => {
186
+ const toastSelector = '.p-toast-message, .p-toast-message-content';
187
+ try {
188
+ const toast = await page.waitForSelector(toastSelector, { timeout: 4000 });
189
+ const text = await toast.innerText();
190
+ const isSuccess = (await toast.getAttribute('class') || '').includes('p-toast-message-success');
191
+ return { success: true, text, isSuccess };
192
+ } catch (e) {
193
+ return { success: false, message: 'Toast did not appear or expired too quickly' };
194
+ }
195
+ }
196
+ ```
197
+
198
+ ## Recipe 11: Record Deletion & Confirmation Dialog (`p-confirmdialog`)
199
+ ```javascript
200
+ async (page, targetKeyword) => {
201
+ const searchInput = page.locator('input.p-inputtext[placeholder*="Tìm kiếm"], input[placeholder*="tên"], input[placeholder*="mã"]').first();
202
+ await searchInput.fill(targetKeyword);
203
+ await searchInput.press('Enter');
204
+ await page.waitForTimeout(1000);
205
+
206
+ const deleteBtn = page.locator('p-table tbody tr:first-child button.p-button-danger, p-table tbody tr:first-child button[icon*="trash"]').first();
207
+ await deleteBtn.click();
208
+ await page.waitForTimeout(500);
209
+
210
+ const confirmBtn = page.locator('.p-confirm-dialog-accept, .p-dialog-footer button:has-text("Xóa"), .p-dialog-footer button:has-text("Đồng ý"), button.p-button-danger:has-text("Xóa")').first();
211
+ await confirmBtn.waitFor({ state: 'visible', timeout: 3000 });
212
+ await confirmBtn.click();
213
+ await page.waitForTimeout(1500);
214
+
215
+ await searchInput.fill(targetKeyword);
216
+ await searchInput.press('Enter');
217
+ await page.waitForTimeout(1000);
218
+ const remainingRows = await page.locator('p-table table tbody tr').count();
219
+ const firstRowText = remainingRows > 0 ? await page.locator('p-table table tbody tr').first().innerText() : '';
220
+ const isDeleted = remainingRows === 0 || !firstRowText.includes(targetKeyword) || firstRowText.includes('Không có dữ liệu');
221
+
222
+ return { isDeleted, remainingRows, firstRowText };
223
+ }
224
+ ```
225
+
226
+ ## Recipe 12: PrimeNG Calendar Date Input (`p-calendar`)
227
+ ```javascript
228
+ async (page) => {
229
+ const dateValue = '25/10/2026';
230
+ await page.evaluate(({ selector, val }) => {
231
+ const calInput = document.querySelector(selector);
232
+ if (calInput) {
233
+ calInput.value = val;
234
+ calInput.dispatchEvent(new Event('input', { bubbles: true }));
235
+ calInput.dispatchEvent(new Event('change', { bubbles: true }));
236
+ calInput.dispatchEvent(new Event('blur', { bubbles: true }));
237
+ }
238
+ }, { selector: 'p-calendar input, input[formcontrolname*="date"], input[formcontrolname*="Date"]', val: dateValue });
239
+
240
+ await page.waitForTimeout(300);
241
+ return { status: 'Date filled', value: dateValue };
242
+ }
243
+ ```
244
+
245
+ ## Recipe 13: PrimeNG MultiSelect Dropdown (`p-multiselect`)
246
+ ```javascript
247
+ async (page, optionsToSelect = ['Công nghiệp chế biến', 'Điện lực']) => {
248
+ const multiSelect = page.locator('p-multiselect').first();
249
+ await multiSelect.click();
250
+ await page.waitForSelector('.p-multiselect-panel', { state: 'visible', timeout: 3000 });
251
+ await page.waitForTimeout(300);
252
+
253
+ for (const optText of optionsToSelect) {
254
+ const item = page.locator('.p-multiselect-item').filter({ hasText: optText }).first();
255
+ if (await item.isVisible()) {
256
+ await item.click();
257
+ await page.waitForTimeout(200);
258
+ }
259
+ }
260
+
261
+ const closeBtn = page.locator('.p-multiselect-close').first();
262
+ if (await closeBtn.isVisible()) {
263
+ await closeBtn.click();
264
+ } else {
265
+ await page.keyboard.press('Escape');
266
+ }
267
+ await page.waitForTimeout(300);
268
+ return { selected: optionsToSelect };
269
+ }
270
+ ```
271
+
272
+ ## Recipe 14: Handling Stale Elements & Angular DOM Re-render Retries
273
+ ```javascript
274
+ async (page, actionFn) => {
275
+ const maxRetries = 3;
276
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
277
+ try {
278
+ return await actionFn(page);
279
+ } catch (err) {
280
+ if (err.message.includes('Element is not attached to the DOM') || err.message.includes('stale') || attempt === maxRetries) {
281
+ if (attempt === maxRetries) throw err;
282
+ await page.waitForTimeout(500 * attempt);
283
+ } else {
284
+ throw err;
285
+ }
286
+ }
287
+ }
288
+ }
289
+ ```
290
+
291
+
292
+ ## Recipe 15: Instant Angular Form Audit (Diagnose Disabled Save Button)
293
+ ```javascript
294
+ async (page) => {
295
+ return await page.evaluate(() => {
296
+ const invalidInputs = Array.from(document.querySelectorAll('form .ng-invalid, input.ng-invalid, p-dropdown.ng-invalid, textarea.ng-invalid'));
297
+ return invalidInputs.map(el => ({
298
+ tag: el.tagName.toLowerCase(),
299
+ control: el.getAttribute('formcontrolname') || el.getAttribute('name') || el.id || 'unnamed',
300
+ placeholder: el.getAttribute('placeholder') || '',
301
+ currentValue: el.value || el.innerText.trim()
302
+ }));
303
+ });
304
+ }
305
+ ```
306
+
307
+ ## Recipe 16: Smart Action Batching (Atomic Execution with Pause-on-Failure)
308
+ ```javascript
309
+ async (page) => {
310
+ return await page.evaluate(() => {
311
+ const steps = [
312
+ { name: 'fill_code', fn: () => { const el = document.querySelector("input[formcontrolname='code']"); el.value = 'CCN_TEST'; el.dispatchEvent(new Event('input', {bubbles: true})); } },
313
+ { name: 'fill_name', fn: () => { const el = document.querySelector("input[formcontrolname='name']"); el.value = 'Cụm CN Test'; el.dispatchEvent(new Event('input', {bubbles: true})); } },
314
+ { name: 'submit', fn: () => { document.querySelector("button[type='submit']").click(); } }
315
+ ];
316
+
317
+ for (let i = 0; i < steps.length; i++) {
318
+ try {
319
+ steps[i].fn();
320
+ } catch (err) {
321
+ return { status: 'FAILED', failedStep: i + 1, name: steps[i].name, error: err.message, url: location.href };
322
+ }
323
+ }
324
+ return { status: 'SUCCESS', totalSteps: steps.length };
325
+ });
326
+ }
327
+ ```
328
+
329
+ ## Recipe 17: Runtime Helper Injection (`window.__qa`)
330
+ ```javascript
331
+ async (page) => {
332
+ // Injects the centralized browser helper tools/browser_helpers.js
333
+ const fs = require('fs');
334
+ const helperCode = fs.readFileSync('tools/browser_helpers.js', 'utf8');
335
+ await page.evaluate(helperCode);
336
+ return await page.evaluate(() => ({
337
+ version: window.__qa.__version,
338
+ auditResults: window.__qa.audit()
339
+ }));
340
+ }
341
+ ```
342
+
343
+ ## Recipe 18: Self-Healing Locator Fallback (`window.__qa.heal`)
344
+ ```javascript
345
+ async (page, brokenXPath, expectedText) => {
346
+ return await page.evaluate(({ xpath, text }) => {
347
+ if (window.__qa) {
348
+ return window.__qa.heal(xpath, text);
349
+ }
350
+ // Fallback search
351
+ const buttons = Array.from(document.querySelectorAll('button')).filter(b => b.innerText.includes(text));
352
+ return {
353
+ candidates: buttons.map(b => `//button[contains(normalize-space(.), '${b.innerText.trim()}')]`)
354
+ };
355
+ }, { xpath: brokenXPath, text: expectedText });
356
+ }
357
+ ```
358
+
359
+ ## Recipe 19: Overlay-Proof Safe Click (`window.__qa.clickSafe`)
360
+ ```javascript
361
+ async (page, selector) => {
362
+ return await page.evaluate((sel) => {
363
+ if (window.__qa) {
364
+ return window.__qa.clickSafe(sel);
365
+ }
366
+ const el = document.querySelector(sel);
367
+ if (!el) return false;
368
+ document.querySelectorAll('.p-component-overlay').forEach(o => o.style.pointerEvents = 'none');
369
+ el.click();
370
+ return true;
371
+ }, selector);
372
+ }
373
+ ```
374
+
375
+ ## Recipe 20: OpenLayers GIS Canvas Coordinate Inspector (`window.__qa.gisHUD`)
376
+ ```javascript
377
+ async (page) => {
378
+ return await page.evaluate(() => {
379
+ if (window.__qa) {
380
+ return window.__qa.gisHUD();
381
+ }
382
+ const vp = document.querySelector('.ol-viewport');
383
+ if (!vp) return { status: 'NO_MAP' };
384
+ const r = vp.getBoundingClientRect();
385
+ return { width: r.width, height: r.height, safeCenter: { x: r.width / 2, y: r.height / 2 } };
386
+ });
387
+ }
388
+ ```