@globisnv/cypress-core 0.0.2 → 0.0.3

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 (297) hide show
  1. package/README.md +12 -7
  2. package/dist/apiMethods/IGS_request.d.ts +98 -0
  3. package/dist/apiMethods/IGS_request.js +161 -0
  4. package/dist/apiMethods/IGS_response.d.ts +47 -0
  5. package/dist/apiMethods/IGS_response.js +1 -0
  6. package/dist/blueprints/GLO_addressPopup.d.ts +45 -0
  7. package/dist/blueprints/GLO_addressPopup.js +96 -0
  8. package/dist/blueprints/GLO_article.d.ts +45 -0
  9. package/dist/blueprints/GLO_article.js +110 -0
  10. package/dist/blueprints/GLO_articleLot.d.ts +12 -0
  11. package/dist/blueprints/GLO_articleLot.js +23 -0
  12. package/dist/blueprints/GLO_articleVersion.d.ts +4 -0
  13. package/dist/blueprints/GLO_articleVersion.js +4 -0
  14. package/dist/blueprints/GLO_attachmentPopup.d.ts +9 -0
  15. package/dist/blueprints/GLO_attachmentPopup.js +10 -0
  16. package/dist/blueprints/GLO_bankAccount.d.ts +7 -0
  17. package/dist/blueprints/GLO_bankAccount.js +7 -0
  18. package/dist/blueprints/GLO_billingProcess.d.ts +4 -0
  19. package/dist/blueprints/GLO_billingProcess.js +4 -0
  20. package/dist/blueprints/GLO_billingProposal.d.ts +9 -0
  21. package/dist/blueprints/GLO_billingProposal.js +9 -0
  22. package/dist/blueprints/GLO_cancel.d.ts +7 -0
  23. package/dist/blueprints/GLO_cancel.js +7 -0
  24. package/dist/blueprints/GLO_commandlist.d.ts +10 -0
  25. package/dist/blueprints/GLO_commandlist.js +10 -0
  26. package/dist/blueprints/GLO_complaint.d.ts +27 -0
  27. package/dist/blueprints/GLO_complaint.js +51 -0
  28. package/dist/blueprints/GLO_composition.d.ts +14 -0
  29. package/dist/blueprints/GLO_composition.js +20 -0
  30. package/dist/blueprints/GLO_consignment.d.ts +61 -0
  31. package/dist/blueprints/GLO_consignment.js +104 -0
  32. package/dist/blueprints/GLO_consignmentContainerGroup.d.ts +6 -0
  33. package/dist/blueprints/GLO_consignmentContainerGroup.js +6 -0
  34. package/dist/blueprints/GLO_consignmentGoods.d.ts +40 -0
  35. package/dist/blueprints/GLO_consignmentGoods.js +62 -0
  36. package/dist/blueprints/GLO_consignmentHandling.d.ts +4 -0
  37. package/dist/blueprints/GLO_consignmentHandling.js +4 -0
  38. package/dist/blueprints/GLO_consignmentOrder.d.ts +31 -0
  39. package/dist/blueprints/GLO_consignmentOrder.js +37 -0
  40. package/dist/blueprints/GLO_consignmentPart.d.ts +13 -0
  41. package/dist/blueprints/GLO_consignmentPart.js +13 -0
  42. package/dist/blueprints/GLO_consignmentParts.d.ts +5 -0
  43. package/dist/blueprints/GLO_consignmentParts.js +12 -0
  44. package/dist/blueprints/GLO_consignmentPrice.d.ts +11 -0
  45. package/dist/blueprints/GLO_consignmentPrice.js +19 -0
  46. package/dist/blueprints/GLO_consignmentSupplier.d.ts +6 -0
  47. package/dist/blueprints/GLO_consignmentSupplier.js +11 -0
  48. package/dist/blueprints/GLO_contacts.d.ts +10 -0
  49. package/dist/blueprints/GLO_contacts.js +14 -0
  50. package/dist/blueprints/GLO_container.d.ts +6 -0
  51. package/dist/blueprints/GLO_container.js +4 -0
  52. package/dist/blueprints/GLO_contract.d.ts +22 -0
  53. package/dist/blueprints/GLO_contract.js +30 -0
  54. package/dist/blueprints/GLO_contractArticle.d.ts +17 -0
  55. package/dist/blueprints/GLO_contractArticle.js +35 -0
  56. package/dist/blueprints/GLO_contractArticleCost.d.ts +5 -0
  57. package/dist/blueprints/GLO_contractArticleCost.js +8 -0
  58. package/dist/blueprints/GLO_countryVatZone.d.ts +8 -0
  59. package/dist/blueprints/GLO_countryVatZone.js +9 -0
  60. package/dist/blueprints/GLO_currencyDeviation.d.ts +14 -0
  61. package/dist/blueprints/GLO_currencyDeviation.js +15 -0
  62. package/dist/blueprints/GLO_customerArticle.d.ts +12 -0
  63. package/dist/blueprints/GLO_customerArticle.js +15 -0
  64. package/dist/blueprints/GLO_customsFlow.d.ts +8 -0
  65. package/dist/blueprints/GLO_customsFlow.js +19 -0
  66. package/dist/blueprints/GLO_customsFlowDefinition.d.ts +15 -0
  67. package/dist/blueprints/GLO_customsFlowDefinition.js +14 -0
  68. package/dist/blueprints/GLO_customsUnion.d.ts +4 -0
  69. package/dist/blueprints/GLO_customsUnion.js +4 -0
  70. package/dist/blueprints/GLO_deliveryReceipt.d.ts +10 -0
  71. package/dist/blueprints/GLO_deliveryReceipt.js +22 -0
  72. package/dist/blueprints/GLO_departureSchedule.d.ts +27 -0
  73. package/dist/blueprints/GLO_departureSchedule.js +57 -0
  74. package/dist/blueprints/GLO_departureScheduleTimes.d.ts +49 -0
  75. package/dist/blueprints/GLO_departureScheduleTimes.js +61 -0
  76. package/dist/blueprints/GLO_document.d.ts +7 -0
  77. package/dist/blueprints/GLO_document.js +7 -0
  78. package/dist/blueprints/GLO_documentAttachment.d.ts +4 -0
  79. package/dist/blueprints/GLO_documentAttachment.js +4 -0
  80. package/dist/blueprints/GLO_documentDefinitionEntity.d.ts +5 -0
  81. package/dist/blueprints/GLO_documentDefinitionEntity.js +5 -0
  82. package/dist/blueprints/GLO_documentGroup.d.ts +4 -0
  83. package/dist/blueprints/GLO_documentGroup.js +4 -0
  84. package/dist/blueprints/GLO_documentType.d.ts +12 -0
  85. package/dist/blueprints/GLO_documentType.js +11 -0
  86. package/dist/blueprints/GLO_enterCountry.d.ts +21 -0
  87. package/dist/blueprints/GLO_enterCountry.js +47 -0
  88. package/dist/blueprints/GLO_enterDiscount.d.ts +8 -0
  89. package/dist/blueprints/GLO_enterDiscount.js +9 -0
  90. package/dist/blueprints/GLO_equipment.d.ts +34 -0
  91. package/dist/blueprints/GLO_equipment.js +55 -0
  92. package/dist/blueprints/GLO_equipmentCategory.d.ts +15 -0
  93. package/dist/blueprints/GLO_equipmentCategory.js +14 -0
  94. package/dist/blueprints/GLO_equipmentEntity.d.ts +5 -0
  95. package/dist/blueprints/GLO_equipmentEntity.js +5 -0
  96. package/dist/blueprints/GLO_equipmentServiceCategory.d.ts +13 -0
  97. package/dist/blueprints/GLO_equipmentServiceCategory.js +13 -0
  98. package/dist/blueprints/GLO_equipmentType.d.ts +21 -0
  99. package/dist/blueprints/GLO_equipmentType.js +20 -0
  100. package/dist/blueprints/GLO_equipmentTypeServiceCategory.d.ts +13 -0
  101. package/dist/blueprints/GLO_equipmentTypeServiceCategory.js +12 -0
  102. package/dist/blueprints/GLO_fieldInputValidation.d.ts +46 -0
  103. package/dist/blueprints/GLO_fieldInputValidation.js +73 -0
  104. package/dist/blueprints/GLO_formula.d.ts +9 -0
  105. package/dist/blueprints/GLO_formula.js +9 -0
  106. package/dist/blueprints/GLO_gang.d.ts +12 -0
  107. package/dist/blueprints/GLO_gang.js +12 -0
  108. package/dist/blueprints/GLO_holiday.d.ts +7 -0
  109. package/dist/blueprints/GLO_holiday.js +8 -0
  110. package/dist/blueprints/GLO_journey.d.ts +18 -0
  111. package/dist/blueprints/GLO_journey.js +24 -0
  112. package/dist/blueprints/GLO_journeyStop.d.ts +32 -0
  113. package/dist/blueprints/GLO_journeyStop.js +44 -0
  114. package/dist/blueprints/GLO_layout.d.ts +6 -0
  115. package/dist/blueprints/GLO_layout.js +6 -0
  116. package/dist/blueprints/GLO_logging.d.ts +9 -0
  117. package/dist/blueprints/GLO_logging.js +12 -0
  118. package/dist/blueprints/GLO_logisticConnection.d.ts +10 -0
  119. package/dist/blueprints/GLO_logisticConnection.js +11 -0
  120. package/dist/blueprints/GLO_logisticConnectionType.d.ts +5 -0
  121. package/dist/blueprints/GLO_logisticConnectionType.js +5 -0
  122. package/dist/blueprints/GLO_logisticContract.d.ts +28 -0
  123. package/dist/blueprints/GLO_logisticContract.js +80 -0
  124. package/dist/blueprints/GLO_logisticContractDetail.d.ts +42 -0
  125. package/dist/blueprints/GLO_logisticContractDetail.js +122 -0
  126. package/dist/blueprints/GLO_logisticContractDetailHandling.d.ts +11 -0
  127. package/dist/blueprints/GLO_logisticContractDetailHandling.js +14 -0
  128. package/dist/blueprints/GLO_logisticContractDetailItem.d.ts +5 -0
  129. package/dist/blueprints/GLO_logisticContractDetailItem.js +8 -0
  130. package/dist/blueprints/GLO_logisticContractItem.d.ts +14 -0
  131. package/dist/blueprints/GLO_logisticContractItem.js +40 -0
  132. package/dist/blueprints/GLO_logisticContractItemActivity.d.ts +4 -0
  133. package/dist/blueprints/GLO_logisticContractItemActivity.js +4 -0
  134. package/dist/blueprints/GLO_logisticContractItemSurcharge.d.ts +7 -0
  135. package/dist/blueprints/GLO_logisticContractItemSurcharge.js +7 -0
  136. package/dist/blueprints/GLO_logisticContractItemSurchargeItem.d.ts +4 -0
  137. package/dist/blueprints/GLO_logisticContractItemSurchargeItem.js +4 -0
  138. package/dist/blueprints/GLO_logisticContractReference.d.ts +4 -0
  139. package/dist/blueprints/GLO_logisticContractReference.js +4 -0
  140. package/dist/blueprints/GLO_logisticContractVolumeConstraint.d.ts +9 -0
  141. package/dist/blueprints/GLO_logisticContractVolumeConstraint.js +8 -0
  142. package/dist/blueprints/GLO_logisticLocation.d.ts +7 -0
  143. package/dist/blueprints/GLO_logisticLocation.js +7 -0
  144. package/dist/blueprints/GLO_logisticPriceRequestDetailExternalPrice.d.ts +19 -0
  145. package/dist/blueprints/GLO_logisticPriceRequestDetailExternalPrice.js +30 -0
  146. package/dist/blueprints/GLO_logisticPurchaseOrder.d.ts +4 -0
  147. package/dist/blueprints/GLO_logisticPurchaseOrder.js +4 -0
  148. package/dist/blueprints/GLO_logistic_waiting_hours.d.ts +6 -0
  149. package/dist/blueprints/GLO_logistic_waiting_hours.js +6 -0
  150. package/dist/blueprints/GLO_operationalDefault.d.ts +23 -0
  151. package/dist/blueprints/GLO_operationalDefault.js +25 -0
  152. package/dist/blueprints/GLO_outputDetailMail.d.ts +14 -0
  153. package/dist/blueprints/GLO_outputDetailMail.js +13 -0
  154. package/dist/blueprints/GLO_postalcode.d.ts +6 -0
  155. package/dist/blueprints/GLO_postalcode.js +6 -0
  156. package/dist/blueprints/GLO_priceDependency.d.ts +5 -0
  157. package/dist/blueprints/GLO_priceDependency.js +4 -0
  158. package/dist/blueprints/GLO_priceEntry.d.ts +17 -0
  159. package/dist/blueprints/GLO_priceEntry.js +17 -0
  160. package/dist/blueprints/GLO_priceRequest.d.ts +8 -0
  161. package/dist/blueprints/GLO_priceRequest.js +20 -0
  162. package/dist/blueprints/GLO_priceRequestLine.d.ts +12 -0
  163. package/dist/blueprints/GLO_priceRequestLine.js +17 -0
  164. package/dist/blueprints/GLO_principal.d.ts +9 -0
  165. package/dist/blueprints/GLO_principal.js +13 -0
  166. package/dist/blueprints/GLO_printPopup.d.ts +10 -0
  167. package/dist/blueprints/GLO_printPopup.js +62 -0
  168. package/dist/blueprints/GLO_production.d.ts +9 -0
  169. package/dist/blueprints/GLO_production.js +9 -0
  170. package/dist/blueprints/GLO_productionLineMaterial.d.ts +8 -0
  171. package/dist/blueprints/GLO_productionLineMaterial.js +13 -0
  172. package/dist/blueprints/GLO_productionMaterial.d.ts +5 -0
  173. package/dist/blueprints/GLO_productionMaterial.js +5 -0
  174. package/dist/blueprints/GLO_purchaseCosts.d.ts +6 -0
  175. package/dist/blueprints/GLO_purchaseCosts.js +9 -0
  176. package/dist/blueprints/GLO_purchaseInvoice.d.ts +12 -0
  177. package/dist/blueprints/GLO_purchaseInvoice.js +25 -0
  178. package/dist/blueprints/GLO_purchaseInvoiceLine.d.ts +8 -0
  179. package/dist/blueprints/GLO_purchaseInvoiceLine.js +13 -0
  180. package/dist/blueprints/GLO_purchaseOrder.d.ts +14 -0
  181. package/dist/blueprints/GLO_purchaseOrder.js +25 -0
  182. package/dist/blueprints/GLO_purchaseOrderLine.d.ts +10 -0
  183. package/dist/blueprints/GLO_purchaseOrderLine.js +24 -0
  184. package/dist/blueprints/GLO_purchasePrices.d.ts +10 -0
  185. package/dist/blueprints/GLO_purchasePrices.js +10 -0
  186. package/dist/blueprints/GLO_quotation.d.ts +12 -0
  187. package/dist/blueprints/GLO_quotation.js +32 -0
  188. package/dist/blueprints/GLO_quotationLine.d.ts +12 -0
  189. package/dist/blueprints/GLO_quotationLine.js +28 -0
  190. package/dist/blueprints/GLO_receipt.d.ts +13 -0
  191. package/dist/blueprints/GLO_receipt.js +17 -0
  192. package/dist/blueprints/GLO_receiptFromTempReceipt.d.ts +8 -0
  193. package/dist/blueprints/GLO_receiptFromTempReceipt.js +8 -0
  194. package/dist/blueprints/GLO_receiptLine.d.ts +22 -0
  195. package/dist/blueprints/GLO_receiptLine.js +28 -0
  196. package/dist/blueprints/GLO_relationAddress.d.ts +26 -0
  197. package/dist/blueprints/GLO_relationAddress.js +58 -0
  198. package/dist/blueprints/GLO_relationBankAccount.d.ts +9 -0
  199. package/dist/blueprints/GLO_relationBankAccount.js +9 -0
  200. package/dist/blueprints/GLO_relationContact.d.ts +23 -0
  201. package/dist/blueprints/GLO_relationContact.js +45 -0
  202. package/dist/blueprints/GLO_relationGroupType.d.ts +7 -0
  203. package/dist/blueprints/GLO_relationGroupType.js +6 -0
  204. package/dist/blueprints/GLO_relations.d.ts +67 -0
  205. package/dist/blueprints/GLO_relations.js +186 -0
  206. package/dist/blueprints/GLO_relationtype.d.ts +15 -0
  207. package/dist/blueprints/GLO_relationtype.js +31 -0
  208. package/dist/blueprints/GLO_remarkType.d.ts +10 -0
  209. package/dist/blueprints/GLO_remarkType.js +9 -0
  210. package/dist/blueprints/GLO_remarkTypeDefaultValue.d.ts +4 -0
  211. package/dist/blueprints/GLO_remarkTypeDefaultValue.js +14 -0
  212. package/dist/blueprints/GLO_remarkTypeEntityType.d.ts +10 -0
  213. package/dist/blueprints/GLO_remarkTypeEntityType.js +9 -0
  214. package/dist/blueprints/GLO_representative.d.ts +7 -0
  215. package/dist/blueprints/GLO_representative.js +6 -0
  216. package/dist/blueprints/GLO_salesInvoice.d.ts +9 -0
  217. package/dist/blueprints/GLO_salesInvoice.js +15 -0
  218. package/dist/blueprints/GLO_salesInvoiceLine.d.ts +9 -0
  219. package/dist/blueprints/GLO_salesInvoiceLine.js +13 -0
  220. package/dist/blueprints/GLO_salesOrder.d.ts +13 -0
  221. package/dist/blueprints/GLO_salesOrder.js +25 -0
  222. package/dist/blueprints/GLO_salesOrderLine.d.ts +13 -0
  223. package/dist/blueprints/GLO_salesOrderLine.js +34 -0
  224. package/dist/blueprints/GLO_stockCorrection.d.ts +14 -0
  225. package/dist/blueprints/GLO_stockCorrection.js +24 -0
  226. package/dist/blueprints/GLO_stopSchedule.d.ts +4 -0
  227. package/dist/blueprints/GLO_stopSchedule.js +4 -0
  228. package/dist/blueprints/GLO_supplierArticle.d.ts +8 -0
  229. package/dist/blueprints/GLO_supplierArticle.js +8 -0
  230. package/dist/blueprints/GLO_tclSetting.d.ts +6 -0
  231. package/dist/blueprints/GLO_tclSetting.js +6 -0
  232. package/dist/blueprints/GLO_todo.d.ts +10 -0
  233. package/dist/blueprints/GLO_todo.js +21 -0
  234. package/dist/blueprints/GLO_todoSchedule.d.ts +23 -0
  235. package/dist/blueprints/GLO_todoSchedule.js +79 -0
  236. package/dist/blueprints/GLO_tractionDistance.d.ts +6 -0
  237. package/dist/blueprints/GLO_tractionDistance.js +6 -0
  238. package/dist/blueprints/GLO_transportOrder.d.ts +4 -0
  239. package/dist/blueprints/GLO_transportOrder.js +4 -0
  240. package/dist/blueprints/GLO_transportPrice.d.ts +18 -0
  241. package/dist/blueprints/GLO_transportPrice.js +34 -0
  242. package/dist/blueprints/GLO_transportRequirement.d.ts +18 -0
  243. package/dist/blueprints/GLO_transportRequirement.js +32 -0
  244. package/dist/blueprints/GLO_transportRequirementContainer.d.ts +8 -0
  245. package/dist/blueprints/GLO_transportRequirementContainer.js +8 -0
  246. package/dist/blueprints/GLO_transportRequirementGoods.d.ts +33 -0
  247. package/dist/blueprints/GLO_transportRequirementGoods.js +51 -0
  248. package/dist/blueprints/GLO_transportScenarioExecutionLeg.d.ts +6 -0
  249. package/dist/blueprints/GLO_transportScenarioExecutionLeg.js +6 -0
  250. package/dist/blueprints/GLO_transportStock.d.ts +4 -0
  251. package/dist/blueprints/GLO_transportStock.js +4 -0
  252. package/dist/blueprints/GLO_visit.d.ts +9 -0
  253. package/dist/blueprints/GLO_visit.js +10 -0
  254. package/dist/blueprints/GLO_warehouseMove.d.ts +9 -0
  255. package/dist/blueprints/GLO_warehouseMove.js +9 -0
  256. package/dist/blueprints/GLO_warehouseOrderSubline.d.ts +6 -0
  257. package/dist/blueprints/GLO_warehouseOrderSubline.js +11 -0
  258. package/dist/blueprints/GLO_workflowLoggingTable.d.ts +20 -0
  259. package/dist/blueprints/GLO_workflowLoggingTable.js +20 -0
  260. package/dist/blueprints/GLO_workflowVerification.d.ts +10 -0
  261. package/dist/blueprints/GLO_workflowVerification.js +19 -0
  262. package/dist/blueprints/GLO_zone.d.ts +9 -0
  263. package/dist/blueprints/GLO_zone.js +9 -0
  264. package/dist/blueprints/GLO_zone_postalcode.d.ts +12 -0
  265. package/dist/blueprints/GLO_zone_postalcode.js +10 -0
  266. package/dist/blueprints/GLO_zone_type.d.ts +8 -0
  267. package/dist/blueprints/GLO_zone_type.js +7 -0
  268. package/dist/blueprints/GLT_goods.d.ts +2 -0
  269. package/dist/blueprints/GLT_goods.js +12 -0
  270. package/dist/commands/FOB_commands.d.ts +124 -0
  271. package/dist/commands/FOB_commands.js +211 -0
  272. package/dist/commands/GLJ_commands.d.ts +28 -0
  273. package/dist/commands/GLJ_commands.js +40 -0
  274. package/dist/commands/GLO_commands.d.ts +685 -0
  275. package/dist/commands/GLO_commands.js +2321 -0
  276. package/dist/commands/GLS_commands.d.ts +63 -0
  277. package/dist/commands/GLS_commands.js +103 -0
  278. package/dist/commands/GLT_commands.d.ts +78 -0
  279. package/dist/commands/GLT_commands.js +148 -0
  280. package/dist/commands/POR_commands.d.ts +85 -0
  281. package/dist/commands/POR_commands.js +122 -0
  282. package/dist/config.d.ts +9 -0
  283. package/dist/config.js +115 -0
  284. package/dist/index.d.ts +2 -0
  285. package/dist/index.js +15 -0
  286. package/dist/plugins/prometheusWriter.d.ts +15 -0
  287. package/dist/plugins/prometheusWriter.js +111 -0
  288. package/dist/register.d.ts +8 -0
  289. package/dist/register.js +23 -0
  290. package/dist/tsHelper.d.ts +161 -0
  291. package/dist/tsHelper.js +294 -0
  292. package/dist/types.d.ts +363 -0
  293. package/dist/types.js +4 -0
  294. package/dist/websocketDetector.d.ts +71 -0
  295. package/dist/websocketDetector.js +1593 -0
  296. package/package.json +10 -2
  297. package/template/package.json +1 -1
@@ -0,0 +1,2321 @@
1
+ import { bottom, buttonSearch, check, contains, cypressWebsocketLoader, defaultTimeout, enabled, equal, greaterEqual, gridDropDown, gridInput, gridInputWithoutEnter, gridSelect, hideInCommandLog, input, isInvalid, noEqualBool, noSkipCheck, notEqual, parseLocalizedNumber, row, select, skipCheck, span, top, unCheck } from '../tsHelper.js';
2
+ import { attachWebSocketSpy } from '../websocketDetector.js';
3
+ //! File-specific helpers
4
+ const monthNames = ['januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'];
5
+ const monthMap = new Map(monthNames.map((month, index) => [month, index + 1]));
6
+ const monthNamesEN = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
7
+ monthNamesEN.forEach((month, index) => {
8
+ monthMap.set(month, index + 1);
9
+ });
10
+ const globisUrlTypes = ['globtake', 'globis_external_portal'];
11
+ const viewModeButtons = ['btnList', 'btnDetail'];
12
+ const extractSelectorParts = (selector) => {
13
+ const last = selector[selector.length - 1];
14
+ if (typeof last === 'number')
15
+ return { baseSelector: selector.slice(0, -1), index: last };
16
+ return { baseSelector: selector, index: 0 };
17
+ };
18
+ // When index refers to a grid row (element repeated per row), GLO_getElement handles row filtering via the full selector.
19
+ // When index refers to a sub-input within a single element (e.g. a field with two inputs), the caller uses eq(index).
20
+ // This helper distinguishes the two cases synchronously.
21
+ const isGridRowSelector = (baseSelector, index) => {
22
+ if (index === 0)
23
+ return false;
24
+ const selectorStr = baseSelector.length === 2 ? `[data-cy$="${baseSelector[0]}.${baseSelector[1]}"]` : `[name="${baseSelector[0]}"]`;
25
+ const el = Cypress.$(`div[formslist]:not([hidden]) ${selectorStr}`)[0];
26
+ return !!el?.closest('[row-index]');
27
+ };
28
+ // Row queries must ignore grids that are not on screen, but neither obvious filter does that job.
29
+ // Servoy marks the *containers* hidden - `div[formslist][hidden]` for background programs,
30
+ // `[tabpane][hidden]` for closed tabs (see GLO_getElement) - so `[row-index]:not([hidden])` filters
31
+ // nothing at all and matches rows of every grid in the DOM. `:visible` overshoots the other way:
32
+ // AG Grid's ag-root and ag-viewport use `overflow: hidden`, so Cypress' out-of-bounds check reports
33
+ // rendered-but-scrolled-past rows as hidden (measured: 14 of 21 rendered rows drop out of `:visible`
34
+ // after scrolling a grid to the bottom). Filter on the container, which is what both were after.
35
+ const isInOpenContainer = (_, el) => !el.closest('[hidden]');
36
+ const pickByRowIndex = (chain, index, log = false) => {
37
+ if (log)
38
+ Cypress.log({ name: 'idx', message: String(index) });
39
+ return chain.filter((i, el) => {
40
+ const rowIndexEl = el.closest('[row-index]');
41
+ return rowIndexEl ? rowIndexEl.getAttribute('row-index') === String(index) : i === index;
42
+ });
43
+ };
44
+ Cypress.Commands.add('GLO_getElement', (selectorArg, options = {}) => {
45
+ const arr = selectorArg;
46
+ const hasIndex = typeof arr[arr.length - 1] === 'number';
47
+ const index = hasIndex ? arr[arr.length - 1] : undefined;
48
+ const strParts = (hasIndex ? arr.slice(0, -1) : arr);
49
+ const selector = strParts.length === 2 ? `[data-cy$="${strParts[0]}.${strParts[1]}"]` : `[name="${strParts[0]}"]`;
50
+ const safeCallerOpts = Object.fromEntries(Object.entries(options).filter(([, v]) => v !== undefined));
51
+ const getOpts = { timeout: defaultTimeout, log: true, ...safeCallerOpts };
52
+ const withinChain = cy.state('withinSubjectChain');
53
+ const withinSubject = withinChain ? cy.getSubjectFromChain(withinChain) : undefined;
54
+ if (!withinSubject) {
55
+ cy.wrap(null, { log: false, timeout: getOpts.timeout }).should(() => {
56
+ if (Cypress.$(`div[formslist]:not([hidden]) [tabpane][hidden] ${selector}`).length > 0) {
57
+ throw new Error(`GLO_getElement: element "${selector}" found in crumb but its tab is closed`);
58
+ }
59
+ });
60
+ const elementInCrumb = Cypress.$(`div[formslist] ${selector}`).length > 0;
61
+ if (elementInCrumb) {
62
+ const result = cy.get(`div[formslist]:not([hidden]) ${selector}`, getOpts);
63
+ return pickByRowIndex(result, index ?? 0, hasIndex);
64
+ }
65
+ }
66
+ const result = cy.get(selector, getOpts);
67
+ // Ponytail: sync DOM check — if element absent, skip filter so cy.get + should('not.exist') chain works
68
+ return hasIndex || Cypress.$(selector).length > 0 ? pickByRowIndex(result, index ?? 0, hasIndex) : result;
69
+ });
70
+ Cypress.Commands.add('GLO_getVisible', (elementName, timeOut = defaultTimeout) => {
71
+ return cy.get(elementName, { timeout: timeOut }).filter(':visible', { timeout: timeOut });
72
+ });
73
+ Cypress.Commands.add('GLO_checkUser', () => {
74
+ const username = String(Cypress.expose('username'));
75
+ if (/globisadmin/.exec(username.toLowerCase())) {
76
+ throw new Error('Deletion of created data not allowed when globisadmin. Please log in using cypress user to proceed testing.'); // Stop the test by throwing an error
77
+ }
78
+ });
79
+ Cypress.Commands.add('GLO_login', (company, urlType) => {
80
+ const loginType = String(Cypress.expose('loginType'));
81
+ if (!Cypress.expose('globisUrl')) {
82
+ throw new Error('No Globis URL defined as environment parameter!');
83
+ }
84
+ switch (loginType.toLocaleLowerCase()) {
85
+ case 'servoy':
86
+ cy.GLO_servoyLogin(company);
87
+ break;
88
+ case 'okta':
89
+ cy.GLO_oktaLogin(company, urlType);
90
+ break;
91
+ case 'default':
92
+ cy.GLO_defaultlogin(company, undefined, undefined, urlType);
93
+ break;
94
+ default:
95
+ throw new Error('Incorrect type for GLO_login. Available types: servoy, okta, default');
96
+ }
97
+ });
98
+ Cypress.Commands.add('GLO_defaultlogin', (company, usernameENV = 'username', passwordENV = 'password', urlType) => {
99
+ // Change the Url if necessary
100
+ let url = Cypress.expose('globisUrl');
101
+ if (urlType) {
102
+ const target = globisUrlTypes.includes(urlType) ? 'globis_online' : 'online';
103
+ url = url.replace(target, urlType);
104
+ }
105
+ if (cypressWebsocketLoader) {
106
+ cy.WS_visitWithSpy(url);
107
+ }
108
+ else {
109
+ cy.visit(url);
110
+ }
111
+ cy.GLO_checkLoadingIndicator();
112
+ urlType === 'globjob' ? fillInUserAndPasswordGlobjob(usernameENV) : fillInUserAndPasswordDefault(usernameENV, passwordENV);
113
+ if (company) {
114
+ cy.GLO_selectCompany(company);
115
+ }
116
+ cy.GLO_checkLoadingIndicator();
117
+ cy.get('body').then(($body) => {
118
+ if ($body.find('.svy-message-dialog .window-title').text() === 'Locked record') {
119
+ // biome-ignore plugin: wait for locked record dialog choice to register
120
+ cy.wait(15000);
121
+ cy.GLO_dialogChoice('!globis.label.yes!');
122
+ }
123
+ });
124
+ if (!urlType) {
125
+ cy.get('[data-cy="ng$main_form2.navbar_top"]', { timeout: 120000 }).should('exist');
126
+ cy.get('loading-indicator', { timeout: 1200000 }).should('not.exist');
127
+ }
128
+ });
129
+ const fillInUserAndPasswordDefault = (usernameENV, passwordENV) => {
130
+ cy.get('[data-cy="ng2$svy_nav_login.logoImage"]', { timeout: 120000 }).should('exist');
131
+ cy.GLO_checkLoadingIndicator(); // The logo might be visible but there can still be a loading indicator
132
+ cy.GLO_getElement(['ng2$svy_nav_login', 'txtUser'], { timeout: 30000 })
133
+ .find('input:visible', { timeout: 30000 })
134
+ .eq(0)
135
+ .clear()
136
+ .type(Cypress.expose(usernameENV));
137
+ cy.env([passwordENV]).then((envVars) => {
138
+ cy.GLO_getElement(['ng2$svy_nav_login', 'txtPass'])
139
+ .find('input')
140
+ .eq(0)
141
+ .clear()
142
+ .type(envVars[passwordENV], { log: false });
143
+ });
144
+ cy.get('[data-cy="ng2$svy_nav_login.btnLogin"]').click();
145
+ cy.GLO_checkLoadingIndicator();
146
+ };
147
+ const fillInUserAndPasswordGlobjob = (usernameENV) => {
148
+ cy.GLO_getElement(['mobile$svy_nav_login', 'txt_login'])
149
+ .find('input')
150
+ .eq(0)
151
+ .clear()
152
+ .type(Cypress.expose(usernameENV));
153
+ cy.get('[data-cy="mobile$svy_nav_login.btn_login"]').click();
154
+ cy.GLO_checkLoadingIndicator();
155
+ };
156
+ Cypress.Commands.add('GLO_defaultloginExtraUser', (usernameENV, passwordENV, company) => {
157
+ cy.GLO_getElement(['ng2$svy_nav_login', 'txtUser'], { timeout: 60000 }).should('exist');
158
+ fillInUserAndPasswordDefault(usernameENV, passwordENV);
159
+ if (company) {
160
+ cy.GLO_selectCompany(company);
161
+ }
162
+ cy.get('[data-cy="ng$main_form2.navbar_top"]', { timeout: 120000 }).should('exist');
163
+ });
164
+ const waitForWebSocketAfterOkta = () => {
165
+ cy.url({ timeout: 60000 }).should('not.include', new URL(Cypress.expose('oktaUrl')).hostname);
166
+ cy.window({ timeout: 60000 }).should((win) => {
167
+ const w = win;
168
+ if (!w.wsMessages)
169
+ attachWebSocketSpy(win);
170
+ assert.isTrue(w.wsMessages?.wsConnected === true);
171
+ });
172
+ cy.window().then((win) => {
173
+ cy.wrap(win.wsMessages).as('wsMessages');
174
+ cy.task('log', `WebSocket spy confirmed connected, ${win.wsMessages?.messages.length} messages captured so far`);
175
+ });
176
+ };
177
+ Cypress.Commands.add('GLO_oktaLogin', (company, urlType, skipvNavCheck) => {
178
+ // Change the Url if necessary
179
+ let url = Cypress.expose('globisUrl');
180
+ if (urlType) {
181
+ const target = globisUrlTypes.includes(urlType) ? 'globis_online' : 'online';
182
+ url = url.replace(target, urlType);
183
+ }
184
+ if (cypressWebsocketLoader) {
185
+ cy.WS_visitWithSpy(url);
186
+ }
187
+ else {
188
+ cy.visit(url);
189
+ }
190
+ cy.task('log', 'Redirect to okta url');
191
+ if (cypressWebsocketLoader) {
192
+ cy.on('window:before:load', (win) => {
193
+ attachWebSocketSpy(win);
194
+ });
195
+ }
196
+ cy.origin(Cypress.expose('oktaUrl'), () => {
197
+ cy.get('.o-form-input', { timeout: 60000 })
198
+ .find('[autocomplete="username"]')
199
+ .clear()
200
+ .type(Cypress.expose('username'));
201
+ cy.task('log', 'Filled in username');
202
+ cy.get('[type="submit"]').click();
203
+ cy.env(['password']).then(({ password }) => {
204
+ cy.get('.o-form-input')
205
+ .find('[type="password"]', { timeout: 20000 })
206
+ .type(password, { log: false });
207
+ });
208
+ cy.task('log', 'Filled in password');
209
+ cy.get('[type="submit"]').click();
210
+ });
211
+ if (company) {
212
+ cy.GLO_selectCompany(company);
213
+ }
214
+ if (cypressWebsocketLoader)
215
+ waitForWebSocketAfterOkta();
216
+ cy.task('log', 'Start navbar check');
217
+ if (!skipvNavCheck) {
218
+ const navbarSelectors = {
219
+ globtake: '[data-cy="gt$main.form_view_sidebar"]',
220
+ globis_external_portal: '[data-cy$="ep$main.sidemenu"]'
221
+ };
222
+ const selector = urlType ? navbarSelectors[urlType] : '[data-cy="ng$main_form2.navbar_top"]';
223
+ if (selector)
224
+ cy.get(selector, { timeout: 120000 }).should('exist');
225
+ cy.get('loading-indicator', { timeout: 1200000 }).should('not.exist');
226
+ }
227
+ else {
228
+ // This is in case the check is instable
229
+ // biome-ignore plugin: wait in case the check is instable
230
+ cy.wait(5000);
231
+ cy.GLO_checkLoadingIndicator();
232
+ }
233
+ cy.task('log', 'End navbar check');
234
+ });
235
+ Cypress.Commands.add('GLO_logout', () => {
236
+ cy.GLO_getElement(['ng$main_form2', 'navbar_top']).find('.lucide-LogOut').click();
237
+ cy.GLO_getVisible('.window-footer button.svy-btn-primary', 30000)
238
+ .contains(/^(Yes|Ja)$/)
239
+ .click();
240
+ });
241
+ Cypress.Commands.add('GLO_servoyLogin', (company) => {
242
+ if (cypressWebsocketLoader) {
243
+ cy.WS_visitWithSpy(Cypress.expose('globisUrl'));
244
+ }
245
+ else {
246
+ cy.visit(Cypress.expose('globisUrl'));
247
+ }
248
+ cy.GLO_checkLoadingIndicator();
249
+ cy.GLO_getElement(['ng2$svy_nav_login', 'txtUser'], { timeout: 30000 })
250
+ .find('input')
251
+ .eq(0)
252
+ .clear()
253
+ .type(Cypress.expose('username'));
254
+ cy.env(['password']).then(({ password }) => {
255
+ cy.GLO_getElement(['ng2$svy_nav_login', 'txtPass'])
256
+ .find('input')
257
+ .eq(0)
258
+ .clear()
259
+ .type(password, { log: false });
260
+ });
261
+ cy.GLO_addInput(['ng2$svy_nav_login', 'owner'], Cypress.expose('ownerName'));
262
+ cy.GLO_getElement(['ng2$svy_nav_login', 'btnLogin']).click();
263
+ cy.GLO_checkLoadingIndicator();
264
+ if (company) {
265
+ cy.GLO_selectCompany(company);
266
+ }
267
+ cy.get('[data-cy="ng$main_form2.navbar_top"]', { timeout: 120000 }).should('exist');
268
+ });
269
+ Cypress.Commands.add('GLO_clearData', (formulaName) => {
270
+ cy.GLO_searchHeader('Formulas', 'Formulas', 'Formulas');
271
+ cy.GLO_searchDataControl(formulaName);
272
+ cy.GLO_clickDatacontrolBrowserButton('btnEdit');
273
+ cy.GLO_clickButton(['ng$formula_dtl', 'btn_test_formula']);
274
+ cy.task('log', 'Delete records started');
275
+ cy.GLO_clickButton(['ng$test_formula_parameter', 'confirm']);
276
+ cy.GLO_clickDatacontrolBrowserButton('btnSave');
277
+ cy.GLO_closeDashboard();
278
+ cy.GLO_log('Delete records completed');
279
+ });
280
+ Cypress.Commands.add('GLO_clickButton', (selector, buttonType) => {
281
+ cy.GLO_getElement(selector)
282
+ .then(($element) => {
283
+ buttonType ? cy.wrap($element).find(`.${buttonType}`).click() : cy.wrap($element).click();
284
+ })
285
+ .then(() => {
286
+ cy.GLO_checkLoadingIndicator();
287
+ });
288
+ });
289
+ Cypress.Commands.add('GLO_clickButtonText', (textButton) => {
290
+ clickButtonTextHelper(textButton, '.dx-button-content');
291
+ });
292
+ export const clickButtonTextHelper = (textButton, buttonContainer) => {
293
+ cy.get(buttonContainer)
294
+ .contains(textButton)
295
+ .should('not.be.disabled')
296
+ .click()
297
+ .then(() => {
298
+ cy.GLO_checkLoadingIndicator();
299
+ });
300
+ };
301
+ Cypress.Commands.add('GLO_checkField', (selector, value, operator = equal, timeOut = 10000) => {
302
+ if (value === undefined)
303
+ return;
304
+ const { baseSelector, index } = extractSelectorParts(selector);
305
+ const inGrid = isGridRowSelector(baseSelector, index);
306
+ const effectiveSelector = inGrid ? selector : baseSelector;
307
+ const effectiveIndex = inGrid ? 0 : index;
308
+ function assertFieldVal($input) {
309
+ const normalizedValue = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : value;
310
+ let inputVal;
311
+ if (typeof value === 'number') {
312
+ inputVal = $input.find('input[value]').length
313
+ ? Number($input.find('input[value]').eq(effectiveIndex).attr('value'))
314
+ : Number($input.find('.dx-texteditor-input').eq(effectiveIndex).val());
315
+ }
316
+ else {
317
+ // Trim spaces before and after the value, including html space ( ) and other unicode spaces
318
+ inputVal = String($input.find('.dx-texteditor-input').eq(effectiveIndex).val()).replace(/\s+/g, ' ').trim();
319
+ }
320
+ if (operator === equal) {
321
+ expect(inputVal).to.equal(normalizedValue);
322
+ }
323
+ else if (operator === notEqual) {
324
+ expect(inputVal).to.not.equal(normalizedValue);
325
+ }
326
+ else if (operator === contains) {
327
+ expect(inputVal).to.contain(normalizedValue);
328
+ }
329
+ else if (operator === greaterEqual && typeof normalizedValue === 'number' && typeof inputVal === 'number') {
330
+ expect(inputVal).to.be.gte(normalizedValue);
331
+ }
332
+ else {
333
+ throw new Error('Incorrect operator for GLO_checkField. Available operators: equal, notEqual, contains');
334
+ }
335
+ }
336
+ cy.GLO_getElement(effectiveSelector, { timeout: timeOut }).should(assertFieldVal);
337
+ });
338
+ Cypress.Commands.add('GLO_searchHeader', (solutionName, moduleName, programName) => {
339
+ cy.GLO_getElement(['ng$main_form2', 'navbar_top'], { timeout: 30000 }).find('.dx-texteditor-input').type(programName);
340
+ cy.GLO_getElement(['ng$main_form2', 'navbar_top'])
341
+ .find('.dx-texteditor-input')
342
+ .invoke('attr', 'aria-controls')
343
+ .then((dropDownId) => {
344
+ cy.contains(`#${dropDownId} .dx-scrollview-content .dx-item`, `${solutionName} - ${moduleName} - ${programName}`, { timeout: 30000 }).click();
345
+ });
346
+ cy.GLO_checkLoadingIndicator();
347
+ cy.GLO_log(` - Opened the following program: ${solutionName} - ${moduleName} - ${programName}`);
348
+ });
349
+ Cypress.Commands.add('GLO_searchDataControl', (elementName) => {
350
+ cy.GLO_getVisible('globiscomponents-data-control[name=datacontrol]')
351
+ .within(() => {
352
+ cy.get('.dx-texteditor-input').click();
353
+ cy.get('.dx-texteditor-input').type(`{selectall}{del}${elementName}{enter}`);
354
+ })
355
+ .then(() => {
356
+ cy.GLO_checkLoadingIndicator();
357
+ });
358
+ });
359
+ Cypress.Commands.add('GLO_checkLoadingIndicator', () => {
360
+ if (cypressWebsocketLoader) {
361
+ cy.WS_loadingCheck();
362
+ }
363
+ else {
364
+ // biome-ignore plugin: wait before checking loading indicator
365
+ cy.wait(1000);
366
+ cy.get('loading-indicator', { timeout: 1200000 }).should('not.exist');
367
+ cy.get('[data-cy="ng$main_form2.progress"] dx-progress-bar', { timeout: 1200000, log: false }).should('not.exist');
368
+ }
369
+ });
370
+ Cypress.Commands.add('GLO_clickDatacontrolBrowserButton', (buttonName, skipCheck = noSkipCheck) => {
371
+ const datacontrol = 'globiscomponents-data-control[name="datacontrol"]:visible';
372
+ const enabledButton = `${datacontrol} .item-container:has([disabled="false"]:has(#${buttonName}))`;
373
+ const clickAndCheck = () => {
374
+ cy.get(enabledButton, { timeout: 10000 }).click();
375
+ cy.GLO_checkLoadingIndicator();
376
+ // This is needed because loading indicator is unreliable
377
+ if (!skipCheck) {
378
+ switch (buttonName) {
379
+ case 'btnSave':
380
+ case 'btnCancel':
381
+ cy.get('.navigation-item[selected="true"] span.fa-circle.pane-changed', { timeout: 30000 }).should('not.exist');
382
+ break;
383
+ case 'btnAdd':
384
+ case 'btnEdit':
385
+ cy.get('.navigation-item[selected="true"] span.fa-circle.pane-changed', { timeout: 30000 }).should('exist');
386
+ break;
387
+ }
388
+ }
389
+ };
390
+ if (!viewModeButtons.includes(buttonName)) {
391
+ clickAndCheck();
392
+ return;
393
+ }
394
+ // btnList/btnDetail are disabled once their own view is active, and absent in programs without a list/detail toggle: in both cases there is nothing to click.
395
+ cy.get(datacontrol, { timeout: 10000 }).then(($datacontrol) => {
396
+ if (!$datacontrol.find(`.item-container [disabled="false"]:has(#${buttonName})`).length) {
397
+ cy.GLO_log(`${buttonName} is not available or its view is already active: click skipped`);
398
+ return;
399
+ }
400
+ clickAndCheck();
401
+ });
402
+ });
403
+ Cypress.Commands.add('GLO_clickDatacontrolViewerButton', (pageName, buttonName, skipCheck = noSkipCheck) => {
404
+ const IgnoredButtons = ['btnMenu', 'btnDelete', 'btnEdit', 'btnSave', 'btnPrint', 'btnClearAllFilters'];
405
+ // Scroll Into view can't because of same page is opend twice
406
+ cy.GLO_getElement([`ng$${pageName}_tab`, 'datacontrol_viewer'])
407
+ .within(() => {
408
+ cy.get(`#${buttonName}`).closest('.item-container').find('[disabled="false"]', { timeout: 10000 }).click();
409
+ })
410
+ .then(() => {
411
+ if (!IgnoredButtons.includes(buttonName) && !skipCheck)
412
+ cy.get(`div[formslist][hidden] [data-cy$="ng$${pageName}_tab.datacontrol_viewer"]`, { timeout: 60000 }).should('exist');
413
+ cy.GLO_checkLoadingIndicator();
414
+ if (buttonName === 'btnSave' && !skipCheck)
415
+ cy.GLO_getElement([`ng$${pageName}_tab`, 'datacontrol_viewer']).find('[title="Save record"][disabled="true"]', { timeout: 60000 });
416
+ });
417
+ });
418
+ Cypress.Commands.add('GLO_selectCompany', (companyName) => {
419
+ cy.GLO_addInput(['ng2$svy_nav_login', 'company'], companyName);
420
+ cy.get('[data-cy="ng2$svy_nav_login.btnLogin"]').click();
421
+ });
422
+ Cypress.Commands.add('GLO_checkbox', (selector, bool) => {
423
+ if (bool === undefined)
424
+ return;
425
+ checkBoxHelper(selector, bool, 'dx-check-box', '.dx-checkbox-icon');
426
+ cy.GLO_checkLoadingIndicator();
427
+ });
428
+ Cypress.Commands.add('GLO_switch', (selector, bool) => {
429
+ if (bool === undefined)
430
+ return;
431
+ checkBoxHelper(selector, bool, 'dx-switch', '.dx-switch-container');
432
+ cy.GLO_checkLoadingIndicator();
433
+ });
434
+ const checkBoxHelper = (selector, bool, dxElement, dxElementContainer) => {
435
+ // Retry mechanism in case the checkbox is 3-state version and doesn't switch to the desired state on first click.
436
+ const retries = 3;
437
+ const clickElement = (remaining = retries) => {
438
+ return cy
439
+ .GLO_getElement(selector)
440
+ .find(dxElement)
441
+ .filter(':visible')
442
+ .then(($element) => {
443
+ const currentState = $element[0].getAttribute('aria-checked');
444
+ if (currentState === String(bool))
445
+ return;
446
+ if (remaining <= 0)
447
+ throw new Error('Failed to set checkbox/switch to the desired state after multiple attempts.');
448
+ cy.GLO_getElement(selector)
449
+ .find(dxElement)
450
+ .find(dxElementContainer)
451
+ .filter(':visible')
452
+ .click()
453
+ .then(() => {
454
+ cy.GLO_checkLoadingIndicator();
455
+ clickElement(remaining - 1);
456
+ });
457
+ });
458
+ };
459
+ clickElement();
460
+ };
461
+ const GLO_inputValuelist = (selector, rawValue) => {
462
+ if (!rawValue)
463
+ return;
464
+ const selection = String(rawValue);
465
+ // Globis filters only the first part of a string — if '-' is present (compound valuelists) full-string filtering may fail.
466
+ const maxRetries = 3;
467
+ let attempts = 0;
468
+ let unsliced = false;
469
+ let filtered = false;
470
+ let dropdownValue = selection.includes('-') ? selection.split('-')[0].trim() : selection;
471
+ const { baseSelector: vlBaseSelector, index: vlIndex } = extractSelectorParts(selector);
472
+ const vlInGrid = isGridRowSelector(vlBaseSelector, vlIndex);
473
+ const vlEffectiveSelector = vlInGrid ? selector : vlBaseSelector;
474
+ const vlEffectiveIndex = vlInGrid ? 0 : vlIndex;
475
+ const getElement = () => cy.GLO_getElement(vlEffectiveSelector, { log: false });
476
+ // Helper uses cy.document() to escape any .within() scope
477
+ let dropDownId;
478
+ const getDropdown = (dropdownSelector = '', timeout) => {
479
+ return getElement()
480
+ .find('.dx-texteditor-input', { log: false })
481
+ .eq(vlEffectiveIndex)
482
+ .invoke({ log: false }, 'attr', 'aria-controls')
483
+ .then((newId) => {
484
+ if (newId)
485
+ dropDownId = newId;
486
+ const query = dropdownSelector ? `[id="${dropDownId}"] ${dropdownSelector}` : `[id="${dropDownId}"]`;
487
+ return cy
488
+ .document({ timeout: timeout ?? defaultTimeout, log: false })
489
+ .its('body', { log: false })
490
+ .find(query, { log: false });
491
+ });
492
+ };
493
+ cy.GLO_getElement(vlEffectiveSelector).find('.dx-texteditor-input:enabled', { log: false }).eq(vlEffectiveIndex).click();
494
+ getElement()
495
+ .find('.dx-texteditor-input:enabled', { log: false })
496
+ .eq(vlEffectiveIndex)
497
+ .should('have.attr', 'aria-controls')
498
+ .then((id) => {
499
+ dropDownId = id;
500
+ // Wait for dropdown to load its values.
501
+ getDropdown('.dx-scrollable-container')
502
+ .should('exist')
503
+ .then(($el) => {
504
+ if ($el[0].scrollHeight > $el[0].clientHeight) {
505
+ cy.wrap($el, { log: false }).invoke({ log: false }, 'scrollTop', 0);
506
+ cy.wrap($el, { log: false }).should(($e) => {
507
+ expect($e[0].scrollTop).to.eq(0);
508
+ });
509
+ }
510
+ });
511
+ getDropdown(undefined, 30000).should(($el) => {
512
+ assert.exists($el[0]);
513
+ assert.notInclude($el[0].className, 'dx-empty-collection');
514
+ assert.isAbove($el
515
+ .find('.dx-item')
516
+ .toArray()
517
+ .filter((item) => (item.textContent || '').trim() !== '').length, 0);
518
+ });
519
+ getDropdown('.dx-scrollview-content .dx-item')
520
+ .should('exist')
521
+ .then(($list) => {
522
+ if (!clickExactMatch($list))
523
+ attemptDropdownSelect();
524
+ });
525
+ function attemptDropdownSelect() {
526
+ attempts++;
527
+ getDropdown('.dx-scrollview-content .dx-item').as('before', { type: 'static' });
528
+ cy.log(`Attempt __${attempts}__: Selecting dropdown value`);
529
+ getElement()
530
+ .find('.dx-texteditor-input:enabled', { log: false })
531
+ .eq(vlEffectiveIndex)
532
+ .then(($input) => {
533
+ if ($input.val() !== '') {
534
+ cy.GLO_clearField(selector);
535
+ if (filtered) {
536
+ // After clearing a filtered dropdown we check dropdown correctly clears and updates. We then store this cleared version as new baseline 'before'
537
+ cy.get('@before').then(($before) => {
538
+ getDropdown('.dx-scrollview-content .dx-item', 30000)
539
+ .should(($current) => {
540
+ const initialTexts = [...$before].map((el) => el.innerText.trim());
541
+ const currentTexts = [...$current].map((el) => el.innerText.trim());
542
+ const hasChanged = !Cypress._.isEqual(currentTexts, initialTexts);
543
+ // Also accept when the list is the same size or larger: branch-filtered valuelists
544
+ // with few options won't change after clearing because the full list IS the filtered list.
545
+ const listNotSmaller = currentTexts.length >= initialTexts.length;
546
+ return expect(hasChanged || listNotSmaller, 'Dropdown updated after clear').to.be.true;
547
+ })
548
+ .then(() => {
549
+ getDropdown('.dx-scrollview-content .dx-item').as('before', { type: 'static' });
550
+ });
551
+ });
552
+ }
553
+ }
554
+ cy.wrap($input, { log: false }).type(dropdownValue);
555
+ filtered = true;
556
+ });
557
+ // Check if the value is correctly set
558
+ getElement()
559
+ .find('.dx-texteditor-input:enabled', { log: false })
560
+ .eq(vlEffectiveIndex)
561
+ .invoke({ log: false }, 'val')
562
+ .then((value) => {
563
+ if (value !== dropdownValue) {
564
+ // Retry if the value is incorrect
565
+ if (attempts < maxRetries) {
566
+ filtered = false; // Wrong value check doesn't need to wait so we just retry before filtering even has to occur.
567
+ cy.log(`Retrying... (attempt __${attempts}__)`);
568
+ attemptDropdownSelect(); // Recursive call
569
+ }
570
+ else {
571
+ throw new Error(`Failed to set dropdown value after ${maxRetries} attempts`);
572
+ }
573
+ }
574
+ else {
575
+ // Wait for dropdown to update after typing.
576
+ cy.get('@before')
577
+ .then(($before) => {
578
+ getDropdown('.dx-scrollview-content .dx-item', 30000).should(($current) => {
579
+ const initialTexts = [...$before].map((el) => el.innerText.trim());
580
+ const currentTexts = [...$current].map((el) => el.innerText.trim());
581
+ const hasChanged = !Cypress._.isEqual(currentTexts, initialTexts);
582
+ const anyIncludes = currentTexts.every((text) => text.includes(dropdownValue));
583
+ return expect(hasChanged || anyIncludes, `Dropdown updated or contains "${dropdownValue}" already`).to.be.true;
584
+ });
585
+ })
586
+ .then(() => {
587
+ getDropdown('.dx-scrollview-content .dx-item:visible', 15000).then(($list) => {
588
+ // Try to click exact match; if that fails, re-run with full value instead of sliced value.
589
+ if (!clickExactMatch($list) && !unsliced) {
590
+ dropdownValue = selection;
591
+ attempts = 0;
592
+ unsliced = true;
593
+ attemptDropdownSelect();
594
+ }
595
+ });
596
+ });
597
+ }
598
+ });
599
+ }
600
+ const clickExactMatch = ($list) => {
601
+ const normalizeText = (el) => Cypress.$(el).text().replace(/\s+/g, ' ').trim();
602
+ const normalizedTarget = selection.replace(/\s+/g, ' ').trim();
603
+ const $filteredList = $list.filter((_, el) => normalizeText(el) !== '');
604
+ const exactMatch = $filteredList.filter((_, el) => normalizeText(el) === normalizedTarget);
605
+ if ($filteredList.length === 0) {
606
+ cy.log(`__No options available in dropdown to select "${selection}"__`);
607
+ throw new Error(`No options available in dropdown to select "${selection}"`);
608
+ }
609
+ // If _all_ values of the dropdown currently already contain targetText, we can assume to click the partial match since no further filtering will happen.
610
+ const allContain = $filteredList.length > 0 && $filteredList.toArray().every((el) => normalizeText(el).toLowerCase().includes(normalizedTarget.toLowerCase()));
611
+ const hasMatch = exactMatch.length > 0 || allContain;
612
+ if (hasMatch) {
613
+ const matchedText = exactMatch.length > 0 ? normalizeText(exactMatch[0]) : normalizeText($filteredList[0]);
614
+ cy.log(`__Clicking ${exactMatch.length > 0 ? 'exact' : 'partial'} match: "${matchedText}"__`);
615
+ // Using cy.wrap(match[0]) can give issue that element updated, so we need to 're-find' it
616
+ getDropdown('.dx-scrollview-content .dx-item')
617
+ .filter((_, el) => (exactMatch.length > 0 ? normalizeText(el) === normalizedTarget : normalizeText(el).includes(normalizedTarget)))
618
+ .first()
619
+ .click({ log: false });
620
+ getDropdown('.dx-overlay-content.dx-popup-normal[role="dialog"]').should('not.exist'); // Ensure the dropdown is completely handled/closed after selection
621
+ getElement().then(($el) => {
622
+ if (!$el.find('.dx-tagbox').length)
623
+ cy.GLO_checkField(selector, selection, exactMatch.length === 0 ? contains : undefined);
624
+ });
625
+ return true;
626
+ }
627
+ else {
628
+ cy.log(`__No match found for "${selection}"__`);
629
+ return false;
630
+ }
631
+ };
632
+ });
633
+ };
634
+ Cypress.Commands.add('GLO_closeDashboard', () => {
635
+ cy.task('get', ['testFailed'], { log: false }).then(({ testFailed }) => {
636
+ if (testFailed)
637
+ return;
638
+ cy.get('.navigation-item-close', { timeout: 10000 }).click();
639
+ cy.GLO_getVisible('.svy-message-dialog', 30000).should('exist');
640
+ cy.get('.svy-message-dialog').should(($dialog) => {
641
+ expect($dialog.find('.window-title').text().trim()).to.match(/^(Confirm|Bevestigen)$/);
642
+ expect($dialog.find('.window-body').text().trim()).to.match(/^(Close all programs\?|Sluit alle programma's\?)$/);
643
+ });
644
+ cy.get('.svy-message-dialog')
645
+ .find('.svy-btn-primary')
646
+ .contains(/^(Yes|Ja)$/)
647
+ .should('exist')
648
+ .click();
649
+ cy.GLO_checkLoadingIndicator();
650
+ });
651
+ });
652
+ Cypress.Commands.add('GLO_selectTab', (selector, tabName, openOrClose) => {
653
+ cy.GLO_getElement(selector)
654
+ .contains('[tabcontainer] .tab', tabName)
655
+ .then(($tab) => {
656
+ const isOpenAttr = $tab[0].getAttribute('open'); // Attribute is a string 'true' or missing -> null
657
+ const isOpen = isOpenAttr === 'true'; // Convert to boolean
658
+ if (openOrClose !== isOpen) {
659
+ const action = openOrClose ? 'Opening' : 'Closing';
660
+ cy.log(`${action} tab ${tabName}`);
661
+ cy.wrap($tab).click();
662
+ cy.GLO_checkLoadingIndicator();
663
+ }
664
+ else {
665
+ cy.log(`Tab ${tabName} is already ${isOpen ? 'open' : 'closed'}.`);
666
+ }
667
+ });
668
+ });
669
+ Cypress.Commands.add('GLO_selectMenuButton', (buttonName, exactMatch = noEqualBool) => {
670
+ const searchParam = exactMatch ? new RegExp(`^\\s*${Cypress._.escapeRegExp(buttonName)}\\s*$`) : buttonName;
671
+ cy.GLO_clickDatacontrolBrowserButton('btnMenu');
672
+ cy.GLO_getVisible('.dx-submenu', 10000)
673
+ .contains(searchParam)
674
+ .click()
675
+ .then(() => {
676
+ cy.GLO_checkLoadingIndicator();
677
+ });
678
+ cy.GLO_log(` - Used workflow button '${buttonName}'`);
679
+ });
680
+ Cypress.Commands.add('GLO_selectMenuButtonControlView', (tabName, buttonName, exactMatch = noEqualBool) => {
681
+ const searchParam = exactMatch ? new RegExp(`^\\s*${Cypress._.escapeRegExp(buttonName)}\\s*$`) : buttonName;
682
+ cy.GLO_clickDatacontrolViewerButton(tabName, 'btnMenu');
683
+ cy.GLO_getVisible('.dx-submenu', 10000)
684
+ .contains(searchParam)
685
+ .click()
686
+ .then(() => {
687
+ cy.GLO_checkLoadingIndicator();
688
+ });
689
+ cy.GLO_log(` - Used workflow button '${buttonName}'`);
690
+ });
691
+ const navItem = (topBottom, programName, tabNumber, mode) => {
692
+ if (topBottom !== top && topBottom !== bottom)
693
+ throw new Error(`Incorrect type for GLO_${mode}Program. Available types: top, bottom`);
694
+ const location = topBottom === top ? 0 : 1;
695
+ const regex = new RegExp(`^${Cypress._.escapeRegExp(programName)}$`);
696
+ cy.get('drag-scroll')
697
+ .eq(location)
698
+ .within(() => {
699
+ const items = cy
700
+ .get('.navigation-item:not([hidden]) span:not([close-btn])')
701
+ .filter((_, el) => regex.test(el.textContent.trim()))
702
+ .parent();
703
+ const item = tabNumber !== undefined ? items.eq(tabNumber) : items.last();
704
+ if (mode === 'close')
705
+ item.find('[close-btn]').click();
706
+ else
707
+ item.find('span:not([close-btn])').first().click();
708
+ });
709
+ cy.GLO_checkLoadingIndicator();
710
+ };
711
+ Cypress.Commands.add('GLO_closeProgram', (topBottom, programName, tabNumber) => {
712
+ navItem(topBottom, programName, tabNumber, 'close');
713
+ cy.GLO_log(` - Closed program tab ${programName}`);
714
+ });
715
+ Cypress.Commands.add('GLO_dialogChoice', (confirmation) => {
716
+ // biome-ignore plugin: wait for dialog choice body to render
717
+ cy.wait(500);
718
+ cy.get('body').then(($body) => {
719
+ let CSSSearchStr;
720
+ if ($body.find('.svy-message-dialog').length > 0) {
721
+ CSSSearchStr = '.svy-message-dialog button';
722
+ }
723
+ else if ($body.find('.servoycore-dialog-body').length > 0) {
724
+ // Previous version
725
+ CSSSearchStr = '.servoycore-dialog-body button';
726
+ }
727
+ else if ($body.find('.svy-dialog.window').length > 0) {
728
+ // Another variant
729
+ CSSSearchStr = '.svy-dialog.window globiscomponents-button';
730
+ }
731
+ else {
732
+ cy.GLO_log('Popup not found, skipping dialog option');
733
+ return;
734
+ }
735
+ if ($body.find('.window-title.type-error').length > 0 && $body.find('.window-title.type-error').is(':visible')) {
736
+ cy.GLO_log('Error dialog, failing test. use GLO_dialogRequiredCheck.');
737
+ cy.wrap(null).should(() => {
738
+ throw new Error('Error dialog, failing test. use GLO_dialogRequiredCheck.');
739
+ });
740
+ }
741
+ let found = false;
742
+ cy.get(CSSSearchStr)
743
+ .each(($btn) => {
744
+ if ($btn.text().includes(confirmation) && !found) {
745
+ found = true; // Skip other buttons when button has been found
746
+ cy.wrap($btn)
747
+ .contains(confirmation)
748
+ .click()
749
+ .then(() => {
750
+ cy.GLO_checkLoadingIndicator();
751
+ });
752
+ }
753
+ })
754
+ .then(() => {
755
+ if (!found) {
756
+ cy.GLO_log('Popup button not found, skipping dialog option');
757
+ }
758
+ });
759
+ });
760
+ });
761
+ Cypress.Commands.add('GLO_addInputDialog', (text) => {
762
+ cy.get('.svy-message-dialog .window-body form', { timeout: 15000 }).then(($form) => {
763
+ const inputType = $form.children().first().prop('tagName').toLowerCase();
764
+ if (inputType === 'input') {
765
+ cy.wrap($form).find('input').clear().type(text);
766
+ }
767
+ else if (inputType === 'select') {
768
+ cy.wrap($form)
769
+ .find('select')
770
+ .select(text)
771
+ .should(($select) => {
772
+ expect(String($select.val()).trim()).to.eq(text);
773
+ });
774
+ }
775
+ else {
776
+ throw new Error('Unsupported input type');
777
+ }
778
+ });
779
+ });
780
+ const GLO_inputInput = (selector, value, options = {}) => {
781
+ const { selectAllDelete, ...typeOptions } = options;
782
+ const { baseSelector, index } = extractSelectorParts(selector);
783
+ const inGrid = isGridRowSelector(baseSelector, index);
784
+ cy.GLO_getElement(inGrid ? selector : baseSelector, { log: false, ...typeOptions }).then(($el) => {
785
+ const $target = $el.find('.dx-texteditor-input:not([disabled])').eq(inGrid ? 0 : index);
786
+ const isTextarea = $target.is('textarea');
787
+ if (selectAllDelete)
788
+ cy.wrap($target, { log: false }).clear();
789
+ else
790
+ cy.GLO_clearField(selector);
791
+ cy.wrap($target, { log: false }).type(value, typeOptions);
792
+ if (!isTextarea)
793
+ cy.wrap($target, { log: false }).type('{enter}');
794
+ });
795
+ };
796
+ const GLO_inputMonaco = (dataUri, value) => {
797
+ cy.window().then((win) => {
798
+ const monaco = win.monaco;
799
+ const model = monaco.editor.getModels().find((m) => m.uri.toString() === dataUri);
800
+ if (model)
801
+ model.setValue(value);
802
+ else
803
+ throw new Error(`No monaco model found for ${dataUri}`);
804
+ });
805
+ };
806
+ Cypress.Commands.add('GLO_addInput', (selector, value, options) => {
807
+ if (value === undefined)
808
+ return;
809
+ else if (typeof value === 'number')
810
+ value = String(value);
811
+ else if (typeof value === 'object')
812
+ value = JSON.stringify(value);
813
+ options = options || {};
814
+ const { selectAllDelete, ...getOptions } = options;
815
+ const { baseSelector, index } = extractSelectorParts(selector);
816
+ const inGrid = isGridRowSelector(baseSelector, index);
817
+ cy.GLO_getElement(inGrid ? selector : baseSelector, getOptions).then(($el) => {
818
+ const isDropdown = $el
819
+ .find('.dx-texteditor-input:not([disabled])')
820
+ .eq(inGrid ? 0 : index)
821
+ .closest('dx-select-box, dx-tag-box').length > 0;
822
+ if ($el.parent().is('globiscomponents-monacoeditor')) {
823
+ if (selectAllDelete)
824
+ throw new Error('Error: selectAllDelete option is not applicable for monaco inputs and will be ignored.');
825
+ cy.wrap($el, { timeout: 10000 })
826
+ .find('.monaco-editor', { timeout: 10000 })
827
+ .invoke({ timeout: 10000 }, 'attr', 'data-uri')
828
+ .should('exist')
829
+ .then((dataUri) => {
830
+ GLO_inputMonaco(String(dataUri), value);
831
+ });
832
+ }
833
+ else if (isDropdown) {
834
+ if (selectAllDelete)
835
+ throw new Error('Error: selectAllDelete option is not applicable for valuelist inputs and will be ignored.');
836
+ GLO_inputValuelist(selector, value);
837
+ }
838
+ else {
839
+ GLO_inputInput(selector, value, options);
840
+ }
841
+ });
842
+ });
843
+ Cypress.Commands.add('GLO_addDate', (type, selector, value) => {
844
+ if (!value)
845
+ return;
846
+ const dateParts = value.split('/');
847
+ switch (type) {
848
+ case input:
849
+ cy.GLO_addInput(selector, value);
850
+ break;
851
+ case select: // Select from popup
852
+ if (dateParts[0].length === 1)
853
+ dateParts[0] = `0${dateParts[0]}`;
854
+ cy.GLO_getElement(selector).find('.dx-dropdowneditor-icon').click();
855
+ // Select correct year
856
+ selectYear(Number(dateParts[2]));
857
+ // Select correct month
858
+ selectMonth(Number(dateParts[1]));
859
+ cy.GLO_getVisible('.dx-calendar-body')
860
+ .last()
861
+ .within(() => {
862
+ cy.get('tr:visible').find(`[data-value="${dateParts[2]}/${dateParts[1]}/${dateParts[0]}"]`).filter(':not(".dx-calendar-other-month")').click();
863
+ });
864
+ break;
865
+ default:
866
+ throw new Error('Incorrect type for GLO_addDate. Available types: input, inputName, select, selectName');
867
+ }
868
+ });
869
+ Cypress.Commands.add('GLO_uploadFile', (selector, relativePath) => {
870
+ cy.GLO_getElement(selector)
871
+ .find('input[type=file]')
872
+ .selectFile(relativePath, { force: true })
873
+ .then(() => {
874
+ cy.GLO_checkLoadingIndicator();
875
+ });
876
+ });
877
+ Cypress.Commands.add('GLO_uploadFileDialog', (relativePath) => {
878
+ cy.GLO_getVisible('div.svy-upload.active').find('input[type=file]').selectFile(relativePath, { force: true, action: 'drag-drop' });
879
+ cy.GLO_checkLoadingIndicator();
880
+ cy.GLO_getVisible('button.svy-upload-btn-upload').click();
881
+ cy.GLO_checkLoadingIndicator();
882
+ });
883
+ Cypress.Commands.add('GLO_filterGrid', (colId, selector, text, exactMatch = noEqualBool, filterType) => {
884
+ if (text === undefined)
885
+ return;
886
+ else if (typeof text === 'object')
887
+ text = JSON.stringify(text);
888
+ const textString = String(text);
889
+ const searchParam = exactMatch ? new RegExp(`^\\s*${Cypress._.escapeRegExp(textString)}\\s*$`) : textString;
890
+ cy.GLO_getElement(selector).within(() => {
891
+ cy.get(`.ag-header-cell[col-id="${colId}"]`).as('filterHeaderCell');
892
+ cy.get('@filterHeaderCell').find('input:not(:where([hidden] *))').filter(':enabled').clear().as('COM_filterGridInput');
893
+ if (text !== '')
894
+ cy.get('@COM_filterGridInput').type(textString);
895
+ });
896
+ cy.get('@COM_filterGridInput').then(($element) => {
897
+ if ($element[0].getAttribute('role') === 'combobox') {
898
+ cy.get('ngb-typeahead-window').find('button').contains(searchParam).click();
899
+ }
900
+ else {
901
+ cy.get('@COM_filterGridInput').type('{enter}');
902
+ }
903
+ });
904
+ if (filterType) {
905
+ cy.get('@filterHeaderCell').find('button').click();
906
+ cy.get('.ag-filter-menu .ag-filter-select .ag-picker-field-display').click();
907
+ // biome-ignore plugin: special dropdown for filter, doesn't work without small wait
908
+ cy.wait(250); // Special dropdown for filter, doesn't work without small wait..
909
+ cy.get('.ag-filter-menu .ag-picker-expanded')
910
+ .invoke('attr', 'aria-controls')
911
+ .then((dropDownId) => {
912
+ cy.get(`[id="${dropDownId}"]`)
913
+ .should('be.visible')
914
+ .within(() => {
915
+ cy.get('.ag-list-item').contains(filterType).should('be.visible').parent().click();
916
+ });
917
+ });
918
+ cy.GLO_checkAGLoader();
919
+ cy.get('.ag-filter-menu .ag-filter-select .ag-picker-field-display').should('contain.text', filterType);
920
+ cy.press(Cypress.Keyboard.Keys.ESC); // Close the filter menu
921
+ }
922
+ cy.GLO_checkLoadingIndicator();
923
+ cy.GLO_checkAGLoader();
924
+ });
925
+ Cypress.Commands.add('GLO_filterGridCustomView', (selector, headerName, text) => {
926
+ cy.GLO_getElement(selector).within(() => {
927
+ cy.contains('div.ag-header-cell', headerName)
928
+ .invoke('attr', 'aria-colindex')
929
+ .then((ariaColIndex) => {
930
+ cy.get('.ag-header-row-filter').find(`[aria-colindex="${ariaColIndex}"]`).find('input').filter(':enabled').clear().as('COM_filterGridInput');
931
+ if (text !== undefined && text !== '')
932
+ cy.get('.ag-header-row-filter').find(`[aria-colindex="${ariaColIndex}"]`).find('input').filter(':enabled').type(String(text));
933
+ });
934
+ });
935
+ cy.get('body').then(($body) => {
936
+ if ($body.find('ngb-typeahead-window').length > 0) {
937
+ cy.get('ngb-typeahead-window').find('button').contains(String(text)).click();
938
+ }
939
+ });
940
+ cy.GLO_checkAGLoader();
941
+ });
942
+ Cypress.Commands.add('GLO_switchCompany', (companyName) => {
943
+ cy.GLO_getElement(['ng$main_form2', 'navbar_top']).find('#userroles-dropdown').click();
944
+ cy.get('.dx-submenu').find(span).contains('Switch company').click();
945
+ cy.GLO_checkLoadingIndicator();
946
+ cy.get('.svy-dialog').find('.dx-dropdowneditor-icon').click();
947
+ cy.GLO_getVisible('.dx-scrollview-content').contains(companyName).click();
948
+ cy.GLO_clickButton(['ng$login_companies_dlg', 'btn_save']);
949
+ });
950
+ Cypress.Commands.add('GLO_getRole', (aliasName) => {
951
+ cy.GLO_getVisible('[userrole]').invoke('text').as(aliasName, { type: 'static' });
952
+ });
953
+ Cypress.Commands.add('GLO_checkValueGrid', (value, rowFilter, columnId, timeOut = 10000, interval, operator) => {
954
+ if (timeOut > 60000 && !interval)
955
+ interval = 10000;
956
+ validateGridValue(value, undefined, rowFilter, columnId, timeOut, interval, operator);
957
+ });
958
+ const selectYear = (targetYear) => {
959
+ cy.GLO_getVisible('.dx-calendar-navigator')
960
+ .last()
961
+ .find('span')
962
+ .invoke('text')
963
+ .then((currentDateJQuery) => {
964
+ const currentDate = currentDateJQuery;
965
+ const currentYear = Number(currentDate.split(' ')[1]);
966
+ if (currentYear < targetYear) {
967
+ cy.GLO_getVisible('i.dx-icon.dx-icon-chevronright').last().click();
968
+ // biome-ignore plugin: wait for year navigation click to register
969
+ cy.wait(500);
970
+ selectYear(targetYear);
971
+ }
972
+ else if (currentYear > targetYear) {
973
+ cy.GLO_getVisible('i.dx-icon.dx-icon-chevronleft').last().click();
974
+ // biome-ignore plugin: wait for year navigation click to register
975
+ cy.wait(500);
976
+ selectYear(targetYear);
977
+ }
978
+ // If Correct year do nothing
979
+ });
980
+ };
981
+ const selectMonth = (targetMonth) => {
982
+ cy.GLO_getVisible('.dx-calendar-navigator')
983
+ .last()
984
+ .find('span')
985
+ .invoke('text')
986
+ .then((currentDateJQuery) => {
987
+ const currentDate = currentDateJQuery;
988
+ const currentMonth = currentDate.split(' ')[0];
989
+ const currenMonthNumber = monthMap.get(currentMonth);
990
+ if (!currenMonthNumber)
991
+ throw new Error('Given month is invalid');
992
+ if (currenMonthNumber < targetMonth) {
993
+ cy.GLO_getVisible('i.dx-icon.dx-icon-chevronright').last().click();
994
+ // biome-ignore plugin: wait for month navigation click to register
995
+ cy.wait(500);
996
+ selectMonth(targetMonth);
997
+ }
998
+ else if (currenMonthNumber > targetMonth) {
999
+ cy.GLO_getVisible('i.dx-icon.dx-icon-chevronleft').last().click();
1000
+ // biome-ignore plugin: wait for month navigation click to register
1001
+ cy.wait(500);
1002
+ selectMonth(targetMonth);
1003
+ }
1004
+ // If Correct month do nothing
1005
+ });
1006
+ };
1007
+ Cypress.Commands.add('GLO_selectProgram', (topBottom, programName, tabnumber) => {
1008
+ navItem(topBottom, programName, tabnumber, 'select');
1009
+ });
1010
+ Cypress.Commands.add('GLO_getRowInGrid', (rowFilter) => {
1011
+ const rows = cy.get('.ag-center-cols-container [row-index]').filter(isInOpenContainer);
1012
+ return typeof rowFilter === 'number' ? rows.eq(rowFilter) : rows.filter(`:contains("${rowFilter}")`).first();
1013
+ });
1014
+ Cypress.Commands.add('GLO_selectRowInGrid', (rowFilter) => {
1015
+ let found = false;
1016
+ if (typeof rowFilter === 'number') {
1017
+ cy.get(`[row-id="root_${rowFilter}"]`).filter(isInOpenContainer).find('[data-ref="eInput"]').click();
1018
+ return;
1019
+ }
1020
+ // Convert to array if it's a string
1021
+ const filters = typeof rowFilter === 'string' ? [rowFilter] : rowFilter;
1022
+ // Iterate through each row and check if it matches the filter
1023
+ cy.get('[row-index]')
1024
+ .filter(isInOpenContainer)
1025
+ .each(($row) => {
1026
+ const cellTexts = Array.from($row.find('.ag-cell-value')).map((el) => el.innerText.trim());
1027
+ // Check to see if all requested values are present in any of the cells.
1028
+ const matches = filters.every((filterValue) => cellTexts.some((val) => val.includes(filterValue)));
1029
+ if (matches) {
1030
+ found = true;
1031
+ const rowId = $row.attr('row-id');
1032
+ // No isInOpenContainer here: `.ag-root` is the grid of $row, which already passed that
1033
+ // filter above, so everything inside shares its ancestors. The re-query is only needed
1034
+ // because the checkbox lives in the pinned-left twin carrying the same row-id.
1035
+ cy.wrap($row)
1036
+ .closest('.ag-root')
1037
+ .within(() => {
1038
+ cy.get(`[row-id="${rowId}"]`).find('[data-ref="eInput"]').click();
1039
+ });
1040
+ return false; // Stop after the first match
1041
+ }
1042
+ })
1043
+ .then(() => {
1044
+ if (!found) {
1045
+ throw new Error(`No row found matching all of the following values: ${filters.join(', ')}`);
1046
+ }
1047
+ });
1048
+ });
1049
+ Cypress.Commands.add('GLO_dblClickRowInGrid', (type, selector, value, columnId, skipCheck = noSkipCheck, equals = noEqualBool) => {
1050
+ if (value === undefined)
1051
+ return;
1052
+ cy.GLO_getElement(selector)
1053
+ .within(() => {
1054
+ dblClickRowInGrid(type, value, columnId, equals);
1055
+ })
1056
+ .then(() => {
1057
+ if (!skipCheck) {
1058
+ cy.get('@COM_compId')
1059
+ .then((compId) => {
1060
+ cy.get(`[row-index][comp-id="${compId}"]`, { timeout: 300000 }).should('not.be.visible');
1061
+ })
1062
+ .then(() => {
1063
+ cy.GLO_checkLoadingIndicator();
1064
+ });
1065
+ }
1066
+ else {
1067
+ cy.GLO_checkLoadingIndicator();
1068
+ }
1069
+ });
1070
+ });
1071
+ /**
1072
+ *
1073
+ * This a a helper function that will handle all action inside the within
1074
+ * This is seperated because somtimes we don't need to be inside a within see popupwindow
1075
+ */
1076
+ const dblClickRowInGrid = (type, value, columnId, equals) => {
1077
+ const saveValues = ($div) => {
1078
+ const rowIndex = $div.attr('row-index');
1079
+ const compId = $div.attr('comp-id');
1080
+ const selector = `[row-index="${rowIndex}"][comp-id="${compId}"]`;
1081
+ cy.wrap(selector).as('COM_selectedRowSelector');
1082
+ cy.wrap(compId).as('COM_compId');
1083
+ };
1084
+ const helper = (filterSelector) => {
1085
+ if (columnId) {
1086
+ cy.get('[row-index]')
1087
+ .find(`[col-id="${columnId}"]`)
1088
+ .filter(filterSelector, { timeout: 10000 })
1089
+ .parentsUntil('.ag-center-cols-container')
1090
+ .filter('[row-index]')
1091
+ .first()
1092
+ .then(saveValues);
1093
+ }
1094
+ else {
1095
+ cy.get('[row-index]').filter(filterSelector, { timeout: 10000 }).first().then(saveValues);
1096
+ }
1097
+ };
1098
+ cy.GLO_checkAGLoader();
1099
+ switch (type) {
1100
+ case row:
1101
+ cy.GLO_getRowInGrid(Number(value)).then(saveValues);
1102
+ break;
1103
+ case span:
1104
+ //* Equals(not working for Array)
1105
+ if (equals) {
1106
+ if (Array.isArray(value))
1107
+ throw new Error('Cannot use an array together with equals');
1108
+ const regex = new RegExp(`^\\s*${String(value)}\\s*$`);
1109
+ cy.get('[row-index]:visible')
1110
+ .find(columnId ? `[col-id="${columnId}"] ${span}` : span)
1111
+ .contains(regex, { timeout: 10000 })
1112
+ .parentsUntil('.ag-center-cols-container')
1113
+ .filter('[row-index]')
1114
+ .first()
1115
+ .then(saveValues);
1116
+ }
1117
+ else if (Array.isArray(value)) {
1118
+ //* Array
1119
+ if (columnId)
1120
+ throw new Error('Cannot use an array together with columnId');
1121
+ const spansArray = String(value)
1122
+ .split(',')
1123
+ .map((span) => span.trim());
1124
+ const filterSelector = spansArray.map((span) => `:contains("${span}")`).join('');
1125
+ helper(filterSelector);
1126
+ }
1127
+ else {
1128
+ //* One span
1129
+ helper(`:contains("${value}")`);
1130
+ }
1131
+ break;
1132
+ default:
1133
+ throw new Error('Incorrect type for GLO_dblClickRowInGrid. Available types: row, span');
1134
+ }
1135
+ // The actual dblClick on the row
1136
+ cy.get('@COM_selectedRowSelector').then((selector) => {
1137
+ columnId ? cy.get(selector).find(`[col-id="${columnId}"]`).dblclick() : cy.get(selector).find('[aria-colindex]').eq(1).dblclick();
1138
+ });
1139
+ };
1140
+ Cypress.Commands.add('GLO_clickRowInGrid', (value, columnId) => {
1141
+ if (value === undefined)
1142
+ throw new Error('Value missing for GLO_clickRowInGrid');
1143
+ columnId ? cy.GLO_getRowInGrid(value).find(`[col-id="${columnId}"]`).click() : cy.GLO_getRowInGrid(value).children().first().click();
1144
+ cy.GLO_checkLoadingIndicator();
1145
+ });
1146
+ // Shared popup-window search + grid selection logic used by GLO_popupWindow.
1147
+ // Assumes the search glass has already been clicked and the popup (`.window-body`) is open.
1148
+ const selectFromPopupWindow = (value, columnId, equals = noEqualBool) => {
1149
+ const loadTimeout = 30000; // 30 seconds
1150
+ // Helper consts to be able to check if grid has been updated with new data.
1151
+ // Window-body is popup element
1152
+ // Dynamically get the first columnId if there is no columnId given.
1153
+ const loadingColumnId = () => columnId
1154
+ ? cy.wrap(columnId)
1155
+ : cy
1156
+ .get('.window-body:visible .ag-center-cols-container [row-index="0"] [role="gridcell"]', { timeout: 30000 })
1157
+ .filter((_, el) => Cypress.$(el).text().trim().length > 0)
1158
+ .first()
1159
+ .invoke('attr', 'col-id');
1160
+ // So we dont have to repeat the whole command everywhere, just looks better in code below.
1161
+ const searchData = () => loadingColumnId().then((colId) => cy.get(`.window-body [row-index] [col-id="${colId}"]`, { timeout: loadTimeout }).should(($els) => {
1162
+ assert.equal($els.toArray().filter((item) => (item.textContent || '').trim() === '').length, 0, 'Grid cells should not have empty values');
1163
+ assert.isTrue(Cypress.$('.ag-loading').length === 0 && Cypress.$('loading-indicator').length === 0, '(Grid) loadingindicator is not visible');
1164
+ }));
1165
+ // Actual function that checks if the grid has loaded and changed.
1166
+ const assertGridLoadedAndChanged = (beforeSearch) => ($afterSearch) => {
1167
+ const noLoadingIndicator = Cypress.$('loading-indicator').length === 0;
1168
+ const noAGLoader = Cypress.$('.ag-loading').length === 0;
1169
+ assert.isTrue(noAGLoader && noLoadingIndicator, '(Grid) loadingindicator is not visible');
1170
+ const afterTexts = Array.from($afterSearch, (el) => el.textContent.trim());
1171
+ const hasData = afterTexts.some((val) => !!val);
1172
+ const newData = !Cypress._.isEqual(afterTexts, beforeSearch);
1173
+ assert.isTrue(hasData && newData, 'Grid stabilized with new data');
1174
+ };
1175
+ // Get the state of the entries before searching. Save as an array of strings so its less heavy on the memory.
1176
+ searchData()
1177
+ .then(($els) => Array.from($els, (el) => el.textContent.trim()))
1178
+ .as('beforeSearch');
1179
+ cy.get('@beforeSearch').then((beforeSearch) => {
1180
+ // If the value is already in the visible grid we can skip the whole search.
1181
+ if (!beforeSearch.includes(String(value))) {
1182
+ cy.GLO_getElement(['svy_nav_selection', 'search_criteria']).type(String(value));
1183
+ cy.GLO_getElement(['svy_nav_selection', 'btn_search']).click();
1184
+ searchData().should(assertGridLoadedAndChanged(beforeSearch));
1185
+ if (columnId) {
1186
+ cy.GLO_getVisible('[row-index]')
1187
+ .find(`[col-id="${columnId}"]`)
1188
+ .then(($list) => {
1189
+ // Only fill in columnId filter if there is no exact match in said column yet.
1190
+ const match = $list.filter((_, el) => new RegExp(`^\\s*${Cypress._.escapeRegExp(String(value))}\\s*$`).test(Cypress.$(el).text().trim()));
1191
+ if (match.length !== 1) {
1192
+ searchData()
1193
+ .then(($els) => Array.from($els, (el) => el.textContent.trim()))
1194
+ .as('beforeSearch');
1195
+ cy.GLO_getVisible('.window-body').within(() => {
1196
+ cy.GLO_getVisible(`.ag-header-cell[col-id="${columnId}"]`)
1197
+ .invoke('attr', 'aria-colindex')
1198
+ .then((ariaColIndex) => {
1199
+ cy.get('.ag-header-row-filter').find(`[aria-colindex="${ariaColIndex}"]`).find('input').filter(':enabled').clear().type(String(value));
1200
+ });
1201
+ });
1202
+ cy.get('body').then(($body) => {
1203
+ if ($body.find('ngb-typeahead-window').length > 0) {
1204
+ cy.get('ngb-typeahead-window').find('button').contains(String(value)).click();
1205
+ }
1206
+ });
1207
+ searchData().should(assertGridLoadedAndChanged(beforeSearch));
1208
+ }
1209
+ else {
1210
+ cy.log('No columnId filtering needed, skipping.');
1211
+ }
1212
+ });
1213
+ }
1214
+ cy.GLO_getVisible('.window-body').within(() => {
1215
+ dblClickRowInGrid(span, value, columnId, equals);
1216
+ });
1217
+ }
1218
+ else {
1219
+ cy.log('Value already exists in the grid, skipping search.');
1220
+ cy.GLO_getVisible('.window-body').within(() => {
1221
+ dblClickRowInGrid(span, value, columnId, equals);
1222
+ });
1223
+ }
1224
+ cy.get('@COM_compId')
1225
+ .then((compId) => {
1226
+ cy.get(`[row-index][comp-id="${compId}"]`, { timeout: 10000 }).should('not.exist');
1227
+ })
1228
+ .then(() => {
1229
+ cy.GLO_checkLoadingIndicator();
1230
+ });
1231
+ });
1232
+ };
1233
+ Cypress.Commands.add('GLO_popupWindow', (selector, value, columnId, equals = noEqualBool, useMagnifierFlag = buttonSearch) => {
1234
+ if (!value)
1235
+ return;
1236
+ if (useMagnifierFlag) {
1237
+ cy.GLO_clearField(selector);
1238
+ cy.GLO_clickButton(selector, 'fa-search');
1239
+ }
1240
+ else {
1241
+ // Clear the input itself rather than through GLO_clearField: on a selector field the
1242
+ // clear icon can be pressed without the value going away, and the typed value then lands
1243
+ // behind the old one.
1244
+ cy.GLO_addInput(selector, value, { selectAllDelete: true });
1245
+ cy.GLO_checkLoadingIndicator();
1246
+ }
1247
+ cy.get('body').then(($body) => {
1248
+ if ($body.find('.window-body').filter(':visible').length > 0) {
1249
+ selectFromPopupWindow(value, columnId, equals);
1250
+ }
1251
+ else {
1252
+ cy.GLO_checkField(selector, value, equals ? contains : undefined);
1253
+ }
1254
+ });
1255
+ });
1256
+ Cypress.Commands.add('GLO_selectMenuButtonLeft', (buttonName) => {
1257
+ cy.GLO_getElement(['ng$main_form2', 'navbar_left']).within(() => {
1258
+ switch (buttonName) {
1259
+ case 'home':
1260
+ cy.get('[class="lucide lucide-house"]').click();
1261
+ break;
1262
+ case 'settings':
1263
+ cy.get('[class="lucide lucide-settings"]').click();
1264
+ break;
1265
+ case 'help':
1266
+ cy.get('[class="lucide lucide-circle-question-mark"]').click();
1267
+ break;
1268
+ case 'support':
1269
+ cy.get('[class="lucide lucide-headphones"]').click();
1270
+ break;
1271
+ case 'excel':
1272
+ cy.get('[class="lucide lucide-file-x"]').click();
1273
+ break;
1274
+ case 'info':
1275
+ cy.get('[class="lucide lucide-info"]').click();
1276
+ break;
1277
+ case 'refresh':
1278
+ cy.get('[class="lucide lucide-rotate-cw"]').click();
1279
+ break;
1280
+ case 'query':
1281
+ cy.get('[class="lucide lucide-database"]').click();
1282
+ break;
1283
+ case 'clients':
1284
+ cy.get('[class="lucide lucide-users"]').click();
1285
+ break;
1286
+ case 'users':
1287
+ cy.get('[class="lucide lucide-user-cog"]').click();
1288
+ break;
1289
+ case 'theme':
1290
+ cy.get('[class="lucide lucide-sun"]').click();
1291
+ break;
1292
+ default:
1293
+ throw new Error('Incorrect type for GLO_selectMenuButtonLeft. Available types: home, settings, excel, info, refresh');
1294
+ }
1295
+ });
1296
+ cy.GLO_checkLoadingIndicator();
1297
+ });
1298
+ Cypress.Commands.add('GLO_changeUser', (username) => {
1299
+ cy.GLO_selectMenuButtonLeft('users');
1300
+ cy.GLO_addInput(['ng$log_in_as_other_user_dlg', 'sec_user_id'], username);
1301
+ cy.GLO_checkLoadingIndicator();
1302
+ cy.GLO_clickButton(['ng$log_in_as_other_user_dlg', 'btn_ok']);
1303
+ cy.get('body', { timeout: 30000 }).find('[data-cy$="ng$log_in_as_other_user_dlg.btn_ok"]', { timeout: 30000 }).should('not.exist');
1304
+ cy.GLO_checkLoadingIndicator();
1305
+ });
1306
+ Cypress.Commands.add('GLO_selectCustomView', (customViewName, subMenuName) => {
1307
+ cy.GLO_clickDatacontrolBrowserButton('btnCustomViews');
1308
+ if (subMenuName) {
1309
+ const menuPath = Array.isArray(customViewName) ? customViewName : [customViewName];
1310
+ const hoverThroughLevels = (path, index) => {
1311
+ if (index >= path.length) {
1312
+ const subRegex = new RegExp(`^\\s*${Cypress._.escapeRegExp(subMenuName)}\\s*$`);
1313
+ cy.get('.dx-menu-item-text', { timeout: 10000 })
1314
+ .contains(subRegex)
1315
+ .click()
1316
+ .then(() => {
1317
+ cy.GLO_checkLoadingIndicator();
1318
+ });
1319
+ return;
1320
+ }
1321
+ const levelRegex = new RegExp(`^\\s*${Cypress._.escapeRegExp(path[index])}\\s*$`);
1322
+ cy.get('.dx-menu-item', { timeout: 10000 })
1323
+ .contains('.dx-menu-item-text', levelRegex)
1324
+ .closest('.dx-menu-item')
1325
+ .trigger('mouseover')
1326
+ .should('be.visible')
1327
+ .then(() => {
1328
+ hoverThroughLevels(path, index + 1);
1329
+ });
1330
+ };
1331
+ hoverThroughLevels(menuPath, 0);
1332
+ }
1333
+ else {
1334
+ const singleViewName = Array.isArray(customViewName) ? customViewName[0] : customViewName;
1335
+ let searchPattern;
1336
+ if (singleViewName.includes('Save grid state')) {
1337
+ searchPattern = /^\s*Save grid state( as \.\.\.)?\s*$/;
1338
+ }
1339
+ else {
1340
+ searchPattern = new RegExp(`^\\s*${Cypress._.escapeRegExp(singleViewName)}\\s*$`);
1341
+ }
1342
+ cy.get('.dx-menu-item-text', { timeout: 10000 })
1343
+ .contains(searchPattern)
1344
+ .click()
1345
+ .then(() => {
1346
+ cy.GLO_checkLoadingIndicator();
1347
+ });
1348
+ }
1349
+ });
1350
+ Cypress.Commands.add('GLO_dblClickLabel', (selector, labelValue) => {
1351
+ cy.GLO_getElement(selector)
1352
+ .contains(labelValue)
1353
+ .dblclick()
1354
+ .then(() => {
1355
+ cy.GLO_checkLoadingIndicator();
1356
+ });
1357
+ });
1358
+ Cypress.Commands.add('GLO_validateCheckbox', (selector, value) => {
1359
+ if (value === undefined)
1360
+ return;
1361
+ cy.GLO_getElement(selector).find('dx-check-box').invoke('attr', 'aria-checked').should('eq', value);
1362
+ });
1363
+ /**
1364
+ * Command to check the presence of the AG Grid loader and wait for it to disappear.
1365
+ * It ensures that the AG Grid has finished loading.
1366
+ */
1367
+ Cypress.Commands.add('GLO_checkAGLoader', () => {
1368
+ // biome-ignore plugin: wait because AGLoader is not instant
1369
+ cy.wait(1000); // Wait because AGLoader is not instant
1370
+ cy.get('.ag-loading', { timeout: 600000 }).should('not.exist');
1371
+ });
1372
+ Cypress.Commands.add('GLO_log', (logValue) => {
1373
+ cy.task('log', logValue);
1374
+ });
1375
+ Cypress.Commands.add('GLO_storeValue', (selector, asValue, log = hideInCommandLog) => {
1376
+ cy.GLO_getElement(selector)
1377
+ .find('.dx-texteditor-input')
1378
+ .invoke('val')
1379
+ .then((text) => {
1380
+ cy.wrap(text).as(asValue);
1381
+ if (log)
1382
+ cy.GLO_log(`${asValue}: ${String(text)}`);
1383
+ });
1384
+ });
1385
+ Cypress.Commands.add('GLO_storeValueGrid', (selector, rowFilter, columnId, asValue, log = hideInCommandLog) => {
1386
+ if (typeof rowFilter === 'string') {
1387
+ cy.GLO_getElement(selector).within(() => {
1388
+ cy.GLO_getAllRowsInGrid()
1389
+ .filter(`:contains("${rowFilter}")`)
1390
+ .eq(0)
1391
+ .find(`[col-id="${columnId}"]`)
1392
+ .should('not.have.text', '')
1393
+ .then((cell) => {
1394
+ const text = cell.text().trim();
1395
+ cy.wrap(text).as(asValue);
1396
+ if (log)
1397
+ cy.GLO_log(`${asValue}: ${text}`);
1398
+ });
1399
+ });
1400
+ }
1401
+ else {
1402
+ cy.GLO_getElement(selector).within(() => {
1403
+ cy.GLO_getRowInGrid(rowFilter)
1404
+ .find(`[col-id="${columnId}"]`)
1405
+ .should('not.have.text', '')
1406
+ .then((cell) => {
1407
+ const text = cell.text().trim();
1408
+ cy.wrap(text).as(asValue);
1409
+ if (log)
1410
+ cy.GLO_log(`${asValue}: ${text}`);
1411
+ });
1412
+ });
1413
+ }
1414
+ });
1415
+ Cypress.Commands.add('GLO_checkNumberOfRowsInGrid', (selector, numberToValidate, lengthCondition = 'have.length', timeOut) => {
1416
+ const slowPollThreshold = 15000;
1417
+ const pollInterval = 5000;
1418
+ const HEADER_ROWS = 2;
1419
+ const getAriaRowcount = ($grid) => parseInt($grid.find('[role="grid"]').attr('aria-rowcount') || '0', 10);
1420
+ // Column header row + filter row, counted in the center viewport only (the pinned-left and
1421
+ // pinned-right containers repeat those same header rows).
1422
+ const headerRowCount = ($grid) => $grid.find('.ag-header-viewport .ag-header-row').length || HEADER_ROWS;
1423
+ const isLazyLoaded = ($grid) => {
1424
+ const ariaRowcount = getAriaRowcount($grid);
1425
+ const renderedRows = $grid.find('.ag-center-cols-container [row-index]').length;
1426
+ return renderedRows > 0 && (ariaRowcount === -1 || ariaRowcount > renderedRows + headerRowCount($grid));
1427
+ };
1428
+ // When a client-side column filter is active, the tab-/datacontrol-counter still shows the
1429
+ // unfiltered foundset total, so we must count the rendered (filtered) rows instead.
1430
+ const hasActiveColumnFilter = ($grid) => $grid.find('.ag-header-cell.ag-header-cell-filtered').length > 0 ||
1431
+ $grid.find('.ag-header-cell input').filter((_, el) => el.value.trim() !== '').length > 0;
1432
+ const getCountFromTabLabel = ($grid) => {
1433
+ const $tabs = $grid.closest('[data-cy$=".tabs"]').addBack().filter('[data-cy$=".tabs"]').add($grid.parents().filter('[data-cy]').first().find('[data-cy$=".tabs"]'));
1434
+ const text = $tabs.find('[open="true"] span').first().text();
1435
+ const match = /\((\d+)\)/.exec(text);
1436
+ if (!match)
1437
+ return null;
1438
+ return parseInt(match[1], 10);
1439
+ };
1440
+ const getCountFromDataControl = ($grid) => {
1441
+ const text = $grid.closest('.formslist').find('#txtCount').text().trim();
1442
+ const parts = text.split('/');
1443
+ if (parts.length !== 2)
1444
+ return null;
1445
+ const total = parseInt(parts[1].trim(), 10);
1446
+ return Number.isNaN(total) ? null : total;
1447
+ };
1448
+ // AG Grid publishes the displayed row total (header rows included, pinned "sum" rows
1449
+ // excluded) in aria-rowcount, so a virtualised grid still reports rows the viewport never
1450
+ // rendered. Verified on ng$consignment_dtl_general.grid: 25 goods -> aria-rowcount 27 with
1451
+ // 2 header rows, while only 12 rows exist in the DOM.
1452
+ const getCountFromAriaRowcount = ($grid) => {
1453
+ const total = getAriaRowcount($grid) - headerRowCount($grid);
1454
+ return total >= 0 ? total : null; // An aria-rowcount of -1 means the total is unknown (infinite row model)
1455
+ };
1456
+ // Form-level counters (#txtCount navigator, tab "(n)" label) only match a grid on list
1457
+ // forms. On non-lst forms with multiple grids the navigator reads the form's own (often
1458
+ // 0/0) foundset, so fall back to ag-grid's own row count there.
1459
+ const formIsListForm = /lst$/i.test(selector[0]);
1460
+ const getTotalCount = ($grid) => {
1461
+ if (!isLazyLoaded($grid))
1462
+ return null; // Everything is rendered: counting the DOM rows is exact
1463
+ if (!formIsListForm)
1464
+ return getCountFromAriaRowcount($grid);
1465
+ if (hasActiveColumnFilter($grid))
1466
+ return null;
1467
+ return getCountFromTabLabel($grid) ?? getCountFromDataControl($grid) ?? getCountFromAriaRowcount($grid);
1468
+ };
1469
+ const toNumberAssertion = (condition) => {
1470
+ const stripped = condition.replace('have.length', '').split('.').filter(Boolean).join('.').trim();
1471
+ if (!stripped)
1472
+ return 'equal';
1473
+ return `be.${stripped}`;
1474
+ };
1475
+ const runChainAssertion = (subject) => {
1476
+ const effectiveCondition = typeof subject === 'number' ? toNumberAssertion(lengthCondition) : lengthCondition;
1477
+ const parts = effectiveCondition.split('.');
1478
+ let assertion = expect(subject).to;
1479
+ let context = assertion;
1480
+ for (const part of parts) {
1481
+ context = assertion;
1482
+ if (!(part in assertion))
1483
+ throw new Error(`Invalid Chai property: ${part} in chain "${effectiveCondition}"`);
1484
+ assertion = assertion[part];
1485
+ }
1486
+ if (typeof assertion !== 'function')
1487
+ throw new Error(`${effectiveCondition} did not resolve to a function`);
1488
+ assertion.call(context, numberToValidate);
1489
+ };
1490
+ if (!timeOut || timeOut < slowPollThreshold) {
1491
+ cy.GLO_getElement(selector, { timeout: timeOut }).should(($grid) => {
1492
+ const tabCount = getTotalCount($grid);
1493
+ if (tabCount !== null) {
1494
+ runChainAssertion(tabCount);
1495
+ }
1496
+ else {
1497
+ runChainAssertion($grid.find('.ag-center-cols-container [row-index]'));
1498
+ }
1499
+ });
1500
+ }
1501
+ else {
1502
+ const maxAttempts = Math.max(1, Math.ceil(timeOut / pollInterval));
1503
+ let attempts = 0;
1504
+ let lastError;
1505
+ const log = Cypress.log({ name: 'checkNumberOfRowsInGrid', message: `Polling rows (${lengthCondition} ${numberToValidate})…`, autoEnd: false, timeout: 0 });
1506
+ const checkStatus = (resolve, reject) => {
1507
+ cy.GLO_getElement(selector, { timeout: timeOut }).then(($grid) => {
1508
+ const $rows = $grid.find('.ag-center-cols-container [row-index]');
1509
+ let actual = $rows.length;
1510
+ let passed = false;
1511
+ try {
1512
+ const tabCount = getTotalCount($grid);
1513
+ if (tabCount !== null) {
1514
+ actual = tabCount;
1515
+ runChainAssertion(actual);
1516
+ }
1517
+ else {
1518
+ runChainAssertion($rows);
1519
+ }
1520
+ passed = true;
1521
+ }
1522
+ catch (e) {
1523
+ lastError = e;
1524
+ if (e.message.includes('Invalid Chai property')) {
1525
+ log.end();
1526
+ reject(e);
1527
+ return;
1528
+ }
1529
+ }
1530
+ if (passed) {
1531
+ cy.log(`SUCCESS: ${actual} to ${lengthCondition} ${numberToValidate}`);
1532
+ log.end();
1533
+ resolve();
1534
+ return;
1535
+ }
1536
+ attempts += 1;
1537
+ if (attempts > maxAttempts) {
1538
+ log.end();
1539
+ cy.log(`FAILURE: ${actual} to ${lengthCondition} ${numberToValidate} after ${Math.round((maxAttempts * pollInterval) / 100) / 10}s, error: ${lastError?.message || 'unknown'}`);
1540
+ reject(new Error(`Timed out after ${Math.round((maxAttempts * pollInterval) / 100) / 10}s: row count was ${actual}, expected "${lengthCondition} ${numberToValidate}"`));
1541
+ return;
1542
+ }
1543
+ // biome-ignore plugin: wait for next poll interval before checking again
1544
+ cy.wait(pollInterval, { log: false }).then(() => {
1545
+ log.set({ message: `Still polling ${actual} to ${lengthCondition} ${numberToValidate} ${attempts}/${maxAttempts}` });
1546
+ checkStatus(resolve, reject);
1547
+ });
1548
+ });
1549
+ };
1550
+ const promise = new Cypress.Promise((resolve, reject) => {
1551
+ checkStatus(resolve, reject);
1552
+ });
1553
+ cy.then(() => promise);
1554
+ }
1555
+ });
1556
+ Cypress.Commands.add('GLO_getNumberOfRowsInGrid', (selector, timeOut) => {
1557
+ return cy.GLO_getAllRowsInGrid(selector, timeOut).its('length');
1558
+ });
1559
+ Cypress.Commands.add('GLO_clearField', (selector) => {
1560
+ let loadingIndicator;
1561
+ let skipCheck = false;
1562
+ let zeroDefaultingField = false;
1563
+ const { baseSelector, index } = extractSelectorParts(selector);
1564
+ const inGrid = isGridRowSelector(baseSelector, index);
1565
+ const effectiveSelector = inGrid ? selector : baseSelector;
1566
+ const effectiveIndex = inGrid ? 0 : index;
1567
+ const stringPartCount = baseSelector.filter((s) => typeof s === 'string').length;
1568
+ const getElement = () => {
1569
+ return stringPartCount >= 2 ? cy.GLO_getElement(effectiveSelector) : cy.GLO_getElement(effectiveSelector).find('.control-field');
1570
+ };
1571
+ const pressClearIcon = (field) => {
1572
+ return cy.wrap(field, { log: false }).then(($el) => {
1573
+ const $buttonContainers = $el.find('.dx-texteditor-buttons-container .dx-clear-button-area');
1574
+ const $visibleContainer = $buttonContainers.filter((_, el) => {
1575
+ return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
1576
+ });
1577
+ const $clearIcon = $visibleContainer.eq(effectiveIndex).find('.dx-icon-clear');
1578
+ if ($clearIcon.length > 0) {
1579
+ return cy
1580
+ .wrap($clearIcon, { log: false })
1581
+ .click({ force: true })
1582
+ .then(() => {
1583
+ loadingIndicator = true;
1584
+ return true;
1585
+ });
1586
+ }
1587
+ return cy.wrap(false);
1588
+ });
1589
+ };
1590
+ const clearInput = (element) => {
1591
+ cy.wrap(element, { log: false }).then(($el) => {
1592
+ const $input = $el.find('.dx-texteditor-input').eq(effectiveIndex);
1593
+ const tag = String($input.prop('tagName')).toLowerCase();
1594
+ if ($input.length === 0) {
1595
+ cy.GLO_log('___No input found, add correct data-cy for clearing.___');
1596
+ skipCheck = true;
1597
+ }
1598
+ else if (tag === 'input' && $input.val()) {
1599
+ cy.wrap($input, { log: false }).type('{selectall}{del}{enter}');
1600
+ loadingIndicator = true;
1601
+ }
1602
+ else {
1603
+ cy.wrap($input, { log: false }).type('{selectall}{del}');
1604
+ }
1605
+ });
1606
+ };
1607
+ const processField = () => {
1608
+ return getElement().then(($field) => {
1609
+ // Some fields get cleared to 0 instead of to '' so we need to make sure the 'clearcheck' at the end of this command will correctly check vs 0 or ''.
1610
+ const isDxNumberBox = $field.find('dx-number-box').length > 0;
1611
+ const hasVisibleNumericInput = $field.find('.dx-texteditor-input[inputmode="decimal"]').length > 0;
1612
+ zeroDefaultingField = isDxNumberBox && hasVisibleNumericInput;
1613
+ pressClearIcon($field).then((hasIconButton) => {
1614
+ if (!hasIconButton)
1615
+ clearInput($field);
1616
+ });
1617
+ });
1618
+ };
1619
+ const getAndProcessField = () => {
1620
+ cy.log('___Clearing field___');
1621
+ processField().then(() => {
1622
+ if (loadingIndicator) {
1623
+ cy.GLO_checkLoadingIndicator();
1624
+ }
1625
+ else if (!skipCheck) {
1626
+ const expectedValue = zeroDefaultingField ? 0 : '';
1627
+ cy.GLO_checkField(selector, expectedValue, equal, 30000);
1628
+ }
1629
+ });
1630
+ };
1631
+ getAndProcessField();
1632
+ });
1633
+ Cypress.Commands.add('GLO_readFile', (fileName, fileType) => {
1634
+ cy.readFile(`${Cypress.config('downloadsFolder')}/${fileName}.${fileType}`);
1635
+ });
1636
+ Cypress.Commands.add('GLO_dialogRequiredCheck', (confirmation, title, content, contentMatchType = 'exact') => {
1637
+ cy.GLO_getVisible('.svy-message-dialog, .svy-dialog', 120000).within(($dialog) => {
1638
+ if ($dialog.find('.window-title.type-error:visible').length > 0) {
1639
+ if (typeof title === 'undefined' && typeof content === 'undefined') {
1640
+ cy.GLO_log('Error dialog, failing test. Explicit definition of the title or content required.');
1641
+ }
1642
+ }
1643
+ if (title)
1644
+ cy.get('.window-title').should('have.text', title);
1645
+ if (content) {
1646
+ cy.get('.window-body').then(($body) => {
1647
+ const hasTextarea = $body.find('textarea').length > 0;
1648
+ let matchType;
1649
+ if (hasTextarea) {
1650
+ matchType = contentMatchType === 'partial' ? 'contain.value' : 'have.value';
1651
+ }
1652
+ else {
1653
+ matchType = contentMatchType === 'partial' ? 'contain.text' : 'have.text';
1654
+ }
1655
+ cy.get(hasTextarea ? '.window-body textarea' : '.window-body span').should(matchType, content);
1656
+ });
1657
+ }
1658
+ cy.get('button').contains(confirmation).closest('button').click();
1659
+ });
1660
+ cy.GLO_checkLoadingIndicator();
1661
+ });
1662
+ Cypress.Commands.add('GLO_radioButton', (selector, selectorName) => {
1663
+ if (selectorName === undefined)
1664
+ return;
1665
+ cy.GLO_getElement(selector).contains(selectorName).parent().find('.dx-radiobutton-icon').click();
1666
+ });
1667
+ Cypress.Commands.add('GLO_radioListCData', (selector, radioButtonName) => {
1668
+ if (!radioButtonName)
1669
+ return;
1670
+ cy.GLO_getElement(selector)
1671
+ .find('.dx-item-content')
1672
+ .filter(`:contains("${String(radioButtonName)}")`)
1673
+ .then(($element) => {
1674
+ cy.wrap($element).parents('.dx-radiobutton').click();
1675
+ });
1676
+ });
1677
+ Cypress.Commands.add('GLO_validateRadioListCData', (selector, radioButtonName, expectedState) => {
1678
+ cy.GLO_getElement(selector)
1679
+ .find('.dx-item-content')
1680
+ .filter(`:contains("${radioButtonName}")`)
1681
+ .then(($element) => {
1682
+ if (expectedState) {
1683
+ cy.wrap($element).parents('.dx-radiobutton').find('.dx-radiobutton-icon-dot').should('exist');
1684
+ }
1685
+ else {
1686
+ cy.wrap($element).parents('.dx-radiobutton').find('.dx-radiobutton-icon-dot').should('not.exist');
1687
+ }
1688
+ });
1689
+ });
1690
+ Cypress.Commands.add('GLO_changeUserRole', (userRole) => {
1691
+ cy.GLO_getElement(['ng$main_form2', 'navbar_top']).find('.lucide-User').click();
1692
+ cy.get('.dx-menu-item-text').contains(userRole).click();
1693
+ cy.GLO_dialogRequiredCheck('Yes');
1694
+ });
1695
+ Cypress.Commands.add('GLO_selectRows', (selector, rowNumber) => {
1696
+ const executeForLoop = (rowNumber) => {
1697
+ for (let i = 1; i < rowNumber; i++) {
1698
+ cy.GLO_selectRowInGrid(i);
1699
+ }
1700
+ };
1701
+ cy.GLO_getElement(selector)
1702
+ .find('.ag-body-viewport')
1703
+ .within(() => {
1704
+ cy.GLO_clickRowInGrid(0); // Always click the first row
1705
+ if (rowNumber !== undefined) {
1706
+ executeForLoop(rowNumber);
1707
+ }
1708
+ else {
1709
+ cy.GLO_getAllRowsInGrid().then(($rows) => {
1710
+ executeForLoop($rows.length);
1711
+ });
1712
+ }
1713
+ });
1714
+ });
1715
+ Cypress.Commands.add('GLO_sortGrid', (selector, headerText) => {
1716
+ cy.GLO_getElement(selector)
1717
+ .find('.ag-header-viewport .ag-header-row.ag-header-row-column')
1718
+ .within(() => {
1719
+ cy.get('.ag-header-cell').contains(headerText).click();
1720
+ });
1721
+ cy.GLO_checkAGLoader();
1722
+ });
1723
+ Cypress.Commands.add('GLO_fillInfieldInGrid', (selector, rowFilter, columnId, value, gridInputType = gridInput) => {
1724
+ if (!value)
1725
+ return;
1726
+ if (typeof rowFilter === 'string') {
1727
+ cy.GLO_getAllRowsInGrid().filter(`:contains("${rowFilter}")`).find(`[col-id="${columnId}"]`).click();
1728
+ cy.GLO_getAllRowsInGrid().filter(`:contains("${rowFilter}")`).find(`[col-id="${columnId}"]`).dblclick();
1729
+ }
1730
+ else {
1731
+ cy.GLO_getElement(selector).within(() => {
1732
+ cy.GLO_clickRowInGrid(rowFilter, columnId);
1733
+ });
1734
+ cy.GLO_dblClickRowInGrid(row, selector, rowFilter, columnId, skipCheck);
1735
+ }
1736
+ cy.get(gridInputType === 'aggrid-texteditor-without-enter' ? gridInput : gridInputType, { timeout: 1000 }).should('exist');
1737
+ switch (gridInputType) {
1738
+ case gridInput:
1739
+ cy.GLO_getVisible(gridInputType).type(`${String(value)}{enter}`);
1740
+ break;
1741
+ case gridInputWithoutEnter:
1742
+ cy.GLO_getVisible(gridInput).type(String(value));
1743
+ break;
1744
+ case gridDropDown:
1745
+ cy.GLO_getVisible('aggrid-typeahededitor input').type(String(value));
1746
+ cy.GLO_getVisible('ngb-typeahead-window').find('button').contains(String(value)).click();
1747
+ break;
1748
+ case gridSelect:
1749
+ cy.GLO_getVisible(`${gridSelect} select`).select(String(value)).type('{enter}');
1750
+ break;
1751
+ default:
1752
+ throw new Error('Incorrect type for GLO_fillInfieldInGrid. Available types: gridInput, gridDropDown');
1753
+ }
1754
+ cy.GLO_checkLoadingIndicator();
1755
+ });
1756
+ Cypress.Commands.add('GLO_dropdownSelectFirstvalue', (selector) => {
1757
+ cy.GLO_getElement(selector).within(() => {
1758
+ cy.get('.dx-dropdowneditor-icon').click();
1759
+ });
1760
+ cy.GLO_getVisible('.dx-scrollview-content .dx-list-item').first().click();
1761
+ });
1762
+ Cypress.Commands.add('GLO_storeCurrentRecordId', (storeValueName) => {
1763
+ cy.GLO_selectMenuButtonLeft('info');
1764
+ cy.GLO_storeValue(['ng$svy_nav_globis_console_dlg', 'primary_key'], storeValueName);
1765
+ cy.GLO_clickButton(['ng$svy_nav_globis_console_dlg', 'close_btn']);
1766
+ });
1767
+ Cypress.Commands.add('GLO_checkAndSelectCompany', (requestedCompanyCode) => {
1768
+ cy.GLO_getElement(['ng$main_form2'], { timeout: 30000 })
1769
+ .find('.footer span')
1770
+ .contains('Company:')
1771
+ .invoke('text')
1772
+ .then((companyText) => {
1773
+ const $currentCompanyCode = companyText.replace('Company:', '').trim();
1774
+ if ($currentCompanyCode === requestedCompanyCode) {
1775
+ cy.log(`Current company is ${$currentCompanyCode}. No switch required.`);
1776
+ }
1777
+ else {
1778
+ cy.log(`Requested company is ${requestedCompanyCode}. Current company is ${$currentCompanyCode}. Switch required!`);
1779
+ cy.GLO_switchCompany(requestedCompanyCode);
1780
+ cy.log(`Company has been switched! Currently working on company ${requestedCompanyCode}`);
1781
+ }
1782
+ });
1783
+ });
1784
+ Cypress.Commands.add('GLO_selectAllGrid', (selector) => {
1785
+ cy.GLO_getElement(selector).within(() => {
1786
+ cy.GLO_getRowInGrid(0).find('[col-id]').eq(2).rightclick();
1787
+ });
1788
+ cy.get('a').contains('Select all', { matchCase: false }).click();
1789
+ });
1790
+ Cypress.Commands.add('GLO_clickButtonInGrid', { prevSubject: ['optional', 'element'] }, (subject, buttonColumn, rowFilter, validationColumn, x = 10, y = 50) => {
1791
+ const getRow = () => {
1792
+ if (subject) {
1793
+ if (rowFilter !== undefined)
1794
+ throw new Error('Cannot use rowFilter when chained!');
1795
+ return cy.wrap(subject);
1796
+ }
1797
+ else if (rowFilter !== undefined) {
1798
+ return cy.GLO_getRowInGrid(rowFilter);
1799
+ }
1800
+ else {
1801
+ return cy.GLO_getVisible('[row-index]').first();
1802
+ }
1803
+ };
1804
+ if (validationColumn) {
1805
+ getRow()
1806
+ .find(`[col-id="${validationColumn}"]`)
1807
+ .invoke('text')
1808
+ .then((text) => cy.wrap(text.trim()).as('beforeValue'));
1809
+ }
1810
+ getRow()
1811
+ .find(`[col-id="${buttonColumn}"]`)
1812
+ .then(($btn) => {
1813
+ const xCor = ($btn.width() * x) / 100;
1814
+ const yCor = ($btn.height() * y) / 100;
1815
+ getRow().find(`[col-id="${buttonColumn}"]`).click(xCor, yCor);
1816
+ });
1817
+ if (validationColumn) {
1818
+ cy.get('@beforeValue').then((beforeValue) => {
1819
+ getRow()
1820
+ .find(`[col-id="${validationColumn}"]`, { timeout: 30000 })
1821
+ .should(($afterValue) => {
1822
+ expect($afterValue.text().trim()).not.to.equal(beforeValue);
1823
+ });
1824
+ });
1825
+ }
1826
+ cy.GLO_checkLoadingIndicator();
1827
+ });
1828
+ Cypress.Commands.add('GLO_oktaAlternativeLogin', (altUser, company) => {
1829
+ cy.GLO_logout();
1830
+ // biome-ignore plugin: wait for Okta to process logout
1831
+ cy.wait(10000); // Wait for Okta to process logout
1832
+ if (cypressWebsocketLoader) {
1833
+ cy.on('window:before:load', (win) => {
1834
+ attachWebSocketSpy(win);
1835
+ });
1836
+ }
1837
+ if (altUser) {
1838
+ cy.origin(Cypress.expose('oktaUrl'), () => {
1839
+ cy.get('.okta-container', { timeout: 120000 });
1840
+ cy.get('.o-form-input', { timeout: 60000 })
1841
+ .find('[autocomplete="username"]')
1842
+ .clear()
1843
+ .type(Cypress.expose('username2'));
1844
+ cy.task('log', 'Filled in username');
1845
+ cy.get('[type="submit"]').click();
1846
+ cy.env(['password2']).then(({ password2 }) => {
1847
+ cy.get('.o-form-input')
1848
+ .find('[type="password"]', { timeout: 20000 })
1849
+ .type(password2, { log: false });
1850
+ });
1851
+ cy.task('log', 'Filled in password');
1852
+ cy.get('[type="submit"]').click();
1853
+ });
1854
+ cy.GLO_log(`Logged in as ${Cypress.expose('username2')}`);
1855
+ }
1856
+ else {
1857
+ cy.origin(Cypress.expose('oktaUrl'), () => {
1858
+ cy.get('.okta-container', { timeout: 120000 });
1859
+ cy.get('.o-form-input', { timeout: 60000 })
1860
+ .find('[autocomplete="username"]')
1861
+ .clear()
1862
+ .type(Cypress.expose('username'));
1863
+ cy.task('log', 'Filled in username');
1864
+ cy.get('[type="submit"]').click();
1865
+ cy.env(['password']).then(({ password }) => {
1866
+ cy.get('.o-form-input')
1867
+ .find('[type="password"]', { timeout: 20000 })
1868
+ .type(password, { log: false });
1869
+ });
1870
+ cy.task('log', 'Filled in password');
1871
+ cy.get('[type="submit"]').click();
1872
+ });
1873
+ cy.GLO_log(`Logged in as ${Cypress.expose('username')}`);
1874
+ }
1875
+ if (cypressWebsocketLoader)
1876
+ waitForWebSocketAfterOkta();
1877
+ cy.get('[data-cy$="ng$main_form2.navbar_top"]', { timeout: 120000 }).should('exist');
1878
+ if (company)
1879
+ cy.GLO_checkAndSelectCompany(company);
1880
+ });
1881
+ Cypress.Commands.add('GLO_getAllRowsInGrid', (selector, timeOut = defaultTimeout) => {
1882
+ if (selector) {
1883
+ const deadline = Date.now() + timeOut;
1884
+ return cy
1885
+ .GLO_getElement(selector, { timeout: timeOut })
1886
+ .find('.ag-center-cols-container [row-index]', { timeout: timeOut + 500 })
1887
+ .should(($rows) => {
1888
+ if ($rows.length === 0 && Date.now() < deadline)
1889
+ throw new Error('GLO_getAllRowsInGrid: waiting for rows');
1890
+ });
1891
+ }
1892
+ else {
1893
+ return cy.get('.ag-center-cols-container [row-index] [role="gridcell"]', { timeout: timeOut }).parent().filter(isInOpenContainer);
1894
+ }
1895
+ });
1896
+ Cypress.Commands.add('GLO_validateGridOrFail', (waitValue, failValue, rowNumber, columnId, timeOut = 600000, interval = 15000) => {
1897
+ validateGridValue(waitValue, failValue, rowNumber, columnId, timeOut, interval);
1898
+ });
1899
+ const validateGridValue = (value, failValue, rowFilter, columnId, timeOut = 10000, interval, operator = contains) => {
1900
+ const assertOrCheckCellValue = ($cells, toCheck, doAssert) => {
1901
+ const compare = (cellText) => {
1902
+ const text = cellText.trim();
1903
+ const normalizedValue = typeof toCheck === 'number' ? toCheck : String(toCheck).trim();
1904
+ const inputVal = typeof normalizedValue === 'number' ? parseLocalizedNumber(text) : text;
1905
+ if (operator === equal) {
1906
+ return inputVal === normalizedValue;
1907
+ }
1908
+ else if (operator === notEqual) {
1909
+ return inputVal !== normalizedValue;
1910
+ }
1911
+ else if (operator === contains) {
1912
+ return String(inputVal).includes(String(normalizedValue));
1913
+ }
1914
+ else if (operator === greaterEqual && typeof inputVal === 'number' && typeof normalizedValue === 'number') {
1915
+ return inputVal >= normalizedValue;
1916
+ }
1917
+ throw new Error('Incorrect operator for GLO_checkField. Available operators: equal, notEqual, contains, greaterEqual');
1918
+ };
1919
+ const match = $cells.toArray().find((cell) => compare(cell.textContent));
1920
+ const found = !!match;
1921
+ if (doAssert)
1922
+ assert.isTrue(found, `Cell value "${operator}" "${String(toCheck)}"`);
1923
+ return found;
1924
+ };
1925
+ const toCheckData = () => {
1926
+ const logOpt = { log: !interval };
1927
+ if (columnId && rowFilter !== undefined)
1928
+ return typeof rowFilter === 'number'
1929
+ ? cy
1930
+ .GLO_getAllRowsInGrid(undefined, timeOut)
1931
+ .find(`[col-id="${columnId}"]`, logOpt)
1932
+ .eq(rowFilter, { timeout: timeOut, ...logOpt })
1933
+ : cy
1934
+ .GLO_getAllRowsInGrid(undefined, timeOut)
1935
+ .filter(`:contains("${rowFilter}")`, { timeout: timeOut, ...logOpt })
1936
+ .find(`[col-id="${columnId}"]`, logOpt);
1937
+ else if (columnId)
1938
+ return cy.GLO_getAllRowsInGrid(undefined, timeOut).find(`[col-id="${columnId}"]`, { timeout: timeOut, ...logOpt });
1939
+ else if (rowFilter !== undefined)
1940
+ return (typeof rowFilter === 'number'
1941
+ ? cy.GLO_getAllRowsInGrid(undefined, timeOut).eq(rowFilter, { timeout: timeOut, ...logOpt })
1942
+ : cy.GLO_getAllRowsInGrid(undefined, timeOut).filter(`:contains("${rowFilter}")`, { timeout: timeOut, ...logOpt })).find('.ag-cell-value');
1943
+ else
1944
+ return cy.GLO_getAllRowsInGrid(undefined, timeOut);
1945
+ };
1946
+ if (Array.isArray(value)) {
1947
+ if (columnId)
1948
+ throw new Error('Cannot use combination of array and column id');
1949
+ const spansArray = value.map((span) => span.trim());
1950
+ const filterSelector = spansArray.map((span) => `:has(span:contains("${span}"))`).join('');
1951
+ if (rowFilter !== undefined)
1952
+ cy.GLO_getRowInGrid(rowFilter).filter(filterSelector, { timeout: timeOut }).should('exist');
1953
+ else
1954
+ cy.GLO_getAllRowsInGrid().filter(filterSelector, { timeout: timeOut }).should('exist');
1955
+ }
1956
+ else if (!interval) {
1957
+ toCheckData().should(($cells) => assertOrCheckCellValue($cells, value, true));
1958
+ }
1959
+ else {
1960
+ const pollInterval = interval;
1961
+ const maxAttempts = Math.max(1, Math.ceil(timeOut / pollInterval));
1962
+ let attempts = 0;
1963
+ const log = Cypress.log({ name: 'gridValueValidator', message: `Polling for "${String(value)}"…`, autoEnd: false, timeout: 0 });
1964
+ const checkStatus = (resolve, reject) => {
1965
+ toCheckData().then(($cells) => {
1966
+ if (failValue !== undefined && assertOrCheckCellValue($cells, failValue, false)) {
1967
+ cy.log(`Found fail value: ${String(failValue)}`);
1968
+ log.end();
1969
+ reject(new Error(`Found fail value: ${String(failValue)}`));
1970
+ return;
1971
+ }
1972
+ if (assertOrCheckCellValue($cells, value, false)) {
1973
+ cy.log(`Succeeded finding "${String(value)}"`);
1974
+ log.end();
1975
+ resolve();
1976
+ return;
1977
+ }
1978
+ attempts += 1;
1979
+ if (attempts >= maxAttempts) {
1980
+ cy.log(`Timed out after ${Math.round((maxAttempts * pollInterval) / 100) / 10}s waiting for: ${String(value)}`);
1981
+ log.end();
1982
+ reject(new Error(`Timed out after ${Math.round((maxAttempts * pollInterval) / 100) / 10}s waiting for: ${String(value)}`));
1983
+ return;
1984
+ }
1985
+ cy.wait(pollInterval, { log: false }).then(() => {
1986
+ log.set({ message: `Still polling… ${attempts}/${maxAttempts}` });
1987
+ checkStatus(resolve, reject);
1988
+ });
1989
+ });
1990
+ };
1991
+ const promise = new Cypress.Promise((resolve, reject) => {
1992
+ checkStatus(resolve, reject);
1993
+ });
1994
+ cy.then(() => promise);
1995
+ }
1996
+ };
1997
+ Cypress.Commands.add('GLO_getCurrentRecordCount', () => {
1998
+ return cy
1999
+ .GLO_getElement(['ng$datacontrol', 'datacontrol'])
2000
+ .find('#txtCount')
2001
+ .invoke('text')
2002
+ .then((records) => {
2003
+ const [currentStr, totalStr] = records.split('/');
2004
+ if (!currentStr || !totalStr) {
2005
+ throw new Error(`Could not parse record count from "${records}"`);
2006
+ }
2007
+ return {
2008
+ currentRecord: parseInt(currentStr.trim(), 10),
2009
+ totalRecords: parseInt(totalStr.trim(), 10)
2010
+ };
2011
+ });
2012
+ });
2013
+ Cypress.Commands.add('GLO_storeObject', (formName, elementNames, index, log = hideInCommandLog) => {
2014
+ const keys = (Array.isArray(elementNames) ? elementNames : [elementNames]);
2015
+ // Initialize saveData object to prevent cypress from failing when saving first alias.
2016
+ cy.then(() => {
2017
+ const current = Cypress.expose('saveData') ?? {};
2018
+ Cypress.expose('saveData', current);
2019
+ cy.wrap(current, { log: false }).as('saveData');
2020
+ });
2021
+ // The actual storing of values.
2022
+ cy.wrap(keys, { log: false }).each((key) => {
2023
+ cy.GLO_getElement([formName, key])
2024
+ .find('.dx-texteditor-input', { log: false })
2025
+ .invoke('val')
2026
+ .then((text) => {
2027
+ const value = String(text);
2028
+ const saveData = Cypress.expose('saveData') ?? {};
2029
+ // Create the nested object if it doesn't exist yet.
2030
+ saveData[formName] ??= {};
2031
+ if (typeof index === 'number') {
2032
+ const existing = saveData[formName][key];
2033
+ // Determine if we need to create a new array or use the existing one.
2034
+ const arr = Array.isArray(existing) ? existing : [];
2035
+ arr[index] = value;
2036
+ saveData[formName][key] = arr;
2037
+ }
2038
+ else {
2039
+ saveData[formName][key] = value;
2040
+ }
2041
+ Cypress.expose('saveData', saveData);
2042
+ cy.wrap(saveData, { log: false }).as('saveData');
2043
+ const indexSuffix = typeof index === 'number' ? `[${index}]` : '';
2044
+ if (log)
2045
+ cy.GLO_log(`Value saved at saveData.${formName}.${key}${indexSuffix}: ${value}`);
2046
+ });
2047
+ });
2048
+ });
2049
+ Cypress.Commands.add('GLO_checkFieldInvalid', (selector, shouldBeInvalid = isInvalid) => {
2050
+ cy.GLO_getElement(selector).within(() => {
2051
+ if (shouldBeInvalid) {
2052
+ cy.get('.dx-texteditor').should('have.class', 'dx-invalid');
2053
+ }
2054
+ else {
2055
+ cy.get('.dx-texteditor').should('not.have.class', 'dx-invalid');
2056
+ }
2057
+ });
2058
+ });
2059
+ Cypress.Commands.add('GLO_checkFieldEnabled', (selector, shouldBeEnabled = enabled) => {
2060
+ cy.GLO_getElement(selector).within(() => {
2061
+ if (shouldBeEnabled) {
2062
+ cy.get('.dx-texteditor').should('not.have.class', 'dx-state-disabled');
2063
+ }
2064
+ else {
2065
+ cy.get('.dx-texteditor').should('have.class', 'dx-state-disabled');
2066
+ }
2067
+ });
2068
+ });
2069
+ Cypress.Commands.add('GLO_checkFieldBgColor', (selector, color = 'red') => {
2070
+ const [formName, fieldOrFields] = selector;
2071
+ const fields = Array.isArray(fieldOrFields) ? fieldOrFields : [fieldOrFields];
2072
+ for (const field of fields) {
2073
+ cy.GLO_getElement([formName, field]).within(() => {
2074
+ cy.get('input.dx-texteditor-input').should('have.class', `input_bg_${color}`);
2075
+ });
2076
+ }
2077
+ });
2078
+ Cypress.Commands.add('GLO_extendedFilter', (filter, clearExistingFilters) => {
2079
+ // **************
2080
+ // * Selectors *
2081
+ // **************
2082
+ const filterComponent = () => cy.GLO_getElement(['ng$extended_filter', 'Extended_Filter_Builder'], { log: false });
2083
+ const getFilterFieldSelector = (path, fieldType, clauseType, includeInputClass = true) => {
2084
+ const prefix = clauseType
2085
+ ? { Attribute: 'clause-', 'Related field': 'relation-', 'Dynamic attribute': 'clause-', Group: 'group-', 'Nested group': 'group-' }[clauseType] || ''
2086
+ : '';
2087
+ return `[data-cy="filter-builder-${fieldType === 'value' ? '' : prefix}${fieldType}-${path}"]${includeInputClass ? ' .dx-texteditor-input' : ''}`;
2088
+ };
2089
+ // **************
2090
+ // * Buttons *
2091
+ // **************
2092
+ const buttonClearall = () => filterComponent().find('[data-cy="filter-builder-clear-all"]').click();
2093
+ const buttonRunquery = () => filterComponent().find('[data-cy="filter-builder-run-query"]').should('not.be.disabled').click();
2094
+ const buttonAdd = (filterType, groupPath) => {
2095
+ const click = (buttonType) => {
2096
+ const prefix = groupPath ? `filter-builder-group-${groupPath}-` : 'filter-builder-';
2097
+ return filterComponent().find(`[data-cy="${prefix}${buttonType}"]`).click();
2098
+ };
2099
+ switch (filterType) {
2100
+ case 'Related field':
2101
+ return click('add-related-field');
2102
+ case 'Dynamic attribute':
2103
+ return click('add-dynamic-attribute');
2104
+ case 'Nested group':
2105
+ if (!groupPath)
2106
+ throw new Error('Nested group can only be added inside a group');
2107
+ return click('add-nested-group');
2108
+ case 'Group':
2109
+ return groupPath ? click('add-nested-group') : click('add-group');
2110
+ default:
2111
+ return click('add-attribute');
2112
+ }
2113
+ };
2114
+ // **************
2115
+ // * Inputs *
2116
+ // **************
2117
+ const dropDown = (path, clauseType, dropDownType, value) => {
2118
+ const selector = getFilterFieldSelector(path, dropDownType, clauseType);
2119
+ filterComponent()
2120
+ .find(selector)
2121
+ .then(($el) => {
2122
+ const id = $el[0].getAttribute('aria-controls');
2123
+ if (!id)
2124
+ cy.wrap($el).click();
2125
+ });
2126
+ filterComponent()
2127
+ .find(selector)
2128
+ .then(($el) => {
2129
+ const id = $el[0].getAttribute('aria-controls');
2130
+ if (!id)
2131
+ throw new Error('Missing aria-controls');
2132
+ cy.get(`[id="${id}"]`, { timeout: 30000 }).should('exist').should('not.have.class', 'dx-empty-collection').find('.dx-item').should('have.length.greaterThan', 0);
2133
+ cy.get(`[id="${id}"] .dx-scrollview-content`).children().contains(value).click();
2134
+ });
2135
+ switch (dropDownType) {
2136
+ case 'select':
2137
+ filterComponent()
2138
+ .find(getFilterFieldSelector(path, dropDownType, clauseType, false))
2139
+ .parent()
2140
+ .find('.dx-tag button')
2141
+ .filter((_, el) => (el.getAttribute('aria-label') ?? '').includes(value))
2142
+ .should('have.length.greaterThan', 0);
2143
+ break;
2144
+ case 'value':
2145
+ if (value === 'Select All')
2146
+ return;
2147
+ filterComponent()
2148
+ .find(`${getFilterFieldSelector(path, dropDownType, clauseType, false)} .dx-tag`)
2149
+ .filter((_, el) => (el.getAttribute('aria-label') ?? '').includes(value))
2150
+ .should('have.length.greaterThan', 0);
2151
+ break;
2152
+ case 'field':
2153
+ case 'logical-operator':
2154
+ case 'operator':
2155
+ filterComponent().find(selector).invoke('val').should('include', value);
2156
+ break;
2157
+ default:
2158
+ break;
2159
+ }
2160
+ };
2161
+ // This is basically the same as checkBoxHelper but adapted for the selector inside the extended filter component.
2162
+ const checkBox = (path, v) => {
2163
+ const retries = 3;
2164
+ const clickElement = (remaining = retries) => {
2165
+ return filterComponent()
2166
+ .find(getFilterFieldSelector(path, 'value', undefined, false))
2167
+ .then(($element) => {
2168
+ const currentState = $element[0].getAttribute('aria-checked');
2169
+ if (currentState === v)
2170
+ return;
2171
+ if (remaining <= 0)
2172
+ throw new Error('Failed to set checkbox/switch to the desired state after multiple attempts.');
2173
+ filterComponent()
2174
+ .find(getFilterFieldSelector(path, 'value', undefined, false))
2175
+ .find('.dx-checkbox-icon')
2176
+ .click()
2177
+ .then(() => {
2178
+ cy.GLO_checkLoadingIndicator();
2179
+ clickElement(remaining - 1);
2180
+ });
2181
+ });
2182
+ };
2183
+ clickElement();
2184
+ };
2185
+ const applyValueForOperator = (operator, path, clauseType, values) => {
2186
+ const vals = Array.isArray(values) ? values : [values];
2187
+ vals.forEach((v, i) => {
2188
+ switch (operator) {
2189
+ case 'is empty':
2190
+ case 'is not empty':
2191
+ case 'today':
2192
+ case 'this week':
2193
+ case 'this month':
2194
+ return;
2195
+ case 'between':
2196
+ filterComponent()
2197
+ .find(getFilterFieldSelector(path, `value-${i === 0 ? 'from' : 'to'}`))
2198
+ .type(`{selectall}{del}${v}{enter}`);
2199
+ return;
2200
+ case 'in':
2201
+ case 'not in':
2202
+ filterComponent()
2203
+ .find(getFilterFieldSelector(path, 'value', undefined, false))
2204
+ .parent()
2205
+ .then(($field) => {
2206
+ if ($field.find('button.fa-search').length > 0) {
2207
+ if (i >= 1)
2208
+ throw new Error('Selector only supports a single value (for now).');
2209
+ cy.wrap($field).find('button.fa-search').click();
2210
+ cy.GLO_getElement(['svy_nav_selection', 'search_criteria']).type(String(v));
2211
+ cy.GLO_clickButton(['svy_nav_selection', 'btn_search']);
2212
+ dblClickRowInGrid(span, String(v));
2213
+ cy.GLO_checkLoadingIndicator();
2214
+ }
2215
+ else {
2216
+ dropDown(path, clauseType, 'value', String(v));
2217
+ if (i === vals.length - 1)
2218
+ filterComponent()
2219
+ .find(getFilterFieldSelector(path, 'value', clauseType))
2220
+ .click();
2221
+ }
2222
+ });
2223
+ return;
2224
+ case '=':
2225
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: intentional - falls through to the shared date/time handling below when v isn't check/uncheck
2226
+ case '≠':
2227
+ if (v === check || v === unCheck) {
2228
+ checkBox(path, v);
2229
+ return;
2230
+ }
2231
+ // Fall through
2232
+ case 'before':
2233
+ case 'after':
2234
+ case '<= today +/- days':
2235
+ case '>= today +/- days':
2236
+ case 'contains':
2237
+ case 'starts with':
2238
+ case 'ends with':
2239
+ case '>':
2240
+ case '>=':
2241
+ case '<':
2242
+ case '<=':
2243
+ filterComponent().find(getFilterFieldSelector(path, 'value')).type(`{selectall}{del}${v}{enter}`);
2244
+ return;
2245
+ default: {
2246
+ const _exhaustive = operator;
2247
+ throw new Error(`Unhandled operator: ${String(_exhaustive)}`);
2248
+ }
2249
+ }
2250
+ });
2251
+ };
2252
+ const applyLeaf = (leaf, path, parentGroupPath, isFirstRoot) => {
2253
+ const clauseType = leaf.type ?? 'Attribute';
2254
+ if (parentGroupPath)
2255
+ buttonAdd(clauseType, parentGroupPath);
2256
+ else {
2257
+ if (isFirstRoot && clauseType !== 'Attribute')
2258
+ filterComponent().find('[data-cy="filter-builder-clause-remove-0"]').click();
2259
+ if (!isFirstRoot || clauseType !== 'Attribute')
2260
+ buttonAdd(clauseType);
2261
+ }
2262
+ leaf.logicalOperator && !isFirstRoot && dropDown(path, clauseType, 'logical-operator', leaf.logicalOperator);
2263
+ if (leaf.type === 'Related field')
2264
+ (Array.isArray(leaf.relationSelect) ? leaf.relationSelect : [leaf.relationSelect]).forEach((v, i) => {
2265
+ dropDown(path, clauseType, 'select', v);
2266
+ if (i !== leaf.relationSelect.length - 1)
2267
+ filterComponent()
2268
+ .find(getFilterFieldSelector(path, 'select', clauseType))
2269
+ .click();
2270
+ });
2271
+ dropDown(path, clauseType, 'field', leaf.field);
2272
+ dropDown(path, clauseType, 'operator', leaf.operator);
2273
+ if (!('values' in leaf))
2274
+ return;
2275
+ applyValueForOperator(leaf.operator, path, clauseType, leaf.values);
2276
+ };
2277
+ const applyNode = (node, path, parentGroupPath, isFirstRoot) => {
2278
+ const isGroup = (n) => n.type === 'Group';
2279
+ const isNestedGroup = (n) => n.type === 'Nested group';
2280
+ if (isGroup(node)) {
2281
+ if (parentGroupPath)
2282
+ buttonAdd('Group', parentGroupPath);
2283
+ else {
2284
+ if (isFirstRoot)
2285
+ filterComponent().find('[data-cy="filter-builder-clause-remove-0"]').click();
2286
+ buttonAdd('Group');
2287
+ }
2288
+ node.logicalOperator && dropDown(path, 'Group', 'logical-operator', node.logicalOperator);
2289
+ node.values.forEach((child, i) => {
2290
+ applyNode(child, `${path}-${i}`, path, false);
2291
+ });
2292
+ return;
2293
+ }
2294
+ if (isNestedGroup(node)) {
2295
+ if (!parentGroupPath)
2296
+ throw new Error('Nested group requires a parent group');
2297
+ buttonAdd('Nested group', parentGroupPath);
2298
+ node.logicalOperator && dropDown(path, 'Nested group', 'logical-operator', node.logicalOperator);
2299
+ node.values.forEach((child, i) => {
2300
+ applyNode(child, `${path}-${i}`, path, false);
2301
+ });
2302
+ return;
2303
+ }
2304
+ applyLeaf(node, path, parentGroupPath, isFirstRoot);
2305
+ };
2306
+ cy.get('div[id^="crumb_"]:not([hidden])', { log: false }).then(($crumbRoot) => {
2307
+ $crumbRoot.find('[data-cy="ng$extended_filter.Extended_Filter_Builder"]').length === 0 && cy.GLO_clickDatacontrolBrowserButton('btnFilter');
2308
+ });
2309
+ if (clearExistingFilters)
2310
+ buttonClearall();
2311
+ filter.forEach((node, i) => {
2312
+ applyNode(node, `${i}`, undefined, i === 0);
2313
+ });
2314
+ buttonRunquery();
2315
+ cy.GLO_checkLoadingIndicator();
2316
+ });
2317
+ Cypress.Commands.add('GLO_selectNamedSearch', (searchName) => {
2318
+ cy.GLO_clickDatacontrolBrowserButton('btnExtendedSearch');
2319
+ cy.GLO_getVisible('.dx-submenu', 10000).contains(searchName).click();
2320
+ cy.GLO_checkLoadingIndicator();
2321
+ });