@lowdefy/server-dev 6.1.0 → 7.0.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 (623) 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 +67 -0
  99. package/lib/docs/dataSets/dataSessionRegistry.js +25 -0
  100. package/lib/docs/dataSets/dataSetJourneys.chromium.mongodb.test.mjs +679 -0
  101. package/lib/docs/dataSets/describeDataSetResult.js +25 -0
  102. package/lib/docs/dataSets/describeDatabaseTarget.js +46 -0
  103. package/lib/docs/dataSets/deserializeFixture.js +34 -0
  104. package/lib/docs/dataSets/drainSessionWork.js +37 -0
  105. package/lib/docs/dataSets/drainSessionWork.test.mjs +64 -0
  106. package/lib/docs/dataSets/duplicateIndexName.js +24 -0
  107. package/lib/docs/dataSets/findCollectionCollisions.js +52 -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 +44 -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 +119 -0
  119. package/lib/docs/dataSets/openDataSession.mongodb.test.mjs +343 -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 +68 -0
  126. package/lib/docs/dataSets/readDataSet.test.mjs +220 -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/sweepOrphanDatabases.js +33 -0
  133. package/lib/docs/dataSets/test/app/lowdefy.yaml +203 -0
  134. package/lib/docs/dataSets/test/app/modules/archive/module.lowdefy.yaml +30 -0
  135. package/lib/docs/dataSets/test/app/tests/data/shop.yaml +8 -0
  136. package/lib/docs/dataSets/test/buildTestApp.mjs +43 -0
  137. package/lib/docs/devEventBus.js +136 -0
  138. package/lib/docs/devEventBus.test.mjs +269 -0
  139. package/lib/docs/devToolDefinitions.js +529 -0
  140. package/lib/docs/docSources.js +21 -0
  141. package/lib/docs/docs.test.mjs +171 -15
  142. package/lib/docs/docsIndex.test.mjs +377 -0
  143. package/lib/docs/evalOperator.js +5 -3
  144. package/lib/docs/evalOperatorHeadless.js +84 -12
  145. package/lib/docs/evalOperatorHeadless.test.mjs +7 -1
  146. package/lib/docs/evalOperatorInTab.js +2 -1
  147. package/lib/docs/findConfig.js +95 -49
  148. package/lib/docs/fitResponse.js +81 -0
  149. package/lib/docs/fitResponse.test.mjs +128 -0
  150. package/lib/docs/formatFeedback.js +26 -5
  151. package/lib/docs/formatFeedback.test.mjs +48 -0
  152. package/lib/docs/getAppMap.js +7 -1
  153. package/lib/docs/getBrowser.js +150 -80
  154. package/lib/docs/getBrowser.test.mjs +407 -16
  155. package/lib/docs/getBuildId.js +61 -0
  156. package/lib/docs/getBuildId.test.mjs +84 -0
  157. package/lib/docs/getBuildStatus.js +54 -6
  158. package/lib/docs/getBuildStatus.test.mjs +133 -0
  159. package/lib/docs/getBuildStatusAfterEdits.js +35 -0
  160. package/lib/docs/getBuildStatusAfterEdits.test.mjs +76 -0
  161. package/lib/docs/getCoreDoc.js +9 -1
  162. package/lib/docs/getDoc.js +100 -0
  163. package/lib/docs/getDocsIndex.js +152 -0
  164. package/lib/docs/getHazards.js +115 -0
  165. package/lib/docs/getHazards.test.mjs +175 -0
  166. package/lib/docs/getMailOutboxDirectory.js +26 -0
  167. package/lib/docs/getOverview.js +79 -13
  168. package/lib/docs/getOverviewBasePath.test.mjs +37 -0
  169. package/lib/docs/getPageBuildStatus.js +46 -0
  170. package/lib/docs/getPluginDoc.js +21 -23
  171. package/lib/docs/getSchema.js +33 -1
  172. package/lib/docs/getStaleStatus.js +36 -0
  173. package/lib/docs/getStaleStatus.test.mjs +74 -0
  174. package/lib/docs/guardJourneyOrigin.js +39 -0
  175. package/lib/docs/inspectState.js +5 -3
  176. package/lib/docs/inspectStateFromTab.js +2 -2
  177. package/lib/docs/inspectStateHeadless.js +89 -27
  178. package/lib/docs/inspectStateHeadless.test.mjs +20 -1
  179. package/lib/docs/installHeadlessShell.failure.test.mjs +45 -0
  180. package/lib/docs/installHeadlessShell.js +123 -0
  181. package/lib/docs/installHeadlessShell.test.mjs +70 -0
  182. package/lib/docs/installHeadlessShell.timeout.test.mjs +72 -0
  183. package/lib/docs/isHeadlessShellIncomplete.js +34 -0
  184. package/lib/docs/isHeadlessShellIncomplete.test.mjs +63 -0
  185. package/lib/docs/isPageReady.js +77 -0
  186. package/lib/docs/isPageReady.test.mjs +235 -0
  187. package/lib/docs/isRunErrorEntry.js +25 -0
  188. package/lib/docs/journeyMutantCookie.chromium.test.mjs +150 -0
  189. package/lib/docs/journeyRunRecording.js +35 -0
  190. package/lib/docs/journeyRunRecording.test.mjs +39 -0
  191. package/lib/docs/keepEventStreamAlive.js +68 -0
  192. package/lib/docs/keepEventStreamAlive.test.mjs +77 -0
  193. package/lib/docs/launchBrowser.js +83 -0
  194. package/lib/docs/launchBrowser.test.mjs +175 -0
  195. package/lib/docs/listModuleDocEntries.js +69 -0
  196. package/lib/docs/listPackageDocFiles.js +67 -0
  197. package/lib/docs/listPluginDocEntries.js +111 -0
  198. package/lib/docs/listPlugins.js +9 -58
  199. package/lib/docs/listTypes.js +14 -6
  200. package/lib/docs/loadFrameworkHazards.js +40 -0
  201. package/lib/docs/loadFrameworkHazards.test.mjs +43 -0
  202. package/lib/docs/loadState.js +103 -35
  203. package/lib/docs/makeModuleManifestDoc.js +174 -0
  204. package/lib/docs/matchExercisedPages.js +35 -0
  205. package/lib/docs/matchJourneyRoute.js +69 -0
  206. package/lib/docs/matchJourneyRoute.test.mjs +178 -0
  207. package/lib/docs/mergeNetworkSnapshots.js +49 -0
  208. package/lib/docs/nextJourneySequence.js +30 -0
  209. package/lib/docs/noBrowserError.js +22 -0
  210. package/lib/docs/observe/TRANSIENT_SELECTOR.js +21 -0
  211. package/lib/docs/observe/claimExpectedErrors.js +67 -0
  212. package/lib/docs/observe/createJourneyAppErrors.js +236 -0
  213. package/lib/docs/observe/createJourneyAppErrors.test.mjs +385 -0
  214. package/lib/docs/observe/evaluateInvariants.js +183 -0
  215. package/lib/docs/observe/evaluateInvariants.test.mjs +299 -0
  216. package/lib/docs/observe/findingKey.js +29 -0
  217. package/lib/docs/observe/hasNoEffect.js +31 -0
  218. package/lib/docs/observe/hasNoEffect.test.mjs +37 -0
  219. package/lib/docs/observe/installStepObserver.js +72 -0
  220. package/lib/docs/observe/isAppApiUrl.js +33 -0
  221. package/lib/docs/observe/parseAppApiUrl.js +37 -0
  222. package/lib/docs/observe/readStepObserver.js +42 -0
  223. package/lib/docs/observe/relativeSource.js +29 -0
  224. package/lib/docs/observe/resolveConfigKeySource.js +33 -0
  225. package/lib/docs/observe/takeJourneyEvents.js +36 -0
  226. package/lib/docs/observe/toAppErrorFailure.js +61 -0
  227. package/lib/docs/observe/usesSearchStage.js +45 -0
  228. package/lib/docs/observe/waitForClientErrorReports.js +31 -0
  229. package/lib/docs/observe/watchJourneyContext.js +75 -0
  230. package/lib/docs/observe/withAppErrors.js +31 -0
  231. package/lib/docs/observe/withAppErrors.test.mjs +101 -0
  232. package/lib/docs/openJourney.js +94 -0
  233. package/lib/docs/openJourneyEmail.js +41 -0
  234. package/lib/docs/openJourneyEmail.test.mjs +187 -0
  235. package/lib/docs/pageBlocks.js +47 -0
  236. package/lib/docs/pageBuildStatus.test.mjs +103 -0
  237. package/lib/{client/setPageId.js → docs/parseModuleSource.js} +11 -12
  238. package/lib/docs/rails/authEngineActions.js +53 -0
  239. package/lib/docs/rails/authEngineActions.test.mjs +69 -0
  240. package/lib/docs/rails/authEngineMethods.js +52 -0
  241. package/lib/docs/rails/authEngineMethods.test.mjs +36 -0
  242. package/lib/docs/rails/checkAuthActionsRan.js +66 -0
  243. package/lib/docs/rails/checkAuthActionsRan.test.mjs +149 -0
  244. package/lib/docs/rails/checkClickRails.js +56 -0
  245. package/lib/docs/rails/createDataSetRails.js +82 -0
  246. package/lib/docs/rails/createDataSetRails.test.mjs +172 -0
  247. package/lib/docs/rails/findExternalBlocks.js +147 -0
  248. package/lib/docs/rails/findExternalBlocks.test.mjs +254 -0
  249. package/lib/docs/rails/installAuthActionRecorder.js +56 -0
  250. package/lib/docs/rails/readAuthActionRecorder.js +32 -0
  251. package/lib/docs/rails/readBuiltArtifact.js +30 -0
  252. package/lib/docs/readConnectionPlugins.js +26 -0
  253. package/lib/docs/readDevAuthMode.js +32 -0
  254. package/lib/docs/readDocEntry.js +40 -0
  255. package/lib/docs/readDocTitle.js +26 -0
  256. package/lib/docs/readJourneyEmailMatch.js +57 -0
  257. package/lib/docs/readJourneyEmailMatch.test.mjs +110 -0
  258. package/lib/docs/readJourneySession.js +56 -0
  259. package/lib/docs/readMailOutbox.js +37 -0
  260. package/lib/docs/readPagePath.js +27 -0
  261. package/lib/docs/readPluginPackages.js +99 -0
  262. package/lib/docs/readProxyBuildWait.js +38 -0
  263. package/lib/docs/readRoutineSteps.js +45 -0
  264. package/lib/docs/registerShownTextEngine.js +118 -0
  265. package/lib/docs/requestRestart.js +30 -0
  266. package/lib/docs/requestRestart.test.mjs +49 -0
  267. package/lib/docs/resolvePageInstance.js +46 -0
  268. package/lib/docs/resolvePageInstance.test.mjs +81 -0
  269. package/lib/docs/resolveSource.js +11 -7
  270. package/lib/docs/resolveSource.test.mjs +1 -1
  271. package/lib/docs/resolveToolCaller.js +62 -0
  272. package/lib/docs/reviewPage.js +51 -0
  273. package/lib/docs/reviewPageBuilds.js +56 -0
  274. package/lib/docs/reviewPageBuilds.test.mjs +207 -0
  275. package/lib/docs/runCheck.js +75 -0
  276. package/lib/docs/runCheck.test.mjs +91 -0
  277. package/lib/docs/runCheckChild.js +49 -0
  278. package/lib/docs/runEndpoint.js +185 -0
  279. package/lib/docs/runEndpoint.test.mjs +509 -0
  280. package/lib/docs/runErrorBuffers.js +99 -0
  281. package/lib/docs/runErrorBuffers.test.mjs +95 -0
  282. package/lib/docs/runJourney.js +347 -0
  283. package/lib/docs/runJourney.test.mjs +2383 -0
  284. package/lib/docs/runJourneyAsUsers.js +44 -0
  285. package/lib/docs/runJourneyDataSet.test.mjs +274 -0
  286. package/lib/docs/runJourneySteps.js +1320 -0
  287. package/lib/docs/runRequest.js +100 -59
  288. package/lib/docs/runRequest.test.mjs +190 -0
  289. package/lib/docs/saveAnnotatedScreenshot.js +1 -1
  290. package/lib/docs/scaffoldPage.js +3 -1
  291. package/lib/docs/screenshotPage.js +163 -11
  292. package/lib/docs/screenshotPage.test.mjs +263 -1
  293. package/lib/docs/screenshotPageClip.test.mjs +4 -0
  294. package/lib/docs/searchDocs.js +82 -30
  295. package/lib/docs/searchDocs.test.mjs +100 -0
  296. package/lib/docs/searchIcons.js +90 -0
  297. package/lib/docs/searchIcons.test.mjs +127 -0
  298. package/lib/docs/selectExercised.js +39 -0
  299. package/lib/docs/selectFinalState.js +66 -0
  300. package/lib/docs/selectFinalState.test.mjs +51 -0
  301. package/lib/docs/serverErrorStore.js +54 -0
  302. package/lib/docs/serverErrorStore.test.mjs +50 -0
  303. package/lib/docs/setupTestFixtures.mjs +133 -39
  304. package/lib/docs/shownTextSelector.js +28 -0
  305. package/lib/docs/snapshotState.js +13 -5
  306. package/lib/docs/splitSearchTerms.js +62 -0
  307. package/lib/docs/splitSearchTerms.test.mjs +50 -0
  308. package/lib/docs/stateCheckpoints.test.mjs +77 -0
  309. package/lib/docs/tabAvailable.js +6 -5
  310. package/lib/docs/tabChannel.js +127 -21
  311. package/lib/docs/tabChannel.test.mjs +187 -29
  312. package/lib/docs/unsettledPageNote.js +24 -0
  313. package/lib/docs/validateJourneyMail.js +36 -0
  314. package/lib/docs/validateJourneyTimeout.js +38 -0
  315. package/lib/docs/validateJourneyTimeout.test.mjs +29 -0
  316. package/lib/docs/validateRecordingParam.js +42 -0
  317. package/lib/docs/validateStateSelection.js +33 -0
  318. package/lib/docs/validateStateSelection.test.mjs +33 -0
  319. package/lib/docs/validateViewport.js +51 -0
  320. package/lib/docs/validateViewport.test.mjs +54 -0
  321. package/lib/docs/waitForBuild.js +50 -0
  322. package/lib/docs/waitForBuild.test.mjs +75 -0
  323. package/lib/docs/waitForJourneyEmail.js +77 -0
  324. package/lib/docs/withBrowserSlot.js +45 -0
  325. package/lib/docs/withBrowserSlot.test.mjs +75 -0
  326. package/lib/docs/withDataSession.js +53 -0
  327. package/lib/server/applyDataSetRedirect.js +83 -0
  328. package/lib/server/applyDataSetRedirect.test.mjs +340 -0
  329. package/lib/server/applyDataSetRedirectTenantReach.test.mjs +204 -0
  330. package/lib/server/applyDataSetRedirectWalled.test.mjs +131 -0
  331. package/lib/server/auth/createSystemContext.js +73 -0
  332. package/lib/server/auth/{getAuthConfig.js → getAuth.js} +15 -9
  333. package/lib/server/auth/getHeadlessUser.js +9 -8
  334. package/lib/server/auth/getHeadlessUser.test.mjs +41 -0
  335. package/lib/server/auth/getMockUser.js +18 -14
  336. package/lib/server/auth/{session.js → getStrategies.js} +17 -16
  337. package/lib/server/auth/headlessUser.js +3 -2
  338. package/lib/server/auth/journeyActor.js +35 -0
  339. package/lib/server/auth/resolveHeadlessUser.test.mjs +0 -4
  340. package/lib/server/checkPageRecord.js +44 -0
  341. package/lib/server/contextMapBudget.js +24 -0
  342. package/lib/server/contextMapBudget.test.mjs +98 -0
  343. package/lib/server/continueJitKeys.js +28 -0
  344. package/lib/server/continueJitKeys.test.mjs +43 -0
  345. package/lib/server/createDataSessionEndedError.js +33 -0
  346. package/lib/server/createDataSetRefusal.js +26 -0
  347. package/lib/server/createLowdefyContext.js +168 -41
  348. package/lib/server/createLowdefyContext.test.mjs +328 -8
  349. package/lib/server/getClientAddress.js +51 -0
  350. package/lib/server/getClientAddress.test.mjs +56 -0
  351. package/lib/server/getPageJitEnrichment.test.mjs +29 -11
  352. package/lib/server/hashConfigContent.js +26 -0
  353. package/lib/server/importAppCode.test.mjs +57 -0
  354. package/lib/server/isRoutineStep.js +27 -0
  355. package/lib/server/jitPageBuilder.js +309 -126
  356. package/lib/server/jitPageBuilder.skipPluginChecks.test.mjs +78 -0
  357. package/lib/server/jitPageBuilder.test.mjs +218 -0
  358. package/lib/server/journeyCookies/JOURNEY_COOKIES.js +37 -0
  359. package/lib/server/journeyCookies/forwardJourneyCookies.js +46 -0
  360. package/lib/server/journeyCookies/readJourneyCookie.js +52 -0
  361. package/lib/server/journeyCookies/writeJourneyCookie.js +46 -0
  362. package/lib/server/journeyCookies.js +22 -0
  363. package/lib/server/journeyCookies.test.mjs +145 -0
  364. package/lib/server/loadDynamicJsMap.js +47 -0
  365. package/lib/server/log/createHandleError.js +38 -2
  366. package/lib/server/log/createHandleError.test.mjs +125 -2
  367. package/lib/server/log/createLogger.js +3 -0
  368. package/lib/server/log/createLogger.test.mjs +15 -0
  369. package/lib/server/log/logRequest.js +2 -3
  370. package/lib/server/mutantDataSetWrappers.test.mjs +112 -0
  371. package/lib/server/mutants/applyMutant.js +39 -0
  372. package/lib/server/mutants/artifactPatterns.js +25 -0
  373. package/lib/server/mutants/buildAnchor.js +45 -0
  374. package/lib/server/mutants/collectSharedIds.js +41 -0
  375. package/lib/server/mutants/configPathSegments.js +38 -0
  376. package/lib/server/mutants/createMutantReadConfigFile.js +42 -0
  377. package/lib/server/mutants/describeEvent.js +22 -0
  378. package/lib/server/mutants/enumerateArtifact.js +56 -0
  379. package/lib/server/mutants/findNodeByKey.js +45 -0
  380. package/lib/server/mutants/findNodeByPointer.js +49 -0
  381. package/lib/server/mutants/getArtifactKind.js +32 -0
  382. package/lib/server/mutants/groupMutantCopies.js +56 -0
  383. package/lib/server/mutants/hashArtifact.js +25 -0
  384. package/lib/server/mutants/hashMutantTarget.js +34 -0
  385. package/lib/server/mutants/listMutants.js +122 -0
  386. package/lib/server/mutants/listMutants.test.mjs +502 -0
  387. package/lib/server/mutants/mutantId.js +30 -0
  388. package/lib/server/mutants/mutantRuns.js +57 -0
  389. package/lib/server/mutants/mutantRuns.test.mjs +136 -0
  390. package/lib/server/mutants/operators/dropAction.js +47 -0
  391. package/lib/server/mutants/operators/dropBlock.js +44 -0
  392. package/lib/server/mutants/operators/dropPayload.js +52 -0
  393. package/lib/server/mutants/operators/dropStep.js +44 -0
  394. package/lib/server/mutants/operators/flipVisible.js +53 -0
  395. package/lib/server/mutants/operators/index.js +37 -0
  396. package/lib/server/mutants/operators/retargetLink.js +72 -0
  397. package/lib/server/mutants/operators/skipValidate.js +77 -0
  398. package/lib/server/mutants/operators/swapIf.js +112 -0
  399. package/lib/server/mutants/operators.test.mjs +396 -0
  400. package/lib/server/mutants/positionFreePath.js +38 -0
  401. package/lib/server/mutants/removeFromParent.js +28 -0
  402. package/lib/server/mutants/test/fixtures.mjs +173 -0
  403. package/lib/server/mutants/test/keyArtifact.mjs +63 -0
  404. package/lib/server/mutants/validateMutantParam.js +69 -0
  405. package/lib/server/mutants/validateMutantsBody.js +88 -0
  406. package/lib/server/mutants/walkArtifact.js +179 -0
  407. package/lib/server/pageBuildRecords.js +131 -0
  408. package/lib/server/pageBuildRecords.test.mjs +258 -0
  409. package/lib/server/pageCache.mjs +22 -7
  410. package/lib/server/pageCache.test.mjs +22 -7
  411. package/lib/server/pageRebuilds.test.mjs +543 -0
  412. package/lib/server/pruneJitMaps.js +36 -0
  413. package/lib/server/pruneJitMaps.test.mjs +55 -0
  414. package/lib/server/readInstanceRecord.js +33 -0
  415. package/lib/server/readInstanceRecord.test.mjs +99 -0
  416. package/lib/server/readMergedMaps.js +86 -0
  417. package/lib/server/readMergedMaps.test.mjs +79 -0
  418. package/lib/server/recording/appendRecords.js +33 -0
  419. package/lib/server/recording/errorRecordingStamp.js +34 -0
  420. package/lib/server/recording/errorRecordingStamp.test.mjs +31 -0
  421. package/lib/server/recording/isRecordingEnabled.js +31 -0
  422. package/lib/server/recording/readRecordingCookie.js +71 -0
  423. package/lib/server/recording/readRecordingCookie.test.mjs +73 -0
  424. package/lib/server/recording/recordingCookiePayload.js +33 -0
  425. package/lib/server/recording/servedBuilds.js +46 -0
  426. package/lib/server/recording/servedBuilds.test.mjs +42 -0
  427. package/lib/server/sinkExternalConnections.js +95 -0
  428. package/lib/server/sinkExternalConnections.test.mjs +135 -0
  429. package/lib/server/skipStaleMapWrites.js +62 -0
  430. package/lib/server/skipStaleMapWrites.test.mjs +113 -0
  431. package/lib/server/startIdleGc.js +67 -0
  432. package/lib/server/startIdleGc.test.mjs +138 -0
  433. package/lib/server/testFixtures/importAppCodeTwice.mjs +70 -0
  434. package/lib/server/trackSessionWork.js +30 -0
  435. package/lib/vite/createDevServerExclude.js +40 -0
  436. package/lib/vite/createDevServerExclude.test.mjs +79 -0
  437. package/manager/getContext.mjs +24 -2
  438. package/manager/processes/initialBuild.mjs +14 -0
  439. package/manager/processes/initialBuild.test.mjs +101 -0
  440. package/manager/processes/lowdefyBuild.mjs +30 -7
  441. package/manager/processes/lowdefyBuild.skipPluginChecks.test.mjs +62 -0
  442. package/manager/processes/lowdefyBuild.test.mjs +136 -1
  443. package/manager/processes/optimizeDependencies.mjs +59 -0
  444. package/manager/processes/restartServer.mjs +34 -6
  445. package/manager/processes/restartServer.test.mjs +162 -0
  446. package/manager/processes/shutdownServer.mjs +11 -0
  447. package/manager/processes/shutdownServer.test.mjs +45 -0
  448. package/manager/processes/startFirstServer.mjs +46 -0
  449. package/manager/processes/startFirstServer.test.mjs +326 -0
  450. package/manager/processes/startMailSink.mjs +135 -0
  451. package/manager/processes/startMailSink.test.mjs +146 -0
  452. package/manager/processes/startProxy.mjs +129 -12
  453. package/manager/processes/startProxy.test.mjs +366 -0
  454. package/manager/processes/startServer.mjs +47 -6
  455. package/manager/processes/startServer.test.mjs +155 -0
  456. package/manager/processes/startWatchers.mjs +6 -4
  457. package/manager/processes/syncServer.mjs +69 -0
  458. package/manager/processes/syncServer.test.mjs +141 -0
  459. package/manager/processes/waitForDevServer.mjs +48 -0
  460. package/manager/processes/waitForDevServer.test.mjs +112 -0
  461. package/manager/run.mjs +128 -43
  462. package/manager/utils/BatchChanges.mjs +21 -5
  463. package/manager/utils/BatchChanges.test.mjs +77 -0
  464. package/manager/utils/acquireDevInstance.mjs +99 -0
  465. package/manager/utils/acquireDevInstance.test.mjs +160 -0
  466. package/manager/utils/createBuildActivity.mjs +67 -0
  467. package/manager/utils/createBuildActivity.test.mjs +58 -0
  468. package/manager/utils/createChangeSignal.mjs +39 -0
  469. package/manager/utils/createChangeSignal.test.mjs +62 -0
  470. package/manager/utils/createCustomPluginMessagesMap.mjs +23 -33
  471. package/manager/utils/createCustomPluginTypesMap.mjs +24 -30
  472. package/manager/utils/createCustomPluginTypesMap.test.mjs +94 -0
  473. package/manager/utils/createDotPathIgnore.mjs +44 -0
  474. package/manager/utils/createDotPathIgnore.test.mjs +65 -0
  475. package/manager/utils/createIdleGc.mjs +51 -0
  476. package/manager/utils/createIdleGc.test.mjs +77 -0
  477. package/manager/utils/createRequestActivity.mjs +100 -0
  478. package/manager/utils/createRequestActivity.test.mjs +121 -0
  479. package/manager/utils/createServerArtifactTracker.mjs +83 -0
  480. package/manager/utils/createServerArtifactTracker.test.mjs +78 -0
  481. package/manager/utils/createServerEnv.mjs +39 -0
  482. package/manager/utils/filesHaveSameBytes.mjs +41 -0
  483. package/manager/utils/findBuildFilesOutsideWatch.mjs +59 -0
  484. package/manager/utils/findBuildFilesOutsideWatch.test.mjs +111 -0
  485. package/manager/utils/importFresh.mjs +46 -0
  486. package/manager/utils/importFresh.test.mjs +72 -0
  487. package/manager/utils/importFreshWorker.mjs +20 -0
  488. package/manager/utils/killTaggedBrowser.mjs +35 -0
  489. package/manager/utils/pagePathChanged.mjs +56 -0
  490. package/manager/utils/pruneRecordings.mjs +85 -0
  491. package/manager/utils/pruneRecordings.test.mjs +117 -0
  492. package/manager/utils/publishBuildDirectory.mjs +121 -0
  493. package/manager/utils/publishBuildDirectory.order.test.mjs +74 -0
  494. package/manager/utils/publishBuildDirectory.test.mjs +262 -0
  495. package/manager/utils/readBasePath.mjs +27 -0
  496. package/manager/utils/readBasePath.test.mjs +43 -0
  497. package/manager/utils/readBuildStatusWait.mjs +67 -0
  498. package/manager/utils/readManagerVersion.mjs +30 -0
  499. package/manager/utils/readManagerVersion.test.mjs +35 -0
  500. package/manager/utils/readPluginDefinitions.mjs +36 -0
  501. package/manager/utils/resolveDevAuthUrl.mjs +58 -0
  502. package/manager/utils/resolveDevAuthUrl.test.mjs +107 -0
  503. package/manager/utils/resolvePorts.mjs +49 -0
  504. package/manager/utils/setupWatcher.mjs +19 -11
  505. package/manager/utils/startRecordingPruner.mjs +45 -0
  506. package/manager/utils/startRecordingPruner.test.mjs +66 -0
  507. package/manager/utils/waitForChildExit.mjs +53 -0
  508. package/manager/utils/waitForChildExit.test.mjs +70 -0
  509. package/manager/utils/waitForServer.mjs +44 -0
  510. package/manager/utils/writeBuildStatus.mjs +1 -1
  511. package/manager/watchers/createPluginSourceIgnore.mjs +77 -0
  512. package/manager/watchers/envWatcher.mjs +9 -2
  513. package/manager/watchers/envWatcher.test.mjs +87 -0
  514. package/manager/watchers/listServerSidePackages.mjs +47 -0
  515. package/manager/watchers/listServerSidePackages.test.mjs +38 -0
  516. package/manager/watchers/lowdefyBuildWatcher.mjs +106 -22
  517. package/manager/watchers/lowdefyBuildWatcher.test.mjs +309 -0
  518. package/manager/watchers/pluginSourceWatcher.mjs +79 -0
  519. package/manager/watchers/pluginSourceWatcher.test.mjs +222 -0
  520. package/manager/watchers/restartRequestWatcher.mjs +71 -0
  521. package/manager/watchers/restartRequestWatcher.test.mjs +111 -0
  522. package/manager/watchers/selectLocalPluginPackages.mjs +48 -0
  523. package/manager/watchers/selectLocalPluginPackages.test.mjs +73 -0
  524. package/manager/watchers/serverPackageWatcher.mjs +38 -0
  525. package/package.json +60 -47
  526. package/package.original.json +60 -47
  527. package/src/app.js +131 -20
  528. package/src/html/renderDevPage.js +63 -11
  529. package/src/html/renderDevPage.test.mjs +138 -0
  530. package/src/lib/getRequestPath.js +29 -0
  531. package/src/middleware/createSameOriginGuard.js +37 -0
  532. package/src/middleware/createSameOriginGuard.test.mjs +102 -0
  533. package/src/middleware/errorHandler.js +51 -1
  534. package/src/middleware/errorHandler.test.mjs +173 -3
  535. package/src/middleware/isRebindingSafeHost.js +44 -0
  536. package/src/middleware/isRebindingSafeHost.test.mjs +32 -0
  537. package/src/middleware/localDevToolsOnly.js +38 -0
  538. package/src/middleware/localDevToolsOnly.test.mjs +61 -0
  539. package/src/middleware/noStore.js +27 -0
  540. package/src/middleware/noStore.test.mjs +48 -0
  541. package/src/middleware/requestTimeout.js +38 -0
  542. package/src/middleware/staleFlag.js +76 -0
  543. package/src/middleware/staleFlag.test.mjs +127 -0
  544. package/src/middleware/stampCaller.js +38 -0
  545. package/src/middleware/stampCaller.test.mjs +64 -0
  546. package/src/routes/agent.js +15 -3
  547. package/src/routes/auth.js +29 -20
  548. package/src/routes/auth.test.mjs +30 -56
  549. package/src/routes/clientError.js +13 -13
  550. package/src/routes/cron.js +3 -1
  551. package/src/routes/cronForward.js +3 -1
  552. package/src/routes/detached.js +22 -9
  553. package/src/routes/devInspect.js +17 -8
  554. package/src/routes/devInspect.test.mjs +44 -7
  555. package/src/routes/devRecording.js +113 -0
  556. package/src/routes/devRecording.test.mjs +253 -0
  557. package/src/routes/docs/buildStatus.js +10 -2
  558. package/src/routes/docs/buildStatus.test.mjs +46 -0
  559. package/src/routes/docs/content.js +5 -3
  560. package/src/routes/docs/evalOperator.js +15 -1
  561. package/src/routes/docs/events.js +50 -0
  562. package/src/routes/docs/events.test.mjs +117 -0
  563. package/src/routes/docs/find.js +9 -1
  564. package/src/routes/docs/icons.js +30 -0
  565. package/src/routes/docs/inspectState.js +16 -1
  566. package/src/routes/docs/journey.js +194 -0
  567. package/src/routes/docs/journey.test.mjs +401 -0
  568. package/src/routes/docs/journeyRefusals.test.mjs +71 -0
  569. package/src/routes/docs/loadState.js +4 -2
  570. package/src/routes/docs/mcp.js +34 -3
  571. package/src/routes/docs/mcp.test.mjs +130 -0
  572. package/src/routes/docs/mutants.js +78 -0
  573. package/src/routes/docs/mutants.test.mjs +180 -0
  574. package/src/routes/docs/parsePathParamsParam.js +41 -0
  575. package/src/routes/docs/parsePathParamsParam.test.mjs +42 -0
  576. package/src/routes/docs/parseUserParam.js +27 -21
  577. package/src/routes/docs/parseUserParam.test.mjs +17 -5
  578. package/src/routes/docs/restart.js +35 -0
  579. package/src/routes/docs/restart.test.mjs +64 -0
  580. package/src/routes/docs/runEndpoint.js +54 -0
  581. package/src/routes/docs/runEndpoint.test.mjs +165 -0
  582. package/src/routes/docs/runRequest.js +25 -8
  583. package/src/routes/docs/runRequest.test.mjs +92 -0
  584. package/src/routes/docs/screenshot.js +50 -2
  585. package/src/routes/docs/screenshot.test.mjs +117 -0
  586. package/src/routes/docs/search.js +15 -2
  587. package/src/routes/docs/snapshotState.js +17 -3
  588. package/src/routes/endpoints.js +27 -26
  589. package/src/routes/endpointsTextAnswer.test.mjs +77 -0
  590. package/src/routes/feedback.js +10 -13
  591. package/src/routes/jitPage.js +99 -35
  592. package/src/routes/jitPage.test.mjs +248 -0
  593. package/src/routes/mcp.js +82 -5
  594. package/src/routes/mcp.test.mjs +215 -0
  595. package/src/routes/mcpMemberToken.test.mjs +125 -0
  596. package/src/routes/mcpProtectedResourceMetadata.js +34 -0
  597. package/src/routes/mcpProtectedResourceMetadata.test.mjs +65 -0
  598. package/src/routes/mountDevPageRoutes.js +25 -0
  599. package/src/routes/mountDevPageRoutes.test.mjs +30 -0
  600. package/src/routes/mountOauthDiscovery.js +44 -0
  601. package/src/routes/mountOauthDiscovery.test.mjs +79 -0
  602. package/src/routes/normalizeMcpProtocolVersionHeader.js +46 -0
  603. package/src/routes/normalizeMcpProtocolVersionHeader.test.mjs +62 -0
  604. package/src/routes/reload.js +16 -0
  605. package/src/routes/request.js +6 -4
  606. package/src/routes/request.test.mjs +9 -5
  607. package/src/routes/requestBody.test.mjs +159 -0
  608. package/src/routes/usage.js +6 -2
  609. package/src/routes/user.js +27 -0
  610. package/src/routes/websocket.js +29 -8
  611. package/src/routes/websocket.test.mjs +65 -0
  612. package/src/routes/wellKnownFallback.js +30 -0
  613. package/src/routes/wellKnownFallback.test.mjs +32 -0
  614. package/src/websocket/devWebSocket.js +41 -26
  615. package/src/websocket/devWebSocket.test.mjs +157 -0
  616. package/vite.config.js +53 -23
  617. package/lib/server/auth/getDevSession.js +0 -70
  618. package/lib/server/auth/getDevSession.test.mjs +0 -138
  619. package/manager/processes/warnAuthUrlPortMismatch.mjs +0 -47
  620. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +0 -93
  621. package/manager/watchers/moduleBuildWatcher.mjs +0 -79
  622. package/manager/watchers/serverArtifactWatcher.mjs +0 -120
  623. package/src/routes/apiPage.js +0 -37
@@ -0,0 +1,236 @@
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 path from 'node:path';
18
+ import { type } from '@lowdefy/helpers';
19
+ import { getStepKey, INTERACTION_STEP_KEYS } from '@lowdefy/node-utils';
20
+
21
+ import claimExpectedErrors from './claimExpectedErrors.js';
22
+
23
+ import evaluateInvariants from './evaluateInvariants.js';
24
+ import installStepObserver from './installStepObserver.js';
25
+ import readStepObserver from './readStepObserver.js';
26
+ import resolveConfigKeySource from './resolveConfigKeySource.js';
27
+ import takeJourneyEvents from './takeJourneyEvents.js';
28
+ import { takeRunErrors } from '../runErrorBuffers.js';
29
+ import toAppErrorFailure from './toAppErrorFailure.js';
30
+ import TRANSIENT_SELECTOR from './TRANSIENT_SELECTOR.js';
31
+ import usesSearchStage from './usesSearchStage.js';
32
+ import waitForClientErrorReports from './waitForClientErrorReports.js';
33
+ import watchJourneyContext from './watchJourneyContext.js';
34
+ import withAppErrors from './withAppErrors.js';
35
+
36
+ function isExpectError(step) {
37
+ return getStepKey(step ?? {}) === 'expect' && getStepKey(step.expect) === 'error';
38
+ }
39
+
40
+ // Steps that cannot cause an error a page reports later, so their window
41
+ // closes without waiting for client error reports in flight. An expect.error
42
+ // waits, so a late report of the interaction before it can be claimed.
43
+ function isQuietStep(step) {
44
+ return ['expect', 'wait', 'screenshot'].includes(getStepKey(step)) && !isExpectError(step);
45
+ }
46
+
47
+ // What fails a journey: an app error. A dead click is only a warning; a
48
+ // journey asserts it with expect.effect.
49
+ function isAppError(finding) {
50
+ return finding.severity === 'error' || finding.kind === 'environment';
51
+ }
52
+
53
+ // Watches a journey run for app errors and judges each step's window with the
54
+ // app error invariants (evaluateInvariants), each error keyed by findingKey. Every
55
+ // actor's context is watched from before its first request (onContext). The
56
+ // windows are contiguous, from the page open to the final drain after the
57
+ // last step, so an error that lands between two steps is never lost. Each
58
+ // window takes what the run's actors buffered (page errors, app API
59
+ // responses) and the error entries the run's own error buffer claimed (see
60
+ // runErrorBuffers), and nothing a developer's own tab or another run caused.
61
+ function createJourneyAppErrors({ origin, basePath, recording, configDirectory }) {
62
+ const buildDirectory = path.join(process.cwd(), 'build');
63
+ const run = { run: recording.run.id, journey: recording.run.journey };
64
+ const events = {
65
+ pageErrors: [],
66
+ requests: [],
67
+ responses: [],
68
+ abortedRequests: [],
69
+ pendingClientErrors: new Set(),
70
+ };
71
+ let since = 0;
72
+ let current = null;
73
+ // The window the last step closed, which an expect.effect step reads for
74
+ // the interaction just before it.
75
+ let closed = null;
76
+
77
+ function onContext({ context }) {
78
+ watchJourneyContext({ context, events, origin, basePath });
79
+ }
80
+
81
+ function takeWindow({ page, observed, urlBefore }) {
82
+ const until = Date.now();
83
+ const window = {
84
+ since,
85
+ until,
86
+ urlBefore,
87
+ urlAfter: page.isClosed() ? null : page.url(),
88
+ emits: observed?.emits ?? [],
89
+ mutationCount: observed?.mutationCount ?? null,
90
+ ...takeJourneyEvents({ events, since, until }),
91
+ errors: takeRunErrors({ ...run, since, until }),
92
+ };
93
+ since = until;
94
+ return window;
95
+ }
96
+
97
+ async function findAppErrors({ step, result, window, pageId }) {
98
+ const findings = await evaluateInvariants({
99
+ step,
100
+ result,
101
+ window,
102
+ pageId,
103
+ basePath,
104
+ configDirectory,
105
+ usesSearchStage: (entry) => usesSearchStage({ buildDirectory, entry }),
106
+ resolveSource: (configKey) =>
107
+ resolveConfigKeySource({ buildDirectory, configDirectory, configKey }),
108
+ });
109
+ return findings.filter(isAppError);
110
+ }
111
+
112
+ // A page mid-navigation, or one that has crashed, has nothing to report.
113
+ async function readPageId(page) {
114
+ return page.evaluate(() => window.lowdefy?.pageId ?? null).catch(() => null);
115
+ }
116
+
117
+ // Judges what opening the journey's first page caused (its onInit and
118
+ // onMount requests included). Returns a failure with phase 'open', or
119
+ // undefined.
120
+ async function judgeOpen({ page }) {
121
+ await waitForClientErrorReports({ events });
122
+ const pageId = await readPageId(page);
123
+ const window = takeWindow({ page, observed: null, urlBefore: page.url() });
124
+ const findings = await findAppErrors({ step: {}, result: { status: 'ok' }, window, pageId });
125
+ if (findings.length === 0) {
126
+ return undefined;
127
+ }
128
+ return toAppErrorFailure({ findings, phase: 'open' });
129
+ }
130
+
131
+ // Opens a step's window on the page the step starts on. The observer is
132
+ // installed in each new document; a page mid-navigation cannot take it, and
133
+ // its window then has no trace emits (its error reports still count).
134
+ async function openWindow({ page }) {
135
+ await page.evaluate(installStepObserver).catch(() => null);
136
+ current = { page, urlBefore: page.url(), pageId: await readPageId(page) };
137
+ }
138
+
139
+ // Closes the step's window and returns the app errors it held, as findings
140
+ // ({ kind, severity, message, pageId, source, configKey, key }).
141
+ async function closeWindow({ journey, step, result }) {
142
+ if (!isQuietStep(step)) {
143
+ await waitForClientErrorReports({ events });
144
+ }
145
+ const { page, urlBefore, pageId } = current;
146
+ current = null;
147
+ const observed = page.isClosed()
148
+ ? null
149
+ : await page
150
+ .evaluate(readStepObserver, { transientSelector: TRANSIENT_SELECTOR })
151
+ .catch(() => null);
152
+ const window = takeWindow({ page: journey.actors.current().page, observed, urlBefore });
153
+ closed = window;
154
+ return findAppErrors({ step, result, window, pageId });
155
+ }
156
+
157
+ // The window of the step before the one running now: an expect step opens
158
+ // its own window, so the last one closed is the interaction's before it.
159
+ function previousWindow() {
160
+ return closed;
161
+ }
162
+
163
+ // After the last step: what its effects reported late (a client error
164
+ // report still in flight when its window closed) is judged against it.
165
+ async function drain({ journey, step, result }) {
166
+ await waitForClientErrorReports({ events });
167
+ const page = journey.actors.current().page;
168
+ const pageId = await readPageId(page);
169
+ const window = takeWindow({ page, observed: null, urlBefore: page.url() });
170
+ return findAppErrors({ step, result, window, pageId });
171
+ }
172
+
173
+ // The app errors of an interaction an expect.error step follows, held for
174
+ // that step to claim instead of failing the interaction at once.
175
+ let held = null;
176
+
177
+ // Closes the step's window and returns the journey's failure once its app
178
+ // errors are counted (see withAppErrors), or undefined while it passes.
179
+ // `failure` is the step's own failure, if any. An interaction followed by
180
+ // an expect.error step holds its app errors for that step to claim (see
181
+ // claimExpectedErrors); it fails on them only if they are not claimed.
182
+ async function judgeStep({ journey, steps, index, results, failure, leftOrigin }) {
183
+ const step = steps[index];
184
+ const result = results[index];
185
+ const findings = await closeWindow({ journey, step, result });
186
+ if (isExpectError(step) && type.isUndefined(failure)) {
187
+ const claim = claimExpectedErrors({ step, index, held, findings });
188
+ held = null;
189
+ if (!type.isUndefined(claim)) {
190
+ results[claim.index].status = 'failed';
191
+ }
192
+ return claim;
193
+ }
194
+ if (
195
+ type.isUndefined(failure) &&
196
+ INTERACTION_STEP_KEYS.includes(getStepKey(step)) &&
197
+ isExpectError(steps[index + 1])
198
+ ) {
199
+ held = { index, step, findings };
200
+ return undefined;
201
+ }
202
+ if (findings.length === 0) {
203
+ return failure;
204
+ }
205
+ result.status = 'failed';
206
+ return withAppErrors({ failure, leftOrigin, findings, index, step });
207
+ }
208
+
209
+ // After the last step passed, what its effects reported late (a client
210
+ // error report still in flight when its window closed) is judged against
211
+ // it.
212
+ async function judgeDrain({ journey, steps, results }) {
213
+ const index = steps.length - 1;
214
+ const step = steps[index];
215
+ const result = results[index];
216
+ const findings = await drain({ journey, step, result });
217
+ if (findings.length === 0) {
218
+ return undefined;
219
+ }
220
+ result.status = 'failed';
221
+ return withAppErrors({ findings, index, step });
222
+ }
223
+
224
+ return {
225
+ onContext,
226
+ judgeOpen,
227
+ openWindow,
228
+ judgeStep,
229
+ judgeDrain,
230
+ closeWindow,
231
+ drain,
232
+ previousWindow,
233
+ };
234
+ }
235
+
236
+ export default createJourneyAppErrors;
@@ -0,0 +1,385 @@
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
+ jest.unstable_mockModule('./waitForClientErrorReports.js', () => ({ default: async () => {} }));
20
+
21
+ const { default: createJourneyAppErrors } = await import('./createJourneyAppErrors.js');
22
+ const { default: installStepObserver } = await import('./installStepObserver.js');
23
+ const { default: readStepObserver } = await import('./readStepObserver.js');
24
+ const { recordRunError, registerRunBuffer, releaseRunBuffer } = await import(
25
+ '../runErrorBuffers.js'
26
+ );
27
+
28
+ const origin = 'http://localhost:3227';
29
+ const run = { run: '20261005T120000Z-ae0001', journey: null };
30
+ const recording = { source: 'journey', run: { id: run.run, by: 'test', journey: null } };
31
+
32
+ let emits;
33
+
34
+ function createPage() {
35
+ return {
36
+ evaluate: jest.fn(async (fn) => {
37
+ if (fn === installStepObserver) return true;
38
+ if (fn === readStepObserver) return { emits, mutationCount: 1 };
39
+ return 'orders';
40
+ }),
41
+ url: () => `${origin}/orders`,
42
+ isClosed: () => false,
43
+ };
44
+ }
45
+
46
+ // A fake browser context: the watch's listeners, called as Playwright would.
47
+ function createContext() {
48
+ const listeners = {};
49
+ return {
50
+ on: (event, callback) => {
51
+ listeners[event] = callback;
52
+ },
53
+ abort({ url }) {
54
+ listeners.requestfailed({
55
+ url: () => url,
56
+ failure: () => ({ errorText: 'net::ERR_ABORTED' }),
57
+ });
58
+ },
59
+ respond({ url, status }) {
60
+ listeners.response({
61
+ url: () => url,
62
+ status: () => status,
63
+ request: () => ({ method: () => 'POST' }),
64
+ });
65
+ },
66
+ };
67
+ }
68
+
69
+ function serverEntry({ message, recordingRun = run.run }) {
70
+ return {
71
+ timestamp: new Date().toISOString(),
72
+ message,
73
+ source: '/app/pages/orders.yaml:12',
74
+ requestId: 'broken',
75
+ pageId: 'orders',
76
+ store: 'server',
77
+ recording: { source: 'journey', run: recordingRun, journey: null },
78
+ };
79
+ }
80
+
81
+ let page;
82
+ let journey;
83
+ let context;
84
+ let appErrors;
85
+
86
+ beforeEach(() => {
87
+ emits = [];
88
+ registerRunBuffer(run);
89
+ appErrors = createJourneyAppErrors({ origin, basePath: '', recording, configDirectory: '/app' });
90
+ context = createContext();
91
+ appErrors.onContext({ context });
92
+ page = createPage();
93
+ journey = { actors: { current: () => ({ page }) } };
94
+ });
95
+
96
+ afterEach(() => {
97
+ releaseRunBuffer(run);
98
+ });
99
+
100
+ async function runStepWindow({ step, during = () => {} }) {
101
+ await appErrors.openWindow({ page });
102
+ during();
103
+ return appErrors.closeWindow({ journey, step, result: { status: 'ok' } });
104
+ }
105
+
106
+ test('a server error entry the run claimed fails the step, with its kind, source and key', async () => {
107
+ const findings = await runStepWindow({
108
+ step: { click: 'broken' },
109
+ during: () => recordRunError(serverEntry({ message: 'Unrecognized pipeline stage' })),
110
+ });
111
+ expect(findings).toEqual([
112
+ expect.objectContaining({
113
+ kind: 'server-error',
114
+ severity: 'error',
115
+ message: 'Unrecognized pipeline stage',
116
+ source: 'pages/orders.yaml:12',
117
+ key: 'server-error|orders|pages/orders.yaml:12',
118
+ }),
119
+ ]);
120
+ });
121
+
122
+ test("an error entry of another run is not this journey's", async () => {
123
+ const other = { run: '20261005T120000Z-ae0002', journey: null };
124
+ registerRunBuffer(other);
125
+ const findings = await runStepWindow({
126
+ step: { click: 'broken' },
127
+ during: () => recordRunError(serverEntry({ message: 'elsewhere', recordingRun: other.run })),
128
+ });
129
+ releaseRunBuffer(other);
130
+ expect(findings).toEqual([]);
131
+ });
132
+
133
+ test('an unexplained 5xx from an app API route fails the step as request-failed', async () => {
134
+ const findings = await runStepWindow({
135
+ step: { click: 'save' },
136
+ during: () => context.respond({ url: `${origin}/api/request/orders/save`, status: 500 }),
137
+ });
138
+ expect(findings.map((finding) => finding.kind)).toEqual(['request-failed']);
139
+ });
140
+
141
+ test.each([
142
+ ['AuthorizationError', 403],
143
+ ['AuthenticationError', 401],
144
+ ['TwoFactorEnrolmentRequiredError', 403],
145
+ ])(
146
+ 'a CallAPI an auth gate refused (%s, %i) is not an app error: the refusal and its emit both pass',
147
+ async (errorName, status) => {
148
+ emits = [
149
+ {
150
+ blockId: 'admin_button',
151
+ eventName: 'onClick',
152
+ success: false,
153
+ failure: { errorName, actionId: 'call_admin', actionType: 'CallAPI', configKey: 'k1' },
154
+ },
155
+ ];
156
+ const findings = await runStepWindow({
157
+ step: { click: 'admin_button' },
158
+ during: () => context.respond({ url: `${origin}/api/endpoints/admin_only`, status }),
159
+ });
160
+ expect(findings).toEqual([]);
161
+ }
162
+ );
163
+
164
+ test('a UserError and a dead click are not app errors', async () => {
165
+ emits = [
166
+ {
167
+ blockId: 'throw_button',
168
+ eventName: 'onClick',
169
+ success: false,
170
+ failure: { errorName: 'UserError', actionId: 'throw', actionType: 'Throw' },
171
+ },
172
+ ];
173
+ const thrown = await runStepWindow({ step: { click: 'throw_button' } });
174
+ expect(thrown).toEqual([]);
175
+ emits = [];
176
+ page.evaluate.mockImplementation(async (fn) => {
177
+ if (fn === readStepObserver) return { emits: [], mutationCount: 0 };
178
+ return fn === installStepObserver ? true : 'orders';
179
+ });
180
+ const dead = await runStepWindow({ step: { click: 'dead_button' } });
181
+ expect(dead).toEqual([]);
182
+ });
183
+
184
+ test('an action that fails with an error that is not a UserError fails the step as action-error', async () => {
185
+ emits = [
186
+ {
187
+ blockId: 'explode_button',
188
+ eventName: 'onClick',
189
+ success: false,
190
+ failure: { errorName: 'RequestError', actionId: 'call_explode', actionType: 'CallAPI' },
191
+ },
192
+ ];
193
+ const findings = await runStepWindow({ step: { click: 'explode_button' } });
194
+ expect(findings.map((finding) => finding.kind)).toEqual(['action-error']);
195
+ });
196
+
197
+ test('an error that lands between two steps is judged in the next window, not lost', async () => {
198
+ expect(await runStepWindow({ step: { click: 'a' } })).toEqual([]);
199
+ recordRunError(serverEntry({ message: 'between steps' }));
200
+ const findings = await runStepWindow({ step: { expect: { visible: 'a' } } });
201
+ expect(findings.map((finding) => finding.message)).toEqual(['between steps']);
202
+ });
203
+
204
+ test('judgeOpen fails with phase open on an error raised while the page opened', async () => {
205
+ recordRunError(serverEntry({ message: 'onInit failed' }));
206
+ const failure = await appErrors.judgeOpen({ page });
207
+ expect(failure).toEqual({
208
+ phase: 'open',
209
+ kind: 'app-error',
210
+ message: 'Opening the page caused an app error: server-error: onInit failed',
211
+ expected: 'no app error',
212
+ actual: ['server-error: onInit failed'],
213
+ errors: [
214
+ {
215
+ kind: 'server-error',
216
+ message: 'onInit failed',
217
+ source: 'pages/orders.yaml:12',
218
+ configKey: null,
219
+ key: 'server-error|orders|pages/orders.yaml:12',
220
+ },
221
+ ],
222
+ });
223
+ expect(await appErrors.judgeOpen({ page })).toBeUndefined();
224
+ });
225
+
226
+ test('drain judges what arrived after the last window closed', async () => {
227
+ expect(await runStepWindow({ step: { click: 'a' } })).toEqual([]);
228
+ recordRunError(serverEntry({ message: 'late' }));
229
+ const findings = await appErrors.drain({
230
+ journey,
231
+ step: { click: 'a' },
232
+ result: { status: 'ok' },
233
+ });
234
+ expect(findings.map((finding) => finding.message)).toEqual(['late']);
235
+ });
236
+
237
+ // Runs steps through judgeStep as runJourneySteps does, with `during(index)`
238
+ // raising what each step causes.
239
+ async function judgeSteps({ steps, during = () => {} }) {
240
+ const results = [];
241
+ let failure;
242
+ for (let index = 0; index < steps.length; index += 1) {
243
+ if (failure !== undefined) {
244
+ results.push({ index, status: 'skipped' });
245
+ continue;
246
+ }
247
+ await appErrors.openWindow({ page });
248
+ during(index);
249
+ results.push({ index, status: 'ok' });
250
+ failure = await appErrors.judgeStep({
251
+ journey,
252
+ steps,
253
+ index,
254
+ results,
255
+ failure,
256
+ leftOrigin: false,
257
+ });
258
+ }
259
+ return { results, failure };
260
+ }
261
+
262
+ const duplicateSteps = [
263
+ { click: 'save' },
264
+ { expect: { error: 'duplicate key' } },
265
+ { expect: { visible: 'already_exists' } },
266
+ ];
267
+
268
+ test('expect.error claims the matching app error of the click before it, and the journey passes', async () => {
269
+ const { results, failure } = await judgeSteps({
270
+ steps: duplicateSteps,
271
+ during: (index) => {
272
+ if (index === 0) recordRunError(serverEntry({ message: 'E11000 duplicate key error' }));
273
+ },
274
+ });
275
+ expect(failure).toBeUndefined();
276
+ expect(results.map((result) => result.status)).toEqual(['ok', 'ok', 'ok']);
277
+ });
278
+
279
+ test('expect.error fails at the expect step when no app error matches', async () => {
280
+ const { results, failure } = await judgeSteps({ steps: duplicateSteps });
281
+ expect(failure).toEqual({
282
+ index: 1,
283
+ step: { expect: { error: 'duplicate key' } },
284
+ expected: 'an app error containing "duplicate key"',
285
+ actual: 'no app error',
286
+ message: 'Expected an app error containing "duplicate key" but found no app error.',
287
+ });
288
+ expect(results.map((result) => result.status)).toEqual(['ok', 'failed', 'skipped']);
289
+ });
290
+
291
+ test('an app error expect.error does not claim still fails the click', async () => {
292
+ const { results, failure } = await judgeSteps({
293
+ steps: duplicateSteps,
294
+ during: (index) => {
295
+ if (index !== 0) return;
296
+ recordRunError(serverEntry({ message: 'E11000 duplicate key error' }));
297
+ recordRunError({
298
+ ...serverEntry({ message: 'Connection lost' }),
299
+ source: '/app/pages/orders.yaml:30',
300
+ });
301
+ },
302
+ });
303
+ expect(failure).toEqual(
304
+ expect.objectContaining({
305
+ index: 0,
306
+ step: { click: 'save' },
307
+ kind: 'app-error',
308
+ actual: ['server-error: Connection lost'],
309
+ })
310
+ );
311
+ expect(results.map((result) => result.status)).toEqual(['failed', 'ok', 'skipped']);
312
+ });
313
+
314
+ test('an app error with no expect.error after it fails its step', async () => {
315
+ const { results, failure } = await judgeSteps({
316
+ steps: [{ click: 'save' }, { expect: { visible: 'saved' } }],
317
+ during: (index) => {
318
+ if (index === 0) recordRunError(serverEntry({ message: 'E11000 duplicate key error' }));
319
+ },
320
+ });
321
+ expect(failure).toEqual(expect.objectContaining({ index: 0, kind: 'app-error' }));
322
+ expect(results.map((result) => result.status)).toEqual(['failed', 'skipped']);
323
+ });
324
+
325
+ test('expect.error claims the failed action that reported the error it matched', async () => {
326
+ const failedInsert = {
327
+ blockId: 'save',
328
+ eventName: 'onClick',
329
+ success: false,
330
+ failure: { errorName: 'ServiceError', actionId: 'insert', actionType: 'Request' },
331
+ };
332
+ const { failure } = await judgeSteps({
333
+ steps: duplicateSteps,
334
+ during: (index) => {
335
+ emits = index === 0 ? [failedInsert] : [];
336
+ if (index === 0) recordRunError(serverEntry({ message: 'E11000 duplicate key error' }));
337
+ },
338
+ });
339
+ expect(failure).toBeUndefined();
340
+ });
341
+
342
+ test('previousWindow is the window the last step closed, for expect.effect to read', async () => {
343
+ expect(appErrors.previousWindow()).toBeNull();
344
+ emits = [{ blockId: 'save', eventName: 'onClick', success: true }];
345
+ await runStepWindow({ step: { click: 'save' } });
346
+ expect(appErrors.previousWindow()).toEqual(
347
+ expect.objectContaining({
348
+ emits,
349
+ mutationCount: 1,
350
+ requests: [],
351
+ urlBefore: `${origin}/orders`,
352
+ urlAfter: `${origin}/orders`,
353
+ })
354
+ );
355
+ });
356
+
357
+ function clientEntry({ message }) {
358
+ return {
359
+ timestamp: new Date().toISOString(),
360
+ name: 'TypeError',
361
+ message,
362
+ source: null,
363
+ store: 'client',
364
+ recording: { source: 'journey', run: run.run, journey: null },
365
+ };
366
+ }
367
+
368
+ test("a goto whose navigation aborted the old page's requests is not failed by their fetch errors", async () => {
369
+ const findings = await runStepWindow({
370
+ step: { goto: 'onboarding-step-connect' },
371
+ during: () => {
372
+ context.abort({ url: `${origin}/api/request/step/get_header` });
373
+ recordRunError(clientEntry({ message: 'Failed to fetch at onboarding-step-2.' }));
374
+ },
375
+ });
376
+ expect(findings).toEqual([]);
377
+ });
378
+
379
+ test('a failed fetch with no aborted request still fails a goto step', async () => {
380
+ const findings = await runStepWindow({
381
+ step: { goto: 'onboarding-step-connect' },
382
+ during: () => recordRunError(clientEntry({ message: 'Failed to fetch at onboarding-step-2.' })),
383
+ });
384
+ expect(findings.map((finding) => finding.kind)).toEqual(['client-error']);
385
+ });