@thinkwise/testwise 0.2.62 → 0.2.99

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 (458) hide show
  1. package/Testwise.ts +35 -0
  2. package/artifact-builder/ArtifactManager.ts +25 -2
  3. package/artifact-builder/ComponentClassModelBuilder.ts +148 -0
  4. package/artifact-builder/InterfaceGenerator.ts +31 -33
  5. package/artifact-builder/ModelDataBuilder.ts +18 -20
  6. package/artifact-builder/ModelDataRefiner.ts +23 -3
  7. package/artifact-builder/SchemaGenerator.ts +45 -35
  8. package/artifact-builder/ScreenInterfaceRefiner.ts +25 -10
  9. package/artifact-builder/SelectorBuilder.ts +30 -24
  10. package/artifact-builder/SubjectClassModelBuilder.ts +209 -0
  11. package/artifact-builder/SubjectComponentGenerator.ts +67 -246
  12. package/artifact-builder/SubjectGenerator.ts +117 -230
  13. package/artifact-builder/SubjectRegistration.ts +41 -32
  14. package/artifact-builder/TsFileModel.ts +94 -0
  15. package/artifact-builder/TsFileSerializer.ts +247 -0
  16. package/artifact-builder/helpers/DataRetriever.ts +64 -27
  17. package/artifact-builder/helpers/NamingHandler.ts +23 -78
  18. package/artifact-builder/helpers/PipelinePaths.ts +123 -0
  19. package/artifact-builder/helpers/Stopwatch.ts +10 -0
  20. package/artifact-builder/helpers/index.ts +1 -0
  21. package/artifact-builder/index.ts +4 -0
  22. package/components/BaseComponentObjects.ts +17 -0
  23. package/components/action-bar/ActionBar.ts +35 -47
  24. package/components/action-bar/ActionBarObjects.ts +2 -0
  25. package/components/action-bar/CustomActionBar.ts +6 -0
  26. package/components/cardlist/Cardlist.ts +6 -0
  27. package/components/chart/Chart.ts +6 -0
  28. package/components/combined-filter/CombinedFilter.ts +9 -0
  29. package/components/cube-view-bar/CubeViewBar.ts +6 -0
  30. package/components/export/ExportComponent.ts +12 -0
  31. package/components/filter/ExcelStyleFilterPopup.ts +83 -10
  32. package/components/filter/ExcelStyleFilterPopupObjects.ts +29 -3
  33. package/components/filter/FilterForm.ts +10 -0
  34. package/components/filter/FilterPopup.ts +30 -9
  35. package/components/filter/FilterPopupObjects.ts +10 -3
  36. package/components/filter/FindForm.ts +7 -0
  37. package/components/filter/ManagePrefilterPopup.ts +11 -0
  38. package/components/form/Form.ts +16 -0
  39. package/components/grid/Grid.ts +50 -14
  40. package/components/grid/GridObjects.ts +0 -15
  41. package/components/maps/Maps.ts +6 -0
  42. package/components/menu/ListBarMenuComponent.ts +10 -0
  43. package/components/pivot-grid/PivotGrid.ts +6 -0
  44. package/components/pivot-grid/PivotGridFieldList.ts +7 -0
  45. package/components/pop-up/AllRowsPopUp.ts +12 -0
  46. package/components/pop-up/PopUpComponent.ts +11 -0
  47. package/components/prefilter-list/PrefilterList.ts +9 -0
  48. package/components/preview/Preview.ts +6 -0
  49. package/components/report-bar/ReportBar.ts +6 -0
  50. package/components/report-tiles/ReportTiles.ts +6 -0
  51. package/components/scheduler/Scheduler.ts +6 -0
  52. package/components/snackbar/SnackBar.ts +12 -0
  53. package/components/splitter/Splitter.ts +6 -0
  54. package/components/tab/BaseTab.ts +5 -0
  55. package/components/tab/DetailTabPage.ts +11 -0
  56. package/components/tab/Tab.ts +10 -0
  57. package/components/task/TaskBar.ts +13 -3
  58. package/components/task/TaskTiles.ts +12 -3
  59. package/components/topbar/TopBar.ts +10 -0
  60. package/components/tree-view/TreeView.ts +9 -0
  61. package/controls/DateControl.ts +47 -9
  62. package/controls/DateControlObjects.ts +15 -0
  63. package/controls/FormComboBox.ts +43 -26
  64. package/controls/FormComboBoxObjects.ts +27 -0
  65. package/controls/GridColumnControl.ts +35 -4
  66. package/controls/GridColumnControlObjects.ts +1 -1
  67. package/controls/InputFieldControl.ts +61 -15
  68. package/controls/InputFieldControlObjects.ts +19 -0
  69. package/controls/LookupDropdown.ts +59 -31
  70. package/controls/LookupDropdownObjects.ts +19 -0
  71. package/dist/Testwise.js +35 -0
  72. package/dist/Testwise.js.map +1 -1
  73. package/dist/artifact-builder/ArtifactManager.d.ts +18 -1
  74. package/dist/artifact-builder/ArtifactManager.js +24 -2
  75. package/dist/artifact-builder/ArtifactManager.js.map +1 -1
  76. package/dist/artifact-builder/ComponentClassModelBuilder.d.ts +22 -0
  77. package/dist/artifact-builder/ComponentClassModelBuilder.js +115 -0
  78. package/dist/artifact-builder/ComponentClassModelBuilder.js.map +1 -0
  79. package/dist/artifact-builder/InterfaceGenerator.d.ts +15 -6
  80. package/dist/artifact-builder/InterfaceGenerator.js +27 -26
  81. package/dist/artifact-builder/InterfaceGenerator.js.map +1 -1
  82. package/dist/artifact-builder/ModelDataBuilder.d.ts +7 -0
  83. package/dist/artifact-builder/ModelDataBuilder.js +13 -5
  84. package/dist/artifact-builder/ModelDataBuilder.js.map +1 -1
  85. package/dist/artifact-builder/ModelDataRefiner.d.ts +18 -1
  86. package/dist/artifact-builder/ModelDataRefiner.js +18 -3
  87. package/dist/artifact-builder/ModelDataRefiner.js.map +1 -1
  88. package/dist/artifact-builder/SchemaGenerator.d.ts +21 -5
  89. package/dist/artifact-builder/SchemaGenerator.js +38 -26
  90. package/dist/artifact-builder/SchemaGenerator.js.map +1 -1
  91. package/dist/artifact-builder/ScreenInterfaceRefiner.d.ts +18 -1
  92. package/dist/artifact-builder/ScreenInterfaceRefiner.js +23 -9
  93. package/dist/artifact-builder/ScreenInterfaceRefiner.js.map +1 -1
  94. package/dist/artifact-builder/SelectorBuilder.d.ts +18 -5
  95. package/dist/artifact-builder/SelectorBuilder.js +29 -14
  96. package/dist/artifact-builder/SelectorBuilder.js.map +1 -1
  97. package/dist/artifact-builder/SubjectClassModelBuilder.d.ts +38 -0
  98. package/dist/artifact-builder/SubjectClassModelBuilder.js +158 -0
  99. package/dist/artifact-builder/SubjectClassModelBuilder.js.map +1 -0
  100. package/dist/artifact-builder/SubjectComponentGenerator.d.ts +25 -14
  101. package/dist/artifact-builder/SubjectComponentGenerator.js +65 -189
  102. package/dist/artifact-builder/SubjectComponentGenerator.js.map +1 -1
  103. package/dist/artifact-builder/SubjectGenerator.d.ts +32 -13
  104. package/dist/artifact-builder/SubjectGenerator.js +81 -165
  105. package/dist/artifact-builder/SubjectGenerator.js.map +1 -1
  106. package/dist/artifact-builder/SubjectRegistration.d.ts +22 -8
  107. package/dist/artifact-builder/SubjectRegistration.js +29 -14
  108. package/dist/artifact-builder/SubjectRegistration.js.map +1 -1
  109. package/dist/artifact-builder/TsFileModel.d.ts +86 -0
  110. package/dist/artifact-builder/TsFileModel.js +2 -0
  111. package/dist/artifact-builder/TsFileModel.js.map +1 -0
  112. package/dist/artifact-builder/TsFileSerializer.d.ts +19 -0
  113. package/dist/artifact-builder/TsFileSerializer.js +225 -0
  114. package/dist/artifact-builder/TsFileSerializer.js.map +1 -0
  115. package/dist/artifact-builder/helpers/DataRetriever.d.ts +22 -3
  116. package/dist/artifact-builder/helpers/DataRetriever.js +56 -26
  117. package/dist/artifact-builder/helpers/DataRetriever.js.map +1 -1
  118. package/dist/artifact-builder/helpers/NamingHandler.d.ts +18 -6
  119. package/dist/artifact-builder/helpers/NamingHandler.js +20 -61
  120. package/dist/artifact-builder/helpers/NamingHandler.js.map +1 -1
  121. package/dist/artifact-builder/helpers/PipelinePaths.d.ts +52 -0
  122. package/dist/artifact-builder/helpers/PipelinePaths.js +99 -0
  123. package/dist/artifact-builder/helpers/PipelinePaths.js.map +1 -0
  124. package/dist/artifact-builder/helpers/Stopwatch.d.ts +10 -0
  125. package/dist/artifact-builder/helpers/Stopwatch.js +10 -0
  126. package/dist/artifact-builder/helpers/Stopwatch.js.map +1 -1
  127. package/dist/artifact-builder/helpers/index.d.ts +1 -0
  128. package/dist/artifact-builder/helpers/index.js +1 -0
  129. package/dist/artifact-builder/helpers/index.js.map +1 -1
  130. package/dist/artifact-builder/index.d.ts +4 -0
  131. package/dist/artifact-builder/index.js +4 -0
  132. package/dist/artifact-builder/index.js.map +1 -1
  133. package/dist/components/BaseComponentObjects.d.ts +17 -0
  134. package/dist/components/BaseComponentObjects.js +17 -0
  135. package/dist/components/BaseComponentObjects.js.map +1 -1
  136. package/dist/components/action-bar/ActionBar.d.ts +21 -24
  137. package/dist/components/action-bar/ActionBar.js +31 -24
  138. package/dist/components/action-bar/ActionBar.js.map +1 -1
  139. package/dist/components/action-bar/ActionBarObjects.d.ts +1 -0
  140. package/dist/components/action-bar/ActionBarObjects.js +1 -0
  141. package/dist/components/action-bar/ActionBarObjects.js.map +1 -1
  142. package/dist/components/action-bar/CustomActionBar.d.ts +6 -0
  143. package/dist/components/action-bar/CustomActionBar.js +6 -0
  144. package/dist/components/action-bar/CustomActionBar.js.map +1 -1
  145. package/dist/components/cardlist/Cardlist.d.ts +6 -0
  146. package/dist/components/cardlist/Cardlist.js +6 -0
  147. package/dist/components/cardlist/Cardlist.js.map +1 -1
  148. package/dist/components/chart/Chart.d.ts +6 -0
  149. package/dist/components/chart/Chart.js +6 -0
  150. package/dist/components/chart/Chart.js.map +1 -1
  151. package/dist/components/combined-filter/CombinedFilter.d.ts +9 -0
  152. package/dist/components/combined-filter/CombinedFilter.js +9 -0
  153. package/dist/components/combined-filter/CombinedFilter.js.map +1 -1
  154. package/dist/components/cube-view-bar/CubeViewBar.d.ts +6 -0
  155. package/dist/components/cube-view-bar/CubeViewBar.js +6 -0
  156. package/dist/components/cube-view-bar/CubeViewBar.js.map +1 -1
  157. package/dist/components/export/ExportComponent.d.ts +12 -0
  158. package/dist/components/export/ExportComponent.js +12 -0
  159. package/dist/components/export/ExportComponent.js.map +1 -1
  160. package/dist/components/filter/ExcelStyleFilterPopup.d.ts +25 -2
  161. package/dist/components/filter/ExcelStyleFilterPopup.js +60 -6
  162. package/dist/components/filter/ExcelStyleFilterPopup.js.map +1 -1
  163. package/dist/components/filter/ExcelStyleFilterPopupObjects.d.ts +8 -1
  164. package/dist/components/filter/ExcelStyleFilterPopupObjects.js +15 -2
  165. package/dist/components/filter/ExcelStyleFilterPopupObjects.js.map +1 -1
  166. package/dist/components/filter/FilterForm.d.ts +10 -0
  167. package/dist/components/filter/FilterForm.js +10 -0
  168. package/dist/components/filter/FilterForm.js.map +1 -1
  169. package/dist/components/filter/FilterPopup.d.ts +22 -4
  170. package/dist/components/filter/FilterPopup.js +17 -9
  171. package/dist/components/filter/FilterPopup.js.map +1 -1
  172. package/dist/components/filter/FilterPopupObjects.d.ts +5 -2
  173. package/dist/components/filter/FilterPopupObjects.js +5 -2
  174. package/dist/components/filter/FilterPopupObjects.js.map +1 -1
  175. package/dist/components/filter/FindForm.d.ts +7 -0
  176. package/dist/components/filter/FindForm.js +7 -0
  177. package/dist/components/filter/FindForm.js.map +1 -1
  178. package/dist/components/filter/ManagePrefilterPopup.d.ts +11 -0
  179. package/dist/components/filter/ManagePrefilterPopup.js +11 -0
  180. package/dist/components/filter/ManagePrefilterPopup.js.map +1 -1
  181. package/dist/components/form/Form.d.ts +16 -0
  182. package/dist/components/form/Form.js +16 -0
  183. package/dist/components/form/Form.js.map +1 -1
  184. package/dist/components/grid/Grid.d.ts +20 -1
  185. package/dist/components/grid/Grid.js +35 -25
  186. package/dist/components/grid/Grid.js.map +1 -1
  187. package/dist/components/grid/GridObjects.d.ts +0 -4
  188. package/dist/components/grid/GridObjects.js +0 -6
  189. package/dist/components/grid/GridObjects.js.map +1 -1
  190. package/dist/components/maps/Maps.d.ts +6 -0
  191. package/dist/components/maps/Maps.js +6 -0
  192. package/dist/components/maps/Maps.js.map +1 -1
  193. package/dist/components/menu/ListBarMenuComponent.d.ts +10 -0
  194. package/dist/components/menu/ListBarMenuComponent.js +10 -0
  195. package/dist/components/menu/ListBarMenuComponent.js.map +1 -1
  196. package/dist/components/pivot-grid/PivotGrid.d.ts +6 -0
  197. package/dist/components/pivot-grid/PivotGrid.js +6 -0
  198. package/dist/components/pivot-grid/PivotGrid.js.map +1 -1
  199. package/dist/components/pivot-grid/PivotGridFieldList.d.ts +7 -0
  200. package/dist/components/pivot-grid/PivotGridFieldList.js +7 -0
  201. package/dist/components/pivot-grid/PivotGridFieldList.js.map +1 -1
  202. package/dist/components/pop-up/AllRowsPopUp.d.ts +12 -0
  203. package/dist/components/pop-up/AllRowsPopUp.js +12 -0
  204. package/dist/components/pop-up/AllRowsPopUp.js.map +1 -1
  205. package/dist/components/pop-up/PopUpComponent.d.ts +11 -0
  206. package/dist/components/pop-up/PopUpComponent.js +11 -0
  207. package/dist/components/pop-up/PopUpComponent.js.map +1 -1
  208. package/dist/components/prefilter-list/PrefilterList.d.ts +9 -0
  209. package/dist/components/prefilter-list/PrefilterList.js +9 -0
  210. package/dist/components/prefilter-list/PrefilterList.js.map +1 -1
  211. package/dist/components/preview/Preview.d.ts +6 -0
  212. package/dist/components/preview/Preview.js +6 -0
  213. package/dist/components/preview/Preview.js.map +1 -1
  214. package/dist/components/report-bar/ReportBar.d.ts +6 -0
  215. package/dist/components/report-bar/ReportBar.js +6 -0
  216. package/dist/components/report-bar/ReportBar.js.map +1 -1
  217. package/dist/components/report-tiles/ReportTiles.d.ts +6 -0
  218. package/dist/components/report-tiles/ReportTiles.js +6 -0
  219. package/dist/components/report-tiles/ReportTiles.js.map +1 -1
  220. package/dist/components/scheduler/Scheduler.d.ts +6 -0
  221. package/dist/components/scheduler/Scheduler.js +6 -0
  222. package/dist/components/scheduler/Scheduler.js.map +1 -1
  223. package/dist/components/snackbar/SnackBar.d.ts +12 -0
  224. package/dist/components/snackbar/SnackBar.js +12 -0
  225. package/dist/components/snackbar/SnackBar.js.map +1 -1
  226. package/dist/components/splitter/Splitter.d.ts +6 -0
  227. package/dist/components/splitter/Splitter.js +6 -0
  228. package/dist/components/splitter/Splitter.js.map +1 -1
  229. package/dist/components/tab/BaseTab.d.ts +5 -0
  230. package/dist/components/tab/BaseTab.js +5 -0
  231. package/dist/components/tab/BaseTab.js.map +1 -1
  232. package/dist/components/tab/DetailTabPage.d.ts +11 -0
  233. package/dist/components/tab/DetailTabPage.js +11 -0
  234. package/dist/components/tab/DetailTabPage.js.map +1 -1
  235. package/dist/components/tab/Tab.d.ts +10 -0
  236. package/dist/components/tab/Tab.js +10 -0
  237. package/dist/components/tab/Tab.js.map +1 -1
  238. package/dist/components/task/TaskBar.d.ts +12 -2
  239. package/dist/components/task/TaskBar.js +13 -3
  240. package/dist/components/task/TaskBar.js.map +1 -1
  241. package/dist/components/task/TaskTiles.d.ts +11 -2
  242. package/dist/components/task/TaskTiles.js +12 -3
  243. package/dist/components/task/TaskTiles.js.map +1 -1
  244. package/dist/components/topbar/TopBar.d.ts +10 -0
  245. package/dist/components/topbar/TopBar.js +10 -0
  246. package/dist/components/topbar/TopBar.js.map +1 -1
  247. package/dist/components/tree-view/TreeView.d.ts +9 -0
  248. package/dist/components/tree-view/TreeView.js +9 -0
  249. package/dist/components/tree-view/TreeView.js.map +1 -1
  250. package/dist/controls/DateControl.d.ts +40 -2
  251. package/dist/controls/DateControl.js +24 -7
  252. package/dist/controls/DateControl.js.map +1 -1
  253. package/dist/controls/DateControlObjects.d.ts +8 -1
  254. package/dist/controls/DateControlObjects.js +8 -1
  255. package/dist/controls/DateControlObjects.js.map +1 -1
  256. package/dist/controls/FormComboBox.d.ts +30 -0
  257. package/dist/controls/FormComboBox.js +19 -16
  258. package/dist/controls/FormComboBox.js.map +1 -1
  259. package/dist/controls/FormComboBoxObjects.d.ts +12 -1
  260. package/dist/controls/FormComboBoxObjects.js +15 -1
  261. package/dist/controls/FormComboBoxObjects.js.map +1 -1
  262. package/dist/controls/GridColumnControl.d.ts +30 -0
  263. package/dist/controls/GridColumnControl.js +13 -4
  264. package/dist/controls/GridColumnControl.js.map +1 -1
  265. package/dist/controls/GridColumnControlObjects.js +1 -1
  266. package/dist/controls/GridColumnControlObjects.js.map +1 -1
  267. package/dist/controls/InputFieldControl.d.ts +50 -3
  268. package/dist/controls/InputFieldControl.js +40 -11
  269. package/dist/controls/InputFieldControl.js.map +1 -1
  270. package/dist/controls/InputFieldControlObjects.d.ts +10 -1
  271. package/dist/controls/InputFieldControlObjects.js +10 -1
  272. package/dist/controls/InputFieldControlObjects.js.map +1 -1
  273. package/dist/controls/LookupDropdown.d.ts +29 -0
  274. package/dist/controls/LookupDropdown.js +36 -25
  275. package/dist/controls/LookupDropdown.js.map +1 -1
  276. package/dist/controls/LookupDropdownObjects.d.ts +10 -0
  277. package/dist/controls/LookupDropdownObjects.js +11 -0
  278. package/dist/controls/LookupDropdownObjects.js.map +1 -0
  279. package/dist/enums/ElementTypes.d.ts +18 -0
  280. package/dist/enums/ElementTypes.js +18 -0
  281. package/dist/enums/ElementTypes.js.map +1 -1
  282. package/dist/enums/ExportFormat.d.ts +8 -0
  283. package/dist/enums/ExportFormat.js +8 -0
  284. package/dist/enums/ExportFormat.js.map +1 -1
  285. package/dist/enums/LogLevel.d.ts +17 -0
  286. package/dist/enums/LogLevel.js +17 -1
  287. package/dist/enums/LogLevel.js.map +1 -1
  288. package/dist/enums/index.d.ts +1 -0
  289. package/dist/enums/index.js +1 -0
  290. package/dist/enums/index.js.map +1 -1
  291. package/dist/helpers/ConfigChecker.d.ts +13 -0
  292. package/dist/helpers/ConfigChecker.js +13 -0
  293. package/dist/helpers/ConfigChecker.js.map +1 -1
  294. package/dist/helpers/Ensure.d.ts +13 -0
  295. package/dist/helpers/Ensure.js +13 -0
  296. package/dist/helpers/Ensure.js.map +1 -1
  297. package/dist/helpers/FlightTracker.d.ts +13 -3
  298. package/dist/helpers/FlightTracker.js +103 -17
  299. package/dist/helpers/FlightTracker.js.map +1 -1
  300. package/dist/helpers/GlobalWaitEventHandler.d.ts +13 -0
  301. package/dist/helpers/GlobalWaitEventHandler.js +13 -0
  302. package/dist/helpers/GlobalWaitEventHandler.js.map +1 -1
  303. package/dist/helpers/LoginHelper.d.ts +35 -1
  304. package/dist/helpers/LoginHelper.js +46 -11
  305. package/dist/helpers/LoginHelper.js.map +1 -1
  306. package/dist/helpers/PathResolver.d.ts +22 -0
  307. package/dist/helpers/PathResolver.js +22 -0
  308. package/dist/helpers/PathResolver.js.map +1 -1
  309. package/dist/helpers/Poll.d.ts +13 -1
  310. package/dist/helpers/Poll.js +12 -0
  311. package/dist/helpers/Poll.js.map +1 -1
  312. package/dist/helpers/RegexHelper.d.ts +15 -0
  313. package/dist/helpers/RegexHelper.js +15 -3
  314. package/dist/helpers/RegexHelper.js.map +1 -1
  315. package/dist/helpers/SubjectNavigator.d.ts +26 -0
  316. package/dist/helpers/SubjectNavigator.js +51 -0
  317. package/dist/helpers/SubjectNavigator.js.map +1 -0
  318. package/dist/helpers/UserSimulationHelper.d.ts +14 -0
  319. package/dist/helpers/UserSimulationHelper.js +14 -0
  320. package/dist/helpers/UserSimulationHelper.js.map +1 -1
  321. package/dist/helpers/index.d.ts +1 -0
  322. package/dist/helpers/index.js +1 -0
  323. package/dist/helpers/index.js.map +1 -1
  324. package/dist/index.d.ts +6 -20
  325. package/dist/index.js +1 -0
  326. package/dist/index.js.map +1 -1
  327. package/dist/interfaces/IComponentObjects.d.ts +12 -0
  328. package/dist/interfaces/IComponentSchemaMeta.d.ts +15 -0
  329. package/dist/interfaces/IComponentSchemaMeta.js +2 -0
  330. package/dist/interfaces/IComponentSchemaMeta.js.map +1 -0
  331. package/dist/interfaces/IProperty.d.ts +13 -0
  332. package/dist/interfaces/IRegisteredSubjects.d.ts +12 -0
  333. package/dist/interfaces/ISeedSubjectDraft.d.ts +16 -0
  334. package/dist/interfaces/ISeedSubjectDraft.js +2 -0
  335. package/dist/interfaces/ISeedSubjectDraft.js.map +1 -0
  336. package/dist/interfaces/ISubject.d.ts +13 -0
  337. package/dist/interfaces/ISubjectDetail.d.ts +11 -0
  338. package/dist/interfaces/ISubjectDetail.js +2 -0
  339. package/dist/interfaces/ISubjectDetail.js.map +1 -0
  340. package/dist/mcp/ArtifactResolver.d.ts +152 -0
  341. package/dist/mcp/ArtifactResolver.js +232 -0
  342. package/dist/mcp/ArtifactResolver.js.map +1 -0
  343. package/dist/mcp/index.d.ts +2 -0
  344. package/dist/mcp/index.js +2 -0
  345. package/dist/mcp/index.js.map +1 -0
  346. package/dist/page-extensions/GlobalComponents.d.ts +30 -0
  347. package/dist/page-extensions/GlobalComponents.js +30 -0
  348. package/dist/page-extensions/GlobalComponents.js.map +1 -1
  349. package/dist/page-extensions/GoToDeepLink.d.ts +9 -0
  350. package/dist/page-extensions/GoToDeepLink.js +9 -0
  351. package/dist/page-extensions/GoToDeepLink.js.map +1 -1
  352. package/dist/page-extensions/LoginFeatures.d.ts +8 -0
  353. package/dist/page-extensions/LoginFeatures.js +8 -0
  354. package/dist/page-extensions/LoginFeatures.js.map +1 -1
  355. package/dist/page-extensions/SubjectRegistry.d.ts +31 -0
  356. package/dist/page-extensions/SubjectRegistry.js +31 -0
  357. package/dist/page-extensions/SubjectRegistry.js.map +1 -1
  358. package/dist/page-extensions/UserSimulation.d.ts +21 -0
  359. package/dist/page-extensions/UserSimulation.js +21 -0
  360. package/dist/page-extensions/UserSimulation.js.map +1 -1
  361. package/dist/page-extensions/WaitEventHandler.d.ts +20 -0
  362. package/dist/page-extensions/WaitEventHandler.js +20 -0
  363. package/dist/page-extensions/WaitEventHandler.js.map +1 -1
  364. package/dist/page-overrides/ClickOverride.d.ts +23 -2
  365. package/dist/page-overrides/ClickOverride.js +34 -50
  366. package/dist/page-overrides/ClickOverride.js.map +1 -1
  367. package/dist/page-overrides/FillOverride.d.ts +23 -0
  368. package/dist/page-overrides/FillOverride.js +46 -66
  369. package/dist/page-overrides/FillOverride.js.map +1 -1
  370. package/dist/services/ConfigBuilder.d.ts +24 -0
  371. package/dist/services/ConfigBuilder.js +24 -0
  372. package/dist/services/ConfigBuilder.js.map +1 -1
  373. package/dist/services/IndiciumApi.service.d.ts +26 -0
  374. package/dist/services/IndiciumApi.service.js +40 -5
  375. package/dist/services/IndiciumApi.service.js.map +1 -1
  376. package/dist/services/Logger.d.ts +13 -0
  377. package/dist/services/Logger.js +39 -0
  378. package/dist/services/Logger.js.map +1 -1
  379. package/dist/types/Components.d.ts +18 -1
  380. package/dist/types/Components.js +0 -1
  381. package/dist/types/Components.js.map +1 -1
  382. package/dist/types/LocatorType.d.ts +5 -0
  383. package/dist/types/LocatorType.js +2 -0
  384. package/dist/types/LocatorType.js.map +1 -0
  385. package/dist/types/PollTypes.d.ts +8 -0
  386. package/dist/types/Test.d.ts +18 -0
  387. package/dist/types/Universal.d.ts +12 -0
  388. package/dist/types/index.d.ts +2 -0
  389. package/dist/types/index.js +2 -0
  390. package/dist/types/index.js.map +1 -1
  391. package/enums/ElementTypes.ts +18 -0
  392. package/enums/ExportFormat.ts +8 -0
  393. package/enums/LogLevel.ts +17 -1
  394. package/enums/index.ts +1 -0
  395. package/helpers/ConfigChecker.ts +13 -0
  396. package/helpers/Ensure.ts +13 -0
  397. package/helpers/FlightTracker.ts +109 -20
  398. package/helpers/GlobalWaitEventHandler.ts +13 -0
  399. package/helpers/LoginHelper.ts +49 -12
  400. package/helpers/PathResolver.ts +22 -0
  401. package/helpers/Poll.ts +13 -1
  402. package/helpers/RegexHelper.ts +15 -3
  403. package/helpers/SubjectNavigator.ts +57 -0
  404. package/helpers/UserSimulationHelper.ts +14 -0
  405. package/helpers/index.ts +1 -0
  406. package/index.ts +1 -0
  407. package/interfaces/IComponentObjects.ts +12 -0
  408. package/interfaces/IComponentSchemaMeta.ts +17 -0
  409. package/interfaces/IProperty.ts +13 -0
  410. package/interfaces/IRegisteredSubjects.ts +12 -0
  411. package/interfaces/ISeedSubjectDraft.ts +17 -0
  412. package/interfaces/ISubject.ts +13 -0
  413. package/interfaces/ISubjectDetail.ts +12 -0
  414. package/mcp/ArtifactResolver.ts +296 -0
  415. package/mcp/index.ts +2 -0
  416. package/package.json +20 -8
  417. package/page-extensions/GlobalComponents.ts +30 -0
  418. package/page-extensions/GoToDeepLink.ts +9 -0
  419. package/page-extensions/LoginFeatures.ts +8 -0
  420. package/page-extensions/SubjectRegistry.ts +32 -0
  421. package/page-extensions/UserSimulation.ts +21 -0
  422. package/page-extensions/WaitEventHandler.ts +20 -0
  423. package/page-overrides/ClickOverride.ts +36 -55
  424. package/page-overrides/FillOverride.ts +59 -73
  425. package/promptCredentials.js +1 -1
  426. package/scripts/Testwise.template.json +1 -1
  427. package/scripts/diagram.js +2267 -0
  428. package/scripts/main.js +84 -63
  429. package/scripts/postinstall.js +55 -42
  430. package/scripts/setup.js +91 -37
  431. package/scripts/sync.js +767 -748
  432. package/services/ConfigBuilder.ts +24 -0
  433. package/services/IndiciumApi.service.ts +44 -6
  434. package/services/Logger.ts +40 -0
  435. package/templates/copilot-instructions.md +130 -0
  436. package/templates/mcp.json +9 -0
  437. package/templates/testwise-test-expert.agent.md +453 -0
  438. package/tests/tsconfig.json +12 -0
  439. package/tsconfig.json +6 -2
  440. package/types/Components.ts +17 -2
  441. package/types/LocatorType.ts +5 -0
  442. package/types/PollTypes.ts +8 -0
  443. package/types/Test.ts +18 -0
  444. package/types/Universal.ts +12 -0
  445. package/types/index.ts +2 -0
  446. package/dist/interfaces/IStoredTestResults.d.ts +0 -5
  447. package/dist/interfaces/IStoredTestResults.js +0 -2
  448. package/dist/interfaces/IStoredTestResults.js.map +0 -1
  449. package/dist/templates/test-artifacts/SubjectPageBase.d.ts +0 -5
  450. package/dist/templates/test-artifacts/SubjectPageBase.js +0 -6
  451. package/dist/templates/test-artifacts/SubjectPageBase.js.map +0 -1
  452. package/dist/templates/test-artifacts/screens/index.d.ts +0 -1
  453. package/dist/templates/test-artifacts/screens/index.js +0 -2
  454. package/dist/templates/test-artifacts/screens/index.js.map +0 -1
  455. package/dist/templates/test-artifacts/subjects/index.d.ts +0 -1
  456. package/dist/templates/test-artifacts/subjects/index.js +0 -2
  457. package/dist/templates/test-artifacts/subjects/index.js.map +0 -1
  458. package/interfaces/IStoredTestResults.ts +0 -6
@@ -0,0 +1,2267 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable @typescript-eslint/naming-convention */
3
+ /** biome-ignore-all lint/suspicious/noAssignInExpressions: reason42 */
4
+ /**
5
+ * Testwise Architecture Diagram
6
+ * Serves a living structured SVG diagram that rescans source files on every refresh.
7
+ * Run via: testwise diagram OR npm run diagram
8
+ */
9
+ import fs from 'node:fs';
10
+ import http from 'node:http';
11
+ import path from 'node:path';
12
+ import process from 'node:process';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const ROOT = path.resolve(__dirname, '..');
17
+
18
+ // ─── Filesystem helpers ───────────────────────────────────────────────────────
19
+
20
+ /** Scan a flat folder for .ts source files; skip index.ts, *.d.ts, *Objects.ts */
21
+ function scanTs(relDir) {
22
+ const dir = path.join(ROOT, relDir);
23
+ if (!fs.existsSync(dir)) return [];
24
+ return fs
25
+ .readdirSync(dir, { withFileTypes: true })
26
+ .filter(
27
+ (e) =>
28
+ e.isFile() &&
29
+ e.name.endsWith('.ts') &&
30
+ !e.name.endsWith('.d.ts') &&
31
+ e.name !== 'index.ts' &&
32
+ !e.name.endsWith('Objects.ts')
33
+ )
34
+ .map((e) => e.name.replace(/\.ts$/, ''));
35
+ }
36
+
37
+ /** Scan components/ subfolders and categorise into subject / screen / overlay */
38
+ function scanComponentFolders() {
39
+ const dir = path.join(ROOT, 'components');
40
+ if (!fs.existsSync(dir)) return { subject: [], screen: [], overlay: [] };
41
+
42
+ const SUBJECT = new Set(['form', 'grid']);
43
+ const OVERLAY = new Set(['pop-up', 'export', 'snackbar', 'topbar', 'menu', 'task']);
44
+
45
+ const subject = [],
46
+ screen = [],
47
+ overlay = [];
48
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
49
+ if (!e.isDirectory()) continue;
50
+ const label = e.name
51
+ .split('-')
52
+ .map((w) => w[0].toUpperCase() + w.slice(1))
53
+ .join('');
54
+ if (SUBJECT.has(e.name)) subject.push(label);
55
+ else if (OVERLAY.has(e.name)) overlay.push(label);
56
+ else screen.push(label);
57
+ }
58
+ return { subject, screen, overlay };
59
+ }
60
+
61
+ // ─── JSDoc parser ─────────────────────────────────────────────────────────────
62
+
63
+ /**
64
+ * Parse JSDoc comments from a TypeScript source file.
65
+ * Returns { summary, members } where members is an array of { name, kind, doc }.
66
+ * - summary: the first top-level file/class/function JSDoc block
67
+ * - members: all exported functions, methods, classes, and constants with their JSDoc
68
+ */
69
+ function parseJsDoc(absPath) {
70
+ if (!fs.existsSync(absPath)) return { summary: '', members: [] };
71
+ const src = fs.readFileSync(absPath, 'utf8');
72
+
73
+ // Strip single-line // comments to avoid false-positive matches
74
+ const stripped = src.replace(/\/\/[^\n]*/g, '');
75
+
76
+ // Extract all /** … */ blocks with the code line that follows them
77
+ const blockRe =
78
+ /\/\*\*([\s\S]*?)\*\/([ \t]*\n[ \t]*(?:export\s+)?((?:async\s+)?(?:class|function|const|interface|type|enum|abstract\s+class)\s+(\w+)|(?:(?:public|protected|private|static|async|readonly|override)\s+)*(\w+)\s*[(<]))/g;
79
+
80
+ const members = [];
81
+ let summary = '';
82
+ let summaryFromExport = false;
83
+ let m;
84
+
85
+ while ((m = blockRe.exec(stripped)) !== null) {
86
+ const rawDoc = m[1];
87
+ const followingLine = m[2].trim();
88
+ const isExported = /^\s*export\s/.test(m[2]);
89
+
90
+ // Clean the JSDoc text: remove leading * on each line, collapse whitespace
91
+ // Strip @example sections (and the code lines that follow) before filtering
92
+ const docLines = rawDoc.split('\n').map((l) => l.replace(/^\s*\*\s?/, '').trim());
93
+ let inExample = false;
94
+ const filteredLines = docLines.filter((l) => {
95
+ if (l.startsWith('@example')) {
96
+ inExample = true;
97
+ return false;
98
+ }
99
+ if (inExample && l.startsWith('@')) inExample = false;
100
+ if (inExample) return false;
101
+ return l && !l.startsWith('@param') && !l.startsWith('@returns') && !l.startsWith('@template');
102
+ });
103
+ const doc = filteredLines.join(' ').replace(/\s+/g, ' ').trim();
104
+
105
+ if (!doc) continue;
106
+
107
+ // Determine the exported name
108
+ const nameMatch =
109
+ followingLine.match(/(?:class|function|const|interface|type|enum|abstract\s+class)\s+(\w+)/) ||
110
+ followingLine.match(/^(?:(?:public|protected|private|static|async|readonly|override)\s+)*(\w+)\s*[(<]/);
111
+ const name = nameMatch ? nameMatch[1] : null;
112
+
113
+ // Determine kind for display
114
+ let kind = 'member';
115
+ if (/\bclass\b/.test(followingLine)) kind = 'class';
116
+ else if (/\bfunction\b/.test(followingLine)) kind = 'function';
117
+ else if (/\bconst\b/.test(followingLine)) kind = 'const';
118
+ else if (/\binterface\b/.test(followingLine)) kind = 'interface';
119
+ else if (/\benum\b/.test(followingLine)) kind = 'enum';
120
+ else if (/\btype\b/.test(followingLine)) kind = 'type';
121
+ else if (/\basync\b/.test(followingLine)) kind = 'method';
122
+ else kind = 'method';
123
+
124
+ // Prefer exported symbols as summary; accept non-exported only if nothing found yet
125
+ if (!summary || (!summaryFromExport && isExported)) {
126
+ if (
127
+ doc.length <= 600 &&
128
+ !doc.startsWith('biome-') &&
129
+ !doc.startsWith('eslint-') &&
130
+ !doc.includes('=>') &&
131
+ !doc.includes('await ') &&
132
+ !doc.includes('Promise<') &&
133
+ !doc.includes('this.')
134
+ ) {
135
+ summary = doc;
136
+ if (isExported) summaryFromExport = true;
137
+ }
138
+ }
139
+
140
+ if (name && name !== 'constructor') {
141
+ // Skip members whose doc looks like code
142
+ if (!doc.includes('=>') && !doc.includes('await ') && !doc.includes('Promise<') && !doc.includes('this.')) {
143
+ members.push({ name, kind, doc });
144
+ }
145
+ }
146
+ }
147
+
148
+ // If no JSDoc found at all, try to get the first block regardless of what follows
149
+ if (!summary) {
150
+ const firstBlock = stripped.match(/\/\*\*([\s\S]*?)\*\//);
151
+ if (firstBlock) {
152
+ const candidate = firstBlock[1]
153
+ .split('\n')
154
+ .map((l) => l.replace(/^\s*\*\s?/, '').trim())
155
+ .filter(Boolean)
156
+ .join(' ')
157
+ .replace(/\s+/g, ' ')
158
+ .trim();
159
+ // Only use it if it looks like a real summary (not a code dump or lint directive)
160
+ if (
161
+ candidate.length <= 600 &&
162
+ !candidate.includes('=>') &&
163
+ !candidate.includes('await ') &&
164
+ !candidate.startsWith('biome-') &&
165
+ !candidate.startsWith('eslint-')
166
+ ) {
167
+ summary = candidate;
168
+ }
169
+ }
170
+ }
171
+
172
+ return { summary, members };
173
+ }
174
+
175
+ /**
176
+ * Build a tips map for all scanned source files.
177
+ * Keys are bare file names (no extension), values are tooltip HTML strings.
178
+ * Reads JSDoc live from each .ts file on every call (living docs).
179
+ */
180
+ function buildTipsMap(fileMap) {
181
+ // fileMap: { 'relDir/fileName': absPath, ... }
182
+ const tips = {};
183
+ for (const [key, absPath] of Object.entries(fileMap)) {
184
+ const name = key.split('/').pop();
185
+ const { summary, members } = parseJsDoc(absPath);
186
+ if (!summary && members.length === 0) continue;
187
+
188
+ let text = summary || '';
189
+ if (members.length > 0) {
190
+ const lines = members.map((mem) => {
191
+ const kindLabel = mem.kind === 'method' || mem.kind === 'function' ? `${mem.name}()` : mem.name;
192
+ const doc = mem.doc.length > 120 ? `${mem.doc.slice(0, 117)}…` : mem.doc;
193
+ return `• ${kindLabel} — ${doc}`;
194
+ });
195
+ text += (text ? '\n\n' : '') + lines.join('\n');
196
+ }
197
+ tips[name] = text;
198
+ }
199
+ return tips;
200
+ }
201
+
202
+ /** Scan a directory recursively for all .ts files (not .d.ts, not node_modules/dist) */
203
+ function scanAllTsFiles() {
204
+ const SKIP_DIRS = new Set(['node_modules', 'dist', 'coverage', '.git', 'test-artifacts', 'seed-data', 'logs']);
205
+ const results = [];
206
+ function walk(dir) {
207
+ if (!fs.existsSync(dir)) return;
208
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
209
+ if (SKIP_DIRS.has(e.name)) continue;
210
+ const full = path.join(dir, e.name);
211
+ if (e.isDirectory()) walk(full);
212
+ else if (e.isFile() && e.name.endsWith('.ts') && !e.name.endsWith('.d.ts')) {
213
+ results.push(full);
214
+ }
215
+ }
216
+ }
217
+ walk(ROOT);
218
+ return results;
219
+ }
220
+
221
+ /**
222
+ * Parse the import statements from a TypeScript file.
223
+ * Returns an array of relative paths that this file imports.
224
+ */
225
+ function parseImports(absPath) {
226
+ try {
227
+ const src = fs.readFileSync(absPath, 'utf8');
228
+ const importRe = /from\s+['"]([^'"]+)['"]/g;
229
+ const imports = [];
230
+ let m;
231
+ while ((m = importRe.exec(src)) !== null) {
232
+ const spec = m[1];
233
+ if (spec.startsWith('.')) imports.push(spec);
234
+ }
235
+ return imports;
236
+ } catch {
237
+ return [];
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Build the full living dependency graph from all .ts files in the project.
243
+ * Returns { nodes, links } where links use actual parsed imports.
244
+ */
245
+ function buildDepGraph() {
246
+ const MOD_COLOR = {
247
+ services: '#9b59b6',
248
+ interfaces: '#5a8abf',
249
+ types: '#9a8a50',
250
+ enums: '#9a8a50',
251
+ 'artifact-builder/helpers': '#c9a558',
252
+ 'artifact-builder': '#d4910a',
253
+ controls: '#c084fc',
254
+ helpers: '#27ae60',
255
+ 'page-overrides': '#c0392b',
256
+ 'page-extensions': '#16a085',
257
+ components: '#4a90d9',
258
+ mcp: '#f97316',
259
+ tests: '#64748b',
260
+ scripts: '#94a3b8',
261
+ root: '#58a6ff'
262
+ };
263
+
264
+ const allFiles = scanAllTsFiles();
265
+ // Map from absolute path → { id, name, group, relPath }
266
+ const byPath = new Map();
267
+ const nodes = [];
268
+
269
+ for (const absFile of allFiles) {
270
+ const rel = path.relative(ROOT, absFile).replace(/\\/g, '/');
271
+ const parts = rel.split('/');
272
+ // Determine group: first folder, or 'root' for top-level files
273
+ let group = parts.length > 1 ? parts[0] : 'root';
274
+ // Sub-group artifact-builder/helpers separately
275
+ if (parts[0] === 'artifact-builder' && parts[1] === 'helpers') group = 'artifact-builder/helpers';
276
+ const name = parts[parts.length - 1].replace(/\.ts$/, '');
277
+ const id = rel.replace(/\.ts$/, '');
278
+ const node = {
279
+ id,
280
+ name,
281
+ group,
282
+ rel,
283
+ absPath: absFile,
284
+ tip: '',
285
+ color: MOD_COLOR[group] || '#8b949e'
286
+ };
287
+ nodes.push(node);
288
+ byPath.set(absFile, node);
289
+ }
290
+
291
+ const links = [];
292
+ const seen = new Set();
293
+
294
+ for (const node of nodes) {
295
+ const imports = parseImports(node.absPath);
296
+ for (const imp of imports) {
297
+ // Resolve the import relative to the importing file's directory
298
+ const impDir = path.dirname(node.absPath);
299
+ // Strip .js extension — TS ESM imports use .js but the actual files are .ts
300
+ const impNoExt = imp.replace(/\.js$/, '');
301
+ const resolved = path.resolve(impDir, impNoExt).replace(/\\/g, '/');
302
+ // Try .ts, then /index.ts
303
+ const candidates = [`${resolved}.ts`, `${resolved}/index.ts`];
304
+ for (const cand of candidates) {
305
+ const target = byPath.get(cand.replace(/\//g, path.sep));
306
+ if (target && target.id !== node.id) {
307
+ const key = `${node.id}->${target.id}`;
308
+ if (!seen.has(key)) {
309
+ seen.add(key);
310
+ links.push({ source: node.id, target: target.id });
311
+ }
312
+ break;
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ return { nodes, links };
319
+ }
320
+
321
+ // ─── HTML page generation ────────────────────────────────────────────────────
322
+
323
+ /** Dep graph computed once at server start (not per request). */
324
+ const _cachedDepGraph = buildDepGraph();
325
+
326
+ function esc(s) {
327
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
328
+ }
329
+
330
+ // ─── HTML page generation ────────────────────────────────────────────────────
331
+
332
+ function generateHtml() {
333
+ const components = scanComponentFolders();
334
+ const controls = scanTs('controls');
335
+ const helpers = scanTs('helpers');
336
+ const pageExts = scanTs('page-extensions').filter((f) => f !== 'SubjectRegistry');
337
+ const pageOvr = scanTs('page-overrides');
338
+ const interfaces = scanTs('interfaces');
339
+ const types = [...scanTs('types'), ...scanTs('enums')];
340
+ const services = scanTs('services');
341
+ const pipeline = scanTs('artifact-builder');
342
+ const pipeHelpers = scanTs('artifact-builder/helpers');
343
+ const scripts =
344
+ scanTs('scripts').length > 0 ? scanTs('scripts') : ['main', 'postinstall', 'setup', 'sync', 'diagram'];
345
+ const mcpFiles = scanTs('mcp').length > 0 ? scanTs('mcp') : ['ArtifactResolver'];
346
+ const generatedAt = new Date().toLocaleString();
347
+ const ROOT_ESC = ROOT.replace(/\\/g, '/');
348
+
349
+ // TIPS are used only for data-tip attributes on arch card items.
350
+ // They are also served via /api/data for client-side use.
351
+ // Build them here so card HTML can include them.
352
+ function absPath(relDir, name) {
353
+ return path.join(ROOT, relDir, `${name}.ts`);
354
+ }
355
+ const fileMap = {};
356
+ const addGroup = (relDir, list) => {
357
+ for (const name of list) fileMap[`${relDir}/${name}`] = absPath(relDir, name);
358
+ };
359
+ addGroup('services', services);
360
+ addGroup('interfaces', interfaces);
361
+ addGroup('types', scanTs('types'));
362
+ addGroup('enums', scanTs('enums'));
363
+ addGroup('artifact-builder', pipeline);
364
+ addGroup('artifact-builder/helpers', pipeHelpers);
365
+ addGroup('controls', controls);
366
+ addGroup('helpers', helpers);
367
+ addGroup('page-overrides', pageOvr);
368
+ addGroup('page-extensions', [...pageExts, 'SubjectRegistry']);
369
+ addGroup('mcp', mcpFiles);
370
+ // Scripts are .js not .ts — add them separately with correct extension
371
+ for (const name of scripts) {
372
+ const jsPath = path.join(ROOT, 'scripts', `${name}.js`);
373
+ if (fs.existsSync(jsPath)) fileMap[`scripts/${name}`] = jsPath;
374
+ }
375
+ const compDir = path.join(ROOT, 'components');
376
+ if (fs.existsSync(compDir)) {
377
+ for (const e of fs.readdirSync(compDir, { withFileTypes: true })) {
378
+ if (!e.isDirectory()) continue;
379
+ // Add ALL non-Objects, non-index .ts files in each component folder
380
+ const folderDir = path.join(compDir, e.name);
381
+ for (const f of fs.readdirSync(folderDir)) {
382
+ if (!f.endsWith('.ts') || f.endsWith('.d.ts') || f.endsWith('Objects.ts') || f === 'index.ts') continue;
383
+ const name = f.replace(/\.ts$/, '');
384
+ fileMap[`components/${name}`] = path.join(folderDir, f);
385
+ }
386
+ }
387
+ }
388
+ const TIPS = buildTipsMap(fileMap);
389
+
390
+ const items = (list, color = '#8b949e') =>
391
+ list
392
+ .map((x) => {
393
+ const tip = TIPS[String(x)] || '';
394
+ const tipAttr = tip ? ` data-tip="${esc(tip)}"` : '';
395
+ return `<div class="item" style="color:${color}"${tipAttr}>${esc(String(x))}</div>`;
396
+ })
397
+ .join('');
398
+
399
+ // fileItems variant that resolves across multiple candidate dirs (first match wins)
400
+ const fileItemsAny = (list, dirs, color = '#8b949e') =>
401
+ list
402
+ .map((name) => {
403
+ const tip = TIPS[name] || '';
404
+ const tipAttr = tip ? ` data-tip="${esc(tip)}"` : '';
405
+ // Find first dir where the file actually exists
406
+ const relDir = dirs.find((d) => fs.existsSync(path.join(ROOT, d, `${name}.ts`))) || dirs[0];
407
+ const p = `${ROOT_ESC}/${relDir}/${name}.ts`;
408
+ return `<div class="item file-item" style="color:${color}" data-path="${p}"${tipAttr}>${esc(name)}</div>`;
409
+ })
410
+ .join('');
411
+
412
+ // Component items — files live in components/<kebab-name>/<PascalName>.ts
413
+ const toKebab = (s) => s.replace(/([A-Z])/g, (_m, c, i) => (i > 0 ? `-${c.toLowerCase()}` : c.toLowerCase()));
414
+ const compItems = (list, color = '#8b949e') =>
415
+ list
416
+ .flatMap((folderLabel) => {
417
+ const folderName = toKebab(folderLabel);
418
+ const folderDir = path.join(ROOT, 'components', folderName);
419
+ let files = [];
420
+ if (fs.existsSync(folderDir)) {
421
+ files = fs
422
+ .readdirSync(folderDir)
423
+ .filter((f) => f.endsWith('.ts') && !f.endsWith('.d.ts') && !f.endsWith('Objects.ts') && f !== 'index.ts')
424
+ .map((f) => f.replace(/\.ts$/, ''));
425
+ }
426
+ if (files.length === 0) files = [folderLabel];
427
+ return files.map((name) => {
428
+ const tip = TIPS[name] || '';
429
+ const tipAttr = tip ? ` data-tip="${esc(tip)}"` : '';
430
+ const p = `${ROOT_ESC}/components/${folderName}/${name}.ts`;
431
+ return `<div class="item file-item" style="color:${color}" data-path="${p}"${tipAttr}>${esc(name)}</div>`;
432
+ });
433
+ })
434
+ .join('');
435
+
436
+ // Root-level file links (no extension in list, but path includes extension)
437
+ const rootItems = (list, color = '#8b949e') =>
438
+ list
439
+ .map((name) => {
440
+ const tip = TIPS[name] || '';
441
+ const tipAttr = tip ? ` data-tip="${esc(tip)}"` : '';
442
+ const absPath = path.join(ROOT, name);
443
+ if (!fs.existsSync(absPath)) return `<div class="item" style="color:${color}">${esc(name)}</div>`;
444
+ const p = `${ROOT_ESC}/${name}`;
445
+ return `<div class="item file-item" style="color:${color}" data-path="${p}"${tipAttr}>${esc(name)}</div>`;
446
+ })
447
+ .join('');
448
+
449
+ // .js file items — links into the scripts/ folder
450
+ const jsFileItems = (list, relDir, color = '#8b949e') =>
451
+ list
452
+ .map((name) => {
453
+ const tip = TIPS[name] || '';
454
+ const tipAttr = tip ? ` data-tip="${esc(tip)}"` : '';
455
+ const p = `${ROOT_ESC}/${relDir}/${name}.js`;
456
+ return `<div class="item file-item" style="color:${color}" data-path="${p}"${tipAttr}>${esc(`${name}.js`)}</div>`;
457
+ })
458
+ .join('');
459
+
460
+ // Render file items — always clickable, tooltip from JSDoc if available
461
+ const fileItems = (list, relDir, color = '#8b949e') =>
462
+ list
463
+ .map((name) => {
464
+ const tip = TIPS[name] || '';
465
+ const tipAttr = tip ? ` data-tip="${esc(tip)}"` : '';
466
+ const p = `${ROOT_ESC}/${relDir}/${name}.ts`;
467
+ return `<div class="item file-item" style="color:${color}" data-path="${p}"${tipAttr}>${esc(name)}</div>`;
468
+ })
469
+ .join('');
470
+
471
+ // Dashed inner sub-box
472
+ const subbox = (title, accent, content, foot = '') =>
473
+ `<div class="subbox" style="--sa:${accent}">` +
474
+ `<div class="subbox-head">${esc(title)}</div>` +
475
+ `<div class="subbox-body">${content}</div>` +
476
+ (foot ? `<div class="subbox-foot">${esc(foot)}</div>` : '') +
477
+ `</div>`;
478
+
479
+ // Main card (tip = optional tooltip on the card header)
480
+ const card = (id, accent, titleBg, bodyBg, title, body, foot = '', tip = '') =>
481
+ `<div class="card" id="${id}" style="--ca:${accent};--ct:${titleBg};--cb:${bodyBg}">` +
482
+ `<div class="card-head"${tip ? ` data-tip="${esc(tip)}"` : ''}>${esc(title)}</div>` +
483
+ `<div class="card-body">${body}</div>` +
484
+ (foot ? `<div class="card-foot">${esc(foot)}</div>` : '') +
485
+ `</div>`;
486
+
487
+ // ── Data ─────────────────────────────────────────────────────────────────
488
+ const svcList = services.length > 0 ? services : ['ConfigBuilder', 'IndiciumApi.service', 'Logger'];
489
+
490
+ const ifaceList =
491
+ interfaces.length > 0
492
+ ? interfaces
493
+ : [
494
+ 'ISubject',
495
+ 'IProperty',
496
+ 'IRegisteredSubjects',
497
+ 'IStoredTestResults',
498
+ 'IComponentObjects',
499
+ 'IComponentSchemaMeta',
500
+ 'ISeedSubjectDraft'
501
+ ];
502
+
503
+ const typeList = types.length > 0 ? types : ['Components', 'Test', 'ElementTypes', 'ExportFormat', 'LogLevel'];
504
+
505
+ const stepList =
506
+ pipeline.length > 0
507
+ ? pipeline
508
+ : [
509
+ 'ArtifactManager',
510
+ 'ModelDataBuilder',
511
+ 'ModelDataRefiner',
512
+ 'SchemaGenerator',
513
+ 'InterfaceGenerator',
514
+ 'ScreenInterfaceRefiner',
515
+ 'SelectorBuilder',
516
+ 'SubjectClassModelBuilder',
517
+ 'ComponentClassModelBuilder',
518
+ 'SubjectGenerator',
519
+ 'SubjectComponentGenerator',
520
+ 'SubjectRegistration',
521
+ 'TsFileModel',
522
+ 'TsFileSerializer'
523
+ ];
524
+
525
+ const pipeHList =
526
+ pipeHelpers.length > 0 ? pipeHelpers : ['DataRetriever', 'NamingHandler', 'Stopwatch', 'PipelinePaths'];
527
+
528
+ const ctrlList =
529
+ controls.length > 0
530
+ ? controls
531
+ : ['InputFieldControl', 'FormComboBox', 'DateControl', 'LookupDropdown', 'GridColumnControl'];
532
+
533
+ const helpList =
534
+ helpers.length > 0
535
+ ? helpers
536
+ : [
537
+ 'FlightTracker',
538
+ 'GlobalWaitEventHandler',
539
+ 'LoginHelper',
540
+ 'SubjectNavigator',
541
+ 'Ensure',
542
+ 'Poll',
543
+ 'RegexHelper',
544
+ 'PathResolver',
545
+ 'UserSimulationHelper',
546
+ 'ConfigChecker'
547
+ ];
548
+
549
+ const ovrList = pageOvr.length > 0 ? pageOvr : ['ClickOverride', 'FillOverride'];
550
+ const extList =
551
+ pageExts.length > 0
552
+ ? pageExts
553
+ : ['GlobalComponents', 'GoToDeepLink', 'LoginFeatures', 'WaitEventHandler', 'UserSimulation'];
554
+
555
+ // ── Section cards ─────────────────────────────────────────────────────────
556
+ const extCard = card(
557
+ 'sec-ext',
558
+ '#3fb950',
559
+ '#163320',
560
+ '#0d2018',
561
+ '📦 External Dependencies',
562
+ items(
563
+ ['@playwright/test', 'playwright-bdd', 'quicktype-core', 'prettier', 'axios', 'ts-morph', 'chalk'],
564
+ '#7cc97e'
565
+ ),
566
+ '',
567
+ 'Third-party npm packages that Testwise depends on. @playwright/test is the core test runner; quicktype-core generates TypeScript interfaces from JSON schemas.'
568
+ );
569
+
570
+ const cfgCard = card(
571
+ 'sec-cfg',
572
+ '#4a90d9',
573
+ '#152238',
574
+ '#0d1829',
575
+ '⚙ Configuration',
576
+ rootItems(['Testwise.json', 'config.json', '.env'], '#7db3d9'),
577
+ 'provided by consumer project',
578
+ 'Configuration files supplied by the consuming project. Testwise.json holds base-URL and credentials; config.json holds environment-specific overrides; .env provides secrets.'
579
+ );
580
+
581
+ const svcCard = card(
582
+ 'sec-svc',
583
+ '#9b59b6',
584
+ '#261040',
585
+ '#16082a',
586
+ '🔌 Services',
587
+ fileItems(svcList, 'services', '#c39cdf'),
588
+ '',
589
+ 'Service classes used internally by the library: configuration access, API calls, and logging.'
590
+ );
591
+
592
+ const ifaceCard = card(
593
+ 'sec-iface',
594
+ '#5a8abf',
595
+ '#152238',
596
+ '#0d1829',
597
+ '🔷 Interfaces',
598
+ fileItems(ifaceList, 'interfaces', '#7db3d9'),
599
+ '',
600
+ 'TypeScript contracts (interfaces) that define the shape of subjects, properties, and components across the library.'
601
+ );
602
+
603
+ const typeCard = card(
604
+ 'sec-types',
605
+ '#9a8a50',
606
+ '#2e2810',
607
+ '#1c1808',
608
+ '🏷 Types & Enums',
609
+ fileItemsAny(typeList, ['types', 'enums'], '#c9b96e'),
610
+ '',
611
+ 'Shared TypeScript type aliases and enumerations used across components, controls, and the artifact pipeline.'
612
+ );
613
+
614
+ const pipeCard = card(
615
+ 'sec-pipe',
616
+ '#d4910a',
617
+ '#2c1c0a',
618
+ '#1a1108',
619
+ '⚙ Build-Time: Artifact Generation Pipeline',
620
+ `<div class="pipe-layout">` +
621
+ `<div class="pipe-steps">${fileItems(stepList, 'artifact-builder', '#c9a558')}</div>` +
622
+ `<div class="stack" style="gap:10px">` +
623
+ subbox('Builder Helpers', '#d4910a', fileItems(pipeHList, 'artifact-builder/helpers', '#c9a558')) +
624
+ subbox(
625
+ 'Templates',
626
+ '#a07830',
627
+ items(['SubjectRegistry.template.ts'], '#c9a558'),
628
+ 'source-of-truth for generated files'
629
+ ) +
630
+ `</div>` +
631
+ `</div>`,
632
+ 'Input: seed-data/ → Output: test-artifacts/',
633
+ 'Reads the Thinkwise application model via API and generates typed TypeScript subject classes and screen interfaces. Run via the testwise sync CLI command.'
634
+ );
635
+
636
+ const genCard = card(
637
+ 'sec-gen',
638
+ '#c9c258',
639
+ '#28280e',
640
+ '#1a1a08',
641
+ '✨ Generated Artifacts',
642
+ items(['SubjectPageBase', 'screens/*.ts', 'subjects/<Name>/*.ts', 'SubjectRegistry.ts'], '#c9c258'),
643
+ 'test-artifacts/ — never edit directly',
644
+ 'Output of the artifact pipeline. Contains typed subject classes (one per Thinkwise subject), screen interfaces, and the SubjectRegistry. Never edit these files — they are regenerated on every sync.'
645
+ );
646
+
647
+ const scriptsCard = card(
648
+ 'sec-scripts',
649
+ '#94a3b8',
650
+ '#1a1f2e',
651
+ '#10141e',
652
+ '📜 Scripts (scripts/)',
653
+ jsFileItems(scripts, 'scripts', '#94a3b8'),
654
+ 'CLI entry points',
655
+ 'Plain JavaScript CLI scripts. main.js is the testwise CLI entry; sync.js runs the full artifact pipeline; postinstall.js runs after npm install; diagram.js serves this living architecture diagram.'
656
+ );
657
+
658
+ const mcpCard = card(
659
+ 'sec-mcp',
660
+ '#f97316',
661
+ '#2a1400',
662
+ '#180d00',
663
+ '🤖 AI / MCP',
664
+ fileItems(mcpFiles, 'mcp', '#fb923c') +
665
+ items(['@thinkwise/testwise-mcp', 'GitHub Copilot Agents', 'VS Code MCP integration'], '#fb923c'),
666
+ '',
667
+ 'ArtifactResolver exposes generated subjects and components to AI tooling via the Model Context Protocol. The MCP server is consumed by @thinkwise/testwise-mcp and powers GitHub Copilot agent workflows.'
668
+ );
669
+
670
+ const devCard = card(
671
+ 'sec-dev',
672
+ '#64748b',
673
+ '#1a1f28',
674
+ '#10141a',
675
+ '🛠 Dev Tooling',
676
+ items(['ESLint', 'Biome (format/lint)', 'vitest (unit tests)', 'ts-morph', 'prettier'], '#94a3b8') +
677
+ `<div style="margin-top:6px">${items(['tests/ (mirrors src structure)', 'coverage/'], '#64748b')}</div>`,
678
+ 'npm test · npm run format:fix',
679
+ 'Development quality tools. ESLint and Biome handle linting and formatting. Vitest runs unit tests (no browser required). Tests live in tests/ and mirror the source structure.'
680
+ );
681
+
682
+ const fixCard = card(
683
+ 'sec-fix',
684
+ '#4a90d9',
685
+ '#152238',
686
+ '#0d1829',
687
+ '⛓ Fixture Chain (Testwise.ts)',
688
+ items(
689
+ [
690
+ '1. @playwright/test base',
691
+ '2. playwright-bdd',
692
+ '3. ClickOverride',
693
+ '4. FillOverride',
694
+ '5. GlobalComponents',
695
+ '6. GoToDeepLink',
696
+ '7. LoginFeatures',
697
+ '8. UserSimulation',
698
+ '9. WaitEventHandler',
699
+ '10. SubjectRegistry',
700
+ '↓ export: test'
701
+ ],
702
+ '#7db3d9'
703
+ ),
704
+ '',
705
+ 'The ordered chain of Playwright test.extend() calls that compose the final test fixture exported by this library. Each layer adds capabilities to the page object. playwright-bdd adds BDD step support.'
706
+ );
707
+
708
+ const consCard = card(
709
+ 'sec-cons',
710
+ '#58a6ff',
711
+ '#0d1828',
712
+ '#08101a',
713
+ '🧪 Consumer Test Project',
714
+ `<pre class="code-block">${esc("import { test } from '@thinkwise/testwise'\n\ntest('scenario', async ({ page }) => {\n const emp = await\n page.subjects\n .Employee.goTo();\n // use components ...\n});")}</pre>`,
715
+ '',
716
+ 'How a consuming project uses Testwise. Import the test fixture, access generated subjects via page.subjects, and interact with components using typed methods.'
717
+ );
718
+
719
+ const compCard = card(
720
+ 'sec-comp',
721
+ '#4a90d9',
722
+ '#152238',
723
+ '#0d1829',
724
+ '🧩 Components (components/)',
725
+ `<div class="comp-row">` +
726
+ subbox('Subject Components', '#4a90d9', compItems(components.subject, '#7db3d9'), 'typed field/col access') +
727
+ subbox('Screen Components', '#4a90d9', `<div class="two-col">${compItems(components.screen, '#8b949e')}</div>`) +
728
+ subbox('Overlay / Global', '#5a8abf', `<div class="two-col">${compItems(components.overlay, '#8b949e')}</div>`) +
729
+ `</div>`,
730
+ 'Each: <Name>.ts (public API) + <Name>Objects.ts (locators) · extends BaseComponentObjects',
731
+ 'Typed wrappers around every Thinkwise UI control. Subject Components (Form, Grid) expose typed field and column accessors. Screen Components handle navigation and layout elements. Overlay/Global components handle pop-ups, menus, and toasts.'
732
+ );
733
+
734
+ const ctrlCard = card(
735
+ 'sec-ctrl',
736
+ '#9b59b6',
737
+ '#221042',
738
+ '#14082a',
739
+ '🎛 Controls (controls/)',
740
+ fileItems(ctrlList, 'controls', '#c39cdf'),
741
+ 'Proxy: Locator + typed methods',
742
+ 'Fine-grained input helpers. Each wraps a specific Thinkwise form field type and exposes typed methods. Acts as both a Playwright Locator and a typed control via Proxy.'
743
+ );
744
+
745
+ const helpCard = card(
746
+ 'sec-help',
747
+ '#27ae60',
748
+ '#102810',
749
+ '#081808',
750
+ '🛠 Helpers (helpers/)',
751
+ fileItems(helpList, 'helpers', '#7cc98a'),
752
+ '',
753
+ 'Standalone utility classes used by test code and page extensions. Handle navigation, waiting, assertions, and user simulation.'
754
+ );
755
+
756
+ const ovrCard = card(
757
+ 'sec-ovr',
758
+ '#c0392b',
759
+ '#2e1010',
760
+ '#1a0808',
761
+ '🔧 Page Overrides',
762
+ fileItems(ovrList, 'page-overrides', '#e07070'),
763
+ 'patch Playwright timing for Thinkwise',
764
+ "Low-level patches that override Playwright's native click() and fill() to inject the extra waits that Thinkwise's event-driven UI requires."
765
+ );
766
+
767
+ const extPageCard = card(
768
+ 'sec-pext',
769
+ '#16a085',
770
+ '#102828',
771
+ '#081818',
772
+ '🔌 Page Extensions',
773
+ fileItems(extList, 'page-extensions', '#60c0b0'),
774
+ 'extend page fixture with typed methods',
775
+ "Each class calls test.extend() to bolt a new property or method onto Playwright's page fixture — e.g. page.login(), page.subjects, page.topbar."
776
+ );
777
+
778
+ // ── Full HTML ─────────────────────────────────────────────────────────────
779
+ return `<!DOCTYPE html>
780
+ <html lang="en">
781
+ <head>
782
+ <meta charset="UTF-8"/>
783
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
784
+ <title>Testwise Architecture</title>
785
+ <script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
786
+ <style>
787
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
788
+
789
+ body {
790
+ background: #0d1117;
791
+ color: #e6edf3;
792
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
793
+ overflow: hidden;
794
+ height: 100vh;
795
+ display: flex;
796
+ flex-direction: column;
797
+ }
798
+
799
+ /* ── Header ── */
800
+ header {
801
+ padding: 8px 16px;
802
+ background: #161b22;
803
+ border-bottom: 1px solid #30363d;
804
+ display: flex;
805
+ align-items: center;
806
+ gap: 12px;
807
+ flex-shrink: 0;
808
+ }
809
+ header h1 { font-size: 0.88rem; font-weight: 700; color: #58a6ff; }
810
+ .meta { font-size: 0.68rem; color: #8b949e; }
811
+ .hbtn {
812
+ padding: 4px 12px;
813
+ border: none;
814
+ border-radius: 5px;
815
+ color: #fff;
816
+ font-size: 0.75rem;
817
+ cursor: pointer;
818
+ transition: filter 0.15s;
819
+ }
820
+ .hbtn:hover { filter: brightness(1.15); }
821
+ .hbtn:disabled { opacity: 0.5; cursor: wait; }
822
+ #export-btn { background: #1f6feb; }
823
+ #reload-btn { background: #238636; }
824
+ #recenter-btn { background: #6e40c9; display: none; }
825
+ #clear-sel-btn { background: #6e40c9; display: none; }
826
+
827
+ /* ── Viewport / zoom ── */
828
+ #viewport {
829
+ flex: 1;
830
+ overflow: hidden;
831
+ position: relative;
832
+ cursor: grab;
833
+ }
834
+ #viewport.dragging { cursor: grabbing; }
835
+
836
+ #diagram {
837
+ position: absolute;
838
+ top: 0; left: 0;
839
+ transform-origin: 0 0;
840
+ padding: 40px 36px;
841
+ display: flex;
842
+ flex-direction: column;
843
+ gap: 32px;
844
+ }
845
+
846
+ .top-row, .bot-row {
847
+ display: flex;
848
+ flex-direction: row;
849
+ gap: 28px;
850
+ align-items: flex-start;
851
+ }
852
+
853
+ .stack {
854
+ display: flex;
855
+ flex-direction: column;
856
+ gap: 24px;
857
+ flex-shrink: 0;
858
+ }
859
+
860
+ /* ── Cards ── */
861
+ .card {
862
+ background: var(--cb, #161b22);
863
+ border: 1.5px solid var(--ca, #30363d);
864
+ border-radius: 7px;
865
+ display: flex;
866
+ flex-direction: column;
867
+ flex-shrink: 0;
868
+ min-width: 150px;
869
+ position: relative;
870
+ z-index: 1;
871
+ }
872
+
873
+ .card-head {
874
+ background: var(--ct, #1e2a3a);
875
+ color: var(--ca, #e6edf3);
876
+ font-size: 10.5px;
877
+ font-weight: 700;
878
+ padding: 7px 11px;
879
+ border-radius: 5px 5px 0 0;
880
+ border-bottom: 1px solid var(--ca);
881
+ white-space: nowrap;
882
+ }
883
+
884
+ .card-body {
885
+ padding: 9px 11px;
886
+ flex: 1;
887
+ }
888
+
889
+ .card-foot {
890
+ font-size: 9px;
891
+ color: #6e8098;
892
+ font-style: italic;
893
+ padding: 5px 11px 8px;
894
+ border-top: 1px solid rgba(255,255,255,0.06);
895
+ }
896
+
897
+ /* ── Items ── */
898
+ .item {
899
+ font-family: ui-monospace, 'Cascadia Code', Consolas, monospace;
900
+ font-size: 10.5px;
901
+ padding: 2px 0;
902
+ white-space: nowrap;
903
+ }
904
+
905
+ /* ── Sub-boxes ── */
906
+ .subbox {
907
+ border: 1px dashed var(--sa, #555);
908
+ border-radius: 5px;
909
+ margin: 4px 0;
910
+ }
911
+ .subbox-head {
912
+ font-size: 9.5px;
913
+ font-weight: 600;
914
+ color: var(--sa);
915
+ padding: 4px 8px;
916
+ border-bottom: 1px dashed var(--sa);
917
+ }
918
+ .subbox-body { padding: 6px 8px; }
919
+ .subbox-foot {
920
+ font-size: 9px;
921
+ color: #6e8098;
922
+ font-style: italic;
923
+ padding: 3px 8px 6px;
924
+ border-top: 1px dashed rgba(255,255,255,0.07);
925
+ }
926
+
927
+ /* ── Pipeline ── */
928
+ .pipe-layout {
929
+ display: flex;
930
+ gap: 16px;
931
+ align-items: flex-start;
932
+ }
933
+ .pipe-steps {
934
+ display: grid;
935
+ grid-template-columns: 1fr 1fr;
936
+ column-gap: 20px;
937
+ flex-shrink: 0;
938
+ }
939
+
940
+ /* ── Components ── */
941
+ .comp-row {
942
+ display: flex;
943
+ gap: 14px;
944
+ align-items: flex-start;
945
+ }
946
+ .comp-row .subbox { flex-shrink: 0; }
947
+ .two-col {
948
+ display: grid;
949
+ grid-template-columns: 1fr 1fr;
950
+ column-gap: 12px;
951
+ }
952
+
953
+ /* ── Code block ── */
954
+ .code-block {
955
+ font-family: ui-monospace, 'Cascadia Code', Consolas, monospace;
956
+ font-size: 10px;
957
+ color: #79c0ff;
958
+ line-height: 1.6;
959
+ white-space: pre;
960
+ }
961
+
962
+ /* ── Tabs ── */
963
+ #tab-bar {
964
+ display: flex;
965
+ gap: 2px;
966
+ background: #161b22;
967
+ border-bottom: 1px solid #30363d;
968
+ padding: 0 12px;
969
+ flex-shrink: 0;
970
+ }
971
+ .tab-btn {
972
+ padding: 6px 16px;
973
+ border: none;
974
+ background: transparent;
975
+ color: #8b949e;
976
+ font-size: 0.78rem;
977
+ cursor: pointer;
978
+ border-bottom: 2px solid transparent;
979
+ margin-bottom: -1px;
980
+ transition: color 0.15s, border-color 0.15s;
981
+ }
982
+ .tab-btn:hover { color: #e6edf3; }
983
+ .tab-btn.active { color: #58a6ff; border-bottom-color: #58a6ff; }
984
+
985
+ .tab-panel { display: none; flex: 1; overflow: hidden; position: relative; }
986
+ .tab-panel.active { display: flex; flex-direction: column; }
987
+
988
+ /* ── Tooltip ── */
989
+ #tw-tooltip {
990
+ position: fixed;
991
+ z-index: 9999;
992
+ pointer-events: none;
993
+ background: #1c2333;
994
+ border: 1px solid #388bfd;
995
+ border-radius: 6px;
996
+ padding: 8px 12px;
997
+ max-width: 520px;
998
+ font-size: 11px;
999
+ line-height: 1.55;
1000
+ color: #e6edf3;
1001
+ box-shadow: 0 4px 18px rgba(0,0,0,0.5);
1002
+ display: none;
1003
+ white-space: pre-wrap;
1004
+ }
1005
+
1006
+ /* ── File item hover ── */
1007
+ .file-item { cursor: pointer; border-radius: 3px; padding: 2px 3px; margin: 0 -3px; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 3px; }
1008
+ .file-item:hover { background: rgba(88,166,255,0.12); text-decoration-style: solid; }
1009
+ .card-head[data-tip] { cursor: help; }
1010
+ .card-head[data-tip]:hover { filter: brightness(1.12); }
1011
+
1012
+ /* ── D3 graph ── */
1013
+ #deps-panel {
1014
+ width: 0; flex-shrink: 0; overflow: hidden;
1015
+ background: #161b22; border-left: 1px solid #30363d;
1016
+ padding: 0; display: flex; flex-direction: column; gap: 6px;
1017
+ font-size: 11px; line-height: 1.55; color: #e6edf3;
1018
+ opacity: 0; pointer-events: none;
1019
+ transition: opacity 0.2s ease, width 0.2s ease, padding 0.2s ease;
1020
+ }
1021
+ #deps-panel.visible { width: 240px; padding: 14px 12px; padding-top: 18px; opacity: 1; pointer-events: auto; overflow-y: auto; }
1022
+ #deps-panel .dp-name { font-weight: 700; font-size: 13px; color: #58a6ff; margin-bottom: 4px; }
1023
+ #deps-panel .dp-meta { color: #8b949e; font-size: 10px; margin-bottom: 8px; }
1024
+ #deps-panel .dp-jsdoc { color: #c9d1d9; white-space: pre-wrap; margin-bottom: 8px; }
1025
+ #deps-panel .dp-connected { color: #8b949e; white-space: pre-wrap; font-size: 10px; }
1026
+ #deps-layout { display: flex; flex: 1; overflow: hidden; }
1027
+ #deps-legend {
1028
+ width: 180px; flex-shrink: 0;
1029
+ background: #161b22; border-right: 1px solid #30363d;
1030
+ padding: 0; overflow: hidden;
1031
+ display: flex; flex-direction: column;
1032
+ }
1033
+ #deps-legend .leg-title {
1034
+ font-size: 10px; font-weight: 600; color: #8b949e;
1035
+ text-transform: uppercase; letter-spacing: 0.06em;
1036
+ padding: 8px 10px 6px; flex-shrink: 0;
1037
+ border-bottom: 1px solid #30363d;
1038
+ }
1039
+ #deps-legend-rows { padding: 6px 10px; display: flex; flex-direction: column; gap: 2px; overflow-y: auto; flex: 1; min-height: 0; }
1040
+ #deps-legend .leg-row {
1041
+ display: flex; align-items: center; gap: 8px;
1042
+ padding: 5px 6px; border-radius: 5px;
1043
+ cursor: pointer; user-select: none;
1044
+ }
1045
+ #deps-legend .leg-row:hover { background: rgba(255,255,255,0.07); }
1046
+ #deps-legend .leg-row:hover .leg-label { color: #ffffff; }
1047
+ #deps-legend .leg-row.leg-selected { background: rgba(255,255,255,0.12); }
1048
+ #deps-legend .leg-row.leg-selected .leg-label { color: #ffffff; font-weight: 700; }
1049
+ #deps-panel .dp-open-btn { margin-top: 12px; padding: 6px 10px; border: 1px solid #388bfd; background: transparent; color: #58a6ff; border-radius: 5px; font-size: 11px; cursor: pointer; width: 100%; }
1050
+ #deps-panel .dp-open-btn:hover { background: rgba(56,139,253,0.15); }
1051
+ #deps-legend .leg-dot {
1052
+ width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0;
1053
+ border: 1.5px solid transparent;
1054
+ }
1055
+ #deps-legend .leg-label { font-size: 13px; color: #c9d1d9; font-family: system-ui; }
1056
+ #deps-search-wrap {
1057
+ padding: 10px 12px; flex-shrink: 0;
1058
+ border-bottom: 1px solid #30363d;
1059
+ }
1060
+ #deps-search {
1061
+ width: 100%; padding: 6px 8px; border-radius: 5px;
1062
+ border: 1px solid #444c56; background: #2d333b; color: #e6edf3;
1063
+ font-size: 11px; outline: none; display: block; box-sizing: border-box;
1064
+ }
1065
+ #deps-search::placeholder { color: #768390; }
1066
+ #deps-search:focus { border-color: #388bfd; background: #353d47; }
1067
+ #deps-search:focus { border-color: #388bfd; }
1068
+ #deps-search-list {
1069
+ display: none; position: absolute; left: 0; right: 0; top: 100%;
1070
+ z-index: 200;
1071
+ background: #161b22; border: 1px solid #388bfd; border-radius: 5px;
1072
+ max-height: 220px; overflow-y: auto;
1073
+ box-shadow: 0 4px 16px rgba(0,0,0,0.5);
1074
+ }
1075
+ #deps-search-list .sl-item {
1076
+ padding: 6px 10px; font-size: 11px; font-family: ui-monospace, monospace;
1077
+ cursor: pointer; color: #e6edf3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1078
+ }
1079
+ #deps-search-list .sl-item:hover, #deps-search-list .sl-item.sl-active { background: rgba(56,139,253,0.2); }
1080
+ #deps-search-list .sl-item .sl-group { font-size: 9px; color: #8b949e; margin-left: 6px; }
1081
+ #deps-viewport { flex: 1; min-width: 0; position: relative; overflow: hidden; }
1082
+ #deps-viewport.dragging { cursor: grabbing; }
1083
+ #deps-svg { width: 100%; height: 100%; opacity: 0; transition: opacity 0.4s ease; }
1084
+ #deps-svg.settled { opacity: 1; }
1085
+ #deps-loading {
1086
+ position: absolute; inset: 0; display: flex; flex-direction: column;
1087
+ align-items: center; justify-content: center; gap: 12px;
1088
+ color: #8b949e; font-size: 0.8rem; pointer-events: none;
1089
+ }
1090
+ #deps-loading.hidden { display: none; }
1091
+ .deps-spinner {
1092
+ width: 32px; height: 32px; border-radius: 50%;
1093
+ border: 3px solid #30363d; border-top-color: #58a6ff;
1094
+ animation: spin 0.7s linear infinite;
1095
+ }
1096
+ @keyframes spin { to { transform: rotate(360deg); } }
1097
+ .d3-node circle { stroke-width: 1.5; cursor: pointer; transition: fill 0.25s, stroke-width 0.25s, opacity 0.25s; }
1098
+ .d3-node circle:hover { stroke-width: 3; filter: brightness(1.3); }
1099
+ .d3-node text { font-size: 9.5px; fill: #e6edf3; pointer-events: none; font-family: ui-monospace, monospace; transition: opacity 0.25s; }
1100
+ .d3-link { stroke-opacity: 0.45; fill: none; }
1101
+ </style>
1102
+ </head>
1103
+ <body>
1104
+ <header>
1105
+ <h1>⬡ Testwise Architecture</h1>
1106
+ <div class="meta">Generated ${esc(generatedAt)} &nbsp;·&nbsp; Refresh to update from source</div>
1107
+ </header>
1108
+
1109
+ <div id="tab-bar">
1110
+ <button class="tab-btn active" onclick="switchTab('arch')">Architecture</button>
1111
+ <button class="tab-btn" onclick="switchTab('deps')">Dependencies</button>
1112
+ <div style="margin-left:auto;display:flex;gap:8px;align-items:center;padding:10px 8px 10px 0">
1113
+ <button id="recenter-btn" class="hbtn" onclick="recenter()">⊙ Recenter</button>
1114
+ <button id="clear-sel-btn" class="hbtn" onclick="clearSelection()">✕ Clear Selection</button>
1115
+ <button id="export-btn" class="hbtn" onclick="exportPng()">⬇ Export PNG</button>
1116
+ <button id="reload-btn" class="hbtn" onclick="location.reload()">↺ Refresh</button>
1117
+ </div>
1118
+ </div>
1119
+
1120
+ <!-- Architecture tab -->
1121
+ <div id="tab-arch" class="tab-panel active">
1122
+ <div id="viewport">
1123
+ <div id="diagram">
1124
+ <div class="top-row">
1125
+ <div class="stack">
1126
+ ${extCard}
1127
+ ${cfgCard}
1128
+ ${svcCard}
1129
+ </div>
1130
+ <div class="stack">
1131
+ ${ifaceCard}
1132
+ ${typeCard}
1133
+ </div>
1134
+ ${pipeCard}
1135
+ ${genCard}
1136
+ ${fixCard}
1137
+ ${consCard}
1138
+ <div class="stack">
1139
+ ${scriptsCard}
1140
+ ${mcpCard}
1141
+ </div>
1142
+ </div>
1143
+ <div class="bot-row">
1144
+ ${compCard}
1145
+ ${ctrlCard}
1146
+ ${helpCard}
1147
+ <div class="stack">
1148
+ ${ovrCard}
1149
+ ${extPageCard}
1150
+ </div>
1151
+ ${devCard}
1152
+ </div>
1153
+ </div>
1154
+ </div>
1155
+ </div>
1156
+
1157
+ <!-- Dependencies tab -->
1158
+ <div id="tab-deps" class="tab-panel">
1159
+ <div id="deps-layout">
1160
+ <div id="deps-legend">
1161
+ <div id="deps-search-wrap">
1162
+ <div style="position:relative">
1163
+ <input id="deps-search" type="text" placeholder="Search nodes..." autocomplete="new-password" spellcheck="false" autocorrect="off" autocapitalize="off" data-form-type="other"/>
1164
+ <div id="deps-search-list"></div>
1165
+ </div>
1166
+ </div>
1167
+ <div class="leg-title">Groups</div>
1168
+ <div id="deps-legend-rows"></div>
1169
+ </div>
1170
+ <div id="deps-viewport">
1171
+ <svg id="deps-svg"></svg>
1172
+ <div id="deps-loading"><div class="deps-spinner"></div><span>Settling layout\u2026</span></div>
1173
+ </div>
1174
+ <div id="deps-panel"></div>
1175
+ </div>
1176
+ </div>
1177
+
1178
+ <!-- Shared tooltip -->
1179
+ <div id="tw-tooltip"></div>
1180
+
1181
+ <script src="https://d3js.org/d3.v7.min.js"></script>
1182
+ <script>
1183
+ // ── Shared data (loaded async from /api/data to avoid any escaping issues) ──
1184
+ let ROOT_PATH = '';
1185
+ let TIPS = {};
1186
+ let GRAPH_NODES = [];
1187
+ let GRAPH_LINKS = [];
1188
+
1189
+ let selectedNode = null;
1190
+ let selectedLegendGroup = null;
1191
+ let selectedLegendRow = null;
1192
+ let archIsCentered = true;
1193
+ let depsIsCentered = false;
1194
+ let _applyNodeHighlight = null;
1195
+ let _legColors = {};
1196
+
1197
+ function updateButtons() {
1198
+ const archActive = document.getElementById('tab-arch').classList.contains('active');
1199
+ const recenterBtn = document.getElementById('recenter-btn');
1200
+ const clearSelBtn = document.getElementById('clear-sel-btn');
1201
+ if (archActive) {
1202
+ recenterBtn.style.display = archIsCentered ? 'none' : 'inline-block';
1203
+ clearSelBtn.style.display = 'none';
1204
+ } else {
1205
+ if (selectedNode || selectedLegendGroup) {
1206
+ recenterBtn.style.display = 'none';
1207
+ clearSelBtn.style.display = 'inline-block';
1208
+ } else {
1209
+ recenterBtn.style.display = depsIsCentered ? 'none' : 'inline-block';
1210
+ clearSelBtn.style.display = 'none';
1211
+ }
1212
+ }
1213
+ }
1214
+
1215
+ function openInVscode(filePath) {
1216
+ window.location.href = 'vscode://file/' + filePath.replace(/\\\\/g, '/');
1217
+ }
1218
+
1219
+ function clearSelection() {
1220
+ selectedNode = null;
1221
+ if (selectedLegendRow) {
1222
+ selectedLegendRow.classList.remove('leg-selected');
1223
+ selectedLegendRow.querySelector('.leg-dot').style.background = _legColors[selectedLegendGroup] + '55';
1224
+ selectedLegendGroup = null;
1225
+ selectedLegendRow = null;
1226
+ }
1227
+ if (_applyNodeHighlight) _applyNodeHighlight(null);
1228
+ document.getElementById('deps-panel').classList.remove('visible');
1229
+ recenter();
1230
+ updateButtons();
1231
+ }
1232
+
1233
+ fetch('/api/data')
1234
+ .then(r => r.json())
1235
+ .then(data => {
1236
+ ROOT_PATH = data.rootPath;
1237
+ TIPS = data.tips;
1238
+ GRAPH_NODES = data.graphNodes;
1239
+ GRAPH_LINKS = data.graphLinks;
1240
+ })
1241
+ .catch(err => console.error('Failed to load diagram data:', err));
1242
+
1243
+ // ── Tab switching ─────────────────────────────────────────────────────
1244
+ function switchTab(name) {
1245
+ document.querySelectorAll('.tab-btn').forEach((b, i) => {
1246
+ const id = ['arch','deps'][i];
1247
+ b.classList.toggle('active', id === name);
1248
+ });
1249
+ document.querySelectorAll('.tab-panel').forEach(p => {
1250
+ p.classList.toggle('active', p.id === 'tab-' + name);
1251
+ });
1252
+ localStorage.setItem('tw-active-tab', name);
1253
+ updateButtons();
1254
+ if (name === 'arch') {
1255
+ // fitToScreen recalculates scale and calls drawArrows — more reliable than rAF alone
1256
+ requestAnimationFrame(() => requestAnimationFrame(fitToScreen));
1257
+ } else if (name === 'deps') {
1258
+ // Data may still be loading; wait for it then init
1259
+ if (GRAPH_NODES.length > 0) {
1260
+ initDepsGraph();
1261
+ } else {
1262
+ fetch('/api/data').then(r => r.json()).then(data => {
1263
+ ROOT_PATH = data.rootPath; TIPS = data.tips;
1264
+ GRAPH_NODES = data.graphNodes; GRAPH_LINKS = data.graphLinks;
1265
+ initDepsGraph();
1266
+ });
1267
+ }
1268
+ }
1269
+ }
1270
+
1271
+ // Restore last active tab on load
1272
+ const _savedTab = localStorage.getItem('tw-active-tab');
1273
+ if (_savedTab && _savedTab !== 'arch') {
1274
+ switchTab(_savedTab);
1275
+ } else {
1276
+ // Arch tab is active — fit and draw arrows after layout is fully painted
1277
+ requestAnimationFrame(() => requestAnimationFrame(fitToScreen));
1278
+ }
1279
+
1280
+ // ── Tooltip ───────────────────────────────────────────────────────────
1281
+ const tooltip = document.getElementById('tw-tooltip');
1282
+ function showTip(text, e) {
1283
+ tooltip.textContent = text;
1284
+ tooltip.style.display = 'block';
1285
+ moveTip(e);
1286
+ }
1287
+ function moveTip(e) {
1288
+ const pad = 14, margin = 8;
1289
+ const tw = tooltip.offsetWidth, th = tooltip.offsetHeight;
1290
+ const vw = window.innerWidth, vh = window.innerHeight;
1291
+ // Prefer below-right; flip left if not enough room on right; flip above if not enough room below
1292
+ let x = e.clientX + pad;
1293
+ let y = e.clientY + pad;
1294
+ if (x + tw > vw - margin) x = e.clientX - tw - pad;
1295
+ if (y + th > vh - margin) y = e.clientY - th - pad;
1296
+ // Clamp so tooltip never goes off the left or top edge
1297
+ if (x < margin) x = margin;
1298
+ if (y < margin) y = margin;
1299
+ tooltip.style.left = x + 'px';
1300
+ tooltip.style.top = y + 'px';
1301
+ }
1302
+ function hideTip() { tooltip.style.display = 'none'; }
1303
+
1304
+ document.body.addEventListener('mouseover', e => {
1305
+ // Don't interfere with D3 node tooltips in the deps graph
1306
+ if (e.target.closest('#deps-svg')) return;
1307
+ const el = e.target.closest('[data-tip]');
1308
+ if (el) showTip(el.dataset.tip, e);
1309
+ else hideTip();
1310
+ });
1311
+ document.body.addEventListener('mousemove', e => {
1312
+ if (tooltip.style.display !== 'none') moveTip(e);
1313
+ });
1314
+ document.body.addEventListener('mouseout', e => {
1315
+ if (e.target.closest('#deps-svg')) return;
1316
+ if (!e.relatedTarget || !e.relatedTarget.closest('[data-tip]')) hideTip();
1317
+ });
1318
+
1319
+ // ── Click file to open in VS Code ─────────────────────────────────────
1320
+ document.body.addEventListener('click', e => {
1321
+ const el = e.target.closest('.file-item[data-path]');
1322
+ if (!el) return;
1323
+ // normalise: forward-slashes → OS-native via URL encoding
1324
+ const raw = el.dataset.path;
1325
+ const uri = 'vscode://file/' + raw.replace(/\\\\/g, '/');
1326
+ window.location.href = uri;
1327
+ });
1328
+
1329
+ // ── Architecture viewport ─────────────────────────────────────────────
1330
+ const viewport = document.getElementById('viewport');
1331
+ const diagram = document.getElementById('diagram');
1332
+ let scale = 1, tx = 0, ty = 0;
1333
+ let dragging = false, dragStartX = 0, dragStartY = 0, startTx = 0, startTy = 0;
1334
+
1335
+ function applyTransform() {
1336
+ diagram.style.transform = \`translate(\${tx}px,\${ty}px) scale(\${scale})\`;
1337
+ }
1338
+
1339
+ function fitToScreen() {
1340
+ const vw = viewport.clientWidth, vh = viewport.clientHeight;
1341
+ const dw = diagram.offsetWidth, dh = diagram.offsetHeight;
1342
+ scale = Math.min(vw / dw, vh / dh) * 0.92;
1343
+ tx = (vw - dw * scale) / 2;
1344
+ ty = Math.max((vh - dh * scale) / 2, 16);
1345
+ applyTransform();
1346
+ archIsCentered = true;
1347
+ updateButtons();
1348
+ requestAnimationFrame(drawArrows);
1349
+ }
1350
+
1351
+ function recenter() {
1352
+ const archActive = document.getElementById('tab-arch').classList.contains('active');
1353
+ if (archActive) {
1354
+ fitToScreen();
1355
+ } else if (GRAPH_NODES.length > 0 && zoomBehaviour) {
1356
+ // Fit deps graph to view
1357
+ const svgEl = document.getElementById('deps-svg');
1358
+ const W = svgEl.clientWidth || 1200, H = svgEl.clientHeight || 700;
1359
+ const pad = 40;
1360
+ const xs = GRAPH_NODES.map(d => d.x), ys = GRAPH_NODES.map(d => d.y);
1361
+ const minX = Math.min(...xs), maxX = Math.max(...xs);
1362
+ const minY = Math.min(...ys), maxY = Math.max(...ys);
1363
+ const bw = maxX - minX || 1, bh = maxY - minY || 1;
1364
+ const k = Math.min((W - pad * 2) / bw, (H - pad * 2) / bh, 2);
1365
+ const tx2 = W / 2 - k * (minX + bw / 2);
1366
+ const ty2 = H / 2 - k * (minY + bh / 2);
1367
+ d3.select('#deps-svg').transition().duration(500)
1368
+ .call(zoomBehaviour.transform, d3.zoomIdentity.translate(tx2, ty2).scale(k));
1369
+ depsIsCentered = true;
1370
+ updateButtons();
1371
+ }
1372
+ }
1373
+
1374
+ window.addEventListener('load', fitToScreen);
1375
+ window.addEventListener('resize', () => {
1376
+ if (document.getElementById('tab-arch').classList.contains('active')) fitToScreen();
1377
+ });
1378
+
1379
+ viewport.addEventListener('wheel', (e) => {
1380
+ e.preventDefault();
1381
+ const rect = viewport.getBoundingClientRect();
1382
+ const mx = e.clientX - rect.left, my = e.clientY - rect.top;
1383
+ const delta = e.deltaY > 0 ? 0.92 : 1.08;
1384
+ const ns = Math.min(Math.max(scale * delta, 0.05), 5);
1385
+ tx = mx - (mx - tx) * (ns / scale);
1386
+ ty = my - (my - ty) * (ns / scale);
1387
+ scale = ns;
1388
+ if (archIsCentered) { archIsCentered = false; updateButtons(); }
1389
+ applyTransform();
1390
+ }, { passive: false });
1391
+
1392
+ viewport.addEventListener('mousedown', (e) => {
1393
+ dragging = true;
1394
+ dragStartX = e.clientX; dragStartY = e.clientY;
1395
+ startTx = tx; startTy = ty;
1396
+ viewport.classList.add('dragging');
1397
+ });
1398
+ window.addEventListener('mousemove', (e) => {
1399
+ if (!dragging) return;
1400
+ tx = startTx + (e.clientX - dragStartX);
1401
+ ty = startTy + (e.clientY - dragStartY);
1402
+ if (archIsCentered) { archIsCentered = false; updateButtons(); }
1403
+ applyTransform();
1404
+ });
1405
+ window.addEventListener('mouseup', () => {
1406
+ dragging = false;
1407
+ viewport.classList.remove('dragging');
1408
+ });
1409
+
1410
+ // ── Arrow overlay ──────────────────────────────────────────────────────
1411
+ function drawArrows() {
1412
+ const old = document.getElementById('arrow-overlay');
1413
+ if (old) old.remove();
1414
+
1415
+ const diagramEl = document.getElementById('diagram');
1416
+ const dr = diagramEl.getBoundingClientRect();
1417
+
1418
+ // Convert a card's screen rect → diagram's own coordinate space (pre-transform)
1419
+ function rel(id) {
1420
+ const el = document.getElementById(id);
1421
+ if (!el) return null;
1422
+ const r = el.getBoundingClientRect();
1423
+ const l = (r.left - dr.left) / scale;
1424
+ const t = (r.top - dr.top) / scale;
1425
+ const w = r.width / scale;
1426
+ const h = r.height / scale;
1427
+ return { l, t, r: l + w, b: t + h, cx: l + w / 2, cy: t + h / 2 };
1428
+ }
1429
+
1430
+ // ── Routing corridors ───────────────────────────────────────────────
1431
+ const topRowEl = document.querySelector('.top-row');
1432
+ const botRowEl = document.querySelector('.bot-row');
1433
+ const aboveY = topRowEl ? (topRowEl.getBoundingClientRect().top - dr.top) / scale - 14 : 10;
1434
+ const aboveYHi = aboveY - 14;
1435
+ const belowY = botRowEl ? (botRowEl.getBoundingClientRect().bottom - dr.top) / scale + 18 : 9999;
1436
+ const belowYLo = belowY + 14;
1437
+ // midY: horizontal corridor in the gap between top and bottom rows
1438
+ const midY = (topRowEl && botRowEl)
1439
+ ? ((topRowEl.getBoundingClientRect().bottom + botRowEl.getBoundingClientRect().top) / 2 - dr.top) / scale
1440
+ : 500;
1441
+ const midYLo = midY + 14;
1442
+
1443
+ // gapX12: vertical corridor between left-stack and iface/types-stack
1444
+ // (guaranteed clear because nothing occupies this x between the two stacks)
1445
+ const rSvcEl = rel('sec-svc');
1446
+ const rIfaceEl = rel('sec-iface');
1447
+ const gapX12 = rSvcEl && rIfaceEl ? (rSvcEl.r + rIfaceEl.l) / 2 : 200;
1448
+
1449
+ // rightMarginX: x corridor to the RIGHT of ALL cards (both rows)
1450
+ const everyId = ['sec-ext','sec-cfg','sec-svc','sec-iface','sec-types',
1451
+ 'sec-pipe','sec-gen','sec-fix','sec-cons','sec-scripts','sec-mcp',
1452
+ 'sec-comp','sec-ctrl','sec-help','sec-ovr','sec-pext','sec-dev'];
1453
+ let rmx = 0;
1454
+ for (const id of everyId) { const rr = rel(id); if (rr) rmx = Math.max(rmx, rr.r); }
1455
+ const rightMarginX = rmx + 20; // first right-margin corridor
1456
+ const rightMarginX2 = rmx + 36; // second corridor (offset so two arrows don't overlap)
1457
+
1458
+ // ── Connection table ─────────────────────────────────────────────────
1459
+ // Strategies:
1460
+ // 'lr' – adjacent cards, Z-bend in horizontal flex gap
1461
+ // 'gap12' – exit source right → gapX12 → up to aboveY → right to target cx → down to target top
1462
+ // 'gap12-hi' – same but uses aboveYHi
1463
+ // 'rm' – exit source right → rightMarginX → up to aboveY → left to (target.cx+8) → down to target top
1464
+ // 'rm2' – exit source right → rightMarginX2 → up to aboveYHi → left to target.cx → down to target top
1465
+ //
1466
+ // Safety proofs:
1467
+ // 'lr' – vertical segment sits inside the flex gap; no card is there ✓
1468
+ // 'gap12' – gapX12 < iface.l and > svc.r; nothing occupies that x between y=f.cy ✓
1469
+ // – aboveY is 14px above top-row top; horizontal at aboveY clears all boxes ✓
1470
+ // 'rm'/'rm2' – rightMarginX > ALL card right edges (both rows); vertical is always clear ✓
1471
+ // – aboveY / aboveYHi are above top-row top ✓
1472
+ const CONNS = [
1473
+ ['sec-pipe', 'sec-gen', '#d4910a', 'lr', false],
1474
+ ['sec-gen', 'sec-fix', '#c9c258', 'lr', false],
1475
+ ['sec-fix', 'sec-cons', '#4a90d9', 'lr', false],
1476
+ ['sec-iface', 'sec-pipe', '#5a8abf', 'lr', true ],
1477
+ ['sec-svc', 'sec-pipe', '#9b59b6', 'gap12',true ],
1478
+ ['sec-ext', 'sec-fix', '#3fb950', 'gap12-hi', true ],
1479
+ ['sec-ovr', 'sec-fix', '#c0392b', 'rm', false],
1480
+ ['sec-pext', 'sec-fix', '#16a085', 'rm2', false],
1481
+ ['sec-comp', 'sec-ctrl', '#4a90d9', 'lr', true ],
1482
+ ['sec-help', 'sec-pext', '#27ae60', 'lr', true ],
1483
+ ['sec-scripts', 'sec-mcp', '#f97316', 'tb', false],
1484
+ ];
1485
+
1486
+ const paths = [];
1487
+ for (const [fid, tid, color, strat, dashed] of CONNS) {
1488
+ const f = rel(fid), t = rel(tid);
1489
+ if (!f || !t) continue;
1490
+ let pts;
1491
+ if (strat === 'lr') {
1492
+ const midX = (f.r + t.l) / 2;
1493
+ pts = [[f.r, f.cy], [midX, f.cy], [midX, t.cy], [t.l, t.cy]];
1494
+ } else if (strat === 'tb') {
1495
+ // straight top→bottom between vertically stacked cards
1496
+ pts = [[f.cx, f.b], [f.cx, t.t]];
1497
+ } else if (strat === 'above-tb') {
1498
+ // exit source top, go above top-row, travel right to target cx, drop down
1499
+ pts = [[f.cx, f.t], [f.cx, aboveYHi], [t.cx + 8, aboveYHi], [t.cx + 8, t.t]];
1500
+ } else if (strat === 'bm') {
1501
+ // exit source bottom → below all bottom-row cards → right margin → above top-row → target
1502
+ pts = [[f.cx, f.b], [f.cx, belowY], [rightMarginX, belowY], [rightMarginX, aboveY], [t.cx + 8, aboveY], [t.cx + 8, t.t]];
1503
+ } else if (strat === 'bm2') {
1504
+ pts = [[f.cx, f.b], [f.cx, belowYLo], [rightMarginX2, belowYLo], [rightMarginX2, aboveYHi], [t.cx, aboveYHi], [t.cx, t.t]];
1505
+ } else if (strat === 'gap-row') {
1506
+ // exit source top → inter-row gap → left toward target cx → up into target
1507
+ pts = [[f.cx, f.t], [f.cx, midY], [t.cx + 8, midY], [t.cx + 8, t.b]];
1508
+ } else if (strat === 'gap-row2') {
1509
+ pts = [[f.cx, f.t], [f.cx, midYLo], [t.cx, midYLo], [t.cx, t.b]];
1510
+ } else if (strat === 'gap12') {
1511
+ pts = [[f.r, f.cy], [gapX12, f.cy], [gapX12, aboveY], [t.cx, aboveY], [t.cx, t.t]];
1512
+ } else if (strat === 'gap12-hi') {
1513
+ pts = [[f.r, f.cy], [gapX12, f.cy], [gapX12, aboveYHi], [t.cx, aboveYHi], [t.cx, t.t]];
1514
+ } else if (strat === 'rm') {
1515
+ // Exit bottom → belowY (clear) → rightMarginX (clear) → aboveY (clear) → target
1516
+ pts = [[f.cx, f.b], [f.cx, belowY], [rightMarginX, belowY], [rightMarginX, aboveY], [t.cx + 8, aboveY], [t.cx + 8, t.t]];
1517
+ } else if (strat === 'rm2') {
1518
+ pts = [[f.cx, f.b], [f.cx, belowYLo], [rightMarginX2, belowYLo], [rightMarginX2, aboveYHi], [t.cx, aboveYHi], [t.cx, t.t]];
1519
+ }
1520
+ if (pts) paths.push({ pts, color, label: '', dashed });
1521
+ }
1522
+
1523
+ const NS = 'http://www.w3.org/2000/svg';
1524
+ const svg = document.createElementNS(NS, 'svg');
1525
+ svg.id = 'arrow-overlay';
1526
+ svg.setAttribute('style', 'position:absolute;top:0;left:0;overflow:visible;pointer-events:none;z-index:0;');
1527
+ svg.setAttribute('width', diagramEl.scrollWidth);
1528
+ svg.setAttribute('height', diagramEl.scrollHeight);
1529
+
1530
+ // Define arrowhead markers per colour
1531
+ const defs = document.createElementNS(NS, 'defs');
1532
+ for (const c of [...new Set(paths.map(p => p.color))]) {
1533
+ const mk = document.createElementNS(NS, 'marker');
1534
+ const mid = 'ah' + c.replace(/#/g, '');
1535
+ mk.setAttribute('id', mid);
1536
+ mk.setAttribute('markerWidth', '7');
1537
+ mk.setAttribute('markerHeight', '7');
1538
+ mk.setAttribute('refX', '6');
1539
+ mk.setAttribute('refY', '3.5');
1540
+ mk.setAttribute('orient', 'auto');
1541
+ const poly = document.createElementNS(NS, 'polygon');
1542
+ poly.setAttribute('points', '0 0, 7 3.5, 0 7');
1543
+ poly.setAttribute('fill', c);
1544
+ mk.appendChild(poly);
1545
+ defs.appendChild(mk);
1546
+ }
1547
+ svg.appendChild(defs);
1548
+
1549
+ for (const { pts, color, label, dashed } of paths) {
1550
+ const markId = 'url(#ah' + color.replace(/#/g, '') + ')';
1551
+
1552
+ const path = document.createElementNS(NS, 'path');
1553
+ const d = 'M ' + pts.map(([x, y]) => x.toFixed(1) + ' ' + y.toFixed(1)).join(' L ');
1554
+ path.setAttribute('d', d);
1555
+ path.setAttribute('fill', 'none');
1556
+ path.setAttribute('stroke', color);
1557
+ path.setAttribute('stroke-width', '1.5');
1558
+ path.setAttribute('stroke-opacity', '0.75');
1559
+ if (dashed) path.setAttribute('stroke-dasharray', '5,3');
1560
+ path.setAttribute('marker-end', markId);
1561
+ svg.appendChild(path);
1562
+
1563
+ if (label && pts.length >= 2) {
1564
+ // Place label at the midpoint of the longest segment
1565
+ let bestIdx = 0, bestLen = 0;
1566
+ for (let i = 0; i < pts.length - 1; i++) {
1567
+ const dx = pts[i+1][0] - pts[i][0], dy = pts[i+1][1] - pts[i][1];
1568
+ const len = Math.sqrt(dx*dx + dy*dy);
1569
+ if (len > bestLen) { bestLen = len; bestIdx = i; }
1570
+ }
1571
+ const lx = ((pts[bestIdx][0] + pts[bestIdx+1][0]) / 2).toFixed(1);
1572
+ const ly = ((pts[bestIdx][1] + pts[bestIdx+1][1]) / 2 - 4).toFixed(1);
1573
+ const txt = document.createElementNS(NS, 'text');
1574
+ txt.setAttribute('x', lx);
1575
+ txt.setAttribute('y', ly);
1576
+ txt.setAttribute('fill', color);
1577
+ txt.setAttribute('font-size', '9');
1578
+ txt.setAttribute('text-anchor', 'middle');
1579
+ txt.setAttribute('font-family', 'system-ui, sans-serif');
1580
+ txt.setAttribute('opacity', '0.85');
1581
+ txt.textContent = label;
1582
+ svg.appendChild(txt);
1583
+ }
1584
+ }
1585
+
1586
+ diagramEl.appendChild(svg);
1587
+ }
1588
+
1589
+ async function exportPng() {
1590
+ const btn = document.getElementById('export-btn');
1591
+ btn.disabled = true; btn.textContent = 'Exporting…';
1592
+ try {
1593
+ const archActive = document.getElementById('tab-arch').classList.contains('active');
1594
+ if (archActive) {
1595
+ // Architecture tab — use html2canvas on the diagram element
1596
+ const saved = diagram.style.transform;
1597
+ diagram.style.transform = 'none';
1598
+ await new Promise(r => requestAnimationFrame(r));
1599
+ const canvas = await html2canvas(diagram, {
1600
+ backgroundColor: '#0d1117',
1601
+ scale: 2,
1602
+ useCORS: true,
1603
+ logging: false,
1604
+ });
1605
+ diagram.style.transform = saved;
1606
+ canvas.toBlob((blob) => {
1607
+ const a = document.createElement('a');
1608
+ a.href = URL.createObjectURL(blob);
1609
+ a.download = 'testwise-architecture.png';
1610
+ a.click();
1611
+ URL.revokeObjectURL(a.href);
1612
+ btn.disabled = false; btn.textContent = '⬇ Export PNG';
1613
+ }, 'image/png');
1614
+ } else {
1615
+ // Dependencies tab — serialize the D3 SVG to a canvas
1616
+ const svgEl = document.getElementById('deps-svg');
1617
+ const { width, height } = svgEl.getBoundingClientRect();
1618
+ // Clone and stamp explicit dimensions so the serialized SVG renders at viewport size
1619
+ const clone = svgEl.cloneNode(true);
1620
+ clone.setAttribute('width', width);
1621
+ clone.setAttribute('height', height);
1622
+ clone.setAttribute('overflow', 'hidden');
1623
+ // Embed the font so text renders correctly
1624
+ const style = document.createElement('style');
1625
+ style.textContent = 'text { font-family: system-ui, sans-serif; font-size: 11px; fill: #c9d1d9; }';
1626
+ clone.insertBefore(style, clone.firstChild);
1627
+ const serializer = new XMLSerializer();
1628
+ const svgStr = serializer.serializeToString(clone);
1629
+ const svgBlob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
1630
+ const url = URL.createObjectURL(svgBlob);
1631
+ const img = new Image();
1632
+ img.onload = () => {
1633
+ const canvas = document.createElement('canvas');
1634
+ const scale = 2;
1635
+ canvas.width = width * scale;
1636
+ canvas.height = height * scale;
1637
+ const ctx = canvas.getContext('2d');
1638
+ ctx.fillStyle = '#0d1117';
1639
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1640
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
1641
+ URL.revokeObjectURL(url);
1642
+ canvas.toBlob((blob) => {
1643
+ const a = document.createElement('a');
1644
+ a.href = URL.createObjectURL(blob);
1645
+ a.download = 'testwise-dependencies.png';
1646
+ a.click();
1647
+ URL.revokeObjectURL(a.href);
1648
+ btn.disabled = false; btn.textContent = '⬇ Export PNG';
1649
+ }, 'image/png');
1650
+ };
1651
+ img.onerror = (e) => {
1652
+ URL.revokeObjectURL(url);
1653
+ throw new Error('SVG render failed');
1654
+ };
1655
+ img.src = url;
1656
+ }
1657
+ } catch (e) {
1658
+ alert('Export error: ' + e.message);
1659
+ btn.disabled = false; btn.textContent = '⬇ Export PNG';
1660
+ }
1661
+ }
1662
+
1663
+ // ── D3 Dependency Graph ────────────────────────────────────────────────
1664
+ // Data loaded via fetch('/api/data') above — GRAPH_NODES / GRAPH_LINKS
1665
+ // are populated before initDepsGraph() is called.
1666
+
1667
+ let depsInited = false;
1668
+ let zoomBehaviour = null;
1669
+ function initDepsGraph() {
1670
+ if (depsInited) return;
1671
+ depsInited = true;
1672
+ if (!GRAPH_NODES || GRAPH_NODES.length === 0) { depsInited = false; return; }
1673
+
1674
+ const svgEl = document.getElementById('deps-svg');
1675
+ const W = svgEl.clientWidth || 1200;
1676
+ const H = svgEl.clientHeight || 700;
1677
+
1678
+ // Count in/out degree for sizing nodes
1679
+ const inDeg = new Map(GRAPH_NODES.map(n => [n.id, 0]));
1680
+ const outDeg = new Map(GRAPH_NODES.map(n => [n.id, 0]));
1681
+ for (const l of GRAPH_LINKS) {
1682
+ outDeg.set(l.source, (outDeg.get(l.source) || 0) + 1);
1683
+ inDeg.set(l.target, (inDeg.get(l.target) || 0) + 1);
1684
+ }
1685
+ // Total degree drives node radius
1686
+ const totalDeg = id => (inDeg.get(id) || 0) + (outDeg.get(id) || 0);
1687
+ const nodeR = d => 4 + Math.min(totalDeg(d.id) * 0.9, 18);
1688
+
1689
+ // Build index so d3 can use string ids
1690
+ const nodeById = new Map(GRAPH_NODES.map(n => [n.id, n]));
1691
+
1692
+ const svg = d3.select('#deps-svg');
1693
+ svg.selectAll('*').remove();
1694
+ const g = svg.append('g');
1695
+
1696
+ zoomBehaviour = d3.zoom().scaleExtent([0.02, 6]).on('zoom', e => {
1697
+ g.attr('transform', e.transform);
1698
+ if (e.sourceEvent) { depsIsCentered = false; updateButtons(); }
1699
+ });
1700
+ svg.call(zoomBehaviour);
1701
+
1702
+ // Legend sidebar (HTML)
1703
+ const groups = [...new Set(GRAPH_NODES.map(n => n.group))].sort();
1704
+ const legendEl = document.getElementById('deps-legend-rows');
1705
+ legendEl.innerHTML = '';
1706
+ function applyLegendHighlight(grp) {
1707
+ if (grp) {
1708
+ node.select('circle').transition('hl').duration(150)
1709
+ .attr('fill', n => n.group === grp ? n.color + 'ff' : n.color + '0d')
1710
+ .attr('stroke-width', n => n.group === grp ? 2.5 : 0.5);
1711
+ node.select('text').transition('hl').duration(150)
1712
+ .attr('opacity', n => n.group === grp ? 1 : 0.05);
1713
+ link.transition('hl').duration(150).attr('stroke-opacity', 0).attr('marker-end', 'none');
1714
+ } else {
1715
+ node.select('circle').transition('hl').duration(150)
1716
+ .attr('fill', n => n.color + 'bb').attr('stroke-width', 1.8);
1717
+ node.select('text').transition('hl').duration(150).attr('opacity', 1);
1718
+ }
1719
+ }
1720
+
1721
+ function applyNodeHighlight(d) {
1722
+ if (d) {
1723
+ const neighbours = new Set([d.id]);
1724
+ GRAPH_LINKS.forEach(l => {
1725
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1726
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1727
+ if (sid === d.id) neighbours.add(tid);
1728
+ if (tid === d.id) neighbours.add(sid);
1729
+ });
1730
+ node.select('circle').transition('hl').duration(200)
1731
+ .attr('fill', n => neighbours.has(n.id) ? n.color + 'ee' : n.color + '22')
1732
+ .attr('stroke-width', n => n.id === d.id ? 2.5 : neighbours.has(n.id) ? 2 : 1);
1733
+ node.select('text').transition('hl').duration(200)
1734
+ .attr('opacity', n => neighbours.has(n.id) ? 1 : 0.2);
1735
+ link.transition('hl').duration(200)
1736
+ .attr('stroke-opacity', l => {
1737
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1738
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1739
+ return (sid === d.id || tid === d.id) ? 1 : 0;
1740
+ })
1741
+ .attr('stroke-dasharray', 'none')
1742
+ .attr('stroke-width', l => {
1743
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1744
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1745
+ return (sid === d.id || tid === d.id) ? 2 : 1.5;
1746
+ })
1747
+ .attr('marker-end', l => {
1748
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1749
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1750
+ return (sid === d.id || tid === d.id) ? 'url(#deps-arrow)' : 'none';
1751
+ });
1752
+ } else {
1753
+ node.select('circle').transition('hl').duration(200)
1754
+ .attr('fill', n => n.color + 'bb').attr('stroke-width', 1.8);
1755
+ node.select('text').transition('hl').duration(200).attr('opacity', 1).attr('fill', '#e6edf3').attr('font-weight', '500');
1756
+ link.transition('hl').duration(200).attr('stroke-opacity', 0).attr('stroke-dasharray', 'none').attr('stroke-width', 1.5).attr('marker-end', 'none');
1757
+ }
1758
+ }
1759
+ _applyNodeHighlight = applyNodeHighlight;
1760
+
1761
+ function showNodePanel(d, showOpenBtn = false) {
1762
+ const neighbours = new Set([d.id]);
1763
+ GRAPH_LINKS.forEach(l => {
1764
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1765
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1766
+ if (sid === d.id) neighbours.add(tid);
1767
+ if (tid === d.id) neighbours.add(sid);
1768
+ });
1769
+ const connectedNames = GRAPH_NODES
1770
+ .filter(n => neighbours.has(n.id) && n.id !== d.id)
1771
+ .map(n => n.name).sort();
1772
+ const jsdoc = TIPS[d.name] || '';
1773
+ const safePath = d.path.replace(/\\\\/g, '/');
1774
+ const panel = document.getElementById('deps-panel');
1775
+ panel.innerHTML =
1776
+ \`<div class="dp-name">\${d.name}</div>\` +
1777
+ \`<div class="dp-meta">Imports: \${outDeg.get(d.id)||0} &nbsp;&middot;&nbsp; Imported by: \${inDeg.get(d.id)||0}</div>\` +
1778
+ (jsdoc ? \`<div class="dp-jsdoc">\${jsdoc}</div>\` : '') +
1779
+ \`<div class="dp-connected"><strong>Connected (\${connectedNames.length}):</strong>\\n\${connectedNames.join('\\n') || '\u2014'}</div>\` +
1780
+ (showOpenBtn ? \`<button class="dp-open-btn" onclick="openInVscode('\${safePath}')">&#8599; Open File</button>\` : '');
1781
+ panel.classList.add('visible');
1782
+ }
1783
+
1784
+ function zoomToNodes(nodeList) {
1785
+ const pad = 80;
1786
+ const xs = nodeList.map(d => d.x), ys = nodeList.map(d => d.y);
1787
+ const minX = Math.min(...xs), maxX = Math.max(...xs);
1788
+ const minY = Math.min(...ys), maxY = Math.max(...ys);
1789
+ const bw = maxX - minX || 1, bh = maxY - minY || 1;
1790
+ const k = Math.min((W - pad * 2) / bw, (H - pad * 2) / bh, 4);
1791
+ const tx2 = W / 2 - k * (minX + bw / 2);
1792
+ const ty2 = H / 2 - k * (minY + bh / 2);
1793
+ svg.transition().duration(500)
1794
+ .call(zoomBehaviour.transform, d3.zoomIdentity.translate(tx2, ty2).scale(k));
1795
+ depsIsCentered = false;
1796
+ updateButtons();
1797
+ }
1798
+
1799
+ const legColors = _legColors;
1800
+ groups.forEach(grp => {
1801
+ const color = (nodeById.get(GRAPH_NODES.find(n => n.group === grp)?.id) || {}).color || '#888';
1802
+ legColors[grp] = color;
1803
+ _legColors[grp] = color;
1804
+ const row = document.createElement('div');
1805
+ row.className = 'leg-row';
1806
+ row.innerHTML = \`<span class="leg-dot" style="background:\${color}55;border-color:\${color}"></span><span class="leg-label">\${grp}</span>\`;
1807
+ row.addEventListener('click', () => {
1808
+ if (!document.getElementById('deps-svg').classList.contains('settled')) return;
1809
+ if (selectedLegendGroup === grp) {
1810
+ row.classList.remove('leg-selected');
1811
+ row.querySelector('.leg-dot').style.background = color + '55';
1812
+ selectedLegendGroup = null;
1813
+ selectedLegendRow = null;
1814
+ if (selectedNode) applyNodeHighlight(selectedNode); else applyLegendHighlight(null);
1815
+ } else {
1816
+ if (selectedLegendRow) {
1817
+ selectedLegendRow.classList.remove('leg-selected');
1818
+ selectedLegendRow.querySelector('.leg-dot').style.background = legColors[selectedLegendGroup] + '55';
1819
+ }
1820
+ selectedNode = null;
1821
+ selectedLegendGroup = grp;
1822
+ selectedLegendRow = row;
1823
+ row.classList.add('leg-selected');
1824
+ row.querySelector('.leg-dot').style.background = color + 'cc';
1825
+ applyLegendHighlight(grp);
1826
+ const groupNodes = GRAPH_NODES.filter(n => n.group === grp);
1827
+ if (groupNodes.length) zoomToNodes(groupNodes);
1828
+ }
1829
+ // Close side panel when switching legend selection
1830
+ document.getElementById('deps-panel').classList.remove('visible');
1831
+ updateButtons();
1832
+ });
1833
+ row.addEventListener('mouseover', () => {
1834
+ if (!document.getElementById('deps-svg').classList.contains('settled')) return;
1835
+ if (selectedLegendGroup || selectedNode) return;
1836
+ row.querySelector('.leg-dot').style.background = color + 'cc';
1837
+ applyLegendHighlight(grp);
1838
+ });
1839
+ row.addEventListener('mouseout', () => {
1840
+ if (selectedLegendGroup || selectedNode) return;
1841
+ row.querySelector('.leg-dot').style.background = color + '55';
1842
+ applyLegendHighlight(null);
1843
+ });
1844
+ legendEl.appendChild(row);
1845
+ });
1846
+
1847
+ // ── Search box ────────────────────────────────────────────────────
1848
+ const searchInput = document.getElementById('deps-search');
1849
+ const searchList = document.getElementById('deps-search-list');
1850
+ const sortedNodes = [...GRAPH_NODES].sort((a, b) => a.name.localeCompare(b.name));
1851
+
1852
+ function selectNodeById(id) {
1853
+ const d = GRAPH_NODES.find(n => n.id === id);
1854
+ if (!d) return;
1855
+ // Clear legend selection
1856
+ if (selectedLegendRow) {
1857
+ selectedLegendRow.classList.remove('leg-selected');
1858
+ selectedLegendRow.querySelector('.leg-dot').style.background = _legColors[selectedLegendGroup] + '55';
1859
+ selectedLegendGroup = null;
1860
+ selectedLegendRow = null;
1861
+ }
1862
+ selectedNode = d;
1863
+ applyNodeHighlight(d);
1864
+ showNodePanel(d, true);
1865
+ const neighbours = new Set([d.id]);
1866
+ GRAPH_LINKS.forEach(l => {
1867
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1868
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1869
+ if (sid === d.id) neighbours.add(tid);
1870
+ if (tid === d.id) neighbours.add(sid);
1871
+ });
1872
+ const relatedNodes = GRAPH_NODES.filter(n => neighbours.has(n.id));
1873
+ if (relatedNodes.length) zoomToNodes(relatedNodes);
1874
+ updateButtons();
1875
+ }
1876
+
1877
+ function renderSearchList(q) {
1878
+ const matches = q.length === 0 ? [] :
1879
+ sortedNodes.filter(n => n.name.toLowerCase().includes(q.toLowerCase())).slice(0, 50);
1880
+ if (matches.length === 0) { searchList.style.display = 'none'; return; }
1881
+ searchList.innerHTML = matches.map((n, i) =>
1882
+ \`<div class="sl-item" data-id="\${n.id}" data-idx="\${i}">\${n.name}<span class="sl-group">\${n.group}</span></div>\`
1883
+ ).join('');
1884
+ searchList.style.display = 'block';
1885
+ searchList.querySelectorAll('.sl-item').forEach(el => {
1886
+ el.addEventListener('mousedown', ev => {
1887
+ ev.preventDefault();
1888
+ searchInput.value = '';
1889
+ searchInput.blur();
1890
+ searchList.style.display = 'none';
1891
+ if (document.getElementById('deps-svg').classList.contains('settled')) {
1892
+ selectNodeById(el.dataset.id);
1893
+ }
1894
+ });
1895
+ });
1896
+ }
1897
+
1898
+ searchInput.addEventListener('input', () => renderSearchList(searchInput.value));
1899
+ searchInput.addEventListener('blur', () => setTimeout(() => { searchList.style.display = 'none'; }, 150));
1900
+ searchInput.addEventListener('focus', () => { if (searchInput.value) renderSearchList(searchInput.value); });
1901
+ // Keyboard navigation
1902
+ searchInput.addEventListener('keydown', ev => {
1903
+ const items = searchList.querySelectorAll('.sl-item');
1904
+ const active = searchList.querySelector('.sl-active');
1905
+ if (ev.key === 'ArrowDown') {
1906
+ ev.preventDefault();
1907
+ const next = active ? active.nextElementSibling : items[0];
1908
+ if (active) active.classList.remove('sl-active');
1909
+ if (next) { next.classList.add('sl-active'); next.scrollIntoView({ block: 'nearest' }); }
1910
+ } else if (ev.key === 'ArrowUp') {
1911
+ ev.preventDefault();
1912
+ const prev = active ? active.previousElementSibling : items[items.length - 1];
1913
+ if (active) active.classList.remove('sl-active');
1914
+ if (prev) { prev.classList.add('sl-active'); prev.scrollIntoView({ block: 'nearest' }); }
1915
+ } else if (ev.key === 'Enter') {
1916
+ ev.preventDefault();
1917
+ const target = active || items[0];
1918
+ if (target) {
1919
+ searchInput.value = '';
1920
+ searchInput.blur();
1921
+ searchList.style.display = 'none';
1922
+ if (document.getElementById('deps-svg').classList.contains('settled')) {
1923
+ selectNodeById(target.dataset.id);
1924
+ }
1925
+ }
1926
+ } else if (ev.key === 'Escape') {
1927
+ searchInput.value = '';
1928
+ searchInput.blur();
1929
+ searchList.style.display = 'none';
1930
+ }
1931
+ });
1932
+
1933
+ // Stats label
1934
+ svg.append('text')
1935
+ .attr('x', W - 8).attr('y', 18)
1936
+ .attr('fill', '#6e8098').attr('font-size', '10px').attr('text-anchor', 'end')
1937
+ .attr('font-family', 'system-ui')
1938
+ .text(\`\${GRAPH_NODES.length} nodes · \${GRAPH_LINKS.length} edges\`);
1939
+
1940
+ svg.append('defs').append('marker')
1941
+ .attr('id', 'deps-arrow')
1942
+ .attr('viewBox', '0 -4 8 8').attr('refX', 8).attr('refY', 0)
1943
+ .attr('markerWidth', 5).attr('markerHeight', 5).attr('orient', 'auto')
1944
+ .append('path').attr('d', 'M0,-4L8,0L0,4').attr('fill', '#5a7a9a');
1945
+
1946
+ const link = g.append('g').selectAll('line')
1947
+ .data(GRAPH_LINKS).join('line')
1948
+ .attr('stroke', '#7ca8cc').attr('stroke-width', 1.5)
1949
+ .attr('stroke-opacity', 0);
1950
+
1951
+ const node = g.append('g').selectAll('g')
1952
+ .data(GRAPH_NODES).join('g')
1953
+ .attr('class', 'd3-node');
1954
+
1955
+ // Radius scaled by total degree (imports + imported-by)
1956
+ node.append('circle')
1957
+ .attr('r', d => nodeR(d))
1958
+ .attr('fill', d => d.color + 'bb').attr('stroke', d => d.color).attr('stroke-width', 1.8);
1959
+
1960
+ node.append('text').attr('dx', d => nodeR(d) + 3)
1961
+ .attr('dy', 4).attr('font-weight', '500').text(d => d.name);
1962
+
1963
+ // Node hover / click / selection
1964
+ node.on('mouseover', (ev, d) => {
1965
+ if (!document.getElementById('deps-svg').classList.contains('settled')) return;
1966
+ if (selectedNode) {
1967
+ if (selectedNode.id === d.id) return;
1968
+ // Compute selected node neighbours (keep solid) and hovered neighbours (dotted)
1969
+ const selNeighbours = new Set([selectedNode.id]);
1970
+ const hovNeighbours = new Set([d.id]);
1971
+ GRAPH_LINKS.forEach(l => {
1972
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1973
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1974
+ if (sid === selectedNode.id) selNeighbours.add(tid);
1975
+ if (tid === selectedNode.id) selNeighbours.add(sid);
1976
+ if (sid === d.id) hovNeighbours.add(tid);
1977
+ if (tid === d.id) hovNeighbours.add(sid);
1978
+ });
1979
+ link.transition('hover').duration(100)
1980
+ .attr('stroke-opacity', l => {
1981
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1982
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1983
+ const isSel = (sid === selectedNode.id || tid === selectedNode.id);
1984
+ const isHov = (sid === d.id || tid === d.id);
1985
+ return (isSel || isHov) ? (isSel ? 1 : 0.7) : 0;
1986
+ })
1987
+ .attr('stroke-dasharray', l => {
1988
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1989
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1990
+ const isSel = (sid === selectedNode.id || tid === selectedNode.id);
1991
+ return isSel ? 'none' : '5,3';
1992
+ })
1993
+ .attr('stroke-width', l => {
1994
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
1995
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
1996
+ return (sid === selectedNode.id || tid === selectedNode.id) ? 2 : 1.5;
1997
+ })
1998
+ .attr('marker-end', l => {
1999
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
2000
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
2001
+ return (sid === selectedNode.id || tid === selectedNode.id) ? 'url(#deps-arrow)' : 'none';
2002
+ });
2003
+ // Brighten hovered node + its neighbours; keep selected node bright; dim rest
2004
+ const allVisible = new Set([...selNeighbours, ...hovNeighbours]);
2005
+ node.select('circle').transition('hover').duration(100)
2006
+ .attr('fill', n => {
2007
+ if (selNeighbours.has(n.id)) return n.color + (n.id === selectedNode.id ? 'ff' : 'ee');
2008
+ if (hovNeighbours.has(n.id)) return n.color + 'cc';
2009
+ return n.color + '15';
2010
+ })
2011
+ .attr('stroke-width', n => {
2012
+ if (n.id === selectedNode.id) return 2.5;
2013
+ if (n.id === d.id) return 2;
2014
+ return hovNeighbours.has(n.id) ? 1.5 : 0.5;
2015
+ });
2016
+ node.select('text').transition('hover').duration(100)
2017
+ .attr('opacity', n => allVisible.has(n.id) ? 1 : 0.12)
2018
+ .attr('fill', n => hovNeighbours.has(n.id) && !selNeighbours.has(n.id) ? '#fff' : '#e6edf3')
2019
+ .attr('font-weight', n => (n.id === d.id || hovNeighbours.has(n.id)) ? '700' : '500');
2020
+ return;
2021
+ }
2022
+ if (selectedLegendGroup) {
2023
+ // Legend is selected — show this node's links + brighten hovered node text
2024
+ const neighbours = new Set([d.id]);
2025
+ GRAPH_LINKS.forEach(l => {
2026
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
2027
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
2028
+ if (sid === d.id) neighbours.add(tid);
2029
+ if (tid === d.id) neighbours.add(sid);
2030
+ });
2031
+ node.select('text').transition('hover').duration(100)
2032
+ .attr('opacity', n => neighbours.has(n.id) ? 1 : (n.group === selectedLegendGroup ? 0.3 : 0.05))
2033
+ .attr('font-weight', n => n.id === d.id ? '700' : '500');
2034
+ link.transition('hover').duration(100)
2035
+ .attr('stroke-opacity', l => {
2036
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
2037
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
2038
+ return (sid === d.id || tid === d.id) ? 0.8 : 0;
2039
+ })
2040
+ .attr('stroke-dasharray', 'none')
2041
+ .attr('stroke-width', 1.8)
2042
+ .attr('marker-end', l => {
2043
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
2044
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
2045
+ return (sid === d.id || tid === d.id) ? 'url(#deps-arrow)' : 'none';
2046
+ });
2047
+ showNodePanel(d);
2048
+ return;
2049
+ }
2050
+ applyNodeHighlight(d);
2051
+ showNodePanel(d);
2052
+ })
2053
+ .on('mouseout', () => {
2054
+ if (selectedNode) {
2055
+ // Restore selected node highlight (remove hover dotted lines)
2056
+ applyNodeHighlight(selectedNode);
2057
+ return;
2058
+ }
2059
+ document.getElementById('deps-panel').classList.remove('visible');
2060
+ if (selectedLegendGroup) {
2061
+ // Restore legend highlight — reset links and text brightening
2062
+ node.select('text').transition('hover').duration(100)
2063
+ .attr('opacity', n => n.group === selectedLegendGroup ? 1 : 0.05)
2064
+ .attr('font-weight', '500');
2065
+ link.transition('hover').duration(100).attr('stroke-opacity', 0).attr('marker-end', 'none').attr('stroke-dasharray', 'none');
2066
+ } else {
2067
+ applyNodeHighlight(null);
2068
+ }
2069
+ })
2070
+ .on('click', (ev, d) => {
2071
+ ev.stopPropagation();
2072
+ if (!document.getElementById('deps-svg').classList.contains('settled')) return;
2073
+ if (selectedNode && selectedNode.id === d.id) {
2074
+ selectedNode = null;
2075
+ if (selectedLegendGroup) applyLegendHighlight(selectedLegendGroup);
2076
+ else applyNodeHighlight(null);
2077
+ document.getElementById('deps-panel').classList.remove('visible');
2078
+ updateButtons();
2079
+ } else {
2080
+ selectedNode = d;
2081
+ // Clear any legend selection
2082
+ if (selectedLegendRow) {
2083
+ selectedLegendRow.classList.remove('leg-selected');
2084
+ selectedLegendRow.querySelector('.leg-dot').style.background = legColors[selectedLegendGroup] + '55';
2085
+ selectedLegendGroup = null;
2086
+ selectedLegendRow = null;
2087
+ }
2088
+ applyNodeHighlight(d);
2089
+ showNodePanel(d, true);
2090
+ const neighbours = new Set([d.id]);
2091
+ GRAPH_LINKS.forEach(l => {
2092
+ const sid = typeof l.source === 'object' ? l.source.id : l.source;
2093
+ const tid = typeof l.target === 'object' ? l.target.id : l.target;
2094
+ if (sid === d.id) neighbours.add(tid);
2095
+ if (tid === d.id) neighbours.add(sid);
2096
+ });
2097
+ const relatedNodes = GRAPH_NODES.filter(n => neighbours.has(n.id));
2098
+ if (relatedNodes.length) zoomToNodes(relatedNodes);
2099
+ updateButtons();
2100
+ }
2101
+ });
2102
+
2103
+ // Click SVG background to deselect
2104
+ svg.on('click', () => {
2105
+ if (!selectedNode) return;
2106
+ selectedNode = null;
2107
+ if (selectedLegendGroup) applyLegendHighlight(selectedLegendGroup);
2108
+ else applyNodeHighlight(null);
2109
+ document.getElementById('deps-panel').classList.remove('visible');
2110
+ updateButtons();
2111
+ });
2112
+
2113
+ const sim = d3.forceSimulation(GRAPH_NODES)
2114
+ .force('link', d3.forceLink(GRAPH_LINKS).id(d => d.id).distance(70).strength(0.35))
2115
+ .force('charge', d3.forceManyBody().strength(-180))
2116
+ .force('center', d3.forceCenter(W / 2, H / 2).strength(0.08))
2117
+ .force('x', d3.forceX(W / 2).strength(0.04))
2118
+ .force('y', d3.forceY(H / 2).strength(0.04))
2119
+ .force('collide', d3.forceCollide(d => nodeR(d) + (d.name.length * 3.5) + 4))
2120
+ .on('tick', () => {
2121
+ link.attr('x1', d => {
2122
+ const dx = d.target.x - d.source.x, dy = d.target.y - d.source.y;
2123
+ const dist = Math.sqrt(dx * dx + dy * dy);
2124
+ return dist ? d.source.x + (dx / dist) * nodeR(d.source) : d.source.x;
2125
+ })
2126
+ .attr('y1', d => {
2127
+ const dx = d.target.x - d.source.x, dy = d.target.y - d.source.y;
2128
+ const dist = Math.sqrt(dx * dx + dy * dy);
2129
+ return dist ? d.source.y + (dy / dist) * nodeR(d.source) : d.source.y;
2130
+ })
2131
+ .attr('x2', d => {
2132
+ const dx = d.target.x - d.source.x, dy = d.target.y - d.source.y;
2133
+ const dist = Math.sqrt(dx * dx + dy * dy);
2134
+ return dist ? d.target.x - (dx / dist) * nodeR(d.target) : d.target.x;
2135
+ })
2136
+ .attr('y2', d => {
2137
+ const dx = d.target.x - d.source.x, dy = d.target.y - d.source.y;
2138
+ const dist = Math.sqrt(dx * dx + dy * dy);
2139
+ return dist ? d.target.y - (dy / dist) * nodeR(d.target) : d.target.y;
2140
+ });
2141
+ node.attr('transform', d => \`translate(\${d.x},\${d.y})\`);
2142
+ })
2143
+ .on('end', () => {
2144
+ // Fit all nodes into view with padding
2145
+ const pad = 40;
2146
+ const xs = GRAPH_NODES.map(d => d.x);
2147
+ const ys = GRAPH_NODES.map(d => d.y);
2148
+ const minX = Math.min(...xs), maxX = Math.max(...xs);
2149
+ const minY = Math.min(...ys), maxY = Math.max(...ys);
2150
+ const bw = maxX - minX || 1, bh = maxY - minY || 1;
2151
+ const scaleX = (W - pad * 2) / bw;
2152
+ const scaleY = (H - pad * 2) / bh;
2153
+ const k = Math.min(scaleX, scaleY, 2);
2154
+ const tx = W / 2 - k * (minX + bw / 2);
2155
+ const ty = H / 2 - k * (minY + bh / 2);
2156
+ svg.transition().duration(600)
2157
+ .call(zoomBehaviour.transform, d3.zoomIdentity.translate(tx, ty).scale(k));
2158
+ document.getElementById('deps-svg').classList.add('settled');
2159
+ depsIsCentered = true;
2160
+ updateButtons();
2161
+ const loader = document.getElementById('deps-loading');
2162
+ if (loader) loader.classList.add('hidden');
2163
+ });
2164
+ }
2165
+ </script>
2166
+ </body>
2167
+ </html>`;
2168
+ }
2169
+
2170
+ // ─── HTTP server ──────────────────────────────────────────────────────────────
2171
+
2172
+ /** Build the /api/data payload — called per-request so it's always fresh. */
2173
+ function buildApiData() {
2174
+ const ROOT_ESC = ROOT.replace(/\\/g, '/');
2175
+ const TIPS = buildTipsMap(
2176
+ (() => {
2177
+ // Build the same fileMap as generateHtml uses, without duplicating all the scan logic
2178
+ const fileMap = {};
2179
+ const add = (dir, list) => {
2180
+ for (const n of list) fileMap[`${dir}/${n}`] = path.join(ROOT, dir, `${n}.ts`);
2181
+ };
2182
+ add('services', scanTs('services'));
2183
+ add('interfaces', scanTs('interfaces'));
2184
+ add('types', scanTs('types'));
2185
+ add('enums', scanTs('enums'));
2186
+ add('artifact-builder', scanTs('artifact-builder'));
2187
+ add('artifact-builder/helpers', scanTs('artifact-builder/helpers'));
2188
+ add('controls', scanTs('controls'));
2189
+ add('helpers', scanTs('helpers'));
2190
+ add('page-overrides', scanTs('page-overrides'));
2191
+ add('page-extensions', [...scanTs('page-extensions'), 'SubjectRegistry']);
2192
+ const compDir = path.join(ROOT, 'components');
2193
+ if (fs.existsSync(compDir)) {
2194
+ for (const e of fs.readdirSync(compDir, { withFileTypes: true })) {
2195
+ if (!e.isDirectory()) continue;
2196
+ const label = e.name
2197
+ .split('-')
2198
+ .map((w) => w[0].toUpperCase() + w.slice(1))
2199
+ .join('');
2200
+ const cand = path.join(compDir, e.name, `${label}.ts`);
2201
+ if (fs.existsSync(cand)) fileMap[`components/${label}`] = cand;
2202
+ }
2203
+ }
2204
+ return fileMap;
2205
+ })()
2206
+ );
2207
+
2208
+ const graph = _cachedDepGraph;
2209
+ return {
2210
+ rootPath: ROOT_ESC,
2211
+ tips: TIPS,
2212
+ graphNodes: graph.nodes.map((n) => ({
2213
+ id: n.id,
2214
+ name: n.name,
2215
+ group: n.group,
2216
+ path: `${ROOT_ESC}/${n.rel}`,
2217
+ tip: TIPS[n.name] || '',
2218
+ color: n.color
2219
+ })),
2220
+ graphLinks: graph.links
2221
+ };
2222
+ }
2223
+
2224
+ export async function diagram(port = 3333) {
2225
+ const server = http.createServer((req, res) => {
2226
+ try {
2227
+ if (req.url === '/api/data') {
2228
+ const data = buildApiData();
2229
+ const json = JSON.stringify(data);
2230
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
2231
+ res.end(json);
2232
+ return;
2233
+ }
2234
+ const html = generateHtml();
2235
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
2236
+ res.end(html);
2237
+ } catch (err) {
2238
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
2239
+ res.end(`Error: ${err.message}\n${err.stack}`);
2240
+ }
2241
+ });
2242
+
2243
+ await new Promise((resolve, reject) => {
2244
+ server.listen(port, '127.0.0.1', resolve);
2245
+ server.on('error', reject);
2246
+ });
2247
+
2248
+ const url = `http://localhost:${port}`;
2249
+ console.log(`\n✔ Testwise Architecture Diagram → ${url}`);
2250
+ console.log(' Refresh the page to regenerate from current source files.\n');
2251
+ console.log(' Ctrl+C to stop.\n');
2252
+
2253
+ try {
2254
+ const { execSync } = await import('node:child_process');
2255
+ const open =
2256
+ process.platform === 'win32'
2257
+ ? `start "" "${url}"`
2258
+ : process.platform === 'darwin'
2259
+ ? `open "${url}"`
2260
+ : `xdg-open "${url}"`;
2261
+ execSync(open);
2262
+ } catch {
2263
+ /* best-effort */
2264
+ }
2265
+
2266
+ await new Promise(() => {});
2267
+ }