@lowdefy/server-dev 6.1.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (624) hide show
  1. package/client/App.jsx +52 -24
  2. package/client/Inspector.jsx +73 -30
  3. package/client/JourneyObserver.jsx +47 -0
  4. package/client/Page.jsx +54 -22
  5. package/client/Recorder.jsx +73 -0
  6. package/client/Routing.jsx +37 -13
  7. package/client/clientBuildImports.test.mjs +2 -0
  8. package/client/devStreamConnections.test.mjs +40 -0
  9. package/client/feedback/FeedbackMount.jsx +2 -2
  10. package/client/feedback/FeedbackOverlay.jsx +7 -4
  11. package/client/feedback/feedbackStyles.js +3 -0
  12. package/client/journeyObserver/startJourneyObserver.js +101 -0
  13. package/client/journeyObserver/startJourneyObserver.test.mjs +149 -0
  14. package/client/openInEditor/OpenInEditorListener.jsx +2 -0
  15. package/client/recorder/buildEngineRecord.js +37 -0
  16. package/client/recorder/buildInteractionRecord.js +64 -0
  17. package/client/recorder/buildTraceEvent.js +57 -0
  18. package/client/recorder/buildTraceEvent.test.mjs +81 -0
  19. package/client/recorder/chunkRecords.js +69 -0
  20. package/client/recorder/chunkRecords.test.mjs +59 -0
  21. package/client/recorder/collectEventActions.js +30 -0
  22. package/client/recorder/collectEventEndpoints.js +40 -0
  23. package/client/recorder/collectEventRequests.js +39 -0
  24. package/client/recorder/createPairingBuffer.js +187 -0
  25. package/client/recorder/createPairingBuffer.test.mjs +405 -0
  26. package/client/recorder/createPasswordRedactor.js +84 -0
  27. package/client/recorder/createRecordBatcher.js +54 -0
  28. package/client/recorder/createRecordBatcher.test.mjs +76 -0
  29. package/client/recorder/createRecorder.js +216 -0
  30. package/client/recorder/createRecorder.test.mjs +318 -0
  31. package/client/recorder/createRecordingSession.js +61 -0
  32. package/client/recorder/createRecordingSession.test.mjs +72 -0
  33. package/client/recorder/diffStateWrites.js +77 -0
  34. package/client/recorder/diffStateWrites.test.mjs +66 -0
  35. package/client/recorder/isEventCall.js +30 -0
  36. package/client/recorder/jsonType.js +39 -0
  37. package/client/recorder/keyChord.js +47 -0
  38. package/client/recorder/keyChord.test.mjs +50 -0
  39. package/client/recorder/sendRecords.js +40 -0
  40. package/client/recorder/toJsonSafe.js +29 -0
  41. package/lib/client/BuildingPage.jsx +3 -1
  42. package/lib/client/ErrorBar.jsx +17 -13
  43. package/lib/client/RedirectingPage.jsx +56 -0
  44. package/lib/client/auth/Auth.jsx +11 -6
  45. package/lib/client/auth/AuthConfigured.jsx +222 -36
  46. package/lib/client/auth/AuthNotConfigured.js +45 -4
  47. package/lib/client/auth/authNotConfiguredParity.test.mjs +61 -0
  48. package/lib/client/auth/authTemplateSync.test.mjs +40 -0
  49. package/lib/client/auth/betterAuthSessionContract.test.mjs +181 -0
  50. package/lib/client/auth/createFetchCheckingBuild.js +45 -0
  51. package/lib/client/bundleBuildId.js +25 -0
  52. package/lib/client/collectVisibleBlockIds.js +51 -0
  53. package/lib/client/getCspNonce.js +25 -0
  54. package/lib/client/getPagePath.js +36 -0
  55. package/lib/client/getPagePath.test.mjs +51 -0
  56. package/lib/client/utils/formatErrorsForCopy.js +30 -0
  57. package/lib/client/utils/formatErrorsForCopy.test.mjs +39 -0
  58. package/lib/client/utils/getErrorBarColor.js +25 -0
  59. package/lib/client/utils/getErrorBarColor.test.mjs +34 -0
  60. package/lib/client/utils/request.js +8 -1
  61. package/lib/client/utils/usePageConfig.js +89 -25
  62. package/lib/client/utils/usePageConfig.test.mjs +198 -13
  63. package/lib/configTree/buildConfigTree.mjs +58 -0
  64. package/lib/configTree/buildConfigTree.test.mjs +77 -0
  65. package/lib/configTree/collectConfigText.mjs +101 -0
  66. package/lib/configTree/collectConfigText.test.mjs +116 -0
  67. package/lib/configTree/prepareScratchServer.mjs +46 -0
  68. package/lib/configTree/runConfigTreeBuild.mjs +88 -0
  69. package/lib/configTree/runConfigTreeBuild.skipPluginChecks.test.mjs +74 -0
  70. package/lib/configTree/runConfigTreeBuild.test.mjs +202 -0
  71. package/lib/docs/JourneyStepError.js +30 -0
  72. package/lib/docs/acquireBrowserSlot.js +30 -0
  73. package/lib/docs/addNestedEndpoints.js +54 -0
  74. package/lib/docs/addNestedEndpoints.test.mjs +185 -0
  75. package/lib/docs/addWriteFlags.js +77 -0
  76. package/lib/docs/buildEditedPages.js +50 -0
  77. package/lib/docs/buildWaitHeader.js +21 -0
  78. package/lib/docs/captureAnnotatedScreenshot.js +23 -12
  79. package/lib/docs/checkAgentFiles.js +148 -0
  80. package/lib/docs/checkAgentFiles.test.mjs +225 -0
  81. package/lib/docs/checkStepReferences.js +239 -0
  82. package/lib/docs/checkStepReferences.test.mjs +173 -0
  83. package/lib/docs/checkpointStore.js +3 -0
  84. package/lib/docs/clientErrorStore.js +18 -1
  85. package/lib/docs/clientErrorStore.test.mjs +46 -0
  86. package/lib/docs/collectExercised.js +52 -0
  87. package/lib/docs/createBrowserLifecycle.js +102 -0
  88. package/lib/docs/createBrowserLifecycle.test.mjs +212 -0
  89. package/lib/docs/createDocsMcpServer.js +345 -388
  90. package/lib/docs/createDocsMcpServer.test.mjs +644 -4
  91. package/lib/docs/createDocsMcpServerRunJourney.test.mjs +148 -0
  92. package/lib/docs/createJourneyActors.chromium.test.mjs +227 -0
  93. package/lib/docs/createJourneyActors.js +213 -0
  94. package/lib/docs/createJourneyActors.test.mjs +79 -0
  95. package/lib/docs/createJourneyObservations.js +80 -0
  96. package/lib/docs/createLeftOriginError.js +26 -0
  97. package/lib/docs/createNetworkCounter.js +101 -0
  98. package/lib/docs/dataSets/checkSharedCollectionIds.js +78 -0
  99. package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
  100. package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +727 -0
  101. package/lib/docs/dataSets/describeDataSetResult.js +25 -0
  102. package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
  103. package/lib/docs/dataSets/describeTargetKey.js +29 -0
  104. package/lib/docs/dataSets/deserializeFixture.js +34 -0
  105. package/lib/docs/dataSets/drainSessionWork.js +37 -0
  106. package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
  107. package/lib/docs/dataSets/duplicateIndexName.js +24 -0
  108. package/lib/docs/dataSets/getDataStore.js +106 -0
  109. package/lib/docs/dataSets/getDataStore.mongodb.test.mjs +63 -0
  110. package/lib/docs/dataSets/getDataStore.test.mjs +195 -0
  111. package/lib/docs/dataSets/groupDataSetByCollection.js +52 -0
  112. package/lib/docs/dataSets/insertFixture.js +57 -0
  113. package/lib/docs/dataSets/insertGeneratedDocuments.js +41 -0
  114. package/lib/docs/dataSets/isDataStoreGone.js +29 -0
  115. package/lib/docs/dataSets/loadDataSetCollection.js +42 -0
  116. package/lib/docs/dataSets/mergeCollectionIndexes.js +52 -0
  117. package/lib/docs/dataSets/mergeCollectionIndexes.test.mjs +59 -0
  118. package/lib/docs/dataSets/openDataSession.js +133 -0
  119. package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +493 -0
  120. package/lib/docs/dataSets/pickDataStorePort.js +58 -0
  121. package/lib/docs/dataSets/pickDataStorePort.test.mjs +63 -0
  122. package/lib/docs/dataSets/readConnectionArtifacts.js +46 -0
  123. package/lib/docs/dataSets/readDataSession.js +41 -0
  124. package/lib/docs/dataSets/readDataSession.test.mjs +57 -0
  125. package/lib/docs/dataSets/readDataSet.js +83 -0
  126. package/lib/docs/dataSets/readDataSet.test.mjs +251 -0
  127. package/lib/docs/dataSets/registerDataStoreShutdown.js +47 -0
  128. package/lib/docs/dataSets/removeDataStoreFiles.js +32 -0
  129. package/lib/docs/dataSets/resolveDataSetCollection.js +46 -0
  130. package/lib/docs/dataSets/resolveJourneyDataSet.js +106 -0
  131. package/lib/docs/dataSets/resolveJourneyDataSet.test.mjs +130 -0
  132. package/lib/docs/dataSets/sessionDatabaseName.js +32 -0
  133. package/lib/docs/dataSets/sweepOrphanDatabases.js +36 -0
  134. package/lib/docs/dataSets/test/app/lowdefy.yaml +225 -0
  135. package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
  136. package/lib/docs/dataSets/test/app/tests/data/shop.yaml +12 -0
  137. package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
  138. package/lib/docs/devEventBus.js +136 -0
  139. package/lib/docs/devEventBus.test.mjs +269 -0
  140. package/lib/docs/devToolDefinitions.js +529 -0
  141. package/lib/docs/docSources.js +21 -0
  142. package/lib/docs/docs.test.mjs +171 -15
  143. package/lib/docs/docsIndex.test.mjs +377 -0
  144. package/lib/docs/evalOperator.js +5 -3
  145. package/lib/docs/evalOperatorHeadless.js +84 -12
  146. package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
  147. package/lib/docs/evalOperatorInTab.js +2 -1
  148. package/lib/docs/findConfig.js +95 -49
  149. package/lib/docs/fitResponse.js +81 -0
  150. package/lib/docs/fitResponse.test.mjs +128 -0
  151. package/lib/docs/formatFeedback.js +26 -5
  152. package/lib/docs/formatFeedback.test.mjs +48 -0
  153. package/lib/docs/getAppMap.js +7 -1
  154. package/lib/docs/getBrowser.js +150 -80
  155. package/lib/docs/getBrowser.test.mjs +407 -16
  156. package/lib/docs/getBuildId.js +61 -0
  157. package/lib/docs/getBuildId.test.mjs +84 -0
  158. package/lib/docs/getBuildStatus.js +81 -7
  159. package/lib/docs/getBuildStatus.test.mjs +229 -0
  160. package/lib/docs/getBuildStatusAfterEdits.js +35 -0
  161. package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
  162. package/lib/docs/getCoreDoc.js +9 -1
  163. package/lib/docs/getDoc.js +100 -0
  164. package/lib/docs/getDocsIndex.js +152 -0
  165. package/lib/docs/getHazards.js +115 -0
  166. package/lib/docs/getHazards.test.mjs +175 -0
  167. package/lib/docs/getMailOutboxDirectory.js +26 -0
  168. package/lib/docs/getOverview.js +79 -13
  169. package/lib/docs/getOverviewBasePath.test.mjs +37 -0
  170. package/lib/docs/getPageBuildStatus.js +46 -0
  171. package/lib/docs/getPluginDoc.js +21 -23
  172. package/lib/docs/getSchema.js +33 -1
  173. package/lib/docs/getStaleStatus.js +36 -0
  174. package/lib/docs/getStaleStatus.test.mjs +74 -0
  175. package/lib/docs/guardJourneyOrigin.js +39 -0
  176. package/lib/docs/inspectState.js +5 -3
  177. package/lib/docs/inspectStateFromTab.js +2 -2
  178. package/lib/docs/inspectStateHeadless.js +89 -27
  179. package/lib/docs/inspectStateHeadless.test.mjs +20 -1
  180. package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
  181. package/lib/docs/installHeadlessShell.js +123 -0
  182. package/lib/docs/installHeadlessShell.test.mjs +70 -0
  183. package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
  184. package/lib/docs/isHeadlessShellIncomplete.js +34 -0
  185. package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
  186. package/lib/docs/isPageReady.js +77 -0
  187. package/lib/docs/isPageReady.test.mjs +235 -0
  188. package/lib/docs/isRunErrorEntry.js +25 -0
  189. package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
  190. package/lib/docs/journeyRunRecording.js +35 -0
  191. package/lib/docs/journeyRunRecording.test.mjs +39 -0
  192. package/lib/docs/keepEventStreamAlive.js +68 -0
  193. package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
  194. package/lib/docs/launchBrowser.js +83 -0
  195. package/lib/docs/launchBrowser.test.mjs +175 -0
  196. package/lib/docs/listModuleDocEntries.js +69 -0
  197. package/lib/docs/listPackageDocFiles.js +67 -0
  198. package/lib/docs/listPluginDocEntries.js +111 -0
  199. package/lib/docs/listPlugins.js +9 -58
  200. package/lib/docs/listTypes.js +14 -6
  201. package/lib/docs/loadFrameworkHazards.js +40 -0
  202. package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
  203. package/lib/docs/loadState.js +103 -35
  204. package/lib/docs/makeModuleManifestDoc.js +174 -0
  205. package/lib/docs/matchExercisedPages.js +35 -0
  206. package/lib/docs/matchJourneyRoute.js +69 -0
  207. package/lib/docs/matchJourneyRoute.test.mjs +178 -0
  208. package/lib/docs/mergeNetworkSnapshots.js +49 -0
  209. package/lib/docs/nextJourneySequence.js +30 -0
  210. package/lib/docs/noBrowserError.js +22 -0
  211. package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
  212. package/lib/docs/observe/claimExpectedErrors.js +67 -0
  213. package/lib/docs/observe/createJourneyAppErrors.js +236 -0
  214. package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
  215. package/lib/docs/observe/evaluateInvariants.js +183 -0
  216. package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
  217. package/lib/docs/observe/findingKey.js +29 -0
  218. package/lib/docs/observe/hasNoEffect.js +31 -0
  219. package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
  220. package/lib/docs/observe/installStepObserver.js +72 -0
  221. package/lib/docs/observe/isAppApiUrl.js +33 -0
  222. package/lib/docs/observe/parseAppApiUrl.js +37 -0
  223. package/lib/docs/observe/readStepObserver.js +42 -0
  224. package/lib/docs/observe/relativeSource.js +29 -0
  225. package/lib/docs/observe/resolveConfigKeySource.js +33 -0
  226. package/lib/docs/observe/takeJourneyEvents.js +36 -0
  227. package/lib/docs/observe/toAppErrorFailure.js +61 -0
  228. package/lib/docs/observe/usesSearchStage.js +45 -0
  229. package/lib/docs/observe/waitForClientErrorReports.js +31 -0
  230. package/lib/docs/observe/watchJourneyContext.js +75 -0
  231. package/lib/docs/observe/withAppErrors.js +31 -0
  232. package/lib/docs/observe/withAppErrors.test.mjs +101 -0
  233. package/lib/docs/openJourney.js +94 -0
  234. package/lib/docs/openJourneyEmail.js +41 -0
  235. package/lib/docs/openJourneyEmail.test.mjs +187 -0
  236. package/lib/docs/pageBlocks.js +47 -0
  237. package/lib/docs/pageBuildStatus.test.mjs +103 -0
  238. package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
  239. package/lib/docs/rails/authEngineActions.js +53 -0
  240. package/lib/docs/rails/authEngineActions.test.mjs +69 -0
  241. package/lib/docs/rails/authEngineMethods.js +52 -0
  242. package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
  243. package/lib/docs/rails/checkAuthActionsRan.js +66 -0
  244. package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
  245. package/lib/docs/rails/checkClickRails.js +56 -0
  246. package/lib/docs/rails/createDataSetRails.js +82 -0
  247. package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
  248. package/lib/docs/rails/findExternalBlocks.js +147 -0
  249. package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
  250. package/lib/docs/rails/installAuthActionRecorder.js +56 -0
  251. package/lib/docs/rails/readAuthActionRecorder.js +32 -0
  252. package/lib/docs/rails/readBuiltArtifact.js +30 -0
  253. package/lib/docs/readConnectionPlugins.js +26 -0
  254. package/lib/docs/readDevAuthMode.js +32 -0
  255. package/lib/docs/readDocEntry.js +40 -0
  256. package/lib/docs/readDocTitle.js +26 -0
  257. package/lib/docs/readJourneyEmailMatch.js +57 -0
  258. package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
  259. package/lib/docs/readJourneySession.js +56 -0
  260. package/lib/docs/readMailOutbox.js +37 -0
  261. package/lib/docs/readPagePath.js +27 -0
  262. package/lib/docs/readPluginPackages.js +99 -0
  263. package/lib/docs/readProxyBuildWait.js +38 -0
  264. package/lib/docs/readRoutineSteps.js +45 -0
  265. package/lib/docs/registerShownTextEngine.js +118 -0
  266. package/lib/docs/requestRestart.js +30 -0
  267. package/lib/docs/requestRestart.test.mjs +49 -0
  268. package/lib/docs/resolvePageInstance.js +46 -0
  269. package/lib/docs/resolvePageInstance.test.mjs +81 -0
  270. package/lib/docs/resolveSource.js +11 -7
  271. package/lib/docs/resolveSource.test.mjs +1 -1
  272. package/lib/docs/resolveToolCaller.js +62 -0
  273. package/lib/docs/reviewPage.js +51 -0
  274. package/lib/docs/reviewPageBuilds.js +56 -0
  275. package/lib/docs/reviewPageBuilds.test.mjs +207 -0
  276. package/lib/docs/runCheck.js +75 -0
  277. package/lib/docs/runCheck.test.mjs +91 -0
  278. package/lib/docs/runCheckChild.js +49 -0
  279. package/lib/docs/runEndpoint.js +185 -0
  280. package/lib/docs/runEndpoint.test.mjs +509 -0
  281. package/lib/docs/runErrorBuffers.js +99 -0
  282. package/lib/docs/runErrorBuffers.test.mjs +95 -0
  283. package/lib/docs/runJourney.js +347 -0
  284. package/lib/docs/runJourney.test.mjs +2383 -0
  285. package/lib/docs/runJourneyAsUsers.js +44 -0
  286. package/lib/docs/runJourneyDataSet.test.mjs +274 -0
  287. package/lib/docs/runJourneySteps.js +1320 -0
  288. package/lib/docs/runRequest.js +100 -59
  289. package/lib/docs/runRequest.test.mjs +190 -0
  290. package/lib/docs/saveAnnotatedScreenshot.js +1 -1
  291. package/lib/docs/scaffoldPage.js +3 -1
  292. package/lib/docs/screenshotPage.js +163 -11
  293. package/lib/docs/screenshotPage.test.mjs +263 -1
  294. package/lib/docs/screenshotPageClip.test.mjs +4 -0
  295. package/lib/docs/searchDocs.js +82 -30
  296. package/lib/docs/searchDocs.test.mjs +100 -0
  297. package/lib/docs/searchIcons.js +90 -0
  298. package/lib/docs/searchIcons.test.mjs +127 -0
  299. package/lib/docs/selectExercised.js +39 -0
  300. package/lib/docs/selectFinalState.js +66 -0
  301. package/lib/docs/selectFinalState.test.mjs +51 -0
  302. package/lib/docs/serverErrorStore.js +54 -0
  303. package/lib/docs/serverErrorStore.test.mjs +50 -0
  304. package/lib/docs/setupTestFixtures.mjs +133 -39
  305. package/lib/docs/shownTextSelector.js +28 -0
  306. package/lib/docs/snapshotState.js +13 -5
  307. package/lib/docs/splitSearchTerms.js +62 -0
  308. package/lib/docs/splitSearchTerms.test.mjs +50 -0
  309. package/lib/docs/stateCheckpoints.test.mjs +77 -0
  310. package/lib/docs/tabAvailable.js +6 -5
  311. package/lib/docs/tabChannel.js +127 -21
  312. package/lib/docs/tabChannel.test.mjs +187 -29
  313. package/lib/docs/unsettledPageNote.js +24 -0
  314. package/lib/docs/validateJourneyMail.js +36 -0
  315. package/lib/docs/validateJourneyTimeout.js +38 -0
  316. package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
  317. package/lib/docs/validateRecordingParam.js +42 -0
  318. package/lib/docs/validateStateSelection.js +33 -0
  319. package/lib/docs/validateStateSelection.test.mjs +33 -0
  320. package/lib/docs/validateViewport.js +51 -0
  321. package/lib/docs/validateViewport.test.mjs +54 -0
  322. package/lib/docs/waitForBuild.js +50 -0
  323. package/lib/docs/waitForBuild.test.mjs +75 -0
  324. package/lib/docs/waitForJourneyEmail.js +77 -0
  325. package/lib/docs/withBrowserSlot.js +45 -0
  326. package/lib/docs/withBrowserSlot.test.mjs +75 -0
  327. package/lib/docs/withDataSession.js +53 -0
  328. package/lib/server/applyDataSetRedirect.js +92 -0
  329. package/lib/server/applyDataSetRedirect.test.mjs +446 -0
  330. package/lib/server/applyDataSetRedirectTenantReach.test.mjs +207 -0
  331. package/lib/server/applyDataSetRedirectWalled.test.mjs +134 -0
  332. package/lib/server/auth/createSystemContext.js +73 -0
  333. package/lib/server/auth/{getAuthConfig.js → getAuth.js} +15 -9
  334. package/lib/server/auth/getHeadlessUser.js +9 -8
  335. package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
  336. package/lib/server/auth/getMockUser.js +18 -14
  337. package/lib/server/auth/{session.js → getStrategies.js} +17 -16
  338. package/lib/server/auth/headlessUser.js +3 -2
  339. package/lib/server/auth/journeyActor.js +35 -0
  340. package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
  341. package/lib/server/checkPageRecord.js +44 -0
  342. package/lib/server/contextMapBudget.js +24 -0
  343. package/lib/server/contextMapBudget.test.mjs +98 -0
  344. package/lib/server/continueJitKeys.js +28 -0
  345. package/lib/server/continueJitKeys.test.mjs +43 -0
  346. package/lib/server/createDataSessionEndedError.js +33 -0
  347. package/lib/server/createDataSetRefusal.js +26 -0
  348. package/lib/server/createLowdefyContext.js +168 -41
  349. package/lib/server/createLowdefyContext.test.mjs +331 -8
  350. package/lib/server/getClientAddress.js +51 -0
  351. package/lib/server/getClientAddress.test.mjs +56 -0
  352. package/lib/server/getPageJitEnrichment.test.mjs +29 -11
  353. package/lib/server/hashConfigContent.js +26 -0
  354. package/lib/server/importAppCode.test.mjs +57 -0
  355. package/lib/server/isRoutineStep.js +27 -0
  356. package/lib/server/jitPageBuilder.js +309 -126
  357. package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
  358. package/lib/server/jitPageBuilder.test.mjs +218 -0
  359. package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
  360. package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
  361. package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
  362. package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
  363. package/lib/server/journeyCookies.js +22 -0
  364. package/lib/server/journeyCookies.test.mjs +145 -0
  365. package/lib/server/loadDynamicJsMap.js +47 -0
  366. package/lib/server/log/createHandleError.js +38 -2
  367. package/lib/server/log/createHandleError.test.mjs +125 -2
  368. package/lib/server/log/createLogger.js +3 -0
  369. package/lib/server/log/createLogger.test.mjs +15 -0
  370. package/lib/server/log/logRequest.js +2 -3
  371. package/lib/server/mutantDataSetWrappers.test.mjs +115 -0
  372. package/lib/server/mutants/applyMutant.js +39 -0
  373. package/lib/server/mutants/artifactPatterns.js +25 -0
  374. package/lib/server/mutants/buildAnchor.js +45 -0
  375. package/lib/server/mutants/collectSharedIds.js +41 -0
  376. package/lib/server/mutants/configPathSegments.js +38 -0
  377. package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
  378. package/lib/server/mutants/describeEvent.js +22 -0
  379. package/lib/server/mutants/enumerateArtifact.js +56 -0
  380. package/lib/server/mutants/findNodeByKey.js +45 -0
  381. package/lib/server/mutants/findNodeByPointer.js +49 -0
  382. package/lib/server/mutants/getArtifactKind.js +32 -0
  383. package/lib/server/mutants/groupMutantCopies.js +56 -0
  384. package/lib/server/mutants/hashArtifact.js +25 -0
  385. package/lib/server/mutants/hashMutantTarget.js +34 -0
  386. package/lib/server/mutants/listMutants.js +122 -0
  387. package/lib/server/mutants/listMutants.test.mjs +502 -0
  388. package/lib/server/mutants/mutantId.js +30 -0
  389. package/lib/server/mutants/mutantRuns.js +57 -0
  390. package/lib/server/mutants/mutantRuns.test.mjs +136 -0
  391. package/lib/server/mutants/operators/dropAction.js +47 -0
  392. package/lib/server/mutants/operators/dropBlock.js +44 -0
  393. package/lib/server/mutants/operators/dropPayload.js +52 -0
  394. package/lib/server/mutants/operators/dropStep.js +44 -0
  395. package/lib/server/mutants/operators/flipVisible.js +53 -0
  396. package/lib/server/mutants/operators/index.js +37 -0
  397. package/lib/server/mutants/operators/retargetLink.js +72 -0
  398. package/lib/server/mutants/operators/skipValidate.js +77 -0
  399. package/lib/server/mutants/operators/swapIf.js +112 -0
  400. package/lib/server/mutants/operators.test.mjs +396 -0
  401. package/lib/server/mutants/positionFreePath.js +38 -0
  402. package/lib/server/mutants/removeFromParent.js +28 -0
  403. package/lib/server/mutants/test/fixtures.mjs +173 -0
  404. package/lib/server/mutants/test/keyArtifact.mjs +63 -0
  405. package/lib/server/mutants/validateMutantParam.js +69 -0
  406. package/lib/server/mutants/validateMutantsBody.js +88 -0
  407. package/lib/server/mutants/walkArtifact.js +179 -0
  408. package/lib/server/pageBuildRecords.js +131 -0
  409. package/lib/server/pageBuildRecords.test.mjs +258 -0
  410. package/lib/server/pageCache.mjs +22 -7
  411. package/lib/server/pageCache.test.mjs +22 -7
  412. package/lib/server/pageRebuilds.test.mjs +543 -0
  413. package/lib/server/pruneJitMaps.js +36 -0
  414. package/lib/server/pruneJitMaps.test.mjs +55 -0
  415. package/lib/server/readInstanceRecord.js +33 -0
  416. package/lib/server/readInstanceRecord.test.mjs +99 -0
  417. package/lib/server/readMergedMaps.js +86 -0
  418. package/lib/server/readMergedMaps.test.mjs +79 -0
  419. package/lib/server/recording/appendRecords.js +33 -0
  420. package/lib/server/recording/errorRecordingStamp.js +34 -0
  421. package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
  422. package/lib/server/recording/isRecordingEnabled.js +31 -0
  423. package/lib/server/recording/readRecordingCookie.js +71 -0
  424. package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
  425. package/lib/server/recording/recordingCookiePayload.js +33 -0
  426. package/lib/server/recording/servedBuilds.js +46 -0
  427. package/lib/server/recording/servedBuilds.test.mjs +42 -0
  428. package/lib/server/sinkExternalConnections.js +95 -0
  429. package/lib/server/sinkExternalConnections.test.mjs +135 -0
  430. package/lib/server/skipStaleMapWrites.js +62 -0
  431. package/lib/server/skipStaleMapWrites.test.mjs +113 -0
  432. package/lib/server/startIdleGc.js +67 -0
  433. package/lib/server/startIdleGc.test.mjs +138 -0
  434. package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
  435. package/lib/server/trackSessionWork.js +30 -0
  436. package/lib/vite/createDevServerExclude.js +40 -0
  437. package/lib/vite/createDevServerExclude.test.mjs +79 -0
  438. package/manager/getContext.mjs +25 -3
  439. package/manager/processes/initialBuild.mjs +17 -1
  440. package/manager/processes/initialBuild.test.mjs +104 -0
  441. package/manager/processes/lowdefyBuild.mjs +31 -7
  442. package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +72 -0
  443. package/manager/processes/lowdefyBuild.test.mjs +136 -1
  444. package/manager/processes/optimizeDependencies.mjs +59 -0
  445. package/manager/processes/restartServer.mjs +34 -6
  446. package/manager/processes/restartServer.test.mjs +162 -0
  447. package/manager/processes/shutdownServer.mjs +11 -0
  448. package/manager/processes/shutdownServer.test.mjs +45 -0
  449. package/manager/processes/startFirstServer.mjs +46 -0
  450. package/manager/processes/startFirstServer.test.mjs +326 -0
  451. package/manager/processes/startMailSink.mjs +135 -0
  452. package/manager/processes/startMailSink.test.mjs +146 -0
  453. package/manager/processes/startProxy.mjs +129 -12
  454. package/manager/processes/startProxy.test.mjs +366 -0
  455. package/manager/processes/startServer.mjs +47 -6
  456. package/manager/processes/startServer.test.mjs +155 -0
  457. package/manager/processes/startWatchers.mjs +6 -4
  458. package/manager/processes/syncServer.mjs +69 -0
  459. package/manager/processes/syncServer.test.mjs +141 -0
  460. package/manager/processes/waitForDevServer.mjs +48 -0
  461. package/manager/processes/waitForDevServer.test.mjs +112 -0
  462. package/manager/run.mjs +128 -43
  463. package/manager/utils/BatchChanges.mjs +21 -5
  464. package/manager/utils/BatchChanges.test.mjs +77 -0
  465. package/manager/utils/acquireDevInstance.mjs +99 -0
  466. package/manager/utils/acquireDevInstance.test.mjs +160 -0
  467. package/manager/utils/createBuildActivity.mjs +67 -0
  468. package/manager/utils/createBuildActivity.test.mjs +58 -0
  469. package/manager/utils/createChangeSignal.mjs +39 -0
  470. package/manager/utils/createChangeSignal.test.mjs +62 -0
  471. package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
  472. package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
  473. package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
  474. package/manager/utils/createDotPathIgnore.mjs +44 -0
  475. package/manager/utils/createDotPathIgnore.test.mjs +65 -0
  476. package/manager/utils/createIdleGc.mjs +51 -0
  477. package/manager/utils/createIdleGc.test.mjs +77 -0
  478. package/manager/utils/createRequestActivity.mjs +100 -0
  479. package/manager/utils/createRequestActivity.test.mjs +121 -0
  480. package/manager/utils/createServerArtifactTracker.mjs +83 -0
  481. package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
  482. package/manager/utils/createServerEnv.mjs +39 -0
  483. package/manager/utils/filesHaveSameBytes.mjs +41 -0
  484. package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
  485. package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
  486. package/manager/utils/importFresh.mjs +46 -0
  487. package/manager/utils/importFresh.test.mjs +72 -0
  488. package/manager/utils/importFreshWorker.mjs +20 -0
  489. package/manager/utils/killTaggedBrowser.mjs +35 -0
  490. package/manager/utils/pagePathChanged.mjs +56 -0
  491. package/manager/utils/pruneRecordings.mjs +85 -0
  492. package/manager/utils/pruneRecordings.test.mjs +117 -0
  493. package/manager/utils/publishBuildDirectory.mjs +121 -0
  494. package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
  495. package/manager/utils/publishBuildDirectory.test.mjs +262 -0
  496. package/manager/utils/readBasePath.mjs +27 -0
  497. package/manager/utils/readBasePath.test.mjs +43 -0
  498. package/manager/utils/readBuildStatusWait.mjs +67 -0
  499. package/manager/utils/readManagerVersion.mjs +30 -0
  500. package/manager/utils/readManagerVersion.test.mjs +35 -0
  501. package/manager/utils/readPluginDefinitions.mjs +36 -0
  502. package/manager/utils/resolveDevAuthUrl.mjs +58 -0
  503. package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
  504. package/manager/utils/resolvePorts.mjs +49 -0
  505. package/manager/utils/setupWatcher.mjs +19 -11
  506. package/manager/utils/startRecordingPruner.mjs +45 -0
  507. package/manager/utils/startRecordingPruner.test.mjs +66 -0
  508. package/manager/utils/waitForChildExit.mjs +53 -0
  509. package/manager/utils/waitForChildExit.test.mjs +70 -0
  510. package/manager/utils/waitForServer.mjs +44 -0
  511. package/manager/utils/writeBuildStatus.mjs +1 -1
  512. package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
  513. package/manager/watchers/envWatcher.mjs +9 -2
  514. package/manager/watchers/envWatcher.test.mjs +87 -0
  515. package/manager/watchers/listServerSidePackages.mjs +47 -0
  516. package/manager/watchers/listServerSidePackages.test.mjs +38 -0
  517. package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
  518. package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
  519. package/manager/watchers/pluginSourceWatcher.mjs +79 -0
  520. package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
  521. package/manager/watchers/restartRequestWatcher.mjs +71 -0
  522. package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
  523. package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
  524. package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
  525. package/manager/watchers/serverPackageWatcher.mjs +38 -0
  526. package/package.json +60 -47
  527. package/package.original.json +60 -47
  528. package/src/app.js +131 -20
  529. package/src/html/renderDevPage.js +63 -11
  530. package/src/html/renderDevPage.test.mjs +138 -0
  531. package/src/lib/getRequestPath.js +29 -0
  532. package/src/middleware/createSameOriginGuard.js +37 -0
  533. package/src/middleware/createSameOriginGuard.test.mjs +102 -0
  534. package/src/middleware/errorHandler.js +51 -1
  535. package/src/middleware/errorHandler.test.mjs +173 -3
  536. package/src/middleware/isRebindingSafeHost.js +44 -0
  537. package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
  538. package/src/middleware/localDevToolsOnly.js +38 -0
  539. package/src/middleware/localDevToolsOnly.test.mjs +61 -0
  540. package/src/middleware/noStore.js +27 -0
  541. package/src/middleware/noStore.test.mjs +48 -0
  542. package/src/middleware/requestTimeout.js +38 -0
  543. package/src/middleware/staleFlag.js +76 -0
  544. package/src/middleware/staleFlag.test.mjs +127 -0
  545. package/src/middleware/stampCaller.js +38 -0
  546. package/src/middleware/stampCaller.test.mjs +64 -0
  547. package/src/routes/agent.js +15 -3
  548. package/src/routes/auth.js +29 -20
  549. package/src/routes/auth.test.mjs +30 -56
  550. package/src/routes/clientError.js +13 -13
  551. package/src/routes/cron.js +3 -1
  552. package/src/routes/cronForward.js +3 -1
  553. package/src/routes/detached.js +22 -9
  554. package/src/routes/devInspect.js +17 -8
  555. package/src/routes/devInspect.test.mjs +44 -7
  556. package/src/routes/devRecording.js +113 -0
  557. package/src/routes/devRecording.test.mjs +253 -0
  558. package/src/routes/docs/buildStatus.js +10 -2
  559. package/src/routes/docs/buildStatus.test.mjs +46 -0
  560. package/src/routes/docs/content.js +5 -3
  561. package/src/routes/docs/evalOperator.js +15 -1
  562. package/src/routes/docs/events.js +50 -0
  563. package/src/routes/docs/events.test.mjs +117 -0
  564. package/src/routes/docs/find.js +9 -1
  565. package/src/routes/docs/icons.js +30 -0
  566. package/src/routes/docs/inspectState.js +16 -1
  567. package/src/routes/docs/journey.js +194 -0
  568. package/src/routes/docs/journey.test.mjs +401 -0
  569. package/src/routes/docs/journeyRefusals.test.mjs +71 -0
  570. package/src/routes/docs/loadState.js +4 -2
  571. package/src/routes/docs/mcp.js +34 -3
  572. package/src/routes/docs/mcp.test.mjs +130 -0
  573. package/src/routes/docs/mutants.js +78 -0
  574. package/src/routes/docs/mutants.test.mjs +180 -0
  575. package/src/routes/docs/parsePathParamsParam.js +41 -0
  576. package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
  577. package/src/routes/docs/parseUserParam.js +27 -21
  578. package/src/routes/docs/parseUserParam.test.mjs +17 -5
  579. package/src/routes/docs/restart.js +35 -0
  580. package/src/routes/docs/restart.test.mjs +64 -0
  581. package/src/routes/docs/runEndpoint.js +54 -0
  582. package/src/routes/docs/runEndpoint.test.mjs +165 -0
  583. package/src/routes/docs/runRequest.js +25 -8
  584. package/src/routes/docs/runRequest.test.mjs +92 -0
  585. package/src/routes/docs/screenshot.js +50 -2
  586. package/src/routes/docs/screenshot.test.mjs +117 -0
  587. package/src/routes/docs/search.js +15 -2
  588. package/src/routes/docs/snapshotState.js +17 -3
  589. package/src/routes/endpoints.js +27 -26
  590. package/src/routes/endpointsTextAnswer.test.mjs +77 -0
  591. package/src/routes/feedback.js +10 -13
  592. package/src/routes/jitPage.js +99 -35
  593. package/src/routes/jitPage.test.mjs +248 -0
  594. package/src/routes/mcp.js +82 -5
  595. package/src/routes/mcp.test.mjs +215 -0
  596. package/src/routes/mcpMemberToken.test.mjs +125 -0
  597. package/src/routes/mcpProtectedResourceMetadata.js +34 -0
  598. package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
  599. package/src/routes/mountDevPageRoutes.js +25 -0
  600. package/src/routes/mountDevPageRoutes.test.mjs +30 -0
  601. package/src/routes/mountOauthDiscovery.js +44 -0
  602. package/src/routes/mountOauthDiscovery.test.mjs +79 -0
  603. package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
  604. package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
  605. package/src/routes/reload.js +16 -0
  606. package/src/routes/request.js +6 -4
  607. package/src/routes/request.test.mjs +9 -5
  608. package/src/routes/requestBody.test.mjs +159 -0
  609. package/src/routes/usage.js +6 -2
  610. package/src/routes/user.js +27 -0
  611. package/src/routes/websocket.js +29 -8
  612. package/src/routes/websocket.test.mjs +65 -0
  613. package/src/routes/wellKnownFallback.js +30 -0
  614. package/src/routes/wellKnownFallback.test.mjs +32 -0
  615. package/src/websocket/devWebSocket.js +41 -26
  616. package/src/websocket/devWebSocket.test.mjs +157 -0
  617. package/vite.config.js +53 -23
  618. package/lib/server/auth/getDevSession.js +0 -70
  619. package/lib/server/auth/getDevSession.test.mjs +0 -138
  620. package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
  621. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
  622. package/manager/watchers/moduleBuildWatcher.mjs +0 -79
  623. package/manager/watchers/serverArtifactWatcher.mjs +0 -120
  624. package/src/routes/apiPage.js +0 -37
@@ -0,0 +1,405 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ /*
5
+ Copyright 2020-2026 Lowdefy, Inc
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
18
+ */
19
+
20
+ import { jest } from '@jest/globals';
21
+ // Only the validator: the package index loads undici, which needs Node globals jsdom lacks.
22
+ import validateTraceRecord from '@lowdefy/node-utils/journeyTrace/validateTraceRecord.js';
23
+
24
+ import createPairingBuffer, { HOLD_MS } from './createPairingBuffer.js';
25
+ import createPasswordRedactor from './createPasswordRedactor.js';
26
+
27
+ const START = Date.parse('2026-10-03T14:00:00.000Z');
28
+ const SESSION = '20261003T140000Z-k3x9qa';
29
+ const BUILD_A = '2026-10-03T13:58:00.000Z';
30
+ const BUILD_B = '2026-10-03T14:00:05.000Z';
31
+ let records;
32
+ let buffer;
33
+ let build;
34
+
35
+ // A stand-in for the engine's describeElement: block ids from the bl-
36
+ // wrappers, innermost first, and the element's text.
37
+ function describe(element) {
38
+ const blockIds = [];
39
+ for (let node = element; node !== null; node = node.parentElement) {
40
+ if (node.id?.startsWith('bl-')) blockIds.push(node.id.slice(3));
41
+ }
42
+ const text = element.textContent.trim();
43
+ return {
44
+ page_id: 'tickets',
45
+ block_id: blockIds[0] ?? null,
46
+ block_type: null,
47
+ row: null,
48
+ column: null,
49
+ text: text === '' ? null : text,
50
+ nth: null,
51
+ option: element.getAttribute('role') === 'option',
52
+ block_ids: blockIds,
53
+ };
54
+ }
55
+
56
+ function payload({
57
+ blockId,
58
+ eventName = 'onClick',
59
+ at,
60
+ debounceMs = 0,
61
+ scope = 'page',
62
+ success = true,
63
+ failure = null,
64
+ state = {},
65
+ stateBefore = {},
66
+ requests = {},
67
+ responses = {},
68
+ }) {
69
+ return {
70
+ scope,
71
+ pageId: 'tickets',
72
+ blockId,
73
+ blockType: 'Button',
74
+ eventName,
75
+ success,
76
+ failure,
77
+ debounceMs,
78
+ actions: [],
79
+ record: { startTimestamp: new Date(START + at), responses },
80
+ context: { state, requests },
81
+ stateBefore,
82
+ };
83
+ }
84
+
85
+ function interact({ element, kind = 'click', at, value }) {
86
+ buffer.addInteraction({
87
+ t: START + at,
88
+ kind,
89
+ element,
90
+ target: describe(element),
91
+ pageId: 'tickets',
92
+ value,
93
+ });
94
+ }
95
+
96
+ function event(options) {
97
+ jest.setSystemTime(START + options.arrive);
98
+ buffer.addEvent(payload(options), { urlAfter: '/tickets' });
99
+ }
100
+
101
+ function closeAll() {
102
+ jest.advanceTimersByTime(HOLD_MS + 60000);
103
+ }
104
+
105
+ function el(id) {
106
+ return document.getElementById(id);
107
+ }
108
+
109
+ beforeEach(() => {
110
+ jest.useFakeTimers({ doNotFake: ['performance'], now: START });
111
+ records = [];
112
+ buffer = createPairingBuffer({
113
+ onRecord: (record) => records.push(record),
114
+ getSession: () => SESSION,
115
+ getRoles: () => ['support'],
116
+ getBuild: () => build,
117
+ redactor: createPasswordRedactor(),
118
+ });
119
+ build = BUILD_A;
120
+ document.body.innerHTML = `
121
+ <div id="bl-card"><div id="bl-save"><button id="save-button">Save</button></div></div>
122
+ <div id="bl-table"><div id="row">Row 1</div></div>
123
+ <div id="bl-search"><input id="search_input" /></div>
124
+ <div id="bl-status"><div class="popup"><div id="option-open" role="option">Open</div></div></div>
125
+ <div id="bl-password"><input id="password_input" type="password" /></div>
126
+ <div id="bl-reveal"><input id="reveal_input" type="password" /><span id="reveal-eye">eye</span></div>
127
+ <div id="bl-users"><div id="bl-users.0.pw"><input id="users.0.pw_input" type="password" /></div></div>
128
+ <div id="bl-outer"><div id="bl-inner"><button id="inner-button">Go</button></div></div>
129
+ <div id="blank"></div>
130
+ `;
131
+ });
132
+
133
+ afterEach(() => {
134
+ jest.useRealTimers();
135
+ });
136
+
137
+ test('typing then a debounced trailing onChange pairs the event with the last keystroke only', () => {
138
+ [0, 100, 200].forEach((at, index) => {
139
+ interact({ element: el('search_input'), kind: 'change', at, value: 'abc'.slice(0, index + 1) });
140
+ });
141
+ event({ blockId: 'search', eventName: 'onChange', at: 700, arrive: 710, debounceMs: 500 });
142
+ closeAll();
143
+ expect(records.map((record) => record.kind)).toEqual(['change', 'change', 'change']);
144
+ expect(records.map((record) => record.value)).toEqual(['a', 'ab', 'abc']);
145
+ expect(records[0].event).toBe(null);
146
+ expect(records[1].event).toBe(null);
147
+ expect(records[2].event).toMatchObject({ name: 'onChange', block_id: 'search' });
148
+ });
149
+
150
+ test('a mount-time request becomes a kind engine record and pairs with nothing', () => {
151
+ interact({ element: el('save-button'), at: 0 });
152
+ event({
153
+ blockId: 'tickets',
154
+ eventName: 'onMount',
155
+ at: 10,
156
+ arrive: 200,
157
+ context: undefined,
158
+ requests: { get_tickets: [{ actionId: 'fetch', blockId: 'tickets', responseTime: 40 }] },
159
+ responses: { fetch: { type: 'Request' } },
160
+ });
161
+ expect(records).toHaveLength(1);
162
+ expect(records[0]).toMatchObject({
163
+ kind: 'engine',
164
+ scope: 'page',
165
+ page_id: 'tickets',
166
+ target: null,
167
+ event: { name: 'onMount', requests: [{ id: 'get_tickets', ok: true, ms: 40 }] },
168
+ });
169
+ closeAll();
170
+ expect(records[1]).toMatchObject({ kind: 'click', event: null });
171
+ });
172
+
173
+ test('an app event becomes a kind engine record with scope app', () => {
174
+ event({ blockId: 'root', eventName: 'onInit', scope: 'app', at: 0, arrive: 5 });
175
+ expect(records[0]).toMatchObject({ kind: 'engine', scope: 'app' });
176
+ });
177
+
178
+ test("a click on a Button with no actions pairs with the enclosing Card's onClick", () => {
179
+ interact({ element: el('save-button'), at: 0 });
180
+ event({ blockId: 'card', at: 20, arrive: 30 });
181
+ closeAll();
182
+ expect(records).toHaveLength(1);
183
+ expect(records[0]).toMatchObject({
184
+ kind: 'click',
185
+ target: { block_id: 'save', text: 'Save' },
186
+ event: { name: 'onClick', block_id: 'card' },
187
+ });
188
+ });
189
+
190
+ test('a Table onSelectionChange during a click on a Button outside it becomes a kind engine record', () => {
191
+ interact({ element: el('save-button'), at: 0 });
192
+ event({ blockId: 'table', eventName: 'onSelectionChange', at: 50, arrive: 60 });
193
+ closeAll();
194
+ expect(records.map((record) => record.kind)).toEqual(['engine', 'click']);
195
+ expect(records[1].event).toBe(null);
196
+ });
197
+
198
+ test("a Selector popup option click pairs with the Selector's onChange", () => {
199
+ interact({ element: el('option-open'), at: 0 });
200
+ event({ blockId: 'status', eventName: 'onChange', at: 15, arrive: 20 });
201
+ closeAll();
202
+ expect(records[0]).toMatchObject({
203
+ kind: 'click',
204
+ target: { block_id: 'status', option: true, text: 'Open' },
205
+ event: { name: 'onChange', block_id: 'status' },
206
+ });
207
+ });
208
+
209
+ test('bubble: true with two handlers records the inner event and the outer in also', () => {
210
+ interact({ element: el('inner-button'), at: 0 });
211
+ event({ blockId: 'outer', at: 5, arrive: 900 });
212
+ event({ blockId: 'inner', at: 5, arrive: 40 });
213
+ closeAll();
214
+ expect(records[0].event).toMatchObject({ block_id: 'inner' });
215
+ expect(records[0].also).toEqual([expect.objectContaining({ block_id: 'outer' })]);
216
+ });
217
+
218
+ test('a slow event that finishes after a second still pairs with its click', () => {
219
+ interact({ element: el('save-button'), at: 0 });
220
+ event({ blockId: 'save', at: 10, arrive: 4000 });
221
+ closeAll();
222
+ expect(records).toHaveLength(1);
223
+ expect(records[0].event).toMatchObject({ block_id: 'save' });
224
+ });
225
+
226
+ test('a click with no Lowdefy event is recorded with event null, and a click on nothing is dropped', () => {
227
+ interact({ element: el('save-button'), at: 0 });
228
+ interact({ element: el('blank'), at: 10 });
229
+ closeAll();
230
+ expect(records).toHaveLength(1);
231
+ expect(records[0]).toMatchObject({ kind: 'click', event: null });
232
+ });
233
+
234
+ test('a record keeps page_id on the record, not the target, and a failed event carries its error', () => {
235
+ interact({ element: el('save-button'), at: 0 });
236
+ event({
237
+ blockId: 'save',
238
+ at: 10,
239
+ arrive: 50,
240
+ success: false,
241
+ failure: {
242
+ actionId: 'validate',
243
+ actionType: 'Validate',
244
+ configKey: 'k1',
245
+ errorName: 'UserError',
246
+ invalidBlocks: ['assignee'],
247
+ },
248
+ });
249
+ closeAll();
250
+ const [record] = records;
251
+ expect(record.page_id).toBe('tickets');
252
+ expect(record.target).not.toHaveProperty('page_id');
253
+ expect(record.target).not.toHaveProperty('block_ids');
254
+ expect(record.roles).toEqual(['support']);
255
+ expect(record.event).toMatchObject({
256
+ success: false,
257
+ error: { name: 'UserError', config_key: 'k1', action_type: 'Validate', action_id: 'validate' },
258
+ invalid_blocks: ['assignee'],
259
+ });
260
+ });
261
+
262
+ test('a password value and its state writes are redacted, including a later SetState echo', () => {
263
+ interact({ element: el('password_input'), kind: 'change', at: 0, value: 'hunter2' });
264
+ event({
265
+ blockId: 'password',
266
+ eventName: 'onChange',
267
+ at: 5,
268
+ arrive: 10,
269
+ stateBefore: {},
270
+ state: { password: 'hunter2', other: 'x' },
271
+ });
272
+ event({
273
+ blockId: 'save',
274
+ at: 20000,
275
+ arrive: 20010,
276
+ stateBefore: { password: 'hunter2' },
277
+ state: { password: 'hunter3' },
278
+ });
279
+ closeAll();
280
+ const change = records.find((record) => record.kind === 'change');
281
+ expect(change).toMatchObject({ value: null, redacted: true });
282
+ expect(change.event.state_writes).toEqual([
283
+ { path: 'password', type: 'string', value: null, redacted: true },
284
+ { path: 'other', type: 'string', value: 'x' },
285
+ ]);
286
+ const echo = records.find((record) => record.kind === 'engine');
287
+ expect(echo.event.state_writes).toEqual([
288
+ { path: 'password', type: 'string', value: null, redacted: true },
289
+ ]);
290
+ expect(JSON.stringify(records)).not.toContain('hunter');
291
+ });
292
+
293
+ test('a password revealed with the visibility toggle before typing is still redacted', () => {
294
+ interact({ element: el('reveal-eye'), at: 0 });
295
+ el('reveal_input').type = 'text';
296
+ interact({ element: el('reveal_input'), kind: 'change', at: 100, value: 'hunter2' });
297
+ event({
298
+ blockId: 'reveal',
299
+ eventName: 'onChange',
300
+ at: 105,
301
+ arrive: 110,
302
+ state: { reveal: 'hunter2' },
303
+ });
304
+ closeAll();
305
+ const change = records.find((record) => record.kind === 'change');
306
+ expect(change).toMatchObject({ value: null, redacted: true });
307
+ expect(JSON.stringify(records)).not.toContain('hunter');
308
+ });
309
+
310
+ test('a password block inside a list is redacted from the list array write', () => {
311
+ interact({ element: el('users.0.pw_input'), kind: 'change', at: 0, value: 'hunter2' });
312
+ event({
313
+ blockId: 'users.0.pw',
314
+ eventName: 'onChange',
315
+ at: 5,
316
+ arrive: 10,
317
+ stateBefore: { users: [{}] },
318
+ state: { users: [{ pw: 'hunter2' }] },
319
+ });
320
+ closeAll();
321
+ const change = records.find((record) => record.kind === 'change');
322
+ expect(change).toMatchObject({ value: null, redacted: true });
323
+ expect(change.event.state_writes).toEqual([
324
+ { path: 'users', type: 'array', value: null, redacted: true },
325
+ ]);
326
+ expect(JSON.stringify(records)).not.toContain('hunter');
327
+ });
328
+
329
+ test('pageview and back are recorded at once and flushAll ends every hold', () => {
330
+ buffer.addInteraction({ t: START, kind: 'pageview', pageId: 'tickets', url: '/tickets?id=1' });
331
+ buffer.addInteraction({ t: START + 5, kind: 'back', pageId: 'tickets' });
332
+ interact({ element: el('save-button'), at: 10 });
333
+ expect(records.map((record) => record.kind)).toEqual(['pageview', 'back']);
334
+ buffer.flushAll();
335
+ expect(records.map((record) => record.kind)).toEqual(['pageview', 'back', 'click']);
336
+ expect(records[0]).toMatchObject({ url: '/tickets?id=1', target: null, event: null });
337
+ });
338
+
339
+ test('an interaction whose hold closes after the build changed keeps the build it was captured under', () => {
340
+ interact({ element: el('save-button'), at: 0 });
341
+ event({ blockId: 'save', at: 10, arrive: 20 });
342
+ build = BUILD_B;
343
+ event({ blockId: 'save', eventName: 'onSecond', at: 30, arrive: 40 });
344
+ closeAll();
345
+ expect(records).toHaveLength(1);
346
+ expect(records[0]).toMatchObject({ kind: 'click', build: BUILD_A });
347
+ });
348
+
349
+ test('an interaction flushed after the build changed keeps the build it was captured under', () => {
350
+ interact({ element: el('save-button'), at: 0 });
351
+ build = BUILD_B;
352
+ buffer.flushAll();
353
+ expect(records[0]).toMatchObject({ kind: 'click', build: BUILD_A });
354
+ });
355
+
356
+ test('pageview, back and engine records carry the build named when they were captured', () => {
357
+ buffer.addInteraction({ t: START, kind: 'pageview', pageId: 'tickets', url: '/tickets' });
358
+ event({ blockId: 'root', eventName: 'onInit', scope: 'app', at: 5, arrive: 10 });
359
+ build = BUILD_B;
360
+ buffer.addInteraction({ t: START + 20, kind: 'back', pageId: 'tickets' });
361
+ expect(records.map((record) => [record.kind, record.build])).toEqual([
362
+ ['pageview', BUILD_A],
363
+ ['engine', BUILD_A],
364
+ ['back', BUILD_B],
365
+ ]);
366
+ });
367
+
368
+ test('every record the buffer builds passes validateTraceRecord once the route stamps its source', () => {
369
+ buffer.addInteraction({ t: START, kind: 'pageview', pageId: 'tickets', url: '/tickets' });
370
+ interact({ element: el('search_input'), kind: 'change', at: 10, value: 'a' });
371
+ interact({ element: el('password_input'), kind: 'change', at: 20, value: 'secret' });
372
+ buffer.addInteraction({
373
+ t: START + 30,
374
+ kind: 'key',
375
+ key: 'Enter',
376
+ target: describe(el('search_input')),
377
+ pageId: 'tickets',
378
+ });
379
+ event({ blockId: 'search', eventName: 'onChange', at: 15, arrive: 20 });
380
+ event({ blockId: 'table', eventName: 'onSelectionChange', at: 40, arrive: 50 });
381
+ event({ blockId: 'root', eventName: 'onInit', scope: 'app', at: 60, arrive: 70 });
382
+ interact({ element: el('save-button'), at: 80 });
383
+ event({
384
+ blockId: 'save',
385
+ at: 90,
386
+ arrive: 95,
387
+ success: false,
388
+ failure: { errorName: 'Error', invalidBlocks: [] },
389
+ });
390
+ closeAll();
391
+ expect(records.map((record) => record.kind).sort()).toEqual([
392
+ 'change',
393
+ 'change',
394
+ 'click',
395
+ 'engine',
396
+ 'engine',
397
+ 'key',
398
+ 'pageview',
399
+ ]);
400
+ records.forEach((record) => {
401
+ expect(record.build).toBe(BUILD_A);
402
+ expect(validateTraceRecord({ record: { ...record, source: 'dev' } })).toEqual({});
403
+ expect(validateTraceRecord({ record: { ...record, source: 'dev', build: null } })).toEqual({});
404
+ });
405
+ });
@@ -0,0 +1,84 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { journeyTargetSelectors } from '@lowdefy/helpers';
18
+
19
+ const { blockWrapperPrefix } = journeyTargetSelectors;
20
+
21
+ function isPasswordInput(element) {
22
+ return (
23
+ typeof element?.tagName === 'string' &&
24
+ element.tagName.toLowerCase() === 'input' &&
25
+ element.type === 'password'
26
+ );
27
+ }
28
+
29
+ // A write holds a password block's value when its path is the block's id, or
30
+ // an ancestor of it: arrays are diffed as leaves, so a password block inside a
31
+ // List is written as the whole list array.
32
+ function holdsPassword(path, passwordBlocks) {
33
+ if (passwordBlocks.has(path)) return true;
34
+ return [...passwordBlocks].some((blockId) => blockId.startsWith(`${path}.`));
35
+ }
36
+
37
+ function redactWrites(event, passwordBlocks) {
38
+ if (event === null || event === undefined) return;
39
+ (event.state_writes ?? []).forEach((write) => {
40
+ if (holdsPassword(write.path, passwordBlocks)) {
41
+ write.value = null;
42
+ write.redacted = true;
43
+ }
44
+ });
45
+ }
46
+
47
+ // Password inputs are never recorded. A block is a password block when the
48
+ // interaction's element, or its block's `<blockId>_input` element, is an
49
+ // `<input type="password">`. It is remembered for the rest of the tab session,
50
+ // so its value is redacted from every later change and from every state write
51
+ // to its path, a later SetState echo included. It is remembered from any
52
+ // interaction with the block, a click included: the visibility toggle turns
53
+ // the input into `type="text"`, and a click on it comes before the typing.
54
+ function createPasswordRedactor() {
55
+ const passwordBlocks = new Set();
56
+
57
+ function isPassword(element) {
58
+ if (isPasswordInput(element)) return true;
59
+ const wrapper = element?.closest?.(`[id^="${blockWrapperPrefix}"]`);
60
+ if (!wrapper) return false;
61
+ const blockId = wrapper.id.slice(blockWrapperPrefix.length);
62
+ return isPasswordInput(element.ownerDocument?.getElementById(`${blockId}_input`));
63
+ }
64
+
65
+ function remember(blockId) {
66
+ if (typeof blockId === 'string' && blockId !== '') {
67
+ passwordBlocks.add(blockId);
68
+ }
69
+ }
70
+
71
+ function redact(record) {
72
+ if (record.kind === 'change' && passwordBlocks.has(record.target?.block_id)) {
73
+ record.value = null;
74
+ record.redacted = true;
75
+ }
76
+ redactWrites(record.event, passwordBlocks);
77
+ (record.also ?? []).forEach((event) => redactWrites(event, passwordBlocks));
78
+ return record;
79
+ }
80
+
81
+ return { isPassword, redact, remember };
82
+ }
83
+
84
+ export default createPasswordRedactor;
@@ -0,0 +1,54 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ const BATCH_SIZE = 20;
18
+ const BATCH_DELAY_MS = 5000;
19
+
20
+ // Collects finished records and sends them: at 20 records, 5 s after the
21
+ // first record of a batch, or when flush() is called (pagehide, the stream's
22
+ // reload event, a headless run's flush). flush() resolves when every send it
23
+ // started has settled.
24
+ function createRecordBatcher({ send }) {
25
+ let batch = [];
26
+ let timer = null;
27
+
28
+ function flush() {
29
+ if (timer !== null) {
30
+ clearTimeout(timer);
31
+ timer = null;
32
+ }
33
+ if (batch.length === 0) return Promise.resolve();
34
+ const records = batch;
35
+ batch = [];
36
+ return Promise.resolve(send(records)).catch(() => {});
37
+ }
38
+
39
+ function add(record) {
40
+ batch.push(record);
41
+ if (batch.length >= BATCH_SIZE) {
42
+ flush();
43
+ return;
44
+ }
45
+ if (timer === null) {
46
+ timer = setTimeout(flush, BATCH_DELAY_MS);
47
+ }
48
+ }
49
+
50
+ return { add, flush };
51
+ }
52
+
53
+ export { BATCH_DELAY_MS, BATCH_SIZE };
54
+ export default createRecordBatcher;
@@ -0,0 +1,76 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { jest } from '@jest/globals';
18
+
19
+ import createRecordBatcher from './createRecordBatcher.js';
20
+
21
+ beforeEach(() => {
22
+ jest.useFakeTimers({ doNotFake: ['performance'] });
23
+ });
24
+
25
+ afterEach(() => {
26
+ jest.useRealTimers();
27
+ });
28
+
29
+ test('createRecordBatcher sends at 20 records', () => {
30
+ const send = jest.fn();
31
+ const batcher = createRecordBatcher({ send });
32
+ for (let index = 0; index < 19; index += 1) batcher.add({ index });
33
+ expect(send).not.toHaveBeenCalled();
34
+ batcher.add({ index: 19 });
35
+ expect(send).toHaveBeenCalledTimes(1);
36
+ expect(send.mock.calls[0][0]).toHaveLength(20);
37
+ });
38
+
39
+ test('createRecordBatcher sends 5 s after the first record of a batch', () => {
40
+ const send = jest.fn();
41
+ const batcher = createRecordBatcher({ send });
42
+ batcher.add({ index: 0 });
43
+ jest.advanceTimersByTime(4000);
44
+ batcher.add({ index: 1 });
45
+ jest.advanceTimersByTime(999);
46
+ expect(send).not.toHaveBeenCalled();
47
+ jest.advanceTimersByTime(1);
48
+ expect(send).toHaveBeenCalledWith([{ index: 0 }, { index: 1 }]);
49
+ });
50
+
51
+ test('createRecordBatcher flush resolves when the send settles, and swallows a failed send', async () => {
52
+ let settle;
53
+ const send = jest.fn(
54
+ () =>
55
+ new Promise((resolve, reject) => {
56
+ settle = reject;
57
+ })
58
+ );
59
+ const batcher = createRecordBatcher({ send });
60
+ batcher.add({ index: 0 });
61
+ let resolved = false;
62
+ const flushed = batcher.flush().then(() => {
63
+ resolved = true;
64
+ });
65
+ await Promise.resolve();
66
+ expect(resolved).toBe(false);
67
+ settle(new Error('offline'));
68
+ await flushed;
69
+ expect(resolved).toBe(true);
70
+ });
71
+
72
+ test('createRecordBatcher flush with nothing batched sends nothing', async () => {
73
+ const send = jest.fn();
74
+ await createRecordBatcher({ send }).flush();
75
+ expect(send).not.toHaveBeenCalled();
76
+ });